デバッグ エクスペリエンスの改善
ここ数か月にわたり、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
のソースマップです。出力ファイルの生成には、foo.js
と lib.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.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
フォルダの内容が「無視」としてマークされています。
これは、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
非同期スタックタグ付けを使用しない場合、フレームワークによって複雑な方法で非同期で実行されるコードのスタック トレースは、スケジュール設定されたコードに接続されずに表示されます。
非同期スタックタグ付けを使用すると、このコンテキストを指定できます。スタック トレースは次のようになります。
これを実現するには、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 でのプロファイリングのエクスペリエンスを向上させる方法について説明します。