diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index 51cbfb208ea..9ee3b2fdd2a 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -166,7 +166,13 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type item->component = component; item->set_name(name_type, static_name, hash_or_id); item->type = type; - item->callback = std::move(func); + // Use destroy + placement-new instead of move-assignment. + // GCC's std::function::operator=(function&&) does a full swap dance even when the + // target is empty. Since recycled/new items always have an empty callback, we can + // destroy the empty one (no-op) and move-construct directly, saving ~40 bytes of + // swap/destructor code on Xtensa. + item->callback.~function(); + new (&item->callback) std::function(std::move(func)); // Reset remove flag - recycled items may have been cancelled (remove=true) in previous use this->set_item_removed_(item, false); item->is_retry = is_retry; diff --git a/tests/benchmarks/components/binary_sensor/__init__.py b/tests/benchmarks/components/binary_sensor/__init__.py new file mode 100644 index 00000000000..b08f67a0956 --- /dev/null +++ b/tests/benchmarks/components/binary_sensor/__init__.py @@ -0,0 +1,5 @@ +from tests.testing_helpers import ComponentManifestOverride + + +def override_manifest(manifest: ComponentManifestOverride) -> None: + manifest.enable_codegen() diff --git a/tests/benchmarks/components/binary_sensor/bench_binary_sensor_publish.cpp b/tests/benchmarks/components/binary_sensor/bench_binary_sensor_publish.cpp new file mode 100644 index 00000000000..8bae943e2ea --- /dev/null +++ b/tests/benchmarks/components/binary_sensor/bench_binary_sensor_publish.cpp @@ -0,0 +1,61 @@ +#include + +#include "esphome/components/binary_sensor/binary_sensor.h" + +namespace esphome::binary_sensor::benchmarks { + +static constexpr int kInnerIterations = 2000; + +// Benchmark: publish_state with alternating values (forces state change every time) +static void BinarySensorPublish_Alternating(benchmark::State &state) { + BinarySensor sensor; + + // First publish to establish initial state + sensor.publish_initial_state(false); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + sensor.publish_state(i % 2 == 0); + } + benchmark::DoNotOptimize(sensor.state); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(BinarySensorPublish_Alternating); + +// Benchmark: publish_state with same value (tests dedup fast path) +static void BinarySensorPublish_NoChange(benchmark::State &state) { + BinarySensor sensor; + + sensor.publish_initial_state(true); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + sensor.publish_state(true); + } + benchmark::DoNotOptimize(sensor.state); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(BinarySensorPublish_NoChange); + +// Benchmark: publish_state with a callback registered +static void BinarySensorPublish_WithCallback(benchmark::State &state) { + BinarySensor sensor; + + int callback_count = 0; + sensor.add_on_state_callback([&callback_count](bool) { callback_count++; }); + + sensor.publish_initial_state(false); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + sensor.publish_state(i % 2 == 0); + } + benchmark::DoNotOptimize(callback_count); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(BinarySensorPublish_WithCallback); + +} // namespace esphome::binary_sensor::benchmarks diff --git a/tests/benchmarks/components/binary_sensor/benchmark.yaml b/tests/benchmarks/components/binary_sensor/benchmark.yaml new file mode 100644 index 00000000000..fc0db6c52c9 --- /dev/null +++ b/tests/benchmarks/components/binary_sensor/benchmark.yaml @@ -0,0 +1 @@ +binary_sensor: diff --git a/tests/benchmarks/components/sensor/__init__.py b/tests/benchmarks/components/sensor/__init__.py new file mode 100644 index 00000000000..5a593aa8c27 --- /dev/null +++ b/tests/benchmarks/components/sensor/__init__.py @@ -0,0 +1,12 @@ +import esphome.codegen as cg +from tests.testing_helpers import ComponentManifestOverride + + +def override_manifest(manifest: ComponentManifestOverride) -> None: + # Sensor filter benchmarks need USE_SENSOR_FILTER defined. + # We use a custom to_code instead of enable_codegen() to avoid + # pulling in the full sensor component setup. + async def to_code(config): + cg.add_define("USE_SENSOR_FILTER") + + manifest.to_code = to_code diff --git a/tests/benchmarks/components/sensor/bench_sensor_filter.cpp b/tests/benchmarks/components/sensor/bench_sensor_filter.cpp new file mode 100644 index 00000000000..e4aa3976901 --- /dev/null +++ b/tests/benchmarks/components/sensor/bench_sensor_filter.cpp @@ -0,0 +1,78 @@ +#include + +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/sensor/filter.h" + +namespace esphome::sensor::benchmarks { + +static constexpr int kInnerIterations = 2000; + +// Benchmark: sensor publish through a SlidingWindowMovingAverageFilter (window=5, send_every=1) +static void SensorFilter_SlidingWindowAvg(benchmark::State &state) { + Sensor sensor; + + // Create filter: window_size=5, send_every=1, send_first_at=1 + 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); +} +BENCHMARK(SensorFilter_SlidingWindowAvg); + +// Benchmark: sensor publish through ExponentialMovingAverageFilter +static void SensorFilter_ExponentialMovingAvg(benchmark::State &state) { + Sensor sensor; + + // alpha=0.1, send_every=1, send_first_at=1 + 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); +} +BENCHMARK(SensorFilter_ExponentialMovingAvg); + +// Benchmark: sensor publish through a chain of 3 filters (offset + multiply + sliding window) +static void SensorFilter_Chain3(benchmark::State &state) { + Sensor sensor; + + sensor.add_filters({ + new OffsetFilter(1.0f), + new MultiplyFilter(2.0f), + new SlidingWindowMovingAverageFilter(5, 1, 1), + }); + + 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); +} +BENCHMARK(SensorFilter_Chain3); + +} // namespace esphome::sensor::benchmarks diff --git a/tests/benchmarks/components/sensor/benchmark.yaml b/tests/benchmarks/components/sensor/benchmark.yaml new file mode 100644 index 00000000000..e1fb52cdd6a --- /dev/null +++ b/tests/benchmarks/components/sensor/benchmark.yaml @@ -0,0 +1 @@ +sensor: