diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 61d5a66db36..4cd61f0d5ea 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -778,9 +778,10 @@ message SubscribeLogsResponse { option (source) = SOURCE_SERVER; option (log) = false; option (no_delay) = false; + option (speed_optimized) = true; - LogLevel level = 1; - bytes message = 3; + LogLevel level = 1 [(force) = true]; + bytes message = 3 [(force) = true]; } // ==================== NOISE ENCRYPTION ==================== diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 829dd56d0a9..704c156225d 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -916,15 +916,19 @@ bool SubscribeLogsRequest::decode_varint(uint32_t field_id, proto_varint_value_t } return true; } +__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes) uint8_t *SubscribeLogsResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { uint8_t *__restrict__ pos = buffer.get_pos(); - ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, static_cast(this->level)); - ProtoEncode::encode_bytes(pos PROTO_ENCODE_DEBUG_ARG, 3, this->message_ptr_, this->message_len_); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, static_cast(this->level), true); + ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, 26); + ProtoEncode::encode_varint_raw(pos PROTO_ENCODE_DEBUG_ARG, this->message_len_); + ProtoEncode::encode_raw(pos PROTO_ENCODE_DEBUG_ARG, this->message_ptr_, this->message_len_); return pos; } +__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes) uint32_t SubscribeLogsResponse::calculate_size() const { uint32_t size = 0; - size += this->level ? 2 : 0; + size += 2; size += ProtoSize::calc_length(1, this->message_len_); return size; } diff --git a/requirements.txt b/requirements.txt index 1466eccc9b7..fa642b579ba 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.19 esptool==5.2.0 click==8.3.2 esphome-dashboard==20260408.1 -aioesphomeapi==44.13.3 +aioesphomeapi==44.14.0 zeroconf==0.148.0 puremagic==1.30 ruamel.yaml==0.19.1 # dashboard_import diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index a58474f9fe9..1743d2bb340 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -2683,7 +2683,11 @@ def build_message_type( # When set, __attribute__((optimize("O2"))) is added to the definitions # so GCC inlines the small ProtoEncode helpers even under -Os. is_speed_optimized = get_opt(desc, pb.speed_optimized, False) - speed_attr = '__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes)\n' if is_speed_optimized else "" + speed_attr = ( + '__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes)\n' + if is_speed_optimized + else "" + ) # Only generate encode method if this message needs encoding and has fields if needs_encode and encode and not is_inline_only: diff --git a/tests/benchmarks/components/api/bench_log_response.cpp b/tests/benchmarks/components/api/bench_log_response.cpp new file mode 100644 index 00000000000..4ef57987beb --- /dev/null +++ b/tests/benchmarks/components/api/bench_log_response.cpp @@ -0,0 +1,118 @@ +#include + +#include "esphome/components/api/api_pb2.h" +#include "esphome/components/api/api_buffer.h" + +namespace esphome::api::benchmarks { + +// Inner iteration count to amortize CodSpeed instrumentation overhead. +static constexpr int kInnerIterations = 2000; + +// Typical log line: "[12:34:56][D][sensor:094]: 'Temperature': Sending state 23.50000 with 1 decimals of accuracy" +static constexpr const char *kTypicalLogLine = + "[12:34:56][D][sensor:094]: 'Temperature': Sending state 23.50000 with 1 decimals of accuracy"; + +// Short log line: "[12:34:56][I][app:029]: Running..." +static constexpr const char *kShortLogLine = "[12:34:56][I][app:029]: Running..."; + +// --- Encode --- + +static void Encode_LogResponse_Typical(benchmark::State &state) { + APIBuffer buffer; + SubscribeLogsResponse msg; + msg.level = enums::LOG_LEVEL_DEBUG; + msg.set_message(reinterpret_cast(kTypicalLogLine), strlen(kTypicalLogLine)); + uint32_t size = msg.calculate_size(); + buffer.resize(size); + + for (auto _ : state) { + 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_LogResponse_Typical); + +static void Encode_LogResponse_Short(benchmark::State &state) { + APIBuffer buffer; + SubscribeLogsResponse msg; + msg.level = enums::LOG_LEVEL_INFO; + msg.set_message(reinterpret_cast(kShortLogLine), strlen(kShortLogLine)); + uint32_t size = msg.calculate_size(); + buffer.resize(size); + + for (auto _ : state) { + 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_LogResponse_Short); + +// --- Calculate Size --- + +static void CalculateSize_LogResponse_Typical(benchmark::State &state) { + SubscribeLogsResponse msg; + msg.level = enums::LOG_LEVEL_DEBUG; + msg.set_message(reinterpret_cast(kTypicalLogLine), strlen(kTypicalLogLine)); + + for (auto _ : state) { + 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_LogResponse_Typical); + +// --- Calc + Encode (steady state) --- + +static void CalcAndEncode_LogResponse_Typical(benchmark::State &state) { + APIBuffer buffer; + SubscribeLogsResponse msg; + msg.level = enums::LOG_LEVEL_DEBUG; + msg.set_message(reinterpret_cast(kTypicalLogLine), strlen(kTypicalLogLine)); + + for (auto _ : state) { + 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_LogResponse_Typical); + +// --- Calc + Encode (fresh allocation each time) --- + +static void CalcAndEncode_LogResponse_Typical_Fresh(benchmark::State &state) { + SubscribeLogsResponse msg; + msg.level = enums::LOG_LEVEL_DEBUG; + msg.set_message(reinterpret_cast(kTypicalLogLine), strlen(kTypicalLogLine)); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + APIBuffer buffer; + uint32_t size = msg.calculate_size(); + buffer.resize(size); + ProtoWriteBuffer writer(&buffer, 0); + msg.encode(writer); + benchmark::DoNotOptimize(buffer.data()); + } + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(CalcAndEncode_LogResponse_Typical_Fresh); + +} // namespace esphome::api::benchmarks