From dd52c9129005a5313d70ae175827936e5edde469 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 16 Mar 2026 23:32:49 -1000 Subject: [PATCH 01/19] Add inner iteration loops to amortize CodSpeed instrumentation overhead Sub-microsecond benchmarks are dominated by the ~60ns per-iteration valgrind start/stop cost in CodSpeed simulation mode. Add kInnerIterations (1000) inner loops to all fast benchmarks so the actual work dominates. Move DoNotOptimize calls outside inner loops to prevent artificial overhead. Also address review feedback: - Use tokenless CodSpeed (public repo, no CODSPEED_TOKEN needed) - Fix warning message to show component-specific path - Fix stray ". :" in error message - Verify pinned SHA on re-runs to prevent stale checkouts --- .github/workflows/ci.yml | 1 - script/setup_codspeed_lib.py | 15 +++ script/test_helpers.py | 4 +- .../components/api/bench_proto_decode.cpp | 20 +++- .../components/api/bench_proto_encode.cpp | 98 ++++++++++++++----- .../components/api/bench_proto_varint.cpp | 63 +++++++++--- tests/benchmarks/core/bench_helpers.cpp | 19 +++- tests/benchmarks/core/bench_scheduler.cpp | 21 +++- 8 files changed, 187 insertions(+), 54 deletions(-) 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/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..db1c981b82 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 @@ -284,7 +284,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/api/bench_proto_decode.cpp b/tests/benchmarks/components/api/bench_proto_decode.cpp index c9313c2fca..2208a61692 100644 --- a/tests/benchmarks/components/api/bench_proto_decode.cpp +++ b/tests/benchmarks/components/api/bench_proto_decode.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 = 1000; + // --- HelloRequest decode (string + varint fields) --- static void Decode_HelloRequest(benchmark::State &state) { @@ -21,9 +26,12 @@ static void Decode_HelloRequest(benchmark::State &state) { for (auto _ : state) { HelloRequest msg; - msg.decode(encoded, sizeof(encoded)); + for (int i = 0; i < kInnerIterations; i++) { + msg.decode(encoded, sizeof(encoded)); + } benchmark::DoNotOptimize(msg.api_version_major); } + state.SetItemsProcessed(state.iterations() * kInnerIterations); } BENCHMARK(Decode_HelloRequest); @@ -39,9 +47,12 @@ static void Decode_SwitchCommandRequest(benchmark::State &state) { for (auto _ : state) { SwitchCommandRequest msg; - msg.decode(encoded, sizeof(encoded)); + for (int i = 0; i < kInnerIterations; i++) { + msg.decode(encoded, sizeof(encoded)); + } benchmark::DoNotOptimize(msg.state); } + state.SetItemsProcessed(state.iterations() * kInnerIterations); } BENCHMARK(Decode_SwitchCommandRequest); @@ -110,9 +121,12 @@ static void Decode_LightCommandRequest(benchmark::State &state) { for (auto _ : state) { LightCommandRequest msg; - msg.decode(encoded, sizeof(encoded)); + for (int i = 0; i < kInnerIterations; i++) { + msg.decode(encoded, sizeof(encoded)); + } 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..d58a2b01d2 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 = 1000; + // --- 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,12 +58,15 @@ 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); @@ -84,10 +100,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 +123,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 +155,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 +182,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 +223,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 +240,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,12 +256,15 @@ 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); @@ -232,13 +273,16 @@ 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..440769ea67 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 = 1000; + // --- 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/core/bench_helpers.cpp b/tests/benchmarks/core/bench_helpers.cpp index d337a27f61..5610d833e3 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 = 1000; + // --- 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_scheduler.cpp b/tests/benchmarks/core/bench_scheduler.cpp index 4ce24abf94..8382a4b228 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 = 1000; + // --- 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,9 +41,12 @@ 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); @@ -54,9 +65,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); From 8e4091baa3f3c6d720233ef1c6f50dbc81591715 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 16 Mar 2026 23:40:18 -1000 Subject: [PATCH 02/19] Increase inner iterations from 1000 to 2000 to reduce jitter --- tests/benchmarks/components/api/bench_proto_decode.cpp | 2 +- tests/benchmarks/components/api/bench_proto_encode.cpp | 2 +- tests/benchmarks/components/api/bench_proto_varint.cpp | 2 +- tests/benchmarks/core/bench_helpers.cpp | 2 +- tests/benchmarks/core/bench_scheduler.cpp | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/benchmarks/components/api/bench_proto_decode.cpp b/tests/benchmarks/components/api/bench_proto_decode.cpp index 2208a61692..a5ecf78cde 100644 --- a/tests/benchmarks/components/api/bench_proto_decode.cpp +++ b/tests/benchmarks/components/api/bench_proto_decode.cpp @@ -8,7 +8,7 @@ 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 = 1000; +static constexpr int kInnerIterations = 2000; // --- HelloRequest decode (string + varint fields) --- diff --git a/tests/benchmarks/components/api/bench_proto_encode.cpp b/tests/benchmarks/components/api/bench_proto_encode.cpp index d58a2b01d2..c5dbd685be 100644 --- a/tests/benchmarks/components/api/bench_proto_encode.cpp +++ b/tests/benchmarks/components/api/bench_proto_encode.cpp @@ -8,7 +8,7 @@ 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 = 1000; +static constexpr int kInnerIterations = 2000; // --- SensorStateResponse (highest frequency message) --- diff --git a/tests/benchmarks/components/api/bench_proto_varint.cpp b/tests/benchmarks/components/api/bench_proto_varint.cpp index 440769ea67..ff4a656980 100644 --- a/tests/benchmarks/components/api/bench_proto_varint.cpp +++ b/tests/benchmarks/components/api/bench_proto_varint.cpp @@ -8,7 +8,7 @@ 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 = 1000; +static constexpr int kInnerIterations = 2000; // --- ProtoVarInt::parse() benchmarks --- diff --git a/tests/benchmarks/core/bench_helpers.cpp b/tests/benchmarks/core/bench_helpers.cpp index 5610d833e3..c6e1e6930e 100644 --- a/tests/benchmarks/core/bench_helpers.cpp +++ b/tests/benchmarks/core/bench_helpers.cpp @@ -7,7 +7,7 @@ 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 = 1000; +static constexpr int kInnerIterations = 2000; // --- random_float() --- // Ported from ol.yaml:148 "Random Float Benchmark" diff --git a/tests/benchmarks/core/bench_scheduler.cpp b/tests/benchmarks/core/bench_scheduler.cpp index 8382a4b228..d9d1575ebd 100644 --- a/tests/benchmarks/core/bench_scheduler.cpp +++ b/tests/benchmarks/core/bench_scheduler.cpp @@ -8,7 +8,7 @@ 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 = 1000; +static constexpr int kInnerIterations = 2000; // --- Scheduler fast path: no work to do --- From 5d5a48c369a6087aaa165ac303d59c94d8e1e45c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 16 Mar 2026 23:52:27 -1000 Subject: [PATCH 03/19] Fix _Fresh benchmark consistency and address review nits MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove inner loop from CalcAndEncode_DeviceInfoResponse_Fresh to match SensorStateResponse_Fresh — both now measure single alloc+encode per iteration as intended for heap allocation cost benchmarking - Add comments documenting why _Fresh variants skip inner loops - Add first-wins comment to load_component_yaml_configs - Clean up .gitignore template comment --- script/test_helpers.py | 5 +++++ tests/benchmarks/components/.gitignore | 3 --- .../components/api/bench_proto_encode.cpp | 21 +++++++++---------- 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/script/test_helpers.py b/script/test_helpers.py index db1c981b82..24d99acaeb 100644 --- a/script/test_helpers.py +++ b/script/test_helpers.py @@ -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 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_encode.cpp b/tests/benchmarks/components/api/bench_proto_encode.cpp index c5dbd685be..11e4ccc4e3 100644 --- a/tests/benchmarks/components/api/bench_proto_encode.cpp +++ b/tests/benchmarks/components/api/bench_proto_encode.cpp @@ -70,7 +70,8 @@ static void CalcAndEncode_SensorStateResponse(benchmark::State &state) { } BENCHMARK(CalcAndEncode_SensorStateResponse); -// Cold path: fresh buffer each iteration (measures heap allocation) +// Cold path: fresh buffer each iteration (measures heap allocation). +// No inner loop — the point is to measure one alloc+encode per iteration. static void CalcAndEncode_SensorStateResponse_Fresh(benchmark::State &state) { SensorStateResponse msg; msg.key = 0x12345678; @@ -268,21 +269,19 @@ static void CalcAndEncode_DeviceInfoResponse(benchmark::State &state) { } BENCHMARK(CalcAndEncode_DeviceInfoResponse); -// Cold path: fresh buffer each iteration (measures heap allocation) +// Cold path: fresh buffer each iteration (measures heap allocation). +// No inner loop — the point is to measure one alloc+encode per iteration. static void CalcAndEncode_DeviceInfoResponse_Fresh(benchmark::State &state) { auto msg = make_device_info_response(); for (auto _ : state) { - 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()); - } + 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); From 2061fa23935ddb832642f66aa3623b5de1945fd1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 16 Mar 2026 23:53:19 -1000 Subject: [PATCH 04/19] Add inner loops to _Fresh benchmarks for CodSpeed consistency Both _Fresh variants now use kInnerIterations with fresh buffer creation inside the inner loop. This amortizes CodSpeed's per-iteration instrumentation overhead while still measuring alloc+calc+encode per item. --- .../components/api/bench_proto_encode.cpp | 42 ++++++++++++------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/tests/benchmarks/components/api/bench_proto_encode.cpp b/tests/benchmarks/components/api/bench_proto_encode.cpp index 11e4ccc4e3..656c1e17db 100644 --- a/tests/benchmarks/components/api/bench_proto_encode.cpp +++ b/tests/benchmarks/components/api/bench_proto_encode.cpp @@ -70,8 +70,10 @@ static void CalcAndEncode_SensorStateResponse(benchmark::State &state) { } BENCHMARK(CalcAndEncode_SensorStateResponse); -// Cold path: fresh buffer each iteration (measures heap allocation). -// No inner loop — the point is to measure one alloc+encode per iteration. +// 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; @@ -79,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); @@ -269,19 +274,24 @@ static void CalcAndEncode_DeviceInfoResponse(benchmark::State &state) { } BENCHMARK(CalcAndEncode_DeviceInfoResponse); -// Cold path: fresh buffer each iteration (measures heap allocation). -// No inner loop — the point is to measure one alloc+encode per iteration. +// 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); From ed539e17ff3ce8221d82a1c7290a90bc349dfd99 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 16 Mar 2026 23:57:39 -1000 Subject: [PATCH 05/19] Add scheduler benchmark with 5 intervals firing per call Adds Scheduler_Call_5IntervalsFiring: 5 intervals with 1ms period, time advancing each inner iteration so all 5 fire every call(). This benchmarks the real scheduler hot path where callbacks execute. --- tests/benchmarks/core/bench_scheduler.cpp | 28 +++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/benchmarks/core/bench_scheduler.cpp b/tests/benchmarks/core/bench_scheduler.cpp index d9d1575ebd..babcfc0be3 100644 --- a/tests/benchmarks/core/bench_scheduler.cpp +++ b/tests/benchmarks/core/bench_scheduler.cpp @@ -50,6 +50,34 @@ static void Scheduler_Call_TimersNotDue(benchmark::State &state) { } 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; + + // Add 5 intervals with 1ms period — they fire every call when time advances + for (int i = 0; i < 5; i++) { + scheduler.set_interval(&dummy_component, static_cast(i), 1, [&fire_count]() { fire_count++; }); + } + scheduler.process_to_add(); + + // Start at a known time so intervals are immediately due + uint32_t now = millis() + 100; + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + scheduler.call(now); + // Advance time by 1ms so intervals are due again next call + now++; + } + benchmark::DoNotOptimize(fire_count); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(Scheduler_Call_5IntervalsFiring); + // --- Scheduler: next_schedule_in() calculation --- static void Scheduler_NextScheduleIn(benchmark::State &state) { From 3fdb201f592b4e68100a1fa6f2fc83f2721edada Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 17 Mar 2026 00:16:44 -1000 Subject: [PATCH 06/19] Suppress blocking warnings in scheduler benchmarks Under valgrind, 2000 inner iterations take long enough in wall clock to trigger WarnIfComponentBlockingGuard. Use a BenchComponent subclass that sets warn_if_blocking_over_ to UINT16_MAX to prevent log noise. --- tests/benchmarks/core/bench_scheduler.cpp | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/tests/benchmarks/core/bench_scheduler.cpp b/tests/benchmarks/core/bench_scheduler.cpp index babcfc0be3..c32bfebcde 100644 --- a/tests/benchmarks/core/bench_scheduler.cpp +++ b/tests/benchmarks/core/bench_scheduler.cpp @@ -10,6 +10,15 @@ namespace esphome::benchmarks { // sub-microsecond benchmarks. static constexpr int kInnerIterations = 2000; +// Component subclass that suppresses blocking warnings. +// Under valgrind, 2000 inner iterations take long enough in wall clock +// to trigger WarnIfComponentBlockingGuard. Setting the threshold to max +// prevents log noise without affecting the benchmarked code path. +class BenchComponent : public Component { + public: + BenchComponent() { this->warn_if_blocking_over_ = UINT16_MAX; } +}; + // --- Scheduler fast path: no work to do --- static void Scheduler_Call_NoWork(benchmark::State &state) { @@ -30,7 +39,7 @@ BENCHMARK(Scheduler_Call_NoWork); static void Scheduler_Call_TimersNotDue(benchmark::State &state) { Scheduler scheduler; - Component dummy_component; + BenchComponent dummy_component; // Add some timeouts far in the future for (int i = 0; i < 10; i++) { @@ -54,7 +63,7 @@ BENCHMARK(Scheduler_Call_TimersNotDue); static void Scheduler_Call_5IntervalsFiring(benchmark::State &state) { Scheduler scheduler; - Component dummy_component; + BenchComponent dummy_component; int fire_count = 0; // Add 5 intervals with 1ms period — they fire every call when time advances @@ -82,7 +91,7 @@ BENCHMARK(Scheduler_Call_5IntervalsFiring); static void Scheduler_NextScheduleIn(benchmark::State &state) { Scheduler scheduler; - Component dummy_component; + BenchComponent dummy_component; // Add some timeouts for (int i = 0; i < 10; i++) { From 8c058cd7257df1864cbff9dac4b8ff90f6e48f7d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 17 Mar 2026 00:20:52 -1000 Subject: [PATCH 07/19] Replace hand-encoded protobuf bytes with programmatic encoding in decode benchmarks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Encode messages once in setup using the real protobuf API, then decode the resulting bytes in the benchmark loop. This keeps decode benchmarks automatically in sync with the protobuf schema — hand-encoded byte arrays would silently break when fields change. --- .../components/api/bench_proto_decode.cpp | 116 ++++++------------ 1 file changed, 38 insertions(+), 78 deletions(-) diff --git a/tests/benchmarks/components/api/bench_proto_decode.cpp b/tests/benchmarks/components/api/bench_proto_decode.cpp index a5ecf78cde..113201dd8a 100644 --- a/tests/benchmarks/components/api/bench_proto_decode.cpp +++ b/tests/benchmarks/components/api/bench_proto_decode.cpp @@ -10,24 +10,32 @@ namespace esphome::api::benchmarks { // 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; for (int i = 0; i < kInnerIterations; i++) { - msg.decode(encoded, sizeof(encoded)); + msg.decode(encoded.data(), encoded.size()); } benchmark::DoNotOptimize(msg.api_version_major); } @@ -38,17 +46,15 @@ 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; for (int i = 0; i < kInnerIterations; i++) { - msg.decode(encoded, sizeof(encoded)); + msg.decode(encoded.data(), encoded.size()); } benchmark::DoNotOptimize(msg.state); } @@ -59,70 +65,24 @@ 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; for (int i = 0; i < kInnerIterations; i++) { - msg.decode(encoded, sizeof(encoded)); + msg.decode(encoded.data(), encoded.size()); } benchmark::DoNotOptimize(msg.brightness); } From eb944651742354a623b7d8bd4b995b4a7a0c294a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 17 Mar 2026 00:23:15 -1000 Subject: [PATCH 08/19] Add logger benchmarks for format string hot paths Three benchmarks covering the most common ESP_LOGW patterns: - Logger_NoFormat: plain string, no format specifiers (fastest path) - Logger_3Uint32: 3x uint32_t (common for status/diagnostics) - Logger_3Float: 3x float with precision (common for sensor values) --- tests/benchmarks/core/bench_logger.cpp | 54 ++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 tests/benchmarks/core/bench_logger.cpp 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 From ca2cf4044c89b88ed86d4db638533ddf960492d9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 17 Mar 2026 00:35:53 -1000 Subject: [PATCH 09/19] Fix stray quote in main.cpp and scheduler time overflow - Remove trailing " from AUTO GENERATED INCLUDE BLOCK END comment - Reset scheduler `now` at start of each outer iteration to avoid unbounded growth toward UINT32_MAX across benchmark iterations --- tests/benchmarks/components/main.cpp | 2 +- tests/benchmarks/core/bench_scheduler.cpp | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) 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_scheduler.cpp b/tests/benchmarks/core/bench_scheduler.cpp index c32bfebcde..f49b19a626 100644 --- a/tests/benchmarks/core/bench_scheduler.cpp +++ b/tests/benchmarks/core/bench_scheduler.cpp @@ -72,10 +72,9 @@ static void Scheduler_Call_5IntervalsFiring(benchmark::State &state) { } scheduler.process_to_add(); - // Start at a known time so intervals are immediately due - uint32_t now = millis() + 100; - for (auto _ : state) { + // Reset each outer iteration to avoid unbounded growth toward UINT32_MAX + uint32_t now = 100; for (int i = 0; i < kInnerIterations; i++) { scheduler.call(now); // Advance time by 1ms so intervals are due again next call From e45cfc5451d52bfaa92cee48c25f4ff6a71f13b6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 17 Mar 2026 00:37:58 -1000 Subject: [PATCH 10/19] =?UTF-8?q?Revert=20scheduler=20now=20reset=20?= =?UTF-8?q?=E2=80=94=20must=20be=20monotonic=20for=20millis=5F64=5Ffrom=5F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit millis_64_from_() tracks 32-bit rollovers, so going backwards in time would appear as a ~49 day forward jump. Keep now monotonically increasing across all iterations. With 2000 inner iterations per outer iteration, overflow is not a practical concern. --- tests/benchmarks/core/bench_scheduler.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/benchmarks/core/bench_scheduler.cpp b/tests/benchmarks/core/bench_scheduler.cpp index f49b19a626..c0f47b0fc4 100644 --- a/tests/benchmarks/core/bench_scheduler.cpp +++ b/tests/benchmarks/core/bench_scheduler.cpp @@ -72,9 +72,13 @@ static void Scheduler_Call_5IntervalsFiring(benchmark::State &state) { } scheduler.process_to_add(); + // Start at a known time so intervals are immediately due. + // now increases monotonically across all iterations — this is required + // because millis_64_from_() tracks rollovers and going backwards would + // appear as a 32-bit wrap (~49 day jump forward). + uint32_t now = millis() + 100; + for (auto _ : state) { - // Reset each outer iteration to avoid unbounded growth toward UINT32_MAX - uint32_t now = 100; for (int i = 0; i < kInnerIterations; i++) { scheduler.call(now); // Advance time by 1ms so intervals are due again next call From 662780bcc61753a9aec6d5052e5c072b3f869b79 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 17 Mar 2026 00:38:48 -1000 Subject: [PATCH 11/19] Simplify scheduler now comment --- tests/benchmarks/core/bench_scheduler.cpp | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/benchmarks/core/bench_scheduler.cpp b/tests/benchmarks/core/bench_scheduler.cpp index c0f47b0fc4..897e3adc98 100644 --- a/tests/benchmarks/core/bench_scheduler.cpp +++ b/tests/benchmarks/core/bench_scheduler.cpp @@ -72,10 +72,7 @@ static void Scheduler_Call_5IntervalsFiring(benchmark::State &state) { } scheduler.process_to_add(); - // Start at a known time so intervals are immediately due. - // now increases monotonically across all iterations — this is required - // because millis_64_from_() tracks rollovers and going backwards would - // appear as a 32-bit wrap (~49 day jump forward). + // Must be monotonic — millis_64_from_() tracks rollovers. uint32_t now = millis() + 100; for (auto _ : state) { From 4565167bec8d98f664ecfed4115c9bcdb22ed336 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 17 Mar 2026 00:53:10 -1000 Subject: [PATCH 12/19] Fix scheduler benchmark triggering warn_blocking WarnIfComponentBlockingGuard compares the `now` passed to scheduler.call() against real millis() in finish(). Using fake time ahead of real millis() caused uint32_t underflow in the guard, triggering blocking warnings. Fix by reading real millis() at the start of each outer iteration so fake time stays close to wall clock. --- tests/benchmarks/core/bench_scheduler.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/benchmarks/core/bench_scheduler.cpp b/tests/benchmarks/core/bench_scheduler.cpp index 897e3adc98..40bed05327 100644 --- a/tests/benchmarks/core/bench_scheduler.cpp +++ b/tests/benchmarks/core/bench_scheduler.cpp @@ -72,13 +72,13 @@ static void Scheduler_Call_5IntervalsFiring(benchmark::State &state) { } scheduler.process_to_add(); - // Must be monotonic — millis_64_from_() tracks rollovers. - uint32_t now = millis() + 100; - for (auto _ : state) { + // Use real millis() each outer iteration so our fake time stays close + // to wall clock — WarnIfComponentBlockingGuard compares the `now` we + // pass to scheduler.call() against real millis() in finish(). + uint32_t now = millis(); for (int i = 0; i < kInnerIterations; i++) { scheduler.call(now); - // Advance time by 1ms so intervals are due again next call now++; } benchmark::DoNotOptimize(fire_count); From d7be19703bdcb56e8e7a7097bc6a568ccf7380c7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 17 Mar 2026 00:54:29 -1000 Subject: [PATCH 13/19] Fix scheduler intervals not firing and warn_blocking Use interval=0 so all 5 intervals fire unconditionally every call(). Pass real millis() to scheduler.call() so WarnIfComponentBlockingGuard doesn't see fake time ahead of wall clock (which causes uint32_t underflow in the blocking time calculation). --- tests/benchmarks/core/bench_scheduler.cpp | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/tests/benchmarks/core/bench_scheduler.cpp b/tests/benchmarks/core/bench_scheduler.cpp index 40bed05327..6282cc1597 100644 --- a/tests/benchmarks/core/bench_scheduler.cpp +++ b/tests/benchmarks/core/bench_scheduler.cpp @@ -66,20 +66,18 @@ static void Scheduler_Call_5IntervalsFiring(benchmark::State &state) { BenchComponent dummy_component; int fire_count = 0; - // Add 5 intervals with 1ms period — they fire every call when time advances + // Add 5 intervals with 0ms period — they fire every call() unconditionally. + // WarnIfComponentBlockingGuard compares the `now` we pass against real + // millis() in finish(), so we must pass real millis() to avoid underflow. + // With interval=0, all 5 fire every call without needing to advance time. for (int i = 0; i < 5; i++) { - scheduler.set_interval(&dummy_component, static_cast(i), 1, [&fire_count]() { fire_count++; }); + scheduler.set_interval(&dummy_component, static_cast(i), 0, [&fire_count]() { fire_count++; }); } scheduler.process_to_add(); for (auto _ : state) { - // Use real millis() each outer iteration so our fake time stays close - // to wall clock — WarnIfComponentBlockingGuard compares the `now` we - // pass to scheduler.call() against real millis() in finish(). - uint32_t now = millis(); for (int i = 0; i < kInnerIterations; i++) { - scheduler.call(now); - now++; + scheduler.call(millis()); } benchmark::DoNotOptimize(fire_count); } From ec60e2c228177c4422d0879573030c61218b5105 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 17 Mar 2026 01:00:06 -1000 Subject: [PATCH 14/19] Fix scheduler benchmark: use fake time with guard disabled at compile time interval=0 causes infinite loop (reschedules at same time, never breaks). interval=1 with millis() doesn't work (real time doesn't advance fast enough between inner iterations for intervals to re-fire). Solution: use interval=1 with monotonically increasing fake time (now++) and disable WarnIfComponentBlockingGuard at compile time via -DWARN_IF_BLOCKING_OVER_MS=UINT32_MAX in benchmark build flags. This prevents the guard's (millis() - started_) underflow when fake time exceeds real millis(). --- script/cpp_benchmark.py | 4 ++++ tests/benchmarks/core/bench_scheduler.cpp | 16 ++++++++++------ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/script/cpp_benchmark.py b/script/cpp_benchmark.py index f30054fbad..17613c2842 100755 --- a/script/cpp_benchmark.py +++ b/script/cpp_benchmark.py @@ -34,6 +34,10 @@ PLATFORMIO_OPTIONS = { "-O2", # optimize for speed (CodSpeed recommends RelWithDebInfo) "-g", # debug symbols for profiling USE_TIME_TIMEZONE_FLAG, + # Disable WarnIfComponentBlockingGuard check — scheduler benchmarks + # use fake monotonic time ahead of real millis(), causing uint32_t + # underflow in the guard's (millis() - started_) calculation. + "-DWARN_IF_BLOCKING_OVER_MS=UINT32_MAX", ], # Use deep+ LDF mode to ensure PlatformIO detects the benchmark # library dependency from nested includes. diff --git a/tests/benchmarks/core/bench_scheduler.cpp b/tests/benchmarks/core/bench_scheduler.cpp index 6282cc1597..4e3ef57084 100644 --- a/tests/benchmarks/core/bench_scheduler.cpp +++ b/tests/benchmarks/core/bench_scheduler.cpp @@ -66,18 +66,22 @@ static void Scheduler_Call_5IntervalsFiring(benchmark::State &state) { BenchComponent dummy_component; int fire_count = 0; - // Add 5 intervals with 0ms period — they fire every call() unconditionally. - // WarnIfComponentBlockingGuard compares the `now` we pass against real - // millis() in finish(), so we must pass real millis() to avoid underflow. - // With interval=0, all 5 fire every call without needing to advance time. + // Add 5 intervals with 1ms period — they fire every call when time advances. + // We use monotonically increasing fake time (now++) so intervals reliably fire. + // WARN_IF_BLOCKING_OVER_MS=UINT32_MAX in benchmark build flags prevents the + // WarnIfComponentBlockingGuard from triggering when fake time exceeds real millis(). + // Note: interval=0 causes an infinite loop (reschedules at same time, never breaks). for (int i = 0; i < 5; i++) { - scheduler.set_interval(&dummy_component, static_cast(i), 0, [&fire_count]() { fire_count++; }); + 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) { for (int i = 0; i < kInnerIterations; i++) { - scheduler.call(millis()); + scheduler.call(now); + now++; } benchmark::DoNotOptimize(fire_count); } From 1f9380ddc06a30e3cf2ddd7e92781942bd59b4be Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 17 Mar 2026 01:05:04 -1000 Subject: [PATCH 15/19] Fix scheduler firing benchmark: no inner loop, warm-up call MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Revert component.h change (no core changes for benchmarks) - Remove -DWARN_IF_BLOCKING_OVER_MS from build flags (can't shadow constexpr) - Drop inner loop — 5 heap pops + callbacks + pushes per call is well above CodSpeed's 60ns instrumentation overhead - Add warm-up call before benchmark loop to trigger the blocking guard once and ramp the threshold - interval=0 causes infinite loop, must use interval=1 with fake time --- script/cpp_benchmark.py | 4 ---- tests/benchmarks/core/bench_scheduler.cpp | 20 +++++++++++--------- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/script/cpp_benchmark.py b/script/cpp_benchmark.py index 17613c2842..f30054fbad 100755 --- a/script/cpp_benchmark.py +++ b/script/cpp_benchmark.py @@ -34,10 +34,6 @@ PLATFORMIO_OPTIONS = { "-O2", # optimize for speed (CodSpeed recommends RelWithDebInfo) "-g", # debug symbols for profiling USE_TIME_TIMEZONE_FLAG, - # Disable WarnIfComponentBlockingGuard check — scheduler benchmarks - # use fake monotonic time ahead of real millis(), causing uint32_t - # underflow in the guard's (millis() - started_) calculation. - "-DWARN_IF_BLOCKING_OVER_MS=UINT32_MAX", ], # Use deep+ LDF mode to ensure PlatformIO detects the benchmark # library dependency from nested includes. diff --git a/tests/benchmarks/core/bench_scheduler.cpp b/tests/benchmarks/core/bench_scheduler.cpp index 4e3ef57084..1b78c44376 100644 --- a/tests/benchmarks/core/bench_scheduler.cpp +++ b/tests/benchmarks/core/bench_scheduler.cpp @@ -67,25 +67,27 @@ static void Scheduler_Call_5IntervalsFiring(benchmark::State &state) { int fire_count = 0; // Add 5 intervals with 1ms period — they fire every call when time advances. - // We use monotonically increasing fake time (now++) so intervals reliably fire. - // WARN_IF_BLOCKING_OVER_MS=UINT32_MAX in benchmark build flags prevents the - // WarnIfComponentBlockingGuard from triggering when fake time exceeds real millis(). - // Note: interval=0 causes an infinite loop (reschedules at same time, never breaks). + // No inner loop needed: 5 heap pops + 5 callbacks + 5 heap pushes per call + // is well above CodSpeed's ~60ns instrumentation overhead. + // Note: interval=0 causes infinite loop (reschedules at same now, never breaks). for (int i = 0; i < 5; i++) { scheduler.set_interval(&dummy_component, static_cast(i), 1, [&fire_count]() { fire_count++; }); } scheduler.process_to_add(); + // Monotonically increasing fake time so intervals are due every call. + // Can't use real millis() — it doesn't advance fast enough between calls. + // Warm-up call outside the benchmark to trigger the blocking guard once + // and ramp the component's warn_if_blocking_over_ threshold to max. uint32_t now = millis() + 100; + scheduler.call(now); + now++; for (auto _ : state) { - for (int i = 0; i < kInnerIterations; i++) { - scheduler.call(now); - now++; - } + scheduler.call(now); + now++; benchmark::DoNotOptimize(fire_count); } - state.SetItemsProcessed(state.iterations() * kInnerIterations); } BENCHMARK(Scheduler_Call_5IntervalsFiring); From 885d7c3938c31d7d6c5afdef76368021e1eadf9b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 17 Mar 2026 01:10:57 -1000 Subject: [PATCH 16/19] [core] Fix uint32_t underflow in WarnIfComponentBlockingGuard::finish() When curr_time (from millis()) is less than started_ (the `now` passed to scheduler.call()), the subtraction wraps to a huge value (~4 billion). This triggers spurious blocking warnings with nonsensical times. This can happen when the scheduler's execute_item_() returns a millis() value that subsequent items use as their guard start, but the next scheduler.call() passes a `now` value from a slightly different source. Fix by skipping the blocking check when curr_time < started_ (underflow). Also restore the scheduler firing benchmark to use intervals with monotonically increasing fake time, now that the guard handles underflow. --- esphome/core/component.h | 11 ++++++++--- tests/benchmarks/core/bench_scheduler.cpp | 11 +++-------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/esphome/core/component.h b/esphome/core/component.h index 5fdf23e128..64f9971627 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -594,12 +594,17 @@ class WarnIfComponentBlockingGuard { // Inlined: the fast path is just millis() + subtract + compare inline uint32_t HOT finish() { uint32_t curr_time = millis(); - uint32_t blocking_time = curr_time - this->started_; #ifdef USE_RUNTIME_STATS this->record_runtime_stats_(); #endif - if (blocking_time > WARN_IF_BLOCKING_OVER_MS) [[unlikely]] { - warn_blocking(this->component_, blocking_time); + // Guard against underflow: if curr_time < started_, the subtraction wraps + // to a huge value. This can happen when the scheduler passes a `now` value + // slightly ahead of real millis() (e.g. from execute_item_ return values). + if (curr_time >= this->started_) [[likely]] { + uint32_t blocking_time = curr_time - this->started_; + if (blocking_time > WARN_IF_BLOCKING_OVER_MS) [[unlikely]] { + warn_blocking(this->component_, blocking_time); + } } return curr_time; } diff --git a/tests/benchmarks/core/bench_scheduler.cpp b/tests/benchmarks/core/bench_scheduler.cpp index 1b78c44376..160ab3c123 100644 --- a/tests/benchmarks/core/bench_scheduler.cpp +++ b/tests/benchmarks/core/bench_scheduler.cpp @@ -67,21 +67,16 @@ static void Scheduler_Call_5IntervalsFiring(benchmark::State &state) { int fire_count = 0; // Add 5 intervals with 1ms period — they fire every call when time advances. - // No inner loop needed: 5 heap pops + 5 callbacks + 5 heap pushes per call - // is well above CodSpeed's ~60ns instrumentation overhead. + // We use monotonically increasing fake time (now++) so intervals reliably fire. + // The underflow guard in WarnIfComponentBlockingGuard::finish() (curr_time >= started_) + // prevents warn_blocking from firing when fake time exceeds real millis(). // Note: interval=0 causes infinite loop (reschedules at same now, never breaks). for (int i = 0; i < 5; i++) { scheduler.set_interval(&dummy_component, static_cast(i), 1, [&fire_count]() { fire_count++; }); } scheduler.process_to_add(); - // Monotonically increasing fake time so intervals are due every call. - // Can't use real millis() — it doesn't advance fast enough between calls. - // Warm-up call outside the benchmark to trigger the blocking guard once - // and ramp the component's warn_if_blocking_over_ threshold to max. uint32_t now = millis() + 100; - scheduler.call(now); - now++; for (auto _ : state) { scheduler.call(now); From 2785edaac878d59bdfe6351e317465f881f5403f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 17 Mar 2026 01:11:48 -1000 Subject: [PATCH 17/19] [core] Clamp underflowed blocking_time in warn_blocking cold path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When millis() < started_ (e.g. scheduler passes a now value slightly ahead of real millis()), the uint32_t subtraction in finish() wraps to ~4 billion. This caused warn_blocking to fire on every call since the underflowed value always exceeds the uint16_t threshold max (65535). Fix by clamping blocking_time to uint16_t max in the cold warn_blocking path. After one warning, should_warn_of_blocking() saturates the threshold to 65535 and subsequent clamped values (65535) don't exceed it. Zero cost on the hot path — the clamp is in the noinline cold function. --- esphome/core/component.cpp | 7 +++++++ esphome/core/component.h | 11 +++-------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index bfe9beb272..172842ee3d 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -512,6 +512,13 @@ void PollingComponent::set_update_interval(uint32_t update_interval) { this->upd void __attribute__((noinline, cold)) WarnIfComponentBlockingGuard::warn_blocking(Component *component, uint32_t blocking_time) { + // Clamp underflowed values: if millis() < started_ (e.g. scheduler passes + // a `now` slightly ahead of real millis()), the subtraction wraps to ~4 billion. + // Clamping to uint16_t max lets should_warn_of_blocking() saturate the + // threshold and suppress further warnings. + if (blocking_time > std::numeric_limits::max()) { + blocking_time = std::numeric_limits::max(); + } bool should_warn; if (component != nullptr) { should_warn = component->should_warn_of_blocking(blocking_time); diff --git a/esphome/core/component.h b/esphome/core/component.h index 64f9971627..5fdf23e128 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -594,17 +594,12 @@ class WarnIfComponentBlockingGuard { // Inlined: the fast path is just millis() + subtract + compare inline uint32_t HOT finish() { uint32_t curr_time = millis(); + uint32_t blocking_time = curr_time - this->started_; #ifdef USE_RUNTIME_STATS this->record_runtime_stats_(); #endif - // Guard against underflow: if curr_time < started_, the subtraction wraps - // to a huge value. This can happen when the scheduler passes a `now` value - // slightly ahead of real millis() (e.g. from execute_item_ return values). - if (curr_time >= this->started_) [[likely]] { - uint32_t blocking_time = curr_time - this->started_; - if (blocking_time > WARN_IF_BLOCKING_OVER_MS) [[unlikely]] { - warn_blocking(this->component_, blocking_time); - } + if (blocking_time > WARN_IF_BLOCKING_OVER_MS) [[unlikely]] { + warn_blocking(this->component_, blocking_time); } return curr_time; } From e01670eed897821979ede5ffdaa7788d63f835ce Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 17 Mar 2026 01:14:39 -1000 Subject: [PATCH 18/19] Revert core changes, use intervals with fake time for scheduler benchmark MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The warn_blocking underflow only happens with fake time in benchmarks, not in production (millis() is monotonic). Accept the consistent overhead from one warning per call — CodSpeed regression detection works on relative changes, not absolute values. --- esphome/core/component.cpp | 7 ------- tests/benchmarks/core/bench_scheduler.cpp | 11 ++++++----- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index 172842ee3d..bfe9beb272 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -512,13 +512,6 @@ void PollingComponent::set_update_interval(uint32_t update_interval) { this->upd void __attribute__((noinline, cold)) WarnIfComponentBlockingGuard::warn_blocking(Component *component, uint32_t blocking_time) { - // Clamp underflowed values: if millis() < started_ (e.g. scheduler passes - // a `now` slightly ahead of real millis()), the subtraction wraps to ~4 billion. - // Clamping to uint16_t max lets should_warn_of_blocking() saturate the - // threshold and suppress further warnings. - if (blocking_time > std::numeric_limits::max()) { - blocking_time = std::numeric_limits::max(); - } bool should_warn; if (component != nullptr) { should_warn = component->should_warn_of_blocking(blocking_time); diff --git a/tests/benchmarks/core/bench_scheduler.cpp b/tests/benchmarks/core/bench_scheduler.cpp index 160ab3c123..0ae8b4add0 100644 --- a/tests/benchmarks/core/bench_scheduler.cpp +++ b/tests/benchmarks/core/bench_scheduler.cpp @@ -66,11 +66,12 @@ static void Scheduler_Call_5IntervalsFiring(benchmark::State &state) { BenchComponent dummy_component; int fire_count = 0; - // Add 5 intervals with 1ms period — they fire every call when time advances. - // We use monotonically increasing fake time (now++) so intervals reliably fire. - // The underflow guard in WarnIfComponentBlockingGuard::finish() (curr_time >= started_) - // prevents warn_blocking from firing when fake time exceeds real millis(). - // Note: interval=0 causes infinite loop (reschedules at same now, never breaks). + // Benchmarks the heap-based scheduler dispatch with 5 callbacks firing. + // Uses monotonically increasing fake time so intervals reliably fire every call. + // The first item per call triggers one WarnIfComponentBlockingGuard warning + // (fake now > real millis() causes underflow in finish()), but this is + // consistent overhead per iteration so CodSpeed regression detection works. + // 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++; }); } From 804e2330ecdc7fa002dba681d996c928c08436d5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 17 Mar 2026 01:16:34 -1000 Subject: [PATCH 19/19] Ifdef out WarnIfComponentBlockingGuard for benchmark builds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add USE_BENCHMARK define to benchmark build flags. Guard the warn_blocking call in finish() with #ifndef USE_BENCHMARK so scheduler benchmarks using fake monotonic time don't trigger the underflow (fake now > real millis()). Remove BenchComponent — no longer needed with the ifdef. --- esphome/core/component.h | 2 ++ script/cpp_benchmark.py | 1 + tests/benchmarks/core/bench_scheduler.cpp | 20 +++++--------------- 3 files changed, 8 insertions(+), 15 deletions(-) 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/tests/benchmarks/core/bench_scheduler.cpp b/tests/benchmarks/core/bench_scheduler.cpp index 0ae8b4add0..3d2cd0bda2 100644 --- a/tests/benchmarks/core/bench_scheduler.cpp +++ b/tests/benchmarks/core/bench_scheduler.cpp @@ -10,15 +10,6 @@ namespace esphome::benchmarks { // sub-microsecond benchmarks. static constexpr int kInnerIterations = 2000; -// Component subclass that suppresses blocking warnings. -// Under valgrind, 2000 inner iterations take long enough in wall clock -// to trigger WarnIfComponentBlockingGuard. Setting the threshold to max -// prevents log noise without affecting the benchmarked code path. -class BenchComponent : public Component { - public: - BenchComponent() { this->warn_if_blocking_over_ = UINT16_MAX; } -}; - // --- Scheduler fast path: no work to do --- static void Scheduler_Call_NoWork(benchmark::State &state) { @@ -39,7 +30,7 @@ BENCHMARK(Scheduler_Call_NoWork); static void Scheduler_Call_TimersNotDue(benchmark::State &state) { Scheduler scheduler; - BenchComponent dummy_component; + Component dummy_component; // Add some timeouts far in the future for (int i = 0; i < 10; i++) { @@ -63,14 +54,13 @@ BENCHMARK(Scheduler_Call_TimersNotDue); static void Scheduler_Call_5IntervalsFiring(benchmark::State &state) { Scheduler scheduler; - BenchComponent dummy_component; + 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. - // The first item per call triggers one WarnIfComponentBlockingGuard warning - // (fake now > real millis() causes underflow in finish()), but this is - // consistent overhead per iteration so CodSpeed regression detection works. + // 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++; }); @@ -91,7 +81,7 @@ BENCHMARK(Scheduler_Call_5IntervalsFiring); static void Scheduler_NextScheduleIn(benchmark::State &state) { Scheduler scheduler; - BenchComponent dummy_component; + Component dummy_component; // Add some timeouts for (int i = 0; i < 10; i++) {