From 347f981768e8ff9baca9db4814ef3672d5c65be1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 2 Apr 2026 11:18:17 -1000 Subject: [PATCH] [scheduler] Fix unrealistic scheduler benchmarks missing periodic drain MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Scheduler registration benchmarks (SetTimeout, SetInterval, Defer) were not calling scheduler.call() periodically to drain and clean up cancelled items. In production, call() runs every loop iteration, keeping the scheduler containers small. Without draining, cancelled items accumulated causing O(n²) scan cost in cancel_item_locked_ that doesn't reflect real-world behavior. - SetTimeout: was only calling process_to_add() (no cleanup), now calls call() every kKeyCount iterations - SetInterval: was calling process_to_add() (no cleanup of items_), now calls call() for proper cleanup - Defer: was never draining the defer queue, now calls call() to process deferred items as production does - All three now advance time (++now) to match production loop behavior --- tests/benchmarks/core/bench_scheduler.cpp | 44 +++++++++++++++++------ 1 file changed, 34 insertions(+), 10 deletions(-) diff --git a/tests/benchmarks/core/bench_scheduler.cpp b/tests/benchmarks/core/bench_scheduler.cpp index 9357734cc8..b2616af2ea 100644 --- a/tests/benchmarks/core/bench_scheduler.cpp +++ b/tests/benchmarks/core/bench_scheduler.cpp @@ -82,12 +82,24 @@ BENCHMARK(Scheduler_Call_5IntervalsFiring); static void Scheduler_SetTimeout(benchmark::State &state) { Scheduler scheduler; Component dummy_component; + // Number of distinct timeout keys; controls how many unique timers exist + // simultaneously and the drain cadence for process_to_add(). + static constexpr int kKeyCount = 5; for (auto _ : state) { + uint32_t now = millis(); for (int i = 0; i < kInnerIterations; i++) { - scheduler.set_timeout(&dummy_component, static_cast(i % 5), 1000, []() {}); + scheduler.set_timeout(&dummy_component, static_cast(i % kKeyCount), 1000, []() {}); + // Drain periodically to reflect production behavior where call() runs + // each main loop iteration. call() moves to_add_ into items_ and cleans + // up cancelled items. Without this, cancelled items accumulate causing + // O(n²) scan cost in cancel_item_locked_. + if ((i + 1) % kKeyCount == 0) { + scheduler.call(++now); + } } - scheduler.process_to_add(); + // Final drain in case kInnerIterations is not a multiple of kKeyCount + scheduler.call(++now); benchmark::DoNotOptimize(scheduler); } state.SetItemsProcessed(state.iterations() * kInnerIterations); @@ -104,17 +116,19 @@ static void Scheduler_SetInterval(benchmark::State &state) { static constexpr int kKeyCount = 5; for (auto _ : state) { + uint32_t now = millis(); for (int i = 0; i < kInnerIterations; i++) { scheduler.set_interval(&dummy_component, static_cast(i % kKeyCount), 1000, []() {}); - // Drain to_add_ periodically to reflect production behavior where - // process_to_add() runs each main loop iteration. Without this, - // cancelled items accumulate in to_add_ causing O(n²) scan cost. + // Drain periodically to reflect production behavior where call() runs + // each main loop iteration. call() moves to_add_ into items_ and cleans + // up cancelled items. Without this, cancelled items accumulate causing + // O(n²) scan cost in cancel_item_locked_. if ((i + 1) % kKeyCount == 0) { - scheduler.process_to_add(); + scheduler.call(++now); } } - // Final drain in case kInnerIterations is not a multiple of 5 - scheduler.process_to_add(); + // Final drain in case kInnerIterations is not a multiple of kKeyCount + scheduler.call(++now); benchmark::DoNotOptimize(scheduler); } state.SetItemsProcessed(state.iterations() * kInnerIterations); @@ -126,14 +140,24 @@ BENCHMARK(Scheduler_SetInterval); static void Scheduler_Defer(benchmark::State &state) { Scheduler scheduler; Component dummy_component; + // Number of distinct defer keys; controls how many unique defers exist + // simultaneously and the drain cadence for call(). + static constexpr int kKeyCount = 5; // defer() is Component::defer which calls set_timeout(delay=0). // Call set_timeout directly since defer() is protected. + // Drain with call() periodically to reflect production behavior where + // call() runs each main loop iteration, keeping the defer queue small. for (auto _ : state) { + uint32_t now = millis(); for (int i = 0; i < kInnerIterations; i++) { - scheduler.set_timeout(&dummy_component, static_cast(i % 5), 0, []() {}); + scheduler.set_timeout(&dummy_component, static_cast(i % kKeyCount), 0, []() {}); + if ((i + 1) % kKeyCount == 0) { + scheduler.call(++now); + } } - scheduler.process_to_add(); + // Final drain in case kInnerIterations is not a multiple of kKeyCount + scheduler.call(++now); benchmark::DoNotOptimize(scheduler); } state.SetItemsProcessed(state.iterations() * kInnerIterations);