Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion scripts/build/deps/webkit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@
* for local mode. Override via `--webkit-version=<hash>` 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.
Expand Down
3 changes: 3 additions & 0 deletions src/bundler/BundleThread.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ pub struct BuildResult {
pub output_files: Vec<crate::options::OutputFile>,
pub metafile: Option<Box<[u8]>>,
pub metafile_markdown: Option<Box<[u8]>>,
/// `(InternalModuleRegistry id, JSC cache entry)` for each JS builtin the bundle can
/// reach, for `--compile --bytecode` to embed. Empty otherwise.
Comment thread
robobun marked this conversation as resolved.
pub builtin_bytecode: Vec<(u32, Box<[u8]>)>,
}

pub enum BundleV2Result {
Expand Down
116 changes: 116 additions & 0 deletions src/bundler/bundle_v2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1377,6 +1377,19 @@ pub mod bv2_impl {
source: &[u8],
source_provider_url: &mut bun_core::String,
) -> Option<Box<[u8]>>;

/// Defined `#[no_mangle]` in `bun_jsc::cached_bytecode`. Serializes one JS
/// builtin's compiled tree. `None` for native modules and compile failures.
Comment thread
robobun marked this conversation as resolved.
safe fn __bun_jsc_generate_builtin_module_bytecode(module_id: u32)
-> Option<Box<[u8]>>;

/// Defined `#[no_mangle]` in `bun_jsc::cached_bytecode`. The builtins
/// `module_id` requires directly, as a view into a static C++ table.
Comment thread
robobun marked this conversation as resolved.
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.
Comment thread
robobun marked this conversation as resolved.
safe fn __bun_jsc_builtin_module_id_for_specifier(specifier: &[u8]) -> Option<u32>;
}

unsafe extern "Rust" {
Expand Down Expand Up @@ -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<Box<[u8]>> {
__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<u32> {
__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
Expand Down Expand Up @@ -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.
Comment thread
robobun marked this conversation as resolved.
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<u32> = 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.
Comment thread
robobun marked this conversation as resolved.
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<u32> = 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.
Comment thread
robobun marked this conversation as resolved.
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,
Expand Down Expand Up @@ -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::<false>(
&mut this.linker,
&mut chunks,
Expand Down Expand Up @@ -4001,6 +4106,7 @@ pub mod bv2_impl {
output_files,
metafile,
metafile_markdown: None,
builtin_bytecode,
})
})();

Expand Down Expand Up @@ -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::<false>(
&mut self.linker,
&mut chunks,
Expand Down Expand Up @@ -5156,6 +5271,7 @@ pub mod bv2_impl {
output_files,
metafile,
metafile_markdown,
builtin_bytecode,
})
}
}
Expand Down
6 changes: 6 additions & 0 deletions src/bundler/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Comment thread
robobun marked this conversation as resolved.
pub compile_target_is_host: bool,
pub metafile: bool,
/// Path to write JSON metafile (for Bun.build API)
pub metafile_json_path: Box<[u8]>,
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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(),
Expand Down
114 changes: 96 additions & 18 deletions src/codegen/bundle-modules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, string>): string[] {
// The bundled sources already have require() rewritten to registry lookups,
// `internalModuleRegistry, <index>` — 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, <index>`,
* 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.
*/
Comment thread
robobun marked this conversation as resolved.
Comment thread
robobun marked this conversation as resolved.
function requireGraph(modules: string[], outputs: Map<string, string>): number[][] {
const requireRe = /internalModuleRegistry, ?(\d+)/g;
const deps = new Map<string, string[]>();
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<string>();
const edges = new Set<number>();
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<string, string[]>();
for (let i = 0; i < modules.length; i++) {
deps.set(
modules[i],
graph[i].map(dep => modules[dep]),
);
}
const hotRoots = [
"node/fs.ts",
Expand Down Expand Up @@ -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,
});
Expand Down Expand Up @@ -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 });
Expand Down Expand Up @@ -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
Expand All @@ -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.
Comment thread
robobun marked this conversation as resolved.
{
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 <wtf/text/ASCIILiteral.h>

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.
Comment thread
robobun marked this conversation as resolved.
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.
Comment thread
robobun marked this conversation as resolved.
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"),
Expand All @@ -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}
}`;
Expand Down
Loading
Loading