Backport #119383 to release/10.0: workstation background GC silently falls back to blocking gen2
メンテナーはふだん 1 日以内に返信
まだ誰も着手していません。
評価
- 難易度
- 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日 14時間
- マージ済み PR(30日)
- 585
環境構築
このプロジェクトの開発コンテナを、あなたの GitHub アカウントでブラウザ上に起動します。
- Dockerfile・Docker Compose ファイルなし
- プルリクエストのテンプレートあり
- コントリビューションガイドを読む
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
dotnet/runtime のほかの issue
-
area-Host untriaged
難易度 2/5 1〜3時間 初心者へのやさしさ 68/100
dotnet/runtime#137187 · コメント 3 件 ·
メンテナーはふだん 1 日以内に返信
-
ImmutableSortedSet<T>.Builder: enumerating through IEnumerable<T> doesn't throw on modification and can return wrong items対応中かも @aw0lid が今日担当しました。 オープンarea-System.Collections
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
dotnet/runtime#137167 · コメント 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
-
Issue-Bug Needs-Triage
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
メンテナーはふだん 2 日以内に返信
-
area/access-management Frontend onboarding candidate :handshake:
難易度 2/5 1〜3時間 初心者へのやさしさ 65/100
Altinn/altinn-auth#4941 ·
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
aws/aws-sdk-net#4586 ·
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 82/100
Kryptos-FR/MarkView.Avalonia#269 · リアクション 1 件 ·
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100