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
64 changes: 48 additions & 16 deletions src/jsc/hot_reloader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -405,8 +405,10 @@ pub struct NewHotReloader<Ctx, EventLoopType, const RELOAD_IMMEDIATELY: bool> {

pub(crate) main: MainFile,

/// Last cached listing per watched directory, kept past `bust_dir_cache`
/// to invalidate per-file stat caches; see [`Self::probe_entries_cache`].
Comment thread
robobun marked this conversation as resolved.
#[cfg(not(windows))]
pub(crate) tombstones: StringHashMap<*mut Fs::EntriesOption>,
pub(crate) tombstones: StringHashMap<*mut Fs::DirEntry>,

/// See [`HotReloaderCtx::reload_handle`].
pub(crate) reload_handle: Option<crate::VmHandle>,
Expand Down Expand Up @@ -792,15 +794,39 @@ where
}

#[cfg(not(windows))]
fn put_tombstone(&mut self, key: &[u8], value: *mut Fs::EntriesOption) {
fn put_tombstone(&mut self, key: &[u8], value: *mut Fs::DirEntry) {
self.tombstones.put(key, value).expect("unreachable");
}

#[cfg(not(windows))]
fn get_tombstone(&mut self, key: &[u8]) -> Option<*mut Fs::EntriesOption> {
fn get_tombstone(&mut self, key: &[u8]) -> Option<*mut Fs::DirEntry> {
self.tombstones.get(key).copied()
}

/// Looks up `key` in the process-global directory-entry cache and returns
/// the cached `DirEntry`'s stable address (`None` for a cached read
/// error).
///
/// Resolver and bundler threads rewrite the cache in place under
/// `entries_mutex` (`read_directory`, `entries_at`), so this watcher
/// thread takes that lock for every probe or `data`-map walk. The pointee
/// is a cache-owned leaked `Box<DirEntry>` (process lifetime), valid
/// after the guard drops. Lock order `Watcher.mutex` then `entries_mutex`
/// matches this thread's `bust_dir_cache`; nothing takes the reverse.
Comment thread
robobun marked this conversation as resolved.
#[cfg(not(windows))]
fn probe_entries_cache(
rfs: &mut Fs::file_system::RealFS,
key: &[u8],
) -> Option<*mut Fs::DirEntry> {
let _entries_lock = rfs.entries_mutex.lock_guard();
match rfs.entries.get(key) {
Some(Fs::EntriesOption::Entries(existing)) => {
Some(std::ptr::from_mut::<Fs::DirEntry>(*existing))
}
_ => None,
}
}

pub(crate) fn on_error(_: &mut Self, err: &bun_sys::Error) {
// `bun_sys::Error::name()` does the errno→tag-name lookup.
Output::err(err.name(), "Watcher crashed", ());
Expand Down Expand Up @@ -969,7 +995,7 @@ where
#[cfg(not(windows))]
{
let mut affected_buf: [&[u8]; 128] = [b"".as_slice(); 128];
let mut entries_option: Option<*mut Fs::EntriesOption> = None;
let mut entries_option: Option<*mut Fs::DirEntry> = None;

// Note: the affected-name element type differs by
// platform (kqueue vs inotify). Split into two locals;
Expand All @@ -980,9 +1006,7 @@ where

let affected_len: usize = 'brk: {
if IS_KQUEUE {
// SAFETY: hot-reload runs single-threaded on the JS thread;
// no other live `&mut EntriesOption` for this key here.
if let Some(existing) = rfs.entries.get(file_path) {
if let Some(existing) = Self::probe_entries_cache(rfs, file_path) {
self.put_tombstone(file_path, existing);
entries_option = Some(existing);
} else if let Some(existing) = self.get_tombstone(file_path) {
Expand Down Expand Up @@ -1073,7 +1097,7 @@ where
};

if affected_len > 0 && !IS_KQUEUE {
if let Some(existing) = rfs.entries.get(file_path) {
if let Some(existing) = Self::probe_entries_cache(rfs, file_path) {
self.put_tombstone(file_path, existing);
entries_option = Some(existing);
} else if let Some(existing) = self.get_tombstone(file_path) {
Expand Down Expand Up @@ -1134,11 +1158,6 @@ where
}

if let Some(dir_ent) = entries_option {
// SAFETY: dir_ent points into rfs.entries (or a tombstoned copy);
// both outlive this loop iteration. Shared access only —
// `entries()` takes `&self` and per-entry mutation below goes
// through the entry's own mutex + cells.
let dir_ent = unsafe { &*dir_ent };
let mut last_file_hash: bun_watcher::HashType =
bun_watcher::HashType::MAX;

Expand Down Expand Up @@ -1171,10 +1190,23 @@ where
let path_string: bun_ptr::Interned;
let file_hash: bun_watcher::HashType;
let abs_path: &[u8] = 'brk: {
if let Some(file_ent) = dir_ent.entries().get(changed_name)
{
// Locked walk (see `probe_entries_cache`);
// the `*mut Entry` is EntryStore-owned and
// may outlive the guard.
Comment thread
robobun marked this conversation as resolved.
let file_ent: Option<core::ptr::NonNull<Fs::Entry>> = {
let _entries_lock = rfs.entries_mutex.lock_guard();
// SAFETY: `dir_ent` is cache-owned
// (see `probe_entries_cache`); the
// shared borrow is confined to
// this statement, under the lock.
let lookup = unsafe { (*dir_ent).get(changed_name) };
lookup.map(|l| core::ptr::NonNull::from(l.entry()))
};
if let Some(file_ent) = file_ent {
// reset the file descriptor
let ent = file_ent.entry();
// SAFETY: EntryStore-owned slot;
// never freed.
let ent = unsafe { file_ent.as_ref() };
{
// Every cached-`Entry` rewrite takes
// the per-entry mutex.
Expand Down
Loading