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

Abierto
#137,005 1 comentario 0 reacciones 0 asignados Ver en GitHub

Los mantenedores suelen responder en 1 día

Nadie ha tomado este issue todavía.

Evaluación

Dificultad
4/5
Tiempo estimado
3-5 días
Aptitud para principiantes
35/100
Tipo de issue
Error
Claridad
Bien especificado
Estado de actividad
Activo
Stack tecnológico
csharp

Línea de trabajo

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.

Escrito por el modelo de indexación a partir del texto del issue.

Descripción

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

Lenguaje dominante
C#
Estrellas
18.4k
Forks
5.8k
Merge medio
2 d 12 h
PR fusionados (30 d)
581

Preparar el entorno

Abrir en Codespaces

Inicia el contenedor de desarrollo del proyecto en tu navegador, con tu propia cuenta de GitHub.

Primeros pasos

  1. Lee el issue completo y luego la guía de contribución del proyecto.
  2. Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
  3. Haz un fork del repositorio y trabaja en una rama.
  4. Abre un pull request que haga referencia al número del issue.

Más de dotnet/runtime

Todos los issues de dotnet/runtime

Issues similares

Más issues de C#

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.