Backport #119383 to release/10.0: workstation background GC silently falls back to blocking gen2
维护者通常 1 天内回复
还没有人认领这个 Issue。
评估
- 难度
- 4/5
- 预计耗时
- 3-5 天
- 新手友好度
- 35/100
- Issue 类型
- 缺陷
- 描述清晰度
- 描述清楚
- 活跃度
- 活跃
- 技术栈
- csharp
- 领域
- backend, performance
调研方向
Read src/coreclr/gc/gc.cpp around the eligibility check at v10.0.14 L24801 and compare it with the fix in PR #119383, which changes the check to bgc_thread_running. The issue includes a deterministic reproduction using a delayed thread publish and a C# harness that checks GCMemoryInfo.Concurrent. Verify the backport prevents requested background gen2 collections from falling back to blocking collections; this needs familiarity with the GC and release backport process.
由索引模型根据 Issue 内容生成。
描述
Description
With workstation concurrent GC, the background GC thread exits after 20 s idle. The next background gen2 creates a new thread, then decides whether it can run concurrently by checking bgc_thread. The new thread publishes bgc_thread only after GCToEEInterface::CreateThread has returned, so the check can see null, and the collection silently becomes a full blocking gen2. GCStart has already been raised as a background GC, so start-event-based monitoring does not see it.
This was fixed on main by #119383 ("Fix eligibility check for WKS concurrent gen2 GC", Fix #13238), which checks bgc_thread_running instead of bgc_thread. release/10.0 branched before that change and still has the old check, up to and including v10.0.14 (gc.cpp L24801). #119383 is locked, so I'm opening this to ask for a backport to release/10.0.
Code at v10.0.6:
- BGC thread exits after 20 s idle and clears
bgc_thread: https://github.com/dotnet/runtime/blob/v10.0.6/src/coreclr/gc/gc.cpp#L40142-L40173 CreateThreadreturns onceThreadStartedEventis set, before the thread proc runs: https://github.com/dotnet/runtime/blob/v10.0.6/src/coreclr/vm/gcenv.ee.cpp#L1514-L1545bgc_threadis published later, inbgc_thread_stub: https://github.com/dotnet/runtime/blob/v10.0.6/src/coreclr/gc/gc.cpp#L38051- The check: https://github.com/dotnet/runtime/blob/v10.0.6/src/coreclr/gc/gc.cpp#L24724
- The silent fallback: https://github.com/dotnet/runtime/blob/v10.0.6/src/coreclr/gc/gc.cpp#L24870-L24873
Reproduction Steps
The window is a few instructions wide, so on most machines the stock runtime does not lose the race. To make it deterministic, add a 20 ms sleep before the publish and load it as a standalone GC (DOTNET_GCPath=<dir>/libclrgc.so, built with src/coreclr/build-runtime.sh -x64 -release -component clrgc):
--- a/src/coreclr/gc/gc.cpp
+++ b/src/coreclr/gc/gc.cpp
@@ -38048,6 +38048,7 @@ void gc_heap::bgc_thread_stub (void* arg)
dprintf (6666, ("h%d %s", heap->heap_number, (wait_p ? "waited" : "did not wait")));
#endif
+ GCToOSInterface::Sleep (20);
heap->bgc_thread = GCToEEInterface::GetThread();
assert(heap->bgc_thread != nullptr);
heap->bgc_thread_function();
Repro: builds a ~1.2 GB live heap; each round sleeps 25 s (so the BGC thread exits), requests a background gen2, and reads GC.GetGCMemoryInfo(GCKind.Any). Concurrent == false on a gen2 means it fell back.
BgcRace.csproj:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<ServerGarbageCollector>false</ServerGarbageCollector>
<ConcurrentGarbageCollection>true</ConcurrentGarbageCollection>
</PropertyGroup>
</Project>
Program.cs:
using System.Diagnostics;
int rounds = int.Parse(Environment.GetEnvironmentVariable("ROUNDS") ?? "12");
int idleSeconds = int.Parse(Environment.GetEnvironmentVariable("IDLE") ?? "25");
long heapMegabytes = long.Parse(Environment.GetEnvironmentVariable("HEAP_MB") ?? "1024");
Console.WriteLine($"gcServer={System.Runtime.GCSettings.IsServerGC} concurrent=true rounds={rounds} idle={idleSeconds}s heap={heapMegabytes}MB");
var liveHeap = BuildLiveHeap(heapMegabytes);
GC.Collect();
Console.WriteLine($"live heap {GC.GetTotalMemory(false) >> 20} MB");
int fellBack = 0;
double maxPauseMs = 0;
for (int round = 1; round <= rounds; round++)
{
Thread.Sleep(TimeSpan.FromSeconds(idleSeconds));
long indexBefore = GC.GetGCMemoryInfo(GCKind.Any).Index;
GC.Collect(2, GCCollectionMode.Forced, blocking: false, compacting: false);
var info = WaitForGen2(indexBefore);
double pauseMs = info.PauseDurations[0].TotalMilliseconds;
string kind = info.Concurrent ? "background" : "FELL_BACK_TO_BLOCKING";
fellBack += info.Concurrent ? 0 : 1;
maxPauseMs = Math.Max(maxPauseMs, pauseMs);
Console.WriteLine($"round {round,2}: gc#{info.Index} gen{info.Generation} concurrent={info.Concurrent} pause={pauseMs:F1}ms {kind}");
}
Console.WriteLine($"fell_back={fellBack}/{rounds} max_pause={maxPauseMs:F1}ms");
GC.KeepAlive(liveHeap);
return fellBack > 0 ? 1 : 0;
static GCMemoryInfo WaitForGen2(long indexBefore)
{
var timeout = Stopwatch.StartNew();
GCMemoryInfo info;
do
{
Thread.Sleep(10);
info = GC.GetGCMemoryInfo(GCKind.Any);
}
while ((info.Index <= indexBefore || info.Generation != 2) && timeout.Elapsed < TimeSpan.FromMinutes(2));
return info;
}
static Node[][] BuildLiveHeap(long megabytes)
{
const int NodesPerChunk = 1 << 16;
long numNodes = megabytes * 1024 * 1024 / 40;
var chunks = new Node[numNodes / NodesPerChunk + 1][];
Node? previous = null;
for (int chunk = 0; chunk < chunks.Length; chunk++)
{
chunks[chunk] = new Node[NodesPerChunk];
for (int i = 0; i < NodesPerChunk; i++)
{
previous = new Node { Next = previous, Value = i };
chunks[chunk][i] = previous;
}
}
return chunks;
}
class Node
{
public Node? Next;
public long Value;
public long Padding;
}
Run:
dotnet build -c Release -o out
DOTNET_GCPath=<dir>/libclrgc.so IDLE=25 ROUNDS=12 dotnet out/BgcRace.dll
Expected behavior
A gen2 requested as background runs as a background GC once a BGC thread has been successfully created, whether or not that thread has reached bgc_thread_stub yet, as on main after #119383.
Actual behavior
The gen2 runs as a full blocking collection ("couldn't create BGC threads, reverting to doing a blocking GC"). With the delay, on .NET 10.0.6, Linux x64:
| Configuration | Fell back to blocking | Max pause |
|---|---|---|
| Stock, workstation | 0/12 | 0.6 ms |
| Stock, warm BGC thread (IDLE=1) | 0/12 | 0.4 ms |
| Delayed publish, workstation | 6/12 | 474 ms |
Delayed publish, DOTNET_gcServer=1 DOTNET_GCHeapCount=1 |
0/12 | 1.9 ms |
round 1: gc#135 gen2 concurrent=False pause=473.5ms FELL_BACK_TO_BLOCKING
round 2: gc#136 gen2 concurrent=True pause=0.3ms background
round 3: gc#137 gen2 concurrent=False pause=451.4ms FELL_BACK_TO_BLOCKING
...
fell_back=6/12 max_pause=473.5ms
Every other round falls back: the fallback round sets keep_bgc_threads_p (L24549), so the thread it created survives its idle timeout and the next round finds it; the next real BGC clears the flag (L40318).
In production (stock 10.0.6, no delay) this caused 0.6-1.1 s pauses of all managed threads on about one in three gen2s.
Regression?
No response
Known Workarounds
https://github.com/dotnet/runtime/pull/119383 This is the fix
Configuration
No response
Other information
No response
- 主要语言
- C#
- 星标
- 18.4k
- 派生
- 5.8k
- 平均合并
- 2 天 12 小时
- 30 天内合并 PR
- 581
环境准备
在浏览器里用你自己的 GitHub 账号启动这个项目的开发容器。
- 没有 Dockerfile 或 Docker Compose 文件
- 有 Pull Request 模板
- 阅读贡献指南
从这里开始
- 先读完整个 Issue,再读项目的贡献指南。
- 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
- Fork 仓库,在一个分支上完成修改。
- 提交 Pull Request,并在描述里引用这个 Issue 编号。
dotnet/runtime 的其他 Issue
-
area-System.Collections untriaged
难度 2/5 1-3 小时 新手友好度 78/100
dotnet/runtime#137167 · 1 条评论 ·
维护者通常 1 天内回复
-
Incorrect loop bound in SizeOptIListSelectIterator.ToList可能已有人在做 @equmagg 于 2 天前认领。 未关闭area-System.Collections untriaged
难度 2/5 1-3 小时 新手友好度 64/100
dotnet/runtime#137126 · 1 条评论 ·
维护者通常 1 天内回复
-
DbTransaction.SupportsSavepoints docs say Release/ReleaseAsync are expected to throw, but they are no-ops by design可能已有人在做 @alaricd 于 4 天前认领。 未关闭area-System.Transactions untriaged
难度 2/5 1-3 小时 新手友好度 72/100
dotnet/runtime#137040 · 1 条评论 ·
维护者通常 1 天内回复
-
FileSystemGlobbing: Matcher throws ArgumentOutOfRangeException when the rootDir is the same value as the file可能已有人在做 @makazeu 于 4 天前认领。 未关闭area-Extensions-FileSystem
难度 2/5 1-3 小时 新手友好度 78/100
dotnet/runtime#136989 · 1 条评论 ·
维护者通常 1 天内回复
-
Configuration binder source generator emits unqualified List<T> when binding arrays without implicit usings可能已有人在做 @makazeu 于 4 天前认领。 未关闭area-Extensions-Configuration bug source-generator
难度 2/5 1-3 小时 新手友好度 78/100
dotnet/runtime#136972 · 1 条评论 ·
维护者通常 1 天内回复
相似的 Issue
-
难度 2/5 1-3 小时 新手友好度 78/100
sebastienros/jint#4531 ·
维护者通常 1 天内回复
-
Unable to build未关闭Bug
难度 2/5 1-3 小时 新手友好度 72/100
维护者通常 1 天内回复
-
Assert.That on a null non-generic IEnumerable throws ArgumentNullException, so IsNull() cannot pass未关闭bug P2
难度 2/5 1-3 小时 新手友好度 72/100
维护者通常 1 天内回复
-
area/automation area/sdk kind/bug
难度 2/5 1-3 小时 新手友好度 78/100
unoplatform/uno.templates#2406 ·
维护者通常 1 天内回复
-
bug frontend ui
难度 2/5 1-3 小时 新手友好度 68/100
维护者通常 1 天内回复