Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
455555e
wip
pavelsavara Jan 26, 2026
4d248aa
GCX_COOP, BEGIN_EXTERNAL_ENTRYPOINT, EX_TRY
pavelsavara Jan 26, 2026
a97615b
Merge branch 'main' into browser_finalizer
pavelsavara Jan 26, 2026
74ded88
fix
pavelsavara Jan 26, 2026
ff1340e
Update src/coreclr/vm/finalizerthread.cpp
pavelsavara Jan 26, 2026
5d1b3fe
Update src/coreclr/vm/finalizerthread.cpp
pavelsavara Jan 26, 2026
8c843c9
Update src/coreclr/vm/ceemain.cpp
pavelsavara Jan 26, 2026
c15760c
Update src/coreclr/vm/finalizerthread.cpp
pavelsavara Jan 26, 2026
9d77348
Update src/coreclr/vm/finalizerthread.cpp
pavelsavara Jan 26, 2026
51d7483
Update src/coreclr/vm/finalizerthread.cpp
pavelsavara Jan 26, 2026
8272ca1
Update src/coreclr/vm/finalizerthread.h
pavelsavara Jan 26, 2026
cdb1fd7
feedback
pavelsavara Jan 26, 2026
c57b7be
feedback
pavelsavara Jan 26, 2026
9abe1be
capture the current (single) thread as the finalizer thread
pavelsavara Jan 26, 2026
943a522
DisablePreemptiveGC() at the end
pavelsavara Jan 26, 2026
045c722
Merge branch 'main' into browser_finalizer
pavelsavara Jan 27, 2026
5b9ce38
feedback
pavelsavara Jan 27, 2026
df7b420
feedback
pavelsavara Jan 27, 2026
0e13fbb
more contracts
pavelsavara Jan 27, 2026
4f0aab4
Update src/coreclr/vm/finalizerthread.cpp
pavelsavara Jan 27, 2026
364d6d2
DoExtraWorkForFinalizer feedback
pavelsavara Jan 27, 2026
3c89e41
Merge branch 'main' into browser_finalizer
pavelsavara Jan 27, 2026
5a73a97
Update src/coreclr/vm/finalizerthread.cpp
pavelsavara Jan 27, 2026
945bad5
feedback
pavelsavara Jan 27, 2026
5904b9d
Merge branch 'main' into browser_finalizer
pavelsavara Jan 28, 2026
901a515
fail faster
pavelsavara Jan 28, 2026
ec59876
fail fast on throttling
pavelsavara Jan 28, 2026
623e43b
TODO 123712
pavelsavara Feb 1, 2026
9e27094
Merge branch 'main' into browser_finalizer
pavelsavara Feb 1, 2026
a1a3840
ActiveIssue https://github.com/dotnet/runtime/issues/123572
pavelsavara Feb 1, 2026
042ec8e
unix only contract
pavelsavara Feb 2, 2026
5354bf4
TARGET_BROWSER feedback
pavelsavara Feb 2, 2026
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
4 changes: 2 additions & 2 deletions src/coreclr/vm/ceemain.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -922,8 +922,8 @@ void EEStartupHelper()
// right before doing HasStarted(). We will release it now.
FinalizerThread::EnableFinalization();
#elif defined(TARGET_WASM)
// on wasm we need to run finalizers on main thread as we are single threaded
// active issue: https://github.com/dotnet/runtime/issues/114096
// On wasm this schedules finalization onto the browser event loop.
FinalizerThread::EnableFinalization();
#else
// This isn't done as part of InitializeGarbageCollector() above because
// debugger must be initialized before creating EE thread objects
Expand Down
229 changes: 148 additions & 81 deletions src/coreclr/vm/finalizerthread.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,54 @@ bool FinalizerThread::IsCurrentThreadFinalizer()
return GetThreadNULLOk() == g_pFinalizerThread;
}

#ifdef TARGET_WASM

extern "C" void SystemJS_ScheduleFinalization();
extern "C"
{
void SystemJS_ExecuteFinalizationCallback()
{
CONTRACTL
{
NOTHROW;
GC_TRIGGERS;
ENTRY_POINT; // This is called by a host.
}
CONTRACTL_END;

HRESULT hr=S_OK;

Comment thread
pavelsavara marked this conversation as resolved.
Outdated
BEGIN_EXTERNAL_ENTRYPOINT(&hr);

EX_TRY
{
GCX_COOP();
FinalizerThread::FinalizerThreadWorkerIteration();
}
EX_HOOK
{
Exception *ex = GET_EXCEPTION();
SString err;
ex->GetMessage(err);
LogErrorToHost("Error message: %s", err.GetUTF8());
abort();
Comment thread
pavelsavara marked this conversation as resolved.
Outdated
}
EX_END_HOOK;

END_EXTERNAL_ENTRYPOINT;
}
Comment thread
pavelsavara marked this conversation as resolved.
}

#endif // !TARGET_WASM
Comment thread
pavelsavara marked this conversation as resolved.
Outdated

void FinalizerThread::EnableFinalization()
{
WRAPPER_NO_CONTRACT;

#ifndef TARGET_WASM
hEventFinalizer->Set();
#else // !TARGET_WASM
SystemJS_ScheduleFinalization();
#endif // !TARGET_WASM
Comment thread
pavelsavara marked this conversation as resolved.
}

Expand Down Expand Up @@ -379,123 +421,148 @@ void FinalizerThread::WaitForFinalizerEvent (CLREvent *event)

static BOOL s_FinalizerThreadOK = FALSE;
static BOOL s_InitializedFinalizerThreadForPlatform = FALSE;
static BOOL s_PriorityBoosted = FALSE;

VOID FinalizerThread::FinalizerThreadWorker(void *args)
{
BOOL bPriorityBoosted = FALSE;

while (!fQuitFinalizer)
{
// Wait for work to do...
FinalizerThread::FinalizerThreadWorkerIteration();
}

if (s_InitializedFinalizerThreadForPlatform)
Thread::CleanUpForManagedThreadInNative(GetFinalizerThread());
}

VOID FinalizerThread::FinalizerThreadWorkerIteration()
{
Comment thread
jkotas marked this conversation as resolved.
Comment thread
pavelsavara marked this conversation as resolved.
#ifndef TARGET_WASM
// Wait for work to do...

_ASSERTE(GetFinalizerThread()->PreemptiveGCDisabled());
#ifdef _DEBUG
Comment thread
pavelsavara marked this conversation as resolved.
if (g_pConfig->FastGCStressLevel())
{
GetFinalizerThread()->m_GCOnTransitionsOK = FALSE;
}
if (g_pConfig->FastGCStressLevel())
{
GetFinalizerThread()->m_GCOnTransitionsOK = FALSE;
}
#endif
GetFinalizerThread()->EnablePreemptiveGC();
GetFinalizerThread()->EnablePreemptiveGC();
#ifdef _DEBUG
if (g_pConfig->FastGCStressLevel())
{
GetFinalizerThread()->m_GCOnTransitionsOK = TRUE;
}
if (g_pConfig->FastGCStressLevel())
{
GetFinalizerThread()->m_GCOnTransitionsOK = TRUE;
}
#endif
#if 0
// Setting the event here, instead of at the bottom of the loop, could
// cause us to skip draining the Q, if the request is made as soon as
// the app starts running.
SignalFinalizationDone();
// Setting the event here, instead of at the bottom of the loop, could
// cause us to skip draining the Q, if the request is made as soon as
// the app starts running.
SignalFinalizationDone();
#endif //0

WaitForFinalizerEvent (hEventFinalizer);
WaitForFinalizerEvent (hEventFinalizer);

// Process pending finalizer work items from the GC first.
FinalizerWorkItem* pWork = GCHeapUtilities::GetGCHeap()->GetExtraWorkForFinalization();
while (pWork != NULL)
{
FinalizerWorkItem* pNext = pWork->next;
pWork->callback(pWork);
pWork = pNext;
}
// Process pending finalizer work items from the GC first.
Comment thread
jkotas marked this conversation as resolved.
FinalizerWorkItem* pWork = GCHeapUtilities::GetGCHeap()->GetExtraWorkForFinalization();
while (pWork != NULL)
{
FinalizerWorkItem* pNext = pWork->next;
pWork->callback(pWork);
pWork = pNext;
}

#if defined(__linux__) && defined(FEATURE_EVENT_TRACE)
if (g_TriggerHeapDump && (minipal_lowres_ticks() > (LastHeapDumpTime + LINUX_HEAP_DUMP_TIME_OUT)))
{
s_forcedGCInProgress = true;
GetFinalizerThread()->DisablePreemptiveGC();
GCHeapUtilities::GetGCHeap()->GarbageCollect(2, false, collection_blocking);
GetFinalizerThread()->EnablePreemptiveGC();
s_forcedGCInProgress = false;
if (g_TriggerHeapDump && (minipal_lowres_ticks() > (LastHeapDumpTime + LINUX_HEAP_DUMP_TIME_OUT)))
{
s_forcedGCInProgress = true;
GetFinalizerThread()->DisablePreemptiveGC();
GCHeapUtilities::GetGCHeap()->GarbageCollect(2, false, collection_blocking);
GetFinalizerThread()->EnablePreemptiveGC();
s_forcedGCInProgress = false;

LastHeapDumpTime = minipal_lowres_ticks();
g_TriggerHeapDump = FALSE;
}
LastHeapDumpTime = minipal_lowres_ticks();
g_TriggerHeapDump = FALSE;
}
#endif
if (gcGenAnalysisState == GcGenAnalysisState::Done)
if (gcGenAnalysisState == GcGenAnalysisState::Done)
{
gcGenAnalysisState = GcGenAnalysisState::Disabled;
if (gcGenAnalysisTrace)
{
gcGenAnalysisState = GcGenAnalysisState::Disabled;
if (gcGenAnalysisTrace)
{
#ifdef FEATURE_PERFTRACING
EventPipeAdapter::Disable(gcGenAnalysisEventPipeSessionId);
EventPipeAdapter::Disable(gcGenAnalysisEventPipeSessionId);
#ifdef GEN_ANALYSIS_STRESS
GenAnalysis::EnableGenerationalAwareSession();
GenAnalysis::EnableGenerationalAwareSession();
#endif //GEN_ANALYSIS_STRESS
#endif //FEATURE_PERFTRACING
}

// Writing an empty file to indicate completion
WCHAR outputPath[MAX_PATH];
ReplacePid(GENAWARE_COMPLETION_FILE_NAME, outputPath, MAX_PATH);
FILE* fp = NULL;
if (fopen_lp(&fp, outputPath, W("w+")) == 0)
{
fclose(fp);
}
}

if (!bPriorityBoosted)
// Writing an empty file to indicate completion
WCHAR outputPath[MAX_PATH];
ReplacePid(GENAWARE_COMPLETION_FILE_NAME, outputPath, MAX_PATH);
FILE* fp = NULL;
if (fopen_lp(&fp, outputPath, W("w+")) == 0)
{
if (GetFinalizerThread()->SetThreadPriority(THREAD_PRIORITY_HIGHEST))
bPriorityBoosted = TRUE;
fclose(fp);
}
}

// The Finalizer thread is started very early in EE startup. We deferred
// some initialization until a point we are sure the EE is up and running. At
// this point we make a single attempt and if it fails won't try again.
if (!s_InitializedFinalizerThreadForPlatform)
{
s_InitializedFinalizerThreadForPlatform = TRUE;
Thread::InitializationForManagedThreadInNative(GetFinalizerThread());
}
if (!s_PriorityBoosted)
{
if (GetFinalizerThread()->SetThreadPriority(THREAD_PRIORITY_HIGHEST))
s_PriorityBoosted = TRUE;
}

JitHost::Reclaim();
// The Finalizer thread is started very early in EE startup. We deferred
// some initialization until a point we are sure the EE is up and running. At
// this point we make a single attempt and if it fails won't try again.
if (!s_InitializedFinalizerThreadForPlatform)
{
s_InitializedFinalizerThreadForPlatform = TRUE;
Thread::InitializationForManagedThreadInNative(GetFinalizerThread());
}

GetFinalizerThread()->DisablePreemptiveGC();
JitHost::Reclaim();

// we might want to do some extra work on the finalizer thread
// check and do it
if (HaveExtraWorkForFinalizer())
{
DoExtraWorkForFinalizer(GetFinalizerThread());
}
LOG((LF_GC, LL_INFO100, "***** Calling Finalizers\n"));
GetFinalizerThread()->DisablePreemptiveGC();

int observedFullGcCount =
GCHeapUtilities::GetGCHeap()->CollectionCount(GCHeapUtilities::GetGCHeap()->GetMaxGeneration());
FinalizeAllObjects();
// we might want to do some extra work on the finalizer thread
// check and do it
if (HaveExtraWorkForFinalizer())
{
DoExtraWorkForFinalizer(GetFinalizerThread());
}
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.
// Thus we include the Full GC count that we have certaily observed.
SignalFinalizationDone(observedFullGcCount);
// 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.
SignalFinalizationDone(observedFullGcCount);

#else // !TARGET_WASM

// Process pending finalizer work items from the GC first.
FinalizerWorkItem* pWork = GCHeapUtilities::GetGCHeap()->GetExtraWorkForFinalization();
Comment thread
pavelsavara marked this conversation as resolved.
Outdated
while (pWork != NULL)
{
FinalizerWorkItem* pNext = pWork->next;
pWork->callback(pWork);
pWork = pNext;
}

if (s_InitializedFinalizerThreadForPlatform)
Thread::CleanUpForManagedThreadInNative(GetFinalizerThread());
JitHost::Reclaim();

LOG((LF_GC, LL_INFO100, "***** Calling Finalizers\n"));

int observedFullGcCount =
GCHeapUtilities::GetGCHeap()->CollectionCount(GCHeapUtilities::GetGCHeap()->GetMaxGeneration());
FinalizeAllObjects();
#endif // !TARGET_WASM
}

DWORD WINAPI FinalizerThread::FinalizerThreadStart(void *args)
Expand Down
3 changes: 3 additions & 0 deletions src/coreclr/vm/finalizerthread.h
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ class FinalizerThread
static void SignalFinalizationDone(int observedFullGcCount);

static VOID FinalizerThreadWorker(void *args);

static VOID FinalizerThreadWorkerIteration();

static DWORD WINAPI FinalizerThreadStart(void *args);

static void FinalizerThreadCreate();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -232,8 +232,7 @@ private static bool GetLinqExpressionsBuiltWithIsInterpretingOnly()
// heavily on Reflection.Emit
public static bool IsXmlDsigXsltTransformSupported => !PlatformDetection.IsInAppContainer && IsReflectionEmitSupported;

public static bool IsPreciseGcSupported => !IsMonoRuntime
&& !IsBrowser; // TODO-WASM: https://github.com/dotnet/runtime/issues/114096
public static bool IsPreciseGcSupported => !IsMonoRuntime;
Comment thread
pavelsavara marked this conversation as resolved.
Comment thread
pavelsavara marked this conversation as resolved.

public static bool IsRareEnumsSupported => !IsNativeAot;

Expand Down
1 change: 1 addition & 0 deletions src/native/libs/Common/JavaScript/types/ems-ambient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export type EmsAmbientSymbolsType = EmscriptenModuleInternal & {
_GetDotNetRuntimeContractDescriptor: () => void;
_SystemJS_ExecuteTimerCallback: () => void;
_SystemJS_ExecuteBackgroundJobCallback: () => void;
_SystemJS_ExecuteFinalizationCallback: () => void;
_BrowserHost_CreateHostContract: () => VoidPtr;
_BrowserHost_InitializeCoreCLR: (propertiesCount: number, propertyKeys: CharPtrPtr, propertyValues: CharPtrPtr) => number;
_BrowserHost_ExecuteAssembly: (mainAssemblyNamePtr: number, argsLength: number, argsPtr: number) => number;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@

let commonDeps = [
"$BROWSER_UTILS",
"SystemJS_ExecuteTimerCallback", "SystemJS_ExecuteBackgroundJobCallback"
"SystemJS_ExecuteTimerCallback",
"SystemJS_ExecuteBackgroundJobCallback",
"SystemJS_ExecuteFinalizationCallback",
];
const lib = {
$DOTNET: {
Expand Down
2 changes: 1 addition & 1 deletion src/native/libs/System.Native.Browser/native/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import GitHash from "consts:gitHash";
export { SystemJS_RandomBytes } from "./crypto";
export { SystemJS_GetLocaleInfo } from "./globalization-locale";
export { SystemJS_RejectMainPromise, SystemJS_ResolveMainPromise, SystemJS_ConsoleClear } from "./main";
export { SystemJS_ScheduleTimer, SystemJS_ScheduleBackgroundJob } from "./scheduling";
export { SystemJS_ScheduleTimer, SystemJS_ScheduleBackgroundJob, SystemJS_ScheduleFinalization } from "./scheduling";

export const gitHash = GitHash;
export function dotnetInitializeModule(internals: InternalExchange): void {
Expand Down
14 changes: 14 additions & 0 deletions src/native/libs/System.Native.Browser/native/scheduling.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,17 @@ export function SystemJS_ScheduleBackgroundJob(): void {
_ems_._SystemJS_ExecuteBackgroundJobCallback();
}
}

export function SystemJS_ScheduleFinalization(): void {
if (_ems_.DOTNET.lastScheduledFinalizationId) {
globalThis.clearTimeout(_ems_.DOTNET.lastScheduledFinalizationId);
_ems_.runtimeKeepalivePop();
_ems_.DOTNET.lastScheduledFinalizationId = undefined;
}
_ems_.DOTNET.lastScheduledFinalizationId = _ems_.safeSetTimeout(SystemJS_ScheduleFinalizationTick, 0);

function SystemJS_ScheduleFinalizationTick(): void {
_ems_.DOTNET.lastScheduledFinalizationId = undefined;
_ems_._SystemJS_ExecuteFinalizationCallback();
}
}
5 changes: 5 additions & 0 deletions src/native/libs/System.Native.Browser/utils/host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ export function abortBackgroundTimers(): void {
_ems_.runtimeKeepalivePop();
_ems_.DOTNET.lastScheduledThreadPoolId = undefined;
}
if (_ems_.DOTNET.lastScheduledFinalizationId) {
globalThis.clearTimeout(_ems_.DOTNET.lastScheduledFinalizationId);
_ems_.runtimeKeepalivePop();
_ems_.DOTNET.lastScheduledFinalizationId = undefined;
}
}

export function abortPosix(exitCode: number): void {
Expand Down