Case study: migliore debug Angular con DevTools

Un'esperienza di debug migliorata

Negli ultimi mesi, il team di Chrome DevTools ha collaborato con il team di Angular per lanciare miglioramenti all'esperienza di debug in Chrome DevTools. I membri di entrambi i team hanno lavorato insieme e hanno intrapreso dei passi per consentire agli sviluppatori di eseguire il debug e il profiling delle applicazioni web dal punto di vista dell'autore: in termini di linguaggio di origine e struttura del progetto, con accesso a informazioni familiari e pertinenti.

Questo post esamina il funzionamento interno per vedere quali modifiche ad Angular e Chrome DevTools sono state necessarie per raggiungere questo obiettivo. Anche se alcune di queste modifiche vengono dimostrate tramite Angular, possono essere applicate anche ad altri framework. Il team di DevTools di Chrome incoraggia altri framework ad adottare le nuove API di console e i punti di estensione delle mappe di origine per offrire anche ai propri utenti un'esperienza di debug migliore.

Codice di ignoramento della scheda

Quando eseguono il debug delle applicazioni utilizzando Chrome DevTools, in genere gli autori vogliono vedere solo il proprio codice, non quello del framework sottostante o di qualche dipendenza nascosta 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 eseguire senza dover eseguire la configurazione manuale in precedenza.

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 di avanzamento e ripresa nel debugger.

Una GIF animata che mostra DevTools prima e dopo. Nota come nell'immagine successiva 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 sources originali che hanno contribuito alla generazione del 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 nel 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 punto di interruzione e durante l'esecuzione passo passo.

Ora è possibile includere un'ulteriore informazione nelle mappe delle origini 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 tramite una modifica in angular-cli mediante la creazione di un plug-in che si collega al modulo Compiler di webpack

Il plug-in webpack creato dai nostri ingegneri si inserisce nella fase PROCESS_ASSETS_STAGE_DEV_TOOLING e compila il campo x_google_ignoreList nelle mappe sorgente 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. Questo è particolarmente vero quando alcune operazioni sono pianificate per essere eseguite in modo asincrono in un secondo momento: potrebbe comunque essere interessante conoscere la "causa principale" o il lato di pianificazione di queste operazioni, ma questo è esattamente qualcosa che non farà parte di una traccia dello stack asincrona.

V8 dispone internamente di un meccanismo per tenere traccia di queste attività asincrone quando vengono utilizzate le primitive di pianificazione del browser standard, come 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 che vengono 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.

Una traccia dello stack di un codice eseguito in modo 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;
}

L'invocazione di console.createTask() restituisce un'istanza di 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 non verrà eseguita la raccolta dei rifiuti dell'oggetto attività.

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 pianifichi 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 generano spesso codice da tutti i tipi di linguaggi di creazione di modelli, ad esempio i modelli Angular o JSX che trasformano il codice simile a HTML in semplice JavaScript che 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. Al momento stiamo esplorando i modi per includere i nomi delle funzioni nel campo "names" 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 gestore di eventi e il relativo nome non è intuitivo o viene rimosso durante la minimizzazione, le mappe di origine ora possono includere il nome più intuitivo per questa funzione nel campo "names" e la mappatura per l'inizio dell'ambito della funzione ora può fare riferimento a questo nome (ovvero la parentesi tonda sinistra dell'elenco dei 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 ricevere feedback dagli sviluppatori di framework su questi punti di estensione.

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