Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ensure that WaitForPendingFinalizers has seen the expected Full GC count #105289

Merged
merged 6 commits into from
Jul 25, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,7 @@ internal static extern unsafe IntPtr RhpCallPropagateExceptionCallback(

// Indicate that the current round of finalizations is complete.
[DllImport(Redhawk.BaseName)]
internal static extern void RhpSignalFinalizationComplete(uint fCount);
internal static extern void RhpSignalFinalizationComplete(uint fCount, int observedFullGcCount);

[DllImport(Redhawk.BaseName)]
internal static extern ulong RhpGetTickCount64();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,14 @@ public static void ProcessFinalizers()
// otherwise memory is low and we should initiate a collection.
if (InternalCalls.RhpWaitForFinalizerRequest() != 0)
{
int observedFullGcCount = RuntimeImports.RhGetGcCollectionCount(RuntimeImports.RhGetMaxGcGeneration(), false);
uint finalizerCount = DrainQueue();

// Tell anybody that's interested that the finalization pass is complete (there is a race condition here
// where we might immediately signal a new request as complete, but this is acceptable).
InternalCalls.RhpSignalFinalizationComplete(finalizerCount);
// Anyone waiting to drain the Q can now wake up. Note that there is a
// race in that another thread starting a drain, as we leave a drain, may
// consider itself satisfied by the drain that just completed.
// Thus we include the Full GC count that we have certaily observed.
InternalCalls.RhpSignalFinalizationComplete(finalizerCount, observedFullGcCount);
}
else
{
Expand Down
44 changes: 32 additions & 12 deletions src/coreclr/nativeaot/Runtime/FinalizerHelpers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,22 @@ EXTERN_C void QCALLTYPE RhInitializeFinalizerThread()
g_FinalizerEvent.Set();
}

static int32_t g_fullGcCountSeenByFinalization;

// Indicate that the current round of finalizations is complete.
EXTERN_C void QCALLTYPE RhpSignalFinalizationComplete(uint32_t fcount, int32_t observedFullGcCount)
{
FireEtwGCFinalizersEnd_V1(fcount, GetClrInstanceId());

g_fullGcCountSeenByFinalization = observedFullGcCount;
g_FinalizerDoneEvent.Set();

if (YieldProcessorNormalization::IsMeasurementScheduled())
{
YieldProcessorNormalization::PerformMeasurement();
}
}

EXTERN_C void QCALLTYPE RhWaitForPendingFinalizers(UInt32_BOOL allowReentrantWait)
{
// This must be called via p/invoke rather than RuntimeImport since it blocks and could starve the GC if
Expand All @@ -103,13 +119,29 @@ EXTERN_C void QCALLTYPE RhWaitForPendingFinalizers(UInt32_BOOL allowReentrantWai
// Can't call this from the finalizer thread itself.
if (ThreadStore::GetCurrentThread() != g_pFinalizerThread)
{
// We may see a completion of finalization cycle that might not see objects that became
// F-reachable in recent GCs. In such case we want to wait for a completion of another cycle.
// However, since an object cannot be prevented from promoting, one can only rely on Full GCs
// to collect unreferenced objects deterministically. Thus we only care about Full GCs here.
int desiredFullGcCount =
GCHeapUtilities::GetGCHeap()->CollectionCount(GCHeapUtilities::GetGCHeap()->GetMaxGeneration());

tryAgain:
// Clear any current indication that a finalization pass is finished and wake the finalizer thread up
// (if there's no work to do it'll set the done event immediately).
g_FinalizerDoneEvent.Reset();
g_FinalizerEvent.Set();

// Wait for the finalizer thread to get back to us.
g_FinalizerDoneEvent.Wait(INFINITE, false, allowReentrantWait);

if (desiredFullGcCount - g_fullGcCountSeenByFinalization > 0)
{
// There were some Full GCs happening before we started waiting and possibly not seen by the
// last finalization cycle. This is rare, but we need to be sure we have seen those,
// so we try one more time.
goto tryAgain;
}
}
}

Expand Down Expand Up @@ -176,18 +208,6 @@ EXTERN_C UInt32_BOOL QCALLTYPE RhpWaitForFinalizerRequest()
} while (true);
}

// Indicate that the current round of finalizations is complete.
EXTERN_C void QCALLTYPE RhpSignalFinalizationComplete(uint32_t fcount)
{
FireEtwGCFinalizersEnd_V1(fcount, GetClrInstanceId());
g_FinalizerDoneEvent.Set();

if (YieldProcessorNormalization::IsMeasurementScheduled())
{
YieldProcessorNormalization::PerformMeasurement();
}
}

//
// The following helpers are special in that they interact with internal GC state or directly manipulate
// managed references so they're called with a special co-operative p/invoke.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,5 +104,15 @@ internal static IntPtr RhGetModuleSection(TypeManagerHandle module, ReadyToRunSe
[MethodImplAttribute(MethodImplOptions.InternalCall)]
[RuntimeImport(RuntimeLibrary, "RhBulkMoveWithWriteBarrier")]
internal static extern unsafe void RhBulkMoveWithWriteBarrier(ref byte dmem, ref byte smem, nuint size);

// Get maximum GC generation number.
[MethodImplAttribute(MethodImplOptions.InternalCall)]
[RuntimeImport(RuntimeLibrary, "RhGetMaxGcGeneration")]
internal static extern int RhGetMaxGcGeneration();

// Get count of collections so far.
[MethodImplAttribute(MethodImplOptions.InternalCall)]
[RuntimeImport(RuntimeLibrary, "RhGetGcCollectionCount")]
internal static extern int RhGetGcCollectionCount(int generation, bool getSpecialGCCount);
}
}
31 changes: 26 additions & 5 deletions src/coreclr/vm/finalizerthread.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -404,13 +404,15 @@ VOID FinalizerThread::FinalizerThreadWorker(void *args)
}
LOG((LF_GC, LL_INFO100, "***** Calling Finalizers\n"));

int observedFullGcCount =
GCHeapUtilities::GetGCHeap()->CollectionCount(GCHeapUtilities::GetGCHeap()->GetMaxGeneration());
FinalizeAllObjects();

// Anyone waiting to drain the Q can now wake up. Note that there is a
// race in that another thread starting a drain, as we leave a drain, may
// consider itself satisfied by the drain that just completed. This is
// acceptable.
SignalFinalizationDone();
// consider itself satisfied by the drain that just completed.
// Thus we include the Full GC count that we have certaily observed.
SignalFinalizationDone(observedFullGcCount);
}

if (s_InitializedFinalizerThreadForPlatform)
Expand Down Expand Up @@ -538,10 +540,13 @@ void FinalizerThread::FinalizerThreadCreate()
}
}

void FinalizerThread::SignalFinalizationDone()
static int g_fullGcCountSeenByFinalization;

void FinalizerThread::SignalFinalizationDone(int observedFullGcCount)
{
WRAPPER_NO_CONTRACT;

g_fullGcCountSeenByFinalization = observedFullGcCount;
hEventFinalizerDone->Set();
}

Expand All @@ -555,6 +560,13 @@ void FinalizerThread::FinalizerThreadWait()
// Can't call this from within a finalized method.
if (!IsCurrentThreadFinalizer())
{
// We may see a completion of finalization cycle that might not see objects that became
// F-reachable in recent GCs. In such case we want to wait for a completion of another cycle.
// However, since an object cannot be prevented from promoting, one can only rely on Full GCs
// to collect unreferenced objects deterministically. Thus we only care about Full GCs here.
int desiredFullGcCount =
GCHeapUtilities::GetGCHeap()->CollectionCount(GCHeapUtilities::GetGCHeap()->GetMaxGeneration());

GCX_PREEMP();

#ifdef FEATURE_COMINTEROP
Expand All @@ -565,8 +577,8 @@ void FinalizerThread::FinalizerThreadWait()
g_pRCWCleanupList->CleanupWrappersInCurrentCtxThread();
#endif // FEATURE_COMINTEROP

tryAgain:
hEventFinalizerDone->Reset();

EnableFinalization();

// Under GC stress the finalizer queue may never go empty as frequent
Expand All @@ -580,6 +592,15 @@ void FinalizerThread::FinalizerThreadWait()

DWORD status;
status = hEventFinalizerDone->Wait(INFINITE,TRUE);

if (desiredFullGcCount - g_fullGcCountSeenByFinalization > 0)
Copy link
Member

@jkotas jkotas Jul 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume that the subtraction and > 0 are to handle GC counts wrapping around. Signed integer overflows are undefined behavior in C++. I am not sure whether it can bite us here. Unsigned integer overflows have fully defined behavior in C++, so it would be more portable to do this with unsigned types.

Copy link
Member Author

@VSadov VSadov Jul 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, a billion of gen2 gc would be a rare, but plausible scenario for a long term service.
I’d prefer the API to return size_t or uint64_t here, but it is what it is. Not sure why signed int was chosen.

We do know that collection counts are monotonically incrementing ticks that may wrap around. I’ll throw in some unsigned casts, just in case the compiler does something unexpected with compares.

Copy link
Member Author

@VSadov VSadov Jul 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One billion seconds is about 30 years. ;)

1-10 times per second is the rough guess for gen2 frequency, I think, excluding stress scenarios. So maybe signed int was chosen because it just does not matter.

{
// There were some Full GCs happening before we started waiting and possibly not seen by the
// last finalization cycle. This is rare, but we need to be sure we have seen those,
// so we try one more time.
goto tryAgain;
}

_ASSERTE(status == WAIT_OBJECT_0);
}
}
2 changes: 1 addition & 1 deletion src/coreclr/vm/finalizerthread.h
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ class FinalizerThread

static void FinalizerThreadWait();

static void SignalFinalizationDone();
static void SignalFinalizationDone(int observedFullGcCount);

static VOID FinalizerThreadWorker(void *args);
static DWORD WINAPI FinalizerThreadStart(void *args);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using System.Runtime.InteropServices;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using System.Runtime;
using Microsoft.DotNet.RemoteExecutor;
using Xunit;
Expand Down Expand Up @@ -292,6 +293,50 @@ private class TestObject
}
}

// [OuterLoop] // TODO: VS uncomment before merging
[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsPreciseGcSupported))]
public unsafe static void WaitForPendingFinalizersRaces()
{
Task.Run(Test);
Task.Run(Test);
Task.Run(Test);
Task.Run(Test);
Task.Run(Test);
Task.Run(Test);
Test();

static void Test()
{
for (int i = 0; i < 2000; i++)
{
bool finalized = false;
MakeAndNull(&finalized);
GC.Collect();
GC.WaitForPendingFinalizers();
Assert.True(finalized);
}
}

[MethodImpl(MethodImplOptions.NoInlining)]
static void MakeAndNull(bool* flag)
{
var deadObj = new TestObjectWithFinalizer(flag);
// it's dead here
};
}

unsafe class TestObjectWithFinalizer
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is unsafe implementation required here?

Copy link
Member Author

@VSadov VSadov Jul 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is unsafe implementation required here?

Not required. I could allocate a mutable class and change a field. It is a testcase, so I used whatever was simpler.

Accessing a local on another thread's stack is technically a UB in the memory model. The current code should work in any known dotnet implementation, but perhaps should change it to just use a class - to not rely on UB.

{
bool* _flag;

public TestObjectWithFinalizer(bool* flag)
{
_flag = flag;
}

~TestObjectWithFinalizer() => *_flag = true;
}

[Fact]
public static void SuppressFinalizer_NullObject_ThrowsArgumentNullException()
{
Expand Down
Loading