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
Background and motivation
n .NET 9, the runtime introduced excellent built-in metrics via the
System.Runtimemeter, includingdotnet.gc.pause.time(which maps toGC.GetTotalPauseDuration()). This is a great addition for measuring the overall percentage of time an application spends in GC.However,
dotnet.gc.pause.timeis implemented as a single, cumulativeCounter<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:
To get this data today, developers (and telemetry libraries like OpenTelemetry) are forced to build custom
EventListenerstate machines to correlateGCSuspendEEBegin,GCStart, andGCRestartEEEndevents. 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.Runtimemeter 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:
dotnet.gc.pause.duration(or similar)Histogram<double>s(seconds)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.Runtimemeter.API Usage
Alternative Designs
Other major language runtimes already expose this exact telemetry natively, which has allowed OpenTelemetry to standardize it in their Semantic Conventions:
jvm.gc.durationas a Histogram tagged with the generation (jvm.gc.namee.g.,G1 Young GenerationvsG1 Old Generation) and action.v8js.gc.durationas a Histogram tagged withv8js.gc.type(e.g.,Minorfor scavenge vsMajorfor mark-sweep-compact).Adding this to
System.Runtimewould 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.EventListener: Building anEventListenerin 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.dotnet.gc.pause.timecounter: 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