diff --git a/tests/benchmarks/proto_encode_benchmark.cpp b/tests/benchmarks/proto_encode_benchmark.cpp deleted file mode 100644 index ac49e139aec..00000000000 --- a/tests/benchmarks/proto_encode_benchmark.cpp +++ /dev/null @@ -1,795 +0,0 @@ -/** - * 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; -} diff --git a/tests/benchmarks/proto_message_benchmark.cpp b/tests/benchmarks/proto_message_benchmark.cpp deleted file mode 100644 index 395cc4b8060..00000000000 --- a/tests/benchmarks/proto_message_benchmark.cpp +++ /dev/null @@ -1,783 +0,0 @@ -/** - * Benchmark: Virtual dispatch vs direct calls for protobuf message encoding - * - * Compares: - * OLD: virtual dispatch for encode/calculate_size + ProtoSize accumulator object - * NEW: direct template calls for encode/calculate_size + static ProtoSize methods - * - * Build (from repo root): - * g++ -std=gnu++20 -O2 \ - * tests/benchmarks/proto_message_benchmark.cpp \ - * -o tests/benchmarks/proto_message_benchmark - * - * Run: - * ./tests/benchmarks/proto_message_benchmark - */ - -#include -#include -#include -#include -#include -#include -#include -#include - -// ============================================================================ -// Benchmark infrastructure -// ============================================================================ - -struct BenchResult { - const char *name; - double ns_per_op; - double ops_per_sec; - size_t iterations; -}; - -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, 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 ~500ms for stability - iterations = std::max(100000, static_cast(500'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}; -} - -void print_results(const std::vector &results) { - printf("%-55s %12s %15s %12s\n", "Benchmark", "ns/op", "ops/sec", "iters"); - printf("%-55s %12s %15s %12s\n", std::string(55, '-').c_str(), "--------", "--------", "--------"); - for (const auto &r : results) { - printf("%-55s %12.1f %15.0f %12zu\n", r.name, r.ns_per_op, r.ops_per_sec, r.iterations); - } -} - -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; - const char *dir = speedup > 1.0 ? "faster" : "slower"; - printf(" %-51s %5.2fx %s\n", label, speedup > 1.0 ? speedup : 1.0 / speedup, dir); -} - -// ============================================================================ -// Shared encoding helpers (same for both old and new) -// ============================================================================ - -static constexpr uint32_t varint_size(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; -} - -class WriteBuffer { - public: - WriteBuffer(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_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_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); - std::memcpy(this->pos_, &value, 4); - this->pos_ += 4; - } - - 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); - } - - // Nested message encoding (for old-style virtual dispatch) - void encode_message_virtual(uint32_t field_id, uint32_t nested_size, const void *value, - void (*encode_fn)(const void *, WriteBuffer &), bool force) { - if (nested_size == 0 && !force) - return; - this->encode_field_raw(field_id, 2); - this->encode_varint_raw(nested_size); - encode_fn(value, *this); - } - - // Nested message encoding (for new-style direct calls) - template void encode_message(uint32_t field_id, const T &value, bool force = true) { - uint32_t nested_size = value.calculate_size(); - if (nested_size == 0 && !force) - return; - this->encode_field_raw(field_id, 2); - this->encode_varint_raw(nested_size); - value.encode(*this); - } - - std::vector *buffer_; - uint8_t *pos_; -}; - -// ============================================================================ -// OLD approach: ProtoSize accumulator + virtual dispatch -// ============================================================================ - -namespace old_style { - -class ProtoSize { - public: - ProtoSize() = default; - uint32_t get_size() const { return total_size_; } - - void add_uint32(uint32_t field_id_size, uint32_t value) { - if (value != 0) - total_size_ += field_id_size + varint_size(value); - } - void add_bool(uint32_t field_id_size, bool value) { - if (value) - total_size_ += field_id_size + 1; - } - void add_float(uint32_t field_id_size, float value) { - if (value != 0.0f) - total_size_ += field_id_size + 4; - } - void add_fixed32(uint32_t field_id_size, uint32_t value) { - if (value != 0) - total_size_ += field_id_size + 4; - } - void add_length(uint32_t field_id_size, size_t len) { - if (len != 0) - total_size_ += field_id_size + varint_size(static_cast(len)) + static_cast(len); - } - void add_message_field_force(uint32_t field_id_size, uint32_t nested_size) { - total_size_ += field_id_size + varint_size(nested_size) + nested_size; - } - - private: - uint32_t total_size_ = 0; -}; - -class ProtoMessage { - public: - virtual void encode(WriteBuffer &buffer) const = 0; - virtual uint32_t calculate_size() const = 0; - virtual ~ProtoMessage() = default; -}; - -// Empty message (ping, disconnect, etc.) -class EmptyMessage : public ProtoMessage { - public: - static constexpr uint8_t MESSAGE_TYPE = 1; - void encode(WriteBuffer &buffer) const override {} - uint32_t calculate_size() const override { return 0; } -}; - -// SensorStateResponse: fixed32 key, float state, bool missing_state -class SensorStateResponse : public ProtoMessage { - public: - static constexpr uint8_t MESSAGE_TYPE = 25; - uint32_t key{0x12345678}; - float state{23.5f}; - bool missing_state{false}; - - void encode(WriteBuffer &buffer) const override { - buffer.encode_fixed32(1, this->key); - buffer.encode_float(2, this->state); - buffer.encode_bool(3, this->missing_state); - } - - uint32_t calculate_size() const override { - ProtoSize size; - size.add_fixed32(1, this->key); - size.add_float(1, this->state); - size.add_bool(1, this->missing_state); - return size.get_size(); - } -}; - -// ListEntitiesSensorResponse: multiple strings + numeric fields -class ListEntitiesSensorResponse : public ProtoMessage { - public: - static constexpr uint8_t MESSAGE_TYPE = 16; - std::string object_id{"living_room_temp"}; - uint32_t key{0xABCD1234}; - std::string name{"Living Room Temperature"}; - std::string unique_id{"esp32_01-sensor-living_room_temp"}; - std::string icon{"mdi:thermometer"}; - std::string unit_of_measurement{"\xc2\xb0" - "C"}; - uint32_t accuracy_decimals{1}; - bool force_update{false}; - std::string device_class{"temperature"}; - uint32_t state_class{1}; - - void encode(WriteBuffer &buffer) const override { - buffer.encode_string(1, this->object_id.data(), this->object_id.size()); - buffer.encode_fixed32(2, this->key); - buffer.encode_string(3, this->name.data(), this->name.size()); - buffer.encode_string(4, this->unique_id.data(), this->unique_id.size()); - buffer.encode_string(5, this->icon.data(), this->icon.size()); - buffer.encode_string(6, this->unit_of_measurement.data(), this->unit_of_measurement.size()); - buffer.encode_uint32(7, this->accuracy_decimals); - buffer.encode_bool(8, this->force_update); - buffer.encode_string(9, this->device_class.data(), this->device_class.size()); - buffer.encode_uint32(10, this->state_class); - } - - uint32_t calculate_size() const override { - ProtoSize size; - size.add_length(1, this->object_id.size()); - size.add_fixed32(1, this->key); - size.add_length(1, this->name.size()); - size.add_length(1, this->unique_id.size()); - size.add_length(1, this->icon.size()); - size.add_length(1, this->unit_of_measurement.size()); - size.add_uint32(1, this->accuracy_decimals); - size.add_bool(1, this->force_update); - size.add_length(1, this->device_class.size()); - size.add_uint32(1, this->state_class); - return size.get_size(); - } -}; - -// SubscribeLogsResponse: level + message bytes -class SubscribeLogsResponse : public ProtoMessage { - public: - static constexpr uint8_t MESSAGE_TYPE = 29; - uint32_t level{3}; - std::string message{"[sensor:042]: 'Temperature': Sending state 23.50 C with 1 decimals of accuracy"}; - - void encode(WriteBuffer &buffer) const override { - buffer.encode_uint32(1, this->level); - buffer.encode_bytes(3, reinterpret_cast(this->message.data()), this->message.size()); - } - - uint32_t calculate_size() const override { - ProtoSize size; - size.add_uint32(1, this->level); - size.add_length(1, this->message.size()); - return size.get_size(); - } -}; - -// Nested message: BluetoothGATTService with characteristics -class BluetoothGATTCharacteristic : public ProtoMessage { - public: - uint32_t uuid1{0x2A19}; - uint32_t handle{3}; - uint32_t properties{2}; - - void encode(WriteBuffer &buffer) const override { - buffer.encode_uint32(1, this->uuid1); - buffer.encode_uint32(2, this->handle); - buffer.encode_uint32(3, this->properties); - } - - uint32_t calculate_size() const override { - ProtoSize size; - size.add_uint32(1, this->uuid1); - size.add_uint32(1, this->handle); - size.add_uint32(1, this->properties); - return size.get_size(); - } -}; - -class BluetoothGATTService : public ProtoMessage { - public: - uint32_t uuid1{0x180F}; - uint32_t handle{1}; - std::vector characteristics; - - BluetoothGATTService() { characteristics.resize(4); } - - void encode(WriteBuffer &buffer) const override { - buffer.encode_uint32(1, this->uuid1); - buffer.encode_uint32(2, this->handle); - for (const auto &ch : this->characteristics) { - buffer.encode_message_virtual( - 3, ch.calculate_size(), &ch, - [](const void *msg, WriteBuffer &buf) { static_cast(msg)->encode(buf); }, - true); - } - } - - uint32_t calculate_size() const override { - ProtoSize size; - size.add_uint32(1, this->uuid1); - size.add_uint32(1, this->handle); - for (const auto &ch : this->characteristics) { - size.add_message_field_force(1, ch.calculate_size()); - } - return size.get_size(); - } -}; - -// send_message simulation: virtual dispatch through base pointer -__attribute__((noinline)) bool send_message(const ProtoMessage &msg, uint8_t msg_type, std::vector &buf) { - uint32_t size = msg.calculate_size(); - buf.resize(size); - WriteBuffer writer(&buf, 0); - msg.encode(writer); - do_not_optimize(buf); - return true; -} - -} // namespace old_style - -// ============================================================================ -// NEW approach: static ProtoSize + direct template calls -// ============================================================================ - -namespace new_style { - -class ProtoSize { - public: - static constexpr uint32_t calc_uint32(uint32_t field_id_size, uint32_t value) { - return value ? field_id_size + varint_size(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_size(static_cast(len)) + static_cast(len) : 0; - } - static constexpr uint32_t calc_message_force(uint32_t field_id_size, uint32_t nested_size) { - return field_id_size + varint_size(nested_size) + nested_size; - } -}; - -class ProtoMessage { - public: - // Non-virtual defaults — concrete types hide these - void encode(WriteBuffer &buffer) const {} - uint32_t calculate_size() const { return 0; } - ~ProtoMessage() = default; -}; - -// Empty message -class EmptyMessage : public ProtoMessage { - public: - static constexpr uint8_t MESSAGE_TYPE = 1; - static constexpr uint32_t ESTIMATED_SIZE = 0; - void encode(WriteBuffer &buffer) const {} - uint32_t calculate_size() const { return 0; } -}; - -// SensorStateResponse -class SensorStateResponse : public ProtoMessage { - public: - static constexpr uint8_t MESSAGE_TYPE = 25; - static constexpr uint32_t ESTIMATED_SIZE = 10; - uint32_t key{0x12345678}; - float state{23.5f}; - bool missing_state{false}; - - void encode(WriteBuffer &buffer) const { - buffer.encode_fixed32(1, this->key); - buffer.encode_float(2, this->state); - buffer.encode_bool(3, this->missing_state); - } - - uint32_t calculate_size() const { - uint32_t size = 0; - size += ProtoSize::calc_fixed32(1, this->key); - size += ProtoSize::calc_float(1, this->state); - size += ProtoSize::calc_bool(1, this->missing_state); - return size; - } -}; - -// ListEntitiesSensorResponse -class ListEntitiesSensorResponse : public ProtoMessage { - public: - static constexpr uint8_t MESSAGE_TYPE = 16; - static constexpr uint32_t ESTIMATED_SIZE = 128; - std::string object_id{"living_room_temp"}; - uint32_t key{0xABCD1234}; - std::string name{"Living Room Temperature"}; - std::string unique_id{"esp32_01-sensor-living_room_temp"}; - std::string icon{"mdi:thermometer"}; - std::string unit_of_measurement{"\xc2\xb0" - "C"}; - uint32_t accuracy_decimals{1}; - bool force_update{false}; - std::string device_class{"temperature"}; - uint32_t state_class{1}; - - void encode(WriteBuffer &buffer) const { - buffer.encode_string(1, this->object_id.data(), this->object_id.size()); - buffer.encode_fixed32(2, this->key); - buffer.encode_string(3, this->name.data(), this->name.size()); - buffer.encode_string(4, this->unique_id.data(), this->unique_id.size()); - buffer.encode_string(5, this->icon.data(), this->icon.size()); - buffer.encode_string(6, this->unit_of_measurement.data(), this->unit_of_measurement.size()); - buffer.encode_uint32(7, this->accuracy_decimals); - buffer.encode_bool(8, this->force_update); - buffer.encode_string(9, this->device_class.data(), this->device_class.size()); - buffer.encode_uint32(10, this->state_class); - } - - uint32_t calculate_size() const { - uint32_t size = 0; - size += ProtoSize::calc_length(1, this->object_id.size()); - size += ProtoSize::calc_fixed32(1, this->key); - size += ProtoSize::calc_length(1, this->name.size()); - size += ProtoSize::calc_length(1, this->unique_id.size()); - size += ProtoSize::calc_length(1, this->icon.size()); - size += ProtoSize::calc_length(1, this->unit_of_measurement.size()); - size += ProtoSize::calc_uint32(1, this->accuracy_decimals); - size += ProtoSize::calc_bool(1, this->force_update); - size += ProtoSize::calc_length(1, this->device_class.size()); - size += ProtoSize::calc_uint32(1, this->state_class); - return size; - } -}; - -// SubscribeLogsResponse -class SubscribeLogsResponse : public ProtoMessage { - public: - static constexpr uint8_t MESSAGE_TYPE = 29; - static constexpr uint32_t ESTIMATED_SIZE = 80; - uint32_t level{3}; - std::string message{"[sensor:042]: 'Temperature': Sending state 23.50 C with 1 decimals of accuracy"}; - - void encode(WriteBuffer &buffer) const { - buffer.encode_uint32(1, this->level); - buffer.encode_bytes(3, reinterpret_cast(this->message.data()), this->message.size()); - } - - uint32_t calculate_size() const { - uint32_t size = 0; - size += ProtoSize::calc_uint32(1, this->level); - size += ProtoSize::calc_length(1, this->message.size()); - return size; - } -}; - -// Nested: BluetoothGATTCharacteristic -class BluetoothGATTCharacteristic : public ProtoMessage { - public: - static constexpr uint32_t ESTIMATED_SIZE = 10; - uint32_t uuid1{0x2A19}; - uint32_t handle{3}; - uint32_t properties{2}; - - void encode(WriteBuffer &buffer) const { - buffer.encode_uint32(1, this->uuid1); - buffer.encode_uint32(2, this->handle); - buffer.encode_uint32(3, this->properties); - } - - uint32_t calculate_size() const { - uint32_t size = 0; - size += ProtoSize::calc_uint32(1, this->uuid1); - size += ProtoSize::calc_uint32(1, this->handle); - size += ProtoSize::calc_uint32(1, this->properties); - return size; - } -}; - -// Nested: BluetoothGATTService -class BluetoothGATTService : public ProtoMessage { - public: - static constexpr uint32_t ESTIMATED_SIZE = 64; - uint32_t uuid1{0x180F}; - uint32_t handle{1}; - std::vector characteristics; - - BluetoothGATTService() { characteristics.resize(4); } - - void encode(WriteBuffer &buffer) const { - buffer.encode_uint32(1, this->uuid1); - buffer.encode_uint32(2, this->handle); - for (const auto &ch : this->characteristics) { - buffer.encode_message(3, ch, true); - } - } - - uint32_t calculate_size() const { - uint32_t size = 0; - size += ProtoSize::calc_uint32(1, this->uuid1); - size += ProtoSize::calc_uint32(1, this->handle); - for (const auto &ch : this->characteristics) { - size += ProtoSize::calc_message_force(1, ch.calculate_size()); - } - return size; - } -}; - -// Encode thunk for non-template core -template void encode_msg(const void *msg, WriteBuffer &buf) { static_cast(msg)->encode(buf); } - -static void encode_msg_noop(const void *, WriteBuffer &) {} - -// send_message template: direct calls, no virtual dispatch -template __attribute__((noinline)) bool send_message(const T &msg, std::vector &buf) { - uint32_t size; - void (*encode_fn)(const void *, WriteBuffer &); - if constexpr (T::ESTIMATED_SIZE == 0) { - size = 0; - encode_fn = &encode_msg_noop; - } else { - size = msg.calculate_size(); - encode_fn = &encode_msg; - } - buf.resize(size); - WriteBuffer writer(&buf, 0); - encode_fn(&msg, writer); - do_not_optimize(buf); - return true; -} - -} // namespace new_style - -// ============================================================================ -// Correctness verification -// ============================================================================ - -static bool verify_correctness() { - std::vector old_buf, new_buf; - bool all_pass = true; - - auto check = [&](const char *name) { - if (old_buf.size() != new_buf.size() || - (old_buf.size() > 0 && 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; - } else { - printf(" OK: %s (%zu bytes)\n", name, old_buf.size()); - } - }; - - // Empty - { - old_style::EmptyMessage old_msg; - new_style::EmptyMessage new_msg; - old_style::send_message(old_msg, old_msg.MESSAGE_TYPE, old_buf); - new_style::send_message(new_msg, new_buf); - check("EmptyMessage"); - } - // SensorState - { - old_style::SensorStateResponse old_msg; - new_style::SensorStateResponse new_msg; - old_style::send_message(old_msg, old_msg.MESSAGE_TYPE, old_buf); - new_style::send_message(new_msg, new_buf); - check("SensorStateResponse"); - } - // ListEntities - { - old_style::ListEntitiesSensorResponse old_msg; - new_style::ListEntitiesSensorResponse new_msg; - old_style::send_message(old_msg, old_msg.MESSAGE_TYPE, old_buf); - new_style::send_message(new_msg, new_buf); - check("ListEntitiesSensorResponse"); - } - // Log - { - old_style::SubscribeLogsResponse old_msg; - new_style::SubscribeLogsResponse new_msg; - old_style::send_message(old_msg, old_msg.MESSAGE_TYPE, old_buf); - new_style::send_message(new_msg, new_buf); - check("SubscribeLogsResponse"); - } - // Nested (GATT service) - { - old_style::BluetoothGATTService old_msg; - new_style::BluetoothGATTService new_msg; - old_style::send_message(old_msg, 7, old_buf); - new_style::send_message(new_msg, new_buf); - check("BluetoothGATTService (nested)"); - } - - return all_pass; -} - -// ============================================================================ -// Benchmark: calculate_size only -// ============================================================================ - -template __attribute__((noinline)) uint32_t bench_calc_size_virtual(const T &msg) { - // Force virtual dispatch by going through base pointer - const old_style::ProtoMessage *base = &msg; - uint32_t s = base->calculate_size(); - do_not_optimize(s); - return s; -} - -template __attribute__((noinline)) uint32_t bench_calc_size_direct(const T &msg) { - uint32_t s = msg.calculate_size(); - do_not_optimize(s); - return s; -} - -// ============================================================================ -// Main -// ============================================================================ - -int main() { - printf("=== Proto Message Encoding Benchmark ===\n"); - printf("Comparing virtual dispatch + accumulator ProtoSize vs direct calls + static ProtoSize\n\n"); - - // Verify correctness - printf("--- Correctness Verification ---\n"); - if (!verify_correctness()) { - printf("\nCORRECTNESS CHECK FAILED!\n"); - return 1; - } - printf("All outputs match.\n\n"); - - std::vector buf; - buf.reserve(1024); - - std::vector results; - - // ---- calculate_size benchmarks ---- - printf("--- Running calculate_size Benchmarks ---\n\n"); - - { - old_style::SensorStateResponse old_msg; - new_style::SensorStateResponse new_msg; - results.push_back(benchmark("calc_size: SensorState (virtual)", [&] { bench_calc_size_virtual(old_msg); })); - results.push_back(benchmark("calc_size: SensorState (direct+static)", [&] { bench_calc_size_direct(new_msg); })); - } - { - old_style::ListEntitiesSensorResponse old_msg; - new_style::ListEntitiesSensorResponse new_msg; - results.push_back(benchmark("calc_size: ListEntities (virtual)", [&] { bench_calc_size_virtual(old_msg); })); - results.push_back(benchmark("calc_size: ListEntities (direct+static)", [&] { bench_calc_size_direct(new_msg); })); - } - { - old_style::SubscribeLogsResponse old_msg; - new_style::SubscribeLogsResponse new_msg; - results.push_back(benchmark("calc_size: LogResponse (virtual)", [&] { bench_calc_size_virtual(old_msg); })); - results.push_back(benchmark("calc_size: LogResponse (direct+static)", [&] { bench_calc_size_direct(new_msg); })); - } - { - old_style::BluetoothGATTService old_msg; - new_style::BluetoothGATTService new_msg; - results.push_back(benchmark("calc_size: GATTService/nested (virtual)", [&] { bench_calc_size_virtual(old_msg); })); - results.push_back( - benchmark("calc_size: GATTService/nested (direct+static)", [&] { bench_calc_size_direct(new_msg); })); - } - - // ---- Full send_message benchmarks ---- - printf("--- Running send_message Benchmarks ---\n\n"); - - { - old_style::EmptyMessage old_msg; - new_style::EmptyMessage new_msg; - results.push_back(benchmark("send: EmptyMessage (virtual)", [&] { old_style::send_message(old_msg, 1, buf); })); - results.push_back(benchmark("send: EmptyMessage (direct+static)", [&] { new_style::send_message(new_msg, buf); })); - } - { - old_style::SensorStateResponse old_msg; - new_style::SensorStateResponse new_msg; - results.push_back(benchmark("send: SensorState (virtual)", [&] { old_style::send_message(old_msg, 25, buf); })); - results.push_back(benchmark("send: SensorState (direct+static)", [&] { new_style::send_message(new_msg, buf); })); - } - { - old_style::ListEntitiesSensorResponse old_msg; - new_style::ListEntitiesSensorResponse new_msg; - results.push_back(benchmark("send: ListEntities (virtual)", [&] { old_style::send_message(old_msg, 16, buf); })); - results.push_back(benchmark("send: ListEntities (direct+static)", [&] { new_style::send_message(new_msg, buf); })); - } - { - old_style::SubscribeLogsResponse old_msg; - new_style::SubscribeLogsResponse new_msg; - results.push_back(benchmark("send: LogResponse (virtual)", [&] { old_style::send_message(old_msg, 29, buf); })); - results.push_back(benchmark("send: LogResponse (direct+static)", [&] { new_style::send_message(new_msg, buf); })); - } - { - old_style::BluetoothGATTService old_msg; - new_style::BluetoothGATTService new_msg; - results.push_back( - benchmark("send: GATTService/nested (virtual)", [&] { old_style::send_message(old_msg, 7, buf); })); - results.push_back( - benchmark("send: GATTService/nested (direct+static)", [&] { new_style::send_message(new_msg, 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]); - } - - return 0; -}