共用方式為


測量啟動時的擴充功能影響

專注於 Visual Studio 2017 中的擴充功能效能

根據客戶的意見反應,Visual Studio 2017 版本的其中一個焦點領域是啟動和解決方案載入效能。 身為 Visual Studio 平台小組,我們一直在努力改善啟動和解決方案負載效能。 一般而言,我們的度量建議已安裝的擴充功能也可能會對這些案例產生相當大的影響。

為了協助使用者了解這項影響,我們在 Visual Studio 中新增了新功能,以通知使用者擴充功能變慢。 有時候,Visual Studio 會偵測到可拖慢解決方案載入或啟動速度的新擴充功能。 偵測到速度變慢時,使用者會在 IDE 中看到通知,指向新的 [管理 Visual Studio 效能] 對話方塊。 您也可以透過 [說明] 功能表存取此對話方塊,以瀏覽先前偵測到的擴充功能。

manage Visual Studio performance

本文件旨在描述擴充功能影響計算的方式,協助擴充功能開發人員。 本文件也會說明如何在本機分析擴充功能影響。 在本機分析擴充功能影響將判斷擴充功能是否會顯示為影響效能的擴充功能。

注意

本文件著重於擴充功能對啟動和解決方案載入的影響。 擴充功能也會在導致 UI 沒有回應時影響 Visual Studio 效能。 如需本主題的詳細資訊,請參閱操作說明:診斷擴充功能所造成的 UI 延遲

擴充功能如何影響啟動

擴充功能影響啟動效能的最常見方式之一,是選擇在其中一個已知的啟動 UI 內容自動載入,例如 NoSolutionExists 或 ShellInitialized。 啟動期間會啟動這些 UI 內容。 所有在其定義中包含 ProvideAutoLoad 屬性的套件,都會在該時間載入和初始化。

當我們測量擴充功能的影響時,我們主要著重於那些選擇在上述內容中自動載入的擴充功能所花費的時間。 測量時間會包含但不限於:

  • 載入同步封裝的擴充功能元件
  • 同步封裝的套件類別建構函式所花費的時間
  • 同步封裝的 Package Initialize (或 SetSite) 方法所花費的時間
  • 針對非同步封裝,上述作業會在背景執行緒上執行。 因此,作業會從監控中排除。
  • 在封裝初始化期間排程的任何非同步工作所花費的時間,以在主執行緒上執行
  • 在事件處理常式中花費的時間,尤其是 Shell 初始化的內容啟用或 Shell 殭屍狀態變更
  • 從 Visual Studio 2017 Update 3 開始,我們也會在初始化 Shell 之前開始監控閒置呼叫所花費的時間。 閒置處理常式中的長時間作業也會造成 IDE 沒有回應,並導致使用者察覺到的啟動時間。

我們已從 Visual Studio 2015 開始新增許多功能。 這些功能有助於移除自動載入套件的需求。 這些功能也會延後套件載入至更特定案例的需求。 這些案例包括使用者更確定使用擴充功能的範例,或在自動載入時減少擴充功能的影響。

您可以在下列檔案中找到這些功能的詳細資料:

以規則為基礎的 UI 內容:以 UI 內容為基礎建置的更豐富規則型引擎,可讓您根據專案類型、變體和屬性建立自訂內容。 自訂內容可用於在更特定的案例中載入套件。 這些特定案例包括具有特定功能的專案,而不是啟動。 自訂內容也可根據專案元件或其他可用詞彙,將命令可見性繫結至自訂內容。 這項功能不需要載入套件來註冊命令狀態查詢處理常式。

非同步套件支援:如果自動載入屬性或非同步服務查詢要求封裝載入,Visual Studio 2015 中的新 AsyncPackage 基礎類別可讓 Visual Studio 套件以非同步方式在背景中載入。 此背景載入可讓 IDE 保持回應。 即使擴充功能在背景初始化,而且啟動和解決方案載入等重要案例也不會受到影響,IDE 仍會回應。

異步服務:透過非同步封裝支援,我們也新增了非同步查詢服務的支援,並能夠註冊非同步服務。 更重要的是,我們正在轉換核心 Visual Studio 服務以支援非同步查詢,讓非同步查詢中的大部分工作都在背景執行緒中發生。 SComponentModel (Visual Studio MEF 主機) 是目前支援非同步查詢的主要服務之一,可讓擴充功能完全支援非同步載入。

減少自動載入擴充功能的影響

如果封裝仍然需要在啟動時自動載入,請務必將封裝初始化期間完成的工作降到最低。 將封裝初始化工作降至最低,可減少影響啟動的擴充功能機率。

可能導致套件初始化成本高昂的一些範例如下:

使用同步封裝載入,而不是非同步封裝載入

由於同步套件預設會在主執行緒上載入,因此我們鼓勵已自動載入套件的擴充功能擁有者改用非同步封裝基礎類別,如先前所述。 將自動載入套件變更為支援非同步載入,也可讓您更輕鬆地解決下列其他問題。

同步檔案/網路 IO 要求

在理想情況下,應該避免在主執行緒中發生任何同步檔案或網路 IO 要求。 其影響將取決於電腦狀態,在某些情況下可能會長時間封鎖。

使用非同步封裝載入和非同步 IO API 時,應該確定套件初始化不會在這類情況下封鎖主執行緒。 使用者也可以在背景發生 I/O 要求時,繼續與 Visual Studio 互動。

服務、元件的早期初始化

封裝初始化中的其中一個常見模式是初始化 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 並從 [收集] 功能表開啟 [收集] 對話方塊,以記錄啟動的追蹤。

perfview collect menu

預設選項會提供 CPU 耗用量的呼叫堆疊,但由於我們也有興趣封鎖時間,因此您也應該啟用執行緒時間堆疊。 設定準備就緒後,您可以按一下 [開始集合],然後在記錄開始之後開啟 Visual Studio。

停止收集之前,您想要確定 Visual Studio 已完全初始化、主視窗完全可見,而且如果您的擴充功能有任何 UI 片段會自動顯示,也會顯示它們。 當 Visual Studio 完全載入並初始化擴充功能時,您可以停止記錄以分析追蹤。

使用 PerfView 分析追蹤:

記錄完成後,PerfView 會自動開啟追蹤並展開選項。

針對此範例的目的,我們主要對 [執行緒時間堆疊] 檢視感興趣,您可以在 [進階群組] 下找到該檢視。 此檢視會顯示方法在執行緒上花費的總時間,包括 CPU 時間和封鎖時間,例如磁碟 IO 或等候控制代碼。

thread time stacks

開啟 [執行緒時間堆疊] 檢視時,您應該選擇 devenv 程序以開始分析。

PerfView 有詳細的指導方針,說明如何在自己的 [說明] 功能表下讀取執行緒時間堆疊,以取得更詳細的分析。 針對此範例的目的,我們只想要包含套件模組名稱和啟動執行緒的堆疊,以進一步篩選此檢視。

  1. GroupPats 設定為空白文字,以移除預設新增的任何群組。
  2. 除了現有的程序篩選之外,請將 IncPats 設定為包含元件名稱和啟動執行緒的一部分。 在此情況下,它應該是 devenv;Startup Thread;MakeVsSlowExtension

現在檢視只會顯示與擴充功能相關元件相關聯的成本。 在此檢視中,任何列在 [Inc (內含成本)] 資料行下啟動執行緒的時間,都會與我們的篩選擴充功能相關,且會影響啟動。

針對上述範例,一些有趣的呼叫堆疊會是:

  1. 使用 System.IO 類別的 IO:雖然追蹤中這些畫面的內含成本可能不太昂貴,但它們是問題的潛在原因,因為檔案 IO 速度會因各電腦而有所不同。

    system io frames

  2. 封鎖等候其他非同步工作的呼叫:在此情況下,內含時間代表主執行緒在非同步工作完成時封鎖的時間。

    blocking call frames

其他追蹤中的其中一個檢視,有助於判斷影響將是 [影像載入堆疊]。 您可以套用與套用至 [執行緒時間堆疊] 檢視相同的篩選,並找出因為自動載入套件所執行的程式碼而載入的所有組件。

請務必將封裝初始化例行性工作內的載入組件數目降到最低,因為每個額外的組件都會牽涉到額外的磁碟 I/O,這可能會讓較慢的電腦上的啟動速度變慢。

摘要

Visual Studio 的啟動是我們持續取得意見反應的領域之一。 如先前所述,我們的目標是讓所有使用者都能有一致的啟動體驗,無論其已安裝的元件和擴充功能為何。 我們想要與擴充功能擁有者合作,協助他們協助我們達成該目標。 上述指導方針應該有助於了解擴充功能對啟動的影響,並避免需要自動載入或以非同步方式載入它,以將對使用者生產力的影響降到最低。