perfview 是基于 etw 的高性能事件采集分析器,专治 gc 频繁、jit 开销大、线程阻塞、异步堆积、内存泄漏、threadpool 饱和等 .net 难定位性能问题;它无插桩、低开销、可捕获 clr 内部事件。

PerfView 是什么,它能帮你解决哪类 C# 性能问题
PerfView 不是传统意义上的“ profiler UI 工具”,而是一个基于 ETW(Event Tracing for Windows)的高性能事件采集与分析器。它特别适合诊断 .NET 应用中那些“跑得慢但看不出卡在哪”的问题:比如 GC 频繁、JIT 编译开销大、线程阻塞、异步等待堆积、内存泄漏嫌疑、甚至 ThreadPool 饱和导致的吞吐下降。
它不依赖代码插桩,对运行时影响极小(尤其在采样模式下),且能捕获 CLR 内部事件(如 GCStart、JitJittedMethodILToNativeMap、ThreadPoolWorkerThreadStart),这是很多第三方工具做不到的。
如何启动一次最小可行的 PerfView 录制(.NET 6+ / .NET Core 场景)
- 确保目标进程是 .NET 6 或更高版本(旧版需额外配置
DOTNET_STARTUP_HOOKS 或启用 LegacyETW)
- 下载最新
PerfView.exe(无需安装,单文件),以管理员权限运行(部分事件需要)
- 使用命令行启动录制,避免 GUI 操作引入干扰:
PerfView.exe collect -CollectMultiple -NoGui -CircularMB:512 -Merge:true -Zip:true MyApp.exe
- 其中关键参数:
-
-CollectMultiple:同时采集多个进程(含子进程),适合 ASP.NET Core 自托管或带子进程的场景
-
-CircularMB:512:环形缓冲区大小,防止磁盘爆满;实际分析时再加 -LogFile:xxx.etl.zip 指定输出
-
-Merge:true:自动合并多进程事件,避免手动关联
- 不要勾选 “Include Input Events” 或 “Heap Dump”,除非你明确需要 UI 输入延迟或对象快照——它们显著增加开销和体积
打开 etl.zip 后,该看哪几个视图定位 C# 瓶颈
DOTNET_STARTUP_HOOKS 或启用 LegacyETW)PerfView.exe(无需安装,单文件),以管理员权限运行(部分事件需要)PerfView.exe collect -CollectMultiple -NoGui -CircularMB:512 -Merge:true -Zip:true MyApp.exe
-
-CollectMultiple:同时采集多个进程(含子进程),适合 ASP.NET Core 自托管或带子进程的场景 -
-CircularMB:512:环形缓冲区大小,防止磁盘爆满;实际分析时再加-LogFile:xxx.etl.zip指定输出 -
-Merge:true:自动合并多进程事件,避免手动关联
PerfView 默认打开的是“Events”视图,但真正高效分析要切到这几个标签页:
-
GC Heap Alloc View:直接显示每个方法分配了多少字节,按
Alloc MB排序,一眼揪出高频临时对象创建点(比如string.Concat、Enumerable.ToList、JSON 序列化中的中间字符串) -
Call Tree(by Weight):右键选择 “Group Pats → By Module → By Method”,展开后重点关注:
- 耗时占比高的
clr!JIT_*调用 → JIT 编译热点(可能因泛型爆炸或动态代码) - 大量
coreclr!SVR::gc_heap::allocate_more_space→ GC 压力大,配合 GC Heap Alloc 看谁在猛分配 - 线程长时间停在
ntdll!NtWaitForSingleObject→ 查看其调用栈上层是否为Task.Wait、GetAwaiter().GetResult()或锁竞争
- 耗时占比高的
-
Processes → [YourApp] → Threads:观察线程状态分布,若大量线程处于 “Blocked” 或 “Sleeping” 且堆栈指向
Monitor.Enter、ConcurrentQueue<t>.Enqueue</t>,说明同步瓶颈明显
常见误操作和容易被忽略的细节
- 直接双击 .exe 启动 PerfView GUI 再点 “Collect” → 很可能漏掉进程启动初期的关键事件(如 JIT、AssemblyLoad)。必须用命令行 +
collect 参数启动目标进程
- 在 .NET 5+ 中未设置环境变量就尝试抓取 GC 详细事件 → 需提前运行:
set COMPLUS_GCStress=0 & set DOTNET_gcServer=1
(后者确保服务端 GC 开启,否则 GCHeapStats 事件不全)
- 把
PerfView.exe 放在中文路径下运行 → 某些版本会因路径编码失败静默退出,一律用英文路径
- 分析 ASP.NET Core 应用时没启用
Microsoft-AspNetCore-Server-IIS 或 Microsoft-Extensions-Logging provider → 导致无法关联请求生命周期,应在录制命令中加:-Providers:Microsoft-AspNetCore-Server-Kestrel;Microsoft-Extensions-Logging
collect 参数启动目标进程set COMPLUS_GCStress=0 & set DOTNET_gcServer=1(后者确保服务端 GC 开启,否则
GCHeapStats 事件不全)PerfView.exe 放在中文路径下运行 → 某些版本会因路径编码失败静默退出,一律用英文路径Microsoft-AspNetCore-Server-IIS 或 Microsoft-Extensions-Logging provider → 导致无法关联请求生命周期,应在录制命令中加:-Providers:Microsoft-AspNetCore-Server-Kestrel;Microsoft-Extensions-Logging
PerfView 的深度不在界面有多炫,而在你能看到 CLR 运行时“没说出口”的话。真正难的不是采集,而是读懂那些看似杂乱的事件链——比如一个 ThreadPoolWorkerThreadStart 后紧跟着三次 ThreadPoolWorkerThreadStop,往往意味着线程被频繁销毁重建,背后可能是 Task.Run 被滥用或 async void 导致异常逃逸。











