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”)

  4. 直接使用 Virtual Alloc 分配的内存(在 vmmap 中称为“专用数据”)


根据其中哪一个是大的(因此很有趣,你以不同的方式攻击它。如果映射的 DLL 或 EXE 是问题所在,则需要加载较少的 DLL 或 EXE。PerfView 的“图像加载堆栈”将显示加载 DLL 的位置。如果问题出在 GC 堆上,则需要按照“何时关注 GC 堆”中所述进行 GC 堆调查。如果问题是最后两个问题之一,则本部分将告诉您如何深入了解该问题。


最后,进程中的所有内存要么被映射(例如 DLL 或 EXE),要么由 Windows VirtualAlloc API 分配。 PerfView 允许你通过选中高级收集对话框中的“虚拟分配”复选框,在每个 VirtualAlloc 调用(以及每个 VirtualFree 调用)上收集堆栈跟踪。 VirtualAlloc 旨在用于分配大量数据(实际上最小大小为 64K),因此打开此选项不太可能影响应用的性能,因此请随意执行此操作。  然而,正是因为 VirtualAllocs 很少被调用(通常是当另一个分配器需要更多内存时),所以这些信息通常是“粗略的”,并且仅在用户代码直接调用此 API 时才有用(这是不寻常的)。


更常见的是,您会注意到 VMMAP 中的显示中的“堆”条目很大,因此您需要钻取到操作系统堆中。 为此,我们需要在每次发生操作系统堆分配或空闲时收集数据。 这更为常见。 事实上,它是如此普遍,以至于操作系统没有提供一种在系统范围内打开它的方法(这将是太多的数据),相反,在集合对话框的高级部分有两个对话框。


  1. “OS 堆 Exe”文本框 - 指定 EXE 名称(无路径或扩展名)以打开尚未启动的进程的 OS 堆事件。

  2. “OS 堆进程”文本框 - 指定 EXE 名称或进程 ID 以打开已启动进程的 OS 堆事件。


使用这两种技术之一,您可以为感兴趣的进程打开操作系统堆事件。  (可选)还可以打开 VirtualAlloc 事件。


完成此操作并收集数据后,您将获得以下视图


  1. OS Heap Alloc Stacks 视图(如果要求提供 OS 堆事件)

  2. VirtualAlloc Stacks 视图是否请求 VirtualAlloc 事件。


这两个视图的工作方式相同。跟踪中的每个分配都会被赋予与分配的字节数相等的权重。每个空闲都被赋予负权重和分配的调用堆栈(这样它们就可以完美地“抵消”)。无法与整个跟踪中的分配匹配的释放将被忽略。在此之后,PerfView 将像处理任何其他基于堆栈的数据一样对待堆栈。它只考虑与其筛选器匹配的样本并显示结果。请注意,这意味着 VALUES 可以为负数。如果你选择一个只发生自由的时间愤怒,那么你会得到一个负数。基本的不变性是,视图显示所选范围的 NET 内存分配。由于指标现在可能为负数,因此“时间”列可能需要显示负数。这些字母使用小写字母显示(有关详细信息,请参阅“当列”)。


请注意,这意味着,如果理论上显示程序的总执行量,则应看到值为 0(您释放了分配的所有内容)。 在实践中,这是不正确的,但事实是,您通常对进程终止前使用的 FINAL 内存不感兴趣,而是对 PEAK 内存分配感兴趣。  为此,您需要找到内存分配达到峰值的时间。


您可以通过转到“CallTree View”并选择 When 列作为层次结构的根来执行此操作(大致)。拖动 when 列的区域时,PerfView 将计算拖动区域中的净值和峰值指标。因此,通过拖动,您可以快速确定峰值的位置。通常,您只需要点击“设置范围”(Alt-R),现在您就有了建立到峰值内存使用量的时间区域。


您还可以通过选择任何特定操作的时间范围来轻松调查该操作的净内存使用情况。所有正常的过滤、折叠和分组运算符都有效。对于内存情况。最后,通过打开两个视图,您可以使用 Diff 功能对应用程序的两次运行进行分析。


 目录大小分析


目录大小菜单项将生成一个 *.directorySize.perfView.xml.zip 文件,该文件是目录中所有文件大小的分层总和(递归)。因此,它是一个很好的工具,可以确定什么占用了磁盘驱动器上的磁盘空间并“清理”了价值较低的文件。


选择此菜单项将打开一个目录选择器,用于选择要分析的目录以及将保存收集的数据的文件的名称。选择后,PerfView 将对该目录进行递归扫描,这需要一段时间。当它完成时(对于大型目录可能需要一段时间),它将自动打开它生成的数据文件)。以后可以随时重新打开该文件,只需在 PerfView 的主树视图中单击它即可。


目录大小的“when”字段的工作方式与大多数性能数据略有不同。对于每个数据文件,其“时间戳”是从收集数据到上次修改的天数(可以是小数)。因此,通过选择从 0 到 7 的时间范围,您将看到不到一周前修改的所有文件。此信息对于查看数据的“旧”程度非常有用(这通常有助于确定是否保留数据)。

 图像尺寸分析

 收集数据


选择“大小 -> 图像大小”菜单项将打开一个对话框,用于指定 DLL 或 EXE 来执行大小分析。此外,它还允许您设置保存结果数据的输出文件的名称。该对话框将从输入文件名派生输出文件名,通常此默认值很好。

 分析数据


图像大小菜单项将生成一个 .imagesize.xml 文件,该文件描述了 DLL 或 EXE 文件的大小细分。它通过在其 PDB 文件中查找 DLL/EXE 的每个符号并为文件的每个块使用这些名称来实现此目的。它还查找从文件的一部分到另一个部分的引用(例如,内存 blob 中的指针或指向其他内存 blob 或汇编代码的汇编代码)。由于这些引用可以形成任意依赖关系图,就像 GC 堆对象形成依赖关系图一样,因此 PerfView 以与 GC 堆非常相似的方式显示此数据。与 GC 堆一样,“When”、“First”和“Last”列不显示时间,而是表示加载时特定项在虚拟地址空间中的位置的地址。因此,您还可以使用它来了解加载时文件中不同符号的位置。

 展平跟踪


如前所述,默认情况下,PerfView 会尝试在 DLL 中创建项的“GC 堆”,如果一个项引用另一个项,它将具有从引用方到被引用对象的链接。但是,此行为可能会干扰某些分析。.特别是,如果您使用“include pats”或“exclude pats”文本框,它将包含或排除 ON THE WHOLE PATH。当这不是你想要的时,解决问题的一个简单方法是“展平”图形。


展平一组节点需要一组节点,并返回一个新的“GC 堆”,其中


因此,如果您进入“RefTree”视图,选择与“ROOT”节点关联的指标,单击鼠标右键,然后选择“展平”,您将获得一个新视图,其中节点之间没有链接。现在,“include pats”和“exclude pats”将根据 ONLY THAT NAME(而不是其任何父节点的名称)选择一个节点。


图像尺寸分析中某些标签的含义


图像大小报告中使用的许多名称都是符号名称,这些符号名称与源代码中的名称有直接关系。但是,其他名称描述可移植可执行文件 (PE) 格式的实体,这些实体需要准备要运行的 DLL/EXE 中的代码/数据。在这里,我们描述了其中一些可能在输出中突出显示的内容。


其他名称与 .NET 运行时本机文件格式相关联。

 IL尺寸分析

 收集数据


通过选择“大小 ->IL 大小”菜单项,可以对 .NET 中间文件 (IL) 中的内容进行分析,这是 .NET 编译器(如 C# 和 VB)创建的内容。它将生成一个 .gcdump 文件,该文件在 IL 文件中生成类型、方法、字段和其他结构的图形,其中图形的每个节点都表示它在文件中的大小,节点之间的弧线是从一项到另一项的引用。因此,您可以像 GC 堆中的对象一样进行依赖关系分析(哪些事物指的是其他事物)。


“Size -> IL Size”菜单项将打开一个对话框,用于指定要对其执行大小分析的 DLL 或 EXE。此文件必须是包含 .NET IL 的 DLL 或 EXE(例如 .NET 编译器的输出)。此外,它还允许您设置保存结果数据的输出文件的名称。该对话框将从输入文件名派生输出文件名,通常此默认值很好。

 分析数据


图像大小菜单项将生成一个 .gcdump 文件,该文件描述了 IL 文件中类型、方法、字段和其他项目的细分。它的工作方式与 GC 堆分析或本机图像大小分析大致相同。

 多文件堆


菜单项仅允许您在为 IL 代码创建节点弧图时指定一个 IL 文件。此文件之外的任何引用都不会遍历,而只是标记为特殊的“外部引用”节点。有时选择一组 IL 文件(例如,代表一个完整的应用程序)会很有用,这些文件会被遍历,只有当您离开该组时,您才会使用“外部引用”节点。您可以使用“ILSize.ILSize”用户命令执行此操作。因此,命令


将创建一个包含 File1.dll、File2.dll 和 File3.dll 的 GC 堆,就好像它们是一个文件一样。


多场景分析(聚合跟踪)


通常,跨多个跟踪分析一个程序的性能很有用。这些跟踪可能表示各种方案中的一个大型项目,或者表示多个程序正在使用的公共库的行为。PerfView 支持用于此类多方案分析的多种功能。


同时分析多个场景(数据文件)时,一个主要挑战就是要处理的数据量。  单个场景通常可以有一个 100 兆字节的 ETL 文件,如果您有 100 个这样的场景,您现在正在谈论要处理的 10-100 GB 的信息。 因此,该过程旨在尽快减少数据量并保持这种“精益”形式,以便控制查看时的数据量。  因此,使用多个多个方案有两个主要步骤


  1. 对于每个 .ETL(或 .ETL.ZIP 文件),创建一个新文件(.PERFVIEW.XML.ZIP 文件),该文件仅包含查看 PerfView 堆栈查看器中的数据所需的信息。  这会将数据量减少 100 倍或更多。  此步骤可以“离线”完成,一旦完成,在新数据传入之前不需要重复。  该工具是“智能”的,因为如果将新的输入文件添加到一组现有的数据文件中,它会跳过已经转换的文件。  对于实际转换的每个数据文件,此过程需要几秒钟到 10 秒。  如果方案中有重要的非托管 DLL,则必须在其阶段正确设置 PDB 符号路径(例如 _NT_SYMBOL_PATH)。 转换为 XML.ZIP 后,将无法再解析符号。

  2. 一种新的查看文件(.SCENARIOSET.XML 文件),表示一组 PERFVIEW.XML.ZIP 文件的聚合。 打开此类型的文件时,PerfView 将同时显示所有数据文件中的数据。  您可以生成其中许多文件,以形成相同数据文件的不同子集。 当 PerfView 打开这些文件时,每个数据文件都会被赋予一个表示数据的“顶部节点”(在“进程节点”上方)。  然后,可以将 PerfView 的标准分组技术用于感兴趣的区域(例如,特定库或函数在所有方案中的使用量,或者在所有方案中“平均”花费 CPU 时间的位置)。  此外,PerfView 还具有特殊功能(“哪一列”),可帮助你快速了解哪些方案对任何特定指标有影响。  发现“热点”区域后,可以使用“哪一列”来了解问题在不同场景中的均匀分布情况。


以下是有关执行这些步骤的更详细说明。


步骤 1:预处理 ETL 数据并形成表示所有数据文件的 ScenarioSet


同时查看多个数据文件的第一步是将数据预处理为“场景集”。您可以使用“SaveScenarioCPUStacks”用户命令执行此操作(目前仅支持 CPU 采样聚合)。您可以使用“File->UserCommand”菜单项从 PerfView GUI 运行它,也可以通过执行以下命令从命令行运行它


SaveScenarioCPUStacks 命令采用一个参数。此参数可以是目录名称(如上例所示),也可以是 XML 配置文件的路径。


如果传入目录, SaveScenarioCPUStacks 将以“自动”模式运行。它将处理在目录(或任何子目录)中找到的所有 ETL 和 ETL.ZIP 文件,使用启发式方法自动检测跟踪感兴趣的进程。用于选择感兴趣过程的启发式方法是


  1. 如果跟踪包含 Win8 应用商店应用,则选择第一个 Windows 应用商店应用。

  2. 如果没有 Windows 应用商店应用,则第一个要启动的可执行文件将运行超过跟踪长度的一半(这往往会忽略安装脚本)。

  3. 如果没有匹配的应用 (2),则跟踪启动后启动的第一个应用。


通常,这种启发式方法效果很好,但是,如果您需要控制运行方式 SaveScenarioCPUStacks ,则可以传入一个 XML 配置文件,以便您可以很好地控制 ETL 文件的处理。下面是一个示例 XML 配置文件:



如您所见,配置文件由根元素组成,根 ScenarioConfig 元素包含一个或多个 Scenarios 元素。每个 Scenarios 元素都设置了属性,用于控制方案的处理方式:


运行 SaveScenarioCPUStacks 命令的结果是以下输出文件。


如果您愿意,还可以生成自己的 scenarioSet.xml 文件。scenarioSet 文件类似于方案配置文件,但属性略有不同。下面是一个示例 scenarioSet 文件:



如您所见,它基本上是一个文件模式列表(指示应包含 ScenarioSet.xml 文件的目录(或任何子目录)中的哪些文件),以及允许您获取该文件名并将其转换为方案名称的模式。   您可以创建自己的 XML 文件,以创建某些数据的有趣子集。


步骤二:查看多个场景


处理方案数据后,可以继续查看它。为此,请使用主视图中的树视图浏览到生成的 scenarioSet.xml 数据文件,然后双击将其打开。


在大多数情况下,这是您在单个 ETL 文件上使用的熟悉的堆栈查看器,主要区别在于来自特定数据文件(场景)的每个堆栈在最顶部都有一个新的伪帧,用于标识示例来自的场景。  因此,堆栈属于线程,属于进程,属于场景。  堆栈查看器的其他所有内容都与在单场景情况下一样工作。 堆栈视图看起来就像同一台计算机上同时显示每个方案一样。


除了每个堆栈的新“顶部”节点外,查看器还具有一些仅在多场景情况下可见的增强功能。  您将看到:


就像“when”列允许您为视图中的每一行看到一个小图形一样,该小图形在时间上将样本显示为函数(直方图),而“which”则向您显示具有样本参与该行的方案的直方图。  因此,您可以快速确定该行的成本是否在方案中均匀分布,或者是否只有少数方案对成本有贡献。


which 字段具有许多与之关联的方便功能。


 合并


如果打算将使用 PerfView 收集的数据传输到另一台计算机,则需要执行称为合并的附加步骤。


PerfView 使用内置于 Windows 中的 Windows 事件跟踪 (ETW)Windows (ETW) 工具来收集分析信息。此基础结构不会自然地为数据创建单个文件,而是将来自 OS 内核的数据与其他事件隔离开来。因此,生成的“原始”数据由两个文件组成(一个只是 etl,另一个是 .kernel.etl)。此外,这些文件缺少在另一台计算机上完全解码文件所需的一些信息(最值得注意的是,操作系统内核名称到 NTFS 文件名的映射以及允许明确查找符号信息 (PDB) 的符号服务器“密钥”)。如果您在收集数据的同一台计算机上使用数据,则这些限制都不是问题,但如果您希望将其传输到另一台计算机,则应首先合并数据。


合并是将 .kernel.etl 合并到主 .etl 文件中的过程。  此外,还收集了缺少的特定于系统的信息,并将其放置在 .etl 文件中。 结果是可以将单个文件复制到另一台计算机进行分析。  此过程可能需要相当长的时间(10 秒),这就是 PerfView 默认不执行此操作的原因。   您可以通过以下方式执行合并


合并文件后,您只需将单个文件复制到另一台机器上即可进行“离线”分析。   但请注意,虽然 ETL 文件包含 .NET 运行时代码的符号信息,但它不包含非托管代码的符号信息。  因此,如果查看非托管代码的符号名称很重要,则需要确保进行分析的计算机有权访问包含此信息的 PDB 文件。



NGen Pdbs(和压缩)


合并在收集数据的计算机以外的计算机上查看 ETL 文件所需的操作。但是,这并非适用于所有情况。虽然生成的合并文件包含查找符号信息(用于堆栈跟踪)的所有信息,但它并不能保证符号信息可用。具体而言,在收集其进程使用 .NET 运行时的跟踪时,必须引用托管代码(如果是 NGENed)的本机代码映像(NGEN 映像)的符号信息(PDB 文件)。这些 NGEN Pdb 不是 IL 映像的 PDB 文件(由 CSC.exe 或 VBC.exe 等 IL 编译器创建的内容)。NGEN PDB 由 .NET Framework 附带的 NGen.exe 命令生成,并且只能在生成 NGEN 映像的计算机上可靠地生成。


作为 ZIPPing 过程的一部分,PerfView 将查找 ETL 文件中的所有地址,并确定使用了哪些 NGEN 映像,并在必要时为这些映像生成 PDB 文件。然后,它会将 ETL 文件以及任何 NGEN PDB 压缩到一个 ZIP 文件中,现在可以在任何计算机上查看该文件(PerfView 知道如何自动解压缩这些文件)。



从命令行收集数据(脚本、自动化)


另请参阅通过生成 PerfView 扩展来实现高级自动化的 PerfView 扩展。


另请参阅命令行参考,以获取可在命令行中使用的选项的完整列表


PerfView 旨在使你能够使用批处理文件或其他脚本自动收集配置文件数据。三种可能的情况是:


  1. 用户只想从命令行快速收集数据,以便在同一台机器或另一台机器上进行即时分析。

  2. 用户想要制作一个简单的脚本来自动收集数据,但在收集过程中仍然需要在场(例如,手动测试 GUI 应用程序),但不希望立即分析数据(其他人会这样做)。

  3. 数据收集是完全自动化的,完全不受监控的收集。


在第一种情况下,您可能希望使用“run”或“collect”命令


“run”命令会立即运行该命令并启动堆栈查看器。  如果程序易于启动并且可以运行完成,则这是首选选项。  但是,有时很难做到这一点(应用程序是服务的一部分,或者由复杂的脚本激活),然后您可以使用“collect”命令启动系统范围的收集。


跳过 Rundown (/NoRundown)


默认情况下,“collect”命令执行“rundown”,其中要正确解码在分析之前收集的符号信息的信息将停止。  此操作可能相对昂贵(只需几秒钟,并将文件大小增加 10 秒 Meg)。   当进程关闭时,自然会提供此信息,但“collect”命令不知道您是否关闭了感兴趣的进程,因此它会执行纲要。    如果您知道感兴趣的进程已退出,则 rundown 毫无意义,可以通过指定 /NoRundown 限定符来避免。  此选项可以节省时间和文件大小。


禁止查看 (/NoView)


默认情况下,PerfView 假定你希望立即查看收集的数据,但如果收集数据的人(例如测试人员)不是分析数据的人(例如开发人员),则我们希望禁止查看者。  这就是 /noView 限定符的作用,它适用于 'collect' 和 'run' 命令。 因此


将打开日志记录并运行给定的命令。它还将合并该文件,假设该文件可能会从当前系统中移出。但是,它仍然会调出 GUI,并且在完成后不会自动退出(以便用户可以对任何故障或消息做出反应,并且是“collect”命令所必需的,以便用户可以指示何时应停止收集)。


自动收集 (/LogFile:FileName)


另请参阅命令行参考,以获取可在命令行中使用的选项的完整列表


/NoView 在难以完全自动化数据收集(在 GUI 应用中测量临时方案)的地方很有意义。  但是,对于全自动收集,您根本不需要 GUI。 这就是 /LogFile 限定符的用途。  通过指定此限定符,可以指示不应打开 GUI,并且程序应在命令行上运行命令后退出。  本应在 GUI 中报告的任何错误消息都会附加到日志文件中(我们附加,以便你可以将同一文件用于多个 PerfView 命令。  PerfView 进程的退出代码将指示收集是成功还是失败,日志文件将包含详细的诊断消息。


请注意,/LogFile 限定符将禁止显示 GUI,但如果指定了“Collect”命令且未提供 /MaxCollectSec 限定符,则不会禁止生成控制台。原因是如果没有 /MaxCollectSec=XXX,Collect 命令可能会永远运行,并且您将无法完全停止它(您必须终止该进程)。如果要使用 /LogFile 和 Collect(因为希望使用 /StopOn* 限定符),并希望禁止显示任何控制台,则可以通过指定一个非常大的 /MaxCollectSec 值来实现此目的。


除了 /logFile 限定符之外,最好还将 /AcceptEula 限定符应用于调用 PerfView 的脚本。默认情况下,首次在任何特定计算机上运行 PerfView 时,它会显示一个弹出窗口,要求用户接受使用协议 (EULA) 。这对脚本来说可能是个问题,因为它需要人工交互。若要避免这种情况,可以在以无提示方式执行此操作的命令行上使用 /AcceptEula 限定符。


因此,/logFile 和 /AcceptEula 限定符的典型用法是命令


从脚本(无 GUI)运行“教程.exe”。  如果需要在系统范围内收集(要使用“collect”而不是“run”),则存在问题,因为 PerfView 不知道何时停止。 有两种方法可以解决这个问题。 第一种是使用“/MaxCollectSec”限定符。 例如,以下命令将收集 10 秒,然后退出。


如果您希望通过时间限制以外的其他方式控制停止,您还可以使用“开始”和“停止”和“中止”命令。


这些旨在在脚本中使用。第一个将开始记录,即使在程序退出后也保持打开状态。第二个停止日志记录。如果可以,应避免使用它们(请改用 collect /MaxCollectSec)。原因是,如果脚本在启动和停止命令之间失败,日志记录可能不会停止,并且将“永远”运行。因此,在使用这些产品时需要格外小心。“abort”命令旨在帮助确保 PerfView 不会进行日志记录。它应该在你知道 PerfView 不应运行的位置调用,并确保它确实没有运行。您应该在使用“start”命令的脚本中自由使用它。


最小化收集对系统的影响 (/LowPriority)


正常的 Windows 事件跟踪 (ETW) 日志记录通常非常有效 (通常< 3%) 但是,在跟踪完成后,PerfView 通常会执行相对昂贵的操作来打包数据 (包括合并、NGEN 符号创建和 ZIP 压缩) 。显然,这些操作可能会使用可能会减慢机器上运行的任何其他操作的资源。


如果在命令行上将 /LowPriority 选项传递给 PerfView,则 PerfView 将以较低的 CPU 优先级执行这些操作。这可以显著减慢打包数据所需的时间,但可以最大程度地减少对系统的影响。



在 Windows Server (Docker) 容器中使用 PerfView


最好将容器视为轻量级虚拟机。有关 Windows 容器的更多背景信息,请参阅 Windows 10 上的 Windows 容器。特别是,Windows 支持称为“Windows Server 容器”的轻量级容器,其中内核在计算机上运行的所有容器之间共享。此类容器与名为 Docker 的工具结合使用,该工具允许您在虚拟化环境中创建操作系统映像并运行应用程序。


理想情况下,容器应该与使用 PerfView 无关,因为容器是一种 Windows 操作系统,而 PerfView 只是运行在那里的 Windows 应用程序。这基本上是正确的,但有一些差异需要考虑。


  1. 由于容器共享内核,并且 PerfView 所依赖的 ETW 事件由内核生成,因此需要操作系统中的特殊支持来“虚拟化”事件并将其转发到相应容器中的 ETW 会话。此支持已添加到操作系统的 RedStone (RS) 3 版本(也称为版本 1709,10/2017 发布)中。命令“cmd -c ver”将告诉您当前运行的操作系统的 BUILD 版本,Windows 10 版本历史记录页面可以将其与您的 Windows 10 版本相关联。请注意,从该版本开始,仅支持 CPU 和上下文切换事件。但这足以做很多有用的分析。

  2. 容器没有 GUI,PerfView 是一个 GUI 应用。这意味着,如果从容器中的命令提示符运行 PerfView,它似乎不执行任何操作。它所做的是启动您看不到的 GUI,并从当前控制台分离。因此,它正在做它一直做的事情,只是在容器中没有那么有用。但是,PerfView 支持强大的命令行选项来自动收集,这些选项在容器中工作正常。


因此,PerfView 在容器中工作,但需要确保你拥有足够新的操作系统版本,并且使用自动收集中的技术在不使用 GUI 的情况下收集数据。

 容器使用示例


一个例子胜过一千个解释,所以这里有一个例子。首先,您需要从 Web 设置安装适用于 Windows 的 Docker。网上有很多很好的教程。设置 docker 后,您可以执行以下操作


  1. docker pull microsoft/windowsservercore:1803 cmd


这将拉下 Windows Server Core 的 1803 版本(约为 5GB)并在其中运行“cmd”命令。显然,您也可以拉下更高版本(1803 是 RS-4 版本,于 2018 年 4 月发布)。重要的是它是 RS-3 或更高版本。结果是 C> 命令提示符。


此时,可以将 PerfView 复制到容器中(例如,'net use \\SomeShare\SomeSpot)。复制 PerfView 后,可以执行以下操作

  1. PerfView /logFile=log.txt /maxCollectSec=30 collect


这将导致 PerfView 与控制台断开连接,并将任何诊断记录到 out.txt。最终,此命令将以正常方式创建 PerfViewData.etl 文件。您可以执行“键入 log.txt”来查看运行过程中的进展情况。如果将此命令放在批处理文件中,则它不会与控制台分离,因此在收集完成之前,批处理文件不会继续。因此,可以创建一个调用 PerfView 的批处理文件,然后将生成的文件复制到某个位置。如果希望让批处理文件开始收集,在监视时启动某些操作,然后停止它,还可以使用“start”和“stop”PerfView 命令而不是“collect”命令。关键是它的工作方式与普通窗口一样,PerfView 非常灵活。你几乎可以做任何事情。


Windows Nanoserver 和 PerfViewCollect


windowsservercore docker 映像是 Windows 的一个相当完整的版本。特别是,它有一个完整的 .NET 运行时,这是 PerfView 需要运行的。Microsoft还支持更小的Windows的Docker映像,称为microsoft/nanoserver(300 MB,而不是5GB)。此操作系统支持 ETW,因此理论上可以在其上收集 PerfView 数据,但它没有桌面运行时,因此 PerfView.exe 工具本身无法运行。这就是“PerfViewCollect”工具的用途。


PerfViewCollect 是 PerfView 的一个版本,它已剥离其 GUI(它仅执行收集),并使用 .NET Core 运行时生成。生成 .NET Core 应用程序时,可以将其生成为独立应用程序,这意味着应用程序附带运行它所需的所有 .NET 运行时和框架 DLL。因此,您只需要基本的操作系统功能,特别是它将在 NanoServer 上运行。


目前,我们不会创建 PerfViewCollect 的二进制分发,它必须从 https://github.com/Microsoft/perfview 的源代码生成。若要生成,但是不需要 Visual Studio,只需要 .NET Core SDK,因此该过程是


最后一个命令会将 PerfViewCollect 应用程序生成为独立应用程序。该工具会告诉你它的位置,但它应该位于 src\PerfViewCollect\bin\Release\netcoreapp3.1\win-x64\publish 中。该工具是该目录中的 PerfViewCollect.exe。可以执行 PerfViewCollect /?获取一些帮助(但它将与 PerfView.exe 的命令行帮助完全相同)。


如果将此目录复制到纳米服务器,则也应该能够在那里运行 PerfViewCollect.exe,因此可以执行命令


在 Window nanoserver 上收集数据。


已知问题(在 Windows 版本 1803 或更早版本中)


自 2018 年 10 月(或更早版本)起存在已知问题。基本上,问题在于作为容器中操作系统一部分的DLL(例如内核,ntdll,kernelbase等)最终使用HOST路径而不是CONTAINER路径。这没什么大不了的,除了 DLL 加载事件不包含用于在 Microsoft 符号服务器上查找 DLL 的符号文件的特殊唯一标识符。通常,作为准备(合并)要从系统复制的文件的一部分,这些唯一 ID 将添加到跟踪中。但是,由于这是在容器中完成的,并且事件具有 HOST 路径,因此执行此操作的逻辑将失败,因此 system.DLL 没有唯一 ID。这意味着 PerfView 无法查找符号名称。


有一个解决方法。如果获取正确的符号文件 (PDB) 并将其放在目录中,并使用“文件->设置符号路径”来包含此目录,并将 /UnsafePDBMatch 选项传递给 PerfView,则它应该可以正常工作。


有多种方法可以获取正确的符号文件,但一种方法是在容器中使用调试器,并要求调试器加载必要的系统文件。然后转到调试器放置它们的位置。


 生产监控


另请参阅命令行参考,以获取可在命令行中使用的选项的完整列表


PerfView 具有一些功能,这些功能专门用于收集有关生产工作负载的数据,以诊断仅在实际负载下发生的性能问题。我们已经看到了 /noView 选项,该选项指示在数据收集完成后,PerfView 应直接退出 (而不是尝试显示数据) 。还有其他几个有用的命令行选项可用于生产监视。首先是 /MaxCollectSec:N 限定符。命令


将指示 PerfView 最多应收集 20 秒。因此,此命令不需要用户交互即可收集数据样本。由于还提供了 /logFile 选项,因此有关集合的任何诊断信息都将发送到“collectionLog.txt”。因此,这可以通过单个命令行命令在服务器计算机上完全自动收集数据。


使用性能计数器触发收集停止 (停止触发限定符)


/MaxCollectSec 限定符可用于立即收集示例。但是,服务器遇到间歇性性能问题(例如,高 CPU 或高 GC 使用率等)的情况并不少见。因此,需要的是能够监控服务器,并且仅在发生“有趣”的事情时捕获样本。这就是 /StopOnPerfCounter 选项的用途。/StopOnPerfCounter 限定符的基本语法是


其中 CATEGORY:COUNTERNAME:INSTANCE 指示特定的性能计数器(遵循 PerfMon 使用的相同命名约定),OP 是 < 或 >,NUM 是数字。例如


指示 PerfView 应收集数据,直到“.NET CLR 内存”类别的“GC 中时间百分比”的 _Global_ 实例(表示系统上所有进程的所有 GC 堆的总和)大于 20%。因此,当 GC 时间较长时,将触发此规范。默认情况下,“collect”在“循环缓冲区模式”下运行,默认大小为 500MB。因此,上面的命令将只收集 500MB 的数据(通常是几分钟的数据),然后开始丢弃最旧的数据。当性能计数器触发时,命令将停止,您将拥有导致“性能不佳”的最后几分钟数据(在本例中为 GC 时间过长)。


某些计数器(如系统全局计数器“Memory:Committed Bytes”)没有实例,因为整个计算机只有一个实例。对于这些,请指定一个空字符串。例如


当整台计算机的提交字节数超过 50GB 时,将停止收集。请注意,计数器仍为 CATEGORY:NAME:INSTANCE,但在本例中,INSTANCE 为空字符串(尾随:)。


当 PerfView 检测到计数器满足条件一定秒数(默认为 3 秒)时,将触发性能计数器。可以使用标志 /MinSecForTrigger:N 进行控制,将阈值设置为 N 秒。


当性能计数器触发时,PerfView 实际上会在停止之前再收集 10 秒的跟踪。这样,您就可以在感兴趣的事件发生之前和之后获得条件。当触发性能计数器时,PerfView 会将名为 StopReason 的事件记录到 ETW 事件流中,以便你可以在查看数据时准确查看发生此事件的时间。


若要查找要在 /StopOnPerfCounter' 限定符中使用的性能计数器的确切名称,可以使用 Windows 中内置的 PerfMon 实用工具。若要启动它,只需在命令行中键入“start PerfMon”。然后单击左侧窗格中的“性能监视器”图标。这带来了右手疼痛中的性能计数器图。您可以单击顶部的 + 图标来添加新的性能计数器。这将打开“添加计数器”对话框,其中填充了性能计数器类别。例如,可以打开“.NET CLR 内存”类别,将看到“所有堆中的 # 字节数”和“GC 中的时间百分比”等计数器。然后选择其中一个计数器将显示具有这些计数器的所有实例(进程)。这三个名称(类别、计数器、实例)是需要为 '/StopOnPerfCounter 限定符指定的值。


在等待很长时间之前,您需要测试 /StopOn* 规范,以查看它是否正确捕获跟踪。如果打开日志(或使用 /MaxCollectSec=XXX 强制其快速停止,然后查看 /LogFile 指定的文件,或在“*.etl.zip”的“TraceInfo 视图”中查找此捕获的日志文件),则在监视性能计数器时会找到诊断消息。您应该会看到显示它设置性能计数器的消息以及它每隔几秒钟看到的值。这可以让您确信您没有拼错计数器,您拥有正确的实例,并且您选择了合理的阈值。


可以多次指定 /StopOnPerfCounter 限定符,每个限定符都充当触发器。因此,您将获得所有触发器的逻辑“OR”(其中任何一个都会导致跟踪停止)。目前无法指定逻辑“AND”。


如果要监视的进程存在很长时间,则可以在 /StopOnPerfCounter 限定符中指定该进程的实例。但是,有时很难确定所需的流程实例。某些计数器(如 GC 计数器和其他计数器)具有以某种方式表示“所有”进程的特殊实例。在 PerfMon 的“实例”列表框中查找这些实例。这些可以派上用场。如果没有聚合实例,则可以对可能存在的每个进程实例进行 /StopOnPerfCounter。这并不难做到,因为性能计数器的名称包括 EXE、EXE#1、EXE#2 等。因此,可以为 N 中的每一个指定 /StopOnPerfCounter,从 1 到预期的最大实例数。PerfView 对不存在的实例很可靠 (它等待它们存在) ,因此你可以获得所需的行为。


下面是一些其他有用的 /StopOnPerfCounter 示例


监视 ETL 文件中的性能计数器。


将性能计数器数据记录到 ETL 文件通常很有用,以便可以将性能计数器中的数据与其他 ETW 数据相关联。这就是 /MonitorPerfCounter=spec 限定符的作用。它的格式为 CATEGORY:COUNTERNAME:INSTANCE@NUM其中 CATEGORY:COUNTERNAME:INSTANCE,标识性能计数器(与 PerfMon 相同),NUM 是表示秒的数字。@NUM部分是可选的,默认为 2。在收集数据时,可以有几个这样的限定符。性能计数器的值将作为每 NUM 秒的事件记录到 ETL 文件中。因此


此命令每 10 秒记录一次 Available MBytes 性能计数器。此数据显示在 PerfView/PerformanceCounterUpdate 事件下的“事件”视图中。监视服务器的 RPS 负载或内存使用情况通常很有用。


使用日志 HTTP 请求作为停止的触发器


一个相当常见的情况是,你有一个 Web 服务,并且你有兴趣调查响应时间较长的情况。但是,大多数时候响应时间都很好。因此,简单地收集样本不太可能有用。您需要的是作为“飞行记录器”运行,直到发生长时间的请求,然后停止。这就是 /StopOnRequestOverMSec 限定符的作用。命令


当 IIS(例如 ASP.NET)请求花费的时间超过 2000 毫秒时将停止。还可以添加 /CollectMultiple:N 选项,以便收集其中的 N(文件名经过变形以添加 .1、.2 ....)。


最后,还可以使 PerfView 在将消息写入 Windows 应用程序事件日志时停止。因此,命令:


当消息写入与 .NET 正则表达式模式“Pattern”匹配的 Windows 事件日志时,将停止。默认情况下,PerfView 监视应用程序事件日志,但如果要监视另一个事件日志,可以通过在“Pattern”前面加上事件日志的名称后跟 @ 来实现。


使用长 .NET GC 作为停止的触发器


另一个相当常见的情况是,你有一些非基于 HTTP 的服务正在经历暂停时间,并且你有一个大型 .NET 堆。  使用 /gccollectOnly 选项进行收集,您可以进行很长的跟踪(数小时到数天),并且确实发现不时发生长 GC,但只是偶尔发生。  这些长 GC 是阻塞的,因此可能是暂停时间过长的原因,您希望获得有关长 GC 的详细信息。   这就是 /StopOnGCOverMSec 限定符的作用。命令


将收集详细信息,这些信息将在任何超过 5 秒的 GC 之前捕获大约 2 分钟的详细信息。  此详细信息包括有关上下文开关(/ThreadTime 限定符)的信息,并将在关闭之前为 3 个单独的长 GC 收集最多三个单独的文件(命名为默认值:PerfViewData.etl.zip、PerfViewData.1.etl.zip 和 PerfViewData.2.etl.zip)。


使用异常触发止损


另一种常见情况是在引发异常后触发停止。这使您可以查看异常发生之前发生的情况。还可以匹配名称异常或引发的异常中的文本。例如


每当从 MyService 进程中引发具有“ApplicationException”的异常时,将停止(请注意,/Process 会选择具有给定名称的第一个进程作为焦点,而不是具有该名称的所有进程)。/StopOnException 的模式参数可以是任何 .NET 正则表达式。


每当类型中包含“FileNotFound”且消息文本中某处包含“Foo.dll”的异常时,将停止。请注意,您可以在模式中使用 .NET 正则表达式 .*。您可以使用 .Net 正则表达式的全部功能。


收集问题的多个实例


默认情况下,当给出任何 /Stop* 参数时,PerfView 将在触发器触发后停止并退出。在一次会话中收集问题的多个实例通常很有用,这就是 /CollectMuliple:N 限定符的作用。例如


只会对超过 5000 个 ASP.NET 请求触发,但是一旦触发,它将返回并恢复监视,直到创建 3 个此类示例。因此,上述命令最多将生成 3 个文件。结果 .ETL.ZIP 文件在 前面有一个数字。ETL.ZIP 后缀,使文件名唯一。


将触发器限制为特定进程


默认情况下,当 ANY 进程满足触发器时,将触发 /StopOn*OverMsec 和 /StopOnException。在运行许多服务的服务器上,如果您只对特定进程感兴趣,则可能会导致错误触发器。这就是 /Process:processNameOrID 限定符的用途。例如


仅当 ID 为 3543 的进程的 Web 请求超过 5000 毫秒时才会触发。您也可以为筛选器使用进程名称(不带路径或扩展名的 exe),但此名称仅用于查找具有该名称的第一个进程。因此,如果在启动收集时有多个进程具有该名称,则选取的确切进程实际上是随机的。因此,您需要对现有进程使用数字 ID,除非进程名称在系统上是唯一的。在收集开始后启动的进程可以明确使用该名称。


使用 /DecayToZeroHours:XX 选项


使用 /StopOn*Over 或 /StopOnPerfCounter 时可能遇到的一个问题是选择一个好的阈值数。 选择过高的数字将意味着触发器永远不会触发。 选择的数字太低会导致它触发无趣的情况。  这就是 /DecayToZeroHours 选项的用途。 基本思想是将触发器设置为一个数字,该数字位于您认为可能的上限范围内。 还可以将 /DecayToZeroHours:XX 设置为“long”值(通常为 24 小时。  通过指定此选项,您已经指示原始触发值应在这段时间内缓慢衰减为零。 因此,命令


将从 5000 毫秒的停止阈值开始,但它的衰减速度使其在 24 小时内达到零。 因此,在 12 小时内,它将达到 2500 毫秒。 因此,在这段时间内,触发器最终会变得足够小,可以触发,但很有可能它会在“相当大”的情况下触发。


将收集详细信息,这些信息将在任何超过 5 秒的 GC 之前捕获大约 2 分钟的详细信息。  此详细信息包括有关上下文开关(/ThreadTime 限定符)的信息,并将在关闭之前为 3 个单独的长 GC 收集最多三个单独的文件(命名为默认值:PerfViewData.etl.zip、PerfViewData.1.etl.zip 和 PerfViewData.2.etl.zip)。


使用 /StopOn* 选项收集时进行日志记录


当 /StopOn* 触发器选项处于活动状态时,PerfView 将以 10 秒为间隔将日志记录到 PerfView 日志以及有关平均请求和最大请求的 ETL 文件消息中。 在进行数据收集时,您可以通过单击主窗口上的“日志”按钮来查看这些日志(即使收集对话框已打开)。 它们也将位于 ETL 文件中,可以通过筛选到“PerfView/PerfViewLog”事件在“事件”视图中查看。  这些有助于更多地了解最大值如何随时间变化。


在停止触发器触发后捕获更多数据


触发 /StopOn* 触发器后,默认情况下,PerfView 会等待 5 秒,然后停止跟踪。这可确保您不仅看到触发器之前的时间段,还看到触发器之后的 5 秒。这对于大多数方案来说已经足够了,但如果需要更多,可以使用 /DelayAfterTriggerSec=N 指定更长的时间段。但请记住,通常默认的 500Meg 循环缓冲区将仅保存 2-3 分钟的跟踪,因此指定大于 100-200 秒的数字可能会允许触发前的时间段被新数据覆盖。


在停止触发器触发时执行外部命令。


在某些情况下,还会收集其他日志记录以及 PerfView 数据。当 PerfView 触发停止时,执行停止此日志记录的命令很有用。这就是 /StopCommand 的用途。该参数可以使用变量名称 %OUTPUTDIR% 或 %OUTPUTBASENAME% 来表示目录和要传递给外部命令的基名称(不带目录或文件扩展名的文件名)。


在任意 ETW 事件或任意启动-停止对上停止


/StopOnRequestOverMSec 用于测量 IIS 启动和 IIS 停止事件之间的持续时间。许多服务使用 IIS 来路由其请求,因此此选项在很多时候都很有用。但是,也可以使用 /StopOnEtwEvent 对发生的单个 ETW 事件或持续时间长于触发量的启动-停止对触发停止。一般语法是


“提供者”的位置


“EventName”可以是


通常,PerfView 的“事件”视图中显示的事件名称是正确的。最后,键值对给出了影响语义的其他“选项”。它们都是可选的,以下是对键值对有效的键。


/StopOnEtwEvent 使用示例


正如你所看到的,有很多选择,但大多数情况下你不需要它们。此选项对于您自己的 EventSource 事件可能最有用。如果您定义了一个事件“MyWarning”,则可以通过执行以下操作在该警告条件下停止


如果定义了提供程序“MyEventSource”,并且有两个事件“MyRequestStart”和“MyRequestStop”,则只要请求花费的时间超过 2 秒,就可以停止


如果要在名为“GCTest”的进程(即 exe 名为 GCTest.exe)停止时停止(也可以使用进程号)。


如果要在进程启动时停止,则问题会更大一些,因为“start”事件实际上发生在生成进程的进程中,而不是正在创建的进程中。相反,您可以使用 ProcessStart 具有“ImageName”字段这一事实,并且可以使用 FieldFilter 选项的 ~ 运算符来触发该字段。因此,要在启动名为 GCTest.exe 的进程时停止,您可以执行以下操作


下面是一个稍微复杂的示例,我们仅在 GCTest.exe 可执行文件失败并出现非零退出代码时停止。在这里,我们使用 ImageName 字段来查找特定的 Exe,并使用 ExitCode 字段来确定进程是否失败。当大型脚本中的特定进程失败时,可以使用此功能停止 PerfView(这是一种相当常见的情况)。


下面是一个示例,当 ASP.NET 服务器为特定 URL 提供服务时,我们希望停止。基本上,当 ASP.NET Request 事件触发时,我们会停止,并带有与模式匹配的“FullUrl”字段(以 /stop.aspx 结尾)。


下面是一个示例,当磁盘 I/O 花费的时间超过 10000 毫秒时,我们希望停止。我们想要监视 Windows 内核跟踪/DiskIO/读取事件,并在 FieldFilter 表达式中使用“DiskServiceTimeMSec”字段。


通常,该选项非常强大,特别是如果能够将 ETW 事件添加到代码 (EventSource) 与 FieldFilter 结合使用,可以使用它来停止特定进程中的特定 DLL,加载或卸载注册表项、正在触摸的文件、正在打开的文件,以及发生的任何特定 EventSource 事件(测试其参数)。


在 /StopOnEtwEvent 提供程序上使用关键字


在前面的示例中,我们打开了与特定提供商关联的所有“关键字”。例如,为了跟踪进程的启动和停止,我们打开了 Microsoft-Windows-Kernel-Process 提供程序中的所有事件。虽然这可行,但这可能意味着触发逻辑必须查看并丢弃许多不重要的事件。通过使用“关键字”选项减少订阅的事件数量,可以提高效率,并使触发的任何调试更容易。例如


这与前面的示例相同,但它具有 Keywords=0x10 选项。这会告知 PerfView 仅打开由 0x10 位字段指定的特定事件。唯一的问题是你怎么知道0x10是什么意思?可以通过查看 Microsoft-Windows-Kernel-Process 提供程序的清单来确定这一点。为此,您可以打开“集合”对话框的高级部分,然后单击“提供程序浏览器”按钮。在“提供商”列表框中选择感兴趣的提供商,然后单击“查看清单”按钮。这将显示提供程序的完整 XML 清单。您将找到一个“关键字”部分,并在其中找到每个关键字的定义。因此,我们发现 WINEVENT_KEYWORD_PROCESS 关键字的值为 0x10,我们可以看到感兴趣的事件 (ProcessStop/Stop) 与该关键字相关联,我们知道这是我们实际需要的唯一关键字。因此,我们知道要给上面的“关键字”选项的“神奇”数字。查找关键字的另一种方法是使用“logman query providers provider”。请注意,您不必这样做,但它确实使调试更容易,处理更高效(因为必须筛选掉的事件更少)。


调试触发问题


尝试 /StopOnEtwEvent 限定符并发现它不执行所需的操作(通常是因为它未触发)的情况并不少见。有时,日志中的内容会有所帮助,但 PerfView 不能在日志中放置太多内容,因为它可能会淹没日志。相反,它会将特殊的 PerfView StopTriggerDebugMessage 事件发送到 ETW 流中,以便你可以在“事件”视图中查看数据,并找出它无法正常工作的原因。如果您在触发方面遇到问题,您肯定要查看这些事件。


使用性能计数器触发收集启动(启动触发器限定符)


在许多情况下,只需使用 /StopOnPerfCounter 就足够了(可能还有 /DelayAfterTriggerSec)在有趣的时间点(当性能计数器异常高或低时)收集数据。但是,该技术的缺点是要求收集持续打开。如果兴趣点在性能计数器触发之后很久,则效率低下。在这种情况下,在有趣的时间之前不开始收集事件更有意义。这就是 /StartOnPerfCounter 选项的用途。它的语法与 /StopOnPerfCounter 相同,只是在触发器跳闸之前,它甚至不会开始收集。标志 /MinSecForTrigger:N 适用于 /StartOnPerfCounter,以控制性能计数器在触发收集之前必须满足条件的秒数(默认值为 3 秒)。

 

 

 

 

 

 



将 PerfView 与 EventSources 配合使用


.NET V4.5 运行时附带一个名为 System.Diagnostics.Tracing.EventSource 的类,该类可用于以非常方便的方式记录 ETW 事件。例如,这里有一个名为 MyCompanyEventSource 的简单 EventSource,它有一个“Load”和“Unload”事件。每个事件都会记录对该事件有意义的任何有趣信息,在本例中为加载的“imageBase”以及名称。


密封类 MyCompanyEventSource:EventSource { public static MyCompanyEventSource 日志 = new MyCompanyEventSource(); 日志本身 public void Load(long ImageBase, string Name) { WriteEvent(1, ImageBase, Name); public void Unload(long ImageBase) { WriteEvent(2, ImageBase); } 在其他代码中 MyCompanyEventSource.Log.Load(myImageBase, “我的姓名”); 在另一个地方 MyCompanyEventSource.Log.Unload(我的图像库);


由于 EventSources 可以以标准方式记录到 ETW 日志记录文件,因此 PerfView 可以以有用的方式显示这些内容。本节介绍一些常用技术

 命名 EventSources


与所有 ETW 提供程序一样,EventSource 具有唯一标识它的 8 字节 GUID。通常,GUID 使用起来不方便,您更愿意使用名称。如果 ETW 提供程序将自身注册到操作系统,则 PerfView 可以要求 OS 查找名称并获取 GUID。但是,通常 EventSources 不会执行此操作,因为它会使应用程序的部署复杂化。相反,EventSources 通常使用 Internet 标准方式从名称生成 GUID。因此,给定一个名称,您可以找到 GUID,而无需 EventSource 注册自身。PerfView 支持将此约定与 *NAME 语法一起使用。如果提供程序名称以 * 开头,则假定它是提供程序 GUID,该 GUID 是通过以标准方式对 NAME 进行哈希处理而生成的。(哈希不区分大小写)。EventSource 名称是由应用于 EventSource 类的 EventSourceAttribute 的 Name 参数提供的名称,或者是类的简单名称(无命名空间)(如果没有显式给出的名称)。知道 EventSource 的名称后,可以使用 /providers 限定符打开 EventSource。例如


将在详细级别打开名为“MyCompanyEventSource”的所有关键字 (eventGroups) EventSource。请注意,所有这些都只是“标准”ETW。唯一的特殊部分是 *,用于在不注册的情况下引用 EventSource。


在前面的示例中,除了标准内核和 CLR 提供程序之外,还激活了 MyCompanyEventSource。这对于监视细粒度性能非常有用,但对于简单监视来说太冗长了。虽然可以使用 /kernelEvents=none /clrEvents=none /NoRundown 限定符来关闭默认日志记录,但有一个“/onlyProviders”限定符可以使此操作更加容易。因此


将仅从提到的提供程序(在本例中为 MyCompanyEventSource)收集,并关闭所有其他默认日志记录。因此,这些文件往往保持非常小,并且适用于只希望查看 EventSource 消息的情况。


可以通过打开“高级”下拉列表,取消选中“.NET Rundown”、“Kernel Base”和“,在 GUI 中实现与 /OnlyProviders 限定符相同的效果。NET“复选框,然后在”其他提供程序“文本框中添加 EventSource 规范。


就像任何其他 ETW 源一样,可以通过将事件的“关键字”(组)或日志记录的详细程度指定为 /OnlyProviders 限定符 有关此语法的更多详细信息,请参阅 AdditionalProviders 上的帮助。此处一个非常有趣的选项是打开提供程序的“堆栈”选项,该选项将在每次触发 ETW 事件时记录堆栈跟踪。然后,可以在生成的日志文件的“任何堆栈”视图中查看此内容。


收集数据后,可以使用 PerfView 以正常方式查看数据 这几乎肯定意味着打开“事件”视图,选择感兴趣的事件并更新显示。如果需要,可以使用事件视图中的右键单击上下文菜单将事件另存为 XML 或 CSV 文件。


将 EventSource 数据转换为 XML


在事件查看器中查看 EventSource 的输出非常适合临时调查,因为 GUI 允许快速筛选和转换为 CSV 或 XML 文件(在 EventViewer 中单击鼠标右键)。   但是,你可能只想使用其他工具分析数据,而这些工具希望与 PerfView/ETW 保持非常松散的耦合。 对于这些应用程序,您只需要获取 ETL 文件并将其转换为 XML 文件,然后可以使用其他工具对其进行处理。  有一个 PerfView 命令可以执行此操作。


上面的命令运行名为“DumpEventsAsXml”的“UserCommand”,为其提供参数“PerfViewData.etl.zip”。  这将创建一个名为 PerfViewData.etl.xml 的文件,该文件是原始文件中所有 ETL 数据的 XML 转储(因此文件可能会变大)。   它适用于任何 ETL 或 ETL.ZIP 文件,但它适用于使用 /OnlyProviders 限定符生成的文件,这些文件仅打开了 EventSources,因此将产生相对较少的输出。


细心的用户会想知道什么是“UserCommand”。PerfView 具有“内置”命令,但它也能够使用用户提供的代码进行扩展(有关详细信息,请参阅 PerfView 扩展)。其中一些用户命令变得足够有用,默认情况下,它们随 PerfView 本身一起提供。DumpEventsAsXml 是这些命令之一。可以通过查看“帮助”->“用户命令帮助”菜单选项来查看 PerfView 当前知道的所有用户命令。


PerfView Extensions (Automating PerfView)


PerfView具有使用命令行命令收集数据的能力,可用于自动执行简单的收集任务,但是对于自动执行分析和收集也很有用。对于这个简单的命令行选项是不够的,您需要编程语言的全部功能来支持各种有用的数据操作。这就是 PerfView 扩展的用途。PerfView 允许创建扩展,该扩展是一个 .NET DLL,它与定义用户定义命令的 PerfView.exe 一起存在。这些命令可以控制 PerfView 的收集或分析功能。它非常强大,开辟了广泛的自动化场景,包括


  1. 计算复杂的指标,如启动时间,这需要你找到两个事件之间的差异(例如,进程开始和第一次渲染事件

  2. 基于堆栈中名称的自定义分组和其他分析。

  3. 有关磁盘 I/O、引用集或其他指标的自定义报告

  4. 不仅自动执行 ETW 收集,还自动执行符号解析,将数据减少到单个进程,并将各种视图另存为 PERFVIEW.XML.ZIP 文件,从而大大减少了数据量(因此可以存档更多数据)并加快了该数据的使用速度(因为符号已解析且文件大小非常小)


调用用户定义的命令


除了内置的命令行命令(如“run”、“collect”和“view”)外,还有一个“userCommand”。用户命令是在 PerfView 中激活用户定义功能的一种方法。例如,当您运行命令


PerfView 将在 PerfView.exe 旁边查找名为“PerfViewExtensions\Global.dll”的 DLL。然后,它将查找一个名为“Commands”的类型并创建它的实例。然后,它会在该类型中查找名为“DemoCommandWithDefaults”的方法。然后,它将命令的其余参数传递给该方法。通常,方法目标是 varags(它的最后一个参数是 'params string[]'),它允许它处理任意数量的参数。


名为“Global”的扩展名的特殊之处在于,如果用户命令中没有“.”,则该扩展名被假定为“Global”扩展名。因此,上面的命令可以短接到


从 GUI 调用用户定义的命令


您还可以使用主查看器上的“文件”->“用户命令”菜单选项 (Alt-U) 从 GUI 调用用户命令。此命令将打开一个对话框,您可以在其中输入命令。PerfView 会记住以前执行的用户命令(甚至在程序调用之间),因此通常只需键入前几个字符就足以选择过去执行过的命令。按 Tab 键将提交完成,按 Enter 键将运行该命令。因此,只需敲几下键盘,您就可以执行用户定义的命令。


有关用户定义命令的帮助


帮助>“用户定义的命令”菜单项以及用户命令对话框中的“命令帮助”按钮将打开一个对话框,其中包含有关各种用户定义命令的帮助


创建 PerfView 扩展(创建用户命令)


在调用用户定义的命令之前,需要创建包含命令的扩展 DLL。这就是 PerfView CreateExtensionProject 命令的作用。由于扩展 DLL 是通过查找相对于 PerfView.exe 的 RELATIVE 来定位的,因此创建自己的扩展的第一步是将 PerfView.exe 复制到你控制的位置。例如:


完成此操作后,您可以执行命令(请注意,我们启动了 perfview 的 LOCAL 副本)


您将在 PerfView.exe 旁边创建 PerfViewExtensions 目录,并执行三项操作


  1. 在 PerfViewExtenionsExtensionNameSrc 中创建新的 C# 项目。如果 ExtensionName 缺失/为空,则使用扩展名称“Global”。

  2. 创建/修改解决方案文件 PerfViewExtenions\Extensions.sln 以包含新项目。

  3. 在 Visual Studio 2010 中打开 PerfViewExtenions\Extensions.sln。


因此,在运行 CreateExtensionProject 命令后,只需打开 PerfViewExtenions\Extensions.sln 即可运行、编译和测试新的 PerfView 扩展。如果您安装了 VS2010,则可以在几秒钟内启动并运行。


因此,最好的开始方法可能是:


探索 PerfView 对象模型


  1. INTELLISENSE 是你的朋友!默认情况下,只有 PerfViewExtensibility 命名空间处于打开状态,这是 PerfView 对象模型中最重要的类所在的位置。这意味着,如果您输入一些字符,很有可能会找到您要查找的内容。

  2. CommmandEnvironment 是一个很好的起点。这是定义“全局”方法的类。如果选择下面的 CommmandEnvironment 并按 F12,则可以浏览其他全局方法。这些方法将返回对象模型中的其他重要类型(例如 EtlFile、Events、Stacks)。

  3. 首先了解 PerfViewExtensibility 中的类。可以使用对象浏览器 (Ctrl-W J) 并在 PerfView.PerfViewExtensibility 命名空间下查看。

  4. 请看一下示例命令。它们在上下文中使用了许多重要功能(日志记录、符号查找、HTML 报告),这非常有用。


熟悉 PerfView 对象模型后,需要意识到一个重要的注意事项


这意味着,如果要将 PerfView.exe 升级到较新版本,则很有可能必须更新扩展以匹配自上一版本以来对 PerfView 所做的任何更改。原因很简单。PerfView 对象模型实际上最好被认为是“Beta”版本,因为根本没有足够的时间来找到最佳的 API 图面。因此,变化是不可避免的,保持兼容性的成本根本不值得。因此,您可以自由创建 PerfView 扩展,但在决定创建扩展时,必须准备好支付升级的移植成本。


使用用户命令扩展 GUI


用户命令使您能够调用代码来创建专用的数据视图,但它并未集成到 GUI 本身中。本部分介绍如何使用户命令成为正常 GUI 体验的一部分。执行此操作的关键是 PerfView.exe 文件旁边的“PerfViewExtensions”目录中的“PerfViewStartup”文件。如果存在此类文件,则此文件中的命令将在 PerfView 启动时执行。此文件是逐行读取的,并具有以下命令


 查看 Linux 数据


Linux 有一个称为 Perf Events 的内核级事件日志记录系统,它与 ETW 没有什么不同,特别是知道如何以定期间隔(例如 1 毫秒)捕获 CPU 堆栈 PerfView 知道如何读取此数据,因此可以使用 Linux 上的 Perf 事件工具收集数据,将数据复制到 Windows 计算机,并使用 PerfView 的堆栈查看器进行查看。本节的其余部分大部分内容都是 linux-performance-tracing.md 文档的克隆。您可能还希望在那里查看是否有这些说明的最新版本。

 设置


获取 perfcollect 脚本


Brian Robbins编写了一个BASH(shell)脚本,该脚本将运行Perf.exe,解析符号,并将所有信息收集到ZIP文件中,以便传输到另一台计算机。您可以使用网络浏览器或使用“cURL”实用程序下载它


下载后,要让它运行,您必须使其可执行


如果这可行,你应该能够做到


它应该打印出一些帮助。


安装 Linux Perf 工具


您将需要 Perf.exe 命令以及 LTTng 包,您可以通过执行以下操作来获取这些命令


请注意,您需要成为超级用户才能执行此操作,因此如果您还没有成为超级用户,这就是为什么上面的命令在执行安装脚本之前使用 sudo 命令提升为超级用户的原因。

 收集数据


如果运行的是 .NET 运行时应用程序,则必须设置一个环境变量,该变量将告知运行时发出有关实时 (JIT) 编译方法的符号信息。因此,在运行应用程序之前,必须确保设置了以下环境变量


此时,您可以开始收集。为此,请打开另一个命令窗口并运行以下命令。


此时,您可以转到第一个窗口(设置COMPlus_PerfMapEnabled)并启动应用程序。应用程序完成后,可以使用 Ctrl-C 停止收集。结果是 FILENAME.trace.zip 文件。这包含跟踪以及用于解析符号信息的所有其他文件。


使用 PerfView 查看数据


创建 FILENAME.trace.zip 文件后,可以将其传输到 Windows 计算机,只需使用 PerfView 打开它即可。它将在 CPU 示例的堆栈窗口中打开文件,并且 CPU 调查的所有常规技术都适用。


引擎盖下发生的事情是 PerfView 正在打开 FILENAME.trace.zip 文件以在存档中找到后缀为 *.data.txt 的文件并读取该文件。此文件应为运行“Perf script”命令的输出。PerfView 还知道如何直接读取带有 *.data.txt 后缀的文件,因此,如果您不希望在收集 Linux 数据时使用“perfcollect”脚本,您仍然可以轻松地将数据提供给 PerfView。(还可以将 *.data.txt 文件压缩成后缀为 *.trace.zip 的文件,PerfView 会很乐意打开它)


 查看外部数据


PerfView 最强大的方面之一是其堆栈查看器。也许这个查看器最有趣的事情之一是它非常通用。此查看器中显示的数据只是一组示例,其中每个示例包含


  1. 表示时间的(可选)浮点值。

  2. 一个值(默认为 1),表示样本的指标或成本。

  3. 表示分层树中的堆栈或路径的名称列表。


堆栈查看器的所有其他魔力、包容性和排他性成本、时间线、过滤、调用方和被调用方视图,都只是这些数据的不同聚合。


这意味着几乎任何分层数据都可以在堆栈查看器中有用地显示。例如,磁盘视图上的大小只是将文件名的路径作为形成“堆栈”的路径,并将文件的大小作为指标来形成磁盘视图上总大小的模型。这意味着可以使用堆栈查看器查看来自其他探查器或数据形成层次结构的任何其他地方的数据。


简单的 .perfView.xml 格式


现在,在 PerfView 的实现中,有一个名为“StackSource”的类,它表示此示例列表,其中包含 PerfView 的查看器查看的堆栈。还有一个名为“InternStackSource”的类,旨在使读取其他格式变得容易,并将该数据转换为StackSource。但是,PerfView 还具有两种格式,可以非常轻松地允许其他工具输出 perfview 可以简单读取的堆栈。其中一种格式是基于XML的,另一种是基于JSON的,它们都不足为奇,它们只是堆栈查看器在这些格式中需要的数据的“明显”编码。例如,下面是 .perfView.xml 格式的示例


<采样时间=“10” 指标=“10”> Helper嵌套 助手1 功能3 功能 主要 <采样时间=“20” 指标=“10”> 功能3 功能 主要 <采样时间=“30” 公制=“10”> 助手X 助手1 功能3 功能 主要 <采样时间=“40” 公制=“10”> 功能 主要


您可以看到格式可以非常简单。有一个“StackSource”元素,它有一个成员“Samples”,而“Samples”又包含一个 Sample 列表,每个 Samples 都有一个时间和一个指标(这两者都是可选的,时间默认为 0,指标默认为 1) 每个示例中都有一个堆栈帧列表,每行一个。它们按从最具体(或最深的调用树嵌套)到最不具体(主程序)的顺序排列。这就是 PerfView 读取数据所需的全部内容。只需将上述文本粘贴到“*.perfView.xml”文件中,然后在 perfview 中打开该文件即可尝试此操作。PerfView 将在堆栈查看器中打开该数据(尝试一下!


有相应的 *.perfView.json 格式,该格式与 XML 格式完全类似。基本结构是相同的:StackSource 具有相同的示例列表,每个示例具有表示堆栈的时间、指标和名称列表。下面是一个示例。与前面的示例一样,可以剪切并粘贴到 *.perfView.json 文件中,然后在 PerfView 中打开它,以在堆栈查看器中查看数据。


{ “堆栈源”:{ “样本” : [ { “时间” : “10”, “公制”: “10”, “堆栈”:[ “HelperNested”, “助手 1”, “功能”, “主要” ] }, { “时间” : “20”, “公制”: “10”, “堆栈”:[ “Func3”, “功能”, “主要” ] }, { “时间” : “30”, “公制”: “10”, “堆栈”:[ “助手X”, “助手 1”, “Func3”, “功能”, “主要” ] }, { “时间” : “40”, “公制”: “10”, “堆栈”:[ “功能”, “主要” ] } ] } }


高级 .perfView.xml 格式


简单的格式很好,因为它很容易解释,但效率非常低。您可以看到,对于每个样本,每个堆栈都必须完整地重复,并且大多数情况下,堆栈彼此非常相似。此外,当你将示例读入查看器时,你不会获得 PerfView 的分组、折叠和筛选选项的任何默认值,这使得体验不太理想。


好吧,.perfView.xml 格式实际上比到目前为止显示的要复杂得多。事实上,您可以将 ID 分配给堆栈的每个唯一帧,并使用 ID 而不是名称(节省大量空间)。同样,您可以将 ID 分配给可在示例中使用的每个唯一堆栈(从帧 ID 构建)(节省更多空间)。这种压缩大大缩短了加载数据的时间。最后,可以为每个堆栈查看器文本框指定所有默认值和所有选项(例如,Group Pats、Fold Pats Include Pats ...文本框)。简而言之,在生成 .perfView.xml 文件时,只需多做一点工作,就可以使体验明显更好。


与其记录这些格式的具体格式,不如简单地向您展示一个示例。PerfView 堆栈查看器具有“文件->保存”命令,这会将当前堆栈视图另存为 .perfView.xml.zip 文件。如果解压缩此文件,则将看到数据数据以这种更完整、更高效的格式表示。因此,您可以采用上面的示例之一,打开它,将一些数据添加到文本框(记住历史记录)中,然后保存视图。然后你解压缩它并查看格式。格式完全简单明了。



使用 WPA (Windows Performance Analyzer)


Windows 性能分析器 (WPA) 是由 Windows 构建的工具,作为 Windows 评估和部署工具包的一部分免费提供。与 Windows 性能记录器 (WPR) 一起,它可用于收集和查看 ETW 数据。由于它们都使用相同的数据格式(ETW 跟踪日志 (ETL) 文件),因此使用一种工具进行收集并使用另一种工具进行查看很容易。这很有用,因为 WPA 具有 PerfView 所没有的非常强大的绘制和查看数据的方法,并且 PerfView 具有收集 WPA 中不存在的数据和其他视图的强大方法。


使用 PerfView 收集数据,使用 WPA 查看数据。


PerfView 具有许多生产监视(例如 /StopOnPerfCounter)功能,而这些功能目前 WPR 没有。此外,PerfView 易于从任何 Web 下载,并且 XCOPY 部署为单个 EXE,这使得 PerfView 非常适合在现场收集数据。在这种情况下,只需使用 PerfView collect 命令(如果可能正在执行挂钟调查,则使用 /threadTime 选项)进行收集,结果将是 .ETL.ZIP 文件已准备好上传。遗憾的是,目前WPA不会打开ETL.ZIP文件,但您可以使用以下命令


这将解压缩数据文件以及任何 NGEN PDBS 并将它们存储在 .NGENPDB 文件夹,因此以这种方式解压后,可以在数据文件上运行 WPA 命令来查看 WPA 中的数据。


在上面的方案中,PerfView 将像往常一样设置 ETW 提供程序。但是,PerfView 还能够模拟 WPR 默认打开的提供程序。因此,如果您希望使用 PerfView 收集数据并尝试尽可能多地模拟 WPR,请使用以下命令收集数据。


这应该会生成与 WPR 生成的数据文件非常接近(如果不完全相同)的数据文件。具体而言,它不会生成 ZIPPed 文件,而是输出 .ETL 文件和 .NGENPDB 目录,就像 WPR 一样。与所有集合命令一样,可以使用“/Providers”限定符添加更多提供程序,也可以使用 /KernelEvents 或 /ClrEvents 限定符来微调 Kernel 和 .NET 提供程序事件。


如果希望像 WPR 一样生成文件,但利用 PerfView 的 ZIPPing 功能,则可以组合 /wpr 和 /zip 命令,如下所示。


此命令将像 WPR 一样打开提供程序,但会像 PerfView 一样对其进行 ZIP。这对于远程收集很有用。您可以使用它来收集数据,并使用 PerfView /wpr unzip 在其目标位置解压缩数据,以便使用 WPA 进行查看。


使用 PerfView 查看使用 WPR 收集的数据。


PerfView 具有许多 WPA 所没有的视图和查看功能。因此,在 PerfView 中查看使用 WPR 收集的数据通常很有用。此方案“有效”:PerfView 已经知道如何打开 ETL 文件,并且它足够聪明,可以注意到符号信息的 NGENPDB 目录并适当地使用它。


 命令行参考


大多数与查看无关的功能都可以从命令行获得,以便轻松实现数据收集的自动化。 在命令行键入


或者,从 PerfView 主窗口导航到“帮助->命令行帮助”将提供更完整的详细信息。


有关为 PerfView 生成扩展的信息,另请参阅 PerfView 扩展。


在脚本中使用 PerfView(/LogFile 限定符)


默认情况下,PerfView 在执行任何操作(包括数据收集)时将始终显示一个 GUI 窗口。这样做是为了允许报告错误。对于无人值守的自动化,这可能是不可取的。这是 /LogFile:FileName 限定符的用途。当指定此限定符而不是启动 GUI 时,该命令会将所有输出发送到指定的文件。其目的是脚本将使用此限定符来避免 GUI。如果命令成功,PerfView 的退出代码将为 0。


高级数据收集


PerfView 数据收集基于 Windows 事件跟踪 (ETW) 。这是以低开销方式记录信息的通用工具。它在整个 Windows 操作系统中非常有用,特别是 Windows 操作系统内核和 .NET CLR 运行时都使用它。默认情况下,PerfView 会选择一组默认的事件,这些事件对 PerfView 可以可视化的分析类型具有较高的价值。但是,PerfView 也可以简单地用作数据收集器,此时打开其他事件可能很有用。这就是 /KernelEvents: /ClrEvents: 和 /Provider: 限定符的作用


所有 ETW 事件都记录以下信息


  1. 事件发生的时间(达到 100ns 分辨率)

  2. 记录事件的提供程序(例如,内核、CLR 或某些用户提供程序)。

  3. 事件编号(指示如何解码有效负载)

  4. 与事件关联的进程和线程(有些事件没有有用的进程或线程 ID,但大多数都有)

 内核事件


到目前为止,Windows 内核中内置的 ETW 事件是最基本和最有用的事件。几乎任何数据收集都希望至少打开其中的一些。PerfView 将内核事件分为三组 请参阅内核 ETW 事件


默认内核组


默认组是 PerfView 默认打开的组。  这些事件中最详细的是“配置文件”事件,该事件每毫秒触发一次计算机上每个 CPU 的堆栈跟踪(以便您知道 CPU 在做什么)。   因此,在 4 处理器机器上,每秒跟踪时间将获得 4000 个样本(带有堆栈跟踪)。  这可以加起来。  假设每秒跟踪将获得至少 1 Meg 的文件大小。  如果需要运行很长的跟踪(100 秒),则应强烈考虑使用循环缓冲区模式来控制日志。  以下是您在默认组下获得的事件:


  1. 默认值 = DiskIO |磁盘文件IO |磁盘IOInit |图像加载 |内存HardFaults |网络TCPIP |工艺流程 |进程计数器 |公司简介 |线

  2. DiskIO - 每次物理磁盘读取完成时触发,指示大小以及操作所花费的时间。 无堆栈跟踪。

  3. DiskIOInit - 每次磁盘 I/O 操作开始时触发(其中 DiskIO 在结束时触发)。 与 DiskIO 不同,它记录堆栈跟踪。

  4. DiskFileIO - 记录操作系统文件对象句柄与文件名之间的映射。 如果没有这个,许多内核事件就没有用处,因为您无法将操作与有意义的名称相关联。   你几乎总是想要这个事件。 无堆栈跟踪。

  5. ImageLoad - 当 DLL 或 EXE 加载到内存中执行(调用 LoadLibaryEx)时触发。 如果要将内存地址映射回符号名称,则需要此项。 记录堆栈跟踪。

  6. MemoryHardFaults - 当 OS 必须导致物理磁盘读取以响应映射虚拟内存时触发。  记录堆栈跟踪。

  7. NetworkTCPIP - 在发送或接收 TCP 或 UDP 数据包时触发。  记录两个端点和大小。 无堆栈跟踪。

  8. 进程 - 在创建或销毁进程时触发。 指示命令行(启动时)或退出代码(结束时)。 记录堆栈跟踪。

  9. ProcessCounters - 在进程终止或跟踪结束之前记录进程内存统计信息。  无堆栈跟踪。

  10. 配置文件 - 每个处理器每 1 毫秒触发一次,并指示指令指针当前列表的位置,并作为定位跟踪。

  11. 线程 - 每次创建或销毁线程时触发。  记录堆栈跟踪。


默认情况下,以下内核事件不处于打开状态,因为它们可能相对详细,或者用于更专业的性能调查。


  1. ThreadTime = 默认值 |上下文切换 |Dispatcher - 这是最常见的详细选项。除了所有默认提供程序之外。如果要在 perfview 中使用“线程时间”视图,则需要此选项。

  2. 详细 = 默认值 |上下文切换 |磁盘IOInit |调度程序 |文件IO |文件IOInit |MemoryPageFaults |注册表 |虚拟Alloc

  3. ContextSwitch - 每次操作系统停止运行切换到另一个开关时触发。 它表示丢失处理器和获取它的线程。 此事件触发> 10K 秒,具体取决于场景,但对于确定某些进程正在等待的原因非常有用。 记录堆栈跟踪。

  4. Dispatcher -(也称为 ReadyThread)在线程从等待变为就绪时触发(请注意,如果没有可用的 CPU,线程可能不会实际运行)。 这也可以> 10K / 秒的速度触发,但对于理解为什么会发生等待非常有用。

  5. FileIO - 在文件操作完成时触发(即使该操作不会导致磁盘读取(因为它位于文件系统缓存中)。 不记录堆栈跟踪。

  6. FileIOInit - 在文件操作开始时触发。 与 FileIO 不同,这将记录堆栈跟踪。

  7. MemoryPageFaults - 当虚拟内存页可访问时触发(由物理内存支持)。  这不仅在需要从磁盘获取页面时触发,而且在页面已经在文件系统缓存中或只需要清零时也会触发。   记录堆栈跟踪。

  8. 注册表 - 在发生注册表操作时触发。  记录堆栈跟踪。

  9. VirtualAlloc - 在发生虚拟内存分配或空闲操作时触发。 进程中的所有内存要么已映射,要么通过 Virtual Alloc 操作进行分配。


最后一组内核事件通常对于编写设备驱动程序或试图理解硬件或低级操作系统软件行为异常的原因的人很有用


  1. 操作系统 = AdvancedLocalProcedureCalls |DeferedProcedureCalls |驱动程序 |中断

  2. AdvancedLocalProcedureCalls - 在进行操作系统计算机本地过程调用时记录。

  3. DeferedProcedureCalls - 在进行 OS Deferred 过程调用时记录

  4. SplitIO - 当磁盘 I/O 必须拆分为多个部分时记录

  5. 驱动程序 - 记录发生的各种硬件驱动程序事件。

  6. 中断 - 发生硬件中断时记录。

 CLR 事件


除了内核事件之外,如果运行的是 .NET 运行时代码,则可能还需要打开 CLR ETW 事件。默认情况下,PerfView 会打开其中的一些功能。有关这些事件的详细信息,请参阅 CLR ETW 事件。


  1. 默认值 = GC |类型 |GCHeap生存与运动 |粘结剂 |装载机 |吉特 |NGen公司 |SupressNGen 中文 |StopEnumeration |安全 |AppDomainResourceManagement (应用域资源管理) |例外 |线程 |争用 |堆栈 |JittedMethodILToNativeMap |线程传输

  2. GC - 在 GC 启动和停止时触发

  3. 活页夹 - 目前仅对 CLR 团队有用。

  4. 加载程序 (Loader) - 在加载或卸载程序集时触发

  5. Jit - 在编译方法实时 (JIT) 时触发。

  6. NGen - 使用预编译的 NGEN 映像执行的操作发生时触发

  7. 安全 - 在各种安全检查中触发

  8. AppDomainResourceManagement - 在发生某些 appdomain 资源管理事件时触发。

  9. 争用 - 当托管锁导致线程休眠时触发。

  10. 异常 - 在发生托管异常时触发。

  11. 线程 - 在各种 System.Threading.ThreadPool 操作上触发

  12. 停止枚举 - 尽早转储符号信息(不推荐)

  13. 开始枚举 - 尽可能晚地转储符号信息(通常在进程停止时)。这是默认设置。

  14. JitTracing - 有关实时编译的详细信息(为什么事情是内联的......

  15. Interop - 有关生成本机互操作代码的详细信息。

  16. 堆栈 - 为各种 CLR 事件启用堆栈跟踪。

 ASP.NET 活动


ASP.NET 有一组事件,这些事件在处理每个请求时发送。  PerfView 具有一个特殊视图,可以在打开 ASP.NET 事件时打开该视图。  默认情况下,PerfView 会打开 ASP.NET 事件,但是,在安装 ASP.NET 时,还必须选择“跟踪”选项才能使这些事件正常工作。 因此,如果您没有看到 ASP.NET 事件,则您正在运行 ASP.NET 方案,这可能是您未获取数据的原因之一。


启用 ASP.NET 跟踪


打开跟踪的最简单方法是使用操作系统附带的 DISM 工具。  从提升的命令提示符运行以下命令


请注意,此命令将重新启动 Web 服务(使其生效),如果 ASP.NET 服务处理较长(许多秒)的请求,这可能会导致复杂情况。这将强制 DISM 延迟(重新启动)或中止未完成的请求。因此,您可能希望将其安排在其他服务器维护中。在特定计算机上完成此配置后,它将持续存在。


您也可以使用 GUI 界面手动完成此配置。 您首先需要进入用于配置 Windows 软件的对话框。 这取决于您使用的是客户端还是服务器版本的操作系统。


 符号解析


另请参阅源代码查找。


在收集时,当获取 CPU 样本或堆栈跟踪时,它由内存中的地址表示。   此内存地址需要转换为符号形式才能用于分析。  这分两步进行。


  1. 首先确定代码是否属于特定的 DLL(模块)。

  2. 给定 DLL,查找详细的符号信息


如果第一步失败(不常见),则为地址提供符号名称?!?(未知的模块和方法)。但是,如果第二步失败(更常见),那么您至少可以知道模块,并且地址被赋予符号名称模块?.

 ?!?方法


不属于任何 DLL 的代码必须是动态生成的。如果此代码是由 .NET 运行时通过编译 .NET 方法生成的,则它应该已由 PerfView 解码。但是,如果指定了 /NoRundown 或日志文件不完整,则解码地址所需的信息可能已丢失。然而,更常见的是,运行时会生成许多“匿名”帮助程序方法,并且由于这些方法没有名称,因此除了将它们保留为?!?.之外,没有太多事情可做这些帮助程序通常不感兴趣(它们没有太多独占时间),并且可以在分析期间折叠到其调用程序中(将?!?添加到 FoldPats 文本框)。它们通常发生在托管代码和非托管代码的边界处。

 模块!?方法


未在运行时生成的代码始终是 DLL 正文的一部分,因此始终可以确定 DLL 名称。预编译的托管代码位于 (NGEN) 映像中,这些映像的名称中包含 .ni,并且信息应位于 PerfView 收集的 ETL 文件中。如果您在包含 .ni 的模块中看到未知的函数名称,则意味着 CLR rundown 出了问题(请参阅 ?!? 方法)。对于非托管代码(没有 .ni),需要在与该 DLL 关联的符号信息中查找地址。此符号信息存储在程序数据库文件 (PDB) 中,并且解析大型跟踪的成本可能相当高(10 秒或更长时间)。因此,默认情况下,PerfView 不会解析任何非托管符号。


相反,它会等到您作为用户请求更多符号信息。通常,这是在堆栈查看器中通过右键单击带有模块的单元来完成的!?名称,然后选择“查找符号”。这表示 PerfView 应搜索 PDB 文件并解析它可以在模块中的任何名称。查找正确 PDB 的问题并不少见,因此不能保证成功,可能需要几秒钟才能完成。如果“查找符号”失败,请参阅日志文件。


通常,PerfView 支持在多个单元上执行命令。 这对于符号解析非常方便。 例如,如果有几个未解析的模块看起来很感兴趣(因为它们的 CPU 使用率很高),您可以将它们全部选中(通过拖动或按住 shift 键单击),然后选择“查找符号”。


可以从命令行“预取”符号。为此,请指定 /SymbolsForDlls:dll1,dll2 ...启动 PerfView 时。传递给 /SymbolsForDlls 的列表中的 dll 没有其文件扩展名或路径。

 默认符号路径


到目前为止,最常见的非托管 DLL 是 Microsoft 作为操作系统的一部分提供的 DLL。   因此,如果未指定_NT_SYMBOL_PATH则 PerfView 将使用以下“标准”方法


这表示在标准 Microsoft PDB 服务器 https://msdl.microsoft.com/download/symbols 查找 PDB,并将它们本地缓存在 %TEMP%\SymbolCache 中。  因此,默认情况下,您始终可以找到标准 Microsoft DLL 的 PDB。


但是,如果你对Microsoft不发布的 DLL 的符号感兴趣(例如,你自己的非托管代码),则必须在启动 PerfView 之前提供指定查找位置的_NT_SYMBOL_PATH。


在 GUI 中设置_NT_SYMBOL_PATH


如果需要更改符号路径,可以在启动 PerfView 之前设置 _NT_SYMBOL_PATH 环境变量,也可以使用 StackViewer 窗口中的“文件”->“SetSymbolPath”菜单选项。  此命令将弹出一个简单的对话框,显示_NT_SYMBOL_PATH变量的当前值,并允许您更改它。  _NT_SYMBOL_PATH是以分号分隔的查找符号的位置列表。每个此类条目可以是


  1. 一个简单的文件系统路径。这些路径可以是相对路径,但建议使用绝对路径

  2. SRV*localPath*symbolServer 形式的语法。其中 localPath 是可选的,并指定本地计算机上用于缓存从符号服务器提取的文件的位置。始终建议使用此项,如果不输入它,PerfView 将为你添加它 (使用 %TEMP%\SymbolCache) 。SymbolServer 是符号服务器的名称。它可以是 UNC 文件名(例如 \\MySymbols\symbols)或 URL(例如 https://msdl.microsoft.com/download/symbols)


通常,如果在执行“查找符号”时没有获得非托管符号,请检查日志,并在必要时向符号路径添加新路径。另请参阅符号解析。


PerfView 支持 Azure DevOps 符号服务器,它将使用本地开发凭据(Visual Studio 或 VSCode)或提示你登录来自动进行身份验证。

 总结


因此,通常您只需要获得好的符号


  1. 如果要调查不是来自 Microsoft 的 EXE 的非托管 DLL 的性能问题(例如,你自己生成了它们),则必须在启动 PerfView 之前将_NT_SYMBOL_PATH设置为包含这些 PDB 的位置。


  2. 选择具有 !?在查看器中,右键单击并选择“查找符号”


源代码查找


一个非常有用但容易被忽视的功能是 PerfView 的源代码支持。通过在堆栈查看器中选择名称并键入 Alt-D(D 表示定义),或右键单击并选择“转到源”来激活此支持。这将在文本编辑器中显示该名称的源代码,其中每一行都用该行的指标进行了注释。此功能对于在方法中进行分析是必不可少的,并且通常对于理解代码的一般功能也很有用。


源代码支持是一种相对脆弱的机制,因为除了具有以符号方式查找方法名称 (PDB) 的所有信息外,PerfView 还需要行级信息以及对源代码本身的访问。这些额外的条件很容易被破坏,这将破坏该功能。但是,源代码支持通常非常有用,因此值得麻烦地让事情正常工作。


为了使源代码正常工作,您需要满足以下条件


  1. 代码必须支持行级符号信息。这包括

  2. PerfView 必须能够找到源代码。这可以通过多种方式实现。


PerfView 在 PerfView 的日志中提供了查找源代码所执行步骤的详细消息。因此,如果在查找源代码时有任何问题,则此日志是开始的地方。


在 GUI 中设置_NT_SOURCE_PATH


通常不需要设置 _NT_SOURCE_PATH 变量,因为默认情况下,PerfView 将搜索原始生成时位置(如果在运行的同一台计算机上生成,则该位置将起作用)以及 PDB 符号文件中指定的符号服务器(如果代码已使用源服务器编制索引,则该位置有效)。但是,在其他情况下,您必须设置_NT_SOURCE_PATH。就像 _NT_SYMBOL_PATH 的情况一样,您可以通过转到堆栈查看器的 File -> 'Set Source Path' 菜单条目在 GUI 中设置此变量。此值在 PerfView 程序的不同调用中保留。


另请参阅源代码查找。


向Azure DevOps符号服务器和专用源存储库进行身份验证。


如果符号位于Azure DevOps项目存储中,或者源代码不是公开的,则 PerfView 可能会提示你登录。目前支持Azure DevOps和专用GitHub存储库。如果已安装,PerfView 将尝试使用 Git 凭据管理器,该管理器通常随 Git For Windows 一起安装。如果未安装 Git 凭据管理器,PerfView 将回退到备用身份验证机制。可以在 PerfView 主窗口的“选项”菜单上的“身份验证”子菜单上配置身份验证机制。身份验证选项如下所述。



跟踪中的“BROKEN”堆栈帧。
 


获取样本时,ETW 系统会尝试进行堆栈跟踪。   由于各种原因,在采用完整堆栈之前,此操作可能会失败。   PerfView 使用启发式方法,即所有堆栈都应以负责创建线程的特定 OS DLL (ntdll) 中的帧结尾。  如果堆栈未在此处结束,则 PerfView 假定它已损坏,并在线程和提取的堆栈部分之间注入一个名为“BROKEN”的伪节点(至少它将具有获取示例的地址)。   因此,BROKEN 堆栈应始终是表示操作系统线程的某个帧的直接子级。


当 BROKEN 堆栈的数量很少(例如<总样本的 3%)时,可以简单地忽略它们。 这是常见的情况。  然而,破碎的堆栈越多,“自上而下”分析(使用CallTree视图)的用处就越小,因为实际上一些重要的样本部分没有被放置在适当的位置,在堆栈顶部附近给你倾斜的结果。   “自下而上”分析(首先查看样品发生的方法)不受损坏堆栈的影响(但是,当该分析“向上”移动时,它可能会受到影响)


损坏堆栈的发生原因如下


  1. 在 32 位进程中,ETW 依赖于编译器通过发出“EBP 帧”来标记堆栈。 当它无法完全执行此操作并将 EBP 寄存器用于其他目的时,它会破坏堆栈。  对于操作系统代码或 .NET 运行时代码,不应发生这种情况,但对于第三方代码,可能会发生这种情况。

  2. 在 64 位 Windows 7 或 Windows Server 2008 上的 32 位进程中,存在一个 bug,即堆栈在某些会话中被统一丢弃。 好消息是,它只是间歇性地发生。  因此,如果您再次收集数据,很可能会回避此错误。  这应该在 Windows 8 中修复。

  3. 在 64 位进程中,ETW 依赖于不同的机制来遍历堆栈。 在这种机制中,编译器生成“展开信息”。   目前,此 ETW 机制不适用于动态生成的代码(由 .NET 运行时 JIT 编译器生成)。 这会导致堆栈在堆栈上的第一个 JIT 编译方法处中断(您会看到 JIT 编译方法,但没有该方法的调用方)。   此问题已在 Window 8 上修复,但在以前的操作系统版本中未修复。

  4. 异步活动。  堆栈爬行是一项“尽力而为”的服务。  如果在无法安全地进行日志记录时采集样本,则操作系统只会跳过它。  例如,如果在内核中的堆栈爬取期间发现堆栈页面被交换到磁盘,则堆栈爬行将直接中止。


解决 64 位堆栈中断问题:


如果您正在分析一个 64 位进程,那么您很有可能受到上述场景 (2) 的影响。   在这种情况下,有三种解决方法可以解决损坏的堆栈


  1. NGEN 应用程序。失败发生在 JIT 编译的代码中。如果对应用程序进行 NGEN,则不需要 JIT 编译,并且损坏的堆栈将消失。要 NGEN 您的应用程序,只需键入


    C:\Windows\Microsoft.NET\Framework64\v4.0.30319\N安装 YourApp.exe。


    每次重新编译应用程序时,都必须重复此操作。如果代码是从服务器调用的,则需要对所有重要的 DLL 进行 NGEN(与上述命令行相同)。


    对于服务器应用程序,通常没有可以传递给上述 NGEN 命令的主 EXE,但是您可以使用相同的语法 (NGEN install DLLPATH) 对特定 DLL 进行 NGEN。如果不知道 DLL 的路径名称,可以通过转到“事件”视图并选择“ModuleLoad”和“ModuleDCStop”事件以及“ModuleILPath”和“ModuleNativePath”列来找到它们。任何没有“ModuleNativePath”的 DLL 都是 NGEN 的候选者。


  2. 切换到 32 位。如果代码是纯托管代码,则可以将其作为 32 位或 64 位进程运行。通过切换使用 32 位进程,可以避免此问题。如果您采用仅存在于 64 位的依赖项本机代码,则此操作不起作用。可以使用 .NET SDK 附带的 CorFlags 实用工具将应用程序转换为运行 32 位。它也是 Visual Studio 的一部分(打开 VS 命令提示符)。要切换,只需键入 CorFlags /32bit+ YourApp.exe。每次重新编译应用程序时,都必须重复此操作。


    对于 ASP.NET 应用程序,您可以按照此博客中的说明进行设置,以便以 32 位进程加载页面


  3. 仅执行自下而上的分析。  即使有许多损坏的堆栈,配置文件中也有很多信息,并且可以进行“自下而上”的分析。


堆栈上缺少帧(堆栈表示 A 调用 C,而在源中 A 调用 B 时,B 调用 C)


丢失的堆栈帧与损坏的堆栈不同,因为丢失的是堆栈“中间”的帧。  通常只缺少一种或两种方法。  缺少堆栈有三个基本原因。


  1. 内联。  如果 A 调用 B 调用 C,如果 B 非常小,编译器简单地将 B 的主体“内联”到 A 的主体中并不罕见。  在这种情况下,显然 B 没有出现,因为在非常真实的意义上,B 在本机代码级别上并不存在。

  2. 尾巴叫。  如果方法 B 在返回之前做的最后一件事是调用 C,则编译器可以执行其他优化。  B 可以简单地跳转到 C,而不是调用 C 然后返回 A。   当 C 返回时,它将直接返回给 A。   从探查器的角度来看,当 CPU 执行 C 时,B 已从堆栈中删除,因此不会显示在跟踪中。  另请注意,B 不需要很小,这种优化才是有益的。 唯一的要求是调用 C 是 B 做的最后一件事。

  3. EBP 帧优化。在 32 位进程(64 位进程不使用 EBP 帧)中,探查器依赖于编译器通过在称为 EBP 帧的方法的开头发出代码来“标记”调用。如果编译器根本没有设置帧,而是将 EBP 寄存器用于自己使用,则会导致堆栈损坏。但是,即使编译器意识到需要生成 EBP 帧,这样做也会产生开销(方法开头和结尾有 2 条指令。对于小方法(太大而无法内联,但仍然很小),编译器可以选择简单地省略帧的生成(但保持 EBP 不变)。这会导致帧丢失。应该注意的是,该方法设置的 EBP 帧标记的是 CALLER,而不是它本身。因此,如果方法 B 似乎丢失了,那不是因为 B 省略了其 EBP 帧,而是因为方法 C 省略了。因此,当方法 C 较小时,不会发生这种帧遗漏,而当方法 B 较小时,则不会发生这种帧遗漏。


虽然丢失的帧可能会造成混淆,从而减慢分析速度,但它们很少真正阻止它。  缺失帧是以非常低的开销方式分析未修改的代码所付出的代价。


 故障 排除


主视图疑难解答


Stack Viewer 故障排除


事件查看器疑难解答


 技巧


以下是一些有用的技术,起初可能并不明显:

 一般提示



常见问题 (FAQ))

 

 发行说明