diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f64c6ffbb4..4a5ac0e29e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -340,7 +340,6 @@ jobs: uses: CodSpeedHQ/action@281164b0f014a4e7badd2c02cecad9b595b70537 # v4 with: run: ${{ steps.build.outputs.binary }} - token: ${{ secrets.CODSPEED_TOKEN }} mode: simulation clang-tidy-single: diff --git a/esphome/core/component.h b/esphome/core/component.h index 5fdf23e128..557ba09bbc 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -598,9 +598,11 @@ class WarnIfComponentBlockingGuard { #ifdef USE_RUNTIME_STATS this->record_runtime_stats_(); #endif +#ifndef USE_BENCHMARK if (blocking_time > WARN_IF_BLOCKING_OVER_MS) [[unlikely]] { warn_blocking(this->component_, blocking_time); } +#endif return curr_time; } diff --git a/script/cpp_benchmark.py b/script/cpp_benchmark.py index f30054fbad..bd92266ea6 100755 --- a/script/cpp_benchmark.py +++ b/script/cpp_benchmark.py @@ -34,6 +34,7 @@ PLATFORMIO_OPTIONS = { "-O2", # optimize for speed (CodSpeed recommends RelWithDebInfo) "-g", # debug symbols for profiling USE_TIME_TIMEZONE_FLAG, + "-DUSE_BENCHMARK", # disable WarnIfComponentBlockingGuard in finish() ], # Use deep+ LDF mode to ensure PlatformIO detects the benchmark # library dependency from nested includes. diff --git a/script/setup_codspeed_lib.py b/script/setup_codspeed_lib.py index 214714d698..959c89d05b 100755 --- a/script/setup_codspeed_lib.py +++ b/script/setup_codspeed_lib.py @@ -171,6 +171,21 @@ def setup_codspeed_lib(output_dir: Path) -> None: """ if not (output_dir / ".git").exists(): _clone_repo(output_dir) + else: + # Verify the existing checkout matches the pinned SHA + result = subprocess.run( + ["git", "-C", str(output_dir), "rev-parse", "HEAD"], + capture_output=True, + text=True, + check=False, + ) + if result.returncode != 0 or result.stdout.strip() != CODSPEED_CPP_SHA: + print( + f"Stale codspeed-cpp checkout, re-cloning at {CODSPEED_CPP_SHA}", + file=sys.stderr, + ) + shutil.rmtree(output_dir) + _clone_repo(output_dir) benchmark_dir = output_dir / GOOGLE_BENCHMARK_SUBDIR lib_src = benchmark_dir / "src" diff --git a/script/test_helpers.py b/script/test_helpers.py index 36a3e4b859..24d99acaeb 100644 --- a/script/test_helpers.py +++ b/script/test_helpers.py @@ -81,7 +81,7 @@ def filter_components_with_files(components: list[str], tests_dir: Path) -> list filtered_components.append(component) else: print( - f"WARNING: No files found for component '{component}' in {tests_dir}, skipping.", + f"WARNING: No files found for component '{component}' in {test_dir}, skipping.", file=sys.stderr, ) return filtered_components @@ -139,6 +139,11 @@ def load_component_yaml_configs(components: list[str], tests_dir: Path) -> dict: Returns: Merged dict of component configs to add to the base config """ + # Note: components are processed in sorted order. For conflicting keys + # (e.g. two benchmark.yaml files both declaring sensor:), the first + # component alphabetically wins via setdefault(). This is fine for now + # with a single benchmark component (api) but would need a real merge + # strategy if multiple components declare overlapping configs. merged: dict = {} for component in components: yaml_path = tests_dir / component / BENCHMARK_YAML_FILENAME @@ -284,7 +289,7 @@ def compile_and_get_binary( print(f"Error compiling {label} for {', '.join(components)}") return exit_code, None except Exception as e: - print(f"Error compiling {label} for {', '.join(components)}. Check path. : {e}") + print(f"Error compiling {label} for {', '.join(components)}: {e}") return EXIT_COMPILE_ERROR, None # After a successful compilation, locate the executable: diff --git a/tests/benchmarks/components/.gitignore b/tests/benchmarks/components/.gitignore index d8b4157aef..163bec7b80 100644 --- a/tests/benchmarks/components/.gitignore +++ b/tests/benchmarks/components/.gitignore @@ -1,5 +1,2 @@ -# Gitignore settings for ESPHome -# This is an example and may include too much for your use-case. -# You can modify this file to suit your needs. /.esphome/ /secrets.yaml diff --git a/tests/benchmarks/components/api/bench_proto_decode.cpp b/tests/benchmarks/components/api/bench_proto_decode.cpp index c9313c2fca..113201dd8a 100644 --- a/tests/benchmarks/components/api/bench_proto_decode.cpp +++ b/tests/benchmarks/components/api/bench_proto_decode.cpp @@ -5,114 +5,88 @@ namespace esphome::api::benchmarks { +// Inner iteration count to amortize CodSpeed instrumentation overhead. +// Without this, the ~60ns per-iteration valgrind start/stop cost dominates +// sub-microsecond benchmarks. +static constexpr int kInnerIterations = 2000; + +// Helper: encode a message into a buffer and return it. +// Benchmarks encode once in setup, then decode the resulting bytes in a loop. +// This keeps decode benchmarks in sync with the actual protobuf schema — +// hand-encoded byte arrays would silently break when fields change. +template static APIBuffer encode_message(const T &msg) { + APIBuffer buffer; + uint32_t size = msg.calculate_size(); + buffer.resize(size); + ProtoWriteBuffer writer(&buffer, 0); + msg.encode(writer); + return buffer; +} + // --- HelloRequest decode (string + varint fields) --- static void Decode_HelloRequest(benchmark::State &state) { - // Manually encoded HelloRequest: - // field 1 (string): "aioesphomeapi" - // field 2 (varint): 1 (api_version_major) - // field 3 (varint): 10 (api_version_minor) - uint8_t encoded[] = { - 0x0A, 0x0D, // field 1, length 13 - 'a', 'i', 'o', 'e', 's', 'p', 'h', 'o', 'm', 'e', 'a', 'p', 'i', // "aioesphomeapi" - 0x10, 0x01, // field 2, value 1 - 0x18, 0x0A, // field 3, value 10 - }; + HelloRequest source; + source.client_info = StringRef::from_lit("aioesphomeapi"); + source.api_version_major = 1; + source.api_version_minor = 10; + auto encoded = encode_message(source); for (auto _ : state) { HelloRequest msg; - msg.decode(encoded, sizeof(encoded)); + for (int i = 0; i < kInnerIterations; i++) { + msg.decode(encoded.data(), encoded.size()); + } benchmark::DoNotOptimize(msg.api_version_major); } + state.SetItemsProcessed(state.iterations() * kInnerIterations); } BENCHMARK(Decode_HelloRequest); // --- SwitchCommandRequest decode (simple command) --- static void Decode_SwitchCommandRequest(benchmark::State &state) { - // field 1 (fixed32): key = 0x12345678 - // field 2 (varint): state = true - uint8_t encoded[] = { - 0x0D, 0x78, 0x56, 0x34, 0x12, // field 1, fixed32 - 0x10, 0x01, // field 2, varint true - }; + SwitchCommandRequest source; + source.key = 0x12345678; + source.state = true; + auto encoded = encode_message(source); for (auto _ : state) { SwitchCommandRequest msg; - msg.decode(encoded, sizeof(encoded)); + for (int i = 0; i < kInnerIterations; i++) { + msg.decode(encoded.data(), encoded.size()); + } benchmark::DoNotOptimize(msg.state); } + state.SetItemsProcessed(state.iterations() * kInnerIterations); } BENCHMARK(Decode_SwitchCommandRequest); // --- LightCommandRequest decode (complex command with many fields) --- static void Decode_LightCommandRequest(benchmark::State &state) { - uint8_t encoded[] = { - // field 1: key (fixed32) = 0x11223344 - 0x0D, - 0x44, - 0x33, - 0x22, - 0x11, - // field 2: has_state (varint) = true - 0x10, - 0x01, - // field 3: state (varint) = true - 0x18, - 0x01, - // field 4: has_brightness (varint) = true - 0x20, - 0x01, - // field 5: brightness (fixed32/float) = 0.8 - 0x2D, - 0xCD, - 0xCC, - 0x4C, - 0x3F, - // field 9: has_rgb (varint) = true - 0x48, - 0x01, - // field 10: red (fixed32/float) = 1.0 - 0x55, - 0x00, - 0x00, - 0x80, - 0x3F, - // field 11: green (fixed32/float) = 0.5 - 0x5D, - 0x00, - 0x00, - 0x00, - 0x3F, - // field 12: blue (fixed32/float) = 0.2 - 0x65, - 0xCD, - 0xCC, - 0x4C, - 0x3E, - // field 20: has_effect (varint) = true - 0xA0, - 0x01, - 0x01, - // field 21: effect (string) = "rainbow" - 0xAA, - 0x01, - 0x07, - 'r', - 'a', - 'i', - 'n', - 'b', - 'o', - 'w', - }; + LightCommandRequest source; + source.key = 0x11223344; + source.has_state = true; + source.state = true; + source.has_brightness = true; + source.brightness = 0.8f; + source.has_rgb = true; + source.red = 1.0f; + source.green = 0.5f; + source.blue = 0.2f; + source.has_effect = true; + source.effect = StringRef::from_lit("rainbow"); + auto encoded = encode_message(source); for (auto _ : state) { LightCommandRequest msg; - msg.decode(encoded, sizeof(encoded)); + for (int i = 0; i < kInnerIterations; i++) { + msg.decode(encoded.data(), encoded.size()); + } benchmark::DoNotOptimize(msg.brightness); } + state.SetItemsProcessed(state.iterations() * kInnerIterations); } BENCHMARK(Decode_LightCommandRequest); diff --git a/tests/benchmarks/components/api/bench_proto_encode.cpp b/tests/benchmarks/components/api/bench_proto_encode.cpp index 2cd050f820..656c1e17db 100644 --- a/tests/benchmarks/components/api/bench_proto_encode.cpp +++ b/tests/benchmarks/components/api/bench_proto_encode.cpp @@ -5,6 +5,11 @@ namespace esphome::api::benchmarks { +// Inner iteration count to amortize CodSpeed instrumentation overhead. +// Without this, the ~60ns per-iteration valgrind start/stop cost dominates +// sub-microsecond benchmarks. +static constexpr int kInnerIterations = 2000; + // --- SensorStateResponse (highest frequency message) --- static void Encode_SensorStateResponse(benchmark::State &state) { @@ -17,10 +22,13 @@ static void Encode_SensorStateResponse(benchmark::State &state) { buffer.resize(size); for (auto _ : state) { - ProtoWriteBuffer writer(&buffer, 0); - msg.encode(writer); + for (int i = 0; i < kInnerIterations; i++) { + ProtoWriteBuffer writer(&buffer, 0); + msg.encode(writer); + } benchmark::DoNotOptimize(buffer.data()); } + state.SetItemsProcessed(state.iterations() * kInnerIterations); } BENCHMARK(Encode_SensorStateResponse); @@ -31,8 +39,13 @@ static void CalculateSize_SensorStateResponse(benchmark::State &state) { msg.missing_state = false; for (auto _ : state) { - benchmark::DoNotOptimize(msg.calculate_size()); + uint32_t result = 0; + for (int i = 0; i < kInnerIterations; i++) { + result += msg.calculate_size(); + } + benchmark::DoNotOptimize(result); } + state.SetItemsProcessed(state.iterations() * kInnerIterations); } BENCHMARK(CalculateSize_SensorStateResponse); @@ -45,16 +58,22 @@ static void CalcAndEncode_SensorStateResponse(benchmark::State &state) { msg.missing_state = false; for (auto _ : state) { - uint32_t size = msg.calculate_size(); - buffer.resize(size); - ProtoWriteBuffer writer(&buffer, 0); - msg.encode(writer); + for (int i = 0; i < kInnerIterations; i++) { + uint32_t size = msg.calculate_size(); + buffer.resize(size); + ProtoWriteBuffer writer(&buffer, 0); + msg.encode(writer); + } benchmark::DoNotOptimize(buffer.data()); } + state.SetItemsProcessed(state.iterations() * kInnerIterations); } BENCHMARK(CalcAndEncode_SensorStateResponse); -// Cold path: fresh buffer each iteration (measures heap allocation) +// Cold path: fresh buffer each iteration (measures heap allocation cost). +// Inner loop still needed to amortize CodSpeed instrumentation overhead. +// Each inner iteration creates a fresh buffer, so this measures +// alloc+calc+encode per item. static void CalcAndEncode_SensorStateResponse_Fresh(benchmark::State &state) { SensorStateResponse msg; msg.key = 0x12345678; @@ -62,13 +81,16 @@ static void CalcAndEncode_SensorStateResponse_Fresh(benchmark::State &state) { msg.missing_state = false; for (auto _ : state) { - APIBuffer buffer; - uint32_t size = msg.calculate_size(); - buffer.resize(size); - ProtoWriteBuffer writer(&buffer, 0); - msg.encode(writer); - benchmark::DoNotOptimize(buffer.data()); + for (int i = 0; i < kInnerIterations; i++) { + APIBuffer buffer; + uint32_t size = msg.calculate_size(); + buffer.resize(size); + ProtoWriteBuffer writer(&buffer, 0); + msg.encode(writer); + benchmark::DoNotOptimize(buffer.data()); + } } + state.SetItemsProcessed(state.iterations() * kInnerIterations); } BENCHMARK(CalcAndEncode_SensorStateResponse_Fresh); @@ -84,10 +106,13 @@ static void Encode_BinarySensorStateResponse(benchmark::State &state) { buffer.resize(size); for (auto _ : state) { - ProtoWriteBuffer writer(&buffer, 0); - msg.encode(writer); + for (int i = 0; i < kInnerIterations; i++) { + ProtoWriteBuffer writer(&buffer, 0); + msg.encode(writer); + } benchmark::DoNotOptimize(buffer.data()); } + state.SetItemsProcessed(state.iterations() * kInnerIterations); } BENCHMARK(Encode_BinarySensorStateResponse); @@ -104,10 +129,13 @@ static void Encode_HelloResponse(benchmark::State &state) { buffer.resize(size); for (auto _ : state) { - ProtoWriteBuffer writer(&buffer, 0); - msg.encode(writer); + for (int i = 0; i < kInnerIterations; i++) { + ProtoWriteBuffer writer(&buffer, 0); + msg.encode(writer); + } benchmark::DoNotOptimize(buffer.data()); } + state.SetItemsProcessed(state.iterations() * kInnerIterations); } BENCHMARK(Encode_HelloResponse); @@ -133,10 +161,13 @@ static void Encode_LightStateResponse(benchmark::State &state) { buffer.resize(size); for (auto _ : state) { - ProtoWriteBuffer writer(&buffer, 0); - msg.encode(writer); + for (int i = 0; i < kInnerIterations; i++) { + ProtoWriteBuffer writer(&buffer, 0); + msg.encode(writer); + } benchmark::DoNotOptimize(buffer.data()); } + state.SetItemsProcessed(state.iterations() * kInnerIterations); } BENCHMARK(Encode_LightStateResponse); @@ -157,8 +188,13 @@ static void CalculateSize_LightStateResponse(benchmark::State &state) { msg.effect = StringRef::from_lit("rainbow"); for (auto _ : state) { - benchmark::DoNotOptimize(msg.calculate_size()); + uint32_t result = 0; + for (int i = 0; i < kInnerIterations; i++) { + result += msg.calculate_size(); + } + benchmark::DoNotOptimize(result); } + state.SetItemsProcessed(state.iterations() * kInnerIterations); } BENCHMARK(CalculateSize_LightStateResponse); @@ -193,8 +229,13 @@ static void CalculateSize_DeviceInfoResponse(benchmark::State &state) { auto msg = make_device_info_response(); for (auto _ : state) { - benchmark::DoNotOptimize(msg.calculate_size()); + uint32_t result = 0; + for (int i = 0; i < kInnerIterations; i++) { + result += msg.calculate_size(); + } + benchmark::DoNotOptimize(result); } + state.SetItemsProcessed(state.iterations() * kInnerIterations); } BENCHMARK(CalculateSize_DeviceInfoResponse); @@ -205,10 +246,13 @@ static void Encode_DeviceInfoResponse(benchmark::State &state) { buffer.resize(total_size); for (auto _ : state) { - ProtoWriteBuffer writer(&buffer, 0); - msg.encode(writer); + for (int i = 0; i < kInnerIterations; i++) { + ProtoWriteBuffer writer(&buffer, 0); + msg.encode(writer); + } benchmark::DoNotOptimize(buffer.data()); } + state.SetItemsProcessed(state.iterations() * kInnerIterations); } BENCHMARK(Encode_DeviceInfoResponse); @@ -218,27 +262,36 @@ static void CalcAndEncode_DeviceInfoResponse(benchmark::State &state) { APIBuffer buffer; for (auto _ : state) { - uint32_t size = msg.calculate_size(); - buffer.resize(size); - ProtoWriteBuffer writer(&buffer, 0); - msg.encode(writer); + for (int i = 0; i < kInnerIterations; i++) { + uint32_t size = msg.calculate_size(); + buffer.resize(size); + ProtoWriteBuffer writer(&buffer, 0); + msg.encode(writer); + } benchmark::DoNotOptimize(buffer.data()); } + state.SetItemsProcessed(state.iterations() * kInnerIterations); } BENCHMARK(CalcAndEncode_DeviceInfoResponse); -// Cold path: fresh buffer each iteration (measures heap allocation) +// Cold path: fresh buffer each iteration (measures heap allocation cost). +// Inner loop still needed to amortize CodSpeed instrumentation overhead. +// Each inner iteration creates a fresh buffer, so this measures +// alloc+calc+encode per item. static void CalcAndEncode_DeviceInfoResponse_Fresh(benchmark::State &state) { auto msg = make_device_info_response(); for (auto _ : state) { - APIBuffer buffer; - uint32_t size = msg.calculate_size(); - buffer.resize(size); - ProtoWriteBuffer writer(&buffer, 0); - msg.encode(writer); - benchmark::DoNotOptimize(buffer.data()); + for (int i = 0; i < kInnerIterations; i++) { + APIBuffer buffer; + uint32_t size = msg.calculate_size(); + buffer.resize(size); + ProtoWriteBuffer writer(&buffer, 0); + msg.encode(writer); + benchmark::DoNotOptimize(buffer.data()); + } } + state.SetItemsProcessed(state.iterations() * kInnerIterations); } BENCHMARK(CalcAndEncode_DeviceInfoResponse_Fresh); diff --git a/tests/benchmarks/components/api/bench_proto_varint.cpp b/tests/benchmarks/components/api/bench_proto_varint.cpp index d33dd4b5ac..ff4a656980 100644 --- a/tests/benchmarks/components/api/bench_proto_varint.cpp +++ b/tests/benchmarks/components/api/bench_proto_varint.cpp @@ -5,66 +5,84 @@ namespace esphome::api::benchmarks { +// Inner iteration count to amortize CodSpeed instrumentation overhead. +// Without this, the ~60ns per-iteration valgrind start/stop cost dominates +// sub-microsecond benchmarks. +static constexpr int kInnerIterations = 2000; + // --- ProtoVarInt::parse() benchmarks --- static void ProtoVarInt_Parse_SingleByte(benchmark::State &state) { - // Single-byte varint (0-127) — the most common case (fast path) uint8_t buf[] = {0x42}; // value = 66 for (auto _ : state) { - auto result = ProtoVarInt::parse(buf, sizeof(buf)); + ProtoVarIntResult result{}; + for (int i = 0; i < kInnerIterations; i++) { + result = ProtoVarInt::parse(buf, sizeof(buf)); + } benchmark::DoNotOptimize(result); } + state.SetItemsProcessed(state.iterations() * kInnerIterations); } BENCHMARK(ProtoVarInt_Parse_SingleByte); static void ProtoVarInt_Parse_TwoByte(benchmark::State &state) { - // Two-byte varint (128-16383) uint8_t buf[] = {0x80, 0x01}; // value = 128 for (auto _ : state) { - auto result = ProtoVarInt::parse(buf, sizeof(buf)); + ProtoVarIntResult result{}; + for (int i = 0; i < kInnerIterations; i++) { + result = ProtoVarInt::parse(buf, sizeof(buf)); + } benchmark::DoNotOptimize(result); } + state.SetItemsProcessed(state.iterations() * kInnerIterations); } BENCHMARK(ProtoVarInt_Parse_TwoByte); static void ProtoVarInt_Parse_FiveByte(benchmark::State &state) { - // Five-byte varint (max uint32 = 4294967295) uint8_t buf[] = {0xFF, 0xFF, 0xFF, 0xFF, 0x0F}; for (auto _ : state) { - auto result = ProtoVarInt::parse(buf, sizeof(buf)); + ProtoVarIntResult result{}; + for (int i = 0; i < kInnerIterations; i++) { + result = ProtoVarInt::parse(buf, sizeof(buf)); + } benchmark::DoNotOptimize(result); } + state.SetItemsProcessed(state.iterations() * kInnerIterations); } BENCHMARK(ProtoVarInt_Parse_FiveByte); // --- Varint encoding benchmarks --- static void Encode_Varint_Small(benchmark::State &state) { - // Value < 128 — single byte fast path APIBuffer buffer; buffer.resize(16); for (auto _ : state) { - ProtoWriteBuffer writer(&buffer, 0); - writer.encode_varint_raw(42); + for (int i = 0; i < kInnerIterations; i++) { + ProtoWriteBuffer writer(&buffer, 0); + writer.encode_varint_raw(42); + } benchmark::DoNotOptimize(buffer.data()); } + state.SetItemsProcessed(state.iterations() * kInnerIterations); } BENCHMARK(Encode_Varint_Small); static void Encode_Varint_Large(benchmark::State &state) { - // Value > 128 — multi-byte slow path APIBuffer buffer; buffer.resize(16); for (auto _ : state) { - ProtoWriteBuffer writer(&buffer, 0); - writer.encode_varint_raw(300); + for (int i = 0; i < kInnerIterations; i++) { + ProtoWriteBuffer writer(&buffer, 0); + writer.encode_varint_raw(300); + } benchmark::DoNotOptimize(buffer.data()); } + state.SetItemsProcessed(state.iterations() * kInnerIterations); } BENCHMARK(Encode_Varint_Large); @@ -73,10 +91,13 @@ static void Encode_Varint_MaxUint32(benchmark::State &state) { buffer.resize(16); for (auto _ : state) { - ProtoWriteBuffer writer(&buffer, 0); - writer.encode_varint_raw(0xFFFFFFFF); + for (int i = 0; i < kInnerIterations; i++) { + ProtoWriteBuffer writer(&buffer, 0); + writer.encode_varint_raw(0xFFFFFFFF); + } benchmark::DoNotOptimize(buffer.data()); } + state.SetItemsProcessed(state.iterations() * kInnerIterations); } BENCHMARK(Encode_Varint_MaxUint32); @@ -84,15 +105,25 @@ BENCHMARK(Encode_Varint_MaxUint32); static void ProtoSize_Varint_Small(benchmark::State &state) { for (auto _ : state) { - benchmark::DoNotOptimize(ProtoSize::varint(42)); + uint32_t result = 0; + for (int i = 0; i < kInnerIterations; i++) { + result += ProtoSize::varint(42); + } + benchmark::DoNotOptimize(result); } + state.SetItemsProcessed(state.iterations() * kInnerIterations); } BENCHMARK(ProtoSize_Varint_Small); static void ProtoSize_Varint_Large(benchmark::State &state) { for (auto _ : state) { - benchmark::DoNotOptimize(ProtoSize::varint(0xFFFFFFFF)); + uint32_t result = 0; + for (int i = 0; i < kInnerIterations; i++) { + result += ProtoSize::varint(0xFFFFFFFF); + } + benchmark::DoNotOptimize(result); } + state.SetItemsProcessed(state.iterations() * kInnerIterations); } BENCHMARK(ProtoSize_Varint_Large); diff --git a/tests/benchmarks/components/main.cpp b/tests/benchmarks/components/main.cpp index 02fdf288a5..9bc0c31a15 100644 --- a/tests/benchmarks/components/main.cpp +++ b/tests/benchmarks/components/main.cpp @@ -10,7 +10,7 @@ It replaces the default ESPHome main with a benchmark runner. // Auto generated code by esphome // ========== AUTO GENERATED INCLUDE BLOCK BEGIN =========== -// ========== AUTO GENERATED INCLUDE BLOCK END ===========" +// ========== AUTO GENERATED INCLUDE BLOCK END =========== void original_setup() { // Code-generated App initialization (pre_setup, area/device registration, etc.) diff --git a/tests/benchmarks/core/bench_helpers.cpp b/tests/benchmarks/core/bench_helpers.cpp index d337a27f61..c6e1e6930e 100644 --- a/tests/benchmarks/core/bench_helpers.cpp +++ b/tests/benchmarks/core/bench_helpers.cpp @@ -4,13 +4,23 @@ namespace esphome::benchmarks { +// Inner iteration count to amortize CodSpeed instrumentation overhead. +// Without this, the ~60ns per-iteration valgrind start/stop cost dominates +// sub-microsecond benchmarks. +static constexpr int kInnerIterations = 2000; + // --- random_float() --- // Ported from ol.yaml:148 "Random Float Benchmark" static void RandomFloat(benchmark::State &state) { for (auto _ : state) { - benchmark::DoNotOptimize(random_float()); + float result = 0.0f; + for (int i = 0; i < kInnerIterations; i++) { + result += random_float(); + } + benchmark::DoNotOptimize(result); } + state.SetItemsProcessed(state.iterations() * kInnerIterations); } BENCHMARK(RandomFloat); @@ -18,8 +28,13 @@ BENCHMARK(RandomFloat); static void RandomUint32(benchmark::State &state) { for (auto _ : state) { - benchmark::DoNotOptimize(random_uint32()); + uint32_t result = 0; + for (int i = 0; i < kInnerIterations; i++) { + result += random_uint32(); + } + benchmark::DoNotOptimize(result); } + state.SetItemsProcessed(state.iterations() * kInnerIterations); } BENCHMARK(RandomUint32); diff --git a/tests/benchmarks/core/bench_logger.cpp b/tests/benchmarks/core/bench_logger.cpp new file mode 100644 index 0000000000..b7e9a1c4ea --- /dev/null +++ b/tests/benchmarks/core/bench_logger.cpp @@ -0,0 +1,54 @@ +#include + +#include "esphome/core/log.h" + +namespace esphome::benchmarks { + +// Inner iteration count to amortize CodSpeed instrumentation overhead. +// Without this, the ~60ns per-iteration valgrind start/stop cost dominates +// sub-microsecond benchmarks. +static constexpr int kInnerIterations = 2000; + +static const char *const TAG = "bench"; + +// --- Log a message with no format specifiers (fastest path) --- + +static void Logger_NoFormat(benchmark::State &state) { + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + ESP_LOGW(TAG, "Something happened"); + } + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(Logger_NoFormat); + +// --- Log a message with 3 uint32_t format specifiers --- + +static void Logger_3Uint32(benchmark::State &state) { + uint32_t a = 12345, b = 67890, c = 99999; + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + ESP_LOGW(TAG, "Values: %" PRIu32 " %" PRIu32 " %" PRIu32, a, b, c); + } + benchmark::DoNotOptimize(a); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(Logger_3Uint32); + +// --- Log a message with 3 floats (common for sensor values) --- + +static void Logger_3Float(benchmark::State &state) { + float temp = 23.456f, humidity = 67.89f, pressure = 1013.25f; + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + ESP_LOGW(TAG, "Sensor: %.2f %.1f %.2f", temp, humidity, pressure); + } + benchmark::DoNotOptimize(temp); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(Logger_3Float); + +} // namespace esphome::benchmarks diff --git a/tests/benchmarks/core/bench_scheduler.cpp b/tests/benchmarks/core/bench_scheduler.cpp index 4ce24abf94..3d2cd0bda2 100644 --- a/tests/benchmarks/core/bench_scheduler.cpp +++ b/tests/benchmarks/core/bench_scheduler.cpp @@ -5,6 +5,11 @@ namespace esphome::benchmarks { +// Inner iteration count to amortize CodSpeed instrumentation overhead. +// Without this, the ~60ns per-iteration valgrind start/stop cost dominates +// sub-microsecond benchmarks. +static constexpr int kInnerIterations = 2000; + // --- Scheduler fast path: no work to do --- static void Scheduler_Call_NoWork(benchmark::State &state) { @@ -12,9 +17,12 @@ static void Scheduler_Call_NoWork(benchmark::State &state) { uint32_t now = millis(); for (auto _ : state) { - scheduler.call(now); + for (int i = 0; i < kInnerIterations; i++) { + scheduler.call(now); + } benchmark::DoNotOptimize(now); } + state.SetItemsProcessed(state.iterations() * kInnerIterations); } BENCHMARK(Scheduler_Call_NoWork); @@ -33,12 +41,42 @@ static void Scheduler_Call_TimersNotDue(benchmark::State &state) { uint32_t now = millis(); for (auto _ : state) { - scheduler.call(now); + for (int i = 0; i < kInnerIterations; i++) { + scheduler.call(now); + } benchmark::DoNotOptimize(now); } + state.SetItemsProcessed(state.iterations() * kInnerIterations); } BENCHMARK(Scheduler_Call_TimersNotDue); +// --- Scheduler with 5 intervals firing every call --- + +static void Scheduler_Call_5IntervalsFiring(benchmark::State &state) { + Scheduler scheduler; + Component dummy_component; + int fire_count = 0; + + // Benchmarks the heap-based scheduler dispatch with 5 callbacks firing. + // Uses monotonically increasing fake time so intervals reliably fire every call. + // USE_BENCHMARK ifdef in component.h disables WarnIfComponentBlockingGuard + // (fake now > real millis() would cause underflow in finish()). + // interval=0 would cause an infinite loop (reschedules at same now). + for (int i = 0; i < 5; i++) { + scheduler.set_interval(&dummy_component, static_cast(i), 1, [&fire_count]() { fire_count++; }); + } + scheduler.process_to_add(); + + uint32_t now = millis() + 100; + + for (auto _ : state) { + scheduler.call(now); + now++; + benchmark::DoNotOptimize(fire_count); + } +} +BENCHMARK(Scheduler_Call_5IntervalsFiring); + // --- Scheduler: next_schedule_in() calculation --- static void Scheduler_NextScheduleIn(benchmark::State &state) { @@ -54,9 +92,13 @@ static void Scheduler_NextScheduleIn(benchmark::State &state) { uint32_t now = millis(); for (auto _ : state) { - auto result = scheduler.next_schedule_in(now); + optional result; + for (int i = 0; i < kInnerIterations; i++) { + result = scheduler.next_schedule_in(now); + } benchmark::DoNotOptimize(result); } + state.SetItemsProcessed(state.iterations() * kInnerIterations); } BENCHMARK(Scheduler_NextScheduleIn);