Fehlerbehebung bei WebAssembly mit modernen Tools

Ingvar Stepanyan
Ingvar Stepanyan

Bisherige Entwicklung

Vor einem Jahr wurde in Chrome die anfängliche Unterstützung für das native WebAssembly-Debugging in den Chrome-Entwicklertools angekündigt.

Wir haben die Unterstützung für grundlegende Stufen demonstriert und über die Möglichkeiten gesprochen, die sich durch die Verwendung von DWARF-Informationen anstelle von Quellkarten in Zukunft ergeben:

  • Variablennamen auflösen
  • Typen für die Quelltextformatierung
  • Ausdrücke in Quellsprachen auswerten
  • …und vieles mehr!

Heute können wir die versprochenen Funktionen präsentieren und zeigen, welche Fortschritte die Emscripten- und Chrome DevTools-Teams in diesem Jahr insbesondere bei C- und C++-Apps gemacht haben.

Bevor wir beginnen, möchten wir Sie darauf hinweisen, dass dies noch eine Betaversion der neuen Version ist. Die Verwendung der neuesten Version aller Tools erfolgt auf eigenes Risiko. Sollten Probleme auftreten, melden Sie sie bitte unter https://issues.chromium.org/issues/new?noWizard=true&template=0&component=1456350.

Beginnen wir mit demselben einfachen C-Beispiel wie beim letzten Mal:

#include <stdlib.h>

void assert_less(int x, int y) {
  if (x >= y) {
    abort();
  }
}

int main() {
  assert_less(10, 20);
  assert_less(30, 20);
}

Zum Kompilieren verwenden wir die aktuelle Emscripten-Version und übergeben ein -g-Flag, genau wie im ursprünglichen Beitrag, um Informationen zur Fehlerbehebung anzugeben:

emcc -g temp.c -o temp.html

Jetzt können wir die generierte Seite über einen lokalen HTTP-Server (z. B. mit serve) bereitstellen und sie in der neuesten Version von Chrome Canary öffnen.

Dieses Mal benötigen wir auch eine Hilfserweiterung, die in die Chrome DevTools eingebunden ist und dabei hilft, alle in der WebAssembly-Datei codierten Informationen zur Fehlerbehebung zu verstehen. Sie können sie unter goo.gle/wasm-debugging-extension installieren.

Außerdem müssen Sie das WebAssembly-Debugging in den DevTools unter Experiments aktivieren. Öffnen Sie die Chrome-Entwicklertools, klicken Sie oben rechts im Bereich „Entwicklertools“ auf das Zahnradsymbol , gehen Sie zum Bereich Experimente und setzen Sie ein Häkchen bei WebAssembly-Debugging: DWARF-Unterstützung aktivieren.

Bereich „Tests“ in den DevTools-Einstellungen

Wenn Sie die Einstellungen schließen, werden Sie von den Entwicklertools aufgefordert, sie neu zu laden, um die Einstellungen anzuwenden. Tun wir das. Damit ist die einmalige Einrichtung abgeschlossen.

Jetzt können wir zum Bereich Quellen zurückkehren, Bei Ausnahmen pausieren (Symbol ⏸) aktivieren, dann Bei erkannten Ausnahmen pausieren auswählen und die Seite neu laden. Die DevTools sollten bei einer Ausnahme pausiert sein:

Screenshot des Bereichs „Quellen“ mit der Aktivierung von „Bei erkannten Ausnahmen anhalten“

Standardmäßig wird bei einem von Emscripten generierten Glue-Code angehalten. Rechts sehen Sie jedoch eine Aufrufabfolge, die den Stacktrace des Fehlers darstellt. Sie können auch zur ursprünglichen C-Zeile wechseln, die abort aufgerufen hat:

In den DevTools wurde die Ausführung in der Funktion „assert_less“ angehalten und in der Ansicht „Scope“ werden die Werte „x“ und „y“ angezeigt

Wenn Sie jetzt in der Ansicht Scope (Umfang) nachsehen, sehen Sie die ursprünglichen Namen und Werte der Variablen im C/C++-Code. Sie müssen also nicht mehr herausfinden, was unleserliche Namen wie $localN bedeuten und in welcher Beziehung sie zum von Ihnen geschriebenen Quellcode stehen.

Dies gilt nicht nur für primitive Werte wie Ganzzahlen, sondern auch für zusammengesetzte Typen wie Strukturen, Klassen, Arrays usw.

Unterstützung für Rich-Typen

Sehen wir uns ein etwas komplizierteres Beispiel an. Dieses Mal zeichnen wir mit dem folgenden C++-Code ein Mandelbrot-Fraktal:

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

Wie Sie sehen, ist diese Anwendung noch ziemlich klein – es ist eine einzelne Datei mit 50 Codezeilen. Dieses Mal verwende ich jedoch auch einige externe APIs, z. B. die SDL-Bibliothek für Grafiken sowie komplexe Zahlen aus der C++-Standardbibliothek.

Ich werde es mit demselben -g-Flag wie oben kompilieren, um Debug-Informationen einzubeziehen. Außerdem werde ich Emscripten bitten, die SDL2-Bibliothek bereitzustellen und einen beliebigen Arbeitsspeicher zuzulassen:

emcc -g mandelbrot.cc -o mandelbrot.html \
     -s USE_SDL=2 \
     -s ALLOW_MEMORY_GROWTH=1

Wenn ich die generierte Seite im Browser aufrufe, sehe ich die schöne Fraktalform mit einigen zufälligen Farben:

Demoseite

Wenn ich die Entwicklertools öffne, sehe ich wieder die ursprüngliche C++-Datei. Dieses Mal haben wir jedoch keinen Fehler im Code (puh!). Legen wir stattdessen einen Haltepunkt am Anfang des Codes fest.

Wenn wir die Seite noch einmal neu laden, wird der Debugger direkt in unserer C++-Quelldatei angehalten:

DevTools wurde beim Aufruf von „SDL_Init“ pausiert

Rechts sehen wir bereits alle Variablen, aber derzeit sind nur width und height initialisiert. Es gibt also nicht viel zu prüfen.

Legen wir einen weiteren Haltepunkt in der Hauptschleife des Mandelbrot-Algorithmus fest und fahren wir fort, um ein wenig vorwärts zu springen.

DevTools in den verschachtelten Schleifen pausiert

Unser palette ist jetzt mit einigen zufälligen Farben gefüllt. Wir können sowohl das Array selbst als auch die einzelnen SDL_Color-Strukturen maximieren und ihre Komponenten prüfen, um sicherzustellen, dass alles in Ordnung ist (z. B., dass der Alphakanal immer auf volle Deckkraft gesetzt ist). Ebenso können wir den reellen und imaginären Teil der komplexen Zahl, die in der Variablen center gespeichert ist, ausweiten und prüfen.

Wenn Sie auf eine tief verschachtelte Property zugreifen möchten, die über die Ansicht Umfang sonst nur schwer zu finden ist, können Sie auch die Console verwenden. Komplexere C++-Ausdrücke werden jedoch noch nicht unterstützt.

Konsolenbereich mit dem Ergebnis von „palette[10].r“

Führen wir die Ausführung noch ein paar Mal fort, um zu sehen, wie sich auch die innere x ändert. Dazu können wir entweder noch einmal in der Ansicht Scope nachsehen, den Variablennamen der Beobachtungsliste hinzufügen, ihn in der Konsole auswerten oder den Mauszeiger im Quellcode auf die Variable bewegen:

Tooltip für die Variable „x“ in der Quelle mit dem Wert „3“

Hier können wir C++-Anweisungen Schritt für Schritt ausführen und beobachten, wie sich auch andere Variablen ändern:

Kurzinfos und Bereichsansicht mit Werten für „color“, „point“ und andere Variablen

Okay, das funktioniert also hervorragend, wenn Debug-Informationen verfügbar sind. Was aber, wenn wir Code debuggen möchten, der nicht mit den Debugging-Optionen erstellt wurde?

Rohes WebAssembly-Debugging

Wir haben Emscripten beispielsweise gebeten, eine vorkonfigurierte SDL-Bibliothek für uns bereitzustellen, anstatt sie selbst aus der Quelle zu kompilieren. Daher kann der Debugger zumindest derzeit keine zugehörigen Quellen finden. Sehen wir uns noch einmal an, wie Sie die SDL_RenderDrawColor aufrufen:

DevTools mit der Ansicht „Disassembly“ von „mandelbrot.wasm“

Wir sind wieder beim reinen WebAssembly-Debugging.

Das sieht zwar etwas beängstigend aus und ist etwas, mit dem die meisten Webentwickler nie zu tun haben werden, aber gelegentlich möchten Sie vielleicht eine Bibliothek debuggen, die ohne Debug-Informationen erstellt wurde. Das kann daran liegen, dass es sich um eine Drittanbieterbibliothek handelt, über die Sie keine Kontrolle haben, oder dass Sie auf einen dieser Fehler stoßen, die nur in der Produktion auftreten.

Um Ihnen in diesen Fällen zu helfen, haben wir auch einige Verbesserungen am grundlegenden Debugging vorgenommen.

Wenn Sie schon einmal das Debugging von Roh-WebAssembly verwendet haben, werden Sie feststellen, dass die gesamte Deassemblage jetzt in einer einzigen Datei angezeigt wird. Sie müssen also nicht mehr raten, welcher Funktion ein Eintrag wasm-53834e3e/ wasm-53834e3e-7 unter Quellen möglicherweise entspricht.

Neues Schema zur Namensgenerierung

Außerdem wurden die Namen in der Demontageansicht verbessert. Bisher wurden nur numerische Indizes oder bei Funktionen gar kein Name angezeigt.

Jetzt generieren wir Namen ähnlich wie bei anderen Deaktivierungstools. Dazu verwenden wir Hinweise aus dem WebAssembly-Namensabschnitt, Import-/Exportpfade und, wenn alles andere fehlschlägt, generieren wir sie basierend auf dem Typ und dem Index des Elements wie $func123. Wie Sie im Screenshot oben sehen, führt dies bereits zu etwas besser lesbaren Stacktraces und Disassemblierungen.

Wenn keine Typinformationen verfügbar sind, kann es schwierig sein, Werte außer den primitiven zu prüfen. Beispielsweise werden Verweise als reguläre Ganzzahlen angezeigt, ohne dass man weiß, was im Speicher dahinter gespeichert ist.

Arbeitsspeicherprüfung

Bisher konnten Sie das WebAssembly-Speicherobjekt, das in der Ansicht Umfang durch env.memory dargestellt wird, nur maximieren, um einzelne Bytes abzurufen. Das funktionierte in einigen einfachen Szenarien, war aber nicht besonders praktisch zu erweitern und erlaubte nicht, Daten in anderen Formaten als Bytewerten neu zu interpretieren. Dazu haben wir eine neue Funktion hinzugefügt: einen linearen Speicherprüfer.

Wenn Sie mit der rechten Maustaste auf das env.memory klicken, sollte jetzt die neue Option Arbeitsspeicher prüfen angezeigt werden:

Kontextmenü für „env.memory“ im Bereich „Scope“ (Umfang) mit dem Menüpunkt „Inspect Memory“ (Arbeitsspeicher prüfen)

Daraufhin wird der Speicher-Inspektor geöffnet, in dem Sie den WebAssembly-Speicher in Hexadezimal- und ASCII-Ansicht prüfen, bestimmte Adressen aufrufen und die Daten in verschiedenen Formaten interpretieren können:

Bereich „Memory Inspector“ in den DevTools mit Hexadezimal- und ASCII-Ansicht des Arbeitsspeichers

Erweiterte Szenarien und Einschränkungen

Profilerstellung für WebAssembly-Code

Wenn Sie die Entwicklertools öffnen, wird WebAssembly-Code in eine nicht optimierte Version heruntergestuft, um das Debuggen zu ermöglichen. Diese Version ist viel langsamer. Das bedeutet, dass Sie console.time, performance.now und andere Methoden zur Messung der Geschwindigkeit Ihres Codes nicht verwenden können, während die DevTools geöffnet sind, da die erhaltenen Zahlen nicht die tatsächliche Leistung widerspiegeln.

Stattdessen sollten Sie den Bereich „Leistung“ in den DevTools verwenden. Dort wird der Code mit voller Geschwindigkeit ausgeführt und Sie erhalten eine detaillierte Aufschlüsselung der Zeit, die in den verschiedenen Funktionen verbracht wurde:

Bereich „Profiling“ mit verschiedenen Wasm-Funktionen

Alternativ können Sie Ihre Anwendung auch mit geschlossenen Entwicklertools ausführen und sie dann öffnen, um die Konsole zu prüfen.

Wir werden die Profiling-Szenarien in Zukunft verbessern, aber derzeit ist dies ein wichtiger Hinweis. Weitere Informationen zu WebAssembly-Tiering-Szenarien finden Sie in unserer Dokumentation zur WebAssembly-Kompilierungspipeline.

Builds und Debugging auf verschiedenen Maschinen (einschließlich Docker / Host)

Wenn Sie in einer Docker-Umgebung, auf einer virtuellen Maschine oder auf einem Remote-Build-Server erstellen, kommt es wahrscheinlich vor, dass die Pfade zu den Quelldateien, die während des Builds verwendet werden, nicht mit den Pfaden in Ihrem eigenen Dateisystem übereinstimmen, in dem die Chrome DevTools ausgeführt werden. In diesem Fall werden Dateien im Bereich Quellen angezeigt, aber nicht geladen.

Um dieses Problem zu beheben, haben wir in den C/C++-Erweiterungsoptionen eine Pfadzuordnungsfunktion implementiert. Sie können damit beliebige Pfade neu zuordnen und den DevTools helfen, Quellen zu finden.

Wenn sich das Projekt auf Ihrem Hostcomputer beispielsweise unter dem Pfad C:\src\my_project befindet, aber in einem Docker-Container erstellt wurde, in dem dieser Pfad als /mnt/c/src/my_project dargestellt wurde, können Sie ihn beim Debuggen neu zuordnen, indem Sie diese Pfade als Präfixe angeben:

Optionsseite der C/C++-Debugging-Erweiterung

Das erste übereinstimmende Präfix „gewinnt“. Wenn Sie mit anderen C++-Debuggern vertraut sind, entspricht diese Option dem Befehl set substitute-path in GDB oder einer target.source-map-Einstellung in LLDB.

Optimierte Builds beheben

Wie bei anderen Sprachen funktioniert das Debuggen am besten, wenn Optimierungen deaktiviert sind. Bei Optimierungen werden Funktionen möglicherweise ineinander verschachtelt, Code neu angeordnet oder Codeteile vollständig entfernt. All das kann den Debugger und damit auch Sie als Nutzer verwirren.

Wenn Sie mit einer eingeschränkten Fehlerbehebung einverstanden sind und trotzdem einen optimierten Build debuggen möchten, funktionieren die meisten Optimierungen wie erwartet, mit Ausnahme der Funktions-Inline-Optimierung. Wir planen, die verbleibenden Probleme in Zukunft anzugehen. Bis dahin können Sie die Funktion mit -fno-inline deaktivieren, wenn Sie mit Optimierungen auf -O-Ebene kompilieren, z. B.:

emcc -g temp.c -o temp.html \
     -O3 -fno-inline

Informationen zur Fehlerbehebung trennen

In den Debug-Informationen werden viele Details zu Ihrem Code, zu definierten Typen, Variablen, Funktionen, Bereichen und Speicherorten gespeichert – alles, was für den Debugger nützlich sein könnte. Daher kann sie oft größer sein als der Code selbst.

Um das Laden und Kompilieren des WebAssembly-Moduls zu beschleunigen, können Sie diese Debug-Informationen in eine separate WebAssembly-Datei aufteilen. Geben Sie dazu in Emscripten ein -gseparate-dwarf=…-Flag mit dem gewünschten Dateinamen an:

emcc -g temp.c -o temp.html \
     -gseparate-dwarf=temp.debug.wasm

In diesem Fall speichert die Hauptanwendung nur einen Dateinamen temp.debug.wasm. Die Hilfserweiterung kann ihn finden und laden, wenn Sie die DevTools öffnen.

In Kombination mit den oben beschriebenen Optimierungen können Sie mit dieser Funktion sogar fast optimierte Produktions-Builds Ihrer Anwendung bereitstellen und sie später mit einer lokalen Datei beheben. In diesem Fall müssen wir zusätzlich die gespeicherte URL überschreiben, damit die Erweiterung die Sidefile finden kann, z. B.:

emcc -g temp.c -o temp.html \
     -O3 -fno-inline \
     -gseparate-dwarf=temp.debug.wasm \
     -s SEPARATE_DWARF_URL=file://[local path to temp.debug.wasm]

Fortsetzung folgt…

Das waren ganz schön viele neue Funktionen!

Mit all diesen neuen Integrationen werden die Chrome-Entwicklertools zu einem leistungsstarken Debugger, der nicht nur für JavaScript, sondern auch für C- und C++-Apps geeignet ist. So können Apps, die mit einer Vielzahl von Technologien erstellt wurden, einfacher denn je in ein gemeinsames, plattformübergreifendes Web übertragen werden.

Wir sind jedoch noch nicht am Ende. Hier einige der Dinge, an denen wir in Zukunft arbeiten werden:

  • Die Fehlerbehebung wurde optimiert.
  • Unterstützung für Formatierer benutzerdefinierter Typen hinzugefügt.
  • Wir arbeiten an Verbesserungen des Profilings für WebAssembly-Apps.
  • Unterstützung für die Codeabdeckung hinzugefügt, um ungenutzten Code leichter zu finden.
  • Verbesserte Unterstützung von Ausdrücken bei der Konsolenerstellung.
  • Unterstützung für weitere Sprachen
  • und weitere!

In der Zwischenzeit können Sie die aktuelle Betaversion mit Ihrem eigenen Code testen und alle gefundenen Probleme unter https://issues.chromium.org/issues/new?noWizard=true&template=0&component=1456350 melden.

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.