Merge branch 'codspeed-benchmarks' into scheduler-process-to-add-fast-path

This commit is contained in:
J. Nick Koston
2026-03-17 01:38:00 -10:00
committed by GitHub
13 changed files with 329 additions and 141 deletions
-1
View File
@@ -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:
+2
View File
@@ -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;
}
+1
View File
@@ -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.
+15
View File
@@ -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"
+7 -2
View File
@@ -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:
-3
View File
@@ -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
@@ -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<typename T> 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);
@@ -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);
@@ -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);
+1 -1
View File
@@ -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.)
+17 -2
View File
@@ -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);
+54
View File
@@ -0,0 +1,54 @@
#include <benchmark/benchmark.h>
#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
+45 -3
View File
@@ -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<uint32_t>(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<uint32_t> 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);