Schnellere Fehlerbehebung in WebAssembly

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

Auf dem Chrome Dev Summit 2020 haben wir zum ersten Mal die Debugging-Unterstützung von Chrome für WebAssembly-Anwendungen im Web demonstriert. Seitdem hat das Team viel Energie darauf verwendet, die Entwicklungsumgebung für große und sogar sehr große Anwendungen zu skalieren. In diesem Beitrag zeigen wir euch die Regler, die wir in den verschiedenen Tools hinzugefügt oder aktiviert haben, und wie ihr sie verwendet.

Skalierbare Fehlerbehebung

Machen wir da weiter, wo wir in unserem Beitrag von 2020 aufgehört haben. Hier ist das Beispiel, das wir damals betrachtet haben:

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

Es ist immer noch ein ziemlich kleines Beispiel und Sie werden wahrscheinlich keine der echten Probleme sehen, die Sie in einer wirklich großen Anwendung sehen würden. Wir können Ihnen aber trotzdem zeigen, was die neuen Funktionen sind. Die Einrichtung ist schnell und einfach.

Im letzten Beitrag haben wir besprochen, wie dieses Beispiel kompiliert und debuggt wird. Wiederholen wir das noch einmal, aber werfen wir auch einen Blick auf //performance//:

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

Dieser Befehl erzeugt eine 3 MB große WASM-Binärdatei. Der Großteil davon sind, wie Sie sich vorstellen können, Informationen zur Fehlerbehebung. Sie können dies mit dem llvm-objdump-Tool [1] überprüfen:

$ 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

Diese Ausgabe zeigt alle Abschnitte in der generierten WASM-Datei. Die meisten davon sind Standard-WebAssembly-Abschnitte, aber es gibt auch mehrere benutzerdefinierte Abschnitte, deren Name mit .debug_ beginnt. Dort finden Sie die Informationen zur Fehlerbehebung. Wenn wir alle Größen zusammenzählen, sehen wir, dass die Debug-Informationen etwa 2,3 MB unserer 3 MB großen Datei ausmachen. Wenn wir auch den Befehl time emcc ausführen, sehen wir, dass die Ausführung auf unserem Computer etwa 1,5 Sekunden gedauert hat. Diese Zahlen sind eine gute kleine Baseline, aber sie sind so gering, dass sie wahrscheinlich niemanden interessieren würden. In realen Anwendungen kann das Debug-Binary jedoch leicht eine Größe von mehreren GB erreichen und das Erstellen kann Minuten dauern.

Binärdateien überspringen

Beim Erstellen einer WASM-Anwendung mit Emscripten wird als einer der letzten Buildschritte der Binaryen-Optimierer ausgeführt. Binaryen ist ein Compiler-Toolkit, mit dem WebAssembly-ähnliche Binärdateien sowohl optimiert als auch legalisiert werden. Das Ausführen von Binaryen im Rahmen des Builds ist ziemlich aufwendig, aber nur unter bestimmten Bedingungen erforderlich. Bei Debug-Builds können wir die Buildzeit erheblich verkürzen, wenn wir die Notwendigkeit von Binärdurchläufen vermeiden. Der am häufigsten erforderliche Binaryen-Pass dient der Legalisierung von Funktionssignaturen mit 64‑Bit-Ganzzahlwerten. Durch die Aktivierung der WebAssembly BigInt-Integration mit -sWASM_BIGINT können wir dies vermeiden.

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

Wir haben zur Sicherheit auch das Flag -sERROR_ON_WASM_CHANGES_AFTER_LINK hinzugefügt. So lässt sich erkennen, wenn Binaryen ausgeführt wird und das Binärprogramm unerwartet neu geschrieben wird. So können wir dafür sorgen, dass wir auf dem schnellsten Weg bleiben.

Obwohl unser Beispiel recht klein ist, sehen wir trotzdem die Auswirkungen des Überspringens von Binaryen. Laut time dauert die Ausführung dieses Befehls knapp eine Sekunde, also eine halbe Sekunde schneller als zuvor.

Erweiterte Optimierungen

Scannen der Eingabedatei überspringen

Normalerweise werden beim Verknüpfen eines Emscripten-Projekts alle Eingabeobjektdateien und ‑bibliotheken von emcc gescannt. Dies geschieht, um genaue Abhängigkeiten zwischen JavaScript-Bibliotheksfunktionen und nativen Symbolen in Ihrem Programm zu implementieren. Bei größeren Projekten kann dieses zusätzliche Scannen von Eingabedateien (mit llvm-nm) die Verknüpfungszeit erheblich verlängern.

Stattdessen können Sie -sREVERSE_DEPS=all verwenden, um emcc anzuweisen, alle möglichen nativen Abhängigkeiten von JavaScript-Funktionen einzubeziehen. Dies hat einen kleinen Codegrößen-Overhead, kann aber die Verknüpfungszeit verkürzen und ist für Debug-Builds nützlich.

Bei einem so kleinen Projekt wie unserem Beispiel macht das keinen großen Unterschied. Wenn Sie jedoch Hunderte oder sogar Tausende von Objektdateien in Ihrem Projekt haben, kann sich das deutlich auf die Verknüpfungszeit auswirken.

Entfernen des Abschnitts „name“

In großen Projekten, insbesondere in solchen, in denen viele C++-Vorlagen verwendet werden, kann der Bereich „name“ von WebAssembly sehr groß sein. In unserem Beispiel ist das nur ein kleiner Bruchteil der Gesamtdateigröße (siehe Ausgabe von llvm-objdump oben), in einigen Fällen kann es aber sehr hoch sein. Wenn der Abschnitt „name“ Ihrer Anwendung sehr groß ist und die Dwarf-Debug-Informationen für Ihre Debugging-Anforderungen ausreichen, kann es vorteilhaft sein, den Abschnitt „name“ zu entfernen:

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

Dadurch wird der WebAssembly-Abschnitt „name“ entfernt, während die DWARF-Debug-Abschnitte erhalten bleiben.

Fission debuggen

Binärdateien mit vielen Debugging-Daten belasten nicht nur die Buildzeit, sondern auch die Debugging-Zeit. Der Debugger muss die Daten laden und einen Index dafür erstellen, damit er schnell auf Abfragen wie „Was ist der Typ der lokalen Variablen x?“ reagieren kann.

Mit der Debug-Spaltung können wir die Debug-Informationen für ein Binärprogramm in zwei Teile aufteilen: einen, der im Binärprogramm verbleibt, und einen, der in einer separaten sogenannten DWARF-Objektdatei (.dwo) enthalten ist. Sie können es aktivieren, indem Sie das Flag -gsplit-dwarf an Emscripten übergeben:

$ 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

Unten sehen Sie die verschiedenen Befehle und die Dateien, die beim Kompilieren ohne Debug-Daten, mit Debug-Daten und schließlich mit Debug-Daten und Debug-Spaltung generiert werden.

die verschiedenen Befehle und die generierten Dateien

Beim Aufteilen der DWARF-Daten befindet sich ein Teil der Debug-Daten zusammen mit dem Binärcode, während der Großteil in die mandelbrot.dwo-Datei verschoben wird (siehe Abbildung oben).

Für mandelbrot haben wir nur eine Quelldatei, aber im Allgemeinen sind Projekte größer und enthalten mehr als eine Datei. Bei der Debug-Spaltung wird für jede davon eine .dwo-Datei generiert. Damit die aktuelle Betaversion des Debuggers (0.1.6.1615) diese aufgeteilten Debug-Informationen laden kann, müssen wir sie in einem sogenannten DWARF-Paket (.dwp) bündeln. Das geht so:

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

dwo-Dateien in einem DWARF-Paket bündeln

Das Erstellen des DWARF-Pakets aus den einzelnen Objekten hat den Vorteil, dass Sie nur eine zusätzliche Datei bereitstellen müssen. Wir arbeiten derzeit daran, in einer zukünftigen Version auch alle einzelnen Objekte zu laden.

Was ist mit DWARF 5?

Wie Sie vielleicht bemerkt haben, haben wir dem Befehl emcc oben ein weiteres Flag hinzugefügt: -gdwarf-5. Durch Aktivieren von Version 5 der DWARF-Symbole, die derzeit nicht standardmäßig aktiviert ist, können wir schneller mit dem Debuggen beginnen. Dabei werden bestimmte Informationen in der Haupt-Binärdatei gespeichert, die in der Standardversion 4 fehlen. Insbesondere können wir die gesamte Gruppe von Quelldateien nur anhand der Haupt-Binärdatei ermitteln. So kann der Debugger grundlegende Aktionen ausführen, z. B. den vollständigen Quellbaum anzeigen und Haltestellen setzen, ohne die vollständigen Symboldaten zu laden und zu analysieren. Dadurch wird das Debuggen mit geteilten Symbolen viel schneller. Wir verwenden die Befehlszeilen-Flags -gsplit-dwarf und -gdwarf-5 daher immer zusammen.

Mit dem DWARF5-Debugformat erhalten wir außerdem Zugriff auf eine weitere nützliche Funktion. Dadurch wird in den Debug-Daten ein Namenindex eingeführt, der generiert wird, wenn das Flag -gpubnames übergeben wird:

$ 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

Während einer Debugging-Sitzung werden Symbole häufig nach Namen gesucht, z.B. bei der Suche nach einer Variablen oder einem Typ. Der Nameindex beschleunigt diese Suche, da er direkt auf die Kompilierungseinheit verweist, die diesen Namen definiert. Ohne einen Namensindex wäre eine umfassende Suche in den gesamten Debug-Daten erforderlich, um die richtige Kompilierungseinheit zu finden, die das gesuchte benannte Element definiert.

Für alle Wissbegierigen: Debug-Daten ansehen

Mit llvm-dwarfdump können Sie sich die DWARF-Daten ansehen. Probieren wir es aus:

llvm-dwarfdump mandelbrot.wasm

So erhalten wir einen Überblick über die „Compile-Einheiten“ (grob gesagt die Quelldateien), für die wir Debug-Informationen haben. In diesem Beispiel haben wir nur die Informationen zur Fehlerbehebung für mandelbrot.cc. Die allgemeinen Informationen zeigen uns, dass wir eine Skeletteinheit haben. Das bedeutet lediglich, dass wir unvollständige Daten zu dieser Datei haben und dass es eine separate .dwo-Datei gibt, die die verbleibenden Debug-Informationen enthält:

mandelbrot.wasm und Informationen zur Fehlerbehebung

Sie können sich auch andere Tabellen in dieser Datei ansehen, z.B. die Zeilentabelle, in der die Zuordnung von WASM-Bytecode zu C++-Zeilen dargestellt wird (verwenden Sie dazu llvm-dwarfdump -debug-line).

Wir können uns auch die Debug-Informationen ansehen, die in der separaten .dwo-Datei enthalten sind:

llvm-dwarfdump mandelbrot.dwo

mandelbrot.wasm und Informationen zur Fehlerbehebung

Zusammenfassung: Welche Vorteile bietet die Debug-Spaltung?

Die Aufteilung der Debug-Informationen hat bei großen Anwendungen mehrere Vorteile:

  1. Schnelleres Verknüpfen: Der Linker muss nicht mehr die gesamten Debug-Informationen parsen. Linker müssen in der Regel die gesamten DWARF-Daten im Binärcode parsen. Durch das Entfernen großer Teile der Debug-Informationen in separate Dateien müssen Linker mit kleineren Binärdateien arbeiten, was zu kürzeren Verknüpfungszeiten führt (insbesondere bei großen Anwendungen).

  2. Schnelleres Debugging: Der Debugger kann das Parsen der zusätzlichen Symbole in .dwo-/.dwp-Dateien für einige Symbolsuchen überspringen. Bei einigen Suchanfragen (z. B. Anfragen zur Zeilenzuordnung von WASM-zu-C++-Dateien) müssen wir uns nicht die zusätzlichen Debug-Daten ansehen. So sparen wir Zeit, da wir die zusätzlichen Debug-Daten nicht laden und analysieren müssen.

1: Wenn Sie keine aktuelle Version von llvm-objdump auf Ihrem System haben und emsdk verwenden, finden Sie sie im Verzeichnis emsdk/upstream/bin.

Vorschaukanäle herunterladen

Verwenden Sie als Standard-Entwicklungsbrowser Chrome Canary, Chrome Dev oder Chrome Beta. Diese Vorabversionen bieten Ihnen Zugriff auf die neuesten DevTools-Funktionen, ermöglichen es Ihnen, innovative Webplattform-APIs zu testen, und helfen Ihnen, Probleme auf Ihrer Website zu finden, bevor Ihre Nutzer sie bemerken.

Chrome-Entwicklertools-Team kontaktieren

Mit den folgenden Optionen können Sie über neue Funktionen, Updates oder andere Themen im Zusammenhang mit den DevTools sprechen.