2 Punkte von GN⁺ 2024-04-27 | 1 Kommentare | Auf WhatsApp teilen
  • Bun v1.1.5 fügt mit bun.report einen Crash-Reporter hinzu, der selbst bei Crashs oder Panics Zig/C++-Stack-Informationen über eine etwa 150 Byte große URL ohne personenbezogene Daten übermittelt
  • Bestehende OS-Crash-Reporter und Core Dumps bringen hohe Kosten bei Debug-Symbolen, Performance, Datenschutz und Binärgröße mit sich und sind deshalb für ein CLI-Tool wie Bun schwer einsetzbar
  • Der neue Ansatz wandelt durch ASLR unklare Adressen in relative Adressen bezogen auf das Modul um, und der Server rekonstruiert mithilfe von Commit-SHA und plattformspezifischen Debug-Symbolen wieder die Funktionsnamen
  • Die URL enthält Plattform, Subcommand, Commit-SHA, Feature-Flags, Stack-Adressen, Crash-Typ und Nachricht; die Stack-Adressen werden kompakt mit base64 VLQ kodiert
  • Es werden weder JavaScript-/TypeScript-Quellcode noch Umgebungsvariablen gesendet; das Bun-Team erhält nur die für die Diagnose nötigen Zig/C++-Stack-Informationen und einige Metadaten

Warum Bun einen eigenen Crash-Reporter gebaut hat

  • Für Bun gibt es zum Zeitpunkt des Schreibens mehr als 2.600 offene GitHub-Issues, und einige davon sind besonders schwer zu reproduzieren und zu debuggen
  • Crash-Reporting-Dienste wie Sentry eignen sich gut für Apps und SaaS-Produkte, aber bei einem CLI-Tool wie Bun führen hochgeladene Core Dumps zu Problemen bei Datenschutz, Performance und Binärgröße
  • Bun v1.1.5 führt ein kleines neues Format für Crash-Reports aus Zig und C++ ein
    • Ein Crash-Report passt in eine URL von etwa 150 Byte
    • Er enthält keine personenbezogenen Daten

Was reine OS-Crash-Reporter nicht leisten

  • Einige Betriebssysteme wie macOS haben eingebaute Crash-Reporter, aber um sie sinnvoll zu nutzen, müssen in der Regel Debug-Symbole zusammen mit der Anwendung ausgeliefert werden
  • Debug-Symbole vergrößern die Distribution von Bun erheblich
    • Linux-Debug-Symbole: etwa 30 MB
    • macOS-Debug-Symbole: etwa 9 MB
    • Windows-.pdb-Dateien: mehr als 250 MB
  • Ein Beispiel für die Bun-Binärdatei schrumpft mit llvm-strip von 60M auf 51M
  • Tritt ein Crash ohne Debug-Symbole auf, enthält der Stack-Trace nur ??? und Adressen und ist damit kaum nützlich
  • Wegen ASLR (Address space layout randomization) enthalten Funktionsadressen zufällige Offsets, sodass sich Funktionsnamen nicht direkt rekonstruieren lassen

So funktioniert bun.report

  • Wenn in Bun v1.1.5 ein Crash oder Panic auftritt, gibt Bun zusammen mit Version, Plattform, Ausführungsargumenten, Speicherverbrauch und Crash-Meldung einen bun.report-Link aus
  • Öffnet der Nutzer den Link, erfolgt eine Weiterleitung zu einem vorausgefüllten GitHub-Issue-Formular
  • In der URL ist ein neu zugeordnetes Stack-Trace-Format kodiert
  • Der Server stellt anhand der in der URL enthaltenen Informationen die Stack-Adressen wieder her und wandelt sie in einen für das Bun-Team lesbaren Crash-Report um

Verfahren zur Umwandlung von Adressen in lesbare Stack-Traces

  • Funktionsadressen sind Pointer auf die Speicherposition, an der der Anwendungscode geladen wurde, und enthalten aus Sicherheitsgründen einen zufälligen Offset
  • Die Grundidee ist, von der Rohadresse die Basisadresse (base address) der Binärdatei abzuziehen, um eine relative Adresse zu erhalten
  • Die tatsächliche Implementierung ist wegen plattformspezifischer API-Unterschiede komplexer
    • Unter Windows wird GetModuleHandleExW mit dem Flag GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS verwendet; der Modul-Pointer dient als Basisadresse
    • Unter Linux werden mit dl_iterate_phdr die geladenen Module durchlaufen, und dl_phdr_info.dlpi_addr des Moduls, das die Adresse enthält, wird als Basisadresse verwendet
    • Unter macOS werden Module mit _dyld_image_count und _dyld_get_image_header durchlaufen, und mit _dyld_get_image_vmaddr_slide wird der ASLR-Slide ermittelt
      • In den resultierenden macOS-Adressen bleibt ein Image-Offset erhalten; bei Bun ist das 0x100000000
      • Um die URL kurz zu halten, wird dieser Offset entfernt, muss aber vor der Rückzuordnung mit llvm-symbolizer wieder addiert werden
  • Unter Linux und macOS verweist das erste Modul auf die Binärdatei der Hauptanwendung
  • Unter Windows lässt sich die Hauptbinärdatei durch Vergleich des Modulnamens mit peb.ProcessParameters.ImagePathName bestimmen
  • Bun lädt und parst Debug-Symbole nicht lokal, sondern überlässt das Demangling dem Server
    • Der Server kann Debug-Symbole cachen
    • Er kann Stack-Traces innerhalb weniger Sekunden demangeln
    • Gleichzeitig dient er als Link zum Erstellen eines neuen GitHub-Issues

Aufbau der bun.report-URL

  • In der bun.report-URL sind die folgenden Informationen kodiert
    • Platform: ein Zeichen für die Plattform, zum Beispiel w für x86_64 Windows und M für aarch64 macOS
    • Subcommand: ein Zeichen für Subcommands wie bun test, bun install oder bun run
    • Commit SHA: der Commit-SHA der aktuellen Bun-Version, der später zum Abrufen der Debug-Symbole verwendet wird
    • Feature Flags: Kennzeichen für verwendete APIs und Features vor dem Crash
    • Stack Trace Addresses: die im vorherigen Schritt berechneten Adressen
    • Crash Type: ein Zeichen für den Crash-Typ
    • Crash Message: eine je nach Crash-Typ unterschiedlich formatierte Nachricht
  • Die in der URL enthaltene Versionsnummer dient in erster Linie der Lesbarkeit für Menschen, nicht der eigentlichen Verarbeitung
  • Schon diese Informationen allein erlauben es, einige Crash-Merkmale manuell zu erkennen
    • Am Kennzeichen w lässt sich schnell erkennen, dass es sich um einen Windows-Crash handelt
    • Am A2 am Ende der Zeichenkette lässt sich ein Segmentation Fault identifizieren

VLQ-Kodierung für kurze URLs

  • Die Stack-Trace-Adressen werden als base64 Variable Length Quantity (VLQ) kodiert, um die URL kurz zu halten
  • VLQ stellt kleine Zahlen mit weniger Zeichen dar und kann dennoch große Zahlen kodieren
  • Dieselbe Technik wird auch in JavaScript-Source-Maps zum Speichern von Zeilennummern verwendet
  • Der Server dekodiert die VLQ-Werte wieder in relative Adressen, lädt mit Commit-Hash und Plattform die Debug-Symbole herunter und demangelt die Funktionsnamen mit llvm-symbolizer
  • Im Beispiel-Crash zeigt sich, dass eine Assertion in dirInfoCachedMaybeLog fehlgeschlagen ist, also in einem Teil des Modul-Resolver-Codes unter Windows

Kodierung von Feature-Flags

  • Die URL kodiert auch 64-Bit-Ganzzahlen, wobei jedes Bit dafür steht, ob ein bestimmtes Feature von Bun verwendet wurde
  • Diese Flags geben Hinweise darauf, welche APIs und Systeme zum Crash beigetragen haben könnten
    • Wenn .env-Dateien automatisch geladen werden, wird das Feature dotenv gesetzt
    • Wenn fetch() verwendet wird, wird das Feature fetch gesetzt
  • Bun verfolgt die Feature-Nutzung mit einem globalen Variablencontainer und markiert die Verwendung, indem innerhalb der jeweiligen API die entsprechende Zahl erhöht wird
  • Mit Zig-Compile-Time-Metaprogrammierung wird die Feature-Liste durchlaufen und für jedes Feature dynamisch eine Packed-Struct mit je 1 Bit erzeugt
  • Mit inline for kann die Feature-Liste zur Compile-Zeit iteriert werden, während das eigentliche Setzen der Bits zur Laufzeit erfolgt
  • Wird dem bestehenden Features-Struct ein neues Feature hinzugefügt, verarbeitet der Crash-Reporter es ohne zusätzliche Wiederholungsarbeit
  • Dasselbe Prinzip wäre auch mit C- oder Rust-Makros möglich, in der Bun-Implementierung wird Zig-comptime aber einfacher und besser lesbar eingesetzt

Unterschied zu Core Dumps

  • Core Dumps enthalten deutlich mehr Informationen, sind aber groß, nur mit Debug-Symbolen sinnvoll nutzbar und können viele sensible oder vertrauliche Daten enthalten
  • Der neue Report-Ansatz von Bun vermeidet Situationen, in denen JavaScript-/TypeScript-Quellcode, Umgebungsvariablen oder andere sensible Informationen gesendet werden
  • Statt standardmäßig alles zu übertragen, sendet er nur den Zig/C++-Stack-Trace und einige Details, die mit hoher Wahrscheinlichkeit für die Diagnose relevant sind
  • Wenn zusätzliche Informationen nötig sind, können diese separat vom Nutzer angefragt werden
  • Für das Bun-Team wird es damit einfacher, Crashs zu diagnostizieren, als wenn wie zuvor nur nicht zugeordnete Adressen übrig bleiben

Demo

  • Eine kleine Web-App zum Testen des Crash-Reporters steht unter bun.report bereit
  • Hängt man an jede beliebige Crash-Report-URL /view an, gelangt man zur Ansicht dieser Web-App

1 Kommentare

 
GN⁺ 2024-04-27
Hacker-News-Meinungen
  • Wenn der Grund für diesen Ansatz statt eines normalen Stacktraces darin liegt, mehrere Megabyte an Debug-Symbolen nicht ausliefern zu müssen, wirkt es so, als sei die bessere Option ignoriert worden, nur Funktionsnamen in die Debug-Tabellen aufzunehmen.
    Das ist eine deutlich bessere Methode, als einen Webservice nutzen zu müssen, um den Stacktrace anzusehen, und es ist nicht nur Theorie, sondern bereits in LLVM implementiert: https://clang.llvm.org/docs/UsersManual.html#cmdoption-gline...

    • Der Hauptgrund, diesen Ansatz statt eines normalen Stacktraces zu verwenden, ist nicht die Größe der Debug-Symbole, sondern dass nur sehr wenige Menschen genug Geduld haben, Crash-Reports in GitHub-Issues zu posten.
      Wenn eine URL den nötigen Inhalt fast automatisch ausfüllt, wird es einfach genug, und nur dann reichen Entwickler tatsächlich Crash-Reports ein. Die Größe ist ebenfalls wichtig, damit für Nutzer kein Nachteil entsteht, aber der Kern ist, den gesamten Prozess extrem einfach zu machen.
    • Formulierungen wie „die bessere Option ignoriert“ und „offensichtlich besser“ klingen etwas kategorisch. Vermutlich kannten sie diese Möglichkeit.
      In diesem Nutzungsszenario ist es kein großer Nachteil, dass man den Stacktrace über einen Webservice ansehen muss. Es ist fast dasselbe, wie ein Frontend-JavaScript-Bundle zu obfuskieren/minifizieren, Source Maps zu Sentry hochzuladen und dann Stacktraces aus den Browsern der Nutzer über Sentry wiederherzustellen. Der Nutzer wird diesen Stacktrace ohnehin nicht ansehen, und für mich ist es nicht unbequem, ihn mit Sentry anzusehen. Sonst hätte ich ihn überhaupt nicht sehen können.
    • Ohne den Diskussionskontext zu kennen und ohne zu wissen, welche Kompromisse es gab, klingt es etwas arrogant, mit „offensichtlich“ und „einfach“ zu kritisieren, man solle etwas anderes machen.
      Es gibt viele Arten, Alternativen vorzuschlagen.
    • Unter macOS/iOS gibt es auch die Möglichkeit, nur die LC_FUNCTION_STARTS-Section in das Mach-O-Binary aufzunehmen und auszuliefern.
      Auf diesen Plattformen findet die Symbolizierung so die Funktionsnamen von Systembibliotheken auch ohne vollständige Debug-Symbole.
    • Es kann trotzdem ziemlich groß werden. Ich persönlich halte es für wertvoll und nehme es normalerweise immer auf, aber die meiste Software macht das nicht.
  • Hervorragend und sehr kreativ. Viele Projekte könnten diesen Ansatz übernehmen. Der Kern ist, Stacktraces mit einem relativen Program Counter bezogen auf die ausführbare Datei/das Shared Object zu speichern.
    Soweit ich weiß, ist Bun statisch gelinkt; bei einem dynamisch gelinkten System müsste man jedem normalisierten Program Counter wohl eine kleine numerische Shared-Object-ID voranstellen.

    • Ganz neu ist das nicht. In Umgebungen wie Spielen, in denen Symbole nicht verteilt werden können, wird es häufig verwendet, wenn es auf Spieler-PCs zu Crashes kommt.
      Der Crash Reporter der Unreal Engine konnte zum Beispiel schon vor Jahren ein so einfaches Format senden und daraus für jeden Stackframe recht genaue Funktions-/Zeilennummern rekonstruieren. Meist bevorzugt man allerdings Minidumps, weil Stackvariablen zusätzliche Hinweise darauf geben können, was passiert ist.
  • Microsoft ist in solchen Dingen wirklich gut. In SQL Server wurden Minidumps verwendet, aus denen personenbezogene Daten entfernt waren; sie waren sehr klein und äußerst nützlich.
    Schon damals, vor 15 Jahren, waren vollständige Dumps eines produktiven SQL Servers riesige Dateien, die sich kaum übertragen ließen.

    • Mich würde interessieren, ob das für interne Dienste von Microsoft oder für Kundeninstallationen gedacht war. Falls Letzteres: Woher wussten sie, was personenbezogene Daten sind?
  • Nachdem ich den ersten Tweet zu Zig gesehen hatte, habe ich Bun jahrelang verfolgt und vor Kurzem angefangen, es zu nutzen; es funktioniert einfach gut, ohne großen Aufwand.

  • Bun ist ziemlich reizvoll. Ich habe es in ein paar kleinen Beispielprojekten ausprobiert; die Geschwindigkeit ist gut, und mir gefällt, dass Paketverwaltung und JavaScript-Runtime zusammengeführt sind.
    Allerdings nutze ich in den meisten ernsthaften Projekten Dependabot. Soweit ich weiß, ist Bun-Support in Dependabot in Arbeit oder wird zumindest in einigen Repository-Issues diskutiert, daher schiebe ich die Nutzung auf, bis der Support veröffentlicht ist.

    • Auch wir haben vor der Umstellung wegen der fehlenden Dependabot-Unterstützung gezögert, aber dann festgestellt, dass Renovate mit Bun funktioniert und vorerst ein ausreichender Ersatz ist.
      Keinerlei Reue. Die kumulierten Einsparungen durch die schnelleren Teile und die große Verbesserung der Developer Experience sind genau so wertvoll wie erwartet.
  • Viele werden gar nicht bemerken, wie viel Sorgfalt in solche Dinge geflossen ist. Ich finde es gut, weil es zeigt, wie sehr dem Bun-Team sein Handwerk am Herzen liegt.

  • Bun ist erstaunlich, aber als ich kürzlich mit Fastify einen HTTP/2-Server bauen wollte, ging es nicht.
    Es kam der Fehler node:http2 createServer is not yet implemented in Bun, und das Issue, auf das die Meldung verweist, betrifft eigentlich HTTP/2-Client-Support. Der Client-Support wurde bereits in v1.0.13 veröffentlicht: https://bun.sh/blog/bun-v1.0.13#http2-client-support
    Die NotImplementedError-Meldung sollte so geändert werden, dass sie auf das serverseitige Issue verweist: https://github.com/oven-sh/bun/issues/8823
    HTTP/2-Server-Support gehört zu den am höchsten priorisierten Feature Requests: https://github.com/oven-sh/bun/issues?q=is%3Aissue+is%3Aopen...
    Wenn dieses Feature kommt, dürften deutlich mehr Menschen zu Bun wechseln können.

    • Genau das ist derzeit der Zustand von Bun. Man wartet auf eine Implementierung, und wenn sie fertig ist, merkt man, dass noch eine andere API-Implementierung fehlt; dann wartet man wieder, sie kommt zwar, crasht aber in diversen Grenzfällen, und dann wartet man erneut.
      Bun ist in seinem Lebenszyklus noch in einer sehr frühen Phase. Trotzdem habe ich große Erwartungen an das Projekt.
  • Ich frage mich, ob es Leute gibt, die Bun tatsächlich verwenden. Ist es so gut wie erhofft?

    • In Produktion habe ich es noch nicht eingesetzt, aber für einmalige Skripte und Nebenprojekte war es sehr gut.
      In einer TypeScript-Node-Umgebung ts-node, ts-jest, ESM-Support, Top-Level await usw. einzurichten, ist unnötig umständlich. Neuere Node-Releases haben einige Unannehmlichkeiten reduziert, aber so einfach wie bun init ist es nicht. Auch die Bun-Shell-API nutze ich mit Freude: https://bun.sh/blog/the-bun-shell
    • Wenn man keinen REPL braucht und keine nativen Module verwenden will, ist es in Ordnung. Es gibt zwar einen REPL, aber nach jedem Update hat er immer mehr als 6 Sekunden Verzögerung, was sehr störend ist.
      Die Fehlermeldungen sind ebenfalls deutlich schlechter als bei Node. Ich habe es eine Zeit lang genutzt, aber heutzutage erledigt Node mit —loader tsx alles, was ich möchte, ohne Nachteile. Für einen einfachen Server, etwa mit WebSocket und wenn man sicher ist, dass keine nativen Module nötig sind, ist Bun eine Überlegung wert. Ich betreibe tatsächlich ein paar solche Dienste.
    • Ich habe direkt nach der Veröffentlichung von 1.0 damit angefangen und bin nicht zurückgegangen. Inzwischen setze ich es in allen Projekten ein.
    • Ich nutze es als Entwicklungs- und Test-Runner in einem Programmiersprachenprojekt mit etwa 15.000 Zeilen, und bisher gab es keine Bun-spezifischen Probleme.
      Die sofortige Startgeschwindigkeit überrascht mich immer noch.
    • Ich nutze es seit Kurzem und es ist sehr gut. Quality-of-Life-Verbesserungen wie sich nicht um die TypeScript-Kompilierung kümmern zu müssen, sind wirklich angenehm, und es ist schnell.
      Es fehlen noch ein paar Dinge, aber für mich ist es bereits besser als Node.
  • Dieser Artikel fühlt sich auch wie eine hervorragende Zig-Fallstudie an. Interessant.

  • Bun muss 37 Pakete herunterladen, bevor man den REPL nutzen kann. Ohne Internet kann man nicht einmal den REPL verwenden.
    Beim Ausführen von bun repl erscheint ein Fehler, dass der Download des Paketmanifests von bun-repl fehlgeschlagen ist. Kein großes Problem, aber ich hatte erwartet und mich ziemlich darauf gefreut, dass es ohne Installation sofort funktioniert, wenn man nur eine einzelne ausführbare Datei in den PATH legt.

    • Die REPL-Implementierung hatte bisher keine Priorität. Der aktuelle REPL ist das von der Community implementierte bun-repl-npm-Paket.
      Intern macht bun repl dasselbe wie bunx bun-repl.