-
-
Notifications
You must be signed in to change notification settings - Fork 35k
diagnostics_channel: add iterator support to tracing channel #61686
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -281,6 +281,8 @@ function tracingChannelFrom(nameOrChannels, name) { | |
| } | ||
|
|
||
| class TracingChannel { | ||
| #nextChannel; | ||
|
|
||
| constructor(nameOrChannels) { | ||
| for (let i = 0; i < traceEvents.length; ++i) { | ||
| const eventName = traceEvents[i]; | ||
|
|
@@ -428,6 +430,84 @@ class TracingChannel { | |
| } | ||
| }); | ||
| } | ||
|
|
||
| traceIterator(fn, context = {}, thisArg, ...args) { | ||
| if (!this.hasSubscribers) { | ||
| return ReflectApply(fn, thisArg, args); | ||
| } | ||
|
|
||
| const { start, end, asyncStart, asyncEnd, error } = this; | ||
|
|
||
| const nextChannel = this.#nextChannel ||= tracingChannel({ | ||
| start: channel(start.name.slice(0, -6) + ':next:start'), | ||
| end: channel(end.name.slice(0, -4) + ':next:end'), | ||
| asyncStart: channel(asyncStart.name.slice(0, -11) + ':next:asyncStart'), | ||
| asyncEnd: channel(asyncEnd.name.slice(0, -9) + ':next:asyncEnd'), | ||
| error: channel(error.name.slice(0, -6) + ':next:error'), | ||
| }); | ||
|
|
||
| const wrapIter = (iter) => { | ||
| const { next: iterNext, return: iterReturn, throw: iterThrow } = iter; | ||
|
|
||
| iter.next = (...args) => | ||
| nextChannel.#traceMaybePromise(iterNext, context, iter, ...args); | ||
| iter.return = (...args) => | ||
| nextChannel.#traceMaybePromise(iterReturn, context, iter, ...args); | ||
| iter.throw = (...args) => | ||
| nextChannel.#traceMaybePromise(iterThrow, context, iter, ...args); | ||
|
|
||
| return iter; | ||
| }; | ||
|
|
||
| const result = this.#traceMaybePromise(fn, context, thisArg, ...args); | ||
|
|
||
| return result instanceof Promise ? | ||
| PromisePrototypeThen(result, wrapIter) : | ||
| wrapIter(result); | ||
| } | ||
|
|
||
| #traceMaybePromise(fn, context = {}, thisArg, ...args) { | ||
| if (!this.hasSubscribers) { | ||
| return ReflectApply(fn, thisArg, args); | ||
| } | ||
|
|
||
| const { start, end, asyncStart, asyncEnd, error } = this; | ||
|
|
||
| function reject(err) { | ||
| context.error = err; | ||
| error.publish(context); | ||
| asyncStart.publish(context); | ||
| // TODO: Is there a way to have asyncEnd _after_ the continuation? | ||
| asyncEnd.publish(context); | ||
| return PromiseReject(err); | ||
| } | ||
|
|
||
| function resolve(result) { | ||
| context.result = result; | ||
| asyncStart.publish(context); | ||
| // TODO: Is there a way to have asyncEnd _after_ the continuation? | ||
| asyncEnd.publish(context); | ||
| return result; | ||
| } | ||
|
|
||
| return start.runStores(context, () => { | ||
| try { | ||
| const result = ReflectApply(fn, thisArg, args); | ||
| // TODO: Should tracePromise just always do this? | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No, because thenables. That's why it does the
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What about functions that can return both though? Sometimes the caller handles that, and changing the return type may be unexpected. In some cases we end up having to manually do a |
||
| if (!(result instanceof Promise)) { | ||
| context.result = result; | ||
| return result; | ||
| } | ||
| return PromisePrototypeThen(result, resolve, reject); | ||
| } catch (err) { | ||
| context.error = err; | ||
| error.publish(context); | ||
| throw err; | ||
| } finally { | ||
| end.publish(context); | ||
| } | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| function tracingChannel(nameOrChannels) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| 'use strict'; | ||
|
|
||
| const common = require('../common'); | ||
| const dc = require('diagnostics_channel'); | ||
| const assert = require('assert'); | ||
|
|
||
| const channel = dc.tracingChannel('test'); | ||
| const nextChannel = dc.tracingChannel('test:next'); | ||
|
|
||
| const expectedResult = { foo: 'bar' }; | ||
| const input = { foo: 'bar' }; | ||
| const thisArg = { baz: 'buz' }; | ||
|
|
||
| function check(found) { | ||
| assert.deepStrictEqual(found, input); | ||
| } | ||
|
|
||
| function checkNextAsync(found) { | ||
| check(found); | ||
| assert.strictEqual(found.error, undefined); | ||
| assert.deepStrictEqual(found.result, { value: expectedResult, done: false }); | ||
| } | ||
|
|
||
| // Async function* returns an AsyncGenerator synchronously, so no asyncStart/asyncEnd | ||
| // for the fn call itself | ||
| const handlers = { | ||
| start: common.mustCall(check), | ||
| end: common.mustCall(check), | ||
| asyncStart: common.mustNotCall(), | ||
| asyncEnd: common.mustNotCall(), | ||
| error: common.mustNotCall(), | ||
| }; | ||
|
|
||
| // next() on an AsyncGenerator returns a Promise | ||
| const nextHandlers = { | ||
| start: common.mustCall(check), | ||
| end: common.mustCall(check), | ||
| asyncStart: common.mustCall(checkNextAsync), | ||
| asyncEnd: common.mustCall(checkNextAsync), | ||
| error: common.mustNotCall(), | ||
| }; | ||
|
|
||
| channel.subscribe(handlers); | ||
| nextChannel.subscribe(nextHandlers); | ||
|
|
||
| const iter = channel.traceIterator(common.mustCall(async function*(value) { | ||
| assert.deepStrictEqual(this, thisArg); | ||
| yield value; | ||
| }), input, thisArg, expectedResult); | ||
|
|
||
| // next() returns a Promise since iter is an AsyncGenerator | ||
| iter.next().then(common.mustCall((result) => { | ||
| assert.deepStrictEqual(result, { value: expectedResult, done: false }); | ||
| })); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| 'use strict'; | ||
|
|
||
| const common = require('../common'); | ||
| const dc = require('diagnostics_channel'); | ||
|
|
||
| const channel = dc.tracingChannel('test'); | ||
| const nextChannel = dc.tracingChannel('test:next'); | ||
|
|
||
| const handlers = { | ||
| start: common.mustNotCall(), | ||
| end: common.mustNotCall(), | ||
| asyncStart: common.mustNotCall(), | ||
| asyncEnd: common.mustNotCall(), | ||
| error: common.mustNotCall(), | ||
| }; | ||
|
|
||
| const nextHandlers = { | ||
| start: common.mustNotCall(), | ||
| end: common.mustNotCall(), | ||
| asyncStart: common.mustNotCall(), | ||
| asyncEnd: common.mustNotCall(), | ||
| error: common.mustNotCall(), | ||
| }; | ||
|
|
||
| // Subscribe after traceIterator call - no events should fire for the iterator | ||
| // or for subsequent next() calls since the iterator was not wrapped | ||
| const iter = channel.traceIterator(function*() { | ||
| yield 1; | ||
| }, {}); | ||
|
|
||
| channel.subscribe(handlers); | ||
| nextChannel.subscribe(nextHandlers); | ||
|
|
||
| iter.next(); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| 'use strict'; | ||
|
|
||
| const common = require('../common'); | ||
| const dc = require('diagnostics_channel'); | ||
| const assert = require('assert'); | ||
|
|
||
| const channel = dc.tracingChannel('test'); | ||
| const nextChannel = dc.tracingChannel('test:next'); | ||
|
|
||
| const expectedError = new Error('test'); | ||
| const input = { foo: 'bar' }; | ||
| const thisArg = { baz: 'buz' }; | ||
|
|
||
| function check(found) { | ||
| assert.deepStrictEqual(found, input); | ||
| } | ||
|
|
||
| function checkError(found) { | ||
| check(found); | ||
| assert.deepStrictEqual(found.error, expectedError); | ||
| } | ||
|
|
||
| // Two traceIterator calls: one for next() error, one for throw() error | ||
| const handlers = { | ||
| start: common.mustCall(check, 2), | ||
| end: common.mustCall(check, 2), | ||
| asyncStart: common.mustNotCall(), | ||
| asyncEnd: common.mustNotCall(), | ||
| error: common.mustNotCall(), | ||
| }; | ||
|
|
||
| // iter1: next() success + next() throws = start×2, end×2, error×1 | ||
| // iter2: throw() throws = start×1, end×1, error×1 | ||
| const nextHandlers = { | ||
| start: common.mustCall(check, 3), | ||
| end: common.mustCall(check, 3), | ||
| asyncStart: common.mustNotCall(), | ||
| asyncEnd: common.mustNotCall(), | ||
| error: common.mustCall(checkError, 2), | ||
| }; | ||
|
|
||
| channel.subscribe(handlers); | ||
| nextChannel.subscribe(nextHandlers); | ||
|
|
||
| // Test next(): generator throws after the first yield | ||
| const iter1 = channel.traceIterator(common.mustCall(function*() { | ||
| assert.deepStrictEqual(this, thisArg); | ||
| yield 1; | ||
| throw expectedError; | ||
| }), input, thisArg); | ||
|
|
||
| assert.deepStrictEqual(iter1.next(), { value: 1, done: false }); | ||
| assert.throws(() => iter1.next(), expectedError); | ||
|
|
||
| // Test throw(): propagates error through the iterator | ||
| const iter2 = channel.traceIterator(common.mustCall(function*() { | ||
| yield 1; | ||
| }), input, thisArg); | ||
|
|
||
| assert.throws(() => iter2.throw(expectedError), expectedError); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not a huge fan of magically deriving these from the main channels. Could we perhaps implement this
traceIteratorfunctionality as an entirely other class which we could simply construct with two separateTracingChannelinstances for the overall execution and the per-yield execution?I've been thinking we might want to split up
TracingChannelinto specializations anyway, something like:SyncTracingChannel,CallbackTracingChannel,PromiseTracingChannel, and a newIteratorTracingChannel?Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree and I'm also not a fan, but it felt like the simplest approach.
Are you saying to do instead something like this?
I thought about that, but I wasn't sure since it would be the first time that
TracingChannelinstruments anything that is not a function (I mean it still patches the functions on the iterator, but that's more magic than usual). It also makes it impossible to reimplement differently later, although in practice I'm not sure that would happen. And it would also potentially be inconsistent across implementors. One could use:yield, another could use:next, etc, and thenTracingChannelwould lose the consistency it was built for, and subscribers could not be built generically.Worth noting: I'm adding the same functionality to Orchestrion-JS right now and it's unclear whether the
FunctionTypeshould beIteratoror not. If this is split, then I guess it would probably be a separateIteratorType?I prefer the one class because we often have to mix functionalities or use the underlying channels directly for example when functions accept a callback but can also return a promise or a sync value.