Memory & Garbage Collection · Lesson 17

A profiling plan starts with a reproducible workload

Instrument logical work before selecting target-specific memory tools.

Explore the interactive Memory Lab

Change owners, step through generations, pinning and finalization, or compare memory under load. The model explains concepts; the SDK examples measure real runtime observations.

Open the Memory Lab ↗

The memory contract

A memory investigation needs a reproducible workload before it needs a clever profiler screenshot. State the input, expected output, operation lifetime, and symptom: excessive allocation rate, growing retention, long pauses, or scarce-resource exhaustion are different problems. Logical counters can verify that two runs performed comparable work. They are not substitutes for memory counters. This lesson uses a small report pipeline to establish that distinction and then outlines how a target-specific investigation would collect additional evidence without mislabeling application counts as collector statistics.

Trace the actual program

Six prepared integers enter the report pipeline. The filter accepts two, four, and six, and each becomes a ReportRow. The program prints input-records:6, report-records:3, and report-total:12. KeepAlive retains the resulting report through the observation. These deterministic values confirm the workload's logical result. They do not count every physical allocation, reveal array headers, or describe how many objects survived a collection. The source and report are intentionally ordinary managed data that the bundled browser runtime can execute without opening a profiler connection or writing a dump file.

How the mechanism works

Choose tools according to the actual runtime. Desktop CoreCLR can expose supported counters, traces, and dumps through .NET diagnostic tooling such as dotnet-counters and dotnet-dump. Heap retaining-path analysis and allocation traces answer different questions. A Mono/WebAssembly host does not automatically accept the same desktop attachment workflow, and browser JavaScript memory views do not by themselves identify every managed object relationship. Record the target profile and available tooling, then select the narrow evidence needed to distinguish the current hypothesis from plausible alternatives.

Boundaries and misconceptions

A debug build, cold initialization, or changed input size can make two screenshots incomparable. Instrumentation itself also has cost and can retain data if it stores references carelessly. Avoid attaching labels such as leak or fragmentation to any upward graph without identifying its units and lifetime. A retained cache that reaches a stable documented bound is different from unbounded growth across repeated operations. Similarly, a fast operation that silently skipped half the records is not an optimization of the intended workload.

Practice: change one ownership path

First append eight to the input and predict seven input records, four report records, and a report total of twenty. Hint: update the hand-calculated result before comparing any memory observation. Then change only the predicate to odd values and predict three report records totaling nine for the original six inputs. These controlled edits demonstrate why workload counters and correctness checks belong beside performance evidence rather than being discarded as soon as a profiler opens.

Practice: test a second boundary

Next run a bounded repeated operation with a clear end-of-operation cleanup boundary and record which owner collections should return to their initial counts. Hint: an empty owner count is a lifecycle assertion, not a claim of immediate collection. On a supported target, capture allocation and retained-object evidence separately and record the runtime and build mode. If browser tooling cannot answer a particular retaining-path question, report that limitation and create a separate correctly labeled desktop reproduction instead of presenting desktop results as measurements from SharpRune.

Review the evidence

A good investigation report links symptom, hypothesis, workload, correctness, evidence, and conclusion. The baseline counts make the logical work auditable while deliberately leaving collector claims unmade. Add measurements only after their units and target are clear, and preserve enough context for another person to repeat them. That practice is more useful than chasing a single large memory number, because it makes both improvements and regressions explainable without confusing data ownership, allocation traffic, and the runtime's reservation policy. Preserve the same accepted rows and completion behavior in every compared run.

  • The original report processes six inputs and retains three report rows.
  • The report total twelve is a correctness check, not an allocation counter.
  • Profiler availability and evidence must be tied to the actual runtime.

A working example

using System;
using System.Linq;
using System.Collections.Generic;
using System.Text;
using System.Buffers;
using System.Runtime.InteropServices;
using System.Runtime.CompilerServices;

int[] input = { 1, 2, 3, 4, 5, 6 };
var report = input.Where(value => value % 2 == 0).Select(value => new ReportRow(value)).ToArray();
Console.WriteLine($"input-records:{input.Length}");
Console.WriteLine($"report-records:{report.Length}");
Console.WriteLine($"report-total:{report.Sum(row => row.Value)}");
GC.KeepAlive(report);
record ReportRow(int Value);

Expected output

input-records:6
report-records:3
report-total:12
Try this
The original report processes six inputs and retains three report rows.

Further reading