Skip to content

Commit 83c8e57

Browse files
committed
perf(otel): eliminate heap allocations in metrics hash and flatten hotpaths
- Introduce `StackBuf` in `compute_series_hash` to format non-string attribute values (int/float/bool) directly to a 32-byte stack array instead of calling `.to_string()`. This removes transient heap allocations in the ingestion path. The 32-byte limit is derived from the max string representation of OTel i64/f64 values. - Fix `Map::with_capacity` in `flatten_number_data_points` to accurately account for exemplar fields (+4 per exemplar).
1 parent cc0dd62 commit 83c8e57

1 file changed

Lines changed: 60 additions & 15 deletions

File tree

src/otel/metrics.rs

Lines changed: 60 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ use opentelemetry_proto::tonic::metrics::v1::{
2525
use serde_json::{Map, Value};
2626

2727
use rustc_hash::FxHasher;
28-
use std::borrow::Cow;
2928
use std::collections::HashSet;
3029
use std::hash::Hasher;
3130
use tracing::info_span;
@@ -87,6 +86,37 @@ pub const OTEL_METRICS_KNOWN_FIELD_LIST: [&str; 37] = [
8786
static OTEL_METRICS_KNOWN_FIELDS: Lazy<HashSet<&'static str>> =
8887
Lazy::new(|| OTEL_METRICS_KNOWN_FIELD_LIST.iter().copied().collect());
8988

89+
/// To avoid heap allocations for non-string attributes (int/float/bool)
90+
/// values are formatted via a zero-alloc [`StackBuf`] backed by a 32-byte
91+
/// stack array. This limit is safely bounded by the maximum string length
92+
/// of OTel `i64` (20 bytes) and `f64` (24 bytes) values
93+
struct StackBuf {
94+
data: [u8; 32],
95+
len: usize,
96+
}
97+
98+
impl StackBuf {
99+
fn new() -> Self {
100+
Self {
101+
data: [0u8; 32],
102+
len: 0,
103+
}
104+
}
105+
fn as_bytes(&self) -> &[u8] {
106+
&self.data[..self.len]
107+
}
108+
}
109+
110+
impl std::fmt::Write for StackBuf {
111+
fn write_str(&mut self, s: &str) -> std::fmt::Result {
112+
let bytes = s.as_bytes();
113+
let end = std::cmp::min(self.len + bytes.len(), 32);
114+
self.data[self.len..end].copy_from_slice(&bytes[..end - self.len]);
115+
self.len = end;
116+
Ok(())
117+
}
118+
}
119+
90120
/// Compute a stable u64 identifier for the physical series a sample
91121
/// belongs to. Hashes `metric_name` plus every attribute key/value pair
92122
/// that survived OTel flattening — everything in the flattened data
@@ -97,18 +127,13 @@ static OTEL_METRICS_KNOWN_FIELDS: Lazy<HashSet<&'static str>> =
97127
/// non-cryptographic) and feeds keys in sorted order so the hash
98128
/// doesn't depend on JSON Map iteration order.
99129
fn compute_series_hash(dp: &Map<String, Value>) -> u64 {
100-
let mut label_pairs: Vec<(&str, Cow<'_, str>)> = Vec::with_capacity(dp.len());
101-
for (key, value) in dp {
102-
if OTEL_METRICS_KNOWN_FIELDS.contains(key.as_str()) {
103-
continue;
130+
let mut keys: Vec<&str> = Vec::with_capacity(dp.len());
131+
for key in dp.keys() {
132+
if !OTEL_METRICS_KNOWN_FIELDS.contains(key.as_str()) {
133+
keys.push(key);
104134
}
105-
let value = match value {
106-
Value::String(s) => Cow::Borrowed(s.as_str()),
107-
other => Cow::Owned(other.to_string()),
108-
};
109-
label_pairs.push((key.as_str(), value));
110135
}
111-
label_pairs.sort_by(|a, b| a.0.cmp(b.0));
136+
keys.sort_unstable();
112137

113138
let mut hasher = FxHasher::default();
114139
// Include metric_name in the identity. Without it, two different
@@ -117,10 +142,28 @@ fn compute_series_hash(dp: &Map<String, Value>) -> u64 {
117142
hasher.write(name.as_bytes());
118143
hasher.write(b"\0");
119144
}
120-
for (k, v) in &label_pairs {
121-
hasher.write(k.as_bytes());
145+
for key in keys {
146+
hasher.write(key.as_bytes());
122147
hasher.write(b"=");
123-
hasher.write(v.as_bytes());
148+
if let Some(val) = dp.get(key) {
149+
match val {
150+
Value::String(s) => hasher.write(s.as_bytes()),
151+
Value::Bool(b) => {
152+
if *b {
153+
hasher.write(b"true");
154+
} else {
155+
hasher.write(b"false");
156+
}
157+
}
158+
Value::Number(n) => {
159+
// Zero- Alloc number formatting
160+
let mut buf = StackBuf::new();
161+
let _ = std::fmt::Write::write_fmt(&mut buf, format_args!("{}", n));
162+
hasher.write(buf.as_bytes());
163+
}
164+
_ => hasher.write(b"null"),
165+
}
166+
}
124167
hasher.write(b"\0");
125168
}
126169
hasher.finish()
@@ -172,7 +215,9 @@ fn flatten_number_data_points(data_points: &[NumberDataPoint]) -> Vec<Map<String
172215
data_points
173216
.iter()
174217
.map(|data_point| {
175-
let mut data_point_json = Map::with_capacity(data_point.attributes.len() + 8);
218+
let mut data_point_json = Map::with_capacity(
219+
data_point.attributes.len() + 8 + (data_point.exemplars.len() * 4),
220+
);
176221
insert_attributes(&mut data_point_json, &data_point.attributes);
177222
data_point_json.insert(
178223
"start_time_unix_nano".to_string(),

0 commit comments

Comments
 (0)