diff --git a/lib/internal/async_hooks.js b/lib/internal/async_hooks.js index da7b2745e53bbc..21f31173d67617 100644 --- a/lib/internal/async_hooks.js +++ b/lib/internal/async_hooks.js @@ -6,6 +6,7 @@ const { ObjectPrototypeHasOwnProperty, ObjectDefineProperty, Promise, + SafeMap, Symbol, } = primordials; @@ -38,7 +39,6 @@ const async_wrap = internalBinding('async_wrap'); const { async_hook_fields, async_id_fields, - execution_async_resources } = async_wrap; // Store the pair executionAsyncId and triggerAsyncId in a AliasedFloat64Array // in Environment::AsyncHooks::async_ids_stack_ which tracks the resource @@ -47,7 +47,9 @@ const { // each hook's after() callback. const { pushAsyncContext: pushAsyncContext_, - popAsyncContext: popAsyncContext_ + popAsyncContext: popAsyncContext_, + clearAsyncIdStack, + executionAsyncResource: executionAsyncResource_, } = async_wrap; // For performance reasons, only track Promises when a hook is enabled. const { enablePromiseHook, disablePromiseHook } = async_wrap; @@ -84,9 +86,11 @@ const { resource_symbol, owner_symbol } = internalBinding('symbols'); // Each constant tracks how many callbacks there are for any given step of // async execution. These are tracked so if the user didn't include callbacks // for a given step, that step can bail out early. -const { kInit, kBefore, kAfter, kDestroy, kTotals, kPromiseResolve, - kCheck, kExecutionAsyncId, kAsyncIdCounter, kTriggerAsyncId, - kDefaultTriggerAsyncId, kStackLength } = async_wrap.constants; +const { + kInit, kBefore, kAfter, kDestroy, kTotals, kPromiseResolve, + kCheck, kExecutionAsyncId, kAsyncIdCounter, kTriggerAsyncId, + kDefaultTriggerAsyncId, kCachedResourceIsValid, kStackLength +} = async_wrap.constants; const { async_id_symbol, trigger_async_id_symbol } = internalBinding('symbols'); @@ -104,12 +108,24 @@ const emitPromiseResolveNative = emitHookFactory(promise_resolve_symbol, 'emitPromiseResolveNative'); const topLevelResource = {}; +// Contains a [stack height] -> [resource] map for things pushed onto the +// async resource stack from JS. +const jsResourceStack = new SafeMap(); +// Contains either a single key (null) or nothing. If the key is present, +// this points to the current async resource. +const cachedResourceHolder = new SafeMap(); function executionAsyncResource() { const index = async_hook_fields[kStackLength] - 1; if (index === -1) return topLevelResource; - const resource = execution_async_resources[index]; - return lookupPublicResource(resource); + + if (async_hook_fields[kCachedResourceIsValid]) + return cachedResourceHolder.get(null); + const resource = jsResourceStack.get(index) ?? executionAsyncResource_(); + const publicResource = lookupPublicResource(resource); + async_hook_fields[kCachedResourceIsValid] = 1; + cachedResourceHolder.set(null, publicResource); + return publicResource; } // Used to fatally abort the process if a callback throws. @@ -450,16 +466,6 @@ function emitDestroyScript(asyncId) { } -// Keep in sync with Environment::AsyncHooks::clear_async_id_stack -// in src/env-inl.h. -function clearAsyncIdStack() { - async_id_fields[kExecutionAsyncId] = 0; - async_id_fields[kTriggerAsyncId] = 0; - async_hook_fields[kStackLength] = 0; - execution_async_resources.splice(0, execution_async_resources.length); -} - - function hasAsyncIdStack() { return hasHooks(kStackLength); } @@ -472,15 +478,22 @@ function pushAsyncContext(asyncId, triggerAsyncId, resource) { return pushAsyncContext_(asyncId, triggerAsyncId, resource); async_wrap.async_ids_stack[offset * 2] = async_id_fields[kExecutionAsyncId]; async_wrap.async_ids_stack[offset * 2 + 1] = async_id_fields[kTriggerAsyncId]; - execution_async_resources[offset] = resource; async_hook_fields[kStackLength]++; async_id_fields[kExecutionAsyncId] = asyncId; async_id_fields[kTriggerAsyncId] = triggerAsyncId; + jsResourceStack.set(offset, resource); + if (async_hook_fields[kCachedResourceIsValid]) + cachedResourceHolder.clear(); + async_hook_fields[kCachedResourceIsValid] = 0; } // This is the equivalent of the native pop_async_ids() call. function popAsyncContext(asyncId) { + async_hook_fields[kCachedResourceIsValid] = 0; + if (async_hook_fields[kCachedResourceIsValid]) + cachedResourceHolder.clear(); + const stackLength = async_hook_fields[kStackLength]; if (stackLength === 0) return false; @@ -490,9 +503,17 @@ function popAsyncContext(asyncId) { } const offset = stackLength - 1; + + if (!jsResourceStack.has(offset)) { + // For some reason this popAsyncContext() call removes a resource for a + // corresponding push() call from C++, so let the C++ code handle this + // pop operation since it's on the native the resource stack. + return popAsyncContext_(asyncId); + } + async_id_fields[kExecutionAsyncId] = async_wrap.async_ids_stack[2 * offset]; async_id_fields[kTriggerAsyncId] = async_wrap.async_ids_stack[2 * offset + 1]; - execution_async_resources.pop(); + jsResourceStack.delete(offset); async_hook_fields[kStackLength] = offset; return offset > 0; } @@ -547,5 +568,7 @@ module.exports = { after: emitAfterNative, destroy: emitDestroyNative, promise_resolve: emitPromiseResolveNative - } + }, + jsResourceStack, + cachedResourceHolder }; diff --git a/lib/internal/bootstrap/node.js b/lib/internal/bootstrap/node.js index fbc80389c3985a..0b79f87931c83d 100644 --- a/lib/internal/bootstrap/node.js +++ b/lib/internal/bootstrap/node.js @@ -105,8 +105,11 @@ if (credentials.implementsPosixCredentials) { // process. They use the same functions as the JS embedder API. These callbacks // are setup immediately to prevent async_wrap.setupHooks() from being hijacked // and the cost of doing so is negligible. -const { nativeHooks } = require('internal/async_hooks'); -internalBinding('async_wrap').setupHooks(nativeHooks); +const { + nativeHooks, jsResourceStack, cachedResourceHolder +} = require('internal/async_hooks'); +internalBinding('async_wrap').setupHooks( + nativeHooks, jsResourceStack, cachedResourceHolder); const { setupTaskQueue, diff --git a/src/async_wrap.cc b/src/async_wrap.cc index 46a6ea6eebe85f..5c17d14eca1ff3 100644 --- a/src/async_wrap.cc +++ b/src/async_wrap.cc @@ -39,6 +39,7 @@ using v8::HandleScope; using v8::Integer; using v8::Isolate; using v8::Local; +using v8::Map; using v8::Maybe; using v8::MaybeLocal; using v8::Name; @@ -415,6 +416,11 @@ static void SetupHooks(const FunctionCallbackInfo& args) { SET_HOOK_FN(destroy); SET_HOOK_FN(promise_resolve); #undef SET_HOOK_FN + + CHECK(args[1]->IsMap()); + CHECK(args[2]->IsMap()); + env->async_hooks()->set_js_execution_async_resources(args[1].As()); + env->async_hooks()->set_cached_resource_holder(args[2].As()); } static void EnablePromiseHook(const FunctionCallbackInfo& args) { @@ -566,6 +572,16 @@ Local AsyncWrap::GetConstructorTemplate(Environment* env) { return tmpl; } +static void ExecutionAsyncResource(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + args.GetReturnValue().Set(env->async_hooks()->execution_async_resource()); +} + +static void ClearAsyncIdStack(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + env->async_hooks()->clear_async_id_stack(); +} + void AsyncWrap::Initialize(Local target, Local unused, Local context, @@ -581,6 +597,8 @@ void AsyncWrap::Initialize(Local target, env->SetMethod(target, "enablePromiseHook", EnablePromiseHook); env->SetMethod(target, "disablePromiseHook", DisablePromiseHook); env->SetMethod(target, "registerDestroyHook", RegisterDestroyHook); + env->SetMethod(target, "executionAsyncResource", ExecutionAsyncResource); + env->SetMethod(target, "clearAsyncIdStack", ClearAsyncIdStack); PropertyAttribute ReadOnlyDontDelete = static_cast(ReadOnly | DontDelete); @@ -613,10 +631,6 @@ void AsyncWrap::Initialize(Local target, "async_id_fields", env->async_hooks()->async_id_fields().GetJSArray()); - FORCE_SET_TARGET_FIELD(target, - "execution_async_resources", - env->async_hooks()->execution_async_resources()); - target->Set(context, env->async_ids_stack_string(), env->async_hooks()->async_ids_stack().GetJSArray()).Check(); @@ -637,6 +651,7 @@ void AsyncWrap::Initialize(Local target, SET_HOOKS_CONSTANT(kTriggerAsyncId); SET_HOOKS_CONSTANT(kAsyncIdCounter); SET_HOOKS_CONSTANT(kDefaultTriggerAsyncId); + SET_HOOKS_CONSTANT(kCachedResourceIsValid); SET_HOOKS_CONSTANT(kStackLength); #undef SET_HOOKS_CONSTANT FORCE_SET_TARGET_FIELD(target, "constants", constants); diff --git a/src/env-inl.h b/src/env-inl.h index 97ccc24f809e5e..7367e17da82be1 100644 --- a/src/env-inl.h +++ b/src/env-inl.h @@ -113,8 +113,32 @@ inline AliasedFloat64Array& AsyncHooks::async_ids_stack() { return async_ids_stack_; } -inline v8::Local AsyncHooks::execution_async_resources() { - return PersistentToLocal::Strong(execution_async_resources_); +v8::Local AsyncHooks::execution_async_resource() { + if (fields_[kStackLength] == 0) return {}; + uint32_t offset = fields_[kStackLength] - 1; + if (LIKELY(offset < native_execution_async_resources_.size())) + return PersistentToLocal::Strong(native_execution_async_resources_[offset]); + + // This async resource was stored in the JS async resource map. + v8::Local js_map = + PersistentToLocal::Strong(js_execution_async_resources_); + v8::Local key = + v8::Integer::NewFromUnsigned(env()->isolate(), offset); + v8::Local ret; + if (UNLIKELY(js_map.IsEmpty() || + !js_map->Get(env()->context(), key).ToLocal(&ret) || + ret->IsUndefined())) { + return {}; + } + return ret; +} + +void AsyncHooks::set_js_execution_async_resources(v8::Local value) { + js_execution_async_resources_.Reset(env()->isolate(), value); +} + +void AsyncHooks::set_cached_resource_holder(v8::Local value) { + cached_resource_holder_.Reset(env()->isolate(), value); } inline v8::Local AsyncHooks::provider_string(int idx) { @@ -151,12 +175,23 @@ inline void AsyncHooks::push_async_context(double async_id, async_id_fields_[kExecutionAsyncId] = async_id; async_id_fields_[kTriggerAsyncId] = trigger_async_id; - auto resources = execution_async_resources(); - USE(resources->Set(env()->context(), offset, resource)); +#ifdef DEBUG + for (uint32_t i = offset; i < native_execution_async_resources_.size(); i++) + CHECK(native_execution_async_resources_[i].IsEmpty()); +#endif + native_execution_async_resources_.resize(offset + 1); + native_execution_async_resources_[offset].Reset(env()->isolate(), resource); + if (fields_[kCachedResourceIsValid]) + PersistentToLocal::Strong(cached_resource_holder_)->Clear(); + fields_[kCachedResourceIsValid] = 0; } // Remember to keep this code aligned with popAsyncContext() in JS. inline bool AsyncHooks::pop_async_context(double async_id) { + if (fields_[kCachedResourceIsValid]) + PersistentToLocal::Strong(cached_resource_holder_)->Clear(); + fields_[kCachedResourceIsValid] = 0; + // In case of an exception then this may have already been reset, if the // stack was multiple MakeCallback()'s deep. if (fields_[kStackLength] == 0) return false; @@ -185,21 +220,42 @@ inline bool AsyncHooks::pop_async_context(double async_id) { async_id_fields_[kTriggerAsyncId] = async_ids_stack_[2 * offset + 1]; fields_[kStackLength] = offset; - auto resources = execution_async_resources(); - USE(resources->Delete(env()->context(), offset)); + if (LIKELY(offset < native_execution_async_resources_.size() && + !native_execution_async_resources_[offset].IsEmpty())) { +#ifdef DEBUG + for (uint32_t i = offset + 1; + i < native_execution_async_resources_.size(); + i++) { + CHECK(native_execution_async_resources_[i].IsEmpty()); + } +#endif + native_execution_async_resources_[offset].Reset(); + native_execution_async_resources_.resize(offset); + if (native_execution_async_resources_.size() < + native_execution_async_resources_.capacity() / 2) { + native_execution_async_resources_.shrink_to_fit(); + } + } else { + USE(PersistentToLocal::Strong(js_execution_async_resources_)->Delete( + env()->context(), + v8::Integer::NewFromUnsigned(env()->isolate(), offset))); + } return fields_[kStackLength] > 0; } -// Keep in sync with clearAsyncIdStack in lib/internal/async_hooks.js. -inline void AsyncHooks::clear_async_id_stack() { - auto isolate = env()->isolate(); - v8::HandleScope handle_scope(isolate); - execution_async_resources_.Reset(isolate, v8::Array::New(isolate)); +void AsyncHooks::clear_async_id_stack() { + if (!js_execution_async_resources_.IsEmpty()) { + PersistentToLocal::Strong(cached_resource_holder_)->Clear(); + PersistentToLocal::Strong(js_execution_async_resources_)->Clear(); + } + native_execution_async_resources_.clear(); + native_execution_async_resources_.shrink_to_fit(); async_id_fields_[kExecutionAsyncId] = 0; async_id_fields_[kTriggerAsyncId] = 0; fields_[kStackLength] = 0; + fields_[kCachedResourceIsValid] = 0; } // The DefaultTriggerAsyncIdScope(AsyncWrap*) constructor is defined in diff --git a/src/env.h b/src/env.h index 1ef085ee867f38..05b095577ffa72 100644 --- a/src/env.h +++ b/src/env.h @@ -639,6 +639,7 @@ class AsyncHooks : public MemoryRetainer { kTotals, kCheck, kStackLength, + kCachedResourceIsValid, kFieldsCount, }; @@ -653,7 +654,7 @@ class AsyncHooks : public MemoryRetainer { inline AliasedUint32Array& fields(); inline AliasedFloat64Array& async_id_fields(); inline AliasedFloat64Array& async_ids_stack(); - inline v8::Local execution_async_resources(); + inline v8::Local execution_async_resource(); inline v8::Local provider_string(int idx); @@ -664,6 +665,8 @@ class AsyncHooks : public MemoryRetainer { v8::Local execution_async_resource_); inline bool pop_async_context(double async_id); inline void clear_async_id_stack(); // Used in fatal exceptions. + inline void set_js_execution_async_resources(v8::Local value); + inline void set_cached_resource_holder(v8::Local value); AsyncHooks(const AsyncHooks&) = delete; AsyncHooks& operator=(const AsyncHooks&) = delete; @@ -706,7 +709,9 @@ class AsyncHooks : public MemoryRetainer { void grow_async_ids_stack(); - v8::Global execution_async_resources_; + std::vector> native_execution_async_resources_; + v8::Global js_execution_async_resources_; + v8::Global cached_resource_holder_; }; class ImmediateInfo : public MemoryRetainer {