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:

  1. Event IDs: Every method marked with [Event] must have a unique integer ID passed to WriteEvent.
  2. Parameters: Parameters passed to the method must match the parameters passed to WriteEvent in both count and type.
  3. 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-OrderService
  • Windows: 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 Keywords and Tasks properties of the [Event] attribute.
  • Guard with IsEnabled(): Ensure conditions prevent string concatenation or object resolution when providers are inactive.
  • Adopt WriteEventCore for Hot Paths: Eliminate allocation overhead inside tight loops or high-throughput request pipelines.