Repository navigation
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<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: blockingAlternative 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.durationas a Histogram tagged with the generation (jvm.gc.namee.g.,G1 Young GenerationvsG1 Old Generation) and action. - Node.js (V8): Exposes
v8js.gc.durationas a Histogram tagged withv8js.gc.type(e.g.,Minorfor scavenge vsMajorfor 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 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. - Adding tags to the existing
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
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsNo status
Activity