Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@ namespace Babylon
// queue explicitly (Napi::DrainJobs / JS_ExecutePendingJob).
void DrainMicrotasks(Napi::Env env);

// Engine-specific maintenance that should run once after a complete
// dispatcher turn, rather than after each callback in that turn.
void DrainPostDispatchWork(Napi::Env env);

Internal::DelayedTaskScheduler& GetDelayedTaskScheduler();

Options m_options;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,10 @@ namespace Babylon

while (!m_impl->m_cancelSource.cancelled())
{
m_impl->m_dispatcher.blocking_tick(m_impl->m_cancelSource);
if (m_impl->m_dispatcher.blocking_tick(m_impl->m_cancelSource))
{
DrainPostDispatchWork(env);
}
}

Napi::HandleScope scope{env};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,4 +82,8 @@ namespace Babylon
// JsSetPromiseContinuationCallback hook (see RunEnvironmentTier).
// No explicit pump needed here.
}

void AppRuntime::DrainPostDispatchWork(Napi::Env)
{
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,8 @@ namespace Babylon
// observes the same "between turns" semantics it gets on V8/Chakra.
Napi::DrainJobs(env);
}

void AppRuntime::DrainPostDispatchWork(Napi::Env)
{
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,8 @@ namespace Babylon
{
// JSI/V8 backed JSI auto-drains microtasks per scope.
}

void AppRuntime::DrainPostDispatchWork(Napi::Env)
{
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,8 @@ namespace Babylon
{
// JavaScriptCore drains microtasks automatically at script boundaries.
}

void AppRuntime::DrainPostDispatchWork(Napi::Env)
{
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,4 +68,8 @@ namespace Babylon
{
}
}

void AppRuntime::DrainPostDispatchWork(Napi::Env)
{
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -126,4 +126,11 @@ namespace Babylon
{
// V8 auto-drains microtasks. Foreground tasks run on AppRuntime's dispatcher.
}

void AppRuntime::DrainPostDispatchWork(Napi::Env env)
{
// N-API finalizers deferred by a V8 garbage collection require a safe
// host boundary, but should not monopolize a dispatcher turn.
Napi::DrainFinalizers(env);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ namespace Napi

void Detach(Napi::Env);

void DrainFinalizers(Napi::Env);

Napi::Value Eval(Napi::Env env, const char* source, const char* sourceUrl);

v8::Local<v8::Context> GetContext(Napi::Env);
Expand Down
27 changes: 27 additions & 0 deletions Dependencies/JsRuntimeHost/Core/Node-API/Source/env_v8.cc
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,15 @@
#include <napi/js_native_api_types.h>
#include "js_native_api_v8.h"

#include <chrono>

namespace Napi
{
namespace
{
constexpr auto FinalizerDrainBudget{std::chrono::milliseconds{8}};
}

Env Attach(v8::Local<v8::Context> isolate)
{
// second argument is module version
Expand All @@ -16,6 +23,26 @@ namespace Napi
env_ptr->DeleteMe();
}

void DrainFinalizers(Env env)
{
napi_env env_ptr{env};
const auto start = std::chrono::steady_clock::now();

// Finalizers can mutate this queue. Bound each dispatcher turn so a large
// batch cannot monopolize the runtime thread.
while (!env_ptr->pending_finalizers.empty())
{
auto* finalizer = *env_ptr->pending_finalizers.begin();
env_ptr->pending_finalizers.erase(finalizer);
finalizer->Finalize();

if (std::chrono::steady_clock::now() - start >= FinalizerDrainBudget)
{
break;
}
}
}

v8::Local<v8::Context> GetContext(Env env)
{
napi_env env_ptr{env};
Expand Down
3 changes: 3 additions & 0 deletions Dependencies/JsRuntimeHost/Tests/UnitTests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,9 @@ target_compile_definitions(${_jsruntimehost_unit_tests_target} PRIVATE NAPI_JAVA
if(NAPI_JAVASCRIPT_ENGINE STREQUAL "JSI")
target_compile_definitions(${_jsruntimehost_unit_tests_target} PRIVATE JSRUNTIMEHOST_NAPI_ENGINE_JSI)
endif()
if(NAPI_JAVASCRIPT_ENGINE STREQUAL "V8")
target_compile_definitions(${_jsruntimehost_unit_tests_target} PRIVATE JSRUNTIMEHOST_NAPI_ENGINE_V8)
endif()

target_link_libraries(${_jsruntimehost_unit_tests_target}
PRIVATE AppRuntime
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ target_compile_definitions(UnitTestsJNI PRIVATE ARCANA_TEST_HOOKS)
if(NAPI_JAVASCRIPT_ENGINE STREQUAL "JSI")
target_compile_definitions(UnitTestsJNI PRIVATE JSRUNTIMEHOST_NAPI_ENGINE_JSI)
endif()
if(NAPI_JAVASCRIPT_ENGINE STREQUAL "V8")
target_compile_definitions(UnitTestsJNI PRIVATE JSRUNTIMEHOST_NAPI_ENGINE_V8)
endif()

target_include_directories(UnitTestsJNI
PRIVATE ${UNIT_TESTS_DIR})
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
#include <Babylon/AppRuntime.h>
#include <gtest/gtest.h>
#include <arcana/threading/blocking_concurrent_queue.h>
#include <atomic>
#include <chrono>
#include <future>
#include <memory>
#include <thread>

#if defined(JSRUNTIMEHOST_NAPI_ENGINE_V8)
#include <napi/env.h>
#endif

TEST(AppRuntime, DestroyDoesNotDeadlock)
{
// Regression test verifying AppRuntime destruction doesn't deadlock.
Expand Down Expand Up @@ -102,3 +107,90 @@ TEST(AppRuntime, DestroyDoesNotDeadlock)

testThread.join();
}

#if defined(JSRUNTIMEHOST_NAPI_ENGINE_V8)
TEST(AppRuntime, V8FinalizersDrainAfterDispatch)
{
constexpr size_t ExternalCount{32};
std::atomic<size_t> finalized{};
std::promise<void> created;
std::promise<void> collectionRequested;
std::promise<size_t> observed;

Babylon::AppRuntime runtime{};
runtime.Dispatch([&finalized, &created](Napi::Env env) {
for (size_t index{}; index < ExternalCount; ++index)
{
Napi::External<std::atomic<size_t>>::New(
env,
&finalized,
[](Napi::Env, std::atomic<size_t>* count) {
count->fetch_add(1, std::memory_order_relaxed);
});
}
created.set_value();
});
created.get_future().wait();

runtime.Dispatch([&collectionRequested](Napi::Env env) {
Napi::GetContext(env)->GetIsolate()->LowMemoryNotification();
collectionRequested.set_value();
});
collectionRequested.get_future().wait();

runtime.Dispatch([&finalized, &observed](Napi::Env) {
observed.set_value(finalized.load(std::memory_order_relaxed));
});

EXPECT_EQ(observed.get_future().get(), ExternalCount);
}

TEST(AppRuntime, V8FinalizerDrainYieldsBetweenDispatcherTurns)
{
constexpr size_t ExternalCount{16};
std::atomic<size_t> finalized{};
std::promise<void> created;
std::promise<void> collectionRequested;

Babylon::AppRuntime runtime{};
runtime.Dispatch([&](Napi::Env env) {
for (size_t index{}; index < ExternalCount; ++index)
{
Napi::External<std::atomic<size_t>>::New(
env,
&finalized,
[](Napi::Env, std::atomic<size_t>* count) {
std::this_thread::sleep_for(std::chrono::milliseconds{2});
count->fetch_add(1, std::memory_order_relaxed);
});
}
created.set_value();
});
created.get_future().wait();

runtime.Dispatch([&](Napi::Env env) {
Napi::GetContext(env)->GetIsolate()->LowMemoryNotification();
collectionRequested.set_value();
});
collectionRequested.get_future().wait();

const auto observeFinalized = [&]() {
std::promise<size_t> observed;
auto future = observed.get_future();
runtime.Dispatch([&](Napi::Env) {
observed.set_value(finalized.load(std::memory_order_relaxed));
});
return future.get();
};

auto observed = observeFinalized();
EXPECT_GT(observed, 0u);
EXPECT_LT(observed, ExternalCount);

for (size_t turn{}; turn < ExternalCount && observed < ExternalCount; ++turn)
{
observed = observeFinalized();
}
EXPECT_EQ(observed, ExternalCount);
}
#endif
Loading