From d34dcd2cdb38a6ed6b4e0e8be6eb10ad62258407 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sun, 14 Jun 2026 23:52:56 +0200 Subject: [PATCH] feat(runtime): gate node:dgram behind `mod-dgram` (binary size) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `node:dgram` (UDP sockets — `crate::dgram` + `crate::dgram_reactor`, ~43 KB incl. the `js_dgram_*` externs codegen emits direct calls to) is now compiled only when a program imports it. The compiler detects `module: "dgram"` in the HIR (a dgram namespace can only arise from the import) and forwards `perry-runtime/mod-dgram` to the auto-optimize build; a program that never uses dgram links none of it (hello-world −~0.1 MB). No speed change. This is the first instance of a general per-Node-module gating pattern: cfg the module impl + its `native_module_dispatch` arm + its gc root-scanner registration behind a `mod-` feature, enabled from compile-time usage detection. dgram is detected via HIR (it's runtime-only, so absent from `native_module_imports`, which tracks only stdlib modules). The dgram dispatch arm + the gc_init `dgram_reactor` root-scanner registration are cfg-gated so the off build links cleanly; codegen only emits `js_dgram_*` calls for dgram programs, which get the feature on, so no dangling symbols. --- crates/perry-runtime/Cargo.toml | 11 ++++++++++- crates/perry-runtime/src/gc/mod.rs | 1 + crates/perry-runtime/src/lib.rs | 4 ++++ .../src/object/native_module_dispatch.rs | 6 ++++++ crates/perry/src/commands/compile/collect_modules.rs | 6 ++++++ crates/perry/src/commands/compile/optimized_libs.rs | 10 +++++++++- crates/perry/src/commands/compile/types.rs | 9 +++++++++ 7 files changed, 45 insertions(+), 2 deletions(-) diff --git a/crates/perry-runtime/Cargo.toml b/crates/perry-runtime/Cargo.toml index b7878e8136..0b8b4f96f3 100644 --- a/crates/perry-runtime/Cargo.toml +++ b/crates/perry-runtime/Cargo.toml @@ -15,7 +15,16 @@ crate-type = ["rlib", "staticlib"] # actually needs (see optimized_libs.rs), so the heavy subsystems below # (regex engine, Temporal, URL/IDNA, normalize, segmenter) are *opt-in per # app* and absent from binaries that never use them. -default = ["full", "regex-engine", "temporal", "url-engine", "string-normalize", "intl-segmenter", "diagnostics"] +default = ["full", "regex-engine", "temporal", "url-engine", "string-normalize", "intl-segmenter", "diagnostics", "mod-dgram"] +# Per-module Node-API gate (binary-size): compiles `node:dgram`'s UDP-socket +# implementation (`crate::dgram` + `crate::dgram_reactor`, ~2.2k LOC, incl. the +# `js_dgram_*` externs codegen emits direct calls to) + its dispatch arm only +# when the program imports `dgram`. The compiler enables it on `module: "dgram"` +# usage (see the auto-optimize import-detection path), so a program that never +# imports `dgram` links none of its code. Pure cfg gate, no extra deps. (This is +# the first instance of a general per-module gating pattern; more modules can +# follow the same shape.) +mod-dgram = [] # Cold-path diagnostic JSON serializers (~67 KB of code + the `serde_json` # pulled only by them, which dead-strips when unreferenced): GC cycle telemetry # (`PERRY_GC_DIAG`), typed-feedback trace dump (`PERRY_TYPED_FEEDBACK`), the v8 diff --git a/crates/perry-runtime/src/gc/mod.rs b/crates/perry-runtime/src/gc/mod.rs index ab6a35fa88..4ab5a78a5d 100644 --- a/crates/perry-runtime/src/gc/mod.rs +++ b/crates/perry-runtime/src/gc/mod.rs @@ -362,6 +362,7 @@ pub fn gc_init() { // #4911: a bound node:dgram socket is reachable only from the dgram // reactor's registry while its recv thread runs; scan + rewrite it so a GC // between ticks doesn't reclaim the object whose `message` handlers fire. + #[cfg(feature = "mod-dgram")] gc_register_mutable_root_scanner(crate::dgram_reactor::scan_roots_mut); gc_register_mutable_root_scanner(json_parse_mutable_root_scanner); gc_register_mutable_root_scanner(intern_table_mutable_root_scanner); diff --git a/crates/perry-runtime/src/lib.rs b/crates/perry-runtime/src/lib.rs index ab372b5fde..913d3ea30f 100644 --- a/crates/perry-runtime/src/lib.rs +++ b/crates/perry-runtime/src/lib.rs @@ -41,7 +41,9 @@ pub mod collection_iter; pub mod collection_iter_object; pub mod color_parse; pub mod date; +#[cfg(feature = "mod-dgram")] pub mod dgram; +#[cfg(feature = "mod-dgram")] pub mod dgram_reactor; pub mod disposable; pub mod dns; @@ -393,6 +395,7 @@ mod stdlib_pump { // #4911: deliver queued UDP datagrams as `'message'` events. Lives in // perry-runtime so node:dgram works without perry-stdlib linked. // Zero-cost (one relaxed load) when no sockets are bound. + #[cfg(feature = "mod-dgram")] crate::dgram_reactor::pump(); crate::process::js_process_ipc_drain(); let f = STDLIB_PUMP_FN.load(Ordering::Acquire); @@ -431,6 +434,7 @@ mod stdlib_pump { return 1; } // #4911: a bound + `ref`'d node:dgram socket keeps the loop alive. + #[cfg(feature = "mod-dgram")] if crate::dgram_reactor::has_active() { return 1; } diff --git a/crates/perry-runtime/src/object/native_module_dispatch.rs b/crates/perry-runtime/src/object/native_module_dispatch.rs index 56133e1f37..fa98f19565 100644 --- a/crates/perry-runtime/src/object/native_module_dispatch.rs +++ b/crates/perry-runtime/src/object/native_module_dispatch.rs @@ -1524,6 +1524,12 @@ pub(crate) unsafe fn dispatch_native_module_method( ("punycode.ucs2", "encode") => crate::punycode::js_punycode_ucs2_encode(arg(0)), // ── dgram namespace (`node:dgram` / `dgram`) ── + // Gated behind `mod-dgram`: `crate::dgram` is only compiled when the + // program imports `dgram` (the compiler enables the feature on + // `module: "dgram"` usage), so this arm — and the `js_dgram_*` externs + // it calls — are absent otherwise. Unreachable when off (a dgram + // namespace can't exist without the import that enables the feature). + #[cfg(feature = "mod-dgram")] ("dgram", "createSocket") | ("dgram", "Socket") => { crate::dgram::js_dgram_create_socket(pack_args()) } diff --git a/crates/perry/src/commands/compile/collect_modules.rs b/crates/perry/src/commands/compile/collect_modules.rs index 8ad98fb8a1..4d36a3bf7c 100644 --- a/crates/perry/src/commands/compile/collect_modules.rs +++ b/crates/perry/src/commands/compile/collect_modules.rs @@ -1942,6 +1942,12 @@ fn collect_module_finish( { ctx.uses_diagnostics = true; } + // `node:dgram` (UDP) → gates `perry-runtime/mod-dgram` (~43 KB; dgram + // lowers to `NativeMethodCall { module: "dgram" }`, runtime-only so not + // in `native_module_imports`). + if hir_debug.contains("module: \"dgram\"") { + ctx.uses_dgram = true; + } } // Detect readline usage via process.stdin raw/lifecycle methods. These diff --git a/crates/perry/src/commands/compile/optimized_libs.rs b/crates/perry/src/commands/compile/optimized_libs.rs index 83d58ba849..b9c62b206d 100644 --- a/crates/perry/src/commands/compile/optimized_libs.rs +++ b/crates/perry/src/commands/compile/optimized_libs.rs @@ -670,7 +670,7 @@ pub(super) fn build_optimized_libs( // Cheap djb2 — no need for the SipHash overhead. let target_str = target.unwrap_or("host"); let key_input = format!( - "{}|{}|{}|wasm={}|regex={}|temporal={}|url={}|norm={}|seg={}|diag={}|v={}", + "{}|{}|{}|wasm={}|regex={}|temporal={}|url={}|norm={}|seg={}|diag={}|dgram={}|v={}", feature_arg, panic_abort_safe, target_str, @@ -681,6 +681,7 @@ pub(super) fn build_optimized_libs( ctx.uses_string_normalize, ctx.uses_intl_segmenter, ctx.uses_diagnostics, + ctx.uses_dgram, env!("CARGO_PKG_VERSION"), ); let mut hash: u64 = 5381; @@ -804,6 +805,13 @@ pub(super) fn build_optimized_libs( if ctx.uses_diagnostics { cross_features.push("perry-runtime/diagnostics".to_string()); } + // Per-Node-module gating: `node:dgram`'s implementation + dispatch arm are + // behind `mod-dgram`, enabled only when the program uses dgram (detected via + // `module: "dgram"` in the HIR). codegen only emits the `js_dgram_*` externs + // for dgram programs, so detection is complete (no dangling symbols). + if ctx.uses_dgram { + cross_features.push("perry-runtime/mod-dgram".to_string()); + } if !cross_features.is_empty() { cargo_cmd.arg("--features").arg(cross_features.join(",")); } diff --git a/crates/perry/src/commands/compile/types.rs b/crates/perry/src/commands/compile/types.rs index 782cb99e6b..1f6b791cd5 100644 --- a/crates/perry/src/commands/compile/types.rs +++ b/crates/perry/src/commands/compile/types.rs @@ -578,6 +578,14 @@ pub struct CompilationContext { /// the same feature and degrade gracefully when it's off, so they're absent /// from size-optimized binaries unless one of these APIs is also used. pub uses_diagnostics: bool, + /// Whether any TS module imports `node:dgram` (UDP sockets). Gates + /// `perry-runtime/mod-dgram` (`crate::dgram` + `crate::dgram_reactor`, + /// ~43 KB, incl. the `js_dgram_*` externs codegen emits direct calls to). + /// Detected from `module: "dgram"` in the HIR (a `dgram` namespace can only + /// arise from importing it), so a program that never imports `dgram` links + /// none of it. NB: not via `native_module_imports`, which only tracks + /// `requires_stdlib` modules — dgram is runtime-only. + pub uses_dgram: bool, /// Whether `perry/thread` is imported. When true, the runtime must /// keep `panic = "unwind"` so that worker-thread panics translate to /// promise rejections via `catch_unwind` in `perry-runtime/src/thread.rs` @@ -827,6 +835,7 @@ impl CompilationContext { uses_string_normalize: false, uses_intl_segmenter: false, uses_diagnostics: false, + uses_dgram: false, needs_thread: false, cross_module_class_field_types: HashMap::new(), min_windows_version: "10".to_string(),