Compare commits

...
3 changed files with 240 additions and 32 deletions

No files matched your search

@@ -1,5 +1,6 @@
from tests.testing_helpers import ComponentManifestOverride
from tests.testing_helpers import ComponentManifestOverride, add_defines_to_code
def override_manifest(manifest: ComponentManifestOverride) -> None:
manifest.enable_codegen()
# The filter benchmarks need filter.cpp, which is only built with this define
add_defines_to_code(manifest, "USE_BINARY_SENSOR_FILTER")
@@ -0,0 +1,101 @@
#include <benchmark/benchmark.h>
#include "esphome/components/binary_sensor/binary_sensor.h"
#include "esphome/components/binary_sensor/filter.h"
#include "esphome/core/application.h"
#include "esphome/core/hal.h"
namespace esphome::binary_sensor::benchmarks {
static constexpr int kInnerIterations = 2000;
// Long enough that no delay fires during a run, so every publish is a bounce
static constexpr uint32_t DELAY_MS = 3600000;
// Alternate the input with one scheduler pass per publish, like a bouncing GPIO polled each loop
static void publish_bounce(benchmark::State &state, BinarySensor &sensor, bool run_scheduler, bool bounce) {
sensor.publish_initial_state(false);
const uint32_t now = millis();
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
sensor.publish_state(bounce && i % 2 == 0);
if (run_scheduler)
App.scheduler.call(now);
}
benchmark::DoNotOptimize(sensor.state);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
// Resetting the filters cancels their pending timers before the filter leaves scope
sensor.invalidate_state();
App.scheduler.call(now);
}
// Benchmark: no filter, unchanged input; subtract from the delayed filters that hold their output
static void BinarySensorFilter_SchedulerBaseline(benchmark::State &state) {
BinarySensor sensor;
publish_bounce(state, sensor, /*run_scheduler=*/true, /*bounce=*/false);
}
BENCHMARK(BinarySensorFilter_SchedulerBaseline);
// Benchmark: invert, no scheduler pass since it schedules nothing, so not comparable to the five below
static void BinarySensorFilter_Invert(benchmark::State &state) {
BinarySensor sensor;
InvertFilter filter;
sensor.add_filter(&filter);
publish_bounce(state, sensor, /*run_scheduler=*/false, /*bounce=*/true);
}
BENCHMARK(BinarySensorFilter_Invert);
// Benchmark: delayed_on bounce, set_timeout on press and cancel on release
static void BinarySensorFilter_DelayedOn(benchmark::State &state) {
BinarySensor sensor;
DelayedOnFilter filter;
filter.set_delay([]() -> uint32_t { return DELAY_MS; });
sensor.add_filter(&filter);
publish_bounce(state, sensor, /*run_scheduler=*/true, /*bounce=*/true);
}
BENCHMARK(BinarySensorFilter_DelayedOn);
// Benchmark: delayed_off bounce, cancel on press and set_timeout on release
static void BinarySensorFilter_DelayedOff(benchmark::State &state) {
BinarySensor sensor;
DelayedOffFilter filter;
filter.set_delay([]() -> uint32_t { return DELAY_MS; });
sensor.add_filter(&filter);
publish_bounce(state, sensor, /*run_scheduler=*/true, /*bounce=*/true);
}
BENCHMARK(BinarySensorFilter_DelayedOff);
// Same shape as the function codegen emits for delayed_on_off with time_on and time_off
static optional<bool> delayed_on_off_fn(Filter *filter, bool x) {
return delay_output(filter, x ? DELAY_MS : DELAY_MS / 2, x);
}
// Benchmark: delayed_on_off bounce, every edge replaces the pending timeout
static void BinarySensorFilter_DelayedOnOff(benchmark::State &state) {
BinarySensor sensor;
ApplyFilter<delayed_on_off_fn> filter;
sensor.add_filter(&filter);
publish_bounce(state, sensor, /*run_scheduler=*/true, /*bounce=*/true);
}
BENCHMARK(BinarySensorFilter_DelayedOnOff);
// Benchmark: settle bounce, every edge re-arms the timeout
static void BinarySensorFilter_Settle(benchmark::State &state) {
BinarySensor sensor;
SettleFilter filter;
filter.set_delay([]() -> uint32_t { return DELAY_MS; });
sensor.add_filter(&filter);
publish_bounce(state, sensor, /*run_scheduler=*/true, /*bounce=*/true);
}
BENCHMARK(BinarySensorFilter_Settle);
// Benchmark: autorepeat, passes every press and release, so it also pays a state change per publish
static void BinarySensorFilter_Autorepeat(benchmark::State &state) {
BinarySensor sensor;
AutorepeatFilter<1> filter({{DELAY_MS, 100, 900}});
sensor.add_filter(&filter);
publish_bounce(state, sensor, /*run_scheduler=*/true, /*bounce=*/true);
}
BENCHMARK(BinarySensorFilter_Autorepeat);
} // namespace esphome::binary_sensor::benchmarks
@@ -2,10 +2,57 @@
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/sensor/filter.h"
#include "esphome/core/application.h"
#include "esphome/core/hal.h"
#include <iterator>
#include <limits>
namespace esphome::sensor::benchmarks {
static constexpr int kInnerIterations = 2000;
// Long enough that interval filters never fire during a run
static constexpr uint32_t NEVER_FIRE_MS = 3600000;
static constexpr float NO_MAX_DELTA = std::numeric_limits<float>::infinity();
static constexpr uint32_t LOOP_TICK_MS = 16;
static inline void advance_ramp(float &value) {
value += 0.1f;
if (value > 1000.0f)
value = 0.0f;
}
// Publish a 0..1000 ramp in 0.1 steps
static void publish_ramp(benchmark::State &state, Sensor &sensor) {
float value = 0.0f;
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
sensor.publish_state(value);
advance_ramp(value);
}
benchmark::DoNotOptimize(sensor.state);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
// Publish the ramp one loop tick apart from t=1, then restore the cached loop time for later benchmarks
static void publish_ramp_ticked(benchmark::State &state, Sensor &sensor) {
const uint32_t saved = App.get_loop_component_start_time();
uint32_t now = 1;
float value = 0.0f;
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
LoopBlockingGuard tick{nullptr, nullptr, now};
sensor.publish_state(value);
now += LOOP_TICK_MS;
advance_ramp(value);
}
benchmark::DoNotOptimize(sensor.state);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
LoopBlockingGuard restore{nullptr, nullptr, saved};
}
// Benchmark: sensor publish through a SlidingWindowMovingAverageFilter (window=5, send_every=1)
static void SensorFilter_SlidingWindowAvg(benchmark::State &state) {
@@ -15,17 +62,7 @@ static void SensorFilter_SlidingWindowAvg(benchmark::State &state) {
auto *filter = new SlidingWindowMovingAverageFilter(5, 1, 1);
sensor.add_filter(filter);
float value = 0.0f;
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
sensor.publish_state(value);
value += 0.1f;
if (value > 1000.0f)
value = 0.0f;
}
benchmark::DoNotOptimize(sensor.state);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
publish_ramp(state, sensor);
}
BENCHMARK(SensorFilter_SlidingWindowAvg);
@@ -37,17 +74,7 @@ static void SensorFilter_ExponentialMovingAvg(benchmark::State &state) {
auto *filter = new ExponentialMovingAverageFilter(0.1f, 1, 1);
sensor.add_filter(filter);
float value = 0.0f;
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
sensor.publish_state(value);
value += 0.1f;
if (value > 1000.0f)
value = 0.0f;
}
benchmark::DoNotOptimize(sensor.state);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
publish_ramp(state, sensor);
}
BENCHMARK(SensorFilter_ExponentialMovingAvg);
@@ -61,18 +88,97 @@ static void SensorFilter_Chain3(benchmark::State &state) {
new SlidingWindowMovingAverageFilter(5, 1, 1),
});
float value = 0.0f;
publish_ramp(state, sensor);
}
BENCHMARK(SensorFilter_Chain3);
// Benchmark: throttle 1s, one publish per 16ms loop tick (1 in 63 passes)
static void SensorFilter_Throttle(benchmark::State &state) {
Sensor sensor;
sensor.add_filter(new ThrottleFilter(1000));
publish_ramp_ticked(state, sensor);
}
BENCHMARK(SensorFilter_Throttle);
// Benchmark: delta 0.5 on a 0.1 ramp (1 in 5 passes)
static void SensorFilter_Delta(benchmark::State &state) {
Sensor sensor;
sensor.add_filter(new DeltaFilter(0.5f, 0.0f, NO_MAX_DELTA, 0.0f));
publish_ramp(state, sensor);
}
BENCHMARK(SensorFilter_Delta);
// Fixed zig-zag so the median window is rarely already sorted
static constexpr float MEDIAN_INPUT[] = {3.0f, 9.0f, 1.0f, 7.0f, 4.0f, 8.0f, 2.0f, 6.0f,
5.0f, 0.0f, 9.5f, 1.5f, 7.5f, 3.5f, 8.5f, 2.5f};
// Benchmark: median of 5, computed on every publish
static void SensorFilter_Median(benchmark::State &state) {
Sensor sensor;
sensor.add_filter(new MedianFilter(5, 1, 1));
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
sensor.publish_state(value);
value += 0.1f;
if (value > 1000.0f)
value = 0.0f;
}
for (int i = 0; i < kInnerIterations; i++)
sensor.publish_state(MEDIAN_INPUT[i % std::size(MEDIAN_INPUT)]);
benchmark::DoNotOptimize(sensor.state);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(SensorFilter_Chain3);
BENCHMARK(SensorFilter_Median);
// Cancel the interval so it never fires into the sensor after it leaves scope
static void publish_ramp_then_cancel(benchmark::State &state, Sensor &sensor, Filter *filter) {
publish_ramp(state, sensor);
App.scheduler.cancel_interval(filter);
App.scheduler.call(millis());
}
// Benchmark: throttle_average accumulate path (interval never fires)
static void SensorFilter_ThrottleAverage(benchmark::State &state) {
Sensor sensor;
auto *filter = new ThrottleAverageFilter(NEVER_FIRE_MS);
sensor.add_filter(filter);
publish_ramp_then_cancel(state, sensor, filter);
}
BENCHMARK(SensorFilter_ThrottleAverage);
// Benchmark: heartbeat store path (interval never fires)
static void SensorFilter_Heartbeat(benchmark::State &state) {
Sensor sensor;
auto *filter = new HeartbeatFilter(NEVER_FIRE_MS);
sensor.add_filter(filter);
publish_ramp_then_cancel(state, sensor, filter);
}
BENCHMARK(SensorFilter_Heartbeat);
// Same shape as the function codegen emits for a lambda filter
static optional<float> lambda_filter_fn(float x) {
if (x < 0.0f)
return {};
return x * 1.8f + 32.0f;
}
// Benchmark: lambda filter as generated (ApplyFilter)
static void SensorFilter_Lambda(benchmark::State &state) {
Sensor sensor;
sensor.add_filter(new ApplyFilter<lambda_filter_fn>());
publish_ramp(state, sensor);
}
BENCHMARK(SensorFilter_Lambda);
// Benchmark: round to 1 decimal
static void SensorFilter_Round(benchmark::State &state) {
Sensor sensor;
sensor.add_filter(new RoundFilter(1));
publish_ramp(state, sensor);
}
BENCHMARK(SensorFilter_Round);
// Benchmark: throttle 60s or delta 0.5 ("on change or every minute"), one publish per tick
static void SensorFilter_Or(benchmark::State &state) {
Sensor sensor;
sensor.add_filter(new OrFilter<2>(new ThrottleFilter(60000), new DeltaFilter(0.5f, 0.0f, NO_MAX_DELTA, 0.0f)));
publish_ramp_ticked(state, sensor);
}
BENCHMARK(SensorFilter_Or);
} // namespace esphome::sensor::benchmarks