Case study: migliore debug Angular con DevTools

Un'esperienza di debug migliorata

Negli ultimi mesi il team Chrome DevTools ha collaborato con il team Angular per introdurre miglioramenti all'esperienza di debug in Chrome DevTools. I membri di entrambi i team hanno collaborato e hanno intrapreso delle azioni per consentire agli sviluppatori di eseguire il debug e la profilazione delle applicazioni web dal punto di vista dell'autore, in termini di lingua di origine e struttura del progetto, con accesso a informazioni familiari e pertinenti.

Questo post offre un'occhiata dietro le quinte per scoprire quali modifiche in Angular e Chrome DevTools sono state necessarie per ottenere questo risultato. Anche se alcune di queste modifiche vengono dimostrate tramite Angular, possono essere applicate anche ad altri framework. Il team di Chrome DevTools incoraggia altri framework ad adottare le nuove API della console e i punti di estensione delle mappe di origine, in modo che anche loro possano offrire una migliore esperienza di debug agli utenti.

Ignorare il codice inserito in una scheda

Durante il debug di applicazioni utilizzando Chrome DevTools, gli autori in genere vogliono vedere solo solo il loro codice, non quello del framework sottostante o alcune dipendenze nascoste nella cartella node_modules.

Per raggiungere questo obiettivo, il team DevTools ha introdotto un'estensione per le mappe di origine, chiamata x_google_ignoreList. Questa estensione viene utilizzata per identificare origini di terze parti come codice framework o codice generato dal bundler. Quando un framework utilizza questa estensione, gli autori ora evitano automaticamente il codice che non vogliono vedere o che non vogliono vedere senza doverlo configurare manualmente in anticipo.

In pratica, Chrome DevTools può nascondere automaticamente il codice identificato come tale nelle analisi dello stack, nell'albero delle origini e nella finestra di dialogo di apertura rapida, oltre a migliorare il comportamento di avanzamento e ripristino nel debugger.

Un'immagine GIF animata che mostra DevTools prima e dopo. Nota come nell'immagine successiva DevTools mostra il codice creato nell'albero, non suggerisce più alcun file framework nel menu "Apertura rapida" e mostra un'analisi dello stack molto più pulita sulla destra.

L'estensione della mappa di origine x_google_ignoreList

Nelle mappe di origine, il nuovo campo x_google_ignoreList fa riferimento all'array sources ed elenca gli indici di tutte le origini di terze parti note in quella mappa delle origini. Durante l'analisi della mappa di origine, Chrome DevTools lo utilizzerà per capire quali sezioni del codice devono essere inserite nell'elenco di elementi da ignorare.

Di seguito è riportata una mappa di origine per un file generato out.js. Esistono due elementi sources originali che hanno contribuito a generare il file di output: foo.js e lib.js. La prima è un testo scritto da uno sviluppatore di siti web, mentre il secondo è un framework utilizzato dagli sviluppatori.

{
  "version" : 3,
  "file": "out.js",
  "sourceRoot": "",
  "sources": ["foo.js", "lib.js"],
  "sourcesContent": ["...", "..."],
  "names": ["src", "maps", "are", "fun"],
  "mappings": "A,AAAB;;ABCDE;"
}

sourcesContent è incluso per entrambe le origini originali e Chrome DevTools mostra questi file per impostazione predefinita in Debugger:

  • Come file nell'albero delle origini.
  • Come risultati nella finestra di dialogo Apertura rapida.
  • Come posizioni di frame di chiamata mappate nelle analisi dello stack di errori durante la pausa su un punto di interruzione e durante il passo.

Ora c'è un'altra informazione che può essere inclusa nelle mappe di origine per identificare quale di queste origini è codice proprietario o di terze parti:

{
  ...
  "sources": ["foo.js", "lib.js"],
  "x_google_ignoreList": [1],
  ...
}

Il nuovo campo x_google_ignoreList contiene un singolo indice che fa riferimento all'array sources: 1. Questo specifica che le regioni mappate a lib.js sono in realtà codice di terze parti che dovrebbe essere aggiunto automaticamente all'elenco degli elementi da ignorare.

In un esempio più complesso, mostrato di seguito, gli indici 2, 4 e 5 specificano che le regioni mappate a lib1.ts, lib2.coffee e hmr.js sono tutto codice di terze parti che deve essere aggiunto automaticamente all'elenco degli elementi da ignorare.

{
  ...
  "sources": ["foo.html", "bar.css", "lib1.ts", "baz.js", "lib2.coffee", "hmr.js"],
  "x_google_ignoreList": [2, 4, 5],
  ...
}

Se sei uno sviluppatore di framework o di bundler, assicurati che le mappe di origine generate durante il processo di compilazione includano questo campo per sfruttare queste nuove funzionalità in Chrome DevTools.

x_google_ignoreList in Angular

A partire dalla versione Angular v14.1.0, i contenuti delle cartelle node_modules e webpack sono stati contrassegnati come "da ignorare".

Ciò è stato ottenuto attraverso una modifica a angular-cli creando un plug-in che si aggancia al modulo Compiler di webpack

Il plug-in webpack creato dai nostri tecnici si collega allo stage PROCESS_ASSETS_STAGE_DEV_TOOLING e compila il campo x_google_ignoreList nelle mappe di origine per gli asset finali generati da Webpack e caricati dal browser.

const map = JSON.parse(mapContent) as SourceMap;
const ignoreList = [];

for (const [index, path] of map.sources.entries()) {
  if (path.includes('/node_modules/') || path.startsWith('webpack/')) {
    ignoreList.push(index);
  }
}

map[`x_google_ignoreList`] = ignoreList;
compilation.updateAsset(name, new RawSource(JSON.stringify(map)));

Analisi dello stack collegate

Le analisi dello stack rispondono alla domanda "come sono arrivato qui?", ma spesso questo accade dal punto di vista della macchina e non necessariamente corrisponde al punto di vista dello sviluppatore o al suo modello mentale di runtime dell'applicazione. Ciò è particolarmente vero quando alcune operazioni sono pianificate per essere eseguite in modo asincrono più tardi: potrebbe comunque essere interessante conoscere la "causa principale" o il lato della pianificazione di queste operazioni, ma è esattamente un aspetto che non farà parte di un'analisi dello stack asincrona.

V8 ha internamente un meccanismo per tenere traccia di queste attività asincrone quando vengono utilizzate le primitive di pianificazione del browser standard, ad esempio setTimeout. In questi casi questa operazione viene eseguita per impostazione predefinita, quindi gli sviluppatori possono già controllarli. Nei progetti più complessi, però, non è così semplice, soprattutto quando si utilizza un framework con meccanismi di pianificazione più avanzati, ad esempio uno che esegue il monitoraggio delle zone, l'accodamento di attività personalizzate o che suddivide gli aggiornamenti in più unità di lavoro eseguite nel tempo.

Per risolvere questo problema, DevTools espone sull'oggetto console un meccanismo chiamato "API di tagging dello stack asincrono", che consente agli sviluppatori del framework di suggerire sia le località in cui sono pianificate le operazioni sia dove vengono eseguite queste operazioni.

L'API Async Stack Tagging

Senza il tagging asincrono dello stack, le analisi dello stack relative al codice eseguito in modo asincrono in modi complessi dai framework vengono visualizzate senza connessione al codice in cui è stato pianificato.

Un'analisi dello stack di un codice eseguito asincrono senza informazioni su quando è stato pianificato. Mostra solo l'analisi dello stack a partire da "requestAnimationFrame", ma non contiene informazioni relative alla data di pianificazione.

Con il tagging asincrono dello stack è possibile fornire questo contesto e l'analisi dello stack ha il seguente aspetto:

Un'analisi dello stack di un codice eseguito asincrono con informazioni sulla data di pianificazione. Nota come, a differenza di prima, include "businessLogic" e "schedule" nell'analisi dello stack.

A questo scopo, utilizza un nuovo metodo console denominato console.createTask() fornito dall'API Async Stack Tagging. La sua firma è la seguente:

interface Console {
  createTask(name: string): Task;
}

interface Task {
  run<T>(f: () => T): T;
}

La chiamata di console.createTask() restituisce un'istanza Task che puoi utilizzare in un secondo momento per eseguire il codice asincrono.

// Task Creation
const task = console.createTask(name);

// Task Execution
task.run(f);

Anche le operazioni asincrone possono essere nidificate e le "cause principali" verranno visualizzate in sequenza nell'analisi dello stack.

Le attività possono essere eseguite un numero qualsiasi di volte e il payload di lavoro può variare tra un'esecuzione e l'altra. Lo stack di chiamate nel sito di pianificazione verrà memorizzato fino a quando l'oggetto dell'attività non verrà garbage collection.

L'API Async Stack Tagging in Angular

In Angular, sono state apportate modifiche a NgZone, il contesto di esecuzione di Angular che persiste nelle attività asincrone.

Quando pianifica un'attività, utilizza console.createTask() se disponibile. L'istanza Task risultante viene archiviata per essere utilizzata ulteriormente. Dopo aver richiamato l'attività, NgZone utilizzerà l'istanza Task archiviata per eseguirla.

Queste modifiche sono state applicate alla zona NgZone 0.11.8 di Angular tramite le richieste di pull #46693 e #46958.

Inquadrature per le chiamate amichevoli

Durante la creazione di un progetto, i framework spesso generano codice da tutti i tipi di linguaggi di modelli, come i modelli Angular o JSX, che trasformano il codice HTML in un semplice JavaScript che alla fine viene eseguito nel browser. A volte, a questi tipi di funzioni generate vengono dati nomi che non sono molto amichevoli: nomi di una sola lettera dopo essere stati minimizzati o alcuni nomi ambigui o sconosciuti, anche quando non lo sono.

Non è raro vedere frame di chiamata con nomi come AppComponent_Template_app_button_handleClick_1_listener nelle analisi dello stack in Angular.

Screenshot dell&#39;analisi dello stack con il nome di una funzione generato automaticamente.

Per risolvere questo problema, Chrome DevTools ora supporta la ridenominazione di queste funzioni tramite mappe di origine. Se una mappa di origine contiene una voce di nome per l'inizio dell'ambito di una funzione (ovvero la parentesi sinistra dell'elenco dei parametri), il frame di chiamata dovrebbe visualizzare questo nome nell'analisi dello stack.

Inquadrature di chiamata amichevoli in Angular

La ridenominazione dei frame di chiamata in Angular è un'operazione costante. Prevediamo che questi miglioramenti si atterranno gradualmente nel tempo.

Durante l'analisi dei modelli HTML scritti dagli autori, il compilatore Angular genera codice TypeScript, che viene poi trasformato in codice JavaScript caricato ed eseguito dal browser.

Nell'ambito di questo processo di generazione del codice, vengono create anche le mappe di origine. Stiamo attualmente esplorando modi per includere i nomi delle funzioni nel campo "nomi" delle mappe di origine e fare riferimento a questi nomi nelle mappature tra il codice generato e il codice originale.

Ad esempio, se viene generata una funzione per un listener di eventi e il suo nome non è semplice o è stato rimosso durante la minimizzazione, le mappe di origine possono ora includere il nome più semplice per questa funzione nel campo "names" e la mappatura per l'inizio dell'ambito della funzione può ora fare riferimento a questo nome (ovvero la parentesi aperta dell'elenco di parametri). Chrome DevTools utilizzerà questi nomi per rinominare i frame di chiamata nelle analisi dello stack.

Prospettive future

Utilizzare Angular come progetto pilota di prova per verificare il nostro lavoro è stato un'esperienza meravigliosa. Ci piacerebbe conoscere gli sviluppatori di framework e fornire un feedback su questi punti di estensione.

Ci sono altre aree che vorremmo esplorare. In particolare, verrà spiegato come migliorare l'esperienza di profilazione in DevTools.