#include #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(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(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(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(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