mirror of
https://github.com/esphome/esphome.git
synced 2026-05-22 10:25:46 +08:00
[benchmarks] Add host platform benchmarks for number, select, and switch (#15405)
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
from tests.testing_helpers import ComponentManifestOverride
|
||||
|
||||
|
||||
def override_manifest(manifest: ComponentManifestOverride) -> None:
|
||||
manifest.enable_codegen()
|
||||
@@ -0,0 +1,121 @@
|
||||
#include <benchmark/benchmark.h>
|
||||
|
||||
#include "esphome/components/number/number.h"
|
||||
|
||||
namespace esphome::benchmarks {
|
||||
|
||||
// Inner iteration count to amortize CodSpeed instrumentation overhead.
|
||||
static constexpr int kInnerIterations = 2000;
|
||||
|
||||
// Minimal Number for benchmarking — control() publishes the value back.
|
||||
class BenchNumber : public number::Number {
|
||||
public:
|
||||
void configure(const char *name) { this->configure_entity_(name, 0x12345678, 0); }
|
||||
|
||||
protected:
|
||||
void control(float value) override { this->publish_state(value); }
|
||||
};
|
||||
|
||||
// Helper to create a typical number entity for benchmarks.
|
||||
static void setup_number(BenchNumber &number) {
|
||||
number.configure("test_number");
|
||||
number.traits.set_min_value(0.0f);
|
||||
number.traits.set_max_value(100.0f);
|
||||
number.traits.set_step(1.0f);
|
||||
number.traits.set_mode(number::NUMBER_MODE_SLIDER);
|
||||
}
|
||||
|
||||
// --- Number::publish_state() ---
|
||||
// Measures the publish path: set_has_state, store value, callback dispatch.
|
||||
|
||||
static void NumberPublish_State(benchmark::State &state) {
|
||||
BenchNumber number;
|
||||
setup_number(number);
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
number.publish_state(static_cast<float>(i % 100));
|
||||
}
|
||||
benchmark::DoNotOptimize(number.state);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(NumberPublish_State);
|
||||
|
||||
// --- Number::publish_state() with callback ---
|
||||
// Measures callback dispatch overhead.
|
||||
|
||||
static void NumberPublish_WithCallback(benchmark::State &state) {
|
||||
BenchNumber number;
|
||||
setup_number(number);
|
||||
|
||||
uint64_t callback_count = 0;
|
||||
number.add_on_state_callback([&callback_count](float) { callback_count++; });
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
number.publish_state(static_cast<float>(i % 100));
|
||||
}
|
||||
benchmark::DoNotOptimize(callback_count);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(NumberPublish_WithCallback);
|
||||
|
||||
// --- NumberCall::perform() set value ---
|
||||
// The most common number call — setting an absolute value.
|
||||
// Exercises: validation against min/max, control() dispatch.
|
||||
|
||||
static void NumberCall_SetValue(benchmark::State &state) {
|
||||
BenchNumber number;
|
||||
setup_number(number);
|
||||
number.publish_state(50.0f);
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
float val = static_cast<float>(i % 100);
|
||||
number.make_call().set_value(val).perform();
|
||||
}
|
||||
benchmark::DoNotOptimize(number.state);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(NumberCall_SetValue);
|
||||
|
||||
// --- NumberCall::perform() increment ---
|
||||
// Exercises: state read, step arithmetic, max clamping.
|
||||
|
||||
static void NumberCall_Increment(benchmark::State &state) {
|
||||
BenchNumber number;
|
||||
setup_number(number);
|
||||
number.publish_state(0.0f);
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
number.make_call().number_increment(true).perform();
|
||||
}
|
||||
benchmark::DoNotOptimize(number.state);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(NumberCall_Increment);
|
||||
|
||||
// --- NumberCall::perform() decrement ---
|
||||
// Exercises: state read, step arithmetic, min clamping.
|
||||
|
||||
static void NumberCall_Decrement(benchmark::State &state) {
|
||||
BenchNumber number;
|
||||
setup_number(number);
|
||||
number.publish_state(100.0f);
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
number.make_call().number_decrement(true).perform();
|
||||
}
|
||||
benchmark::DoNotOptimize(number.state);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(NumberCall_Decrement);
|
||||
|
||||
} // namespace esphome::benchmarks
|
||||
@@ -0,0 +1 @@
|
||||
number:
|
||||
@@ -0,0 +1,5 @@
|
||||
from tests.testing_helpers import ComponentManifestOverride
|
||||
|
||||
|
||||
def override_manifest(manifest: ComponentManifestOverride) -> None:
|
||||
manifest.enable_codegen()
|
||||
@@ -0,0 +1,157 @@
|
||||
#include <benchmark/benchmark.h>
|
||||
|
||||
#include "esphome/components/select/select.h"
|
||||
|
||||
namespace esphome::benchmarks {
|
||||
|
||||
// Inner iteration count to amortize CodSpeed instrumentation overhead.
|
||||
static constexpr int kInnerIterations = 2000;
|
||||
|
||||
// Minimal Select for benchmarking — control() publishes directly by index.
|
||||
class BenchSelect : public select::Select {
|
||||
public:
|
||||
void configure(const char *name) { this->configure_entity_(name, 0x12345678, 0); }
|
||||
|
||||
protected:
|
||||
void control(size_t index) override { this->publish_state(index); }
|
||||
};
|
||||
|
||||
// Helper to create a select with the given options.
|
||||
static void setup_select(BenchSelect &select, const char *name, std::initializer_list<const char *> options) {
|
||||
select.configure(name);
|
||||
select.traits.set_options(options);
|
||||
select.publish_state(size_t(0));
|
||||
}
|
||||
|
||||
// --- Select::publish_state(size_t) ---
|
||||
// The fast path: publish by index, no string lookup.
|
||||
|
||||
static void SelectPublish_ByIndex(benchmark::State &state) {
|
||||
BenchSelect select;
|
||||
setup_select(select, "test_select", {"off", "still", "move", "still+move"});
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
select.publish_state(static_cast<size_t>(i % 4));
|
||||
}
|
||||
benchmark::DoNotOptimize(select.active_index());
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(SelectPublish_ByIndex);
|
||||
|
||||
// --- Select::publish_state(const char *) ---
|
||||
// The string path: requires index_of() lookup via strncmp.
|
||||
|
||||
static void SelectPublish_ByString(benchmark::State &state) {
|
||||
BenchSelect select;
|
||||
setup_select(select, "test_select", {"off", "still", "move", "still+move"});
|
||||
|
||||
const char *options[] = {"off", "still", "move", "still+move"};
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
select.publish_state(options[i % 4]);
|
||||
}
|
||||
benchmark::DoNotOptimize(select.active_index());
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(SelectPublish_ByString);
|
||||
|
||||
// --- Select::publish_state() with callback ---
|
||||
// Measures callback dispatch overhead on the index path.
|
||||
|
||||
static void SelectPublish_WithCallback(benchmark::State &state) {
|
||||
BenchSelect select;
|
||||
setup_select(select, "test_select", {"off", "still", "move", "still+move"});
|
||||
|
||||
uint64_t callback_count = 0;
|
||||
select.add_on_state_callback([&callback_count](size_t) { callback_count++; });
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
select.publish_state(static_cast<size_t>(i % 4));
|
||||
}
|
||||
benchmark::DoNotOptimize(callback_count);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(SelectPublish_WithCallback);
|
||||
|
||||
// --- SelectCall::perform() set by index ---
|
||||
// The fast call path — no string matching needed.
|
||||
|
||||
static void SelectCall_SetByIndex(benchmark::State &state) {
|
||||
BenchSelect select;
|
||||
setup_select(select, "test_select", {"off", "still", "move", "still+move"});
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
select.make_call().set_index(i % 4).perform();
|
||||
}
|
||||
benchmark::DoNotOptimize(select.active_index());
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(SelectCall_SetByIndex);
|
||||
|
||||
// --- SelectCall::perform() set by option string ---
|
||||
// Exercises the string lookup path through index_of().
|
||||
|
||||
static void SelectCall_SetByOption(benchmark::State &state) {
|
||||
BenchSelect select;
|
||||
setup_select(select, "test_select", {"off", "still", "move", "still+move"});
|
||||
|
||||
const char *options[] = {"off", "still", "move", "still+move"};
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
select.make_call().set_option(options[i % 4]).perform();
|
||||
}
|
||||
benchmark::DoNotOptimize(select.active_index());
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(SelectCall_SetByOption);
|
||||
|
||||
// --- SelectCall::perform() next with cycling ---
|
||||
// Exercises the navigation path through active_index_.
|
||||
|
||||
static void SelectCall_NextCycle(benchmark::State &state) {
|
||||
BenchSelect select;
|
||||
setup_select(select, "test_select", {"off", "still", "move", "still+move"});
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
select.make_call().select_next(true).perform();
|
||||
}
|
||||
benchmark::DoNotOptimize(select.active_index());
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(SelectCall_NextCycle);
|
||||
|
||||
// --- SelectCall with 10 options (string lookup) ---
|
||||
// Worst-case string matching with more options.
|
||||
|
||||
static void SelectCall_SetByOption_10Options(benchmark::State &state) {
|
||||
BenchSelect select;
|
||||
setup_select(
|
||||
select, "test_select",
|
||||
{"off", "still", "move", "still+move", "custom1", "custom2", "custom3", "custom4", "custom5", "custom6"});
|
||||
|
||||
// Pick options spread across the list to exercise different search depths
|
||||
const char *picks[] = {"off", "custom3", "custom6", "move"};
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
select.make_call().set_option(picks[i % 4]).perform();
|
||||
}
|
||||
benchmark::DoNotOptimize(select.active_index());
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(SelectCall_SetByOption_10Options);
|
||||
|
||||
} // namespace esphome::benchmarks
|
||||
@@ -0,0 +1 @@
|
||||
select:
|
||||
@@ -0,0 +1,5 @@
|
||||
from tests.testing_helpers import ComponentManifestOverride
|
||||
|
||||
|
||||
def override_manifest(manifest: ComponentManifestOverride) -> None:
|
||||
manifest.enable_codegen()
|
||||
@@ -0,0 +1,137 @@
|
||||
#include <benchmark/benchmark.h>
|
||||
|
||||
#include "esphome/components/switch/switch.h"
|
||||
|
||||
namespace esphome::benchmarks {
|
||||
|
||||
// Inner iteration count to amortize CodSpeed instrumentation overhead.
|
||||
static constexpr int kInnerIterations = 2000;
|
||||
|
||||
// Minimal Switch for benchmarking — write_state() publishes directly.
|
||||
class BenchSwitch : public switch_::Switch {
|
||||
public:
|
||||
void configure(const char *name) { this->configure_entity_(name, 0x12345678, 0); }
|
||||
|
||||
protected:
|
||||
void write_state(bool state) override { this->publish_state(state); }
|
||||
};
|
||||
|
||||
// --- Switch::publish_state() alternating ---
|
||||
// Forces state change every call, exercising the full publish path.
|
||||
|
||||
static void SwitchPublish_Alternating(benchmark::State &state) {
|
||||
BenchSwitch sw;
|
||||
sw.configure("test_switch");
|
||||
sw.set_restore_mode(switch_::SWITCH_ALWAYS_OFF);
|
||||
sw.publish_state(false);
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
sw.publish_state(i % 2 == 0);
|
||||
}
|
||||
benchmark::DoNotOptimize(sw.state);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(SwitchPublish_Alternating);
|
||||
|
||||
// --- Switch::publish_state() no change ---
|
||||
// Tests the deduplication fast path in publish_dedup_.
|
||||
|
||||
static void SwitchPublish_NoChange(benchmark::State &state) {
|
||||
BenchSwitch sw;
|
||||
sw.configure("test_switch");
|
||||
sw.set_restore_mode(switch_::SWITCH_ALWAYS_OFF);
|
||||
sw.publish_state(true);
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
sw.publish_state(true);
|
||||
}
|
||||
benchmark::DoNotOptimize(sw.state);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(SwitchPublish_NoChange);
|
||||
|
||||
// --- Switch::publish_state() with callback ---
|
||||
// Measures callback dispatch overhead on state changes.
|
||||
|
||||
static void SwitchPublish_WithCallback(benchmark::State &state) {
|
||||
BenchSwitch sw;
|
||||
sw.configure("test_switch");
|
||||
sw.set_restore_mode(switch_::SWITCH_ALWAYS_OFF);
|
||||
|
||||
uint64_t callback_count = 0;
|
||||
sw.add_on_state_callback([&callback_count](bool) { callback_count++; });
|
||||
sw.publish_state(false);
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
sw.publish_state(i % 2 == 0);
|
||||
}
|
||||
benchmark::DoNotOptimize(callback_count);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(SwitchPublish_WithCallback);
|
||||
|
||||
// --- Switch::turn_on() / turn_off() ---
|
||||
// The front-end call path: turn_on → write_state → publish_state.
|
||||
|
||||
static void SwitchTurnOn(benchmark::State &state) {
|
||||
BenchSwitch sw;
|
||||
sw.configure("test_switch");
|
||||
sw.set_restore_mode(switch_::SWITCH_ALWAYS_OFF);
|
||||
sw.publish_state(false);
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
sw.turn_on();
|
||||
}
|
||||
benchmark::DoNotOptimize(sw.state);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(SwitchTurnOn);
|
||||
|
||||
// --- Switch::toggle() alternating ---
|
||||
// Exercises the toggle path which reads current state to determine target.
|
||||
|
||||
static void SwitchToggle(benchmark::State &state) {
|
||||
BenchSwitch sw;
|
||||
sw.configure("test_switch");
|
||||
sw.set_restore_mode(switch_::SWITCH_ALWAYS_OFF);
|
||||
sw.publish_state(false);
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
sw.toggle();
|
||||
}
|
||||
benchmark::DoNotOptimize(sw.state);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(SwitchToggle);
|
||||
|
||||
// --- Switch::publish_state() inverted ---
|
||||
// Verifies the inversion path doesn't add significant overhead.
|
||||
|
||||
static void SwitchPublish_Inverted(benchmark::State &state) {
|
||||
BenchSwitch sw;
|
||||
sw.configure("test_switch");
|
||||
sw.set_restore_mode(switch_::SWITCH_ALWAYS_OFF);
|
||||
sw.set_inverted(true);
|
||||
sw.publish_state(false);
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
sw.publish_state(i % 2 == 0);
|
||||
}
|
||||
benchmark::DoNotOptimize(sw.state);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(SwitchPublish_Inverted);
|
||||
|
||||
} // namespace esphome::benchmarks
|
||||
@@ -0,0 +1 @@
|
||||
switch:
|
||||
Reference in New Issue
Block a user