diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /toolkit/components/glean/api/src/private | |
parent | Initial commit. (diff) | |
download | firefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz firefox-26a029d407be480d791972afb5975cf62c9360a6.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/components/glean/api/src/private')
21 files changed, 4161 insertions, 0 deletions
diff --git a/toolkit/components/glean/api/src/private/boolean.rs b/toolkit/components/glean/api/src/private/boolean.rs new file mode 100644 index 0000000000..ea9ba6b6fe --- /dev/null +++ b/toolkit/components/glean/api/src/private/boolean.rs @@ -0,0 +1,155 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use inherent::inherent; +use std::sync::Arc; + +use glean::traits::Boolean; + +use super::CommonMetricData; + +use crate::ipc::need_ipc; +use crate::private::MetricId; + +/// A boolean metric. +/// +/// Records a simple true or false value. +#[derive(Clone)] +pub enum BooleanMetric { + Parent(Arc<glean::private::BooleanMetric>), + Child(BooleanMetricIpc), +} +#[derive(Clone, Debug)] +pub struct BooleanMetricIpc; + +impl BooleanMetric { + /// Create a new boolean metric. + pub fn new(_id: MetricId, meta: CommonMetricData) -> Self { + if need_ipc() { + BooleanMetric::Child(BooleanMetricIpc) + } else { + BooleanMetric::Parent(Arc::new(glean::private::BooleanMetric::new(meta))) + } + } + + #[cfg(test)] + pub(crate) fn child_metric(&self) -> Self { + match self { + BooleanMetric::Parent(_) => BooleanMetric::Child(BooleanMetricIpc), + BooleanMetric::Child(_) => panic!("Can't get a child metric from a child metric"), + } + } +} + +#[inherent] +impl Boolean for BooleanMetric { + /// Set to the specified boolean value. + /// + /// ## Arguments + /// + /// * `value` - the value to set. + pub fn set(&self, value: bool) { + match self { + BooleanMetric::Parent(p) => { + p.set(value); + } + BooleanMetric::Child(_) => { + log::error!("Unable to set boolean metric in non-main process. This operation will be ignored."); + // If we're in automation we can panic so the instrumentor knows they've gone wrong. + // This is a deliberate violation of Glean's "metric APIs must not throw" design. + assert!(!crate::ipc::is_in_automation(), "Attempted to set boolean metric in non-main process, which is forbidden. This panics in automation."); + // TODO: Record an error. + } + } + } + + /// **Test-only API.** + /// + /// Get the currently stored value as a boolean. + /// This doesn't clear the stored value. + /// + /// ## Arguments + /// + /// * `ping_name` - the storage name to look into. + /// + /// ## Return value + /// + /// Returns the stored value or `None` if nothing stored. + pub fn test_get_value<'a, S: Into<Option<&'a str>>>(&self, ping_name: S) -> Option<bool> { + let ping_name = ping_name.into().map(|s| s.to_string()); + match self { + BooleanMetric::Parent(p) => p.test_get_value(ping_name), + BooleanMetric::Child(_) => { + panic!("Cannot get test value for boolean metric in non-main process!",) + } + } + } + + /// **Exported for test purposes.** + /// + /// Gets the number of recorded errors for the given metric and error type. + /// + /// # Arguments + /// + /// * `error` - The type of error + /// * `ping_name` - represents the optional name of the ping to retrieve the + /// metric for. Defaults to the first value in `send_in_pings`. + /// + /// # Returns + /// + /// The number of errors reported. + pub fn test_get_num_recorded_errors(&self, error: glean::ErrorType) -> i32 { + match self { + BooleanMetric::Parent(p) => p.test_get_num_recorded_errors(error), + BooleanMetric::Child(_) => panic!( + "Cannot get the number of recorded errors for boolean metric in non-main process!" + ), + } + } +} + +#[cfg(test)] +mod test { + use crate::{common_test::*, ipc, metrics}; + + #[test] + fn sets_boolean_value() { + let _lock = lock_test(); + + let metric = &metrics::test_only_ipc::a_bool; + metric.set(true); + + assert!(metric.test_get_value("store1").unwrap()); + } + + #[test] + fn boolean_ipc() { + // BooleanMetric doesn't support IPC. + let _lock = lock_test(); + + let parent_metric = &metrics::test_only_ipc::a_bool; + + parent_metric.set(false); + + { + let child_metric = parent_metric.child_metric(); + + // scope for need_ipc RAII + let _raii = ipc::test_set_need_ipc(true); + + // Instrumentation calls do not panic. + child_metric.set(true); + + // (They also shouldn't do anything, + // but that's not something we can inspect in this test) + } + + assert!(ipc::replay_from_buf(&ipc::take_buf().unwrap()).is_ok()); + + assert!( + false == parent_metric.test_get_value("store1").unwrap(), + "Boolean metrics should only work in the parent process" + ); + } +} diff --git a/toolkit/components/glean/api/src/private/counter.rs b/toolkit/components/glean/api/src/private/counter.rs new file mode 100644 index 0000000000..67ad3204d6 --- /dev/null +++ b/toolkit/components/glean/api/src/private/counter.rs @@ -0,0 +1,204 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use inherent::inherent; +use std::sync::Arc; + +use glean::traits::Counter; + +use super::{CommonMetricData, MetricId}; +use crate::ipc::{need_ipc, with_ipc_payload}; + +/// A counter metric. +/// +/// Used to count things. +/// The value can only be incremented, not decremented. +#[derive(Clone)] +pub enum CounterMetric { + Parent { + /// The metric's ID. + /// + /// **TEST-ONLY** - Do not use unless gated with `#[cfg(test)]`. + id: MetricId, + inner: Arc<glean::private::CounterMetric>, + }, + Child(CounterMetricIpc), +} +#[derive(Clone, Debug)] +pub struct CounterMetricIpc(MetricId); + +impl CounterMetric { + /// Create a new counter metric. + pub fn new(id: MetricId, meta: CommonMetricData) -> Self { + if need_ipc() { + CounterMetric::Child(CounterMetricIpc(id)) + } else { + let inner = Arc::new(glean::private::CounterMetric::new(meta)); + CounterMetric::Parent { id, inner } + } + } + + /// Special-purpose ctor for use by codegen. + /// Only useful if the metric is: + /// * not disabled + /// * lifetime: ping + /// * and is sent in precisely one ping. + pub fn codegen_new(id: u32, category: &str, name: &str, ping: &str) -> Self { + if need_ipc() { + CounterMetric::Child(CounterMetricIpc(id.into())) + } else { + let inner = Arc::new(glean::private::CounterMetric::new(CommonMetricData { + category: category.into(), + name: name.into(), + send_in_pings: vec![ping.into()], + ..Default::default() + })); + CounterMetric::Parent { + id: id.into(), + inner, + } + } + } + + #[cfg(test)] + pub(crate) fn metric_id(&self) -> MetricId { + match self { + CounterMetric::Parent { id, .. } => *id, + CounterMetric::Child(c) => c.0, + } + } + + #[cfg(test)] + pub(crate) fn child_metric(&self) -> Self { + match self { + CounterMetric::Parent { id, .. } => CounterMetric::Child(CounterMetricIpc(*id)), + CounterMetric::Child(_) => panic!("Can't get a child metric from a child metric"), + } + } +} + +#[inherent] +impl Counter for CounterMetric { + /// Increase the counter by `amount`. + /// + /// ## Arguments + /// + /// * `amount` - The amount to increase by. Should be positive. + /// + /// ## Notes + /// + /// Logs an error if the `amount` is 0 or negative. + pub fn add(&self, amount: i32) { + match self { + CounterMetric::Parent { inner, .. } => { + inner.add(amount); + } + CounterMetric::Child(c) => { + with_ipc_payload(move |payload| { + if let Some(v) = payload.counters.get_mut(&c.0) { + *v += amount; + } else { + payload.counters.insert(c.0, amount); + } + }); + } + } + } + + /// **Test-only API.** + /// + /// Get the currently stored value as an integer. + /// This doesn't clear the stored value. + /// + /// ## Arguments + /// + /// * `ping_name` - the storage name to look into. + /// + /// ## Return value + /// + /// Returns the stored value or `None` if nothing stored. + pub fn test_get_value<'a, S: Into<Option<&'a str>>>(&self, ping_name: S) -> Option<i32> { + let ping_name = ping_name.into().map(|s| s.to_string()); + match self { + CounterMetric::Parent { inner, .. } => inner.test_get_value(ping_name), + CounterMetric::Child(c) => { + panic!("Cannot get test value for {:?} in non-parent process!", c.0) + } + } + } + + /// **Test-only API.** + /// + /// Gets the number of recorded errors for the given metric and error type. + /// + /// # Arguments + /// + /// * `error` - The type of error + /// * `ping_name` - represents the optional name of the ping to retrieve the + /// metric for. Defaults to the first value in `send_in_pings`. + /// + /// # Returns + /// + /// The number of errors reported. + pub fn test_get_num_recorded_errors(&self, error: glean::ErrorType) -> i32 { + match self { + CounterMetric::Parent { inner, .. } => inner.test_get_num_recorded_errors(error), + CounterMetric::Child(c) => panic!( + "Cannot get the number of recorded errors for {:?} in non-parent process!", + c.0 + ), + } + } +} + +#[cfg(test)] +mod test { + use crate::{common_test::*, ipc, metrics}; + + #[test] + fn sets_counter_value_parent() { + let _lock = lock_test(); + + let metric = &metrics::test_only_ipc::a_counter; + metric.add(1); + + assert_eq!(1, metric.test_get_value("store1").unwrap()); + } + + #[test] + fn sets_counter_value_child() { + let _lock = lock_test(); + + let parent_metric = &metrics::test_only_ipc::a_counter; + parent_metric.add(3); + + { + let child_metric = parent_metric.child_metric(); + + // scope for need_ipc RAII + let _raii = ipc::test_set_need_ipc(true); + let metric_id = child_metric.metric_id(); + + child_metric.add(42); + + ipc::with_ipc_payload(move |payload| { + assert!( + 42 == *payload.counters.get(&metric_id).unwrap(), + "Stored the correct value in the ipc payload" + ); + }); + } + + assert!( + false == ipc::need_ipc(), + "RAII dropped, should not need ipc any more" + ); + assert!(ipc::replay_from_buf(&ipc::take_buf().unwrap()).is_ok()); + + assert!( + 45 == parent_metric.test_get_value("store1").unwrap(), + "Values from the 'processes' should be summed" + ); + } +} diff --git a/toolkit/components/glean/api/src/private/custom_distribution.rs b/toolkit/components/glean/api/src/private/custom_distribution.rs new file mode 100644 index 0000000000..2114430898 --- /dev/null +++ b/toolkit/components/glean/api/src/private/custom_distribution.rs @@ -0,0 +1,171 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use inherent::inherent; + +use super::{CommonMetricData, MetricId}; +use glean::{DistributionData, ErrorType, HistogramType}; + +use crate::ipc::{need_ipc, with_ipc_payload}; +use glean::traits::CustomDistribution; + +/// A custom distribution metric. +/// +/// Custom distributions are used to record the distribution of arbitrary values. +pub enum CustomDistributionMetric { + Parent { + /// The metric's ID. + /// + /// **TEST-ONLY** - Do not use unless gated with `#[cfg(test)]`. + id: MetricId, + inner: glean::private::CustomDistributionMetric, + }, + Child(CustomDistributionMetricIpc), +} +#[derive(Debug)] +pub struct CustomDistributionMetricIpc(MetricId); + +impl CustomDistributionMetric { + /// Create a new timing distribution metric. + pub fn new( + id: MetricId, + meta: CommonMetricData, + range_min: u64, + range_max: u64, + bucket_count: u64, + histogram_type: HistogramType, + ) -> Self { + if need_ipc() { + CustomDistributionMetric::Child(CustomDistributionMetricIpc(id)) + } else { + debug_assert!( + range_min <= i64::MAX as u64, + "sensible limits enforced by glean_parser" + ); + debug_assert!( + range_max <= i64::MAX as u64, + "sensible limits enforced by glean_parser" + ); + debug_assert!( + bucket_count <= i64::MAX as u64, + "sensible limits enforced by glean_parser" + ); + let inner = glean::private::CustomDistributionMetric::new( + meta, + range_min as i64, + range_max as i64, + bucket_count as i64, + histogram_type, + ); + CustomDistributionMetric::Parent { id, inner } + } + } + + #[cfg(test)] + pub(crate) fn child_metric(&self) -> Self { + match self { + CustomDistributionMetric::Parent { id, .. } => { + CustomDistributionMetric::Child(CustomDistributionMetricIpc(*id)) + } + CustomDistributionMetric::Child(_) => { + panic!("Can't get a child metric from a child metric") + } + } + } +} + +#[inherent] +impl CustomDistribution for CustomDistributionMetric { + pub fn accumulate_samples_signed(&self, samples: Vec<i64>) { + match self { + CustomDistributionMetric::Parent { inner, .. } => inner.accumulate_samples(samples), + CustomDistributionMetric::Child(c) => { + with_ipc_payload(move |payload| { + if let Some(v) = payload.custom_samples.get_mut(&c.0) { + v.extend(samples); + } else { + payload.custom_samples.insert(c.0, samples); + } + }); + } + } + } + + pub fn test_get_value<'a, S: Into<Option<&'a str>>>( + &self, + ping_name: S, + ) -> Option<DistributionData> { + let ping_name = ping_name.into().map(|s| s.to_string()); + match self { + CustomDistributionMetric::Parent { inner, .. } => inner.test_get_value(ping_name), + CustomDistributionMetric::Child(c) => { + panic!("Cannot get test value for {:?} in non-parent process!", c) + } + } + } + + pub fn test_get_num_recorded_errors(&self, error: ErrorType) -> i32 { + match self { + CustomDistributionMetric::Parent { inner, .. } => { + inner.test_get_num_recorded_errors(error) + } + CustomDistributionMetric::Child(c) => panic!( + "Cannot get number of recorded errors for {:?} in non-parent process!", + c + ), + } + } +} + +#[cfg(test)] +mod test { + use crate::{common_test::*, ipc, metrics}; + + #[test] + fn smoke_test_custom_distribution() { + let _lock = lock_test(); + + let metric = &metrics::test_only_ipc::a_custom_dist; + + metric.accumulate_samples_signed(vec![1, 2, 3]); + + assert!(metric.test_get_value("store1").is_some()); + } + + #[test] + fn custom_distribution_child() { + let _lock = lock_test(); + + let parent_metric = &metrics::test_only_ipc::a_custom_dist; + parent_metric.accumulate_samples_signed(vec![1, 268435458]); + + { + let child_metric = parent_metric.child_metric(); + + // scope for need_ipc RAII + let _raii = ipc::test_set_need_ipc(true); + + child_metric.accumulate_samples_signed(vec![4, 268435460]); + } + + let buf = ipc::take_buf().unwrap(); + assert!(buf.len() > 0); + assert!(ipc::replay_from_buf(&buf).is_ok()); + + let data = parent_metric + .test_get_value("store1") + .expect("should have some data"); + + assert_eq!(2, data.values[&1], "Low bucket has 2 values"); + assert_eq!( + 2, data.values[&268435456], + "Next higher bucket has 2 values" + ); + assert_eq!( + 1 + 4 + 268435458 + 268435460, + data.sum, + "Sum of all recorded values" + ); + } +} diff --git a/toolkit/components/glean/api/src/private/datetime.rs b/toolkit/components/glean/api/src/private/datetime.rs new file mode 100644 index 0000000000..f95b3c33fb --- /dev/null +++ b/toolkit/components/glean/api/src/private/datetime.rs @@ -0,0 +1,246 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use inherent::inherent; + +use super::{CommonMetricData, MetricId}; + +use super::TimeUnit; +use crate::ipc::need_ipc; +use chrono::{FixedOffset, TimeZone}; +use glean::traits::Datetime; + +/// A datetime metric of a certain resolution. +/// +/// Datetimes are used to make record when something happened according to the +/// client's clock. +#[derive(Clone)] +pub enum DatetimeMetric { + Parent(glean::private::DatetimeMetric), + Child(DatetimeMetricIpc), +} +#[derive(Debug, Clone)] +pub struct DatetimeMetricIpc; + +impl DatetimeMetric { + /// Create a new datetime metric. + pub fn new(_id: MetricId, meta: CommonMetricData, time_unit: TimeUnit) -> Self { + if need_ipc() { + DatetimeMetric::Child(DatetimeMetricIpc) + } else { + DatetimeMetric::Parent(glean::private::DatetimeMetric::new(meta, time_unit)) + } + } + + #[cfg(test)] + pub(crate) fn child_metric(&self) -> Self { + match self { + DatetimeMetric::Parent { .. } => DatetimeMetric::Child(DatetimeMetricIpc), + DatetimeMetric::Child(_) => panic!("Can't get a child metric from a child metric"), + } + } + + /// Sets the metric to a date/time including the timezone offset. + /// + /// # Arguments + /// + /// * `year` - the year to set the metric to. + /// * `month` - the month to set the metric to (1-12). + /// * `day` - the day to set the metric to (1-based). + /// * `hour` - the hour to set the metric to (0-23). + /// * `minute` - the minute to set the metric to. + /// * `second` - the second to set the metric to. + /// * `nano` - the nanosecond fraction to the last whole second. + /// * `offset_seconds` - the timezone difference, in seconds, for the Eastern + /// Hemisphere. Negative seconds mean Western Hemisphere. + #[cfg_attr(not(feature = "with-gecko"), allow(dead_code))] + #[allow(clippy::too_many_arguments)] + pub(crate) fn set_with_details( + &self, + year: i32, + month: u32, + day: u32, + hour: u32, + minute: u32, + second: u32, + nano: u32, + offset_seconds: i32, + ) { + match self { + DatetimeMetric::Parent(p) => { + let tz = FixedOffset::east_opt(offset_seconds); + if tz.is_none() { + log::error!( + "Unable to set datetime metric with invalid offset seconds {}", + offset_seconds + ); + // TODO: Record an error + return; + } + + let value = FixedOffset::east(offset_seconds) + .ymd_opt(year, month, day) + .and_hms_nano_opt(hour, minute, second, nano); + match value.single() { + Some(d) => p.set(Some(d.into())), + _ => { + log::error!("Unable to construct datetime") + // TODO: Record an error + } + } + } + DatetimeMetric::Child(_) => { + log::error!("Unable to set datetime metric in non-main process. This operation will be ignored."); + // If we're in automation we can panic so the instrumentor knows they've gone wrong. + // This is a deliberate violation of Glean's "metric APIs must not throw" design. + assert!(!crate::ipc::is_in_automation(), "Attempted to set datetime in non-main process, which is forbidden. This panics in automation."); + // TODO: Record an error. + } + } + } +} + +#[inherent] +impl Datetime for DatetimeMetric { + /// Sets the metric to a date/time which including the timezone offset. + /// + /// ## Arguments + /// + /// - `value` - The date and time and timezone value to set. + /// If None we use the current local time. + pub fn set(&self, value: Option<glean::Datetime>) { + match self { + DatetimeMetric::Parent(p) => { + p.set(value); + } + DatetimeMetric::Child(_) => { + log::error!( + "Unable to set datetime metric DatetimeMetric in non-main process. This operation will be ignored." + ); + // If we're in automation we can panic so the instrumentor knows they've gone wrong. + // This is a deliberate violation of Glean's "metric APIs must not throw" design. + assert!(!crate::ipc::is_in_automation(), "Attempted to set datetime metric in non-main process, which is forbidden. This panics in automation."); + // TODO: Record an error. + } + } + } + + /// **Exported for test purposes.** + /// + /// Gets the currently stored value as a Datetime. + /// + /// The precision of this value is truncated to the `time_unit` precision. + /// + /// This doesn't clear the stored value. + /// + /// # Arguments + /// + /// * `ping_name` - represents the optional name of the ping to retrieve the + /// metric for. Defaults to the first value in `send_in_pings`. + pub fn test_get_value<'a, S: Into<Option<&'a str>>>( + &self, + ping_name: S, + ) -> Option<glean::Datetime> { + let ping_name = ping_name.into().map(|s| s.to_string()); + match self { + DatetimeMetric::Parent(p) => p.test_get_value(ping_name), + DatetimeMetric::Child(_) => { + panic!("Cannot get test value for DatetimeMetric in non-main process!") + } + } + } + + /// **Exported for test purposes.** + /// + /// Gets the number of recorded errors for the given metric and error type. + /// + /// # Arguments + /// + /// * `error` - The type of error + /// * `ping_name` - represents the optional name of the ping to retrieve the + /// metric for. Defaults to the first value in `send_in_pings`. + /// + /// # Returns + /// + /// The number of errors reported. + pub fn test_get_num_recorded_errors(&self, error: glean::ErrorType) -> i32 { + match self { + DatetimeMetric::Parent(p) => p.test_get_num_recorded_errors(error), + DatetimeMetric::Child(_) => panic!( + "Cannot get the number of recorded errors for DatetimeMetric in non-main process!" + ), + } + } +} + +#[cfg(test)] +mod test { + use chrono::{DateTime, FixedOffset, TimeZone}; + + use crate::{common_test::*, ipc, metrics}; + + #[test] + fn sets_datetime_value() { + let _lock = lock_test(); + + let metric = &metrics::test_only_ipc::a_date; + + let a_datetime = FixedOffset::east(5 * 3600) + .ymd(2020, 05, 07) + .and_hms(11, 58, 00); + metric.set(Some(a_datetime.into())); + + let expected: glean::Datetime = DateTime::parse_from_rfc3339("2020-05-07T11:58:00+05:00") + .unwrap() + .into(); + assert_eq!(expected, metric.test_get_value("store1").unwrap()); + } + + #[test] + fn sets_datetime_value_with_details() { + let _lock = lock_test(); + + let metric = &metrics::test_only_ipc::a_date; + + metric.set_with_details(2020, 05, 07, 11, 58, 0, 0, 5 * 3600); + + let expected: glean::Datetime = DateTime::parse_from_rfc3339("2020-05-07T11:58:00+05:00") + .unwrap() + .into(); + assert_eq!(expected, metric.test_get_value("store1").unwrap()); + } + + #[test] + fn datetime_ipc() { + // DatetimeMetric doesn't support IPC. + let _lock = lock_test(); + + let parent_metric = &metrics::test_only_ipc::a_date; + + // Instrumentation calls do not panic. + let a_datetime = FixedOffset::east(5 * 3600) + .ymd(2020, 10, 13) + .and_hms(16, 41, 00); + parent_metric.set(Some(a_datetime.into())); + + { + let child_metric = parent_metric.child_metric(); + + let _raii = ipc::test_set_need_ipc(true); + + let a_datetime = FixedOffset::east(0).ymd(2018, 4, 7).and_hms(12, 01, 00); + child_metric.set(Some(a_datetime.into())); + + // (They also shouldn't do anything, + // but that's not something we can inspect in this test) + } + + assert!(ipc::replay_from_buf(&ipc::take_buf().unwrap()).is_ok()); + + let expected: glean::Datetime = DateTime::parse_from_rfc3339("2020-10-13T16:41:00+05:00") + .unwrap() + .into(); + assert_eq!(expected, parent_metric.test_get_value("store1").unwrap()); + } +} diff --git a/toolkit/components/glean/api/src/private/denominator.rs b/toolkit/components/glean/api/src/private/denominator.rs new file mode 100644 index 0000000000..64982b5050 --- /dev/null +++ b/toolkit/components/glean/api/src/private/denominator.rs @@ -0,0 +1,157 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use inherent::inherent; + +use super::CommonMetricData; + +use glean::traits::Counter; + +use crate::ipc::{need_ipc, with_ipc_payload}; +use crate::private::MetricId; + +/// Developer-facing API for recording counter metrics that are acting as +/// external denominators for rate metrics. +/// +/// Instances of this class type are automatically generated by the parsers +/// at build time, allowing developers to record values that were previously +/// registered in the metrics.yaml file. +#[derive(Clone)] +pub enum DenominatorMetric { + Parent { + /// The metric's ID. + /// + /// **TEST-ONLY** - Do not use unless gated with `#[cfg(test)]`. + id: MetricId, + inner: glean::private::DenominatorMetric, + }, + Child(DenominatorMetricIpc), +} +#[derive(Clone, Debug)] +pub struct DenominatorMetricIpc(MetricId); + +impl DenominatorMetric { + /// The constructor used by automatically generated metrics. + pub fn new(id: MetricId, meta: CommonMetricData, numerators: Vec<CommonMetricData>) -> Self { + if need_ipc() { + DenominatorMetric::Child(DenominatorMetricIpc(id)) + } else { + let inner = glean::private::DenominatorMetric::new(meta, numerators); + DenominatorMetric::Parent { id, inner } + } + } + + #[cfg(test)] + pub(crate) fn metric_id(&self) -> MetricId { + match self { + DenominatorMetric::Parent { id, .. } => *id, + DenominatorMetric::Child(c) => c.0, + } + } + + #[cfg(test)] + pub(crate) fn child_metric(&self) -> Self { + match self { + DenominatorMetric::Parent { id, .. } => { + DenominatorMetric::Child(DenominatorMetricIpc(*id)) + } + DenominatorMetric::Child(_) => panic!("Can't get a child metric from a child metric"), + } + } +} + +#[inherent] +impl Counter for DenominatorMetric { + pub fn add(&self, amount: i32) { + match self { + DenominatorMetric::Parent { inner, .. } => { + inner.add(amount); + } + DenominatorMetric::Child(c) => { + with_ipc_payload(move |payload| { + if let Some(v) = payload.denominators.get_mut(&c.0) { + *v += amount; + } else { + payload.denominators.insert(c.0, amount); + } + }); + } + } + } + + pub fn test_get_value<'a, S: Into<Option<&'a str>>>(&self, ping_name: S) -> Option<i32> { + let ping_name = ping_name.into().map(|s| s.to_string()); + match self { + DenominatorMetric::Parent { inner, .. } => inner.test_get_value(ping_name), + DenominatorMetric::Child(c) => { + panic!("Cannot get test value for {:?} in non-parent process!", c.0); + } + } + } + + pub fn test_get_num_recorded_errors(&self, error: glean::ErrorType) -> i32 { + match self { + DenominatorMetric::Parent { inner, .. } => inner.test_get_num_recorded_errors(error), + DenominatorMetric::Child(c) => { + panic!( + "Cannot get the number of recorded errors for {:?} in non-parent process!", + c.0 + ); + } + } + } +} + +#[cfg(test)] +mod test { + use crate::{common_test::*, ipc, metrics}; + + #[test] + fn sets_denominator_value_parent() { + let _lock = lock_test(); + + let metric = &metrics::test_only_ipc::an_external_denominator; + metric.add(1); + + assert_eq!(1, metric.test_get_value("store1").unwrap()); + } + + #[test] + fn sets_denominator_value_child() { + let _lock = lock_test(); + + let parent_metric = &metrics::test_only_ipc::an_external_denominator; + parent_metric.add(3); + + { + // scope for need_ipc RAII + let child_metric = parent_metric.child_metric(); + let _raii = ipc::test_set_need_ipc(true); + let metric_id = child_metric.metric_id(); + + child_metric.add(42); + + ipc::with_ipc_payload(move |payload| { + assert_eq!( + 42, + *payload.denominators.get(&metric_id).unwrap(), + "Stored the correct value in the ipc payload" + ); + }); + } + + assert_eq!( + false, + ipc::need_ipc(), + "RAII dropped, should not need ipc any more" + ); + assert!(ipc::replay_from_buf(&ipc::take_buf().unwrap()).is_ok()); + + assert_eq!( + 45, + parent_metric.test_get_value("store1").unwrap(), + "Values from the 'processes' should be summed" + ); + } +} diff --git a/toolkit/components/glean/api/src/private/event.rs b/toolkit/components/glean/api/src/private/event.rs new file mode 100644 index 0000000000..11f8213bef --- /dev/null +++ b/toolkit/components/glean/api/src/private/event.rs @@ -0,0 +1,241 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use std::collections::HashMap; + +use inherent::inherent; + +use super::{CommonMetricData, MetricId, RecordedEvent}; + +use crate::ipc::{need_ipc, with_ipc_payload}; + +use glean::traits::Event; +pub use glean::traits::{EventRecordingError, ExtraKeys, NoExtraKeys}; + +/// An event metric. +/// +/// Events allow recording of e.g. individual occurences of user actions, say +/// every time a view was open and from where. Each time you record an event, it +/// records a timestamp, the event's name and a set of custom values. +pub enum EventMetric<K> { + Parent { + /// The metric's ID. + /// + /// **TEST-ONLY** - Do not use unless gated with `#[cfg(test)]`. + id: MetricId, + inner: glean::private::EventMetric<K>, + }, + Child(EventMetricIpc), +} + +#[derive(Debug)] +pub struct EventMetricIpc(MetricId); + +impl<K: 'static + ExtraKeys + Send + Sync> EventMetric<K> { + /// Create a new event metric. + pub fn new(id: MetricId, meta: CommonMetricData) -> Self { + if need_ipc() { + EventMetric::Child(EventMetricIpc(id)) + } else { + let inner = glean::private::EventMetric::new(meta); + EventMetric::Parent { id, inner } + } + } + + pub fn with_runtime_extra_keys( + id: MetricId, + meta: CommonMetricData, + allowed_extra_keys: Vec<String>, + ) -> Self { + if need_ipc() { + EventMetric::Child(EventMetricIpc(id)) + } else { + let inner = + glean::private::EventMetric::with_runtime_extra_keys(meta, allowed_extra_keys); + EventMetric::Parent { id, inner } + } + } + + #[cfg(test)] + pub(crate) fn child_metric(&self) -> Self { + match self { + EventMetric::Parent { id, .. } => EventMetric::Child(EventMetricIpc(*id)), + EventMetric::Child(_) => panic!("Can't get a child metric from a child metric"), + } + } + + /// Record a new event with the raw `extra key ID -> String` map. + /// + /// Should only be used when taking in data over FFI, where extra keys only exists as IDs. + pub(crate) fn record_raw(&self, extra: HashMap<String, String>) { + let now = glean::get_timestamp_ms(); + self.record_with_time(now, extra); + } + + /// Record a new event with the given timestamp and the raw `extra key ID -> String` map. + /// + /// Should only be used when applying previously recorded events, e.g. from IPC. + pub(crate) fn record_with_time(&self, timestamp: u64, extra: HashMap<String, String>) { + match self { + EventMetric::Parent { inner, .. } => { + inner.record_with_time(timestamp, extra); + } + EventMetric::Child(c) => { + with_ipc_payload(move |payload| { + if let Some(v) = payload.events.get_mut(&c.0) { + v.push((timestamp, extra)); + } else { + let v = vec![(timestamp, extra)]; + payload.events.insert(c.0, v); + } + }); + } + } + } +} + +#[inherent] +impl<K: 'static + ExtraKeys + Send + Sync> Event for EventMetric<K> { + type Extra = K; + + pub fn record<M: Into<Option<K>>>(&self, extra: M) { + match self { + EventMetric::Parent { inner, .. } => { + inner.record(extra); + } + EventMetric::Child(_) => { + let now = glean::get_timestamp_ms(); + let extra = extra.into().map(|extra| extra.into_ffi_extra()); + let extra = extra.unwrap_or_else(HashMap::new); + self.record_with_time(now, extra); + } + } + } + + pub fn test_get_value<'a, S: Into<Option<&'a str>>>( + &self, + ping_name: S, + ) -> Option<Vec<RecordedEvent>> { + match self { + EventMetric::Parent { inner, .. } => inner.test_get_value(ping_name), + EventMetric::Child(_) => { + panic!("Cannot get test value for event metric in non-main process!",) + } + } + } + + pub fn test_get_num_recorded_errors(&self, error: glean::ErrorType) -> i32 { + match self { + EventMetric::Parent { inner, .. } => inner.test_get_num_recorded_errors(error), + EventMetric::Child(c) => panic!( + "Cannot get the number of recorded errors for {:?} in non-main process!", + c.0 + ), + } + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::{common_test::*, ipc, metrics}; + + #[test] + fn smoke_test_event() { + let _lock = lock_test(); + + let metric = EventMetric::<NoExtraKeys>::new( + 0.into(), + CommonMetricData { + name: "event_metric".into(), + category: "telemetry".into(), + send_in_pings: vec!["store1".into()], + disabled: false, + ..Default::default() + }, + ); + + // No extra keys + metric.record(None); + + let recorded = metric.test_get_value("store1").unwrap(); + + assert!(recorded.iter().any(|e| e.name == "event_metric")); + } + + #[test] + fn event_ipc() { + use metrics::test_only_ipc::AnEventExtra; + + let _lock = lock_test(); + + let parent_metric = &metrics::test_only_ipc::an_event; + + // No extra keys + parent_metric.record(None); + + { + let child_metric = parent_metric.child_metric(); + + // scope for need_ipc RAII. + let _raii = ipc::test_set_need_ipc(true); + + child_metric.record(None); + + let extra = AnEventExtra { + extra1: Some("a-child-value".into()), + ..Default::default() + }; + child_metric.record(extra); + } + + // Record in the parent after the child. + let extra = AnEventExtra { + extra1: Some("a-valid-value".into()), + ..Default::default() + }; + parent_metric.record(extra); + + assert!(ipc::replay_from_buf(&ipc::take_buf().unwrap()).is_ok()); + + let events = parent_metric.test_get_value("store1").unwrap(); + assert_eq!(events.len(), 4); + + // Events from the child process are last, they might get sorted later by Glean. + assert_eq!(events[0].extra, None); + assert!(events[1].extra.as_ref().unwrap().get("extra1").unwrap() == "a-valid-value"); + assert_eq!(events[2].extra, None); + assert!(events[3].extra.as_ref().unwrap().get("extra1").unwrap() == "a-child-value"); + } + + #[test] + fn events_with_typed_extras() { + use metrics::test_only_ipc::EventWithExtraExtra; + let _lock = lock_test(); + + let event = &metrics::test_only_ipc::event_with_extra; + // Record in the parent after the child. + let extra = EventWithExtraExtra { + extra1: Some("a-valid-value".into()), + extra2: Some(37), + extra3_longer_name: Some(false), + }; + event.record(extra); + + let recorded = event.test_get_value("store1").unwrap(); + + assert_eq!(recorded.len(), 1); + assert!(recorded[0].extra.as_ref().unwrap().get("extra1").unwrap() == "a-valid-value"); + assert!(recorded[0].extra.as_ref().unwrap().get("extra2").unwrap() == "37"); + assert!( + recorded[0] + .extra + .as_ref() + .unwrap() + .get("extra3_longer_name") + .unwrap() + == "false" + ); + } +} diff --git a/toolkit/components/glean/api/src/private/labeled.rs b/toolkit/components/glean/api/src/private/labeled.rs new file mode 100644 index 0000000000..60034ae5f1 --- /dev/null +++ b/toolkit/components/glean/api/src/private/labeled.rs @@ -0,0 +1,369 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use inherent::inherent; + +use super::{ + CommonMetricData, ErrorType, LabeledBooleanMetric, LabeledCounterMetric, LabeledStringMetric, + MetricId, +}; +use crate::ipc::need_ipc; +use std::borrow::Cow; +use std::marker::PhantomData; + +/// Sealed traits protect against downstream implementations. +/// +/// We wrap it in a private module that is inaccessible outside of this module. +mod private { + use super::{ + need_ipc, LabeledBooleanMetric, LabeledCounterMetric, LabeledStringMetric, MetricId, + }; + use crate::private::CounterMetric; + use std::sync::Arc; + + /// The sealed trait. + /// + /// This allows us to define which FOG metrics can be used + /// as labeled types. + pub trait Sealed { + type GleanMetric: glean::private::AllowLabeled + Clone; + fn from_glean_metric(id: MetricId, metric: Arc<Self::GleanMetric>, label: &str) -> Self; + } + + // `LabeledMetric<LabeledBooleanMetric>` is possible. + // + // See [Labeled Booleans](https://mozilla.github.io/glean/book/user/metrics/labeled_booleans.html). + impl Sealed for LabeledBooleanMetric { + type GleanMetric = glean::private::BooleanMetric; + fn from_glean_metric(_id: MetricId, metric: Arc<Self::GleanMetric>, _label: &str) -> Self { + if need_ipc() { + // TODO: Instrument this error. + LabeledBooleanMetric::Child(crate::private::boolean::BooleanMetricIpc) + } else { + LabeledBooleanMetric::Parent(metric) + } + } + } + + // `LabeledMetric<LabeledStringMetric>` is possible. + // + // See [Labeled Strings](https://mozilla.github.io/glean/book/user/metrics/labeled_strings.html). + impl Sealed for LabeledStringMetric { + type GleanMetric = glean::private::StringMetric; + fn from_glean_metric(_id: MetricId, metric: Arc<Self::GleanMetric>, _label: &str) -> Self { + if need_ipc() { + // TODO: Instrument this error. + LabeledStringMetric::Child(crate::private::string::StringMetricIpc) + } else { + LabeledStringMetric::Parent(metric) + } + } + } + + // `LabeledMetric<LabeledCounterMetric>` is possible. + // + // See [Labeled Counters](https://mozilla.github.io/glean/book/user/metrics/labeled_counters.html). + impl Sealed for LabeledCounterMetric { + type GleanMetric = glean::private::CounterMetric; + fn from_glean_metric(id: MetricId, metric: Arc<Self::GleanMetric>, label: &str) -> Self { + if need_ipc() { + LabeledCounterMetric::Child { + id, + label: label.to_string(), + } + } else { + LabeledCounterMetric::Parent(CounterMetric::Parent { id, inner: metric }) + } + } + } +} + +/// Marker trait for metrics that can be nested inside a labeled metric. +/// +/// This trait is sealed and cannot be implemented for types outside this crate. +pub trait AllowLabeled: private::Sealed {} + +// Implement the trait for everything we marked as allowed. +impl<T> AllowLabeled for T where T: private::Sealed {} + +/// A labeled metric. +/// +/// Labeled metrics allow to record multiple sub-metrics of the same type under different string labels. +/// +/// ## Example +/// +/// The following piece of code will be generated by `glean_parser`: +/// +/// ```rust,ignore +/// use glean::metrics::{LabeledMetric, BooleanMetric, CommonMetricData, Lifetime}; +/// use once_cell::sync::Lazy; +/// +/// mod error { +/// pub static seen_one: Lazy<LabeledMetric<BooleanMetric, DynamicLabel>> = Lazy::new(|| LabeledMetric::new(CommonMetricData { +/// name: "seen_one".into(), +/// category: "error".into(), +/// send_in_pings: vec!["ping".into()], +/// disabled: false, +/// lifetime: Lifetime::Ping, +/// ..Default::default() +/// }, None)); +/// } +/// ``` +/// +/// It can then be used with: +/// +/// ```rust,ignore +/// errro::seen_one.get("upload").set(true); +/// ``` +pub struct LabeledMetric<T: AllowLabeled, E> { + /// The metric ID of the underlying metric. + id: MetricId, + + /// Wrapping the underlying core metric. + /// + /// We delegate all functionality to this and wrap it up again in our own metric type. + core: glean::private::LabeledMetric<T::GleanMetric>, + + label_enum: PhantomData<E>, +} + +impl<T, E> LabeledMetric<T, E> +where + T: AllowLabeled, +{ + /// Create a new labeled metric from the given metric instance and optional list of labels. + /// + /// See [`get`](#method.get) for information on how static or dynamic labels are handled. + pub fn new( + id: MetricId, + meta: CommonMetricData, + labels: Option<Vec<Cow<'static, str>>>, + ) -> LabeledMetric<T, E> { + let core = glean::private::LabeledMetric::new(meta, labels); + LabeledMetric { + id, + core, + label_enum: PhantomData, + } + } +} + +#[inherent] +impl<U, E> glean::traits::Labeled<U> for LabeledMetric<U, E> +where + U: AllowLabeled + Clone, +{ + /// Gets a specific metric for a given label. + /// + /// If a set of acceptable labels were specified in the `metrics.yaml` file, + /// and the given label is not in the set, it will be recorded under the special `OTHER_LABEL` label. + /// + /// If a set of acceptable labels was not specified in the `metrics.yaml` file, + /// only the first 16 unique labels will be used. + /// After that, any additional labels will be recorded under the special `OTHER_LABEL` label. + /// + /// Labels must be `snake_case` and less than 30 characters. + /// If an invalid label is used, the metric will be recorded in the special `OTHER_LABEL` label. + pub fn get(&self, label: &str) -> U { + let metric = self.core.get(label); + U::from_glean_metric(self.id, metric, label) + } + + /// **Exported for test purposes.** + /// + /// Gets the number of recorded errors for the given metric and error type. + /// + /// # Arguments + /// + /// * `error` - The type of error + /// * `ping_name` - represents the optional name of the ping to retrieve the + /// metric for. Defaults to the first value in `send_in_pings`. + /// + /// # Returns + /// + /// The number of errors reported. + pub fn test_get_num_recorded_errors(&self, error: ErrorType) -> i32 { + if need_ipc() { + panic!("Use of labeled metrics in IPC land not yet implemented!"); + } else { + self.core.test_get_num_recorded_errors(error) + } + } +} + +#[cfg(test)] +mod test { + use once_cell::sync::Lazy; + + use super::*; + use crate::common_test::*; + use crate::metrics::DynamicLabel; + + // Smoke test for what should be the generated code. + static GLOBAL_METRIC: Lazy<LabeledMetric<LabeledBooleanMetric, DynamicLabel>> = + Lazy::new(|| { + LabeledMetric::new( + 0.into(), + CommonMetricData { + name: "global".into(), + category: "metric".into(), + send_in_pings: vec!["ping".into()], + disabled: false, + ..Default::default() + }, + None, + ) + }); + + #[test] + fn smoke_test_global_metric() { + let _lock = lock_test(); + + GLOBAL_METRIC.get("a_value").set(true); + assert_eq!( + true, + GLOBAL_METRIC.get("a_value").test_get_value("ping").unwrap() + ); + } + + #[test] + fn sets_labeled_bool_metrics() { + let _lock = lock_test(); + let store_names: Vec<String> = vec!["store1".into()]; + + let metric: LabeledMetric<LabeledBooleanMetric, DynamicLabel> = LabeledMetric::new( + 0.into(), + CommonMetricData { + name: "bool".into(), + category: "labeled".into(), + send_in_pings: store_names, + disabled: false, + ..Default::default() + }, + None, + ); + + metric.get("upload").set(true); + + assert!(metric.get("upload").test_get_value("store1").unwrap()); + assert_eq!(None, metric.get("download").test_get_value("store1")); + } + + #[test] + fn sets_labeled_string_metrics() { + let _lock = lock_test(); + let store_names: Vec<String> = vec!["store1".into()]; + + let metric: LabeledMetric<LabeledStringMetric, DynamicLabel> = LabeledMetric::new( + 0.into(), + CommonMetricData { + name: "string".into(), + category: "labeled".into(), + send_in_pings: store_names, + disabled: false, + ..Default::default() + }, + None, + ); + + metric.get("upload").set("Glean"); + + assert_eq!( + "Glean", + metric.get("upload").test_get_value("store1").unwrap() + ); + assert_eq!(None, metric.get("download").test_get_value("store1")); + } + + #[test] + fn sets_labeled_counter_metrics() { + let _lock = lock_test(); + let store_names: Vec<String> = vec!["store1".into()]; + + let metric: LabeledMetric<LabeledCounterMetric, DynamicLabel> = LabeledMetric::new( + 0.into(), + CommonMetricData { + name: "counter".into(), + category: "labeled".into(), + send_in_pings: store_names, + disabled: false, + ..Default::default() + }, + None, + ); + + metric.get("upload").add(10); + + assert_eq!(10, metric.get("upload").test_get_value("store1").unwrap()); + assert_eq!(None, metric.get("download").test_get_value("store1")); + } + + #[test] + fn records_errors() { + let _lock = lock_test(); + let store_names: Vec<String> = vec!["store1".into()]; + + let metric: LabeledMetric<LabeledBooleanMetric, DynamicLabel> = LabeledMetric::new( + 0.into(), + CommonMetricData { + name: "bool".into(), + category: "labeled".into(), + send_in_pings: store_names, + disabled: false, + ..Default::default() + }, + None, + ); + + metric.get(&"1".repeat(72)).set(true); + + assert_eq!( + 1, + metric.test_get_num_recorded_errors(ErrorType::InvalidLabel) + ); + } + + #[test] + fn predefined_labels() { + let _lock = lock_test(); + let store_names: Vec<String> = vec!["store1".into()]; + + #[allow(dead_code)] + enum MetricLabels { + Label1 = 0, + Label2 = 1, + } + let metric: LabeledMetric<LabeledBooleanMetric, MetricLabels> = LabeledMetric::new( + 0.into(), + CommonMetricData { + name: "bool".into(), + category: "labeled".into(), + send_in_pings: store_names, + disabled: false, + ..Default::default() + }, + Some(vec!["label1".into(), "label2".into()]), + ); + + metric.get("label1").set(true); + metric.get("label2").set(false); + metric.get("not_a_label").set(true); + + assert_eq!(true, metric.get("label1").test_get_value("store1").unwrap()); + assert_eq!( + false, + metric.get("label2").test_get_value("store1").unwrap() + ); + // The label not in the predefined set is recorded to the `other` bucket. + assert_eq!( + true, + metric.get("__other__").test_get_value("store1").unwrap() + ); + + assert_eq!( + 0, + metric.test_get_num_recorded_errors(ErrorType::InvalidLabel) + ); + } +} diff --git a/toolkit/components/glean/api/src/private/labeled_counter.rs b/toolkit/components/glean/api/src/private/labeled_counter.rs new file mode 100644 index 0000000000..73a6697601 --- /dev/null +++ b/toolkit/components/glean/api/src/private/labeled_counter.rs @@ -0,0 +1,179 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use inherent::inherent; + +use glean::traits::Counter; + +use super::CommonMetricData; + +use crate::ipc::{need_ipc, with_ipc_payload}; +use crate::private::{CounterMetric, MetricId}; +use std::collections::HashMap; + +/// A counter metric that knows it's a labeled counter's submetric. +/// +/// It has special work to do when in a non-parent process. +/// When on the parent process, it dispatches calls to the normal CounterMetric. +#[derive(Clone)] +pub enum LabeledCounterMetric { + Parent(CounterMetric), + Child { id: MetricId, label: String }, +} + +impl LabeledCounterMetric { + /// Create a new labeled counter submetric. + pub fn new(id: MetricId, meta: CommonMetricData, label: String) -> Self { + if need_ipc() { + LabeledCounterMetric::Child { id, label } + } else { + LabeledCounterMetric::Parent(CounterMetric::new(id, meta)) + } + } + + #[cfg(test)] + pub(crate) fn metric_id(&self) -> MetricId { + match self { + LabeledCounterMetric::Parent(p) => p.metric_id(), + LabeledCounterMetric::Child { id, .. } => *id, + } + } +} + +#[inherent] +impl Counter for LabeledCounterMetric { + /// Increase the counter by `amount`. + /// + /// ## Arguments + /// + /// * `amount` - The amount to increase by. Should be positive. + /// + /// ## Notes + /// + /// Logs an error if the `amount` is 0 or negative. + pub fn add(&self, amount: i32) { + match self { + LabeledCounterMetric::Parent(p) => p.add(amount), + LabeledCounterMetric::Child { id, label } => { + with_ipc_payload(move |payload| { + if let Some(map) = payload.labeled_counters.get_mut(id) { + if let Some(v) = map.get_mut(label) { + *v += amount; + } else { + map.insert(label.to_string(), amount); + } + } else { + let mut map = HashMap::new(); + map.insert(label.to_string(), amount); + payload.labeled_counters.insert(*id, map); + } + }); + } + } + } + + /// **Test-only API.** + /// + /// Get the currently stored value as an integer. + /// This doesn't clear the stored value. + /// + /// ## Arguments + /// + /// * `ping_name` - the storage name to look into. + /// + /// ## Return value + /// + /// Returns the stored value or `None` if nothing stored. + pub fn test_get_value<'a, S: Into<Option<&'a str>>>(&self, ping_name: S) -> Option<i32> { + match self { + LabeledCounterMetric::Parent(p) => p.test_get_value(ping_name), + LabeledCounterMetric::Child { id, .. } => { + panic!("Cannot get test value for {:?} in non-parent process!", id) + } + } + } + + /// **Test-only API.** + /// + /// Gets the number of recorded errors for the given metric and error type. + /// + /// # Arguments + /// + /// * `error` - The type of error + /// * `ping_name` - represents the optional name of the ping to retrieve the + /// metric for. Defaults to the first value in `send_in_pings`. + /// + /// # Returns + /// + /// The number of errors reported. + pub fn test_get_num_recorded_errors(&self, error: glean::ErrorType) -> i32 { + match self { + LabeledCounterMetric::Parent(p) => p.test_get_num_recorded_errors(error), + LabeledCounterMetric::Child { id, .. } => panic!( + "Cannot get the number of recorded errors for {:?} in non-parent process!", + id + ), + } + } +} + +#[cfg(test)] +mod test { + use crate::{common_test::*, ipc, metrics}; + + #[test] + fn sets_labeled_counter_value_parent() { + let _lock = lock_test(); + + let metric = &metrics::test_only_ipc::a_labeled_counter; + metric.get("a_label").add(1); + + assert_eq!(1, metric.get("a_label").test_get_value("store1").unwrap()); + } + + #[test] + fn sets_labeled_counter_value_child() { + let _lock = lock_test(); + + let label = "some_label"; + + let parent_metric = &metrics::test_only_ipc::a_labeled_counter; + parent_metric.get(label).add(3); + + { + // scope for need_ipc RAII + let _raii = ipc::test_set_need_ipc(true); + let child_metric = parent_metric.get(label); + + let metric_id = child_metric.metric_id(); + + child_metric.add(42); + + ipc::with_ipc_payload(move |payload| { + assert_eq!( + 42, + *payload + .labeled_counters + .get(&metric_id) + .unwrap() + .get(label) + .unwrap(), + "Stored the correct value in the ipc payload" + ); + }); + } + + assert!( + false == ipc::need_ipc(), + "RAII dropped, should not need ipc any more" + ); + assert!(ipc::replay_from_buf(&ipc::take_buf().unwrap()).is_ok()); + + assert_eq!( + 45, + parent_metric.get(label).test_get_value("store1").unwrap(), + "Values from the 'processes' should be summed" + ); + } +} diff --git a/toolkit/components/glean/api/src/private/memory_distribution.rs b/toolkit/components/glean/api/src/private/memory_distribution.rs new file mode 100644 index 0000000000..b529819562 --- /dev/null +++ b/toolkit/components/glean/api/src/private/memory_distribution.rs @@ -0,0 +1,206 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use inherent::inherent; +use std::convert::TryInto; + +use super::{CommonMetricData, DistributionData, MemoryUnit, MetricId}; + +use glean::traits::MemoryDistribution; + +use crate::ipc::{need_ipc, with_ipc_payload}; + +/// A memory distribution metric. +/// +/// Memory distributions are used to accumulate and store memory measurements for analyzing distributions of the memory data. +#[derive(Clone)] +pub enum MemoryDistributionMetric { + Parent { + /// The metric's ID. + /// + /// **TEST-ONLY** - Do not use unless gated with `#[cfg(test)]`. + id: MetricId, + inner: glean::private::MemoryDistributionMetric, + }, + Child(MemoryDistributionMetricIpc), +} +#[derive(Clone, Debug)] +pub struct MemoryDistributionMetricIpc(MetricId); + +impl MemoryDistributionMetric { + /// Create a new memory distribution metric. + pub fn new(id: MetricId, meta: CommonMetricData, memory_unit: MemoryUnit) -> Self { + if need_ipc() { + MemoryDistributionMetric::Child(MemoryDistributionMetricIpc(id)) + } else { + let inner = glean::private::MemoryDistributionMetric::new(meta, memory_unit); + MemoryDistributionMetric::Parent { id, inner } + } + } + + #[cfg(test)] + pub(crate) fn child_metric(&self) -> Self { + match self { + MemoryDistributionMetric::Parent { id, .. } => { + MemoryDistributionMetric::Child(MemoryDistributionMetricIpc(*id)) + } + MemoryDistributionMetric::Child(_) => { + panic!("Can't get a child metric from a child metric") + } + } + } +} + +#[inherent] +impl MemoryDistribution for MemoryDistributionMetric { + /// Accumulates the provided sample in the metric. + /// + /// ## Arguments + /// + /// * `sample` - The sample to be recorded by the metric. The sample is assumed to be in the + /// configured memory unit of the metric. + /// + /// ## Notes + /// + /// Values bigger than 1 Terabyte (2<sup>40</sup> bytes) are truncated + /// and an `ErrorType::InvalidValue` error is recorded. + pub fn accumulate(&self, sample: u64) { + match self { + MemoryDistributionMetric::Parent { inner, .. } => { + // values are capped at 2**40. + // If the value doesn't fit into `i64` it's definitely to large + // and cause an error. + // glean-core handles that. + let sample = sample.try_into().unwrap_or_else(|_| { + log::warn!( + "Memory size too large to fit into into 64-bytes. Saturating at i64::MAX." + ); + i64::MAX + }); + inner.accumulate(sample); + } + MemoryDistributionMetric::Child(c) => { + with_ipc_payload(move |payload| { + if let Some(v) = payload.memory_samples.get_mut(&c.0) { + v.push(sample); + } else { + payload.memory_samples.insert(c.0, vec![sample]); + } + }); + } + } + } + + /// **Test-only API.** + /// + /// Get the currently-stored histogram as a DistributionData of the serialized value. + /// This doesn't clear the stored value. + /// + /// ## Arguments + /// + /// * `ping_name` - the storage name to look into. + /// + /// ## Return value + /// + /// Returns the stored value or `None` if nothing stored. + pub fn test_get_value<'a, S: Into<Option<&'a str>>>( + &self, + ping_name: S, + ) -> Option<DistributionData> { + let ping_name = ping_name.into().map(|s| s.to_string()); + match self { + MemoryDistributionMetric::Parent { inner, .. } => inner.test_get_value(ping_name), + MemoryDistributionMetric::Child(c) => { + panic!("Cannot get test value for {:?} in non-parent process!", c.0) + } + } + } + + /// **Exported for test purposes.** + /// + /// Gets the number of recorded errors for the given error type. + /// + /// # Arguments + /// + /// * `error` - The type of error + /// * `ping_name` - represents the optional name of the ping to retrieve the + /// metric for. Defaults to the first value in `send_in_pings`. + /// + /// # Returns + /// + /// The number of errors recorded. + pub fn test_get_num_recorded_errors(&self, error: glean::ErrorType) -> i32 { + match self { + MemoryDistributionMetric::Parent { inner, .. } => { + inner.test_get_num_recorded_errors(error) + } + MemoryDistributionMetric::Child(c) => panic!( + "Cannot get the number of recorded errors for {:?} in non-parent process!", + c.0 + ), + } + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::{common_test::*, ipc, metrics}; + + #[test] + fn smoke_test_memory_distribution() { + let _lock = lock_test(); + + let metric = MemoryDistributionMetric::new( + 0.into(), + CommonMetricData { + name: "memory_distribution_metric".into(), + category: "telemetry".into(), + send_in_pings: vec!["store1".into()], + disabled: false, + ..Default::default() + }, + MemoryUnit::Kilobyte, + ); + + metric.accumulate(42); + + let metric_data = metric.test_get_value("store1").unwrap(); + assert_eq!(1, metric_data.values[&42494]); + assert_eq!(0, metric_data.values[&44376]); + assert_eq!(43008, metric_data.sum); + } + + #[test] + fn memory_distribution_child() { + let _lock = lock_test(); + + let parent_metric = &metrics::test_only_ipc::a_memory_dist; + parent_metric.accumulate(42); + + { + let child_metric = parent_metric.child_metric(); + + // scope for need_ipc RAII + let _raii = ipc::test_set_need_ipc(true); + child_metric.accumulate(13 * 9); + } + + let metric_data = parent_metric.test_get_value("store1").unwrap(); + assert_eq!(1, metric_data.values[&42494]); + assert_eq!(0, metric_data.values[&44376]); + assert_eq!(43008, metric_data.sum); + + // Single-process IPC machine goes brrrrr... + let buf = ipc::take_buf().unwrap(); + assert!(buf.len() > 0); + assert!(ipc::replay_from_buf(&buf).is_ok()); + + let data = parent_metric.test_get_value(None).expect("must have data"); + assert_eq!(2, data.values.values().fold(0, |acc, count| acc + count)); + assert_eq!(1, data.values[&42494]); + assert_eq!(1, data.values[&115097]); + assert_eq!(162816, data.sum); + } +} diff --git a/toolkit/components/glean/api/src/private/mod.rs b/toolkit/components/glean/api/src/private/mod.rs new file mode 100644 index 0000000000..b0b1e11393 --- /dev/null +++ b/toolkit/components/glean/api/src/private/mod.rs @@ -0,0 +1,76 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +//! The different metric types supported by the Glean SDK to handle data. + +use serde::{Deserialize, Serialize}; + +// Re-export of `glean` types we can re-use. +// That way a user only needs to depend on this crate, not on glean (and there can't be a +// version mismatch). +pub use glean::{ + traits, CommonMetricData, DistributionData, ErrorType, Lifetime, MemoryUnit, RecordedEvent, + TimeUnit, TimerId, +}; + +mod boolean; +mod counter; +mod custom_distribution; +mod datetime; +mod denominator; +mod event; +mod labeled; +mod labeled_counter; +mod memory_distribution; +mod numerator; +mod ping; +mod quantity; +mod rate; +pub(crate) mod string; +mod string_list; +mod text; +mod timespan; +mod timing_distribution; +mod url; +mod uuid; + +pub use self::boolean::BooleanMetric; +pub use self::boolean::BooleanMetric as LabeledBooleanMetric; +pub use self::counter::CounterMetric; +pub use self::custom_distribution::CustomDistributionMetric; +pub use self::datetime::DatetimeMetric; +pub use self::denominator::DenominatorMetric; +pub use self::event::{EventMetric, EventRecordingError, ExtraKeys, NoExtraKeys}; +pub use self::labeled::LabeledMetric; +pub use self::labeled_counter::LabeledCounterMetric; +pub use self::memory_distribution::MemoryDistributionMetric; +pub use self::numerator::NumeratorMetric; +pub use self::ping::Ping; +pub use self::quantity::QuantityMetric; +pub use self::rate::RateMetric; +pub use self::string::StringMetric; +pub use self::string::StringMetric as LabeledStringMetric; +pub use self::string_list::StringListMetric; +pub use self::text::TextMetric; +pub use self::timespan::TimespanMetric; +pub use self::timing_distribution::TimingDistributionMetric; +pub use self::url::UrlMetric; +pub use self::uuid::UuidMetric; + +/// Uniquely identifies a single metric within its metric type. +#[derive(Debug, PartialEq, Eq, Hash, Copy, Clone, Deserialize, Serialize)] +#[repr(transparent)] +pub struct MetricId(pub(crate) u32); + +impl MetricId { + pub fn new(id: u32) -> Self { + Self(id) + } +} + +impl From<u32> for MetricId { + fn from(id: u32) -> Self { + Self(id) + } +} diff --git a/toolkit/components/glean/api/src/private/numerator.rs b/toolkit/components/glean/api/src/private/numerator.rs new file mode 100644 index 0000000000..0a22bf5bfc --- /dev/null +++ b/toolkit/components/glean/api/src/private/numerator.rs @@ -0,0 +1,152 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use inherent::inherent; + +use super::CommonMetricData; + +use glean::traits::Numerator; +use glean::Rate; + +use crate::ipc::{need_ipc, with_ipc_payload}; +use crate::private::MetricId; + +/// Developer-facing API for recording rate metrics with external denominators. +/// +/// Instances of this class type are automatically generated by the parsers +/// at build time, allowing developers to record values that were previously +/// registered in the metrics.yaml file. +#[derive(Clone)] +pub enum NumeratorMetric { + Parent { + /// The metric's ID. + /// + /// **TEST-ONLY** - Do not use unless gated with `#[cfg(test)]`. + id: MetricId, + inner: glean::private::NumeratorMetric, + }, + Child(NumeratorMetricIpc), +} +#[derive(Clone, Debug)] +pub struct NumeratorMetricIpc(MetricId); + +impl NumeratorMetric { + /// The public constructor used by automatically generated metrics. + pub fn new(id: MetricId, meta: CommonMetricData) -> Self { + if need_ipc() { + NumeratorMetric::Child(NumeratorMetricIpc(id)) + } else { + let inner = glean::private::NumeratorMetric::new(meta); + NumeratorMetric::Parent { id, inner } + } + } + + #[cfg(test)] + pub(crate) fn metric_id(&self) -> MetricId { + match self { + NumeratorMetric::Parent { id, .. } => *id, + NumeratorMetric::Child(c) => c.0, + } + } + + #[cfg(test)] + pub(crate) fn child_metric(&self) -> Self { + match self { + NumeratorMetric::Parent { id, .. } => NumeratorMetric::Child(NumeratorMetricIpc(*id)), + NumeratorMetric::Child(_) => panic!("Can't get a child metric from a child metric"), + } + } +} + +#[inherent] +impl Numerator for NumeratorMetric { + pub fn add_to_numerator(&self, amount: i32) { + match self { + NumeratorMetric::Parent { inner, .. } => { + inner.add_to_numerator(amount); + } + NumeratorMetric::Child(c) => { + with_ipc_payload(move |payload| { + if let Some(v) = payload.numerators.get_mut(&c.0) { + *v += amount; + } else { + payload.numerators.insert(c.0, amount); + } + }); + } + } + } + + pub fn test_get_value<'a, S: Into<Option<&'a str>>>(&self, ping_name: S) -> Option<Rate> { + let ping_name = ping_name.into().map(|s| s.to_string()); + match self { + NumeratorMetric::Parent { inner, .. } => inner.test_get_value(ping_name), + NumeratorMetric::Child(c) => { + panic!("Cannot get test value for {:?} in non-parent process!", c.0); + } + } + } + + pub fn test_get_num_recorded_errors(&self, error: glean::ErrorType) -> i32 { + match self { + NumeratorMetric::Parent { inner, .. } => inner.test_get_num_recorded_errors(error), + NumeratorMetric::Child(c) => { + panic!( + "Cannot get the number of recorded errors for {:?} in non-parent process!", + c.0 + ); + } + } + } +} + +#[cfg(test)] +mod test { + use crate::{common_test::*, ipc, metrics}; + + #[test] + fn sets_numerator_value_parent() { + let _lock = lock_test(); + + let metric = &metrics::test_only_ipc::rate_with_external_denominator; + metric.add_to_numerator(1); + + assert_eq!(1, metric.test_get_value("store1").unwrap().numerator); + } + + #[test] + fn sets_numerator_value_child() { + let _lock = lock_test(); + + let parent_metric = &metrics::test_only_ipc::rate_with_external_denominator; + parent_metric.add_to_numerator(3); + + { + // scope for need_ipc RAII + let child_metric = parent_metric.child_metric(); + let _raii = ipc::test_set_need_ipc(true); + let metric_id = child_metric.metric_id(); + + child_metric.add_to_numerator(42); + + ipc::with_ipc_payload(move |payload| { + assert!( + 42 == *payload.numerators.get(&metric_id).unwrap(), + "Stored the correct value in the ipc payload" + ); + }); + } + + assert!( + false == ipc::need_ipc(), + "RAII dropped, should not need ipc any more" + ); + assert!(ipc::replay_from_buf(&ipc::take_buf().unwrap()).is_ok()); + + assert!( + 45 == parent_metric.test_get_value("store1").unwrap().numerator, + "Values from the 'processes' should be summed" + ); + } +} diff --git a/toolkit/components/glean/api/src/private/ping.rs b/toolkit/components/glean/api/src/private/ping.rs new file mode 100644 index 0000000000..cc9585eea1 --- /dev/null +++ b/toolkit/components/glean/api/src/private/ping.rs @@ -0,0 +1,121 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use inherent::inherent; + +use crate::ipc::need_ipc; + +/// A Glean ping. +/// +/// See [Glean Pings](https://mozilla.github.io/glean/book/user/pings/index.html). +#[derive(Clone)] +pub enum Ping { + Parent(glean::private::PingType), + Child, +} + +impl Ping { + /// Create a new ping type for the given name, whether to include the client ID and whether to + /// send this ping empty. + /// + /// ## Arguments + /// + /// * `name` - The name of the ping. + /// * `include_client_id` - Whether to include the client ID in the assembled ping when submitting. + /// * `send_if_empty` - Whether the ping should be sent empty or not. + /// * `reason_codes` - The valid reason codes for this ping. + pub fn new<S: Into<String>>( + name: S, + include_client_id: bool, + send_if_empty: bool, + precise_timestamps: bool, + reason_codes: Vec<String>, + ) -> Self { + if need_ipc() { + Ping::Child + } else { + Ping::Parent(glean::private::PingType::new( + name, + include_client_id, + send_if_empty, + precise_timestamps, + reason_codes, + )) + } + } + + /// **Test-only API** + /// + /// Attach a callback to be called right before a new ping is submitted. + /// The provided function is called exactly once before submitting a ping. + /// + /// Note: The callback will be called on any call to submit. + /// A ping might not be sent afterwards, e.g. if the ping is otherwise empty (and + /// `send_if_empty` is `false`). + pub fn test_before_next_submit(&self, cb: impl FnOnce(Option<&str>) + Send + 'static) { + match self { + Ping::Parent(p) => p.test_before_next_submit(cb), + Ping::Child => { + panic!("Cannot use ping test API from non-parent process!"); + } + }; + } +} + +#[inherent] +impl glean::traits::Ping for Ping { + /// Submits the ping for eventual uploading + /// + /// # Arguments + /// + /// * `reason` - the reason the ping was triggered. Included in the + /// `ping_info.reason` part of the payload. + pub fn submit(&self, reason: Option<&str>) { + match self { + Ping::Parent(p) => { + p.submit(reason); + } + Ping::Child => { + log::error!( + "Unable to submit ping in non-main process. This operation will be ignored." + ); + // If we're in automation we can panic so the instrumentor knows they've gone wrong. + // This is a deliberate violation of Glean's "metric APIs must not throw" design. + assert!(!crate::ipc::is_in_automation(), "Attempted to submit a ping in non-main process, which is forbidden. This panics in automation."); + // TODO: Record an error. + } + }; + } +} + +#[cfg(test)] +mod test { + use once_cell::sync::Lazy; + + use super::*; + use crate::common_test::*; + + use std::sync::{ + atomic::{AtomicBool, Ordering}, + Arc, + }; + + // Smoke test for what should be the generated code. + static PROTOTYPE_PING: Lazy<Ping> = + Lazy::new(|| Ping::new("prototype", false, true, true, vec![])); + + #[test] + fn smoke_test_custom_ping() { + let _lock = lock_test(); + + let called = Arc::new(AtomicBool::new(false)); + let rcalled = Arc::clone(&called); + PROTOTYPE_PING.test_before_next_submit(move |reason| { + (*rcalled).store(true, Ordering::Relaxed); + assert_eq!(None, reason); + }); + PROTOTYPE_PING.submit(None); + assert!((*called).load(Ordering::Relaxed)); + } +} diff --git a/toolkit/components/glean/api/src/private/quantity.rs b/toolkit/components/glean/api/src/private/quantity.rs new file mode 100644 index 0000000000..ee7021890e --- /dev/null +++ b/toolkit/components/glean/api/src/private/quantity.rs @@ -0,0 +1,155 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use inherent::inherent; + +use glean::traits::Quantity; + +use super::CommonMetricData; + +use crate::ipc::need_ipc; +use crate::private::MetricId; + +/// A quantity metric. +/// +/// Records a single numeric value of a specific unit. +#[derive(Clone)] +pub enum QuantityMetric { + Parent(glean::private::QuantityMetric), + Child(QuantityMetricIpc), +} +#[derive(Clone, Debug)] +pub struct QuantityMetricIpc; + +impl QuantityMetric { + /// Create a new quantity metric. + pub fn new(_id: MetricId, meta: CommonMetricData) -> Self { + if need_ipc() { + QuantityMetric::Child(QuantityMetricIpc) + } else { + QuantityMetric::Parent(glean::private::QuantityMetric::new(meta)) + } + } + + #[cfg(test)] + pub(crate) fn child_metric(&self) -> Self { + match self { + QuantityMetric::Parent(_) => QuantityMetric::Child(QuantityMetricIpc), + QuantityMetric::Child(_) => panic!("Can't get a child metric from a child metric"), + } + } +} + +#[inherent] +impl Quantity for QuantityMetric { + /// Set the value. Must be non-negative. + /// + /// # Arguments + /// + /// * `value` - The value. Must be non-negative. + /// + /// ## Notes + /// + /// Logs an error if the `value` is negative. + pub fn set(&self, value: i64) { + match self { + QuantityMetric::Parent(p) => { + p.set(value); + } + QuantityMetric::Child(_) => { + log::error!("Unable to set quantity metric in non-main process. This operation will be ignored."); + // If we're in automation we can panic so the instrumentor knows they've gone wrong. + // This is a deliberate violation of Glean's "metric APIs must not throw" design. + assert!(!crate::ipc::is_in_automation(), "Attempted to set quantity metric in non-main process, which is forbidden. This panics in automation."); + // TODO: Record an error. + } + } + } + + /// **Test-only API.** + /// + /// Get the currently stored value. + /// This doesn't clear the stored value. + /// + /// ## Arguments + /// + /// * `ping_name` - the storage name to look into. + /// + /// ## Return value + /// + /// Returns the stored value or `None` if nothing stored. + pub fn test_get_value<'a, S: Into<Option<&'a str>>>(&self, ping_name: S) -> Option<i64> { + let ping_name = ping_name.into().map(|s| s.to_string()); + match self { + QuantityMetric::Parent(p) => p.test_get_value(ping_name), + QuantityMetric::Child(_) => { + panic!("Cannot get test value for quantity metric in non-main process!",) + } + } + } + + /// **Test-only API.** + /// + /// Gets the number of recorded errors for the given metric and error type. + /// + /// # Arguments + /// + /// * `error` - The type of error + /// * `ping_name` - represents the optional name of the ping to retrieve the + /// metric for. Defaults to the first value in `send_in_pings`. + /// + /// # Returns + /// + /// The number of errors reported. + pub fn test_get_num_recorded_errors(&self, error: glean::ErrorType) -> i32 { + match self { + QuantityMetric::Parent(p) => p.test_get_num_recorded_errors(error), + QuantityMetric::Child(_) => panic!( + "Cannot get the number of recorded errors for quantity metric in non-main process!" + ), + } + } +} + +#[cfg(test)] +mod test { + use crate::{common_test::*, ipc, metrics}; + + #[test] + fn sets_quantity_metric() { + let _lock = lock_test(); + + let metric = &metrics::test_only_ipc::a_quantity; + metric.set(14); + + assert_eq!(14, metric.test_get_value("store1").unwrap()); + } + + #[test] + fn quantity_ipc() { + // QuantityMetric doesn't support IPC. + let _lock = lock_test(); + + let parent_metric = &metrics::test_only_ipc::a_quantity; + + parent_metric.set(15); + + { + let child_metric = parent_metric.child_metric(); + + // scope for need_ipc RAII + let _raii = ipc::test_set_need_ipc(true); + + // Instrumentation calls do not panic. + child_metric.set(30); + + // (They also shouldn't do anything, + // but that's not something we can inspect in this test) + } + + assert!(ipc::replay_from_buf(&ipc::take_buf().unwrap()).is_ok()); + + assert_eq!(15, parent_metric.test_get_value(None).unwrap()); + } +} diff --git a/toolkit/components/glean/api/src/private/rate.rs b/toolkit/components/glean/api/src/private/rate.rs new file mode 100644 index 0000000000..39f04db767 --- /dev/null +++ b/toolkit/components/glean/api/src/private/rate.rs @@ -0,0 +1,186 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use inherent::inherent; + +use super::CommonMetricData; + +use glean::traits::Rate; + +use crate::ipc::{need_ipc, with_ipc_payload}; +use crate::private::MetricId; + +/// Developer-facing API for recording rate metrics. +/// +/// Instances of this class type are automatically generated by the parsers +/// at build time, allowing developers to record values that were previously +/// registered in the metrics.yaml file. +#[derive(Clone)] +pub enum RateMetric { + Parent { + /// The metric's ID. + /// + /// **TEST-ONLY** - Do not use unless gated with `#[cfg(test)]`. + id: MetricId, + inner: glean::private::RateMetric, + }, + Child(RateMetricIpc), +} +#[derive(Clone, Debug)] +pub struct RateMetricIpc(MetricId); + +impl RateMetric { + /// The public constructor used by automatically generated metrics. + pub fn new(id: MetricId, meta: CommonMetricData) -> Self { + if need_ipc() { + RateMetric::Child(RateMetricIpc(id)) + } else { + let inner = glean::private::RateMetric::new(meta); + RateMetric::Parent { id, inner } + } + } + + #[cfg(test)] + pub(crate) fn metric_id(&self) -> MetricId { + match self { + RateMetric::Parent { id, .. } => *id, + RateMetric::Child(c) => c.0, + } + } + + #[cfg(test)] + pub(crate) fn child_metric(&self) -> Self { + match self { + RateMetric::Parent { id, .. } => RateMetric::Child(RateMetricIpc(*id)), + RateMetric::Child(_) => panic!("Can't get a child metric from a child metric"), + } + } +} + +#[inherent] +impl Rate for RateMetric { + pub fn add_to_numerator(&self, amount: i32) { + match self { + RateMetric::Parent { inner, .. } => { + inner.add_to_numerator(amount); + } + RateMetric::Child(c) => { + with_ipc_payload(move |payload| { + if let Some(r) = payload.rates.get_mut(&c.0) { + r.0 += amount; + } else { + payload.rates.insert(c.0, (amount, 0)); + } + }); + } + } + } + + pub fn add_to_denominator(&self, amount: i32) { + match self { + RateMetric::Parent { inner, .. } => { + inner.add_to_denominator(amount); + } + RateMetric::Child(c) => { + with_ipc_payload(move |payload| { + if let Some(r) = payload.rates.get_mut(&c.0) { + r.1 += amount; + } else { + payload.rates.insert(c.0, (0, amount)); + } + }); + } + } + } + + pub fn test_get_value<'a, S: Into<Option<&'a str>>>( + &self, + ping_name: S, + ) -> Option<glean::Rate> { + let ping_name = ping_name.into().map(|s| s.to_string()); + match self { + RateMetric::Parent { inner, .. } => inner.test_get_value(ping_name), + RateMetric::Child(c) => { + panic!("Cannot get test value for {:?} in non-parent process!", c.0); + } + } + } + + pub fn test_get_num_recorded_errors(&self, error: glean::ErrorType) -> i32 { + match self { + RateMetric::Parent { inner, .. } => inner.test_get_num_recorded_errors(error), + RateMetric::Child(c) => { + panic!( + "Cannot get the number of recorded errors for {:?} in non-parent process!", + c.0 + ); + } + } + } +} + +#[cfg(test)] +mod test { + use crate::{common_test::*, ipc, metrics}; + use glean::Rate; + + #[test] + fn sets_rate_value_parent() { + let _lock = lock_test(); + + let metric = &metrics::test_only_ipc::irate; + metric.add_to_numerator(1); + metric.add_to_denominator(100); + + assert_eq!( + Rate { + numerator: 1, + denominator: 100 + }, + metric.test_get_value("store1").unwrap() + ); + } + + #[test] + fn sets_rate_value_child() { + let _lock = lock_test(); + + let parent_metric = &metrics::test_only_ipc::irate; + parent_metric.add_to_numerator(3); + parent_metric.add_to_denominator(9); + + { + // scope for need_ipc RAII + let child_metric = parent_metric.child_metric(); + let _raii = ipc::test_set_need_ipc(true); + let metric_id = child_metric.metric_id(); + + child_metric.add_to_numerator(42); + child_metric.add_to_denominator(24); + + ipc::with_ipc_payload(move |payload| { + assert_eq!( + (42, 24), + *payload.rates.get(&metric_id).unwrap(), + "Stored the correct value in the ipc payload" + ); + }); + } + + assert!( + false == ipc::need_ipc(), + "RAII dropped, should not need ipc any more" + ); + assert!(ipc::replay_from_buf(&ipc::take_buf().unwrap()).is_ok()); + + assert_eq!( + Rate { + numerator: 45, + denominator: 33 + }, + parent_metric.test_get_value("store1").unwrap(), + "Values from the 'processes' should be summed" + ); + } +} diff --git a/toolkit/components/glean/api/src/private/string.rs b/toolkit/components/glean/api/src/private/string.rs new file mode 100644 index 0000000000..e61064c3d5 --- /dev/null +++ b/toolkit/components/glean/api/src/private/string.rs @@ -0,0 +1,185 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use inherent::inherent; +use std::sync::Arc; + +use super::{CommonMetricData, MetricId}; +use crate::ipc::need_ipc; + +/// A string metric. +/// +/// Record an Unicode string value with arbitrary content. +/// Strings are length-limited to `MAX_LENGTH_VALUE` bytes. +/// +/// # Example +/// +/// The following piece of code will be generated by `glean_parser`: +/// +/// ```rust,ignore +/// use glean::metrics::{StringMetric, CommonMetricData, Lifetime}; +/// use once_cell::sync::Lazy; +/// +/// mod browser { +/// pub static search_engine: Lazy<StringMetric> = Lazy::new(|| StringMetric::new(CommonMetricData { +/// name: "search_engine".into(), +/// category: "browser".into(), +/// lifetime: Lifetime::Ping, +/// disabled: false, +/// dynamic_label: None +/// })); +/// } +/// ``` +/// +/// It can then be used with: +/// +/// ```rust,ignore +/// browser::search_engine.set("websearch"); +/// ``` +#[derive(Clone)] +pub enum StringMetric { + Parent(Arc<glean::private::StringMetric>), + Child(StringMetricIpc), +} +#[derive(Clone, Debug)] +pub struct StringMetricIpc; + +impl StringMetric { + /// Create a new string metric. + pub fn new(_id: MetricId, meta: CommonMetricData) -> Self { + if need_ipc() { + StringMetric::Child(StringMetricIpc) + } else { + StringMetric::Parent(Arc::new(glean::private::StringMetric::new(meta))) + } + } + + #[cfg(test)] + pub(crate) fn child_metric(&self) -> Self { + match self { + StringMetric::Parent(_) => StringMetric::Child(StringMetricIpc), + StringMetric::Child(_) => panic!("Can't get a child metric from a child metric"), + } + } +} + +#[inherent] +impl glean::traits::String for StringMetric { + /// Sets to the specified value. + /// + /// # Arguments + /// + /// * `value` - The string to set the metric to. + /// + /// ## Notes + /// + /// Truncates the value if it is longer than `MAX_STRING_LENGTH` bytes and logs an error. + pub fn set<S: Into<std::string::String>>(&self, value: S) { + match self { + StringMetric::Parent(p) => { + p.set(value.into()); + } + StringMetric::Child(_) => { + log::error!("Unable to set string metric in non-main process. This operation will be ignored."); + // If we're in automation we can panic so the instrumentor knows they've gone wrong. + // This is a deliberate violation of Glean's "metric APIs must not throw" design. + assert!(!crate::ipc::is_in_automation(), "Attempted to set string metric in non-main process, which is forbidden. This panics in automation."); + // TODO: Record an error. + } + }; + } + + /// **Exported for test purposes.** + /// + /// Gets the currently stored value as a string. + /// + /// This doesn't clear the stored value. + /// + /// # Arguments + /// + /// * `ping_name` - represents the optional name of the ping to retrieve the + /// metric for. Defaults to the first value in `send_in_pings`. + pub fn test_get_value<'a, S: Into<Option<&'a str>>>( + &self, + ping_name: S, + ) -> Option<std::string::String> { + let ping_name = ping_name.into().map(|s| s.to_string()); + match self { + StringMetric::Parent(p) => p.test_get_value(ping_name), + StringMetric::Child(_) => { + panic!("Cannot get test value for string metric in non-main process!") + } + } + } + + /// **Exported for test purposes.** + /// + /// Gets the number of recorded errors for the given metric and error type. + /// + /// # Arguments + /// + /// * `error` - The type of error + /// * `ping_name` - represents the optional name of the ping to retrieve the + /// metric for. Defaults to the first value in `send_in_pings`. + /// + /// # Returns + /// + /// The number of errors reported. + pub fn test_get_num_recorded_errors(&self, error: glean::ErrorType) -> i32 { + match self { + StringMetric::Parent(p) => p.test_get_num_recorded_errors(error), + StringMetric::Child(_) => panic!( + "Cannot get the number of recorded errors for string metric in non-main process!" + ), + } + } +} + +#[cfg(test)] +mod test { + use crate::{common_test::*, ipc, metrics}; + + #[test] + fn sets_string_value() { + let _lock = lock_test(); + + let metric = &metrics::test_only_ipc::a_string; + + metric.set("test_string_value"); + + assert_eq!( + "test_string_value", + metric.test_get_value("store1").unwrap() + ); + } + + #[test] + fn string_ipc() { + // StringMetric doesn't support IPC. + let _lock = lock_test(); + + let parent_metric = &metrics::test_only_ipc::a_string; + + parent_metric.set("test_parent_value"); + + { + let child_metric = parent_metric.child_metric(); + + let _raii = ipc::test_set_need_ipc(true); + + // Instrumentation calls do not panic. + child_metric.set("test_string_value"); + + // (They also shouldn't do anything, + // but that's not something we can inspect in this test) + } + + assert!(ipc::replay_from_buf(&ipc::take_buf().unwrap()).is_ok()); + + assert!( + "test_parent_value" == parent_metric.test_get_value("store1").unwrap(), + "String metrics should only work in the parent process" + ); + } +} diff --git a/toolkit/components/glean/api/src/private/string_list.rs b/toolkit/components/glean/api/src/private/string_list.rs new file mode 100644 index 0000000000..7edd4e89d4 --- /dev/null +++ b/toolkit/components/glean/api/src/private/string_list.rs @@ -0,0 +1,212 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use inherent::inherent; + +use super::{CommonMetricData, MetricId}; + +use glean::traits::StringList; + +use crate::ipc::{need_ipc, with_ipc_payload}; + +/// A string list metric. +/// +/// This allows appending a string value with arbitrary content to a list. +#[derive(Clone)] +pub enum StringListMetric { + Parent { + /// The metric's ID. + /// + /// **TEST-ONLY** - Do not use unless gated with `#[cfg(test)]`. + id: MetricId, + inner: glean::private::StringListMetric, + }, + Child(StringListMetricIpc), +} +#[derive(Clone, Debug)] +pub struct StringListMetricIpc(MetricId); + +impl StringListMetric { + /// Create a new string list metric. + pub fn new(id: MetricId, meta: CommonMetricData) -> Self { + if need_ipc() { + StringListMetric::Child(StringListMetricIpc(id)) + } else { + let inner = glean::private::StringListMetric::new(meta); + StringListMetric::Parent { id, inner } + } + } + + #[cfg(test)] + pub(crate) fn child_metric(&self) -> Self { + match self { + StringListMetric::Parent { id, .. } => { + StringListMetric::Child(StringListMetricIpc(*id)) + } + StringListMetric::Child(_) => panic!("Can't get a child metric from a child metric"), + } + } +} + +#[inherent] +impl StringList for StringListMetric { + /// Add a new string to the list. + /// + /// ## Arguments + /// + /// * `value` - The string to add. + /// + /// ## Notes + /// + /// Truncates the value if it is longer than `MAX_STRING_LENGTH` bytes and logs an error. + /// See [String list metric limits](https://mozilla.github.io/glean/book/user/metrics/string_list.html#limits). + pub fn add<S: Into<String>>(&self, value: S) { + match self { + StringListMetric::Parent { inner, .. } => { + inner.add(value.into()); + } + StringListMetric::Child(c) => { + with_ipc_payload(move |payload| { + if let Some(v) = payload.string_lists.get_mut(&c.0) { + v.push(value.into()); + } else { + let v = vec![value.into()]; + payload.string_lists.insert(c.0, v); + } + }); + } + } + } + + /// Set to a specific list of strings. + /// + /// ## Arguments + /// + /// * `value` - The list of string to set the metric to. + /// + /// ## Notes + /// + /// If passed an empty list, records an error and returns. + /// Truncates the list if it is longer than `MAX_LIST_LENGTH` and logs an error. + /// Truncates any value in the list if it is longer than `MAX_STRING_LENGTH` and logs an error. + pub fn set(&self, value: Vec<String>) { + match self { + StringListMetric::Parent { inner, .. } => { + inner.set(value); + } + StringListMetric::Child(c) => { + log::error!( + "Unable to set string list metric {:?} in non-main process. This operation will be ignored.", + c.0 + ); + // If we're in automation we can panic so the instrumentor knows they've gone wrong. + // This is a deliberate violation of Glean's "metric APIs must not throw" design.assert!(!crate::ipc::is_in_automation()); + assert!(!crate::ipc::is_in_automation(), "Attempted to set string list metric in non-main process, which is forbidden. This panics in automation."); + // TODO: Record an error. + } + } + } + + /// **Test-only API.** + /// + /// Get the currently stored values. + /// This doesn't clear the stored value. + /// + /// ## Arguments + /// + /// * `storage_name` - the storage name to look into. + /// + /// ## Return value + /// + /// Returns the stored value or `None` if nothing stored. + pub fn test_get_value<'a, S: Into<Option<&'a str>>>( + &self, + ping_name: S, + ) -> Option<Vec<String>> { + let ping_name = ping_name.into().map(|s| s.to_string()); + match self { + StringListMetric::Parent { inner, .. } => inner.test_get_value(ping_name), + StringListMetric::Child(c) => { + panic!("Cannot get test value for {:?} in non-parent process!", c.0) + } + } + } + + /// **Exported for test purposes.** + /// + /// Gets the number of recorded errors for the given error type. + /// + /// # Arguments + /// + /// * `error` - The type of error + /// * `ping_name` - represents the optional name of the ping to retrieve the + /// metric for. Defaults to the first value in `send_in_pings`. + /// + /// # Returns + /// + /// The number of errors recorded. + pub fn test_get_num_recorded_errors(&self, error: glean::ErrorType) -> i32 { + match self { + StringListMetric::Parent { inner, .. } => inner.test_get_num_recorded_errors(error), + StringListMetric::Child(c) => panic!( + "Cannot get the number of recorded errors for {:?} in non-parent process!", + c.0 + ), + } + } +} + +#[cfg(test)] +mod test { + use crate::{common_test::*, ipc, metrics}; + + #[test] + #[ignore] // TODO: Enable them back when bug 1677454 lands. + fn sets_string_list_value() { + let _lock = lock_test(); + + let metric = &metrics::test_only_ipc::a_string_list; + + metric.set(vec!["test_string_value".to_string()]); + metric.add("another test value"); + + assert_eq!( + vec!["test_string_value", "another test value"], + metric.test_get_value("store1").unwrap() + ); + } + + #[test] + #[ignore] // TODO: Enable them back when bug 1677454 lands. + fn string_list_ipc() { + // StringListMetric supports IPC only for `add`, not `set`. + let _lock = lock_test(); + + let parent_metric = &metrics::test_only_ipc::a_string_list; + + parent_metric.set(vec!["test_string_value".to_string()]); + parent_metric.add("another test value"); + + { + let child_metric = parent_metric.child_metric(); + + // scope for need_ipc RAII + let _raii = ipc::test_set_need_ipc(true); + + // Recording APIs do not panic, even when they don't work. + child_metric.set(vec!["not gonna be set".to_string()]); + + child_metric.add("child_value"); + assert!(ipc::take_buf().unwrap().len() > 0); + } + + // TODO: implement replay. See bug 1646165. + // Then perform the replay and assert we have the values from both "processes". + assert!(ipc::replay_from_buf(&ipc::take_buf().unwrap()).is_ok()); + assert_eq!( + vec!["test_string_value", "another test value"], + parent_metric.test_get_value("store1").unwrap() + ); + } +} diff --git a/toolkit/components/glean/api/src/private/text.rs b/toolkit/components/glean/api/src/private/text.rs new file mode 100644 index 0000000000..970ac419eb --- /dev/null +++ b/toolkit/components/glean/api/src/private/text.rs @@ -0,0 +1,179 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use inherent::inherent; +use std::sync::Arc; + +use super::{CommonMetricData, MetricId}; +use crate::ipc::need_ipc; + +/// A text metric. +/// +/// Record a string value with arbitrary content. Supports non-ASCII +/// characters. +/// +/// # Example +/// +/// The following piece of code will be generated by `glean_parser`: +/// +/// ```rust,ignore +/// use glean::metrics::{TextMetric, CommonMetricData, Lifetime}; +/// use once_cell::sync::Lazy; +/// +/// mod browser { +/// pub static bread_recipe: Lazy<TextMetric> = Lazy::new(|| TextMetric::new(CommonMetricData { +/// name: "bread_recipe".into(), +/// category: "browser".into(), +/// lifetime: Lifetime::Ping, +/// disabled: false, +/// dynamic_label: None +/// })); +/// } +/// ``` +/// +/// It can then be used with: +/// +/// ```rust,ignore +/// browser::bread_recipe.set("The 'baguette de tradition française' is made from wheat flour, water, yeast, and common salt. It may contain up to 2% broad bean flour, up to 0.5% soya flour, and up to 0.3% wheat malt flour."); +/// ``` + +#[derive(Clone)] +pub enum TextMetric { + Parent(Arc<glean::private::TextMetric>), + Child(TextMetricIpc), +} + +#[derive(Clone, Debug)] +pub struct TextMetricIpc; + +impl TextMetric { + /// Create a new text metric. + pub fn new(_id: MetricId, meta: CommonMetricData) -> Self { + if need_ipc() { + TextMetric::Child(TextMetricIpc) + } else { + TextMetric::Parent(Arc::new(glean::private::TextMetric::new(meta))) + } + } + + #[cfg(test)] + pub(crate) fn child_metric(&self) -> Self { + match self { + TextMetric::Parent(_) => TextMetric::Child(TextMetricIpc), + TextMetric::Child(_) => panic!("Can't get a child metric from a child process"), + } + } +} + +#[inherent] +impl glean::traits::Text for TextMetric { + /// Sets to the specified value. + /// + /// # Arguments + /// + /// * `value` - The text to set the metric to. + pub fn set<S: Into<std::string::String>>(&self, value: S) { + match self { + TextMetric::Parent(p) => { + p.set(value.into()); + } + TextMetric::Child(_) => { + log::error!("Unable to set text metric in non-main process. This operation will be ignored."); + // If we're in automation we can panic so the instrumentor knows they've gone wrong. + // This is a deliberate violation of Glean's "metric APIs must not throw" design. + assert!(!crate::ipc::is_in_automation(), "Attempted to set text metric in non-main process, which is forbidden. This panics in automation."); + } + } + } + + /// **Exported for test purposes.** + /// + /// Gets the currently stored value as a string. + /// + /// This doesn't clear the stored value. + /// + /// # Arguments + /// + /// * `ping_name` - represents the optional name of the ping to retrieve the + /// metric for. Defaults to the first value in `send_in_pings`. + pub fn test_get_value<'a, S: Into<Option<&'a str>>>( + &self, + ping_name: S, + ) -> Option<std::string::String> { + let ping_name = ping_name.into().map(|s| s.to_string()); + match self { + TextMetric::Parent(p) => p.test_get_value(ping_name), + TextMetric::Child(_) => { + panic!("Cannot get test value for text metric in non-main process!") + } + } + } + + /// **Exported for test purposes.** + /// + /// Gets the number of recorded errors for the given metric and error type. + /// + /// # Arguments + /// + /// * `error` - The type of error + /// * `ping_name` - represents the optional name of the ping to retrieve the + /// metric for. Defaults to the first value in `send_in_pings`. + /// + /// # Returns + /// + /// The number of errors reported. + pub fn test_get_num_recorded_errors(&self, error: glean::ErrorType) -> i32 { + match self { + TextMetric::Parent(p) => p.test_get_num_recorded_errors(error), + TextMetric::Child(_) => panic!( + "Cannot get the number of recorded errors for text metric in non-main process!" + ), + } + } +} + +#[cfg(test)] +mod test { + use crate::{common_test::*, ipc, metrics}; + + #[test] + fn sets_text_value() { + let _lock = lock_test(); + + let metric = &metrics::test_only_ipc::a_text; + + metric.set("test_text_value"); + + assert_eq!("test_text_value", metric.test_get_value("store1").unwrap()); + } + + #[test] + fn text_ipc() { + // TextMetric doesn't support IPC. + let _lock = lock_test(); + + let parent_metric = &metrics::test_only_ipc::a_text; + + parent_metric.set("test_parent_value"); + + { + let child_metric = parent_metric.child_metric(); + + let _raii = ipc::test_set_need_ipc(true); + + // Instrumentation calls do not panic. + child_metric.set("test_child_value"); + + // (They also shouldn't do anything, + // but that's not something we can inspect in this test) + } + + assert!(ipc::replay_from_buf(&ipc::take_buf().unwrap()).is_ok()); + + assert!( + "test_parent_value" == parent_metric.test_get_value("store1").unwrap(), + "Text metrics should only work in the parent process" + ); + } +} diff --git a/toolkit/components/glean/api/src/private/timespan.rs b/toolkit/components/glean/api/src/private/timespan.rs new file mode 100644 index 0000000000..f817d6996f --- /dev/null +++ b/toolkit/components/glean/api/src/private/timespan.rs @@ -0,0 +1,180 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use inherent::inherent; + +use super::{CommonMetricData, MetricId, TimeUnit}; +use std::convert::TryInto; +use std::time::Duration; + +use glean::traits::Timespan; + +use crate::ipc::need_ipc; + +/// A timespan metric. +/// +/// Timespans are used to make a measurement of how much time is spent in a particular task. +pub enum TimespanMetric { + Parent(glean::private::TimespanMetric, TimeUnit), + Child, +} + +impl TimespanMetric { + /// Create a new timespan metric. + pub fn new(_id: MetricId, meta: CommonMetricData, time_unit: TimeUnit) -> Self { + if need_ipc() { + TimespanMetric::Child + } else { + TimespanMetric::Parent( + glean::private::TimespanMetric::new(meta, time_unit), + time_unit, + ) + } + } + + /// Only to be called from the MLA FFI. + /// If you don't know what that is, don't call this. + pub fn set_raw_unitless(&self, duration: u64) { + match self { + TimespanMetric::Parent(p, time_unit) => { + p.set_raw(Duration::from_nanos(time_unit.as_nanos(duration))); + } + TimespanMetric::Child => { + log::error!( + "Unable to set_raw_unitless on timespan in non-main process. This operation will be ignored." + ); + // If we're in automation we can panic so the instrumentor knows they've gone wrong. + // This is a deliberate violation of Glean's "metric APIs must not throw" design. + assert!(!crate::ipc::is_in_automation(), "Attempted to set_raw_unitless on timespan metric in non-main process, which is forbidden. This panics in automation."); + // TODO: Record an error. bug 1704504. + } + } + } +} + +#[inherent] +impl Timespan for TimespanMetric { + pub fn start(&self) { + match self { + TimespanMetric::Parent(p, _) => p.start(), + TimespanMetric::Child => { + log::error!("Unable to start timespan metric in non-main process. This operation will be ignored."); + // If we're in automation we can panic so the instrumentor knows they've gone wrong. + // This is a deliberate violation of Glean's "metric APIs must not throw" design.assert!(!crate::ipc::is_in_automation()); + assert!(!crate::ipc::is_in_automation(), "Attempted to start timespan metric in non-main process, which is forbidden. This panics in automation."); + // TODO: Record an error. bug 1704504. + } + } + } + + pub fn stop(&self) { + match self { + TimespanMetric::Parent(p, _) => p.stop(), + TimespanMetric::Child => { + log::error!("Unable to stop timespan metric in non-main process. This operation will be ignored."); + // If we're in automation we can panic so the instrumentor knows they've gone wrong. + // This is a deliberate violation of Glean's "metric APIs must not throw" design.assert!(!crate::ipc::is_in_automation()); + assert!(!crate::ipc::is_in_automation(), "Attempted to stop timespan metric in non-main process, which is forbidden. This panics in automation."); + // TODO: Record an error. bug 1704504. + } + } + } + + pub fn cancel(&self) { + match self { + TimespanMetric::Parent(p, _) => p.cancel(), + TimespanMetric::Child => { + log::error!("Unable to cancel timespan metric in non-main process. This operation will be ignored."); + // If we're in automation we can panic so the instrumentor knows they've gone wrong. + // This is a deliberate violation of Glean's "metric APIs must not throw" design.assert!(!crate::ipc::is_in_automation()); + assert!(!crate::ipc::is_in_automation(), "Attempted to cancel timespan metric in non-main process, which is forbidden. This panics in automation."); + // TODO: Record an error. bug 1704504. + } + } + } + + pub fn set_raw(&self, elapsed: Duration) { + let elapsed = elapsed.as_nanos().try_into().unwrap_or(i64::MAX); + match self { + TimespanMetric::Parent(p, _) => p.set_raw_nanos(elapsed), + TimespanMetric::Child => { + log::error!("Unable to set_raw on timespan in non-main process. This operation will be ignored."); + // If we're in automation we can panic so the instrumentor knows they've gone wrong. + // This is a deliberate violation of Glean's "metric APIs must not throw" design.assert!(!crate::ipc::is_in_automation()); + assert!(!crate::ipc::is_in_automation(), "Attempted to set_raw on timespan metric in non-main process, which is forbidden. This panics in automation."); + // TODO: Record an error. bug 1704504. + } + } + } + + pub fn test_get_value<'a, S: Into<Option<&'a str>>>(&self, ping_name: S) -> Option<u64> { + let ping_name = ping_name.into().map(|s| s.to_string()); + match self { + // Conversion is ok here: + // Timespans are really tricky to set to excessive values with the pleasant APIs. + TimespanMetric::Parent(p, _) => p.test_get_value(ping_name).map(|i| i as u64), + TimespanMetric::Child => { + panic!("Cannot get test value for in non-main process!"); + } + } + } + + pub fn test_get_num_recorded_errors(&self, error: glean::ErrorType) -> i32 { + match self { + TimespanMetric::Parent(p, _) => p.test_get_num_recorded_errors(error), + TimespanMetric::Child => { + panic!("Cannot get the number of recorded errors for timespan metric in non-main process!"); + } + } + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::{common_test::*, ipc, metrics}; + + #[test] + fn smoke_test_timespan() { + let _lock = lock_test(); + + let metric = TimespanMetric::new( + 0.into(), + CommonMetricData { + name: "timespan_metric".into(), + category: "telemetry".into(), + send_in_pings: vec!["store1".into()], + disabled: false, + ..Default::default() + }, + TimeUnit::Nanosecond, + ); + + metric.start(); + // Stopping right away might not give us data, if the underlying clock source is not precise + // enough. + // So let's cancel and make sure nothing blows up. + metric.cancel(); + + assert_eq!(None, metric.test_get_value("store1")); + } + + #[test] + fn timespan_ipc() { + let _lock = lock_test(); + let _raii = ipc::test_set_need_ipc(true); + + let child_metric = &metrics::test_only::can_we_time_it; + + // Instrumentation calls do not panic. + child_metric.start(); + // Stopping right away might not give us data, + // if the underlying clock source is not precise enough. + // So let's cancel and make sure nothing blows up. + child_metric.cancel(); + + // (They also shouldn't do anything, + // but that's not something we can inspect in this test) + } +} diff --git a/toolkit/components/glean/api/src/private/timing_distribution.rs b/toolkit/components/glean/api/src/private/timing_distribution.rs new file mode 100644 index 0000000000..0ab25cc900 --- /dev/null +++ b/toolkit/components/glean/api/src/private/timing_distribution.rs @@ -0,0 +1,487 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use inherent::inherent; +use std::collections::HashMap; +use std::convert::TryInto; +use std::sync::{ + atomic::{AtomicUsize, Ordering}, + RwLock, +}; +use std::time::{Duration, Instant}; + +use super::{CommonMetricData, MetricId, TimeUnit}; +use glean::{DistributionData, ErrorType, TimerId}; + +use crate::ipc::{need_ipc, with_ipc_payload}; +use glean::traits::TimingDistribution; + +/// A timing distribution metric. +/// +/// Timing distributions are used to accumulate and store time measurements for analyzing distributions of the timing data. +pub enum TimingDistributionMetric { + Parent { + /// The metric's ID. + /// + /// No longer test-only, is also used for GIFFT. + id: MetricId, + inner: glean::private::TimingDistributionMetric, + }, + Child(TimingDistributionMetricIpc), +} +#[derive(Debug)] +pub struct TimingDistributionMetricIpc { + metric_id: MetricId, + next_timer_id: AtomicUsize, + instants: RwLock<HashMap<u64, Instant>>, +} + +impl TimingDistributionMetric { + /// Create a new timing distribution metric. + pub fn new(id: MetricId, meta: CommonMetricData, time_unit: TimeUnit) -> Self { + if need_ipc() { + TimingDistributionMetric::Child(TimingDistributionMetricIpc { + metric_id: id, + next_timer_id: AtomicUsize::new(0), + instants: RwLock::new(HashMap::new()), + }) + } else { + let inner = glean::private::TimingDistributionMetric::new(meta, time_unit); + TimingDistributionMetric::Parent { id, inner } + } + } + + #[cfg(test)] + pub(crate) fn child_metric(&self) -> Self { + match self { + TimingDistributionMetric::Parent { id, .. } => { + TimingDistributionMetric::Child(TimingDistributionMetricIpc { + metric_id: *id, + next_timer_id: AtomicUsize::new(0), + instants: RwLock::new(HashMap::new()), + }) + } + TimingDistributionMetric::Child(_) => { + panic!("Can't get a child metric from a child metric") + } + } + } + + /// Accumulates a time duration sample for the provided metric. + /// + /// Adds a count to the corresponding bucket in the timing distribution. + /// Saturates at u64::MAX nanoseconds. + /// + /// Prefer start() and stop_and_accumulate() where possible. + /// + /// Users of this API are responsible for ensuring the timing source used + /// to calculate the duration is monotonic and consistent across platforms. + /// + /// # Arguments + /// + /// * `duration` - The [`Duration`] of the accumulated sample. + pub fn accumulate_raw_duration(&self, duration: Duration) { + let sample = duration.as_nanos().try_into().unwrap_or_else(|_| { + // TODO: Instrument this error + log::warn!( + "Elapsed nanoseconds larger than fits into 64-bytes. Saturating at u64::MAX." + ); + u64::MAX + }); + // May be unused in builds without gecko. + let _sample_ms = duration.as_millis().try_into().unwrap_or_else(|_| { + // TODO: Instrument this error + log::warn!( + "Elapsed milliseconds larger than fits into 32-bytes. Saturating at u32::MAX." + ); + u32::MAX + }); + match self { + TimingDistributionMetric::Parent { + id: _metric_id, + inner, + } => { + #[cfg(feature = "with_gecko")] + { + extern "C" { + fn GIFFT_TimingDistributionAccumulateRawMillis(metric_id: u32, sample: u32); + } + // SAFETY: using only primitives, no return value. + unsafe { + GIFFT_TimingDistributionAccumulateRawMillis(_metric_id.0, _sample_ms); + } + } + inner.accumulate_raw_samples_nanos(vec![sample]); + } + TimingDistributionMetric::Child(c) => { + #[cfg(feature = "with_gecko")] + { + extern "C" { + fn GIFFT_TimingDistributionAccumulateRawMillis(metric_id: u32, sample: u32); + } + // SAFETY: using only primitives, no return value. + unsafe { + GIFFT_TimingDistributionAccumulateRawMillis(c.metric_id.0, _sample_ms); + } + } + with_ipc_payload(move |payload| { + if let Some(v) = payload.timing_samples.get_mut(&c.metric_id) { + v.push(sample); + } else { + payload.timing_samples.insert(c.metric_id, vec![sample]); + } + }); + } + } + } +} + +#[inherent] +impl TimingDistribution for TimingDistributionMetric { + /// Starts tracking time for the provided metric. + /// + /// This records an error if it’s already tracking time (i.e. + /// [`start`](TimingDistribution::start) was already called with no corresponding + /// [`stop_and_accumulate`](TimingDistribution::stop_and_accumulate)): in that case the + /// original start time will be preserved. + /// + /// # Returns + /// + /// A unique [`TimerId`] for the new timer. + pub fn start(&self) -> TimerId { + match self { + TimingDistributionMetric::Parent { id: _id, inner } => { + let timer_id = inner.start(); + #[cfg(feature = "with_gecko")] + { + extern "C" { + fn GIFFT_TimingDistributionStart(metric_id: u32, timer_id: u64); + } + // SAFETY: using only primitives, no return value. + unsafe { + GIFFT_TimingDistributionStart(_id.0, timer_id.id); + } + } + timer_id.into() + } + TimingDistributionMetric::Child(c) => { + // There is no glean-core on this process to give us a TimerId, + // so we'll have to make our own and do our own bookkeeping. + let id = c + .next_timer_id + .fetch_add(1, Ordering::SeqCst) + .try_into() + .unwrap(); + let mut map = c + .instants + .write() + .expect("lock of instants map was poisoned"); + if let Some(_v) = map.insert(id, Instant::now()) { + // TODO: report an error and find a different TimerId. + } + #[cfg(feature = "with_gecko")] + { + extern "C" { + fn GIFFT_TimingDistributionStart(metric_id: u32, timer_id: u64); + } + // SAFETY: using only primitives, no return value. + unsafe { + GIFFT_TimingDistributionStart(c.metric_id.0, id); + } + } + id.into() + } + } + } + + /// Stops tracking time for the provided metric and associated timer id. + /// + /// Adds a count to the corresponding bucket in the timing distribution. + /// This will record an error if no [`start`](TimingDistribution::start) was + /// called. + /// + /// # Arguments + /// + /// * `id` - The [`TimerId`] to associate with this timing. This allows + /// for concurrent timing of events associated with different ids to the + /// same timespan metric. + pub fn stop_and_accumulate(&self, id: TimerId) { + match self { + TimingDistributionMetric::Parent { + id: _metric_id, + inner, + } => { + #[cfg(feature = "with_gecko")] + { + extern "C" { + fn GIFFT_TimingDistributionStopAndAccumulate(metric_id: u32, timer_id: u64); + } + // SAFETY: using only primitives, no return value. + unsafe { + GIFFT_TimingDistributionStopAndAccumulate(_metric_id.0, id.id); + } + } + inner.stop_and_accumulate(id); + } + TimingDistributionMetric::Child(c) => { + #[cfg(feature = "with_gecko")] + { + extern "C" { + fn GIFFT_TimingDistributionStopAndAccumulate(metric_id: u32, timer_id: u64); + } + // SAFETY: using only primitives, no return value. + unsafe { + GIFFT_TimingDistributionStopAndAccumulate(c.metric_id.0, id.id); + } + } + let mut map = c + .instants + .write() + .expect("Write lock must've been poisoned."); + if let Some(start) = map.remove(&id.id) { + let now = Instant::now(); + let sample = now + .checked_duration_since(start) + .map(|s| s.as_nanos().try_into()); + let sample = match sample { + Some(Ok(sample)) => sample, + Some(Err(_)) => { + log::warn!("Elapsed time larger than fits into 64-bytes. Saturating at u64::MAX."); + u64::MAX + } + None => { + log::warn!("Time went backwards. Not recording."); + // TODO: report an error (timer id for stop was started, but time went backwards). + return; + } + }; + with_ipc_payload(move |payload| { + if let Some(v) = payload.timing_samples.get_mut(&c.metric_id) { + v.push(sample); + } else { + payload.timing_samples.insert(c.metric_id, vec![sample]); + } + }); + } else { + // TODO: report an error (timer id for stop wasn't started). + } + } + } + } + + /// Aborts a previous [`start`](TimingDistribution::start) call. No + /// error is recorded if no [`start`](TimingDistribution::start) was + /// called. + /// + /// # Arguments + /// + /// * `id` - The [`TimerId`] to associate with this timing. This allows + /// for concurrent timing of events associated with different ids to the + /// same timing distribution metric. + pub fn cancel(&self, id: TimerId) { + match self { + TimingDistributionMetric::Parent { + id: _metric_id, + inner, + } => { + #[cfg(feature = "with_gecko")] + { + extern "C" { + fn GIFFT_TimingDistributionCancel(metric_id: u32, timer_id: u64); + } + // SAFETY: using only primitives, no return value. + unsafe { + GIFFT_TimingDistributionCancel(_metric_id.0, id.id); + } + } + inner.cancel(id); + } + TimingDistributionMetric::Child(c) => { + let mut map = c + .instants + .write() + .expect("Write lock must've been poisoned."); + if map.remove(&id.id).is_none() { + // TODO: report an error (cancelled a non-started id). + } + #[cfg(feature = "with_gecko")] + { + extern "C" { + fn GIFFT_TimingDistributionCancel(metric_id: u32, timer_id: u64); + } + // SAFETY: using only primitives, no return value. + unsafe { + GIFFT_TimingDistributionCancel(c.metric_id.0, id.id); + } + } + } + } + } + + /// Accumulates the provided signed samples in the metric. + /// + /// This is required so that the platform-specific code can provide us with + /// 64 bit signed integers if no `u64` comparable type is available. This + /// will take care of filtering and reporting errors for any provided negative + /// sample. + /// + /// Please note that this assumes that the provided samples are already in + /// the "unit" declared by the instance of the metric type (e.g. if the + /// instance this method was called on is using [`crate::TimeUnit::Second`], then + /// `samples` are assumed to be in that unit). + /// + /// # Arguments + /// + /// * `samples` - The vector holding the samples to be recorded by the metric. + /// + /// ## Notes + /// + /// Discards any negative value in `samples` and report an [`ErrorType::InvalidValue`] + /// for each of them. Reports an [`ErrorType::InvalidOverflow`] error for samples that + /// are longer than `MAX_SAMPLE_TIME`. + pub fn accumulate_samples(&self, samples: Vec<i64>) { + match self { + TimingDistributionMetric::Parent { id: _id, inner } => { + inner.accumulate_samples(samples) + } + TimingDistributionMetric::Child(_c) => { + // TODO: Instrument this error + log::error!("Can't record samples for a timing distribution from a child metric"); + } + } + } + + /// Accumulates the provided samples in the metric. + /// + /// # Arguments + /// + /// * `samples` - A list of samples recorded by the metric. + /// Samples must be in nanoseconds. + /// ## Notes + /// + /// Reports an [`ErrorType::InvalidOverflow`] error for samples that + /// are longer than `MAX_SAMPLE_TIME`. + pub fn accumulate_raw_samples_nanos(&self, samples: Vec<u64>) { + match self { + TimingDistributionMetric::Parent { id: _id, inner } => { + inner.accumulate_raw_samples_nanos(samples) + } + TimingDistributionMetric::Child(_c) => { + // TODO: Instrument this error + log::error!("Can't record samples for a timing distribution from a child metric"); + } + } + } + + /// **Exported for test purposes.** + /// + /// Gets the currently stored value of the metric. + /// + /// This doesn't clear the stored value. + /// + /// # Arguments + /// + /// * `ping_name` - represents the optional name of the ping to retrieve the + /// metric for. Defaults to the first value in `send_in_pings`. + pub fn test_get_value<'a, S: Into<Option<&'a str>>>( + &self, + ping_name: S, + ) -> Option<DistributionData> { + let ping_name = ping_name.into().map(|s| s.to_string()); + match self { + TimingDistributionMetric::Parent { inner, .. } => inner.test_get_value(ping_name), + TimingDistributionMetric::Child(c) => { + panic!("Cannot get test value for {:?} in non-parent process!", c) + } + } + } + + /// **Exported for test purposes.** + /// + /// Gets the number of recorded errors for the given error type. + /// + /// # Arguments + /// + /// * `error` - The type of error + /// * `ping_name` - represents the optional name of the ping to retrieve the + /// metric for. Defaults to the first value in `send_in_pings`. + /// + /// # Returns + /// + /// The number of errors recorded. + pub fn test_get_num_recorded_errors(&self, error: ErrorType) -> i32 { + match self { + TimingDistributionMetric::Parent { inner, .. } => { + inner.test_get_num_recorded_errors(error) + } + TimingDistributionMetric::Child(c) => panic!( + "Cannot get number of recorded errors for {:?} in non-parent process!", + c + ), + } + } +} + +#[cfg(test)] +mod test { + use crate::{common_test::*, ipc, metrics}; + + #[test] + fn smoke_test_timing_distribution() { + let _lock = lock_test(); + + let metric = &metrics::test_only_ipc::a_timing_dist; + + let id = metric.start(); + // Stopping right away might not give us data, if the underlying clock source is not precise + // enough. + // So let's cancel and make sure nothing blows up. + metric.cancel(id); + + // We can't inspect the values yet. + assert!(metric.test_get_value("store1").is_none()); + } + + #[test] + fn timing_distribution_child() { + let _lock = lock_test(); + + let parent_metric = &metrics::test_only_ipc::a_timing_dist; + let id = parent_metric.start(); + std::thread::sleep(std::time::Duration::from_millis(10)); + parent_metric.stop_and_accumulate(id); + + { + let child_metric = parent_metric.child_metric(); + + // scope for need_ipc RAII + let _raii = ipc::test_set_need_ipc(true); + + let id = child_metric.start(); + let id2 = child_metric.start(); + assert_ne!(id, id2); + std::thread::sleep(std::time::Duration::from_millis(10)); + child_metric.stop_and_accumulate(id); + + child_metric.cancel(id2); + } + + let buf = ipc::take_buf().unwrap(); + assert!(buf.len() > 0); + assert!(ipc::replay_from_buf(&buf).is_ok()); + + let data = parent_metric + .test_get_value("store1") + .expect("should have some data"); + + // No guarantees from timers means no guarantees on buckets. + // But we can guarantee it's only two samples. + assert_eq!( + 2, + data.values.values().fold(0, |acc, count| acc + count), + "record 2 values, one parent, one child measurement" + ); + assert!(0 < data.sum, "record some time"); + } +} diff --git a/toolkit/components/glean/api/src/private/url.rs b/toolkit/components/glean/api/src/private/url.rs new file mode 100644 index 0000000000..09f21dad5e --- /dev/null +++ b/toolkit/components/glean/api/src/private/url.rs @@ -0,0 +1,129 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use inherent::inherent; + +use super::{CommonMetricData, MetricId}; + +use crate::ipc::need_ipc; + +/// Developer-facing API for recording URL metrics. +/// +/// Instances of this class type are automatically generated by the parsers +/// at build time, allowing developers to record values that were previously +/// registered in the metrics.yaml file. +#[derive(Clone)] +pub enum UrlMetric { + Parent(glean::private::UrlMetric), + Child(UrlMetricIpc), +} +#[derive(Clone, Debug)] +pub struct UrlMetricIpc; + +impl UrlMetric { + /// Create a new Url metric. + pub fn new(_id: MetricId, meta: CommonMetricData) -> Self { + if need_ipc() { + UrlMetric::Child(UrlMetricIpc) + } else { + UrlMetric::Parent(glean::private::UrlMetric::new(meta)) + } + } + + #[cfg(test)] + pub(crate) fn child_metric(&self) -> Self { + match self { + UrlMetric::Parent(_) => UrlMetric::Child(UrlMetricIpc), + UrlMetric::Child(_) => panic!("Can't get a child metric from a child metric"), + } + } +} + +#[inherent] +impl glean::traits::Url for UrlMetric { + pub fn set<S: Into<std::string::String>>(&self, value: S) { + match self { + UrlMetric::Parent(p) => p.set(value), + UrlMetric::Child(_) => { + log::error!( + "Unable to set Url metric in non-main process. This operation will be ignored." + ); + // If we're in automation we can panic so the instrumentor knows they've gone wrong. + // This is a deliberate violation of Glean's "metric APIs must not throw" design. + assert!(!crate::ipc::is_in_automation(), "Attempted to set URL metric in non-main process, which is forbidden. This panics in automation."); + // TODO: Record an error. + } + }; + } + + pub fn test_get_value<'a, S: Into<Option<&'a str>>>( + &self, + ping_name: S, + ) -> Option<std::string::String> { + let ping_name = ping_name.into().map(|s| s.to_string()); + match self { + UrlMetric::Parent(p) => p.test_get_value(ping_name), + UrlMetric::Child(_) => { + panic!("Cannot get test value for Url metric in non-main process!") + } + } + } + + pub fn test_get_num_recorded_errors(&self, error: glean::ErrorType) -> i32 { + match self { + UrlMetric::Parent(p) => p.test_get_num_recorded_errors(error), + UrlMetric::Child(_) => panic!( + "Cannot get the number of recorded errors for Url metric in non-main process!" + ), + } + } +} + +#[cfg(test)] +mod test { + use crate::{common_test::*, ipc, metrics}; + + #[test] + fn sets_url_value() { + let _lock = lock_test(); + + let metric = &metrics::test_only_ipc::a_url; + + metric.set("https://example.com"); + + assert_eq!( + "https://example.com", + metric.test_get_value("store1").unwrap() + ); + } + + #[test] + fn url_ipc() { + // UrlMetric doesn't support IPC. + let _lock = lock_test(); + + let parent_metric = &metrics::test_only_ipc::a_url; + + parent_metric.set("https://example.com/parent"); + + { + let child_metric = parent_metric.child_metric(); + + let _raii = ipc::test_set_need_ipc(true); + + // Instrumentation calls do not panic. + child_metric.set("https://example.com/child"); + + // (They also shouldn't do anything, + // but that's not something we can inspect in this test) + } + + assert!(ipc::replay_from_buf(&ipc::take_buf().unwrap()).is_ok()); + + assert!( + "https://example.com/parent" == parent_metric.test_get_value("store1").unwrap(), + "Url metrics should only work in the parent process" + ); + } +} diff --git a/toolkit/components/glean/api/src/private/uuid.rs b/toolkit/components/glean/api/src/private/uuid.rs new file mode 100644 index 0000000000..101a1a9875 --- /dev/null +++ b/toolkit/components/glean/api/src/private/uuid.rs @@ -0,0 +1,171 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use inherent::inherent; + +use uuid::Uuid; + +use super::{CommonMetricData, MetricId}; + +use crate::ipc::need_ipc; + +/// A UUID metric. +/// +/// Stores UUID values. +pub enum UuidMetric { + Parent(glean::private::UuidMetric), + Child(UuidMetricIpc), +} + +#[derive(Debug)] +pub struct UuidMetricIpc; + +impl UuidMetric { + /// Create a new UUID metric. + pub fn new(_id: MetricId, meta: CommonMetricData) -> Self { + if need_ipc() { + UuidMetric::Child(UuidMetricIpc) + } else { + UuidMetric::Parent(glean::private::UuidMetric::new(meta)) + } + } + + #[cfg(test)] + pub(crate) fn child_metric(&self) -> Self { + match self { + UuidMetric::Parent(_) => UuidMetric::Child(UuidMetricIpc), + UuidMetric::Child(_) => panic!("Can't get a child metric from a child metric"), + } + } +} + +#[inherent] +impl glean::traits::Uuid for UuidMetric { + /// Set to the specified value. + /// + /// ## Arguments + /// + /// * `value` - The UUID to set the metric to. + pub fn set(&self, value: Uuid) { + match self { + UuidMetric::Parent(p) => p.set(value.to_string()), + UuidMetric::Child(_c) => { + log::error!("Unable to set the uuid metric in non-main process. This operation will be ignored."); + // If we're in automation we can panic so the instrumentor knows they've gone wrong. + // This is a deliberate violation of Glean's "metric APIs must not throw" design. + assert!(!crate::ipc::is_in_automation(), "Attempted to set uuid metric in non-main process, which is forbidden. This panics in automation."); + // TODO: Record an error. + } + }; + } + + /// Generate a new random UUID and set the metric to it. + /// + /// ## Return value + /// + /// Returns the stored UUID value or `Uuid::nil` if called from + /// a non-main process. + pub fn generate_and_set(&self) -> Uuid { + match self { + UuidMetric::Parent(p) => Uuid::parse_str(&p.generate_and_set()).unwrap(), + UuidMetric::Child(_c) => { + log::error!("Unable to set the uuid metric in non-main process. This operation will be ignored."); + // If we're in automation we can panic so the instrumentor knows they've gone wrong. + // This is a deliberate violation of Glean's "metric APIs must not throw" design. + assert!(!crate::ipc::is_in_automation(), "Attempted to set uuid metric in non-main process, which is forbidden. This panics in automation."); + // TODO: Record an error. + Uuid::nil() + } + } + } + + /// **Test-only API.** + /// + /// Get the stored UUID value. + /// This doesn't clear the stored value. + /// + /// ## Arguments + /// + /// * `storage_name` - the storage name to look into. + /// + /// ## Return value + /// + /// Returns the stored value or `None` if nothing stored. + pub fn test_get_value<'a, S: Into<Option<&'a str>>>(&self, storage_name: S) -> Option<Uuid> { + let storage_name = storage_name.into().map(|s| s.to_string()); + match self { + UuidMetric::Parent(p) => p + .test_get_value(storage_name) + .and_then(|s| Uuid::parse_str(&s).ok()), + UuidMetric::Child(_c) => panic!("Cannot get test value for in non-main process!"), + } + } + + /// **Exported for test purposes.** + /// + /// Gets the number of recorded errors for the given metric and error type. + /// + /// # Arguments + /// + /// * `error` - The type of error + /// * `ping_name` - represents the optional name of the ping to retrieve the + /// metric for. Defaults to the first value in `send_in_pings`. + /// + /// # Returns + /// + /// The number of errors reported. + pub fn test_get_num_recorded_errors(&self, error: glean::ErrorType) -> i32 { + match self { + UuidMetric::Parent(p) => p.test_get_num_recorded_errors(error), + UuidMetric::Child(_c) => { + panic!("Cannot get test value for UuidMetric in non-main process!") + } + } + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::{common_test::*, ipc, metrics}; + + #[test] + fn sets_uuid_value() { + let _lock = lock_test(); + + let metric = &metrics::test_only_ipc::a_uuid; + let expected = Uuid::new_v4(); + metric.set(expected.clone()); + + assert_eq!(expected, metric.test_get_value("store1").unwrap()); + } + + #[test] + fn uuid_ipc() { + // UuidMetric doesn't support IPC. + let _lock = lock_test(); + + let parent_metric = &metrics::test_only_ipc::a_uuid; + let expected = Uuid::new_v4(); + parent_metric.set(expected.clone()); + + { + let child_metric = parent_metric.child_metric(); + + // Instrumentation calls do not panic. + child_metric.set(Uuid::new_v4()); + + // (They also shouldn't do anything, + // but that's not something we can inspect in this test) + } + + assert!(ipc::replay_from_buf(&ipc::take_buf().unwrap()).is_ok()); + + assert_eq!( + expected, + parent_metric.test_get_value("store1").unwrap(), + "UUID metrics should only work in the parent process" + ); + } +} |