From 6fd90aea82661f654cfbcf80d90c057b3db89397 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Fri, 3 Jul 2026 23:34:47 +0000 Subject: [PATCH 1/2] Bytecode cache for the JS builtins in bun build --compile --bytecode A compiled binary already ships bytecode for the app's own modules, but every builtin it touches (node:net, node:fs, the internal/* modules they require) is still parsed from source on first use. That is the bulk of the parse work left at startup. Builtins are compiled with createBuiltinExecutable() into an UnlinkedFunctionExecutable, which the bytecode cache had no entry type for; oven-sh/WebKit#270 adds encodeFunctionExecutable()/decodeFunctionExecutable() for exactly this. WEBKIT_VERSION points at that PR's preview build for now and must be re-pointed at the merged main sha before landing. Unlike the other bytecode generation steps, this one has to walk a graph the bundler cannot see. The bundle's import records name only the builtins the app imports directly; those then require() each other through InternalModuleRegistry by numeric id, resolved at runtime. bundle-modules.ts already derives that graph from the bundled output to lay the source blob out in dependency order; it now also emits it as an adjacency table, and caching "net" means caching node:net plus every internal/* module it transitively reaches. The entries are embedded in the standalone module graph and looked up by module id on first require, behind a flag the graph sets at startup so a Bun that embeds nothing never takes the lookup. JSC validates each entry against a cache version and a SourceCodeKey over the builtin's source; the runtime and the generator obtain that source and build that SourceCode through the same two functions, so a build's entries are always keyed on what the same build parses. A rejected or missing entry parses in builtin mode exactly as before. Cross-compiles skip generation: src/js is specialized per platform at Bun's own build time, so the key would never match. The bytecode VM only registers Bun's private names (JSVMClientData:: registerBuiltinNames) rather than creating full client data: the builtin-mode lexer needs the names, and nothing else in client data applies to a VM with no Bun VirtualMachine behind it. Measured on a debug build running require("node:net") + node:http + node:fs: ~1.3s to ~0.77s startup; 69 builtins served from the cache. --- scripts/build/deps/webkit.ts | 6 +- src/bundler/BundleThread.rs | 3 + src/bundler/bundle_v2.rs | 120 +++++++++++++ src/bundler/options.rs | 6 + src/codegen/bundle-modules.ts | 119 +++++++++++-- src/js/internal-for-testing.ts | 8 + src/jsc/CachedBytecode.rs | 73 ++++++++ src/jsc/bindings/BuiltinModuleBytecode.cpp | 164 ++++++++++++++++++ src/jsc/bindings/BuiltinModuleBytecode.h | 33 ++++ src/jsc/bindings/BunClientData.cpp | 10 ++ src/jsc/bindings/BunClientData.h | 7 + src/jsc/bindings/InternalModuleRegistry.cpp | 65 +++---- src/jsc/bindings/ZigSourceProvider.cpp | 19 +- src/jsc/bindings/ZigSourceProvider.h | 5 + src/jsc/lib.rs | 9 +- src/runtime/api/js_bundle_completion_task.rs | 20 ++- src/runtime/cli/build_command.rs | 12 +- src/standalone_graph/StandaloneModuleGraph.rs | 145 ++++++++++++++-- test/bundler/bundler_compile.test.ts | 49 ++++++ 19 files changed, 782 insertions(+), 91 deletions(-) create mode 100644 src/jsc/bindings/BuiltinModuleBytecode.cpp create mode 100644 src/jsc/bindings/BuiltinModuleBytecode.h diff --git a/scripts/build/deps/webkit.ts b/scripts/build/deps/webkit.ts index 0b1c956da743..ee5fded8a39c 100644 --- a/scripts/build/deps/webkit.ts +++ b/scripts/build/deps/webkit.ts @@ -3,7 +3,11 @@ * for local mode. Override via `--webkit-version=` to test a branch. * From https://github.com/oven-sh/WebKit releases. */ -export const WEBKIT_VERSION = "f0f60fd2324817dae9656d8bf2fcae25ceaccc37"; +// Temporarily a PR preview build: the builtin bytecode cache needs +// encodeFunctionExecutable()/decodeFunctionExecutable(), added in +// https://github.com/oven-sh/WebKit/pull/270 (rebased onto f0f60fd2, the sha +// main pins). Re-point this at the merged main autobuild sha before landing. +export const WEBKIT_VERSION = "autobuild-preview-pr-270-ce6f2091"; /** * WebKit (JavaScriptCore) — the JS engine. diff --git a/src/bundler/BundleThread.rs b/src/bundler/BundleThread.rs index cef2a0487354..1348e5d85894 100644 --- a/src/bundler/BundleThread.rs +++ b/src/bundler/BundleThread.rs @@ -24,6 +24,9 @@ pub struct BuildResult { pub output_files: Vec, pub metafile: Option>, pub metafile_markdown: Option>, + /// `(InternalModuleRegistry id, JSC cache entry)` for each JS builtin the bundle can + /// reach, for `--compile --bytecode` to embed. Empty otherwise. + pub builtin_bytecode: Vec<(u32, Box<[u8]>)>, } pub enum BundleV2Result { diff --git a/src/bundler/bundle_v2.rs b/src/bundler/bundle_v2.rs index f9be9d28d01a..3076a60721b4 100644 --- a/src/bundler/bundle_v2.rs +++ b/src/bundler/bundle_v2.rs @@ -1377,6 +1377,19 @@ pub mod bv2_impl { source: &[u8], source_provider_url: &mut bun_core::String, ) -> Option>; + + /// Defined `#[no_mangle]` in `bun_jsc::cached_bytecode`. Serializes one JS + /// builtin's compiled tree. `None` for native modules and compile failures. + safe fn __bun_jsc_generate_builtin_module_bytecode(module_id: u32) + -> Option>; + + /// Defined `#[no_mangle]` in `bun_jsc::cached_bytecode`. The builtins + /// `module_id` requires directly, as a view into a static C++ table. + safe fn __bun_jsc_builtin_module_dependencies(module_id: u32) -> &'static [u32]; + + /// Defined `#[no_mangle]` in `bun_jsc::cached_bytecode`. Canonical builtin + /// specifier (`b"node:net"`) to `InternalModuleRegistry` id. + safe fn __bun_jsc_builtin_module_id_for_specifier(specifier: &[u8]) -> Option; } unsafe extern "Rust" { @@ -1418,6 +1431,24 @@ pub mod bv2_impl { __bun_jsc_generate_cached_bytecode(format, source, source_provider_url) } + /// Bytecode cache entry for one JS builtin, covering its whole nested tree. + #[inline] + pub fn generate_builtin_module_bytecode(module_id: u32) -> Option> { + __bun_jsc_generate_builtin_module_bytecode(module_id) + } + + /// The builtins `module_id` requires directly. + #[inline] + pub fn builtin_module_dependencies(module_id: u32) -> &'static [u32] { + __bun_jsc_builtin_module_dependencies(module_id) + } + + /// `InternalModuleRegistry` id for a canonical builtin specifier. + #[inline] + pub fn builtin_module_id_for_specifier(specifier: &[u8]) -> Option { + __bun_jsc_builtin_module_id_for_specifier(specifier) + } + /// CYCLEBREAK GENUINE: `JSBundleCompletionTask` — the /// concrete struct lives in `bun_runtime` (its fields name `Config`/ /// `Plugin`/`HTMLBundle::Route`). The bundler reads exactly two things @@ -3877,6 +3908,70 @@ pub mod bv2_impl { (fetcher.on_fetch)(fetcher.ctx, &mut result) } + /// Bytecode cache entries for every JS builtin a `bun build --compile --bytecode` + /// binary can reach, sorted by module id. + /// + /// The bundle's import records only name the builtins the app imports directly + /// (`node:net`). Those then `require()` each other through `InternalModuleRegistry`, + /// which the bundler never sees, so the set is closed over the internal dependency + /// graph: caching "net" means caching `node:net` plus every `internal/*` module it + /// transitively reaches. + pub fn collect_builtin_module_bytecode( + &mut self, + reachable_files: &[Index], + ) -> Vec<(u32, Box<[u8]>)> { + use crate::bundle_v2::dispatch as jsc; + use bun_resolve_builtins::HardcodedModule; + + let cfg = HardcodedModule::Cfg::default(); + let import_records = self.graph.ast.items_import_records(); + + let mut pending: Vec = Vec::new(); + for source_index in reachable_files { + let records: &[ImportRecord] = + import_records[source_index.get() as usize].as_slice(); + for record in records { + if record.source_index.is_valid() { + continue; + } + if !matches!( + record.tag, + bun_ast::ImportRecordTag::Builtin | bun_ast::ImportRecordTag::Bun + ) { + continue; + } + // `resolve_import_records` strips the `node:` prefix from the record, so + // go back through the alias table for the canonical specifier the tag + // table is keyed on. + let Some(alias) = + HardcodedModule::Alias::get(record.path.text, Target::Bun, cfg) + else { + continue; + }; + let Some(id) = jsc::builtin_module_id_for_specifier(alias.path.as_bytes()) + else { + continue; + }; + pending.push(id); + } + } + + let mut reached: std::collections::BTreeSet = std::collections::BTreeSet::new(); + while let Some(id) = pending.pop() { + if !reached.insert(id) { + continue; + } + pending.extend_from_slice(jsc::builtin_module_dependencies(id)); + } + + // Native modules have no JS source, so `generate_builtin_module_bytecode` + // returns `None` for them — as it does for anything that fails to compile. + reached + .into_iter() + .filter_map(|id| Some((id, jsc::generate_builtin_module_bytecode(id)?))) + .collect() + } + pub fn generate_from_cli( transpiler: &'a mut Transpiler<'a>, alloc: &'a bun_alloc::Arena, @@ -3971,9 +4066,21 @@ pub mod bv2_impl { output_files: Vec::new(), metafile: None, metafile_markdown: None, + builtin_bytecode: Vec::new(), }); } + // Before the chunk bytecode runs, so the JS builtins are compiled on this + // thread's bytecode VM rather than racing the linker's pool for one. + let builtin_bytecode = if this.transpiler.options.compile_mode.is_executable() + && this.transpiler.options.bytecode + && this.transpiler.options.compile_target_is_host + { + this.collect_builtin_module_bytecode(&reachable_files) + } else { + Vec::new() + }; + let output_files = crate::linker_context_mod::generate_chunks_in_parallel::( &mut this.linker, &mut chunks, @@ -4001,6 +4108,7 @@ pub mod bv2_impl { output_files, metafile, metafile_markdown: None, + builtin_bytecode, }) })(); @@ -5090,6 +5198,17 @@ pub mod bv2_impl { return Err(crate::Error::BuildFailed); } + // Before the chunk bytecode runs, so the JS builtins are compiled on this + // thread's bytecode VM rather than racing the linker's pool for one. + let builtin_bytecode = if self.transpiler.options.compile_mode.is_executable() + && self.transpiler.options.bytecode + && self.transpiler.options.compile_target_is_host + { + self.collect_builtin_module_bytecode(&reachable_files) + } else { + Vec::new() + }; + let mut output_files = crate::linker_context_mod::generate_chunks_in_parallel::( &mut self.linker, &mut chunks, @@ -5156,6 +5275,7 @@ pub mod bv2_impl { output_files, metafile, metafile_markdown, + builtin_bytecode, }) } } diff --git a/src/bundler/options.rs b/src/bundler/options.rs index 66e0c833cd85..c050d8b4aa48 100644 --- a/src/bundler/options.rs +++ b/src/bundler/options.rs @@ -1290,6 +1290,10 @@ pub struct BundleOptions<'a> { pub debugger: bool, pub compile_mode: CompileMode, + /// `--compile` without a cross `--target`. The JS builtins are specialized per platform + /// at Bun's own build time, so their bytecode cache is only worth embedding when the + /// executable will run on the platform that generated it. + pub compile_target_is_host: bool, pub metafile: bool, /// Path to write JSON metafile (for Bun.build API) pub metafile_json_path: Box<[u8]>, @@ -1470,6 +1474,7 @@ impl<'a> BundleOptions<'a> { code_coverage: self.code_coverage, debugger: self.debugger, compile_mode: self.compile_mode, + compile_target_is_host: self.compile_target_is_host, metafile: self.metafile, metafile_json_path: self.metafile_json_path.clone(), metafile_markdown_path: self.metafile_markdown_path.clone(), @@ -1714,6 +1719,7 @@ impl<'a> BundleOptions<'a> { code_coverage: false, debugger: false, compile_mode: CompileMode::None, + compile_target_is_host: false, metafile: false, metafile_json_path: Box::default(), metafile_markdown_path: Box::default(), diff --git a/src/codegen/bundle-modules.ts b/src/codegen/bundle-modules.ts index 92d7d60d11eb..852c357b50bb 100644 --- a/src/codegen/bundle-modules.ts +++ b/src/codegen/bundle-modules.ts @@ -296,20 +296,37 @@ mark("Postprocesss modules"); * deterministic; the graph over-approximates lazy requires, which only makes * neighbours of things that *might* co-load — free for layout. */ -function layoutOrder(modules: string[], outputs: Map): string[] { - // The bundled sources already have require() rewritten to registry lookups, - // `internalModuleRegistry, ` — the index is the position in - // `modules`, which makes the edge list unambiguous. +/** + * The static require() graph between builtins: `requireGraph[i]` is the sorted list of + * module indices that module `i` requires directly. The bundled sources already have + * every require() rewritten to a registry lookup, `internalModuleRegistry, `, + * where the index is the position in `modules`, so the edge list is unambiguous. + * + * These edges are resolved at runtime through InternalModuleRegistry and are invisible + * to the JS bundler, which is why both consumers below need them precomputed: the blob + * layout wants dependencies adjacent to their dependents, and `--compile --bytecode` + * has to embed bytecode for everything a requested builtin can transitively reach. + */ +function requireGraph(modules: string[], outputs: Map): number[][] { const requireRe = /internalModuleRegistry, ?(\d+)/g; - const deps = new Map(); - for (const id of modules) { + return modules.map((id, i) => { const src = outputs.get(id.slice(0, -3).replaceAll("/", path.sep)) ?? ""; - const edges = new Set(); + const edges = new Set(); for (const m of src.matchAll(requireRe)) { - const dep = modules[Number(m[1])]; - if (dep && dep !== id) edges.add(dep); + const dep = Number(m[1]); + if (dep < modules.length && dep !== i) edges.add(dep); } - deps.set(id, [...edges]); + return [...edges].sort((a, b) => a - b); + }); +} + +function layoutOrder(modules: string[], graph: number[][]): string[] { + const deps = new Map(); + for (let i = 0; i < modules.length; i++) { + deps.set( + modules[i], + graph[i].map(dep => modules[dep]), + ); } const hotRoots = [ "node/fs.ts", @@ -368,6 +385,16 @@ function idToPublicSpecifierOrEnumName(id: string) { return idToEnumName(id); } +/** The `builtin://` origin a module is parsed under. Part of its bytecode cache key. */ +function moduleUrl(id: string) { + return "builtin://" + id.replace(/\.[mc]?[tj]s$/, "").replace(/[^a-zA-Z0-9]+/g, "/"); +} + +/** The bundled file debug builds read the module from, relative to the build's js/ dir. */ +function moduleFile(id: string) { + return id.replace(/\.[mc]?[tj]s$/, ".js"); +} + const { combinedSourceCode: functionsSource } = await bundleBuiltinFunctions({ requireTransformer, }); @@ -409,12 +436,15 @@ writeIfNotChanged( // // In debug builds the module sources are read from disk (BUN_DYNAMIC_JS_LOAD_PATH), // so every module offset/length is 0. The functions span is still real in debug. +const jsModules = moduleList.slice(0, nativeStartIndex); +const jsModuleGraph = requireGraph(jsModules, outputs); + const moduleSpans: { enumName: string; offset: number; length: number }[] = []; let blob: Buffer; { const chunks: Buffer[] = [Buffer.from(functionsSource + "\0", "latin1")]; let offset = chunks[0].length; - for (const id of layoutOrder(moduleList.slice(0, nativeStartIndex), outputs)) { + for (const id of layoutOrder(jsModules, jsModuleGraph)) { const enumName = idToEnumName(id); if (debug) { moduleSpans.push({ enumName, offset: 0, length: 0 }); @@ -458,7 +488,7 @@ BUN_SYM(bun_internal_modules_data): `, ); -// Offset/length table. Included only by InternalModuleRegistry.cpp. +// Offset/length table. Included only by BuiltinModuleBytecode.cpp. writeIfNotChanged( path.join(CODEGEN_DIR, "InternalModuleRegistryConstants.h"), `// clang-format off @@ -484,6 +514,64 @@ ${moduleSpans `, ); +// Per-index view of the same data for BuiltinModuleBytecode.cpp, which has to get at a +// builtin by the numeric id InternalModuleRegistry hands it: where its source is, the +// name/url it is parsed under (these must match createInternalModuleById exactly, since +// they are part of the bytecode cache key), where to find it on disk in debug builds, and +// which builtins it requires. The dependency edges are the require graph computed above. +{ + const dependencyOffsets: number[] = [0]; + const dependencyIds: number[] = []; + for (const edges of jsModuleGraph) { + dependencyIds.push(...edges); + dependencyOffsets.push(dependencyIds.length); + } + + writeIfNotChanged( + path.join(CODEGEN_DIR, "InternalModuleRegistry+builtinBytecode.h"), + `// clang-format off +// Generated by src/codegen/bundle-modules.ts +#pragma once +#include "InternalModuleRegistryConstants.h" +#include + +namespace Bun { +namespace InternalModuleRegistryBuiltins { + +struct Module { + uint32_t sourceOffset; + uint32_t sourceLength; + ASCIILiteral name; + ASCIILiteral url; + ASCIILiteral file; +}; + +// Indexed by InternalModuleRegistry::Field. Ids at or above jsModuleCount are native +// modules: no JS source, nothing to cache. +static constexpr unsigned jsModuleCount = ${nativeStartIndex}; +static constexpr Module modules[] = { +${jsModules + .map(id => { + const enumName = idToEnumName(id); + return ( + ` { InternalModuleRegistryConstants::${enumName}CodeOffset, InternalModuleRegistryConstants::${enumName}CodeLength, ` + + `"${idToPublicSpecifierOrEnumName(id)}"_s, "${moduleUrl(id)}"_s, ${JSON.stringify(moduleFile(id))}_s },` + ); + }) + .join("\n")} +}; + +// dependencyIds[dependencyOffsets[id] .. dependencyOffsets[id + 1]) are the builtins that +// builtin \`id\` requires directly. +static constexpr uint32_t dependencyOffsets[] = { ${dependencyOffsets.join(", ")} }; +static constexpr uint32_t dependencyIds[] = { ${dependencyIds.length ? dependencyIds.join(", ") : "0"} }; + +} // namespace InternalModuleRegistryBuiltins +} // namespace Bun +`, + ); +} + // This code slice is used in InternalModuleRegistry.cpp. It defines the loading function for modules. writeIfNotChanged( path.join(CODEGEN_DIR, "InternalModuleRegistry+createInternalModuleById.h"), @@ -494,15 +582,10 @@ JSValue InternalModuleRegistry::createInternalModuleById(JSGlobalObject* globalO // JS internal modules ${moduleList .map((id, n) => { - const moduleName = idToPublicSpecifierOrEnumName(id); - const fileBase = JSON.stringify(id.replace(/\.[mc]?[tj]s$/, ".js")); - const urlString = "builtin://" + id.replace(/\.[mc]?[tj]s$/, "").replace(/[^a-zA-Z0-9]+/g, "/"); const inner = n >= nativeStartIndex ? `return generateNativeModule(globalObject, vm, generateNativeModule_${nativeModuleEnums[id]});` - : `INTERNAL_MODULE_REGISTRY_GENERATE(globalObject, vm, "${moduleName}"_s, ${fileBase}_s, ` + - `InternalModuleRegistryConstants::${idToEnumName(id)}CodeOffset, ` + - `InternalModuleRegistryConstants::${idToEnumName(id)}CodeLength, "${urlString}"_s);`; + : `INTERNAL_MODULE_REGISTRY_GENERATE(globalObject, vm, ${n}, "${idToPublicSpecifierOrEnumName(id)}"_s, "${moduleUrl(id)}"_s);`; return `case Field::${idToEnumName(id)}: { ${inner} }`; diff --git a/src/js/internal-for-testing.ts b/src/js/internal-for-testing.ts index 9b1fed3e91d9..74c66a7baf92 100644 --- a/src/js/internal-for-testing.ts +++ b/src/js/internal-for-testing.ts @@ -15,6 +15,14 @@ export const escapePowershell = (code: string) => fmtBinding(code, "escape-power export const canonicalizeIP = $newCppFunction("NodeTLS.cpp", "Bun__canonicalizeIP", 1); +// How many JS builtins this process loaded from an embedded bytecode cache rather than +// parsing. Only ever nonzero inside a `bun build --compile --bytecode` executable. +export const builtinModuleBytecodeDecodedCount: () => number = $newCppFunction( + "BuiltinModuleBytecode.cpp", + "Bun__builtinModuleBytecodeDecodedCount", + 0, +); + // Runtime-dispatched SIMD xxHash3 kernel (src/jsc/bindings/xxhash3.cpp), driven // directly so tests can exercise the Highway path independent of Bun.hash. export const xxHash3ForTesting: (view: ArrayBufferView, seed?: number | bigint) => bigint = $newCppFunction( diff --git a/src/jsc/CachedBytecode.rs b/src/jsc/CachedBytecode.rs index c9b2056d4eb0..4b80a85bfc3b 100644 --- a/src/jsc/CachedBytecode.rs +++ b/src/jsc/CachedBytecode.rs @@ -27,6 +27,22 @@ unsafe extern "C" { cached_bytecode: *mut Option>, ) -> bool; + /// Defined in `BuiltinModuleBytecode.cpp`. Compiles one JS builtin (and every + /// function nested inside it) and serializes the whole tree. + fn Bun__generateBuiltinModuleBytecode( + module_id: u32, + output_byte_code: *mut Option>, + output_byte_code_size: *mut usize, + cached_bytecode: *mut Option>, + ) -> bool; + + /// The builtins `module_id` requires directly, as a view into a static table. + fn Bun__builtinModuleDependencies( + module_id: u32, + output_ids: *mut *const u32, + output_len: *mut usize, + ); + // safe: `CachedBytecode` is an `opaque_ffi!` ZST handle (`!Freeze` via // `UnsafeCell`); `&mut` is ABI-identical to a non-null `*mut` and the C++ // refcount decrement is interior to the cell. @@ -144,3 +160,60 @@ pub(crate) fn __bun_jsc_generate_cached_bytecode( CachedBytecode__deref(CachedBytecode::opaque_mut(handle.as_ptr())); Some(owned) } + +/// Link-time entry point for `bun_bundler`. Same thread setup as +/// [`__bun_jsc_generate_cached_bytecode`], but for one JS builtin: `module_id` indexes +/// `InternalModuleRegistry`, and the returned blob covers that builtin's top-level +/// function plus every function nested inside it. +/// +/// Returns `None` for native modules, out-of-range ids, and any module whose source +/// fails to compile, which leaves the builtin to be parsed at runtime as usual. +#[unsafe(no_mangle)] +pub(crate) fn __bun_jsc_generate_builtin_module_bytecode(module_id: u32) -> Option> { + crate::virtual_machine::IS_BUNDLER_THREAD_FOR_BYTECODE_CACHE.set(true); + crate::initialize(false); + + let mut handle: Option> = None; + let mut size: usize = 0; + let mut ptr: Option> = None; + // SAFETY: out-params are valid for write. + let ok = unsafe { + Bun__generateBuiltinModuleBytecode(module_id, &raw mut ptr, &raw mut size, &raw mut handle) + }; + if !ok { + return None; + } + + // SAFETY: on success C++ guarantees both out-params are non-null and the bytes stay + // valid until the handle is deref'd. + let bytes = unsafe { bun_core::ffi::slice(ptr.unwrap().as_ptr(), size) }; + let owned = Box::<[u8]>::from(bytes); + CachedBytecode__deref(CachedBytecode::opaque_mut(handle.unwrap().as_ptr())); + Some(owned) +} + +/// Link-time entry point for `bun_bundler`. The `InternalModuleRegistry` id behind a +/// canonical builtin specifier (`b"node:net"`), or `None` if it isn't a builtin. +#[unsafe(no_mangle)] +pub(crate) fn __bun_jsc_builtin_module_id_for_specifier(specifier: &[u8]) -> Option { + crate::ResolvedSourceTag::try_from_name(specifier) + .and_then(crate::ResolvedSourceTag::internal_module_id) +} + +/// Link-time entry point for `bun_bundler`. The builtins `module_id` requires directly. +/// +/// These edges are `@createInternalModuleById(N)` calls the builtin bundler emits for each +/// `require()` between builtins. They resolve at runtime through `InternalModuleRegistry`, +/// so they are invisible to the JS bundler — the bytecode cache has to walk them itself. +#[unsafe(no_mangle)] +pub(crate) fn __bun_jsc_builtin_module_dependencies(module_id: u32) -> &'static [u32] { + let mut ids: *const u32 = core::ptr::null(); + let mut len: usize = 0; + // SAFETY: out-params are valid for write. + unsafe { Bun__builtinModuleDependencies(module_id, &raw mut ids, &raw mut len) }; + if ids.is_null() || len == 0 { + return &[]; + } + // SAFETY: C++ hands back a view into a `static constexpr` table. + unsafe { core::slice::from_raw_parts(ids, len) } +} diff --git a/src/jsc/bindings/BuiltinModuleBytecode.cpp b/src/jsc/bindings/BuiltinModuleBytecode.cpp new file mode 100644 index 000000000000..436f8ae89d3b --- /dev/null +++ b/src/jsc/bindings/BuiltinModuleBytecode.cpp @@ -0,0 +1,164 @@ +#include "BuiltinModuleBytecode.h" + +#include "ZigSourceProvider.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "InternalModuleRegistry+builtinBytecode.h" + +namespace Bun { + +using namespace JSC; +namespace Builtins = InternalModuleRegistryBuiltins; + +WTF::String builtinModuleSource(unsigned moduleId) +{ + if (moduleId >= Builtins::jsModuleCount) + return {}; + const auto& module = Builtins::modules[moduleId]; + +#ifdef BUN_DYNAMIC_JS_LOAD_PATH + // Debug builds read the bundled module off disk so that editing src/js does not + // require relinking. + WTF::String file = makeString(ASCIILiteral::fromLiteralUnsafe(BUN_DYNAMIC_JS_LOAD_PATH), "/"_s, module.file); + auto contents = WTF::FileSystemImpl::readEntireFile(file); + if (!contents) { + printf("\nFATAL: bun-debug failed to load bundled version of \"%s\" at \"%s\" (was it deleted?)\n" + "Please re-compile Bun to continue.\n\n", + module.name.characters(), file.utf8().span().data()); + CRASH(); + } + return WTF::String::fromUTF8(contents.value()); +#else + // The sources are linked into the executable as one read-only blob + // (bun_internal_modules_data, see the generated InternalModuleRegistryConstants.S). + return WTF::String(WTF::StringImpl::createWithoutCopying(std::span(bun_internal_modules_data + module.sourceOffset, module.sourceLength))); +#endif +} + +JSC::SourceCode builtinModuleSourceCode(const WTF::String& source, const WTF::String& moduleName, const WTF::String& urlString) +{ + return JSC::makeSource(source, SourceOrigin(WTF::URL(urlString)), JSC::SourceTaintedOrigin::Untainted, moduleName); +} + +JSC::UnlinkedFunctionExecutable* createBuiltinModuleExecutable(JSC::VM& vm, const JSC::SourceCode& source, const WTF::String& moduleName) +{ + return createBuiltinExecutable( + vm, source, + Identifier::fromString(vm, moduleName), + ImplementationVisibility::Public, + ConstructorKind::None, + ConstructAbility::CannotConstruct, + InlineAttribute::None); +} + +// Looks up one entry in the embedded section. Defined in `StandaloneModuleGraph.rs`. +extern "C" bool Bun__getBuiltinModuleBytecode(unsigned moduleId, uint8_t** outBytes, size_t* outLength); + +// Only a `--compile --bytecode` executable carries builtin bytecode. Every other Bun process +// compiles its builtins from source, and this is on the path of every one of them, so gate on +// a plain flag the standalone graph sets at startup rather than calling across into Rust. +static std::atomic s_hasBuiltinModuleBytecode { false }; + +extern "C" void Bun__setHasBuiltinModuleBytecode() +{ + s_hasBuiltinModuleBytecode.store(true, std::memory_order_relaxed); +} + +// How many builtins have been loaded from the embedded cache rather than parsed. Read by +// `bun:internal-for-testing` so the `--compile --bytecode` tests can tell the two apart. +static std::atomic s_builtinsLoadedFromBytecode { 0 }; + +JSC::UnlinkedFunctionExecutable* decodeBuiltinModuleBytecode(JSC::JSGlobalObject* globalObject, JSC::VM& vm, const JSC::SourceCode& source, const WTF::String& moduleName, unsigned moduleId) +{ + if (!s_hasBuiltinModuleBytecode.load(std::memory_order_relaxed)) + return nullptr; + + uint8_t* bytes = nullptr; + size_t length = 0; + if (!Bun__getBuiltinModuleBytecode(moduleId, &bytes, &length) || !length) + return nullptr; + + // The entry was keyed on an empty code generation mode. A debugger or a profiler changes + // what the runtime would generate, so don't reuse bytecode generated without one. + if (!globalObject->defaultCodeGenerationMode().isEmpty()) + return nullptr; + + // The bytes live in the compiled executable's embedded section for the whole process + // lifetime, so nothing frees them when the CachedBytecode goes away. + Ref cachedBytecode = JSC::CachedBytecode::create( + std::span { bytes, length }, [](const void*) {}, {}); + + auto key = JSC::sourceCodeKeyForSerializedFunctionExecutable(vm, source, moduleName); + auto* executable = JSC::decodeFunctionExecutable(vm, key, WTF::move(cachedBytecode)); + if (executable) + s_builtinsLoadedFromBytecode.fetch_add(1, std::memory_order_relaxed); + return executable; +} + +// Generates the cache entry for one builtin. Runs on a bytecode-cache thread with its own +// VM, never on a JS thread. `cachedBytecodePtr` owns the returned bytes; the caller must +// release it with CachedBytecode__deref. +extern "C" bool Bun__generateBuiltinModuleBytecode(unsigned moduleId, const uint8_t** outBytes, size_t* outLength, JSC::CachedBytecode** cachedBytecodePtr) +{ + WTF::String source = builtinModuleSource(moduleId); + if (source.isNull()) + return false; + const auto& module = Builtins::modules[moduleId]; + + JSC::VM& vm = Zig::vmForBytecodeCache(); + JSC::JSLockHolder locker(vm); + + WTF::String moduleName { module.name }; + JSC::SourceCode sourceCode = builtinModuleSourceCode(source, moduleName, WTF::String { module.url }); + UnlinkedFunctionExecutable* executable = createBuiltinModuleExecutable(vm, sourceCode, moduleName); + + ParserError parserError; + if (!JSC::recursivelyGenerateUnlinkedCodeBlockForFunctionExecutable(vm, executable, sourceCode, parserError)) + return false; + + auto key = JSC::sourceCodeKeyForSerializedFunctionExecutable(vm, sourceCode, moduleName); + dataLogLnIf(JSC::Options::verboseDiskCache(), "[Bytecode Build] builtin ", moduleName, " sourceSize=", source.length(), " keyHash=", key.hash()); + + JSC::BytecodeCacheError cacheError; + RefPtr cachedBytecode = JSC::encodeFunctionExecutable(vm, key, executable, cacheError); + if (!cachedBytecode || cacheError.isValid()) + return false; + + cachedBytecode->ref(); + *cachedBytecodePtr = cachedBytecode.get(); + *outBytes = cachedBytecode->span().data(); + *outLength = cachedBytecode->span().size(); + return true; +} + +BUN_DEFINE_HOST_FUNCTION(Bun__builtinModuleBytecodeDecodedCount, (JSC::JSGlobalObject * globalObject, JSC::CallFrame*)) +{ + UNUSED_PARAM(globalObject); + return JSValue::encode(jsNumber(s_builtinsLoadedFromBytecode.load(std::memory_order_relaxed))); +} + +// The builtins that `moduleId` requires directly. Empty for native modules. +extern "C" void Bun__builtinModuleDependencies(unsigned moduleId, const uint32_t** outIds, size_t* outLength) +{ + *outIds = nullptr; + *outLength = 0; + if (moduleId >= Builtins::jsModuleCount) + return; + + uint32_t begin = Builtins::dependencyOffsets[moduleId]; + uint32_t end = Builtins::dependencyOffsets[moduleId + 1]; + if (begin == end) + return; + *outIds = &Builtins::dependencyIds[begin]; + *outLength = end - begin; +} + +} // namespace Bun diff --git a/src/jsc/bindings/BuiltinModuleBytecode.h b/src/jsc/bindings/BuiltinModuleBytecode.h new file mode 100644 index 000000000000..a294c7c004d1 --- /dev/null +++ b/src/jsc/bindings/BuiltinModuleBytecode.h @@ -0,0 +1,33 @@ +#pragma once + +#include "root.h" + +#include +#include + +namespace Bun { + +// How many builtins this process loaded from an embedded bytecode cache instead of parsing. +// Exposed to `bun:internal-for-testing`. +BUN_DECLARE_HOST_FUNCTION(Bun__builtinModuleBytecodeDecodedCount); + +// The bundled source of the JS builtin with this InternalModuleRegistry id: a view into the +// linked blob in release builds, the file under BUN_DYNAMIC_JS_LOAD_PATH in debug builds. +// Null for native modules. The runtime and the bytecode cache generator both read through +// here, so a build's cache entries are always keyed on the bytes that build parses. +WTF::String builtinModuleSource(unsigned moduleId); + +// `(function (){ ... })` under the `builtin://` origin it is keyed on. The runtime and the +// generator share this so the SourceCodeKey cannot drift between them. +JSC::SourceCode builtinModuleSourceCode(const WTF::String& source, const WTF::String& moduleName, const WTF::String& urlString); + +// Compiles a builtin in builtin parse mode, which is what lets the `@`-prefixed intrinsics +// through the lexer. The runtime's fallback path and the generator both use it. +JSC::UnlinkedFunctionExecutable* createBuiltinModuleExecutable(JSC::VM&, const JSC::SourceCode&, const WTF::String& moduleName); + +// Decode the embedded bytecode cache entry for a builtin, if this executable carries one +// and it still matches the source. Null when there is nothing usable, in which case the +// caller compiles from source. +JSC::UnlinkedFunctionExecutable* decodeBuiltinModuleBytecode(JSC::JSGlobalObject*, JSC::VM&, const JSC::SourceCode&, const WTF::String& moduleName, unsigned moduleId); + +} // namespace Bun diff --git a/src/jsc/bindings/BunClientData.cpp b/src/jsc/bindings/BunClientData.cpp index c538322f8e39..94db422be92f 100644 --- a/src/jsc/bindings/BunClientData.cpp +++ b/src/jsc/bindings/BunClientData.cpp @@ -103,6 +103,16 @@ JSVMClientData::~JSVMClientData() if (vmHandle) Bun__VmHandle__release(std::exchange(vmHandle, nullptr)); } + +void JSVMClientData::registerBuiltinNames(VM& vm) +{ + // Intentionally leaked. BuiltinNames::appendExternalName() stores the private symbols + // by pointer in the VM's private-name set, and the Identifiers that own those symbols + // live in this object, so it has to stay alive for as long as the VM does; the only + // caller's VM is thread-local and never destroyed either. + new BunBuiltinNames(vm); +} + void JSVMClientData::create(VM* vm, void* bunVM, bool isWorkerVM) { auto provider = WebCore::createBuiltinsSourceProvider(); diff --git a/src/jsc/bindings/BunClientData.h b/src/jsc/bindings/BunClientData.h index 21a163421c03..d61cff6e34a2 100644 --- a/src/jsc/bindings/BunClientData.h +++ b/src/jsc/bindings/BunClientData.h @@ -136,6 +136,13 @@ class JSVMClientData : public JSC::VM::ClientData { static void create(JSC::VM*, void* bunVM, bool isWorkerVM); + // The one piece of client-data setup a VM needs in order to *compile* Bun's builtins: + // registering the `@`-private names with the VM's property table, without which the + // builtin-mode lexer rejects `@getInternalField` and friends. For VMs that only ever + // generate bytecode and never run it (Zig::vmForBytecodeCache()); such a VM has no Bun + // VirtualMachine, so the event-loop half of create() has nothing to attach to. + static void registerBuiltinNames(JSC::VM&); + JSHeapData& heapData() { return *m_heapData; } BunBuiltinNames& builtinNames() { return m_builtinNames; } JSBuiltinFunctions& builtinFunctions() { return *m_builtinFunctions; } diff --git a/src/jsc/bindings/InternalModuleRegistry.cpp b/src/jsc/bindings/InternalModuleRegistry.cpp index f62c5ba050f9..d2f31a8673f3 100644 --- a/src/jsc/bindings/InternalModuleRegistry.cpp +++ b/src/jsc/bindings/InternalModuleRegistry.cpp @@ -9,9 +9,9 @@ #include #include -#include "InternalModuleRegistryConstants.h" #include "wtf/Forward.h" +#include "BuiltinModuleBytecode.h" #include "NativeModuleImpl.h" namespace Bun { @@ -29,27 +29,28 @@ static void maybeAddCodeCoverage(JSC::VM& vm, const JSC::SourceCode& code) #endif } -// The `INTERNAL_MODULE_REGISTRY_GENERATE` macro handles inlining code to compile and run a -// JS builtin that acts as a module. In debug mode, we use a different implementation that reads -// from the developer's filesystem. This allows reloading code without recompiling bindings. - -JSC::JSValue generateModule(JSC::JSGlobalObject* globalObject, JSC::VM& vm, const String& SOURCE, const String& moduleName, const String& urlString) +// Compiles and runs a JS builtin that acts as a module. Where the source comes from (the +// linked blob, or the developer's build directory in debug builds) is BuiltinModuleBytecode's +// concern; see INTERNAL_MODULE_REGISTRY_GENERATE below. +JSC::JSValue generateModule(JSC::JSGlobalObject* globalObject, JSC::VM& vm, const String& SOURCE, const String& moduleName, const String& urlString, unsigned moduleId) { auto throwScope = DECLARE_THROW_SCOPE(vm); - auto&& origin = SourceOrigin(WTF::URL(urlString)); - SourceCode source = JSC::makeSource(SOURCE, origin, JSC::SourceTaintedOrigin::Untainted, moduleName); + // Shared with the bytecode cache generator so both sides key the cache on the same + // SourceCode; a mismatch here would silently turn every lookup into a miss. + SourceCode source = builtinModuleSourceCode(SOURCE, moduleName, urlString); maybeAddCodeCoverage(vm, source); + + // `bun build --compile --bytecode` embeds a cache entry for every builtin the app can + // reach. When it is absent or stale we fall through to parsing, which is what every + // other build does. + UnlinkedFunctionExecutable* unlinkedExecutable = decodeBuiltinModuleBytecode(globalObject, vm, source, moduleName, moduleId); + if (!unlinkedExecutable) + unlinkedExecutable = createBuiltinModuleExecutable(vm, source, moduleName); + JSFunction* func = JSFunction::create( vm, globalObject, - createBuiltinExecutable( - vm, source, - Identifier::fromString(vm, moduleName), - ImplementationVisibility::Public, - ConstructorKind::None, - ConstructAbility::CannotConstruct, - InlineAttribute::None) - ->link(vm, nullptr, source), + unlinkedExecutable->link(vm, nullptr, source), static_cast(globalObject)); RETURN_IF_EXCEPTION(throwScope, {}); @@ -100,33 +101,11 @@ ALWAYS_INLINE JSC::JSValue generateNativeModule( return defaultValue; } -#ifdef BUN_DYNAMIC_JS_LOAD_PATH -JSValue initializeInternalModuleFromDisk(JSGlobalObject* globalObject, VM& vm, const WTF::String& moduleName, WTF::String fileBase, const WTF::String& urlString) -{ - WTF::String file = makeString(ASCIILiteral::fromLiteralUnsafe(BUN_DYNAMIC_JS_LOAD_PATH), "/"_s, WTF::move(fileBase)); - if (auto contents = WTF::FileSystemImpl::readEntireFile(file)) { - auto string = WTF::String::fromUTF8(contents.value()); - return generateModule(globalObject, vm, string, moduleName, urlString); - } else { - printf("\nFATAL: bun-debug failed to load bundled version of \"%s\" at \"%s\" (was it deleted?)\n" - "Please re-compile Bun to continue.\n\n", - moduleName.utf8().span().data(), file.utf8().span().data()); - CRASH(); - } -} -#define INTERNAL_MODULE_REGISTRY_GENERATE(globalObject, vm, moduleId, filename, OFFSET, LENGTH, urlString) \ - return initializeInternalModuleFromDisk(globalObject, vm, moduleId, filename, urlString) -#else - -// The module sources are linked as one read-only blob (bun_internal_modules_data, -// see the generated InternalModuleRegistryConstants.S); each module is a span at -// a known offset/length. createWithoutCopying is the same path the old -// ASCIILiteral → String conversion took. -#define INTERNAL_MODULE_REGISTRY_GENERATE(globalObject, vm, moduleId, filename, OFFSET, LENGTH, urlString) \ - return generateModule(globalObject, vm, \ - WTF::String(WTF::StringImpl::createWithoutCopying(std::span(bun_internal_modules_data + (OFFSET), (LENGTH)))), \ - moduleId, urlString) -#endif +// builtinModuleSource() is the blob span in release and the on-disk bundle in debug +// (BUN_DYNAMIC_JS_LOAD_PATH); the bytecode cache generator reads through the same +// function, so whatever this build parses is also what its cache entries were keyed on. +#define INTERNAL_MODULE_REGISTRY_GENERATE(globalObject, vm, index, moduleName, urlString) \ + return generateModule(globalObject, vm, builtinModuleSource(index), moduleName, urlString, index) const ClassInfo InternalModuleRegistry::s_info = { "InternalModuleRegistry"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(InternalModuleRegistry) }; diff --git a/src/jsc/bindings/ZigSourceProvider.cpp b/src/jsc/bindings/ZigSourceProvider.cpp index 50074091373e..c4242106d28d 100644 --- a/src/jsc/bindings/ZigSourceProvider.cpp +++ b/src/jsc/bindings/ZigSourceProvider.cpp @@ -6,6 +6,7 @@ #include "MimallocWTFMalloc.h" #include "BunAnalyzeTranspiledModule.h" +#include "BunClientData.h" #include "ZigGlobalObject.h" #include "wtf/Assertions.h" @@ -183,17 +184,21 @@ extern "C" void CachedBytecode__deref(JSC::CachedBytecode* cachedBytecode) cachedBytecode->deref(); } -static JSC::VM& getVMForBytecodeCache() +JSC::VM& vmForBytecodeCache() { - static thread_local JSC::VM* vmForBytecodeCache = nullptr; - if (!vmForBytecodeCache) { + static thread_local JSC::VM* cachedVM = nullptr; + if (!cachedVM) { const auto heapSize = JSC::HeapType::Small; auto vmPtr = JSC::VM::tryCreate(heapSize); vmPtr->refSuppressingSaferCPPChecking(); - vmForBytecodeCache = vmPtr.get(); + cachedVM = vmPtr.get(); vmPtr->heap.acquireAccess(); + // Chunk bytecode parses as ordinary JS, but the builtins parse in builtin mode and + // need Bun's `@`-private names registered or the lexer rejects them. Only the names: + // this VM has no Bun VirtualMachine to hang the rest of the client data off. + WebCore::JSVMClientData::registerBuiltinNames(*cachedVM); } - return *vmForBytecodeCache; + return *cachedVM; } extern "C" bool generateCachedModuleByteCodeFromSourceCode(BunString* sourceProviderURL, const Latin1Character* inputSourceCode, size_t inputSourceCodeSize, const uint8_t** outputByteCode, size_t* outputByteCodeSize, JSC::CachedBytecode** cachedBytecodePtr) @@ -201,7 +206,7 @@ extern "C" bool generateCachedModuleByteCodeFromSourceCode(BunString* sourceProv std::span sourceCodeSpan(inputSourceCode, inputSourceCodeSize); JSC::SourceCode sourceCode = JSC::makeSource(WTF::String(sourceCodeSpan), toSourceOrigin(sourceProviderURL->toWTFString(), false), JSC::SourceTaintedOrigin::Untainted); - JSC::VM& vm = getVMForBytecodeCache(); + JSC::VM& vm = vmForBytecodeCache(); JSC::JSLockHolder locker(vm); LexicallyScopedFeatures lexicallyScopedFeatures = StrictModeLexicallyScopedFeature; @@ -236,7 +241,7 @@ extern "C" bool generateCachedCommonJSProgramByteCodeFromSourceCode(BunString* s std::span sourceCodeSpan(inputSourceCode, inputSourceCodeSize); JSC::SourceCode sourceCode = JSC::makeSource(WTF::String(sourceCodeSpan), toSourceOrigin(sourceProviderURL->toWTFString(), false), JSC::SourceTaintedOrigin::Untainted); - JSC::VM& vm = getVMForBytecodeCache(); + JSC::VM& vm = vmForBytecodeCache(); JSC::JSLockHolder locker(vm); LexicallyScopedFeatures lexicallyScopedFeatures = NoLexicallyScopedFeatures; diff --git a/src/jsc/bindings/ZigSourceProvider.h b/src/jsc/bindings/ZigSourceProvider.h index 3513fbbe6140..3a9725b30260 100644 --- a/src/jsc/bindings/ZigSourceProvider.h +++ b/src/jsc/bindings/ZigSourceProvider.h @@ -21,6 +21,11 @@ namespace Zig { class GlobalObject; JSC::SourceID sourceIDForSourceURL(const WTF::String& sourceURL); + +// The thread-local VM used to compile bytecode off the JS thread. It never gets a global +// object; it only ever parses and runs the bytecode generator. +JSC::VM& vmForBytecodeCache(); + JSC::SourceOrigin toSourceOrigin(const String& sourceURL, bool isBuiltin); class SourceProvider final : public JSC::SourceProvider { WTF_DEPRECATED_MAKE_FAST_ALLOCATED(SourceProvider); diff --git a/src/jsc/lib.rs b/src/jsc/lib.rs index 752d17f49301..fc6378abae35 100644 --- a/src/jsc/lib.rs +++ b/src/jsc/lib.rs @@ -846,7 +846,7 @@ pub mod resolved_source_tag { /// `HardcodedModule` variant has no matching entry in the generated /// module table (`INTERNAL_MODULE_TAG`). pub fn from_name(name: &[u8]) -> Self { - if let Some(&tag) = INTERNAL_MODULE_TAG.get(name) { + if let Some(tag) = Self::try_from_name(name) { return tag; } debug_assert!( @@ -856,6 +856,13 @@ pub mod resolved_source_tag { ); Self::Javascript } + + /// The `InternalModuleRegistry` id behind a builtin-module tag, if this is one. + /// Mirrors `SyntheticModuleType::InternalModuleRegistryFlag` on the C++ side. + pub(crate) fn internal_module_id(self) -> Option { + const FLAG: u32 = 1 << 9; + (self.0 & FLAG != 0).then_some(self.0 & (FLAG - 1)) + } } impl Default for ResolvedSourceTag { diff --git a/src/runtime/api/js_bundle_completion_task.rs b/src/runtime/api/js_bundle_completion_task.rs index b5099cad4fff..21f5bd4cd83e 100644 --- a/src/runtime/api/js_bundle_completion_task.rs +++ b/src/runtime/api/js_bundle_completion_task.rs @@ -288,7 +288,11 @@ impl JSBundleCompletionTask { } /// Port of `JSBundleCompletionTask.doCompilation`. - fn do_compilation(&mut self, output_files: &mut Vec) -> CompileResult { + fn do_compilation( + &mut self, + output_files: &mut Vec, + builtin_bytecode: &[(u32, Box<[u8]>)], + ) -> CompileResult { let compile_options = self .config .compile @@ -447,6 +451,7 @@ impl JSBundleCompletionTask { Some(&compile_options.executable_path.list) }, flags, + builtin_bytecode, ) { Ok(r) => r, Err(err) => { @@ -657,12 +662,15 @@ impl JSBundleCompletionTask { // `&mut output_files` from inside `self.result`. Temporarily move the // Vec out via `take` so the method gets a disjoint `&mut self`. if matches!(this.result, BundleV2Result::Value(_)) && this.config.compile.is_some() { - let mut output_files = match &mut this.result { - BundleV2Result::Value(build) => core::mem::take(&mut build.output_files), + let (mut output_files, builtin_bytecode) = match &mut this.result { + BundleV2Result::Value(build) => ( + core::mem::take(&mut build.output_files), + core::mem::take(&mut build.builtin_bytecode), + ), // SAFETY: arm checked above. _ => unsafe { core::hint::unreachable_unchecked() }, }; - let compile_result = this.do_compilation(&mut output_files); + let compile_result = this.do_compilation(&mut output_files, &builtin_bytecode); // `defer compile_result.deinit()` — `CompileResult` is a Rust enum // with owned `Vec` payloads; drops at end of scope. @@ -1009,6 +1017,10 @@ impl CompletionStruct for JSBundleCompletionTask { } else { options::CompileMode::None }; + transpiler.options.compile_target_is_host = config + .compile + .as_ref() + .is_some_and(|compile| compile.compile_target.is_default()); // For compile mode, set the public_path to the target-specific base path // This ensures embedded resources like yoga.wasm are correctly found diff --git a/src/runtime/cli/build_command.rs b/src/runtime/cli/build_command.rs index 92003b6f7c64..632edd07d8e9 100644 --- a/src/runtime/cli/build_command.rs +++ b/src/runtime/cli/build_command.rs @@ -159,6 +159,8 @@ impl BuildCommand { } else { options::CompileMode::None }; + this_transpiler.options.compile_target_is_host = + ctx.bundler_options.compile_target.is_default(); if this_transpiler.options.source_map == options::SourceMapOption::External && ctx.bundler_options.outdir.is_empty() @@ -587,7 +589,10 @@ impl BuildCommand { let opt_transform_only = this_transpiler.options.transform_only; let env_ptr = this_transpiler.env; - let mut output_files: Vec = 'brk: { + let (mut output_files, builtin_bytecode): ( + Vec, + Vec<(u32, Box<[u8]>)>, + ) = 'brk: { if ctx.bundler_options.transform_only { this_transpiler.options.import_path_format = options::ImportPathFormat::Relative; this_transpiler.options.allow_runtime = false; @@ -607,7 +612,7 @@ impl BuildCommand { } } - break 'brk result.output_files.into_vec(); + break 'brk (result.output_files.into_vec(), Vec::new()); } if ctx.bundler_options.outdir.is_empty() @@ -736,7 +741,7 @@ impl BuildCommand { } } - break 'brk build_result.output_files; + break 'brk (build_result.output_files, build_result.builtin_bytecode); }; if ctx.bundler_options.compile && !ctx.bundler_options.compile_assets.is_empty() { @@ -914,6 +919,7 @@ impl BuildCommand { } flags }, + &builtin_bytecode, ) { Ok(r) => r, Err(err) => { diff --git a/src/standalone_graph/StandaloneModuleGraph.rs b/src/standalone_graph/StandaloneModuleGraph.rs index 6a1a7b82c97b..f695310ab6a3 100644 --- a/src/standalone_graph/StandaloneModuleGraph.rs +++ b/src/standalone_graph/StandaloneModuleGraph.rs @@ -45,6 +45,10 @@ pub struct StandaloneModuleGraph { pub entry_point_id: u32, pub compile_exec_argv: &'static [u8], pub flags: Flags, + /// `(InternalModuleRegistry id, JSC cache entry)` for the JS builtins this binary + /// embedded bytecode for, sorted by id. BACKREF into the embedded section, raw `*mut` + /// for the same provenance reason as `File::bytecode`. + pub builtin_bytecode: Box<[(u32, *mut [u8])]>, } // We never want to hit the filesystem for these files @@ -613,6 +617,18 @@ pub(crate) struct Offsets { pub entry_point_id: u32, pub compile_exec_argv_ptr: StringPointer, pub flags: Flags, + /// The `BuiltinBytecodeEntry` array, or a zero-length pointer when the binary was + /// built without `--bytecode`. + pub builtin_bytecode_ptr: StringPointer, +} + +/// Where the JSC bytecode cache entry for one JS builtin lives inside the embedded +/// section. `module_id` indexes `InternalModuleRegistry`; the array is sorted by it. +#[repr(C)] +#[derive(Clone, Copy, Default)] +pub(crate) struct BuiltinBytecodeEntry { + pub module_id: u32, + pub bytecode: StringPointer, } bitflags::bitflags! { @@ -643,6 +659,7 @@ impl StandaloneModuleGraph { entry_point_id: 0, compile_exec_argv: b"", flags: Flags::default(), + builtin_bytecode: Box::default(), }); } @@ -754,6 +771,29 @@ impl StandaloneModuleGraph { } dirs.lock_pointers(); + // SAFETY: the entry array is a read-only subrange serialized by `to_bytes`, disjoint + // from every payload it points at. Like the modules blob it sits at an arbitrary byte + // offset, so each fixed-size record is `read_unaligned` into a local. + let builtin_entry_bytes = + unsafe { slice_to(raw_const, raw_len, offsets.builtin_bytecode_ptr) }; + let builtin_entry_count = builtin_entry_bytes.len() / size_of::(); + let builtin_entry_base = builtin_entry_bytes.as_ptr(); + let mut builtin_bytecode: Vec<(u32, *mut [u8])> = Vec::with_capacity(builtin_entry_count); + for i in 0..builtin_entry_count { + // SAFETY: index < count derived from byte length above. + let entry: BuiltinBytecodeEntry = unsafe { + core::ptr::read_unaligned( + builtin_entry_base + .add(i * size_of::()) + .cast::(), + ) + }; + // SAFETY: subrange in-bounds (serialized by `to_bytes`) and disjoint from every + // other payload. Kept as `*mut` so no shared reference ever spans JSC's buffer. + let bytes = unsafe { slice_to_mut(raw_ptr, raw_len, entry.bytecode) }; + builtin_bytecode.push((entry.module_id, bytes)); + } + Ok(StandaloneModuleGraph { // Stored as a raw fat pointer — `byte_count` covers the writable // bytecode/module_info regions, so a `&'static [u8]` here would alias them. @@ -767,10 +807,46 @@ impl StandaloneModuleGraph { } .as_bytes(), flags: offsets.flags, + builtin_bytecode: builtin_bytecode.into_boxed_slice(), }) } } +unsafe extern "C" { + /// Defined in `BuiltinModuleBytecode.cpp`. Flips the flag that gates the lookup below, + /// so that the builtin compile path in a Bun that embeds no builtin bytecode (every Bun + /// that is not a `--compile --bytecode` executable) never calls into here at all. + safe fn Bun__setHasBuiltinModuleBytecode(); +} + +/// The embedded JSC bytecode cache entry for JS builtin `module_id`. Only reached once +/// `Bun__setHasBuiltinModuleBytecode()` has run, i.e. in an executable that actually carries +/// entries. +/// +/// The bytes are validated by JSC: the entry carries a cache version and a `SourceCodeKey` +/// over the builtin's source, so a mismatch (different Bun, different platform's `src/js` +/// bundle) is rejected and the builtin is parsed instead. +#[unsafe(no_mangle)] +pub extern "C" fn Bun__getBuiltinModuleBytecode( + module_id: u32, + out_bytes: &mut *mut u8, + out_length: &mut usize, +) -> bool { + let Some(graph) = StandaloneModuleGraph::get() else { + return false; + }; + // SAFETY: `INSTANCE` is written once before any worker thread exists; this reads the + // immutable `builtin_bytecode` slot only. + let entries = unsafe { &(*graph).builtin_bytecode }; + let Ok(index) = entries.binary_search_by_key(&module_id, |(id, _)| *id) else { + return false; + }; + let bytes = entries[index].1; + *out_bytes = bytes.cast::(); + *out_length = bytes.len(); + true +} + /// Read-only subslice helper. Builds a `&'static [u8]` over the *subrange only* so no /// shared reference ever spans the writable bytecode/module_info regions of the same /// allocation (which would be invalidated by JSC's in-place writes). @@ -819,12 +895,29 @@ unsafe fn slice_to_z(base: *const u8, len: usize, ptr: StringPointer) -> &'stati unsafe { ZStr::from_raw(base.add(off), n) } } +/// Pads `string_builder` so that the next byte written lands on a 128-byte boundary once +/// the section is mapped. See the long comment at the `bytecode` field in `to_bytes`: +/// PE/Mach-O put the data 8 bytes after a page-aligned section start, so the worst-case +/// target is `offset % 128 == 120`. +fn align_for_bytecode(string_builder: &mut bun_core::StringBuilder) { + let target_mod: usize = 128 - size_of::(); + let current_mod = string_builder.len % 128; + let padding = if current_mod <= target_mod { + target_mod - current_mod + } else { + 128 - current_mod + target_mod + }; + string_builder.writable()[0..padding].fill(0); + string_builder.len += padding; +} + pub(crate) fn to_bytes( prefix: &[u8], output_files: &[OutputFile], output_format: Format, compile_exec_argv: &[u8], flags: Flags, + builtin_bytecode: &[(u32, Box<[u8]>)], ) -> crate::Result> { // RAII trace handle ends on drop. let _serialize_trace = bun_perf::trace(bun_perf::PerfEvent::StandaloneModuleGraphSerialize); @@ -873,6 +966,12 @@ pub(crate) fn to_bytes( string_builder.cap += size_of::(); string_builder.count_z(compile_exec_argv); + // Each builtin cache entry gets its own 128-byte alignment slot, plus the index record. + for (_, bytes) in builtin_bytecode { + string_builder.cap += bytes.len() + 128; + } + string_builder.cap += size_of::() * builtin_bytecode.len(); + string_builder.allocate()?; let mut modules: Vec = Vec::with_capacity(module_count); @@ -928,20 +1027,7 @@ pub(crate) fn to_bytes( let bytecode = output_files[output_file.bytecode_index as usize] .value .as_slice(); - let current_offset = string_builder.len; - // Calculate padding so that (current_offset + padding) % 128 == 120 - // This accounts for the 8-byte section header on PE/Mach-O platforms. - let target_mod: usize = 128 - size_of::(); // 120 = accounts for 8-byte header - let current_mod = current_offset % 128; - let padding = if current_mod <= target_mod { - target_mod - current_mod - } else { - 128 - current_mod + target_mod - }; - // Zero the padding bytes to ensure deterministic output - let writable = string_builder.writable(); - writable[0..padding].fill(0); - string_builder.len += padding; + align_for_bytecode(&mut string_builder); let aligned_offset = string_builder.len; let writable_after_padding = string_builder.writable(); writable_after_padding[0..bytecode.len()] @@ -1082,6 +1168,31 @@ pub(crate) fn to_bytes( modules.push(module); } + // JSC deserializes these in place, so each one needs the same alignment as a chunk's + // bytecode. The index is written afterwards, once the payload offsets are known. + let mut builtin_entries: Vec = Vec::with_capacity(builtin_bytecode.len()); + for (module_id, bytes) in builtin_bytecode { + align_for_bytecode(&mut string_builder); + let offset = string_builder.len; + string_builder.writable()[0..bytes.len()].copy_from_slice(bytes); + string_builder.len += bytes.len(); + builtin_entries.push(BuiltinBytecodeEntry { + module_id: *module_id, + bytecode: StringPointer { + offset: offset as u32, + length: bytes.len() as u32, + }, + }); + } + + // SAFETY: `BuiltinBytecodeEntry` is `#[repr(C)]` POD; same rationale as the modules blob. + let builtin_entries_as_bytes: &[u8] = unsafe { + core::slice::from_raw_parts( + builtin_entries.as_ptr().cast::(), + builtin_entries.len() * size_of::(), + ) + }; + // SAFETY: `CompiledModuleGraphFile` is `#[repr(C)]` POD with no padding-dependent // invariants; reinterpreting its backing storage as bytes is sound. let modules_as_bytes: &[u8] = unsafe { @@ -1094,6 +1205,7 @@ pub(crate) fn to_bytes( entry_point_id: entry_point_id.unwrap() as u32, modules_ptr: string_builder.append_count(modules_as_bytes), compile_exec_argv_ptr: string_builder.append_count_z(compile_exec_argv), + builtin_bytecode_ptr: string_builder.append_count(builtin_entries_as_bytes), byte_count: string_builder.len, flags, }; @@ -1892,6 +2004,7 @@ pub fn to_executable( compile_exec_argv: &[u8], self_exe_path: Option<&[u8]>, flags: Flags, + builtin_bytecode: &[(u32, Box<[u8]>)], ) -> crate::Result { #[cfg(windows)] let _ = root_dir; @@ -1901,6 +2014,7 @@ pub fn to_executable( output_format, compile_exec_argv, flags, + builtin_bytecode, ) { Ok(b) => b, Err(e) => { @@ -2322,6 +2436,9 @@ fn from_bytes_alloc( offsets: Offsets, ) -> crate::Result<*mut StandaloneModuleGraph> { let graph = StandaloneModuleGraph::from_bytes(raw_ptr, raw_len, offsets)?; + if !graph.builtin_bytecode.is_empty() { + Bun__setHasBuiltinModuleBytecode(); + } Ok(StandaloneModuleGraph::set(graph)) } diff --git a/test/bundler/bundler_compile.test.ts b/test/bundler/bundler_compile.test.ts index 01b514dbbe54..93c2b669fda7 100644 --- a/test/bundler/bundler_compile.test.ts +++ b/test/bundler/bundler_compile.test.ts @@ -1391,3 +1391,52 @@ test("compile --compile-executable-path rejects a template shorter than the exec expect(exitCode).toBe(1); } }, 60_000); + +// `--bytecode` embeds a JSC bytecode cache for every JS builtin the app can reach. The +// entry point only names `node:net`; everything else comes from walking the internal +// require() graph that InternalModuleRegistry resolves at runtime, which the bundler +// never sees. Without `--bytecode` nothing is embedded and every builtin is parsed. +test("compile/BytecodeCachesBuiltinModules", async () => { + using dir = tempDir("compile-builtin-bytecode", { + "entry.ts": ` + import net from "node:net"; + const { builtinModuleBytecodeDecodedCount } = require("bun:internal-for-testing"); + if (typeof net.createServer !== "function") throw new Error("node:net did not load"); + console.log("decoded:" + builtinModuleBytecodeDecodedCount()); + `, + }); + const cwd = String(dir); + // `bun:internal-for-testing` is gated in release builds; this is the same switch + // `--expose-internals` flips, and a compiled executable never parses that flag. + const env = { ...bunEnv, BUN_FEATURE_FLAG_INTERNAL_FOR_TESTING: "1" }; + // One outfile, overwritten between the two runs: each of these is a full copy of the + // Bun binary, so two at once is hundreds of megabytes for no reason. + const outfile = join(cwd, isWindows ? "out.exe" : "out"); + + async function compileAndRun(extraArgs: string[]): Promise { + await using build = Bun.spawn({ + cmd: [bunExe(), "build", "--compile", ...extraArgs, "entry.ts", "--outfile", outfile], + env, + cwd, + stdout: "pipe", + stderr: "pipe", + }); + const [buildErr, buildCode] = await Promise.all([build.stderr.text(), build.exited]); + expect({ buildErr, buildCode }).toMatchObject({ buildCode: 0 }); + + await using proc = Bun.spawn({ cmd: [outfile], env, cwd, stdout: "pipe", stderr: "pipe" }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect({ stdout, stderr, exitCode }).toMatchObject({ exitCode: 0 }); + + const match = stdout.match(/^decoded:(\d+)$/m); + expect({ stdout, match: match !== null }).toMatchObject({ match: true }); + return Number(match![1]); + } + + expect(await compileAndRun([])).toBe(0); + + // node:net reaches a couple dozen internal/* modules. Assert a floor rather than an + // exact count so refactoring src/js doesn't break this, but a floor high enough that + // only the transitive walk can reach it. + expect(await compileAndRun(["--bytecode"])).toBeGreaterThan(5); +}, 120_000); From 247677ba8805151eefc85b4b73544e6f2932ce06 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Fri, 14 Aug 2026 23:12:44 +0000 Subject: [PATCH 2/2] Don't leak the BunBuiltinNames used to register names on the bytecode VM BuiltinNames::appendExternalName() copies each name into the VM's own private-name set (a set of Strings), so the BunBuiltinNames object is only needed for the duration of its constructor. Leaking it showed up under LSan as a 2992-byte direct leak from every thread that creates the bytecode VM, which since the node:module compile cache landed is most --bytecode users, not just this feature. Also trims comments that restated what the code or a neighbouring definition already says, and removes two that gave a wrong reason for an ordering that does not matter (the bytecode VM is thread-local). --- src/bundler/bundle_v2.rs | 8 ++----- src/codegen/bundle-modules.ts | 13 ++++------- src/jsc/CachedBytecode.rs | 17 +++++--------- src/jsc/bindings/BuiltinModuleBytecode.cpp | 25 ++++++--------------- src/jsc/bindings/BuiltinModuleBytecode.h | 22 ++++++++---------- src/jsc/bindings/BunClientData.cpp | 8 +++---- src/jsc/bindings/BunClientData.h | 7 ++---- src/jsc/bindings/InternalModuleRegistry.cpp | 13 ++--------- 8 files changed, 34 insertions(+), 79 deletions(-) diff --git a/src/bundler/bundle_v2.rs b/src/bundler/bundle_v2.rs index 3076a60721b4..9b51b5797b13 100644 --- a/src/bundler/bundle_v2.rs +++ b/src/bundler/bundle_v2.rs @@ -3964,8 +3964,8 @@ pub mod bv2_impl { pending.extend_from_slice(jsc::builtin_module_dependencies(id)); } - // Native modules have no JS source, so `generate_builtin_module_bytecode` - // returns `None` for them — as it does for anything that fails to compile. + // `None` is a native module (no JS source) or a compile failure; either way it is + // simply parsed at runtime, so there is nothing to report here. reached .into_iter() .filter_map(|id| Some((id, jsc::generate_builtin_module_bytecode(id)?))) @@ -4070,8 +4070,6 @@ pub mod bv2_impl { }); } - // Before the chunk bytecode runs, so the JS builtins are compiled on this - // thread's bytecode VM rather than racing the linker's pool for one. let builtin_bytecode = if this.transpiler.options.compile_mode.is_executable() && this.transpiler.options.bytecode && this.transpiler.options.compile_target_is_host @@ -5198,8 +5196,6 @@ pub mod bv2_impl { return Err(crate::Error::BuildFailed); } - // Before the chunk bytecode runs, so the JS builtins are compiled on this - // thread's bytecode VM rather than racing the linker's pool for one. let builtin_bytecode = if self.transpiler.options.compile_mode.is_executable() && self.transpiler.options.bytecode && self.transpiler.options.compile_target_is_host diff --git a/src/codegen/bundle-modules.ts b/src/codegen/bundle-modules.ts index 852c357b50bb..a37ed19c43b6 100644 --- a/src/codegen/bundle-modules.ts +++ b/src/codegen/bundle-modules.ts @@ -302,10 +302,8 @@ mark("Postprocesss modules"); * every require() rewritten to a registry lookup, `internalModuleRegistry, `, * where the index is the position in `modules`, so the edge list is unambiguous. * - * These edges are resolved at runtime through InternalModuleRegistry and are invisible - * to the JS bundler, which is why both consumers below need them precomputed: the blob - * layout wants dependencies adjacent to their dependents, and `--compile --bytecode` - * has to embed bytecode for everything a requested builtin can transitively reach. + * Consumed by the blob layout below and, via the generated table, by the + * `--compile --bytecode` builtin bytecode cache. */ function requireGraph(modules: string[], outputs: Map): number[][] { const requireRe = /internalModuleRegistry, ?(\d+)/g; @@ -514,11 +512,8 @@ ${moduleSpans `, ); -// Per-index view of the same data for BuiltinModuleBytecode.cpp, which has to get at a -// builtin by the numeric id InternalModuleRegistry hands it: where its source is, the -// name/url it is parsed under (these must match createInternalModuleById exactly, since -// they are part of the bytecode cache key), where to find it on disk in debug builds, and -// which builtins it requires. The dependency edges are the require graph computed above. +// The same data indexed by module id, for BuiltinModuleBytecode.cpp. The name and url are +// part of each module's bytecode cache key, hence the shared helpers with the switch above. { const dependencyOffsets: number[] = [0]; const dependencyIds: number[] = []; diff --git a/src/jsc/CachedBytecode.rs b/src/jsc/CachedBytecode.rs index 4b80a85bfc3b..e77455446114 100644 --- a/src/jsc/CachedBytecode.rs +++ b/src/jsc/CachedBytecode.rs @@ -161,13 +161,9 @@ pub(crate) fn __bun_jsc_generate_cached_bytecode( Some(owned) } -/// Link-time entry point for `bun_bundler`. Same thread setup as -/// [`__bun_jsc_generate_cached_bytecode`], but for one JS builtin: `module_id` indexes -/// `InternalModuleRegistry`, and the returned blob covers that builtin's top-level -/// function plus every function nested inside it. -/// -/// Returns `None` for native modules, out-of-range ids, and any module whose source -/// fails to compile, which leaves the builtin to be parsed at runtime as usual. +/// Link-time entry point for `bun_bundler`: [`__bun_jsc_generate_cached_bytecode`] for the +/// JS builtin with this `InternalModuleRegistry` id. `None` for native modules and for +/// anything that fails to compile, which leaves that builtin to be parsed at runtime. #[unsafe(no_mangle)] pub(crate) fn __bun_jsc_generate_builtin_module_bytecode(module_id: u32) -> Option> { crate::virtual_machine::IS_BUNDLER_THREAD_FOR_BYTECODE_CACHE.set(true); @@ -200,11 +196,8 @@ pub(crate) fn __bun_jsc_builtin_module_id_for_specifier(specifier: &[u8]) -> Opt .and_then(crate::ResolvedSourceTag::internal_module_id) } -/// Link-time entry point for `bun_bundler`. The builtins `module_id` requires directly. -/// -/// These edges are `@createInternalModuleById(N)` calls the builtin bundler emits for each -/// `require()` between builtins. They resolve at runtime through `InternalModuleRegistry`, -/// so they are invisible to the JS bundler — the bytecode cache has to walk them itself. +/// Link-time entry point for `bun_bundler`. The builtins `module_id` requires directly, from +/// the require graph `bundle-modules.ts` emits. #[unsafe(no_mangle)] pub(crate) fn __bun_jsc_builtin_module_dependencies(module_id: u32) -> &'static [u32] { let mut ids: *const u32 = core::ptr::null(); diff --git a/src/jsc/bindings/BuiltinModuleBytecode.cpp b/src/jsc/bindings/BuiltinModuleBytecode.cpp index 436f8ae89d3b..635d31f6fd3f 100644 --- a/src/jsc/bindings/BuiltinModuleBytecode.cpp +++ b/src/jsc/bindings/BuiltinModuleBytecode.cpp @@ -25,8 +25,6 @@ WTF::String builtinModuleSource(unsigned moduleId) const auto& module = Builtins::modules[moduleId]; #ifdef BUN_DYNAMIC_JS_LOAD_PATH - // Debug builds read the bundled module off disk so that editing src/js does not - // require relinking. WTF::String file = makeString(ASCIILiteral::fromLiteralUnsafe(BUN_DYNAMIC_JS_LOAD_PATH), "/"_s, module.file); auto contents = WTF::FileSystemImpl::readEntireFile(file); if (!contents) { @@ -37,8 +35,7 @@ WTF::String builtinModuleSource(unsigned moduleId) } return WTF::String::fromUTF8(contents.value()); #else - // The sources are linked into the executable as one read-only blob - // (bun_internal_modules_data, see the generated InternalModuleRegistryConstants.S). + // bun_internal_modules_data is the blob InternalModuleRegistryConstants.S links in. return WTF::String(WTF::StringImpl::createWithoutCopying(std::span(bun_internal_modules_data + module.sourceOffset, module.sourceLength))); #endif } @@ -59,12 +56,11 @@ JSC::UnlinkedFunctionExecutable* createBuiltinModuleExecutable(JSC::VM& vm, cons InlineAttribute::None); } -// Looks up one entry in the embedded section. Defined in `StandaloneModuleGraph.rs`. +// Defined in StandaloneModuleGraph.rs. extern "C" bool Bun__getBuiltinModuleBytecode(unsigned moduleId, uint8_t** outBytes, size_t* outLength); -// Only a `--compile --bytecode` executable carries builtin bytecode. Every other Bun process -// compiles its builtins from source, and this is on the path of every one of them, so gate on -// a plain flag the standalone graph sets at startup rather than calling across into Rust. +// Set by the standalone graph at startup. Every builtin load in every Bun process passes +// through decodeBuiltinModuleBytecode(), and almost none of them embed anything. static std::atomic s_hasBuiltinModuleBytecode { false }; extern "C" void Bun__setHasBuiltinModuleBytecode() @@ -72,8 +68,6 @@ extern "C" void Bun__setHasBuiltinModuleBytecode() s_hasBuiltinModuleBytecode.store(true, std::memory_order_relaxed); } -// How many builtins have been loaded from the embedded cache rather than parsed. Read by -// `bun:internal-for-testing` so the `--compile --bytecode` tests can tell the two apart. static std::atomic s_builtinsLoadedFromBytecode { 0 }; JSC::UnlinkedFunctionExecutable* decodeBuiltinModuleBytecode(JSC::JSGlobalObject* globalObject, JSC::VM& vm, const JSC::SourceCode& source, const WTF::String& moduleName, unsigned moduleId) @@ -86,13 +80,11 @@ JSC::UnlinkedFunctionExecutable* decodeBuiltinModuleBytecode(JSC::JSGlobalObject if (!Bun__getBuiltinModuleBytecode(moduleId, &bytes, &length) || !length) return nullptr; - // The entry was keyed on an empty code generation mode. A debugger or a profiler changes - // what the runtime would generate, so don't reuse bytecode generated without one. + // Entries are generated with an empty CodeGenerationMode; a debugger or profiler changes it. if (!globalObject->defaultCodeGenerationMode().isEmpty()) return nullptr; - // The bytes live in the compiled executable's embedded section for the whole process - // lifetime, so nothing frees them when the CachedBytecode goes away. + // The bytes are part of the executable image; nothing owns or frees them. Ref cachedBytecode = JSC::CachedBytecode::create( std::span { bytes, length }, [](const void*) {}, {}); @@ -103,9 +95,7 @@ JSC::UnlinkedFunctionExecutable* decodeBuiltinModuleBytecode(JSC::JSGlobalObject return executable; } -// Generates the cache entry for one builtin. Runs on a bytecode-cache thread with its own -// VM, never on a JS thread. `cachedBytecodePtr` owns the returned bytes; the caller must -// release it with CachedBytecode__deref. +// `*cachedBytecodePtr` owns the returned bytes; the caller releases it with CachedBytecode__deref. extern "C" bool Bun__generateBuiltinModuleBytecode(unsigned moduleId, const uint8_t** outBytes, size_t* outLength, JSC::CachedBytecode** cachedBytecodePtr) { WTF::String source = builtinModuleSource(moduleId); @@ -145,7 +135,6 @@ BUN_DEFINE_HOST_FUNCTION(Bun__builtinModuleBytecodeDecodedCount, (JSC::JSGlobalO return JSValue::encode(jsNumber(s_builtinsLoadedFromBytecode.load(std::memory_order_relaxed))); } -// The builtins that `moduleId` requires directly. Empty for native modules. extern "C" void Bun__builtinModuleDependencies(unsigned moduleId, const uint32_t** outIds, size_t* outLength) { *outIds = nullptr; diff --git a/src/jsc/bindings/BuiltinModuleBytecode.h b/src/jsc/bindings/BuiltinModuleBytecode.h index a294c7c004d1..e36838ef9ef1 100644 --- a/src/jsc/bindings/BuiltinModuleBytecode.h +++ b/src/jsc/bindings/BuiltinModuleBytecode.h @@ -7,27 +7,23 @@ namespace Bun { -// How many builtins this process loaded from an embedded bytecode cache instead of parsing. -// Exposed to `bun:internal-for-testing`. +// Number of builtins this process loaded from an embedded bytecode cache instead of parsing. BUN_DECLARE_HOST_FUNCTION(Bun__builtinModuleBytecodeDecodedCount); -// The bundled source of the JS builtin with this InternalModuleRegistry id: a view into the -// linked blob in release builds, the file under BUN_DYNAMIC_JS_LOAD_PATH in debug builds. -// Null for native modules. The runtime and the bytecode cache generator both read through -// here, so a build's cache entries are always keyed on the bytes that build parses. +// InternalModuleRegistry (the runtime) and Bun__generateBuiltinModuleBytecode (the +// `--compile --bytecode` generator) both compile builtins through the three functions +// below. That is what keeps the SourceCodeKey an entry was written under identical to the +// one it is looked up with. + +// Null for native modules. WTF::String builtinModuleSource(unsigned moduleId); -// `(function (){ ... })` under the `builtin://` origin it is keyed on. The runtime and the -// generator share this so the SourceCodeKey cannot drift between them. JSC::SourceCode builtinModuleSourceCode(const WTF::String& source, const WTF::String& moduleName, const WTF::String& urlString); -// Compiles a builtin in builtin parse mode, which is what lets the `@`-prefixed intrinsics -// through the lexer. The runtime's fallback path and the generator both use it. +// Builtin parse mode: the only mode whose lexer accepts the `@`-prefixed intrinsics. JSC::UnlinkedFunctionExecutable* createBuiltinModuleExecutable(JSC::VM&, const JSC::SourceCode&, const WTF::String& moduleName); -// Decode the embedded bytecode cache entry for a builtin, if this executable carries one -// and it still matches the source. Null when there is nothing usable, in which case the -// caller compiles from source. +// Null unless this executable embeds an entry for the builtin and it still matches `source`. JSC::UnlinkedFunctionExecutable* decodeBuiltinModuleBytecode(JSC::JSGlobalObject*, JSC::VM&, const JSC::SourceCode&, const WTF::String& moduleName, unsigned moduleId); } // namespace Bun diff --git a/src/jsc/bindings/BunClientData.cpp b/src/jsc/bindings/BunClientData.cpp index 94db422be92f..e5ad33c0ba4f 100644 --- a/src/jsc/bindings/BunClientData.cpp +++ b/src/jsc/bindings/BunClientData.cpp @@ -106,11 +106,9 @@ JSVMClientData::~JSVMClientData() void JSVMClientData::registerBuiltinNames(VM& vm) { - // Intentionally leaked. BuiltinNames::appendExternalName() stores the private symbols - // by pointer in the VM's private-name set, and the Identifiers that own those symbols - // live in this object, so it has to stay alive for as long as the VM does; the only - // caller's VM is thread-local and never destroyed either. - new BunBuiltinNames(vm); + // The constructor's appendExternalName() calls copy every name into the VM's own + // private-name set; the object itself is not needed afterwards. + BunBuiltinNames names(vm); } void JSVMClientData::create(VM* vm, void* bunVM, bool isWorkerVM) diff --git a/src/jsc/bindings/BunClientData.h b/src/jsc/bindings/BunClientData.h index d61cff6e34a2..cd36a52899d8 100644 --- a/src/jsc/bindings/BunClientData.h +++ b/src/jsc/bindings/BunClientData.h @@ -136,11 +136,8 @@ class JSVMClientData : public JSC::VM::ClientData { static void create(JSC::VM*, void* bunVM, bool isWorkerVM); - // The one piece of client-data setup a VM needs in order to *compile* Bun's builtins: - // registering the `@`-private names with the VM's property table, without which the - // builtin-mode lexer rejects `@getInternalField` and friends. For VMs that only ever - // generate bytecode and never run it (Zig::vmForBytecodeCache()); such a VM has no Bun - // VirtualMachine, so the event-loop half of create() has nothing to attach to. + // Registers Bun's `@`-private names with a VM that compiles builtins but never runs + // them and so has no Bun VirtualMachine for create() to attach to. static void registerBuiltinNames(JSC::VM&); JSHeapData& heapData() { return *m_heapData; } diff --git a/src/jsc/bindings/InternalModuleRegistry.cpp b/src/jsc/bindings/InternalModuleRegistry.cpp index d2f31a8673f3..e3d953e37192 100644 --- a/src/jsc/bindings/InternalModuleRegistry.cpp +++ b/src/jsc/bindings/InternalModuleRegistry.cpp @@ -29,20 +29,14 @@ static void maybeAddCodeCoverage(JSC::VM& vm, const JSC::SourceCode& code) #endif } -// Compiles and runs a JS builtin that acts as a module. Where the source comes from (the -// linked blob, or the developer's build directory in debug builds) is BuiltinModuleBytecode's -// concern; see INTERNAL_MODULE_REGISTRY_GENERATE below. +// Compiles and runs a JS builtin that acts as a module. JSC::JSValue generateModule(JSC::JSGlobalObject* globalObject, JSC::VM& vm, const String& SOURCE, const String& moduleName, const String& urlString, unsigned moduleId) { auto throwScope = DECLARE_THROW_SCOPE(vm); - // Shared with the bytecode cache generator so both sides key the cache on the same - // SourceCode; a mismatch here would silently turn every lookup into a miss. SourceCode source = builtinModuleSourceCode(SOURCE, moduleName, urlString); maybeAddCodeCoverage(vm, source); - // `bun build --compile --bytecode` embeds a cache entry for every builtin the app can - // reach. When it is absent or stale we fall through to parsing, which is what every - // other build does. + // Only a `--compile --bytecode` executable has entries to decode; everything else parses. UnlinkedFunctionExecutable* unlinkedExecutable = decodeBuiltinModuleBytecode(globalObject, vm, source, moduleName, moduleId); if (!unlinkedExecutable) unlinkedExecutable = createBuiltinModuleExecutable(vm, source, moduleName); @@ -101,9 +95,6 @@ ALWAYS_INLINE JSC::JSValue generateNativeModule( return defaultValue; } -// builtinModuleSource() is the blob span in release and the on-disk bundle in debug -// (BUN_DYNAMIC_JS_LOAD_PATH); the bytecode cache generator reads through the same -// function, so whatever this build parses is also what its cache entries were keyed on. #define INTERNAL_MODULE_REGISTRY_GENERATE(globalObject, vm, index, moduleName, urlString) \ return generateModule(globalObject, vm, builtinModuleSource(index), moduleName, urlString, index)