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
5 changes: 2 additions & 3 deletions src/bun_core/env_var.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,9 +87,8 @@ new!(pub BUN_FEATURE_FLAG_DUMP_CODE: string, "BUN_FEATURE_FLAG_DUMP_CODE", {});
new!(pub BUN_GC_RUNS_UNTIL_SKIP_RELEASE_ACCESS: unsigned, "BUN_GC_RUNS_UNTIL_SKIP_RELEASE_ACCESS", {});
new!(pub BUN_GC_TIMER_DISABLE: boolean, "BUN_GC_TIMER_DISABLE", {});
new!(pub BUN_GC_TIMER_INTERVAL: unsigned, "BUN_GC_TIMER_INTERVAL", {});
// TODO(markovejnovic): It's unclear why the default here is 100_000, but this was legacy behavior
// so we'll keep it for now.
new!(pub BUN_INOTIFY_COALESCE_INTERVAL: unsigned, "BUN_INOTIFY_COALESCE_INTERVAL", { default: 100_000 });
// Nanoseconds. Honoured by every watcher backend, not just inotify.
new!(pub BUN_INOTIFY_COALESCE_INTERVAL: unsigned, "BUN_INOTIFY_COALESCE_INTERVAL", { default: 10_000_000 });
new!(pub BUN_INSPECT: string, "BUN_INSPECT", { default: b"" });
new!(pub BUN_INSPECT_CONNECT_TO: string, "BUN_INSPECT_CONNECT_TO", { default: b"" });
new!(pub BUN_INSPECT_PRELOAD: string, "BUN_INSPECT_PRELOAD", {});
Expand Down
5 changes: 5 additions & 0 deletions src/jsc/hot_reloader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -540,6 +540,11 @@ where
}

pub(crate) fn append(&mut self, id: u32) {
// One save repeats its hash; duplicates would hit the mid-update `enqueue` below.
if self.hashes[..self.count as usize].contains(&id) {
return;
}

if self.count == 8 {
self.enqueue();
self.count = 0;
Expand Down
90 changes: 45 additions & 45 deletions src/watcher/INotifyWatcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@ use core::ffi::c_int;
use core::mem::{align_of, size_of};
use core::sync::atomic::{AtomicU32, Ordering};

use bun_core::{ZStr, env_var, output as Output};
use bun_core::{ZStr, output as Output};
use bun_paths::MAX_PATH_BYTES;
use bun_sys::{self, Fd};
use bun_threading::Futex;

use crate::watcher_impl::{MAX_COUNT as max_count, Op, WatchEvent, WatchItemIndex, Watcher};
use crate::watcher_impl::{
MAX_COALESCE_ITERATIONS, MAX_COUNT as max_count, Op, WatchEvent, WatchItemIndex, Watcher,
coalesce_interval_ns, coalesce_timespec,
};
use bun_collections::index_sort;

bun_core::declare_scope!(watcher, visible);
Expand Down Expand Up @@ -54,8 +57,8 @@ pub struct INotifyWatcher {
read_ptr: Option<ReadPtr>,

pub(crate) watch_count: AtomicU32,
/// nanoseconds
pub(crate) coalesce_interval: isize,
/// See [`coalesce_interval_ns`].
pub(crate) coalesce_interval: u64,
}

impl Default for INotifyWatcher {
Expand All @@ -67,7 +70,7 @@ impl Default for INotifyWatcher {
eventlist_ptrs: [core::ptr::null(); max_count],
read_ptr: None,
watch_count: AtomicU32::new(0),
coalesce_interval: 100_000,
coalesce_interval: 0,
}
}
}
Expand Down Expand Up @@ -197,10 +200,7 @@ impl INotifyWatcher {
Ok(Self {
fd,
loaded: true,
coalesce_interval: env_var::BUN_INOTIFY_COALESCE_INTERVAL
.get()
.and_then(|v| isize::try_from(v).ok())
.unwrap_or(100_000),
coalesce_interval: coalesce_interval_ns(),
..Self::default()
})
}
Expand Down Expand Up @@ -246,19 +246,22 @@ impl INotifyWatcher {
return Ok(&[]);
}

// IN_MODIFY is very noisy
// we do a 0.1ms sleep to try to coalesce events better
const DOUBLE_READ_THRESHOLD: usize = Event::LARGEST_SIZE * (max_count / 2);
if read_len < DOUBLE_READ_THRESHOLD {
// Drain until quiet; beyond `max_count` the parser sets `read_ptr` anyway.
let timespec = coalesce_timespec(self.coalesce_interval);
let mut iterations: u32 = 0;
while read_len < size_of::<Event>() * max_count
&& iterations < MAX_COALESCE_ITERATIONS
{
let rest = &mut self.eventlist_bytes.0[read_len..];
if rest.len() < Event::LARGEST_SIZE {
break; // buffer nearly full
}

let mut fds = [system::pollfd {
fd: self.fd.native(),
events: (libc::POLLIN | libc::POLLERR) as _,
revents: 0,
}];
let timespec = libc::timespec {
tv_sec: 0,
tv_nsec: self.coalesce_interval as _,
};
// SAFETY: fds and timespec are valid stack locals; sigmask is null.
let poll_n = unsafe {
system::ppoll(
Expand All @@ -268,37 +271,34 @@ impl INotifyWatcher {
core::ptr::null(),
)
};
if poll_n > 0 {
'inner: loop {
let rest = &mut self.eventlist_bytes.0[read_len..];
debug_assert!(!rest.is_empty());
// SAFETY: fd valid; rest is a valid mutable buffer.
let new_rc = unsafe {
system::read(
self.fd.native(),
rest.as_mut_ptr(),
rest.len(),
)
};
let e = get_errno(new_rc);
match e {
E::SUCCESS => {
read_len += usize::try_from(new_rc).expect("int cast");
break 'outer read_len;
}
E::EAGAIN | E::EINTR => {
continue 'inner;
}
_ => {
return Err(bun_sys::Error {
errno: e as u32 as _,
syscall: bun_sys::Tag::read,
..Default::default()
});
}
if poll_n <= 0 {
break; // quiet
}

'inner: loop {
// SAFETY: fd valid; rest is a valid mutable buffer.
let new_rc = unsafe {
system::read(self.fd.native(), rest.as_mut_ptr(), rest.len())
};
let e = get_errno(new_rc);
match e {
E::SUCCESS => {
read_len += usize::try_from(new_rc).expect("int cast");
break 'inner;
}
E::EAGAIN | E::EINTR => {
continue 'inner;
}
_ => {
return Err(bun_sys::Error {
errno: e as u32 as _,
syscall: bun_sys::Tag::read,
..Default::default()
});
}
}
}
iterations += 1;
}

break 'outer read_len;
Expand Down
28 changes: 19 additions & 9 deletions src/watcher/KEventWatcher.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
use bun_core::output as Output;
use bun_sys::Fd;

use crate::watcher_impl::{Op, WatchEvent, Watcher};
use crate::watcher_impl::{
MAX_COALESCE_ITERATIONS, Op, WatchEvent, Watcher, coalesce_interval_ns, coalesce_timespec,
};

pub(crate) type Platform = KEventWatcher;

pub struct KEventWatcher {
pub(crate) fd: Fd,
/// See [`coalesce_interval_ns`].
pub(crate) coalesce_interval: u64,
}

const CHANGELIST_COUNT: usize = 128;
Expand All @@ -17,7 +21,10 @@ impl KEventWatcher {
if fd.native() == 0 {
return Err(crate::Error::KQueueError);
}
Ok(Self { fd })
Ok(Self {
fd,
coalesce_interval: coalesce_interval_ns(),
})
}

pub(crate) fn stop(&mut self) {
Expand Down Expand Up @@ -58,13 +65,16 @@ pub(crate) fn watch_loop_cycle(this: &mut Watcher) -> bun_sys::Result<()> {

let mut count = bun_sys::kevent(fd, &[], &mut changelist, None)?;

// Give the events more time to coalesce
if count < CHANGELIST_COUNT / 2 {
let ts = libc::timespec {
tv_sec: 0,
tv_nsec: 100_000,
}; // 0.0001 seconds
count += bun_sys::kevent(fd, &[], &mut changelist[count..], Some(&ts))?;
// Drain until quiet.
let ts = coalesce_timespec(this.platform.coalesce_interval);
let mut iterations: u32 = 0;
while count > 0 && count < CHANGELIST_COUNT && iterations < MAX_COALESCE_ITERATIONS {
// Don't let a failed drain poll discard the events already read.
match bun_sys::kevent(fd, &[], &mut changelist[count..], Some(&ts)) {
Ok(0) | Err(_) => break,
Ok(extra) => count += extra,
}
iterations += 1;
}

let changes = &changelist[..count];
Expand Down
22 changes: 21 additions & 1 deletion src/watcher/Watcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use core::fmt;
use std::borrow::Cow;

use bun_collections::MultiArrayList;
use bun_core::{ThreadLock, ZStr, feature_flags, output as Output, strings, zstr};
use bun_core::{ThreadLock, ZStr, env_var, feature_flags, output as Output, strings, zstr};
use bun_sys::{self as sys, Fd};
use bun_threading::Mutex;

Expand All @@ -28,6 +28,26 @@ bun_core::define_scoped_log!(log, watcher, visible);

pub const MAX_COUNT: usize = 128;

/// Quiet window (ns) that folds one editor save's several events into one dispatch (#13511).
pub(crate) fn coalesce_interval_ns() -> u64 {
env_var::BUN_INOTIFY_COALESCE_INTERVAL
.get()
.expect("BUN_INOTIFY_COALESCE_INTERVAL declares a default")
}

/// Caps a drain (kqueue wakes once per event) so a continuously written file can't starve the loop.
pub(crate) const MAX_COALESCE_ITERATIONS: u32 = 32;

/// `ns` split into a `timespec`; `tv_nsec` must stay below one second.
#[cfg(not(windows))]
pub(crate) fn coalesce_timespec(ns: u64) -> libc::timespec {
const NS_PER_S: u64 = 1_000_000_000;
libc::timespec {
tv_sec: (ns / NS_PER_S) as _,
tv_nsec: (ns % NS_PER_S) as _,
}
}

#[cfg(any(target_os = "macos", target_os = "freebsd"))]
pub const REQUIRES_FILE_DESCRIPTORS: bool = true;
#[cfg(not(any(target_os = "macos", target_os = "freebsd")))]
Expand Down
40 changes: 21 additions & 19 deletions src/watcher/WindowsWatcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@
use core::mem::size_of;
use core::ptr;

use crate::watcher_impl::{Op, WatchEvent, WatchItemColumns, WatchItemIndex, Watcher};
use crate::watcher_impl::{
MAX_COALESCE_ITERATIONS, Op, WatchEvent, WatchItemColumns, WatchItemIndex, Watcher,
coalesce_interval_ns,
};
use bun_core::strings;
use bun_paths::resolve_path::{ParentEqual, is_parent_or_equal};
use bun_paths::{PathBuffer, WPathBuffer};
Expand All @@ -22,6 +25,8 @@ pub struct WindowsWatcher {
pub(crate) watcher: DirWatcher,
pub(crate) buf: PathBuffer,
pub(crate) base_idx: usize,
/// [`coalesce_interval_ns`] in the milliseconds `GetQueuedCompletionStatus` takes.
pub(crate) coalesce_interval_ms: w::DWORD,
}

impl Default for WindowsWatcher {
Expand All @@ -35,6 +40,7 @@ impl Default for WindowsWatcher {
},
buf: PathBuffer::uninit(),
base_idx: 0,
coalesce_interval_ms: 0,
}
}
}
Expand Down Expand Up @@ -293,14 +299,18 @@ impl WindowsWatcher {
root.len()
};

// div_ceil so a sub-millisecond override still waits instead of becoming 0.
self.coalesce_interval_ms =
w::DWORD::try_from(coalesce_interval_ns().div_ceil(1_000_000)).unwrap_or(w::INFINITE);

// disarm the cleanup scopeguards on success
scopeguard::ScopeGuard::into_inner(iocp_guard);
scopeguard::ScopeGuard::into_inner(handle_guard);
Ok(())
}

/// wait until new events are available
fn next(&mut self, timeout: Timeout) -> bun_sys::Result<Option<EventIterator>> {
/// Waits up to `timeout_ms` (a `GetQueuedCompletionStatus` timeout) for events.
fn next(&mut self, timeout_ms: w::DWORD) -> bun_sys::Result<Option<EventIterator>> {
if let Err(err) = self.watcher.prepare() {
bun_core::scoped_log!(watcher, "prepare() returned error");
return Err(err);
Expand All @@ -317,7 +327,7 @@ impl WindowsWatcher {
&mut nbytes,
&mut key,
&mut overlapped,
timeout as w::DWORD,
timeout_ms,
)
};
if rc == 0 {
Expand Down Expand Up @@ -389,31 +399,23 @@ impl WindowsWatcher {
}
}

#[repr(u32)]
#[derive(Copy, Clone, Eq, PartialEq)]
pub(crate) enum Timeout {
Infinite = w::INFINITE,
None = 0,
}

pub(crate) fn watch_loop_cycle(this: &mut Watcher) -> bun_sys::Result<()> {
// We re-borrow buf inside the inner loop instead of holding `&this.platform.buf`
// across calls to `this.platform.next()`.
let base_idx = this.platform.base_idx;

let mut event_id: usize = 0;

// first wait has infinite timeout - we're waiting for the next event and don't want to spin
let mut timeout = Timeout::Infinite;
loop {
let mut iter = match this.platform.next(timeout)? {
// `<=`: the blocking INFINITE wait is iteration zero; the coalesce sweeps are the rest.
let mut timeout_ms: w::DWORD = w::INFINITE;
let mut iterations: u32 = 0;
while iterations <= MAX_COALESCE_ITERATIONS {
let mut iter = match this.platform.next(timeout_ms)? {
Some(it) => it,
None => break,
};
// after the first wait, we want to coalesce further events but don't want to wait for them
// NOTE: using a 1ms timeout would be ideal, but that actually makes the thread wait for at least 10ms more than it should
// Instead we use a 0ms timeout, which may not do as much coalescing but is more responsive.
timeout = Timeout::None;
timeout_ms = this.platform.coalesce_interval_ms;
iterations += 1;
bun_core::scoped_log!(
watcher,
"number of watched items: {}",
Expand Down
Loading
Loading