Skip to content

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

Closed

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

Activity

  1. dotnet-policy-service commented on Aug 10, 2026

    @dotnet-policy-service
    Contributor

    Tagging subscribers to this area: @agocke, @dotnet/ilc-contrib
    See info in area-owners.md if you want to be subscribed.

  2. changed the title [-]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[/+] on Aug 10, 2026
  3. changed the title [-]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[/+] on Aug 10, 2026
  4. MCRcortex commented on Aug 10, 2026

    @MCRcortex
    Author

    The changes seem to have been introduced in this pr dotnet/coreclr#16765

  5. jkotas commented on Aug 10, 2026

    @jkotas
    Member

    Great catch! @hoyosjs I think this may be the perf issue with long GC pauses that you have been chasing down.

    One such way to possibly fix this is to replace the condition g_fSuspensionPending > 0 inside the bgc with g_fSuspensionPending > 0 || GCHeapUtilities::IsGCInProgress()

    The complicated scaffolding is trying make this check very cheap (single comparison). The additional check for GCHeapUtilities::IsGCInProgress would defeat that.

    #132083 should be the right fix. @MCRcortex Would you mind verifying that it fixes the issue for you?

  6. MCRcortex commented on Aug 11, 2026

    @MCRcortex
    Author

    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!

  7. added a commit that references this issue on Aug 11, 2026
    4f6f3a5
  8. locked and limited conversation to collaborators on Sep 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

  • Status
    No status

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions