Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
23 changes: 23 additions & 0 deletions spectator/gauge_test.cc
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
#include "stateless_meters.h"
#include "test_publisher.h"
#include <gtest/gtest.h>
#include <cmath>
#include <limits>

namespace {

Expand All @@ -24,6 +26,27 @@ TEST(Gauge, Set) {
EXPECT_EQ(publisher.SentMessages(), expected);
}

TEST(Gauge, NaN) {
TestPublisher publisher;
auto id = std::make_shared<Id>("gauge", Tags{});
Gauge g{id, &publisher};
g.Set(std::numeric_limits<double>::quiet_NaN());
// Legacy absl::StrFormat("%f") produced "nan"; verify we preserve that.
std::vector<std::string> expected = {"g:gauge:nan"};
EXPECT_EQ(publisher.SentMessages(), expected);
}

TEST(Gauge, Infinity) {
TestPublisher publisher;
auto id = std::make_shared<Id>("gauge", Tags{});
Gauge g{id, &publisher};
g.Set(std::numeric_limits<double>::infinity());
g.Set(-std::numeric_limits<double>::infinity());
// Legacy absl::StrFormat("%f") produced "inf" / "-inf".
std::vector<std::string> 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
Expand Down
83 changes: 67 additions & 16 deletions spectator/stateless_meters.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
#pragma once
#include <cassert>
#include <charconv>
#include <cmath>
#include "id.h"
#include "absl/strings/str_format.h"
#include "absl/strings/str_cat.h"
#include "absl/time/time.h"

namespace spectator {
Expand All @@ -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()};
}
Expand Down Expand Up @@ -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 <typename T>
T restrict(T amount, T min, T max) {
auto r = amount;
Expand All @@ -64,38 +79,74 @@ 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_; }
[[nodiscard]] virtual std::string_view Type() = 0;

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<size_t>(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 <typename Pub>
Expand Down
Loading