glean_core/metrics/
custom_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::mem;
6use std::sync::Arc;
7
8use crate::common_metric_data::{CommonMetricDataInternal, DynamicLabelType};
9use crate::error_recording::{record_error, test_get_num_recorded_errors, ErrorType};
10use crate::histogram::{Bucketing, Histogram, HistogramType, LinearOrExponential};
11use crate::metrics::{DistributionData, Metric, MetricType};
12use crate::storage::StorageManager;
13use crate::Glean;
14use crate::{CommonMetricData, TestGetValue};
15
16/// A custom distribution metric.
17#[derive(Clone, Debug)]
18pub struct CustomDistributionMetric {
19    meta: Arc<CommonMetricDataInternal>,
20    range_min: u64,
21    range_max: u64,
22    bucket_count: u64,
23    histogram_type: HistogramType,
24}
25
26/// Create a snapshot of the histogram.
27///
28/// The snapshot can be serialized into the payload format.
29pub(crate) fn snapshot<B: Bucketing>(hist: &Histogram<B>) -> DistributionData {
30    DistributionData {
31        values: hist
32            .snapshot_values()
33            .into_iter()
34            .map(|(k, v)| (k as i64, v as i64))
35            .collect(),
36        sum: hist.sum() as i64,
37        count: hist.count() as i64,
38    }
39}
40
41impl MetricType for CustomDistributionMetric {
42    fn meta(&self) -> &CommonMetricDataInternal {
43        &self.meta
44    }
45
46    fn with_name(&self, name: String) -> Self {
47        let mut meta = (*self.meta).clone();
48        meta.inner.name = name;
49        Self {
50            meta: Arc::new(meta),
51            range_min: self.range_min,
52            range_max: self.range_max,
53            bucket_count: self.bucket_count,
54            histogram_type: self.histogram_type,
55        }
56    }
57
58    fn with_dynamic_label(&self, label: DynamicLabelType) -> Self {
59        let mut meta = (*self.meta).clone();
60        meta.inner.dynamic_label = Some(label);
61        Self {
62            meta: Arc::new(meta),
63            range_min: self.range_min,
64            range_max: self.range_max,
65            bucket_count: self.bucket_count,
66            histogram_type: self.histogram_type,
67        }
68    }
69}
70
71// IMPORTANT:
72//
73// When changing this implementation, make sure all the operations are
74// also declared in the related trait in `../traits/`.
75impl CustomDistributionMetric {
76    /// Creates a new memory distribution metric.
77    pub fn new(
78        meta: CommonMetricData,
79        range_min: i64,
80        range_max: i64,
81        bucket_count: i64,
82        histogram_type: HistogramType,
83    ) -> Self {
84        Self {
85            meta: Arc::new(meta.into()),
86            range_min: range_min as u64,
87            range_max: range_max as u64,
88            bucket_count: bucket_count as u64,
89            histogram_type,
90        }
91    }
92
93    /// Accumulates the provided signed samples in the metric.
94    ///
95    /// This is required so that the platform-specific code can provide us with
96    /// 64 bit signed integers if no `u64` comparable type is available. This
97    /// will take care of filtering and reporting errors for any provided negative
98    /// sample.
99    ///
100    /// # Arguments
101    ///
102    /// - `samples` - The vector holding the samples to be recorded by the metric.
103    ///
104    /// ## Notes
105    ///
106    /// Discards any negative value in `samples` and report an [`ErrorType::InvalidValue`]
107    /// for each of them.
108    pub fn accumulate_samples(&self, samples: Vec<i64>) {
109        let metric = self.clone();
110        crate::launch_with_glean(move |glean| metric.accumulate_samples_sync(glean, &samples))
111    }
112
113    /// Accumulates precisely one signed sample and appends it to the metric.
114    ///
115    /// Signed is required so that the platform-specific code can provide us with a
116    /// 64 bit signed integer if no `u64` comparable type is available. This
117    /// will take care of filtering and reporting errors.
118    ///
119    /// # Arguments
120    ///
121    /// - `sample` - The singular sample to be recorded by the metric.
122    ///
123    /// ## Notes
124    ///
125    /// Discards any negative value of `sample` and reports an
126    /// [`ErrorType::InvalidValue`](crate::ErrorType::InvalidValue).
127    pub fn accumulate_single_sample(&self, sample: i64) {
128        let metric = self.clone();
129        crate::launch_with_glean(move |glean| metric.accumulate_samples_sync(glean, &[sample]))
130    }
131
132    /// Accumulates the provided sample in the metric synchronously.
133    ///
134    /// See [`accumulate_samples`](Self::accumulate_samples) for details.
135    #[doc(hidden)]
136    pub fn accumulate_samples_sync(&self, glean: &Glean, samples: &[i64]) {
137        if !self.should_record(glean) {
138            return;
139        }
140
141        let mut num_negative_samples = 0;
142
143        // Generic accumulation function to handle the different histogram types and count negative
144        // samples.
145        fn accumulate<B: Bucketing, F>(
146            samples: &[i64],
147            mut hist: Histogram<B>,
148            metric: F,
149        ) -> (i32, Metric)
150        where
151            F: Fn(Histogram<B>) -> Metric,
152        {
153            let mut num_negative_samples = 0;
154            for &sample in samples.iter() {
155                if sample < 0 {
156                    num_negative_samples += 1;
157                } else {
158                    let sample = sample as u64;
159                    hist.accumulate(sample);
160                }
161            }
162            (num_negative_samples, metric(hist))
163        }
164
165        glean.storage().record_with(glean, &self.meta, |old_value| {
166            let (num_negative, hist) = match self.histogram_type {
167                HistogramType::Linear => {
168                    let hist = if let Some(Metric::CustomDistributionLinear(hist)) = old_value {
169                        hist
170                    } else {
171                        Histogram::linear(
172                            self.range_min,
173                            self.range_max,
174                            self.bucket_count as usize,
175                        )
176                    };
177                    accumulate(samples, hist, Metric::CustomDistributionLinear)
178                }
179                HistogramType::Exponential => {
180                    let hist = if let Some(Metric::CustomDistributionExponential(hist)) = old_value
181                    {
182                        hist
183                    } else {
184                        Histogram::exponential(
185                            self.range_min,
186                            self.range_max,
187                            self.bucket_count as usize,
188                        )
189                    };
190                    accumulate(samples, hist, Metric::CustomDistributionExponential)
191                }
192            };
193
194            num_negative_samples = num_negative;
195            hist
196        });
197
198        if num_negative_samples > 0 {
199            let msg = format!("Accumulated {} negative samples", num_negative_samples);
200            record_error(
201                glean,
202                &self.meta,
203                ErrorType::InvalidValue,
204                msg,
205                num_negative_samples,
206            );
207        }
208    }
209
210    /// Gets the currently stored histogram.
211    #[doc(hidden)]
212    pub fn get_value<'a, S: Into<Option<&'a str>>>(
213        &self,
214        glean: &Glean,
215        ping_name: S,
216    ) -> Option<DistributionData> {
217        let queried_ping_name = ping_name
218            .into()
219            .unwrap_or_else(|| &self.meta().inner.send_in_pings[0]);
220
221        match StorageManager.snapshot_metric_for_test(
222            glean.storage(),
223            queried_ping_name,
224            &self.meta.identifier(glean),
225            self.meta.inner.lifetime,
226        ) {
227            // Boxing the value, in order to return either of the possible buckets
228            Some(Metric::CustomDistributionExponential(hist)) => Some(snapshot(&hist)),
229            Some(Metric::CustomDistributionLinear(hist)) => Some(snapshot(&hist)),
230            _ => None,
231        }
232    }
233
234    /// **Exported for test purposes.**
235    ///
236    /// Gets the number of recorded errors for the given metric and error type.
237    ///
238    /// # Arguments
239    ///
240    /// * `error` - The type of error
241    ///
242    /// # Returns
243    ///
244    /// The number of errors reported.
245    pub fn test_get_num_recorded_errors(&self, error: ErrorType) -> i32 {
246        crate::block_on_dispatcher();
247
248        crate::core::with_glean(|glean| {
249            test_get_num_recorded_errors(glean, self.meta(), error).unwrap_or(0)
250        })
251    }
252
253    /// **Experimental:** Start a new histogram buffer associated with this custom distribution metric.
254    ///
255    /// A histogram buffer accumulates in-memory.
256    /// Data is recorded into the metric on drop.
257    pub fn start_buffer(&self) -> LocalCustomDistribution<'_> {
258        LocalCustomDistribution::new(self)
259    }
260
261    fn commit_histogram(&self, histogram: Histogram<LinearOrExponential>) {
262        let metric = self.clone();
263        crate::launch_with_glean(move |glean| {
264            glean
265                .storage()
266                .record_with(glean, &metric.meta, move |old_value| {
267                    match metric.histogram_type {
268                        HistogramType::Linear => {
269                            let mut hist =
270                                if let Some(Metric::CustomDistributionLinear(hist)) = old_value {
271                                    hist
272                                } else {
273                                    Histogram::linear(
274                                        metric.range_min,
275                                        metric.range_max,
276                                        metric.bucket_count as usize,
277                                    )
278                                };
279
280                            hist._merge(&histogram);
281                            Metric::CustomDistributionLinear(hist)
282                        }
283                        HistogramType::Exponential => {
284                            let mut hist = if let Some(Metric::CustomDistributionExponential(
285                                hist,
286                            )) = old_value
287                            {
288                                hist
289                            } else {
290                                Histogram::exponential(
291                                    metric.range_min,
292                                    metric.range_max,
293                                    metric.bucket_count as usize,
294                                )
295                            };
296
297                            hist._merge(&histogram);
298                            Metric::CustomDistributionExponential(hist)
299                        }
300                    }
301                });
302        });
303    }
304}
305
306impl TestGetValue<DistributionData> for CustomDistributionMetric {
307    /// **Test-only API (exported for FFI purposes).**
308    ///
309    /// Gets the currently stored value as an integer.
310    ///
311    /// This doesn't clear the stored value.
312    ///
313    /// # Arguments
314    ///
315    /// * `ping_name` - the optional name of the ping to retrieve the metric
316    ///                 for. Defaults to the first value in `send_in_pings`.
317    ///
318    /// # Returns
319    ///
320    /// The stored value or `None` if nothing stored.
321    fn test_get_value(&self, ping_name: Option<String>) -> Option<DistributionData> {
322        crate::block_on_dispatcher();
323        crate::core::with_glean(|glean| self.get_value(glean, ping_name.as_deref()))
324    }
325}
326
327/// **Experimental:** A histogram buffer associated with a specific instance of a [`CustomDistributionMetric`].
328///
329/// Accumulation happens in-memory.
330/// Data is merged into the metric on [`Drop::drop`].
331pub struct LocalCustomDistribution<'a> {
332    histogram: Histogram<LinearOrExponential>,
333    metric: &'a CustomDistributionMetric,
334}
335
336impl<'a> LocalCustomDistribution<'a> {
337    /// Create a new histogram buffer referencing the custom distribution it will record into.
338    fn new(metric: &'a CustomDistributionMetric) -> Self {
339        let histogram = match metric.histogram_type {
340            HistogramType::Linear => Histogram::<LinearOrExponential>::_linear(
341                metric.range_min,
342                metric.range_max,
343                metric.bucket_count as usize,
344            ),
345            HistogramType::Exponential => Histogram::<LinearOrExponential>::_exponential(
346                metric.range_min,
347                metric.range_max,
348                metric.bucket_count as usize,
349            ),
350        };
351        Self { histogram, metric }
352    }
353
354    /// Accumulates one sample into the histogram.
355    ///
356    /// The provided sample must be in the "unit" declared by the instance of the metric type
357    /// (e.g. if the instance this method was called on is using [`crate::TimeUnit::Second`], then
358    /// `sample` is assumed to be in seconds).
359    ///
360    /// Accumulation happens in-memory only.
361    pub fn accumulate(&mut self, sample: u64) {
362        self.histogram.accumulate(sample)
363    }
364
365    /// Abandon this histogram buffer and don't commit accumulated data.
366    pub fn abandon(mut self) {
367        self.histogram.clear();
368    }
369}
370
371impl Drop for LocalCustomDistribution<'_> {
372    fn drop(&mut self) {
373        if self.histogram.is_empty() {
374            return;
375        }
376
377        // We want to move that value.
378        // A `0/0` histogram doesn't allocate.
379        let empty = Histogram::_linear(0, 0, 0);
380        let buffer = mem::replace(&mut self.histogram, empty);
381        self.metric.commit_histogram(buffer);
382    }
383}