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 al debug di Chrome per le applicazioni WebAssembly sul web. Da allora, il team ha investito molte energie per adattare l'esperienza degli sviluppatori ad applicazioni di grandi dimensioni e persino di grandi dimensioni. In questo post vi mostreremo le manopole che abbiamo aggiunto (o fatto funzionare) nei diversi strumenti e come utilizzarli.

Debug scalabile

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

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

È ancora un esempio piuttosto piccolo e probabilmente non vedrai nessuno dei problemi reali che riscontreresti in un'applicazione davvero grande, ma possiamo comunque mostrarti quali sono le nuove funzionalità. È facile e veloce da configurare e da provare in prima persona.

Nell'ultimo post abbiamo parlato di come compilare ed eseguire il debug di questo esempio. Facciamo di nuovo lo stesso, ma diamo anche un'occhiata all'//performance//:

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

Questo comando produce un file binario wasm di 3 MB. La maggior parte di tutto questo, come ci si potrebbe aspettare, sono le 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 di queste sono sezioni WebAssembly standard, ma ci sono anche diverse sezioni personalizzate il cui nome inizia con .debug_. È qui che il file binario contiene le nostre informazioni di debug. Se sommamo tutte le dimensioni, notiamo che le informazioni di debug costituiscono circa 2,3 MB dei nostri 3 MB. Se eseguiamo anche il comando time emcc, vediamo che l'esecuzione sul nostro computer ha richiesto circa 1,5 secondi. Questi numeri rappresentano una piccola base di riferimento, ma sono così piccoli che probabilmente nessuno li avrebbe osservati. Nelle applicazioni reali, tuttavia, il programma binario di debug può facilmente raggiungere una dimensione in GB e richiedere alcuni minuti per la sua creazione.

Ignorare Binaryen

Quando si crea 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(simili). L'esecuzione di Binaryen come parte della build è piuttosto costosa, ma è richiesta solo in determinate condizioni. Per le build di debug, possiamo velocizzare significativamente la durata della build se evitiamo la necessità di usare passaggi Binaryen. Il passaggio Binaryen più comune richiesto è per la legalizzazione delle firme delle funzioni che coinvolgono valori interi a 64 bit. Attivando l'integrazione BigInt di WebAssembly utilizzando -sWASM_BIGINT, possiamo evitare che questo accada.

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

Abbiamo lanciato la bandiera -sERROR_ON_WASM_CHANGES_AFTER_LINK per sicurezza. Consente di rilevare quando Binaryen è in esecuzione e di riscrivere il file binario in modo imprevisto. In questo modo, possiamo assicurarci di rimanere sulla strada più veloce.

Anche se il nostro esempio è abbastanza 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 rispetto a prima.

Ritocchi avanzati

Ignorazione della scansione dei file di input

Normalmente, quando colleghi un progetto Emscripten, emcc analizzerà tutti i file e le librerie degli oggetti di input. allo scopo di implementare dipendenze precise tra le funzioni della libreria JavaScript e i simboli nativi nel programma. Per i progetti più grandi, questa ulteriore analisi dei file di input (utilizzando llvm-nm) può incrementare notevolmente il tempo di collegamento.

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

Per un progetto piccolo come il nostro esempio questo non fa davvero la differenza, ma se nel tuo progetto sono presenti centinaia o perfino migliaia di file oggetto, i tempi di collegamento possono migliorare significativamente.

Rimozione della sezione "name"

Nei progetti di grandi dimensioni, in particolare quelli che utilizzano molto modelli C++, la sezione "Nome" di WebAssembly può essere molto grande. Nel nostro esempio si tratta solo di una piccola parte delle dimensioni complessive del file (vedi l'output di llvm-objdump sopra), ma in alcuni casi può essere molto significativo. Se la sezione "name" dell'applicazione è molto grande e le informazioni di debug Dwarf sono sufficienti per le tue esigenze di debug, può essere vantaggioso 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

Le versioni binarie con molti dati di debug non pongono solo pressione sul tempo di creazione, ma anche sul tempo di debug. Il debugger deve caricare i dati e creare un apposito indice, in modo da poter rispondere rapidamente a query come "Qual è il tipo di variabile locale x?".

La fissione di debug ci consente di suddividere le informazioni di debug di un file binario in due parti: una che rimane nel file binario e l'altra che è contenuta in un file separato, il cosiddetto oggetto DWARF (.dwo). Può essere abilitato 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 vengono mostrati 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 diversi 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 hanno dimensioni maggiori e includono più file. La fissione di debug genera un file .dwo per ognuno di essi. Affinché l'attuale versione beta del debugger (0.1.6.1615) sia in grado di caricare queste informazioni di debug suddivise, dobbiamo raggrupparle tutte 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 dai singoli oggetti ha il vantaggio di dover pubblicare solo un file in più. Al momento stiamo lavorando anche sul caricamento di tutti i singoli oggetti in una versione futura.

Cosa c'è in DWARF 5?

Potresti aver notato che abbiamo aggiunto un'altra segnalazione al comando emcc sopra, -gdwarf-5. L'attivazione della versione 5 dei simboli DWARF, che al momento non è quella predefinita, è un altro trucco per aiutarci a velocizzare il debug. In questo modo, alcune informazioni vengono memorizzate nel file binario principale, escluse dalla versione 4 predefinita. Nello specifico, possiamo determinare il set completo di file sorgente semplicemente dal file binario principale. In questo modo il debugger può eseguire azioni di base come mostrare l'intero albero di origine e impostare punti di interruzione senza caricare e analizzare tutti i dati dei simboli. In questo modo il debug con i simboli divisi è molto più veloce, quindi utilizziamo sempre i flag della riga di comando -gsplit-dwarf e -gdwarf-5 insieme.

Con il formato di debug DWARF5 abbiamo anche accesso a un'altra utile funzionalità. Introduce un indice dei nomi nei dati di debug che verrà generato al momento del 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, le ricerche di simboli spesso vengono eseguite cercando 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 definisce l'entità denominata che stiamo cercando.

Curiosità: esaminare i dati di debug

Puoi usare llvm-dwarfdump per dare un'occhiata ai dati della piattaforma DWARF. Proviamo:

llvm-dwarfdump mandelbrot.wasm

Questo ci offre una panoramica delle "unità di compilazione" (approssimativamente, i file di origine) per le quali disponiamo di informazioni di debug. In questo esempio sono disponibili solo le informazioni di debug per mandelbrot.cc. Le informazioni generali ci consentono di sapere che abbiamo un'unità scheletro, il che significa semplicemente che il file contiene dati incompleti e che è presente un file .dwo separato contenente le restanti informazioni di debug:

mandelbrot.wasm e informazioni di debug

Puoi dare un'occhiata anche ad altre tabelle all'interno di questo file, ad es. nella tabella a linee che mostra la mappatura del bytecode wasm alle linee C++ (prova a usare 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 dell'utilizzo della fissione di debug?

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

  1. Collegamento più rapido: il linker non deve più analizzare tutte le informazioni di debug. Di solito i linker devono analizzare tutti i dati DWARF che si trovano nel file binario. Escludendo grandi parti delle informazioni di debug in file separati, i linker gestiscono binari più piccoli, il che si traduce in tempi di collegamento più rapidi (in particolare 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 (ad esempio richieste sulla mappatura di riga di file wasm-to-C++), non è necessario esaminare i dati di debug aggiuntivi. Questo ci consente di risparmiare tempo, senza dover caricare e analizzare i dati di debug aggiuntivi.

1: se sul tuo sistema non è installata una versione recente di llvm-objdump e stai utilizzando emsdk, puoi trovarlo nella directory emsdk/upstream/bin.

Scaricare i canali in anteprima

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

Contattare il team di Chrome DevTools

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

  • Inviaci un suggerimento o un feedback tramite crbug.com.
  • Segnala un problema di DevTools utilizzando Altre opzioni   Altro > Guida > Segnala un problema di DevTools in DevTools.
  • Invia un tweet all'indirizzo @ChromeDevTools.
  • Lascia commenti sulle novità nei video di YouTube di DevTools o nei video di YouTube dei suggerimenti di DevTools.