mirror of
https://github.com/esphome/esphome.git
synced 2026-09-01 10:36:01 +00:00
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
168 lines
5.8 KiB
C++
168 lines
5.8 KiB
C++
#include <benchmark/benchmark.h>
|
|
|
|
#include "esphome/core/scheduler.h"
|
|
#include "esphome/core/hal.h"
|
|
|
|
namespace esphome::benchmarks {
|
|
|
|
// Inner iteration count to amortize CodSpeed instrumentation overhead.
|
|
// Without this, the ~60ns per-iteration valgrind start/stop cost dominates
|
|
// sub-microsecond benchmarks.
|
|
static constexpr int kInnerIterations = 2000;
|
|
|
|
// --- Scheduler fast path: no work to do ---
|
|
|
|
static void Scheduler_Call_NoWork(benchmark::State &state) {
|
|
Scheduler scheduler;
|
|
uint32_t now = millis();
|
|
|
|
for (auto _ : state) {
|
|
for (int i = 0; i < kInnerIterations; i++) {
|
|
scheduler.call(now);
|
|
}
|
|
benchmark::DoNotOptimize(now);
|
|
}
|
|
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
|
}
|
|
BENCHMARK(Scheduler_Call_NoWork);
|
|
|
|
// --- Scheduler with timers: call() when timers exist but aren't due ---
|
|
|
|
static void Scheduler_Call_TimersNotDue(benchmark::State &state) {
|
|
Scheduler scheduler;
|
|
Component dummy_component;
|
|
|
|
// Add some timeouts far in the future
|
|
for (int i = 0; i < 10; i++) {
|
|
scheduler.set_timeout(&dummy_component, static_cast<uint32_t>(i), 1000000, []() {});
|
|
}
|
|
scheduler.process_to_add();
|
|
|
|
uint32_t now = millis();
|
|
|
|
for (auto _ : state) {
|
|
for (int i = 0; i < kInnerIterations; i++) {
|
|
scheduler.call(now);
|
|
}
|
|
benchmark::DoNotOptimize(now);
|
|
}
|
|
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
|
}
|
|
BENCHMARK(Scheduler_Call_TimersNotDue);
|
|
|
|
// --- Scheduler with 5 intervals firing every call ---
|
|
|
|
static void Scheduler_Call_5IntervalsFiring(benchmark::State &state) {
|
|
Scheduler scheduler;
|
|
Component dummy_component;
|
|
int fire_count = 0;
|
|
|
|
// Benchmarks the heap-based scheduler dispatch with 5 callbacks firing.
|
|
// Uses monotonically increasing fake time so intervals reliably fire every call.
|
|
// USE_BENCHMARK ifdef in component.h disables WarnIfComponentBlockingGuard
|
|
// (fake now > real millis() would cause underflow in finish()).
|
|
// interval=0 would cause an infinite loop (reschedules at same now).
|
|
for (int i = 0; i < 5; i++) {
|
|
scheduler.set_interval(&dummy_component, static_cast<uint32_t>(i), 1, [&fire_count]() { fire_count++; });
|
|
}
|
|
scheduler.process_to_add();
|
|
|
|
uint32_t now = millis() + 100;
|
|
|
|
for (auto _ : state) {
|
|
scheduler.call(now);
|
|
now++;
|
|
benchmark::DoNotOptimize(fire_count);
|
|
}
|
|
}
|
|
BENCHMARK(Scheduler_Call_5IntervalsFiring);
|
|
|
|
// --- Scheduler: set_timeout registration ---
|
|
|
|
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 % 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);
|
|
}
|
|
}
|
|
// Final drain in case kInnerIterations is not a multiple of kKeyCount
|
|
scheduler.call(++now);
|
|
benchmark::DoNotOptimize(scheduler);
|
|
}
|
|
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
|
}
|
|
BENCHMARK(Scheduler_SetTimeout);
|
|
|
|
// --- Scheduler: set_interval registration ---
|
|
|
|
static void Scheduler_SetInterval(benchmark::State &state) {
|
|
Scheduler scheduler;
|
|
Component dummy_component;
|
|
// Number of distinct interval 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_interval(&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);
|
|
}
|
|
}
|
|
// Final drain in case kInnerIterations is not a multiple of kKeyCount
|
|
scheduler.call(++now);
|
|
benchmark::DoNotOptimize(scheduler);
|
|
}
|
|
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
|
}
|
|
BENCHMARK(Scheduler_SetInterval);
|
|
|
|
// --- Scheduler: defer registration (set_timeout with delay=0) ---
|
|
|
|
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 % kKeyCount), 0, []() {});
|
|
if ((i + 1) % kKeyCount == 0) {
|
|
scheduler.call(++now);
|
|
}
|
|
}
|
|
// Final drain in case kInnerIterations is not a multiple of kKeyCount
|
|
scheduler.call(++now);
|
|
benchmark::DoNotOptimize(scheduler);
|
|
}
|
|
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
|
}
|
|
BENCHMARK(Scheduler_Defer);
|
|
|
|
} // namespace esphome::benchmarks
|