Commit d97041fff91 for nodejs

commit d97041fff91a4574660d07adc3d0a6cf289a3991
Author: Nigro Simone <nigro.simone@gmail.com>
Date:   Sat Sep 26 14:59:55 2026 +0200

    src: reduce InternalCallbackScope overhead

    InternalCallbackScope looks up the Environment from the isolate two
    times per call, inside async_context_frame::exchange, and it keeps the
    prior async context frame in a v8::Global also when there is no frame,
    that is the common case. Every call from native code into JS pays this:
    MakeCallback, CallbackScope, AsyncWrap, Node-API.

    Now the scope passes the Environment it already has, the option is read
    with an inline accessor instead of copying the shared_ptr, and the
    global handle is created only when the prior frame is not undefined.

    benchmark/napi/make_callback, Node 26.3.0 built with and without this
    change, Linux x64, 30 runs: from 202-208 ns to 155-159 ns per call.

    Refs: https://github.com/nodejs/performance/issues/24
    Signed-off-by: Nigro Simone <nigro.simone@gmail.com>
    PR-URL: https://github.com/nodejs/node/pull/66316
    Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
    Reviewed-By: Stephen Belanger <admin@stephenbelanger.com>
    Reviewed-By: James M Snell <jasnell@gmail.com>

diff --git a/src/api/callback.cc b/src/api/callback.cc
index c3850fa4afe..217aebae9e1 100644
--- a/src/api/callback.cc
+++ b/src/api/callback.cc
@@ -112,8 +112,12 @@ InternalCallbackScope::InternalCallbackScope(

   isolate->SetIdle(false);

-  prior_context_frame_.Reset(
-      isolate, async_context_frame::exchange(isolate, context_frame));
+  // The prior frame is usually undefined: no global handle then.
+  Local<Value> prior_context_frame =
+      async_context_frame::exchange(env, context_frame);
+  if (!prior_context_frame->IsUndefined()) {
+    prior_context_frame_.Reset(isolate, prior_context_frame);
+  }

   env->async_hooks()->push_async_context(
       async_context_.async_id, async_context_.trigger_async_id, object);
@@ -159,7 +163,7 @@ void InternalCallbackScope::Close() {
   if (pushed_ids_) {
     env_->async_hooks()->pop_async_context(async_context_.async_id);

-    async_context_frame::exchange(isolate, prior_context_frame_.Get(isolate));
+    async_context_frame::set(env_, prior_context_frame_.Get(isolate));
   }

   if (failed_) return;
diff --git a/src/async_context_frame.cc b/src/async_context_frame.cc
index 8e5fb3dff95..2e637904573 100644
--- a/src/async_context_frame.cc
+++ b/src/async_context_frame.cc
@@ -37,23 +37,30 @@ Local<Value> current(Isolate* isolate) {
   return isolate->GetContinuationPreservedEmbedderDataV2().As<Value>();
 }

-void set(Isolate* isolate, Local<Value> value) {
-  auto env = Environment::GetCurrent(isolate);
-  if (!env->options()->async_context_frame) {
+void set(Environment* env, Local<Value> value) {
+  if (!env->async_context_frame_enabled()) {
     return;
   }

-  isolate->SetContinuationPreservedEmbedderDataV2(value);
+  env->isolate()->SetContinuationPreservedEmbedderDataV2(value);
+}
+
+void set(Isolate* isolate, Local<Value> value) {
+  set(Environment::GetCurrent(isolate), value);
 }

 // NOTE: It's generally recommended to use async_context_frame::Scope
 // but sometimes (such as enterWith) a direct exchange is needed.
-Local<Value> exchange(Isolate* isolate, Local<Value> value) {
-  auto prior = current(isolate);
-  set(isolate, value);
+Local<Value> exchange(Environment* env, Local<Value> value) {
+  auto prior = current(env->isolate());
+  set(env, value);
   return prior;
 }

+Local<Value> exchange(Isolate* isolate, Local<Value> value) {
+  return exchange(Environment::GetCurrent(isolate), value);
+}
+
 void CreatePerContextProperties(Local<Object> target,
                                 Local<Value> unused,
                                 Local<Context> context,
diff --git a/src/async_context_frame.h b/src/async_context_frame.h
index 389f740d643..8ec59ebbd96 100644
--- a/src/async_context_frame.h
+++ b/src/async_context_frame.h
@@ -23,7 +23,9 @@ class Scope {

 v8::Local<v8::Value> current(v8::Isolate* isolate);
 void set(v8::Isolate* isolate, v8::Local<v8::Value> value);
+void set(Environment* env, v8::Local<v8::Value> value);
 v8::Local<v8::Value> exchange(v8::Isolate* isolate, v8::Local<v8::Value> value);
+v8::Local<v8::Value> exchange(Environment* env, v8::Local<v8::Value> value);

 }  // namespace async_context_frame
 }  // namespace node
diff --git a/src/env-inl.h b/src/env-inl.h
index c2abf1c29a8..12ffdb48a0b 100644
--- a/src/env-inl.h
+++ b/src/env-inl.h
@@ -460,6 +460,10 @@ inline std::shared_ptr<EnvironmentOptions> Environment::options() {
   return options_;
 }

+inline bool Environment::async_context_frame_enabled() const {
+  return options_->async_context_frame;
+}
+
 inline const std::vector<std::string>& Environment::argv() {
   return argv_;
 }
diff --git a/src/env.h b/src/env.h
index a5ce5ee0231..bbe417852e9 100644
--- a/src/env.h
+++ b/src/env.h
@@ -1113,6 +1113,7 @@ class Environment final : public MemoryRetainer {
                                  void* data);

   inline std::shared_ptr<EnvironmentOptions> options();
+  inline bool async_context_frame_enabled() const;
   inline std::shared_ptr<ExclusiveAccess<HostPort>> inspector_host_port();

   inline int64_t stack_trace_limit() const;