Memory.Introspect

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:

app/Diagnostics/HeapWatcher.cs
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.

Where next?

Analyzing output

Open the .gcdump in PerfView or Visual Studio.

Sampling profiles

Capture CPU samples — same wrapper, different API.

© 2026 Memory.Introspect. All rights reserved.