問題が発生しデバッグをする必要があるときに、今、自分にはどの選択肢があり、何をして、何を解決できるのか、体系的にまとめる試みです。
ただ今回は、バグを修正するよりも、問題の特定に焦点を当てて、まとめてみようと思います。
体系的にデバッグ手法をまとめることで、バグを落ち着いてデバッグを行うことができ、自信を持って対処することで、必要な指示を仰いだり、誰かに手伝ってもらったり、チームでのコミュニケーションを円滑に行うことができます。また、バグを解決する中でクライアントとも連絡をとることもあると思いますが、クライアントに不必要な不安を煽ることなく対処することができます。さらに、問題解決のスケージュールや、どのくらいで解決することができるかなどの見積もりすることも可能になるでしょう。
以下に、デバッグ時のフローチャートをまとめてみます。実際にデバッグを行うときには、必ずこの通りのフローになるということではなく、情報収集をして、仮説を立て、検証し、また情報収集に戻るというような、このフローを反復しながら、デバッグをすすめていきます。
情報収集
情報収集の目的には、上長や、クライアントに報告するための情報収集など、いろいろな目的があると思いますが、バグを修正するためには、問題の再現に必要な情報収集を行っていきます。
問題を再現するためには、誰が何をしたときに、何の問題がおきたのかを確認します。すべてのプログラマーが自然にできていることだと思いますが、注意すべきことは、「誰が」の部分は、なるべく一次情報をあたるようにしましょう。他の人を通して聞くと、その過程で勘違いがおきて、全く別のところを調べているということはよくある話だと思います。また、「何をしたとき」についても、難しいところで、技術に詳しくないクライアントとやり取りしているときにはよくあることですが、全く別のタイミングでおきた問題を指していたり、別のページについて話していたりと、正しい情報を得るのに苦労することがあります。クライアントに連絡を取る前に、勘違いしそうな部分などを調べておくと、スムーズに問題を特定することができることもあります。
バグの発生には、様々な要因で発生するので、一度の情報収集で、全ての情報を得ることは、難しいので、適宜情報収集をしながら、デバッグをすすめましょう。
問題の再現
得られた情報から自分の環境で再現するか検証していきます。問題を再現することができない場合は、解決は非常に困難になります。OSの種類や、バージョン、モジュールのアップデートのタイミング、外部サービスの状況、ログの確認など、情報収集を行い、問題を再現することに努めましょう。
ここからは、具体的な問題の特定方法をみていきます。
(方法1)googleで検索
特定のOSやモジュール、バージョンなどに問題がある場合、ほとんどの場合、すでに、報告と解決策が示されています。モジュールのアップデートのタイミングで問題が発生した場合など、モジュールの仕様変更や、バグなどが想定される場合、それを検索してみましょう。クライアントからの情報収集や、自分の環境でも問題再現をさせたときに出るダイアログや、コンソールにでるログなど、特徴的なエラーメッセージがある場合もそれを検索にかけてみると問題を特定できる場合があります。
(方法2)仮説を立てる
問題の挙動を元に仮説を立ててそれを検証していきます。
仮説をたて、その仮説が正しければ、出てくる特徴的な挙動をログや、実際にシステムを実行してみたり、テストコードを作成することで検証していきます。デバッグには、問題を切り分け、どこが今対処している問題と関係がないか調べるのは重要なので、仮説が正しければ、そのような挙動を示すことは無いなど、仮説を否定する検証も重要です。
この方法は、問題の再現に失敗している場合でも、適用できる方法ですが、仮説を立てるためには、問題がおきているシステム自体や、システムに使われているフレームワークやモジュールなどの根本的な動作原理などに熟知していないと、仮説を立てること自体が難しいです。
(方法3)ソースコードのトレース
ソースコードのトレースは個人的には、問題の特定に最も時間のかかる方法なので、できれば、他の方法で問題の特定を試みてダメだった場合にトレースしてみます。また、configファイルなどの設定に問題がある場合、ソースコードのトレースでは、解決が難しい問題になります。
まずはじめに、ソースコードをトレースするときは、頭から始めるのではなく、クラッシュしている場合には、ログなどから、クラッシュしている行を特定する。または、表示がおかしいのであれば、おかしいデータが確認できる行を特定します。
次に、問題の原因となっている行を特定します。クラッシュしている行が問題となっている場合は、難しいことではありませんが、多くの場合は、実際にクラッシュしている行とは別の行が問題になっていることが多々あります。
「ぬるぽ」などのデバッグが難しい理由はここにあると思います。論理的には、そこの行でnullになることは無いはずなのに、どこかで適切な初期化が行われておらず、その変数にアクセスしたときに、はじめて問題が出てくるような場合です。
コールスタックのトレースや、ブレークポイントを設定、デバッガーをアタッチできない場合はprint文を挿入することで、どこで問題のある値が入れられるのかを探し、問題の原因の行を特定していきます。
エラーを握りつぶしている場合や、非同期処理のエラーをメインのスレッドに伝播させてない場合などは、トレースの作業が難しくなります。
自分が使う問題の原因の行を特定する方法は5つあります。
- 仮説を立てる
- 二分探索(コードの実行順序)
- 二分探索(ソフトウェアのバージョン)
- Reduced Test Cases
- Characterization Test
仮説を立てる
仮説を立てられる場合は、その仮説が確認できる行の周辺にブレークポイントを設定して、検証していきます。
二分探索(コードの実行順序)
全く仮説をたてることができない場合は、問題のある変数の伝播をコールスタックをもとにソースコードを二分探索していきます。
二分探索(ソフトウェアのバージョン)
前は、正常に動作していたが、ある時点で問題が出始めた場合などは、ソフトウェアのバージョンを時間軸で並べて二分探索で、一つづつ実行して問題が発生するバージョンを探していきます。 具体的には、git bisectで、問題の出るコミットを探していきます。
Reduced Test Case
少しづつ関係ないと思われるコードを削除、または、コメントアウトすることで、 問題の出る最小の構成になるまで、動作確認とコードの排除を繰り返し行います。
Characterization Test
特性テスト(Characterization Test)の応用で、モジュールのソースコードが手に入らなかったり、レガシーなシステムで、全く処理の内容がわからない場合に、使う手法で、 とり得る値のすべての組み合わせを入力し、問題となっているものと同じ出力になるものを特定する方法です。
以上が、自分が実践している問題の原因の特定方法です。
最後に
今回、なんとなくやっていたバグに対する対処方法をまとめてみました。
やはり、どの方法を取るにしても、見つかる時間が立てば立つほど、コストが高くなっていくので、開発の段階で問題が見つかるように工夫するのがベストだなと思いました。その方法は、テストになると思いますが、ユニットテストは書きやすいけど、統合テストはコストがかかる。ユニットテストについて、最近読んだ本の中で、面白かったのが、「街灯の下で鍵を探す」(どこにあるかわからないが、暗いところでは探せないので、明るいところだけを探す)という言葉が強烈に印象深く残り、難しい問題だなと思いました。