[scheduler] Fix unrealistic scheduler benchmarks missing periodic drain

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
This commit is contained in:
J. Nick Koston
2026-04-02 11:18:17 -10:00
parent 4d0d3cc271
commit 347f981768
+34 -10
View File
@@ -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<uint32_t>(i % 5), 1000, []() {});
scheduler.set_timeout(&dummy_component, static_cast<uint32_t>(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<uint32_t>(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<uint32_t>(i % 5), 0, []() {});
scheduler.set_timeout(&dummy_component, static_cast<uint32_t>(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);