Skip to content

[API Proposal]: Native GC pause duration histogram with generation and type attributes in System.Runtime metrics #125753

Description

@AguirreMoy

Background and motivation

n .NET 9, the runtime introduced excellent built-in metrics via the System.Runtime meter, including dotnet.gc.pause.time (which maps to GC.GetTotalPauseDuration()). This is a great addition for measuring the overall percentage of time an application spends in GC.

However, dotnet.gc.pause.time is implemented as a single, cumulative Counter<double> representing the total sum of all pauses. For deep performance tuning and diagnosing latency spikes, a cumulative total is insufficient. Engineers need to know the distribution of these pauses and, critically, what caused them.

Currently, there is no built-in metric that answers:

  • "Was this 500ms of GC time caused by 500 sub-millisecond Gen 0 collections, or a single 500ms Blocking Gen 2 collection?"
  • "Are my Gen 2 collections Background (short STW phases) or Blocking/Non-Concurrent (massive STW phases)?"

To get this data today, developers (and telemetry libraries like OpenTelemetry) are forced to build custom EventListener state machines to correlate GCSuspendEEBegin, GCStart, and GCRestartEEEnd events. This incurs unnecessary managed overhead (parsing ETW/EventPipe payloads) for data the native GC engine already calculates internally.

Proposed Solution

I propose adding a new Histogram metric to the System.Runtime meter that records individual Stop-The-World (STW) pause durations, enriched with tags for the generation and the GC type.

Because the native GC engine knows when it stops/starts the execution engine, the target generation, and whether the GC is background or blocking, it could emit this Histogram directly with minimal overhead.

Proposed Metric:

  • Name: dotnet.gc.pause.duration (or similar)
  • Instrument: Histogram<double>
  • Unit: s (seconds)
  • Description: "Distribution of individual garbage collection pause durations."

Proposed Attributes (Tags):

  • gc.heap.generation: "gen0", "gen1", "gen2", "loh", "poh"
  • gc.pause.type: "blocking", "background" (or "concurrent", "non-concurrent")

API Proposal

There is no new public C# API surface (no new classes or methods in the BCL). The proposed API is the standardized metric schema emitted by the built-in System.Runtime meter.

// Meter: System.Runtime
// Instrument: Histogram<double>
// Name: dotnet.gc.pause.duration
// Unit: s
// Description: Distribution of individual garbage collection pause durations.

// Required Tags
// -------------
// Name: gc.heap.generation
// Type: string
// Values: "gen0", "gen1", "gen2", "loh", "poh"

// Name: gc.pause.type
// Type: string
// Values: "blocking", "background", "concurrent"

API Usage

using System.Diagnostics.Metrics;

var meterListener = new MeterListener();
meterListener.InstrumentPublished = (instrument, listener) =>
{
    if (instrument.Meter.Name == "System.Runtime" && instrument.Name == "dotnet.gc.pause.duration")
    {
        listener.EnableMeasurementEvents(instrument);
    }
};

meterListener.SetMeasurementEventCallback<double>((instrument, measurement, tags, state) =>
{
    Console.WriteLine($"Pause: {measurement} seconds");
    
    foreach (var tag in tags)
    {
        Console.WriteLine($"  {tag.Key}: {tag.Value}");
        // Example output:
        //   gc.heap.generation: gen2
        //   gc.pause.type: blocking
    }
});

meterListener.Start();

// Example Console Output:
// Pause Duration: 0.250s
//   gc.heap.generation: gen2
//   gc.pause.type: blocking

Alternative Designs

Other major language runtimes already expose this exact telemetry natively, which has allowed OpenTelemetry to standardize it in their Semantic Conventions:

  • Java (JVM): Exposes jvm.gc.duration as a Histogram tagged with the generation (jvm.gc.name e.g., G1 Young Generation vs G1 Old Generation) and action.
  • Node.js (V8): Exposes v8js.gc.duration as a Histogram tagged with v8js.gc.type (e.g., Minor for scavenge vs Major for mark-sweep-compact).

Adding this to System.Runtime would bring .NET to full feature parity with Java and Node.js in the observability ecosystem, allowing for out-of-the-box dashboards that highlight latency-killing blocking collections without requiring developers to write custom ETW listeners.

  • Keep relying on EventListener: Building an EventListener in APM tools/OpenTelemetry is possible, but it requires allocating and parsing event payloads in managed code to calculate durations. Tapping into the discrete pause duration exactly when the C++ GC calculates it is much more efficient.
  • Adding tags to the existing dotnet.gc.pause.time counter: Counters are great for rates, but Histograms are the industry standard for measuring latency/duration distributions (e.g., p95, p99 pause times).

Risks

No response

No activity

Activity on this issue will appear here.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

  • Status
    No status

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions