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..9b51b5797b13 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)); + } + + // `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)?))) + .collect() + } + pub fn generate_from_cli( transpiler: &'a mut Transpiler<'a>, alloc: &'a bun_alloc::Arena, @@ -3971,9 +4066,19 @@ pub mod bv2_impl { output_files: Vec::new(), metafile: None, metafile_markdown: None, + builtin_bytecode: Vec::new(), }); } + 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 +4106,7 @@ pub mod bv2_impl { output_files, metafile, metafile_markdown: None, + builtin_bytecode, }) })(); @@ -5090,6 +5196,15 @@ pub mod bv2_impl { return Err(crate::Error::BuildFailed); } + 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 +5271,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..a37ed19c43b6 100644 --- a/src/codegen/bundle-modules.ts +++ b/src/codegen/bundle-modules.ts @@ -296,20 +296,35 @@ 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. + * + * 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; - 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 +383,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 +434,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 +486,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 +512,61 @@ ${moduleSpans `, ); +// 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[] = []; + 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 +577,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..e77455446114 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,53 @@ 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`: [`__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); + 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, 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(); + 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..635d31f6fd3f --- /dev/null +++ b/src/jsc/bindings/BuiltinModuleBytecode.cpp @@ -0,0 +1,153 @@ +#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 + 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 + // 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 +} + +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); +} + +// Defined in StandaloneModuleGraph.rs. +extern "C" bool Bun__getBuiltinModuleBytecode(unsigned moduleId, uint8_t** outBytes, size_t* outLength); + +// 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() +{ + s_hasBuiltinModuleBytecode.store(true, std::memory_order_relaxed); +} + +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; + + // Entries are generated with an empty CodeGenerationMode; a debugger or profiler changes it. + if (!globalObject->defaultCodeGenerationMode().isEmpty()) + return nullptr; + + // The bytes are part of the executable image; nothing owns or frees them. + 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; +} + +// `*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); + 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))); +} + +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..e36838ef9ef1 --- /dev/null +++ b/src/jsc/bindings/BuiltinModuleBytecode.h @@ -0,0 +1,29 @@ +#pragma once + +#include "root.h" + +#include +#include + +namespace Bun { + +// Number of builtins this process loaded from an embedded bytecode cache instead of parsing. +BUN_DECLARE_HOST_FUNCTION(Bun__builtinModuleBytecodeDecodedCount); + +// 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); + +JSC::SourceCode builtinModuleSourceCode(const WTF::String& source, const WTF::String& moduleName, const WTF::String& urlString); + +// Builtin parse mode: the only mode whose lexer accepts the `@`-prefixed intrinsics. +JSC::UnlinkedFunctionExecutable* createBuiltinModuleExecutable(JSC::VM&, const JSC::SourceCode&, const WTF::String& moduleName); + +// 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 c538322f8e39..e5ad33c0ba4f 100644 --- a/src/jsc/bindings/BunClientData.cpp +++ b/src/jsc/bindings/BunClientData.cpp @@ -103,6 +103,14 @@ JSVMClientData::~JSVMClientData() if (vmHandle) Bun__VmHandle__release(std::exchange(vmHandle, nullptr)); } + +void JSVMClientData::registerBuiltinNames(VM& 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) { auto provider = WebCore::createBuiltinsSourceProvider(); diff --git a/src/jsc/bindings/BunClientData.h b/src/jsc/bindings/BunClientData.h index 21a163421c03..cd36a52899d8 100644 --- a/src/jsc/bindings/BunClientData.h +++ b/src/jsc/bindings/BunClientData.h @@ -136,6 +136,10 @@ class JSVMClientData : public JSC::VM::ClientData { static void create(JSC::VM*, void* bunVM, bool isWorkerVM); + // 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; } 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..e3d953e37192 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,22 @@ 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. +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); + SourceCode source = builtinModuleSourceCode(SOURCE, moduleName, urlString); maybeAddCodeCoverage(vm, source); + + // 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); + 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 +95,8 @@ 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 +#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);