async_hooks: eliminate native PromiseHook

PR-URL: https://github.com/nodejs/node/pull/39135
Reviewed-By: Rich Trott <rtrott@gmail.com>
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
Reviewed-By: Gerhard Stöbich <deb2001-github@yahoo.de>
This commit is contained in:
Stephen Belanger 2021-06-23 22:35:46 -07:00 committed by Node.js GitHub Bot
parent 44ee6c2623
commit 84dfa1f3e3
5 changed files with 34 additions and 456 deletions

View File

@ -52,7 +52,7 @@ const {
clearAsyncIdStack,
} = async_wrap;
// For performance reasons, only track Promises when a hook is enabled.
const { enablePromiseHook, disablePromiseHook, setPromiseHooks } = async_wrap;
const { setPromiseHooks } = async_wrap;
// Properties in active_hooks are used to keep track of the set of hooks being
// executed in case another hook is enabled/disabled. The new set of hooks is
// then restored once the active set of hooks is finished executing.
@ -307,9 +307,13 @@ function trackPromise(promise, parent) {
return;
}
promise[async_id_symbol] = newAsyncId();
promise[trigger_async_id_symbol] = parent ? getOrSetAsyncId(parent) :
// Get trigger id from parent async id before making the async id of the
// child so if a new one must be made it will be lower than the child.
const triggerAsyncId = parent ? getOrSetAsyncId(parent) :
getDefaultTriggerAsyncId();
promise[async_id_symbol] = newAsyncId();
promise[trigger_async_id_symbol] = triggerAsyncId;
}
function promiseInitHook(promise, parent) {
@ -319,6 +323,21 @@ function promiseInitHook(promise, parent) {
emitInitScript(asyncId, 'PROMISE', triggerAsyncId, promise);
}
function promiseInitHookWithDestroyTracking(promise, parent) {
promiseInitHook(promise, parent);
destroyTracking(promise, parent);
}
const destroyedSymbol = Symbol('destroyed');
function destroyTracking(promise, parent) {
trackPromise(promise, parent);
const asyncId = promise[async_id_symbol];
const destroyed = { destroyed: false };
promise[destroyedSymbol] = destroyed;
registerDestroyHook(promise, asyncId, destroyed);
}
function promiseBeforeHook(promise) {
trackPromise(promise);
const asyncId = promise[async_id_symbol];
@ -355,18 +374,19 @@ function enableHooks() {
function updatePromiseHookMode() {
wantPromiseHook = true;
if (destroyHooksExist()) {
enablePromiseHook();
setPromiseHooks(undefined, undefined, undefined, undefined);
} else {
disablePromiseHook();
setPromiseHooks(
initHooksExist() ? promiseInitHook : undefined,
promiseBeforeHook,
promiseAfterHook,
promiseResolveHooksExist() ? promiseResolveHook : undefined,
);
let initHook;
if (initHooksExist()) {
initHook = destroyHooksExist() ? promiseInitHookWithDestroyTracking :
promiseInitHook;
} else if (destroyHooksExist()) {
initHook = destroyTracking;
}
setPromiseHooks(
initHook,
promiseBeforeHook,
promiseAfterHook,
promiseResolveHooksExist() ? promiseResolveHook : undefined,
);
}
function disableHooks() {
@ -381,7 +401,6 @@ function disableHooks() {
function disablePromiseHookIfNecessary() {
if (!wantPromiseHook) {
disablePromiseHook();
setPromiseHooks(undefined, undefined, undefined, undefined);
}
}

View File

@ -39,19 +39,12 @@ using v8::Global;
using v8::HandleScope;
using v8::Integer;
using v8::Isolate;
using v8::Just;
using v8::Local;
using v8::Maybe;
using v8::MaybeLocal;
using v8::Name;
using v8::Nothing;
using v8::Number;
using v8::Object;
using v8::ObjectTemplate;
using v8::Promise;
using v8::PromiseHookType;
using v8::PropertyAttribute;
using v8::PropertyCallbackInfo;
using v8::ReadOnly;
using v8::String;
using v8::Undefined;
@ -161,255 +154,6 @@ void AsyncWrap::EmitAfter(Environment* env, double async_id) {
// TODO(addaleax): Remove once we're on C++17.
constexpr double AsyncWrap::kInvalidAsyncId;
static Maybe<double> GetAssignedPromiseAsyncId(Environment* env,
Local<Promise> promise,
Local<Value> id_symbol) {
Local<Value> maybe_async_id;
if (!promise->Get(env->context(), id_symbol).ToLocal(&maybe_async_id)) {
return Nothing<double>();
}
return maybe_async_id->IsNumber()
? maybe_async_id->NumberValue(env->context())
: Just(AsyncWrap::kInvalidAsyncId);
}
class PromiseWrap : public AsyncWrap {
public:
PromiseWrap(Environment* env, Local<Object> object, bool silent)
: AsyncWrap(env, object, PROVIDER_PROMISE, kInvalidAsyncId, silent) {
MakeWeak();
}
PromiseWrap(Environment* env, Local<Object> object, double asyncId,
double triggerAsyncId)
: AsyncWrap(env, object, PROVIDER_PROMISE, asyncId, triggerAsyncId) {
MakeWeak();
}
SET_NO_MEMORY_INFO()
SET_MEMORY_INFO_NAME(PromiseWrap)
SET_SELF_SIZE(PromiseWrap)
static PromiseWrap* New(Environment* env,
Local<Promise> promise,
bool silent);
static void GetAsyncId(Local<Name> property,
const PropertyCallbackInfo<Value>& args);
static void GetTriggerAsyncId(Local<Name> property,
const PropertyCallbackInfo<Value>& args);
static void Initialize(Environment* env);
};
PromiseWrap* PromiseWrap::New(Environment* env,
Local<Promise> promise,
bool silent) {
Local<Context> context = env->context();
Local<Object> obj;
if (!env->promise_wrap_template()->NewInstance(context).ToLocal(&obj))
return nullptr;
CHECK_NULL(promise->GetAlignedPointerFromInternalField(0));
promise->SetInternalField(0, obj);
// Skip for init events
if (silent) {
double async_id;
double trigger_async_id;
if (!GetAssignedPromiseAsyncId(env, promise, env->async_id_symbol())
.To(&async_id)) return nullptr;
if (!GetAssignedPromiseAsyncId(env, promise, env->trigger_async_id_symbol())
.To(&trigger_async_id)) return nullptr;
if (async_id != AsyncWrap::kInvalidAsyncId &&
trigger_async_id != AsyncWrap::kInvalidAsyncId) {
return new PromiseWrap(
env, obj, async_id, trigger_async_id);
}
}
return new PromiseWrap(env, obj, silent);
}
void PromiseWrap::GetAsyncId(Local<Name> property,
const PropertyCallbackInfo<Value>& info) {
Isolate* isolate = info.GetIsolate();
HandleScope scope(isolate);
PromiseWrap* wrap = Unwrap<PromiseWrap>(info.Holder());
double value = wrap->get_async_id();
info.GetReturnValue().Set(Number::New(isolate, value));
}
void PromiseWrap::GetTriggerAsyncId(Local<Name> property,
const PropertyCallbackInfo<Value>& info) {
Isolate* isolate = info.GetIsolate();
HandleScope scope(isolate);
PromiseWrap* wrap = Unwrap<PromiseWrap>(info.Holder());
double value = wrap->get_trigger_async_id();
info.GetReturnValue().Set(Number::New(isolate, value));
}
void PromiseWrap::Initialize(Environment* env) {
Isolate* isolate = env->isolate();
HandleScope scope(isolate);
Local<FunctionTemplate> ctor = FunctionTemplate::New(isolate);
ctor->SetClassName(FIXED_ONE_BYTE_STRING(isolate, "PromiseWrap"));
Local<ObjectTemplate> promise_wrap_template = ctor->InstanceTemplate();
env->set_promise_wrap_template(promise_wrap_template);
promise_wrap_template->SetInternalFieldCount(
PromiseWrap::kInternalFieldCount);
promise_wrap_template->SetAccessor(
env->async_id_symbol(),
PromiseWrap::GetAsyncId);
promise_wrap_template->SetAccessor(
env->trigger_async_id_symbol(),
PromiseWrap::GetTriggerAsyncId);
}
static PromiseWrap* extractPromiseWrap(Local<Promise> promise) {
// This check is imperfect. If the internal field is set, it should
// be an object. If it's not, we just ignore it. Ideally v8 would
// have had GetInternalField returning a MaybeLocal but this works
// for now.
Local<Value> obj = promise->GetInternalField(0);
return obj->IsObject() ? Unwrap<PromiseWrap>(obj.As<Object>()) : nullptr;
}
static uint16_t ToAsyncHooksType(PromiseHookType type) {
switch (type) {
case PromiseHookType::kInit: return AsyncHooks::kInit;
case PromiseHookType::kBefore: return AsyncHooks::kBefore;
case PromiseHookType::kAfter: return AsyncHooks::kAfter;
case PromiseHookType::kResolve: return AsyncHooks::kPromiseResolve;
}
UNREACHABLE();
}
// Simplified JavaScript hook fast-path for when there is no destroy hook
static void FastPromiseHook(PromiseHookType type, Local<Promise> promise,
Local<Value> parent) {
Local<Context> context = promise->GetCreationContext().ToLocalChecked();
Environment* env = Environment::GetCurrent(context);
if (env == nullptr) return;
if (type == PromiseHookType::kBefore &&
env->async_hooks()->fields()[AsyncHooks::kBefore] == 0) {
double async_id;
double trigger_async_id;
if (!GetAssignedPromiseAsyncId(env, promise, env->async_id_symbol())
.To(&async_id)) return;
if (!GetAssignedPromiseAsyncId(env, promise, env->trigger_async_id_symbol())
.To(&trigger_async_id)) return;
if (async_id != AsyncWrap::kInvalidAsyncId &&
trigger_async_id != AsyncWrap::kInvalidAsyncId) {
env->async_hooks()->push_async_context(
async_id, trigger_async_id, promise);
return;
}
}
if (type == PromiseHookType::kAfter &&
env->async_hooks()->fields()[AsyncHooks::kAfter] == 0) {
double async_id;
if (!GetAssignedPromiseAsyncId(env, promise, env->async_id_symbol())
.To(&async_id)) return;
if (async_id != AsyncWrap::kInvalidAsyncId) {
if (env->execution_async_id() == async_id) {
// This condition might not be true if async_hooks was enabled during
// the promise callback execution.
env->async_hooks()->pop_async_context(async_id);
}
return;
}
}
if (type == PromiseHookType::kResolve &&
env->async_hooks()->fields()[AsyncHooks::kPromiseResolve] == 0) {
return;
}
// Getting up to this point means either init type or
// that there are active hooks of another type.
// In both cases fast-path JS hook should be called.
Local<Value> argv[] = {
Integer::New(env->isolate(), ToAsyncHooksType(type)),
promise,
parent
};
TryCatchScope try_catch(env, TryCatchScope::CatchMode::kFatal);
Local<Function> promise_hook = env->promise_hook_handler();
USE(promise_hook->Call(context, Undefined(env->isolate()), 3, argv));
}
static void FullPromiseHook(PromiseHookType type, Local<Promise> promise,
Local<Value> parent) {
Local<Context> context = promise->GetCreationContext().ToLocalChecked();
Environment* env = Environment::GetCurrent(context);
if (env == nullptr) return;
TraceEventScope trace_scope(TRACING_CATEGORY_NODE1(environment),
"EnvPromiseHook", env);
PromiseWrap* wrap = extractPromiseWrap(promise);
if (type == PromiseHookType::kInit || wrap == nullptr) {
bool silent = type != PromiseHookType::kInit;
// set parent promise's async Id as this promise's triggerAsyncId
if (parent->IsPromise()) {
// parent promise exists, current promise
// is a chained promise, so we set parent promise's id as
// current promise's triggerAsyncId
Local<Promise> parent_promise = parent.As<Promise>();
PromiseWrap* parent_wrap = extractPromiseWrap(parent_promise);
if (parent_wrap == nullptr) {
parent_wrap = PromiseWrap::New(env, parent_promise, true);
if (parent_wrap == nullptr) return;
}
AsyncHooks::DefaultTriggerAsyncIdScope trigger_scope(parent_wrap);
wrap = PromiseWrap::New(env, promise, silent);
} else {
wrap = PromiseWrap::New(env, promise, silent);
}
}
if (wrap == nullptr) return;
if (type == PromiseHookType::kBefore) {
env->async_hooks()->push_async_context(wrap->get_async_id(),
wrap->get_trigger_async_id(), wrap->object());
wrap->EmitTraceEventBefore();
AsyncWrap::EmitBefore(wrap->env(), wrap->get_async_id());
} else if (type == PromiseHookType::kAfter) {
wrap->EmitTraceEventAfter(wrap->provider_type(), wrap->get_async_id());
AsyncWrap::EmitAfter(wrap->env(), wrap->get_async_id());
if (env->execution_async_id() == wrap->get_async_id()) {
// This condition might not be true if async_hooks was enabled during
// the promise callback execution.
// Popping it off the stack can be skipped in that case, because it is
// known that it would correspond to exactly one call with
// PromiseHookType::kBefore that was not witnessed by the PromiseHook.
env->async_hooks()->pop_async_context(wrap->get_async_id());
}
} else if (type == PromiseHookType::kResolve) {
AsyncWrap::EmitPromiseResolve(wrap->env(), wrap->get_async_id());
}
}
static void SetupHooks(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
@ -441,17 +185,6 @@ static void SetupHooks(const FunctionCallbackInfo<Value>& args) {
#undef SET_HOOK_FN
}
static void EnablePromiseHook(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
if (args[0]->IsFunction()) {
env->set_promise_hook_handler(args[0].As<Function>());
args.GetIsolate()->SetPromiseHook(FastPromiseHook);
} else {
args.GetIsolate()->SetPromiseHook(FullPromiseHook);
}
}
static void SetPromiseHooks(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
@ -462,17 +195,6 @@ static void SetPromiseHooks(const FunctionCallbackInfo<Value>& args) {
args[3]->IsFunction() ? args[3].As<Function>() : Local<Function>());
}
static void DisablePromiseHook(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
env->set_promise_hook_handler(Local<Function>());
// The per-Isolate API provides no way of knowing whether there are multiple
// users of the PromiseHook. That hopefully goes away when V8 introduces
// a per-context API.
args.GetIsolate()->SetPromiseHook(nullptr);
}
class DestroyParam {
public:
double asyncId;
@ -639,9 +361,7 @@ void AsyncWrap::Initialize(Local<Object> target,
env->SetMethod(target, "executionAsyncResource", ExecutionAsyncResource);
env->SetMethod(target, "clearAsyncIdStack", ClearAsyncIdStack);
env->SetMethod(target, "queueDestroyAsyncId", QueueDestroyAsyncId);
env->SetMethod(target, "enablePromiseHook", EnablePromiseHook);
env->SetMethod(target, "setPromiseHooks", SetPromiseHooks);
env->SetMethod(target, "disablePromiseHook", DisablePromiseHook);
env->SetMethod(target, "registerDestroyHook", RegisterDestroyHook);
PropertyAttribute ReadOnlyDontDelete =
@ -720,9 +440,6 @@ void AsyncWrap::Initialize(Local<Object> target,
env->set_async_hooks_destroy_function(Local<Function>());
env->set_async_hooks_promise_resolve_function(Local<Function>());
env->set_async_hooks_binding(target);
// TODO(qard): maybe this should be GetConstructorTemplate instead?
PromiseWrap::Initialize(env);
}
void AsyncWrap::RegisterExternalReferences(
@ -734,15 +451,11 @@ void AsyncWrap::RegisterExternalReferences(
registry->Register(ExecutionAsyncResource);
registry->Register(ClearAsyncIdStack);
registry->Register(QueueDestroyAsyncId);
registry->Register(EnablePromiseHook);
registry->Register(SetPromiseHooks);
registry->Register(DisablePromiseHook);
registry->Register(RegisterDestroyHook);
registry->Register(AsyncWrap::GetAsyncId);
registry->Register(AsyncWrap::AsyncReset);
registry->Register(AsyncWrap::GetProviderType);
registry->Register(PromiseWrap::GetAsyncId);
registry->Register(PromiseWrap::GetTriggerAsyncId);
}
AsyncWrap::AsyncWrap(Environment* env,

View File

@ -1,41 +0,0 @@
#include <node.h>
#include <v8.h>
namespace {
using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::Object;
using v8::Promise;
using v8::String;
using v8::Value;
static void ThrowError(Isolate* isolate, const char* err_msg) {
Local<String> str = String::NewFromOneByte(
isolate,
reinterpret_cast<const uint8_t*>(err_msg)).ToLocalChecked();
isolate->ThrowException(str);
}
static void GetPromiseField(const FunctionCallbackInfo<Value>& args) {
auto isolate = args.GetIsolate();
if (!args[0]->IsPromise())
return ThrowError(isolate, "arg is not an Promise");
auto p = args[0].As<Promise>();
if (p->InternalFieldCount() < 1)
return ThrowError(isolate, "Promise has no internal field");
auto l = p->GetInternalField(0);
args.GetReturnValue().Set(l);
}
inline void Initialize(v8::Local<v8::Object> binding) {
NODE_SET_METHOD(binding, "getPromiseField", GetPromiseField);
}
NODE_MODULE(NODE_GYP_MODULE_NAME, Initialize)
} // anonymous namespace

View File

@ -1,9 +0,0 @@
{
'targets': [
{
'target_name': 'binding',
'sources': [ 'binding.cc' ],
'includes': ['../common.gypi'],
}
]
}

View File

@ -1,104 +0,0 @@
'use strict';
// Flags: --expose-internals
const common = require('../../common');
const assert = require('assert');
const async_hooks = require('async_hooks');
const { async_id_symbol,
trigger_async_id_symbol } = require('internal/async_hooks').symbols;
const binding = require(`./build/${common.buildType}/binding`);
if (process.env.NODE_TEST_WITH_ASYNC_HOOKS) {
common.skip('cannot test with env var NODE_TEST_WITH_ASYNC_HOOKS');
return;
}
// Baseline to make sure the internal field isn't being set.
assert.strictEqual(
binding.getPromiseField(Promise.resolve(1)),
0);
const emptyHook = async_hooks.createHook({}).enable();
// Check that no PromiseWrap is created when there are no hook callbacks.
assert.strictEqual(
binding.getPromiseField(Promise.resolve(1)),
0);
emptyHook.disable();
let lastResource;
let lastAsyncId;
let lastTriggerAsyncId;
const initOnlyHook = async_hooks.createHook({
init(asyncId, type, triggerAsyncId, resource) {
lastAsyncId = asyncId;
lastTriggerAsyncId = triggerAsyncId;
lastResource = resource;
}
}).enable();
// Check that no PromiseWrap is created when only using an init hook.
{
const promise = Promise.resolve(1);
assert.strictEqual(binding.getPromiseField(promise), 0);
assert.strictEqual(lastResource, promise);
assert.strictEqual(lastAsyncId, promise[async_id_symbol]);
assert.strictEqual(lastTriggerAsyncId, promise[trigger_async_id_symbol]);
}
initOnlyHook.disable();
lastResource = null;
const hookWithDestroy = async_hooks.createHook({
init(asyncId, type, triggerAsyncId, resource) {
lastAsyncId = asyncId;
lastTriggerAsyncId = triggerAsyncId;
lastResource = resource;
},
destroy() {
}
}).enable();
// Check that the internal field returns the same PromiseWrap passed to init().
{
const promise = Promise.resolve(1);
const promiseWrap = binding.getPromiseField(promise);
assert.strictEqual(lastResource, promiseWrap);
assert.strictEqual(lastAsyncId, promiseWrap[async_id_symbol]);
assert.strictEqual(lastTriggerAsyncId, promiseWrap[trigger_async_id_symbol]);
}
hookWithDestroy.disable();
// Check that internal fields are no longer being set. This needs to be delayed
// a bit because the `disable()` call only schedules disabling the hook in a
// future microtask.
setImmediate(() => {
assert.strictEqual(
binding.getPromiseField(Promise.resolve(1)),
0);
const noDestroyHook = async_hooks.createHook({
init(asyncId, type, triggerAsyncId, resource) {
lastAsyncId = asyncId;
lastTriggerAsyncId = triggerAsyncId;
lastResource = resource;
},
before() {},
after() {},
resolve() {}
}).enable();
// Check that no PromiseWrap is created when there is no destroy hook.
const promise = Promise.resolve(1);
assert.strictEqual(binding.getPromiseField(promise), 0);
assert.strictEqual(lastResource, promise);
assert.strictEqual(lastAsyncId, promise[async_id_symbol]);
assert.strictEqual(lastTriggerAsyncId, promise[trigger_async_id_symbol]);
noDestroyHook.disable();
});