1use std::collections::HashMap;
2use std::hash::BuildHasher;
3use std::iter::FromIterator;
4use std::sync::Arc;
5
6use crate::dogstats::aggregator::SigFig;
7use crate::DefaultMetricHasher;
8use crate::MetricResult;
9
10#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
12pub enum HistogramBaseMetric {
13 Count,
15 Min,
17 Avg,
19 Max,
21}
22
23impl HistogramBaseMetric {
24 const fn mask(self) -> u8 {
25 match self {
26 Self::Count => 1 << 0,
27 Self::Min => 1 << 1,
28 Self::Avg => 1 << 2,
29 Self::Max => 1 << 3,
30 }
31 }
32}
33
34#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
35#[repr(transparent)]
36pub struct HistogramBaseMetrics(u8);
37
38impl HistogramBaseMetrics {
39 pub(crate) const NONE: Self = Self(0);
40 pub(crate) const ALL: Self = Self(
41 HistogramBaseMetric::Count.mask()
42 | HistogramBaseMetric::Min.mask()
43 | HistogramBaseMetric::Avg.mask()
44 | HistogramBaseMetric::Max.mask(),
45 );
46
47 pub(crate) const fn only(metric: HistogramBaseMetric) -> Self {
48 Self(metric.mask())
49 }
50
51 pub(crate) const fn contains(self, metric: HistogramBaseMetric) -> bool {
52 self.0 & metric.mask() != 0
53 }
54
55 pub(crate) const fn with(self, metric: HistogramBaseMetric) -> Self {
56 Self(self.0 | metric.mask())
57 }
58
59 pub(crate) const fn without(self, metric: HistogramBaseMetric) -> Self {
60 Self(self.0 & !metric.mask())
61 }
62}
63
64impl From<HistogramBaseMetric> for HistogramBaseMetrics {
65 fn from(metric: HistogramBaseMetric) -> Self {
66 Self::only(metric)
67 }
68}
69
70impl<const N: usize> From<[HistogramBaseMetric; N]> for HistogramBaseMetrics {
71 fn from(metrics: [HistogramBaseMetric; N]) -> Self {
72 Self::from_iter(metrics)
73 }
74}
75
76impl FromIterator<HistogramBaseMetric> for HistogramBaseMetrics {
77 fn from_iter<T: IntoIterator<Item = HistogramBaseMetric>>(iter: T) -> Self {
78 iter.into_iter().fold(Self::NONE, Self::with)
79 }
80}
81
82#[derive(Debug, Clone)]
94pub struct HistogramConfig {
95 sig_fig: SigFig,
96 bounds: Bounds,
97 percentiles: Arc<[f64]>,
98 emit_base_metrics: HistogramBaseMetrics,
99}
100
101#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
103pub struct Bounds {
104 min: u64,
105 max: u64,
106}
107
108impl Bounds {
109 pub fn new(min: u64, max: u64) -> MetricResult<Self> {
114 if min < 1 {
115 return Err("Invalid histogram bounds: min must be >= 1".into());
116 }
117 if max < min {
118 return Err("Invalid histogram bounds: max must be >= min".into());
119 }
120
121 Ok(Self { min, max })
122 }
123
124 #[must_use]
126 pub const fn min(self) -> u64 {
127 self.min
128 }
129
130 #[must_use]
132 pub const fn max(self) -> u64 {
133 self.max
134 }
135}
136
137impl Default for Bounds {
138 fn default() -> Self {
139 Self {
140 min: 1,
141 max: u64::MAX,
142 }
143 }
144}
145
146impl HistogramConfig {
147 const fn set_emit_base_metric(&mut self, metric: HistogramBaseMetric, emit: bool) {
148 self.emit_base_metrics = if emit {
149 self.emit_base_metrics.with(metric)
150 } else {
151 self.emit_base_metrics.without(metric)
152 };
153 }
154
155 pub fn new(sig_fig: SigFig, percentiles: Vec<f64>) -> MetricResult<Self> {
160 for percentile in &percentiles {
161 if !percentile.is_finite() || *percentile < 0.0 || *percentile >= 1.0 {
162 return Err("Invalid percentile: must be finite and in range [0.0, 1.0)".into());
163 }
164 }
165
166 Ok(Self {
167 sig_fig,
168 bounds: Bounds::default(),
169 percentiles: percentiles.into(),
170 emit_base_metrics: HistogramBaseMetrics::ALL,
171 })
172 }
173
174 #[must_use]
176 pub fn with_base_metrics(
177 mut self,
178 emit_base_metrics: impl IntoIterator<Item = HistogramBaseMetric>,
179 ) -> Self {
180 self.emit_base_metrics = HistogramBaseMetrics::from_iter(emit_base_metrics);
181 self
182 }
183
184 #[must_use]
186 pub const fn with_count(mut self, emit: bool) -> Self {
187 self.set_emit_base_metric(HistogramBaseMetric::Count, emit);
188 self
189 }
190
191 #[must_use]
193 pub const fn with_min(mut self, emit: bool) -> Self {
194 self.set_emit_base_metric(HistogramBaseMetric::Min, emit);
195 self
196 }
197
198 #[must_use]
202 pub const fn with_avg(mut self, emit: bool) -> Self {
203 self.set_emit_base_metric(HistogramBaseMetric::Avg, emit);
204 self
205 }
206
207 #[must_use]
209 pub const fn with_max(mut self, emit: bool) -> Self {
210 self.set_emit_base_metric(HistogramBaseMetric::Max, emit);
211 self
212 }
213
214 pub(crate) const fn sig_fig(&self) -> SigFig {
215 self.sig_fig
216 }
217
218 pub(crate) const fn percentiles(&self) -> &Arc<[f64]> {
219 &self.percentiles
220 }
221
222 pub(crate) const fn bounds(&self) -> Bounds {
223 self.bounds
224 }
225
226 pub(crate) const fn emit_base_metrics(&self) -> HistogramBaseMetrics {
227 self.emit_base_metrics
228 }
229
230 const fn with_bounds_checked(mut self, bounds: Bounds) -> Self {
231 self.bounds = bounds;
232 self
233 }
234
235 pub fn with_bounds(self, min: u64, max: u64) -> MetricResult<Self> {
242 Ok(self.with_bounds_checked(Bounds::new(min, max)?))
243 }
244}
245
246impl Default for HistogramConfig {
247 fn default() -> Self {
248 Self {
249 sig_fig: SigFig::default(),
250 bounds: Bounds::default(),
251 percentiles: vec![0.95, 0.99].into(),
252 emit_base_metrics: HistogramBaseMetrics::ALL,
253 }
254 }
255}
256
257#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
258pub struct HistogramPoolSpec {
259 pub sig_fig: SigFig,
260 pub bounds: Bounds,
261}
262
263impl HistogramPoolSpec {
264 pub(crate) const fn from_config(config: &HistogramConfig) -> Self {
265 Self {
266 sig_fig: config.sig_fig,
267 bounds: config.bounds,
268 }
269 }
270}
271
272#[derive(Debug, Clone)]
273pub struct ResolvedHistogramConfig {
274 config: HistogramConfig,
275 pool_id: usize,
276}
277
278impl ResolvedHistogramConfig {
279 pub const fn from_config(config: HistogramConfig, pool_id: usize) -> Self {
280 Self { config, pool_id }
281 }
282
283 pub(crate) const fn pool_id(&self) -> usize {
284 self.pool_id
285 }
286
287 pub(crate) const fn sig_fig(&self) -> SigFig {
288 self.config.sig_fig()
289 }
290
291 pub(crate) const fn bounds(&self) -> Bounds {
292 self.config.bounds()
293 }
294
295 pub(crate) const fn percentiles(&self) -> &Arc<[f64]> {
296 self.config.percentiles()
297 }
298
299 pub(crate) const fn emit_base_metrics(&self) -> HistogramBaseMetrics {
300 self.config.emit_base_metrics()
301 }
302}
303
304pub struct ResolvedHistogramConfigs<S = DefaultMetricHasher>
305where
306 S: BuildHasher + Clone,
307{
308 pub default_histogram_config: ResolvedHistogramConfig,
309 pub histogram_configs: HashMap<String, ResolvedHistogramConfig, S>,
310 pub pool_specs: Arc<[HistogramPoolSpec]>,
311 pub pool_count: usize,
312}
313
314pub fn resolve_histogram_configs<S>(
315 default_histogram_config: HistogramConfig,
316 histogram_configs: HashMap<String, HistogramConfig, S>,
317 hasher_builder: &S,
318) -> ResolvedHistogramConfigs<S>
319where
320 S: BuildHasher + Clone,
321{
322 let register_pool = |config: &HistogramConfig,
323 pool_ids: &mut HashMap<HistogramPoolSpec, usize, S>,
324 next_pool_id: &mut usize| {
325 let pool_spec = HistogramPoolSpec::from_config(config);
326 *pool_ids.entry(pool_spec).or_insert_with(|| {
327 let id = *next_pool_id;
328 *next_pool_id += 1;
329 id
330 })
331 };
332
333 let mut pool_ids = HashMap::with_hasher(hasher_builder.clone());
334 let mut next_pool_id = 0;
335 let default_pool_id =
336 register_pool(&default_histogram_config, &mut pool_ids, &mut next_pool_id);
337
338 let mut resolved_histogram_configs =
339 HashMap::with_capacity_and_hasher(histogram_configs.len(), hasher_builder.clone());
340 for (metric, config) in histogram_configs {
341 let pool_id = register_pool(&config, &mut pool_ids, &mut next_pool_id);
342 resolved_histogram_configs.insert(
343 metric,
344 ResolvedHistogramConfig::from_config(config, pool_id),
345 );
346 }
347
348 let mut pool_specs =
349 vec![HistogramPoolSpec::from_config(&default_histogram_config); next_pool_id];
350 for (pool_spec, pool_id) in pool_ids {
351 pool_specs[pool_id] = pool_spec;
352 }
353
354 ResolvedHistogramConfigs {
355 default_histogram_config: ResolvedHistogramConfig::from_config(
356 default_histogram_config,
357 default_pool_id,
358 ),
359 histogram_configs: resolved_histogram_configs,
360 pool_specs: pool_specs.into(),
361 pool_count: next_pool_id,
362 }
363}
364
365#[cfg(test)]
366mod tests {
367 use super::{resolve_histogram_configs, Bounds, HistogramBaseMetric, HistogramConfig};
368 use crate::dogstats::aggregator::SigFig;
369 use std::collections::HashMap;
370
371 #[test]
372 fn histogram_config_allows_empty_percentiles() {
373 let config = HistogramConfig::new(SigFig::default(), vec![]);
374 assert!(config.is_ok());
375 }
376
377 #[test]
378 fn histogram_config_rejects_invalid_percentiles() {
379 let invalid = [f64::NAN, f64::INFINITY, -0.1, 1.0];
380 for percentile in invalid {
381 let config = HistogramConfig::new(SigFig::default(), vec![percentile]);
382 assert!(config.is_err());
383 }
384 }
385
386 #[test]
387 fn bounds_reject_invalid_values() {
388 assert!(Bounds::new(0, 10).is_err());
389 assert!(Bounds::new(10, 9).is_err());
390 }
391
392 #[test]
393 fn histogram_base_metrics_builds_typed_sets() {
394 let metrics = super::HistogramBaseMetrics::from([
395 HistogramBaseMetric::Count,
396 HistogramBaseMetric::Max,
397 ]);
398
399 assert!(metrics.contains(HistogramBaseMetric::Count));
400 assert!(!metrics.contains(HistogramBaseMetric::Min));
401 assert!(metrics.contains(HistogramBaseMetric::Max));
402 }
403
404 #[test]
405 fn histogram_config_with_base_metrics_replaces_selection() {
406 let config = HistogramConfig::new(SigFig::default(), Vec::new())
407 .unwrap()
408 .with_base_metrics([HistogramBaseMetric::Count, HistogramBaseMetric::Max]);
409
410 let metrics = config.emit_base_metrics();
411 assert!(metrics.contains(HistogramBaseMetric::Count));
412 assert!(!metrics.contains(HistogramBaseMetric::Min));
413 assert!(!metrics.contains(HistogramBaseMetric::Avg));
414 assert!(metrics.contains(HistogramBaseMetric::Max));
415 }
416
417 #[test]
418 fn resolve_histogram_configs_reuses_pool_ids_for_matching_specs() {
419 let default_config = HistogramConfig::default();
420 let shared = HistogramConfig::new(SigFig::default(), vec![0.95]).unwrap();
421 let distinct = HistogramConfig::new(SigFig::TWO, vec![0.95]).unwrap();
422 let mut configs = HashMap::new();
423 configs.insert("metric.a".to_string(), shared.clone());
424 configs.insert("metric.b".to_string(), shared);
425 configs.insert("metric.c".to_string(), distinct);
426
427 let resolved =
428 resolve_histogram_configs(default_config, configs, &std::hash::RandomState::new());
429
430 assert_eq!(resolved.pool_count, 2);
431 assert_eq!(
432 resolved.histogram_configs["metric.a"].pool_id(),
433 resolved.histogram_configs["metric.b"].pool_id()
434 );
435 assert_ne!(
436 resolved.histogram_configs["metric.a"].pool_id(),
437 resolved.histogram_configs["metric.c"].pool_id()
438 );
439 assert_eq!(resolved.pool_specs.len(), resolved.pool_count);
440 }
441}