测量启动中的扩展影响

专注于 Visual Studio 2017 中的扩展性能

根据客户反馈,Visual Studio 2017 版本的重点领域之一是启动和解决方案加载性能。 作为 Visual Studio 平台团队,我们一直在努力提高启动和解决方案加载性能。 一般来说,我们的度量表明已安装的扩展也会对这些方案产生相当大的影响。

为了帮助用户了解这种影响,我们在 Visual Studio 中添加了一项新功能,以通知用户扩展速度缓慢。 有时,Visual Studio 会检测降低解决方案加载或启动速度的新扩展。 检测到速度变慢时,用户会在 IDE 中看到一条通知,指出他们指向新的“管理 Visual Studio 性能”对话框。 还可以始终通过“帮助”菜单访问此对话框来浏览以前检测到的扩展。

manage Visual Studio performance

本文档旨在通过描述如何计算扩展影响来帮助扩展开发人员。 本文档还介绍了如何在本地分析扩展影响。 本地分析扩展影响将确定扩展是否显示为影响性能的扩展。

注意

本文档重点介绍扩展对启动和解决方案加载的影响。 当扩展导致 UI 无响应时,扩展也会影响 Visual Studio 性能。 有关本主题的详细信息,请参阅 如何:诊断扩展导致的 UI 延迟。

扩展如何影响启动

扩展影响启动性能的最常见方式之一是在 NoSolutionExists 或 ShellInitialized 等已知启动 UI 上下文中选择自动加载。 启动期间激活这些 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 交互。

服务、组件的早期初始化

包初始化中的常见模式之一是初始化包或方法中由该包constructorinitialize使用或提供的服务。 虽然这可确保服务可供使用,但如果这些服务未立即使用,它也可能会增加包加载不必要的成本。 相反,应按需初始化此类服务,以最大程度地减少包初始化中完成的工作。

对于包提供的全局服务,可以使用 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

此示例显示,在 Visual Studio 启动时,GUID 为 GUID“3cd7f5bf-6662-4ff0-ade8-97b5ff12f39c”的包花费了 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;启动线程;MakeVsSlowExtension

现在,视图仅显示与与扩展相关的程序集关联的成本。 在此视图中,启动线程的 Inc(非独占成本) 列下列出的任何时间都与我们的筛选扩展相关,并且会影响启动。

对于上面的示例,一些有趣的调用堆栈是:

  1. 使用 System.IO 类的 IO:尽管这些帧的包容性成本在跟踪中可能不太昂贵,但它们是问题的潜在原因,因为文件 IO 速度因计算机而异。

    system io frames

  2. 阻止等待其他异步工作的调用:在这种情况下,非独占时间表示在完成异步工作时阻止主线程的时间。

    blocking call frames

跟踪中的其他视图之一,用于确定影响将是 图像加载堆栈。 可以应用应用于 线程时间堆栈 视图的相同筛选器,并找出由于自动加载包执行的代码而加载的所有程序集。

请务必将包初始化例程中加载的程序集数降到最低,因为每个附加程序集都涉及额外的磁盘 I/O,这可能会大大降低在较慢计算机上启动的速度。

总结

Visual Studio 的启动是我们不断获得反馈的领域之一。 如前所述,我们的目标是让所有用户都能获得一致的启动体验,而不考虑他们已安装的组件和扩展。 我们希望与扩展所有者合作,帮助他们帮助我们实现这一目标。 上述指南应该有助于了解对启动的扩展影响,并避免需要自动加载或异步加载它,以最大程度地降低对用户工作效率的影响。