diff --git a/Core/AppRuntime/Include/Babylon/AppRuntime.h b/Core/AppRuntime/Include/Babylon/AppRuntime.h index e001fa32..7142cbf9 100644 --- a/Core/AppRuntime/Include/Babylon/AppRuntime.h +++ b/Core/AppRuntime/Include/Babylon/AppRuntime.h @@ -12,6 +12,8 @@ namespace Babylon { + class DeadlineScheduler; + class AppRuntime final { public: @@ -76,6 +78,8 @@ namespace Babylon // queue explicitly (Napi::DrainJobs / JS_ExecutePendingJob). void DrainMicrotasks(Napi::Env env); + DeadlineScheduler& GetDeadlineScheduler(); + Options m_options; class Impl; diff --git a/Core/AppRuntime/Source/AppRuntime.cpp b/Core/AppRuntime/Source/AppRuntime.cpp index 176bc849..5f6d9298 100644 --- a/Core/AppRuntime/Source/AppRuntime.cpp +++ b/Core/AppRuntime/Source/AppRuntime.cpp @@ -1,5 +1,7 @@ #include "AppRuntime.h" +#include + #include #include @@ -35,6 +37,7 @@ namespace Babylon std::optional> m_suspensionLock{}; arcana::cancellation_source m_cancelSource{}; arcana::manual_dispatcher<128> m_dispatcher{}; + std::unique_ptr m_deadlineScheduler{std::make_unique()}; std::thread m_thread; }; @@ -50,7 +53,7 @@ namespace Babylon m_impl->m_thread = std::thread{[this] { RunPlatformTier(); }}; Dispatch([this](Napi::Env env) { - JsRuntime::CreateForJavaScript(env, [this](auto func) { Dispatch(std::move(func)); }); + JsRuntime::CreateForJavaScript(env, [this](auto func) { Dispatch(std::move(func)); }, GetDeadlineScheduler()); }); } @@ -91,6 +94,11 @@ namespace Babylon m_impl->m_dispatcher.clear(); } + DeadlineScheduler& AppRuntime::GetDeadlineScheduler() + { + return *m_impl->m_deadlineScheduler; + } + void AppRuntime::Suspend() { auto suspensionMutex = std::make_shared(); diff --git a/Core/AppRuntime/Source/AppRuntime_V8.cpp b/Core/AppRuntime/Source/AppRuntime_V8.cpp index 89928dcf..aad6cbdc 100644 --- a/Core/AppRuntime/Source/AppRuntime_V8.cpp +++ b/Core/AppRuntime/Source/AppRuntime_V8.cpp @@ -1,18 +1,269 @@ #include "AppRuntime.h" #include +#include + #include +#include + +// Android builds against V8 11.0, desktop against 11.9. A few v8::Platform members below do not +// exist in 11.0, and overriding a method the base class does not declare is a hard error, so they +// are gated. Where a member is gated out, v8::Platform's own default is used instead of forwarding +// to the inner platform; all of them are optional hooks whose defaults are benign (null allocator, +// null blocking scope, clock values derived from CurrentClockTimeMillis). +#define JSRH_V8_AT_LEAST(major, minor) \ + (V8_MAJOR_VERSION > (major) || (V8_MAJOR_VERSION == (major) && V8_MINOR_VERSION >= (minor))) + #ifdef ENABLE_V8_INSPECTOR #include #endif +#include +#include +#include +#include +#include #include +#include +#include +#include namespace Babylon { namespace { + // V8 hands work that finishes off-thread - most visibly asynchronous WebAssembly + // compilation - back to the isolate by posting a task to the platform's *foreground* + // task runner. Chromium runs those as `v8::Task::Run` on the host task runner. The + // host JavaScript thread is AppRuntime's dispatcher, so this platform's foreground + // runner posts the task itself instead of parking it on libplatform's queue and + // pumping later. Delayed posts reuse the DeadlineScheduler behind setTimeout. + class DispatchingPlatform final : public v8::Platform + { + public: + explicit DispatchingPlatform(std::unique_ptr inner) + : m_inner{std::move(inner)} + { + } + + struct Host + { + AppRuntime* runtime{}; + DeadlineScheduler* scheduler{}; + }; + + // Isolate::New can ask for a foreground runner before SetHost has the + // isolate pointer. Posts from this thread during construction fall back + // to the thread-local host set around New. + void SetCurrentHost(AppRuntime* runtime, DeadlineScheduler* scheduler) + { + t_currentRuntime = runtime; + t_currentScheduler = scheduler; + } + + void SetHost(v8::Isolate* isolate, AppRuntime* runtime, DeadlineScheduler* scheduler) + { + std::shared_ptr runner; + { + std::scoped_lock lock{m_mutex}; + if (runtime != nullptr && scheduler != nullptr) + { + m_hosts[isolate] = Host{runtime, scheduler}; + } + else + { + m_hosts.erase(isolate); + auto entry = m_taskRunners.find(isolate); + if (entry != m_taskRunners.end()) + { + runner = std::move(entry->second); + m_taskRunners.erase(entry); + } + } + } + } + + Host GetHost(v8::Isolate* isolate) + { + std::scoped_lock lock{m_mutex}; + const auto entry = m_hosts.find(isolate); + if (entry != m_hosts.end()) + { + return entry->second; + } + return {t_currentRuntime, t_currentScheduler}; + } + + v8::PageAllocator* GetPageAllocator() override { return m_inner->GetPageAllocator(); } +#if JSRH_V8_AT_LEAST(11, 9) + v8::ThreadIsolatedAllocator* GetThreadIsolatedAllocator() override { return m_inner->GetThreadIsolatedAllocator(); } +#endif + v8::ZoneBackingAllocator* GetZoneBackingAllocator() override { return m_inner->GetZoneBackingAllocator(); } + void OnCriticalMemoryPressure() override { m_inner->OnCriticalMemoryPressure(); } + int NumberOfWorkerThreads() override { return m_inner->NumberOfWorkerThreads(); } + + std::shared_ptr GetForegroundTaskRunner(v8::Isolate* isolate) override + { + return WrapForegroundTaskRunner(isolate); + } + +#if JSRH_V8_AT_LEAST(11, 9) + std::shared_ptr GetForegroundTaskRunner(v8::Isolate* isolate, v8::TaskPriority) override + { + return WrapForegroundTaskRunner(isolate); + } +#endif + + void CallOnWorkerThread(std::unique_ptr task) override { m_inner->CallOnWorkerThread(std::move(task)); } + void CallBlockingTaskOnWorkerThread(std::unique_ptr task) override { m_inner->CallBlockingTaskOnWorkerThread(std::move(task)); } + void CallLowPriorityTaskOnWorkerThread(std::unique_ptr task) override { m_inner->CallLowPriorityTaskOnWorkerThread(std::move(task)); } + void CallDelayedOnWorkerThread(std::unique_ptr task, double delayInSeconds) override { m_inner->CallDelayedOnWorkerThread(std::move(task), delayInSeconds); } + bool IdleTasksEnabled(v8::Isolate*) override { return false; } + std::unique_ptr PostJob(v8::TaskPriority priority, std::unique_ptr jobTask) override { return m_inner->PostJob(priority, std::move(jobTask)); } + std::unique_ptr CreateJob(v8::TaskPriority priority, std::unique_ptr jobTask) override { return m_inner->CreateJob(priority, std::move(jobTask)); } +#if JSRH_V8_AT_LEAST(11, 9) + std::unique_ptr CreateBlockingScope(v8::BlockingType blockingType) override { return m_inner->CreateBlockingScope(blockingType); } +#endif + double MonotonicallyIncreasingTime() override { return m_inner->MonotonicallyIncreasingTime(); } +#if JSRH_V8_AT_LEAST(11, 9) + int64_t CurrentClockTimeMilliseconds() override { return m_inner->CurrentClockTimeMilliseconds(); } +#endif + double CurrentClockTimeMillis() override { return m_inner->CurrentClockTimeMillis(); } +#if JSRH_V8_AT_LEAST(11, 9) + double CurrentClockTimeMillisecondsHighResolution() override { return m_inner->CurrentClockTimeMillisecondsHighResolution(); } +#endif + StackTracePrinter GetStackTracePrinter() override { return m_inner->GetStackTracePrinter(); } + v8::TracingController* GetTracingController() override { return m_inner->GetTracingController(); } + void DumpWithoutCrashing() override { m_inner->DumpWithoutCrashing(); } + v8::HighAllocationThroughputObserver* GetHighAllocationThroughputObserver() override { return m_inner->GetHighAllocationThroughputObserver(); } + + private: + std::shared_ptr WrapForegroundTaskRunner(v8::Isolate* isolate); + + std::unique_ptr m_inner; + std::mutex m_mutex; + std::map m_hosts; + std::map> m_taskRunners; + + static thread_local AppRuntime* t_currentRuntime; + static thread_local DeadlineScheduler* t_currentScheduler; + }; + + thread_local AppRuntime* DispatchingPlatform::t_currentRuntime{}; + thread_local DeadlineScheduler* DispatchingPlatform::t_currentScheduler{}; + + class DispatchingTaskRunner final : public v8::TaskRunner + { + public: + DispatchingTaskRunner(DispatchingPlatform& platform, v8::Isolate* isolate) + : m_platform{platform} + , m_isolate{isolate} + { + } + + ~DispatchingTaskRunner() override + { + std::scoped_lock lock{m_mutex}; + for (const auto& pending : m_pending) + { + pending.scheduler->Cancel(pending.id); + } + m_pending.clear(); + } + + void PostTask(std::unique_ptr task) override + { + PostImmediate(std::move(task)); + } + + void PostNonNestableTask(std::unique_ptr task) override + { + PostImmediate(std::move(task)); + } + + void PostDelayedTask(std::unique_ptr task, double delayInSeconds) override + { + PostDelayed(std::move(task), delayInSeconds); + } + + void PostNonNestableDelayedTask(std::unique_ptr task, double delayInSeconds) override + { + PostDelayed(std::move(task), delayInSeconds); + } + + void PostIdleTask(std::unique_ptr) override + { + } + + bool IdleTasksEnabled() override { return false; } + bool NonNestableTasksEnabled() const override { return true; } + bool NonNestableDelayedTasksEnabled() const override { return true; } + + private: + void PostImmediate(std::unique_ptr task) + { + auto host = m_platform.GetHost(m_isolate); + if (host.runtime == nullptr) + { + return; + } + + auto shared = std::shared_ptr(std::move(task)); + host.runtime->Dispatch([shared, isolate = m_isolate](Napi::Env) { + v8::Isolate::Scope isolate_scope{isolate}; + shared->Run(); + }); + } + + void PostDelayed(std::unique_ptr task, double delayInSeconds) + { + auto host = m_platform.GetHost(m_isolate); + if (host.runtime == nullptr || host.scheduler == nullptr) + { + return; + } + + auto shared = std::shared_ptr(std::move(task)); + auto delay = std::chrono::duration_cast(std::chrono::duration(delayInSeconds)); + if (delay.count() < 0) + { + delay = std::chrono::milliseconds{0}; + } + + std::scoped_lock lock{m_mutex}; + const auto id = host.scheduler->Schedule(delay, [runtime = host.runtime, isolate = m_isolate, shared]() { + runtime->Dispatch([shared, isolate](Napi::Env) { + v8::Isolate::Scope isolate_scope{isolate}; + shared->Run(); + }); + }); + m_pending.push_back(Pending{host.scheduler, id}); + } + + struct Pending + { + DeadlineScheduler* scheduler{}; + DeadlineScheduler::Id id{}; + }; + + DispatchingPlatform& m_platform; + v8::Isolate* m_isolate; + std::mutex m_mutex; + std::vector m_pending; + }; + + std::shared_ptr DispatchingPlatform::WrapForegroundTaskRunner(v8::Isolate* isolate) + { + std::scoped_lock lock{m_mutex}; + auto& runner = m_taskRunners[isolate]; + if (!runner) + { + runner = std::make_shared(*this, isolate); + } + return runner; + } + class Module final { public: @@ -20,7 +271,7 @@ namespace Babylon { v8::V8::InitializeICUDefaultLocation(executablePath); v8::V8::InitializeExternalStartupData(executablePath); - m_platform = v8::platform::NewDefaultPlatform(); + m_platform = std::make_unique(v8::platform::NewDefaultPlatform()); v8::V8::InitializePlatform(m_platform.get()); v8::V8::Initialize(); } @@ -49,13 +300,13 @@ namespace Babylon return *s_module; } - v8::Platform& Platform() + DispatchingPlatform& Platform() { return *m_platform; } private: - std::unique_ptr m_platform; + std::unique_ptr m_platform; static std::unique_ptr s_module; }; @@ -67,11 +318,14 @@ namespace Babylon { // Create the isolate. Module::Initialize(executablePath); + Module::Instance().Platform().SetCurrentHost(this, &GetDeadlineScheduler()); v8::Isolate::CreateParams create_params; create_params.array_buffer_allocator = v8::ArrayBuffer::Allocator::NewDefaultAllocator(); v8::Isolate* isolate = v8::Isolate::New(create_params); + Module::Instance().Platform().SetHost(isolate, this, &GetDeadlineScheduler()); + // Use the isolate within a scope. { v8::Isolate::Scope isolate_scope{isolate}; @@ -108,6 +362,9 @@ namespace Babylon } // Destroy the isolate. + Module::Instance().Platform().SetHost(isolate, nullptr, nullptr); + Module::Instance().Platform().SetCurrentHost(nullptr, nullptr); + // todo : GetArrayBufferAllocator not available? // delete isolate->GetArrayBufferAllocator(); isolate->Dispose(); @@ -115,7 +372,9 @@ namespace Babylon void AppRuntime::DrainMicrotasks(Napi::Env) { - // V8 auto-drains microtasks at the end of each script/callback when - // using the default MicrotasksPolicy. No explicit pump needed. + // V8 auto-drains microtasks at the end of each script/callback when using the default + // MicrotasksPolicy. Foreground platform tasks (including async WebAssembly settlement) + // are posted through DispatchingTaskRunner onto AppRuntime's dispatcher, so they do + // not need a PumpMessageLoop here. } } diff --git a/Core/JsRuntime/CMakeLists.txt b/Core/JsRuntime/CMakeLists.txt index a5f12428..cf1048e3 100644 --- a/Core/JsRuntime/CMakeLists.txt +++ b/Core/JsRuntime/CMakeLists.txt @@ -1,6 +1,8 @@ set(SOURCES + "Include/Babylon/DeadlineScheduler.h" "Include/Babylon/JsRuntime.h" "Include/Babylon/JsRuntimeScheduler.h" + "Source/DeadlineScheduler.cpp" "Source/JsRuntime.cpp") add_library(JsRuntime ${SOURCES}) diff --git a/Core/JsRuntime/Include/Babylon/DeadlineScheduler.h b/Core/JsRuntime/Include/Babylon/DeadlineScheduler.h new file mode 100644 index 00000000..46c88557 --- /dev/null +++ b/Core/JsRuntime/Include/Babylon/DeadlineScheduler.h @@ -0,0 +1,34 @@ +#pragma once + +#include +#include +#include +#include + +namespace Babylon +{ + /// Native deadline queue used by setTimeout/setInterval and by the V8 + /// foreground task runner's delayed posts. Callbacks run on the scheduler + /// thread; callers that need the JavaScript thread Dispatch themselves. + class DeadlineScheduler final + { + public: + using Id = int32_t; + using Callback = std::function; + using TimePoint = std::chrono::time_point; + + DeadlineScheduler(); + ~DeadlineScheduler(); + + DeadlineScheduler(const DeadlineScheduler&) = delete; + DeadlineScheduler& operator=(const DeadlineScheduler&) = delete; + + Id Schedule(TimePoint when, Callback callback); + Id Schedule(std::chrono::milliseconds delay, Callback callback); + void Cancel(Id id); + + private: + class Impl; + std::unique_ptr m_impl; + }; +} diff --git a/Core/JsRuntime/Include/Babylon/JsRuntime.h b/Core/JsRuntime/Include/Babylon/JsRuntime.h index 75a7f4d9..51e6f1b7 100644 --- a/Core/JsRuntime/Include/Babylon/JsRuntime.h +++ b/Core/JsRuntime/Include/Babylon/JsRuntime.h @@ -4,10 +4,13 @@ #include #include +#include #include namespace Babylon { + class DeadlineScheduler; + class JsRuntime { public: @@ -37,17 +40,22 @@ namespace Babylon // later -- an instance of an inheriting type, for example. The dispatch function // must be safely callable as soon as it is passed to the JsRuntime constructor. static JsRuntime& BABYLON_API CreateForJavaScript(Napi::Env, DispatchFunctionT); + static JsRuntime& BABYLON_API CreateForJavaScript(Napi::Env, DispatchFunctionT, DeadlineScheduler&); static JsRuntime& BABYLON_API GetFromJavaScript(Napi::Env); void Dispatch(std::function); + DeadlineScheduler& GetDeadlineScheduler(); + ~JsRuntime(); protected: JsRuntime(const JsRuntime&) = delete; JsRuntime& operator=(const JsRuntime&) = delete; private: - JsRuntime(Napi::Env, DispatchFunctionT); + JsRuntime(Napi::Env, DispatchFunctionT, DeadlineScheduler*); DispatchFunctionT m_dispatchFunction{}; std::mutex m_mutex{}; + std::unique_ptr m_ownedDeadlineScheduler{}; + DeadlineScheduler* m_deadlineScheduler{}; }; } diff --git a/Core/JsRuntime/Source/DeadlineScheduler.cpp b/Core/JsRuntime/Source/DeadlineScheduler.cpp new file mode 100644 index 00000000..f3157391 --- /dev/null +++ b/Core/JsRuntime/Source/DeadlineScheduler.cpp @@ -0,0 +1,205 @@ +#include "DeadlineScheduler.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Babylon +{ + namespace + { + DeadlineScheduler::TimePoint Now() + { + return std::chrono::time_point_cast(std::chrono::steady_clock::now()); + } + } + + class DeadlineScheduler::Impl + { + public: + Impl() + : m_thread{&Impl::ThreadFunction, this} + { + } + + ~Impl() + { + { + std::unique_lock lk{m_mutex}; + m_shutdown = true; + m_idMap.clear(); + m_timeMap.clear(); + } + + m_condVariable.notify_one(); + m_thread.join(); + } + + Id Schedule(TimePoint when, Callback callback) + { + std::unique_lock lk{m_mutex}; + if (m_shutdown) + { + throw std::runtime_error{"DeadlineScheduler: Schedule after shutdown"}; + } + + const auto earliestTime = m_timeMap.empty() ? TimePoint::max() : m_timeMap.begin()->first; + const auto id = NextId(); + auto item = std::make_unique(id, when, std::move(callback)); + Item* const raw = item.get(); + const auto [it, inserted] = m_idMap.try_emplace(id, std::move(item)); + if (!inserted) + { + throw std::logic_error{"DeadlineScheduler: NextId returned a duplicate id"}; + } + m_timeMap.insert({when, raw}); + + if (when <= earliestTime) + { + m_condVariable.notify_one(); + } + + return id; + } + + void Cancel(Id id) + { + std::unique_lock lk{m_mutex}; + const auto it = m_idMap.find(id); + if (it == m_idMap.end()) + { + return; + } + + const auto timeRange = m_timeMap.equal_range(it->second->time); + for (auto itTime = timeRange.first; itTime != timeRange.second; ++itTime) + { + if (itTime->second->id == id) + { + m_timeMap.erase(itTime); + break; + } + } + + m_idMap.erase(it); + } + + private: + struct Item + { + Id id; + TimePoint time; + Callback callback; + + Item(Id id, TimePoint time, Callback callback) + : id{id} + , time{time} + , callback{std::move(callback)} + { + } + }; + + Id NextId() + { + while (true) + { + ++m_lastId; + if (m_lastId <= 0) + { + m_lastId = 1; + } + + if (m_idMap.find(m_lastId) == m_idMap.end()) + { + return m_lastId; + } + } + } + + void ThreadFunction() + { + while (!m_shutdown) + { + std::unique_lock lk{m_mutex}; + while (!m_shutdown && m_timeMap.empty()) + { + m_condVariable.wait(lk); + } + + if (m_shutdown) + { + return; + } + + const auto nextTimePoint = m_timeMap.begin()->first; + if (nextTimePoint > Now()) + { + m_condVariable.wait_until(lk, nextTimePoint); + if (m_shutdown) + { + return; + } + } + + std::vector due; + while (!m_timeMap.empty() && m_timeMap.begin()->first <= Now()) + { + Item* const item = m_timeMap.begin()->second; + due.push_back(std::move(item->callback)); + m_idMap.erase(item->id); + m_timeMap.erase(m_timeMap.begin()); + } + + lk.unlock(); + for (auto& callback : due) + { + if (callback) + { + callback(); + } + } + } + } + + std::mutex m_mutex{}; + std::condition_variable m_condVariable{}; + Id m_lastId{0}; + std::unordered_map> m_idMap; + std::multimap m_timeMap; + std::atomic m_shutdown{false}; + std::thread m_thread; + }; + + DeadlineScheduler::DeadlineScheduler() + : m_impl{std::make_unique()} + { + } + + DeadlineScheduler::~DeadlineScheduler() = default; + + DeadlineScheduler::Id DeadlineScheduler::Schedule(TimePoint when, Callback callback) + { + return m_impl->Schedule(when, std::move(callback)); + } + + DeadlineScheduler::Id DeadlineScheduler::Schedule(std::chrono::milliseconds delay, Callback callback) + { + if (delay.count() < 0) + { + delay = std::chrono::milliseconds{0}; + } + + return Schedule(Now() + delay, std::move(callback)); + } + + void DeadlineScheduler::Cancel(Id id) + { + m_impl->Cancel(id); + } +} diff --git a/Core/JsRuntime/Source/JsRuntime.cpp b/Core/JsRuntime/Source/JsRuntime.cpp index 27db82b0..44a6da7f 100644 --- a/Core/JsRuntime/Source/JsRuntime.cpp +++ b/Core/JsRuntime/Source/JsRuntime.cpp @@ -1,6 +1,9 @@ #include "JsRuntime.h" +#include "Babylon/DeadlineScheduler.h" #include "Babylon/DebugTrace.h" +#include + namespace Babylon { namespace @@ -9,9 +12,19 @@ namespace Babylon static constexpr auto JS_WINDOW_NAME = "window"; } - JsRuntime::JsRuntime(Napi::Env env, DispatchFunctionT dispatchFunction) + JsRuntime::JsRuntime(Napi::Env env, DispatchFunctionT dispatchFunction, DeadlineScheduler* deadlineScheduler) : m_dispatchFunction{std::move(dispatchFunction)} { + if (deadlineScheduler != nullptr) + { + m_deadlineScheduler = deadlineScheduler; + } + else + { + m_ownedDeadlineScheduler = std::make_unique(); + m_deadlineScheduler = m_ownedDeadlineScheduler.get(); + } + auto global = env.Global(); if (global.Get(JS_WINDOW_NAME).IsUndefined()) @@ -28,12 +41,30 @@ namespace Babylon DEBUG_TRACE("JsRuntime created"); } + JsRuntime::~JsRuntime() = default; + JsRuntime& BABYLON_API JsRuntime::CreateForJavaScript(Napi::Env env, DispatchFunctionT dispatchFunction) { - auto* runtime = new JsRuntime(env, std::move(dispatchFunction)); + auto* runtime = new JsRuntime(env, std::move(dispatchFunction), nullptr); return *runtime; } + JsRuntime& BABYLON_API JsRuntime::CreateForJavaScript(Napi::Env env, DispatchFunctionT dispatchFunction, DeadlineScheduler& deadlineScheduler) + { + auto* runtime = new JsRuntime(env, std::move(dispatchFunction), &deadlineScheduler); + return *runtime; + } + + DeadlineScheduler& JsRuntime::GetDeadlineScheduler() + { + if (m_deadlineScheduler == nullptr) + { + throw std::runtime_error{"JsRuntime deadline scheduler is not available"}; + } + + return *m_deadlineScheduler; + } + JsRuntime& BABYLON_API JsRuntime::GetFromJavaScript(Napi::Env env) { return *NativeObject::GetFromJavaScript(env) diff --git a/Polyfills/Scheduling/Source/TimeoutDispatcher.cpp b/Polyfills/Scheduling/Source/TimeoutDispatcher.cpp index 1dea6585..c16b9caf 100644 --- a/Polyfills/Scheduling/Source/TimeoutDispatcher.cpp +++ b/Polyfills/Scheduling/Source/TimeoutDispatcher.cpp @@ -1,15 +1,14 @@ #include "TimeoutDispatcher.h" -#include #include +#include +#include namespace Babylon::Polyfills::Internal { namespace { - using TimePoint = std::chrono::time_point; - - TimePoint Now() + DeadlineScheduler::TimePoint Now() { return std::chrono::time_point_cast(std::chrono::steady_clock::now()); } @@ -18,17 +17,11 @@ namespace Babylon::Polyfills::Internal struct TimeoutDispatcher::Timeout { TimeoutId id; - - // Distinguishes this timeout from a later one that happens to reuse the - // same id, so an in-flight callback can never re-arm its replacement. uint64_t sequence; - - // Make this non-shared when JsRuntime::Dispatch supports it. std::shared_ptr function; - TimePoint time; - std::optional interval; + DeadlineScheduler::Id scheduleId{}; Timeout(TimeoutId id, uint64_t sequence, std::shared_ptr function, TimePoint time, std::optional interval) : id{id} @@ -45,29 +38,21 @@ namespace Babylon::Polyfills::Internal TimeoutDispatcher::TimeoutDispatcher(Babylon::JsRuntime& runtime) : m_runtime{runtime} - , m_thread{std::thread{&TimeoutDispatcher::ThreadFunction, this}} + , m_scheduler{runtime.GetDeadlineScheduler()} { } TimeoutDispatcher::~TimeoutDispatcher() { + std::unique_lock lk{m_mutex}; + for (auto& [id, timeout] : m_idMap) { - std::unique_lock lk{m_mutex}; - m_idMap.clear(); - m_timeMap.clear(); + m_scheduler.Cancel(timeout->scheduleId); } - - m_shutdown = true; - m_condVariable.notify_one(); - m_thread.join(); + m_idMap.clear(); } TimeoutDispatcher::TimeoutId TimeoutDispatcher::Dispatch(std::shared_ptr function, std::chrono::milliseconds delay, bool repeat) - { - return DispatchImpl(function, delay, repeat, 0); - } - - TimeoutDispatcher::TimeoutId TimeoutDispatcher::DispatchImpl(std::shared_ptr function, std::chrono::milliseconds delay, bool repeat, TimeoutId id) { if (delay.count() < 0) { @@ -76,20 +61,20 @@ namespace Babylon::Polyfills::Internal std::unique_lock lk{m_mutex}; - if (id == 0) - { - id = NextTimeoutId(); - } - const auto earliestTime = m_timeMap.empty() ? TimePoint::max() : m_timeMap.cbegin()->second->time; + const auto id = NextTimeoutId(); + const auto sequence = ++m_lastSequence; const auto time = Now() + delay; - const auto result = m_idMap.insert({id, std::make_unique(id, ++m_lastSequence, std::move(function), time, repeat ? std::make_optional(delay) : std::nullopt)}); - m_timeMap.insert({time, result.first->second.get()}); - - if (time <= earliestTime) + auto timeout = std::make_unique(id, sequence, std::move(function), time, repeat ? std::make_optional(delay) : std::nullopt); + const auto [it, inserted] = m_idMap.try_emplace(id, std::move(timeout)); + if (!inserted) { - m_condVariable.notify_one(); + throw std::logic_error{"TimeoutDispatcher: NextTimeoutId returned a duplicate id"}; } + it->second->scheduleId = m_scheduler.Schedule(time, [this, id, sequence]() { + CallFunction(id, sequence); + }); + return id; } @@ -99,19 +84,7 @@ namespace Babylon::Polyfills::Internal const auto itId = m_idMap.find(id); if (itId != m_idMap.end()) { - const auto& timeout = itId->second; - const auto timeRange = m_timeMap.equal_range(timeout->time); - - // Remove any pending entries that have not yet been dispatched. - for (auto itTime = timeRange.first; itTime != timeRange.second; itTime++) - { - if (itTime->second->id == id) - { - m_timeMap.erase(itTime); - break; - } - } - + m_scheduler.Cancel(itId->second->scheduleId); m_idMap.erase(itId); } } @@ -134,48 +107,6 @@ namespace Babylon::Polyfills::Internal } } - void TimeoutDispatcher::ThreadFunction() - { - while (!m_shutdown) - { - std::unique_lock lk{m_mutex}; - TimePoint nextTimePoint{}; - - while (!m_timeMap.empty()) - { - nextTimePoint = m_timeMap.begin()->second->time; - if (nextTimePoint <= Now()) - { - break; - } - - m_condVariable.wait_until(lk, nextTimePoint); - } - - while (!m_timeMap.empty() && m_timeMap.begin()->second->time == nextTimePoint) - { - const auto id = m_timeMap.begin()->second->id; - const auto sequence = m_timeMap.begin()->second->sequence; - m_timeMap.erase(m_timeMap.begin()); - - // Repeating timeouts are deliberately NOT re-armed here. They are - // re-armed on the JS thread once the callback has actually run, so - // that at most one invocation of a given interval is ever queued. - // Re-arming here instead would let this thread -- which never waits - // while a due timeout exists -- spin and enqueue callbacks far - // faster than the JS thread can drain them. The resulting unbounded - // backlog starves every other item on the JS dispatch queue: other - // timers, and native async completions such as shader compilation. - CallFunction(id, sequence); - } - - while (!m_shutdown && m_timeMap.empty()) - { - m_condVariable.wait(lk); - } - } - } - void TimeoutDispatcher::CallFunction(TimeoutId id, uint64_t sequence) { m_runtime.Dispatch([id, sequence, this](Napi::Env) { @@ -260,15 +191,9 @@ namespace Babylon::Polyfills::Internal nextTime = now; } - const auto earliestTime = m_timeMap.empty() ? TimePoint::max() : m_timeMap.cbegin()->second->time; it->second->time = nextTime; - m_timeMap.insert({nextTime, it->second.get()}); - - if (nextTime <= earliestTime) - { - // The timer thread parks while m_timeMap is empty, which is the case - // whenever this timeout was the only one pending. - m_condVariable.notify_one(); - } + it->second->scheduleId = m_scheduler.Schedule(nextTime, [this, id, sequence]() { + CallFunction(id, sequence); + }); } -} +} \ No newline at end of file diff --git a/Polyfills/Scheduling/Source/TimeoutDispatcher.h b/Polyfills/Scheduling/Source/TimeoutDispatcher.h index 0ab4b135..a793f478 100644 --- a/Polyfills/Scheduling/Source/TimeoutDispatcher.h +++ b/Polyfills/Scheduling/Source/TimeoutDispatcher.h @@ -1,15 +1,14 @@ #pragma once +#include #include #include -#include #include -#include -#include -#include #include -#include +#include +#include +#include namespace Babylon::Polyfills::Internal { @@ -26,23 +25,17 @@ namespace Babylon::Polyfills::Internal void Clear(TimeoutId id); private: - using TimePoint = std::chrono::time_point; - - TimeoutId DispatchImpl(std::shared_ptr function, std::chrono::milliseconds delay, bool repeat, TimeoutId id); + using TimePoint = DeadlineScheduler::TimePoint; TimeoutId NextTimeoutId(); - void ThreadFunction(); void CallFunction(TimeoutId id, uint64_t sequence); void Rearm(TimeoutId id, uint64_t sequence, TimePoint scheduledTime, std::chrono::milliseconds interval); Babylon::JsRuntime& m_runtime; + DeadlineScheduler& m_scheduler; std::recursive_mutex m_mutex{}; - std::condition_variable_any m_condVariable{}; TimeoutId m_lastTimeoutId{0}; uint64_t m_lastSequence{0}; std::unordered_map> m_idMap; - std::multimap m_timeMap; - std::atomic m_shutdown{false}; - std::thread m_thread; }; } diff --git a/Tests/UnitTests/Android/app/src/main/cpp/CMakeLists.txt b/Tests/UnitTests/Android/app/src/main/cpp/CMakeLists.txt index 5f7c83a0..84250e46 100644 --- a/Tests/UnitTests/Android/app/src/main/cpp/CMakeLists.txt +++ b/Tests/UnitTests/Android/app/src/main/cpp/CMakeLists.txt @@ -23,6 +23,7 @@ add_library(UnitTestsJNI SHARED ${UNIT_TESTS_DIR}/Shared/Shared.cpp) target_compile_definitions(UnitTestsJNI PRIVATE JSRUNTIMEHOST_PLATFORM="${JSRUNTIMEHOST_PLATFORM}") +target_compile_definitions(UnitTestsJNI PRIVATE NAPI_JAVASCRIPT_ENGINE="${NAPI_JAVASCRIPT_ENGINE}") target_compile_definitions(UnitTestsJNI PRIVATE ARCANA_TEST_HOOKS) target_include_directories(UnitTestsJNI diff --git a/Tests/UnitTests/CMakeLists.txt b/Tests/UnitTests/CMakeLists.txt index b8446eb2..b4917824 100644 --- a/Tests/UnitTests/CMakeLists.txt +++ b/Tests/UnitTests/CMakeLists.txt @@ -46,6 +46,7 @@ endif() add_executable(UnitTests ${SOURCES} ${SCRIPTS} ${TYPE_SCRIPTS} ${ASSETS}) target_compile_definitions(UnitTests PRIVATE JSRUNTIMEHOST_PLATFORM="${JSRUNTIMEHOST_PLATFORM}") +target_compile_definitions(UnitTests PRIVATE NAPI_JAVASCRIPT_ENGINE="${NAPI_JAVASCRIPT_ENGINE}") # The V8JSI Node-API shim does not implement napi_create_dataview, so the # CreateDataViewRejectsOverflowingRange test is compiled out on that backend. diff --git a/Tests/UnitTests/Scripts/tests.ts b/Tests/UnitTests/Scripts/tests.ts index bb484ba7..654c60b6 100644 --- a/Tests/UnitTests/Scripts/tests.ts +++ b/Tests/UnitTests/Scripts/tests.ts @@ -6,6 +6,7 @@ Mocha.setup('bdd'); Mocha.reporter('spec'); declare const hostPlatform: string; +declare const hostEngine: string; declare const setExitCode: (code: number) => void; @@ -2234,6 +2235,42 @@ describe("FileReader", function () { }); }); +describe("WebAssembly", function () { + this.timeout(30000); + + // Only the V8 AppRuntime pumps V8's foreground task queue, which is what lets these promises + // settle. The other engines' runtimes have the same class of gap and hang here instead of + // failing, so scope the suite rather than leave a 30s timeout on every non-V8 leg. + beforeEach(function () { + if (hostEngine !== "V8" || typeof WebAssembly === "undefined") { + this.skip(); + } + }); + + // Minimal valid module: the 8-byte header (magic + version) and no sections. + const emptyModule = new Uint8Array([0x00, 0x61, 0x73, 0x6D, 0x01, 0x00, 0x00, 0x00]); + + it("should settle the promise returned by WebAssembly.compile", async function () { + const module = await WebAssembly.compile(emptyModule); + expect(module).to.be.an.instanceof(WebAssembly.Module); + }); + + it("should settle the promise returned by WebAssembly.instantiate", async function () { + const result = await WebAssembly.instantiate(emptyModule); + expect(result.instance).to.be.an.instanceof(WebAssembly.Instance); + }); + + it("should reject the promise returned by WebAssembly.compile for invalid bytes", async function () { + let threw = false; + try { + await WebAssembly.compile(new Uint8Array([0x00, 0x61, 0x73, 0x6D, 0xFF])); + } catch (e) { + threw = true; + } + expect(threw).to.equal(true); + }); +}); + function runTests() { mocha.run((failures: number) => { // Test program will wait for code to be set before exiting diff --git a/Tests/UnitTests/Shared/Shared.cpp b/Tests/UnitTests/Shared/Shared.cpp index 0267ce37..1c7e9ff7 100644 --- a/Tests/UnitTests/Shared/Shared.cpp +++ b/Tests/UnitTests/Shared/Shared.cpp @@ -100,6 +100,7 @@ TEST(JavaScript, All) env.Global().Set("setExitCode", setExitCodeCallback); env.Global().Set("hostPlatform", Napi::Value::From(env, JSRUNTIMEHOST_PLATFORM)); + env.Global().Set("hostEngine", Napi::Value::From(env, NAPI_JAVASCRIPT_ENGINE)); }); Babylon::ScriptLoader loader{runtime};