2 ポイント 投稿者 GN⁺ 2024-04-27 | 1件のコメント | WhatsAppで共有
  • Bun v1.1.5では、クラッシュやパニックが発生しても個人情報を含まない約150バイトのURLだけでZig/C++のスタック情報を送れるbun.reportクラッシュレポーターが追加された
  • 既存のOSクラッシュレポーターやコアダンプは、デバッグシンボル・性能・個人情報・実行ファイルサイズの負担が大きく、CLIツールであるBunには適用しにくい
  • 新方式では、ASLRによって意味が分かりにくくなったアドレスをモジュール基準の相対アドレスに変換し、サーバーがコミットSHAとプラットフォームに合ったデバッグシンボルで関数名を復元する
  • URLにはプラットフォーム、サブコマンド、コミットSHA、機能フラグ、スタックアドレス、クラッシュ種別とメッセージが含まれ、スタックアドレスはbase64 VLQで短くエンコードされる
  • JavaScript/TypeScriptのソースコードや環境変数は送信せず、Bunチームが診断に必要なZig/C++スタック情報と一部のメタデータだけを送る

Bunが独自のクラッシュレポーターを作った理由

  • Bunには執筆時点で2,600件以上の未解決GitHub Issueがあり、一部のIssueは再現とデバッグが特に難しい
  • SentryのようなクラッシュレポーティングサービスはアプリやSaaS製品には適しているが、BunのようなCLIツールでコアダンプをアップロードすると、個人情報、性能、実行ファイルサイズの問題が大きくなる
  • Bun v1.1.5では、ZigとC++のクラッシュレポート向けに小さな新フォーマットを導入した
    • クラッシュレポートは約150バイトのURLに収まる
    • 個人情報は含まない

OSクラッシュレポーターだけでは足りない点

  • macOSなど一部のOSには組み込みのクラッシュレポーターがあるが、適切に活用するには通常、アプリケーションと一緒にデバッグシンボルを配布する必要がある
  • デバッグシンボルはBunの配布サイズを大きく増やす
    • Linuxのデバッグシンボルは約30MB
    • macOSのデバッグシンボルは約9MB
    • Windowsの.pdbファイルは250MB以上
  • Bun実行ファイルの例では、llvm-stripの前後で60Mから51Mに減少する
  • デバッグシンボルなしでクラッシュが発生すると、スタックトレースには???とアドレスだけが残り、有用性が低い
  • ASLR(Address space layout randomization) により関数アドレスにはランダムなオフセットが混ざるため、そのままでは関数名を復元できない

bun.reportの動作方式

  • Bun v1.1.5でクラッシュやパニックが発生すると、Bunはバージョン、プラットフォーム、実行引数、メモリ使用量、クラッシュメッセージとともにbun.reportリンクを出力する
  • ユーザーがリンクを開くと、事前入力済みのGitHub Issueフォームへリダイレクトされる
  • URL内には再マッピングされたスタックトレースがエンコードされている
  • サーバーはURLに含まれる情報をもとにスタックアドレスを復元し、Bunチームが読めるクラッシュレポートに変換する

アドレスを読めるスタックトレースに変換する手順

  • 関数アドレスはアプリケーションコードがメモリにロードされた位置を指すポインターであり、セキュリティ上ランダムなオフセットが含まれる
  • 基本的な考え方は、生のアドレスからバイナリの基準アドレス(base address) を引いて相対アドレスを得ること
  • 実際の実装は、プラットフォームごとのAPI差異によりさらに複雑になる
    • WindowsではGetModuleHandleExWGET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESSフラグを使い、モジュールポインターを基準アドレスとする
    • Linuxではdl_iterate_phdrでロード済みモジュールを走査し、アドレスを含むモジュールのdl_phdr_info.dlpi_addrを基準アドレスとして使う
    • macOSでは_dyld_image_count_dyld_get_image_headerでモジュールを走査し、_dyld_get_image_vmaddr_slideでASLRスライドを取得する
      • macOSの結果アドレスにはイメージオフセットが残っており、Bunの場合は0x100000000である
      • URLを短くするためにこのオフセットを取り除くが、llvm-symbolizerで再マッピングする前には再び足す必要がある
  • LinuxとmacOSでは、最初のモジュールがメインアプリケーションバイナリを指す
  • Windowsでは、モジュール名とpeb.ProcessParameters.ImagePathNameを比較してメインバイナリかどうか判断できる
  • Bunはローカルでデバッグシンボルをダウンロードして解析せず、サーバーにデマングリングを任せる
    • サーバーはデバッグシンボルをキャッシュできる
    • スタックトレースを数秒以内にデマングルできる
    • 同時に新しいGitHub Issueを開くリンクとしても機能する

bun.report URLの構造

  • bun.report URLには次の情報がエンコードされる
    • Platform: プラットフォームを表す1文字。たとえばwはx86_64 Windows、Mはaarch64 macOS
    • Subcommand: bun testbun installbun runなどのサブコマンドを表す1文字
    • Commit SHA: 現在のBunバージョンのコミットSHAで、後でデバッグシンボルを取得するために使われる
    • Feature Flags: クラッシュ前に使われたAPIや機能を示す印
    • Stack Trace Addresses: 前段階で計算したアドレス群
    • Crash Type: クラッシュ種別を表す1文字
    • Crash Message: クラッシュ種別によって形式が異なるメッセージ
  • URLに含まれるバージョン番号は、実際の処理よりも人間が読むための表示である
  • この情報だけでも、一部のクラッシュの特徴を手作業で把握できる
    • w識別子を見れば、Windowsのクラッシュだと素早く分かる
    • 文字列末尾のA2を見れば、セグメンテーションフォルトを識別できる

短いURLのためのVLQエンコーディング

  • スタックトレースアドレスは、URLを短く保つためにbase64 Variable Length Quantity(VLQ) 数値としてエンコードされる
  • VLQは小さな数値をより少ない文字で表現しつつ、大きな数値もエンコードできる
  • JavaScriptソースマップで行番号を保存するときにも同じ手法が使われる
  • サーバーはVLQ値を相対アドレスへデコードし直し、コミットハッシュとプラットフォームを使ってデバッグシンボルをダウンロードしたうえで、llvm-symbolizerで関数名をデマングルする
  • 例のクラッシュでは、Windowsのモジュールリゾルバーコードの一部であるdirInfoCachedMaybeLogでアサーションが失敗したことが分かる

機能フラグのエンコーディング

  • URLは64ビット整数もエンコードし、各ビットはBunの特定機能の使用有無に対応する
  • このフラグは、どのAPIやシステムがクラッシュに影響した可能性があるかのヒントになる
    • .envファイルが自動ロードされるとdotenv機能が設定される
    • fetch()を使うとfetch機能が設定される
  • Bunはグローバル変数コンテナで機能の使用有無を追跡し、各API内部で該当する数値を増やして使用を示す
  • Zigのコンパイル時メタプログラミングを使って機能リストを走査し、機能ごとに1ビットを使うpacked structを動的に生成する
  • inline forを使うと、コンパイル時に機能リストを走査しつつ、実際のビット設定は実行時に行える
  • 新機能を既存のFeatures structに追加すると、クラッシュレポーター側でも繰り返し記述なしで処理される
  • 同じ方式はCやRustのマクロでも可能だが、Bun側の実装ではZigのcomptimeがより単純で読みやすい方法として使われている

コアダンプとの違い

  • コアダンプははるかに多くの情報を含むが、サイズが大きく、デバッグシンボルがなければ有用性が低く、機微情報や機密情報を多く含む可能性がある
  • Bunの新しいレポート方式は、JavaScript/TypeScriptのソースコード、環境変数、その他の機微情報を送る状況を避ける
  • デフォルトですべてを送るのではなく、問題診断に必要である可能性が高いZig/C++スタックトレースといくつかの詳細情報だけを送る
  • 追加情報が必要な場合は、ユーザーに別途依頼できる
  • 以前のようにマッピングされていないアドレスだけが残る状況より、Bunチームがクラッシュを診断しやすくなる

デモ

  • クラッシュレポーターをテストできる小さなWebアプリがbun.reportで提供されている
  • どのクラッシュレポートURLでも末尾に/viewを付けると、そのWebアプリ画面へ移動する

1件のコメント

 
GN⁺ 2024-04-27
Hacker News の意見
  • 通常のスタックトレースの代わりにこの方式を使う理由が、数 MB のデバッグシンボルを配布しないためなら、より良い選択肢である、デバッグテーブルに関数名だけを入れる方式を無視しているように見える。
    スタックトレースを見るために Web サービスを使わなければならないより、ずっと良い方法で、これは単なる理論ではなく LLVM にすでに実装されている: https://clang.llvm.org/docs/UsersManual.html#cmdoption-gline...

    • 通常のスタックトレースよりこの方式を使う主な理由は、デバッグシンボルのサイズではなく、GitHub Issue にクラッシュレポートを投稿してくれるほど辛抱強い人がほとんどいないから。
      URL 1 つで必要な内容をほぼ自動入力できれば十分に簡単になり、そうして初めて開発者が実際にクラッシュレポートを投稿してくれる。サイズも重要で、ユーザーに不利益が出ないようにしたかったが、核心はプロセス全体を非常に簡単にすること。
    • 「より良い選択肢を無視した」「当然そちらの方が良い」という表現は少し断定的に聞こえる。おそらくその可能性は認識していたはず。
      このユースケースでは、スタックトレースを見るのに Web サービスが必要という点は大きな欠点ではない。フロントエンドの JavaScript バンドルを難読化/縮小し、ソースマップを Sentry にアップロードして、ユーザーのブラウザから来たスタックトレースを Sentry で復元して見るのとほぼ同じ。ユーザーはどうせそのスタックトレースを見ることはないし、自分も Sentry を使って見るのは不便ではない。そうでなければ、そもそも見ることすらできなかったのだから。
    • 議論の文脈も、どんなトレードオフがあったのかも知らない状態で、「当然」「ただ」別のことをすればよいと批判すると、少し傲慢に聞こえる。
      代替案を提案する方法はいろいろある。
    • macOS/iOS では、Mach-O バイナリに LC_FUNCTION_STARTS セクションだけを入れて配布する方法もある。
      これらのプラットフォームで、完全なデバッグシンボルなしでもシステムライブラリの関数名をシンボル化が見つけ出す方式。
    • それでもかなり大きくなり得る。個人的には価値があると思うので普通は必ず含めるが、ほとんどのソフトウェアはそうしていない。
  • 素晴らしく、とても創造的。多くのプロジェクトがこの方式を真似してもよさそう。要点は、実行ファイル/共有オブジェクトを基準にした相対プログラムカウンタでスタックトレースを残すこと。
    Bun は静的リンクされていると理解しているが、動的リンクのシステムなら、正規化されたプログラムカウンタごとに、小さな数値形式の共有オブジェクト ID を前に付ける必要があるだろう。

    • 完全に新しい方式ではない。ゲームのようにシンボルを配布できない環境で、プレイヤーの PC でクラッシュが起きるときによく使われる。
      例えば Unreal Engine のクラッシュレポーターは、何年も前からこうした単純な形式を送信でき、各スタックフレームからかなり正確な関数/行番号を復元できる。ただし通常は、スタック変数まであると何が起きたのか追加の手掛かりが得られるため、ミニダンプの方が好まれる。
  • Microsoft はこういうことが本当にうまい。SQL Server では個人情報を除去したミニダンプを使っていたが、とても小さく非常に有用だった。
    当時でも、15 年前にもかかわらず、本番環境の SQL Server の完全なダンプは移動が難しいほど巨大なファイルだった。

    • 気になるのだが、Microsoft 内部サービス向けだったのか、顧客の配布環境向けだったのか分からない。後者なら、何が個人識別情報なのかどうやって分かったのだろう?
  • Zig 関連の最初のツイートを見てから数年間 Bun を追っていて、最近使い始めたが、特に手間もなく普通にうまく動いている。

  • Bun はかなり魅力的。小さなサンプルプロジェクトをいくつかで試したが速度も良く、パッケージ管理と JavaScript ランタイムを組み合わせている点が気に入っている。
    ただし、本格的なプロジェクトの多くで Dependabot を使っている。Dependabot の Bun 対応は作業中、少なくともいくつかのリポジトリの Issue で議論中だと理解しているので、サポートがリリースされるまでは利用を控えている。

    • 自分たちも移行前に Dependabot 非対応のため迷っていたが、Renovate が Bun で動作し、当面は十分な代替になることが分かった。
      後悔はまったくない。速くなった部分が積み重なってもたらす節約効果と、開発者体験の大幅な向上は、期待どおりの価値がある。
  • こういうところにどれだけ多くの注意が払われているかに気づく人は多くないだろう。Bun チームが自分たちの craft をどれほど大事にしているかがよく分かって良い。

  • Bun は驚くほどすごいが、最近 Fastify で HTTP/2 サーバーを作ろうとしたらできなかった。
    node:http2 createServer is not yet implemented in Bun というエラーが出て、メッセージが指している Issue は実際には HTTP/2 クライアント対応に関するもの。クライアント対応は v1.0.13 ですでにリリースされている: https://bun.sh/blog/bun-v1.0.13#http2-client-support
    NotImplementedError メッセージはサーバー側の Issue を指すように変更されるべき: https://github.com/oven-sh/bun/issues/8823
    HTTP/2 サーバー対応は上位の機能リクエストの 1 つ: https://github.com/oven-sh/bun/issues?q=is%3Aissue+is%3Aopen...
    この機能が出れば、もっと多くの人が Bun に移行できそう。

    • それが今の Bun の状態。実装を待つと、終わった後には別の API 実装がさらに必要だと分かり、また待つと出てはくるが、さまざまな境界条件でクラッシュし、また待つ、という繰り返し。
      Bun はまだライフサイクル上あまりにも早い段階。それでもプロジェクトには大きく期待している。
  • 実際に Bun を使っている人がいるのか気になる。期待どおり良いのか?

    • まだ本番環境では使っていないが、使い捨てスクリプトやサイドプロジェクトにはとても良かった。
      TypeScript の Node 環境で ts-node、ts-jest、ESM 対応、トップレベル await などをセットアップするのは、必要以上に面倒。最近の Node リリースで一部の不便は減ったが、bun init ほど簡単ではない。bun shell API も楽しく使っている: https://bun.sh/blog/the-bun-shell
    • REPL が必要だったり、ネイティブモジュールを使う予定がなければ悪くない。REPL があるとはいえ、アップデートされるたびにいつも 6 秒以上遅延するので非常に不便。
      エラーメッセージも Node よりかなり悪い。しばらく使ってみたが、最近は Node に —loader tsx を付ければ、自分が求めることをすべてやってくれて欠点もない。単純なサーバー、例えば WebSocket を使い、ネイティブモジュールが不要だと確信できるなら、Bun を検討する価値はある。実際にそういうサービスをいくつか運用している。
    • 1.0 が出てすぐ使い始め、戻ることはなかった。今はすべてのプロジェクトに適用しているところ。
    • 約 1 万 5 千行のプログラミング言語プロジェクトで開発およびテストランナーとして使っているが、今のところ Bun 特有の問題はない。
      即座に起動する速さには今でも驚かされる。
    • 最近使っているが、とても良い。TypeScript のコンパイルを気にしなくてよいといった生活の質の改善が本当に便利で、速度も速い。
      まだ足りないものはあるが、自分にとってはすでに Node より良い。
  • この記事は Zig のケーススタディとしても素晴らしく感じる。興味深い。

  • Bun は REPL を使う前に 37 個のパッケージをダウンロードしなければならない。インターネットがなければ REPL も使えない。
    bun repl 実行時に bun-repl パッケージのマニフェストのダウンロードに失敗するというエラーが残る。大きな問題ではないが、単一の実行ファイルを PATH に入れるだけで、インストールなしにすぐ動くことを期待していて、かなり楽しみにしていた。

    • まだ REPL 実装を優先していない。現在の REPL はコミュニティが実装した bun-repl npm パッケージ
      内部的には bun replbunx bun-repl と同じことをしている。