From cdcc5e5932c99f6127925f8f4dd24a821c4ecb86 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 3 Mar 2026 20:22:27 -1000 Subject: [PATCH] Rename ProtoSize static methods with calc_ prefix for clang-tidy --- tests/benchmarks/proto_encode_benchmark.cpp | 795 ++++++++++++++++++++ 1 file changed, 795 insertions(+) create mode 100644 tests/benchmarks/proto_encode_benchmark.cpp diff --git a/tests/benchmarks/proto_encode_benchmark.cpp b/tests/benchmarks/proto_encode_benchmark.cpp new file mode 100644 index 0000000000..ac49e139ae --- /dev/null +++ b/tests/benchmarks/proto_encode_benchmark.cpp @@ -0,0 +1,795 @@ +/** + * Benchmark: ProtoWriteBuffer encoding performance + * + * Compares the old push_back()-based encoding against the new pre-sized + * pointer-write approach introduced in PR #14018. + * + * Build (from repo root): + * g++ -std=gnu++20 -O2 \ + * tests/benchmarks/proto_encode_benchmark.cpp \ + * -o tests/benchmarks/proto_encode_benchmark + * + * For ESP-like size-optimized builds (-Os): + * g++ -std=gnu++20 -Os \ + * tests/benchmarks/proto_encode_benchmark.cpp \ + * -o tests/benchmarks/proto_encode_benchmark + * + * Run: + * ./tests/benchmarks/proto_encode_benchmark + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// ============================================================================ +// Minimal stubs to avoid pulling in the full ESPHome framework +// ============================================================================ + +namespace esphome { + +class StringRef { + public: + constexpr StringRef() : base_(""), len_(0) {} + explicit StringRef(const char *s) : base_(s), len_(strlen(s)) {} + constexpr StringRef(const char *s, size_t n) : base_(s), len_(n) {} + explicit StringRef(const std::string &s) : base_(s.c_str()), len_(s.size()) {} + + const char *c_str() const { return base_; } + size_t size() const { return len_; } + bool empty() const { return len_ == 0; } + + private: + const char *base_; + size_t len_; +}; + +} // namespace esphome + +// ============================================================================ +// Old-style ProtoWriteBuffer (push_back based) - from dev branch +// ============================================================================ + +class OldProtoWriteBuffer { + public: + explicit OldProtoWriteBuffer(std::vector *buffer) : buffer_(buffer) {} + + void encode_varint_raw(uint32_t value) { + while (value > 0x7F) { + this->buffer_->push_back(static_cast(value | 0x80)); + value >>= 7; + } + this->buffer_->push_back(static_cast(value)); + } + + void encode_varint_raw_64(uint64_t value) { + while (value > 0x7F) { + this->buffer_->push_back(static_cast(value | 0x80)); + value >>= 7; + } + this->buffer_->push_back(static_cast(value)); + } + + void encode_field_raw(uint32_t field_id, uint32_t type) { this->encode_varint_raw((field_id << 3) | type); } + + void encode_string(uint32_t field_id, const char *string, size_t len, bool force = false) { + if (len == 0 && !force) + return; + this->encode_field_raw(field_id, 2); + this->encode_varint_raw(len); + size_t old_size = this->buffer_->size(); + this->buffer_->resize(old_size + len); + std::memcpy(this->buffer_->data() + old_size, string, len); + } + + void encode_string(uint32_t field_id, const esphome::StringRef &ref, bool force = false) { + this->encode_string(field_id, ref.c_str(), ref.size(), force); + } + + void encode_uint32(uint32_t field_id, uint32_t value, bool force = false) { + if (value == 0 && !force) + return; + this->encode_field_raw(field_id, 0); + this->encode_varint_raw(value); + } + + void encode_bool(uint32_t field_id, bool value, bool force = false) { + if (!value && !force) + return; + this->encode_field_raw(field_id, 0); + this->buffer_->push_back(value ? 0x01 : 0x00); + } + + void encode_fixed32(uint32_t field_id, uint32_t value, bool force = false) { + if (value == 0 && !force) + return; + this->encode_field_raw(field_id, 5); + this->buffer_->push_back((value >> 0) & 0xFF); + this->buffer_->push_back((value >> 8) & 0xFF); + this->buffer_->push_back((value >> 16) & 0xFF); + this->buffer_->push_back((value >> 24) & 0xFF); + } + + void encode_float(uint32_t field_id, float value, bool force = false) { + if (value == 0.0f && !force) + return; + union { + float value; + uint32_t raw; + } val{}; + val.value = value; + this->encode_fixed32(field_id, val.raw); + } + + void encode_bytes(uint32_t field_id, const uint8_t *data, size_t len, bool force = false) { + this->encode_string(field_id, reinterpret_cast(data), len, force); + } + + std::vector *get_buffer() const { return buffer_; } + + protected: + std::vector *buffer_; +}; + +// ============================================================================ +// New-style ProtoWriteBuffer (pointer-write based) - from this PR +// ============================================================================ + +class NewProtoWriteBuffer { + public: + NewProtoWriteBuffer(std::vector *buffer, size_t write_pos) + : buffer_(buffer), pos_(buffer->data() + write_pos) {} + + void encode_varint_raw(uint32_t value) { + while (value > 0x7F) { + *this->pos_++ = static_cast(value | 0x80); + value >>= 7; + } + *this->pos_++ = static_cast(value); + } + + void encode_varint_raw_64(uint64_t value) { + while (value > 0x7F) { + *this->pos_++ = static_cast(value | 0x80); + value >>= 7; + } + *this->pos_++ = static_cast(value); + } + + void encode_field_raw(uint32_t field_id, uint32_t type) { this->encode_varint_raw((field_id << 3) | type); } + + void encode_string(uint32_t field_id, const char *string, size_t len, bool force = false) { + if (len == 0 && !force) + return; + this->encode_field_raw(field_id, 2); + this->encode_varint_raw(len); + std::memcpy(this->pos_, string, len); + this->pos_ += len; + } + + void encode_string(uint32_t field_id, const esphome::StringRef &ref, bool force = false) { + this->encode_string(field_id, ref.c_str(), ref.size(), force); + } + + void encode_uint32(uint32_t field_id, uint32_t value, bool force = false) { + if (value == 0 && !force) + return; + this->encode_field_raw(field_id, 0); + this->encode_varint_raw(value); + } + + void encode_bool(uint32_t field_id, bool value, bool force = false) { + if (!value && !force) + return; + this->encode_field_raw(field_id, 0); + *this->pos_++ = value ? 0x01 : 0x00; + } + + void encode_fixed32(uint32_t field_id, uint32_t value, bool force = false) { + if (value == 0 && !force) + return; + this->encode_field_raw(field_id, 5); +#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ + std::memcpy(this->pos_, &value, 4); + this->pos_ += 4; +#else + *this->pos_++ = (value >> 0) & 0xFF; + *this->pos_++ = (value >> 8) & 0xFF; + *this->pos_++ = (value >> 16) & 0xFF; + *this->pos_++ = (value >> 24) & 0xFF; +#endif + } + + void encode_float(uint32_t field_id, float value, bool force = false) { + if (value == 0.0f && !force) + return; + union { + float value; + uint32_t raw; + } val{}; + val.value = value; + this->encode_fixed32(field_id, val.raw); + } + + void encode_bytes(uint32_t field_id, const uint8_t *data, size_t len, bool force = false) { + this->encode_string(field_id, reinterpret_cast(data), len, force); + } + + uint8_t *pos() const { return pos_; } + std::vector *get_buffer() const { return buffer_; } + + protected: + std::vector *buffer_; + uint8_t *pos_; +}; + +// ============================================================================ +// ProtoSize - calculate exact encoded size (shared by both approaches) +// ============================================================================ + +class ProtoSize { + public: + static constexpr uint32_t varint(uint32_t value) { + if (value < 128) + return 1; + if (value < 16384) + return 2; + if (value < 2097152) + return 3; + if (value < 268435456) + return 4; + return 5; + } + + static constexpr uint32_t field(uint32_t field_id, uint32_t type) { return varint((field_id << 3) | (type & 0x7)); } + + static constexpr uint32_t calc_uint32(uint32_t field_id_size, uint32_t value) { + return value ? field_id_size + varint(value) : 0; + } + + static constexpr uint32_t calc_bool(uint32_t field_id_size, bool value) { return value ? field_id_size + 1 : 0; } + + static constexpr uint32_t calc_float(uint32_t field_id_size, float value) { + return value != 0.0f ? field_id_size + 4 : 0; + } + + static constexpr uint32_t calc_fixed32(uint32_t field_id_size, uint32_t value) { + return value ? field_id_size + 4 : 0; + } + + static constexpr uint32_t calc_length(uint32_t field_id_size, size_t len) { + return len ? field_id_size + varint(static_cast(len)) + static_cast(len) : 0; + } +}; + +// ============================================================================ +// Benchmark infrastructure +// ============================================================================ + +struct BenchResult { + const char *name; + double ns_per_op; + double ops_per_sec; + size_t iterations; + size_t bytes_per_op; +}; + +// Prevent compiler from optimizing away the result +template __attribute__((noinline)) void do_not_optimize(T &value) { + asm volatile("" : "+r,m"(value) : : "memory"); +} + +__attribute__((noinline)) void clobber_memory() { asm volatile("" : : : "memory"); } + +template BenchResult benchmark(const char *name, size_t bytes_per_op, Func func) { + // Warmup + for (int i = 0; i < 1000; i++) { + func(); + } + + // Determine iteration count (target ~100ms) + size_t iterations = 1000; + auto start = std::chrono::high_resolution_clock::now(); + for (size_t i = 0; i < iterations; i++) { + func(); + } + auto end = std::chrono::high_resolution_clock::now(); + double elapsed_ns = std::chrono::duration_cast(end - start).count(); + double ns_per_op = elapsed_ns / iterations; + + // Scale iterations to target ~200ms + iterations = std::max(10000, static_cast(200'000'000.0 / ns_per_op)); + + // Actual benchmark run + start = std::chrono::high_resolution_clock::now(); + for (size_t i = 0; i < iterations; i++) { + func(); + clobber_memory(); + } + end = std::chrono::high_resolution_clock::now(); + elapsed_ns = std::chrono::duration_cast(end - start).count(); + ns_per_op = elapsed_ns / iterations; + + return BenchResult{name, ns_per_op, 1'000'000'000.0 / ns_per_op, iterations, bytes_per_op}; +} + +void print_results(const std::vector &results) { + printf("%-50s %12s %12s %12s %10s\n", "Benchmark", "ns/op", "ops/sec", "iters", "bytes/op"); + printf("%-50s %12s %12s %12s %10s\n", std::string(50, '-').c_str(), "--------", "--------", "--------", "--------"); + for (const auto &r : results) { + printf("%-50s %12.1f %12.0f %12zu %10zu\n", r.name, r.ns_per_op, r.ops_per_sec, r.iterations, r.bytes_per_op); + } +} + +void print_comparison(const char *label, const BenchResult &old_result, const BenchResult &new_result) { + double speedup = old_result.ns_per_op / new_result.ns_per_op; + printf(" %-46s %.1fx %s\n", label, speedup, speedup > 1.0 ? "faster" : "slower"); +} + +// ============================================================================ +// Benchmark: Varint encoding +// ============================================================================ + +static void bench_varint_old(std::vector &buf) { + buf.clear(); + OldProtoWriteBuffer writer(&buf); + // Encode a mix of varint sizes (1-5 bytes) + writer.encode_varint_raw(0x01); // 1 byte + writer.encode_varint_raw(0x80); // 2 bytes + writer.encode_varint_raw(0x4000); // 3 bytes + writer.encode_varint_raw(0x200000); // 4 bytes + writer.encode_varint_raw(0x10000000); // 5 bytes +} + +static void bench_varint_new(std::vector &buf, size_t size) { + buf.resize(size); + NewProtoWriteBuffer writer(&buf, 0); + writer.encode_varint_raw(0x01); + writer.encode_varint_raw(0x80); + writer.encode_varint_raw(0x4000); + writer.encode_varint_raw(0x200000); + writer.encode_varint_raw(0x10000000); +} + +// ============================================================================ +// Benchmark: String encoding (simulates entity names, object_ids, etc.) +// ============================================================================ + +static const char SHORT_STR[] = "sensor_1"; // 8 bytes +static const char MEDIUM_STR[] = "living_room_temperature_sensor"; // 30 bytes +static const char LONG_STR[] = + "esphome_very_long_device_name_with_many_characters_for_testing_purposes_abcdef"; // 78 bytes + +static void bench_strings_old(std::vector &buf) { + buf.clear(); + OldProtoWriteBuffer writer(&buf); + writer.encode_string(1, SHORT_STR, strlen(SHORT_STR)); + writer.encode_string(2, MEDIUM_STR, strlen(MEDIUM_STR)); + writer.encode_string(3, LONG_STR, strlen(LONG_STR)); +} + +static size_t calc_strings_size() { + uint32_t size = 0; + size += ProtoSize::calc_length(1, strlen(SHORT_STR)); + size += ProtoSize::calc_length(1, strlen(MEDIUM_STR)); + size += ProtoSize::calc_length(1, strlen(LONG_STR)); + return size; +} + +static void bench_strings_new(std::vector &buf, size_t size) { + buf.resize(size); + NewProtoWriteBuffer writer(&buf, 0); + writer.encode_string(1, SHORT_STR, strlen(SHORT_STR)); + writer.encode_string(2, MEDIUM_STR, strlen(MEDIUM_STR)); + writer.encode_string(3, LONG_STR, strlen(LONG_STR)); +} + +// ============================================================================ +// Benchmark: Fixed32 encoding (simulates key fields in state responses) +// ============================================================================ + +static void bench_fixed32_old(std::vector &buf) { + buf.clear(); + OldProtoWriteBuffer writer(&buf); + for (uint32_t i = 1; i <= 10; i++) { + writer.encode_fixed32(i, 0xDEADBEEF); + } +} + +static size_t calc_fixed32_size() { + uint32_t size = 0; + for (uint32_t i = 1; i <= 10; i++) { + size += ProtoSize::calc_fixed32(1, 0xDEADBEEF); + } + return size; +} + +static void bench_fixed32_new(std::vector &buf, size_t size) { + buf.resize(size); + NewProtoWriteBuffer writer(&buf, 0); + for (uint32_t i = 1; i <= 10; i++) { + writer.encode_fixed32(i, 0xDEADBEEF); + } +} + +// ============================================================================ +// Benchmark: Simulate SensorStateResponse encoding +// SensorStateResponse has: fixed32 key, float state, bool missing_state +// This is the most frequent message type during normal operation. +// ============================================================================ + +static void bench_sensor_state_old(std::vector &buf) { + buf.clear(); + OldProtoWriteBuffer writer(&buf); + writer.encode_fixed32(1, 0x12345678); // key + writer.encode_float(2, 23.5f); // state + writer.encode_bool(3, false); // missing_state (default, skipped) +} + +static size_t calc_sensor_state_size() { + uint32_t size = 0; + size += ProtoSize::calc_fixed32(1, 0x12345678); + size += ProtoSize::calc_float(1, 23.5f); + size += ProtoSize::calc_bool(1, false); + return size; +} + +static void bench_sensor_state_new(std::vector &buf, size_t size) { + buf.resize(size); + NewProtoWriteBuffer writer(&buf, 0); + writer.encode_fixed32(1, 0x12345678); + writer.encode_float(2, 23.5f); + writer.encode_bool(3, false); +} + +// ============================================================================ +// Benchmark: Simulate ListEntitiesSensorResponse encoding +// This is a larger message sent during entity listing. +// Fields: object_id, key, name, unique_id, icon, unit_of_measurement, +// accuracy_decimals, force_update, device_class, state_class +// ============================================================================ + +static const char OBJ_ID[] = "living_room_temp"; +static const char NAME[] = "Living Room Temperature"; +static const char UNIQUE_ID[] = "esp32_01-sensor-living_room_temp"; +static const char ICON[] = "mdi:thermometer"; +static const char UNIT[] = "\xc2\xb0" + "C"; // UTF-8 degree C +static const char DEVICE_CLASS[] = "temperature"; + +static void bench_list_entities_old(std::vector &buf) { + buf.clear(); + OldProtoWriteBuffer writer(&buf); + writer.encode_string(1, OBJ_ID, strlen(OBJ_ID)); // object_id + writer.encode_fixed32(2, 0xABCD1234); // key + writer.encode_string(3, NAME, strlen(NAME)); // name + writer.encode_string(4, UNIQUE_ID, strlen(UNIQUE_ID)); // unique_id + writer.encode_string(5, ICON, strlen(ICON)); // icon + writer.encode_string(6, UNIT, strlen(UNIT)); // unit_of_measurement + writer.encode_uint32(7, 1); // accuracy_decimals + writer.encode_bool(8, false); // force_update + writer.encode_string(9, DEVICE_CLASS, strlen(DEVICE_CLASS)); // device_class + writer.encode_uint32(10, 1); // state_class +} + +static size_t calc_list_entities_size() { + uint32_t size = 0; + size += ProtoSize::calc_length(1, strlen(OBJ_ID)); + size += ProtoSize::calc_fixed32(1, 0xABCD1234); + size += ProtoSize::calc_length(1, strlen(NAME)); + size += ProtoSize::calc_length(1, strlen(UNIQUE_ID)); + size += ProtoSize::calc_length(1, strlen(ICON)); + size += ProtoSize::calc_length(1, strlen(UNIT)); + size += ProtoSize::calc_uint32(1, 1); + size += ProtoSize::calc_bool(1, false); + size += ProtoSize::calc_length(1, strlen(DEVICE_CLASS)); + size += ProtoSize::calc_uint32(1, 1); + return size; +} + +static void bench_list_entities_new(std::vector &buf, size_t size) { + buf.resize(size); + NewProtoWriteBuffer writer(&buf, 0); + writer.encode_string(1, OBJ_ID, strlen(OBJ_ID)); + writer.encode_fixed32(2, 0xABCD1234); + writer.encode_string(3, NAME, strlen(NAME)); + writer.encode_string(4, UNIQUE_ID, strlen(UNIQUE_ID)); + writer.encode_string(5, ICON, strlen(ICON)); + writer.encode_string(6, UNIT, strlen(UNIT)); + writer.encode_uint32(7, 1); + writer.encode_bool(8, false); + writer.encode_string(9, DEVICE_CLASS, strlen(DEVICE_CLASS)); + writer.encode_uint32(10, 1); +} + +// ============================================================================ +// Benchmark: Simulate BLE advertisement batch encoding +// BluetoothLERawAdvertisementsResponse with multiple advertisements. +// Each advert has: uint64 address, sint32 rssi, uint32 address_type, bytes data +// This is a high-frequency message that benefits most from optimization. +// ============================================================================ + +static const uint8_t FAKE_BLE_DATA[31] = {0x02, 0x01, 0x06, 0x11, 0x07, 0xFB, 0x34, 0x9B, 0x5F, 0x80, 0x00, + 0x00, 0x80, 0x00, 0x10, 0x00, 0x00, 0x15, 0x12, 0x00, 0x00, 0x03, + 0x09, 0x54, 0x65, 0x73, 0x74, 0x00, 0x00, 0x00, 0x00}; + +static void bench_ble_batch_old(std::vector &buf) { + buf.clear(); + OldProtoWriteBuffer writer(&buf); + // Simulate encoding 8 BLE advertisements + for (int i = 0; i < 8; i++) { + // Each advertisement fields (flattened, no nested message for simplicity) + writer.encode_uint32(1, static_cast(0xAABBCCDD + i)); // address (lower 32) + writer.encode_uint32(2, static_cast(-70 + i)); // rssi + writer.encode_uint32(3, 0); // address_type (public) + writer.encode_bytes(4, FAKE_BLE_DATA, sizeof(FAKE_BLE_DATA)); // data + } +} + +static size_t calc_ble_batch_size() { + uint32_t size = 0; + for (int i = 0; i < 8; i++) { + size += ProtoSize::calc_uint32(1, static_cast(0xAABBCCDD + i)); + size += ProtoSize::calc_uint32(1, static_cast(-70 + i)); + size += ProtoSize::calc_uint32(1, 0); + size += ProtoSize::calc_length(1, sizeof(FAKE_BLE_DATA)); + } + return size; +} + +static void bench_ble_batch_new(std::vector &buf, size_t size) { + buf.resize(size); + NewProtoWriteBuffer writer(&buf, 0); + for (int i = 0; i < 8; i++) { + writer.encode_uint32(1, static_cast(0xAABBCCDD + i)); + writer.encode_uint32(2, static_cast(-70 + i)); + writer.encode_uint32(3, 0); + writer.encode_bytes(4, FAKE_BLE_DATA, sizeof(FAKE_BLE_DATA)); + } +} + +// ============================================================================ +// Benchmark: Simulate SubscribeLogsResponse encoding +// This is a frequent message: level (enum/uint32) + message (bytes) +// Message sizes vary from short to long log lines. +// ============================================================================ + +static const char LOG_SHORT[] = "[sensor:042]: 'Temperature': Sending state 23.50 °C"; +static const char LOG_LONG[] = "[wifi:042]: Connecting to 'MyNetwork'... [wifi:042]: Connected! " + "IP=192.168.1.100, SSID=MyNetwork, BSSID=AA:BB:CC:DD:EE:FF, Channel=6, RSSI=-42 dB"; + +static void bench_log_msg_old(std::vector &buf) { + buf.clear(); + OldProtoWriteBuffer writer(&buf); + writer.encode_uint32(1, 3); // level = DEBUG + writer.encode_bytes(3, reinterpret_cast(LOG_SHORT), strlen(LOG_SHORT)); +} + +static size_t calc_log_msg_size() { + uint32_t size = 0; + size += ProtoSize::calc_uint32(1, 3); + size += ProtoSize::calc_length(1, strlen(LOG_SHORT)); + return size; +} + +static void bench_log_msg_new(std::vector &buf, size_t size) { + buf.resize(size); + NewProtoWriteBuffer writer(&buf, 0); + writer.encode_uint32(1, 3); + writer.encode_bytes(3, reinterpret_cast(LOG_SHORT), strlen(LOG_SHORT)); +} + +static void bench_log_long_old(std::vector &buf) { + buf.clear(); + OldProtoWriteBuffer writer(&buf); + writer.encode_uint32(1, 3); + writer.encode_bytes(3, reinterpret_cast(LOG_LONG), strlen(LOG_LONG)); +} + +static size_t calc_log_long_size() { + uint32_t size = 0; + size += ProtoSize::calc_uint32(1, 3); + size += ProtoSize::calc_length(1, strlen(LOG_LONG)); + return size; +} + +static void bench_log_long_new(std::vector &buf, size_t size) { + buf.resize(size); + NewProtoWriteBuffer writer(&buf, 0); + writer.encode_uint32(1, 3); + writer.encode_bytes(3, reinterpret_cast(LOG_LONG), strlen(LOG_LONG)); +} + +// ============================================================================ +// Benchmark: Full encode cycle including calculate_size + resize + encode +// This measures the realistic overhead of the pre-sizing approach. +// ============================================================================ + +static void bench_full_cycle_sensor_old(std::vector &buf) { + // Old approach: just encode directly (vector grows as needed) + buf.clear(); + buf.reserve(32); // Typical small reserve + OldProtoWriteBuffer writer(&buf); + writer.encode_fixed32(1, 0x12345678); + writer.encode_float(2, 23.5f); + writer.encode_bool(3, false); +} + +static void bench_full_cycle_sensor_new(std::vector &buf) { + // New approach: calculate size, resize, then encode + uint32_t size = 0; + size += ProtoSize::calc_fixed32(1, 0x12345678); + size += ProtoSize::calc_float(1, 23.5f); + size += ProtoSize::calc_bool(1, false); + + buf.clear(); + buf.resize(size); + NewProtoWriteBuffer writer(&buf, 0); + writer.encode_fixed32(1, 0x12345678); + writer.encode_float(2, 23.5f); + writer.encode_bool(3, false); +} + +// ============================================================================ +// Correctness verification +// ============================================================================ + +static bool verify_encoding_match() { + std::vector old_buf, new_buf; + bool all_pass = true; + + auto check = [&](const char *name) { + if (old_buf.size() != new_buf.size() || memcmp(old_buf.data(), new_buf.data(), old_buf.size()) != 0) { + printf("FAIL: %s - output mismatch (old=%zu bytes, new=%zu bytes)\n", name, old_buf.size(), new_buf.size()); + all_pass = false; + } + }; + + // Varint + bench_varint_old(old_buf); + bench_varint_new(new_buf, old_buf.size()); + check("varint"); + + // Strings + bench_strings_old(old_buf); + bench_strings_new(new_buf, calc_strings_size()); + check("strings"); + + // Fixed32 + bench_fixed32_old(old_buf); + bench_fixed32_new(new_buf, calc_fixed32_size()); + check("fixed32"); + + // SensorStateResponse + bench_sensor_state_old(old_buf); + bench_sensor_state_new(new_buf, calc_sensor_state_size()); + check("sensor_state"); + + // ListEntitiesSensorResponse + bench_list_entities_old(old_buf); + bench_list_entities_new(new_buf, calc_list_entities_size()); + check("list_entities"); + + // BLE batch + bench_ble_batch_old(old_buf); + bench_ble_batch_new(new_buf, calc_ble_batch_size()); + check("ble_batch"); + + // Log message + bench_log_msg_old(old_buf); + bench_log_msg_new(new_buf, calc_log_msg_size()); + check("log_short"); + + // Long log message + bench_log_long_old(old_buf); + bench_log_long_new(new_buf, calc_log_long_size()); + check("log_long"); + + return all_pass; +} + +// ============================================================================ +// Main +// ============================================================================ + +int main() { + printf("=== ProtoWriteBuffer Encoding Benchmark ===\n"); + printf("Comparing push_back() vs pre-sized pointer writes\n\n"); + + // Verify correctness first + printf("--- Correctness Verification ---\n"); + if (!verify_encoding_match()) { + printf("CORRECTNESS CHECK FAILED - encoding output differs!\n"); + return 1; + } + printf("All encoding outputs match between old and new implementations.\n\n"); + + // Calculate sizes for pre-allocation + size_t varint_size = 1 + 2 + 3 + 4 + 5; // 15 bytes + size_t strings_size = calc_strings_size(); + size_t fixed32_size = calc_fixed32_size(); + size_t sensor_state_size = calc_sensor_state_size(); + size_t list_entities_size = calc_list_entities_size(); + size_t ble_batch_size = calc_ble_batch_size(); + size_t log_msg_size = calc_log_msg_size(); + size_t log_long_size = calc_log_long_size(); + + std::vector buf; + buf.reserve(1024); // Pre-allocate to avoid measuring allocation + + std::vector results; + + // --- Varint encoding --- + printf("--- Running Benchmarks ---\n\n"); + + results.push_back(benchmark("varint_mix (old/push_back)", varint_size, [&] { bench_varint_old(buf); })); + results.push_back(benchmark("varint_mix (new/pointer)", varint_size, [&] { bench_varint_new(buf, varint_size); })); + + // --- String encoding --- + results.push_back(benchmark("strings_mix (old/push_back)", strings_size, [&] { bench_strings_old(buf); })); + results.push_back( + benchmark("strings_mix (new/pointer)", strings_size, [&] { bench_strings_new(buf, strings_size); })); + + // --- Fixed32 encoding --- + results.push_back(benchmark("fixed32_x10 (old/push_back)", fixed32_size, [&] { bench_fixed32_old(buf); })); + results.push_back( + benchmark("fixed32_x10 (new/pointer)", fixed32_size, [&] { bench_fixed32_new(buf, fixed32_size); })); + + // --- SensorStateResponse --- + results.push_back(benchmark("sensor_state (old/push_back)", sensor_state_size, [&] { bench_sensor_state_old(buf); })); + results.push_back(benchmark("sensor_state (new/pointer)", sensor_state_size, + [&] { bench_sensor_state_new(buf, sensor_state_size); })); + + // --- ListEntitiesSensorResponse --- + results.push_back( + benchmark("list_entities (old/push_back)", list_entities_size, [&] { bench_list_entities_old(buf); })); + results.push_back(benchmark("list_entities (new/pointer)", list_entities_size, + [&] { bench_list_entities_new(buf, list_entities_size); })); + + // --- BLE batch --- + results.push_back(benchmark("ble_batch_x8 (old/push_back)", ble_batch_size, [&] { bench_ble_batch_old(buf); })); + results.push_back( + benchmark("ble_batch_x8 (new/pointer)", ble_batch_size, [&] { bench_ble_batch_new(buf, ble_batch_size); })); + + // --- Log messages --- + results.push_back(benchmark("log_short (old/push_back)", log_msg_size, [&] { bench_log_msg_old(buf); })); + results.push_back(benchmark("log_short (new/pointer)", log_msg_size, [&] { bench_log_msg_new(buf, log_msg_size); })); + + results.push_back(benchmark("log_long (old/push_back)", log_long_size, [&] { bench_log_long_old(buf); })); + results.push_back( + benchmark("log_long (new/pointer)", log_long_size, [&] { bench_log_long_new(buf, log_long_size); })); + + // --- Full encode cycle (calculate_size + resize + encode) --- + results.push_back( + benchmark("full_cycle_sensor (old/push_back)", sensor_state_size, [&] { bench_full_cycle_sensor_old(buf); })); + results.push_back( + benchmark("full_cycle_sensor (new/pointer)", sensor_state_size, [&] { bench_full_cycle_sensor_new(buf); })); + + // Print all results + printf("\n--- Results ---\n\n"); + print_results(results); + + // Print comparison summary + printf("\n--- Speedup Summary (new vs old) ---\n\n"); + for (size_t i = 0; i + 1 < results.size(); i += 2) { + print_comparison(results[i].name, results[i], results[i + 1]); + } + + printf("\n--- Encoded Sizes ---\n\n"); + printf(" varint_mix: %3zu bytes\n", varint_size); + printf(" strings_mix: %3zu bytes\n", strings_size); + printf(" fixed32_x10: %3zu bytes\n", fixed32_size); + printf(" sensor_state: %3zu bytes\n", sensor_state_size); + printf(" list_entities: %3zu bytes\n", list_entities_size); + printf(" ble_batch_x8: %3zu bytes\n", ble_batch_size); + printf(" log_short: %3zu bytes\n", log_msg_size); + printf(" log_long: %3zu bytes\n", log_long_size); + + return 0; +}