From b86b81ac1b91dc0d12cc820da7ee510d405c60d1 Mon Sep 17 00:00:00 2001 From: Romain Lanz Date: Mon, 24 Aug 2026 12:13:42 +0200 Subject: [PATCH] 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 --- .../tracing-channel-promise.js | 53 ++++++++++++ .../tracing-channel-sync.js | 43 ++++++++++ lib/diagnostics_channel.js | 12 ++- ...nostics-channel-tracing-channel-context.js | 85 +++++++++++++++++++ 4 files changed, 191 insertions(+), 2 deletions(-) create mode 100644 benchmark/diagnostics_channel/tracing-channel-promise.js create mode 100644 benchmark/diagnostics_channel/tracing-channel-sync.js create mode 100644 test/parallel/test-diagnostics-channel-tracing-channel-context.js diff --git a/benchmark/diagnostics_channel/tracing-channel-promise.js b/benchmark/diagnostics_channel/tracing-channel-promise.js new file mode 100644 index 000000000000..594043d63fe4 --- /dev/null +++ b/benchmark/diagnostics_channel/tracing-channel-promise.js @@ -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); +} diff --git a/benchmark/diagnostics_channel/tracing-channel-sync.js b/benchmark/diagnostics_channel/tracing-channel-sync.js new file mode 100644 index 000000000000..bfd9dd939cc0 --- /dev/null +++ b/benchmark/diagnostics_channel/tracing-channel-sync.js @@ -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); +} diff --git a/lib/diagnostics_channel.js b/lib/diagnostics_channel.js index 7c1f4fa845ed..54c25839e611 100644 --- a/lib/diagnostics_channel.js +++ b/lib/diagnostics_channel.js @@ -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; diff --git a/test/parallel/test-diagnostics-channel-tracing-channel-context.js b/test/parallel/test-diagnostics-channel-tracing-channel-context.js new file mode 100644 index 000000000000..e0e605854032 --- /dev/null +++ b/test/parallel/test-diagnostics-channel-tracing-channel-context.js @@ -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); + }));