デバッグの技術

この記事は、アムステルダムで2015年に開かれたFronteersのカンファレンスで私が行った講演、「デバッグの技術」に対応するものです。

要約:利用可能なあらゆるツールの使い方を学び、必要なときにそれを使うことで、バグの撃退を楽しみましょう。そのほうが、キーボードを無暗に叩いて6か月も費やしてしまうより、ずっと楽しいものです。

art-of-debugging-cover

本題に入る前に…

この記事を終わりまでスキップしたければ……

Don’t.

Write.

Bugs.

drop-mic

とはいえ……

おそらくこれを読んでいるあなたはロボットではないでしょうから、1個や2個のバグぐらいは書いてしまったことがあるでしょう。「銀の弾丸」は存在しないのです。

実際、先ほどジョークで申し上げた『バグを書くな』というのは、デバッグの仕方を学ぶことの対極にあるものです。必要なのは経験です。バグに対するアプローチを見つけられるようになるためにはバグに遭遇しなければいけません。

デバッグのために習得できるスキルに、迅速かつ絶対のものは存在しません(私はそう信じています)。何かの事態に遭遇し、それについて悪戦苦闘するうちに習得できるものなのです。問題を修正しようと何時間も何時間も費やすことの結果として、次にそれが起こった時に時間をかけずに問題を修正できるようになる、というものです。

10年前に働いていた会社では、新入社員はみんな最初は「この会社はどういう仕事をしていて、どういう問題を解決しようとしているのか」という事に興奮していたのに、入社当日から早速バグの解決に割り振られて3ヵ月を費やす、といった状況でした。これは彼らの期待を損ねてしまうようなものでしたが、3ヵ月も経つと彼らはバグ解決チームにいさせてくれるよう頼むようになっていました。

バグ解決チームではビジネスの色々な領域に渡る仕事を経験することができたのです。普通の開発者は単一の機能のコーディングに3ヵ月や半年、ややもすると1年もかかりっきりになったりし、いざ完成して動かそうとしたところでバグが見つかり、バグ解決チームがそれを処理するので栄光の一部が横取りされていく……というのとは対照的でした。

思うに、そのような実践的な経験を得ることは、「良い開発者である」ということだけではなく、実は「あらゆるものをどうデバッグするかを知る」ということの鍵になります。10年前のその会社にいたデザイナーのChrisはCSSのプロで、サーバーサイドの開発者がちょっとしたことに詰まった時の答えを全て知っていました。気付けば私は「比較的シンプルなデザインのはずなのに、レイアウトが酷い崩れ方をする」という時には彼にその原因を尋ねるようになっていました。多くの場合、彼の答えは「その要素にzoom: 1を追加してみて」というものでした。

彼は、目で見て分かる視覚的なバグの解決を数多く見てきたために、脳内でデバッグのステップを完了して「そのバグはこうすれば直る」という特定の対処を提案できたのです。

そして、それは私がバグに遭遇した多くのケースで行うことでもあります。問題を解決し始めるにあたって有用なシステムが何であるか、というのを知っているということです。

続きの話に移る前に、2つほど「この記事で扱わないこと」を述べておきます…

免責事項 #1 – フレームワーク

説教臭く言われる前に言っておきますが、私が述べるのはwebのデバッグにおける唯一・決定的な方法ではありません。多くのやり方がありますし、たまたま私が知っていて行っていることがこの記事の内容になります。この内容が読者の助けになれば
素晴らしいことですし、違うやり方で上手く行くならそれは素晴らしいことです。

私は個人的に フレームワークや巨大な(強固な)ライブラリを使いません。Ember, Polymer, React, Anglularなどのライブラリです。今までやってきたことでそれを必要としたことがないので、学ぶ必要がなかったのです(だからといって、「教えてほしい」等と解釈しないでくださいね)。

それはつまり、私が使っているツールがあなたのワークフローに適合しないかもしれないということです。実際、全く適合しない可能性だってあります。

この問題は、あなたが使っているアプリケーションの複雑性に部分的にかかわっています(ここでは「アプリケーション」は、あなたが構築しようとしているサイトの、あなたがサポートするコードを指します)。例えば、Reactは独自の言語を作っており、それによって開発者によるアプリ構築にに最大限の混乱影響をもたらしました。しかしそれにより、人間には理解しがたい、コンピュータ/ブラウザ向けの中間コードが生成されます。従って、それをデバッグするには少なくともソースマップが必要ですし、さらに(あくまで私は推測でしか言えませんが)Reactで使用される状態管理(あるいは他のファンシーな玩具)のためにReactアプリのデバッグのための開発者ツールの拡張機能をインストールすることが推奨されます(私が思うにEmberも同様かと思います)。

かといって、この記事があなたにとって役に立たないという訳ではありません。私はデバッグの時に重要になるひとつのアイデアについて触れていきたいと思っているのです。私があくまで言っておきたいのは、「私はフレームワークを使いませんので、フレームワークにまつわるデバッグをしません」ということだけです。

免責事項 #2 – 私はクロスブラウザテストをほとんどしません

はい。言いましたからね。でも、私をオオカミの群れに放り込む前にちょっと聞いてください。私はクロスブラウザテストをしませんが、その理由は、私の仕事においては大抵「DOMとインタラクションする」JavaScriptではなくVanilla Javascriptを書くからです。

私が見るところでは、私の関心対象のJavaScriptには2つのタイプがあります。「ブラウザでのインタラクション」と、「それ以外」です。

「それ以外」のJavaScriptは、ES5で動作する必要があります(おそらく少しES6を含むかもしれません)が、それだけです。IE8でもサポートしようと思わない限り(私の最近のプロジェクトではサポートしませんでした)、私のJavaScriptはあらゆるブラウザで動作しました。なぜならそれはこの程度のモノだったからです。

function magicNumber(a, b) {
  return Math.pow(a, b) * b / Math.PI;
}

コードがどのブラウザ上で実行されようと関係なく、バグがあればそれは全てのブラウザ上でバグになります。バグがなければその逆です。

また、このことは「私のコードが他のブラウザでテストされていない」ということを意味するものではありません。可能かつ必要であれば、異なるブラウザ環境での自動テストを行います(KarmaやZuulなどのツールを使用しています。しかし、完全に自動化したクロスブラウザテストについて、その手法はまだ完全に固まっていません。混迷の中にあります)。

繰り返しますが、これは完全に私の仕事の性質によるものです。のちほど、どのように私がクロスブラウザテストを行うか(あるいは、「私がクロスブラウザテストを行うか否か」までも)を解説します。

デバッグの技術

これは、私がデバッグのワークショップを行う時、まず最初に開くものです。見てください、Wikipediaさえも言っています、「デバッグは技術である」と。つまり、たかがモノなのです

wiki-debugging

私は、(頭の中で)デバッグを以下の3段階に分解しています。

  1. 再現する 別の言い方をすると: バグを見る
  2. 分離する 別の言い方をすると: バグを理解する
  3. 除去する 別の言い方をすると: バグを修正する

バグを再現する

デバッグ全体において、バグを再現することはもっとも難しい部分です。大抵のバグレポートは、たった一行、

保存ができません。

…とだけ書かれていたりします。

ええ、そのレポートに「はい、そうですね。」と答えて閉じてしまいたいのは山々なのですが、きちんと外交的に対応しなければならないだけでなく、このユーザが何を見ているのかを再現できるだけの情報を集める必要があります。

もしユーザがjsbinに関する話をしているのであれば、私もちょうど使ったところなので保存がうまくいくことはわかっていますから、保存が彼の場合だけ上手く行かない(潜在的には他の人もですが)ことを意味します。言いかえれば「保存はの場合は上手く行く」ということです。

ユーザが話しているURLを訪れて、その問題がすぐに起きた場合、それはラッキーです。これはリトマス試験であり、常にやってみる価値があります。いきなり100%再現しようとするのではなく、段階を追って再現していきましょう。しかし、ふつうバグが明らかに起こる前にいくつかの予兆的なイベントが起こりがちなものです。なので、それらが何かを理解して、自分自身で繰り返してみるようにしましょう。

慎重に、注意深く、システマティックにやりましょう。一度のみならず、繰り返し繰り返し行えるようにしなければならない(もしくは、少なくとも2回はできなければいけない)ことなので、これは重要なことです。

環境の再現に役立つツールは色々ありますが、少なくとも2つ、除外可能な環境要因を同定するためのツールがあります。

シークレットモード

Chromeのシークレットモード(他のブラウザでは違う名前で呼ばれています)によって、自分のブラウザ拡張が(ほとんど)実行されない状態でサイトを見ることができます。また、”通常の”ブラウジングのセッションで利用されるようなクッキーやローカルストレージ、その他の既存設定などもまっさらにした状態で開始することができます。

確かに言えることとして、毎年見てきたバグのうち少なくとも年1件は非常に珍しいものであり、ユーザにブラウザ上でWebサイトのコードを妨害してしまうような不届きな拡張機能によるものです。

シークレットモードで実行してバグがないようならば、ユーザに同様のことをしてくれるよう頼みましょう。そうすることで、そのバグに対する外部要因(典型的なのはブラウザ拡張機能です)の存在を速やかに確かめることができます。

複数のプロファイル

Chromeで私は自分の個人のプロファイルを使っています。そのうちの1つでは、いちいちログインするか尋ねられることなくメールを確認することができます(もっとも……おそらく本当はこれは良くないです。でも気にせず次の話に進みましょう)

私は他に2つのプロファイルを使っています。

  • 匿名 – 全体的にクリーンなユーザ。拡張機能や履歴がない。

  • トロール – 匿名に近いものですが、さらにクッキーを無効にしており、セキュリティ設定も最大限にしてあります。

これらのプロファイルに切替えることはそう多くない(自分のテスト過程ではもっと早い段階でバグを再現できることがほとんどなので)のですが、これらのプロファイルに容易に移行できるようにしてあります。

トロールのユーザプロファイルは特に有用です。ユーザによってはより高いセキュリティ設定を設定することがあるので、それによってlocalStorageのようなAPIが例外を発生し、さらにそれを検出できないと事態は混迷を極めるのですが、(私の場合は)このことは忘れ去られがちだからです。


さて、一貫してバグの再現が可能になったところで、次はノイズや混乱の元を減らすため、できるだけのものを取り除くことにします。バグ修復の前に行います。

バグを分離する

バグの分離とは、バグとそれ以外のことを可能な限り分けることです。もしブラウザの拡張機能がバグの原因なら、1度に1つの拡張を無効化し、原因であるものが見つかるまでそれを繰り返します。

大量のユーザインタラクションを必要とする複雑なJavaScript内にバグがある場合、私は以下のように自問自答することにしています。「分離してテストしたり、特定の状態を注入できるように、この特定部分のコードを分離するようリファクタリングできないか?」

まさにこの問題のために私が作ったのがjsbin.comです。問題を取り出し、余計なものを引きはがし、そして修正したり、必要な人にシェアするためのものです。

余計な物を引きはがし、望ましい状態にまで持って行ったあとは、バグの修正にかかります。

バグを除去する

いったん再現側に気を使っていれば、これはとても容易になります。最近(2015)の私のプロジェクトにおいては、「直すべきバグを再現するような”失敗”のテストを作成し、そのテストについて修正する」ということをよくやります。これの利点は明らかですね。

この段階まで来ればあとはとてもシンプルで、ただコードを書くのと同じぐらいシンプルです(タッチタイプさえできれば)。問題の解決において難しい部分は、キーボードにタイピングしている間に起こるものではないのです。

バグがうまく再現できないときは……

うーん……運が悪いようです。見て見ぬふりをすることもできます。しかし、これはデバッグではありません。ハイゼンバグ(私はこの単語が好きです)に陥ってしまったどうかを考える必要があります。これは、確認しようとすると形を変えてしまうようなバグのことを指します。

私も個人的にこれに何度か遭遇したことがあります。(私にとって)最悪なのは、これらのバグが継続的インテグレーションのシステム上(例えばTravisなど)で起こった時です。ローカル環境ではバグの修正が完了し、コードもきちんと「バグが修正された」とわかる状態になっているのに、テストが通らないのです。こうなってくるとタスクの種類は「テスト環境のデバッグ」というものに変わってきます。継続的インテグレーションシステムの場合、このテスト環境はクローズドなシステムになります。

もう一つ、こういった類の問題に私が遭遇したことがあったのは、Firebugを使っていた頃でした(2009-2010年ごろには使うのをやめましたが)。Firebugは出しゃばりなデバッグツールで、デバッグのためにDOMにコンテンツを注入します。さらに、Firebugそれ自体にバグがあります(開発者ツールや他のデバッガもですが。この投稿の始めをご覧ください)。つまり、「デバッガのバグを引き起こし、デバッグを余計に大変にしてしまう」ようなバグのエッジケースが存在する、ということを意味します。

同じことは現在もある種の真実です。開発者ツールのtimelineを用いたデバッグの際には、全ての記録チェックボックスをオンにしないこと、理想的には他のタブWebkitを用いるもの全て(例えばSpotifyなど。WebKitとBlinkのOSアクセスにはオーバーラップがあると私は見ています)を閉じることをお勧めします。理由として、これら全てがパフォーマンスの記録に影響を及ぼすからです。

デバッグのアプローチ

利用可能なツールは、2つの種類に分けられます。

  • 内から外
  • 外から内

あまり良い呼び名でないことはわかっています。内から外とは、バグの根源がわかっていることを指します。特定の関数や行、debuggerステートメントやブレークポイント、条件付きブレークポイント(ある条件が真のときにブレークするもの)などです。

外から内とはもっと興味深いもので、「ある要素が想定通りの動作をしないなど、視覚的にバグの存在を確認できる」時のものです。ソースコードについて特に知ることなしに、視覚的な問題からその原因のソースコードと招いてくれるようなツールがたくさんあります。

例えば以下のようなツールです。

  • DOM breakpoints – 子要素の変更や属性の変更、要素の消去などのタイミングでブレークする
  • Ajax breakpoints – XHR呼び出しが実行されたときにブレークする
  • XHRの再生 – あるXHR呼び出しのレスポンスを再び注入できるようになります。
  • Timeline screenshots – ネットワーク(主に起動時)とランタイムでのタイムラインの両方に利用可能です。

私のお気に入りの/良く使うツール

最後に、私の使うワークフローと、私がいつも使っているツールをシェアしたいと思います。

ワークスペースとリアルタイム更新

開発者ツールを開いてsourcesパネルを選択し、ワークスペースを作りたいローカルのディレクトリをソースパネルにドラッグします。開発者ツールがアクセス許可を求めてくるのでそれを承認する必要があります。

これで終わりではありません。http://localhost:8000といったOriginがワークスペースから読み込まれていることを開発者ツールに教える必要があるので、少なくとも1つのファイルをマッピングする必要があります。Originのリストから一つのファイルを右クリックし、「Map to file system resource」を選択し、対応するローカルのファイルを選択します。

これで、行った変更を保存すると自動でローカルのファイルに保存されるようになります。これがなぜ重要なのでしょうか?それは、コンテクストの切替え、エディタとブラウザの間の切り替えなしにデバッグし、直接変更をコミットできるようになるからです。

また、CSSファイルも同様にマッピング可能であり、これもとても面白くかつパワフルです。elementsパネルでのスタイルへの変更が全て直接CSSファイルに反映されます。つまり、elementsパネル(私はCSSの変更についてはこれが使い慣れています)でちょっとした視覚的な変更をすると、それがファイルに自動的に書き込まれている、ということになるのです。

アンドゥ

私はデバッグについてのワークショップを数多く行っていますが、自分のワークスペースをお見せした後に、いつも尋ねられる一つの質問があります。

elementsパネルで行った変更を差し戻すにはどうすればいいのですか?

elementsパネルで変更を行うと、ソースコードを直接操作するよりも当てにならないように見えがちです。

しかし、これは実際に公平な疑問です。私はこれにこう回答しています。

  1. ソースコントロール
  2. アンドゥ

Chromeの開発者ツールはアンドゥを実によくサポートしています。CSSに一連の変更を行った後、JavaScriptに移行してDOMを変更し、その後なおCSSに戻ってそちらの変更だけを全て差し戻すことが可能です。

アンドゥをするためには、それと一致するパネルとソースにフォーカスしないといけない(アンドゥの履歴がパネルと関連付けされているのだと思います)ということに気付いたのですが、これはとても良いことです。

当然ながら、リロードすると履歴は失われます。これはSublime Editorでも同様であり、Sublimeをアンロード・リロード(つまりアプリケーションの再起動)すればアンドゥの履歴は失われます。

コンソールのショートカット

  • $ & $$ – jQueryの $ と同様、ページ上の要素をクエリする関数になります。
  • $_ - 最後に行った$$$の結果を返します。
  • $0 – elementsパネルで現在選択されているDOMノードを表します。
  • copy(...) – クリップボードへのコピー。オブジェクトはJSON.stringifyされますが、DOMノードの場合はouterのHTMLを得られます。私はcopy($0)をよく使います。

Timeline screenshots

アプリケーションの起動(あるいはインタラクション)時において何がページで変更されたか確認するために時間を遡るよい方法です。私は最近、これを異なる2つの問題の解決のために利用しました。

1つ目は、jsbin.comの起動時のスクリーンショットを見直したところ、「フォントのロードが一番最後に行われており、それに時間がかかっている(=総起動時間が伸びている)」ということに気付いたことです。ドキュメントが全て準備完了になる直前にフォントが正しく読み込まれているのを見られたことでこれに気付きました。そこで、フォントローディングのテクニックを使ってローカルストレージ経由でロードすることで、体感上の起動時間を向上させることができました。

2つ目は、confwall.comという私のプロダクトの時です。タブのシステムの読み込みにおいて明らかなレイテンシ―があったことが問題でした。以下のアニメーションを観れば(50%の速度で動かしています)タブのレンダリングが遅いことが分かります。

tabs-loading

レンダリングのタイムラインにおいて、カメラのアイコンからキャプチャ可能です。

devtools-screenshots

ここから、タブが再レンダリングされるタイミングをレイアウト時に移行させ、何が実行され、何がブロッキングしているかを突き止めることが出来ました。

Throttling

ネットワークのスロットリングにより、遅い/完全にオフラインな接続のエミュレーションについてとても迅速に俯瞰することができ、遅いネットワークの影響をすぐに見ることができます。

典型的な例はこのようなものです。:「低速なネットワーク接続では、カスタムフォントを含んだ私のサイトはどう見えるだろう?長時間にわたって空白に見えるようになってはいないか?フォントのレンダリングを妨げる他のアセットはないだろうか?これについて何かできることは?」

ネットワーク詳細と応答

ネットワークリクエストの視覚化は重要ですが、ヘッダの調査と、未加工のレスポンスのコピーもとても重要であることに気付きました。

devtools-copy-response

サーバーサイドでレスポンスが正しくない(JSONではなくHTMLを送り返してしまうなど)バグをデバッグしていた時、デバッグしてサーバーを再起動したあとは、ブラウザでページをリロードして状態と現状のスタックをクリーンにしてしまう代わりに、”XHRの再生”によってリクエストを再び送信することで更新後のサーバーコンテンツに応じたコールバックをできることに気付きました。

DOMの更新でブレーク

前述したとおり、「DOMの更新でブレーク」は外から内のアプローチで私がデバッグするときのやり方の一つです。ビジュアル的な変更があることが分かっているがソースコードのどこで起こっているのかわからない、という時にはこれをよく使います。

「Break on…」のうちどれを使うべきか、というのを知るのは難しいことに気付きました。普通「break on attribute modification」はシンプルで、たとえばclassNameが変更されたときにブレークしたい時に使います。そうでなければ、私はブレークが起こるまで全ての「break on…」を使いがちです。そして、コールスタックを戻ったり進んだりするようにしています。

プロのアドバイスのおまけとして書いておくと、コールスタックは非同期の呼び出しによって無意味になることがあります。開発者ツールでは、ソースパネル上でこれの対処法を提供しています(メモリコストが高いので、オフにするのを忘れないでください)。”Async”のチェックボックスをチェックし、そのバグを再び起こします。これで、非同期呼び出しを含めた完全なコールスタックを得られます。

メモリリークのサーフェース・スキャン

最後に、メモリリークは従来からデバッグにおける最難関です(私にとっては確実に)。実際、「何か私の思いもよらない飛躍が起こっている」と感じない限りはメモリを確認することはほとんどありません。しかし、開発者ツールはメモリリークについて深く掘り下げるのを容易にするように実に進歩してきました。

私がとるアプローチは2つです。数年前のChromeの素晴らしい動画からわかるものです。

  1. 階段効果のサーフェース・テスト
  2. プロファイリングツールを使って、リークの原因の手がかりを捉える

階段効果はメモリリークがあるかどうかを判断する第一の手がかりです。自分の場合、やり方は「リークの効果を再生産する」というものです。開発者ツールのTimelineの記録時に”Memory”にのみチェックを入れます(他は入れません)。そしてインタラクションを始め、記録を止める前にガベージコレクションを強制的におこすゴミ箱をクリックします。そしてこの工程を再び繰り返したのち、記録を終えます。

ここで私が行っていることは、以下のようになります。:メモリ消費のベースライン(インタラクションを開始する前のデータ)を定め、インタラクションを起こす。もしここで、ガベージコレクションできない明らかな量のメモリがあれば、リークが起こっているという事になる。そのうえでプロファイリングに進む。

memory-leaks

プロファイリングには2つのアプローチがあります。一つ目は「インタラクション開始時と終了時の2つのヒープ・ダンプをキャプチャする」というものです。また、同様に2つのインタラクションを行いますが、2回目の実行の前にガベージコレクションを強制的に行います。このタスクは、deltaを比べることになります。二つ目のヒープダンプを選び、”summary”から”comparison”に変えて、”delta”で並び替えます。ここで探すのは、メモリがになっている項目です。この項目は、ガベージコレクションできなかった項目になります。

これで、(上手く行けば)何がリークしているかという要素を知ることができます。通常はDOM要素や、JavaScriptの参照がどのノードを指し続けているかというものになります。苛立たしいことに、これはふつうJavaScriptのライブラリ内なので、ライブラリがどう動作するかの知識がここでは助けになるのです。

memory-comparison

結論

最初に言ったように、銀の弾丸は存在しません。この投稿の読者の多くは、使えるパーツの上澄みを救ってコピー&ペーストしたのではないかと思います。クールなことですし、私も同じことをします。

デバッグのスキルを磨くことはとても長いゲームで、バグというノイズを生む行為である「コードを書く」という行為に直結しています。読者のみなさんが、デバッグする機会でこれも飛躍的に向上させられることを願っています。

覚えておいてほしいのは、「デバッグを一休みする」ということも価値のあることだということです。多くの、実に多くのバグはコンピューターの近くにいないときに解決したりします(長い散歩や、シャワーなど)。時にコンピューターは少しストレスフルにもなります。

この投稿は2015年10月14日にWebに投稿されました。TwitterではJavaScriptやHTML5やその他の事について(ありがちなTweet的な自己宣伝に交えて)投稿していますので、ぜひ見てみてください