Backport #119383 to release/10.0: workstation background GC silently falls back to blocking gen2

オープン
#137,005 コメント 1 件 リアクション 0 件 担当者 0 名 GitHub で見る

メンテナーはふだん 1 日以内に返信

まだ誰も着手していません。

評価

難易度
4/5
見積もり時間
3〜5日
初心者へのやさしさ
35/100
issue の種類
バグ
明瞭さ
明確に書かれている
活発さ
活発
技術スタック
csharp

調査の方向性

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 の本文から書いたものです。

説明

area-GC-coreclr untriaged
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:

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

環境構築

Codespaces で開く

このプロジェクトの開発コンテナを、あなたの GitHub アカウントでブラウザ上に起動します。

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

dotnet/runtime のほかの issue

dotnet/runtime の issue をすべて見る

似ている issue

C# の issue をもっと見る

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。