事例紹介: 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 のソースマップです。出力ファイルの生成には、foo.jslib.js という 2 つの元の sources が寄与しています。前者はウェブサイトの開発者が記述したもので、後者は使用したフレームワークです。

{
  "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": ["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 の変更によって実現しました。

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

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 では、NgZone(非同期タスク全体で維持される Angular の実行コンテキスト)が変更されました。

タスクのスケジュールを設定する際は、可能な場合に 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 のフレンドリー コールフレーム

Angular での呼び出しフレームの名前の変更は現在進行中です。これらの改善は、徐々に改善していく予定です。

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

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

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

今後に向けて

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

調査してみたい分野は他にもたくさんあります。特に、DevTools でのプロファイリングのエクスペリエンスを向上させる方法について説明します。