Skip to content

Commit 2da4f87

Browse files
authored
perf(node): lazy stdio + fix LazyEsmModuleLoader source consumption (#34440)
Results Snapshot size: 9,407,439 → 8,521,932 bytes (−885 KB, −9.4%) Startup time (100 runs, hyperfine -N, after 20 warmups): ``` ┌─────────────────────────────────────────────────┬──────────────────────────┬───────────────┬───────────────┐ │ Command │ baseline (main@upstream) │ ollzspzs/1 │ delta │ ├─────────────────────────────────────────────────┼──────────────────────────┼───────────────┼───────────────┤ │ deno eval 1 │ 22.6 ± 0.4 ms │ 21.7 ± 1.6 ms │ −0.9 ms (−4%) │ ├─────────────────────────────────────────────────┼──────────────────────────┼───────────────┼───────────────┤ │ deno run hello.mjs │ 21.9 ± 4.4 ms │ 20.9 ± 0.9 ms │ −1.0 ms (−5%) │ ├─────────────────────────────────────────────────┼──────────────────────────┼───────────────┼───────────────┤ │ deno run -A hello.cjs │ 23.1 ± 0.7 ms │ 22.5 ± 2.4 ms │ −0.6 ms (−3%) │ ├─────────────────────────────────────────────────┼──────────────────────────┼───────────────┼───────────────┤ │ deno run stdout.mjs (uses process.stdout.write) │ 22.0 ± 2.7 ms │ 23.6 ± 1.0 ms │ +1.6 ms (+7%) │ └─────────────────────────────────────────────────┴──────────────────────────┴───────────────┴───────────────┘ ``` The regression on the last row is the expected tradeoff: scripts that actually touch process.stdout/stderr/stdin pay the deferred construction cost on first access. Net win for the common case (deno run of a script that doesn't immediately touch Node stdio) and across-the-board snapshot win.
1 parent c31fb5c commit 2da4f87

7 files changed

Lines changed: 333 additions & 200 deletions

File tree

ext/node/lib.rs

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -429,21 +429,17 @@ deno_core::extension!(deno_node,
429429
"internal_binding/mod.ts",
430430
"node:module" = "01_require.js",
431431
"node:process" = "process.ts",
432-
// node:stream + node:stream/promises stay eager: every Deno program
433-
// pays their parse/compile cost at runtime startup via
434-
// `__bootstrapNodeProcess` -> `createWritableStdioStream` -> Writable,
435-
// so keeping them in the snapshot is a net startup-time win even
436-
// though most programs never directly require('stream').
432+
],
433+
lazy_loaded_esm = [
434+
dir "polyfills",
435+
// Previously eager. Combined with the lazy stdio refactor in
436+
// process.ts (process.stdout/stderr/stdin are accessor properties),
437+
// these modules only load when a script actually touches stdio or
438+
// requires node:stream/net/tty directly.
437439
"node:stream" = "stream.ts",
438440
"node:stream/promises" = "stream/promises.js",
439-
// node:net and node:tty are needed at every TTY-stdout startup via
440-
// internal/tty.js's TTYWriteStream constructor extending net.Socket.
441-
// Keeping them eager is a startup-time win for interactive runs.
442441
"node:net" = "net_esm.ts",
443442
"node:tty" = "tty_esm.ts",
444-
],
445-
lazy_loaded_esm = [
446-
dir "polyfills",
447443
"internal/streams/compose.js",
448444
"internal/streams/duplexpair.js",
449445
"internal/streams/lazy_transform.js",

ext/node/polyfills/01_require.js

Lines changed: 60 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -88,21 +88,9 @@ const _httpAgent = core.createLazyLoader("node:_http_agent");
8888
const _httpCommon = core.createLazyLoader("node:_http_common");
8989
const _httpOutgoing = core.createLazyLoader("node:_http_outgoing");
9090
const _httpServer = core.createLazyLoader("node:_http_server");
91-
const _streamDuplex = core.loadExtScript(
92-
"ext:deno_node/internal/streams/duplex.js",
93-
).default;
94-
const _streamPassthrough = core.loadExtScript(
95-
"ext:deno_node/internal/streams/passthrough.js",
96-
).default;
97-
const _streamReadable = core.loadExtScript(
98-
"ext:deno_node/internal/streams/readable.js",
99-
).default;
100-
const _streamTransform = core.loadExtScript(
101-
"ext:deno_node/internal/streams/transform.js",
102-
).default;
103-
const _streamWritable = core.loadExtScript(
104-
"ext:deno_node/internal/streams/writable.js",
105-
).default;
91+
// Heavy stream class definitions. Lazified - only loaded if a script
92+
// actually `require('_stream_*')` or pulls them transitively via
93+
// `node:stream` after our lazy stdio refactor.
10694
// _tls_common, _tls_wrap are lazy-loaded via `lazyNodeModules` below: their
10795
// scripts extend net.Socket at module body, which pulls node:net (and then
10896
// node:stream) into the snapshot.
@@ -146,10 +134,6 @@ const fs = core.loadExtScript("ext:deno_node/fs.ts");
146134
// http/http2/https are lazy-loaded via `lazyNodeModules` below: their script
147135
// bodies eagerly chain into the entire node:_http_* / node:net / node:stream
148136
// graph, so running them at snapshot time defeats lazifying _http_*.
149-
const inspector = core.loadExtScript("ext:deno_node/inspector.js");
150-
const inspectorPromises = core.loadExtScript(
151-
"ext:deno_node/inspector/promises.js",
152-
);
153137
const internalAssertMyersDiff = core.loadExtScript(
154138
"ext:deno_node/internal/assert/myers_diff.js",
155139
);
@@ -249,17 +233,16 @@ const internalStreamsState =
249233
const internalSocketAddress = core.loadExtScript(
250234
"ext:deno_node/internal/socketaddress.js",
251235
);
252-
const internalJsStreamSocket = core.loadExtScript(
253-
"ext:deno_node/internal/js_stream_socket.js",
254-
).default;
255236
const internalNet = core.loadExtScript("ext:deno_node/internal/net.ts");
256237
const internalTestBinding = core.loadExtScript(
257238
"ext:deno_node/internal/test/binding.ts",
258239
);
259240
const internalTimers = core.loadExtScript(
260241
"ext:deno_node/internal/timers.mjs",
261242
);
262-
import * as internalTty from "ext:deno_node/internal/tty.js";
243+
const lazyInternalTty = core.createLazyLoader(
244+
"ext:deno_node/internal/tty.js",
245+
);
263246
const internalUrl = core.loadExtScript("ext:deno_node/internal/url.ts");
264247
const internalUtil = core.loadExtScript("ext:deno_node/internal/util.mjs");
265248
const internalUtilDebuglog = core.loadExtScript(
@@ -286,54 +269,31 @@ const internalWorkerJsTransferable = core.loadExtScript(
286269
const internalConsole = core.loadExtScript(
287270
"ext:deno_node/internal/console/constructor.mjs",
288271
).default;
289-
// net stays eager: internal/tty.js's TTYWriteStream extends net.Socket
290-
// inside its constructor, and process bootstrap creates a TTYWriteStream
291-
// for stdout/stderr whenever they're TTYs (every interactive run).
292-
import net from "node:net";
272+
// net, path, stream, tty lazified - see lazyNodeModules below.
273+
const lazyNet = core.createLazyLoader("node:net");
293274
const os = core.loadExtScript("ext:deno_node/os.ts").default;
294-
import pathPosix from "node:path/posix";
295-
import pathWin32 from "node:path/win32";
296-
import path from "node:path";
297-
const perfHooks = core.loadExtScript("ext:deno_node/perf_hooks.js").default;
298-
const punycode = core.loadExtScript("ext:deno_node/punycode.ts").default;
275+
const lazyPathPosix = core.createLazyLoader("node:path/posix");
276+
const lazyPathWin32 = core.createLazyLoader("node:path/win32");
277+
const lazyPath = core.createLazyLoader("node:path");
299278
import process from "node:process";
300-
const querystring = core.loadExtScript("ext:deno_node/querystring.js").default;
301279
const readline = core.createLazyLoader("node:readline");
302280
const readlinePromises = core.createLazyLoader("node:readline/promises");
303281
const repl = core.createLazyLoader("node:repl");
304282
const internalRepl = core.createLazyLoader(
305283
"ext:deno_node/internal/repl.ts",
306284
);
307-
const sqlite = core.loadExtScript("ext:deno_node/sqlite.ts");
308-
// node:stream and node:stream/promises are eager (`esm` in lib.rs): every
309-
// program pays their cost at startup via `__bootstrapNodeProcess` building
310-
// `process.stdout`/`stderr` via `new Writable(...)`, so having them in the
311-
// snapshot is a startup-time win. Static imports here ensure they end up
312-
// in v8's evaluation graph.
313-
import stream from "node:stream";
285+
const lazyStream = core.createLazyLoader("node:stream");
314286
const streamConsumers = core.loadExtScript("ext:deno_node/stream/consumers.js");
315-
import streamPromises from "node:stream/promises";
316-
// stream/web pulls ext:deno_web/14_compression.js -> 06_streams (208 KB).
317-
// Only loaded when `require("node:stream/web")` happens.
318-
const stringDecoder =
319-
core.loadExtScript("ext:deno_node/string_decoder.ts").default;
287+
const lazyStreamPromises = core.createLazyLoader("node:stream/promises");
320288
const test = core.loadExtScript("ext:deno_node/testing.ts").default;
321289
const timers = core.loadExtScript("ext:deno_node/timers.ts");
322-
const timersPromises = core.loadExtScript(
323-
"ext:deno_node/timers/promises.ts",
324-
);
325290
const tls = core.createLazyLoader("node:tls");
326-
const traceEvents = core.loadExtScript("ext:deno_node/trace_events.ts").default;
327-
import tty from "node:tty";
291+
const lazyTty = core.createLazyLoader("node:tty");
328292
const url = core.loadExtScript("ext:deno_node/url.ts");
329-
const utilTypes = core.loadExtScript("ext:deno_node/internal/util/types.ts");
330293
const util = core.loadExtScript("ext:deno_node/util.ts");
331-
const v8 = core.loadExtScript("ext:deno_node/v8.ts");
332-
const vm = core.loadExtScript("ext:deno_node/vm.js").default;
333294
const workerThreads = core.loadExtScript(
334295
"ext:deno_node/worker_threads.ts",
335296
);
336-
const wasi = core.loadExtScript("ext:deno_node/wasi.ts").default;
337297
// zlib is lazy-loaded via `lazyNodeModules` below: zlib.js extends
338298
// `Transform` from `node:stream` at module body, so loading it eagerly
339299
// pulls the stream subtree into the snapshot.
@@ -390,6 +350,47 @@ const lazyNodeModules = {
390350
"internal/child_process": () =>
391351
core.loadExtScript("ext:deno_node/internal/child_process.ts").default,
392352
"stream/web": () => core.loadExtScript("ext:deno_node/stream/web.js"),
353+
"inspector": () => core.loadExtScript("ext:deno_node/inspector.js"),
354+
"inspector/promises": () =>
355+
core.loadExtScript("ext:deno_node/inspector/promises.js"),
356+
"perf_hooks": () => core.loadExtScript("ext:deno_node/perf_hooks.js").default,
357+
"querystring": () =>
358+
core.loadExtScript("ext:deno_node/querystring.js").default,
359+
"sqlite": () => core.loadExtScript("ext:deno_node/sqlite.ts"),
360+
"string_decoder": () =>
361+
core.loadExtScript("ext:deno_node/string_decoder.ts").default,
362+
"timers/promises": () =>
363+
core.loadExtScript("ext:deno_node/timers/promises.ts"),
364+
"trace_events": () =>
365+
core.loadExtScript("ext:deno_node/trace_events.ts").default,
366+
"util/types": () =>
367+
core.loadExtScript("ext:deno_node/internal/util/types.ts"),
368+
"v8": () => core.loadExtScript("ext:deno_node/v8.ts"),
369+
"vm": () => core.loadExtScript("ext:deno_node/vm.js").default,
370+
"wasi": () => core.loadExtScript("ext:deno_node/wasi.ts").default,
371+
"punycode": () => core.loadExtScript("ext:deno_node/punycode.ts").default,
372+
// Previously eager via static imports. Lazified together with the
373+
// process.stdio lazy-getter refactor.
374+
"net": () => lazyNet().default,
375+
"path": () => lazyPath().default,
376+
"path/posix": () => lazyPathPosix().default,
377+
"path/win32": () => lazyPathWin32().default,
378+
"stream": () => lazyStream().default,
379+
"stream/promises": () => lazyStreamPromises().default,
380+
"tty": () => lazyTty().default,
381+
"internal/tty": () => lazyInternalTty(),
382+
"internal/js_stream_socket": () =>
383+
core.loadExtScript("ext:deno_node/internal/js_stream_socket.js").default,
384+
"_stream_duplex": () =>
385+
core.loadExtScript("ext:deno_node/internal/streams/duplex.js").default,
386+
"_stream_passthrough": () =>
387+
core.loadExtScript("ext:deno_node/internal/streams/passthrough.js").default,
388+
"_stream_readable": () =>
389+
core.loadExtScript("ext:deno_node/internal/streams/readable.js").default,
390+
"_stream_transform": () =>
391+
core.loadExtScript("ext:deno_node/internal/streams/transform.js").default,
392+
"_stream_writable": () =>
393+
core.loadExtScript("ext:deno_node/internal/streams/writable.js").default,
393394
};
394395

395396
function defineLazyNativeModule(name, loader) {
@@ -414,11 +415,6 @@ function defineLazyNativeModule(name, loader) {
414415
// NOTE(bartlomieju): keep this list in sync with `ext/node/lib.rs`
415416
function setupBuiltinModules() {
416417
const nodeModules = {
417-
"_stream_duplex": _streamDuplex,
418-
"_stream_passthrough": _streamPassthrough,
419-
"_stream_readable": _streamReadable,
420-
"_stream_transform": _streamTransform,
421-
"_stream_writable": _streamWritable,
422418
assert,
423419
"async_hooks": asyncHooks,
424420
buffer,
@@ -431,8 +427,6 @@ function setupBuiltinModules() {
431427
domain,
432428
events,
433429
fs,
434-
inspector,
435-
"inspector/promises": inspectorPromises,
436430
"internal/assert/myers_diff": internalAssertMyersDiff.default,
437431
"internal/async_hooks": internalAsyncHooks,
438432
"internal/console/constructor": internalConsole,
@@ -459,12 +453,10 @@ function setupBuiltinModules() {
459453
"internal/streams/add-abort-signal": internalStreamsAddAbortSignal,
460454
"internal/streams/state": internalStreamsState,
461455
"internal/socketaddress": internalSocketAddress,
462-
"internal/js_stream_socket": internalJsStreamSocket,
463456
"internal/net": internalNet,
464457
"internal/options": internalOptions,
465458
"internal/test/binding": internalTestBinding,
466459
"internal/timers": internalTimers,
467-
"internal/tty": internalTty,
468460
"internal/url": internalUrl,
469461
"internal/util/debuglog": internalUtilDebuglog.default,
470462
"internal/util/inspect": internalUtilInspect,
@@ -475,40 +467,14 @@ function setupBuiltinModules() {
475467
"internal/webstreams/util": internalWebstreamsUtil,
476468
"internal/worker/js_transferable": internalWorkerJsTransferable,
477469
module: Module,
478-
net,
479470
os,
480-
"path/posix": pathPosix,
481-
"path/win32": pathWin32,
482-
path,
483-
perf_hooks: perfHooks,
484471
process,
485-
get punycode() {
486-
process.emitWarning(
487-
"The `punycode` module is deprecated. Please use a userland " +
488-
"alternative instead.",
489-
"DeprecationWarning",
490-
"DEP0040",
491-
);
492-
return punycode;
493-
},
494-
querystring,
495-
sqlite,
496-
stream,
497472
"stream/consumers": streamConsumers,
498-
"stream/promises": streamPromises,
499-
string_decoder: stringDecoder,
500473
sys: util,
501474
test,
502475
timers,
503-
"timers/promises": timersPromises,
504-
trace_events: traceEvents,
505-
tty,
506476
url,
507477
util,
508-
"util/types": utilTypes,
509-
v8,
510-
vm,
511-
wasi,
512478
worker_threads: workerThreads,
513479
};
514480
// Match Node's schemelessBlockList: these modules can only be imported
@@ -2470,6 +2436,11 @@ deprecatedNativeModules._stream_writable = [
24702436
"The _stream_writable module is deprecated. Use `node:stream` instead.",
24712437
"DEP0193",
24722438
];
2439+
deprecatedNativeModules.punycode = [
2440+
"The `punycode` module is deprecated. Please use a userland " +
2441+
"alternative instead.",
2442+
"DEP0040",
2443+
];
24732444

24742445
const emittedNativeModuleDeprecations = new SafeSet();
24752446
function maybeEmitNativeModuleDeprecation(request) {

0 commit comments

Comments
 (0)