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

未关闭
#137,005 1 条评论 0 个 reaction 已指派 0 人 在 GitHub 查看

维护者通常 1 天内回复

还没有人认领这个 Issue。

评估

难度
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 天 12 小时
30 天内合并 PR
581

环境准备

在 Codespaces 中打开

在浏览器里用你自己的 GitHub 账号启动这个项目的开发容器。

从这里开始

  1. 先读完整个 Issue,再读项目的贡献指南。
  2. 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
  3. Fork 仓库,在一个分支上完成修改。
  4. 提交 Pull Request,并在描述里引用这个 Issue 编号。

dotnet/runtime 的其他 Issue

查看 dotnet/runtime 的全部 Issue

相似的 Issue

更多 C# Issue

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。