glean_core/metrics/
memory_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::{Functional, Histogram};
11use crate::metrics::memory_unit::MemoryUnit;
12use crate::metrics::{DistributionData, Metric, MetricType};
13use crate::storage::StorageManager;
14use crate::Glean;
15use crate::{CommonMetricData, TestGetValue};
16
17// The base of the logarithm used to determine bucketing
18const LOG_BASE: f64 = 2.0;
19
20// The buckets per each order of magnitude of the logarithm.
21const BUCKETS_PER_MAGNITUDE: f64 = 16.0;
22
23// Set a maximum recordable value of 1 terabyte so the buckets aren't
24// completely unbounded.
25const MAX_BYTES: u64 = 1 << 40;
26
27/// A memory distribution metric.
28///
29/// Memory distributions are used to accumulate and store memory sizes.
30#[derive(Clone, Debug)]
31pub struct MemoryDistributionMetric {
32    meta: Arc<CommonMetricDataInternal>,
33    memory_unit: MemoryUnit,
34}
35
36/// Create a snapshot of the histogram.
37///
38/// The snapshot can be serialized into the payload format.
39pub(crate) fn snapshot(hist: &Histogram<Functional>) -> DistributionData {
40    DistributionData {
41        // **Caution**: This cannot use `Histogram::snapshot_values` and needs to use the more
42        // specialized snapshot function.
43        values: hist
44            .snapshot()
45            .iter()
46            .map(|(&k, &v)| (k as i64, v as i64))
47            .collect(),
48        sum: hist.sum() as i64,
49        count: hist.count() as i64,
50    }
51}
52
53impl MetricType for MemoryDistributionMetric {
54    fn meta(&self) -> &CommonMetricDataInternal {
55        &self.meta
56    }
57
58    fn with_name(&self, name: String) -> Self {
59        let mut meta = (*self.meta).clone();
60        meta.inner.name = name;
61        Self {
62            meta: Arc::new(meta),
63            memory_unit: self.memory_unit,
64        }
65    }
66
67    fn with_dynamic_label(&self, label: DynamicLabelType) -> Self {
68        let mut meta = (*self.meta).clone();
69        meta.inner.dynamic_label = Some(label);
70        Self {
71            meta: Arc::new(meta),
72            memory_unit: self.memory_unit,
73        }
74    }
75}
76
77// IMPORTANT:
78//
79// When changing this implementation, make sure all the operations are
80// also declared in the related trait in `../traits/`.
81impl MemoryDistributionMetric {
82    /// Creates a new memory distribution metric.
83    pub fn new(meta: CommonMetricData, memory_unit: MemoryUnit) -> Self {
84        Self {
85            meta: Arc::new(meta.into()),
86            memory_unit,
87        }
88    }
89
90    /// Accumulates the provided sample in the metric.
91    ///
92    /// # Arguments
93    ///
94    /// * `sample` - The sample to be recorded by the metric. The sample is assumed to be in the
95    ///   configured memory unit of the metric.
96    ///
97    /// ## Notes
98    ///
99    /// Values bigger than 1 Terabyte (2<sup>40</sup> bytes) are truncated
100    /// and an [`ErrorType::InvalidValue`] error is recorded.
101    pub fn accumulate(&self, sample: i64) {
102        let metric = self.clone();
103        crate::launch_with_glean(move |glean| metric.accumulate_sync(glean, sample))
104    }
105
106    /// Accumulates the provided sample in the metric synchronously.
107    ///
108    /// See [`accumulate`](Self::accumulate) for details.
109    #[doc(hidden)]
110    pub fn accumulate_sync(&self, glean: &Glean, sample: i64) {
111        if !self.should_record(glean) {
112            return;
113        }
114
115        if sample < 0 {
116            record_error(
117                glean,
118                &self.meta,
119                ErrorType::InvalidValue,
120                "Accumulated a negative sample",
121                None,
122            );
123            return;
124        }
125
126        let mut sample = self.memory_unit.as_bytes(sample as u64);
127
128        if sample > MAX_BYTES {
129            let msg = "Sample is bigger than 1 terabyte";
130            record_error(glean, &self.meta, ErrorType::InvalidValue, msg, None);
131            sample = MAX_BYTES;
132        }
133
134        // Let's be defensive here:
135        // The uploader tries to store some memory distribution metrics,
136        // but in tests that storage might be gone already.
137        // Let's just ignore those.
138        // We do the same for counters and timing distributions.
139        // This should never happen in real app usage.
140        if let Some(storage) = glean.storage_opt() {
141            storage.record_with(glean, &self.meta, |old_value| match old_value {
142                Some(Metric::MemoryDistribution(mut hist)) => {
143                    hist.accumulate(sample);
144                    Metric::MemoryDistribution(hist)
145                }
146                _ => {
147                    let mut hist = Histogram::functional(LOG_BASE, BUCKETS_PER_MAGNITUDE);
148                    hist.accumulate(sample);
149                    Metric::MemoryDistribution(hist)
150                }
151            });
152        } else {
153            log::warn!(
154                "Couldn't get storage. Can't record memory distribution '{}'.",
155                self.meta.base_identifier()
156            );
157        }
158    }
159
160    /// Accumulates the provided signed samples in the metric.
161    ///
162    /// This is required so that the platform-specific code can provide us with
163    /// 64 bit signed integers if no `u64` comparable type is available. This
164    /// will take care of filtering and reporting errors for any provided negative
165    /// sample.
166    ///
167    /// Please note that this assumes that the provided samples are already in
168    /// the "unit" declared by the instance of the metric type (e.g. if the the
169    /// instance this method was called on is using [`MemoryUnit::Kilobyte`], then
170    /// `samples` are assumed to be in that unit).
171    ///
172    /// # Arguments
173    ///
174    /// * `samples` - The vector holding the samples to be recorded by the metric.
175    ///
176    /// ## Notes
177    ///
178    /// Discards any negative value in `samples` and report an [`ErrorType::InvalidValue`]
179    /// for each of them.
180    ///
181    /// Values bigger than 1 Terabyte (2<sup>40</sup> bytes) are truncated
182    /// and an [`ErrorType::InvalidValue`] error is recorded.
183    pub fn accumulate_samples(&self, samples: Vec<i64>) {
184        let metric = self.clone();
185        crate::launch_with_glean(move |glean| metric.accumulate_samples_sync(glean, samples))
186    }
187
188    /// Accumulates the provided signed samples in the metric synchronously.
189    ///
190    /// See [`accumulate_samples`](Self::accumulate_samples) for details.
191    #[doc(hidden)]
192    pub fn accumulate_samples_sync(&self, glean: &Glean, samples: Vec<i64>) {
193        if !self.should_record(glean) {
194            return;
195        }
196
197        let mut num_negative_samples = 0;
198        let mut num_too_log_samples = 0;
199
200        glean.storage().record_with(glean, &self.meta, |old_value| {
201            let mut hist = match old_value {
202                Some(Metric::MemoryDistribution(hist)) => hist,
203                _ => Histogram::functional(LOG_BASE, BUCKETS_PER_MAGNITUDE),
204            };
205
206            for &sample in samples.iter() {
207                if sample < 0 {
208                    num_negative_samples += 1;
209                } else {
210                    let sample = sample as u64;
211                    let mut sample = self.memory_unit.as_bytes(sample);
212                    if sample > MAX_BYTES {
213                        num_too_log_samples += 1;
214                        sample = MAX_BYTES;
215                    }
216
217                    hist.accumulate(sample);
218                }
219            }
220            Metric::MemoryDistribution(hist)
221        });
222
223        if num_negative_samples > 0 {
224            let msg = format!("Accumulated {} negative samples", num_negative_samples);
225            record_error(
226                glean,
227                &self.meta,
228                ErrorType::InvalidValue,
229                msg,
230                num_negative_samples,
231            );
232        }
233
234        if num_too_log_samples > 0 {
235            let msg = format!(
236                "Accumulated {} samples larger than 1TB",
237                num_too_log_samples
238            );
239            record_error(
240                glean,
241                &self.meta,
242                ErrorType::InvalidValue,
243                msg,
244                num_too_log_samples,
245            );
246        }
247    }
248
249    /// Gets the currently stored value synchronously.
250    #[doc(hidden)]
251    pub fn get_value<'a, S: Into<Option<&'a str>>>(
252        &self,
253        glean: &Glean,
254        ping_name: S,
255    ) -> Option<DistributionData> {
256        let queried_ping_name = ping_name
257            .into()
258            .unwrap_or_else(|| &self.meta().inner.send_in_pings[0]);
259
260        match StorageManager.snapshot_metric_for_test(
261            glean.storage(),
262            queried_ping_name,
263            &self.meta.identifier(glean),
264            self.meta.inner.lifetime,
265        ) {
266            Some(Metric::MemoryDistribution(hist)) => Some(snapshot(&hist)),
267            _ => None,
268        }
269    }
270
271    /// **Exported for test purposes.**
272    ///
273    /// Gets the number of recorded errors for the given metric and error type.
274    ///
275    /// # Arguments
276    ///
277    /// * `error` - The type of error
278    ///
279    /// # Returns
280    ///
281    /// The number of errors reported.
282    pub fn test_get_num_recorded_errors(&self, error: ErrorType) -> i32 {
283        crate::block_on_dispatcher();
284
285        crate::core::with_glean(|glean| {
286            test_get_num_recorded_errors(glean, self.meta(), error).unwrap_or(0)
287        })
288    }
289
290    /// **Experimental:** Start a new histogram buffer associated with this memory distribution metric.
291    ///
292    /// A histogram buffer accumulates in-memory.
293    /// Data is recorded into the metric on drop.
294    pub fn start_buffer(&self) -> LocalMemoryDistribution<'_> {
295        LocalMemoryDistribution::new(self)
296    }
297
298    fn commit_histogram(&self, histogram: Histogram<Functional>, errors: usize) {
299        let metric = self.clone();
300        crate::launch_with_glean(move |glean| {
301            if errors > 0 {
302                let msg = format!("Accumulated {} samples larger than 1TB", errors);
303                record_error(
304                    glean,
305                    &metric.meta,
306                    ErrorType::InvalidValue,
307                    msg,
308                    Some(errors as i32),
309                );
310            }
311
312            glean
313                .storage()
314                .record_with(glean, &metric.meta, move |old_value| {
315                    let mut hist = match old_value {
316                        Some(Metric::MemoryDistribution(hist)) => hist,
317                        _ => Histogram::functional(LOG_BASE, BUCKETS_PER_MAGNITUDE),
318                    };
319
320                    hist.merge(&histogram);
321                    Metric::MemoryDistribution(hist)
322                });
323        });
324    }
325}
326
327impl TestGetValue<DistributionData> for MemoryDistributionMetric {
328    /// **Test-only API (exported for FFI purposes).**
329    ///
330    /// Gets the currently stored value.
331    ///
332    /// This doesn't clear the stored value.
333    ///
334    /// # Arguments
335    ///
336    /// * `ping_name` - the optional name of the ping to retrieve the metric
337    ///                 for. Defaults to the first value in `send_in_pings`.
338    ///
339    /// # Returns
340    ///
341    /// The stored value or `None` if nothing stored.
342    fn test_get_value(&self, ping_name: Option<String>) -> Option<DistributionData> {
343        crate::block_on_dispatcher();
344        crate::core::with_glean(|glean| self.get_value(glean, ping_name.as_deref()))
345    }
346}
347
348/// **Experimental:** A histogram buffer associated with a specific instance of a [`MemoryDistributionMetric`].
349///
350/// Accumulation happens in-memory.
351/// Data is merged into the metric on [`Drop::drop`].
352#[derive(Debug)]
353pub struct LocalMemoryDistribution<'a> {
354    histogram: Histogram<Functional>,
355    metric: &'a MemoryDistributionMetric,
356    errors: usize,
357}
358
359impl<'a> LocalMemoryDistribution<'a> {
360    /// Create a new histogram buffer referencing the memory distribution it will record into.
361    fn new(metric: &'a MemoryDistributionMetric) -> Self {
362        let histogram = Histogram::functional(LOG_BASE, BUCKETS_PER_MAGNITUDE);
363        Self {
364            histogram,
365            metric,
366            errors: 0,
367        }
368    }
369
370    /// Accumulates one sample into the histogram.
371    ///
372    /// The provided sample must be in the "unit" declared by the instance of the metric type
373    /// (e.g. if the instance this method was called on is using [`crate::MemoryUnit::Kilobyte`], then
374    /// `sample` is assumed to be in kilobytes).
375    ///
376    /// Accumulation happens in-memory only.
377    pub fn accumulate(&mut self, sample: u64) {
378        let mut sample = self.metric.memory_unit.as_bytes(sample);
379        if sample > MAX_BYTES {
380            self.errors += 1;
381            sample = MAX_BYTES;
382        }
383        self.histogram.accumulate(sample)
384    }
385
386    /// Abandon this histogram buffer and don't commit accumulated data.
387    pub fn abandon(mut self) {
388        // Replace any recordings with an empty histogram.
389        self.histogram.clear();
390    }
391}
392
393impl Drop for LocalMemoryDistribution<'_> {
394    fn drop(&mut self) {
395        if self.histogram.is_empty() {
396            return;
397        }
398
399        // We want to move that value.
400        // A `0/0` histogram doesn't allocate.
401        let buffer = mem::replace(&mut self.histogram, Histogram::functional(0.0, 0.0));
402        self.metric.commit_histogram(buffer, self.errors);
403    }
404}