diff options
Diffstat (limited to 'toolkit/components/glean/api')
49 files changed, 5841 insertions, 0 deletions
diff --git a/toolkit/components/glean/api/Cargo.toml b/toolkit/components/glean/api/Cargo.toml new file mode 100644 index 0000000000..e8e9ad2199 --- /dev/null +++ b/toolkit/components/glean/api/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "firefox-on-glean" +version = "0.1.0" +authors = ["Glean SDK team <glean-team@mozilla.com>"] +edition = "2018" +publish = false +license = "MPL-2.0" + +[dependencies] +bincode = "1.0" +chrono = "0.4.10" +glean = "52.7.0" +inherent = "1.0.0" +log = "0.4" +nsstring = { path = "../../../../xpcom/rust/nsstring", optional = true } +once_cell = "1.2.0" +serde = { version = "1.0", features = ["derive"] } +uuid = { version = "1.0", features = ["v4"] } +xpcom = { path = "../../../../xpcom/rust/xpcom", optional = true } +thin-vec = { version = "0.2.1", features = ["gecko-ffi"] } +mozbuild = "0.1" + +[dev-dependencies] +tempfile = "3.1.0" + +[features] +with_gecko = ["xpcom", "nsstring"] diff --git a/toolkit/components/glean/api/src/common_test.rs b/toolkit/components/glean/api/src/common_test.rs new file mode 100644 index 0000000000..aeb13da67f --- /dev/null +++ b/toolkit/components/glean/api/src/common_test.rs @@ -0,0 +1,54 @@ +// 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::sync::{Mutex, MutexGuard}; + +use once_cell::sync::Lazy; + +const GLOBAL_APPLICATION_ID: &str = "org.mozilla.firefox.test"; + +/// UGLY HACK. +/// We use a global lock to force synchronization of all tests, even if run multi-threaded. +/// This allows us to run without `--test-threads 1`.` +pub fn lock_test() -> (MutexGuard<'static, ()>, tempfile::TempDir) { + static GLOBAL_LOCK: Lazy<Mutex<()>> = Lazy::new(|| Mutex::new(())); + + let lock = GLOBAL_LOCK.lock().unwrap(); + + let dir = setup_glean(None); + (lock, dir) +} + +// Create a new instance of Glean with a temporary directory. +// We need to keep the `TempDir` alive, so that it's not deleted before we stop using it. +fn setup_glean(tempdir: Option<tempfile::TempDir>) -> tempfile::TempDir { + let dir = match tempdir { + Some(tempdir) => tempdir, + None => tempfile::tempdir().unwrap(), + }; + let tmpname = dir.path().to_path_buf(); + + let cfg = glean::Configuration { + upload_enabled: true, + data_path: tmpname, + application_id: GLOBAL_APPLICATION_ID.into(), + max_events: None, + delay_ping_lifetime_io: false, + server_endpoint: None, + uploader: None, + use_core_mps: false, + trim_data_to_registered_pings: false, + log_level: None, + }; + + let client_info = glean::ClientInfoMetrics { + app_build: "test-build".into(), + app_display_version: "1.2.3".into(), + channel: None, + }; + + glean::test_reset_glean(cfg, client_info, true); + + dir +} diff --git a/toolkit/components/glean/api/src/factory.rs b/toolkit/components/glean/api/src/factory.rs new file mode 100644 index 0000000000..f7984f4c46 --- /dev/null +++ b/toolkit/components/glean/api/src/factory.rs @@ -0,0 +1,16 @@ +// 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/. + +//! This file contains the Generated JOG Factory for the runtime-registration +//! of Glean metrics in Firefox on Glean. +//! You probably should just ignore stuff in here and ask on the +//! [#glean Matrix channel](https://chat.mozilla.org/#/room/#glean:mozilla.org) +//! if you have questions. +//! +//! Most of the contents of this module are generated by +//! `toolkit/components/glean/build_scripts/glean_parser_ext/run_glean_parser.py` + +include!(mozbuild::objdir_path!( + "toolkit/components/glean/api/src/factory.rs" +)); diff --git a/toolkit/components/glean/api/src/ffi/boolean.rs b/toolkit/components/glean/api/src/ffi/boolean.rs new file mode 100644 index 0000000000..9184b23c00 --- /dev/null +++ b/toolkit/components/glean/api/src/ffi/boolean.rs @@ -0,0 +1,28 @@ +// 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/. + +#![cfg(feature = "with_gecko")] + +use nsstring::nsACString; + +#[no_mangle] +pub extern "C" fn fog_boolean_test_has_value(id: u32, ping_name: &nsACString) -> bool { + with_metric!(BOOLEAN_MAP, id, metric, test_has!(metric, ping_name)) +} + +#[no_mangle] +pub extern "C" fn fog_boolean_test_get_value(id: u32, ping_name: &nsACString) -> bool { + with_metric!(BOOLEAN_MAP, id, metric, test_get!(metric, ping_name)) +} + +#[no_mangle] +pub extern "C" fn fog_boolean_test_get_error(id: u32, error_str: &mut nsACString) -> bool { + let err = with_metric!(BOOLEAN_MAP, id, metric, test_get_errors!(metric)); + err.map(|err_str| error_str.assign(&err_str)).is_some() +} + +#[no_mangle] +pub extern "C" fn fog_boolean_set(id: u32, value: bool) { + with_metric!(BOOLEAN_MAP, id, metric, metric.set(value)); +} diff --git a/toolkit/components/glean/api/src/ffi/counter.rs b/toolkit/components/glean/api/src/ffi/counter.rs new file mode 100644 index 0000000000..5fba7c0dea --- /dev/null +++ b/toolkit/components/glean/api/src/ffi/counter.rs @@ -0,0 +1,28 @@ +// 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/. + +#![cfg(feature = "with_gecko")] + +use nsstring::nsACString; + +#[no_mangle] +pub unsafe extern "C" fn fog_counter_add(id: u32, amount: i32) { + with_metric!(COUNTER_MAP, id, metric, metric.add(amount)); +} + +#[no_mangle] +pub unsafe extern "C" fn fog_counter_test_has_value(id: u32, ping_name: &nsACString) -> bool { + with_metric!(COUNTER_MAP, id, metric, test_has!(metric, ping_name)) +} + +#[no_mangle] +pub unsafe extern "C" fn fog_counter_test_get_value(id: u32, ping_name: &nsACString) -> i32 { + with_metric!(COUNTER_MAP, id, metric, test_get!(metric, ping_name)) +} + +#[no_mangle] +pub extern "C" fn fog_counter_test_get_error(id: u32, error_str: &mut nsACString) -> bool { + let err = with_metric!(COUNTER_MAP, id, metric, test_get_errors!(metric)); + err.map(|err_str| error_str.assign(&err_str)).is_some() +} diff --git a/toolkit/components/glean/api/src/ffi/custom_distribution.rs b/toolkit/components/glean/api/src/ffi/custom_distribution.rs new file mode 100644 index 0000000000..853a6e9845 --- /dev/null +++ b/toolkit/components/glean/api/src/ffi/custom_distribution.rs @@ -0,0 +1,82 @@ +// 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/. + +#![cfg(feature = "with_gecko")] + +use nsstring::nsACString; +use thin_vec::ThinVec; + +#[no_mangle] +pub extern "C" fn fog_custom_distribution_test_has_value(id: u32, ping_name: &nsACString) -> bool { + with_metric!( + CUSTOM_DISTRIBUTION_MAP, + id, + metric, + test_has!(metric, ping_name) + ) +} + +#[no_mangle] +pub extern "C" fn fog_custom_distribution_test_get_value( + id: u32, + ping_name: &nsACString, + sum: &mut u64, + buckets: &mut ThinVec<u64>, + counts: &mut ThinVec<u64>, +) { + let val = with_metric!( + CUSTOM_DISTRIBUTION_MAP, + id, + metric, + test_get!(metric, ping_name) + ); + // FIXME(bug 1771885): Glean should use `u64` where it can. + *sum = val.sum as _; + for (&bucket, &count) in val.values.iter() { + buckets.push(bucket as _); + counts.push(count as _); + } +} + +#[no_mangle] +pub extern "C" fn fog_custom_distribution_accumulate_samples(id: u32, samples: &ThinVec<u64>) { + // N.B.: Avoid reallocation here by making the underlying type take a slice. + let samples = samples.into_iter().map(|&i| i as i64).collect(); + with_metric!( + CUSTOM_DISTRIBUTION_MAP, + id, + metric, + metric.accumulate_samples_signed(samples) + ); +} + +#[no_mangle] +pub extern "C" fn fog_custom_distribution_accumulate_samples_signed( + id: u32, + samples: &ThinVec<i64>, +) { + // N.B.: Avoid reallocation here by making the underlying type take a slice. + let samples = samples.to_vec(); + with_metric!( + CUSTOM_DISTRIBUTION_MAP, + id, + metric, + metric.accumulate_samples_signed(samples) + ); +} + +#[no_mangle] +pub extern "C" fn fog_custom_distribution_test_get_error( + id: u32, + + error_str: &mut nsACString, +) -> bool { + let err = with_metric!( + CUSTOM_DISTRIBUTION_MAP, + id, + metric, + test_get_errors!(metric) + ); + err.map(|err_str| error_str.assign(&err_str)).is_some() +} diff --git a/toolkit/components/glean/api/src/ffi/datetime.rs b/toolkit/components/glean/api/src/ffi/datetime.rs new file mode 100644 index 0000000000..7529a524e6 --- /dev/null +++ b/toolkit/components/glean/api/src/ffi/datetime.rs @@ -0,0 +1,66 @@ +// 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/. + +#![cfg(feature = "with_gecko")] + +use nsstring::nsACString; + +#[repr(C)] +pub struct FogDatetime { + year: i32, + month: u32, + day: u32, + hour: u32, + minute: u32, + second: u32, + nano: u32, + offset_seconds: i32, +} + +#[no_mangle] +pub extern "C" fn fog_datetime_test_has_value(id: u32, ping_name: &nsACString) -> bool { + with_metric!(DATETIME_MAP, id, metric, test_has!(metric, ping_name)) +} + +#[no_mangle] +pub extern "C" fn fog_datetime_test_get_value( + id: u32, + ping_name: &nsACString, + value: &mut FogDatetime, +) { + let val = with_metric!(DATETIME_MAP, id, metric, test_get!(metric, ping_name)); + value.year = val.year; + value.month = val.month; + value.day = val.day; + value.hour = val.hour; + value.minute = val.minute; + value.second = val.second; + value.nano = val.nanosecond; + value.offset_seconds = val.offset_seconds; +} + +#[no_mangle] +pub extern "C" fn fog_datetime_set(id: u32, dt: &FogDatetime) { + with_metric!( + DATETIME_MAP, + id, + metric, + metric.set_with_details( + dt.year, + dt.month, + dt.day, + dt.hour, + dt.minute, + dt.second, + dt.nano, + dt.offset_seconds + ) + ); +} + +#[no_mangle] +pub extern "C" fn fog_datetime_test_get_error(id: u32, error_str: &mut nsACString) -> bool { + let err = with_metric!(DATETIME_MAP, id, metric, test_get_errors!(metric)); + err.map(|err_str| error_str.assign(&err_str)).is_some() +} diff --git a/toolkit/components/glean/api/src/ffi/denominator.rs b/toolkit/components/glean/api/src/ffi/denominator.rs new file mode 100644 index 0000000000..ccb047f530 --- /dev/null +++ b/toolkit/components/glean/api/src/ffi/denominator.rs @@ -0,0 +1,28 @@ +// 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/. + +#![cfg(feature = "with_gecko")] + +use nsstring::nsACString; + +#[no_mangle] +pub unsafe extern "C" fn fog_denominator_add(id: u32, amount: i32) { + with_metric!(DENOMINATOR_MAP, id, metric, metric.add(amount)); +} + +#[no_mangle] +pub unsafe extern "C" fn fog_denominator_test_has_value(id: u32, ping_name: &nsACString) -> bool { + with_metric!(DENOMINATOR_MAP, id, metric, test_has!(metric, ping_name)) +} + +#[no_mangle] +pub unsafe extern "C" fn fog_denominator_test_get_value(id: u32, ping_name: &nsACString) -> i32 { + with_metric!(DENOMINATOR_MAP, id, metric, test_get!(metric, ping_name)) +} + +#[no_mangle] +pub extern "C" fn fog_denominator_test_get_error(id: u32, error_str: &mut nsACString) -> bool { + let err = with_metric!(DENOMINATOR_MAP, id, metric, test_get_errors!(metric)); + err.map(|err_str| error_str.assign(&err_str)).is_some() +} diff --git a/toolkit/components/glean/api/src/ffi/event.rs b/toolkit/components/glean/api/src/ffi/event.rs new file mode 100644 index 0000000000..bd167021d6 --- /dev/null +++ b/toolkit/components/glean/api/src/ffi/event.rs @@ -0,0 +1,168 @@ +// 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/. + +#![cfg(feature = "with_gecko")] + +use std::collections::HashMap; + +use nsstring::{nsACString, nsCString}; +use thin_vec::ThinVec; + +use crate::metrics::__glean_metric_maps as metric_maps; +use crate::private::EventRecordingError; + +#[no_mangle] +pub extern "C" fn fog_event_record( + id: u32, + extra_keys: &ThinVec<nsCString>, + extra_values: &ThinVec<nsCString>, +) { + // If no extra keys are passed, we can shortcut here. + if extra_keys.is_empty() { + if id & (1 << crate::factory::DYNAMIC_METRIC_BIT) > 0 { + let map = crate::factory::__jog_metric_maps::EVENT_MAP + .read() + .expect("Read lock for dynamic metric map was poisoned"); + match map.get(&id.into()) { + Some(m) => m.record_raw(Default::default()), + None => panic!("No (dynamic) metric for event with id {}", id), + } + return; + } + + if metric_maps::record_event_by_id(id, Default::default()).is_err() { + panic!("No event for id {}", id); + } + + return; + } + + assert_eq!( + extra_keys.len(), + extra_values.len(), + "Extra keys and values differ in length. ID: {}", + id + ); + + // Otherwise we need to decode them and pass them along. + let extra = extra_keys + .iter() + .zip(extra_values.iter()) + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect(); + if id & (1 << crate::factory::DYNAMIC_METRIC_BIT) > 0 { + let map = crate::factory::__jog_metric_maps::EVENT_MAP + .read() + .expect("Read lock for dynamic metric map was poisoned"); + match map.get(&id.into()) { + Some(m) => m.record_raw(extra), + None => panic!("No (dynamic) metric for event with id {}", id), + } + return; + } else { + match metric_maps::record_event_by_id(id, extra) { + Ok(()) => {} + Err(EventRecordingError::InvalidId) => panic!("No event for id {}", id), + Err(_) => panic!("Unpossible!"), + } + } +} + +#[no_mangle] +pub unsafe extern "C" fn fog_event_test_has_value(id: u32, ping_name: &nsACString) -> bool { + let storage = if ping_name.is_empty() { + None + } else { + Some(ping_name.to_utf8().into_owned()) + }; + if id & (1 << crate::factory::DYNAMIC_METRIC_BIT) > 0 { + let map = crate::factory::__jog_metric_maps::EVENT_MAP + .read() + .expect("Read lock for dynamic metric map was poisoned"); + match map.get(&id.into()) { + Some(m) => m.test_get_value(storage.as_deref()).is_some(), + None => panic!("No (dynamic) metric for event with id {}", id), + } + } else { + metric_maps::event_test_get_value_wrapper(id, storage).is_some() + } +} + +#[no_mangle] +pub extern "C" fn fog_event_test_get_error(id: u32, error_str: &mut nsACString) -> bool { + let err = if id & (1 << crate::factory::DYNAMIC_METRIC_BIT) > 0 { + let map = crate::factory::__jog_metric_maps::EVENT_MAP + .read() + .expect("Read lock for dynamic metric map was poisoned"); + match map.get(&id.into()) { + Some(m) => test_get_errors!(m), + None => panic!("No (dynamic) metric for event with id {}", id), + } + } else { + metric_maps::event_test_get_error(id) + }; + err.map(|err_str| error_str.assign(&err_str)).is_some() +} + +/// FFI-compatible representation of recorded event data. +#[repr(C)] +pub struct FfiRecordedEvent { + timestamp: u64, + category: nsCString, + name: nsCString, + + /// Array of extra data, keys and values are interleaved. + extras: ThinVec<nsCString>, +} + +#[no_mangle] +pub extern "C" fn fog_event_test_get_value( + id: u32, + ping_name: &nsACString, + out_events: &mut ThinVec<FfiRecordedEvent>, +) { + let storage = if ping_name.is_empty() { + None + } else { + Some(ping_name.to_utf8().into_owned()) + }; + + let events = if id & (1 << crate::factory::DYNAMIC_METRIC_BIT) > 0 { + let map = crate::factory::__jog_metric_maps::EVENT_MAP + .read() + .expect("Read lock for dynamic metric map was poisoned"); + let events = match map.get(&id.into()) { + Some(m) => m.test_get_value(storage.as_deref()), + None => return, + }; + match events { + Some(events) => events, + None => return, + } + } else { + match metric_maps::event_test_get_value_wrapper(id, storage) { + Some(events) => events, + None => return, + } + }; + + for event in events { + let extra = event.extra.unwrap_or_else(HashMap::new); + let extra_len = extra.len(); + let mut extras = ThinVec::with_capacity(extra_len * 2); + for (k, v) in extra.into_iter() { + extras.push(nsCString::from(k)); + extras.push(nsCString::from(v)); + } + + let event = FfiRecordedEvent { + timestamp: event.timestamp, + category: nsCString::from(event.category), + name: nsCString::from(event.name), + extras, + }; + + out_events.push(event); + } +} diff --git a/toolkit/components/glean/api/src/ffi/labeled.rs b/toolkit/components/glean/api/src/ffi/labeled.rs new file mode 100644 index 0000000000..2cd61230f7 --- /dev/null +++ b/toolkit/components/glean/api/src/ffi/labeled.rs @@ -0,0 +1,84 @@ +// 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/. + +#![cfg(feature = "with_gecko")] + +use crate::metrics::__glean_metric_maps as metric_maps; +use nsstring::nsACString; +use std::sync::atomic::Ordering; + +#[no_mangle] +pub extern "C" fn fog_labeled_enum_to_str(id: u32, label: u16, value: &mut nsACString) { + let val = metric_maps::labeled_enum_to_str(id, label); + value.assign(&val); +} + +#[no_mangle] +pub extern "C" fn fog_labeled_boolean_get(id: u32, label: &nsACString) -> u32 { + labeled_submetric_get!( + id, + label, + LABELED_BOOLEAN_MAP, + labeled_boolean_get, + BOOLEAN_MAP, + LabeledBooleanMetric + ) +} + +#[no_mangle] +pub extern "C" fn fog_labeled_boolean_enum_get(id: u32, label: u16) -> u32 { + labeled_submetric_enum_get!( + id, + label, + labeled_boolean_enum_get, + BOOLEAN_MAP, + LabeledBooleanMetric + ) +} + +#[no_mangle] +pub extern "C" fn fog_labeled_counter_get(id: u32, label: &nsACString) -> u32 { + labeled_submetric_get!( + id, + label, + LABELED_COUNTER_MAP, + labeled_counter_get, + COUNTER_MAP, + LabeledCounterMetric + ) +} + +#[no_mangle] +pub extern "C" fn fog_labeled_counter_enum_get(id: u32, label: u16) -> u32 { + labeled_submetric_enum_get!( + id, + label, + labeled_counter_enum_get, + COUNTER_MAP, + LabeledCounterMetric + ) +} + +#[no_mangle] +pub extern "C" fn fog_labeled_string_get(id: u32, label: &nsACString) -> u32 { + labeled_submetric_get!( + id, + label, + LABELED_STRING_MAP, + labeled_string_get, + STRING_MAP, + LabeledStringMetric + ) +} + +#[no_mangle] +pub extern "C" fn fog_labeled_string_enum_get(id: u32, label: u16) -> u32 { + labeled_submetric_enum_get!( + id, + label, + labeled_string_enum_get, + STRING_MAP, + LabeledStringMetric + ) +} diff --git a/toolkit/components/glean/api/src/ffi/macros.rs b/toolkit/components/glean/api/src/ffi/macros.rs new file mode 100644 index 0000000000..3571ebd88b --- /dev/null +++ b/toolkit/components/glean/api/src/ffi/macros.rs @@ -0,0 +1,289 @@ +// 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/. + +//! Helper macros for implementing the FFI API for metric types. + +/// Get a metric object by ID from the corresponding map, then +/// execute the provided closure with it. +/// +/// # Arguments +/// +/// * `$map` - The name of the hash map within `metrics::__glean_metric_maps` +/// (or `factory::__jog_metric_maps`) +/// as generated by glean_parser. +/// * `$id` - The ID of the metric to get. +/// * `$m` - The identifier to use for the retrieved metric. +/// The expression `$f` can use this identifier. +/// * `$f` - The expression to execute with the retrieved metric `$m`. +macro_rules! with_metric { + (BOOLEAN_MAP, $id:ident, $m:ident, $f:expr) => { + maybe_labeled_with_metric!(BOOLEAN_MAP, $id, $m, $f) + }; + (COUNTER_MAP, $id:ident, $m:ident, $f:expr) => { + maybe_labeled_with_metric!(COUNTER_MAP, $id, $m, $f) + }; + (STRING_MAP, $id:ident, $m:ident, $f:expr) => { + maybe_labeled_with_metric!(STRING_MAP, $id, $m, $f) + }; + ($map:ident, $id:ident, $m:ident, $f:expr) => { + just_with_metric!($map, $id, $m, $f) + }; +} + +/// Get a dynamically-registered metric object by id from the corresponding map, +/// then execute the provided closure with it. +/// +/// Assumes `$id` is for a dynamic non-submetric metric. +/// Will panic if it isn't. +/// +/// # Arguments +/// +/// * `$map` - The name of the hash map within `factory::__jog_metric_maps` +/// as generated by glean_parser. +/// * `$id` - The ID of the metric to get. +/// * `$m` - The identifier to use for the retrieved metric. +/// The expression `$f` can use this identifier. +/// * `$f` - The expression to execute with the retrieved metric `$m`. +macro_rules! just_with_jog_metric { + ($map:ident, $id:ident, $m:ident, $f:expr) => {{ + let map = $crate::factory::__jog_metric_maps::$map + .read() + .expect("Read lock for dynamic metric map was poisoned"); + match map.get(&$id.into()) { + Some($m) => $f, + None => panic!("No (dynamic) metric for id {}", $id), + } + }}; +} + +/// Get a metric object by id from the corresponding map, then +/// execute the provided closure with it. +/// +/// Ignores the possibility that the $id might be for a labeled submetric. +/// +/// # Arguments +/// +/// * `$map` - The name of the hash map within `metrics::__glean_metric_maps` +/// (or `factory::__jog_metric_maps`) +/// as generated by glean_parser. +/// * `$id` - The ID of the metric to get. +/// * `$m` - The identifier to use for the retrieved metric. +/// The expression `$f` can use this identifier. +/// * `$f` - The expression to execute with the retrieved metric `$m`. +macro_rules! just_with_metric { + ($map:ident, $id:ident, $m:ident, $f:expr) => { + if $id & (1 << $crate::factory::DYNAMIC_METRIC_BIT) > 0 { + just_with_jog_metric!($map, $id, $m, $f) + } else { + match $crate::metrics::__glean_metric_maps::$map.get(&$id.into()) { + Some($m) => $f, + None => panic!("No metric for id {}", $id), + } + } + }; +} + +/// Get a metric object by id from the corresponding map, then +/// execute the provided closure with it. +/// +/// Requires that the provided $map be of a type that can be labeled, since it +/// assumes the presence of a same-named map in +/// `metrics::_glean_metrics_map::submetric_maps`. +/// +/// # Arguments +/// +/// * `$map` - The name of the hash map within `metrics::__glean_metric_maps` +/// and `metrics::__glean_metric_maps::submetric_maps` as generated +/// by glean_parser. +/// * `$id` - The ID of the metric to get. +/// * `$m` - The identifier to use for the retrieved metric. +/// The expression `$f` can use this identifier. +/// * `$f` - The expression to execute with the retrieved metric `$m`. +macro_rules! maybe_labeled_with_metric { + ($map:ident, $id:ident, $m:ident, $f:expr) => { + if $id & (1 << $crate::metrics::__glean_metric_maps::submetric_maps::SUBMETRIC_BIT) > 0 { + let map = $crate::metrics::__glean_metric_maps::submetric_maps::$map + .read() + .expect("Read lock for labeled metric map was poisoned"); + match map.get(&$id.into()) { + Some($m) => $f, + None => panic!("No submetric for id {}", $id), + } + } else { + just_with_metric!($map, $id, $m, $f) + } + }; +} + +/// Test whether a value is stored for the given metric. +/// +/// # Arguments +/// +/// * `$metric` - The metric to test. +/// * `$storage` - the storage name to look into. +macro_rules! test_has { + ($metric:ident, $storage:ident) => {{ + let storage = if $storage.is_empty() { + None + } else { + Some($storage.to_utf8()) + }; + $metric.test_get_value(storage.as_deref()).is_some() + }}; +} + +/// Get the currently stored value for the given metric. +/// +/// # Arguments +/// +/// * `$metric` - The metric to test. +/// * `$storage` - the storage name to look into. +macro_rules! test_get { + ($metric:ident, $storage:ident) => {{ + let storage = if $storage.is_empty() { + None + } else { + Some($storage.to_utf8()) + }; + $metric.test_get_value(storage.as_deref()).unwrap() + }}; +} + +/// Check the provided metric in the provided storage for errors. +/// On finding one, return an error string. +/// +/// # Arguments +/// +/// * `$metric` - The metric to test. +macro_rules! test_get_errors { + ($metric:path) => {{ + let error_types = [ + glean::ErrorType::InvalidValue, + glean::ErrorType::InvalidLabel, + glean::ErrorType::InvalidState, + glean::ErrorType::InvalidOverflow, + ]; + let mut error_str = None; + for &error_type in error_types.iter() { + let num_errors = $metric.test_get_num_recorded_errors(error_type); + if num_errors > 0 { + error_str = Some(format!( + "Metric had {} error(s) of type {}!", + num_errors, + error_type.as_str() + )); + break; + } + } + error_str + }}; +} + +/// Get the submetric id for a given labeled metric and label. +/// +/// # Arguments +/// +/// * `$id` - The id of the labeled metric. +/// * `$label` - The (string) label of the submetric. +/// * `$labeled_map` - The name of the labeled metric's map for retrieval (JOG only). +/// * `$labeled_get` - The name of the labeled metric's get fn for retrieval. +/// * `$submetric_map`- The name of the submetrics' map for storage. +/// * `$metric_type` - The submetric's type (needed for an internal closure). +macro_rules! labeled_submetric_get { + ($id:ident, $label:ident, $labeled_map:ident, $labeled_get:ident, $submetric_map:ident, $metric_type:ty) => {{ + let tuple = ($id, $label.to_utf8().into()); + { + let map = $crate::metrics::__glean_metric_maps::submetric_maps::LABELED_METRICS_TO_IDS + .read() + .expect("read lock of submetric ids was poisoned"); + if let Some(submetric_id) = map.get(&tuple) { + return *submetric_id; + } + } + + // Gotta actually create a new submetric with a new id. + let submetric_id = + $crate::metrics::__glean_metric_maps::submetric_maps::NEXT_LABELED_SUBMETRIC_ID + .fetch_add(1, Ordering::SeqCst); + { + if $id & (1 << $crate::factory::DYNAMIC_METRIC_BIT) > 0 { + just_with_jog_metric!($labeled_map, $id, metric, { + let submetric = metric.get(&tuple.1); + let mut map = + $crate::metrics::__glean_metric_maps::submetric_maps::$submetric_map + .write() + .expect("write lock of submetric map was poisoned"); + map.insert(submetric_id.into(), submetric); + }); + } else { + let mut map = $crate::metrics::__glean_metric_maps::submetric_maps::$submetric_map + .write() + .expect("write lock of submetric map was poisoned"); + map.insert( + submetric_id.into(), + $crate::metrics::__glean_metric_maps::$labeled_get($id, &tuple.1), + ); + } + } + + let mut map = $crate::metrics::__glean_metric_maps::submetric_maps::LABELED_METRICS_TO_IDS + .write() + .expect("write lock of submetric ids was poisoned"); + map.insert(tuple, submetric_id); + submetric_id + }}; +} + +/// Get the submetric id for a given labeled metric and label enum. +/// +/// # Arguments +/// +/// * `$id` - The id of the labeled metric. +/// * `$label` - The (enum) label of the submetric. +/// * `$labeled_get` - The name of the labeled metric's get fn for retrieval. +/// * `$submetric_map`- The name of the submetrics' map for storage. +/// * `$metric_type` - The submetric's type (needed for an internal closure). +macro_rules! labeled_submetric_enum_get { + ($id:ident, $label_enum:ident, $labeled_get:ident, $submetric_map:ident, $metric_type:ty) => {{ + let tuple = ($id, $label_enum.into()); + // First: Have we seen this enum before? If so, give out the same submetric id. + { + let map = $crate::metrics::__glean_metric_maps::submetric_maps::LABELED_ENUMS_TO_IDS + .read() + .expect("read lock of enum submetric ids was poisoned"); + if let Some(submetric_id) = map.get(&tuple) { + return *submetric_id; + } + } + + // Alas, this is the first time we've needed to handle this metric with this enum. + // Gotta actually create a new submetric with a new id. + let submetric_id = + $crate::metrics::__glean_metric_maps::submetric_maps::NEXT_LABELED_SUBMETRIC_ID + .fetch_add(1, Ordering::SeqCst); + { + // What if the dynamic bit is set? + // JOG only supports JS, and enum_get isn't (yet) supported in JS. + assert_eq!( + 0, + $id & (1 << $crate::factory::DYNAMIC_METRIC_BIT), + "No enum_get support for JOG" + ); + let mut map = $crate::metrics::__glean_metric_maps::submetric_maps::$submetric_map + .write() + .expect("write lock of submetric map was poisoned"); + map.insert( + submetric_id.into(), + $crate::metrics::__glean_metric_maps::$labeled_get($id, tuple.1), + ); + } + + // And now ensure we store the submetric so we need not create it on subsequent calls. + let mut map = $crate::metrics::__glean_metric_maps::submetric_maps::LABELED_ENUMS_TO_IDS + .write() + .expect("write lock of submetric ids was poisoned"); + map.insert(tuple, submetric_id); + submetric_id + }}; +} diff --git a/toolkit/components/glean/api/src/ffi/memory_distribution.rs b/toolkit/components/glean/api/src/ffi/memory_distribution.rs new file mode 100644 index 0000000000..cf09d3f8de --- /dev/null +++ b/toolkit/components/glean/api/src/ffi/memory_distribution.rs @@ -0,0 +1,64 @@ +// 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/. + +#![cfg(feature = "with_gecko")] + +use nsstring::nsACString; +use thin_vec::ThinVec; + +#[no_mangle] +pub extern "C" fn fog_memory_distribution_test_has_value(id: u32, ping_name: &nsACString) -> bool { + with_metric!( + MEMORY_DISTRIBUTION_MAP, + id, + metric, + test_has!(metric, ping_name) + ) +} + +#[no_mangle] +pub extern "C" fn fog_memory_distribution_test_get_value( + id: u32, + ping_name: &nsACString, + sum: &mut u64, + buckets: &mut ThinVec<u64>, + counts: &mut ThinVec<u64>, +) { + let val = with_metric!( + MEMORY_DISTRIBUTION_MAP, + id, + metric, + test_get!(metric, ping_name) + ); + *sum = val.sum as _; + for (&bucket, &count) in val.values.iter() { + buckets.push(bucket as _); + counts.push(count as _); + } +} + +#[no_mangle] +pub extern "C" fn fog_memory_distribution_accumulate(id: u32, sample: u64) { + with_metric!( + MEMORY_DISTRIBUTION_MAP, + id, + metric, + metric.accumulate(sample) + ); +} + +#[no_mangle] +pub extern "C" fn fog_memory_distribution_test_get_error( + id: u32, + + error_str: &mut nsACString, +) -> bool { + let err = with_metric!( + MEMORY_DISTRIBUTION_MAP, + id, + metric, + test_get_errors!(metric) + ); + err.map(|err_str| error_str.assign(&err_str)).is_some() +} diff --git a/toolkit/components/glean/api/src/ffi/mod.rs b/toolkit/components/glean/api/src/ffi/mod.rs new file mode 100644 index 0000000000..23235fc2f1 --- /dev/null +++ b/toolkit/components/glean/api/src/ffi/mod.rs @@ -0,0 +1,28 @@ +// 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/. + +#![cfg(feature = "with_gecko")] + +#[macro_use] +mod macros; + +mod boolean; +mod counter; +mod custom_distribution; +mod datetime; +mod denominator; +mod event; +mod labeled; +mod memory_distribution; +mod numerator; +mod ping; +mod quantity; +mod rate; +mod string; +mod string_list; +mod text; +mod timespan; +mod timing_distribution; +mod url; +mod uuid; diff --git a/toolkit/components/glean/api/src/ffi/numerator.rs b/toolkit/components/glean/api/src/ffi/numerator.rs new file mode 100644 index 0000000000..e679e426c4 --- /dev/null +++ b/toolkit/components/glean/api/src/ffi/numerator.rs @@ -0,0 +1,35 @@ +// 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/. + +#![cfg(feature = "with_gecko")] + +use nsstring::nsACString; + +#[no_mangle] +pub unsafe extern "C" fn fog_numerator_add_to_numerator(id: u32, amount: i32) { + with_metric!(NUMERATOR_MAP, id, metric, metric.add_to_numerator(amount)); +} + +#[no_mangle] +pub unsafe extern "C" fn fog_numerator_test_has_value(id: u32, ping_name: &nsACString) -> bool { + with_metric!(NUMERATOR_MAP, id, metric, test_has!(metric, ping_name)) +} + +#[no_mangle] +pub unsafe extern "C" fn fog_numerator_test_get_value( + id: u32, + ping_name: &nsACString, + num: &mut i32, + den: &mut i32, +) { + let rate = with_metric!(NUMERATOR_MAP, id, metric, test_get!(metric, ping_name)); + *num = rate.numerator; + *den = rate.denominator; +} + +#[no_mangle] +pub extern "C" fn fog_numerator_test_get_error(id: u32, error_str: &mut nsACString) -> bool { + let err = with_metric!(NUMERATOR_MAP, id, metric, test_get_errors!(metric)); + err.map(|err_str| error_str.assign(&err_str)).is_some() +} diff --git a/toolkit/components/glean/api/src/ffi/ping.rs b/toolkit/components/glean/api/src/ffi/ping.rs new file mode 100644 index 0000000000..2834655a03 --- /dev/null +++ b/toolkit/components/glean/api/src/ffi/ping.rs @@ -0,0 +1,18 @@ +// 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/. + +#![cfg(feature = "with_gecko")] + +use crate::pings; +use nsstring::nsACString; + +#[no_mangle] +pub extern "C" fn fog_submit_ping_by_id(id: u32, reason: &nsACString) { + let reason = if reason.is_empty() { + None + } else { + Some(reason.to_utf8()) + }; + pings::submit_ping_by_id(id, reason.as_deref()); +} diff --git a/toolkit/components/glean/api/src/ffi/quantity.rs b/toolkit/components/glean/api/src/ffi/quantity.rs new file mode 100644 index 0000000000..7f94bcff27 --- /dev/null +++ b/toolkit/components/glean/api/src/ffi/quantity.rs @@ -0,0 +1,28 @@ +// 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/. + +#![cfg(feature = "with_gecko")] + +use nsstring::nsACString; + +#[no_mangle] +pub extern "C" fn fog_quantity_set(id: u32, value: i64) { + with_metric!(QUANTITY_MAP, id, metric, metric.set(value)); +} + +#[no_mangle] +pub extern "C" fn fog_quantity_test_has_value(id: u32, ping_name: &nsACString) -> bool { + with_metric!(QUANTITY_MAP, id, metric, test_has!(metric, ping_name)) +} + +#[no_mangle] +pub extern "C" fn fog_quantity_test_get_value(id: u32, ping_name: &nsACString) -> i64 { + with_metric!(QUANTITY_MAP, id, metric, test_get!(metric, ping_name)) +} + +#[no_mangle] +pub extern "C" fn fog_quantity_test_get_error(id: u32, error_str: &mut nsACString) -> bool { + let err = with_metric!(QUANTITY_MAP, id, metric, test_get_errors!(metric)); + err.map(|err_str| error_str.assign(&err_str)).is_some() +} diff --git a/toolkit/components/glean/api/src/ffi/rate.rs b/toolkit/components/glean/api/src/ffi/rate.rs new file mode 100644 index 0000000000..c14b33f3ea --- /dev/null +++ b/toolkit/components/glean/api/src/ffi/rate.rs @@ -0,0 +1,40 @@ +// 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/. + +#![cfg(feature = "with_gecko")] + +use nsstring::nsACString; + +#[no_mangle] +pub unsafe extern "C" fn fog_rate_add_to_numerator(id: u32, amount: i32) { + with_metric!(RATE_MAP, id, metric, metric.add_to_numerator(amount)); +} + +#[no_mangle] +pub unsafe extern "C" fn fog_rate_add_to_denominator(id: u32, amount: i32) { + with_metric!(RATE_MAP, id, metric, metric.add_to_denominator(amount)); +} + +#[no_mangle] +pub unsafe extern "C" fn fog_rate_test_has_value(id: u32, ping_name: &nsACString) -> bool { + with_metric!(RATE_MAP, id, metric, test_has!(metric, ping_name)) +} + +#[no_mangle] +pub unsafe extern "C" fn fog_rate_test_get_value( + id: u32, + ping_name: &nsACString, + num: &mut i32, + den: &mut i32, +) { + let rate = with_metric!(RATE_MAP, id, metric, test_get!(metric, ping_name)); + *num = rate.numerator; + *den = rate.denominator; +} + +#[no_mangle] +pub extern "C" fn fog_rate_test_get_error(id: u32, error_str: &mut nsACString) -> bool { + let err = with_metric!(RATE_MAP, id, metric, test_get_errors!(metric)); + err.map(|err_str| error_str.assign(&err_str)).is_some() +} diff --git a/toolkit/components/glean/api/src/ffi/string.rs b/toolkit/components/glean/api/src/ffi/string.rs new file mode 100644 index 0000000000..fc28e03a38 --- /dev/null +++ b/toolkit/components/glean/api/src/ffi/string.rs @@ -0,0 +1,33 @@ +// 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/. + +#![cfg(feature = "with_gecko")] + +use nsstring::nsACString; + +#[no_mangle] +pub extern "C" fn fog_string_test_has_value(id: u32, ping_name: &nsACString) -> bool { + with_metric!(STRING_MAP, id, metric, test_has!(metric, ping_name)) +} + +#[no_mangle] +pub extern "C" fn fog_string_test_get_value( + id: u32, + ping_name: &nsACString, + value: &mut nsACString, +) { + let val = with_metric!(STRING_MAP, id, metric, test_get!(metric, ping_name)); + value.assign(&val); +} + +#[no_mangle] +pub extern "C" fn fog_string_set(id: u32, value: &nsACString) { + with_metric!(STRING_MAP, id, metric, metric.set(value.to_utf8())); +} + +#[no_mangle] +pub extern "C" fn fog_string_test_get_error(id: u32, error_str: &mut nsACString) -> bool { + let err = with_metric!(STRING_MAP, id, metric, test_get_errors!(metric)); + err.map(|err_str| error_str.assign(&err_str)).is_some() +} diff --git a/toolkit/components/glean/api/src/ffi/string_list.rs b/toolkit/components/glean/api/src/ffi/string_list.rs new file mode 100644 index 0000000000..42ebd9e445 --- /dev/null +++ b/toolkit/components/glean/api/src/ffi/string_list.rs @@ -0,0 +1,42 @@ +// 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/. + +#![cfg(feature = "with_gecko")] + +use nsstring::{nsACString, nsCString}; +use thin_vec::ThinVec; + +#[no_mangle] +pub extern "C" fn fog_string_list_test_has_value(id: u32, ping_name: &nsACString) -> bool { + with_metric!(STRING_LIST_MAP, id, metric, test_has!(metric, ping_name)) +} + +#[no_mangle] +pub extern "C" fn fog_string_list_test_get_value( + id: u32, + ping_name: &nsACString, + value: &mut ThinVec<nsCString>, +) { + let val = with_metric!(STRING_LIST_MAP, id, metric, test_get!(metric, ping_name)); + for v in val { + value.push(v.into()); + } +} + +#[no_mangle] +pub extern "C" fn fog_string_list_add(id: u32, value: &nsACString) { + with_metric!(STRING_LIST_MAP, id, metric, metric.add(value.to_utf8())); +} + +#[no_mangle] +pub extern "C" fn fog_string_list_set(id: u32, value: &ThinVec<nsCString>) { + let value = value.iter().map(|s| s.to_utf8().into()).collect(); + with_metric!(STRING_LIST_MAP, id, metric, metric.set(value)); +} + +#[no_mangle] +pub extern "C" fn fog_string_list_test_get_error(id: u32, error_str: &mut nsACString) -> bool { + let err = with_metric!(STRING_LIST_MAP, id, metric, test_get_errors!(metric)); + err.map(|err_str| error_str.assign(&err_str)).is_some() +} diff --git a/toolkit/components/glean/api/src/ffi/text.rs b/toolkit/components/glean/api/src/ffi/text.rs new file mode 100644 index 0000000000..da46fb849f --- /dev/null +++ b/toolkit/components/glean/api/src/ffi/text.rs @@ -0,0 +1,29 @@ +// 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/. + +#![cfg(feature = "with_gecko")] + +use nsstring::nsACString; + +#[no_mangle] +pub extern "C" fn fog_text_test_has_value(id: u32, ping_name: &nsACString) -> bool { + with_metric!(TEXT_MAP, id, metric, test_has!(metric, ping_name)) +} + +#[no_mangle] +pub extern "C" fn fog_text_test_get_value(id: u32, ping_name: &nsACString, value: &mut nsACString) { + let val = with_metric!(TEXT_MAP, id, metric, test_get!(metric, ping_name)); + value.assign(&val); +} + +#[no_mangle] +pub extern "C" fn fog_text_set(id: u32, value: &nsACString) { + with_metric!(TEXT_MAP, id, metric, metric.set(value.to_utf8())); +} + +#[no_mangle] +pub extern "C" fn fog_text_test_get_error(id: u32, error_str: &mut nsACString) -> bool { + let err = with_metric!(TEXT_MAP, id, metric, test_get_errors!(metric)); + err.map(|err_str| error_str.assign(&err_str)).is_some() +} diff --git a/toolkit/components/glean/api/src/ffi/timespan.rs b/toolkit/components/glean/api/src/ffi/timespan.rs new file mode 100644 index 0000000000..5de411ffab --- /dev/null +++ b/toolkit/components/glean/api/src/ffi/timespan.rs @@ -0,0 +1,48 @@ +// 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/. + +#![cfg(feature = "with_gecko")] + +use nsstring::nsACString; + +#[no_mangle] +pub extern "C" fn fog_timespan_start(id: u32) { + with_metric!(TIMESPAN_MAP, id, metric, metric.start()); +} + +#[no_mangle] +pub extern "C" fn fog_timespan_stop(id: u32) { + with_metric!(TIMESPAN_MAP, id, metric, metric.stop()); +} + +#[no_mangle] +pub extern "C" fn fog_timespan_cancel(id: u32) { + with_metric!(TIMESPAN_MAP, id, metric, metric.cancel()); +} + +#[no_mangle] +pub extern "C" fn fog_timespan_set_raw(id: u32, duration: u32) { + with_metric!( + TIMESPAN_MAP, + id, + metric, + metric.set_raw_unitless(duration.into()) + ); +} + +#[no_mangle] +pub extern "C" fn fog_timespan_test_has_value(id: u32, ping_name: &nsACString) -> bool { + with_metric!(TIMESPAN_MAP, id, metric, test_has!(metric, ping_name)) +} + +#[no_mangle] +pub extern "C" fn fog_timespan_test_get_value(id: u32, ping_name: &nsACString) -> u64 { + with_metric!(TIMESPAN_MAP, id, metric, test_get!(metric, ping_name)) +} + +#[no_mangle] +pub extern "C" fn fog_timespan_test_get_error(id: u32, error_str: &mut nsACString) -> bool { + let err = with_metric!(TIMESPAN_MAP, id, metric, test_get_errors!(metric)); + err.map(|err_str| error_str.assign(&err_str)).is_some() +} diff --git a/toolkit/components/glean/api/src/ffi/timing_distribution.rs b/toolkit/components/glean/api/src/ffi/timing_distribution.rs new file mode 100644 index 0000000000..4ac5d03986 --- /dev/null +++ b/toolkit/components/glean/api/src/ffi/timing_distribution.rs @@ -0,0 +1,90 @@ +// 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/. + +#![cfg(feature = "with_gecko")] + +use nsstring::nsACString; +use std::time::Duration; +use thin_vec::ThinVec; + +#[no_mangle] +pub extern "C" fn fog_timing_distribution_start(id: u32) -> u64 { + with_metric!(TIMING_DISTRIBUTION_MAP, id, metric, metric.start().id) +} + +#[no_mangle] +pub extern "C" fn fog_timing_distribution_stop_and_accumulate(id: u32, timing_id: u64) { + with_metric!( + TIMING_DISTRIBUTION_MAP, + id, + metric, + metric.stop_and_accumulate(timing_id.into()) + ); +} + +#[no_mangle] +pub extern "C" fn fog_timing_distribution_accumulate_raw_nanos(id: u32, sample: u64) { + with_metric!( + TIMING_DISTRIBUTION_MAP, + id, + metric, + metric.accumulate_raw_duration(Duration::from_nanos(sample)) + ); +} + +#[no_mangle] +pub extern "C" fn fog_timing_distribution_cancel(id: u32, timing_id: u64) { + with_metric!( + TIMING_DISTRIBUTION_MAP, + id, + metric, + metric.cancel(timing_id.into()) + ); +} + +#[no_mangle] +pub extern "C" fn fog_timing_distribution_test_has_value(id: u32, ping_name: &nsACString) -> bool { + with_metric!( + TIMING_DISTRIBUTION_MAP, + id, + metric, + test_has!(metric, ping_name) + ) +} + +#[no_mangle] +pub extern "C" fn fog_timing_distribution_test_get_value( + id: u32, + ping_name: &nsACString, + sum: &mut u64, + buckets: &mut ThinVec<u64>, + counts: &mut ThinVec<u64>, +) { + let val = with_metric!( + TIMING_DISTRIBUTION_MAP, + id, + metric, + test_get!(metric, ping_name) + ); + *sum = val.sum as _; + for (&bucket, &count) in val.values.iter() { + buckets.push(bucket as _); + counts.push(count as _); + } +} + +#[no_mangle] +pub extern "C" fn fog_timing_distribution_test_get_error( + id: u32, + + error_str: &mut nsACString, +) -> bool { + let err = with_metric!( + TIMING_DISTRIBUTION_MAP, + id, + metric, + test_get_errors!(metric) + ); + err.map(|err_str| error_str.assign(&err_str)).is_some() +} diff --git a/toolkit/components/glean/api/src/ffi/url.rs b/toolkit/components/glean/api/src/ffi/url.rs new file mode 100644 index 0000000000..b94915f7cf --- /dev/null +++ b/toolkit/components/glean/api/src/ffi/url.rs @@ -0,0 +1,29 @@ +// 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/. + +#![cfg(feature = "with_gecko")] + +use nsstring::nsACString; + +#[no_mangle] +pub extern "C" fn fog_url_test_has_value(id: u32, ping_name: &nsACString) -> bool { + with_metric!(URL_MAP, id, metric, test_has!(metric, ping_name)) +} + +#[no_mangle] +pub extern "C" fn fog_url_test_get_value(id: u32, ping_name: &nsACString, value: &mut nsACString) { + let val = with_metric!(URL_MAP, id, metric, test_get!(metric, ping_name)); + value.assign(&val); +} + +#[no_mangle] +pub extern "C" fn fog_url_set(id: u32, value: &nsACString) { + with_metric!(URL_MAP, id, metric, metric.set(value.to_utf8())); +} + +#[no_mangle] +pub extern "C" fn fog_url_test_get_error(id: u32, error_str: &mut nsACString) -> bool { + let err = with_metric!(URL_MAP, id, metric, test_get_errors!(metric)); + err.map(|err_str| error_str.assign(&err_str)).is_some() +} diff --git a/toolkit/components/glean/api/src/ffi/uuid.rs b/toolkit/components/glean/api/src/ffi/uuid.rs new file mode 100644 index 0000000000..e3101863ea --- /dev/null +++ b/toolkit/components/glean/api/src/ffi/uuid.rs @@ -0,0 +1,37 @@ +// 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/. + +#![cfg(feature = "with_gecko")] + +use nsstring::nsACString; +use uuid::Uuid; + +#[no_mangle] +pub extern "C" fn fog_uuid_test_has_value(id: u32, ping_name: &nsACString) -> bool { + with_metric!(UUID_MAP, id, metric, test_has!(metric, ping_name)) +} + +#[no_mangle] +pub extern "C" fn fog_uuid_test_get_value(id: u32, ping_name: &nsACString, value: &mut nsACString) { + let uuid = with_metric!(UUID_MAP, id, metric, test_get!(metric, ping_name)).to_string(); + value.assign(&uuid); +} + +#[no_mangle] +pub extern "C" fn fog_uuid_set(id: u32, value: &nsACString) { + if let Ok(uuid) = Uuid::parse_str(&value.to_utf8()) { + with_metric!(UUID_MAP, id, metric, metric.set(uuid)); + } +} + +#[no_mangle] +pub extern "C" fn fog_uuid_generate_and_set(id: u32) { + with_metric!(UUID_MAP, id, metric, metric.generate_and_set()); +} + +#[no_mangle] +pub extern "C" fn fog_uuid_test_get_error(id: u32, error_str: &mut nsACString) -> bool { + let err = with_metric!(UUID_MAP, id, metric, test_get_errors!(metric)); + err.map(|err_str| error_str.assign(&err_str)).is_some() +} diff --git a/toolkit/components/glean/api/src/ipc.rs b/toolkit/components/glean/api/src/ipc.rs new file mode 100644 index 0000000000..38ac868831 --- /dev/null +++ b/toolkit/components/glean/api/src/ipc.rs @@ -0,0 +1,349 @@ +// 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/. + +//! IPC Implementation, Rust part + +use crate::private::MetricId; +use once_cell::sync::Lazy; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +#[cfg(not(feature = "with_gecko"))] +use std::sync::atomic::AtomicBool; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::Mutex; +#[cfg(feature = "with_gecko")] +use {std::convert::TryInto, std::sync::atomic::AtomicU32, xpcom::interfaces::nsIXULRuntime}; + +use super::metrics::__glean_metric_maps; + +type EventRecord = (u64, HashMap<String, String>); + +/// Contains all the information necessary to update the metrics on the main +/// process. +#[derive(Debug, Default, Deserialize, Serialize)] +pub struct IPCPayload { + pub counters: HashMap<MetricId, i32>, + pub custom_samples: HashMap<MetricId, Vec<i64>>, + pub denominators: HashMap<MetricId, i32>, + pub events: HashMap<MetricId, Vec<EventRecord>>, + pub labeled_counters: HashMap<MetricId, HashMap<String, i32>>, + pub memory_samples: HashMap<MetricId, Vec<u64>>, + pub numerators: HashMap<MetricId, i32>, + pub rates: HashMap<MetricId, (i32, i32)>, + pub string_lists: HashMap<MetricId, Vec<String>>, + pub timing_samples: HashMap<MetricId, Vec<u64>>, +} + +/// Global singleton: pending IPC payload. +static PAYLOAD: Lazy<Mutex<IPCPayload>> = Lazy::new(|| Mutex::new(IPCPayload::default())); +/// Global singleton: number of times the IPC payload was accessed. +static PAYLOAD_ACCESS_COUNT: AtomicUsize = AtomicUsize::new(0); + +// The maximum size of an IPC message in Firefox Desktop is 256MB. +// (See IPC::Channel::kMaximumMessageSize) +// In `IPCPayload` the largest size can be attained in the fewest accesses via events. +// Each event could be its own u64 id, u64 timestamp, and HashMap of ten i32 to ten 100-byte strings. +// That's 1056B = 8 + 8 + 10(4 + 100) +// In 256MB we can fit 254200 or so of these, not counting overhead. +// Let's take a conservative estimate of 100000 to +// 0) Account for overhead +// 1) Not be greedy +// 2) Allow time for the dispatch to main thread which will actually perform the flush +// "Why the -1?" Because fetch_add returns the value before the addition. +const PAYLOAD_ACCESS_WATERMARK: usize = 100000 - 1; + +pub fn with_ipc_payload<F, R>(f: F) -> R +where + F: FnOnce(&mut IPCPayload) -> R, +{ + if PAYLOAD_ACCESS_COUNT.fetch_add(1, Ordering::SeqCst) > PAYLOAD_ACCESS_WATERMARK { + // We reset this before the actual flush to keep all the logic together. + // Otherwise the count reset would need to happen down in take_buf(). + // This may overcount (resulting in undersized payloads) which is okay. + PAYLOAD_ACCESS_COUNT.store(0, Ordering::SeqCst); + handle_payload_filling(); + } + let mut payload = PAYLOAD.lock().unwrap(); + f(&mut payload) +} + +/// Do we need IPC? +/// +/// Thread-safe. +#[cfg(feature = "with_gecko")] +static PROCESS_TYPE: Lazy<AtomicU32> = Lazy::new(|| { + extern "C" { + fn FOG_GetProcessType() -> i32; + } + // SAFETY NOTE: Safe because it returns a primitive by value. + let process_type = unsafe { FOG_GetProcessType() }; + // It's impossible for i32 to overflow u32, but maybe someone got clever + // and introduced a negative process type constant. Default to parent. + let process_type = process_type + .try_into() + .unwrap_or(nsIXULRuntime::PROCESS_TYPE_DEFAULT); + // We don't have process-specific init locations outside of the main + // process, so we introduce this side-effect to a global static init. + // This is the absolute first time we decide which process type we're + // treating this process as, so this is the earliest we can do this. + register_process_shutdown(process_type); + AtomicU32::new(process_type) +}); + +#[cfg(feature = "with_gecko")] +pub fn need_ipc() -> bool { + PROCESS_TYPE.load(Ordering::Relaxed) != nsIXULRuntime::PROCESS_TYPE_DEFAULT +} + +/// The first time we're used in a process, +/// we'll need to start thinking about cleanup. +/// +/// Please only call once per process. +/// Multiple calls may register multiple handlers. +#[cfg(feature = "with_gecko")] +fn register_process_shutdown(process_type: u32) { + match process_type { + nsIXULRuntime::PROCESS_TYPE_DEFAULT => { + // Parent process shutdown is handled by the FOG XPCOM Singleton. + } + nsIXULRuntime::PROCESS_TYPE_CONTENT => { + // Content child shutdown is in C++ for access to RunOnShutdown(). + extern "C" { + fn FOG_RegisterContentChildShutdown(); + } + unsafe { + FOG_RegisterContentChildShutdown(); + }; + } + nsIXULRuntime::PROCESS_TYPE_GMPLUGIN => { + // GMP process shutdown is handled in GMPChild::ActorDestroy. + } + nsIXULRuntime::PROCESS_TYPE_GPU => { + // GPU process shutdown is handled in GPUParent::ActorDestroy. + } + nsIXULRuntime::PROCESS_TYPE_RDD => { + // RDD process shutdown is handled in RDDParent::ActorDestroy. + } + nsIXULRuntime::PROCESS_TYPE_SOCKET => { + // Socket process shutdown is handled in SocketProcessChild::ActorDestroy. + } + nsIXULRuntime::PROCESS_TYPE_UTILITY => { + // Utility process shutdown is handled in UtilityProcessChild::ActorDestroy. + } + _ => { + // We don't yet support other process types. + log::error!("Process type {} tried to use FOG, but isn't supported! (Process type constants are in nsIXULRuntime.rs)", process_type); + } + } +} + +/// An RAII that, on drop, restores the value used to determine whether FOG +/// needs IPC. Used in tests. +/// ```rust,ignore +/// #[test] +/// fn test_need_ipc_raii() { +/// assert!(false == ipc::need_ipc()); +/// { +/// let _raii = ipc::test_set_need_ipc(true); +/// assert!(ipc::need_ipc()); +/// } +/// assert!(false == ipc::need_ipc()); +/// } +/// ``` +#[cfg(not(feature = "with_gecko"))] +pub struct TestNeedIpcRAII { + prev_value: bool, +} + +#[cfg(not(feature = "with_gecko"))] +impl Drop for TestNeedIpcRAII { + fn drop(&mut self) { + TEST_NEED_IPC.store(self.prev_value, Ordering::Relaxed); + } +} + +#[cfg(not(feature = "with_gecko"))] +static TEST_NEED_IPC: AtomicBool = AtomicBool::new(false); + +/// Test-only API for telling FOG to use IPC mechanisms even if the test has +/// only the one process. See TestNeedIpcRAII for an example. +#[cfg(not(feature = "with_gecko"))] +pub fn test_set_need_ipc(need_ipc: bool) -> TestNeedIpcRAII { + TestNeedIpcRAII { + prev_value: TEST_NEED_IPC.swap(need_ipc, Ordering::Relaxed), + } +} + +#[cfg(not(feature = "with_gecko"))] +pub fn need_ipc() -> bool { + TEST_NEED_IPC.load(Ordering::Relaxed) +} + +pub fn take_buf() -> Option<Vec<u8>> { + with_ipc_payload(move |payload| { + let buf = bincode::serialize(&payload).ok(); + *payload = IPCPayload { + ..Default::default() + }; + buf + }) +} + +#[cfg(not(feature = "with_gecko"))] +fn handle_payload_filling() { + // Space intentionally left blank. + // Without Gecko IPC to drain the buffer, there's nothing we can do. +} + +#[cfg(feature = "with_gecko")] +fn handle_payload_filling() { + extern "C" { + fn FOG_IPCPayloadFull(); + } + // SAFETY NOTE: Safe because it doesn't take or return values. + unsafe { FOG_IPCPayloadFull() }; +} + +// Reason: We instrument the error counts, +// but don't need more detailed error information at the moment. +#[allow(clippy::result_unit_err)] +pub fn replay_from_buf(buf: &[u8]) -> Result<(), ()> { + // TODO: Instrument failures to find metrics by id. + let ipc_payload: IPCPayload = bincode::deserialize(buf).map_err(|_| ())?; + for (id, value) in ipc_payload.counters.into_iter() { + if id.0 & (1 << crate::factory::DYNAMIC_METRIC_BIT) > 0 { + let map = crate::factory::__jog_metric_maps::COUNTER_MAP + .read() + .expect("Read lock for dynamic counter map was poisoned"); + if let Some(metric) = map.get(&id) { + metric.add(value); + } + } else if let Some(metric) = __glean_metric_maps::COUNTER_MAP.get(&id) { + metric.add(value); + } + } + for (id, samples) in ipc_payload.custom_samples.into_iter() { + if id.0 & (1 << crate::factory::DYNAMIC_METRIC_BIT) > 0 { + let map = crate::factory::__jog_metric_maps::CUSTOM_DISTRIBUTION_MAP + .read() + .expect("Read lock for dynamic custom distribution map was poisoned"); + if let Some(metric) = map.get(&id) { + metric.accumulate_samples_signed(samples); + } + } else if let Some(metric) = __glean_metric_maps::CUSTOM_DISTRIBUTION_MAP.get(&id) { + metric.accumulate_samples_signed(samples); + } + } + for (id, value) in ipc_payload.denominators.into_iter() { + if id.0 & (1 << crate::factory::DYNAMIC_METRIC_BIT) > 0 { + let map = crate::factory::__jog_metric_maps::DENOMINATOR_MAP + .read() + .expect("Read lock for dynamic denominator map was poisoned"); + if let Some(metric) = map.get(&id) { + metric.add(value); + } + } else if let Some(metric) = __glean_metric_maps::DENOMINATOR_MAP.get(&id) { + metric.add(value); + } + } + for (id, records) in ipc_payload.events.into_iter() { + if id.0 & (1 << crate::factory::DYNAMIC_METRIC_BIT) > 0 { + let map = crate::factory::__jog_metric_maps::EVENT_MAP + .read() + .expect("Read lock for dynamic event map was poisoned"); + if let Some(metric) = map.get(&id) { + for (timestamp, extra) in records.into_iter() { + metric.record_with_time(timestamp, extra); + } + } + } else { + for (timestamp, extra) in records.into_iter() { + let _ = __glean_metric_maps::record_event_by_id_with_time(id, timestamp, extra); + } + } + } + for (id, labeled_counts) in ipc_payload.labeled_counters.into_iter() { + if id.0 & (1 << crate::factory::DYNAMIC_METRIC_BIT) > 0 { + let map = crate::factory::__jog_metric_maps::LABELED_COUNTER_MAP + .read() + .expect("Read lock for dynamic labeled counter map was poisoned"); + if let Some(metric) = map.get(&id) { + for (label, count) in labeled_counts.into_iter() { + metric.get(&label).add(count); + } + } + } else { + for (label, count) in labeled_counts.into_iter() { + __glean_metric_maps::labeled_counter_get(id.0, &label).add(count); + } + } + } + for (id, samples) in ipc_payload.memory_samples.into_iter() { + if id.0 & (1 << crate::factory::DYNAMIC_METRIC_BIT) > 0 { + let map = crate::factory::__jog_metric_maps::MEMORY_DISTRIBUTION_MAP + .read() + .expect("Read lock for dynamic memory dist map was poisoned"); + if let Some(metric) = map.get(&id) { + samples + .into_iter() + .for_each(|sample| metric.accumulate(sample)); + } + } else if let Some(metric) = __glean_metric_maps::MEMORY_DISTRIBUTION_MAP.get(&id) { + samples + .into_iter() + .for_each(|sample| metric.accumulate(sample)); + } + } + for (id, value) in ipc_payload.numerators.into_iter() { + if id.0 & (1 << crate::factory::DYNAMIC_METRIC_BIT) > 0 { + let map = crate::factory::__jog_metric_maps::NUMERATOR_MAP + .read() + .expect("Read lock for dynamic numerator map was poisoned"); + if let Some(metric) = map.get(&id) { + metric.add_to_numerator(value); + } + } else if let Some(metric) = __glean_metric_maps::NUMERATOR_MAP.get(&id) { + metric.add_to_numerator(value); + } + } + for (id, (n, d)) in ipc_payload.rates.into_iter() { + if id.0 & (1 << crate::factory::DYNAMIC_METRIC_BIT) > 0 { + let map = crate::factory::__jog_metric_maps::RATE_MAP + .read() + .expect("Read lock for dynamic rate map was poisoned"); + if let Some(metric) = map.get(&id) { + metric.add_to_numerator(n); + metric.add_to_denominator(d); + } + } else if let Some(metric) = __glean_metric_maps::RATE_MAP.get(&id) { + metric.add_to_numerator(n); + metric.add_to_denominator(d); + } + } + for (id, strings) in ipc_payload.string_lists.into_iter() { + if id.0 & (1 << crate::factory::DYNAMIC_METRIC_BIT) > 0 { + let map = crate::factory::__jog_metric_maps::STRING_LIST_MAP + .read() + .expect("Read lock for dynamic string list map was poisoned"); + if let Some(metric) = map.get(&id) { + strings.iter().for_each(|s| metric.add(s)); + } + } else if let Some(metric) = __glean_metric_maps::STRING_LIST_MAP.get(&id) { + strings.iter().for_each(|s| metric.add(s)); + } + } + for (id, samples) in ipc_payload.timing_samples.into_iter() { + if id.0 & (1 << crate::factory::DYNAMIC_METRIC_BIT) > 0 { + let map = crate::factory::__jog_metric_maps::TIMING_DISTRIBUTION_MAP + .read() + .expect("Read lock for dynamic timing distribution map was poisoned"); + if let Some(metric) = map.get(&id) { + metric.accumulate_raw_samples_nanos(samples); + } + } else if let Some(metric) = __glean_metric_maps::TIMING_DISTRIBUTION_MAP.get(&id) { + metric.accumulate_raw_samples_nanos(samples); + } + } + Ok(()) +} diff --git a/toolkit/components/glean/api/src/lib.rs b/toolkit/components/glean/api/src/lib.rs new file mode 100644 index 0000000000..2f51b94917 --- /dev/null +++ b/toolkit/components/glean/api/src/lib.rs @@ -0,0 +1,27 @@ +// 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 public FOG APIs, for Rust consumers. + +// Re-exporting for later use in generated code. +pub extern crate chrono; +pub extern crate once_cell; +pub extern crate uuid; + +// Re-exporting for use in user tests. +pub use private::{DistributionData, ErrorType, RecordedEvent}; + +// Must appear before `metrics` or its use of `ffi`'s macros will fail. +#[macro_use] +mod ffi; + +pub mod factory; +pub mod metrics; +pub mod pings; +pub mod private; + +pub mod ipc; + +#[cfg(test)] +mod common_test; diff --git a/toolkit/components/glean/api/src/metrics.rs b/toolkit/components/glean/api/src/metrics.rs new file mode 100644 index 0000000000..bca886e110 --- /dev/null +++ b/toolkit/components/glean/api/src/metrics.rs @@ -0,0 +1,20 @@ +// 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/. + +//! This file contains the Generated Glean Metrics API +//! +//! The contents of this module are generated by +//! `toolkit/components/glean/build_scripts/glean_parser_ext/run_glean_parser.py`, from +//! metrics files identified in 'toolkit/components/glean/metrics_index.py`. + +include!(mozbuild::objdir_path!( + "toolkit/components/glean/api/src/metrics.rs" +)); + +use crate::private::{EventMetric, ExtraKeys}; + +/// Helper to get the number of allowed extra keys for a given event metric. +fn extra_keys_len<K: ExtraKeys>(_event: &EventMetric<K>) -> usize { + K::ALLOWED_KEYS.len() +} diff --git a/toolkit/components/glean/api/src/pings.rs b/toolkit/components/glean/api/src/pings.rs new file mode 100644 index 0000000000..f1d0332695 --- /dev/null +++ b/toolkit/components/glean/api/src/pings.rs @@ -0,0 +1,13 @@ +// 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/. + +//! This file contains the Generated Glean Metrics API (Ping portion) +//! +//! The contents of this module are generated by +//! `toolkit/components/glean/build_scripts/glean_parser_ext/run_glean_parser.py`, from +//! 'toolkit/components/glean/pings.yaml`. + +include!(mozbuild::objdir_path!( + "toolkit/components/glean/api/src/pings.rs" +)); 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..3869b31f3b --- /dev/null +++ b/toolkit/components/glean/api/src/private/boolean.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 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-parent process. Ignoring."); + // 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-parent 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-parent 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..7e1498f70f --- /dev/null +++ b/toolkit/components/glean/api/src/private/counter.rs @@ -0,0 +1,182 @@ +// 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 } + } + } + + #[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..9892ccf90c --- /dev/null +++ b/toolkit/components/glean/api/src/private/datetime.rs @@ -0,0 +1,238 @@ +// 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-parent process. Ignoring."); + // 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-parent process. Ignoring." + ); + // 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-parent 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-parent 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..0f45eb9b93 --- /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-parent 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-parent 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..0934752657 --- /dev/null +++ b/toolkit/components/glean/api/src/private/ping.rs @@ -0,0 +1,113 @@ +// 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, + reason_codes: Vec<String>, + ) -> Self { + if need_ipc() { + Ping::Child + } else { + Ping::Parent(glean::private::PingType::new( + name, + include_client_id, + send_if_empty, + 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. Ignoring."); + // 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, 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..cce42c4aea --- /dev/null +++ b/toolkit/components/glean/api/src/private/quantity.rs @@ -0,0 +1,154 @@ +// 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-parent process. Ignoring."); + // 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-parent 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-parent 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..06e5cd3db1 --- /dev/null +++ b/toolkit/components/glean/api/src/private/string.rs @@ -0,0 +1,182 @@ +// 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. Ignoring."); + // 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-parent 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-parent 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..b3c7dd4d26 --- /dev/null +++ b/toolkit/components/glean/api/src/private/string_list.rs @@ -0,0 +1,209 @@ +// 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. Ignoring.", + c.0 + ); + // 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..3e1ef4bff3 --- /dev/null +++ b/toolkit/components/glean/api/src/private/text.rs @@ -0,0 +1,176 @@ +// 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. Ignoring.") + } + } + } + + /// **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-parent 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-parent 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..0d215cd7ff --- /dev/null +++ b/toolkit/components/glean/api/src/private/timespan.rs @@ -0,0 +1,165 @@ +// 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. Ignoring." + ); + // 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. Ignoring."); + // 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. Ignoring."); + // 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. Ignoring."); + // 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. Ignoring."); + // 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-parent 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-parent 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..44ebf0a884 --- /dev/null +++ b/toolkit/components/glean/api/src/private/timing_distribution.rs @@ -0,0 +1,444 @@ +// 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") + } + } + } + + pub(crate) fn accumulate_raw_samples_nanos(&self, samples: Vec<u64>) { + match self { + TimingDistributionMetric::Parent { inner, .. } => { + inner.accumulate_raw_samples_nanos(samples); + } + TimingDistributionMetric::Child(_) => { + // TODO: Instrument this error + log::error!("Can't record samples for a timing distribution 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); + } + } + } + } + } + + /// **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..1ad49c3a7d --- /dev/null +++ b/toolkit/components/glean/api/src/private/url.rs @@ -0,0 +1,124 @@ +// 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. Ignoring."); + // 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-parent 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-parent 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..a34602ac48 --- /dev/null +++ b/toolkit/components/glean/api/src/private/uuid.rs @@ -0,0 +1,165 @@ +// 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. Ignoring."); + // 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-parent 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. Ignoring."); + // 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-parent 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-parent 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" + ); + } +} |