Skip to content

Commit 6a02ab2

Browse files
cshungjkotas
andauthored
NoGCRegion Callback (#82045)
Co-authored-by: Jan Kotas <[email protected]>
1 parent 82327bb commit 6a02ab2

File tree

29 files changed

+763
-19
lines changed

29 files changed

+763
-19
lines changed

src/coreclr/System.Private.CoreLib/src/System/GC.CoreCLR.cs

Lines changed: 92 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
using System.Collections.Generic;
1717
using System.Runtime.CompilerServices;
1818
using System.Runtime.InteropServices;
19+
using System.Threading;
1920

2021
namespace System
2122
{
@@ -615,6 +616,95 @@ internal static void RegisterMemoryLoadChangeNotification(float lowMemoryPercent
615616
}
616617
}
617618

619+
private unsafe struct NoGCRegionCallbackFinalizerWorkItem
620+
{
621+
// FinalizerWorkItem
622+
public NoGCRegionCallbackFinalizerWorkItem* next;
623+
public delegate* unmanaged<NoGCRegionCallbackFinalizerWorkItem*, void> callback;
624+
625+
public bool scheduled;
626+
public bool abandoned;
627+
628+
public GCHandle action;
629+
}
630+
631+
internal enum EnableNoGCRegionCallbackStatus
632+
{
633+
Success,
634+
NotStarted,
635+
InsufficientBudget,
636+
AlreadyRegistered,
637+
}
638+
639+
/// <summary>
640+
/// Register a callback to be invoked when we allocated a certain amount of memory in the no GC region.
641+
/// <param name="totalSize">The total size of the no GC region. Must be a number > 0 or an ArgumentOutOfRangeException will be thrown.</param>
642+
/// <param name="callback">The callback to be executed when we allocated a certain amount of memory in the no GC region..</param>
643+
/// <exception cref="System.ArgumentOutOfRangeException"> The <paramref name="totalSize"/> argument is less than or equal to 0.</exception>
644+
/// <exception cref="System.ArgumentNullException">The <paramref name="callback"/> argument is null.</exception>
645+
/// <exception cref="InvalidOperationException"><para>The GC is not currently under a NoGC region.</para>
646+
/// <para>-or-</para>
647+
/// <para>Another callback is already registered.</para>
648+
/// <para>-or-</para>
649+
/// <para>The <paramref name="totalSize"/> exceeds the size of the No GC region.</para>
650+
/// <para>-or-</para>
651+
/// <para>We failed to withheld memory for the callback before of already made allocation.</para>
652+
/// </exception>
653+
/// </summary>
654+
public static unsafe void RegisterNoGCRegionCallback(long totalSize, Action callback)
655+
{
656+
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(totalSize);
657+
ArgumentNullException.ThrowIfNull(callback);
658+
659+
NoGCRegionCallbackFinalizerWorkItem* pWorkItem = null;
660+
try
661+
{
662+
pWorkItem = (NoGCRegionCallbackFinalizerWorkItem*)NativeMemory.AllocZeroed((nuint)sizeof(NoGCRegionCallbackFinalizerWorkItem));
663+
pWorkItem->action = GCHandle.Alloc(callback);
664+
pWorkItem->callback = &Callback;
665+
666+
EnableNoGCRegionCallbackStatus status = (EnableNoGCRegionCallbackStatus)_EnableNoGCRegionCallback(pWorkItem, totalSize);
667+
if (status != EnableNoGCRegionCallbackStatus.Success)
668+
{
669+
switch (status)
670+
{
671+
case EnableNoGCRegionCallbackStatus.NotStarted:
672+
throw new InvalidOperationException(SR.Format(SR.InvalidOperationException_NoGCRegionNotInProgress));
673+
case EnableNoGCRegionCallbackStatus.InsufficientBudget:
674+
throw new InvalidOperationException(SR.Format(SR.InvalidOperationException_NoGCRegionAllocationExceeded));
675+
case EnableNoGCRegionCallbackStatus.AlreadyRegistered:
676+
throw new InvalidOperationException(SR.InvalidOperationException_NoGCRegionCallbackAlreadyRegistered);
677+
}
678+
Debug.Assert(false);
679+
}
680+
pWorkItem = null; // Ownership transferred
681+
}
682+
finally
683+
{
684+
if (pWorkItem != null)
685+
Free(pWorkItem);
686+
}
687+
688+
[UnmanagedCallersOnly]
689+
static void Callback(NoGCRegionCallbackFinalizerWorkItem* pWorkItem)
690+
{
691+
Debug.Assert(pWorkItem->scheduled);
692+
if (!pWorkItem->abandoned)
693+
((Action)(pWorkItem->action.Target!))();
694+
Free(pWorkItem);
695+
}
696+
697+
static void Free(NoGCRegionCallbackFinalizerWorkItem* pWorkItem)
698+
{
699+
if (pWorkItem->action.IsAllocated)
700+
pWorkItem->action.Free();
701+
NativeMemory.Free(pWorkItem);
702+
}
703+
}
704+
705+
[LibraryImport(RuntimeHelpers.QCall, EntryPoint = "GCInterface_EnableNoGCRegionCallback")]
706+
private static unsafe partial EnableNoGCRegionCallbackStatus _EnableNoGCRegionCallback(NoGCRegionCallbackFinalizerWorkItem* callback, long totalSize);
707+
618708
internal static void UnregisterMemoryLoadChangeNotification(Action notification)
619709
{
620710
ArgumentNullException.ThrowIfNull(notification);
@@ -720,7 +810,7 @@ internal struct GCConfigurationContext
720810
}
721811

722812
[UnmanagedCallersOnly]
723-
private static unsafe void Callback(void* configurationContext, void* name, void* publicKey, GCConfigurationType type, long data)
813+
private static unsafe void ConfigCallback(void* configurationContext, void* name, void* publicKey, GCConfigurationType type, long data)
724814
{
725815
// If the public key is null, it means that the corresponding configuration isn't publicly available
726816
// and therefore, we shouldn't add it to the configuration dictionary to return to the user.
@@ -768,7 +858,7 @@ public static unsafe IReadOnlyDictionary<string, object> GetConfigurationVariabl
768858
Configurations = new Dictionary<string, object>()
769859
};
770860

771-
_EnumerateConfigurationValues(Unsafe.AsPointer(ref context), &Callback);
861+
_EnumerateConfigurationValues(Unsafe.AsPointer(ref context), &ConfigCallback);
772862
return context.Configurations!;
773863
}
774864

src/coreclr/gc/env/gcenv.ee.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ class GCToEEInterface
6767
static void DiagWalkBGCSurvivors(void* gcContext);
6868
static void StompWriteBarrier(WriteBarrierParameters* args);
6969

70-
static void EnableFinalization(bool foundFinalizers);
70+
static void EnableFinalization(bool gcHasWorkForFinalizerThread);
7171

7272
static void HandleFatalError(unsigned int exitCode);
7373
static bool EagerFinalized(Object* obj);

src/coreclr/gc/gc.cpp

Lines changed: 188 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2717,6 +2717,7 @@ size_t gc_heap::interesting_data_per_gc[max_idp_count];
27172717
#endif //MULTIPLE_HEAPS
27182718

27192719
no_gc_region_info gc_heap::current_no_gc_region_info;
2720+
FinalizerWorkItem* gc_heap::finalizer_work;
27202721
BOOL gc_heap::proceed_with_gc_p = FALSE;
27212722
GCSpinLock gc_heap::gc_lock;
27222723

@@ -11472,7 +11473,16 @@ heap_segment* gc_heap::get_free_region (int gen_number, size_t size)
1147211473
region = free_regions[huge_free_region].unlink_smallest_region (size);
1147311474
if (region == nullptr)
1147411475
{
11475-
ASSERT_HOLDING_SPIN_LOCK(&gc_lock);
11476+
if (settings.pause_mode == pause_no_gc)
11477+
{
11478+
// In case of no-gc-region, the gc lock is being held by the thread
11479+
// triggering the GC.
11480+
assert (gc_lock.holding_thread != (Thread*)-1);
11481+
}
11482+
else
11483+
{
11484+
ASSERT_HOLDING_SPIN_LOCK(&gc_lock);
11485+
}
1147611486

1147711487
// get it from the global list of huge free regions
1147811488
region = global_free_huge_regions.unlink_smallest_region (size);
@@ -21228,11 +21238,41 @@ BOOL gc_heap::should_proceed_with_gc()
2122821238
{
2122921239
if (current_no_gc_region_info.started)
2123021240
{
21231-
// The no_gc mode was already in progress yet we triggered another GC,
21232-
// this effectively exits the no_gc mode.
21233-
restore_data_for_no_gc();
21241+
if (current_no_gc_region_info.soh_withheld_budget != 0)
21242+
{
21243+
dprintf(1, ("[no_gc_callback] allocation budget exhausted with withheld, time to trigger callback\n"));
21244+
#ifdef MULTIPLE_HEAPS
21245+
for (int i = 0; i < gc_heap::n_heaps; i++)
21246+
{
21247+
gc_heap* hp = gc_heap::g_heaps [i];
21248+
#else
21249+
{
21250+
gc_heap* hp = pGenGCHeap;
21251+
#endif
21252+
dd_new_allocation (hp->dynamic_data_of (soh_gen0)) += current_no_gc_region_info.soh_withheld_budget;
21253+
dd_new_allocation (hp->dynamic_data_of (loh_generation)) += current_no_gc_region_info.loh_withheld_budget;
21254+
}
21255+
current_no_gc_region_info.soh_withheld_budget = 0;
21256+
current_no_gc_region_info.loh_withheld_budget = 0;
2123421257

21235-
memset (&current_no_gc_region_info, 0, sizeof (current_no_gc_region_info));
21258+
// Trigger the callback
21259+
schedule_no_gc_callback (false);
21260+
current_no_gc_region_info.callback = nullptr;
21261+
return FALSE;
21262+
}
21263+
else
21264+
{
21265+
dprintf(1, ("[no_gc_callback] GC triggered while in no_gc mode. Exiting no_gc mode.\n"));
21266+
// The no_gc mode was already in progress yet we triggered another GC,
21267+
// this effectively exits the no_gc mode.
21268+
restore_data_for_no_gc();
21269+
if (current_no_gc_region_info.callback != nullptr)
21270+
{
21271+
dprintf (1, ("[no_gc_callback] detaching callback on exit"));
21272+
schedule_no_gc_callback (true);
21273+
}
21274+
memset (&current_no_gc_region_info, 0, sizeof (current_no_gc_region_info));
21275+
}
2123621276
}
2123721277
else
2123821278
return should_proceed_for_no_gc();
@@ -22345,14 +22385,50 @@ end_no_gc_region_status gc_heap::end_no_gc_region()
2234522385
status = end_no_gc_alloc_exceeded;
2234622386

2234722387
if (settings.pause_mode == pause_no_gc)
22388+
{
2234822389
restore_data_for_no_gc();
22390+
if (current_no_gc_region_info.callback != nullptr)
22391+
{
22392+
dprintf (1, ("[no_gc_callback] detaching callback on exit"));
22393+
schedule_no_gc_callback (true);
22394+
}
22395+
}
2234922396

2235022397
// sets current_no_gc_region_info.started to FALSE here.
2235122398
memset (&current_no_gc_region_info, 0, sizeof (current_no_gc_region_info));
2235222399

2235322400
return status;
2235422401
}
2235522402

22403+
void gc_heap::schedule_no_gc_callback (bool abandoned)
22404+
{
22405+
// We still want to schedule the work even when the no-gc callback is abandoned
22406+
// so that we can free the memory associated with it.
22407+
current_no_gc_region_info.callback->abandoned = abandoned;
22408+
22409+
if (!current_no_gc_region_info.callback->scheduled)
22410+
{
22411+
current_no_gc_region_info.callback->scheduled = true;
22412+
schedule_finalizer_work(current_no_gc_region_info.callback);
22413+
}
22414+
}
22415+
22416+
void gc_heap::schedule_finalizer_work (FinalizerWorkItem* callback)
22417+
{
22418+
FinalizerWorkItem* prev;
22419+
do
22420+
{
22421+
prev = finalizer_work;
22422+
callback->next = prev;
22423+
}
22424+
while (Interlocked::CompareExchangePointer (&finalizer_work, callback, prev) != prev);
22425+
22426+
if (prev == nullptr)
22427+
{
22428+
GCToEEInterface::EnableFinalization(true);
22429+
}
22430+
}
22431+
2235622432
//update counters
2235722433
void gc_heap::update_collection_counts ()
2235822434
{
@@ -44395,6 +44471,103 @@ class NoGCRegionLockHolder
4439544471
}
4439644472
};
4439744473

44474+
enable_no_gc_region_callback_status gc_heap::enable_no_gc_callback(NoGCRegionCallbackFinalizerWorkItem* callback, uint64_t callback_threshold)
44475+
{
44476+
dprintf(1, ("[no_gc_callback] calling enable_no_gc_callback with callback_threshold = %llu\n", callback_threshold));
44477+
enable_no_gc_region_callback_status status = enable_no_gc_region_callback_status::succeed;
44478+
suspend_EE();
44479+
{
44480+
if (!current_no_gc_region_info.started)
44481+
{
44482+
status = enable_no_gc_region_callback_status::not_started;
44483+
}
44484+
else if (current_no_gc_region_info.callback != nullptr)
44485+
{
44486+
status = enable_no_gc_region_callback_status::already_registered;
44487+
}
44488+
else
44489+
{
44490+
uint64_t total_original_soh_budget = 0;
44491+
uint64_t total_original_loh_budget = 0;
44492+
#ifdef MULTIPLE_HEAPS
44493+
for (int i = 0; i < gc_heap::n_heaps; i++)
44494+
{
44495+
gc_heap* hp = gc_heap::g_heaps [i];
44496+
#else
44497+
{
44498+
gc_heap* hp = pGenGCHeap;
44499+
#endif
44500+
total_original_soh_budget += hp->soh_allocation_no_gc;
44501+
total_original_loh_budget += hp->loh_allocation_no_gc;
44502+
}
44503+
uint64_t total_original_budget = total_original_soh_budget + total_original_loh_budget;
44504+
if (total_original_budget >= callback_threshold)
44505+
{
44506+
uint64_t total_withheld = total_original_budget - callback_threshold;
44507+
44508+
float soh_ratio = ((float)total_original_soh_budget)/total_original_budget;
44509+
float loh_ratio = ((float)total_original_loh_budget)/total_original_budget;
44510+
44511+
size_t soh_withheld_budget = (size_t)(soh_ratio * total_withheld);
44512+
size_t loh_withheld_budget = (size_t)(loh_ratio * total_withheld);
44513+
44514+
#ifdef MULTIPLE_HEAPS
44515+
soh_withheld_budget = soh_withheld_budget / gc_heap::n_heaps;
44516+
loh_withheld_budget = loh_withheld_budget / gc_heap::n_heaps;
44517+
#endif
44518+
soh_withheld_budget = max(soh_withheld_budget, 1);
44519+
soh_withheld_budget = Align(soh_withheld_budget, get_alignment_constant (TRUE));
44520+
loh_withheld_budget = Align(loh_withheld_budget, get_alignment_constant (FALSE));
44521+
#ifdef MULTIPLE_HEAPS
44522+
for (int i = 0; i < gc_heap::n_heaps; i++)
44523+
{
44524+
gc_heap* hp = gc_heap::g_heaps [i];
44525+
#else
44526+
{
44527+
gc_heap* hp = pGenGCHeap;
44528+
#endif
44529+
if (dd_new_allocation (hp->dynamic_data_of (soh_gen0)) <= (ptrdiff_t)soh_withheld_budget)
44530+
{
44531+
dprintf(1, ("[no_gc_callback] failed because of running out of soh budget= %llu\n", soh_withheld_budget));
44532+
status = insufficient_budget;
44533+
}
44534+
if (dd_new_allocation (hp->dynamic_data_of (loh_generation)) <= (ptrdiff_t)loh_withheld_budget)
44535+
{
44536+
dprintf(1, ("[no_gc_callback] failed because of running out of loh budget= %llu\n", loh_withheld_budget));
44537+
status = insufficient_budget;
44538+
}
44539+
}
44540+
44541+
if (status == enable_no_gc_region_callback_status::succeed)
44542+
{
44543+
dprintf(1, ("[no_gc_callback] enabling succeed\n"));
44544+
#ifdef MULTIPLE_HEAPS
44545+
for (int i = 0; i < gc_heap::n_heaps; i++)
44546+
{
44547+
gc_heap* hp = gc_heap::g_heaps [i];
44548+
#else
44549+
{
44550+
gc_heap* hp = pGenGCHeap;
44551+
#endif
44552+
dd_new_allocation (hp->dynamic_data_of (soh_gen0)) -= soh_withheld_budget;
44553+
dd_new_allocation (hp->dynamic_data_of (loh_generation)) -= loh_withheld_budget;
44554+
}
44555+
current_no_gc_region_info.soh_withheld_budget = soh_withheld_budget;
44556+
current_no_gc_region_info.loh_withheld_budget = loh_withheld_budget;
44557+
current_no_gc_region_info.callback = callback;
44558+
}
44559+
}
44560+
else
44561+
{
44562+
status = enable_no_gc_region_callback_status::insufficient_budget;
44563+
}
44564+
}
44565+
}
44566+
restart_EE();
44567+
44568+
return status;
44569+
}
44570+
4439844571
// An explanation of locking for finalization:
4439944572
//
4440044573
// Multiple threads allocate objects. During the allocation, they are serialized by
@@ -46135,6 +46308,16 @@ unsigned int GCHeap::WhichGeneration (Object* object)
4613546308
return g;
4613646309
}
4613746310

46311+
enable_no_gc_region_callback_status GCHeap::EnableNoGCRegionCallback(NoGCRegionCallbackFinalizerWorkItem* callback, uint64_t callback_threshold)
46312+
{
46313+
return gc_heap::enable_no_gc_callback(callback, callback_threshold);
46314+
}
46315+
46316+
FinalizerWorkItem* GCHeap::GetExtraWorkForFinalization()
46317+
{
46318+
return Interlocked::ExchangePointer(&gc_heap::finalizer_work, nullptr);
46319+
}
46320+
4613846321
unsigned int GCHeap::GetGenerationWithRange (Object* object, uint8_t** ppStart, uint8_t** ppAllocated, uint8_t** ppReserved)
4613946322
{
4614046323
int generation = -1;

src/coreclr/gc/gcenv.ee.standalone.inl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -183,10 +183,10 @@ inline void GCToEEInterface::StompWriteBarrier(WriteBarrierParameters* args)
183183
g_theGCToCLR->StompWriteBarrier(args);
184184
}
185185

186-
inline void GCToEEInterface::EnableFinalization(bool foundFinalizers)
186+
inline void GCToEEInterface::EnableFinalization(bool gcHasWorkForFinalizerThread)
187187
{
188188
assert(g_theGCToCLR != nullptr);
189-
g_theGCToCLR->EnableFinalization(foundFinalizers);
189+
g_theGCToCLR->EnableFinalization(gcHasWorkForFinalizerThread);
190190
}
191191

192192
inline void GCToEEInterface::HandleFatalError(unsigned int exitCode)

src/coreclr/gc/gcimpl.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,8 @@ class GCHeap : public IGCHeapInternal
202202

203203
int StartNoGCRegion(uint64_t totalSize, bool lohSizeKnown, uint64_t lohSize, bool disallowFullBlockingGC);
204204
int EndNoGCRegion();
205+
enable_no_gc_region_callback_status EnableNoGCRegionCallback(NoGCRegionCallbackFinalizerWorkItem* callback, uint64_t callback_threshold);
206+
FinalizerWorkItem* GetExtraWorkForFinalization();
205207

206208
unsigned GetGcCount();
207209

0 commit comments

Comments
 (0)