65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIconstarIcon

5.00/5 (2投票s)

2019 年 6 月 11 日

CPOL

8分钟阅读

viewsIcon

2747

在本文中,我将承接之前阐述的概念,并演示如何有效地使用 Visual Studio 来排查与性能相关的问题。

引言

本文的第一部分,我们探讨了使用 PerfView 和 DebugDiag 等工具排查与性能相关问题的传统方法来构建高性能应用程序。然而,开发人员通常不会在日常开发任务中使用这些工具。在本文中,我将承接之前阐述的概念,并演示如何有效地使用 Visual Studio 来排查与性能相关的问题。

这是微软多年来大力发展其工具和技术的领域。Visual Studio 不仅能够让开发人员测量代码的性能,还提供了深入了解此类性能问题根本原因的能力。PerfTips、IntelliTrace、Performance & Diagnostic Hub 就是此类工具的示例。

审查应用程序

本文的第一部分建立了应用程序的通用行为。下图展示了 获取方向 按钮的点击处理程序代码。

代码并不复杂。以下是对该函数内部发生情况的概要总结。

  1. 验证起点和终点的输入。如果验证失败,则调用 RouterView 类中的 DisplayAddressValidationError 方法。
  2. 调用 SatelliteManager 类中的 Connect 方法。如果此方法返回失败,则调用 RouteViewer.DisplaySatelliteConnectionError
  3. 如果 SatelliteManager.Connect 成功,则在起点和终点地址上调用 GetSatelliteLocationFromAddress 方法。
  4. 调用 RouteCalculator.CalculateRoute 并返回一个 RouteDirection 对象。
  5. 最后,调用 RouteViewer 类中的 DisplayDirection 方法。
  6. 如果 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 日:初始版本
© . All rights reserved.