事例紹介: 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 コードを生成します。このコードは最終的に、ブラウザが読み込んで実行する JavaScript コードに変換されます。

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

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

今後に向けて

Angular をテストパイロットとして使用して、作業を確認できたのは素晴らしい経験でした。フレームワーク デベロッパーの皆様から、これらの拡張ポイントに関するフィードバックをお寄せください。

他にも検討すべき領域はありますが、特に、DevTools でのプロファイリングの使い勝手を改善する方法について説明します。