Files
rustfs/crates/obs/src/metrics/report.rs
T
houseme 5088a6cde4 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>
2026-07-11 17:46:13 +00:00

217 lines
7.9 KiB
Rust

// Copyright 2024 RustFS Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use crate::metrics::schema::{MetricDescriptor, MetricType};
use metrics::{counter, describe_counter, describe_gauge, describe_histogram, gauge};
use std::borrow::Cow;
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);
let mut cache = cache.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
if let Some(existing) = cache.get(value) {
existing
} else {
let value = Box::leak(value.to_string().into_boxed_str());
cache.insert(value.to_string(), value);
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> {
if !value.is_finite() || value < 0.0 {
return None;
}
if value >= u64::MAX as f64 {
Some(u64::MAX)
} else {
Some(value as u64)
}
}
pub fn report_metrics(metrics: &[PrometheusMetric]) {
for metric in metrics {
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) {
counter!(name, &metric.labels).absolute(value);
}
}
MetricType::Gauge => gauge!(name, &metric.labels).set(metric.value),
MetricType::Histogram => metrics::histogram!(name, &metric.labels).record(metric.value),
}
}
}
#[derive(Debug, Clone)]
pub struct PrometheusMetric {
pub name: Cow<'static, str>,
pub metric_type: MetricType,
pub help: Cow<'static, str>,
pub labels: Vec<(&'static str, Cow<'static, str>)>,
pub value: f64,
}
impl PrometheusMetric {
#[inline]
pub const fn new(name: &'static str, metric_type: MetricType, help: &'static str, value: f64) -> Self {
Self {
name: Cow::Borrowed(name),
metric_type,
help: Cow::Borrowed(help),
labels: Vec::new(),
value,
}
}
#[inline]
pub fn new_owned(name: String, metric_type: MetricType, help: String, value: f64) -> Self {
Self {
name: Cow::Owned(name),
metric_type,
help: Cow::Owned(help),
labels: Vec::new(),
value,
}
}
#[inline]
pub fn from_descriptor(descriptor: &MetricDescriptor, value: f64) -> Self {
let help = intern_string(&HELP_CACHE, &descriptor.help);
Self {
name: Cow::Owned(descriptor.get_full_metric_name()),
metric_type: descriptor.metric_type,
help: Cow::Borrowed(help),
labels: Vec::new(),
value,
}
}
#[inline]
#[allow(dead_code)]
pub fn with_label(mut self, key: &'static str, value: impl Into<Cow<'static, str>>) -> Self {
self.labels.push((key, value.into()));
self
}
#[inline]
#[allow(dead_code)]
pub fn with_label_owned(mut self, key: &'static str, value: String) -> Self {
self.labels.push((key, Cow::Owned(value)));
self
}
#[inline]
#[allow(dead_code)]
pub fn with_labels(mut self, labels: Vec<(&'static str, Cow<'static, str>)>) -> Self {
self.labels = labels;
self
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::metrics::schema::{MetricName, MetricNamespace, MetricSubsystem};
#[test]
fn from_descriptor_uses_prometheus_metric_names_for_all_types() {
let cases = [
(MetricType::Counter, "rustfs_api_requests_total"),
(MetricType::Gauge, "rustfs_system_memory_used_bytes"),
(MetricType::Histogram, "rustfs_custom_path_latency_seconds"),
];
for (metric_type, expected_name) in cases {
let subsystem = match metric_type {
MetricType::Counter => MetricSubsystem::ApiRequests,
MetricType::Gauge => MetricSubsystem::SystemMemory,
MetricType::Histogram => MetricSubsystem::new("/custom/path"),
};
let name = match metric_type {
MetricType::Counter => MetricName::ApiRequestsTotal,
MetricType::Gauge => MetricName::Custom("used_bytes".to_string()),
MetricType::Histogram => MetricName::Custom("latency_seconds".to_string()),
};
let metric = PrometheusMetric::from_descriptor(
&MetricDescriptor::new(name, metric_type, "test help".to_string(), vec![], MetricNamespace::RustFS, subsystem),
1.0,
);
assert_eq!(metric.name, expected_name);
assert_eq!(metric.metric_type, metric_type);
}
}
#[test]
fn counter_value_from_f64_rejects_negative_and_nan_inputs() {
assert_eq!(counter_value_from_f64(-1.0), None);
assert_eq!(counter_value_from_f64(f64::NAN), None);
assert_eq!(counter_value_from_f64(f64::NEG_INFINITY), None);
}
#[test]
fn counter_value_from_f64_clamps_large_values() {
assert_eq!(counter_value_from_f64(42.0), Some(42));
assert_eq!(counter_value_from_f64(u64::MAX as f64 * 2.0), Some(u64::MAX));
}
}