Debug più veloce di WebAssembly

Philip Pfaffe
Kim-Anh Tran
Kim-Anh Tran
Eric Leese
Sam Clegg

In occasione del Chrome Dev Summit 2020, abbiamo mostrato per la prima volta il supporto per il debug di Chrome per le applicazioni WebAssembly sul web. Da allora, il team ha investito molta energia per estendere l'esperienza degli sviluppatori a applicazioni di grandi dimensioni e persino di grandi dimensioni. In questo post ti mostreremo le manopole che abbiamo aggiunto (o che abbiamo fatto funzionare) ai diversi strumenti e come utilizzarli.

Debug scalabile

Ricominciamo da dove avevamo interrotto nel nostro post del 2020. Ecco l'esempio che stavamo esaminando all'epoca:

#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();
}

Si tratta comunque di un esempio piuttosto piccolo e probabilmente non vedresti nessuno dei problemi reali che vedresti in un'applicazione molto grande, ma possiamo comunque mostrarti le nuove funzionalità. È facile e veloce da configurare e da provare!

Nell'ultimo post abbiamo visto come compilare ed eseguire il debug di questo esempio. Riproviamo, ma diamo un'occhiata anche a //performance//:

$ emcc -sUSE_SDL=2 -g -O0 -o mandelbrot.html mandelbrot.cc -sALLOW_MEMORY_GROWTH

Questo comando produce un binario Wasm di 3 MB. E la maggior parte, come ci si potrebbe aspettare, sono informazioni di debug. Puoi verificarlo con lo strumento llvm-objdump [1], ad esempio:

$ llvm-objdump -h mandelbrot.wasm

mandelbrot.wasm:        file format wasm

Sections:
Idx Name          Size     VMA      Type
  0 TYPE          0000026f 00000000
  1 IMPORT        00001f03 00000000
  2 FUNCTION      0000043e 00000000
  3 TABLE         00000007 00000000
  4 MEMORY        00000007 00000000
  5 GLOBAL        00000021 00000000
  6 EXPORT        0000014a 00000000
  7 ELEM          00000457 00000000
  8 CODE          0009308a 00000000 TEXT
  9 DATA          0000e4cc 00000000 DATA
 10 name          00007e58 00000000
 11 .debug_info   000bb1c9 00000000
 12 .debug_loc    0009b407 00000000
 13 .debug_ranges 0000ad90 00000000
 14 .debug_abbrev 000136e8 00000000
 15 .debug_line   000bb3ab 00000000
 16 .debug_str    000209bd 00000000

Questo output mostra tutte le sezioni presenti nel file Wasm generato, la maggior parte delle quali sono sezioni WebAssembly standard, ma sono anche presenti diverse sezioni personalizzate il cui nome inizia con .debug_. È qui che il programma binario contiene le nostre informazioni di debug. Se aggiungiamo tutte le dimensioni, vediamo che le informazioni di debug costituiscono circa 2,3 MB del nostro file da 3 MB. Se timeanche il comando emcc, vediamo che l'esecuzione sulla nostra macchina ha richiesto circa 1,5 secondi. Questi numeri rappresentano una piccola base di riferimento, ma sono così piccoli che probabilmente nessuno li batterà d'occhio. Nelle applicazioni reali, tuttavia, il programma binario di debug può raggiungere facilmente una dimensione nei GB e richiederne la creazione in pochi minuti.

Saltare Binaryen

Quando crei un'applicazione Wasm con Emscripten, uno dei passaggi finali della build è l'esecuzione dell'ottimizzatore Binaryen. Binaryen è un toolkit di compilazione che ottimizza e legalizza i file binari di WebAssembly. L'esecuzione di Binaryen come parte della build è piuttosto costosa, ma è richiesta solo in determinate condizioni. Per le build di debug, possiamo accelerare in modo significativo i tempi di build se evitiamo la necessità dei pass di Binaryen. Il passaggio Binaryen richiesto più comune è per legalizzare le firme di funzioni che coinvolgono valori interi a 64 bit. Se attivi l'integrazione BigInt di WebAssembly utilizzando -sWASM_BIGINT, possiamo evitarlo.

$ emcc -sUSE_SDL=2 -g -O0 -o mandelbrot.html mandelbrot.cc -sALLOW_MEMORY_GROWTH -sWASM_BIGINT -sERROR_ON_WASM_CHANGES_AFTER_LINK

Per sicurezza, abbiamo inserito la bandiera di -sERROR_ON_WASM_CHANGES_AFTER_LINK. Consente di rilevare quando Binaryen è in esecuzione e di riscrivere il programma binario in modo imprevisto. In questo modo, possiamo essere certi di rimanere sulla strada più veloce.

Anche se il nostro esempio è piuttosto piccolo, possiamo comunque vedere l'effetto di ignorare Binaryen. Secondo time, questo comando viene eseguito poco meno di 1 secondo, quindi mezzo secondo più veloce di prima.

Ritocchi avanzati

Omissione della scansione dei file di input

Solitamente, quando colleghi un progetto Emscripten, emcc analizza tutti i file e le librerie degli oggetti di input. Lo fa per implementare dipendenze precise tra le funzioni della libreria JavaScript e i simboli nativi nel programma. Per i progetti più grandi, questa analisi aggiuntiva dei file di input (utilizzando llvm-nm) può aumentare notevolmente il tempo di collegamento.

È possibile eseguirlo con -sREVERSE_DEPS=all, che indica a emcc di includere tutte le possibili dipendenze native delle funzioni JavaScript. Questo ha un overhead di dimensioni ridotte per il codice, ma può velocizzare i tempi di collegamento ed essere utile per le build di debug.

Per un progetto così piccolo come il nostro esempio non c'è alcuna differenza, ma se hai centinaia o anche migliaia di file oggetto nel tuo progetto, i tempi di collegamento possono migliorare notevolmente.

Eliminazione della sezione "Nome"

Nei progetti di grandi dimensioni, in particolare quelli con un elevato utilizzo dei modelli C++, la sezione "nome" di WebAssembly può essere molto grande. Nel nostro esempio si tratta solo di una piccola parte della dimensione complessiva del file (vedi l'output di llvm-objdump sopra), ma in alcuni casi può essere molto significativa. Se la sezione "name" della tua applicazione è molto grande e le informazioni di debug dwarf sono sufficienti per le tue esigenze di debug, può essere utile rimuovere la sezione "name":

$ emstrip --no-strip-all --remove-section=name mandelbrot.wasm

Questa operazione rimuoverà la sezione "name" di WebAssembly, mantenendo le sezioni di debug DWARF.

Debug della fissione

I programmi binari con molti dati di debug non fanno solo pressione sulla durata della build, ma anche sui tempi di debug. Il debugger deve caricare i dati e deve creare un indice per i dati, in modo da poter rispondere rapidamente a query come "Qual è il tipo di variabile locale x?".

La fissione debug ci permette di suddividere le informazioni di debug di un programma binario in due parti: una, che rimane nel binario, e l'altra, che è contenuta in un file separato, il cosiddetto oggetto DWARF (.dwo). Può essere attivato passando il flag -gsplit-dwarf a Emscripten:

$ emcc -sUSE_SDL=2 -g -gsplit-dwarf -gdwarf-5 -O0 -o mandelbrot.html mandelbrot.cc  -sALLOW_MEMORY_GROWTH -sWASM_BIGINT -sERROR_ON_WASM_CHANGES_AFTER_LINK

Di seguito sono riportati i diversi comandi e i file generati mediante la compilazione senza dati di debug, con i dati di debug e infine con i dati di debug e la fissione di debug.

i vari comandi e i file generati

Quando suddividi i dati DWARF, una parte dei dati di debug risiede insieme al file binario, mentre la maggior parte viene inserita nel file mandelbrot.dwo (come illustrato sopra).

Per mandelbrot abbiamo un solo file di origine, ma in genere i progetti sono più grandi di queste dimensioni e includono più di un file. La fissione di debug genera un file .dwo per ognuno. Affinché l'attuale versione beta del debugger (0.1.6.1615) sia in grado di caricare queste informazioni di debug divise, è necessario raggrupparle in un cosiddetto pacchetto DWARF (.dwp) come questo:

$ emdwp -e mandelbrot.wasm -o mandelbrot.dwp

raggruppare file dwo in un pacchetto DWARF

La creazione del pacchetto DWARF a partire dai singoli oggetti presenta il vantaggio di dover pubblicare un solo file in più. Stiamo lavorando anche sul caricamento di tutti i singoli oggetti in una versione futura.

Cosa c'è con DWARF 5?

Come forse avrai notato, abbiamo inserito un altro flag nel comando emcc qui sopra, -gdwarf-5. L'abilitazione della versione 5 dei simboli DWARF, che al momento non è quella predefinita, è un altro trucco per consentirci di iniziare il debug più velocemente. Con questo strumento, alcune informazioni vengono memorizzate nel programma binario principale che la versione predefinita 4 escludeva. Nello specifico, possiamo determinare l'insieme completo di file sorgente semplicemente dal file binario principale. Ciò consente al debugger di eseguire azioni di base come la visualizzazione dell'intero albero di origine e l'impostazione dei punti di interruzione senza caricare e analizzare i dati completi del simbolo. Questo velocizza molto il debug con i simboli suddivisi, quindi utilizziamo sempre i flag delle righe di comando -gsplit-dwarf e -gdwarf-5 insieme.

Con il formato di debug DWARF5 otteniamo anche accesso a un'altra utile funzionalità. Introduce un indice dei nomi nei dati di debug che verrà generato durante il passaggio del flag -gpubnames:

$ emcc -sUSE_SDL=2 -g -gdwarf-5 -gsplit-dwarf -gpubnames -O0 -o mandelbrot.html mandelbrot.cc -sALLOW_MEMORY_GROWTH -sWASM_BIGINT -sERROR_ON_WASM_CHANGES_AFTER_LINK

Durante una sessione di debug, la ricerca di simboli avviene spesso tramite la ricerca di un'entità per nome, ad esempio quando si cerca una variabile o un tipo. L'indice dei nomi accelera questa ricerca puntando direttamente all'unità di compilazione che definisce quel nome. Senza un indice dei nomi, sarebbe necessaria una ricerca esaustiva di tutti i dati di debug per trovare l'unità di compilazione corretta che definisca l'entità denominata che stiamo cercando.

Per chi è curioso: esaminare i dati di debug

Puoi utilizzare llvm-dwarfdump per esaminare i dati del DWARF. Proviamoci:

llvm-dwarfdump mandelbrot.wasm

Ecco una panoramica delle "Unità di compilazione" (ovvero i file di origine) per le quali disponiamo di informazioni di debug. In questo esempio sono presenti solo le informazioni di debug per mandelbrot.cc. Le informazioni generali ci comunicheranno che abbiamo uno scheletro, il che significa semplicemente che il file contiene dati incompleti e che esiste un file .dwo separato che contiene le informazioni di debug rimanenti:

mandelbrot.wasm e informazioni di debug

Puoi dare un'occhiata anche ad altre tabelle all'interno di questo file, ad esempio la tabella a linee che mostra la mappatura del bytecode wasm alle righe C++ (prova a utilizzare llvm-dwarfdump -debug-line).

Possiamo anche dare un'occhiata alle informazioni di debug contenute nel file .dwo separato:

llvm-dwarfdump mandelbrot.dwo

mandelbrot.wasm e informazioni di debug

TL;DR: Qual è il vantaggio di utilizzare la fissione di debug?

La suddivisione delle informazioni di debug offre diversi vantaggi se si lavora con applicazioni di grandi dimensioni:

  1. Collegamento più rapido: il linker non deve più analizzare tutte le informazioni di debug. I linker di solito devono analizzare tutti i dati DWARF presenti nel programma binario. Eliminando grandi parti delle informazioni di debug in file separati, i linker gestiscono file binari più piccoli, il che si traduce in tempi di collegamento più rapidi (soprattutto per le applicazioni di grandi dimensioni).

  2. Debug più veloce: il debugger può saltare l'analisi dei simboli aggiuntivi nei file .dwo/.dwp per alcune ricerche di simboli. Per alcune ricerche (come le richieste sulla mappatura delle linee dei file wasm-to-C++), non è necessario esaminare i dati di debug aggiuntivi. In questo modo possiamo risparmiare tempo, in quanto non è necessario caricare e analizzare i dati di debug aggiuntivi.

1: se non disponi di una versione recente di llvm-objdump sul sistema e utilizzi emsdk, puoi trovarla nella directory emsdk/upstream/bin.

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.