Schnellere Fehlerbehebung in WebAssembly

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

Beim Chrome Dev Summit 2020 haben wir zum ersten Mal die Fehlerbehebungsunterstützung von WebAssembly-Anwendungen in Chrome im Web vorgestellt. Seitdem hat das Team viel Energie investiert, um die Entwicklererfahrung für große und sogar große Anwendungen skalieren zu können. In diesem Beitrag zeigen wir Ihnen die Drehknöpfe, die wir den verschiedenen Tools hinzugefügt (oder bearbeitet) haben, und wie Sie sie verwenden.

Skalierbare Fehlerbehebung

Machen wir im Beitrag von 2020 dort weiter, wo wir aufgehört haben. Hier ist das Beispiel, das wir uns damals angesehen 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 handelt sich immer noch um ein relativ kleines Beispiel und Sie werden wahrscheinlich keines der tatsächlichen Probleme sehen, die bei einer sehr großen Anwendung auftreten würden, aber wir können Ihnen dennoch zeigen, was die neuen Funktionen sind. Die Einrichtung ist schnell und einfach und du kannst sie selbst ausprobieren.

Im letzten Post haben wir besprochen, wie Sie dieses Beispiel kompilieren und debuggen können. Sehen wir uns als Nächstes die Leistung unter //performance// an:

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

Dieser Befehl erzeugt ein Wasm-Binärprogramm mit 3 MB. Und der Großteil davon sind erwartungsgemäß Informationen zur Fehlerbehebung. Sie können dies mit dem llvm-objdump-Tool [1] prüfen. Beispiel:

$ 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, die sich in der generierten Wasm-Datei befinden. Die meisten davon sind Standard-WebAssembly-Abschnitte, aber es gibt auch mehrere benutzerdefinierte Abschnitte, deren Name mit .debug_ beginnt. Hier enthält die Binärdatei unsere Debug-Informationen. Wenn wir alle Größen addieren, sehen wir, dass die Debug-Informationen etwa 2,3 MB unserer 3 MB-Datei ausmachen. Wenn wir auch den Befehl emcc mit time ausführen, sehen wir, dass die Ausführung auf unserem Computer etwa 1,5 Sekunden gedauert hat. Diese Zahlen sind ein netter kleiner Anhaltspunkt, aber sie sind so klein, dass sie wahrscheinlich nicht infrage kommen. In echten Anwendungen kann das Debug-Binärprogramm jedoch problemlos eine Größe in GB erreichen und die Erstellung dauert einige Minuten.

Binaryen überspringen

Beim Erstellen einer Wasm-Anwendung mit Emscripten wird in einem der letzten Build-Schritte das Optimierungstool Binaryen ausgeführt. Binaryen ist ein Compiler-Toolkit, mit dem WebAssembly-Binärdateien optimiert und legalisiert werden. Das Ausführen von Binaryen als Teil des Builds ist relativ teuer, ist jedoch nur unter bestimmten Bedingungen erforderlich. Bei Debug-Builds können wir die Build-Zeit erheblich beschleunigen, wenn wir keine Binaryen-Pässe mehr benötigen. Der am häufigsten erforderliche Binaryen-Pass ist für die Legalisierung von Funktionssignaturen mit 64-Bit-Ganzzahlwerten erforderlich. Durch Aktivieren 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 für einen guten Messwert das Flag -sERROR_ON_WASM_CHANGES_AFTER_LINK eingefügt. Sie kann erkennen, wenn Binaryen ausgeführt wird, und die Binärdatei unerwartet neu schreiben. So können wir sicher sein, dass wir auf dem schnellen Weg bleiben.

Auch wenn unser Beispiel recht klein ist, können wir den Effekt des Überspringens von Binaryen sehen! Laut time wird dieser Befehl knapp unter einer Sekunde ausgeführt, also eine halbe Sekunde schneller als zuvor.

Erweiterte Optimierungen

Scannen der Eingabedatei überspringen

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

Sie können die Ausführung stattdessen mit -sREVERSE_DEPS=all ausführen. Dadurch wird emcc angewiesen, alle möglichen nativen Abhängigkeiten von JavaScript-Funktionen einzubeziehen. Dies hat einen geringen Mehraufwand für die Codegröße, kann jedoch die Verbindungszeiten beschleunigen und kann bei der Fehlerbehebung von Builds hilfreich sein.

Bei einem Projekt, das so klein wie in unserem Beispiel ist, macht dies keinen wirklichen Unterschied, aber wenn Sie Hunderte oder sogar Tausende von Objektdateien in Ihrem Projekt haben, können Sie die Verknüpfungszeiten erheblich verbessern.

Abschnitt „Name“ entfernen

In großen Projekten, insbesondere solchen mit häufig genutzter C++-Vorlagen, kann der Abschnitt „Name“ von WebAssembly sehr groß sein. In unserem Beispiel macht dies nur einen kleinen Bruchteil der gesamten Dateigröße aus (siehe Ausgabe von llvm-objdump oben), kann aber in manchen Fällen auch sehr bedeutend sein. Wenn der "name"-Abschnitt Ihrer Anwendung sehr groß ist und die Zwerg-Debug-Informationen für Ihre Debugging-Anforderungen ausreichen, kann es von Vorteil sein, den Abschnitt "name" zu entfernen:

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

Dadurch wird der Abschnitt „Name“ von WebAssembly entfernt, während die DWARF-Debug-Bereiche erhalten bleiben.

Fehlersuche

Binärdateien mit vielen Debug-Daten wirken sich nicht nur auf die Build-Zeit aus, sondern auch auf die Debugging-Zeit. Der Debugger muss die Daten laden und einen Index dafür erstellen, damit er schnell auf Abfragen wie „Welcher Typ ist die lokale Variable x?“ reagieren kann.

Mit der Fehlersuche können wir die Debug-Informationen für eine Binärdatei in zwei Teile aufteilen: einen Teil, der im Binärprogramm verbleibt, und einen, der in einer separaten, sogenannten DWARF-Objektdatei (.dwo) enthalten ist. Zur Aktivierung kann das Flag -gsplit-dwarf an Emscripten übergeben werden:

$ 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 durch die Kompilierung ohne Debug-Daten, mit Debug-Daten und schließlich mit Debug-Daten und Debugging-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ärprogramm, während der große Teil in die Datei mandelbrot.dwo verschoben wird (wie oben dargestellt).

Für mandelbrot gibt es nur eine Quelldatei. In der Regel sind Projekte jedoch größer und enthalten mehrere Dateien. Bei der Debugging-Suche wird für jeden eine .dwo-Datei generiert. Damit die aktuelle Betaversion des Debuggers (0.1.6.1615) diese Informationen zur aufgeteilten Fehlerbehebung laden kann, müssen wir alle diese Informationen in einem sogenannten DWARF-Paket (.dwp) bündeln:

$ 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 auch daran, in einer zukünftigen Version auch alle einzelnen Objekte zu laden.

Was ist mit DWARF 5?

Wie Sie vielleicht bemerkt haben, haben wir oben ein weiteres Flag in den emcc-Befehl eingefügt: -gdwarf-5. Die Aktivierung von Version 5 der DWARF-Symbole, die derzeit nicht die Standardeinstellung ist, ist ein weiterer Trick, mit dem wir die Fehlerbehebung beschleunigen können. Damit werden bestimmte Informationen im Hauptbinärprogramm gespeichert, die in der Standardversion 4 ausgelassen wurden. Insbesondere können wir den vollständigen Satz der Quelldateien allein aus dem Hauptbinärprogramm ermitteln. Auf diese Weise kann der Debugger grundlegende Aktionen ausführen, wie den vollständigen Quellbaum anzeigen und Haltepunkte festlegen, ohne die vollständigen Symboldaten zu laden und zu parsen. Dadurch wird die Fehlerbehebung mit aufgeteilten Symbolen viel schneller, daher verwenden wir immer die Befehlszeilen-Flags -gsplit-dwarf und -gdwarf-5 zusammen.

Mit dem DWARF5-Debug-Format erhalten wir auch Zugriff auf eine weitere nützliche Funktion. In den Debug-Daten wird ein Namensindex eingeführt, der beim Übergeben des Flags -gpubnames generiert 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 erfolgt die Symbolsuche häufig durch die Suche nach einer Entität anhand des Namens, z.B. wenn Sie nach einer Variablen oder einem Typ suchen. Der Namensindex beschleunigt diese Suche, indem er direkt auf die Kompilierungseinheit verweist, die diesen Namen definiert. Ohne Namensindex wäre eine umfassende Suche in allen Debug-Daten erforderlich, um die richtige Kompilierungseinheit zu finden, die die gesuchte benannte Entität definiert.

Für alle Wissbegierigen: Debug-Daten ansehen

Sie können llvm-dwarfdump verwenden, um einen Einblick in die DWARF-Daten zu erhalten. Probieren wir es aus:

llvm-dwarfdump mandelbrot.wasm

Dies gibt uns einen Überblick über die kompilierten Einheiten (grob gesprochen, die Quelldateien), für die uns Debug-Informationen vorliegen. In diesem Beispiel liegen uns nur die Informationen zur Fehlerbehebung für mandelbrot.cc vor. Aus den allgemeinen Informationen geht hervor, dass wir eine Basiseinheit haben, was nur bedeutet, dass die Daten unvollständig sind und dass es eine separate .dwo-Datei mit den verbleibenden Debug-Informationen gibt:

mandelbrot.wasm und Informationen zur Fehlerbehebung

Sie können sich auch andere Tabellen in dieser Datei ansehen, z.B. in der Zeilentabelle, die die Zuordnung des Wasm-Bytecodes zu C++-Zeilen zeigt (versuchen Sie es mit llvm-dwarfdump -debug-line).

Wir können uns auch die Informationen zur Fehlerbehebung in der separaten Datei .dwo ansehen:

llvm-dwarfdump mandelbrot.dwo

mandelbrot.wasm und Informationen zur Fehlerbehebung

Zusammenfassung: Was ist der Vorteil der Debugging-Spalte?

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

  1. Schnellere Verknüpfung: Die Verknüpfung muss nicht mehr die gesamten Debug-Informationen parsen. Verknüpfungen müssen normalerweise die gesamten DWARF-Daten im Binärprogramm parsen. Durch das Entfernen großer Teile der Debug-Informationen in separate Dateien können Linker mit kleineren Binärdateien umgehen, was zu kürzeren Verknüpfungszeiten führt (insbesondere bei großen Anwendungen).

  2. Schnelleres Debugging: Bei der Suche nach Symbolen muss der Debugger das Parsen der zusätzlichen Symbole in den Dateien .dwo/.dwp überspringen. Bei einigen Suchvorgängen (z. B. Anfragen zur Zeilenzuordnung von Wasm-zu-C++-Dateien) müssen wir uns die zusätzlichen Debug-Daten nicht ansehen. Dadurch sparen wir Zeit, da wir die zusätzlichen Debug-Daten nicht laden und parsen 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

Sie können Canary, Dev oder Beta als Standardbrowser für die Entwicklung verwenden. Über diese Vorschaukanäle erhalten Sie Zugriff auf die neuesten Entwicklertools, können innovative Webplattform-APIs testen und Probleme auf Ihrer Website erkennen, bevor Ihre Nutzer es tun.

Kontaktaufnahme mit dem Team für Chrome-Entwicklertools

Mit den folgenden Optionen kannst du die neuen Funktionen und Änderungen in dem Beitrag oder andere Aspekte der Entwicklertools besprechen.

  • Senden Sie uns über crbug.com einen Vorschlag oder Feedback.
  • Problem mit den Entwicklertools über Weitere Optionen melden Mehr > Hilfe > Hier kannst du Probleme mit den Entwicklertools in den Entwicklertools melden.
  • Twittern Sie unter @ChromeDevTools.
  • Hinterlasse Kommentare in den YouTube-Videos mit den Neuerungen in den Entwicklertools oder in YouTube-Videos mit Tipps zu den Entwicklertools.