使用Visual Studio进行性能问题排查

在现代软件开发过程中,性能优化是一个不可忽视的重要环节。尽管有像PerfView和DebugDiag这样的工具可以帮助排查性能问题,但开发者在日常开发中往往不会使用这些工具。本文将介绍如何使用Visual Studio来有效地排查性能相关问题。

微软在工具和技术方面不断进步,Visual Studio不仅能够帮助开发者测量代码性能,还能深入分析性能问题的根源。例如PerfTips、IntelliTrace和Performance & Diagnostic Hub等工具。

应用程序审查

在本系列文章的第一部分,建立了应用程序的一般行为。图1显示了“获取方向”按钮点击处理代码。代码本身并不复杂,以下是该函数内部发生事情的高层次总结。

输入的起点和目的地会被验证。如果验证失败,将调用RouterView类的DisplayAddressValidationError方法。调用SatelliteManager类的Connect方法。如果此方法返回失败,则调用RouteViewer类的DisplaySatelliteConnectionError。如果SatelliteManager.Connect成功,则调用GetSatelliteLocationFromAddress方法。调用RouteCalculator类的CalculateRoute方法,并返回RouteDirection对象。最后,调用RouteViewer类的DisplayDirection方法。

如果SatelliteManager.Connect方法返回失败,则调用RouteViewer类的DisplaySatelliteConnectionError方法。从应用程序行为知道,当用户点击“获取方向”按钮时,应用程序会消耗大量的CPU周期和内存资源。然而,关键问题是如何确定这些代码行的成本。更具体地说,想知道执行这些代码行需要多长时间,以及每行代码的CPU/内存成本是多少。

遵循Joe Duffy的建议,这些正是开发者在开发过程中应该问自己的问题,以便他们了解代码的成本。这些问题也是Visual Studio团队在IDE中引入PerfTip和Diagnostics Tools特性的原因。

在Visual Studio中逐步检查代码性能

首先,让在函数的开始和结束处添加断点。运行应用程序。一旦第二个断点被击中,视图应该类似于这里显示的。

在函数的末尾,会看到红色矩形中显示的34,137毫秒已过去。这就是所谓的性能工具提示(简称PerfTip)。这是一个估计数字,显示了从上一步或自上一个断点以来运行代码所需的时间。在这种情况下,运行这个函数大约需要34秒。

在右侧,还可以看到两个关键测量值。这两个断点之间的内存和CPU消耗。正如诊断会话窗口所示,最初CPU消耗相当高,然后内存使用量接近2GB。这些测量值与应用程序在Visual Studio外部运行时观察到的相当一致。

应该在调试器下多次运行应用程序,以确保应用程序行为在合理样本大小下是一致的。在确保问题可重现后,可以返回并逐步执行相同的代码块,并观察应用程序行为。这有助于缩小到最昂贵的特定代码行。

接下来的屏幕显示了逐步执行下一行代码的过程,它既不花费更长的时间来运行代码,也没有显著的内存/CPU资源消耗。由于这段代码按预期执行,只看几行。

接下来,可以看到执行RouteCalculator类的CalculateRoute方法。执行这行代码大约需要10秒。可以看到CPU消耗相当高,尽管内存消耗很低(大约60MB),与整个方法的2GB相比微不足道。

下两行代码的资源消耗相当微不足道,但执行RouteViewer类的DisplayDirection方法大约需要17秒,而内存消耗也上升到大约2GB。

逐步执行函数中剩余的代码行没有显著的内存/CPU资源消耗。

深入性能分析

到目前为止的分析,知道两个函数RouteCalculator类的CalculateRoute方法和RouteViewer类的DisplayDirection是btnDirections_Click总共34秒中的27秒。也知道:

RouteCalculator类的CalculateRoute方法主要负责高CPU使用率。RouteViewer类的DisplayDirection负责高内存消耗。这意味着开发者不仅可以知道每行代码执行需要多长时间,还可以确切知道它的内存/CPU成本。

但等等,这个分析并没有就此停止。诊断工具还提供了进一步调查这些资源消耗背后原因的机制。与内存使用相关的“拍摄快照”和与CPU使用相关的“记录CPU配置文件”按钮可以帮助理解特定代码行的行为。

首先,让尝试理解应用程序在执行RouteCalculator类的CalculateRoute方法时消耗CPU周期的原因。为此,需要点击记录CPU配置文件按钮并运行RouteCalculator类的CalculateRoute代码行。这里的分析结果如下所示:

在右侧,这些结果以表格格式显示,列包含函数名称和总CPU百分比。表中的数据按总CPU消耗降序排序。这里的结果表明,RouteCalculator类的XmlDataProcessor方法消耗了96%的CPU周期,而XmlDocument.LoadXml方法负责其中72%的周期。

双击该表中的函数名称将显示调用树视图,显示方法调用链。这表明在RouteCalculator类的XmlDataProcessor方法消耗的96%时间中,72%的时间被RouteCalculator类的XmlDataProcessor方法使用,明确指出开发者应该关注的地方以找到问题的根本原因。

双击调用树视图中的函数将显示相关源代码。正如从代码中看到的,RouteCalculator类的XmlDataProcessor方法正在调用XmlDocument.LoadXml方法,导致高CPU。

还看到XmlDataProcessor方法使用任务并行库(TPL)数据并行Parallel.For调用。这解释了为什么PerfView跟踪显示多个线程调用此方法。当然,Visual Studio可以使用并行堆栈显示相同的信息。

现在知道了CPU使用背后的原因,让将注意力转向导致高内存消耗的原因。知道从之前的分析中,RouteViewer类的DisplayDirection方法负责高内存消耗。分析内存相关问题的典型方法是拍摄两个内存快照:一个在高内存消耗之前,一个在之后。这两个快照可以比较和分析以找出有问题的对象。

应该在执行RouteViewer类的DisplayDirection方法之前使用“拍摄快照”按钮拍摄第一个内存快照。内存快照的结果以表格形式显示。这个表包含GC堆中的对象数量和堆的大小。

点击对象数量或堆大小值将显示一个表格,显示堆上的所有对象。现在执行RouteViewer类的DisplayDirection方法并拍摄另一个快照,因为内存使用量已经上升。此时,内存使用选项卡也将显示第二个快照的结果。这些结果清楚地表明,对象计数和堆大小都增加了。

点击第二个快照中堆大小变化链接将列出堆中的对象,按两个快照之间的大小差异排序。这表明XmlNode对象数组位于顶部。点击表中的该对象本身将填充底部选项卡中的引用图,显示该对象是如何引用的。

XmlNode[]的所有引用类型也可以查看。在这里,可以看到XmlNode[]对象引用了大量的XmlElement对象。这次分析为提供了一个合理的想法,即哪些对象负责内存消耗。可以检查RouterViewer.directionPoints的代码。它是一个静态字段,也在一个紧密循环中填充,导致高内存消耗。

底线是,有了Visual Studio,开发者可以在日常工作流程中关注应用程序的性能,而不必依赖任何其他工具。

这一系列的两篇文章展示了Visual Studio功能如何帮助开发者在日常工作流程中排查复杂的性能问题。

沪ICP备2024098111号-1
上海秋旦网络科技中心:上海市奉贤区金大公路8218号1幢 联系电话:17898875485