Debug di WebAssembly con strumenti moderni

Ingvar Stepanyan
Ingvar Stepanyan

La strada finora

Un anno fa, Chrome ha annunciato il supporto iniziale per il debug di WebAssembly nativo in Chrome DevTools.

Abbiamo dimostrato il supporto di base per i passaggi e parlato delle opportunità che si apriranno in futuro con l'utilizzo delle informazioni DWARF anziché delle mappe di origine:

  • Risolvere i nomi delle variabili
  • Tipi di formattazione del codice
  • Valutazione delle espressioni nelle lingue di origine
  • …e molto altro ancora.

Oggi siamo lieti di mostrare le funzionalità promesse che prendono vita e i progressi compiuti dai team di Emscripten e Chrome DevTools nel corso di questo anno, in particolare per le app C e C++.

Prima di iniziare, tieni presente che questa è ancora una versione beta della nuova esperienza. Devi utilizzare la versione più recente di tutti gli strumenti a tuo rischio e, in caso di problemi, segnalali all'indirizzo https://issues.chromium.org/issues/new?noWizard=true&template=0&component=1456350.

Iniziamo con lo stesso semplice esempio C dell'ultima volta:

#include <stdlib.h>

void assert_less(int x, int y) {
  if (x >= y) {
    abort();
  }
}

int main() {
  assert_less(10, 20);
  assert_less(30, 20);
}

Per compilarlo, utilizziamo la versione più recente di Emscripten e passiamo un flag -g, proprio come nel post originale, per includere le informazioni di debugging:

emcc -g temp.c -o temp.html

Ora possiamo pubblicare la pagina generata da un server HTTP localhost (ad esempio con serve) e aprirla nell'ultima versione di Chrome Canary.

Questa volta avremo bisogno anche di un'estensione helper che si integri con Chrome DevTools e lo aiuti a comprendere tutte le informazioni di debug codificate nel file WebAssembly. Installala seguendo questo link: goo.gle/wasm-debugging-extension

Ti consigliamo inoltre di attivare il debug di WebAssembly in Esperimenti di DevTools. Apri Chrome DevTools, fai clic sull'icona a forma di ingranaggio () nell'angolo in alto a destra del riquadro di DevTools, vai al riquadro Sperimentali e seleziona Debug WebAssembly: abilita il supporto DWARF.

Riquadro Esperimenti delle impostazioni di DevTools

Quando chiudi Impostazioni, DevTools ti suggerirà di ricaricarsi per applicare le impostazioni, quindi fai proprio questo. È tutto per la configurazione singola.

Ora possiamo tornare al riquadro Origini, attivare Metti in pausa su eccezioni (icona ⏸), quindi selezionare Metti in pausa su eccezioni rilevate e reloading la pagina. Dovresti vedere DevTools in pausa su un'eccezione:

Screenshot del riquadro Origini che mostra come attivare &quot;Metti in pausa in caso di eccezioni rilevate&quot;

Per impostazione predefinita, si arresta su un codice di collegamento generato da Emscripten, ma a destra puoi vedere una visualizzazione Call Stack che rappresenta lo stack trace dell'errore e puoi passare alla riga C originale che ha invocatoabort:

DevTools è in pausa nella funzione &quot;assert_less&quot; e mostra i valori di &quot;x&quot; e &quot;y&quot; nella visualizzazione Ambito

Ora, se guardi nella visualizzazione Ambito, puoi vedere i nomi e i valori originali delle variabili nel codice C/C++ e non devi più capire il significato di nomi alterati come $localN e il loro rapporto con il codice sorgente che hai scritto.

Questo vale non solo per i valori primitivi come gli interi, ma anche per i tipi composti come strutture, classi, array e così via.

Supporto di tipi avanzati

Vediamo un esempio più complicato per mostrarli. Questa volta, disegneremo un frattale di Mandelbrot con il seguente codice C++:

#include <SDL2/SDL.h>
#include <complex>

int main() {
  // Init SDL.
  int width = 600, height = 600;
  SDL_Init(SDL_INIT_VIDEO);
  SDL_Window* window;
  SDL_Renderer* renderer;
  SDL_CreateWindowAndRenderer(width, height, SDL_WINDOW_OPENGL, &window,
                              &renderer);

  // Generate a palette with random colors.
  enum { MAX_ITER_COUNT = 256 };
  SDL_Color palette[MAX_ITER_COUNT];
  srand(time(0));
  for (int i = 0; i < MAX_ITER_COUNT; ++i) {
    palette[i] = {
        .r = (uint8_t)rand(),
        .g = (uint8_t)rand(),
        .b = (uint8_t)rand(),
        .a = 255,
    };
  }

  // Calculate and draw the Mandelbrot set.
  std::complex<double> center(0.5, 0.5);
  double scale = 4.0;
  for (int y = 0; y < height; y++) {
    for (int x = 0; x < width; x++) {
      std::complex<double> point((double)x / width, (double)y / height);
      std::complex<double> c = (point - center) * scale;
      std::complex<double> z(0, 0);
      int i = 0;
      for (; i < MAX_ITER_COUNT - 1; i++) {
        z = z * z + c;
        if (abs(z) > 2.0)
          break;
      }
      SDL_Color color = palette[i];
      SDL_SetRenderDrawColor(renderer, color.r, color.g, color.b, color.a);
      SDL_RenderDrawPoint(renderer, x, y);
    }
  }

  // Render everything we've drawn to the canvas.
  SDL_RenderPresent(renderer);

  // SDL_Quit();
}

Puoi vedere che questa applicazione è ancora abbastanza piccola: è un singolo file contenente 50 righe di codice, ma questa volta sto utilizzando anche alcune API esterne, come la libreria SDL per la grafica e i numeri complessi della libreria standard C++.

Lo compilerò con lo stesso flag -g di cui sopra per includere informazioni di debug e chiederò anche a Emscripten di fornire la libreria SDL2 e consentire una memoria di dimensioni arbitrarie:

emcc -g mandelbrot.cc -o mandelbrot.html \
     -s USE_SDL=2 \
     -s ALLOW_MEMORY_GROWTH=1

Quando visito la pagina generata nel browser, vedo la bellissima forma frattale con alcuni colori casuali:

Pagina demo

Quando apro DevTools, riesco di nuovo a vedere il file C++ originale. Questa volta, però, non c'è un errore nel codice (meno male!), quindi impostiamo un breakpoint all'inizio del codice.

Quando ricarichiamo di nuovo la pagina, il debugger si mette in pausa proprio all'interno del codice sorgente C++:

DevTools è in pausa sulla chiamata &quot;SDL_Init&quot;

Possiamo già vedere tutte le nostre variabili a destra, ma al momento solo width e height sono inizializzate, quindi non c'è molto da controllare.

Impostiamo un altro punto di interruzione all'interno del nostro loop Mandelbrot principale e riprendiamo l'esecuzione per andare un po' avanti.

DevTools è in pausa all&#39;interno dei loop nidificati

A questo punto, il nostro palette è stato riempito con alcuni colori casuali, e possiamo espandere sia l'array stesso sia le singole strutture SDL_Color e ispezionarne i componenti per verificare che tutto sia a posto (ad esempio, che il canale "alpha" sia sempre impostato su opacità totale). Analogamente, possiamo espandere e controllare le parti reale e immaginaria del numero complesso memorizzato nella variabile center.

Se vuoi accedere a una proprietà nidificata in modo complesso, che altrimenti è difficile da raggiungere tramite la visualizzazione Ambito, puoi utilizzare anche la valutazione della console. Tuttavia, tieni presente che le espressioni C++ più complesse non sono ancora supportate.

Riquadro della console che mostra il risultato di &quot;palette[10].r&quot;

Riprenderemo l'esecuzione alcune volte e possiamo vedere come sta cambiando anche il x interno. Osservando di nuovo la vista Ambito, aggiungendo il nome della variabile alla lista di controllo, valutandola nella console o passando il mouse sopra la variabile nel codice sorgente:

Descrizione comando sopra la variabile &quot;x&quot; nel codice sorgente che mostra il valore &quot;3&quot;

Da qui, possiamo analizzare o eseguire il passaggio delle istruzioni C++ e osservare come stanno cambiando anche altre variabili:

Descrizioni comando e visualizzazione Ambito che mostrano i valori di &quot;color&quot;, &quot;point&quot; e altre variabili

Bene, tutto questo funziona benissimo quando sono disponibili informazioni di debug, ma cosa succede se vogliamo eseguire il debug di un codice che non è stato creato con le opzioni di debug?

Debug di WebAssembly non elaborato

Ad esempio, abbiamo chiesto a Emscripten di fornirci una libreria SDL precompilata, anziché compilarla noi stessi dal codice sorgente, quindi, almeno al momento, non c'è modo per il debugger di trovare le origini associate. Riproviamo ad accedere a SDL_RenderDrawColor:

Strumenti DevTools che mostrano la visualizzazione disassemblata di &quot;mandelbrot.wasm&quot;

Torniamo all'esperienza di debug non elaborata di WebAssembly.

Sembra un po' spaventoso e non è qualcosa che la maggior parte degli sviluppatori web dovrà mai gestire, ma a volte potresti voler eseguire il debug di una libreria creata senza informazioni di debug, ad esempio perché si tratta di una libreria di terza parte su cui non hai alcun controllo o perché hai riscontrato uno di quei bug che si verificano solo in produzione.

Per aiutarti in questi casi, abbiamo apportato alcuni miglioramenti anche all'esperienza di debug di base.

Innanzitutto, se in precedenza hai utilizzato il debug di WebAssembly non elaborato, potresti notare che l'intero disassemblage ora viene mostrato in un unico file. Non dovrai più indovinare a quale funzione potrebbe corrispondere una voce wasm-53834e3e/ wasm-53834e3e-7 Origini.

Schema di generazione del nuovo nome

Abbiamo migliorato i nomi anche nella visualizzazione disassemblata. In precedenza veniva visualizzato solo gli indici numerici o, nel caso delle funzioni, nessun nome.

Ora generiamo i nomi in modo simile ad altri strumenti di smontaggio, utilizzando i suggerimenti della sezione dei nomi di WebAssembly, i percorsi di importazione/esportazione e, infine, se non funzionano, li generiamo in base al tipo e all'indice dell'elemento, ad esempio $func123. Puoi vedere come, nello screenshot qui sopra, questo aiuta già a ottenere stacktrace e disassembly leggermente più leggibili.

Quando non sono disponibili informazioni sul tipo, potrebbe essere difficile ispezionare qualsiasi valore oltre a quelli primitivi. Ad esempio, i puntatori verranno visualizzati come interi regolari, senza alcun modo di sapere cosa è memorizzato al loro interno nella memoria.

Ispezione della memoria

In precedenza, potevi espandere solo l'oggetto di memoria WebAssembly, rappresentato da env.memory nella visualizzazione Ambito, per cercare singoli byte. Questo approccio ha funzionato in alcuni scenari banali, ma non era particolarmente comodo da espandere e non consentiva di reinterpretare i dati in formati diversi dai valori in byte. Abbiamo aggiunto anche una nuova funzionalità per aiutarti: un'ispezione della memoria lineare.

Se fai clic con il tasto destro del mouse su env.memory, ora dovresti visualizzare una nuova opzione denominata Esamina memoria:

Menu contestuale su &quot;env.memory&quot; nel riquadro Ambito che mostra un elemento &quot;Esamina memoria&quot;

Dopo aver fatto clic, viene visualizzato un Memory Inspector, in cui puoi ispezionare la memoria WebAssembly nelle visualizzazioni esadecimali e ASCII, accedere ad indirizzi specifici e interpretare i dati in diversi formati:

Riquadro Controllo memoria in DevTools che mostra le visualizzazioni esadecimale e ASCII della memoria

Scenari avanzati e avvertenze

Profilazione del codice WebAssembly

Quando apri DevTools, il codice WebAssembly viene "ridotto" a una versione non ottimizzata per abilitare il debug. Questa versione è molto più lenta, il che significa che non puoi fare affidamento su console.time, performance.now e su altri metodi per misurare la velocità del codice quando DevTools è aperto, poiché i numeri ottenuti non rappresentano affatto le prestazioni reali.

Ti consigliamo invece di utilizzare il pannello Rendimento di DevTools, che eseguirà il codice a piena velocità e ti fornirà un'analisi dettagliata del tempo trascorso nelle diverse funzioni:

Riquadro di profilazione che mostra varie funzioni Wasm

In alternativa, puoi eseguire l'applicazione con DevTools chiusi e aprirli al termine dell'operazione per ispezionare la console.

Miglioreremo gli scenari di profilazione in futuro, ma per il momento è un caveat da tenere presente. Per scoprire di più sugli scenari di tiering di WebAssembly, consulta la nostra documentazione sulla pipeline di compilazione di WebAssembly.

Creazione di build e debug su macchine diverse (inclusi Docker / host)

Quando esegui la compilazione in Docker, in una macchina virtuale o su un server di compilazione remoto, è probabile che si verifichino situazioni in cui i percorsi dei file di origine utilizzati durante la compilazione non corrispondono ai percorsi sul tuo file system su cui vengono eseguiti gli strumenti di sviluppo di Chrome. In questo caso, i file verranno visualizzati nel riquadro Origini ma non verranno caricati.

Per risolvere il problema, abbiamo implementato una funzionalità di mappatura dei percorsi nelle opzioni di estensione C/C++. Puoi utilizzarlo per rimappare percorsi arbitrari e aiutare DevTools a individuare le origini.

Ad esempio, se il progetto sulla tua macchina host si trova in un percorsoC:\src\my_project, ma è stato creato all'interno di un contenitore Docker in cui quel percorso era rappresentato come /mnt/c/src/my_project, puoi rimappare nuovamente il percorso durante il debug specificando questi percorsi come prefissi:

Pagina Opzioni dell&#39;estensione di debug C/C++

Il primo prefisso trovato "vince". Se hai dimestichezza con altri debugger C++, questa opzione è simile al comando set substitute-path in GDB o a un'impostazione target.source-map in LLDB.

Eseguire il debug delle build ottimizzate

Come per qualsiasi altra lingua, il debug funziona al meglio se le ottimizzazioni sono disabilitate. Le ottimizzazioni potrebbero inserire in linea le funzioni una nell'altra, riordinare il codice o rimuoverne parti del tutto e tutto ciò potrebbe confondere il debugger e, di conseguenza, te come utente.

Se non ti dispiace un'esperienza di debug più limitata e vuoi comunque eseguire il debugging di una build ottimizzata, la maggior parte delle ottimizzazioni funzionerà come previsto, ad eccezione dell'inserimento in linea delle funzioni. Prevediamo di risolvere i problemi rimanenti in futuro, ma per il momento utilizza -fno-inline per disattivarlo durante la compilazione con eventuali ottimizzazioni a livello di -O, ad esempio:

emcc -g temp.c -o temp.html \
     -O3 -fno-inline

Separazione delle informazioni di debug

Le informazioni di debug conservano molti dettagli sul codice, tipi, variabili, funzioni, ambiti e posizioni definiti, ovvero tutto ciò che potrebbe essere utile al debugger. Di conseguenza, spesso può essere più grande del codice stesso.

Per velocizzare il caricamento e la compilazione del modulo WebAssembly, potresti spartire queste informazioni di debug in un file WebAssembly distinto. Per farlo in Emscripten, passa un flag -gseparate-dwarf=… con il nome del file che preferisci:

emcc -g temp.c -o temp.html \
     -gseparate-dwarf=temp.debug.wasm

In questo caso, l'applicazione principale memorizza solo un nome filetemp.debug.wasm e l'estensione di supporto sarà in grado di trovarlo e caricarlo quando apri DevTools.

Se combinata con le ottimizzazioni descritte sopra, questa funzionalità può essere utilizzata anche per pubblicare build di produzione quasi ottimizzate della tua applicazione e successivamente eseguire il debug con un file secondario locale. In questo caso, dovremo inoltre sostituire l'URL archiviato per consentire all'estensione di trovare il file aggiuntivo, ad esempio:

emcc -g temp.c -o temp.html \
     -O3 -fno-inline \
     -gseparate-dwarf=temp.debug.wasm \
     -s SEPARATE_DWARF_URL=file://[local path to temp.debug.wasm]

To be continued…

Wow, quante nuove funzionalità!

Con tutte queste nuove integrazioni, Chrome DevTools diventa un debugger valido e potente non solo per JavaScript, ma anche per le app C e C++, semplificando più che mai il trasferimento di app create in una serie di tecnologie su un web cross-platform condiviso.

Tuttavia, il nostro percorso non è ancora terminato. Ecco alcune delle funzionalità su cui lavoreremo da qui in poi:

  • Eliminazione dei punti critici nell'esperienza di debug.
  • Aggiunta del supporto per i formatter dei tipi personalizzati.
  • Stiamo lavorando a miglioramenti al profiling per le app WebAssembly.
  • È stato aggiunto il supporto per la copertura del codice per trovare più facilmente il codice inutilizzato.
  • Miglioramento del supporto delle espressioni nella valutazione della console.
  • Aggiunta del supporto per altre lingue.
  • …e altro ancora.

Nel frattempo, aiutaci provando l'attuale versione beta sul tuo codice e segnalando eventuali problemi riscontrati all'indirizzo https://issues.chromium.org/issues/new?noWizard=true&amp;template=0&amp;component=1456350.

Scaricare i canali di anteprima

Valuta la possibilità di utilizzare Chrome Canary, Dev o Beta come browser di sviluppo predefinito. Questi canali di anteprima ti danno accesso alle ultime funzionalità di DevTools, ti consentono di testare le API delle piattaforme web all'avanguardia e ti aiutano a individuare i problemi sul tuo sito prima che lo facciano gli utenti.

Contatta il team di Chrome DevTools

Utilizza le seguenti opzioni per discutere di nuove funzionalità, aggiornamenti o qualsiasi altro argomento relativo a DevTools.