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

来源:这里教程网 时间:2026-02-21 17:37:24 作者:

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
不支持任意对象序列化。它只接受
int
string
Guid
DateTime
long
、枚举、以及标记
[EventData]
的简单
struct
。传入 class 实例、
Dictionary<string object></string>
或匿名类型会直接抛出
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);
}

这样在低开销采集中,可只保留

path
statusCode
,彻底跳过
userAgent
字符串的复制与写入。

真正容易被忽略的是:这种裁剪只在 ETW 层生效;如果你用

EventListener
在进程内监听,所有字段仍会传入,裁剪无效。所以生产环境高频打点,务必搭配外部工具(PerfView / Windows Performance Recorder)采集,而非依赖进程内监听。

相关推荐