Skip to content
Open
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
28 changes: 28 additions & 0 deletions src/ast/transpiler_cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,28 @@
use crate::{ExportsKind, Source};
use core::ptr::NonNull;

/// What a reader does with the ESM record (the serialized `ModuleInfo`) of an entry.
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum EsmRecordUse {
/// The record is not read.
Unused,
/// The record is read when the entry has one.
IfStored,
/// An ES-module entry without a record is a miss for `get()`, and `put()` can add the record.
Required,
}

impl EsmRecordUse {
/// Macro mode prints other output under the same key, so that reader takes an entry as it is.
pub fn for_reader(uses_records: bool, macro_mode: bool) -> Self {
match (uses_records, macro_mode) {
(false, _) => Self::Unused,
Comment thread
robobun marked this conversation as resolved.
Resolved
(true, true) => Self::IfStored,
(true, false) => Self::Required,
}
}
}

pub struct RuntimeTranspilerCache {
pub input_hash: Option<u64>,
pub input_byte_length: Option<u64>,
Expand All @@ -25,6 +47,10 @@ pub struct RuntimeTranspilerCache {
/// Opaque storage for `bun_bundler::cache::RuntimeTranspilerCacheEntry` —
/// the concrete type lives a tier up and is round-tripped via cast.
pub entry: Option<*mut ()>,
/// Set by the caller before the parse.
pub esm_record_use: EsmRecordUse,
/// Output hash of the entry that `get()` refused for its missing ESM record.
pub refused_output_hash: Option<u64>,

/// Dispatch slot — `bun_jsc` sets `Some(TranspilerCacheImplKind::Jsc)` at
/// init. `None` ⇒ caching disabled (e.g. wasm builds, `--no-transpiler-cache`).
Expand All @@ -40,6 +66,8 @@ impl Default for RuntimeTranspilerCache {
exports_kind: ExportsKind::None,
output_code: None,
entry: None,
esm_record_use: EsmRecordUse::Unused,
refused_output_hash: None,
r#impl: None,
}
}
Expand Down
89 changes: 78 additions & 11 deletions src/jsc/RuntimeTranspilerCache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
use core::cell::{Cell, RefCell};
use core::sync::atomic::{AtomicBool, Ordering};

use bun_ast::ExportsKind;
use bun_ast::Source;
use bun_ast::{EsmRecordUse, ExportsKind};
use bun_bundler::analyze_transpiled_module::ModuleInfoDeserialized;
use bun_core::{FeatureFlags, env_var};
use bun_core::{String as BunString, StringView, ZStr};
use bun_js_parser::ParserOptions;
Expand Down Expand Up @@ -253,7 +254,7 @@ pub struct Entry {
pub metadata: Metadata,
pub output_code: BunString,
pub sourcemap: Box<[u8]>,
pub esm_record: Box<[u8]>,
pub(crate) esm_record: Box<[u8]>,
}

impl Entry {
Expand Down Expand Up @@ -404,7 +405,13 @@ impl Entry {
Ok(())
}

pub(crate) fn load(&mut self, file: &sys::File, stat_size: u64) -> crate::CrateResult<()> {
/// `read_esm_record = false` skips the read, the allocation and the hash of the record.
pub(crate) fn load(
&mut self,
file: &sys::File,
stat_size: u64,
read_esm_record: bool,
) -> crate::CrateResult<()> {
let required = (Metadata::SIZE as u64)
.saturating_add(self.metadata.output_byte_length)
.saturating_add(self.metadata.sourcemap_byte_length)
Expand Down Expand Up @@ -536,7 +543,7 @@ impl Entry {
return Err(crate::CrateError::InvalidSourceMap);
}

if self.metadata.esm_record_byte_length > 0 {
if read_esm_record && self.metadata.esm_record_byte_length > 0 {
let esm_record = pread_box(
file,
self.metadata.esm_record_byte_length as usize,
Expand All @@ -554,14 +561,33 @@ impl Entry {

Ok(())
}

/// `None` unless `load` read a record. It reads none for `EsmRecordUse::Unused`.
pub fn module_info(&self) -> Option<Box<ModuleInfoDeserialized>> {
if self.metadata.module_type == ModuleType::Cjs || self.esm_record.is_empty() {
return None;
}
ModuleInfoDeserialized::create_from_cached_record(&self.esm_record)
}
}

/// What a lookup found when the header of the entry on disk matches.
pub(crate) enum Found {
Hit(Box<Entry>),
/// An ES-module entry without an ESM record, for a reader that requires one.
MissingEsmRecord {
output_hash: u64,
},
}

pub struct RuntimeTranspilerCache {
pub(crate) input_hash: Option<u64>,
pub(crate) input_byte_length: Option<u64>,
pub(crate) features_hash: Option<u64>,
pub(crate) exports_kind: ExportsKind,
pub(crate) entry: Option<Entry>,
pub(crate) esm_record_use: EsmRecordUse,
pub(crate) refused_output_hash: Option<u64>,
pub(crate) entry: Option<Box<Entry>>,
// `sourcemap` / `esm_record` are owned `Box<[u8]>` (global mimalloc).
// The per-call arena that once backed the output code is gone: the UTF-8
// load arm preads straight into WTF storage (see `Entry::load`), so no
Expand Down Expand Up @@ -741,7 +767,8 @@ impl RuntimeTranspilerCache {
input_hash: u64,
feature_hash: u64,
input_stat_size: u64,
) -> crate::CrateResult<Entry> {
esm_record_use: EsmRecordUse,
) -> crate::CrateResult<Found> {
let _tracer = bun_core::perf::trace("RuntimeTranspilerCache.fromFile");

let mut cache_file_path_buf = bun_paths::path_buffer_pool::get();
Expand All @@ -752,6 +779,7 @@ impl RuntimeTranspilerCache {
input_hash,
feature_hash,
input_stat_size,
esm_record_use,
)
}

Expand All @@ -760,7 +788,8 @@ impl RuntimeTranspilerCache {
input_hash: u64,
feature_hash: u64,
input_stat_size: u64,
) -> crate::CrateResult<Entry> {
esm_record_use: EsmRecordUse,
) -> crate::CrateResult<Found> {
let mut metadata_bytes_buf = [0u8; Metadata::SIZE];
// NONBLOCK: a FIFO must not block the open. On Windows it would make the handle overlapped.
#[cfg(unix)]
Expand Down Expand Up @@ -801,10 +830,26 @@ impl RuntimeTranspilerCache {
return Err(crate::CrateError::MismatchedFeatureHash);
}

entry.load(&file, stat_size)?;
if esm_record_use == EsmRecordUse::Required
&& entry.metadata.module_type != ModuleType::Cjs
&& entry.metadata.esm_record_byte_length == 0
{
// keep the cache in this case: it is a hit for a reader that does not require the record
let _ = scopeguard::ScopeGuard::into_inner(unlink_guard);
return Ok(Found::MissingEsmRecord {
Comment thread
robobun marked this conversation as resolved.
Resolved
output_hash: entry.metadata.output_hash,
});
}

entry.load(&file, stat_size, esm_record_use != EsmRecordUse::Unused)?;
debug_assert!(
esm_record_use != EsmRecordUse::Required
|| entry.metadata.module_type == ModuleType::Cjs
|| !entry.esm_record.is_empty()
);

let _ = scopeguard::ScopeGuard::into_inner(unlink_guard);
Ok(entry)
Ok(Found::Hit(Box::new(entry)))
}

pub(crate) fn to_file(
Expand Down Expand Up @@ -911,8 +956,18 @@ impl RuntimeTranspilerCache {
input_hash,
self.features_hash.unwrap(),
source.contents.len() as u64,
self.esm_record_use,
) {
Ok(e) => Some(e),
Ok(Found::Hit(e)) => Some(e),
Ok(Found::MissingEsmRecord { output_hash }) => {
self.refused_output_hash = Some(output_hash);
bun_core::scoped_log!(
cache,
"get(\"{}\") = MissingEsmRecord",
bstr::BStr::new(source.path.text)
);
return false;
}
Err(err) => {
bun_core::scoped_log!(
cache,
Expand Down Expand Up @@ -979,15 +1034,18 @@ bun_ast::link_impl_TranspilerCacheImpl! {
input_byte_length: this.input_byte_length,
features_hash: this.features_hash,
exports_kind: this.exports_kind,
esm_record_use: this.esm_record_use,
refused_output_hash: None,
entry: None,
};
let hit = jsc.get(source, parser_options, used_jsx);
this.input_hash = jsc.input_hash;
this.input_byte_length = jsc.input_byte_length;
this.features_hash = jsc.features_hash;
this.exports_kind = jsc.exports_kind;
this.refused_output_hash = jsc.refused_output_hash;
if let Some(entry) = jsc.entry {
this.entry = Some(bun_core::heap::into_raw(Box::new(entry)).cast::<()>());
this.entry = Some(bun_core::heap::into_raw(entry).cast::<()>());
}
hit
},
Expand All @@ -1002,6 +1060,15 @@ bun_ast::link_impl_TranspilerCacheImpl! {
}
debug_assert!(this.entry.is_none());

// The key does not cover every input of a transpile: the refused entry stays unless this is its output.
if this
.refused_output_hash
.is_some_and(|output_hash| hash(output_code_bytes) != output_hash)
{
bun_core::scoped_log!(cache, "put() = kept the entry: other output");
return;
}
Comment thread
robobun marked this conversation as resolved.
Resolved

Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
// Borrowed Latin-1 view: `to_file` only reads `byte_slice()` + the encoding
// tag (unmarked 8-bit EncodedSlice -> Encoding::LATIN1, same as clone_latin1),
// and `output_code_bytes` outlives the synchronous `to_file` call.
Expand Down
29 changes: 12 additions & 17 deletions src/jsc/RuntimeTranspilerStore.rs
Original file line number Diff line number Diff line change
Expand Up @@ -720,6 +720,17 @@ impl TranspilerJob {
unsafe { &mut *(&raw mut *transpiler_storage).cast::<Transpiler<'_>>() };
transpiler.set_arena(&arena);
transpiler.set_log(&raw mut log);
// SAFETY: leaf scalar field read; see `vm` note above. Inlined
// `VirtualMachine::use_isolation_source_provider_cache` to avoid forming
// `&VirtualMachine`.
let use_isolation_source_provider_cache = unsafe { (*vm).test_isolation_enabled }
&& !bun_core::env_var::feature_flag::BUN_FEATURE_FLAG_DISABLE_ISOLATION_SOURCE_CACHE::get()
.unwrap_or(false);
// The copy has the macro target if the JS thread was in a macro when it was made.
cache.esm_record_use = bun_ast::EsmRecordUse::for_reader(
use_isolation_source_provider_cache,
transpiler.options.target == bun_ast::Target::BunMacro,
);
// Note: the resolver already shares opts with the parent
// Transpiler via raw pointer; set_arena/set_log keep them in sync.
transpiler.macro_context = None;
Expand Down Expand Up @@ -959,13 +970,6 @@ impl TranspilerJob {
}
}

// SAFETY: leaf scalar field read; see `vm` note above. Inlined
// `VirtualMachine::use_isolation_source_provider_cache` to avoid forming
// `&VirtualMachine`.
let use_isolation_source_provider_cache = unsafe { (*vm).test_isolation_enabled }
&& !bun_core::env_var::feature_flag::BUN_FEATURE_FLAG_DISABLE_ISOLATION_SOURCE_CACHE::get()
.unwrap_or(false);

if let Some(entry_ptr) = cache.entry.take() {
// SAFETY: `entry` was boxed by `JSC_PARSER_CACHE_VTABLE.get` from a
// concrete `crate::runtime_transpiler_cache::Entry`; sole owner.
Expand All @@ -987,16 +991,7 @@ impl TranspilerJob {
dump_source_string(vm, specifier, entry.output_code.byte_slice());
}

let module_info = if use_isolation_source_provider_cache
&& entry.metadata.module_type != CacheModuleType::Cjs
&& !entry.esm_record.is_empty()
{
analyze_transpiled_module::ModuleInfoDeserialized::create_from_cached_record(
&entry.esm_record,
)
} else {
None
};
let module_info = entry.module_info();

self.resolved_source = ResolvedSource {
source_code: core::mem::take(&mut entry.output_code),
Expand Down
34 changes: 15 additions & 19 deletions src/runtime/jsc_hooks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2438,8 +2438,19 @@ fn transpile_source_code_of_any_provenance<const DATA_ONLY: bool>(
}

// ── RuntimeTranspilerCache ──────────────────────────────────────
// SAFETY: per fn contract.
let (use_isolation_source_provider_cache, macro_mode) = unsafe {
(
(*jsc_vm).use_isolation_source_provider_cache(),
(*jsc_vm).macro_mode,
)
};
let mut cache = bun_ast::RuntimeTranspilerCache {
r#impl: Some(bun_ast::TranspilerCacheImplKind::Jsc),
esm_record_use: bun_ast::EsmRecordUse::for_reader(
use_isolation_source_provider_cache,
macro_mode,
),
..Default::default()
};

Expand Down Expand Up @@ -2485,12 +2496,8 @@ fn transpile_source_code_of_any_provenance<const DATA_ONLY: bool>(
let is_node_override = !DATA_ONLY && specifier.starts_with(node_fallbacks::IMPORT_PATH);

// SAFETY: per fn contract.
let (macro_mode, has_any_macro_remappings) = unsafe {
(
(*jsc_vm).macro_mode,
!DATA_ONLY && (*jsc_vm).has_any_macro_remappings,
)
};
let has_any_macro_remappings =
unsafe { !DATA_ONLY && (*jsc_vm).has_any_macro_remappings };
let macro_remappings = if macro_mode || !has_any_macro_remappings || is_node_override {
bun_resolver::package_json::MacroMap::default()
} else {
Expand Down Expand Up @@ -2940,17 +2947,7 @@ fn transpile_source_code_of_any_provenance<const DATA_ONLY: bool>(
// Rebuild the cached ESM record for the
// isolation source-provider cache (same shape as
// `RuntimeTranspilerStore`).
// SAFETY: per fn contract — `jsc_vm` is the live per-thread VM.
let module_info = if unsafe { &*jsc_vm }.use_isolation_source_provider_cache()
&& entry.metadata.module_type != CacheModuleType::Cjs
&& !entry.esm_record.is_empty()
{
bun_bundler::analyze_transpiled_module::ModuleInfoDeserialized::create_from_cached_record(
&entry.esm_record,
)
} else {
None
};
let module_info = entry.module_info();
let is_commonjs_module = entry.metadata.module_type == CacheModuleType::Cjs;
// Node compile cache hook (transpiler-cache-hit path); must
// read `output_code` before it is consumed below. UTF-16
Expand Down Expand Up @@ -3103,10 +3100,9 @@ fn transpile_source_code_of_any_provenance<const DATA_ONLY: bool>(
|| parse_result.ast.exports_kind == bun_ast::ExportsKind::Cjs;
// Collect the ESM record while printing, for the isolation
// source-provider cache (same shape as `RuntimeTranspilerStore`).
// SAFETY: per fn contract — `jsc_vm` is the live per-thread VM.
let mut module_info: Option<
Box<bun_bundler::analyze_transpiled_module::ModuleInfo>,
> = if unsafe { &*jsc_vm }.use_isolation_source_provider_cache()
> = if use_isolation_source_provider_cache
&& !is_commonjs_module
&& loader.is_java_script_like()
{
Expand Down
Loading
Loading