c# ETW事件和 EventSource 在性能分析中的作用

14次阅读

ETW是windows内核级高性能事件追踪子系统,采用内核缓冲区、环形队列和延迟写入设计,EnableTrace启用后单次事件写入开销极低。

c# ETW事件和 EventSource 在性能分析中的作用

ETW 是 windows 上真正低开销的事件采集机制

ETW(Event Tracing for Windows)不是 .net 专属,而是 Windows 内核级的高性能事件追踪子系统。它用内核缓冲区 + 环形队列 + 延迟写入设计,EnableTrace 开启后,单次事件写入通常仅需 Console.WriteLine 或日志库的毫秒级开销。关键在于:它不依赖托管堆分配、不触发 GC、不走 .NET 日志管道。

实际使用中,EventSource 类只是 ETW 的 .NET 封装层,它在运行时自动生成 ETW provider GUID,并把 WriteEvent 调用翻译为 EventWriteTransfer 等原生 ETW API。这意味着你写的 EventSource 代码,最终跑的是 Windows 原生事件路径。

常见误区是认为“加了 [EventSource(Name = "MyApp")] 就自动高性能”——其实不然。如果在事件方法里做了字符串拼接、对象序列化、或调用了 ToString(),这些操作仍在用户态执行,会显著拖慢吞吐。性能收益只在“事件写入”环节,前置计算仍由你负责。

EventSource.WriteEvent 的参数传递必须是原始类型或结构体

EventSource 不支持任意对象序列化。它只接受 intstringGuidDateTimelong、枚举、以及标记 [EventData] 的简单 struct。传入 class 实例、Dictionary 或匿名类型会直接抛出 ArgumentException:“The event field type is not supported.”

典型错误写法:

public void LogRequest(HttpRequest req) {     WriteEvent(1, req.Path, req.Method); // ❌ req.Path 可能是 null 或复杂属性链 }

正确做法是提前提取值,且避免空引用:

  • req?.Path ?? "(null)" 替代 req.Path
  • 不要传 req.Headers,而应传 req.Headers.Count 或预提取关键 header 值
  • 若需结构化数据,定义轻量 struct 并用 [NonEvent] 方法做转换

用 PerfView 捕获 EventSource 事件时要注意 Provider 名称匹配

PerfView 默认只收集已知 provider(如 Microsoft-Windows-DotNETRuntime),你自定义的 EventSource 必须显式启用。Provider 名称默认是类名全限定名,但可通过构造函数覆盖:

public sealed class MyEventSource : EventSource {     public static MyEventSource Log = new MyEventSource();     private MyEventSource() : base("MyCompany-MyApp") { } // ✅ 显式指定名称 }

启动 PerfView 时,必须在 “Collect → Additional Providers” 中填入:MyCompany-MyApp:0x10000:5(其中 0x10000 是 Level=Verbose,5 是 Keyword=All)。漏掉冒号或关键字位会导致事件完全不出现。

另一个常见问题:程序启动后才打开 PerfView,会错过初始化阶段的事件(如 EventSource 自身的 ManifestData 事件)。建议先在 PerfView 中点击 “Collect”,再启动目标进程。

高频率场景下必须用 EventSourceMessageAttribute 控制字段裁剪

当每秒写入数千次事件时,即使参数是原始类型,字符串字段仍可能成为瓶颈。ETW 对每个事件的大小有限制(默认约 64KB),但更现实的瓶颈是内存拷贝和内核缓冲区竞争。

[EventSourceMessage] 不是装饰用的——它让编译器在生成 manifest 时把字段标记为可选(eventFieldAttr="Optional"),配合 PerfView 的 “Filter Events” 或 TraceEvent 库的 Filter,可在采集时跳过未启用的字段,减少序列化开销。

例如:

[Event(1, Level = EventLevel.Informational)] public void RequestStarted(     string path,     [EventSourceMessage] string userAgent, // ✅ 可被过滤掉     int statusCode) {     WriteEvent(1, path, userAgent, statusCode); }

这样在低开销采集中,可只保留 pathstatusCode,彻底跳过 userAgent 字符串的复制与写入。

真正容易被忽略的是:这种裁剪只在 ETW 层生效;如果你用 EventListener 在进程内监听,所有字段仍会传入,裁剪无效。所以生产环境高频打点,务必搭配外部工具(PerfView / Windows Performance Recorder)采集,而非依赖进程内监听。

text=ZqhQzanResources