mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-01 07:33:12 +08:00
Keep timer deadlines on macOS and re-arm intervals as Node does
The event loop slept to its next timer with nanosleep. macOS applies the timer slack of the thread's QoS class to nanosleep: a 5 ms sleep takes about 38 ms at utility QoS and 180 ms at background QoS, where launchd services such as CI jobs run. Timers then fired out of order against Node, whose libuv loop waits with a kevent timeout that keeps its deadline at every QoS. On macOS the loop now sleeps with a kevent timeout on a kqueue with no events, shared by every thread's loop. Intervals also re-arm from the time their callback started, as Node's listOnTimeout does, instead of from the clock after the callback: a callback that takes 30 ms of a 50 ms interval keeps the ticks 50 ms apart, and one slower than the interval makes the next tick due on the next turn. interval-slow-callback.ts checks both against Node.
This commit is contained in:
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/interval-slow-callback.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -58,6 +58,9 @@
|
||||
#include <sys/mman.h>
|
||||
#include <ucontext.h>
|
||||
#include <unistd.h>
|
||||
#ifdef __APPLE__
|
||||
#include <sys/event.h>
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#if defined(__has_feature)
|
||||
@@ -1052,6 +1055,44 @@ double scr_now_ms(void) {
|
||||
return (double)ts.tv_sec * 1000.0 + (double)ts.tv_nsec / 1e6;
|
||||
}
|
||||
|
||||
/* The loop's sleep to its next timer when nothing else can wake it. macOS
|
||||
* gives nanosleep the timer slack of the thread's QoS class: a 5 ms sleep
|
||||
* takes about 38 ms at utility QoS and 180 ms at background QoS, which is
|
||||
* where launchd services, CI jobs among them, run. A kevent timeout keeps
|
||||
* its deadline at every QoS, as libuv's loop does. */
|
||||
#if defined(__APPLE__)
|
||||
/* One queue with no events for every thread that sleeps (worker loops
|
||||
* included), created on first use. */
|
||||
static _Atomic int scr_sleep_kq = -1;
|
||||
#endif
|
||||
static void scr_loop_sleep(double wait_ms) {
|
||||
struct timespec ts = {(time_t)(wait_ms / 1000.0),
|
||||
(long)((wait_ms - (double)((time_t)(wait_ms / 1000.0)) * 1000.0) * 1e6)};
|
||||
#if defined(__APPLE__)
|
||||
int kq = atomic_load_explicit(&scr_sleep_kq, memory_order_acquire);
|
||||
if (kq < 0) {
|
||||
int created = kqueue();
|
||||
if (created >= 0) {
|
||||
(void)fcntl(created, F_SETFD, FD_CLOEXEC);
|
||||
int none = -1;
|
||||
if (atomic_compare_exchange_strong(&scr_sleep_kq, &none, created)) {
|
||||
kq = created;
|
||||
} else {
|
||||
close(created);
|
||||
kq = none;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (kq >= 0) {
|
||||
/* An interrupted wait returns early, as nanosleep does. */
|
||||
struct kevent ignored;
|
||||
(void)kevent(kq, NULL, 0, &ignored, 1, &ts);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
nanosleep(&ts, NULL);
|
||||
}
|
||||
|
||||
static bool scr_timer_before(const ScrTimer *a, const ScrTimer *b) {
|
||||
if (a->deadline_ms != b->deadline_ms) return a->deadline_ms < b->deadline_ms;
|
||||
return a->seq < b->seq;
|
||||
@@ -5962,9 +6003,7 @@ static bool scr_loop_run_pass(ScrPromise *top_level, bool first_checkpoint) {
|
||||
} else
|
||||
#endif
|
||||
if (due > now) {
|
||||
double wait = due - now;
|
||||
struct timespec ts = {(time_t)(wait / 1000.0), (long)((wait - (double)((time_t)(wait / 1000.0)) * 1000.0) * 1e6)};
|
||||
nanosleep(&ts, NULL);
|
||||
scr_loop_sleep(due - now);
|
||||
now = due;
|
||||
}
|
||||
}
|
||||
@@ -5986,13 +6025,16 @@ static bool scr_loop_run_pass(ScrPromise *top_level, bool first_checkpoint) {
|
||||
scr_firing_reffed = t.reffed; /* unref/ref during the callback lands here */
|
||||
scr_firing_refresh = false;
|
||||
}
|
||||
double started = t.id != 0 && t.repeat_ms > 0 ? scr_now_ms() : 0;
|
||||
((void (*)(ScrClosure *))t.cb->fn)(t.cb);
|
||||
if (t.id != 0 && t.repeat_ms > 0 && !scr_firing_cleared) {
|
||||
/* Re-arm relative to the post-callback clock (libuv's uv_timer
|
||||
* repeat behavior: no catch-up bursts after a slow callback). The
|
||||
* ref state carries across ticks (a self-unref'd interval stays
|
||||
* unref'd). */
|
||||
ScrTimer again = {scr_now_ms() + t.repeat_ms, scr_timer_seq++, t.cb, t.repeat_ms, t.id, scr_firing_reffed, t.delay_ms};
|
||||
/* Re-arm relative to the time the callback started, as Node's
|
||||
* listOnTimeout does (getLibuvNow() before _onTimeout): a slow
|
||||
* callback does not push the next tick back by its own duration.
|
||||
* One slower than the interval makes the next tick due on the next
|
||||
* turn, without catch-up bursts. The ref state carries across
|
||||
* ticks (a self-unref'd interval stays unref'd). */
|
||||
ScrTimer again = {started + t.repeat_ms, scr_timer_seq++, t.cb, t.repeat_ms, t.id, scr_firing_reffed, t.delay_ms};
|
||||
scr_timer_push(again);
|
||||
} else if (t.id != 0 && scr_firing_refresh && !scr_firing_cleared) {
|
||||
/* refresh() from inside the one-shot's own callback: re-arm to
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
// setInterval schedules each tick from the time its callback started, as
|
||||
// Node does: a callback that takes 30 ms of a 50 ms interval keeps the ticks
|
||||
// 50 ms apart, and one that takes 70 ms makes the next tick due at once (70
|
||||
// ms apart, not 120). Each line prints whether every gap after the first
|
||||
// tick stays under a bound between those spacings and the ones a re-arm
|
||||
// after the callback would give (80 and 120 ms).
|
||||
function run(work: number, bound: number, done: () => void): void {
|
||||
const gaps: number[] = [];
|
||||
let last = -1;
|
||||
const interval = setInterval(() => {
|
||||
const now = performance.now();
|
||||
if (last >= 0) gaps.push(now - last);
|
||||
last = now;
|
||||
while (performance.now() - now < work) {}
|
||||
if (gaps.length === 4) {
|
||||
clearInterval(interval);
|
||||
console.log(`work ${work} ms:`, gaps.every((gap) => gap < bound));
|
||||
done();
|
||||
}
|
||||
}, 50);
|
||||
}
|
||||
|
||||
run(30, 75, () => run(70, 110, () => console.log("done")));
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 5676, "san": 6800 }
|
||||
Reference in New Issue
Block a user