glean_core/metrics/timing_distribution.rs
1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5use std::collections::HashMap;
6use std::mem;
7use std::sync::atomic::{AtomicUsize, Ordering};
8use std::sync::{Arc, Mutex};
9use std::time::Duration;
10
11use malloc_size_of_derive::MallocSizeOf;
12
13use crate::common_metric_data::{CommonMetricDataInternal, MetricLabel};
14use crate::error_recording::{record_error, test_get_num_recorded_errors, ErrorType};
15use crate::histogram::{Functional, Histogram};
16use crate::metrics::time_unit::TimeUnit;
17use crate::metrics::{DistributionData, Metric, MetricType};
18use crate::Glean;
19use crate::{CommonMetricData, TestGetValue};
20
21// The base of the logarithm used to determine bucketing
22const LOG_BASE: f64 = 2.0;
23
24// The buckets per each order of magnitude of the logarithm.
25const BUCKETS_PER_MAGNITUDE: f64 = 8.0;
26
27// Maximum time, which means we retain a maximum of 316 buckets.
28// It is automatically adjusted based on the `time_unit` parameter
29// so that:
30//
31// - `nanosecond` - 10 minutes
32// - `microsecond` - ~6.94 days
33// - `millisecond` - ~19 years
34const MAX_SAMPLE_TIME: u64 = 1000 * 1000 * 1000 * 60 * 10;
35
36/// Identifier for a running timer.
37///
38/// Its internals are considered private,
39/// but due to UniFFI's behavior we expose its field for now.
40#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, MallocSizeOf)]
41pub struct TimerId {
42 /// This timer's id.
43 pub id: u64,
44}
45
46impl From<u64> for TimerId {
47 fn from(val: u64) -> TimerId {
48 TimerId { id: val }
49 }
50}
51
52impl From<usize> for TimerId {
53 fn from(val: usize) -> TimerId {
54 TimerId { id: val as u64 }
55 }
56}
57
58/// A timing distribution metric.
59///
60/// Timing distributions are used to accumulate and store time measurement, for analyzing distributions of the timing data.
61#[derive(Clone, Debug)]
62pub struct TimingDistributionMetric {
63 meta: Arc<CommonMetricDataInternal>,
64 time_unit: TimeUnit,
65 next_id: Arc<AtomicUsize>,
66 start_times: Arc<Mutex<HashMap<TimerId, u64>>>,
67}
68
69impl ::malloc_size_of::MallocSizeOf for TimingDistributionMetric {
70 fn size_of(&self, ops: &mut malloc_size_of::MallocSizeOfOps) -> usize {
71 // Note: This is behind an `Arc`.
72 // `size_of` should only be called on the main thread to avoid double-counting.
73 self.meta.size_of(ops)
74 + self.time_unit.size_of(ops)
75 + self.next_id.size_of(ops)
76 + self.start_times.lock().unwrap().size_of(ops)
77 }
78}
79
80/// Create a snapshot of the histogram with a time unit.
81///
82/// The snapshot can be serialized into the payload format.
83pub(crate) fn snapshot(hist: &Histogram<Functional>) -> DistributionData {
84 DistributionData {
85 // **Caution**: This cannot use `Histogram::snapshot_values` and needs to use the more
86 // specialized snapshot function.
87 values: hist
88 .snapshot()
89 .iter()
90 .map(|(&k, &v)| (k as i64, v as i64))
91 .collect(),
92 sum: hist.sum() as i64,
93 count: hist.count() as i64,
94 }
95}
96
97impl MetricType for TimingDistributionMetric {
98 fn meta(&self) -> &CommonMetricDataInternal {
99 &self.meta
100 }
101
102 fn with_name(&self, name: String) -> Self {
103 let mut meta = (*self.meta).clone();
104 meta.inner.name = name;
105 Self {
106 meta: Arc::new(meta),
107 time_unit: self.time_unit,
108 next_id: Arc::new(AtomicUsize::new(1)),
109 start_times: Arc::new(Mutex::new(Default::default())),
110 }
111 }
112
113 fn with_label(&self, label: MetricLabel) -> Self {
114 let mut meta = (*self.meta).clone();
115 meta.inner.label = Some(label);
116 Self {
117 meta: Arc::new(meta),
118 time_unit: self.time_unit,
119 next_id: Arc::new(AtomicUsize::new(1)),
120 start_times: Arc::new(Mutex::new(Default::default())),
121 }
122 }
123}
124
125// IMPORTANT:
126//
127// When changing this implementation, make sure all the operations are
128// also declared in the related trait in `../traits/`.
129impl TimingDistributionMetric {
130 /// Creates a new timing distribution metric.
131 pub fn new(meta: CommonMetricData, time_unit: TimeUnit) -> Self {
132 Self {
133 meta: Arc::new(meta.into()),
134 time_unit,
135 next_id: Arc::new(AtomicUsize::new(1)),
136 start_times: Arc::new(Mutex::new(Default::default())),
137 }
138 }
139
140 /// Starts tracking time for the provided metric.
141 ///
142 /// This records an error if it’s already tracking time (i.e.
143 /// [`set_start`](TimingDistributionMetric::set_start) was already called with no
144 /// corresponding [`set_stop_and_accumulate`](TimingDistributionMetric::set_stop_and_accumulate)): in
145 /// that case the original start time will be preserved.
146 ///
147 /// # Arguments
148 ///
149 /// * `start_time` - Timestamp in nanoseconds.
150 ///
151 /// # Returns
152 ///
153 /// A unique [`TimerId`] for the new timer.
154 pub fn start(&self) -> TimerId {
155 let start_time = zeitstempel::now_awake();
156 let id = self.next_id.fetch_add(1, Ordering::SeqCst).into();
157 let metric = self.clone();
158 crate::launch_with_glean(move |_glean| metric.set_start(id, start_time));
159 id
160 }
161
162 pub(crate) fn start_sync(&self) -> TimerId {
163 let start_time = zeitstempel::now_awake();
164 let id = self.next_id.fetch_add(1, Ordering::SeqCst).into();
165 let metric = self.clone();
166 metric.set_start(id, start_time);
167 id
168 }
169
170 /// **Test-only API (exported for testing purposes).**
171 ///
172 /// Set start time for this metric synchronously.
173 ///
174 /// Use [`start`](Self::start) instead.
175 #[doc(hidden)]
176 pub fn set_start(&self, id: TimerId, start_time: u64) {
177 let mut map = self.start_times.lock().expect("can't lock timings map");
178 map.insert(id, start_time);
179 }
180
181 /// Stops tracking time for the provided metric and associated timer id.
182 ///
183 /// Adds a count to the corresponding bucket in the timing distribution.
184 /// This will record an error if no
185 /// [`set_start`](TimingDistributionMetric::set_start) was called.
186 ///
187 /// # Arguments
188 ///
189 /// * `id` - The [`TimerId`] to associate with this timing. This allows
190 /// for concurrent timing of events associated with different ids to the
191 /// same timespan metric.
192 /// * `stop_time` - Timestamp in nanoseconds.
193 pub fn stop_and_accumulate(&self, id: TimerId) {
194 let stop_time = zeitstempel::now_awake();
195 let metric = self.clone();
196 crate::launch_with_glean(move |glean| metric.set_stop_and_accumulate(glean, id, stop_time));
197 }
198
199 fn set_stop(&self, id: TimerId, stop_time: u64) -> Result<u64, (ErrorType, &str)> {
200 let mut start_times = self.start_times.lock().expect("can't lock timings map");
201 let start_time = match start_times.remove(&id) {
202 Some(start_time) => start_time,
203 None => return Err((ErrorType::InvalidState, "Timing not running")),
204 };
205
206 let duration = match stop_time.checked_sub(start_time) {
207 Some(duration) => duration,
208 None => {
209 return Err((
210 ErrorType::InvalidValue,
211 "Timer stopped with negative duration",
212 ))
213 }
214 };
215
216 Ok(duration)
217 }
218
219 /// **Test-only API (exported for testing purposes).**
220 ///
221 /// Set stop time for this metric synchronously.
222 ///
223 /// Use [`stop_and_accumulate`](Self::stop_and_accumulate) instead.
224 #[doc(hidden)]
225 pub fn set_stop_and_accumulate(&self, glean: &Glean, id: TimerId, stop_time: u64) {
226 if !self.should_record(glean) {
227 let mut start_times = self.start_times.lock().expect("can't lock timings map");
228 start_times.remove(&id);
229 return;
230 }
231
232 // Duration is in nanoseconds.
233 let mut duration = match self.set_stop(id, stop_time) {
234 Err((err_type, err_msg)) => {
235 record_error(glean, &self.meta, err_type, err_msg, None);
236 return;
237 }
238 Ok(duration) => duration,
239 };
240
241 let min_sample_time = self.time_unit.as_nanos(1);
242 let max_sample_time = self.time_unit.as_nanos(MAX_SAMPLE_TIME);
243
244 duration = if duration < min_sample_time {
245 // If measurement is less than the minimum, just truncate. This is
246 // not recorded as an error.
247 min_sample_time
248 } else if duration > max_sample_time {
249 let msg = format!(
250 "Sample is longer than the max for a time_unit of {:?} ({} ns)",
251 self.time_unit, max_sample_time
252 );
253 record_error(glean, &self.meta, ErrorType::InvalidOverflow, msg, None);
254 max_sample_time
255 } else {
256 duration
257 };
258
259 if !self.should_record(glean) {
260 return;
261 }
262
263 // Let's be defensive here:
264 // The uploader tries to store some timing distribution metrics,
265 // but in tests that storage might be gone already.
266 // Let's just ignore those.
267 // We do the same for counters.
268 // This should never happen in real app usage.
269 if let Some(storage) = glean.storage_opt() {
270 storage.record_with(glean, &self.meta, |old_value| match old_value {
271 Some(Metric::TimingDistribution(mut hist)) => {
272 hist.accumulate(duration);
273 Metric::TimingDistribution(hist)
274 }
275 _ => {
276 let mut hist = Histogram::functional(LOG_BASE, BUCKETS_PER_MAGNITUDE);
277 hist.accumulate(duration);
278 Metric::TimingDistribution(hist)
279 }
280 });
281 } else {
282 log::warn!(
283 "Couldn't get storage. Can't record timing distribution '{}'.",
284 self.meta.base_identifier()
285 );
286 }
287 }
288
289 /// Aborts a previous [`start`](Self::start) call.
290 ///
291 /// No error is recorded if no [`start`](Self::start) was called.
292 ///
293 /// # Arguments
294 ///
295 /// * `id` - The [`TimerId`] to associate with this timing. This allows
296 /// for concurrent timing of events associated with different ids to the
297 /// same timing distribution metric.
298 pub fn cancel(&self, id: TimerId) {
299 let metric = self.clone();
300 crate::launch_with_glean(move |_glean| metric.cancel_sync(id));
301 }
302
303 /// Aborts a previous [`start`](Self::start) call synchronously.
304 pub(crate) fn cancel_sync(&self, id: TimerId) {
305 let mut map = self.start_times.lock().expect("can't lock timings map");
306 map.remove(&id);
307 }
308
309 /// Accumulates the provided signed samples in the metric.
310 ///
311 /// This is required so that the platform-specific code can provide us with
312 /// 64 bit signed integers if no `u64` comparable type is available. This
313 /// will take care of filtering and reporting errors for any provided negative
314 /// sample.
315 ///
316 /// Please note that this assumes that the provided samples are already in
317 /// the "unit" declared by the instance of the metric type (e.g. if the
318 /// instance this method was called on is using [`TimeUnit::Second`], then
319 /// `samples` are assumed to be in that unit).
320 ///
321 /// # Arguments
322 ///
323 /// * `samples` - The vector holding the samples to be recorded by the metric.
324 ///
325 /// ## Notes
326 ///
327 /// Discards any negative value in `samples` and report an [`ErrorType::InvalidValue`]
328 /// for each of them. Reports an [`ErrorType::InvalidOverflow`] error for samples that
329 /// are longer than `MAX_SAMPLE_TIME`.
330 pub fn accumulate_samples(&self, samples: Vec<i64>) {
331 let metric = self.clone();
332 crate::launch_with_glean(move |glean| metric.accumulate_samples_sync(glean, &samples))
333 }
334
335 /// Accumulates precisely one signed sample and appends it to the metric.
336 ///
337 /// Precludes the need for a collection in the most common use case.
338 ///
339 /// Sign is required so that the platform-specific code can provide us with
340 /// a 64 bit signed integer if no `u64` comparable type is available. This
341 /// will take care of filtering and reporting errors for any provided negative
342 /// sample.
343 ///
344 /// Please note that this assumes that the provided sample is already in
345 /// the "unit" declared by the instance of the metric type (e.g. if the
346 /// instance this method was called on is using [`crate::TimeUnit::Second`], then
347 /// `sample` is assumed to be in that unit).
348 ///
349 /// # Arguments
350 ///
351 /// * `sample` - The singular sample to be recorded by the metric.
352 ///
353 /// ## Notes
354 ///
355 /// Discards any negative value and reports an [`ErrorType::InvalidValue`].
356 /// Reports an [`ErrorType::InvalidOverflow`] error if the sample is longer than
357 /// `MAX_SAMPLE_TIME`.
358 pub fn accumulate_single_sample(&self, sample: i64) {
359 let metric = self.clone();
360 crate::launch_with_glean(move |glean| metric.accumulate_samples_sync(glean, &[sample]))
361 }
362
363 /// **Test-only API (exported for testing purposes).**
364 /// Accumulates the provided signed samples in the metric.
365 ///
366 /// Use [`accumulate_samples`](Self::accumulate_samples)
367 #[doc(hidden)]
368 pub fn accumulate_samples_sync(&self, glean: &Glean, samples: &[i64]) {
369 if !self.should_record(glean) {
370 return;
371 }
372
373 let mut num_negative_samples = 0;
374 let mut num_too_long_samples = 0;
375 let max_sample_time = self.time_unit.as_nanos(MAX_SAMPLE_TIME);
376
377 glean.storage().record_with(glean, &self.meta, |old_value| {
378 let mut hist = match old_value {
379 Some(Metric::TimingDistribution(hist)) => hist,
380 _ => Histogram::functional(LOG_BASE, BUCKETS_PER_MAGNITUDE),
381 };
382
383 for &sample in samples.iter() {
384 if sample < 0 {
385 num_negative_samples += 1;
386 } else {
387 let mut sample = sample as u64;
388
389 // Check the range prior to converting the incoming unit to
390 // nanoseconds, so we can compare against the constant
391 // MAX_SAMPLE_TIME.
392 if sample == 0 {
393 sample = 1;
394 } else if sample > MAX_SAMPLE_TIME {
395 num_too_long_samples += 1;
396 sample = MAX_SAMPLE_TIME;
397 }
398
399 sample = self.time_unit.as_nanos(sample);
400
401 hist.accumulate(sample);
402 }
403 }
404
405 Metric::TimingDistribution(hist)
406 });
407
408 if num_negative_samples > 0 {
409 let msg = format!("Accumulated {} negative samples", num_negative_samples);
410 record_error(
411 glean,
412 &self.meta,
413 ErrorType::InvalidValue,
414 msg,
415 num_negative_samples,
416 );
417 }
418
419 if num_too_long_samples > 0 {
420 let msg = format!(
421 "{} samples are longer than the maximum of {}",
422 num_too_long_samples, max_sample_time
423 );
424 record_error(
425 glean,
426 &self.meta,
427 ErrorType::InvalidOverflow,
428 msg,
429 num_too_long_samples,
430 );
431 }
432 }
433
434 /// Accumulates the provided samples in the metric.
435 ///
436 /// # Arguments
437 ///
438 /// * `samples` - A list of samples recorded by the metric.
439 /// Samples must be in nanoseconds.
440 /// ## Notes
441 ///
442 /// Reports an [`ErrorType::InvalidOverflow`] error for samples that
443 /// are longer than `MAX_SAMPLE_TIME`.
444 pub fn accumulate_raw_samples_nanos(&self, samples: Vec<u64>) {
445 let metric = self.clone();
446 crate::launch_with_glean(move |glean| {
447 metric.accumulate_raw_samples_nanos_sync(glean, &samples)
448 })
449 }
450
451 /// Accumulates precisely one duration to the metric.
452 ///
453 /// Like `TimingDistribution::accumulate_single_sample`, but for use when the
454 /// duration is:
455 ///
456 /// * measured externally, or
457 /// * is in a unit different from the timing_distribution's internal TimeUnit.
458 ///
459 /// # Arguments
460 ///
461 /// * `duration` - The single duration to be recorded in the metric.
462 ///
463 /// ## Notes
464 ///
465 /// Reports an [`ErrorType::InvalidOverflow`] error if `duration` is longer than
466 /// `MAX_SAMPLE_TIME`.
467 ///
468 /// The API client is responsible for ensuring that `duration` is derived from a
469 /// monotonic clock source that behaves consistently over computer sleep across
470 /// the application's platforms. Otherwise the resulting data may not share the same
471 /// guarantees that other `timing_distribution` metrics' data do.
472 pub fn accumulate_raw_duration(&self, duration: Duration) {
473 let duration_ns = duration.as_nanos().try_into().unwrap_or(u64::MAX);
474 let metric = self.clone();
475 crate::launch_with_glean(move |glean| {
476 metric.accumulate_raw_samples_nanos_sync(glean, &[duration_ns])
477 })
478 }
479
480 /// **Test-only API (exported for testing purposes).**
481 ///
482 /// Accumulates the provided samples in the metric.
483 ///
484 /// Use [`accumulate_raw_samples_nanos`](Self::accumulate_raw_samples_nanos) instead.
485 #[doc(hidden)]
486 pub fn accumulate_raw_samples_nanos_sync(&self, glean: &Glean, samples: &[u64]) {
487 log::debug!("logging samples: {samples:?}");
488 if !self.should_record(glean) {
489 return;
490 }
491
492 let mut num_too_long_samples = 0;
493 let min_sample_time = self.time_unit.as_nanos(1);
494 let max_sample_time = self.time_unit.as_nanos(MAX_SAMPLE_TIME);
495
496 glean.storage().record_with(glean, &self.meta, |old_value| {
497 let mut hist = match old_value {
498 Some(Metric::TimingDistribution(hist)) => hist,
499 _ => Histogram::functional(LOG_BASE, BUCKETS_PER_MAGNITUDE),
500 };
501
502 for &sample in samples.iter() {
503 let mut sample = sample;
504
505 if sample < min_sample_time {
506 sample = min_sample_time;
507 } else if sample > max_sample_time {
508 num_too_long_samples += 1;
509 sample = max_sample_time;
510 }
511
512 // `sample` is in nanoseconds.
513 hist.accumulate(sample);
514 }
515
516 Metric::TimingDistribution(hist)
517 });
518
519 if num_too_long_samples > 0 {
520 let msg = format!(
521 "{} samples are longer than the maximum of {}",
522 num_too_long_samples, max_sample_time
523 );
524 record_error(
525 glean,
526 &self.meta,
527 ErrorType::InvalidOverflow,
528 msg,
529 num_too_long_samples,
530 );
531 }
532 }
533
534 /// Gets the currently stored value as an integer.
535 #[doc(hidden)]
536 pub fn get_value<'a, S: Into<Option<&'a str>>>(
537 &self,
538 glean: &Glean,
539 ping_name: S,
540 ) -> Option<DistributionData> {
541 let queried_ping_name = ping_name
542 .into()
543 .unwrap_or_else(|| &self.meta().inner.send_in_pings[0]);
544
545 match glean.storage().get_metric(self.meta(), queried_ping_name) {
546 Some(Metric::TimingDistribution(hist)) => Some(snapshot(&hist)),
547 _ => None,
548 }
549 }
550
551 /// **Exported for test purposes.**
552 ///
553 /// Gets the number of recorded errors for the given metric and error type.
554 ///
555 /// # Arguments
556 ///
557 /// * `error` - The type of error
558 ///
559 /// # Returns
560 ///
561 /// The number of errors reported.
562 pub fn test_get_num_recorded_errors(&self, error: ErrorType) -> i32 {
563 crate::block_on_dispatcher();
564
565 crate::core::with_glean(|glean| {
566 test_get_num_recorded_errors(glean, self.meta(), error).unwrap_or(0)
567 })
568 }
569
570 /// **Experimental:** Start a new histogram buffer associated with this timing distribution metric.
571 ///
572 /// A histogram buffer accumulates in-memory.
573 /// Data is recorded into the metric on drop.
574 pub fn start_buffer(&self) -> LocalTimingDistribution<'_> {
575 LocalTimingDistribution::new(self)
576 }
577
578 fn commit_histogram(&self, histogram: Histogram<Functional>, errors: usize) {
579 let metric = self.clone();
580 crate::launch_with_glean(move |glean| {
581 if errors > 0 {
582 let max_sample_time = metric.time_unit.as_nanos(MAX_SAMPLE_TIME);
583 let msg = format!(
584 "{} samples are longer than the maximum of {}",
585 errors, max_sample_time
586 );
587 record_error(
588 glean,
589 &metric.meta,
590 ErrorType::InvalidValue,
591 msg,
592 Some(errors as i32),
593 );
594 }
595
596 glean
597 .storage()
598 .record_with(glean, &metric.meta, move |old_value| {
599 let mut hist = match old_value {
600 Some(Metric::TimingDistribution(hist)) => hist,
601 _ => Histogram::functional(LOG_BASE, BUCKETS_PER_MAGNITUDE),
602 };
603
604 hist.merge(&histogram);
605 Metric::TimingDistribution(hist)
606 });
607 });
608 }
609}
610
611impl TestGetValue for TimingDistributionMetric {
612 type Output = DistributionData;
613
614 /// **Test-only API (exported for FFI purposes).**
615 ///
616 /// Gets the currently stored value as an integer.
617 ///
618 /// This doesn't clear the stored value.
619 ///
620 /// # Arguments
621 ///
622 /// * `ping_name` - the optional name of the ping to retrieve the metric
623 /// for. Defaults to the first value in `send_in_pings`.
624 ///
625 /// # Returns
626 ///
627 /// The stored value or `None` if nothing stored.
628 fn test_get_value(&self, ping_name: Option<String>) -> Option<DistributionData> {
629 crate::block_on_dispatcher();
630 crate::core::with_glean(|glean| self.get_value(glean, ping_name.as_deref()))
631 }
632}
633
634/// **Experimental:** A histogram buffer associated with a specific instance of a [`TimingDistributionMetric`].
635///
636/// Accumulation happens in-memory.
637/// Data is merged into the metric on [`Drop::drop`].
638#[derive(Debug)]
639pub struct LocalTimingDistribution<'a> {
640 histogram: Histogram<Functional>,
641 metric: &'a TimingDistributionMetric,
642 errors: usize,
643}
644
645impl<'a> LocalTimingDistribution<'a> {
646 /// Create a new histogram buffer referencing the timing distribution it will record into.
647 fn new(metric: &'a TimingDistributionMetric) -> Self {
648 let histogram = Histogram::functional(LOG_BASE, BUCKETS_PER_MAGNITUDE);
649 Self {
650 histogram,
651 metric,
652 errors: 0,
653 }
654 }
655
656 /// Accumulates one sample into the histogram.
657 ///
658 /// The provided sample must be in the "unit" declared by the instance of the metric type
659 /// (e.g. if the instance this method was called on is using [`crate::TimeUnit::Second`], then
660 /// `sample` is assumed to be in seconds).
661 ///
662 /// Accumulation happens in-memory only.
663 pub fn accumulate(&mut self, sample: u64) {
664 // Check the range prior to converting the incoming unit to
665 // nanoseconds, so we can compare against the constant
666 // MAX_SAMPLE_TIME.
667 let sample = if sample == 0 {
668 1
669 } else if sample > MAX_SAMPLE_TIME {
670 self.errors += 1;
671 MAX_SAMPLE_TIME
672 } else {
673 sample
674 };
675
676 let sample = self.metric.time_unit.as_nanos(sample);
677 self.histogram.accumulate(sample)
678 }
679
680 /// Abandon this histogram buffer and don't commit accumulated data.
681 pub fn abandon(mut self) {
682 self.histogram.clear();
683 }
684}
685
686impl Drop for LocalTimingDistribution<'_> {
687 fn drop(&mut self) {
688 if self.histogram.is_empty() {
689 return;
690 }
691
692 // We want to move that value.
693 // A `0/0` histogram doesn't allocate.
694 let buffer = mem::replace(&mut self.histogram, Histogram::functional(0.0, 0.0));
695 self.metric.commit_histogram(buffer, self.errors);
696 }
697}
698
699#[cfg(test)]
700mod test {
701 use super::*;
702
703 #[test]
704 fn can_snapshot() {
705 use serde_json::json;
706
707 let mut hist = Histogram::functional(2.0, 8.0);
708
709 for i in 1..=10 {
710 hist.accumulate(i);
711 }
712
713 let snap = snapshot(&hist);
714
715 let expected_json = json!({
716 "sum": 55,
717 "values": {
718 "1": 1,
719 "2": 1,
720 "3": 1,
721 "4": 1,
722 "5": 1,
723 "6": 1,
724 "7": 1,
725 "8": 1,
726 "9": 1,
727 "10": 1,
728 },
729 });
730
731 assert_eq!(expected_json, json!(snap));
732 }
733
734 #[test]
735 fn can_snapshot_sparse() {
736 use serde_json::json;
737
738 let mut hist = Histogram::functional(2.0, 8.0);
739
740 hist.accumulate(1024);
741 hist.accumulate(1024);
742 hist.accumulate(1116);
743 hist.accumulate(1448);
744
745 let snap = snapshot(&hist);
746
747 let expected_json = json!({
748 "sum": 4612,
749 "values": {
750 "1024": 2,
751 "1116": 1,
752 "1448": 1,
753 },
754 });
755
756 assert_eq!(expected_json, json!(snap));
757 }
758}