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/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 df12eed..f214400 100644 --- a/spectator/stateless_meters.h +++ b/spectator/stateless_meters.h @@ -1,6 +1,9 @@ #pragma once +#include +#include +#include #include "id.h" -#include "absl/strings/str_format.h" +#include "absl/strings/str_cat.h" #include "absl/time/time.h" namespace spectator { @@ -9,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()}; } @@ -43,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; @@ -64,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_; } @@ -74,28 +87,66 @@ 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; } - 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. + // 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); + if (ec == std::errc{}) { + tl_msg.append(num_buf, ptr); + } else { + // 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 + 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()); - } - auto msg = absl::StrFormat("%s%u", value_prefix_, value); - publisher_->send(msg); + ensure_prefix(); + char num_buf[24]; + auto [ptr, ec] = std::to_chars(num_buf, num_buf + sizeof(num_buf), value); + 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); } private: IdPtr id_; Pub* publisher_; std::string value_prefix_; + + void ensure_prefix() { + if (value_prefix_.empty()) { + value_prefix_ = detail::create_prefix(*id_, Type()); + } + } }; template