From 35cdabdc59b21f65535a4ecf42a2435944679ec4 Mon Sep 17 00:00:00 2001 From: Eduardo Speroni Date: Wed, 13 May 2026 15:52:53 -0300 Subject: [PATCH] feat: implement message loop observer for V8's foreground task queue --- NativeScript/runtime/Runtime.h | 1 + NativeScript/runtime/Runtime.mm | 42 +++++++++++++++++++++++++++------ 2 files changed, 36 insertions(+), 7 deletions(-) diff --git a/NativeScript/runtime/Runtime.h b/NativeScript/runtime/Runtime.h index 92d95dba..9ff9290c 100644 --- a/NativeScript/runtime/Runtime.h +++ b/NativeScript/runtime/Runtime.h @@ -82,6 +82,7 @@ class Runtime { std::unique_ptr moduleInternal_; int workerId_; CFRunLoopRef runtimeLoop_; + CFRunLoopObserverRef messageLoopObserver_ = nullptr; double startTime; double realtimeOrigin; // TODO: refactor this. This is only needed because, during program diff --git a/NativeScript/runtime/Runtime.mm b/NativeScript/runtime/Runtime.mm index 2de309cc..8ad6fc5a 100644 --- a/NativeScript/runtime/Runtime.mm +++ b/NativeScript/runtime/Runtime.mm @@ -22,15 +22,15 @@ #include "DisposerPHV.h" #include "IsolateWrapper.h" +#include #include +#include "DevFlags.h" +#include "HMRSupport.h" #include "ModuleBinding.hpp" #include "ModuleInternalCallbacks.h" #include "URLImpl.h" #include "URLPatternImpl.h" #include "URLSearchParamsImpl.h" -#include -#include "HMRSupport.h" -#include "DevFlags.h" #define STRINGIZE(x) #x #define STRINGIZE_VALUE_OF(x) STRINGIZE(x) @@ -128,7 +128,7 @@ static void InitializeImportMetaObject(Local context, Local mod std::atomic Runtime::nextIsolateId{0}; SimpleAllocator allocator_; NSDictionary* AppPackageJson = nil; -static std::unordered_map AppConfigCache; // generic cache for app config values +static std::unordered_map AppConfigCache; // generic cache for app config values static std::mutex AppConfigCacheMutex; // Global flag to track when JavaScript errors occur during execution @@ -185,6 +185,12 @@ void DisposeIsolateWhenPossible(Isolate* isolate) { } Runtime::~Runtime() { + if (messageLoopObserver_) { + CFRunLoopObserverInvalidate(messageLoopObserver_); + CFRelease(messageLoopObserver_); + messageLoopObserver_ = nullptr; + } + auto currentIsolate = this->isolate_; { // make sure we remove the isolate from the list of active isolates first @@ -299,8 +305,8 @@ void DisposeIsolateWhenPossible(Isolate* isolate) { DefineDrainMicrotaskMethod(isolate, globalTemplate); // queueMicrotask(callback) per spec { - Local qmtTemplate = FunctionTemplate::New( - isolate, [](const FunctionCallbackInfo& info) { + Local qmtTemplate = + FunctionTemplate::New(isolate, [](const FunctionCallbackInfo& info) { auto* isolate = info.GetIsolate(); if (info.Length() < 1 || !info[0]->IsFunction()) { isolate->ThrowException(Exception::TypeError( @@ -425,6 +431,27 @@ void DisposeIsolateWhenPossible(Isolate* isolate) { cache->SetContext(context); this->isolate_ = isolate; + + // Pump V8's foreground task queue on each CFRunLoop iteration. + // FinalizationRegistry cleanup callbacks are posted as foreground tasks by V8 + // during GC — without this, they never execute. + CFRunLoopObserverContext obsCtx = {0, this, nullptr, nullptr, nullptr}; + messageLoopObserver_ = CFRunLoopObserverCreate( + kCFAllocatorDefault, kCFRunLoopBeforeWaiting, true, 0, + [](CFRunLoopObserverRef observer, CFRunLoopActivity activity, void* info) { + auto* runtime = static_cast(info); + auto* isolate = runtime->GetIsolate(); + if (!IsAlive(isolate)) { + return; + } + v8::Locker locker(isolate); + while (v8::platform::PumpMessageLoop(platform_.get(), isolate, + v8::platform::MessageLoopBehavior::kDoNotWait)) { + continue; + } + }, + &obsCtx); + CFRunLoopAddObserver(runtimeLoop_, messageLoopObserver_, kCFRunLoopCommonModes); } void Runtime::RunMainScript() { @@ -486,7 +513,8 @@ void DisposeIsolateWhenPossible(Isolate* isolate) { result = AppPackageJson[nsKey]; } - // Store in cache (can cache nil as NSNull to differentiate presence if desired; for now, cache as-is) + // Store in cache (can cache nil as NSNull to differentiate presence if desired; for now, cache + // as-is) { std::lock_guard lock(AppConfigCacheMutex); AppConfigCache[key] = result;