事例紹介: 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 は、foo.jslib.js の 2 つです。前者はウェブサイトの開発者が作成したもので、後者は使用したフレームワークです。

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

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

  • [Sources] ツリー内のファイルとして。
  • [クイック オープン] ダイアログの結果。
  • ブレークポイントで一時停止したときとステップ中に、エラー スタック トレース内のマッピングされた呼び出しフレーム位置。

ソースマップに情報を追加して、どのソースがファースト パーティのコードか、サードパーティのコードかを識別できるようになりました。

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

新しい x_google_ignoreList フィールドには、sources 配列を参照するインデックスが 1 つ含まれています。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 フォルダの内容は「to ignore」としてマークされています。

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

Google のエンジニアが作成した 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 オブジェクトで「Async Stack Tagging 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 インスタンスを使用してタスクを実行します。

これらの変更は、pull リクエスト(#46693 および #46958)を通じて Angular の NgZone 0.11.8 に実装されました。

フレンドリーな通話フレーム

フレームワークでは多くの場合、プロジェクトの作成時にあらゆる種類のテンプレート言語からコードが生成されます。たとえば、HTML に見えるコードをプレーン JavaScript に変換し、最終的にブラウザ内で実行する Angular や JSX のテンプレートなどがあります。この種の生成された関数は、あまり親しみのない名前が付けられることがあります。圧縮された後の 1 文字の名前や、そうでなくても不明瞭な名前や見慣れない名前などがこれにあたります。

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

自動生成された関数名が表示されたスタック トレースのスクリーンショット。

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

Angular の Friendly Call Frame

Angular での呼び出しフレーム名の変更には継続的な作業が進められています。これらの改善は、時間の経過とともに徐々に反映される予定です。

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

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

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

今後に向けて

テスト パイロットとして Angular を使用して作業を検証できたのは、素晴らしい経験でした。フレームワーク デベロッパーの皆様からのご意見、拡張ポイントに関するフィードバックの提供をお待ちしています

検討すべき分野は他にもあります。具体的には、DevTools でのプロファイリング エクスペリエンスを改善する方法に関するものです。