Visual Studio”15“的内存溢出崩溃锐减

[原文发表地址]: Reduced Out of Memory Crashes in Visual Studio “15”

[原文发表时间]: October 12, 2016

 

这是Visual Studio “15” Preview 5中关于性能改善五部分系列中的第三篇博客。前两篇博客介绍的是在Visual Studio “15”中启动更快项目解决方案加载时间更短

Visual Studio集多种功能于一身,数百万的开发人员都依赖于它进行高效的工作。支持如此多的功能,而且随着开发者期望的响应速度的提高,内存消耗就有所增加。然而,在Visual Studio 2015中,在某些情况下的内存使用量过大。这导致了一些不利的影响,例如内存溢出崩溃,UI反应迟缓。我们收到了大量用户对于这些问题的反馈。在VS “15”中我们正在解决这些问题,同时不会削弱Visual Studio丰富的功能和性能。

我们正在优化Visual Studio的很多功能,这篇文章介绍了三个具体领域的进展: JavaScript与 TypeScript语言服务,调试器符号文件加载,VS中的Git支持。在本文中我将对每一个测试场景比较以下两个指标,来展示我们所取得的进展:

峰值虚拟内存: Visual Studio是一个32位的应用,这意味着消耗的虚拟内存可以达到4GB。超过上限的内存分配会导致Visual Studio出现内存溢出错误(OOM)而崩溃。峰值虚拟内存是一个用来度量进程的内存将如何接近所限制的4GB的指标,或者换句话说,可以度量一个进程是如何接近崩溃的指标。

峰值私有工作集:一个包含了进程执行的代码或进程涉及的数据的虚拟内存的子集需要被放在物理内存中。“工作集”是这样的物理内存消耗的度量标准。这种工作集的一部分称作“私有工作集”,是属于一个给定进程的内存,并且是单独属于这个进程的。因为这样的内存不是进程间共享的,他们在系统上的消耗相对较高。本文的测量数据包括了Visual Studio (devenv.exe)的峰值私有工作集和相关的附属进程。

 

JavaScript语言服务

超过三分之一的Visual Studio开发人员定期编写JavaScript (JS),使JS语言服务成为在相当数量的Visual Studio会话中需要加载的一部分。JS语言服务提供的功能如智能感知、代码导航和一些使JS编辑高效的功能。

为了支持这种生产力特性并且确保他们响应迅速,语言服务消耗了不少的内存。内存的使用量取决于解决方案的特性、工程的数量、文件的数量以及文件大小。此外,JS语言服务通常会和另一种语言服务一起被加载,例如C#,这会增加进程的内存压力。因此,提高JS语言服务的内存占用对减少VS中内存溢出崩溃是至关重要的。

在VS “15”中,我们想要确保Visual Studio可靠性不会被JS代码造成的内存消耗而不利地影响。为了实现这个目标而不影响JavaScript的编辑体验,在VS“15” Preview 5中我们已经将整个JS语言服务移动到一个可以与Visual Studio进行通信的附属进程Node,js中。我们还合并了JavaScript和TypeScript语言服务,这意味着我们通过实现两种语言服务被同时加载来减少内存。

为了测量对内存的影响,我们在以下场景中比较了Visual Studio 2015 Update 3 和VS “15” Preview 5中的峰值虚拟内存和峰值私有工作集:

  • 打开WebSpaDurandal解决方案。这是一个Asp.Net的例子
  • 创建并启用_references.js的自动同步
  • 打开10个JS文件
  • 进行编辑,启动完成,创建/删除文件,运行格式工具

这是比较结果:

Reduced-Out-Of-memory-Perf-JavaScript-Language-Servcie

表1:JavaScript语言服务的内存使用量

Visual Studio的峰值虚拟内存使用减少了33%,这为今天面临内存溢出崩溃的JS开发人员提供了较大的帮助。总体的峰值私有工作集代表Preview 5中Visual Studio进程和我们的附属进程的总和,与Visual Studio 2015中的差不多。

 

调试器符号文件加载

符号信息对有效的调试来说是至关重要的。Windows里大多数的微软编辑器将符号信息存储在一个PDB文件里面。一个PDB文件包含大量代码信息,例如,函数名称,可执行的二进制文件的偏移量,类的类型信息和可执行文件中定义的结构,资源文件名称等等。当Visual Studio调试器显示一个调用堆栈,计算一个变量或表达式等等时,它加载相应的PDB文件并读取相关信息。

在Visual Studio 2012之前,用复杂的natvis视图评估类型的性能不够理想。这是因为大量的类型信息会根据需要从PDB获取,这将导致对磁盘上的PDB文件随机的输入输出。在多数驱动器上这都将表现不佳。

在Visual Studio 2012 中,C++调试器中增加了一个新特性:会在早期的调试环节从PDB文件中提前提取大量的符号数据。这就显著地改进了性能。

不幸的是,在提前提取符号数据时,这种优化错误很多。在某些情况下,它导致了比需要的更多的符号数据被读取。例如,显示调用堆栈时,栈上的所有模块的符号数据都将被提取到,即使是那些在Locals 或者Watch 窗口里不需要被评估类型的数据。在大型的项目中有很多可用的符号数据模型,这将导致在每一个调试会话中使用大量的内存。

在VS “15” Preview 5中,我们已经采取措施来减少符号信息的内存使用,同时保持预先提取的性能优势。我们现在只有在模块被要求评估和显示一个变量或者表达式时启用预提取。

我们通过以下场景来测试对内存的影响:

  • 加载Unreal Engine解决方案,UE4.sln
  • 启动Unreal Engine Editor
  • 将VS调试器附加到Unreal Engine进程
  • 在E:\UEngine\Engine\Source\Runtime\Core\Public\Delegates\DelegateInstancesImpl_Variadics.inl的640行设置断点
  • 直到停在断点

这是结果:

Reduced-Out-Of-memory-Perf-Symbol-loading

表2:当VS调试器被附加到Unreal Engine进程时的内存使用情况

在此场景中由于内存溢出导致VS 2015崩溃。VS “15” Preview 5消耗虚拟内存3GB和私有工作集1.8GB。相比以前版本,这显然得到了改善,但也还不是期望的值。我们将继续在以后的VS “15”开发中减少本地调试方案的内存使用。

 

Visual Studio的Git支持

当我们在Visual Studio中引入Git支持时,我们使用了一个名为libgit2的库。对于各种操作,libgit2会映射整个git索引文件到内存中。索引文件的大小与代码库大小成正比。这意味着对于大的代码库,Git操作会导致大量的虚拟内存消耗。如果VS已经遭遇虚拟内存压力,这些消耗会导致内存溢出崩溃。

在VS “15” Preview 5,我们不再使用libgit2而是调用git.exe,因此避免了VS进程的虚拟内存消耗。我们使用git.exe不仅为了减少了VS进程的内存使用量,而且它也使我们增加和构建功能更加容易。

为了测量一个Git操作对于内存增量的影响,我们在下面的场景中比较了Visual Studio 2015 Update 3和VS “15” Preview 5:

  • 打开Team Explore的 Chromium repo
  • 到“Changes”面板查看待定的更改
  • 点击F5进行刷新

这是结果:

Reduced-Out-Of-memory-Perf-Git-Support-in-Visual-Studio

表3:当Team Explore的“Changes”面板被刷新时内存使用量的增长情况

VS 2015中,在更新操作期间虚拟内存消耗大约300MB。在VS “15”中,没有显著的虚拟内存的增加。VS 2015的私有工作集的增量是79MB,而VS “15”是72MB,完全是来自git.exe.

 

总结

在 VS “15”中,我们正在努力减少Visual Studio的内存使用量。在这篇文章中,我介绍了我们在三个功能上所取得的进展。但是,在取得成功的道路上我们仍然有很多的工作要做。

您可以通过以下途径来帮助我们:

第一种,我们会观察所有发布版本的遥测,包括预发布版本。所以请下载并使用VS “15” Preview 5。我们能在您的日常使用中获取到的外部资源越多,得到的信息就越准确,这将会极大地帮助到我们。

第二种,使用Report-a-problem工具来报告高内存(或者其他质量)问题。报告里最好能够通过提供给我们一个例子或者一个真实的方案来最终重现问题。我知道也不是只能这么做,如果能在报告问题时附加一段问题重现记录(我们的Report-a-problem工具可以帮你很容易做到)并且尽可能描述更多的信息就更好了。