Chrome DevTools を使った非同期 JavaScript のデバッグ

HTML5 Rocks

はじめに

JavaScript をユニークなものにしている強力な特徴の1つはそのコールバック関数を通して非同期に動作させられることです。非同期コールバックを割り当てるとイベントドリブンなコードを書くことができますが、JavaScript は直線的には実行されないため、バグを追い掛けるのがやっかいです。

幸いなことに、現在、Chrome Canary のデベロッパー ツールで非同期 JavaScript の完全なコールスタックを見ることができます!

A quick teaser overview of async call stacks
非同期コールスタックの外観です。

(このデモの流れはすぐ後に解説します。)

デベロッパー ツールで非同期コールスタック機能を有効にすると、Web アプリのいろんなポイントでの状態を深掘って見ることができます。イベントリスナーや setIntervalsetTimeoutXMLHttpRequest、プロミス、requestAnimationFrameMutationObservers やそのほかの完全なスタックトレースを追うことができます。

スタックトレースを追うと、実行時の特定のポイントでのあらゆる変数の値を分析することもできます。ウォッチ式のタイムマシーンみたいに動作します。

この機能を有効にして、これらシナリオをいくつか見てみましょう。

Chrome Canary で非同期デバッギングを有効にする

Chrome Canary(ビルド 35 以降)でこの新しい機能を有効にして試してみましょう。Chrome Canary のデベロッパー ツールの「Sources」パネルに行きます。

右側にある「Call Stack」パネルの隣に、「Async」という新しいチェックボックスがあります。チェックボックスをトグルして非同期デバッギングをオンやオフにします。(一度オンにしたら、二度とオフにしたいと思わないとしてもね。)

Toggle the async feature on or off

遅延されたタイマーイベントと XHR レスポンスを取得する

以前きっと、このような表示を Gmail で目にしたことがあるでしょう:

Gmail retrying to send an email

リクエストを送信するのに問題があった(サーバに問題があったりクライアントサイドのネットワーク接続の問題だったり)場合、Gmail は短時間送信を中断した後、自動的にメッセージの再送信を試みます。

非同期コールスタックがどのように遅延されたタイマーイベントと XHR レスポンスを分析するのに役立つかを理解するために、私は Gmail のモックサンプルでフローを再作成してみました。完全な JavaScript コードは上のリンクにありますが、フローは下記のようになります:

Flow chart of mock Gmail example
上のダイアグラムで、青くハイライトされたメソッドが非同期で動作するので、この新しいデベロッパー ツールの機能を利用する上で重要なポイントになります。

以前のバージョンのデベロッパー ツールで Call Stack パネルを見ると、postOnFail() の中のブレークポイントは postOnFail() がどこから呼ばれたのかについて少しだけしか情報を表示してくれません。しかし、非同期スタックを有効にしたときとの違いを見てください:

従来
Breakpoint set in mock Gmail example without async call stacks
非同期が有効になっていないコールスタックパネル。

ここで、postOnFail() が AJAX のコールバックで開始していることが分かりますが、それ以上の情報はありません。

現在
Breakpoint set in mock Gmail example with async call stacks
非同期が有効になっているコールスタックパネル。

ここで、XHR が submitHandler() から開始していることが分かり、submitHandler() は script.js のクリックハンドラから開始していることが分かります。ナイス!

非同期コールスタックを有効にすると、リクエストが(送信ボタンをクリックした後に発生する)submitHandler() もしくは(setTimeout() での遅延の後に発生する)retrySubmit() のどちらから開始されているか全体のコールスタックを見ることができます:

submitHandler()
Breakpoint set in mock Gmail example with async call stacks
retrySubmit()
Another breakpoint set in mock Gmail example with async call stacks

式を非同期に監視する

完全なコールスタックを行き来していると、あなたが監視している式がそのタイミングでの状態を反映するために更新されていきます!

An example of using watch expressions with aysnc call stacks

過去のスコープからコードを評価する

単純に式を監視することに加えて、デベロッパー ツールの JavaScript コンソールパネルの過去のスコープからのコードとやり取りすることができます。

あなたがドクター・フーで、ターディスに乗る前と現在との時間を比べるのに少し助けを必要としていると想像してみてください。デベロッパー ツールのコンソールから、さまざまな実行ポイントで値を評価し、保存し、計算することが簡単にできます。

An example of using the JavaScript console with aysnc call stacks
あなたのコードをデバッグするために JavaScript コンソールを非同期コールスタックと合わせて使いましょう。上のデモはここにあります。

デベロッパー ツールの中で式を操作することで、ソースコードを編集してブラウザをリフレッシュしなければいけなかった時間を節約できます。

近日公開:プロミス解決の連鎖を解く

もしあなたが、先の Gmail のモックの流れを解いていくのが非同期コールスタックの機能が有効じゃなければ大変だと思うなら、連鎖しているプロミスのような更に複雑な非同期フローがどれだけ大変か想像できるでしょうか。Jake Archibald の JavaScript Promises チュートリアルの最後の例を再度見てみましょう。

JavaScript Promises からフローダイアグラム。

ここに Jake の async-best-example.html の例でコールスタックを行き来しているちょっとしたアニメーションがあります。

従来
Breakpoint set in promises example without async call stacks
非同期が有効になっていないコールスタックパネル。

プロミスをデバッグしようとするときコールスタックパネルの情報がとても短かいことに注意してください。

現在
Breakpoint set in promises example with async call stacks
非同期が有効になっているコールスタックパネル。

Wow!なんてプロミスでしょう。たくさんのコールバックです。

プロミスの実装が Blink のバージョンから 最終的な V8 のバージョンにスイッチするにともない、プロミスのコールスタックはすぐにサポートされます。

時間を遡る気持ちで、もしあなたがプロモスの非同期コールスタックを下見したいなら、Chrome 33 か Chrome 34 でチェックできます。アドレスバーに chrome://flags/#enable-devtools-experiments と打って、デベロッパー ツールのテストを有効にしてください。Canary を再起動した後、デベロッパー ツールの設定に行くと、enable support for async stack traces のオプションがあるはずです。

Web アニメーションについての洞察を得る

HTML5 Rocks のアーカイブを更に深掘ってみましょう。Paul Lewis の Leaner, Meaner, Faster Animations with requestAnimationFrame を覚えていますか?

requestAnimationFrame のデモを開き、post.html の(874行目あたりにある)update() メソッドの最初にブレークポイントを追加します。非同期コールスタックで、スクロールイベントのコールバックを開始するところまで含めて、requestAnimationFrame の深い洞察を得ることができます。

従来
Breakpoint set in requestAnimationFrame example without async call stacks
非同期が有効になっていないコールスタックパネル。
現在
Breakpoint set in requestAnimationFrame example with async call stacks
そして、非同期を有効にしたもの

MutationObserver 使用時の DOM の更新を追う

MutationObserver は DOM の変化を監視させてくれます。このシンプルな例では、ボタンをクリックしたときに、新しい DOM ノードを <div class="rows"></div> に追加します。

demo.html の(31行目の)nodeAdded() の中にブレークポイントを追加してください。非同期コールスタックが有効になっていれば、addNode() を通って最初のクリックイベントまで遡ることができます。

従来
Breakpoint set in mutationObserver example without async call stacks
非同期が有効になっていないコールスタックパネル。
現在
Breakpoint set in mutationObserver example with async call stacks
そして、非同期を有効にしたもの。

非同期コールスタックで JavaScript をデバッグするための Tips

関数に名前を付ける

もしあなたが全てのコールバックを無名関数として割り当ててしてしまいがちなら、そうするよりコールスタックをもっと簡単に見れるように名前を付けたいと思うかもしれません。

例として、こんな無名関数を挙げます:

window.addEventListener('load', function(){
  // 何か処理します
});

そして、これに windowLoaded() のような名前を付けます:

window.addEventListener('load', function windowLoaded(){
  // 何か処理します
});

読み込みイベントが発火したとき、デベロッパー ツールのスタックトレースに「anonymous function」といった隠れた表示ではなく、その関数名が表示されます。こうすると、スタックトレース内で何が起きているのか一目で理解しやすくなります。

従来
An anonymous function
現在
A named function

もっと詳しく知る

まとめると、下記がデベロッパー ツールが完全なコールスタックを表示する非同期コールバックの全てです:

  • タイマー: setTimeout() や setInterval() が開始された場所まで戻ります。
  • XHR: xhr.send() が呼ばれた場所まで戻ります。
  • アニメーションフレーム: requestAnimationFrame が呼ばれた場所まで戻ります。
  • MutationObserver: MutationObserver のイベントが発火した場所まで戻ります。
  • addEventListener() からの選ばれた DOM イベント: イベントが発火した場所まで戻ります。パフォーマンス上の理由で、全ての DOM イベントが非同期コールスタック機能に選ばれてはいません。現在利用可能なイベントの例は:「scroll」、「hashchange」、「selectionchange」です。
  • addEventListener() からのマルチメディアイベント: イベントが発火した場所まで戻ります。利用可能なマルチメディアイベントは:audio と video のイベント(例:「play」、「pause」、「ratechange」)、WebRTC の MediaStreamTrackList イベント(例:「addtrack」、「removetrack」)、MediaSource イベント(例:「sourceopen」)です。

下記の実験的な JavaScript API についても完全なコールスタックができるようになる予定です:

  • プロミス: プロミスが解決した場所まで戻ります。
  • Object.observe: オブザーバのコールバックが最初にバインドされた場所まで戻ります。

JavaScript コールバックの完全なスタックトレースを見ることができれば、はげる程頭を悩ませることもないはずです。デベロッパー ツールのこの機能は、特に複数の非同期イベントが互いに関連して発生したときに役に立ちます。もしくは、非同期のコールバックの中から捕捉されない例外が投げられたときに役に立ちます。

Chrome Canary で試してみてください。この新機能にフィードバックがある場合は、Chrome デベロッパー ツールのバグトラッカーChrome デベロッパー ツールのグループに知らせてください。

Comments

0