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, &'_ Histogram)> + '_ {
106 self.histograms
107 .iter()
108 .enumerate()
109 .map(|(idx, histogram)| (L::from_index(idx), histogram))
110 }
111}
112
113#[cfg(test)]
114mod tests {
115 use super::*;
116
117 #[derive(Copy, Clone, Debug, PartialEq)]
118 enum TestLabel {
119 Fast,
120 Medium,
121 Slow,
122 }
123
124 impl LabelEnum for TestLabel {
125 const CARDINALITY: usize = 3;
126 const LABEL_NAME: &'static str = "speed";
127
128 fn as_index(self) -> usize {
129 self as usize
130 }
131
132 fn from_index(index: usize) -> Self {
133 match index {
134 0 => Self::Fast,
135 1 => Self::Medium,
136 _ => Self::Slow,
137 }
138 }
139
140 fn variant_name(self) -> &'static str {
141 match self {
142 Self::Fast => "fast",
143 Self::Medium => "medium",
144 Self::Slow => "slow",
145 }
146 }
147 }
148
149 #[test]
150 fn test_basic_recording() {
151 let histogram: LabeledHistogram<TestLabel> = LabeledHistogram::new(&[10, 100, 1000], 4);
152
153 histogram.record(TestLabel::Fast, 5);
154 histogram.record(TestLabel::Fast, 8);
155 histogram.record(TestLabel::Medium, 50);
156 histogram.record(TestLabel::Slow, 5000);
157
158 assert_eq!(histogram.get(TestLabel::Fast).count(), 2);
159 assert_eq!(histogram.get(TestLabel::Medium).count(), 1);
160 assert_eq!(histogram.get(TestLabel::Slow).count(), 1);
161 }
162
163 #[test]
164 fn test_iteration() {
165 let histogram: LabeledHistogram<TestLabel> = LabeledHistogram::new(&[100], 4);
166
167 histogram.record(TestLabel::Fast, 50);
168 histogram.record(TestLabel::Medium, 150);
169
170 let data: Vec<_> = histogram.iter().collect();
171 assert_eq!(data.len(), 3);
172
173 assert_eq!(data[0].0, TestLabel::Fast);
175 assert_eq!(data[0].1.count(), 1);
176
177 assert_eq!(data[1].0, TestLabel::Medium);
179 assert_eq!(data[1].1.count(), 1);
180
181 assert_eq!(data[2].0, TestLabel::Slow);
183 assert_eq!(data[2].1.count(), 0);
184 }
185
186 #[test]
187 fn test_latency_buckets() {
188 let histogram: LabeledHistogram<TestLabel> = LabeledHistogram::with_latency_buckets(4);
189
190 histogram.record(TestLabel::Fast, 5); histogram.record(TestLabel::Fast, 100); histogram.record(TestLabel::Medium, 1_000); assert_eq!(histogram.get(TestLabel::Fast).count(), 2);
195 assert_eq!(histogram.get(TestLabel::Medium).count(), 1);
196 }
197
198 #[test]
199 fn test_concurrent_recording() {
200 let histogram: LabeledHistogram<TestLabel> = LabeledHistogram::new(&[100], 4);
201
202 std::thread::scope(|s| {
203 for _ in 0..4 {
204 s.spawn(|| {
205 for i in 0..1000 {
206 histogram.record(TestLabel::Fast, i);
207 }
208 });
209 }
210 });
211
212 assert_eq!(histogram.get(TestLabel::Fast).count(), 4000);
213 }
214}