EventSource in C#: High-Performance Event Tracing
This article provides a comprehensive overview of
EventSource in C#, explaining its architecture and role in
the .NET diagnostics ecosystem. You will learn how to define custom
structured events, consume them using in-process and out-of-process
tools, and apply advanced optimization techniques—such as bypassing
object allocation with WriteEventCore—to achieve minimal
runtime overhead in performance-critical applications.
What is EventSource?
EventSource is a core class residing in the
System.Diagnostics.Tracing namespace. It allows developers
to generate structured, strongly-typed event tracing data from .NET
applications.
Unlike traditional text-based logging frameworks (such as log4net or
Serilog writing strings to disk), EventSource is designed
for ultra-low overhead telemetry. It integrates natively with
platform-level tracing mechanisms:
- Windows: Integrates with Event Tracing for Windows (ETW), routing events directly to the operating system kernel buffer.
- Linux/macOS: Emits events via EventPipe, which can
be captured using cross-platform CLI tools like
dotnet-trace.
Events generated by EventSource are self-describing,
structured, and can be enabled or disabled dynamically at runtime
without restarting the application.
Implementing a Basic EventSource
To implement an event source, create a class that derives from
EventSource, apply the [EventSource]
attribute, define event methods, and expose a thread-safe singleton
instance.
using System.Diagnostics.Tracing;
[EventSource(Name = "Company-Application-OrderService")]
public sealed class OrderEventSource : EventSource
{
public static readonly OrderEventSource Log = new OrderEventSource();
private OrderEventSource() : base(EventSourceSettings.EtwSelfDescribingEventFormat) { }
[Event(1, Level = EventLevel.Informational, Message = "Order {0} received.")]
public void OrderReceived(long orderId)
{
if (IsEnabled())
{
WriteEvent(1, orderId);
}
}
[Event(2, Level = EventLevel.Warning, Message = "Order {0} failed: {1}")]
public void OrderFailed(long orderId, string reason)
{
if (IsEnabled())
{
WriteEvent(2, orderId, reason);
}
}
}Key Rules for Event Methods:
- Event IDs: Every method marked with
[Event]must have a unique integer ID passed toWriteEvent. - Parameters: Parameters passed to the method must
match the parameters passed to
WriteEventin both count and type. - IsEnabled Check: Always wrap calls in
IsEnabled()to avoid unnecessary argument evaluation or boxing when tracing is inactive.
High-Performance Optimization: Zero-Allocation Tracing
While standard WriteEvent calls are fast, overloads that
accept more than a few arguments—or non-primitive types—often fall back
to WriteEvent(int, params object[]), causing heap
allocations and boxing.
For performance-critical code paths, use WriteEventCore
combined with stackalloc and the unsafe
keyword to eliminate memory allocations entirely.
using System;
using System.Diagnostics.Tracing;
[EventSource(Name = "Company-Application-OrderService")]
public sealed class HighPerformanceOrderEventSource : EventSource
{
public static readonly HighPerformanceOrderEventSource Log = new HighPerformanceOrderEventSource();
private HighPerformanceOrderEventSource() : base(EventSourceSettings.EtwSelfDescribingEventFormat) { }
[Event(3, Level = EventLevel.Informational, Message = "Order processed.")]
public unsafe void OrderProcessed(long orderId, double processingTimeMs, int itemsCount)
{
if (!IsEnabled()) return;
// Allocate the descriptor array on the stack (zero heap allocation)
EventData* descrs = stackalloc EventData[3];
descrs[0].DataPointer = (IntPtr)(&orderId);
descrs[0].Size = sizeof(long);
descrs[1].DataPointer = (IntPtr)(&processingTimeMs);
descrs[1].Size = sizeof(double);
descrs[2].DataPointer = (IntPtr)(&itemsCount);
descrs[2].Size = sizeof(int);
WriteEventCore(3, 3, descrs);
}
}Why WriteEventCore is
Faster:
- No Garbage Collection Pressure: Memory for
descriptors is allocated directly on the call stack using
stackalloc. - No Value-Type Boxing: Primitives are passed by
pointer reference rather than cast to
object. - Direct Memory Blitting: The underlying runtime reads memory blocks sequentially.
Consuming Tracing Events
EventSource events can be captured either in-process or
out-of-process.
1. In-Process
Consumption (EventListener)
You can consume events within the same process by subclassing
EventListener:
using System;
using System.Diagnostics.Tracing;
public sealed class ConsoleEventListener : EventListener
{
protected override void OnEventSourceCreated(EventSource eventSource)
{
base.OnEventSourceCreated(eventSource);
if (eventSource.Name == "Company-Application-OrderService")
{
EnableEvents(eventSource, EventLevel.Verbose, EventKeywords.All);
}
}
protected override void OnEventWritten(EventWrittenEventArgs eventData)
{
Console.WriteLine($"[Event {eventData.EventId}] {eventData.EventName} - Payload: {string.Join(", ", eventData.Payload ?? Array.Empty<object>())}");
}
}2. Out-of-Process Consumption
Out-of-process tracing allows production debugging with negligible impact on CPU and memory:
Cross-Platform: Collect traces using
dotnet-trace:dotnet-trace collect --name YourApp --providers Company-Application-OrderServiceWindows: Capture events using PerfView or the Windows Performance Toolkit (WPT) by enabling the provider name matching your
[EventSource(Name = "...")].
Best Practices Checklist
- Use the Singleton Pattern: Keep a single instance
per event source
(
public static readonly MySource Log). - Leverage Keywords and Tasks: Organize high-volume
and low-volume events using the
KeywordsandTasksproperties of the[Event]attribute. - Guard with
IsEnabled(): Ensure conditions prevent string concatenation or object resolution when providers are inactive. - Adopt
WriteEventCorefor Hot Paths: Eliminate allocation overhead inside tight loops or high-throughput request pipelines.