一子相伝!デバッガーいらずのシステマチックバグフィックス


はじめに

「デバッグという作業がバグを取り除くことなら、プログラミングとはバグを注入する作業に違いない」
                                                       By ダイクストラ

私はこの言葉が好きです。
ダイクストラさんがどんな人かは知らないけれど、エンジニアがどんな仕事かを、簡潔かつ具体的に表していると思っています。

そう。
私たちは未来の私たちを怨霊渦巻く地獄へと連れ去るべく、今日もせっせとバグという爆弾を仕込んでいます。
そして、エンジニアはたとえそれが自分の作った爆弾でなくとも、取り除く義務があります。取り除かなければなりません。
業務時間の大半は、その作業に割り当てられます。

今回は、デバッグを行うにあたってとりわけ過酷な環境に置かれた私が、業務中に行なっている、
「これをしとけば大体不具合の原因は見つかるよ!」的なルーティーンをご紹介いたします。

※なお、私のデバッグはCareer Skillsという書籍から多大な影響を受けています。
気になる方は読んでみると良いかもしれません。

お前誰よ?

前置きが長くなってしまってすみません。
今年で3年目になる、ギリ駆け出しエンジニアです。
業務では車載機の開発を、個人ではandroidアプリ開発を行なっています。

私が普段勤めている現場では、とある事情によりデバッガーが使えません。
そのため、ステップ実行もなければ、ブレークポイントなんてものもありません。
また、nullで落ちていても、どの部分で落ちているかがわかりません。
あるのはただログのみ。
渡された10万、20万行のログの中から、不具合の原因を探ります。

時には、不具合の原因がログに吐き出されてなかったりします。
時には、ログそのものがなかったりします。
時には、不具合なんて起こってなかったりします。

これほどまでに多種多様な不具合報告を、どうやって捌いていくのか。
早速見ていきましょう。

0.まずはコードを見る。

これは、絶対にやってはいけないことの一つです。
大事なので二回書きます。
最初からコードを読むようなことは、絶対にやってはいけません。
無意味なのでやめましょう。

まず、なぜバグが産まれるのか考えてみましょう。
私たちはコードを書いて、(まともなエンジニアなら)リファクタリングをします。
そして、動作確認を繰り返し、「大丈夫!うごくぞ!」となってから、(まともな職場なら)レビューをしてもらうはずです。

全てのコードがその過程を通ってきているにも関わらず、それでもバグは混入してしまいます。
これだけ確認しても、気づかないのです。
にも関わらず、愚直にコードを読むようなことをしても、ほぼ間違いなく不具合の原因には気づきません。
どこまでコードを読んだかわからなくなって、同じファイルを閉じては開くのが関の山です。
効率の良いデバッグには、そのための準備が必要です。

1.作業環境を整える。

不具合報告を受けてからまず最初にやらなければならないことは、作業できる環境を整えることです。
この時に必要なものは主に三つ。

1.不具合を再現する環境
2.不具合が発生した時のログ
3.時間

一つ一つ解説していきます。

1-1.不具合を再現する環境

私が経験した不具合報告の中に、下記のような事例がありました。

「○○の機能を起動しても、うんともすんともいいません。起動しないんです。」

若き日の私は、まだ愚かでした。
この不具合報告を受け、まず愚直にコードを読みました。
機能が起動しない原因を、思いつく限り列挙しました。
しかし、コードを読んでも読んでも、不具合の原因がわかりません。
「なんでこれで起動してないんだ...?」

ついに私は自分一人で解決することを諦め、隣のデスクのつよつよエンジニアに助けを求めました。
すると、一つの事実がわかります。
そもそもこの機体、この機能サポートしてねーじゃん!!
環境を整えておけば、発生しない不具合だったのです。

私がこの事例から学んだ教訓は一つです。
基本的に、不具合報告を真に受けるのはやめましょう。
まずは自分で環境を整えて、不具合が発生するその瞬間をきちんと、自分の目で確認しましょう。
それができないのであれば、不具合が発生している動画を撮ってもらいましょう。

再現性のない不具合は、存在しないのと同義です。
確かな再現手順があるということが担保できない限りは、一度行動を控えましょう。
発生しない不具合を直すことは不可能です。

1-2.不具合が発生した時のログ

ログには、そのプログラムがどう動いたか、またはどんな値の受け渡しがあったかといった内容が吐き出されるはずです。
これは、デバッグの際に私たちに大きな力を与えてくれます。

特に、デバッガーが使えない環境では、ログがデバッグのためのコンパスになります。
必ず用意しましょう。

1-3.時間

通常、デバッグにはプログラムを書くよりもはるかに多くの時間を使います。
皆さんは、マルチタスクでプログラムを書くことができますでしょうか?

少なくとも、私にはできません。
割り込みで作業をすると、その前に行なっていた作業のことなど、そうそう覚えてられません。
不具合を直すには、腰を据えてデバッグを行う必要があります。

一度業務の優先順位を確認して、まとまった時間を確保できる状態を整えましょう。
時間を使うことが、結果的に一番の近道です。

2.実態と期待値をまとめる。

もっと簡単に言うと「今どんな動きをしていて、どうあるのが正しいのか」を整理します。

世の中には、信じられないくらい考える力のない人間がいます。
私もそのうちの一人です。

バグを直したい気持ちが逸って、闇雲に行動してしまいます。
そうすると、今起こっている不具合が、どうなればいいのかもわからないまま動くことになってしまいます。
これでは、不具合の原因など見つけられるはずがありませんね。

まずは一度立ち止まって、実態と期待値をまとめましょう。
ノートやメモに書くと、より効果的です。
私は必ずでかでかとノートに実態と期待値を書くようにしています。
書く必要はありませんが、何かあった時にすぐ立ち返るため、すぐに見れるようにしておきましょう。

3.考える。

冗談ではありません。いたって本気です。

まずは、その不具合が発生する原因をきちんと考えましょう。
仮説を最低2つ、できれば3つ以上立てます。

これがないとどうなるかというと、私のようにただコードを眺めるだけになります。
コードを読むときは、不具合の原因を見つけるための切り口が必要です。

コードを読みながらでも良いので、不具合が発生するであろうパターンを考えてみましょう。

4.仮説の検証を行う。

それでは、仮説の検証です。
自分で立てた仮説を元に、不具合の原因を追っていきましょう。

  • この真偽値が(true/false)だった場合はどうなるのか。
  • ここの値が渡されていないのが原因じゃあないのか。
  • ここの計算式が間違っているんじゃないのか。

思いつく限りの全ての仮説を検証してみましょう。
原因はわからなくても、きっと何かのヒントが得られるはずです。

ここにきて初めて、コードを確認します。
検証結果と見比べて、コードはどう書かれているのか。
どのファイルの、どの関数が怪しいのか。
大体のあたりは付いているはずです。
めげずに探してみましょう。

5.ログを埋め込んでみる。

4まで試して原因がわからなかった場合は、ログを埋め込んでみましょう。
ここでログを出すための目的は下記の3つです。

1.どの関数まで実行されているかを確認する。
2.どんな値が設定されているのかを確認する。
3.仮説の検証結果を確認する。

大体ログを取得する目的に書いた内容と同じですね。
この記事を読んでいるあなたが、どんな言語を使用しているのかはわかりませんが、
標準出力などに値を吐き出す関数があるはずです。
それを使って、上の三つのどれかを書き出してみましょう。
何かわかるはずです。

※ログは基本的に膨大な行数の文字が吐き出されます。
0から読んでいったのでは日が暮れてしまうので、検索して探せるようにしておきましょう。
自分の場合は下記のようにログを出して、"Log ○○○"と検索すればすぐにたどり着けるようにしています。

〜〜〜〜
Log ○○○
吐き出したい値
〜〜〜〜

上記の手順を行えば、大概の場合は解決の糸口を掴むことができます。

6.それでも何もわからなかったら。

帰りましょう。
そういう時は、帰って寝るのが一番です。

人間は、朝の方が脳みそのパフォーマンスが良いと聞いたことがあります。
クソコードによってダメージを負ったあなたの脳みそは、今は疲れてしまっているだけです。
明日の朝になったら何かわかるかもしれません。

引くことを覚えましょう。

7.不具合の原因(っぽいの)がわかったら

おめでとうございます。
ただ、ここでデバッグをやめるのはもったいないです。
もう少し耐え忍びましょう。

不具合の原因がわかったら、
「なぜその不具合が発生したのか」を考えましょう。
俗に言う抽象化です。

なぜこんなことをするのかと言うと、同じような不具合が見つかる可能性があるからです。
そして将来、その不具合が発見された時にデバッグを頼まれるのは、あなたかもしれません。
その時あなたは、この1から6の手順を繰り返し、全く同じ憤怒の河を渡ることになります。
そんな悲しみは二度と繰り返さないようにしましょう。

プロジェクト内に似たような不具合がないか、もう一度だけ確認してあげましょう。
それが優しさというものです。

以上が、私が普段行なっている、バグフィックスのルーティーンになります。

デバッグをより簡単にするTips

最後に、バグフィックスをより簡単に、効率的に行うTipsをご紹介いたします。

1.関数ごとにLogを埋め込む

どこまでプログラムが実行されたか、簡単にわかるようになります。
バグフィックスを行うのがあなたじゃなかったとしても、多大な恩恵を与えること間違いなしです。
極端に出力回数が増える場合を除いては、関数ごとにログを出すようにしておくと便利です。

2.過去の不具合報告内容をまとめておく

不具合の内容をタイプごとに分けて引き出せるようにしておくと便利です。
レイアウトの不具合なのか。ロジックの不具合なのか。
デバッグは勘と経験が結構重要なので、知見をドキュメント化してためておくといいです。

3.メンテナンスしやすいコードを書く

真理です。
仮説の検証の効率が段違いに良くなります。

4.デバッガーを使う

文明の利器です。
使えるなら使った方がいいに決まっています。(怒)

終わりに

エンジニアは、1日のうちの大半をコードのメンテナンスに使うと言われています。
そのため、デバッグの効率が上がれば、自然と業務も早くなってくると私は考えています。
素晴らしいデバッグ技術はあなたのプログラミングに良い影響を与えること間違いなしです!
それでは、良いデバッグライフを!