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
lib: add diagnostics_channel events to module loading #44340
base: main
Are you sure you want to change the base?
Conversation
Review requested:
|
I'd be more comfortable not sending something that will be directly patched
as it won't work for ESM
…On Mon, Aug 22, 2022, 7:39 AM Stephen Belanger ***@***.***> wrote:
***@***.**** commented on this pull request.
------------------------------
In lib/internal/modules/cjs/loader.js
<#44340 (comment)>:
> @@ -797,6 +800,7 @@ Module._load = function(request, parent, isMain) {
throw new ERR_UNKNOWN_BUILTIN_MODULE(request);
}
+ onModuleLoad.publish(module);
The hasSubscribers check is only relevant if you're building input
objects. If you just pass stuff in as-is it will be a no-op when there's no
subscribers. Do we want to match anyway though just for consistency?
—
Reply to this email directly, view it on GitHub
<#44340 (comment)>, or
unsubscribe
<https://github.com/notifications/unsubscribe-auth/AABZJI2NJF3NWVW462NJJ3TV2NYJNANCNFSM57HMHFCQ>
.
You are receiving this because you are on a team that was mentioned.Message
ID: ***@***.***>
|
Since the ESM loader can also load CommonJS, should the event perhaps just be "module load" without the specific of whether it's ESM or CommonJS? I assume there's a payload with details like what module was loaded (?) so that could be where we specify which loader was used, which format the file was recognized as, etc. |
seen = module; | ||
})); | ||
|
||
import('http').then(http => { |
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.
Can you write a test in ESM (in an .mjs file) to confirm that works too? What about static import
statements?
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 sure how that would work. Wouldn't the module graph already be resolved/loaded by the time any code runs to actually register a subscriber? Maybe it could work in a loader?
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.
That's why I asked 😄
We should add a test that shows expected esm behavior.
Don't forget to add it to the docs here: https://nodejs.org/api/diagnostics_channel.html#built-in-channels |
Could more events be added? There are use cases where having just one event is not enough, for example capturing how long each require call took which would require 2 events surrounding the function. Also including more data than just the module would be helpful, for example the path to the entry point, whether it's a core module, the base path, etc. Basically most of what require-in-the-middle has been doing historically, except the version since that's specific to package managers. Basically I think there could be 3 events: 1 when require is called, 1 when it finishes, and 1 when the module is actually loaded with module resolution information included as well. |
@Qard Thanks for doing this. I just want to let you know that this would cover all the use cases for the data we've been collecting internally. |
@Qard Would you mind fixing the lint error so all the tests can pass? |
There's some more changes I've been meaning to make. I'll try to get to that after NodeConf EU. Just had a very busy few weeks. 😅 |
Thanks. Let’s also please not land this until all open questions are resolved. |
Going to redo this on #44943 after it lands. |
Thanks. Will that other PR enable things like I mentioned in #44340 (comment), where we could have more generic events like “module load” and then additional properties for extra data like module specifier, module system (CommonJS or ESM), resolved path, etc.? |
92affe4
to
99f0481
Compare
I've reimplemented this on top of #44943. Ignore the first commit, that's inherited from that PR. I'll rebase after that lands to fix this PR. :) |
FYI there's a lot of refactoring in progress for module loading, including starting to merge together the CJS and ESM loaders. I would wait until the dust settles on that work before landing this. I would also suggest you make the events less tied to subsystem: like module resolution, module loading; whether it was from require or import, etc. |
99f0481
to
c565e0e
Compare
That's fine. Rebasing this before TracingChannel lands mainly just to prove its functionality and to gather feedback on this pattern to verify it works for the APM case. As for splitting it up into resolution and loading, there's not really much value apart from tracking the overall load. Probably best to keep it simple for now. We can always add more events later if anyone sees specific value in it. I would like to add an extra field to the context object though to indicate if it was a fresh load or a cache hit though. Also, I split it into separate tracing channels for |
doc/api/diagnostics_channel.md
Outdated
another async task is triggered internally which fails and then the sync part | ||
of the function then throws and error two `error` events will be emitted, one | ||
for the sync error and one for the async error. | ||
|
||
### Built-in Channels |
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.
The new channels should be added here
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.
Yep, will do when we finalize this. Need to wait for the ESM changes @GeoffreyBooth mentioned first. Thanks for the reminder though. :)
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.
Which changes in particular?
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.
The refactoring/merging stuff you mentioned. Not in a huge rush for this PR, it can wait for those changes.
In #44710 we’re starting the process toward merging together the CommonJS and ESM loaders. Can these events please be renamed to just |
Lots of conflicts here. |
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’m going to put my foot down and ask that the event names not include cjs
or esm
. The two loaders already share code, and will overlap more and more over time. It will become a constraint in the future to know whether to fire a diagnostics event for cjs
or esm
when such an event is on a merged codepath.
I think ideally the event names would map to the steps like resolve
and load
, and have a metadata object with additional info like what the parent module was, whether it was via require
or import
, etc.
Categorization type data like that should be part of the event name, not the message. The whole point of diagnostics_channel is to be able to filter down to the specific types of messages you want using the channel name. If we remove all that differentiation from the name then we just have a firehose that we need to always filter back down on the other end. I disagree that should be removed, though I think maybe it would be better expressed as things loaded with import and things loaded with require rather than cjs/esm which is more a reference to the syntax than how it was loaded. 🤔 |
Yes, that’s more future proof, as the syntax used in user source code is unrelated to how we implement the module loader internals. You could also distinguish between static and dynamic Just an idea, would analytics users care if a module is loaded as a result of first-party application code or from a dependency? Like maybe they want to filter out events triggered by stuff under |
The use case this is specifically designed for is APMs wanting to measure all loaded modules, the time they took to load, and ideally structured in a way that we can reconstruct the dependency graph from the load information. So knowing the parent, or parents, of a given module load. Having the unresolved and resolved strings could be valuable but we don't really care when those happen so we probably do need separate events for those, we can just include that data in the load events. It would also be important to differentiate cache hits from actual from-disk loads. Basically, we want to be able to compute startup time impact of different modules and map out dependencies for things like vulnerability warnings having the context of how you're transitively depending on a vulnerable module. My plan is to update this now that TracingChannel has landed and use this in a way that would enable APMs to produce a complete startup trace as we do with request traces to show spans for each loaded module and the timing of when the load occurs. As for loading of user code vs node_modules, if we have a handy flag already available we can pass that along, otherwise we can just continue parsing the module path to identify the source. We already do that plus some extra stuff to differentiate built-in modules too. |
c565e0e
to
fb35626
Compare
fb35626
to
9595bfb
Compare
@@ -151,6 +152,10 @@ const { | |||
} = require('internal/util/types'); | |||
|
|||
const { kEvaluated } = internalBinding('module_wrap'); | |||
|
|||
const { tracingChannel } = require('diagnostics_channel'); | |||
const onLoad = tracingChannel('module.require'); |
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.
const onLoad = tracingChannel('module.require'); | |
const onRequire = tracingChannel('module.require'); |
@@ -1231,7 +1236,11 @@ Module.prototype.require = function(id) { | |||
} | |||
requireDepth++; | |||
try { | |||
return Module._load(id, this, /* isMain */ false); | |||
return onLoad.traceSync(Module._load, { |
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.
return onLoad.traceSync(Module._load, { | |
return onRequire.traceSync(Module._load, { |
__proto__: null, | ||
parentFilename: this.filename, | ||
id, | ||
}, Module, id, this, /* isMain */ false); |
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.
What are the performance implications of this? It seems like even if an app isn’t using Diagnostics Channel, its function still wraps every module load. Surely this must slow things down slightly?
@@ -28,6 +28,9 @@ const { | |||
} = require('internal/modules/esm/utils'); | |||
let defaultResolve, defaultLoad, defaultLoadSync, importMetaInitializer; | |||
|
|||
const { tracingChannel } = require('diagnostics_channel'); | |||
const onLoad = tracingChannel('module.import'); |
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.
const onLoad = tracingChannel('module.import'); | |
const onImport = tracingChannel('module.import'); |
this, url, undefined, evalInstance, false, false); | ||
this.loadCache.set(url, undefined, job); | ||
const { module } = await job.run(); | ||
const namespace = await onLoad.tracePromise(async () => { |
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.
const namespace = await onLoad.tracePromise(async () => { | |
const namespace = await onImport.tracePromise(async () => { |
const moduleJob = await this.getModuleJob(specifier, parentURL, importAttributes); | ||
const { module } = await moduleJob.run(); | ||
return module.getNamespace(); | ||
return onLoad.tracePromise(async () => { |
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.
return onLoad.tracePromise(async () => { | |
return onImport.tracePromise(async () => { |
__proto__: null, | ||
parentURL: '<eval>', | ||
url, | ||
}); |
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.
So this is capturing ESM code that comes from --eval
or STDIN? Shouldn’t we do the same for CommonJS string input?
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.
Maybe? I'm not sure we actually need this here. I'm still working on this and figuring out testing and some changes that probably need to be made.
|
||
node:diagnostics_channel:315 | ||
throw err; | ||
^ |
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 think the previous stack trace is much more useful. I see that you’re importing hideStackFrames
but it appears unused, perhaps it can help preserve the old stack trace?
@@ -0,0 +1,65 @@ | |||
'use strict'; |
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.
Lately we’ve been trying to write new tests in ESM using the test runner. Would you mind updating these?
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.
That's not how diagnostics_channel tests have been written though. That wouldn't match with the rest of the diagnostics_channel tests.
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 don’t think that’s a strong argument for continuing with the old pattern. Eventually it’ll be more important that things work correctly in ESM than in CommonJS, so we avoid creating tech debt for ourselves by writing tests in ESM now.
And if there are existing Diagnostics Channel tests in CommonJS, all the more reason to have new ones in ESM since we already have coverage for the “in CommonJS” case.
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.
Eventually it’ll be more important
that's a pretty bold claim. if it's important to have full coverage, then tests should be authored in both (or evaluated in both), rather than excluding one for the other's sake.
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 also find the new test runner-style tests much easier to read, and we can take advantage of improvements it offers like the snapshot tool. Especially with regard to some of these tests that are asserting stack traces, those are ideal things to use the snapshot tool for so that future PRs don’t need to update unrelated tests every time stack traces change.
I think the best way forward would be to do a separate PR to refactor the existing tests that get modified in this one, to migrate them to the new style and snapshot tool, so that then this PR doesn’t need to edit “unrelated” tests to add Diagnostics Channel to the CommonJS loader. There are several contributors helping migrate the out/message
tests into the new style who might be interested in helping if you want to open an issue listing the tests that need migrating.
@@ -2,6 +2,8 @@ | |||
const common = require('../common'); | |||
const assert = require('assert'); | |||
|
|||
Error.stackTraceLimit = Infinity; |
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.
Could you make this 0 or 1 so that we exclude the CommonJS loader internals from the stack trace? Or filter them out using one of the snapshot helpers? I want to be able to refactor those without causing unrelated tests to fail.
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.
No. The trace functions appear in the middle of the stack trace somewhere. Any changes to things which appear in the stack trace will require updating the corresponding output pattern as it's fairly strict about count and ordering. It'd probably be better if the tests were more generalized, but I feel like that's beyond the scope of this PR.
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.
Then please use the new test runner and its snapshot ability to remove the stack trace lines that are irrelevant. See https://github.com/nodejs/node/blob/fc102f2180dfc8e1654ce672185f5bf97ec21b53/test/parallel/test-node-output-console.mjs and other test files that import common/assertSnapshot.js
.
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.
Again, this is not a test I wrote or have the context to rewrite correctly. I've edited it just to make this case pass. If you think that test should work differently feel free to rewrite it.
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.
Ah, I was assuming that this corresponded with one of the snapshots that changed in your PR, where the CommonJS loader internals are leaking. If it doesn’t, then sure I don’t care about this. In general I don’t want more CommonJS stack traces being asserted as part of tests; I don’t want to repeat PRs like #44197.
/^ {4}at .*/, // at Channel.runStores | ||
/^ {4}at .*/, // at TracingChannel.traceSync | ||
/^ {4}at .*/, // at Module.require | ||
/^ {4}at .*/, // at require |
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’m glad the function names aren’t in the trace being asserted, but the number of stack frames is, which means that if I add so much as an extra function call to this path within the CommonJS loader then I break this test (just as you did, adding the diagnostics channel). I understand that this test was fragile before, but rather than ignoring the problem (by just increasing the number of frames) it would be better to just solve it, to assert less so that CommonJS loader refactors don’t break this test.
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.
Agreed, but I think it's beyond the scope of this PR to define new testing patterns for stack traces. Unless you have a concrete solution to provide I don't think I'll be able to do anything about it here.
more work to do based on feedback and failing tests
Adding
module.cjs.load
andmodule.esm.load
diagnostics_channel events so APMs don't need to patch the module system to know when a particular module has been loaded.