Visual Studio 2017 で拡張機能のパフォーマンスに焦点を当てる
お客様からのフィードバックに基づいて、Visual Studio 2017 リリースの焦点領域の 1 つは、起動とソリューションの読み込みパフォーマンスとなっています。 Visual Studio プラットフォーム チームは、起動とソリューションの読み込みパフォーマンスの向上に取り組んでいます。 測定によれば、一般に、インストールされている拡張機能は、これらのシナリオに大きな影響を与える可能性があります。
ユーザーがこの影響を理解できるように、Visual Studio に低速な拡張機能をユーザーに通知する新しい機能を追加しました。 場合によっては、ソリューションの読み込みまたは起動の速度を低下させる新しい拡張機能が Visual Studio によって検出されることがあります。 速度低下が検出されると、IDE でユーザーに通知が表示され、新しい [Visual Studio のパフォーマンスの管理] ダイアログが表示されます。 以前に検出された拡張機能を参照するため、いつでも [ヘルプ] メニューからこのダイアログにアクセスできます。
このドキュメントでは、拡張機能の影響の計算方法を記述することによって、拡張機能の開発者を支援します。 このドキュメントでは、拡張機能の影響をローカルで分析する方法についても説明します。 拡張機能の影響をローカルで分析すると、拡張機能がパフォーマンスに影響する拡張機能として表示されるかどうかを判断できます。
Note
このドキュメントでは、拡張機能が起動とソリューションの読み込みに与える影響に焦点を当てています。 また、拡張機能は、UI が応答しなくなったときの Visual Studio のパフォーマンスにも影響します。 このトピックの詳細については、「方法: 拡張機能による診断 UI の遅延」を参照してください。
拡張機能が起動に与える影響
拡張機能が起動のパフォーマンスに影響を与える最も一般的な方法の 1 つは、NoSolutionExists や ShellInitialized などの既知の起動 UI コンテキストのいずれかで自動読み込みを選択することです。 これらの UI コンテキストは、起動時にアクティブ化されます。 これらのコンテキストがあり、定義に ProvideAutoLoad
属性を含むパッケージは、その時点で読み込まれ、初期化されます。
拡張機能の影響を測定する際は、主に、上記のコンテキストで自動読み込みを選択する拡張機能によって費やされた時間に重点を置いています。 測定される時間には次のものが含まれますが、これに限定されません。
- 同期パッケージの拡張機能アセンブリの読み込み
- 同期パッケージのパッケージ クラス コンストラクターに費やされた時間
- 同期パッケージのパッケージ Initialize (または SetSite) メソッドで費やされた時間
- 非同期パッケージの場合、上記の操作はバックグラウンド スレッドで実行されます。 そのため、操作は監視から除外されます。
- パッケージの初期化中に、メイン スレッドで実行されるようにスケジュールされている任意の非同期作業に費やされた時間
- イベント ハンドラーで費やされた時間 (具体的には、シェルで初期化されたコンテキストのアクティブ化またはシェルのゾンビ状態の変更)
- Visual Studio 2017 Update 3 以降では、シェルが初期化される前に、アイドル状態の呼び出しに費やされた時間の監視も開始します。 アイドル ハンドラーでの長時間の操作も、IDE の無応答の原因となり、ユーザーが認識する起動時間に寄与します。
Visual Studio 2015 以降で多くの機能を追加しました。 これらの機能を使用すると、パッケージを自動的に読み込む必要がなくなります。 また、この機能により、パッケージを読み込む必要性がより具体的なケースに合わせて先送りされます。 このような場合には、ユーザーが拡張機能をより確実に使用したり、自動的に読み込みを行うときに拡張機能の影響を軽減したりする例が含まれます。
これらの機能の詳細については、次のドキュメントを参照してください。
ルールベースの UI コンテキスト: UI コンテキストを中心に構築された高度なルールベースのエンジンを使用すると、プロジェクトの種類、フレーバー、属性に基づいてカスタム コンテキストを作成できます。 カスタム コンテキストを使用すると、より具体的なシナリオでパッケージを読み込むことができます。 これらの特定のシナリオには、起動ではなく特定の機能を持つプロジェクトが存在することが含まれます。 カスタム コンテキストでは、プロジェクト コンポーネントまたはその他の使用可能な使用条件に基づいて、コマンドの可視性をカスタム コンテキストに関連付けることもできます。 この機能により、コマンド ステータス クエリ ハンドラーを登録するためのパッケージを読み込む必要がなくなります。
非同期パッケージのサポート: Visual Studio 2015 の新しい AsyncPackage 基底クラスを使用すると、パッケージの読み込みが自動読み込み属性または非同期サービス クエリによって要求された場合に、Visual Studio パッケージをバックグラウンドで非同期的に読み込むことができます。 このバックグラウンド読み込みによって、IDE の応答性を維持できます。 拡張機能がバックグラウンドで初期化されている間も、IDE の応答性が維持され、起動やソリューションの読み込みなどの重要なシナリオに影響を与えることはありません。
非同期サービス: 非同期パッケージをサポートすることで、サービスを非同期にクエリし、非同期サービスを登録できるようになりました。 さらに重要な点として、非同期クエリの処理の大部分がバックグラウンド スレッドで発生するように、コア Visual Studio サービスを変換して非同期クエリをサポートする作業を進めています。 SComponentModel (Visual Studio MEF ホスト) は、拡張機能で非同期読み込みを完全にサポートできる形で非同期クエリをサポートする主なサービスの 1 つです。
自動で読み込まれる拡張機能の影響を軽減する
起動時にパッケージを自動的に読み込む必要がある場合は、パッケージの初期化中に実行される作業を最小限に抑えることが重要です。 パッケージ初期化作業を最小化すると、拡張機能が起動に影響する可能性が低くなります。
パッケージの初期化に負荷がかかる可能性のある例を次に示します。
非同期パッケージ読み込みではなく、同期パッケージ読み込みを使用する
既定では、同期パッケージはメイン スレッドに読み込まれるため、自動的に読み込まれるパッケージがある拡張機能の所有者は、前に説明したように、非同期パッケージの基底クラスを使用することをお勧めします。 自動読み込みされるパッケージを変更して非同期読み込みをサポートすることにより、以下の他の問題を簡単に解決できるようになります。
同期ファイル/ネットワーク IO 要求
メイン スレッドでは、同期ファイルまたはネットワーク IO の要求をすべて回避することが理想的です。 影響はコンピューターの状態によって異なり、場合によっては長期間ブロックされることがあります。
非同期パッケージ読み込みと非同期 IO API を使用すると、そのような場合にパッケージの初期化によってメイン スレッドがブロックされなくなります。 また、ユーザーは、バックグラウンドで I/O 要求が発生しても、Visual Studio との対話を続けることができます。
サービス、コンポーネントの早期初期化
パッケージの初期化における一般的なパターンの 1 つは、そのパッケージによって使用または提供されるサービスを、パッケージの constructor
または initialize
メソッドで初期化することです。 これにより、サービスを使用できるようになりますが、これらのサービスがすぐに使用されない場合は、パッケージの読み込みに不要なコストを追加する可能性があります。 このようなサービスは、パッケージの初期化で実行される作業を最小限にするために、オンデマンドで初期化する必要があります。
パッケージによって提供されるグローバル サービスの場合、コンポーネントによって要求された場合にのみ、関数を受け取ってサービスを遅延初期化する AddService
メソッドを使用できます。 パッケージ内で使用されるサービスについては、Lazy<T> または AsyncLazy<T> を使用して、最初の使用時にサービスが初期化またはクエリされるようにすることができます。
アクティビティ ログを使用して、自動読み込みされる拡張機能の影響を測定する
Visual Studio 2017 Update 3 以降では、Visual Studio アクティビティ ログに、起動時およびソリューションの読み込み中のパッケージのパフォーマンスへの影響に関するエントリが含まれるようになりました。 これらの測定値を表示するには、/log スイッチを使用して Visual Studio を開き、ActivityLog.xml ファイルを開く必要があります。
アクティビティ ログでは、エントリは [Visual Studio のパフォーマンスの管理] ソースの下に表示され、次の例のようになります。
Component: 3cd7f5bf-6662-4ff0-ade8-97b5ff12f39c, Inclusive Cost: 2008.9381, Exclusive Cost: 2008.9381, Top Level Inclusive Cost: 2008.9381
この例は、GUID が "3cd7f5bf-6662-4ff0-ade8-97b5ff12f39c" のパッケージが Visual Studio の起動時に 2008 ミリ秒を費やしたことを示しています。 Visual Studio では、パッケージの影響を計算する際に、最上位レベルのコストが主要な数値と見なされることに注意してください。これは、ユーザーがパッケージの拡張機能を無効にしたときに得られる節約量です。
PerfView を使用して、自動読み込みされる拡張機能の影響を測定する
コード分析はパッケージの初期化を遅くする可能性があるコード パスを特定するのに役立ちますが、PerfView などのアプリケーションを使用してトレースを利用して、Visual Studio の起動時にパッケージの読み込みの影響を把握することもできます。
PerfView は、システム全体のトレース ツールです。 このツールを使用すると、CPU 使用率やシステム呼び出しのブロックによって、アプリケーションのホット パスを理解することができます。 PerfView を使用してサンプル拡張機能を分析する簡単な例を次に示します。
コード例:
この例は、次のサンプル コードに基づいています。これは、一般的な遅延の原因を示すために設計されています。
protected override void Initialize()
{
// Initialize a class from another assembly as an example
MakeVsSlowServiceImpl service = new MakeVsSlowServiceImpl();
// Costly work in main thread involving file IO
string systemPath = Environment.GetFolderPath(Environment.SpecialFolder.Windows);
foreach (string file in Directory.GetFiles(systemPath))
{
DateTime creationDate = File.GetCreationTime(file);
}
// Costly work after shell is initialized. This callback executes on main thread
KnownUIContexts.ShellInitializedContext.WhenActivated(() =>
{
DoMoreWork();
});
// Start async work on background thread
DoAsyncWork().Forget();
}
private async Task DoAsyncWork()
{
// Switch to background thread to do expensive work
await TaskScheduler.Default;
System.Threading.Thread.Sleep(500);
}
private void DoMoreWork()
{
// Costly work
System.Threading.Thread.Sleep(500);
// Blocking call to an asynchronous work.
ThreadHelper.JoinableTaskFactory.Run(async () => { await DoAsyncWork(); });
}
PerfView を使用したトレースの記録:
拡張機能がインストールされている Visual Studio 環境をセットアップしたら、PerfView を開き、[収集] メニューから [収集] ダイアログを開くことで、起動のトレースを記録できます。
既定のオプションでは、呼び出し履歴の CPU 使用量が提供されますが、ブロック時間にも関心があるため、スレッド時間スタックを有効にする必要もあります。 設定の準備ができたら、[コレクションの開始] をクリックし、記録の開始後に Visual Studio を開きます。
コレクションを停止する前に、Visual Studio が完全に初期化されていること、メイン ウィンドウが完全に表示されていること、および拡張機能に自動的に表示される UI 要素が含まれている場合はそれらも表示されていることを確認します。 Visual Studio が完全に読み込まれ、拡張機能が初期化されたら、記録を停止してトレースを分析することができます。
PerfView を使用したトレースの分析:
記録が完了すると、PerfView によってトレースと展開のオプションが自動的に開きます。
この例では、主に、[詳細グループ] の下にある [スレッド時間 スタック]ビューに注目します。 このビューには、CPU 時間とブロック時間の両方を含む、メソッドごとにスレッドに費やされた合計時間が表示されます。これには、ディスク IO やハンドルでの待機が含まれます。
[スレッド時間スタック] ビューを開くときに、分析を開始する devenv プロセスを選択します。
PerfView には、詳細な分析を行うために、独自のヘルプ メニューのスレッド時間スタックを読み取る方法についての詳細なガイダンスがあります。 この例では、パッケージ モジュール名と起動スレッドを含むスタックだけを含めるように、このビューをさらにフィルター処理します。
- [GroupPats] を空のテキストに設定して、既定で追加されたグループ分けをすべて削除します。
- 既存のプロセス フィルターに加えて、アセンブリ名と起動スレッドの一部も含めるように [IncPats] を設定します。 この場合は、devenv;Startup Thread;MakeVsSlowExtension にする必要があります。
これで、ビューには、拡張機能に関連するアセンブリに関連付けられているコストのみが表示されるようになりました。 このビューでは、起動スレッドの [包括 (包括コスト)] 列の下に表示されるすべての時刻は、フィルター処理された拡張機能に関連し、起動に影響します。
上記の例では、いくつかの興味深い呼び出し履歴は次のようになります。
System.IO
クラスを使用した IO: これらのフレームの包括コストは、トレースではコストが高くならない可能性がありますが、ファイル IO 速度はコンピューターによって異なるため、問題の原因となる可能性があります。他の非同期作業を待機しているブロッキング呼び出し: この場合、包括時間は、非同期処理の完了時にメイン スレッドがブロックされた時間を表します。
影響を判断するのに役立つ、トレース内の他のビューの 1 つは、イメージの読み込みスタックです。 [スレッド時間スタック] ビューに適用されているものと同じフィルターを適用し、自動的に読み込まれるパッケージによって実行されるコードのために読み込まれたすべてのアセンブリを確認することができます。
パッケージ初期化ルーチン内で読み込まれるアセンブリの数を最小限に抑えることが重要です。これは、追加の各アセンブリに余分なディスク I/O が必要になるためです。これにより、遅いコンピューターの速度が大幅に低下する可能性があります。
まとめ
Visual Studio の起動は、継続的にフィードバックを受け取る領域の 1 つです。 前述のように、私たちの目標は、インストールされているコンポーネントと拡張機能に関係なく、すべてのユーザーが一貫した起動エクスペリエンスを持つことです。 この目標を達成するために、拡張機能の所有者への協力やサポートを行います。 上記のガイダンスは、拡張機能が起動時に与える影響を理解し、ユーザーの生産性への影響を最小限に抑えるために、自動読み込みの必要性をなくしたり、非同期的に読み込んだりする場合に役立ちます。