使用火焰图分析.NET的 start-up 期间花费的时间
本文将告诉你从“dotnet run”到“hello world”,如何使用 PerfView 来分析 .NET 运行时在启动期间所花费的时间。
代码示例
在这个示例中,我故意只关注.NET运行时在程序启动(start-up)过程中的操作,以确保运行的用户代码最少,因此只有以下的“Hello World”:
using System;
namespace HelloWorld {
class Program {
static void Main(string[]args) {
Console.WriteLine("Hello World!");
Console.WriteLine("Press <ENTER> to exit");
Console.ReadLine();
}
}
}
添加
Console.ReadLine()
是为了确保在 PerfView 仍在收集数据时进程不会退出。数据收集
PerfView 是一个非常强大的程序,但不是最易于使用的工具,因此我写了一个逐步指南:
- 下载并运行最新版本的'PerfView.exe'
- 点击“Run a command(运行命令)”或(Alt-R)并选择“collect data while the command is running(在命令运行时收集数据)”
-
确保已输入以下值:
- “Command”(命令)
- “Current Dir”(当前目录)
- 如果尚未选中“Cpu Samples(CPU采样)”,那请选中它,确保这选项是打勾的
- 将“Max Collect Sec(最长收集时间)”设置为15秒(因为我们的“HelloWorld”应用程序永远不会退出,我们需要确保PerfView在某个时刻停止收集数据)
- 确保选择了“.NET Symbol Collection(.NET符号收集)”
- 点击“Run Command(运行命令)”
然后,如果您检查日志,您会看到它正在收集数据,获取符号,然后最终将所有内容写入一个.zip文件。完成后,您应该在主界面的左侧窗格中看到新创建的文件,本例中称为“PerfViewData.etl.zip”。
数据处理
一旦你有了“.etl.zip”文件,双击它,您将看到一个树视图,显示所有可用的数据。现在,选择“CPU Stacks”,您将看到以下视图:
注意列表中有很多“?”字符,这意味着PerfView无法解析运行时dll所需的方法名称。让我们修复这个问题:
- 打开“CPU Stacks”
- 在列表中,选择“HelloWorld”进程(PerfView在整个机器上收集数据)
- 在“GroupPats”下拉菜单中,选择“[no grouping]”
- 可选项,将“Symbol Path”从默认值更改为其他值
- 在“By name”选项卡中,按下“Ctrl+A”选择所有行
- 右键单击并选择“Lookup Symbols”(或按下“Alt+S”)
现在,“CPU Stacks”视图应该类似于以下内容:
最后,我们可以获得我们想要的数据:
- 选择“Flame Graph”选项卡
-
将“GroupPats”更改为以下选项之一,以获得更好的火焰图:
- [group module entries] {%}!=>module $1
- [group class entries] {%!*}.%(=>class $1;{%!*}::=>class $1
- 将“Fold%”更改为更高的数字,例如3%,以去除那些细小的条(过高的数字会导致丢失信息)
现在,我实际上建议将PerfView数据导出为可以加载到 https://speedscope.app/ 中的格式,因为它可以提供更好的体验。要做到这一点,请单击“文件”->“另存为视图”,然后在“另存为类型”框中选择“Speed Scope Format”。完成后,你可以在speedscope.app上“浏览”该文件。
注意:如果你以前从了解到过“火焰图”,我强烈建议阅读 Julia Evans 关于 perf & flamegraphs 的精彩解释:
这张图的大体意思是:
火焰图是可视化分析数据的一种很棒的方法,由 Brendan Gregs 推广。
火焰图是通过从程序中采样的大量(通常是数千)堆栈跟踪构建的。上面火焰图 意味着 40% 的堆栈跟踪以“main -> panda”开始,32% 以“main -> alligator -> teeth”开始
要生成火焰图,可以获取 “http://github.com/brendangregg/Flamegraph” 并将它放入你的路径中。下面是如何生成火焰图。$ sudo perf script | stackcollapse -perf.pl | flamegraph.pl > graph.svg
分析性能数据的 3 种方法
分析.NET Runtime的Startup
最后,我们可以回答最初的问题:
.NET Runtime在启动期间花费时间较多的地方是哪里?
以下是火焰图中汇总为文本的数据,其中包含“.NET Core Runtime”源代码中相应函数的链接:
- 整个应用程序 - 100% - 233.28毫秒
- 除了
helloworld!wmain
之外的所有内容 - 21% -
helloworld!wmain
- 79% - 184.57毫秒hostpolicy!create_hostpolicy_context
- 30% - 70.92毫秒(create_hostpolicy_context函数)-
hostpolicy!create_coreclr
- 22% - 50.51毫秒(create_coreclr 函数coreclr!CorHost2::Start
- 9% - 20.98毫秒(CorHost2::Start 函数)coreclr!CorHost2::CreateAppDomain
- 10% - 23.52毫秒(CorHost2::CreateAppDomain 函数)
-
hostpolicy!runapp
- 20% - 46.20毫秒(runapp 函数),最终调用Assembly::ExecuteMainMethod
(Assembly::ExecuteMainMethod 函数)coreclr!RunMain
- 9.9% - 23.12毫秒(RunMain 函数)coreclr!RunStartupHooks
- 8.1% - 19.00毫秒(RunStartupHooks 函数)
hostfxr!resolve_frameworks_for_app
- 3.4% - 7.89毫秒(fx_resolver_t::resolve_frameworks_for_app 函数)
因此,运行时花费时间最多的地方是:
- 30%的总时间用于启动runtime,由“host policy”控制,其中大部分发生在
hostpolicy!create_hostpolicy_context
中(总时间的30%) - 22%的时间用于初始化runtime本身以及它创建的初始(也是唯一的)
AppDomain
,这可以在CorHost2::Start
(native 本地)和CorHost2::CreateAppDomain
(managed 托管)中看到。有关这方面的更多信息,请参阅CLR在执行一行代码之前所做的68件事情 - 20%用于 JIT编译和执行 “Hello World”代码示例中的
Main
方法,这是从上面的Assembly::ExecuteMainMethod
中开始的。
为了确认最后一点,我们可以返回到 PerfView 并查看它生成的“JIT Stats Summary(JIT统计摘要)”。从主菜单的“Advanced Group”->“JIT Stats”下,我们可以看到23.1毫秒或总CPU时间的9.1%花在了JIT编译上。