Skip to content

Commit 17c2bef

Browse files
committed
move thread_local to shared, handle double denormals
1 parent 9bf2f84 commit 17c2bef

2 files changed

Lines changed: 71 additions & 21 deletions

File tree

spectator/gauge_test.cc

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
#include "stateless_meters.h"
22
#include "test_publisher.h"
33
#include <gtest/gtest.h>
4+
#include <cmath>
5+
#include <limits>
46

57
namespace {
68

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

29+
TEST(Gauge, NaN) {
30+
TestPublisher publisher;
31+
auto id = std::make_shared<Id>("gauge", Tags{});
32+
Gauge g{id, &publisher};
33+
g.Set(std::numeric_limits<double>::quiet_NaN());
34+
// Legacy absl::StrFormat("%f") produced "nan"; verify we preserve that.
35+
std::vector<std::string> expected = {"g:gauge:nan"};
36+
EXPECT_EQ(publisher.SentMessages(), expected);
37+
}
38+
39+
TEST(Gauge, Infinity) {
40+
TestPublisher publisher;
41+
auto id = std::make_shared<Id>("gauge", Tags{});
42+
Gauge g{id, &publisher};
43+
g.Set(std::numeric_limits<double>::infinity());
44+
g.Set(-std::numeric_limits<double>::infinity());
45+
// Legacy absl::StrFormat("%f") produced "inf" / "-inf".
46+
std::vector<std::string> expected = {"g:gauge:inf", "g:gauge:-inf"};
47+
EXPECT_EQ(publisher.SentMessages(), expected);
48+
}
49+
2750
TEST(Gauge, InvalidTags) {
2851
TestPublisher publisher;
2952
// test with a single tag, because tags order is not guaranteed in a flat_hash_map

spectator/stateless_meters.h

Lines changed: 48 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
#pragma once
2+
#include <cassert>
23
#include <charconv>
4+
#include <cmath>
35
#include "id.h"
46
#include "absl/strings/str_cat.h"
5-
#include "absl/strings/str_format.h"
67
#include "absl/time/time.h"
78

89
namespace spectator {
@@ -11,6 +12,10 @@ namespace detail {
1112

1213
#include "valid_chars.inc"
1314

15+
// IEEE 754 double in fixed notation requires at most 1076 chars
16+
// (sign + 1074 fractional digits + decimal point for minimum subnormal).
17+
static constexpr size_t kMaxFixedDoubleLen = 1076;
18+
1419
inline std::string as_string(std::string_view v) {
1520
return {v.data(), v.size()};
1621
}
@@ -45,6 +50,14 @@ inline std::string create_prefix(const Id& id, std::string_view type_name) {
4550
return res;
4651
}
4752

53+
// Single thread-local send buffer shared across all StatelessMeter instantiations.
54+
// Non-template so all Pub types resolve to the same storage slot per thread.
55+
// Not re-entrant: callers must complete send() before the buffer is safe to reuse.
56+
inline std::string& tl_send_buf() {
57+
thread_local std::string buf;
58+
return buf;
59+
}
60+
4861
template <typename T>
4962
T restrict(T amount, T min, T max) {
5063
auto r = amount;
@@ -66,51 +79,59 @@ class StatelessMeter {
6679
}
6780
virtual ~StatelessMeter() = default;
6881
std::string GetPrefix() {
69-
if (value_prefix_.empty()) {
70-
value_prefix_ = detail::create_prefix(*id_, Type());
71-
}
82+
ensure_prefix();
7283
return value_prefix_;
7384
}
7485
[[nodiscard]] IdPtr MeterId() const noexcept { return id_; }
7586
[[nodiscard]] virtual std::string_view Type() = 0;
7687

7788
protected:
7889
void send(double value) {
79-
if (value_prefix_.empty()) {
80-
value_prefix_ = detail::create_prefix(*id_, Type());
90+
ensure_prefix();
91+
auto& tl_msg = detail::tl_send_buf();
92+
tl_msg.assign(value_prefix_);
93+
94+
// Early exit: match absl::StrFormat("%f") behaviour for special values.
95+
if (std::isnan(value)) {
96+
tl_msg.append("nan");
97+
publisher_->send(tl_msg);
98+
return;
99+
}
100+
101+
if (std::isinf(value)) {
102+
tl_msg.append(value > 0 ? "inf" : "-inf");
103+
publisher_->send(tl_msg);
104+
return;
81105
}
106+
82107
// std::to_chars with fixed format: no trailing zeros, no scientific notation,
83108
// ~5-10x faster than absl::StrFormat("%s%f",...) + erase.
84109
// Stack buffer covers typical values; heap fallback for extreme cases (subnormals).
85110
char num_buf[64];
86111
auto [ptr, ec] = std::to_chars(num_buf, num_buf + sizeof(num_buf), value,
87112
std::chars_format::fixed);
88-
// thread_local retains capacity after warmup — zero allocation per send.
89-
thread_local std::string tl_msg;
90-
tl_msg.assign(value_prefix_);
91113
if (ec == std::errc{}) {
92114
tl_msg.append(num_buf, ptr);
93115
} else {
94-
// Fallback for extreme values (subnormals): write into tl_msg via resize.
95-
// We do not take this pathway normally as it will issue a write of \0's into
96-
// the 1076 chars every time
116+
// Fallback for subnormal values, which require up to 1076 chars in fixed
117+
// notation. NaN/Inf are handled above, so this branch is subnormals only.
97118
auto off = tl_msg.size();
98-
tl_msg.resize(off + 1076);
99-
auto [hp, hec] = std::to_chars(tl_msg.data() + off,
100-
tl_msg.data() + tl_msg.size(), value,
101-
std::chars_format::fixed);
102-
tl_msg.resize(static_cast<size_t>(hp - tl_msg.data()));
119+
tl_msg.resize(off + detail::kMaxFixedDoubleLen);
120+
auto [heap_ptr, heap_ec] = std::to_chars(tl_msg.data() + off,
121+
tl_msg.data() + tl_msg.size(), value,
122+
std::chars_format::fixed);
123+
assert(heap_ec == std::errc{});
124+
tl_msg.resize(static_cast<size_t>(heap_ptr - tl_msg.data()));
103125
}
104126
publisher_->send(tl_msg);
105127
}
106128

107129
void send_uint(uint64_t value) {
108-
if (value_prefix_.empty()) {
109-
value_prefix_ = detail::create_prefix(*id_, Type());
110-
}
130+
ensure_prefix();
111131
char num_buf[24];
112132
auto [ptr, ec] = std::to_chars(num_buf, num_buf + sizeof(num_buf), value);
113-
thread_local std::string tl_msg;
133+
assert(ec == std::errc{});
134+
auto& tl_msg = detail::tl_send_buf();
114135
tl_msg.assign(value_prefix_);
115136
tl_msg.append(num_buf, ptr);
116137
publisher_->send(tl_msg);
@@ -120,6 +141,12 @@ class StatelessMeter {
120141
IdPtr id_;
121142
Pub* publisher_;
122143
std::string value_prefix_;
144+
145+
void ensure_prefix() {
146+
if (value_prefix_.empty()) {
147+
value_prefix_ = detail::create_prefix(*id_, Type());
148+
}
149+
}
123150
};
124151

125152
template <typename Pub>

0 commit comments

Comments
 (0)