Skip to content

Commit 54a9efd

Browse files
authored
Ensure that WaitForPendingFinalizers has seen the expected Full GC count (dotnet#105289)
* Ensure that WaitForPendingFinalizers has seen the expected Full GC * NativeAOT and some renames * a testcase * make the test not unsafe and make OuterLoop * Use unsigned math when comparing collection ticks * cast the diff to int when comparing gc ticks
1 parent f6b4960 commit 54a9efd

File tree

7 files changed

+132
-22
lines changed

7 files changed

+132
-22
lines changed

src/coreclr/nativeaot/Runtime.Base/src/System/Runtime/InternalCalls.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,7 @@ internal static extern unsafe IntPtr RhpCallPropagateExceptionCallback(
276276

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

281281
[DllImport(Redhawk.BaseName)]
282282
internal static extern ulong RhpGetTickCount64();

src/coreclr/nativeaot/Runtime.Base/src/System/Runtime/__Finalizer.cs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,14 @@ public static void ProcessFinalizers()
2929
// otherwise memory is low and we should initiate a collection.
3030
if (InternalCalls.RhpWaitForFinalizerRequest() != 0)
3131
{
32+
int observedFullGcCount = RuntimeImports.RhGetGcCollectionCount(RuntimeImports.RhGetMaxGcGeneration(), false);
3233
uint finalizerCount = DrainQueue();
3334

34-
// Tell anybody that's interested that the finalization pass is complete (there is a race condition here
35-
// where we might immediately signal a new request as complete, but this is acceptable).
36-
InternalCalls.RhpSignalFinalizationComplete(finalizerCount);
35+
// Anyone waiting to drain the Q can now wake up. Note that there is a
36+
// race in that another thread starting a drain, as we leave a drain, may
37+
// consider itself satisfied by the drain that just completed.
38+
// Thus we include the Full GC count that we have certaily observed.
39+
InternalCalls.RhpSignalFinalizationComplete(finalizerCount, observedFullGcCount);
3740
}
3841
else
3942
{

src/coreclr/nativeaot/Runtime/FinalizerHelpers.cpp

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,22 @@ EXTERN_C void QCALLTYPE RhInitializeFinalizerThread()
9494
g_FinalizerEvent.Set();
9595
}
9696

97+
static int32_t g_fullGcCountSeenByFinalization;
98+
99+
// Indicate that the current round of finalizations is complete.
100+
EXTERN_C void QCALLTYPE RhpSignalFinalizationComplete(uint32_t fcount, int32_t observedFullGcCount)
101+
{
102+
FireEtwGCFinalizersEnd_V1(fcount, GetClrInstanceId());
103+
104+
g_fullGcCountSeenByFinalization = observedFullGcCount;
105+
g_FinalizerDoneEvent.Set();
106+
107+
if (YieldProcessorNormalization::IsMeasurementScheduled())
108+
{
109+
YieldProcessorNormalization::PerformMeasurement();
110+
}
111+
}
112+
97113
EXTERN_C void QCALLTYPE RhWaitForPendingFinalizers(UInt32_BOOL allowReentrantWait)
98114
{
99115
// This must be called via p/invoke rather than RuntimeImport since it blocks and could starve the GC if
@@ -103,13 +119,32 @@ EXTERN_C void QCALLTYPE RhWaitForPendingFinalizers(UInt32_BOOL allowReentrantWai
103119
// Can't call this from the finalizer thread itself.
104120
if (ThreadStore::GetCurrentThread() != g_pFinalizerThread)
105121
{
122+
// We may see a completion of finalization cycle that might not see objects that became
123+
// F-reachable in recent GCs. In such case we want to wait for a completion of another cycle.
124+
// However, since an object cannot be prevented from promoting, one can only rely on Full GCs
125+
// to collect unreferenced objects deterministically. Thus we only care about Full GCs here.
126+
int desiredFullGcCount =
127+
GCHeapUtilities::GetGCHeap()->CollectionCount(GCHeapUtilities::GetGCHeap()->GetMaxGeneration());
128+
129+
tryAgain:
106130
// Clear any current indication that a finalization pass is finished and wake the finalizer thread up
107131
// (if there's no work to do it'll set the done event immediately).
108132
g_FinalizerDoneEvent.Reset();
109133
g_FinalizerEvent.Set();
110134

111135
// Wait for the finalizer thread to get back to us.
112136
g_FinalizerDoneEvent.Wait(INFINITE, false, allowReentrantWait);
137+
138+
// we use unsigned math here as the collection counts, which are size_t internally,
139+
// can in theory overflow an int and wrap around.
140+
// unsigned math would have more defined/portable behavior in such case
141+
if ((int)((unsigned int)desiredFullGcCount - (unsigned int)g_fullGcCountSeenByFinalization) > 0)
142+
{
143+
// There were some Full GCs happening before we started waiting and possibly not seen by the
144+
// last finalization cycle. This is rare, but we need to be sure we have seen those,
145+
// so we try one more time.
146+
goto tryAgain;
147+
}
113148
}
114149
}
115150

@@ -176,18 +211,6 @@ EXTERN_C UInt32_BOOL QCALLTYPE RhpWaitForFinalizerRequest()
176211
} while (true);
177212
}
178213

179-
// Indicate that the current round of finalizations is complete.
180-
EXTERN_C void QCALLTYPE RhpSignalFinalizationComplete(uint32_t fcount)
181-
{
182-
FireEtwGCFinalizersEnd_V1(fcount, GetClrInstanceId());
183-
g_FinalizerDoneEvent.Set();
184-
185-
if (YieldProcessorNormalization::IsMeasurementScheduled())
186-
{
187-
YieldProcessorNormalization::PerformMeasurement();
188-
}
189-
}
190-
191214
//
192215
// The following helpers are special in that they interact with internal GC state or directly manipulate
193216
// managed references so they're called with a special co-operative p/invoke.

src/coreclr/nativeaot/Test.CoreLib/src/System/Runtime/RuntimeImports.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,5 +104,15 @@ internal static IntPtr RhGetModuleSection(TypeManagerHandle module, ReadyToRunSe
104104
[MethodImplAttribute(MethodImplOptions.InternalCall)]
105105
[RuntimeImport(RuntimeLibrary, "RhBulkMoveWithWriteBarrier")]
106106
internal static extern unsafe void RhBulkMoveWithWriteBarrier(ref byte dmem, ref byte smem, nuint size);
107+
108+
// Get maximum GC generation number.
109+
[MethodImplAttribute(MethodImplOptions.InternalCall)]
110+
[RuntimeImport(RuntimeLibrary, "RhGetMaxGcGeneration")]
111+
internal static extern int RhGetMaxGcGeneration();
112+
113+
// Get count of collections so far.
114+
[MethodImplAttribute(MethodImplOptions.InternalCall)]
115+
[RuntimeImport(RuntimeLibrary, "RhGetGcCollectionCount")]
116+
internal static extern int RhGetGcCollectionCount(int generation, bool getSpecialGCCount);
107117
}
108118
}

src/coreclr/vm/finalizerthread.cpp

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -404,13 +404,15 @@ VOID FinalizerThread::FinalizerThreadWorker(void *args)
404404
}
405405
LOG((LF_GC, LL_INFO100, "***** Calling Finalizers\n"));
406406

407+
int observedFullGcCount =
408+
GCHeapUtilities::GetGCHeap()->CollectionCount(GCHeapUtilities::GetGCHeap()->GetMaxGeneration());
407409
FinalizeAllObjects();
408410

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

416418
if (s_InitializedFinalizerThreadForPlatform)
@@ -538,10 +540,13 @@ void FinalizerThread::FinalizerThreadCreate()
538540
}
539541
}
540542

541-
void FinalizerThread::SignalFinalizationDone()
543+
static int g_fullGcCountSeenByFinalization;
544+
545+
void FinalizerThread::SignalFinalizationDone(int observedFullGcCount)
542546
{
543547
WRAPPER_NO_CONTRACT;
544548

549+
g_fullGcCountSeenByFinalization = observedFullGcCount;
545550
hEventFinalizerDone->Set();
546551
}
547552

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

560572
#ifdef FEATURE_COMINTEROP
@@ -565,8 +577,8 @@ void FinalizerThread::FinalizerThreadWait()
565577
g_pRCWCleanupList->CleanupWrappersInCurrentCtxThread();
566578
#endif // FEATURE_COMINTEROP
567579

580+
tryAgain:
568581
hEventFinalizerDone->Reset();
569-
570582
EnableFinalization();
571583

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

581593
DWORD status;
582594
status = hEventFinalizerDone->Wait(INFINITE,TRUE);
595+
596+
// we use unsigned math here as the collection counts, which are size_t internally,
597+
// can in theory overflow an int and wrap around.
598+
// unsigned math would have more defined/portable behavior in such case
599+
if ((int)((unsigned int)desiredFullGcCount - (unsigned int)g_fullGcCountSeenByFinalization) > 0)
600+
{
601+
// There were some Full GCs happening before we started waiting and possibly not seen by the
602+
// last finalization cycle. This is rare, but we need to be sure we have seen those,
603+
// so we try one more time.
604+
goto tryAgain;
605+
}
606+
583607
_ASSERTE(status == WAIT_OBJECT_0);
584608
}
585609
}

src/coreclr/vm/finalizerthread.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ class FinalizerThread
6767

6868
static void FinalizerThreadWait();
6969

70-
static void SignalFinalizationDone();
70+
static void SignalFinalizationDone(int observedFullGcCount);
7171

7272
static VOID FinalizerThreadWorker(void *args);
7373
static DWORD WINAPI FinalizerThreadStart(void *args);

src/libraries/System.Runtime/tests/System.Runtime.Tests/System/GCTests.cs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using System.Runtime.InteropServices;
88
using System.Diagnostics;
99
using System.Threading;
10+
using System.Threading.Tasks;
1011
using System.Runtime;
1112
using Microsoft.DotNet.RemoteExecutor;
1213
using Xunit;
@@ -292,6 +293,55 @@ private class TestObject
292293
}
293294
}
294295

296+
[OuterLoop]
297+
[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsPreciseGcSupported))]
298+
public static void WaitForPendingFinalizersRaces()
299+
{
300+
Task.Run(Test);
301+
Task.Run(Test);
302+
Task.Run(Test);
303+
Task.Run(Test);
304+
Task.Run(Test);
305+
Task.Run(Test);
306+
Test();
307+
308+
static void Test()
309+
{
310+
for (int i = 0; i < 20000; i++)
311+
{
312+
BoxedFinalized flag = new BoxedFinalized();
313+
MakeAndNull(flag);
314+
GC.Collect();
315+
GC.WaitForPendingFinalizers();
316+
Assert.True(flag.finalized);
317+
}
318+
}
319+
320+
[MethodImpl(MethodImplOptions.NoInlining)]
321+
static void MakeAndNull(BoxedFinalized flag)
322+
{
323+
var deadObj = new TestObjectWithFinalizer(flag);
324+
// it's dead here
325+
};
326+
}
327+
328+
class BoxedFinalized
329+
{
330+
public bool finalized;
331+
}
332+
333+
class TestObjectWithFinalizer
334+
{
335+
BoxedFinalized _flag;
336+
337+
public TestObjectWithFinalizer(BoxedFinalized flag)
338+
{
339+
_flag = flag;
340+
}
341+
342+
~TestObjectWithFinalizer() => _flag.finalized = true;
343+
}
344+
295345
[Fact]
296346
public static void SuppressFinalizer_NullObject_ThrowsArgumentNullException()
297347
{

0 commit comments

Comments
 (0)