LibWeb: Use repeating timers for setInterval() to reduce drift

The timer nesting level throttling change (7577fd2a57) inadvertently
reverted the repeating timer optimization from 4d27e9aa5e, causing
setInterval() to use single-shot timers that re-arm after the callback
completes. This meant the next firing was scheduled relative to callback
completion rather than the previous fire time, causing drift
proportional to callback execution time.

For example, DiabloWeb's 50ms game loop with ~20ms of WASM work per
frame was only achieving 14 FPS (71ms intervals) instead of 20 FPS.

Fix this by using repeating Core::Timer for setInterval() again. The
repeating timer fires on schedule regardless of callback duration. On
re-arm, we update the callback (for nesting level changes) and the
interval (in case nesting level clamping kicks in), but don't restart
the timer since it's already running on the correct schedule.
This commit is contained in:
Andreas Kling
2026-03-12 21:43:26 -05:00
committed by Tim Ledbetter
parent ef899027c5
commit 269d5f739b
Notes: github-actions[bot] 2026-03-13 03:32:23 +00:00
2 changed files with 13 additions and 8 deletions

View File

@@ -686,7 +686,7 @@ i32 WindowOrWorkerGlobalScopeMixin::run_timer_initialization_steps(TimerHandler
// 13. Set uniqueHandle to the result of running steps after a timeout given global, "setTimeout/setInterval",
// timeout, and completionStep.
// FIXME: run_steps_after_a_timeout() needs to be updated to return a unique internal value that can be used here.
run_steps_after_a_timeout_impl(timeout, move(completion_step), id);
run_steps_after_a_timeout_impl(timeout, move(completion_step), id, repeat);
// FIXME: 14. Set global's map of setTimeout and setInterval IDs[id] to uniqueHandle.
@@ -1091,7 +1091,7 @@ void WindowOrWorkerGlobalScopeMixin::run_steps_after_a_timeout(i32 timeout, Func
run_steps_after_a_timeout_impl(timeout, move(completion_step), {});
}
void WindowOrWorkerGlobalScopeMixin::run_steps_after_a_timeout_impl(i32 timeout, Function<void()> completion_step, Optional<i32> timer_key)
void WindowOrWorkerGlobalScopeMixin::run_steps_after_a_timeout_impl(i32 timeout, Function<void()> completion_step, Optional<i32> timer_key, Repeat repeat)
{
// 1. Assert: if timerKey is given, then the caller of this algorithm is the timer initialization steps. (Other specifications must not pass timerKey.)
// Note: This is enforced by the caller.
@@ -1112,10 +1112,12 @@ void WindowOrWorkerGlobalScopeMixin::run_steps_after_a_timeout_impl(i32 timeout,
// FIXME: 3. Let startTime be the current high resolution time given global.
// NB: We always use single-shot timers. For repeating timers, the task callback will call
// run_timer_initialization_steps again to re-arm the timer. This ensures the timer only fires after the previous
// task has been processed, which is necessary for the timer nesting level throttling.
auto timer = existing_timer ? GC::Ref { *existing_timer } : Timer::create(this_impl(), timeout, move(completion_step), timer_key.value(), Timer::Repeating::No);
// NB: For repeating timers (setInterval), we use a repeating Core::Timer to reduce drift.
// The next firing is based on when the timer was supposed to fire, not when the callback
// completed. The task callback still calls run_timer_initialization_steps to update nesting
// levels and potentially clamp the interval.
auto repeating = repeat == Repeat::Yes ? Timer::Repeating::Yes : Timer::Repeating::No;
auto timer = existing_timer ? GC::Ref { *existing_timer } : Timer::create(this_impl(), timeout, move(completion_step), timer_key.value(), repeating);
// FIXME: 4. Set global's map of active timers[timerKey] to startTime plus milliseconds.
m_timers.set(timer_key.value(), timer);
@@ -1128,7 +1130,10 @@ void WindowOrWorkerGlobalScopeMixin::run_steps_after_a_timeout_impl(i32 timeout,
// FIXME: 4. Perform completionSteps.
// FIXME: 5. If timerKey is a non-numeric value, remove global's map of active timers[timerKey].
timer->start();
// NB: Don't restart an already-active repeating timer. It's already firing on schedule and
// restarting it would cause drift (next fire = now + interval instead of previous fire + interval).
if (!existing_timer)
timer->start();
}
// https://w3c.github.io/hr-time/#dom-windoworworkerglobalscope-performance