Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
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
120 changes: 120 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));
}

// Native modules have no JS source, so `generate_builtin_module_bytecode`
// returns `None` for them — as it does for anything that fails to compile.
Comment thread
robobun marked this conversation as resolved.
Outdated
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,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.
Comment thread
robobun marked this conversation as resolved.
Outdated
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 +4108,7 @@ pub mod bv2_impl {
output_files,
metafile,
metafile_markdown: None,
builtin_bytecode,
})
})();

Expand Down Expand Up @@ -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.
Comment thread
robobun marked this conversation as resolved.
Outdated
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 +5275,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
119 changes: 101 additions & 18 deletions src/codegen/bundle-modules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, 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.
*
* 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.
*/
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 +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,
});
Expand Down Expand Up @@ -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 });
Expand Down Expand Up @@ -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
Expand All @@ -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.
Comment thread
robobun marked this conversation as resolved.
Outdated
{
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 +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}
}`;
Expand Down
Loading
Loading