Skip to content
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
cc48ed1
resolver: after a miss, look up once more at a fresh generation
robobun Nov 1, 2026
4d76b75
resolver: size a listing read again in place from the previous one
robobun Nov 1, 2026
62169f0
resolver: keep an unchanged listing at a newer generation, check cach…
robobun Nov 1, 2026
892c55a
resolver: shorten the comments on the generation retry
robobun Nov 1, 2026
d5dede3
[autofix.ci] apply automated fixes
autofix-ci[bot] Nov 1, 2026
c4b8c44
resolver: check the files a lookup reads, not only the listings, afte…
robobun Nov 1, 2026
02043aa
resolver: never keep a listing without a change time, parse a listed …
robobun Nov 1, 2026
dcc43db
resolver: a listing whose directory fails to open again keeps its old…
robobun Nov 1, 2026
cc03e0b
resolver: a listing whose re-read failed drops its identity too
robobun Nov 1, 2026
1fdf1ee
resolver: a directory that is another one now drops its DirInfo after…
robobun Nov 1, 2026
adccf9d
resolver: shorten a comment
robobun Nov 1, 2026
8697795
resolver: compare the device with the inode, read the identity under …
robobun Nov 1, 2026
3c173c7
resolver: shorten a comment
robobun Nov 1, 2026
c27533e
resolver test: pin a config file that did not parse and a directory t…
robobun Nov 1, 2026
3e57ae3
resolver: check each directory level once per retry
robobun Nov 1, 2026
b4b308d
resolver: shorten a doc comment
robobun Nov 1, 2026
a13022e
resolver: stat the names a DirInfo derives from instead of listing it…
robobun Nov 9, 2026
84ad6a4
resolver: shorten a doc comment
robobun Nov 9, 2026
b8cb027
resolver: check a DirInfo against the listing it was built from
robobun Nov 9, 2026
10a25ca
Merge origin/main into robobun/3de5c29e/resolver-retry-at-fresh-gener…
robobun Nov 9, 2026
d50e63f
resolver: stat a linked name even when its directory is unchanged
robobun Nov 9, 2026
a11300a
resolver: shorten a comment
robobun Nov 9, 2026
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
13 changes: 11 additions & 2 deletions src/bun_core/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2266,8 +2266,17 @@ impl StackCheck {
// Small helpers that downstream crates need.
// ──────────────────────────────────────────────────────────────────────────

/// Bumped each rebuild/rescan to invalidate stale cache entries.
pub type Generation = u16;
/// Stamp on a cached directory listing: a reader at a newer generation reads the directory again.
pub type Generation = u64;

static GENERATION_COUNTER: core::sync::atomic::AtomicU64 = core::sync::atomic::AtomicU64::new(0);

/// A generation newer than every one handed out before, on any thread. Never 0.
pub fn next_generation() -> Generation {
// Relaxed: only the uniqueness of the value matters. The stamp reaches
// other threads through `entries_mutex`.
Comment thread
robobun marked this conversation as resolved.
Resolved
GENERATION_COUNTER.fetch_add(1, core::sync::atomic::Ordering::Relaxed) + 1
}
Comment thread
robobun marked this conversation as resolved.
Resolved

// ── Ordinal ───────────────────────────────────────────────────────────────
// ABI-equivalent of WTF::OrdinalNumber:
Expand Down
14 changes: 3 additions & 11 deletions src/bundler/BundleThread.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@ pub(crate) struct BundleThread<C: Node> {
// `bun.UnboundedQueue(CompletionStruct, .next)` — intrusive over `C.next`;
// the field offset is encoded via the `Node` supertrait on `CompletionStruct`.
pub(crate) queue: UnboundedQueue<C>,
pub(crate) generation: bun_core::Generation,
}

/// Trait capturing the interface a completion task must satisfy.
Expand Down Expand Up @@ -131,7 +130,6 @@ impl<C: CompletionStruct> BundleThread<C> {
waker: Async::Waker::placeholder(),
waker_error: None,
queue: UnboundedQueue::new(),
generation: 0,
ready_event: ResetEvent::default(),
}
}
Expand All @@ -156,7 +154,7 @@ impl<C: CompletionStruct> BundleThread<C> {
let ptr = ptr;
// SAFETY: caller guarantees `instance` is valid for 'static; `thread_main`
// accesses fields only via raw-ptr projection (never `&Self`/`&mut Self`)
// and is the sole writer of `waker`/`generation`, so concurrent `enqueue()`
// and is the sole writer of `waker`, so concurrent `enqueue()`
// from other threads is sound.
unsafe { Self::thread_main(ptr.0) }
})?;
Expand Down Expand Up @@ -257,8 +255,8 @@ impl<C: CompletionStruct> BundleThread<C> {
}
// SAFETY: as above; started ⇒ the owner waits for us.
let completion = unsafe { &mut *completion };
// SAFETY: `generation` is only read/written on this (bundle) thread.
let generation = unsafe { (*instance).generation };
// Newer than every cached listing, also one a runtime retry read again.
let generation = bun_core::next_generation();
// `panic = "abort"` → a Rust panic on this thread enters the
// crash-handler hook and aborts the whole process.
// No `catch_unwind` — there is nothing to catch.
Expand All @@ -271,12 +269,6 @@ impl<C: CompletionStruct> BundleThread<C> {
}
has_bundled = true;
}
// SAFETY: `generation` is only read/written on this (bundle) thread.
unsafe {
let g = core::ptr::addr_of_mut!((*instance).generation);
*g = (*g).saturating_add(1);
}

if has_bundled {
crate::bundle_v2::dispatch::__bun_jsc_destroy_bytecode_cache_vm();
bun_alloc::mimalloc::mi_collect(false);
Expand Down
145 changes: 54 additions & 91 deletions src/jsc/VirtualMachine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4363,21 +4363,6 @@ fn normalize_specifier_for_resolution<'a>(
}
}

/// Heap-backed so only a pointer lives in TLS; see test/js/bun/binary/tls-segment-size.
#[thread_local]
static SPECIFIER_CACHE_RESOLVER_BUF: core::cell::Cell<*mut bun_paths::PathBuffer> =
core::cell::Cell::new(core::ptr::null_mut());

#[inline]
fn specifier_cache_resolver_buf() -> *mut bun_paths::PathBuffer {
let mut p = SPECIFIER_CACHE_RESOLVER_BUF.get();
if p.is_null() {
p = bun_core::heap::into_raw(Box::new(bun_paths::PathBuffer::ZEROED));
SPECIFIER_CACHE_RESOLVER_BUF.set(p);
}
p
}

fn ensure_source_code_printer() {
if SOURCE_CODE_PRINTER.get().is_none() {
let writer = bun_js_printer::BufferWriter::init();
Expand Down Expand Up @@ -5582,14 +5567,13 @@ impl VirtualMachine {
top_level_dir
};

// A `loop`
// returning the resolver result; `retry_on_not_found` is consumed on
// the first miss.
// `retry_on_not_found` is consumed on the first miss.
let mut retry_on_not_found = bun_paths::is_absolute(source_to_use);
let mut may_retry_from_root = self.finds_entry_points_by_path && !is_special_source;
let mut source_before_root: Option<&[u8]> = None;
let mut has_read_folders_again = false;
let result: bun_resolver::Result = loop {
let mut fresh_generation: Option<bun_resolver::FreshGeneration> = None;
let result: crate::CrateResult<bun_resolver::Result> = loop {
let import_kind = if is_esm {
bun_ast::ImportKind::Stmt
} else {
Expand All @@ -5601,8 +5585,8 @@ impl VirtualMachine {
import_kind,
global_cache,
) {
ResultUnion::Success(r) => break r,
ResultUnion::Failure(e) => return Err(e.into()),
ResultUnion::Success(r) => break Ok(r),
ResultUnion::Failure(e) => break Err(e.into()),
ResultUnion::Pending(_) | ResultUnion::NotFound => {
if core::mem::take(&mut may_retry_from_root)
&& let Some(root) = self
Expand All @@ -5621,9 +5605,9 @@ impl VirtualMachine {
source_to_use,
import_kind,
) {
break chai;
break Ok(chai);
}
return Err(crate::CrateError::ModuleNotFound);
break Err(crate::CrateError::ModuleNotFound);
}
retry_on_not_found = false;
has_read_folders_again = true;
Expand All @@ -5636,81 +5620,60 @@ impl VirtualMachine {
may_retry_from_root = true;
}

// SAFETY: thread-local heap allocation; sole `&mut` on the JS
// thread for the duration of the bust below.
let buf = unsafe { &mut *specifier_cache_resolver_buf() }.as_mut_slice();
let mut busted_as_spelled = false;
let buster_name: &[u8] = if bun_paths::is_absolute(normalized_specifier) {
if let Some(dir) = bun_paths::dirname(normalized_specifier) {
if dir.len() > buf.len() {
return Err(crate::CrateError::ModuleNotFound);
}
// `load_as_file` caches the listing under the specifier's own slashes, so bust that key too.
busted_as_spelled = self.transpiler.resolver.bust_dir_cache(
bun_paths::string_paths::without_trailing_slash_windows_path(dir),
);
// Normalized without trailing slash.
bun_paths::string_paths::normalize_slashes_only(
buf,
dir,
bun_paths::SEP,
)
} else {
// Absolute but root — fall through to join.
&b""[..]
}
} else {
&b""[..]
};
let buster_name: &[u8] = if !buster_name.is_empty() {
buster_name
} else {
// If the specifier is too long to join, it can't name
// a real directory — skip the cache bust and fail.
if source_to_use.len() + normalized_specifier.len() + 4 >= buf.len() {
return Err(crate::CrateError::ModuleNotFound);
// The folder of a module of an executable stands for a folder on disk, and that is where the resolver has looked.
let is_in_executable =
bun_options_types::standalone_path::is_bun_standalone_file_path(
source_to_use,
);
let folder_on_disk = match self.standalone_module_graph {
Some(graph) if is_in_executable => {
let root = graph.root_on_disk();
let base = root.as_deref().unwrap_or(top_level_dir);
let mut folders = graph
.directories_of_importer(source_to_use, base)
.into_iter();
let resolver = &mut self.transpiler.resolver;
let found = folders
.find(|it| resolver.read_dir_info_ignore_error(it).is_some());
Some(found.unwrap_or_else(|| top_level_dir.to_vec()))
}
// The folder of a module of an executable stands for a folder on disk, and that is where the resolver has looked.
let is_in_executable =
bun_options_types::standalone_path::is_bun_standalone_file_path(
source_to_use,
);
let folder_on_disk = match self.standalone_module_graph {
Some(graph) if is_in_executable => {
let root = graph.root_on_disk();
let base = root.as_deref().unwrap_or(top_level_dir);
let mut folders = graph
.directories_of_importer(source_to_use, base)
.into_iter();
let resolver = &mut self.transpiler.resolver;
let found = folders
.find(|it| resolver.read_dir_info_ignore_error(it).is_some());
Some(found.unwrap_or_else(|| top_level_dir.to_vec()))
}
_ => None,
};
let parts: [&[u8]; 3] = [
folder_on_disk.as_deref().unwrap_or(source_to_use),
normalized_specifier,
bun_paths::path_literal!("..").as_bytes(),
];
bun_paths::resolve_path::join_abs_string_buf_z::<
bun_paths::resolve_path::platform::Auto,
>(top_level_dir, buf, &parts)
.as_bytes()
_ => None,
};

// Only re-query if we previously had something cached.
if self.transpiler.resolver.bust_dir_cache(
bun_paths::string_paths::without_trailing_slash_windows_path(buster_name),
) || busted_as_spelled
let importer_dir = folder_on_disk.as_deref().unwrap_or(source_to_use);
let resolver = &mut self.transpiler.resolver;
// SAFETY: the resolver is a field of this VM, which outlives `_resolve`.
fresh_generation = Some(unsafe { resolver.enter_fresh_generation() });
resolver.drop_stale_dir_info_chain(importer_dir);
// The directory a path specifier names: its `package.json` can have appeared.
if bun_paths::is_absolute(normalized_specifier) {
resolver.drop_stale_dir_info_chain(normalized_specifier);
Comment thread
robobun marked this conversation as resolved.
Resolved
} else if bun_paths::is_absolute(importer_dir)
&& (normalized_specifier.starts_with(b"./")
|| normalized_specifier.starts_with(b"../")
|| normalized_specifier.starts_with(b".\\")
|| normalized_specifier.starts_with(b"..\\"))
{
continue;
let mut buf = bun_paths::path_buffer_pool::get();
if let Some(named) = bun_paths::resolve_path::join_abs_string_buf_checked::<
bun_paths::resolve_path::platform::Auto,
>(
importer_dir, &mut buf[..], &[normalized_specifier]
) {
resolver.drop_stale_dir_info_chain(named);
}
}
// `require.resolve(id, { paths })` looks in each of these, not from the importer.
if let Some(paths) = resolver.custom_dir_paths {
for path in paths {
let path = path.to_utf8();
resolver.drop_stale_dir_info_chain(path.slice());
}
}
return Err(crate::CrateError::ModuleNotFound);
}
}
};
drop(fresh_generation);
let result = result?;

if !self.macro_mode {
self.has_any_macro_remappings =
Expand Down
4 changes: 4 additions & 0 deletions src/resolver/dir_info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,10 @@ bitflags::bitflags! {
/// This directory has a node_modules subdirectory
const HasNodeModules = 1 << 1;
const InsideNodeModules = 1 << 2;
/// A `package.json` file was listed, whether or not it parsed.
const HasPackageJsonFile = 1 << 3;
/// A `tsconfig.json` or `jsconfig.json` file was listed, whether or not it loaded.
const HasTsconfigFile = 1 << 4;
}
}
impl Flags {
Expand Down
Loading
Loading