diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
commit | 43a97878ce14b72f0981164f87f2e35e14151312 (patch) | |
tree | 620249daf56c0258faa40cbdcf9cfba06de2a846 /toolkit/components/glean | |
parent | Initial commit. (diff) | |
download | firefox-upstream.tar.xz firefox-upstream.zip |
Adding upstream version 110.0.1.upstream/110.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
212 files changed, 27047 insertions, 0 deletions
diff --git a/toolkit/components/glean/Cargo.toml b/toolkit/components/glean/Cargo.toml new file mode 100644 index 0000000000..2bf4a745e0 --- /dev/null +++ b/toolkit/components/glean/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "fog_control" +version = "0.1.0" +authors = ["Glean SDK team <glean-team@mozilla.com>"] +edition = "2018" +license = "MPL-2.0" + +[dependencies] +glean = "51.8.2" +log = "0.4" +nserror = { path = "../../../xpcom/rust/nserror" } +nsstring = { path = "../../../xpcom/rust/nsstring" } +static_prefs = { path = "../../../modules/libpref/init/static_prefs" } +xpcom = { path = "../../../xpcom/rust/xpcom" } +once_cell = "1.2.0" +fog = { path = "./api" } +cstr = "0.2" +viaduct = "0.1" +url = "2.1" +thin-vec = { version = "0.2.1", features = ["gecko-ffi"] } + +[features] +# Leave data collection enabled, but disable upload. +disable_upload = [] +# Letting us know we're compiling with Gecko symbols. +with_gecko = ["fog/with_gecko"] +# Opt into a larger preinit queue +million_queue = ["glean/preinit_million_queue"] diff --git a/toolkit/components/glean/api/Cargo.toml b/toolkit/components/glean/api/Cargo.toml new file mode 100644 index 0000000000..9879bc800a --- /dev/null +++ b/toolkit/components/glean/api/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "fog" +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 = "51.8.2" +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..49b7416d72 --- /dev/null +++ b/toolkit/components/glean/api/src/common_test.rs @@ -0,0 +1,52 @@ +// 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, + }; + + 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..8aeb095d54 --- /dev/null +++ b/toolkit/components/glean/api/src/ffi/labeled.rs @@ -0,0 +1,41 @@ +// 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::sync::atomic::Ordering; + +#[no_mangle] +pub extern "C" fn fog_labeled_boolean_get(id: u32, label: &nsACString) -> u32 { + labeled_submetric_get!( + id, + label, + LABELED_BOOLEAN_MAP, + 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, + 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, + 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..17b2faa6eb --- /dev/null +++ b/toolkit/components/glean/api/src/ffi/macros.rs @@ -0,0 +1,204 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +//! 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 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 { + 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), + } + } 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 label of the submetric. +/// * `$labeled_map` - The name of the labeled metric's map 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, $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); + { + with_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); + }); + } + + 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 + }}; +} 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..8f31d769c2 --- /dev/null +++ b/toolkit/components/glean/api/src/ffi/mod.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/. + +#![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 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/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..ac5653d9e6 --- /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 if let Some(metric) = __glean_metric_maps::LABELED_COUNTER_MAP.get(&id) { + for (label, count) in labeled_counts.into_iter() { + metric.get(&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..196edee293 --- /dev/null +++ b/toolkit/components/glean/api/src/metrics.rs @@ -0,0 +1,24 @@ +// 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`. +//! +//! When running clippy, the contents are not generated. +//! There is a clippy-only section at the bottom that may need to be hand-edited +//! to fix linter errors. + +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..b0bedafa15 --- /dev/null +++ b/toolkit/components/glean/api/src/private/labeled.rs @@ -0,0 +1,356 @@ +// 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; + +/// 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>> = 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> { + /// 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>, +} + +impl<T> LabeledMetric<T> +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<String>>, + ) -> LabeledMetric<T> { + let core = glean::private::LabeledMetric::new(meta, labels); + LabeledMetric { id, core } + } +} + +#[inherent] +impl<U> glean::traits::Labeled<U> for LabeledMetric<U> +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::*; + + // Smoke test for what should be the generated code. + static GLOBAL_METRIC: Lazy<LabeledMetric<LabeledBooleanMetric>> = 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> = 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> = 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> = 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> = LabeledMetric::new( + 0.into(), + CommonMetricData { + name: "bool".into(), + category: "labeled".into(), + send_in_pings: store_names, + disabled: false, + ..Default::default() + }, + None, + ); + + metric + .get("this_string_has_more_than_thirty_characters") + .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()]; + + let metric: LabeledMetric<LabeledBooleanMetric> = 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..9b8b57818c --- /dev/null +++ b/toolkit/components/glean/api/src/private/mod.rs @@ -0,0 +1,74 @@ +// 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 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::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/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" + ); + } +} diff --git a/toolkit/components/glean/bindings/Category.cpp b/toolkit/components/glean/bindings/Category.cpp new file mode 100644 index 0000000000..112fc365d4 --- /dev/null +++ b/toolkit/components/glean/bindings/Category.cpp @@ -0,0 +1,75 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/dom/GleanBinding.h" +#include "mozilla/glean/bindings/Glean.h" +#include "mozilla/glean/bindings/Category.h" +#include "mozilla/glean/bindings/GleanJSMetricsLookup.h" +#include "mozilla/glean/bindings/jog/JOG.h" + +namespace mozilla::glean { + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_0(Category) + +NS_IMPL_CYCLE_COLLECTING_ADDREF(Category) +NS_IMPL_CYCLE_COLLECTING_RELEASE(Category) +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(Category) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +JSObject* Category::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return dom::GleanCategory_Binding::Wrap(aCx, this, aGivenProto); +} + +already_AddRefed<nsISupports> Category::NamedGetter(const nsAString& aName, + bool& aFound) { + aFound = false; + + nsCString metricName; + metricName.AppendASCII(mName); + metricName.AppendLiteral("."); + AppendUTF16toUTF8(aName, metricName); + + Maybe<uint32_t> metricIdx = JOG::GetMetric(metricName); + if (metricIdx.isNothing() && !JOG::AreRuntimeMetricsComprehensive()) { + metricIdx = MetricByNameLookup(metricName); + } + + if (metricIdx.isNothing()) { + aFound = false; + return nullptr; + } + + aFound = true; + return NewMetricFromId(metricIdx.value()); +} + +bool Category::NameIsEnumerable(const nsAString& aName) { return false; } + +void Category::GetSupportedNames(nsTArray<nsString>& aNames) { + // We don't get dynamic metric names because we don't want to store them. + if (!JOG::AreRuntimeMetricsComprehensive()) { + for (metric_entry_t entry : sMetricByNameLookupEntries) { + const char* identifierBuf = GetMetricIdentifier(entry); + nsDependentCString identifier(identifierBuf); + + // We're iterating all metrics, + // so we need to check for the ones in the right category. + // + // We need to ensure that we found _only_ the exact category by checking + // it is followed by a dot. + if (StringBeginsWith(identifier, mName) && + identifier.CharAt(mName.Length()) == '.') { + const char* metricName = &identifierBuf[mName.Length() + 1]; + aNames.AppendElement()->AssignASCII(metricName); + } + } + } +} + +} // namespace mozilla::glean diff --git a/toolkit/components/glean/bindings/Category.h b/toolkit/components/glean/bindings/Category.h new file mode 100644 index 0000000000..ce85c71138 --- /dev/null +++ b/toolkit/components/glean/bindings/Category.h @@ -0,0 +1,42 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_glean_Category_h +#define mozilla_glean_Category_h + +#include "js/TypeDecls.h" +#include "nsISupports.h" +#include "nsTArrayForwardDeclare.h" +#include "nsWrapperCache.h" + +namespace mozilla::glean { + +class Category final : public nsISupports, public nsWrapperCache { + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(Category) + + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + nsISupports* GetParentObject() { return nullptr; } + + explicit Category(nsCString&& aName) : mName(aName) {} + + already_AddRefed<nsISupports> NamedGetter(const nsAString& aName, + bool& aFound); + bool NameIsEnumerable(const nsAString& aName); + void GetSupportedNames(nsTArray<nsString>& aNames); + + private: + nsCString mName; + + protected: + virtual ~Category() = default; +}; + +} // namespace mozilla::glean + +#endif /* mozilla_glean_Category_h */ diff --git a/toolkit/components/glean/bindings/Glean.cpp b/toolkit/components/glean/bindings/Glean.cpp new file mode 100644 index 0000000000..141e17e0f3 --- /dev/null +++ b/toolkit/components/glean/bindings/Glean.cpp @@ -0,0 +1,111 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/dom/DOMJSClass.h" +#include "mozilla/dom/GleanBinding.h" +#include "mozilla/dom/BindingUtils.h" +#include "mozilla/glean/bindings/Glean.h" +#include "mozilla/glean/bindings/Category.h" +#include "mozilla/glean/bindings/GleanJSMetricsLookup.h" +#include "mozilla/glean/bindings/jog/jog_ffi_generated.h" +#include "mozilla/glean/bindings/jog/JOG.h" +#include "MainThreadUtils.h" +#include "js/PropertyAndElement.h" // JS_DefineProperty + +namespace mozilla::glean { + +// Whether the runtime-registered metrics should be treated as comprehensive, +// or additive. If comprehensive, a metric not registered at runtime is a +// metric that doesn't exist. If additive, a metric not registered at runtime +// may still exist if it was registered at compile time. +// If we're supporting Artefact Builds, we treat them as comprehensive. +// Threading: Must only be read or written to on the main thread. +static bool gRuntimeMetricsComprehensive = false; + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_0(Glean) + +NS_IMPL_CYCLE_COLLECTING_ADDREF(Glean) +NS_IMPL_CYCLE_COLLECTING_RELEASE(Glean) +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(Glean) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +JSObject* Glean::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) { + return dom::GleanImpl_Binding::Wrap(aCx, this, aGivenProto); +} + +// static +bool Glean::DefineGlean(JSContext* aCx, JS::Handle<JSObject*> aGlobal) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(JS::GetClass(aGlobal)->flags & JSCLASS_DOM_GLOBAL, + "Passed object is not a global object!"); + + nsIGlobalObject* global = xpc::CurrentNativeGlobal(aCx); + if (NS_WARN_IF(!global)) { + return false; + } + + JS::Rooted<JS::Value> glean(aCx); + js::AssertSameCompartment(aCx, aGlobal); + + auto impl = MakeRefPtr<Glean>(); + if (!dom::GetOrCreateDOMReflector(aCx, impl.get(), &glean)) { + return false; + } + + return JS_DefineProperty(aCx, aGlobal, "Glean", glean, JSPROP_ENUMERATE); +} + +already_AddRefed<Category> Glean::NamedGetter(const nsAString& aName, + bool& aFound) { + MOZ_ASSERT(NS_IsMainThread()); + + JOG::EnsureRuntimeMetricsRegistered(); + + NS_ConvertUTF16toUTF8 categoryName(aName); + if (JOG::HasCategory(categoryName)) { + aFound = true; + return MakeAndAddRef<Category>(std::move(categoryName)); + } + + if (gRuntimeMetricsComprehensive) { + // This category might be built-in, but since the runtime metrics are + // comprehensive, that just signals that the category was removed locally. + aFound = false; + return nullptr; + } + + Maybe<uint32_t> categoryIdx = CategoryByNameLookup(categoryName); + if (categoryIdx.isNothing()) { + aFound = false; + return nullptr; + } + + aFound = true; + nsDependentCString name(&gCategoryStringTable[categoryIdx.value()]); + return MakeAndAddRef<Category>(std::move(name)); +} + +bool Glean::NameIsEnumerable(const nsAString& aName) { return false; } + +void Glean::GetSupportedNames(nsTArray<nsString>& aNames) { + JOG::GetCategoryNames(aNames); + if (!JOG::AreRuntimeMetricsComprehensive()) { + for (category_entry_t entry : sCategoryByNameLookupEntries) { + const char* categoryName = GetCategoryName(entry); + aNames.AppendElement()->AssignASCII(categoryName); + } + } +} + +// static +void Glean::TestSetRuntimeMetricsComprehensive(bool aIsComprehensive) { + MOZ_ASSERT(NS_IsMainThread()); + gRuntimeMetricsComprehensive = aIsComprehensive; +} + +} // namespace mozilla::glean diff --git a/toolkit/components/glean/bindings/Glean.h b/toolkit/components/glean/bindings/Glean.h new file mode 100644 index 0000000000..4305e68aa7 --- /dev/null +++ b/toolkit/components/glean/bindings/Glean.h @@ -0,0 +1,51 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_glean_Glean_h +#define mozilla_glean_Glean_h + +#include "js/TypeDecls.h" +#include "nsISupports.h" +#include "nsTArrayForwardDeclare.h" +#include "nsWrapperCache.h" + +namespace mozilla::glean { + +class Category; + +class Glean final : public nsISupports, public nsWrapperCache { + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(Glean) + + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + nsISupports* GetParentObject() { return nullptr; } + + static bool DefineGlean(JSContext* aCx, JS::Handle<JSObject*> aGlobal); + + already_AddRefed<Category> NamedGetter(const nsAString& aName, bool& aFound); + bool NameIsEnumerable(const nsAString& aName); + void GetSupportedNames(nsTArray<nsString>& aNames); + + /* + * Test-only method. + * + * Set whether we should treat runtime-registered metrics as the + * comprehensive list of all metrics, or whether compile-time-registered + * metrics are allowed to count too. + * + * Allows us to test Artifact Build support flexibly. + */ + static void TestSetRuntimeMetricsComprehensive(bool aIsComprehensive); + + protected: + virtual ~Glean() = default; +}; + +} // namespace mozilla::glean + +#endif /* mozilla_glean_Glean */ diff --git a/toolkit/components/glean/bindings/GleanPings.cpp b/toolkit/components/glean/bindings/GleanPings.cpp new file mode 100644 index 0000000000..5103adb22f --- /dev/null +++ b/toolkit/components/glean/bindings/GleanPings.cpp @@ -0,0 +1,88 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/glean/bindings/GleanPings.h" + +#include "mozilla/dom/BindingUtils.h" +#include "mozilla/dom/DOMJSClass.h" +#include "mozilla/dom/GleanPingsBinding.h" +#include "mozilla/glean/bindings/GleanJSPingsLookup.h" +#include "mozilla/glean/bindings/Ping.h" +#include "MainThreadUtils.h" +#include "js/PropertyAndElement.h" // JS_DefineProperty + +namespace mozilla::glean { + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_0(GleanPings) + +NS_IMPL_CYCLE_COLLECTING_ADDREF(GleanPings) +NS_IMPL_CYCLE_COLLECTING_RELEASE(GleanPings) +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(GleanPings) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +JSObject* GleanPings::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return dom::GleanPingsImpl_Binding::Wrap(aCx, this, aGivenProto); +} + +// static +bool GleanPings::DefineGleanPings(JSContext* aCx, + JS::Handle<JSObject*> aGlobal) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(JS::GetClass(aGlobal)->flags & JSCLASS_DOM_GLOBAL, + "Passed object is not a global object!"); + + nsIGlobalObject* global = xpc::CurrentNativeGlobal(aCx); + if (NS_WARN_IF(!global)) { + return false; + } + + JS::Rooted<JS::Value> gleanPings(aCx); + js::AssertSameCompartment(aCx, aGlobal); + + auto impl = MakeRefPtr<GleanPings>(); + if (!dom::GetOrCreateDOMReflector(aCx, impl.get(), &gleanPings)) { + return false; + } + + return JS_DefineProperty(aCx, aGlobal, "GleanPings", gleanPings, + JSPROP_ENUMERATE); +} + +already_AddRefed<GleanPing> GleanPings::NamedGetter(const nsAString& aName, + bool& aFound) { + aFound = false; + + NS_ConvertUTF16toUTF8 pingName(aName); + + JOG::EnsureRuntimeMetricsRegistered(); + + Maybe<uint32_t> pingId = JOG::GetPing(pingName); + if (pingId.isNothing() && !JOG::AreRuntimeMetricsComprehensive()) { + pingId = PingByNameLookup(pingName); + } + + if (pingId.isNothing()) { + aFound = false; + return nullptr; + } + + aFound = true; + return MakeAndAddRef<GleanPing>(pingId.value()); +} + +bool GleanPings::NameIsEnumerable(const nsAString& aName) { return false; } + +void GleanPings::GetSupportedNames(nsTArray<nsString>& aNames) { + for (uint8_t idx : sPingByNameLookupEntries) { + const char* pingName = GetPingName(idx); + aNames.AppendElement()->AssignASCII(pingName); + } +} + +} // namespace mozilla::glean diff --git a/toolkit/components/glean/bindings/GleanPings.h b/toolkit/components/glean/bindings/GleanPings.h new file mode 100644 index 0000000000..2dd1ff4b83 --- /dev/null +++ b/toolkit/components/glean/bindings/GleanPings.h @@ -0,0 +1,38 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_glean_GleanPings_h +#define mozilla_glean_GleanPings_h + +#include "mozilla/dom/BindingDeclarations.h" +#include "mozilla/glean/bindings/Ping.h" +#include "nsISupports.h" +#include "nsWrapperCache.h" + +namespace mozilla::glean { + +class GleanPings final : public nsISupports, public nsWrapperCache { + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(GleanPings) + + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + nsISupports* GetParentObject() { return nullptr; } + + static bool DefineGleanPings(JSContext* aCx, JS::Handle<JSObject*> aGlobal); + + already_AddRefed<GleanPing> NamedGetter(const nsAString& aName, bool& aFound); + bool NameIsEnumerable(const nsAString& aName); + void GetSupportedNames(nsTArray<nsString>& aNames); + + protected: + virtual ~GleanPings() = default; +}; + +} // namespace mozilla::glean + +#endif /* mozilla_glean_GleanPings */ diff --git a/toolkit/components/glean/bindings/MetricTypes.h b/toolkit/components/glean/bindings/MetricTypes.h new file mode 100644 index 0000000000..bec4e87ad2 --- /dev/null +++ b/toolkit/components/glean/bindings/MetricTypes.h @@ -0,0 +1,26 @@ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_Glean_MetricTypes_h +#define mozilla_Glean_MetricTypes_h + +#include "mozilla/glean/bindings/Boolean.h" +#include "mozilla/glean/bindings/Counter.h" +#include "mozilla/glean/bindings/CustomDistribution.h" +#include "mozilla/glean/bindings/Datetime.h" +#include "mozilla/glean/bindings/Denominator.h" +#include "mozilla/glean/bindings/Event.h" +#include "mozilla/glean/bindings/Labeled.h" +#include "mozilla/glean/bindings/MemoryDistribution.h" +#include "mozilla/glean/bindings/Numerator.h" +#include "mozilla/glean/bindings/Quantity.h" +#include "mozilla/glean/bindings/Rate.h" +#include "mozilla/glean/bindings/String.h" +#include "mozilla/glean/bindings/StringList.h" +#include "mozilla/glean/bindings/Timespan.h" +#include "mozilla/glean/bindings/TimingDistribution.h" +#include "mozilla/glean/bindings/Url.h" +#include "mozilla/glean/bindings/Uuid.h" + +#endif // mozilla_Glean_MetricTypes_h diff --git a/toolkit/components/glean/bindings/jog/Cargo.toml b/toolkit/components/glean/bindings/jog/Cargo.toml new file mode 100644 index 0000000000..c0ef574214 --- /dev/null +++ b/toolkit/components/glean/bindings/jog/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "jog" +version = "0.1.0" +authors = ["Glean SDK team <glean-team@mozilla.com>"] +edition = "2021" +publish = false +license = "MPL-2.0" + +[dependencies] +fog = { path = "../../api" } +log = "0.4" +mozbuild = "0.1" +nsstring = { path = "../../../../../xpcom/rust/nsstring", optional = true } +once_cell = "1.2.0" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +thin-vec = { version = "0.2.1", features = ["gecko-ffi"] } + +[features] +with_gecko = [ "nsstring" ] diff --git a/toolkit/components/glean/bindings/jog/JOG.cpp b/toolkit/components/glean/bindings/jog/JOG.cpp new file mode 100644 index 0000000000..164d639016 --- /dev/null +++ b/toolkit/components/glean/bindings/jog/JOG.cpp @@ -0,0 +1,221 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/glean/bindings/jog/JOG.h" + +#include <locale> + +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/DataMutex.h" +#include "mozilla/glean/bindings/jog/jog_ffi_generated.h" +#include "mozilla/Omnijar.h" +#include "mozilla/Tuple.h" +#include "nsDirectoryServiceDefs.h" +#include "nsDirectoryServiceUtils.h" +#include "nsThreadUtils.h" +#include "nsTHashMap.h" +#include "nsTHashSet.h" + +namespace mozilla::glean { + +// Storage +// Thread Safety: Only used on the main thread. +StaticAutoPtr<nsTHashSet<nsCString>> gCategories; +StaticAutoPtr<nsTHashMap<nsCString, uint32_t>> gMetrics; +StaticAutoPtr<nsTHashMap<nsCString, uint32_t>> gPings; + +// static +bool JOG::HasCategory(const nsACString& aCategoryName) { + MOZ_ASSERT(NS_IsMainThread()); + + return gCategories && gCategories->Contains(aCategoryName); +} + +static Maybe<bool> sFoundAndLoadedJogfile; + +// static +bool JOG::EnsureRuntimeMetricsRegistered(bool aForce) { + MOZ_ASSERT(NS_IsMainThread()); + +#ifdef MOZILLA_OFFICIAL + // In the event we're an official build we want there to be no chance we might + // accidentally perform I/O on the main thread. + return false; +#endif + + if (sFoundAndLoadedJogfile) { + return sFoundAndLoadedJogfile.value(); + } + sFoundAndLoadedJogfile.emplace(false); + + if (!mozilla::IsDevelopmentBuild()) { + // Supporting Artifact Builds is a developer-only thing. + // We're on the main thread here. + // Let's not spend any more time than we need to. + return false; + } + // The metrics we need to process were placed in GreD in jogfile.json + // That file was generated by + // toolkit/components/glean/build_scripts/glean_parser_ext/jog.py + nsCOMPtr<nsIFile> jogfile; + if (NS_WARN_IF(NS_FAILED( + NS_GetSpecialDirectory(NS_GRE_DIR, getter_AddRefs(jogfile))))) { + return false; + } + if (NS_WARN_IF(NS_FAILED(jogfile->Append(u"jogfile.json"_ns)))) { + return false; + } + bool jogfileExists = false; + if (NS_WARN_IF(NS_FAILED(jogfile->Exists(&jogfileExists))) || + !jogfileExists) { + return false; + } + + // We _could_ register everything here in C++ land, + // but let's use Rust because (among other reasons) it's more fun. + nsAutoString jogfileString; + if (NS_WARN_IF(NS_FAILED(jogfile->GetPath(jogfileString)))) { + return false; + } + sFoundAndLoadedJogfile.emplace(jog::jog_load_jogfile(&jogfileString)); + return sFoundAndLoadedJogfile.value(); +} + +// static +bool JOG::AreRuntimeMetricsComprehensive() { + MOZ_ASSERT(NS_IsMainThread()); + return sFoundAndLoadedJogfile && sFoundAndLoadedJogfile.value(); +} + +// static +void JOG::GetCategoryNames(nsTArray<nsString>& aNames) { + MOZ_ASSERT(NS_IsMainThread()); + if (!gCategories) { + return; + } + for (const auto& category : *gCategories) { + aNames.EmplaceBack(NS_ConvertUTF8toUTF16(category)); + } +} + +// static +Maybe<uint32_t> JOG::GetMetric(const nsACString& aMetricName) { + MOZ_ASSERT(NS_IsMainThread()); + return !gMetrics ? Nothing() : gMetrics->MaybeGet(aMetricName); +} + +// static +Maybe<uint32_t> JOG::GetPing(const nsACString& aPingName) { + MOZ_ASSERT(NS_IsMainThread()); + return !gPings ? Nothing() : gPings->MaybeGet(aPingName); +} + +} // namespace mozilla::glean + +// static +nsCString dottedSnakeToCamel(const nsACString& aSnake) { + nsCString camel; + bool first = true; + for (const nsACString& segment : aSnake.Split('_')) { + for (const nsACString& part : segment.Split('.')) { + if (first) { + first = false; + camel.Append(part); + } else if (part.Length()) { + char lower = part.CharAt(0); + if ('a' <= lower && lower <= 'z') { + camel.Append( + std::toupper(lower, std::locale())); // append the Capital. + camel.Append(part.BeginReading() + 1, + part.Length() - 1); // append the rest. + } else { + // Not gonna try to capitalize anything outside a->z. + camel.Append(part); + } + } + } + } + return camel; +} + +// static +nsCString kebabToCamel(const nsACString& aKebab) { + nsCString camel; + bool first = true; + for (const nsACString& segment : aKebab.Split('-')) { + if (first) { + first = false; + camel.Append(segment); + } else if (segment.Length()) { + char lower = segment.CharAt(0); + if ('a' <= lower && lower <= 'z') { + camel.Append( + std::toupper(lower, std::locale())); // append the Capital. + camel.Append(segment.BeginReading() + 1, + segment.Length() - 1); // append the rest. + } else { + // Not gonna try to capitalize anything outside a->z. + camel.Append(segment); + } + } + } + return camel; +} + +using mozilla::AppShutdown; +using mozilla::ShutdownPhase; +using mozilla::glean::gCategories; +using mozilla::glean::gMetrics; +using mozilla::glean::gPings; + +extern "C" NS_EXPORT void JOG_RegisterMetric(const nsACString& aCategory, + const nsACString& aName, + uint32_t aMetric) { + MOZ_ASSERT(NS_IsMainThread()); + + if (AppShutdown::IsInOrBeyond(ShutdownPhase::XPCOMWillShutdown)) { + return; + } + + // aCategory is dotted.snake_case. aName is snake_case. + auto categoryCamel = dottedSnakeToCamel(aCategory); + auto nameCamel = dottedSnakeToCamel(aName); + + // Register the category + if (!gCategories) { + gCategories = new nsTHashSet<nsCString>(); + RunOnShutdown([&] { gCategories = nullptr; }, + ShutdownPhase::XPCOMWillShutdown); + } + gCategories->Insert(categoryCamel); + + // Register the metric + if (!gMetrics) { + gMetrics = new nsTHashMap<nsCString, uint32_t>(); + RunOnShutdown([&] { gMetrics = nullptr; }, + ShutdownPhase::XPCOMWillShutdown); + } + gMetrics->InsertOrUpdate(categoryCamel + "."_ns + nameCamel, aMetric); +} + +extern "C" NS_EXPORT void JOG_RegisterPing(const nsACString& aPingName, + uint32_t aPingId) { + MOZ_ASSERT(NS_IsMainThread()); + + if (AppShutdown::IsInOrBeyond(ShutdownPhase::XPCOMWillShutdown)) { + return; + } + + // aPingName is kebab-case. JS expects camelCase. + auto pingCamel = kebabToCamel(aPingName); + + // Register the ping + if (!gPings) { + gPings = new nsTHashMap<nsCString, uint32_t>(); + RunOnShutdown([&] { gPings = nullptr; }, ShutdownPhase::XPCOMWillShutdown); + } + gPings->InsertOrUpdate(pingCamel, aPingId); +} diff --git a/toolkit/components/glean/bindings/jog/JOG.h b/toolkit/components/glean/bindings/jog/JOG.h new file mode 100644 index 0000000000..e261d04d88 --- /dev/null +++ b/toolkit/components/glean/bindings/jog/JOG.h @@ -0,0 +1,92 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_glean_JOG_h +#define mozilla_glean_JOG_h + +namespace mozilla::glean { + +class JOG { + public: + /** + * Returns whether JOG knows about a category by this name + * + * @param aCategoryName The category name to check. + * + * @returns true if JOG is aware of a category by the given name at this time + */ + static bool HasCategory(const nsACString& aCategoryName); + + /** + * Runs the runtime registrar. + * + * Locates the runtime metrics file and, if present, loads and processes it. + * + * Only does any work at all if mozilla::IsDevelopmentBuild() + * + * **Note:** When this function does something, it is expensive, running + * synchronous file I/O to ensure that the registration is complete when this + * call returns. + * + * @param aForce Set to `true` if you want to force the I/O to run. Defaults + * to `false`, which doesn't run the I/O if it's already run and + * returns the previous return value. + * @returns whether it found the runtime metrics file and succesfully loaded, + * processed, and registered the described metrics. + */ + static bool EnsureRuntimeMetricsRegistered(bool aForce = false); + + /** + * Returns whether, if a metric is absent in the runtime-registered metrics, + * you should check the compile-time-registered metrics. + * + * Runtime-registered metrics can either replace all compile-time-registered + * metrics (like in artefact builds) or just be supplementing compile-time- + * registered metrics (like addons/dynamic telemetry/etc). + * + * This is tied to the current state of runtime metric registration. So it + * may return false at one time and true later (e.g. if RuntimeRegistrar is + * run in between). + * + * @return true if you should treat the runtime-registered metrics as + * authoritative and comprehensive. + */ + static bool AreRuntimeMetricsComprehensive(); + + /** + * Adds the runtime-registered metrics' categories to `aNames`. + * + * @param aNames The list to add the categories' names to. + */ + static void GetCategoryNames(nsTArray<nsString>& aNames); + + /** + * Get the metric id+type in a u32 for a named runtime-registered metric. + * + * Return value's only useful to GleanJSMetricsLookup.h + * + * @param aMetricName The `myCategory.myName` dotted.camelCase metric name. + * @return Nothing() if no metric by that name was registered at runtime. + * Otherwise, the encoded u32 with metric id and metric type id for + * the runtime-registered metric. + */ + static Maybe<uint32_t> GetMetric(const nsACString& aMetricName); + + /** + * Get the ping id in a u32 for a named runtime-registered ping. + * + * Return value's only useful to GleanJSPingsLookup.h + * + * @param aPingName The ping name. + * @return Nothing() if no ping by that name was registered at runtime. + * Otherwise, the id for the runtime-registered ping. + */ + static Maybe<uint32_t> GetPing(const nsACString& aPingName); +}; + +} // namespace mozilla::glean + +#endif /* mozilla_glean_JOG_h */ diff --git a/toolkit/components/glean/bindings/jog/cbindgen.toml b/toolkit/components/glean/bindings/jog/cbindgen.toml new file mode 100644 index 0000000000..62139cc6c6 --- /dev/null +++ b/toolkit/components/glean/bindings/jog/cbindgen.toml @@ -0,0 +1,26 @@ +header = """/* 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 http://mozilla.org/MPL/2.0/. */ +#ifndef mozilla_glean_jog_ffi_generated_h +#define mozilla_glean_jog_ffi_generated_h +""" +trailer = """ +#endif // mozilla_glean_jog_ffi_generated_h +""" +autogen_warning = """/* DO NOT MODIFY THIS MANUALLY! This file was generated using cbindgen. */""" +include_version = true +braces = "SameLine" +line_length = 100 +tab_width = 2 +language = "C++" +namespaces = ["mozilla::glean::jog"] +includes = ["nsTArray.h", "nsString.h"] + +[export.rename] +"ThinVec" = "nsTArray" +#"nsCStringRepr" = "nsCString" + +[parse] +#parse_deps = true +#include = ["fog"] +#extra_bindings = ["fog"] diff --git a/toolkit/components/glean/bindings/jog/src/lib.rs b/toolkit/components/glean/bindings/jog/src/lib.rs new file mode 100644 index 0000000000..885a7a5bbb --- /dev/null +++ b/toolkit/components/glean/bindings/jog/src/lib.rs @@ -0,0 +1,248 @@ +/* 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 http://mozilla.org/MPL/2.0/. */ + +use fog::factory; +use fog::private::traits::HistogramType; +use fog::private::{CommonMetricData, Lifetime, MemoryUnit, TimeUnit}; +#[cfg(feature = "with_gecko")] +use nsstring::{nsACString, nsAString, nsCString}; +use serde::Deserialize; +use std::collections::HashMap; +use std::fs::File; +use std::io::BufReader; +use thin_vec::ThinVec; + +#[derive(Default, Deserialize)] +struct ExtraMetricArgs { + time_unit: Option<TimeUnit>, + memory_unit: Option<MemoryUnit>, + allowed_extra_keys: Option<Vec<String>>, + range_min: Option<u64>, + range_max: Option<u64>, + bucket_count: Option<u64>, + histogram_type: Option<HistogramType>, + numerators: Option<Vec<CommonMetricData>>, + labels: Option<Vec<String>>, +} + +/// Test-only method. +/// +/// Registers a metric. +/// Doesn't check to see if it's been registered before. +/// Doesn't check that it would pass schema validation if it were a real metric. +/// +/// `extra_args` is a JSON-encoded string in a form that serde can read into an ExtraMetricArgs. +/// +/// No effort has been made to make this pleasant to use, since it's for +/// internal testing only (ie, the testing of JOG itself). +#[cfg(feature = "with_gecko")] +#[no_mangle] +pub extern "C" fn jog_test_register_metric( + metric_type: &nsACString, + category: &nsACString, + name: &nsACString, + send_in_pings: &ThinVec<nsCString>, + lifetime: &nsACString, + disabled: bool, + extra_args: &nsACString, +) -> u32 { + log::warn!("Type: {:?}, Category: {:?}, Name: {:?}, SendInPings: {:?}, Lifetime: {:?}, Disabled: {}, ExtraArgs: {}", + metric_type, category, name, send_in_pings, lifetime, disabled, extra_args); + let metric_type = &metric_type.to_utf8(); + let category = category.to_string(); + let name = name.to_string(); + let send_in_pings = send_in_pings.iter().map(|ping| ping.to_string()).collect(); + let lifetime = serde_json::from_str(&lifetime.to_utf8()) + .expect("Lifetime didn't deserialize happily. Is it valid JSON?"); + + let extra_args: ExtraMetricArgs = if extra_args.is_empty() { + Default::default() + } else { + serde_json::from_str(&extra_args.to_utf8()) + .expect("Extras didn't deserialize happily. Are they valid JSON?") + }; + create_and_register_metric( + metric_type, + category, + name, + send_in_pings, + lifetime, + disabled, + extra_args, + ) + .expect("Creation/Registration of metric failed") // ok to panic in test-only method +} + +fn create_and_register_metric( + metric_type: &str, + category: String, + name: String, + send_in_pings: Vec<String>, + lifetime: Lifetime, + disabled: bool, + extra_args: ExtraMetricArgs, +) -> Result<u32, Box<dyn std::error::Error>> { + let ns_name = nsCString::from(&name); + let ns_category = nsCString::from(&category); + let metric_id = factory::create_and_register_metric( + metric_type, + category, + name, + send_in_pings, + lifetime, + disabled, + extra_args.time_unit, + extra_args.memory_unit, + extra_args.allowed_extra_keys.or_else(|| Some(Vec::new())), + extra_args.range_min, + extra_args.range_max, + extra_args.bucket_count, + extra_args.histogram_type, + extra_args.numerators, + extra_args.labels, + ); + extern "C" { + fn JOG_RegisterMetric(category: &nsACString, name: &nsACString, metric: u32); + } + if let Ok(metric_id) = metric_id { + unsafe { + // Safety: We're loaning to C++ data we don't later use. + JOG_RegisterMetric(&ns_category, &ns_name, metric_id); + } + } else { + log::warn!( + "Could not register metric {}.{} due to {:?}", + ns_category, + ns_name, + metric_id + ); + } + metric_id +} + +/// Test-only method. +/// +/// Registers a ping. Doesn't check to see if it's been registered before. +/// Doesn't check that it would pass schema validation if it were a real ping. +#[no_mangle] +pub extern "C" fn jog_test_register_ping( + name: &nsACString, + include_client_id: bool, + send_if_empty: bool, + reason_codes: &ThinVec<nsCString>, +) -> u32 { + let ping_name = name.to_string(); + let reason_codes = reason_codes + .iter() + .map(|reason| reason.to_string()) + .collect(); + create_and_register_ping(ping_name, include_client_id, send_if_empty, reason_codes) + .expect("Creation or registration of ping failed.") // permitted to panic in test-only method. +} + +fn create_and_register_ping( + ping_name: String, + include_client_id: bool, + send_if_empty: bool, + reason_codes: Vec<String>, +) -> Result<u32, Box<dyn std::error::Error>> { + let ns_name = nsCString::from(&ping_name); + let ping_id = factory::create_and_register_ping( + ping_name, + include_client_id, + send_if_empty, + reason_codes, + ); + extern "C" { + fn JOG_RegisterPing(name: &nsACString, ping_id: u32); + } + if let Ok(ping_id) = ping_id { + unsafe { + // Safety: We're loaning to C++ data we don't later use. + JOG_RegisterPing(&ns_name, ping_id); + } + } else { + log::warn!("Could not register ping {} due to {:?}", ns_name, ping_id); + } + ping_id +} + +/// Test-only method. +/// +/// Clears all runtime registration storage of registered metrics and pings. +#[no_mangle] +pub extern "C" fn jog_test_clear_registered_metrics_and_pings() {} + +#[derive(Default, Deserialize)] +struct Jogfile { + metrics: HashMap<String, Vec<MetricDefinitionData>>, + pings: Vec<PingDefinitionData>, +} + +#[derive(Default, Deserialize)] +struct MetricDefinitionData { + metric_type: String, + name: String, + send_in_pings: Vec<String>, + lifetime: Lifetime, + disabled: bool, + #[serde(default)] + extra_args: Option<ExtraMetricArgs>, +} + +#[derive(Default, Deserialize)] +struct PingDefinitionData { + name: String, + include_client_id: bool, + send_if_empty: bool, + reason_codes: Option<Vec<String>>, +} + +/// Read the file at the provided location, interpret it as a jogfile, +/// and register those pings and metrics. +/// Returns true if we successfully parsed the jogfile. Does not mean +/// all or any metrics and pings successfully registered, +/// just that serde managed to deserialize it into metrics and pings and we tried to register them all. +#[no_mangle] +pub extern "C" fn jog_load_jogfile(jogfile_path: &nsAString) -> bool { + let f = match File::open(jogfile_path.to_string()) { + Ok(f) => f, + _ => { + log::error!("Boo, couldn't open jogfile at {}", jogfile_path.to_string()); + return false; + } + }; + let reader = BufReader::new(f); + + let mut j: Jogfile = match serde_json::from_reader(reader) { + Ok(j) => j, + Err(e) => { + log::error!("Boo, couldn't read jogfile because of: {:?}", e); + return false; + } + }; + log::trace!("Loaded jogfile. Registering metrics+pings."); + for (category, metrics) in j.metrics.drain() { + for metric in metrics.into_iter() { + let _ = create_and_register_metric( + &metric.metric_type, + category.to_string(), + metric.name, + metric.send_in_pings, + metric.lifetime, + metric.disabled, + metric.extra_args.unwrap_or_else(Default::default), + ); + } + } + for ping in j.pings.into_iter() { + let _ = create_and_register_ping( + ping.name, + ping.include_client_id, + ping.send_if_empty, + ping.reason_codes.unwrap_or_else(Vec::new), + ); + } + true +} diff --git a/toolkit/components/glean/bindings/private/Boolean.cpp b/toolkit/components/glean/bindings/private/Boolean.cpp new file mode 100644 index 0000000000..05520ece6b --- /dev/null +++ b/toolkit/components/glean/bindings/private/Boolean.cpp @@ -0,0 +1,78 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/glean/bindings/Boolean.h" + +#include "nsString.h" +#include "mozilla/Components.h" +#include "mozilla/ResultVariant.h" +#include "mozilla/glean/bindings/ScalarGIFFTMap.h" +#include "mozilla/glean/fog_ffi_generated.h" +#include "nsIClassInfoImpl.h" +#include "Common.h" + +namespace mozilla::glean { + +namespace impl { + +void BooleanMetric::Set(bool aValue) const { + auto scalarId = ScalarIdForMetric(mId); + if (scalarId) { + Telemetry::ScalarSet(scalarId.extract(), aValue); + } else if (IsSubmetricId(mId)) { + GetLabeledMirrorLock().apply([&](auto& lock) { + auto tuple = lock.ref()->MaybeGet(mId); + if (tuple) { + Telemetry::ScalarSet(Get<0>(tuple.ref()), Get<1>(tuple.ref()), aValue); + } + }); + } + fog_boolean_set(mId, int(aValue)); +} + +Result<Maybe<bool>, nsCString> BooleanMetric::TestGetValue( + const nsACString& aPingName) const { + nsCString err; + if (fog_boolean_test_get_error(mId, &err)) { + return Err(err); + } + if (!fog_boolean_test_has_value(mId, &aPingName)) { + return Maybe<bool>(); + } + return Some(fog_boolean_test_get_value(mId, &aPingName)); +} + +} // namespace impl + +NS_IMPL_CLASSINFO(GleanBoolean, nullptr, 0, {0}) +NS_IMPL_ISUPPORTS_CI(GleanBoolean, nsIGleanBoolean) + +NS_IMETHODIMP +GleanBoolean::Set(bool aValue) { + mBoolean.Set(aValue); + return NS_OK; +} + +NS_IMETHODIMP +GleanBoolean::TestGetValue(const nsACString& aStorageName, + JS::MutableHandle<JS::Value> aResult) { + auto result = mBoolean.TestGetValue(aStorageName); + if (result.isErr()) { + aResult.set(JS::UndefinedValue()); + LogToBrowserConsole(nsIScriptError::errorFlag, + NS_ConvertUTF8toUTF16(result.unwrapErr())); + return NS_ERROR_LOSS_OF_SIGNIFICANT_DATA; + } + auto optresult = result.unwrap(); + if (optresult.isNothing()) { + aResult.set(JS::UndefinedValue()); + } else { + aResult.set(JS::BooleanValue(optresult.value())); + } + return NS_OK; +} + +} // namespace mozilla::glean diff --git a/toolkit/components/glean/bindings/private/Boolean.h b/toolkit/components/glean/bindings/private/Boolean.h new file mode 100644 index 0000000000..890820440d --- /dev/null +++ b/toolkit/components/glean/bindings/private/Boolean.h @@ -0,0 +1,72 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_glean_GleanBoolean_h +#define mozilla_glean_GleanBoolean_h + +#include "mozilla/Result.h" +#include "nsIGleanMetrics.h" +#include "nsString.h" + +namespace mozilla { +namespace glean { + +namespace impl { + +class BooleanMetric { + public: + constexpr explicit BooleanMetric(uint32_t id) : mId(id) {} + + /** + * Set to the specified boolean value. + * + * @param aValue the value to set. + */ + void Set(bool aValue) const; + + /** + * **Test-only API** + * + * Gets the currently stored value as a boolean. + * + * This function will attempt to await the last parent-process task (if any) + * writing to the the metric's storage engine before returning a value. + * This function will not wait for data from child processes. + * + * This doesn't clear the stored value. + * Parent process only. Panics in child processes. + * + * @param aPingName The (optional) name of the ping to retrieve the metric + * for. Defaults to the first value in `send_in_pings`. + * + * @return value of the stored metric. + */ + Result<Maybe<bool>, nsCString> TestGetValue( + const nsACString& aPingName = nsCString()) const; + + private: + const uint32_t mId; +}; + +} // namespace impl + +class GleanBoolean final : public nsIGleanBoolean { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIGLEANBOOLEAN + + explicit GleanBoolean(uint32_t id) : mBoolean(id){}; + + private: + virtual ~GleanBoolean() = default; + + const impl::BooleanMetric mBoolean; +}; + +} // namespace glean +} // namespace mozilla + +#endif /* mozilla_glean_GleanBoolean.h */ diff --git a/toolkit/components/glean/bindings/private/Common.cpp b/toolkit/components/glean/bindings/private/Common.cpp new file mode 100644 index 0000000000..f84c39c05d --- /dev/null +++ b/toolkit/components/glean/bindings/private/Common.cpp @@ -0,0 +1,33 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#include "Common.h" +#include "nsComponentManagerUtils.h" +#include "nsIConsoleService.h" +#include "nsIScriptError.h" +#include "nsServiceManagerUtils.h" + +namespace mozilla::glean { + +// This is copied from TelemetryCommons.cpp (and modified because consoleservice +// handles threading), but that one is not exported. +// There's _at least_ a third instance of `LogToBrowserConsole`, +// but that one is slightly different. +void LogToBrowserConsole(uint32_t aLogLevel, const nsAString& aMsg) { + nsCOMPtr<nsIConsoleService> console( + do_GetService("@mozilla.org/consoleservice;1")); + if (!console) { + NS_WARNING("Failed to log message to console."); + return; + } + + nsCOMPtr<nsIScriptError> error(do_CreateInstance(NS_SCRIPTERROR_CONTRACTID)); + error->Init(aMsg, u""_ns, u""_ns, 0, 0, aLogLevel, "chrome javascript"_ns, + false /* from private window */, true /* from chrome context */); + console->LogMessage(error); +} + +} // namespace mozilla::glean diff --git a/toolkit/components/glean/bindings/private/Common.h b/toolkit/components/glean/bindings/private/Common.h new file mode 100644 index 0000000000..e3dd7a0a47 --- /dev/null +++ b/toolkit/components/glean/bindings/private/Common.h @@ -0,0 +1,24 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_glean_Common_h +#define mozilla_glean_Common_h + +#include "nsIScriptError.h" + +namespace mozilla::glean { + +/** + * Dumps a log message to the Browser Console using the provided level. + * + * @param aLogLevel The level to use when displaying the message in the browser + * console (e.g. nsIScriptError::warningFlag, ...). + * @param aMsg The text message to print to the console. + */ +void LogToBrowserConsole(uint32_t aLogLevel, const nsAString& aMsg); + +} // namespace mozilla::glean + +#endif /* mozilla_glean_Common_h */ diff --git a/toolkit/components/glean/bindings/private/Counter.cpp b/toolkit/components/glean/bindings/private/Counter.cpp new file mode 100644 index 0000000000..2a4477b4fc --- /dev/null +++ b/toolkit/components/glean/bindings/private/Counter.cpp @@ -0,0 +1,80 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/glean/bindings/Counter.h" + +#include "nsString.h" +#include "mozilla/Components.h" +#include "mozilla/ResultVariant.h" +#include "mozilla/glean/bindings/ScalarGIFFTMap.h" +#include "mozilla/glean/fog_ffi_generated.h" +#include "nsIClassInfoImpl.h" + +namespace mozilla::glean { + +namespace impl { + +void CounterMetric::Add(int32_t aAmount) const { + auto scalarId = ScalarIdForMetric(mId); + if (aAmount >= 0) { + if (scalarId) { + Telemetry::ScalarAdd(scalarId.extract(), aAmount); + } else if (IsSubmetricId(mId)) { + GetLabeledMirrorLock().apply([&](auto& lock) { + auto tuple = lock.ref()->MaybeGet(mId); + if (tuple && aAmount > 0) { + Telemetry::ScalarAdd(Get<0>(tuple.ref()), Get<1>(tuple.ref()), + (uint32_t)aAmount); + } + }); + } + } + fog_counter_add(mId, aAmount); +} + +Result<Maybe<int32_t>, nsCString> CounterMetric::TestGetValue( + const nsACString& aPingName) const { + nsCString err; + if (fog_counter_test_get_error(mId, &err)) { + return Err(err); + } + if (!fog_counter_test_has_value(mId, &aPingName)) { + return Maybe<int32_t>(); // can't use Nothing() or templates will fail. + } + return Some(fog_counter_test_get_value(mId, &aPingName)); +} + +} // namespace impl + +NS_IMPL_CLASSINFO(GleanCounter, nullptr, 0, {0}) +NS_IMPL_ISUPPORTS_CI(GleanCounter, nsIGleanCounter) + +NS_IMETHODIMP +GleanCounter::Add(int32_t aAmount) { + mCounter.Add(aAmount); + return NS_OK; +} + +NS_IMETHODIMP +GleanCounter::TestGetValue(const nsACString& aStorageName, + JS::MutableHandle<JS::Value> aResult) { + auto result = mCounter.TestGetValue(aStorageName); + if (result.isErr()) { + aResult.set(JS::UndefinedValue()); + LogToBrowserConsole(nsIScriptError::errorFlag, + NS_ConvertUTF8toUTF16(result.unwrapErr())); + return NS_ERROR_LOSS_OF_SIGNIFICANT_DATA; + } + auto optresult = result.unwrap(); + if (optresult.isNothing()) { + aResult.set(JS::UndefinedValue()); + } else { + aResult.set(JS::Int32Value(optresult.value())); + } + return NS_OK; +} + +} // namespace mozilla::glean diff --git a/toolkit/components/glean/bindings/private/Counter.h b/toolkit/components/glean/bindings/private/Counter.h new file mode 100644 index 0000000000..e1e7190e4e --- /dev/null +++ b/toolkit/components/glean/bindings/private/Counter.h @@ -0,0 +1,70 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_glean_GleanCounter_h +#define mozilla_glean_GleanCounter_h + +#include "mozilla/Maybe.h" +#include "mozilla/Result.h" +#include "nsIGleanMetrics.h" +#include "nsString.h" + +namespace mozilla::glean { + +namespace impl { + +class CounterMetric { + public: + constexpr explicit CounterMetric(uint32_t aId) : mId(aId) {} + + /* + * Increases the counter by `amount`. + * + * @param aAmount The amount to increase by. Should be positive. + */ + void Add(int32_t aAmount = 1) const; + + /** + * **Test-only API** + * + * Gets the currently stored value as an integer. + * + * This function will attempt to await the last parent-process task (if any) + * writing to the the metric's storage engine before returning a value. + * This function will not wait for data from child processes. + * + * This doesn't clear the stored value. + * Parent process only. Panics in child processes. + * + * @param aPingName The (optional) name of the ping to retrieve the metric + * for. Defaults to the first value in `send_in_pings`. + * + * @return value of the stored metric, or Nothing() if there is no value. + */ + Result<Maybe<int32_t>, nsCString> TestGetValue( + const nsACString& aPingName = nsCString()) const; + + private: + const uint32_t mId; +}; +} // namespace impl + +class GleanCounter final : public nsIGleanCounter { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIGLEANCOUNTER + + explicit GleanCounter(uint32_t id) : mCounter(id){}; + + private: + virtual ~GleanCounter() = default; + + const impl::CounterMetric mCounter; +}; + +} // namespace mozilla::glean + +#endif /* mozilla_glean_GleanCounter_h */ diff --git a/toolkit/components/glean/bindings/private/CustomDistribution.cpp b/toolkit/components/glean/bindings/private/CustomDistribution.cpp new file mode 100644 index 0000000000..4abd0100e7 --- /dev/null +++ b/toolkit/components/glean/bindings/private/CustomDistribution.cpp @@ -0,0 +1,125 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/glean/bindings/CustomDistribution.h" + +#include "Common.h" +#include "mozilla/Components.h" +#include "mozilla/ResultVariant.h" +#include "mozilla/glean/bindings/HistogramGIFFTMap.h" +#include "mozilla/glean/fog_ffi_generated.h" +#include "nsIClassInfoImpl.h" +#include "nsJSUtils.h" +#include "nsPrintfCString.h" +#include "nsString.h" +#include "js/PropertyAndElement.h" // JS_DefineProperty + +namespace mozilla::glean { + +namespace impl { + +void CustomDistributionMetric::AccumulateSamples( + const nsTArray<uint64_t>& aSamples) const { + auto hgramId = HistogramIdForMetric(mId); + if (hgramId) { + auto id = hgramId.extract(); + // N.B.: There is an `Accumulate(nsTArray<T>)`, but `T` is `uint32_t` and + // we got `uint64_t`s here. + for (auto sample : aSamples) { + Telemetry::Accumulate(id, sample); + } + } + fog_custom_distribution_accumulate_samples(mId, &aSamples); +} + +void CustomDistributionMetric::AccumulateSamplesSigned( + const nsTArray<int64_t>& aSamples) const { + auto hgramId = HistogramIdForMetric(mId); + if (hgramId) { + auto id = hgramId.extract(); + // N.B.: There is an `Accumulate(nsTArray<T>)`, but `T` is `uint32_t` and + // we got `int64_t`s here. + for (auto sample : aSamples) { + Telemetry::Accumulate(id, sample); + } + } + fog_custom_distribution_accumulate_samples_signed(mId, &aSamples); +} + +Result<Maybe<DistributionData>, nsCString> +CustomDistributionMetric::TestGetValue(const nsACString& aPingName) const { + nsCString err; + if (fog_custom_distribution_test_get_error(mId, &err)) { + return Err(err); + } + if (!fog_custom_distribution_test_has_value(mId, &aPingName)) { + return Maybe<DistributionData>(); + } + nsTArray<uint64_t> buckets; + nsTArray<uint64_t> counts; + uint64_t sum; + fog_custom_distribution_test_get_value(mId, &aPingName, &sum, &buckets, + &counts); + return Some(DistributionData(buckets, counts, sum)); +} + +} // namespace impl + +NS_IMPL_CLASSINFO(GleanCustomDistribution, nullptr, 0, {0}) +NS_IMPL_ISUPPORTS_CI(GleanCustomDistribution, nsIGleanCustomDistribution) + +NS_IMETHODIMP +GleanCustomDistribution::AccumulateSamples(const nsTArray<int64_t>& aSamples) { + mCustomDist.AccumulateSamplesSigned(aSamples); + return NS_OK; +} + +NS_IMETHODIMP +GleanCustomDistribution::TestGetValue(const nsACString& aPingName, + JSContext* aCx, + JS::MutableHandle<JS::Value> aResult) { + auto result = mCustomDist.TestGetValue(aPingName); + if (result.isErr()) { + aResult.set(JS::UndefinedValue()); + LogToBrowserConsole(nsIScriptError::errorFlag, + NS_ConvertUTF8toUTF16(result.unwrapErr())); + return NS_ERROR_LOSS_OF_SIGNIFICANT_DATA; + } + auto optresult = result.unwrap(); + if (optresult.isNothing()) { + aResult.set(JS::UndefinedValue()); + } else { + // Build return value of the form: { sum: #, values: {bucket1: count1, ...} + JS::Rooted<JSObject*> root(aCx, JS_NewPlainObject(aCx)); + if (!root) { + return NS_ERROR_FAILURE; + } + uint64_t sum = optresult.ref().sum; + if (!JS_DefineProperty(aCx, root, "sum", static_cast<double>(sum), + JSPROP_ENUMERATE)) { + return NS_ERROR_FAILURE; + } + JS::Rooted<JSObject*> valuesObj(aCx, JS_NewPlainObject(aCx)); + if (!valuesObj || + !JS_DefineProperty(aCx, root, "values", valuesObj, JSPROP_ENUMERATE)) { + return NS_ERROR_FAILURE; + } + auto& data = optresult.ref().values; + for (const auto& entry : data) { + const uint64_t bucket = entry.GetKey(); + const uint64_t count = entry.GetData(); + if (!JS_DefineProperty(aCx, valuesObj, + nsPrintfCString("%" PRIu64, bucket).get(), + static_cast<double>(count), JSPROP_ENUMERATE)) { + return NS_ERROR_FAILURE; + } + } + aResult.setObject(*root); + } + return NS_OK; +} + +} // namespace mozilla::glean diff --git a/toolkit/components/glean/bindings/private/CustomDistribution.h b/toolkit/components/glean/bindings/private/CustomDistribution.h new file mode 100644 index 0000000000..1ed9b78788 --- /dev/null +++ b/toolkit/components/glean/bindings/private/CustomDistribution.h @@ -0,0 +1,83 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_glean_GleanCustomDistribution_h +#define mozilla_glean_GleanCustomDistribution_h + +#include "mozilla/glean/bindings/DistributionData.h" +#include "mozilla/Maybe.h" +#include "mozilla/Result.h" +#include "nsIGleanMetrics.h" +#include "nsTArray.h" + +namespace mozilla::glean { + +namespace impl { + +class CustomDistributionMetric { + public: + constexpr explicit CustomDistributionMetric(uint32_t aId) : mId(aId) {} + + /** + * Accumulates the provided samples in the metric. + * + * @param aSamples The vector holding the samples to be recorded by the + * metric. + */ + void AccumulateSamples(const nsTArray<uint64_t>& aSamples) const; + + /** + * Accumulates the provided samples in the metric. + * + * @param aSamples The vector holding the samples to be recorded by the + * metric. + * + * Notes: Discards any negative value in `samples` + * and reports an `InvalidValue` error for each of them. + */ + void AccumulateSamplesSigned(const nsTArray<int64_t>& aSamples) const; + + /** + * **Test-only API** + * + * Gets the currently stored value as a DistributionData. + * + * This function will attempt to await the last parent-process task (if any) + * writing to the the metric's storage engine before returning a value. + * This function will not wait for data from child processes. + * + * This doesn't clear the stored value. + * Parent process only. Panics in child processes. + * + * @param aPingName The (optional) name of the ping to retrieve the metric + * for. Defaults to the first value in `send_in_pings`. + * + * @return value of the stored metric, or Nothing() if there is no value. + */ + Result<Maybe<DistributionData>, nsCString> TestGetValue( + const nsACString& aPingName = nsCString()) const; + + private: + const uint32_t mId; +}; +} // namespace impl + +class GleanCustomDistribution final : public nsIGleanCustomDistribution { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIGLEANCUSTOMDISTRIBUTION + + explicit GleanCustomDistribution(uint64_t aId) : mCustomDist(aId){}; + + private: + virtual ~GleanCustomDistribution() = default; + + const impl::CustomDistributionMetric mCustomDist; +}; + +} // namespace mozilla::glean + +#endif /* mozilla_glean_GleanCustomDistribution_h */ diff --git a/toolkit/components/glean/bindings/private/Datetime.cpp b/toolkit/components/glean/bindings/private/Datetime.cpp new file mode 100644 index 0000000000..900b8e6a13 --- /dev/null +++ b/toolkit/components/glean/bindings/private/Datetime.cpp @@ -0,0 +1,124 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/glean/bindings/Datetime.h" + +#include "jsapi.h" +#include "js/Date.h" +#include "nsString.h" +#include "mozilla/Components.h" +#include "mozilla/ResultVariant.h" +#include "mozilla/glean/bindings/ScalarGIFFTMap.h" +#include "mozilla/glean/fog_ffi_generated.h" +#include "nsIClassInfoImpl.h" +#include "prtime.h" + +namespace mozilla::glean { + +namespace impl { + +void DatetimeMetric::Set(const PRExplodedTime* aValue) const { + PRExplodedTime exploded; + if (!aValue) { + PR_ExplodeTime(PR_Now(), PR_LocalTimeParameters, &exploded); + } else { + exploded = *aValue; + } + + auto id = ScalarIdForMetric(mId); + if (id) { + const uint32_t buflen = 64; // More than enough for now. + char buf[buflen]; + uint32_t written = PR_FormatTime(buf, buflen, "%FT%T%z", &exploded); + if (written > 2 && written < 64) { + // Format's still not quite there. Gotta put a `:` between timezone + // hours and minutes + buf[written] = '\0'; + buf[written - 1] = buf[written - 2]; + buf[written - 2] = buf[written - 3]; + buf[written - 3] = ':'; + Telemetry::ScalarSet(id.extract(), NS_ConvertASCIItoUTF16(buf)); + } + } + + int32_t offset = + exploded.tm_params.tp_gmt_offset + exploded.tm_params.tp_dst_offset; + FogDatetime dt{exploded.tm_year, + static_cast<uint32_t>(exploded.tm_month + 1), + static_cast<uint32_t>(exploded.tm_mday), + static_cast<uint32_t>(exploded.tm_hour), + static_cast<uint32_t>(exploded.tm_min), + static_cast<uint32_t>(exploded.tm_sec), + static_cast<uint32_t>(exploded.tm_usec * 1000), + offset}; + fog_datetime_set(mId, &dt); +} + +Result<Maybe<PRExplodedTime>, nsCString> DatetimeMetric::TestGetValue( + const nsACString& aPingName) const { + nsCString err; + if (fog_datetime_test_get_error(mId, &err)) { + return Err(err); + } + if (!fog_datetime_test_has_value(mId, &aPingName)) { + return Maybe<PRExplodedTime>(); + } + FogDatetime ret{0}; + fog_datetime_test_get_value(mId, &aPingName, &ret); + PRExplodedTime pret{0}; + pret.tm_year = static_cast<PRInt16>(ret.year); + pret.tm_month = static_cast<PRInt32>(ret.month - 1); + pret.tm_mday = static_cast<PRInt32>(ret.day); + pret.tm_hour = static_cast<PRInt32>(ret.hour); + pret.tm_min = static_cast<PRInt32>(ret.minute); + pret.tm_sec = static_cast<PRInt32>(ret.second); + pret.tm_usec = static_cast<PRInt32>(ret.nano / 1000); // truncated is fine + pret.tm_params.tp_gmt_offset = static_cast<PRInt32>(ret.offset_seconds); + return Some(std::move(pret)); +} + +} // namespace impl + +NS_IMPL_CLASSINFO(GleanDatetime, nullptr, 0, {0}) +NS_IMPL_ISUPPORTS_CI(GleanDatetime, nsIGleanDatetime) + +NS_IMETHODIMP +GleanDatetime::Set(PRTime aValue, uint8_t aOptionalArgc) { + if (aOptionalArgc == 0) { + mDatetime.Set(); + } else { + PRExplodedTime exploded; + PR_ExplodeTime(aValue, PR_LocalTimeParameters, &exploded); + mDatetime.Set(&exploded); + } + + return NS_OK; +} + +NS_IMETHODIMP +GleanDatetime::TestGetValue(const nsACString& aStorageName, JSContext* aCx, + JS::MutableHandle<JS::Value> aResult) { + auto result = mDatetime.TestGetValue(aStorageName); + if (result.isErr()) { + aResult.set(JS::UndefinedValue()); + LogToBrowserConsole(nsIScriptError::errorFlag, + NS_ConvertUTF8toUTF16(result.unwrapErr())); + return NS_ERROR_LOSS_OF_SIGNIFICANT_DATA; + } + auto optresult = result.unwrap(); + if (optresult.isNothing()) { + aResult.set(JS::UndefinedValue()); + } else { + double millis = + static_cast<double>(PR_ImplodeTime(optresult.ptr())) / PR_USEC_PER_MSEC; + JS::Rooted<JSObject*> root(aCx, + JS::NewDateObject(aCx, JS::TimeClip(millis))); + aResult.setObject(*root); + } + return NS_OK; +} + +} // namespace mozilla::glean diff --git a/toolkit/components/glean/bindings/private/Datetime.h b/toolkit/components/glean/bindings/private/Datetime.h new file mode 100644 index 0000000000..4fec93d251 --- /dev/null +++ b/toolkit/components/glean/bindings/private/Datetime.h @@ -0,0 +1,71 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_glean_GleanDatetime_h +#define mozilla_glean_GleanDatetime_h + +#include "mozilla/Maybe.h" +#include "mozilla/Result.h" +#include "nsIGleanMetrics.h" +#include "nsString.h" +#include "prtime.h" + +namespace mozilla::glean { + +namespace impl { + +class DatetimeMetric { + public: + constexpr explicit DatetimeMetric(uint32_t aId) : mId(aId) {} + + /* + * Set the datetime to the provided value, or the local now. + * + * @param amount The date value to set. + */ + void Set(const PRExplodedTime* aValue = nullptr) const; + + /** + * **Test-only API** + * + * Gets the currently stored value as a PRExplodedTime. + * + * This function will attempt to await the last parent-process task (if any) + * writing to the the metric's storage engine before returning a value. + * This function will not wait for data from child processes. + * + * This doesn't clear the stored value. + * Parent process only. Panics in child processes. + * + * @param aPingName The (optional) name of the ping to retrieve the metric + * for. Defaults to the first value in `send_in_pings`. + * + * @return value of the stored metric, or Nothing() if there is no value. + */ + Result<Maybe<PRExplodedTime>, nsCString> TestGetValue( + const nsACString& aPingName = nsCString()) const; + + private: + const uint32_t mId; +}; +} // namespace impl + +class GleanDatetime final : public nsIGleanDatetime { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIGLEANDATETIME + + explicit GleanDatetime(uint32_t aId) : mDatetime(aId){}; + + private: + virtual ~GleanDatetime() = default; + + const impl::DatetimeMetric mDatetime; +}; + +} // namespace mozilla::glean + +#endif /* mozilla_glean_GleanDatetime_h */ diff --git a/toolkit/components/glean/bindings/private/Denominator.cpp b/toolkit/components/glean/bindings/private/Denominator.cpp new file mode 100644 index 0000000000..e922c4f145 --- /dev/null +++ b/toolkit/components/glean/bindings/private/Denominator.cpp @@ -0,0 +1,70 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/glean/bindings/Denominator.h" + +#include "nsString.h" +#include "mozilla/Components.h" +#include "mozilla/ResultVariant.h" +#include "mozilla/glean/bindings/ScalarGIFFTMap.h" +#include "mozilla/glean/fog_ffi_generated.h" +#include "nsIClassInfoImpl.h" + +namespace mozilla::glean { + +namespace impl { + +void DenominatorMetric::Add(int32_t aAmount) const { + auto scalarId = ScalarIdForMetric(mId); + if (scalarId && aAmount >= 0) { + Telemetry::ScalarAdd(scalarId.extract(), aAmount); + } + fog_denominator_add(mId, aAmount); +} + +Result<Maybe<int32_t>, nsCString> DenominatorMetric::TestGetValue( + const nsACString& aPingName) const { + nsCString err; + if (fog_denominator_test_get_error(mId, &err)) { + return Err(err); + } + if (!fog_denominator_test_has_value(mId, &aPingName)) { + return Maybe<int32_t>(); + } + return Some(fog_denominator_test_get_value(mId, &aPingName)); +} + +} // namespace impl + +NS_IMPL_CLASSINFO(GleanDenominator, nullptr, 0, {0}) +NS_IMPL_ISUPPORTS_CI(GleanDenominator, nsIGleanDenominator) + +NS_IMETHODIMP +GleanDenominator::Add(int32_t aAmount) { + mDenominator.Add(aAmount); + return NS_OK; +} + +NS_IMETHODIMP +GleanDenominator::TestGetValue(const nsACString& aStorageName, + JS::MutableHandle<JS::Value> aResult) { + auto result = mDenominator.TestGetValue(aStorageName); + if (result.isErr()) { + aResult.set(JS::UndefinedValue()); + LogToBrowserConsole(nsIScriptError::errorFlag, + NS_ConvertUTF8toUTF16(result.unwrapErr())); + return NS_ERROR_LOSS_OF_SIGNIFICANT_DATA; + } + auto optresult = result.unwrap(); + if (optresult.isNothing()) { + aResult.set(JS::UndefinedValue()); + } else { + aResult.set(JS::Int32Value(optresult.value())); + } + return NS_OK; +} + +} // namespace mozilla::glean diff --git a/toolkit/components/glean/bindings/private/Denominator.h b/toolkit/components/glean/bindings/private/Denominator.h new file mode 100644 index 0000000000..c96bf17e8d --- /dev/null +++ b/toolkit/components/glean/bindings/private/Denominator.h @@ -0,0 +1,70 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_glean_GleanDenominator_h +#define mozilla_glean_GleanDenominator_h + +#include "mozilla/Maybe.h" +#include "mozilla/Result.h" +#include "nsIGleanMetrics.h" +#include "nsString.h" + +namespace mozilla::glean { + +namespace impl { + +class DenominatorMetric { + public: + constexpr explicit DenominatorMetric(uint32_t aId) : mId(aId) {} + + /* + * Increases the counter by `amount`. + * + * @param aAmount The amount to increase by. Should be positive. + */ + void Add(int32_t aAmount = 1) const; + + /** + * **Test-only API** + * + * Gets the currently stored value as an integer. + * + * This function will attempt to await the last parent-process task (if any) + * writing to the the metric's storage engine before returning a value. + * This function will not wait for data from child processes. + * + * This doesn't clear the stored value. + * Parent process only. Panics in child processes. + * + * @param aPingName The (optional) name of the ping to retrieve the metric + * for. Defaults to the first value in `send_in_pings`. + * + * @return value of the stored metric, or Nothing() if there is no value. + */ + Result<Maybe<int32_t>, nsCString> TestGetValue( + const nsACString& aPingName = nsCString()) const; + + private: + const uint32_t mId; +}; +} // namespace impl + +class GleanDenominator final : public nsIGleanDenominator { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIGLEANDENOMINATOR + + explicit GleanDenominator(uint32_t id) : mDenominator(id){}; + + private: + virtual ~GleanDenominator() = default; + + const impl::DenominatorMetric mDenominator; +}; + +} // namespace mozilla::glean + +#endif /* mozilla_glean_GleanDenominator_h */ diff --git a/toolkit/components/glean/bindings/private/DistributionData.h b/toolkit/components/glean/bindings/private/DistributionData.h new file mode 100644 index 0000000000..6ff995f222 --- /dev/null +++ b/toolkit/components/glean/bindings/private/DistributionData.h @@ -0,0 +1,32 @@ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_glean_DistributionData_h +#define mozilla_glean_DistributionData_h + +#include "nsTHashMap.h" + +namespace mozilla::glean { + +struct DistributionData final { + uint64_t sum; + nsTHashMap<nsUint64HashKey, uint64_t> values; + + /** + * Create distribution data from the buckets, counts and sum, + * as returned by `fog_*_distribution_test_get_value`. + */ + DistributionData(const nsTArray<uint64_t>& aBuckets, + const nsTArray<uint64_t>& aCounts, uint64_t aSum) + : sum(aSum) { + for (size_t i = 0; i < aBuckets.Length(); ++i) { + this->values.InsertOrUpdate(aBuckets[i], aCounts[i]); + } + } +}; + +} // namespace mozilla::glean + +#endif /* mozilla_glean_DistributionData_h */ diff --git a/toolkit/components/glean/bindings/private/Event.cpp b/toolkit/components/glean/bindings/private/Event.cpp new file mode 100644 index 0000000000..d2eb5b3ca6 --- /dev/null +++ b/toolkit/components/glean/bindings/private/Event.cpp @@ -0,0 +1,183 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/glean/bindings/Event.h" + +#include "Common.h" +#include "nsString.h" +#include "mozilla/Components.h" +#include "mozilla/dom/ToJSValue.h" +#include "nsIClassInfoImpl.h" +#include "jsapi.h" +#include "js/PropertyAndElement.h" // JS_DefineElement, JS_DefineProperty, JS_Enumerate, JS_GetProperty, JS_GetPropertyById +#include "nsIScriptError.h" + +namespace mozilla::glean { + +NS_IMPL_CLASSINFO(GleanEvent, nullptr, 0, {0}) +NS_IMPL_ISUPPORTS_CI(GleanEvent, nsIGleanEvent) + +NS_IMETHODIMP +GleanEvent::Record(JS::Handle<JS::Value> aExtra, JSContext* aCx) { + if (aExtra.isNullOrUndefined()) { + mEvent.Record(); + return NS_OK; + } + + if (!aExtra.isObject()) { + LogToBrowserConsole(nsIScriptError::warningFlag, + u"Extras need to be an object"_ns); + return NS_OK; + } + + nsTArray<nsCString> extraKeys; + nsTArray<nsCString> extraValues; + CopyableTArray<Telemetry::EventExtraEntry> telExtras; + + JS::Rooted<JSObject*> obj(aCx, &aExtra.toObject()); + JS::Rooted<JS::IdVector> ids(aCx, JS::IdVector(aCx)); + if (!JS_Enumerate(aCx, obj, &ids)) { + LogToBrowserConsole(nsIScriptError::warningFlag, + u"Failed to enumerate object."_ns); + return NS_OK; + } + + for (size_t i = 0, n = ids.length(); i < n; i++) { + nsAutoJSCString jsKey; + if (!jsKey.init(aCx, ids[i])) { + LogToBrowserConsole( + nsIScriptError::warningFlag, + u"Extra dictionary should only contain string keys."_ns); + return NS_OK; + } + + JS::Rooted<JS::Value> value(aCx); + if (!JS_GetPropertyById(aCx, obj, ids[i], &value)) { + LogToBrowserConsole(nsIScriptError::warningFlag, + u"Failed to get extra property."_ns); + return NS_OK; + } + + nsAutoJSCString jsValue; + if (value.isString() || (value.isInt32() && value.toInt32() >= 0) || + value.isBoolean()) { + if (!jsValue.init(aCx, value)) { + LogToBrowserConsole(nsIScriptError::warningFlag, + u"Can't extract extra property"_ns); + return NS_OK; + } + } else if (value.isNullOrUndefined()) { + // The extra key is present, but has an empty value. + // Treat as though it weren't here at all. + continue; + } else { + LogToBrowserConsole( + nsIScriptError::warningFlag, + u"Extra properties should have string, bool or non-negative integer values."_ns); + return NS_OK; + } + + extraKeys.AppendElement(jsKey); + extraValues.AppendElement(jsValue); + telExtras.EmplaceBack(Telemetry::EventExtraEntry{jsKey, jsValue}); + } + + // Since this calls the implementation directly, we need to implement GIFFT + // here as well as in EventMetric::Record. + auto id = EventIdForMetric(mEvent.mId); + if (id) { + Telemetry::RecordEvent(id.extract(), Nothing(), + telExtras.IsEmpty() ? Nothing() : Some(telExtras)); + } + + // Calling the implementation directly, because we have a `string->string` + // map, not a `T->string` map the C++ API expects. + impl::fog_event_record(mEvent.mId, &extraKeys, &extraValues); + return NS_OK; +} + +NS_IMETHODIMP +GleanEvent::TestGetValue(const nsACString& aStorageName, JSContext* aCx, + JS::MutableHandle<JS::Value> aResult) { + auto resEvents = mEvent.TestGetValue(aStorageName); + if (resEvents.isErr()) { + aResult.set(JS::UndefinedValue()); + LogToBrowserConsole(nsIScriptError::errorFlag, + NS_ConvertUTF8toUTF16(resEvents.unwrapErr())); + return NS_ERROR_LOSS_OF_SIGNIFICANT_DATA; + } + auto optEvents = resEvents.unwrap(); + if (optEvents.isNothing()) { + aResult.set(JS::UndefinedValue()); + return NS_OK; + } + + auto events = optEvents.extract(); + + auto count = events.Length(); + JS::Rooted<JSObject*> eventArray(aCx, JS::NewArrayObject(aCx, count)); + if (NS_WARN_IF(!eventArray)) { + return NS_ERROR_FAILURE; + } + + for (size_t i = 0; i < count; i++) { + auto* value = &events[i]; + + JS::Rooted<JSObject*> eventObj(aCx, JS_NewPlainObject(aCx)); + if (NS_WARN_IF(!eventObj)) { + return NS_ERROR_FAILURE; + } + + if (!JS_DefineProperty(aCx, eventObj, "timestamp", + (double)value->mTimestamp, JSPROP_ENUMERATE)) { + NS_WARNING("Failed to define timestamp for event object."); + return NS_ERROR_FAILURE; + } + + JS::Rooted<JS::Value> catStr(aCx); + if (!dom::ToJSValue(aCx, value->mCategory, &catStr) || + !JS_DefineProperty(aCx, eventObj, "category", catStr, + JSPROP_ENUMERATE)) { + NS_WARNING("Failed to define category for event object."); + return NS_ERROR_FAILURE; + } + JS::Rooted<JS::Value> nameStr(aCx); + if (!dom::ToJSValue(aCx, value->mName, &nameStr) || + !JS_DefineProperty(aCx, eventObj, "name", nameStr, JSPROP_ENUMERATE)) { + NS_WARNING("Failed to define name for event object."); + return NS_ERROR_FAILURE; + } + + JS::Rooted<JSObject*> extraObj(aCx, JS_NewPlainObject(aCx)); + if (!JS_DefineProperty(aCx, eventObj, "extra", extraObj, + JSPROP_ENUMERATE)) { + NS_WARNING("Failed to define extra for event object."); + return NS_ERROR_FAILURE; + } + + for (auto pair : value->mExtra) { + auto key = mozilla::Get<0>(pair); + auto val = mozilla::Get<1>(pair); + JS::Rooted<JS::Value> valStr(aCx); + if (!dom::ToJSValue(aCx, val, &valStr) || + !JS_DefineProperty(aCx, extraObj, key.Data(), valStr, + JSPROP_ENUMERATE)) { + NS_WARNING("Failed to define extra property for event object."); + return NS_ERROR_FAILURE; + } + } + + if (!JS_DefineElement(aCx, eventArray, i, eventObj, JSPROP_ENUMERATE)) { + NS_WARNING("Failed to define item in events array."); + return NS_ERROR_FAILURE; + } + } + + aResult.setObject(*eventArray); + return NS_OK; +} + +} // namespace mozilla::glean diff --git a/toolkit/components/glean/bindings/private/Event.h b/toolkit/components/glean/bindings/private/Event.h new file mode 100644 index 0000000000..111be2cbc2 --- /dev/null +++ b/toolkit/components/glean/bindings/private/Event.h @@ -0,0 +1,162 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_glean_GleanEvent_h +#define mozilla_glean_GleanEvent_h + +#include "nsIGleanMetrics.h" +#include "mozilla/glean/bindings/EventGIFFTMap.h" +#include "mozilla/glean/fog_ffi_generated.h" +#include "mozilla/ResultVariant.h" +#include "mozilla/Tuple.h" +#include "nsString.h" +#include "nsTArray.h" + +namespace mozilla::glean { + +// forward declaration +class GleanEvent; + +namespace impl { + +/** + * Represents the recorded data for a single event + */ +struct RecordedEvent { + public: + uint64_t mTimestamp; + nsCString mCategory; + nsCString mName; + + nsTArray<Tuple<nsCString, nsCString>> mExtra; +}; + +template <class T> +class EventMetric { + friend class mozilla::glean::GleanEvent; + + public: + constexpr explicit EventMetric(uint32_t id) : mId(id) {} + + /** + * Record an event. + * + * @param aExtras The list of (extra key, value) pairs. Allowed extra keys are + * defined in the metric definition. + * If the wrong keys are used or values are too large + * an error is report and no event is recorded. + */ + void Record(const Maybe<T>& aExtras = Nothing()) const { + auto id = EventIdForMetric(mId); + if (id) { + // NB. In case `aExtras` is filled we call `ToFfiExtra`, causing + // twice the required allocation. We could be smarter and reuse the data. + // But this is GIFFT-only allocation, so wait to be told it's a problem. + Maybe<CopyableTArray<Telemetry::EventExtraEntry>> telExtras; + if (aExtras) { + CopyableTArray<Telemetry::EventExtraEntry> extras; + auto serializedExtras = aExtras->ToFfiExtra(); + auto keys = std::move(Get<0>(serializedExtras)); + auto values = std::move(Get<1>(serializedExtras)); + for (size_t i = 0; i < keys.Length(); i++) { + extras.EmplaceBack(Telemetry::EventExtraEntry{keys[i], values[i]}); + } + telExtras = Some(extras); + } + Telemetry::RecordEvent(id.extract(), Nothing(), telExtras); + } + if (aExtras) { + auto extra = aExtras->ToFfiExtra(); + fog_event_record(mId, &mozilla::Get<0>(extra), &mozilla::Get<1>(extra)); + } else { + nsTArray<nsCString> keys; + nsTArray<nsCString> vals; + fog_event_record(mId, &keys, &vals); + } + } + + /** + * **Test-only API** + * + * Get a list of currently stored events for this event metric. + * + * This function will attempt to await the last parent-process task (if any) + * writing to the the metric's storage engine before returning a value. + * This function will not wait for data from child processes. + * + * This doesn't clear the stored value. + * Parent process only. Panics in child processes. + * + * @param aPingName The (optional) name of the ping to retrieve the metric + * for. Defaults to the first value in `send_in_pings`. + * + * @return value of the stored metric, or Nothing() if there is no value. + */ + Result<Maybe<nsTArray<RecordedEvent>>, nsCString> TestGetValue( + const nsACString& aPingName = nsCString()) const { + nsCString err; + if (fog_event_test_get_error(mId, &err)) { + return Err(err); + } + + if (!fog_event_test_has_value(mId, &aPingName)) { + return Maybe<nsTArray<RecordedEvent>>(); + } + + nsTArray<FfiRecordedEvent> events; + fog_event_test_get_value(mId, &aPingName, &events); + + nsTArray<RecordedEvent> result; + for (const auto& event : events) { + auto ev = result.AppendElement(); + ev->mTimestamp = event.timestamp; + ev->mCategory.Append(event.category); + ev->mName.Assign(event.name); + + MOZ_ASSERT(event.extras.Length() % 2 == 0); + ev->mExtra.SetCapacity(event.extras.Length() / 2); + for (unsigned int i = 0; i < event.extras.Length(); i += 2) { + // keys & values are interleaved. + nsCString key = std::move(event.extras[i]); + nsCString value = std::move(event.extras[i + 1]); + ev->mExtra.AppendElement(MakeTuple(std::move(key), std::move(value))); + } + } + return Some(std::move(result)); + } + + private: + static const nsCString ExtraStringForKey(uint32_t aKey); + + const uint32_t mId; +}; + +} // namespace impl + +struct NoExtraKeys { + Tuple<nsTArray<nsCString>, nsTArray<nsCString>> ToFfiExtra() const { + nsTArray<nsCString> extraKeys; + nsTArray<nsCString> extraValues; + return MakeTuple(std::move(extraKeys), std::move(extraValues)); + } +}; + +class GleanEvent final : public nsIGleanEvent { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIGLEANEVENT + + explicit GleanEvent(uint32_t id) : mEvent(id){}; + + private: + virtual ~GleanEvent() = default; + + const impl::EventMetric<NoExtraKeys> mEvent; +}; + +} // namespace mozilla::glean + +#endif /* mozilla_glean_GleanEvent.h */ diff --git a/toolkit/components/glean/bindings/private/Labeled.cpp b/toolkit/components/glean/bindings/private/Labeled.cpp new file mode 100644 index 0000000000..b01867d729 --- /dev/null +++ b/toolkit/components/glean/bindings/private/Labeled.cpp @@ -0,0 +1,99 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/glean/bindings/Labeled.h" + +#include "mozilla/dom/GleanBinding.h" +#include "mozilla/glean/fog_ffi_generated.h" +#include "mozilla/glean/bindings/GleanJSMetricsLookup.h" +#include "mozilla/glean/bindings/MetricTypes.h" +#include "mozilla/UniquePtr.h" +#include "nsString.h" + +namespace mozilla::glean { + +namespace impl { +template <> +BooleanMetric Labeled<BooleanMetric>::Get(const nsACString& aLabel) const { + auto submetricId = fog_labeled_boolean_get(mId, &aLabel); + // If this labeled metric is mirrored, we need to map the submetric id back + // to the label string and mirrored scalar so we can mirror its operations. + auto mirrorId = ScalarIdForMetric(mId); + if (mirrorId) { + GetLabeledMirrorLock().apply([&](auto& lock) { + auto tuple = MakeTuple<Telemetry::ScalarID, nsString>( + mirrorId.extract(), NS_ConvertUTF8toUTF16(aLabel)); + lock.ref()->InsertOrUpdate(submetricId, std::move(tuple)); + }); + } + return BooleanMetric(submetricId); +} + +template <> +CounterMetric Labeled<CounterMetric>::Get(const nsACString& aLabel) const { + auto submetricId = fog_labeled_counter_get(mId, &aLabel); + // If this labeled metric is mirrored, we need to map the submetric id back + // to the label string and mirrored scalar so we can mirror its operations. + auto mirrorId = ScalarIdForMetric(mId); + if (mirrorId) { + GetLabeledMirrorLock().apply([&](auto& lock) { + auto tuple = MakeTuple<Telemetry::ScalarID, nsString>( + mirrorId.extract(), NS_ConvertUTF8toUTF16(aLabel)); + lock.ref()->InsertOrUpdate(submetricId, std::move(tuple)); + }); + } + return CounterMetric(submetricId); +} + +template <> +StringMetric Labeled<StringMetric>::Get(const nsACString& aLabel) const { + auto submetricId = fog_labeled_string_get(mId, &aLabel); + // Why no GIFFT map here? + // Labeled Strings can't be mirrored. Telemetry has no compatible probe. + return StringMetric(submetricId); +} +} // namespace impl + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_0(GleanLabeled) + +NS_IMPL_CYCLE_COLLECTING_ADDREF(GleanLabeled) +NS_IMPL_CYCLE_COLLECTING_RELEASE(GleanLabeled) +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(GleanLabeled) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +JSObject* GleanLabeled::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return dom::GleanLabeled_Binding::Wrap(aCx, this, aGivenProto); +} + +already_AddRefed<nsISupports> GleanLabeled::NamedGetter(const nsAString& aName, + bool& aFound) { + auto label = NS_ConvertUTF16toUTF8(aName); + aFound = true; + uint32_t submetricId = 0; + already_AddRefed<nsISupports> submetric = + NewSubMetricFromIds(mTypeId, mId, label, &submetricId); + + auto mirrorId = ScalarIdForMetric(mId); + if (mirrorId) { + GetLabeledMirrorLock().apply([&](auto& lock) { + auto tuple = MakeTuple<Telemetry::ScalarID, nsString>(mirrorId.extract(), + nsString(aName)); + lock.ref()->InsertOrUpdate(submetricId, std::move(tuple)); + }); + } + return submetric; +} + +bool GleanLabeled::NameIsEnumerable(const nsAString& aName) { return false; } + +void GleanLabeled::GetSupportedNames(nsTArray<nsString>& aNames) { + // We really don't know, so don't do anything. +} + +} // namespace mozilla::glean diff --git a/toolkit/components/glean/bindings/private/Labeled.h b/toolkit/components/glean/bindings/private/Labeled.h new file mode 100644 index 0000000000..be46b3b56e --- /dev/null +++ b/toolkit/components/glean/bindings/private/Labeled.h @@ -0,0 +1,75 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_glean_Labeled_h +#define mozilla_glean_Labeled_h + +#include "nsIGleanMetrics.h" +#include "nsISupports.h" +#include "nsWrapperCache.h" +#include "mozilla/dom/BindingDeclarations.h" +#include "mozilla/glean/fog_ffi_generated.h" + +namespace mozilla::glean { + +namespace impl { + +template <typename T> +class Labeled { + public: + constexpr explicit Labeled<T>(uint32_t id) : mId(id) {} + + /** + * 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. + * + * @param aLabel - a snake_case string under 30 characters in length, + * otherwise the metric will be recorded under the special + * `OTHER_LABEL` label and an error will be recorded. + */ + T Get(const nsACString& aLabel) const; + + private: + const uint32_t mId; +}; + +} // namespace impl + +class GleanLabeled final : public nsISupports, public nsWrapperCache { + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(GleanLabeled) + + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + nsISupports* GetParentObject() { return nullptr; } + + explicit GleanLabeled(uint32_t aId, uint32_t aTypeId) + : mId(aId), mTypeId(aTypeId){}; + + already_AddRefed<nsISupports> NamedGetter(const nsAString& aName, + bool& aFound); + bool NameIsEnumerable(const nsAString& aName); + void GetSupportedNames(nsTArray<nsString>& aNames); + + private: + virtual ~GleanLabeled() = default; + + const uint32_t mId; + const uint32_t mTypeId; +}; + +} // namespace mozilla::glean + +#endif /* mozilla_glean_Labeled_h */ diff --git a/toolkit/components/glean/bindings/private/MemoryDistribution.cpp b/toolkit/components/glean/bindings/private/MemoryDistribution.cpp new file mode 100644 index 0000000000..41899bf7c1 --- /dev/null +++ b/toolkit/components/glean/bindings/private/MemoryDistribution.cpp @@ -0,0 +1,104 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/glean/bindings/MemoryDistribution.h" + +#include "mozilla/Components.h" +#include "mozilla/glean/bindings/HistogramGIFFTMap.h" +#include "mozilla/glean/fog_ffi_generated.h" +#include "nsIClassInfoImpl.h" +#include "nsJSUtils.h" +#include "nsPrintfCString.h" +#include "nsString.h" +#include "js/PropertyAndElement.h" // JS_DefineProperty + +namespace mozilla::glean { + +namespace impl { + +void MemoryDistributionMetric::Accumulate(size_t aSample) const { + auto hgramId = HistogramIdForMetric(mId); + if (hgramId) { + Telemetry::Accumulate(hgramId.extract(), aSample); + } + static_assert(sizeof(size_t) <= sizeof(uint64_t), + "Memory distribution samples might overflow."); + fog_memory_distribution_accumulate(mId, aSample); +} + +Result<Maybe<DistributionData>, nsCString> +MemoryDistributionMetric::TestGetValue(const nsACString& aPingName) const { + nsCString err; + if (fog_memory_distribution_test_get_error(mId, &err)) { + return Err(err); + } + if (!fog_memory_distribution_test_has_value(mId, &aPingName)) { + return Maybe<DistributionData>(); + } + nsTArray<uint64_t> buckets; + nsTArray<uint64_t> counts; + uint64_t sum; + fog_memory_distribution_test_get_value(mId, &aPingName, &sum, &buckets, + &counts); + return Some(DistributionData(buckets, counts, sum)); +} + +} // namespace impl + +NS_IMPL_CLASSINFO(GleanMemoryDistribution, nullptr, 0, {0}) +NS_IMPL_ISUPPORTS_CI(GleanMemoryDistribution, nsIGleanMemoryDistribution) + +NS_IMETHODIMP +GleanMemoryDistribution::Accumulate(uint64_t aSample) { + mMemoryDist.Accumulate(aSample); + return NS_OK; +} + +NS_IMETHODIMP +GleanMemoryDistribution::TestGetValue(const nsACString& aPingName, + JSContext* aCx, + JS::MutableHandle<JS::Value> aResult) { + auto result = mMemoryDist.TestGetValue(aPingName); + if (result.isErr()) { + aResult.set(JS::UndefinedValue()); + return NS_ERROR_LOSS_OF_SIGNIFICANT_DATA; + } + auto optresult = result.unwrap(); + if (optresult.isNothing()) { + aResult.set(JS::UndefinedValue()); + } else { + // Build return value of the form: + // { sum: #, values: {bucket1: count1, ...} } + JS::Rooted<JSObject*> root(aCx, JS_NewPlainObject(aCx)); + if (!root) { + return NS_ERROR_FAILURE; + } + uint64_t sum = optresult.ref().sum; + if (!JS_DefineProperty(aCx, root, "sum", static_cast<double>(sum), + JSPROP_ENUMERATE)) { + return NS_ERROR_FAILURE; + } + JS::Rooted<JSObject*> valuesObj(aCx, JS_NewPlainObject(aCx)); + if (!valuesObj || + !JS_DefineProperty(aCx, root, "values", valuesObj, JSPROP_ENUMERATE)) { + return NS_ERROR_FAILURE; + } + auto& data = optresult.ref().values; + for (const auto& entry : data) { + const uint64_t bucket = entry.GetKey(); + const uint64_t count = entry.GetData(); + if (!JS_DefineProperty(aCx, valuesObj, + nsPrintfCString("%" PRIu64, bucket).get(), + static_cast<double>(count), JSPROP_ENUMERATE)) { + return NS_ERROR_FAILURE; + } + } + aResult.setObject(*root); + } + return NS_OK; +} + +} // namespace mozilla::glean diff --git a/toolkit/components/glean/bindings/private/MemoryDistribution.h b/toolkit/components/glean/bindings/private/MemoryDistribution.h new file mode 100644 index 0000000000..136cdb6c91 --- /dev/null +++ b/toolkit/components/glean/bindings/private/MemoryDistribution.h @@ -0,0 +1,74 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_glean_GleanMemoryDistribution_h +#define mozilla_glean_GleanMemoryDistribution_h + +#include "mozilla/glean/bindings/DistributionData.h" +#include "mozilla/Maybe.h" +#include "nsIGleanMetrics.h" +#include "nsTArray.h" + +namespace mozilla::glean { + +namespace impl { + +class MemoryDistributionMetric { + public: + constexpr explicit MemoryDistributionMetric(uint32_t aId) : mId(aId) {} + + /* + * Accumulates the provided sample in the metric. + * + * @param aSample The sample to be recorded by the metric. The sample is + * assumed to be in the confgured memory unit of the metric. + * + * Notes: Values bigger than 1 Terabyte (2^40 bytes) are truncated and an + * InvalidValue error is recorded. + */ + void Accumulate(size_t aSample) const; + + /** + * **Test-only API** + * + * Gets the currently stored value as a DistributionData. + * + * This function will attempt to await the last parent-process task (if any) + * writing to the the metric's storage engine before returning a value. + * This function will not wait for data from child processes. + * + * This doesn't clear the stored value. + * Parent process only. Panics in child processes. + * + * @param aPingName The (optional) name of the ping to retrieve the metric + * for. Defaults to the first value in `send_in_pings`. + * + * @return value of the stored metric, or Nothing() if there is no value. + */ + Result<Maybe<DistributionData>, nsCString> TestGetValue( + const nsACString& aPingName = nsCString()) const; + + private: + const uint32_t mId; +}; +} // namespace impl + +class GleanMemoryDistribution final : public nsIGleanMemoryDistribution { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIGLEANMEMORYDISTRIBUTION + + explicit GleanMemoryDistribution(uint64_t aId) : mMemoryDist(aId){}; + + private: + virtual ~GleanMemoryDistribution() = default; + + const impl::MemoryDistributionMetric mMemoryDist; +}; + +} // namespace mozilla::glean + +#endif /* mozilla_glean_GleanMemoryDistribution_h */ diff --git a/toolkit/components/glean/bindings/private/Numerator.cpp b/toolkit/components/glean/bindings/private/Numerator.cpp new file mode 100644 index 0000000000..79a211773b --- /dev/null +++ b/toolkit/components/glean/bindings/private/Numerator.cpp @@ -0,0 +1,89 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/glean/bindings/Numerator.h" + +#include "nsString.h" +#include "mozilla/Components.h" +#include "mozilla/ResultVariant.h" +#include "mozilla/glean/bindings/ScalarGIFFTMap.h" +#include "mozilla/glean/fog_ffi_generated.h" +#include "nsIClassInfoImpl.h" + +namespace mozilla::glean { + +namespace impl { + +void NumeratorMetric::AddToNumerator(int32_t aAmount) const { + auto scalarId = ScalarIdForMetric(mId); + if (scalarId && aAmount >= 0) { + Telemetry::ScalarAdd(scalarId.extract(), u"numerator"_ns, aAmount); + } + fog_numerator_add_to_numerator(mId, aAmount); +} + +Result<Maybe<std::pair<int32_t, int32_t>>, nsCString> +NumeratorMetric::TestGetValue(const nsACString& aPingName) const { + nsCString err; + if (fog_numerator_test_get_error(mId, &err)) { + return Err(err); + } + if (!fog_numerator_test_has_value(mId, &aPingName)) { + return Maybe<std::pair<int32_t, int32_t>>(); + } + int32_t num = 0; + int32_t den = 0; + fog_numerator_test_get_value(mId, &aPingName, &num, &den); + return Some(std::make_pair(num, den)); +} + +} // namespace impl + +NS_IMPL_CLASSINFO(GleanNumerator, nullptr, 0, {0}) +NS_IMPL_ISUPPORTS_CI(GleanNumerator, nsIGleanNumerator) + +NS_IMETHODIMP +GleanNumerator::AddToNumerator(int32_t aAmount) { + mNumerator.AddToNumerator(aAmount); + return NS_OK; +} + +NS_IMETHODIMP +GleanNumerator::TestGetValue(const nsACString& aPingName, JSContext* aCx, + JS::MutableHandle<JS::Value> aResult) { + auto result = mNumerator.TestGetValue(aPingName); + if (result.isErr()) { + aResult.set(JS::UndefinedValue()); + LogToBrowserConsole(nsIScriptError::errorFlag, + NS_ConvertUTF8toUTF16(result.unwrapErr())); + return NS_ERROR_LOSS_OF_SIGNIFICANT_DATA; + } + auto optresult = result.unwrap(); + if (optresult.isNothing()) { + aResult.set(JS::UndefinedValue()); + } else { + // Build return value of the form: { numerator: n, denominator: d } + JS::Rooted<JSObject*> root(aCx, JS_NewPlainObject(aCx)); + if (!root) { + return NS_ERROR_FAILURE; + } + auto pair = optresult.extract(); + int32_t num = pair.first; + int32_t den = pair.second; + if (!JS_DefineProperty(aCx, root, "numerator", static_cast<double>(num), + JSPROP_ENUMERATE)) { + return NS_ERROR_FAILURE; + } + if (!JS_DefineProperty(aCx, root, "denominator", static_cast<double>(den), + JSPROP_ENUMERATE)) { + return NS_ERROR_FAILURE; + } + aResult.setObject(*root); + } + return NS_OK; +} + +} // namespace mozilla::glean diff --git a/toolkit/components/glean/bindings/private/Numerator.h b/toolkit/components/glean/bindings/private/Numerator.h new file mode 100644 index 0000000000..66b7eef295 --- /dev/null +++ b/toolkit/components/glean/bindings/private/Numerator.h @@ -0,0 +1,72 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_glean_GleanNumerator_h +#define mozilla_glean_GleanNumerator_h + +#include "mozilla/Maybe.h" +#include "mozilla/Result.h" +#include "nsIGleanMetrics.h" +#include "nsString.h" + +namespace mozilla::glean { + +namespace impl { + +// Actually a RateMetric, but one whose denominator is a CounterMetric external +// to the RateMetric. +class NumeratorMetric { + public: + constexpr explicit NumeratorMetric(uint32_t aId) : mId(aId) {} + + /* + * Increases the numerator by `amount`. + * + * @param aAmount The amount to increase by. Should be positive. + */ + void AddToNumerator(int32_t aAmount = 1) const; + + /** + * **Test-only API** + * + * Gets the currently stored value as a pair of integers. + * + * This function will attempt to await the last parent-process task (if any) + * writing to the the metric's storage engine before returning a value. + * This function will not wait for data from child processes. + * + * This doesn't clear the stored value. + * Parent process only. Panics in child processes. + * + * @param aPingName The (optional) name of the ping to retrieve the metric + * for. Defaults to the first value in `send_in_pings`. + * + * @return value of the stored metric, or Nothing() if there is no value. + */ + Result<Maybe<std::pair<int32_t, int32_t>>, nsCString> TestGetValue( + const nsACString& aPingName = nsCString()) const; + + private: + const uint32_t mId; +}; +} // namespace impl + +class GleanNumerator final : public nsIGleanNumerator { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIGLEANNUMERATOR + + explicit GleanNumerator(uint32_t id) : mNumerator(id){}; + + private: + virtual ~GleanNumerator() = default; + + const impl::NumeratorMetric mNumerator; +}; + +} // namespace mozilla::glean + +#endif /* mozilla_glean_GleanNumerator_h */ diff --git a/toolkit/components/glean/bindings/private/Ping.cpp b/toolkit/components/glean/bindings/private/Ping.cpp new file mode 100644 index 0000000000..c341a61148 --- /dev/null +++ b/toolkit/components/glean/bindings/private/Ping.cpp @@ -0,0 +1,94 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/glean/bindings/Ping.h" + +#ifndef MOZ_GLEAN_ANDROID +# include "mozilla/AppShutdown.h" +# include "mozilla/ClearOnShutdown.h" +#endif +#include "mozilla/Components.h" +#include "nsIClassInfoImpl.h" +#include "nsString.h" + +namespace mozilla::glean { + +namespace impl { + +#ifndef MOZ_GLEAN_ANDROID +using CallbackMapType = nsTHashMap<uint32_t, PingTestCallback>; +using MetricIdToCallbackMutex = StaticDataMutex<UniquePtr<CallbackMapType>>; +static Maybe<MetricIdToCallbackMutex::AutoLock> GetCallbackMapLock() { + static MetricIdToCallbackMutex sCallbacks("sCallbacks"); + auto lock = sCallbacks.Lock(); + // Test callbacks will continue to work until the end of AppShutdownTelemetry + if (AppShutdown::IsInOrBeyond(ShutdownPhase::XPCOMWillShutdown)) { + return Nothing(); + } + if (!*lock) { + *lock = MakeUnique<CallbackMapType>(); + RunOnShutdown( + [&] { + auto lock = sCallbacks.Lock(); + *lock = nullptr; // deletes, see UniquePtr.h + }, + ShutdownPhase::XPCOMWillShutdown); + } + return Some(std::move(lock)); +} +#endif + +void Ping::Submit(const nsACString& aReason) const { +#ifdef MOZ_GLEAN_ANDROID + Unused << mId; +#else + { + GetCallbackMapLock().apply([&](auto& lock) { + auto callback = lock.ref()->Extract(mId); + if (callback) { + callback.extract()(aReason); + } + }); + } + fog_submit_ping_by_id(mId, &aReason); +#endif +} + +void Ping::TestBeforeNextSubmit(PingTestCallback&& aCallback) const { +#ifdef MOZ_GLEAN_ANDROID + return; +#else + { + GetCallbackMapLock().apply( + [&](auto& lock) { lock.ref()->InsertOrUpdate(mId, aCallback); }); + } +#endif +} + +} // namespace impl + +NS_IMPL_CLASSINFO(GleanPing, nullptr, 0, {0}) +NS_IMPL_ISUPPORTS_CI(GleanPing, nsIGleanPing) + +NS_IMETHODIMP +GleanPing::Submit(const nsACString& aReason) { + mPing.Submit(aReason); + return NS_OK; +} + +NS_IMETHODIMP +GleanPing::TestBeforeNextSubmit(nsIGleanPingTestCallback* aCallback) { + if (NS_WARN_IF(!aCallback)) { + return NS_ERROR_INVALID_ARG; + } + // Throw the bare ptr into a COM ptr to keep it around in the lambda. + nsCOMPtr<nsIGleanPingTestCallback> callback = aCallback; + mPing.TestBeforeNextSubmit( + [callback](const nsACString& aReason) { callback->Call(aReason); }); + return NS_OK; +} + +} // namespace mozilla::glean diff --git a/toolkit/components/glean/bindings/private/Ping.h b/toolkit/components/glean/bindings/private/Ping.h new file mode 100644 index 0000000000..6bcd4cb478 --- /dev/null +++ b/toolkit/components/glean/bindings/private/Ping.h @@ -0,0 +1,83 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_glean_Ping_h +#define mozilla_glean_Ping_h + +#include "mozilla/DataMutex.h" +#include "mozilla/glean/fog_ffi_generated.h" +#include "mozilla/Maybe.h" +#include "nsIGleanMetrics.h" +#include "nsString.h" + +namespace mozilla::glean { + +typedef std::function<void(const nsACString& aReason)> PingTestCallback; + +namespace impl { + +class Ping { + public: + constexpr explicit Ping(uint32_t aId) : mId(aId) {} + + /** + * Collect and submit the ping for eventual upload. + * + * This will collect all stored data to be included in the ping. + * Data with lifetime `ping` will then be reset. + * + * If the ping is configured with `send_if_empty = false` + * and the ping currently contains no content, + * it will not be queued for upload. + * If the ping is configured with `send_if_empty = true` + * it will be queued for upload even if empty. + * + * Pings always contain the `ping_info` and `client_info` sections. + * See [ping + * sections](https://mozilla.github.io/glean/book/user/pings/index.html#ping-sections) + * for details. + * + * @param aReason - Optional. The reason the ping is being submitted. + * Must match one of the configured `reason_codes`. + */ + void Submit(const nsACString& aReason = nsCString()) const; + + /** + * **Test-only API** + * + * Register a callback to be called right before this ping is next submitted. + * The provided function is called exactly once before submitting. + * + * Note: The callback will be called on any call to submit. + * A ping may not be sent afterwards, e.g. if the ping is empty and + * `send_if_empty` is `false` + * + * @param aCallback - The callback to call on the next submit. + */ + void TestBeforeNextSubmit(PingTestCallback&& aCallback) const; + + private: + const uint32_t mId; +}; + +} // namespace impl + +class GleanPing final : public nsIGleanPing { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIGLEANPING + + explicit GleanPing(uint32_t aId) : mPing(aId) {} + + private: + virtual ~GleanPing() = default; + + const impl::Ping mPing; +}; + +} // namespace mozilla::glean + +#endif /* mozilla_glean_Ping_h */ diff --git a/toolkit/components/glean/bindings/private/Quantity.cpp b/toolkit/components/glean/bindings/private/Quantity.cpp new file mode 100644 index 0000000000..62591e6c90 --- /dev/null +++ b/toolkit/components/glean/bindings/private/Quantity.cpp @@ -0,0 +1,74 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/glean/bindings/Quantity.h" + +#include "mozilla/Components.h" +#include "mozilla/ResultVariant.h" +#include "mozilla/glean/bindings/ScalarGIFFTMap.h" +#include "mozilla/glean/fog_ffi_generated.h" +#include "nsIClassInfoImpl.h" +#include "nsString.h" + +namespace mozilla::glean { + +namespace impl { + +void QuantityMetric::Set(int64_t aValue) const { + auto scalarId = ScalarIdForMetric(mId); + if (scalarId && aValue >= 0) { + uint32_t theValue = static_cast<uint32_t>(aValue); + if (aValue > std::numeric_limits<uint32_t>::max()) { + theValue = std::numeric_limits<uint32_t>::max(); + } + Telemetry::ScalarSet(scalarId.extract(), theValue); + } + fog_quantity_set(mId, aValue); +} + +Result<Maybe<int64_t>, nsCString> QuantityMetric::TestGetValue( + const nsACString& aPingName) const { + nsCString err; + if (fog_quantity_test_get_error(mId, &err)) { + return Err(err); + } + if (!fog_quantity_test_has_value(mId, &aPingName)) { + return Maybe<int64_t>(); + } + return Some(fog_quantity_test_get_value(mId, &aPingName)); +} + +} // namespace impl + +NS_IMPL_CLASSINFO(GleanQuantity, nullptr, 0, {0}) +NS_IMPL_ISUPPORTS_CI(GleanQuantity, nsIGleanQuantity) + +NS_IMETHODIMP +GleanQuantity::Set(int64_t aValue) { + mQuantity.Set(aValue); + return NS_OK; +} + +NS_IMETHODIMP +GleanQuantity::TestGetValue(const nsACString& aPingName, + JS::MutableHandle<JS::Value> aResult) { + auto result = mQuantity.TestGetValue(aPingName); + if (result.isErr()) { + aResult.set(JS::UndefinedValue()); + LogToBrowserConsole(nsIScriptError::errorFlag, + NS_ConvertUTF8toUTF16(result.unwrapErr())); + return NS_ERROR_LOSS_OF_SIGNIFICANT_DATA; + } + auto optresult = result.unwrap(); + if (optresult.isNothing()) { + aResult.set(JS::UndefinedValue()); + } else { + aResult.set(JS::DoubleValue(static_cast<double>(optresult.value()))); + } + return NS_OK; +} + +} // namespace mozilla::glean diff --git a/toolkit/components/glean/bindings/private/Quantity.h b/toolkit/components/glean/bindings/private/Quantity.h new file mode 100644 index 0000000000..9de1007d10 --- /dev/null +++ b/toolkit/components/glean/bindings/private/Quantity.h @@ -0,0 +1,68 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_glean_GleanQuantity_h +#define mozilla_glean_GleanQuantity_h + +#include "nsIGleanMetrics.h" + +namespace mozilla::glean { + +namespace impl { + +class QuantityMetric { + public: + constexpr explicit QuantityMetric(uint32_t id) : mId(id) {} + + /** + * Set to the specified value. + * + * @param aValue the value to set. + */ + void Set(int64_t aValue) const; + + /** + * **Test-only API** + * + * Gets the currently stored value. + * + * This function will attempt to await the last parent-process task (if any) + * writing to the the metric's storage engine before returning a value. + * This function will not wait for data from child processes. + * + * This doesn't clear the stored value. + * Parent process only. Panics in child processes. + * + * @param aPingName The (optional) name of the ping to retrieve the metric + * for. Defaults to the first value in `send_in_pings`. + * + * @return value of the stored metric. + */ + Result<Maybe<int64_t>, nsCString> TestGetValue( + const nsACString& aPingName = nsCString()) const; + + private: + const uint32_t mId; +}; + +} // namespace impl + +class GleanQuantity final : public nsIGleanQuantity { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIGLEANQUANTITY + + explicit GleanQuantity(uint32_t id) : mQuantity(id){}; + + private: + virtual ~GleanQuantity() = default; + + const impl::QuantityMetric mQuantity; +}; + +} // namespace mozilla::glean + +#endif /* mozilla_glean_GleanQuantity.h */ diff --git a/toolkit/components/glean/bindings/private/Rate.cpp b/toolkit/components/glean/bindings/private/Rate.cpp new file mode 100644 index 0000000000..6bc6ad61c9 --- /dev/null +++ b/toolkit/components/glean/bindings/private/Rate.cpp @@ -0,0 +1,105 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/glean/bindings/Rate.h" + +#include "jsapi.h" +#include "nsString.h" +#include "mozilla/Components.h" +#include "mozilla/ResultVariant.h" +#include "mozilla/glean/bindings/Common.h" +#include "mozilla/glean/bindings/ScalarGIFFTMap.h" +#include "mozilla/glean/fog_ffi_generated.h" +#include "nsIClassInfoImpl.h" + +namespace mozilla::glean { + +namespace impl { + +void RateMetric::AddToNumerator(int32_t aAmount) const { + auto scalarId = ScalarIdForMetric(mId); + if (scalarId && aAmount >= 0) { + Telemetry::ScalarAdd(scalarId.extract(), u"numerator"_ns, aAmount); + } + fog_rate_add_to_numerator(mId, aAmount); +} + +void RateMetric::AddToDenominator(int32_t aAmount) const { + auto scalarId = ScalarIdForMetric(mId); + if (scalarId && aAmount >= 0) { + Telemetry::ScalarAdd(scalarId.extract(), u"denominator"_ns, aAmount); + } + fog_rate_add_to_denominator(mId, aAmount); +} + +Result<Maybe<std::pair<int32_t, int32_t>>, nsCString> RateMetric::TestGetValue( + const nsACString& aPingName) const { + nsCString err; + if (fog_rate_test_get_error(mId, &err)) { + return Err(err); + } + if (!fog_rate_test_has_value(mId, &aPingName)) { + return Maybe<std::pair<int32_t, int32_t>>(); + } + int32_t num = 0; + int32_t den = 0; + fog_rate_test_get_value(mId, &aPingName, &num, &den); + return Some(std::make_pair(num, den)); +} + +} // namespace impl + +NS_IMPL_CLASSINFO(GleanRate, nullptr, 0, {0}) +NS_IMPL_ISUPPORTS_CI(GleanRate, nsIGleanRate) + +NS_IMETHODIMP +GleanRate::AddToNumerator(int32_t aAmount) { + mRate.AddToNumerator(aAmount); + return NS_OK; +} + +NS_IMETHODIMP +GleanRate::AddToDenominator(int32_t aAmount) { + mRate.AddToDenominator(aAmount); + return NS_OK; +} + +NS_IMETHODIMP +GleanRate::TestGetValue(const nsACString& aPingName, JSContext* aCx, + JS::MutableHandle<JS::Value> aResult) { + auto result = mRate.TestGetValue(aPingName); + if (result.isErr()) { + aResult.set(JS::UndefinedValue()); + LogToBrowserConsole(nsIScriptError::errorFlag, + NS_ConvertUTF8toUTF16(result.unwrapErr())); + return NS_ERROR_LOSS_OF_SIGNIFICANT_DATA; + } + auto optresult = result.unwrap(); + if (optresult.isNothing()) { + aResult.set(JS::UndefinedValue()); + } else { + // Build return value of the form: { numerator: n, denominator: d } + JS::Rooted<JSObject*> root(aCx, JS_NewPlainObject(aCx)); + if (!root) { + return NS_ERROR_FAILURE; + } + auto pair = optresult.extract(); + int32_t num = pair.first; + int32_t den = pair.second; + if (!JS_DefineProperty(aCx, root, "numerator", static_cast<double>(num), + JSPROP_ENUMERATE)) { + return NS_ERROR_FAILURE; + } + if (!JS_DefineProperty(aCx, root, "denominator", static_cast<double>(den), + JSPROP_ENUMERATE)) { + return NS_ERROR_FAILURE; + } + aResult.setObject(*root); + } + return NS_OK; +} + +} // namespace mozilla::glean diff --git a/toolkit/components/glean/bindings/private/Rate.h b/toolkit/components/glean/bindings/private/Rate.h new file mode 100644 index 0000000000..85e8b4e1d6 --- /dev/null +++ b/toolkit/components/glean/bindings/private/Rate.h @@ -0,0 +1,77 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_glean_GleanRate_h +#define mozilla_glean_GleanRate_h + +#include "mozilla/Maybe.h" +#include "mozilla/Result.h" +#include "nsIGleanMetrics.h" +#include "nsString.h" + +namespace mozilla::glean { + +namespace impl { + +class RateMetric { + public: + constexpr explicit RateMetric(uint32_t aId) : mId(aId) {} + + /* + * Increases the numerator by `amount`. + * + * @param aAmount The amount to increase by. Should be positive. + */ + void AddToNumerator(int32_t aAmount = 1) const; + + /* + * Increases the denominator by `amount`. + * + * @param aAmount The amount to increase by. Should be positive. + */ + void AddToDenominator(int32_t aAmount = 1) const; + + /** + * **Test-only API** + * + * Gets the currently stored value as a pair of integers. + * + * This function will attempt to await the last parent-process task (if any) + * writing to the the metric's storage engine before returning a value. + * This function will not wait for data from child processes. + * + * This doesn't clear the stored value. + * Parent process only. Panics in child processes. + * + * @param aPingName The (optional) name of the ping to retrieve the metric + * for. Defaults to the first value in `send_in_pings`. + * + * @return value of the stored metric, or Nothing() if there is no value. + */ + Result<Maybe<std::pair<int32_t, int32_t>>, nsCString> TestGetValue( + const nsACString& aPingName = nsCString()) const; + + private: + const uint32_t mId; +}; +} // namespace impl + +class GleanRate final : public nsIGleanRate { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIGLEANRATE + + explicit GleanRate(uint32_t id) : mRate(id){}; + + private: + virtual ~GleanRate() = default; + + const impl::RateMetric mRate; +}; + +} // namespace mozilla::glean + +#endif /* mozilla_glean_GleanRate_h */ diff --git a/toolkit/components/glean/bindings/private/String.cpp b/toolkit/components/glean/bindings/private/String.cpp new file mode 100644 index 0000000000..bc191a7400 --- /dev/null +++ b/toolkit/components/glean/bindings/private/String.cpp @@ -0,0 +1,77 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/glean/bindings/String.h" + +#include "Common.h" +#include "jsapi.h" +#include "js/String.h" +#include "nsString.h" +#include "mozilla/Components.h" +#include "mozilla/ResultVariant.h" +#include "mozilla/glean/bindings/ScalarGIFFTMap.h" +#include "mozilla/glean/fog_ffi_generated.h" +#include "nsIClassInfoImpl.h" + +namespace mozilla::glean { + +namespace impl { + +void StringMetric::Set(const nsACString& aValue) const { + auto scalarId = ScalarIdForMetric(mId); + if (scalarId) { + Telemetry::ScalarSet(scalarId.extract(), NS_ConvertUTF8toUTF16(aValue)); + } + fog_string_set(mId, &aValue); +} + +Result<Maybe<nsCString>, nsCString> StringMetric::TestGetValue( + const nsACString& aPingName) const { + nsCString err; + if (fog_string_test_get_error(mId, &err)) { + return Err(err); + } + if (!fog_string_test_has_value(mId, &aPingName)) { + return Maybe<nsCString>(); + } + nsCString ret; + fog_string_test_get_value(mId, &aPingName, &ret); + return Some(ret); +} + +} // namespace impl + +NS_IMPL_CLASSINFO(GleanString, nullptr, 0, {0}) +NS_IMPL_ISUPPORTS_CI(GleanString, nsIGleanString) + +NS_IMETHODIMP +GleanString::Set(const nsACString& aValue) { + mString.Set(aValue); + return NS_OK; +} + +NS_IMETHODIMP +GleanString::TestGetValue(const nsACString& aStorageName, JSContext* aCx, + JS::MutableHandle<JS::Value> aResult) { + auto result = mString.TestGetValue(aStorageName); + if (result.isErr()) { + aResult.set(JS::UndefinedValue()); + LogToBrowserConsole(nsIScriptError::errorFlag, + NS_ConvertUTF8toUTF16(result.unwrapErr())); + return NS_ERROR_LOSS_OF_SIGNIFICANT_DATA; + } + auto optresult = result.unwrap(); + if (optresult.isNothing()) { + aResult.set(JS::UndefinedValue()); + } else { + const NS_ConvertUTF8toUTF16 str(optresult.ref()); + aResult.set( + JS::StringValue(JS_NewUCStringCopyN(aCx, str.Data(), str.Length()))); + } + return NS_OK; +} + +} // namespace mozilla::glean diff --git a/toolkit/components/glean/bindings/private/String.h b/toolkit/components/glean/bindings/private/String.h new file mode 100644 index 0000000000..8c3c6ae05e --- /dev/null +++ b/toolkit/components/glean/bindings/private/String.h @@ -0,0 +1,73 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_glean_GleanString_h +#define mozilla_glean_GleanString_h + +#include "mozilla/Maybe.h" +#include "mozilla/Result.h" +#include "nsIGleanMetrics.h" +#include "nsString.h" + +namespace mozilla::glean { + +namespace impl { + +class StringMetric { + public: + constexpr explicit StringMetric(uint32_t aId) : mId(aId) {} + + /* + * Set to the specified value. + * + * Truncates the value if it is longer than 100 bytes and logs an error. + * See https://mozilla.github.io/glean/book/user/metrics/string.html#limits. + * + * @param aValue The string to set the metric to. + */ + void Set(const nsACString& aValue) const; + + /** + * **Test-only API** + * + * Gets the currently stored value as a string. + * + * This function will attempt to await the last parent-process task (if any) + * writing to the the metric's storage engine before returning a value. + * This function will not wait for data from child processes. + * + * This doesn't clear the stored value. + * Parent process only. Panics in child processes. + * + * @param aPingName The (optional) name of the ping to retrieve the metric + * for. Defaults to the first value in `send_in_pings`. + * + * @return value of the stored metric, or Nothing() if there is no value. + */ + Result<Maybe<nsCString>, nsCString> TestGetValue( + const nsACString& aPingName = nsCString()) const; + + private: + const uint32_t mId; +}; +} // namespace impl + +class GleanString final : public nsIGleanString { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIGLEANSTRING + + explicit GleanString(uint32_t aId) : mString(aId){}; + + private: + virtual ~GleanString() = default; + + const impl::StringMetric mString; +}; + +} // namespace mozilla::glean + +#endif /* mozilla_glean_GleanString_h */ diff --git a/toolkit/components/glean/bindings/private/StringList.cpp b/toolkit/components/glean/bindings/private/StringList.cpp new file mode 100644 index 0000000000..8882922551 --- /dev/null +++ b/toolkit/components/glean/bindings/private/StringList.cpp @@ -0,0 +1,92 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/glean/bindings/StringList.h" + +#include "Common.h" +#include "mozilla/Components.h" +#include "mozilla/ResultVariant.h" +#include "mozilla/dom/ToJSValue.h" +#include "mozilla/glean/bindings/ScalarGIFFTMap.h" +#include "mozilla/glean/fog_ffi_generated.h" +#include "nsIClassInfoImpl.h" +#include "nsString.h" +#include "nsTArray.h" + +namespace mozilla::glean { + +namespace impl { + +void StringListMetric::Add(const nsACString& aValue) const { + auto scalarId = ScalarIdForMetric(mId); + if (scalarId) { + Telemetry::ScalarSet(scalarId.extract(), NS_ConvertUTF8toUTF16(aValue), + true); + } + fog_string_list_add(mId, &aValue); +} + +void StringListMetric::Set(const nsTArray<nsCString>& aValue) const { + // Calling `Set` on a mirrored labeled_string is likely an error. + // We can't remove keys from the mirror scalar and handle this 'properly', + // so you shouldn't use this operation at all. + (void)NS_WARN_IF(ScalarIdForMetric(mId).isSome()); + fog_string_list_set(mId, &aValue); +} + +Result<Maybe<nsTArray<nsCString>>, nsCString> StringListMetric::TestGetValue( + const nsACString& aPingName) const { + nsCString err; + if (fog_string_list_test_get_error(mId, &err)) { + return Err(err); + } + if (!fog_string_list_test_has_value(mId, &aPingName)) { + return Maybe<nsTArray<nsCString>>(); + } + nsTArray<nsCString> ret; + fog_string_list_test_get_value(mId, &aPingName, &ret); + return Some(std::move(ret)); +} + +} // namespace impl + +NS_IMPL_CLASSINFO(GleanStringList, nullptr, 0, {0}) +NS_IMPL_ISUPPORTS_CI(GleanStringList, nsIGleanStringList) + +NS_IMETHODIMP +GleanStringList::Add(const nsACString& aValue) { + mStringList.Add(aValue); + return NS_OK; +} + +NS_IMETHODIMP +GleanStringList::Set(const nsTArray<nsCString>& aValue) { + mStringList.Set(aValue); + return NS_OK; +} + +NS_IMETHODIMP +GleanStringList::TestGetValue(const nsACString& aStorageName, JSContext* aCx, + JS::MutableHandle<JS::Value> aResult) { + auto result = mStringList.TestGetValue(aStorageName); + if (result.isErr()) { + aResult.set(JS::UndefinedValue()); + LogToBrowserConsole(nsIScriptError::errorFlag, + NS_ConvertUTF8toUTF16(result.unwrapErr())); + return NS_ERROR_LOSS_OF_SIGNIFICANT_DATA; + } + auto optresult = result.unwrap(); + if (optresult.isNothing()) { + aResult.set(JS::UndefinedValue()); + } else { + if (!dom::ToJSValue(aCx, optresult.ref(), aResult)) { + return NS_ERROR_FAILURE; + } + } + return NS_OK; +} + +} // namespace mozilla::glean diff --git a/toolkit/components/glean/bindings/private/StringList.h b/toolkit/components/glean/bindings/private/StringList.h new file mode 100644 index 0000000000..6b7b9358e2 --- /dev/null +++ b/toolkit/components/glean/bindings/private/StringList.h @@ -0,0 +1,84 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_glean_GleanStringList_h +#define mozilla_glean_GleanStringList_h + +#include "mozilla/Maybe.h" +#include "nsIGleanMetrics.h" +#include "nsString.h" +#include "nsTArray.h" + +namespace mozilla::glean { + +namespace impl { + +class StringListMetric { + public: + constexpr explicit StringListMetric(uint32_t aId) : mId(aId) {} + + /* + * Adds a new string to the list. + * + * Truncates the value if it is longer than 50 bytes and logs an error. + * + * @param aValue The string to add. + */ + void Add(const nsACString& aValue) const; + + /* + * Set to a specific list of strings. + * + * Truncates any values longer than 50 bytes and logs an error. + * Truncates the list if it is over 20 items long. + * See + * https://mozilla.github.io/glean/book/user/metrics/string_list.html#limits. + * + * @param aValue The list of strings to set the metric to. + */ + void Set(const nsTArray<nsCString>& aValue) const; + + /** + * **Test-only API** + * + * Gets the currently stored value. + * + * This function will attempt to await the last parent-process task (if any) + * writing to the the metric's storage engine before returning a value. + * This function will not wait for data from child processes. + * + * This doesn't clear the stored value. + * Parent process only. Panics in child processes. + * + * @param aPingName The (optional) name of the ping to retrieve the metric + * for. Defaults to the first value in `send_in_pings`. + * + * @return value of the stored metric, or Nothing() if there is no value. + */ + Result<Maybe<nsTArray<nsCString>>, nsCString> TestGetValue( + const nsACString& aPingName = nsCString()) const; + + private: + const uint32_t mId; +}; +} // namespace impl + +class GleanStringList final : public nsIGleanStringList { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIGLEANSTRINGLIST + + explicit GleanStringList(uint32_t aId) : mStringList(aId){}; + + private: + virtual ~GleanStringList() = default; + + const impl::StringListMetric mStringList; +}; + +} // namespace mozilla::glean + +#endif /* mozilla_glean_GleanStringList_h */ diff --git a/toolkit/components/glean/bindings/private/Timespan.cpp b/toolkit/components/glean/bindings/private/Timespan.cpp new file mode 100644 index 0000000000..e8377c8fcf --- /dev/null +++ b/toolkit/components/glean/bindings/private/Timespan.cpp @@ -0,0 +1,133 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/glean/bindings/Timespan.h" + +#include "Common.h" +#include "nsString.h" +#include "mozilla/Components.h" +#include "mozilla/ResultVariant.h" +#include "mozilla/glean/bindings/ScalarGIFFTMap.h" +#include "mozilla/glean/fog_ffi_generated.h" +#include "nsIClassInfoImpl.h" + +namespace mozilla::glean { + +namespace impl { + +void TimespanMetric::Start() const { + auto optScalarId = ScalarIdForMetric(mId); + if (optScalarId) { + auto scalarId = optScalarId.extract(); + GetTimesToStartsLock().apply([&](auto& lock) { + (void)NS_WARN_IF(lock.ref()->Remove(scalarId)); + lock.ref()->InsertOrUpdate(scalarId, TimeStamp::Now()); + }); + } + fog_timespan_start(mId); +} + +void TimespanMetric::Stop() const { + auto optScalarId = ScalarIdForMetric(mId); + if (optScalarId) { + auto scalarId = optScalarId.extract(); + GetTimesToStartsLock().apply([&](auto& lock) { + auto optStart = lock.ref()->Extract(scalarId); + if (!NS_WARN_IF(!optStart)) { + double delta = (TimeStamp::Now() - optStart.extract()).ToMilliseconds(); + uint32_t theDelta = static_cast<uint32_t>(delta); + if (delta > std::numeric_limits<uint32_t>::max()) { + theDelta = std::numeric_limits<uint32_t>::max(); + } else if (MOZ_UNLIKELY(delta < 0)) { + theDelta = 0; + } + Telemetry::ScalarSet(scalarId, theDelta); + } + }); + } + fog_timespan_stop(mId); +} + +void TimespanMetric::Cancel() const { + auto optScalarId = ScalarIdForMetric(mId); + if (optScalarId) { + auto scalarId = optScalarId.extract(); + GetTimesToStartsLock().apply( + [&](auto& lock) { lock.ref()->Remove(scalarId); }); + } + fog_timespan_cancel(mId); +} + +void TimespanMetric::SetRaw(uint32_t aDuration) const { + auto optScalarId = ScalarIdForMetric(mId); + if (optScalarId) { + auto scalarId = optScalarId.extract(); + Telemetry::ScalarSet(scalarId, aDuration); + } + fog_timespan_set_raw(mId, aDuration); +} + +Result<Maybe<uint64_t>, nsCString> TimespanMetric::TestGetValue( + const nsACString& aPingName) const { + nsCString err; + if (fog_timespan_test_get_error(mId, &err)) { + return Err(err); + } + if (!fog_timespan_test_has_value(mId, &aPingName)) { + return Maybe<uint64_t>(); + } + return Some(fog_timespan_test_get_value(mId, &aPingName)); +} + +} // namespace impl + +NS_IMPL_CLASSINFO(GleanTimespan, nullptr, 0, {0}) +NS_IMPL_ISUPPORTS_CI(GleanTimespan, nsIGleanTimespan) + +NS_IMETHODIMP +GleanTimespan::Start() { + mTimespan.Start(); + return NS_OK; +} + +NS_IMETHODIMP +GleanTimespan::Stop() { + mTimespan.Stop(); + return NS_OK; +} + +NS_IMETHODIMP +GleanTimespan::Cancel() { + mTimespan.Cancel(); + return NS_OK; +} + +NS_IMETHODIMP +GleanTimespan::SetRaw(uint32_t aDuration) { + mTimespan.SetRaw(aDuration); + return NS_OK; +} + +NS_IMETHODIMP +GleanTimespan::TestGetValue(const nsACString& aStorageName, + JS::MutableHandle<JS::Value> aResult) { + auto result = mTimespan.TestGetValue(aStorageName); + if (result.isErr()) { + aResult.set(JS::UndefinedValue()); + LogToBrowserConsole(nsIScriptError::errorFlag, + NS_ConvertUTF8toUTF16(result.unwrapErr())); + return NS_ERROR_LOSS_OF_SIGNIFICANT_DATA; + } + auto optresult = result.unwrap(); + if (optresult.isNothing()) { + aResult.set(JS::UndefinedValue()); + } else { + aResult.set(JS::DoubleValue(static_cast<double>(optresult.value()))); + } + return NS_OK; +} + +} // namespace mozilla::glean diff --git a/toolkit/components/glean/bindings/private/Timespan.h b/toolkit/components/glean/bindings/private/Timespan.h new file mode 100644 index 0000000000..b82a174034 --- /dev/null +++ b/toolkit/components/glean/bindings/private/Timespan.h @@ -0,0 +1,99 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_glean_GleanTimespan_h +#define mozilla_glean_GleanTimespan_h + +#include "mozilla/Maybe.h" +#include "mozilla/Result.h" +#include "nsIGleanMetrics.h" + +namespace mozilla::glean { + +namespace impl { + +class TimespanMetric { + public: + constexpr explicit TimespanMetric(uint32_t aId) : mId(aId) {} + + /** + * Start tracking time for the provided metric. + * + * This records an error if it’s already tracking time (i.e. start was already + * called with no corresponding [stop]): in that case the original + * start time will be preserved. + */ + void Start() const; + + /** + * Stop tracking time for the provided metric. + * + * Sets the metric to the elapsed time, but does not overwrite an already + * existing value. + * This will record an error if no [start] was called or there is an already + * existing value. + */ + void Stop() const; + + /** + * Abort a previous Start. + * + * No error will be recorded if no Start was called. + */ + void Cancel() const; + + /** + * Explicitly sets the timespan value + * + * This API should only be used if you cannot make use of + * `start`/`stop`/`cancel`. + * + * @param aDuration The duration of this timespan, in units matching the + * `time_unit` of this metric's definition. + */ + void SetRaw(uint32_t aDuration) const; + + /** + * **Test-only API** + * + * Gets the currently stored value as an integer. + * + * This function will attempt to await the last parent-process task (if any) + * writing to the the metric's storage engine before returning a value. + * This function will not wait for data from child processes. + * + * This doesn't clear the stored value. + * Parent process only. Panics in child processes. + * + * @param aPingName The (optional) name of the ping to retrieve the metric + * for. Defaults to the first value in `send_in_pings`. + * + * @return value of the stored metric, or Nothing() if there is no value. + */ + Result<Maybe<uint64_t>, nsCString> TestGetValue( + const nsACString& aPingName = nsCString()) const; + + private: + const uint32_t mId; +}; +} // namespace impl + +class GleanTimespan final : public nsIGleanTimespan { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIGLEANTIMESPAN + + explicit GleanTimespan(uint32_t aId) : mTimespan(aId){}; + + private: + virtual ~GleanTimespan() = default; + + const impl::TimespanMetric mTimespan; +}; + +} // namespace mozilla::glean + +#endif /* mozilla_glean_GleanTimespan_h */ diff --git a/toolkit/components/glean/bindings/private/TimingDistribution.cpp b/toolkit/components/glean/bindings/private/TimingDistribution.cpp new file mode 100644 index 0000000000..7ec20f94b7 --- /dev/null +++ b/toolkit/components/glean/bindings/private/TimingDistribution.cpp @@ -0,0 +1,196 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/glean/bindings/TimingDistribution.h" + +#include "Common.h" +#include "mozilla/Components.h" +#include "mozilla/ResultVariant.h" +#include "mozilla/Tuple.h" +#include "mozilla/dom/ToJSValue.h" +#include "mozilla/glean/bindings/HistogramGIFFTMap.h" +#include "mozilla/glean/fog_ffi_generated.h" +#include "nsIClassInfoImpl.h" +#include "nsJSUtils.h" +#include "nsPrintfCString.h" +#include "nsString.h" +#include "js/PropertyAndElement.h" // JS_DefineProperty + +// Called from within FOG's Rust impl. +extern "C" NS_EXPORT void GIFFT_TimingDistributionStart( + uint32_t aMetricId, mozilla::glean::TimerId aTimerId) { + auto mirrorId = mozilla::glean::HistogramIdForMetric(aMetricId); + if (mirrorId) { + mozilla::glean::GetTimerIdToStartsLock().apply([&](auto& lock) { + auto tuple = mozilla::MakeTuple(aMetricId, aTimerId); + // It should be all but impossible for anyone to have already inserted + // this timer for this metric given the monotonicity of timer ids. + (void)NS_WARN_IF(lock.ref()->Remove(tuple)); + lock.ref()->InsertOrUpdate(tuple, mozilla::TimeStamp::Now()); + }); + } +} + +// Called from within FOG's Rust impl. +extern "C" NS_EXPORT void GIFFT_TimingDistributionStopAndAccumulate( + uint32_t aMetricId, mozilla::glean::TimerId aTimerId) { + auto mirrorId = mozilla::glean::HistogramIdForMetric(aMetricId); + if (mirrorId) { + mozilla::glean::GetTimerIdToStartsLock().apply([&](auto& lock) { + auto optStart = + lock.ref()->Extract(mozilla::MakeTuple(aMetricId, aTimerId)); + // The timer might not be in the map to be removed if it's already been + // cancelled or stop_and_accumulate'd. + if (!NS_WARN_IF(!optStart)) { + AccumulateTimeDelta(mirrorId.extract(), optStart.extract()); + } + }); + } +} + +// Called from within FOG's Rust impl. +extern "C" NS_EXPORT void GIFFT_TimingDistributionAccumulateRawMillis( + uint32_t aMetricId, uint32_t aMS) { + auto mirrorId = mozilla::glean::HistogramIdForMetric(aMetricId); + if (mirrorId) { + Accumulate(mirrorId.extract(), aMS); + } +} + +// Called from within FOG's Rust impl. +extern "C" NS_EXPORT void GIFFT_TimingDistributionCancel( + uint32_t aMetricId, mozilla::glean::TimerId aTimerId) { + auto mirrorId = mozilla::glean::HistogramIdForMetric(aMetricId); + if (mirrorId) { + mozilla::glean::GetTimerIdToStartsLock().apply([&](auto& lock) { + // The timer might not be in the map to be removed if it's already been + // cancelled or stop_and_accumulate'd. + (void)NS_WARN_IF( + !lock.ref()->Remove(mozilla::MakeTuple(aMetricId, aTimerId))); + }); + } +} + +namespace mozilla::glean { + +namespace impl { + +TimerId TimingDistributionMetric::Start() const { + return fog_timing_distribution_start(mId); +} + +void TimingDistributionMetric::StopAndAccumulate(const TimerId&& aId) const { + fog_timing_distribution_stop_and_accumulate(mId, aId); +} + +// Intentionally not exposed to JS for lack of use case and a time duration +// type. +void TimingDistributionMetric::AccumulateRawDuration( + const TimeDuration& aDuration) const { + fog_timing_distribution_accumulate_raw_nanos( + mId, uint64_t(aDuration.ToMicroseconds() * 1000.00)); +} + +void TimingDistributionMetric::Cancel(const TimerId&& aId) const { + fog_timing_distribution_cancel(mId, aId); +} + +Result<Maybe<DistributionData>, nsCString> +TimingDistributionMetric::TestGetValue(const nsACString& aPingName) const { + nsCString err; + if (fog_timing_distribution_test_get_error(mId, &err)) { + return Err(err); + } + if (!fog_timing_distribution_test_has_value(mId, &aPingName)) { + return Maybe<DistributionData>(); + } + nsTArray<uint64_t> buckets; + nsTArray<uint64_t> counts; + uint64_t sum; + fog_timing_distribution_test_get_value(mId, &aPingName, &sum, &buckets, + &counts); + return Some(DistributionData(buckets, counts, sum)); +} + +} // namespace impl + +NS_IMPL_CLASSINFO(GleanTimingDistribution, nullptr, 0, {0}) +NS_IMPL_ISUPPORTS_CI(GleanTimingDistribution, nsIGleanTimingDistribution) + +NS_IMETHODIMP +GleanTimingDistribution::Start(JSContext* aCx, + JS::MutableHandle<JS::Value> aResult) { + if (!dom::ToJSValue(aCx, mTimingDist.Start(), aResult)) { + return NS_ERROR_FAILURE; + } + return NS_OK; +} + +NS_IMETHODIMP +GleanTimingDistribution::StopAndAccumulate(uint64_t aId) { + mTimingDist.StopAndAccumulate(std::move(aId)); + return NS_OK; +} + +NS_IMETHODIMP +GleanTimingDistribution::Cancel(uint64_t aId) { + mTimingDist.Cancel(std::move(aId)); + return NS_OK; +} + +NS_IMETHODIMP +GleanTimingDistribution::TestGetValue(const nsACString& aPingName, + JSContext* aCx, + JS::MutableHandle<JS::Value> aResult) { + auto result = mTimingDist.TestGetValue(aPingName); + if (result.isErr()) { + aResult.set(JS::UndefinedValue()); + LogToBrowserConsole(nsIScriptError::errorFlag, + NS_ConvertUTF8toUTF16(result.unwrapErr())); + return NS_ERROR_LOSS_OF_SIGNIFICANT_DATA; + } + auto optresult = result.unwrap(); + if (optresult.isNothing()) { + aResult.set(JS::UndefinedValue()); + } else { + // Build return value of the form: { sum: #, values: {bucket1: count1, + // ...} + JS::Rooted<JSObject*> root(aCx, JS_NewPlainObject(aCx)); + if (!root) { + return NS_ERROR_FAILURE; + } + uint64_t sum = optresult.ref().sum; + if (!JS_DefineProperty(aCx, root, "sum", static_cast<double>(sum), + JSPROP_ENUMERATE)) { + return NS_ERROR_FAILURE; + } + JS::Rooted<JSObject*> valuesObj(aCx, JS_NewPlainObject(aCx)); + if (!valuesObj || + !JS_DefineProperty(aCx, root, "values", valuesObj, JSPROP_ENUMERATE)) { + return NS_ERROR_FAILURE; + } + auto& data = optresult.ref().values; + for (const auto& entry : data) { + const uint64_t bucket = entry.GetKey(); + const uint64_t count = entry.GetData(); + if (!JS_DefineProperty(aCx, valuesObj, + nsPrintfCString("%" PRIu64, bucket).get(), + static_cast<double>(count), JSPROP_ENUMERATE)) { + return NS_ERROR_FAILURE; + } + } + aResult.setObject(*root); + } + return NS_OK; +} + +NS_IMETHODIMP +GleanTimingDistribution::TestAccumulateRawMillis(uint64_t aSample) { + mTimingDist.AccumulateRawDuration(TimeDuration::FromMilliseconds(aSample)); + return NS_OK; +} + +} // namespace mozilla::glean diff --git a/toolkit/components/glean/bindings/private/TimingDistribution.h b/toolkit/components/glean/bindings/private/TimingDistribution.h new file mode 100644 index 0000000000..7c2e69d89e --- /dev/null +++ b/toolkit/components/glean/bindings/private/TimingDistribution.h @@ -0,0 +1,109 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_glean_GleanTimingDistribution_h +#define mozilla_glean_GleanTimingDistribution_h + +#include "mozilla/glean/bindings/DistributionData.h" +#include "mozilla/Maybe.h" +#include "mozilla/Result.h" +#include "mozilla/TimeStamp.h" +#include "nsIGleanMetrics.h" +#include "nsTArray.h" + +namespace mozilla::glean { + +typedef uint64_t TimerId; + +namespace impl { +class TimingDistributionMetric { + public: + constexpr explicit TimingDistributionMetric(uint32_t aId) : mId(aId) {} + + /* + * Starts tracking time for the provided metric. + * + * @returns A unique TimerId for the new timer + */ + TimerId Start() const; + + /* + * 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` was called on this TimerId or + * if this TimerId was used to call `Cancel`. + * + * @param aId The TimerId to associate with this timing. This allows for + * concurrent timing of events associated with different ids. + */ + void StopAndAccumulate(const TimerId&& aId) const; + + /* + * Adds a duration sample to a timing distribution metric. + * + * Adds a count to the corresponding bucket in the timing distribution. + * Prefer Start() and StopAndAccumulate() where possible. + * Users of this API are responsible for ensuring the timing source used + * to calculate the TimeDuration is monotonic and consistent accross + * platforms. + * + * NOTE: Negative durations are not handled and will saturate to INT64_MAX + * nanoseconds. + * + * @param aDuration The duration of the sample to add to the distribution. + */ + void AccumulateRawDuration(const TimeDuration& aDuration) const; + + /* + * Aborts a previous `Start` call. No error is recorded if no `Start` was + * called. + * + * @param aId The TimerId whose `Start` you wish to abort. + */ + void Cancel(const TimerId&& aId) const; + + /** + * **Test-only API** + * + * Gets the currently stored value as a DistributionData. + * + * This function will attempt to await the last parent-process task (if any) + * writing to the the metric's storage engine before returning a value. + * This function will not wait for data from child processes. + * + * This doesn't clear the stored value. + * Parent process only. Panics in child processes. + * + * @param aPingName The (optional) name of the ping to retrieve the metric + * for. Defaults to the first value in `send_in_pings`. + * + * @return value of the stored metric, or Nothing() if there is no value. + */ + Result<Maybe<DistributionData>, nsCString> TestGetValue( + const nsACString& aPingName = nsCString()) const; + + private: + const uint32_t mId; +}; +} // namespace impl + +class GleanTimingDistribution final : public nsIGleanTimingDistribution { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIGLEANTIMINGDISTRIBUTION + + explicit GleanTimingDistribution(uint64_t aId) : mTimingDist(aId){}; + + private: + virtual ~GleanTimingDistribution() = default; + + const impl::TimingDistributionMetric mTimingDist; +}; + +} // namespace mozilla::glean + +#endif /* mozilla_glean_GleanTimingDistribution_h */ diff --git a/toolkit/components/glean/bindings/private/Url.cpp b/toolkit/components/glean/bindings/private/Url.cpp new file mode 100644 index 0000000000..b86c724e17 --- /dev/null +++ b/toolkit/components/glean/bindings/private/Url.cpp @@ -0,0 +1,77 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/glean/bindings/Url.h" + +#include "Common.h" +#include "jsapi.h" +#include "js/String.h" +#include "nsString.h" +#include "mozilla/Components.h" +#include "mozilla/ResultVariant.h" +#include "mozilla/glean/bindings/ScalarGIFFTMap.h" +#include "mozilla/glean/fog_ffi_generated.h" +#include "nsIClassInfoImpl.h" + +namespace mozilla::glean { + +namespace impl { + +void UrlMetric::Set(const nsACString& aValue) const { + auto scalarId = ScalarIdForMetric(mId); + if (scalarId) { + Telemetry::ScalarSet(scalarId.extract(), NS_ConvertUTF8toUTF16(aValue)); + } + fog_url_set(mId, &aValue); +} + +Result<Maybe<nsCString>, nsCString> UrlMetric::TestGetValue( + const nsACString& aPingName) const { + nsCString err; + if (fog_url_test_get_error(mId, &err)) { + return Err(err); + } + if (!fog_url_test_has_value(mId, &aPingName)) { + return Maybe<nsCString>(); + } + nsCString ret; + fog_url_test_get_value(mId, &aPingName, &ret); + return Some(ret); +} + +} // namespace impl + +NS_IMPL_CLASSINFO(GleanUrl, nullptr, 0, {0}) +NS_IMPL_ISUPPORTS_CI(GleanUrl, nsIGleanUrl) + +NS_IMETHODIMP +GleanUrl::Set(const nsACString& aValue) { + mUrl.Set(aValue); + return NS_OK; +} + +NS_IMETHODIMP +GleanUrl::TestGetValue(const nsACString& aStorageName, JSContext* aCx, + JS::MutableHandle<JS::Value> aResult) { + auto result = mUrl.TestGetValue(aStorageName); + if (result.isErr()) { + aResult.set(JS::UndefinedValue()); + LogToBrowserConsole(nsIScriptError::errorFlag, + NS_ConvertUTF8toUTF16(result.unwrapErr())); + return NS_ERROR_LOSS_OF_SIGNIFICANT_DATA; + } + auto optresult = result.unwrap(); + if (optresult.isNothing()) { + aResult.set(JS::UndefinedValue()); + } else { + const NS_ConvertUTF8toUTF16 str(optresult.ref()); + aResult.set( + JS::StringValue(JS_NewUCStringCopyN(aCx, str.Data(), str.Length()))); + } + return NS_OK; +} + +} // namespace mozilla::glean diff --git a/toolkit/components/glean/bindings/private/Url.h b/toolkit/components/glean/bindings/private/Url.h new file mode 100644 index 0000000000..115eb073f5 --- /dev/null +++ b/toolkit/components/glean/bindings/private/Url.h @@ -0,0 +1,70 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_glean_GleanUrl_h +#define mozilla_glean_GleanUrl_h + +#include "mozilla/Maybe.h" +#include "mozilla/Result.h" +#include "nsIGleanMetrics.h" +#include "nsString.h" + +namespace mozilla::glean { + +namespace impl { + +class UrlMetric { + public: + constexpr explicit UrlMetric(uint32_t aId) : mId(aId) {} + + /* + * Set to the specified value. + * + * @param aValue The stringified Url to set the metric to. + */ + void Set(const nsACString& aValue) const; + + /** + * **Test-only API** + * + * Gets the currently stored value as a string. + * + * This function will attempt to await the last parent-process task (if any) + * writing to the the metric's storage engine before returning a value. + * This function will not wait for data from child processes. + * + * This doesn't clear the stored value. + * Parent process only. Panics in child processes. + * + * @param aPingName The (optional) name of the ping to retrieve the metric + * for. Defaults to the first value in `send_in_pings`. + * + * @return value of the stored metric, or Nothing() if there is no value. + */ + Result<Maybe<nsCString>, nsCString> TestGetValue( + const nsACString& aPingName = nsCString()) const; + + private: + const uint32_t mId; +}; +} // namespace impl + +class GleanUrl final : public nsIGleanUrl { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIGLEANURL + + explicit GleanUrl(uint32_t aId) : mUrl(aId){}; + + private: + virtual ~GleanUrl() = default; + + const impl::UrlMetric mUrl; +}; + +} // namespace mozilla::glean + +#endif /* mozilla_glean_GleanUrl_h */ diff --git a/toolkit/components/glean/bindings/private/Uuid.cpp b/toolkit/components/glean/bindings/private/Uuid.cpp new file mode 100644 index 0000000000..205dc94ec7 --- /dev/null +++ b/toolkit/components/glean/bindings/private/Uuid.cpp @@ -0,0 +1,89 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/glean/bindings/Uuid.h" + +#include "Common.h" +#include "jsapi.h" +#include "mozilla/Components.h" +#include "mozilla/ResultVariant.h" +#include "mozilla/glean/bindings/ScalarGIFFTMap.h" +#include "mozilla/glean/fog_ffi_generated.h" +#include "nsIClassInfoImpl.h" +#include "nsString.h" + +namespace mozilla::glean { + +namespace impl { + +void UuidMetric::Set(const nsACString& aValue) const { + auto scalarId = ScalarIdForMetric(mId); + if (scalarId) { + Telemetry::ScalarSet(scalarId.extract(), NS_ConvertUTF8toUTF16(aValue)); + } + fog_uuid_set(mId, &aValue); +} + +void UuidMetric::GenerateAndSet() const { + // We don't have the generated value to mirror to the scalar, + // so calling this function on a mirrored metric is likely an error. + (void)NS_WARN_IF(ScalarIdForMetric(mId).isSome()); + fog_uuid_generate_and_set(mId); +} + +Result<Maybe<nsCString>, nsCString> UuidMetric::TestGetValue( + const nsACString& aPingName) const { + nsCString err; + if (fog_uuid_test_get_error(mId, &err)) { + return Err(err); + } + if (!fog_uuid_test_has_value(mId, &aPingName)) { + return Maybe<nsCString>(); + } + nsCString ret; + fog_uuid_test_get_value(mId, &aPingName, &ret); + return Some(ret); +} + +} // namespace impl + +NS_IMPL_CLASSINFO(GleanUuid, nullptr, 0, {0}) +NS_IMPL_ISUPPORTS_CI(GleanUuid, nsIGleanUuid) + +NS_IMETHODIMP +GleanUuid::Set(const nsACString& aValue) { + mUuid.Set(aValue); + return NS_OK; +} + +NS_IMETHODIMP +GleanUuid::GenerateAndSet() { + mUuid.GenerateAndSet(); + return NS_OK; +} + +NS_IMETHODIMP +GleanUuid::TestGetValue(const nsACString& aStorageName, JSContext* aCx, + JS::MutableHandle<JS::Value> aResult) { + auto result = mUuid.TestGetValue(aStorageName); + if (result.isErr()) { + aResult.set(JS::UndefinedValue()); + LogToBrowserConsole(nsIScriptError::errorFlag, + NS_ConvertUTF8toUTF16(result.unwrapErr())); + return NS_ERROR_LOSS_OF_SIGNIFICANT_DATA; + } + auto optresult = result.unwrap(); + if (optresult.isNothing()) { + aResult.set(JS::UndefinedValue()); + } else { + const NS_ConvertUTF8toUTF16 str(optresult.value()); + aResult.set( + JS::StringValue(JS_NewUCStringCopyN(aCx, str.Data(), str.Length()))); + } + return NS_OK; +} + +} // namespace mozilla::glean diff --git a/toolkit/components/glean/bindings/private/Uuid.h b/toolkit/components/glean/bindings/private/Uuid.h new file mode 100644 index 0000000000..941ce42540 --- /dev/null +++ b/toolkit/components/glean/bindings/private/Uuid.h @@ -0,0 +1,75 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_glean_GleanUuid_h +#define mozilla_glean_GleanUuid_h + +#include "mozilla/Maybe.h" +#include "mozilla/Result.h" +#include "nsIGleanMetrics.h" +#include "nsString.h" + +namespace mozilla::glean { + +namespace impl { + +class UuidMetric { + public: + constexpr explicit UuidMetric(uint32_t aId) : mId(aId) {} + + /* + * Sets to the specified value. + * + * @param aValue The UUID to set the metric to. + */ + void Set(const nsACString& aValue) const; + + /* + * Generate a new random UUID and set the metric to it. + */ + void GenerateAndSet() const; + + /** + * **Test-only API** + * + * Gets the currently stored value as a hyphenated string. + * + * This function will attempt to await the last parent-process task (if any) + * writing to the the metric's storage engine before returning a value. + * This function will not wait for data from child processes. + * + * This doesn't clear the stored value. + * Parent process only. Panics in child processes. + * + * @param aPingName The (optional) name of the ping to retrieve the metric + * for. Defaults to the first value in `send_in_pings`. + * + * @return value of the stored metric, or Nothing() if there is no value. + */ + Result<Maybe<nsCString>, nsCString> TestGetValue( + const nsACString& aPingName = nsCString()) const; + + private: + const uint32_t mId; +}; +} // namespace impl + +class GleanUuid final : public nsIGleanUuid { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIGLEANUUID + + explicit GleanUuid(uint32_t aId) : mUuid(aId){}; + + private: + virtual ~GleanUuid() = default; + + const impl::UuidMetric mUuid; +}; + +} // namespace mozilla::glean + +#endif /* mozilla_glean_GleanUuid_h */ diff --git a/toolkit/components/glean/build_scripts/glean_parser_ext/cpp.py b/toolkit/components/glean/build_scripts/glean_parser_ext/cpp.py new file mode 100644 index 0000000000..f1ead48669 --- /dev/null +++ b/toolkit/components/glean/build_scripts/glean_parser_ext/cpp.py @@ -0,0 +1,136 @@ +# -*- coding: utf-8 -*- + +# 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 http://mozilla.org/MPL/2.0/. + +""" +Outputter to generate C++ code for metrics. +""" + +import json + +import jinja2 +from glean_parser import util +from util import generate_metric_ids, generate_ping_ids, get_metrics + + +def cpp_datatypes_filter(value): + """ + A Jinja2 filter that renders C++ literals. + + Based on Python's JSONEncoder, but overrides: + - lists to array literals {} + - strings to "value" + """ + + class CppEncoder(json.JSONEncoder): + def iterencode(self, value): + if isinstance(value, list): + yield "{" + first = True + for subvalue in list(value): + if not first: + yield ", " + yield from self.iterencode(subvalue) + first = False + yield "}" + elif isinstance(value, str): + yield '"' + value + '"' + else: + yield from super().iterencode(value) + + return "".join(CppEncoder().iterencode(value)) + + +def type_name(obj): + """ + Returns the C++ type to use for a given metric object. + """ + + if getattr(obj, "labeled", False): + class_name = util.Camelize(obj.type[8:]) # strips "labeled_" off the front. + return "Labeled<impl::{}Metric>".format(class_name) + generate_enums = getattr(obj, "_generate_enums", []) # Extra Keys? Reasons? + if len(generate_enums): + for name, suffix in generate_enums: + if not len(getattr(obj, name)) and suffix == "Keys": + return util.Camelize(obj.type) + "Metric<NoExtraKeys>" + else: + # we always use the `extra` suffix, + # because we only expose the new event API + suffix = "Extra" + return "{}Metric<{}>".format( + util.Camelize(obj.type), util.Camelize(obj.name) + suffix + ) + return util.Camelize(obj.type) + "Metric" + + +def extra_type_name(typ: str) -> str: + """ + Returns the corresponding Rust type for event's extra key types. + """ + + if typ == "boolean": + return "bool" + elif typ == "string": + return "nsCString" + elif typ == "quantity": + return "uint32_t" + else: + return "UNSUPPORTED" + + +def output_cpp(objs, output_fd, options={}): + """ + Given a tree of objects, output C++ code to the file-like object `output_fd`. + + :param objs: A tree of objects (metrics and pings) as returned from + `parser.parse_objects`. + :param output_fd: Writeable file to write the output to. + :param options: options dictionary. + """ + + # Monkeypatch a util.snake_case function for the templates to use + util.snake_case = lambda value: value.replace(".", "_").replace("-", "_") + # Monkeypatch util.get_jinja2_template to find templates nearby + + def get_local_template(template_name, filters=()): + env = jinja2.Environment( + loader=jinja2.PackageLoader("cpp", "templates"), + trim_blocks=True, + lstrip_blocks=True, + ) + env.filters["camelize"] = util.camelize + env.filters["Camelize"] = util.Camelize + for filter_name, filter_func in filters: + env.filters[filter_name] = filter_func + return env.get_template(template_name) + + util.get_jinja2_template = get_local_template + get_metric_id = generate_metric_ids(objs) + get_ping_id = generate_ping_ids(objs) + + if "pings" in objs: + template_filename = "cpp_pings.jinja2" + if objs.get("tags"): + del objs["tags"] + else: + template_filename = "cpp.jinja2" + objs = get_metrics(objs) + + template = util.get_jinja2_template( + template_filename, + filters=( + ("cpp", cpp_datatypes_filter), + ("snake_case", util.snake_case), + ("type_name", type_name), + ("extra_type_name", extra_type_name), + ("metric_id", get_metric_id), + ("ping_id", get_ping_id), + ("Camelize", util.Camelize), + ), + ) + + output_fd.write(template.render(all_objs=objs)) + output_fd.write("\n") diff --git a/toolkit/components/glean/build_scripts/glean_parser_ext/jog.py b/toolkit/components/glean/build_scripts/glean_parser_ext/jog.py new file mode 100644 index 0000000000..b869ba3d14 --- /dev/null +++ b/toolkit/components/glean/build_scripts/glean_parser_ext/jog.py @@ -0,0 +1,201 @@ +# -*- coding: utf-8 -*- + +# 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 http://mozilla.org/MPL/2.0/. + +""" +Outputter to generate Rust code for metrics. +""" + +import enum +import json +import sys + +import jinja2 +from glean_parser import util +from glean_parser.metrics import Rate +from util import type_ids_and_categories + +from js import ID_BITS, PING_INDEX_BITS + +# The list of all args to CommonMetricData. +# No particular order is required, but I have these in common_metric_data.rs +# order just to be organized. +# Note that this is util.common_metric_args + "dynamic_label" +common_metric_data_args = [ + "name", + "category", + "send_in_pings", + "lifetime", + "disabled", + "dynamic_label", +] + +# List of all metric-type-specific args that JOG understands. +known_extra_args = [ + "time_unit", + "memory_unit", + "allowed_extra_keys", + "reason_codes", + "range_min", + "range_max", + "bucket_count", + "histogram_type", + "numerators", +] + +# List of all ping-specific args that JOG undertsands. +known_ping_args = [ + "name", + "include_client_id", + "send_if_empty", + "reason_codes", +] + + +def ensure_jog_support_for_args(): + """ + glean_parser or the Glean SDK might add new metric/ping args. + To ensure JOG doesn't fall behind in support, + we check the list of JOG-supported args vs glean_parser's. + We fail the build if glean_parser has one or more we haven't seen before. + """ + + unknown_args = set(util.extra_metric_args) - set(known_extra_args) + + unknown_args |= set(util.ping_args) - set(known_ping_args) + + if len(unknown_args): + print(f"Unknown glean_parser args {unknown_args}") + print("JOG must be updated to support the new args") + sys.exit(1) + + +def load_monkeypatches(): + """ + Monkeypatch jinja template loading because we're not glean_parser. + We're glean_parser_ext. + """ + # Monkeypatch util.get_jinja2_template to find templates nearby + def get_local_template(template_name, filters=()): + env = jinja2.Environment( + loader=jinja2.PackageLoader("rust", "templates"), + trim_blocks=True, + lstrip_blocks=True, + ) + env.filters["camelize"] = util.camelize + env.filters["Camelize"] = util.Camelize + for filter_name, filter_func in filters: + env.filters[filter_name] = filter_func + return env.get_template(template_name) + + util.get_jinja2_template = get_local_template + + +def output_factory(objs, output_fd, options={}): + """ + Given a tree of objects, output Rust code to the file-like object `output_fd`. + Specifically, Rust code that can generate Rust metrics instances. + + :param objs: A tree of objects (metrics and pings) as returned from + `parser.parse_objects`. + :param output_fd: Writeable file to write the output to. + :param options: options dictionary, presently unused. + """ + + ensure_jog_support_for_args() + load_monkeypatches() + + # Get the metric type ids. Must be the same ids generated in js.py + metric_types, categories = type_ids_and_categories(objs) + + template = util.get_jinja2_template( + "jog_factory.jinja2", + filters=(("snake_case", util.snake_case),), + ) + + output_fd.write( + template.render( + all_objs=objs, + common_metric_data_args=common_metric_data_args, + extra_args=util.extra_args, + metric_types=metric_types, + runtime_metric_bit=ID_BITS - 1, + runtime_ping_bit=PING_INDEX_BITS - 1, + ID_BITS=ID_BITS, + ) + ) + output_fd.write("\n") + + +def camel_to_snake(s): + assert "_" not in s, "JOG doesn't encode metric typenames with underscores" + return "".join(["_" + c.lower() if c.isupper() else c for c in s]).lstrip("_") + + +def output_file(objs, output_fd, options={}): + """ + Given a tree of objects, output them to the file-like object `output_fd`. + Specifically, in a format that describes all the metrics and pings defined in objs. + + :param objs: A tree of objects (metrics and pings) as returned from + `parser.parse_objects`. + Presently a dictionary with keys of literals "pings" and "tags" + as well as one key per metric category mapped to lists of + pings, tags, and metrics (respecitvely) + :param output_fd: Writeable file to write the output to. + :param options: options dictionary, presently unused. + """ + + ensure_jog_support_for_args() + + jog_data = {"pings": [], "metrics": {}} + + if "tags" in objs: + del objs["tags"] # JOG has no use for tags. + + pings = objs["pings"] + del objs["pings"] + for ping in pings.values(): + ping_arg_list = [] + for arg in known_ping_args: + if hasattr(ping, arg): + ping_arg_list.append(getattr(ping, arg)) + jog_data["pings"].append(ping_arg_list) + + def encode(value): + if isinstance(value, enum.Enum): + return value.name + if isinstance(value, Rate): # `numerators` for an external Denominator metric + args = [] + for arg_name in common_metric_data_args[:-1]: + args.append(getattr(value, arg_name)) + + # These are deserialized as CommonMetricData. + # CMD have a final param JOG never uses: `dynamic_label` + # It's optional, so we should be able to omit it, but we'd need to + # annotate it with #[serde(default)]... so here we add the sixth + # param as None. + args.append(None) + return args + return json.dumps(value) + + for category, metrics in objs.items(): + dict_cat = jog_data["metrics"].setdefault(category, []) + for metric in metrics.values(): + metric_arg_list = [camel_to_snake(metric.__class__.__name__)] + for arg in common_metric_data_args[:-1]: + if arg in ["category"]: + continue # We don't include the category in each metric. + metric_arg_list.append(getattr(metric, arg)) + extra = {} + for arg in known_extra_args: + if hasattr(metric, arg): + extra[arg] = getattr(metric, arg) + if len(extra): + metric_arg_list.append(extra) + dict_cat.append(metric_arg_list) + + # TODO: Measure the speed gain of removing `indent=2` + json.dump(jog_data, output_fd, sort_keys=True, default=encode, indent=2) diff --git a/toolkit/components/glean/build_scripts/glean_parser_ext/js.py b/toolkit/components/glean/build_scripts/glean_parser_ext/js.py new file mode 100644 index 0000000000..cafb179ae5 --- /dev/null +++ b/toolkit/components/glean/build_scripts/glean_parser_ext/js.py @@ -0,0 +1,272 @@ +# -*- coding: utf-8 -*- + +# 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 http://mozilla.org/MPL/2.0/. + +""" +Outputter to generate C++ code for the JavaScript API for metrics. + +The code for the JavaScript API is a bit special in that we only generate C++ code, +string tables and mapping functions. +The rest is handled by the WebIDL and XPIDL implementation +that uses this code to look up metrics by name. +""" + +import jinja2 +from glean_parser import util +from perfecthash import PerfectHash +from string_table import StringTable +from util import generate_metric_ids, generate_ping_ids, get_metrics + +""" +We need to store several bits of information in the Perfect Hash Map Entry: + +1. An index into the string table to check for string equality with a search key + The perfect hash function will give false-positive for non-existent keys, + so we need to verify these ourselves. +2. Type information to instantiate the correct C++ class +3. The metric's actual ID to lookup the underlying instance. +4. Whether the metric is a "submetric" (generated per-label for labeled_* metrics) +5. Whether the metric was registered at runtime + +We have 64 bits to play with, so we dedicate: + +1. 32 bit to the string table offset. More than enough for a large string table (~60M metrics). +2. 5 bit for the type. That allows for 32 metric types. We're not even close to that yet. +3. 25 bit for the metric ID. That allows for 33.5 million metrics. Let's not go there. +4. 1 bit for signifying that this metric is a submetric +5. 1 bit for signifying that this metric was registered at runtime + +These values are interpolated into the template as well, so changing them here +ensures the generated C++ code follows. +If we ever need more bits for a part (e.g. when we add the 33rd metric type), +we figure out if either the string table indices or the range of possible IDs can be reduced +and adjust the constants below. +""" +ENTRY_WIDTH = 64 +INDEX_BITS = 32 +ID_BITS = 27 # Includes ID_SIGNAL_BITS +ID_SIGNAL_BITS = 2 +TYPE_BITS = 5 + +PING_INDEX_BITS = 16 + + +def ping_entry(ping_id, ping_string_index): + """ + The 2 pieces of information of a ping encoded into a single 32-bit integer. + """ + assert ping_id < 2 ** (32 - PING_INDEX_BITS) + assert ping_string_index < 2 ** PING_INDEX_BITS + return ping_id << PING_INDEX_BITS | ping_string_index + + +def create_entry(metric_id, type_id, idx): + """ + The 3 pieces of information of a metric encoded into a single 64-bit integer. + """ + return metric_id << INDEX_BITS | type_id << (INDEX_BITS + ID_BITS) | idx + + +def metric_identifier(category, metric_name): + """ + The metric's unique identifier, including the category and name + """ + return f"{category}.{util.camelize(metric_name)}" + + +def type_name(obj): + """ + Returns the C++ type to use for a given metric object. + """ + + if getattr(obj, "labeled", False): + return "GleanLabeled" + return "Glean" + util.Camelize(obj.type) + + +def subtype_name(obj): + """ + Returns the subtype name for labeled metrics. + (e.g. 'boolean' for 'labeled_boolean'). + Returns "" for non-labeled metrics. + """ + if getattr(obj, "labeled", False): + type = obj.type[8:] # strips "labeled_" off the front + return "Glean" + util.Camelize(type) + return "" + + +def output_js(objs, output_fd, options={}): + """ + Given a tree of objects, output code for the JS API to the file-like object `output_fd`. + + :param objs: A tree of objects (metrics and pings) as returned from + `parser.parse_objects`. + :param output_fd: Writeable file to write the output to. + :param options: options dictionary. + """ + + # Monkeypatch util.get_jinja2_template to find templates nearby + + def get_local_template(template_name, filters=()): + env = jinja2.Environment( + loader=jinja2.PackageLoader("js", "templates"), + trim_blocks=True, + lstrip_blocks=True, + ) + env.filters["Camelize"] = util.Camelize + for filter_name, filter_func in filters: + env.filters[filter_name] = filter_func + return env.get_template(template_name) + + util.get_jinja2_template = get_local_template + + if "pings" in objs: + write_pings({"pings": objs["pings"]}, output_fd, "js_pings.jinja2") + else: + write_metrics(get_metrics(objs), output_fd, "js.jinja2") + + +def write_metrics(objs, output_fd, template_filename): + """ + Given a tree of objects `objs`, output metrics-only code for the JS API to the + file-like object `output_fd` using template `template_filename` + """ + + template = util.get_jinja2_template( + template_filename, + ) + + assert ( + INDEX_BITS + TYPE_BITS + ID_BITS <= ENTRY_WIDTH + ), "INDEX_BITS, TYPE_BITS, or ID_BITS are larger than allowed" + + get_metric_id = generate_metric_ids(objs) + # Mapping from a metric's identifier to the entry (metric ID | type id | index) + metric_id_mapping = {} + categories = [] + + category_string_table = StringTable() + metric_string_table = StringTable() + # Mapping from a type name to its ID + metric_type_ids = {} + + for category_name, objs in get_metrics(objs).items(): + category_name = util.camelize(category_name) + id = category_string_table.stringIndex(category_name) + categories.append((category_name, id)) + + for metric in objs.values(): + identifier = metric_identifier(category_name, metric.name) + metric_type_tuple = (type_name(metric), subtype_name(metric)) + if metric_type_tuple in metric_type_ids: + type_id, _ = metric_type_ids[metric_type_tuple] + else: + type_id = len(metric_type_ids) + 1 + metric_type_ids[metric_type_tuple] = (type_id, metric.type) + + idx = metric_string_table.stringIndex(identifier) + metric_id = get_metric_id(metric) + entry = create_entry(metric_id, type_id, idx) + metric_id_mapping[identifier] = entry + + # Create a lookup table for the metric categories only + category_string_table = category_string_table.writeToString("gCategoryStringTable") + category_map = [(bytearray(category, "ascii"), id) for (category, id) in categories] + name_phf = PerfectHash(category_map, 64) + category_by_name_lookup = name_phf.cxx_codegen( + name="CategoryByNameLookup", + entry_type="category_entry_t", + lower_entry=lambda x: str(x[1]) + "ul", + key_type="const nsACString&", + key_bytes="aKey.BeginReading()", + key_length="aKey.Length()", + return_type="static Maybe<uint32_t>", + return_entry="return category_result_check(aKey, entry);", + ) + + # Create a lookup table for metric's identifiers. + metric_string_table = metric_string_table.writeToString("gMetricStringTable") + metric_map = [ + (bytearray(metric_name, "ascii"), metric_id) + for (metric_name, metric_id) in metric_id_mapping.items() + ] + metric_phf = PerfectHash(metric_map, 64) + metric_by_name_lookup = metric_phf.cxx_codegen( + name="MetricByNameLookup", + entry_type="metric_entry_t", + lower_entry=lambda x: str(x[1]) + "ull", + key_type="const nsACString&", + key_bytes="aKey.BeginReading()", + key_length="aKey.Length()", + return_type="static Maybe<uint32_t>", + return_entry="return metric_result_check(aKey, entry);", + ) + + output_fd.write( + template.render( + categories=categories, + metric_id_mapping=metric_id_mapping, + metric_type_ids=metric_type_ids, + entry_width=ENTRY_WIDTH, + index_bits=INDEX_BITS, + id_bits=ID_BITS, + type_bits=TYPE_BITS, + id_signal_bits=ID_SIGNAL_BITS, + category_string_table=category_string_table, + category_by_name_lookup=category_by_name_lookup, + metric_string_table=metric_string_table, + metric_by_name_lookup=metric_by_name_lookup, + ) + ) + output_fd.write("\n") + + +def write_pings(objs, output_fd, template_filename): + """ + Given a tree of objects `objs`, output pings-only code for the JS API to the + file-like object `output_fd` using template `template_filename` + """ + + template = util.get_jinja2_template( + template_filename, + filters=(), + ) + + ping_string_table = StringTable() + get_ping_id = generate_ping_ids(objs) + # The map of a ping's name to its entry (a combination of a monotonic + # integer and its index in the string table) + pings = {} + for ping_name in objs["pings"].keys(): + ping_id = get_ping_id(ping_name) + ping_name = util.camelize(ping_name) + pings[ping_name] = ping_entry(ping_id, ping_string_table.stringIndex(ping_name)) + + ping_map = [ + (bytearray(ping_name, "ascii"), ping_entry) + for (ping_name, ping_entry) in pings.items() + ] + ping_string_table = ping_string_table.writeToString("gPingStringTable") + ping_phf = PerfectHash(ping_map, 64) + ping_by_name_lookup = ping_phf.cxx_codegen( + name="PingByNameLookup", + entry_type="ping_entry_t", + lower_entry=lambda x: str(x[1]), + key_type="const nsACString&", + key_bytes="aKey.BeginReading()", + key_length="aKey.Length()", + return_type="static Maybe<uint32_t>", + return_entry="return ping_result_check(aKey, entry);", + ) + + output_fd.write( + template.render( + ping_index_bits=PING_INDEX_BITS, + ping_by_name_lookup=ping_by_name_lookup, + ping_string_table=ping_string_table, + ) + ) + output_fd.write("\n") diff --git a/toolkit/components/glean/build_scripts/glean_parser_ext/run_glean_parser.py b/toolkit/components/glean/build_scripts/glean_parser_ext/run_glean_parser.py new file mode 100644 index 0000000000..5d70f204e2 --- /dev/null +++ b/toolkit/components/glean/build_scripts/glean_parser_ext/run_glean_parser.py @@ -0,0 +1,212 @@ +# -*- coding: utf-8 -*- + +# 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 http://mozilla.org/MPL/2.0/. + +import os +import sys +from pathlib import Path + +import cpp +import jinja2 +import jog +import rust +from glean_parser import lint, parser, translate, util +from mozbuild.util import FileAvoidWrite +from util import generate_metric_ids + +import js + + +class ParserError(Exception): + """Thrown from parse if something goes wrong""" + + pass + + +GIFFT_TYPES = { + "Event": ["event"], + "Histogram": ["timing_distribution", "memory_distribution", "custom_distribution"], + "Scalar": [ + "boolean", + "labeled_boolean", + "counter", + "labeled_counter", + "string", + "string_list", + "timespan", + "uuid", + "datetime", + "quantity", + "rate", + "url", + ], +} + + +def get_parser_options(moz_app_version): + app_version_major = moz_app_version.split(".", 1)[0] + return { + "allow_reserved": False, + "expire_by_version": int(app_version_major), + } + + +def parse(args): + """ + Parse and lint the input files, + then return the parsed objects for further processing. + """ + + # Unfortunately, GeneratedFile appends `flags` directly after `inputs` + # instead of listifying either, so we need to pull stuff from a *args. + yaml_array = args[:-1] + moz_app_version = args[-1] + + input_files = [Path(x) for x in yaml_array] + + options = get_parser_options(moz_app_version) + + return parse_with_options(input_files, options) + + +def parse_with_options(input_files, options): + # Derived heavily from glean_parser.translate.translate. + # Adapted to how mozbuild sends us a fd, and to expire on versions not dates. + + # Lint the yaml first, then lint the metrics. + if lint.lint_yaml_files(input_files, parser_config=options): + # Warnings are Errors + raise ParserError("linter found problems") + + all_objs = parser.parse_objects(input_files, options) + if util.report_validation_errors(all_objs): + raise ParserError("found validation errors during parse") + + nits = lint.lint_metrics(all_objs.value, options) + if nits is not None and any(nit.check_name != "EXPIRED" for nit in nits): + # Treat Warnings as Errors in FOG. + # But don't fail the whole build on expired metrics (it blocks testing). + raise ParserError("glinter nits found during parse") + + objects = all_objs.value + + translate.transform_metrics(objects) + + return objects, options + + +# Must be kept in sync with the length of `deps` in moz.build. +DEPS_LEN = 17 + + +def main(cpp_fd, *args): + def open_output(filename): + return FileAvoidWrite(os.path.join(os.path.dirname(cpp_fd.name), filename)) + + [js_h_path, rust_path] = args[-2:] + args = args[DEPS_LEN:-2] + all_objs, options = parse(args) + + cpp.output_cpp(all_objs, cpp_fd, options) + + with open_output(js_h_path) as js_fd: + js.output_js(all_objs, js_fd, options) + + with open_output(rust_path) as rust_fd: + rust.output_rust(all_objs, rust_fd, options) + + +def gifft_map(output_fd, *args): + probe_type = args[-1] + args = args[DEPS_LEN:-1] + all_objs, options = parse(args) + + # Events also need to output maps from event extra enum to strings. + # Sadly we need to generate code for all possible events, not just mirrored. + # Otherwise we won't compile. + if probe_type == "Event": + output_path = Path(os.path.dirname(output_fd.name)) + with FileAvoidWrite(output_path / "EventExtraGIFFTMaps.cpp") as cpp_fd: + output_gifft_map(output_fd, probe_type, all_objs, cpp_fd) + else: + output_gifft_map(output_fd, probe_type, all_objs, None) + + +def output_gifft_map(output_fd, probe_type, all_objs, cpp_fd): + get_metric_id = generate_metric_ids(all_objs) + ids_to_probes = {} + for category_name, objs in all_objs.items(): + for metric in objs.values(): + if ( + hasattr(metric, "telemetry_mirror") + and metric.telemetry_mirror is not None + ): + info = (metric.telemetry_mirror, f"{category_name}.{metric.name}") + if metric.type in GIFFT_TYPES[probe_type]: + if any( + metric.telemetry_mirror == value[0] + for value in ids_to_probes.values() + ): + print( + f"Telemetry mirror {metric.telemetry_mirror} already registered", + file=sys.stderr, + ) + sys.exit(1) + ids_to_probes[get_metric_id(metric)] = info + # If we don't support a mirror for this metric type: build error. + elif not any( + [ + metric.type in types_for_probe + for types_for_probe in GIFFT_TYPES.values() + ] + ): + print( + f"Glean metric {category_name}.{metric.name} is of type {metric.type}" + " which can't be mirrored (we don't know how).", + file=sys.stderr, + ) + sys.exit(1) + + env = jinja2.Environment( + loader=jinja2.PackageLoader("run_glean_parser", "templates"), + trim_blocks=True, + lstrip_blocks=True, + ) + env.filters["snake_case"] = lambda value: value.replace(".", "_").replace("-", "_") + env.filters["Camelize"] = util.Camelize + template = env.get_template("gifft.jinja2") + output_fd.write( + template.render( + ids_to_probes=ids_to_probes, + probe_type=probe_type, + id_bits=js.ID_BITS, + id_signal_bits=js.ID_SIGNAL_BITS, + ) + ) + output_fd.write("\n") + + # Events also need to output maps from event extra enum to strings. + # Sadly we need to generate code for all possible events, not just mirrored. + # Otherwise we won't compile. + if probe_type == "Event": + template = env.get_template("gifft_events.jinja2") + cpp_fd.write(template.render(all_objs=all_objs)) + cpp_fd.write("\n") + + +def jog_factory(output_fd, *args): + args = args[DEPS_LEN:] + all_objs, options = parse(args) + jog.output_factory(all_objs, output_fd, options) + + +def jog_file(output_fd, *args): + args = args[DEPS_LEN:] + all_objs, options = parse(args) + jog.output_file(all_objs, output_fd, options) + + +if __name__ == "__main__": + main(sys.stdout, *sys.argv[1:]) diff --git a/toolkit/components/glean/build_scripts/glean_parser_ext/rust.py b/toolkit/components/glean/build_scripts/glean_parser_ext/rust.py new file mode 100644 index 0000000000..a7a38ba68b --- /dev/null +++ b/toolkit/components/glean/build_scripts/glean_parser_ext/rust.py @@ -0,0 +1,272 @@ +# -*- coding: utf-8 -*- + +# 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 http://mozilla.org/MPL/2.0/. + +""" +Outputter to generate Rust code for metrics. +""" + +import enum +import json + +import jinja2 +from glean_parser import util +from glean_parser.metrics import Rate +from util import generate_metric_ids, generate_ping_ids, get_metrics + +from js import ID_BITS, ID_SIGNAL_BITS + +# The list of all args to CommonMetricData. +# No particular order is required, but I have these in common_metric_data.rs +# order just to be organized. +common_metric_data_args = [ + "name", + "category", + "send_in_pings", + "lifetime", + "disabled", + "dynamic_label", +] + + +def rust_datatypes_filter(value): + """ + A Jinja2 filter that renders Rust literals. + + Based on Python's JSONEncoder, but overrides: + - dicts and sets to raise an error + - sets to vec![] (used in labels) + - enums to become Class::Value + - lists to vec![] (used in send_in_pings) + - null to None + - strings to "value".into() + - Rate objects to a CommonMetricData initializer + (for external Denominators' Numerators lists) + """ + + class RustEncoder(json.JSONEncoder): + def iterencode(self, value): + if isinstance(value, dict): + raise ValueError("RustEncoder doesn't know dicts {}".format(str(value))) + elif isinstance(value, enum.Enum): + yield (value.__class__.__name__ + "::" + util.Camelize(value.name)) + elif isinstance(value, set): + yield from self.iterencode(sorted(list(value))) + elif isinstance(value, list): + if len(value) > 8 and all(isinstance(v, str) for v in value): + # For large enough sets and lists of strings, we use a single string + # with an array of lengths and convert to a Vec at runtime. This yields + # smaller code, data, and relocations than using vec![]. + yield "{" + yield f"""const S: &'static str = "{"".join(value)}";""" + lengths = [len(v) for v in value] + largest = max(lengths) + # Use a type adequate for the largest string. + # In most cases, this will be u8. + len_type = f"u{((largest.bit_length() + 7) // 8) * 8}" + yield f"const LENGTHS: [{len_type}; {len(lengths)}] = {lengths};" + yield "let mut offset = 0;" + yield "LENGTHS.iter().map(|len| {" + yield " let start = offset;" + yield " offset += *len as usize;" + yield " S[start..offset].into()" + yield "}).collect()" + yield "}" + else: + yield "vec![" + first = True + for subvalue in list(value): + if not first: + yield ", " + yield from self.iterencode(subvalue) + first = False + yield "]" + elif value is None: + yield "None" + elif isinstance(value, str): + yield '"' + value + '".into()' + elif isinstance(value, Rate): + yield "CommonMetricData {" + for arg_name in common_metric_data_args: + if hasattr(value, arg_name): + yield f"{arg_name}: " + yield from self.iterencode(getattr(value, arg_name)) + yield ", " + yield " ..Default::default()}" + else: + yield from super().iterencode(value) + + return "".join(RustEncoder().iterencode(value)) + + +def ctor(obj): + """ + Returns the scope and name of the constructor to use for a metric object. + Necessary because LabeledMetric<T> is constructed using LabeledMetric::new + not LabeledMetric<T>::new + """ + if getattr(obj, "labeled", False): + return "LabeledMetric::new" + return class_name(obj.type) + "::new" + + +def type_name(obj): + """ + Returns the Rust type to use for a given metric or ping object. + """ + + if getattr(obj, "labeled", False): + return "LabeledMetric<Labeled{}>".format(class_name(obj.type)) + generate_enums = getattr(obj, "_generate_enums", []) # Extra Keys? Reasons? + if len(generate_enums): + for name, suffix in generate_enums: + if not len(getattr(obj, name)) and suffix == "Keys": + return class_name(obj.type) + "<NoExtraKeys>" + else: + # we always use the `extra` suffix, + # because we only expose the new event API + suffix = "Extra" + return "{}<{}>".format( + class_name(obj.type), util.Camelize(obj.name) + suffix + ) + return class_name(obj.type) + + +def extra_type_name(typ: str) -> str: + """ + Returns the corresponding Rust type for event's extra key types. + """ + + if typ == "boolean": + return "bool" + elif typ == "string": + return "String" + elif typ == "quantity": + return "u32" + else: + return "UNSUPPORTED" + + +def class_name(obj_type): + """ + Returns the Rust class name for a given metric or ping type. + """ + if obj_type == "ping": + return "Ping" + if obj_type.startswith("labeled_"): + obj_type = obj_type[8:] + return util.Camelize(obj_type) + "Metric" + + +def extra_keys(allowed_extra_keys): + """ + Returns the &'static [&'static str] ALLOWED_EXTRA_KEYS for impl ExtraKeys + """ + return "&[" + ", ".join(map(lambda key: '"' + key + '"', allowed_extra_keys)) + "]" + + +def output_rust(objs, output_fd, options={}): + """ + Given a tree of objects, output Rust code to the file-like object `output_fd`. + + :param objs: A tree of objects (metrics and pings) as returned from + `parser.parse_objects`. + :param output_fd: Writeable file to write the output to. + :param options: options dictionary, presently unused. + """ + + # Monkeypatch a util.snake_case function for the templates to use + util.snake_case = lambda value: value.replace(".", "_").replace("-", "_") + # Monkeypatch util.get_jinja2_template to find templates nearby + + def get_local_template(template_name, filters=()): + env = jinja2.Environment( + loader=jinja2.PackageLoader("rust", "templates"), + trim_blocks=True, + lstrip_blocks=True, + ) + env.filters["camelize"] = util.camelize + env.filters["Camelize"] = util.Camelize + for filter_name, filter_func in filters: + env.filters[filter_name] = filter_func + return env.get_template(template_name) + + util.get_jinja2_template = get_local_template + get_metric_id = generate_metric_ids(objs) + get_ping_id = generate_ping_ids(objs) + + # Map from a tuple (const, typ) to an array of tuples (id, path) + # where: + # const: The Rust constant name to be used for the lookup map + # typ: The metric type to be stored in the lookup map + # id: The numeric metric ID + # path: The fully qualified path to the metric object in Rust + # + # This map is only filled for metrics, not for pings. + # + # Example: + # + # ("COUNTERS", "CounterMetric") -> [(1, "test_only::clicks"), ...] + objs_by_type = {} + + # Map from a metric ID to the fully qualified path of the event object in Rust. + # Required for the special handling of event lookups. + # + # Example: + # + # 17 -> "test_only::an_event" + events_by_id = {} + + if "pings" in objs: + template_filename = "rust_pings.jinja2" + objs = {"pings": objs["pings"]} + else: + template_filename = "rust.jinja2" + objs = get_metrics(objs) + for category_name, category_value in objs.items(): + for metric in category_value.values(): + # The constant is all uppercase and suffixed by `_MAP` + const_name = util.snake_case(metric.type).upper() + "_MAP" + typ = type_name(metric) + key = (const_name, typ) + + metric_name = util.snake_case(metric.name) + category_name = util.snake_case(category_name) + full_path = f"{category_name}::{metric_name}" + + if metric.type == "event": + events_by_id[get_metric_id(metric)] = full_path + continue + + if key not in objs_by_type: + objs_by_type[key] = [] + objs_by_type[key].append((get_metric_id(metric), full_path)) + + # Now for the modules for each category. + template = util.get_jinja2_template( + template_filename, + filters=( + ("rust", rust_datatypes_filter), + ("snake_case", util.snake_case), + ("type_name", type_name), + ("extra_type_name", extra_type_name), + ("ctor", ctor), + ("extra_keys", extra_keys), + ("metric_id", get_metric_id), + ("ping_id", get_ping_id), + ), + ) + + output_fd.write( + template.render( + all_objs=objs, + common_metric_data_args=common_metric_data_args, + metric_by_type=objs_by_type, + extra_args=util.extra_args, + events_by_id=events_by_id, + submetric_bit=ID_BITS - ID_SIGNAL_BITS, + ) + ) + output_fd.write("\n") diff --git a/toolkit/components/glean/build_scripts/glean_parser_ext/string_table.py b/toolkit/components/glean/build_scripts/glean_parser_ext/string_table.py new file mode 100644 index 0000000000..1958eef604 --- /dev/null +++ b/toolkit/components/glean/build_scripts/glean_parser_ext/string_table.py @@ -0,0 +1,97 @@ +# -*- coding: utf-8 -*- + +# 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 http://mozilla.org/MPL/2.0/. + +from io import StringIO + + +class StringTable: + """Manages a string table and allows C style serialization to a file.""" + + def __init__(self): + self.current_index = 0 + self.table = {} + + def c_strlen(self, string): + """The length of a string including the null terminating character. + :param string: the input string. + """ + return len(string) + 1 + + def stringIndex(self, string): + """Returns the index in the table of the provided string. Adds the string to + the table if it's not there. + :param string: the input string. + """ + if string in self.table: + return self.table[string] + result = self.current_index + self.table[string] = result + self.current_index += self.c_strlen(string) + return result + + def stringIndexes(self, strings): + """Returns a list of indexes for the provided list of strings. + Adds the strings to the table if they are not in it yet. + :param strings: list of strings to put into the table. + """ + return [self.stringIndex(s) for s in strings] + + def writeToString(self, name): + """Writes the string table to a string as a C const char array. + + See `writeDefinition` for details + + :param name: the name of the output array. + """ + + output = StringIO() + self.writeDefinition(output, name) + return output.getvalue() + + def writeDefinition(self, f, name): + """Writes the string table to a file as a C const char array. + + This writes out the string table as one single C char array for memory + size reasons, separating the individual strings with '\0' characters. + This way we can index directly into the string array and avoid the additional + storage costs for the pointers to them (and potential extra relocations for those). + + :param f: the output stream. + :param name: the name of the output array. + """ + entries = self.table.items() + + # Avoid null-in-string warnings with GCC and potentially + # overlong string constants; write everything out the long way. + def explodeToCharArray(string): + def toCChar(s): + if s == "'": + return "'\\''" + return "'%s'" % s + + return ", ".join(map(toCChar, string)) + + f.write("#if defined(_MSC_VER) && !defined(__clang__)\n") + f.write("const char %s[] = {\n" % name) + f.write("#else\n") + f.write("constexpr char %s[] = {\n" % name) + f.write("#endif\n") + for (string, offset) in sorted(entries, key=lambda x: x[1]): + if "*/" in string: + raise ValueError( + "String in string table contains unexpected sequence '*/': %s" + % string + ) + + e = explodeToCharArray(string) + if e: + f.write( + " /* %5d - \"%s\" */ %s, '\\0',\n" + % (offset, string, explodeToCharArray(string)) + ) + else: + f.write(" /* %5d - \"%s\" */ '\\0',\n" % (offset, string)) + f.write("};\n\n") diff --git a/toolkit/components/glean/build_scripts/glean_parser_ext/templates/cpp.jinja2 b/toolkit/components/glean/build_scripts/glean_parser_ext/templates/cpp.jinja2 new file mode 100644 index 0000000000..e9fca2a10c --- /dev/null +++ b/toolkit/components/glean/build_scripts/glean_parser_ext/templates/cpp.jinja2 @@ -0,0 +1,86 @@ +// -*- mode: C++ -*- + +// AUTOGENERATED BY glean_parser. DO NOT EDIT. +{# The rendered source is autogenerated, but this +Jinja2 template is not. Please file bugs! #} + +/* 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 http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_Metrics_h +#define mozilla_Metrics_h + +#include "mozilla/glean/bindings/MetricTypes.h" +#include "mozilla/Tuple.h" +#include "mozilla/Maybe.h" +#include "nsTArray.h" +#include "nsPrintfCString.h" + +namespace mozilla::glean { + +{%- macro generate_extra_keys(obj) -%} +{% for name, suffix in obj["_generate_enums"] %} +{# we always use the `extra` suffix, because we only expose the new event API #} +{% set suffix = "Extra" %} +{% if obj|attr(name)|length %} + {% if obj.has_extra_types %} + {{ extra_keys_with_types(obj, name, suffix)|indent }} + {% else %} +#error "Untyped event extras not supported. Please annotate event extras with a type. See documentation for details. (Metric: {{obj.category}}.{{obj.name}}, defined in: {{obj.defined_in['filepath']}}:{{obj.defined_in['line']}})" + {% endif %} +{% endif %} +{% endfor %} +{%- endmacro -%} + +{%- macro extra_keys_with_types(obj, name, suffix) -%} +struct {{ obj.name|Camelize }}{{ suffix }} { + {% for item, type in obj|attr(name) %} + mozilla::Maybe<{{type|extra_type_name}}> {{ item|camelize }}; + {% endfor %} + + Tuple<nsTArray<nsCString>, nsTArray<nsCString>> ToFfiExtra() const { + nsTArray<nsCString> extraKeys; + nsTArray<nsCString> extraValues; + {% for item, type in obj|attr(name) %} + if ({{item|camelize}}) { + extraKeys.AppendElement()->AssignASCII("{{item}}"); + {% if type == "string" %} + extraValues.EmplaceBack({{item|camelize}}.value()); + {% elif type == "boolean" %} + extraValues.AppendElement()->AssignASCII({{item|camelize}}.value() ? "true" : "false"); + {% elif type == "quantity" %} + extraValues.EmplaceBack(nsPrintfCString("%d", {{item|camelize}}.value())); + {% else %} +#error "Glean: Invalid extra key type for metric {{obj.category}}.{{obj.name}}, defined in: {{obj.defined_in['filepath']}}:{{obj.defined_in['line']}})" + {% endif %} + } + {% endfor %} + return MakeTuple(std::move(extraKeys), std::move(extraValues)); + } +}; +{%- endmacro %} + +struct NoExtraKeys; + +{% for category_name, objs in all_objs.items() %} +namespace {{ category_name|snake_case }} { + {% for obj in objs.values() %} + /** + * generated from {{ category_name }}.{{ obj.name }} + */ + {% if obj|attr("_generate_enums") %} +{{ generate_extra_keys(obj) }} + {%- endif %} + /** + * {{ obj.description|wordwrap() | replace('\n', '\n * ') }} + */ + constexpr impl::{{ obj|type_name }} {{obj.name|snake_case }}({{obj|metric_id}}); + + {% endfor %} +} +{% endfor %} + +} // namespace mozilla::glean + +#endif // mozilla_Metrics_h diff --git a/toolkit/components/glean/build_scripts/glean_parser_ext/templates/cpp_pings.jinja2 b/toolkit/components/glean/build_scripts/glean_parser_ext/templates/cpp_pings.jinja2 new file mode 100644 index 0000000000..f877cb9685 --- /dev/null +++ b/toolkit/components/glean/build_scripts/glean_parser_ext/templates/cpp_pings.jinja2 @@ -0,0 +1,30 @@ +// -*- mode: C++ -*- + +// AUTOGENERATED BY glean_parser. DO NOT EDIT. +{# The rendered source is autogenerated, but this +Jinja2 template is not. Please file bugs! #} + +/* 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 http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_glean_Pings_h +#define mozilla_glean_Pings_h + +#include "mozilla/glean/bindings/Ping.h" + +namespace mozilla::glean_pings { + +{% for obj in all_objs['pings'].values() %} +/* + * Generated from {{ obj.name }}. + * + * {{ obj.description|wordwrap() | replace('\n', '\n * ') }} + */ +constexpr glean::impl::Ping {{ obj.name|Camelize }}({{ obj.name|ping_id }}); + +{% endfor %} + +} // namespace mozilla::glean_pings + +#endif // mozilla_glean_Pings_h diff --git a/toolkit/components/glean/build_scripts/glean_parser_ext/templates/gifft.jinja2 b/toolkit/components/glean/build_scripts/glean_parser_ext/templates/gifft.jinja2 new file mode 100644 index 0000000000..6df2650b52 --- /dev/null +++ b/toolkit/components/glean/build_scripts/glean_parser_ext/templates/gifft.jinja2 @@ -0,0 +1,237 @@ +// -*- mode: C++ -*- + +/* This file is auto-generated by run_glean_parser.py. + It is only for internal use by types in + toolkit/components/glean/bindings/private */ +{# The rendered source is autogenerated, but this +Jinja2 template is not. Please file bugs! #} + +#include "mozilla/AppShutdown.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/Maybe.h" +#include "mozilla/Telemetry.h" +#include "mozilla/Tuple.h" +#include "mozilla/DataMutex.h" +{% if probe_type == "Scalar" %} +#include "mozilla/Tuple.h" +#include "nsClassHashtable.h" +#include "nsIThread.h" +#include "nsTHashMap.h" +{% endif %} +#include "nsThreadUtils.h" + +#ifndef mozilla_glean_{{ probe_type }}GifftMap_h +#define mozilla_glean_{{ probe_type }}GifftMap_h + +namespace mozilla::glean { + +using Telemetry::{{ probe_type }}ID; + +{% if probe_type == "Histogram" %} + +using MetricId = uint32_t; // Same type as in api/src/private/mod.rs +using TimerId = uint64_t; // Same as in TimingDistribution.h. +using MetricTimerTuple = Tuple<MetricId, TimerId>; +class MetricTimerTupleHashKey : public PLDHashEntryHdr { + public: + using KeyType = const MetricTimerTuple&; + using KeyTypePointer = const MetricTimerTuple*; + + explicit MetricTimerTupleHashKey(KeyTypePointer aKey) : mValue(*aKey) {} + MetricTimerTupleHashKey(MetricTimerTupleHashKey&& aOther) + : PLDHashEntryHdr(std::move(aOther)), + mValue(std::move(aOther.mValue)) {} + ~MetricTimerTupleHashKey() = default; + + KeyType GetKey() const { return mValue; } + bool KeyEquals(KeyTypePointer aKey) const { + return Get<0>(*aKey) == Get<0>(mValue) && Get<1>(*aKey) == Get<1>(mValue); + } + + static KeyTypePointer KeyToPointer(KeyType aKey) { return &aKey; } + static PLDHashNumber HashKey(KeyTypePointer aKey) { + // Chosen because this is how nsIntegralHashKey does it. + return HashGeneric(Get<0>(*aKey), Get<1>(*aKey)); + } + enum { ALLOW_MEMMOVE = true }; + + private: + const MetricTimerTuple mValue; +}; + +typedef StaticDataMutex<UniquePtr<nsTHashMap<MetricTimerTupleHashKey, TimeStamp>>> TimerToStampMutex; +static inline Maybe<TimerToStampMutex::AutoLock> GetTimerIdToStartsLock() { + static TimerToStampMutex sTimerIdToStarts("sTimerIdToStarts"); + auto lock = sTimerIdToStarts.Lock(); + // GIFFT will work up to the end of AppShutdownTelemetry. + if (AppShutdown::IsInOrBeyond(ShutdownPhase::XPCOMWillShutdown)) { + return Nothing(); + } + if (!*lock) { + *lock = MakeUnique<nsTHashMap<MetricTimerTupleHashKey, TimeStamp>>(); + RefPtr<nsIRunnable> cleanupFn = NS_NewRunnableFunction(__func__, [&] { + if (AppShutdown::IsInOrBeyond(ShutdownPhase::XPCOMWillShutdown)) { + auto lock = sTimerIdToStarts.Lock(); + *lock = nullptr; // deletes, see UniquePtr.h + return; + } + RunOnShutdown([&] { + auto lock = sTimerIdToStarts.Lock(); + *lock = nullptr; // deletes, see UniquePtr.h + }, ShutdownPhase::XPCOMWillShutdown); + }); + // Both getting the main thread and dispatching to it can fail. + // In that event we leak. Grab a pointer so we have something to NS_RELEASE + // in that case. + nsIRunnable* temp = cleanupFn.get(); + nsCOMPtr<nsIThread> mainThread; + if (NS_FAILED(NS_GetMainThread(getter_AddRefs(mainThread))) + || NS_FAILED(mainThread->Dispatch(cleanupFn.forget(), nsIThread::DISPATCH_NORMAL)) + ) { + // Failed to dispatch cleanup routine. + // First, un-leak the runnable (but only if we actually attempted dispatch) + if (!cleanupFn) { + NS_RELEASE(temp); + } + // Next, cleanup immediately, and allow metrics to try again later. + *lock = nullptr; + return Nothing(); + } + } + return Some(std::move(lock)); +} +{% elif probe_type == "Scalar" %} +typedef nsUint32HashKey SubmetricIdHashKey; +typedef nsTHashMap<SubmetricIdHashKey, Tuple<ScalarID, nsString>> + SubmetricToLabeledMirrorMapType; +typedef StaticDataMutex<UniquePtr<SubmetricToLabeledMirrorMapType>> + SubmetricToMirrorMutex; +static inline Maybe<SubmetricToMirrorMutex::AutoLock> GetLabeledMirrorLock() { + static SubmetricToMirrorMutex sLabeledMirrors("sLabeledMirrors"); + auto lock = sLabeledMirrors.Lock(); + // GIFFT will work up to the end of AppShutdownTelemetry. + if (AppShutdown::IsInOrBeyond(ShutdownPhase::XPCOMWillShutdown)) { + return Nothing(); + } + if (!*lock) { + *lock = MakeUnique<SubmetricToLabeledMirrorMapType>(); + RefPtr<nsIRunnable> cleanupFn = NS_NewRunnableFunction(__func__, [&] { + if (AppShutdown::IsInOrBeyond(ShutdownPhase::XPCOMWillShutdown)) { + auto lock = sLabeledMirrors.Lock(); + *lock = nullptr; // deletes, see UniquePtr.h + return; + } + RunOnShutdown([&] { + auto lock = sLabeledMirrors.Lock(); + *lock = nullptr; // deletes, see UniquePtr.h + }, ShutdownPhase::XPCOMWillShutdown); + }); + // Both getting the main thread and dispatching to it can fail. + // In that event we leak. Grab a pointer so we have something to NS_RELEASE + // in that case. + nsIRunnable* temp = cleanupFn.get(); + nsCOMPtr<nsIThread> mainThread; + if (NS_FAILED(NS_GetMainThread(getter_AddRefs(mainThread))) + || NS_FAILED(mainThread->Dispatch(cleanupFn.forget(), nsIThread::DISPATCH_NORMAL)) + ) { + // Failed to dispatch cleanup routine. + // First, un-leak the runnable (but only if we actually attempted dispatch) + if (!cleanupFn) { + NS_RELEASE(temp); + } + // Next, cleanup immediately, and allow metrics to try again later. + *lock = nullptr; + return Nothing(); + } + } + return Some(std::move(lock)); +} + +namespace { +class ScalarIDHashKey : public PLDHashEntryHdr { + public: + typedef const ScalarID& KeyType; + typedef const ScalarID* KeyTypePointer; + + explicit ScalarIDHashKey(KeyTypePointer aKey) : mValue(*aKey) {} + ScalarIDHashKey(ScalarIDHashKey&& aOther) + : PLDHashEntryHdr(std::move(aOther)), mValue(std::move(aOther.mValue)) {} + ~ScalarIDHashKey() = default; + + KeyType GetKey() const { return mValue; } + bool KeyEquals(KeyTypePointer aKey) const { return *aKey == mValue; } + + static KeyTypePointer KeyToPointer(KeyType aKey) { return &aKey; } + static PLDHashNumber HashKey(KeyTypePointer aKey) { + return static_cast<std::underlying_type<ScalarID>::type>(*aKey); + } + enum { ALLOW_MEMMOVE = true }; + + private: + const ScalarID mValue; +}; +} // namespace +typedef StaticDataMutex<UniquePtr<nsTHashMap<ScalarIDHashKey, TimeStamp>>> TimesToStartsMutex; +static inline Maybe<TimesToStartsMutex::AutoLock> GetTimesToStartsLock() { + static TimesToStartsMutex sTimespanStarts("sTimespanStarts"); + auto lock = sTimespanStarts.Lock(); + // GIFFT will work up to the end of AppShutdownTelemetry. + if (AppShutdown::IsInOrBeyond(ShutdownPhase::XPCOMWillShutdown)) { + return Nothing(); + } + if (!*lock) { + *lock = MakeUnique<nsTHashMap<ScalarIDHashKey, TimeStamp>>(); + RefPtr<nsIRunnable> cleanupFn = NS_NewRunnableFunction(__func__, [&] { + if (AppShutdown::IsInOrBeyond(ShutdownPhase::XPCOMWillShutdown)) { + auto lock = sTimespanStarts.Lock(); + *lock = nullptr; // deletes, see UniquePtr.h + return; + } + RunOnShutdown([&] { + auto lock = sTimespanStarts.Lock(); + *lock = nullptr; // deletes, see UniquePtr.h + }, ShutdownPhase::XPCOMWillShutdown); + }); + // Both getting the main thread and dispatching to it can fail. + // In that event we leak. Grab a pointer so we have something to NS_RELEASE + // in that case. + nsIRunnable* temp = cleanupFn.get(); + nsCOMPtr<nsIThread> mainThread; + if (NS_FAILED(NS_GetMainThread(getter_AddRefs(mainThread))) + || NS_FAILED(mainThread->Dispatch(cleanupFn.forget(), nsIThread::DISPATCH_NORMAL)) + ) { + // Failed to dispatch cleanup routine. + // First, un-leak the runnable (but only if we actually attempted dispatch) + if (!cleanupFn) { + NS_RELEASE(temp); + } + // Next, cleanup immediately, and allow metrics to try again later. + *lock = nullptr; + return Nothing(); + } + } + return Some(std::move(lock)); +} + +static inline bool IsSubmetricId(uint32_t aId) { + // Submetrics have the 2^{{id_bits - id_signal_bits}} bit set. + // (ID_BITS - ID_SIGNAL_BITS, keep it in sync with js.py). + return (aId & (1 << {{id_bits - id_signal_bits}})) > 0; +} +{% endif %} + +static{% if probe_type == "Event" %} inline{% endif %} Maybe<{{ probe_type }}ID> {{ probe_type }}IdForMetric(uint32_t aId) { + switch(aId) { +{% for id, (mirror, metric_name) in ids_to_probes.items() %} + case {{ id }}: { // {{ metric_name }} + return Some({{ probe_type }}ID::{{ mirror }}); + } +{% endfor %} + default: { + return Nothing(); + } + } +} + +} // namespace mozilla::glean +#endif // mozilla_glean_{{ probe_type }}GifftMaps_h diff --git a/toolkit/components/glean/build_scripts/glean_parser_ext/templates/gifft_events.jinja2 b/toolkit/components/glean/build_scripts/glean_parser_ext/templates/gifft_events.jinja2 new file mode 100644 index 0000000000..f32640fc19 --- /dev/null +++ b/toolkit/components/glean/build_scripts/glean_parser_ext/templates/gifft_events.jinja2 @@ -0,0 +1,52 @@ +// -*- mode: C++ -*- + +/* This file is auto-generated by run_glean_parser.py. + It is only for internal use by types in + toolkit/components/glean/bindings/private */ +{# The rendered source is autogenerated, but this +Jinja2 template is not. Pleas file bugs! #} + +#include "mozilla/glean/bindings/Event.h" +#include "mozilla/glean/GleanMetrics.h" + +namespace mozilla::glean { + +template <> +/*static*/ const nsCString impl::EventMetric<NoExtraKeys>::ExtraStringForKey(uint32_t aKey) { + MOZ_ASSERT_UNREACHABLE("What are you doing here? No extra keys!"); + return ""_ns; +} + +{% for category_name, objs in all_objs.items() %} +{% for obj in objs.values() %} +{% if obj|attr("_generate_enums") %} +{# we always use the `extra` suffix, because we only expose the new event API #} +{% set suffix = "Extra" %} +{% for name, _ in obj["_generate_enums"] %} +{% if obj|attr(name)|length %} +{% set ns %}{{ category_name|snake_case }}{% endset %} +{% set type %}{{ obj.name|Camelize }}{{ suffix }}{% endset %} +template <> +/*static*/ const nsCString impl::EventMetric<{{ ns }}::{{ type }}>::ExtraStringForKey(uint32_t aKey) { + using {{ ns }}::{{ type }}; + switch (aKey) { +{% if obj|attr("telemetry_mirror") %}{# Optimization: Do not generate switch if not mirrored #} +{% for key, _ in obj|attr(name) %} + case {{loop.index-1}}: { + return "{{ key }}"_ns; + } +{% endfor %} +{% endif %} + default: { + MOZ_ASSERT_UNREACHABLE("Impossible event key reached."); + return ""_ns; + } + } +} + +{% endif %} +{% endfor %} +{% endif %} +{% endfor %} +{% endfor %} +}; // namespace mozilla::glean diff --git a/toolkit/components/glean/build_scripts/glean_parser_ext/templates/jog_factory.jinja2 b/toolkit/components/glean/build_scripts/glean_parser_ext/templates/jog_factory.jinja2 new file mode 100644 index 0000000000..23e5dc4f2b --- /dev/null +++ b/toolkit/components/glean/build_scripts/glean_parser_ext/templates/jog_factory.jinja2 @@ -0,0 +1,146 @@ +// -*- mode: Rust -*- + +// AUTOGENERATED BY glean_parser. DO NOT EDIT. +{# The rendered source is autogenerated, but this +Jinja2 template is not. Please file bugs! #} + +/* 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 http://mozilla.org/MPL/2.0/. */ + +/// This file contains factory implementation information for the +/// JOG Runtime Registration module. +/// It is responsible for being able to build metrics and pings described at runtime. +/// It is generated to keep it in sync with how the runtime definitions are defined. + +use std::sync::atomic::{AtomicU32, Ordering}; +use crate::private::{ + CommonMetricData, + Lifetime, + MemoryUnit, + TimeUnit, + Ping, + LabeledMetric, +{% for metric_type_name in metric_types.keys() if not metric_type_name.startswith('labeled_') %} + {{ metric_type_name|Camelize }}Metric, +{% endfor %}}; +use crate::private::traits::HistogramType; + +pub(crate) static DYNAMIC_METRIC_BIT: u32 = {{runtime_metric_bit}}; +// 2**DYNAMIC_METRIC_BIT + 1 (+1 because we reserve the 0 metric id) +static NEXT_METRIC_ID: AtomicU32 = AtomicU32::new({{2**runtime_metric_bit + 1}}); +#[cfg(feature = "with_gecko")] // only used in submit_ping_by_id, which is gecko-only. +pub(crate) static DYNAMIC_PING_BIT: u32 = {{runtime_ping_bit}}; +// 2**DYNAMIC_PING_BIT + 1 (+1 because we reserve the 0 ping id) +static NEXT_PING_ID: AtomicU32 = AtomicU32::new({{2**runtime_ping_bit + 1}}); + +pub(crate) mod __jog_metric_maps { + use crate::private::MetricId; + use crate::private::{ + Ping, + LabeledMetric, + NoExtraKeys, + {% for metric_type_name in metric_types.keys() %} + {{ metric_type_name|Camelize }}Metric, + {% endfor %} + }; + use once_cell::sync::Lazy; + use std::collections::HashMap; + use std::sync::{Arc, RwLock}; + +{% for metric_type_name in metric_types.keys() if metric_type_name != "event" and not metric_type_name.startswith('labeled_') %} + pub static {{ metric_type_name.upper() }}_MAP: Lazy<Arc<RwLock<HashMap<MetricId, {{ metric_type_name|Camelize }}Metric>>>> = + Lazy::new(|| Arc::new(RwLock::new(HashMap::new()))); + +{% endfor %} +{# Labeled metrics are special because they're LabeledMetric<Labeled{Counter|Boolean|...}Metric> #} +{% for metric_type_name in metric_types.keys() if metric_type_name.startswith('labeled_') %} + pub static {{ metric_type_name.upper() }}_MAP: Lazy<Arc<RwLock<HashMap<MetricId, LabeledMetric<{{ metric_type_name|Camelize }}Metric>>>>> = + Lazy::new(|| Arc::new(RwLock::new(HashMap::new()))); + +{% endfor %} + pub static PING_MAP: Lazy<Arc<RwLock<HashMap<u32, Ping>>>> = + Lazy::new(|| Arc::new(RwLock::new(HashMap::new()))); + +{# Event metrics are special because they're EventMetric<K> #} + pub static EVENT_MAP: Lazy<Arc<RwLock<HashMap<MetricId, EventMetric<NoExtraKeys>>>>> = + Lazy::new(|| Arc::new(RwLock::new(HashMap::new()))); +} + +#[derive(Debug)] +struct MetricTypeNotFoundError(String); +impl std::error::Error for MetricTypeNotFoundError {} +impl std::fmt::Display for MetricTypeNotFoundError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "Metric type {} not found", self.0) + } +} + +/// Creates and registers a metric, returning its type+id. +pub fn create_and_register_metric( + metric_type: &str, +{# The rest of these are handrolled because it proved easier than maintaining a +map of argument name to argument type. I may regret this if I need it again. #} +{# In order of util.common_metric_args and util.extra_metric_args, because why not. #} + category: String, + name: String, + send_in_pings: Vec<String>, + lifetime: Lifetime, + disabled: bool, + time_unit: Option<TimeUnit>, + memory_unit: Option<MemoryUnit>, + allowed_extra_keys: Option<Vec<String>>, +{# Skipping reason_codes since that's a ping thing. #} + range_min: Option<u64>, + range_max: Option<u64>, + bucket_count: Option<u64>, + histogram_type: Option<HistogramType>, + numerators: Option<Vec<CommonMetricData>>, +{# And, don't forget the list of acceptable labels for a labeled metric. #} + labels: Option<Vec<String>>, +) -> Result<u32, Box<dyn std::error::Error>> { + let metric_id = NEXT_METRIC_ID.fetch_add(1, Ordering::SeqCst); + let metric32 = match metric_type { +{% for metric_type_name, metric_type in metric_types.items() %} + "{{ metric_type_name }}" => { + let metric = {{ metric_type_name|Camelize if not metric_type_name.startswith('labeled_') else "Labeled"}}Metric::{% if metric_type_name == 'event' %}with_runtime_extra_keys{% else %}new{% endif %}(metric_id.into(), CommonMetricData { + {% for arg_name in common_metric_data_args if arg_name in metric_type.args %} + {{ arg_name }}, + {% endfor %} + ..Default::default() + } + {%- for arg_name in metric_type.args if arg_name not in common_metric_data_args -%} + , {{ arg_name }}.unwrap() + {%- endfor -%} + {%- if metric_type_name.startswith('labeled_') -%} + , labels + {%- endif -%} + ); + let metric32: u32 = ({{metric_type.id}} << {{ID_BITS}}) | metric_id; + assert!( + __jog_metric_maps::{{metric_type_name.upper()}}_MAP.write()?.insert(metric_id.into(), metric).is_none(), + "We should never insert a runtime metric with an already-used id." + ); + metric32 + } +{% endfor %} + _ => return Err(Box::new(MetricTypeNotFoundError(metric_type.to_string()))) + }; + Ok(metric32) +} + +/// Creates and registers a ping, returning its id. +pub fn create_and_register_ping( + ping_name: String, + include_client_id: bool, + send_if_empty: bool, + reason_codes: Vec<String>, +) -> Result<u32, Box<dyn std::error::Error>> { + let ping_id = NEXT_PING_ID.fetch_add(1, Ordering::SeqCst); + let ping = Ping::new(ping_name, include_client_id, send_if_empty, reason_codes); + assert!( + __jog_metric_maps::PING_MAP.write()?.insert(ping_id.into(), ping).is_none(), + "We should never insert a runtime ping with an already-used id." + ); + Ok(ping_id) +} diff --git a/toolkit/components/glean/build_scripts/glean_parser_ext/templates/js.jinja2 b/toolkit/components/glean/build_scripts/glean_parser_ext/templates/js.jinja2 new file mode 100644 index 0000000000..b2c9d1031f --- /dev/null +++ b/toolkit/components/glean/build_scripts/glean_parser_ext/templates/js.jinja2 @@ -0,0 +1,166 @@ +// -*- mode: C++ -*- + +// AUTOGENERATED BY glean_parser. DO NOT EDIT. +{# The rendered source is autogenerated, but this +Jinja2 template is not. Please file bugs! #} + +/* 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 http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_GleanJSMetricsLookup_h +#define mozilla_GleanJSMetricsLookup_h + +#include "mozilla/PerfectHash.h" +#include "mozilla/Maybe.h" +#include "mozilla/glean/bindings/MetricTypes.h" +#include "mozilla/glean/fog_ffi_generated.h" + +#define GLEAN_INDEX_BITS ({{index_bits}}) +#define GLEAN_TYPE_BITS ({{type_bits}}) +#define GLEAN_ID_BITS ({{id_bits}}) +#define GLEAN_TYPE_ID(id) ((id) >> GLEAN_ID_BITS) +#define GLEAN_METRIC_ID(id) ((id) & ((1ULL << GLEAN_ID_BITS) - 1)) +#define GLEAN_OFFSET(entry) (entry & ((1ULL << GLEAN_INDEX_BITS) - 1)) + +namespace mozilla::glean { + +// The category lookup table's entry type +using category_entry_t = uint32_t; +// The metric lookup table's entry type +// This is a bitpacked type with {{index_bits}} bits available to index into +// the string table, {{type_bits}} bits available to signify the metric type, +// and the remaining {{id_bits}} bits devoted to {{id_signal_bits}} "signal" +// bits to signify important characteristics (metric's a labeled metric's +// submetric, metric's been registered at runtime) and {{id_bits - id_signal_bits}} bits +// for built-in metric ids. +// Gives room for {{2 ** (id_bits - id_signal_bits)}} of each combination of +// characteristics (which hopefully will prove to be enough). +using metric_entry_t = uint64_t; + +static_assert(GLEAN_INDEX_BITS + GLEAN_TYPE_BITS + GLEAN_ID_BITS == sizeof(metric_entry_t) * 8, "Index, Type, and ID bits need to fit into a metric_entry_t"); +static_assert(GLEAN_TYPE_BITS + GLEAN_ID_BITS <= sizeof(uint32_t) * 8, "Metric Types and IDs need to fit into at most 32 bits"); +static_assert({{ categories|length }} < UINT32_MAX, "Too many metric categories generated."); +static_assert({{ metric_id_mapping|length }} < {{2 ** (id_bits - id_signal_bits)}}, "Too many metrics generated. Need room for {{id_signal_bits}} signal bits."); +static_assert({{ metric_type_ids|length }} < {{2 ** type_bits}}, "Too many different metric types."); + +static already_AddRefed<nsISupports> NewMetricFromId(uint32_t id) { + uint32_t typeId = GLEAN_TYPE_ID(id); + uint32_t metricId = GLEAN_METRIC_ID(id); + + switch (typeId) { + {% for (type_name, subtype_name), (type_id, original_type) in metric_type_ids.items() %} + case {{ type_id }}: /* {{ original_type }} */ + { + return MakeAndAddRef<{{type_name}}>(metricId{% if subtype_name|length > 0 %}, {{ type_id }}{% endif %}); + } + {% endfor %} + default: + MOZ_ASSERT_UNREACHABLE("Invalid type ID reached when trying to instantiate a new metric"); + return nullptr; + } +} + +/** + * Create a submetric instance for a labeled metric of the provided type and id for the given label. + * Assigns or retrieves an id for the submetric from the SDK. + * + * @param aParentTypeId - The type of the parent labeled metric identified as a number generated during codegen. + * Only used to identify which X of LabeledX you are so that X can be created here. + * @param aParentMetricId - The metric id for the parent labeled metric. + * @param aLabel - The label for the submetric. Might not adhere to the SDK label format. + * @param aSubmetricId - an outparam which is assigned the submetric's SDK-generated submetric id. + * Used only by GIFFT. + */ +static already_AddRefed<nsISupports> NewSubMetricFromIds(uint32_t aParentTypeId, uint32_t aParentMetricId, const nsACString& aLabel, uint32_t* aSubmetricId) { + switch (aParentTypeId) { + {% for (type_name, subtype_name), (type_id, original_type) in metric_type_ids.items() %} + {% if subtype_name|length > 0 %} + case {{ type_id }}: { /* {{ original_type }} */ + auto id = impl::fog_{{original_type}}_get(aParentMetricId, &aLabel); + *aSubmetricId = id; + return MakeAndAddRef<{{subtype_name}}>(id); + } + {% endif %} + {% endfor %} + default: { + MOZ_ASSERT_UNREACHABLE("Invalid type ID for submetric."); + return nullptr; + } + } +} + +static Maybe<uint32_t> category_result_check(const nsACString& aKey, category_entry_t entry); +static Maybe<uint32_t> metric_result_check(const nsACString& aKey, metric_entry_t entry); + +{{ category_string_table }} +static_assert(sizeof(gCategoryStringTable) < UINT32_MAX, "Category string table is too large."); + +{{ category_by_name_lookup }} + +{{ metric_string_table }} +static_assert(sizeof(gMetricStringTable) < {{2 ** index_bits}}, "Metric string table is too large."); + +{{ metric_by_name_lookup }} + +/** + * Get a category's name from the string table. + */ +static const char* GetCategoryName(category_entry_t entry) { + MOZ_ASSERT(entry < sizeof(gCategoryStringTable), "Entry identifier offset larger than string table"); + return &gCategoryStringTable[entry]; +} + +/** + * Get a metric's identifier from the string table. + */ +static const char* GetMetricIdentifier(metric_entry_t entry) { + uint32_t offset = GLEAN_OFFSET(entry); + MOZ_ASSERT(offset < sizeof(gMetricStringTable), "Entry identifier offset larger than string table"); + return &gMetricStringTable[offset]; +} + +/** + * Check that the found entry is pointing to the right key + * and return it. + * Or return `Nothing()` if the entry was not found. + */ +static Maybe<uint32_t> category_result_check(const nsACString& aKey, category_entry_t entry) { + if (MOZ_UNLIKELY(entry > sizeof(gCategoryStringTable))) { + return Nothing(); + } + if (aKey.EqualsASCII(gCategoryStringTable + entry)) { + return Some(entry); + } + return Nothing(); +} + +/** + * Check if the found entry index is pointing to the right key + * and return the corresponding metric ID. + * Or return `Nothing()` if the entry was not found. + */ +static Maybe<uint32_t> metric_result_check(const nsACString& aKey, uint64_t entry) { + uint32_t metricId = entry >> GLEAN_INDEX_BITS; + uint32_t offset = GLEAN_OFFSET(entry); + + if (offset > sizeof(gMetricStringTable)) { + return Nothing(); + } + + if (aKey.EqualsASCII(gMetricStringTable + offset)) { + return Some(metricId); + } + + return Nothing(); +} + + +#undef GLEAN_INDEX_BITS +#undef GLEAN_ID_BITS +#undef GLEAN_TYPE_ID +#undef GLEAN_METRIC_ID +#undef GLEAN_OFFSET + +} // namespace mozilla::glean +#endif // mozilla_GleanJSMetricsLookup_h diff --git a/toolkit/components/glean/build_scripts/glean_parser_ext/templates/js_pings.jinja2 b/toolkit/components/glean/build_scripts/glean_parser_ext/templates/js_pings.jinja2 new file mode 100644 index 0000000000..85665ec558 --- /dev/null +++ b/toolkit/components/glean/build_scripts/glean_parser_ext/templates/js_pings.jinja2 @@ -0,0 +1,64 @@ +// -*- mode: C++ -*- + +// AUTOGENERATED BY glean_parser. DO NOT EDIT. +{# The rendered source is autogenerated, but this +Jinja2 template is not. Please file bugs! #} + +/* 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 http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_GleanJSPingsLookup_h +#define mozilla_GleanJSPingsLookup_h + +#define GLEAN_PING_INDEX_BITS ({{ping_index_bits}}) +#define GLEAN_PING_ID(entry) ((entry) >> GLEAN_PING_INDEX_BITS) +#define GLEAN_PING_INDEX(entry) ((entry) & ((1UL << GLEAN_PING_INDEX_BITS) - 1)) + +namespace mozilla::glean { + +// Contains the ping id and the index into the ping string table. +using ping_entry_t = uint32_t; + +static Maybe<uint32_t> ping_result_check(const nsACString& aKey, ping_entry_t aEntry); + +{{ ping_string_table }} + +{{ ping_by_name_lookup }} + +/** + * Get a ping's name given its entry from the PHF. + */ +static const char* GetPingName(ping_entry_t aEntry) { + uint32_t idx = GLEAN_PING_INDEX(aEntry); + MOZ_ASSERT(idx < sizeof(gPingStringTable), "Ping index larger than string table"); + return &gPingStringTable[idx]; +} + +/** + * Check if the found entry is pointing at the correct ping. + * PHF can false-positive a result when the key isn't present, so we check + * for a string match. If it fails, return Nothing(). If we found it, + * return the ping's id. + */ +static Maybe<uint32_t> ping_result_check(const nsACString& aKey, ping_entry_t aEntry) { + uint32_t idx = GLEAN_PING_INDEX(aEntry); + uint32_t id = GLEAN_PING_ID(aEntry); + + if (MOZ_UNLIKELY(idx > sizeof(gPingStringTable))) { + return Nothing(); + } + + if (aKey.EqualsASCII(&gPingStringTable[idx])) { + return Some(id); + } + + return Nothing(); +} + +#undef GLEAN_PING_INDEX_BITS +#undef GLEAN_PING_ID +#undef GLEAN_PING_INDEX + +} // namespace mozilla::glean +#endif // mozilla_GleanJSPingsLookup_h diff --git a/toolkit/components/glean/build_scripts/glean_parser_ext/templates/rust.jinja2 b/toolkit/components/glean/build_scripts/glean_parser_ext/templates/rust.jinja2 new file mode 100644 index 0000000000..d43d1c3ea0 --- /dev/null +++ b/toolkit/components/glean/build_scripts/glean_parser_ext/templates/rust.jinja2 @@ -0,0 +1,271 @@ +// -*- mode: Rust -*- + +// AUTOGENERATED BY glean_parser. DO NOT EDIT. +{# The rendered source is autogenerated, but this +Jinja2 template is not. Please file bugs! #} + +/* 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 http://mozilla.org/MPL/2.0/. */ + +{% macro generate_extra_keys(obj) -%} +{% for name, _ in obj["_generate_enums"] %} +{# we always use the `extra` suffix, because we only expose the new event API #} +{% set suffix = "Extra" %} +{% if obj|attr(name)|length %} + {% if obj.has_extra_types %} + {{ extra_keys_with_types(obj, name, suffix)|indent }} + {% else %} + compile_error!("Untyped event extras not supported. Please annotate event extras with a type. See documentation for details. (Metric: {{obj.category}}.{{obj.name}}, defined in: {{obj.defined_in['filepath']}}:{{obj.defined_in['line']}})"); + {% endif %} +{% endif %} +{% endfor %} +{%- endmacro -%} + +{%- macro extra_keys_with_types(obj, name, suffix) -%} +#[derive(Default, Debug, Clone, Hash, Eq, PartialEq)] +pub struct {{ obj.name|Camelize }}{{ suffix }} { + {% for item, type in obj|attr(name) %} + pub {{ item|snake_case }}: Option<{{type|extra_type_name}}>, + {% endfor %} +} + +impl ExtraKeys for {{ obj.name|Camelize }}{{ suffix }} { + const ALLOWED_KEYS: &'static [&'static str] = {{ obj.allowed_extra_keys|extra_keys }}; + + fn into_ffi_extra(self) -> ::std::collections::HashMap<String, String> { + let mut map = ::std::collections::HashMap::new(); + {% for key, _ in obj|attr(name) %} + self.{{key|snake_case}}.and_then(|val| map.insert("{{key|snake_case}}".into(), val.to_string())); + {% endfor %} + map + } +} +{%- endmacro -%} + +{% for category_name, objs in all_objs.items() %} +pub mod {{ category_name|snake_case }} { + use crate::private::*; + use glean::CommonMetricData; + #[allow(unused_imports)] // HistogramType might be unusued, let's avoid warnings + use glean::HistogramType; + use once_cell::sync::Lazy; + + {% for obj in objs.values() %} + {% if obj|attr("_generate_enums") %} +{{ generate_extra_keys(obj) }} + {%- endif %} + #[allow(non_upper_case_globals)] + /// generated from {{ category_name }}.{{ obj.name }} + /// + /// {{ obj.description|wordwrap() | replace('\n', '\n /// ') }} + pub static {{ obj.name|snake_case }}: Lazy<{{ obj|type_name }}> = Lazy::new(|| { + {{ obj|ctor }}({{obj|metric_id}}.into(), CommonMetricData { + {% for arg_name in common_metric_data_args if obj[arg_name] is defined %} + {{ arg_name }}: {{ obj[arg_name]|rust }}, + {% endfor %} + ..Default::default() + } + {%- for arg_name in extra_args if obj[arg_name] is defined and arg_name not in common_metric_data_args and arg_name != 'allowed_extra_keys' -%} + , {{ obj[arg_name]|rust }} + {%- endfor -%} + {{ ", " if obj.labeled else ")\n" }} + {%- if obj.labeled -%} + {%- if obj.labels -%} + Some({{ obj.labels|rust }}) + {%- else -%} + None + {%- endif -%}) + {% endif %} + }); + + {% endfor %} +} +{% endfor %} + +{% if metric_by_type|length > 0 %} +#[allow(dead_code)] +pub(crate) mod __glean_metric_maps { + use std::collections::HashMap; + + use crate::metrics::extra_keys_len; + use crate::private::*; + use once_cell::sync::Lazy; + +{% for typ, metrics in metric_by_type.items() %} + pub static {{typ.0}}: Lazy<HashMap<MetricId, &Lazy<{{typ.1}}>>> = Lazy::new(|| { + let mut map = HashMap::with_capacity({{metrics|length}}); + {% for metric in metrics %} + map.insert({{metric.0}}.into(), &super::{{metric.1}}); + {% endfor %} + map + }); + +{% endfor %} + + /// Wrapper to record an event based on its metric ID. + /// + /// # Arguments + /// + /// * `metric_id` - The metric's ID to look up + /// * `extra` - An map of (extra key id, string) pairs. + /// The map will be decoded into the appropriate `ExtraKeys` type. + /// # Returns + /// + /// Returns `Ok(())` if the event was found and `record` was called with the given `extra`, + /// or an `EventRecordingError::InvalidId` if no event by that ID exists + /// or an `EventRecordingError::InvalidExtraKey` if the `extra` map could not be deserialized. + pub(crate) fn record_event_by_id(metric_id: u32, extra: HashMap<String, String>) -> Result<(), EventRecordingError> { + match metric_id { +{% for metric_id, event in events_by_id.items() %} + {{metric_id}} => { + assert!( + extra_keys_len(&super::{{event}}) != 0 || extra.is_empty(), + "No extra keys allowed, but some were passed" + ); + + super::{{event}}.record_raw(extra); + Ok(()) + } +{% endfor %} + _ => Err(EventRecordingError::InvalidId), + } + } + + /// Wrapper to record an event based on its metric ID, with a provided timestamp. + /// + /// # Arguments + /// + /// * `metric_id` - The metric's ID to look up + /// * `timestamp` - The time at which this event was recorded. + /// * `extra` - An map of (extra key id, string) pairs. + /// The map will be decoded into the appropriate `ExtraKeys` type. + /// # Returns + /// + /// Returns `Ok(())` if the event was found and `record` was called with the given `extra`, + /// or an `EventRecordingError::InvalidId` if no event by that ID exists + /// or an `EventRecordingError::InvalidExtraKey` if the event doesn't take extra pairs, + /// but some are passed in. + pub(crate) fn record_event_by_id_with_time(metric_id: MetricId, timestamp: u64, extra: HashMap<String, String>) -> Result<(), EventRecordingError> { + match metric_id { +{% for metric_id, event in events_by_id.items() %} + MetricId({{metric_id}}) => { + if extra_keys_len(&super::{{event}}) == 0 && !extra.is_empty() { + return Err(EventRecordingError::InvalidExtraKey); + } + + super::{{event}}.record_with_time(timestamp, extra); + Ok(()) + } +{% endfor %} + _ => Err(EventRecordingError::InvalidId), + } + } + + /// Wrapper to record an event based on its metric ID. + /// + /// # Arguments + /// + /// * `metric_id` - The metric's ID to look up + /// * `extra` - An map of (string, string) pairs. + /// The map will be decoded into the appropriate `ExtraKeys` types. + /// # Returns + /// + /// Returns `Ok(())` if the event was found and `record` was called with the given `extra`, + /// or an `EventRecordingError::InvalidId` if no event by that ID exists + /// or an `EventRecordingError::InvalidExtraKey` if the `extra` map could not be deserialized. + pub(crate) fn record_event_by_id_with_strings(metric_id: u32, extra: HashMap<String, String>) -> Result<(), EventRecordingError> { + match metric_id { +{% for metric_id, event in events_by_id.items() %} + {{metric_id}} => { + assert!( + extra_keys_len(&super::{{event}}) != 0 || extra.is_empty(), + "No extra keys allowed, but some were passed" + ); + + super::{{event}}.record_raw(extra); + Ok(()) + } +{% endfor %} + _ => Err(EventRecordingError::InvalidId), + } + } + + /// Wrapper to get the currently stored events for event metric. + /// + /// # Arguments + /// + /// * `metric_id` - The metric's ID to look up + /// * `ping_name` - (Optional) The ping name to look into. + /// Defaults to the first value in `send_in_pings`. + /// + /// # Returns + /// + /// Returns the recorded events or `None` if nothing stored. + /// + /// # Panics + /// + /// Panics if no event by the given metric ID could be found. + pub(crate) fn event_test_get_value_wrapper(metric_id: u32, ping_name: Option<String>) -> Option<Vec<RecordedEvent>> { + match metric_id { +{% for metric_id, event in events_by_id.items() %} + {{metric_id}} => super::{{event}}.test_get_value(ping_name.as_deref()), +{% endfor %} + _ => panic!("No event for metric id {}", metric_id), + } + } + + /// Check the provided event for errors. + /// + /// # Arguments + /// + /// * `metric_id` - The metric's ID to look up + /// * `ping_name` - (Optional) The ping name to look into. + /// Defaults to the first value in `send_in_pings`. + /// + /// # Returns + /// + /// Returns a string for the recorded error or `None`. + /// + /// # Panics + /// + /// Panics if no event by the given metric ID could be found. + #[allow(unused_variables)] + pub(crate) fn event_test_get_error(metric_id: u32) -> Option<String> { + #[cfg(feature = "with_gecko")] + match metric_id { +{% for metric_id, event in events_by_id.items() %} + {{metric_id}} => test_get_errors!(super::{{event}}), +{% endfor %} + _ => panic!("No event for metric id {}", metric_id), + } + + #[cfg(not(feature = "with_gecko"))] + { + return None; + } + } + + pub(crate) mod submetric_maps { + use std::sync::{ + atomic::AtomicU32, + RwLock, + }; + use super::*; + + pub(crate) const SUBMETRIC_BIT: u32 = {{submetric_bit}}; + pub(crate) static NEXT_LABELED_SUBMETRIC_ID: AtomicU32 = AtomicU32::new((1 << SUBMETRIC_BIT) + 1); + pub(crate) static LABELED_METRICS_TO_IDS: Lazy<RwLock<HashMap<(u32, String), u32>>> = Lazy::new(|| + RwLock::new(HashMap::new()) + ); + +{% for typ, metrics in metric_by_type.items() %} +{% if typ.0 in ('BOOLEAN_MAP', 'COUNTER_MAP', 'STRING_MAP') %} + pub static {{typ.0}}: Lazy<RwLock<HashMap<MetricId, Labeled{{typ.1}}>>> = Lazy::new(|| + RwLock::new(HashMap::new()) + ); +{% endif %} +{% endfor%} + } +} +{% endif %} diff --git a/toolkit/components/glean/build_scripts/glean_parser_ext/templates/rust_pings.jinja2 b/toolkit/components/glean/build_scripts/glean_parser_ext/templates/rust_pings.jinja2 new file mode 100644 index 0000000000..a00da2c1bf --- /dev/null +++ b/toolkit/components/glean/build_scripts/glean_parser_ext/templates/rust_pings.jinja2 @@ -0,0 +1,59 @@ +// -*- mode: Rust -*- + +// AUTOGENERATED BY glean_parser. DO NOT EDIT. +{# The rendered source is autogenerated, but this +Jinja2 template is not. Please file bugs! #} + +/* 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 http://mozilla.org/MPL/2.0/. */ + +use crate::private::Ping; +use once_cell::sync::Lazy; + +{% for obj in all_objs['pings'].values() %} +#[allow(non_upper_case_globals)] +/// {{ obj.description|wordwrap() | replace('\n', '\n/// ') }} +pub static {{ obj.name|snake_case }}: Lazy<Ping> = Lazy::new(|| { + Ping::new( + "{{ obj.name }}", + {{ obj.include_client_id|rust }}, + {{ obj.send_if_empty|rust }}, + {{ obj.reason_codes|rust }}, + ) +}); + +{% endfor %} + +/// Instantiate each custom ping once to trigger registration. +#[doc(hidden)] +pub fn register_pings() { + {% for obj in all_objs['pings'].values() %} + let _ = &*{{obj.name|snake_case }}; + {% endfor %} +} + +#[cfg(feature = "with_gecko")] +pub(crate) fn submit_ping_by_id(id: u32, reason: Option<&str>) { + if id & (1 << crate::factory::DYNAMIC_PING_BIT) > 0 { + let map = crate::factory::__jog_metric_maps::PING_MAP + .read() + .expect("Read lock for dynamic ping map was poisoned!"); + if let Some(ping) = map.get(&id) { + ping.submit(reason); + } else { + // TODO: instrument this error. + log::error!("Cannot submit unknown dynamic ping {} by id.", id); + } + return; + } + match id { +{% for obj in all_objs['pings'].values() %} + {{ obj.name|ping_id }} => {{ obj.name | snake_case }}.submit(reason), +{% endfor %} + _ => { + // TODO: instrument this error. + log::error!("Cannot submit unknown ping {} by id.", id); + } + } +} diff --git a/toolkit/components/glean/build_scripts/glean_parser_ext/util.py b/toolkit/components/glean/build_scripts/glean_parser_ext/util.py new file mode 100644 index 0000000000..722a3e409c --- /dev/null +++ b/toolkit/components/glean/build_scripts/glean_parser_ext/util.py @@ -0,0 +1,102 @@ +# -*- coding: utf-8 -*- + +# 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 http://mozilla.org/MPL/2.0/. + +""" +Utility functions for the glean_parser-based code generator +""" +import copy +from typing import Dict, List, Tuple + +from glean_parser import util + + +def generate_ping_ids(objs): + """ + Return a lookup function for ping IDs per ping name. + + :param objs: A tree of objects as returned from `parser.parse_objects`. + """ + + if "pings" not in objs: + + def no_ping_ids_for_you(): + assert False + + return no_ping_ids_for_you + + # Ping ID 0 is reserved (but unused) right now. + ping_id = 1 + + ping_id_mapping = {} + for ping_name in objs["pings"].keys(): + ping_id_mapping[ping_name] = ping_id + ping_id += 1 + + return lambda ping_name: ping_id_mapping[ping_name] + + +def generate_metric_ids(objs): + """ + Return a lookup function for metric IDs per metric object. + + :param objs: A tree of metrics as returned from `parser.parse_objects`. + """ + + # Metric ID 0 is reserved (but unused) right now. + metric_id = 1 + + # Mapping from a tuple of (category name, metric name) to the metric's numeric ID + metric_id_mapping = {} + for category_name, metrics in objs.items(): + for metric in metrics.values(): + metric_id_mapping[(category_name, metric.name)] = metric_id + metric_id += 1 + + return lambda metric: metric_id_mapping[(metric.category, metric.name)] + + +def get_metrics(objs): + """ + Returns *just* the metrics in a set of Glean objects + """ + ret = copy.copy(objs) + for category in ["pings", "tags"]: + if ret.get(category): + del ret[category] + return ret + + +def type_ids_and_categories(objs) -> Tuple[Dict[str, Tuple[int, List[str]]], List[str]]: + """ + Iterates over the metrics in objs, constructing two metadata structures: + - metric_types: Dict[str, Tuple[int, List[str]]] - map from a metric + type (snake_case) to its metric type id and ordered list of arguments. + - categories: List[str] - category names (snake_case) + + Is stable across invocations: Will generate same ids for same objs. + (If it doesn't, JOG's factory disagreeing with GleanJSMetricsLookup + will break the build). + Uses the same order of metric args set out in glean_parser.util's + common_metric_args and extra_metric_args. + (If it didn't, it would supply args in the wrong order to metric type + constructors with multiple extra args (e.g. custom_distribution)). + """ + metric_type_ids = {} + categories = [] + + for category_name, objs in get_metrics(objs).items(): + categories.append(category_name) + + for metric in objs.values(): + if metric.type not in metric_type_ids: + type_id = len(metric_type_ids) + 1 + args = util.common_metric_args.copy() + for arg_name in util.extra_metric_args: + if hasattr(metric, arg_name): + args.append(arg_name) + metric_type_ids[metric.type] = {"id": type_id, "args": args} + + return (metric_type_ids, categories) diff --git a/toolkit/components/glean/build_scripts/mach_commands.py b/toolkit/components/glean/build_scripts/mach_commands.py new file mode 100644 index 0000000000..f722d98987 --- /dev/null +++ b/toolkit/components/glean/build_scripts/mach_commands.py @@ -0,0 +1,231 @@ +# 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 http://mozilla.org/MPL/2.0/. + +from mach.decorators import Command, CommandArgument + +LICENSE_HEADER = """# 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 http://mozilla.org/MPL/2.0/. +""" + +GENERATED_HEADER = """ +### This file was AUTOMATICALLY GENERATED by `./mach update-glean-tags` +### DO NOT edit it by hand. +""" + + +@Command( + "data-review", + category="misc", + description="Generate a skeleton data review request form for a given bug's data", +) +@CommandArgument( + "bug", default=None, nargs="?", type=str, help="bug number or search pattern" +) +def data_review(command_context, bug=None): + # Get the metrics_index's list of metrics indices + # by loading the index as a module. + import sys + from os import path + + sys.path.append(path.join(path.dirname(__file__), path.pardir)) + from pathlib import Path + + from glean_parser import data_review + from metrics_index import metrics_yamls + + return data_review.generate( + bug, [Path(command_context.topsrcdir) / x for x in metrics_yamls] + ) + + +@Command( + "perf-data-review", + category="misc", + description="Generate a skeleton performance data review request form for a given bug's data", +) +@CommandArgument( + "bug", default=None, nargs="?", type=str, help="bug number or search pattern" +) +def perf_data_review(command_context, bug=None): + # Get the metrics_index's list of metrics indices + # by loading the index as a module. + import sys + from os import path + + sys.path.append(path.join(path.dirname(__file__), path.pardir)) + from metrics_index import metrics_yamls + + sys.path.append(path.dirname(__file__)) + from pathlib import Path + + import perf_data_review + + return perf_data_review.generate( + bug, [Path(command_context.topsrcdir) / x for x in metrics_yamls] + ) + + +@Command( + "update-glean-tags", + category="misc", + description=( + "Creates a list of valid glean tags based on in-tree bugzilla component definitions" + ), +) +def update_glean_tags(command_context): + from pathlib import Path + + import yaml + from mozbuild.backend.configenvironment import ConfigEnvironment + from mozbuild.frontend.reader import BuildReader + + config = ConfigEnvironment( + command_context.topsrcdir, + command_context.topobjdir, + defines=command_context.defines, + substs=command_context.substs, + ) + + reader = BuildReader(config) + bug_components = set() + for p in reader.read_topsrcdir(): + if p.get("BUG_COMPONENT"): + bug_components.add(p["BUG_COMPONENT"]) + + tags_filename = (Path(__file__).parent / "../tags.yaml").resolve() + + tags = {"$schema": "moz://mozilla.org/schemas/glean/tags/1-0-0"} + for bug_component in bug_components: + product = bug_component.product.strip() + component = bug_component.component.strip() + tags["{} :: {}".format(product, component)] = { + "description": "The Bugzilla component which applies to this object." + } + + open(tags_filename, "w").write( + "{}\n{}\n\n".format(LICENSE_HEADER, GENERATED_HEADER) + + yaml.dump(tags, width=78, explicit_start=True) + ) + + +def replace_in_file(path, pattern, replace): + """ + Replace `pattern` with `replace` in the file `path`. + The file is modified on disk. + + Returns `True` if exactly one replacement happened. + `False` otherwise. + """ + + import re + + with open(path, "r+") as file: + data = file.read() + data, subs_made = re.subn(pattern, replace, data, flags=re.MULTILINE) + + file.seek(0) + file.write(data) + file.truncate() + + if subs_made != 1: + return False + + return True + + +def replace_in_file_or_die(path, pattern, replace): + """ + Replace `pattern` with `replace` in the file `path`. + The file is modified on disk. + + If not exactly one occurrence of `pattern` was replaced it will exit with exit code 1. + """ + + import sys + + success = replace_in_file(path, pattern, replace) + if not success: + print(f"ERROR: Failed to replace one occurrence in {path}") + print(f" Pattern: {pattern}") + print(f" Replace: {replace}") + print("File was modified. Check the diff.") + sys.exit(1) + + +@Command( + "update-glean", + category="misc", + description="Update Glean to the given version", +) +@CommandArgument("version", help="Glean version to upgrade to") +def update_glean(command_context, version): + import textwrap + from pathlib import Path + + topsrcdir = Path(command_context.topsrcdir) + + replace_in_file_or_die( + topsrcdir / "build.gradle", + r'gleanVersion = "[0-9.]+"', + f'gleanVersion = "{version}"', + ) + replace_in_file_or_die( + topsrcdir / "toolkit" / "components" / "glean" / "Cargo.toml", + r'^glean = "[0-9.]+"', + f'glean = "{version}"', + ) + replace_in_file_or_die( + topsrcdir / "toolkit" / "components" / "glean" / "api" / "Cargo.toml", + r'^glean = "[0-9.]+"', + f'glean = "{version}"', + ) + replace_in_file_or_die( + topsrcdir / "gfx" / "wr" / "webrender" / "Cargo.toml", + r'^glean = "[0-9.]+"', + f'glean = "{version}"', + ) + replace_in_file_or_die( + topsrcdir / "python" / "sites" / "mach.txt", + r"glean-sdk==[0-9.]+", + f"glean-sdk=={version}", + ) + + instructions = f""" + We've edited the necessary files to require Glean SDK {version}. + To ensure it and Firefox's other Rust dependencies are appropriately vendored, + please run the following commands: + + cargo update -p glean + mach vendor rust --ignore-modified + + `mach vendor rust` may identify version mismatches. + Please consult the Updating the Glean SDK docs for assistance: + https://firefox-source-docs.mozilla.org/toolkit/components/glean/dev/updating_sdk.html + + Once you resolve these issues and `mach vendor rust` completes successfully, + (or if there were no issues in the first place) + you will need to certify that the Glean SDK crates are okay to include in + Firefox using `mach cargo vet`. + + Please run these commands, reading and following their instructions: + + mach cargo vet certify glean {version} + mach cargo vet certify glean-core {version} + + You then get to again run: + + mach vendor rust --ignore-modified + + Then, to update webrender which independently relies on the Glean SDK, run: + + cd gfx/wr + cargo update -p glean + + Then, to ensure all is well, build Firefox and run the FOG tests. + Instructions can be found here: + https://firefox-source-docs.mozilla.org/toolkit/components/glean/dev/testing.html + """ + + print(textwrap.dedent(instructions)) diff --git a/toolkit/components/glean/build_scripts/perf_data_review.py b/toolkit/components/glean/build_scripts/perf_data_review.py new file mode 100644 index 0000000000..8c84249a2a --- /dev/null +++ b/toolkit/components/glean/build_scripts/perf_data_review.py @@ -0,0 +1,168 @@ +# -*- coding: utf-8 -*- + +# 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 http://mozilla.org/MPL/2.0/. + +""" +Produce skeleton Performance Data Review Requests. + +This was mostly copies from glean_parser, and should be kept in sync. +""" + +import re +from pathlib import Path +from typing import Sequence + +from glean_parser import parser, util + + +def generate( + bug: str, + metrics_files: Sequence[Path], +) -> int: + """ + Commandline helper for Data Review Request template generation. + + :param bug: pattern to match in metrics' bug_numbers lists. + :param metrics_files: List of Path objects to load metrics from. + :return: Non-zero if there were any errors. + """ + + metrics_files = util.ensure_list(metrics_files) + + # Accept any value of expires. + parser_options = { + "allow_reserved": True, + "custom_is_expired": lambda expires: False, + "custom_validate_expires": lambda expires: True, + } + all_objects = parser.parse_objects(metrics_files, parser_options) + + if util.report_validation_errors(all_objects): + return 1 + + # I tried [\W\Z] but it complained. So `|` it is. + reobj = re.compile(f"\\W{bug}\\W|\\W{bug}$") + durations = set() + responsible_emails = set() + metrics_table = "" + for category_name, metrics in all_objects.value.items(): + for metric in metrics.values(): + if not any([len(reobj.findall(bug)) == 1 for bug in metric.bugs]): + continue + + metric_name = util.snake_case(metric.name) + category_name = util.snake_case(category_name) + one_line_desc = metric.description.replace("\n", " ") + sensitivity = ", ".join([s.name for s in metric.data_sensitivity]) + last_bug = metric.bugs[-1] + metrics_table += f"`{category_name}.{metric_name}` | " + metrics_table += f"{one_line_desc} | {sensitivity} | {last_bug}\n" + if metric.type == "event" and len(metric.allowed_extra_keys): + for extra_name, extra_detail in metric.extra_keys.items(): + extra_one_line_desc = extra_detail["description"].replace("\n", " ") + metrics_table += f"`{category_name}.{metric_name}#{extra_name}` | " + metrics_table += ( + f"{extra_one_line_desc} | {sensitivity} | {last_bug}\n" + ) + + durations.add(metric.expires) + + if metric.expires == "never": + responsible_emails.update(metric.notification_emails) + + if len(durations) == 1: + duration = next(iter(durations)) + if duration == "never": + collection_duration = "This collection will be collected permanently." + else: + collection_duration = f"This collection has expiry '{duration}'" + else: + collection_duration = "Parts of this collection expire at different times: " + collection_duration += f"{durations}" + + if "never" in durations: + collection_duration += "\n" + ", ".join(responsible_emails) + " " + collection_duration += "will be responsible for the permanent collections." + + if len(durations) == 0: + print(f"I'm sorry, I couldn't find metrics matching the bug number {bug}.") + return 1 + + # This template is pulled from + # https://github.com/mozilla/data-review/blob/main/request.md + print( + """ +!! Reminder: it is your responsibility to complete and check the correctness of +!! this automatically-generated request skeleton before requesting Data +!! Collection Review. See https://wiki.mozilla.org/Data_Collection for details. + +DATA REVIEW REQUEST +1. What questions will you answer with this data? + +TODO: Fill this in. + +2. Why does Mozilla need to answer these questions? Are there benefits for users? + Do we need this information to address product or business requirements? + +In order to guarantee the performance of our products, it is vital to monitor +real-world installs used by real-world users. + +3. What alternative methods did you consider to answer these questions? + Why were they not sufficient? + +Our ability to measure the practical performance impact of changes through CI +and manual testing is limited. Monitoring the performance of our products in +the wild among real users is the only way to be sure we have an accurate +picture. + +4. Can current instrumentation answer these questions? + +No. + +5. List all proposed measurements and indicate the category of data collection for each + measurement, using the Firefox data collection categories found on the Mozilla wiki. + +Measurement Name | Measurement Description | Data Collection Category | Tracking Bug +---------------- | ----------------------- | ------------------------ | ------------""" + ) + print(metrics_table) + print( + """ +6. Please provide a link to the documentation for this data collection which + describes the ultimate data set in a public, complete, and accurate way. + +This collection is Glean so is documented +[in the Glean Dictionary](https://dictionary.telemetry.mozilla.org). + +7. How long will this data be collected? +""" + ) + print(collection_duration) + print( + """ +8. What populations will you measure? + +All channels, countries, and locales. No filters. + +9. If this data collection is default on, what is the opt-out mechanism for users? + +These collections are Glean. The opt-out can be found in the product's preferences. + +10. Please provide a general description of how you will analyze this data. + +This will be continuously monitored for regression and improvement detection. + +11. Where do you intend to share the results of your analysis? + +Internal monitoring (GLAM, Redash, Looker, etc.). + +12. Is there a third-party tool (i.e. not Telemetry) that you + are proposing to use for this data collection? + +No. +""" + ) + + return 0 diff --git a/toolkit/components/glean/cbindgen.toml b/toolkit/components/glean/cbindgen.toml new file mode 100644 index 0000000000..d514585ee1 --- /dev/null +++ b/toolkit/components/glean/cbindgen.toml @@ -0,0 +1,25 @@ +header = """/* 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 http://mozilla.org/MPL/2.0/. */ +#ifndef mozilla_glean_fog_ffi_generated_h +#define mozilla_glean_fog_ffi_generated_h +""" +trailer = """ +#endif // mozilla_glean_fog_ffi_generated_h +""" +autogen_warning = """/* DO NOT MODIFY THIS MANUALLY! This file was generated using cbindgen. */""" +include_version = true +braces = "SameLine" +line_length = 100 +tab_width = 2 +language = "C++" +namespaces = ["mozilla::glean::impl"] +includes = ["nsTArray.h", "nsString.h"] + +[export.rename] +"ThinVec" = "nsTArray" + +[parse] +parse_deps = true +include = ["fog"] +extra_bindings = ["fog"] diff --git a/toolkit/components/glean/docs/dev/builtin_pings.md b/toolkit/components/glean/docs/dev/builtin_pings.md new file mode 100644 index 0000000000..cbfff8c559 --- /dev/null +++ b/toolkit/components/glean/docs/dev/builtin_pings.md @@ -0,0 +1,13 @@ +# Built-in Pings + +FOG embeds the Glean SDK so +[its documentation on pings is authoritative](https://mozilla.github.io/glean/book/user/pings/index.html). +The only detail FOG adds is to clarify +[the "baseline" ping's schedule](https://mozilla.github.io/glean/book/user/pings/baseline.html#scheduling). +Specifically, in Firefox Desktop, the application is considered +* "active" when started, + or when a user interacts with Firefox after a 20min period of inactivity, +* "inactive" after the user stops interacting with Firefox after 2min of activity. + +For more details about why, see the bug tree around +[bug 1635242](https://bugzilla.mozilla.org/show_bug.cgi?id=1635242). diff --git a/toolkit/components/glean/docs/dev/code_organization.md b/toolkit/components/glean/docs/dev/code_organization.md new file mode 100644 index 0000000000..e943dbfd50 --- /dev/null +++ b/toolkit/components/glean/docs/dev/code_organization.md @@ -0,0 +1,56 @@ +# FOG code organization + +![Modules of Project FOG](images/fog-modules.svg) + +The diagram shows the different modules of Project FOG. + +## FOG control + +This module is the glue between Firefox and Glean. + +* The code lives in `toolkit/components/glean/src`. +* It is written in Rust. +* The crate is named `fog_control`. +* It is not published to crates.io. +* It is not consumed by other Rust crates inside mozilla-central. + +This module is responsible for + +* collecting and assembling the [client information](https://mozilla.github.io/glean/book/user/pings/index.html#the-client_info-section) +* configuring the Glean SDK via the Rust Language Binding +* watching the Firefox Telemetry data upload preference (`datareporting.healthreport.uploadEnabled`) +* scheduling builtin pings +* controling ping upload workers +* passing IPC buffers + +It calls into `glean` (the Glean SDK Rust Language Binding) to: + +* configure and initialize Glean +* toggle `upload_enabled` +* get upload tasks + +It calls into `fog` to: + +* pass IPC buffers +* record to its own metrics + +## FOG API + +This module provides the user-facing API for Glean inside mozilla-central. + +* The code lives in `toolkit/components/glean/api`. +* It is written in Rust. +* The crate is named `fog`. +* It is not published to crates.io. +* It can be consumed by other Rust crates inside mozilla-central for their Glean usage. + +This module is responsible for + +* exposing a specific metric API in Rust +* wrapping metric implementations for handling IPC +* exposing FFI functionality to implement other language APIs on top. + See also [Adding a New Metric Type](new_metric_types.md). + +It calls into `glean` (the Glean SDK Rust Language Binding) for: + +* metric types (including pings) diff --git a/toolkit/components/glean/docs/dev/images/fog-modules.svg b/toolkit/components/glean/docs/dev/images/fog-modules.svg new file mode 100644 index 0000000000..c0271e9f6e --- /dev/null +++ b/toolkit/components/glean/docs/dev/images/fog-modules.svg @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- 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 http://mozilla.org/MPL/2.0/. --> +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> +<svg xmlns="http://www.w3.org/2000/svg" style="background-color: rgb(255, 255, 255);" version="1.1" width="601px" height="511px" viewBox="-0.5 -0.5 601 511" content="<mxfile host="app.diagrams.net" modified="2020-03-27T09:36:49.445Z" agent="Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:76.0) Gecko/20100101 Firefox/76.0" etag="_te5ruI7nKv5hlBG0WM7" version="12.9.3" type="device"><diagram id="02tFoor6QwIjLFNeosYU" name="Page-1">7Vtbd6M2EP41fkyPQYDxo+NcmnbbpOvu2eZRBtmmxYgVsmP311eAZC6SDU4AX5ok5wSNLsA334xGI9ED4+XmkcBw8Rt2kd/T++6mB+56uq4NLYP9iyVbLtHsYSqZE8/lskww8f5FXNjn0pXnoqjQkGLsUy8sCh0cBMihBRkkBL8Vm82wX7xrCOdIEkwc6MvS755LF6nUNvuZ/GfkzRfizlqf1yyhaMwF0QK6+C0nAvc9MCYY0/RquRkjP0ZP4JL2e9hTu3swggJap0P/YUNGP0L/5QvY+N8XzwT/Y93wUdbQX/EX5g9LtwIBgleBi+JB+j1w+7bwKJqE0Ilr35jSmWxBlz4raexy5vn+GPuYJH3BbDbTHYfJI8puh3I1rjW1TIvVyK8hngkRijY5EX+tR4SXiJItayJqBcScZBYvvmUKs4RaFnlliX6Qk2S+GzrDkV1wKI+AVW8XVtdEtmuoYLX1KbBaglWza+IKQFu4AgWuls9ue+t66/iGvjcPkgrrxyo2rFuSPNauyK7m/H/SbYYZPHm9iIZxxU2UuKMRa6BZ4UYe5Q6tkY9DRGLPAh0vmItx2dulQxdvx8TJgwppiRNMMbSo+KKCAxygEhu4SLx4+rrgNtayx7zYiMuXnuvGd1ESrUjFBphj2pUGqRkq4rTFG6OCN0frv2umPQUUkQD60SfBEoINq12TBrpkmGZKYCOXRRK8iAld4DlmGrzPpCVcsjZfMA65fv5GlG55WARXFBe1hzYe/Svu/pPJS6+5mrsNHzkpbEUhYO+b6xQXX/N1WbektK1SW4RXxEEHsLF43AbJHPGuT9/MP23zj8lo8818CbSvweDGFsFIjNtBEhDkQ+qtixGaSqNJ1xEhcJtrEGIvoFFu5JdYkHELDErcGpbiqlJ73TjYnl2kT5Bxa/cq76ebJTk0Ns5t8tdk+GY7SB2+TW3TMBsyZqAVARwqbFlX2LLVli0PJXB/gWs4cYgX0qNdah6sffAK5+qg2M2f3Lsa9ukUonQMxqdv3Tvv6BflXM3DzvI8nKvAtJ4DOMrBXrh1quGyPs1zLzYiZ5UzT3VDcBb2qfdL9jaosM/D7VuyT1XWKrecSxc/X1cRrbks+lAuBiJ7poyRLMdG01lDuRjj3IxeTsU8PD8ywejlieEMlzFqwTQK08Vpqdy/AhXs1pInC4tqZBk7cMTvd44CrErnqDcevHwsu9vfw/0xZtMG9q+A3IZxagejy07+0UcwYKLJ3a8XCLE+PDuIZQeSTpoXh+1uG6IKy/Z2fOT58HIzMlYpqqs707WHrimh+/Qyzr3mg4zyAi+nq6iTJVk5g7UDIgeYrcDLbg2vQY3QIHBH8QY5Kzk+jCLPKS2w3rlYes/C7AMxhNl0DJHTmanQmZAdtw6TFk5GycTMspdP35z3OrACM8zSQFppoBQZaaCmFmO63STVCkTLeFdBNS1HtIx2TVNNLHmqqWZ+Uq0VqsmJ+df7icS2j29ynkkeHpTw3k0zOd6ottFbm4ZFHJBTwO/PV4x/KWLXFRF7t/i3fVzLjH+VwXry00tPK+Tk6U87iW1V2KmrNvV3ScjmAa+TYmk7juogihKoVk5toG6a+n8ytR29/1V2KVYH+XJwmg2azjdbQN0ADRg1WdxNPhHIuQNKYBDNkoOFKHCwmxwtvLi0jAlKbB/IHr3bRAKwT2IKF7FXKayi2nzqrm86Mh/5dGeaHIrY1NxsfNRJ9q1sNaqwv1urMcCn1VQZQ7XVDM/LavalVAly1tdgNapD591aTY3V2lnvzzbP2D3Bdyl9bgy6Cb6NisMt+56r3WBdTnLlgkEXXU0waJx8z86QV/epC/ThNkb7WvNaVo2TLIOWFHHobKriPFftr2Y01VczsS7T80jqb2YubpLbqUBorr2MMCtm36amzi37xBfc/wc=</diagram></mxfile>"><defs/><g><rect x="0" y="60" width="600" height="120" fill="#fff2cc" stroke="#d6b656" pointer-events="all"/><rect x="0" y="180" width="600" height="330" fill="#d5e8d4" stroke="#82b366" pointer-events="all"/><rect x="460" y="60" width="140" height="30" fill="none" stroke="none" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe flex-end; width: 138px; height: 1px; padding-top: 75px; margin-left: 460px;"><div style="box-sizing: border-box; font-size: 0; text-align: right; "><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; "><div align="right"><font style="font-size: 16px">Developer facing</font></div></div></div></div></foreignObject><text x="598" y="79" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="end">Developer facing</text></switch></g><rect x="470" y="180" width="130" height="30" fill="none" stroke="none" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe flex-end; width: 128px; height: 1px; padding-top: 195px; margin-left: 470px;"><div style="box-sizing: border-box; font-size: 0; text-align: right; "><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; "><div style="font-size: 16px" align="right"><font style="font-size: 16px">Internals</font></div></div></div></div></foreignObject><text x="598" y="199" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="end">Internals</text></switch></g><path d="M 250 150 L 250 190 L 120 190 L 120 203.63" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 120 208.88 L 116.5 201.88 L 120 203.63 L 123.5 201.88 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><rect x="190" y="90" width="120" height="60" fill="#f8cecc" stroke="#b85450" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 118px; height: 1px; padding-top: 120px; margin-left: 191px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; "><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">C++</div></div></div></foreignObject><text x="250" y="124" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">C++</text></switch></g><rect x="360" y="90" width="120" height="60" fill="#f8cecc" stroke="#b85450" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 118px; height: 1px; padding-top: 120px; margin-left: 361px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; "><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">JavaScript</div></div></div></foreignObject><text x="420" y="124" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">JavaScript</text></switch></g><path d="M 420 150 L 420 190 L 120 190 L 120 203.63" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 120 208.88 L 116.5 201.88 L 120 203.63 L 123.5 201.88 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><rect x="360" y="90" width="120" height="60" fill="#ffffff" stroke="#000000" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 118px; height: 1px; padding-top: 120px; margin-left: 361px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; "><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">JavaScript</div></div></div></foreignObject><text x="420" y="124" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">JavaScript</text></switch></g><path d="M 80 150 L 80 170 L 80 223.63" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 80 228.88 L 76.5 221.88 L 80 223.63 L 83.5 221.88 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><rect x="20" y="90" width="120" height="60" fill="#dae8fc" stroke="#6c8ebf" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 118px; height: 1px; padding-top: 120px; margin-left: 21px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; "><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; "><div>Rust</div></div></div></div></foreignObject><text x="80" y="124" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">Rust</text></switch></g><rect x="20" y="230" width="120" height="60" fill="#dae8fc" stroke="#6c8ebf" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 118px; height: 1px; padding-top: 260px; margin-left: 21px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; "><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">FOG API  </div></div></div></foreignObject><text x="80" y="264" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">FOG API  </text></switch></g><path d="M 140 470 L 160 470 L 150 470 L 163.63 470" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 168.88 470 L 161.88 473.5 L 163.63 470 L 161.88 466.5 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><rect x="20" y="440" width="120" height="60" fill="#dae8fc" stroke="#6c8ebf" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 118px; height: 1px; padding-top: 470px; margin-left: 21px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; "><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">FOG Control</div></div></div></foreignObject><text x="80" y="474" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">FOG Control</text></switch></g><rect x="170" y="440" width="120" height="60" fill="#dae8fc" stroke="#6c8ebf" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 118px; height: 1px; padding-top: 470px; margin-left: 171px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; "><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">Glean SDK</div></div></div></foreignObject><text x="230" y="474" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">Glean SDK</text></switch></g><rect x="480" y="0" width="120" height="20" fill="#dae8fc" stroke="#6c8ebf" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 118px; height: 1px; padding-top: 10px; margin-left: 481px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; "><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">Rust</div></div></div></foreignObject><text x="540" y="14" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">Rust</text></switch></g><rect x="480" y="30" width="120" height="20" fill="#f8cecc" stroke="#b85450" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 118px; height: 1px; padding-top: 40px; margin-left: 481px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; "><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">C++</div></div></div></foreignObject><text x="540" y="44" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">C++</text></switch></g><path d="M 230 220 L 270 260 L 230 300 L 190 260 Z" fill="#ffffff" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 78px; height: 1px; padding-top: 260px; margin-left: 191px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; "><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">IPC parent?</div></div></div></foreignObject><text x="230" y="264" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">IPC parent?</text></switch></g><path d="M 230 300 L 230 433.63" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 230 438.88 L 226.5 431.88 L 230 433.63 L 233.5 431.88 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><path d="M 140 260 L 183.63 260" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 188.88 260 L 181.88 263.5 L 183.63 260 L 181.88 256.5 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><rect x="230" y="310" width="40" height="20" fill="none" stroke="none" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 38px; height: 1px; padding-top: 320px; margin-left: 231px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; "><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">YES</div></div></div></foreignObject><text x="250" y="324" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">YES</text></switch></g><rect x="270" y="240" width="40" height="20" fill="none" stroke="none" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 38px; height: 1px; padding-top: 250px; margin-left: 271px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; "><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">NO</div></div></div></foreignObject><text x="290" y="254" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">NO</text></switch></g><rect x="360" y="230" width="230" height="200" fill="#f5f5f5" stroke="#666666" pointer-events="all"/><path d="M 270 260 L 460 260 Q 470 260 470 261.82 L 470 263.63" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 470 268.88 L 466.5 261.88 L 470 263.63 L 473.5 261.88 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><path d="M 470 290 L 470 310 L 470 290 L 470 303.63" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 470 308.88 L 466.5 301.88 L 470 303.63 L 473.5 301.88 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><rect x="410" y="270" width="120" height="20" fill="#dae8fc" stroke="#6c8ebf" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 118px; height: 1px; padding-top: 280px; margin-left: 411px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; "><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">transfer encoding</div></div></div></foreignObject><text x="470" y="284" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">transfer encoding</text></switch></g><path d="M 470 330 L 470 353.63" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 470 358.88 L 466.5 351.88 L 470 353.63 L 473.5 351.88 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><rect x="410" y="310" width="120" height="20" fill="#f8cecc" stroke="#b85450" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 118px; height: 1px; padding-top: 320px; margin-left: 411px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; "><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">IPC send</div></div></div></foreignObject><text x="470" y="324" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">IPC send</text></switch></g><path d="M 470 380 L 470 393.63" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 470 398.88 L 466.5 391.88 L 470 393.63 L 473.5 391.88 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><rect x="410" y="360" width="120" height="20" fill="#f8cecc" stroke="#b85450" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 118px; height: 1px; padding-top: 370px; margin-left: 411px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; "><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">IPC recv</div></div></div></foreignObject><text x="470" y="374" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">IPC recv</text></switch></g><path d="M 470 420 L 470 470 L 296.37 470" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 291.12 470 L 298.12 466.5 L 296.37 470 L 298.12 473.5 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><rect x="410" y="400" width="120" height="20" fill="#dae8fc" stroke="#6c8ebf" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 118px; height: 1px; padding-top: 410px; margin-left: 411px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; "><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">transfer decoding</div></div></div></foreignObject><text x="470" y="414" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">transfer decoding</text></switch></g><rect x="520" y="230" width="70" height="20" fill="none" stroke="none" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 68px; height: 1px; padding-top: 240px; margin-left: 521px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; "><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">IPC layer</div></div></div></foreignObject><text x="555" y="244" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">IPC layer</text></switch></g><rect x="100" y="210" width="40" height="20" fill="#f8cecc" stroke="#b85450" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 38px; height: 1px; padding-top: 220px; margin-left: 101px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; "><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; "><font style="font-size: 11px">C API</font></div></div></div></foreignObject><text x="120" y="224" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">C API</text></switch></g></g><switch><g requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"/><a transform="translate(0,-5)" href="https://desk.draw.io/support/solutions/articles/16000042487" target="_blank"><text text-anchor="middle" font-size="10px" x="50%" y="100%">Viewer does not support full SVG 1.1</text></a></switch></svg>
\ No newline at end of file diff --git a/toolkit/components/glean/docs/dev/index.md b/toolkit/components/glean/docs/dev/index.md new file mode 100644 index 0000000000..334f040ed9 --- /dev/null +++ b/toolkit/components/glean/docs/dev/index.md @@ -0,0 +1,15 @@ +# Developing Firefox on Glean + +This section of docs is designed to be helpful to people developing FOG. +If you're not touching code, tests, or documentation in `toolkit/components/glean`, +and you're not interested in implementation details, you probably want +[the user docs instead](../user/index). + +```{toctree} +:titlesonly: +:maxdepth: 1 +:glob: + +* +Glean SDK Source <https://github.com/mozilla/glean/> +``` diff --git a/toolkit/components/glean/docs/dev/ipc.md b/toolkit/components/glean/docs/dev/ipc.md new file mode 100644 index 0000000000..b571aa7275 --- /dev/null +++ b/toolkit/components/glean/docs/dev/ipc.md @@ -0,0 +1,183 @@ +# Inter-process Communication (IPC) + +Firefox Desktop is a multi-process desktop application. +Code requiring instrumentation may be on any of its processes, +so FOG provide facilities to do just that. + +## Design + +The IPC Design of FOG was worked out in +[bug 1618253](https://bugzilla.mozilla.org/show_bug.cgi?id=1618253). + +It centred around a few specific concepts: + +### Forbidding Non-Commutative Operations + +Because we cannot nicely impose a canonical ordering of metric operations across all processes, +FOG forbids non-[commutative](https://en.wikipedia.org/wiki/Commutative_property) +metric operations in some circumstances. + +For example, +`Add()`-ing to a Counter metric works from multiple processes because the order doesn't matter. +However, given a String metric being `Set()` from multiple processes simultaneously, +which value should it take? + +This ambiguity is not a good foundation to build trust on, +so we forbid setting a String metric from multiple processes. + +#### List of Forbidden Operations + +* Boolean's `set` (this is the metric type's only operation) +* Labeled Boolean's `set` (this is the metric type's only operation) +* String's `set` (this is the metric type's only operation) +* Labeled String's `set` (this is the metric type's only operation) +* String List's `set` + * `add` is permitted (order and uniqueness are not guaranteed) +* Timespan's `start`, `stop`, and `cancel` (these are the metric type's only operations) +* UUID's `set` and `generateAndSet` (these are the metric type's only operations) +* Datetime's `set` (this is the metric type's only operation) +* Quantity's `set` (this is the metric type's only operation) + +This list may grow over time as new metric types are added. +If there's an operation/metric type on this list that you need to use in a non-parent process, +please reach out +[on the #glean channel](https://chat.mozilla.org/#/room/#glean:mozilla.org) +and we'll help you out. + +### Process Agnosticism + +For metric types that can be used cross-process, +FOG provides no facility for identifying which process the instrumentation is on. + +What this means is that if you accumulate to a +[Timing Distribution](https://mozilla.github.io/glean/book/user/metrics/timing_distribution.html) +in multiple processes, +all the samples from all the processes will be combined in the same metric. + +If you wish to distinguish samples from different process types, +you will need multiple metrics and inline code to select the proper one for the given process. +For example: + +```C++ +if (XRE_GetProcessType() == GeckoProcessType_Default) { + mozilla::glean::performance::cache_size.Accumulate(numBytes / 1024); +} else { + mozilla::glean::performance::non_main_process_cache_size.Accumulate(numBytes / 1024); +} +``` + +### Scheduling + +FOG makes no guarantee about when non-main-process metric values are sent across IPC. +FOG will try its best to schedule opportunistically in idle moments, +and during orderly shutdowns. + +There are a few cases where we provide more firm guarantees: + +#### Tests + +There are test-only APIs in Rust, C++, and Javascript. +These do not await a flush of child process metric values. +You can use the test-only method `testFlushAllChildren` on the `FOG` +XPCOM component to await child data's arrival: +```js +await Services.fog.testFlushAllChildren(); +``` +See [the test documentation](testing) for more details on testing FOG. +For writing tests about instrumentation, see +[the instrumentation test documentation](../user/instrumentation_tests). + +#### Pings + +We do not guarantee that non-main-process data has made it into a specific ping. + +[Built-in pings](https://mozilla.github.io/glean/book/user/pings/index.html) +are submitted by the Rust Glean SDK at times FOG doesn't directly control, +so there may be data not present in the parent process when a built-in ping is submitted. +We don't anticipate this causing a problem since child-process data that +"misses" a given ping will be included in the next one. + +At this time, +[Custom Pings](https://mozilla.github.io/glean/book/user/pings/custom.html) +must be sent in the parent process and have no mechanism +to schedule their submission for after child-process data arrives in the parent process. +[bug 1732118](https://bugzilla.mozilla.org/show_bug.cgi?id=1732118) +tracks the addition of such a mechanism or guarantee. + +#### Shutdown + +We will make a best effort during an orderly shutdown to flush all pending data in child processes. +This means a disorderly shutdown (usually a crash) +may result in child process data being lost. + +#### Size + +We don't measure or keep an up-to-date calculation of the size of the IPC Payload. +We do, however, keep a count of the number of times the IPC Payload has been accessed. +This is used as a (very) conservative estimate of the size of the IPC Payload so we do not exceed the +[IPC message size limit](https://searchfox.org/mozilla-central/search?q=kMaximumMessageSize). + +See [bug 1745660](https://bugzilla.mozilla.org/show_bug.cgi?id=1745660). + +### Mechanics + +The rough design is that the parent process can request an immediate flush of pending data, +and each child process can decide to flush its pending data whenever it wishes. +The former is via `FlushFOGData() returns (ByteBuf)` and the latter via `FOGData(ByteBuf)`. + +Pending Data is a buffer of bytes generated by `bincode` in Rust in the Child, +handed off to C++, passed over IPC, +then given back to `bincode` in Rust on the Parent. + +Rust is then responsible for turning the pending data into +[metrics API][glean-metrics] calls on the metrics in the parent process. + +#### Supported Process Types + +FOG supports messaging between the following types of child process and the parent process: +* content children (via `PContent` + (for now. See [bug 1641989](https://bugzilla.mozilla.org/show_bug.cgi?id=1641989)) +* gmp children (via `PGMP`) +* gpu children (via `PGPU`) +* rdd children (via `PRDD`) +* socket children (via `PSocketProcess`) + +See +[the process model docs](/dom/ipc/process_model.rst) +for more information about what that means. + +### Adding Support for a new Process Type + +Adding support for a new process type is a matter of extending the two messages +mentioned above in "Mechanics" to another process type's protocol (ipdl file). + +1. Add two messages to the appropriate sections in `P<ProcessType>.ipdl` + * (( **Note:** `PGPU` _should_ be the only ipdl where `parent` + means the non-parent/-main/-UI process, + but double-check that you get this correct.)) + * Add `async FOGData(ByteBuf&& aBuf);` to the parent/main/UI process side of things + (most often `parent:`). + * Add `async FlushFOGData() returns (ByteBuf buf);` to the non-parent/-main/-UI side + (most often `child:`). +2. Implement the protocol endpoints in `P<ProcessType>{Child|Parent}.{h|cpp}` + * The message added to the `parent: ` section goes in + `P<ProcessType>Parent.{h|cpp}` and vice versa. +3. Add to `FOGIPC.cpp`'s `FlushAllChildData` code that + 1. Enumerates all processes of the newly-supported type (there may only be one), + 2. Calls `SendFlushFOGData on each, and + 3. Adds the resulting promise to the array. +4. Add to `FOGIPC.cpp`'s `SendFOGData` the correct `GeckoProcessType_*` + enum value, and appropriate code for getting the parent process singleton and calling + `SendFOGData` on it. +5. Add to the fog crate's `register_process_shutdown` function + handling for at-shutdown flushing of IPC data. + If this isn't added, we will log (but not panic) + on the first use of Glean APIs on an unsupported process type. + * "Handling" might be an empty block with a comment explaining where to find it + (like how `PROCESS_TYPE_DEFAULT` is handled) + * Or it might be custom code + (like `PROCESS_TYPE_CONTENT`'s) +6. Add to the documented [list of supported process types](#supported-process-types) + the process type you added support for. + +[glean-metrics]: https://mozilla.github.io/glean/book/reference/metrics/index.html diff --git a/toolkit/components/glean/docs/dev/jog.md b/toolkit/components/glean/docs/dev/jog.md new file mode 100644 index 0000000000..3fb5691593 --- /dev/null +++ b/toolkit/components/glean/docs/dev/jog.md @@ -0,0 +1,91 @@ +# Runtime Metric Definition Subsystem: JOG + +```{admonition} I'm Sorry +Why is it caled JOG? Because it's concerned with... run... time. +``` + +The normal mechanism for registering metrics, +for reasons as varied from ease-of-impl to performance, +happens at compile time. +However, this doesn't support use cases like +* [Artifact Builds][artifact-build] + (Where only the JavaScript of Firefox Desktop is repackaged at build time, + so there is no compile environment) +* Dynamic Telemetry + (A theorized system for instrumenting Firefox Desktop without shipping code) +* Web Extensions + (Or at least the kind that can't or won't use + [the Glean JS SDK][glean-js]) + +Thus we need a subsystem that supports the runtime registration of metrics. +We call it JOG and it was implemented in [bug 1698184][impl-bug]. + +## JavaScript Only + +Metrics in C++ and Rust are identified by identifiers which we can't swap out at runtime. +Thus, in order for changes to metrics to be visible to instrumented systems in C++ or Rust, you must compile. + +JavaScript, on the other hand, we supply instances to on-demand. +It not only supports the specific use cases driving this project, +it's the only environment that can benefit from runtime metric definition in Firefox Desktop. + +## Design + +The original design was done as part of +[bug 1662863][design-bug]. +Things have mostly just been refined from there. + +## Architecture + +We silo as much of the subsystem as we can into the +`jog` module located in `toolkit/components/glean/bindings/jog/`. +This includes the metrics construction factory and storage for metrics instances and their names and ids. + +Unfortunately, so that the metrics instances can be accessed by FFI, +the Rust metrics instances created by the `jog` crate are stored within the `fog` crate. + +Speaking of FFI, the `jog` crate is using cbindgen to be accessible to C++. + +If necessary or pleasant, it is probably possible to do away with the C++ storage, +moving the category set and metrics id map to Rust and moving information over FFI as needed. + +Test methods are run from `nsIFOG` (so we can use them in JS in xpcshell) +to static `JOG::` functions. + +### Build Integration + +If JOG detects we're an artifact build (by checking `COMPILE_ENVIRONMENT`), +it generates `jogfile.json` and ensures it is placed in `GreD` +(next to the `firefox` binary). + +`jogfile.json` includes only the metric and ping information necessary to register them at runtime. +(It doesn't know about tags or descriptions, just the shapes and names of things) + +This file is read the first time JS tries to get a metric category from the +`Glean` global or a ping from the `GleanPings` global. + +Yes, this is on the main thread. Yes, this is synchronous. Yes, this is file I/O. + +Since this is a developer build, we think this is worth it to support Artifact Builds. + +If we're wrong about this and there are additional conditions we should place JOG under, +please [contact us][glean-channel]. + +#### If things get weird, delete `objdir/dist/bin/jogfile.json` + +Sometimes, metrics or pings you've added may not appear when you run Firefox. +For these and other odd cases, the solution is the same: +delete `jogfile.json` from the `dist/bin` directory of your objdir, then try again. + +This shouldn't happen if you keep your artifact and non-artifact objdirs segregated +(as is good practice). + +If, despite doing things properly you still see this or something else odd, then that's a bug. +Please [file it in Toolkit :: Telemetry][file-bug] + +[artifact-build]: https://firefox-source-docs.mozilla.org/contributing/build/artifact_builds.html +[glean-js]: https://mozilla.github.io/glean/book/user/adding-glean-to-your-project/javascript.html +[impl-bug]: https://bugzilla.mozilla.org/show_bug.cgi?id=1698184 +[design-bug]: https://bugzilla.mozilla.org/show_bug.cgi?id=1662863 +[glean-channel]: https://chat.mozilla.org/#/room/#glean:mozilla.org +[file-bug]: https://bugzilla.mozilla.org/enter_bug.cgi?assigned_to=nobody%40mozilla.org&bug_ignored=0&bug_severity=--&bug_status=NEW&bug_type=defect&cf_a11y_review_project_flag=---&cf_fx_iteration=---&cf_fx_points=---&cf_performance_impact=---&cf_status_firefox106=---&cf_status_firefox107=---&cf_status_firefox108=---&cf_status_firefox_esr102=---&cf_status_thunderbird_esr102=---&cf_status_thunderbird_esr91=---&cf_tracking_firefox106=---&cf_tracking_firefox107=---&cf_tracking_firefox108=---&cf_tracking_firefox_esr102=---&cf_tracking_firefox_relnote=---&cf_tracking_thunderbird_esr102=---&cf_tracking_thunderbird_esr91=---&cf_webcompat_priority=---&component=Telemetry&contenttypemethod=list&contenttypeselection=text%2Fplain&defined_groups=1&filed_via=standard_form&flag_type-203=X&flag_type-37=X&flag_type-41=X&flag_type-607=X&flag_type-721=X&flag_type-737=X&flag_type-787=X&flag_type-799=X&flag_type-800=X&flag_type-803=X&flag_type-846=X&flag_type-855=X&flag_type-864=X&flag_type-930=X&flag_type-936=X&flag_type-937=X&flag_type-952=X&form_name=enter_bug&maketemplate=Remember%20values%20as%20bookmarkable%20template&op_sys=Unspecified&priority=--&product=Toolkit&rep_platform=Unspecified&short_desc=Problem%20with%20JOG%3A%20&target_milestone=---&version=unspecified diff --git a/toolkit/components/glean/docs/dev/local_glean.md b/toolkit/components/glean/docs/dev/local_glean.md new file mode 100644 index 0000000000..046cf6e79c --- /dev/null +++ b/toolkit/components/glean/docs/dev/local_glean.md @@ -0,0 +1,100 @@ +# Developing with a local Glean build + +FOG uses a release version of Glean, as published on [crates.io][cratesio-glean]. + +For local development and try runs you can replace this Glean implementation with a local or remote version. + +1. To tell `mach` where to find your Glean, patch the [top-level `Cargo.toml`][cargo-toml]. E.g. like this: + + ```toml + [patches.crates-io] + glean = { git = "https://github.com/myfork/glean", branch = "my-feature-branch" } + glean-core = { git = "https://github.com/myfork/glean", branch = "my-feature-branch" } + ``` + + Both crates are required to ensure they are in sync. + + You can specify the exact code to use by `branch`, `tag` or `rev` (Git commit). + See the [cargo documentation for details][cargo-doc]. + + You can also use a path dependency: + + ```toml + [patches.crates-io] + glean = { path = "../glean/glean-core/rlb" } + glean-core = { path = "../glean/glean-core" } + ``` + +2. If the crate version in the patched repository is not + [semver]-compatible with the version required by the + `fog` and `fog_control` crates, + you need to change the version in the following files to match the ones in your + `glean` repo: + + ``` + toolkit/components/glean/Cargo.toml + toolkit/components/glean/api/Cargo.toml + ``` + + This tells FOG's crates that it needs your local Glean's version. + +3. Update the Cargo lockfile: + + ``` + cargo update -p glean + ``` + +4. Mozilla's supply-chain management policy requires that third-party software + (which includes the Glean SDK because it is distributed as though it is third-party) + be audited and certified as safe. + Your local Glean SDK probably hasn't been vetted. If you try to vendor right now, + `./mach vendor rust` will complain something like: + + ``` + Vet error: There are some issues with your policy.audit-as-crates-io entries + ``` + + This is because your local Glean SDK is neither of a version nor is from a source that has been vetted. + To allow your local Glean crates to be treated as crates.io-sourced crates for vetting, + add the following sections to the top of `supply-chain/config.toml`: + + ```toml + [policy.glean] + audit-as-crates-io = true + + [policy.glean-core] + audit-as-crates-io = true + ``` + + If your local Glean is of a non-vetted version, you can update `glean` and + `glean-core`'s entries in `supply-chain/audits.toml` to the version you're using. + If you don't, `./mach vendor rust` will complain and not complete. + + **Note:** Do not attempt to check these changes in. + These changes bypass supply chain defenses. + `@supply-chain-reviewers` may become cross as they `r-` your patch. + +5. Vendor the changed crates: + + ``` + ./mach vendor rust + ``` + + **Note:** If you're using a path dependency, `mach vendor rust` doesn't actually change files. + Instead it pulls the files directly from the location on disk you specify. + +6. Finally, build Firefox: + + ``` + ./mach build + ``` + +A remote reference works for try runs as well, +but a path dependency will not. + +Please ensure to not land a non-release version of Glean. + +[cratesio-glean]: https://crates.io/crates/glean +[cargo-toml]: https://searchfox.org/mozilla-central/rev/f07a609a76136ef779c65185165ff5ac513cc172/Cargo.toml#76 +[cargo-doc]: https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#specifying-dependencies-from-git-repositories +[semver]: https://semver.org/ diff --git a/toolkit/components/glean/docs/dev/new_metric_types.md b/toolkit/components/glean/docs/dev/new_metric_types.md new file mode 100644 index 0000000000..c9195b5291 --- /dev/null +++ b/toolkit/components/glean/docs/dev/new_metric_types.md @@ -0,0 +1,292 @@ +# Adding a New Metric Type + +This document covers how to add a new metric type to FOG. +You should only have to do this if a new metric type is added to the +[Glean SDK](https://mozilla.github.io/glean/book/user/metrics/index.html) +and it is needed in Firefox Desktop. + +## IPC + +For detailed information about the IPC design, +including a list of forbidden operations, +please consult +[the FOG IPC documentation](ipc.md). + +When adding a new metric type, the main IPC considerations are: +* Which operations are forbidden because they are not commutative? + * Most `set`-style operations cannot be reconciled sensibly across multiple processes. +* If there are non-forbidden operations, +what partial representation will this metric have in non-main processes? +Put another way, what shape of storage will this take up in the +[IPC Payload](https://hg.mozilla.org/mozilla-central/file/tip/toolkit/components/glean/api/src/ipc.rs)? + * For example, Counters can aggregate all partial counts together to a single + "partial sum". So + [its representation](https://searchfox.org/mozilla-central/rev/803b368879fa332e8e2c1840bf1ec164f7ed2c32/toolkit/components/glean/api/src/ipc.rs#45) + in the IPC Payload is just a single number per Counter. + * In contrast, Timing Distributions' bucket arrangements are known only to the core, + so it can't combine sample counts in child processes. + Instead we record durations in the highest resolution (nanos), + and send a stream of high-precision samples across IPC. + +To implement IPC support in a metric type, +we split the metric into three pieces: +1. An umbrella `enum` with the name `MetricTypeMetric`. + * It has a `Child` and a `Parent` variant. + * It is IPC-aware and is responsible for + * If on a non-parent-process, + either storing partial representations in the IPC Payload, + or logging errors if forbidden non-test APIs are called. + (Or panicking if test APIs are called.) + * If on the parent process, dispatching API calls on its inner Rust Language Binding metric. +2. The parent-process implementation is supplied by + [the RLB](https://crates.io/crates/glean/). + * For testing, it stores the `MetricId` that identifies this particular metric in a cross-process fashion. + * For testing, it exposes a `child_metric()` function to create its `Child` equivalent. + * For testing and if it supports operations in a non-parent-process, + it exposes a `metric_id()` function to access the stored `MetricId`. +3. The `MetricTypeIpc` is the non-parent-process implementation. + * If it does support operations in non-parent processes it stores the + `MetricId` that identifies this particular metric in a cross-process fashion. + +## Mirrors + +FOG can mirror Glean metrics to Telemetry probes via the +[Glean Interface For Firefox Telemetry](../user/gifft.md). + +Can this metric type be mirrored? +Should it be mirrored? + +If so, add an appropriate Telemetry probe for it to mirror to, +documenting the compatibility in +[the GIFFT docs](../user/gifft.md). + +### GIFFT Tests + +If you add a GIFFT mirror, don't forget to test that the mirror works. +You should be able to do this by adding a task to +[`toolkit/components/glean/tests/xpcshell/test_GIFFT.js`](https://hg.mozilla.org/mozilla-central/file/tip/toolkit/components/glean/tests/xpcshell/test_GIFFT.js). + +### GIFFT C++ State: Typical Locking and Shutdown + +Some metric types (`labeled_*`, `timespan`, `timing_distribution`) +require holding state in C++ to make GIFFT work. +Pings also hold state to support `testBeforeNextSubmit()`. +If your new metric type requires state in C++, +the current state-of-the-art is a `StaticDataMutex`-locked `UniquePtr` to a `nsTHashTable`. +Access to the inner map is guarded by the lock and is controlled and lazily-instantiated through a single access function. +[See Ping's `GetCallbackMapLock()`](https://searchfox.org/mozilla-central/source/toolkit/components/glean/bindings/private/Ping.cpp) +for example. + +It is important to clear this state to avoid leaks. +(See [bug 1752417](https://bugzilla.mozilla.org/show_bug.cgi?id=1752417).) +However, instrumentation may call metrics APIs at any time. + +Therefore, GIFFT explicitly stops supporting these state-requiring operations after the +[`AppShutdownTelemetry` shutdown phase](https://searchfox.org/mozilla-central/source/xpcom/base/ShutdownPhase.h). +This is because during the next phase (`XPCOMWillShutdown`) we clear the state. + +## Rust + +FOG uses the Rust Language Binding APIs (the `glean` crate) with a layer of IPC on top. + +The IPC additions and glean-core trait implementations are in the +[`private` module of the `fog` crate](https://hg.mozilla.org/mozilla-central/file/tip/toolkit/components/glean/api/src/metrics). + +Each metric type gets its own file, mimicking the structure in +[`glean_core`](https://github.com/mozilla/glean/tree/main/glean-core/src/metrics) +and [`glean`](https://github.com/mozilla/glean/tree/main/glean-core/rlb/src/private). +Unless, of course, that metric is a labeled metric type. +Then the sub metric type gets its own file, +and you need to add "Labeledness" to it by implementing +`Sealed` for your new type following the pattern in +[`api/src/private/labeled.rs`](https://hg.mozilla.org/mozilla-central/file/tip/toolkit/components/glean/api/src/private/labeled.rs). + +Every method on the metric type is public for now, +including test methods, +and is at least all the methods exposed via the +[metric traits](https://github.com/mozilla/glean/tree/main/glean-core/src/traits). + +To support IPC and the MLA FFI (see below) +we identify metric instances by MetricId and store them in maps in +[the `__glean_metric_maps` mod of `metrics.rs`](https://hg.mozilla.org/mozilla-central/toolkit/components/glean/api/src/metrics.rs). +This work is done by the `rust.py` and `rust(_pings).jinja2` extensions to `glean_parser` found +[in the `build_scripts/glean_parser_ext/` folder](https://searchfox.org/mozilla-central/source/toolkit/components/glean/build_scripts/glean_parser_ext). + +You shouldn't have to edit these files for new metric types, +as the original modifications to `glean_parser` for this type should already be generating correct code. + +### Dealing with Clippy + +[Clippy](https://github.com/rust-lang/rust-clippy) +cannot find the generated Rust metrics maps in `__glean_metric_maps` +(see [bug 1674728](https://bugzilla.mozilla.org/show_bug.cgi?id=1674728)). +This means any new metric type that is generating structures via `glean_parser` +extensions requires you to add a copy of the new map to the clippy-only +`__glean_metric_maps` at the bottom of +[the non-generated `metrics.rs`](https://searchfox.org/mozilla-central/source/toolkit/components/glean/api/src/metrics.rs). + +### Rust Tests + +You should be able to smoke test the basic functionality in Rust unit tests. +You can do this within the metric type implementation file directly. + +## C++ and JS + +The C++ and JS APIs are implemented [atop the Rust API](code_organization.md). +We treat them both together since, though they're different languages, +they're both implemented in C++ and share much of their implementation. + +The overall design is to build the C++ API atop the Multi-Language Architecture's +(MLA's) FFI, then build the JS API atop the C++ API. +This allows features like the +[Glean Interface For Firefox Telemetry (GIFFT)](../user/gifft.md) +that target only C++ and JS to be more simply implemented in the C++ layer. +Exceptions to this (where the JS uses the FFI directly) are discouraged. + +Each metric type has six pieces you'll need to cover: + +### 1. MLA FFI + +- Using our convenient macros, + define the metric type's Multi-Language Architecture FFI layer above the Rust API in + [`api/src/ffi/`](https://hg.mozilla.org/mozilla-central/file/tip/toolkit/components/glean/api/src/ffi/). + +### 2. C++ Impl + +- Implement a type called `XMetric` (e.g. `CounterMetric`) in `mozilla::glean::impl` in + [`bindings/private/`](https://hg.mozilla.org/mozilla-central/file/tip/toolkit/components/glean/bindings/private/). + - Its methods should be named the same as the ones in the Rust API, transformed to `CamelCase`. + - They should all be public. + - Multiplex the FFI's `test_have` and `test_get` functions into a single + `TestGetValue` function that returns a + `mozilla::Maybe` wrapping the C++ type that best fits the metric type. +- Include the new metric type in + [`bindings/MetricTypes.h`](https://hg.mozilla.org/mozilla-central/file/tip/toolkit/components/glean/bindings/MetricTypes.h). +- Include the new files in + [`moz.build`](https://hg.mozilla.org/mozilla-central/file/tip/toolkit/components/glean/moz.build). + The header file should be added to `EXPORTS.mozilla.glean.bindings` and the + `.cpp` file should be added to `UNIFIED_SOURCES`. + +### 3. IDL + +- Duplicate the public API (including its docs) to + [`xpcom/nsIGleanMetrics.idl`](https://hg.mozilla.org/mozilla-central/file/tip/toolkit/components/glean/xpcom/nsIGleanMetrics.idl) + with the name `nsIGleanX` (e.g. `nsIGleanCounter`). + - Inherit from `nsISupports`. + - The naming style for members here is `lowerCamelCase`. + You'll need a + [GUID](https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Generating_GUIDs) + because this is XPCOM, but you'll only need the canonical form since we're only exposing to JS. + - The `testGetValue` method will return a + `jsval` to permit it to return `undefined` when there is no value. + +### 4. JS Impl + +- Add an `nsIGleanX`-deriving, `XMetric`-owning type called + `GleanX` (e.g. `GleanCounter`) in the same header and `.cpp` as `XMetric` in + [`bindings/private/`](https://hg.mozilla.org/mozilla-central/file/tip/toolkit/components/glean/bindings/private/). + - Don't declare any methods beyond a ctor + (takes a `uint32_t` metric id, init-constructs a `impl::XMetric` member) + and dtor (`default`): the IDL will do the rest so long as you remember to add + `NS_DECL_ISUPPORTS` and `NS_DECL_NSIGLEANX`. + - In the definition of `GleanX`, member identifiers are back to + `CamelCase` and need macros like `NS_IMETHODIMP`. + Delegate operations to the owned `XMetric`, returning + `NS_OK` no matter what in non-test methods. + - Test-only methods can return `NS_ERROR` codes on failures, + but mostly return `NS_OK` and use `undefined` in the + `JS::MutableHandleValue` result to signal no value. + +### 6. Tests + +Two languages means two test suites. + +- Add a never-expiring test-only metric of your type to + [`test_metrics.yaml`](https://hg.mozilla.org/mozilla-central/file/tip/toolkit/components/glean/tests/test_metrics.yaml). + - Feel free to be clever with the name, + but be sure to make clear that it is test-only. +- **C++ Tests (GTest)** - Add a small test case to + [`gtest/TestFog.cpp`](https://hg.mozilla.org/mozilla-central/file/tip/toolkit/components/glean/tests/gtest/TestFog.cpp). + - For more details, peruse the [testing docs](testing.md). +- **JS Tests (xpcshell)** - Add a small test case to + [`xpcshell/test_Glean.js`](https://hg.mozilla.org/mozilla-central/file/tip/toolkit/components/glean/tests/xpcshell/test_Glean.js) + and + [`xpcshell/test_JOG.js`](https://hg.mozilla.org/mozilla-central/file/tip/toolkit/components/glean/tests/xpcshell/test_JOG.js). + If your metric type has supported IPC operations, also add cases to the `IPC` variants of these test files. + - For more details, peruse the [testing docs](testing.md). + +### 7. API Documentation + +Metric API Documentation is centralized in +[the Glean SDK Book](https://mozilla.github.io/glean/book/user/metrics/index.html). + +You will need to craft a Pull Request against +[the SDK](https://github.com/mozilla/glean/) +adding a C++ and JS example to the specific metric type's API docs. + +Add a notice at the top of both examples that these APIs are only available in Firefox Desktop: +````md +<div data-lang="C++" class="tab"> + +> **Note**: C++ APIs are only available in Firefox Desktop. + +```c++ +#include "mozilla/glean/GleanMetrics.h" + +mozilla::glean::category_name::metric_name.Api(args); +``` + +There are test APIs available too: + +```c++ +#include "mozilla/glean/GleanMetrics.h" + +ASSERT_EQ(value, mozilla::glean::category_name::metric_name.TestGetValue().ref()); +``` +</div> + +// and again for <div data-lang="JS"> +```` + +If you're lucky, the Rust API will have already been added. +Otherwise you'll need to write an example for that one too. + +### 8. Labeled metrics (if necessary) + +If your new metric type is Labeled, you have more work to do. +I'm assuming you've already implemented the non-labeled sub metric type following the steps above. +Now you must add "Labeledness" to it. + +There are four pieces to this: + +#### FFI + +- To add the writeable storage Rust will use to store the dynamically-generated sub metric instances, + add your sub metric type's map as a list item in the `submetric_maps` `mod` of + [`rust.jinja2`](https://hg.mozilla.org/mozilla-central/file/tip/toolkit/components/glean/build_scripts/glean_parser_ext/templates/rust.jinja2). +- Following the pattern of the others, add a `fog_{your labeled metric name here}_get()` FFI API to + `api/src/ffi/mod.rs`. + This is what C++ and JS will use to allocate and retrieve sub metric instances by id. + +#### C++ + +- Following the pattern of the others, add a template specialiation for `Labeled<YourSubMetric>::Get` to + [`bindings/private/Labeled.cpp`](https://hg.mozilla.org/mozilla-central/file/tip/toolkit/components/glean/bindings/private/Labeled.cpp). + This will ensure C++ consumers can fetch or create sub metric instances. + +#### JS + +- Already handled for you since the JS types all inherit from `nsISupports` + and the JS template knows to add your new type to `NewSubMetricFromIds(...)` + (see `GleanLabeled::NamedGetter` if you're curious). + +#### Tests + +- The labeled variant will need tests the same as Step #6. + A tip: be sure to test two labels with different values. + +## Python Tests + +We have a suite of tests for ensuring code generation generates appropriate code. +You should add a metric to [that suite](testing.md) for your new metric type. +You will need to regenerate the expected files. diff --git a/toolkit/components/glean/docs/dev/preferences.md b/toolkit/components/glean/docs/dev/preferences.md new file mode 100644 index 0000000000..97a3e91e92 --- /dev/null +++ b/toolkit/components/glean/docs/dev/preferences.md @@ -0,0 +1,90 @@ +# Preferences and Defines + +## User Preferences + +`datareporting.healthreport.uploadEnabled` + +This determines whether the Glean SDK is enabled. +It can be controlled by users via `about:preferences#privacy`. +If this is set to false from true, we send a +["deletion-request" ping](https://mozilla.github.io/glean/book/user/pings/deletion_request.html) +and no data collections will be persisted or reported from that point. + +## Test-only Preferences + +`telemetry.fog.test.localhost_port` + +If set to a value `port` which is greater than 0, pings will be sent to +`http://localhost:port` instead of `https://incoming.telemetry.mozilla.org`. +If set to a value `port` which is less than 0, FOG will: +1) Tell Glean that upload is enabled, even if it isn't. +2) Take all pings scheduled for upload and drop them on the floor, + telling the Glean SDK that it was sent successfully. + +This is how you emulate "recording enabled but upload disabled" +like developer builds have in Firefox Telemetry. +When switching from `port < 0` to `port >= 0`, +Glean will be told (if just temporarily) that upload is disabled. +This clears the stores of recorded-but-not-reported data. +Defaults to 0. + +`telemetry.fog.test.activity_limit` +`telemetry.fog.test.inactivity_limit` + +This pair of prefs control the length of time of activity before inactivity +(or vice versa) +needed before FOG informs the SDK's Client Activity API that the client was (in)active. +Present to allow testing without figuring out how to mock Rust's clock. +Their values are integer seconds. +Defaults to 120 (activity), 1200 (inactivity). + +## Defines + +`MOZ_GLEAN_ANDROID` + +If set, recording Glean metrics are a no-op. Glean will not be initialized. +Only set on Android. +This define will be removed after we sort out how Android and Geckoview will work +(see [bug 1670261](https://bugzilla.mozilla.org/show_bug.cgi?id=1670261)). +It can be queried in C++ via `#ifndef MOZ_GLEAN_ANDROID`, +and in JS via `AppConstants.MOZ_GLEAN_ANDROID`. + +`MOZILLA_OFFICIAL` + +If unset, we set a `glean_disable_upload` Rust feature in +`gkrust` and `gkrust-shared` which is forwarded to `fog_control` as `disable_upload`. +This feature defaults FOG to an "upload disabled" +mode where collection on the client proceeds as normal but no ping is sent. +This mode can be overridden at runtime in two ways: +* If the ping has a + [Debug Tag](https://mozilla.github.io/glean/book/user/debugging/index.html) + then it is sent so that it may be inspected in the + [Glean Debug Ping Viewer](https://debug-ping-preview.firebaseapp.com/). +* If the preference `telemetry.fog.test.localhost_port` is set to a value greater than 0, + then pings are sent to a server operating locally at that port + (even if the ping has a Debug Tag), to enable testing. + +Also, if set, [JOG](./jog) is disabled. +Artifact builds will not exhibit changes to their Glean metrics. + +`MOZILLA_OFFICIAL` tends to be set on most builds released to users, +including builds distributed by Linux distributions. +It tends to not be set on local developer builds. +See [bug 1680025](https://bugzilla.mozilla.org/show_bug.cgi?id=1680025) for details. + +`COMPILE_ENVIRONMENT` + +If `COMPILE_ENVIRONMENT` isn't set in the build config, +[JOG](./jog) will generate a file for the runtime-registration of metrics and pings. +This is to support [Artifact Builds](/contributing/build/artifact_builds). + +`OS_TARGET` + +If not set to `'Android'` we set a `glean_million_queue` Rust feature +([see gkrust-features.mozbuild][gkrust-features]) +which, when passed to the Glean SDK, +opts us into a preinit queue that doesn't discard tasks until there are 10^6 of them. + +See [bug 1797494](https://bugzilla.mozilla.org/show_bug.cgi?id=1797494) for details. + +[gkrust-features]: https://searchfox.org/mozilla-central/source/toolkit/library/rust/gkrust-features.mozbuild diff --git a/toolkit/components/glean/docs/dev/storage.md b/toolkit/components/glean/docs/dev/storage.md new file mode 100644 index 0000000000..00464c6173 --- /dev/null +++ b/toolkit/components/glean/docs/dev/storage.md @@ -0,0 +1,14 @@ +# Storage + +Both FOG and the Glean SDK require some storage in the +[Firefox Profile Directory](https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Multiple_profiles). + +## FOG + +At present FOG's storage is limited to its [preferences](preferences.md). + +## Glean SDK + +The Glean SDK stores things in the +[Glean Data Directory](https://mozilla.github.io/glean/book/dev/core/internal/directory-structure.html) +which can be found at `<profile_dir>/datareporting/glean`. diff --git a/toolkit/components/glean/docs/dev/style_guide.md b/toolkit/components/glean/docs/dev/style_guide.md new file mode 100644 index 0000000000..c289919f93 --- /dev/null +++ b/toolkit/components/glean/docs/dev/style_guide.md @@ -0,0 +1,41 @@ +# FOG Documentation Style Guide + +FOG's Documentation is written in Markdown. +You can find its source at `toolkit/components/glean/docs`. + +## Line breaks + +We will use [semantic linefeeds]: +* Break anywhere before 80-100 characters +* Break after any punctuation when it makes sense +* Break before or after any markdown when it makes sense + +**Tip:** To keep lines narrow, use markdown's [reference link] +feature whenever it makes sense (or all the time. Up to you.). + +## Linking to other documentation + +Linking to other external documentation is [easy][reference link]. +Linking to other pieces of documentation in the source docs requires a +link to the source file in the sphinx tree. + +Links can be relative e.g. to link to the [preferences] docs: + +```md +[preferences](preferences.md) +``` + +Or they can be absolute e.g. to link to the [Telemetry] docs: +```md +[Telemetry](/toolkit/components/telemetry/index.rst) +``` + +Sphinx will automagically transform that to an +appropriately-base-url'd url with a `.html` suffix. + + +[semantic linefeeds]: https://rhodesmill.org/brandon/2012/one-sentence-per-line/ +[reference link]: https://spec.commonmark.org/0.29/#reference-link +[Telemetry]: /toolkit/components/telemetry/index.rst +[#firefox-source-docs:mozilla.org]: https://chat.mozilla.org/#/room/#firefox-source-docs:mozilla.org +[bug 1621950]: https://bugzilla.mozilla.org/show_bug.cgi?id=1621950 diff --git a/toolkit/components/glean/docs/dev/testing.md b/toolkit/components/glean/docs/dev/testing.md new file mode 100644 index 0000000000..9ce9287a00 --- /dev/null +++ b/toolkit/components/glean/docs/dev/testing.md @@ -0,0 +1,246 @@ +# Testing + +```{admonition} This documentation is about testing FOG itself +This document contains information about how FOG tests itself, +how to add new tests, how and what to log, and stuff like that. +If you're interested in learning how to test instrumentation you added, +you'll want to read +[the instrumetnation testing docs](../user/instrumentation_tests) instead. +``` + +Given the multiple API languages, processes, and dependencies, +testing FOG is a matter of choosing the right tool for the situation. + +## One Big Command + +To run all the things, here's the tl;dr: + +`./mach build && ./mach lint -Ww -o --fix +&& ./mach lint --linter clippy toolkit/components/glean/api/src +&& ./mach rusttests && ./mach gtest "FOG*" +&& python3 ./mach python-test toolkit/components/glean/tests/pytest +&& ./mach test toolkit/components/glean/tests/xpcshell +&& ./mach telemetry-tests-client toolkit/components/telemetry/tests/marionette/tests/client/test_fog* --gecko-log "-" +&& ./mach test toolkit/components/glean/tests/browser +` + +## Logging + +An often-overlooked first line of testing is "what do the logs say?". +To turn on logging for FOG, use any of the following: +* Run Firefox with `RUST_LOG="fog_control,fog,glean_core"`. + * On some platforms this will use terminal colours to indicate log level. +* Run Firefox with `MOZ_LOG="timestamp,sync,glean::*:5,fog::*:5,fog_control::*:5,glean_core::*:5"`. +* Set the following prefs: + * `logging.config.timestamp` to `true` + * `logging.config.sync` to `true` + * `logging.fog_control::*` to `5` + * `logging.fog::*` to `5` + * `logging.glean::*` to `5` + * `logging.glean_core::*` to `5` + * `logging.config.clear_on_startup` to `false` (or all these prefs will be cleared on startup) + +For more information on logging in Gecko, see the +[Gecko Logging docs](/xpcom/logging). + +User-destined logs (of the "You did something wrong" variety) might output to the +[Browser Console](/devtools-user/browser_console/index) +if they originate in JS land. Open via +<kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>J</kbd>, or +<kbd>Cmd</kbd>+<kbd>Shift</kbd>+<kbd>J</kbd>. + +```{admonition} Note +At present, Rust logging in non-main processes just doesn't work. +``` + +### What to log, and to where? + +FOG covers a lot a ground (languages, layers): +where you are determines what logging you have available. + +Here are some common situtations for logging: + +#### JS to C++ + +If your logging is aimed at the user using the JS API +(e.g. because the type provided isn't convertable to the necessary C++ type) +then use the Browser Console via +[FOG's Common's `LogToBrowserConsole`](https://searchfox.org/mozilla-central/rev/d107bc8aeadcc816ba85cb21c1a6a1aac1d4ef9f/toolkit/components/glean/bindings/private/Common.cpp#19). + +#### C++ + +If you are in C++ and didn't come from JS, use `MOZ_LOG` with module `fog`. + +#### Rust + +Use the logging macros from `log`, e.g. `log::info!` or `log::error!`. +Remember that, no matter the log level, `log::debug!` and `log::trace!` +[will not appear in non-debug builds](/testing-rust-code/index.html#gecko-logging) + +If you are logging due to a situation caused by and fixable by a developer using the API, +use `log::error!(...)`. Otherwise, use a quieter level. + +## `about:glean` + +`about:glean` is a page in a running Firefox that allows you to +[debug the Glean SDK](https://mozilla.github.io/glean/book/user/debugging/index.html) +in Firefox Desktop. +It does this through the displayed user interface (just follow the instructions). + +## Linting + +To keep in accordance with Mozilla's various and several Coding Styles, +we rely on `mach lint`. + +To lint the code in the "usual" way, automatically fixing where possible, run: +`./mach lint -Ww -o --fix` + +This should keep you from checking in code that will automatically be backed out. + +In addition, we need to run the Rust formatter `clippy` on the `fog` crate: +`./mach lint --linter clippy toolkit/components/glean/api/src` + +This will ensure that clippy-only builds will have all the symbols they need to lint our code. + +## Rust + +Not all of our Rust code can be tested in a single fashion, unfortunately. + +### Using `rusttests` (Treeherder symbol `Br` (a build task)) + +If the crate you're testing has no Gecko symbols you can write standard +[Rust tests](https://doc.rust-lang.org/book/ch11-01-writing-tests.html). + +This supports both unit tests +(inline in the file under test) and integration tests +(in the `tests/` folder in the crate root). +Metric type tests are currently written as unit tests inline in the file, +as they require access to the metric ID, which should only be exposed in tests. + +To run FOG's `rusttests` suite use `mach rusttests` + +If the crate uses only a few Gecko symbols, they may use the +`with_gecko` feature to conditionally use them. +This allows the crate to test its non-Gecko-adjacent code using Rust tests. +(You will need to cover the Gecko-adjacent code via another means.) + +**Note:** Some FOG rusttests panic on purpose. They print stack traces to stdout. +If the rusttests fail and you see a stack trace, +double-check it isn't from a purposefully-panicking test. + +**Note:** If a test fails, it is very likely they'll poison the test lock. +This will cause all subsequent tests that attempt to take the test lock +(which is all of them) +to also fail due to `PoisonError`s. They can be safely ignored. + +### Using `gtest` (Treeherder symbol `GTest` (a build task)) + +Because Gecko symbols aren't built for the +`rusttests` build, +any test that is written for code that uses Gecko symbols should be written as a +[`gtest`](https://github.com/google/googletest) +in `toolkit/components/glean/tests/gtest/`. +You can write the actual test code in Rust. +It needs to be accompanied by a C++ GTest that calls a C FFI-exported Rust function. +See [Testing & Debugging Rust Code](/testing-rust-code/index.md) for more. +See [`toolkit/components/glean/tests/gtest/TestFog.cpp`](https://searchfox.org/mozilla-central/source/toolkit/components/glean/tests/gtest/TestFog.cpp) +and [`toolkit/components/glean/tests/gtest/test.rs`](https://searchfox.org/mozilla-central/source/toolkit/components/glean/tests/gtest/test.rs) +for an example. + +By necessity these can only be integration tests against the compiled crate. + +**Note:** When adding a new test file, don't forget to add it to +`toolkit/components/glean/tests/gtest/moz.build` and use the +`FOG` prefix in your test names +(e.g. `TEST(FOG, YourTestName) { ... }`). + +To run FOG's Rust `gtest` suite use `mach gtest FOG.*` + +## Python (Treeherder symbol `py3(fp)` aka `source-test-python-fog`) + +The [Glean Parser](https://github.com/mozilla/glean_parser/) +has been augmented to generate FOG-specific APIs for Glean metrics. +This augmentation is tested by running: + +`mach test toolkit/components/glean/tests/pytest` + +These tests require Python 3+. +If your default Python is Python 2, you may need to instead run: + +`python3 mach python-test toolkit/components/glean/tests/pytest` + +These tests check the code generator output against known good file contents. +If you change the code generator the files will need an update. +Run the test suite with the `UPDATE_EXPECT` environment variable set to do that automatically: + +`UPDATE_EXPECT=1 mach test toolkit/components/glean/tests/pytest` + +## C++ (Treeherder symbol `GTest` (a build task)) + +To test the C++ parts of FOG's implementation +(like metric types) +you should use `gtest`. +FOG's `gtest` tests are in +[`gtest/`](https://hg.mozilla.org/mozilla-central/file/tip/toolkit/components/glean/tests/gtest/). + +You can either add a test case to an existing file or add a new file. +If you add a new file, remember to add it to the +[`moz.build`](https://hg.mozilla.org/mozilla-central/file/tip/toolkit/components/glean/tests/gtest/moz.build)) +or the test runner won't be able to find it. + +All tests should start with `FOG` so that all tests are run with +`./mach gtest FOG*`. + +## JS (Treeherder symbol `X(Xn)` for some number `n`) + +To test the JS parts of FOG's implementation +(like metric types) +you should use `xpcshell`. +FOG's `xpcshell` tests are in +[`xpcshell/`](https://hg.mozilla.org/mozilla-central/file/tip/toolkit/components/glean/tests/xpcshell). + +You can either add a test case to an existing file or add a new file. +If you add a new file, remember to add it to the +[`xpcshell.ini`](https://hg.mozilla.org/mozilla-central/file/tip/toolkit/components/glean/tests/xpcshell/xpcshell.ini) +or the test runner will not be able to find it. + +To run FOG's JS tests, run: +`./mach test toolkit/components/glean/tests/xpcshell` + +## Non-content-process multiprocess (Browser Chrome Mochitests with Treeherder symbol `M(bcN)` for some number `N`) + +To test e.g. the GPU process support you need a full Firefox browser: +xpcshell doesn't have the flexibility. +To test that and have access to privileged JS (i.e. `Glean` and `FOG` APIs), +we use browser-chrome-flavoured mochitests you can find in +[`browser/`](https://hg.mozilla.org/mozilla-central/file/tip/toolkit/components/glean/tests/browser). + +If you need to add a new test file, remember to add it to the +[`browser.ini`](https://hg.mozilla.org/mozilla-central/file/tip/toolkit/components/glean/tests/browser/browser.ini) +manifest, or the test runner will not be able to find it. + +To run FOG's browser chrome tests, run: +`./mach test toolkit/components/glean/tests/browser` + +## Integration (Marionette, borrowing `telemetry-tests-client` Treeherder symbol `tt(c)`) + +To test pings (See [bug 1681742](https://bugzilla.mozilla.org/show_bug.cgi?id=1681742)) +or anything that requires one or more full browsers running, +we use the `telemetry-tests-client` suite in +[`toolkit/components/telemetry/tests/marionette/tests/client/`](https://hg.mozilla.org/mozilla-central/file/tip/toolkit/components/telemetry/tests/marionette/tests/client/). + +For more information on this suite, look to +[Firefox Telemetry's Test Documentation](https://firefox-source-docs.mozilla.org/toolkit/components/telemetry/internals/tests.html#integration-tests-telemetry-tests-client-and-telemetry-integration-tests) +and +[Marionette's Documentation](/testing/marionette/Testing.md). + +To run these integration tests, run: +`./mach telemetry-tests-client toolkit/components/telemetry/tests/marionette/tests/client/` + +To capture the Firefox under test's logs, use the `--gecko-log` parameter. +For example, to echo to stdout: +`./mach telemetry-tests-client toolkit/components/telemetry/tests/marionette/tests/client/test_fog* --gecko-log "-"` + +**Note:** Running the `tt(c)` suite in this way ignored skip directives in the manifest. +This means that you might run tests that are not expected to succeed on your platform. +Check `toolkit/components/telemetry/tests/marionette/tests/client/manifest.ini` for details. diff --git a/toolkit/components/glean/docs/dev/updating_parser.md b/toolkit/components/glean/docs/dev/updating_parser.md new file mode 100644 index 0000000000..4609f76a80 --- /dev/null +++ b/toolkit/components/glean/docs/dev/updating_parser.md @@ -0,0 +1,52 @@ +# Updating glean_parser + +Project FOG uses the `glean_parser` to generate code from metric definitions. +It depends on [glean-parser] from pypi.org + +[glean-parser]: https://pypi.org/project/glean-parser/ + +To update the in-tree glean-parser change the version in `third_party/python/requirements.in`, +then run + +``` +./mach vendor python +``` + +```{note} +**Important**: the glean_parser and all of its dependencies must support Python 3.5, as discussed here. +This is the minimum version supported by mach and installed on the CI images for running tests. +This is enforced by the version ranges declared in the Python installation manifest. +``` + +## Version mismatch of the Python dependencies + +The logic for handling version mismatches is very similar to the one for the Rust crates. +See [Updating the Glean SDK](updating_sdk.md) for details. +However, updating Python packages also requires to think about Python 3.5 (and Python 2, still) compatibility. + +## Keeping versions in sync + +The Glean SDK and `glean_parser` are currently released as separate projects. +However each Glean SDK release requires a specific `glean_parser` version. +When updating one or the other ensure versions stay compatible. +You can find the currently used `glean_parser` version in the Glean SDK source tree, e.g. in [sdk_generator.sh]. + +[sdk_generator.sh]: https://github.com/mozilla/glean/blob/main/glean-core/ios/sdk_generator.sh#L28 + +## Using a local `glean_parser` development version + +To test out a new `glean_parser` in mozilla-central follow these steps: + +1. Remove `glean_parser` from the user-wide virtual environment. + This can be found in a path like `~/.mozbuild/srcdirs/gecko-f5e3b9c6ded5/_virtualenvs/mach/lib/python3.10/site-packages/glean_parser` + Note that the `gecko-f5e3b9c6ded5` part will be different depending on your local checkout. + Remove all directories and files mentioning `glean_parser` +2. Remove `glean_parser` from the build virtual enviromment. + This can be found in `$MOZ_OBJDIR/_virtualenvs/common/lib/python3.6/site-packages/glean_parser`. + Note that `$MOZ_OBJDIR` depends on your local mozconfig configuration. + Remove all directories and files mentioning `glean_parser` +3. Copy the local `glean_parser` checkout into `third_party/python/glean_parser`. + E.g. `cp ~/code/glean_parser $GECKO/third_party/python/glean_parser`. + +You should now be able to build `mozilla-central` and it will use the modified `glean_parser`. +You can make further edits in `$GECKO/third_party/python/glean_parser`. diff --git a/toolkit/components/glean/docs/dev/updating_sdk.md b/toolkit/components/glean/docs/dev/updating_sdk.md new file mode 100644 index 0000000000..fafec08d39 --- /dev/null +++ b/toolkit/components/glean/docs/dev/updating_sdk.md @@ -0,0 +1,48 @@ +# Updating the Glean SDK + +Project FOG uses the Glean SDK published as the [`glean`][glean-crate] +and [`glean-core`][glean-core] crates on crates.io. + +[glean-crate]: https://crates.io/crates/glean +[glean-core]: https://crates.io/crates/glean-core + +These two crates' versions are included in several places in mozilla-central. +To update them all, you should use the command +`mach update-glean <version, like "54.1.0">`. + +This is a semi-manual process. +Please pay attention to the output of `mach update-glean` for instructions, +and follow them closely. + +## Version mismatches of Rust dependencies + +Other crates that are already vendored might require a different version of the same dependencies that the Glean SDK requires. +The general strategy for Rust dependencies is to keep one single version of the dependency in-tree +(see [comment #8 in bug 1591555](https://bugzilla.mozilla.org/show_bug.cgi?id=1591555#c8)). +This might be hard to do in reality since some dependencies might require tweaks in order to work. +The following strategy can be followed to decide on version mismatches: + +* If the versions only **differ by the patch version**, Cargo will keep the vendored version, + unless some other dependency pinned specific patch versions; + assuming it doesn’t break the Glean SDK; + if it does, follow the next steps; +* If the version of the **vendored dependency is newer** (greater major or minor version) than the version required by the Glean SDK, + [file a bug in the Glean SDK component][glean-bug] to get Glean to require the same version; + * You will have to abandon updating the Glean SDK to this version. + You will have to wait for Glean SDK to update its dependency and for a new Glean SDK release. + Then you will have to update to that new Glean SDK version. +* If the version of the **vendored dependency is older** (lower major or minor version), consider updating the vendored version to the newer one; + seek review from the person who vendored that dependency in the first place; + if that is not possible or breaks mozilla-central build, then consider keeping both versions vendored in-tree; please note that this option will probably only be approved for small crates, + and will require updating the `TOLERATED_DUPES` list in `mach vendor` + (instructions are provided as you go). + +## Keeping versions in sync + +The Glean SDK and `glean_parser` are currently released as separate projects. +However each Glean SDK release requires a specific `glean_parser` version. +When updating one or the other ensure versions stay compatible. +You can find the currently used `glean_parser` version in the Glean SDK source tree, e.g. in [sdk_generator.sh]. + +[sdk_generator.sh]: https://github.com/mozilla/glean/blob/main/glean-core/ios/sdk_generator.sh#L28 +[glean-bug]: https://bugzilla.mozilla.org/enter_bug.cgi?product=Data+Platform+and+Tools&component=Glean%3A+SDK&priority=P3&status_whiteboard=%5Btelemetry%3Aglean-rs%3Am%3F%5D diff --git a/toolkit/components/glean/docs/index.md b/toolkit/components/glean/docs/index.md new file mode 100644 index 0000000000..c9d8f42eb4 --- /dev/null +++ b/toolkit/components/glean/docs/index.md @@ -0,0 +1,30 @@ +# Firefox on Glean (FOG) + +Firefox on Glean (FOG) is the name of the layer that integrates the +[Glean SDK][glean-sdk] into +[Firefox Desktop](https://www.firefox.com/). + +The [Glean SDK][glean-sdk] +is a data collection library built by Mozilla for use in its products. +Like [Telemetry][telemetry], it can be used to +(in accordance with our [Privacy Policy][privacy-policy]) +send anonymous usage statistics to Mozilla in order to make better decisions. + +If you have any questions, +please reach out to the team on +[#glean:mozilla.org][glean-matrix]. + +```{toctree} +:titlesonly: +:maxdepth: 2 +:glob: + +user/index +dev/index +``` + +[telemetry]: ../telemetry/index +[glean-sdk]: https://github.com/mozilla/glean/ +[book-of-glean]: https://mozilla.github.io/glean/book/index.html +[privacy-policy]: https://www.mozilla.org/privacy/ +[glean-matrix]: https://chat.mozilla.org/#/room/#glean:mozilla.org diff --git a/toolkit/components/glean/docs/user/getting_started.md b/toolkit/components/glean/docs/user/getting_started.md new file mode 100644 index 0000000000..50b955bced --- /dev/null +++ b/toolkit/components/glean/docs/user/getting_started.md @@ -0,0 +1,97 @@ +# Getting Started with Firefox on Glean (FOG) + +This documentation is designed to be helpful to those who are +* New to data collection in Firefox Desktop, +* Experienced with data collection in Firefox Desktop, but not the Glean kind +* Those who are just interested in a refresher. + +## What is FOG? + +Firefox on Glean (FOG) is the library that brings +[the Glean SDK](https://mozilla.github.io/glean/book/index.html), +Mozilla's modern data collection system, +to Firefox Desktop. + +FOG's code is in `toolkit/components/glean` and is considered part of the +`Toolkit :: Telemetry` [module][modules]. +Bugs against FOG can be [filed][file-fog-bugs] +in Bugzilla in the `Toolkit` product and the `Telemetry` component. +(No bugs about adding new instrumentation, please. +You can file those in the components that you want instrumented.) +You can find folks who can help answer your questions about FOG in +* [#glean:mozilla.org](https://chat.mozilla.org/#/room/#glean:mozilla.org) +* [#telemetry:mozilla.org](https://chat.mozilla.org/#/room/#telemetry:mozilla.org) +* Slack#data-help + +On top of the usual things Glean embedders supply +(user engagement monitoring, network upload configuration, data upload preference watching, ...) +FOG supplies Firefox-Desktop-specific things: +* Privileged JS API +* C++ API +* IPC +* Test Preferences +* Support for `xpcshell`, browser-chrome mochitests, GTests, and rusttests +* `about:glean` +* ...and more. + +## What do I need to know about Glean? + +You use the APIs supplied by the Glean SDK to instrument Mozilla projects. + +The unit of instrumentation is the **metric**. +Recording the number of times a user opens a new tab? That's a metric. +Timing how long each JS garbage collector pass takes? Also a metric. + +Glean has documentation about +[how to add a new metric][add-a-metric] +that you should follow to learn how to add a metric to instrument Firefox Desktop. +There are some [peculiarities specific to Firefox Desktop](new_definitions_file) +that you'll wish to review as well. +Don't forget to get [Data Collection Review][data-review] +for any new or expanded data collections in mozilla projects. + +By adding a metric you've told the Glean SDK what shape of instrumentation you want. +And by using the metric's APIs to instrument your code, +you've put your interesting data into that metric. +But how does the data leave Firefox Desktop and make it to Mozilla's Data Pipeline? + +Batches of related metrics are collected into **pings** +which are submitted according to their specific schedules. +If you don't say otherwise, any non-`event`-metric will be sent in the +[built-in Glean "metrics" ping][metrics-ping] about once a day. +(`event` metrics are sent in [the "events" ping][events-ping] +more frequently than that). + +With data being sent to Mozilla's Data Pipeline, how do you analyse it? + +That's an impossible question to answer completely without knowing a _lot_ about what questions you want to answer. +However, in general, if you want to see what data is being collected by your instrumentation, +[go to its page in Glean Dictionary][glean-dictionary] +and you'll find links and information there about how to proceed. + +## Where do I learn more? + +Here in the [FOG User Documentation](./index) you will find FOG-specific details like +[how to write instrumentation tests](instrumentation_tests), or +[how to use Glean APIs to mirror data to Telemetry](gifft). + +Most of what you should have to concern yourself with, as an instrumentor, +is documented in [the Book of Glean](https://mozilla.github.io/glean/book/index.html). +Such as its [illuminating glossary][glean-glossary], +the [list of all metric types][metrics-types], +or the index of our long-running blog series [This Week in Glean][twig-index]. + +And for anything else you need help with, please find us in +[#glean:mozilla.org](https://chat.mozilla.org/#/room/#glean:mozilla.org). +We'll be happy to help you learn more about FOG and Glean. + +[add-a-metric]: https://mozilla.github.io/glean/book/user/metrics/adding-new-metrics.html +[metrics-ping]: https://mozilla.github.io/glean/book/user/pings/metrics.html +[events-ping]: https://mozilla.github.io/glean/book/user/pings/events.html +[modules]: https://wiki.mozilla.org/Modules/All +[data-review]: https://wiki.mozilla.org/Data_Collection +[glean-dictionary]: https://dictionary.telemetry.mozilla.org/ +[glean-glossary]: https://mozilla.github.io/glean/book/appendix/glossary.html +[twig-index]: https://mozilla.github.io/glean/book/appendix/twig.html +[metrics-types]: https://mozilla.github.io/glean/book/reference/metrics/index.html +[file-fog-bugs]: https://bugzilla.mozilla.org/enter_bug.cgi?product=Toolkit&component=Telemetry diff --git a/toolkit/components/glean/docs/user/gifft.md b/toolkit/components/glean/docs/user/gifft.md new file mode 100644 index 0000000000..892805f5c0 --- /dev/null +++ b/toolkit/components/glean/docs/user/gifft.md @@ -0,0 +1,223 @@ +# Glean Interface For Firefox Telemetry (GIFFT) + +To make Migration from Firefox Telemetry to Glean easier, +the C++ and JS Glean API can be configured +(on a metric-by-metric basis) +to mirror data collection to both the Glean metric and a Telemetry probe. + +GIFFT should ideally be used only when the data you require for analysis still mostly lives in Telemetry, +and should be removed promptly when no longer needed. +Instrumentors are encouraged to have the Telemetry mirror probe expire within six versions. +(As always you can renew an expiring probe if you're still using it, +but this will help us get closer to the time when we eventually turn Telemetry off.) + +**Note:** GIFFT only works for data provided via C++ or JS. +Rust Glean metrics APIs will not mirror to Telemetry as Telemetry does not have a Rust API. + +**Note:** Using the Glean API replaces the Telemetry API. +Do not use any mix of the two APIs for the same probe. + +## How to Mirror a Glean Metric to a Firefox Telemetry Probe + +For the mirror to work, you need three things: +* A compatible Glean metric (defined in a `metrics.yaml`) +* A compatible Telemetry probe + (defined in `Histograms.json`, `Scalars.yaml`, or `Events.yaml`) +* A `telemetry_mirror` property on the Glean metric definition identifying the Telemetry probe + +### Compatibility + +This compatibility table explains which Telemetry probe types can be mirrors for which Glean metric types: + +| Glean Metric Type | Telementry Probe Type | +| ----------------- | --------------------- | +| [boolean](https://mozilla.github.io/glean/book/user/metrics/boolean.html) | [Scalar of kind: boolean](/toolkit/components/telemetry/collection/scalars.rst) | +| [labeled_boolean](https://mozilla.github.io/glean/book/user/metrics/labeled_booleans.html) | [Keyed scalar of kind: boolean](/toolkit/components/telemetry/collection/scalars.rst) | +| [counter](https://mozilla.github.io/glean/book/user/metrics/counter.html) | [Scalar of kind: uint](/toolkit/components/telemetry/collection/scalars.rst) | +| [labeled_counter](https://mozilla.github.io/glean/book/user/metrics/labeled_counters.html) | [Keyed Scalar of kind: uint](/toolkit/components/telemetry/collection/scalars.rst) | +| [string](https://mozilla.github.io/glean/book/user/metrics/string.html) | [Scalar of kind: string](/toolkit/components/telemetry/collection/scalars.rst) | +| [labeled_string](https://mozilla.github.io/glean/book/user/metrics/labeled_strings.html) | *No Supported Telemetry Type* | +| [string_list](https://mozilla.github.io/glean/book/user/metrics/string_list.html) | [Keyed Scalar of kind: boolean](/toolkit/components/telemetry/collection/scalars.rst). The keys are the strings. The values are all `true`. Calling `Set` on the labeled_string is not mirrored (since there's no way to remove keys from a keyed scalar of kind boolean). Doing so will log a warning. | +| [timespan](https://mozilla.github.io/glean/book/user/metrics/timespan.html) | [Scalar of kind: uint](/toolkit/components/telemetry/collection/scalars.rst). The value is in units of milliseconds. | +| [timing_distribution](https://mozilla.github.io/glean/book/user/metrics/timing_distribution.html) | [Histogram of kind "linear" or "exponential"](/toolkit/components/telemetry/collection/histograms.rst#exponential). Samples will be in units of milliseconts. | +| [memory_distribution](https://mozilla.github.io/glean/book/user/metrics/memory_distribution.html) | [Histogram of kind "linear" or "exponential"](/toolkit/components/telemetry/collection/histograms.rst#exponential). Samples will be in `memory_unit` units. | +| [custom_distribution](https://mozilla.github.io/glean/book/user/metrics/custom_distribution.html) | [Histogram of kind "linear" or "exponential"](/toolkit/components/telemetry/collection/histograms.rst#exponential). Samples will be used as is. Ensure the bucket count and range match. | +| [uuid](https://mozilla.github.io/glean/book/user/metrics/uuid.html) | [Scalar of kind: string](/toolkit/components/telemetry/collection/scalars.rst). Value will be in canonical 8-4-4-4-12 format. Value is not guaranteed to be valid, and invalid values may be present in the mirrored scalar while the uuid metric remains empty. Calling `GenerateAndSet` on the uuid is not mirrored, and will log a warning. | +| [url](https://mozilla.github.io/glean/book/user/metrics/url.html) | [Scalar of kind: string](/toolkit/components/telemetry/collection/scalars.rst). The stringified Url will be cropped to the maximum length allowed by the legacy type. | +| [datetime](https://mozilla.github.io/glean/book/user/metrics/datetime.html) | [Scalar of kind: string](/toolkit/components/telemetry/collection/scalars.rst). Value will be in ISO8601 format. | +| [events](https://mozilla.github.io/glean/book/user/metrics/event.html) | [Events](/toolkit/components/telemetry/collection/events.rst). The `value` field will be left empty. | +| [quantity](https://mozilla.github.io/glean/book/user/metrics/quantity.html) | [Scalar of kind: uint](/toolkit/components/telemetry/collection/scalars.rst) | +| [rate](https://mozilla.github.io/glean/book/user/metrics/rate.html) | [Keyed Scalar of kind: uint](/toolkit/components/telemetry/collection/scalars.rst). The keys are "numerator" and "denominator". Does not work for `rate` metrics with external denominators. | + +### The `telemetry_mirror` property in `metrics.yaml` + +You must use the C++ enum identifier of the Histogram, Scalar, or Event being mirrored to: +* For Histograms, the Telemetry C++ enum identifier is the histogram's name + * e.g. The C++ enum identifier for `WR_RENDERER_TIME` is + `WR_RENDERER_TIME` (see {searchfox}`gfx/metrics.yaml`) +* For Scalars, the Telemetry C++ enum identifier is the Scalar category and name in + `SCREAMING_SNAKE_CASE` with any `.` replaced with `_` + * e.g. The enum identifier for `extensions.startupCache.load_time` is + `EXTENSIONS_STARTUPCACHE_LOAD_TIME` (see {searchfox}`toolkit/components/extensions/metrics.yaml`) +* For Events, the Telemetry C++ enum identifier is the Event category, method, and object + rendered in `Snakey_CamelCase`. + * e.g. The enum identifier for `page_load.toplevel#content` is + `Page_load_Toplevel_Content` (see {searchfox}`dom/metrics.yaml`) + +If you use the wrong enum identifier, this will manifest as a build error. + +If you are having trouble finding the correct conjugation for the mirror Telemetry probe, +you can find the specific value in the list of all Telemetry C++ enum identifiers in +`<objdir>/toolkit/components/telemetry/Telemetry{Histogram|Scalar|Event}Enums.h`. +(Choose the file appropriate to the type of the Telemetry mirror.) + +## Artifact Build Support + +Sadly, GIFFT does not support Artifact builds. +You must build Firefox when you add the mirrored metric so the C++ enum value is present, +even if you only use the metric from Javascript. + +## Analysis Gotchas + +Firefox Telemetry and the Glean SDK are very different. +Though GIFFT bridges the differences as best it can, +there are many things it cannot account for. + +These are a few of the ways that differences between Firefox Telemetry and the Glean SDK might manifest as anomalies during analysis. + +### Processes, Products, and Channels + +Like Firefox on Glean itself, +GIFFT doesn't know what process, product, or channel it is recording in. +Telemetry does, and imposes restrictions on which probes can be recorded to and when. + +Ensure that the following fields in any Telemetry mirror's definition aren't too restrictive for your use: +* `record_in_processes` +* `products` +* `release_channel_collection`/`releaseChannelCollection` + +A mismatch won't result in an error. +If you, for example, +record to a Glean metric in a release channel that the Telemetry mirror probe doesn't permit, +then the Glean metric will have a value and the Telemetry mirror probe won't. + +Also recall that Telemetry probes split their values across processes. +[Glean metrics do not](../dev/ipc.md). +This may manifest as curious anomalies when comparing the Glean metric to its Telemetry mirror probe. +Ensure your analyses are aggregating Telemetry values from all processes, +or define and use process-specific Glean metrics and Telemetry mirror probes to keep things separate. + +### Pings + +Glean and Telemetry both send their built-in pings on their own schedules. +This means the values present in these pings may not agree since they reflect state at different time. + +For example, if you are measuring "Number of Monitors" with a +[`quantity`](https://mozilla.github.io/glean/book/user/metrics/quantity.html) +sent by default in the Glean "metrics" ping mirrored to a +[Scalar of kind: uint](/toolkit/components/telemetry/collection/scalars.rst) +sent by default in the Telemetry "main" ping, +then if the user plugs in a second monitor between midnight +(when Telemetry "main" pings with reason "daily" are sent) and 4AM +(when Glean "metrics" pings with reason "today" are sent), +the value in the `quantity` will be `2` +while the value in the Scalar of kind: uint will be `1`. + +If the metric or mirrored probe are sent in Custom pings, +the schedules could line up exactly or be entirely unrelated. + +### Labels + +Labeled metrics supported by GIFFT +(`labeled_boolean` and `labeled_counter`) +adhere to the Glean SDK's +[label format](https://mozilla.github.io/glean/book/user/metrics/index.html#label-format). + +Keyed Scalars, on the other hand, do not have a concept of an "Invalid key". +Firefox Telemetry will accept just about any sequence of bytes as a key. + +This means that a label deemed invalid by the Glean SDK may appear in the mirrored probe's data. +For example, using `InvalidLabel` as a label that doesn't conform to the format +(it has upper-case letters) +see that the `labeled_boolean` metric +[correctly ascribes it to `__other__`](https://mozilla.github.io/glean/book/user/metrics/index.html#labeled-metrics) +whereas the mirrored Keyed Scalar with kind boolean stores and retrieves it without change: +```js +Glean.testOnly.mirrorsForLabeledBools.InvalidLabel.set(true); +Assert.equal(true, Glean.testOnly.mirrorsForLabeledBools.__other__.testGetValue()); +let snapshot = Services.telemetry.getSnapshotForKeyedScalars().parent; +Assert.equal(true, snapshot["telemetry.test.mirror_for_labeled_bool"]["InvalidLabel"]); +``` + +### Telemetry Events + +A Glean event can be mirrored to a Telemetry Event. +Telemetry Events must be enabled before they can be recorded to via the API +`Telemetry.setEventRecordingEnabled(category, enable);`. +If the Telemetry Event isn't enabled, +recording to the Glean event will still work, +and the event will be Summarized in Telemetry as all disabled events are. + +See +[the Telemetry Event docs](/toolkit/components/telemetry/collection/events.rst) +for details on how disabled Telemetry Events behave. + +### Numeric Values + +The arguments and storage formats for Glean's numeric types +(`counter`, `labeled_counter`, `quantity`, `rate`, and `timespan`) +are different from Telemetry's numeric type +(Scalar of kind `uint`). + +This results in a few notable differences. + +#### Saturation and Overflow + +`counter`, `labeled_counter`, and `rate` metrics are stored as 32-bit signed values. +`quantity` metrics are stored as 64-bit signed values. +`timing_distribution` samples can be 64-bit signed values. +All of these Glean numeric metric types saturate at their maximum representable value, +or according to the Limits section of the Glean metric type documentation. + +Scalars of kind `uint` are stored as 32-bit unsigned values. +They will overflow if they exceed the value $2^{32} - 1$. + +If a Glean numeric type saturates, it will record an error of type `invalid_overflow`. +In your analyses please check for these errors. + +#### Quantity Value Over-size + +Values greater than $2^{32} - 1$ passed to a `quantity` metric's +`set()` method will be clamped to $2^{32} - 1$ before being passed to the metric's Telemetry mirror. + +#### Negative Values + +Values less than 0 passed to any numeric metric type's API will not be passed on to the Telemetry mirror. +This avoids small negative numbers being cast into a stunningly large numbers, +and keeps the Telemetry mirror's value closer to that of the Glean metric. + +#### Long Time Spans + +If the number of milliseconds between calls to a +`timespan` metric's `start()` and `stop()` methods exceeds $2^{32} - 1$, +the value passed to the metric's Telemetry mirror will be clamped to $2^{32} - 1$. + +The same happens for samples in `timing_distribution` metrics: +values passed to the Telemetry mirror histogram will saturate at $2^{32} - 1$ +until they get past $2^{64}$ when they'll overflow. + +### App Shutdown + +Telemetry only works up to +[`ShutdownPhase::AppShutdownTelemetry` aka `profile-before-change-telemetry`][app-shutdown]. +Telemetry data recorded after that phase just aren't persisted. + +FOG _presently_ shuts down Glean in a later phase, +and so is able to collect data deeper into shutdown. +(The particular phase is not presently something anyone's asked us to guarantee, +so that's why I'm not being precise.) + +What this means is that, for data recorded later in shutdown, +Glean will report more complete information than Telemetry will. + +[app-shutdown]: https://searchfox.org/mozilla-central/source/xpcom/base/AppShutdown.cpp#57 diff --git a/toolkit/components/glean/docs/user/index.md b/toolkit/components/glean/docs/user/index.md new file mode 100644 index 0000000000..79b4163a44 --- /dev/null +++ b/toolkit/components/glean/docs/user/index.md @@ -0,0 +1,17 @@ +# Using Firefox on Glean + +This section of docs is designed to be helpful to people instrumenting Firefox Desktop. +You may wish to begin with the [Getting Started](./getting_started.md) docs. +Or, if you're already acquainted with Glean concepts and what FOG is, +you might want to know [how to migrate a piece of Firefox Telemetry to Glean](migration). + +```{toctree} +:titlesonly: +:maxdepth: 1 +:glob: + +getting_started +new_definitions_file +* +Glean SDK Documentation <https://mozilla.github.io/glean/book/index.html> +``` diff --git a/toolkit/components/glean/docs/user/instrumentation_tests.md b/toolkit/components/glean/docs/user/instrumentation_tests.md new file mode 100644 index 0000000000..ed28c6d4e8 --- /dev/null +++ b/toolkit/components/glean/docs/user/instrumentation_tests.md @@ -0,0 +1,224 @@ +# Writing Instrumentation Tests + +```{admonition} Old Glean Proverb +If it's important enough to be instrumented, it's important enough to be tested. +``` + +All metrics and pings in the Glean SDK have [well-documented APIs for testing][glean-metrics-apis]. +You'll want to familiarize yourself with `TestGetValue()` +(here's [an example JS (xpcshell) test of some metrics][metrics-xpcshell-test]) +for metrics and +[`TestBeforeNextSubmit()`][test-before-next-submit] +(here's [an example C++ (gtest) test of a custom ping][ping-gtest]) +for pings. + +All test APIs are available in all three of FOG's supported languages: +Rust, C++, and JavaScript. + +But how do you get into a position where you can even call these test APIs? +How do they fit in with Firefox Desktop's testing frameworks? + +## Manual Testing and Debugging + +The Glean SDK has [debugging capabilities][glean-debug] +for manually verifying that instrumentation makes it to Mozilla's Data Pipeline. +Firefox Desktop supports these via environment variables _and_ +via the interface on `about:glean`. + +This is all well and good for getting a good sense check that things are going well _now_, +but in order to check that everything stays good through the future, +you're going to want to write some automated tests. + +## General Things To Bear In Mind + +* You may see values from previous tests persist across tests because the profile directory was shared between test cases. + * You can reset Glean before your test by calling + `Services.fog.testResetFOG()` (in JS). + * You shouldn't have to do this in C++ or Rust since there you should use the + `FOGFixture` test fixture. +* If your metric is based on timing (`timespan`, `timing_distribution`), + do not expect to be able to assert the correct timing value. + Glean does a lot of timing for you deep in the SDK, so unless you mock the system's monotonic clock, + do not expect the values to be predictable. + * Instead, check that a value is `> 0` or that the number of samples is expected. + * You might be able to assert that the value is at least as much as a known, timed value, + but beware of rounding. +* Errors in instrumentation APIs do not panic, throw, or crash. + But Glean remembers that the errors happened. + * Test APIs, on the other hand, are permitted + (some may say "encouraged") + to panic, throw, or crash on bad behaviour. + * If you call a test API and it panics, throws, or crashes, + that means your instrumentation did something wrong. + Check your test logs for details about what went awry. + +## The Usual Test Format + +Instrumentation tests tend to follow the same three-part format: +1) Assert no value in the metric +2) Express behaviour +3) Assert correct value in the metric + +Your choice of test suite will depend on how the instrumented behaviour can be expressed. + + +## `xpcshell` Tests + +If the instrumented behaviour is on the main or content process and can be called from privileged JS, +`xpcshell` is an excellent choice. + +`xpcshell` is so minimal an environment, however, that +(pending [bug 1756055](https://bugzilla.mozilla.org/show_bug.cgi?id=1756055)) +you'll need to manually tell it you need two things: +1) A profile directory +2) An initialized FOG + +```js +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_setup(function test_setup() { + // FOG needs a profile directory to put its data in. + do_get_profile(); + + // FOG needs to be initialized in order for data to flow. + Services.fog.initializeFOG(); +}); +``` + +From there, just follow The Usual Test Format: + +```js +add_task(function test_instrumentation() { + // 1) Assert no value + Assert.equal(undefined, Glean.myMetricCategory.myMetricName.testGetValue()); + + // 2) Express behaviour + // ...<left as an exercise to the reader>... + + // 3) Assert correct value + Assert.equal(kValue, Glean.myMetricCategory.myMetricName.testGetValue()); +}); +``` + +If your new instrumentation includes a new custom ping, +there are two small additions to The Usual Test Format: + +* 1.1) Call `testBeforeNextSubmit` _before_ your ping is submitted. + The callback you register in `testBeforeNextSubmit` + is called synchronously with the call to the ping's `submit()`. +* 3.1) Check that the ping actually was submitted. + If all your Asserts are inside `testBeforeNextSubmit`'s closure, + another way this test could pass is by not running any of them. + +```js +add_task(function test_custom_ping() { + // 1) Assert no value + Assert.equal(undefined, Glean.myMetricCategory.myMetricName.testGetValue()); + + // 1.1) Set up Step 3. + let submitted = false; + GleanPings.myPing.testBeforeNextSubmit(reason => { + submitted = true; + // 3) Assert correct value + Assert.equal(kExpectedReason, reason, "Reason of submitted ping must match."); + Assert.equal(kExpectedMetricValue, Glean.myMetricCategory.myMetricName.testGetValue()); + }); + + // 2) Express behaviour that sends a ping with expected reason and contents + // ...<left as an exercise to the reader>... + + // 3.1) Check that the ping actually was submitted. + Assert.ok(submitted, "Ping was submitted, callback was called."); +}); +``` + +(( We acknowledge that this isn't the most ergonomic form. +Please follow +[bug 1756637](https://bugzilla.mozilla.org/show_bug.cgi?id=1756637) +for updates on a better design and implementation for ping tests. )) + +## mochitest + +`browser-chrome`-flavoured mochitests can be tested very similarly to `xpcshell`. + +Prefer `xpcshell` and only use mochitests if you cannot express the behaviour in `xpcshell`. +This can happen, for example, if the behaviour happens on a non-main process. + +### IPC + +All test APIs must be called on the main process +(they'll assert otherwise). +But your instrumentation might be on any process, so how do you test it? + +In this case there's a slight addition to the Usual Test Format: +1) Assert no value in the metric +2) Express behaviour +3) _Flush all pending FOG IPC operations with `Services.fog.testFlushAllChildren()`_ +4) Assert correct value in the metric. + +## GTests/Google Tests + +Please make use of the `FOGFixture` fixture when writing your tests, like: + +```cpp +TEST_F(FOGFixture, MyTestCase) { + // 1) Assert no value + ASSERT_EQ(mozilla::Nothing(), + my_metric_category::my_metric_name.TestGetValue()); + + // 2) Express behaviour + // ...<left as an exercise to the reader>... + + // 3) Assert correct value + ASSERT_EQ(kValue, + my_metric_category::my_metric_name.TestGetValue().unwrap().ref()); +} +``` + +The fixture will take care of ensuring storage is reset between tests. + +## Rust `rusttests` + +The general-purpose +[Testing & Debugging Rust Code in Firefox](/testing-rust-code/index) +is a good thing to review first. + +Unfortunately, FOG requires gecko +(to tell it where the profile dir is, and other things), +which means we need to use the +[GTest + FFI approach](/testing-rust-code/index.html#gtests) +where GTest is the runner and Rust is just the language the test is written in. + +This means your test will look like a GTest like this: + +```cpp +extern "C" void Rust_MyRustTest(); +TEST_F(FOGFixture, MyRustTest) { Rust_MyRustTest(); } +``` + +Plus a Rust test like this: + +```rust +#[no_mangle] +pub extern "C" fn Rust_MyRustTest() { + // 1) Assert no value + assert_eq!(None, + fog::metrics::my_metric_category::my_metric_name.test_get_value(None)); + + // 2) Express behaviour + // ...<left as an exercise to the reader>... + + // 3) Assert correct value + assert_eq!(Some(value), + fog::metrics::my_metric_category::my_metric_name.test_get_value(None)); +} +``` + +[glean-metrics-apis]: https://mozilla.github.io/glean/book/reference/metrics/index.html +[metrics-xpcshell-test]: https://searchfox.org/mozilla-central/rev/66e59131c1c76fe486424dc37f0a8a399ca874d4/toolkit/mozapps/update/tests/unit_background_update/test_backgroundupdate_glean.js#28 +[ping-gtest]: https://searchfox.org/mozilla-central/rev/66e59131c1c76fe486424dc37f0a8a399ca874d4/toolkit/components/glean/tests/gtest/TestFog.cpp#232 +[test-before-next-submit]: https://mozilla.github.io/glean/book/reference/pings/index.html#testbeforenextsubmit +[glean-debug]: https://mozilla.github.io/glean/book/reference/debug/index.html diff --git a/toolkit/components/glean/docs/user/migration.md b/toolkit/components/glean/docs/user/migration.md new file mode 100644 index 0000000000..f3c86a182b --- /dev/null +++ b/toolkit/components/glean/docs/user/migration.md @@ -0,0 +1,909 @@ +# Migrating Firefox Telemetry to Glean + +This guide aims to help you migrate individual data collections from +[Firefox Telemetry](/toolkit/components/telemetry/index.rst) +to +[Glean][book-of-glean] via [Firefox on Glean](../index.md). + +This is intended to be a reference to help you fill out your +[migration worksheet][migration-worksheet], +or for mentally translating Telemetry concepts to Glean ones. + +```{contents} +``` + +## General Things To Bear In Mind + +You should familiarize yourself with +[the guide on adding new metrics to Firefox Desktop](new_definitions_file.md). +Its advice stacks with the advice included in this guide as +(once you've figured out what kind) you will indeed be adding new metrics. + +There are some other broad topics specific to migrating Firefox Telemetry stuff to Glean stuff: + +### Process-Agnosticism: No more `record_in_processes` field + +Glean (and thus FOG) [doesn't know anything about processes][ipc-dev-doc] +except what it has to in order to ensure all the data makes it to the parent process. +Firefox Telemetry cared very much about which process was collecting which specific data, +keeping them separate. + +If you collect data in multiple processes and wish to keep data from each process type separate, +you will need to provide this separation yourself. + +Please see [this dev doc][ipc-dev-doc] for an example of how to do that. + +### Channel-Agnosticism: No more `release_channel_collection: opt-out` + +FOG doesn't make a differentiation between pre-release Firefox and release Firefox, +except inasmuch as is necessary to put the correct channel in `client_info.app_channel`. + +This means all data is collected in all build configurations. + +If you wish or are required to only collect your data in pre-release Firefox, +please avail yourself of the `EARLY_BETA_OR_EARLIER` `#define` or `AppConstant`. + +### File-level Product Inclusion/Exclusion: No more `products` field + +Glean determines which metrics are recorded in which products via +[a dependency tree][repositories-yaml]. +This means FOG doesn't distinguish between products at the per-product level. + +If some of your metrics are recorded in different sets of products +(e.g. some of your metrics are collected in both Firefox Desktop _and_ Firefox for Android, +but others are Firefox Desktop-specific) +you must separate them into separate [definitions files](new_definitions_file.md). + +### Many Definitions Files + +Each component is expected to own and care for its own +[metrics definitions files](new_definitions_file.md). +There is no centralized `Histograms.json` or `Scalars.yaml` or `Events.yaml`. + +Instead the component being instrumented will have its own `metrics.yaml` +(and `pings.yaml` for any [Custom Pings][custom-pings]) +in which you will define the data. + +See [this guide](new_definitions_file.md) for details. + +### Testing + +Firefox Telemetry had very uneven support for testing instrumentation code. +FOG has much better support. Anywhere you can instrument is someplace you can test. + +It's as simple as calling `testGetValue`. + +All migrated collections are expected to be tested. +If you can't test them, then you'd better have an exceptionally good reason why not. + +For more details, please peruse the +[instrumentation testing docs](instrumentation_tests). + +## Which Glean Metric Type Should I Use? + +Glean uses higher-level metric types than Firefox Telemetry does. +This complicates migration as something that is "just a number" +in Firefox Telemetry might map to any number of Glean metric types. + +Please choose the most specific metric type that solves your problem. +This'll make analysis easier as +1. Others will know more about how to analyse the metric from more specific types. +2. Tooling will be able to present only relevant operations for more specific types. + +Example: +> In Firefox Telemetry I record the number of monitors attached to the computer that Firefox Desktop is running on. +> I could record this number as a [`string`][string-metric], a [`counter`][counter-metric], +> or a [`quantity`][quantity-metric]. +> The `string` is an obvious trap. It doesn't even have the correct data type (string vs number). +> But is it a `counter` or `quantity`? +> If you pay attention to this guide you'll learn that `counter`s are used to accumulate sums of information, +> whereas `quantity` metrics are used to record specific values. +> The "sum" of monitors over time doesn't make sense, so `counter` is out. +> `quantity` is the correct choice. + +## Histograms + +[Histograms][telemetry-histograms] +are the oldest Firefox Telemetry data type, and as such they've accumulated +([ha!][histogram-accumulate]) the most ways of being used. + +### Scalar Values in Histograms: kind `flag` and `count` + +If you have a Histogram that records exactly one value, +please scroll down and look at the migration guide for the relevant Scalar: +* For Histograms of kind `flag` see "Scalars of kind `bool`" +* For Histograms of kind `count` see "Scalars of kind `uint`" + +### Continuous Distributions: kind `linear` and `exponential` + +If the Histogram you wish to migrate is formed of multiple buckets that together form a single continuous range +(like you have buckets 1-5, 6-10, 11-19, and 20-50 - they form the range 1-50), +then you will want a "distribution" metric type in Glean. +Which kind of "distribution" metric type depends on what the samples are. + +#### Timing samples - Use Glean's `timing_distribution` + +The most common type of continuous distribution in Firefox Telemetry is a histogram of timing samples like +[`GC_MS`][gc-ms]. + +In Glean this sort of data is recorded using a +[`timing-distribution`][timing-distribution-metric] metric type. + +You will no longer need to worry about the range of values or number or distribution of buckets +(represented by the `low`, `high`, `n_buckets`, or `kind` in your Histogram's definition). +Glean uses a [clever automatic bucketing algorithm][timing-distribution-metric] instead. + +So for a Histogram that records timing samples like this: + +``` + "GC_MS": { + "record_in_processes": ["main", "content"], + "products": ["firefox", "geckoview_streaming"], + "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org", "jcoppeard@mozilla.com"], + "expires_in_version": "never", + "releaseChannelCollection": "opt-out", + "kind": "exponential", + "high": 10000, + "n_buckets": 50, + "bug_numbers": [1636419], + "description": "Time spent running JS GC (ms)" + }, +``` + +You will migrate to a `timing_distibution` metric type like this: + +```yaml +js: + gc: + type: timing_distribution + time_unit: millisecond + description: | + Time spent running the Javascript Garbage Collector. + Migrated from Firefox Telemetry's `GC_MS`. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1636419 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1636419#c8 + data_sensitivity: + - technical + notification_emails: + - dev-telemetry-gc-alerts@mozilla.org + - jcoppeard@mozilla.com + expires: never +``` + +**GIFFT:** This type of collection is mirrorable back to Firefox Telemetry via the +[Glean Interface For Firefox Telemetry][gifft]. +See [the guide][gifft] for instructions. + +#### Memory Samples - Use Glean's `memory_distribution` + +Another common content of `linear` or `exponential` +Histograms in Firefox Telemetry is memory samples. +For example, [`MEMORY_TOTAL`][memory-total]'s samples are in kilobytes. + +In Glean this sort of data is recorded using a +[`memory-distribution`][memory-distribution-metric] metric type. + +You will no longer need to worry about the range of values or number or distribution of buckets +(represented by the `low`, `high`, `n_buckets`, or `kind` in your Histogram's definition). +Glean uses a [clever automatic bucketing algorithm][memory-distribution-metric] instead. + +So for a Histogram that records memory samples like this: + +``` + "MEMORY_TOTAL": { + "record_in_processes": ["main"], + "products": ["firefox", "thunderbird"], + "alert_emails": ["memshrink-telemetry-alerts@mozilla.com", "amccreight@mozilla.com"], + "bug_numbers": [1198209, 1511918], + "expires_in_version": "never", + "kind": "exponential", + "low": 32768, + "high": 16777216, + "n_buckets": 100, + "description": "Total Memory Across All Processes (KB)", + "releaseChannelCollection": "opt-out" + }, +``` + +You will migrate to a `memory_distribution` metric type like this: + +```yaml +memory: + total: + type: memory_distribution + memory_unit: kilobyte + description: | + The total memory allocated across all processes. + Migrated from Telemetry's `MEMORY_TOTAL`. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1198209 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1511918 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1511918#c9 + data_sensitivity: + - technical + notification_emails: + - memshrink-telemetry-alerts@mozilla.com + - amccreight@mozilla.com + expires: never +``` + +**GIFFT:** This type of collection is mirrorable back to Firefox Telemetry via the +[Glean Interface For Firefox Telemetry][gifft]. +See [the guide][gifft] for instructions. + +#### Percentage Samples - Comment on bug 1657467 + +A very common Histogram in Firefox Desktop is a distribution of percentage samples. +[For example, `GC_SLICE_DURING_IDLE`][gc-idle]. + +Glean doesn't currently have a good metric type for this. +But we [intend to add one][new-metric-percent]. +If you are migrating a collection of this type, +please add a comment to the bug detailing which probe you are migrating, +and when you need it migrated by. +We'll prioritize adding this metric type accordingly. + +#### Other - Use Glean's `custom_distribution` + +Continuous Distribution Histograms have been around long enough to have gotten weird. +If you're migrating one of those histograms with units like +["square root of pixels times milliseconds"][checkerboard-severity], +we have a "catch all" metric type for you: [Custom Distribution][custom-distribution-metric]. + +Sadly, you'll have to care about the bucketing algorithm and bucket ranges for this one. +So for a Histogram with artisinal samples like: + +``` + "CHECKERBOARD_SEVERITY": { + "record_in_processes": ["main", "content", "gpu"], + "products": ["firefox", "fennec", "geckoview_streaming"], + "alert_emails": ["gfx-telemetry-alerts@mozilla.com", "botond@mozilla.com"], + "bug_numbers": [1238040, 1539309, 1584109], + "releaseChannelCollection": "opt-out", + "expires_in_version": "never", + "kind": "exponential", + "high": 1073741824, + "n_buckets": 50, + "description": "Opaque measure of the severity of a checkerboard event" + }, +``` + +You will migrate it to a `custom_distribution` like: + +```yaml +gfx.checkerboard: + severity: + type: custom_distribution + range_max: 1073741824 + bucket_count: 50 + histogram_type: exponential + unit: Opaque unit + description: > + An opaque measurement of the severity of a checkerboard event. + This doesn't have units, it's just useful for comparing two checkerboard + events to see which one is worse, for some implementation-specific + definition of "worse". The larger the value, the worse the + checkerboarding. + Migrated from Telemetry's `CHECKERBOARD_SEVERITY`. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1238040 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1539309 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1584109 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1584109#c1 + notification_emails: + - gfx-telemetry-alerts@mozilla.com + - botond@mozilla.com + data_sensitivity: + - technical + expires: never +``` + +**TODO [Bug 1677447](https://bugzilla.mozilla.org/show_bug.cgi?id=1677447):** +Custom Distributions aren't yet implemented in FOG. We're working on it. +When they're done we'll see if they'll support GIFFT like the other distributions. + +#### Keyed Histograms with Continuous Sample Distributions - Ask on #glean:mozilla.org for assistance + +Glean doesn't currently have a good metric type for keyed continuous distributions +like video play time keyed by codec. +Please [reach out to us][glean-matrix] to explain your use-case. +We will help you either work within what Glean currently affords or +[design a new metric type for you][new-metric-type]. + +### Discrete Distributions: kind `categorical`, `enumerated`, or `boolean` - Use Glean's `labeled_counter` + +If the samples don't fall in a continuous range and instead fall into a known number of buckets, +Glean provides the [Labeled Counter][labeled-counter-metric] for these cases. + +Simply enumerate the discrete categories as `labels` in the `labeled_counter`. + +For example, for a Histogram of kind `categorical` like: + +``` + "AVIF_DECODE_RESULT": { + "record_in_processes": ["main", "content"], + "products": ["firefox", "geckoview_streaming"], + "alert_emails": ["cchang@mozilla.com", "jbauman@mozilla.com"], + "expires_in_version": "never", + "releaseChannelCollection": "opt-out", + "kind": "categorical", + "labels": [ + "success", + "parse_error", + "no_primary_item", + "decode_error", + "size_overflow", + "out_of_memory", + "pipe_init_error", + "write_buffer_error", + "alpha_y_sz_mismatch", + "alpha_y_bpc_mismatch" + ], + "description": "Decode result of AVIF image", + "bug_numbers": [1670827] + }, +``` + +You would migrate to a `labeled_counter` like: + +```yaml +avif: + decode_result: + type: labeled_counter, + description: | + Each AVIF image's decode result. + Migrated from Telemetry's `AVIF_DECODE_RESULT`. + labels: + - success + - parse_error + - no_primary_item + - decode_error + - size_overflow + - out_of_memory + - pipe_init_error + - write_buffer_error + - alpha_y_sz_mismatch + - alpha_y_bpc_mismatch + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1670827 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1670827#c9 + data_sensitivity: + - technical + notification_emails: + - cchang@mozilla.com + - jbauman@mozilla.com + expires: never +``` + +**N.B:** Glean Labels have a strict regex. +You may have to transform some categories to +`snake_case` so that they're safe for the data pipeline. + +**GIFFT:** This type of collection is mirrorable back to Firefox Telemetry via the +[Glean Interface For Firefox Telemetry][gifft]. +See [the guide][gifft] for instructions. +**N.B.:** This will mirror back as a Keyed Scalar of kind `uint`, +not as any kind of Histogram, +so your original un-migrated histogram cannot be used as the mirror. + +#### Keyed Histograms with Discrete Sample Distributions: `"keyed": true` and kind `categorical`, `enumerated`, or `boolean` - Comment on bug 1657470 + +Glean doesn't currently have a good metric type for this. +But we [intend to add one][new-metric-keyed-categorical]. +If you are migrating a collection of this type, +please add a comment to the bug detailing which probe you are migrating, +and when you need it migrated by. +We'll prioritize adding this metric type accordingly. + +## Scalars + +[Scalars][telemetry-scalars] are low-level individual data collections with a variety of uses. + +### Scalars of `kind: uint` that you call `scalarAdd` on - Use Glean's `counter` + +The most common kind of Scalar is of `kind: uint`. +The most common use of such a scalar is to repeatedly call `scalarAdd` +on it as countable things happen. + +The Glean metric type for countable things is [the `counter` metric type][counter-metric]. + +So for a Scalar like this: + +```yaml +script.preloader: + mainthread_recompile: + bug_numbers: + - 1364235 + description: + How many times we ended up recompiling a script from the script preloader + on the main thread. + expires: "100" + keyed: false + kind: uint + notification_emails: + - dothayer@mozilla.com + - plawless@mozilla.com + release_channel_collection: opt-out + products: + - 'firefox' + - 'fennec' + record_in_processes: + - 'main' + - 'content' +``` + +You will migrate to a `counter` metric type like this: + +```yaml +script.preloader: + mainthread_recompile: + type: counter + description: | + How many times we ended up recompiling a script from the script preloader + on the main thread. + Migrated from Telemetry's `script.preloader.mainthread_recompile`. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1364235 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1364235#c25 + data_sensitivity: + - technical + notification_emails: + - dothayer@mozilla.com + - plawless@mozilla.com + expires: "100" +``` + +**GIFFT:** This type of collection is mirrorable back to Firefox Telemetry via the +[Glean Interface For Firefox Telemetry][gifft]. +See [the guide][gifft] for instructions. + +#### Keyed Scalars of `kind: uint` that you call `scalarAdd` on - Use Glean's `labeled_counter` + +Another very common use of Scalars is to have a Keyed Scalar of +`kind: uint`. This was often used to track UI usage. + +This is supported by the [Glean `labeled_counter` metric type][labeled-counter-metric]. + +So for a Keyed Scalar of `kind: uint` like this: + +```yaml +urlbar: + tips: + bug_numbers: + - 1608461 + description: > + A keyed uint recording how many times particular tips are shown in the + Urlbar and how often their confirm and help buttons are pressed. + expires: never + kind: uint + keyed: true + notification_emails: + - email@example.com + release_channel_collection: opt-out + products: + - 'firefox' + record_in_processes: + - main +``` + +You would migrate it to a `labeled_counter` like this: + +```yaml +urlbar: + tips: + type: labeled_counter + description: > + A keyed uint recording how many times particular tips are shown in the + Urlbar and how often their confirm and help buttons are pressed. + Migrated from Telemetry's `urlbar.tips`. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1608461 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1608461#c42 + data_sensitivity: + - interaction + expires: never + notification_emails: + - email@example.com +``` + +Now, if your Keyed Scalar has a list of known keys, +you should provide it to the `labeled_counter` using the `labels` property like so: + +```yaml +urlbar: + tips: + type: labeled_counter + labels: + - tabtosearch_onboard_shown + - tabtosearch_shown + - searchtip_onboard_shown + ... +``` + +**N.B:** Glean Labels have a strict regex. +You may have to transform some categories to +`snake_case` so that they're safe for the data pipeline. + +**GIFFT:** This type of collection is mirrorable back to Firefox Telemetry via the +[Glean Interface For Firefox Telemetry][gifft]. +See [the guide][gifft] for instructions. + +### Scalars of `kind: uint` that you call `scalarSet` on - Use Glean's `quantity` + +Distinct from counts which are partial sums, +Scalars of `kind: uint` that you _set_ could contain just about anything. +The best metric type depends on the type of data you're setting +(See "Other Scalar-ish types" for some possibilities). + +If it's a numerical value you are setting, chances are you will be best served by +[Glean's `quantity` metric type][quantity-metric]. + +For a such a quantitative Scalar like: + +```yaml +gfx.display: + primary_height: + bug_numbers: + - 1594145 + description: > + Height of the primary display, takes device rotation into account. + expires: never + kind: uint + notification_emails: + - gfx-telemetry-alerts@mozilla.com + - ktaeleman@mozilla.com + products: + - 'geckoview_streaming' + record_in_processes: + - 'main' + release_channel_collection: opt-out +``` + +You would migrate it to a `quantity` like: + +```yaml +gfx.display: + primary_height: + type: quantity + unit: pixels + description: > + Height of the primary display, takes device rotation into account. + Migrated from Telemetry's `gfx.display.primary_height`. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1594145 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1687219 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1594145#c4 + data_sensitivity: + - technical + notification_emails: + - gfx-telemetry-alerts@mozilla.com + expires: never +``` + +Note the required `unit` property. + +**GIFFT:** This type of collection is mirrorable back to Firefox Telemetry via the +[Glean Interface For Firefox Telemetry][gifft]. +See [the guide][gifft] for instructions. + +**IPC Note:** Due to `set` not being a [commutative operation][ipc-docs], using `quantity` +on non-parent processes is forbidden. +This is a restriction that favours correctness over friendliness, +which we may revisit if enough use cases require it. +Please [contact us][glean-matrix] if you'd like us to do so. + +#### Keyed Scalars of `kind: uint` that you call `scalarSet` on - Ask on #glean:mozilla.org for assistance + +Glean doesn't currently have a good metric type for keyed quantities. +Please [reach out to us][glean-matrix] to explain your use-case. +We will help you either work within what Glean currently affords or +[design a new metric type for you][new-metric-type]. + +### Scalars of `kind: uint` that you call `scalarSetMaximum` or some combination of operations on - Ask on #glean:mozilla.org for assistance + +Glean doesn't currently have a good metric type for dealing with maximums, +or for dealing with values you both count and set. +Please [reach out to us][glean-matrix] to explain your use-case. +We will help you either work within what Glean currently affords or +[design a new metric type for you][new-metric-type]. + +### Scalars of `kind: string` - Use Glean's `string` + +If your string value is a unique identifier, then consider +[Glean's `uuid` metric type][uuid-metric] first. + +If the string scalar value doesn't fit that or any other more specific metric type, +then [Glean's `string` metric type][string-metric] will do. + +For a Scalar of `kind: string` like: + +```yaml +widget: + gtk_version: + bug_numbers: + - 1670145 + description: > + The version of Gtk 3 in use. + kind: string + expires: never + notification_emails: + - layout-telemetry-alerts@mozilla.com + release_channel_collection: opt-out + products: + - 'firefox' + record_in_processes: + - 'main' +``` + +You will migrate it to a `string` metric like: + +```yaml +widget: + gtk_version: + type: string + description: > + The version of Gtk 3 in use. + Migrated from Telemetry's `widget.gtk_version`. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1670145 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1670145#c7 + data_sensitivity: + - technical + notification_emails: + - layout-telemetry-alerts@mozilla.com + expires: never +``` + +**GIFFT:** This type of collection is mirrorable back to Firefox Telemetry via the +[Glean Interface For Firefox Telemetry][gifft]. +See [the guide][gifft] for instructions. + +**IPC Note:** Due to `set` not being a [commutative operation][ipc-docs], using `string` +on non-parent processes is forbidden. +This is a restriction that favours correctness over friendliness, +which we may revisit if enough use cases require it. +Please [contact us][glean-matrix] if you'd like us to do so. + +### Scalars of `kind: boolean` - Use Glean's `boolean` + +If you need to store a simple true/false, +[Glean's `boolean` metric type][boolean-metric] is likely best. + +If you have more that just `true` and `false` to store, +you may prefer a `labeled_counter`. + +For a Scalar of `kind: boolean` like: + +```yaml +widget: + dark_mode: + bug_numbers: + - 1601846 + description: > + Whether the OS theme is dark. + expires: never + kind: boolean + notification_emails: + - layout-telemetry-alerts@mozilla.com + - cmccormack@mozilla.com + release_channel_collection: opt-out + products: + - 'firefox' + - 'fennec' + record_in_processes: + - 'main' +``` + +You would migrate to a `boolean` metric type like: + +```yaml +widget: + dark_mode: + type: boolean + description: > + Whether the OS theme is dark. + Migrated from Telemetry's `widget.dark_mode`. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1601846 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1601846#c5 + data_sensitivity: + - technical + notification_emails: + - layout-telemetry-alerts@mozilla.com + - cmccormack@mozilla.com + expires: never +``` + +**GIFFT:** This type of collection is mirrorable back to Firefox Telemetry via the +[Glean Interface For Firefox Telemetry][gifft]. +See [the guide][gifft] for instructions. + +**IPC Note:** Due to `set` not being a [commutative operation][ipc-docs], using `boolean` +on non-parent processes is forbidden. +This is a restriction that favours correctness over friendliness, +which we may revisit if enough use cases require it. +Please [contact us][glean-matrix] if you'd like us to do so. + +#### Keyed Scalars of `kind: boolean` - Use Glean's `labeled_boolean` + +If you have multiple related true/false values, you may have put them in a +Keyed Scalar of `kind: boolean`. + +The best match for this is +[Glean's `labeled_boolean` metric type][labeled-boolean-metric]. + +For a Keyed Scalar of `kind: boolean` like: + +```yaml +devtools.tool: + registered: + bug_numbers: + - 1447302 + - 1503568 + - 1587985 + description: > + Recorded on enable tool checkbox check/uncheck in Developer Tools options + panel. Boolean stating if the tool was enabled or disabled by the user. + Keyed by tool id. Current default tools with their id's are defined in + https://searchfox.org/mozilla-central/source/devtools/client/definitions.js + expires: never + kind: boolean + keyed: true + notification_emails: + - dev-developer-tools@lists.mozilla.org + - accessibility@mozilla.com + release_channel_collection: opt-out + products: + - 'firefox' + - 'fennec' + record_in_processes: + - 'main' +``` + +You would migrate to a `labeled_boolean` like: + +```yaml +devtools.tool: + registered: + type: labeled_boolean + description: > + Recorded on enable tool checkbox check/uncheck in Developer Tools options + panel. Boolean stating if the tool was enabled or disabled by the user. + Migrated from Telemetry's `devtools.tool`. + labels: + - options + - inspector + - webconsole + - jsdebugger + - styleeditor + - performance + - memory + - netmonitor + - storage + - dom + - accessibility + - application + - dark + - light + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1447302 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1503568 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1587985 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1447302#c17 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1503568#c3 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1587985#c5 + data_sensitivity: + - interaction + notification_emails: + - dev-developer-tools@lists.mozilla.org + - accessibility@mozilla.com + expires: never +``` + +**N.B:** Glean Labels have a strict regex. +You may have to transform some categories to +`snake_case` so that they're safe for the data pipeline. + +**GIFFT:** This type of collection is mirrorable back to Firefox Telemetry via the +[Glean Interface For Firefox Telemetry][gifft]. +See [the guide][gifft] for instructions. + +**IPC Note:** Due to `set` not being a [commutative operation][ipc-docs], using `labeled_boolean` +on non-parent processes is forbidden. +This is a restriction that favours correctness over friendliness, +which we may revisit if enough use cases require it. +Please [contact us][glean-matrix] if you'd like us to do so. + +### Other Scalar-ish types: `rate`, `timespan`, `datetime`, `uuid` + +The Glean SDK provides some very handy higher-level metric types for specific data. +If your data +* Is two or more numbers that are related (like failure count vs total count), + then consider the [Glean `rate` metric type][rate-metric]. +* Is a single duration or span of time (like how long Firefox takes to start), + then consider the [Glean `timespan` metric type][timespan-metric]. +* Is a single point in time (like the most recent sync time), + then consider the [Glean `datetime` metric type][datetime-metric]. +* Is a unique identifier (like a session id), + then consider the [Glean `uuid` metric type][uuid-metric]. + +**GIFFT:** These types of collection are mirrorable back to Firefox Telemetry via the +[Glean Interface For Firefox Telemetry][gifft]. +See [the guide][gifft] for instructions. + +## Events - Use Glean's `event` + +[Telemetry Events][telemetry-events] +are a lesser-used form of data collection in Firefox Desktop. +Glean aimed to remove some of the stumbling blocks facing instrumentors when using events +in the [Glean `event` metric type][event-metric]: + +* Don't worry about enabling event categories. + In Glean all `events` are always on. +* No more event `name`. + Events in Glean follow the same `category.name.metric_name` + naming structure that other metrics do. +* No more `method`/`object`/`value`. + Events in Glean are just their identifier and an `extras` key/value dictionary. + +Since the two Event types aren't that analogous you will need to decide if your event +* Prefers to put its `method`/`object`/`value` in the `extras` dictionary +* Prefers to fold its `method`/`object`/`value` into its identifier + +**GIFFT:** Events are mirrorable back to Firefox Telemetry via the +[Glean Interface For Firefox Telemetry][gifft]. +See [the guide][gifft] for instructions. + +## Other: Environment, Crash Annotations, Use Counters, Etc - Ask on #glean:mozilla.org for assistance + +Telemetry has a lot of collection subsystems build adjacent to those already mentioned. +We have solutions for the common ones, +but they are entirely dependent on the specific use case. +Please [reach out to us][glean-matrix] to explain it to us so we can help you either +work within what Glean currently affords or +[design a new metric type for you][new-metric-type]. + +[book-of-glean]: https://mozilla.github.io/glean/book/index.html +[gc-ms]: https://glam.telemetry.mozilla.org/firefox/probe/gc_ms/explore +[histogram-accumulate]: https://searchfox.org/mozilla-central/rev/d59bdea4956040e16113b05296c56867f761735b/toolkit/components/telemetry/core/Telemetry.h#61 +[ipc-docs]: ../dev/ipc.md +[gifft]: gifft.md +[memory-total]: https://glam.telemetry.mozilla.org/firefox/probe/memory_total/explore +[migration-worksheet]: https://docs.google.com/spreadsheets/d/1uEK7zSIJDcGGmof9NywP5AwaovVQCv_Bm3iNqibtESI/edit#gid=0 +[boolean-metric]: https://mozilla.github.io/glean/book/reference/metrics/boolean.html +[labeled-boolean-metric]: https://mozilla.github.io/glean/book/reference/metrics/labeled_booleans.html +[counter-metric]: https://mozilla.github.io/glean/book/reference/metrics/counter.html +[labeled-counter-metric]: https://mozilla.github.io/glean/book/reference/metrics/labeled_counters.html +[string-metric]: https://mozilla.github.io/glean/book/reference/metrics/string.html +[labeled-string-metric]: https://mozilla.github.io/glean/book/reference/metrics/labeled_strings.html +[timespan-metric]: https://mozilla.github.io/glean/book/reference/metrics/timespan.html +[timing-distribution-metric]: https://mozilla.github.io/glean/book/reference/metrics/timing_distribution.html +[memory-distribution-metric]: https://mozilla.github.io/glean/book/reference/metrics/memory_distribution.html +[uuid-metric]: https://mozilla.github.io/glean/book/reference/metrics/uuid.html +[datetime-metric]: https://mozilla.github.io/glean/book/reference/metrics/datetime.html +[event-metric]: https://mozilla.github.io/glean/book/reference/metrics/event.html +[custom-distribution-metric]: https://mozilla.github.io/glean/book/reference/metrics/custom_distribution.html +[quantity-metric]: https://mozilla.github.io/glean/book/reference/metrics/quantity.html +[rate-metric]: https://mozilla.github.io/glean/book/reference/metrics/rate.html +[ipc-dev-doc]: ../dev/ipc.md +[gc-idle]: https://glam.telemetry.mozilla.org/firefox/probe/gc_slice_during_idle/explore +[new-metric-keyed-categorical]: https://bugzilla.mozilla.org/show_bug.cgi?id=1657470 +[new-metric-percent]: https://bugzilla.mozilla.org/show_bug.cgi?id=1657467 +[new-metric-type]: https://wiki.mozilla.org/Glean/Adding_or_changing_Glean_metric_types +[glean-matrix]: https://chat.mozilla.org/#/room/#glean:mozilla.org +[checkerboard-severity]: https://searchfox.org/mozilla-central/rev/d59bdea4956040e16113b05296c56867f761735b/gfx/layers/apz/src/CheckerboardEvent.cpp#44 +[telemetry-events]: /toolkit/components/telemetry/collection/events.rst +[telemetry-scalars]: /toolkit/components/telemetry/collection/scalars.rst +[telemetry-histograms]: /toolkit/components/telemetry/collection/histograms.rst +[repositories-yaml]: https://github.com/mozilla/probe-scraper/blob/main/repositories.yaml diff --git a/toolkit/components/glean/docs/user/new_definitions_file.md b/toolkit/components/glean/docs/user/new_definitions_file.md new file mode 100644 index 0000000000..976f34b209 --- /dev/null +++ b/toolkit/components/glean/docs/user/new_definitions_file.md @@ -0,0 +1,107 @@ +# New Metrics and Pings + +To add a new metric or ping to Firefox Desktop you should follow the +[Glean SDK documentation on the subject](https://mozilla.github.io/glean/book/user/adding-new-metrics.html), +with some few twists we detail herein: + +## IPC + +Firefox Desktop is made of multiple processes. +You can record data from any process in Firefox Desktop +[subject to certain conditions](../dev/ipc.md). + +If you will be recording data to this metric in multiple processes, +you should make yourself aware of those conditions. + +## Where do I Define new Metrics and Pings? + +Metrics and pings are defined in their definitions files +(`metrics.yaml` or `pings.yaml`, respectively). +But where can you find `metrics.yaml` or `pings.yaml`? + +If you're not the first person in your component to ask that question, +the answer is likely "in the root of your component". +Look for the definitions files near to where you are instrumenting your code. +Or you can look in +`toolkit/components/glean/metrics_index.py` +to see the list of all currently-known definitions files. + +If you _are_ the first person in your component to ask that question, +you get to choose where to start them! +We recommend adding them in the root of your component, next to a `moz.build`. +Be sure to link to this document at the top of the file! +It contains many useful tidbits of information that anyone adding new metrics should know. +Preferably, use this blank template to get started, +substituting your component's `product :: component` tag from +[the list](https://searchfox.org/mozilla-central/source/toolkit/components/glean/tags.yaml): + +```yaml +# 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 http://mozilla.org/MPL/2.0/. + +# Adding a new metric? We have docs for that! +# https://firefox-source-docs.mozilla.org/toolkit/components/glean/user/new_definitions_file.html + +--- +$schema: moz://mozilla.org/schemas/glean/metrics/2-0-0 +$tags: + - 'Your Product :: Your Component' + +``` + +If you add a new definitions file, be sure to edit +`toolkit/components/glean/metrics_index.py`, +adding your definitions files to the Python lists therein. +If you don't, no API will be generated for your metrics and your build will fail. +You will have to decide which products your metrics will be used in. +For code that's also used in other Gecko-based products (Firefox Desktop, Firefox for Android, Focus for Android), use `gecko_metrics`. +For Desktop-only instrumentation use `firefox_desktop_metrics`. +For other products use their respective lists. + +Changes to `metrics_index.py` are automatically reflected in the data pipeline once a day +using the [fog-updater automation in probe-scraper](https://github.com/mozilla/probe-scraper/tree/main/fog-updater). +Data will not show up in datasets and tools until this happens. +If something is unclear or data is not showing up in time you will need to file a bug in +`Data Platform and Tools :: General`. + +If you have any questions, be sure to ask on +[the #glean channel](https://chat.mozilla.org/#/room/#glean:mozilla.org). + +**Note:** Do _not_ use `toolkit/components/glean/metrics.yaml` +or `toolkit/components/glean/pings.yaml`. +These are for metrics instrumenting the code under `toolkit/components/glean` +and are not general-purpose locations for adding metrics and pings. + +## How does Expiry Work? + +In FOG, +unlike in other Glean-SDK-using projects, +metrics expire based on Firefox application version. +This is to allow metrics to be valid over the entire life of an application version, +whether that is the 4-6 weeks of usual releases or the 13 months of ESR releases. + +There are three values accepted in the `expires` field of `metrics.yaml`s for FOG: +* `"X"` (where `X` is the major portion of a Firefox Desktop version) - + The metric will be expired when the `MOZ_APP_VERSION` reaches or exceeds `X`. + (For example, when the Firefox Version is `88.0a1`, + all metrics marked with `expires: "88"` or lower will be expired.) + This is the recommended form for all new metrics to ensure they stop recording when they stop being relevant. +* `expired` - For marking a metric as manually expired. + Not usually used, but sometimes helpful for internal tests. +* `never` - For marking a metric as part of a permanent data collection. + Metrics marked with `never` must have + [instrumentation tests](instrumentation_tests). + +For more information on what expiry means and the +`metrics.yaml` format, see +[the Glean SDK docs](https://mozilla.github.io/glean/book/user/metric-parameters.html) +on this subject. Some quick facts: + +* Data collected to expired metrics is not recorded or sent. +* Recording to expired metrics is not an error at runtime. +* Expired metrics being in a `metrics.yaml` is a linting error in `glean_parser`. +* Expired (and non-expired) metrics that are no longer useful should be promptly removed from your `metrics.yaml`. + This reduces the size and improves the performance of Firefox + (and speeds up the Firefox build process) + by decreasing the amount of code that needs to be generated. diff --git a/toolkit/components/glean/ipc/FOGIPC.cpp b/toolkit/components/glean/ipc/FOGIPC.cpp new file mode 100644 index 0000000000..20258498f8 --- /dev/null +++ b/toolkit/components/glean/ipc/FOGIPC.cpp @@ -0,0 +1,545 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#include "FOGIPC.h" + +#include <limits> +#include "mozilla/glean/fog_ffi_generated.h" +#include "mozilla/glean/GleanMetrics.h" +#include "mozilla/dom/BrowsingContextGroup.h" +#include "mozilla/dom/ContentChild.h" +#include "mozilla/dom/ContentParent.h" +#include "mozilla/dom/DocGroup.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/gfx/GPUChild.h" +#include "mozilla/gfx/GPUParent.h" +#include "mozilla/gfx/GPUProcessManager.h" +#include "mozilla/Hal.h" +#include "mozilla/MozPromise.h" +#include "mozilla/net/SocketProcessChild.h" +#include "mozilla/net/SocketProcessParent.h" +#include "mozilla/ProcInfo.h" +#include "mozilla/RDDChild.h" +#include "mozilla/RDDParent.h" +#include "mozilla/RDDProcessManager.h" +#include "mozilla/ipc/UtilityProcessChild.h" +#include "mozilla/ipc/UtilityProcessManager.h" +#include "mozilla/ipc/UtilityProcessParent.h" +#include "mozilla/ipc/UtilityProcessSandboxing.h" +#include "mozilla/Unused.h" +#include "GMPPlatform.h" +#include "GMPServiceParent.h" +#include "nsIClassifiedChannel.h" +#include "nsIXULRuntime.h" +#include "nsTArray.h" +#include "nsThreadUtils.h" + +using mozilla::dom::ContentParent; +using mozilla::gfx::GPUChild; +using mozilla::gfx::GPUProcessManager; +using mozilla::ipc::ByteBuf; +using mozilla::ipc::UtilityProcessChild; +using mozilla::ipc::UtilityProcessManager; +using mozilla::ipc::UtilityProcessParent; +using FlushFOGDataPromise = mozilla::dom::ContentParent::FlushFOGDataPromise; + +namespace geckoprofiler::markers { + +using namespace mozilla; + +struct ProcessingTimeMarker { + static constexpr Span<const char> MarkerTypeName() { + return MakeStringSpan("ProcessingTime"); + } + static void StreamJSONMarkerData(baseprofiler::SpliceableJSONWriter& aWriter, + int64_t aDiffMs, + const ProfilerString8View& aType, + const ProfilerString8View& aTrackerType) { + aWriter.IntProperty("time", aDiffMs); + aWriter.StringProperty("label", aType); + if (aTrackerType.Length() > 0) { + aWriter.StringProperty("tracker", aTrackerType); + } + } + static MarkerSchema MarkerTypeDisplay() { + using MS = MarkerSchema; + MS schema{MS::Location::MarkerChart, MS::Location::MarkerTable}; + schema.AddKeyLabelFormat("time", "Recorded Time", MS::Format::Milliseconds); + schema.AddKeyLabelFormat("tracker", "Tracker Type", MS::Format::String); + schema.SetTooltipLabel("{marker.name} - {marker.data.label}"); + schema.SetTableLabel( + "{marker.name} - {marker.data.label}: {marker.data.time}"); + return schema; + } +}; + +} // namespace geckoprofiler::markers + +namespace mozilla::glean { + +#ifdef NIGHTLY_BUILD +// The 2 following static global variables are set within RecordPowerMetrics +// so that RecordThreadCpuUse can avoid computing the Glean process type +// (matching a label of per_process_type_labels). +// This is useful because RecordThreadCpuUse will either be called in a loop +// for every thread (recomputing the process type for every thread would be +// expensive), or will be called off main thread when a thread is unregisters +// itself (some APIs needed to compute the process type might not be available +// off main thread). +// It is fine to call RecordThreadCpuUse during startup before the first +// RecordPowerMetrics call. In that case the parent process will be recorded +// as inactive, and other processes will be ignored (content processes start +// in the 'prealloc' type for which we don't record per-thread CPU use data). +using LabeledCounterMetric = const impl::Labeled<impl::CounterMetric>; +static Atomic<LabeledCounterMetric*> gCpuTimePerThreadMetric(nullptr); +static Atomic<LabeledCounterMetric*> gWakeupsPerThreadMetric(nullptr); + +// These 2 macros are only meant to reduce code duplication, there is no +// requirement of the 2 variables being set atomically as a single value. +# define SET_PER_THREAD_CPU_METRICS(aProcessType) \ + gCpuTimePerThreadMetric = &power_cpu_ms_per_thread::aProcessType; \ + gWakeupsPerThreadMetric = &power_wakeups_per_thread::aProcessType; + +# define RESET_PER_THREAD_CPU_METRICS() \ + gCpuTimePerThreadMetric = nullptr; \ + gWakeupsPerThreadMetric = nullptr; + +void RecordThreadCpuUse(const nsACString& aThreadName, uint64_t aCpuTimeMs, + uint64_t aWakeCount) { + // Copy the values of the atomics to local variables so that we don't have to + // worry about other threads changing them during the execution of this + // function. + LabeledCounterMetric* cpuTimeMetric = gCpuTimePerThreadMetric; + LabeledCounterMetric* wakeupsMetric = gWakeupsPerThreadMetric; + + if (!cpuTimeMetric || !wakeupsMetric) { + if (XRE_IsParentProcess()) { + // The metrics can be null for the parent process during startup, + // and we want to record during that time. + SET_PER_THREAD_CPU_METRICS(parent_inactive); + cpuTimeMetric = gCpuTimePerThreadMetric; + wakeupsMetric = gWakeupsPerThreadMetric; + if (!cpuTimeMetric || !wakeupsMetric) { + return; + } + } else { + // We are not interested in per-thread CPU use data for the current + // process type. + return; + } + } + + nsAutoCString threadName(aThreadName); + for (size_t i = 0; i < threadName.Length(); ++i) { + const char c = threadName.CharAt(i); + + // Valid characters. + if ((c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || c == '-' || + c == '_') { + continue; + } + + // Should only use lower case characters + if (c >= 'A' && c <= 'Z') { + threadName.SetCharAt(c + ('a' - 'A'), i); + continue; + } + + // Replace everything else with _ + threadName.SetCharAt('_', i); + } + + if (aCpuTimeMs != 0 && + MOZ_LIKELY(aCpuTimeMs < std::numeric_limits<int32_t>::max())) { + cpuTimeMetric->Get(threadName).Add(int32_t(aCpuTimeMs)); + } + + if (aWakeCount != 0 && + MOZ_LIKELY(aWakeCount < std::numeric_limits<int32_t>::max())) { + wakeupsMetric->Get(threadName).Add(int32_t(aWakeCount)); + } +} +#else // ifdef NIGHTLY_BUILD +# define SET_PER_THREAD_CPU_METRICS(aProcessType) +# define RESET_PER_THREAD_CPU_METRICS() +#endif + +void GetTrackerType(nsAutoCString& aTrackerType) { + using namespace mozilla::dom; + uint32_t trackingFlags = + (nsIClassifiedChannel::CLASSIFIED_CRYPTOMINING | + nsIClassifiedChannel::CLASSIFIED_FINGERPRINTING | + nsIClassifiedChannel::CLASSIFIED_TRACKING | + nsIClassifiedChannel::CLASSIFIED_TRACKING_AD | + nsIClassifiedChannel::CLASSIFIED_TRACKING_ANALYTICS | + nsIClassifiedChannel::CLASSIFIED_TRACKING_SOCIAL); + AutoTArray<RefPtr<BrowsingContextGroup>, 5> bcGroups; + BrowsingContextGroup::GetAllGroups(bcGroups); + for (auto& bcGroup : bcGroups) { + AutoTArray<DocGroup*, 5> docGroups; + bcGroup->GetDocGroups(docGroups); + for (auto* docGroup : docGroups) { + for (Document* doc : *docGroup) { + nsCOMPtr<nsIClassifiedChannel> classifiedChannel = + do_QueryInterface(doc->GetChannel()); + if (classifiedChannel) { + uint32_t classificationFlags = + classifiedChannel->GetThirdPartyClassificationFlags(); + trackingFlags &= classificationFlags; + if (!trackingFlags) { + return; + } + } + } + } + } + + // The if-elseif-else chain works because the tracker types listed here are + // currently mutually exclusive and should be maintained that way by policy. + if (trackingFlags == nsIClassifiedChannel::CLASSIFIED_TRACKING_AD) { + aTrackerType = "ad"; + } else if (trackingFlags == + nsIClassifiedChannel::CLASSIFIED_TRACKING_ANALYTICS) { + aTrackerType = "analytics"; + } else if (trackingFlags == + nsIClassifiedChannel::CLASSIFIED_TRACKING_SOCIAL) { + aTrackerType = "social"; + } else if (trackingFlags == nsIClassifiedChannel::CLASSIFIED_CRYPTOMINING) { + aTrackerType = "cryptomining"; + } else if (trackingFlags == nsIClassifiedChannel::CLASSIFIED_FINGERPRINTING) { + aTrackerType = "fingerprinting"; + } else if (trackingFlags == nsIClassifiedChannel::CLASSIFIED_TRACKING) { + // CLASSIFIED_TRACKING means we were not able to identify the type of + // classification. + aTrackerType = "unknown"; + } +} + +void RecordPowerMetrics() { + static uint64_t previousCpuTime = 0, previousGpuTime = 0; + + uint64_t cpuTime, newCpuTime = 0; + if (NS_SUCCEEDED(GetCpuTimeSinceProcessStartInMs(&cpuTime)) && + cpuTime > previousCpuTime) { + newCpuTime = cpuTime - previousCpuTime; + } + + uint64_t gpuTime, newGpuTime = 0; + // Avoid loading gdi32.dll for the Socket process where the GPU is never used. + if (!XRE_IsSocketProcess() && + NS_SUCCEEDED(GetGpuTimeSinceProcessStartInMs(&gpuTime)) && + gpuTime > previousGpuTime) { + newGpuTime = gpuTime - previousGpuTime; + } + + if (!newCpuTime && !newGpuTime) { + // Nothing to record. + return; + } + + // Compute the process type string. + nsAutoCString type(XRE_GetProcessTypeString()); + nsAutoCString trackerType; + if (XRE_IsContentProcess()) { + auto* cc = dom::ContentChild::GetSingleton(); + if (cc) { + type.Assign(dom::RemoteTypePrefix(cc->GetRemoteType())); + if (StringBeginsWith(type, WEB_REMOTE_TYPE)) { + type.AssignLiteral("web"); + switch (cc->GetProcessPriority()) { + case hal::PROCESS_PRIORITY_BACKGROUND: + type.AppendLiteral(".background"); + SET_PER_THREAD_CPU_METRICS(content_background); + break; + case hal::PROCESS_PRIORITY_FOREGROUND: + type.AppendLiteral(".foreground"); + SET_PER_THREAD_CPU_METRICS(content_foreground); + break; + case hal::PROCESS_PRIORITY_BACKGROUND_PERCEIVABLE: + type.AppendLiteral(".background-perceivable"); + RESET_PER_THREAD_CPU_METRICS(); + break; + default: + RESET_PER_THREAD_CPU_METRICS(); + break; + } + } + GetTrackerType(trackerType); + } else { + RESET_PER_THREAD_CPU_METRICS(); + } + } else if (XRE_IsParentProcess()) { + if (nsContentUtils::GetUserIsInteracting()) { + type.AssignLiteral("parent.active"); + SET_PER_THREAD_CPU_METRICS(parent_active); + } else { + type.AssignLiteral("parent.inactive"); + SET_PER_THREAD_CPU_METRICS(parent_inactive); + } + hal::WakeLockInformation info; + GetWakeLockInfo(u"video-playing"_ns, &info); + if (info.numLocks() != 0 && info.numHidden() < info.numLocks()) { + type.AppendLiteral(".playing-video"); + } else { + GetWakeLockInfo(u"audio-playing"_ns, &info); + if (info.numLocks()) { + type.AppendLiteral(".playing-audio"); + } + } + } else if (XRE_IsGPUProcess()) { + SET_PER_THREAD_CPU_METRICS(gpu_process); + } else { + RESET_PER_THREAD_CPU_METRICS(); + } + + if (newCpuTime) { + // The counters are reset at least once a day. Assuming all cores are used + // continuously, an int32 can hold the data for 24.85 cores. + // This should be fine for now, but may overflow in the future. + // Bug 1751277 tracks a newer, bigger counter. + int32_t nNewCpuTime = int32_t(newCpuTime); + if (newCpuTime < std::numeric_limits<int32_t>::max()) { + power::total_cpu_time_ms.Add(nNewCpuTime); + power::cpu_time_per_process_type_ms.Get(type).Add(nNewCpuTime); + if (!trackerType.IsEmpty()) { + power::cpu_time_per_tracker_type_ms.Get(trackerType).Add(nNewCpuTime); + } + } else { + power::cpu_time_bogus_values.Add(1); + } + PROFILER_MARKER("Process CPU Time", OTHER, {}, ProcessingTimeMarker, + nNewCpuTime, type, trackerType); + previousCpuTime += newCpuTime; + } + + if (newGpuTime) { + int32_t nNewGpuTime = int32_t(newGpuTime); + if (newGpuTime < std::numeric_limits<int32_t>::max()) { + power::total_gpu_time_ms.Add(nNewGpuTime); + power::gpu_time_per_process_type_ms.Get(type).Add(nNewGpuTime); + } else { + power::gpu_time_bogus_values.Add(1); + } + PROFILER_MARKER("Process GPU Time", OTHER, {}, ProcessingTimeMarker, + nNewGpuTime, type, trackerType); + previousGpuTime += newGpuTime; + } + + profiler_record_wakeup_count(type); +} + +/** + * Flush your data ASAP, either because the parent process is asking you to + * or because the process is about to shutdown. + * + * @param aResolver - The function you need to call with the bincoded, + * serialized payload that the Rust impl hands you. + */ +void FlushFOGData(std::function<void(ipc::ByteBuf&&)>&& aResolver) { + // Record power metrics right before data is sent to the parent. + RecordPowerMetrics(); + + ByteBuf buf; + uint32_t ipcBufferSize = impl::fog_serialize_ipc_buf(); + bool ok = buf.Allocate(ipcBufferSize); + if (!ok) { + return; + } + uint32_t writtenLen = impl::fog_give_ipc_buf(buf.mData, buf.mLen); + if (writtenLen != ipcBufferSize) { + return; + } + aResolver(std::move(buf)); +} + +/** + * Called by FOG on the parent process when it wants to flush all its + * children's data. + * @param aResolver - The function that'll be called with the results. + */ +void FlushAllChildData( + std::function<void(nsTArray<ipc::ByteBuf>&&)>&& aResolver) { + auto timerId = fog_ipc::flush_durations.Start(); + + nsTArray<ContentParent*> parents; + ContentParent::GetAll(parents); + nsTArray<RefPtr<FlushFOGDataPromise>> promises; + for (auto* parent : parents) { + promises.EmplaceBack(parent->SendFlushFOGData()); + } + + if (GPUProcessManager* gpuManager = GPUProcessManager::Get()) { + if (GPUChild* gpuChild = gpuManager->GetGPUChild()) { + promises.EmplaceBack(gpuChild->SendFlushFOGData()); + } + } + + if (RDDProcessManager* rddManager = RDDProcessManager::Get()) { + if (RDDChild* rddChild = rddManager->GetRDDChild()) { + promises.EmplaceBack(rddChild->SendFlushFOGData()); + } + } + + if (net::SocketProcessParent* socketParent = + net::SocketProcessParent::GetSingleton()) { + promises.EmplaceBack(socketParent->SendFlushFOGData()); + } + + { + RefPtr<mozilla::gmp::GeckoMediaPluginServiceParent> gmps( + mozilla::gmp::GeckoMediaPluginServiceParent::GetSingleton()); + // There can be multiple Gecko Media Plugin processes, but iterating + // through them requires locking a mutex and the IPCs need to be sent + // from a different thread, so it's better to let the + // GeckoMediaPluginServiceParent code do it for us. + gmps->SendFlushFOGData(promises); + } + + if (RefPtr<UtilityProcessManager> utilityManager = + UtilityProcessManager::GetIfExists()) { + for (RefPtr<UtilityProcessParent>& parent : + utilityManager->GetAllProcessesProcessParent()) { + promises.EmplaceBack(parent->SendFlushFOGData()); + } + } + + if (promises.Length() == 0) { + // No child processes at the moment. Resolve synchronously. + fog_ipc::flush_durations.Cancel(std::move(timerId)); + nsTArray<ipc::ByteBuf> results; + aResolver(std::move(results)); + return; + } + + // If fog.ipc.flush_failures ever gets too high: + // TODO: Don't throw away resolved data if some of the promises reject. + // (not sure how, but it'll mean not using ::All... maybe a custom copy of + // AllPromiseHolder? Might be impossible outside MozPromise.h) + FlushFOGDataPromise::All(GetCurrentSerialEventTarget(), promises) + ->Then(GetCurrentSerialEventTarget(), __func__, + [aResolver = std::move(aResolver), timerId]( + FlushFOGDataPromise::AllPromiseType::ResolveOrRejectValue&& + aValue) { + fog_ipc::flush_durations.StopAndAccumulate(std::move(timerId)); + if (aValue.IsResolve()) { + aResolver(std::move(aValue.ResolveValue())); + } else { + fog_ipc::flush_failures.Add(1); + nsTArray<ipc::ByteBuf> results; + aResolver(std::move(results)); + } + }); +} + +/** + * A child process has sent you this buf as a treat. + * @param buf - a bincoded serialized payload that the Rust impl understands. + */ +void FOGData(ipc::ByteBuf&& buf) { + fog_ipc::buffer_sizes.Accumulate(buf.mLen); + impl::fog_use_ipc_buf(buf.mData, buf.mLen); +} + +/** + * Called by FOG on a child process when it wants to send a buf to the parent. + * @param buf - a bincoded serialized payload that the Rust impl understands. + */ +void SendFOGData(ipc::ByteBuf&& buf) { + switch (XRE_GetProcessType()) { + case GeckoProcessType_Content: + mozilla::dom::ContentChild::GetSingleton()->SendFOGData(std::move(buf)); + break; + case GeckoProcessType_GMPlugin: { + mozilla::gmp::SendFOGData(std::move(buf)); + } break; + case GeckoProcessType_GPU: + Unused << mozilla::gfx::GPUParent::GetSingleton()->SendFOGData( + std::move(buf)); + break; + case GeckoProcessType_RDD: + Unused << mozilla::RDDParent::GetSingleton()->SendFOGData(std::move(buf)); + break; + case GeckoProcessType_Socket: + Unused << net::SocketProcessChild::GetSingleton()->SendFOGData( + std::move(buf)); + break; + case GeckoProcessType_Utility: + Unused << ipc::UtilityProcessChild::GetSingleton()->SendFOGData( + std::move(buf)); + break; + default: + MOZ_ASSERT_UNREACHABLE("Unsuppored process type"); + } +} + +/** + * Called on the parent process to ask all child processes for data, + * sending it all down into Rust to be used. + */ +RefPtr<GenericPromise> FlushAndUseFOGData() { + // Record power metrics on the parent before sending requests to child + // processes. + RecordPowerMetrics(); + + RefPtr<GenericPromise::Private> ret = new GenericPromise::Private(__func__); + std::function<void(nsTArray<ByteBuf> &&)> resolver = + [ret](nsTArray<ByteBuf>&& bufs) { + for (ByteBuf& buf : bufs) { + FOGData(std::move(buf)); + } + ret->Resolve(true, __func__); + }; + FlushAllChildData(std::move(resolver)); + return ret; +} + +void TestTriggerMetrics(uint32_t aProcessType, + const RefPtr<dom::Promise>& promise) { + switch (aProcessType) { + case nsIXULRuntime::PROCESS_TYPE_GMPLUGIN: { + RefPtr<mozilla::gmp::GeckoMediaPluginServiceParent> gmps( + mozilla::gmp::GeckoMediaPluginServiceParent::GetSingleton()); + gmps->TestTriggerMetrics()->Then( + GetCurrentSerialEventTarget(), __func__, + [promise]() { promise->MaybeResolveWithUndefined(); }, + [promise]() { promise->MaybeRejectWithUndefined(); }); + } break; + case nsIXULRuntime::PROCESS_TYPE_GPU: + gfx::GPUProcessManager::Get()->TestTriggerMetrics()->Then( + GetCurrentSerialEventTarget(), __func__, + [promise]() { promise->MaybeResolveWithUndefined(); }, + [promise]() { promise->MaybeRejectWithUndefined(); }); + break; + case nsIXULRuntime::PROCESS_TYPE_RDD: + RDDProcessManager::Get()->TestTriggerMetrics()->Then( + GetCurrentSerialEventTarget(), __func__, + [promise]() { promise->MaybeResolveWithUndefined(); }, + [promise]() { promise->MaybeRejectWithUndefined(); }); + break; + case nsIXULRuntime::PROCESS_TYPE_SOCKET: + Unused << net::SocketProcessParent::GetSingleton() + ->SendTestTriggerMetrics() + ->Then( + GetCurrentSerialEventTarget(), __func__, + [promise]() { promise->MaybeResolveWithUndefined(); }, + [promise]() { promise->MaybeRejectWithUndefined(); }); + break; + case nsIXULRuntime::PROCESS_TYPE_UTILITY: + Unused << ipc::UtilityProcessManager::GetSingleton() + ->GetProcessParent(ipc::SandboxingKind::GENERIC_UTILITY) + ->SendTestTriggerMetrics() + ->Then( + GetCurrentSerialEventTarget(), __func__, + [promise]() { promise->MaybeResolveWithUndefined(); }, + [promise]() { promise->MaybeRejectWithUndefined(); }); + break; + default: + promise->MaybeRejectWithUndefined(); + break; + } +} + +} // namespace mozilla::glean diff --git a/toolkit/components/glean/ipc/FOGIPC.h b/toolkit/components/glean/ipc/FOGIPC.h new file mode 100644 index 0000000000..3701f71838 --- /dev/null +++ b/toolkit/components/glean/ipc/FOGIPC.h @@ -0,0 +1,102 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#ifndef FOGIPC_h__ +#define FOGIPC_h__ + +#include <functional> + +#include "mozilla/Maybe.h" +#include "mozilla/MozPromise.h" +#include "nsTArrayForwardDeclare.h" + +namespace mozilla { +namespace ipc { +class ByteBuf; +} // namespace ipc +} // namespace mozilla + +// This module provides the interface for FOG to communicate between processes. + +namespace mozilla { +namespace glean { + +/** + * The parent process is asking you to flush your data ASAP. + * + * @param aResolver - The function you need to call with the bincoded, + * serialized payload that the Rust impl hands you. + */ +void FlushFOGData(std::function<void(ipc::ByteBuf&&)>&& aResolver); + +/** + * Called by FOG on the parent process when it wants to flush all its + * children's data. + * @param aResolver - The function that'll be called with the results. + */ +void FlushAllChildData( + std::function<void(nsTArray<ipc::ByteBuf>&&)>&& aResolver); + +/** + * A child process has sent you this buf as a treat. + * @param buf - a bincoded serialized payload that the Rust impl understands. + */ +void FOGData(ipc::ByteBuf&& buf); + +/** + * Called by FOG on a child process when it wants to send a buf to the parent. + * @param buf - a bincoded serialized payload that the Rust impl understands. + */ +void SendFOGData(ipc::ByteBuf&& buf); + +/** + * Called on the parent process to ask all child processes for data, + * sending it all down into Rust to be used. + * + * @returns a Promise that resolves when the data has made it to the parent. + */ +RefPtr<GenericPromise> FlushAndUseFOGData(); + +/** + * ** Test-only Method ** + * + * Trigger GMP, GPU, RDD or Socket process test instrumentation. + * + * @param processType - one of the PROCESS_TYPE_* constants from nsIXULRuntime. + * @param promise - a promise that will be resolved when the data has made it to + * the target process. + */ +void TestTriggerMetrics(uint32_t processType, + const RefPtr<dom::Promise>& promise); + +#ifdef NIGHTLY_BUILD +/** + * This function records the CPU activity (CPU time used and wakeup count) + * of a specific thread. It is called only by profiler code, either multiple + * times in a row when RecordPowerMetrics asks the profiler to record + * the wakeup counts of all threads, or once when a thread is unregistered. + * + * @param aThreadName The name of the thread for which the CPU data is being + * recorded. + * The name will be converted to lower case, and characters + * that are not valid for glean labels will be replaced with + * '_'. The resulting name should be part of the + * per_thread_labels static list of labels defined in + * toolkit/components/processtools/metrics.yaml. + * @param aCpuTimeMs CPU time in miliseconds since the last time CPU use data + * was recorded for this thread. + * @param aWakeCount How many times the thread woke up since the previous time + * CPU use data was recorded for this thread. + */ +void RecordThreadCpuUse(const nsACString& aThreadName, uint64_t aCpuTimeMs, + uint64_t aWakeCount); +#endif + +void RecordPowerMetrics(); + +} // namespace glean +} // namespace mozilla + +#endif // FOGIPC_h__ diff --git a/toolkit/components/glean/ipc/Support.cpp b/toolkit/components/glean/ipc/Support.cpp new file mode 100644 index 0000000000..479b9b42b0 --- /dev/null +++ b/toolkit/components/glean/ipc/Support.cpp @@ -0,0 +1,74 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */ +/* 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 http://mozilla.org/MPL/2.0/. */ + +// This file is for support functions for the Rust IPC module. +// Some information just isn't available to Rust and must be made available over +// FFI. +#include "FOGIPC.h" +#include "mozilla/AppShutdown.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/Unused.h" +#include "nsThreadUtils.h" + +using mozilla::AppShutdown; +using mozilla::RunOnShutdown; +using mozilla::ShutdownPhase; +using mozilla::Unused; +using mozilla::glean::FlushFOGData; +using mozilla::glean::SendFOGData; +using mozilla::ipc::ByteBuf; + +extern "C" { +void FOG_RegisterContentChildShutdown() { + if (AppShutdown::IsInOrBeyond(ShutdownPhase::AppShutdownConfirmed)) { + return; + } + + // If there is no main thread (too early in startup or too late in shutdown), + // there's nothing we can do but log. + bool failed = + NS_FAILED(NS_DispatchToMainThread(NS_NewRunnableFunction(__func__, [] { + // By the time the main thread dispatched this, it may already be too + // late. + if (AppShutdown::IsInOrBeyond(ShutdownPhase::AppShutdownConfirmed)) { + return; + } + RunOnShutdown( + [] { + FlushFOGData( + [](ByteBuf&& aBuf) { SendFOGData(std::move(aBuf)); }); + }, + ShutdownPhase::AppShutdownConfirmed); + }))); + if (failed) { + NS_WARNING( + "Failed to register FOG content child shutdown flush. " + "Will lose shutdown data and leak a runnable."); + mozilla::glean::fog_ipc::shutdown_registration_failures.Add(1); + } +} + +int FOG_GetProcessType() { return XRE_GetProcessType(); } + +/** + * Called from FOG IPC in Rust when the IPC Payload might be getting full. + * We should probably flush before we reach the max IPC message size. + */ +void FOG_IPCPayloadFull() { + // NS_DispatchToMainThread can leak the runnable (bug 1787589), so let's be + // sure not to create it too late in shutdown. + // We choose XPCOMShutdown to match gFOG->Shutdown(). + if (AppShutdown::IsInOrBeyond(ShutdownPhase::XPCOMShutdown)) { + return; + } + // FOG IPC must happen on the main thread until bug 1641989. + // If there is no main thread (too early in startup or too late in shutdown), + // there's nothing we can do but log. + Unused << NS_WARN_IF(NS_FAILED(NS_DispatchToMainThread( + NS_NewRunnableFunction("FOG IPC Payload getting full", [] { + FlushFOGData([](ByteBuf&& aBuf) { SendFOGData(std::move(aBuf)); }); + })))); +} +} diff --git a/toolkit/components/glean/metrics.yaml b/toolkit/components/glean/metrics.yaml new file mode 100644 index 0000000000..40961c5297 --- /dev/null +++ b/toolkit/components/glean/metrics.yaml @@ -0,0 +1,137 @@ +# 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 http://mozilla.org/MPL/2.0/. + +# This file is for Internal FOG Use Only. +# Please don't add anything here unless you have the permission of a +# Telemetry Module Peer. + +--- +$schema: moz://mozilla.org/schemas/glean/metrics/2-0-0 +$tags: + - 'Toolkit :: Telemetry' + +fog: + initialization: + type: timespan + time_unit: nanosecond + description: | + Time the FOG initialization takes. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1662123 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1662123#c3 + data_sensitivity: + - technical + notification_emails: + - jrediger@mozilla.com + - glean-team@mozilla.com + expires: never + + failed_idle_registration: + type: boolean + lifetime: application + description: | + True if we failed to register with the idle service. Absent otherwise. + Means IPC probably isn't working well. + Child-process data will likely be absent, or incomplete. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1694739 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1694739#c5 + data_sensitivity: + - technical + notification_emails: + - chutten@mozilla.com + - glean-team@mozilla.com + expires: never + +fog.ipc: + replay_failures: + type: counter + description: | + The number of times the ipc buffer failed to be replayed in the + parent process. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1664461 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1664461 + data_sensitivity: + - technical + notification_emails: + - chutten@mozilla.com + - glean-team@mozilla.com + expires: never + + buffer_sizes: + type: memory_distribution + memory_unit: byte + description: | + The number and size of the IPC buffers being received over FOG IPC. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1694739 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1694739#c5 + data_sensitivity: + - technical + notification_emails: + - chutten@mozilla.com + - glean-team@mozilla.com + expires: never + + flush_durations: + type: timing_distribution + time_unit: microsecond + description: | + The length of time between asking the child processes for their + IPC buffers and all of them being received by the parent. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1694739 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1694739#c5 + data_sensitivity: + - technical + notification_emails: + - chutten@mozilla.com + - glean-team@mozilla.com + expires: never + + flush_failures: + type: counter + description: | + The number of times we failed to flush all non-parent-process data, + throwing even partial results into the trash. + If this number is high, we might consider writing custom `MozPromise`- + handling code instead of using `MozPromise::All`. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1729026 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1729026 + data_sensitivity: + - technical + notification_emails: + - chutten@mozilla.com + - glean-team@mozilla.com + expires: never + + shutdown_registration_failures: + type: counter + description: | + The number of times we tried to register shutdown flush routines for + content child processes, and failed (probably because there was no main + thread). + As a result there may be data loss from content child processes. + Large or rising number of clients experiencing this indicates we should + perhaps refactor content child shutdown in FOG to try harder to register + flush operations. + Will likely be obsoleted by bug 1641989. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1766977 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1766977 + data_sensitivity: + - technical + notification_emails: + - chutten@mozilla.com + - glean-team@mozilla.com + expires: never diff --git a/toolkit/components/glean/metrics_index.py b/toolkit/components/glean/metrics_index.py new file mode 100644 index 0000000000..fc6cf9cb09 --- /dev/null +++ b/toolkit/components/glean/metrics_index.py @@ -0,0 +1,102 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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 http://mozilla.org/MPL/2.0/. + +# ATTENTION: Changes to this file will need to be reflected in probe-scraper[1]. +# This should happen automatically once a day. +# If something is unclear or data is not showing up in time +# you will need to file a bug in Data Platform and Tools :: General. +# +# [1] https://github.com/mozilla/probe-scraper + +# Metrics that are sent by Gecko and everyone using Gecko +# (Firefox Desktop, Firefox for Android, Focus for Android, etc.). +# Order is lexicographical, enforced by t/c/glean/tests/pytest/test_yaml_indices.py +gecko_metrics = [ + "browser/base/content/metrics.yaml", + "dom/media/metrics.yaml", + "dom/media/webrtc/metrics.yaml", + "dom/metrics.yaml", + "gfx/metrics.yaml", + "netwerk/metrics.yaml", + "netwerk/protocol/http/metrics.yaml", + "toolkit/components/cookiebanners/metrics.yaml", + "toolkit/components/glean/metrics.yaml", + "toolkit/components/pdfjs/metrics.yaml", + "toolkit/components/processtools/metrics.yaml", +] + +# Metrics that are sent by Firefox Desktop +# Order is lexicographical, enforced by t/c/glean/tests/pytest/test_yaml_indices.py +firefox_desktop_metrics = [ + "browser/components/metrics.yaml", + "browser/components/newtab/metrics.yaml", + "browser/components/search/metrics.yaml", + "browser/components/urlbar/metrics.yaml", + "browser/modules/metrics.yaml", + "toolkit/components/extensions/metrics.yaml", + "toolkit/components/nimbus/metrics.yaml", + "toolkit/components/search/metrics.yaml", + "toolkit/components/telemetry/dap/metrics.yaml", + "toolkit/components/telemetry/metrics.yaml", + "toolkit/xre/metrics.yaml", +] + +# Metrics that are sent by the Firefox Desktop Background Update Task +# Order is lexicographical, enforced by t/c/glean/tests/pytest/test_yaml_indices.py +background_update_metrics = [ + "toolkit/mozapps/update/metrics.yaml", +] + +# Test metrics +# Order is lexicographical, enforced by t/c/glean/tests/pytest/test_yaml_indices.py +test_metrics = [ + "toolkit/components/glean/tests/test_metrics.yaml", +] + +# The list of all Glean metrics.yaml files, relative to the top src dir. +# ONLY TO BE MODIFIED BY FOG PEERS! +metrics_yamls = ( + gecko_metrics + firefox_desktop_metrics + background_update_metrics + test_metrics +) + +# Pings that are sent by Gecko and everyone using Gecko +# (Firefox Desktop, Firefox for Android, Focus for Android, etc.). +# Order is lexicographical, enforced by t/c/glean/tests/pytest/test_yaml_indices.py +gecko_pings = [ + "dom/pings.yaml", + "toolkit/components/glean/pings.yaml", +] + +# Pings that are sent by Firefox Desktop. +# Order is lexicographical, enforced by t/c/glean/tests/pytest/test_yaml_indices.py +firefox_desktop_pings = [ + "browser/components/newtab/pings.yaml", + "toolkit/components/telemetry/pings.yaml", +] + +# Pings that are sent by the Firefox Desktop Background Update Task +# Order is lexicographical, enforced by t/c/glean/tests/pytest/test_yaml_indices.py +background_update_pings = [ + "toolkit/mozapps/update/pings.yaml", +] + +# Test pings +# Order is lexicographical, enforced by t/c/glean/tests/pytest/test_yaml_indices.py +test_pings = [ + "toolkit/components/glean/tests/test_pings.yaml", +] + +# The list of all Glean pings.yaml files, relative to the top src dir. +# ONLY TO BE MODIFIED BY FOG PEERS! +pings_yamls = gecko_pings + firefox_desktop_pings + background_update_pings + test_pings + +# The list of tags that are allowed in the above to files, and their +# descriptions. Currently we restrict to a set scraped from bugzilla +# (via `./mach update-glean-tags`) +# Order is lexicographical, enforced by t/c/glean/tests/pytest/test_yaml_indices.py +tags_yamls = [ + "toolkit/components/glean/tags.yaml", +] diff --git a/toolkit/components/glean/moz.build b/toolkit/components/glean/moz.build new file mode 100644 index 0000000000..4af10cdc0a --- /dev/null +++ b/toolkit/components/glean/moz.build @@ -0,0 +1,213 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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 http://mozilla.org/MPL/2.0/. + +SPHINX_TREES["/toolkit/components/glean"] = "docs" + +# Needed so that we can include IPC things. +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" + +EXPORTS.mozilla += [ + "ipc/FOGIPC.h", +] + +EXPORTS.mozilla.glean += [ + "!GleanMetrics.h", + "!GleanPings.h", +] + +EXPORTS.mozilla.glean.bindings += [ + "!EventGIFFTMap.h", + "!GleanJSMetricsLookup.h", + "!GleanJSPingsLookup.h", + "!HistogramGIFFTMap.h", + "!ScalarGIFFTMap.h", + "bindings/Category.h", + "bindings/Glean.h", + "bindings/GleanPings.h", + "bindings/MetricTypes.h", + "bindings/private/Boolean.h", + "bindings/private/Common.h", + "bindings/private/Counter.h", + "bindings/private/CustomDistribution.h", + "bindings/private/Datetime.h", + "bindings/private/Denominator.h", + "bindings/private/DistributionData.h", + "bindings/private/Event.h", + "bindings/private/Labeled.h", + "bindings/private/MemoryDistribution.h", + "bindings/private/Numerator.h", + "bindings/private/Ping.h", + "bindings/private/Quantity.h", + "bindings/private/Rate.h", + "bindings/private/String.h", + "bindings/private/StringList.h", + "bindings/private/Timespan.h", + "bindings/private/TimingDistribution.h", + "bindings/private/Url.h", + "bindings/private/Uuid.h", +] + +EXPORTS.mozilla.glean.bindings.jog += [ + "bindings/jog/JOG.h", +] + +if CONFIG["COMPILE_ENVIRONMENT"]: + EXPORTS.mozilla.glean += [ + "!fog_ffi_generated.h", + ] + + EXPORTS.mozilla.glean.bindings.jog += [ + "!jog_ffi_generated.h", + ] + + CbindgenHeader("fog_ffi_generated.h", inputs=["/toolkit/components/glean"]) + CbindgenHeader( + "jog_ffi_generated.h", inputs=["/toolkit/components/glean/bindings/jog"] + ) + +UNIFIED_SOURCES += [ + "bindings/Category.cpp", + "bindings/Glean.cpp", + "bindings/GleanPings.cpp", + "bindings/jog/JOG.cpp", + "bindings/private/Boolean.cpp", + "bindings/private/Common.cpp", + "bindings/private/Counter.cpp", + "bindings/private/CustomDistribution.cpp", + "bindings/private/Datetime.cpp", + "bindings/private/Denominator.cpp", + "bindings/private/Event.cpp", + "bindings/private/Labeled.cpp", + "bindings/private/MemoryDistribution.cpp", + "bindings/private/Numerator.cpp", + "bindings/private/Ping.cpp", + "bindings/private/Quantity.cpp", + "bindings/private/Rate.cpp", + "bindings/private/String.cpp", + "bindings/private/StringList.cpp", + "bindings/private/Timespan.cpp", + "bindings/private/TimingDistribution.cpp", + "bindings/private/Url.cpp", + "bindings/private/Uuid.cpp", + "ipc/FOGIPC.cpp", + "ipc/Support.cpp", +] + +SOURCES += [ + "!EventExtraGIFFTMaps.cpp", +] + +# Provides us the list of dependent metrics|pings.yaml. +include("metrics_index.py") +# GeneratedFile's `inputs` are relative to our dir. +# The yamls arrays are relative to topsrcdir, so we need to transform: +metrics_yamls = ["../../../" + x for x in metrics_yamls] +pings_yamls = ["../../../" + x for x in pings_yamls] +tags_yamls = ["../../../" + x for x in tags_yamls] +# If you change the length of this deps list, update DEPS_LEN in run_glean_parser.py +deps = [ + "metrics_index.py", + "build_scripts/glean_parser_ext/cpp.py", + "build_scripts/glean_parser_ext/jog.py", + "build_scripts/glean_parser_ext/js.py", + "build_scripts/glean_parser_ext/run_glean_parser.py", + "build_scripts/glean_parser_ext/rust.py", + "build_scripts/glean_parser_ext/string_table.py", + "build_scripts/glean_parser_ext/util.py", + "build_scripts/glean_parser_ext/templates/cpp.jinja2", + "build_scripts/glean_parser_ext/templates/cpp_pings.jinja2", + "build_scripts/glean_parser_ext/templates/gifft.jinja2", + "build_scripts/glean_parser_ext/templates/gifft_events.jinja2", + "build_scripts/glean_parser_ext/templates/jog_factory.jinja2", + "build_scripts/glean_parser_ext/templates/js.jinja2", + "build_scripts/glean_parser_ext/templates/js_pings.jinja2", + "build_scripts/glean_parser_ext/templates/rust.jinja2", + "build_scripts/glean_parser_ext/templates/rust_pings.jinja2", +] + +GeneratedFile( + "GleanMetrics.h", + "GleanJSMetricsLookup.h", + "api/src/metrics.rs", + script="build_scripts/glean_parser_ext/run_glean_parser.py", + flags=[CONFIG["MOZ_APP_VERSION"], "GleanJSMetricsLookup.h", "api/src/metrics.rs"], + inputs=deps + metrics_yamls + tags_yamls, +) + +GeneratedFile( + "GleanPings.h", + "GleanJSPingsLookup.h", + "api/src/pings.rs", + script="build_scripts/glean_parser_ext/run_glean_parser.py", + flags=[CONFIG["MOZ_APP_VERSION"], "GleanJSPingsLookup.h", "api/src/pings.rs"], + inputs=deps + pings_yamls + tags_yamls, +) + +# Glean Interface For Firefox Telemetry Maps from Glean MetricId to Telemetry ProbeId +# We split it one map per header to avoid unused function warnings on build. +GeneratedFile( + "EventGIFFTMap.h", + script="build_scripts/glean_parser_ext/run_glean_parser.py", + entry_point="gifft_map", + flags=[CONFIG["MOZ_APP_VERSION"], "Event"], + inputs=deps + metrics_yamls + tags_yamls, +) + +GeneratedFile( + "HistogramGIFFTMap.h", + script="build_scripts/glean_parser_ext/run_glean_parser.py", + entry_point="gifft_map", + flags=[CONFIG["MOZ_APP_VERSION"], "Histogram"], + inputs=deps + metrics_yamls + tags_yamls, +) + +GeneratedFile( + "ScalarGIFFTMap.h", + script="build_scripts/glean_parser_ext/run_glean_parser.py", + entry_point="gifft_map", + flags=[CONFIG["MOZ_APP_VERSION"], "Scalar"], + inputs=deps + metrics_yamls + tags_yamls, +) + +# JOG provides both the Rust factory for building runtime-registered metrics +# and pings _and_ the YAML file used at runtime to register those metrics and +# pings for Artifact Builds. +# The factory lives inside the `fog` crate to avoid a circular dependency. +GeneratedFile( + "api/src/factory.rs", + script="build_scripts/glean_parser_ext/run_glean_parser.py", + entry_point="jog_factory", + flags=[CONFIG["MOZ_APP_VERSION"]], + inputs=deps + pings_yamls + metrics_yamls + tags_yamls, +) + +# Only generate jogfile.json in Artifact Builds since +# its presence triggers main-thread I/O (!MOZILLA_OFFICIAL builds only). +if not CONFIG["COMPILE_ENVIRONMENT"]: + GeneratedFile( + "jogfile.json", + script="build_scripts/glean_parser_ext/run_glean_parser.py", + entry_point="jog_file", + flags=[CONFIG["MOZ_APP_VERSION"]], + inputs=deps + pings_yamls + metrics_yamls + tags_yamls, + ) + # Once generated, it needs to be placed in GreD so it can be found. + FINAL_TARGET_FILES += ["!jogfile.json"] + +DIRS += [ + "tests", # Must be in DIRS, not TEST_DIRS or python-test won't find it. + "xpcom", +] + +with Files("docs/**"): + SCHEDULES.exclusive = ["docs"] + +with Files("**"): + BUG_COMPONENT = ("Toolkit", "Telemetry") + +REQUIRES_UNIFIED_BUILD = True diff --git a/toolkit/components/glean/pings.yaml b/toolkit/components/glean/pings.yaml new file mode 100644 index 0000000000..d4710a47c2 --- /dev/null +++ b/toolkit/components/glean/pings.yaml @@ -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 http://mozilla.org/MPL/2.0/. + +# This file defines the pings that are recorded by the Glean SDK. They are +# automatically converted to platform-specific code at build time using the +# `glean_parser` PyPI package. + +# This file is presently for Internal FOG Use Only. +# You should not add pings here until probably about January of 2021. + +--- +$schema: moz://mozilla.org/schemas/glean/pings/2-0-0 diff --git a/toolkit/components/glean/src/init/mod.rs b/toolkit/components/glean/src/init/mod.rs new file mode 100644 index 0000000000..5bf217e081 --- /dev/null +++ b/toolkit/components/glean/src/init/mod.rs @@ -0,0 +1,330 @@ +// 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::env; +use std::ffi::CString; +use std::ops::DerefMut; +use std::path::PathBuf; + +#[cfg(target_os = "android")] +use nserror::NS_ERROR_NOT_IMPLEMENTED; +use nserror::{nsresult, NS_ERROR_FAILURE}; +use nsstring::{nsACString, nsCString, nsString}; +use xpcom::interfaces::{nsIFile, nsIPrefService, nsIProperties, nsIXULAppInfo, nsIXULRuntime}; +use xpcom::{RefPtr, XpCom}; + +use glean::{ClientInfoMetrics, Configuration}; + +#[cfg(not(target_os = "android"))] +mod upload_pref; +#[cfg(not(target_os = "android"))] +mod user_activity; +#[cfg(not(target_os = "android"))] +mod viaduct_uploader; + +#[cfg(not(target_os = "android"))] +use upload_pref::UploadPrefObserver; +#[cfg(not(target_os = "android"))] +use user_activity::UserActivityObserver; +#[cfg(not(target_os = "android"))] +use viaduct_uploader::ViaductUploader; + +/// Project FOG's entry point. +/// +/// This assembles client information and the Glean configuration and then initializes the global +/// Glean instance. +#[cfg(not(target_os = "android"))] +#[no_mangle] +pub extern "C" fn fog_init( + data_path_override: &nsACString, + app_id_override: &nsACString, +) -> nsresult { + let upload_enabled = static_prefs::pref!("datareporting.healthreport.uploadEnabled"); + let recording_enabled = static_prefs::pref!("telemetry.fog.test.localhost_port") < 0; + let uploader = Some(Box::new(ViaductUploader) as Box<dyn glean::net::PingUploader>); + + fog_init_internal( + data_path_override, + app_id_override, + upload_enabled || recording_enabled, + uploader, + ) + .into() +} + +/// Project FOG's entry point on Android. +/// +/// This assembles client information and the Glean configuration and then initializes the global +/// Glean instance. +/// It always enables upload and set no uploader. +/// This should only be called in test scenarios. +/// In normal use Glean should be initialized and controlled by the Glean Kotlin SDK. +#[cfg(target_os = "android")] +#[no_mangle] +pub extern "C" fn fog_init( + data_path_override: &nsACString, + app_id_override: &nsACString, +) -> nsresult { + // On Android always enable Glean upload. + let upload_enabled = true; + // Don't set up an uploader. + let uploader = None; + + fog_init_internal( + data_path_override, + app_id_override, + upload_enabled, + uploader, + ) + .into() +} + +fn fog_init_internal( + data_path_override: &nsACString, + app_id_override: &nsACString, + upload_enabled: bool, + uploader: Option<Box<dyn glean::net::PingUploader>>, +) -> Result<(), nsresult> { + fog::metrics::fog::initialization.start(); + + log::debug!("Initializing FOG."); + + setup_observers()?; + + let (mut conf, client_info) = build_configuration(data_path_override, app_id_override)?; + + conf.upload_enabled = upload_enabled; + conf.uploader = uploader; + + // If we're operating in automation without any specific source tags to set, + // set the tag "automation" so any pings that escape don't clutter the tables. + // See https://mozilla.github.io/glean/book/user/debugging/index.html#enabling-debugging-features-through-environment-variables + if env::var("MOZ_AUTOMATION").is_ok() && env::var("GLEAN_SOURCE_TAGS").is_err() { + log::info!("In automation, setting 'automation' source tag."); + glean::set_source_tags(vec!["automation".to_string()]); + log::info!("In automation, disabling MPS to avoid 4am issues."); + conf.use_core_mps = false; + } + + log::debug!("Configuration: {:#?}", conf); + + // Register all custom pings before we initialize. + fog::pings::register_pings(); + + glean::initialize(conf, client_info); + + fog::metrics::fog::initialization.stop(); + + Ok(()) +} + +fn build_configuration( + data_path_override: &nsACString, + app_id_override: &nsACString, +) -> Result<(Configuration, ClientInfoMetrics), nsresult> { + let data_path_str = if data_path_override.is_empty() { + get_data_path()? + } else { + data_path_override.to_utf8().to_string() + }; + let data_path = PathBuf::from(&data_path_str); + + let (app_build, app_display_version, channel) = get_app_info()?; + + let client_info = ClientInfoMetrics { + app_build, + app_display_version, + channel: Some(channel), + }; + log::debug!("Client Info: {:#?}", client_info); + + const SERVER: &str = "https://incoming.telemetry.mozilla.org"; + let localhost_port = static_prefs::pref!("telemetry.fog.test.localhost_port"); + let server = if localhost_port > 0 { + format!("http://localhost:{}", localhost_port) + } else { + String::from(SERVER) + }; + + // In the event that this isn't "firefox.desktop", we don't use core's MPS. + let mut use_core_mps = false; + let application_id = if app_id_override.is_empty() { + use_core_mps = true; + "firefox.desktop".to_string() + } else { + app_id_override.to_utf8().to_string() + }; + + let configuration = Configuration { + upload_enabled: false, + data_path, + application_id, + max_events: None, + delay_ping_lifetime_io: true, + server_endpoint: Some(server), + uploader: None, + use_core_mps, + }; + + Ok((configuration, client_info)) +} + +#[cfg(not(target_os = "android"))] +fn setup_observers() -> Result<(), nsresult> { + if let Err(e) = UploadPrefObserver::begin_observing() { + log::error!( + "Could not observe data upload pref. Abandoning FOG init due to {:?}", + e + ); + return Err(e); + } + + if let Err(e) = UserActivityObserver::begin_observing() { + log::error!( + "Could not observe user activity. Abandoning FOG init due to {:?}", + e + ); + return Err(e); + } + + Ok(()) +} + +#[cfg(target_os = "android")] +fn setup_observers() -> Result<(), nsresult> { + // No observers are set up on Android. + Ok(()) +} + +/// Construct and return the data_path from the profile dir, or return an error. +fn get_data_path() -> Result<String, nsresult> { + let dir_svc: RefPtr<nsIProperties> = match xpcom::components::Directory::service() { + Ok(ds) => ds, + _ => return Err(NS_ERROR_FAILURE), + }; + let mut profile_dir = xpcom::GetterAddrefs::<nsIFile>::new(); + unsafe { + dir_svc + .Get( + cstr!("ProfD").as_ptr(), + &nsIFile::IID, + profile_dir.void_ptr(), + ) + .to_result()?; + } + let profile_dir = profile_dir.refptr().ok_or(NS_ERROR_FAILURE)?; + let mut profile_path = nsString::new(); + unsafe { + (*profile_dir).GetPath(&mut *profile_path).to_result()?; + } + let profile_path = String::from_utf16(&profile_path[..]).map_err(|_| NS_ERROR_FAILURE)?; + let data_path = profile_path + "/datareporting/glean"; + Ok(data_path) +} + +/// Return a tuple of the build_id, app version, and build channel. +/// If the XUL Runtime isn't a XULAppInfo (e.g. in xpcshell), +/// build_id ad app_version will be "unknown". +/// Other problems result in an error being returned instead. +fn get_app_info() -> Result<(String, String, String), nsresult> { + let xul: RefPtr<nsIXULRuntime> = + xpcom::components::XULRuntime::service().map_err(|_| NS_ERROR_FAILURE)?; + + let pref_service: RefPtr<nsIPrefService> = + xpcom::components::Preferences::service().map_err(|_| NS_ERROR_FAILURE)?; + let branch = xpcom::getter_addrefs(|p| { + // Safe because: + // * `null` is explicitly allowed per documentation + // * `p` is a valid outparam guaranteed by `getter_addrefs` + unsafe { pref_service.GetDefaultBranch(std::ptr::null(), p) } + })?; + let pref_name = CString::new("app.update.channel").map_err(|_| NS_ERROR_FAILURE)?; + let mut channel = nsCString::new(); + // Safe because: + // * `branch` is non-null (otherwise `getter_addrefs` would've been `Err` + // * `pref_name` exists so a pointer to it is valid for the life of the function + // * `channel` exists so a pointer to it is valid, and it can be written to + unsafe { + if (*branch) + .GetCharPref(pref_name.as_ptr(), channel.deref_mut() as *mut nsACString) + .to_result() + .is_err() + { + channel = "unknown".into(); + } + } + + let app_info = match xul.query_interface::<nsIXULAppInfo>() { + Some(ai) => ai, + // In e.g. xpcshell the XULRuntime isn't XULAppInfo. + // We still want to return sensible values so tests don't explode. + _ => { + return Ok(( + "unknown".to_owned(), + "unknown".to_owned(), + channel.to_string(), + )) + } + }; + + let mut build_id = nsCString::new(); + unsafe { + app_info.GetAppBuildID(&mut *build_id).to_result()?; + } + + let mut version = nsCString::new(); + unsafe { + app_info.GetVersion(&mut *version).to_result()?; + } + + Ok(( + build_id.to_string(), + version.to_string(), + channel.to_string(), + )) +} + +/// **TEST-ONLY METHOD** +/// Resets FOG and the underlying Glean SDK, clearing stores. +#[cfg(not(target_os = "android"))] +#[no_mangle] +pub extern "C" fn fog_test_reset( + data_path_override: &nsACString, + app_id_override: &nsACString, +) -> nsresult { + fog_test_reset_internal(data_path_override, app_id_override).into() +} + +// Split out into its own function so I could use `?` +#[cfg(not(target_os = "android"))] +fn fog_test_reset_internal( + data_path_override: &nsACString, + app_id_override: &nsACString, +) -> Result<(), nsresult> { + let (mut conf, client_info) = build_configuration(data_path_override, app_id_override)?; + + let upload_enabled = static_prefs::pref!("datareporting.healthreport.uploadEnabled"); + let recording_enabled = static_prefs::pref!("telemetry.fog.test.localhost_port") < 0; + conf.upload_enabled = upload_enabled || recording_enabled; + + // Don't accidentally send "main" pings during tests. + conf.use_core_mps = false; + + // I'd prefer to reuse the uploader, but it gets moved into Glean so we build anew. + conf.uploader = Some(Box::new(ViaductUploader) as Box<dyn glean::net::PingUploader>); + + glean::test_reset_glean(conf, client_info, true); + Ok(()) +} + +/// **TEST-ONLY METHOD** +/// Does nothing on Android. Returns NS_ERROR_NOT_IMPLEMENTED. +#[cfg(target_os = "android")] +#[no_mangle] +pub extern "C" fn fog_test_reset( + _data_path_override: &nsACString, + _app_id_override: &nsACString, +) -> nsresult { + NS_ERROR_NOT_IMPLEMENTED +} diff --git a/toolkit/components/glean/src/init/upload_pref.rs b/toolkit/components/glean/src/init/upload_pref.rs new file mode 100644 index 0000000000..737230c16c --- /dev/null +++ b/toolkit/components/glean/src/init/upload_pref.rs @@ -0,0 +1,99 @@ +// 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::ffi::CStr; +use std::os::raw::c_char; +use std::sync::atomic::{AtomicBool, Ordering}; + +use nserror::{nsresult, NS_ERROR_FAILURE, NS_OK}; +use nsstring::{nsACString, nsCStr}; +use xpcom::{ + interfaces::{nsIPrefBranch, nsISupports}, + RefPtr, +}; + +/// Whether the current value of the localhost testing pref is permitting +/// metric recording (even if upload is disabled). +static RECORDING_ENABLED: AtomicBool = AtomicBool::new(false); + +// Partially cargo-culted from https://searchfox.org/mozilla-central/rev/598e50d2c3cd81cd616654f16af811adceb08f9f/security/manager/ssl/cert_storage/src/lib.rs#1192 +#[xpcom(implement(nsIObserver), atomic)] +pub(crate) struct UploadPrefObserver {} + +#[allow(non_snake_case)] +impl UploadPrefObserver { + pub(crate) fn begin_observing() -> Result<(), nsresult> { + // Ensure we begin with the correct current value of RECORDING_ENABLED. + let recording_enabled = static_prefs::pref!("telemetry.fog.test.localhost_port") < 0; + RECORDING_ENABLED.store(recording_enabled, Ordering::SeqCst); + + // SAFETY: Everything here is self-contained. + // + // * We allocate the pref observer, created by the xpcom macro + // * We query the pref service and bail out if it doesn't exist. + // * We create a nsCStr from a static string. + // * We control all input to `AddObserverImpl` + unsafe { + let pref_obs = Self::allocate(InitUploadPrefObserver {}); + let pref_branch: RefPtr<nsIPrefBranch> = + xpcom::components::Preferences::service().map_err(|_| NS_ERROR_FAILURE)?; + let pref_nscstr = + &nsCStr::from("datareporting.healthreport.uploadEnabled") as &nsACString; + (*pref_branch) + .AddObserverImpl(pref_nscstr, pref_obs.coerce(), false) + .to_result()?; + let pref_nscstr = &nsCStr::from("telemetry.fog.test.localhost_port") as &nsACString; + (*pref_branch) + .AddObserverImpl(pref_nscstr, pref_obs.coerce(), false) + .to_result()?; + } + + Ok(()) + } + + unsafe fn Observe( + &self, + _subject: *const nsISupports, + topic: *const c_char, + pref_name: *const u16, + ) -> nserror::nsresult { + let topic = CStr::from_ptr(topic).to_str().unwrap(); + // Conversion utf16 to utf8 is messy. + // We should only ever observe changes to one of the two prefs we want, + // but just to be on the safe side let's assert. + + // cargo-culted from https://searchfox.org/mozilla-central/rev/598e50d2c3cd81cd616654f16af811adceb08f9f/security/manager/ssl/cert_storage/src/lib.rs#1606-1612 + // (with a little transformation) + let len = (0..).take_while(|&i| *pref_name.offset(i) != 0).count(); // find NUL. + let slice = std::slice::from_raw_parts(pref_name, len); + let pref_name = match String::from_utf16(slice) { + Ok(name) => name, + Err(_) => return NS_ERROR_FAILURE, + }; + log::info!("Observed {:?}, {:?}", topic, pref_name); + debug_assert!(topic == "nsPref:changed"); + debug_assert!( + pref_name == "datareporting.healthreport.uploadEnabled" + || pref_name == "telemetry.fog.test.localhost_port" + ); + + let upload_enabled = static_prefs::pref!("datareporting.healthreport.uploadEnabled"); + let recording_enabled = static_prefs::pref!("telemetry.fog.test.localhost_port") < 0; + log::info!( + "New upload_enabled {}, recording_enabled {}", + upload_enabled, + recording_enabled + ); + if RECORDING_ENABLED.load(Ordering::SeqCst) && !recording_enabled { + // Whenever the test pref goes from permitting recording to forbidding it, + // ensure Glean is told to wipe the stores. + // This may send a "deletion-request" ping for a client_id that's never sent + // any other pings. + glean::set_upload_enabled(false); + } + RECORDING_ENABLED.store(recording_enabled, Ordering::SeqCst); + glean::set_upload_enabled(upload_enabled || recording_enabled); + NS_OK + } +} diff --git a/toolkit/components/glean/src/init/user_activity.rs b/toolkit/components/glean/src/init/user_activity.rs new file mode 100644 index 0000000000..dbafeca4e8 --- /dev/null +++ b/toolkit/components/glean/src/init/user_activity.rs @@ -0,0 +1,129 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use std::ffi::CStr; +use std::os::raw::c_char; +use std::sync::{ + atomic::{AtomicBool, Ordering}, + RwLock, +}; +use std::time::{Duration, Instant}; + +use nserror::{nsresult, NS_ERROR_FAILURE, NS_OK}; +use xpcom::{ + interfaces::{nsIObserverService, nsISupports}, + RefPtr, +}; + +// Partially cargo-culted from UploadPrefObserver. +#[xpcom(implement(nsIObserver), atomic)] +pub(crate) struct UserActivityObserver { + last_edge: RwLock<Instant>, + was_active: AtomicBool, +} + +/// Listens to Firefox Desktop's `user-interaction-(in)active` topics, +/// debouncing them before calling into the Glean SDK Client Activity API. +/// See +/// [the docs](https://firefox-source-docs.mozilla.org/toolkit/components/glean/builtin_pings.html) +/// for more info. +#[allow(non_snake_case)] +impl UserActivityObserver { + pub(crate) fn begin_observing() -> Result<(), nsresult> { + // First and foremost, even if we can't get the ObserverService, + // init always means client activity. + glean::handle_client_active(); + + // SAFETY: Everything here is self-contained. + // + // * We allocate the activity observer, created by the xpcom macro + // * We create cstr from a static string. + // * We control all input to `AddObserver` + unsafe { + let activity_obs = Self::allocate(InitUserActivityObserver { + last_edge: RwLock::new(Instant::now()), + was_active: AtomicBool::new(false), + }); + let obs_service: RefPtr<nsIObserverService> = + xpcom::components::Observer::service().map_err(|_| NS_ERROR_FAILURE)?; + let rv = obs_service.AddObserver( + activity_obs.coerce(), + cstr!("user-interaction-active").as_ptr(), + false, + ); + if !rv.succeeded() { + return Err(rv); + } + let rv = obs_service.AddObserver( + activity_obs.coerce(), + cstr!("user-interaction-inactive").as_ptr(), + false, + ); + if !rv.succeeded() { + return Err(rv); + } + } + Ok(()) + } + + unsafe fn Observe( + &self, + _subject: *const nsISupports, + topic: *const c_char, + _data: *const u16, + ) -> nserror::nsresult { + match CStr::from_ptr(topic).to_str() { + Ok("user-interaction-active") => self.handle_active(), + Ok("user-interaction-inactive") => self.handle_inactive(), + _ => NS_OK, + } + } + + fn handle_active(&self) -> nserror::nsresult { + let was_active = self.was_active.swap(true, Ordering::SeqCst); + if !was_active { + let inactivity = self + .last_edge + .read() + .expect("Edge lock poisoned.") + .elapsed(); + // We only care after a certain period of inactivity (default 20min). + let limit = static_prefs::pref!("telemetry.fog.test.inactivity_limit"); + if inactivity >= Duration::from_secs(limit.into()) { + log::info!( + "User triggers core activity after {}s!", + inactivity.as_secs() + ); + glean::handle_client_active(); + } + let mut edge = self.last_edge.write().expect("Edge lock poisoned."); + *edge = Instant::now(); + } + NS_OK + } + + fn handle_inactive(&self) -> nserror::nsresult { + let was_active = self.was_active.swap(false, Ordering::SeqCst); + // This is actually always so. Inactivity is only notified once. + if was_active { + let activity = self + .last_edge + .read() + .expect("Edge lock poisoned.") + .elapsed(); + // We only care after a certain period of activity (default 2min). + let limit = static_prefs::pref!("telemetry.fog.test.activity_limit"); + if activity >= Duration::from_secs(limit.into()) { + log::info!( + "User triggers core inactivity after {}s!", + activity.as_secs() + ); + glean::handle_client_inactive(); + } + let mut edge = self.last_edge.write().expect("Edge lock poisoned."); + *edge = Instant::now(); + } + NS_OK + } +} diff --git a/toolkit/components/glean/src/init/viaduct_uploader.rs b/toolkit/components/glean/src/init/viaduct_uploader.rs new file mode 100644 index 0000000000..381567b488 --- /dev/null +++ b/toolkit/components/glean/src/init/viaduct_uploader.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/. + +use glean::net::{PingUploader, UploadResult}; +use url::Url; +use viaduct::{Error::*, Request}; + +/// An uploader that uses [Viaduct](https://github.com/mozilla/application-services/tree/main/components/viaduct). +#[derive(Debug)] +pub(crate) struct ViaductUploader; + +impl PingUploader for ViaductUploader { + /// Uploads a ping to a server. + /// + /// # Arguments + /// + /// * `url` - the URL path to upload the data to. + /// * `body` - the serialized text data to send. + /// * `headers` - a vector of tuples containing the headers to send with + /// the request, i.e. (Name, Value). + fn upload(&self, url: String, body: Vec<u8>, headers: Vec<(String, String)>) -> UploadResult { + log::trace!("FOG Ping Uploader uploading to {}", url); + let url_clone = url.clone(); + let result: std::result::Result<UploadResult, viaduct::Error> = (move || { + let debug_tagged = headers.iter().any(|(name, _)| name == "X-Debug-ID"); + let localhost_port = static_prefs::pref!("telemetry.fog.test.localhost_port"); + if localhost_port < 0 + || (localhost_port == 0 && !debug_tagged && cfg!(feature = "disable_upload")) + { + log::info!("FOG Ping uploader faking success"); + return Ok(UploadResult::http_status(200)); + } + let parsed_url = Url::parse(&url_clone)?; + + log::info!("FOG Ping uploader uploading to {:?}", parsed_url); + + let mut req = Request::post(parsed_url.clone()).body(body.clone()); + for (header_key, header_value) in &headers { + req = req.header(header_key.to_owned(), header_value)?; + } + + log::trace!("FOG Ping Uploader sending ping to {}", parsed_url); + let res = req.send()?; + Ok(UploadResult::http_status(res.status as i32)) + })(); + log::trace!( + "FOG Ping Uploader completed uploading to {} (Result {:?})", + url, + result + ); + match result { + Ok(result) => result, + Err(NonTlsUrl | UrlError(_)) => UploadResult::unrecoverable_failure(), + Err( + RequestHeaderError(_) + | BackendError(_) + | NetworkError(_) + | BackendNotInitialized + | SetBackendError, + ) => UploadResult::recoverable_failure(), + } + } +} diff --git a/toolkit/components/glean/src/lib.rs b/toolkit/components/glean/src/lib.rs new file mode 100644 index 0000000000..e74fc106d1 --- /dev/null +++ b/toolkit/components/glean/src/lib.rs @@ -0,0 +1,193 @@ +// 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/. + +//! Firefox on Glean (FOG) is the name of the layer that integrates the [Glean SDK][glean-sdk] into Firefox Desktop. +//! It is currently being designed and implemented. +//! +//! The [Glean SDK][glean-sdk] is a data collection library built by Mozilla for use in its products. +//! Like [Telemetry][telemetry], it can be used to +//! (in accordance with our [Privacy Policy][privacy-policy]) +//! send anonymous usage statistics to Mozilla in order to make better decisions. +//! +//! Documentation can be found online in the [Firefox Source Docs][docs]. +//! +//! [glean-sdk]: https://github.com/mozilla/glean/ +//! [book-of-glean]: https://mozilla.github.io/glean/book/index.html +//! [privacy-policy]: https://www.mozilla.org/privacy/ +//! [docs]: https://firefox-source-docs.mozilla.org/toolkit/components/glean/ + +// No one is currently using the Glean SDK, so let's export it, so we know it gets +// compiled. +pub extern crate fog; + +use nserror::{nsresult, NS_ERROR_FAILURE, NS_OK}; +use nsstring::{nsACString, nsCString}; +use thin_vec::ThinVec; + +#[macro_use] +extern crate cstr; +#[cfg_attr(not(target_os = "android"), macro_use)] +extern crate xpcom; + +mod init; + +pub use init::fog_init; + +#[no_mangle] +pub extern "C" fn fog_shutdown() { + glean::shutdown(); +} + +#[no_mangle] +pub extern "C" fn fog_register_pings() { + fog::pings::register_pings(); +} + +static mut PENDING_BUF: Vec<u8> = Vec::new(); + +// IPC serialization/deserialization methods +// Crucially important that the first two not be called on multiple threads. + +/// Only safe if only called on a single thread (the same single thread you call +/// fog_give_ipc_buf on). +#[no_mangle] +pub unsafe extern "C" fn fog_serialize_ipc_buf() -> usize { + if let Some(buf) = fog::ipc::take_buf() { + PENDING_BUF = buf; + PENDING_BUF.len() + } else { + PENDING_BUF = vec![]; + 0 + } +} + +/// Only safe if called on a single thread (the same single thread you call +/// fog_serialize_ipc_buf on), and if buf points to an allocated buffer of at +/// least buf_len bytes. +#[no_mangle] +pub unsafe extern "C" fn fog_give_ipc_buf(buf: *mut u8, buf_len: usize) -> usize { + let pending_len = PENDING_BUF.len(); + if buf.is_null() || buf_len < pending_len { + return 0; + } + std::ptr::copy_nonoverlapping(PENDING_BUF.as_ptr(), buf, pending_len); + PENDING_BUF = Vec::new(); + pending_len +} + +/// Only safe if buf points to an allocated buffer of at least buf_len bytes. +/// No ownership is transfered to Rust by this method: caller owns the memory at +/// buf before and after this call. +#[no_mangle] +pub unsafe extern "C" fn fog_use_ipc_buf(buf: *const u8, buf_len: usize) { + let slice = std::slice::from_raw_parts(buf, buf_len); + let res = fog::ipc::replay_from_buf(slice); + if res.is_err() { + log::warn!("Unable to replay ipc buffer. This will result in data loss."); + fog::metrics::fog_ipc::replay_failures.add(1); + } +} + +/// Sets the debug tag for pings assembled in the future. +/// Returns an error result if the provided value is not a valid tag. +#[no_mangle] +pub extern "C" fn fog_set_debug_view_tag(value: &nsACString) -> nsresult { + let result = glean::set_debug_view_tag(&value.to_string()); + if result { + return NS_OK; + } else { + return NS_ERROR_FAILURE; + } +} + +/// Submits a ping by name. +#[no_mangle] +pub extern "C" fn fog_submit_ping(ping_name: &nsACString) -> nsresult { + glean::submit_ping_by_name(&ping_name.to_string(), None); + NS_OK +} + +/// Turns ping logging on or off. +/// Returns an error if the logging failed to be configured. +#[no_mangle] +pub extern "C" fn fog_set_log_pings(value: bool) -> nsresult { + glean::set_log_pings(value); + NS_OK +} + +/// Flushes ping-lifetime data to the db when delay_ping_lifetime_io is true. +#[no_mangle] +pub extern "C" fn fog_persist_ping_lifetime_data() -> nsresult { + glean::persist_ping_lifetime_data(); + NS_OK +} + +/// Indicate that an experiment is running. +/// Glean will add an experiment annotation which is sent with pings. +/// This information is not persisted between runs. +/// +/// See [`glean_core::Glean::set_experiment_active`]. +#[no_mangle] +pub extern "C" fn fog_set_experiment_active( + experiment_id: &nsACString, + branch: &nsACString, + extra_keys: &ThinVec<nsCString>, + extra_values: &ThinVec<nsCString>, +) { + assert_eq!( + extra_keys.len(), + extra_values.len(), + "Experiment extra keys and values differ in length." + ); + let extra = if extra_keys.len() == 0 { + None + } else { + Some( + extra_keys + .iter() + .zip(extra_values.iter()) + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect(), + ) + }; + glean::set_experiment_active(experiment_id.to_string(), branch.to_string(), extra); +} + +/// Indicate that an experiment is no longer running. +/// +/// See [`glean_core::Glean::set_experiment_inactive`]. +#[no_mangle] +pub extern "C" fn fog_set_experiment_inactive(experiment_id: &nsACString) { + glean::set_experiment_inactive(experiment_id.to_string()); +} + +/// TEST ONLY FUNCTION +/// +/// Returns true if the identified experiment is active. +#[no_mangle] +pub extern "C" fn fog_test_is_experiment_active(experiment_id: &nsACString) -> bool { + glean::test_is_experiment_active(experiment_id.to_string()) +} + +/// TEST ONLY FUNCTION +/// +/// Fills `branch`, `extra_keys`, and `extra_values` with the identified experiment's data. +/// Panics if the identified experiment isn't active. +#[no_mangle] +pub extern "C" fn fog_test_get_experiment_data( + experiment_id: &nsACString, + branch: &mut nsACString, + extra_keys: &mut ThinVec<nsCString>, + extra_values: &mut ThinVec<nsCString>, +) { + let data = glean::test_get_experiment_data(experiment_id.to_string()); + if let Some(data) = data { + branch.assign(&data.branch); + if let Some(extra) = data.extra { + let (data_keys, data_values): (Vec<_>, Vec<_>) = extra.iter().unzip(); + extra_keys.extend(data_keys.into_iter().map(|key| key.into())); + extra_values.extend(data_values.into_iter().map(|value| value.into())); + } + } +} diff --git a/toolkit/components/glean/tags.yaml b/toolkit/components/glean/tags.yaml new file mode 100644 index 0000000000..4fadce4c94 --- /dev/null +++ b/toolkit/components/glean/tags.yaml @@ -0,0 +1,531 @@ +# 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 http://mozilla.org/MPL/2.0/. + + +### This file was AUTOMATICALLY GENERATED by `./mach update-glean-tags` +### DO NOT edit it by hand. + + +--- +$schema: moz://mozilla.org/schemas/glean/tags/1-0-0 +'Cloud Services :: Firefox: Common': + description: The Bugzilla component which applies to this object. +'Conduit :: mots': + description: The Bugzilla component which applies to this object. +'Core :: Audio/Video': + description: The Bugzilla component which applies to this object. +'Core :: Audio/Video: GMP': + description: The Bugzilla component which applies to this object. +'Core :: Audio/Video: MediaStreamGraph': + description: The Bugzilla component which applies to this object. +'Core :: Audio/Video: Playback': + description: The Bugzilla component which applies to this object. +'Core :: Audio/Video: Recording': + description: The Bugzilla component which applies to this object. +'Core :: Audio/Video: cubeb': + description: The Bugzilla component which applies to this object. +'Core :: AutoConfig (Mission Control Desktop)': + description: The Bugzilla component which applies to this object. +'Core :: CSS Parsing and Computation': + description: The Bugzilla component which applies to this object. +'Core :: CSS Transitions and Animations': + description: The Bugzilla component which applies to this object. +'Core :: DOM: Animation': + description: The Bugzilla component which applies to this object. +'Core :: DOM: Bindings (WebIDL)': + description: The Bugzilla component which applies to this object. +'Core :: DOM: CSS Object Model': + description: The Bugzilla component which applies to this object. +'Core :: DOM: Content Processes': + description: The Bugzilla component which applies to this object. +'Core :: DOM: Copy & Paste and Drag & Drop': + description: The Bugzilla component which applies to this object. +'Core :: DOM: Core & HTML': + description: The Bugzilla component which applies to this object. +'Core :: DOM: Device Interfaces': + description: The Bugzilla component which applies to this object. +'Core :: DOM: Editor': + description: The Bugzilla component which applies to this object. +'Core :: DOM: Events': + description: The Bugzilla component which applies to this object. +'Core :: DOM: File': + description: The Bugzilla component which applies to this object. +'Core :: DOM: Forms': + description: The Bugzilla component which applies to this object. +'Core :: DOM: Geolocation': + description: The Bugzilla component which applies to this object. +'Core :: DOM: HTML Parser': + description: The Bugzilla component which applies to this object. +'Core :: DOM: Navigation': + description: The Bugzilla component which applies to this object. +'Core :: DOM: Networking': + description: The Bugzilla component which applies to this object. +'Core :: DOM: Performance': + description: The Bugzilla component which applies to this object. +'Core :: DOM: Push Notifications': + description: The Bugzilla component which applies to this object. +'Core :: DOM: Security': + description: The Bugzilla component which applies to this object. +'Core :: DOM: Selection': + description: The Bugzilla component which applies to this object. +'Core :: DOM: Serializers': + description: The Bugzilla component which applies to this object. +'Core :: DOM: Service Workers': + description: The Bugzilla component which applies to this object. +'Core :: DOM: Streams': + description: The Bugzilla component which applies to this object. +'Core :: DOM: UI Events & Focus Handling': + description: The Bugzilla component which applies to this object. +'Core :: DOM: Web Authentication': + description: The Bugzilla component which applies to this object. +'Core :: DOM: Web Payments': + description: The Bugzilla component which applies to this object. +'Core :: DOM: Workers': + description: The Bugzilla component which applies to this object. +'Core :: DOM: postMessage': + description: The Bugzilla component which applies to this object. +'Core :: Disability Access APIs': + description: The Bugzilla component which applies to this object. +'Core :: Gecko Profiler': + description: The Bugzilla component which applies to this object. +'Core :: General': + description: The Bugzilla component which applies to this object. +'Core :: Graphics': + description: The Bugzilla component which applies to this object. +'Core :: Graphics: Canvas2D': + description: The Bugzilla component which applies to this object. +'Core :: Graphics: CanvasWebGL': + description: The Bugzilla component which applies to this object. +'Core :: Graphics: ImageLib': + description: The Bugzilla component which applies to this object. +'Core :: Graphics: Text': + description: The Bugzilla component which applies to this object. +'Core :: Graphics: WebGPU': + description: The Bugzilla component which applies to this object. +'Core :: Graphics: WebRender': + description: The Bugzilla component which applies to this object. +'Core :: Hardware Abstraction Layer (HAL)': + description: The Bugzilla component which applies to this object. +'Core :: IPC': + description: The Bugzilla component which applies to this object. +'Core :: Internationalization': + description: The Bugzilla component which applies to this object. +'Core :: JavaScript Engine': + description: The Bugzilla component which applies to this object. +'Core :: JavaScript Engine: JIT': + description: The Bugzilla component which applies to this object. +'Core :: JavaScript: GC': + description: The Bugzilla component which applies to this object. +'Core :: JavaScript: Internationalization API': + description: The Bugzilla component which applies to this object. +'Core :: JavaScript: Standard Library': + description: The Bugzilla component which applies to this object. +'Core :: JavaScript: WebAssembly': + description: The Bugzilla component which applies to this object. +'Core :: Javascript: Web Assembly': + description: The Bugzilla component which applies to this object. +'Core :: Javascript: WebAssembly': + description: The Bugzilla component which applies to this object. +'Core :: Layout': + description: The Bugzilla component which applies to this object. +'Core :: Layout: Block and Inline': + description: The Bugzilla component which applies to this object. +'Core :: Layout: Columns': + description: The Bugzilla component which applies to this object. +'Core :: Layout: Flexbox': + description: The Bugzilla component which applies to this object. +'Core :: Layout: Floats': + description: The Bugzilla component which applies to this object. +'Core :: Layout: Form Controls': + description: The Bugzilla component which applies to this object. +'Core :: Layout: Generated Content, Lists, and Counters': + description: The Bugzilla component which applies to this object. +'Core :: Layout: Grid': + description: The Bugzilla component which applies to this object. +'Core :: Layout: Images, Video, and HTML Frames': + description: The Bugzilla component which applies to this object. +'Core :: Layout: Positioned': + description: The Bugzilla component which applies to this object. +'Core :: Layout: Ruby': + description: The Bugzilla component which applies to this object. +'Core :: Layout: Scrolling and Overflow': + description: The Bugzilla component which applies to this object. +'Core :: Layout: Tables': + description: The Bugzilla component which applies to this object. +'Core :: Layout: Text and Fonts': + description: The Bugzilla component which applies to this object. +'Core :: Localization': + description: The Bugzilla component which applies to this object. +'Core :: MFBT': + description: The Bugzilla component which applies to this object. +'Core :: MathML': + description: The Bugzilla component which applies to this object. +'Core :: Memory Allocator': + description: The Bugzilla component which applies to this object. +'Core :: Networking': + description: The Bugzilla component which applies to this object. +'Core :: Networking: Cache': + description: The Bugzilla component which applies to this object. +'Core :: Networking: Cookies': + description: The Bugzilla component which applies to this object. +'Core :: Networking: DNS': + description: The Bugzilla component which applies to this object. +'Core :: Networking: File': + description: The Bugzilla component which applies to this object. +'Core :: Networking: HTTP': + description: The Bugzilla component which applies to this object. +'Core :: Networking: JAR': + description: The Bugzilla component which applies to this object. +'Core :: Networking: WebSockets': + description: The Bugzilla component which applies to this object. +'Core :: Panning and Zooming': + description: The Bugzilla component which applies to this object. +'Core :: Performance': + description: The Bugzilla component which applies to this object. +'Core :: Permission Manager': + description: The Bugzilla component which applies to this object. +'Core :: Preferences: Backend': + description: The Bugzilla component which applies to this object. +'Core :: Printing: Output': + description: The Bugzilla component which applies to this object. +'Core :: Privacy: Anti-Tracking': + description: The Bugzilla component which applies to this object. +'Core :: SVG': + description: The Bugzilla component which applies to this object. +'Core :: Security': + description: The Bugzilla component which applies to this object. +'Core :: Security: CAPS': + description: The Bugzilla component which applies to this object. +'Core :: Security: PSM': + description: The Bugzilla component which applies to this object. +'Core :: Security: Process Sandboxing': + description: The Bugzilla component which applies to this object. +'Core :: Spelling checker': + description: The Bugzilla component which applies to this object. +'Core :: Storage: Cache API': + description: The Bugzilla component which applies to this object. +'Core :: Storage: IndexedDB': + description: The Bugzilla component which applies to this object. +'Core :: Storage: Quota Manager': + description: The Bugzilla component which applies to this object. +'Core :: Storage: localStorage & sessionStorage': + description: The Bugzilla component which applies to this object. +'Core :: String': + description: The Bugzilla component which applies to this object. +'Core :: Web Audio': + description: The Bugzilla component which applies to this object. +'Core :: Web Painting': + description: The Bugzilla component which applies to this object. +'Core :: Web Speech': + description: The Bugzilla component which applies to this object. +'Core :: WebRTC': + description: The Bugzilla component which applies to this object. +'Core :: WebRTC: Audio/Video': + description: The Bugzilla component which applies to this object. +'Core :: WebRTC: Networking': + description: The Bugzilla component which applies to this object. +'Core :: WebRTC: Signaling': + description: The Bugzilla component which applies to this object. +'Core :: WebVR': + description: The Bugzilla component which applies to this object. +'Core :: Widget': + description: The Bugzilla component which applies to this object. +'Core :: Widget: Cocoa': + description: The Bugzilla component which applies to this object. +'Core :: Widget: Gtk': + description: The Bugzilla component which applies to this object. +'Core :: Widget: Win32': + description: The Bugzilla component which applies to this object. +'Core :: Window Management': + description: The Bugzilla component which applies to this object. +'Core :: XML': + description: The Bugzilla component which applies to this object. +'Core :: XPCOM': + description: The Bugzilla component which applies to this object. +'Core :: XPConnect': + description: The Bugzilla component which applies to this object. +'Core :: XSLT': + description: The Bugzilla component which applies to this object. +'Core :: XUL': + description: The Bugzilla component which applies to this object. +'Core :: js-ctypes': + description: The Bugzilla component which applies to this object. +'Core :: mozglue': + description: The Bugzilla component which applies to this object. +'Developer Infrastructure :: Lint and Formatting': + description: The Bugzilla component which applies to this object. +'DevTools :: Accessibility Tools': + description: The Bugzilla component which applies to this object. +'DevTools :: Console': + description: The Bugzilla component which applies to this object. +'DevTools :: DOM': + description: The Bugzilla component which applies to this object. +'DevTools :: Debugger': + description: The Bugzilla component which applies to this object. +'DevTools :: Framework': + description: The Bugzilla component which applies to this object. +'DevTools :: General': + description: The Bugzilla component which applies to this object. +'DevTools :: Inspector': + description: The Bugzilla component which applies to this object. +'DevTools :: Inspector: Animations': + description: The Bugzilla component which applies to this object. +'DevTools :: Inspector: Changes': + description: The Bugzilla component which applies to this object. +'DevTools :: Inspector: Compatibility': + description: The Bugzilla component which applies to this object. +'DevTools :: Inspector: Layout': + description: The Bugzilla component which applies to this object. +'DevTools :: Inspector: Rules': + description: The Bugzilla component which applies to this object. +'DevTools :: JSON Viewer': + description: The Bugzilla component which applies to this object. +'DevTools :: Memory': + description: The Bugzilla component which applies to this object. +'DevTools :: Netmonitor': + description: The Bugzilla component which applies to this object. +'DevTools :: Performance Tools (Profiler/Timeline)': + description: The Bugzilla component which applies to this object. +'DevTools :: Responsive Design Mode': + description: The Bugzilla component which applies to this object. +'DevTools :: Shared Components': + description: The Bugzilla component which applies to this object. +'DevTools :: Source Editor': + description: The Bugzilla component which applies to this object. +'DevTools :: Storage Inspector': + description: The Bugzilla component which applies to this object. +'DevTools :: Style Editor': + description: The Bugzilla component which applies to this object. +'DevTools :: about:debugging': + description: The Bugzilla component which applies to this object. +'Firefox :: Address Bar': + description: The Bugzilla component which applies to this object. +'Firefox :: Bookmarks & History': + description: The Bugzilla component which applies to this object. +'Firefox :: Distributions': + description: The Bugzilla component which applies to this object. +'Firefox :: Downloads Panel': + description: The Bugzilla component which applies to this object. +'Firefox :: Enterprise Policies': + description: The Bugzilla component which applies to this object. +'Firefox :: File Handling': + description: The Bugzilla component which applies to this object. +'Firefox :: Firefox Accounts': + description: The Bugzilla component which applies to this object. +'Firefox :: General': + description: The Bugzilla component which applies to this object. +'Firefox :: Headless': + description: The Bugzilla component which applies to this object. +'Firefox :: Installer': + description: The Bugzilla component which applies to this object. +'Firefox :: Keyboard Navigation': + description: The Bugzilla component which applies to this object. +'Firefox :: Menus': + description: The Bugzilla component which applies to this object. +'Firefox :: Messaging System': + description: The Bugzilla component which applies to this object. +'Firefox :: Migration': + description: The Bugzilla component which applies to this object. +'Firefox :: New Tab Page': + description: The Bugzilla component which applies to this object. +'Firefox :: Nimbus Desktop Client': + description: The Bugzilla component which applies to this object. +'Firefox :: Normandy Client': + description: The Bugzilla component which applies to this object. +'Firefox :: PDF Viewer': + description: The Bugzilla component which applies to this object. +'Firefox :: Page Info Window': + description: The Bugzilla component which applies to this object. +'Firefox :: Pocket': + description: The Bugzilla component which applies to this object. +'Firefox :: Preferences': + description: The Bugzilla component which applies to this object. +'Firefox :: Private Browsing': + description: The Bugzilla component which applies to this object. +'Firefox :: Protections UI': + description: The Bugzilla component which applies to this object. +'Firefox :: Remote Settings Client': + description: The Bugzilla component which applies to this object. +'Firefox :: Screenshots': + description: The Bugzilla component which applies to this object. +'Firefox :: Search': + description: The Bugzilla component which applies to this object. +'Firefox :: Security': + description: The Bugzilla component which applies to this object. +'Firefox :: Services Automation': + description: The Bugzilla component which applies to this object. +'Firefox :: Session Restore': + description: The Bugzilla component which applies to this object. +'Firefox :: Shell Integration': + description: The Bugzilla component which applies to this object. +'Firefox :: Site Identity': + description: The Bugzilla component which applies to this object. +'Firefox :: Site Permissions': + description: The Bugzilla component which applies to this object. +'Firefox :: Sync': + description: The Bugzilla component which applies to this object. +'Firefox :: Tabbed Browser': + description: The Bugzilla component which applies to this object. +'Firefox :: Theme': + description: The Bugzilla component which applies to this object. +'Firefox :: Toolbars and Customization': + description: The Bugzilla component which applies to this object. +'Firefox :: Tours': + description: The Bugzilla component which applies to this object. +'Firefox :: Translation': + description: The Bugzilla component which applies to this object. +'Firefox :: about:logins': + description: The Bugzilla component which applies to this object. +'Firefox Build System :: Bootstrap Configuration': + description: The Bugzilla component which applies to this object. +'Firefox Build System :: General': + description: The Bugzilla component which applies to this object. +'Firefox Build System :: Generated Documentation': + description: The Bugzilla component which applies to this object. +'Firefox Build System :: Mach Core': + description: The Bugzilla component which applies to this object. +'Firefox Build System :: Source Code Analysis': + description: The Bugzilla component which applies to this object. +'Firefox Build System :: Task Configuration': + description: The Bugzilla component which applies to this object. +'Firefox Build System :: Try': + description: The Bugzilla component which applies to this object. +'GeckoView :: General': + description: The Bugzilla component which applies to this object. +'Localization Infrastructure and Tools :: Fluent Migration': + description: The Bugzilla component which applies to this object. +'Localization Infrastructure and Tools :: General': + description: The Bugzilla component which applies to this object. +'Localization Infrastructure and Tools :: compare-locales': + description: The Bugzilla component which applies to this object. +'NSPR :: NSPR': + description: The Bugzilla component which applies to this object. +'NSS :: Libraries': + description: The Bugzilla component which applies to this object. +'Release Engineering :: General': + description: The Bugzilla component which applies to this object. +'Release Engineering :: Release Automation: Updates': + description: The Bugzilla component which applies to this object. +'Remote Protocol :: Agent': + description: The Bugzilla component which applies to this object. +'Remote Protocol :: CDP': + description: The Bugzilla component which applies to this object. +'Remote Protocol :: WebDriver BiDi': + description: The Bugzilla component which applies to this object. +'Taskcluster :: General': + description: The Bugzilla component which applies to this object. +'Taskcluster :: Platform Libraries': + description: The Bugzilla component which applies to this object. +'Testing :: CPPUnitTest': + description: The Bugzilla component which applies to this object. +'Testing :: Code Coverage': + description: The Bugzilla component which applies to this object. +'Testing :: Firefox UI Tests': + description: The Bugzilla component which applies to this object. +'Testing :: GTest': + description: The Bugzilla component which applies to this object. +'Testing :: General': + description: The Bugzilla component which applies to this object. +'Testing :: Marionette': + description: The Bugzilla component which applies to this object. +'Testing :: Mochitest': + description: The Bugzilla component which applies to this object. +'Testing :: Mozbase': + description: The Bugzilla component which applies to this object. +'Testing :: Mozbase Rust': + description: The Bugzilla component which applies to this object. +'Testing :: Python Test': + description: The Bugzilla component which applies to this object. +'Testing :: Reftest': + description: The Bugzilla component which applies to this object. +'Testing :: XPCShell Harness': + description: The Bugzilla component which applies to this object. +'Testing :: geckodriver': + description: The Bugzilla component which applies to this object. +'Testing :: mozperftest': + description: The Bugzilla component which applies to this object. +'Testing :: web-platform-tests': + description: The Bugzilla component which applies to this object. +'Toolkit :: Add-ons Manager': + description: The Bugzilla component which applies to this object. +'Toolkit :: Application Update': + description: The Bugzilla component which applies to this object. +'Toolkit :: Async Tooling': + description: The Bugzilla component which applies to this object. +'Toolkit :: Autocomplete': + description: The Bugzilla component which applies to this object. +'Toolkit :: Background Tasks': + description: The Bugzilla component which applies to this object. +'Toolkit :: Blocklist Implementation': + description: The Bugzilla component which applies to this object. +'Toolkit :: Crash Reporting': + description: The Bugzilla component which applies to this object. +'Toolkit :: Data Sanitization': + description: The Bugzilla component which applies to this object. +'Toolkit :: Downloads API': + description: The Bugzilla component which applies to this object. +'Toolkit :: FeatureGate': + description: The Bugzilla component which applies to this object. +'Toolkit :: Find Toolbar': + description: The Bugzilla component which applies to this object. +'Toolkit :: Form Autofill': + description: The Bugzilla component which applies to this object. +'Toolkit :: Form Manager': + description: The Bugzilla component which applies to this object. +'Toolkit :: General': + description: The Bugzilla component which applies to this object. +'Toolkit :: NSIS Installer': + description: The Bugzilla component which applies to this object. +'Toolkit :: Notifications and Alerts': + description: The Bugzilla component which applies to this object. +'Toolkit :: OS.File': + description: The Bugzilla component which applies to this object. +'Toolkit :: Password Manager': + description: The Bugzilla component which applies to this object. +'Toolkit :: Performance Monitoring': + description: The Bugzilla component which applies to this object. +'Toolkit :: Picture-in-Picture': + description: The Bugzilla component which applies to this object. +'Toolkit :: Places': + description: The Bugzilla component which applies to this object. +'Toolkit :: Preferences': + description: The Bugzilla component which applies to this object. +'Toolkit :: Printing': + description: The Bugzilla component which applies to this object. +'Toolkit :: Reader Mode': + description: The Bugzilla component which applies to this object. +'Toolkit :: Safe Browsing': + description: The Bugzilla component which applies to this object. +'Toolkit :: Startup and Profile System': + description: The Bugzilla component which applies to this object. +'Toolkit :: Storage': + description: The Bugzilla component which applies to this object. +'Toolkit :: Telemetry': + description: The Bugzilla component which applies to this object. +'Toolkit :: Themes': + description: The Bugzilla component which applies to this object. +'Toolkit :: Video/Audio Controls': + description: The Bugzilla component which applies to this object. +'Toolkit :: View Source': + description: The Bugzilla component which applies to this object. +'Toolkit :: XUL Widgets': + description: The Bugzilla component which applies to this object. +'Toolkit :: about:memory': + description: The Bugzilla component which applies to this object. +'Web Compatibility :: Tooling & Investigations': + description: The Bugzilla component which applies to this object. +'WebExtensions :: General': + description: The Bugzilla component which applies to this object. +'WebExtensions :: Request Handling': + description: The Bugzilla component which applies to this object. +'WebExtensions :: Storage': + description: The Bugzilla component which applies to this object. +'WebExtensions :: Themes': + description: The Bugzilla component which applies to this object. +'WebExtensions :: Untriaged': + description: The Bugzilla component which applies to this object. +'mozilla.org :: Licensing': + description: The Bugzilla component which applies to this object. +'mozilla.org :: MozillaBuild': + description: The Bugzilla component which applies to this object. diff --git a/toolkit/components/glean/tests/browser/browser.ini b/toolkit/components/glean/tests/browser/browser.ini new file mode 100644 index 0000000000..eae3f38730 --- /dev/null +++ b/toolkit/components/glean/tests/browser/browser.ini @@ -0,0 +1,19 @@ +# Please keep test files lexicographically sorted, with whitespace between. +[DEFAULT] +support-files = + +[browser_event_leak.js] + +[browser_fog_gmp.js] +support-files = empty_file.html + +[browser_fog_gpu.js] + +[browser_fog_rdd.js] +support-files = small-shot.ogg + +[browser_fog_socket.js] + +[browser_fog_utility.js] + +[browser_labeled_gifft.js] diff --git a/toolkit/components/glean/tests/browser/browser_event_leak.js b/toolkit/components/glean/tests/browser/browser_event_leak.js new file mode 100644 index 0000000000..1e69f17064 --- /dev/null +++ b/toolkit/components/glean/tests/browser/browser_event_leak.js @@ -0,0 +1,26 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async () => { + Services.fog.testResetFOG(); // Needed for TV which reuses profiles on repeat + Assert.equal( + undefined, + Glean.testOnlyIpc.eventWithExtra.testGetValue(), + "Nothing to begin with" + ); + Glean.testOnlyIpc.eventWithExtra.record({ + extra1: "Some extra string", + extra2: 42, + extra3_longer_name: false, + }); + Assert.equal( + 1, + Glean.testOnlyIpc.eventWithExtra.testGetValue().length, + "One event? One event." + ); + + // AND NOW, FOR THE TRUE TEST: + // Will this leak memory all over the place? +}); diff --git a/toolkit/components/glean/tests/browser/browser_fog_gmp.js b/toolkit/components/glean/tests/browser/browser_fog_gmp.js new file mode 100644 index 0000000000..4219605eea --- /dev/null +++ b/toolkit/components/glean/tests/browser/browser_fog_gmp.js @@ -0,0 +1,84 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Return a web-based URL for a given file based on the testing directory. + * @param {String} fileName + * file that caller wants its web-based url + */ +function GetTestWebBasedURL(fileName) { + return ( + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.org" + ) + fileName + ); +} + +add_task(async () => { + await SpecialPowers.pushPrefEnv({ + set: [["media.eme.enabled", true]], + }); + + await BrowserTestUtils.withNewTab( + GetTestWebBasedURL("empty_file.html"), + async function(browser) { + await SpecialPowers.spawn(browser, [], async function() { + try { + let config = [ + { + initDataTypes: ["webm"], + videoCapabilities: [{ contentType: 'video/webm; codecs="vp9"' }], + }, + ]; + let access = await content.navigator.requestMediaKeySystemAccess( + "org.w3.clearkey", + config + ); + + content.mediaKeys = await access.createMediaKeys(); + info("got media keys, which should ensure a GMP process exists"); + } catch (ex) { + ok(false, ex.toString()); + } + }); + + ok( + (await ChromeUtils.requestProcInfo()).children.some( + p => p.type == "gmpPlugin" + ), + "Found the GMP process." + ); + + Services.fog.testResetFOG(); + + is( + undefined, + Glean.testOnlyIpc.aCounter.testGetValue(), + "Ensure we begin without value." + ); + + await TestUtils.waitForCondition(async () => { + try { + await Services.fog.testTriggerMetrics( + Ci.nsIXULRuntime.PROCESS_TYPE_GMPLUGIN + ); + return true; + } catch (e) { + info(e); + return false; + } + }, "waiting until we can successfully send the TestTriggerMetrics IPC."); + + await Services.fog.testFlushAllChildren(); + + is( + Glean.testOnlyIpc.aCounter.testGetValue(), + Ci.nsIXULRuntime.PROCESS_TYPE_GMPLUGIN, + "Ensure the GMP-process-set value shows up in the parent process." + ); + } + ); +}); diff --git a/toolkit/components/glean/tests/browser/browser_fog_gpu.js b/toolkit/components/glean/tests/browser/browser_fog_gpu.js new file mode 100644 index 0000000000..da0e699c20 --- /dev/null +++ b/toolkit/components/glean/tests/browser/browser_fog_gpu.js @@ -0,0 +1,34 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async () => { + if ( + !(await ChromeUtils.requestProcInfo()).children.some(p => p.type == "gpu") + ) { + ok( + true, + 'No GPU process? No test. Try again with --setpref "layers.gpu-process.force-enabled=true".' + ); + return; + } + ok(true, "GPU Process found: Let's test."); + + Services.fog.testResetFOG(); + + is( + undefined, + Glean.testOnlyIpc.aCounter.testGetValue(), + "Ensure we begin without value." + ); + + await Services.fog.testTriggerMetrics(Ci.nsIXULRuntime.PROCESS_TYPE_GPU); + await Services.fog.testFlushAllChildren(); + + is( + Glean.testOnlyIpc.aCounter.testGetValue(), + Ci.nsIXULRuntime.PROCESS_TYPE_GPU, + "Ensure the GPU-process-set value shows up in the parent process." + ); +}); diff --git a/toolkit/components/glean/tests/browser/browser_fog_rdd.js b/toolkit/components/glean/tests/browser/browser_fog_rdd.js new file mode 100644 index 0000000000..caf9a6db1f --- /dev/null +++ b/toolkit/components/glean/tests/browser/browser_fog_rdd.js @@ -0,0 +1,63 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Return a web-based URL for a given file based on the testing directory. + * @param {String} fileName + * file that caller wants its web-based url + */ +function GetTestWebBasedURL(fileName) { + return ( + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "http://example.org" + ) + fileName + ); +} + +add_task(async () => { + await SpecialPowers.pushPrefEnv({ + set: [["media.rdd-process.enabled", true]], + }); + + let url = GetTestWebBasedURL("small-shot.ogg"); + info( + `Opening ${url} in a new tab to trigger the creation of the RDD process` + ); + let tab = BrowserTestUtils.addTab(gBrowser, url); + + await TestUtils.waitForCondition( + async () => + (await ChromeUtils.requestProcInfo()).children.some(p => p.type == "rdd"), + "waiting to find RDD process." + ); + + Services.fog.testResetFOG(); + + is( + undefined, + Glean.testOnlyIpc.aCounter.testGetValue(), + "Ensure we begin without value." + ); + + await TestUtils.waitForCondition(async () => { + try { + await Services.fog.testTriggerMetrics(Ci.nsIXULRuntime.PROCESS_TYPE_RDD); + return true; + } catch (e) { + return false; + } + }, "waiting until we can successfully send the TestTriggerMetrics IPC."); + + await Services.fog.testFlushAllChildren(); + + is( + Glean.testOnlyIpc.aCounter.testGetValue(), + Ci.nsIXULRuntime.PROCESS_TYPE_RDD, + "Ensure the RDD-process-set value shows up in the parent process." + ); + + BrowserTestUtils.removeTab(tab); +}); diff --git a/toolkit/components/glean/tests/browser/browser_fog_socket.js b/toolkit/components/glean/tests/browser/browser_fog_socket.js new file mode 100644 index 0000000000..4ca4f085a1 --- /dev/null +++ b/toolkit/components/glean/tests/browser/browser_fog_socket.js @@ -0,0 +1,33 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async () => { + if ( + !(await ChromeUtils.requestProcInfo()).children.some( + p => p.type == "socket" + ) + ) { + ok(true, "No Socket process? No test."); + return; + } + ok(true, "Socket process found: Let's test."); + + Services.fog.testResetFOG(); + + is( + undefined, + Glean.testOnlyIpc.aCounter.testGetValue(), + "Ensure we begin without value." + ); + + await Services.fog.testTriggerMetrics(Ci.nsIXULRuntime.PROCESS_TYPE_SOCKET); + await Services.fog.testFlushAllChildren(); + + is( + Glean.testOnlyIpc.aCounter.testGetValue(), + Ci.nsIXULRuntime.PROCESS_TYPE_SOCKET, + "Ensure the Socket-process-set value shows up in the parent process." + ); +}); diff --git a/toolkit/components/glean/tests/browser/browser_fog_utility.js b/toolkit/components/glean/tests/browser/browser_fog_utility.js new file mode 100644 index 0000000000..aaea9cfb42 --- /dev/null +++ b/toolkit/components/glean/tests/browser/browser_fog_utility.js @@ -0,0 +1,37 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async () => { + const utilityProcessTest = Cc[ + "@mozilla.org/utility-process-test;1" + ].createInstance(Ci.nsIUtilityProcessTest); + await utilityProcessTest + .startProcess() + .then(async () => { + Services.fog.testResetFOG(); + + is( + undefined, + Glean.testOnlyIpc.aCounter.testGetValue(), + "Ensure we begin without value." + ); + + await Services.fog.testTriggerMetrics( + Ci.nsIXULRuntime.PROCESS_TYPE_UTILITY + ); + await Services.fog.testFlushAllChildren(); + + is( + Glean.testOnlyIpc.aCounter.testGetValue(), + Ci.nsIXULRuntime.PROCESS_TYPE_UTILITY, + "Ensure the utility-process-set value shows up in the parent process." + ); + }) + .catch(async () => { + ok(false, "Cannot start Utility process?"); + }); + + await utilityProcessTest.stopProcess(); +}); diff --git a/toolkit/components/glean/tests/browser/browser_labeled_gifft.js b/toolkit/components/glean/tests/browser/browser_labeled_gifft.js new file mode 100644 index 0000000000..2203424e82 --- /dev/null +++ b/toolkit/components/glean/tests/browser/browser_labeled_gifft.js @@ -0,0 +1,50 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +function keyedScalarValue(aScalarName) { + let snapshot = Services.telemetry.getSnapshotForKeyedScalars(); + return "parent" in snapshot ? snapshot.parent[aScalarName] : undefined; +} + +add_task(async () => { + Assert.equal( + undefined, + Glean.testOnlyIpc.aLabeledCounter.a_label.testGetValue(), + "New labels with no values should return undefined" + ); + Glean.testOnlyIpc.aLabeledCounter.a_label.add(1); + Glean.testOnlyIpc.aLabeledCounter.another_label.add(2); + Assert.equal(1, Glean.testOnlyIpc.aLabeledCounter.a_label.testGetValue()); + Assert.equal( + 2, + Glean.testOnlyIpc.aLabeledCounter.another_label.testGetValue() + ); + // What about invalid/__other__? + Assert.equal( + undefined, + Glean.testOnlyIpc.aLabeledCounter.__other__.testGetValue() + ); + Glean.testOnlyIpc.aLabeledCounter.InvalidLabel.add(3); + Assert.throws( + () => Glean.testOnlyIpc.aLabeledCounter.__other__.testGetValue(), + /NS_ERROR_LOSS_OF_SIGNIFICANT_DATA/, + "Can't get the value when you're error'd" + ); + + let value = keyedScalarValue( + "telemetry.test.another_mirror_for_labeled_counter" + ); + Assert.deepEqual( + { + a_label: 1, + another_label: 2, + InvalidLabel: 3, + }, + value + ); + + // AND NOW, FOR THE TRUE TEST: + // Will this leak memory all over the place? +}); diff --git a/toolkit/components/glean/tests/browser/empty_file.html b/toolkit/components/glean/tests/browser/empty_file.html new file mode 100644 index 0000000000..af8440ac16 --- /dev/null +++ b/toolkit/components/glean/tests/browser/empty_file.html @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="UTF-8"> + </head> + <body> + This page is intentionally left blank. + </body> +</html> diff --git a/toolkit/components/glean/tests/browser/small-shot.ogg b/toolkit/components/glean/tests/browser/small-shot.ogg Binary files differnew file mode 100644 index 0000000000..1a41623f81 --- /dev/null +++ b/toolkit/components/glean/tests/browser/small-shot.ogg diff --git a/toolkit/components/glean/tests/gtest/Cargo.toml b/toolkit/components/glean/tests/gtest/Cargo.toml new file mode 100644 index 0000000000..914efad469 --- /dev/null +++ b/toolkit/components/glean/tests/gtest/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "fog-gtest" +version = "0.1.0" +authors = ["glean-team@mozilla.com"] +license = "MPL-2.0" +description = "Tests for the FOG crate" + +[dependencies] +fog = { path = "../../api" } +jog = { path = "../../bindings/jog" } +nsstring = { path = "../../../../../xpcom/rust/nsstring" } + +[lib] +path = "test.rs" diff --git a/toolkit/components/glean/tests/gtest/FOGFixture.h b/toolkit/components/glean/tests/gtest/FOGFixture.h new file mode 100644 index 0000000000..da3c6fc123 --- /dev/null +++ b/toolkit/components/glean/tests/gtest/FOGFixture.h @@ -0,0 +1,23 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +#ifndef FOGFixture_h_ +#define FOGFixture_h_ + +#include "gtest/gtest.h" +#include "mozilla/glean/fog_ffi_generated.h" +#include "nsString.h" + +using namespace mozilla::glean::impl; + +class FOGFixture : public ::testing::Test { + protected: + FOGFixture() = default; + virtual void SetUp() { + nsCString empty; + ASSERT_EQ(NS_OK, fog_test_reset(&empty, &empty)); + } +}; + +#endif // FOGFixture_h_ diff --git a/toolkit/components/glean/tests/gtest/TestFog.cpp b/toolkit/components/glean/tests/gtest/TestFog.cpp new file mode 100644 index 0000000000..c3dd1a7914 --- /dev/null +++ b/toolkit/components/glean/tests/gtest/TestFog.cpp @@ -0,0 +1,378 @@ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#include "FOGFixture.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include "mozilla/glean/GleanMetrics.h" +#include "mozilla/glean/GleanPings.h" +#include "mozilla/glean/fog_ffi_generated.h" +#include "mozilla/Maybe.h" +#include "mozilla/Result.h" +#include "mozilla/ResultVariant.h" +#include "mozilla/Tuple.h" +#include "nsTArray.h" + +#include "mozilla/Preferences.h" +#include "mozilla/Unused.h" +#include "nsString.h" +#include "prtime.h" + +using mozilla::Preferences; +using namespace mozilla::glean; +using namespace mozilla::glean::impl; + +#define DATA_PREF "datareporting.healthreport.uploadEnabled" + +extern "C" { +// This function is called by the rust code in test.rs if a non-fatal test +// failure occurs. +void GTest_FOG_ExpectFailure(const char* aMessage) { + EXPECT_STREQ(aMessage, ""); +} +} + +TEST_F(FOGFixture, BuiltinPingsRegistered) { + Preferences::SetInt("telemetry.fog.test.localhost_port", -1); + nsAutoCString metricsPingName("metrics"); + nsAutoCString baselinePingName("baseline"); + nsAutoCString eventsPingName("events"); + ASSERT_EQ(NS_OK, fog_submit_ping(&metricsPingName)); + ASSERT_EQ(NS_OK, fog_submit_ping(&baselinePingName)); + ASSERT_EQ(NS_OK, fog_submit_ping(&eventsPingName)); +} + +TEST_F(FOGFixture, TestCppCounterWorks) { + mozilla::glean::test_only::bad_code.Add(42); + + ASSERT_EQ(42, mozilla::glean::test_only::bad_code.TestGetValue("test-ping"_ns) + .unwrap() + .value()); + // And test that the ping name's optional, while you're at it: + ASSERT_EQ(42, test_only::bad_code.TestGetValue().unwrap().value()); +} + +TEST_F(FOGFixture, TestCppStringWorks) { + auto kValue = "cheez!"_ns; + mozilla::glean::test_only::cheesy_string.Set(kValue); + + ASSERT_STREQ(kValue.get(), mozilla::glean::test_only::cheesy_string + .TestGetValue("test-ping"_ns) + .unwrap() + .value() + .get()); +} + +TEST_F(FOGFixture, TestCppTimespanWorks) { + mozilla::glean::test_only::can_we_time_it.Start(); + PR_Sleep(PR_MillisecondsToInterval(10)); + mozilla::glean::test_only::can_we_time_it.Stop(); + + ASSERT_TRUE( + mozilla::glean::test_only::can_we_time_it.TestGetValue("test-ping"_ns) + .unwrap() + .value() > 0); +} + +TEST_F(FOGFixture, TestCppUuidWorks) { + nsCString kTestUuid("decafdec-afde-cafd-ecaf-decafdecafde"); + test_only::what_id_it.Set(kTestUuid); + ASSERT_STREQ(kTestUuid.get(), + test_only::what_id_it.TestGetValue("test-ping"_ns) + .unwrap() + .value() + .get()); + + test_only::what_id_it.GenerateAndSet(); + // Since we generate v4 UUIDs, and the first character of the third group + // isn't 4, this won't ever collide with kTestUuid. + ASSERT_STRNE(kTestUuid.get(), + test_only::what_id_it.TestGetValue("test-ping"_ns) + .unwrap() + .value() + .get()); +} + +TEST_F(FOGFixture, TestCppBooleanWorks) { + mozilla::glean::test_only::can_we_flag_it.Set(false); + + ASSERT_EQ(false, mozilla::glean::test_only::can_we_flag_it + .TestGetValue("test-ping"_ns) + .unwrap() + .value()); +} + +MATCHER_P(BitEq, x, "bit equal") { + static_assert(sizeof(x) == sizeof(arg)); + return std::memcmp(&arg, &x, sizeof(x)) == 0; +} + +TEST_F(FOGFixture, TestCppDatetimeWorks) { + PRExplodedTime date{0, 35, 10, 12, 6, 10, 2020, 0, 0, {5 * 60 * 60, 0}}; + test_only::what_a_date.Set(&date); + + auto received = test_only::what_a_date.TestGetValue("test-ping"_ns).unwrap(); + ASSERT_THAT(received.value(), BitEq(date)); +} + +using mozilla::MakeTuple; +using mozilla::Some; +using mozilla::Tuple; +using mozilla::glean::test_only_ipc::AnEventExtra; +using mozilla::glean::test_only_ipc::EventWithExtraExtra; + +TEST_F(FOGFixture, TestCppEventWorks) { + test_only_ipc::no_extra_event.Record(); + ASSERT_TRUE(test_only_ipc::no_extra_event.TestGetValue("store1"_ns) + .unwrap() + .isSome()); + + AnEventExtra extra = {.extra1 = Some("can set extras"_ns)}; + test_only_ipc::an_event.Record(Some(extra)); + auto optEvents = test_only_ipc::an_event.TestGetValue("store1"_ns).unwrap(); + ASSERT_TRUE(optEvents.isSome()); + + auto events = optEvents.extract(); + ASSERT_EQ(1UL, events.Length()); + ASSERT_STREQ("test_only.ipc", events[0].mCategory.get()); + ASSERT_STREQ("an_event", events[0].mName.get()); + ASSERT_EQ(1UL, events[0].mExtra.Length()); + ASSERT_STREQ("extra1", mozilla::Get<0>(events[0].mExtra[0]).get()); + ASSERT_STREQ("can set extras", mozilla::Get<1>(events[0].mExtra[0]).get()); +} + +TEST_F(FOGFixture, TestCppEventsWithDifferentExtraTypes) { + EventWithExtraExtra extra = {.extra1 = Some("can set extras"_ns), + .extra2 = Some(37), + .extra3LongerName = Some(false)}; + test_only_ipc::event_with_extra.Record(Some(extra)); + auto optEvents = + test_only_ipc::event_with_extra.TestGetValue("store1"_ns).unwrap(); + ASSERT_TRUE(optEvents.isSome()); + + auto events = optEvents.extract(); + ASSERT_EQ(1UL, events.Length()); + + // The list of extra key/value pairs can be in any order. + ASSERT_EQ(3UL, events[0].mExtra.Length()); + for (auto extra : events[0].mExtra) { + auto key = mozilla::Get<0>(extra); + auto value = mozilla::Get<1>(extra); + + if (key == "extra1"_ns) { + ASSERT_STREQ("can set extras", value.get()); + } else if (key == "extra2"_ns) { + ASSERT_STREQ("37", value.get()); + } else if (key == "extra3_longer_name"_ns) { + ASSERT_STREQ("false", value.get()); + } else { + ASSERT_TRUE(false) + << "Invalid extra item recorded."; + } + } +} + +TEST_F(FOGFixture, TestCppMemoryDistWorks) { + test_only::do_you_remember.Accumulate(7); + test_only::do_you_remember.Accumulate(17); + + DistributionData data = + test_only::do_you_remember.TestGetValue("test-ping"_ns).unwrap().ref(); + // Sum is in bytes, test_only::do_you_remember is in megabytes. So + // multiplication ahoy! + ASSERT_EQ(data.sum, 24UL * 1024 * 1024); + for (const auto& entry : data.values) { + const uint64_t bucket = entry.GetKey(); + const uint64_t count = entry.GetData(); + ASSERT_TRUE(count == 0 || + (count == 1 && (bucket == 17520006 || bucket == 7053950))) + << "Only two occupied buckets"; + } +} + +TEST_F(FOGFixture, TestCppCustomDistWorks) { + test_only_ipc::a_custom_dist.AccumulateSamples({7, 268435458}); + + DistributionData data = + test_only_ipc::a_custom_dist.TestGetValue("store1"_ns).unwrap().ref(); + ASSERT_EQ(data.sum, 7UL + 268435458); + for (const auto& entry : data.values) { + const uint64_t bucket = entry.GetKey(); + const uint64_t count = entry.GetData(); + ASSERT_TRUE(count == 0 || + (count == 1 && (bucket == 1 || bucket == 268435456))) + << "Only two occupied buckets"; + } +} + +TEST_F(FOGFixture, TestCppPings) { + test_only::one_ping_one_bool.Set(false); + const auto& ping = mozilla::glean_pings::OnePingOnly; + bool submitted = false; + ping.TestBeforeNextSubmit([&submitted](const nsACString& aReason) { + submitted = true; + ASSERT_EQ(false, + test_only::one_ping_one_bool.TestGetValue().unwrap().ref()); + }); + ping.Submit(); + ASSERT_TRUE(submitted) + << "Must have actually called the lambda."; +} + +TEST_F(FOGFixture, TestCppStringLists) { + auto kValue = "cheez!"_ns; + auto kValue2 = "cheezier!"_ns; + auto kValue3 = "cheeziest."_ns; + + nsTArray<nsCString> cheezList; + cheezList.EmplaceBack(kValue); + cheezList.EmplaceBack(kValue2); + + test_only::cheesy_string_list.Set(cheezList); + + auto val = test_only::cheesy_string_list.TestGetValue().unwrap().value(); + // Note: This is fragile if the order is ever not preserved. + ASSERT_STREQ(kValue.get(), val[0].get()); + ASSERT_STREQ(kValue2.get(), val[1].get()); + + test_only::cheesy_string_list.Add(kValue3); + + val = test_only::cheesy_string_list.TestGetValue().unwrap().value(); + ASSERT_STREQ(kValue3.get(), val[2].get()); +} + +TEST_F(FOGFixture, TestCppTimingDistWorks) { + auto id1 = test_only::what_time_is_it.Start(); + auto id2 = test_only::what_time_is_it.Start(); + PR_Sleep(PR_MillisecondsToInterval(5)); + auto id3 = test_only::what_time_is_it.Start(); + test_only::what_time_is_it.Cancel(std::move(id1)); + PR_Sleep(PR_MillisecondsToInterval(5)); + test_only::what_time_is_it.StopAndAccumulate(std::move(id2)); + test_only::what_time_is_it.StopAndAccumulate(std::move(id3)); + + DistributionData data = + test_only::what_time_is_it.TestGetValue().unwrap().ref(); + const uint64_t NANOS_IN_MILLIS = 1e6; + + // bug 1701847 - Sleeps don't necessarily round up as you'd expect. + // Give ourselves a 40000ns (0.04ms) window to be off on fast machines. + const uint64_t EPSILON = 40000; + + // We don't know exactly how long those sleeps took, only that it was at + // least 15ms total. + ASSERT_GT(data.sum, (uint64_t)(15 * NANOS_IN_MILLIS) - EPSILON); + + // We also can't guarantee the buckets, but we can guarantee two samples. + uint64_t sampleCount = 0; + for (const auto& value : data.values.Values()) { + sampleCount += value; + } + ASSERT_EQ(sampleCount, (uint64_t)2); +} + +TEST_F(FOGFixture, TestLabeledBooleanWorks) { + ASSERT_EQ(mozilla::Nothing(), + test_only::mabels_like_balloons.Get("hot_air"_ns) + .TestGetValue() + .unwrap()); + test_only::mabels_like_balloons.Get("hot_air"_ns).Set(true); + test_only::mabels_like_balloons.Get("helium"_ns).Set(false); + ASSERT_EQ(true, test_only::mabels_like_balloons.Get("hot_air"_ns) + .TestGetValue() + .unwrap() + .ref()); + ASSERT_EQ(false, test_only::mabels_like_balloons.Get("helium"_ns) + .TestGetValue() + .unwrap() + .ref()); +} + +TEST_F(FOGFixture, TestLabeledCounterWorks) { + ASSERT_EQ(mozilla::Nothing(), + test_only::mabels_kitchen_counters.Get("marble"_ns) + .TestGetValue() + .unwrap()); + test_only::mabels_kitchen_counters.Get("marble"_ns).Add(1); + test_only::mabels_kitchen_counters.Get("laminate"_ns).Add(2); + ASSERT_EQ(1, test_only::mabels_kitchen_counters.Get("marble"_ns) + .TestGetValue() + .unwrap() + .ref()); + ASSERT_EQ(2, test_only::mabels_kitchen_counters.Get("laminate"_ns) + .TestGetValue() + .unwrap() + .ref()); +} + +TEST_F(FOGFixture, TestLabeledStringWorks) { + ASSERT_EQ(mozilla::Nothing(), + test_only::mabels_balloon_strings.Get("twine"_ns) + .TestGetValue() + .unwrap()); + test_only::mabels_balloon_strings.Get("twine"_ns).Set("seems acceptable"_ns); + test_only::mabels_balloon_strings.Get("parachute_cord"_ns) + .Set("preferred"_ns); + ASSERT_STREQ("seems acceptable", + test_only::mabels_balloon_strings.Get("twine"_ns) + .TestGetValue() + .unwrap() + .ref() + .get()); + ASSERT_STREQ("preferred", + test_only::mabels_balloon_strings.Get("parachute_cord"_ns) + .TestGetValue() + .unwrap() + .ref() + .get()); +} + +TEST_F(FOGFixture, TestCppQuantityWorks) { + // This joke only works in base 13. + const uint32_t kValue = 6 * 9; + mozilla::glean::test_only::meaning_of_life.Set(kValue); + + ASSERT_EQ(kValue, mozilla::glean::test_only::meaning_of_life.TestGetValue() + .unwrap() + .value()); +} + +TEST_F(FOGFixture, TestCppRateWorks) { + // 1) Standard rate with internal denominator + const int32_t kNum = 22; + const int32_t kDen = 7; // because I like pi, even just approximately. + + test_only_ipc::irate.AddToNumerator(kNum); + test_only_ipc::irate.AddToDenominator(kDen); + auto value = test_only_ipc::irate.TestGetValue().unwrap(); + ASSERT_EQ(kNum, value.ref().first); + ASSERT_EQ(kDen, value.ref().second); + + // 2) Rate with external denominator + test_only_ipc::rate_with_external_denominator.AddToNumerator(kNum); + test_only_ipc::an_external_denominator.Add(kDen); + value = test_only_ipc::rate_with_external_denominator.TestGetValue().unwrap(); + ASSERT_EQ(kNum, value.ref().first); + ASSERT_EQ(kDen, value.ref().second); + ASSERT_EQ( + kDen, + test_only_ipc::an_external_denominator.TestGetValue().unwrap().extract()); +} + +TEST_F(FOGFixture, TestCppUrlWorks) { + auto kValue = "https://example.com/fog/gtest"_ns; + mozilla::glean::test_only_ipc::a_url.Set(kValue); + + ASSERT_STREQ(kValue.get(), + mozilla::glean::test_only_ipc::a_url.TestGetValue("store1"_ns) + .unwrap() + .value() + .get()); +} + +extern "C" void Rust_TestRustInGTest(); +TEST_F(FOGFixture, TestRustInGTest) { Rust_TestRustInGTest(); } + +extern "C" void Rust_TestJogfile(); +TEST_F(FOGFixture, TestJogfile) { Rust_TestJogfile(); } diff --git a/toolkit/components/glean/tests/gtest/moz.build b/toolkit/components/glean/tests/gtest/moz.build new file mode 100644 index 0000000000..329604e929 --- /dev/null +++ b/toolkit/components/glean/tests/gtest/moz.build @@ -0,0 +1,16 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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 http://mozilla.org/MPL/2.0/. + +if not CONFIG["MOZ_GLEAN_ANDROID"]: + UNIFIED_SOURCES += [ + "TestFog.cpp", + ] + + TEST_HARNESS_FILES.gtest += ["../pytest/jogfile_output"] + +FINAL_LIBRARY = "xul-gtest" + +REQUIRES_UNIFIED_BUILD = True diff --git a/toolkit/components/glean/tests/gtest/test.rs b/toolkit/components/glean/tests/gtest/test.rs new file mode 100644 index 0000000000..f84454b266 --- /dev/null +++ b/toolkit/components/glean/tests/gtest/test.rs @@ -0,0 +1,52 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +extern crate nsstring; +use nsstring::nsString; + +fn nonfatal_fail(msg: String) { + extern "C" { + fn GTest_FOG_ExpectFailure(message: *const ::std::os::raw::c_char); + } + unsafe { + let msg = ::std::ffi::CString::new(msg).unwrap(); + GTest_FOG_ExpectFailure(msg.as_ptr()); + } +} + +/// This macro checks if the expression evaluates to true, +/// and causes a non-fatal GTest test failure if it doesn't. +macro_rules! expect { + ($x:expr) => { + match (&$x) { + true => {} + false => nonfatal_fail(format!( + "check failed: (`{}`) at {}:{}", + stringify!($x), + file!(), + line!() + )), + } + }; +} + +#[no_mangle] +pub extern "C" fn Rust_TestRustInGTest() { + // Just a smoke test, we show here how tests might work that both + // a) Are in Rust, and + // b) Require Gecko + // This demonstration doesn't actually require Gecko. But we pretend it + // does so we remember how to do this rust-in-gtest pattern. + fog::metrics::test_only::bad_code.add(12); + expect!(fog::metrics::test_only::bad_code.test_get_value(None) == Some(12)); +} + +#[no_mangle] +pub extern "C" fn Rust_TestJogfile() { + // Ensure that the generated jogfile in t/c/g/tests/pytest + // (which is installed nearby using TEST_HARNESS_FILES) + // can be consumed by JOG's inner workings + // + // If it can't, that's perhaps a sign that the inner workings need to be updated. + expect!(jog::jog_load_jogfile(&nsString::from("jogfile_output"))); +} diff --git a/toolkit/components/glean/tests/moz.build b/toolkit/components/glean/tests/moz.build new file mode 100644 index 0000000000..e7de2c3b66 --- /dev/null +++ b/toolkit/components/glean/tests/moz.build @@ -0,0 +1,16 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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 http://mozilla.org/MPL/2.0/. + +PYTHON_UNITTEST_MANIFESTS += ["pytest/python.ini"] + +TEST_DIRS += ["gtest"] + +XPCSHELL_TESTS_MANIFESTS += ["xpcshell/xpcshell.ini"] + +BROWSER_CHROME_MANIFESTS += ["browser/browser.ini"] + +with Files("**"): + BUG_COMPONENT = ("Toolkit", "Telemetry") diff --git a/toolkit/components/glean/tests/pytest/expect_helper.py b/toolkit/components/glean/tests/pytest/expect_helper.py new file mode 100644 index 0000000000..543ef04026 --- /dev/null +++ b/toolkit/components/glean/tests/pytest/expect_helper.py @@ -0,0 +1,34 @@ +# 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 http://mozilla.org/MPL/2.0/. + +import inspect +import os + + +def expect(path, actual): + """ + Assert that the content of the file at `path` contains `actual`. + + If the environment variable `UPDATE_EXPECT` is set, the path content is updated to `actual`. + This allows to update the file contents easily. + """ + + callerframerecord = inspect.stack()[1] + frame = callerframerecord[0] + info = inspect.getframeinfo(frame) + msg = f""" +Unexpected content in {path} (at {info.filename}:{info.lineno}) + +If the code generation was changed, +run the test suite again with `UPDATE_EXPECT=1` set to update the test files. +""".strip() + + if "UPDATE_EXPECT" in os.environ: + with open(path, "w") as file: + file.write(actual) + + expected = None + with open(path, "r") as file: + expected = file.read() + assert actual == expected, msg diff --git a/toolkit/components/glean/tests/pytest/gifft_output_Event b/toolkit/components/glean/tests/pytest/gifft_output_Event new file mode 100644 index 0000000000..565fcca59d --- /dev/null +++ b/toolkit/components/glean/tests/pytest/gifft_output_Event @@ -0,0 +1,38 @@ +// -*- mode: C++ -*- + +/* This file is auto-generated by run_glean_parser.py. + It is only for internal use by types in + toolkit/components/glean/bindings/private */ + +#include "mozilla/AppShutdown.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/Maybe.h" +#include "mozilla/Telemetry.h" +#include "mozilla/Tuple.h" +#include "mozilla/DataMutex.h" +#include "nsThreadUtils.h" + +#ifndef mozilla_glean_EventGifftMap_h +#define mozilla_glean_EventGifftMap_h + +namespace mozilla::glean { + +using Telemetry::EventID; + + +static inline Maybe<EventID> EventIdForMetric(uint32_t aId) { + switch(aId) { + case 17: { // test.nested.event_metric + return Some(EventID::EventMetric_EnumNames_AreStrange); + } + case 18: { // test.nested.event_metric_with_extra + return Some(EventID::EventMetric_EnumName_WithExtra); + } + default: { + return Nothing(); + } + } +} + +} // namespace mozilla::glean +#endif // mozilla_glean_EventGifftMaps_h diff --git a/toolkit/components/glean/tests/pytest/gifft_output_EventExtra b/toolkit/components/glean/tests/pytest/gifft_output_EventExtra new file mode 100644 index 0000000000..f5f2671d99 --- /dev/null +++ b/toolkit/components/glean/tests/pytest/gifft_output_EventExtra @@ -0,0 +1,35 @@ +// -*- mode: C++ -*- + +/* This file is auto-generated by run_glean_parser.py. + It is only for internal use by types in + toolkit/components/glean/bindings/private */ + +#include "mozilla/glean/bindings/Event.h" +#include "mozilla/glean/GleanMetrics.h" + +namespace mozilla::glean { + +template <> +/*static*/ const nsCString impl::EventMetric<NoExtraKeys>::ExtraStringForKey(uint32_t aKey) { + MOZ_ASSERT_UNREACHABLE("What are you doing here? No extra keys!"); + return ""_ns; +} + +template <> +/*static*/ const nsCString impl::EventMetric<test_nested::EventMetricWithExtraExtra>::ExtraStringForKey(uint32_t aKey) { + using test_nested::EventMetricWithExtraExtra; + switch (aKey) { + case 0: { + return "an_extra_key"_ns; + } + case 1: { + return "another_extra_key"_ns; + } + default: { + MOZ_ASSERT_UNREACHABLE("Impossible event key reached."); + return ""_ns; + } + } +} + +}; // namespace mozilla::glean diff --git a/toolkit/components/glean/tests/pytest/gifft_output_Histogram b/toolkit/components/glean/tests/pytest/gifft_output_Histogram new file mode 100644 index 0000000000..ef01708dfe --- /dev/null +++ b/toolkit/components/glean/tests/pytest/gifft_output_Histogram @@ -0,0 +1,113 @@ +// -*- mode: C++ -*- + +/* This file is auto-generated by run_glean_parser.py. + It is only for internal use by types in + toolkit/components/glean/bindings/private */ + +#include "mozilla/AppShutdown.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/Maybe.h" +#include "mozilla/Telemetry.h" +#include "mozilla/Tuple.h" +#include "mozilla/DataMutex.h" +#include "nsThreadUtils.h" + +#ifndef mozilla_glean_HistogramGifftMap_h +#define mozilla_glean_HistogramGifftMap_h + +namespace mozilla::glean { + +using Telemetry::HistogramID; + + +using MetricId = uint32_t; // Same type as in api/src/private/mod.rs +using TimerId = uint64_t; // Same as in TimingDistribution.h. +using MetricTimerTuple = Tuple<MetricId, TimerId>; +class MetricTimerTupleHashKey : public PLDHashEntryHdr { + public: + using KeyType = const MetricTimerTuple&; + using KeyTypePointer = const MetricTimerTuple*; + + explicit MetricTimerTupleHashKey(KeyTypePointer aKey) : mValue(*aKey) {} + MetricTimerTupleHashKey(MetricTimerTupleHashKey&& aOther) + : PLDHashEntryHdr(std::move(aOther)), + mValue(std::move(aOther.mValue)) {} + ~MetricTimerTupleHashKey() = default; + + KeyType GetKey() const { return mValue; } + bool KeyEquals(KeyTypePointer aKey) const { + return Get<0>(*aKey) == Get<0>(mValue) && Get<1>(*aKey) == Get<1>(mValue); + } + + static KeyTypePointer KeyToPointer(KeyType aKey) { return &aKey; } + static PLDHashNumber HashKey(KeyTypePointer aKey) { + // Chosen because this is how nsIntegralHashKey does it. + return HashGeneric(Get<0>(*aKey), Get<1>(*aKey)); + } + enum { ALLOW_MEMMOVE = true }; + + private: + const MetricTimerTuple mValue; +}; + +typedef StaticDataMutex<UniquePtr<nsTHashMap<MetricTimerTupleHashKey, TimeStamp>>> TimerToStampMutex; +static inline Maybe<TimerToStampMutex::AutoLock> GetTimerIdToStartsLock() { + static TimerToStampMutex sTimerIdToStarts("sTimerIdToStarts"); + auto lock = sTimerIdToStarts.Lock(); + // GIFFT will work up to the end of AppShutdownTelemetry. + if (AppShutdown::IsInOrBeyond(ShutdownPhase::XPCOMWillShutdown)) { + return Nothing(); + } + if (!*lock) { + *lock = MakeUnique<nsTHashMap<MetricTimerTupleHashKey, TimeStamp>>(); + RefPtr<nsIRunnable> cleanupFn = NS_NewRunnableFunction(__func__, [&] { + if (AppShutdown::IsInOrBeyond(ShutdownPhase::XPCOMWillShutdown)) { + auto lock = sTimerIdToStarts.Lock(); + *lock = nullptr; // deletes, see UniquePtr.h + return; + } + RunOnShutdown([&] { + auto lock = sTimerIdToStarts.Lock(); + *lock = nullptr; // deletes, see UniquePtr.h + }, ShutdownPhase::XPCOMWillShutdown); + }); + // Both getting the main thread and dispatching to it can fail. + // In that event we leak. Grab a pointer so we have something to NS_RELEASE + // in that case. + nsIRunnable* temp = cleanupFn.get(); + nsCOMPtr<nsIThread> mainThread; + if (NS_FAILED(NS_GetMainThread(getter_AddRefs(mainThread))) + || NS_FAILED(mainThread->Dispatch(cleanupFn.forget(), nsIThread::DISPATCH_NORMAL)) + ) { + // Failed to dispatch cleanup routine. + // First, un-leak the runnable (but only if we actually attempted dispatch) + if (!cleanupFn) { + NS_RELEASE(temp); + } + // Next, cleanup immediately, and allow metrics to try again later. + *lock = nullptr; + return Nothing(); + } + } + return Some(std::move(lock)); +} + +static Maybe<HistogramID> HistogramIdForMetric(uint32_t aId) { + switch(aId) { + case 12: { // test.timing_distribution_metric + return Some(HistogramID::SOME_TIME_HISTOGRAM_MS); + } + case 13: { // test.memory_distribution_metric + return Some(HistogramID::SOME_MEM_HISTOGRAM_KB); + } + case 14: { // test.custom_distribution_metric + return Some(HistogramID::SOME_LINEAR_HISTOGRAM); + } + default: { + return Nothing(); + } + } +} + +} // namespace mozilla::glean +#endif // mozilla_glean_HistogramGifftMaps_h diff --git a/toolkit/components/glean/tests/pytest/gifft_output_Scalar b/toolkit/components/glean/tests/pytest/gifft_output_Scalar new file mode 100644 index 0000000000..0e79e4d00d --- /dev/null +++ b/toolkit/components/glean/tests/pytest/gifft_output_Scalar @@ -0,0 +1,189 @@ +// -*- mode: C++ -*- + +/* This file is auto-generated by run_glean_parser.py. + It is only for internal use by types in + toolkit/components/glean/bindings/private */ + +#include "mozilla/AppShutdown.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/Maybe.h" +#include "mozilla/Telemetry.h" +#include "mozilla/Tuple.h" +#include "mozilla/DataMutex.h" +#include "mozilla/Tuple.h" +#include "nsClassHashtable.h" +#include "nsIThread.h" +#include "nsTHashMap.h" +#include "nsThreadUtils.h" + +#ifndef mozilla_glean_ScalarGifftMap_h +#define mozilla_glean_ScalarGifftMap_h + +namespace mozilla::glean { + +using Telemetry::ScalarID; + +typedef nsUint32HashKey SubmetricIdHashKey; +typedef nsTHashMap<SubmetricIdHashKey, Tuple<ScalarID, nsString>> + SubmetricToLabeledMirrorMapType; +typedef StaticDataMutex<UniquePtr<SubmetricToLabeledMirrorMapType>> + SubmetricToMirrorMutex; +static inline Maybe<SubmetricToMirrorMutex::AutoLock> GetLabeledMirrorLock() { + static SubmetricToMirrorMutex sLabeledMirrors("sLabeledMirrors"); + auto lock = sLabeledMirrors.Lock(); + // GIFFT will work up to the end of AppShutdownTelemetry. + if (AppShutdown::IsInOrBeyond(ShutdownPhase::XPCOMWillShutdown)) { + return Nothing(); + } + if (!*lock) { + *lock = MakeUnique<SubmetricToLabeledMirrorMapType>(); + RefPtr<nsIRunnable> cleanupFn = NS_NewRunnableFunction(__func__, [&] { + if (AppShutdown::IsInOrBeyond(ShutdownPhase::XPCOMWillShutdown)) { + auto lock = sLabeledMirrors.Lock(); + *lock = nullptr; // deletes, see UniquePtr.h + return; + } + RunOnShutdown([&] { + auto lock = sLabeledMirrors.Lock(); + *lock = nullptr; // deletes, see UniquePtr.h + }, ShutdownPhase::XPCOMWillShutdown); + }); + // Both getting the main thread and dispatching to it can fail. + // In that event we leak. Grab a pointer so we have something to NS_RELEASE + // in that case. + nsIRunnable* temp = cleanupFn.get(); + nsCOMPtr<nsIThread> mainThread; + if (NS_FAILED(NS_GetMainThread(getter_AddRefs(mainThread))) + || NS_FAILED(mainThread->Dispatch(cleanupFn.forget(), nsIThread::DISPATCH_NORMAL)) + ) { + // Failed to dispatch cleanup routine. + // First, un-leak the runnable (but only if we actually attempted dispatch) + if (!cleanupFn) { + NS_RELEASE(temp); + } + // Next, cleanup immediately, and allow metrics to try again later. + *lock = nullptr; + return Nothing(); + } + } + return Some(std::move(lock)); +} + +namespace { +class ScalarIDHashKey : public PLDHashEntryHdr { + public: + typedef const ScalarID& KeyType; + typedef const ScalarID* KeyTypePointer; + + explicit ScalarIDHashKey(KeyTypePointer aKey) : mValue(*aKey) {} + ScalarIDHashKey(ScalarIDHashKey&& aOther) + : PLDHashEntryHdr(std::move(aOther)), mValue(std::move(aOther.mValue)) {} + ~ScalarIDHashKey() = default; + + KeyType GetKey() const { return mValue; } + bool KeyEquals(KeyTypePointer aKey) const { return *aKey == mValue; } + + static KeyTypePointer KeyToPointer(KeyType aKey) { return &aKey; } + static PLDHashNumber HashKey(KeyTypePointer aKey) { + return static_cast<std::underlying_type<ScalarID>::type>(*aKey); + } + enum { ALLOW_MEMMOVE = true }; + + private: + const ScalarID mValue; +}; +} // namespace +typedef StaticDataMutex<UniquePtr<nsTHashMap<ScalarIDHashKey, TimeStamp>>> TimesToStartsMutex; +static inline Maybe<TimesToStartsMutex::AutoLock> GetTimesToStartsLock() { + static TimesToStartsMutex sTimespanStarts("sTimespanStarts"); + auto lock = sTimespanStarts.Lock(); + // GIFFT will work up to the end of AppShutdownTelemetry. + if (AppShutdown::IsInOrBeyond(ShutdownPhase::XPCOMWillShutdown)) { + return Nothing(); + } + if (!*lock) { + *lock = MakeUnique<nsTHashMap<ScalarIDHashKey, TimeStamp>>(); + RefPtr<nsIRunnable> cleanupFn = NS_NewRunnableFunction(__func__, [&] { + if (AppShutdown::IsInOrBeyond(ShutdownPhase::XPCOMWillShutdown)) { + auto lock = sTimespanStarts.Lock(); + *lock = nullptr; // deletes, see UniquePtr.h + return; + } + RunOnShutdown([&] { + auto lock = sTimespanStarts.Lock(); + *lock = nullptr; // deletes, see UniquePtr.h + }, ShutdownPhase::XPCOMWillShutdown); + }); + // Both getting the main thread and dispatching to it can fail. + // In that event we leak. Grab a pointer so we have something to NS_RELEASE + // in that case. + nsIRunnable* temp = cleanupFn.get(); + nsCOMPtr<nsIThread> mainThread; + if (NS_FAILED(NS_GetMainThread(getter_AddRefs(mainThread))) + || NS_FAILED(mainThread->Dispatch(cleanupFn.forget(), nsIThread::DISPATCH_NORMAL)) + ) { + // Failed to dispatch cleanup routine. + // First, un-leak the runnable (but only if we actually attempted dispatch) + if (!cleanupFn) { + NS_RELEASE(temp); + } + // Next, cleanup immediately, and allow metrics to try again later. + *lock = nullptr; + return Nothing(); + } + } + return Some(std::move(lock)); +} + +static inline bool IsSubmetricId(uint32_t aId) { + // Submetrics have the 2^25 bit set. + // (ID_BITS - ID_SIGNAL_BITS, keep it in sync with js.py). + return (aId & (1 << 25)) > 0; +} + +static Maybe<ScalarID> ScalarIdForMetric(uint32_t aId) { + switch(aId) { + case 1: { // test.boolean_metric + return Some(ScalarID::SOME_BOOL_SCALAR); + } + case 2: { // test.labeled_boolean_metric + return Some(ScalarID::SOME_KEYED_BOOL_SCALAR); + } + case 3: { // test.labeled_boolean_metric_labels + return Some(ScalarID::SOME_OTHER_KEYED_BOOL_SCALAR); + } + case 4: { // test.counter_metric + return Some(ScalarID::SOME_UINT_SCALAR); + } + case 5: { // test.labeled_counter_metric + return Some(ScalarID::SOME_KEYED_UINT_SCALAR); + } + case 6: { // test.labeled_counter_metric_labels + return Some(ScalarID::SOME_OTHER_KEYED_UINT_SCALAR); + } + case 7: { // test.string_metric + return Some(ScalarID::SOME_STRING_SCALAR); + } + case 10: { // test.string_list_metric + return Some(ScalarID::YET_ANOTHER_KEYED_BOOL_SCALAR); + } + case 11: { // test.timespan_metric + return Some(ScalarID::SOME_OTHER_UINT_SCALAR); + } + case 15: { // test.nested.uuid_metric + return Some(ScalarID::SOME_OTHER_STRING_SCALAR); + } + case 16: { // test.nested.datetime_metric + return Some(ScalarID::SOME_STILL_OTHER_STRING_SCALAR); + } + case 19: { // test.nested.quantity_metric + return Some(ScalarID::TELEMETRY_TEST_MIRROR_FOR_QUANTITY); + } + default: { + return Nothing(); + } + } +} + +} // namespace mozilla::glean +#endif // mozilla_glean_ScalarGifftMaps_h diff --git a/toolkit/components/glean/tests/pytest/jogfile_output b/toolkit/components/glean/tests/pytest/jogfile_output new file mode 100644 index 0000000000..7a38cc2fc0 --- /dev/null +++ b/toolkit/components/glean/tests/pytest/jogfile_output @@ -0,0 +1,287 @@ +{ + "metrics": { + "test": [ + [ + "boolean", + "boolean_metric", + [ + "metrics" + ], + "application", + false + ], + [ + "labeled_boolean", + "labeled_boolean_metric", + [ + "metrics" + ], + "application", + false + ], + [ + "labeled_boolean", + "labeled_boolean_metric_labels", + [ + "metrics" + ], + "application", + false + ], + [ + "counter", + "counter_metric", + [ + "metrics" + ], + "application", + false + ], + [ + "labeled_counter", + "labeled_counter_metric", + [ + "metrics" + ], + "application", + false + ], + [ + "labeled_counter", + "labeled_counter_metric_labels", + [ + "metrics" + ], + "application", + false + ], + [ + "string", + "string_metric", + [ + "metrics" + ], + "application", + false + ], + [ + "labeled_string", + "labeled_string_metric", + [ + "metrics" + ], + "application", + false + ], + [ + "labeled_string", + "labeled_string_metric_labels", + [ + "metrics" + ], + "application", + false + ], + [ + "string_list", + "string_list_metric", + [ + "metrics" + ], + "application", + false + ], + [ + "timespan", + "timespan_metric", + [ + "metrics" + ], + "application", + false, + { + "time_unit": "millisecond" + } + ], + [ + "timing_distribution", + "timing_distribution_metric", + [ + "metrics" + ], + "application", + false, + { + "time_unit": "nanosecond" + } + ], + [ + "memory_distribution", + "memory_distribution_metric", + [ + "metrics" + ], + "application", + false, + { + "memory_unit": "kilobyte" + } + ], + [ + "custom_distribution", + "custom_distribution_metric", + [ + "metrics" + ], + "application", + false, + { + "bucket_count": 100, + "histogram_type": "linear", + "range_max": 100, + "range_min": 0 + } + ] + ], + "test.nested": [ + [ + "uuid", + "uuid_metric", + [ + "metrics" + ], + "application", + false + ], + [ + "datetime", + "datetime_metric", + [ + "metrics" + ], + "application", + false, + { + "time_unit": "millisecond" + } + ], + [ + "event", + "event_metric", + [ + "events" + ], + "ping", + false, + { + "allowed_extra_keys": [] + } + ], + [ + "event", + "event_metric_with_extra", + [ + "events" + ], + "ping", + false, + { + "allowed_extra_keys": [ + "an_extra_key", + "another_extra_key" + ] + } + ], + [ + "quantity", + "quantity_metric", + [ + "metrics" + ], + "ping", + false + ], + [ + "rate", + "rate_metric", + [ + "metrics" + ], + "ping", + false + ], + [ + "rate", + "rate_with_external_denominator", + [ + "metrics" + ], + "ping", + false + ], + [ + "denominator", + "external_denominator", + [ + "metrics" + ], + "ping", + false, + { + "numerators": [ + [ + "rate_with_external_denominator", + "test.nested", + [ + "metrics" + ], + "ping", + false, + null + ] + ] + } + ] + ] + }, + "pings": [ + [ + "not-baseline", + true, + false, + [ + "background", + "dirty_startup", + "foreground" + ] + ], + [ + "not-metrics", + true, + false, + [ + "overdue", + "reschedule", + "today", + "tomorrow", + "upgrade" + ] + ], + [ + "not-events", + true, + false, + [ + "background", + "max_capacity", + "startup" + ] + ], + [ + "not-deletion-request", + true, + true, + [] + ] + ] +}
\ No newline at end of file diff --git a/toolkit/components/glean/tests/pytest/metrics_expires_versions_test.yaml b/toolkit/components/glean/tests/pytest/metrics_expires_versions_test.yaml new file mode 100644 index 0000000000..ea73a6465e --- /dev/null +++ b/toolkit/components/glean/tests/pytest/metrics_expires_versions_test.yaml @@ -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 http://mozilla.org/MPL/2.0/. + +# This file is FOR TESTING PURPOSES ONLY. + +--- +$schema: moz://mozilla.org/schemas/glean/metrics/2-0-0 + +test: + expired1: + type: boolean + expires: 41 + description: | + A multi-line + description + lifetime: application + notification_emails: + - glean-team@mozilla.com + bugs: + - https://bugzilla.mozilla.org/1664306 + data_reviews: + - https://example.com + no_lint: + - EXPIRED + + expired2: + type: labeled_boolean + expires: 42 + description: | + A multi-line + description + lifetime: application + notification_emails: + - glean-team@mozilla.com + bugs: + - https://bugzilla.mozilla.org/1664306 + data_reviews: + - https://example.com + no_lint: + - EXPIRED + + unexpired: + type: labeled_boolean + expires: 100 + description: | + A multi-line + description + lifetime: application + notification_emails: + - glean-team@mozilla.com + bugs: + - https://bugzilla.mozilla.org/1664306 + data_reviews: + - https://example.com + labels: + - one_label + - two_labels + + never: + type: string + expires: never + description: A never-expiring metric + lifetime: application + notification_emails: + - glean-team@mozilla.com + bugs: + - https://bugzilla.mozilla.org/1664306 + data_reviews: + - https://example.com + + always: + type: string + expires: expired + description: An already-expired metric + lifetime: application + notification_emails: + - glean-team@mozilla.com + bugs: + - https://bugzilla.mozilla.org/1664306 + data_reviews: + - https://example.com + no_lint: + - EXPIRED diff --git a/toolkit/components/glean/tests/pytest/metrics_test.yaml b/toolkit/components/glean/tests/pytest/metrics_test.yaml new file mode 100644 index 0000000000..8b4a7404f1 --- /dev/null +++ b/toolkit/components/glean/tests/pytest/metrics_test.yaml @@ -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 http://mozilla.org/MPL/2.0/. + +# This file defines the metrics that are recorded by the Glean SDK. They are +# automatically converted to platform-specific code at build time using the +# `glean_parser` PyPI package. + +# This file is presently for Internal FOG Use Only. +# You should not add metrics here until probably about January of 2021. +# If you're looking for the metrics.yaml for Geckoveiw Streaming Telemetry, +# you can find that one in toolkit/components/telemetry/geckoview/streaming. + +--- +$schema: moz://mozilla.org/schemas/glean/metrics/2-0-0 + +test: + boolean_metric: + type: boolean + expires: never + description: | + A multi-line + description + lifetime: application + notification_emails: + - glean-team@mozilla.com + bugs: + - https://bugzilla.mozilla.org/1635260/ + data_reviews: + - https://example.com + telemetry_mirror: SOME_BOOL_SCALAR + + labeled_boolean_metric: + type: labeled_boolean + expires: never + description: | + A multi-line + description + lifetime: application + notification_emails: + - glean-team@mozilla.com + bugs: + - https://bugzilla.mozilla.org/1635260/ + data_reviews: + - https://example.com + telemetry_mirror: SOME_KEYED_BOOL_SCALAR + + labeled_boolean_metric_labels: + type: labeled_boolean + expires: never + description: | + A multi-line + description + lifetime: application + notification_emails: + - glean-team@mozilla.com + bugs: + - https://bugzilla.mozilla.org/1635260/ + data_reviews: + - https://example.com + labels: + - one_label + - two_labels + - three_labels + - four_labels + - five_labels + - six_labels + - seven_labels + - eight_labels + - nine_labels + - ten_labels + telemetry_mirror: SOME_OTHER_KEYED_BOOL_SCALAR + + counter_metric: + type: counter + expires: never + description: | + A multi-line + description + lifetime: application + notification_emails: + - glean-team@mozilla.com + bugs: + - https://bugzilla.mozilla.org/1635260/ + data_reviews: + - https://example.com + telemetry_mirror: SOME_UINT_SCALAR + + labeled_counter_metric: + type: labeled_counter + expires: never + description: | + A multi-line + description + lifetime: application + notification_emails: + - glean-team@mozilla.com + bugs: + - https://bugzilla.mozilla.org/1635260/ + data_reviews: + - https://example.com + telemetry_mirror: SOME_KEYED_UINT_SCALAR + + labeled_counter_metric_labels: + type: labeled_counter + expires: never + description: | + A multi-line + description + lifetime: application + notification_emails: + - glean-team@mozilla.com + bugs: + - https://bugzilla.mozilla.org/1635260/ + data_reviews: + - https://example.com + labels: + - one_label + - two_labels + telemetry_mirror: SOME_OTHER_KEYED_UINT_SCALAR + + string_metric: + type: string + expires: never + description: | + A multi-line + description + lifetime: application + notification_emails: + - glean-team@mozilla.com + bugs: + - https://bugzilla.mozilla.org/1635260/ + data_reviews: + - https://example.com + telemetry_mirror: SOME_STRING_SCALAR + + labeled_string_metric: + type: labeled_string + expires: never + description: | + A multi-line + description + lifetime: application + notification_emails: + - glean-team@mozilla.com + bugs: + - https://bugzilla.mozilla.org/1635260/ + data_reviews: + - https://example.com + + labeled_string_metric_labels: + type: labeled_string + expires: never + description: | + A multi-line + description + lifetime: application + notification_emails: + - glean-team@mozilla.com + bugs: + - https://bugzilla.mozilla.org/1635260/ + data_reviews: + - https://example.com + labels: + - one_label + - two_labels + + string_list_metric: + type: string_list + expires: never + description: | + A multi-line + description + lifetime: application + notification_emails: + - glean-team@mozilla.com + bugs: + - https://bugzilla.mozilla.org/1635260/ + data_reviews: + - https://example.com + telemetry_mirror: YET_ANOTHER_KEYED_BOOL_SCALAR + + timespan_metric: + type: timespan + expires: never + description: | + A multi-line + description + lifetime: application + notification_emails: + - glean-team@mozilla.com + bugs: + - https://bugzilla.mozilla.org/1635260/ + data_reviews: + - https://example.com + telemetry_mirror: SOME_OTHER_UINT_SCALAR + + timing_distribution_metric: + type: timing_distribution + expires: never + description: | + A multi-line + description + lifetime: application + notification_emails: + - glean-team@mozilla.com + bugs: + - https://bugzilla.mozilla.org/1635260/ + data_reviews: + - https://example.com + telemetry_mirror: SOME_TIME_HISTOGRAM_MS + + memory_distribution_metric: + type: memory_distribution + expires: never + description: | + A multi-line + description + lifetime: application + notification_emails: + - glean-team@mozilla.com + bugs: + - https://bugzilla.mozilla.org/1635260/ + data_reviews: + - https://example.com + memory_unit: kilobyte + telemetry_mirror: SOME_MEM_HISTOGRAM_KB + + custom_distribution_metric: + type: custom_distribution + expires: never + description: | + A multi-line + description + lifetime: application + notification_emails: + - glean-team@mozilla.com + bugs: + - https://bugzilla.mozilla.org/1635260/ + data_reviews: + - https://example.com + range_min: 0 + range_max: 100 + bucket_count: 100 + histogram_type: linear + telemetry_mirror: SOME_LINEAR_HISTOGRAM + +test.nested: + uuid_metric: + type: uuid + expires: never + description: | + A multi-line + description + lifetime: application + notification_emails: + - glean-team@mozilla.com + bugs: + - https://bugzilla.mozilla.org/1635260/ + data_reviews: + - https://example.com + telemetry_mirror: SOME_OTHER_STRING_SCALAR + + datetime_metric: + type: datetime + expires: never + description: | + A multi-line + description + lifetime: application + notification_emails: + - glean-team@mozilla.com + bugs: + - https://bugzilla.mozilla.org/1635260/ + data_reviews: + - https://example.com + telemetry_mirror: SOME_STILL_OTHER_STRING_SCALAR + + event_metric: + type: event + expires: never + description: | + A multi-line + description + lifetime: ping + notification_emails: + - glean-team@mozilla.com + bugs: + - https://bugzilla.mozilla.org/1635260/ + data_reviews: + - https://example.com + telemetry_mirror: EventMetric_EnumNames_AreStrange + + event_metric_with_extra: + type: event + expires: never + description: | + A multi-line + description + lifetime: ping + notification_emails: + - glean-team@mozilla.com + bugs: + - https://bugzilla.mozilla.org/1635260/ + data_reviews: + - https://example.com + extra_keys: + an_extra_key: + type: string + description: An extra key description + another_extra_key: + type: string + description: Another extra key description + telemetry_mirror: EventMetric_EnumName_WithExtra + + quantity_metric: + type: quantity + unit: someunit + expires: never + description: | + A multi-line + description + notification_emails: + - glean-team@mozilla.com + bugs: + - https://bugzilla.mozilla.org/1704846/ + data_reviews: + - https://example.com + telemetry_mirror: TELEMETRY_TEST_MIRROR_FOR_QUANTITY + + rate_metric: + type: rate + expires: never + description: | + A multi-line + description + notification_emails: + - glean-team@mozilla.com + bugs: + - https://bugzilla.mozilla.org/1704846/ + data_reviews: + - https://example.com + + rate_with_external_denominator: + type: rate + denominator_metric: test.nested.external_denominator + expires: never + description: | + A multi-line + description + notification_emails: + - glean-team@mozilla.com + bugs: + - https://bugzilla.mozilla.org/1704846/ + data_reviews: + - https://example.com + + external_denominator: + type: counter + expires: never + description: | + A multi-line + description + notification_emails: + - glean-team@mozilla.com + bugs: + - https://bugzilla.mozilla.org/1704846/ + data_reviews: + - https://example.com diff --git a/toolkit/components/glean/tests/pytest/metrics_test_output b/toolkit/components/glean/tests/pytest/metrics_test_output new file mode 100644 index 0000000000..9bb1438306 --- /dev/null +++ b/toolkit/components/glean/tests/pytest/metrics_test_output @@ -0,0 +1,693 @@ +// -*- mode: Rust -*- + +// AUTOGENERATED BY glean_parser. DO NOT EDIT. + +/* 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 http://mozilla.org/MPL/2.0/. */ + +pub mod test { + use crate::private::*; + use glean::CommonMetricData; + #[allow(unused_imports)] // HistogramType might be unusued, let's avoid warnings + use glean::HistogramType; + use once_cell::sync::Lazy; + + #[allow(non_upper_case_globals)] + /// generated from test.boolean_metric + /// + /// A multi-line + /// description + pub static boolean_metric: Lazy<BooleanMetric> = Lazy::new(|| { + BooleanMetric::new(1.into(), CommonMetricData { + name: "boolean_metric".into(), + category: "test".into(), + send_in_pings: vec!["metrics".into()], + lifetime: Lifetime::Application, + disabled: false, + ..Default::default() + }) + }); + + #[allow(non_upper_case_globals)] + /// generated from test.labeled_boolean_metric + /// + /// A multi-line + /// description + pub static labeled_boolean_metric: Lazy<LabeledMetric<LabeledBooleanMetric>> = Lazy::new(|| { + LabeledMetric::new(2.into(), CommonMetricData { + name: "labeled_boolean_metric".into(), + category: "test".into(), + send_in_pings: vec!["metrics".into()], + lifetime: Lifetime::Application, + disabled: false, + ..Default::default() + }, None) + }); + + #[allow(non_upper_case_globals)] + /// generated from test.labeled_boolean_metric_labels + /// + /// A multi-line + /// description + pub static labeled_boolean_metric_labels: Lazy<LabeledMetric<LabeledBooleanMetric>> = Lazy::new(|| { + LabeledMetric::new(3.into(), CommonMetricData { + name: "labeled_boolean_metric_labels".into(), + category: "test".into(), + send_in_pings: vec!["metrics".into()], + lifetime: Lifetime::Application, + disabled: false, + ..Default::default() + }, Some({const S: &'static str = "eight_labelsfive_labelsfour_labelsnine_labelsone_labelseven_labelssix_labelsten_labelsthree_labelstwo_labels";const LENGTHS: [u8; 10] = [12, 11, 11, 11, 9, 12, 10, 10, 12, 10];let mut offset = 0;LENGTHS.iter().map(|len| { let start = offset; offset += *len as usize; S[start..offset].into()}).collect()})) + }); + + #[allow(non_upper_case_globals)] + /// generated from test.counter_metric + /// + /// A multi-line + /// description + pub static counter_metric: Lazy<CounterMetric> = Lazy::new(|| { + CounterMetric::new(4.into(), CommonMetricData { + name: "counter_metric".into(), + category: "test".into(), + send_in_pings: vec!["metrics".into()], + lifetime: Lifetime::Application, + disabled: false, + ..Default::default() + }) + }); + + #[allow(non_upper_case_globals)] + /// generated from test.labeled_counter_metric + /// + /// A multi-line + /// description + pub static labeled_counter_metric: Lazy<LabeledMetric<LabeledCounterMetric>> = Lazy::new(|| { + LabeledMetric::new(5.into(), CommonMetricData { + name: "labeled_counter_metric".into(), + category: "test".into(), + send_in_pings: vec!["metrics".into()], + lifetime: Lifetime::Application, + disabled: false, + ..Default::default() + }, None) + }); + + #[allow(non_upper_case_globals)] + /// generated from test.labeled_counter_metric_labels + /// + /// A multi-line + /// description + pub static labeled_counter_metric_labels: Lazy<LabeledMetric<LabeledCounterMetric>> = Lazy::new(|| { + LabeledMetric::new(6.into(), CommonMetricData { + name: "labeled_counter_metric_labels".into(), + category: "test".into(), + send_in_pings: vec!["metrics".into()], + lifetime: Lifetime::Application, + disabled: false, + ..Default::default() + }, Some(vec!["one_label".into(), "two_labels".into()])) + }); + + #[allow(non_upper_case_globals)] + /// generated from test.string_metric + /// + /// A multi-line + /// description + pub static string_metric: Lazy<StringMetric> = Lazy::new(|| { + StringMetric::new(7.into(), CommonMetricData { + name: "string_metric".into(), + category: "test".into(), + send_in_pings: vec!["metrics".into()], + lifetime: Lifetime::Application, + disabled: false, + ..Default::default() + }) + }); + + #[allow(non_upper_case_globals)] + /// generated from test.labeled_string_metric + /// + /// A multi-line + /// description + pub static labeled_string_metric: Lazy<LabeledMetric<LabeledStringMetric>> = Lazy::new(|| { + LabeledMetric::new(8.into(), CommonMetricData { + name: "labeled_string_metric".into(), + category: "test".into(), + send_in_pings: vec!["metrics".into()], + lifetime: Lifetime::Application, + disabled: false, + ..Default::default() + }, None) + }); + + #[allow(non_upper_case_globals)] + /// generated from test.labeled_string_metric_labels + /// + /// A multi-line + /// description + pub static labeled_string_metric_labels: Lazy<LabeledMetric<LabeledStringMetric>> = Lazy::new(|| { + LabeledMetric::new(9.into(), CommonMetricData { + name: "labeled_string_metric_labels".into(), + category: "test".into(), + send_in_pings: vec!["metrics".into()], + lifetime: Lifetime::Application, + disabled: false, + ..Default::default() + }, Some(vec!["one_label".into(), "two_labels".into()])) + }); + + #[allow(non_upper_case_globals)] + /// generated from test.string_list_metric + /// + /// A multi-line + /// description + pub static string_list_metric: Lazy<StringListMetric> = Lazy::new(|| { + StringListMetric::new(10.into(), CommonMetricData { + name: "string_list_metric".into(), + category: "test".into(), + send_in_pings: vec!["metrics".into()], + lifetime: Lifetime::Application, + disabled: false, + ..Default::default() + }) + }); + + #[allow(non_upper_case_globals)] + /// generated from test.timespan_metric + /// + /// A multi-line + /// description + pub static timespan_metric: Lazy<TimespanMetric> = Lazy::new(|| { + TimespanMetric::new(11.into(), CommonMetricData { + name: "timespan_metric".into(), + category: "test".into(), + send_in_pings: vec!["metrics".into()], + lifetime: Lifetime::Application, + disabled: false, + ..Default::default() + }, TimeUnit::Millisecond) + }); + + #[allow(non_upper_case_globals)] + /// generated from test.timing_distribution_metric + /// + /// A multi-line + /// description + pub static timing_distribution_metric: Lazy<TimingDistributionMetric> = Lazy::new(|| { + TimingDistributionMetric::new(12.into(), CommonMetricData { + name: "timing_distribution_metric".into(), + category: "test".into(), + send_in_pings: vec!["metrics".into()], + lifetime: Lifetime::Application, + disabled: false, + ..Default::default() + }, TimeUnit::Nanosecond) + }); + + #[allow(non_upper_case_globals)] + /// generated from test.memory_distribution_metric + /// + /// A multi-line + /// description + pub static memory_distribution_metric: Lazy<MemoryDistributionMetric> = Lazy::new(|| { + MemoryDistributionMetric::new(13.into(), CommonMetricData { + name: "memory_distribution_metric".into(), + category: "test".into(), + send_in_pings: vec!["metrics".into()], + lifetime: Lifetime::Application, + disabled: false, + ..Default::default() + }, MemoryUnit::Kilobyte) + }); + + #[allow(non_upper_case_globals)] + /// generated from test.custom_distribution_metric + /// + /// A multi-line + /// description + pub static custom_distribution_metric: Lazy<CustomDistributionMetric> = Lazy::new(|| { + CustomDistributionMetric::new(14.into(), CommonMetricData { + name: "custom_distribution_metric".into(), + category: "test".into(), + send_in_pings: vec!["metrics".into()], + lifetime: Lifetime::Application, + disabled: false, + ..Default::default() + }, 0, 100, 100, HistogramType::Linear) + }); + +} +pub mod test_nested { + use crate::private::*; + use glean::CommonMetricData; + #[allow(unused_imports)] // HistogramType might be unusued, let's avoid warnings + use glean::HistogramType; + use once_cell::sync::Lazy; + + #[allow(non_upper_case_globals)] + /// generated from test.nested.uuid_metric + /// + /// A multi-line + /// description + pub static uuid_metric: Lazy<UuidMetric> = Lazy::new(|| { + UuidMetric::new(15.into(), CommonMetricData { + name: "uuid_metric".into(), + category: "test.nested".into(), + send_in_pings: vec!["metrics".into()], + lifetime: Lifetime::Application, + disabled: false, + ..Default::default() + }) + }); + + #[allow(non_upper_case_globals)] + /// generated from test.nested.datetime_metric + /// + /// A multi-line + /// description + pub static datetime_metric: Lazy<DatetimeMetric> = Lazy::new(|| { + DatetimeMetric::new(16.into(), CommonMetricData { + name: "datetime_metric".into(), + category: "test.nested".into(), + send_in_pings: vec!["metrics".into()], + lifetime: Lifetime::Application, + disabled: false, + ..Default::default() + }, TimeUnit::Millisecond) + }); + + #[allow(non_upper_case_globals)] + /// generated from test.nested.event_metric + /// + /// A multi-line + /// description + pub static event_metric: Lazy<EventMetric<NoExtraKeys>> = Lazy::new(|| { + EventMetric::new(17.into(), CommonMetricData { + name: "event_metric".into(), + category: "test.nested".into(), + send_in_pings: vec!["events".into()], + lifetime: Lifetime::Ping, + disabled: false, + ..Default::default() + }) + }); + + #[derive(Default, Debug, Clone, Hash, Eq, PartialEq)] + pub struct EventMetricWithExtraExtra { + pub an_extra_key: Option<String>, + pub another_extra_key: Option<String>, + } + + impl ExtraKeys for EventMetricWithExtraExtra { + const ALLOWED_KEYS: &'static [&'static str] = &["an_extra_key", "another_extra_key"]; + + fn into_ffi_extra(self) -> ::std::collections::HashMap<String, String> { + let mut map = ::std::collections::HashMap::new(); + self.an_extra_key.and_then(|val| map.insert("an_extra_key".into(), val.to_string())); + self.another_extra_key.and_then(|val| map.insert("another_extra_key".into(), val.to_string())); + map + } + } + #[allow(non_upper_case_globals)] + /// generated from test.nested.event_metric_with_extra + /// + /// A multi-line + /// description + pub static event_metric_with_extra: Lazy<EventMetric<EventMetricWithExtraExtra>> = Lazy::new(|| { + EventMetric::new(18.into(), CommonMetricData { + name: "event_metric_with_extra".into(), + category: "test.nested".into(), + send_in_pings: vec!["events".into()], + lifetime: Lifetime::Ping, + disabled: false, + ..Default::default() + }) + }); + + #[allow(non_upper_case_globals)] + /// generated from test.nested.quantity_metric + /// + /// A multi-line + /// description + pub static quantity_metric: Lazy<QuantityMetric> = Lazy::new(|| { + QuantityMetric::new(19.into(), CommonMetricData { + name: "quantity_metric".into(), + category: "test.nested".into(), + send_in_pings: vec!["metrics".into()], + lifetime: Lifetime::Ping, + disabled: false, + ..Default::default() + }) + }); + + #[allow(non_upper_case_globals)] + /// generated from test.nested.rate_metric + /// + /// A multi-line + /// description + pub static rate_metric: Lazy<RateMetric> = Lazy::new(|| { + RateMetric::new(20.into(), CommonMetricData { + name: "rate_metric".into(), + category: "test.nested".into(), + send_in_pings: vec!["metrics".into()], + lifetime: Lifetime::Ping, + disabled: false, + ..Default::default() + }) + }); + + #[allow(non_upper_case_globals)] + /// generated from test.nested.rate_with_external_denominator + /// + /// A multi-line + /// description + pub static rate_with_external_denominator: Lazy<NumeratorMetric> = Lazy::new(|| { + NumeratorMetric::new(21.into(), CommonMetricData { + name: "rate_with_external_denominator".into(), + category: "test.nested".into(), + send_in_pings: vec!["metrics".into()], + lifetime: Lifetime::Ping, + disabled: false, + ..Default::default() + }) + }); + + #[allow(non_upper_case_globals)] + /// generated from test.nested.external_denominator + /// + /// A multi-line + /// description + pub static external_denominator: Lazy<DenominatorMetric> = Lazy::new(|| { + DenominatorMetric::new(22.into(), CommonMetricData { + name: "external_denominator".into(), + category: "test.nested".into(), + send_in_pings: vec!["metrics".into()], + lifetime: Lifetime::Ping, + disabled: false, + ..Default::default() + }, vec![CommonMetricData {name: "rate_with_external_denominator".into(), category: "test.nested".into(), send_in_pings: vec!["metrics".into()], lifetime: Lifetime::Ping, disabled: false, ..Default::default()}]) + }); + +} + +#[allow(dead_code)] +pub(crate) mod __glean_metric_maps { + use std::collections::HashMap; + + use crate::metrics::extra_keys_len; + use crate::private::*; + use once_cell::sync::Lazy; + + pub static BOOLEAN_MAP: Lazy<HashMap<MetricId, &Lazy<BooleanMetric>>> = Lazy::new(|| { + let mut map = HashMap::with_capacity(1); + map.insert(1.into(), &super::test::boolean_metric); + map + }); + + pub static LABELED_BOOLEAN_MAP: Lazy<HashMap<MetricId, &Lazy<LabeledMetric<LabeledBooleanMetric>>>> = Lazy::new(|| { + let mut map = HashMap::with_capacity(2); + map.insert(2.into(), &super::test::labeled_boolean_metric); + map.insert(3.into(), &super::test::labeled_boolean_metric_labels); + map + }); + + pub static COUNTER_MAP: Lazy<HashMap<MetricId, &Lazy<CounterMetric>>> = Lazy::new(|| { + let mut map = HashMap::with_capacity(1); + map.insert(4.into(), &super::test::counter_metric); + map + }); + + pub static LABELED_COUNTER_MAP: Lazy<HashMap<MetricId, &Lazy<LabeledMetric<LabeledCounterMetric>>>> = Lazy::new(|| { + let mut map = HashMap::with_capacity(2); + map.insert(5.into(), &super::test::labeled_counter_metric); + map.insert(6.into(), &super::test::labeled_counter_metric_labels); + map + }); + + pub static STRING_MAP: Lazy<HashMap<MetricId, &Lazy<StringMetric>>> = Lazy::new(|| { + let mut map = HashMap::with_capacity(1); + map.insert(7.into(), &super::test::string_metric); + map + }); + + pub static LABELED_STRING_MAP: Lazy<HashMap<MetricId, &Lazy<LabeledMetric<LabeledStringMetric>>>> = Lazy::new(|| { + let mut map = HashMap::with_capacity(2); + map.insert(8.into(), &super::test::labeled_string_metric); + map.insert(9.into(), &super::test::labeled_string_metric_labels); + map + }); + + pub static STRING_LIST_MAP: Lazy<HashMap<MetricId, &Lazy<StringListMetric>>> = Lazy::new(|| { + let mut map = HashMap::with_capacity(1); + map.insert(10.into(), &super::test::string_list_metric); + map + }); + + pub static TIMESPAN_MAP: Lazy<HashMap<MetricId, &Lazy<TimespanMetric>>> = Lazy::new(|| { + let mut map = HashMap::with_capacity(1); + map.insert(11.into(), &super::test::timespan_metric); + map + }); + + pub static TIMING_DISTRIBUTION_MAP: Lazy<HashMap<MetricId, &Lazy<TimingDistributionMetric>>> = Lazy::new(|| { + let mut map = HashMap::with_capacity(1); + map.insert(12.into(), &super::test::timing_distribution_metric); + map + }); + + pub static MEMORY_DISTRIBUTION_MAP: Lazy<HashMap<MetricId, &Lazy<MemoryDistributionMetric>>> = Lazy::new(|| { + let mut map = HashMap::with_capacity(1); + map.insert(13.into(), &super::test::memory_distribution_metric); + map + }); + + pub static CUSTOM_DISTRIBUTION_MAP: Lazy<HashMap<MetricId, &Lazy<CustomDistributionMetric>>> = Lazy::new(|| { + let mut map = HashMap::with_capacity(1); + map.insert(14.into(), &super::test::custom_distribution_metric); + map + }); + + pub static UUID_MAP: Lazy<HashMap<MetricId, &Lazy<UuidMetric>>> = Lazy::new(|| { + let mut map = HashMap::with_capacity(1); + map.insert(15.into(), &super::test_nested::uuid_metric); + map + }); + + pub static DATETIME_MAP: Lazy<HashMap<MetricId, &Lazy<DatetimeMetric>>> = Lazy::new(|| { + let mut map = HashMap::with_capacity(1); + map.insert(16.into(), &super::test_nested::datetime_metric); + map + }); + + pub static QUANTITY_MAP: Lazy<HashMap<MetricId, &Lazy<QuantityMetric>>> = Lazy::new(|| { + let mut map = HashMap::with_capacity(1); + map.insert(19.into(), &super::test_nested::quantity_metric); + map + }); + + pub static RATE_MAP: Lazy<HashMap<MetricId, &Lazy<RateMetric>>> = Lazy::new(|| { + let mut map = HashMap::with_capacity(1); + map.insert(20.into(), &super::test_nested::rate_metric); + map + }); + + pub static NUMERATOR_MAP: Lazy<HashMap<MetricId, &Lazy<NumeratorMetric>>> = Lazy::new(|| { + let mut map = HashMap::with_capacity(1); + map.insert(21.into(), &super::test_nested::rate_with_external_denominator); + map + }); + + pub static DENOMINATOR_MAP: Lazy<HashMap<MetricId, &Lazy<DenominatorMetric>>> = Lazy::new(|| { + let mut map = HashMap::with_capacity(1); + map.insert(22.into(), &super::test_nested::external_denominator); + map + }); + + + /// Wrapper to record an event based on its metric ID. + /// + /// # Arguments + /// + /// * `metric_id` - The metric's ID to look up + /// * `extra` - An map of (extra key id, string) pairs. + /// The map will be decoded into the appropriate `ExtraKeys` type. + /// # Returns + /// + /// Returns `Ok(())` if the event was found and `record` was called with the given `extra`, + /// or an `EventRecordingError::InvalidId` if no event by that ID exists + /// or an `EventRecordingError::InvalidExtraKey` if the `extra` map could not be deserialized. + pub(crate) fn record_event_by_id(metric_id: u32, extra: HashMap<String, String>) -> Result<(), EventRecordingError> { + match metric_id { + 17 => { + assert!( + extra_keys_len(&super::test_nested::event_metric) != 0 || extra.is_empty(), + "No extra keys allowed, but some were passed" + ); + + super::test_nested::event_metric.record_raw(extra); + Ok(()) + } + 18 => { + assert!( + extra_keys_len(&super::test_nested::event_metric_with_extra) != 0 || extra.is_empty(), + "No extra keys allowed, but some were passed" + ); + + super::test_nested::event_metric_with_extra.record_raw(extra); + Ok(()) + } + _ => Err(EventRecordingError::InvalidId), + } + } + + /// Wrapper to record an event based on its metric ID, with a provided timestamp. + /// + /// # Arguments + /// + /// * `metric_id` - The metric's ID to look up + /// * `timestamp` - The time at which this event was recorded. + /// * `extra` - An map of (extra key id, string) pairs. + /// The map will be decoded into the appropriate `ExtraKeys` type. + /// # Returns + /// + /// Returns `Ok(())` if the event was found and `record` was called with the given `extra`, + /// or an `EventRecordingError::InvalidId` if no event by that ID exists + /// or an `EventRecordingError::InvalidExtraKey` if the event doesn't take extra pairs, + /// but some are passed in. + pub(crate) fn record_event_by_id_with_time(metric_id: MetricId, timestamp: u64, extra: HashMap<String, String>) -> Result<(), EventRecordingError> { + match metric_id { + MetricId(17) => { + if extra_keys_len(&super::test_nested::event_metric) == 0 && !extra.is_empty() { + return Err(EventRecordingError::InvalidExtraKey); + } + + super::test_nested::event_metric.record_with_time(timestamp, extra); + Ok(()) + } + MetricId(18) => { + if extra_keys_len(&super::test_nested::event_metric_with_extra) == 0 && !extra.is_empty() { + return Err(EventRecordingError::InvalidExtraKey); + } + + super::test_nested::event_metric_with_extra.record_with_time(timestamp, extra); + Ok(()) + } + _ => Err(EventRecordingError::InvalidId), + } + } + + /// Wrapper to record an event based on its metric ID. + /// + /// # Arguments + /// + /// * `metric_id` - The metric's ID to look up + /// * `extra` - An map of (string, string) pairs. + /// The map will be decoded into the appropriate `ExtraKeys` types. + /// # Returns + /// + /// Returns `Ok(())` if the event was found and `record` was called with the given `extra`, + /// or an `EventRecordingError::InvalidId` if no event by that ID exists + /// or an `EventRecordingError::InvalidExtraKey` if the `extra` map could not be deserialized. + pub(crate) fn record_event_by_id_with_strings(metric_id: u32, extra: HashMap<String, String>) -> Result<(), EventRecordingError> { + match metric_id { + 17 => { + assert!( + extra_keys_len(&super::test_nested::event_metric) != 0 || extra.is_empty(), + "No extra keys allowed, but some were passed" + ); + + super::test_nested::event_metric.record_raw(extra); + Ok(()) + } + 18 => { + assert!( + extra_keys_len(&super::test_nested::event_metric_with_extra) != 0 || extra.is_empty(), + "No extra keys allowed, but some were passed" + ); + + super::test_nested::event_metric_with_extra.record_raw(extra); + Ok(()) + } + _ => Err(EventRecordingError::InvalidId), + } + } + + /// Wrapper to get the currently stored events for event metric. + /// + /// # Arguments + /// + /// * `metric_id` - The metric's ID to look up + /// * `ping_name` - (Optional) The ping name to look into. + /// Defaults to the first value in `send_in_pings`. + /// + /// # Returns + /// + /// Returns the recorded events or `None` if nothing stored. + /// + /// # Panics + /// + /// Panics if no event by the given metric ID could be found. + pub(crate) fn event_test_get_value_wrapper(metric_id: u32, ping_name: Option<String>) -> Option<Vec<RecordedEvent>> { + match metric_id { + 17 => super::test_nested::event_metric.test_get_value(ping_name.as_deref()), + 18 => super::test_nested::event_metric_with_extra.test_get_value(ping_name.as_deref()), + _ => panic!("No event for metric id {}", metric_id), + } + } + + /// Check the provided event for errors. + /// + /// # Arguments + /// + /// * `metric_id` - The metric's ID to look up + /// * `ping_name` - (Optional) The ping name to look into. + /// Defaults to the first value in `send_in_pings`. + /// + /// # Returns + /// + /// Returns a string for the recorded error or `None`. + /// + /// # Panics + /// + /// Panics if no event by the given metric ID could be found. + #[allow(unused_variables)] + pub(crate) fn event_test_get_error(metric_id: u32) -> Option<String> { + #[cfg(feature = "with_gecko")] + match metric_id { + 17 => test_get_errors!(super::test_nested::event_metric), + 18 => test_get_errors!(super::test_nested::event_metric_with_extra), + _ => panic!("No event for metric id {}", metric_id), + } + + #[cfg(not(feature = "with_gecko"))] + { + return None; + } + } + + pub(crate) mod submetric_maps { + use std::sync::{ + atomic::AtomicU32, + RwLock, + }; + use super::*; + + pub(crate) const SUBMETRIC_BIT: u32 = 25; + pub(crate) static NEXT_LABELED_SUBMETRIC_ID: AtomicU32 = AtomicU32::new((1 << SUBMETRIC_BIT) + 1); + pub(crate) static LABELED_METRICS_TO_IDS: Lazy<RwLock<HashMap<(u32, String), u32>>> = Lazy::new(|| + RwLock::new(HashMap::new()) + ); + + pub static BOOLEAN_MAP: Lazy<RwLock<HashMap<MetricId, LabeledBooleanMetric>>> = Lazy::new(|| + RwLock::new(HashMap::new()) + ); + pub static COUNTER_MAP: Lazy<RwLock<HashMap<MetricId, LabeledCounterMetric>>> = Lazy::new(|| + RwLock::new(HashMap::new()) + ); + pub static STRING_MAP: Lazy<RwLock<HashMap<MetricId, LabeledStringMetric>>> = Lazy::new(|| + RwLock::new(HashMap::new()) + ); + } +} + diff --git a/toolkit/components/glean/tests/pytest/metrics_test_output_cpp b/toolkit/components/glean/tests/pytest/metrics_test_output_cpp new file mode 100644 index 0000000000..c9a9cbc619 --- /dev/null +++ b/toolkit/components/glean/tests/pytest/metrics_test_output_cpp @@ -0,0 +1,244 @@ +// -*- mode: C++ -*- + +// AUTOGENERATED BY glean_parser. DO NOT EDIT. + +/* 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 http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_Metrics_h +#define mozilla_Metrics_h + +#include "mozilla/glean/bindings/MetricTypes.h" +#include "mozilla/Tuple.h" +#include "mozilla/Maybe.h" +#include "nsTArray.h" +#include "nsPrintfCString.h" + +namespace mozilla::glean { +struct NoExtraKeys; + +namespace test { + /** + * generated from test.boolean_metric + */ + /** + * A multi-line + * description + */ + constexpr impl::BooleanMetric boolean_metric(1); + + /** + * generated from test.labeled_boolean_metric + */ + /** + * A multi-line + * description + */ + constexpr impl::Labeled<impl::BooleanMetric> labeled_boolean_metric(2); + + /** + * generated from test.labeled_boolean_metric_labels + */ + /** + * A multi-line + * description + */ + constexpr impl::Labeled<impl::BooleanMetric> labeled_boolean_metric_labels(3); + + /** + * generated from test.counter_metric + */ + /** + * A multi-line + * description + */ + constexpr impl::CounterMetric counter_metric(4); + + /** + * generated from test.labeled_counter_metric + */ + /** + * A multi-line + * description + */ + constexpr impl::Labeled<impl::CounterMetric> labeled_counter_metric(5); + + /** + * generated from test.labeled_counter_metric_labels + */ + /** + * A multi-line + * description + */ + constexpr impl::Labeled<impl::CounterMetric> labeled_counter_metric_labels(6); + + /** + * generated from test.string_metric + */ + /** + * A multi-line + * description + */ + constexpr impl::StringMetric string_metric(7); + + /** + * generated from test.labeled_string_metric + */ + /** + * A multi-line + * description + */ + constexpr impl::Labeled<impl::StringMetric> labeled_string_metric(8); + + /** + * generated from test.labeled_string_metric_labels + */ + /** + * A multi-line + * description + */ + constexpr impl::Labeled<impl::StringMetric> labeled_string_metric_labels(9); + + /** + * generated from test.string_list_metric + */ + /** + * A multi-line + * description + */ + constexpr impl::StringListMetric string_list_metric(10); + + /** + * generated from test.timespan_metric + */ + /** + * A multi-line + * description + */ + constexpr impl::TimespanMetric timespan_metric(11); + + /** + * generated from test.timing_distribution_metric + */ + /** + * A multi-line + * description + */ + constexpr impl::TimingDistributionMetric timing_distribution_metric(12); + + /** + * generated from test.memory_distribution_metric + */ + /** + * A multi-line + * description + */ + constexpr impl::MemoryDistributionMetric memory_distribution_metric(13); + + /** + * generated from test.custom_distribution_metric + */ + /** + * A multi-line + * description + */ + constexpr impl::CustomDistributionMetric custom_distribution_metric(14); + +} +namespace test_nested { + /** + * generated from test.nested.uuid_metric + */ + /** + * A multi-line + * description + */ + constexpr impl::UuidMetric uuid_metric(15); + + /** + * generated from test.nested.datetime_metric + */ + /** + * A multi-line + * description + */ + constexpr impl::DatetimeMetric datetime_metric(16); + + /** + * generated from test.nested.event_metric + */ + /** + * A multi-line + * description + */ + constexpr impl::EventMetric<NoExtraKeys> event_metric(17); + + /** + * generated from test.nested.event_metric_with_extra + */ + struct EventMetricWithExtraExtra { + mozilla::Maybe<nsCString> anExtraKey; + mozilla::Maybe<nsCString> anotherExtraKey; + + Tuple<nsTArray<nsCString>, nsTArray<nsCString>> ToFfiExtra() const { + nsTArray<nsCString> extraKeys; + nsTArray<nsCString> extraValues; + if (anExtraKey) { + extraKeys.AppendElement()->AssignASCII("an_extra_key"); + extraValues.EmplaceBack(anExtraKey.value()); + } + if (anotherExtraKey) { + extraKeys.AppendElement()->AssignASCII("another_extra_key"); + extraValues.EmplaceBack(anotherExtraKey.value()); + } + return MakeTuple(std::move(extraKeys), std::move(extraValues)); + } + }; + /** + * A multi-line + * description + */ + constexpr impl::EventMetric<EventMetricWithExtraExtra> event_metric_with_extra(18); + + /** + * generated from test.nested.quantity_metric + */ + /** + * A multi-line + * description + */ + constexpr impl::QuantityMetric quantity_metric(19); + + /** + * generated from test.nested.rate_metric + */ + /** + * A multi-line + * description + */ + constexpr impl::RateMetric rate_metric(20); + + /** + * generated from test.nested.rate_with_external_denominator + */ + /** + * A multi-line + * description + */ + constexpr impl::NumeratorMetric rate_with_external_denominator(21); + + /** + * generated from test.nested.external_denominator + */ + /** + * A multi-line + * description + */ + constexpr impl::DenominatorMetric external_denominator(22); + +} + +} // namespace mozilla::glean + +#endif // mozilla_Metrics_h diff --git a/toolkit/components/glean/tests/pytest/metrics_test_output_js b/toolkit/components/glean/tests/pytest/metrics_test_output_js new file mode 100644 index 0000000000..3c682d5ee7 --- /dev/null +++ b/toolkit/components/glean/tests/pytest/metrics_test_output_js @@ -0,0 +1,342 @@ +// -*- mode: C++ -*- + +// AUTOGENERATED BY glean_parser. DO NOT EDIT. + +/* 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 http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_GleanJSMetricsLookup_h +#define mozilla_GleanJSMetricsLookup_h + +#include "mozilla/PerfectHash.h" +#include "mozilla/Maybe.h" +#include "mozilla/glean/bindings/MetricTypes.h" +#include "mozilla/glean/fog_ffi_generated.h" + +#define GLEAN_INDEX_BITS (32) +#define GLEAN_TYPE_BITS (5) +#define GLEAN_ID_BITS (27) +#define GLEAN_TYPE_ID(id) ((id) >> GLEAN_ID_BITS) +#define GLEAN_METRIC_ID(id) ((id) & ((1ULL << GLEAN_ID_BITS) - 1)) +#define GLEAN_OFFSET(entry) (entry & ((1ULL << GLEAN_INDEX_BITS) - 1)) + +namespace mozilla::glean { + +// The category lookup table's entry type +using category_entry_t = uint32_t; +// The metric lookup table's entry type +// This is a bitpacked type with 32 bits available to index into +// the string table, 5 bits available to signify the metric type, +// and the remaining 27 bits devoted to 2 "signal" +// bits to signify important characteristics (metric's a labeled metric's +// submetric, metric's been registered at runtime) and 25 bits +// for built-in metric ids. +// Gives room for 33554432 of each combination of +// characteristics (which hopefully will prove to be enough). +using metric_entry_t = uint64_t; + +static_assert(GLEAN_INDEX_BITS + GLEAN_TYPE_BITS + GLEAN_ID_BITS == sizeof(metric_entry_t) * 8, "Index, Type, and ID bits need to fit into a metric_entry_t"); +static_assert(GLEAN_TYPE_BITS + GLEAN_ID_BITS <= sizeof(uint32_t) * 8, "Metric Types and IDs need to fit into at most 32 bits"); +static_assert(2 < UINT32_MAX, "Too many metric categories generated."); +static_assert(22 < 33554432, "Too many metrics generated. Need room for 2 signal bits."); +static_assert(18 < 32, "Too many different metric types."); + +static already_AddRefed<nsISupports> NewMetricFromId(uint32_t id) { + uint32_t typeId = GLEAN_TYPE_ID(id); + uint32_t metricId = GLEAN_METRIC_ID(id); + + switch (typeId) { + case 1: /* boolean */ + { + return MakeAndAddRef<GleanBoolean>(metricId); + } + case 2: /* labeled_boolean */ + { + return MakeAndAddRef<GleanLabeled>(metricId, 2); + } + case 3: /* counter */ + { + return MakeAndAddRef<GleanCounter>(metricId); + } + case 4: /* labeled_counter */ + { + return MakeAndAddRef<GleanLabeled>(metricId, 4); + } + case 5: /* string */ + { + return MakeAndAddRef<GleanString>(metricId); + } + case 6: /* labeled_string */ + { + return MakeAndAddRef<GleanLabeled>(metricId, 6); + } + case 7: /* string_list */ + { + return MakeAndAddRef<GleanStringList>(metricId); + } + case 8: /* timespan */ + { + return MakeAndAddRef<GleanTimespan>(metricId); + } + case 9: /* timing_distribution */ + { + return MakeAndAddRef<GleanTimingDistribution>(metricId); + } + case 10: /* memory_distribution */ + { + return MakeAndAddRef<GleanMemoryDistribution>(metricId); + } + case 11: /* custom_distribution */ + { + return MakeAndAddRef<GleanCustomDistribution>(metricId); + } + case 12: /* uuid */ + { + return MakeAndAddRef<GleanUuid>(metricId); + } + case 13: /* datetime */ + { + return MakeAndAddRef<GleanDatetime>(metricId); + } + case 14: /* event */ + { + return MakeAndAddRef<GleanEvent>(metricId); + } + case 15: /* quantity */ + { + return MakeAndAddRef<GleanQuantity>(metricId); + } + case 16: /* rate */ + { + return MakeAndAddRef<GleanRate>(metricId); + } + case 17: /* numerator */ + { + return MakeAndAddRef<GleanNumerator>(metricId); + } + case 18: /* denominator */ + { + return MakeAndAddRef<GleanDenominator>(metricId); + } + default: + MOZ_ASSERT_UNREACHABLE("Invalid type ID reached when trying to instantiate a new metric"); + return nullptr; + } +} + +/** + * Create a submetric instance for a labeled metric of the provided type and id for the given label. + * Assigns or retrieves an id for the submetric from the SDK. + * + * @param aParentTypeId - The type of the parent labeled metric identified as a number generated during codegen. + * Only used to identify which X of LabeledX you are so that X can be created here. + * @param aParentMetricId - The metric id for the parent labeled metric. + * @param aLabel - The label for the submetric. Might not adhere to the SDK label format. + * @param aSubmetricId - an outparam which is assigned the submetric's SDK-generated submetric id. + * Used only by GIFFT. + */ +static already_AddRefed<nsISupports> NewSubMetricFromIds(uint32_t aParentTypeId, uint32_t aParentMetricId, const nsACString& aLabel, uint32_t* aSubmetricId) { + switch (aParentTypeId) { + case 2: { /* labeled_boolean */ + auto id = impl::fog_labeled_boolean_get(aParentMetricId, &aLabel); + *aSubmetricId = id; + return MakeAndAddRef<GleanBoolean>(id); + } + case 4: { /* labeled_counter */ + auto id = impl::fog_labeled_counter_get(aParentMetricId, &aLabel); + *aSubmetricId = id; + return MakeAndAddRef<GleanCounter>(id); + } + case 6: { /* labeled_string */ + auto id = impl::fog_labeled_string_get(aParentMetricId, &aLabel); + *aSubmetricId = id; + return MakeAndAddRef<GleanString>(id); + } + default: { + MOZ_ASSERT_UNREACHABLE("Invalid type ID for submetric."); + return nullptr; + } + } +} + +static Maybe<uint32_t> category_result_check(const nsACString& aKey, category_entry_t entry); +static Maybe<uint32_t> metric_result_check(const nsACString& aKey, metric_entry_t entry); + +#if defined(_MSC_VER) && !defined(__clang__) +const char gCategoryStringTable[] = { +#else +constexpr char gCategoryStringTable[] = { +#endif + /* 0 - "test" */ 't', 'e', 's', 't', '\0', + /* 5 - "testNested" */ 't', 'e', 's', 't', 'N', 'e', 's', 't', 'e', 'd', '\0', +}; + + +static_assert(sizeof(gCategoryStringTable) < UINT32_MAX, "Category string table is too large."); + +const category_entry_t sCategoryByNameLookupEntries[] = { + 5ul, + 0ul +}; + + + +static Maybe<uint32_t> +CategoryByNameLookup(const nsACString& aKey) +{ + static const uint8_t BASES[] = { + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + }; + + + const char* bytes = aKey.BeginReading(); + size_t length = aKey.Length(); + auto& entry = mozilla::perfecthash::Lookup(bytes, length, BASES, + sCategoryByNameLookupEntries); + return category_result_check(aKey, entry); +} + + +#if defined(_MSC_VER) && !defined(__clang__) +const char gMetricStringTable[] = { +#else +constexpr char gMetricStringTable[] = { +#endif + /* 0 - "test.booleanMetric" */ 't', 'e', 's', 't', '.', 'b', 'o', 'o', 'l', 'e', 'a', 'n', 'M', 'e', 't', 'r', 'i', 'c', '\0', + /* 19 - "test.labeledBooleanMetric" */ 't', 'e', 's', 't', '.', 'l', 'a', 'b', 'e', 'l', 'e', 'd', 'B', 'o', 'o', 'l', 'e', 'a', 'n', 'M', 'e', 't', 'r', 'i', 'c', '\0', + /* 45 - "test.labeledBooleanMetricLabels" */ 't', 'e', 's', 't', '.', 'l', 'a', 'b', 'e', 'l', 'e', 'd', 'B', 'o', 'o', 'l', 'e', 'a', 'n', 'M', 'e', 't', 'r', 'i', 'c', 'L', 'a', 'b', 'e', 'l', 's', '\0', + /* 77 - "test.counterMetric" */ 't', 'e', 's', 't', '.', 'c', 'o', 'u', 'n', 't', 'e', 'r', 'M', 'e', 't', 'r', 'i', 'c', '\0', + /* 96 - "test.labeledCounterMetric" */ 't', 'e', 's', 't', '.', 'l', 'a', 'b', 'e', 'l', 'e', 'd', 'C', 'o', 'u', 'n', 't', 'e', 'r', 'M', 'e', 't', 'r', 'i', 'c', '\0', + /* 122 - "test.labeledCounterMetricLabels" */ 't', 'e', 's', 't', '.', 'l', 'a', 'b', 'e', 'l', 'e', 'd', 'C', 'o', 'u', 'n', 't', 'e', 'r', 'M', 'e', 't', 'r', 'i', 'c', 'L', 'a', 'b', 'e', 'l', 's', '\0', + /* 154 - "test.stringMetric" */ 't', 'e', 's', 't', '.', 's', 't', 'r', 'i', 'n', 'g', 'M', 'e', 't', 'r', 'i', 'c', '\0', + /* 172 - "test.labeledStringMetric" */ 't', 'e', 's', 't', '.', 'l', 'a', 'b', 'e', 'l', 'e', 'd', 'S', 't', 'r', 'i', 'n', 'g', 'M', 'e', 't', 'r', 'i', 'c', '\0', + /* 197 - "test.labeledStringMetricLabels" */ 't', 'e', 's', 't', '.', 'l', 'a', 'b', 'e', 'l', 'e', 'd', 'S', 't', 'r', 'i', 'n', 'g', 'M', 'e', 't', 'r', 'i', 'c', 'L', 'a', 'b', 'e', 'l', 's', '\0', + /* 228 - "test.stringListMetric" */ 't', 'e', 's', 't', '.', 's', 't', 'r', 'i', 'n', 'g', 'L', 'i', 's', 't', 'M', 'e', 't', 'r', 'i', 'c', '\0', + /* 250 - "test.timespanMetric" */ 't', 'e', 's', 't', '.', 't', 'i', 'm', 'e', 's', 'p', 'a', 'n', 'M', 'e', 't', 'r', 'i', 'c', '\0', + /* 270 - "test.timingDistributionMetric" */ 't', 'e', 's', 't', '.', 't', 'i', 'm', 'i', 'n', 'g', 'D', 'i', 's', 't', 'r', 'i', 'b', 'u', 't', 'i', 'o', 'n', 'M', 'e', 't', 'r', 'i', 'c', '\0', + /* 300 - "test.memoryDistributionMetric" */ 't', 'e', 's', 't', '.', 'm', 'e', 'm', 'o', 'r', 'y', 'D', 'i', 's', 't', 'r', 'i', 'b', 'u', 't', 'i', 'o', 'n', 'M', 'e', 't', 'r', 'i', 'c', '\0', + /* 330 - "test.customDistributionMetric" */ 't', 'e', 's', 't', '.', 'c', 'u', 's', 't', 'o', 'm', 'D', 'i', 's', 't', 'r', 'i', 'b', 'u', 't', 'i', 'o', 'n', 'M', 'e', 't', 'r', 'i', 'c', '\0', + /* 360 - "testNested.uuidMetric" */ 't', 'e', 's', 't', 'N', 'e', 's', 't', 'e', 'd', '.', 'u', 'u', 'i', 'd', 'M', 'e', 't', 'r', 'i', 'c', '\0', + /* 382 - "testNested.datetimeMetric" */ 't', 'e', 's', 't', 'N', 'e', 's', 't', 'e', 'd', '.', 'd', 'a', 't', 'e', 't', 'i', 'm', 'e', 'M', 'e', 't', 'r', 'i', 'c', '\0', + /* 408 - "testNested.eventMetric" */ 't', 'e', 's', 't', 'N', 'e', 's', 't', 'e', 'd', '.', 'e', 'v', 'e', 'n', 't', 'M', 'e', 't', 'r', 'i', 'c', '\0', + /* 431 - "testNested.eventMetricWithExtra" */ 't', 'e', 's', 't', 'N', 'e', 's', 't', 'e', 'd', '.', 'e', 'v', 'e', 'n', 't', 'M', 'e', 't', 'r', 'i', 'c', 'W', 'i', 't', 'h', 'E', 'x', 't', 'r', 'a', '\0', + /* 463 - "testNested.quantityMetric" */ 't', 'e', 's', 't', 'N', 'e', 's', 't', 'e', 'd', '.', 'q', 'u', 'a', 'n', 't', 'i', 't', 'y', 'M', 'e', 't', 'r', 'i', 'c', '\0', + /* 489 - "testNested.rateMetric" */ 't', 'e', 's', 't', 'N', 'e', 's', 't', 'e', 'd', '.', 'r', 'a', 't', 'e', 'M', 'e', 't', 'r', 'i', 'c', '\0', + /* 511 - "testNested.rateWithExternalDenominator" */ 't', 'e', 's', 't', 'N', 'e', 's', 't', 'e', 'd', '.', 'r', 'a', 't', 'e', 'W', 'i', 't', 'h', 'E', 'x', 't', 'e', 'r', 'n', 'a', 'l', 'D', 'e', 'n', 'o', 'm', 'i', 'n', 'a', 't', 'o', 'r', '\0', + /* 550 - "testNested.externalDenominator" */ 't', 'e', 's', 't', 'N', 'e', 's', 't', 'e', 'd', '.', 'e', 'x', 't', 'e', 'r', 'n', 'a', 'l', 'D', 'e', 'n', 'o', 'm', 'i', 'n', 'a', 't', 'o', 'r', '\0', +}; + + +static_assert(sizeof(gMetricStringTable) < 4294967296, "Metric string table is too large."); + +const metric_entry_t sMetricByNameLookupEntries[] = { + 6341068335467200842ull, + 1729382274090139725ull, + 1152921513196781587ull, + 2305843034983497850ull, + 5764607578868810028ull, + 8070450605262373272ull, + 3458764548180279468ull, + 10376293635950903846ull, + 8646911366155731407ull, + 9799832879352513023ull, + 2882303791581888666ull, + 1152921517491748909ull, + 4035225309073637604ull, + 6917529092065591656ull, + 4611686065672028410ull, + 7493989848663982462ull, + 8070450609557340591ull, + 3458764552475246789ull, + 5188146822270419214ull, + 576460756598390784ull, + 2305843030688530528ull, + 9223372122754122217ull +}; + + + +static Maybe<uint32_t> +MetricByNameLookup(const nsACString& aKey) +{ + static const uint8_t BASES[] = { + 0, 0, 0, 1, 0, 2, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, + 1, 1, 0, 0, 1, 0, 0, 1, 0, 4, 0, 0, 1, 0, 1, 0, + 2, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 3, 0, 6, 0, 0, + 0, 0, 10, 0, 0, 1, 0, 0, 0, 0, 4, 0, 0, 0, 0, 17, + }; + + + const char* bytes = aKey.BeginReading(); + size_t length = aKey.Length(); + auto& entry = mozilla::perfecthash::Lookup(bytes, length, BASES, + sMetricByNameLookupEntries); + return metric_result_check(aKey, entry); +} + + +/** + * Get a category's name from the string table. + */ +static const char* GetCategoryName(category_entry_t entry) { + MOZ_ASSERT(entry < sizeof(gCategoryStringTable), "Entry identifier offset larger than string table"); + return &gCategoryStringTable[entry]; +} + +/** + * Get a metric's identifier from the string table. + */ +static const char* GetMetricIdentifier(metric_entry_t entry) { + uint32_t offset = GLEAN_OFFSET(entry); + MOZ_ASSERT(offset < sizeof(gMetricStringTable), "Entry identifier offset larger than string table"); + return &gMetricStringTable[offset]; +} + +/** + * Check that the found entry is pointing to the right key + * and return it. + * Or return `Nothing()` if the entry was not found. + */ +static Maybe<uint32_t> category_result_check(const nsACString& aKey, category_entry_t entry) { + if (MOZ_UNLIKELY(entry > sizeof(gCategoryStringTable))) { + return Nothing(); + } + if (aKey.EqualsASCII(gCategoryStringTable + entry)) { + return Some(entry); + } + return Nothing(); +} + +/** + * Check if the found entry index is pointing to the right key + * and return the corresponding metric ID. + * Or return `Nothing()` if the entry was not found. + */ +static Maybe<uint32_t> metric_result_check(const nsACString& aKey, uint64_t entry) { + uint32_t metricId = entry >> GLEAN_INDEX_BITS; + uint32_t offset = GLEAN_OFFSET(entry); + + if (offset > sizeof(gMetricStringTable)) { + return Nothing(); + } + + if (aKey.EqualsASCII(gMetricStringTable + offset)) { + return Some(metricId); + } + + return Nothing(); +} + + +#undef GLEAN_INDEX_BITS +#undef GLEAN_ID_BITS +#undef GLEAN_TYPE_ID +#undef GLEAN_METRIC_ID +#undef GLEAN_OFFSET + +} // namespace mozilla::glean +#endif // mozilla_GleanJSMetricsLookup_h diff --git a/toolkit/components/glean/tests/pytest/pings_test.yaml b/toolkit/components/glean/tests/pytest/pings_test.yaml new file mode 100644 index 0000000000..efa6ba9e0a --- /dev/null +++ b/toolkit/components/glean/tests/pytest/pings_test.yaml @@ -0,0 +1,116 @@ +# 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 http://mozilla.org/MPL/2.0/. + +# This file defines the built-in pings that are recorded by the Glean SDK. They +# are automatically converted to Kotlin code at build time using the +# `glean_parser` PyPI package. + +--- +$schema: moz://mozilla.org/schemas/glean/pings/1-0-0 + +not-baseline: + description: > + This ping is intended to provide metrics that are managed by the library + itself, and not explicitly set by the application or included in the + application's `metrics.yaml` file. + The `baseline` ping is automatically sent when the application is moved to + the background. + include_client_id: true + bugs: + - https://bugzilla.mozilla.org/1512938 + - https://bugzilla.mozilla.org/1599877 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1512938#c3 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1599877#c25 + notification_emails: + - glean-team@mozilla.com + reasons: + dirty_startup: | + The ping was submitted at startup, because the application process was + killed before the Glean SDK had the chance to generate this ping, when + going to background, in the last session. + + *Note*: this ping will not contain the `glean.baseline.duration` metric. + background: | + The ping was submitted before going to background. + foreground: | + The ping was submitted when the application went to foreground, which + includes when the application starts. + + *Note*: this ping will not contain the `glean.baseline.duration` metric. + +not-metrics: + description: > + The `metrics` ping is intended for all of the metrics that are explicitly + set by the application or are included in the application's `metrics.yaml` + file (except events). + The reported data is tied to the ping's *measurement window*, which is the + time between the collection of two `metrics` ping. Ideally, this window is + expected to be about 24 hours, given that the collection is scheduled daily + at 4AM. Data in the `ping_info` section of the ping can be used to infer the + length of this window. + include_client_id: true + bugs: + - https://bugzilla.mozilla.org/1512938 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1512938#c3 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1557048#c13 + notification_emails: + - glean-team@mozilla.com + reasons: + overdue: | + The last ping wasn't submitted on the current calendar day, but it's after + 4am, so this ping submitted immediately + today: | + The last ping wasn't submitted on the current calendar day, but it is + still before 4am, so schedule to send this ping on the current calendar + day at 4am. + tomorrow: | + The last ping was already submitted on the current calendar day, so + schedule this ping for the next calendar day at 4am. + upgrade: | + This ping was submitted at startup because the application was just + upgraded. + reschedule: | + A ping was just submitted. This ping was rescheduled for the next calendar + day at 4am. + +not-events: + description: > + The events ping's purpose is to transport all of the event metric + information. The `events` ping is automatically sent when the application is + moved to the background. + include_client_id: true + bugs: + - https://bugzilla.mozilla.org/1512938 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1512938#c3 + notification_emails: + - glean-team@mozilla.com + reasons: + startup: | + The ping was submitted at startup. The events ping is always sent if there + are any pending events at startup, because event timestamps can not be + mixed across runs of the application. + background: | + The ping was submitted before going to background. + max_capacity: | + The maximum number of events was reached (default 500 events). + +not-deletion-request: + description: > + This ping is submitted when a user opts out of + sending technical and interaction data to Mozilla. + This ping is intended to communicate to the Data Pipeline + that the user wishes to have their reported Telemetry data deleted. + As such it attempts to send itself at the moment the user + opts out of data collection. + include_client_id: true + send_if_empty: true + bugs: + - https://bugzilla.mozilla.org/1587095 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1587095#c6 + notification_emails: + - glean-team@mozilla.com diff --git a/toolkit/components/glean/tests/pytest/pings_test_output b/toolkit/components/glean/tests/pytest/pings_test_output new file mode 100644 index 0000000000..a6e32ddadf --- /dev/null +++ b/toolkit/components/glean/tests/pytest/pings_test_output @@ -0,0 +1,105 @@ +// -*- mode: Rust -*- + +// AUTOGENERATED BY glean_parser. DO NOT EDIT. + +/* 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 http://mozilla.org/MPL/2.0/. */ + +use crate::private::Ping; +use once_cell::sync::Lazy; + +#[allow(non_upper_case_globals)] +/// This ping is intended to provide metrics that are managed by the library +/// itself, and not explicitly set by the application or included in the +/// application's `metrics.yaml` file. The `baseline` ping is automatically sent +/// when the application is moved to the background. +pub static not_baseline: Lazy<Ping> = Lazy::new(|| { + Ping::new( + "not-baseline", + true, + false, + vec!["background".into(), "dirty_startup".into(), "foreground".into()], + ) +}); + +#[allow(non_upper_case_globals)] +/// The `metrics` ping is intended for all of the metrics that are explicitly set +/// by the application or are included in the application's `metrics.yaml` file +/// (except events). The reported data is tied to the ping's *measurement window*, +/// which is the time between the collection of two `metrics` ping. Ideally, this +/// window is expected to be about 24 hours, given that the collection is scheduled +/// daily at 4AM. Data in the `ping_info` section of the ping can be used to infer +/// the length of this window. +pub static not_metrics: Lazy<Ping> = Lazy::new(|| { + Ping::new( + "not-metrics", + true, + false, + vec!["overdue".into(), "reschedule".into(), "today".into(), "tomorrow".into(), "upgrade".into()], + ) +}); + +#[allow(non_upper_case_globals)] +/// The events ping's purpose is to transport all of the event metric information. +/// The `events` ping is automatically sent when the application is moved to the +/// background. +pub static not_events: Lazy<Ping> = Lazy::new(|| { + Ping::new( + "not-events", + true, + false, + vec!["background".into(), "max_capacity".into(), "startup".into()], + ) +}); + +#[allow(non_upper_case_globals)] +/// This ping is submitted when a user opts out of sending technical and +/// interaction data to Mozilla. This ping is intended to communicate to the Data +/// Pipeline that the user wishes to have their reported Telemetry data deleted. As +/// such it attempts to send itself at the moment the user opts out of data +/// collection. +pub static not_deletion_request: Lazy<Ping> = Lazy::new(|| { + Ping::new( + "not-deletion-request", + true, + true, + vec![], + ) +}); + + +/// Instantiate each custom ping once to trigger registration. +#[doc(hidden)] +pub fn register_pings() { + let _ = &*not_baseline; + let _ = &*not_metrics; + let _ = &*not_events; + let _ = &*not_deletion_request; +} + +#[cfg(feature = "with_gecko")] +pub(crate) fn submit_ping_by_id(id: u32, reason: Option<&str>) { + if id & (1 << crate::factory::DYNAMIC_PING_BIT) > 0 { + let map = crate::factory::__jog_metric_maps::PING_MAP + .read() + .expect("Read lock for dynamic ping map was poisoned!"); + if let Some(ping) = map.get(&id) { + ping.submit(reason); + } else { + // TODO: instrument this error. + log::error!("Cannot submit unknown dynamic ping {} by id.", id); + } + return; + } + match id { + 1 => not_baseline.submit(reason), + 2 => not_metrics.submit(reason), + 3 => not_events.submit(reason), + 4 => not_deletion_request.submit(reason), + _ => { + // TODO: instrument this error. + log::error!("Cannot submit unknown ping {} by id.", id); + } + } +} diff --git a/toolkit/components/glean/tests/pytest/pings_test_output_cpp b/toolkit/components/glean/tests/pytest/pings_test_output_cpp new file mode 100644 index 0000000000..7f9cc03947 --- /dev/null +++ b/toolkit/components/glean/tests/pytest/pings_test_output_cpp @@ -0,0 +1,62 @@ +// -*- mode: C++ -*- + +// AUTOGENERATED BY glean_parser. DO NOT EDIT. + +/* 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 http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_glean_Pings_h +#define mozilla_glean_Pings_h + +#include "mozilla/glean/bindings/Ping.h" + +namespace mozilla::glean_pings { + +/* + * Generated from not-baseline. + * + * This ping is intended to provide metrics that are managed by the library + * itself, and not explicitly set by the application or included in the + * application's `metrics.yaml` file. The `baseline` ping is automatically sent + * when the application is moved to the background. + */ +constexpr glean::impl::Ping NotBaseline(1); + +/* + * Generated from not-metrics. + * + * The `metrics` ping is intended for all of the metrics that are explicitly set + * by the application or are included in the application's `metrics.yaml` file + * (except events). The reported data is tied to the ping's *measurement window*, + * which is the time between the collection of two `metrics` ping. Ideally, this + * window is expected to be about 24 hours, given that the collection is scheduled + * daily at 4AM. Data in the `ping_info` section of the ping can be used to infer + * the length of this window. + */ +constexpr glean::impl::Ping NotMetrics(2); + +/* + * Generated from not-events. + * + * The events ping's purpose is to transport all of the event metric information. + * The `events` ping is automatically sent when the application is moved to the + * background. + */ +constexpr glean::impl::Ping NotEvents(3); + +/* + * Generated from not-deletion-request. + * + * This ping is submitted when a user opts out of sending technical and + * interaction data to Mozilla. This ping is intended to communicate to the Data + * Pipeline that the user wishes to have their reported Telemetry data deleted. As + * such it attempts to send itself at the moment the user opts out of data + * collection. + */ +constexpr glean::impl::Ping NotDeletionRequest(4); + + +} // namespace mozilla::glean_pings + +#endif // mozilla_glean_Pings_h diff --git a/toolkit/components/glean/tests/pytest/pings_test_output_js b/toolkit/components/glean/tests/pytest/pings_test_output_js new file mode 100644 index 0000000000..ecbc6ccf7c --- /dev/null +++ b/toolkit/components/glean/tests/pytest/pings_test_output_js @@ -0,0 +1,99 @@ +// -*- mode: C++ -*- + +// AUTOGENERATED BY glean_parser. DO NOT EDIT. + +/* 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 http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_GleanJSPingsLookup_h +#define mozilla_GleanJSPingsLookup_h + +#define GLEAN_PING_INDEX_BITS (16) +#define GLEAN_PING_ID(entry) ((entry) >> GLEAN_PING_INDEX_BITS) +#define GLEAN_PING_INDEX(entry) ((entry) & ((1UL << GLEAN_PING_INDEX_BITS) - 1)) + +namespace mozilla::glean { + +// Contains the ping id and the index into the ping string table. +using ping_entry_t = uint32_t; + +static Maybe<uint32_t> ping_result_check(const nsACString& aKey, ping_entry_t aEntry); + +#if defined(_MSC_VER) && !defined(__clang__) +const char gPingStringTable[] = { +#else +constexpr char gPingStringTable[] = { +#endif + /* 0 - "notBaseline" */ 'n', 'o', 't', 'B', 'a', 's', 'e', 'l', 'i', 'n', 'e', '\0', + /* 12 - "notMetrics" */ 'n', 'o', 't', 'M', 'e', 't', 'r', 'i', 'c', 's', '\0', + /* 23 - "notEvents" */ 'n', 'o', 't', 'E', 'v', 'e', 'n', 't', 's', '\0', + /* 33 - "notDeletionRequest" */ 'n', 'o', 't', 'D', 'e', 'l', 'e', 't', 'i', 'o', 'n', 'R', 'e', 'q', 'u', 'e', 's', 't', '\0', +}; + + + +const ping_entry_t sPingByNameLookupEntries[] = { + 65536, + 196631, + 262177, + 131084 +}; + + + +static Maybe<uint32_t> +PingByNameLookup(const nsACString& aKey) +{ + static const uint8_t BASES[] = { + 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + }; + + + const char* bytes = aKey.BeginReading(); + size_t length = aKey.Length(); + auto& entry = mozilla::perfecthash::Lookup(bytes, length, BASES, + sPingByNameLookupEntries); + return ping_result_check(aKey, entry); +} + + +/** + * Get a ping's name given its entry from the PHF. + */ +static const char* GetPingName(ping_entry_t aEntry) { + uint32_t idx = GLEAN_PING_INDEX(aEntry); + MOZ_ASSERT(idx < sizeof(gPingStringTable), "Ping index larger than string table"); + return &gPingStringTable[idx]; +} + +/** + * Check if the found entry is pointing at the correct ping. + * PHF can false-positive a result when the key isn't present, so we check + * for a string match. If it fails, return Nothing(). If we found it, + * return the ping's id. + */ +static Maybe<uint32_t> ping_result_check(const nsACString& aKey, ping_entry_t aEntry) { + uint32_t idx = GLEAN_PING_INDEX(aEntry); + uint32_t id = GLEAN_PING_ID(aEntry); + + if (MOZ_UNLIKELY(idx > sizeof(gPingStringTable))) { + return Nothing(); + } + + if (aKey.EqualsASCII(&gPingStringTable[idx])) { + return Some(id); + } + + return Nothing(); +} + +#undef GLEAN_PING_INDEX_BITS +#undef GLEAN_PING_ID +#undef GLEAN_PING_INDEX + +} // namespace mozilla::glean +#endif // mozilla_GleanJSPingsLookup_h diff --git a/toolkit/components/glean/tests/pytest/python.ini b/toolkit/components/glean/tests/pytest/python.ini new file mode 100644 index 0000000000..87573cf519 --- /dev/null +++ b/toolkit/components/glean/tests/pytest/python.ini @@ -0,0 +1,10 @@ +[DEFAULT] +subsuite = fog + +[test_gifft.py] +[test_glean_parser_rust.py] +[test_glean_parser_cpp.py] +[test_glean_parser_js.py] +[test_jogfile_output.py] +[test_no_expired_metrics.py] +[test_yaml_indices.py] diff --git a/toolkit/components/glean/tests/pytest/test_gifft.py b/toolkit/components/glean/tests/pytest/test_gifft.py new file mode 100644 index 0000000000..5de0640bca --- /dev/null +++ b/toolkit/components/glean/tests/pytest/test_gifft.py @@ -0,0 +1,49 @@ +# 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 http://mozilla.org/MPL/2.0/. + +import io +import sys +from os import path +from pathlib import Path + +import mozunit +from expect_helper import expect + +# Shenanigans to import the FOG glean_parser runner +FOG_ROOT_PATH = path.abspath( + path.join(path.dirname(__file__), path.pardir, path.pardir) +) +sys.path.append(path.join(FOG_ROOT_PATH, "build_scripts", "glean_parser_ext")) +import run_glean_parser + + +def test_gifft_codegen(): + """ + A regression test. Very fragile. + It generates C++ for GIFFT for metrics_test.yaml and compares it + byte-for-byte with expected output C++ files. + To generate new expected output files, set `UPDATE_EXPECT=1` when running the test suite: + + UPDATE_EXPECT=1 mach test toolkit/components/glean/pytest + """ + + options = {"allow_reserved": False} + here_path = Path(path.dirname(__file__)) + input_files = [here_path / "metrics_test.yaml"] + + all_objs, options = run_glean_parser.parse_with_options(input_files, options) + + for probe_type in ("Event", "Histogram", "Scalar"): + output_fd = io.StringIO() + cpp_fd = io.StringIO() + run_glean_parser.output_gifft_map(output_fd, probe_type, all_objs, cpp_fd) + + expect(here_path / f"gifft_output_{probe_type}", output_fd.getvalue()) + + if probe_type == "Event": + expect(here_path / "gifft_output_EventExtra", cpp_fd.getvalue()) + + +if __name__ == "__main__": + mozunit.main() diff --git a/toolkit/components/glean/tests/pytest/test_glean_parser_cpp.py b/toolkit/components/glean/tests/pytest/test_glean_parser_cpp.py new file mode 100644 index 0000000000..05fd691782 --- /dev/null +++ b/toolkit/components/glean/tests/pytest/test_glean_parser_cpp.py @@ -0,0 +1,70 @@ +# 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 http://mozilla.org/MPL/2.0/. + +import io +import sys +from os import path +from pathlib import Path + +import mozunit +from expect_helper import expect + +# Shenanigans to import the cpp outputter extension +FOG_ROOT_PATH = path.abspath( + path.join(path.dirname(__file__), path.pardir, path.pardir) +) +sys.path.append(path.join(FOG_ROOT_PATH, "build_scripts", "glean_parser_ext")) +import cpp +import run_glean_parser + + +def test_all_metric_types(): + """Honestly, this is a pretty bad test. + It generates C++ for a given test metrics.yaml and compares it byte-for-byte + with an expected output C++ file. + Expect it to be fragile. + To generate new expected output files, set `UPDATE_EXPECT=1` when running the test suite: + + UPDATE_EXPECT=1 mach test toolkit/components/glean/pytest + """ + + options = {"allow_reserved": False} + input_files = [Path(path.join(path.dirname(__file__), "metrics_test.yaml"))] + + all_objs, options = run_glean_parser.parse_with_options(input_files, options) + + output_fd = io.StringIO() + cpp.output_cpp(all_objs, output_fd, options) + + expect( + path.join(path.dirname(__file__), "metrics_test_output_cpp"), + output_fd.getvalue(), + ) + + +def test_fake_pings(): + """Another similarly-fragile test. + It generates C++ for pings_test.yaml, comparing it byte-for-byte + with an expected output C++ file `pings_test_output_cpp`. + Expect it to be fragile. + To generate new expected output files, set `UPDATE_EXPECT=1` when running the test suite: + + UPDATE_EXPECT=1 mach test toolkit/components/glean/pytest + """ + + options = {"allow_reserved": False} + input_files = [Path(path.join(path.dirname(__file__), "pings_test.yaml"))] + + all_objs, options = run_glean_parser.parse_with_options(input_files, options) + + output_fd = io.StringIO() + cpp.output_cpp(all_objs, output_fd, options) + + expect( + path.join(path.dirname(__file__), "pings_test_output_cpp"), output_fd.getvalue() + ) + + +if __name__ == "__main__": + mozunit.main() diff --git a/toolkit/components/glean/tests/pytest/test_glean_parser_js.py b/toolkit/components/glean/tests/pytest/test_glean_parser_js.py new file mode 100644 index 0000000000..81026231a6 --- /dev/null +++ b/toolkit/components/glean/tests/pytest/test_glean_parser_js.py @@ -0,0 +1,71 @@ +# 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 http://mozilla.org/MPL/2.0/. + +import io +import sys +from os import path +from pathlib import Path + +import mozunit +from expect_helper import expect + +# Shenanigans to import the js outputter extension +FOG_ROOT_PATH = path.abspath( + path.join(path.dirname(__file__), path.pardir, path.pardir) +) +sys.path.append(path.join(FOG_ROOT_PATH, "build_scripts", "glean_parser_ext")) +import run_glean_parser + +import js + + +def test_all_metric_types(): + """Honestly, this is a pretty bad test. + It generates C++ for a given test metrics.yaml and compares it byte-for-byte + with an expected output C++ file. + Expect it to be fragile. + To generate new expected output files, set `UPDATE_EXPECT=1` when running the test suite: + + UPDATE_EXPECT=1 mach test toolkit/components/glean/tests/pytest + """ + + options = {"allow_reserved": False} + input_files = [Path(path.join(path.dirname(__file__), "metrics_test.yaml"))] + + all_objs, options = run_glean_parser.parse_with_options(input_files, options) + + output_fd = io.StringIO() + js.output_js(all_objs, output_fd, options) + + expect( + path.join(path.dirname(__file__), "metrics_test_output_js"), + output_fd.getvalue(), + ) + + +def test_fake_pings(): + """Another similarly-fragile test. + It generates C++ for pings_test.yaml, comparing it byte-for-byte + with an expected output C++ file `pings_test_output_js`. + Expect it to be fragile. + To generate new expected output files, set `UPDATE_EXPECT=1` when running the test suite: + + UPDATE_EXPECT=1 mach test toolkit/components/glean/tests/pytest + """ + + options = {"allow_reserved": False} + input_files = [Path(path.join(path.dirname(__file__), "pings_test.yaml"))] + + all_objs, options = run_glean_parser.parse_with_options(input_files, options) + + output_fd = io.StringIO() + js.output_js(all_objs, output_fd, options) + + expect( + path.join(path.dirname(__file__), "pings_test_output_js"), output_fd.getvalue() + ) + + +if __name__ == "__main__": + mozunit.main() diff --git a/toolkit/components/glean/tests/pytest/test_glean_parser_rust.py b/toolkit/components/glean/tests/pytest/test_glean_parser_rust.py new file mode 100644 index 0000000000..a586b0419b --- /dev/null +++ b/toolkit/components/glean/tests/pytest/test_glean_parser_rust.py @@ -0,0 +1,89 @@ +# 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 http://mozilla.org/MPL/2.0/. + +import io +import sys +from os import path +from pathlib import Path + +import mozunit +from expect_helper import expect + +# Shenanigans to import the rust outputter extension +FOG_ROOT_PATH = path.abspath( + path.join(path.dirname(__file__), path.pardir, path.pardir) +) +sys.path.append(path.join(FOG_ROOT_PATH, "build_scripts", "glean_parser_ext")) +import run_glean_parser +import rust + +# Shenanigans to import the in-tree glean_parser +GECKO_PATH = path.join(FOG_ROOT_PATH, path.pardir, path.pardir, path.pardir) +sys.path.append(path.join(GECKO_PATH, "third_party", "python", "glean_parser")) + + +def test_all_metric_types(): + """Honestly, this is a pretty bad test. + It generates Rust for a given test metrics.yaml and compares it byte-for-byte + with an expected output Rust file. + Expect it to be fragile. + To generate new expected output files, set `UPDATE_EXPECT=1` when running the test suite: + + UPDATE_EXPECT=1 mach test toolkit/components/glean/pytest + """ + + options = {"allow_reserved": False} + input_files = [Path(path.join(path.dirname(__file__), "metrics_test.yaml"))] + + all_objs, options = run_glean_parser.parse_with_options(input_files, options) + + output_fd = io.StringIO() + rust.output_rust(all_objs, output_fd, options) + + expect( + path.join(path.dirname(__file__), "metrics_test_output"), output_fd.getvalue() + ) + + +def test_fake_pings(): + """Another similarly-bad test. + It generates Rust for pings_test.yaml, comparing it byte-for-byte + with an expected output Rust file. + Expect it to be fragile. + To generate new expected output files, set `UPDATE_EXPECT=1` when running the test suite: + + UPDATE_EXPECT=1 mach test toolkit/components/glean/pytest + """ + + options = {"allow_reserved": False} + input_files = [Path(path.join(path.dirname(__file__), "pings_test.yaml"))] + + all_objs, options = run_glean_parser.parse_with_options(input_files, options) + + output_fd = io.StringIO() + rust.output_rust(all_objs, output_fd, options) + + expect(path.join(path.dirname(__file__), "pings_test_output"), output_fd.getvalue()) + + +def test_expires_version(): + """This test relies on the intermediary object format output by glean_parser. + Expect it to be fragile on glean_parser updates that change that format. + """ + + # The test file has 41, 42, 100. Use 42.0a1 here to ensure "expires == version" means expired. + options = run_glean_parser.get_parser_options("42.0a1") + input_files = [ + Path(path.join(path.dirname(__file__), "metrics_expires_versions_test.yaml")) + ] + + all_objs, options = run_glean_parser.parse_with_options(input_files, options) + + assert all_objs["test"]["expired1"].disabled is True + assert all_objs["test"]["expired2"].disabled is True + assert all_objs["test"]["unexpired"].disabled is False + + +if __name__ == "__main__": + mozunit.main() diff --git a/toolkit/components/glean/tests/pytest/test_jogfile_output.py b/toolkit/components/glean/tests/pytest/test_jogfile_output.py new file mode 100644 index 0000000000..801c0967ff --- /dev/null +++ b/toolkit/components/glean/tests/pytest/test_jogfile_output.py @@ -0,0 +1,50 @@ +# 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 http://mozilla.org/MPL/2.0/. + +import io +import sys +from os import path +from pathlib import Path + +import mozunit +from expect_helper import expect + +# Shenanigans to import the FOG glean_parser runner +FOG_ROOT_PATH = path.abspath( + path.join(path.dirname(__file__), path.pardir, path.pardir) +) +sys.path.append(path.join(FOG_ROOT_PATH, "build_scripts", "glean_parser_ext")) +import jog +import run_glean_parser + + +def test_jogfile_output(): + """ + A regression test. Very fragile. + It generates a jogfile for metrics_test.yaml and compares it + byte-for-byte with an expected output file. + + Also, part one of a two-part test. + The generated jogfile is consumed in Rust_TestJogfile in t/c/g/tests/gtest/test.rs + This is to ensure that the jogfile we generate in Python can be consumed in Rust. + + To generate new expected output files, set `UPDATE_EXPECT=1` when running the test suite: + + UPDATE_EXPECT=1 mach test toolkit/components/glean/pytest + """ + + options = {"allow_reserved": False} + here_path = Path(path.dirname(__file__)) + input_files = [here_path / "metrics_test.yaml", here_path / "pings_test.yaml"] + + all_objs, options = run_glean_parser.parse_with_options(input_files, options) + + output_fd = io.StringIO() + jog.output_file(all_objs, output_fd, options) + + expect(here_path / "jogfile_output", output_fd.getvalue()) + + +if __name__ == "__main__": + mozunit.main() diff --git a/toolkit/components/glean/tests/pytest/test_no_expired_metrics.py b/toolkit/components/glean/tests/pytest/test_no_expired_metrics.py new file mode 100644 index 0000000000..9783acaf0a --- /dev/null +++ b/toolkit/components/glean/tests/pytest/test_no_expired_metrics.py @@ -0,0 +1,46 @@ +# 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 http://mozilla.org/MPL/2.0/. + +import sys +from os import path +from pathlib import Path + +import mozunit + +# Shenanigans to import the metrics index's list of metrics.yamls +FOG_ROOT_PATH = path.abspath( + path.join(path.dirname(__file__), path.pardir, path.pardir) +) +sys.path.append(FOG_ROOT_PATH) +from metrics_index import metrics_yamls, tags_yamls + +# Shenanigans to import run_glean_parser +sys.path.append(path.join(FOG_ROOT_PATH, "build_scripts", "glean_parser_ext")) +import run_glean_parser + +# Shenanigans to import the in-tree glean_parser +GECKO_PATH = path.join(FOG_ROOT_PATH, path.pardir, path.pardir, path.pardir) +sys.path.append(path.join(GECKO_PATH, "third_party", "python", "glean_parser")) +from glean_parser import lint, parser, util + + +def test_no_metrics_expired(): + """ + Of all the metrics included in this build, are any expired? + If so, they must be removed or renewed. + + (This also checks other lints, as a treat.) + """ + with open("browser/config/version.txt", "r") as version_file: + app_version = version_file.read().strip() + + options = run_glean_parser.get_parser_options(app_version) + paths = [Path(x) for x in metrics_yamls] + [Path(x) for x in tags_yamls] + all_objs = parser.parse_objects(paths, options) + assert not util.report_validation_errors(all_objs) + assert not lint.lint_metrics(all_objs.value, options) + + +if __name__ == "__main__": + mozunit.main() diff --git a/toolkit/components/glean/tests/pytest/test_yaml_indices.py b/toolkit/components/glean/tests/pytest/test_yaml_indices.py new file mode 100644 index 0000000000..a8d185e59f --- /dev/null +++ b/toolkit/components/glean/tests/pytest/test_yaml_indices.py @@ -0,0 +1,38 @@ +# 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 http://mozilla.org/MPL/2.0/. + +import sys +from os import path + +import mozunit + +# Shenanigans to import the metrics index's lists of yamls +FOG_ROOT_PATH = path.abspath( + path.join(path.dirname(__file__), path.pardir, path.pardir) +) +sys.path.append(FOG_ROOT_PATH) +import metrics_index + + +def test_yamls_sorted(): + """ + Ensure the yamls indices are sorted lexicographically. + """ + # Ignore lists that are the concatenation of others. + to_ignore = ["metrics_yamls", "pings_yamls"] + + # Fetch names of all variables defined in the `metrics_index` module. + yaml_lists = [item for item in dir(metrics_index) if not item.startswith("__")] + for name in yaml_lists: + if name in to_ignore: + continue + + yamls_to_test = metrics_index.__dict__[name] + assert ( + sorted(yamls_to_test) == yamls_to_test + ), f"{name} must be be lexicographically sorted." + + +if __name__ == "__main__": + mozunit.main() diff --git a/toolkit/components/glean/tests/test_metrics.yaml b/toolkit/components/glean/tests/test_metrics.yaml new file mode 100644 index 0000000000..9ff471fd6b --- /dev/null +++ b/toolkit/components/glean/tests/test_metrics.yaml @@ -0,0 +1,780 @@ +# 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 http://mozilla.org/MPL/2.0/. + +# This file is for Internal FOG Test Use Only. + +--- +$schema: moz://mozilla.org/schemas/glean/metrics/2-0-0 + +test_only: + bad_code: + type: counter + description: | + Number of times we encountered bad code. + This is a test-only metric. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1646165 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1646165#c1 + data_sensitivity: + - technical + notification_emails: + - glean-team@mozilla.com + expires: never + send_in_pings: + - test-ping + + can_we_time_it: + type: timespan + time_unit: nanosecond + description: | + Test metric for a timespan. + This is a test-only metric. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1646165 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1646165#c1 + data_sensitivity: + - technical + notification_emails: + - glean-team@mozilla.com + expires: never + send_in_pings: + - test-ping + + cheesy_string: + type: string + description: | + Only the cheesiest of strings. + This is a test-only metric. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1673662 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1673662#c1 + data_sensitivity: + - technical + notification_emails: + - glean-team@mozilla.com + expires: never + send_in_pings: + - test-ping + + cheesy_string_list: + type: string_list + description: | + Only the cheesiest of strings. In list form! + This is a test-only metric. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1682960 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1682960#c1 + data_sensitivity: + - technical + notification_emails: + - glean-team@mozilla.com + expires: never + send_in_pings: + - test-ping + + what_a_date: + type: datetime + time_unit: second + description: > + ...To be writing FOG code. + This is a test-only metric. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1646165 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1646165#c1 + data_sensitivity: + - technical + notification_emails: + - glean-team@mozilla.com + expires: never + send_in_pings: + - test-ping + + what_id_it: # Using a different metrics yaml style for fun. + type: uuid + description: | + Just a UUID. + This is a test-only metric. + bugs: ["https://bugzilla.mozilla.org/show_bug.cgi?id=1673664"] + data_reviews: ["https://bugzilla.mozilla.org/show_bug.cgi?id=1673664#c1"] + data_sensitivity: ["technical"] + notification_emails: ["glean-team@mozilla.com"] + expires: never + send_in_pings: ["test-ping"] + + can_we_flag_it: + type: boolean + description: | + Test metric for a boolean. + This is a test-only metric. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1646165 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1646165#c1 + data_sensitivity: + - technical + notification_emails: + - glean-team@mozilla.com + expires: never + send_in_pings: + - test-ping + + do_you_remember: + type: memory_distribution + memory_unit: megabyte + description: | + They say it's the second thing to go. + This is a test-only metric. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1673648 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1673648#c1 + data_sensitivity: + - technical + expires: never + notification_emails: + - glean-team@mozilla.com + send_in_pings: + - test-ping + + what_time_is_it: + type: timing_distribution + time_unit: microsecond + description: | + Adheres to at least two of the top ten fallacies programmers believe + about time. + This is a test-only metric. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1673663 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1673663#c1 + data_sensitivity: + - technical + expires: never + notification_emails: + - glean-team@mozilla.com + send_in_pings: + - test-ping + telemetry_mirror: TELEMETRY_TEST_MIRROR_FOR_TIMING + + mabels_kitchen_counters: + type: labeled_counter + description: | + Counts Mabels labeled by their kitchen counters. + This is a test-only metric. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1675277 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1675277#c1 + data_sensitivity: + - technical + expires: never + notification_emails: + - glean-team@mozilla.com + send_in_pings: + - test-ping + + mabels_bathroom_counters: + type: labeled_counter + description: | + Counts Mabels labeled by their bathroom counters. + This is a test-only metric. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1683171 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1683171#c1 + data_sensitivity: + - technical + expires: never + notification_emails: + - glean-team@mozilla.com + send_in_pings: + - test-ping + + mabels_like_balloons: + type: labeled_boolean + description: | + Does the labeled Mabel like balloons? + This is a test-only metric. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1675277 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1675277#c1 + data_sensitivity: + - technical + expires: never + notification_emails: + - glean-team@mozilla.com + send_in_pings: + - test-ping + + mabels_balloon_strings: + type: labeled_string + description: | + What do the labeled Mabel's liked balloons' strings say? + This is a test-only metric. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1675277 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1675277#c1 + data_sensitivity: + - technical + expires: never + notification_emails: + - glean-team@mozilla.com + send_in_pings: + - test-ping + + mabels_label_maker: + type: labeled_string + description: | + Mabel just got a label maker and wants to party like it's + 1999. + This is a test-only metric. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1696388 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1696388 + data_sensitivity: + - technical + expires: never + notification_emails: + - glean-team@mozilla.com + send_in_pings: + - test-ping + + mirror_time: + type: timespan + time_unit: nanosecond + description: | + Mirrored metric for a timespan. + This is a test-only metric. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1685406 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1685406#c1 + data_sensitivity: + - technical + notification_emails: + - glean-team@mozilla.com + expires: never + send_in_pings: + - test-ping + telemetry_mirror: TELEMETRY_TEST_MIRROR_FOR_TIMESPAN + + mirror_time_nanos: + type: timespan + time_unit: nanosecond + description: | + Mirrored metric for a timespan. + This is a test-only metric. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1704106 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1704106#c1 + data_sensitivity: + - technical + notification_emails: + - glean-team@mozilla.com + expires: never + send_in_pings: + - test-ping + telemetry_mirror: TELEMETRY_TEST_MIRROR_FOR_TIMESPAN_NANOS + + mirrors_for_labeled_bools: + type: labeled_boolean + description: | + Mirrored metric. + This is a test-only metric. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1675277 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1675277#c1 + data_sensitivity: + - technical + expires: never + notification_emails: + - glean-team@mozilla.com + send_in_pings: + - test-ping + telemetry_mirror: TELEMETRY_TEST_MIRROR_FOR_LABELED_BOOL + + one_ping_one_bool: + type: boolean + description: | + One bool for one ping only. + This is a test-only metric. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1685402 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1685402#c1 + data_sensitivity: + - technical + expires: never + notification_emails: + - glean-team@mozilla.com + send_in_pings: + - one-ping-only + + meaning_of_life: + type: quantity + unit: unfathomable + description: | + Measures the one true answer to the Ultimate Question of Life, + the Universe, and Everything. + Approximately. + This is a test-only metric. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1704846 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1704846#c1 + data_sensitivity: + - technical + expires: never + notification_emails: + - glean-team@mozilla.com + send_in_pings: + - test-ping + telemetry_mirror: TELEMETRY_TEST_MIRROR_FOR_QUANTITY + + +test_only.ipc: + a_counter: + type: counter + description: | + This is a test-only metric. + Just counting things. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1646165 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1685406 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1646165#c1 + data_sensitivity: + - technical + notification_emails: + - glean-team@mozilla.com + expires: never + send_in_pings: + - store1 + no_lint: + - COMMON_PREFIX + telemetry_mirror: TELEMETRY_TEST_MIRROR_FOR_COUNTER + a_bool: + type: boolean + description: | + This is a test-only metric. + Just flagging things. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1646165 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1685406 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1646165#c1 + data_sensitivity: + - technical + notification_emails: + - glean-team@mozilla.com + expires: never + send_in_pings: + - store1 + no_lint: + - COMMON_PREFIX + telemetry_mirror: TELEMETRY_TEST_BOOLEAN_KIND + a_date: + type: datetime + time_unit: second + description: | + This is a test-only metric. + Just putting things on the calendar. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1646165 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1685406 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1646165#c1 + data_sensitivity: + - technical + notification_emails: + - glean-team@mozilla.com + expires: never + send_in_pings: + - store1 + no_lint: + - COMMON_PREFIX + telemetry_mirror: TELEMETRY_TEST_MIRROR_FOR_DATE + a_string: + type: string + description: | + This is a test-only metric. + Just setting some strings. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1646165 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1685406 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1646165#c1 + data_sensitivity: + - technical + notification_emails: + - glean-team@mozilla.com + expires: never + send_in_pings: + - store1 + no_lint: + - COMMON_PREFIX + telemetry_mirror: TELEMETRY_TEST_MULTIPLE_STORES_STRING + a_memory_dist: + type: memory_distribution + memory_unit: kilobyte + description: | + This is a test-only metric. + Just measuring memory. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1646165 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1685406 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1646165#c1 + data_sensitivity: + - technical + notification_emails: + - glean-team@mozilla.com + expires: never + send_in_pings: + - store1 + no_lint: + - COMMON_PREFIX + telemetry_mirror: TELEMETRY_TEST_LINEAR + a_timing_dist: + type: timing_distribution + description: | + This is a test-only metric. + Just measuring time. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1646165 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1685406 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1646165#c1 + data_sensitivity: + - technical + notification_emails: + - glean-team@mozilla.com + expires: never + send_in_pings: + - store1 + no_lint: + - COMMON_PREFIX + telemetry_mirror: TELEMETRY_TEST_EXPONENTIAL + a_custom_dist: + type: custom_distribution + description: | + This is a test-only metric. + Just measuring samples. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1713398 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1713398#c1 + data_sensitivity: + - technical + notification_emails: + - glean-team@mozilla.com + expires: never + send_in_pings: + - store1 + no_lint: + - COMMON_PREFIX + range_min: 1 + range_max: 2147483646 + bucket_count: 10 + histogram_type: linear + telemetry_mirror: TELEMETRY_TEST_MIRROR_FOR_CUSTOM + a_string_list: + type: string_list + description: | + This is a test-only metric. + Just appending some strings. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1646165 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1685406 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1646165#c1 + data_sensitivity: + - technical + notification_emails: + - glean-team@mozilla.com + expires: never + send_in_pings: + - store1 + no_lint: + - COMMON_PREFIX + telemetry_mirror: TELEMETRY_TEST_KEYED_BOOLEAN_KIND + an_event: + type: event + extra_keys: + extra1: + type: string + description: "Some extra data" + extra2: + type: string + description: "Some extra data again" + description: | + This is a test-only metric. + Just recording some events. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1646165 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1685406 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1646165#c1 + data_sensitivity: + - technical + notification_emails: + - glean-team@mozilla.com + expires: never + send_in_pings: + - store1 + no_lint: + - COMMON_PREFIX + telemetry_mirror: TelemetryTest_MirrorWithExtra_Object1 + event_with_extra: + type: event + extra_keys: + extra1: + type: string + description: "Some extra data" + extra2: + type: quantity + description: "Some extra data again" + extra3_longer_name: + type: boolean + description: "Some extra data again. Also tests extras with underscores" + description: | + This is a test-only metric. + Just recording some events. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1646165 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1685406 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1646165#c1 + data_sensitivity: + - technical + notification_emails: + - glean-team@mozilla.com + expires: never + send_in_pings: + - store1 + no_lint: + - COMMON_PREFIX + no_extra_event: + type: event + description: | + This is a test-only metric. + Just recording some events without the extra fuss. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1646165 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1685406 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1646165#c1 + data_sensitivity: + - technical + notification_emails: + - glean-team@mozilla.com + expires: never + send_in_pings: + - store1 + telemetry_mirror: TelemetryTest_NotExpiredOptout_Object1 + a_uuid: + type: uuid + description: | + This is a test-only metric. + Just recording some unique identifiers. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1646165 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1685406 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1646165#c1 + data_sensitivity: + - technical + notification_emails: + - glean-team@mozilla.com + expires: never + send_in_pings: + - store1 + no_lint: + - COMMON_PREFIX + telemetry_mirror: TELEMETRY_TEST_STRING_KIND + a_labeled_counter: + type: labeled_counter + description: | + This is a test-only metric. + Just counting labeled things. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1688281 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1685406 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1688281#c1 + data_sensitivity: + - technical + notification_emails: + - glean-team@mozilla.com + expires: never + send_in_pings: + - store1 + no_lint: + - COMMON_PREFIX + telemetry_mirror: TELEMETRY_TEST_ANOTHER_MIRROR_FOR_LABELED_COUNTER + another_labeled_counter: + type: labeled_counter + description: | + This is a test-only metric. + Just another metric counting labeled things. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1688281 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1685406 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1758795 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1688281#c1 + data_sensitivity: + - technical + notification_emails: + - glean-team@mozilla.com + expires: never + send_in_pings: + - store1 + no_lint: + - COMMON_PREFIX + telemetry_mirror: TELEMETRY_TEST_MIRROR_FOR_LABELED_COUNTER + a_quantity: + type: quantity + unit: squad + description: | + This is a test-only metric. + Just quantifying things. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1704846 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1704846#c1 + data_sensitivity: + - technical + notification_emails: + - glean-team@mozilla.com + expires: never + send_in_pings: + - store1 + no_lint: + - COMMON_PREFIX + irate: + type: rate + description: | + This is a test-only metric. + A rate that isn't happy about it. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1694496 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1694496#c1 + data_sensitivity: + - technical + notification_emails: + - glean-team@mozilla.com + expires: never + send_in_pings: + - store1 + telemetry_mirror: TELEMETRY_TEST_MIRROR_FOR_RATE + rate_with_external_denominator: + type: rate + denominator_metric: test_only.ipc.an_external_denominator + description: | + This is a test-only metric. + A rate with a denominator that is Out There. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1694496 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1694496#c1 + data_sensitivity: + - technical + notification_emails: + - glean-team@mozilla.com + expires: never + send_in_pings: + - store1 + an_external_denominator: + type: counter + description: | + This is a test-only metric. + A denominator not from around here. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1694496 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1694496#c1 + data_sensitivity: + - technical + notification_emails: + - glean-team@mozilla.com + expires: never + send_in_pings: + - store1 + no_lint: + - COMMON_PREFIX + a_url: + type: url + description: | + This is a test-only metric. + Just setting some Urls. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1766980 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1646165#c1 + data_sensitivity: + - technical + notification_emails: + - glean-team@mozilla.com + expires: never + send_in_pings: + - store1 + no_lint: + - COMMON_PREFIX + telemetry_mirror: TELEMETRY_TEST_MIRROR_FOR_URL + +test_only.jog: + a_counter: + type: counter + description: | + This is a test-only metric. + Just counting things. + Tied closely to test_jog_name_collision. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1767039 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1767039 + data_sensitivity: + - technical + notification_emails: + - glean-team@mozilla.com + expires: never + send_in_pings: + - store1 + + an_event: + type: event + extra_keys: + extra1: + type: string + description: "Some extra data" + extra2: + type: string + description: "Some extra data again" + description: | + This is a test-only metric. + Just recording some events. + Tied closely to test_jog_name_collision. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1767039 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1767039 + data_sensitivity: + - technical + notification_emails: + - glean-team@mozilla.com + expires: never + send_in_pings: + - store1 diff --git a/toolkit/components/glean/tests/test_pings.yaml b/toolkit/components/glean/tests/test_pings.yaml new file mode 100644 index 0000000000..d62f682109 --- /dev/null +++ b/toolkit/components/glean/tests/test_pings.yaml @@ -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 http://mozilla.org/MPL/2.0/. + +# This file defines the pings that are recorded by the Glean SDK. They are +# automatically converted to platform-specific code at build time using the +# `glean_parser` PyPI package. + +# This file is presently for Internal FOG Test Use Only. + +--- +$schema: moz://mozilla.org/schemas/glean/pings/2-0-0 + +one-ping-only: + description: | + This ping is for tests only. + include_client_id: false + send_if_empty: true + bugs: + - https://bugzilla.mozilla.org/1673660 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1673660#c1 + notification_emails: + - chutten@mozilla.com + - glean-team@mozilla.com + no_lint: + - REDUNDANT_PING + +test-ping: + description: | + This ping is for tests only. + include_client_id: false + send_if_empty: true + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1737157 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1737157#c1 + notification_emails: + - chutten@mozilla.com + - glean-team@mozilla.com + no_lint: + - REDUNDANT_PING diff --git a/toolkit/components/glean/tests/xpcshell/head.js b/toolkit/components/glean/tests/xpcshell/head.js new file mode 100644 index 0000000000..f42bd02822 --- /dev/null +++ b/toolkit/components/glean/tests/xpcshell/head.js @@ -0,0 +1,6 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +const { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); diff --git a/toolkit/components/glean/tests/xpcshell/test_FOGIPCLimit.js b/toolkit/components/glean/tests/xpcshell/test_FOGIPCLimit.js new file mode 100644 index 0000000000..10ab9f5bc3 --- /dev/null +++ b/toolkit/components/glean/tests/xpcshell/test_FOGIPCLimit.js @@ -0,0 +1,51 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); + +const { ContentTaskUtils } = ChromeUtils.importESModule( + "resource://testing-common/ContentTaskUtils.sys.mjs" +); + +add_setup( + /* on Android FOG is set up through head.js */ + { skip_if: () => !runningInParent || AppConstants.platform == "android" }, + function test_setup() { + // Give FOG a temp profile to init within. + do_get_profile(); + + // We need to initialize it once, otherwise operations will be stuck in the pre-init queue. + Services.fog.initializeFOG(); + } +); + +// Keep in sync with ipc.rs. +// "Why no -1?" Because the limit's 100k. The -1 is because of atomic ops. +const FOG_IPC_PAYLOAD_ACCESS_LIMIT = 100000; + +add_task({ skip_if: () => runningInParent }, async function run_child_stuff() { + for (let i = 0; i < FOG_IPC_PAYLOAD_ACCESS_LIMIT + 1; i++) { + Glean.testOnly.badCode.add(1); + } +}); + +add_task( + { skip_if: () => !runningInParent }, + async function test_fog_ipc_limit() { + await run_test_in_child("test_FOGIPCLimit.js"); + + await ContentTaskUtils.waitForCondition(() => { + return !!Glean.testOnly.badCode.testGetValue(); + }, "Waiting for IPC."); + + // The child exceeded the number of accesses to trigger an IPC flush. + Assert.greater( + Glean.testOnly.badCode.testGetValue(), + FOG_IPC_PAYLOAD_ACCESS_LIMIT + ); + } +); diff --git a/toolkit/components/glean/tests/xpcshell/test_FOGInit.js b/toolkit/components/glean/tests/xpcshell/test_FOGInit.js new file mode 100644 index 0000000000..f8c4a410ad --- /dev/null +++ b/toolkit/components/glean/tests/xpcshell/test_FOGInit.js @@ -0,0 +1,41 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); + +add_setup( + /* on Android FOG is set up through head.js */ + { skip_if: () => AppConstants.platform == "android" }, + function test_setup() { + // FOG needs a profile directory to put its data in. + do_get_profile(); + + // Glean init (via `chrono`) gets the timezone via unprotected write. + // This is being worked around: + // https://github.com/chronotope/chrono/pull/677 + // Until that reaches a release and we update to it (bug 1780401), ensure + // local time has been loaded by JS before we kick of Glean init. + new Date().getHours(); // used for its side effect. + + // We need to initialize it once, otherwise operations will be stuck in the pre-init queue. + Services.fog.initializeFOG(); + } +); + +add_task(function test_fog_init_works() { + if (new Date().getHours() >= 3 && new Date().getHours() <= 4) { + // We skip this test if it's too close to 4AM, when we might send a + // "metrics" ping between init and this test being run. + Assert.ok(true, "Too close to 'metrics' ping send window. Skipping test."); + return; + } + Assert.greater( + Glean.fog.initialization.testGetValue(), + 0, + "FOG init happened, and its time was measured." + ); +}); diff --git a/toolkit/components/glean/tests/xpcshell/test_FOGPrefs.js b/toolkit/components/glean/tests/xpcshell/test_FOGPrefs.js new file mode 100644 index 0000000000..943ffb3186 --- /dev/null +++ b/toolkit/components/glean/tests/xpcshell/test_FOGPrefs.js @@ -0,0 +1,47 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TELEMETRY_SERVER_PREF = "toolkit.telemetry.server"; +const UPLOAD_PREF = "datareporting.healthreport.uploadEnabled"; +const LOCALHOST_PREF = "telemetry.fog.test.localhost_port"; + +// FOG needs a profile directory to put its data in. +do_get_profile(); + +// We want Glean to use a localhost server so we can be SURE not to send data to the outside world. +// Yes, the port spells GLEAN on a T9 keyboard, why do you ask? +Services.prefs.setIntPref(LOCALHOST_PREF, 45326); +// We need to initialize it once, otherwise operations will be stuck in the pre-init queue. +Services.fog.initializeFOG(); + +add_task(function test_fog_upload_only() { + // Don't forget to point the telemetry server to localhost, or Telemetry + // might make a non-local connection during the test run. + Services.prefs.setStringPref( + TELEMETRY_SERVER_PREF, + "http://localhost/telemetry-fake/" + ); + // Be sure to set port=-1 for faking success _before_ enabling upload. + // Or else there's a short window where we might send something. + Services.prefs.setIntPref(LOCALHOST_PREF, -1); + Services.prefs.setBoolPref(UPLOAD_PREF, true); + + const value = 42; + Glean.testOnly.meaningOfLife.set(value); + // We specifically look at the custom ping's value because we know it + // won't be reset by being sent. + Assert.equal(value, Glean.testOnly.meaningOfLife.testGetValue("test-ping")); + + // Despite upload being disabled, we keep the old values around. + Services.prefs.setBoolPref(UPLOAD_PREF, false); + Assert.equal(value, Glean.testOnly.meaningOfLife.testGetValue("test-ping")); + + // Now, when we turn the fake upload off, we clear the stores + Services.prefs.setIntPref(LOCALHOST_PREF, 0); + Assert.equal( + undefined, + Glean.testOnly.meaningOfLife.testGetValue("test-ping") + ); +}); diff --git a/toolkit/components/glean/tests/xpcshell/test_GIFFT.js b/toolkit/components/glean/tests/xpcshell/test_GIFFT.js new file mode 100644 index 0000000000..20923d057e --- /dev/null +++ b/toolkit/components/glean/tests/xpcshell/test_GIFFT.js @@ -0,0 +1,538 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); +const { setTimeout } = ChromeUtils.importESModule( + "resource://gre/modules/Timer.sys.mjs" +); +const { TelemetryTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TelemetryTestUtils.sys.mjs" +); + +const Telemetry = Services.telemetry; + +function sleep(ms) { + /* eslint-disable mozilla/no-arbitrary-setTimeout */ + return new Promise(resolve => setTimeout(resolve, ms)); +} + +function scalarValue(aScalarName) { + let snapshot = Telemetry.getSnapshotForScalars(); + return "parent" in snapshot ? snapshot.parent[aScalarName] : undefined; +} + +function keyedScalarValue(aScalarName) { + let snapshot = Telemetry.getSnapshotForKeyedScalars(); + return "parent" in snapshot ? snapshot.parent[aScalarName] : undefined; +} + +add_setup(function test_setup() { + // FOG needs a profile directory to put its data in. + do_get_profile(); + + Services.prefs.setBoolPref( + "toolkit.telemetry.testing.overrideProductsCheck", + true + ); + + // We need to initialize it once, otherwise operations will be stuck in the pre-init queue. + // On Android FOG is set up through head.js. + if (AppConstants.platform != "android") { + Services.fog.initializeFOG(); + } +}); + +add_task(function test_gifft_counter() { + Glean.testOnlyIpc.aCounter.add(20); + Assert.equal(20, Glean.testOnlyIpc.aCounter.testGetValue()); + Assert.equal(20, scalarValue("telemetry.test.mirror_for_counter")); +}); + +add_task(function test_gifft_boolean() { + Glean.testOnlyIpc.aBool.set(false); + Assert.equal(false, Glean.testOnlyIpc.aBool.testGetValue()); + Assert.equal(false, scalarValue("telemetry.test.boolean_kind")); +}); + +add_task(function test_gifft_datetime() { + const dateStr = "2021-03-22T16:06:00"; + const value = new Date(dateStr); + Glean.testOnlyIpc.aDate.set(value.getTime() * 1000); + + let received = Glean.testOnlyIpc.aDate.testGetValue(); + Assert.equal(value.getTime(), received.getTime()); + Assert.ok(scalarValue("telemetry.test.mirror_for_date").startsWith(dateStr)); +}); + +add_task(function test_gifft_string() { + const value = "a string!"; + Glean.testOnlyIpc.aString.set(value); + + Assert.equal(value, Glean.testOnlyIpc.aString.testGetValue()); + Assert.equal(value, scalarValue("telemetry.test.multiple_stores_string")); +}); + +add_task(function test_gifft_memory_dist() { + Glean.testOnlyIpc.aMemoryDist.accumulate(7); + Glean.testOnlyIpc.aMemoryDist.accumulate(17); + + let data = Glean.testOnlyIpc.aMemoryDist.testGetValue(); + // `data.sum` is in bytes, but the metric is in KB. + Assert.equal(24 * 1024, data.sum, "Sum's correct"); + for (let [bucket, count] of Object.entries(data.values)) { + Assert.ok( + count == 0 || (count == 1 && (bucket == 6888 || bucket == 17109)), + `Only two buckets have a sample ${bucket} ${count}` + ); + } + + data = Telemetry.getHistogramById("TELEMETRY_TEST_LINEAR").snapshot(); + Telemetry.getHistogramById("TELEMETRY_TEST_LINEAR").clear(); + Assert.equal(24, data.sum, "Histogram's in `memory_unit` units"); + Assert.equal(2, data.values["1"], "Both samples in a low bucket"); + + // MemoryDistribution's Accumulate method to takes + // a platform specific type (size_t). + // Glean's, however, is i64, and, glean_memory_dist is uint64_t + // What happens when we give accumulate dubious values? + // This may occur on some uncommon platforms. + // Note: there are issues in JS with numbers above 2**53 + Glean.testOnlyIpc.aMemoryDist.accumulate(36893488147419103232); + let dubiousValue = Object.entries( + Glean.testOnlyIpc.aMemoryDist.testGetValue().values + )[0][1]; + Assert.equal( + dubiousValue, + 1, + "Greater than 64-Byte number did not accumulate correctly" + ); + + // Values lower than the out-of-range value are not clamped + // resulting in an exception being thrown from the glean side + // when the value exceeds the glean maximum allowed value + Glean.testOnlyIpc.aMemoryDist.accumulate(Math.pow(2, 31)); + Assert.throws( + () => Glean.testOnlyIpc.aMemoryDist.testGetValue(), + /NS_ERROR_LOSS_OF_SIGNIFICANT_DATA/, + "Did not accumulate correctly" + ); +}); + +add_task(function test_gifft_custom_dist() { + Glean.testOnlyIpc.aCustomDist.accumulateSamples([7, 268435458]); + + let data = Glean.testOnlyIpc.aCustomDist.testGetValue(); + Assert.equal(7 + 268435458, data.sum, "Sum's correct"); + for (let [bucket, count] of Object.entries(data.values)) { + Assert.ok( + count == 0 || (count == 1 && (bucket == 1 || bucket == 268435456)), + `Only two buckets have a sample ${bucket} ${count}` + ); + } + + data = Telemetry.getHistogramById( + "TELEMETRY_TEST_MIRROR_FOR_CUSTOM" + ).snapshot(); + Telemetry.getHistogramById("TELEMETRY_TEST_MIRROR_FOR_CUSTOM").clear(); + Assert.equal(7 + 268435458, data.sum, "Sum in histogram is correct"); + Assert.equal(1, data.values["1"], "One sample in the low bucket"); + // Yes, the bucket is off-by-one compared to Glean. + Assert.equal(1, data.values["268435457"], "One sample in the next bucket"); +}); + +add_task(async function test_gifft_timing_dist() { + let t1 = Glean.testOnlyIpc.aTimingDist.start(); + // Interleave some other metric's samples. bug 1768636. + let ot1 = Glean.testOnly.whatTimeIsIt.start(); + let t2 = Glean.testOnlyIpc.aTimingDist.start(); + let ot2 = Glean.testOnly.whatTimeIsIt.start(); + Glean.testOnly.whatTimeIsIt.cancel(ot1); + Glean.testOnly.whatTimeIsIt.cancel(ot2); + + await sleep(5); + + let t3 = Glean.testOnlyIpc.aTimingDist.start(); + Glean.testOnlyIpc.aTimingDist.cancel(t1); + + await sleep(5); + + Glean.testOnlyIpc.aTimingDist.stopAndAccumulate(t2); // 10ms + Glean.testOnlyIpc.aTimingDist.stopAndAccumulate(t3); // 5ms + + let data = Glean.testOnlyIpc.aTimingDist.testGetValue(); + const NANOS_IN_MILLIS = 1e6; + // bug 1701949 - Sleep gets close, but sometimes doesn't wait long enough. + const EPSILON = 40000; + + // Variance in timing makes getting the sum impossible to know. + // 10 and 5 input value can be trunacted to 4. + 9. >= 13. from cast + Assert.greater(data.sum, 13 * NANOS_IN_MILLIS - EPSILON); + + // No guarantees from timers means no guarantees on buckets. + // But we can guarantee it's only two samples. + Assert.equal( + 2, + Object.entries(data.values).reduce( + (acc, [bucket, count]) => acc + count, + 0 + ), + "Only two buckets with samples" + ); + + data = Telemetry.getHistogramById("TELEMETRY_TEST_EXPONENTIAL").snapshot(); + // Suffers from same cast truncation issue of 9.... and 4.... values + Assert.greaterOrEqual(data.sum, 13, "Histogram's in milliseconds"); + Assert.equal( + 2, + Object.entries(data.values).reduce( + (acc, [bucket, count]) => acc + count, + 0 + ), + "Only two samples" + ); +}); + +add_task(function test_gifft_string_list_works() { + const value = "a string!"; + const value2 = "another string!"; + const value3 = "yet another string."; + + // `set` doesn't work in the mirror, so use `add` + Glean.testOnlyIpc.aStringList.add(value); + Glean.testOnlyIpc.aStringList.add(value2); + Glean.testOnlyIpc.aStringList.add(value3); + + let val = Glean.testOnlyIpc.aStringList.testGetValue(); + // Note: This is incredibly fragile and will break if we ever rearrange items + // in the string list. + Assert.deepEqual([value, value2, value3], val); + + val = keyedScalarValue("telemetry.test.keyed_boolean_kind"); + // This too may be fragile. + Assert.deepEqual( + { + [value]: true, + [value2]: true, + [value3]: true, + }, + val + ); +}); + +add_task(function test_gifft_events() { + Telemetry.setEventRecordingEnabled("telemetry.test", true); + + Glean.testOnlyIpc.noExtraEvent.record(); + var events = Glean.testOnlyIpc.noExtraEvent.testGetValue(); + Assert.equal(1, events.length); + Assert.equal("test_only.ipc", events[0].category); + Assert.equal("no_extra_event", events[0].name); + + let extra = { extra1: "can set extras", extra2: "passing more data" }; + Glean.testOnlyIpc.anEvent.record(extra); + events = Glean.testOnlyIpc.anEvent.testGetValue(); + Assert.equal(1, events.length); + Assert.equal("test_only.ipc", events[0].category); + Assert.equal("an_event", events[0].name); + Assert.deepEqual(extra, events[0].extra); + + TelemetryTestUtils.assertEvents( + [ + ["telemetry.test", "not_expired_optout", "object1", undefined, undefined], + ["telemetry.test", "mirror_with_extra", "object1", null, extra], + ], + { category: "telemetry.test" } + ); +}); + +add_task(function test_gifft_uuid() { + const kTestUuid = "decafdec-afde-cafd-ecaf-decafdecafde"; + Glean.testOnlyIpc.aUuid.set(kTestUuid); + Assert.equal(kTestUuid, Glean.testOnlyIpc.aUuid.testGetValue()); + Assert.equal(kTestUuid, scalarValue("telemetry.test.string_kind")); +}); + +add_task(function test_gifft_labeled_counter() { + Assert.equal( + undefined, + Glean.testOnlyIpc.aLabeledCounter.a_label.testGetValue(), + "New labels with no values should return undefined" + ); + Glean.testOnlyIpc.aLabeledCounter.a_label.add(1); + Glean.testOnlyIpc.aLabeledCounter.another_label.add(2); + Glean.testOnlyIpc.aLabeledCounter.a_label.add(3); + Assert.equal(4, Glean.testOnlyIpc.aLabeledCounter.a_label.testGetValue()); + Assert.equal( + 2, + Glean.testOnlyIpc.aLabeledCounter.another_label.testGetValue() + ); + // What about invalid/__other__? + Assert.equal( + undefined, + Glean.testOnlyIpc.aLabeledCounter.__other__.testGetValue() + ); + Glean.testOnlyIpc.aLabeledCounter.InvalidLabel.add(3); + Assert.throws( + () => Glean.testOnlyIpc.aLabeledCounter.__other__.testGetValue(), + /NS_ERROR_LOSS_OF_SIGNIFICANT_DATA/, + "Can't get the value when you're error'd" + ); + + let value = keyedScalarValue( + "telemetry.test.another_mirror_for_labeled_counter" + ); + Assert.deepEqual( + { + a_label: 4, + another_label: 2, + InvalidLabel: 3, + }, + value + ); +}); + +add_task(async function test_gifft_timespan() { + // We start, briefly sleep and then stop. + // That guarantees some time to measure. + Glean.testOnly.mirrorTime.start(); + await sleep(10); + Glean.testOnly.mirrorTime.stop(); + + const NANOS_IN_MILLIS = 1e6; + // bug 1701949 - Sleep gets close, but sometimes doesn't wait long enough. + const EPSILON = 40000; + Assert.greater( + Glean.testOnly.mirrorTime.testGetValue(), + 10 * NANOS_IN_MILLIS - EPSILON + ); + // Mirrored to milliseconds. + Assert.greaterOrEqual(scalarValue("telemetry.test.mirror_for_timespan"), 9); +}); + +add_task(async function test_gifft_timespan_raw() { + Glean.testOnly.mirrorTimeNanos.setRaw(15 /*ns*/); + + Assert.equal(15, Glean.testOnly.mirrorTimeNanos.testGetValue()); + // setRaw, unlike start/stop, mirrors the raw value directly. + Assert.equal(scalarValue("telemetry.test.mirror_for_timespan_nanos"), 15); +}); + +add_task(async function test_gifft_labeled_boolean() { + Assert.equal( + undefined, + Glean.testOnly.mirrorsForLabeledBools.a_label.testGetValue(), + "New labels with no values should return undefined" + ); + Glean.testOnly.mirrorsForLabeledBools.a_label.set(true); + Glean.testOnly.mirrorsForLabeledBools.another_label.set(false); + Assert.equal( + true, + Glean.testOnly.mirrorsForLabeledBools.a_label.testGetValue() + ); + Assert.equal( + false, + Glean.testOnly.mirrorsForLabeledBools.another_label.testGetValue() + ); + // What about invalid/__other__? + Assert.equal( + undefined, + Glean.testOnly.mirrorsForLabeledBools.__other__.testGetValue() + ); + Glean.testOnly.mirrorsForLabeledBools.InvalidLabel.set(true); + Assert.throws( + () => Glean.testOnly.mirrorsForLabeledBools.__other__.testGetValue(), + /NS_ERROR_LOSS_OF_SIGNIFICANT_DATA/, + "Should throw because of a recording error." + ); + + // In Telemetry there is no invalid label + let value = keyedScalarValue("telemetry.test.mirror_for_labeled_bool"); + Assert.deepEqual( + { + a_label: true, + another_label: false, + InvalidLabel: true, + }, + value + ); +}); + +add_task(function test_gifft_boolean() { + Glean.testOnly.meaningOfLife.set(42); + Assert.equal(42, Glean.testOnly.meaningOfLife.testGetValue()); + Assert.equal(42, scalarValue("telemetry.test.mirror_for_quantity")); +}); + +add_task(function test_gifft_rate() { + Glean.testOnlyIpc.irate.addToNumerator(22); + Glean.testOnlyIpc.irate.addToDenominator(7); + Assert.deepEqual( + { numerator: 22, denominator: 7 }, + Glean.testOnlyIpc.irate.testGetValue() + ); + Assert.deepEqual( + { numerator: 22, denominator: 7 }, + keyedScalarValue("telemetry.test.mirror_for_rate") + ); +}); + +add_task( + { + // bug 1670259 to see if we can implement `testResetFOG` on Android. + skip_if: () => AppConstants.platform == "android", + }, + function test_gifft_numeric_limits() { + // Glean and Telemetry don't share the same storage sizes or signedness. + // Check the edges. + + // 0) Reset everything + Services.fog.testResetFOG(); + Services.telemetry.getSnapshotForHistograms("main", true /* aClearStore */); + Services.telemetry.getSnapshotForScalars("main", true /* aClearStore */); + Services.telemetry.getSnapshotForKeyedScalars( + "main", + true /* aClearStore */ + ); + + // 1) Counter: i32 (saturates), mirrored to uint Scalar: u32 (overflows) + // 1.1) Negative parameters refused. + Glean.testOnlyIpc.aCounter.add(-20); + // Unfortunately we can't check what the error was, due to API design. + // (chutten blames chutten for his shortsightedness) + Assert.throws( + () => Glean.testOnlyIpc.aCounter.testGetValue(), + /NS_ERROR_LOSS_OF_SIGNIFICANT_DATA/, + "Can't get the value when you're error'd" + ); + Assert.equal(undefined, scalarValue("telemetry.test.mirror_for_counter")); + // Clear the error state + Services.fog.testResetFOG(); + + // 1.2) Values that sum larger than u32::max saturate (counter) and overflow (Scalar) + // Sums to 2^32 + 1 + Glean.testOnlyIpc.aCounter.add(Math.pow(2, 31) - 1); + Glean.testOnlyIpc.aCounter.add(1); + Glean.testOnlyIpc.aCounter.add(Math.pow(2, 31) - 1); + Glean.testOnlyIpc.aCounter.add(2); + // Glean doesn't actually throw on saturation (bug 1751469), + // so we can just check the saturation value. + Assert.equal( + Math.pow(2, 31) - 1, + Glean.testOnlyIpc.aCounter.testGetValue() + ); + // Telemetry will have wrapped around to 1 + Assert.equal(1, scalarValue("telemetry.test.mirror_for_counter")); + + // 2) Quantity: i64 (saturates), mirrored to uint Scalar: u32 (overflows) + // 2.1) Negative parameters refused. + Glean.testOnly.meaningOfLife.set(-42); + // Glean will error on this. + Assert.throws( + () => Glean.testOnly.meaningOfLife.testGetValue(), + /NS_ERROR_LOSS_OF_SIGNIFICANT_DATA/, + "Can't get the value when you're error'd" + ); + // GIFFT doesn't tell Telemetry about the weird value at all. + Assert.equal(undefined, scalarValue("telemetry.test.mirror_for_quantity")); + // Clear the error state + Services.fog.testResetFOG(); + + // 2.2) A parameter larger than u32::max is passed to Glean unchanged, + // but is clamped to u32::max before being passed to Telemetry. + Glean.testOnly.meaningOfLife.set(Math.pow(2, 32)); + Assert.equal(Math.pow(2, 32), Glean.testOnly.meaningOfLife.testGetValue()); + Assert.equal( + Math.pow(2, 32) - 1, + scalarValue("telemetry.test.mirror_for_quantity") + ); + + // 3) Rate: two i32 (saturates), mirrored to keyed uint Scalar: u32s (overflow) + // 3.1) Negative parameters refused. + Glean.testOnlyIpc.irate.addToNumerator(-22); + Glean.testOnlyIpc.irate.addToDenominator(7); + Assert.throws( + () => Glean.testOnlyIpc.irate.testGetValue(), + /NS_ERROR_LOSS_OF_SIGNIFICANT_DATA/, + "Can't get the value when you're error'd" + ); + Assert.deepEqual( + { denominator: 7 }, + keyedScalarValue("telemetry.test.mirror_for_rate") + ); + // Clear the error state + Services.fog.testResetFOG(); + // Clear the partial Telemetry value + Services.telemetry.getSnapshotForKeyedScalars( + "main", + true /* aClearStore */ + ); + + // Now the denominator: + Glean.testOnlyIpc.irate.addToNumerator(22); + Glean.testOnlyIpc.irate.addToDenominator(-7); + Assert.throws( + () => Glean.testOnlyIpc.irate.testGetValue(), + /NS_ERROR_LOSS_OF_SIGNIFICANT_DATA/, + "Can't get the value when you're error'd" + ); + Assert.deepEqual( + { numerator: 22 }, + keyedScalarValue("telemetry.test.mirror_for_rate") + ); + + // 4) Timespan + // ( Can't overflow time without finding a way to get TimeStamp to think + // we're 2^32 milliseconds later without waiting a month ) + + // 5) TimingDistribution + // ( Can't overflow time with start() and stopAndAccumulate() without + // waiting for ages. But we _do_ have a test-only raw API...) + // The max sample for timing_distribution is 600000000000. + // The type for timing_distribution samples is i64. + // This means when we explore the edges of GIFFT's limits, we're well past + // Glean's limits. All we can get out of Glean is errors. + // (Which is good for data, difficult for tests.) + // But GIFFT should properly saturate in Telemetry at i32::max, + // so we shall test that. + Glean.testOnlyIpc.aTimingDist.testAccumulateRawMillis(Math.pow(2, 31) + 1); + Glean.testOnlyIpc.aTimingDist.testAccumulateRawMillis(Math.pow(2, 32) + 1); + Assert.throws( + () => Glean.testOnlyIpc.aTimingDist.testGetValue(), + /NS_ERROR_LOSS_OF_SIGNIFICANT_DATA/, + "Can't get the value when you're error'd" + ); + let snapshot = Telemetry.getHistogramById( + "TELEMETRY_TEST_EXPONENTIAL" + ).snapshot(); + Assert.equal( + snapshot.values["2147483646"], + 2, + "samples > i32::max should end up in the top bucket" + ); + } +); + +add_task(function test_gifft_url() { + const value = "https://www.example.com"; + Glean.testOnlyIpc.aUrl.set(value); + + Assert.equal(value, Glean.testOnlyIpc.aUrl.testGetValue()); + Assert.equal(value, scalarValue("telemetry.test.mirror_for_url")); +}); + +add_task(function test_gifft_url_cropped() { + const value = `https://example.com${"/test".repeat(47)}`; + Glean.testOnlyIpc.aUrl.set(value); + + Assert.equal(value, Glean.testOnlyIpc.aUrl.testGetValue()); + // We expect the mirrored URL to be truncated at the maximum + // length supported by string scalars. + Assert.equal( + value.substring(0, 50), + scalarValue("telemetry.test.mirror_for_url") + ); +}); diff --git a/toolkit/components/glean/tests/xpcshell/test_GIFFTIPC.js b/toolkit/components/glean/tests/xpcshell/test_GIFFTIPC.js new file mode 100644 index 0000000000..73a9da9be2 --- /dev/null +++ b/toolkit/components/glean/tests/xpcshell/test_GIFFTIPC.js @@ -0,0 +1,315 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); +const { ContentTaskUtils } = ChromeUtils.importESModule( + "resource://testing-common/ContentTaskUtils.sys.mjs" +); +const { setTimeout } = ChromeUtils.importESModule( + "resource://gre/modules/Timer.sys.mjs" +); +const Telemetry = Services.telemetry; +const { TelemetryTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TelemetryTestUtils.sys.mjs" +); + +function sleep(ms) { + /* eslint-disable mozilla/no-arbitrary-setTimeout */ + return new Promise(resolve => setTimeout(resolve, ms)); +} + +function scalarValue(aScalarName, aProcessName) { + let snapshot = Telemetry.getSnapshotForScalars(); + return aProcessName in snapshot + ? snapshot[aProcessName][aScalarName] + : undefined; +} + +function keyedScalarValue(aScalarName, aProcessName) { + let snapshot = Telemetry.getSnapshotForKeyedScalars(); + return aProcessName in snapshot + ? snapshot[aProcessName][aScalarName] + : undefined; +} + +add_setup({ skip_if: () => !runningInParent }, function test_setup() { + // Give FOG a temp profile to init within. + do_get_profile(); + + // Allows these tests to properly run on e.g. Thunderbird + Services.prefs.setBoolPref( + "toolkit.telemetry.testing.overrideProductsCheck", + true + ); + + // We need to initialize it once, otherwise operations will be stuck in the pre-init queue. + // on Android FOG is set up through head.js + if (AppConstants.platform != "android") { + Services.fog.initializeFOG(); + } +}); + +const COUNT = 42; +const CHEESY_STRING = "a very cheesy string!"; +const CHEESIER_STRING = "a much cheesier string!"; +const CUSTOM_SAMPLES = [3, 4]; +const EVENT_EXTRA = { extra1: "so very extra" }; +const MEMORIES = [13, 31]; +const MEMORY_BUCKETS = ["13193", "31378"]; // buckets are strings : | +const A_LABEL_COUNT = 3; +const ANOTHER_LABEL_COUNT = 5; +const INVALID_COUNTERS = 7; +const IRATE_NUMERATOR = 44; +const IRATE_DENOMINATOR = 14; + +add_task({ skip_if: () => runningInParent }, async function run_child_stuff() { + let oldCanRecordBase = Telemetry.canRecordBase; + Telemetry.canRecordBase = true; // Ensure we're able to record things. + + Glean.testOnlyIpc.aCounter.add(COUNT); + Glean.testOnlyIpc.aStringList.add(CHEESY_STRING); + Glean.testOnlyIpc.aStringList.add(CHEESIER_STRING); + + Glean.testOnlyIpc.noExtraEvent.record(); + Glean.testOnlyIpc.anEvent.record(EVENT_EXTRA); + + for (let memory of MEMORIES) { + Glean.testOnlyIpc.aMemoryDist.accumulate(memory); + } + + let t1 = Glean.testOnlyIpc.aTimingDist.start(); + let t2 = Glean.testOnlyIpc.aTimingDist.start(); + + await sleep(5); + + let t3 = Glean.testOnlyIpc.aTimingDist.start(); + Glean.testOnlyIpc.aTimingDist.cancel(t1); + + await sleep(5); + + Glean.testOnlyIpc.aTimingDist.stopAndAccumulate(t2); // 10ms + Glean.testOnlyIpc.aTimingDist.stopAndAccumulate(t3); // 5ms + + Glean.testOnlyIpc.aCustomDist.accumulateSamples(CUSTOM_SAMPLES); + + Glean.testOnlyIpc.aLabeledCounter.a_label.add(A_LABEL_COUNT); + Glean.testOnlyIpc.aLabeledCounter.another_label.add(ANOTHER_LABEL_COUNT); + + // Has to be different from aLabeledCounter so the error we record doesn't + // get in the way. + Glean.testOnlyIpc.anotherLabeledCounter.InvalidLabel.add(INVALID_COUNTERS); + + Glean.testOnlyIpc.irate.addToNumerator(IRATE_NUMERATOR); + Glean.testOnlyIpc.irate.addToDenominator(IRATE_DENOMINATOR); + Telemetry.canRecordBase = oldCanRecordBase; +}); + +add_task( + { skip_if: () => !runningInParent }, + async function test_child_metrics() { + Telemetry.setEventRecordingEnabled("telemetry.test", true); + + // Clear any stray Telemetry data + Telemetry.clearScalars(); + Telemetry.getSnapshotForHistograms("main", true); + Telemetry.clearEvents(); + + await run_test_in_child("test_GIFFTIPC.js"); + + // Wait for both IPC mechanisms to flush. + await Services.fog.testFlushAllChildren(); + await ContentTaskUtils.waitForCondition(() => { + let snapshot = Telemetry.getSnapshotForKeyedScalars(); + return ( + "content" in snapshot && + "telemetry.test.mirror_for_rate" in snapshot.content + ); + }, "failed to find content telemetry in parent"); + + // boolean + // Doesn't work over IPC + + // counter + Assert.equal(Glean.testOnlyIpc.aCounter.testGetValue(), COUNT); + Assert.equal( + scalarValue("telemetry.test.mirror_for_counter", "content"), + COUNT, + "content-process Scalar has expected count" + ); + + // custom_distribution + const customSampleSum = CUSTOM_SAMPLES.reduce((acc, a) => acc + a, 0); + const customData = Glean.testOnlyIpc.aCustomDist.testGetValue("store1"); + Assert.equal(customSampleSum, customData.sum, "Sum's correct"); + for (let [bucket, count] of Object.entries(customData.values)) { + Assert.ok( + count == 0 || (count == CUSTOM_SAMPLES.length && bucket == 1), // both values in the low bucket + `Only two buckets have a sample ${bucket} ${count}` + ); + } + const histSnapshot = Telemetry.getSnapshotForHistograms( + "main", + false, + false + ); + const histData = histSnapshot.content.TELEMETRY_TEST_MIRROR_FOR_CUSTOM; + Assert.equal(customSampleSum, histData.sum, "Sum in histogram's correct"); + Assert.equal(2, histData.values["1"], "Two samples in the first bucket"); + + // datetime + // Doesn't work over IPC + + // event + var events = Glean.testOnlyIpc.noExtraEvent.testGetValue(); + Assert.equal(1, events.length); + Assert.equal("test_only.ipc", events[0].category); + Assert.equal("no_extra_event", events[0].name); + + events = Glean.testOnlyIpc.anEvent.testGetValue(); + Assert.equal(1, events.length); + Assert.equal("test_only.ipc", events[0].category); + Assert.equal("an_event", events[0].name); + Assert.deepEqual(EVENT_EXTRA, events[0].extra); + + TelemetryTestUtils.assertEvents( + [ + [ + "telemetry.test", + "not_expired_optout", + "object1", + undefined, + undefined, + ], + ["telemetry.test", "mirror_with_extra", "object1", null, EVENT_EXTRA], + ], + { category: "telemetry.test" }, + { process: "content" } + ); + + // labeled_boolean + // Doesn't work over IPC + + // labeled_counter + const counters = Glean.testOnlyIpc.aLabeledCounter; + Assert.equal(counters.a_label.testGetValue(), A_LABEL_COUNT); + Assert.equal(counters.another_label.testGetValue(), ANOTHER_LABEL_COUNT); + + Assert.throws( + () => Glean.testOnlyIpc.anotherLabeledCounter.__other__.testGetValue(), + /NS_ERROR_LOSS_OF_SIGNIFICANT_DATA/, + "Invalid labels record errors, which throw" + ); + + let value = keyedScalarValue( + "telemetry.test.another_mirror_for_labeled_counter", + "content" + ); + Assert.deepEqual( + { + a_label: A_LABEL_COUNT, + another_label: ANOTHER_LABEL_COUNT, + }, + value + ); + value = keyedScalarValue( + "telemetry.test.mirror_for_labeled_counter", + "content" + ); + Assert.deepEqual( + { + InvalidLabel: INVALID_COUNTERS, + }, + value + ); + + // labeled_string + // Doesn't work over IPC + + // memory_distribution + const memoryData = Glean.testOnlyIpc.aMemoryDist.testGetValue(); + const memorySum = MEMORIES.reduce((acc, a) => acc + a, 0); + // The sum's in bytes, but the metric's in KB + Assert.equal(memorySum * 1024, memoryData.sum); + for (let [bucket, count] of Object.entries(memoryData.values)) { + // We could assert instead, but let's skip to save the logspam. + if (count == 0) { + continue; + } + Assert.ok(count == 1 && MEMORY_BUCKETS.includes(bucket)); + } + + const memoryHist = histSnapshot.content.TELEMETRY_TEST_LINEAR; + Assert.equal( + memorySum, + memoryHist.sum, + "Histogram's in `memory_unit` units" + ); + Assert.equal(2, memoryHist.values["1"], "Samples are in the right bucket"); + + // quantity + // Doesn't work over IPC + + // rate + Assert.deepEqual( + { numerator: IRATE_NUMERATOR, denominator: IRATE_DENOMINATOR }, + Glean.testOnlyIpc.irate.testGetValue() + ); + Assert.deepEqual( + { numerator: IRATE_NUMERATOR, denominator: IRATE_DENOMINATOR }, + keyedScalarValue("telemetry.test.mirror_for_rate", "content") + ); + + // string + // Doesn't work over IPC + + // string_list + // Note: this will break if string list ever rearranges its items. + const cheesyStrings = Glean.testOnlyIpc.aStringList.testGetValue(); + Assert.deepEqual(cheesyStrings, [CHEESY_STRING, CHEESIER_STRING]); + // Note: this will break if keyed scalars rearrange their items. + Assert.deepEqual( + { + [CHEESY_STRING]: true, + [CHEESIER_STRING]: true, + }, + keyedScalarValue("telemetry.test.keyed_boolean_kind", "content") + ); + + // timespan + // Doesn't work over IPC + + // timing_distribution + const NANOS_IN_MILLIS = 1e6; + const EPSILON = 40000; // bug 1701949 + const times = Glean.testOnlyIpc.aTimingDist.testGetValue(); + Assert.greater(times.sum, 15 * NANOS_IN_MILLIS - EPSILON); + // We can't guarantee any specific time values (thank you clocks), + // but we can assert there are only two samples. + Assert.equal( + 2, + Object.entries(times.values).reduce( + (acc, [bucket, count]) => acc + count, + 0 + ) + ); + const timingHist = histSnapshot.content.TELEMETRY_TEST_EXPONENTIAL; + Assert.greaterOrEqual(timingHist.sum, 13, "Histogram's in milliseconds."); + // Both values, 10 and 5, are truncated by a cast in AccumulateTimeDelta + // Minimally downcast 9. + 4. could realistically result in 13. + Assert.equal( + 2, + Object.entries(timingHist.values).reduce( + (acc, [bucket, count]) => acc + count, + 0 + ), + "Only two samples" + ); + + // uuid + // Doesn't work over IPC + } +); diff --git a/toolkit/components/glean/tests/xpcshell/test_Glean.js b/toolkit/components/glean/tests/xpcshell/test_Glean.js new file mode 100644 index 0000000000..cfab9d77ff --- /dev/null +++ b/toolkit/components/glean/tests/xpcshell/test_Glean.js @@ -0,0 +1,405 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); +const { setTimeout } = ChromeUtils.importESModule( + "resource://gre/modules/Timer.sys.mjs" +); + +function sleep(ms) { + /* eslint-disable mozilla/no-arbitrary-setTimeout */ + return new Promise(resolve => setTimeout(resolve, ms)); +} + +add_setup( + /* on Android FOG is set up through head.js */ + { skip_if: () => AppConstants.platform == "android" }, + function test_setup() { + // FOG needs a profile directory to put its data in. + do_get_profile(); + + // We need to initialize it once, otherwise operations will be stuck in the pre-init queue. + Services.fog.initializeFOG(); + } +); + +add_task(function test_fog_counter_works() { + Glean.testOnly.badCode.add(31); + Assert.equal(31, Glean.testOnly.badCode.testGetValue("test-ping")); +}); + +add_task(async function test_fog_string_works() { + const value = "a cheesy string!"; + Glean.testOnly.cheesyString.set(value); + + Assert.equal(value, Glean.testOnly.cheesyString.testGetValue("test-ping")); +}); + +add_task(async function test_fog_string_list_works() { + const value = "a cheesy string!"; + const value2 = "a cheesier string!"; + const value3 = "the cheeziest of strings."; + + const cheeseList = [value, value2]; + Glean.testOnly.cheesyStringList.set(cheeseList); + + let val = Glean.testOnly.cheesyStringList.testGetValue(); + // Note: This is incredibly fragile and will break if we ever rearrange items + // in the string list. + Assert.deepEqual(cheeseList, val); + + Glean.testOnly.cheesyStringList.add(value3); + Assert.ok(Glean.testOnly.cheesyStringList.testGetValue().includes(value3)); +}); + +add_task(async function test_fog_timespan_works() { + Glean.testOnly.canWeTimeIt.start(); + Glean.testOnly.canWeTimeIt.cancel(); + Assert.equal(undefined, Glean.testOnly.canWeTimeIt.testGetValue()); + + // We start, briefly sleep and then stop. + // That guarantees some time to measure. + Glean.testOnly.canWeTimeIt.start(); + await sleep(10); + Glean.testOnly.canWeTimeIt.stop(); + + Assert.ok(Glean.testOnly.canWeTimeIt.testGetValue("test-ping") > 0); +}); + +add_task(async function test_fog_timespan_throws_on_stop_wout_start() { + Glean.testOnly.canWeTimeIt.stop(); + Assert.throws( + () => Glean.testOnly.canWeTimeIt.testGetValue(), + /NS_ERROR_LOSS_OF_SIGNIFICANT_DATA/, + "Should throw because stop was called without start." + ); +}); + +add_task(async function test_fog_uuid_works() { + const kTestUuid = "decafdec-afde-cafd-ecaf-decafdecafde"; + Glean.testOnly.whatIdIt.set(kTestUuid); + Assert.equal(kTestUuid, Glean.testOnly.whatIdIt.testGetValue("test-ping")); + + Glean.testOnly.whatIdIt.generateAndSet(); + // Since we generate v4 UUIDs, and the first character of the third group + // isn't 4, this won't ever collide with kTestUuid. + Assert.notEqual(kTestUuid, Glean.testOnly.whatIdIt.testGetValue("test-ping")); +}); + +add_task(function test_fog_datetime_works() { + const value = new Date("2020-06-11T12:00:00"); + + Glean.testOnly.whatADate.set(value.getTime() * 1000); + + const received = Glean.testOnly.whatADate.testGetValue("test-ping"); + Assert.equal(received.getTime(), value.getTime()); +}); + +add_task(function test_fog_boolean_works() { + Glean.testOnly.canWeFlagIt.set(false); + Assert.equal(false, Glean.testOnly.canWeFlagIt.testGetValue("test-ping")); + // While you're here, might as well test that the ping name's optional. + Assert.equal(false, Glean.testOnly.canWeFlagIt.testGetValue()); +}); + +add_task(async function test_fog_event_works() { + Glean.testOnlyIpc.noExtraEvent.record(); + var events = Glean.testOnlyIpc.noExtraEvent.testGetValue(); + Assert.equal(1, events.length); + Assert.equal("test_only.ipc", events[0].category); + Assert.equal("no_extra_event", events[0].name); + + let extra = { extra1: "can set extras", extra2: "passing more data" }; + Glean.testOnlyIpc.anEvent.record(extra); + events = Glean.testOnlyIpc.anEvent.testGetValue(); + Assert.equal(1, events.length); + Assert.equal("test_only.ipc", events[0].category); + Assert.equal("an_event", events[0].name); + Assert.deepEqual(extra, events[0].extra); + + let extra2 = { + extra1: "can set extras", + extra2: 37, + extra3_longer_name: false, + }; + Glean.testOnlyIpc.eventWithExtra.record(extra2); + events = Glean.testOnlyIpc.eventWithExtra.testGetValue(); + Assert.equal(1, events.length); + Assert.equal("test_only.ipc", events[0].category); + Assert.equal("event_with_extra", events[0].name); + let expectedExtra = { + extra1: "can set extras", + extra2: "37", + extra3_longer_name: "false", + }; + Assert.deepEqual(expectedExtra, events[0].extra); + + // Quantities need to be non-negative. + // This does not record a Glean error. + let extra4 = { + extra2: -1, + }; + Glean.testOnlyIpc.eventWithExtra.record(extra4); + events = Glean.testOnlyIpc.eventWithExtra.testGetValue(); + // Unchanged number of events + Assert.equal(1, events.length, "Recorded one event too many."); + + // Invalid extra keys don't crash, the event is not recorded, + // but an error is recorded. + let extra3 = { + extra1_nonexistent_extra: "this does not crash", + }; + Glean.testOnlyIpc.eventWithExtra.record(extra3); + Assert.throws( + () => Glean.testOnlyIpc.eventWithExtra.testGetValue(), + /NS_ERROR_LOSS_OF_SIGNIFICANT_DATA/, + "Should throw because of a recording error." + ); +}); + +add_task(async function test_fog_memory_distribution_works() { + Glean.testOnly.doYouRemember.accumulate(7); + Glean.testOnly.doYouRemember.accumulate(17); + + let data = Glean.testOnly.doYouRemember.testGetValue("test-ping"); + // `data.sum` is in bytes, but the metric is in MB. + Assert.equal(24 * 1024 * 1024, data.sum, "Sum's correct"); + for (let [bucket, count] of Object.entries(data.values)) { + Assert.ok( + count == 0 || (count == 1 && (bucket == 17520006 || bucket == 7053950)), + "Only two buckets have a sample" + ); + } +}); + +add_task(async function test_fog_custom_distribution_works() { + Glean.testOnlyIpc.aCustomDist.accumulateSamples([7, 268435458]); + + let data = Glean.testOnlyIpc.aCustomDist.testGetValue("store1"); + Assert.equal(7 + 268435458, data.sum, "Sum's correct"); + for (let [bucket, count] of Object.entries(data.values)) { + Assert.ok( + count == 0 || (count == 1 && (bucket == 1 || bucket == 268435456)), + `Only two buckets have a sample ${bucket} ${count}` + ); + } + + // Negative values will not be recorded, instead an error is recorded. + Glean.testOnlyIpc.aCustomDist.accumulateSamples([-7]); + Assert.throws( + () => Glean.testOnlyIpc.aCustomDist.testGetValue(), + /NS_ERROR_LOSS_OF_SIGNIFICANT_DATA/ + ); +}); + +add_task( + /* TODO(bug 1737520): Enable custom ping support on Android */ + { skip_if: () => AppConstants.platform == "android" }, + function test_fog_custom_pings() { + Assert.ok("onePingOnly" in GleanPings); + let submitted = false; + Glean.testOnly.onePingOneBool.set(false); + GleanPings.onePingOnly.testBeforeNextSubmit(reason => { + submitted = true; + Assert.equal(false, Glean.testOnly.onePingOneBool.testGetValue()); + }); + GleanPings.onePingOnly.submit(); + Assert.ok(submitted, "Ping was submitted, callback was called."); + } +); + +add_task(async function test_fog_timing_distribution_works() { + let t1 = Glean.testOnly.whatTimeIsIt.start(); + let t2 = Glean.testOnly.whatTimeIsIt.start(); + + await sleep(5); + + let t3 = Glean.testOnly.whatTimeIsIt.start(); + Glean.testOnly.whatTimeIsIt.cancel(t1); + + await sleep(5); + + Glean.testOnly.whatTimeIsIt.stopAndAccumulate(t2); // 10ms + Glean.testOnly.whatTimeIsIt.stopAndAccumulate(t3); // 5ms + + let data = Glean.testOnly.whatTimeIsIt.testGetValue(); + const NANOS_IN_MILLIS = 1e6; + // bug 1701949 - Sleep gets close, but sometimes doesn't wait long enough. + const EPSILON = 40000; + + // Variance in timing makes getting the sum impossible to know. + Assert.greater(data.sum, 15 * NANOS_IN_MILLIS - EPSILON); + + // No guarantees from timers means no guarantees on buckets. + // But we can guarantee it's only two samples. + Assert.equal( + 2, + Object.entries(data.values).reduce( + (acc, [bucket, count]) => acc + count, + 0 + ), + "Only two buckets with samples" + ); +}); + +add_task(async function test_fog_labels_conform() { + Glean.testOnly.mabelsLabelMaker.singleword.set("portmanteau"); + Assert.equal( + "portmanteau", + Glean.testOnly.mabelsLabelMaker.singleword.testGetValue() + ); + Glean.testOnly.mabelsLabelMaker.snake_case.set("snek"); + Assert.equal( + "snek", + Glean.testOnly.mabelsLabelMaker.snake_case.testGetValue() + ); + Glean.testOnly.mabelsLabelMaker["dash-character"].set("Dash Rendar"); + Assert.equal( + "Dash Rendar", + Glean.testOnly.mabelsLabelMaker["dash-character"].testGetValue() + ); + Glean.testOnly.mabelsLabelMaker["dot.separated"].set("dot product"); + Assert.equal( + "dot product", + Glean.testOnly.mabelsLabelMaker["dot.separated"].testGetValue() + ); + Glean.testOnly.mabelsLabelMaker.camelCase.set("wednesday"); + Assert.throws( + () => Glean.testOnly.mabelsLabelMaker.camelCase.testGetValue(), + /NS_ERROR_LOSS_OF_SIGNIFICANT_DATA/, + "Should throw because of an invalid label." + ); + Assert.throws( + () => Glean.testOnly.mabelsLabelMaker.__other__.testGetValue(), + /NS_ERROR_LOSS_OF_SIGNIFICANT_DATA/, + "Should throw because of an invalid label." + ); + // This test _should_ throw because we are calling data after an invalid label + // has been set. + Assert.throws( + () => Glean.testOnly.mabelsLabelMaker["dot.separated"].testGetValue(), + /NS_ERROR_LOSS_OF_SIGNIFICANT_DATA/, + "Should throw because of an invalid label." + ); +}); + +add_task(async function test_fog_labeled_boolean_works() { + Assert.equal( + undefined, + Glean.testOnly.mabelsLikeBalloons.at_parties.testGetValue(), + "New labels with no values should return undefined" + ); + Glean.testOnly.mabelsLikeBalloons.at_parties.set(true); + Glean.testOnly.mabelsLikeBalloons.at_funerals.set(false); + Assert.equal( + true, + Glean.testOnly.mabelsLikeBalloons.at_parties.testGetValue() + ); + Assert.equal( + false, + Glean.testOnly.mabelsLikeBalloons.at_funerals.testGetValue() + ); + // What about invalid/__other__? + Assert.equal( + undefined, + Glean.testOnly.mabelsLikeBalloons.__other__.testGetValue() + ); + Glean.testOnly.mabelsLikeBalloons.InvalidLabel.set(true); + Assert.throws( + () => Glean.testOnly.mabelsLikeBalloons.__other__.testGetValue(), + /NS_ERROR_LOSS_OF_SIGNIFICANT_DATA/, + "Should throw because of a recording error." + ); +}); + +add_task(async function test_fog_labeled_counter_works() { + Assert.equal( + undefined, + Glean.testOnly.mabelsKitchenCounters.near_the_sink.testGetValue(), + "New labels with no values should return undefined" + ); + Glean.testOnly.mabelsKitchenCounters.near_the_sink.add(1); + Glean.testOnly.mabelsKitchenCounters.with_junk_on_them.add(2); + Assert.equal( + 1, + Glean.testOnly.mabelsKitchenCounters.near_the_sink.testGetValue() + ); + Assert.equal( + 2, + Glean.testOnly.mabelsKitchenCounters.with_junk_on_them.testGetValue() + ); + // What about invalid/__other__? + Assert.equal( + undefined, + Glean.testOnly.mabelsKitchenCounters.__other__.testGetValue() + ); + Glean.testOnly.mabelsKitchenCounters.InvalidLabel.add(1); + Assert.throws( + () => Glean.testOnly.mabelsKitchenCounters.__other__.testGetValue(), + /NS_ERROR_LOSS_OF_SIGNIFICANT_DATA/, + "Should throw because of a recording error." + ); +}); + +add_task(async function test_fog_labeled_string_works() { + Assert.equal( + undefined, + Glean.testOnly.mabelsBalloonStrings.colour_of_99.testGetValue(), + "New labels with no values should return undefined" + ); + Glean.testOnly.mabelsBalloonStrings.colour_of_99.set("crimson"); + Glean.testOnly.mabelsBalloonStrings.string_lengths.set("various"); + Assert.equal( + "crimson", + Glean.testOnly.mabelsBalloonStrings.colour_of_99.testGetValue() + ); + Assert.equal( + "various", + Glean.testOnly.mabelsBalloonStrings.string_lengths.testGetValue() + ); + // What about invalid/__other__? + Assert.equal( + undefined, + Glean.testOnly.mabelsBalloonStrings.__other__.testGetValue() + ); + Glean.testOnly.mabelsBalloonStrings.InvalidLabel.set("valid"); + Assert.throws( + () => Glean.testOnly.mabelsBalloonStrings.__other__.testGetValue(), + /NS_ERROR_LOSS_OF_SIGNIFICANT_DATA/ + ); +}); + +add_task(function test_fog_quantity_works() { + Glean.testOnly.meaningOfLife.set(42); + Assert.equal(42, Glean.testOnly.meaningOfLife.testGetValue()); +}); + +add_task(function test_fog_rate_works() { + // 1) Standard rate with internal denominator + Glean.testOnlyIpc.irate.addToNumerator(22); + Glean.testOnlyIpc.irate.addToDenominator(7); + Assert.deepEqual( + { numerator: 22, denominator: 7 }, + Glean.testOnlyIpc.irate.testGetValue() + ); + + // 2) Rate with external denominator + Glean.testOnlyIpc.anExternalDenominator.add(11); + Glean.testOnlyIpc.rateWithExternalDenominator.addToNumerator(121); + Assert.equal(11, Glean.testOnlyIpc.anExternalDenominator.testGetValue()); + Assert.deepEqual( + { numerator: 121, denominator: 11 }, + Glean.testOnlyIpc.rateWithExternalDenominator.testGetValue() + ); +}); + +add_task(async function test_fog_url_works() { + const value = "https://www.example.com/fog"; + Glean.testOnlyIpc.aUrl.set(value); + + Assert.equal(value, Glean.testOnlyIpc.aUrl.testGetValue("store1")); +}); diff --git a/toolkit/components/glean/tests/xpcshell/test_GleanExperiments.js b/toolkit/components/glean/tests/xpcshell/test_GleanExperiments.js new file mode 100644 index 0000000000..cf8871e9d1 --- /dev/null +++ b/toolkit/components/glean/tests/xpcshell/test_GleanExperiments.js @@ -0,0 +1,28 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// FOG needs a profile directory to put its data in. +do_get_profile(); + +// We need to initialize it once, otherwise operations will be stuck in the pre-init queue. +Services.fog.initializeFOG(); + +add_task(function test_fog_experiment_annotations() { + const id = "my-experiment-id"; + const branch = "my-branch"; + const extra = { extra_key: "extra_value" }; + Services.fog.setExperimentActive(id, branch, extra); + + let data = Services.fog.testGetExperimentData(id); + Assert.equal(data.branch, branch); + Assert.deepEqual(data.extra, extra); + + // Unknown id gets nothing. + Assert.equal(undefined, Services.fog.testGetExperimentData(id + id)); + + // Inactive id gets nothing. + Services.fog.setExperimentInactive(id); + Assert.equal(undefined, Services.fog.testGetExperimentData(id)); +}); diff --git a/toolkit/components/glean/tests/xpcshell/test_GleanIPC.js b/toolkit/components/glean/tests/xpcshell/test_GleanIPC.js new file mode 100644 index 0000000000..182a584841 --- /dev/null +++ b/toolkit/components/glean/tests/xpcshell/test_GleanIPC.js @@ -0,0 +1,157 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); +const { setTimeout } = ChromeUtils.importESModule( + "resource://gre/modules/Timer.sys.mjs" +); + +function sleep(ms) { + /* eslint-disable mozilla/no-arbitrary-setTimeout */ + return new Promise(resolve => setTimeout(resolve, ms)); +} + +add_setup( + /* on Android FOG is set up through head.js */ + { skip_if: () => !runningInParent || AppConstants.platform == "android" }, + function test_setup() { + // Give FOG a temp profile to init within. + do_get_profile(); + + // We need to initialize it once, otherwise operations will be stuck in the pre-init queue. + Services.fog.initializeFOG(); + } +); + +const BAD_CODE_COUNT = 42; +const CHEESY_STRING = "a very cheesy string!"; +const CHEESIER_STRING = "a much cheesier string!"; +const EVENT_EXTRA = { extra1: "so very extra" }; +const MEMORIES = [13, 31]; +const MEMORY_BUCKETS = ["13509772", "32131834"]; // buckets are strings : | +const COUNTERS_NEAR_THE_SINK = 3; +const COUNTERS_WITH_JUNK_ON_THEM = 5; +const INVALID_COUNTERS = 7; + +add_task({ skip_if: () => runningInParent }, async function run_child_stuff() { + Glean.testOnly.badCode.add(BAD_CODE_COUNT); + Glean.testOnly.cheesyStringList.add(CHEESY_STRING); + Glean.testOnly.cheesyStringList.add(CHEESIER_STRING); + + Glean.testOnlyIpc.noExtraEvent.record(); + Glean.testOnlyIpc.anEvent.record(EVENT_EXTRA); + + for (let memory of MEMORIES) { + Glean.testOnly.doYouRemember.accumulate(memory); + } + + let t1 = Glean.testOnly.whatTimeIsIt.start(); + let t2 = Glean.testOnly.whatTimeIsIt.start(); + + await sleep(5); + + let t3 = Glean.testOnly.whatTimeIsIt.start(); + Glean.testOnly.whatTimeIsIt.cancel(t1); + + await sleep(5); + + Glean.testOnly.whatTimeIsIt.stopAndAccumulate(t2); // 10ms + Glean.testOnly.whatTimeIsIt.stopAndAccumulate(t3); // 5ms + + Glean.testOnlyIpc.aCustomDist.accumulateSamples([3, 4]); + + Glean.testOnly.mabelsKitchenCounters.near_the_sink.add( + COUNTERS_NEAR_THE_SINK + ); + Glean.testOnly.mabelsKitchenCounters.with_junk_on_them.add( + COUNTERS_WITH_JUNK_ON_THEM + ); + + Glean.testOnly.mabelsBathroomCounters.InvalidLabel.add(INVALID_COUNTERS); + + Glean.testOnlyIpc.irate.addToNumerator(44); + Glean.testOnlyIpc.irate.addToDenominator(14); +}); + +add_task( + { skip_if: () => !runningInParent }, + async function test_child_metrics() { + await run_test_in_child("test_GleanIPC.js"); + await Services.fog.testFlushAllChildren(); + + Assert.equal(Glean.testOnly.badCode.testGetValue(), BAD_CODE_COUNT); + + // Note: this will break if string list ever rearranges its items. + const cheesyStrings = Glean.testOnly.cheesyStringList.testGetValue(); + Assert.deepEqual(cheesyStrings, [CHEESY_STRING, CHEESIER_STRING]); + + const data = Glean.testOnly.doYouRemember.testGetValue(); + Assert.equal(MEMORIES.reduce((a, b) => a + b, 0) * 1024 * 1024, data.sum); + for (let [bucket, count] of Object.entries(data.values)) { + // We could assert instead, but let's skip to save the logspam. + if (count == 0) { + continue; + } + Assert.ok(count == 1 && MEMORY_BUCKETS.includes(bucket)); + } + + const customData = Glean.testOnlyIpc.aCustomDist.testGetValue("store1"); + Assert.equal(3 + 4, customData.sum, "Sum's correct"); + for (let [bucket, count] of Object.entries(customData.values)) { + Assert.ok( + count == 0 || (count == 2 && bucket == 1), // both values in the low bucket + `Only two buckets have a sample ${bucket} ${count}` + ); + } + + var events = Glean.testOnlyIpc.noExtraEvent.testGetValue(); + Assert.equal(1, events.length); + Assert.equal("test_only.ipc", events[0].category); + Assert.equal("no_extra_event", events[0].name); + + events = Glean.testOnlyIpc.anEvent.testGetValue(); + Assert.equal(1, events.length); + Assert.equal("test_only.ipc", events[0].category); + Assert.equal("an_event", events[0].name); + Assert.deepEqual(EVENT_EXTRA, events[0].extra); + + const NANOS_IN_MILLIS = 1e6; + const EPSILON = 40000; // bug 1701949 + const times = Glean.testOnly.whatTimeIsIt.testGetValue(); + Assert.greater(times.sum, 15 * NANOS_IN_MILLIS - EPSILON); + // We can't guarantee any specific time values (thank you clocks), + // but we can assert there are only two samples. + Assert.equal( + 2, + Object.entries(times.values).reduce( + (acc, [bucket, count]) => acc + count, + 0 + ) + ); + + const mabelsCounters = Glean.testOnly.mabelsKitchenCounters; + Assert.equal( + mabelsCounters.near_the_sink.testGetValue(), + COUNTERS_NEAR_THE_SINK + ); + Assert.equal( + mabelsCounters.with_junk_on_them.testGetValue(), + COUNTERS_WITH_JUNK_ON_THEM + ); + + Assert.throws( + () => Glean.testOnly.mabelsBathroomCounters.__other__.testGetValue(), + /NS_ERROR_LOSS_OF_SIGNIFICANT_DATA/, + "Invalid labels record errors, which throw" + ); + + Assert.deepEqual( + { numerator: 44, denominator: 14 }, + Glean.testOnlyIpc.irate.testGetValue() + ); + } +); diff --git a/toolkit/components/glean/tests/xpcshell/test_JOG.js b/toolkit/components/glean/tests/xpcshell/test_JOG.js new file mode 100644 index 0000000000..c18ce8cb74 --- /dev/null +++ b/toolkit/components/glean/tests/xpcshell/test_JOG.js @@ -0,0 +1,723 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); +const { setTimeout } = ChromeUtils.importESModule( + "resource://gre/modules/Timer.sys.mjs" +); + +function sleep(ms) { + /* eslint-disable mozilla/no-arbitrary-setTimeout */ + return new Promise(resolve => setTimeout(resolve, ms)); +} + +add_task( + /* on Android FOG is set up through head.js */ + { skip_if: () => AppConstants.platform == "android" }, + function test_setup() { + // FOG needs a profile directory to put its data in. + do_get_profile(); + + // We need to initialize it once, otherwise operations will be stuck in the pre-init queue. + Services.fog.initializeFOG(); + } +); + +add_task(function test_jog_counter_works() { + Services.fog.testRegisterRuntimeMetric( + "counter", + "jog_cat", + "jog_counter", + ["test-only"], + `"ping"`, + false + ); + Glean.jogCat.jogCounter.add(53); + Assert.equal(53, Glean.jogCat.jogCounter.testGetValue()); +}); + +add_task(async function test_jog_string_works() { + const value = "an active string!"; + Services.fog.testRegisterRuntimeMetric( + "string", + "jog_cat", + "jog_string", + ["test-only"], + `"ping"`, + false + ); + Glean.jogCat.jogString.set(value); + + Assert.equal(value, Glean.jogCat.jogString.testGetValue()); +}); + +add_task(async function test_jog_string_list_works() { + const value = "an active string!"; + const value2 = "a more active string!"; + const value3 = "the most active of strings."; + Services.fog.testRegisterRuntimeMetric( + "string_list", + "jog_cat", + "jog_string_list", + ["test-only"], + `"ping"`, + false + ); + + const jogList = [value, value2]; + Glean.jogCat.jogStringList.set(jogList); + + let val = Glean.jogCat.jogStringList.testGetValue(); + // Note: This is incredibly fragile and will break if we ever rearrange items + // in the string list. + Assert.deepEqual(jogList, val); + + Glean.jogCat.jogStringList.add(value3); + Assert.ok(Glean.jogCat.jogStringList.testGetValue().includes(value3)); +}); + +add_task(async function test_jog_timespan_works() { + Services.fog.testRegisterRuntimeMetric( + "timespan", + "jog_cat", + "jog_timespan", + ["test-only"], + `"ping"`, + false, + JSON.stringify({ time_unit: "millisecond" }) + ); + Glean.jogCat.jogTimespan.start(); + Glean.jogCat.jogTimespan.cancel(); + Assert.equal(undefined, Glean.jogCat.jogTimespan.testGetValue()); + + // We start, briefly sleep and then stop. + // That guarantees some time to measure. + Glean.jogCat.jogTimespan.start(); + await sleep(10); + Glean.jogCat.jogTimespan.stop(); + + Assert.ok(Glean.jogCat.jogTimespan.testGetValue() > 0); +}); + +add_task(async function test_jog_uuid_works() { + const kTestUuid = "decafdec-afde-cafd-ecaf-decafdecafde"; + Services.fog.testRegisterRuntimeMetric( + "uuid", + "jog_cat", + "jog_uuid", + ["test-only"], + `"ping"`, + false + ); + Glean.jogCat.jogUuid.set(kTestUuid); + Assert.equal(kTestUuid, Glean.jogCat.jogUuid.testGetValue()); + + Glean.jogCat.jogUuid.generateAndSet(); + // Since we generate v4 UUIDs, and the first character of the third group + // isn't 4, this won't ever collide with kTestUuid. + Assert.notEqual(kTestUuid, Glean.jogCat.jogUuid.testGetValue()); +}); + +add_task(function test_jog_datetime_works() { + const value = new Date("2020-06-11T12:00:00"); + Services.fog.testRegisterRuntimeMetric( + "datetime", + "jog_cat", + "jog_datetime", + ["test-only"], + `"ping"`, + false, + JSON.stringify({ time_unit: "nanosecond" }) + ); + + Glean.jogCat.jogDatetime.set(value.getTime() * 1000); + + const received = Glean.jogCat.jogDatetime.testGetValue(); + Assert.equal(received.getTime(), value.getTime()); +}); + +add_task(function test_jog_boolean_works() { + Services.fog.testRegisterRuntimeMetric( + "boolean", + "jog_cat", + "jog_bool", + ["test-only"], + `"ping"`, + false + ); + Glean.jogCat.jogBool.set(false); + Assert.equal(false, Glean.jogCat.jogBool.testGetValue()); +}); + +add_task(async function test_jog_event_works() { + Services.fog.testRegisterRuntimeMetric( + "event", + "jog_cat", + "jog_event_no_extra", + ["test-only"], + `"ping"`, + false + ); + Glean.jogCat.jogEventNoExtra.record(); + var events = Glean.jogCat.jogEventNoExtra.testGetValue(); + Assert.equal(1, events.length); + Assert.equal("jog_cat", events[0].category); + Assert.equal("jog_event_no_extra", events[0].name); + + Services.fog.testRegisterRuntimeMetric( + "event", + "jog_cat", + "jog_event", + ["test-only"], + `"ping"`, + false, + JSON.stringify({ allowed_extra_keys: ["extra1", "extra2"] }) + ); + let extra = { extra1: "can set extras", extra2: "passing more data" }; + Glean.jogCat.jogEvent.record(extra); + events = Glean.jogCat.jogEvent.testGetValue(); + Assert.equal(1, events.length); + Assert.equal("jog_cat", events[0].category); + Assert.equal("jog_event", events[0].name); + Assert.deepEqual(extra, events[0].extra); + + Services.fog.testRegisterRuntimeMetric( + "event", + "jog_cat", + "jog_event_with_extra", + ["test-only"], + `"ping"`, + false, + JSON.stringify({ + allowed_extra_keys: ["extra1", "extra2", "extra3_longer_name"], + }) + ); + let extra2 = { + extra1: "can set extras", + extra2: 37, + extra3_longer_name: false, + }; + Glean.jogCat.jogEventWithExtra.record(extra2); + events = Glean.jogCat.jogEventWithExtra.testGetValue(); + Assert.equal(1, events.length); + Assert.equal("jog_cat", events[0].category); + Assert.equal("jog_event_with_extra", events[0].name); + let expectedExtra = { + extra1: "can set extras", + extra2: "37", + extra3_longer_name: "false", + }; + Assert.deepEqual(expectedExtra, events[0].extra); + + // Quantities need to be non-negative. + let extra4 = { + extra2: -1, + }; + Glean.jogCat.jogEventWithExtra.record(extra4); + events = Glean.jogCat.jogEventWithExtra.testGetValue(); + Assert.equal(1, events.length, "Recorded one event too many."); + + // Invalid extra keys don't crash, the event is not recorded. + let extra3 = { + extra1_nonexistent_extra: "this does not crash", + }; + Glean.jogCat.jogEventWithExtra.record(extra3); + // And test methods throw appropriately + Assert.throws( + () => Glean.jogCat.jogEventWithExtra.testGetValue(), + /NS_ERROR_LOSS_OF_SIGNIFICANT_DATA/ + ); +}); + +add_task(async function test_jog_memory_distribution_works() { + Services.fog.testRegisterRuntimeMetric( + "memory_distribution", + "jog_cat", + "jog_memory_dist", + ["test-only"], + `"ping"`, + false, + JSON.stringify({ memory_unit: "megabyte" }) + ); + Glean.jogCat.jogMemoryDist.accumulate(7); + Glean.jogCat.jogMemoryDist.accumulate(17); + + let data = Glean.jogCat.jogMemoryDist.testGetValue(); + // `data.sum` is in bytes, but the metric is in MB. + Assert.equal(24 * 1024 * 1024, data.sum, "Sum's correct"); + for (let [bucket, count] of Object.entries(data.values)) { + Assert.ok( + count == 0 || (count == 1 && (bucket == 17520006 || bucket == 7053950)), + "Only two buckets have a sample" + ); + } +}); + +add_task(async function test_jog_custom_distribution_works() { + Services.fog.testRegisterRuntimeMetric( + "custom_distribution", + "jog_cat", + "jog_custom_dist", + ["test-only"], + `"ping"`, + false, + JSON.stringify({ + range_min: 1, + range_max: 2147483646, + bucket_count: 10, + histogram_type: "linear", + }) + ); + Glean.jogCat.jogCustomDist.accumulateSamples([7, 268435458]); + + let data = Glean.jogCat.jogCustomDist.testGetValue(); + Assert.equal(7 + 268435458, data.sum, "Sum's correct"); + for (let [bucket, count] of Object.entries(data.values)) { + Assert.ok( + count == 0 || (count == 1 && (bucket == 1 || bucket == 268435456)), + `Only two buckets have a sample ${bucket} ${count}` + ); + } + + // Negative values will not be recorded, instead an error is recorded. + Glean.jogCat.jogCustomDist.accumulateSamples([-7]); + Assert.throws( + () => Glean.jogCat.jogCustomDist.testGetValue(), + /NS_ERROR_LOSS_OF_SIGNIFICANT_DATA/ + ); +}); + +add_task( + /* TODO: Enable custom ping support on Android */ + { skip_if: () => AppConstants.platform == "android" }, + async function test_jog_custom_pings() { + Services.fog.testRegisterRuntimeMetric( + "boolean", + "jog_cat", + "jog_ping_bool", + ["jog-ping"], + `"ping"`, + false + ); + Services.fog.testRegisterRuntimePing("jog-ping", true, true, []); + Assert.ok("jogPing" in GleanPings); + let submitted = false; + Glean.jogCat.jogPingBool.set(false); + GleanPings.jogPing.testBeforeNextSubmit(reason => { + submitted = true; + Assert.equal(false, Glean.jogCat.jogPingBool.testGetValue()); + }); + GleanPings.jogPing.submit(); + Assert.ok(submitted, "Ping was submitted, callback was called."); + // ping-lifetime value was cleared. + Assert.equal(undefined, Glean.jogCat.jogPingBool.testGetValue()); + } +); + +add_task(async function test_jog_timing_distribution_works() { + Services.fog.testRegisterRuntimeMetric( + "timing_distribution", + "jog_cat", + "jog_timing_dist", + ["test-only"], + `"ping"`, + false, + JSON.stringify({ time_unit: "microsecond" }) + ); + let t1 = Glean.jogCat.jogTimingDist.start(); + let t2 = Glean.jogCat.jogTimingDist.start(); + + await sleep(5); + + let t3 = Glean.jogCat.jogTimingDist.start(); + Glean.jogCat.jogTimingDist.cancel(t1); + + await sleep(5); + + Glean.jogCat.jogTimingDist.stopAndAccumulate(t2); // 10ms + Glean.jogCat.jogTimingDist.stopAndAccumulate(t3); // 5ms + + let data = Glean.jogCat.jogTimingDist.testGetValue(); + const NANOS_IN_MILLIS = 1e6; + // bug 1701949 - Sleep gets close, but sometimes doesn't wait long enough. + const EPSILON = 40000; + + // Variance in timing makes getting the sum impossible to know. + Assert.greater(data.sum, 15 * NANOS_IN_MILLIS - EPSILON); + + // No guarantees from timers means no guarantees on buckets. + // But we can guarantee it's only two samples. + Assert.equal( + 2, + Object.entries(data.values).reduce( + (acc, [bucket, count]) => acc + count, + 0 + ), + "Only two buckets with samples" + ); +}); + +add_task(async function test_jog_labeled_boolean_works() { + Services.fog.testRegisterRuntimeMetric( + "labeled_boolean", + "jog_cat", + "jog_labeled_bool", + ["test-only"], + `"ping"`, + false + ); + Assert.equal( + undefined, + Glean.jogCat.jogLabeledBool.label_1.testGetValue(), + "New labels with no values should return undefined" + ); + Glean.jogCat.jogLabeledBool.label_1.set(true); + Glean.jogCat.jogLabeledBool.label_2.set(false); + Assert.equal(true, Glean.jogCat.jogLabeledBool.label_1.testGetValue()); + Assert.equal(false, Glean.jogCat.jogLabeledBool.label_2.testGetValue()); + // What about invalid/__other__? + Assert.equal(undefined, Glean.jogCat.jogLabeledBool.__other__.testGetValue()); + Glean.jogCat.jogLabeledBool.InvalidLabel.set(true); + Assert.throws( + () => Glean.jogCat.jogLabeledBool.__other__.testGetValue(), + /NS_ERROR_LOSS_OF_SIGNIFICANT_DATA/, + "Should throw because of a recording error." + ); +}); + +add_task(async function test_jog_labeled_boolean_with_static_labels_works() { + Services.fog.testRegisterRuntimeMetric( + "labeled_boolean", + "jog_cat", + "jog_labeled_bool_with_labels", + ["test-only"], + `"ping"`, + false, + JSON.stringify({ labels: ["label_1", "label_2"] }) + ); + Assert.equal( + undefined, + Glean.jogCat.jogLabeledBoolWithLabels.label_1.testGetValue(), + "New labels with no values should return undefined" + ); + Glean.jogCat.jogLabeledBoolWithLabels.label_1.set(true); + Glean.jogCat.jogLabeledBoolWithLabels.label_2.set(false); + Assert.equal( + true, + Glean.jogCat.jogLabeledBoolWithLabels.label_1.testGetValue() + ); + Assert.equal( + false, + Glean.jogCat.jogLabeledBoolWithLabels.label_2.testGetValue() + ); + // What about invalid/__other__? + Assert.equal( + undefined, + Glean.jogCat.jogLabeledBoolWithLabels.__other__.testGetValue() + ); + Glean.jogCat.jogLabeledBoolWithLabels.label_3.set(true); + Assert.equal( + true, + Glean.jogCat.jogLabeledBoolWithLabels.__other__.testGetValue() + ); + // TODO: Test that we have the right number and type of errors (bug 1683171) +}); + +add_task(async function test_jog_labeled_counter_works() { + Services.fog.testRegisterRuntimeMetric( + "labeled_counter", + "jog_cat", + "jog_labeled_counter", + ["test-only"], + `"ping"`, + false + ); + Assert.equal( + undefined, + Glean.jogCat.jogLabeledCounter.label_1.testGetValue(), + "New labels with no values should return undefined" + ); + Glean.jogCat.jogLabeledCounter.label_1.add(1); + Glean.jogCat.jogLabeledCounter.label_2.add(2); + Assert.equal(1, Glean.jogCat.jogLabeledCounter.label_1.testGetValue()); + Assert.equal(2, Glean.jogCat.jogLabeledCounter.label_2.testGetValue()); + // What about invalid/__other__? + Assert.equal( + undefined, + Glean.jogCat.jogLabeledCounter.__other__.testGetValue() + ); + Glean.jogCat.jogLabeledCounter.InvalidLabel.add(1); + Assert.throws( + () => Glean.jogCat.jogLabeledCounter.__other__.testGetValue(), + /NS_ERROR_LOSS_OF_SIGNIFICANT_DATA/, + "Should throw because of a recording error." + ); +}); + +add_task(async function test_jog_labeled_counter_with_static_labels_works() { + Services.fog.testRegisterRuntimeMetric( + "labeled_counter", + "jog_cat", + "jog_labeled_counter_with_labels", + ["test-only"], + `"ping"`, + false, + JSON.stringify({ labels: ["label_1", "label_2"] }) + ); + Assert.equal( + undefined, + Glean.jogCat.jogLabeledCounterWithLabels.label_1.testGetValue(), + "New labels with no values should return undefined" + ); + Glean.jogCat.jogLabeledCounterWithLabels.label_1.add(1); + Glean.jogCat.jogLabeledCounterWithLabels.label_2.add(2); + Assert.equal( + 1, + Glean.jogCat.jogLabeledCounterWithLabels.label_1.testGetValue() + ); + Assert.equal( + 2, + Glean.jogCat.jogLabeledCounterWithLabels.label_2.testGetValue() + ); + // What about invalid/__other__? + Assert.equal( + undefined, + Glean.jogCat.jogLabeledCounterWithLabels.__other__.testGetValue() + ); + Glean.jogCat.jogLabeledCounterWithLabels.InvalidLabel.add(1); + // TODO:(bug 1766515) - This should throw. + /*Assert.throws( + () => Glean.jogCat.jogLabeledCounterWithLabels.__other__.testGetValue(), + /NS_ERROR_LOSS_OF_SIGNIFICANT_DATA/, + "Should throw because of a recording error." + );*/ + Assert.equal( + 1, + Glean.jogCat.jogLabeledCounterWithLabels.__other__.testGetValue() + ); +}); + +add_task(async function test_jog_labeled_string_works() { + Services.fog.testRegisterRuntimeMetric( + "labeled_string", + "jog_cat", + "jog_labeled_string", + ["test-only"], + `"ping"`, + false + ); + Assert.equal( + undefined, + Glean.jogCat.jogLabeledString.label_1.testGetValue(), + "New labels with no values should return undefined" + ); + Glean.jogCat.jogLabeledString.label_1.set("crimson"); + Glean.jogCat.jogLabeledString.label_2.set("various"); + Assert.equal("crimson", Glean.jogCat.jogLabeledString.label_1.testGetValue()); + Assert.equal("various", Glean.jogCat.jogLabeledString.label_2.testGetValue()); + // What about invalid/__other__? + Assert.equal( + undefined, + Glean.jogCat.jogLabeledString.__other__.testGetValue() + ); + Glean.jogCat.jogLabeledString.InvalidLabel.set("valid"); + Assert.throws( + () => Glean.jogCat.jogLabeledString.__other__.testGetValue(), + /NS_ERROR_LOSS_OF_SIGNIFICANT_DATA/ + ); +}); + +add_task(async function test_jog_labeled_string_with_labels_works() { + Services.fog.testRegisterRuntimeMetric( + "labeled_string", + "jog_cat", + "jog_labeled_string_with_labels", + ["test-only"], + `"ping"`, + false, + JSON.stringify({ labels: ["label_1", "label_2"] }) + ); + Assert.equal( + undefined, + Glean.jogCat.jogLabeledStringWithLabels.label_1.testGetValue(), + "New labels with no values should return undefined" + ); + Glean.jogCat.jogLabeledStringWithLabels.label_1.set("crimson"); + Glean.jogCat.jogLabeledStringWithLabels.label_2.set("various"); + Assert.equal( + "crimson", + Glean.jogCat.jogLabeledStringWithLabels.label_1.testGetValue() + ); + Assert.equal( + "various", + Glean.jogCat.jogLabeledStringWithLabels.label_2.testGetValue() + ); + // What about invalid/__other__? + Assert.equal( + undefined, + Glean.jogCat.jogLabeledStringWithLabels.__other__.testGetValue() + ); + Glean.jogCat.jogLabeledStringWithLabels.InvalidLabel.set("valid"); + // TODO:(bug 1766515) - This should throw. + /*Assert.throws( + () => Glean.jogCat.jogLabeledStringWithLabels.__other__.testGetValue(), + /NS_ERROR_LOSS_OF_SIGNIFICANT_DATA/ + );*/ + Assert.equal( + "valid", + Glean.jogCat.jogLabeledStringWithLabels.__other__.testGetValue() + ); +}); + +add_task(function test_jog_quantity_works() { + Services.fog.testRegisterRuntimeMetric( + "quantity", + "jog_cat", + "jog_quantity", + ["test-only"], + `"ping"`, + false + ); + Glean.jogCat.jogQuantity.set(42); + Assert.equal(42, Glean.jogCat.jogQuantity.testGetValue()); +}); + +add_task(function test_jog_rate_works() { + Services.fog.testRegisterRuntimeMetric( + "rate", + "jog_cat", + "jog_rate", + ["test-only"], + `"ping"`, + false + ); + // 1) Standard rate with internal denominator + Glean.jogCat.jogRate.addToNumerator(22); + Glean.jogCat.jogRate.addToDenominator(7); + Assert.deepEqual( + { numerator: 22, denominator: 7 }, + Glean.jogCat.jogRate.testGetValue() + ); + + Services.fog.testRegisterRuntimeMetric( + "denominator", + "jog_cat", + "jog_denominator", + ["test-only"], + `"ping"`, + false, + JSON.stringify({ + numerators: [ + { + name: "jog_rate_ext", + category: "jog_cat", + send_in_pings: ["test-only"], + lifetime: "ping", + disabled: false, + }, + ], + }) + ); + Services.fog.testRegisterRuntimeMetric( + "rate", + "jog_cat", + "jog_rate_ext", + ["test-only"], + `"ping"`, + false + ); + // 2) Rate with external denominator + Glean.jogCat.jogDenominator.add(11); + Glean.jogCat.jogRateExt.addToNumerator(121); + Assert.equal(11, Glean.jogCat.jogDenominator.testGetValue()); + Assert.deepEqual( + { numerator: 121, denominator: 11 }, + Glean.jogCat.jogRateExt.testGetValue() + ); +}); + +add_task(function test_jog_dotted_categories_work() { + Services.fog.testRegisterRuntimeMetric( + "counter", + "jog_cat.dotted", + "jog_counter", + ["test-only"], + `"ping"`, + false + ); + Glean.jogCatDotted.jogCounter.add(314); + Assert.equal(314, Glean.jogCatDotted.jogCounter.testGetValue()); +}); + +add_task( + /* TODO: Enable custom ping support on Android */ + { skip_if: () => AppConstants.platform == "android" }, + async function test_jog_ping_works() { + const kReason = "reason-1"; + Services.fog.testRegisterRuntimePing("my-ping", true, true, [kReason]); + let submitted = false; + GleanPings.myPing.testBeforeNextSubmit(reason => { + submitted = true; + Assert.equal(kReason, reason); + }); + GleanPings.myPing.submit("reason-1"); + Assert.ok(submitted, "Ping must have been submitted"); + } +); + +add_task(function test_jog_name_collision() { + Assert.ok("aCounter" in Glean.testOnlyJog); + Assert.equal(undefined, Glean.testOnlyJog.aCounter.testGetValue()); + const kValue = 42; + Glean.testOnlyJog.aCounter.add(kValue); + Assert.equal(kValue, Glean.testOnlyJog.aCounter.testGetValue()); + + // Let's overwrite the test_only.jog.a_counter counter. + Services.fog.testRegisterRuntimeMetric( + "counter", + "test_only.jog", + "a_counter", + ["store1"], + `"ping"`, + true // changing the metric to disabled. + ); + + Assert.ok("aCounter" in Glean.testOnlyJog); + Assert.equal(kValue, Glean.testOnlyJog.aCounter.testGetValue()); + Glean.testOnlyJog.aCounter.add(kValue); + Assert.equal( + kValue, + Glean.testOnlyJog.aCounter.testGetValue(), + "value of now-disabled metric remains unchanged." + ); + + // Now let's mess with events: + Assert.ok("anEvent" in Glean.testOnlyJog); + Assert.equal(undefined, Glean.testOnlyJog.anEvent.testGetValue()); + const extra12 = { + extra1: "a value", + extra2: "another value", + }; + Glean.testOnlyJog.anEvent.record(extra12); + Assert.deepEqual(extra12, Glean.testOnlyJog.anEvent.testGetValue()[0].extra); + Services.fog.testRegisterRuntimeMetric( + "event", + "test_only.jog", + "an_event", + ["store1"], + `"ping"`, + false, + JSON.stringify({ allowed_extra_keys: ["extra1", "extra2", "extra3"] }) // New extra key just dropped + ); + const extra123 = { + extra1: "different value", + extra2: "another different value", + extra3: 42, + }; + Glean.testOnlyJog.anEvent.record(extra123); + Assert.deepEqual(extra123, Glean.testOnlyJog.anEvent.testGetValue()[1].extra); +}); diff --git a/toolkit/components/glean/tests/xpcshell/test_JOGIPC.js b/toolkit/components/glean/tests/xpcshell/test_JOGIPC.js new file mode 100644 index 0000000000..2df5a934ba --- /dev/null +++ b/toolkit/components/glean/tests/xpcshell/test_JOGIPC.js @@ -0,0 +1,260 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); +const { setTimeout } = ChromeUtils.importESModule( + "resource://gre/modules/Timer.sys.mjs" +); + +function sleep(ms) { + /* eslint-disable mozilla/no-arbitrary-setTimeout */ + return new Promise(resolve => setTimeout(resolve, ms)); +} + +add_task( + /* on Android FOG is set up through head.js */ + { skip_if: () => !runningInParent || AppConstants.platform == "android" }, + function test_setup() { + // Give FOG a temp profile to init within. + do_get_profile(); + + // We need to initialize it once, otherwise operations will be stuck in the pre-init queue. + Services.fog.initializeFOG(); + } +); + +const COUNT = 42; +const STRING = "a string!"; +const ANOTHER_STRING = "another string!"; +const EVENT_EXTRA = { extra1: "so very extra" }; +const MEMORIES = [13, 31]; +const MEMORY_BUCKETS = ["13509772", "32131834"]; // buckets are strings : | +const COUNTERS_1 = 3; +const COUNTERS_2 = 5; +const INVALID_COUNTERS = 7; + +// It is CRUCIAL that we register metrics in the same order in the parent and +// in the child or their metric ids will not line up and ALL WILL EXPLODE. +const METRICS = [ + ["counter", "jog_ipc", "jog_counter", ["test-only"], `"ping"`, false], + ["string_list", "jog_ipc", "jog_string_list", ["test-only"], `"ping"`, false], + ["event", "jog_ipc", "jog_event_no_extra", ["test-only"], `"ping"`, false], + [ + "event", + "jog_ipc", + "jog_event", + ["test-only"], + `"ping"`, + false, + JSON.stringify({ allowed_extra_keys: ["extra1"] }), + ], + [ + "memory_distribution", + "jog_ipc", + "jog_memory_dist", + ["test-only"], + `"ping"`, + false, + JSON.stringify({ memory_unit: "megabyte" }), + ], + [ + "timing_distribution", + "jog_ipc", + "jog_timing_dist", + ["test-only"], + `"ping"`, + false, + JSON.stringify({ time_unit: "nanosecond" }), + ], + [ + "custom_distribution", + "jog_ipc", + "jog_custom_dist", + ["test-only"], + `"ping"`, + false, + JSON.stringify({ + range_min: 1, + range_max: 2147483646, + bucket_count: 10, + histogram_type: "linear", + }), + ], + [ + "labeled_counter", + "jog_ipc", + "jog_labeled_counter", + ["test-only"], + `"ping"`, + false, + ], + [ + "labeled_counter", + "jog_ipc", + "jog_labeled_counter_err", + ["test-only"], + `"ping"`, + false, + ], + [ + "labeled_counter", + "jog_ipc", + "jog_labeled_counter_with_labels", + ["test-only"], + `"ping"`, + false, + JSON.stringify({ labels: ["label_1", "label_2"] }), + ], + [ + "labeled_counter", + "jog_ipc", + "jog_labeled_counter_with_labels_err", + ["test-only"], + `"ping"`, + false, + JSON.stringify({ labels: ["label_1", "label_2"] }), + ], + ["rate", "jog_ipc", "jog_rate", ["test-only"], `"ping"`, false], +]; + +add_task({ skip_if: () => runningInParent }, async function run_child_stuff() { + for (let metric of METRICS) { + Services.fog.testRegisterRuntimeMetric(...metric); + } + Glean.jogIpc.jogCounter.add(COUNT); + Glean.jogIpc.jogStringList.add(STRING); + Glean.jogIpc.jogStringList.add(ANOTHER_STRING); + + Glean.jogIpc.jogEventNoExtra.record(); + Glean.jogIpc.jogEvent.record(EVENT_EXTRA); + + for (let memory of MEMORIES) { + Glean.jogIpc.jogMemoryDist.accumulate(memory); + } + + let t1 = Glean.jogIpc.jogTimingDist.start(); + let t2 = Glean.jogIpc.jogTimingDist.start(); + + await sleep(5); + + let t3 = Glean.jogIpc.jogTimingDist.start(); + Glean.jogIpc.jogTimingDist.cancel(t1); + + await sleep(5); + + Glean.jogIpc.jogTimingDist.stopAndAccumulate(t2); // 10ms + Glean.jogIpc.jogTimingDist.stopAndAccumulate(t3); // 5ms + + Glean.jogIpc.jogCustomDist.accumulateSamples([3, 4]); + + Glean.jogIpc.jogLabeledCounter.label_1.add(COUNTERS_1); + Glean.jogIpc.jogLabeledCounter.label_2.add(COUNTERS_2); + + Glean.jogIpc.jogLabeledCounterErr.InvalidLabel.add(INVALID_COUNTERS); + + Glean.jogIpc.jogLabeledCounterWithLabels.label_1.add(COUNTERS_1); + Glean.jogIpc.jogLabeledCounterWithLabels.label_2.add(COUNTERS_2); + + Glean.jogIpc.jogLabeledCounterWithLabelsErr.InvalidLabel.add( + INVALID_COUNTERS + ); + + Glean.jogIpc.jogRate.addToNumerator(44); + Glean.jogIpc.jogRate.addToDenominator(14); +}); + +add_task( + { skip_if: () => !runningInParent }, + async function test_child_metrics() { + for (let metric of METRICS) { + Services.fog.testRegisterRuntimeMetric(...metric); + } + await run_test_in_child("test_JOGIPC.js"); + await Services.fog.testFlushAllChildren(); + + Assert.equal(Glean.jogIpc.jogCounter.testGetValue(), COUNT); + + // Note: this will break if string list ever rearranges its items. + const strings = Glean.jogIpc.jogStringList.testGetValue(); + Assert.deepEqual(strings, [STRING, ANOTHER_STRING]); + + const data = Glean.jogIpc.jogMemoryDist.testGetValue(); + Assert.equal(MEMORIES.reduce((a, b) => a + b, 0) * 1024 * 1024, data.sum); + for (let [bucket, count] of Object.entries(data.values)) { + // We could assert instead, but let's skip to save the logspam. + if (count == 0) { + continue; + } + Assert.ok(count == 1 && MEMORY_BUCKETS.includes(bucket)); + } + + const customData = Glean.jogIpc.jogCustomDist.testGetValue(); + Assert.equal(3 + 4, customData.sum, "Sum's correct"); + for (let [bucket, count] of Object.entries(customData.values)) { + Assert.ok( + count == 0 || (count == 2 && bucket == 1), // both values in the low bucket + `Only two buckets have a sample ${bucket} ${count}` + ); + } + + let events = Glean.jogIpc.jogEventNoExtra.testGetValue(); + Assert.equal(1, events.length); + Assert.equal("jog_ipc", events[0].category); + Assert.equal("jog_event_no_extra", events[0].name); + + events = Glean.jogIpc.jogEvent.testGetValue(); + Assert.equal(1, events.length); + Assert.equal("jog_ipc", events[0].category); + Assert.equal("jog_event", events[0].name); + Assert.deepEqual(EVENT_EXTRA, events[0].extra); + + const NANOS_IN_MILLIS = 1e6; + const EPSILON = 40000; // bug 1701949 + const times = Glean.jogIpc.jogTimingDist.testGetValue(); + Assert.greater(times.sum, 15 * NANOS_IN_MILLIS - EPSILON); + // We can't guarantee any specific time values (thank you clocks), + // but we can assert there are only two samples. + Assert.equal( + 2, + Object.entries(times.values).reduce( + (acc, [bucket, count]) => acc + count, + 0 + ) + ); + + const labeledCounter = Glean.jogIpc.jogLabeledCounter; + Assert.equal(labeledCounter.label_1.testGetValue(), COUNTERS_1); + Assert.equal(labeledCounter.label_2.testGetValue(), COUNTERS_2); + + Assert.throws( + () => Glean.jogIpc.jogLabeledCounterErr.__other__.testGetValue(), + /NS_ERROR_LOSS_OF_SIGNIFICANT_DATA/, + "Invalid labels record errors, which throw" + ); + + const labeledCounterWLabels = Glean.jogIpc.jogLabeledCounterWithLabels; + Assert.equal(labeledCounterWLabels.label_1.testGetValue(), COUNTERS_1); + Assert.equal(labeledCounterWLabels.label_2.testGetValue(), COUNTERS_2); + + // TODO:(bug 1766515) - This should throw. + /*Assert.throws( + () => + Glean.jogIpc.jogLabeledCounterWithLabelsErr.__other__.testGetValue(), + /NS_ERROR_LOSS_OF_SIGNIFICANT_DATA/, + "Invalid labels record errors, which throw" + );*/ + Assert.equal( + Glean.jogIpc.jogLabeledCounterWithLabelsErr.__other__.testGetValue(), + INVALID_COUNTERS + ); + + Assert.deepEqual( + { numerator: 44, denominator: 14 }, + Glean.jogIpc.jogRate.testGetValue() + ); + } +); diff --git a/toolkit/components/glean/tests/xpcshell/test_MillionQ.js b/toolkit/components/glean/tests/xpcshell/test_MillionQ.js new file mode 100644 index 0000000000..d98e73b451 --- /dev/null +++ b/toolkit/components/glean/tests/xpcshell/test_MillionQ.js @@ -0,0 +1,19 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(function test_queue_longer_than_1k() { + // FOG needs a profile directory to put its data in. + do_get_profile(); + + // Before init, try and fill the preinit queue with > 1000 tasks. + const kIterations = 2000; + for (let _i = 0; _i < kIterations; _i++) { + Glean.testOnly.badCode.add(1); + } + + Services.fog.initializeFOG(); + + Assert.equal(kIterations, Glean.testOnly.badCode.testGetValue()); +}); diff --git a/toolkit/components/glean/tests/xpcshell/xpcshell.ini b/toolkit/components/glean/tests/xpcshell/xpcshell.ini new file mode 100644 index 0000000000..01c527d865 --- /dev/null +++ b/toolkit/components/glean/tests/xpcshell/xpcshell.ini @@ -0,0 +1,30 @@ +# Please keep test files lexicographically sorted, with whitespace between. +[DEFAULT] +firefox-appdir = browser +head = head.js + +[test_FOGInit.js] + +[test_FOGIPCLimit.js] + +[test_FOGPrefs.js] +skip-if = os == "android" # FOG isn't responsible for monitoring prefs and controlling upload on Android + +[test_GIFFT.js] +run-sequentially = very high failure rate in parallel + +[test_GIFFTIPC.js] + +[test_Glean.js] + +[test_GleanIPC.js] + +[test_GleanExperiments.js] +skip-if = os == "android" # FOG isn't responsible for experiment annotations on Android + +[test_JOG.js] + +[test_JOGIPC.js] + +[test_MillionQ.js] +skip-if = os == "android" # Android inits its own FOG, so the test won't work. diff --git a/toolkit/components/glean/xpcom/FOG.cpp b/toolkit/components/glean/xpcom/FOG.cpp new file mode 100644 index 0000000000..a113ae86d9 --- /dev/null +++ b/toolkit/components/glean/xpcom/FOG.cpp @@ -0,0 +1,363 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/FOG.h" + +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/FOGIPC.h" +#include "mozilla/glean/bindings/Common.h" +#include "mozilla/glean/bindings/jog/jog_ffi_generated.h" +#include "mozilla/glean/fog_ffi_generated.h" +#include "mozilla/glean/GleanMetrics.h" +#include "mozilla/MozPromise.h" +#include "mozilla/ShutdownPhase.h" +#include "mozilla/Unused.h" +#include "nsContentUtils.h" +#include "nsIFOG.h" +#include "nsIUserIdleService.h" +#include "nsServiceManagerUtils.h" + +namespace mozilla { + +using glean::LogToBrowserConsole; + +#ifdef MOZ_GLEAN_ANDROID +// Defined by `glean-core`. We reexport it here for later use. +extern "C" NS_EXPORT void glean_enable_logging(void); + +// Workaround to force a re-export of the `no_mangle` symbols from `glean-core` +// +// Due to how linking works and hides symbols the symbols from `glean-core` +// might not be re-exported and thus not usable. By forcing use of _at least +// one_ symbol in an exported function the functions will also be rexported. +// +// See also https://github.com/rust-lang/rust/issues/50007 +extern "C" NS_EXPORT void _fog_force_reexport_donotcall(void) { + glean_enable_logging(); +} +#endif + +static StaticRefPtr<FOG> gFOG; + +// We wait for 5s of idle before dumping IPC and flushing ping data to disk. +// This number hasn't been tuned, so if you have a reason to change it, +// please by all means do. +const uint32_t kIdleSecs = 5; + +// static +already_AddRefed<FOG> FOG::GetSingleton() { + if (gFOG) { + return do_AddRef(gFOG); + } + + gFOG = new FOG(); + + if (XRE_IsParentProcess()) { + nsresult rv; + nsCOMPtr<nsIUserIdleService> idleService = + do_GetService("@mozilla.org/widget/useridleservice;1", &rv); + NS_ENSURE_SUCCESS(rv, nullptr); + MOZ_ASSERT(idleService); + if (NS_WARN_IF(NS_FAILED(idleService->AddIdleObserver(gFOG, kIdleSecs)))) { + glean::fog::failed_idle_registration.Set(true); + } + + RunOnShutdown( + [&] { + nsresult rv; + nsCOMPtr<nsIUserIdleService> idleService = + do_GetService("@mozilla.org/widget/useridleservice;1", &rv); + if (NS_SUCCEEDED(rv)) { + MOZ_ASSERT(idleService); + Unused << idleService->RemoveIdleObserver(gFOG, kIdleSecs); + } + gFOG->Shutdown(); + gFOG = nullptr; + }, + ShutdownPhase::XPCOMShutdown); + } + return do_AddRef(gFOG); +} + +void FOG::Shutdown() { + MOZ_ASSERT(XRE_IsParentProcess()); + glean::impl::fog_shutdown(); +} + +NS_IMETHODIMP +FOG::InitializeFOG(const nsACString& aDataPathOverride, + const nsACString& aAppIdOverride) { + MOZ_ASSERT(XRE_IsParentProcess()); + return glean::impl::fog_init(&aDataPathOverride, &aAppIdOverride); +} + +NS_IMETHODIMP +FOG::RegisterCustomPings() { + MOZ_ASSERT(XRE_IsParentProcess()); + glean::impl::fog_register_pings(); + return NS_OK; +} + +NS_IMETHODIMP +FOG::SetLogPings(bool aEnableLogPings) { +#ifdef MOZ_GLEAN_ANDROID + return NS_OK; +#else + MOZ_ASSERT(XRE_IsParentProcess()); + return glean::impl::fog_set_log_pings(aEnableLogPings); +#endif +} + +NS_IMETHODIMP +FOG::SetTagPings(const nsACString& aDebugTag) { +#ifdef MOZ_GLEAN_ANDROID + return NS_OK; +#else + MOZ_ASSERT(XRE_IsParentProcess()); + return glean::impl::fog_set_debug_view_tag(&aDebugTag); +#endif +} + +NS_IMETHODIMP +FOG::SendPing(const nsACString& aPingName) { +#ifdef MOZ_GLEAN_ANDROID + return NS_OK; +#else + MOZ_ASSERT(XRE_IsParentProcess()); + return glean::impl::fog_submit_ping(&aPingName); +#endif +} + +NS_IMETHODIMP +FOG::SetExperimentActive(const nsACString& aExperimentId, + const nsACString& aBranch, JS::HandleValue aExtra, + JSContext* aCx) { +#ifdef MOZ_GLEAN_ANDROID + NS_WARNING("Don't set experiments from Gecko in Android. Ignoring."); + return NS_OK; +#else + MOZ_ASSERT(XRE_IsParentProcess()); + nsTArray<nsCString> extraKeys; + nsTArray<nsCString> extraValues; + if (!aExtra.isNullOrUndefined()) { + JS::RootedObject obj(aCx, &aExtra.toObject()); + JS::Rooted<JS::IdVector> keys(aCx, JS::IdVector(aCx)); + if (!JS_Enumerate(aCx, obj, &keys)) { + LogToBrowserConsole(nsIScriptError::warningFlag, + u"Failed to enumerate experiment extras object."_ns); + return NS_OK; + } + + for (size_t i = 0, n = keys.length(); i < n; i++) { + nsAutoJSCString jsKey; + if (!jsKey.init(aCx, keys[i])) { + LogToBrowserConsole( + nsIScriptError::warningFlag, + u"Extra dictionary should only contain string keys."_ns); + return NS_OK; + } + + JS::Rooted<JS::Value> value(aCx); + if (!JS_GetPropertyById(aCx, obj, keys[i], &value)) { + LogToBrowserConsole(nsIScriptError::warningFlag, + u"Failed to get experiment extra property."_ns); + return NS_OK; + } + + nsAutoJSCString jsValue; + if (!value.isString()) { + LogToBrowserConsole( + nsIScriptError::warningFlag, + u"Experiment extra properties must have string values."_ns); + return NS_OK; + } + + if (!jsValue.init(aCx, value)) { + LogToBrowserConsole(nsIScriptError::warningFlag, + u"Can't extract experiment extra property"_ns); + return NS_OK; + } + + extraKeys.AppendElement(jsKey); + extraValues.AppendElement(jsValue); + } + } + glean::impl::fog_set_experiment_active(&aExperimentId, &aBranch, &extraKeys, + &extraValues); + return NS_OK; +#endif +} + +NS_IMETHODIMP +FOG::SetExperimentInactive(const nsACString& aExperimentId) { +#ifdef MOZ_GLEAN_ANDROID + NS_WARNING("Don't unset experiments from Gecko in Android. Ignoring."); + return NS_OK; +#else + MOZ_ASSERT(XRE_IsParentProcess()); + glean::impl::fog_set_experiment_inactive(&aExperimentId); + return NS_OK; +#endif +} + +NS_IMETHODIMP +FOG::TestGetExperimentData(const nsACString& aExperimentId, JSContext* aCx, + JS::MutableHandleValue aResult) { +#ifdef MOZ_GLEAN_ANDROID + NS_WARNING("Don't test experiments from Gecko in Android. Throwing."); + aResult.set(JS::UndefinedValue()); + return NS_ERROR_FAILURE; +#else + MOZ_ASSERT(XRE_IsParentProcess()); + if (!glean::impl::fog_test_is_experiment_active(&aExperimentId)) { + aResult.set(JS::UndefinedValue()); + return NS_OK; + } + + // We could struct-up the branch and extras and do what + // EventMetric::TestGetValue does... but keeping allocation on this side feels + // cleaner to me at the moment. + nsCString branch; + nsTArray<nsCString> extraKeys; + nsTArray<nsCString> extraValues; + + glean::impl::fog_test_get_experiment_data(&aExperimentId, &branch, &extraKeys, + &extraValues); + MOZ_ASSERT(extraKeys.Length() == extraValues.Length()); + + JS::RootedObject jsExperimentDataObj(aCx, JS_NewPlainObject(aCx)); + if (NS_WARN_IF(!jsExperimentDataObj)) { + return NS_ERROR_FAILURE; + } + + JS::RootedValue jsBranchStr(aCx); + if (!dom::ToJSValue(aCx, branch, &jsBranchStr) || + !JS_DefineProperty(aCx, jsExperimentDataObj, "branch", jsBranchStr, + JSPROP_ENUMERATE)) { + NS_WARNING("Failed to define branch for experiment data object."); + return NS_ERROR_FAILURE; + } + + JS::RootedObject jsExtraObj(aCx, JS_NewPlainObject(aCx)); + if (!JS_DefineProperty(aCx, jsExperimentDataObj, "extra", jsExtraObj, + JSPROP_ENUMERATE)) { + NS_WARNING("Failed to define extra for experiment data object."); + return NS_ERROR_FAILURE; + } + + for (unsigned int i = 0; i < extraKeys.Length(); i++) { + JS::RootedValue jsValueStr(aCx); + if (!dom::ToJSValue(aCx, extraValues[i], &jsValueStr) || + !JS_DefineProperty(aCx, jsExtraObj, extraKeys[i].Data(), jsValueStr, + JSPROP_ENUMERATE)) { + NS_WARNING("Failed to define extra property for experiment data object."); + return NS_ERROR_FAILURE; + } + } + aResult.setObject(*jsExperimentDataObj); + return NS_OK; +#endif +} + +NS_IMETHODIMP +FOG::TestFlushAllChildren(JSContext* aCx, mozilla::dom::Promise** aOutPromise) { + MOZ_ASSERT(XRE_IsParentProcess()); + NS_ENSURE_ARG(aOutPromise); + *aOutPromise = nullptr; + nsIGlobalObject* global = xpc::CurrentNativeGlobal(aCx); + if (NS_WARN_IF(!global)) { + return NS_ERROR_FAILURE; + } + + ErrorResult erv; + RefPtr<dom::Promise> promise = dom::Promise::Create(global, erv); + if (NS_WARN_IF(erv.Failed())) { + return erv.StealNSResult(); + } + + glean::FlushAndUseFOGData()->Then( + GetCurrentSerialEventTarget(), __func__, + [promise]() { promise->MaybeResolveWithUndefined(); }); + + promise.forget(aOutPromise); + return NS_OK; +} + +NS_IMETHODIMP +FOG::Observe(nsISupports* aSubject, const char* aTopic, const char16_t* aData) { + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(NS_IsMainThread()); + + // On idle, opportunistically flush child process data to the parent, + // then persist ping-lifetime data to the db. + if (!strcmp(aTopic, OBSERVER_TOPIC_IDLE)) { + glean::FlushAndUseFOGData(); +#ifndef MOZ_GLEAN_ANDROID + Unused << glean::impl::fog_persist_ping_lifetime_data(); +#endif + } + + return NS_OK; +} + +NS_IMETHODIMP +FOG::TestResetFOG(const nsACString& aDataPathOverride, + const nsACString& aAppIdOverride) { + MOZ_ASSERT(XRE_IsParentProcess()); + return glean::impl::fog_test_reset(&aDataPathOverride, &aAppIdOverride); +} + +NS_IMETHODIMP +FOG::TestTriggerMetrics(uint32_t aProcessType, JSContext* aCx, + mozilla::dom::Promise** aOutPromise) { + MOZ_ASSERT(XRE_IsParentProcess()); + NS_ENSURE_ARG(aOutPromise); + *aOutPromise = nullptr; + nsIGlobalObject* global = xpc::CurrentNativeGlobal(aCx); + if (NS_WARN_IF(!global)) { + return NS_ERROR_FAILURE; + } + + ErrorResult erv; + RefPtr<dom::Promise> promise = dom::Promise::Create(global, erv); + if (NS_WARN_IF(erv.Failed())) { + return erv.StealNSResult(); + } + + glean::TestTriggerMetrics(aProcessType, promise); + + promise.forget(aOutPromise); + return NS_OK; +} + +NS_IMETHODIMP +FOG::TestRegisterRuntimeMetric( + const nsACString& aType, const nsACString& aCategory, + const nsACString& aName, const nsTArray<nsCString>& aPings, + const nsACString& aLifetime, const bool aDisabled, + const nsACString& aExtraArgs, uint32_t* aMetricIdOut) { + *aMetricIdOut = 0; + *aMetricIdOut = glean::jog::jog_test_register_metric( + &aType, &aCategory, &aName, &aPings, &aLifetime, aDisabled, &aExtraArgs); + return NS_OK; +} + +NS_IMETHODIMP +FOG::TestRegisterRuntimePing(const nsACString& aName, + const bool aIncludeClientId, + const bool aSendIfEmpty, + const nsTArray<nsCString>& aReasonCodes, + uint32_t* aPingIdOut) { + *aPingIdOut = 0; + *aPingIdOut = glean::jog::jog_test_register_ping(&aName, aIncludeClientId, + aSendIfEmpty, &aReasonCodes); + return NS_OK; +} + +NS_IMPL_ISUPPORTS(FOG, nsIFOG, nsIObserver) + +} // namespace mozilla diff --git a/toolkit/components/glean/xpcom/FOG.h b/toolkit/components/glean/xpcom/FOG.h new file mode 100644 index 0000000000..55e155052e --- /dev/null +++ b/toolkit/components/glean/xpcom/FOG.h @@ -0,0 +1,29 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#ifndef mozilla_FOG_h +#define mozilla_FOG_h + +#include "nsIFOG.h" +#include "nsIObserver.h" + +namespace mozilla { +class FOG final : public nsIFOG, public nsIObserver { + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIFOG + NS_DECL_NSIOBSERVER + + public: + FOG() = default; + static already_AddRefed<FOG> GetSingleton(); + + private: + ~FOG() = default; + void Shutdown(); +}; + +}; // namespace mozilla + +#endif // mozilla_FOG_h diff --git a/toolkit/components/glean/xpcom/components.conf b/toolkit/components/glean/xpcom/components.conf new file mode 100644 index 0000000000..b5a86537c0 --- /dev/null +++ b/toolkit/components/glean/xpcom/components.conf @@ -0,0 +1,18 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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 http://mozilla.org/MPL/2.0/. + +Classes = [ + { + 'cid': '{98d0e975-9cad-4ce3-ae2f-f878b8be6307}', + 'contract_ids': ['@mozilla.org/toolkit/glean;1'], + 'singleton': True, + 'type': 'mozilla::FOG', + 'headers': ['mozilla/FOG.h'], + 'constructor': 'mozilla::FOG::GetSingleton', + 'js_name': 'fog', + 'interfaces': ['nsIFOG'], + } +] diff --git a/toolkit/components/glean/xpcom/moz.build b/toolkit/components/glean/xpcom/moz.build new file mode 100644 index 0000000000..3a9ededa44 --- /dev/null +++ b/toolkit/components/glean/xpcom/moz.build @@ -0,0 +1,27 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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 http://mozilla.org/MPL/2.0/. + +FINAL_LIBRARY = "xul" + +EXPORTS.mozilla += [ + "FOG.h", +] + +UNIFIED_SOURCES += [ + "FOG.cpp", +] + +XPCOM_MANIFESTS += ["components.conf"] + +XPIDL_MODULE = "fog" + +XPIDL_SOURCES += [ + "nsIFOG.idl", + "nsIGleanMetrics.idl", +] + +with Files("**"): + BUG_COMPONENT = ("Toolkit", "Telemetry") diff --git a/toolkit/components/glean/xpcom/nsIFOG.idl b/toolkit/components/glean/xpcom/nsIFOG.idl new file mode 100644 index 0000000000..3c3829db95 --- /dev/null +++ b/toolkit/components/glean/xpcom/nsIFOG.idl @@ -0,0 +1,178 @@ +/* -*- Mode: C++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 8 -*- */ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +[scriptable, uuid(98d0e975-9cad-4ce3-ae2f-f878b8be6307)] +interface nsIFOG : nsISupports +{ + + /** + * Initialize FOG. + * + * To be scheduled at some opportune time after the bulk of Firefox startup + * has completed. + * + * @param aDataPathOverride - The path of a custom Glean data path to use + * instead of the profile dir. + * @param aAppIdOverride - The application_id to use instead of + * "firefox.desktop". + */ + void initializeFOG([optional] in AUTF8String aDataPathOverride, [optional] in AUTF8String aAppIdOverride); + + /** + * Register custom pings. + * + * Ensure all custom pings are registered with Glean. + */ + void registerCustomPings(); + + /** + * Enable or Disable the logging of pings in the Glean SDK. + * See https://firefox-source-docs.mozilla.org/toolkit/components/glean/testing.html + * for details. + * + * @param aEnableLogPings - true to enable logging, false to disable. + */ + void setLogPings(in boolean aEnableLogPings); + + /** + * Set the tag to be applied to pings assembled from now on. + * See https://firefox-source-docs.mozilla.org/toolkit/components/glean/testing.html + * for details. + * + * @param aDebugTag - The string tag to apply. + * If it cannot be applied (e.g it contains characters that are + * forbidden in HTTP headers) the old value will remain. + */ + void setTagPings(in ACString aDebugTag); + + /** + * Send the named ping. + * See https://firefox-source-docs.mozilla.org/toolkit/components/glean/testing.html + * for details. + * + * @param aPingName - The name of the ping to send. If no ping of that name + * exists, or the ping is known but cannot be assembled + * (e.g if it is empty), no ping will be sent. + */ + void sendPing(in ACString aPingName); + + /** + * Indicate that an experiment is running. + * Glean will add an experiment annotation which is sent with pings. + * This information is not persisted between runs. + * + * See `glean_core::Glean::set_experiment_active`. + * + * Logs on error, but does not throw. + * + * @param aExperimentId - The id/slug of the experiment. + * @param aBranch - The name of the active branch of the experiment. + * @param aExtra - Optional string -> string dictionary of extra information. + */ + [implicit_jscontext] + void setExperimentActive(in ACString aExperimentId, in ACString aBranch, [optional] in jsval aExtra); + + /** + * Indicate that an experiment is no longer running. + * + * See `glean_core::Glean::set_experiment_inactive`. + * + * Logs on error, but does not throw. + * + * @param aExperimentId - The id/slug of the experiment from setExperimentActive. + */ + void setExperimentInactive(in ACString aExperimentId); + + /** + * **Test-only API** + * + * If the identified experiment was set active and hasn't been set inactive, + * this will give you the active branch and extra information. + * + * @param aExperimentId - The id/slug of the experiment from setExperimentActive. + * + * @return an object of the form + * {branch: "branch-name", extra: {extra_key1: extra_value1, ...}} + * if there is an active experiment. Undefined, otherwise. + */ + [implicit_jscontext] + jsval testGetExperimentData(in ACString aExperimentId); + + /** + * ** Test-only Method ** + * + * Flush all data from all child processes. + * + * @returns A promise that resolves when the data's been stored. + */ + [implicit_jscontext] + Promise testFlushAllChildren(); + + /** + * ** Test-only Method ** + * + * Reset FOG and the Glean SDK, clearing storage. + */ + void testResetFOG([optional] in AUTF8String aDataPathOverride, [optional] in AUTF8String aAppIdOverride); + + /** + * ** Test-only Method ** + * + * Trigger test metric instrumentation on the GPU, RDD or Socket process. + * + * @param aProcessType - A PROCESS_TYPE_* value from the constants defined + * in the nsIXULRuntime interface. + * + * @returns A promise that resolves when the test data has been added. + * The promise will be rejected if the process type is not supported + * or if sending the IPC to the child process fails. + */ + [implicit_jscontext] + Promise testTriggerMetrics(in unsigned long aProcessType); + + /** + * ** Test-only Method ** + * + * Register a metric. + * + * This function is deliberately not too friendly to use. You probably aren't + * supposed to use it unless you're testing metric registration itself. + * + * @param aType - The metric's type. + * @param aCategory - The metric's category. + * @param aName - The metric's name. + * @param aPings - The pings to send it in. + * @param aLifetime - The metric's lifetime. + * @param aDisabled - Whether the metric, though existing, isn't enabled. + * @param aExtraArgs - Optional JSON string of extra args. + */ + uint32_t testRegisterRuntimeMetric(in ACString aType, + in ACString aCategory, + in ACString aName, + in Array<ACString> aPings, + in ACString aLifetime, + in boolean aDisabled, + [optional] in ACString aExtraArgs); + + /** + * ** Test-only Method ** + * + * Register a ping. + * + * This function is deliberately not too friendly to use. You probably aren't + * supposed to use it unless you're testing ping registration itself. + * + * @param aName - The ping's name. + * @param aIncludeClientId - Whether the ping should include the client_id. + * @param aSendIfEmpty - Whether the ping should send even if empty. + * @param aReasonCodes - The list of valid reasons for ping submission. + */ + uint32_t testRegisterRuntimePing(in ACString aName, + in boolean aIncludeClientId, + in boolean aSendIfEmpty, + in Array<ACString> aReasonCodes); +}; diff --git a/toolkit/components/glean/xpcom/nsIGleanMetrics.idl b/toolkit/components/glean/xpcom/nsIGleanMetrics.idl new file mode 100644 index 0000000000..b4faf26be5 --- /dev/null +++ b/toolkit/components/glean/xpcom/nsIGleanMetrics.idl @@ -0,0 +1,665 @@ +/* -*- Mode: C++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 8 -*- */ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +[scriptable, uuid(d3180fe0-19fa-11eb-8b6f-0800200c9a66)] +interface nsIGleanBoolean : nsISupports +{ + /** + * Set to the specified boolean value. + * + * @param value the value to set. + */ + void set(in bool value); + + /** + * **Test-only API** + * + * Gets the currently stored value as a boolean. + * + * This function will attempt to await the last parent-process task (if any) + * writing to the the metric's storage engine before returning a value. + * This function will not wait for data from child processes. + * + * This doesn't clear the stored value. + * Parent process only. Panics in child processes. + * + * @param aPingName The (optional) name of the ping to retrieve the metric + * for. Defaults to the first value in `send_in_pings`. + * + * @return value of the stored metric, or undefined if there is no value. + */ + jsval testGetValue([optional] in AUTF8String aPingName); +}; + +[scriptable, uuid(aa15fd20-1e8a-11eb-9bec-0800200c9a66)] +interface nsIGleanDatetime : nsISupports +{ + /** + * Set the datetime to the provided value, or the local now. + * The internal value will store the local timezone. + * + * Note: The metric's time_unit affects the resolution of the value, not the + * unit of this function's parameter (which is always PRTime/nanos). + * + * @param aValue The (optional) time value as PRTime (nanoseconds since epoch). + * Defaults to local now. + */ + [optional_argc] + void set([optional] in PRTime aValue); + + /** + * **Test-only API** + * + * Gets the currently stored value as an integer. + * + * This function will attempt to await the last parent-process task (if any) + * writing to the the metric's storage engine before returning a value. + * This function will not wait for data from child processes. + * + * This doesn't clear the stored value. + * Parent process only. Panics in child processes. + * + * @param aPingName The (optional) name of the ping to retrieve the metric + * for. Defaults to the first value in `send_in_pings`. + * + * @return value of the stored metric as a JS Date with timezone, + * or undefined if there is no value. + */ + [implicit_jscontext] + jsval testGetValue([optional] in AUTF8String aPingName); +}; + +[scriptable, uuid(05b89d2a-d57c-11ea-82da-3f63399a6f5a)] +interface nsIGleanCounter : nsISupports +{ + /* + * Increases the counter by `amount`. + * + * @param amount The amount to increase by. Should be positive. + */ + void add(in int32_t amount); + + /** + * **Test-only API** + * + * Gets the currently stored value as an integer. + * + * This function will attempt to await the last parent-process task (if any) + * writing to the the metric's storage engine before returning a value. + * This function will not wait for data from child processes. + * + * This doesn't clear the stored value. + * Parent process only. Panics in child processes. + * + * @param aPingName The (optional) name of the ping to retrieve the metric + * for. Defaults to the first value in `send_in_pings`. + * + * @return value of the stored metric, or undefined if there is no value. + */ + jsval testGetValue([optional] in AUTF8String aPingName); +}; + +[scriptable, uuid(92e14730-9b5f-45a1-b018-f588d0b964d8)] +interface nsIGleanTimingDistribution : nsISupports +{ + /** + * Starts tracking time for the provided metric. + * + * @returns A unique timer id for the new timer + */ + [implicit_jscontext] + jsval start(); + + /** + * Stops tracking time for the provided metric and timer id. + * + * Adds a count to the corresponding bucket in the timing distribution. + * This will record an error if no `start` was called for this TimerId or + * if this TimerId was used to call `cancel`. + * + * @param aId The TimerId associated with this timing. This allows for + * concurrent timing of events associated with different ids. + */ + void stopAndAccumulate(in uint64_t aId); + + /** + * Aborts a previous `start` call. No error is recorded if no `start` was + * called. (But then where did you get that id from?) + * + * @param aId The TimerID whose `start` you wish to abort. + */ + void cancel(in uint64_t aId); + + /** + * **Test-only API** + * + * Gets the currently stored value as a DistributionData. + * + * This function will attempt to await the last parent-process task (if any) + * writing to the the metric's storage engine before returning a value. + * This function will not wait for data from child processes. + * + * This doesn't clear the stored value. + * Parent process only. Panics in child processes. + * + * @param aPingName The (optional) name of the ping to retrieve the metric + * for. Defaults to the first value in `send_in_pings`. + * + * @return value of the stored metric, or undefined if there is no value. + */ + [implicit_jscontext] + jsval testGetValue([optional] in ACString aPingName); + + /** + * **Test-only API** + * + * Accumulates a raw numeric sample of milliseconds. + * + * Test-only until we find a use-case and decent JS Time Duration type. + * + * @param aSample The sample, in milliseconds, to add. + */ + void testAccumulateRawMillis(in uint64_t aSample); +}; + +[scriptable, uuid(eea5ed46-16ba-46cd-bb1f-504581987fe1)] +interface nsIGleanMemoryDistribution : nsISupports +{ + /* + * Accumulates the provided sample in the metric. + * + * @param aSample The sample to be recorded by the metric. The sample is + * assumed to be in the confgured memory unit of the metric. + * + * Notes: Values bigger than 1 Terabyte (2^40 bytes) are truncated and an + * InvalidValue error is recorded. + */ + void accumulate(in uint64_t aSample); + + /** + * **Test-only API** + * + * Gets the currently stored value as a DistributionData. + * + * This function will attempt to await the last parent-process task (if any) + * writing to the the metric's storage engine before returning a value. + * This function will not wait for data from child processes. + * + * This doesn't clear the stored value. + * Parent process only. Panics in child processes. + * + * @param aPingName The (optional) name of the ping to retrieve the metric + * for. Defaults to the first value in `send_in_pings`. + * + * @return value of the stored metric, or undefined if there is no value. + */ + [implicit_jscontext] + jsval testGetValue([optional] in ACString aPingName); +}; + +[scriptable, uuid(45cc016f-c1d5-4d54-aaa5-a802cf65f23b)] +interface nsIGleanCustomDistribution : nsISupports +{ + /* + * Accumulates the provided signed samples in the metric. + * + * @param aSamples - The vector holding the samples to be recorded by the metric. + * + * Notes: Discards any negative value in `samples` + * and report an `ErrorType::InvalidValue` for each of them. + */ + void accumulateSamples(in Array<int64_t> aSamples); + + /** + * **Test-only API** + * + * Gets the currently stored value as a DistributionData. + * + * This function will attempt to await the last parent-process task (if any) + * writing to the the metric's storage engine before returning a value. + * This function will not wait for data from child processes. + * + * This doesn't clear the stored value. + * Parent process only. Panics in child processes. + * + * @param aPingName The (optional) name of the ping to retrieve the metric + * for. Defaults to the first value in `send_in_pings`. + * + * @return value of the stored metric, or undefined if there is no value. + */ + [implicit_jscontext] + jsval testGetValue([optional] in ACString aPingName); +}; + +[scriptable, function, uuid(e5447f62-4b03-497c-81e9-6ab683d20380)] +interface nsIGleanPingTestCallback : nsISupports +{ + void call(in ACString aReason); +}; + +[scriptable, uuid(5223a48b-687d-47ff-a629-fd4a72d1ecfa)] +interface nsIGleanPing : nsISupports +{ + /** + * Collect and submit the ping for eventual upload. + * + * This will collect all stored data to be included in the ping. + * Data with lifetime `ping` will then be reset. + * + * If the ping is configured with `send_if_empty = false` + * and the ping currently contains no content, + * it will not be queued for upload. + * If the ping is configured with `send_if_empty = true` + * it will be queued for upload even if empty. + * + * Pings always contain the `ping_info` and `client_info` sections. + * See [ping sections](https://mozilla.github.io/glean/book/user/pings/index.html#ping-sections) + * for details. + * + * @param aReason - Optional. The reason the ping is being submitted. + * Must match one of the configured `reason_codes`. + */ + void submit([optional] in ACString aReason); + + /** + * **Test-only API** + * + * Register a callback to be called right before this ping is next submitted. + * The provided function is called exactly once before submitting. + * + * Note: The callback will be called on any call to submit. + * A ping might not be sent afterwards, e.g. if the ping is empty and + * `send_if_empty` is `false`. + * + * @param aCallback - The callback to call on the next submit. + */ + void testBeforeNextSubmit(in nsIGleanPingTestCallback aCallback); +}; + +[scriptable, uuid(d84a3555-46f1-48c1-9122-e8e88b069d2b)] +interface nsIGleanString : nsISupports +{ + /* + * Set to the specified value. + * + * @param value The string to set the metric to. + */ + void set(in AUTF8String value); + + /** + * **Test-only API** + * + * Gets the currently stored value as a string. + * + * This function will attempt to await the last parent-process task (if any) + * writing to the the metric's storage engine before returning a value. + * This function will not wait for data from child processes. + * + * This doesn't clear the stored value. + * Parent process only. Panics in child processes. + * + * @param aPingName The (optional) name of the ping to retrieve the metric + * for. Defaults to the first value in `send_in_pings`. + * + * @return value of the stored metric, or undefined if there is no value. + */ + [implicit_jscontext] + jsval testGetValue([optional] in AUTF8String aPingName); +}; + +[scriptable, uuid(46751205-2ac7-47dc-91d2-ef4a95ef2af9)] +interface nsIGleanStringList : nsISupports +{ + /** + * Adds a new string to the list. + * + * Truncates the value and logs an error if it is longer than 50 bytes. + * + * @param value The string to add. + */ + void add(in AUTF8String value); + + /** + * Sets to a specific list of strings. + * + * Truncates the list and logs an error if longer than 20 items. + * Truncates any item longer than 50 bytes and logs an error. + * + * @param value The list of strings to set. + */ + void set(in Array<AUTF8String> value); + + /** + * **Test-only API** + * + * Gets the currently stored value. + * + * This function will attempt to await the last parent-process task (if any) + * writing to the the metric's storage engine before returning a value. + * This function will not wait for data from child processes. + * + * This doesn't clear the stored value. + * Parent process only. Panics in child processes. + * + * @param aPingName The (optional) name of the ping to retrieve the metric + * for. Defaults to the first value in `send_in_pings`. + * + * @return value of the stored metric, or undefined if there is no value. + */ + [implicit_jscontext] + jsval testGetValue([optional] in AUTF8String aPingName); +}; + +[scriptable, uuid(2586530c-030f-11eb-93cb-cbf30d25225a)] +interface nsIGleanTimespan : nsISupports +{ + /** + * Start tracking time for the provided metric. + * + * This records an error if it’s already tracking time (i.e. start was already + * called with no corresponding [stop]): in that case the original + * start time will be preserved. + */ + void start(); + + /** + * Stop tracking time for the provided metric. + * + * Sets the metric to the elapsed time, but does not overwrite an already + * existing value. + * This will record an error if no [start] was called or there is an already + * existing value. + */ + void stop(); + + /** + * Aborts a previous start. + * + * Does not record an error if there was no previous call to start. + */ + void cancel(); + + /** + * Explicitly sets the timespan value. + * + * This API should only be used if you cannot make use of + * `start`/`stop`/`cancel`. + * + * @param aDuration The duration of this timespan, in units matching the + * `time_unit` of this metric's definition. + */ + void setRaw(in uint32_t aDuration); + + /** + * **Test-only API** + * + * Gets the currently stored value as an integer. + * + * This function will attempt to await the last parent-process task (if any) + * writing to the the metric's storage engine before returning a value. + * This function will not wait for data from child processes. + * + * This doesn't clear the stored value. + * Parent process only. Panics in child processes. + * + * @param aPingName The (optional) name of the ping to retrieve the metric + * for. Defaults to the first value in `send_in_pings`. + * + * @return value of the stored metric, or undefined if there is no value. + */ + jsval testGetValue([optional] in AUTF8String aPingName); +}; + +[scriptable, uuid(395700e7-06f6-46be-adcc-ea58977fda6d)] +interface nsIGleanUuid : nsISupports +{ + /** + * Set to the specified value. + * + * @param aValue The UUID to set the metric to. + */ + void set(in AUTF8String aValue); + + /** + * Generate a new random UUID and set the metric to it. + */ + void generateAndSet(); + + /** + * **Test-only API** + * + * Gets the currently stored value as an integer. + * + * This function will attempt to await the last parent-process task (if any) + * writing to the the metric's storage engine before returning a value. + * This function will not wait for data from child processes. + * + * This doesn't clear the stored value. + * Parent process only. Panics in child processes. + * + * @param aPingName The (optional) name of the ping to retrieve the metric + * for. Defaults to the first value in `send_in_pings`. + * + * @return value of the stored metric, or undefined if there is no value. + */ + [implicit_jscontext] + jsval testGetValue([optional] in AUTF8String aPingName); +}; + +[scriptable, uuid(1b01424a-1f55-11eb-92a5-0754f6c3f240)] +interface nsIGleanEvent : nsISupports +{ + /* + * Record an event. + * + * @param aExtra An (optional) map of extra values. + */ + [implicit_jscontext] + void record([optional] in jsval aExtra); + + /** + * **Test-only API** + * + * Get a list of currently stored events for this event metric. + * + * This function will attempt to await the last parent-process task (if any) + * writing to the the metric's storage engine before returning a value. + * This function will not wait for data from child processes. + * + * This doesn't clear the stored value. + * Parent process only. Panics in child processes. + * + * @param aPingName The (optional) name of the ping to retrieve the metric + * for. Defaults to the first value in `send_in_pings`. + * + * @return value of the stored metric, or undefined if there is no value. + * + * The data is an array of objects: + * + * ``` + * [ + * { + * timestamp: Integer, + * category: String, + * name: String, + * extra: { + * String: String + * ... + * } + * }, + * ... + * ] + * ``` + * + * The difference between event timestamps is in milliseconds + * See https://mozilla.github.io/glean/book/user/metrics/event.html for further details. + * Due to limitations of numbers in JavaScript, the timestamp will only be accurate up until 2^53. + * (This is probably not an issue with the current clock implementation. Probably.) + */ + [implicit_jscontext] + jsval testGetValue([optional] in AUTF8String aPingName); +}; + +[scriptable, uuid(0558c1b2-2cb1-4e21-a0a0-6a91a35ef219)] +interface nsIGleanQuantity : nsISupports +{ + /** + * Set to the specified value. + * + * @param value the value to set. + */ + void set(in int64_t value); + + /** + * **Test-only API** + * + * Gets the currently stored value. + * + * This function will attempt to await the last parent-process task (if any) + * writing to the the metric's storage engine before returning a value. + * This function will not wait for data from child processes. + * + * This doesn't clear the stored value. + * Parent process only. Panics in child processes. + * + * @param aPingName The (optional) name of the ping to retrieve the metric + * for. Defaults to the first value in `send_in_pings`. + * + * @return value of the stored metric, or undefined if there is no value. + */ + jsval testGetValue([optional] in AUTF8String aPingName); +}; + +[scriptable, uuid(394d9d3b-9e7e-48cc-b76c-a89a51830da3)] +interface nsIGleanDenominator : nsISupports +{ + /* + * Increases the counter by `amount`. + * + * @param amount The amount to increase by. Should be positive. + */ + void add(in int32_t amount); + + /** + * **Test-only API** + * + * Gets the currently stored value as an integer. + * + * This function will attempt to await the last parent-process task (if any) + * writing to the the metric's storage engine before returning a value. + * This function will not wait for data from child processes. + * + * This doesn't clear the stored value. + * Parent process only. Panics in child processes. + * + * @param aPingName The (optional) name of the ping to retrieve the metric + * for. Defaults to the first value in `send_in_pings`. + * + * @return value of the stored metric, or undefined if there is no value. + */ + jsval testGetValue([optional] in AUTF8String aPingName); +}; + +[scriptable, uuid(153fff71-7edd-49b4-a166-4697aa89c7a1)] +interface nsIGleanNumerator : nsISupports +{ + /* + * Increases the numerator by `amount`. + * + * @param amount The amount to increase by. Should be positive. + */ + void addToNumerator(in int32_t amount); + + /** + * **Test-only API** + * + * Gets the currently stored value in the form {numerator: n, denominator: d} + * + * This function will attempt to await the last parent-process task (if any) + * writing to the the metric's storage engine before returning a value. + * This function will not wait for data from child processes. + * + * This doesn't clear the stored value. + * Parent process only. Panics in child processes. + * + * @param aPingName The (optional) name of the ping to retrieve the metric + * for. Defaults to the first value in `send_in_pings`. + * + * @return value of the stored metric, or undefined if there is no value. + */ + [implicit_jscontext] + jsval testGetValue([optional] in AUTF8String aPingName); +}; + +[scriptable, uuid(920cf631-2b1e-4efe-ae2e-f03277c3112a)] +interface nsIGleanRate : nsISupports +{ + /* + * Increases the numerator by `amount`. + * + * @param amount The amount to increase by. Should be positive. + */ + void addToNumerator(in int32_t amount); + + /* + * Increases the denominator by `amount`. + * + * @param amount The amount to increase by. Should be positive. + */ + void addToDenominator(in int32_t amount); + + /** + * **Test-only API** + * + * Gets the currently stored value in the form {numerator: n, denominator: d} + * + * This function will attempt to await the last parent-process task (if any) + * writing to the the metric's storage engine before returning a value. + * This function will not wait for data from child processes. + * + * This doesn't clear the stored value. + * Parent process only. Panics in child processes. + * + * @param aPingName The (optional) name of the ping to retrieve the metric + * for. Defaults to the first value in `send_in_pings`. + * + * @return value of the stored metric, or undefined if there is no value. + */ + [implicit_jscontext] + jsval testGetValue([optional] in AUTF8String aPingName); +}; + +[scriptable, uuid(a59672c4-bc48-4bfe-8f9c-6f408a59d819)] +interface nsIGleanUrl : nsISupports +{ + /* + * Sets to the specified stringified URL. + * + * @param value The stringified URL to set the metric to. + */ + void set(in AUTF8String value); + + /** + * **Test-only API** + * + * Gets the currently stored value as a string. + * + * This function will attempt to await the last parent-process task (if any) + * writing to the the metric's storage engine before returning a value. + * This function will not wait for data from child processes. + * + * This doesn't clear the stored value. + * Parent process only. Panics in child processes. + * + * @param aPingName The (optional) name of the ping to retrieve the metric + * for. Defaults to the first value in `send_in_pings`. + * + * @return value of the stored metric, or undefined if there is no value. + */ + [implicit_jscontext] + jsval testGetValue([optional] in AUTF8String aPingName); +}; |