Skip to content

Commit 25fa765

Browse files
committed
Cache metric instances in *Vec::with_label_values
Add FNV-hash-keyed caches to IntCounterVec, IntGaugeVec, IntUpDownCounterVec, and HistogramVec, following the same strategy used by the prometheus crate internally. On cache hit, with_label_values now returns a clone of the cached metric (Arc refcount bumps only) instead of re-allocating OTel attributes on every call.
1 parent ed6cb2d commit 25fa765

1 file changed

Lines changed: 102 additions & 9 deletions

File tree

quickwit/quickwit-common/src/metrics.rs

Lines changed: 102 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,11 @@
1313
// limitations under the License.
1414

1515
use std::collections::{BTreeMap, HashMap};
16-
use std::sync::{Arc, LazyLock, OnceLock};
16+
use std::hash::{BuildHasherDefault, Hasher};
17+
use std::sync::{Arc, LazyLock, OnceLock, RwLock};
1718
use std::time::Instant;
1819

20+
use fnv::FnvHasher;
1921
use opentelemetry::metrics::Meter;
2022
use opentelemetry::{Key, KeyValue};
2123
use prometheus::{HistogramOpts, Opts, TextEncoder};
@@ -57,6 +59,68 @@ fn build_prometheus_labels(const_labels: &[(&str, &str)]) -> HashMap<String, Str
5759
.collect()
5860
}
5961

62+
/// Identity hasher for pre-hashed `u64` keys. The metric cache keys are
63+
/// already FNV-hashed, so re-hashing them inside the HashMap (default SipHash)
64+
/// would be redundant.
65+
#[derive(Default)]
66+
struct NoHashHasher(u64);
67+
68+
impl Hasher for NoHashHasher {
69+
fn finish(&self) -> u64 {
70+
self.0
71+
}
72+
73+
fn write(&mut self, _bytes: &[u8]) {
74+
unreachable!("NoHashHasher only supports write_u64");
75+
}
76+
77+
fn write_u64(&mut self, i: u64) {
78+
self.0 = i;
79+
}
80+
}
81+
82+
type BuildNoHashHasher = BuildHasherDefault<NoHashHasher>;
83+
84+
fn hash_label_values(values: &[&str]) -> u64 {
85+
let mut hasher = FnvHasher::default();
86+
for value in values {
87+
// Length prefix prevents collisions when label boundaries shift,
88+
// e.g. ["ab", "c"] vs ["a", "bc"] produce the same byte stream
89+
// without this.
90+
hasher.write_u64(value.len() as u64);
91+
hasher.write(value.as_bytes());
92+
}
93+
hasher.finish()
94+
}
95+
96+
fn new_metric_cache<T>() -> Arc<RwLock<HashMap<u64, T, BuildNoHashHasher>>> {
97+
Arc::new(RwLock::new(HashMap::with_hasher(
98+
BuildNoHashHasher::default(),
99+
)))
100+
}
101+
102+
fn get_or_insert_cached<T: Clone>(
103+
cache: &RwLock<HashMap<u64, T, BuildNoHashHasher>>,
104+
label_values: &[&str],
105+
build: impl FnOnce() -> T,
106+
) -> T {
107+
let hash = hash_label_values(label_values);
108+
{
109+
let cache = cache.read().expect("metric cache lock poisoned");
110+
if let Some(metric) = cache.get(&hash) {
111+
return metric.clone();
112+
}
113+
}
114+
let metric = build();
115+
let mut cache = cache.write().expect("metric cache lock poisoned");
116+
// Another thread may have inserted between the read lock and the write lock acquisition.
117+
if let Some(cached) = cache.get(&hash) {
118+
return cached.clone();
119+
}
120+
cache.insert(hash, metric.clone());
121+
metric
122+
}
123+
60124
struct OtelState<T> {
61125
build_instrument: Box<dyn Fn(&Meter) -> T + Send + Sync>,
62126
instrument: OnceLock<T>,
@@ -221,6 +285,7 @@ pub struct IntCounterVec<const N: usize> {
221285
prometheus: prometheus::IntCounterVec,
222286
otel: OtelMetric<opentelemetry::metrics::Counter<u64>>,
223287
label_names: Arc<[Key]>,
288+
cache: Arc<RwLock<HashMap<u64, IntCounter, BuildNoHashHasher>>>,
224289
}
225290

226291
impl<const N: usize> IntCounterVec<N> {
@@ -242,14 +307,15 @@ impl<const N: usize> IntCounterVec<N> {
242307
prometheus: prom,
243308
otel: OtelMetric::new(None, build_otel_attributes(const_labels)),
244309
label_names: build_otel_label_names(label_names),
310+
cache: new_metric_cache(),
245311
}
246312
}
247313

248314
pub fn with_label_values(&self, label_values: [&str; N]) -> IntCounter {
249-
IntCounter {
315+
get_or_insert_cached(&self.cache, &label_values, || IntCounter {
250316
prometheus: self.prometheus.with_label_values(&label_values),
251317
otel: self.otel.with_attributes(&self.label_names, label_values),
252-
}
318+
})
253319
}
254320
}
255321

@@ -275,14 +341,15 @@ pub struct IntGaugeVec<const N: usize> {
275341
prometheus: prometheus::IntGaugeVec,
276342
otel: OtelMetric<opentelemetry::metrics::Gauge<i64>>,
277343
label_names: Arc<[Key]>,
344+
cache: Arc<RwLock<HashMap<u64, IntGauge, BuildNoHashHasher>>>,
278345
}
279346

280347
impl<const N: usize> IntGaugeVec<N> {
281348
pub fn with_label_values(&self, label_values: [&str; N]) -> IntGauge {
282-
IntGauge {
349+
get_or_insert_cached(&self.cache, &label_values, || IntGauge {
283350
prometheus: self.prometheus.with_label_values(&label_values),
284351
otel: self.otel.with_attributes(&self.label_names, label_values),
285-
}
352+
})
286353
}
287354
}
288355

@@ -323,14 +390,15 @@ pub struct IntUpDownCounterVec<const N: usize> {
323390
prometheus: prometheus::IntGaugeVec,
324391
otel: OtelMetric<opentelemetry::metrics::UpDownCounter<i64>>,
325392
label_names: Arc<[Key]>,
393+
cache: Arc<RwLock<HashMap<u64, IntUpDownCounter, BuildNoHashHasher>>>,
326394
}
327395

328396
impl<const N: usize> IntUpDownCounterVec<N> {
329397
pub fn with_label_values(&self, label_values: [&str; N]) -> IntUpDownCounter {
330-
IntUpDownCounter {
398+
get_or_insert_cached(&self.cache, &label_values, || IntUpDownCounter {
331399
prometheus: self.prometheus.with_label_values(&label_values),
332400
otel: self.otel.with_attributes(&self.label_names, label_values),
333-
}
401+
})
334402
}
335403
}
336404

@@ -397,14 +465,15 @@ pub struct HistogramVec<const N: usize> {
397465
prometheus: prometheus::HistogramVec,
398466
otel: OtelMetric<opentelemetry::metrics::Histogram<f64>>,
399467
label_names: Arc<[Key]>,
468+
cache: Arc<RwLock<HashMap<u64, Histogram, BuildNoHashHasher>>>,
400469
}
401470

402471
impl<const N: usize> HistogramVec<N> {
403472
pub fn with_label_values(&self, label_values: [&str; N]) -> Histogram {
404-
Histogram {
473+
get_or_insert_cached(&self.cache, &label_values, || Histogram {
405474
prometheus: self.prometheus.with_label_values(&label_values),
406475
otel: self.otel.with_attributes(&self.label_names, label_values),
407-
}
476+
})
408477
}
409478
}
410479

@@ -550,6 +619,7 @@ pub fn new_counter_vec<const N: usize>(
550619
build_otel_attributes(const_labels),
551620
),
552621
label_names: build_otel_label_names(label_names),
622+
cache: new_metric_cache(),
553623
}
554624
}
555625

@@ -597,6 +667,7 @@ pub fn new_gauge_vec<const N: usize>(
597667
build_otel_attributes(const_labels),
598668
),
599669
label_names: build_otel_label_names(label_names),
670+
cache: new_metric_cache(),
600671
}
601672
}
602673

@@ -644,6 +715,7 @@ pub fn new_up_down_counter_vec<const N: usize>(
644715
build_otel_attributes(const_labels),
645716
),
646717
label_names: build_otel_label_names(label_names),
718+
cache: new_metric_cache(),
647719
}
648720
}
649721

@@ -721,6 +793,7 @@ pub fn new_histogram_vec<const N: usize>(
721793
build_otel_attributes(const_labels),
722794
),
723795
label_names: build_otel_label_names(label_names),
796+
cache: new_metric_cache(),
724797
}
725798
}
726799

@@ -1371,4 +1444,24 @@ mod tests {
13711444
assert!(payload.contains("quickwit_test_test_payload_ctr"));
13721445
assert!(payload.contains("42"));
13731446
}
1447+
1448+
#[test]
1449+
fn test_hash_label_values() {
1450+
// Same values produce the same hash.
1451+
assert_eq!(
1452+
hash_label_values(&["foo", "bar"]),
1453+
hash_label_values(&["foo", "bar"]),
1454+
);
1455+
// Different values produce different hashes.
1456+
assert_ne!(
1457+
hash_label_values(&["foo", "bar"]),
1458+
hash_label_values(&["foo", "baz"]),
1459+
);
1460+
// Shifted label boundaries produce different hashes
1461+
// (length prefix prevents "ab"+"c" from colliding with "a"+"bc").
1462+
assert_ne!(
1463+
hash_label_values(&["ab", "c"]),
1464+
hash_label_values(&["a", "bc"]),
1465+
);
1466+
}
13741467
}

0 commit comments

Comments
 (0)