事例紹介: DevTools による改善された Angular デバッグ

デバッグ機能の改善

ここ数か月にわたり、Chrome DevTools チームは Angular チームと共同で、Chrome DevTools のデバッグ エクスペリエンスの改善に取り組みました。両チームのスタッフが協力し、デベロッパーがオーサリングの観点からウェブ アプリケーションのデバッグとプロファイリングを行えるようにするための措置を講じました。具体的には、ソース言語とプロジェクト構造の観点から、使い慣れた関連性の高い情報にアクセスできるようにしました。

この記事では、この実現に必要な Angular と Chrome DevTools の変更について詳しく説明します。これらの変更の一部は Angular で説明されていますが、他のフレームワークにも適用できます。Chrome DevTools チームは、他のフレームワークでも新しいコンソール API とソースマップ拡張ポイントを採用し、ユーザーに優れたデバッグ エクスペリエンスを提供することを推奨しています。

無視リストのコード

Chrome DevTools を使用してアプリケーションをデバッグする場合、通常は、下位のフレームワークのコードや node_modules フォルダに隠れている依存関係ではなく、自分のコードのみを表示します。

そのために、DevTools チームは ソースマップの拡張機能である x_google_ignoreList を導入しました。この拡張機能は、フレームワーク コードやバンドル生成コードなどのサードパーティ ソースを特定するために使用されます。フレームワークがこの拡張機能を使用する場合、事前に手動で設定しなくても、表示またはステップスルーしたくないコードが自動的に除外されるようになりました。

実際には、Chrome DevTools では、スタック トレース、[ソース] ツリー、クイック開くダイアログで、非表示にすべきコードを自動的に非表示にできます。また、デバッガのステップ実行と再開の動作も改善されます。

変更前と変更後の DevTools を示すアニメーション GIF。後者の画像では、DevTools がツリーに作成コードを表示し、[クイック開く] メニューにフレームワーク ファイルが表示されなくなったこと、右側に非常にクリーンなスタック トレースが表示されていることに注目してください。

x_google_ignoreList ソースマップ拡張機能

ソースマップでは、新しい x_google_ignoreList フィールドは sources 配列を参照し、そのソースマップ内の既知のサードパーティ ソースのインデックスをすべて一覧表示します。ソースマップを解析する際、Chrome DevTools はこれを使用して、コードのどのセクションを無視すべきかを判断します。

以下は、生成されたファイル out.js のソースマップです。出力ファイルの生成に貢献した元の sources は 2 つあります(foo.jslib.js)。前者はウェブサイト デベロッパーが作成したもので、後者はデベロッパーが使用したフレームワークです。

{
  "version" : 3,
  "file": "out.js",
  "sourceRoot": "",
  "sources": ["foo.js", "lib.js"],
  "sourcesContent": ["...", "..."],
  "names": ["src", "maps", "are", "fun"],
  "mappings": "A,AAAB;;ABCDE;"
}

sourcesContent はこれらの元のソースの両方に含まれており、Chrome DevTools ではデフォルトでこれらのファイルがデバッガ全体に表示されます。

  • ソースツリー内のファイルとして。
  • クイック開くダイアログの結果として。
  • ブレークポイントで一時停止している間やステップ実行中に、エラー スタック トレース内のマッピングされたコールフレーム位置として。

ソースマップには、ソースがファーストパーティ コードかサードパーティ コードかを識別するための追加情報が 1 つ含まれるようになりました。

{
  ...
  "sources": ["foo.js", "lib.js"],
  "x_google_ignoreList": [1],
  ...
}

新しい x_google_ignoreList フィールドには、sources 配列を参照する単一のインデックス(1)が含まれています。これは、lib.js にマッピングされたリージョンが、無視リストに自動的に追加されるサードパーティ コードであることを指定します。

次の複雑な例では、インデックス 2、4、5 で、lib1.tslib2.coffeehmr.js にマッピングされたリージョンがすべてサードパーティ コードであり、無視リストに自動的に追加されるように指定しています。

{
  ...
  "sources": ["foo.html", "bar.css", "lib1.ts", "baz.js", "lib2.coffee", "hmr.js"],
  "x_google_ignoreList": [2, 4, 5],
  ...
}

フレームワークまたはバンドルのデベロッパーは、Chrome DevTools の新しい機能にフックできるように、ビルドプロセス中に生成されたソースマップにこのフィールドが含まれていることを確認してください。

Angular の x_google_ignoreList

Angular v14.1.0 以降、node_modules フォルダと webpack フォルダの内容は「無視」としてマークされています。

これは、webpack の Compiler モジュールに接続するプラグインを作成することで、angular-cli を変更することで実現しました。

エンジニアが作成した webpack プラグインは、PROCESS_ASSETS_STAGE_DEV_TOOLING ステージにフックを作成し、webpack が生成してブラウザが読み込む最終的なアセットのソースマップの x_google_ignoreList フィールドにデータを入力します。

const map = JSON.parse(mapContent) as SourceMap;
const ignoreList = [];

for (const [index, path] of map.sources.entries()) {
  if (path.includes('/node_modules/') || path.startsWith('webpack/')) {
    ignoreList.push(index);
  }
}

map[`x_google_ignoreList`] = ignoreList;
compilation.updateAsset(name, new RawSource(JSON.stringify(map)));

リンクされたスタック トレース

スタック トレースは「なぜこの状態になったのか」という質問に対する回答を提供しますが、多くの場合、これはマシンの視点からのものであり、必ずしもデベロッパーの視点や、アプリケーション ランタイムのメンタルモデルと一致するものではありません。これは、一部のオペレーションが後で非同期的に実行されるようにスケジュール設定されている場合に特に当てはまります。そのようなオペレーションの「根本原因」やスケジューリング側を知ることは興味深いことですが、それはまさに非同期スタック トレースの一部にはならないものです。

V8 には、setTimeout などの標準のブラウザ スケジューリング プリミティブが使用されている場合に、このような非同期タスクを追跡するメカニズムが内部に用意されています。これらのケースではデフォルトで行われるため、デベロッパーはすでに検査できます。しかし、より複雑なプロジェクトでは、それほど簡単ではありません。特に、ゾーン トラッキングやカスタムタスク キューイングを実行するフレームワークや、更新を経時的に実行される複数の作業単位に分割するフレームワークなど、より高度なスケジューリング メカニズムを備えたフレームワークを使用する場合はなおさらです。

これに対処するため、DevTools は console オブジェクトに「非同期スタック タグ付け API」と呼ばれるメカニズムを公開します。これにより、フレームワーク デベロッパーは、オペレーションがスケジュールされる場所と、これらのオペレーションが実行される場所の両方をヒントとして提供できます。

Async Stack Tagging API

非同期スタック タグ付けがないと、フレームワークによって複雑な方法で非同期的に実行されるコードのスタック トレースは、スケジュールされたコードとの関連性なしに表示されます。

非同期で実行されたコードのスタック トレース。スケジュールされた日時に関する情報はありません。「requestAnimationFrame」から始まるスタック トレースのみが表示され、スケジュールされたときの情報は保持されません。

非同期スタック タグ設定を使用すると、このコンテキストを提供できます。スタック トレースは次のようになります。

非同期で実行されたコードのスタック トレース(スケジュールされた時刻に関する情報を含む)。以前とは異なり、スタック トレースには「businessLogic」と「schedule」が含まれています。

これを実現するには、Async Stack Tagging API が提供する console.createTask() という新しい console メソッドを使用します。シグネチャは次のとおりです。

interface Console {
  createTask(name: string): Task;
}

interface Task {
  run<T>(f: () => T): T;
}

console.createTask() を呼び出すと、Task インスタンスが返されます。このインスタンスは、後で非同期コードの実行に使用できます。

// Task Creation
const task = console.createTask(name);

// Task Execution
task.run(f);

非同期オペレーションはネストすることもできます。スタック トレースには「根本原因」が順番に表示されます。

タスクは任意の数回実行でき、ワーク ペイロードは実行ごとに異なる場合があります。スケジュール設定サイトのコールスタックは、タスク オブジェクトがガベージ コレクションされるまで保持されます。

Angular の Async Stack Tagging API

Angular では、非同期タスク間で保持される Angular の実行コンテキストである NgZone が変更されました。

タスクのスケジュール設定時に、利用可能な場合は console.createTask() を使用します。生成された Task インスタンスは、今後の使用のために保存されます。タスクを呼び出すと、NgZone は保存されている Task インスタンスを使用してタスクを実行します。

これらの変更は、プルリクエスト #46693#46958 を通じて Angular の NgZone 0.11.8 に反映されました。

フレンドリーな呼び出しフレーム

フレームワークは、プロジェクトの構築時にさまざまなテンプレート言語からコードを生成します。たとえば、Angular や JSX テンプレートでは、HTML に似たコードを、最終的にブラウザで実行される単純な JavaScript に変換します。生成されたこのような関数の中には、あまり親しみのない名前が付けられることもあります。たとえば、圧縮された後に 1 文字の名前であったり、そうでなくても不明瞭な名前やなじみのない名前であったりします。

Angular では、スタック トレース内に AppComponent_Template_app_button_handleClick_1_listener などの名前のコールフレームが表示されることは珍しくありません。

自動生成された関数名を含むスタック トレースのスナップショット。

この問題に対処するため、Chrome DevTools ではソースマップを使用してこれらの関数の名前変更がサポートされるようになりました。ソースマップに関数スコープの開始(パラメータリスト内の左括弧)の名前エントリがある場合、コールフレームはその名前をスタックトレース内に表示します。

Angular のフレンドリー コールフレーム

Angular での呼び出しフレームの名前の変更は現在進行中です。これらの改善は、今後徐々に導入される予定です。

作成者が記述した HTML テンプレートを解析する際、Angular コンパイラは TypeScript コードを生成します。この TypeScript は、最終的にブラウザが読み込んで実行する JavaScript コードにトランスパイルされます。

このコード生成プロセスの一環として、ソースマップも作成されます。現在、ソースマップの「名前」フィールドに関数名を含め、生成されたコードと元のコード間のマッピングでそれらの名前を参照する方法を検討しています。

たとえば、イベント リスナーの関数が生成され、その名前がわかりにくいか、圧縮中に削除された場合、ソースマップでは、この関数のわかりやすい名前を「names」フィールドに含めることができ、関数スコープの先頭のマッピングでこの名前(つまり、パラメータリスト内の左括弧)を参照できるようになりました。Chrome DevTools では、これらの名前を使用して、スタック トレース内の呼び出しフレームの名前を変更します。

今後に向けて

作業を検証するためのテスト パイロットとして Angular を利用したことは、すばらしい経験になりました。フレームワーク デベロッパーからのご意見や、これらの拡張機能についてのフィードバックをお待ちしています。

調査してみたい分野は他にもたくさんあります。特に、DevTools でのプロファイリングの使い勝手を改善する方法について説明します。