lib: load fewer builtins when bootstrapping without a snapshot - #65329
Open
codebytere wants to merge 1 commit into
Open
lib: load fewer builtins when bootstrapping without a snapshot#65329codebytere wants to merge 1 commit into
codebytere wants to merge 1 commit into
Conversation
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>
Collaborator
|
Review requested:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Worker startup gets ~6 % faster and a snapshot-less main-thread bootstrap (embedders that create their own isolate,
--no-node-snapshot) ~10 % faster, by not eagerly loading builtins those paths never use. Startup with the snapshot isunchanged.
Builtins compiled: bare worker 95 → 83;
--no-node-snapshotempty CJS entry 76 → 59, empty ESM entry 76 → 69.Contexts that aren't deserialized from the snapshot compile every builtin the bootstrap touches, so each eager
requirethere is startup time (~0.15–0.4 ms apiece). Several are eager only so that they land in the snapshot, or forfeatures the bootstrap doesn't use. This makes them lazy without changing the snapshot's contents:
is_main_thread.js:util,url, the ESM loader chain,internal/blobandinternal/dns/utilsare preloaded onlyif (isBuildingSnapshot()); otherwise they load on first use.fs→internal/blob: only forfs.openAsBlob().internal/url→internal/data_url: only for the Buffer-returningfile-URL helpers.
execution, cjs loader,esm/translators,esm/load→ TypeScript stripper /data:helpers: only forthose inputs.
pre_execution→internal/dns/utils: only for an explicit--dns-result-order(still validated at startup) or asnapshot build;
'verbatim'becomes the variable's initializer, so a snapshot-timesetDefaultResultOrder()stillsurvives deserialization.
internal/worker→ ELU /error_serdeson demand;worker_threads.locksviadefineLazyProperties(asutildoes).An intermediate version that didn't re-add these to the snapshot regressed
node empty.mjsby 2–4 %, which is why theisBuildingSnapshot()block lists them explicitly.test-bootstrap-modulesis adjusted for the worker-side list.Tests:
test-bootstrap-modulesplus worker, url, fs, dns, process, cli, vm, snapshot, blob, esm, inspector, module,util, test-runner and single-executable suites pass.
Disclosure: the code, test, measurements and this description were written by Claude Code, directed and reviewed by @codebytere.