Schnellere Fehlerbehebung in WebAssembly

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

Beim Chrome Dev Summit 2020 haben wir die Chrome-Unterstützung für die Fehlerbehebung für WebAssembly-Anwendungen erstmals im Web vorgestellt. Seitdem hat das Team viel Energie in die Skalierung der Entwicklungsumgebung für große und sogar riesige Anwendungen investiert. In diesem Beitrag zeigen wir Ihnen die Knöpfe, die wir in den verschiedenen Tools hinzugefügt (oder bearbeitet) haben und wie sie verwendet werden.

Skalierbare Fehlerbehebung

Machen wir mit unserem Beitrag aus dem Jahr 2020 da weiter, wo wir aufgehört haben. Hier ist das damalige Beispiel:

#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 recht kleines Beispiel, und Sie werden wahrscheinlich keine der echten Probleme sehen, die Sie in einer wirklich großen Anwendung sehen würden, aber wir können Ihnen dennoch die neuen Funktionen zeigen. Die Einrichtung ist schnell und einfach und Sie können es selbst ausprobieren.

Im letzten Post haben wir besprochen, wie dieses Beispiel kompiliert und debuggt. Gehen wir wieder so vor, aber sehen wir uns auch //performance// an:

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

Dieser Befehl erzeugt eine 3 MB-Wasm-Binärdatei. Der Großteil davon sind Debug-Informationen. 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 WebAssembly-Standardabschnitte. Es gibt aber auch mehrere benutzerdefinierte Abschnitte, deren Name mit .debug_ beginnt. Hier enthält das Binärprogramm unsere Debug-Informationen. Addieren wir alle Größen zusammen, sehen wir, dass die Debug-Informationen etwa 2,3 MB der 3 MB großen Datei ausmachen. Wenn wir auch den Befehl emcc time, sehen wir, dass die Ausführung auf unserem Computer etwa 1,5 Sekunden gedauert hat. Diese Zahlen sind eine schöne Basis, aber sie sind so klein, dass sie wahrscheinlich niemand im Auge behalten würde. In echten Anwendungen kann das Debug-Binärprogramm jedoch leicht eine Größe im GB erreichen und die Erstellung nur wenige Minuten in Anspruch nehmen.

Binaryen wird übersprungen

Beim Erstellen einer Wasm-Anwendung mit Emscripten besteht einer der letzten Build-Schritte darin, den Binaryen-Optimierer auszuführen. Binaryen ist ein Compiler-Toolkit, das WebAssembly-Binärprogramme (ähnliche) optimiert und legalisiert. Das Ausführen von Binaryen im Rahmen des Builds ist relativ teuer, ist aber nur unter bestimmten Bedingungen erforderlich. Bei Debug-Builds können wir die Build-Zeit erheblich verkürzen, wenn keine Binaryen-Durchgänge mehr erforderlich sind. Der häufigste erforderliche Binaryen-Pass ist für die Legalisierung von Funktionssignaturen mit ganzzahligen 64-Bit-Werten. Wenn wir die WebAssembly BigInt-Integration mithilfe von -sWASM_BIGINT aktivieren, 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 das Flag -sERROR_ON_WASM_CHANGES_AFTER_LINK als gute Maßnahme eingefügt. Er erkennt, wenn Binaryen ausgeführt wird, und schreibt die Binärdatei unerwartet neu. Auf diese Weise können wir sicherstellen, dass wir auf dem schnellen Weg bleiben.

Auch wenn unser Beispiel eher klein ist, können wir immer noch die Auswirkungen des Überspringens von Binaryen sehen. Laut time wird dieser Befehl knapp unter 1 Sekunde ausgeführt, also eine halbe Sekunde schneller als zuvor.

Erweiterte Optimierungen

Scannen der Eingabedatei überspringen

Wenn Sie ein Emscripten-Projekt verknüpfen, scannt emcc normalerweise alle Eingabeobjektdateien und -bibliotheken. Dies ist erforderlich, 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.

Sie können stattdessen auch mit -sREVERSE_DEPS=all ausführen, wodurch emcc alle möglichen nativen Abhängigkeiten von JavaScript-Funktionen einbezogen hat. Dies hat einen geringen Code-Aufwand, kann jedoch die Verbindungszeiten verkürzen und bei Debug-Builds nützlich sein.

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

Abschnitt „Name“ entfernen

In großen Projekten, insbesondere solchen mit häufiger Verwendung von C++-Vorlagen, kann der WebAssembly-Abschnitt „name“ sehr groß sein. In unserem Beispiel ist dies nur ein winziger Bruchteil der Gesamtgröße der Datei (siehe die Ausgabe von llvm-objdump oben). In einigen Fällen kann sie aber sehr signifikant sein. Wenn der Abschnitt „name“ Ihrer Anwendung sehr groß ist und die Dwarf-Debug-Informationen für Ihre Debugging-Anforderungen ausreichen, kann es sinnvoll sein, den Abschnitt „name“ zu entfernen:

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

Dadurch wird der WebAssembly-Bereich „Name“ entfernt, während die DWARF-Fehlerbehebungsabschnitte beibehalten werden.

Spaltung der Fehlerbehebung

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

Mit der Spaltung der Fehlerbehebung können Sie die Debug-Informationen für eine Binärdatei in zwei Teile aufteilen: einen Teil, der in der Binärdatei verbleibt, und eine weitere, die sich in einer separaten, sogenannten DWARF-Objektdatei (.dwo) befindet. Er kann aktiviert werden, indem das Flag -gsplit-dwarf an Emscripten übergeben wird:

$ 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

Nachfolgend sehen Sie die verschiedenen Befehle und die Dateien, die durch Kompilierung ohne Debug-Daten, mit Debug-Daten und schließlich mit sowohl Debug-Daten als auch mit Debug-Daten generiert werden.

verschiedenen Befehlen und den generierten Dateien

Beim Aufteilen der DWARF-Daten verbleibt ein Teil der Debug-Daten zusammen mit der Binärdatei, während der große Teil in der mandelbrot.dwo-Datei gespeichert wird (wie oben dargestellt).

Bei mandelbrot haben wir nur eine Quelldatei, aber in der Regel sind Projekte größer als diese und enthalten mehr als eine Datei. Bei der Fehlerbehebung wird für jede von ihnen eine .dwo-Datei erstellt. Damit die aktuelle Betaversion des Debuggers (0.1.6.1615) diese aufgeteilten Debug-Informationen laden kann, müssen wir alle diese in einem sogenannten DWARF-Paket (.dwp) bündeln. Das sieht dann so aus:

$ 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, dass alle einzelnen Objekte auch in einer zukünftigen Version geladen werden können.

Was hat es mit DWARF 5?

Vielleicht ist Ihnen schon aufgefallen, dass wir im obigen emcc-Befehl ein weiteres Flag (-gdwarf-5) eingefügt haben. Die Aktivierung von Version 5 der DWARF-Symbole, die derzeit nicht der Standard ist, ist ein weiterer Trick, um das Debugging schneller zu starten. Damit werden bestimmte Informationen, die in der Standardversion 4 ausgelassen wurden, im Hauptbinärprogramm gespeichert. Insbesondere können wir den gesamten Satz der Quelldateien nur aus dem Hauptbinärprogramm ermitteln. Dadurch kann der Debugger grundlegende Aktionen ausführen, z. B. die vollständige Quellstruktur anzeigen und Haltepunkte festlegen, ohne die vollständigen Symboldaten laden und parsen zu müssen. Dadurch wird das Debugging mit geteilten Symbolen viel schneller. Deshalb 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. Damit wird ein Namensindex in die Debug-Daten 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 erfolgt die Symbolsuche häufig, indem nach dem Namen einer Entität gesucht wird, z.B. bei der Suche nach einer Variablen oder einem Typ. Der Namensindex beschleunigt die Suche, indem er direkt auf die Kompilierungseinheit verweist, die den Namen definiert. Ohne Namensindex wäre eine umfassende Suche in allen Fehlerbehebungsdaten erforderlich, um die richtige Kompilierungseinheit zu finden, die die gesuchte benannte Entität definiert.

Für Interessierte: 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 „Kompilierungseinheiten“ (gro gesagt die Quelldateien), für die uns Debug-Informationen vorliegen. In diesem Beispiel liegen nur die Informationen zur Fehlerbehebung für mandelbrot.cc vor. Die allgemeinen Informationen teilen uns mit, dass wir eine Skeleton-Einheit haben. Das bedeutet nur, dass die Datei unvollständige Daten enthält und dass es eine separate .dwo-Datei gibt, die die verbleibenden Fehlerbehebungsinformationen enthält:

mandelbrot.wasm und Informationen zur Fehlerbehebung

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

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

llvm-dwarfdump mandelbrot.dwo

mandelbrot.wasm und Informationen zur Fehlerbehebung

Zusammenfassung: Was ist der Vorteil der Debug-Spaltung?

Das Aufteilen der Debug-Informationen bietet mehrere Vorteile, wenn eine Anwendung mit großen Anwendungen arbeitet:

  1. Schnellere Verknüpfung: Die Verknüpfung muss nicht mehr die gesamten Informationen zur Fehlerbehebung parsen. Verknüpfer müssen normalerweise die gesamten DWARF-Daten parsen, die sich in der Binärdatei befinden. Durch das Entfernen großer Teile der Debug-Informationen in separaten Dateien verarbeiten Verknüpfer kleinere Binärprogramme. Dies führt zu kürzeren Verknüpfungszeiten, 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 Lookups (z. B. Anfragen zur Zeilenzuordnung von Wasm-zu-C++-Dateien) müssen wir uns die zusätzlichen Debug-Daten nicht ansehen. Das spart Zeit, da die zusätzlichen Debug-Daten nicht geladen und geparst werden müssen.

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

Vorschaukanäle herunterladen

Sie können Chrome Canary, Dev oder Beta als Standardbrowser für die Entwicklung verwenden. Über diese Vorschaukanäle erhältst du Zugriff auf die neuesten Entwicklertools-Funktionen, kannst neue Webplattform-APIs testen und Probleme auf deiner Website erkennen, bevor deine Nutzer es tun.

Chrome-Entwicklertools-Team kontaktieren

Verwende die folgenden Optionen, um die neuen Funktionen und Änderungen im Beitrag oder andere Themen im Zusammenhang mit den Entwicklertools zu besprechen.

  • Sende uns über crbug.com Vorschläge oder Feedback.
  • Wenn du ein Problem mit den Entwicklertools melden möchtest, klicke in den Entwicklertools auf Weitere Optionen   Mehr   > Hilfe > Probleme mit den Entwicklertools melden.
  • Senden Sie einen Tweet an @ChromeDevTools.
  • Hinterlasse Kommentare zu den Neuheiten in den Entwicklertools YouTube-Videos oder YouTube-Videos in den Entwicklertools-Tipps.