mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2026-04-18 18:00:31 +00:00
LibWeb: Implement timer nesting level throttling
This clamps the interval of repeating timers to 4ms after they have run 5 times. This prevents CPU spinning for `setInterval()` calls with low timeouts.
This commit is contained in:
parent
24055b60ef
commit
7577fd2a57
Notes:
github-actions[bot]
2026-02-11 16:34:54 +00:00
Author: https://github.com/tcl3
Commit: 7577fd2a57
Pull-request: https://github.com/LadybirdBrowser/ladybird/pull/7734
Reviewed-by: https://github.com/rcorsi ✅
4 changed files with 54 additions and 9 deletions
|
|
@ -519,6 +519,7 @@ void WindowOrWorkerGlobalScopeMixin::clear_timeout(i32 id)
|
|||
if (auto timer = m_timers.get(id); timer.has_value())
|
||||
timer.value()->stop();
|
||||
m_timers.remove(id);
|
||||
m_timer_nesting_levels.remove(id);
|
||||
}
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/timers-and-user-prompts.html#dom-clearinterval
|
||||
|
|
@ -527,6 +528,7 @@ void WindowOrWorkerGlobalScopeMixin::clear_interval(i32 id)
|
|||
if (auto timer = m_timers.get(id); timer.has_value())
|
||||
timer.value()->stop();
|
||||
m_timers.remove(id);
|
||||
m_timer_nesting_levels.remove(id);
|
||||
}
|
||||
|
||||
void WindowOrWorkerGlobalScopeMixin::clear_map_of_active_timers()
|
||||
|
|
@ -534,6 +536,7 @@ void WindowOrWorkerGlobalScopeMixin::clear_map_of_active_timers()
|
|||
for (auto& it : m_timers)
|
||||
it.value->stop();
|
||||
m_timers.clear();
|
||||
m_timer_nesting_levels.clear();
|
||||
}
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/timers-and-user-prompts.html#timer-initialisation-steps
|
||||
|
|
@ -545,13 +548,19 @@ i32 WindowOrWorkerGlobalScopeMixin::run_timer_initialization_steps(TimerHandler
|
|||
// 2. If previousId was given, let id be previousId; otherwise, let id be an implementation-defined integer that is greater than zero and does not already exist in global's map of setTimeout and setInterval IDs.
|
||||
auto id = previous_id.has_value() ? previous_id.value() : m_timer_id_allocator.allocate();
|
||||
|
||||
// FIXME: 3. If the surrounding agent's event loop's currently running task is a task that was created by this algorithm, then let nesting level be the task's timer nesting level. Otherwise, let nesting level be 0.
|
||||
// 3. If the surrounding agent's event loop's currently running task is a task that was created by this algorithm,
|
||||
// then let nesting level be the task's timer nesting level. Otherwise, let nesting level be 0.
|
||||
u32 nesting_level = 0;
|
||||
if (previous_id.has_value())
|
||||
nesting_level = m_timer_nesting_levels.get(previous_id.value()).value_or(0);
|
||||
|
||||
// 4. If timeout is less than 0, then set timeout to 0.
|
||||
if (timeout < 0)
|
||||
timeout = 0;
|
||||
|
||||
// FIXME: 5. If nesting level is greater than 5, and timeout is less than 4, then set timeout to 4.
|
||||
// 5. If nesting level is greater than 5, and timeout is less than 4, then set timeout to 4.
|
||||
if (nesting_level > 5 && timeout < 4)
|
||||
timeout = 4;
|
||||
|
||||
// 6. Let realm be global's relevant realm.
|
||||
auto& realm = relevant_realm(this_impl());
|
||||
|
|
@ -655,12 +664,16 @@ i32 WindowOrWorkerGlobalScopeMixin::run_timer_initialization_steps(TimerHandler
|
|||
// 10. Otherwise, remove global's map of active timers[id].
|
||||
case Repeat::No:
|
||||
m_timers.remove(id);
|
||||
m_timer_nesting_levels.remove(id);
|
||||
break;
|
||||
}
|
||||
}));
|
||||
|
||||
// FIXME: 10. Increment nesting level by one.
|
||||
// FIXME: 11. Set task's timer nesting level to nesting level.
|
||||
// 10. Increment nesting level by one.
|
||||
nesting_level++;
|
||||
|
||||
// 11. Set task's timer nesting level to nesting level.
|
||||
m_timer_nesting_levels.set(id, nesting_level);
|
||||
|
||||
// 12. Let completionStep be an algorithm step which queues a global task on the timer task source given global to run task.
|
||||
Function<void()> completion_step = [this, task = move(task)]() mutable {
|
||||
|
|
@ -673,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, repeat);
|
||||
run_steps_after_a_timeout_impl(timeout, move(completion_step), id);
|
||||
|
||||
// FIXME: 14. Set global's map of setTimeout and setInterval IDs[id] to uniqueHandle.
|
||||
|
||||
|
|
@ -1075,10 +1088,10 @@ WindowOrWorkerGlobalScopeMixin::AffectedAnyWebSockets WindowOrWorkerGlobalScopeM
|
|||
// https://html.spec.whatwg.org/multipage/timers-and-user-prompts.html#run-steps-after-a-timeout
|
||||
void WindowOrWorkerGlobalScopeMixin::run_steps_after_a_timeout(i32 timeout, Function<void()> completion_step)
|
||||
{
|
||||
return run_steps_after_a_timeout_impl(timeout, move(completion_step), {}, Repeat::No);
|
||||
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, Repeat repeat)
|
||||
void WindowOrWorkerGlobalScopeMixin::run_steps_after_a_timeout_impl(i32 timeout, Function<void()> completion_step, Optional<i32> timer_key)
|
||||
{
|
||||
// 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.
|
||||
|
|
@ -1090,6 +1103,7 @@ void WindowOrWorkerGlobalScopeMixin::run_steps_after_a_timeout_impl(i32 timeout,
|
|||
if (result.has_value()) {
|
||||
existing_timer = result.value().ptr();
|
||||
existing_timer->set_callback(move(completion_step));
|
||||
existing_timer->set_interval(timeout);
|
||||
}
|
||||
} else {
|
||||
// 2. If timerKey is not given, then set it to a new unique non-numeric value.
|
||||
|
|
@ -1097,7 +1111,11 @@ void WindowOrWorkerGlobalScopeMixin::run_steps_after_a_timeout_impl(i32 timeout,
|
|||
}
|
||||
|
||||
// FIXME: 3. Let startTime be the current high resolution time given global.
|
||||
auto timer = existing_timer ? GC::Ref { *existing_timer } : Timer::create(this_impl(), timeout, move(completion_step), timer_key.value(), repeat == Repeat::Yes ? Timer::Repeating::Yes : Timer::Repeating::No);
|
||||
|
||||
// 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);
|
||||
|
||||
// FIXME: 4. Set global's map of active timers[timerKey] to startTime plus milliseconds.
|
||||
m_timers.set(timer_key.value(), timer);
|
||||
|
|
|
|||
|
|
@ -119,7 +119,7 @@ private:
|
|||
No,
|
||||
};
|
||||
i32 run_timer_initialization_steps(TimerHandler handler, i32 timeout, GC::RootVector<JS::Value> arguments, Repeat repeat, Optional<i32> previous_id = {});
|
||||
void run_steps_after_a_timeout_impl(i32 timeout, Function<void()> completion_step, Optional<i32> timer_key, Repeat);
|
||||
void run_steps_after_a_timeout_impl(i32 timeout, Function<void()> completion_step, Optional<i32> timer_key);
|
||||
|
||||
GC::Ref<WebIDL::Promise> create_image_bitmap_impl(ImageBitmapSource& image, Optional<WebIDL::Long> sx, Optional<WebIDL::Long> sy, Optional<WebIDL::Long> sw, Optional<WebIDL::Long> sh, Optional<ImageBitmapOptions>& options) const;
|
||||
|
||||
|
|
@ -131,6 +131,9 @@ private:
|
|||
IDAllocator m_timer_id_allocator;
|
||||
HashMap<int, GC::Ref<Timer>> m_timers;
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/timers-and-user-prompts.html#timer-nesting-level
|
||||
HashMap<i32, u32> m_timer_nesting_levels;
|
||||
|
||||
// https://www.w3.org/TR/performance-timeline/#performance-timeline
|
||||
// Each global object has:
|
||||
// - a performance observer task queued flag
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
Callback count bounded (< 100 in 100ms): true
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
<!DOCTYPE html>
|
||||
<script src="../include.js"></script>
|
||||
<script>
|
||||
asyncTest(done => {
|
||||
// With 4ms clamping after nesting level 5, running for 100ms should yield
|
||||
// roughly 5-6 unclamped calls plus approximately 25 clamped calls.
|
||||
// Without clamping, we'd get thousands of calls.
|
||||
// We test that the count is bounded, proving clamping is working.
|
||||
const testDurationMilliseconds = 100;
|
||||
let callCount = 0;
|
||||
|
||||
const id = setInterval(() => {
|
||||
callCount++;
|
||||
}, 0);
|
||||
|
||||
setTimeout(() => {
|
||||
clearInterval(id);
|
||||
|
||||
println(`Callback count bounded (< 100 in 100ms): ${callCount < 100}`);
|
||||
done();
|
||||
}, testDurationMilliseconds);
|
||||
});
|
||||
</script>
|
||||
Loading…
Add table
Add a link
Reference in a new issue