使用 Visual Studio 进行云性能工程(第二部分)





5.00/5 (2投票s)
在本文中,我将承接之前阐述的概念,并演示如何有效地使用 Visual Studio 来排查与性能相关的问题。
引言
在本文的第一部分,我们探讨了使用 PerfView 和 DebugDiag 等工具排查与性能相关问题的传统方法来构建高性能应用程序。然而,开发人员通常不会在日常开发任务中使用这些工具。在本文中,我将承接之前阐述的概念,并演示如何有效地使用 Visual Studio 来排查与性能相关的问题。
这是微软多年来大力发展其工具和技术的领域。Visual Studio 不仅能够让开发人员测量代码的性能,还提供了深入了解此类性能问题根本原因的能力。PerfTips、IntelliTrace、Performance & Diagnostic Hub 就是此类工具的示例。
审查应用程序
本文的第一部分建立了应用程序的通用行为。下图展示了 获取方向 按钮的点击处理程序代码。
代码并不复杂。以下是对该函数内部发生情况的概要总结。
- 验证起点和终点的输入。如果验证失败,则调用
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 消耗。正如 Diagnostic Session 窗口所示,最初 CPU 消耗很高,然后内存使用量接近 2GB。这些测量值与应用程序在 Visual Studio 外部运行时观察到的情况非常一致。
应该在调试器下运行应用程序几次,以确保应用程序行为在合理的样本大小上是一致的。在确保问题可重现后,我们可以返回来逐行逐步调试同一块代码,并观察应用程序的行为。这有助于缩小范围,找出成本最高的特定代码行。
以下屏幕显示了逐步调试下一行代码的过程,这些代码运行时间不长,也没有显著的内存/CPU 资源消耗。由于此代码的运行符合预期,我们只看几行。
接下来,您可以看到 RouteCalculator.CalculateRoute
方法的执行。执行此特定代码行大约花费了 10 秒。我们可以看到 CPU 消耗很高,尽管内存消耗很低(约 60 MB),与整个方法的 2GB 相比微不足道。
接下来的两行代码的资源消耗微不足道,但 RouteViewer.DisplayDirection
方法的执行花费了大约 17 秒,同时内存消耗也上升到约 2GB。
逐步调试此函数中其余代码行时,没有显示出显著的内存/CPU 资源消耗。
深入分析性能
通过迄今为止的分析,我们知道 RouteCalculator.CalculateRoute
方法和 RouteViewer.DisplayDirection
这两个函数是导致 btnDirections_Click
总共 34 秒中的 27 秒的原因。我们还知道:
RouteCalculator.CalculateRouteMethod
主要负责高 CPU 使用率RouteViewer.DisplayDirection
负责高内存消耗
这意味着,在不离开 Visual Studio 的舒适区的情况下,开发人员不仅可以知道每一行代码的执行时间,还可以确切地了解其内存/CPU 成本。
等等,分析并没有就此结束。Diagnostic Tools 还提供了机制来进一步调查这些资源消耗背后的原因。内存使用相关的 Take Snapshot 按钮和 CPU 使用率相关的 Record CPU Profile 按钮可以帮助理解特定代码行的行为。
揭示 CPU 使用率
让我们首先尝试理解为什么应用程序在执行 RouteCalculator.CalculateRoute
方法期间会消耗 CPU 周期。
为此,我们需要点击 Record CPU Profile 按钮并运行 RouteCalculator.CalculateRoute
代码行。分析结果如下所示:
在右侧,这些结果以表格格式显示,列包含函数名称和总 CPU 百分比。此表中的数据按总 CPU 消耗降序排序。结果表明,RouteCalculator.XmlDataProcessor
方法消耗了 96% 的 CPU 周期,而 XmlDocument.LoadXml
方法消耗了其中 72% 的周期。
双击该表中的函数名将打开 Call Tree 视图,显示方法调用的链条。这表明在 RouteCalculator.XmlDataProcessor
方法消耗的 96% 的时间中,有 72% 是由 RouteCalculator.XmlDataProcessor
方法使用的,这是查找问题根本原因的明确指示。
双击 Call Tree 视图中的函数会打开相关的源代码。从代码中可以看出,RouteCalculator.XmlDataProcessor
方法在一个紧密的循环中调用 XmlDocument.LoadXml
方法,导致 CPU 占用率高。
我们还看到 XmlDataProcessor
方法是使用 任务并行库 (TPL) 数据并行 Parallel.For 调用的。这解释了为什么 PerfView 跟踪显示多个线程调用此方法。当然,Visual Studio 可以使用 Parallel Stack 显示相同的信息。
理解内存消耗
既然我们已经知道了 CPU 使用率的原因,现在让我们转向导致内存消耗过高的原因。
根据我们之前的分析,我们知道 RouteViewer.DisplayDirection
方法是导致内存消耗过高的原因。分析内存相关问题的典型方法是获取两个内存快照:一个在内存消耗高之前,一个在之后。可以比较这两个快照并分析有问题的对象。
我们应该使用 Take Snapshot 按钮在执行 RouteViewer.DisplayDirection
方法之前获取第一个内存快照。内存快照的结果以表格形式显示。此表包含 GC 堆中的对象数量以及堆的大小。
单击对象数量或堆大小的值将打开一个显示堆中所有对象的表格。
现在执行 RouteViewer.DisplayDirection
方法并获取另一个快照,因为内存使用量已悄然增加。此时,Memory Usage 选项卡将显示第二个快照的结果。这些结果清楚地表明,对象计数和堆大小都已增加。
单击第二个快照中的堆大小更改链接将显示堆中对象的列表,按两个快照之间的 Size Diff 排序。这表明 XmlNode
对象数组位于顶部。
单击表格中的对象本身将在底部选项卡中填充引用图,该图可以显示该对象是如何被引用的。
还可以查看 XmlNode[]
的所有引用类型。在这里,您可以看到许多 XmlElement
对象被 XmlNode[]
对象引用。
此分析提供了一个关于哪些对象导致内存消耗的合理想法。我们可以检查 RouterViewer.directionPoints
的代码。这是一个静态字段,它也在一个紧密的循环中填充,导致内存消耗过高。
底线是,通过 Visual Studio,开发人员可以在日常工作流程中关注应用程序的性能,而无需依赖任何其他工具。
结论
这一系列两篇文章展示了 Visual Studio 功能如何在开发人员的日常开发工作流程中帮助他们排查复杂的性能问题。
历史
- 2019 年 6 月 11 日:初始版本