Backport #119383 to release/10.0: workstation background GC silently falls back to blocking gen2
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ả
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
- 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
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.
- Không có Dockerfile hay tệp Docker Compose
- Có mẫu pull request
- Đọc hướng dẫn đóng góp
Bắt đầu từ đâu
- Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
- 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.
- Fork repository và làm thay đổi trên một nhánh.
- Mở pull request có tham chiếu số hiệu của issue.
Issue khác của dotnet/runtime
-
area-Host untriaged
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 68/100
dotnet/runtime#137187 · 3 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
ImmutableSortedSet<T>.Builder: enumerating through IEnumerable<T> doesn't throw on modification and can return wrong itemsCó thể đã có người làm @aw0lid đã nhận hôm nay. Đang mởarea-System.Collections
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 78/100
dotnet/runtime#137167 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
DbTransaction.SupportsSavepoints docs say Release/ReleaseAsync are expected to throw, but they are no-ops by designCó thể đã có người làm @alaricd đã nhận 4 ngày trước. Đang mởarea-System.Transactions untriaged
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 72/100
dotnet/runtime#137040 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
FileSystemGlobbing: Matcher throws ArgumentOutOfRangeException when the rootDir is the same value as the fileCó thể đã có người làm @makazeu đã nhận 5 ngày trước. Đang mởarea-Extensions-FileSystem
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 78/100
dotnet/runtime#136989 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Configuration binder source generator emits unqualified List<T> when binding arrays without implicit usingsCó thể đã có người làm @makazeu đã nhận 5 ngày trước. Đang mởarea-Extensions-Configuration bug source-generator
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 78/100
dotnet/runtime#136972 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
Tất cả issue của dotnet/runtime
Issue tương tự
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 82/100
Maintainer thường phản hồi trong vòng 1 ngày
-
Issue-Bug Needs-Triage
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 78/100
Maintainer thường phản hồi trong vòng 2 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 66/100
JiaFeiMiao-K-Cat/DiscImageStudio#281 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
bug triage
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 76/100
-
area/access-management Frontend onboarding candidate :handshake:
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 65/100
Altinn/altinn-auth#4941 ·
Maintainer thường phản hồi trong vòng 1 ngày