• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1'use strict';
2
3const {
4  ArrayPrototypeSlice,
5  ErrorCaptureStackTrace,
6  ObjectPrototypeHasOwnProperty,
7  ObjectDefineProperty,
8  Symbol,
9} = primordials;
10
11const async_wrap = internalBinding('async_wrap');
12const { setCallbackTrampoline } = async_wrap;
13/* async_hook_fields is a Uint32Array wrapping the uint32_t array of
14 * Environment::AsyncHooks::fields_[]. Each index tracks the number of active
15 * hooks for each type.
16 *
17 * async_id_fields is a Float64Array wrapping the double array of
18 * Environment::AsyncHooks::async_id_fields_[]. Each index contains the ids for
19 * the various asynchronous states of the application. These are:
20 *  kExecutionAsyncId: The async_id assigned to the resource responsible for the
21 *    current execution stack.
22 *  kTriggerAsyncId: The async_id of the resource that caused (or 'triggered')
23 *    the resource corresponding to the current execution stack.
24 *  kAsyncIdCounter: Incremental counter tracking the next assigned async_id.
25 *  kDefaultTriggerAsyncId: Written immediately before a resource's constructor
26 *    that sets the value of the init()'s triggerAsyncId. The precedence order
27 *    of retrieving the triggerAsyncId value is:
28 *    1. the value passed directly to the constructor
29 *    2. value set in kDefaultTriggerAsyncId
30 *    3. executionAsyncId of the current resource.
31 *
32 * async_ids_stack is a Float64Array that contains part of the async ID
33 * stack. Each pushAsyncContext() call adds two doubles to it, and each
34 * popAsyncContext() call removes two doubles from it.
35 * It has a fixed size, so if that is exceeded, calls to the native
36 * side are used instead in pushAsyncContext() and popAsyncContext().
37 */
38const {
39  async_hook_fields,
40  async_id_fields,
41  execution_async_resources
42} = async_wrap;
43// Store the pair executionAsyncId and triggerAsyncId in a AliasedFloat64Array
44// in Environment::AsyncHooks::async_ids_stack_ which tracks the resource
45// responsible for the current execution stack. This is unwound as each resource
46// exits. In the case of a fatal exception this stack is emptied after calling
47// each hook's after() callback.
48const {
49  pushAsyncContext: pushAsyncContext_,
50  popAsyncContext: popAsyncContext_,
51  executionAsyncResource: executionAsyncResource_,
52  clearAsyncIdStack,
53} = async_wrap;
54// For performance reasons, only track Promises when a hook is enabled.
55const { setPromiseHooks } = async_wrap;
56// Properties in active_hooks are used to keep track of the set of hooks being
57// executed in case another hook is enabled/disabled. The new set of hooks is
58// then restored once the active set of hooks is finished executing.
59const active_hooks = {
60  // Array of all AsyncHooks that will be iterated whenever an async event
61  // fires. Using var instead of (preferably const) in order to assign
62  // active_hooks.tmp_array if a hook is enabled/disabled during hook
63  // execution.
64  array: [],
65  // Use a counter to track nested calls of async hook callbacks and make sure
66  // the active_hooks.array isn't altered mid execution.
67  call_depth: 0,
68  // Use to temporarily store and updated active_hooks.array if the user
69  // enables or disables a hook while hooks are being processed. If a hook is
70  // enabled() or disabled() during hook execution then the current set of
71  // active hooks is duplicated and set equal to active_hooks.tmp_array. Any
72  // subsequent changes are on the duplicated array. When all hooks have
73  // completed executing active_hooks.tmp_array is assigned to
74  // active_hooks.array.
75  tmp_array: null,
76  // Keep track of the field counts held in active_hooks.tmp_array. Because the
77  // async_hook_fields can't be reassigned, store each uint32 in an array that
78  // is written back to async_hook_fields when active_hooks.array is restored.
79  tmp_fields: null
80};
81
82const { registerDestroyHook } = async_wrap;
83const { enqueueMicrotask } = internalBinding('task_queue');
84const { resource_symbol, owner_symbol } = internalBinding('symbols');
85
86// Each constant tracks how many callbacks there are for any given step of
87// async execution. These are tracked so if the user didn't include callbacks
88// for a given step, that step can bail out early.
89const {
90  kInit, kBefore, kAfter, kDestroy, kTotals, kPromiseResolve,
91  kCheck, kExecutionAsyncId, kAsyncIdCounter, kTriggerAsyncId,
92  kDefaultTriggerAsyncId, kStackLength, kUsesExecutionAsyncResource,
93} = async_wrap.constants;
94
95const { async_id_symbol,
96        trigger_async_id_symbol } = internalBinding('symbols');
97
98// Lazy load of internal/util/inspect;
99let inspect;
100
101// Used in AsyncHook and AsyncResource.
102const init_symbol = Symbol('init');
103const before_symbol = Symbol('before');
104const after_symbol = Symbol('after');
105const destroy_symbol = Symbol('destroy');
106const promise_resolve_symbol = Symbol('promiseResolve');
107const emitBeforeNative = emitHookFactory(before_symbol, 'emitBeforeNative');
108const emitAfterNative = emitHookFactory(after_symbol, 'emitAfterNative');
109const emitDestroyNative = emitHookFactory(destroy_symbol, 'emitDestroyNative');
110const emitPromiseResolveNative =
111    emitHookFactory(promise_resolve_symbol, 'emitPromiseResolveNative');
112
113let domain_cb;
114function useDomainTrampoline(fn) {
115  domain_cb = fn;
116}
117
118function callbackTrampoline(asyncId, resource, cb, ...args) {
119  const index = async_hook_fields[kStackLength] - 1;
120  execution_async_resources[index] = resource;
121
122  if (asyncId !== 0 && hasHooks(kBefore))
123    emitBeforeNative(asyncId);
124
125  let result;
126  if (asyncId === 0 && typeof domain_cb === 'function') {
127    args.unshift(cb);
128    result = domain_cb.apply(this, args);
129  } else {
130    result = cb.apply(this, args);
131  }
132
133  if (asyncId !== 0 && hasHooks(kAfter))
134    emitAfterNative(asyncId);
135
136  execution_async_resources.pop();
137  return result;
138}
139
140setCallbackTrampoline(callbackTrampoline);
141
142const topLevelResource = {};
143
144function executionAsyncResource() {
145  // Indicate to the native layer that this function is likely to be used,
146  // in which case it will inform JS about the current async resource via
147  // the trampoline above.
148  async_hook_fields[kUsesExecutionAsyncResource] = 1;
149
150  const index = async_hook_fields[kStackLength] - 1;
151  if (index === -1) return topLevelResource;
152  const resource = execution_async_resources[index] ||
153    executionAsyncResource_(index);
154  return lookupPublicResource(resource);
155}
156
157function inspectExceptionValue(e) {
158  inspect = inspect ?? require('internal/util/inspect').inspect;
159  return { message: inspect(e) };
160}
161
162// Used to fatally abort the process if a callback throws.
163function fatalError(e) {
164  if (typeof e?.stack === 'string') {
165    process._rawDebug(e.stack);
166  } else {
167    const o = inspectExceptionValue(e);
168    ErrorCaptureStackTrace(o, fatalError);
169    process._rawDebug(o.stack);
170  }
171
172  const { getOptionValue } = require('internal/options');
173  if (getOptionValue('--abort-on-uncaught-exception')) {
174    process.abort();
175  }
176  process.exit(1);
177}
178
179function lookupPublicResource(resource) {
180  if (typeof resource !== 'object' || resource === null) return resource;
181  // TODO(addaleax): Merge this with owner_symbol and use it across all
182  // AsyncWrap instances.
183  const publicResource = resource[resource_symbol];
184  if (publicResource !== undefined)
185    return publicResource;
186  return resource;
187}
188
189// Emit From Native //
190
191// Used by C++ to call all init() callbacks. Because some state can be setup
192// from C++ there's no need to perform all the same operations as in
193// emitInitScript.
194function emitInitNative(asyncId, type, triggerAsyncId, resource) {
195  active_hooks.call_depth += 1;
196  resource = lookupPublicResource(resource);
197  // Use a single try/catch for all hooks to avoid setting up one per iteration.
198  try {
199    // Using var here instead of let because "for (var ...)" is faster than let.
200    // Refs: https://github.com/nodejs/node/pull/30380#issuecomment-552948364
201    for (var i = 0; i < active_hooks.array.length; i++) {
202      if (typeof active_hooks.array[i][init_symbol] === 'function') {
203        active_hooks.array[i][init_symbol](
204          asyncId, type, triggerAsyncId,
205          resource
206        );
207      }
208    }
209  } catch (e) {
210    fatalError(e);
211  } finally {
212    active_hooks.call_depth -= 1;
213  }
214
215  // Hooks can only be restored if there have been no recursive hook calls.
216  // Also the active hooks do not need to be restored if enable()/disable()
217  // weren't called during hook execution, in which case active_hooks.tmp_array
218  // will be null.
219  if (active_hooks.call_depth === 0 && active_hooks.tmp_array !== null) {
220    restoreActiveHooks();
221  }
222}
223
224// Called from native. The asyncId stack handling is taken care of there
225// before this is called.
226function emitHook(symbol, asyncId) {
227  active_hooks.call_depth += 1;
228  // Use a single try/catch for all hook to avoid setting up one per
229  // iteration.
230  try {
231    // Using var here instead of let because "for (var ...)" is faster than let.
232    // Refs: https://github.com/nodejs/node/pull/30380#issuecomment-552948364
233    for (var i = 0; i < active_hooks.array.length; i++) {
234      if (typeof active_hooks.array[i][symbol] === 'function') {
235        active_hooks.array[i][symbol](asyncId);
236      }
237    }
238  } catch (e) {
239    fatalError(e);
240  } finally {
241    active_hooks.call_depth -= 1;
242  }
243
244  // Hooks can only be restored if there have been no recursive hook calls.
245  // Also the active hooks do not need to be restored if enable()/disable()
246  // weren't called during hook execution, in which case
247  // active_hooks.tmp_array will be null.
248  if (active_hooks.call_depth === 0 && active_hooks.tmp_array !== null) {
249    restoreActiveHooks();
250  }
251}
252
253function emitHookFactory(symbol, name) {
254  const fn = emitHook.bind(undefined, symbol);
255
256  // Set the name property of the function as it looks good in the stack trace.
257  ObjectDefineProperty(fn, 'name', {
258    value: name
259  });
260  return fn;
261}
262
263// Manage Active Hooks //
264
265function getHookArrays() {
266  if (active_hooks.call_depth === 0)
267    return [active_hooks.array, async_hook_fields];
268  // If this hook is being enabled while in the middle of processing the array
269  // of currently active hooks then duplicate the current set of active hooks
270  // and store this there. This shouldn't fire until the next time hooks are
271  // processed.
272  if (active_hooks.tmp_array === null)
273    storeActiveHooks();
274  return [active_hooks.tmp_array, active_hooks.tmp_fields];
275}
276
277
278function storeActiveHooks() {
279  active_hooks.tmp_array = ArrayPrototypeSlice(active_hooks.array);
280  // Don't want to make the assumption that kInit to kDestroy are indexes 0 to
281  // 4. So do this the long way.
282  active_hooks.tmp_fields = [];
283  copyHooks(active_hooks.tmp_fields, async_hook_fields);
284}
285
286function copyHooks(destination, source) {
287  destination[kInit] = source[kInit];
288  destination[kBefore] = source[kBefore];
289  destination[kAfter] = source[kAfter];
290  destination[kDestroy] = source[kDestroy];
291  destination[kPromiseResolve] = source[kPromiseResolve];
292}
293
294
295// Then restore the correct hooks array in case any hooks were added/removed
296// during hook callback execution.
297function restoreActiveHooks() {
298  active_hooks.array = active_hooks.tmp_array;
299  copyHooks(async_hook_fields, active_hooks.tmp_fields);
300
301  active_hooks.tmp_array = null;
302  active_hooks.tmp_fields = null;
303}
304
305function trackPromise(promise, parent) {
306  if (promise[async_id_symbol]) {
307    return;
308  }
309
310  // Get trigger id from parent async id before making the async id of the
311  // child so if a new one must be made it will be lower than the child.
312  const triggerAsyncId = parent ? getOrSetAsyncId(parent) :
313    getDefaultTriggerAsyncId();
314
315  promise[async_id_symbol] = newAsyncId();
316  promise[trigger_async_id_symbol] = triggerAsyncId;
317}
318
319function promiseInitHook(promise, parent) {
320  trackPromise(promise, parent);
321  const asyncId = promise[async_id_symbol];
322  const triggerAsyncId = promise[trigger_async_id_symbol];
323  emitInitScript(asyncId, 'PROMISE', triggerAsyncId, promise);
324}
325
326function promiseInitHookWithDestroyTracking(promise, parent) {
327  promiseInitHook(promise, parent);
328  destroyTracking(promise, parent);
329}
330
331const destroyedSymbol = Symbol('destroyed');
332
333function destroyTracking(promise, parent) {
334  trackPromise(promise, parent);
335  const asyncId = promise[async_id_symbol];
336  const destroyed = { destroyed: false };
337  promise[destroyedSymbol] = destroyed;
338  registerDestroyHook(promise, asyncId, destroyed);
339}
340
341function promiseBeforeHook(promise) {
342  trackPromise(promise);
343  const asyncId = promise[async_id_symbol];
344  const triggerId = promise[trigger_async_id_symbol];
345  emitBeforeScript(asyncId, triggerId, promise);
346}
347
348function promiseAfterHook(promise) {
349  trackPromise(promise);
350  const asyncId = promise[async_id_symbol];
351  if (hasHooks(kAfter)) {
352    emitAfterNative(asyncId);
353  }
354  if (asyncId === executionAsyncId()) {
355    // This condition might not be true if async_hooks was enabled during
356    // the promise callback execution.
357    // Popping it off the stack can be skipped in that case, because it is
358    // known that it would correspond to exactly one call with
359    // PromiseHookType::kBefore that was not witnessed by the PromiseHook.
360    popAsyncContext(asyncId);
361  }
362}
363
364function promiseResolveHook(promise) {
365  trackPromise(promise);
366  const asyncId = promise[async_id_symbol];
367  emitPromiseResolveNative(asyncId);
368}
369
370let wantPromiseHook = false;
371function enableHooks() {
372  async_hook_fields[kCheck] += 1;
373}
374
375function updatePromiseHookMode() {
376  wantPromiseHook = true;
377  let initHook;
378  if (initHooksExist()) {
379    initHook = destroyHooksExist() ? promiseInitHookWithDestroyTracking :
380      promiseInitHook;
381  } else if (destroyHooksExist()) {
382    initHook = destroyTracking;
383  }
384  setPromiseHooks(
385    initHook,
386    promiseBeforeHook,
387    promiseAfterHook,
388    promiseResolveHooksExist() ? promiseResolveHook : undefined,
389  );
390}
391
392function disableHooks() {
393  async_hook_fields[kCheck] -= 1;
394
395  wantPromiseHook = false;
396
397  // Delay the call to `disablePromiseHook()` because we might currently be
398  // between the `before` and `after` calls of a Promise.
399  enqueueMicrotask(disablePromiseHookIfNecessary);
400}
401
402function disablePromiseHookIfNecessary() {
403  if (!wantPromiseHook) {
404    setPromiseHooks(undefined, undefined, undefined, undefined);
405  }
406}
407
408// Internal Embedder API //
409
410// Increment the internal id counter and return the value. Important that the
411// counter increment first. Since it's done the same way in
412// Environment::new_async_uid()
413function newAsyncId() {
414  return ++async_id_fields[kAsyncIdCounter];
415}
416
417function getOrSetAsyncId(object) {
418  if (ObjectPrototypeHasOwnProperty(object, async_id_symbol)) {
419    return object[async_id_symbol];
420  }
421
422  return object[async_id_symbol] = newAsyncId();
423}
424
425
426// Return the triggerAsyncId meant for the constructor calling it. It's up to
427// the user to safeguard this call and make sure it's zero'd out when the
428// constructor is complete.
429function getDefaultTriggerAsyncId() {
430  const defaultTriggerAsyncId = async_id_fields[kDefaultTriggerAsyncId];
431  // If defaultTriggerAsyncId isn't set, use the executionAsyncId
432  if (defaultTriggerAsyncId < 0)
433    return async_id_fields[kExecutionAsyncId];
434  return defaultTriggerAsyncId;
435}
436
437
438function clearDefaultTriggerAsyncId() {
439  async_id_fields[kDefaultTriggerAsyncId] = -1;
440}
441
442
443function defaultTriggerAsyncIdScope(triggerAsyncId, block, ...args) {
444  if (triggerAsyncId === undefined)
445    return block.apply(null, args);
446  // CHECK(NumberIsSafeInteger(triggerAsyncId))
447  // CHECK(triggerAsyncId > 0)
448  const oldDefaultTriggerAsyncId = async_id_fields[kDefaultTriggerAsyncId];
449  async_id_fields[kDefaultTriggerAsyncId] = triggerAsyncId;
450
451  try {
452    return block.apply(null, args);
453  } finally {
454    async_id_fields[kDefaultTriggerAsyncId] = oldDefaultTriggerAsyncId;
455  }
456}
457
458function hasHooks(key) {
459  return async_hook_fields[key] > 0;
460}
461
462function enabledHooksExist() {
463  return hasHooks(kCheck);
464}
465
466function initHooksExist() {
467  return hasHooks(kInit);
468}
469
470function afterHooksExist() {
471  return hasHooks(kAfter);
472}
473
474function destroyHooksExist() {
475  return hasHooks(kDestroy);
476}
477
478function promiseResolveHooksExist() {
479  return hasHooks(kPromiseResolve);
480}
481
482
483function emitInitScript(asyncId, type, triggerAsyncId, resource) {
484  // Short circuit all checks for the common case. Which is that no hooks have
485  // been set. Do this to remove performance impact for embedders (and core).
486  if (!hasHooks(kInit))
487    return;
488
489  if (triggerAsyncId === null) {
490    triggerAsyncId = getDefaultTriggerAsyncId();
491  }
492
493  emitInitNative(asyncId, type, triggerAsyncId, resource);
494}
495
496
497function emitBeforeScript(asyncId, triggerAsyncId, resource) {
498  pushAsyncContext(asyncId, triggerAsyncId, resource);
499
500  if (hasHooks(kBefore))
501    emitBeforeNative(asyncId);
502}
503
504
505function emitAfterScript(asyncId) {
506  if (hasHooks(kAfter))
507    emitAfterNative(asyncId);
508
509  popAsyncContext(asyncId);
510}
511
512
513function emitDestroyScript(asyncId) {
514  // Return early if there are no destroy callbacks, or invalid asyncId.
515  if (!hasHooks(kDestroy) || asyncId <= 0)
516    return;
517  async_wrap.queueDestroyAsyncId(asyncId);
518}
519
520
521function hasAsyncIdStack() {
522  return hasHooks(kStackLength);
523}
524
525
526// This is the equivalent of the native push_async_ids() call.
527function pushAsyncContext(asyncId, triggerAsyncId, resource) {
528  const offset = async_hook_fields[kStackLength];
529  execution_async_resources[offset] = resource;
530  if (offset * 2 >= async_wrap.async_ids_stack.length)
531    return pushAsyncContext_(asyncId, triggerAsyncId);
532  async_wrap.async_ids_stack[offset * 2] = async_id_fields[kExecutionAsyncId];
533  async_wrap.async_ids_stack[offset * 2 + 1] = async_id_fields[kTriggerAsyncId];
534  async_hook_fields[kStackLength]++;
535  async_id_fields[kExecutionAsyncId] = asyncId;
536  async_id_fields[kTriggerAsyncId] = triggerAsyncId;
537}
538
539
540// This is the equivalent of the native pop_async_ids() call.
541function popAsyncContext(asyncId) {
542  const stackLength = async_hook_fields[kStackLength];
543  if (stackLength === 0) return false;
544
545  if (enabledHooksExist() && async_id_fields[kExecutionAsyncId] !== asyncId) {
546    // Do the same thing as the native code (i.e. crash hard).
547    return popAsyncContext_(asyncId);
548  }
549
550  const offset = stackLength - 1;
551  async_id_fields[kExecutionAsyncId] = async_wrap.async_ids_stack[2 * offset];
552  async_id_fields[kTriggerAsyncId] = async_wrap.async_ids_stack[2 * offset + 1];
553  execution_async_resources.pop();
554  async_hook_fields[kStackLength] = offset;
555  return offset > 0;
556}
557
558
559function executionAsyncId() {
560  return async_id_fields[kExecutionAsyncId];
561}
562
563function triggerAsyncId() {
564  return async_id_fields[kTriggerAsyncId];
565}
566
567
568module.exports = {
569  executionAsyncId,
570  triggerAsyncId,
571  // Private API
572  getHookArrays,
573  symbols: {
574    async_id_symbol, trigger_async_id_symbol,
575    init_symbol, before_symbol, after_symbol, destroy_symbol,
576    promise_resolve_symbol, owner_symbol
577  },
578  constants: {
579    kInit, kBefore, kAfter, kDestroy, kTotals, kPromiseResolve
580  },
581  enableHooks,
582  disableHooks,
583  updatePromiseHookMode,
584  clearDefaultTriggerAsyncId,
585  clearAsyncIdStack,
586  hasAsyncIdStack,
587  executionAsyncResource,
588  // Internal Embedder API
589  newAsyncId,
590  getOrSetAsyncId,
591  getDefaultTriggerAsyncId,
592  defaultTriggerAsyncIdScope,
593  enabledHooksExist,
594  initHooksExist,
595  afterHooksExist,
596  destroyHooksExist,
597  emitInit: emitInitScript,
598  emitBefore: emitBeforeScript,
599  emitAfter: emitAfterScript,
600  emitDestroy: emitDestroyScript,
601  registerDestroyHook,
602  useDomainTrampoline,
603  nativeHooks: {
604    init: emitInitNative,
605    before: emitBeforeNative,
606    after: emitAfterNative,
607    destroy: emitDestroyNative,
608    promise_resolve: emitPromiseResolveNative
609  }
610};
611