Skip to content

NativeAOT(GarbageCollector): BGC allow_fgc does not yield to GCToEEInterface::SuspendEE during FGC in NativeAoT #132064

Description

@MCRcortex

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

Image ~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

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

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 (
enter_spin_lock(&gc_lock);
such as here) which internally will swap from cooperative to preemptive and back allowing it to suspend
Note this is not an issue in a non nativeAOT enviroment as the coreclr gcenv uses the ThreadSuspend::SuspendEE method (
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();
}
) which does update 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

Assignees

Type

No type

Projects

Status
No status

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions