mirror of
https://github.com/esphome/esphome.git
synced 2026-09-15 09:08:41 +00:00
Merge branch 'codspeed-benchmarks' into scheduler-process-to-add-fast-path
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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.)
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user