Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 9128df4

Browse files
author
Stephen Belanger
committedDec 15, 2022
lib: add tracing channel to diagnostics_channel
1 parent 22c645d commit 9128df4

8 files changed

+984
-21
lines changed
 

‎doc/api/diagnostics_channel.md

+472
Large diffs are not rendered by default.

‎lib/diagnostics_channel.js

+248-21
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,14 @@ const {
44
ArrayPrototypeIndexOf,
55
ArrayPrototypePush,
66
ArrayPrototypeSplice,
7+
FunctionPrototypeBind,
78
ObjectCreate,
89
ObjectGetPrototypeOf,
910
ObjectSetPrototypeOf,
11+
PromisePrototypeThen,
12+
PromiseReject,
13+
ReflectApply,
14+
SafeMap,
1015
SymbolHasInstance,
1116
} = primordials;
1217

@@ -23,11 +28,40 @@ const { triggerUncaughtException } = internalBinding('errors');
2328

2429
const { WeakReference } = internalBinding('util');
2530

31+
function decRef(channel) {
32+
channel._weak.decRef();
33+
if (channel._weak.getRef() === 0) {
34+
delete channels[channel.name];
35+
}
36+
}
37+
38+
function markActive(channel) {
39+
// eslint-disable-next-line no-use-before-define
40+
ObjectSetPrototypeOf(channel, ActiveChannel.prototype);
41+
channel._subscribers = [];
42+
channel._stores = new SafeMap();
43+
}
44+
45+
function maybeMarkInactive(channel) {
46+
// When there are no more active subscribers, restore to fast prototype.
47+
if (!channel._subscribers.length && !channel._stores.size) {
48+
// eslint-disable-next-line no-use-before-define
49+
ObjectSetPrototypeOf(channel, Channel.prototype);
50+
channel._subscribers = undefined;
51+
channel._stores = undefined;
52+
}
53+
}
54+
55+
function wrapStoreRun(store, data, next, transform = (v) => v) {
56+
return () => store.run(transform(data), next);
57+
}
58+
2659
// TODO(qard): should there be a C++ channel interface?
2760
class ActiveChannel {
2861
subscribe(subscription) {
2962
validateFunction(subscription, 'subscription');
3063
ArrayPrototypePush(this._subscribers, subscription);
64+
this._weak.incRef();
3165
}
3266

3367
unsubscribe(subscription) {
@@ -36,12 +70,28 @@ class ActiveChannel {
3670

3771
ArrayPrototypeSplice(this._subscribers, index, 1);
3872

39-
// When there are no more active subscribers, restore to fast prototype.
40-
if (!this._subscribers.length) {
41-
// eslint-disable-next-line no-use-before-define
42-
ObjectSetPrototypeOf(this, Channel.prototype);
73+
decRef(this);
74+
maybeMarkInactive(this);
75+
76+
return true;
77+
}
78+
79+
bindStore(store, transform) {
80+
const replacing = this._stores.has(store);
81+
if (!replacing) this._weak.incRef();
82+
this._stores.set(store, transform);
83+
}
84+
85+
unbindStore(store) {
86+
if (!this._stores.has(store)) {
87+
return false;
4388
}
4489

90+
this._stores.delete(store);
91+
92+
decRef(this);
93+
maybeMarkInactive(this);
94+
4595
return true;
4696
}
4797

@@ -61,11 +111,28 @@ class ActiveChannel {
61111
}
62112
}
63113
}
114+
115+
runStores(data, fn, thisArg, ...args) {
116+
this.publish(data);
117+
118+
// Bind base fn first due to AsyncLocalStorage.run not having thisArg
119+
fn = FunctionPrototypeBind(fn, thisArg, ...args);
120+
121+
for (const entry of this._stores.entries()) {
122+
const store = entry[0];
123+
const transform = entry[1];
124+
fn = wrapStoreRun(store, data, fn, transform);
125+
}
126+
127+
return fn();
128+
}
64129
}
65130

66131
class Channel {
67132
constructor(name) {
68133
this._subscribers = undefined;
134+
this._stores = undefined;
135+
this._weak = undefined;
69136
this.name = name;
70137
}
71138

@@ -76,20 +143,32 @@ class Channel {
76143
}
77144

78145
subscribe(subscription) {
79-
ObjectSetPrototypeOf(this, ActiveChannel.prototype);
80-
this._subscribers = [];
146+
markActive(this);
81147
this.subscribe(subscription);
82148
}
83149

84150
unsubscribe() {
85151
return false;
86152
}
87153

154+
bindStore(store, transform = (v) => v) {
155+
markActive(this);
156+
this.bindStore(store, transform);
157+
}
158+
159+
unbindStore() {
160+
return false;
161+
}
162+
88163
get hasSubscribers() {
89164
return false;
90165
}
91166

92167
publish() {}
168+
169+
runStores(data, fn, thisArg, ...args) {
170+
return ReflectApply(fn, thisArg, args);
171+
}
93172
}
94173

95174
const channels = ObjectCreate(null);
@@ -105,27 +184,17 @@ function channel(name) {
105184
}
106185

107186
channel = new Channel(name);
108-
channels[name] = new WeakReference(channel);
187+
channel._weak = new WeakReference(channel);
188+
channels[name] = channel._weak;
109189
return channel;
110190
}
111191

112192
function subscribe(name, subscription) {
113-
const chan = channel(name);
114-
channels[name].incRef();
115-
chan.subscribe(subscription);
193+
return channel(name).subscribe(subscription);
116194
}
117195

118196
function unsubscribe(name, subscription) {
119-
const chan = channel(name);
120-
if (!chan.unsubscribe(subscription)) {
121-
return false;
122-
}
123-
124-
channels[name].decRef();
125-
if (channels[name].getRef() === 0) {
126-
delete channels[name];
127-
}
128-
return true;
197+
return channel(name).unsubscribe(subscription);
129198
}
130199

131200
function hasSubscribers(name) {
@@ -139,10 +208,168 @@ function hasSubscribers(name) {
139208
return channel.hasSubscribers;
140209
}
141210

211+
const traceEvents = [
212+
'start',
213+
'end',
214+
'asyncStart',
215+
'asyncEnd',
216+
'error',
217+
];
218+
219+
function assertChannel(value, name) {
220+
if (!(value instanceof Channel)) {
221+
throw new ERR_INVALID_ARG_TYPE(name, ['Channel'], value);
222+
}
223+
}
224+
225+
class TracingChannel {
226+
constructor(nameOrChannels) {
227+
if (typeof nameOrChannels === 'string') {
228+
this.start = channel(`tracing:${nameOrChannels}:start`);
229+
this.end = channel(`tracing:${nameOrChannels}:end`);
230+
this.asyncStart = channel(`tracing:${nameOrChannels}:asyncStart`);
231+
this.asyncEnd = channel(`tracing:${nameOrChannels}:asyncEnd`);
232+
this.error = channel(`tracing:${nameOrChannels}:error`);
233+
} else if (typeof nameOrChannels === 'object') {
234+
const { start, end, asyncStart, asyncEnd, error } = nameOrChannels;
235+
236+
assertChannel(start, 'nameOrChannels.start');
237+
assertChannel(end, 'nameOrChannels.end');
238+
assertChannel(asyncStart, 'nameOrChannels.asyncStart');
239+
assertChannel(asyncEnd, 'nameOrChannels.asyncEnd');
240+
assertChannel(error, 'nameOrChannels.error');
241+
242+
this.start = start;
243+
this.end = end;
244+
this.asyncStart = asyncStart;
245+
this.asyncEnd = asyncEnd;
246+
this.error = error;
247+
} else {
248+
throw new ERR_INVALID_ARG_TYPE('nameOrChannels',
249+
['string', 'object', 'Channel'],
250+
nameOrChannels);
251+
}
252+
}
253+
254+
subscribe(handlers) {
255+
for (const name of traceEvents) {
256+
if (!handlers[name]) continue;
257+
258+
this[name]?.subscribe(handlers[name]);
259+
}
260+
}
261+
262+
unsubscribe(handlers) {
263+
let done = true;
264+
265+
for (const name of traceEvents) {
266+
if (!handlers[name]) continue;
267+
268+
if (!this[name]?.unsubscribe(handlers[name])) {
269+
done = false;
270+
}
271+
}
272+
273+
return done;
274+
}
275+
276+
traceSync(fn, ctx = {}, thisArg, ...args) {
277+
const { start, end, error } = this;
278+
279+
try {
280+
const result = start.runStores(ctx, fn, thisArg, ...args);
281+
ctx.result = result;
282+
return result;
283+
} catch (err) {
284+
ctx.error = err;
285+
error.publish(ctx);
286+
throw err;
287+
} finally {
288+
end.publish(ctx);
289+
}
290+
}
291+
292+
tracePromise(fn, ctx = {}, thisArg, ...args) {
293+
const { start, end, asyncStart, asyncEnd, error } = this;
294+
295+
function reject(err) {
296+
ctx.error = err;
297+
error.publish(ctx);
298+
asyncStart.publish(ctx);
299+
// TODO: Is there a way to have asyncEnd _after_ the continuation?
300+
asyncEnd.publish(ctx);
301+
return PromiseReject(err);
302+
}
303+
304+
function resolve(result) {
305+
ctx.result = result;
306+
asyncStart.publish(ctx);
307+
// TODO: Is there a way to have asyncEnd _after_ the continuation?
308+
asyncEnd.publish(ctx);
309+
return result;
310+
}
311+
312+
try {
313+
const promise = start.runStores(ctx, fn, thisArg, ...args);
314+
return PromisePrototypeThen(promise, resolve, reject);
315+
} catch (err) {
316+
ctx.error = err;
317+
error.publish(ctx);
318+
throw err;
319+
} finally {
320+
end.publish(ctx);
321+
}
322+
}
323+
324+
traceCallback(fn, position = 0, ctx = {}, thisArg, ...args) {
325+
const { start, end, asyncStart, asyncEnd, error } = this;
326+
327+
function wrap(fn) {
328+
return function wrappedCallback(err, res) {
329+
if (err) {
330+
ctx.error = err;
331+
error.publish(ctx);
332+
} else {
333+
ctx.result = res;
334+
}
335+
336+
asyncStart.publish(ctx);
337+
try {
338+
if (fn) {
339+
return ReflectApply(fn, this, arguments);
340+
}
341+
} finally {
342+
asyncEnd.publish(ctx);
343+
}
344+
};
345+
}
346+
347+
if (position >= 0) {
348+
args.splice(position, 1, wrap(args.at(position)));
349+
}
350+
351+
try {
352+
return start.runStores(ctx, fn, thisArg, ...args);
353+
} catch (err) {
354+
ctx.error = err;
355+
error.publish(ctx);
356+
throw err;
357+
} finally {
358+
end.publish(ctx);
359+
}
360+
}
361+
}
362+
363+
function tracingChannel(nameOrChannels) {
364+
return new TracingChannel(nameOrChannels);
365+
}
366+
142367
module.exports = {
143368
channel,
144369
hasSubscribers,
145370
subscribe,
371+
tracingChannel,
146372
unsubscribe,
147-
Channel
373+
Channel,
374+
TracingChannel
148375
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
const assert = require('assert');
5+
const dc = require('diagnostics_channel');
6+
const { AsyncLocalStorage } = require('async_hooks');
7+
8+
let n = 0;
9+
const thisArg = new Date();
10+
const inputs = [
11+
{ foo: 'bar' },
12+
{ baz: 'buz' },
13+
];
14+
15+
const channel = dc.channel('test');
16+
17+
// Bind a storage directly to published data
18+
const store1 = new AsyncLocalStorage();
19+
channel.bindStore(store1);
20+
21+
// Bind a store with transformation of published data
22+
const store2 = new AsyncLocalStorage();
23+
channel.bindStore(store2, common.mustCall((data) => {
24+
assert.deepStrictEqual(data, inputs[n]);
25+
return { data };
26+
}, 3));
27+
28+
// Regular subscribers should see publishes from runStores calls
29+
channel.subscribe(common.mustCall((data) => {
30+
assert.deepStrictEqual(data, inputs[n]);
31+
}, 3));
32+
33+
// Verify stores are empty before run
34+
assert.strictEqual(store1.getStore(), undefined);
35+
assert.strictEqual(store2.getStore(), undefined);
36+
37+
channel.runStores(inputs[n], common.mustCall(function(a, b) {
38+
// Verify this and argument forwarding
39+
assert.deepStrictEqual(this, thisArg);
40+
assert.strictEqual(a, 1);
41+
assert.strictEqual(b, 2);
42+
43+
// Verify store 1 state matches input
44+
assert.deepStrictEqual(store1.getStore(), inputs[n]);
45+
46+
// Verify store 2 state has expected transformation
47+
assert.deepStrictEqual(store2.getStore(), { data: inputs[n] });
48+
49+
// Should support nested contexts
50+
n++;
51+
channel.runStores(inputs[n], common.mustCall(function() {
52+
// Verify this and argument forwarding
53+
assert.strictEqual(this, undefined);
54+
55+
// Verify store 1 state matches input
56+
assert.deepStrictEqual(store1.getStore(), inputs[n]);
57+
58+
// Verify store 2 state has expected transformation
59+
assert.deepStrictEqual(store2.getStore(), { data: inputs[n] });
60+
}));
61+
n--;
62+
63+
// Verify store 1 state matches input
64+
assert.deepStrictEqual(store1.getStore(), inputs[n]);
65+
66+
// Verify store 2 state has expected transformation
67+
assert.deepStrictEqual(store2.getStore(), { data: inputs[n] });
68+
}), thisArg, 1, 2);
69+
70+
// Verify stores are empty after run
71+
assert.strictEqual(store1.getStore(), undefined);
72+
assert.strictEqual(store2.getStore(), undefined);
73+
74+
// Verify unbinding works
75+
assert.ok(channel.unbindStore(store1));
76+
77+
// Verify unbinding a store that is not bound returns false
78+
assert.ok(!channel.unbindStore(store1));
79+
80+
n++;
81+
channel.runStores(inputs[n], common.mustCall(() => {
82+
// Verify after unbinding store 1 will remain undefined
83+
assert.strictEqual(store1.getStore(), undefined);
84+
85+
// Verify still bound store 2 receives expected data
86+
assert.deepStrictEqual(store2.getStore(), { data: inputs[n] });
87+
}));
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
const dc = require('diagnostics_channel');
5+
const assert = require('assert');
6+
7+
const channel = dc.tracingChannel('test');
8+
9+
const expectedError = new Error('test');
10+
const input = { foo: 'bar' };
11+
const thisArg = { baz: 'buz' };
12+
13+
function check(found) {
14+
assert.deepStrictEqual(found, input);
15+
}
16+
17+
const handlers = {
18+
start: common.mustCall(check, 2),
19+
end: common.mustCall(check, 2),
20+
asyncStart: common.mustCall(check, 2),
21+
asyncEnd: common.mustCall(check, 2),
22+
error: common.mustCall((found) => {
23+
check(found);
24+
assert.deepStrictEqual(found.error, expectedError);
25+
}, 2)
26+
};
27+
28+
channel.subscribe(handlers);
29+
30+
channel.traceCallback(function(cb, err) {
31+
assert.deepStrictEqual(this, thisArg);
32+
setImmediate(cb, err);
33+
}, 0, input, thisArg, common.mustCall((err, res) => {
34+
assert.strictEqual(err, expectedError);
35+
assert.strictEqual(res, undefined);
36+
}), expectedError);
37+
38+
channel.tracePromise(function(value) {
39+
assert.deepStrictEqual(this, thisArg);
40+
return Promise.reject(value);
41+
}, input, thisArg, expectedError).then(
42+
common.mustNotCall(),
43+
common.mustCall((value) => {
44+
assert.deepStrictEqual(value, expectedError);
45+
})
46+
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
const dc = require('diagnostics_channel');
5+
const assert = require('assert');
6+
7+
const channel = dc.tracingChannel('test');
8+
9+
const expectedResult = { foo: 'bar' };
10+
const input = { foo: 'bar' };
11+
const thisArg = { baz: 'buz' };
12+
13+
function check(found) {
14+
assert.deepStrictEqual(found, input);
15+
}
16+
17+
const handlers = {
18+
start: common.mustCall(check, 2),
19+
end: common.mustCall(check, 2),
20+
asyncStart: common.mustCall((found) => {
21+
check(found);
22+
assert.strictEqual(found.error, undefined);
23+
assert.deepStrictEqual(found.result, expectedResult);
24+
}, 2),
25+
asyncEnd: common.mustCall((found) => {
26+
check(found);
27+
assert.strictEqual(found.error, undefined);
28+
assert.deepStrictEqual(found.result, expectedResult);
29+
}, 2),
30+
error: common.mustNotCall()
31+
};
32+
33+
channel.subscribe(handlers);
34+
35+
channel.traceCallback(function(cb, err, res) {
36+
assert.deepStrictEqual(this, thisArg);
37+
setImmediate(cb, err, res);
38+
}, 0, input, thisArg, common.mustCall((err, res) => {
39+
assert.strictEqual(err, null);
40+
assert.deepStrictEqual(res, expectedResult);
41+
}), null, expectedResult);
42+
43+
channel.tracePromise(function(value) {
44+
assert.deepStrictEqual(this, thisArg);
45+
return Promise.resolve(value);
46+
}, input, thisArg, expectedResult).then(
47+
common.mustCall((value) => {
48+
assert.deepStrictEqual(value, expectedResult);
49+
}),
50+
common.mustNotCall()
51+
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
const dc = require('diagnostics_channel');
5+
const assert = require('assert');
6+
7+
const channel = dc.tracingChannel('test');
8+
9+
const expectedError = new Error('test');
10+
const input = { foo: 'bar' };
11+
const thisArg = { baz: 'buz' };
12+
13+
function check(found) {
14+
assert.deepStrictEqual(found, input);
15+
}
16+
17+
const handlers = {
18+
start: common.mustCall(check),
19+
end: common.mustCall(check),
20+
asyncStart: common.mustNotCall(),
21+
asyncEnd: common.mustNotCall(),
22+
error: common.mustCall((found) => {
23+
check(found);
24+
assert.deepStrictEqual(found.error, expectedError);
25+
})
26+
};
27+
28+
channel.subscribe(handlers);
29+
try {
30+
channel.traceSync(function(err) {
31+
assert.deepStrictEqual(this, thisArg);
32+
assert.strictEqual(err, expectedError);
33+
throw err;
34+
}, input, thisArg, expectedError);
35+
36+
throw new Error('It should not reach this error');
37+
} catch (error) {
38+
assert.deepStrictEqual(error, expectedError);
39+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
const dc = require('diagnostics_channel');
5+
const assert = require('assert');
6+
7+
const channel = dc.tracingChannel('test');
8+
9+
const expectedResult = { foo: 'bar' };
10+
const input = { foo: 'bar' };
11+
12+
function check(found) {
13+
assert.deepStrictEqual(found, input);
14+
}
15+
16+
const handlers = {
17+
start: common.mustCall(check),
18+
end: common.mustCall((found) => {
19+
check(found);
20+
assert.deepStrictEqual(found.result, expectedResult);
21+
}),
22+
asyncStart: common.mustNotCall(),
23+
asyncEnd: common.mustNotCall(),
24+
error: common.mustNotCall()
25+
};
26+
27+
assert.strictEqual(channel.start.hasSubscribers, false);
28+
channel.subscribe(handlers);
29+
assert.strictEqual(channel.start.hasSubscribers, true);
30+
channel.traceSync(() => {
31+
return expectedResult;
32+
}, input);
33+
34+
channel.unsubscribe(handlers);
35+
assert.strictEqual(channel.start.hasSubscribers, false);
36+
channel.traceSync(() => {
37+
return expectedResult;
38+
}, input);

‎tools/doc/type-parser.mjs

+3
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ const customTypesMap = {
5757
'Module Namespace Object':
5858
'https://tc39.github.io/ecma262/#sec-module-namespace-exotic-objects',
5959

60+
'AsyncLocalStorage': 'async_context.html#class-asynclocalstorage',
61+
6062
'AsyncHook': 'async_hooks.html#async_hookscreatehookcallbacks',
6163
'AsyncResource': 'async_hooks.html#class-asyncresource',
6264

@@ -108,6 +110,7 @@ const customTypesMap = {
108110
'dgram.Socket': 'dgram.html#class-dgramsocket',
109111

110112
'Channel': 'diagnostics_channel.html#class-channel',
113+
'TracingChannel': 'diagnostics_channel.html#class-tracingchannel',
111114

112115
'Domain': 'domain.html#class-domain',
113116

0 commit comments

Comments
 (0)
Please sign in to comment.