From 9c529072d4983b6f0b631728e38fe71fcc98ec6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=BCrg=C3=BCn=20Day=C4=B1o=C4=9Flu?= Date: Sun, 26 Apr 2026 13:55:21 +0200 Subject: [PATCH 1/2] src: fast path empty native immediate drain MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Gürgün Dayıoğlu --- src/env.cc | 9 +++++ test/cctest/test_environment.cc | 63 +++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+) diff --git a/src/env.cc b/src/env.cc index f618fca2e2d157..29d5566195fbfc 100644 --- a/src/env.cc +++ b/src/env.cc @@ -1414,6 +1414,15 @@ void Environment::RunAndClearInterrupts() { void Environment::RunAndClearNativeImmediates(bool only_refed) { TRACE_EVENT0(TRACING_CATEGORY_NODE1(environment), "RunAndClearNativeImmediates"); + // Skip the callback scope on the common empty path. These are the only + // queues drained by this function. If a threadsafe immediate is queued + // concurrently after this check, uv_async_send() schedules another drain. + if (native_immediates_.size() == 0 && + native_immediates_threadsafe_.size() == 0 && + native_immediates_interrupts_.size() == 0) { + return; + } + HandleScope handle_scope(isolate_); // In case the Isolate is no longer accessible just use an empty Local. This // is not an issue for InternalCallbackScope as this case is already handled diff --git a/test/cctest/test_environment.cc b/test/cctest/test_environment.cc index ee9052a1dad4e4..59c71835499e41 100644 --- a/test/cctest/test_environment.cc +++ b/test/cctest/test_environment.cc @@ -401,6 +401,69 @@ TEST_F(EnvironmentTest, SetImmediateCleanup) { EXPECT_EQ(called_unref, 0); } +TEST_F(EnvironmentTest, RunAndClearNativeImmediatesSkipsEmptyScope) { + const v8::HandleScope handle_scope(isolate_); + const Argv argv; + Env env{handle_scope, argv}; + v8::Local context = env.context(); + + using IntVec = std::vector; + IntVec callback_calls; + v8::Local must_call = + v8::Function::New( + context, + [](const v8::FunctionCallbackInfo& info) { + IntVec* callback_calls = + static_cast(info.Data().As()->Value( + v8::kExternalPointerTypeTagDefault)); + callback_calls->push_back(info[0].As()->Value()); + }, + v8::External::New(isolate_, + static_cast(&callback_calls), + v8::kExternalPointerTypeTagDefault)) + .ToLocalChecked(); + context->Global() + ->Set(context, + v8::String::NewFromUtf8Literal(isolate_, "mustCall"), + must_call) + .Check(); + + v8::Local eval_in_env = + node::LoadEnvironment(*env, "return eval;") + .ToLocalChecked() + .As(); + + v8::Local queue_microtask = v8::String::NewFromUtf8Literal( + isolate_, "Promise.resolve().then(() => mustCall(1));"); + eval_in_env->Call(context, v8::Null(isolate_), 1, &queue_microtask) + .ToLocalChecked(); + EXPECT_TRUE(callback_calls.empty()); + (*env)->RunAndClearNativeImmediates(); + EXPECT_TRUE(callback_calls.empty()); + + context->GetMicrotaskQueue()->PerformCheckpoint(isolate_); + EXPECT_EQ(callback_calls, (IntVec{1})); + callback_calls.clear(); + + queue_microtask = v8::String::NewFromUtf8Literal( + isolate_, "Promise.resolve().then(() => mustCall(2));"); + eval_in_env->Call(context, v8::Null(isolate_), 1, &queue_microtask) + .ToLocalChecked(); + EXPECT_TRUE(callback_calls.empty()); + + bool native_immediate_called = false; + (*env)->SetImmediate( + [&](node::Environment* env_arg) { + EXPECT_EQ(env_arg, *env); + native_immediate_called = true; + }, + node::CallbackFlags::kRefed); + + (*env)->RunAndClearNativeImmediates(); + EXPECT_TRUE(native_immediate_called); + EXPECT_EQ(callback_calls, (IntVec{2})); +} + static char hello[] = "hello"; TEST_F(EnvironmentTest, BufferWithFreeCallbackIsDetached) { From 1584afc71383c2c4d2236d760f78a5edfe30ad49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=BCrg=C3=BCn=20Day=C4=B1o=C4=9Flu?= Date: Sun, 26 Apr 2026 15:38:15 +0200 Subject: [PATCH 2/2] simplify --- src/env.cc | 16 ++++++++++------ src/env.h | 1 + 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/env.cc b/src/env.cc index 29d5566195fbfc..141991a9a5736a 100644 --- a/src/env.cc +++ b/src/env.cc @@ -1411,15 +1411,16 @@ void Environment::RunAndClearInterrupts() { } } +bool Environment::HasNativeImmediates() const { + return native_immediates_.size() > 0 || + native_immediates_threadsafe_.size() > 0 || + native_immediates_interrupts_.size() > 0; +} + void Environment::RunAndClearNativeImmediates(bool only_refed) { TRACE_EVENT0(TRACING_CATEGORY_NODE1(environment), "RunAndClearNativeImmediates"); - // Skip the callback scope on the common empty path. These are the only - // queues drained by this function. If a threadsafe immediate is queued - // concurrently after this check, uv_async_send() schedules another drain. - if (native_immediates_.size() == 0 && - native_immediates_threadsafe_.size() == 0 && - native_immediates_interrupts_.size() == 0) { + if (!HasNativeImmediates()) { return; } @@ -1595,6 +1596,9 @@ void Environment::CheckImmediate(uv_check_t* handle) { Environment* env = Environment::from_immediate_check_handle(handle); TRACE_EVENT0(TRACING_CATEGORY_NODE1(environment), "CheckImmediate"); + if (env->immediate_info()->count() == 0 && !env->HasNativeImmediates()) + return; + HandleScope scope(env->isolate()); Context::Scope context_scope(env->context()); diff --git a/src/env.h b/src/env.h index 5fabc366c6e68b..2e644983ea1d0a 100644 --- a/src/env.h +++ b/src/env.h @@ -1034,6 +1034,7 @@ class Environment final : public MemoryRetainer { void RunAndClearNativeImmediates(bool only_refed = false); void RunAndClearInterrupts(); + bool HasNativeImmediates() const; uv_buf_t allocate_managed_buffer(const size_t suggested_size); std::unique_ptr release_managed_buffer(const uv_buf_t& buf);