Files
node/src/api/callback.cc
T
Nigro Simone 5a566ac6b9 src: cut small costs in node::MakeCallback
Enter the Environment's context only when it is not already the
entered and current one, set the async context frame only when it
changes, pass the resource variant to push_async_context() by const
reference, and grow and shrink the native resource stack with
push_back() and pop_back() instead of resize().

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/66395
Reviewed-By: Xuguang Mei <meixuguang@gmail.com>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Chengzhong Wu <legendecas@gmail.com>
2026-10-20 14:24:33 +00:00

409 lines
14 KiB
C++

#include "async_context_frame.h"
#include "async_wrap-inl.h"
#include "env-inl.h"
#include "node.h"
#include "v8.h"
#include <optional>
namespace node {
using v8::Context;
using v8::Function;
using v8::Global;
using v8::HandleScope;
using v8::Isolate;
using v8::Local;
using v8::MaybeLocal;
using v8::Number;
using v8::Object;
using v8::String;
using v8::Undefined;
using v8::Value;
CallbackScope::CallbackScope(Isolate* isolate,
Local<Object> object,
async_context async_context)
: CallbackScope(Environment::GetCurrent(isolate), object, async_context) {}
CallbackScope::CallbackScope(Environment* env,
Local<Object> object,
async_context asyncContext)
: resource_storage_({.local = object}),
private_(new InternalCallbackScope(
env, &resource_storage_.local, asyncContext)),
try_catch_(env->isolate()) {
try_catch_.SetVerbose(true);
}
CallbackScope::CallbackScope(Environment* env,
Global<Object>* object,
async_context asyncContext)
: resource_storage_({.global_ptr = object}),
private_(new InternalCallbackScope(
env, resource_storage_.global_ptr, asyncContext)),
try_catch_(env->isolate()) {
try_catch_.SetVerbose(true);
// These checks can be removed in a future major version -- they ensure
// ABI compatibility with previous Node.js versions.
static_assert(sizeof(resource_storage_) == sizeof(Global<Object>*));
static_assert(sizeof(resource_storage_) == sizeof(Local<Object>));
static_assert(alignof(decltype(resource_storage_)) ==
alignof(Global<Object>*));
static_assert(alignof(decltype(resource_storage_)) == alignof(Local<Object>));
}
CallbackScope::~CallbackScope() {
if (try_catch_.HasCaught())
private_->MarkAsFailed();
delete private_;
}
InternalCallbackScope::InternalCallbackScope(AsyncWrap* async_wrap, int flags)
: InternalCallbackScope(
async_wrap->env(),
async_wrap->object(),
{async_wrap->get_async_id(), async_wrap->get_trigger_async_id()},
flags,
async_wrap->context_frame()) {}
InternalCallbackScope::InternalCallbackScope(
Environment* env,
std::variant<Local<Object>, Local<Object>*, Global<Object>*> object_arg,
const async_context& asyncContext,
int flags,
Local<Value> context_frame)
: env_(env),
async_context_(asyncContext),
skip_hooks_(flags & kSkipAsyncHooks),
skip_task_queues_(flags & kSkipTaskQueues) {
CHECK_NOT_NULL(env);
std::variant<v8::Local<v8::Object>*, v8::Global<v8::Object>*> object;
if (std::holds_alternative<Local<Object>>(object_arg)) {
object_storage_ = std::get<Local<Object>>(object_arg);
object = &object_storage_;
} else if (std::holds_alternative<Local<Object>*>(object_arg)) {
object = std::get<Local<Object>*>(object_arg);
} else {
object = std::get<Global<Object>*>(object_arg);
}
std::visit([](auto* ptr) { CHECK_NOT_NULL(ptr); }, object);
env->PushAsyncCallbackScope();
if (!env->can_call_into_js()) {
failed_ = true;
return;
}
Isolate* isolate = env->isolate();
if (handle_cleanup_depth > 0) allow_js_.emplace(isolate);
HandleScope handle_scope(isolate);
Local<Context> current_context = isolate->GetCurrentContext();
// If you hit this assertion, the caller forgot to enter the right Node.js
// Environment's v8::Context first.
// We first check `env->context() != current_context` because the contexts
// likely *are* the same, in which case we can skip the slightly more
// expensive Environment::GetCurrent() call.
if (env->context() != current_context) [[unlikely]] {
CHECK_EQ(Environment::GetCurrent(isolate), env);
}
isolate->SetIdle(false);
// 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);
pushed_ids_ = true;
if (asyncContext.async_id != 0 && !skip_hooks_) {
// No need to check a return value because the application will exit if
// an exception occurs.
AsyncWrap::EmitBefore(env, asyncContext.async_id);
}
}
InternalCallbackScope::~InternalCallbackScope() {
Close();
env_->PopAsyncCallbackScope();
}
void InternalCallbackScope::Close() {
if (closed_) return;
closed_ = true;
// This function must ends up with either cleanup the
// async id stack or pop the topmost one from it
auto perform_stopping_check = [&]() {
if (env_->is_stopping()) {
MarkAsFailed();
env_->async_hooks()->clear_async_id_stack();
}
};
perform_stopping_check();
if (env_->is_stopping()) return;
Isolate* isolate = env_->isolate();
auto idle = OnScopeLeave([&]() { isolate->SetIdle(true); });
if (!failed_ && async_context_.async_id != 0 && !skip_hooks_) {
AsyncWrap::EmitAfter(env_, async_context_.async_id);
}
if (pushed_ids_) {
env_->async_hooks()->pop_async_context(async_context_.async_id);
async_context_frame::set(env_, prior_context_frame_.Get(isolate));
}
if (failed_) return;
if (env_->async_callback_scope_depth() > 1 || skip_task_queues_) {
return;
}
TickInfo* tick_info = env_->tick_info();
if (!env_->can_call_into_js()) return;
auto weakref_cleanup = OnScopeLeave([&]() { env_->RunWeakRefCleanup(); });
Local<Context> context = env_->context();
if (!tick_info->has_tick_scheduled()) {
context->GetMicrotaskQueue()->PerformCheckpoint(isolate);
perform_stopping_check();
}
// Make sure the stack unwound properly. If there are nested MakeCallback's
// then it should return early and not reach this code.
if (env_->async_hooks()->fields()[AsyncHooks::kTotals]) {
CHECK_EQ(env_->execution_async_id(), 0);
CHECK_EQ(env_->trigger_async_id(), 0);
}
if (!tick_info->has_tick_scheduled() && !tick_info->has_rejection_to_warn()) {
return;
}
HandleScope handle_scope(isolate);
Local<Object> process = env_->process_object();
if (!env_->can_call_into_js()) return;
Local<Function> tick_callback = env_->tick_callback_function();
// The tick is triggered before JS land calls SetTickCallback
// to initializes the tick callback during bootstrap.
CHECK(!tick_callback.IsEmpty());
if (tick_callback->Call(context, process, 0, nullptr).IsEmpty()) {
failed_ = true;
}
perform_stopping_check();
}
MaybeLocal<Value> InternalMakeCallback(Environment* env,
Local<Object> resource,
Local<Object> recv,
const Local<Function> callback,
int argc,
Local<Value> argv[],
async_context asyncContext,
Local<Value> context_frame) {
CHECK(!recv.IsEmpty());
#ifdef DEBUG
for (int i = 0; i < argc; i++)
CHECK(!argv[i].IsEmpty());
#endif
Local<Function> hook_cb = env->async_hooks_callback_trampoline();
int flags = InternalCallbackScope::kNoFlags;
bool use_async_hooks_trampoline = false;
AsyncHooks* async_hooks = env->async_hooks();
if (!hook_cb.IsEmpty()) {
// Use the callback trampoline if there are any before or after hooks, or
// we can expect some kind of usage of async_hooks.executionAsyncResource().
flags = InternalCallbackScope::kSkipAsyncHooks;
use_async_hooks_trampoline =
async_hooks->fields()[AsyncHooks::kBefore] +
async_hooks->fields()[AsyncHooks::kAfter] +
async_hooks->fields()[AsyncHooks::kUsesExecutionAsyncResource] > 0;
}
InternalCallbackScope scope(
env, resource, asyncContext, flags, context_frame);
if (scope.Failed()) {
return MaybeLocal<Value>();
}
MaybeLocal<Value> ret;
Local<Context> context = env->context();
if (use_async_hooks_trampoline) {
MaybeStackBuffer<Value, 16> args(env->isolate(), 3 + argc);
args[0] = Number::New(env->isolate(), asyncContext.async_id);
args[1] = resource;
args[2] = callback;
for (int i = 0; i < argc; i++) {
args[i + 3] = argv[i];
}
ret = hook_cb->Call(context, recv, args.length(), &args[0]);
} else {
ret = callback->Call(context, recv, argc, argv);
}
if (ret.IsEmpty()) {
scope.MarkAsFailed();
return MaybeLocal<Value>();
}
scope.Close();
if (scope.Failed()) {
return MaybeLocal<Value>();
}
return ret;
}
// Public MakeCallback()s
MaybeLocal<Value> MakeCallback(Isolate* isolate,
Local<Object> recv,
const char* method,
int argc,
Local<Value> argv[],
async_context asyncContext) {
Local<String> method_string;
if (!String::NewFromUtf8(isolate, method).ToLocal(&method_string)) {
return {};
}
return MakeCallback(isolate, recv, method_string, argc, argv, asyncContext);
}
MaybeLocal<Value> MakeCallback(Isolate* isolate,
Local<Object> recv,
Local<String> symbol,
int argc,
Local<Value> argv[],
async_context asyncContext) {
// Check can_call_into_js() first because calling Get() might do so.
Local<Context> context;
if (!recv->GetCreationContext().ToLocal(&context)) {
return {};
}
Environment* env = Environment::GetCurrent(context);
CHECK_NOT_NULL(env);
if (!env->can_call_into_js()) return {};
Local<Value> callback_v;
if (!recv->Get(isolate->GetCurrentContext(), symbol).ToLocal(&callback_v)) {
return {};
}
if (!callback_v->IsFunction()) {
// This used to return an empty value, but Undefined() makes more sense
// since no exception is pending here.
return Undefined(isolate);
}
Local<Function> callback = callback_v.As<Function>();
return MakeCallback(isolate, recv, callback, argc, argv, asyncContext);
}
MaybeLocal<Value> MakeCallback(Isolate* isolate,
Local<Object> recv,
Local<Function> callback,
int argc,
Local<Value> argv[],
async_context asyncContext) {
return InternalMakeCallback(
isolate, recv, callback, argc, argv, asyncContext, Undefined(isolate));
}
MaybeLocal<Value> InternalMakeCallback(Isolate* isolate,
Local<Object> recv,
Local<Function> callback,
int argc,
Local<Value> argv[],
async_context asyncContext,
Local<Value> context_frame) {
// Observe the following two subtleties:
//
// 1. The environment is retrieved from the callback function's context.
// 2. The context to enter is retrieved from the environment.
//
// Because of the AssignToContext() call in src/node_contextify.cc,
// the two contexts need not be the same.
Local<Context> context;
if (!callback->GetCreationContext().ToLocal(&context)) {
return {};
}
Environment* env = Environment::GetCurrent(context);
CHECK_NOT_NULL(env);
// Entering the context changes nothing when it is already the entered and
// the current one, which is the common case for addons.
Local<Context> env_context = env->context();
std::optional<Context::Scope> context_scope;
if (isolate->GetEnteredOrMicrotaskContext() != env_context ||
isolate->GetCurrentContext() != env_context) {
context_scope.emplace(env_context);
}
MaybeLocal<Value> ret = InternalMakeCallback(
env, recv, recv, callback, argc, argv, asyncContext, context_frame);
if (ret.IsEmpty() && env->async_callback_scope_depth() == 0) {
// This is only for legacy compatibility and we may want to look into
// removing/adjusting it.
return Undefined(isolate);
}
return ret;
}
// Use this if you just want to safely invoke some JS callback and
// would like to retain the currently active async_context, if any.
// In case none is available, a fixed default context will be
// installed otherwise.
MaybeLocal<Value> MakeSyncCallback(Isolate* isolate,
Local<Object> recv,
Local<Function> callback,
int argc,
Local<Value> argv[]) {
Local<Context> context;
if (!callback->GetCreationContext().ToLocal(&context)) {
return {};
}
Environment* env = Environment::GetCurrent(context);
CHECK_NOT_NULL(env);
if (!env->can_call_into_js()) return {};
Context::Scope context_scope(context);
if (env->async_callback_scope_depth()) {
// There's another MakeCallback() on the stack, piggy back on it.
// In particular, retain the current async_context.
return callback->Call(context, recv, argc, argv);
}
// This is a toplevel invocation and the caller (intentionally)
// didn't provide any async_context to run in. Install a default context.
MaybeLocal<Value> ret = InternalMakeCallback(env,
env->process_object(),
recv,
callback,
argc,
argv,
async_context{0, 0},
Undefined(isolate));
return ret;
}
} // namespace node