Debug di WebAssembly con strumenti moderni

Ingvar Stepanyan
Ingvar Stepanyan

La strada per oggi

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

Abbiamo dimostrato un supporto di base nei passaggi e parlato delle opportunità di utilizzo delle informazioni DWARF anziché delle mappe di origine aperte per noi in futuro:

  • Risoluzione dei nomi delle variabili
  • Tipi di stampe
  • Valutazione delle espressioni nei linguaggi di origine
  • ...e molto altro ancora!

Oggi siamo entusiasti di presentare le funzionalità promesse e i progressi compiuti dai team di Emscripten e Chrome DevTools nel corso di quest'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 pericolo e, se riscontri problemi, segnalali all'indirizzo https://bugs.chromium.org/p/chromium/issues/entry?template=DevTools+issue.

Iniziamo con lo stesso esempio semplice di 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 l'ultima versione di Emscripten e trasmettiamo un flag -g, come nel post originale, per includere le informazioni di debug:

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 anche bisogno di un'estensione helper che si integri con Chrome DevTools e che consenta di interpretare tutte le informazioni di debug codificate nel file WebAssembly. Per installarla, vai a questo link: goo.gle/wasm-debugging-extension

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

Riquadro Esperimenti delle impostazioni DevTools

Quando chiudi le Impostazioni, DevTools suggerirà di ricaricarsi per applicare le impostazioni. Proviamo a farlo. Questo è tutto per la configurazione una tantum.

Ora possiamo tornare al riquadro Origini, attivare Metti in pausa in caso di eccezioni (icona ⏸), quindi selezionare Metti in pausa in caso di eccezioni rilevate e ricaricare la pagina. Dovresti vedere che DevTools è in pausa a causa di un'eccezione:

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

Per impostazione predefinita, si ferma su un glue code generato da Emscripten, ma a destra puoi vedere una vista Stack di chiamate che rappresenta lo stacktrace dell'errore e puoi passare alla riga C originale che ha richiamato abort:

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

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

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

Supporto di tipo avanzato

Diamo un'occhiata a un esempio più complicato per mostrarli. Questa volta disegneremo un frattole 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();
}

Come puoi notare, l'applicazione è ancora abbastanza piccola: si tratta di un singolo file contenente 50 righe di codice, ma questa volta utilizzo anche alcune API esterne, come la libreria SDL per le immagini e i numeri complessi della libreria standard di C++.

Lo compilerò con lo stesso flag -g di cui sopra per includere le informazioni di debug e chiederò anche a Emscripten di fornire la libreria SDL2 e consentire la 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, ancora una volta vedo il file C++ originale. Questa volta, però, il codice non contiene errori, quindi impostiamo un punto di interruzione all'inizio.

Quando ricarichiamo la pagina, il debugger si interrompe direttamente all'interno della nostra origine C++:

DevTools è stato messo in pausa nella chiamata &quot;SDL_Init&quot;

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

Impostiamo un altro punto di interruzione all'interno del ciclo di Mandelbrot principale e riprendi l'esecuzione per andare avanti.

DevTools è stato messo 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 funzioni correttamente (ad esempio, il canale "alpha" è sempre impostato sulla massima opacità). Analogamente, possiamo espandere e controllare le parti reali e immaginarie del numero complesso memorizzato nella variabile center.

Se vuoi accedere a una proprietà profondamente nidificata che altrimenti è difficile da raggiungere tramite la vista 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;

Riprendiamo l'esecuzione alcune volte e possiamo vedere come sta cambiando l'elemento x interno: tornando alla visualizzazione Ambito, aggiungendo il nome della variabile alla watchlist, valutandola nella console o passando il mouse sopra la variabile nel codice sorgente:

Descrizione comando sulla variabile &quot;x&quot; nell&#39;origine che mostra il valore &quot;3&quot;

Da qui, possiamo eseguire un passaggio o un step delle istruzioni C++ e osservare come cambiano anche le altre variabili:

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

OK, tutto funziona perfettamente 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 predefinita, anziché compilarla direttamente dal codice sorgente. Di conseguenza, almeno al momento il debugger non può trovare le fonti associate. Rispondiamo di nuovo per accedere a SDL_RenderDrawColor:

DevTools che mostra la visualizzazione del disassemblaggio di &quot;mandelbrot.wasm&quot;

Siamo tornati all'esperienza di debug non elaborata di WebAssembly.

Sembra un po' inquietante e non è una cosa che la maggior parte degli sviluppatori web dovrà gestire, ma di tanto in tanto potresti voler eseguire il debug di una libreria creata senza informazioni di debug, perché si tratta di una libreria di terze parti su cui non hai controllo o perché stai riscontrando uno di questi 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 non elaborato di WebAssembly, potresti notare che l'intero disassemblaggio viene ora mostrato in un singolo file, senza dover più indovinare a quale funzione potrebbe corrispondere una voce di tipo Origini wasm-53834e3e/ wasm-53834e3e-7.

Nuovo schema di generazione dei nomi

Abbiamo migliorato i nomi anche in visualizzazione disassemblaggio. In precedenza, venivano visualizzati solo indici numerici o, nel caso delle funzioni, nessun nome.

Ora generiamo nomi simili ad altri strumenti di smontaggio, utilizzando i suggerimenti della sezione dei nomi WebAssembly, i percorsi di importazione/esportazione e, infine, se tutto il resto non funziona, generandoli in base al tipo e all'indice dell'elemento, ad esempio $func123. Nello screenshot qui sopra puoi vedere che queste operazioni sono già utili per ottenere analisi dello stack e smontaggio leggermente più leggibili.

Quando non sono disponibili informazioni sul tipo, potrebbe essere difficile ispezionare valori diversi dalle primitive. Ad esempio, i puntatori verranno visualizzati come numeri interi regolari, senza modo di sapere cosa è archiviato dietro di loro.

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 ha funzionato in alcuni scenari banali, ma non era particolarmente conveniente per l'espansione e non consentiva di reinterpretare i dati in formati diversi dai valori in byte. Anche in questo caso abbiamo aggiunto una nuova funzionalità: l'ispezione della memoria lineare.

Se fai clic con il tasto destro del mouse su env.memory, ora dovresti vedere una nuova opzione chiamata Ispeziona memoria:

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

Una volta fatto clic, si aprirà un Memory Inspector in cui puoi esaminare la memoria WebAssembly nelle visualizzazioni esadecimale e ASCII, passare a indirizzi specifici e interpretare i dati in diversi formati:

Riquadro Controllo memoria in DevTools che mostra una visualizzazione esadecimale e ASCII della memoria

Scenari e avvertenze avanzate

Profilazione del codice WebAssembly

Quando apri DevTools, il codice WebAssembly viene "scalato" in una versione non ottimizzata per attivare il debug. Questa versione è molto più lenta, il che significa che non puoi fare affidamento su console.time, performance.now e altri metodi per misurare la velocità del tuo codice mentre DevTools è aperto, in quanto i dati ottenuti non rappresentano affatto le prestazioni reali.

Utilizza invece il riquadro Prestazioni di DevTools, che eseguirà il codice alla massima 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 chiuso e aprirle al termine per esaminare la console.

Miglioreremo gli scenari di profilazione in futuro, ma per il momento è un cavere a cui essere a conoscenza. Se vuoi scoprire di più sugli scenari di livelli di WebAssembly, consulta la nostra documentazione sulla pipeline di compilazione WebAssembly.

Creazione e debug su macchine diverse (tra cui Docker / host)

Quando crei in un Docker, in una macchina virtuale o in un server di compilazione remoto, probabilmente ti imbatterai in situazioni in cui i percorsi dei file di origine utilizzati durante la build non corrispondono a quelli nel tuo file system in cui sono in esecuzione Chrome DevTools. 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 dell'estensione C/C++. Puoi usarla per rimappare percorsi arbitrari e aiutare DevTools a individuare le origini.

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

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

Il primo prefisso corrispondente è "wins". Se conosci altri debug C++, questa opzione è simile al comando set substitute-path in GDB o a un'impostazione target.source-map in LLDB.

Debug delle build ottimizzate

Come con qualsiasi altro linguaggio, il debug funziona al meglio se le ottimizzazioni sono disattivate. Le ottimizzazioni potrebbero integrare le funzioni l'una nell'altra, riordinare il codice o rimuovere completamente parti del codice e tutto questo può confondere il debugger e, di conseguenza, l'utente in quanto utente.

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

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

Separare le informazioni di debug

Le informazioni di debug conservano molti dettagli sul codice, i tipi definiti, le variabili, le funzioni, gli ambiti e le posizioni, tutto ciò che possa essere utile per il debugger. Di conseguenza, spesso può essere più grande del codice stesso.

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

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

In questo caso, l'applicazione principale archivierà solo un nome file temp.debug.wasm e l'estensione helper sarà in grado di individuarlo e caricarlo quando apri DevTools.

Se combinata con le ottimizzazioni come descritte sopra, questa funzionalità può essere utilizzata anche per distribuire build di produzione quasi ottimizzate dell'applicazione e successivamente eseguirne il debug con un file laterale locale. In questo caso, dovremo anche eseguire l'override dell'URL archiviato per consentire all'estensione di trovare il file laterale, 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]

Continua...

Wow, erano tantissime 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++. In questo modo, è più facile che mai utilizzare le app, integrate in una varietà di tecnologie, e portarle su un Web multipiattaforma condiviso.

Tuttavia, il nostro viaggio non è ancora finito. Ecco alcuni degli aspetti su cui lavoreremo d'ora in poi:

  • Pulizia degli aspetti critici nell'esperienza di debug.
  • Aggiunta del supporto per i formattatori di tipi personalizzati.
  • Stiamo lavorando ai miglioramenti dei profili per le app WebAssembly.
  • Aggiunta del supporto per la copertura del codice per semplificare la ricerca del codice inutilizzato.
  • Miglioramento del supporto per le espressioni nella valutazione della console.
  • Aggiunta del supporto di altre lingue.
  • …e altro ancora.

Nel frattempo, ti invitiamo a provare la versione beta corrente sul tuo codice e a segnalare eventuali problemi riscontrati all'indirizzo https://bugs.chromium.org/p/chromium/issues/entry?template=DevTools+issue.

Scarica i canali in anteprima

Prendi in considerazione l'utilizzo di Chrome Canary, Dev o beta come browser di sviluppo predefinito. Questi canali in anteprima ti consentono di accedere alle funzionalità di DevTools più recenti, di testare le API per piattaforme web all'avanguardia e di individuare eventuali problemi sul tuo sito prima che lo facciano gli utenti.

Contattare il team di Chrome DevTools

Utilizza le opzioni seguenti per discutere delle nuove funzionalità e delle modifiche nel post o di qualsiasi altra cosa relativa a DevTools.

  • Inviaci un suggerimento o un feedback tramite crbug.com.
  • Segnala un problema DevTools utilizzando Altre opzioni   Altre   > Guida > Segnala i problemi di DevTools in DevTools.
  • Tweet all'indirizzo @ChromeDevTools.
  • Lascia commenti sui video di YouTube o sui suggerimenti di DevTools in DevTools Video di YouTube.