Memory graphs
CollectMemoryGraphAsync is the API you'll reach for most often. It captures a snapshot of the managed heap in the standard .gcdump format — the same format produced by dotnet-gcdump collect.
The basic capture
using System.Diagnostics;
using Memory.Introspect;
int pid = Process.GetCurrentProcess().Id;
var introspector = MemoryIntrospector.Create();
var result = await introspector.CollectMemoryGraphAsync(pid);
if (result.Success)
{
result.SaveToDisk("snapshot.gcdump");
}
result.Success is the single field to check before doing anything else with the result. On failure, result.Exception carries the underlying error and result.Cancelled / result.Timeouted / result.NoHeapFound indicate why.
Reading the graph in memory
You don't have to write the dump to disk. result.Graph is a MemoryGraph object you can walk directly:
var graph = result.Graph;
graph.AllowReading();
Console.WriteLine($"Node count: {graph.NodeIndexLimit}");
Console.WriteLine($"Total size: {graph.TotalSize:N0} bytes");
This is the right path for "trigger a capture and feed a metric to Prometheus" scenarios — no temporary files required.
Capturing under a metric
A common pattern is to fire a capture when working-set or LOH size crosses a threshold:
public class HeapWatcher : BackgroundService
{
readonly long _thresholdBytes;
readonly MemoryIntrospector _introspector;
readonly ILogger<HeapWatcher> _logger;
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
long total = GC.GetTotalMemory(forceFullCollection: false);
if (total > _thresholdBytes)
{
_logger.LogWarning("Managed heap above threshold ({Bytes:N0}) — capturing", total);
var result = await _introspector.CollectMemoryGraphAsync(
Process.GetCurrentProcess().Id,
stoppingToken);
if (result.Success)
{
var file = $"heap-{DateTimeOffset.UtcNow:yyyy-MM-dd-HHmmss}.gcdump";
result.SaveToDisk(file);
_logger.LogWarning("Wrote {File}", file);
}
}
await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
}
}
}
Don't capture too frequently — each .gcdump triggers a full GC and pauses the runtime for the duration of the heap walk. Once per minute, gated on a threshold, is a reasonable upper bound for production code.
Large heaps
For processes with very large heaps, two options help:
var introspector = MemoryIntrospector.Create(new()
{
ExpectLargeGraph = true, // tells MemoryGraph to expect millions of nodes
MaxNodeCount = 50_000_000, // raise the node limit (default: 10M)
CircularBufferSizeInMB = 2048, // bigger EventPipe buffer
Timeout = TimeSpan.FromMinutes(10),
});
MaxNodeCount is a hard ceiling — captures that would exceed it return Success = false with NoHeapFound = true. Increase it (and your timeout) for heaps with tens of millions of objects.
Cancellation
CollectMemoryGraphAsync accepts a CancellationToken. Cancelling mid-capture cleanly tears down the EventPipe session and returns a result.Cancelled = true outcome.
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(60));
var result = await introspector.CollectMemoryGraphAsync(pid, cts.Token);
Note that the Timeout option on MemoryIntrospectorOptions is a minimum — the library enforces a floor of 30 seconds because shorter timeouts rarely complete a capture against a non-trivial heap.
Common pitfalls
Captures trigger a GC
The heap walk runs at the end of a GC pause. Don't capture inside a latency-sensitive request path — schedule it on a background loop or behind a feature flag.
Check the cache directory location
By default, EventPipe sessions communicate via a socket under /tmp (Linux/macOS) or a named pipe (Windows). On constrained containers, ensure /tmp is writable and not noexec — the .NET runtime needs it for diagnostics.