using System;
using System.Runtime.CompilerServices;
using NUnit.Framework;
namespace ZeroAlloc.Tests;
///
/// Test helper that asserts a code path performs zero managed (GC heap) allocations
/// in steady state, using .
///
///
/// The action is executed several times before measurement so that JIT compilation,
/// tiering and any one-time lazy initialization inside the measured code complete
/// before bytes are counted. The measured region runs entirely on the calling thread,
/// so background JIT activity does not pollute the counter (it allocates natively,
/// not on this thread's GC budget).
///
public static class AllocationAssert
{
///
/// Asserts that allocates zero managed bytes per invocation
/// after warm-up.
///
/// The code path under audit. Must be repeatable.
/// Invocations performed before measurement begins.
/// Invocations performed inside the measured window.
/// Optional label included in the failure message.
[MethodImpl(MethodImplOptions.NoInlining)]
public static void Zero(Action action, int warmupIterations = 64, int measuredIterations = 256,
string label = "code path")
{
ArgumentNullException.ThrowIfNull(action);
for (int i = 0; i < warmupIterations; i++)
{
action();
}
// Drain any pending finalization noise before measuring.
GC.Collect();
GC.WaitForPendingFinalizers();
long before = GC.GetAllocatedBytesForCurrentThread();
for (int i = 0; i < measuredIterations; i++)
{
action();
}
long delta = GC.GetAllocatedBytesForCurrentThread() - before;
Assert.That(delta, Is.EqualTo(0),
$"Expected zero allocations for {label}, but {delta} bytes were allocated " +
$"across {measuredIterations} iterations ({(double)delta / measuredIterations:F1} B/op).");
}
///
/// Asserts that allocates at most
/// managed bytes per invocation after warm-up. Useful for auditing intentionally-allocating
/// cold paths.
///
/// Maximum tolerated managed bytes per invocation.
/// The code path under audit.
/// Invocations performed before measurement begins.
/// Invocations performed inside the measured window.
[MethodImpl(MethodImplOptions.NoInlining)]
public static void AtMost(long maxBytesPerOp, Action action,
int warmupIterations = 64, int measuredIterations = 256)
{
ArgumentNullException.ThrowIfNull(action);
for (int i = 0; i < warmupIterations; i++)
{
action();
}
GC.Collect();
GC.WaitForPendingFinalizers();
long before = GC.GetAllocatedBytesForCurrentThread();
for (int i = 0; i < measuredIterations; i++)
{
action();
}
long delta = GC.GetAllocatedBytesForCurrentThread() - before;
double perOp = (double)delta / measuredIterations;
Assert.That(perOp, Is.LessThanOrEqualTo(maxBytesPerOp),
$"Expected at most {maxBytesPerOp} B/op, measured {perOp:F1} B/op.");
}
}