diff --git a/packages/bun-usockets/src/eventing/epoll_kqueue.c b/packages/bun-usockets/src/eventing/epoll_kqueue.c index 7e347a378d5b..1f29cadbac8a 100644 --- a/packages/bun-usockets/src/eventing/epoll_kqueue.c +++ b/packages/bun-usockets/src/eventing/epoll_kqueue.c @@ -101,18 +101,6 @@ void us_internal_poll_set_type(struct us_poll_t *p, int poll_type) { p->state.poll_type = poll_type | (p->state.poll_type & POLL_TYPE_POLLING_MASK); } -/* Timer */ -void *us_timer_ext(struct us_timer_t *timer) { - return ((struct us_internal_callback_t *) timer) + 1; -} - -struct us_loop_t *us_timer_loop(struct us_timer_t *t) { - struct us_internal_callback_t *internal_cb = (struct us_internal_callback_t *) t; - - return internal_cb->loop; -} - - #if defined(LIBUS_USE_EPOLL) #include @@ -243,9 +231,9 @@ static void us_internal_dispatch_ready_polls(struct us_loop_t *loop) { const uint16_t flags = loop->ready_polls[i].flags; struct kevent_flags bits = { #if defined(__APPLE__) - .readable = (filter == EVFILT_READ || filter == EVFILT_TIMER || filter == EVFILT_MACHPORT), + .readable = (filter == EVFILT_READ || filter == EVFILT_MACHPORT), #else - .readable = (filter == EVFILT_READ || filter == EVFILT_TIMER || filter == EVFILT_USER), + .readable = (filter == EVFILT_READ || filter == EVFILT_USER), #endif .writable = (filter == EVFILT_WRITE), .error = !!(flags & EV_ERROR), @@ -320,26 +308,45 @@ static void us_internal_drain_ready_polls(struct us_loop_t *loop) { } } -void us_loop_run(struct us_loop_t *loop) { - us_loop_integrate(loop); +/* Bound `timeout` by the socket-timeout sweep deadline (NULL == forever). */ +static const struct timespec *us_internal_clamp_to_sweep(struct us_loop_t *loop, const struct timespec *timeout, struct timespec *storage) { + long long ns = us_internal_sweep_timeout_ns(loop); + if (ns < 0) { + return timeout; + } + long long sweep_sec = ns / 1000000000LL; + long long sweep_nsec = ns % 1000000000LL; + if (timeout && (timeout->tv_sec < sweep_sec || + (timeout->tv_sec == sweep_sec && timeout->tv_nsec <= sweep_nsec))) { + return timeout; + } + storage->tv_sec = (time_t) sweep_sec; + storage->tv_nsec = (long) sweep_nsec; + return storage; +} +void us_loop_run(struct us_loop_t *loop) { /* While we have non-fallthrough polls we shouldn't fall through */ while (loop->num_polls) { loop->data.tick_depth++; /* Emit pre callback */ us_internal_loop_pre(loop); + struct timespec sweep_ts; + const struct timespec *timeout = us_internal_clamp_to_sweep(loop, NULL, &sweep_ts); + /* Fetch ready polls */ #ifdef LIBUS_USE_EPOLL - loop->num_ready_polls = bun_epoll_pwait2(loop->fd, loop->ready_polls, LIBUS_MAX_READY_POLLS, NULL); + loop->num_ready_polls = bun_epoll_pwait2(loop->fd, loop->ready_polls, LIBUS_MAX_READY_POLLS, timeout); #else do { - loop->num_ready_polls = kevent64(loop->fd, NULL, 0, loop->ready_polls, LIBUS_MAX_READY_POLLS, 0, NULL); + loop->num_ready_polls = kevent64(loop->fd, NULL, 0, loop->ready_polls, LIBUS_MAX_READY_POLLS, 0, timeout); } while (IS_EINTR(loop->num_ready_polls)); #endif us_internal_dispatch_ready_polls(loop); us_internal_drain_ready_polls(loop); + us_internal_sweep_if_due(loop); /* Emit post callback */ us_internal_loop_post(loop); @@ -355,14 +362,6 @@ void us_loop_run_bun_tick(struct us_loop_t *loop, const struct timespec* timeout loop->data.tick_depth++; - struct us_internal_callback_t *timer_callback = (struct us_internal_callback_t*)loop->data.sweep_timer; - - // Only integrate the loop if we haven't already. - // Otherwise we will keep restarting the timer. - if(!timer_callback->cb) { - us_loop_integrate(loop); - } - /* Emit pre callback */ us_internal_loop_pre(loop); @@ -381,6 +380,9 @@ void us_loop_run_bun_tick(struct us_loop_t *loop, const struct timespec* timeout } } + struct timespec sweep_ts; + timeout = us_internal_clamp_to_sweep(loop, timeout, &sweep_ts); + const unsigned int had_wakeups = __atomic_exchange_n(&loop->pending_wakeups, 0, __ATOMIC_ACQUIRE); const int will_idle_inside_event_loop = had_wakeups == 0 && (!timeout || (timeout->tv_nsec != 0 || timeout->tv_sec != 0)); if (will_idle_inside_event_loop && loop->data.jsc_vm) @@ -408,6 +410,7 @@ void us_loop_run_bun_tick(struct us_loop_t *loop, const struct timespec* timeout us_internal_dispatch_ready_polls(loop); us_internal_drain_ready_polls(loop); + us_internal_sweep_if_due(loop); /* Emit post callback */ us_internal_loop_post(loop); @@ -595,123 +598,11 @@ size_t us_internal_accept_poll_event(struct us_poll_t *p) { } while (IS_EINTR(read_length)); return buf; #else - /* Kqueue has no underlying FD for timers or user events */ + /* Kqueue has no underlying FD for user events */ return 0; #endif } -/* Timer */ -#ifdef LIBUS_USE_EPOLL -struct us_timer_t *us_create_timer(struct us_loop_t *loop, int fallthrough, unsigned int ext_size) { - struct us_poll_t *p = us_create_poll(loop, fallthrough, sizeof(struct us_internal_callback_t) + ext_size); - memset(p, 0, sizeof(struct us_internal_callback_t) + ext_size); - int timerfd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK | TFD_CLOEXEC); - if (timerfd == -1) { - return NULL; - } - us_poll_init(p, timerfd, POLL_TYPE_CALLBACK); - - struct us_internal_callback_t *cb = (struct us_internal_callback_t *) p; - cb->loop = loop; - cb->cb_expects_the_loop = 0; - cb->leave_poll_ready = 0; - cb->has_added_timer_to_event_loop = 0; - - return (struct us_timer_t *) cb; -} -#else -struct us_timer_t *us_create_timer(struct us_loop_t *loop, int fallthrough, unsigned int ext_size) { - struct us_internal_callback_t *cb = us_calloc(1, sizeof(struct us_internal_callback_t) + ext_size); - - cb->loop = loop; - cb->cb_expects_the_loop = 0; - cb->leave_poll_ready = 0; - - /* Bug: us_internal_poll_set_type does not SET the type, it only CHANGES it */ - cb->p.state.poll_type = POLL_TYPE_POLLING_IN; - us_internal_poll_set_type((struct us_poll_t *) cb, POLL_TYPE_CALLBACK); - - if (!fallthrough) { - loop->num_polls++; - } - - return (struct us_timer_t *) cb; -} -#endif - -#ifdef LIBUS_USE_EPOLL -void us_timer_close(struct us_timer_t *timer, int fallthrough) { - struct us_internal_callback_t *cb = (struct us_internal_callback_t *) timer; - - us_poll_stop(&cb->p, cb->loop); - close(us_poll_fd(&cb->p)); - - /* (regular) sockets are the only polls which are not freed immediately */ - if(fallthrough){ - us_free(timer); - }else { - us_poll_free((struct us_poll_t *) timer, cb->loop); - } -} - -void us_timer_set(struct us_timer_t *t, void (*cb)(struct us_timer_t *t), int ms, int repeat_ms) { - struct us_internal_callback_t *internal_cb = (struct us_internal_callback_t *) t; - - internal_cb->cb = (void (*)(struct us_internal_callback_t *)) cb; - - struct itimerspec timer_spec = { - {repeat_ms / 1000, (long) (repeat_ms % 1000) * (long) 1000000}, - {ms / 1000, (long) (ms % 1000) * (long) 1000000} - }; - - timerfd_settime(us_poll_fd((struct us_poll_t *) t), 0, &timer_spec, NULL); - - // Avoid the system call overhead of re-adding this timer to the event loop only to receive EEXIST - if (internal_cb->loop->data.sweep_timer == t) { - if (internal_cb->has_added_timer_to_event_loop) { - return; - } - internal_cb->has_added_timer_to_event_loop = 1; - } - us_poll_start((struct us_poll_t *) t, internal_cb->loop, LIBUS_SOCKET_READABLE); -} -#else -void us_timer_close(struct us_timer_t *timer, int fallthrough) { - struct us_internal_callback_t *internal_cb = (struct us_internal_callback_t *) timer; - - struct kevent64_s event; - EV_SET64(&event, (uint64_t) (void*) internal_cb, EVFILT_TIMER, EV_DELETE, 0, 0, (uint64_t)internal_cb, 0, 0); - int ret; - do { - ret = kevent64(internal_cb->loop->fd, &event, 1, &event, 1, KEVENT_FLAG_ERROR_EVENTS, NULL); - } while (IS_EINTR(ret)); - - - /* (regular) sockets are the only polls which are not freed immediately */ - if(fallthrough){ - us_free(timer); - }else { - us_poll_free((struct us_poll_t *) timer, internal_cb->loop); - } -} - -void us_timer_set(struct us_timer_t *t, void (*cb)(struct us_timer_t *t), int ms, int repeat_ms) { - struct us_internal_callback_t *internal_cb = (struct us_internal_callback_t *) t; - - internal_cb->cb = (void (*)(struct us_internal_callback_t *)) cb; - - /* Bug: repeat_ms must be the same as ms, or 0 */ - struct kevent64_s event; - uint64_t ptr = (uint64_t)(void*)internal_cb; - EV_SET64(&event, ptr, EVFILT_TIMER, EV_ADD | (repeat_ms ? 0 : EV_ONESHOT), 0, ms, (uint64_t)internal_cb, 0, 0); - - int ret; - do { - ret = kevent64(internal_cb->loop->fd, &event, 1, &event, 1, KEVENT_FLAG_ERROR_EVENTS, NULL); - } while (IS_EINTR(ret)); -} -#endif - /* Async (internal helper for loop's wakeup feature) */ #ifdef LIBUS_USE_EPOLL struct us_internal_async *us_internal_create_async(struct us_loop_t *loop, int fallthrough, unsigned int ext_size) { diff --git a/packages/bun-usockets/src/internal/eventing/epoll_kqueue.h b/packages/bun-usockets/src/internal/eventing/epoll_kqueue.h index f7ed8e835fee..daad8a330e96 100644 --- a/packages/bun-usockets/src/internal/eventing/epoll_kqueue.h +++ b/packages/bun-usockets/src/internal/eventing/epoll_kqueue.h @@ -22,7 +22,6 @@ #ifdef LIBUS_USE_EPOLL #include -#include #include #define LIBUS_SOCKET_READABLE EPOLLIN #define LIBUS_SOCKET_WRITABLE EPOLLOUT diff --git a/packages/bun-usockets/src/internal/internal.h b/packages/bun-usockets/src/internal/internal.h index a6bace29201c..ad52a99f6f37 100644 --- a/packages/bun-usockets/src/internal/internal.h +++ b/packages/bun-usockets/src/internal/internal.h @@ -149,6 +149,10 @@ void us_internal_dispatch_ready_poll(struct us_poll_t *p, int error, int eof, in void us_internal_timer_sweep(us_loop_r loop); void us_internal_enable_sweep_timer(struct us_loop_t *loop); void us_internal_disable_sweep_timer(struct us_loop_t *loop); +#ifndef LIBUS_USE_LIBUV +long long us_internal_sweep_timeout_ns(struct us_loop_t *loop); +void us_internal_sweep_if_due(struct us_loop_t *loop); +#endif void us_internal_free_closed_sockets(us_loop_r loop); void us_internal_loop_link_group(struct us_loop_t *loop, struct us_socket_group_t *group); void us_internal_loop_unlink_group(struct us_loop_t *loop, struct us_socket_group_t *group); @@ -373,7 +377,9 @@ struct us_internal_callback_t { int cb_expects_the_loop; int leave_poll_ready; void (*cb)(struct us_internal_callback_t *cb); +#ifdef LIBUS_USE_LIBUV unsigned has_added_timer_to_event_loop; +#endif }; #endif diff --git a/packages/bun-usockets/src/internal/loop_data.h b/packages/bun-usockets/src/internal/loop_data.h index 4294a6636be4..8e369f11df0b 100644 --- a/packages/bun-usockets/src/internal/loop_data.h +++ b/packages/bun-usockets/src/internal/loop_data.h @@ -36,7 +36,13 @@ typedef void* zig_mutex_t; struct us_quic_socket_context_s; struct us_internal_loop_data_t { +#ifdef LIBUS_USE_LIBUV struct us_timer_t *sweep_timer; +#else + /* Absolute monotonic ns of the next sweep, or -1. Folded into the poll + * timeout — no timerfd, no EVFILT_TIMER. */ + long long sweep_next_tick_ns; +#endif int sweep_timer_count; struct us_internal_async *wakeup_async; struct us_socket_group_t *head; @@ -52,11 +58,12 @@ struct us_internal_loop_data_t { * the gap between loop_post and getTimeout is sub-µs so storing the * relative diff is precise enough. */ long long quic_next_tick_us; - /* libuv only: a fallthrough us_timer_t armed to quic_next_tick_us so the - * uv loop wakes for lsquic's time-driven state. POSIX folds the deadline - * into the epoll_pwait2 timeout via getTimeout() instead, so this stays - * NULL there. */ +#ifdef LIBUS_USE_LIBUV + /* A fallthrough us_timer_t armed to quic_next_tick_us so the uv loop wakes + * for lsquic's time-driven state. POSIX folds the deadline into the + * epoll_pwait2 timeout via getTimeout() instead. */ struct us_timer_t *quic_timer; +#endif struct us_socket_group_t *iterator; char *recv_buf; char *send_buf; diff --git a/packages/bun-usockets/src/libusockets.h b/packages/bun-usockets/src/libusockets.h index c1ccae19acfe..89c208b7f747 100644 --- a/packages/bun-usockets/src/libusockets.h +++ b/packages/bun-usockets/src/libusockets.h @@ -203,7 +203,9 @@ void *us_udp_socket_user(struct us_udp_socket_t *s); /* Binds the UDP socket to an interface and port */ int us_udp_socket_bind(struct us_udp_socket_t *s, const char *hostname, unsigned int port); -/* Public interfaces for timers */ +/* Public interfaces for timers. libuv (Windows) only — epoll/kqueue schedules + * on bun.JSC.EventLoopTimer, no file descriptor or syscall. */ +#ifdef _WIN32 /* Create a new high precision, low performance timer. May fail and return null */ struct us_timer_t *us_create_timer(us_loop_r loop, int fallthrough, unsigned int ext_size); @@ -221,6 +223,8 @@ void us_timer_set(struct us_timer_t *timer, void (*cb)(struct us_timer_t *t), in /* Returns the loop for this timer */ struct us_loop_t *us_timer_loop(struct us_timer_t *t); +#endif + /* ────────────────────────────────────────────────────────────────────────── * Socket groups & dispatch * diff --git a/packages/bun-usockets/src/loop.c b/packages/bun-usockets/src/loop.c index 967494184b24..b216a5633578 100644 --- a/packages/bun-usockets/src/loop.c +++ b/packages/bun-usockets/src/loop.c @@ -21,6 +21,7 @@ #include #include #include +#include #ifndef WIN32 #include #endif @@ -45,6 +46,8 @@ extern const size_t Bun__lock__size; extern void Bun__internal_ensureDateHeaderTimerIsEnabled(struct us_loop_t *loop); +#ifdef LIBUS_USE_LIBUV + void sweep_timer_cb(struct us_internal_callback_t *cb); // when the sweep timer is disabled, we don't need to do anything @@ -65,11 +68,63 @@ void us_internal_disable_sweep_timer(struct us_loop_t *loop) { } } -/* The loop has 2 fallthrough polls */ +#else + +#define LIBUS_TIMEOUT_GRANULARITY_NS ((long long) LIBUS_TIMEOUT_GRANULARITY * 1000000000LL) + +static long long us_internal_monotonic_ns(void) { + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (long long) ts.tv_sec * 1000000000LL + (long long) ts.tv_nsec; +} + +void us_internal_enable_sweep_timer(struct us_loop_t *loop) { + loop->data.sweep_timer_count++; + if (loop->data.sweep_timer_count == 1) { + loop->data.sweep_next_tick_ns = us_internal_monotonic_ns() + LIBUS_TIMEOUT_GRANULARITY_NS; + Bun__internal_ensureDateHeaderTimerIsEnabled(loop); + } +} + +void us_internal_disable_sweep_timer(struct us_loop_t *loop) { + loop->data.sweep_timer_count--; + if (loop->data.sweep_timer_count == 0) { + loop->data.sweep_next_tick_ns = -1; + } +} + +long long us_internal_sweep_timeout_ns(struct us_loop_t *loop) { + if (loop->data.sweep_next_tick_ns < 0) { + return -1; + } + long long diff = loop->data.sweep_next_tick_ns - us_internal_monotonic_ns(); + return diff > 0 ? diff : 0; +} + +void us_internal_sweep_if_due(struct us_loop_t *loop) { + if (loop->data.sweep_next_tick_ns < 0) { + return; + } + long long now = us_internal_monotonic_ns(); + if (now < loop->data.sweep_next_tick_ns) { + return; + } + /* Re-arm first: a timeout handler may unlink the last socket and disarm. */ + loop->data.sweep_next_tick_ns = now + LIBUS_TIMEOUT_GRANULARITY_NS; + us_internal_timer_sweep(loop); +} + +#endif + + void us_internal_loop_data_init(struct us_loop_t *loop, void (*wakeup_cb)(struct us_loop_t *loop), void (*pre_cb)(struct us_loop_t *loop), void (*post_cb)(struct us_loop_t *loop)) { // We allocate with calloc, so we only need to initialize the specific fields in use. +#ifdef LIBUS_USE_LIBUV loop->data.sweep_timer = us_create_timer(loop, 1, 0); +#else + loop->data.sweep_next_tick_ns = -1; +#endif loop->data.sweep_timer_count = 0; loop->data.recv_buf = malloc(LIBUS_RECV_BUFFER_LENGTH + LIBUS_RECV_BUFFER_PADDING * 2); loop->data.send_buf = malloc(LIBUS_SEND_BUFFER_LENGTH); @@ -95,8 +150,10 @@ void us_internal_loop_data_free(struct us_loop_t *loop) { free(loop->data.recv_buf); free(loop->data.send_buf); +#ifdef LIBUS_USE_LIBUV us_timer_close(loop->data.sweep_timer, 0); if (loop->data.quic_timer) us_timer_close(loop->data.quic_timer, 0); +#endif us_internal_async_close(loop->data.wakeup_async); } @@ -325,9 +382,11 @@ void us_internal_free_closed_sockets(struct us_loop_t *loop) { loop->data.closed_connecting_head = NULL; } +#ifdef LIBUS_USE_LIBUV void sweep_timer_cb(struct us_internal_callback_t *cb) { us_internal_timer_sweep(cb->loop); } +#endif __attribute__((always_inline)) long long us_loop_iteration_number(struct us_loop_t *loop) { return loop->data.iteration_nr; diff --git a/src/event_loop/EventLoopTimer.rs b/src/event_loop/EventLoopTimer.rs index c2397060c68b..2c084578d660 100644 --- a/src/event_loop/EventLoopTimer.rs +++ b/src/event_loop/EventLoopTimer.rs @@ -203,6 +203,8 @@ pub enum Tag { BunTest, EventLoopDelayMonitor, CronJob, + GcOneShot, + GcRepeating, } impl Tag { @@ -212,6 +214,7 @@ impl Tag { | Tag::BunTest // for test timeouts | Tag::EventLoopDelayMonitor // probably important | Tag::StatWatcherScheduler + | Tag::GcOneShot | Tag::GcRepeating // internal GC pacing => false, _ => true, } diff --git a/src/jsc/GarbageCollectionController.rs b/src/jsc/GarbageCollectionController.rs index dce4f5f3d100..931d559fc9d6 100644 --- a/src/jsc/GarbageCollectionController.rs +++ b/src/jsc/GarbageCollectionController.rs @@ -20,36 +20,42 @@ use core::ffi::c_int; +use bun_core::{Timespec, TimespecMockMode}; +use bun_event_loop::EventLoopTimer::{EventLoopTimer, State as TimerState, Tag as TimerTag}; use bun_uws as uws; use crate::VM; use crate::virtual_machine::VirtualMachine; +const SLOW_REPEAT_INTERVAL_MS: i32 = 30_000; + pub struct GarbageCollectionController { - // Raw FFI handle created by `uws::Timer::create_fallthrough` in `init`, - // freed in Drop. Stored as `Option>` (None = uninit). - pub gc_timer: Option>, + pub gc_timer: EventLoopTimer, + pub gc_repeating_timer: EventLoopTimer, pub gc_last_heap_size: usize, pub gc_last_heap_size_on_repeating_timer: usize, pub heap_size_didnt_change_for_repeating_timer_ticks_count: u8, pub gc_timer_state: GCTimerState, - // Raw FFI handle created by `uws::Timer::create_fallthrough` in `init`, - // freed in Drop. - pub gc_repeating_timer: Option>, pub gc_timer_interval: i32, pub gc_repeating_timer_fast: bool, pub disabled: bool, } +bun_event_loop::impl_timer_owner!( + GarbageCollectionController; + from_gc_timer_ptr => gc_timer, + from_gc_repeating_timer_ptr => gc_repeating_timer, +); + impl Default for GarbageCollectionController { fn default() -> Self { Self { - gc_timer: None, + gc_timer: EventLoopTimer::init_paused(TimerTag::GcOneShot), + gc_repeating_timer: EventLoopTimer::init_paused(TimerTag::GcRepeating), gc_last_heap_size: 0, gc_last_heap_size_on_repeating_timer: 0, heap_size_didnt_change_for_repeating_timer_ticks_count: 0, gc_timer_state: GCTimerState::Pending, - gc_repeating_timer: None, gc_timer_interval: 0, gc_repeating_timer_fast: true, disabled: false, @@ -64,69 +70,35 @@ pub enum GcRepeatSetting { } impl GarbageCollectionController { - /// Recover `&mut Self` from a uws timer's ext slot. Single audited deref - /// for the two `extern "C"` callbacks below so they stay safe-bodied. - /// - /// `timer` is the live uws timer whose ext data was set to - /// `*mut GarbageCollectionController` in [`Self::init`]; the controller is - /// a BACKREF that strictly outlives the timer (`deinit()` closes the timer - /// before `self` is dropped). `Timer` is an `opaque_ffi!` ZST handle, so - /// [`uws::Timer::opaque_mut`] is the centralised non-null deref proof for - /// the handle itself; only the recovered `*mut Self` needs the audited - /// deref below. - #[inline] - fn from_timer_ext<'a>(timer: *mut uws::Timer) -> &'a mut Self { - let ptr = uws::Timer::opaque_mut(timer).as_::<*mut Self>(); - // SAFETY: BACKREF — see doc comment above. - unsafe { &mut *ptr } - } - - /// Accessor for the init-once `gc_timer` handle. Consolidates the four - /// open-coded `(*self..unwrap().as_ptr())` deref sites into one - /// SAFETY block so call sites are safe. - #[inline] - fn gc_timer_mut(&mut self) -> &mut uws::Timer { - // SAFETY: `gc_timer` is set in `init()` (via `Timer::create_fallthrough`) - // before any code path reaches a deref site, and remains a live FFI - // handle until `deinit()` closes it. The Timer lives on the uws heap, - // not inside `self`, so the returned `&mut` cannot alias `self`. - unsafe { &mut *self.gc_timer.expect("gc_timer set in init()").as_ptr() } + /// Remove `t` from the heap if linked, set its deadline to `now + ms`, and + /// insert. JS-thread only. Real time, not the mocked clock: GC pacing is + /// Bun's, not the test's. + fn arm(vm: *mut VirtualMachine, t: *mut EventLoopTimer, ms: i32) { + // SAFETY: `t` is one of the two embedded nodes of the per-VM controller, + // address-stable for the VM lifetime; JS-thread only. + unsafe { + if (*t).state == TimerState::ACTIVE { + VirtualMachine::timer_remove(vm, t); + } + (*t).next = Timespec::now(TimespecMockMode::ForceRealTime).add_ms(i64::from(ms)); + VirtualMachine::timer_insert(vm, t); + } } - /// Accessor for the init-once `gc_repeating_timer` handle (see - /// [`gc_timer_mut`] for the invariant). #[inline] - fn gc_repeating_timer_mut(&mut self) -> &mut uws::Timer { - // SAFETY: same invariant as `gc_timer_mut` — set in `init()`, live - // until `deinit()`, FFI-heap-owned. - unsafe { - &mut *self - .gc_repeating_timer - .expect("gc_repeating_timer set in init()") - .as_ptr() + fn repeat_interval(&self) -> i32 { + if self.gc_repeating_timer_fast { + self.gc_timer_interval + } else { + SLOW_REPEAT_INTERVAL_MS } } pub fn init(&mut self, vm: &mut VirtualMachine) { // SAFETY: uws::Loop::get() returns the live process-global loop. let actual = unsafe { &mut *uws::Loop::get() }; - self.gc_timer = Some(uws::Timer::create_fallthrough( - actual, - std::ptr::from_mut::(self), - )); - self.gc_repeating_timer = Some(uws::Timer::create_fallthrough( - actual, - std::ptr::from_mut::(self), - )); actual.internal_loop_data.jsc_vm = vm.jsc_vm.cast(); - // `Transpiler::init` is deferred to the high-tier - // `init_runtime_state` hook (which runs *after* `ensure_waker` → - // this `init`), so `vm.transpiler.env` is still the zeroed null ptr - // here on the main boot path. Fall back to defaults when null — these are debug/tuning - // knobs (BUN_GC_TIMER_INTERVAL / BUN_GC_TIMER_DISABLE / - // BUN_GC_RUNS_UNTIL_SKIP_RELEASE_ACCESS) and the dot_env loader would - // just be reading process env anyway. let env = vm.env_loader_opt(); let mut gc_timer_interval: i32 = 1000; @@ -149,45 +121,31 @@ impl GarbageCollectionController { } self.disabled = env.is_some_and(|e| e.has(b"BUN_GC_TIMER_DISABLE")); - - if !self.disabled { - let ext = std::ptr::from_mut::(self); - self.gc_repeating_timer_mut().set( - ext, - Some(on_gc_repeating_timer), - gc_timer_interval, - gc_timer_interval, - ); - } } pub fn schedule_gc_timer(&mut self) { self.gc_timer_state = GCTimerState::Scheduled; - let ext = std::ptr::from_mut::(self); - self.gc_timer_mut().set(ext, Some(on_gc_timer), 16, 0); + Self::arm(VirtualMachine::get_mut_ptr(), &raw mut self.gc_timer, 16); } pub fn bun_vm(&mut self) -> &mut VirtualMachine { - // S017: dropped `container_of` recovery — provenance of `&mut self` - // (which only covers `vm.gc_controller`) cannot soundly widen to the - // whole `VirtualMachine` under Stacked Borrows. Route through the - // per-thread singleton instead (same pointer, full-allocation - // provenance via `VirtualMachine::get_mut_ptr`). VirtualMachine::get().as_mut() } - /// Explicit teardown. Idempotent — `Drop` forwards here. - /// Kept as an inherent method because callers (web_worker, VM exit path) - /// need to release the uws timers before the owning VM storage is freed. + /// Idempotent. Must run before JSC teardown: `~RunLoop::Timer` frees the + /// `WTFTimer` nodes sharing the heap, so an unlink afterwards walks freed + /// siblings. pub fn deinit(&mut self) { - // SAFETY: timers were created via uws::Timer::create_fallthrough; close:: - // frees the fallthrough timer. `take()` ensures we close at most once. - unsafe { - if let Some(t) = self.gc_timer.take() { - uws::Timer::close::(t.as_ptr()); - } - if let Some(t) = self.gc_repeating_timer.take() { - uws::Timer::close::(t.as_ptr()); + self.disabled = true; + let Some(vm) = VirtualMachine::get_or_null() else { + return; + }; + for t in [&raw mut self.gc_timer, &raw mut self.gc_repeating_timer] { + // SAFETY: JS-thread; nodes are linked iff state == ACTIVE. + unsafe { + if (*t).state == TimerState::ACTIVE { + VirtualMachine::timer_remove(vm, t); + } } } } @@ -204,19 +162,20 @@ impl GarbageCollectionController { // When the heap size is increasing, we always switch to fast mode // When the heap size has been the same or less for 30 seconds, we switch to slow mode pub fn update_gc_repeat_timer(&mut self, setting: GcRepeatSetting) { - if setting == GcRepeatSetting::Fast && !self.gc_repeating_timer_fast { - self.gc_repeating_timer_fast = true; - let ext = std::ptr::from_mut::(self); - let interval = self.gc_timer_interval; - self.gc_repeating_timer_mut() - .set(ext, Some(on_gc_repeating_timer), interval, interval); - self.heap_size_didnt_change_for_repeating_timer_ticks_count = 0; - } else if setting == GcRepeatSetting::Slow && self.gc_repeating_timer_fast { - self.gc_repeating_timer_fast = false; - let ext = std::ptr::from_mut::(self); - self.gc_repeating_timer_mut() - .set(ext, Some(on_gc_repeating_timer), 30_000, 30_000); - self.heap_size_didnt_change_for_repeating_timer_ticks_count = 0; + let want_fast = match setting { + GcRepeatSetting::Fast if !self.gc_repeating_timer_fast => true, + GcRepeatSetting::Slow if self.gc_repeating_timer_fast => false, + _ => return, + }; + self.gc_repeating_timer_fast = want_fast; + self.heap_size_didnt_change_for_repeating_timer_ticks_count = 0; + if self.gc_repeating_timer.state == TimerState::ACTIVE { + let interval = self.repeat_interval(); + Self::arm( + VirtualMachine::get_mut_ptr(), + &raw mut self.gc_repeating_timer, + interval, + ); } } @@ -225,6 +184,14 @@ impl GarbageCollectionController { if self.disabled { return; } + if self.gc_repeating_timer.state == TimerState::PENDING { + let interval = self.repeat_interval(); + Self::arm( + VirtualMachine::get_mut_ptr(), + &raw mut self.gc_repeating_timer, + interval, + ); + } let vm = VirtualMachine::get().jsc_vm(); self.process_gc_timer_with_heap_size(vm, vm.block_bytes_allocated()); } @@ -272,38 +239,54 @@ impl GarbageCollectionController { vm.collect_async(); self.gc_last_heap_size = vm.block_bytes_allocated(); } -} -impl Drop for GarbageCollectionController { - fn drop(&mut self) { - self.deinit(); + /// `Tag::GcOneShot` fire body. + /// + /// # Safety + /// `this` is the live per-VM controller; JS-thread only. + pub unsafe fn on_gc_timer(this: *mut Self) { + // SAFETY: per fn contract. + let this = unsafe { &mut *this }; + this.gc_timer.state = TimerState::FIRED; + if this.disabled { + return; + } + this.gc_timer_state = GCTimerState::RunOnNextTick; } -} -pub(crate) extern "C" fn on_gc_timer(timer: *mut uws::Timer) { - let this = GarbageCollectionController::from_timer_ext(timer); - if this.disabled { - return; + /// `Tag::GcRepeating` fire body. + /// + /// # Safety + /// `this` is the live per-VM controller; `vm` is the per-thread VM. + pub unsafe fn on_gc_repeating_timer(this: *mut Self, vm: *mut VirtualMachine) { + // SAFETY: per fn contract. + let this = unsafe { &mut *this }; + this.gc_repeating_timer.state = TimerState::FIRED; + if this.disabled { + return; + } + let prev_heap_size = this.gc_last_heap_size_on_repeating_timer; + this.perform_gc(); + this.gc_last_heap_size_on_repeating_timer = this.gc_last_heap_size; + if prev_heap_size == this.gc_last_heap_size_on_repeating_timer { + this.heap_size_didnt_change_for_repeating_timer_ticks_count = this + .heap_size_didnt_change_for_repeating_timer_ticks_count + .saturating_add(1); + if this.heap_size_didnt_change_for_repeating_timer_ticks_count >= 30 { + this.update_gc_repeat_timer(GcRepeatSetting::Slow); + } + } else { + this.heap_size_didnt_change_for_repeating_timer_ticks_count = 0; + this.update_gc_repeat_timer(GcRepeatSetting::Fast); + } + let interval = this.repeat_interval(); + Self::arm(vm, &raw mut this.gc_repeating_timer, interval); } - this.gc_timer_state = GCTimerState::RunOnNextTick; } -pub(crate) extern "C" fn on_gc_repeating_timer(timer: *mut uws::Timer) { - let this = GarbageCollectionController::from_timer_ext(timer); - let prev_heap_size = this.gc_last_heap_size_on_repeating_timer; - this.perform_gc(); - this.gc_last_heap_size_on_repeating_timer = this.gc_last_heap_size; - if prev_heap_size == this.gc_last_heap_size_on_repeating_timer { - this.heap_size_didnt_change_for_repeating_timer_ticks_count = this - .heap_size_didnt_change_for_repeating_timer_ticks_count - .saturating_add(1); - if this.heap_size_didnt_change_for_repeating_timer_ticks_count >= 30 { - // make the timer interval longer - this.update_gc_repeat_timer(GcRepeatSetting::Slow); - } - } else { - this.heap_size_didnt_change_for_repeating_timer_ticks_count = 0; - this.update_gc_repeat_timer(GcRepeatSetting::Fast); +impl Drop for GarbageCollectionController { + fn drop(&mut self) { + self.deinit(); } } diff --git a/src/jsc/VirtualMachine.rs b/src/jsc/VirtualMachine.rs index 5af7f93e15e2..b06bdf1744e4 100644 --- a/src/jsc/VirtualMachine.rs +++ b/src/jsc/VirtualMachine.rs @@ -1517,10 +1517,11 @@ impl VirtualMachine { // self.event_loop().tick(); if self.should_destruct_main_thread_on_exit() { + #[cfg(windows)] if let Some(t) = self.event_loop_mut().forever_timer.take() { // SAFETY: `t` is the live usockets timer created in - // `EventLoop::auto_tick`; `close::()` (fallthrough) - // frees it without re-entering the loop. + // `EventLoop::tick_possibly_forever`; `close::()` + // (fallthrough) frees it without re-entering the loop. unsafe { uws::Timer::close::(t.as_ptr()) }; } // Drain `TimeoutObject`s / `ImmediateObject`s from `All.timers` @@ -1536,6 +1537,8 @@ impl VirtualMachine { // `destroy()`, well after `global_exit`). unsafe { (hooks.cancel_all_timers)(core::ptr::from_mut(self)) }; } + // Same reason: the GC timers are heap nodes too. + self.gc_controller.deinit(); // Detached worker threads may still be in startVM()/spin() using // the process-global resolver BSSMap singletons. transpiler.deinit() // below frees those singletons, so request termination of every @@ -1595,7 +1598,6 @@ impl VirtualMachine { // loop, which is live for the process lifetime. unsafe { (*uws::Loop::get()).drain_closed_sockets() }; - self.gc_controller.deinit(); self.destroy(); } bun_core::Global::exit(u32::from(self.exit_handler.exit_code)) diff --git a/src/jsc/event_loop.rs b/src/jsc/event_loop.rs index 46763240a0e1..5bc9a66c36db 100644 --- a/src/jsc/event_loop.rs +++ b/src/jsc/event_loop.rs @@ -75,8 +75,11 @@ pub struct EventLoop { // BACKREF — owning `*VirtualMachine` (EventLoop is a value field of it). pub virtual_machine: Option>, pub waker: Option, - // `?*uws.Timer` FFI handle. + // see `hold_forever_poll` + #[cfg(windows)] pub forever_timer: Option>, + #[cfg(not(windows))] + pub holds_forever_poll: bool, pub deferred_tasks: DeferredTaskQueue::DeferredTaskQueue, #[cfg(windows)] // `?*uws.Loop` FFI handle. @@ -115,7 +118,10 @@ impl Default for EventLoop { global: None, virtual_machine: None, waker: None, + #[cfg(windows)] forever_timer: None, + #[cfg(not(windows))] + holds_forever_poll: false, deferred_tasks: DeferredTaskQueue::DeferredTaskQueue::default(), #[cfg(windows)] uws_loop: None, @@ -1051,6 +1057,36 @@ impl EventLoop { Ok(result) } + /// Keep one poll registered with the loop so `us_loop_run_bun_tick` parks + /// instead of returning immediately on `num_polls == 0`. + #[cfg(not(windows))] + fn hold_forever_poll(&mut self, loop_: &mut uws::Loop) { + if !self.holds_forever_poll { + loop_.inc(); + self.holds_forever_poll = true; + } + } + + #[cfg(windows)] + fn hold_forever_poll(&mut self, loop_: &mut uws::Loop) { + if self.forever_timer.is_none() { + let mut t = uws::Timer::create( + loop_, + std::ptr::from_mut::(self).cast::(), + ); + // SAFETY: t is a fresh non-null timer handle + unsafe { + t.as_mut().set( + std::ptr::from_mut::(self).cast::(), + Some(noop_forever_timer), + 1000 * 60 * 4, + 1000 * 60 * 4, + ) + }; + self.forever_timer = Some(t); + } + } + pub fn tick_possibly_forever(&mut self) { let loop_ptr = self.usockets_loop(); // SAFETY: usockets_loop() returns a live uws loop for the VM lifetime. @@ -1065,27 +1101,15 @@ impl EventLoop { } if !loop_.is_active() { - if self.forever_timer.is_none() { - let mut t = uws::Timer::create( - loop_, - std::ptr::from_mut::(self).cast::(), - ); - // SAFETY: t is a fresh non-null timer handle - unsafe { - t.as_mut().set( - std::ptr::from_mut::(self).cast::(), - Some(noop_forever_timer), - 1000 * 60 * 4, - 1000 * 60 * 4, - ) - }; - self.forever_timer = Some(t); - } + self.hold_forever_poll(loop_); } self.process_gc_timer(); self.process_gc_timer(); - loop_.tick(); + // `tick()` below can start work (e.g. a --hot reload) whose only wake + // source is a cross-thread `wakeup()`; bound the park, same as the GC + // timerfd used to. libuv's `tick_with_timeout` ignores the argument. + loop_.tick_with_timeout(Some(&bun_core::Timespec { sec: 1, nsec: 0 })); self.vm_ref().as_mut().on_after_event_loop(); self.tick_concurrent(); @@ -1177,6 +1201,7 @@ pub fn get_active_tasks(global_object: &JSGlobalObject, _frame: &CallFrame) -> J Ok(result) } +#[cfg(windows)] extern "C" fn noop_forever_timer(_: *mut uws::Timer) { // do nothing } diff --git a/src/jsc/web_worker.rs b/src/jsc/web_worker.rs index ceb045b8f5e6..ed93376cbaaf 100644 --- a/src/jsc/web_worker.rs +++ b/src/jsc/web_worker.rs @@ -1237,6 +1237,8 @@ impl WebWorker { // worker thread is still installed (torn down in `destroy()`). unsafe { (hooks.cancel_all_timers)(vm_ptr) }; } + // Same reason: the GC timers are heap nodes too. + vm.gc_controller.deinit(); // Embedded socket groups must drain while JSC is still alive — // closeAll() fires on_close → JS callbacks. RareData.deinit() runs // after teardownJSCVM and only deinit()s (asserts empty in debug). @@ -1282,12 +1284,6 @@ impl WebWorker { // SAFETY: loop owned by this thread's VM; no concurrent access. unsafe { (*loop_).internal_loop_data.jsc_vm = core::ptr::null_mut() }; } - if !vm_ptr.is_null() { - // SAFETY: vm_ptr valid; sole owner. - // Must precede Loop.shutdown so uv_close isn't called twice on the - // GC timer. - unsafe { (*vm_ptr).gc_controller.deinit() }; - } #[cfg(windows)] { // Per-thread libuv loop teardown; closes any handles still open on diff --git a/src/runtime/cli/run_command.rs b/src/runtime/cli/run_command.rs index 49b21f9613dc..53bf18368aeb 100644 --- a/src/runtime/cli/run_command.rs +++ b/src/runtime/cli/run_command.rs @@ -1518,14 +1518,12 @@ impl Run { // When --hot/--watch is on (or a user // `uncaughtException` handler swallowed the error), keep the // process alive instead of hard-exiting on a rejected entry. + // The core run-loop below does the actual waiting. if vm.hot_reload != 0 || handled { vm.add_main_to_watcher_if_needed(); // SAFETY: `event_loop` is a self-pointer into this VM; // uniquely accessed here. vm.event_loop_ref().tick(); - // SAFETY: as above — `event_loop` is a self-pointer into - // this VM; uniquely accessed here. - vm.event_loop_ref().tick_possibly_forever(); } else { exit_with_unhandled_note(vm); } diff --git a/src/runtime/dispatch.rs b/src/runtime/dispatch.rs index 9ddfc8c0454b..22049be0b6ab 100644 --- a/src/runtime/dispatch.rs +++ b/src/runtime/dispatch.rs @@ -164,6 +164,7 @@ use bun_sql_jsc::postgres::PostgresSQLConnection; use crate::test_runner::bun_test::{BunTest, BunTestPtr}; use crate::timer::{DateHeaderTimer, EventLoopDelayMonitor}; use bun_jsc::abort_signal::Timeout as AbortSignalTimeout; +use bun_jsc::garbage_collection_controller::GarbageCollectionController; #[cfg(not(windows))] use bun_io::pipe_writer::PosixPipeWriter; // brings `on_poll` into scope for FileSinkPoll/StaticPipeWriterPoll/etc. @@ -975,6 +976,18 @@ pub unsafe fn __bun_fire_timer(t: *mut EventLoopTimer, now: *const ElTimespec, v AbortSignalTimeout::run(c, vm) }) } + EventLoopTimerTag::GcOneShot => { + timer_arm!(GarbageCollectionController, gc_timer, |c, _now, _vm| { + GarbageCollectionController::on_gc_timer(c) + }) + } + EventLoopTimerTag::GcRepeating => { + timer_arm!( + GarbageCollectionController, + gc_repeating_timer, + |c, _now, vm| GarbageCollectionController::on_gc_repeating_timer(c, vm) + ) + } EventLoopTimerTag::DateHeaderTimer => { timer_arm!(DateHeaderTimer, event_loop_timer, |c, _now, vm| (*c) .run(&mut *vm)) diff --git a/src/runtime/timer/mod.rs b/src/runtime/timer/mod.rs index d86f406f3168..9309549ec06f 100644 --- a/src/runtime/timer/mod.rs +++ b/src/runtime/timer/mod.rs @@ -744,7 +744,13 @@ impl All { // https://github.com/nodejs/node/blob/f552c86fecd6c2ba9e832ea129b731dd63abdbe2/src/env.cc#L1512 let wait_ms = core::cmp::max(1, wait.ms_unsigned()); - self.uv_timer.start(wait_ms, 0, Some(Self::on_uv_timer)); + // SAFETY: `uv_timer_init` ran above; the handle is live. + let due_in = unsafe { uv::uv_timer_get_due_in(&self.uv_timer) }; + // Restarting an overdue handle shifts the wakeup out by 1ms. Done + // on every insert, the already-due callback never runs. + if !(self.uv_timer.is_active() && due_in <= wait_ms) { + self.uv_timer.start(wait_ms, 0, Some(Self::on_uv_timer)); + } if self.active_timer_count > 0 { self.uv_timer.ref_(); diff --git a/src/uws/lib.rs b/src/uws/lib.rs index 56b3965368cc..a098654536d1 100644 --- a/src/uws/lib.rs +++ b/src/uws/lib.rs @@ -17,9 +17,11 @@ use bun_core::ZStr; // `SocketAddress`) stay defined here; `bun_uws_sys::socket` has lifetime- // bearing variants of the same names that are not yet reconciled. +#[cfg(windows)] +pub use bun_uws_sys::Timer; pub use bun_uws_sys::{ AnyWebSocket, BodyReaderMixin, ConnectingSocket, ListenSocket, NewApp, RawWebSocket, Request, - Timer, WebSocketBehavior, us_socket_stream_buffer_t, us_socket_t, uws_res, + WebSocketBehavior, us_socket_stream_buffer_t, us_socket_t, uws_res, }; /// `#[uws_callback]` — wraps a `&self`/`&mut self` method in an `extern "C"` diff --git a/src/uws_sys/InternalLoopData.rs b/src/uws_sys/InternalLoopData.rs index 74df8437d991..5a38af507434 100644 --- a/src/uws_sys/InternalLoopData.rs +++ b/src/uws_sys/InternalLoopData.rs @@ -1,6 +1,8 @@ use core::ffi::{c_char, c_int, c_void}; -use crate::{ConnectingSocket, Loop, SocketGroup, Timer, udp, us_socket_t}; +#[cfg(windows)] +use crate::Timer; +use crate::{ConnectingSocket, Loop, SocketGroup, udp, us_socket_t}; /// Layout placeholder for the `mutex` field of `us_internal_loop_data_t`. /// Must match `zig_mutex_t` in `packages/bun-usockets/src/internal/loop_data.h` @@ -22,12 +24,16 @@ bun_opaque::opaque_ffi! { #[repr(C)] pub struct InternalLoopData { + #[cfg(windows)] pub sweep_timer: *mut Timer, + #[cfg(not(windows))] + pub sweep_next_tick_ns: i64, pub sweep_timer_count: i32, pub wakeup_async: *mut us_internal_async, pub head: *mut SocketGroup, pub quic_head: *mut c_void, pub quic_next_tick_us: i64, + #[cfg(windows)] pub quic_timer: *mut Timer, pub iterator: *mut SocketGroup, pub recv_buf: *mut u8, diff --git a/src/uws_sys/Timer.rs b/src/uws_sys/Timer.rs index 0dcfa09ca714..c6c8aed41875 100644 --- a/src/uws_sys/Timer.rs +++ b/src/uws_sys/Timer.rs @@ -6,21 +6,11 @@ use crate::Loop; bun_core::declare_scope!(uws, visible); -// **DEPRECATED** -// **DO NOT USE IN NEW CODE!** -// -// Use `JSC.EventLoopTimer` instead. -// -// This code will be deleted eventually! It is very inefficient on POSIX. On -// Linux, it holds an entire file descriptor for every single timer. On macOS, -// it's several system calls. +// Windows (libuv) only. Use `JSC.EventLoopTimer` everywhere else. bun_opaque::opaque_ffi! { pub struct Timer; } impl Timer { pub fn create(loop_: &mut Loop, _ptr: T) -> NonNull { - // never fallthrough poll - // the problem is uSockets hardcodes it on the other end - // so we can never free non-fallthrough polls // SAFETY: `loop_` is a valid loop pointer. let t = unsafe { us_create_timer( @@ -37,26 +27,6 @@ impl Timer { }) } - pub fn create_fallthrough(loop_: &mut Loop, _ptr: T) -> NonNull { - // never fallthrough poll - // the problem is uSockets hardcodes it on the other end - // so we can never free non-fallthrough polls - // SAFETY: `loop_` is a valid loop pointer. - let t = unsafe { - us_create_timer( - loop_, - 1, - c_uint::try_from(size_of::()).expect("int cast"), - ) - }; - NonNull::new(t).unwrap_or_else(|| { - panic!( - "us_create_timer: returned null: {}", - std::io::Error::last_os_error().raw_os_error().unwrap_or(0) - ) - }) - } - pub fn set( &mut self, ptr: T, @@ -73,44 +43,14 @@ impl Timer { } } - // Not `impl Drop` — FFI opaque handle with a const-generic param; - // destruction is an explicit C call and Drop cannot take parameters. Per PORTING.md - // FFI-handle exception, expose `unsafe fn close(*mut Self)` instead of `deinit(&mut self)`. pub unsafe fn close(this: *mut Self) { bun_core::scoped_log!(uws, "Timer.deinit()"); - // SAFETY: `this` is a live timer handle; us_timer_close frees it (caller must not - // use `this` afterward). + // SAFETY: `this` is a live timer handle; us_timer_close frees it. unsafe { us_timer_close(this, FALLTHROUGH as i32) }; } - - pub fn ext(&mut self) -> Option<&mut T> { - unsafe { - // SAFETY: us_timer_ext returns a pointer to the ext slot (`*?*anyopaque`); - // deref + unwrap, then cast to *mut T. Caller guarantees T matches the - // type used at create()/set(). - let slot: *mut Option> = us_timer_ext(self).cast(); - Some(&mut *(*slot).expect("unreachable").as_ptr().cast::()) - } - } - - // Named `as_` because `as` is a Rust keyword. - pub fn as_(&mut self) -> T { - unsafe { - // SAFETY: the ext slot was allocated with `size_of::()` and - // written via [`set`] as a bare `T`, so read it as `T` directly. - // Wrapping in `Option` here would over-read and misinterpret - // the bytes (`Option<*mut T>` has no niche, so it is two words - // while the slot is one). Callers pass pointer-ish `T` and - // tolerate a (debug-asserted) null read. - let slot: *mut T = us_timer_ext(self).cast(); - slot.read() - } - } } unsafe extern "C" { - // `Loop` is a sized `#[repr(C)]` mirror (not an opaque ZST) — keep raw `*mut` - // so the FFI boundary does not annotate `noalias` over real loop fields. pub(crate) fn us_create_timer( loop_: *mut Loop, fallthrough: i32, @@ -124,5 +64,4 @@ unsafe extern "C" { ms: i32, repeat_ms: i32, ); - pub safe fn us_timer_loop(t: &mut Timer) -> *mut Loop; } diff --git a/src/uws_sys/lib.rs b/src/uws_sys/lib.rs index 164c88481c81..bd16222a618e 100644 --- a/src/uws_sys/lib.rs +++ b/src/uws_sys/lib.rs @@ -379,6 +379,8 @@ pub mod socket_group; pub mod socket_kind; #[path = "thunk.rs"] pub mod thunk; +// libuv only — use `bun_event_loop::EventLoopTimer` elsewhere. +#[cfg(windows)] #[path = "Timer.rs"] pub mod timer; #[path = "udp.rs"] @@ -444,6 +446,7 @@ pub use internal_loop_data::InternalLoopData; pub use loop_::WindowsLoop; pub use loop_::{Loop, PosixLoop}; pub use socket_kind::SocketKind; +#[cfg(windows)] pub use timer::Timer; #[cfg(not(windows))] pub type WindowsLoop = loop_::PosixLoop; // unified on non-Windows