Reduce idle CPU usage in long-running processes (#21579)

### What does this PR do?

Releasing heap access causes all the heap helper threads to wake up and
lock and then unlock futexes, but it's important to do that to ensure
finalizers run quickly.

That means releasing heap access is a balance between:
 1. CPU usage
 2. Memory usage

Not releasing heap access causes benchmarks like
https://github.com/oven-sh/bun/pull/14885 to regress due to finalizers
not being called quickly enough.

Releasing heap access too often causes high idle CPU usage.

 For the following code:
 ```
 setTimeout(() => {}, 10 * 1000)
 ```

command time -v when with defaultRemainingRunsUntilSkipReleaseAccess =
0:
>
>   Involuntary context switches: 605
>

command time -v when with defaultRemainingRunsUntilSkipReleaseAccess =
5:
>
>   Involuntary context switches: 350
>

command time -v when with defaultRemainingRunsUntilSkipReleaseAccess =
10:
>
>  Involuntary context switches: 241
>

 Also comapre the #14885 benchmark with different values.

 The idea here is if you entered JS "recently", running any
 finalizers that might've been waiting to be run is a good idea.
 But if you haven't, like if the process is just waiting on I/O
 then don't bother.

### How did you verify your code works?
This commit is contained in:
Jarred Sumner
2025-08-03 18:14:40 -07:00
committed by GitHub
parent 276eee74eb
commit 1ac2391b20
2 changed files with 58 additions and 6 deletions

View File

@@ -246,7 +246,7 @@ void us_loop_run(struct us_loop_t *loop) {
}
}
extern void Bun__JSC_onBeforeWait(void*);
extern int Bun__JSC_onBeforeWait(void*);
extern void Bun__JSC_onAfterWait(void*);
void us_loop_run_bun_tick(struct us_loop_t *loop, const struct timespec* timeout) {
@@ -265,7 +265,7 @@ void us_loop_run_bun_tick(struct us_loop_t *loop, const struct timespec* timeout
us_internal_loop_pre(loop);
/* Safe if jsc_vm is NULL */
Bun__JSC_onBeforeWait(loop->data.jsc_vm);
int must_call_on_after_wait = Bun__JSC_onBeforeWait(loop->data.jsc_vm);
/* Fetch ready polls */
#ifdef LIBUS_USE_EPOLL
@@ -276,7 +276,9 @@ void us_loop_run_bun_tick(struct us_loop_t *loop, const struct timespec* timeout
} while (IS_EINTR(loop->num_ready_polls));
#endif
Bun__JSC_onAfterWait(loop->data.jsc_vm);
if (must_call_on_after_wait) {
Bun__JSC_onAfterWait(loop->data.jsc_vm);
}
/* Iterate ready polls, dispatching them by type */
for (loop->current_ready_poll = 0; loop->current_ready_poll < loop->num_ready_polls; loop->current_ready_poll++) {

View File

@@ -12,7 +12,7 @@ static thread_local std::optional<JSC::JSLock::DropAllLocks> drop_all_locks { st
extern "C" void WTFTimer__runIfImminent(void* bun_vm);
// Safe if VM is nullptr
extern "C" void Bun__JSC_onBeforeWait(JSC::VM* vm)
extern "C" int Bun__JSC_onBeforeWait(JSC::VM* vm)
{
ASSERT(!drop_all_locks.has_value());
if (vm) {
@@ -20,11 +20,61 @@ extern "C" void Bun__JSC_onBeforeWait(JSC::VM* vm)
// sanity check for debug builds to ensure we're not doing a
// use-after-free here
ASSERT(vm->refCount() > 0);
drop_all_locks.emplace(*vm);
if (previouslyHadAccess) {
vm->heap.releaseAccess();
// Releasing heap access is a balance between:
// 1. CPU usage
// 2. Memory usage
//
// Not releasing heap access causes benchmarks like
// https://github.com/oven-sh/bun/pull/14885 to regress due to
// finalizers not being called quickly enough.
//
// Releasing heap access too often causes high idle CPU usage.
//
// For the following code:
// ```
// setTimeout(() => {}, 10 * 1000)
// ```
//
// command time -v when with defaultRemainingRunsUntilSkipReleaseAccess = 0:
//
// Involuntary context switches: 605
//
// command time -v when with defaultRemainingRunsUntilSkipReleaseAccess = 5:
//
// Involuntary context switches: 350
//
// command time -v when with defaultRemainingRunsUntilSkipReleaseAccess = 10:
//
// Involuntary context switches: 241
//
// Also comapre the #14885 benchmark with different values.
//
// The idea here is if you entered JS "recently", running any
// finalizers that might've been waiting to be run is a good idea.
// But if you haven't, like if the process is just waiting on I/O
// then don't bother.
static constexpr int defaultRemainingRunsUntilSkipReleaseAccess = 10;
static thread_local int remainingRunsUntilSkipReleaseAccess = 0;
// Note: usage of `didEnterVM` in JSC::VM conflicts with Options::validateDFGClobberize
// We don't need to use that option, so it should be fine.
if (vm->didEnterVM) {
vm->didEnterVM = false;
remainingRunsUntilSkipReleaseAccess = defaultRemainingRunsUntilSkipReleaseAccess;
}
if (remainingRunsUntilSkipReleaseAccess-- > 0) {
drop_all_locks.emplace(*vm);
vm->heap.releaseAccess();
vm->didEnterVM = false;
return 1;
}
}
}
return 0;
}
extern "C" void Bun__JSC_onAfterWait(JSC::VM* vm)