March 2016

Volume 31 Number 3

Visual Studio - Visual Studio 2015 におけるデバッグの強化

Andrew Hall | March 2016

最初から正しく機能するコードを作成するように最善の努力を尽くしても、デバッグに多くの時間がかかるのが現実です。Visual Studio 2015 には、開発者が開発サイクルの早い段階で問題を特定でき、バグの発見と解決に必要な能力が向上する新機能や機能強化が盛り込まれています。今回は、.NET 開発者と C++ 開発者向けに、Visual Studio 2015 に加えられた機能強化を取り上げます。

まず、ユーザビリティの強化点を 2 つ紹介し、Microsoft .NET Framework と C++ の両方のデバッグに利用できるパフォーマンス ツールの追加について説明します。また、.NET 開発を具体的なターゲットとする一連の機能強化や、C++ 開発者向けの新しい開発についても取り上げます。

ブレークポイントの設定

ブレークポイントはデバッガーの基本機能で、デバッガーを起動するたびに利用するような使用機会の多い機能です。バグの原因をすばやく突き止めるために、条件付きブレークポイントを使用することもあります。Visual Studio 2015 では、条件付きブレークポイントを簡単に利用できるように、コードのコンテキストに合わせてブレークポイントを設定できるモードレス ウィンドウが導入されます。これにより、同じウィンドウ内で異なる設定を簡単に組み合わせることができます。

Visual Studio 2015 には、以下のブレークポイントの設定が用意されています。

  • [条件式] は、指定した条件に一致する場合にのみ停止します。論理的には、if ステートメントをコードに追加して、if ステートメント内部にブレークポイントを設定するのと同じことで、if 条件が true の場合にのみ停止します。[条件式] は、複数回呼び出される中で、特定の入力でのみバグが発生するコードがあるときに有効です。ブレークポイントで停止するたびに手動で値を確認してからデバッグを再開するのは非常に面倒です。
  • [ヒット カウント] は、ブレークポイントに特定の回数到達した時点でのみ停止します。[ヒット カウント] は、コードが複数回呼び出される場合に、エラーが発生する回数がわかっている場合や、少なくとも何回繰り返せばエラーが発生するかがおおまかにわかっている場合に有効です。
  • [フィルター] は、特定のスレッド、プロセス、またはコンピューターでブレークポイントに到達したときに停止します。[フィルター] ブレークポイントは、並列実行されるコードのデバッグで、1 つのインスタンスでのみ停止させる場合に有効です。
  • [メッセージを記録する] (トレースポイント) は、メッセージを出力ウィンドウに出力し、自動的に実行を再開します。トレースポイントは一時的なログ記録に有効で、トレースを続行する必要があり、停止して手作業で各値を追跡することが適切ではない場合に使用します。

条件付きブレークポイントの値をデモするために、ループの内側で種類別にアプリを取得する例を考えます (図 1 参照)。コードはほとんどの入力文字列で機能しても、入力が「desktop」の場合にのみエラーが発生するものとします。1 つの選択肢は、ブレークポイントを設定し、毎回 appType の値を確認して、入力が「desktop」になったらステップ実行を開始して、機能しない部分を探すことです。しかし、appType が「desktop」に等しくなったときのみ停止する条件式を指定するブレークポイントを作成する方が便利です (図 1 参照)。

条件式を指定している [ブレークポイントの設定] ウィンドウ
図 1. 条件式を指定している [ブレークポイントの設定] ウィンドウ

条件式を指定したブレークポイントを利用すれば、デバッグに適した状態になるよう手動で行う手順を省略できます。

例外設定

開発者としては、アプリの実行時に例外が発生する可能性を認識しています。多くの場合、try/catch ステートメントを追加して、問題が発生する可能性を考慮します。たとえば、アプリケーションからネットワーク呼び出しを使って情報を取得するとします。このとき、ユーザーのネットワーク接続が機能していない場合や、サーバーが応答しない場合は、ネットワーク呼び出しで例外がスローされます。このような場合は、ネットワーク要求を try ステートメント内に含め、例外発生時に適切なエラー メッセージをユーザーに表示します。機能すると想定している要求が失敗する場合 (コードで不適切な形式の URL を指定している場合など)、通常は、コード全体を眺めてブレークポイントを設定する場所を探すか、デバッガーがハンドルされない例外で停止するように try/catch ステートメントを削除します。

ですが、もっと効率のよいアプローチがあります。このような場合は、[例外設定] ダイアログ ボックスを使用して、例外がスローされたときにデバッガーが停止するように構成します。[例外設定] ダイアログ ボックスでは、種類を問わず例外がスローされた場合、または特定の種類の例外がスローされた場合のみデバッガーが停止するように設定できます。以前のバージョンの Visual Studio では、[例外設定] ダイアログ ボックスが開くのが遅すぎるといった意見や、検索機能が不十分だという意見が寄せられました。そこで、Visual Studio 2015 ではこうした意見を参考にして [例外設定] ダイアログ ボックスを一新し、瞬時に開き、高速かつ一貫した検索機能を利用できるようにしました (図 2 参照)。

「Web」を含むすべての例外の種類を検索するように設定した Visual Studio 2015 の [例外設定] ウィンドウ
図 2.「Web」を含むすべての例外の種類を検索するように設定した Visual Studio 2015 の [例外設定] ウィンドウ

デバッグ中のパフォーマンス ツール

高速に動作し、応答性が高いソフトウェアを期待するエンド ユーザーが増えているため、ソフトウェアの読み込み中を示す回転マークが長時間表示されたり、UI の反応が鈍いと、ユーザー満足度が低下し、ユーザーの興味をつなぎとめるのが難しくなります。時間は貴重です。機能が同じアプリケーションが複数あれば、ユーザーは反応の良い UX を備えたアプリケーションを選択します。

しかし、開発者の多くは、ソフトウェアの作成時点では積極的なパフォーマンス チューニングを行わず、アプリケーションが十分高速に動作することを願って、ベスト プラクティスに従います。開発者が積極的なパフォーマンス チューニングを先送りにする主な理由は、作業に時間がかかることと、パフォーマンスの測定が困難な点にあります。さらに、問題を見つけてもパフォーマンスを向上する方法を探すのはもっと困難です。そこで、Visual Studio Diagnostics チームは、Visual Studio 2015 で一連のパフォーマンス ツールをデバッガーに直接統合しました。それが、パフォーマンスのヒント (PerfTips) と [診断ツール] ウィンドウです。これらのツールにより、開発者は日常のデバッグ作業の一部としてコードのパフォーマンスを把握できるようになります。その結果、問題の早期発見や確かな情報に基づく設計上の意思決定が可能になり、最初からパフォーマンスを意識してアプリケーションをビルドできるようになります。

アプリケーションのパフォーマンスを把握する安直な方法は、デバッガーを使用して単純にコードをステップ スルーし、コードの各行の実行時間を大まかに体感することです。残念ながら、このやり方では感覚的な違いしかわからず、科学的とは言えません。体感で、25 ミリ秒と 75 ミリ秒の違いを区別するのは困難です。正確な情報を得るためにコードに手を加えてタイマーを追加することもできますが、当然手間がかかります。

この問題を解決するのがパフォーマンス ヒントです。これは、デバッガーの停止時に、停止した行の右側にミリ秒単位の時間を表示して、コードの実行時間を示します (図 3 参照)。パフォーマンス ヒントは、アプリケーションがデバッガーで停止してから次に停止するまでにかかった時間を示します。つまり、2 回目の停止がコードのステップ スルーによるものでも、ブレークポイントによるものでも機能します。

関数呼び出しをステップ オーバーするのにかかった時間を示すパフォーマンス ヒント
図 3. 関数呼び出しをステップ オーバーするのにかかった時間を示すパフォーマンス ヒント

コードの実行時間の把握にパフォーマンス ヒントが役立つ簡単な例を紹介します。ユーザーがボタンをクリックしたらディスクからファイルを読み込み、読み込んだファイルを適宜処理するアプリケーションがあるとします。ほんの数ミリ秒でディスクからファイルを読み込めると考えていましたが、パフォーマンス ヒントを見ると、非常に長い時間がかかっているのがわかりました。この情報に基づいて、変更コストが膨大になる前の開発サイクルの早い段階でアプリケーションの設計を変更し、ユーザーがボタンをクリックしたときにすべてのファイルを読み込む方法は採用しないようにします。パフォーマンス ヒントの詳細については、aka.ms/perftips (英語) を参照してください。

[診断ツール] ウィンドウは、アプリケーションの CPU 使用率とメモリ使用量の履歴、およびデバッガーの全停止イベント (ブレークポイント、ステップ、例外、すべて中断) を表示します。このウィンドウには、[イベント]、[メモリ使用量]、[CPU 使用率] という 3 つのタブがあります。[イベント] タブには、デバッガーでのすべての停止イベントが表示されます。つまり、パフォーマンス ヒントのすべての値の完全な記録が含まれています。Visual Studio 2015 Enterprise バージョンでは、IntelliTrace イベントもすべて表示されます (後ほど詳しく取り上げます)。図 4 に、Visual Studio 2015 Enterprise の [イベント] タブを示します。また、デバッグ セッション中に CPU やメモリの情報を更新することによって、コードの特定のセクションにおける CPU やメモリの特性を確認できます。たとえば、メソッド呼び出しをステップ オーバーして、グラフの変化を監視して、この特定のメソッドの影響を測定できます。

[イベント] タブを選択した状態の [診断ツール] ウィンドウ
図 4. [イベント] タブを選択した状態の [診断ツール] ウィンドウ

メモリ グラフでは、アプリケーションでのメモリ総使用量を確認して、メモリの使用傾向を把握できます。たとえば、メモリ グラフが着実に右肩上がりで増えている場合は、最終的にクラッシュにつながるメモリ リークが発生している可能性があります。ここからは、.NET Framework のしくみを説明してから、C++ でのエクスペリエンスを紹介します。

.NET Framework をデバッグしている場合、メモリ グラフにはガベージ コレクション (GC) が行われたタイミングとメモリ総量が表示されます。これは、アプリケーションが使用するメモリ総量が許容レベルにあっても、GC が頻繁に行われることにより、アプリケーションのパフォーマンスに悪影響が出ている場合に有効です (GC が頻繁に行われるのは、一般的に有効期間の短いオブジェクトを多数割り当てていることが原因です)。[メモリ使用量] タブでは、[スナップショットの作成] ボタンを使って、特定の時点におけるメモリ内のオブジェクトのスナップショットを取得できます。また、異なる 2 つのスナップショットを比較して、簡単にメモリ リークを特定することもできます。スナップショットを作成し、メモリに影響を与えないと想定される方法でアプリケーションを一定期間使用し続けて、2 回目のスナップショットを作成します。図 5 は、2 つの .NET スナップショットが含むメモリ ツールを示しています。

2 つのスナップショットを含む [診断ツール] ウィンドウの [メモリ使用量] タブ
図 5. 2 つのスナップショットを含む [診断ツール] ウィンドウの [メモリ使用量] タブ

スナップショットをクリックすると、メモリ内のオブジェクトの詳細 (合計数やメモリ使用量など) を示す、[ヒープの表示] ウィンドウが開きます。[ヒープの表示] の下部には、ガベージ コレクションの対象になっていないオブジェクトへの参照 ([ルートのパス]) が表示され、[参照された型] タブには、選択した型の参照先の型が表示されます。図 6 に、2 つのスナップショットの相違点を示す [ヒープの表示] ウィンドウを示します。

2 つの .NET スナップショットの相違点を示す [ヒープの表示] ウィンドウ
図 6. 2 つの .NET スナップショットの相違点を示す [ヒープの表示] ウィンドウ

この C++ メモリ ツールは、メモリの割り当てと割り当て解除を追跡して、特定の時点におけるメモリの状態を把握します。データを表示するには、[スナップショットの作成] ボタンを使用して、その時点における割り当て情報のレコードを作成します。スナップショットを比較して、2 つのスナップショットの間で変更されたメモリを確認できるため、完全に解放されていると想定するコード パスのメモリ リークを簡単に追跡できます。スナップショットを 1 つ選択して [ヒープの表示] ウィンドウを開くと、オブジェクトの種類がサイズと共に一覧されます。2 つのスナップショットを比較すると、これらの数値の相違が表示されます。さらにメモリ使用量の調査を進めたい特定の種類を見つけら、そのインスタンスを表示します。インスタンスのビューには、各インスタンスの大きさ、メモリ内に存続している時間、およびメモリ割り当てたの呼び出し履歴が表示されます。図 7 にこのインスタンスのビューを示します。

[割り当て呼び出し履歴] ウィンドウとインスタンスのビューを表示する C++ メモリの [ヒープの表示] ウィンドウ
図 7. [割り当て呼び出し履歴] ウィンドウとインスタンスのビューを表示する C++ メモリの [ヒープの表示] ウィンドウ

CPU のグラフは、コンピューターのすべてのコアに占める割合として、アプリケーションの CPU 使用率を表示します。つまり、CPU 使用率が不必要に高くなる操作や、CPU を最大限に活用していない処理などを特定できます。大量のデータがあり、各レコードを個別に処理する例を考えてみます。この処理をデバッグすると、4 つのコアを搭載するコンピューターの CPU グラフが 25% をわずかに下回る位置で停滞していることがわかったとします。これは、コンピューターの全コアを使ってデータを並列処理し、アプリケーションのパフォーマンスを向上できる可能性があることを示します。

Visual Studio 2015 Update 1 では、Visual Studio Diagnostics チームがさらに 1 歩踏み込んで、[CPU 使用率] タブにデバッガー統合型の CPU プロファイラーを追加しました。このプロファイラーは、アプリケーションで CPU を使用している関数の詳細情報を提供します (図 8 参照)。たとえば、ユーザーが入力した電子メール アドレスの形式が有効かどうかを正規表現を使用して検証するコードがあるとします。有効な電子メール アドレスが入力された場合はコードが高速に実行されますが、形式が不適切な場合は有効かどうかを判断するのに約 2 秒かかることがパフォーマンス ヒントに示されています。[診断ツール] ウィンドウを見ると、この処理での CPU 使用率が急上昇しています。そこで、[CPU 使用率] タブの呼び出し履歴を確認すると、正規表現のマッチングに CPU が利用されていることがわかります (図 8 参照)。これで C# の正規表現に問題があることがわかります。複雑なステートメントのマッチングに失敗すると、文字列全体を処理するコストが高くなっている可能性があります。パフォーマンス ヒントと [診断ツール] ウィンドウの CPU 使用率ツールを使用すると、正規表現を使用するすべての状況で、アプリケーションのパフォーマンスがすぐに許容範囲を超えることがわかります。そこで、代わりになんらかの標準文字列操作を使用するようにコードを変更して、不適切なデータが入力されても優れたパフォーマンスが得られるようにします。これを運用段階まで放置して、その後解決するとなるとコストは飛躍的に増大する可能性があります。さいわい、このデバッガー統合型ツールを使用すれば、開発中に設計を変更して一貫したパフォーマンスを確保できます。[診断ツール] ウィンドウの詳細については、aka.ms/diagtoolswindow (英語) を参照してください。

正規表現マッチングの CPU 使用率を示す [CPU 使用率] タブ
図 8. 正規表現マッチングの CPU 使用率を示す [CPU 使用率] タブ

次は、.NET のデバッグ専用に行われた機能強化を取り上げます。

[ウォッチ] ウィンドウと [イミディエイト] ウィンドウにおけるラムダ サポート

LINQ などのラムダ式は、データのコレクションを素早く処理するためによく使用される非常に強力な手段です。これにより、1 行のコードで複雑な操作が可能になります。デバッガーのウィンドウで式の変更をテストしたり、デバッガーでコレクションを手動で展開するのではなく、LINQ を使用してコレクションにクエリすることはよくあります。たとえば、アプリケーションからアイテムのコレクションにクエリして、結果が 0 件だったとしましょう。目的の条件に一致するアイテムが含まれているのはわかっています。そこで、まず、コレクションにクエリして、リストの個別の要素を抽出します。その結果、目的の条件と一致する要素があることはわかりましたが、文字列の大文字と小文字に不一致があることが示されています。確かに、大文字と小文字の違いには注意を払っていませんでした。文字列比較で大文字と小文字を区別しないようにクエリを変更する必要があるという仮説を立てます。この仮説をごく簡単にテストするには、[ウォッチ] ウィンドウまたは [イミディエイト] ウィンドウに新しいクエリを入力して、想定する結果が返されるかどうかを確認します (図 9 参照)。

残念ながら、Visual Studio 2015 よりも前のバージョンでは、デバッガーのウィンドウにラムダ式を入力するとエラー メッセージが表示されます。そこで、この機能を求める多数の要望に答えて、デバッガーのウィンドウでラムダ式を使用できるようになりました。

ラムダ式の評価を 2 つ含む [イミディエイト] ウィンドウ
図 9. ラムダ式の評価を 2 つ含む [イミディエイト] ウィンドウ

.NET エディット コンティニュの機能強化

Visual Studio でよく使われているデバッグの生産性向上機能がエディット コンティニュです。エディット コンティニュにより、デバッガーで停止中にコードを変更できるようになります。つまり、その編集によって問題が解決するかどうかを確かめるために、デバッグを停止、再コンパイルして、同じ場所までアプリケーションを実行する必要はありません。ただし、エディット コンティニュを使用する際の最も大きなストレスは、変更を加えて実行を再開しようとすると、デバッグ中は変更を適用できないというメッセージが表示されることです。エディット コンティニュがサポートできない言語機能 (ラムダ式や非同期メソッドなど) をフレームワークが新たに追加し続けるにつれて、この問題が大きくなってきました。

この状況を改善するため、マイクロソフトは以前サポート対象ではなかったエディットの種類に対するサポートをいくつか追加しています。そのため、デバッグ時に変更を正しく適用できる場面が大幅に増えています。強化された機能には、ラムダ式の変更、匿名メソッドや非同期ソッドの編集、動的型の操作、C# 6.0 の機能 (たとえば、文字列の補完や null 条件演算子) などがあります。サポート対象のエディット機能の完全な一覧については、aka.ms/dotnetenc (英語) を参照してください。エディットを行って、エディットを適用できないというエラー メッセージが表示される場合、コンパイラは、エディット コンティニュを使用してエディット内容をコンパイルできなかった理由に関する詳細情報をエラー一覧に出力します。

他にも、x86 CoreCLR と x64 CoreCLR を使用するアプリケーションのサポート (エミュレーターで Windows Phone アプリをデバッグする際にエディット コンティニュを使用できることを意味します) や、リモート デバッグのサポートなどでも、エディット コンティニュの機能が強化されています。

IntelliTrace の新しいエクスペリエンス

IntelliTrace はアプリケーションに関する履歴情報を提供します。これにより、Enterprise エディションでの手探り状態のデバッグをサポートし、少ないデバッグ セッションでコードの関連部分に早く到達できるようにします。機能強化の包括的なセットが IntelliTrace に追加されたため、以前よりも使用が簡単になりました。機能強化には、イベントが発生した時点を示すタイムライン、イベントのリアルタイム表示機能、トレースポイントのサポート、[診断ツール] ウィンドウへの統合などがあります。

タイムラインでは、イベントが発生するタイミングを把握し、関連性の高いイベントのグループを特定することができます。イベントは [イベント] タブにリアルタイムに表示されます。以前のバージョンでは、IntelliTrace が収集したイベントを確認するため、デバッガーを停止状態にする必要がありました。トレースポイント統合により、デバッガーの標準機能を使用して IntelliTrace のカスタム イベントを作成できるようになります。最後に、[診断ツール] ウィンドウへの統合により、IntelliTrace のイベントがパフォーマンス情報と関連して表示されるようになるため、共通のタイムラインに IntelliTrace の豊富な情報を関連付けて、パフォーマンス問題やメモリ問題の原因を突き止めることができるようになります。

アプリケーションに問題があることがわかると、通常は、調査を開始する場所について仮説を立て、ブレークポイントを設定してから、シナリオを再度実行します。その仮説では問題が再現されない場合は、新たな仮説を立てて、デバッガーで適切な場所に移動し、このプロセスを再開する必要があります。IntelliTrace は、シナリオを再実行する必要性をなくすことによって、このワークフローを強化することを目指しています。

今回最初に示した、ネットワーク呼び出しが予期せず失敗する例を考えてみます。IntelliTrace を使用しないとすると、最初にエラーが表示されるのを見て、例外がスローされたときにデバッガーが停止するようにブレークポイントを設定してから、シナリオを再実行する必要があります。IntelliTrace を使用している場合は、エラーが表示されたときに [診断ツール] ウィンドウの [イベント] タブを見るだけです。そこには例外がイベントとして表示されます。このイベントを選択して、[デバッグ履歴の有効化] をクリックします。次に、例外が発生した時点のソースの場所に移動すると、[ローカル] ウィンドウと [自動変数] ウィンドウに例外情報が表示され、[呼び出し履歴ウィンドウ] に例外が発生するまでの呼び出し履歴が表示されます (図 10 参照)。

例外がスローされた場所を示す履歴デバッグ モードの Visual Studio
図 10 例外がスローされた場所を示す履歴デバッグ モードの Visual Studio

最後に、Visual Studio 2015 で最も注目すべき、C++ のデバッグ向けに行われた強化点を紹介します。

C++ エディット コンティニュ

前述のように、エディット コンティニュは生産性向上機能で、デバッガーの停止中に変更したコードが実行再開時に適用されます。つまり、デバッグを停止し、変更したアプリケーションを再コンパイルする必要はありません。以前のバージョンでは、C++ のエディット コンティニュには大きな制限事項が 2 つありました。1 つは、サポート対象が x86 アプリケーションに限定されていたことです。もう 1 つは、エディット コンティニュを有効にするためには、Visual Studio で Visual Studio 2010 C++ デバッガーを使用する必要があることです。このデバッガーには、Natvis データ視覚化のサポートなど、新しい機能が含まれていません (https://msdn.microsoft.com/ja-jp/library/jj620914.aspx 参照)。Visual Studio 2015 では、この制限事項がどちらも解消されています。エディット コンティニュは、x86 アプリケーションでも x64 アプリケーションでも C++ のプロジェクトでは既定で有効になり、プロセスやリモート デバッグにアタッチされていても機能します。

Android と iOS のサポート

世界がモバイル優先の考えに移るにつれて、多くの組織がモバイル アプリケーションを作成する必要性を認めるようになりました。プラットフォームが多様化する中、C++ は任意のデバイスや OS で使用できる数少ないテクノロジの 1 つです。多くの大規模組織では、幅広い製品で再利用できる共通のビジネス ロジックとして、共有 C++ コードを使用しています。そのため、Visual Studio 2015 では、モバイル開発者が Visual Studio から直接 Android や iOS をターゲットにするツールが用意されています。こうしたツールには、Windows での日常の C++ 作業で開発者が使い慣れた Visual Studio デバッグ エクスペリエンスが含まれています。

まとめ

Visual Studio Diagnostics チームは、Visual Studio 2015 のデバッグ エクスペリエンスに追加した幅広い機能について非常に期待を寄せています。ここで説明した IntelliTrace 以外の機能は、Visual Studio 2015 の Community エディションで利用できます。

チームの進捗状況と今後の機能強化については、aka.ms/diagnosticsblog (英語) のチーム ブログで確認できます。今回取り上げた機能をお試しいただき、デバッグのニーズを満たすために今後 Visual Studio を強化すべき方法についてのご意見をお寄せください。ブログ投稿にご意見やご質問をお寄せいただくことも、IDE の右上部にある [フィードバックの送信] アイコンに移動して Visual Studio から直接フィードバックをお送りいただくこともできます。


Andrew Hall は、中核となるデバッグ、プロファイリングや IntelliTrace のエクスペリエンス、Visual Studio 向け Android Emulator をビルドする Visual Studio Diagnostics チームの主任プログラム マネージャーです。何年にもわたって、Visual Studio のデバッガー、プロファイラー、およびコード分析ツールに直接携わってきました。

この記事のレビューに協力してくれた技術スタッフの Angelos Petropoulos、Dan Taylor、Kasey Uhlenhuth、および Adam Welch に心より感謝いたします。