Skip to content

Commit a442603

Browse files
committed
async_hooks: use resource objects for Promises
Use `PromiseWrap` resource objects whose lifetimes are tied to the `Promise` instances themselves to track promises, and have a `.promise` getter that points to the `Promise` and a `.parent` property that points to the parent Promise’s resource object, if there is any. The properties are implemented as getters for internal fields rather than normal properties in the hope that it helps keep performance for the common case that async_hooks users will often not inspect them. PR-URL: #13452 Reviewed-By: Andreas Madsen <[email protected]> Reviewed-By: Trevor Norris <[email protected]>
1 parent 3691111 commit a442603

File tree

4 files changed

+111
-10
lines changed

4 files changed

+111
-10
lines changed

doc/api/async_hooks.md

+11-2
Original file line numberDiff line numberDiff line change
@@ -203,13 +203,16 @@ UDPSENDWRAP, UDPWRAP, WRITEWRAP, ZLIB, SSLCONNECTION, PBKDF2REQUEST,
203203
RANDOMBYTESREQUEST, TLSWRAP, Timeout, Immediate, TickObject
204204
```
205205

206+
There is also the `PROMISE` resource type, which is used to track `Promise`
207+
instances and asynchronous work scheduled by them.
208+
206209
Users are be able to define their own `type` when using the public embedder API.
207210

208211
*Note:* It is possible to have type name collisions. Embedders are encouraged
209212
to use a unique prefixes, such as the npm package name, to prevent collisions
210213
when listening to the hooks.
211214

212-
###### `triggerid`
215+
###### `triggerId`
213216

214217
`triggerId` is the `asyncId` of the resource that caused (or "triggered") the
215218
new resource to initialize and that caused `init` to call. This is different
@@ -258,7 +261,13 @@ considered public, but using the Embedder API users can provide and document
258261
their own resource objects. Such as resource object could for example contain
259262
the SQL query being executed.
260263

261-
*Note:* In some cases the resource object is reused for performance reasons,
264+
In the case of Promises, the `resource` object will have `promise` property
265+
that refers to the Promise that is being initialized, and a `parentId` property
266+
that equals the `asyncId` of a parent Promise, if there is one, and
267+
`undefined` otherwise. For example, in the case of `b = a.then(handler)`,
268+
`a` is considered a parent Promise of `b`.
269+
270+
*Note*: In some cases the resource object is reused for performance reasons,
262271
it is thus not safe to use it as a key in a `WeakMap` or add properties to it.
263272

264273
###### asynchronous context example

src/async-wrap.cc

+76-8
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ using v8::Context;
3636
using v8::Float64Array;
3737
using v8::Function;
3838
using v8::FunctionCallbackInfo;
39+
using v8::FunctionTemplate;
3940
using v8::HandleScope;
4041
using v8::HeapProfiler;
4142
using v8::Integer;
@@ -44,8 +45,10 @@ using v8::Local;
4445
using v8::MaybeLocal;
4546
using v8::Number;
4647
using v8::Object;
48+
using v8::ObjectTemplate;
4749
using v8::Promise;
4850
using v8::PromiseHookType;
51+
using v8::PropertyCallbackInfo;
4952
using v8::RetainedObjectInfo;
5053
using v8::String;
5154
using v8::Symbol;
@@ -282,37 +285,86 @@ bool AsyncWrap::EmitAfter(Environment* env, double async_id) {
282285
class PromiseWrap : public AsyncWrap {
283286
public:
284287
PromiseWrap(Environment* env, Local<Object> object, bool silent)
285-
: AsyncWrap(env, object, PROVIDER_PROMISE, silent) {}
288+
: AsyncWrap(env, object, PROVIDER_PROMISE, silent) {
289+
MakeWeak(this);
290+
}
286291
size_t self_size() const override { return sizeof(*this); }
292+
293+
static constexpr int kPromiseField = 1;
294+
static constexpr int kParentIdField = 2;
295+
static constexpr int kInternalFieldCount = 3;
296+
297+
static PromiseWrap* New(Environment* env,
298+
Local<Promise> promise,
299+
PromiseWrap* parent_wrap,
300+
bool silent);
301+
static void GetPromise(Local<String> property,
302+
const PropertyCallbackInfo<Value>& info);
303+
static void GetParentId(Local<String> property,
304+
const PropertyCallbackInfo<Value>& info);
287305
};
288306

307+
PromiseWrap* PromiseWrap::New(Environment* env,
308+
Local<Promise> promise,
309+
PromiseWrap* parent_wrap,
310+
bool silent) {
311+
Local<Object> object = env->promise_wrap_template()
312+
->NewInstance(env->context()).ToLocalChecked();
313+
object->SetInternalField(PromiseWrap::kPromiseField, promise);
314+
if (parent_wrap != nullptr) {
315+
object->SetInternalField(PromiseWrap::kParentIdField,
316+
Number::New(env->isolate(),
317+
parent_wrap->get_id()));
318+
}
319+
CHECK_EQ(promise->GetAlignedPointerFromInternalField(0), nullptr);
320+
promise->SetInternalField(0, object);
321+
return new PromiseWrap(env, object, silent);
322+
}
323+
324+
void PromiseWrap::GetPromise(Local<String> property,
325+
const PropertyCallbackInfo<Value>& info) {
326+
info.GetReturnValue().Set(info.Holder()->GetInternalField(kPromiseField));
327+
}
328+
329+
void PromiseWrap::GetParentId(Local<String> property,
330+
const PropertyCallbackInfo<Value>& info) {
331+
info.GetReturnValue().Set(info.Holder()->GetInternalField(kParentIdField));
332+
}
289333

290334
static void PromiseHook(PromiseHookType type, Local<Promise> promise,
291335
Local<Value> parent, void* arg) {
292336
Local<Context> context = promise->CreationContext();
293337
Environment* env = Environment::GetCurrent(context);
294-
PromiseWrap* wrap = Unwrap<PromiseWrap>(promise);
338+
Local<Value> resource_object_value = promise->GetInternalField(0);
339+
PromiseWrap* wrap = nullptr;
340+
if (resource_object_value->IsObject()) {
341+
Local<Object> resource_object = resource_object_value.As<Object>();
342+
wrap = Unwrap<PromiseWrap>(resource_object);
343+
}
295344
if (type == PromiseHookType::kInit || wrap == nullptr) {
296345
bool silent = type != PromiseHookType::kInit;
346+
PromiseWrap* parent_wrap = nullptr;
347+
297348
// set parent promise's async Id as this promise's triggerId
298349
if (parent->IsPromise()) {
299350
// parent promise exists, current promise
300351
// is a chained promise, so we set parent promise's id as
301352
// current promise's triggerId
302353
Local<Promise> parent_promise = parent.As<Promise>();
303-
auto parent_wrap = Unwrap<PromiseWrap>(parent_promise);
354+
Local<Value> parent_resource = parent_promise->GetInternalField(0);
355+
if (parent_resource->IsObject()) {
356+
parent_wrap = Unwrap<PromiseWrap>(parent_resource.As<Object>());
357+
}
304358

305359
if (parent_wrap == nullptr) {
306-
// create a new PromiseWrap for parent promise with silent parameter
307-
parent_wrap = new PromiseWrap(env, parent_promise, true);
308-
parent_wrap->MakeWeak(parent_wrap);
360+
parent_wrap = PromiseWrap::New(env, parent_promise, nullptr, true);
309361
}
310362
// get id from parentWrap
311363
double trigger_id = parent_wrap->get_id();
312364
env->set_init_trigger_id(trigger_id);
313365
}
314-
wrap = new PromiseWrap(env, promise, silent);
315-
wrap->MakeWeak(wrap);
366+
367+
wrap = PromiseWrap::New(env, promise, parent_wrap, silent);
316368
} else if (type == PromiseHookType::kResolve) {
317369
// TODO(matthewloring): need to expose this through the async hooks api.
318370
}
@@ -351,6 +403,22 @@ static void SetupHooks(const FunctionCallbackInfo<Value>& args) {
351403
SET_HOOK_FN(destroy);
352404
env->AddPromiseHook(PromiseHook, nullptr);
353405
#undef SET_HOOK_FN
406+
407+
{
408+
Local<FunctionTemplate> ctor =
409+
FunctionTemplate::New(env->isolate());
410+
ctor->SetClassName(FIXED_ONE_BYTE_STRING(env->isolate(), "PromiseWrap"));
411+
Local<ObjectTemplate> promise_wrap_template = ctor->InstanceTemplate();
412+
promise_wrap_template->SetInternalFieldCount(
413+
PromiseWrap::kInternalFieldCount);
414+
promise_wrap_template->SetAccessor(
415+
FIXED_ONE_BYTE_STRING(env->isolate(), "promise"),
416+
PromiseWrap::GetPromise);
417+
promise_wrap_template->SetAccessor(
418+
FIXED_ONE_BYTE_STRING(env->isolate(), "parentId"),
419+
PromiseWrap::GetParentId);
420+
env->set_promise_wrap_template(promise_wrap_template);
421+
}
354422
}
355423

356424

src/env.h

+1
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,7 @@ namespace node {
268268
V(pipe_constructor_template, v8::FunctionTemplate) \
269269
V(process_object, v8::Object) \
270270
V(promise_reject_function, v8::Function) \
271+
V(promise_wrap_template, v8::ObjectTemplate) \
271272
V(push_values_to_array_function, v8::Function) \
272273
V(randombytes_constructor_template, v8::ObjectTemplate) \
273274
V(script_context_constructor_template, v8::FunctionTemplate) \
+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
'use strict';
2+
const common = require('../common');
3+
const assert = require('assert');
4+
const async_hooks = require('async_hooks');
5+
6+
const initCalls = [];
7+
8+
async_hooks.createHook({
9+
init: common.mustCall((id, type, triggerId, resource) => {
10+
assert.strictEqual(type, 'PROMISE');
11+
initCalls.push({id, triggerId, resource});
12+
}, 2)
13+
}).enable();
14+
15+
const a = Promise.resolve(42);
16+
const b = a.then(common.mustCall());
17+
18+
assert.strictEqual(initCalls[0].triggerId, 1);
19+
assert.strictEqual(initCalls[0].resource.parentId, undefined);
20+
assert.strictEqual(initCalls[0].resource.promise, a);
21+
assert.strictEqual(initCalls[1].triggerId, initCalls[0].id);
22+
assert.strictEqual(initCalls[1].resource.parentId, initCalls[0].id);
23+
assert.strictEqual(initCalls[1].resource.promise, b);

0 commit comments

Comments
 (0)