dotnet / dotnet/runtime

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

Open
#125,753 0 comments 2 reactions 1 assignee Claimed by @matyaskollert View on GitHub
area-GC-coreclr
Dominant language
C#
Stars
18.3k
Forks
5.6k
PR merge metrics
PR metrics pending

Description

### 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` 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`
* **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.

```csharp
// Meter: System.Runtime
// Instrument: Histogram
// 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

```csharp
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((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_

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.