Skip to content
Closed
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
13 changes: 13 additions & 0 deletions packages/bun-usockets/src/eventing/epoll_kqueue.c
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,13 @@ void us_loop_run_bun_tick(struct us_loop_t *loop, const struct timespec* timeout
}
}

/* Only ticks that really park are timed, so a busy loop pays nothing and a
* parked one pays two vDSO reads against a syscall it was making anyway.
* Publish the entry so a cross-thread reader can add the in-progress park. */
const uint64_t idle_start_ns = will_idle_inside_event_loop ? us_internal_monotonic_ns() : 0;
if (will_idle_inside_event_loop)
__atomic_store_n(&loop->data.idle_entry_ns, idle_start_ns, __ATOMIC_RELEASE);

/* Fetch ready polls */
#ifdef LIBUS_USE_EPOLL
/* A zero timespec already has a fast path in ep_poll (fs/eventpoll.c):
Expand All @@ -428,6 +435,12 @@ void us_loop_run_bun_tick(struct us_loop_t *loop, const struct timespec* timeout
} while (IS_EINTR(loop->num_ready_polls));
#endif

if (will_idle_inside_event_loop) {
__atomic_add_fetch(&loop->data.idle_ns, us_internal_monotonic_ns() - idle_start_ns,
__ATOMIC_RELAXED);
__atomic_store_n(&loop->data.idle_entry_ns, 0, __ATOMIC_RELEASE);
}

/* Before anything can allocate again. */
if (handed_off)
mi_on_thread_idle_end();
Expand Down
4 changes: 4 additions & 0 deletions packages/bun-usockets/src/eventing/libuv.c
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,10 @@ struct us_loop_t *us_create_loop(void *hint,

loop->uv_loop = hint ? hint : uv_loop_new();
loop->is_default = hint != 0;
/* Without this libuv never accumulates provider_idle_time, so
* uv_metrics_idle_time() — and performance.eventLoopUtilization() — read 0.
* node enables it unconditionally too (node.cc, node_worker.cc). */
uv_loop_configure(loop->uv_loop, UV_METRICS_IDLE_TIME);

loop->uv_pre = us_malloc(sizeof(uv_prepare_t));
uv_prepare_init(loop->uv_loop, loop->uv_pre);
Expand Down
3 changes: 3 additions & 0 deletions packages/bun-usockets/src/internal/internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,9 @@ uint64_t us_internal_monotonic_ns(void);
long long us_internal_sweep_timeout_ns(struct us_loop_t *loop);
void us_internal_sweep_if_due(struct us_loop_t *loop);
#endif
/* Nanoseconds this loop has spent parked, including a park in progress. Safe
* from another thread. Both platforms: Rust calls it uncgated. */
uint64_t us_loop_idle_ns(struct us_loop_t *loop);
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);
Expand Down
9 changes: 9 additions & 0 deletions packages/bun-usockets/src/internal/loop_data.h
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,15 @@ struct us_internal_loop_data_t {
* 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
#ifndef LIBUS_USE_LIBUV
/* Nanoseconds parked, for eventLoopUtilization(). Read cross-thread —
* __atomic_* only. MIRRORED in src/uws_sys/InternalLoopData.rs: this struct
* is us_loop_t's first member, so a field here shifts num_polls. */
unsigned long long idle_ns;
/* Monotonic ns the current park began, 0 when not parked: a mid-park reader
* must add (now - entry) or it sees a stale total. So does libuv. */
unsigned long long idle_entry_ns;
#endif
struct us_socket_group_t *iterator;
char *recv_buf;
Expand Down
20 changes: 20 additions & 0 deletions packages/bun-usockets/src/loop.c
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,26 @@ void us_internal_sweep_if_due(struct us_loop_t *loop) {
#endif


/* Nanoseconds this loop has spent parked, for performance.eventLoopUtilization().
* Safe to call from another thread. */
uint64_t us_loop_idle_ns(struct us_loop_t *loop) {
#ifdef LIBUS_USE_LIBUV
return uv_metrics_idle_time(loop->uv_loop);
#else
uint64_t idle = __atomic_load_n(&loop->data.idle_ns, __ATOMIC_RELAXED);
/* Parked right now? The total is only folded in when the park ends, so add
* the in-progress interval — otherwise a mid-park reader sees a stale idle
* and over-reports active. Same as libuv's uv_metrics_idle_time. */
uint64_t entry = __atomic_load_n(&loop->data.idle_entry_ns, __ATOMIC_ACQUIRE);
if (entry > 0) {
uint64_t now = us_internal_monotonic_ns();
if (now > entry)
idle += now - entry;
}
return idle;
#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.
Expand Down
35 changes: 35 additions & 0 deletions src/js/internal/perf/event_loop_utilization.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Shared by perf_hooks and worker_threads, as node shares
// lib/internal/perf/event_loop_utilization.js between the two.
//
// `elu` is [elapsedSinceLoopStartMs, idleMs] from native, or null when the loop
// has not turned yet — node's equivalent of its `loopStart <= 0` branch, and it
// is checked first there too, so elu(u, u) before the loop turns is {0,0,0}
// rather than NaN.
//
// The divisions are deliberately unguarded: node returns NaN for a zero total
// (verified on v26.3.0 — eventLoopUtilization(u, u) after the loop has turned
// yields NaN), so collapsing that to 0 would diverge.
function internalEventLoopUtilization(elu, util1, util2) {
if (elu === null) {
return { idle: 0, active: 0, utilization: 0 };
}

if (util2) {
const idle = util1.idle - util2.idle;
const active = util1.active - util2.active;
return { idle, active, utilization: active / (idle + active) };
}

const idle = elu[1];
const active = elu[0] - idle;

if (!util1) {
return { idle, active, utilization: active / (idle + active) };
}

const idleDelta = idle - util1.idle;
const activeDelta = active - util1.active;
return { idle: idleDelta, active: activeDelta, utilization: activeDelta / (idleDelta + activeDelta) };
}

export default { internalEventLoopUtilization };
13 changes: 7 additions & 6 deletions src/js/node/perf_hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,12 +114,13 @@ function createPerformanceNodeTiming() {
return object;
}

function eventLoopUtilization(_utilization1, _utilization2) {
return {
idle: 0,
active: 0,
utilization: 0,
};
// [elapsedSinceLoopStartMs, idleMs] for this thread's loop, or null before it
// has turned.
const getLoopELU = $newRustFunction("bun.rs", "getLoopELU", 0);
const { internalEventLoopUtilization } = require("internal/perf/event_loop_utilization");

function eventLoopUtilization(utilization1, utilization2) {
return internalEventLoopUtilization(getLoopELU(), utilization1, utilization2);
}

// PerformanceEntry is not a valid constructor, so we have to fake it.
Expand Down
20 changes: 11 additions & 9 deletions src/js/node/worker_threads.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@
const EventEmitter = require("node:events");
const { SafeMap } = require("internal/primordials");
const Readable = require("internal/streams/readable");
const { internalEventLoopUtilization } = require("internal/perf/event_loop_utilization");
const Writable = require("internal/streams/writable");
const { throwNotImplemented, warnNotImplementedOnce } = require("internal/shared");

Check failure on line 11 in src/js/node/worker_threads.ts

View workflow job for this annotation

GitHub Actions / Lint JavaScript

eslint(no-unused-vars)

Variable 'warnNotImplementedOnce' is declared but never used. Unused variables should start with a '_'.
const {
validateString,
validateObject,
Expand Down Expand Up @@ -135,7 +136,9 @@

function wrapped(run, listener) {
return function (event) {
return listener(run(event));
// node invokes emitter listeners with the emitter as `this`; an
// addEventListener handler's `this` is already the target, so forward it.
return listener.$call(this, run(event));
};
}

Expand Down Expand Up @@ -1187,17 +1190,16 @@

get performance() {
return (this.#performance ??= {
eventLoopUtilization() {
warnNotImplementedOnce("worker_threads.Worker.performance");
return {
idle: 0,
active: 0,
utilization: 0,
};
},
eventLoopUtilization: this.#eventLoopUtilization.bind(this),
});
}

#eventLoopUtilization(utilization1, utilization2) {
// null covers both "thread gone" and "loop has not turned" — node reports
// all-zero for each.
return internalEventLoopUtilization(this.#worker.eventLoopUtilizationInternal(), utilization1, utilization2);
}

terminate(callback: unknown) {
if (typeof callback === "function") {
process.emitWarning(
Expand Down
5 changes: 5 additions & 0 deletions src/jsc/VirtualMachine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,10 @@ pub struct VirtualMachine {
pub argv: Vec<Box<[u8]>>,

pub origin_timer: std::time::Instant,
/// When THIS thread's loop started, for performance.eventLoopUtilization().
/// Not `origin_timer`, which is the process origin shared by every thread —
/// a worker's active time is measured from its own start.
pub loop_start: std::time::Instant,
pub origin_timestamp: u64,
/// For fake timers: override performance.now() with a specific value (in nanoseconds).
pub overridden_performance_now: Option<u64>,
Expand Down Expand Up @@ -2106,6 +2110,7 @@ impl VirtualMachine {
addr_of_mut!((*vm).pending_internal_promise_reported_at).write(u32::MAX);
addr_of_mut!((*vm).on_unhandled_rejection)
.write(VirtualMachine::default_on_unhandled_rejection);
addr_of_mut!((*vm).loop_start).write(std::time::Instant::now());
let (origin_timer, origin_timestamp) = process_origin();
addr_of_mut!((*vm).origin_timer).write(origin_timer);
addr_of_mut!((*vm).origin_timestamp).write(origin_timestamp);
Expand Down
29 changes: 29 additions & 0 deletions src/jsc/bindings/webcore/JSWorker.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ static JSC_DECLARE_HOST_FUNCTION(jsWorkerPrototypeFunction_ref);
static JSC_DECLARE_HOST_FUNCTION(jsWorkerPrototypeFunction_getHeapSnapshot);
static JSC_DECLARE_HOST_FUNCTION(jsWorkerPrototypeFunction_getHeapStatistics);
static JSC_DECLARE_HOST_FUNCTION(jsWorkerPrototypeFunction_startCpuProfileInternal);
static JSC_DECLARE_HOST_FUNCTION(jsWorkerPrototypeFunction_eventLoopUtilizationInternal);
static JSC_DECLARE_HOST_FUNCTION(jsWorkerPrototypeFunction_stopCpuProfileInternal);
static JSC_DECLARE_HOST_FUNCTION(jsWorkerPrototypeFunction_cpuUsageInternal);

Expand Down Expand Up @@ -447,6 +448,7 @@ static const HashTableValue JSWorkerPrototypeTableValues[] = {
{ "startCpuProfileInternal"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | JSC::PropertyAttribute::DontEnum), NoIntrinsic, { HashTableValue::NativeFunctionType, jsWorkerPrototypeFunction_startCpuProfileInternal, 0 } },
{ "stopCpuProfileInternal"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | JSC::PropertyAttribute::DontEnum), NoIntrinsic, { HashTableValue::NativeFunctionType, jsWorkerPrototypeFunction_stopCpuProfileInternal, 0 } },
{ "cpuUsageInternal"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | JSC::PropertyAttribute::DontEnum), NoIntrinsic, { HashTableValue::NativeFunctionType, jsWorkerPrototypeFunction_cpuUsageInternal, 0 } },
{ "eventLoopUtilizationInternal"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | JSC::PropertyAttribute::DontEnum), NoIntrinsic, { HashTableValue::NativeFunctionType, jsWorkerPrototypeFunction_eventLoopUtilizationInternal, 0 } },
};

const ClassInfo JSWorkerPrototype::s_info = { "Worker"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSWorkerPrototype) };
Expand Down Expand Up @@ -857,6 +859,33 @@ static inline JSC::EncodedJSValue jsWorkerPrototypeFunction_stopCpuProfileIntern
return JSValue::encode(promise);
}

// Synchronous by contract: node's worker.performance.eventLoopUtilization()
// returns a value, it does not await the worker. Safe to read cross-thread —
// the counter is atomic and the loop start is immutable after publish.
static inline JSC::EncodedJSValue jsWorkerPrototypeFunction_eventLoopUtilizationInternalBody(JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame, typename IDLOperation<JSWorker>::ClassParameter castedThis)
{
auto* globalObject = defaultGlobalObject(lexicalGlobalObject);
auto& vm = JSC::getVM(globalObject);
auto throwScope = DECLARE_THROW_SCOPE(vm);
UNUSED_PARAM(callFrame);
double elapsedMs = 0;
double idleMs = 0;
if (!castedThis->wrapped().eventLoopUtilization(elapsedMs, idleMs))
RELEASE_AND_RETURN(throwScope, JSValue::encode(JSC::jsNull()));
JSC::JSArray* result = JSC::constructEmptyArray(globalObject, nullptr, 2);
RETURN_IF_EXCEPTION(throwScope, {});
result->putDirectIndex(globalObject, 0, JSC::jsNumber(elapsedMs));
RETURN_IF_EXCEPTION(throwScope, {});
result->putDirectIndex(globalObject, 1, JSC::jsNumber(idleMs));
RETURN_IF_EXCEPTION(throwScope, {});
RELEASE_AND_RETURN(throwScope, JSValue::encode(result));
}

JSC_DEFINE_HOST_FUNCTION(jsWorkerPrototypeFunction_eventLoopUtilizationInternal, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame))
{
return IDLOperation<JSWorker>::call<jsWorkerPrototypeFunction_eventLoopUtilizationInternalBody>(*lexicalGlobalObject, *callFrame, "eventLoopUtilizationInternal");
}

static inline JSC::EncodedJSValue jsWorkerPrototypeFunction_cpuUsageInternalBody(JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame, typename IDLOperation<JSWorker>::ClassParameter castedThis)
{
auto* globalObject = defaultGlobalObject(lexicalGlobalObject);
Expand Down
11 changes: 11 additions & 0 deletions src/jsc/bindings/webcore/Worker.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,10 @@ void WebWorker__releaseParentPollRef(void* worker);
// Free the native WebWorker struct. Called from ~Worker.
void WebWorker__destroy(void* worker);

// Read this worker's loop counters from the parent thread. False if the worker
// VM is gone. See src/jsc/web_worker.rs.
bool WebWorker__getELU(void* worker, double* outElapsedMs, double* outIdleMs);

} // extern "C"
// -------------------------------------------------------------------------------------------------

Expand Down Expand Up @@ -375,6 +379,13 @@ void Worker::terminate()
WebWorker__notifyNeedTermination(impl_);
}

bool Worker::eventLoopUtilization(double& elapsedMs, double& idleMs)
{
if (!impl_)
return false;
return WebWorker__getELU(impl_, &elapsedMs, &idleMs);
}

void Worker::setKeepAlive(bool keepAlive)
{
// Once terminate() has been called or the close task has started, the
Expand Down
3 changes: 3 additions & 0 deletions src/jsc/bindings/webcore/Worker.h
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,9 @@ class Worker final : public ThreadSafeRefCounted<Worker>, public EventTargetWith
void dispatchOnline(Zig::GlobalObject* workerGlobalObject);
void fireEarlyMessages(Zig::GlobalObject* workerGlobalObject);
void dispatchErrorWithMessage(WTF::String message, WTF::String code);
/// `[elapsedSinceLoopStartMs, idleMs]` for this worker's loop, read live from
/// the parent. False once the thread is gone (node reports all-zero then).
bool eventLoopUtilization(double& elapsedMs, double& idleMs);
static WTF::String errorCodeOf(JSC::JSGlobalObject*, JSC::JSValue);
bool dispatchErrorWithValue(Zig::GlobalObject* workerGlobalObject, JSValue value);
bool dispatchExit(int32_t exitCode);
Expand Down
43 changes: 43 additions & 0 deletions src/jsc/web_worker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,49 @@ pub fn terminate_all_and_wait(timeout_ms: u64) {
}
}

/// The PARENT reading a live worker's loop counters. False once the worker VM is
/// gone, which node reports as all-zero. `vm_lock` only closes the TOCTOU on
/// `vm`: `idle_ns` is atomic and `loop_start` is fixed before publish.
///
/// # Safety
/// `worker` is a live `WebWorker*` owned by the calling C++ `Worker`; the out
/// params are non-null and writable.
#[unsafe(no_mangle)]
pub(crate) unsafe extern "C" fn WebWorker__getELU(
worker: *mut WebWorker,
out_elapsed_ms: *mut f64,
out_idle_ms: *mut f64,
) -> bool {
// SAFETY: per fn contract — a live WebWorker for the duration of the call.
let w = unsafe { &*worker };
w.vm_lock.lock();
let vm_ptr = w.vm_ptr();
let live = !vm_ptr.is_null();
if live {
// No `&VirtualMachine` binding: the worker thread may hold a live mutable
// view. Raw-pointer access keeps any autoref scoped to the access, as the
// terminate path above does.
// SAFETY: event_loop() is the live self-pointer; the reads below are an
// atomic load and a Copy field written before the VM was published.
let loop_ = unsafe { (*(*vm_ptr).event_loop()).usockets_loop().as_ref() };
let Some(loop_) = loop_ else {
w.vm_lock.unlock();
return false;
};
// Idle BEFORE elapsed, matching node's order — reversed, idle is dated
// after now and active = now - idle comes out short.
let idle_ms = loop_.idle_ns() as f64 / 1_000_000.0;
let elapsed_ms = unsafe { (*vm_ptr).loop_start }.elapsed().as_secs_f64() * 1000.0;
// SAFETY: per fn contract — out params are writable.
unsafe {
*out_elapsed_ms = elapsed_ms;
*out_idle_ms = idle_ms;
}
}
w.vm_lock.unlock();
live
}

#[unsafe(no_mangle)]
pub(crate) extern "C" fn WebWorker__getParentWorker(vm: &VirtualMachine) -> *mut c_void {
vm.worker_ref()
Expand Down
31 changes: 27 additions & 4 deletions src/runtime/dispatch_js2native.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,33 @@ pub(crate) fn bun_get_use_system_ca(
_global: &JSGlobalObject,
_frame: &CallFrame,
) -> JsResult<JSValue> {
Ok(match bun_jsc::virtual_machine::VirtualMachine::get().use_system_ca {
Some(v) => JSValue::js_boolean(v),
None => JSValue::UNDEFINED,
})
Ok(
match bun_jsc::virtual_machine::VirtualMachine::get().use_system_ca {
Some(v) => JSValue::js_boolean(v),
None => JSValue::UNDEFINED,
},
)
}

/// `[elapsedSinceLoopStartMs, idleMs]` for THIS thread's loop — the two numbers
/// performance.eventLoopUtilization() is defined in terms of (node derives
/// active as now - loopStart - idle).
pub(crate) fn bun_get_loop_elu(global: &JSGlobalObject, _frame: &CallFrame) -> JsResult<JSValue> {
let vm = bun_jsc::virtual_machine::VirtualMachine::get();
// SAFETY: the VM owns this loop and this runs on its thread.
let loop_ = unsafe { (*vm.event_loop).usockets_loop().as_ref() };
let Some(loop_) = loop_ else {
return Ok(JSValue::NULL);
};
// Idle BEFORE elapsed, matching node's order (it passes loopIdleTime() in
// and reads process.hrtime() after). Reversed, idle is dated after now and
// active = now - idle comes out short.
let idle_ms = loop_.idle_ns() as f64 / 1_000_000.0;
let elapsed_ms = vm.loop_start.elapsed().as_secs_f64() * 1000.0;
let arr = JSValue::create_empty_array(global, 2)?;
arr.put_index(global, 0, JSValue::js_number(elapsed_ms))?;
arr.put_index(global, 1, JSValue::js_number(idle_ms))?;
Ok(arr)
}

mod css {
Expand Down
9 changes: 9 additions & 0 deletions src/uws_sys/InternalLoopData.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,15 @@ pub struct InternalLoopData {
pub quic_next_tick_us: i64,
#[cfg(windows)]
pub quic_timer: *mut Timer,
/// Nanoseconds this loop has spent parked, for eventLoopUtilization().
/// Mirrors the `#ifndef LIBUS_USE_LIBUV` field in loop_data.h — libuv tracks
/// the same itself via uv_metrics_idle_time.
#[cfg(not(windows))]
pub idle_ns: u64,
/// Monotonic ns the current park began, 0 when not parked. Mirrors
/// loop_data.h — see the layout warning on `idle_ns`.
#[cfg(not(windows))]
pub idle_entry_ns: u64,
pub iterator: *mut SocketGroup,
pub recv_buf: *mut u8,
pub send_buf: *mut u8,
Expand Down
Loading
Loading