mirror of
https://github.com/rustfs/rustfs.git
synced 2026-07-26 08:18:18 +00:00
perf(obs): trim per-collection-cycle waste in report_metrics (#4748)
`report_metrics` runs on every metrics collection cycle and did three things it did not need to, for every metric, every cycle: - interned `metric.name`/`metric.help` through a `Mutex<HashMap>` even when the `Cow` was already `Borrowed(&'static str)` (the common case for statically named metrics); - re-ran `describe_*!` (which re-locks the recorder's metadata map) although the metadata never changes; - allocated a fresh `Vec<(String, String)>`, cloning every label key and value, even though `metric.labels` is already `[(&'static str, Cow<'static, str>)]`. Now: names/help resolve to `&'static str` without touching the intern cache when already borrowed; each metric is described once (tracked in a `HashSet`); and the recorder is fed `&metric.labels` directly, removing the per-cycle label clone. No metric names, help, label keys/values, or emitted values change. Verified by building rustfs-obs and running the report unit tests (the `metrics` macro accepts the borrowed label slice directly). Addresses rustfs/backlog#1185 (P3, report path). Co-authored-by: heihutu <heihutu@gmail.com>
This commit is contained in:
@@ -15,11 +15,14 @@
|
||||
use crate::metrics::schema::{MetricDescriptor, MetricType};
|
||||
use metrics::{counter, describe_counter, describe_gauge, describe_histogram, gauge};
|
||||
use std::borrow::Cow;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::sync::{Mutex, OnceLock};
|
||||
|
||||
static NAME_CACHE: OnceLock<Mutex<HashMap<String, &'static str>>> = OnceLock::new();
|
||||
static HELP_CACHE: OnceLock<Mutex<HashMap<String, &'static str>>> = OnceLock::new();
|
||||
/// Metric names already handed to `describe_*!`. The metadata never changes, so
|
||||
/// each name is described once instead of on every collection cycle.
|
||||
static DESCRIBED: OnceLock<Mutex<HashSet<&'static str>>> = OnceLock::new();
|
||||
|
||||
fn intern_string(cache: &OnceLock<Mutex<HashMap<String, &'static str>>>, value: &str) -> &'static str {
|
||||
let cache = cache.get_or_init(Default::default);
|
||||
@@ -34,8 +37,36 @@ fn intern_string(cache: &OnceLock<Mutex<HashMap<String, &'static str>>>, value:
|
||||
}
|
||||
}
|
||||
|
||||
fn into_static_str(cache: &OnceLock<Mutex<HashMap<String, &'static str>>>, value: &str) -> &'static str {
|
||||
intern_string(cache, value)
|
||||
/// Resolve a metric name/help `Cow` to a `&'static str`, skipping the intern
|
||||
/// lock+leak when it is already borrowed (the common case for statically-named
|
||||
/// metrics); only owned strings pay the intern cost.
|
||||
// The `&Cow` is intentional: we must inspect the borrowed-vs-owned variant to
|
||||
// return the existing `&'static str` without interning, so `&str` will not do.
|
||||
#[allow(clippy::ptr_arg)]
|
||||
fn static_str(cache: &OnceLock<Mutex<HashMap<String, &'static str>>>, value: &Cow<'static, str>) -> &'static str {
|
||||
match value {
|
||||
Cow::Borrowed(s) => s,
|
||||
Cow::Owned(s) => intern_string(cache, s),
|
||||
}
|
||||
}
|
||||
|
||||
/// Hand a metric name to `describe_*!` only the first time it is seen. The
|
||||
/// metadata (help text) never changes, so describing on every collection cycle
|
||||
/// only re-locks the recorder's metadata map. The help is resolved lazily, so an
|
||||
/// already-described metric costs one `HashSet` probe and nothing else.
|
||||
// `&Cow` mirrors `static_str`, which needs the borrowed-vs-owned variant.
|
||||
#[allow(clippy::ptr_arg)]
|
||||
fn describe_metric_once(name: &'static str, metric_type: MetricType, help: &Cow<'static, str>) {
|
||||
let described = DESCRIBED.get_or_init(Default::default);
|
||||
let mut described = described.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
|
||||
if described.insert(name) {
|
||||
let help = static_str(&HELP_CACHE, help);
|
||||
match metric_type {
|
||||
MetricType::Counter => describe_counter!(name, help),
|
||||
MetricType::Gauge => describe_gauge!(name, help),
|
||||
MetricType::Histogram => describe_histogram!(name, help),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn counter_value_from_f64(value: f64) -> Option<u64> {
|
||||
@@ -52,32 +83,19 @@ fn counter_value_from_f64(value: f64) -> Option<u64> {
|
||||
|
||||
pub fn report_metrics(metrics: &[PrometheusMetric]) {
|
||||
for metric in metrics {
|
||||
let name = into_static_str(&NAME_CACHE, &metric.name);
|
||||
let help = into_static_str(&HELP_CACHE, &metric.help);
|
||||
|
||||
match metric.metric_type {
|
||||
MetricType::Counter => describe_counter!(name, help),
|
||||
MetricType::Gauge => describe_gauge!(name, help),
|
||||
MetricType::Histogram => describe_histogram!(name, help),
|
||||
}
|
||||
|
||||
let labels: Vec<(String, String)> = metric.labels.iter().map(|(k, v)| (k.to_string(), v.to_string())).collect();
|
||||
let name = static_str(&NAME_CACHE, &metric.name);
|
||||
describe_metric_once(name, metric.metric_type, &metric.help);
|
||||
|
||||
// `metric.labels` is already `[(&'static str, Cow<'static, str>)]`, so emit
|
||||
// straight from it — no per-cycle `Vec<(String, String)>` clone.
|
||||
match metric.metric_type {
|
||||
MetricType::Counter => {
|
||||
if let Some(value) = counter_value_from_f64(metric.value) {
|
||||
let counter = counter!(name, &labels);
|
||||
counter.absolute(value);
|
||||
counter!(name, &metric.labels).absolute(value);
|
||||
}
|
||||
}
|
||||
MetricType::Gauge => {
|
||||
let gauge = gauge!(name, &labels);
|
||||
gauge.set(metric.value);
|
||||
}
|
||||
MetricType::Histogram => {
|
||||
let histogram = metrics::histogram!(name, &labels);
|
||||
histogram.record(metric.value);
|
||||
}
|
||||
MetricType::Gauge => gauge!(name, &metric.labels).set(metric.value),
|
||||
MetricType::Histogram => metrics::histogram!(name, &metric.labels).record(metric.value),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user