diff --git a/src/coreclr/gc/interface.cpp b/src/coreclr/gc/interface.cpp index 00d5f77f1a5e3f..aecd3abb80d6e0 100644 --- a/src/coreclr/gc/interface.cpp +++ b/src/coreclr/gc/interface.cpp @@ -2023,7 +2023,7 @@ uint64_t GCHeap::GetTotalAllocatedBytes() { #ifdef MULTIPLE_HEAPS uint64_t total_alloc_bytes = 0; - for (int i = 0; i < gc_heap::n_heaps; i++) + for (int i = 0; i < gc_heap::n_max_heaps; i++) { gc_heap* hp = gc_heap::g_heaps[i]; total_alloc_bytes += hp->total_alloc_bytes_soh; diff --git a/src/tests/GC/API/GC/GetTotalAllocatedBytesServerGC.cs b/src/tests/GC/API/GC/GetTotalAllocatedBytesServerGC.cs new file mode 100644 index 00000000000000..6168e2071bd91d --- /dev/null +++ b/src/tests/GC/API/GC/GetTotalAllocatedBytesServerGC.cs @@ -0,0 +1,146 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// Regression test for a Server GC accounting bug in GC.GetTotalAllocatedBytes(). +// +// GCHeap::GetTotalAllocatedBytes() summed each heap's cumulative total_alloc_bytes +// over the *currently-active* heaps [0, n_heaps). Under DATAS (Dynamic Adaptation To +// Application Sizes, default-on for Server GC) the active heap count grows and shrinks +// at run time as load changes. Because a decommissioned heap keeps its (real, frozen) +// total_alloc_bytes, excluding it from the sum made the returned value DROP whenever the +// heap count shrank -- i.e. GC.GetTotalAllocatedBytes() became non-monotonic. The +// precise overload has no smoothing, so it exposed the drop directly (surfacing as random +// zero / hallucinated allocation deltas in tools that difference two reads). +// +// This test drives Server GC with an oscillating allocation load (bursts grow the heap +// count, quiet periods let DATAS shrink it) while forcing frequent blocking gen2 GCs -- +// the points at which DATAS re-evaluates and decommissions heaps -- and repeatedly reads +// the precise cumulative total. Because the value is a cumulative counter, it must never +// decrease. A shrink-induced drop equals an entire heap's cumulative allocation, which is +// far larger than the small, by-design fluctuation of the precise value (per-thread unused +// allocation-context bytes); the tolerance below absorbs the latter while catching the bug. +// +// Only meaningful under Server GC (the .csproj sets DOTNET_gcServer=1). Under Workstation +// GC there is a single heap and no heap-count oscillation, so the check is a trivial pass. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Threading; +using Xunit; + +public class GetTotalAllocatedBytesServerGC +{ + [Fact] + public static void TestEntryPoint() + { + if (!System.Runtime.GCSettings.IsServerGC) + { + throw new Exception("ERROR: Requires server GC."); + } + + TimeSpan budget = TimeSpan.FromSeconds(20); + + // The precise total legitimately fluctuates by a small amount (it subtracts each + // thread's currently-unused allocation-context bytes). The bug drops a whole heap's + // cumulative counter, which is orders of magnitude larger. Allow generous slack for + // the benign fluctuation so the test cannot false-fail on a correct runtime. + const long Tolerance = 64L * 1024 * 1024; + + // Cap the allocator count: we only need enough parallel allocators to drive DATAS + // heap-count oscillation, not one per core. Scaling 1:1 with ProcessorCount would spawn + // hundreds of threads on high-core machines, oversubscribing the box and making the test + // needlessly heavy and flaky without improving repro reliability. + int burstThreads = Math.Clamp(Environment.ProcessorCount, 2, 16); + using var cts = new CancellationTokenSource(); + CancellationToken token = cts.Token; + + // Load controller: alternate a heavy BURST (many allocators -> DATAS grows the heap + // count) with a QUIET period (near-zero allocation -> DATAS shrinks it). + var controller = new Thread(() => + { + while (!token.IsCancellationRequested) + { + var burst = new List(); + using var burstCts = new CancellationTokenSource(); + CancellationToken burstToken = burstCts.Token; + for (int i = 0; i < burstThreads; i++) + { + var t = new Thread(() => + { + var rnd = new Random(Environment.CurrentManagedThreadId); + object local = null; + while (!burstToken.IsCancellationRequested) + { + for (int j = 0; j < 2000; j++) + local = new byte[rnd.Next(16, 4096)]; + } + GC.KeepAlive(local); + }) + { IsBackground = true }; + t.Start(); + burst.Add(t); + } + + bool cancelled = token.WaitHandle.WaitOne(1500); + + // Always cancel and join the burst threads so no allocator outlives the + // BURST -- this guarantees the following QUIET period is truly allocation-free. + burstCts.Cancel(); + foreach (var t in burst) + t.Join(2000); + + if (cancelled) + break; + + // QUIET: near-zero allocation so DATAS decides to shrink the heap count. + if (token.WaitHandle.WaitOne(3000)) + break; + } + }) + { IsBackground = true, Name = "load-controller" }; + controller.Start(); + + long maxDrop = 0; + long prevInitial = 0, prevFinal = 0; + long prev = GC.GetTotalAllocatedBytes(precise: true); + var sw = Stopwatch.StartNew(); + try + { + while (sw.Elapsed < budget) + { + // A blocking gen2 GC is where DATAS re-evaluates (and can decommission) heaps. + GC.Collect(2, GCCollectionMode.Forced, blocking: true); + long cur = GC.GetTotalAllocatedBytes(precise: true); + if (cur < prev) + { + long drop = prev - cur; + if (drop > maxDrop) + { + maxDrop = drop; + prevInitial = prev; + prevFinal = cur; + } + + if (maxDrop > Tolerance) + { + break; + } + } + prev = cur; + } + } + finally + { + cts.Cancel(); + controller.Join(3000); + } + + Assert.True( + maxDrop <= Tolerance, + $"GC.GetTotalAllocatedBytes(precise: true) is a cumulative counter and must not " + + $"decrease, but it dropped by {maxDrop:N0} bytes (from {prevInitial:N0} to {prevFinal:N0}), " + + $"exceeding the {Tolerance:N0} byte tolerance. Server GC = {System.Runtime.GCSettings.IsServerGC}, " + + $"processors = {Environment.ProcessorCount}."); + } +} diff --git a/src/tests/GC/API/GC/GetTotalAllocatedBytesServerGC.csproj b/src/tests/GC/API/GC/GetTotalAllocatedBytesServerGC.csproj new file mode 100644 index 00000000000000..3de0661534aef7 --- /dev/null +++ b/src/tests/GC/API/GC/GetTotalAllocatedBytesServerGC.csproj @@ -0,0 +1,22 @@ + + + + true + 1 + + true + + + PdbOnly + + true + + + + + + + + + +