デバッグ エクスペリエンスの向上
ここ数か月間、Chrome DevTools チームは Angular チームと共同で、Chrome DevTools のデバッグ機能の改善に取り組みました。両チームの人々は協力し、デベロッパーがオーサリングの観点からウェブ アプリケーションのデバッグとプロファイリングを行えるようにするための措置を講じました。つまり、ソース言語とプロジェクト構造の観点から、自身になじみのある関連情報へのアクセスを確保しました。
この投稿では、これを実現するために Angular と Chrome DevTools でどのような変更が必要かについて、その裏側をご覧ください。これらの変更の一部は Angular で実証されていますが、他のフレームワークにも適用できます。Chrome DevTools チームでは、他のフレームワークでも新しいコンソール API とソースマップ拡張ポイントを採用して、ユーザーにより優れたデバッグ エクスペリエンスを提供できるように推奨しています。
リスティングを無視するコード
作成者は通常、Chrome DevTools を使用してアプリケーションをデバッグする際、その下のフレームワークや node_modules
フォルダに埋め込まれている依存関係ではなく、自分のコードだけを確認する必要があります。
これを実現するために、DevTools チームは x_google_ignoreList
というソースマップの拡張機能を導入しました。この拡張機能は、フレームワーク コードやバンドラが生成したコードなどのサードパーティのソースを識別するために使用されます。フレームワークでこの拡張機能を使用すると、作成者は表示したくないコードやステップ実行したくないコードを自動的に回避できます。事前に手動で構成する必要はありません。
実際に Chrome DevTools では、スタック トレース、ソースツリー、クイック オープン ダイアログなどで特定されたコードを自動的に非表示にし、デバッガのステップ実行と再開の動作を改善できます。
x_google_ignoreList
ソースマップ拡張機能
ソースマップでは、新しい x_google_ignoreList
フィールドが sources
配列を参照し、そのソースマップ内のすべての既知のサードパーティ ソースのインデックスをリストします。ソースマップを解析する際、Chrome DevTools はこれを使用して、コードのどのセクションを無視リストに含めるべきかを判断します。
以下は、生成されたファイル out.js
のソースマップです。出力ファイルの生成に影響を与えた元の sources
は、foo.js
と lib.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.ts
、lib2.coffee
、hmr.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
非同期スタックタグ付けを使用しないと、フレームワークによって複雑な方法で非同期に実行されるコードのスタック トレースは、スケジュールされたコードとの接続なしで表示されます。
非同期スタックタグ付けを使用すると、このコンテキストを提供できます。スタック トレースは次のようになります。
これを実現するには、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 でのプロファイリング エクスペリエンスを改善する方法に関するものです。