From 0918cc5bb4ad95e05910288b77051893c74effb3 Mon Sep 17 00:00:00 2001 From: Jason Koch Date: Thu, 26 Mar 2026 19:26:03 +0000 Subject: [PATCH 1/4] perf: Use std::to_chars and thread-local buffers for faster stateless meter send Replace absl::StrFormat + trailing-zero erasure with std::to_chars, and, use thread_local strings to eliminate per-send allocations after warmup. --- spectator/stateless_meters.h | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/spectator/stateless_meters.h b/spectator/stateless_meters.h index df12eed..cd38c13 100644 --- a/spectator/stateless_meters.h +++ b/spectator/stateless_meters.h @@ -77,19 +77,28 @@ class StatelessMeter { if (value_prefix_.empty()) { value_prefix_ = detail::create_prefix(*id_, Type()); } - auto msg = absl::StrFormat("%s%f", value_prefix_, value); - // remove trailing zeros and decimal points - msg.erase(msg.find_last_not_of('0') + 1, std::string::npos); - msg.erase(msg.find_last_not_of('.') + 1, std::string::npos); - publisher_->send(msg); + // std::to_chars with fixed format: no trailing zeros, no scientific notation, + // ~5-10x faster than absl::StrFormat("%s%f",...) + erase. + char num_buf[327]; // fixed-format double worst case: DBL_MAX ~309 digits + auto [ptr, ec] = std::to_chars(num_buf, num_buf + sizeof(num_buf), value, + std::chars_format::fixed); + // thread_local retains capacity after warmup — zero allocation per send. + thread_local std::string tl_msg; + tl_msg.assign(value_prefix_); + tl_msg.append(num_buf, ptr); + publisher_->send(tl_msg); } void send_uint(uint64_t value) { if (value_prefix_.empty()) { value_prefix_ = detail::create_prefix(*id_, Type()); } - auto msg = absl::StrFormat("%s%u", value_prefix_, value); - publisher_->send(msg); + char num_buf[24]; + auto [ptr, ec] = std::to_chars(num_buf, num_buf + sizeof(num_buf), value); + thread_local std::string tl_msg; + tl_msg.assign(value_prefix_); + tl_msg.append(num_buf, ptr); + publisher_->send(tl_msg); } private: From 3f48e0991a420cd04a2c8384ef3cc2ad6a3a864b Mon Sep 17 00:00:00 2001 From: Jason Koch Date: Thu, 26 Mar 2026 21:34:38 +0000 Subject: [PATCH 2/4] fix: allow handling of super-large length floating points --- spectator/stateless_meters.h | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/spectator/stateless_meters.h b/spectator/stateless_meters.h index cd38c13..a7e7e39 100644 --- a/spectator/stateless_meters.h +++ b/spectator/stateless_meters.h @@ -79,13 +79,26 @@ class StatelessMeter { } // std::to_chars with fixed format: no trailing zeros, no scientific notation, // ~5-10x faster than absl::StrFormat("%s%f",...) + erase. - char num_buf[327]; // fixed-format double worst case: DBL_MAX ~309 digits + // Stack buffer covers typical values; heap fallback for extreme cases (subnormals). + char num_buf[64]; auto [ptr, ec] = std::to_chars(num_buf, num_buf + sizeof(num_buf), value, std::chars_format::fixed); // thread_local retains capacity after warmup — zero allocation per send. thread_local std::string tl_msg; tl_msg.assign(value_prefix_); - tl_msg.append(num_buf, ptr); + if (ec == std::errc{}) { + tl_msg.append(num_buf, ptr); + } else { + // Fallback for extreme values (subnormals): write into tl_msg via resize. + // We do not take this pathway normally as it will issue a write of \0's into + // the 1076 chars every time + auto off = tl_msg.size(); + tl_msg.resize(off + 1076); + auto [hp, hec] = std::to_chars(tl_msg.data() + off, + tl_msg.data() + tl_msg.size(), value, + std::chars_format::fixed); + tl_msg.resize(static_cast(hp - tl_msg.data())); + } publisher_->send(tl_msg); } From 9bf2f84929f81de65a80c17a7c7365217b65d9ba Mon Sep 17 00:00:00 2001 From: Jason Koch Date: Thu, 26 Mar 2026 22:42:03 +0000 Subject: [PATCH 3/4] fix: include headers to allow build inside envoy project --- BUILD.bazel | 2 ++ spectator/stateless_meters.h | 2 ++ 2 files changed, 4 insertions(+) diff --git a/BUILD.bazel b/BUILD.bazel index 5064ec6..94dde75 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -23,6 +23,8 @@ cc_library( deps = [ "@abseil-cpp//absl/container:flat_hash_map", "@abseil-cpp//absl/strings", + "@abseil-cpp//absl/strings:str_format", + "@abseil-cpp//absl/time", "@asio", "@fmt//:fmt", "@spdlog//:spdlog", diff --git a/spectator/stateless_meters.h b/spectator/stateless_meters.h index a7e7e39..a9d8c5e 100644 --- a/spectator/stateless_meters.h +++ b/spectator/stateless_meters.h @@ -1,5 +1,7 @@ #pragma once +#include #include "id.h" +#include "absl/strings/str_cat.h" #include "absl/strings/str_format.h" #include "absl/time/time.h" From 17c2befdcad7ffd95575ba2b26d24bc65dadc1a7 Mon Sep 17 00:00:00 2001 From: Jason Koch Date: Fri, 3 Apr 2026 00:46:53 +0000 Subject: [PATCH 4/4] move thread_local to shared, handle double denormals --- spectator/gauge_test.cc | 23 ++++++++++++ spectator/stateless_meters.h | 69 +++++++++++++++++++++++++----------- 2 files changed, 71 insertions(+), 21 deletions(-) diff --git a/spectator/gauge_test.cc b/spectator/gauge_test.cc index d92e11a..2822ea8 100644 --- a/spectator/gauge_test.cc +++ b/spectator/gauge_test.cc @@ -1,6 +1,8 @@ #include "stateless_meters.h" #include "test_publisher.h" #include +#include +#include namespace { @@ -24,6 +26,27 @@ TEST(Gauge, Set) { EXPECT_EQ(publisher.SentMessages(), expected); } +TEST(Gauge, NaN) { + TestPublisher publisher; + auto id = std::make_shared("gauge", Tags{}); + Gauge g{id, &publisher}; + g.Set(std::numeric_limits::quiet_NaN()); + // Legacy absl::StrFormat("%f") produced "nan"; verify we preserve that. + std::vector expected = {"g:gauge:nan"}; + EXPECT_EQ(publisher.SentMessages(), expected); +} + +TEST(Gauge, Infinity) { + TestPublisher publisher; + auto id = std::make_shared("gauge", Tags{}); + Gauge g{id, &publisher}; + g.Set(std::numeric_limits::infinity()); + g.Set(-std::numeric_limits::infinity()); + // Legacy absl::StrFormat("%f") produced "inf" / "-inf". + std::vector expected = {"g:gauge:inf", "g:gauge:-inf"}; + EXPECT_EQ(publisher.SentMessages(), expected); +} + TEST(Gauge, InvalidTags) { TestPublisher publisher; // test with a single tag, because tags order is not guaranteed in a flat_hash_map diff --git a/spectator/stateless_meters.h b/spectator/stateless_meters.h index a9d8c5e..f214400 100644 --- a/spectator/stateless_meters.h +++ b/spectator/stateless_meters.h @@ -1,8 +1,9 @@ #pragma once +#include #include +#include #include "id.h" #include "absl/strings/str_cat.h" -#include "absl/strings/str_format.h" #include "absl/time/time.h" namespace spectator { @@ -11,6 +12,10 @@ namespace detail { #include "valid_chars.inc" +// IEEE 754 double in fixed notation requires at most 1076 chars +// (sign + 1074 fractional digits + decimal point for minimum subnormal). +static constexpr size_t kMaxFixedDoubleLen = 1076; + inline std::string as_string(std::string_view v) { return {v.data(), v.size()}; } @@ -45,6 +50,14 @@ inline std::string create_prefix(const Id& id, std::string_view type_name) { return res; } +// Single thread-local send buffer shared across all StatelessMeter instantiations. +// Non-template so all Pub types resolve to the same storage slot per thread. +// Not re-entrant: callers must complete send() before the buffer is safe to reuse. +inline std::string& tl_send_buf() { + thread_local std::string buf; + return buf; +} + template T restrict(T amount, T min, T max) { auto r = amount; @@ -66,9 +79,7 @@ class StatelessMeter { } virtual ~StatelessMeter() = default; std::string GetPrefix() { - if (value_prefix_.empty()) { - value_prefix_ = detail::create_prefix(*id_, Type()); - } + ensure_prefix(); return value_prefix_; } [[nodiscard]] IdPtr MeterId() const noexcept { return id_; } @@ -76,41 +87,51 @@ class StatelessMeter { protected: void send(double value) { - if (value_prefix_.empty()) { - value_prefix_ = detail::create_prefix(*id_, Type()); + ensure_prefix(); + auto& tl_msg = detail::tl_send_buf(); + tl_msg.assign(value_prefix_); + + // Early exit: match absl::StrFormat("%f") behaviour for special values. + if (std::isnan(value)) { + tl_msg.append("nan"); + publisher_->send(tl_msg); + return; + } + + if (std::isinf(value)) { + tl_msg.append(value > 0 ? "inf" : "-inf"); + publisher_->send(tl_msg); + return; } + // std::to_chars with fixed format: no trailing zeros, no scientific notation, // ~5-10x faster than absl::StrFormat("%s%f",...) + erase. // Stack buffer covers typical values; heap fallback for extreme cases (subnormals). char num_buf[64]; auto [ptr, ec] = std::to_chars(num_buf, num_buf + sizeof(num_buf), value, std::chars_format::fixed); - // thread_local retains capacity after warmup — zero allocation per send. - thread_local std::string tl_msg; - tl_msg.assign(value_prefix_); if (ec == std::errc{}) { tl_msg.append(num_buf, ptr); } else { - // Fallback for extreme values (subnormals): write into tl_msg via resize. - // We do not take this pathway normally as it will issue a write of \0's into - // the 1076 chars every time + // Fallback for subnormal values, which require up to 1076 chars in fixed + // notation. NaN/Inf are handled above, so this branch is subnormals only. auto off = tl_msg.size(); - tl_msg.resize(off + 1076); - auto [hp, hec] = std::to_chars(tl_msg.data() + off, - tl_msg.data() + tl_msg.size(), value, - std::chars_format::fixed); - tl_msg.resize(static_cast(hp - tl_msg.data())); + tl_msg.resize(off + detail::kMaxFixedDoubleLen); + auto [heap_ptr, heap_ec] = std::to_chars(tl_msg.data() + off, + tl_msg.data() + tl_msg.size(), value, + std::chars_format::fixed); + assert(heap_ec == std::errc{}); + tl_msg.resize(static_cast(heap_ptr - tl_msg.data())); } publisher_->send(tl_msg); } void send_uint(uint64_t value) { - if (value_prefix_.empty()) { - value_prefix_ = detail::create_prefix(*id_, Type()); - } + ensure_prefix(); char num_buf[24]; auto [ptr, ec] = std::to_chars(num_buf, num_buf + sizeof(num_buf), value); - thread_local std::string tl_msg; + assert(ec == std::errc{}); + auto& tl_msg = detail::tl_send_buf(); tl_msg.assign(value_prefix_); tl_msg.append(num_buf, ptr); publisher_->send(tl_msg); @@ -120,6 +141,12 @@ class StatelessMeter { IdPtr id_; Pub* publisher_; std::string value_prefix_; + + void ensure_prefix() { + if (value_prefix_.empty()) { + value_prefix_ = detail::create_prefix(*id_, Type()); + } + } }; template