diagnostics_channel: lazily create tracing context

Default parameters are evaluated before the function body, so the
tracing context is allocated before the no-subscriber fast path.

Initialize it only for subscribed traces. Preserve method arity with an
undefined default and preserve explicit null behavior.

Add focused regression coverage and reusable TracingChannel benchmarks.

Signed-off-by: Romain Lanz <romain.lanz@pm.me>
PR-URL: https://github.com/nodejs/node/pull/65513
Reviewed-By: Chengzhong Wu <legendecas@gmail.com>
Reviewed-By: Vinícius Lourenço Claro Cardoso <contact@viniciusl.com.br>
This commit is contained in:
Romain Lanz authored and GitHub committed 2026-09-02 16:43:10 +00:00
1 parent 8e8ada7f6b
commit 2befec51ae
4 files changed
+191 -2

No files matched your search

@@ -0,0 +1,53 @@
'use strict';
const common = require('../common.js');
const dc = require('node:diagnostics_channel');
const bench = common.createBenchmark(main, {
n: [1e7],
context: ['omitted', 'undefined', 'provided'],
subscribers: [0, 1],
});
function noop() {}
const thenable = {
then(onResolve) {
onResolve(undefined);
},
};
function returnThenable() {
return thenable;
}
function main({ n, context, subscribers }) {
const channel = dc.tracingChannel('test');
const providedContext = { __proto__: null };
if (subscribers) {
channel.subscribe({ start: noop });
}
bench.start();
switch (context) {
case 'omitted':
for (let i = 0; i < n; i++) {
channel.tracePromise(returnThenable);
}
break;
case 'undefined':
for (let i = 0; i < n; i++) {
channel.tracePromise(returnThenable, undefined);
}
break;
case 'provided':
for (let i = 0; i < n; i++) {
channel.tracePromise(returnThenable, providedContext);
}
break;
default:
throw new Error(`Unsupported context value: ${context}`);
}
bench.end(n);
}
@@ -0,0 +1,43 @@
'use strict';
const common = require('../common.js');
const dc = require('node:diagnostics_channel');
const bench = common.createBenchmark(main, {
n: [1e7],
context: ['omitted', 'undefined', 'provided'],
subscribers: [0, 1],
});
function noop() {}
function main({ n, context, subscribers }) {
const channel = dc.tracingChannel('test');
const providedContext = { __proto__: null };
if (subscribers) {
channel.subscribe({ start: noop });
}
bench.start();
switch (context) {
case 'omitted':
for (let i = 0; i < n; i++) {
channel.traceSync(noop);
}
break;
case 'undefined':
for (let i = 0; i < n; i++) {
channel.traceSync(noop, undefined);
}
break;
case 'provided':
for (let i = 0; i < n; i++) {
channel.traceSync(noop, providedContext);
}
break;
default:
throw new Error(`Unsupported context value: ${context}`);
}
bench.end(n);
}
+10 -2
View File
@@ -522,11 +522,15 @@ class TracingChannel {
return done;
}
traceSync(fn, context = { __proto__: null }, thisArg, ...args) {
traceSync(fn, context = undefined, thisArg, ...args) {
if (!this.hasSubscribers) {
return ReflectApply(fn, thisArg, args);
}
if (context === undefined) {
context = { __proto__: null };
}
const { error } = this;
// eslint-disable-next-line no-unused-vars
@@ -542,7 +546,7 @@ class TracingChannel {
}
}
tracePromise(fn, context = { __proto__: null }, thisArg, ...args) {
tracePromise(fn, context = undefined, thisArg, ...args) {
if (!this.hasSubscribers) {
const result = ReflectApply(fn, thisArg, args);
if (typeof result?.then !== 'function') {
@@ -551,6 +555,10 @@ class TracingChannel {
return result;
}
if (context === undefined) {
context = { __proto__: null };
}
const { error } = this;
const continuationWindow = this.#continuationWindow;
@@ -0,0 +1,85 @@
'use strict';
const common = require('../common');
// This test ensures that tracing channels create an independent mutable
// context for each call while preserving explicitly provided contexts.
const assert = require('node:assert');
const dc = require('node:diagnostics_channel');
const lengthChannel = dc.tracingChannel('test:length');
assert.strictEqual(lengthChannel.traceSync.length, 1);
assert.strictEqual(lengthChannel.tracePromise.length, 1);
function subscribe(channel, contexts, isPromise = false) {
let endCalls = 0;
let asyncStartCalls = 0;
let asyncEndCalls = 0;
channel.subscribe({
start: common.mustCall((context) => {
context.invocation = contexts.length;
contexts.push(context);
}, 4),
end: common.mustCall((context) => {
assert.strictEqual(context.invocation, endCalls++);
}, 4),
asyncStart: common.mustCall((context) => {
assert.strictEqual(context.invocation, asyncStartCalls++);
}, isPromise ? 4 : 0),
asyncEnd: common.mustCall((context) => {
assert.strictEqual(context.invocation, asyncEndCalls++);
}, isPromise ? 4 : 0),
});
}
const syncChannel = dc.tracingChannel('test:sync-context');
const syncContexts = [];
subscribe(syncChannel, syncContexts);
const syncProvided = { provided: true };
assert.strictEqual(syncChannel.traceSync(() => 'omitted'), 'omitted');
assert.strictEqual(syncChannel.traceSync(() => 'undefined', undefined),
'undefined');
assert.strictEqual(syncChannel.traceSync(() => 'provided', syncProvided),
'provided');
assert.strictEqual(Object.getPrototypeOf(syncContexts[0]), null);
assert.strictEqual(Object.getPrototypeOf(syncContexts[1]), null);
assert.notStrictEqual(syncContexts[0], syncContexts[1]);
assert.strictEqual(syncContexts[2], syncProvided);
assert.deepStrictEqual(syncContexts.slice(0, 3).map(({ result }) => result),
['omitted', 'undefined', 'provided']);
const syncError = new Error('sync');
assert.throws(() => syncChannel.traceSync(() => {
throw syncError;
}), (error) => error === syncError);
assert.strictEqual(syncContexts[3].error, syncError);
const promiseChannel = dc.tracingChannel('test:promise-context');
const promiseContexts = [];
subscribe(promiseChannel, promiseContexts, true);
const promiseProvided = { provided: true };
const promiseError = new Error('promise');
Promise.resolve()
.then(() => promiseChannel.tracePromise(() => Promise.resolve('omitted')))
.then(() => promiseChannel.tracePromise(
() => Promise.resolve('undefined'), undefined))
.then(() => promiseChannel.tracePromise(
() => Promise.resolve('provided'), promiseProvided))
.then(() => assert.rejects(
promiseChannel.tracePromise(() => Promise.reject(promiseError)),
promiseError))
.then(common.mustCall(() => {
assert.strictEqual(Object.getPrototypeOf(promiseContexts[0]), null);
assert.strictEqual(Object.getPrototypeOf(promiseContexts[1]), null);
assert.notStrictEqual(promiseContexts[0], promiseContexts[1]);
assert.strictEqual(promiseContexts[2], promiseProvided);
assert.deepStrictEqual(
promiseContexts.slice(0, 3).map(({ result }) => result),
['omitted', 'undefined', 'provided']);
assert.strictEqual(promiseContexts[3].error, promiseError);
}));