diff --git a/NativeScript/inspector/JsV8InspectorClient.mm b/NativeScript/inspector/JsV8InspectorClient.mm index 0737d7e2..0d24ed92 100644 --- a/NativeScript/inspector/JsV8InspectorClient.mm +++ b/NativeScript/inspector/JsV8InspectorClient.mm @@ -13,6 +13,7 @@ #include "Helpers.h" #include "InspectorServer.h" #include "JsV8InspectorClient.h" +#include "NativeScriptPlatform.h" #include "RuntimeConfig.h" #include "WorkerInspectorClient.h" #include "include/libplatform/libplatform.h" @@ -436,9 +437,14 @@ bool ShouldRewriteSourceMapURLs() { shouldWait = true; } - std::shared_ptr platform = tns::Runtime::GetPlatform(); - Isolate* isolate = isolate_; - platform::PumpMessageLoop(platform.get(), isolate, platform::MessageLoopBehavior::kDoNotWait); + // JS frames are on the stack, so only nestable v8 foreground tasks may + // run; everything else fires from its own wakeup after resume. Lookup, + // never create: a create here could mint a registry entry for an isolate + // whose runtime is already gone + auto eventLoop = tns::NativeScriptPlatform::Instance()->LookupEventLoop(isolate_); + if (eventLoop != nullptr) { + eventLoop->RunNestableV8Tasks(); + } if (shouldWait && !terminated_) { dispatch_semaphore_wait(messageArrived_, dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_MSEC)); // 1ms diff --git a/NativeScript/inspector/WorkerInspectorClient.mm b/NativeScript/inspector/WorkerInspectorClient.mm index 4c7c5aea..9ef94bc5 100644 --- a/NativeScript/inspector/WorkerInspectorClient.mm +++ b/NativeScript/inspector/WorkerInspectorClient.mm @@ -8,6 +8,7 @@ #include "Caches.h" #include "Helpers.h" #include "JsV8InspectorClient.h" +#include "NativeScriptPlatform.h" #include "include/libplatform/libplatform.h" #include "utils.h" @@ -190,8 +191,14 @@ StringView Make8BitStringView(const std::string& value) { this->DispatchOne(message); } - std::shared_ptr platform = tns::Runtime::GetPlatform(); - platform::PumpMessageLoop(platform.get(), isolate_, platform::MessageLoopBehavior::kDoNotWait); + // JS frames are on the stack, so only nestable v8 foreground tasks may + // run; everything else fires from its own wakeup after resume. Lookup, + // never create: a create here could mint a registry entry for an isolate + // whose runtime is already gone + auto eventLoop = tns::NativeScriptPlatform::Instance()->LookupEventLoop(isolate_); + if (eventLoop != nullptr) { + eventLoop->RunNestableV8Tasks(); + } if (shouldWait && !pauseTerminated_ && !dying_) { dispatch_semaphore_wait(messageArrived_, dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_MSEC)); // 1ms diff --git a/NativeScript/runtime/Caches.h b/NativeScript/runtime/Caches.h index 68c7f1b7..57bc2c5c 100644 --- a/NativeScript/runtime/Caches.h +++ b/NativeScript/runtime/Caches.h @@ -81,6 +81,9 @@ class Caches { void SetContext(v8::Local context); v8::Local GetContext(); + // GetContext crashes before SetContext; work can run in that window (v8 + // posts foreground tasks during Isolate::New) + inline bool HasContext() { return context_ != nullptr; } // Per-isolate unhandled promise rejection tracking. Fed by // NativeScriptException::OnPromiseRejected and drained once per runloop turn. diff --git a/NativeScript/runtime/ClassBuilder.mm b/NativeScript/runtime/ClassBuilder.mm index 61a24d11..012f1fdc 100644 --- a/NativeScript/runtime/ClassBuilder.mm +++ b/NativeScript/runtime/ClassBuilder.mm @@ -321,7 +321,7 @@ void ScopeClassNameToIsolate(std::string& name, int isolateId) { auto runtime = Runtime::GetRuntime(isolate); auto runtimeLoop = runtime->RuntimeLoop(); void* weakSelf = (__bridge void*)self; - auto gcProtect = ^() { + auto gcProtect = [isolateWrapper, weakSelf, isolate]() { auto innerCache = isolateWrapper.GetCache(); auto it = innerCache->Instances.find((id)weakSelf); if (it != innerCache->Instances.end()) { @@ -337,7 +337,9 @@ void ScopeClassNameToIsolate(std::string& name, int isolateId) { } }; if (CFRunLoopGetCurrent() != runtimeLoop) { - tns::ExecuteOnRunLoop(runtimeLoop, gcProtect); + // bare entry: the closure does its own Locker ceremony, exactly + // like the performed block it replaces + runtime->GetEventLoop()->PostInternalBare(gcProtect); } else { gcProtect(); } @@ -357,7 +359,7 @@ void ScopeClassNameToIsolate(std::string& name, int isolateId) { if ([self retainCount] == 2) { void* weakSelf = (__bridge void*)self; - auto gcUnprotect = ^() { + auto gcUnprotect = [isolateWrapper, weakSelf, isolate]() { auto innerCache = isolateWrapper.GetCache(); auto it = innerCache->Instances.find((id)weakSelf); if (it != innerCache->Instances.end()) { @@ -377,7 +379,9 @@ void ScopeClassNameToIsolate(std::string& name, int isolateId) { auto runtime = Runtime::GetRuntime(isolate); auto runtimeLoop = runtime->RuntimeLoop(); if (CFRunLoopGetCurrent() != runtimeLoop) { - tns::ExecuteOnRunLoop(runtimeLoop, gcUnprotect); + // bare entry: the closure does its own Locker ceremony, exactly + // like the performed block it replaces + runtime->GetEventLoop()->PostInternalBare(gcUnprotect); } else { auto innerCache = isolateWrapper.GetCache(); auto it = innerCache->Instances.find(self); diff --git a/NativeScript/runtime/EventLoop.h b/NativeScript/runtime/EventLoop.h new file mode 100644 index 00000000..8448dacc --- /dev/null +++ b/NativeScript/runtime/EventLoop.h @@ -0,0 +1,230 @@ +#ifndef EventLoop_h +#define EventLoop_h + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "Common.h" +#include "v8-platform.h" + +namespace tns { + +/** + * A producer of ordered-lane work that keeps its own bookkeeping (Timers). + * The EventLoop's token drain consults it so timers and ordered entries form + * ONE due-ordered domain: each anonymous token runs the earliest due item + * across both. Home-thread only. + */ +class OrderedTaskSource { + public: + /** + * If the source's earliest item is due at `now` and is earlier-or-equal to + * `otherDue` (a negative otherDue means no competitor), consumes that slot - + * running the item, or nothing if the slot is a tombstone (cancelled item) - + * and returns true. Consumes exactly one slot per call so tokens and slots + * stay 1:1. Check and run form a single call so the source can do both under + * one acquisition of whatever guards its state (Timers' bookkeeping is + * guarded by the isolate Locker). + */ + virtual bool RunIfEarliest(double now, double otherDue) = 0; + + virtual ~OrderedTaskSource() = default; +}; + +/** + * Per-runtime scheduler for work that must run on the runtime's home thread. + * Two lanes, split by ordering contract (the iOS port of the Android + * runtime's EventLoop; both descend from ExecuteOnRunLoop): + * + * Ordered lane - work whose ordering is observable against other app-level + * runloop work (spec'd macrotasks, JS timers). Every post is an anonymous + * "task due" token on ONE CFRunLoopTimer armed at the earliest pending due + * time (a due-now token gets a past fire date and fires on the next pass), so + * tokens stay in fire-date order with foreign NSTimers and each fire yields a + * full runloop pass - performed-block delivery would be sooner but a + * self-rescheduling chain of blocks starves the before-waiting phase (CA + * rendering, autorelease pool, the rejection drain). Each token runs the + * earliest due item across the ordered entries and the OrderedTaskSource + * (Timers) - one due-ordered domain, and a leftover token is a cheap no-op. + * + * Internal lane - work in its own ordering domain: v8 platform foreground + * tasks (Atomics.waitAsync wakeups, GC tasks, streaming-compilation + * merge-backs), worker->parent messages and drains. Rides a version-0 + * CFRunLoopSource plus one CFRunLoopTimer for delayed work. The source's + * perform callback runs exactly one due entry and re-signals itself while + * more are due, so bursts interleave with other runloop work instead of + * draining in one go. + * + * Posts are accepted from any thread. The loop starts unbound and buffers + * (v8 requests its task runner during Isolate::New, before the home thread is + * committed); BindToCurrentThread attaches both lanes and flushes. Posts + * after Shutdown are dropped (Post* returns false), preserving the + * "message to a terminated runtime" semantics of the mechanisms this + * replaces. Producers only post; entries run exclusively on the home thread, + * which is the only place the isolate's Locker is taken. + */ +class EventLoop { + public: + explicit EventLoop(v8::Isolate* isolate) : isolate_(isolate) {} + + ~EventLoop(); + + /** + * Attaches both lanes to the calling thread's CFRunLoop and flushes work + * buffered before the bind. Must run on the runtime's home thread, before + * its runloop starts dispatching. + */ + void BindToCurrentThread(); + + /** + * Drops all queued work and detaches both lanes; posts after this are + * dropped. Must run on the home thread (invalidating runloop sources + * concurrently with a callback dispatch is racy), before the isolate is + * disposed. + */ + void Shutdown(); + + // ordered lane: one due-ordered domain with JS timers, in fire-date order + // with foreign runloop timers. Returns false if the post was dropped (loop + // already shut down). + bool PostOrdered(std::function fn); + bool PostOrderedDelayed(std::function fn, double delayMs); + + /** + * Posts a bare ordered token due at an absolute CLOCK_MONOTONIC time + * (NowMs() units) for an item the OrderedTaskSource keeps in its own + * bookkeeping (Timers). One token per item; the drain picks the earliest + * due item across the source and the ordered entries, so the token needn't + * name what it will run. Returns the key actually recorded (an overdue due + * time is clamped to now) - the value TryCancelOrderedToken must be given + * to recall this token. + */ + double PostOrderedToken(double dueTimeMs); + + /** + * Removes one not-yet-matured ordered token at this due time, un-arming its + * wakeup. Returns false when no such token is pending - it already matured, + * so its slot cannot be recalled; the caller must leave a tombstone for it + * instead. Tokens are anonymous and counted, + * so cancelling one token plus one item keeps slots 1:1 no matter which + * producer's token is physically removed. + */ + bool TryCancelOrderedToken(double dueTimeMs); + + /** + * Registers the ordered lane's external source. Home thread only; pass + * nullptr to unregister (the source is being destroyed). + */ + void SetTimerSource(OrderedTaskSource* source); + + // internal lane: runs on the home thread as soon as the runloop polls. + // Returns false if the post was dropped (loop already shut down). + bool PostInternal(std::function fn); + bool PostInternalDelayed(std::function fn, double delayMs); + + /** + * Internal-lane post whose fn does its OWN isolate ceremony: RunEntry skips + * the loop's Locker/scopes/microtask checkpoint. Required when the fn locks + * a different isolate than this loop's, or must run with no V8 scopes on + * the stack at all (NativeScriptException's deferred @throw) - bare entries + * also run outside the loop's exception guard so an NSException unwinds + * into the runloop frame exactly like a CFRunLoopPerformBlock did. + */ + bool PostInternalBare(std::function fn); + + /** + * Posts a v8 foreground task into the internal lane. Called by the + * platform's per-isolate v8::TaskRunner adapter, from any thread. + */ + void PostV8Task(std::unique_ptr task, bool nestable, + double delaySeconds); + + /** + * True once Shutdown ran. A stopped loop found in the platform registry for + * a (reused) isolate pointer is stale and must be replaced. + */ + bool IsStopped(); + + /** + * Runs the internal-lane v8 tasks that are due and nestable, bounded to the + * entries present at call time. For nested message loops (inspector pause) + * where the runloop isn't polling: JS is on the stack, so non-nestable + * tasks and plain function posts stay queued and run from their own wakeups + * after the loop unwinds. + */ + void RunNestableV8Tasks(); + + /** + * Runs at most one due ordered-lane item (an entry, a timer, or a + * tombstone), then performs a microtask checkpoint for entries. Invoked + * once per token, on the home thread. + */ + void RunOrderedTask(); + + // CLOCK_MONOTONIC milliseconds - the clock every due time and token is on + static double NowMs(); + + private: + struct Entry { + // exactly one of task/fn is set; fn entries are never drained by + // RunNestableV8Tasks + std::unique_ptr task; + std::function fn; + bool nestable; + // bare entries run without the loop's Locker/scopes/checkpoint/guard + bool bare = false; + // enqueue time for immediate entries, due time for delayed ones, so one + // comparison orders both queues + double time = 0; + }; + struct Lane { + std::deque immediate; + std::multimap delayed; + }; + + // all *Locked members require mutex_ to be held + void PostInternalLocked(Entry entry, double delayMs); + void PostOrderedLocked(Entry entry, double delayMs); + double PostOrderedTokenLocked(double dueTimeMs, double now); + static std::unique_ptr TakeDueLocked(Lane& lane, bool nestableOnly, + bool v8Only, double now); + // earliest due entry time in the lane, or a negative value if none is due + static double PeekDueLocked(Lane& lane, double now); + static bool HasDueLocked(Lane& lane, double now); + void SignalInternalLocked(); + void ArmInternalTimerLocked(double now); + void ArmOrderedTimerLocked(double now); + void RunEntry(Entry& entry); + void RunOneInternal(); + + static void InternalSourcePerform(void* info); + static void InternalTimerFired(CFRunLoopTimerRef timer, void* info); + static void OrderedTimerFired(CFRunLoopTimerRef timer, void* info); + + v8::Isolate* isolate_; + std::mutex mutex_; + Lane internal_; + Lane ordered_; + // ordered-lane source with its own bookkeeping (Timers); home-thread only + OrderedTaskSource* timerSource_ = nullptr; + // future-due ordered tokens awaiting orderedTimer_ + std::multiset pendingTokens_; + // tokens posted before the bind; flushed by BindToCurrentThread + std::vector bufferedTokens_; + CFRunLoopRef loop_ = nullptr; + CFRunLoopSourceRef internalSource_ = nullptr; + CFRunLoopTimerRef internalTimer_ = nullptr; + CFRunLoopTimerRef orderedTimer_ = nullptr; + bool stopped_ = false; +}; + +} // namespace tns + +#endif /* EventLoop_h */ diff --git a/NativeScript/runtime/EventLoop.mm b/NativeScript/runtime/EventLoop.mm new file mode 100644 index 00000000..57aa131b --- /dev/null +++ b/NativeScript/runtime/EventLoop.mm @@ -0,0 +1,501 @@ +#include "EventLoop.h" + +#include +#include +#include + +#include "Caches.h" +#include "Helpers.h" +#include "NativeScriptException.h" + +using namespace v8; + +namespace { + +// far enough that an "unarmed" repeating timer never fires; re-armed with +// CFRunLoopTimerSetNextFireDate when a real deadline exists +const CFTimeInterval kNeverFireInterval = 1.0e10; + +// runs one unit of non-bare work without letting a C++ exception escape into +// a CFRunLoop callback frame. Deliberately no catch(...): on Darwin it would +// also swallow NSExceptions, and bare entries (which may @throw on purpose) +// never come through here anyway. +template +void RunGuarded(F&& body) { + try { + body(); + } catch (tns::NativeScriptException& ex) { + Log(@"NativeScript: uncaught NativeScriptException in event loop task: %s", + ex.getMessage().c_str()); + } catch (std::exception& ex) { + Log(@"NativeScript: c++ exception in event loop task: %s", ex.what()); + } +} + +} // namespace + +namespace tns { + +double EventLoop::NowMs() { + struct timespec res; + clock_gettime(CLOCK_MONOTONIC, &res); + return 1000.0 * res.tv_sec + (double)res.tv_nsec / 1e6; +} + +// converts a CLOCK_MONOTONIC due time to a CFRunLoopTimer fire date. The two +// clocks can drift (CFAbsoluteTime is wall-based), so consumers must treat a +// fire as "check what is due now", never as proof a specific item is due. +static CFAbsoluteTime FireDateFor(double dueMs, double nowMs) { + return CFAbsoluteTimeGetCurrent() + std::max(0.0, dueMs - nowMs) / 1000.0; +} + +void EventLoop::BindToCurrentThread() { + std::lock_guard lock(mutex_); + if (loop_ != nullptr || stopped_) { + return; + } + + loop_ = CFRunLoopGetCurrent(); + + CFRunLoopSourceContext sourceContext = { + 0, this, nullptr, nullptr, nullptr, + nullptr, nullptr, nullptr, nullptr, &EventLoop::InternalSourcePerform}; + internalSource_ = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &sourceContext); + CFRunLoopAddSource(loop_, internalSource_, kCFRunLoopCommonModes); + + CFRunLoopTimerContext timerContext = {0, this, nullptr, nullptr, nullptr}; + internalTimer_ = + CFRunLoopTimerCreate(kCFAllocatorDefault, CFAbsoluteTimeGetCurrent() + kNeverFireInterval, + kNeverFireInterval, 0, 0, &EventLoop::InternalTimerFired, &timerContext); + CFRunLoopAddTimer(loop_, internalTimer_, kCFRunLoopCommonModes); + orderedTimer_ = + CFRunLoopTimerCreate(kCFAllocatorDefault, CFAbsoluteTimeGetCurrent() + kNeverFireInterval, + kNeverFireInterval, 0, 0, &EventLoop::OrderedTimerFired, &timerContext); + CFRunLoopAddTimer(loop_, orderedTimer_, kCFRunLoopCommonModes); + + // flush work buffered before the home thread was known + auto now = NowMs(); + if (HasDueLocked(internal_, now)) { + SignalInternalLocked(); + } + ArmInternalTimerLocked(now); + // replay buffered tokens under their original keys - PostOrderedToken + // already returned those to producers as the recall handle, so the flush + // must not re-clamp them (a past key simply fires on the next pass) + auto tokens = std::move(bufferedTokens_); + bufferedTokens_.clear(); + for (double due : tokens) { + pendingTokens_.insert(due); + } + ArmOrderedTimerLocked(now); +} + +void EventLoop::Shutdown() { + std::lock_guard lock(mutex_); + if (stopped_) { + return; + } + stopped_ = true; + internal_.immediate.clear(); + internal_.delayed.clear(); + ordered_.immediate.clear(); + ordered_.delayed.clear(); + pendingTokens_.clear(); + bufferedTokens_.clear(); + if (internalSource_ != nullptr) { + CFRunLoopSourceInvalidate(internalSource_); + CFRelease(internalSource_); + internalSource_ = nullptr; + } + if (internalTimer_ != nullptr) { + CFRunLoopTimerInvalidate(internalTimer_); + CFRelease(internalTimer_); + internalTimer_ = nullptr; + } + if (orderedTimer_ != nullptr) { + CFRunLoopTimerInvalidate(orderedTimer_); + CFRelease(orderedTimer_); + orderedTimer_ = nullptr; + } + loop_ = nullptr; +} + +EventLoop::~EventLoop() { + // Normally a no-op: ~Runtime already shut the loop down on the home thread. + // A transient shared_ptr taken on a foreign posting thread can be the last + // reference only after that shutdown, when this is just member cleanup. + Shutdown(); +} + +void EventLoop::PostInternalLocked(Entry entry, double delayMs) { + auto now = NowMs(); + if (delayMs <= 0) { + entry.time = now; + internal_.immediate.push_back(std::move(entry)); + SignalInternalLocked(); + } else { + auto due = now + delayMs; + entry.time = due; + internal_.delayed.emplace(due, std::move(entry)); + ArmInternalTimerLocked(now); + } +} + +void EventLoop::PostOrderedLocked(Entry entry, double delayMs) { + auto now = NowMs(); + if (delayMs <= 0) { + entry.time = now; + ordered_.immediate.push_back(std::move(entry)); + PostOrderedTokenLocked(now, now); + } else { + auto due = now + delayMs; + entry.time = due; + ordered_.delayed.emplace(due, std::move(entry)); + PostOrderedTokenLocked(due, now); + } +} + +double EventLoop::PostOrderedTokenLocked(double dueTimeMs, double now) { + if (loop_ == nullptr) { + bufferedTokens_.push_back(dueTimeMs); + return dueTimeMs; + } + // every token rides the timer phase, even due-now ones (a past fire date + // fires on the next runloop pass). Performed blocks would deliver sooner, + // but a self-rescheduling chain of them never lets the loop reach its + // before-waiting phase - starving CA rendering commits, the autorelease + // pool and the rejection drain - while a due timer still yields a full + // pass between fires. This also keeps due-now tokens in fire-date order + // with foreign NSTimers, like the per-timer CFRunLoopTimers this replaces. + double key = std::max(dueTimeMs, now); + pendingTokens_.insert(key); + ArmOrderedTimerLocked(now); + CFRunLoopWakeUp(loop_); + return key; +} + +bool EventLoop::PostInternal(std::function fn) { + std::lock_guard lock(mutex_); + if (stopped_) { + return false; + } + PostInternalLocked(Entry{nullptr, std::move(fn), true, false, 0}, 0); + return true; +} + +bool EventLoop::PostInternalDelayed(std::function fn, double delayMs) { + std::lock_guard lock(mutex_); + if (stopped_) { + return false; + } + PostInternalLocked(Entry{nullptr, std::move(fn), true, false, 0}, delayMs); + return true; +} + +bool EventLoop::PostInternalBare(std::function fn) { + std::lock_guard lock(mutex_); + if (stopped_) { + return false; + } + PostInternalLocked(Entry{nullptr, std::move(fn), true, true, 0}, 0); + return true; +} + +bool EventLoop::PostOrdered(std::function fn) { + std::lock_guard lock(mutex_); + if (stopped_) { + return false; + } + PostOrderedLocked(Entry{nullptr, std::move(fn), true, false, 0}, 0); + return true; +} + +bool EventLoop::PostOrderedDelayed(std::function fn, double delayMs) { + std::lock_guard lock(mutex_); + if (stopped_) { + return false; + } + PostOrderedLocked(Entry{nullptr, std::move(fn), true, false, 0}, delayMs); + return true; +} + +double EventLoop::PostOrderedToken(double dueTimeMs) { + std::lock_guard lock(mutex_); + if (stopped_) { + return dueTimeMs; + } + return PostOrderedTokenLocked(dueTimeMs, NowMs()); +} + +bool EventLoop::TryCancelOrderedToken(double dueTimeMs) { + std::lock_guard lock(mutex_); + if (stopped_) { + return false; + } + auto bufferedIt = std::find(bufferedTokens_.begin(), bufferedTokens_.end(), dueTimeMs); + if (bufferedIt != bufferedTokens_.end()) { + bufferedTokens_.erase(bufferedIt); + return true; + } + auto pendingIt = pendingTokens_.find(dueTimeMs); + if (pendingIt == pendingTokens_.end()) { + return false; + } + pendingTokens_.erase(pendingIt); + ArmOrderedTimerLocked(NowMs()); + return true; +} + +void EventLoop::SetTimerSource(OrderedTaskSource* source) { + // home thread only, like every consumer of timerSource_ + timerSource_ = source; +} + +void EventLoop::PostV8Task(std::unique_ptr task, bool nestable, double delaySeconds) { + std::lock_guard lock(mutex_); + if (stopped_) { + return; + } + PostInternalLocked(Entry{std::move(task), nullptr, nestable, false, 0}, delaySeconds * 1000.0); +} + +bool EventLoop::IsStopped() { + std::lock_guard lock(mutex_); + return stopped_; +} + +std::unique_ptr EventLoop::TakeDueLocked(Lane& lane, bool nestableOnly, + bool v8Only, double now) { + auto matches = [&](const Entry& e) { + return (!nestableOnly || e.nestable) && (!v8Only || e.task != nullptr); + }; + auto imIt = lane.immediate.begin(); + while (imIt != lane.immediate.end() && !matches(*imIt)) { + ++imIt; + } + auto delIt = lane.delayed.begin(); + while (delIt != lane.delayed.end() && !matches(delIt->second)) { + ++delIt; + } + bool hasImmediate = imIt != lane.immediate.end(); + bool hasDelayed = delIt != lane.delayed.end() && delIt->first <= now; + if (hasImmediate && (!hasDelayed || imIt->time <= delIt->first)) { + auto entry = std::make_unique(std::move(*imIt)); + lane.immediate.erase(imIt); + return entry; + } + if (hasDelayed) { + auto entry = std::make_unique(std::move(delIt->second)); + lane.delayed.erase(delIt); + return entry; + } + return nullptr; +} + +double EventLoop::PeekDueLocked(Lane& lane, double now) { + // immediate entries are enqueued with monotonically increasing times, so + // the front is the earliest + double due = lane.immediate.empty() ? -1 : lane.immediate.front().time; + if (!lane.delayed.empty() && lane.delayed.begin()->first <= now && + (due < 0 || lane.delayed.begin()->first < due)) { + due = lane.delayed.begin()->first; + } + return due; +} + +bool EventLoop::HasDueLocked(Lane& lane, double now) { + return !lane.immediate.empty() || (!lane.delayed.empty() && lane.delayed.begin()->first <= now); +} + +void EventLoop::SignalInternalLocked() { + if (internalSource_ != nullptr) { + CFRunLoopSourceSignal(internalSource_); + CFRunLoopWakeUp(loop_); + } +} + +void EventLoop::ArmInternalTimerLocked(double now) { + if (internalTimer_ == nullptr) { + return; + } + // earliest not-yet-due delayed entry; already-due ones are the signal's job + double due = -1; + for (auto& pair : internal_.delayed) { + if (pair.first > now) { + due = pair.first; + break; + } + } + CFRunLoopTimerSetNextFireDate( + internalTimer_, + due >= 0 ? FireDateFor(due, now) : CFAbsoluteTimeGetCurrent() + kNeverFireInterval); +} + +void EventLoop::ArmOrderedTimerLocked(double now) { + if (orderedTimer_ == nullptr) { + return; + } + CFRunLoopTimerSetNextFireDate( + orderedTimer_, !pendingTokens_.empty() ? FireDateFor(*pendingTokens_.begin(), now) + : CFAbsoluteTimeGetCurrent() + kNeverFireInterval); +} + +void EventLoop::RunEntry(Entry& entry) { + if (entry.bare) { + // the fn does its own ceremony - it may lock a different isolate, or + // deliberately @throw with no V8 scopes on the stack + entry.fn(); + return; + } + v8::Locker locker(isolate_); + v8::Isolate::Scope isolate_scope(isolate_); + v8::HandleScope handle_scope(isolate_); + auto run = [&]() { + if (entry.task != nullptr) { + entry.task->Run(); + } else { + entry.fn(); + } + // work may enqueue microtasks without entering JS (e.g. resolving the + // Atomics.waitAsync promise), which never reaches kAuto's depth-0 drain + isolate_->PerformMicrotaskCheckpoint(); + }; + auto cache = Caches::Get(isolate_); + if (cache != nullptr && cache->IsValid() && cache->HasContext()) { + Context::Scope context_scope(cache->GetContext()); + run(); + } else { + // v8 can post tasks before Runtime::Init creates the context + run(); + } +} + +void EventLoop::RunOneInternal() { + std::unique_ptr entry; + { + std::lock_guard lock(mutex_); + if (stopped_) { + return; + } + auto now = NowMs(); + entry = TakeDueLocked(internal_, false, false, now); + // re-signal BEFORE running: one entry per runloop pass keeps the lane + // fair with other runloop work, and a bare entry may @throw and never + // return control here + if (entry != nullptr && HasDueLocked(internal_, now)) { + SignalInternalLocked(); + } + } + if (entry == nullptr) { + // leftover signal: the work it announced ran early from a nested drain + return; + } + if (entry->bare) { + RunEntry(*entry); + return; + } + RunGuarded([&] { RunEntry(*entry); }); +} + +void EventLoop::RunNestableV8Tasks() { + // bounded to the entries present at call time so a task that reposts can't + // wedge the inspector pause loop that called us + size_t budget; + { + std::lock_guard lock(mutex_); + budget = internal_.immediate.size() + internal_.delayed.size(); + } + while (budget-- > 0) { + std::unique_ptr entry; + { + std::lock_guard lock(mutex_); + if (stopped_) { + return; + } + entry = TakeDueLocked(internal_, true, true, NowMs()); + } + if (entry == nullptr) { + return; + } + // the pause loops call this from inside v8 inspector frames - a C++ + // exception must not unwind through them + RunGuarded([&] { RunEntry(*entry); }); + } +} + +void EventLoop::RunOrderedTask() { + // one anonymous token = one due slot across the whole ordered domain: pick + // the earliest due item among the ordered entries and the timer source, + // whichever it is. Timers and entries only ever run on this thread, so the + // peeked winner can't be taken by anyone else before we re-lock (a + // concurrent post can only add later work). + auto now = NowMs(); + double entryDue; + { + std::lock_guard lock(mutex_); + if (stopped_) { + return; + } + entryDue = PeekDueLocked(ordered_, now); + } + if (timerSource_ != nullptr && timerSource_->RunIfEarliest(now, entryDue)) { + return; + } + if (entryDue < 0) { + // leftover token: nothing in the domain is due yet + return; + } + std::unique_ptr entry; + { + std::lock_guard lock(mutex_); + if (stopped_) { + return; + } + entry = TakeDueLocked(ordered_, false, false, NowMs()); + } + if (entry != nullptr) { + RunGuarded([&] { RunEntry(*entry); }); + } +} + +void EventLoop::InternalSourcePerform(void* info) { + static_cast(info)->RunOneInternal(); +} + +void EventLoop::InternalTimerFired(CFRunLoopTimerRef timer, void* info) { + auto self = static_cast(info); + std::lock_guard lock(self->mutex_); + if (self->stopped_) { + return; + } + auto now = NowMs(); + if (HasDueLocked(self->internal_, now)) { + self->SignalInternalLocked(); + } + self->ArmInternalTimerLocked(now); +} + +void EventLoop::OrderedTimerFired(CFRunLoopTimerRef timer, void* info) { + auto self = static_cast(info); + bool due = false; + { + std::lock_guard lock(self->mutex_); + if (self->stopped_) { + return; + } + auto now = NowMs(); + if (!self->pendingTokens_.empty() && *self->pendingTokens_.begin() <= now) { + self->pendingTokens_.erase(self->pendingTokens_.begin()); + due = true; + } + // one matured token per fire: re-arming with an already-past due time + // fires again on the next runloop pass, so foreign timers and blocks due + // between two matured tokens interleave instead of waiting out a batch + self->ArmOrderedTimerLocked(now); + } + if (due) { + self->RunOrderedTask(); + } +} + +} // namespace tns diff --git a/NativeScript/runtime/ModuleInternal.mm b/NativeScript/runtime/ModuleInternal.mm index a864298a..5a6c8c75 100644 --- a/NativeScript/runtime/ModuleInternal.mm +++ b/NativeScript/runtime/ModuleInternal.mm @@ -12,7 +12,7 @@ #include "ModuleInternalCallbacks.h" // for ResolveModuleCallback #include "NativeScriptException.h" #include "NsBuiltinModules.h" -#include "Runtime.h" // for GetAppConfigValue +#include "Runtime.h" #include "RuntimeConfig.h" using namespace v8; @@ -851,7 +851,17 @@ ScriptOrigin origin(urlString, 0, 0, false, -1, Local(), false, false, int maxAttempts = 100; int attempts = 0; + // an await whose resolution arrives as a v8 foreground task (e.g. + // Atomics.waitAsync, streaming compilation) never settles from + // checkpoints alone; JS frames are on the stack, so like the inspector + // pause loops only nestable tasks may run here + Runtime* runtime = Runtime::GetRuntime(isolate); + std::shared_ptr eventLoop = runtime != nullptr ? runtime->GetEventLoop() : nullptr; + while (attempts < maxAttempts && !promiseTc.HasCaught()) { + if (eventLoop != nullptr) { + eventLoop->RunNestableV8Tasks(); + } isolate->PerformMicrotaskCheckpoint(); if (promiseTc.HasCaught()) { diff --git a/NativeScript/runtime/NativeScriptException.mm b/NativeScript/runtime/NativeScriptException.mm index aeb83648..e429b76c 100644 --- a/NativeScript/runtime/NativeScriptException.mm +++ b/NativeScript/runtime/NativeScriptException.mm @@ -227,14 +227,13 @@ static void ScheduleDeferredThrow(Isolate* isolate, NSException* e) { if (rt == nullptr) { return; } - CFRunLoopRef loop = rt->RuntimeLoop(); + auto loop = rt->GetEventLoop(); if (loop == nullptr) { return; } - CFRunLoopPerformBlock(loop, kCFRunLoopCommonModes, ^{ - @throw e; - }); - CFRunLoopWakeUp(loop); + // bare entry: runs with no V8 scopes and outside the loop's exception + // guard, so the NSException unwinds into the runloop frame + loop->PostInternalBare([e]() { @throw e; }); } void NativeScriptException::DepositPendingPolicyThrow(Isolate* isolate, id exception) { @@ -441,15 +440,16 @@ static void ScheduleDeferredThrow(Isolate* isolate, NSException* e) { // scheduled for. DepositPendingPolicyThrow(isolate, fatal); Runtime* rt = Runtime::GetRuntime(isolate); - CFRunLoopRef loop = rt != nullptr ? rt->RuntimeLoop() : nullptr; + auto loop = rt != nullptr ? rt->GetEventLoop() : nullptr; if (loop != nullptr) { - CFRunLoopPerformBlock(loop, kCFRunLoopCommonModes, ^{ + // bare entry: clean, V8-scope-free frame, outside the loop's + // exception guard + loop->PostInternalBare([isolate, fatal]() { id e = ClaimPendingPolicyThrowIfEqual(isolate, fatal); if (e != nil) { @throw e; } }); - CFRunLoopWakeUp(loop); } } else if (policy != "report") { static std::once_flag warnedPolicy; diff --git a/NativeScript/runtime/NativeScriptPlatform.h b/NativeScript/runtime/NativeScriptPlatform.h new file mode 100644 index 00000000..058e8a5e --- /dev/null +++ b/NativeScript/runtime/NativeScriptPlatform.h @@ -0,0 +1,112 @@ +#ifndef NativeScriptPlatform_h +#define NativeScriptPlatform_h + +#include +#include +#include + +#include "Common.h" +#include "EventLoop.h" +#include "v8-platform.h" + +namespace tns { + +/** + * v8::Platform that delegates worker-thread scheduling, time and tracing to + * the default libplatform implementation but serves per-isolate foreground + * task runners backed by each runtime's EventLoop. This is what makes v8's + * own foreground tasks (Atomics.waitAsync wakeups, GC finalization, + * streaming-compilation merge-backs) actually run - nothing pumps the default + * platform's internal queues outside the debugger pause loops. + */ +class NativeScriptPlatform : public v8::Platform { + public: + explicit NativeScriptPlatform(std::unique_ptr defaultPlatform); + + static NativeScriptPlatform* Instance() { return instance_; } + + /** + * Returns the isolate's event loop, creating an unbound one if v8 asks + * before Runtime::CreateIsolate binds it to the isolate's home thread. + */ + std::shared_ptr GetEventLoop(v8::Isolate* isolate); + + /** + * GetEventLoop, but replaces a stopped loop with a fresh one first. Used by + * Runtime::CreateIsolate: a stopped loop under this key is a leftover from + * a disposed isolate that had the same address (worker churn reuses them). + * The registry's v8 runner resolves the loop per post, so replacement also + * redirects tasks posted through an already-handed-out runner. + */ + std::shared_ptr RefreshEventLoop(v8::Isolate* isolate); + + /** + * The loop for the isolate, or null - never creates. The post path uses + * this so a disposed isolate's late posts drop instead of minting a fresh + * registry entry under a dead (or recycled) pointer. + */ + std::shared_ptr LookupEventLoop(v8::Isolate* isolate); + + /** + * Drops the registry entry, but only while it still maps to `loop`: isolate + * pointers are reused, and an unconditional erase from a late destructor + * could evict the pointer's new tenant. ~Runtime calls this after handing + * the isolate to DisposeIsolateWhenPossible. + */ + void IsolateDisposed(v8::Isolate* isolate, + const std::shared_ptr& loop); + + // v8::Platform + v8::PageAllocator* GetPageAllocator() override; + v8::ThreadIsolatedAllocator* GetThreadIsolatedAllocator() override; + size_t GetZeroSegmentSize() override; + void OnCriticalMemoryPressure() override; + int NumberOfWorkerThreads() override; + std::shared_ptr GetForegroundTaskRunner( + v8::Isolate* isolate, v8::TaskPriority priority) override; + bool IdleTasksEnabled(v8::Isolate* isolate) override; + std::unique_ptr CreateBoostablePriorityScope() + override; + std::unique_ptr CreateBlockingScope( + v8::BlockingType blocking_type) override; + double MonotonicallyIncreasingTime() override; + int64_t CurrentClockTimeMilliseconds() override; + double CurrentClockTimeMillis() override; + double CurrentClockTimeMillisecondsHighResolution() override; + StackTracePrinter GetStackTracePrinter() override; + v8::TracingController* GetTracingController() override; + void DumpWithoutCrashing() override; + v8::HighAllocationThroughputObserver* GetHighAllocationThroughputObserver() + override; + + protected: + std::unique_ptr CreateJobImpl( + v8::TaskPriority priority, std::unique_ptr job_task, + const v8::SourceLocation& location) override; + void PostTaskOnWorkerThreadImpl(v8::TaskPriority priority, + std::unique_ptr task, + const v8::SourceLocation& location) override; + void PostDelayedTaskOnWorkerThreadImpl( + v8::TaskPriority priority, std::unique_ptr task, + double delay_in_seconds, const v8::SourceLocation& location) override; + + private: + struct IsolateEntry { + std::shared_ptr loop; + // handed to v8 once per isolate; resolves the loop through the registry + // on every post so RefreshEventLoop redirects it + std::shared_ptr runner; + }; + + IsolateEntry& GetEntryLocked(v8::Isolate* isolate); + + std::unique_ptr default_; + std::mutex loopsMutex_; + std::unordered_map loops_; + + static NativeScriptPlatform* instance_; +}; + +} // namespace tns + +#endif /* NativeScriptPlatform_h */ diff --git a/NativeScript/runtime/NativeScriptPlatform.mm b/NativeScript/runtime/NativeScriptPlatform.mm new file mode 100644 index 00000000..9259cdc6 --- /dev/null +++ b/NativeScript/runtime/NativeScriptPlatform.mm @@ -0,0 +1,184 @@ +#include "NativeScriptPlatform.h" + +using namespace v8; + +namespace tns { + +NativeScriptPlatform* NativeScriptPlatform::instance_ = nullptr; + +namespace { + +/** + * The v8::TaskRunner handed to v8 for one isolate. Stateless beyond the + * isolate pointer: every post resolves the current EventLoop through the + * platform registry, so a stale loop replaced by RefreshEventLoop is + * redirected transparently, and posts for a disposed isolate (registry entry + * gone) drop instead of reviving a dead pointer's entry. + */ +class V8TaskRunnerAdapter : public v8::TaskRunner { + public: + explicit V8TaskRunnerAdapter(Isolate* isolate) : isolate_(isolate) {} + + bool IdleTasksEnabled() override { return false; } + + bool NonNestableTasksEnabled() const override { return true; } + + bool NonNestableDelayedTasksEnabled() const override { return true; } + + protected: + void PostTaskImpl(std::unique_ptr task, const SourceLocation& location) override { + Post(std::move(task), true, 0); + } + + void PostNonNestableTaskImpl(std::unique_ptr task, + const SourceLocation& location) override { + Post(std::move(task), false, 0); + } + + void PostDelayedTaskImpl(std::unique_ptr task, double delay_in_seconds, + const SourceLocation& location) override { + Post(std::move(task), true, delay_in_seconds); + } + + void PostNonNestableDelayedTaskImpl(std::unique_ptr task, double delay_in_seconds, + const SourceLocation& location) override { + Post(std::move(task), false, delay_in_seconds); + } + + private: + void Post(std::unique_ptr task, bool nestable, double delaySeconds) { + auto loop = NativeScriptPlatform::Instance()->LookupEventLoop(isolate_); + if (loop != nullptr) { + loop->PostV8Task(std::move(task), nestable, delaySeconds); + } + } + + Isolate* isolate_; +}; + +} // namespace + +NativeScriptPlatform::NativeScriptPlatform(std::unique_ptr defaultPlatform) + : default_(std::move(defaultPlatform)) { + instance_ = this; +} + +NativeScriptPlatform::IsolateEntry& NativeScriptPlatform::GetEntryLocked(Isolate* isolate) { + auto it = loops_.find(isolate); + if (it != loops_.end()) { + return it->second; + } + auto emplaced = + loops_.emplace(isolate, IsolateEntry{std::make_shared(isolate), + std::make_shared(isolate)}); + return emplaced.first->second; +} + +std::shared_ptr NativeScriptPlatform::GetEventLoop(Isolate* isolate) { + std::lock_guard lock(loopsMutex_); + return GetEntryLocked(isolate).loop; +} + +std::shared_ptr NativeScriptPlatform::RefreshEventLoop(Isolate* isolate) { + std::lock_guard lock(loopsMutex_); + auto& entry = GetEntryLocked(isolate); + if (entry.loop->IsStopped()) { + entry.loop = std::make_shared(isolate); + } + return entry.loop; +} + +std::shared_ptr NativeScriptPlatform::LookupEventLoop(Isolate* isolate) { + std::lock_guard lock(loopsMutex_); + auto it = loops_.find(isolate); + return it != loops_.end() ? it->second.loop : nullptr; +} + +void NativeScriptPlatform::IsolateDisposed(Isolate* isolate, + const std::shared_ptr& loop) { + std::lock_guard lock(loopsMutex_); + auto it = loops_.find(isolate); + if (it != loops_.end() && it->second.loop == loop) { + loops_.erase(it); + } +} + +PageAllocator* NativeScriptPlatform::GetPageAllocator() { return default_->GetPageAllocator(); } + +ThreadIsolatedAllocator* NativeScriptPlatform::GetThreadIsolatedAllocator() { + return default_->GetThreadIsolatedAllocator(); +} + +size_t NativeScriptPlatform::GetZeroSegmentSize() { return default_->GetZeroSegmentSize(); } + +void NativeScriptPlatform::OnCriticalMemoryPressure() { default_->OnCriticalMemoryPressure(); } + +int NativeScriptPlatform::NumberOfWorkerThreads() { return default_->NumberOfWorkerThreads(); } + +std::shared_ptr NativeScriptPlatform::GetForegroundTaskRunner(Isolate* isolate, + TaskPriority priority) { + // one runner regardless of priority: the home runloop's FIFO order is the + // priority model of the runtime thread + std::lock_guard lock(loopsMutex_); + return GetEntryLocked(isolate).runner; +} + +bool NativeScriptPlatform::IdleTasksEnabled(Isolate* isolate) { return false; } + +std::unique_ptr NativeScriptPlatform::CreateBoostablePriorityScope() { + return default_->CreateBoostablePriorityScope(); +} + +std::unique_ptr NativeScriptPlatform::CreateBlockingScope( + BlockingType blocking_type) { + return default_->CreateBlockingScope(blocking_type); +} + +double NativeScriptPlatform::MonotonicallyIncreasingTime() { + return default_->MonotonicallyIncreasingTime(); +} + +int64_t NativeScriptPlatform::CurrentClockTimeMilliseconds() { + return default_->CurrentClockTimeMilliseconds(); +} + +double NativeScriptPlatform::CurrentClockTimeMillis() { return default_->CurrentClockTimeMillis(); } + +double NativeScriptPlatform::CurrentClockTimeMillisecondsHighResolution() { + return default_->CurrentClockTimeMillisecondsHighResolution(); +} + +Platform::StackTracePrinter NativeScriptPlatform::GetStackTracePrinter() { + return default_->GetStackTracePrinter(); +} + +TracingController* NativeScriptPlatform::GetTracingController() { + return default_->GetTracingController(); +} + +void NativeScriptPlatform::DumpWithoutCrashing() { default_->DumpWithoutCrashing(); } + +HighAllocationThroughputObserver* NativeScriptPlatform::GetHighAllocationThroughputObserver() { + return default_->GetHighAllocationThroughputObserver(); +} + +std::unique_ptr NativeScriptPlatform::CreateJobImpl(TaskPriority priority, + std::unique_ptr job_task, + const SourceLocation& location) { + return default_->CreateJob(priority, std::move(job_task), location); +} + +void NativeScriptPlatform::PostTaskOnWorkerThreadImpl(TaskPriority priority, + std::unique_ptr task, + const SourceLocation& location) { + default_->PostTaskOnWorkerThread(priority, std::move(task), location); +} + +void NativeScriptPlatform::PostDelayedTaskOnWorkerThreadImpl(TaskPriority priority, + std::unique_ptr task, + double delay_in_seconds, + const SourceLocation& location) { + default_->PostDelayedTaskOnWorkerThread(priority, std::move(task), delay_in_seconds, location); +} + +} // namespace tns diff --git a/NativeScript/runtime/Runtime.h b/NativeScript/runtime/Runtime.h index d6f3a5ac..2b18b85e 100644 --- a/NativeScript/runtime/Runtime.h +++ b/NativeScript/runtime/Runtime.h @@ -3,6 +3,7 @@ #include "Caches.h" #include "Common.h" +#include "EventLoop.h" #include "MetadataBuilder.h" #include "ModuleInternal.h" #include "SpinLock.h" @@ -26,6 +27,12 @@ class Runtime { inline CFRunLoopRef RuntimeLoop() { return runtimeLoop_; } + /* + * Scheduler bound to this runtime's runloop. Producers on any thread post; + * entries run only on the runtime's home thread. + */ + inline std::shared_ptr GetEventLoop() const { return eventLoop_; } + void RunModule(const std::string moduleName); void RunScript(const std::string script); @@ -89,6 +96,8 @@ class Runtime { v8::Local globalTemplate); void DefineDrainMicrotaskMethod(v8::Isolate* isolate, v8::Local globalTemplate); + void DefineQueueMacrotaskMethod(v8::Isolate* isolate, + v8::Local globalTemplate); void DefineDateTimeConfigurationChangeNotificationMethod( v8::Isolate* isolate, v8::Local globalTemplate); @@ -98,6 +107,7 @@ class Runtime { std::unique_ptr moduleInternal_; int workerId_; CFRunLoopRef runtimeLoop_; + std::shared_ptr eventLoop_; // Drains unhandled promise rejections once per runloop turn // (kCFRunLoopBeforeWaiting). Torn down before isolate disposal in ~Runtime. CFRunLoopObserverRef rejectionObserver_ = nullptr; diff --git a/NativeScript/runtime/Runtime.mm b/NativeScript/runtime/Runtime.mm index 6ff6c072..dea78030 100644 --- a/NativeScript/runtime/Runtime.mm +++ b/NativeScript/runtime/Runtime.mm @@ -12,6 +12,7 @@ #include "InlineFunctions.h" #include "Interop.h" #include "NativeScriptException.h" +#include "NativeScriptPlatform.h" #include "ObjectManager.h" #include "Performance.h" #include "PromiseProxy.h" @@ -22,7 +23,6 @@ #include "TSHelpers.h" #include "WeakRef.h" #include "Worker.h" -// #include "SetTimeout.h" #include "IsolateWrapper.h" @@ -223,6 +223,13 @@ void DisposeIsolateWhenPossible(Isolate* isolate) { { v8::Locker lock(isolate_); + // Stop the event loop before any handle disposal: queued entries touch + // caches and persistents that go away below, and posts from other threads + // must start dropping now. + if (eventLoop_ != nullptr) { + eventLoop_->Shutdown(); + } + // Clear module registry before disposing other handles // This prevents crashes during g_moduleRegistry cleanup extern std::unordered_map> g_moduleRegistry; @@ -249,6 +256,13 @@ void DisposeIsolateWhenPossible(Isolate* isolate) { DisposeIsolateWhenPossible(this->isolate_); + // Matched erase: only removes the registry entry while it still maps to + // this runtime's loop, so a worker isolate that reuses this pointer after + // the (possibly deferred) Dispose can't be evicted by us. + if (eventLoop_ != nullptr) { + NativeScriptPlatform::Instance()->IsolateDisposed(currentIsolate, eventLoop_); + } + currentRuntime_ = nullptr; } @@ -269,7 +283,9 @@ void DisposeIsolateWhenPossible(Isolate* isolate) { std::string flags = RuntimeConfig.IsDebug ? "--expose_gc" : "--expose_gc --no-lazy"; V8::SetFlagsFromString(flags.c_str(), flags.size()); - Runtime::platform_ = platform::NewDefaultPlatform(); + // wrap the default platform so foreground tasks ride each runtime + // thread's CFRunLoop instead of sitting in never-pumped libplatform queues + Runtime::platform_ = std::make_shared(platform::NewDefaultPlatform()); V8::InitializePlatform(Runtime::platform_.get()); V8::Initialize(); @@ -285,6 +301,16 @@ void DisposeIsolateWhenPossible(Isolate* isolate) { create_params.array_buffer_allocator = &allocator_; Isolate* isolate = Isolate::New(create_params); runtimeLoop_ = CFRunLoopGetCurrent(); + // v8 already asked for this isolate's task runner during Isolate::New, so + // the registry may hold an unbound loop - or a stopped one, when a worker + // isolate reuses a disposed isolate's address. Refresh, then attach to this + // thread; foreground tasks buffered so far start flowing from here on. + // (In the reused-address case, tasks v8 posted DURING Isolate::New landed + // in the stale stopped loop and were dropped - refreshing any earlier is + // impossible (the address isn't known) and refreshing on runner lookup + // would defeat the matched-erase teardown protection.) + eventLoop_ = NativeScriptPlatform::Instance()->RefreshEventLoop(isolate); + eventLoop_->BindToCurrentThread(); isolate->SetData(Constants::RUNTIME_SLOT, this); { @@ -313,6 +339,7 @@ void DisposeIsolateWhenPossible(Isolate* isolate) { // Worker::Init(isolate, globalTemplate, isWorker); DefineTimeMethod(isolate, globalTemplate); DefineDrainMicrotaskMethod(isolate, globalTemplate); + DefineQueueMacrotaskMethod(isolate, globalTemplate); // queueMicrotask(callback) per spec { Local qmtTemplate = @@ -329,7 +356,6 @@ void DisposeIsolateWhenPossible(Isolate* isolate) { globalTemplate->Set(tns::ToV8String(isolate, "queueMicrotask"), qmtTemplate); } ObjectManager::Init(isolate, globalTemplate); - // SetTimeout::Init(isolate, globalTemplate); MetadataBuilder::RegisterConstantsOnGlobalObject(isolate, globalTemplate, isWorker); isolate->SetCaptureStackTraceForUncaughtExceptions(true, 100, StackTrace::kOverview); @@ -546,6 +572,40 @@ void DisposeIsolateWhenPossible(Isolate* isolate) { globalTemplate->Set(ToV8String(isolate, "__drainMicrotaskQueue"), drainMicrotaskTemplate); } +// TODO: remove the __ns__ prefix once the event loop's ordered lane backs +// public macrotask APIs (performance observers etc.) +void Runtime::DefineQueueMacrotaskMethod(v8::Isolate* isolate, + v8::Local globalTemplate) { + Local queueMacrotaskTemplate = + FunctionTemplate::New(isolate, [](const FunctionCallbackInfo& info) { + auto* isolate = info.GetIsolate(); + if (info.Length() < 1 || !info[0]->IsFunction()) { + isolate->ThrowException(Exception::TypeError( + tns::ToV8String(isolate, "__ns__queueMacrotask: callback must be a function"))); + return; + } + Runtime* runtime = Runtime::GetRuntime(isolate); + if (runtime == nullptr || runtime->GetEventLoop() == nullptr) { + return; + } + auto callback = + std::make_shared>(isolate, info[0].As()); + // the ordered lane is one due-ordered domain with JS timers, so the + // callback runs as a macrotask in strict FIFO order with them + runtime->GetEventLoop()->PostOrdered([isolate, callback]() { + auto cache = Caches::Get(isolate); + Local context = cache->GetContext(); + Context::Scope context_scope(context); + Local cb = callback->Get(isolate); + callback->Reset(); + // no TryCatch: like timer callbacks, uncaught errors surface + // through the isolate's message listener + (void)cb->Call(context, context->Global(), 0, nullptr); + }); + }); + globalTemplate->Set(ToV8String(isolate, "__ns__queueMacrotask"), queueMacrotaskTemplate); +} + void Runtime::DefineDateTimeConfigurationChangeNotificationMethod( v8::Isolate* isolate, v8::Local globalTemplate) { Local drainMicrotaskTemplate = diff --git a/NativeScript/runtime/SetTimeout.cpp b/NativeScript/runtime/SetTimeout.cpp deleted file mode 100644 index a8472618..00000000 --- a/NativeScript/runtime/SetTimeout.cpp +++ /dev/null @@ -1,106 +0,0 @@ -#include -#include "SetTimeout.h" -#include "Helpers.h" -#include "Caches.h" - -using namespace v8; - -namespace tns { - -void SetTimeout::Init(Isolate* isolate, Local globalTemplate) { - Local setTimeoutFuncTemplate = FunctionTemplate::New(isolate, SetTimeoutCallback); - globalTemplate->Set(ToV8String(isolate, "setTimeout"), setTimeoutFuncTemplate); - - Local clearTimeoutFuncTemplate = FunctionTemplate::New(isolate, ClearTimeoutCallback); - globalTemplate->Set(ToV8String(isolate, "clearTimeout"), clearTimeoutFuncTemplate); -} - -void SetTimeout::SetTimeoutCallback(const FunctionCallbackInfo& args) { - Isolate* isolate = args.GetIsolate(); - if (!args[0]->IsFunction()) { - tns::Assert(false, isolate); - } - - Local context = isolate->GetCurrentContext(); - - double timeout = 0.0; - if (args.Length() > 1 && args[1]->IsNumber()) { - if (!args[1]->NumberValue(context).To(&timeout)) { - tns::Assert(false, isolate); - } - } - - // TODO: implement better unique number generator - uint32_t key = ++count_; - Local callback = args[0].As(); - dispatch_block_t block = dispatch_block_create(DISPATCH_BLOCK_INHERIT_QOS_CLASS, ^{ Elapsed(key); }); - CacheEntry entry(isolate, new Persistent(isolate, callback)); - cache_.emplace(key, entry); - - dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, timeout * NSEC_PER_MSEC); - dispatch_after(time, dispatch_get_main_queue(), block); - - args.GetReturnValue().Set(key); -} - -void SetTimeout::ClearTimeoutCallback(const FunctionCallbackInfo& args) { - Isolate* isolate = args.GetIsolate(); - if (!args[0]->IsNumber()) { - tns::Assert(false, isolate); - } - - Local context = isolate->GetCurrentContext(); - double value; - if (!args[0]->NumberValue(context).To(&value)) { - tns::Assert(false, isolate); - } - - uint32_t key = value; - auto it = cache_.find(key); - if (it == cache_.end()) { - return; - } - - RemoveKey(key); -} - -void SetTimeout::Elapsed(const uint32_t key) { - auto it = cache_.find(key); - if (it == cache_.end()) { - return; - } - - Isolate* isolate = it->second.isolate_; - Persistent* poCallback = it->second.callback_; - v8::Locker locker(isolate); - Isolate::Scope isolate_scope(isolate); - HandleScope handle_scope(isolate); - - Local cb = poCallback->Get(isolate); - std::shared_ptr cache = Caches::Get(isolate); - Local context = cache->GetContext(); - Local global = context->Global(); - Local result; - if (!cb->Call(context, global, 0, nullptr).ToLocal(&result)) { - tns::Assert(false, isolate); - } - - RemoveKey(key); -} - -void SetTimeout::RemoveKey(const uint32_t key) { - auto it = cache_.find(key); - if (it == cache_.end()) { - return; - } - - Persistent* poCallback = it->second.callback_; - poCallback->Reset(); - delete poCallback; - cache_.erase(it); -} - -robin_hood::unordered_map SetTimeout::cache_; -uint32_t SetTimeout::count_ = 0; - -} diff --git a/NativeScript/runtime/SetTimeout.h b/NativeScript/runtime/SetTimeout.h deleted file mode 100644 index bba1bf0d..00000000 --- a/NativeScript/runtime/SetTimeout.h +++ /dev/null @@ -1,34 +0,0 @@ -#ifndef SetTimeout_h -#define SetTimeout_h - -#include "Common.h" -#include "robin_hood.h" - -namespace tns { - -class SetTimeout { -public: - static void Init(v8::Isolate* isolate, v8::Local globalTemplate); -private: - static void SetTimeoutCallback(const v8::FunctionCallbackInfo& args); - static void ClearTimeoutCallback(const v8::FunctionCallbackInfo& args); - static void Elapsed(const uint32_t key); - static void RemoveKey(const uint32_t key); - static uint32_t count_; - - struct CacheEntry { - CacheEntry(v8::Isolate* isolate, v8::Persistent* callback) - : isolate_(isolate), - callback_(callback) { - } - - v8::Isolate* isolate_; - v8::Persistent* callback_; - }; - - static robin_hood::unordered_map cache_; -}; - -} - -#endif /* SetTimeout_h */ diff --git a/NativeScript/runtime/Timers.cpp b/NativeScript/runtime/Timers.cpp index 897e6290..f97e2b6b 100644 --- a/NativeScript/runtime/Timers.cpp +++ b/NativeScript/runtime/Timers.cpp @@ -8,15 +8,32 @@ #include "Timers.hpp" -#include - +#include #include #include "Caches.h" +#include "EventLoop.h" #include "Helpers.h" #include "ModuleBinding.hpp" #include "Runtime.h" +/* + * Overall rules when modifying this file: + * Everything runs on the isolate's home thread under its v8::Locker. + * `sortedTimers_` must always be sorted by dueTime (stable for equal + * dueTimes) and in sync with `timerMap_` (except tombstones, which only live + * in `sortedTimers_`). + * + * Scheduling model: every scheduled timer posts one anonymous "due token" + * through the runtime EventLoop's ordered lane, at a due time >= the timer's. + * Timers therefore share one due-ordered domain with ordered macrotasks and + * stay in fire-date order with foreign runloop timers. The token does not + * name a timer: the EventLoop drain consumes the earliest due item across + * this list and its own ordered entries. Cancelling recalls the pending + * token when possible and otherwise leaves a tombstone so the matured token + * consumes a slot as a no-op instead of lending its position to a later item. + */ + using namespace v8; // Takes a value and transform into a positive number @@ -46,88 +63,188 @@ static double now_ms() { namespace tns { -class TimerState { +struct TimerReference { + int id; + double dueTime; + // clearTimeout/clearInterval tombstones the entry instead of erasing it: + // its already-posted token then consumes this slot as a no-op, so no token + // gains surplus capacity to run a LATER-scheduled item ahead of foreign + // runloop work queued between the two token positions + bool cancelled = false; +}; + +class TimerState : public OrderedTaskSource { public: - std::mutex timerMutex_; std::atomic currentTimerId = 0; robin_hood::unordered_map> timerMap_; - CFRunLoopRef runloop; + // scheduled timers (and tombstones) sorted by exact (sub-millisecond) + // dueTime, stable for equal dueTimes; touched only on the home thread + std::vector sortedTimers_; + v8::Isolate* isolate_ = nullptr; + std::shared_ptr eventLoop_; + bool stopped_ = false; + + ~TimerState() override { + stopped_ = true; + if (eventLoop_ != nullptr) { + // the loop is already shut down by ~Runtime at this point (every + // pending token dropped), but the source pointer must not outlive us + eventLoop_->SetTimerSource(nullptr); + eventLoop_.reset(); + } + for (auto& entry : timerMap_) { + entry.second->Unschedule(); + } + timerMap_.clear(); + sortedTimers_.clear(); + } - void removeTask(const std::shared_ptr& task) { - removeTask(task->id_); + void insertSorted(int id, double dueTime) { + auto it = + std::upper_bound(sortedTimers_.begin(), sortedTimers_.end(), dueTime, + [](double due, const TimerReference& ref) { + return due < ref.dueTime; + }); + sortedTimers_.insert(it, TimerReference{id, dueTime}); } - void removeTask(const int& taskId) { - auto it = timerMap_.find(taskId); - if (it != timerMap_.end()) { - // auto wasScheduled = it->second->queued_; - auto timer = it->second->timer; - it->second->Unschedule(); - timerMap_.erase(it); - CFRunLoopTimerInvalidate(timer); - // CFRunLoopTimerInvalidate triggers our TimerRelease callback, which - // deletes TimerContext, whose destructor calls CFRelease(task->timer) + void postToken(const std::shared_ptr& task) { + if (eventLoop_ == nullptr) { + return; } + // the loop clamps an overdue due time to its own `now`; remember the key + // it actually recorded so cancellation can recall this exact token + task->postedTokenTime_ = eventLoop_->PostOrderedToken(task->dueTime_); } - // this all comes from the android runtime implementation - void addTask(std::shared_ptr task) { + void addTask(const std::shared_ptr& task) { if (task->queued_) { return; } - // auto now = now_ms(); - // task->nestingLevel_ = nesting + 1; task->queued_ = true; - // theoretically this should be >5 on the spec, but we're following chromium - // behavior here again - // if (task->nestingLevel_ >= 5 && task->frequency_ < 4) { - // task->frequency_ = 4; - // task->startTime_ = now; - // } timerMap_.emplace(task->id_, task); - // not needed on the iOS runtime for now - // auto newTime = task->NextTime(now); - // task->dueTime_ = newTime; + insertSorted(task->id_, task->dueTime_); } -}; -// this class is attached to the timer object itself -// we use a retain/release flow because we want to bind this to the Timer itself -// additionally it helps if we deal with timers on different threads -// The current implementation puts the timers on the runtime's runloop, so it -// shouldn't be necessary. -class TimerContext { - public: - std::atomic retainCount{0}; - std::shared_ptr task; - TimerState* state; - ~TimerContext() { - task->Unschedule(); - CFRelease(task->timer); + void removeTask(const int& taskId) { + auto it = timerMap_.find(taskId); + if (it == timerMap_.end()) { + return; + } + if (it->second->queued_) { + auto dueTime = it->second->dueTime_; + auto sit = + std::lower_bound(sortedTimers_.begin(), sortedTimers_.end(), dueTime, + [](const TimerReference& ref, double due) { + return ref.dueTime < due; + }); + while (sit != sortedTimers_.end() && sit->dueTime == dueTime) { + if (sit->id == taskId) { + // a not-yet-matured token is still in the loop's own bookkeeping + // and can be recalled outright, un-arming its wakeup (the + // pre-event-loop behavior of CFRunLoopTimerInvalidate). A matured + // one cannot - its slot gets the tombstone instead. + if (eventLoop_ != nullptr && + eventLoop_->TryCancelOrderedToken(it->second->postedTokenTime_)) { + sortedTimers_.erase(sit); + } else { + sit->cancelled = true; + } + break; + } + ++sit; + } + } + it->second->Unschedule(); + timerMap_.erase(it); } - static const void* TimerRetain(const void* ret) { - auto v = (TimerContext*)(ret); - v->retainCount++; - return ret; - } + /** + * Invoked by the EventLoop's ordered-lane token drain on the isolate's + * thread: if the front slot is due and earlier-or-equal to the loop's own + * earliest entry, consume it - firing the earliest due timer (exact + * sub-millisecond order, not necessarily the timer that enqueued the token) + * or swallowing a tombstone left by clearTimeout/clearInterval. + */ + bool RunIfEarliest(double now, double otherDue) override { + auto isolate = isolate_; + if (stopped_ || isolate == nullptr || isolate->IsDead()) { + return false; + } + v8::Locker locker(isolate); + v8::Isolate::Scope isolate_scope(isolate); + v8::HandleScope handleScope(isolate); + if (sortedTimers_.empty()) { + return false; + } + auto ref = sortedTimers_.front(); + if (ref.dueTime > now_ms() || (otherDue >= 0 && ref.dueTime > otherDue)) { + // not due, or the loop's own entry is earlier - not this source's slot + return false; + } + sortedTimers_.erase(sortedTimers_.begin()); + if (ref.cancelled) { + // tombstone: this slot's token is spent doing nothing, keeping tokens + // and slots 1:1 + return true; + } + auto it = timerMap_.find(ref.id); + if (it == timerMap_.end()) { + return true; + } + auto task = it->second; + if (!task->queued_ || !task->wrapper.IsValid()) { + return true; + } + + // reschedule before invoking, so a throwing callback can't kill the + // interval - matching the repeating CFRunLoopTimer behavior this replaces + if (task->repeats_) { + task->dueTime_ = task->NextTime(now_ms()); + insertSorted(task->id_, task->dueTime_); + postToken(task); + } + + v8::Local cb = task->callback_.Get(isolate); + v8::Local context = + cb->GetCreationContextChecked(v8::Isolate::GetCurrent()); + Context::Scope context_scope(context); + int argc = task->args_ ? static_cast(task->args_->size()) : 0; + if (argc > 0) { + std::vector> argv(argc); + for (int i = 0; i < argc; ++i) { + argv[i] = task->args_->at(i)->Get(isolate); + } + (void)cb->Call(context, context->Global(), argc, argv.data()); + } else { + (void)cb->Call(context, context->Global(), 0, nullptr); + } - static void TimerRelease(const void* ret) { - auto v = (TimerContext*)(ret); - if (--v->retainCount <= 0) { - delete v; - }; + if (!task->repeats_) { + // re-resolve: the callback may have cleared this id itself + auto post = timerMap_.find(ref.id); + if (post != timerMap_.end() && post->second == task) { + post->second->Unschedule(); + timerMap_.erase(post); + } + } + return true; } }; void Timers::Init(Isolate* isolate, Local globalTemplate) { auto timerState = new TimerState(); - timerState->runloop = Runtime::GetRuntime(isolate)->RuntimeLoop(); + timerState->isolate_ = isolate; + // Runtime::CreateIsolate bound the loop to this thread's runloop before + // any builtin initialization runs; guard anyway so an embedding path with + // no runtime degrades to inert timers instead of crashing + Runtime* runtime = Runtime::GetRuntime(isolate); + timerState->eventLoop_ = + runtime != nullptr ? runtime->GetEventLoop() : nullptr; + if (timerState->eventLoop_ != nullptr) { + timerState->eventLoop_->SetTimerSource(timerState); + } Caches::Get(isolate)->registerCacheBoundObject(timerState); - tns::NewFunctionTemplate( - isolate, Timers::SetTimeoutCallback, - v8::External::New(isolate, timerState, - v8::kExternalPointerTypeTagDefault)); tns::SetMethod(isolate, globalTemplate, "__ns__setTimeout", Timers::SetTimeoutCallback, v8::External::New(isolate, timerState, @@ -144,51 +261,6 @@ void Timers::Init(Isolate* isolate, Local globalTemplate) { Timers::ClearTimeoutCallback, v8::External::New(isolate, timerState, v8::kExternalPointerTypeTagDefault)); - Caches::Get(isolate)->registerCacheBoundObject(new TimerState()); -} - -void TimerCallback(CFRunLoopTimerRef timer, void* info) { - TimerContext* data = (TimerContext*)info; - auto task = data->task; - // we check for this first so we can be 100% sure that this task is still - // alive since we're always dealing with the runtime's runloop, it should - // always work if we even support firing the timers in a another runloop, then - // this is useful as it'll avoid use-after-free issues - if (!task->queued_ || !task->wrapper.IsValid()) { - return; - } - auto isolate = task->isolate_; - - v8::Locker locker(isolate); - v8::Isolate::Scope isolate_scope(isolate); - v8::HandleScope handleScope(isolate); - // ensure we're still queued after locking - if (!task->queued_) { - return; - } - - v8::Local cb = task->callback_.Get(isolate); - v8::Local context = - cb->GetCreationContextChecked(v8::Isolate::GetCurrent()); - Context::Scope context_scope(context); - int argc = task->args_ ? static_cast(task->args_->size()) : 0; - if (argc > 0) { - // allocate an array of the right size - std::vector> argv(argc); - - for (int i = 0; i < argc; ++i) { - argv[i] = task->args_->at(i)->Get(isolate); - } - - // pass pointer to the first element - (void)cb->Call(context, context->Global(), argc, argv.data()); - } else { - (void)cb->Call(context, context->Global(), 0, nullptr); - } - - if (!task->repeats_) { - data->state->removeTask(task); - } } void Timers::SetTimer(const v8::FunctionCallbackInfo& args, @@ -228,41 +300,16 @@ void Timers::SetTimer(const v8::FunctionCallbackInfo& args, } } + auto now = now_ms(); auto task = std::make_shared(isolate, handler, timeout, - repeatable, argArray, id, now_ms()); + repeatable, argArray, id, now); #ifdef DEBUG task->callback_.AnnotateStrongRetainer("timer"); #endif task->repeats_ = repeatable; - - CFRunLoopTimerContext timerContext = {0, NULL, NULL, NULL, NULL}; - auto timerData = new TimerContext(); - timerData->task = task; - timerData->state = state; - timerContext.info = timerData; - timerContext.retain = TimerContext::TimerRetain; - timerContext.release = TimerContext::TimerRelease; - - // we do this because the timer should take hold of exactly 1 retaincount - // after scheduling so if by our manual release the retain is 0 then we need - // to cleanup the TimerContext - TimerContext::TimerRetain(timerData); - - // timeout should be bigger than 0 if it's repeatable and 0 - auto timeoutInSeconds = - repeatable && timeout == 0 ? 0.0000001f : timeout / 1000.f; - auto timer = CFRunLoopTimerCreate( - kCFAllocatorDefault, CFAbsoluteTimeGetCurrent() + timeoutInSeconds, - repeatable ? timeoutInSeconds : 0, 0, 0, TimerCallback, &timerContext); + task->dueTime_ = now + (double)timeout; state->addTask(task); - // set the actual timer we created - task->timer = timer; - CFRunLoopAddTimer(state->runloop, timer, kCFRunLoopCommonModes); - TimerContext::TimerRelease(timerData); - // auto task = std::make_shared(isolate, handler, timeout, - // repeatable, - // argArray, id, now_ms()); - // thiz->addTask(task); + state->postToken(task); } args.GetReturnValue().Set(id); } diff --git a/NativeScript/runtime/Timers.hpp b/NativeScript/runtime/Timers.hpp index 9f3b611d..cc9b5c2e 100644 --- a/NativeScript/runtime/Timers.hpp +++ b/NativeScript/runtime/Timers.hpp @@ -43,9 +43,7 @@ class TimerTask { isolate_ = nullptr; queued_ = false; } - - // unused for now as we're using CFRunLoopTimers - // + int nestingLevel_ = 0; v8::Isolate *isolate_; v8::Persistent callback_; @@ -53,12 +51,15 @@ class TimerTask { double frequency_ = 0; bool repeats_ = false; bool queued_ = false; - + double dueTime_ = -1; + // the ordered-token key actually posted for the current cycle: dueTime_, + // or the (later) post time when the timer was already overdue - the value + // cancellation must use to recall the token + double postedTokenTime_ = -1; double startTime_ = -1; int id_; IsolateWrapper wrapper; - CFRunLoopTimerRef timer = nullptr; }; class Timers { public: diff --git a/NativeScript/runtime/Worker.mm b/NativeScript/runtime/Worker.mm index dbb02997..b601dddd 100644 --- a/NativeScript/runtime/Worker.mm +++ b/NativeScript/runtime/Worker.mm @@ -311,7 +311,7 @@ throw NativeScriptException( return; } - tns::ExecuteOnRunLoop(runtime->RuntimeLoop(), [state, message]() { + runtime->GetEventLoop()->PostInternal([state, message]() { Isolate* isolate = state->GetIsolate(); v8::Locker locker(isolate); Isolate::Scope isolate_scope(isolate); diff --git a/NativeScript/runtime/WorkerWrapper.mm b/NativeScript/runtime/WorkerWrapper.mm index c2af86f6..246799a3 100644 --- a/NativeScript/runtime/WorkerWrapper.mm +++ b/NativeScript/runtime/WorkerWrapper.mm @@ -19,6 +19,29 @@ workers_.maxConcurrentOperationCount = 100; } +// Posts to the target runtime's internal lane from the worker thread. When +// async is false, blocks until the entry ran - or until it is destroyed +// unrun by a shutdown that raced the post, which must release the waiter too. +static void PostToRuntimeLoop(Runtime* runtime, std::function fn, bool async) { + auto loop = runtime->GetEventLoop(); + if (loop == nullptr) { + return; + } + if (async) { + loop->PostInternal(std::move(fn)); + return; + } + dispatch_semaphore_t done = dispatch_semaphore_create(0); + // signals when the LAST reference dies: after fn ran, or when Shutdown + // clears the queue and destroys the entry without running it + std::shared_ptr completion(nullptr, [done](void*) { dispatch_semaphore_signal(done); }); + bool posted = loop->PostInternal([fn = std::move(fn), completion]() { fn(); }); + completion.reset(); + if (posted) { + dispatch_semaphore_wait(done, DISPATCH_TIME_FOREVER); + } +} + WorkerWrapper::WorkerWrapper( v8::Isolate* mainIsolate, std::function thiz, std::shared_ptr)> @@ -284,8 +307,8 @@ if (runtime == nullptr) { return; } - tns::ExecuteOnRunLoop( - runtime->RuntimeLoop(), + PostToRuntimeLoop( + runtime, [this, message, src, stackTrace, lineNumber]() { v8::Locker locker(this->mainIsolate_); Isolate::Scope isolate_scope(this->mainIsolate_); @@ -341,8 +364,8 @@ if (runtime == nullptr) { return; } - tns::ExecuteOnRunLoop( - runtime->RuntimeLoop(), + PostToRuntimeLoop( + runtime, [this, message, source, stackTrace, lineNumber]() { v8::Locker locker(this->mainIsolate_); Isolate::Scope isolate_scope(this->mainIsolate_); diff --git a/TestRunner/app/tests/EventLoopTests.js b/TestRunner/app/tests/EventLoopTests.js new file mode 100644 index 00000000..a8e79e59 --- /dev/null +++ b/TestRunner/app/tests/EventLoopTests.js @@ -0,0 +1,416 @@ +// V8 delivers these resolutions as platform foreground tasks, so they only +// settle if the runtime pumps its foreground task runner (the EventLoop's +// internal lane). +describe("event loop foreground tasks", function () { + it("resolves Atomics.waitAsync when notified on the same thread", function (done) { + const sab = new SharedArrayBuffer(4); + const i32 = new Int32Array(sab); + + const result = Atomics.waitAsync(i32, 0, 0); + expect(result.async).toBe(true); + + result.value.then(value => { + expect(value).toBe("ok"); + done(); + }).catch(e => { + fail("Atomics.waitAsync promise rejected: " + e); + done(); + }); + + const woken = Atomics.notify(i32, 0); + expect(woken).toBe(1); + }); + + it("resolves Atomics.waitAsync with 'timed-out' after the timeout", function (done) { + const sab = new SharedArrayBuffer(4); + const i32 = new Int32Array(sab); + + const result = Atomics.waitAsync(i32, 0, 0, 50); + expect(result.async).toBe(true); + + result.value.then(value => { + expect(value).toBe("timed-out"); + done(); + }).catch(e => { + fail("Atomics.waitAsync promise rejected: " + e); + done(); + }); + }); + + it("resolves Atomics.waitAsync synchronously on value mismatch", function () { + const sab = new SharedArrayBuffer(4); + const i32 = new Int32Array(sab); + i32[0] = 42; + + const result = Atomics.waitAsync(i32, 0, 0); + expect(result.async).toBe(false); + expect(result.value).toBe("not-equal"); + }); + + it("keeps ordinary promise chains working alongside foreground tasks", function (done) { + const sab = new SharedArrayBuffer(4); + const i32 = new Int32Array(sab); + const order = []; + + Atomics.waitAsync(i32, 0, 0).value.then(() => { + order.push("waitAsync"); + return Promise.resolve(); + }).then(() => { + order.push("chained"); + expect(order).toEqual(["waitAsync", "chained"]); + done(); + }).catch(e => { + fail("promise chain failed: " + e); + done(); + }); + + Atomics.notify(i32, 0); + }); + + // The kAuto-stall fix: the wakeup task resolves the promise without + // entering JS, so its .then only runs if each loop entry ends with a + // microtask checkpoint. It must beat a timer scheduled well after it. + it("runs microtasks of a natively-resolved promise before a later macrotask", function (done) { + const i32 = new Int32Array(new SharedArrayBuffer(4)); + const order = []; + + Atomics.waitAsync(i32, 0, 0).value.then(() => { + order.push("wait"); + }); + Atomics.notify(i32, 0); + + __ns__setTimeout(() => { + order.push("timer"); + expect(order).toEqual(["wait", "timer"]); + done(); + }, 50); + }); +}); + +// The ordered lane rides the home runloop's performed-block order, so these +// callbacks must be strict macrotasks: after the current turn's microtasks, +// FIFO with native timers by due time. +describe("event loop ordered macrotasks", function () { + it("__ns__queueMacrotask runs the callback asynchronously", function (done) { + let ran = false; + __ns__queueMacrotask(() => { + ran = true; + done(); + }); + expect(ran).toBe(false); + }); + + it("runs after the current turn's microtasks", function (done) { + const order = []; + __ns__queueMacrotask(() => { + order.push("macrotask"); + expect(order).toEqual(["microtask", "macrotask"]); + done(); + }); + Promise.resolve().then(() => order.push("microtask")); + }); + + it("is FIFO among itself", function (done) { + const order = []; + __ns__queueMacrotask(() => order.push(1)); + __ns__queueMacrotask(() => order.push(2)); + __ns__queueMacrotask(() => { + order.push(3); + expect(order).toEqual([1, 2, 3]); + done(); + }); + }); + + // native timers (__ns__*): the app-level `setTimeout` global in this test + // app is an NSTimer-based polyfill, not the runtime timers + it("stays FIFO-ordered with native setTimeout(0)", function (done) { + const order = []; + __ns__queueMacrotask(() => order.push("macro1")); + __ns__setTimeout(() => order.push("timeout"), 0); + __ns__queueMacrotask(() => { + order.push("macro2"); + expect(order).toEqual(["macro1", "timeout", "macro2"]); + done(); + }); + }); + + it("rejects non-function arguments", function () { + expect(() => __ns__queueMacrotask("nope")).toThrowError(TypeError); + expect(() => __ns__queueMacrotask()).toThrowError(TypeError); + }); + + // the global setTimeout is the harness's NSTimer polyfill - a foreign + // CFRunLoopTimer on the same runloop. A foreign timer due between two of + // our matured tokens must fire between them (one token drains per fire; + // the runloop orders due timers by fire date), not after the batch. + it("interleaves with foreign NSTimer timeouts by due time", function (done) { + const order = []; + __ns__setTimeout(() => order.push("ns20"), 20); + setTimeout(() => order.push("nstimer25"), 25); + __ns__setTimeout(() => order.push("ns30"), 30); + // make all three overdue so they drain from the same runloop burst + const start = Date.now(); + while (Date.now() - start < 45) { } + setTimeout(() => { + expect(order).toEqual(["ns20", "nstimer25", "ns30"]); + done(); + }, 30); + }); +}); + +// A self-rescheduling immediate timer must not starve the rest of the +// runloop: foreign timers, the GCD main queue and the before-waiting phase +// all have to keep running between steps. The before-waiting probe is the +// runtime's own rejection drain (a native kCFRunLoopBeforeWaiting observer): +// a rejection created mid-chain must be reported while the chain is still +// running, not after it ends. +describe("event loop immediate timer yielding", function () { + it("yields to native work between immediate timer chain steps", function (done) { + let step = 0; + let nstimerStep = -1; + let mainQueueStep = -1; + let drainStep = -1; + const onRejection = (e) => { + e.preventDefault(); + drainStep = step; + }; + global.addEventListener("unhandledrejection", onRejection); + setTimeout(() => { nstimerStep = step; }, 0); // foreign NSTimer + NSOperationQueue.mainQueue.addOperationWithBlock(() => { mainQueueStep = step; }); + + (function chain() { + step++; + if (step === 5) { + Promise.reject(new Error("event loop yield probe")); + } + if (step < 50) { + __ns__setTimeout(chain, 0); + return; + } + // NSTimer wait: guarantees the loop reaches before-waiting at + // least once after the chain, so a starved drain still fires + // before the assertions instead of leaking into later specs + setTimeout(() => { + global.removeEventListener("unhandledrejection", onRejection); + expect(nstimerStep).toBeGreaterThan(0); + expect(nstimerStep).toBeLessThan(25); + expect(mainQueueStep).toBeGreaterThan(0); + expect(mainQueueStep).toBeLessThan(25); + expect(drainStep).toBeGreaterThan(4); + expect(drainStep).toBeLessThan(45); + done(); + }, 100); + })(); + }); + +}); + +// clearTimeout leaves a tombstone in the merged ordered domain, so the +// cleared timer's already-posted token consumes its own slot as a no-op +// instead of running a later-scheduled item ahead of foreign runloop work +// queued between the two tokens' positions. +describe("event loop ordered tombstones", function () { + it("clear + re-schedule does not reorder against queued macrotasks", function (done) { + const order = []; + const t1 = __ns__setTimeout(() => order.push("cleared"), 0); + __ns__clearTimeout(t1); + __ns__queueMacrotask(() => order.push("macro")); + __ns__setTimeout(() => { + order.push("t2"); + expect(order).toEqual(["macro", "t2"]); + done(); + }, 0); + }); + + it("clearing an overdue timer does not fire a later timer early", function (done) { + const order = []; + const t1 = __ns__setTimeout(() => order.push("cleared"), 0); + __ns__setTimeout(() => order.push("late"), 30); + __ns__clearTimeout(t1); + // busy-wait so both timers are overdue when their tokens drain in the + // same runloop burst + const start = Date.now(); + while (Date.now() - start < 50) { } + __ns__queueMacrotask(() => { + order.push("macro"); + expect(order).toEqual(["late", "macro"]); + done(); + }); + }); + + // The exact hazard the tombstone exists for: a foreign runloop block + // queued between two timer tokens. Without the tombstone, the cleared + // timer's token would fire the later timer ahead of the native block. + it("does not run a later timer ahead of native blocks queued between the tokens", function (done) { + const order = []; + const first = __ns__setTimeout(() => order.push("cleared"), 0); + CFRunLoopPerformBlock(CFRunLoopGetCurrent(), kCFRunLoopCommonModes, () => order.push("native")); + __ns__setTimeout(() => { + order.push("t3"); + expect(order).toEqual(["native", "t3"]); + done(); + }, 0); + __ns__clearTimeout(first); + }); + + it("does not run a queued macrotask ahead of native blocks queued between the tokens", function (done) { + const order = []; + const first = __ns__setTimeout(() => order.push("cleared"), 0); + CFRunLoopPerformBlock(CFRunLoopGetCurrent(), kCFRunLoopCommonModes, () => order.push("native")); + __ns__queueMacrotask(() => { + order.push("macro"); + expect(order).toEqual(["native", "macro"]); + done(); + }); + __ns__clearTimeout(first); + }); + + it("keeps interval callbacks running after an unrelated clear", function (done) { + const order = []; + const cleared = __ns__setInterval(() => order.push("cleared"), 1); + __ns__clearInterval(cleared); + let count = 0; + const interval = __ns__setInterval(() => { + if (++count === 3) { + __ns__clearInterval(interval); + expect(order).toEqual([]); + done(); + } + }, 5); + }); +}); + +// Top-level await whose continuation arrives as a v8 foreground task. The +// waitAsync wakeup is a NON-nestable task (v8 futex-emulation), so the +// synchronous require spin must not run it while JS frames are on the stack: +// require returns a namespace still in its TDZ, and the module completes from +// the loop right after the turn - on main it stayed incomplete forever. +describe("event loop top-level await", function () { + it("require() of a TLA module blocked on a foreground task completes after the turn", function (done) { + const mod = require("./esm/tla-foreground-task.mjs"); + expect(() => mod.value).toThrowError(ReferenceError); + __ns__setTimeout(() => { + expect(mod.value).toBe("ok"); + done(); + }, 100); + }); + + it("dynamic import of a TLA module blocked on a foreground task settles", function (done) { + let importSettled = false; + import("~/tests/esm/tla-foreground-task-import.mjs").then(mod => { + importSettled = true; + expect(mod.value).toBe("ok"); + }).catch(e => { + importSettled = true; + fail("dynamic import rejected: " + e); + }); + __ns__setTimeout(() => { + // the module itself must have completed from the loop by now... + expect(globalThis.__tlaImportProbeDone).toBe("ok"); + // ...and the import promise must have observed that completion + expect(importSettled).toBe(true); + done(); + }, 300); + }); +}); + +describe("event loop workers", function () { + beforeEach(function () { + this.originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL; + jasmine.DEFAULT_TIMEOUT_INTERVAL = 15000; + }); + + afterEach(function () { + jasmine.DEFAULT_TIMEOUT_INTERVAL = this.originalTimeout; + }); + + it("keeps worker->parent messages ordered", function (done) { + const worker = new Worker("./eventLoopEchoWorker.js"); + const received = []; + worker.onmessage = function (msg) { + received.push(msg.data); + if (received.length === 3) { + expect(received).toEqual([1, 2, 3]); + worker.terminate(); + done(); + } + }; + worker.postMessage(1); + worker.postMessage(2); + worker.postMessage(3); + }); + + it("resolves Atomics.waitAsync inside a worker's own loop", function (done) { + const worker = new Worker("./EventLoopWaitAsyncWorker.js"); + worker.onmessage = function (msg) { + expect(msg.data).toBe("ok"); + worker.terminate(); + done(); + }; + worker.postMessage("go"); + }); + + // A worker reply racing an overdue waitAsync timeout on the parent: the + // reply's wakeup must not be starved by the timeout entry. + it("delivers worker messages whose wakeup raced an overdue waitAsync timeout", function (done) { + const worker = new Worker("./eventLoopEchoWorker.js"); + let warm = false; + worker.onmessage = function (msg) { + if (msg.data === "warmup") { + warm = true; + const i32 = new Int32Array(new SharedArrayBuffer(4)); + Atomics.waitAsync(i32, 0, 0, 50); + worker.postMessage("ping"); + // block the runloop until both the timeout and the reply are + // pending, so their wakeups are serviced from the same burst + const start = Date.now(); + while (Date.now() - start < 150) { } + } else { + expect(warm).toBe(true); + expect(msg.data).toBe("ping"); + worker.terminate(); + done(); + } + }; + worker.postMessage("warmup"); + }); + + it("survives terminating a worker with queued loop work", function (done) { + const worker = new Worker("./eventLoopEchoWorker.js"); + let handled = false; + worker.onmessage = function () { + // echoes of the queued messages can arrive before the terminate + // lands; only the first delivery drives the spec + if (handled) { + return; + } + handled = true; + // several messages are still queued on the worker's loop when the + // terminate lands; none of them may crash or hang teardown + for (let i = 0; i < 20; i++) { + worker.postMessage("queued-" + i); + } + worker.terminate(); + __ns__setTimeout(done, 500); + }; + worker.postMessage("alive"); + }); + + // Rapid create/terminate cycles reuse isolate addresses; each new worker + // must get a live loop, not a dead predecessor's (registry refresh path). + it("keeps event loops healthy across worker churn", function (done) { + let remaining = 8; + (function cycle() { + const worker = new Worker("./eventLoopEchoWorker.js"); + worker.onmessage = function () { + worker.terminate(); + if (--remaining === 0) { + __ns__queueMacrotask(done); + } else { + cycle(); + } + }; + worker.postMessage("alive"); + })(); + }); +}); diff --git a/TestRunner/app/tests/EventLoopWaitAsyncWorker.js b/TestRunner/app/tests/EventLoopWaitAsyncWorker.js new file mode 100644 index 00000000..cd67dcb0 --- /dev/null +++ b/TestRunner/app/tests/EventLoopWaitAsyncWorker.js @@ -0,0 +1,7 @@ +onmessage = function () { + const i32 = new Int32Array(new SharedArrayBuffer(4)); + Atomics.waitAsync(i32, 0, 0).value.then(value => { + postMessage(value); + }); + Atomics.notify(i32, 0); +}; diff --git a/TestRunner/app/tests/esm/tla-foreground-task-import.mjs b/TestRunner/app/tests/esm/tla-foreground-task-import.mjs new file mode 100644 index 00000000..01fe6e99 --- /dev/null +++ b/TestRunner/app/tests/esm/tla-foreground-task-import.mjs @@ -0,0 +1,9 @@ +// Same shape as tla-foreground-task.mjs, kept separate so the dynamic-import +// spec is not served the module registry entry the require spec created. +const i32 = new Int32Array(new SharedArrayBuffer(4)); +const wait = Atomics.waitAsync(i32, 0, 0); +Atomics.notify(i32, 0); +export const value = await wait.value; +// lets the spec tell "module never completed" apart from "import promise +// never observed the completion" +globalThis.__tlaImportProbeDone = value; diff --git a/TestRunner/app/tests/esm/tla-foreground-task.mjs b/TestRunner/app/tests/esm/tla-foreground-task.mjs new file mode 100644 index 00000000..df5e04f6 --- /dev/null +++ b/TestRunner/app/tests/esm/tla-foreground-task.mjs @@ -0,0 +1,6 @@ +// Settles only if v8 foreground tasks are pumped while the module evaluation +// promise is pending: the notify wakeup arrives as a platform task. +const i32 = new Int32Array(new SharedArrayBuffer(4)); +const wait = Atomics.waitAsync(i32, 0, 0); +Atomics.notify(i32, 0); +export const value = await wait.value; diff --git a/TestRunner/app/tests/eventLoopEchoWorker.js b/TestRunner/app/tests/eventLoopEchoWorker.js new file mode 100644 index 00000000..dd42a874 --- /dev/null +++ b/TestRunner/app/tests/eventLoopEchoWorker.js @@ -0,0 +1,3 @@ +onmessage = function (msg) { + postMessage(msg.data); +}; diff --git a/TestRunner/app/tests/index.js b/TestRunner/app/tests/index.js index 29099ece..7489eaef 100644 --- a/TestRunner/app/tests/index.js +++ b/TestRunner/app/tests/index.js @@ -139,6 +139,7 @@ require("./Modules"); require("./RuntimeImplementedAPIs"); require("./Timers"); +require("./EventLoopTests"); require("./URL"); require("./URLSearchParams"); diff --git a/v8ios.xcodeproj/project.pbxproj b/v8ios.xcodeproj/project.pbxproj index 91c741cd..2ba17e1b 100644 --- a/v8ios.xcodeproj/project.pbxproj +++ b/v8ios.xcodeproj/project.pbxproj @@ -294,11 +294,12 @@ 4A5C201A2E2B000100000007 /* RuntimeBuiltins.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4A5C201A2E2B000100000003 /* RuntimeBuiltins.cpp */; }; 4A5C201A2E2B000300000006 /* Performance.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4A5C201A2E2B000300000001 /* Performance.cpp */; }; 4A5C201A2E2B000200000006 /* NsBuiltinModules.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4A5C201A2E2B000200000001 /* NsBuiltinModules.cpp */; }; + 4AE7100A2E2B000400000003 /* EventLoop.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4AE7100A2E2B000400000001 /* EventLoop.mm */; }; + 4AE7100A2E2B000400000006 /* NativeScriptPlatform.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4AE7100A2E2B000400000004 /* NativeScriptPlatform.mm */; }; 4A5C201A2E2B000400000003 /* StructuredClone.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4A5C201A2E2B000400000001 /* StructuredClone.cpp */; }; 4A5C201A2E2B000500000003 /* StructuredSerialization.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4A5C201A2E2B000500000001 /* StructuredSerialization.cpp */; }; C2DDEB93229EAC8300345BFE /* ArgConverter.mm in Sources */ = {isa = PBXBuildFile; fileRef = C2DDEB6A229EAC8200345BFE /* ArgConverter.mm */; }; C2DDEB94229EAC8300345BFE /* Console.cpp in Sources */ = {isa = PBXBuildFile; fileRef = C2DDEB6B229EAC8200345BFE /* Console.cpp */; }; - C2DDEB95229EAC8300345BFE /* SetTimeout.cpp in Sources */ = {isa = PBXBuildFile; fileRef = C2DDEB6C229EAC8200345BFE /* SetTimeout.cpp */; }; C2DDEB96229EAC8300345BFE /* Helpers.h in Headers */ = {isa = PBXBuildFile; fileRef = C2DDEB6D229EAC8200345BFE /* Helpers.h */; }; C2DDEB97229EAC8300345BFE /* Metadata.h in Headers */ = {isa = PBXBuildFile; fileRef = C2DDEB6E229EAC8200345BFE /* Metadata.h */; }; C2DDEB98229EAC8300345BFE /* ArrayAdapter.h in Headers */ = {isa = PBXBuildFile; fileRef = C2DDEB6F229EAC8200345BFE /* ArrayAdapter.h */; }; @@ -306,7 +307,6 @@ C2DDEB9A229EAC8300345BFE /* MetadataBuilder.mm in Sources */ = {isa = PBXBuildFile; fileRef = C2DDEB71229EAC8200345BFE /* MetadataBuilder.mm */; }; C2DDEB9B229EAC8300345BFE /* DataWrapper.h in Headers */ = {isa = PBXBuildFile; fileRef = C2DDEB72229EAC8200345BFE /* DataWrapper.h */; }; C2DDEB9C229EAC8300345BFE /* ClassBuilder.cpp in Sources */ = {isa = PBXBuildFile; fileRef = C2DDEB73229EAC8200345BFE /* ClassBuilder.cpp */; }; - C2DDEB9D229EAC8300345BFE /* SetTimeout.h in Headers */ = {isa = PBXBuildFile; fileRef = C2DDEB74229EAC8200345BFE /* SetTimeout.h */; }; C2DDEB9E229EAC8300345BFE /* SymbolLoader.mm in Sources */ = {isa = PBXBuildFile; fileRef = C2DDEB75229EAC8200345BFE /* SymbolLoader.mm */; }; C2DDEB9F229EAC8300345BFE /* Tasks.h in Headers */ = {isa = PBXBuildFile; fileRef = C2DDEB76229EAC8200345BFE /* Tasks.h */; }; C2DDEBA0229EAC8300345BFE /* Interop.mm in Sources */ = {isa = PBXBuildFile; fileRef = C2DDEB77229EAC8200345BFE /* Interop.mm */; }; @@ -816,6 +816,10 @@ 4A5C201A2E2B000100000003 /* RuntimeBuiltins.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = RuntimeBuiltins.cpp; path = generated/RuntimeBuiltins.cpp; sourceTree = ""; }; 4A5C201A2E2B000100000004 /* RuntimeBuiltins.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RuntimeBuiltins.h; path = generated/RuntimeBuiltins.h; sourceTree = ""; }; 4A5C201A2E2B000100000005 /* js */ = {isa = PBXFileReference; lastKnownFileType = folder; path = js; sourceTree = ""; }; + 4AE7100A2E2B000400000001 /* EventLoop.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = EventLoop.mm; sourceTree = ""; }; + 4AE7100A2E2B000400000002 /* EventLoop.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = EventLoop.h; sourceTree = ""; }; + 4AE7100A2E2B000400000004 /* NativeScriptPlatform.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = NativeScriptPlatform.mm; sourceTree = ""; }; + 4AE7100A2E2B000400000005 /* NativeScriptPlatform.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NativeScriptPlatform.h; sourceTree = ""; }; 4A5C201A2E2B000400000001 /* StructuredClone.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = StructuredClone.cpp; sourceTree = ""; }; 4A5C201A2E2B000400000002 /* StructuredClone.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = StructuredClone.h; sourceTree = ""; }; 4A5C201A2E2B000500000001 /* StructuredSerialization.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = StructuredSerialization.cpp; sourceTree = ""; }; @@ -824,7 +828,6 @@ 4A5C201A2E2B000300000002 /* Performance.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Performance.h; sourceTree = ""; }; C2DDEB6A229EAC8200345BFE /* ArgConverter.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ArgConverter.mm; sourceTree = ""; }; C2DDEB6B229EAC8200345BFE /* Console.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = Console.cpp; sourceTree = ""; }; - C2DDEB6C229EAC8200345BFE /* SetTimeout.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = SetTimeout.cpp; sourceTree = ""; }; C2DDEB6D229EAC8200345BFE /* Helpers.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Helpers.h; sourceTree = ""; }; C2DDEB6E229EAC8200345BFE /* Metadata.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Metadata.h; sourceTree = ""; }; C2DDEB6F229EAC8200345BFE /* ArrayAdapter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ArrayAdapter.h; sourceTree = ""; }; @@ -832,7 +835,6 @@ C2DDEB71229EAC8200345BFE /* MetadataBuilder.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = MetadataBuilder.mm; sourceTree = ""; }; C2DDEB72229EAC8200345BFE /* DataWrapper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DataWrapper.h; sourceTree = ""; }; C2DDEB73229EAC8200345BFE /* ClassBuilder.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = ClassBuilder.cpp; sourceTree = ""; }; - C2DDEB74229EAC8200345BFE /* SetTimeout.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SetTimeout.h; sourceTree = ""; }; C2DDEB75229EAC8200345BFE /* SymbolLoader.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = SymbolLoader.mm; sourceTree = ""; }; C2DDEB76229EAC8200345BFE /* Tasks.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Tasks.h; sourceTree = ""; }; C2DDEB77229EAC8200345BFE /* Interop.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = Interop.mm; sourceTree = ""; }; @@ -1520,11 +1522,13 @@ C2DDEB7C229EAC8200345BFE /* Runtime.mm */, C2F4D0C92334A6E70008A2EB /* RuntimeConfig.h */, C2F4D0CC2334B1BC0008A2EB /* RuntimeConfig.cpp */, - C2DDEB74229EAC8200345BFE /* SetTimeout.h */, - C2DDEB6C229EAC8200345BFE /* SetTimeout.cpp */, C2C8EE7922CF64E4001F8CEC /* SimpleAllocator.h */, C2C8EE7822CF64E4001F8CEC /* SimpleAllocator.cpp */, C2DDEB87229EAC8300345BFE /* StringHasher.h */, + 4AE7100A2E2B000400000002 /* EventLoop.h */, + 4AE7100A2E2B000400000001 /* EventLoop.mm */, + 4AE7100A2E2B000400000005 /* NativeScriptPlatform.h */, + 4AE7100A2E2B000400000004 /* NativeScriptPlatform.mm */, 4A5C201A2E2B000400000002 /* StructuredClone.h */, 4A5C201A2E2B000400000001 /* StructuredClone.cpp */, 4A5C201A2E2B000500000002 /* StructuredSerialization.h */, @@ -1663,7 +1667,6 @@ C2DDEBAD229EAC8300345BFE /* Caches.h in Headers */, 3C48F68D2F57905500C14231 /* json.hpp in Headers */, C247C17022F82842001D2CA2 /* libffi.h in Headers */, - C2DDEB9D229EAC8300345BFE /* SetTimeout.h in Headers */, C2DDEB90229EAC8300345BFE /* MetadataBuilder.h in Headers */, C266569F22B282BA00EE15CC /* FunctionReference.h in Headers */, C2FEA17022A3C75C00A5C0FC /* InlineFunctions.h in Headers */, @@ -2300,6 +2303,8 @@ C2DDEB92229EAC8300345BFE /* WeakRef.cpp in Sources */, 4A5C201A2E2B000100000006 /* BuiltinLoader.cpp in Sources */, 4A5C201A2E2B000200000006 /* NsBuiltinModules.cpp in Sources */, + 4AE7100A2E2B000400000003 /* EventLoop.mm in Sources */, + 4AE7100A2E2B000400000006 /* NativeScriptPlatform.mm in Sources */, 4A5C201A2E2B000400000003 /* StructuredClone.cpp in Sources */, 4A5C201A2E2B000500000003 /* StructuredSerialization.cpp in Sources */, 4A5C201A2E2B000100000007 /* RuntimeBuiltins.cpp in Sources */, @@ -2327,7 +2332,6 @@ C2FEA16F22A3C75C00A5C0FC /* InlineFunctions.cpp in Sources */, C2DDEB9A229EAC8300345BFE /* MetadataBuilder.mm in Sources */, C266569722AFFFB000EE15CC /* Reference.cpp in Sources */, - C2DDEB95229EAC8300345BFE /* SetTimeout.cpp in Sources */, 6573B9EE291FE5B700B0ED7C /* JSIRuntime.m in Sources */, F6191AB229C0FCE8003F588F /* InspectorServer.mm in Sources */, 6573B9D3291FE29F00B0ED7C /* HostProxy.cpp in Sources */,