-
Notifications
You must be signed in to change notification settings - Fork 5.5k
Fix GC.GetTotalMemory returning negative under regions GC #130888
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
janvorli
merged 7 commits into
dotnet:main
from
kkokosa:fix/gc-gettotalmemory-negative-106712
Jul 27, 2026
+153
−7
Merged
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
ae3a575
Fix GC.GetTotalMemory returning negative under regions GC
kkokosa 7ce70ab
Small test improvements
kkokosa a22ce85
Address review: drop allocation.cpp reordering and trim interface.cpp…
kkokosa 024b3d6
Clamp ApproxTotalBytesInUse gen0 subtraction as defense-in-depth
kkokosa 1e8f2bc
Small comment improvement
kkokosa d2b9219
Assert worker threads stop within join timeout in regression test
kkokosa c85c472
Merge branch 'main' into fix/gc-gettotalmemory-negative-106712
kkokosa File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,135 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
|
|
||
| // GC.GetTotalMemory(false) must never return a negative value. | ||
| // | ||
| // The value bottoms out in ApproxTotalBytesInUse, which for gen0 computes gen0_size - gen0_frag | ||
| // as an unsigned quantity. gen0 fragmentation (free-list + free-object space) is a per-generation | ||
| // total that spans every gen0 region, so gen0_size must span every gen0 region as well. When gen0 | ||
| // retains a region past the ephemeral one (for example a region held in place by a pinned object), | ||
| // its span has to be included; otherwise fragmentation exceeds the counted span, the subtraction | ||
| // underflows, and the managed API surfaces the wrapped value as a negative long. | ||
| // | ||
| // This test drives concurrent allocation with a large, continuously-refreshed set of pinned | ||
| // objects (to force such retained gen0 regions) while repeatedly probing GetTotalMemory on another | ||
| // thread. Without the accounting fix it returns a negative value in well under a second; with the | ||
| // fix the value stays non-negative. | ||
|
|
||
| using System; | ||
| using System.Diagnostics; | ||
| using System.Runtime.InteropServices; | ||
| using System.Threading; | ||
| using Xunit; | ||
|
|
||
| public class GetTotalMemoryConcurrent | ||
| { | ||
| private static volatile bool s_stop; | ||
|
|
||
| [Fact] | ||
| public static void TestEntryPoint() | ||
| { | ||
| // A couple of seconds is far more than enough: the unfixed runtime fails almost immediately | ||
| // (typically within the first ~100K probes, well under a second). Even a single allocating | ||
| // thread is sufficient to build the region layout that triggers the miscount, so this does | ||
| // not depend on the number of processors. | ||
| const int DurationSeconds = 2; | ||
| int workers = Math.Max(2, Math.Min(8, Environment.ProcessorCount)); | ||
|
|
||
| // Reset in case this ever runs more than once in the same process: the worker threads exit | ||
| // as soon as s_stop is set, so a stale 'true' would leave the heap idle and mask the bug. | ||
| s_stop = false; | ||
|
|
||
| var threads = new Thread[workers]; | ||
| for (int i = 0; i < workers; i++) | ||
| { | ||
| threads[i] = new Thread(PinChurn) { IsBackground = true }; | ||
| threads[i].Start(); | ||
| } | ||
|
|
||
| long probes = 0; | ||
| long minObserved = long.MaxValue; | ||
| long negative = 0; | ||
| bool allStopped = true; | ||
| var sw = Stopwatch.StartNew(); | ||
| try | ||
| { | ||
| while (sw.Elapsed.TotalSeconds < DurationSeconds) | ||
| { | ||
| long total = GC.GetTotalMemory(false); | ||
| probes++; | ||
| if (total < minObserved) | ||
| { | ||
| minObserved = total; | ||
| } | ||
|
|
||
| if (total < 0) | ||
| { | ||
| negative = total; | ||
| break; | ||
| } | ||
| } | ||
| } | ||
| finally | ||
| { | ||
| s_stop = true; | ||
| foreach (Thread t in threads) | ||
| { | ||
| if (!t.Join(TimeSpan.FromSeconds(5))) | ||
| { | ||
| allStopped = false; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| Console.WriteLine($"probes={probes}, min observed={minObserved}, negative={negative}"); | ||
| Assert.True(negative >= 0, $"GC.GetTotalMemory(false) returned a negative value: {negative}"); | ||
| Assert.True(allStopped, "A worker thread did not stop within the join timeout."); | ||
| } | ||
|
|
||
| // Keeps a large ring of pinned tiny objects alive, refreshing the oldest one each iteration, and | ||
| // floods gen0 with throwaway garbage in between. The pins prevent their gen0 regions from being | ||
| // compacted, so those regions are swept and retained with large free lists while the live set | ||
| // stays small - exactly the shape that makes gen0 fragmentation exceed the counted gen0 span. | ||
| private static void PinChurn() | ||
| { | ||
| var rng = new Random(Environment.CurrentManagedThreadId); | ||
| const int RingSize = 4096; | ||
| var ring = new GCHandle[RingSize]; | ||
| int slot = 0; | ||
| long sink = 0; | ||
|
|
||
| try | ||
| { | ||
| while (!s_stop) | ||
| { | ||
| if (ring[slot].IsAllocated) | ||
| { | ||
| ring[slot].Free(); | ||
| } | ||
| ring[slot] = GCHandle.Alloc(new byte[24], GCHandleType.Pinned); | ||
| slot = (slot + 1) % RingSize; | ||
|
|
||
| for (int j = 0; j < 96; j++) | ||
| { | ||
| byte[] junk = new byte[rng.Next(8, 256)]; | ||
| sink += junk.Length; | ||
| } | ||
| } | ||
| } | ||
| finally | ||
| { | ||
| for (int i = 0; i < RingSize; i++) | ||
| { | ||
| if (ring[i].IsAllocated) | ||
| { | ||
| ring[i].Free(); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| if (sink == long.MaxValue) | ||
| { | ||
| Console.WriteLine(sink); | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| <Project Sdk="Microsoft.NET.Sdk"> | ||
| <PropertyGroup> | ||
| <!-- Needed for GCStressIncompatible --> | ||
| <RequiresProcessIsolation>true</RequiresProcessIsolation> | ||
| <CLRTestPriority>1</CLRTestPriority> | ||
| </PropertyGroup> | ||
| <PropertyGroup> | ||
| <DebugType>PdbOnly</DebugType> | ||
| <GCStressIncompatible>true</GCStressIncompatible> | ||
| </PropertyGroup> | ||
| <ItemGroup> | ||
| <Compile Include="GetTotalMemoryConcurrent.cs" /> | ||
| </ItemGroup> | ||
| </Project> |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.