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