mirror of
https://github.com/esphome/esphome.git
synced 2026-06-01 17:57:37 +08:00
[benchmark] Add light call and publish benchmarks (#15176)
This commit is contained in:
@@ -0,0 +1,28 @@
|
|||||||
|
import esphome.codegen as cg
|
||||||
|
from esphome.components.light import generate_gamma_table
|
||||||
|
from tests.testing_helpers import ComponentManifestOverride
|
||||||
|
|
||||||
|
|
||||||
|
def override_manifest(manifest: ComponentManifestOverride) -> None:
|
||||||
|
# Light benchmarks need USE_LIGHT_GAMMA_LUT defined and a gamma table
|
||||||
|
# with external linkage that the benchmark .cpp can reference.
|
||||||
|
manifest.enable_codegen()
|
||||||
|
original_to_code = manifest.to_code
|
||||||
|
|
||||||
|
async def to_code(config):
|
||||||
|
await original_to_code(config)
|
||||||
|
cg.add_define("USE_LIGHT_GAMMA_LUT")
|
||||||
|
# Use the light component's own generate_gamma_table() so the
|
||||||
|
# benchmark stays in sync with any formula changes.
|
||||||
|
forward = generate_gamma_table(2.8)
|
||||||
|
values = ", ".join(f"0x{int(v):04X}" for v in forward)
|
||||||
|
# Use extern-visible (non-static) array so the benchmark .cpp
|
||||||
|
# can reference it via extern declaration.
|
||||||
|
cg.add_global(
|
||||||
|
cg.RawStatement(
|
||||||
|
f"extern const uint16_t bench_gamma_2_8_fwd[256] PROGMEM = {{{values}}};"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
to_code.priority = original_to_code.priority
|
||||||
|
manifest.to_code = to_code
|
||||||
@@ -0,0 +1,253 @@
|
|||||||
|
#include <benchmark/benchmark.h>
|
||||||
|
|
||||||
|
#include "esphome/components/light/light_output.h"
|
||||||
|
#include "esphome/components/light/light_state.h"
|
||||||
|
|
||||||
|
// Gamma 2.8 forward LUT generated by the light component's Python codegen
|
||||||
|
// (see tests/benchmarks/components/light/__init__.py which calls generate_gamma_table())
|
||||||
|
extern const uint16_t bench_gamma_2_8_fwd[256];
|
||||||
|
|
||||||
|
namespace esphome::benchmarks {
|
||||||
|
|
||||||
|
// Inner iteration count to amortize CodSpeed instrumentation overhead.
|
||||||
|
static constexpr int kInnerIterations = 2000;
|
||||||
|
|
||||||
|
// Minimal LightOutput for benchmarking — no real hardware interaction.
|
||||||
|
class BenchLightOutput : public light::LightOutput {
|
||||||
|
public:
|
||||||
|
light::LightTraits get_traits() override { return this->traits_; }
|
||||||
|
void write_state(light::LightState * /*state*/) override {}
|
||||||
|
|
||||||
|
light::LightTraits traits_;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Test subclass to access protected configure_entity_() for benchmark setup.
|
||||||
|
class TestLightState : public light::LightState {
|
||||||
|
public:
|
||||||
|
using LightState::LightState;
|
||||||
|
void configure(const char *name) { this->configure_entity_(name, 0x12345678, 0); }
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper to create a configured RGBWW light state for benchmarks.
|
||||||
|
// Note: setup() is not called (no preferences backend), so save_remote_values_()
|
||||||
|
// is effectively a no-op. This benchmarks the call/validation path, not persistence.
|
||||||
|
static void setup_rgbww_light(BenchLightOutput &output, TestLightState &light) {
|
||||||
|
output.traits_.set_supported_color_modes({light::ColorMode::RGB_COLD_WARM_WHITE});
|
||||||
|
output.traits_.set_min_mireds(153.0f);
|
||||||
|
output.traits_.set_max_mireds(500.0f);
|
||||||
|
light.configure("test_light");
|
||||||
|
light.set_default_transition_length(0);
|
||||||
|
light.set_gamma_correct(2.8f);
|
||||||
|
light.set_gamma_table(bench_gamma_2_8_fwd);
|
||||||
|
light.set_restore_mode(light::LIGHT_ALWAYS_OFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- LightCall::perform() with instant RGB color change (Home Assistant API path) ---
|
||||||
|
// Measures the full call path: validation, set_immediately_, publish, and save.
|
||||||
|
// HA sends color_mode explicitly since API 1.6.
|
||||||
|
|
||||||
|
static void LightCall_RGBInstant(benchmark::State &state) {
|
||||||
|
BenchLightOutput output;
|
||||||
|
TestLightState light(&output);
|
||||||
|
setup_rgbww_light(output, light);
|
||||||
|
|
||||||
|
// Turn on first so subsequent calls are color changes
|
||||||
|
light.make_call().set_state(true).set_brightness(1.0f).set_color_brightness(1.0f).set_transition_length(0).perform();
|
||||||
|
|
||||||
|
for (auto _ : state) {
|
||||||
|
for (int i = 0; i < kInnerIterations; i++) {
|
||||||
|
float v = static_cast<float>(i % 256) / 255.0f;
|
||||||
|
light.make_call()
|
||||||
|
.set_color_mode(light::ColorMode::RGB_COLD_WARM_WHITE)
|
||||||
|
.set_red(v)
|
||||||
|
.set_green(1.0f - v)
|
||||||
|
.set_blue(v * 0.5f)
|
||||||
|
.set_transition_length(0)
|
||||||
|
.perform();
|
||||||
|
}
|
||||||
|
benchmark::DoNotOptimize(light.remote_values);
|
||||||
|
}
|
||||||
|
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||||
|
}
|
||||||
|
BENCHMARK(LightCall_RGBInstant);
|
||||||
|
|
||||||
|
// --- LightCall::perform() turn on/off cycle (Home Assistant API path) ---
|
||||||
|
// HA sends color_mode explicitly since API 1.6, skipping compute_color_mode_().
|
||||||
|
|
||||||
|
static void LightCall_ToggleOnOff(benchmark::State &state) {
|
||||||
|
BenchLightOutput output;
|
||||||
|
TestLightState light(&output);
|
||||||
|
setup_rgbww_light(output, light);
|
||||||
|
|
||||||
|
for (auto _ : state) {
|
||||||
|
for (int i = 0; i < kInnerIterations; i++) {
|
||||||
|
light.make_call()
|
||||||
|
.set_state(i % 2 == 0)
|
||||||
|
.set_color_mode(light::ColorMode::RGB_COLD_WARM_WHITE)
|
||||||
|
.set_transition_length(0)
|
||||||
|
.perform();
|
||||||
|
}
|
||||||
|
benchmark::DoNotOptimize(light.remote_values);
|
||||||
|
}
|
||||||
|
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||||
|
}
|
||||||
|
BENCHMARK(LightCall_ToggleOnOff);
|
||||||
|
|
||||||
|
// --- LightCall::perform() turn on/off via MQTT ---
|
||||||
|
// MQTT never sends color_mode, so compute_color_mode_() runs every call.
|
||||||
|
|
||||||
|
static void LightCall_ToggleOnOff_MQTT(benchmark::State &state) {
|
||||||
|
BenchLightOutput output;
|
||||||
|
TestLightState light(&output);
|
||||||
|
setup_rgbww_light(output, light);
|
||||||
|
|
||||||
|
for (auto _ : state) {
|
||||||
|
for (int i = 0; i < kInnerIterations; i++) {
|
||||||
|
light.make_call().set_state(i % 2 == 0).set_transition_length(0).perform();
|
||||||
|
}
|
||||||
|
benchmark::DoNotOptimize(light.remote_values);
|
||||||
|
}
|
||||||
|
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||||
|
}
|
||||||
|
BENCHMARK(LightCall_ToggleOnOff_MQTT);
|
||||||
|
|
||||||
|
// --- LightCall::perform() with color temperature via MQTT ---
|
||||||
|
// Exercises the transform_parameters_() path that converts color_temperature
|
||||||
|
// to cold/warm white fractions. MQTT never sends color_mode, so this also
|
||||||
|
// hits compute_color_mode_() every call. Modern HA avoids this path entirely
|
||||||
|
// by converting color temp to CW/WW client-side.
|
||||||
|
|
||||||
|
static void LightCall_ColorTemperature_MQTT(benchmark::State &state) {
|
||||||
|
BenchLightOutput output;
|
||||||
|
TestLightState light(&output);
|
||||||
|
setup_rgbww_light(output, light);
|
||||||
|
|
||||||
|
light.make_call().set_state(true).set_brightness(1.0f).set_transition_length(0).perform();
|
||||||
|
|
||||||
|
for (auto _ : state) {
|
||||||
|
for (int i = 0; i < kInnerIterations; i++) {
|
||||||
|
// Sweep through color temperature range
|
||||||
|
float ct = 153.0f + static_cast<float>(i % 348);
|
||||||
|
light.make_call().set_color_temperature(ct).set_transition_length(0).perform();
|
||||||
|
}
|
||||||
|
benchmark::DoNotOptimize(light.remote_values);
|
||||||
|
}
|
||||||
|
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||||
|
}
|
||||||
|
BENCHMARK(LightCall_ColorTemperature_MQTT);
|
||||||
|
|
||||||
|
// --- LightCall::perform() with 1s transition (Home Assistant API path) ---
|
||||||
|
// Exercises start_transition_() which allocates a LightTransformer.
|
||||||
|
// This is the default HA path when transition_length > 0.
|
||||||
|
|
||||||
|
static void LightCall_Transition(benchmark::State &state) {
|
||||||
|
BenchLightOutput output;
|
||||||
|
TestLightState light(&output);
|
||||||
|
setup_rgbww_light(output, light);
|
||||||
|
|
||||||
|
light.make_call().set_state(true).set_brightness(1.0f).set_transition_length(0).perform();
|
||||||
|
|
||||||
|
for (auto _ : state) {
|
||||||
|
for (int i = 0; i < kInnerIterations; i++) {
|
||||||
|
float v = static_cast<float>(i % 256) / 255.0f;
|
||||||
|
light.make_call()
|
||||||
|
.set_color_mode(light::ColorMode::RGB_COLD_WARM_WHITE)
|
||||||
|
.set_red(v)
|
||||||
|
.set_green(1.0f - v)
|
||||||
|
.set_blue(v * 0.5f)
|
||||||
|
.set_transition_length(1000)
|
||||||
|
.perform();
|
||||||
|
}
|
||||||
|
benchmark::DoNotOptimize(light.remote_values);
|
||||||
|
}
|
||||||
|
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||||
|
}
|
||||||
|
BENCHMARK(LightCall_Transition);
|
||||||
|
|
||||||
|
// --- LightCall::perform() with cold/warm white (Home Assistant API path) ---
|
||||||
|
// Mirrors what modern HA sends: explicit color_mode with direct cold_white
|
||||||
|
// and warm_white values. HA converts color temp to CW/WW client-side for
|
||||||
|
// CWWW lights (API >= 1.6), so this is the primary HA path.
|
||||||
|
|
||||||
|
static void LightCall_ColdWarmWhite(benchmark::State &state) {
|
||||||
|
BenchLightOutput output;
|
||||||
|
TestLightState light(&output);
|
||||||
|
setup_rgbww_light(output, light);
|
||||||
|
|
||||||
|
light.make_call().set_state(true).set_brightness(1.0f).set_transition_length(0).perform();
|
||||||
|
|
||||||
|
for (auto _ : state) {
|
||||||
|
for (int i = 0; i < kInnerIterations; i++) {
|
||||||
|
float frac = static_cast<float>(i % 256) / 255.0f;
|
||||||
|
light.make_call()
|
||||||
|
.set_color_mode(light::ColorMode::RGB_COLD_WARM_WHITE)
|
||||||
|
.set_cold_white(1.0f - frac)
|
||||||
|
.set_warm_white(frac)
|
||||||
|
.set_transition_length(0)
|
||||||
|
.perform();
|
||||||
|
}
|
||||||
|
benchmark::DoNotOptimize(light.remote_values);
|
||||||
|
}
|
||||||
|
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||||
|
}
|
||||||
|
BENCHMARK(LightCall_ColdWarmWhite);
|
||||||
|
|
||||||
|
// --- LightState::publish_state() with a remote values listener ---
|
||||||
|
// Measures listener notification overhead.
|
||||||
|
|
||||||
|
static void LightPublish_WithListener(benchmark::State &state) {
|
||||||
|
BenchLightOutput output;
|
||||||
|
TestLightState light(&output);
|
||||||
|
setup_rgbww_light(output, light);
|
||||||
|
|
||||||
|
struct TestListener : public light::LightRemoteValuesListener {
|
||||||
|
void on_light_remote_values_update() override { count_++; }
|
||||||
|
uint64_t count_{0};
|
||||||
|
} listener;
|
||||||
|
light.add_remote_values_listener(&listener);
|
||||||
|
|
||||||
|
for (auto _ : state) {
|
||||||
|
for (int i = 0; i < kInnerIterations; i++) {
|
||||||
|
light.publish_state();
|
||||||
|
}
|
||||||
|
benchmark::DoNotOptimize(listener.count_);
|
||||||
|
}
|
||||||
|
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||||
|
}
|
||||||
|
BENCHMARK(LightPublish_WithListener);
|
||||||
|
|
||||||
|
// --- current_values_as_rgbww output conversion with gamma LUT ---
|
||||||
|
// Measures the output conversion path that real light drivers call
|
||||||
|
// from write_state() to get hardware PWM values, including gamma
|
||||||
|
// table lookups via the LUT generated by Python codegen.
|
||||||
|
|
||||||
|
static void LightOutput_RGBWW(benchmark::State &state) {
|
||||||
|
BenchLightOutput output;
|
||||||
|
TestLightState light(&output);
|
||||||
|
setup_rgbww_light(output, light);
|
||||||
|
|
||||||
|
light.make_call()
|
||||||
|
.set_state(true)
|
||||||
|
.set_brightness(0.8f)
|
||||||
|
.set_color_brightness(0.6f)
|
||||||
|
.set_red(1.0f)
|
||||||
|
.set_green(0.5f)
|
||||||
|
.set_blue(0.2f)
|
||||||
|
.set_cold_white(0.7f)
|
||||||
|
.set_warm_white(0.3f)
|
||||||
|
.set_transition_length(0)
|
||||||
|
.perform();
|
||||||
|
|
||||||
|
float r, g, b, cw, ww;
|
||||||
|
for (auto _ : state) {
|
||||||
|
for (int i = 0; i < kInnerIterations; i++) {
|
||||||
|
light.current_values_as_rgbww(&r, &g, &b, &cw, &ww);
|
||||||
|
}
|
||||||
|
benchmark::DoNotOptimize(r);
|
||||||
|
benchmark::DoNotOptimize(cw);
|
||||||
|
}
|
||||||
|
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||||
|
}
|
||||||
|
BENCHMARK(LightOutput_RGBWW);
|
||||||
|
|
||||||
|
} // namespace esphome::benchmarks
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
light:
|
||||||
Reference in New Issue
Block a user