PerfView 用户指南


PerfView 是一种用于快速轻松地收集和查看时间和内存性能数据的工具。PerfView 使用操作系统的 Windows 事件跟踪 (ETW) 功能,该功能可以收集各种有用事件的信息,如高级收集部分所述。ETW 是 Windows 性能组几乎专门用于跟踪和了解 Windows 性能的强大技术,也是其 Xperf 工具的基础。PerfView 可以被认为是该工具的简化和用户友好版本。此外,PerfView 还能够收集 .NET GC 堆信息以执行内存调查(即使对于非常大的 GC 堆)。PerfView 解码 .NET 符号信息以及 GC 堆的能力使 PerfView 成为托管代码调查的理想选择。


部署和使用 PerfView


PerfView 旨在易于部署和使用。若要部署 PerfView,只需将 PerfView.exe 复制到要使用它的计算机即可。不需要其他文件或安装步骤。PerfView 功能是“可自行发现的”。初始显示是一个“快速入门”指南,引导您收集和查看您的第一组配置文件数据。还有一个内置教程。将鼠标悬停在大多数 GUI 控件上将为您提供简短的说明,超链接会将您带到本用户指南中最合适的部分。最后,PerfView 是“启用右键单击”的,这意味着你想以某种方式操作数据,右键单击可以让你发现 PerfView 可以为你做什么。


PerfView 是 V4.6.2 .NET 应用程序。  因此,您需要在实际运行 PerfView 的计算机上安装 V4.6.2 .NET 运行时。  在 Windows 10 和 Windows Server 2016 上,具有 .NET V4.6.2。在其他受支持的操作系统上,可以从独立安装程序安装 .NET 4.6.2。Win2K3 或 WinXP 不支持 PerfView。   虽然 PerfView 本身需要 V4.6.2 运行时,但它可以收集有关使用 V2.0 和 v4.0 运行时的进程的数据。在未安装 V4.6.2 或更高版本的 .NET 运行时的计算机上,还可以使用其他工具(例如 XPERF 或 PerfMonitor)收集 ETL 数据,然后将数据文件复制到具有 V4.6.2 的计算机上,并使用 PerfView 查看它。


PerfView 能为你做什么?


PerfView 旨在收集和分析时间和内存方案。


  1. CPU 调查:一个更有用的事件(默认情况下打开)是“配置文件”采样事件。此事件每毫秒对计算机的每个 CPU 的指令指针进行采样。每个示例捕获当前执行的线程的完整调用堆栈;提供有关该线程在高抽象和低抽象级别上所做的事情的非常详细和有用的信息。PerfView 聚合这些堆栈跟踪,并在堆栈查看器中显示它们,该查看器具有强大的分组操作,使理解此数据比大多数探查器简单得多。如果应用程序的性能问题与 CPU 使用率过高有关,则 PerfView 将告诉你这一点,并为你提供准确了解应用程序的哪些部分行为不当所需的工具。有关详细信息,请参阅启动 CPU 分析

  2. 托管内存调查:PerfView 还能够拍摄 .NET GC 堆的快照。由于这些堆可能非常大,因此 PerfView 允许控制采集样本的大小,如果堆太大而无法完整捕获,则获取代表性样本会遇到一些麻烦。然后,它将堆中的对象图转换为树形图,并将其显示在用于 CPU 调查的同一堆栈查看器中。有关详细信息,请参阅调查内存和启动 GC 堆分析

  3. 响应时间调查:通过使用“ThreadTime”选项收集足够的信息,以便 PerfView 能够测量每个线程(阻止或未阻止)的内容,收集与每个请求关联的所有线程时间,并将其显示为树。这就是“线程时间(包含启动-停止活动)”视图。有关详细信息,请参阅简化服务器调查。

  4. 挂钟/阻塞时间调查:如果您的程序太慢,但它没有消耗过多的 CPU,那么它必须被阻塞等待其他东西(磁盘网络等)。PerfView 可以指示操作系统在线程休眠或唤醒时记录事件,并具有用于可视化程序等待位置的显示。有关详细信息,请参阅阻塞/挂钟时间调查。

  5. 内存调查:还可以在每次操作系统堆内存分配器分配或释放对象时打开事件。使用这些事件,您可以查看哪些调用堆栈负责最多的净非托管内存分配。有关详细信息,请参阅调查内存和非托管堆分析。

  6. CPU 调查:PerfView 能够读取 Linux 内核中内置的 Linux“Perf Events”收集器的输出。有关详细信息,请参阅查看 Linux 数据。

  7. 在 PerfView 的堆栈查看器中查看自己的分层数据:PerfView 的堆栈查看器功能强大,但也非常灵活。PerfView 定义了一种非常简单的 XML 或 JSON 格式,它可以读入此查看器。这使你能够轻松生成数据,然后可以在 PerfView 强大的堆栈查看器中查看这些数据。有关详细信息,请参阅查看外部数据。


另请参阅 PerfView 参考指南。



发送反馈/询问有关 PerfView 的问题


希望该文档在回答有关 PerfView 和一般性能调查的最常见问题方面做得相当好。如果您有任何疑问,您当然应该首先搜索用户指南以获取信息


然而,不可避免地,会有一些文档没有回答的问题,或者你想要拥有但尚不存在的功能,或者你想报告的错误。PerfView 是一个 GitHub 开源项目,你应该在

 PerfView 问题


如果您只是问一个问题,您可以使用一个名为“问题”的标签来表示这一点。如果这是一个错误,如果您提供足够的信息来重现该错误,它确实会有所帮助。通常,这包括您正在操作的数据文件。您可以将小文件拖到问题本身中,但更有可能的是,您需要将数据文件放在云中的某个位置并在问题中引用它。最后,如果你提出建议,你越具体越好。除非您自己帮助实现,否则大型功能实现的可能性要小得多。请记住这一点。



获取最新版本的 PerfView


可以通过转到 PerfView GitHub 下载页来获取最新版本的 PerfView




基于时间的调查教程


另请参阅 GC 堆内存调查教程


也许最好的入门方法是简单地尝试教程示例。在 Windows 7 上,建议您按照帮助提示中的说明编写帮助文档。PerfView 附带了两个“内置”教程示例。此外,我们强烈建议您编写的任何应用程序都具有性能计划,如 尽早和经常测量性能的第 1 部分和第 2 部分所述。


  1. Tutorial.exe - 一个简单的程序,重复调用“DateTime.Now”,直到它检测到 5 秒过去了。为了使此示例更有趣,它使用两种相互递归的方法(RecSpin 和 RecSpinHelper)执行此操作。这些帮助程序中的每一个都旋转一秒钟,然后调用另一个帮助程序在剩余时间内旋转。有关完整源代码,请参阅 Tutorial.cs。


要运行“教程”示例,请执行以下操作:


  1. 单击主页上的“运行命令”超链接。 这将打开对话框,指示要运行的命令和要创建的数据文件的名称。

  2. 在“命令”文本对话框中输入“教程.exe”,然后点击。

  3. 除非从提升的环境启动 PerfView,否则操作系统将启动用户访问控制,以管理员身份运行(收集配置文件数据是一项特权活动)。 单击“确定”接受。

  4. 此时,它将开始运行该命令。 状态栏将闪烁,表示它正在处理您的命令。  您可以通过点击右下角的“日志”按钮来监控其进度。 完成后,它会弹出一个进程选择对话框。PerfView 询问你关注的是哪个进程。在这种情况下,我们对“教程”过程感兴趣,因此我们应该选择它。如果您对所有过程感兴趣,也有一个按钮。


还可以通过在命令行中键入“PerfView run tutorial”来运行教程示例。有关详细信息,请参阅从命令行收集数据。


选择“Tutorial.exe”作为感兴趣的进程后,PerfView 会显示堆栈查看器,如下所示:

StackView


此视图显示 CPU 时间花费的位置。PerfView 对每个处理器的位置(包括整个堆栈)和每毫秒(请参阅了解性能数据)进行了采样,堆栈查看器显示这些示例。因为我们告诉 PerfView,我们只对 Tutorial.exe 过程感兴趣,所以此视图已被“IncPats”限制为仅显示该过程中使用的示例。


最好通过查看视图顶部的摘要信息来开始调查。这样一来,您就可以确认大部分性能问题确实与 CPU 使用率有关,然后再准确查找 CPU 的使用情况。这就是汇总统计数据的用途。我们看到,该过程花费了 84% 的挂钟时间消耗 CPU,这值得进一步研究。接下来,我们只需查看程序中“Main”方法的“When”列。此列显示 CPU 在收集时间间隔内如何用于该方法(或其调用的任何方法)。时间被分解为 32 个“TimeBucket”(在本例中,我们从汇总统计信息中看到,每个存储桶的长度为 197 毫秒),数字或字母表示 1 个 CPU 的使用百分比。9s 和 As 意味着您接近 100%,我们可以看到,在 main 方法的生命周期中,我们大部分时间都接近 100% 利用 1 个 CPU。主程序之外的区域可能没有兴趣(它们处理运行时启动和进程启动前后的时间),因此我们可能希望“放大”到该区域。


放大到感兴趣的时间范围


您只对部分跟踪感兴趣是很常见的。例如,您可能只关心启动时间,或者从单击鼠标到显示菜单的时间。因此,放大通常是您首先要执行的操作之一。 放大实际上只是选择一个时间区域进行调查。时间区域显示在“开始”和“结束”文本框中。这些可以通过三种方式进行设置


  1. 在文本框中手动输入值。

  2. 选择两个单元格(通常是“第一个”和“最后一个”)特定方法的单元格,右键单击并选择“SetTimeRange”

  3. 选择“时间”单元格。如果再次单击该单元格,该单元格将变为可编辑的,此时您可以右键单击文本选择一个区域,然后选择“SetTimeRange”(或按 Alt-R)以选择与所选字符关联的时间范围。


尝试这些技术中的每一种。   例如,要“放大”主要方法,只需将鼠标拖动到“第一次”和“最后一次”时间上即可同时选择两者,右键单击并选择时间范围。  您可以点击“返回”按钮撤消您所做的任何更改,以便重新选择。  另请注意,每个文本框都会记住该框的最后几个值,因此您还可以通过选择下拉列表(框右侧的小向下数组)并选择所需的值来“返回”特定的过去值。


对于 GUI 应用程序,跟踪整个运行过程,然后“放大”到用户触发活动的点的情况并不少见。您可以通过切换到“CallTree”选项卡来执行此操作。这将向您显示从进程本身开始的 CPU。视图的第一行是“Process32 tutorial.exe”,是整个进程的 CPU 时间摘要。“when”列显示进程随时间推移的 CPU(32 个时间段)。在 GUI 应用程序中,会出现未使用 CPU 的停顿,然后是与用户操作相对应的 CPU 使用率激增。这些显示在“何时”列中的数字中。通过单击“何时”列中的单元格,选择一个范围,右键单击并选择 SetTimeRange(或 Alt-R),您可以放大这些“热点”之一(您可能需要多次放大)。现在,您已经专注于您感兴趣的内容(您可以通过查看在此期间调用的方法进行确认)。这是一种非常有用的技术。


对于托管应用程序,在开始调查之前,您始终希望放大 main 方法。 原因是,在收集配置文件数据时,在 Main 退出后,运行时会花费一些时间将符号信息转储到 ETW 日志。  这几乎从来都不有趣,你想在调查中忽略它。 放大 Main 方法将执行此操作。


解析非托管符号


放大感兴趣区域后,如果要进行非托管调查,则可能需要解析符号。与托管代码不同,非托管代码将其符号信息存储在需要下载和匹配的外部 PDB 文件中。因为这可能需要一段时间,所以默认情况下不会这样做。相反,你会在跟踪中看到问号(如 ntdll!?),指示 PerfView 知道示例来自 ntdll,但它无法进一步解析名称。对于许多 DLL,你永远不需要解析这些符号,因为你根本不关心(你不拥有或调用该代码)。但是,如果您确实在乎,您可以快速获得符号。只需选择一个带有 DLL!?在其中,右键单击,然后选择“查找符号”。然后,PerfView 将查找该 DLL 的符号并重新绘制屏幕。尝试通过选择单元格来查找 ntdll 的符号


右键单击,然后选择“查找符号”。查找符号后,它将变成


如果要执行非托管调查,则可能需要少量 DLL 的符号。常见的工作流是查看 byname 视图,在按住 Ctrl 键的同时,选择包含具有大量 CPU 时间但未解析符号的 dll 的所有单元格。然后右键单击 -> Lookup Symbols,PerfView 将批量查找它们。有关更多详细信息或查找符号失败,请参阅符号解析。


自下而上的调查


PerfView 从“ByName 视图”开始,用于执行自下而上的分析(另请参阅启动分析)。在此视图中,您可以看到示例中涉及的每个方法(方法中出现一个示例,或者调用具有示例的例程的方法)。示例可以是非独占的(发生在该方法中),也可以是非独占的(发生在该方法或该方法调用的任何方法中)。默认情况下,按名称视图根据方法的独占时间对方法进行排序(另请参阅列排序)。这显示了程序中“最热门”的方法。


通常,“自下而上”方法的问题在于程序中的“热门”方法是


  1. 不是很热(使用< 5% 的 CPU)

  2. 往往是“帮助程序”例程(在程序中、库或运行时中),它们在“任何地方”使用并且已经进行了很好的调整。


在这两种情况下,您都不希望看到这些帮助程序例程,而是最低的“语义有趣”例程。   这就是 PerfView 强大的分组功能发挥作用的地方。  默认情况下,PerfView 按以下方式对示例进行分组


  1. 使用 GroupPats 的“Just my code”模式组成两个组。第一组是任何模块中与“exe”本身位于同一目录(递归)中的任何方法。这是“我的代码”组,这些示例不作保留。任何不在第一组中的样本都属于“其他”组。这些样本根据为进入组而调用的方法进行分组。

  2. 使用折叠百分比功能。此值设置为 1,这意味着在“byname”视图中(在视图顶部的摘要中指示的所有样本)少于 1% (包括 1%)的任何方法都不是“有趣的”,不应显示。相反,它的样本被折叠(内联)到其调用方中。


例如,ByName 视图中的第一行是


这是“条目组”的一个示例。“OTHER”是该组的名称和 mscorlib!System.DateTime.get_Now() 是进入组的调用方法。从那时起,该组内 get_Now() 调用的任何方法都不会显示,而是简单地将它们的时间累积到此节点中。实际上,这种分组表示“我不想看到不是我的代码的函数的内部工作,但我确实希望看到我用来调用该代码的公共方法。要让您了解此功能的有用性,只需将其关闭(通过清除“GroupPats”框中的值),然后查看数据即可。您将看到更多带有“get_Now”使用的内部函数名称的方法,这只会使您的分析更加困难。(您可以使用“后退”按钮快速恢复之前的组模式)。


另一个有助于“清理”自下而上视图的功能是折叠百分比功能。此功能将导致所有“小”调用树节点(小于给定的 %)自动折叠到其父节点中。同样,您可以通过清除文本框(这意味着没有折叠)来查看此功能的帮助程度。关闭该功能后,您将看到更多时间“少量”的条目。这些小条目往往只会增加“混乱”,使调查更加困难。

 更多折叠


由于 PerfView 为你执行了分组和折叠操作,因此可以快速看到“DateTime.get_Now()”是“热”方法(占所有样本的 74.6%)。但是,还要注意 PerfView 并没有做“完美”的工作。我们注意到视图具有组 和 这两个重要的操作系统 DLL 分别占用了 CPU 的 9.5% 和 2%,并且只知道 DLL 中的某些函数被调用并不是非常有用。我们有两个选择

  1. 解析这些 DLL 的符号,以便我们具有有意义的名称。有关详细信息,请参阅符号分辨率。

  2. 将这些条目折叠起来。


完成 (2) 的快速方法是添加模式 '!?' 。此模式表示折叠任何没有方法名称的节点。有关详细信息,请参阅 foldPats 文本框。这给我们留下了一个非常“干净”的函数视图,其中只有语义相关的节点。


回顾:所有这些时间选择、分组和折叠是干什么用的?


性能调查的第一阶段是形成一个“性能模型”,目标是将时间分配给语义相关的节点(程序员理解并可以做某事的事情)。  我们通过形成一个语义上有趣的组并为其分配节点来做到这一点,或者通过将节点折叠到现有的语义相关组中,或者(最常见的)利用大型组(模块和类)的入口点,作为方便的“预制”语义相关节点。 目标是将成本分组到相对较少(< 10)的语义相关条目中。这使您可以推断该成本是否合适(这是调查的第二阶段)。

 破碎的堆栈


剩下的一个节点是一个名为“BROKEN”的节点。这是一个特殊节点,表示其堆栈跟踪被确定为不完整,因此无法正确归因的样本。只要这个数字很小(<几个百分点),那么它就可以被简单地忽略。有关详细信息,请参阅损坏的堆栈。

 时间和百分比。


PerfView 将非独占时间和独占时间显示为指标 (毫秒) 和百分比,因为两者都很有用。  百分比可以让您很好地了解节点的相对成本,但是绝对值很有用,因为它非常清楚地表示“时钟时间”(例如,300 个样本表示 300 毫秒的 CPU 时间)。   绝对值也很有用,因为当该值明显小于 10 时,它变得不可靠(当您只有少数样本时,它们可能是“纯偶然”发生的,因此不应依赖。


CallTree 视图(自上而下的调查))


自下而上的视图在确定 get_Now() 方法和“SpinForASecond”消耗的时间最多方面做得非常出色,因此值得仔细研究。这符合我们对 Tutorial.cs 中源代码的期望。但是,了解自上而下消耗 CPU 时间的位置也很有用。这就是 CallTree 视图的用途。只需单击堆栈查看器的“CallTree”选项卡,即可进入该视图。最初,显示屏仅显示根节点,但您可以通过单击复选框(或按空格键)打开节点。这将扩展节点。只要一个节点只有一个子节点,子节点也会自动展开,以节省一些点击时间。您也可以右键单击并选择“展开全部”以展开所选节点下的所有节点。在根节点上执行此操作将产生以下显示

CallTreeView


请注意,调用树视图是多么干净,没有很多“噪音”条目。 事实上,这种观点在描述正在发生的事情方面做得非常好。  请注意,它清楚地显示了 Main 调用 'RecSpin' 的事实,它运行 5 秒(从 894 毫秒到 5899 毫秒),同时消耗 4698 毫秒的 CPU(CPU 不是 5000 毫秒,因为实际收集配置文件的开销(以及其他不归因于此过程的操作系统开销以及损坏的堆栈), 通常在 5-10% 的范围内运行。  在这种情况下,它似乎约为 6%)。  “When”列还清楚地显示了 RecSpin 的一个实例如何运行 SpinForASecond(正好一秒钟),然后调用 RecSpinHelper,该实例在其余时间消耗接近 100% 的 CPU。.  呼叫树是一个精彩的自上而下的概要。


获得“更粗糙”的视图


视图顶部的所有筛选和分组参数都会对任何视图(byname、caller-callee 或 CallTree)产生同等影响。   我们可以利用这一事实和“折叠%”功能来获得调用树“顶部”的更粗略视图。  展开所有节点后,只需右键单击窗口并选择“增加折叠百分比”(或更轻松地按 F7 键)。 这会将“折叠百分比”文本框的数量增加 1.6 倍。  通过反复按 F7 键,您可以不断修剪堆栈的“底部”,直到您只看到使用大量 CPU 时间的方法。   下图显示了按 F7 七次后的 CallTreeView。

CallTreeView


您可以使用“返回”按钮、Shift-F7 键(可降低折叠%)或只需在折叠百分比框中选择 1(例如从下拉菜单中)恢复上一个视图。

 Caller-Callee 视图


获取树的课程视图很有用,但有时您只想将注意力限制在单个节点上发生的情况上。  例如,如果 BROKEN 堆栈的包含时间很长,您可能希望查看“BROKEN”堆栈下的节点,以了解哪些样本在调用树中的正确位置“丢失”。  您可以通过在 Caller-callee 视图中查看 BROKEN 节点来轻松执行此操作。  为此,右键单击 BROKEN 节点,然后选择 Goto -> Caller-callee(或键入 Alt-C)。因为我们的跟踪中很少有样本是 BROKEN 的,所以这个节点不是很有趣。通过将 Fold % 设置为 0(空白),您可以获得以下视图

CallerCalleeView


视图分为三个网格。中间部分显示“当前节点”,在本例中为“BROKEN”。顶部网格显示调用此焦点节点的所有节点。在 BROKEN 的情况下,节点仅在一个线程上。下图显示了按包含时间排序的“BROKEN”调用的所有节点。我们可以看到,大多数损坏的节点来自源自“ntoskrnl”dll(这是Windows操作系统内核)的堆栈,要深入研究,我们首先需要解析此DLL的符号。有关详细信息,请参阅符号分辨率。


钻取到组(取消分组)


虽然组是一个非常强大的功能,可以在“粗略”级别上理解程序的性能,但不可避免地,您希望“钻取”这些组并详细了解特定节点的细节。  例如,如果我们是负责 DateTime.get_Now() 的开发人员,我们就不会对它是从“SpinForASecond”例程调用的事实感兴趣,而是对里面发生的事情感兴趣。  此外,我们不希望看到来自程序其他部分的样本“混乱”了 get_Now() 的分析。   这就是“钻取”命令的用途。   如果我们返回“ByName”视图并右键单击“get_Now”,然后选择“Drill Into”,则会出现一个新窗口,其中仅提取了这些 3792 个样本。


最初,钻取不会更改任何筛选器/分组参数。  但是,现在我们已经分离了感兴趣的样本,我们可以自由地更改分组和折叠,以在新的抽象级别上理解数据。通常,这意味着取消对某些内容的分组。在本例中,我们想看看 mscorlib!get_Now() 是如何工作的,所以我们想看看 mscorlib 内部的细节。为此,我们选择“mscorlib!DateTime.get_Now() 节点,单击鼠标右键,然后选择“取消模块分组”。  这表明我们希望取消对“mscorlib”模块中的任何方法的分组。  这使您可以查看该例程的“内部结构”(无需完全取消分组) 结果是以下显示

Ungrouped


在这一点上,我们可以看到大部分“get_Now”时间都花在名为“GetUtcOffsetFromUniversalTime”和“GetDatePart”的函数中 我们可以使用堆栈查看器的全部功能,折叠、分组、使用 CallTree 或调用方-被调用方视图来进一步优化我们的分析。  由于“钻取”窗口与其父窗口是分开的,因此您可以将 is 视为“一次性”,并在查看完程序性能的这一方面后将其丢弃。


在上面的示例中,我们深入研究了方法的包容性样本。 但是,您也可以执行相同的操作来钻取独家示例。   当涉及用户回调或虚拟函数时,这很有用。  以具有内部帮助程序功能的“排序”例程为例。 在这种情况下,将那些属于节点“内部帮助程序”的样本(将折叠为“排序”的独占样本)与由用户“比较”函数引起的样本(通常不会分组为独占样本,因为它跨越了模块边界)会很有用。   通过钻取到“sort”的独占样本,然后取消分组,你只能看到“sort”中不属于用户回调的那些样本。  通常,这正是负责“排序”例程的程序员希望看到的。


查看源(线级分析)


一旦分析确定方法可能效率低下,下一步就是充分理解代码以进行改进。PerfView 通过实现“转到源”功能来帮助实现此目的。只需选择一个包含方法名称的单元格,右键单击并选择“转到源”(或使用 Alt-D(D 表示定义))。然后,PerfView 尝试查找源代码,如果成功,将启动文本编辑器窗口。例如,如果在“ByName”视图中选择“SpinForASecond”单元格,然后选择“转到源”,则会显示以下窗口。

Ungrouped


如您所见,将显示特定方法,并且每行都以该行所花费的成本(在本例中为 CPU MSec)为前缀。在此视图中,它显示 4.9 秒的 CPU 时间花在方法的第一行上。


源代码注意事项


遗憾的是,在 .NET 运行时 V4.5 之前,运行时没有向 ETL 文件发出足够的信息,无法将示例解析为行号(仅解析为方法)。因此,虽然 PerfView 可以调出源代码,但它无法准确地将示例放在特定行上,除非代码在 V4.5 或更高版本上运行。当 PerfView 没有所需的信息时,它只是将所有成本归因于方法的第一行。事实上,这就是您在上面的示例中看到的。如果在 V4.5 运行时上运行示例,则会得到更有趣的成本分布。本机代码不存在此问题(您将获得行级分辨率)。但是,即使在旧的运行时版本上,您至少也有一种简单的方法可以导航到相关源代码。


PerfView 通过查找与代码关联的 PDB 文件中的信息来查找源代码。因此,第一步是 PerfView 必须能够找到 PDB 文件。默认情况下,大多数工具会将 PDB 文件的完整路径放在它生成的 EXE 或 DLL 中,这意味着,如果尚未移动 PDB 文件(并且位于生成的计算机上),则 PerfView 将找到 PDB。然后,它会查找包含每个源文件的完整路径名的 PDB 文件,如果你在生成二进制文件的计算机上,则 PerfView 将查找源文件。因此,如果你在你构建的同一台机器上运行,它就会“正常工作”。


但是,通常不会在生成的计算机上运行,在这种情况下,PerfView 需要帮助。PerfView 遵循用于查找源代码的其他工具的标准约定。具体而言,如果 _NT_SYMBOL_PATH 变量设置为以分号分隔的路径列表,它将在这些位置查找 PDB 文件。此外,如果_NT_SOURCE_PATH设置为以分号分隔的路径列表,它将在每个路径的子目录中搜索源文件。因此,设置这些环境变量将允许 PerfView 的源代码功能在“外部”计算机上工作。您还可以使用堆栈查看器菜单栏上“文件”菜单上的菜单项在 GUI 内设置_NT_SYMBOL_PATH和_NT_SOURCE_PATH。



GC 堆内存分析教程


另请参阅基于时间的调查教程。虽然目前没有关于进行 GC 堆分析的教程,但如果您还没有学习过基于时间的调查教程,您应该这样做。在内存调查中使用了许多相同的概念。您还应该看看


教程未完成




性能调查最佳实践

 调查时间


收集事件(基于时间)配置文件数据


如简介中所述,ETW 是内置于 Windows 操作系统中的轻量级日志记录机制,可以收集有关计算机中发生的情况的各种信息。PerfView 支持通过两种方式收集 ETW 配置文件数据。


  1. Collect->Run (Alt-R) 菜单项,提示输入要创建的数据文件名和要运行的命令。该命令将打开性能分析,运行该命令,然后关闭性能分析。然后,生成的文件将显示在堆栈查看器中。当很容易启动感兴趣的应用程序时,这是首选机制。如果命令产生输出,它将被捕获到日志中(单击主视图右下角的“日志”按钮)。

  2. Collect->Collect (Alt-C) 菜单项,仅提示输入要创建的数据文件名。单击“开始收集”按钮后,您可以自由地以任何必要的方式与机器进行交互,以捕获感兴趣的活动。由于分析是机器范围的,因此可以保证捕获它。重现问题后,可以关闭该对话框以停止分析并继续分析数据。


您还可以使用命令行选项自动收集配置文件数据。有关详细信息,请参阅从命令行收集数据。


如果您打算进行挂钟时间调查


默认情况下,PerfView 选择一组事件,这些事件不会生成太多数据,但对各种调查很有用。但是,挂钟调查需要的事件量太大,默认情况下无法收集。因此,如果您想进行挂钟调查,则需要在收集对话框中设置“线程时间”复选框。


如果要将 ETL 文件复制到另一台计算机进行分析


默认情况下,为了节省时间,PerfView 不会准备 ETL 文件,以便可以在其他计算机上对其进行分析(请参阅合并)。此外,还有符号信息(NGEN图像的PDBS),如果数据要在任何机器上正常工作,也需要包括这些信息)。如果您打算这样做,则需要使用“ZIP”命令合并并包含 NGEN pdbs。您可以这样做


压缩数据后,文件不仅包含解析符号信息所需的所有信息,而且还经过压缩以加快文件复制速度。如果您打算在另一台机器上使用数据,请指定ZIP选项。



查看堆栈数据


选择感兴趣的进程


收集数据的结果是一个 ETL 文件(可能还有一个 .kernel。ETL 文件,如合并中所述)。当您在主查看器中双击该文件时,它会打开所收集数据的“子视图”。其中一项将是“CPU 堆栈”视图。双击该按钮将打开一个堆栈查看器来查看收集的样本。ETL 文件中的数据包含系统中所有进程的 CPU 信息,但大多数分析都集中在单个进程上。因此,在显示堆栈查看器之前,首先会显示一个对话框,用于选择感兴趣的进程。


默认情况下,此对话框包含收集跟踪时处于活动状态的所有进程的列表,并按每个进程消耗的 CPU 时间量排序。    如果您正在进行 CPU 调查,则感兴趣的进程很有可能位于此列表的顶部附近。 只需双击所需的进程,就会调出过滤到您选择的进程的堆栈查看器。


通过单击列标题,可以按任何列对流程视图进行排序。 因此,如果您希望查找最近启动的进程,可以按开始时间排序以快速找到它。  如果视图按名称排序,则如果键入进程名称的第一个字符,它将导航到具有该名称的第一个进程。


进程筛选器文本框 进程列表正上方的框。如果在此框中键入文本,则仅显示与此字符串匹配的进程(PID、进程名称或命令行,不区分大小写)。* 字符是通配符。这是查找特定进程的快速方法。


如果您希望查看多个过程的样品进行分析,请单击“所有过程”按钮。


请注意,进程选择对话框的唯一效果是添加与您选择的进程匹配的“Inc Pats”过滤器。因此,该对话框实际上只是一个“友好界面”,用于堆栈查看器更强大的过滤选项。特别是,堆栈查看器仍然可以访问所有示例(甚至是所选进程之外的示例),只是由于对话框设置的包含模式,它将其过滤掉。这意味着您可以在分析的稍后时间点删除或修改此过滤器。



了解性能数据


默认情况下,PerfView 堆栈查看器中显示的数据是在系统上的每个处理器上每毫秒执行一次的堆栈跟踪。每毫秒,任何正在运行的进程都会停止,操作系统会“遍历”与正在运行的代码关联的堆栈。进行堆栈跟踪时保留的是堆栈上每个方法的返回地址。堆叠行走可能并不完美。操作系统可能无法找到下一帧(导致堆栈损坏),或者优化编译器删除了方法调用(请参阅缺失的帧),这可能会使分析更加困难。然而,在大多数情况下,该方案运行良好,并且开销低(通常为 10% 的减速),因此可以在“生产”系统上进行监控。


在负载较轻的系统上,许多 CPU 通常处于“空闲”进程中,当操作系统没有其他操作可做时,该进程会运行该进程。   PerfView 会丢弃这些示例,因为它们几乎从不有趣。   然而,所有其他样品都会被保留,无论它们来自什么过程。   大多数分析都集中在单个进程上,并进一步筛选在感兴趣的进程中未发生的所有样本,但 PerfView 还允许你将所有进程中的样本视为一棵大树。  这在端到端涉及多个进程的情况下非常有用,或者当您需要多次运行应用程序以收集足够的样本时。


您需要多少样品?


由于每个处理器每毫秒采集一次样本,因此每个样本表示 1 毫秒的 CPU 时间。  然而,取样的确切位置实际上是“随机的”,因此将整毫秒“充电”到取样时碰巧运行的例程中确实是“不公平的”。  虽然这是真的,但随着采集的样本越来越多,这种“不公平性”会随着样本数量的平方根而减少,这也是事实。  如果一种方法只有 1 或 2 个样本,那么它可能只是随机发生在该特定方法中,但具有 10 个样本的方法可能真正使用了 7 到 13 个样本(30% 误差)。 具有 100 个样本的例程可能在 90 和 110 之间(误差为 10%)。   对于“典型”分析,这意味着您至少需要 1000 个样本,最好是 5000 个样本(10K 后回报递减)。  通过收集几千个样本,您可以确保即使是中等程度的“温”方法也至少有 10 个样本,而“热”方法至少有 100 个样本,从而使误差保持在可接受的范围内。  由于 PerfView 不允许更改采样频率,这意味着需要运行方案至少几秒钟(对于 CPU 密集型任务),以及 10-20 秒(对于较少 CPU 密集型活动)。


如果要测量的程序不能轻易更改为在所需的时间内循环,则可以创建一个批处理文件,重复启动程序并使用它来收集数据。 在这种情况下,您需要查看所有进程的 CPU 示例,然后使用 GroupPat 擦除进程 ID(例如进程 {%}=>$1),从而将所有同名进程组合在一起。


即使有 1000 个样本,仍然存在至少在 3% 范围内的“噪声”(sqrt(1000) ~= 30 = 3%)。  随着所研究的方法/组的样本较少,此误差会变大。  这使得使用基于样本的分析来比较两条迹线来追踪小的回归(比如 3%)成为问题。  噪声可能至少与您试图追踪的“信号”(diff)一样大。  增加样本数量会有所帮助,但是在比较两条迹线之间的微小差异时,应始终牢记采样误差。


独家和包容性指标


由于为每个样本收集堆栈跟踪,因此每个节点都具有一个独占指标(在该特定方法中收集的样本数)和一个非独占指标(在该方法或该方法调用的任何方法中收集的样本数)。通常,您对包含时间感兴趣,但重要的是要意识到折叠(参见 FoldPats 和 Fold %)和分组会人为地增加独占时间(它是该方法(组)中的时间以及折叠到该组中的任何内容)。当您希望查看折叠到节点中的内容的内部结构时,您可以钻取到组以打开一个视图,在该视图中可以撤消分组或折叠。



启动 CPU 分析


如果尚未执行此操作,请考虑逐步完成 尽早和经常测量性能 中的教程和最佳实践。


PerfView 中的默认堆栈查看器会分析进程的 CPU 使用率。  在开始对特定进程进行 CPU 分析时,应始终立即执行三件事。


  1. 确定您至少有 1000 个样本(最好超过 5000 个)。查看我需要多少样品才能获得更多样品。

  2. 确定进程在感兴趣的时间内实际上受 CPU 限制。

  3. 确保您拥有所需的符号信息。有关详细信息,请参阅符号分辨率。


如果上述任一条件失败,则分析的其余部分很可能不准确。  如果你没有足够的样本,你需要回去收集,以便你得到更多,修改程序以运行更长时间,或者多次运行程序以积累更多样本。   如果您的程序运行时间足够长(通常为 5-20 秒),并且您仍然没有至少 1000 个样本,则很可能是因为 CPU 不是瓶颈。 在启动方案中,CPU 不是问题,而是从磁盘获取数据所花费的时间是很常见的。 程序也可能正在等待网络 I/O(服务器响应)或来自本地系统上其他进程的响应。  在所有这些情况下,浪费的时间不受使用多少 CPU 时间的限制,因此 CPU 分析是不合适的。


您可以通过查看“最顶层”方法的“时间”列来快速确定您的进程是否受 CPU 限制。如果 When 列在活动期间有很多 9 或 As,则该进程很可能在这段时间内受 CPU 限制。这是您可以希望优化的时间,如果它不是应用程序总时间的很大一部分,那么优化它的整体效果将很小(参见阿姆达尔定律)。切换到 CallTree 视图并查看程序中一些最顶级方法的“When”列是确认应用程序实际上受 CPU 限制的好方法。


最后,你可能有足够的样本,但你缺乏符号信息来理解它们。这将以 ?在他们身上。默认情况下,.NET 代码应该“正常工作”。对于非托管代码,需要告诉 PerfView 你有兴趣获取哪些 DLL 的符号。有关详细信息,请参阅符号分辨率。您还应该快速检查您是否有很多损坏的堆栈,因为这也会干扰分析。


自上而下和自下而上的分析


一旦确定 CPU 对于优化实际上很重要,您就可以选择如何进行分析。 性能调查可以是“自上而下”的(从主程序开始,以及如何将花费的时间划分为它调用的方法),也可以是“自下而上”(从实际采样的“叶”方法开始,并查找使用大量时间的方法)。  这两种技术都很有用,但是“自下而上”通常是更好的开始方式,因为底部的方法往往更简单,因此更容易理解,并且对它们应该使用多少 CPU 有直觉。


第 1 阶段:选择如何对方法进行分组


PerfView 从“ByName”视图开始,该视图是自下而上分析的适当起点。在自下而上的分析中,将方法分组到语义相关的分组中尤为重要。默认情况下,PerfView 会选择一个好的集合起始组(称为“只是我的代码”)。在此分组中,任何模块中位于 EXE 所在目录以外的目录中的任何方法都被视为“其他”,并且使用条目组功能,通过用于调用此外部代码的方法对它们进行分组。有关“Just My Code”分组含义的更多信息,请参阅教程,以及有关分组的更多信息,请参阅GroupPats参考。


对于简单的应用程序,默认分组效果很好。GroupPats 框的下拉列表中还有其他预定义的分组,您可以根据需要自由创建或扩展这些分组。当您在“ByName”视图中看到的是语义相关的方法名称时,您就知道您有一组“良好”的分组(您识别这些名称,并且知道它们的语义用途是什么),它们的数量不多(少于 20 个左右,具有有趣的独占时间), 但足以将程序分解成“有趣”的部分,您可以依次关注(通过钻取)。


一种非常简单的方法是增加 折叠百分比 ,这会折叠掉小节点。有一个快捷键可以增加(F7 键)或减少(Shift F7)1.6 倍。因此,通过反复按 F7,您可以将小节点“聚集”成大节点,直到只有少数节点存活并显示出来。虽然这既快速又简单,但它并没有注意到生成的组在语义上的相关性。因此,它可能会以糟糕的方式对事物进行分组(折叠掉语义相关的小节点,并将它们分组到您不太想看到的“辅助例程”中)。尽管如此,它是如此快速和简单,至少尝试看看会发生什么总是值得的。此外,折叠掉真正小的节点几乎总是有价值的。即使一个节点在语义上是相关的,如果它使用了总 CPU 时间的 1%,<,你可能并不关心它。


通常,当您在 1-10% 范围内使用 Fold % (以去除最小的节点),然后有选择地折叠任何语义上不有趣的节点时,会出现最佳结果。  这可以很容易地完成,查看“ByName”视图,按住“Shift”键,然后选择图表上具有一些独占时间的每个节点(它们将朝向顶部),而您却无法识别。  完成扫描后,只需右键单击并选择“折叠项目”,这些节点将被折叠到从视图中消失的调用方中。  重复此操作,直到显示中没有使用语义上不相关的独占时间的节点。   你所剩下的就是你要找的。


第 2 阶段:钻取到组


在调查的第一阶段,你花时间组建语义相关的组,这样你就可以理解如何为数百种单独的方法所花费的时间分配“意义”的“大局”。   通常,下一阶段是“钻取”这些似乎花费了太多时间的组之一。 在这个阶段,你有选择地取消对语义组的分组,以了解下一个抽象“较低层次”上发生的事情。


您可以使用两个命令完成此操作


  1. 钻取 - 通过选择表示样本(以及包含或独占列)的单元格,右键单击并选择“钻取”,它将打开一个新的 StackViewer,该 StackViewer 已加载了 JUST THOSE SAMPLES。  这允许您更改该视图中的过滤和分组,而不会让运行其余部分的样本干扰分析。

  2. 取消分组 - 一旦你有一个可以更改分组/折叠的新窗口,你通常希望取消对所选节点之一的分组,以便你可以“看到内部”。 取消分组的方式取决于组的形成方式。 可能性包括

  3. 如果节点是入口点组(例如,OTHER<>),则可以指示只希望取消该入口点的分组。  这就是右键单击并选择“取消分组”的作用。  请注意,原始入口点调用的任何方法现在都将成为组的入口点,因此这只会取消分组到“一个级别”。

  4. 如果节点是入口点组(例如,OTHER<>),则可以指示希望取消该模块中的所有方法,选择节点并使用“取消分组模块”命令。  这往往在一次镜头中显示了该组的大部分有趣的内部结构。

  5. 如果节点是普通组(例如,模块 mscorlib),则可以指示只希望该组取消分组。 “取消分组”就是这样做的。

  6. 如果节点中有许多其他节点折叠到其中(因为 FoldPats 或 Fold %),那么简单地删除这些节点就会“爆炸”该组。  有一个右键单击快捷方式“清除所有折叠”可以做到这一点。


通常,如果“取消分组”或“取消分组模块”命令效果不佳,请使用“清除所有折叠”如果效果不佳,请清除“GroupPats”文本框,该文本框将显示最“未分组”的视图。  如果此视图过于复杂,则可以使用显式折叠(或创建临时组)来构建新的语义分组(就像在分析的第一阶段一样)。

 总结


总之,CPU 性能分析通常包括三个阶段


  1. 确认 CPU 确实是瓶颈,并且您有足够的样本来进行准确的分析。

  2. 使用分组和折叠,以便将方法聚类到语义相关的组中

  3. 通过有选择地取消分组来深入了解最感兴趣的组,以了解更精细的细节。

 调查内存


何时关注内存


很明显,优化时间的好处是:您的程序运行得更快,这意味着您的用户不会等待那么久。对于记忆来说,它并不那么清楚。如果您的程序使用的内存比它多 10%,谁在乎呢?有一篇有用的 MSDN 文章,名为 Memory Usage Auditing for .NET Applications,此处将对此进行总结。从根本上说,你真的只关心内存影响速度时,当你的应用程序变大时就会发生这种情况(TaskManager > 50 Meg 指示使用的内存)。但是,即使您的应用程序很小,也很容易对应用程序的总内存使用情况和 .NET 的 GC 堆,对于任何性能很重要的应用程序,您确实应该这样做。从字面上看,在几秒钟内,您可以获得 GC 堆的转储,并查看内存是否“合理”。如果您的应用确实使用了 50Meg 或 100 Meg 的内存,那么它可能会对性能产生重要影响,您需要花费更多时间来优化其内存使用。有关更多详细信息,请参阅文章。



何时关注 GC 堆


即使您已经确定您关心内存,仍然不清楚您是否关心 GC 堆。如果 GC 堆仅占内存使用量的 10%,则应将精力集中在其他地方。您可以通过打开任务管理器,选择“进程”选项卡并找到进程的“内存(专用工作集)”值来快速确定这一点。(有关专用工作集的说明,请参阅 .NET 应用程序的内存使用情况审核)。接下来,使用 PerfView 拍摄同一进程的堆快照 (Memory -> Take Heap Snapshot)。视图顶部是“总指标”,在本例中为内存字节。如果 GC 堆是进程使用的总内存的很大一部分,则应将内存优化集中在 GC 堆上。


如果发现进程使用大量内存,但它不是 GC 堆,则应下载免费的 SysInternals vmmap 工具。此工具为您提供进程使用的所有内存的细分(它比 .NET 应用程序的内存使用审核中提到的 vadump 工具更好)。如果此实用程序显示托管堆很大,则应对此进行调查。如果它显示“堆”(即操作系统堆)或“私有数据”(即 virtualAllocs),则应调查非托管内存。


收集 GC 堆数据


如果您尚未阅读何时关注内存和何时关注 GC 堆,请阅读以确保 GC 内存与您的性能问题相关。


Memory->Take Heap Snapshot 菜单项允许您拍摄任何正在运行的 .NET 应用程序的 GC 堆的快照。选择此菜单项时,它会打开一个对话框,显示系统上要从中进行选择的所有进程。

Memory Collection


通过在筛选器文本框中键入进程名称的几个字母,可以快速减少显示的进程数。在上图中,只需键入“x”即可将进程数减少到 7,而键入“xm”就足以将其减少到单个进程 (xmlView)。双击该条目将选择该条目并启动堆转储。或者,您只需单击一下即可选择该进程,然后继续更新对话框的其他字段。


如果 PerfView 未以管理员身份运行,则可能不会显示感兴趣的进程(如果它不归你所有)。通过单击“提升到管理员”超链接,以管理员身份重新启动 PerfView,以查看所有进程。


转储过程是对话框的唯一必填字段,但如果需要,您可以设置其他字段。(有关详细信息,请参阅内存收集对话框参考)。要开始转储,请单击“转储堆”按钮或直接键入回车键。


了解 GC 堆性能数据


一旦你有了一些GC堆数据,就必须了解你到底收集了什么,以及它的局限性是什么。从逻辑上讲,捕获的是堆中对象的快照,这些对象是通过遍历一组根的引用(就像 GC 本身一样)找到的。这意味着您只能发现在拍摄快照时处于活动状态的对象。然而,在正常情况下,有两个因素使这种表征不准确。


了解 GC 堆采样


对于某些应用程序,GC 堆可能会变得非常大(> 1GB,可能达到 50GB 或更多),当 GC 堆 1,000,000 个对象时,它会大大减慢查看器的速度,并使堆转储文件的大小变得非常大。


为了避免此问题,默认情况下,PerfView 仅收集小于 50K 对象的堆的完整 GC 堆转储。在此之上,PerfView 仅获取 GC 堆的样本。PerfView 在挑选“好”样本时遇到了一些麻烦。特别


  1. 执行样本时会考虑整个堆(活对象和死对象)

  2. 它实际上将整个堆图收集到内存中,并针对每种类型计算每种类型中的对象数量。 它还知道堆中的对象总数。

  3. 根据堆中的对象总数和对象的“目标”数量(默认为 50K),它计算“采样率”。  并由此计算每种类型的对象“配额”。

  4. 然后,它(线性)遍历堆,随机选择对象以达到每种类型的配额。

  5. 但是,我们还要求每个对象不仅包含自身,还包含“根路径”。为确保这一点

  6. 此外,始终收集大型对象(大小为 85,000 字节>区域。

  7. 选择所有样本后,将包括来自采样图中节点的任何引用。


结果是,所有样本始终包含至少一个根路径(但可能不是所有路径)。  所有大型对象都存在,并且每种类型都至少具有代表性的样本数量(由于原因(5)和(6),可能更多)。


了解 GC 堆扩展


GC 堆采样仅生成 GC 堆中对象的转储部分,但我们希望该样本代表整个 GC 堆。PerfView 通过缩放计数来实现此目的。遗憾的是,由于需要包含任何大型对象和任何对象的根路径,因此单个数字将无法正确缩放采样堆,使其表示原始堆。PerfView 通过记住原始图形中每种类型的总大小以及缩放图形中的总计数来解决此问题。使用此信息,对于每种类型,它会缩放该类型的 COUNT,以便该类型的 SIZE 与原始 GC 堆匹配。因此,在查看器中看到的内容应该非常接近在原始堆中看到的内容(只是 PerfView 更小且更易于消化)。这样一来,大型对象(总是被采用)的计数将不会被缩放,但最常见的类型(例如字符串)将被大量缩放。在打开 .gcdump 文件时,可以通过查看日志来查看 PerfView 用于缩放的原始统计信息和比率。


当 PerfView 显示已采样(因此需要缩放)的 .gcdump 文件时,它将在显示顶部的摘要文本框中显示已缩放类型的 COUNTS 的平均量以及必须缩放 SIZES 的平均量。  这是您正在进行采样/缩放的指示,并注意可能存在一些采样失真。


重要的是要认识到,虽然缩放试图抵消采样的影响(因此显示“看起来”像真实的、未采样的图形),但它并不完美。PER-TYPE 统计信息 SIZE 应始终准确(因为这是用于执行缩放的指标,但 COUNT 可能不是。特别是对于实例大小可能不同的类型(字符串和数组),计数可能处于关闭状态(但是您可以在日志文件中看到真实数字)。此外,堆的 SUBSETS 的计数和大小可以关闭。


例如,如果向下钻取到堆的某个特定部分(例如所有 Dictionary),可能会发现键的计数(字符串类型)和值的计数(MyType 类型)不同。这显然是出乎意料的,因为每个条目都应该有一个条目。此异常是采样的结果。出现此类异常的可能性与您正在推理的堆子集的大小成反比。因此,当你对整个堆进行推理时,应该没有异常,但是如果你对某个子树深处的少量对象进行推理,则可能性非常高。


一般来说,这些异常往往不会对分析产生太大影响。这是因为您通常关心堆的大部分,而这正是采样最准确的地方。因此,通常对这些异常的正确反应是简单地忽略它们。但是,如果它们干扰了您的分析,则可以通过减少采样来减少或消除它们。采样由“Max Dump K Objs”字段控制。默认情况下,将收集 250K 个对象。如果将此数字设置为较大,则采样将减少。如果您将其设置为某个非常大的数字(例如 10 亿),则根本不会对图形进行采样。请注意,PerfView 示例是有原因的。当作的对象数超过 100 万个时,PerfView 的查看器将明显滞后。超过1000万,这将是一次非常令人沮丧的经历。PerfView 在操作如此大的图形时也很有可能会耗尽内存。它还会使 GCDump 文件按比例变大,并且复制起来很笨拙。因此,应仔细考虑更改默认值。使用采样转储通常是更好的选择。


如前所述,GCHeap 集合(适用于 .NET)收集 DEAD 对象和活动对象。  PerfView 之所以这样做,是因为它允许你查看 GC 的“开销”(已消耗的空间量,但未用于活动对象)。  它还更可靠(如果无法遍历根或对象,则不会丢失大量数据)。  当图形显示时,可以确定死对象,因为它们将通过“[无法从根访问]”节点。  通常,您对死对象不感兴趣,因此可以通过排除此节点 (Alt-E) 来排除死对象。


GC 堆集合:冻结还是不冻结?


PerfView 能够冻结进程或允许其在收集 GC 堆时运行。如果进程被冻结,则生成的堆在该时间点是准确的,但是,由于即使对 GC 堆进行采样也可能需要 10 秒,这意味着该进程不会在该时间段内运行。对于“始终启动”的服务器,这是一个问题,因为 10 秒非常明显。另一方面,如果允许进程在收集堆时运行,则意味着堆引用会随时间而变化。事实上,GC 可能会发生,过去指向一个对象的内存现在可能已经死了,相反,将创建新对象,这些对象不会被之前在堆转储中捕获的根根。因此,堆数据将不准确。


因此,我们有一个权衡


PerfView 允许两者,但默认情况下它不会冻结进程。合理的是,对于大多数应用程序,您在进程等待用户输入时拍摄快照(因此进程无论如何都像是冻结一样)。例外情况是服务器应用程序。然而,这正是停止进程 10 秒可能很糟糕的情况。因此,在大多数情况下,允许进程运行的默认值更好。


此外,如果堆很大,则不会转储堆中的所有对象。只要正在运行的进程遗漏的对象在统计上与未移动的对象相似(可能在服务器进程中),那么堆统计信息对于大多数性能调查来说可能足够准确。


但是,如果出于某种原因希望消除正在运行的进程的不准确性,只需使用 Freeze 复选框或 /Freeze 命令行限定符来指示你对 PerfView 的希望。


将堆图转换为堆树


如了解 GC 堆数据中所述,在 .GCDump 文件可能只是 GC 堆的近似值。尽管如此,.GCDump 确实捕获了堆是任意引用图的事实(一个节点可以有任意数量的传入和传出引用,并且引用可以形成循环)。从分析的角度来看,这种任意的图表很不方便,因为没有明显的方法可以以有意义的方式“汇总”成本。因此,数据被进一步调整以将图形变成一棵树。


基本算法是对堆进行加权广度优先遍历,最多访问每个节点一次,并且只保留在访问期间遍历的链接。因此,任意图形被转换为树(没有循环,每个节点(根除外)只有一个父节点)。默认权重旨在选择“最佳”节点作为“父节点”。直觉是,如果您可以选择两个节点作为特定节点的父节点,则需要选择语义上最相关的节点。


使用优先级控制图形到树的转换


gc 堆内存数据的查看器有一个额外的“优先级”文本框,其中包含通过为每个对象分配浮点数值优先级来控制图形到树转换的模式。这是在两步过程中完成的,首先为类型名称分配优先级,然后通过类型为对象分配优先级。


“优先级”文本框是表单表达式的分号列表


其中 PAT 是简化模式匹配中定义的正则表达式模式,NUM 是浮点数。为类型分配优先级的算法很简单:在模式列表中找到与类型名称匹配的第一个模式。如果模式匹配,则分配相应的优先级。如果没有匹配的模式,则分配优先级 0。这样,每种类型都会被赋予优先级。


为对象分配优先级的算法同样简单。它从其类型的优先级开始,但它也在正在形成的生成树中增加了其“父级”的 1/10 优先级。因此,节点将其部分优先级分配给其子节点,因此这往往会鼓励广度优先行为(所有其他优先级相同,即距离具有给定优先级的节点 2 跳,将比距离 3 跳的节点具有更高的优先级)。


为所有“即将遍历”的节点分配优先级后,下一个节点的选择很简单。PerfView 选择优先级最高的节点进行接下来遍历。因此,具有高优先级的节点很可能是 PerfView 形成的生成树的一部分。这很重要,因为分析的所有其他部分都依赖于此生成树。


您可以在“优先级”文本框中查看默认优先级。此默认值背后的基本原理是:


因此,该算法倾向于首先遍历用户定义的类型,并找到路径中具有最多用户定义类型的最短路径。只有当这些链接用完时,它才会遵循框架类型(如集合类型、GUI 基础设施等),并且只有当这些类型用尽时,才会遍历匿名运行时句柄。当有选择时,这往往会将堆中对象的成本(大小)分配给语义更相关的对象。


为类型分配优先级的最佳实践


默认值出乎意料地好,通常您不必增加它们。但是,如果确实为类型分配优先级,则通常希望选择 1 到 10 之间的数字。如果所有类型都遵循此约定,则通常所有子节点都将小于给定显式类型的任何类型(因为它被除以 10)。但是,如果要为节点指定优先级,以便其子节点具有较高的优先级,则可以为其指定一个介于 10 和 100 之间的数字。让这个数字更大,甚至会迫使孙子孙女“赢得”最优先的比较。通过这种方式,您可以强制图形的整个区域具有高优先级。同样,如果存在您不想看到的类型,则应为它们提供一个介于 -1 和 -10 之间的数字。


GUI 能够快速设置特定类型的优先级。如果您在 GUI 中选择文本,右键单击优先级 -> 提高项目优先级 (Alt-P),则该类型的优先级将增加 1。还有类似的“较低项目优先级 (Shift-Alt-P)”。同样,有一个提高模块优先级 (Alt-Q) 和降低模块优先级 (Shift-Alt-Q),它们与与所选单元格具有相同模块的任何类型相匹配。


由于图形已转换为树,因此现在可以明确地将“子项”的成本分配给父项。在这种情况下,成本是对象的大小,因此在根目录上,成本将加起来等于 GC 堆(实际采样)的总(可访问)大小。


查看生成的堆树


将堆图转换为树后,可以在用于 ETW 调用堆栈数据的同一堆栈查看器中查看数据。但是,在此视图中,数据不是分配的堆栈,而是 GC 堆的连接图。您没有调用方和被调用方,而是推荐人和推荐人。没有时间的概念(“何时”、“第一”和“最后一列”),但包含和排他性时间的概念仍然有意义,分组和折叠操作同样有用。


需要注意的是,这种对树的转换是不准确的,因为它将子节点的所有成本归因于一个父节点(遍历中的父节点),而没有成本归因于恰好也指向该节点的任何其他节点。查看数据时请记住这一点。


堆栈查看器中的主节点与辅助节点


如将堆图转换为堆树中所述,在显示内存数据之前,会将其从图形(其中圆弧可以形成循环并具有多个父项)转换为树(其中从节点到根始终只有一条路径)。属于此树的引用称为主参照,在查看器中以黑色显示。但是,查看已修剪的其他引用也很有用。这些其他引用称为辅助节点。当存在辅助节点时,主节点以粗体显示,辅助节点为正常字体粗细。有时,辅助节点会使显示混乱,因此会出现“仅 Pri1”复选框,选中该复选框时会抑制辅助节点的显示。


主节点比辅助节点有用得多,因为存在明显的“所有权”或“包容性”成本概念。谈论主节点的节点及其所有子节点的成本是有意义的。辅助节点不具有此特征。打开辅助节点很容易“迷路”,因为您可能正在遵循一个循环而没有意识到这一点。为了帮助避免这种情况,每个辅助节点都标有其“最小深度”。此数字是从集合中的任何节点到根节点的最短 PRIMARY 路径。因此,如果您尝试使用辅助节点找到根路径,那么跟踪深度较小的节点将带您到达那里。


但是,通常最好不要花时间打开辅助节点。显示这些节点的真正目的是允许您确定“优先级”文本框中的优先级是否合适。如果您发现自己对辅助节点感兴趣,那么最好的响应很可能是简单地添加一个优先级,使这些辅助节点成为主节点。通过这样做,您可以获得合理的包容性指标,这是理解内存数据的关键。


对我们来说,设置优先级的一个好方法是右键单击 -> 优先级 -> 增加优先级 (Alt-P) 并右键单击 -> 优先级 -> 降低优先级 (Alt-Q) 命令。通过选择一个有趣或明确不有趣的节点并执行这些命令,您可以提高或降低其优先级,从而使其位于主树中(或不在主树中)。



开始分析 GC 堆转储


本节假设您已经确定 GC 堆是相关的,您已经收集了 GC 快照,并且您了解堆图是如何转换为树形的,以及堆数据是如何缩放的。除了此处完成的“正常”堆分析之外,使用 GCStats 报告以及 GC Heap Alloc Ignore Free (Coarse Sampling) 视图查看 GC 的批量行为也很有用。


自下而上的分析


与 CPU 时间调查一样,GC 堆调查可以自下而上或自上而下进行。与 CPU 调查一样,自下而上的调查是一个很好的起点。对于内存来说,这比对于 CPU 来说更是如此。原因是与 CPU 不同,视图中显示的树不是“事实”,因为树视图并不表示某些节点被多个节点引用的事实(即它们具有多个父节点)。正因为如此,自上而下的表示有点“武断”,因为你可以得到不同的树,这取决于图形的广度第一次遍历是如何完成的。自下而上的分析相对不受这种不准确性的影响,因此是更好的选择。


与 CPU 调查一样,自下而上的堆调查首先通过“折叠”任何语义不相关的节点来形成语义相关的组。这种情况一直持续到组的规模大到足以引起人们的兴趣为止。然后,可以使用“钻取”功能来启动子分析。如果您不熟悉这些技术,请参阅 CPU 教程。


Goto 调用方视图 (F10) 对于堆调查特别有用,因为它可以快速汇总 GC 根的路径,这些路径指示对象仍处于活动状态的原因。当您发现已经过时的对象时,必须断开其中一个链接,GC 才能收集它。需要注意的是,由于视图显示的是对象的 TREE 而不是 GRAPH,因此可能存在指向对象的其他路径未显示。因此,要使对象死亡,必须切断调用者视图中的一条路径,但这可能还不够。


GC 堆调查的分组和折叠


通常,GC 堆由


  1. 字符串(通常占 GC 堆总大小的 20-25%!

  2. 数组(通常是 byte[])。  这些通常占 10% 或更多。


不幸的是,虽然这些类型在堆的大小中占主导地位,但它们对分析并没有真正的帮助。你真正想知道的不是你使用了很多字符串,而是你控制的对象使用了很多字符串。好消息是,这是“标准问题”,即自下而上的分析,PerfView确实可以很好地解决。默认情况下,PerfView 会添加折叠模式,这些模式会导致所有字符串和数组的成本都记入引用它们的对象(就像字段被“内联”到引用它的结构中一样)。因此,其他对象(更有可能与您在语义上相关)将收取此费用。此外,默认情况下,“Fold%”文本框设置为 1,这表示应删除使用少于 1% 的 GC 堆的任何类型,并向引用它的人收取其成本。


GC 堆的自下而上分析与 CPU 调查大致相同。您可以使用 Stack Viewer 的分组和折叠功能来消除干扰并形成更大的语义相关组。当这些组足够大时,您可以使用“钻取”功能来隔离此类组,并在更详细级别上理解它。通过对应用程序内存使用情况的详细了解,您可以了解最有价值的优化位置。


确定要关注的类型后,了解类型的分配位置通常很有用。有关更多信息,请参阅 GC Alloc Stacks 视图。

 内存泄漏


一种常见的内存问题是“内存泄漏”。这是一组已达到其目的且不再有用的对象,但仍连接到活动对象,因此无法由 GC 堆收集。如果您的 GC 堆随着时间的推移而增长,则很有可能存在内存泄漏。各种类型的缓存是“内存泄漏”的常见来源。


内存泄漏实际上只是正常内存调查的一个极端情况。在任何内存调查中,您都会将语义相关的节点组合在一起,并评估您看到的成本是否因它们为程序带来的价值而合理。在内存泄漏的情况下,该值为零,因此通常只需找到成本即可。此外,还有一种非常简单的方法来查找泄漏


请注意,由于程序通常具有“一次性”缓存,因此通常需要修改上述过程。在获取基线之前,您需要执行一次或两次操作集。这样,在捕获基线时,任何“准时”缓存都将被填满,因此不会显示在差异中。


当您发现可能的泄漏时,请使用节点上的“转到调用方视图 (F10)”来查找从根到该特定节点的路径。这将显示使此对象保持活动状态的对象。若要解决此问题,必须断开其中一个链接(通常通过清空对象字段)。


GC 堆的自上而下分析


虽然自下而上分析通常是最好的开始方式,但通过查看 CallTree 视图来“自上而下”查看树也很有用。GC 堆的顶部是图形的根。这些根中的大多数要么是主动运行方法的局部变量,要么是各种类的静态变量。PerfView 不厌其烦地尝试获取有关根的尽可能多的信息,并按程序集和类对它们进行分组。快速查看哪些类占用了大量堆空间通常是发现泄漏的快速方法。


但是,应谨慎使用此技术。如将堆图转换为堆树一节所述,虽然 PerfView 尝试为节点查找语义上最相关的“父节点”,但如果一个节点有多个父节点,则 PerfView 实际上只是猜测。因此,可能有多个类“负责”一个对象,而您只看到一个。因此,将责任归咎于被任意选择为高成本节点的唯一“所有者”的类可能是“不公平的”。尽管如此,调用树视图中的路径至少是部分原因,并且至少值得进一步调查。请记住视图的局限性。


根信息注意事项


PerfView 使用 .NET 调试器接口收集有关 GC 堆根的符号信息。有时(通常是因为程序在旧的 .NET 运行时上运行)PerfView 无法收集此信息。如果 PerfView 无法收集此信息,它仍然会转储堆,但 GC 根是匿名的,例如,一切都是“其他根”。请参阅 GC 堆转储时的日志,以确定无法收集此信息的确切原因。


GC Stats 报告


典型的 GC 内存调查包括 GC 堆的转储。虽然这提供了有关创建快照时堆的非常详细的信息,但它没有提供有关一段时间内 GC 行为的信息。这就是 GCStats 报告的作用。若要获取 GCStats 报告,必须像 CPU 调查一样收集事件数据(默认情况下,GC 事件处于打开状态)。当您打开生成的 ETL 文件时,其中一个子项将是“GCStats”视图。打开它,系统上每个进程都会有一份报告,详细说明 GC 堆的位、GC 发生的时间以及每个 GC 回收的量。此信息对于大致了解 GC 堆如何随时间变化非常有用。


GC 堆分配忽略空闲(粗略采样)堆栈


除了 GC 统计信息报告所需的信息外,正常的 ETW 事件数据收集还将包括有关对象分配位置的粗略信息。每次分配 100K 个 GC 对象时,都会进行堆栈跟踪。这些堆栈跟踪可以显示在 ETL 文件的“GC Heap Alloc Stacks”视图中。


这些堆栈显示了大量字节的分配位置,但是它不会告诉您这些对象中哪些快速死亡,哪些对象继续存在以增加整个 GC 堆的大小。正是这些后面的对象是最严重的性能问题。但是,通过查看堆转储,您可以看到实时对象,并且在确定某个特定对象具有许多存在时间较长的实例之后,查看它们的分配位置会很有用。这就是 GC Heap Alloc Stacks 视图将向您显示的内容。


请记住,粗略的采样非常粗略。实际上,只有碰巧“绊倒”100KB 采样计数器的对象才会被采样。但是,事实是,所有大小超过 100K 的对象都将被记录下来,并且任何分配了大量小对象也可能被记录下来。在实践中,这已经足够了。

 大型物体


.NET 堆将堆分为“大对象”(超过 85K)和小对象(低于 85K),并完全不同地处理它们。 特别是,大型对象仅在第 2 代 GC 上收集(很少见)。  如果这些大型对象存在很长时间,一切都很好,但是如果大型对象被分配了很多,那么要么你使用了大量内存,要么你创建了大量垃圾,这将迫使大量 Gen 2 集合(这很昂贵)。  因此,不应分配许多大型对象。  GC Heap Alloc 视图有一个特殊的“LargeObject”伪帧,如果对象很大,它会注入该伪帧,从而非常容易找到分配大型对象的所有堆栈。 这是 GC Heap Alloc Stacks 视图的常见用法。


Net GC Heap Allocations Stacks(GC Heap Net Mem 视图)


调查 .NET GC 堆的内存使用率过高的首选是拍摄 GC 堆的堆快照。这是因为对象之所以保持活动状态,是因为它们已获得 root 权限,并且此信息会显示使内存保持活动状态的所有路径。但是,有时了解分配堆栈很有用。GC Heap Alloc Stacks 视图显示这些堆栈,但它不知道对象何时死亡。还可以打开额外的事件,使 PerfView 能够跟踪对象释放和分配,从而计算在 GC 堆上分配的 NET 内存量 (以及这些分配的调用堆栈) 。有两个详细级别可供选择。它们都位于集合对话框的高级部分中


  1. .NET Alloc - 每次在 GC 堆上分配对象时,此选项都会记录事件(和堆栈)

  2. .NET SampAlloc - 每次在 GC 堆上分配 10KB 对象时,此选项都会记录和事件。


在这两种情况下,它们还会记录对象何时被销毁(以便可以计算网络)。   在每次分配时触发事件的选项非常冗长。 如果您的程序分配了很多,它可能会减慢 3 或更多。  在这种情况下,文件也会很大(> 10-20 秒的跟踪为 1GB)。  因此,最好从第二个选项开始,即每 10KB 的分配触发一个事件。  这通常远低于开销的 1%,因此不会对运行时间或文件大小产生太大影响。  对于大多数目的来说,这已经足够了。


启用这些事件时,只有 .NET 进程在开始数据收集后启动。 因此,如果要分析长时间运行的服务,则必须重新启动应用程序才能收集此信息。


获得数据后,您可以在“GC Heap Net Mem”中查看数据,该数据显示所有分配的调用堆栈,其中指标为 GC Net GC 堆的字节数。GC Heap Alloc Stacks 和“GC Heap Net Mem”之间最显着的区别是,前者显示所有对象的分配堆栈,而后者仅显示那些尚未进行垃圾回收的对象的分配堆栈。


使用“.NET Alloc”复选框或“.NET SampAlloc”复选框收集的跟踪之间显示的内容基本上没有区别。  只是在 .NET SampAlloc 的情况下,信息可能不准确,因为特定的调用堆栈和类型被“收费”为 10K 的大小。  但是,从统计学上讲,如果收集了足够的样本,它应该为您提供相同的平均值。


.NET Net 分配分析的工作方式与非托管堆分析的工作方式相同。




PerfView 参考指南


取消操作和状态日志


PerfView 的目标之一是使界面始终保持响应。  这方面的表现是大多数窗口底部的状态栏。 此栏显示一行输出区域以及操作是否正在进行的指示、“取消”按钮和“日志”按钮。 每当长时间操作开始时,状态栏将从“就绪”更改为“正在工作”并闪烁。  取消按钮也会变为活动状态。  如果用户变得不耐烦,他可以随时取消当前操作。   还有一条单行状态消息,会随着进度的进行而更新。


执行复杂操作(如首次执行跟踪或打开跟踪)时,还会收集详细的诊断信息并将其存储在状态日志中。 当出现问题时,此日志可用于调试问题。   只需单击右下角的“日志”按钮即可查看此信息。



PerfView 主视图快速入门


在主视图中,您有三个基本选择:


收集事件(时间)数据的快速入门


虽然我们建议您学习本教程,并查看收集事件数据和了解性能数据,但如果您的目标是尽快查看基于时间的配置文件数据,请按照以下步骤操作


采集GC堆数据快速入门


虽然我们建议您学习本教程,并查看收集 GC 堆数据和了解 GC 堆数据,但如果您的目标是尽快查看内存配置文件数据,请按照以下步骤操作

 实时进程收集

 进程转储收集



主视图提示


除了常规提示之外,以下是特定于主视图的提示。



PerfView 的主视图


主视图是首次启动 PerfView 时迎接你的视图。   主视图有三个主要用途


  1. 它可作为 PerfView 的快速介绍,其中包含指向用户指南中重要起点的链接。

  2. 它承载 PerfView 的所有数据收集功能。

  3. 它的左窗格充当“性能资源管理器”,允许您决定要检查哪些性能数据。 双击项目将打开它们,右键单击将执行其他操作。

  4. 目录文本框 - 左窗格顶部是目录文本框。文件 -> 主查看器上的“转到目录”菜单选项 (CTRL-L) 这设置为要检查的目录。您也可以在其中输入文件名,这将导致它们被打开。当您在视图中打开目录项时,此文本框将更新以保持同步。

  5. “文件筛选器”文本框 目录文本框正下方的框。如果在此框中键入文本,则仅显示与此字符串匹配的文件(不区分大小写)。* 字符是通配符。这是在大型目录中查找特定文件的快速方法。


下图突出显示了主视图的重要部分。

MainViewer

 数据采集


通常,首次使用 PerfView 时,会使用它来收集数据。 PerfView 目前可以收集以下类型的调查的数据


  1. 时间调查:ETW 数据(具有多种变体) 使用“收集”菜单项中的项收集此数据。有关详细信息,请参阅收集 ETW 数据。

  2. .NET 内存调查:.NET 运行时托管堆。您可以使用“内存”菜单项收集此数据,请参阅收集内存数据以了解更多信息。


性能数据/视图的类型


PerfView 了解的数据类型



对象查看器快速入门

 待办事项未完成


 对象查看器提示


除了常规提示之外,下面还有特定于对象查看器的提示。


 对象查看器


对象查看器是一个视图,可用于查看有关 GC 堆上单个对象的特定信息。

 待办事项未完成



Stack Viewer 快速入门


虽然我们建议您学习本教程,但如果您的目标是了解堆栈查看器显示的内容,请按照以下步骤操作



在 Stack Viewer 中设置默认值


您可以使用“文件 -> 设置为默认分组/折叠”菜单项来设置 GroupPats 和 Fold 文本框中使用的默认值。这三个值在该计算机的 PerfView 会话中保留。“File -> Clear User Config”会将这些持久化值重置为默认值,这是撤消错误的简单方法。


GC 堆查看器快速入门


虽然我们建议您学习本教程,并查看了解 GC 堆性能数据和开始分析 GC 堆转储,但如果您的目标是尽快查看内存配置文件数据,请按照以下步骤操作


  1. 确定是否对内存感兴趣(请参阅何时关注内存,特别是何时关注 GC 堆,并拍摄 GC 堆快照 (内存 -> 拍摄堆快照)

  2. 了解 GC 堆栈查看器向您显示的内容,特别是主节点和辅助节点之间的区别。

  3. 对对象进行自下而上的分析,如 Starting a GC Heap Analysis 中所述。


Stack Viewer 提示


除了常规提示之外,这里还有特定于 Stack Viewer 的提示。



堆栈查看器


堆栈查看器是执行性能分析的主窗口。如果您尚未完成本教程或有关开始分析和理解性能数据的部分,那么这些内容将值得一读。这是堆栈查看器的布局

StackViewer


堆栈查看器有三个主要视图:ByName、Caller-Callee 和 CallTree。每个视图在堆栈查看器中都有自己的选项卡,可以使用这些选项卡进行选择。但是,更常见的情况是,使用右键单击或键盘快捷键从一个视图中的节点跳转到另一个视图中的同一节点。双击任何视图中的任何节点,实际上将带您进入Caller-Callee视图,并将焦点设置在该节点上。


无论选择哪种视图,所考虑的样本以及这些样本的分组对于每个视图都是相同的。此筛选和分组由视图顶部的文本框控制,分组和筛选部分对此进行了详细描述。


堆栈查看器的最顶部是汇总统计信息行。这为您提供了有关所有样本的统计信息,包括计数和总持续时间。它计算“TimeBucket”大小,该大小定义为跟踪总时间间隔的 1/32。这是“时间”列中每个字符所表示的时间量。


它还计算指标/间隔。这是对整个跟踪的 CPU 绑定程度的快速度量。值 1 表示平均占用单个处理器的所有 CPU 的程序。除非它很高,否则您的问题不是 CPU(它可能是一些阻塞操作,例如网络/磁盘读取)。


但是,此指标在收集数据时是平均值,因此可以包括感兴趣的进程甚至未运行的时间。因此,通常最好将节点的 When 列用于将进程作为一个整体呈现的节点,以确定进程的 CPU 绑定程度。


除了分组/筛选文本框之外,堆栈查看器还具有查找文本框,可用于搜索(使用 .NET 正则表达式)具有特定名称的节点。

 列说明


堆栈查看器网格中显示的列与显示的视图无关。  只需将列标题拖动到所需的位置即可对列进行重新排序,并且可以通过单击列标题文本右侧的列标题中的(通常不可见的)按钮对大多数列进行排序。   显示的列包括:

 列排序


PerfView 显示中的许多列都可用于对显示进行排序。您可以通过单击列顶部的列标题来执行此操作。再次单击可切换排序方向。请务必避免点击超链接文本(很容易不小心点击超链接)。单击顶部附近通常有效,但您可能需要使列标题变大(通过拖动其中一个列标题分隔符)。已经有人请求更改超链接,以便更轻松地访问列排序功能。


有一个已知的错误,即一旦按列排序,搜索功能就不会遵循新的排序顺序。这意味着在查找下一个实例时,搜索似乎会随机跳转。


ByName 视图(按方法分组)


堆栈查看器的默认视图是 ByName 视图。在此视图中,将显示每个节点(方法或组),并缩短为该节点的总 EXCLUSIVE 时间。这是用于自下而上分析的视图。有关使用此视图的示例,请参阅教程。双击条目将转到所选节点的调用方-被调用方视图。


有关详细信息,请参阅堆栈查看器。

 CallTree 视图


调用树视图显示每个方法如何调用其他方法,以及与从根开始调用的每个方法关联的样本数。它是进行自上而下分析的合适视图。每个节点都有一个与之关联的复选框,选中后显示该节点的所有子节点。通过选中复选框,您可以向下钻取到特定方法,从而发现任何特定调用对进程使用的总 CPU 时间的贡献。

CallTreeView


调用树视图也非常适合“放大”到感兴趣的区域。  通常,您只对程序特定部分的性能感兴趣(例如,鼠标单击与该单击相关的显示更新之间的时间) 通常,通过使用“主程序”节点上的“当”列查找 CPU 使用率较高的区域,可以很容易地发现这些时间区域。 或者,通过使用“SetTimeRange”命令查找已知与活动关联的函数的名称来限制调查范围。


与所有堆栈查看器视图一样,分组/过滤参数在形成调用树之前应用。


如果堆栈查看器窗口已启动以显示所有进程的样本,则每个进程只是“ROOT”节点之外的一个节点。   当您调查“为什么我的机器很慢”并且您真的不知道要查看哪个过程时,这很有用。  通过打开 ROOT 节点并查看 When 列,可以快速查看哪个进程正在使用 CPU 以及在哪个时间段内使用。


有关使用此视图的示例,请参阅教程。有关详细信息,请参阅堆栈查看器。有关不同的视觉表示,请参阅火焰图。


Caller Callee 视图


调用方-被调用方视图旨在让您专注于单个方法的资源消耗。通常,您可以通过双击节点名称从 ByName 或 Calltree 视图导航到此处。如果您有感兴趣的特定方法,请在 ByName 视图中搜索它(查找文本框),然后双击该条目。

CallerCalleeView


ByName 视图具有“当前节点”的概念。 这是感兴趣的节点,是显示器中心的网格线。  然后,显示屏在下部网格中显示当前节点调用的所有节点(方法或组),并在上部窗格中显示调用当前节点的所有节点。  通过双击上部或下部窗格中的节点,可以将当前节点更改为新节点,并以这种方式在调用树中上下导航。


但是,与 CallTree 视图不同的是,Caller-Callee 视图中的节点表示当前节点的所有调用。   例如,在 CallTree 视图中,表示“SpinForASecond”的节点表示该函数的所有实例,这些实例具有相同的根路径。  因此,您将在 CallTree 视图中看到“SpinForASecond”的多个实例。  但是,如果我试图了解“SpinForASecond”对整个程序的影响,则很难在CallTree视图中这样做,因为它将查看所有这些节点。  Caller-Callee 视图聚合了“SpinForASecond”的所有不同路径,因此您可以快速了解整个程序中“SpinForASecond”的所有调用方和“SpinForASecond”的所有被调用方。


重要的是要意识到,当您双击不同的节点以使当前的样本集发生变化时。  当当前节点为“SpinForASecond”时,此视图仅显示其调用堆栈中具有 SpinForASecond“的示例。  但是,如果双击“DateTime.get_Now”(“SpinForASecond”的子项),则视图现在将包含“DateTime.get_Now”由不包含“SpinForASecond”的调用堆栈调用的示例,并且不包括调用“SpinForASecond”但不包括“DateTime.get_Now”的调用堆栈。   如果您不知道它正在发生,这可能会令人困惑。


有时,您希望查看从特定节点访问根目录的所有方法。  由于更改样本集的问题,您不能直接使用调用方-被调用方视图执行此操作。  您可以简单地在 CallTree 视图中搜索节点,但它不会按权重对路径进行排序,这使得查找“最重要”路径变得更加困难。  但是,您可以选择当前节点,右键单击并选择“包含项目”。 这将导致所有不包含当前节点的样本被过滤掉。  这不应更改当前调用方-被调用方视图,因为该视图已仅考虑包含当前节点的节点。  但是,现在,当您将其他节点设置为当前节点时,它们也将仅考虑包含原始节点以及新当前节点的节点。  通过单击调用方节点,您可以追溯到根目录的路径。


由于调用方-被调用方视图聚合了其调用堆栈中具有当前节点 ANYWHERE 的所有样本,因此递归函数存在一个基本问题。  如果单个方法在堆栈上多次出现,则幼稚的方法会多次计算相同的单个样本(调用堆栈上的每个实例一次),从而导致错误的结果。  您可以通过仅计算堆栈上第一个(或最后一个)实例的样本来解决重复计数问题,但这会扭曲调用方-被调用方视图(看起来递归函数从不调用自身,这也是不准确的)。  PerfView 选择的解决方案是“拆分”示例。  如果一个函数在堆栈上出现 N 次,则每个实例的样本数量为 1/N。  因此,样本不会重复计算,但它也以合理的方式显示所有调用方和被调用方。


有关详细信息,请参阅堆栈查看器。

 来电者视图


调用方视图显示方法的所有可能调用方。  它是一个树视图(类似于调用树视图),但节点的“子节点”是节点的“调用者”(因此它是从调用树视图“向后”的)。    一种非常常见的方法是在“byname”视图中找到一个相当大的节点,查看其调用者(“通过双击 byname 视图中的条目”),然后查看是否有更好的语义分组“堆栈”应该折叠到该节点中。


如果双击“调用方”视图中的某个条目,则该条目将成为“调用方”视图、“被调用方”视图和“调用方-被调用方”视图的焦点节点。 因此,双击一个条目,切换到 Callees 视图,双击另一个条目并切换回来是相当常见的。


在调用方视图中,顶部节点始终是特定方法的所有用法的聚合,而不管调用方是谁。因此,顶行的统计信息应始终与“按名称”视图中的统计信息一致。此外,节点的任何子节点都表示父节点的调用方。这意味着


“调用方”视图中的任何子节点都表示父节点的调用方。它们将始终具有 0 的独占时间,因为根据定义,调用者不是堆栈的终端方法(因为它调用了其他方法)。


在“调用方”和“被调用方”视图中处理递归


调用方视图和被调用方视图都是通过查找包含焦点帧的所有样本以及查看相应的相关节点(调用方或被调用方)相关帧而形成的。但是,当焦点帧是递归函数时,有一个,因为调用方和被调用方有多种选择,具体取决于选择的递归实例。


PerfView 通过始终选择堆栈中递归函数的“最深”实例来解决此问题。因此,如果 A 调用 B 调用 C 调用 B 调用 D,并且焦点节点是 B,则此示例将具有 C(不是 A)的调用方和 D(不是 C)的被调用方。

 被调用方视图


被调用方视图是一个树视图,用于显示给定节点的所有可能的被调用方。  它与树视图非常相似,但是树视图始终从根开始,而被调用方视图始终从“焦点”节点开始,并包括到达该被调用方的所有堆栈。  在调用树视图中,节点的不同实例将分散在调用树中,并且很难集中注意力。


如果双击“被调用方”视图中的某个条目,则该条目将成为被调用方视图、“被调用方”视图和“被调用方-被调用方”视图的焦点节点。 因此,双击一个条目,切换到“调用方”视图,双击另一个条目并切换回来是相当常见的。


与调用方的观点一样,当涉及递归函数时,存在重复计数的问题。有关详细信息,请参阅“调用方”和“被调用方”视图中的递归处理。


火焰图视图


火焰图视图显示的数据与调用树视图相同,但使用不同的可视化效果。它为您提供了非常易于理解的概述。该图从底部开始。每个框表示堆栈中的一个方法。每个父母都是被召唤者,孩子是被召唤者。盒子越宽,它在 CPU 上的时间就越长。样本计数显示在工具提示和底部面板中。要更改火焰图的内容,您需要应用调用树视图的过滤器。要了解有关火焰图的更多信息,请访问 http://www.brendangregg.com/flamegraphs.html

FlameGraphView


PerfView 中的火焰图视图传统上反映消耗的内存量,但在绘制堆栈差异时,这可能会发生变化。垃圾回收后,在堆栈差异中检查类型消耗的内存量时,该类型消耗的内存量可能为负数。在这些情况下,相应的火焰图框以蓝色调绘制,表示内存增益。像往常一样,使用黄色/红色调绘制增加的内存使用量。

FlameGraphDiffView

 注释视图


这使您可以记笔记。此视图包含的数据与“注释窗格”中的数据相同,您可以使用 F2 键进行切换。 这些注释在保存视图时保存,因此允许您保留在调查期间需要跟进的潜在顾客等信息。 笔记窗格特别有用,您需要将调查“移交”给另一个人。 通过将性能问题的“解释”放在便笺窗格中,并发送保存的视图,下一个人可以从您离开的地方“继续”。



重用过滤参数

 命名参数集


通常情况下,分组和过滤参数定义变得相当复杂,但它们具有相对简单的语义含义。 能够保存这些参数并将其重用于其他调查也很有用。  为此,可以为过滤器参数集指定一个名称(只需在名称文本框中输入文本,以后可以使用此名称来标识此过滤器参数集)。


命名参数集是当前未由 PerfView 使用的。


区分两条迹线


PerfView 能够获取两个堆栈视图之间的差异。 这对于了解由最近更改引起的回归的原因非常有用。  要使用此功能,您应该


然后,PerfView 将打开一个堆栈视图,其中包含所选的“测试”视图和“基线”之间的差异。  它用来执行此操作的算法非常简单。 它只是否定基线的指标,然后将这些样本与测试样本(未修改)组合在一起。 结果是具有样本的跟踪,该样本具有“测试”和“基线”的样本之和,但是基线中所有样本的计数值和指标值为负。这意味着计数和指标值通常会“抵消”,只留下测试中的内容,而不是基线。


与正常调查一样,您应该使用“按名称”视图开始“差异”调查。   在典型的调查中,“测试”跟踪的指标(回归)严格多于基线,这反映在差异的总数中(差异的总指标应为测试的总指标减去基线的总指标)。  然后,“ByName”视图显示这种差异与使用“GroupPats”选择的组有关(就像正常跟踪一样)。


如果幸运的话,“按名称”视图中的每一行都是正数(或非常小的负数)。  这是“简单”的情况,当这种情况发生时,你就有了你感兴趣的信息(在测试中有额外成本但没有基线的精确组位于“按名称”视图的顶部。 从这一点开始,差异调查就像普通调查一样工作(您可以向下钻取、查看其他视图、更改分组、折叠等......


但是,视图中出现较大的负值的情况并不少见。  当这种情况发生时,差异不是那么有用,因为我们对测试跟踪中的 ADDITIONAL 时间感兴趣,但视图中的负数告诉我们,基线使用的时间比测试多的地方很大。   显然,总和必须加起来才能得到最终的回归,但只要视图中存在较大的负值,我们就不能信任视图中的大正值,因为它们可能会被负值抵消。


因此,对差异跟踪的分析始终有一个加法步骤:在形成差异视图之后,但在进行任何分析之前,必须使用分组/折叠/过滤运算符来确保负值已被充分“抵消”。视图只需要具有正度量数(或无关紧要的负数)。


事实上,PerfView 已经对此有所帮助。  通常,堆栈显示中的进程和线程节点包含该节点的进程和线程 ID。  虽然这是有用的信息,但它也意味着基线和测试跟踪中的节点可能永远不会匹配(因为它们具有不同的 ID)。  如果不加以纠正,这将导致“TreeView”变得非常无用(它将在“测试”过程中显示一个较大的正数,在“基线”下显示一个稍小的大负数,但不会取消。  PerfView 通过提供有效从节点中删除进程和线程 ID 的分组来解决此问题。 现在节点匹配,您将获得所需的取消。


但是,PerfView 只能做这么多。  它可以预测到需要重写进程和线程 ID,但它无法知道您重命名了某个函数,或者延迟初始化导致某些初始化的成本从一个位置移动到另一个位置。  简而言之,PerfView 无法知道您希望忽略的所有“预期”差异。 作为分析师,你的工作是使“预期”差异“完全匹配”,从而抵消。


PerfView 强大的折叠和分组运算符是用于创建此取消的工具。  要记住的口头禅是“分组是你的朋友”,保持你的小组尽可能大。    特别


这种策略背后的基本原理很简单。  你形成的群体越大,“无关紧要”的差异就越有可能简单地“抵消”。   模块往往是最有用的“大组”,因此按模块对所有样本进行分组可能会向您显示取消起作用的视图(视图中只有小负数)。  一旦确定了特定模块中负责回归的样本,就可以使用“钻取”功能来隔离这些样本,并更改分组以显示更多详细信息。  这往往是一个非常有用的策略。


更多差异取消策略


在差异中实现取消的主要技术是选择大组,然后仅钻取感兴趣的样本。  但是,还有一些其他有用的事情需要记住。


  1. 使方案尽可能小。

  2. 通常,只有“自下而上”的分析才适用于差异。在堆栈的“顶部附近”很容易出现差异,从而阻碍取消。通过进行自下而上的分析(“按名称”视图和被调用方视图)来避免这种情况。


修复重命名的函数


分组允许您将任何节点名称重命名为任何其他节点名称。 因此,您可以“修复”跟踪中的任何“预期”差异。  例如,如果 MyDll!MethodA 已重命名为 MyDll!MethodB,您可以添加分组模式


我的Dll!方法A->方法A;我的Dll!方法B->方法AAl!方法B->方法A


这将它们都“重命名”为简单的“MethodA”并解决差异。  折叠也可以用来解决这样的差异。 例如,如果这两种方法对事件不感兴趣(您不需要在调用堆栈上看到它们),那么您可以简单地使用折叠模式始终折叠它们

 方法A;方法B


这使得它们都消失了(因此不会造成差异)。



超重分析的回归调查


超重分析是一种相当简单的技术,其中分析了两条迹线中所有符号的包含成本。通常使用时间指标,但任何包含成本都可以。


这个想法是这样的:使用基础和测试运行,很容易获得回归的整体大小。假设它是 10%。从那里你可以把一切都慢 10% 作为你的零假设。您要查找的是变化超过 10% 的符号,因此在某种意义上对更改负有更大的责任。在这种情况下,增持报告将简单地计算实际增长与预期增长 10% 的比率。当您发现超重超过 100% 的符号时,这些符号会引起极大的兴趣。


假设 main 调用 f 和 g,不做其他任何事情。每个需要 50 毫秒,总共 100 毫秒。现在假设 f 变慢,到 60 毫秒。现在的总数是 110,或恶化了 10%。这个算法将如何提供帮助?好吧,让我们看看增持。当然,主要是 100 到 110,或者说 10%,这是全部,所以预期增长是 10,实际是 10。超重100%。那里没什么可看的。现在让我们看一下 g,它是 50,保持在 50。但它“应该”去 55。超重 0/5 或 0%。最后,我们的大赢家,f,它从50上升到60,增加了10。在10%的增长下,它应该获得5。超重 10/5 或 200%。问题出在哪里非常清楚!但实际上它变得更好了。


假设 f 实际上有两个孩子 x 和 y。每个过去需要 25 毫秒,但现在 x 减慢到 35 毫秒。如果没有归因于 y 的收益,则 y 的超重将为 0%,就像 g 一样。但是,如果我们看一下x,我们会发现它从25上升到35,增加了10,它应该只增长2.5,所以它的超重是10/2.5或400%。在这一点上,模式应该很清楚:



当您靠近子树的根时,超重数字会不断上升,这是问题的根源。低于此值的所有内容都倾向于具有相同的超重。例如,如果问题是 f 再次调用 x,您会发现 x 及其所有子项都具有相同的超重数。


这将我们带到了该技术的第二部分。您想要选择一个具有较大超重但同时负责较大部分回归的交易品种。因此,我们计算其增长并除以总回归成本,得到责任百分比。这很重要,因为有时您得到的叶函数有 2 个样本,并且仅仅因为采样误差而增长到 3 个。这些可能看起来像是巨大的超重,所以你必须专注于具有合理责任百分比和大幅超重的方法。该报告会自动过滤掉责任小于 +/- 2% 的任何内容。


本摘要的大部分内容都可以在线获得,此处提供了更多示例。



事件查看器快速入门


事件查看器是一项相对高级的功能,可让您查看 ETL 文件中收集的“原始”事件。  尽快开始



事件查看器提示


除了常规提示之外,下面还有特定于事件查看器的提示。



事件查看器


某些数据文件(目前在 XPERF、csv 和 csvz 文件上)支持按时间排序的任意事件的视图。  事件查看器是用于显示此数据的窗口。 基本上,它是按时间顺序排列的事件视图,可以对其进行过滤和搜索。  一个典型的方案是,应用程序已使用事件(如 System.Diagnostics.Tracing.EventSource)进行检测,这些事件用于确定感兴趣的时间。

EventViewer


视图有两个主面板。 左侧的面板包含跟踪中的所有事件类型。  您只需按住控制键单击感兴趣的选项即可选择感兴趣的选项(同时选择多个。 右侧窗口包含实际事件记录。  对数据执行扫描以形成列表的成本相对较高,因此您必须明确要求更新正确的面板。  您可以通过多种方式执行此操作


  1. 点击左上角的“更新”按钮
  2.  按 F5 键

  3. 双击左侧面板中的条目(如果有多个选择,则还必须按住 Ctrl 键以免丢失选择)

  4. 右键单击并选择“更新”菜单项。

  5. 在窗口顶部的任何过滤文本框中按回车键。


按进程过滤


除了按事件类型进行筛选外,您还可以通过在“流程过滤器”文本框中放置文本来按流程进行过滤。此文本是 .NET 正则表达式,仅选择具有与此文本匹配的进程的记录。匹配不区分大小写,只需匹配进程名称中的子字符串。您可以使用标准正则表达式 ^ 和 $ 运算符来强制匹配完整字符串。请注意,对于上下文切换事件,进程筛选器将匹配正在切换的进程 (OldProcessName) 以及切换到的新进程 (ProcessName)。


限制返回的记录数


跟踪可能非常大,因此可以在右侧面板中返回大量结果。  为了加快速度,返回合理数量(默认为 10000 条)的记录。 这是“MaxRet”值。  如果它太小,可以将此文本框更新为更大的内容。


按文本过滤


除了按进程筛选外,还可以按返回事件中的文本进行筛选。仅显示整个显示文本与模式匹配的记录。因此,如果更改显示的列,如果“文本过滤器”文本框中有文本,则可能会影响过滤。“文本筛选器”中的字符串被解释为 .NET 正则表达式,默认情况下,与进程筛选器一样,匹配只需匹配子字符串即可成功。如果模式以“!”字符开头,则仅显示与模式不匹配的条目。

 选择列


特定于事件的字段在“数据”列中显示为一系列 NAME=VALUE 对。  此数据列可能很长,并且通常最感兴趣的元素位于末尾,因此视图不方便。  您可以通过在“要显示的列”文本框中放置字段名称(不区分大小写)来指示要显示哪些特定于事件的列来解决此问题。 这可以通过单击“列”按钮轻松填充。 这将显示所有列的弹出列表,您只需单击感兴趣的列(shift 和 ctrl 单击以选择多个条目),然后按“输入”继续。   这些列将按您选择项目的顺序显示,并且“*”可用作表示尚未选择的所有列的通配符。最多 4 个字段将显示在它们自己的列中。在前 4 列之后,指定列的其余部分将显示在“rest”列中。


筛选选定列


可以使用“要显示的列”文本框通过指定表达式和布尔运算符来筛选事件:||和 && 基于方括号 ([]) 内的选定列。单个查询的格式为:LeftOperand 运算符 RightOperand,其中:

 笔记:  例子:


简单查询的示例包括:


一些更复杂的表达式示例:

一些用法的视频示例:

 事件类型


左侧面板包含跟踪中的所有事件。  其中包括 OS 内核收集的事件、.NET 运行时,以及收集数据时指示的任何其他事件。


筛选事件列表


由于事件类型的数量可能很大(通常为数十个),因此事件类型窗格顶部有一个“过滤器”文本框。  如果要查找特定事件,只需在此文本框中键入事件名称的某些部分,显示的列表将被筛选为在名称中某处包含键入文本的事件。您在此处键入的文本实际上是一个 .NET 正则表达式,这意味着您可以使用通配符(. 和 *),也许最重要的是 |运算符表示“或”。这使您可以快速过滤掉除一些有趣事件之外的所有事件。另请记住,Ctrl-A 将选择视图中的所有内容。

 事件直方图


更新事件视图时,除了填充主列表框外,它还会生成事件计数的直方图,该直方图显示所选事件的频率如何随时间变化。由“开始”和“结束”文本框指定的时间间隔被划分为 100 个存储桶,并计算每个存储桶的事件计数,然后缩放此数字,使最大的存储桶表示 100%,并使用堆栈查看器的 When 列中使用的相同约定将此百分比转换为数字(或字母)。它显示在列表框的正上方。与“当”列一样,您可以选择此显示的一部分,然后使用“设置范围过滤器”命令 (Alt-R) “放大”。此外,当您更改直方图文本框中的选择时,PerfView 将计算开始和结束时间、事件总数和平均事件速率,并在状态栏中显示这些值。


重要的内核事件


下面是一些值得了解的内核和 .NET 事件



“ETW 数据收集”对话框


在开始收集之前,PerfView 需要知道一些参数。  它填充除要运行的命令之外的所有命令的默认值。因此,在常见情况下,您只需要填写命令即可运行(您使用的是“运行”命令)并按回车键开始收集数据。


无论您使用“运行”还是“收集”命令,都会在计算机范围内收集配置文件数据。  要收集配置文件数据,您必须具有管理员权限。 否则,PerfView 将尝试提升 (打开 UAC 对话框) ,并使用管理员权限重新启动自身。

 高级选项


PerfView 选择一组有用的默认 ETW 事件进行记录,这些事件允许进行常见的性能分析,但是,可以打开许多 ETW 事件。 以下是这些更高级事件中一些最有用的示例。


除了更高级的事件之外,还有一些您很少需要更改的高级选项。

 提供商浏览器


提供程序浏览器是从 ...“其他提供程序”文本框右侧的按钮。提供程序浏览器允许用户检查可用的提供程序以及任何特定提供程序的可用关键字。


由于计算机范围内有许多可用的 ETW 提供程序,因此浏览器还允许将搜索筛选为仅与特定进程相关的提供程序。

 查看清单


虽然提供程序的名称及其关键字通常足以决定是否打开哪些事件,但您希望了解有关可能事件的更多信息并不罕见。这就是“查看清单”按钮的用途。许多提供程序注册一个称为清单的 XML 文档,该文档以相对精细的细节描述提供程序可以生成的所有事件。此清单中包括


此信息通常足以理解确定要为任何给定应用程序设置的最佳关键字。有关清单中信息的更多详细信息,请参阅官方文档)。

 Abort 命令


ETW 数据收集的模型是在整个计算机范围内收集数据。此外,数据收集可能会超过开始收集的进程的生存期。虽然此特性很有用(它允许独立的启动和停止命令行命令),但它也意味着可能会意外地使 ETW 集合无限期地运行。PerfView 会在一定程度上确保在典型情况下停止数据收集,但是,如果 PerfView 异常终止,或者使用了命令行“启动”操作,则 ETW 数据收集可能会保持打开状态。Collect->Abort 命令就是为这种情况而设计的。它确保 PerfView 打开的任何 ETW 提供程序都处于关闭状态。


最后,还可以轻松地从命令行启动 PerfView 来收集配置文件数据。有关详细信息,请参阅从命令行收集数据。



“内存收集”对话框


内存收集对话框允许您选择用于收集 GC 堆数据的输入和输出,以及设置有关如何收集该数据的其他选项。


过滤/分组堆栈数据


简化的模式匹配


遗憾的是,普通 .NET 正则表达式的语法对于匹配方法名称的模式不是很方便。  特别是,'.'、'\'、'('')',甚至'+'和'?'都用在方法或文件名中,需要转义(或者更糟糕的是,用户会忘记他们需要转义它们,并得到误导性的结果)。  因此,PerfView 使用一组简化的模式来避免这些冲突。  模式是


这种简化的模式匹配用于 GroupPats、FoldPats、IncPats 和 ExcPats 文本框。如果您需要更强大的匹配运算符,可以通过在 ENTIRE PATTERN 前面加上 @ 来做到这一点。这向 PerfView 指示模式的其余部分遵循 .NET 正则表达式语法。


“查找”框中不使用简化的模式匹配。 为此,使用了真正的 .NET 正则表达式。


分组(GroupPats 文本框)


另请参阅简化模式匹配。


从根本上说,PerfView 探查器收集的是一系列堆栈。 每毫秒为计算机上的每个硬件处理器收集一个堆栈。  这是非常详细的信息,但由于“树”(许多不同组件使用的数百甚至数千个“帮助程序”方法的数据),很容易看不到“森林”(语义组件消耗不合理的时间)。    驯服这种复杂性的一个非常重要的工具是将方法分组到语义组中。   PerfView 提供了一种简单但非常强大的方法来执行此操作。


每个示例都由一个堆栈帧列表组成,每个堆栈帧都有一个与之关联的名称。 最初看起来像这样


具体而言,该名称由包含该方法的 DLL 的完整路径组成(但已删除文件名后缀),后跟“!”,后跟方法的全名(包括命名空间和签名)。  默认情况下,PerfView 只是从名称中删除目录路径,并使用该路径进行显示。  但是,可以改为要求 PerfView 将与特定模式匹配的方法组合在一起。 有两种方法可以做到这一点。


  1. PAT->GROUPNAME 将任何与 PAT 匹配的帧名称替换为文本 GROUPNAME。

  2. PAT=>GROUPNAME 类似于 PAT->GROUPNAME,但请记住组的“入口点”。(见条目组)


第一种形式是最容易理解的。  基本上,它只是搜索和替换所有帧名称。    任何与给定模式匹配的帧都将(完整地)替换为 GROUPNAME。  这具有创建组(与特定模式匹配的所有方法)的效果。  例如,规范


将匹配任何具有 mscorlib 的帧!Assembly:: 并将整个框架名称(而不仅仅是匹配的部分)替换为字符串 'class Assembly'。  这样做的效果是将类 Assembly 中的所有方法分组到一个组中。 使用一个简单的命令,您可以将特定类中的所有方法组合在一起。


与 .NET 正则表达式一样,PerfView 正则表达式允许你“捕获”字符串中与模式匹配的部分,并将其用于形成组名称。  通过使用 {} 将模式的各个部分括起来,您可以捕获该部分模式,然后可以使用 $1, $2, ...表示第一,第二,......捕获。 例如


表示匹配 ! 之前包含字母数字字符的任何帧,并将这些字母数字字符捕获到 $1 变量中。  然后,无论匹配什么,都用于形成组名。  这样做的效果是按包含样本的模块对所有样本进行分组(“模块级视图”)。


具有多个组规范很有用,因此组语法支持分组命令的分号列表。 例如,这是另一个有用的。


此规范中有两种模式。 第一个(蓝色)外观捕获了 !以及 (.  这将捕获 .NET 样式方法名称的“类和命名空间”部分。  第二种模式对 C++ 样式名称(使用 :: 将类名与方法名分开)非常相似。   因此,上面的规范按类对方法进行分组。  强!


另一个有用的技术是利用这样一个事实,即模块的完整路径名与组的匹配范围甚至比模块更广泛。 例如,由于 * 匹配任意数量的任意字符,因此模式


将具有将来自任何模块的任何方法分组的效果,该模块将 system32 作为其模块路径的任何部分作为“OS”。  这非常方便,因为通常这是人们想要的。 他们不希望看到操作系统内部方法的任何细节,他们希望将它们组合在一起。 这个简单的命令一举完成此操作。


对优先级组和排除组进行分组


当帧与组匹配时,它按组模式的顺序完成。  一旦发生匹配,就不会对该帧的组模式进行进一步处理(第一个帧获胜)。  此外,如果省略 GROUPNAME,则表示“不进行转换”。  这两种行为可以组合在一起,以强制某些方法不在组中。 例如,规范


强制所有模块使用模块级视图(红色分组模式),但是由于第一种(蓝色)模式,任何具有“myDirectory;在他们的路径中不按红色模式分组(他们被排除在外)。 这可用于创建“只是我的代码”效果。 除了位于“myDirectory”下的代码之外,每个模块的功能都组合在一起。  强!

 条目组


到目前为止,这些例子是“简单组”。  简单组的问题在于,您忘记了有关您如何“进入”组的宝贵信息。 考虑将 System32 中的所有模块分组到前面考虑的名为 OS 的组的示例。  这效果很好,但有局限性。 您可能会看到,在操作系统中调用的特定函数“Foo”可以使其在操作系统中执行的任何操作都需要花费大量时间。  现在,只需查看“Foo”的主体就可以“猜测”正在调用什么操作系统函数,但这显然是一种不必要的痛苦。   收集的数据确切地知道输入了哪个操作系统功能,只是我们的分组已经剥离了该信息。


这是条目组要解决的问题。  它们就像普通组一样,但使用 => 而不是 -> 来表示它们是条目组。  条目组创建与普通组相同的组,但它指示分析逻辑将调用方考虑在内。 实际上,每个“进入组的入口点”都形成了一个组。  如果从组外部到组内部进行呼叫,则入口点的名称将用作组的名称。  只要该方法调用组中的其他方法,堆栈帧就会被标记为在组中。    因此,边界方法被单独保留(它们总是形成另一个组,但内部方法(在组内调用的方法)被分配给任何调用它的入口点组。


这非常符合人们通常的模块化概念。 虽然在某些情况下将操作系统中的所有功能分组为一个组是合理的,但按“公共外围应用”(操作系统的每个入口点的组)对它们进行分组也是合理的。  这就是条目组的作用。  因此,命令


将折叠所有操作系统功能,仅将其入口点保留在列表中。 这非常强大!


组说明(注释)


组可以是一个强大的功能,但通常仅通过查看模式定义并不能清楚地了解组的语义有用性。  因此,允许在实际组模式之前进行组的描述。 此说明用方括号 [] 括起来。  PerfView 会忽略这些描述,但是对于人类查看这些描述以了解模式的意图非常有用。

 折叠(内联)


按名称折叠 (FoldPats TextBox)


另请参阅简化模式匹配。


特定的帮助程序方法在配置文件中显示为“热”的情况并不少见。 您已经查看了此辅助方法,它与制作的一样高效。 没有办法让它变得更好。  因此,在配置文件中看到此方法不再有趣。  你希望此方法“内联”到其每个调用方中,以便向他们收取费用(而不是显示在帮助程序中)。 这正是折叠的作用。  “FoldPats”文本框只是一个要折叠的模式的分号列表。  因此,模式


将从跟踪中删除 MyHelperFunction,将其时间移动到调用它的人中(作为独占时间)。 它具有将 MyHelperFunction “内联”到所有调用方中的效果。


分组转换发生在折叠(或筛选)之前,因此可以使用组的名称来指定折叠。 因此,折叠规格


将在一个简单的命令中折叠所有操作系统功能(到它们的父级)。


折叠小节点 (折叠 % 文本框)


一般来说,如果某个方法在视图中消耗的不超过总数的 1%,那么它通常只是“杂乱无章”地显示。折叠 % 文本框旨在消除此干扰。任何方法整体聚合非独占指标(即“Inc”列的 ByName 视图中显示的内容)都小于总指标的 1%,将被删除,并将其指标提供给其直接父级。


虽然将这个数字增加到一个很大的值(比如 10% 或更多)很诱人,但要强制大多数调用堆栈变得“大”,这通常会产生较差的结果。原因是 % 没有考虑节点的语义相关性。因此,折叠可能会将一个非常语义有意义的节点折叠成某个更高级别函数的“助手”。因此,通常最好选择“你不理解”的节点来折叠,这样你剩下的就是对你有意义的节点。

 滤波


筛选具有特定帧的堆栈(ExcPats TextBox)


分组和折叠具有不影响跟踪中总样本计数的属性。样本不会被删除,它们只是被重命名或分配给另一个节点。完全排除节点也很有用。ExcPats 文本框是简化正则表达式的分号列表(请参阅简化模式匹配)。如果堆栈中的任何帧与此列表中的任何模式匹配,则会将其从视图中删除。模式不必与完整的帧名称匹配,除非它被锚定(例如,使用 ^)。图案在分组和折叠后匹配。


排除筛选的常见用途是查找应用中“第二大问题”的性能问题。  在这种情况下,你发现一个特定的方法(比如“Foo”)设计得很糟糕,你甚至知道如何修复它,但你也知道这不是你唯一的问题。  你想要的是找到下一个最重要的问题。  通过排除调用“Foo”的样本,您可以有效地模拟如果 Foo 是“完美”(不需要时间)程序的行为。  这通常是应用修复程序后程序外观的近似值。  因此,通过简单地排除这些样本,您可以寻找下一个性能问题,从而快速解决其中的许多问题。


筛选任何不包含特定帧的堆栈(IncPats 文本框)


默认情况下,事件是在计算机范围内捕获的,但通常只对某些示例感兴趣。例如,只对一个进程或一个线程感兴趣,或者只对一种方法感兴趣是很常见的。这就是 IncPats 文本框的作用。文本框的内容是以分号分隔的简化正则表达式列表(请参阅简化模式匹配)。堆栈必须至少与 IncPats 列表中的模式之一匹配,才能包含在跟踪中。模式不必与完整的帧名称匹配,除非它被锚定(例如,使用 ^)。图案在分组和折叠后匹配。


如前所述,使用 IncPats 文本框将分析限制为单个进程是很常见的。  使用“|”也非常有用(or) 运算符,以便您可以只包含两个(或更多)进程并排除其余进程。


按时间筛选(“开始”和“结束”) 按时间筛选(“开始”和“结束”文本框)


“放大”到感兴趣的特定时间并过滤掉超出此范围的样本非常有用。  这是通过适当设置“开始文本框”和“结束文本框”来完成的。 这些范围是包含的(在两端),并且从跟踪开始表示为毫秒。    当然,您可以手动输入时间,也可以从显示屏的其他部分剪切和粘贴数字。  此外,如果您将两个数字粘贴到“开始”文本框中,它将设置开始值和结束值。还有其他一些不错的快捷方式可以设置时间间隔。


选择时间范围


树节点的“第一列”和“最后一列”通常是有用的筛选范围。 要轻松做到这一点,只需选择两个框(通过拖动或按住“Ctrl”键,同时单击其他条目),选择两个单元格后,您可以右键单击并选择“设置时间范围”,这会将开始和结束时间设置为第一列和最后一列。您还可以通过将两个数字复制到剪贴板(选择两个单元格并按 Ctrl-C),然后将数字粘贴到“开始”文本框中来选择时间范围。此文本框足够智能,可以识别粘贴的值是一个范围,并将适当地设置“结束”时间。


根据“时间”列选择时间范围也非常有用。 为此,首先选择感兴趣的“时间”单元格。  这将导致视图底部的状态栏显示“何时”文本。  通过将鼠标拖动到字符上,突出显示感兴趣的区域(通常是成本较高的区域)。  然后将鼠标移出所选区域,右键单击并选择“设置时间范围”。 这会将“开始”和“结束”时间设置为您选择的区域。  您最终可能会重复此过程以进一步“放大”到某个区域。


通过采样加速 StackViewer 显示。


如果堆栈查看器中查看的数据样本超过 1M,则响应速度会变得非常缓慢(需要 10 > 秒才能更新)。为了避免这种情况,某些堆栈源(最明显的是内存堆栈源)支持采样的概念。采样背后的基本思想是只处理每 N 个样本。因此,通过将采样文本框设置为 10,堆栈视图只需处理 1/10 的数据,因此速度应该快 10 倍。启用采样后,堆栈查看器会自动按采样率缩放视图中的所有计数(因此也缩放指标)。因此,生成的指标和计数与未采样时大致相同(您可以看到这一点,因为所有计数都是采样率的倍数。


在视图中查找项(“查找”文本框)


通过在堆栈查看器右上角的“查找:”文本框中键入搜索模式,可以对视图中的名称进行文本搜索。Ctrl-F 将带您快速进入此搜索框。搜索模式使用 .NET 正则表达式,并且不区分大小写。搜索从当前光标位置开始,并一直环绕,直到搜索完所有文本。F3 键可用于查找模式的下一个实例。搜索完所有文本后,应用程序将发出哔哔声。之后的下一个 F3 重新开始。表达式的指定与布尔条件相结合,可以类似于在“要显示的列”文本框中筛选选择列。


预设(保存、分组和折叠首选项)


可以编辑 GroupPats、FoldPats 和 Fold% 文本框以包含自定义模式。这些模式组合在一起可以保存为命名预设。


要创建新预设,请使用“预设”->“另存为预设”菜单项。如果 GroupPats 文本框包含描述(包含在 [] 中),则描述将作为预设名称提供。否则,将建议自动生成的名称。


所有创建的预设都将添加到所有活动 PerfView 窗口的“预设”菜单中。在“预设”菜单中选择菜单项以激活预设。预设的名称将显示在 GroupPats 文本框的 [] 中。预设在会话之间保存。预设 -> 管理预设菜单项允许编辑现有预设以及删除它们。



阻塞/挂钟时间调查:线程时间视图


为什么封锁/挂钟时间调查更难


挂钟时间调查分为两起案件。 要么大部分挂钟时间由 CPU 主导(在这种情况下,CPU 调查将起作用),要么不由 CPU 时间主导,在这种情况下,您还需要了解正在消耗的阻塞(非 CPU)时间。   因此,进行挂钟调查的“困难”部分是了解阻塞时间。


阻塞时间调查本质上比 CPU 调查更难。CPU 调查相当简单,因为在大多数情况下,任何 CPU 使用率都是“有趣的”调查,无论它发生在哪里。因此,无论发生在哪里,对每毫秒的 CPU 施加相同的权重的简单算法是合适的。在某些情况下,这实际上并非如此。例如,如果多处理器计算机上有一个后台 CPU 密集型任务,则与该后台任务关联的 CPU 可能不是很有趣,因为它不会消耗“宝贵”资源,并且不在某些用户操作的关键路径上。因此,如果您正在调查此类应用程序上的 CPU,则需要一种方法来过滤掉此“后台”活动,以便您可以专注于“重要”的 CPU 使用。通常,这很容易做到,因为执行此类后台 CPU 活动的线程专用于后台活动(因此,您只需从这些线程中排除所有示例)。但是,想象一下,如果后台线程是一个“服务”,并且在其上安排了重要的前台 CPU 活动,并与空闲的后台活动交错。这将使分析变得相当困难。


这种糟糕的情况正是您在时间受阻时遇到的情况。   通常,有许多线程将大部分时间都花在阻塞上,而这些阻塞时间的大部分时间从来都不有趣,因为它不是关键路径的一部分。  但是,这些线程至少在某些时候会唤醒,并且它们的执行部分可能位于关键路径上(因此非常有趣)。  不幸的是,没有一种简单、通用的方法可以将“重要”阻塞时间(在关键路径上)与无趣的阻塞时间区分开来,而无需对程序的 INTENT 进行额外的“帮助”(注释)。  因此,进行阻塞时间分析的“诀窍”是使用特定于场景的机制来标记“重要”阻塞时间,并允许它与(大量)不重要的阻塞时间分开。


了解线程时间


PerfView 必须了解挂钟时间或阻塞时间的视图称为线程时间视图。  这种观点基于这样的观察,即在任何时刻,每个线程都在做“某事”。 它可能正在消耗 CPU,也可能不是(我们将它定义为 BLOCKED)。  如果它被阻塞,可能是因为它在等待轮到它使用处理器(我们称之为 READIED),或者它可能正在等待其他东西(例如,等待 DISK 请求响应,或者 NETWORK 响应,或者等待某些同步对象(例如事件、互斥、信号量等)改变状态。 无论它在做什么,都有一个与之关联的堆栈。  因此,在每个时刻,每个线程都有一个堆栈,并且该堆栈可以用一个指标进行标记,该指标表示线程在该调用堆栈中消耗的时钟时间。   这是每个线程在系统上执行的操作的“完美”模型。


如果在收集对话框中设置“线程时间”复选框,或将 /ThreadTime 限定符传递给命令行,则 PerfView 将要求操作系统收集以下信息:


  1. 每毫秒,处理器 (CPU) 正在处理的堆栈(这是不带 /ThreadTime 限定符的当前事件)

  2. 在每个上下文切换(当线程从运行转换到阻塞时)上,开始运行的线程堆栈

  3. 创建或销毁任何线程的时间。


有了这些数据,我们就有了关于我们被封锁的地方的“完美”信息。 我们知道开始阻塞和结束的确切时间,因此可以将正确的时间归因于该特定堆栈。  我们还有花费 CPU 时间的大致信息。   如果我们得到一个样本(可能是 CPU 样本或上下文切换),我们可以将该堆栈归因于自上次采样以来所花费的时间(这又是一个上下文切换(例如,如果线程的 CPU 小于 1 毫秒)或另一个 CPU 样本(例如,如果自上次上下文切换以来超过 1 毫秒)。 因此,上面的事件我们可以很好地详细说明每个线程花费时间的确切位置。  有趣的是,您可以获得有关事物使用多少 CPU 时间的“完美”信息(因为您确切地知道线程何时开始消耗 CPU 时间以及何时停止消耗 CPU)。  唯一的缺陷是与 CPU 关联的堆栈只是一个采样。


上下文切换和 CPU 示例的这种转换是 PerfView 中“线程时间堆栈”视图的基础,也是了解挂钟时间(或阻塞时间)的首选视图。  与 CPU 堆栈视图一样,“线程时间堆栈”视图显示包含的“树”,它聚合了线程花费时间的所有这些堆栈。  在每个堆栈的底部(远离线程开始)端附加一个伪帧,该伪帧指示有关该堆栈的已知信息(CPU_TIME、DISK_TIME、HARD_FAULT(获取映射文件的磁盘时间)、NETWORK_TIME、READIED_TIME或BLOCKED_TIME)。  对于某些已知的东西(例如文件或网络端口,因此也会为它们插入伪帧。   通过这些标记,可以轻松地使用 PerfView 的折叠、分组和筛选功能来仅查看某些延迟原因。


挂钟时间调查


从广义上讲,时钟时间调查包括以下步骤


  1. 使用 Thread Time 事件收集跟踪。这是使用 PerfView Run 或 PerfView Collect 命令完成的,但你需要告诉 PerfView 也通过以下任一方式收集上下文切换信息

    1. 在“数据收集”对话框中设置“ThreadTime”复选框

    2. 将命令行上的 /ThreadTime 限定符传递给 PerfView

  2. 打开生成的 ETW 数据的“线程时间堆栈”视图。

  3. 在单个线程中找到您感兴趣的时间段。这是关键部分,因为您实际上只想查看关键路径上的挂钟时间(或阻塞时间)。执行此操作的技术取决于你的方案。以下是“更简单”案例的一些可能性:

    1. 对于具有同步 I/O 的简单顺序程序(一种非常常见的情况,包括典型的应用程序启动),您只需要找到代表您感兴趣的“工作”的方法。并使用“包含项”(Alt-I) 操作将其缩小到该方法(在单个线程上)。

    2. 对于不使用异步 I/O 的 ASP.NET 应用程序,ASP.NET 线程时间视图会将特定请求的关键路径上的线程片段组合在一起。  因此,在代表请求(或请求组)的框架上使用“包含项”,您只能看到“有趣”的时间。

    3. 如果应用程序使用 System.Threading.Threads.Tasks,则可以使用“线程时间(包含任务)”视图。 这将标记正在执行具有该任务 ID 的单个任务的任务段。 我还将任务的时间归因于激活它的任务的调用堆栈。  通过这种方式,可以像分析单线程顺序程序一样分析并发程序。

    4. 可以使用 System.Diagnostics.Tracing.EventSource 为应用程序中的有趣(通常是小)操作发出事件。 如果这些操作不执行异步 I/O 或以其他方式在另一个线程上生成工作,则可以使用这些事件来查找单个线程的有趣段。 然后,您可以在感兴趣的线程上使用“包含项”,以及“开始”和“结束”时间范围来查找要分析的线程的有趣部分。

  4. 一旦你把你的兴趣缩小到单个线程的时间范围,你就可以继续分析它。  通常,您可以通过切换到“按名称”视图并简单地查看所消耗时间的“类型”(CPU、BLOCKED、HARD_FAULT、READIED、DISK、NETWORK)来执行此操作。 从这里开始,分析很像 CPU 分析。


总而言之,挂钟(或阻塞时间)调查总是从筛选开始,以查找“有趣的”挂钟时间(通常在单个线程上)。 在你到达这一点之前,你无法明智地解释“线程时间视图”,但在你找到有趣的时间之后,它就像 CPU 分析一样进行。


阻塞时间和因果关系 (ReadyThread)


有时,确定阻塞时间的大小和调用堆栈足以了解特定的性能问题。  例如,分析应用程序的冷启动时间就属于这一类,因为了解为什么阻塞时间如此之长是很清楚的(需要磁盘读取),因此唯一的问题是这些操作有多长时间以及发生在哪里(是什么堆栈导致了它们)。   但是,在其他情况下,问题在于理解为什么延迟会如此之长。 例如,如果一个线程在等待锁时被阻塞,那么有趣的问题是,为什么其他线程保持锁的时间这么长? 要回答这个问题,您需要确定哪个线程保持锁定。  像这样的问题就是 ReadyThread 事件帮助回答的问题。


打开 /ThreadTime 事件时,不仅会打开上下文切换事件,还会打开 ReadyThread 事件。  当一个线程导致另一个线程从被阻止更改为可运行(即,使线程准备好运行)时,将触发 ReadyThread 事件。  因此,如果线程 A 正在等待线程 B 拥有的锁,则当线程 B 释放锁时,它会使线程 A 准备好运行。   在此示例中,当 ReadyThread 事件触发时,它会记录线程 A 和 B 以及线程 B 的堆栈。  粗略地说,READYTHREAD 记录了线程 B 导致线程 A 唤醒的事实。


PerfView 有一个特殊的视图,用于显示 READYTHREAD 信息,称为“线程时间(使用 ReadyThread)”视图。  此视图的工作方式与“线程时间”视图类似,但此外,线程阻塞的每个堆栈都会“扩展”,并带有附加帧,这些帧会告诉您唤醒线程的线程和堆栈。  这些额外的框架以“(READIED_BY)”为后缀,因此您知道您可以轻松地看到这些不是普通的框架(如果您愿意,可以将它们折叠起来)。 在线程 A 等待锁并被线程 B 释放锁唤醒的示例中,您会看到


这清楚地表明,在阻塞“X!LockEnter”后,线程被线程 B 调用“X!LockExit”唤醒。


任务如何简化线程时间(线程时间(包含任务)视图)


如果您还没有阅读了解线程时间的基础知识,那么现在应该阅读。本节以这些基础知识为基础。


如果需要执行异步或并行操作,强烈建议使用 .NET System.Threading.Tasks.Task 类来表示异步操作完成后的并行活动或线程的“延续”(C# 中的“await”功能使用 Tasks)。   任务对 PerfView 的价值在于,此类在创建任务 (以及已创建任务的 ID) 、调用任务正文时 (以及任务的 ID) 以及任务的正文完成 (再次与 ID ) 一起记录事件。  这在两个重要方面帮助我们


  1. 任务主体表示真实的用户工作,因此可用于将“重要的阻塞时间”与“无趣的基础结构时间”(这些线程在等待用户工作上花费的时间)分开。  这非常有用。

  2. 任务知道它们在哪里被重新创建(谁“导致”了它们),因此有一种非常自然的方式可以将任务的创建者一直“收费”(或任务使用的其他资源)给创建者。


“线程时间(含任务)”视图正是这样做的。当线程调用任务创建方法时,此视图会在此时插入一个伪帧,指示已调度任务,然后在该点插入该任务正文的所有事件。下面是一个示例


在此示例中,名为“DoWork”的“Main”程序具有代码


此调用会导致另一个线程(在本例中为线程 848 启动,并开始执行正文(委托 {...})。 此“内联委托”代码称为匿名委托,编译的 C# 会为其生成名称(在本例中为“c__DisplayClass5.b__3'),它完成工作(请注意,PerfView 的“转到源”(Alt-D) 选项在这一点上非常方便,可以确切地查看此代码是什么)。


这里重要的部分是,从源代码级别来看,很自然地认为在这个匿名委托上花费的任何成本(时间)都应该“收费”到“DoWork”,因为该代码导致该委托实际运行(在不同的线程上)。 这正是“线程时间(包含任务)”视图的作用。 如果应用程序使用 Tasks,则应使用此视图。


使服务器调查变得简单(线程时间(包含启动-停止任务)视图)


从本质上讲,服务器调查通常与响应时间有关。因此,要进行服务器调查,您希望将导致此响应时间更长的所有成本汇总到显示中。这正是具有启动-停止任务视图的线程时间的作用。


这最好通过示例来说明。这是使用“PerfView /threadTime collect”监视的 ASP.NET Web 服务器的示例。由于我们使用 /ThreadTime 参数,因此会收集有关上下文切换和任务的信息,从而允许显示“线程时间”视图,包括“线程时间(使用 StartStop 任务)”显示。下面是打开此视图并专注于 W3WP 进程(即 Web 服务器进程)的结果。

ThreadTimeWithStartStop


在树的顶部,我们看到了流程节点,但随后所有成本立即被分为两部分,与某些启动-停止活动相关的内容以及其他所有内容。因此,这使您可以快速关注可能感兴趣的线程时间。


在“活动”节点下,您可以看到所有“顶级”启动-停止活动,这些活动按成本(即归因于该活动的线程时间)排序。在上面的视图中,我们打开了“IISRequest”活动(具有特定的 ID 号和 URL),该活动恰好具有 730.7 毫秒的线程时间。此 IISRequest 活动恰好导致 AspNetReq 活动的另一个嵌套的 Start-stop 对,因此显示,从那里显示与 AspNetReq 活动关联的所有堆栈。在此示例中,我们可以通过用户代码看到对 MyOtherAsyncMethod 方法的调用堆栈,该方法执行需要 524.5 毫秒的“await”)


希望您能立即看到此视图的有用性。基本上,它占用了与语义相关事物(某人在代码中检测的启动-停止任务)相关的所有线程时间,并根据因果关系显示堆栈(因此,如果执行跃点线程“跟随”它,则事件)。因此,确切地看到时间花在哪里变得微不足道。


典型的策略是立即选择“(活动)”节点,右键单击 -> Include Item,这将排除所有非活动线程时间。这在大多数情况下效果很好,但请记住,一些重要的成本可能在这个(非活动)节点中,特别是像 GC(在服务器或后台 GC 中)这样的事情,或者任何非线程池线程确实工作但从未记录过启动和停止事件。这就是 PerfView 不会隐藏这一点的原因,但通常你首先查看活动,只有在你被引导到那里时才向外看。通常,如果您要过滤以仅查看非活动而仅查看CPU_TIME,则查看该组中“有趣”的内容。


线程时间不是经过的挂钟时间


需要注意的是,显示的是 STILL 线程时间,而不是挂钟时间。因此,如果存在并发性,则总指标加起来很可能超过经过的挂钟时间。这很容易确定是这种情况(因为您将看到多个线程作为活动的子线程),您甚至可以看到重叠(通过查看每个子线程的“时间”列)。尽管如此,这仍然是需要注意的事情。有关详细信息,请参阅了解线程时间。


线程时间也可能小于经过的挂钟时间。这应该是一个更罕见的情况。当代码导致工作发生,但没有使用已检测的机制来检测另一个线程上的工作是由当前线程引起的时,就会发生这种情况。因此,当前线程可能会返回到线程池 (此时它的时间不再归因于活动) ,但由于 PerfView 不知道另一个线程上的工作,因此它无法正确地将该时间归因于活动 (它最终位于非活动节点) 下。因此,请求的线程时间可能存在“间隙”。PerfView 尝试使用名为“UNKNOWN_ASYNC”的伪节点来填补这些空白,以便视图中的代价永远不会小于用于排序目的的挂钟时间,但有时 PerfView 的算法并不完美。然而,无论哪种情况,都很难确定在这些间隙中发生了什么。希望这根本不会发生在你身上......


创建自己的启动-停止任务


通常,.NET Framework 中的“标准”检测会为您提供良好的“启动”活动(如上面的 IISRequest 和 AspNetReq 所做的那样)。但是,如果这些还不够,您可以定义自己的启动-停止活动。如果代码在 .NET Framework V4.6 或更高版本上运行,则添加将显示在此视图中的新启动-停止活动是微不足道的。有关执行此操作的详细信息,请参阅 EventSource 活动。在收集数据时,您需要使用 /Provider=*YOUR_EVENT_SOURCE_NAME 打开事件,此视图将自动合并它们。



非托管内存分析


PerfView 还可用于执行非托管内存分析。通常,内存调查的第一步(无论是托管内存调查还是非托管内存调查)都是使用免费的 SysInternals vmmap 工具等工具来确定进程的内存构成。该工具可以将当前的内存使用情况分解为六类,包括


  1. 映射的 DLL 和 EXE

  2. .NET 运行时分配的内存(GC 堆)

  3. 由非托管操作系统堆分配的内存(例如 C malloc 或 C++ “new”new 运算符,vmmap 简称为“Heap”)