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 anch'essi possano offrire una migliore esperienza di debug agli utenti.

Codice di ignoramento della 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 di 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 il codice del framework o il 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 tracce dello stack, nella struttura ad albero Origini, nella finestra di dialogo Apri rapidamente e migliorare anche il comportamento del passaggio e della ripresa nel debugger.

Una GIF animata che mostra DevTools prima e dopo. Nota come nell'immagine dopo DevTools mostri il codice dell'autore nell'albero, non suggerisca più i file del framework nel menu "Apri rapidamente" e mostri una traccia dello stack molto più chiara a 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 nella mappa di origine. Durante l'analisi della mappa sorgente, Chrome DevTools la utilizzerà per capire quali sezioni del codice devono essere inserite nella lista ignora.

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. Il primo è un codice scritto da uno sviluppatore di siti web, mentre il secondo è un framework utilizzato.

{
  "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 nella struttura ad albero Origini.
  • Come risultati nella finestra di dialogo Apri rapidamente.
  • Come posizioni del frame di chiamata mappate nelle tracce dello stack di errori quando è in pausa su un breakpoint e durante l'esecuzione passo 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 deve essere aggiunto automaticamente all'elenco di ignorati.

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 tutte codice di terze parti che deve essere aggiunto automaticamente all'elenco di ignorati.

{
  ...
  "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 bundler, assicurati che le mappe di origine generate durante il processo di compilazione includano questo campo per collegarti a queste nuove funzionalità in DevTools di Chrome.

x_google_ignoreList in Angular

A partire dalla versione 14.1.0 di Angular, 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 tracce dello stack rispondono alla domanda "come ci sono arrivato", ma spesso questo avviene dal punto di vista della macchina e non necessariamente corrisponde al punto di vista dello sviluppatore o al suo modello mentale del 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 viene eseguita per impostazione predefinita, quindi gli sviluppatori possono già esaminarla. Tuttavia, in progetti più complessi non è così semplice, soprattutto se si utilizza un framework con meccanismi di pianificazione più avanzati, ad esempio uno che esegue il monitoraggio delle zone, l'inserimento in coda di attività personalizzate o che suddivide gli aggiornamenti in più unità di lavoro eseguite nel tempo.

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

L'API Async Stack Tagging

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

Un'analisi dello stack di un codice eseguito asincrono senza informazioni su quando è stato pianificato. Mostra solo la traccia dello stack a partire da "requestAnimationFrame", ma non contiene informazioni sulla sua pianificazione.

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

Una traccia dello stack di un codice eseguito in modo asincrono con informazioni su quando è stato pianificato. Nota che, a differenza di prima, nella traccia dello stack sono inclusi "businessLogic" e "schedule".

Per farlo, utilizza un nuovo metodo console denominato console.createTask() fornito dall'API Async Stack Tagging. La 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);

Le operazioni asincrone possono anche essere nidificate e le "cause principali" verranno visualizzate nella traccia dello stack in sequenza.

Le attività possono essere eseguite un numero qualsiasi di volte e il payload di lavoro può variare da un'esecuzione all'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 tra le attività asincrone.

Quando pianifica un'attività, utilizza console.createTask() se disponibile. L'istanza Task risultante viene archiviata per un uso futuro. All'attivazione dell'attività, NgZone utilizzerà l'istanza Task archiviata per eseguirla.

Queste modifiche sono state implementate in NgZone 0.11.8 di Angular tramite le richieste pull #46693 e #46958.

Cornici di chiamata 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 semplice JavaScript che poi viene eseguito nel browser. A volte, a questi tipi di funzioni generate vengono assegnati nomi non molto intuitivi, ad esempio nomi di una sola lettera dopo la minimizzazione o nomi oscuri o non familiari anche se non lo sono.

In Angular non è raro vedere frame di chiamata con nomi come AppComponent_Template_app_button_handleClick_1_listener nelle tracce dello stack.

Screenshot della traccia dello stack con un nome di funzione generato automaticamente.

Per risolvere il problema, Chrome DevTools ora supporta la ridenominazione di queste funzioni tramite le mappe di origine. Se una mappa di origine contiene una voce di nome per l'inizio dell'ambito di una funzione (ovvero la parentesi tonda sinistra dell'elenco dei parametri), lo stack frame deve mostrare quel nome nella traccia dello stack.

Frame di chiamata amichevoli in Angular

La ridenominazione dei frame di chiamata in Angular è un impegno continuo. Prevediamo che questi miglioramenti verranno implementati gradualmente nel tempo.

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

Nell'ambito di questa procedura di generazione di 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 tracce dello stack.

Prospettive future

Utilizzare Angular come test pilota per verificare il nostro lavoro è stata 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, come migliorare l'esperienza di profilazione in DevTools.