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

Đang mở
#137,005 1 bình luận 0 reaction 0 người được giao Xem trên GitHub

Maintainer thường phản hồi trong vòng 1 ngày

Chưa có ai nhận issue này.

Đánh giá

Độ khó
4/5
Thời gian dự kiến
3-5 ngày
Mức phù hợp với người mới
35/100
Loại issue
Lỗi
Độ rõ ràng
Đặc tả rõ ràng
Mức độ hoạt động
Sôi nổi
Công nghệ
csharp
Lĩnh vực
backend, performance

Hướng nghiên cứu

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.

Do mô hình lập chỉ mục viết ra từ nội dung của issue.

Mô tả

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

Ngôn ngữ chính
C#
Star
18.4k
Fork
5.8k
Merge trung bình
2 ngày 14 giờ
Pull request đã merge (30 ngày)
585

Chuẩn bị môi trường

Mở trong Codespaces

Khởi chạy dev container của dự án ngay trên trình duyệt, bằng tài khoản GitHub của bạn.

Bắt đầu từ đâu

  1. Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
  2. Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
  3. Fork repository và làm thay đổi trên một nhánh.
  4. Mở pull request có tham chiếu số hiệu của issue.

Issue khác của dotnet/runtime

Tất cả issue của dotnet/runtime

Issue tương tự

Thêm issue về C#

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.