-
Notifications
You must be signed in to change notification settings - Fork 29.6k
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: reduce startup time #20567
lib: reduce startup time #20567
Conversation
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.
one thing to keep in mind is that when we start doing snapshots we'll want to revert this
@@ -287,6 +292,7 @@ assert.notEqual = function notEqual(actual, expected, message) { | |||
|
|||
// The equivalence assertion tests a deep equality relation. | |||
assert.deepEqual = function deepEqual(actual, expected, message) { | |||
if (isDeepEqual === undefined) lazyLoadComparison(); |
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.
Instead of this, you could assign isDeepEqual
a function that loads and then overwrites itself. That way you can be sure you caught all the use sites.
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.
@hashseed Can we just use something like
let comparison;
function lazyComparison() {
if (comparison === undefined) {
comparison = require('internal/util/comparisons');
}
return comparison;
}
And use lazyComparison()
everywhere for simplicy? I guess V8 should be able to inline that?
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.
You would have to use lazyComparison()(args)
the way you defined it. Inlining shouldn't be an issue here.
I'd even go a bit further and define a wrapper that can be reused:
function lazify(loader) {
let loaded = undefined;
return function(...args) {
if (loaded === undefined) {
loaded = loader();
loader = undefined;
}
return loaded(...args);
};
}
You can then use this to load comparison
:
let comparison = lazify(() => require('internal/util/comparisons'));
I wonder though whether inlining that would be an issue. @bmeurer
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 just ran benchmarks on all three of the variations and looking at the numbers it seems like everything gets inlined (both ways). Depending on how I write the benchmark, the outcome is favorable for some other different versions.
'use strict';
let foo;
let tmpBaz;
function baz() {
if (tmpBaz === undefined) tmpBaz = require('util');
return tmpBaz;
}
const lazyUtil = lazify(() => require('util'));
function lazify(loader) {
let loaded;
return function() {
if (loaded === undefined) {
loaded = loader();
loader = undefined;
}
return loaded;
};
}
function a(a, b) {
if (foo === undefined) foo = require('util');
return foo.isDeepStrictEqual(a, b);
}
function b(a, b) {
return baz().isDeepStrictEqual(a, b);
}
function c(a, b) {
return lazyUtil().isDeepStrictEqual(a, b);
}
function bench(fn) {
const arr = [];
arr.push(fn(0, 0));
arr.push(fn(1, 1));
arr.push(fn(2, 2));
arr.push(fn(3, 3));
console.time(`Runtime ${fn.name}`);
for (var i = 0; i < 1e7; i++) {
// if (i % 1e4 === 0) {
// foo = undefined;
// tmpBaz = undefined;
// }
arr.push(fn(i, i));
}
console.timeEnd(`Runtime ${fn.name}`);
return arr;
}
for (var i = 0; i < 5; i++) {
bench(a);
bench(b);
bench(c);
}
Without the if block, a
is the winner with ~450ms vs ~500ms. With the if block, b
is the winner with ~450ms vs ~500ms.
I personally normally use it the way as is right now but I see the point for it being error prone. Even though it also some times allows to only lazy load once outside of a loop instead of running a function in a hot loop.
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 guess we should agree on one specific way how to do this. I do not have a strong opinion (anymore) and would like to let others decide which one to pick.
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 like Yang's way if it is fast.
@devsnek Why though? It does not hurt to make the |
Having more code in the startup snapshot is a double-edged sword. Benefits include less time spent on compiling internal scripts if they end up being required. Drawbacks include more memory use and higher GC pressure. |
|
||
const assert = require('assert'); | ||
|
||
assert(process.moduleLoadList.length <= 73, process.moduleLoadList); |
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.
A better way to test this would be to start a child process printing process._rawDebug(JSON.stringify(process.moduleLoadList))
to stdout to reduce the noise.
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.
In what way does the current way produce noise?
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.
You won't need to require('assert')
, which pollutes the moduleLoadList
, I guess.
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.
Hm, calling assert
could indeed pollute the list. Even though that is currently not the case. But that can be easily worked around by writing:
const list = process.moduleLoadList.slice();
const assert = require('assert');
assert(list <= 73, list);
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.
@BridgeAR Using a child process also makes the eslint-disable node-core/required-modules
and the comment explaining why unnecessary.
@hashseed Yeah also I think for some applications where the start up performance does make a difference, the modules eliminated from the initial |
I'll give this a thorough review later, and I appreciate the separate commits but, to be honest, I'd have much preferred each of these in separate PRs. This one or is touching a LOT of stuff and has a non-zero risk of introducing regressions. It will be important to review very carefully |
@jasnell i combined it because i wanted to see the overall impact of it. |
Yeah, I get it, it just significantly increases the risk. |
@@ -483,6 +491,7 @@ Module._load = function(request, parent, isMain) { | |||
} | |||
|
|||
if (experimentalModules && isMain) { | |||
if (asyncESM === undefined) lazyLoadESM(); | |||
asyncESM.loaderPromise.then((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.
I don't think this one requires everything that loads? Mind you, it might just end up getting loaded anyways.
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 did not specifically test for this case but if esm
is used everything should be necessary (at least if more than one file is imported). This is just convenience in this case.
// Ordinarily test files must require('common') but that action causes | ||
// the global console to be compiled, defeating the purpose of this test. | ||
// This makes sure no additional files are added without carefully considering | ||
// lazy loading. Please adjust the value if necessary. |
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.
Good idea!
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.
Looks pretty good to me, but I'd certainly like to see a version using Yang's lazy-function approach, it it works out ok.
I looked into using @hashseed s lazy function approach but after playing around with it I would actually like to change that in a follow up PR and land this as is. The reason is that this lazy loading approach can only load a specific function and there are multiple patterns how to lazy load that are all a bit different. It is for example not possible to lazy load classes with that either. Therefore I would like to work on something like this later on. @Fishrock123 @joyeecheung @hashseed is that fine for you? |
The array was only used to create a key. Instead of allocating that first, just directly create the necessary string.
This makes sure the async hooks and more is only loaded in case it is actually necessary.
The call to url was not necessary since the `url` module itself was already taking care of this.
This reduces the overall startup time by > 10 % on my local machine (comparing 100 runs) by lazy loading a couple modules. At the same time it also reduces the memory usage:
In the bootstrap phase our code is not optimized by the compiler and each file extra takes a while.
We load a total of 96 NativeModules and Bindings when just executing
node -e 'console.log(process.moduleLoadList.length)'
.With this patch that is reduced to 73! There might still be a few more modules that could be removed but it is becoming more tricky now.
I tried to use a very minimal invasive approach to actually only lazy load where it made sense. If a module would be necessary one way or the other, I did not rewrite that.
Ping @joyeecheung @hashseed @mcollina
I wonder if it is possible to reduce the streams files a bit further.
Checklist
make -j4 test
(UNIX), orvcbuild test
(Windows) passes