WebAssembly のデバッグの高速化

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

Chrome Dev Summit 2020 では、ウェブ版 WebAssembly アプリケーションに対する Chrome のデバッグ サポートを初めてデモしました。それ以来、チームは大規模なアプリケーションや大規模なアプリケーションでも開発者エクスペリエンスをスケールできるように、多大なエネルギーを投資してきました。この投稿では、さまざまなツールに追加された(または実際に機能した)ノブとその使い方をご紹介します。

スケーラブルなデバッグ

2020 年の投稿の続きから始めましょう。以下は当時確認していた例です。

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

まだかなり小さな例であり、大規模なアプリケーションで目にする実際の問題は見られないかもしれませんが、新機能の説明はできます。すばやく簡単に設定でき、ご自身でもお試しいただけます。

前回の投稿では、この例のコンパイルとデバッグの方法について説明しました。ここでもう一度 //performance// も確認しておきましょう。

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

このコマンドを実行すると、3MB の Wasm バイナリが生成されます。そして、その大部分はデバッグ情報です。これは llvm-objdump ツール [1] で確認できます。次に例を示します。

$ 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

この出力には、生成された Wasm ファイル内のすべてのセクションが表示されます。そのほとんどは標準の WebAssembly セクションですが、名前が .debug_ で始まるカスタム セクションもいくつかあります。バイナリにはデバッグ情報が含まれています。すべてのサイズを合計すると、デバッグ情報が 3 MB のファイルの約 2.3 MB を占めていることがわかります。また、emcc コマンドを time すると、このマシンでは実行に約 1.5 秒かかったことがわかります。これらの数字はちょっとしたベースラインになりますが、非常に小さいため、誰も目を向けることはないでしょう。しかし実際のアプリでは、デバッグ バイナリのサイズが GB 単位で簡単に到達し、ビルドに数分かかる場合があります。

Binaryen をスキップしています

Emscripten を使用して Wasm アプリケーションをビルドする場合、最終的なビルドステップの一つに Binaryen オプティマイザーの実行があります。Binaryen は、WebAssembly のようなバイナリを最適化および合法化するコンパイラ ツールキットです。ビルドの一環として Binaryen を実行すると、かなりの費用がかかりますが、必要なのは特定の条件下のみです。デバッグビルドでは、Binaryen パスが不要であれば、ビルド時間を大幅に短縮できます。最もよく使用される Binaryen パスは、64 ビット整数値を含む関数シグネチャを正当化する場合です。-sWASM_BIGINT を使用して WebAssembly BigInt 統合にオプトインすることで、これを回避できます。

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

目安として、-sERROR_ON_WASM_CHANGES_AFTER_LINK フラグを設定しました。Binaryen の実行時やバイナリの予期せぬ書き換えを検出できます。そうすることで、より早い段階に進むことができます。

上記の例はかなり小さなものですが、Binaryen をスキップした場合の影響は確認できます。time によると、このコマンドの実行時間は 1 秒未満なので、以前より 0.5 秒速くなっています。

高度な調整

入力ファイルのスキャンをスキップしています

通常、Emscripten プロジェクトをリンクする場合、emcc はすべての入力オブジェクト ファイルとライブラリをスキャンします。これは、JavaScript ライブラリ関数とプログラム内のネイティブ シンボルとの間に正確な依存関係を実装するために行われます。大規模なプロジェクトでは、(llvm-nm を使用して)入力ファイルをスキャンすることで、リンク時間が大幅に長くなる可能性があります。

代わりに -sREVERSE_DEPS=all を指定して、JavaScript 関数の考えられるすべてのネイティブ依存関係を含めるよう emcc に指示することもできます。コードサイズのオーバーヘッドは小さくなりますが、リンク時間を短縮でき、デバッグビルドに役立ちます。

この例のような小規模なプロジェクトでは、実質的な違いはありませんが、プロジェクト内に数百、場合によっては数千のオブジェクト ファイルがある場合は、リンク時間を大幅に短縮できます。

「name」セクションの削除

大規模なプロジェクト、特に C++ テンプレートを多く使用するプロジェクトでは、WebAssembly の「name」セクションが非常に大きくなる可能性があります。この例では、全体のファイルサイズのごく一部に過ぎません(上記の llvm-objdump の出力を参照)が、場合によっては非常に大きくなることがあります。アプリケーションの「name」セクションが非常に大きく、dwarf デバッグ情報がデバッグのニーズに十分である場合は、「name」セクションを除去した方がよい場合があります。

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

これにより、WebAssembly の「name」セクションが削除され、DWARF デバッグ セクションは保持されます。

デバッグの分断

デバッグデータが大量にあるバイナリは、ビルド時間だけでなくデバッグ時間も負荷します。デバッガは、「ローカル変数 x の型は何ですか?」などのクエリにすばやく応答できるように、データを読み込んでインデックスを作成する必要があります。

デバッグ分割を使用すると、バイナリのデバッグ情報を 2 つの部分に分割できます。1 つはバイナリ内に残り、もう 1 つは独立した、いわゆる DWARF オブジェクト(.dwo)ファイルに含まれています。これを有効にするには、-gsplit-dwarf フラグを Emscripten に渡します。

$ 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

以下に、デバッグデータなし、デバッグデータ、デバッグデータとデバッグ分割の両方を使用してコンパイルした場合に、さまざまなコマンドと生成されるファイルを示します。

さまざまなコマンドと、生成されるファイルを

DWARF データを分割すると、デバッグデータの一部はバイナリとともに存在しますが、大部分は mandelbrot.dwo ファイルに配置されます(上の図を参照)。

mandelbrot の場合、ソースファイルは 1 つだけですが、通常はプロジェクトはこれよりも大きく、複数のファイルが含まれます。デバッグの個々の分割では、.dwo ファイルが生成されます。現在のベータ版デバッガ(0.1.6.1615)でこの分割デバッグ情報を読み込むには、次のように、これらすべてをいわゆる DWARF パッケージ(.dwp)にバンドルする必要があります。

$ emdwp -e mandelbrot.wasm -o mandelbrot.dwp

2 つのファイルを DWARF パッケージにバンドル

個々のオブジェクトから DWARF パッケージを構築すると、追加のファイルを 1 つ提供するだけでよいという利点があります。今後のリリースでは、個々のオブジェクトもすべて読み込む作業を進めています。

DWARF 5 の内容

お気づきかもしれませんが、上記の emcc コマンドに -gdwarf-5 という別のフラグを差し込んでいます。また、DWARF シンボルのバージョン 5(現在はデフォルトではありません)を有効にすることも、デバッグを迅速に開始するうえで効果的です。これにより、デフォルトのバージョン 4 で除外された特定の情報がメインバイナリに保存されます。具体的には、メインバイナリからのみ、すべてのソースファイルを特定できます。これにより、完全なシンボルデータの読み込みや解析を行わずに、完全なソースツリーの表示やブレークポイントの設定といった基本的なアクションをデバッガで行えるようになります。これにより、分割シンボルを使用したデバッグが大幅に高速化されるため、常に -gsplit-dwarf-gdwarf-5 コマンドライン フラグを一緒に使用しています。

DWARF5 デバッグ形式を使用すると、別の便利な機能にもアクセスできます。これにより、-gpubnames フラグを渡すと生成されるデバッグデータに名前インデックスが導入されます。

$ 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

デバッグ セッション中、シンボルのルックアップは多くの場合、変数や型を探すときなど、名前でエンティティを検索することによって行われます。名前インデックスは、名前を定義するコンパイル単位を直接指すようにすることで、この検索を高速化します。名前インデックスがないと、探している名前付きエンティティを定義する正しいコンパイル単位を見つけるには、デバッグデータ全体を網羅する必要があります。

デバッグデータを確認すると、

llvm-dwarfdump を使用すると、DWARF データをプレビューできます。試してみましょう。

llvm-dwarfdump mandelbrot.wasm

これにより、デバッグ情報がある「コンパイル ユニット」(おおまかに言えばソースファイル)の概要がわかります。この例では、mandelbrot.cc のデバッグ情報のみがあります。一般情報を見ると、スケルトン ユニットがあることがわかります。これは、このファイルに不完全なデータがあり、残りのデバッグ情報を含む別の .dwo ファイルがあることを意味します。

mandelbrot.wasm とデバッグ情報

このファイル内の他のテーブルも参照できます。たとえば、Wasm バイトコードと C++ 行のマッピングを示す行テーブルです(llvm-dwarfdump -debug-line を使用してみてください)。

別の .dwo ファイルに含まれるデバッグ情報を確認することもできます。

llvm-dwarfdump mandelbrot.dwo

mandelbrot.wasm とデバッグ情報

要約: デバッグ分割を使用するメリットは何ですか?

大規模なアプリケーションを扱う場合、デバッグ情報を分割することにはいくつかの利点があります。

  1. リンクの高速化: リンカーがデバッグ情報全体を解析する必要がなくなりました。リンカーは通常、バイナリ内にある DWARF データ全体を解析する必要があります。リンカーはデバッグ情報の大部分を個別のファイルに取り除くことで、小さなバイナリを処理するため、リンク時間が短縮されます(特に大規模なアプリケーションに当てはまります)。

  2. デバッグの高速化: 一部のシンボル ルックアップで、.dwo/.dwp ファイル内の追加のシンボルの解析がデバッガでスキップされるようになりました。一部のルックアップ(Wasm から C++ ファイルの行マッピングでのリクエストなど)では、追加のデバッグデータを調べる必要はありません。これにより、追加のデバッグデータを読み込んで解析する必要がなくなり、時間を節約できます。

1: システムに llvm-objdump の最新バージョンがなく、emsdk を使用している場合は、emsdk/upstream/bin ディレクトリにあります。

プレビュー チャネルをダウンロードする

Chrome CanaryDevBeta を既定の開発ブラウザとして使用することをご検討ください。これらのプレビュー チャンネルでは、最新の DevTools 機能にアクセスしたり、最先端のウェブ プラットフォーム API をテストしたり、ユーザーが実際に体験する前にサイト上の問題を検出したりできます。

Chrome DevTools チームへのお問い合わせ

投稿内の新機能や変更点、または DevTools に関するその他のことについて話し合うには、次のオプションを使用します。

  • crbug.com からご提案やフィードバックをお送りください。
  • DevTools の問題を報告するには、DevTools でその他のオプション アイコン その他   > [ヘルプ] > [DevTools の問題を報告する] を選択します。
  • @ChromeDevTools にツイートします。
  • 「DevTools の新機能」の YouTube 動画または DevTools のヒントの YouTube 動画でコメントを残してください。