关于 dotnet-profiling
dotnet-profiling 是用于诊断 .NET 应用性能问题的技能。它讲解 dotnet-counters、dotnet-trace 和 dotnet-dump 的使用,重点在于解读指标、火焰图和堆转储。
主要功能
- 01使用 dotnet-counters 监控运行时指标
- 02采集事件跟踪和 CPU 配置文件
- 03转换跟踪数据以分析火焰图
- 04跟踪分配并关联 GC 数据
- 05捕获并检查托管内存转储
适用场景
- 01判断性能问题受 CPU、内存还是 I/O 限制
- 02从 CPU 火焰图定位热点路径
- 03调查分配压力和 GC 活动
- 04追踪疑似内存泄漏中的对象引用
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ wshaddix/ dotnet-skills --skill dotnet-profiling安装后在 Claude Code 中输入/dotnet-profiling确认已加载。
详细介绍
dotnet-profiling
用于调查 .NET 性能问题的诊断工具指南。涵盖使用 dotnet-counters 实时监控指标、使用 dotnet-trace 进行事件跟踪并生成火焰图,以及使用 dotnet-dump 捕获和分析内存转储。重点介绍如何解读性能分析数据(读取火焰图、分析堆转储、关联 GC 指标),而不只是调用工具。
**版本假设:**以 .NET SDK 8.0+ 为基准。所有三种诊断工具(dotnet-counters、dotnet-trace、dotnet-dump)均随 .NET SDK 提供,无需单独安装。
**不在范围内:**OpenTelemetry 指标收集和分布式跟踪设置——请参阅 [skill:dotnet-observability]。微基准测试设置(BenchmarkDotNet)由本 Epic 的配套技能负责——请参阅 [skill:dotnet-benchmarkdotnet]。性能架构模式(Span<T>、ArrayPool、sealed 去虚拟化)由本 Epic 的配套技能负责——请参阅 [skill:dotnet-performance-patterns]。在 CI 中持续检测基准测试回归——请参阅 [skill:dotnet-ci-benchmarking]。架构模式(缓存、弹性)——请参阅 [skill:dotnet-architecture-patterns]。
交叉引用:[skill:dotnet-observability],了解 GC/线程池指标解读和 OpenTelemetry 关联;[skill:dotnet-benchmarkdotnet],了解在性能分析确定热点路径后进行结构化基准测试;[skill:dotnet-performance-patterns],了解如何根据性能分析结果应用优化模式。
dotnet-counters -- 实时指标监控
概述
dotnet-counters 无需修改应用程序代码,即可实时监控 .NET 运行时指标。可将其用作初步分诊工具,在使用更复杂的检测工具之前,先判断性能问题是受 CPU、内存还是 I/O 限制。
监控正在运行的进程
# List running .NET processes
dotnet-counters ps
# Monitor default runtime counters for a process
dotnet-counters monitor --process-id <PID>
# Monitor with a specific refresh interval (seconds)
dotnet-counters monitor --process-id <PID> --refresh-interval 2
主要内置计数器提供程序
| 提供程序 | 计数器 | 可反映的信息 |
|---|---|---|
System.Runtime | CPU 使用率、GC 堆大小、Gen 0/1/2 回收次数、线程池队列长度、异常计数 | 运行时整体状况 |
Microsoft.AspNetCore.Hosting | 请求速率、请求持续时间、活动请求数 | HTTP 请求吞吐量和延迟 |
Microsoft.AspNetCore.Http.Connections | 连接持续时间、当前连接数 | WebSocket/SignalR 连接负载 |
System.Net.Http | 已启动/失败的请求数、活动请求数、连接池大小 | 出站 HTTP 客户端行为 |
System.Net.Sockets | 已发送/接收的字节数、数据报、连接数 | 网络 I/O 量 |
监控特定提供程序
# Monitor runtime and ASP.NET counters together
dotnet-counters monitor --process-id <PID> \
--counters System.Runtime,Microsoft.AspNetCore.Hosting
# Monitor only GC-related counters
dotnet-counters monitor --process-id <PID> \
--counters System.Runtime[gc-heap-size,gen-0-gc-count,gen-1-gc-count,gen-2-gc-count]
自定义 EventCounters
应用程序可以发布自定义计数器,用于领域特定指标:
using System.Diagnostics.Tracing;
[EventSource(Name = "MyApp.Orders")]
public sealed class OrderMetrics : EventSource
{
public static readonly OrderMetrics Instance = new();
private EventCounter? _orderProcessingTime;
private IncrementingEventCounter? _ordersProcessed;
private OrderMetrics()
{
_orderProcessingTime = new EventCounter("order-processing-time", this)
{
DisplayName = "Order Processing Time (ms)",
DisplayUnits = "ms"
};
_ordersProcessed = new IncrementingEventCounter("orders-processed", this)
{
DisplayName = "Orders Processed",
DisplayRateTimeScale = TimeSpan.FromSeconds(1)
};
}
public void RecordProcessingTime(double milliseconds)
=> _orderProcessingTime?.WriteMetric(milliseconds);
public void RecordOrderProcessed()
=> _ordersProcessed?.Increment();
protected override void Dispose(bool disposing)
{
_orderProcessingTime?.Dispose();
_ordersProcessed?.Dispose();
base.Dispose(disposing);
}
}
监控自定义计数器:
dotnet-counters monitor --process-id <PID> --counters MyApp.Orders
解读计数器数据
利用计数器值指导后续调查。关于如何将这些运行时指标与 OpenTelemetry 跟踪关联,请参阅 [skill:dotnet-observability]:
| 症状 | 计数器证据 | 下一步 |
|---|---|---|
| CPU 使用率高 | cpu-usage > 80%,threadpool-queue-length 较低 | 使用 dotnet-trace 进行 CPU 性能分析 |
| 内存增长 | gc-heap-size 持续增加,频繁发生 Gen 2 GC | 使用 dotnet-dump 获取内存转储 |
| 线程饥饿 | threadpool-queue-length 持续增长,threadpool-thread-count 达到最大值 | 检查是否存在 sync-over-async 或阻塞调用 |
| 请求延迟 | request-duration 较高,active-requests 正常 | 使用 dotnet-trace 跟踪单个请求 |
| GC 暂停 | gen-2-gc-count 较高,time-in-gc > 10% | 使用 dotnet-trace gc-collect 进行分配性能分析 |
导出计数器数据
# Export to CSV for analysis
dotnet-counters collect --process-id <PID> \
--format csv \
--output counters.csv \
--counters System.Runtime
# Export to JSON for programmatic consumption
dotnet-counters collect --process-id <PID> \
--format json \
--output counters.json
dotnet-trace -- 事件跟踪与火焰图
概述
dotnet-trace 可捕获正在运行的 .NET 进程的详细事件跟踪。可将跟踪数据分析为火焰图,以识别 CPU 热点路径;也可配置为跟踪内存分配,以找出 GC 压力的来源。
CPU 采样
CPU 采样以固定间隔记录堆栈帧,从而构建应用程序耗时分布的统计画像:
# Collect a CPU sampling trace (default profile)
dotnet-trace collect --process-id <PID> --duration 00:00:30
# Collect with the cpu-sampling profile (explicit)
dotnet-trace collect --process-id <PID> \
--profile cpu-sampling \
--output cpu-trace.nettrace
CPU 采样与插桩
| 方式 | 开销 | 最适合的场景 | 工具 |
|---|---|---|---|
| CPU 采样 | 低(约 2-5%) | 在生产环境中查找 CPU 热点路径 | dotnet-trace --profile cpu-sampling |
| 插桩 | 高(10-50%+) | 精确统计调用次数、方法进入/退出时长 | Rider/VS 性能分析器、PerfView |
由于开销低,CPU 采样适合在生产环境中使用。请将其作为默认方式。仅在需要精确统计调用次数的开发环境中使用插桩。
生成火焰图
必须将跟踪文件(.nettrace)转换为火焰图格式,才能进行可视化分析:
使用 Speedscope(基于浏览器,推荐):
# Convert to Speedscope format
dotnet-trace convert cpu-trace.nettrace --format Speedscope
# Opens cpu-trace.speedscope.json -- load at https://www.speedscope.app/
使用 PerfView(Windows,深度集成 .NET):
# Convert to Chromium trace format (also viewable in chrome://tracing)
dotnet-trace convert cpu-trace.nettrace --format Chromium
阅读火焰图
火焰图展示调用堆栈,其中:
- 宽度表示某个函数占总采样时间的比例(越宽 = 耗时越长)
- 高度表示调用堆栈深度(堆栈越高 = 调用链越深)
- 颜色通常是任意的(没有实际含义),除非工具使用了特定的配色方案
分析工作流:
- 查找宽阔的平台——占用大量采样时间的函数
- 沿最宽的帧向上查找耗时最多的调用方
- 识别异常宽度——本应快速执行的框架方法如果显示较宽,说明存在误用
- 对比优化前后的跟踪结果,验证目标函数的宽度是否有所缩减
.NET 火焰图中的常见模式:
| 模式 | 可能原因 | 调查方法 |
|---|---|---|
较宽的 System.Linq 帧 | 热点路径大量使用 LINQ,并产生委托开销 | 替换为 foreach 循环或基于 Span 的处理 |
较宽的 JIT_New / gc_heap::allocate | 分配过多,导致触发 GC | 使用 --profile gc-collect 进行分配分析 |
较宽的 Monitor.Enter / SpinLock | 锁竞争 | 检查同步策略 |
较宽的 System.Text.RegularExpressions | 正则表达式回溯 | 使用 RegexOptions.NonBacktracking 或编译正则表达式 |
| 深层异步状态机帧 | 紧密循环中的异步开销 | 考虑为 CPU 密集型工作使用同步路径 |
使用 gc-collect 配置文件跟踪分配
gc-collect 配置文件会捕获分配事件,以识别哪些代码路径分配的内存最多:
# Collect allocation data
dotnet-trace collect --process-id <PID> \
--profile gc-collect \
--duration 00:00:30 \
--output alloc-trace.nettrace
生成的跟踪文件会显示:
- 分配字节数最多的方法
- 分配频率最高的类型
- 分配大小及触发分配的调用堆栈
将分配数据与 dotnet-counters 中的 GC 计数器证据关联起来。如果 gen-2-gc-count 较高,分配跟踪会显示哪些代码路径生成了存活时间较长、能存活到 Gen 2 的对象。请参阅 [skill:dotnet-performance-patterns],了解识别出分配热点后可应用的零分配模式。
自定义跟踪提供程序
针对特定事件提供程序进行跟踪,以聚焦于特定内容:
# Trace specific providers with keywords and verbosity
dotnet-trace collect --process-id <PID> \
--providers "Microsoft-Diagnostics-DiagnosticSource:::FilterAndPayloadSpecs=[AS]System.Net.Http"
# Trace EF Core queries (useful with [skill:dotnet-efcore-patterns])
dotnet-trace collect --process-id <PID> \
--providers Microsoft.EntityFrameworkCore
# Trace ASP.NET Core request processing
dotnet-trace collect --process-id <PID> \
--providers Microsoft.AspNetCore
跟踪文件管理
| 格式 | 扩展名 | 查看器 | 跨平台 |
|---|---|---|---|
| NetTrace | .nettrace | PerfView、VS、dotnet-trace convert | 是(采集);Windows(PerfView) |
| Speedscope | .speedscope.json | https://www.speedscope.app/ | 是 |
| Chromium | .chromium.json | Chrome DevTools (chrome://tracing) | 是 |
dotnet-dump — 内存转储分析
概述
dotnet-dump 用于捕获和分析进程内存转储。可用于调查内存泄漏、大对象堆碎片以及对象引用链。与 dotnet-trace 不同,转储会捕获整个托管堆在某一时刻的快照。
捕获转储
# Capture a full heap dump
dotnet-dump collect --process-id <PID> --output app-dump.dmp
# Capture a minimal dump (faster, smaller, but less detail)
dotnet-dump collect --process-id <PID> --type Mini --output app-mini.dmp
适用情况:
- 内存使用量已超过预期基线(与 dotnet-counters
gc-heap-size进行比较) - 应用程序即将出现 OOM
- 负载测试后怀疑存在内存泄漏
- 调查终结器队列积压
使用 SOS 命令分析转储
在交互式分析器中打开转储:
dotnet-dump analyze app-dump.dmp
!dumpheap -- 堆对象摘要
按类型对托管堆上的对象进行分组,并按总大小排序:
> dumpheap -stat
Statistics:
MT Count TotalSize Class Name
00007fff2c6a4320 125 4,000 System.String[]
00007fff2c6a1230 8,432 269,824 System.String
00007fff2c7b5640 2,100 504,000 MyApp.Models.OrderEntity
00007fff2c6a0988 15,230 1,218,400 System.Byte[]
分析方法:
- 查找数量或大小异常高的应用程序类型
- 将数量与预期基数进行比较(例如,有 2,100 个 OrderEntity 对象——这是否符合当前负载的预期?)
- 大量
System.Byte[]通常表示存在无界缓冲或流处理问题
按类型筛选:
> dumpheap -type MyApp.Models.OrderEntity
> dumpheap -type System.Byte[] -min 85000
-min 85000 筛选器会显示大对象堆条目(对象大小 >= 85,000 字节,会造成第 2 代 GC 压力)。
!gcroot -- 查找对象保留链
追踪从 GC 根到特定对象的引用链,说明该对象为何未被回收:
> gcroot 00007fff3c4a2100
HandleTable:
00007fff3c010010 (strong handle)
-> 00007fff3c3a1000 MyApp.Services.CacheService
-> 00007fff3c3a1020 System.Collections.Generic.Dictionary`2
-> 00007fff3c4a2100 MyApp.Models.OrderEntity
Found 1 unique root(s).
常见根类型及其含义:
| 根类型 | 含义 | 可能的问题 |
|---|---|---|
strong handle | 静态字段或 GC 句柄 | 静态集合持续增长且未进行清理 |
pinned handle | 为原生互操作而固定 | 缓冲区固定时间过长 |
async state machine | 在异步闭包中捕获 | 长时间运行的异步操作持有引用 |
finalizer queue | 等待终结器线程处理 | 终结器队列积压,阻碍回收 |
threadpool | 从线程本地存储中引用 | 线程静态缓存未清理 |
!finalizequeue -- 终结器队列分析
显示等待终结的对象,这会使它们至少延迟一个 GC 周期才被回收:
> finalizequeue
SyncBlocks to be cleaned up: 0
Free-Threaded Interfaces to be released: 0
MTA Interfaces to be released: 0
STA Interfaces to be released: 0
----------------------------------
generation 0 has 12 finalizable objects
generation 1 has 45 finalizable objects
generation 2 has 230 finalizable objects
Ready for finalization 8 objects
关键指标:
- “Ready for finalization”中的数量较高,表示终结器线程处理速度跟不上
- 第 2 代可终结对象列表中的对象开销较大——它们至少要经历两个 GC 周期(一个用于安排终结,另一个用于终结器运行后进行回收)
- 未调用
IDisposable.Dispose()的~Destructor()实现是主要原因
用于堆分析的其他 SOS 命令
| 命令 | 用途 | 适用情况 |
|---|---|---|
dumpobj <address> | 显示特定对象的字段值 | 使用 dumpheap 找到对象后检查其状态 |
dumparray <address> | 显示数组内容 | 调查堆统计中发现的大型数组 |
eeheap -gc | 显示 GC 堆段布局 | 调查 LOH 碎片 |
gcwhere <address> | 显示对象所在的 GC 代 | 确定对象是否已固定或位于 LOH |
dumpmt <MT> | 显示方法表详细信息 | 调查类型元数据 |
threads | 列出所有托管线程及其堆栈跟踪 | 识别死锁或阻塞 |
clrstack | 显示当前线程的托管调用堆栈 | 将线程状态与堆数据关联起来 |
内存泄漏调查工作流
- 基线: 在应用程序启动并完成初始预热后捕获转储
- 负载: 运行疑似导致泄漏的工作负载场景
- 比较: 工作负载完成后捕获第二份转储
- 差异: 比较两份转储中的
dumpheap -stat输出——查找数量或总大小显著增长的类型 - 根因: 对增长类型的实例使用
gcroot查找保留链 - 修复: 断开保留链(从静态集合中移除、释放事件订阅、修复异步生命周期问题)
# Tip: save dumpheap output for comparison
# In dump 1:
> dumpheap -stat > /tmp/heap-before.txt
# In dump 2:
> dumpheap -stat > /tmp/heap-after.txt
# Compare externally:
# diff /tmp/heap-before.txt /tmp/heap-after.txt
性能分析工作流总结
按照结构化的调查工作流使用诊断工具:
1. dotnet-counters (triage)
├── CPU high? → dotnet-trace --profile cpu-sampling
│ → Convert to flame graph (Speedscope)
│ → Identify hot methods
├── Memory growing? → dotnet-dump collect
│ → dumpheap -stat (find large/numerous types)
│ → gcroot (find retention chains)
│ → Fix retention + verify with second dump
├── GC pressure? → dotnet-trace --profile gc-collect
│ → Identify allocation hot paths
│ → Apply zero-alloc patterns [skill:dotnet-performance-patterns]
└── Thread starvation? → dotnet-dump analyze
→ threads (list all managed threads)
→ clrstack (check for blocking calls)
性能分析确定瓶颈后,使用 [skill:dotnet-benchmarkdotnet] 创建有针对性的基准测试,量化修复带来的改进。
Agent 常见问题
- 从 dotnet-counters 开始,而不是 dotnet-trace —— 计数器的开销几乎为零,并能识别问题类别(CPU、内存、线程)。只有在计数器缩小调查范围后,才使用 trace 或 dump。
- 在生产环境中使用 CPU 采样(而不是插桩) —— 采样开销为 2-5%,适用于生产环境。插桩会增加 10-50% 以上的开销,应仅限于开发环境。
- 始终将 trace 转换为火焰图进行分析 —— 阅读原始
.nettrace事件日志并不现实。使用dotnet-trace convert --format Speedscope,并在 https://www.speedscope.app/ 中打开进行可视化分析。 - 调查内存泄漏时捕获两份 dump —— 单份 dump 只能显示当前状态,无法区分正常驻留对象与泄漏对象。比较疑似泄漏场景发生前后捕获的两份 dump 的堆统计信息。
- 使用
-min 85000筛选 dumpheap,以查找 LOH 对象 —— 大于等于 85,000 字节的对象会进入大型对象堆(LOH),该堆只会在 Gen 2 GC 时回收。LOH 对象数量较多可能表明存在内存碎片。 - 结合 [skill:dotnet-observability] 解读 GC 计数器数据 —— 运行时 GC/线程池计数器与 OpenTelemetry 指标有所重叠。使用可观测性技能,将性能分析结果与分布式 trace 上下文关联起来。
- 不要混淆 dotnet-trace gc-collect 和 dotnet-dump —— gc-collect 会跟踪一段时间内的分配事件(哪些方法执行了分配);dotnet-dump 会捕获某一时刻的堆快照(存在哪些对象)。使用 gc-collect 分析分配速率;使用 dotnet-dump 分析对象保留情况和内存泄漏。