Repository navigation
NativeAOT(GarbageCollector): BGC allow_fgc does not yield to GCToEEInterface::SuspendEE during FGC in NativeAoT #132064
Description
Description
BGC allow_fgc does not yield to the FGC in nativeaot builds resulting in the execution enviroment stalling for a horrifically long time
Affects all nativeaot builds
Configuration
CoreCLR
NativeAOT
.NET 10 (and is still an issue in main as of commit 9803759)
Regression?
BGC does not yield to FGC in nativeaot, unknown if this is a regression or simply never fixed since nativeaot's creation
Data
~65ms, of which ~60ms is spent waiting for the EE to suspend
(all other cooperative threads have been suspended. only the BGC is still executing)
Analysis
The cause of this is due to allow_fgc using a condition that is never true in NativeAOT GCToEEInterface::SuspendEE
runtime/src/coreclr/gc/background.cpp
Line 1228 in 9803759
| if (g_fSuspensionPending > 0) |
g_fSuspensionPending > 0 is only updated via GCHeap::SetSuspensionPending which at present is only ever used by ThreadStore::SetThreadTrapForSuspension which is not used by NativeAOT gcee
when an FGC is run on nativeaot it invokes this method
runtime/src/coreclr/nativeaot/Runtime/gcenv.ee.cpp
Lines 37 to 53 in 9803759
| void GCToEEInterface::SuspendEE(SUSPEND_REASON reason) | |
| { | |
| #ifdef FEATURE_EVENT_TRACE | |
| ETW::GCLog::ETW_GC_INFO Info; | |
| Info.SuspendEE.Reason = reason; | |
| Info.SuspendEE.GcCount = (((reason == SUSPEND_FOR_GC) || (reason == SUSPEND_FOR_GC_PREP)) ? | |
| (uint32_t)GCHeapUtilities::GetGCHeap()->GetGcCount() : (uint32_t)-1); | |
| #endif // FEATURE_EVENT_TRACE | |
| FireEtwGCSuspendEEBegin_V1(Info.SuspendEE.Reason, Info.SuspendEE.GcCount, GetClrInstanceId()); | |
| GetThreadStore()->LockThreadStore(); | |
| GCHeapUtilities::GetGCHeap()->SetGCInProgress(TRUE); | |
| GetThreadStore()->SuspendAllThreads(true); | |
| FireEtwGCSuspendEEEnd_V1(GetClrInstanceId()); | |
| } |
Which does not update, set or increment
g_fSuspensionPending in any way causing the SuspendAllThreads method to spin wait until the BGC has either completed and exits or finished its phase and tries to acquire the spinlock (runtime/src/coreclr/gc/background.cpp
Line 2562 in 9803759
| enter_spin_lock(&gc_lock); |
Note this is not an issue in a non nativeAOT enviroment as the coreclr gcenv uses the ThreadSuspend::SuspendEE method (
runtime/src/coreclr/vm/gcenv.ee.cpp
Lines 38 to 54 in 9803759
| void GCToEEInterface::SuspendEE(SUSPEND_REASON reason) | |
| { | |
| WRAPPER_NO_CONTRACT; | |
| static_assert(SUSPEND_FOR_GC == (int)ThreadSuspend::SUSPEND_FOR_GC); | |
| static_assert(SUSPEND_FOR_GC_PREP == (int)ThreadSuspend::SUSPEND_FOR_GC_PREP); | |
| _ASSERTE(reason == SUSPEND_FOR_GC || reason == SUSPEND_FOR_GC_PREP); | |
| if (g_pDebugInterface) | |
| g_pDebugInterface->SuspendForGarbageCollectionStarted(); | |
| ThreadSuspend::SuspendEE((ThreadSuspend::SUSPEND_REASON)reason); | |
| if (g_pDebugInterface) | |
| g_pDebugInterface->SuspendForGarbageCollectionCompleted(); | |
| } |
g_fSuspensionPending
This results in situations where the entire EE is suspended for a very extended period of time for no reason (waiting on the BGC which will never yield)
Worst case this can cause the EE to stall for 300+ milliseconds depending on the phase size and speed of the BGC
One such way to possibly fix this is to replace the condition g_fSuspensionPending > 0 inside the bgc with g_fSuspensionPending > 0 || GCHeapUtilities::IsGCInProgress() which will allow it to yield to the FGC when the EE is being suspended
Alternativly nativeaot GCToEEInterface::SuspendEE invoke ThreadStore::SetThreadTrapForSuspension(); on suspend and UnsetThreadTrapForSuspension on resume
Metadata
Metadata
Labels
Type
Projects
- StatusShow more project fieldsNo status
Activity
dotnet-policy-service commentedon Aug 10, 2026
Tagging subscribers to this area: @agocke, @dotnet/ilc-contrib
See info in area-owners.md if you want to be subscribed.
[-]NativeAOT BGC allow_fgc does not yield to GCToEEInterface::SuspendEE during FGC[/-][+]NativeAOT(GarbageCollector): background allow_fgc does not yield to GCToEEInterface::SuspendEE during FGC in NativeAoT[/+][-]NativeAOT(GarbageCollector): background allow_fgc does not yield to GCToEEInterface::SuspendEE during FGC in NativeAoT[/-][+]NativeAOT(GarbageCollector): BGC allow_fgc does not yield to GCToEEInterface::SuspendEE during FGC in NativeAoT[/+]MCRcortex commentedon Aug 10, 2026
The changes seem to have been introduced in this pr dotnet/coreclr#16765
jkotas commentedon Aug 10, 2026
Great catch! @hoyosjs I think this may be the perf issue with long GC pauses that you have been chasing down.
The complicated scaffolding is trying make this check very cheap (single comparison). The additional check for
GCHeapUtilities::IsGCInProgresswould defeat that.#132083 should be the right fix. @MCRcortex Would you mind verifying that it fixes the issue for you?
MCRcortex commentedon Aug 11, 2026
the logic for the fix seems sound! (unfortunatly testing a local dotnet sdk build with the changes and integrate that into our build enviroment is a bit of a challenge sorry) but the fix seems like it should be correct!
Signal GC suspension pending during NativeAOT thread suspension (#132083