2016 年 3 月

第 31 卷,第 3 期

Visual Studio - Visual Studio 2015 中的调试改进

作者 Andrew Hall | 2016 年 3 月

尽管我们在编写代码方面付出了巨大努力,而且这些代码在初次使用时能正常运行,但作为开发人员,我们确实在调试方面花费了大量时间。Visual Studio 2015 引入了全新的功能和改进,帮助开发人员在开发周期早期确定问题,并提高查找和修复 bug 的能力。在本文中,我将介绍面向 .NET 和 C++ 开发人员增添的 Visual Studio 2015 改进。

我们先了解一下,两个可用性改进以及在增加的在调试 Microsoft .NET Framework 和 C++ 过程中使用的性能工具。然后,我将深入探讨专门针对 .NET 开发推出的一系列改进,最后介绍面向 C++ 开发人员的新开发。

断点设置

断点是调试器的基本功能,在上次启动调试器时使用断点的频率会非常高。有时使用条件断点可帮助更快地找出造成 bug 的原因。在 Visual Studio 2015 中,可以通过引入处于代码上下文的非模式断点设置窗口,更轻松地利用条件断点。还可以使您轻松地在同一个窗口中组合不同的设置。

回顾一下,Visual Studio 2015 提供了以下设置的断点:

  • 条件表达式只有在满足指定条件时才会中断。这是一个逻辑对等,将 if 语句添加到代码并将断点置于 if 语句中,因此,只有条件为 true 时,才会中断。在多次调用代码但只能通过特定输入才能显示 bug 时,条件表达式非常有用,因此,必须手动检查值,然后才能重新开始调试,会让人感到厌烦。
  • 只有在断点达到一定次数后,命中计数才会中断。这些设置在以下情况中非常有用:多次调用代码,您要么确切地知道什么时候失败,要么只有一个总体概念“它在至少一定数量的迭代后会失败”。
  • 当特定线程、进程或计算机发生断点时筛选器中断。在您只想停止单个实例时,筛选器断点对于调试并行运行的代码非常有用。
  • 记录消息(称为跟踪点)将消息打印到输出窗口,并能够自动恢复执行。在您需要进行跟踪且不想中断而手动跟踪每个值时,跟踪点在执行临时日志记录方面非常有用。

为了说明条件断点的价值,请看一下图 1 中所示的示例,其中应用程序是由某个循环内的类型进行检索。此代码适用于大多数输入字符串,只有在输入为“desktop”时才会失败。 一个选择是设置断点,然后在每次命中断点时检查 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 2015 中,Visual Studio 诊断团队直接将一系列性能工具集成到调试器中,称为 PerfTips 和诊断工具窗口。这些工具帮助您了解作为日常调试一部分的代码的性能,这样,您就能及早发现问题并做出明智的设计决策,以便您从头开始构建应用程序的性能。

一种了解您的应用程序性能的简单方法就是,使用调试器逐步调试应用程序,以获得执行每行代码所需时间的直觉印象。遗憾的是,这不太科学,因为它依赖于感知差异的能力。所以,很难分辨出需要 25 毫秒执行的操作和需要 75 毫秒执行的操作之间的差异。此外,您可以修改代码以添加捕获精确信息的计时器,但这会带来更改代码的不便。

PerfTips 通过显示执行代码所需的时间来解决这一问题,方法如下:在调试器停止时在当前行的右端显示时间(毫秒),如图 3 中所示。PerfTips 显示了应用程序在调试器的任何两个中断状态之间运行所花费的时间。这意味着,在逐步调试代码和运行到断点时,它们都能正常工作。

PerfTip 显示逐步执行函数调用所要经过的时间
图 3 PerfTip 显示逐步执行函数调用所要经过的时间

让我们看一个快速示例,说明 PerfTip 如何帮助您了解执行代码所需的时间。有这样一款应用程序,在用户单击某个按钮时,应用程序会从磁盘加载文件,然后对其进行相应处理。人们期望应用程序从磁盘加载文件只需几毫秒,但通过使用 PerfTips,您会发现这一过程花费的时间明显比预期的要长。基于这些信息,您可以修改应用程序的设计,以便用户在开发周期早期(在进行更改所付出的变得代价太大之前)单击按钮时,应用程序不会依赖于加载所有文件。要了解有关 PerfTips 的详细信息,请访问 aka.ms/perftips

诊断工具窗口显示应用程序的 CPU 和内存使用历史记录以及所有调试器中断事件(断点、步骤、异常和全部中断)。窗口提供三个选项卡: “事件”、“内存使用”和“CPU 使用”。“事件”选项卡显示所有调试器中断事件的历史记录,这意味着它包含所有 PerfTips 值的完整记录。此外,在 Visual Studio 2015 企业版中,此选项卡还包含所有 IntelliTrace 事件(本文后面会进行介绍)。图 4 显示了 Visual Studio 2015 Enterprise 中的“事件”选项卡另外,在调试会话期间更新 CPU 和内存信息,可以让您看到特定代码部分的 CPU 和内存特性。例如,您可以逐步执行方法调用,观察图表如何改变以测量该特定方法的影响。

选中“事件”选项卡的诊断工具窗口
图 4 选中“事件”选项卡的诊断工具窗口

借助内存图,您可以查看应用程序的总内存使用量,并观察应用程序内存使用的趋势。例如,图表可能会显示平稳向上的趋势,这指示应用程序正在泄露内存,并可能最终崩溃。首先,我将介绍它如何适用于 .NET Framework,然后会涵盖其用于 C++ 的体验。

在调试 .NET Framework 时,图表显示垃圾回收 (GC) 发生的时间以及内存总量;这有助于观察以下这种情况:应用程序的整体内存使用量处于一个可接受的水平,但应用程序的性能可能由于频繁的 GC(通常因分配过多的短期对象引起)而受到影响。使用“内存使用”选项卡,您可以通过“拍摄快照”按钮在任意给定时间点为内存中对象拍摄快照。还可以比较两个不同的快照,这是确认内存泄露的最简单方法。您可以拍摄快照,继续以您预期内存应无关的方式使用应用程序一段时间,然后拍摄第二个快照。图 5 显示了带有两个 .NET 快照的内存工具。

带有两个快照的诊断工具窗口的“内存使用”选项卡
图 5 带有两个快照的诊断工具窗口的“内存使用”选项卡

单击一个快照时,会打开名为堆视图的第二个窗口,向您显示内存中对象的详细信息,包括总数和其内存占用。堆视图的下半部分显示是什么具有对这些对象的引用,防止它们被视作垃圾回收(称为“根的路径”),并显示在“引用类型”选项卡中所选类型引用的其他类型。图 6 显示了说明两个快照之间差异的堆视图。

堆快照视图显示了两个 .NET 快照之间的差异
图 6 堆快照视图显示了两个 .NET 快照之间的差异

C++ 内存工具跟踪内存分配和解除分配,以了解在任何给定时间点内存中所包含的内容。要查看数据,请使用“拍摄快照”按钮创建当前分配信息的记录。还可以比较快照,查看哪些内存在两个快照之间发生更改,以便能够更轻松地跟踪您希望完全释放内存空间的代码路径中的内存泄露。选择要查看的快照后,堆视图会显示类型列表以及类型的大小,对于比较两个快照而言,就是这些数字之间的差异。在您查看想要更好地了解其内存占用情况的类型时,请选择查看该类型的实例。实例视图显示了每个实例的大小,实例在内存中保留的时间以及分配内存的调用堆栈。图 7 显示了实例视图。

显示带有分配调用堆栈的实例视图的 C++ 内存堆视图
图 7 显示带有分配调用堆栈的实例视图的 C++ 内存堆视图

CPU 图将应用程序的 CPU 使用率显示为计算机上所有核心的百分比。这意味着,这有助于识别引起不必要 CPU 使用率峰值的操作,还可以帮助识别未充分利用 CPU 的操作。下面我们看一个处理大量数据的示例,其中每个记录都可以独立进行处理。调试操作时,您会注意到,在四核计算机上 CPU 图表在略低于 25% 处浮动。这表示有机会在计算机的所有核心中并行数据处理,以达到更快的应用程序性能。

在 Visual Studio 2015 Update 1 中,Visual Studio 诊断团队在这方面更深入了一步,将集成调试器的 CPU 探查器添加到“CPU 使用”选项卡,以显示应用程序中使用 CPU 的功能的细目分类,如图 8 中所示。例如,有使用正则表达式验证用户输入的电子邮件地址格式是否有效的代码。在输入有效的电子邮件地址后,代码执行速度极快,但是,如果输入的电子邮件地址格式不正确,PerfTip 显示会花费接近 2 秒的时间以确定该地址无效。查看诊断工具窗口,您会发现在此期间,CPU 中出现了一个峰值。然后查看“CPU 使用”选项卡中的调用树,您会发现占用 CPU 的是正则表达式匹配,同样如图 8 中所示。事实证明,C# 正则表达式有一个缺点: 如果正则表达式匹配复杂语句失败,那么就要付出较高的代价来处理整个字符串。使用诊断工具窗口中的 PerfTips 和 CPU 使用工具,您可以在所有情况下使用正则表达式快速确定不可接受的应用程序性能。因此,您可以修改代码以转为使用在输入错误数据时产生更好性能结果的一些标准字符串操作。这可能会产生一个以后要修复的高开销 bug,尤其是如果它一直参与生产。幸运的是,借助集成调试器的工具,可以在开发过程中更改设计以确保获得一致的性能。要了解有关诊断工具窗口的详细信息,请访问 aka.ms/diagtoolswindow

显示匹配正则表达式的 CPU 占用率的“CPU 使用”选项卡
图 8 显示匹配正则表达式的 CPU 占用率的“CPU 使用”选项卡

接下来,让我们看一看专门为 .NET 调试所进行的改进。

“监视”和“即时”窗口中的 Lambda 支持

Lambda 表达式(如 LINQ)是一种快速处理数据集合的极其强大且常用的方法。这些表达式能够让您通过一行代码来处理复杂操作。您经常需要在调试器窗口中测试对表达式的更改,或者使用 LINQ 来查询集合,而不是在调试器中进行手动扩展。作为示例,请考虑以下情况,应用程序正在查询项目集合并返回 0 个结果。您确定有项目匹配预期条件,因此,您开始查询集合以提取列表中的不同元素。结果证实,有符合您预期条件的元素,但似乎存在字符串大小写不匹配,不过,您并不关心大小写是否完全匹配。您的假设是,在执行字符串比较时需要修改查询以忽略大小写。测试该假设的最简单方法是将新查询键入“监视”或“即时”窗口,并查看是否返回预期结果,如图 9 中所示。

遗憾的是,在 2015 年之前的 Visual Studio 版本中,将 Lambda 表达式键入调试器窗口会导致错误消息。因此,为解决这一顶级功能要求,添加了支持以在调试器窗口中使用 Lambda 表达式。

带有两个计算的 Lambda 表达式的“即时”窗口
图 9 带有两个计算的 Lambda 表达式的“即时”窗口

.NET“编辑并继续”改进

Visual Studio 中最喜爱的高效调试功能是“编辑并继续”。借助“编辑并继续”,您可以在调试器停止时更改代码,然后无需停止调试即可应用编辑,并将应用程序重新编译和运行到同一位置以验证更改是否修复此问题。但是,使用“编辑并继续”功能时,一项最令人头疼的工作就是进行编辑,尝试恢复执行后,看到一条消息,显示在调试时无法应用所做的编辑。这已经成为一个比较常见的问题,因为该框架不断地添加“编辑并继续”无法支持的新语言功能(例如,Lambda 表达式和异步方法)。

要改进此问题,Microsoft 添加了对几种以前不支持编辑类型的支持,这将显著增加调试期间成功应用编辑的次数。这些改进包括能够修改 Lambda 表达式、编辑匿名方法和异步方法、使用动态类型以及修改 C# 6.0 功能(例如,字符串插值和 null 条件运算符)。有关受支持编辑的完整列表,请访问 aka.ms/dotnetenc。如果您进行编辑并收到无法应用编辑的错误消息,请确保检查错误列表,因为编译器会提供有关为什么无法使用“编辑并继续”编译编辑的其他信息。

“编辑并继续”的其他改进包括支持使用 x86 和 x64 CoreCLR 的应用程序,意味着,在模拟器上调试 Windows Phone 应用时可以使用此功能,并支持远程调试。

全新的 IntelliTrace 体验

IntelliTrace 提供应用程序的历史信息以帮助消除您在企业版中进行调试时的主观臆测,通过更少的调试会话,让您更快找到代码的相关部分。IntelliTrace 进行了一整套改进,让它比以往更易于使用。这些改进包括显示事件发生时间的时间线、能够实时查看事件、支持跟踪点以及集成到诊断工具窗口中。

通过时间线,您可以了解事件发生的时间并找出事件组,这两者之间可能有关联。事件在“事件”选项卡中实时显示,在以前的版本中,您需要在调试器中输入中断状态才能查看 IntelliTrace 收集的事件。通过跟踪点集成,您可以使用标准调试器功能创建自定义 IntelliTrace 事件。最后,诊断工具窗口集成将 IntelliTrace 事件置于性能信息上下文中,您可以使用丰富的 IntelliTrace 信息,通过跨共同时间线关联信息来了解性能和内存问题的原因。

在发现应用程序中的问题时,您通常会建立一个假设:从哪里开始调查,放置断点,然后重新运行此方案。如果证明问题不是出现在该位置,则需要建立一个新的假设:如何在调试器中获取正确的位置,然后重新启动该进程。IntelliTrace 旨在通过避免重新运行此方案来改进这一工作流。

我们来看本文前面所提到的一个示例,其中网络调用意外失败。如果不使用 IntelliTrace,您需要找到第一次失败,并在引发异常时启用调试器中断,然后重新运行此方案。如果使用 IntelliTrace,在发现失败时,您只需查看诊断工具窗口的“事件”选项卡。异常以事件形式显示,选中它并单击“激活历史调试”。然后,您及时导航回源中发生异常的位置,“局部变量”和“自动变量”窗口会显示异常信息,“调用堆栈”窗口会填充发生异常的调用堆栈,如图 10 中所示。

位于引发异常位置的历史调试模式中的 Visual Studio
图 10 位于引发异常位置的历史调试模式中的 Visual Studio

最后,让我们看一下为 Visual Studio 2015 中 C++ 调试所做出的最显著的改进。

C++“编辑并继续”

如前所述,“编辑并继续”是一个高效功能,可以使您在调试器停止时修改代码,然后在继续执行时无需停止调试即可应用编辑,以重新编译已修改的应用程序。在以前的版本中,C++“编辑并继续”有两个明显的限制。首先,它仅支持 x86 应用程序。其次,启用“编辑并继续”导致在 Visual Studio 中使用 Visual Studio 2010 C++ 调试器,该调试器缺乏新功能(如支持 Natvis 数据可视化)(请参阅 aka.ms/natvis)。在 Visual Studio 2015 中,这些差距都得到了弥补。默认情况下,为适用于 x86 和 x64 应用程序的 C++ 项目启用“编辑并继续”,它甚至能在附加到进程和远程调试时正常运行。

Android 和 iOS 支持

随着全球步入移动先行的智能阶段,许多组织都需要创建移动应用程序。随着平台的多样化,C++ 是为数不多可以跨任何设备和 OS 使用的技术之一。许多大型组织使用共享的 C++ 代码来实现想要在广泛服务中重用的常见业务逻辑。为帮助实现这一点,Visual Studio 2015 提供工具,使移动开发人员可以直接从 Visual Studio 针对 Android 和 iOS 进行开发。这包括大家熟悉的 Visual Studio 调试体验,类似于开发人员在 Windows 上以 C++ 所进行的日常工作。

总结

Visual Studio 诊断团队感到非常兴奋,因为在 Visual Studio 2015 中,调试体验增加了大量的功能。除了 IntelliTrace 外,本文所介绍的所有功能都会在 Visual Studio 2015 的社区版中提供。

您可以在团队博客上继续关注团队的进度和进一步改进,网址如下:aka.ms/diagnosticsblog。请尝试本文所述的功能,就如何继续改进 Visual Studio 以满足您的调试需要向团队提供反馈。您可以在博客文章中留下意见和问题,或通过 IDE 右上部的“发送反馈”图标直接从 Visual Studio 发送反馈。


Andrew Hall* 是领导 Visual Studio 诊断团队的项目经理,该团队构建核心调试、分析和 IntelliTrace 体验,以及适用于 Visual Studio 的 Android 模拟器。多年来,他一直直接从事 Visual Studio 中调试器、探查器和代码分析工具方面的工作。*

衷心感谢以下技术专家对本文的审阅: Angelos Petropoulos、Dan Taylor、Kasey Uhlenhuth 和 Adam Welch