Skip to content

Commit 0cc532a

Browse files
committed
lib: load fewer builtins when bootstrapping without a snapshot
Contexts that are not deserialized from the built-in snapshot -- worker threads, and the main context of embedders that create their own isolate or of `node --no-node-snapshot` -- compile (with the code cache at best) every builtin the bootstrap touches, so each eagerly required builtin is startup time (~0.15-0.4 ms apiece). A number of them are only required eagerly so that they end up in the snapshot, or for features the bootstrap path never uses. Load lazily what those paths do not need: - is_main_thread.js: preload util, url, the ESM loader (translators, resolver, module_job/map, source maps, node:module, vm modules, mime, data_url, the TypeScript stripper), internal/blob and internal/dns/utils only while building a snapshot; they load on first use otherwise. - fs: internal/blob (+ internal/encoding and its tables) is only used by fs.openAsBlob(). - internal/url: internal/data_url (+ internal/mime) is only used by the Buffer-returning file URL helpers. - internal/process/execution, the CommonJS loader, esm/translators and esm/load: the TypeScript stripper and data: URL helpers are only needed for TypeScript sources / data: URLs. - pre_execution: internal/dns/utils (+ internal/net) is only needed up front to validate an explicit --dns-result-order or to register the resolver's snapshot serializer; the default order becomes the variable's initializer. - internal/worker: event_loop_utilization and error_serdes are only needed once a sub-worker's ELU is read or it reports an error. - worker_threads: `locks` is defined lazily, like util's lazy exports. Main-thread startup with the snapshot is unchanged (the same modules are preloaded into it; the bootstrap-modules test lists are adjusted). A bare worker compiles 95 -> 83 builtins (cold start -5%); without the snapshot an empty CommonJS entry point compiles 76 -> 59 builtins and an empty ES module entry point 76 -> 69. Signed-off-by: Shelley Vohr <shelley.vohr@gmail.com>
1 parent 30bff4a commit 0cc532a

12 files changed

Lines changed: 78 additions & 46 deletions

File tree

lib/fs.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,6 @@ const { isArrayBufferView } = require('internal/util/types');
6464

6565
const binding = internalBinding('fs');
6666

67-
const { createBlobFromFilePath } = require('internal/blob');
68-
6967
const { Buffer } = require('buffer');
7068
const { isBuffer: BufferIsBuffer } = Buffer;
7169
const BufferToString = uncurryThis(Buffer.prototype.toString);
@@ -722,6 +720,7 @@ function openAsBlob(path, options = kEmptyObject) {
722720
// To give ourselves flexibility to maybe return the Blob asynchronously,
723721
// this API returns a Promise.
724722
path = getValidatedPath(path);
723+
const { createBlobFromFilePath } = require('internal/blob');
725724
return PromiseResolve(createBlobFromFilePath(path, { type }));
726725
}
727726

lib/internal/bootstrap/switches/is_main_thread.js

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -292,12 +292,27 @@ rawMethods.resetStdioForTesting = function() {
292292

293293
// Needed by the module loader and generally needed everywhere.
294294
require('fs');
295-
require('util');
296-
require('url'); // eslint-disable-line no-restricted-modules
297295
internalBinding('module_wrap');
298296
require('internal/modules/cjs/loader');
299-
require('internal/modules/esm/loader');
300297
require('internal/modules/esm/utils');
298+
if (isBuildingSnapshot()) {
299+
// Preloaded so that they are part of the snapshot, where they cost nothing
300+
// at startup. When bootstrapping WITHOUT a snapshot (worker threads,
301+
// embedders that create their own isolate, --no-node-snapshot) they are
302+
// loaded on first use instead: the ESM loader (with its translators,
303+
// resolver and their dependencies) by run_main/import(), the public util
304+
// and url modules by whoever requires them, data: URL and TypeScript
305+
// support by the module loaders, internal/blob by fs.openAsBlob(), and the
306+
// DNS helpers by node:dns or an explicit --dns-result-order (see
307+
// pre_execution).
308+
require('util');
309+
require('url'); // eslint-disable-line no-restricted-modules
310+
require('internal/modules/esm/loader');
311+
require('internal/data_url');
312+
require('internal/modules/typescript');
313+
require('internal/blob');
314+
require('internal/dns/utils');
315+
}
301316

302317
// Needed to refresh the time origin.
303318
require('internal/perf/utils');
@@ -311,8 +326,6 @@ internalBinding('wasm_web_api');
311326
internalBinding('worker');
312327
// Needed by most execution modes.
313328
require('internal/modules/run_main');
314-
// Needed to refresh DNS configurations.
315-
require('internal/dns/utils');
316329
// Needed by almost all execution modes. It's fine to
317330
// load them into the snapshot as long as we don't run
318331
// any of the initialization.

lib/internal/dns/utils.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -214,15 +214,15 @@ class ResolverBase {
214214
}
215215

216216
let defaultResolver;
217-
let dnsOrder;
217+
// May already hold a value chosen by the snapshotted application; a
218+
// --dns-result-order flag given at runtime overrides it in initializeDns().
219+
let dnsOrder = 'verbatim';
218220
const validDnsOrders = ['verbatim', 'ipv4first', 'ipv6first'];
219221
const validFamilies = [0, 4, 6];
220222

221223
function initializeDns() {
222224
const orderFromCLI = getOptionValue('--dns-result-order');
223-
if (!orderFromCLI) {
224-
dnsOrder ??= 'verbatim';
225-
} else {
225+
if (orderFromCLI) {
226226
// Allow the deserialized application to override order from CLI.
227227
validateOneOf(orderFromCLI, '--dns-result-order', validDnsOrders);
228228
dnsOrder = orderFromCLI;

lib/internal/modules/cjs/loader.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ const {
180180
resolveWithHooks,
181181
validateLoadStrict,
182182
} = require('internal/modules/customization_hooks');
183-
const { stripTypeScriptModuleTypes } = require('internal/modules/typescript');
183+
const lazyTypeScript = getLazy(() => require('internal/modules/typescript'));
184184
const packageJsonReader = require('internal/modules/package_json_reader');
185185
const { getOptionValue, getEmbedderOptions } = require('internal/options');
186186
const shouldReportRequiredModules = getLazy(() => process.env.WATCH_REPORT_DEPENDENCIES);
@@ -1888,7 +1888,7 @@ function wrapSafe(filename, content, cjsModuleInstance, format) {
18881888
Module.prototype._compile = function(content, filename, format) {
18891889
if (format === 'commonjs-typescript' || format === 'module-typescript' || format === 'typescript') {
18901890
this[kURL] ??= convertCJSFilenameToURL(filename);
1891-
content = stripTypeScriptModuleTypes(content, filename, this[kURL]);
1891+
content = lazyTypeScript().stripTypeScriptModuleTypes(content, filename, this[kURL]);
18921892
switch (format) {
18931893
case 'commonjs-typescript': {
18941894
format = 'commonjs';

lib/internal/modules/esm/load.js

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,6 @@ const {
2020
ERR_UNSUPPORTED_ESM_URL_SCHEME,
2121
} = require('internal/errors').codes;
2222

23-
const {
24-
dataURLProcessor,
25-
} = require('internal/data_url');
2623

2724
/**
2825
* @param {URL} url URL to the module
@@ -40,6 +37,7 @@ function getSourceSync(url, context) {
4037
// Prefer module.registerHooks() or other more formal fs hooks released in the future.
4138
source = fs.readFileSync(url);
4239
} else if (protocol === 'data:') {
40+
const { dataURLProcessor } = require('internal/data_url'); // Only for data: URLs.
4341
const result = dataURLProcessor(url);
4442
if (result === 'failure') {
4543
throw new ERR_INVALID_URL(responseURL);

lib/internal/modules/esm/translators.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,10 @@ const {
3030
stripBOM,
3131
urlToFilename,
3232
} = require('internal/modules/helpers');
33-
const { stripTypeScriptModuleTypes } = require('internal/modules/typescript');
33+
function stripTypeScriptModuleTypes(source, url) {
34+
// Only needed for TypeScript sources; keep it out of the loader's startup path.
35+
return require('internal/modules/typescript').stripTypeScriptModuleTypes(source, url);
36+
}
3437
const {
3538
kIsCachedByESMLoader,
3639
Module: CJSModule,

lib/internal/process/execution.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@ const {
2525
kSourcePhase,
2626
kEvaluationPhase,
2727
} = internalBinding('module_wrap');
28-
const { stripTypeScriptModuleTypes } = require('internal/modules/typescript');
28+
function stripTypeScriptModuleTypes(source, filename) {
29+
return require('internal/modules/typescript').stripTypeScriptModuleTypes(source, filename);
30+
}
2931

3032
const {
3133
executionAsyncId,

lib/internal/process/pre_execution.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,12 @@ function prepareExecution(options) {
139139

140140
initializeConfigFileSupport();
141141

142-
require('internal/dns/utils').initializeDns();
142+
// internal/dns/utils (and internal/net behind it) is only needed up front
143+
// to validate an explicit --dns-result-order or to register the resolver's
144+
// snapshot serialization; otherwise it is loaded with node:dns.
145+
if (getOptionValue('--dns-result-order') || isBuildingSnapshot()) {
146+
require('internal/dns/utils').initializeDns();
147+
}
143148

144149
if (isMainThread) {
145150
assert(internalBinding('worker').isMainThread);

lib/internal/url.js

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,11 @@ const {
9393
kValidateObjectAllowObjects,
9494
} = require('internal/validators');
9595

96-
const { percentDecode } = require('internal/data_url');
96+
let percentDecode;
97+
function lazyPercentDecode(input) {
98+
percentDecode ??= require('internal/data_url').percentDecode;
99+
return percentDecode(input);
100+
}
97101

98102
const querystring = require('querystring');
99103

@@ -1560,7 +1564,7 @@ function getPathBufferFromURLWin32(url) {
15601564
// percent encoded characters and we take the string as is. Any invalid
15611565
// percent encodings, e.g. `%ZZ` are ignored and are passed through
15621566
// literally.
1563-
const decodedu8 = percentDecode(Buffer.from(pathname, 'utf8'));
1567+
const decodedu8 = lazyPercentDecode(Buffer.from(pathname, 'utf8'));
15641568
const decodedPathname = Buffer.from(TypedArrayPrototypeGetBuffer(decodedu8),
15651569
TypedArrayPrototypeGetByteOffset(decodedu8),
15661570
TypedArrayPrototypeGetByteLength(decodedu8));
@@ -1635,7 +1639,7 @@ function getPathBufferFromURLPosix(url) {
16351639
// won't scan for the slashes at all, and instead will decode the bytes
16361640
// literally into the returned Buffer. We're going to do the best we can and
16371641
// just interpret the input url as a sequence of bytes.
1638-
const u8 = percentDecode(Buffer.from(pathname, 'utf8'));
1642+
const u8 = lazyPercentDecode(Buffer.from(pathname, 'utf8'));
16391643
return Buffer.from(TypedArrayPrototypeGetBuffer(u8),
16401644
TypedArrayPrototypeGetByteOffset(u8),
16411645
TypedArrayPrototypeGetByteLength(u8));

lib/internal/worker.js

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,6 @@ const {
3030
const EventEmitter = require('events');
3131
const assert = require('internal/assert');
3232
const path = require('path');
33-
const {
34-
internalEventLoopUtilization,
35-
} = require('internal/perf/event_loop_utilization');
36-
3733
const errorCodes = require('internal/errors').codes;
3834
const {
3935
ERR_WORKER_NOT_RUNNING,
@@ -60,7 +56,6 @@ const {
6056
WritableWorkerStdio,
6157
} = workerIo;
6258
const { createMainThreadPort, destroyMainThreadPort } = require('internal/worker/messaging');
63-
const { deserializeError } = require('internal/error_serdes');
6459
const { fileURLToPath, isURL, pathToFileURL } = require('internal/url');
6560
const {
6661
constructSharedArrayBuffer,
@@ -417,6 +412,7 @@ class Worker extends EventEmitter {
417412

418413
[kOnErrorMessage](serialized) {
419414
// This is what is called for uncaught exceptions.
415+
const { deserializeError } = require('internal/error_serdes');
420416
const error = deserializeError(serialized);
421417
this.emit('error', error);
422418
}
@@ -699,6 +695,7 @@ function makeResourceLimits(float64arr) {
699695
}
700696

701697
function eventLoopUtilization(util1, util2) {
698+
const { internalEventLoopUtilization } = require('internal/perf/event_loop_utilization');
702699
// TODO(trevnorris): Works to solve the thread-safe read/write issue of
703700
// loopTime, but has the drawback that it can't be set until the event loop
704701
// has had a chance to turn. So it will be impossible to read the ELU of

0 commit comments

Comments
 (0)