Compare commits

..
9 changed files with 275 additions and 338 deletions

No files matched your search

-3
View File
@@ -25,8 +25,6 @@ CORE_BENCHMARKS_DIR: Path = Path(root_path) / "tests" / "benchmarks" / "core"
# allow benchmarks to compile on the host platform.
STUBS_DIR: Path = Path(root_path) / "tests" / "benchmarks" / "stubs"
HELPERS_DIR: Path = Path(root_path) / "tests" / "benchmarks" / "helpers"
BUILD_FLAGS = [
"-Os", # match firmware optimization level (detects inlining regressions)
"-g", # debug symbols for profiling
@@ -34,7 +32,6 @@ BUILD_FLAGS = [
"-fdata-sections", # required for dead-code stripping with -Os
"-DUSE_BENCHMARK", # disable WarnIfComponentBlockingGuard in finish()
f"-I{STUBS_DIR}", # stub headers for ESP32-only components
f"-I{HELPERS_DIR}", # shared test doubles such as fake_uart.h
]
@@ -0,0 +1,271 @@
#include <benchmark/benchmark.h>
#include "esphome/components/binary_sensor/binary_sensor.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/switch/switch.h"
#include "esphome/core/application.h"
#include "esphome/core/automation.h"
#include "esphome/core/base_automation.h"
#include "esphome/core/hal.h"
namespace esphome::benchmarks {
// The generated shapes copied here are pinned by tests/unit_tests/test_automation.py; update both together.
// Inner iteration count to amortize CodSpeed instrumentation overhead.
static constexpr int kInnerIterations = 2000;
static switch_::Switch *bench_switch = nullptr;
static binary_sensor::BinarySensor *bench_binary_sensor = nullptr;
static uint32_t bench_counter = 0;
// Optimistic switch: write_state() publishes directly, like an optimistic template switch.
class AutomationBenchSwitch : public switch_::Switch {
public:
AutomationBenchSwitch() {
this->configure_entity_("bench_switch", 0x12345678, 0);
this->set_restore_mode(switch_::SWITCH_ALWAYS_OFF);
this->publish_state(false);
bench_switch = this;
}
protected:
void write_state(bool state) override { this->publish_state(state); }
};
static void setup_binary_sensor(binary_sensor::BinarySensor &binary_sensor, bool state) {
binary_sensor.publish_initial_state(state);
bench_binary_sensor = &binary_sensor;
}
// The functions below copy the shapes codegen emits in main.cpp.
static void toggle_fn(const float &x) { bench_switch->toggle(); }
static void toggle_no_args_fn() { bench_switch->toggle(); }
static void counter_fn(const float &x) { bench_counter++; }
static void merged_toggles_fn(const int &esphome__running, const float &x) {
{
bench_switch->toggle();
}
if (!esphome__running)
return;
{
bench_switch->toggle();
}
if (!esphome__running)
return;
{
bench_switch->toggle();
}
}
static void if_lambda_fn(const float &x) {
{
{
const bool esphome__condition = ([](float x) -> bool { return x > 10; }(x));
if (esphome__condition) {
{
bench_switch->turn_on();
}
} else {
{
bench_switch->turn_off();
}
}
}
}
}
static void if_state_fn(const float &x) {
{
{
const bool esphome__condition = (bench_binary_sensor->state);
if (esphome__condition) {
{
bench_switch->toggle();
}
}
}
}
}
// --- on_value: [lambda: 'id(counter)++;'] ---
// Dispatch only: the direct function bumps a counter, with no switch publishing.
static void Automation_DispatchOnly(benchmark::State &state) {
sensor::Sensor sensor;
sensor.add_on_state_callback([](const float &x) -> void { counter_fn(x); });
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
sensor.publish_state(static_cast<float>(i));
}
benchmark::DoNotOptimize(bench_counter);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(Automation_DispatchOnly);
// --- on_value: [switch.toggle] ---
// One inline action: the callback calls one direct function, no Automation object.
static void Automation_SensorOnValue(benchmark::State &state) {
AutomationBenchSwitch sw;
sensor::Sensor sensor;
sensor.add_on_state_callback([](const float &x) -> void { toggle_fn(x); });
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
sensor.publish_state(static_cast<float>(i));
}
benchmark::DoNotOptimize(sw.state);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(Automation_SensorOnValue);
// --- on_value with automation_id: [switch.toggle x3] ---
// The toggles merge into one ApplyRunAction in an Automation; without automation_id they inline into one function.
static void Automation_MergedRun(benchmark::State &state) {
AutomationBenchSwitch sw;
sensor::Sensor sensor;
Automation<float> automation;
ApplyRunAction<merged_toggles_fn, float> run;
automation.add_action(&run);
sensor.add_on_state_callback(TriggerForwarder<float>{&automation});
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
sensor.publish_state(static_cast<float>(i));
}
benchmark::DoNotOptimize(sw.state);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(Automation_MergedRun);
// --- on_value: [if: {condition: lambda, then: [switch.turn_on], else: [switch.turn_off]}] ---
// Inline branches become statements in one direct function; alternating values flip the switch.
static void Automation_IfInline(benchmark::State &state) {
AutomationBenchSwitch sw;
sensor::Sensor sensor;
sensor.add_on_state_callback([](const float &x) -> void { if_lambda_fn(x); });
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
sensor.publish_state(i % 2 == 0 ? 20.0f : 0.0f);
}
benchmark::DoNotOptimize(sw.state);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(Automation_IfInline);
// --- on_value: [if: {condition: binary_sensor.is_on, then: [switch.toggle]}] ---
// The condition reads the entity state inline in the direct function.
static void Automation_IfStateInline(benchmark::State &state) {
AutomationBenchSwitch sw;
binary_sensor::BinarySensor binary_sensor;
setup_binary_sensor(binary_sensor, true);
sensor::Sensor sensor;
sensor.add_on_state_callback([](const float &x) -> void { if_state_fn(x); });
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
sensor.publish_state(static_cast<float>(i));
}
benchmark::DoNotOptimize(sw.state);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(Automation_IfStateInline);
// --- on_value: [if: {condition: lambda, then: [delay: 1s, switch.toggle], else: [switch.toggle]}] ---
// The delay forces the IfAction object path; values stay low so only the else branch runs.
static void Automation_IfActionObject(benchmark::State &state) {
AutomationBenchSwitch sw;
sensor::Sensor sensor;
DelayAction<float> delay;
delay.set_delay([](float x) -> uint32_t { return 1000; });
StatelessLambdaCondition<float> condition([](float x) -> bool { return x > 10; });
IfAction<true, float> if_action(&condition);
ApplyAction<toggle_fn, float> then_toggle;
ApplyAction<toggle_fn, float> else_toggle;
if_action.add_then_action(&delay);
if_action.add_then_action(&then_toggle);
if_action.finish_then();
if_action.add_else_action(&else_toggle);
if_action.finish_else();
Automation<float> automation;
automation.add_action(&if_action);
sensor.add_on_state_callback(TriggerForwarder<float>{&automation});
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
sensor.publish_state(static_cast<float>(i % 10));
}
benchmark::DoNotOptimize(sw.state);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(Automation_IfActionObject);
// --- on_value: [delay: 1s, ...] ---
// Schedules a delay end to end, scheduler included; the delay cannot fire on host, so stop() cancels it.
static void Automation_DelaySchedule(benchmark::State &state) {
sensor::Sensor sensor;
DelayAction<float> delay;
delay.set_delay([](float x) -> uint32_t { return 1000; });
Automation<float> automation;
automation.add_action(&delay);
sensor.add_on_state_callback(TriggerForwarder<float>{&automation});
uint32_t now = millis();
sensor.publish_state(0.0f);
automation.stop();
App.scheduler.call(++now);
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
sensor.publish_state(static_cast<float>(i));
automation.stop();
App.scheduler.call(++now);
}
benchmark::DoNotOptimize(automation);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(Automation_DelaySchedule);
// --- binary_sensor on_press: [switch.toggle] ---
// Alternating states; the callback returns early on release and calls a no-argument function on press.
static void Automation_BinarySensorOnPress(benchmark::State &state) {
AutomationBenchSwitch sw;
binary_sensor::BinarySensor binary_sensor;
setup_binary_sensor(binary_sensor, false);
binary_sensor.add_on_state_callback([](const bool &state) -> void {
if (!(state))
return;
toggle_no_args_fn();
});
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
binary_sensor.publish_state(i % 2 == 0);
}
benchmark::DoNotOptimize(sw.state);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(Automation_BinarySensorOnPress);
} // namespace esphome::benchmarks
@@ -0,0 +1,4 @@
# Entity domains the automation benchmarks fire and act on.
binary_sensor:
sensor:
switch:
@@ -1,76 +0,0 @@
#include <benchmark/benchmark.h>
#include "esphome/components/binary_sensor/binary_sensor.h"
#include "esphome/components/ld2450/ld2450.h"
#include "esphome/components/sensor/sensor.h"
#include "fake_uart.h"
namespace esphome::ld2450::benchmarks {
using esphome::benchmarks::FakeUART;
// Inner iteration count to amortize CodSpeed instrumentation overhead.
static constexpr int kInnerIterations = 2000;
static constexpr size_t FRAME_SIZE = 30;
// Periodic frames with one moving target at two positions
static constexpr uint8_t FRAME_A[FRAME_SIZE] = {0xAA, 0xFF, 0x03, 0x00, 0x0E, 0x03, 0xB1, 0x86, 0x10, 0x00,
0x40, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x55, 0xCC};
static constexpr uint8_t FRAME_B[FRAME_SIZE] = {0xAA, 0xFF, 0x03, 0x00, 0x2A, 0x03, 0xC8, 0x86, 0x10, 0x00,
0x40, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x55, 0xCC};
// --- loop() parsing one periodic frame, no entities attached ---
static void LD2450_PeriodicFrame(benchmark::State &state) {
FakeUART uart;
LD2450Component radar;
radar.set_uart_parent(&uart);
radar.set_link_timeout(60000);
// Bring the link up outside the timed loop
uart.load(FRAME_B, FRAME_SIZE);
radar.loop();
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
uart.load(FRAME_A, FRAME_SIZE);
radar.loop();
}
benchmark::DoNotOptimize(uart.available());
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(LD2450_PeriodicFrame);
// --- loop() parsing a moving target and publishing its sensors ---
static void LD2450_PeriodicFrameSensors(benchmark::State &state) {
FakeUART uart;
LD2450Component radar;
radar.set_uart_parent(&uart);
radar.set_link_timeout(60000);
sensor::Sensor x, y, speed, distance, angle, count;
binary_sensor::BinarySensor presence;
radar.set_move_x_sensor(0, &x);
radar.set_move_y_sensor(0, &y);
radar.set_move_speed_sensor(0, &speed);
radar.set_move_distance_sensor(0, &distance);
radar.set_move_angle_sensor(0, &angle);
radar.set_target_count_sensor(&count);
radar.set_target_binary_sensor(&presence);
uart.load(FRAME_B, FRAME_SIZE);
radar.loop();
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
uart.load((i & 1) ? FRAME_B : FRAME_A, FRAME_SIZE);
radar.loop();
}
benchmark::DoNotOptimize(x.state);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(LD2450_PeriodicFrameSensors);
} // namespace esphome::ld2450::benchmarks
@@ -1,2 +0,0 @@
sensor:
binary_sensor:
@@ -1,63 +0,0 @@
#include <benchmark/benchmark.h>
#include "esphome/components/modbus/modbus.h"
#include "fake_uart.h"
namespace esphome::modbus::benchmarks {
using esphome::benchmarks::FakeUART;
// Inner iteration count to amortize CodSpeed instrumentation overhead.
static constexpr int kInnerIterations = 2000;
static constexpr uint8_t ADDRESS = 1;
static constexpr uint16_t REGISTER_COUNT = 10;
// Read 10 holding registers from address 0, as an energy meter poll does
static constexpr uint8_t REQUEST_PDU[] = {0x03, 0x00, 0x00, 0x00, REGISTER_COUNT};
// Function 3, byte count 20, 10 registers
static constexpr uint8_t RESPONSE_PDU[] = {0x03, 0x14, 0x09, 0x1F, 0x00, 0x00, 0x01, 0xF4, 0x00, 0x00, 0x13,
0x88, 0x00, 0x32, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x64, 0x12, 0x34};
class SumDevice : public ModbusClientDevice {
public:
using ModbusClientDevice::ModbusClientDevice;
void on_read_holding_registers(uint16_t start_address, std::span<const uint16_t> registers,
ResponseStatus status) override {
for (uint16_t value : registers)
this->sum += value;
}
uint32_t sum{0};
};
class NoDelayHub : public ModbusClientHub {
protected:
// No wait before a send, so every loop() can send the next poll
int32_t tx_delay_remaining() override { return 0; }
};
// --- One continuous poll cycle per loop(): receive, decode and resend ---
static void Modbus_ReadHoldingPollCycle(benchmark::State &state) {
const ModbusFrame response(ADDRESS, RESPONSE_PDU, sizeof(RESPONSE_PDU));
FakeUART uart;
uart.set_baud_rate(9600);
NoDelayHub hub;
hub.set_uart_parent(&uart);
hub.setup();
SumDevice device(&hub, ADDRESS);
hub.queue_pdu(ADDRESS, REQUEST_PDU, &device, {.continuous = true});
// The first loop() sends the poll; every send rewinds the response, so the device answers each one
hub.loop();
uart.load(response.data.data(), response.size());
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
hub.loop();
}
benchmark::DoNotOptimize(device.sum);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(Modbus_ReadHoldingPollCycle);
} // namespace esphome::modbus::benchmarks
@@ -1,56 +0,0 @@
#include <benchmark/benchmark.h>
#include "esphome/components/uart/uart.h"
#include "fake_uart.h"
namespace esphome::uart::benchmarks {
using esphome::benchmarks::FakeUART;
// Inner iteration count to amortize CodSpeed instrumentation overhead.
static constexpr int kInnerIterations = 2000;
// One second of a GPS module's NMEA output
static constexpr char NMEA_BURST[] = "$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47\r\n"
"$GPRMC,123519,A,4807.038,N,01131.000,E,022.4,084.4,230394,003.1,W*6A\r\n"
"$GPGSA,A,3,04,05,,09,12,,,24,,,,,2.5,1.3,2.1*39\r\n"
"$GPGSV,2,1,08,01,40,083,46,02,17,308,41,12,07,344,39,14,22,228,45*75\r\n"
"$GPVTG,054.7,T,034.4,M,005.5,N,010.2,K*48\r\n";
// --- read_available(): drain a whole NMEA burst through a byte-sum callback ---
static void UART_ReadAvailable_NmeaBurst(benchmark::State &state) {
FakeUART uart;
UARTDevice dev(&uart);
uint32_t sum = 0;
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
uart.load(reinterpret_cast<const uint8_t *>(NMEA_BURST), sizeof(NMEA_BURST) - 1);
dev.read_available([&](uint8_t b) { sum += b; });
}
benchmark::DoNotOptimize(sum);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(UART_ReadAvailable_NmeaBurst);
// --- discard_available(): drop a 256 byte backlog, two virtual calls per byte ---
static void UART_DiscardAvailable(benchmark::State &state) {
uint8_t backlog[256]{};
FakeUART uart;
UARTDevice dev(&uart);
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
uart.load(backlog, sizeof(backlog));
dev.discard_available();
}
benchmark::DoNotOptimize(uart.available());
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(UART_DiscardAvailable);
} // namespace esphome::uart::benchmarks
@@ -1,96 +0,0 @@
#include <benchmark/benchmark.h>
#include "esphome/core/frame_reader.h"
#include "esphome/core/helpers.h"
#include "esphome/core/line_reader.h"
namespace esphome::benchmarks {
// Inner iteration count to amortize CodSpeed instrumentation overhead.
static constexpr int kInnerIterations = 2000;
static constexpr char NMEA_GGA[] = "$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47\r\n";
// A 30 byte frame with a 4 byte header and a 2 byte tail
static constexpr uint8_t FRAME_30[30] = {0xAA, 0xFF, 0x03, 0x00, 0x0E, 0x03, 0xB1, 0x86, 0x10, 0x00,
0x40, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x55, 0xCC};
// Four A02YYUW frames among noise; the doubled 0xFF fails the checksum and resyncs onto the next frame
static constexpr uint8_t ULTRASONIC_NOISY[] = {0x12, 0xFF, 0x07, 0xA1, 0xA7, 0x34, 0xFF, 0xFF, 0x07, 0xA1, 0xA7,
0x00, 0xFF, 0x07, 0xA1, 0xA7, 0x56, 0xFF, 0x07, 0xA1, 0xA7};
// --- LineReader: one GGA sentence fed byte by byte ---
static void LineReader_NmeaLine(benchmark::State &state) {
LineReader<96> reader;
const auto *bytes = reinterpret_cast<const uint8_t *>(NMEA_GGA);
uint32_t lines = 0;
for (auto _ : state) {
benchmark::DoNotOptimize(bytes);
for (int i = 0; i < kInnerIterations; i++) {
for (size_t j = 0; j < sizeof(NMEA_GGA) - 1; j++) {
char *line = reader.feed(bytes[j]);
if (line == nullptr)
continue;
// Keeps the line copy and its NUL write
benchmark::DoNotOptimize(line);
lines++;
}
}
benchmark::DoNotOptimize(lines);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(LineReader_NmeaLine);
// --- FrameReader: a 30 byte frame behind a 4 byte header ---
static void FrameReader_30ByteFrame(benchmark::State &state) {
FrameReader<sizeof(FRAME_30), 0xAA, 0xFF, 0x03, 0x00> reader;
const uint8_t *bytes = FRAME_30;
uint32_t frames = 0;
for (auto _ : state) {
benchmark::DoNotOptimize(bytes);
for (int i = 0; i < kInnerIterations; i++) {
for (size_t j = 0; j < sizeof(FRAME_30); j++) {
if (reader.feed(bytes[j]) != nullptr)
frames++;
}
}
benchmark::DoNotOptimize(frames);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(FrameReader_30ByteFrame);
// --- FrameReader: 4 byte checksummed frames, skipping noise and resyncing on a bad checksum ---
static void FrameReader_UltrasonicNoisy(benchmark::State &state) {
FrameReader<4, 0xFF> reader;
const uint8_t *bytes = ULTRASONIC_NOISY;
uint32_t frames = 0;
for (auto _ : state) {
benchmark::DoNotOptimize(bytes);
for (int i = 0; i < kInnerIterations; i++) {
for (size_t j = 0; j < sizeof(ULTRASONIC_NOISY); j++) {
const uint8_t *frame = reader.feed(bytes[j]);
if (frame == nullptr)
continue;
if (frame[3] == sum_bytes(frame, 3)) {
frames++;
} else {
reader.resync();
}
}
}
benchmark::DoNotOptimize(frames);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(FrameReader_UltrasonicNoisy);
} // namespace esphome::benchmarks
-42
View File
@@ -1,42 +0,0 @@
#pragma once
#include <cstdint>
#include <cstring>
#include "esphome/components/uart/uart_component.h"
namespace esphome::benchmarks {
// Serves a fixed byte stream as received data. Every write rewinds it, so the stream answers each request.
class FakeUART : public uart::UARTComponent {
public:
void load(const uint8_t *data, size_t len) {
this->data_ = data;
this->len_ = len;
this->pos_ = 0;
}
void write_array(const uint8_t *data, size_t len) override { this->pos_ = 0; }
bool peek_byte(uint8_t *data) override {
if (this->pos_ >= this->len_)
return false;
*data = this->data_[this->pos_];
return true;
}
bool read_array(uint8_t *data, size_t len) override {
if (len > this->len_ - this->pos_)
return false;
std::memcpy(data, this->data_ + this->pos_, len);
this->pos_ += len;
return true;
}
size_t available() override { return this->len_ - this->pos_; }
uart::UARTFlushResult flush() override { return uart::UARTFlushResult::UART_FLUSH_RESULT_ASSUMED_SUCCESS; }
void check_logger_conflict() override {}
protected:
const uint8_t *data_{nullptr};
size_t len_{0};
size_t pos_{0};
};
} // namespace esphome::benchmarks