fast_telemetry/metric/
labeled_histogram.rs1use crate::Histogram;
4use crate::label::LabelEnum;
5use std::marker::PhantomData;
6
7pub struct LabeledHistogram<L: LabelEnum> {
46 histograms: Vec<Histogram>,
47 _phantom: PhantomData<L>,
48}
49
50impl<L: LabelEnum> LabeledHistogram<L> {
51 pub fn new(bounds: &[u64], shard_count: usize) -> Self {
55 let histograms = (0..L::CARDINALITY)
56 .map(|_| Histogram::new(bounds, shard_count))
57 .collect();
58 Self {
59 histograms,
60 _phantom: PhantomData,
61 }
62 }
63
64 pub fn with_latency_buckets(shard_count: usize) -> Self {
68 let histograms = (0..L::CARDINALITY)
69 .map(|_| Histogram::with_latency_buckets(shard_count))
70 .collect();
71 Self {
72 histograms,
73 _phantom: PhantomData,
74 }
75 }
76
77 #[inline]
79 pub fn record(&self, label: L, value: u64) {
80 let idx = label.as_index();
81 debug_assert!(idx < self.histograms.len(), "label index out of bounds");
82 if cfg!(debug_assertions) {
83 self.histograms[idx].record(value);
84 } else {
85 unsafe { self.histograms.get_unchecked(idx) }.record(value);
86 }
87 }
88
89 pub fn get(&self, label: L) -> &Histogram {
91 let idx = label.as_index();
92 if cfg!(debug_assertions) {
93 &self.histograms[idx]
94 } else {
95 unsafe { self.histograms.get_unchecked(idx) }
96 }
97 }
98
99 pub fn iter(&self) -> impl Iterator<Item = (L, Vec<(u64, u64)>, u64, u64)> + '_ {
103 self.histograms.iter().enumerate().map(|(idx, histogram)| {
104 (
105 L::from_index(idx),
106 histogram.buckets_cumulative(),
107 histogram.sum(),
108 histogram.count(),
109 )
110 })
111 }
112}
113
114#[cfg(test)]
115mod tests {
116 use super::*;
117
118 #[derive(Copy, Clone, Debug, PartialEq)]
119 enum TestLabel {
120 Fast,
121 Medium,
122 Slow,
123 }
124
125 impl LabelEnum for TestLabel {
126 const CARDINALITY: usize = 3;
127 const LABEL_NAME: &'static str = "speed";
128
129 fn as_index(self) -> usize {
130 self as usize
131 }
132
133 fn from_index(index: usize) -> Self {
134 match index {
135 0 => Self::Fast,
136 1 => Self::Medium,
137 _ => Self::Slow,
138 }
139 }
140
141 fn variant_name(self) -> &'static str {
142 match self {
143 Self::Fast => "fast",
144 Self::Medium => "medium",
145 Self::Slow => "slow",
146 }
147 }
148 }
149
150 #[test]
151 fn test_basic_recording() {
152 let histogram: LabeledHistogram<TestLabel> = LabeledHistogram::new(&[10, 100, 1000], 4);
153
154 histogram.record(TestLabel::Fast, 5);
155 histogram.record(TestLabel::Fast, 8);
156 histogram.record(TestLabel::Medium, 50);
157 histogram.record(TestLabel::Slow, 5000);
158
159 assert_eq!(histogram.get(TestLabel::Fast).count(), 2);
160 assert_eq!(histogram.get(TestLabel::Medium).count(), 1);
161 assert_eq!(histogram.get(TestLabel::Slow).count(), 1);
162 }
163
164 #[test]
165 fn test_iteration() {
166 let histogram: LabeledHistogram<TestLabel> = LabeledHistogram::new(&[100], 4);
167
168 histogram.record(TestLabel::Fast, 50);
169 histogram.record(TestLabel::Medium, 150);
170
171 let data: Vec<_> = histogram.iter().collect();
172 assert_eq!(data.len(), 3);
173
174 assert_eq!(data[0].0, TestLabel::Fast);
176 assert_eq!(data[0].3, 1); assert_eq!(data[1].0, TestLabel::Medium);
180 assert_eq!(data[1].3, 1); assert_eq!(data[2].0, TestLabel::Slow);
184 assert_eq!(data[2].3, 0); }
186
187 #[test]
188 fn test_latency_buckets() {
189 let histogram: LabeledHistogram<TestLabel> = LabeledHistogram::with_latency_buckets(4);
190
191 histogram.record(TestLabel::Fast, 5); histogram.record(TestLabel::Fast, 100); histogram.record(TestLabel::Medium, 1_000); assert_eq!(histogram.get(TestLabel::Fast).count(), 2);
196 assert_eq!(histogram.get(TestLabel::Medium).count(), 1);
197 }
198
199 #[test]
200 fn test_concurrent_recording() {
201 let histogram: LabeledHistogram<TestLabel> = LabeledHistogram::new(&[100], 4);
202
203 std::thread::scope(|s| {
204 for _ in 0..4 {
205 s.spawn(|| {
206 for i in 0..1000 {
207 histogram.record(TestLabel::Fast, i);
208 }
209 });
210 }
211 });
212
213 assert_eq!(histogram.get(TestLabel::Fast).count(), 4000);
214 }
215}