mirror of
https://github.com/esphome/esphome.git
synced 2026-11-15 09:36:04 +00:00
Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
890c300125 | ||
|
|
a5bb8a0cbc | ||
|
|
ecd14a2ea7 | ||
|
|
a9bf6f9f10 | ||
|
|
1fbd657cbc | ||
|
|
0f1899e921 |
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
|
||||
Reference in new issue
Block a user