diff options
Diffstat (limited to 'toolkit/components/glean')
119 files changed, 11662 insertions, 0 deletions
diff --git a/toolkit/components/glean/Cargo.toml b/toolkit/components/glean/Cargo.toml new file mode 100644 index 0000000000..0e8094a517 --- /dev/null +++ b/toolkit/components/glean/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "fog_control" +version = "0.1.0" +authors = ["Glean SDK team <glean-team@mozilla.com>"] +edition = "2018" +license = "MPL-2.0" + +[dependencies] +glean = "33.10.2" +glean-core = { version = "33.10.2", features = ["rkv-safe-mode"] } +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 = { git = "https://github.com/mozilla/application-services", rev = "8a576fbe79199fa8664f64285524017f74ebcc5f" } # Copied from toolkit/library/rust/shared/Cargo.toml +url = "2.1" # Copied from viaduct's deps, see https://github.com/mozilla/application-services/issues/3062 + +[features] +with_gecko = ["fog/with_gecko"] diff --git a/toolkit/components/glean/api/Cargo.toml b/toolkit/components/glean/api/Cargo.toml new file mode 100644 index 0000000000..8c72c50823 --- /dev/null +++ b/toolkit/components/glean/api/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "fog" +version = "0.1.0" +authors = ["Glean SDK team <glean-team@mozilla.com>"] +edition = "2018" +publish = false + +[dependencies] +bincode = "1.0" +chrono = "0.4.10" +glean = "33.10.2" +glean-core = { version = "33.10.2", features = ["rkv-safe-mode"] } +inherent = "0.1.4" +log = "0.4" +nsstring = { path = "../../../../xpcom/rust/nsstring", optional = true } +once_cell = "1.2.0" +serde = { version = "1.0", features = ["derive"] } +uuid = { version = "0.8.1", features = ["v4"] } +xpcom = { path = "../../../../xpcom/rust/xpcom", optional = true } +thin-vec = { version = "0.2.1", features = ["gecko-ffi"] } + +[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..c88ae88cb4 --- /dev/null +++ b/toolkit/components/glean/api/src/common_test.rs @@ -0,0 +1,51 @@ +// 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().display().to_string(); + + let cfg = glean::Configuration { + upload_enabled: true, + data_path: tmpname, + application_id: GLOBAL_APPLICATION_ID.into(), + max_events: None, + delay_ping_lifetime_io: false, + channel: None, + server_endpoint: None, + uploader: None, + }; + + let client_info = glean::ClientInfoMetrics { + app_build: "test-build".into(), + app_display_version: "1.2.3".into(), + }; + + glean::test_reset_glean(cfg, client_info, true); + + dir +} 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..6a433eda54 --- /dev/null +++ b/toolkit/components/glean/api/src/ffi/event.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, 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<i32>, + extra_values: &ThinVec<nsCString>, +) { + // If no extra keys are passed, we can shortcut here. + if extra_keys.is_empty() { + if metric_maps::event_record_wrapper(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, v.to_string())) + .collect(); + match metric_maps::event_record_wrapper(id, extra) { + Ok(()) => {} + Err(EventRecordingError::InvalidId) => panic!("No event for id {}", id), + Err(EventRecordingError::InvalidExtraKey) => { + panic!("Invalid extra keys in map for id {}", id) + } + } +} + +#[no_mangle] +pub extern "C" fn fog_event_record_str( + 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 metric_maps::event_record_wrapper_str(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(); + match metric_maps::event_record_wrapper_str(id, extra) { + Ok(()) => {} + Err(EventRecordingError::InvalidId) => panic!("No event for id {}", id), + Err(EventRecordingError::InvalidExtraKey) => { + panic!("Invalid extra keys in map for id {}", id) + } + } +} + +#[no_mangle] +pub unsafe extern "C" fn fog_event_test_has_value(id: u32, storage_name: &nsACString) -> bool { + metric_maps::event_test_get_value_wrapper(id, &storage_name.to_utf8()).is_some() +} 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..c6776e96af --- /dev/null +++ b/toolkit/components/glean/api/src/ffi/macros.rs @@ -0,0 +1,61 @@ +// 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. +/// +/// # Arguments +/// +/// * `$map` - The name of the hash map within `metrics::__glean_metric_maps` +/// as generated by glean_parser. +/// * `$id` - The ID of the metric to get. +macro_rules! metric_get { + ($map:ident, $id:ident) => { + match $crate::metrics::__glean_metric_maps::$map.get(&$id.into()) { + Some(metric) => metric, + None => panic!("No metric for id {}", $id), + } + }; +} + +/// Test whether a value is stored for the metric identified by its ID. +/// +/// # Arguments +/// +/// * `$map` - The name of the hash map within `metrics::__glean_metric_maps` +/// as generated by glean_parser. +/// * `$id` - The ID of the metric to get. +/// * `$storage` - the storage name to look into. +macro_rules! test_has { + ($map:ident, $id:ident, $storage:ident) => {{ + let metric = metric_get!($map, $id); + 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 metric identified by its ID. +/// +/// # Arguments +/// +/// * `$map` - The name of the hash map within `metrics::__glean_metric_maps` +/// as generated by glean_parser. +/// * `$id` - The ID of the metric to get. +/// * `$storage` - the storage name to look into. +macro_rules! test_get { + ($map:ident, $id:ident, $storage:ident) => {{ + let metric = metric_get!($map, $id); + let storage = if $storage.is_empty() { + None + } else { + Some($storage.to_utf8()) + }; + metric.test_get_value(storage.as_deref()).unwrap() + }}; +} 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..b5ba32b430 --- /dev/null +++ b/toolkit/components/glean/api/src/ffi/mod.rs @@ -0,0 +1,268 @@ +// 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, nsCString}; +use thin_vec::ThinVec; +use uuid::Uuid; + +#[macro_use] +mod macros; +mod event; + +#[no_mangle] +pub unsafe extern "C" fn fog_counter_add(id: u32, amount: i32) { + let metric = metric_get!(COUNTER_MAP, id); + metric.add(amount); +} + +#[no_mangle] +pub unsafe extern "C" fn fog_counter_test_has_value(id: u32, storage_name: &nsACString) -> bool { + test_has!(COUNTER_MAP, id, storage_name) +} + +#[no_mangle] +pub unsafe extern "C" fn fog_counter_test_get_value(id: u32, storage_name: &nsACString) -> i32 { + test_get!(COUNTER_MAP, id, storage_name) +} + +#[no_mangle] +pub unsafe extern "C" fn fog_timespan_start(id: u32) { + let metric = metric_get!(TIMESPAN_MAP, id); + metric.start(); +} + +#[no_mangle] +pub unsafe extern "C" fn fog_timespan_stop(id: u32) { + let metric = metric_get!(TIMESPAN_MAP, id); + metric.stop(); +} + +#[no_mangle] +pub unsafe extern "C" fn fog_timespan_test_has_value(id: u32, storage_name: &nsACString) -> bool { + test_has!(TIMESPAN_MAP, id, storage_name) +} + +#[no_mangle] +pub unsafe extern "C" fn fog_timespan_test_get_value(id: u32, storage_name: &nsACString) -> u64 { + test_get!(TIMESPAN_MAP, id, storage_name) +} + +#[no_mangle] +pub unsafe extern "C" fn fog_boolean_test_has_value(id: u32, storage_name: &nsACString) -> bool { + test_has!(BOOLEAN_MAP, id, storage_name) +} + +#[no_mangle] +pub unsafe extern "C" fn fog_boolean_test_get_value(id: u32, storage_name: &nsACString) -> bool { + test_get!(BOOLEAN_MAP, id, storage_name) +} + +#[no_mangle] +pub extern "C" fn fog_boolean_set(id: u32, value: bool) { + let metric = metric_get!(BOOLEAN_MAP, id); + metric.set(value); +} + +// The String functions are custom because test_get needs to use an outparam. +// If we can make test_get optional, we can go back to using the macro to +// generate the rest of the functions, or something. + +#[no_mangle] +pub extern "C" fn fog_string_test_has_value(id: u32, storage_name: &nsACString) -> bool { + test_has!(STRING_MAP, id, storage_name) +} + +#[no_mangle] +pub extern "C" fn fog_string_test_get_value( + id: u32, + storage_name: &nsACString, + value: &mut nsACString, +) { + let val = test_get!(STRING_MAP, id, storage_name); + value.assign(&val); +} + +#[no_mangle] +pub extern "C" fn fog_string_set(id: u32, value: &nsACString) { + let metric = metric_get!(STRING_MAP, id); + metric.set(value.to_utf8()); +} + +// String List Functions: + +#[no_mangle] +pub extern "C" fn fog_string_list_test_has_value(id: u32, storage_name: &nsACString) -> bool { + test_has!(STRING_LIST_MAP, id, storage_name) +} + +#[no_mangle] +pub extern "C" fn fog_string_list_test_get_value( + id: u32, + storage_name: &nsACString, + value: &mut ThinVec<nsCString>, +) { + let val = test_get!(STRING_LIST_MAP, id, storage_name); + for v in val { + value.push(v.into()); + } +} + +#[no_mangle] +pub extern "C" fn fog_string_list_add(id: u32, value: &nsACString) { + let metric = metric_get!(STRING_LIST_MAP, id); + metric.add(value.to_utf8()); +} + +#[no_mangle] +pub extern "C" fn fog_string_list_set(id: u32, value: &ThinVec<nsCString>) { + let metric = metric_get!(STRING_LIST_MAP, id); + let value = value.iter().map(|s| s.to_utf8().into()).collect(); + metric.set(value); +} + +// The Uuid functions are custom because test_get needs to use an outparam. +// If we can make test_get optional, we can go back to using the macro to +// generate the rest of the functions, or something. + +#[no_mangle] +pub extern "C" fn fog_uuid_test_has_value(id: u32, storage_name: &nsACString) -> bool { + test_has!(UUID_MAP, id, storage_name) +} + +#[no_mangle] +pub extern "C" fn fog_uuid_test_get_value( + id: u32, + storage_name: &nsACString, + value: &mut nsACString, +) { + let uuid = test_get!(UUID_MAP, id, storage_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()) { + let metric = metric_get!(UUID_MAP, id); + metric.set(uuid); + } +} + +#[no_mangle] +pub extern "C" fn fog_uuid_generate_and_set(id: u32) { + let metric = metric_get!(UUID_MAP, id); + metric.generate_and_set(); +} + +#[no_mangle] +pub extern "C" fn fog_datetime_test_has_value(id: u32, storage_name: &nsACString) -> bool { + test_has!(DATETIME_MAP, id, storage_name) +} + +#[no_mangle] +pub extern "C" fn fog_datetime_test_get_value( + id: u32, + storage_name: &nsACString, + value: &mut nsACString, +) { + let val = test_get!(DATETIME_MAP, id, storage_name); + value.assign(&val.to_rfc3339()); +} + +#[no_mangle] +pub extern "C" fn fog_datetime_set( + id: u32, + year: i32, + month: u32, + day: u32, + hour: u32, + minute: u32, + second: u32, + nano: u32, + offset_seconds: i32, +) { + let metric = metric_get!(DATETIME_MAP, id); + metric.set_with_details(year, month, day, hour, minute, second, nano, offset_seconds); +} + +#[no_mangle] +pub extern "C" fn fog_memory_distribution_test_has_value( + id: u32, + storage_name: &nsACString, +) -> bool { + test_has!(MEMORY_DISTRIBUTION_MAP, id, storage_name) +} + +#[no_mangle] +pub extern "C" fn fog_memory_distribution_test_get_value( + id: u32, + storage_name: &nsACString, + sum: &mut u64, + buckets: &mut ThinVec<u64>, + counts: &mut ThinVec<u64>, +) { + let val = test_get!(MEMORY_DISTRIBUTION_MAP, id, storage_name); + *sum = val.sum; + for (&bucket, &count) in val.values.iter() { + buckets.push(bucket); + counts.push(count); + } +} + +#[no_mangle] +pub extern "C" fn fog_memory_distribution_accumulate(id: u32, sample: u64) { + let metric = metric_get!(MEMORY_DISTRIBUTION_MAP, id); + metric.accumulate(sample); +} + +#[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()); +} + +#[no_mangle] +pub extern "C" fn fog_timing_distribution_start(id: u32) -> u64 { + let metric = metric_get!(TIMING_DISTRIBUTION_MAP, id); + metric.start() +} + +#[no_mangle] +pub extern "C" fn fog_timing_distribution_stop_and_accumulate(id: u32, timing_id: u64) { + let metric = metric_get!(TIMING_DISTRIBUTION_MAP, id); + metric.stop_and_accumulate(timing_id); +} + +#[no_mangle] +pub extern "C" fn fog_timing_distribution_cancel(id: u32, timing_id: u64) { + let metric = metric_get!(TIMING_DISTRIBUTION_MAP, id); + metric.cancel(timing_id); +} + +#[no_mangle] +pub extern "C" fn fog_timing_distribution_test_has_value(id: u32, ping_name: &nsACString) -> bool { + test_has!(TIMING_DISTRIBUTION_MAP, id, 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 = test_get!(TIMING_DISTRIBUTION_MAP, id, ping_name); + *sum = val.sum; + for (&bucket, &count) in val.values.iter() { + buckets.push(bucket); + counts.push(count); + } +} diff --git a/toolkit/components/glean/api/src/ipc.rs b/toolkit/components/glean/api/src/ipc.rs new file mode 100644 index 0000000000..1b6992d7b5 --- /dev/null +++ b/toolkit/components/glean/api/src/ipc.rs @@ -0,0 +1,127 @@ +// 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::{Instant, MetricId}; +use once_cell::sync::Lazy; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +#[cfg(not(feature = "with_gecko"))] +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Mutex; +#[cfg(feature = "with_gecko")] +use { + std::sync::atomic::{AtomicU32, Ordering}, + xpcom::interfaces::nsIXULRuntime, +}; + +use super::metrics::__glean_metric_maps; + +type EventRecord = (Instant, Option<HashMap<i32, 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 events: HashMap<MetricId, Vec<EventRecord>>, + pub memory_samples: HashMap<MetricId, Vec<u64>>, + pub string_lists: HashMap<MetricId, Vec<String>>, + pub timing_samples: HashMap<MetricId, Vec<u128>>, +} + +/// Global singleton: pending IPC payload. +static PAYLOAD: Lazy<Mutex<IPCPayload>> = Lazy::new(|| Mutex::new(IPCPayload::default())); + +pub fn with_ipc_payload<F, R>(f: F) -> R +where + F: FnOnce(&mut IPCPayload) -> R, +{ + 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(|| { + if let Some(appinfo) = xpcom::services::get_XULRuntime() { + let mut process_type = nsIXULRuntime::PROCESS_TYPE_DEFAULT as u32; + let rv = unsafe { appinfo.GetProcessType(&mut process_type) }; + if rv.succeeded() { + return AtomicU32::new(process_type); + } + } + AtomicU32::new(nsIXULRuntime::PROCESS_TYPE_DEFAULT as u32) +}); + +#[cfg(feature = "with_gecko")] +pub fn need_ipc() -> bool { + PROCESS_TYPE.load(Ordering::Relaxed) != nsIXULRuntime::PROCESS_TYPE_DEFAULT as u32 +} + +/// 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 + }) +} + +pub fn replay_from_buf(buf: &[u8]) -> Result<(), ()> { + let ipc_payload: IPCPayload = bincode::deserialize(buf).map_err(|_| ())?; + for (id, value) in ipc_payload.counters.into_iter() { + log::info!("Asked to replay {:?}, {:?}", id, value); + if let Some(metric) = __glean_metric_maps::COUNTER_MAP.get(&id) { + metric.add(value); + } + } + 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..e2cd29c7af --- /dev/null +++ b/toolkit/components/glean/api/src/lib.rs @@ -0,0 +1,23 @@ +// 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}; + +pub mod metrics; +pub mod pings; +pub mod private; + +pub mod ipc; + +#[cfg(test)] +mod common_test; +mod ffi; diff --git a/toolkit/components/glean/api/src/metrics.rs b/toolkit/components/glean/api/src/metrics.rs new file mode 100644 index 0000000000..9c2860bc29 --- /dev/null +++ b/toolkit/components/glean/api/src/metrics.rs @@ -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 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 +//! 'toolkit/components/glean/metrics.yaml`. + +#[cfg(not(feature = "cargo-clippy"))] +include!(concat!( + env!("MOZ_TOPOBJDIR"), + "/toolkit/components/glean/api/src/metrics.rs" +)); + +// When running clippy the linter, `MOZ_TOPOBJDIR` is not set +// (and the `metrics.rs` file might not even be generated yet), +// so we need to manually define some things we expect from it so the rest of the build can assume +// it's there. +// See https://bugzilla.mozilla.org/show_bug.cgi?id=1674726. +#[cfg(feature = "cargo-clippy")] +#[allow(dead_code)] +pub(crate) mod __glean_metric_maps { + use std::collections::HashMap; + + use crate::private::*; + use once_cell::sync::Lazy; + + pub static TIMESPAN_MAP: Lazy<HashMap<MetricId, &Lazy<TimespanMetric>>> = + Lazy::new(HashMap::new); + + pub static COUNTER_MAP: Lazy<HashMap<MetricId, &Lazy<CounterMetric>>> = Lazy::new(HashMap::new); + + pub static BOOLEAN_MAP: Lazy<HashMap<MetricId, &Lazy<BooleanMetric>>> = Lazy::new(HashMap::new); + + pub static DATETIME_MAP: Lazy<HashMap<MetricId, &Lazy<DatetimeMetric>>> = + Lazy::new(HashMap::new); + + pub static STRING_MAP: Lazy<HashMap<MetricId, &Lazy<StringMetric>>> = Lazy::new(HashMap::new); + + pub static MEMORY_DISTRIBUTION_MAP: Lazy<HashMap<MetricId, &Lazy<MemoryDistributionMetric>>> = + Lazy::new(HashMap::new); + + pub static STRING_LIST_MAP: Lazy<HashMap<MetricId, &Lazy<StringListMetric>>> = + Lazy::new(HashMap::new); + + pub static UUID_MAP: Lazy<HashMap<MetricId, &Lazy<UuidMetric>>> = Lazy::new(HashMap::new); + + pub(crate) fn event_record_wrapper( + _metric_id: u32, + _extra: Option<HashMap<i32, String>>, + ) -> Result<(), EventRecordingError> { + Err(EventRecordingError::InvalidId) + } + + pub(crate) fn event_record_wrapper_str( + _metric_id: u32, + _extra: Option<HashMap<String, String>>, + ) -> Result<(), EventRecordingError> { + Err(EventRecordingError::InvalidId) + } + + pub(crate) fn event_test_get_value_wrapper( + _metric_id: u32, + _storage_name: &str, + ) -> Option<Vec<RecordedEvent>> { + None + } +} diff --git a/toolkit/components/glean/api/src/pings.rs b/toolkit/components/glean/api/src/pings.rs new file mode 100644 index 0000000000..5837278566 --- /dev/null +++ b/toolkit/components/glean/api/src/pings.rs @@ -0,0 +1,15 @@ +// 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`. + +#[cfg(not(feature = "cargo-clippy"))] +include!(concat!( + env!("MOZ_TOPOBJDIR"), + "/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..8fd6dbbd0f --- /dev/null +++ b/toolkit/components/glean/api/src/private/boolean.rs @@ -0,0 +1,128 @@ +// 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_core::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(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(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(pub)] +impl Boolean for BooleanMetric { + /// Set to the specified boolean value. + /// + /// ## Arguments + /// + /// * `value` - the value to set. + fn set(&self, value: bool) { + match self { + BooleanMetric::Parent(p) => { + Boolean::set(&*p, 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. + fn test_get_value<'a, S: Into<Option<&'a str>>>(&self, ping_name: S) -> Option<bool> { + 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!",) + } + } + } +} + +#[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..207589adaf --- /dev/null +++ b/toolkit/components/glean/api/src/private/counter.rs @@ -0,0 +1,188 @@ +// 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_core::traits::Counter; + +use super::CommonMetricData; + +use crate::ipc::{need_ipc, with_ipc_payload}; +use crate::private::MetricId; + +/// 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: 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 = 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(pub)] +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. + fn add(&self, amount: i32) { + match self { + CounterMetric::Parent { inner, .. } => { + Counter::add(&*inner, 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. + fn test_get_value<'a, S: Into<Option<&'a str>>>(&self, ping_name: S) -> Option<i32> { + 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. + fn test_get_num_recorded_errors<'a, S: Into<Option<&'a str>>>( + &self, + error: glean::ErrorType, + ping_name: S, + ) -> i32 { + match self { + CounterMetric::Parent { inner, .. } => { + inner.test_get_num_recorded_errors(error, ping_name) + } + 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/datetime.rs b/toolkit/components/glean/api/src/private/datetime.rs new file mode 100644 index 0000000000..acaafdedd9 --- /dev/null +++ b/toolkit/components/glean/api/src/private/datetime.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 inherent::inherent; + +use super::{CommonMetricData, MetricId}; + +use super::TimeUnit; +use crate::ipc::need_ipc; +use chrono::{FixedOffset, TimeZone}; +use glean_core::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)), + _ => { + 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(pub)] +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. + fn set(&self, value: Option<glean_core::metrics::Datetime>) { + match self { + DatetimeMetric::Parent(p) => { + Datetime::set(&*p, 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`. + fn test_get_value<'a, S: Into<Option<&'a str>>>( + &self, + ping_name: S, + ) -> Option<glean_core::metrics::Datetime> { + 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. + fn test_get_num_recorded_errors<'a, S: Into<Option<&'a str>>>( + &self, + error: glean::ErrorType, + ping_name: S, + ) -> i32 { + match self { + DatetimeMetric::Parent(p) => p.test_get_num_recorded_errors(error, ping_name), + 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)); + + assert_eq!( + DateTime::parse_from_rfc3339("2020-05-07T11:58:00+05:00").unwrap(), + 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); + + assert_eq!( + DateTime::parse_from_rfc3339("2020-05-07T11:58:00+05:00").unwrap(), + 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)); + + { + 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)); + + // (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!( + DateTime::parse_from_rfc3339("2020-10-13T16:41:00+05:00").unwrap(), + parent_metric.test_get_value("store1").unwrap() + ); + } +} 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..306376c269 --- /dev/null +++ b/toolkit/components/glean/api/src/private/event.rs @@ -0,0 +1,237 @@ +// 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, Instant, MetricId, RecordedEvent}; + +use crate::ipc::{need_ipc, with_ipc_payload}; + +use glean_core::traits::Event; +pub use glean_core::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 } + } + } + + #[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"), + } + } +} + +#[inherent(pub)] +impl<K: 'static + ExtraKeys + Send + Sync> Event for EventMetric<K> { + type Extra = K; + + fn record<M: Into<Option<HashMap<K, String>>>>(&self, extra: M) { + match self { + EventMetric::Parent { inner, .. } => { + inner.record(extra); + } + EventMetric::Child(c) => { + let extra = extra.into().map(|hash_map| { + hash_map + .iter() + .map(|(k, v)| (k.index(), v.clone())) + .collect() + }); + let now = Instant::now(); + with_ipc_payload(move |payload| { + if let Some(v) = payload.events.get_mut(&c.0) { + v.push((now, extra)); + } else { + let mut v = vec![]; + v.push((now, extra)); + payload.events.insert(c.0, v); + } + }); + } + } + } + + 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!",) + } + } + } + + fn test_get_num_recorded_errors<'a, S: Into<Option<&'a str>>>( + &self, + error: glean::ErrorType, + ping_name: S, + ) -> i32 { + match self { + EventMetric::Parent { inner, .. } => { + inner.test_get_num_recorded_errors(error, ping_name) + } + 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] + #[ignore] // TODO: Enable them back when bug 1673668 lands. + 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] + #[ignore] // TODO: Enable them back when bug 1673668 lands. + fn smoke_test_event_with_extra() { + let _lock = lock_test(); + + #[derive(Clone, Copy, Debug, Hash, Eq, PartialEq)] + enum TestKeys { + Extra1, + } + + impl ExtraKeys for TestKeys { + const ALLOWED_KEYS: &'static [&'static str] = &["extra1"]; + + fn index(self) -> i32 { + self as i32 + } + } + + let metric = EventMetric::<TestKeys>::new( + 0.into(), + CommonMetricData { + name: "event_metric_with_extra".into(), + category: "telemetry".into(), + send_in_pings: vec!["store1".into()], + disabled: false, + ..Default::default() + }, + ); + + // No extra keys + metric.record(None); + + // A valid extra key + let mut map = HashMap::new(); + map.insert(TestKeys::Extra1, "a-valid-value".into()); + metric.record(map); + + let recorded = metric.test_get_value("store1").unwrap(); + + let events: Vec<&RecordedEvent> = recorded + .iter() + .filter(|&e| e.category == "telemetry" && e.name == "event_metric_with_extra") + .collect(); + assert_eq!(events.len(), 2); + assert_eq!(events[0].extra, None); + assert!(events[1].extra.as_ref().unwrap().get("extra1").unwrap() == "a-valid-value"); + } + + #[test] + #[ignore] // TODO: Enable them back when bug 1673668 lands. + fn event_ipc() { + use metrics::test_only_ipc::AnEventKeys; + + 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 mut map = HashMap::new(); + map.insert(AnEventKeys::Extra1, "a-child-value".into()); + child_metric.record(map); + } + + // Record in the parent after the child. + let mut map = HashMap::new(); + map.insert(AnEventKeys::Extra1, "a-valid-value".into()); + parent_metric.record(map); + + let recorded = parent_metric.test_get_value("store1").unwrap(); + + let events: Vec<&RecordedEvent> = recorded + .iter() + .filter(|&e| e.category == "test_only.ipc" && e.name == "an_event") + .collect(); + assert_eq!(events.len(), 2); + assert_eq!(events[0].extra, None); + + assert!(events[1].extra.as_ref().unwrap().get("extra1").unwrap() == "a-valid-value"); + // TODO: implement replay. See bug 1646165. Then change and add asserts for a-child-value. + // Ensure the replay values apply without error, at least. + let buf = ipc::take_buf().unwrap(); + assert!(buf.len() > 0); + assert!(ipc::replay_from_buf(&ipc::take_buf().unwrap()).is_ok()); + } +} 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..e7e4de61d2 --- /dev/null +++ b/toolkit/components/glean/api/src/private/labeled.rs @@ -0,0 +1,333 @@ +// 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::{BooleanMetric, CommonMetricData, CounterMetric, ErrorType, MetricId, StringMetric}; +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::{BooleanMetric, CounterMetric, StringMetric}; + + /// 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; + } + + // `LabeledMetric<BooleanMetric>` is possible. + // + // See [Labeled Booleans](https://mozilla.github.io/glean/book/user/metrics/labeled_booleans.html). + impl Sealed for BooleanMetric { + type GleanMetric = glean::private::BooleanMetric; + } + + // `LabeledMetric<StringMetric>` is possible. + // + // See [Labeled Strings](https://mozilla.github.io/glean/book/user/metrics/labeled_strings.html). + impl Sealed for StringMetric { + type GleanMetric = glean::private::StringMetric; + } + + // `LabeledMetric<CounterMetric>` is possible. + // + // See [Labeled Counters](https://mozilla.github.io/glean/book/user/metrics/labeled_counters.html). + impl Sealed for CounterMetric { + type GleanMetric = glean::private::CounterMetric; + } +} + +/// 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); +/// ``` +//#[derive(Debug)] +pub struct LabeledMetric<T: AllowLabeled> { + // TODO: the `_id` is currently not needed, hence the + // prefix, but it will be needed when adding IPC support + // to this type. + /// 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: id, core } + } +} + +#[inherent(pub)] +impl<U> glean_core::traits::Labeled<U::GleanMetric> 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. + fn get(&self, label: &str) -> U::GleanMetric { + if need_ipc() { + panic!("Use of labeled metrics in IPC land not yet implemented!"); + } else { + self.core.get(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. + fn test_get_num_recorded_errors<'a, S: Into<Option<&'a str>>>( + &self, + error: ErrorType, + ping_name: S, + ) -> i32 { + if need_ipc() { + panic!("Use of labeled metrics in IPC land not yet implemented!"); + } else { + self.core.test_get_num_recorded_errors(error, ping_name) + } + } +} + +#[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<BooleanMetric>> = 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<BooleanMetric> = 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<StringMetric> = 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<CounterMetric> = 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<BooleanMetric> = 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, None) + ); + } + + #[test] + fn predefined_labels() { + let _lock = lock_test(); + let store_names: Vec<String> = vec!["store1".into()]; + + let metric: LabeledMetric<BooleanMetric> = 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, None) + ); + } +} 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..4db7f73a2b --- /dev/null +++ b/toolkit/components/glean/api/src/private/memory_distribution.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/. + +use inherent::inherent; + +use super::{CommonMetricData, DistributionData, MemoryUnit, MetricId}; + +use glean_core::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(pub)] +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. + fn accumulate(&self, sample: u64) { + match self { + MemoryDistributionMetric::Parent { inner, .. } => { + MemoryDistribution::accumulate(&*inner, 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. + fn test_get_value<'a, S: Into<Option<&'a str>>>( + &self, + ping_name: S, + ) -> Option<DistributionData> { + 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. + fn test_get_num_recorded_errors<'a, S: Into<Option<&'a str>>>( + &self, + error: glean::ErrorType, + ping_name: S, + ) -> i32 { + match self { + MemoryDistributionMetric::Parent { inner, .. } => { + inner.test_get_num_recorded_errors(error, ping_name) + } + 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); + + // TODO: implement replay. See bug 1646165. + // For now, let's ensure there's something in the buffer and replay doesn't error. + let buf = ipc::take_buf().unwrap(); + assert!(buf.len() > 0); + assert!(ipc::replay_from_buf(&buf).is_ok()); + } +} 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..1e77e4b49e --- /dev/null +++ b/toolkit/components/glean/api/src/private/mod.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/. + +//! The different metric types supported by the Glean SDK to handle data. + +use std::convert::TryFrom; +use std::time::{SystemTime, UNIX_EPOCH}; + +use serde::{Deserialize, Serialize}; + +// Re-export of `glean_core` types we can re-use. +// That way a user only needs to depend on this crate, not on glean_core (and there can't be a +// version mismatch). +pub use glean_core::{ + metrics::DistributionData, metrics::MemoryUnit, metrics::RecordedEvent, metrics::TimeUnit, + CommonMetricData, ErrorType, Lifetime, +}; + +mod boolean; +mod counter; +mod datetime; +mod event; +mod labeled; +mod memory_distribution; +mod ping; +pub(crate) mod string; +mod string_list; +mod timespan; +mod timing_distribution; +mod uuid; + +pub use self::boolean::BooleanMetric; +pub use self::counter::CounterMetric; +pub use self::datetime::DatetimeMetric; +pub use self::event::{EventMetric, EventRecordingError, ExtraKeys, NoExtraKeys}; +pub use self::labeled::LabeledMetric; +pub use self::memory_distribution::MemoryDistributionMetric; +pub use self::ping::Ping; +pub use self::string::StringMetric; +pub use self::string_list::StringListMetric; +pub use self::timespan::TimespanMetric; +pub use self::timing_distribution::TimingDistributionMetric; +pub use self::uuid::UuidMetric; + +/// An instant in time. +/// +/// Similar to [`std::time::Instant`](https://doc.rust-lang.org/std/time/struct.Instant.html), +/// but much simpler in that we explicitly expose that it's just an integer. +/// +/// This is needed, as the current `glean-core` API expects timestamps as integers. +/// We probably should move this API into `glean-core` directly. +/// See [Bug 1619253](https://bugzilla.mozilla.org/show_bug.cgi?id=1619253). +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Instant(u64); + +impl Instant { + /// Returns an instant corresponding to "now". + fn now() -> Instant { + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("SystemTime before UNIX epoch!"); + let now = now.as_nanos(); + + match u64::try_from(now) { + Ok(now) => Instant(now), + Err(_) => { + // Greetings to 2554 from 2020! + panic!("timestamp exceeds value range") + } + } + } +} + +/// Uniquely identifies a single metric within its metric type. +#[derive(Debug, PartialEq, Eq, Hash, Copy, Clone, Deserialize, Serialize)] +#[repr(transparent)] +pub struct MetricId(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/ping.rs b/toolkit/components/glean/api/src/private/ping.rs new file mode 100644 index 0000000000..2b11f1ad5b --- /dev/null +++ b/toolkit/components/glean/api/src/private/ping.rs @@ -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 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, Debug)] +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, + )) + } + } +} + +#[inherent(pub)] +impl glean_core::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. + fn submit(&self, reason: Option<&str>) { + match self { + Ping::Parent(p) => { + glean_core::traits::Ping::submit(p, reason); + } + Ping::Child => { + log::error!( + "Unable to submit ping {:?} in non-main process. Ignoring.", + self + ); + // TODO: Record an 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 PROTOTYPE_PING: Lazy<Ping> = Lazy::new(|| Ping::new("prototype", false, true, vec![])); + + #[test] + fn smoke_test_custom_ping() { + let _lock = lock_test(); + + // We can only check that nothing explodes. + // More comprehensive tests are blocked on bug 1673660. + PROTOTYPE_PING.submit(None); + } +} 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..9beb11f3f1 --- /dev/null +++ b/toolkit/components/glean/api/src/private/string.rs @@ -0,0 +1,185 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use inherent::inherent; + +use 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(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(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(pub)] +impl glean_core::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. + fn set<S: Into<std::string::String>>(&self, value: S) { + match self { + StringMetric::Parent(p) => { + glean_core::traits::String::set(&*p, value); + } + 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`. + fn test_get_value<'a, S: Into<Option<&'a str>>>( + &self, + ping_name: S, + ) -> Option<std::string::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. + fn test_get_num_recorded_errors<'a, S: Into<Option<&'a str>>>( + &self, + error: glean::ErrorType, + ping_name: S, + ) -> i32 { + match self { + StringMetric::Parent(p) => p.test_get_num_recorded_errors(error, ping_name), + 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..a583b1fced --- /dev/null +++ b/toolkit/components/glean/api/src/private/string_list.rs @@ -0,0 +1,212 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use inherent::inherent; + +use super::{CommonMetricData, MetricId}; + +use glean_core::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(pub)] +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). + fn add<S: Into<String>>(&self, value: S) { + match self { + StringListMetric::Parent { inner, .. } => { + StringList::add(&*inner, value); + } + 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 mut v = vec![]; + v.push(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. + fn set(&self, value: Vec<String>) { + match self { + StringListMetric::Parent { inner, .. } => { + StringList::set(&*inner, 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. + fn test_get_value<'a, S: Into<Option<&'a str>>>(&self, ping_name: S) -> Option<Vec<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. + fn test_get_num_recorded_errors<'a, S: Into<Option<&'a str>>>( + &self, + error: glean::ErrorType, + ping_name: S, + ) -> i32 { + match self { + StringListMetric::Parent { inner, .. } => { + inner.test_get_num_recorded_errors(error, ping_name) + } + 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..af740d5a41 --- /dev/null +++ b/toolkit/components/glean/api/src/private/timespan.rs @@ -0,0 +1,134 @@ +// 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 glean_core::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), + 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)) + } + } +} + +#[inherent(pub)] +impl Timespan for TimespanMetric { + fn start(&self) { + match self { + TimespanMetric::Parent(p) => Timespan::start(p), + TimespanMetric::Child => { + log::error!("Unable to start timespan metric in non-main process. Ignoring."); + // TODO: Record an error. + } + } + } + + fn stop(&self) { + match self { + TimespanMetric::Parent(p) => Timespan::stop(p), + TimespanMetric::Child => { + log::error!("Unable to stop timespan metric in non-main process. Ignoring."); + // TODO: Record an error. + } + } + } + + fn cancel(&self) { + match self { + TimespanMetric::Parent(p) => Timespan::cancel(p), + TimespanMetric::Child => { + log::error!("Unable to cancel timespan metric in non-main process. Ignoring."); + // TODO: Record an error. + } + } + } + + fn test_get_value<'a, S: Into<Option<&'a str>>>(&self, ping_name: S) -> Option<u64> { + match self { + TimespanMetric::Parent(p) => p.test_get_value(ping_name), + TimespanMetric::Child => { + panic!("Cannot get test value for in non-parent process!"); + } + } + } + + fn test_get_num_recorded_errors<'a, S: Into<Option<&'a str>>>( + &self, + error: glean::ErrorType, + ping_name: S, + ) -> i32 { + match self { + TimespanMetric::Parent(p) => p.test_get_num_recorded_errors(error, ping_name), + 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..bfab77cc4e --- /dev/null +++ b/toolkit/components/glean/api/src/private/timing_distribution.rs @@ -0,0 +1,273 @@ +// 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::Instant; + +use super::{CommonMetricData, MetricId, TimeUnit}; +use glean::{DistributionData, ErrorType}; +use glean_core::metrics::TimerId; + +use crate::ipc::{need_ipc, with_ipc_payload}; +use glean_core::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. + /// + /// **TEST-ONLY** - Do not use unless gated with `#[cfg(test)]`. + 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") + } + } + } +} + +#[inherent(pub)] +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. + fn start(&self) -> TimerId { + match self { + TimingDistributionMetric::Parent { inner, .. } => inner.start(), + 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. + } + id + } + } + } + + /// 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. + fn stop_and_accumulate(&self, id: TimerId) { + match self { + TimingDistributionMetric::Parent { inner, .. } => { + inner.stop_and_accumulate(id); + } + TimingDistributionMetric::Child(c) => { + let mut map = c + .instants + .write() + .expect("Write lock must've been poisoned."); + if let Some(start) = map.remove(&id) { + let sample = start.elapsed().as_nanos(); + 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. + fn cancel(&self, id: TimerId) { + match self { + TimingDistributionMetric::Parent { inner, .. } => { + inner.cancel(id); + } + TimingDistributionMetric::Child(c) => { + let mut map = c + .instants + .write() + .expect("Write lock must've been poisoned."); + if map.remove(&id).is_none() { + // TODO: report an error (cancelled a non-started 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`. + fn test_get_value<'a, S: Into<Option<&'a str>>>( + &self, + ping_name: S, + ) -> Option<DistributionData> { + 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. + fn test_get_num_recorded_errors<'a, S: Into<Option<&'a str>>>( + &self, + error: ErrorType, + ping_name: S, + ) -> i32 { + match self { + TimingDistributionMetric::Parent { inner, .. } => { + inner.test_get_num_recorded_errors(error, ping_name) + } + 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); + } + + // TODO: implement replay. See bug 1646165. + // For now let's ensure there's something in the buffer and replay doesn't error. + let buf = ipc::take_buf().unwrap(); + assert!(buf.len() > 0); + assert!(ipc::replay_from_buf(&buf).is_ok()); + } +} 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..fcffc640fc --- /dev/null +++ b/toolkit/components/glean/api/src/private/uuid.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/. + +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(pub)] +impl glean_core::traits::Uuid for UuidMetric { + /// Set to the specified value. + /// + /// ## Arguments + /// + /// * `value` - The UUID to set the metric to. + fn set(&self, value: Uuid) { + match self { + UuidMetric::Parent(p) => { + glean_core::traits::Uuid::set(&*p, value); + } + 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. + fn generate_and_set(&self) -> Uuid { + match self { + UuidMetric::Parent(p) => glean_core::traits::Uuid::generate_and_set(&*p), + 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. + fn test_get_value<'a, S: Into<Option<&'a str>>>(&self, storage_name: S) -> Option<Uuid> { + match self { + UuidMetric::Parent(p) => p.test_get_value(storage_name), + 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. + fn test_get_num_recorded_errors<'a, S: Into<Option<&'a str>>>( + &self, + error: glean::ErrorType, + ping_name: S, + ) -> i32 { + match self { + UuidMetric::Parent(p) => p.test_get_num_recorded_errors(error, ping_name), + 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..707482eaec --- /dev/null +++ b/toolkit/components/glean/bindings/Category.cpp @@ -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/. */ + +#include "mozilla/dom/GleanBinding.h" +#include "mozilla/glean/bindings/Glean.h" +#include "mozilla/glean/bindings/Category.h" +#include "mozilla/glean/bindings/GleanJSMetricsLookup.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(GetCategoryName(mId), mLength); + metricName.AppendLiteral("."); + AppendUTF16toUTF8(aName, metricName); + + Maybe<uint32_t> 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) { + const char* category = GetCategoryName(mId); + + for (metric_entry_t entry : sMetricByNameLookupEntries) { + const char* identifier = GetMetricIdentifier(entry); + + // 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. + // We need to check the category first to ensure the string is at least that + // long, so the check at `mLength` is valid. + if (strncmp(category, identifier, mLength) == 0 && + identifier[mLength] == '.') { + const char* metricName = &identifier[mLength + 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..b4a6af01b1 --- /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 "mozilla/dom/BindingDeclarations.h" +#include "nsISupports.h" +#include "nsWrapperCache.h" + +namespace mozilla::glean { + +class Category final : public nsISupports, public nsWrapperCache { + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(Category) + + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + nsISupports* GetParentObject() { return nullptr; } + + Category(uint32_t id, uint32_t length) : mId(id), mLength(length) {} + + already_AddRefed<nsISupports> NamedGetter(const nsAString& aName, + bool& aFound); + bool NameIsEnumerable(const nsAString& aName); + void GetSupportedNames(nsTArray<nsString>& aNames); + + private: + uint32_t mId; + uint32_t mLength; + + 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..3c6e023be7 --- /dev/null +++ b/toolkit/components/glean/bindings/Glean.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/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 "MainThreadUtils.h" + +namespace mozilla::glean { + +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) { + Maybe<uint32_t> categoryIdx = + CategoryByNameLookup(NS_ConvertUTF16toUTF8(aName)); + if (categoryIdx.isNothing()) { + aFound = false; + return nullptr; + } + + aFound = true; + uint32_t length = strlen(&gCategoryStringTable[categoryIdx.value()]); + return MakeAndAddRef<Category>(categoryIdx.value(), length); +} + +bool Glean::NameIsEnumerable(const nsAString& aName) { return false; } + +void Glean::GetSupportedNames(nsTArray<nsString>& aNames) { + for (category_entry_t entry : sCategoryByNameLookupEntries) { + const char* categoryName = GetCategoryName(entry); + aNames.AppendElement()->AssignASCII(categoryName); + } +} + +} // 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..dfccc2c3d2 --- /dev/null +++ b/toolkit/components/glean/bindings/Glean.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_Glean_h +#define mozilla_glean_Glean_h + +#include "mozilla/dom/BindingDeclarations.h" +#include "mozilla/glean/bindings/Category.h" +#include "nsISupports.h" +#include "nsWrapperCache.h" + +namespace mozilla::glean { + +class Glean final : public nsISupports, public nsWrapperCache { + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_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); + + 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..1af4124111 --- /dev/null +++ b/toolkit/components/glean/bindings/GleanPings.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/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" + +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) { + Maybe<uint32_t> pingId = PingByNameLookup(NS_ConvertUTF16toUTF8(aName)); + 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..55eb1c4c9b --- /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_SCRIPT_HOLDER_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..a6de2e5f7e --- /dev/null +++ b/toolkit/components/glean/bindings/MetricTypes.h @@ -0,0 +1,19 @@ +/* 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/Datetime.h" +#include "mozilla/glean/bindings/Event.h" +#include "mozilla/glean/bindings/MemoryDistribution.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/Uuid.h" + +#endif // mozilla_Glean_MetricTypes_h diff --git a/toolkit/components/glean/bindings/private/Boolean.cpp b/toolkit/components/glean/bindings/private/Boolean.cpp new file mode 100644 index 0000000000..efcf63dad9 --- /dev/null +++ b/toolkit/components/glean/bindings/private/Boolean.cpp @@ -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/. */ + +#include "mozilla/glean/bindings/Boolean.h" + +#include "nsString.h" +#include "mozilla/Components.h" +#include "nsIClassInfoImpl.h" + +namespace mozilla { +namespace glean { + +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::MutableHandleValue aResult) { + auto result = mBoolean.TestGetValue(aStorageName); + if (result.isNothing()) { + aResult.set(JS::UndefinedValue()); + } else { + aResult.set(JS::BooleanValue(result.value())); + } + return NS_OK; +} + +} // namespace glean +} // namespace mozilla diff --git a/toolkit/components/glean/bindings/private/Boolean.h b/toolkit/components/glean/bindings/private/Boolean.h new file mode 100644 index 0000000000..a26bc38699 --- /dev/null +++ b/toolkit/components/glean/bindings/private/Boolean.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_GleanBoolean_h +#define mozilla_glean_GleanBoolean_h + +#include "nsIGleanMetrics.h" +#include "mozilla/glean/fog_ffi_generated.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 value the value to set. + */ + void Set(bool value) const { fog_boolean_set(mId, int(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. + */ + Maybe<bool> TestGetValue(const nsACString& aPingName = nsCString()) const { + if (!fog_boolean_test_has_value(mId, &aPingName)) { + return Nothing(); + } + return Some(fog_boolean_test_get_value(mId, &aPingName)); + } + + 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..847cfe295b --- /dev/null +++ b/toolkit/components/glean/bindings/private/Common.cpp @@ -0,0 +1,32 @@ +/* -*- 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 "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", + 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..88aae69dc2 --- /dev/null +++ b/toolkit/components/glean/bindings/private/Common.h @@ -0,0 +1,25 @@ +/* -*- 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 "jsapi.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..2b8a429dc8 --- /dev/null +++ b/toolkit/components/glean/bindings/private/Counter.cpp @@ -0,0 +1,36 @@ +/* -*- 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 "nsIClassInfoImpl.h" + +namespace mozilla::glean { + +NS_IMPL_CLASSINFO(GleanCounter, nullptr, 0, {0}) +NS_IMPL_ISUPPORTS_CI(GleanCounter, nsIGleanCounter) + +NS_IMETHODIMP +GleanCounter::Add(uint32_t aAmount) { + mCounter.Add(aAmount); + return NS_OK; +} + +NS_IMETHODIMP +GleanCounter::TestGetValue(const nsACString& aStorageName, + JS::MutableHandleValue aResult) { + auto result = mCounter.TestGetValue(aStorageName); + if (result.isNothing()) { + aResult.set(JS::UndefinedValue()); + } else { + aResult.set(JS::Int32Value(result.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..bafeb0b467 --- /dev/null +++ b/toolkit/components/glean/bindings/private/Counter.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_GleanCounter_h +#define mozilla_glean_GleanCounter_h + +#include "mozilla/Maybe.h" +#include "nsIGleanMetrics.h" +#include "mozilla/glean/fog_ffi_generated.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 { fog_counter_add(mId, aAmount); } + + /** + * **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. + */ + Maybe<int32_t> TestGetValue(const nsACString& aPingName = nsCString()) const { + if (!fog_counter_test_has_value(mId, &aPingName)) { + return Nothing(); + } + return Some(fog_counter_test_get_value(mId, &aPingName)); + } + + 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/Datetime.cpp b/toolkit/components/glean/bindings/private/Datetime.cpp new file mode 100644 index 0000000000..d01616b4ec --- /dev/null +++ b/toolkit/components/glean/bindings/private/Datetime.cpp @@ -0,0 +1,46 @@ +/* -*- 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 "nsString.h" +#include "mozilla/Components.h" +#include "nsIClassInfoImpl.h" +#include "prtime.h" + +namespace mozilla::glean { + +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::MutableHandleValue aResult) { + auto result = mDatetime.TestGetValue(aStorageName); + if (result.isNothing()) { + aResult.set(JS::UndefinedValue()); + } else { + const NS_ConvertUTF8toUTF16 str(result.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/Datetime.h b/toolkit/components/glean/bindings/private/Datetime.h new file mode 100644 index 0000000000..748766f3be --- /dev/null +++ b/toolkit/components/glean/bindings/private/Datetime.h @@ -0,0 +1,91 @@ +/* -*- 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 "nsIGleanMetrics.h" +#include "mozilla/glean/fog_ffi_generated.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 { + PRExplodedTime exploded; + if (!aValue) { + PR_ExplodeTime(PR_Now(), PR_LocalTimeParameters, &exploded); + } else { + exploded = *aValue; + } + + int32_t offset = + exploded.tm_params.tp_gmt_offset + exploded.tm_params.tp_dst_offset; + fog_datetime_set(mId, exploded.tm_year, exploded.tm_month + 1, + exploded.tm_mday, exploded.tm_hour, exploded.tm_min, + exploded.tm_sec, exploded.tm_usec * 1000, offset); + } + + /** + * **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. + */ + Maybe<nsCString> TestGetValue( + const nsACString& aPingName = nsCString()) const { + if (!fog_datetime_test_has_value(mId, &aPingName)) { + return Nothing(); + } + nsCString ret; + fog_datetime_test_get_value(mId, &aPingName, &ret); + return Some(ret); + } + + 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/DistributionData.h b/toolkit/components/glean/bindings/private/DistributionData.h new file mode 100644 index 0000000000..2b75c323c1 --- /dev/null +++ b/toolkit/components/glean/bindings/private/DistributionData.h @@ -0,0 +1,20 @@ +/* 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 "nsDataHashtable.h" + +namespace mozilla::glean { + +struct DistributionData final { + uint64_t sum; + nsDataHashtable<nsUint64HashKey, uint64_t> values; +}; + +} // 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..7bbe7a865b --- /dev/null +++ b/toolkit/components/glean/bindings/private/Event.cpp @@ -0,0 +1,91 @@ +/* -*- 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 "nsIClassInfoImpl.h" +#include "jsapi.h" +#include "nsIScriptError.h" + +namespace mozilla::glean { + +NS_IMPL_CLASSINFO(GleanEvent, nullptr, 0, {0}) +NS_IMPL_ISUPPORTS_CI(GleanEvent, nsIGleanEvent) + +NS_IMETHODIMP +GleanEvent::Record(JS::HandleValue 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; + + JS::RootedObject 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() || !jsValue.init(aCx, value)) { + LogToBrowserConsole(nsIScriptError::warningFlag, + u"Extra properties should have string values."_ns); + return NS_OK; + } + + extraKeys.AppendElement(jsKey); + extraValues.AppendElement(jsValue); + } + + // Calling the implementation directly, because we have a `string->string` + // map, not a `T->string` map the C++ API expects. + impl::fog_event_record_str(mEvent.mId, &extraKeys, &extraValues); + return NS_OK; +} + +NS_IMETHODIMP +GleanEvent::TestGetValue(const nsACString& aStorageName, JSContext* aCx, + JS::MutableHandleValue aResult) { + auto result = mEvent.TestGetValue(aStorageName); + if (result.isNothing()) { + aResult.set(JS::UndefinedValue()); + return NS_OK; + } + + // TODO(bug 1678567): Implement this. + return NS_ERROR_FAILURE; +} + +} // 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..d452ffe316 --- /dev/null +++ b/toolkit/components/glean/bindings/private/Event.h @@ -0,0 +1,113 @@ +/* -*- 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/fog_ffi_generated.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 Span<const Tuple<T, nsCString>>& aExtras = {}) const { + static_assert(sizeof(T) <= sizeof(int32_t), + "Extra keys need to fit into 32 bits"); + + nsTArray<int32_t> extraKeys; + nsTArray<nsCString> extraValues; + for (auto& entry : aExtras) { + extraKeys.AppendElement(static_cast<int32_t>(mozilla::Get<0>(entry))); + extraValues.AppendElement(mozilla::Get<1>(entry)); + } + + fog_event_record(mId, &extraKeys, &extraValues); + } + + /** + * **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. + */ + Maybe<nsTArray<RecordedEvent>> TestGetValue( + const nsACString& aPingName = nsCString()) const { + if (!fog_event_test_has_value(mId, &aPingName)) { + return Nothing(); + } + + // TODO(bug 1678567): Implement this. + nsTArray<RecordedEvent> empty; + return Some(std::move(empty)); + } + + private: + const uint32_t mId; +}; + +} // namespace impl + +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<uint32_t> mEvent; +}; + +} // namespace mozilla::glean + +#endif /* mozilla_glean_GleanEvent.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..82043d8ddb --- /dev/null +++ b/toolkit/components/glean/bindings/private/MemoryDistribution.cpp @@ -0,0 +1,64 @@ +/* -*- 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 "nsIClassInfoImpl.h" +#include "nsJSUtils.h" +#include "nsPrintfCString.h" +#include "nsString.h" + +namespace mozilla::glean { + +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::MutableHandleValue aResult) { + auto result = mMemoryDist.TestGetValue(aPingName); + if (result.isNothing()) { + aResult.set(JS::UndefinedValue()); + } else { + // Build return value of the form: { sum: #, values: {bucket1: count1, ...} + JS::RootedObject root(aCx, JS_NewPlainObject(aCx)); + if (!root) { + return NS_ERROR_FAILURE; + } + uint64_t sum = result.ref().sum; + if (!JS_DefineProperty(aCx, root, "sum", static_cast<double>(sum), + JSPROP_ENUMERATE)) { + return NS_ERROR_FAILURE; + } + JS::RootedObject valuesObj(aCx, JS_NewPlainObject(aCx)); + if (!valuesObj || + !JS_DefineProperty(aCx, root, "values", valuesObj, JSPROP_ENUMERATE)) { + return NS_ERROR_FAILURE; + } + auto& data = result.ref().values; + for (auto iter = data.ConstIter(); !iter.Done(); iter.Next()) { + const uint64_t bucket = iter.Key(); + const uint64_t count = iter.UserData(); + 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..155ffa115c --- /dev/null +++ b/toolkit/components/glean/bindings/private/MemoryDistribution.h @@ -0,0 +1,90 @@ +/* -*- 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/glean/fog_ffi_generated.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(uint64_t aSample) const { + fog_memory_distribution_accumulate(mId, 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 Nothing() if there is no value. + */ + Maybe<DistributionData> TestGetValue( + const nsACString& aPingName = nsCString()) const { + if (!fog_memory_distribution_test_has_value(mId, &aPingName)) { + return Nothing(); + } + nsTArray<uint64_t> buckets; + nsTArray<uint64_t> counts; + DistributionData ret; + fog_memory_distribution_test_get_value(mId, &aPingName, &ret.sum, &buckets, + &counts); + for (size_t i = 0; i < buckets.Length(); ++i) { + ret.values.Put(buckets[i], counts[i]); + } + return Some(std::move(ret)); + } + + 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/Ping.cpp b/toolkit/components/glean/bindings/private/Ping.cpp new file mode 100644 index 0000000000..61e0434b78 --- /dev/null +++ b/toolkit/components/glean/bindings/private/Ping.cpp @@ -0,0 +1,24 @@ +/* -*- 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" + +#include "mozilla/Components.h" +#include "nsIClassInfoImpl.h" +#include "nsString.h" + +namespace mozilla::glean { + +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; +} + +} // 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..14c2dd8646 --- /dev/null +++ b/toolkit/components/glean/bindings/private/Ping.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_Ping_h +#define mozilla_glean_Ping_h + +#include "mozilla/glean/fog_ffi_generated.h" +#include "mozilla/Maybe.h" +#include "nsIGleanMetrics.h" +#include "nsString.h" + +namespace mozilla::glean { + +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 { + fog_submit_ping_by_id(mId, &aReason); + } + + 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/String.cpp b/toolkit/components/glean/bindings/private/String.cpp new file mode 100644 index 0000000000..6c5d9909af --- /dev/null +++ b/toolkit/components/glean/bindings/private/String.cpp @@ -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/. */ + +#include "mozilla/glean/bindings/String.h" + +#include "nsString.h" +#include "mozilla/Components.h" +#include "nsIClassInfoImpl.h" + +namespace mozilla::glean { + +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::MutableHandleValue aResult) { + auto result = mString.TestGetValue(aStorageName); + if (result.isNothing()) { + aResult.set(JS::UndefinedValue()); + } else { + const NS_ConvertUTF8toUTF16 str(result.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/String.h b/toolkit/components/glean/bindings/private/String.h new file mode 100644 index 0000000000..5aff67171a --- /dev/null +++ b/toolkit/components/glean/bindings/private/String.h @@ -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/. */ + +#ifndef mozilla_glean_GleanString_h +#define mozilla_glean_GleanString_h + +#include "mozilla/Maybe.h" +#include "nsIGleanMetrics.h" +#include "mozilla/glean/fog_ffi_generated.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 { fog_string_set(mId, &aValue); } + + /** + * **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. + */ + Maybe<nsCString> TestGetValue( + const nsACString& aPingName = nsCString()) const { + if (!fog_string_test_has_value(mId, &aPingName)) { + return Nothing(); + } + nsCString ret; + fog_string_test_get_value(mId, &aPingName, &ret); + return Some(ret); + } + + 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..e2faeec0ba --- /dev/null +++ b/toolkit/components/glean/bindings/private/StringList.cpp @@ -0,0 +1,46 @@ +/* -*- 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 "mozilla/Components.h" +#include "mozilla/dom/ToJSValue.h" +#include "nsIClassInfoImpl.h" +#include "nsString.h" +#include "nsTArray.h" + +namespace mozilla::glean { + +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::MutableHandleValue aResult) { + auto result = mStringList.TestGetValue(aStorageName); + if (result.isNothing()) { + aResult.set(JS::UndefinedValue()); + } else { + if (!dom::ToJSValue(aCx, result.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..c2774e30ac --- /dev/null +++ b/toolkit/components/glean/bindings/private/StringList.h @@ -0,0 +1,96 @@ +/* -*- 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 "mozilla/glean/fog_ffi_generated.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 { + fog_string_list_add(mId, &aValue); + } + + /* + * 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 { + fog_string_list_set(mId, &aValue); + } + + /** + * **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. + */ + Maybe<nsTArray<nsCString>> TestGetValue( + const nsACString& aPingName = nsCString()) const { + if (!fog_string_list_test_has_value(mId, &aPingName)) { + return Nothing(); + } + nsTArray<nsCString> ret; + fog_string_list_test_get_value(mId, &aPingName, &ret); + return Some(std::move(ret)); + } + + 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..0c2dd5125e --- /dev/null +++ b/toolkit/components/glean/bindings/private/Timespan.cpp @@ -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/. */ + +#include "mozilla/glean/bindings/Timespan.h" + +#include "nsString.h" +#include "mozilla/Components.h" +#include "nsIClassInfoImpl.h" + +namespace mozilla::glean { + +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::TestGetValue(const nsACString& aStorageName, + JS::MutableHandleValue aResult) { + auto result = mTimespan.TestGetValue(aStorageName); + if (result.isNothing()) { + aResult.set(JS::UndefinedValue()); + } else { + aResult.set(JS::DoubleValue(result.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..6425c03046 --- /dev/null +++ b/toolkit/components/glean/bindings/private/Timespan.h @@ -0,0 +1,85 @@ +/* -*- 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 "nsIGleanMetrics.h" +#include "mozilla/glean/fog_ffi_generated.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 { fog_timespan_start(mId); } + + /** + * 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 { fog_timespan_stop(mId); } + + /** + * **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. + */ + Maybe<int64_t> TestGetValue(const nsACString& aPingName = nsCString()) const { + if (!fog_timespan_test_has_value(mId, &aPingName)) { + return Nothing(); + } + return Some(fog_timespan_test_get_value(mId, &aPingName)); + } + + 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..129adea79d --- /dev/null +++ b/toolkit/components/glean/bindings/private/TimingDistribution.cpp @@ -0,0 +1,79 @@ +/* -*- 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 "mozilla/Components.h" +#include "mozilla/dom/ToJSValue.h" +#include "nsIClassInfoImpl.h" +#include "nsJSUtils.h" +#include "nsPrintfCString.h" +#include "nsString.h" + +namespace mozilla::glean { + +NS_IMPL_CLASSINFO(GleanTimingDistribution, nullptr, 0, {0}) +NS_IMPL_ISUPPORTS_CI(GleanTimingDistribution, nsIGleanTimingDistribution) + +NS_IMETHODIMP +GleanTimingDistribution::Start(JSContext* aCx, JS::MutableHandleValue 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::MutableHandleValue aResult) { + auto result = mTimingDist.TestGetValue(aPingName); + if (result.isNothing()) { + aResult.set(JS::UndefinedValue()); + } else { + // Build return value of the form: { sum: #, values: {bucket1: count1, ...} + JS::RootedObject root(aCx, JS_NewPlainObject(aCx)); + if (!root) { + return NS_ERROR_FAILURE; + } + uint64_t sum = result.ref().sum; + if (!JS_DefineProperty(aCx, root, "sum", static_cast<double>(sum), + JSPROP_ENUMERATE)) { + return NS_ERROR_FAILURE; + } + JS::RootedObject valuesObj(aCx, JS_NewPlainObject(aCx)); + if (!valuesObj || + !JS_DefineProperty(aCx, root, "values", valuesObj, JSPROP_ENUMERATE)) { + return NS_ERROR_FAILURE; + } + auto& data = result.ref().values; + for (auto iter = data.ConstIter(); !iter.Done(); iter.Next()) { + const uint64_t bucket = iter.Key(); + const uint64_t count = iter.UserData(); + 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/TimingDistribution.h b/toolkit/components/glean/bindings/private/TimingDistribution.h new file mode 100644 index 0000000000..6217d0fa42 --- /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/glean/fog_ffi_generated.h" +#include "mozilla/Maybe.h" +#include "nsDataHashtable.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 { return fog_timing_distribution_start(mId); } + + /* + * 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(TimerId&& aId) const { + fog_timing_distribution_stop_and_accumulate(mId, aId); + } + + /* + * 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(TimerId&& aId) const { fog_timing_distribution_cancel(mId, 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 Nothing() if there is no value. + */ + Maybe<DistributionData> TestGetValue( + const nsACString& aPingName = nsCString()) const { + if (!fog_timing_distribution_test_has_value(mId, &aPingName)) { + return Nothing(); + } + nsTArray<uint64_t> buckets; + nsTArray<uint64_t> counts; + DistributionData ret; + fog_timing_distribution_test_get_value(mId, &aPingName, &ret.sum, &buckets, + &counts); + for (size_t i = 0; i < buckets.Length(); ++i) { + ret.values.Put(buckets[i], counts[i]); + } + return Some(std::move(ret)); + } + + 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/Uuid.cpp b/toolkit/components/glean/bindings/private/Uuid.cpp new file mode 100644 index 0000000000..d119fcb5ec --- /dev/null +++ b/toolkit/components/glean/bindings/private/Uuid.cpp @@ -0,0 +1,44 @@ +/* -*- 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 "nsString.h" +#include "mozilla/Components.h" +#include "nsIClassInfoImpl.h" + +namespace mozilla::glean { + +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::MutableHandleValue aResult) { + auto result = mUuid.TestGetValue(aStorageName); + if (result.isNothing()) { + aResult.set(JS::UndefinedValue()); + } else { + const NS_ConvertUTF8toUTF16 str(result.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..5c3690e724 --- /dev/null +++ b/toolkit/components/glean/bindings/private/Uuid.h @@ -0,0 +1,82 @@ +/* -*- 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 "nsIGleanMetrics.h" +#include "nsString.h" +#include "mozilla/glean/fog_ffi_generated.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 { fog_uuid_set(mId, &aValue); } + + /* + * Generate a new random UUID and set the metric to it. + */ + void GenerateAndSet() const { fog_uuid_generate_and_set(mId); } + + /** + * **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. + */ + Maybe<nsCString> TestGetValue( + const nsACString& aPingName = nsCString()) const { + if (!fog_uuid_test_has_value(mId, &aPingName)) { + return Nothing(); + } + nsCString ret; + fog_uuid_test_get_value(mId, &aPingName, &ret); + return Some(ret); + } + + 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..64b9ab489a --- /dev/null +++ b/toolkit/components/glean/build_scripts/glean_parser_ext/cpp.py @@ -0,0 +1,112 @@ +# -*- 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 jinja2 +import json + +from util import generate_metric_ids, generate_ping_ids, is_implemented_metric_type +from glean_parser import util + + +def cpp_datatypes_filter(value): + """ + A Jinja2 filter that renders Rust 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. + """ + + 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<uint32_t>" + else: + return "{}Metric<{}>".format( + util.Camelize(obj.type), util.Camelize(obj.name) + suffix + ) + return util.Camelize(obj.type) + "Metric" + + +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 len(objs) == 1 and "pings" in objs: + template_filename = "cpp_pings.jinja2" + else: + template_filename = "cpp.jinja2" + + template = util.get_jinja2_template( + template_filename, + filters=( + ("cpp", cpp_datatypes_filter), + ("snake_case", util.snake_case), + ("type_name", type_name), + ("metric_id", get_metric_id), + ("ping_id", get_ping_id), + ("is_implemented_type", is_implemented_metric_type), + ("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/js.py b/toolkit/components/glean/build_scripts/glean_parser_ext/js.py new file mode 100644 index 0000000000..2334bc7a39 --- /dev/null +++ b/toolkit/components/glean/build_scripts/glean_parser_ext/js.py @@ -0,0 +1,254 @@ +# -*- 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 perfecthash import PerfectHash +from string_table import StringTable + +from util import generate_metric_ids, generate_ping_ids, is_implemented_metric_type +from glean_parser import util + +""" +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. + +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. 27 bit for the metric ID. That allows for 130 million metrics. Let's not go there. + +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 + +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(type): + """ + Returns the C++ type to use for a given metric object. + """ + + return "Glean" + util.Camelize(type) + + +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 len(objs) == 1 and "pings" in objs: + write_pings(objs, output_fd, "js_pings.jinja2") + else: + write_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, + filters=( + ("type_name", type_name), + ("is_implemented_type", is_implemented_metric_type), + ), + ) + + assert ( + INDEX_BITS + ID_BITS < ENTRY_WIDTH + ), "INDEX_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 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) + if metric.type in metric_type_ids: + type_id = metric_type_ids[metric.type] + else: + type_id = len(metric_type_ids) + 1 + metric_type_ids[metric.type] = type_id + + 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]), + 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]), + 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, + 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..3d4ac2f3cc --- /dev/null +++ b/toolkit/components/glean/build_scripts/glean_parser_ext/run_glean_parser.py @@ -0,0 +1,86 @@ +# -*- 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 cpp +import js +import re +import rust +import sys + +from glean_parser import lint, parser, util +from pathlib import Path + + +def get_parser_options(moz_app_version): + app_version_major = moz_app_version.split(".", 1)[0] + return { + "allow_reserved": False, + "custom_is_expired": lambda expires: expires == "expired" + or expires != "never" + and int(expires) <= int(app_version_major), + "custom_validate_expires": lambda expires: expires in ("expired", "never") + or re.fullmatch(r"\d\d+", expires, flags=re.ASCII), + } + + +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] + + # Derived heavily from glean_parser.translate.translate. + # Adapted to how mozbuild sends us a fd, and to expire on versions not dates. + + options = get_parser_options(moz_app_version) + + # Lint the yaml first, then lint the metrics. + if lint.lint_yaml_files(input_files, parser_config=options): + # Warnings are Errors + sys.exit(1) + + all_objs = parser.parse_objects(input_files, options) + if util.report_validation_errors(all_objs): + sys.exit(1) + + if lint.lint_metrics(all_objs.value, options): + # Treat Warnings as Errors in FOG + sys.exit(1) + + return all_objs.value, options + + +# Must be kept in sync with the length of `deps` in moz.build. +DEPS_LEN = 13 + + +def main(output_fd, *args): + args = args[DEPS_LEN:] + all_objs, options = parse(args) + rust.output_rust(all_objs, output_fd, options) + + +def cpp_metrics(output_fd, *args): + args = args[DEPS_LEN:] + all_objs, options = parse(args) + cpp.output_cpp(all_objs, output_fd, options) + + +def js_metrics(output_fd, *args): + args = args[DEPS_LEN:] + all_objs, options = parse(args) + js.output_js(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..8219aff97a --- /dev/null +++ b/toolkit/components/glean/build_scripts/glean_parser_ext/rust.py @@ -0,0 +1,227 @@ +# -*- 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 util import generate_metric_ids, generate_ping_ids +from glean_parser import util + + +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() + """ + + 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 "vec![" + first = True + for subvalue in sorted(list(value)): + if not first: + yield ", " + yield from self.iterencode(subvalue) + first = False + yield "]" + elif isinstance(value, list): + 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()' + 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<{}>".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: + return "{}<{}>".format( + class_name(obj.type), util.Camelize(obj.name) + suffix + ) + return class_name(obj.type) + + +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 len(objs) == 1 and "pings" in objs: + template_filename = "rust_pings.jinja2" + else: + template_filename = "rust.jinja2" + + for category_name, metrics in objs.items(): + for metric in metrics.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), + ("ctor", ctor), + ("extra_keys", extra_keys), + ("metric_id", get_metric_id), + ("ping_id", get_ping_id), + ), + ) + + # The list of all args to CommonMetricData (besides category and name). + # 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", + ] + + 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, + ) + ) + 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..2eaecd237f --- /dev/null +++ b/toolkit/components/glean/build_scripts/glean_parser_ext/templates/cpp.jinja2 @@ -0,0 +1,55 @@ +// -*- 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" + +namespace mozilla::glean { + +enum class NoExtraKeys {}; + +{% macro generate_extra_keys(obj) %} +{% for name, suffix in obj["_generate_enums"] %} +{% if obj|attr(name)|length %} + enum class {{ obj.name|Camelize }}{{ suffix }} : int32_t { + {% for key in obj|attr(name) %} + {{ key | Camelize }}, + {% endfor %} + }; + +{% endif %} +{% endfor %} +{% endmacro %} + +{% for category_name, objs in all_objs.items() %} +namespace {{ category_name|snake_case }} { + {% for obj in objs.values() %} + {% if obj.type|is_implemented_type %} + /** + * 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}}); + + {% endif %} + {% 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/js.jinja2 b/toolkit/components/glean/build_scripts/glean_parser_ext/templates/js.jinja2 new file mode 100644 index 0000000000..b8804b91e5 --- /dev/null +++ b/toolkit/components/glean/build_scripts/glean_parser_ext/templates/js.jinja2 @@ -0,0 +1,129 @@ +// -*- 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" + +#define GLEAN_INDEX_BITS ({{index_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 +using metric_entry_t = uint64_t; + +static_assert(GLEAN_INDEX_BITS + GLEAN_ID_BITS < sizeof(metric_entry_t) * 8, "Index and ID bits need to fit into an category_entry_t"); +static_assert(GLEAN_ID_BITS < sizeof(uint32_t) * 8, "Metric IDs need to fit into less than 32 bit"); +static_assert({{ categories|length }} < UINT32_MAX, "Too many metric categories generated."); +static_assert({{ metric_id_mapping|length }} < {{2 ** id_bits}}, "Too many metrics generated."); +static_assert({{ metric_type_ids|length }} < {{2 ** (entry_width - index_bits - id_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, type_id in metric_type_ids.items() %} + {% if type|is_implemented_type %} + case {{ type_id }}: /* {{ type|Camelize }} */ + { + return MakeAndAddRef<{{type | type_name}}>(metricId); + } + {% endif %} + {% endfor %} + default: + MOZ_ASSERT_UNREACHABLE("Invalid type ID reached when trying to instantiate a new metric"); + 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..ac86d2c6de --- /dev/null +++ b/toolkit/components/glean/build_scripts/glean_parser_ext/templates/rust.jinja2 @@ -0,0 +1,217 @@ +// -*- 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, suffix in obj["_generate_enums"] %} +{% if obj|attr(name)|length %} + #[derive(Clone, Copy, Hash, Eq, PartialEq)] + pub enum {{ obj.name|Camelize }}{{ suffix }} { + {% for key in obj|attr(name) %} + {{ key | Camelize }}, + {% endfor %} + } + + impl ExtraKeys for {{ obj.name|Camelize }}{{ suffix }} { + const ALLOWED_KEYS: &'static [&'static str] = {{ obj.allowed_extra_keys|extra_keys }}; + + fn index(self) -> i32 { + self as i32 + } + } + + /// Convert from an extra key's index to its variant. + impl std::convert::TryFrom<i32> for {{ obj.name|Camelize }}{{ suffix }} { + type Error = EventRecordingError; + + fn try_from(value: i32) -> Result<Self, Self::Error> { + match value { + {% for key in obj|attr(name) %} + {{loop.index-1}} => Ok(Self::{{key | Camelize}}), + {% endfor %} + _ => Err(EventRecordingError::InvalidExtraKey), + } + } + } + + /// Convert from an extra key's string representation to its variant. + impl std::convert::TryFrom<&str> for {{ obj.name|Camelize }}{{ suffix }} { + type Error = EventRecordingError; + + fn try_from(value: &str) -> Result<Self, Self::Error> { + match value { + {% for key in obj|attr(name) %} + "{{key}}" => Ok(Self::{{key | Camelize}}), + {% endfor %} + _ => Err(EventRecordingError::InvalidExtraKey), + } + } + } + +{% endif %} +{% endfor %} +{% endmacro %} + +{% for category_name, objs in all_objs.items() %} +pub mod {{ category_name|snake_case }} { + use crate::private::*; + use glean_core::CommonMetricData; + 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 std::convert::TryInto; + + 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 %} + + /// 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() + } + + /// Wrapper to record an event based on its metric ID. + /// + /// # Arguments + /// + /// * `metric_id` - The metric's ID to look up + /// * `extra` - An (optional) 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` the `extra` map could not be deserialized. + pub(crate) fn event_record_wrapper(metric_id: u32, extra: HashMap<i32, 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" + ); + + // In case of `NoExtraKeys` the whole iterator is impossible, so rustc complains. + #[allow(unused_variables)] + let extra: HashMap<_, _> = extra + .into_iter() + .map(|(k, v)| k.try_into().map(|k| (k, v))) + .collect::<Result<HashMap<_, _>, _>>()?; + super::{{event}}.record(Some(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 (optional) 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` the `extra` map could not be deserialized. + pub(crate) fn event_record_wrapper_str(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" + ); + + // In case of `NoExtraKeys` the whole iterator is impossible, so rustc complains. + #[allow(unused_variables)] + let extra = extra + .into_iter() + .map(|(k, v)| (&*k).try_into().map(|k| (k, v))) + .collect::<Result<HashMap<_, _>, _>>()?; + super::{{event}}.record(Some(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 + /// * `storage_name` - the storage name to look into. + /// + /// # 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, storage_name: &str) -> Option<Vec<RecordedEvent>> { + match metric_id { +{% for metric_id, event in events_by_id.items() %} + {{metric_id}} => super::{{event}}.test_get_value(storage_name), +{% endfor %} + _ => panic!("No event for metric id {}", metric_id), + } + } +} +{% 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..a570cd9b79 --- /dev/null +++ b/toolkit/components/glean/build_scripts/glean_parser_ext/templates/rust_pings.jinja2 @@ -0,0 +1,47 @@ +// -*- 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>) { + 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..19b3d62bc0 --- /dev/null +++ b/toolkit/components/glean/build_scripts/glean_parser_ext/util.py @@ -0,0 +1,76 @@ +# -*- 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/. + +""" +Utitlity functions for the glean_parser-based code generator +""" + + +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)] + + +IMPLEMENTED_CPP_TYPES = [ + "boolean", + "counter", + "datetime", + "event", + "memory_distribution", + "string", + "string_list", + "timespan", + "timing_distribution", + "uuid", +] + + +def is_implemented_metric_type(typ): + """ + Filter out some unimplemented metric types to avoid generating C++ code for them. + Once all types are implemented this code will be removed. + """ + return typ in IMPLEMENTED_CPP_TYPES diff --git a/toolkit/components/glean/cbindgen.toml b/toolkit/components/glean/cbindgen.toml new file mode 100644 index 0000000000..de4046e0f2 --- /dev/null +++ b/toolkit/components/glean/cbindgen.toml @@ -0,0 +1,19 @@ +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/. */""" +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/api.md b/toolkit/components/glean/docs/api.md new file mode 100644 index 0000000000..8561d11d3b --- /dev/null +++ b/toolkit/components/glean/docs/api.md @@ -0,0 +1,4 @@ +# API + +The Metrics API documentation can be found in the +[Glean SDK Book](https://mozilla.github.io/glean/book/user/metrics/index.html). diff --git a/toolkit/components/glean/docs/code_organization.md b/toolkit/components/glean/docs/code_organization.md new file mode 100644 index 0000000000..2055fd0469 --- /dev/null +++ b/toolkit/components/glean/docs/code_organization.md @@ -0,0 +1,64 @@ +# FOG code organization + +```eval_rst +.. note:: + + Project FOG is currently being designed and implemented. + This documentation is incomplete and may change significantly before FOG is usable inside mozilla-central. +``` + +![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 Glean +* watching the Firefox Telemetry data upload preference (`datareporting.healthreport.uploadEnabled`) +* scheduling builtin pings +* controling ping upload workers +* passing IPC buffers + +It calls into `glean_core` 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. +* It will provide a build task for `glean_parser` integration. + +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 + +It calls into `glean_core` for: + +* metric types (including pings) +* querying `upload_enabled` status. diff --git a/toolkit/components/glean/docs/images/fog-modules.svg b/toolkit/components/glean/docs/images/fog-modules.svg new file mode 100644 index 0000000000..2b188c67a1 --- /dev/null +++ b/toolkit/components/glean/docs/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);" xmlns:xlink="http://www.w3.org/1999/xlink" 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)" xlink: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/index.md b/toolkit/components/glean/docs/index.md new file mode 100644 index 0000000000..ad43fa6fee --- /dev/null +++ b/toolkit/components/glean/docs/index.md @@ -0,0 +1,32 @@ +# 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/). +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. + +If you have any questions, please reach out to the team on +[#glean:mozilla.org][glean-matrix]. + +```eval_rst +.. toctree:: + :titlesonly: + :maxdepth: 1 + :glob: + + Glean SDK Source <https://github.com/mozilla/glean/> + Glean SDK Documentation <https://mozilla.github.io/glean/book/index.html> + * + +``` +[telemetry]: ../telemetry +[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/ipc.md b/toolkit/components/glean/docs/ipc.md new file mode 100644 index 0000000000..2b17b6b982 --- /dev/null +++ b/toolkit/components/glean/docs/ipc.md @@ -0,0 +1,106 @@ +# Inter-process Communication (IPC) + +Firefox Desktop is a multi-process desktop application. +Code requiring instrumentation may be on any of its processes, +so FOG needs to provide facilities to enable 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. + +There are a few cases where we provide more firm guarantees: + +#### Tests + +There are test-only APIs in Rust, C++, +and Javascript that expose when a complete flush of child process metric values has completed. +See [the test documentation](testing.md) for more details. + +#### Built-in Pings + +[Built-in pings](https://mozilla.github.io/glean/book/user/pings/index.html) +will send only after all metric values from all child processes have been collected. + +We cannot at this time provide the same guarantee for +[Custom Pings](https://mozilla.github.io/glean/book/user/pings/custom.html). + +#### Shutdown + +We will make a best effort during an orderly shutdown to flush all pending data in child processes. + +### Mechanics + +At present +(see [bug 1641989](https://bugzilla.mozilla.org/show_bug.cgi?id=1641989)) +FOG uses messages on the PContent protocol. +This enables communication between content child processes and the parent process. + +The rough design is that the Parent can request an immediate flush of pending data, +and each Child can decide to flush its pending data whenever it wishes. + +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. diff --git a/toolkit/components/glean/docs/new_definitions_file.md b/toolkit/components/glean/docs/new_definitions_file.md new file mode 100644 index 0000000000..1e5b188caa --- /dev/null +++ b/toolkit/components/glean/docs/new_definitions_file.md @@ -0,0 +1,85 @@ +# New Metrics and Pings + +**Note:** FOG is not ready to be used, +so you probably should not be adding metrics and pings using this guide yet. +Instead, please use Firefox Telemetry unless you have explicitly been given permission by a +[Telemetry Module Peer](https://wiki.mozilla.org/Modules/All#Telemetry). + +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. + +## IPC + +Firefox Desktop is made of multiple processes. +You can record data from any process in Firefox Desktop +[subject to certain conditions](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`. + +When you do so, 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. + +In addition, do not forget to file a bug in `Data Platform and Tools :: General` +asking for your definitions files to be added to the others for `firefox.desktop`. +If you don't, your metrics will not show up in datasets and tools +because the pipeline won't know that they exist. + +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](testing.md). + +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/docs/new_metric_types.md b/toolkit/components/glean/docs/new_metric_types.md new file mode 100644 index 0000000000..c950978f95 --- /dev/null +++ b/toolkit/components/glean/docs/new_metric_types.md @@ -0,0 +1,114 @@ +# 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' durations are calculated in the core, + so it can't calculate samples in child processes. + Instead we will need to record the start and stop instants as pairs, + more or less sending a command stream with timing information. + +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 to the core. +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 ipc-aware 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. + +## 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). + +Every method on the metric type is public for now, +including test methods. + +## 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. + +Each metric type has six pieces you'll need to cover: + +### 1. MLA FFI + +- Using our convenient macros, define the Multi-Language Architecture's FFI layer above the Rust API in [`api/src/ffi/mod.rs`](https://hg.mozilla.org/mozilla-central/file/tip/toolkit/components/glean/api/src/ffi/mod.rs). + +### 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) + and + [`build_scripts/glean_parser_ext/util.py`'s `IMPLEMENTED_CPP_TYPES`](https://hg.mozilla.org/mozilla-central/file/tip/toolkit/components/glean/build_scripts/glean_parser_ext/util.py). +- Include the new files in [`moz.build`](https://hg.mozilla.org/mozilla-central/file/tip/toolkit/components/glean/bindings/MetricTypes.h). 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`. + +### 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/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/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/xpcshell/test_Glean.js). + - For more details, peruse the [testing docs](testing.md). + +### 7. API Documentation + +TODO diff --git a/toolkit/components/glean/docs/preferences.md b/toolkit/components/glean/docs/preferences.md new file mode 100644 index 0000000000..1a0efffc10 --- /dev/null +++ b/toolkit/components/glean/docs/preferences.md @@ -0,0 +1,21 @@ +# Preferences + +## 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 which is less than 0, FOG will not squelch all sent pings, +telling the Glean SDK that the ping was sent successfully. +Defaults to 0. diff --git a/toolkit/components/glean/docs/storage.md b/toolkit/components/glean/docs/storage.md new file mode 100644 index 0000000000..00464c6173 --- /dev/null +++ b/toolkit/components/glean/docs/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/style_guide.md b/toolkit/components/glean/docs/style_guide.md new file mode 100644 index 0000000000..6f37f23a8a --- /dev/null +++ b/toolkit/components/glean/docs/style_guide.md @@ -0,0 +1,58 @@ +# 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 might not be. + +To link to another markdown page in FOG's documentation, you can use +```md +[link text](page_name.md) +``` + +Sphinx will automagically transform that to an +appropriately-base-url'd url with a `.html` suffix. + +Unfortunately, this doesn't work for linking to +`.rst` files like those in use in [Telemetry]'s documentation. +(Follow [bug 1621950] for updates). + +In those cases you have to link it like it's html. +For example, to link to [Telemetry] you can use either of +```md +[Telemetry](../telemetry) +[Telemetry](../telemetry/index.html) +``` + +Both will work. Both will generate warnings. +For example, the first form will generate this: +```console +None:any reference target not found: ../telemetry +``` +But it will still work because linking to a directory in html links to its +`index.html` (which is where `index.rst` ends up). + +We can suppress this by putting a fake anchor +(like `#https://`) on the end to fool Sphinx into not checking it. +But that seems like a hack more unseemly than the warnings, +so let's not. + + +[semantic linefeeds]: https://rhodesmill.org/brandon/2012/one-sentence-per-line/ +[reference link]: https://spec.commonmark.org/0.29/#reference-link +[Telemetry]: ../telemetry +[#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/testing.md b/toolkit/components/glean/docs/testing.md new file mode 100644 index 0000000000..2ef6ace0f6 --- /dev/null +++ b/toolkit/components/glean/docs/testing.md @@ -0,0 +1,117 @@ +# Testing + +Given the multiple API languages, processes, and dependencies, +testing FOG is a matter of choosing the right tool for the situation. + +## 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,glean::*:5,fog::*:5,fog_control::*:5,glean_core::*:5"`. +* Set the following prefs: + * `logging.config.timestamp` to `true` + * `logging.fog_control::*` to `5` + * `logging.fog::*` to `5` + * `logging.glean_core::*` to `5` + +For more information on logging in Firefox Desktop, see the +[Gecko Logging docs](https://developer.mozilla.org/docs/Mozilla/Developer_guide/Gecko_Logging). + +## `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). + +## Rust + +Not all of our Rust code can be tested in a single fashion, unfortunately. + +### Using `rusttests` + +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.) + +### Using `gtest` + +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/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/) for more. +See [`toolkit/components/glean/gtest/TestFog.cpp`](https://searchfox.org/mozilla-central/source/toolkit/components/glean/gtest/TestFog.cpp) +and [`toolkit/components/glean/gtest/test.rs`](https://searchfox.org/mozilla-central/source/toolkit/components/glean/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/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 + +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/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/pytest` + +## C++ + +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/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/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 + +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/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/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/xpcshell` diff --git a/toolkit/components/glean/docs/updating_parser.md b/toolkit/components/glean/docs/updating_parser.md new file mode 100644 index 0000000000..6603090279 --- /dev/null +++ b/toolkit/components/glean/docs/updating_parser.md @@ -0,0 +1,37 @@ +# 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 run + +``` +./mach vendor python glean_parser==M.m.p +``` + +where `M.m.p` is the version number, e.g. 1.28.0. + +```eval_rst +.. 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.html) 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 diff --git a/toolkit/components/glean/docs/updating_sdk.md b/toolkit/components/glean/docs/updating_sdk.md new file mode 100644 index 0000000000..70752c980b --- /dev/null +++ b/toolkit/components/glean/docs/updating_sdk.md @@ -0,0 +1,39 @@ +# Updating the Glean SDK + +Project FOG uses the published Glean SDK. +It currently depends on [glean-core] from crates.io. + +[glean-core]: https://crates.io/crates/glean-core + +To update the dependency: + +1. Bump the version of the `glean-core` crate in and `toolkit/components/glean/Cargo.toml` and `toolkit/components/glean/api/Cargo.toml`. +2. Run `mach vendor rust`. This fetches all dependencies and adds them to `mozilla-central/third_pary/rust`. + +## 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; +* 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. + +## 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/gtest/Cargo.toml b/toolkit/components/glean/gtest/Cargo.toml new file mode 100644 index 0000000000..83bfd3d244 --- /dev/null +++ b/toolkit/components/glean/gtest/Cargo.toml @@ -0,0 +1,12 @@ +[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" } + +[lib] +path = "test.rs" diff --git a/toolkit/components/glean/gtest/TestFog.cpp b/toolkit/components/glean/gtest/TestFog.cpp new file mode 100644 index 0000000000..16add5b973 --- /dev/null +++ b/toolkit/components/glean/gtest/TestFog.cpp @@ -0,0 +1,220 @@ +/* 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 "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/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, ""); +} +} + +// Initialize FOG exactly once. +// This needs to be the first test to run! +TEST(FOG, FogInitDoesntCrash) +{ + Preferences::SetInt("telemetry.fog.test.localhost_port", -1); + ASSERT_EQ(NS_OK, fog_init()); + // Fog init isn't actually done (it passes work to a background thread) + Preferences::SetBool(DATA_PREF, false); + Preferences::SetBool(DATA_PREF, true); +} + +// TODO: to be enabled after changes from bug 1677455 are vendored. +// extern "C" void Rust_MeasureInitializeTime(); +// TEST(FOG, TestMeasureInitializeTime) +// { Rust_MeasureInitializeTime(); } + +TEST(FOG, 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(FOG, TestCppCounterWorks) +{ + mozilla::glean::test_only::bad_code.Add(42); + + ASSERT_EQ( + 42, + mozilla::glean::test_only::bad_code.TestGetValue("test-ping"_ns).value()); + // And test that the ping name's optional, while you're at it: + ASSERT_EQ(42, test_only::bad_code.TestGetValue().value()); +} + +TEST(FOG, 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) + .value() + .get()); +} + +TEST(FOG, 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) + .value() > 0); +} + +TEST(FOG, 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).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).value().get()); +} + +TEST(FOG, 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) + .value()); +} + +// TODO: to be enabled after changes from bug 1677448 are vendored. +// TEST(FOG, 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"); +// ASSERT_STREQ(received.value().get(), "2020-11-06T12:10:35+05:00"); +// } + +using mozilla::MakeTuple; +using mozilla::Tuple; +using mozilla::glean::test_only_ipc::AnEventKeys; + +TEST(FOG, TestCppEventWorks) +{ + test_only_ipc::no_extra_event.Record(); + ASSERT_TRUE(test_only_ipc::no_extra_event.TestGetValue("store1"_ns).isSome()); + + // Ugh, this API... + nsTArray<Tuple<test_only_ipc::AnEventKeys, nsCString>> extra; + nsCString val = "can set extras"_ns; + extra.AppendElement(MakeTuple(AnEventKeys::Extra1, val)); + + test_only_ipc::an_event.Record(std::move(extra)); + ASSERT_TRUE(test_only_ipc::an_event.TestGetValue("store1"_ns).isSome()); +} + +TEST(FOG, 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).ref(); + // Sum is in bytes, test_only::do_you_remember is in megabytes. So + // multiplication ahoy! + ASSERT_EQ(data.sum, 24UL * 1024 * 1024); + for (auto iter = data.values.Iter(); !iter.Done(); iter.Next()) { + const uint64_t bucket = iter.Key(); + const uint64_t count = iter.UserData(); + ASSERT_TRUE(count == 0 || + (count == 1 && (bucket == 17520006 || bucket == 7053950))) + << "Only two occupied buckets"; + } +} + +TEST(FOG, TestCppPings) +{ + auto ping = mozilla::glean_pings::OnePingOnly; + mozilla::Unused << ping; + // That's it. That's the test. It will fail to compile if it's missing. + // For a test that actually submits the ping, we have integration tests. + // See also bug 1681742. +} + +TEST(FOG, 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().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().value(); + ASSERT_STREQ(kValue3.get(), val[2].get()); +} + +TEST(FOG, 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().ref(); + const uint64_t NANOS_IN_MILLIS = 1e6; + + // 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)); + + // We also can't guarantee the buckets, but we can guarantee two samples. + uint64_t sampleCount = 0; + for (auto iter = data.values.Iter(); !iter.Done(); iter.Next()) { + sampleCount += iter.UserData(); + } + ASSERT_EQ(sampleCount, (uint64_t)2); +} diff --git a/toolkit/components/glean/gtest/TestFogIPC.cpp b/toolkit/components/glean/gtest/TestFogIPC.cpp new file mode 100644 index 0000000000..f0ac9c85ac --- /dev/null +++ b/toolkit/components/glean/gtest/TestFogIPC.cpp @@ -0,0 +1,47 @@ +/* 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 "gtest/gtest.h" + +// NOTE: No ContentChild/Parent tests because it includes headers that aren't +// present in GTest builds (or something). + +#include "mozilla/FOGIPC.h" +#include <functional> +#include "mozilla/ipc/ByteBuf.h" +#include "nsTArray.h" +#include "nsXULAppAPI.h" + +using mozilla::ipc::ByteBuf; + +TEST(FOG, TestFlushFOGData) +{ + // A "It doesn't explode" test. + std::function<void(ByteBuf &&)> resolver = [](ByteBuf&& bufs) {}; + mozilla::glean::FlushFOGData(std::move(resolver)); +} + +TEST(FOG, TestFlushAllChildData) +{ + std::function<void(const nsTArray<ByteBuf>&&)> resolver = + [](const nsTArray<ByteBuf>&& bufs) { + ASSERT_TRUE(bufs.Length() == 0) + << "Not expecting any bufs yet."; + }; + mozilla::glean::FlushAllChildData(std::move(resolver)); +} + +TEST(FOG, FOGData) +{ + // Another "It doesn't explode" test. + ByteBuf buf; + mozilla::glean::FOGData(std::move(buf)); +} + +TEST(FOG, SendFOGData) +{ + ASSERT_EQ(XRE_GetProcessType(), GeckoProcessType_Default) + << "If we can run a test as a different process type, we can write a " + "test for this function."; +} diff --git a/toolkit/components/glean/gtest/moz.build b/toolkit/components/glean/gtest/moz.build new file mode 100644 index 0000000000..f70313c7d0 --- /dev/null +++ b/toolkit/components/glean/gtest/moz.build @@ -0,0 +1,13 @@ +# -*- 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 CONFIG["MOZ_GLEAN"]: + UNIFIED_SOURCES += [ + "TestFog.cpp", + "TestFogIPC.cpp", + ] + +FINAL_LIBRARY = "xul-gtest" diff --git a/toolkit/components/glean/gtest/test.rs b/toolkit/components/glean/gtest/test.rs new file mode 100644 index 0000000000..27af068734 --- /dev/null +++ b/toolkit/components/glean/gtest/test.rs @@ -0,0 +1,44 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +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_MeasureInitializeTime() { + // At this point FOG is already initialized. + // We still need for it to finish, as it is running in a separate thread. + + let metric = &*fog::metrics::fog::initialization; + while metric.test_get_value("metrics").is_none() { + // We _know_ this value is recorded early, so let's just yield + // and try again quickly. + std::thread::yield_now(); + } + + let value = metric.test_get_value("metrics").unwrap(); + expect!(value > 0); +} diff --git a/toolkit/components/glean/ipc/FOGIPC.cpp b/toolkit/components/glean/ipc/FOGIPC.cpp new file mode 100644 index 0000000000..e4ee0f28ee --- /dev/null +++ b/toolkit/components/glean/ipc/FOGIPC.cpp @@ -0,0 +1,99 @@ +/* -*- 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 "mozilla/glean/fog_ffi_generated.h" +#include "mozilla/dom/ContentChild.h" +#include "mozilla/dom/ContentParent.h" +#include "mozilla/MozPromise.h" +#include "nsTArray.h" +#include "nsThreadUtils.h" + +using mozilla::dom::ContentParent; +using mozilla::ipc::ByteBuf; +using FlushFOGDataPromise = mozilla::dom::ContentParent::FlushFOGDataPromise; + +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) { + 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(const nsTArray<ipc::ByteBuf>&&)>&& aResolver) { + nsTArray<ContentParent*> parents; + ContentParent::GetAll(parents); + if (parents.Length() == 0) { + nsTArray<ipc::ByteBuf> results; + aResolver(std::move(results)); + return; + } + + nsTArray<RefPtr<FlushFOGDataPromise>> promises; + for (auto parent : parents) { + promises.EmplaceBack(parent->SendFlushFOGData()); + } + // 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]( + FlushFOGDataPromise::AllPromiseType::ResolveOrRejectValue&& + aValue) { + if (aValue.IsResolve()) { + aResolver(std::move(aValue.ResolveValue())); + } else { + 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) { 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; + default: + MOZ_ASSERT_UNREACHABLE("Unsuppored process type"); + } +} + +} // namespace glean +} // namespace mozilla diff --git a/toolkit/components/glean/ipc/FOGIPC.h b/toolkit/components/glean/ipc/FOGIPC.h new file mode 100644 index 0000000000..1b19f355e7 --- /dev/null +++ b/toolkit/components/glean/ipc/FOGIPC.h @@ -0,0 +1,56 @@ +/* -*- 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 "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(const 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); + +} // namespace glean +} // namespace mozilla + +#endif // FOGIPC_h__ diff --git a/toolkit/components/glean/metrics.yaml b/toolkit/components/glean/metrics.yaml new file mode 100644 index 0000000000..1ae8cfb0c0 --- /dev/null +++ b/toolkit/components/glean/metrics.yaml @@ -0,0 +1,109 @@ +# 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/1-0-0 + +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 + +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 + +fog_validation: + legacy_telemetry_client_id: + type: uuid + description: + The Telemetry client_id. + To be sent only in the "fog-validation" ping. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1674233 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1674233#c4 + data_sensitivity: + - technical + - highly_sensitive + lifetime: application + notification_emails: + - chutten@mozilla.com + - glean-team@mozilla.com + expires: "89" + send_in_pings: + - fog-validation + + os_version: + type: string + description: + The version of the OS running Firefox, as detected by Gecko. + To be sent only in the "fog-validation" ping. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1679835 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1679835#c3 + data_sensitivity: + - technical + lifetime: application + notification_emails: + - aplacitelli@mozilla.com + - chutten@mozilla.com + - glean-team@mozilla.com + expires: "89" + send_in_pings: + - fog-validation + + profile_disk_is_ssd: + type: boolean + description: + True iff the type of the disk the current Firefox profile is stored on + is an SSD. (Windows only). + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1675877 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1675877#c7 + data_sensitivity: + - technical + lifetime: application + notification_emails: + - chutten@mozilla.com + - glean-team@mozilla.com + expires: "89" + send_in_pings: + - fog-validation diff --git a/toolkit/components/glean/metrics_index.py b/toolkit/components/glean/metrics_index.py new file mode 100644 index 0000000000..0a8637ce79 --- /dev/null +++ b/toolkit/components/glean/metrics_index.py @@ -0,0 +1,19 @@ +# -*- 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/. + +# The list of all Glean metrics.yaml files, relative to the top src dir. +# New additions should be added to the bottom of the list. +metrics_yamls = [ + "toolkit/components/glean/metrics.yaml", + "toolkit/components/glean/test_metrics.yaml", +] + +# The list of all Glean pings.yaml files, relative to the top src dir. +# New additions should be added to the bottom of the list. +pings_yamls = [ + "toolkit/components/glean/pings.yaml", + "toolkit/components/glean/test_pings.yaml", +] diff --git a/toolkit/components/glean/moz.build b/toolkit/components/glean/moz.build new file mode 100644 index 0000000000..5fdbaeb95e --- /dev/null +++ b/toolkit/components/glean/moz.build @@ -0,0 +1,160 @@ +# -*- 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" + +# Must be defined unconditionally since the tasks on TC don't know MOZ_GLEAN. +# (and the tests don't depend on MOZ_GLEAN to run). +PYTHON_UNITTEST_MANIFESTS += [ + "pytest/python.ini", +] + +if CONFIG["MOZ_GLEAN"]: + # 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 += [ + "!GleanJSMetricsLookup.h", + "!GleanJSPingsLookup.h", + "bindings/Category.h", + "bindings/Glean.h", + "bindings/GleanPings.h", + "bindings/MetricTypes.h", + "bindings/private/Boolean.h", + "bindings/private/Counter.h", + "bindings/private/Datetime.h", + "bindings/private/DistributionData.h", + "bindings/private/Event.h", + "bindings/private/MemoryDistribution.h", + "bindings/private/Ping.h", + "bindings/private/String.h", + "bindings/private/StringList.h", + "bindings/private/Timespan.h", + "bindings/private/TimingDistribution.h", + "bindings/private/Uuid.h", + ] + + if CONFIG["COMPILE_ENVIRONMENT"]: + EXPORTS.mozilla.glean += [ + "!fog_ffi_generated.h", + ] + + CbindgenHeader("fog_ffi_generated.h", inputs=["/toolkit/components/glean"]) + + UNIFIED_SOURCES += [ + "bindings/Category.cpp", + "bindings/Glean.cpp", + "bindings/GleanPings.cpp", + "bindings/private/Boolean.cpp", + "bindings/private/Common.cpp", + "bindings/private/Counter.cpp", + "bindings/private/Datetime.cpp", + "bindings/private/Event.cpp", + "bindings/private/MemoryDistribution.cpp", + "bindings/private/Ping.cpp", + "bindings/private/String.cpp", + "bindings/private/StringList.cpp", + "bindings/private/Timespan.cpp", + "bindings/private/TimingDistribution.cpp", + "bindings/private/Uuid.cpp", + "ipc/FOGIPC.cpp", + ] + + TEST_DIRS += [ + "gtest", + ] + + XPCSHELL_TESTS_MANIFESTS += ["xpcshell/xpcshell.ini"] + + # 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] + # 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/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/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( + "api/src/metrics.rs", + script="build_scripts/glean_parser_ext/run_glean_parser.py", + flags=[CONFIG["MOZ_APP_VERSION"]], + inputs=deps + metrics_yamls, + ) + + GeneratedFile( + "GleanMetrics.h", + script="build_scripts/glean_parser_ext/run_glean_parser.py", + entry_point="cpp_metrics", + flags=[CONFIG["MOZ_APP_VERSION"]], + inputs=deps + metrics_yamls, + ) + + GeneratedFile( + "GleanJSMetricsLookup.h", + script="build_scripts/glean_parser_ext/run_glean_parser.py", + entry_point="js_metrics", + flags=[CONFIG["MOZ_APP_VERSION"]], + inputs=deps + metrics_yamls, + ) + + GeneratedFile( + "api/src/pings.rs", + script="build_scripts/glean_parser_ext/run_glean_parser.py", + flags=[CONFIG["MOZ_APP_VERSION"]], + inputs=deps + pings_yamls, + ) + + GeneratedFile( + "GleanPings.h", + script="build_scripts/glean_parser_ext/run_glean_parser.py", + entry_point="cpp_metrics", + flags=[CONFIG["MOZ_APP_VERSION"]], + inputs=deps + pings_yamls, + ) + + GeneratedFile( + "GleanJSPingsLookup.h", + script="build_scripts/glean_parser_ext/run_glean_parser.py", + entry_point="js_metrics", + flags=[CONFIG["MOZ_APP_VERSION"]], + inputs=deps + pings_yamls, + ) + + DIRS += [ + "xpcom", + ] + +with Files("docs/**"): + SCHEDULES.exclusive = ["docs"] + +with Files("**"): + BUG_COMPONENT = ("Toolkit", "Telemetry") diff --git a/toolkit/components/glean/pings.yaml b/toolkit/components/glean/pings.yaml new file mode 100644 index 0000000000..aae6faee37 --- /dev/null +++ b/toolkit/components/glean/pings.yaml @@ -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 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/1-0-0 + +fog-validation: + description: | + This ping is intended to evaluate the behaviour of FOG before + it ships beyond Nightly. + This is a temporary ping. + It is sent one hour after FOG is initialized, and every hour thereafter. + include_client_id: true + send_if_empty: true + bugs: + - https://bugzilla.mozilla.org/1664461 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1664461 + notification_emails: + - chutten@mozilla.com + - glean-team@mozilla.com diff --git a/toolkit/components/glean/pytest/metrics_expires_number_test.yaml b/toolkit/components/glean/pytest/metrics_expires_number_test.yaml new file mode 100644 index 0000000000..701bc00443 --- /dev/null +++ b/toolkit/components/glean/pytest/metrics_expires_number_test.yaml @@ -0,0 +1,22 @@ +# 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/1-0-0 + +test: + expires_number: + type: boolean + expires: 99 + description: | + A metric with an expires of an invalid type (number) + lifetime: application + notification_emails: + - glean-team@mozilla.com + bugs: + - https://bugzilla.mozilla.org/1664306 + data_reviews: + - https://example.com diff --git a/toolkit/components/glean/pytest/metrics_expires_versions_test.yaml b/toolkit/components/glean/pytest/metrics_expires_versions_test.yaml new file mode 100644 index 0000000000..7520f749e5 --- /dev/null +++ b/toolkit/components/glean/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/1-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/pytest/metrics_test.yaml b/toolkit/components/glean/pytest/metrics_test.yaml new file mode 100644 index 0000000000..ead9217466 --- /dev/null +++ b/toolkit/components/glean/pytest/metrics_test.yaml @@ -0,0 +1,270 @@ +# 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/1-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 + + 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 + + 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 + + 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 + + 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 + + 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 + + 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 + + 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 + + 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 + + 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 + + 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 + +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 + + 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 + + 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 + + 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: + description: An extra key description + another_extra_key: + description: Another extra key description diff --git a/toolkit/components/glean/pytest/metrics_test_output b/toolkit/components/glean/pytest/metrics_test_output new file mode 100644 index 0000000000..0ac9f7b9ba --- /dev/null +++ b/toolkit/components/glean/pytest/metrics_test_output @@ -0,0 +1,542 @@ +// -*- 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_core::CommonMetricData; + 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<BooleanMetric>> = 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<BooleanMetric>> = 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(vec!["one_label".into(), "two_labels".into()])) + }); + + #[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<CounterMetric>> = 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<CounterMetric>> = 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<StringMetric>> = 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<StringMetric>> = 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) + }); + +} +pub mod test_nested { + use crate::private::*; + use glean_core::CommonMetricData; + 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(14.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(15.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(16.into(), CommonMetricData { + name: "event_metric".into(), + category: "test.nested".into(), + send_in_pings: vec!["events".into()], + lifetime: Lifetime::Ping, + disabled: false, + ..Default::default() + }) + }); + + #[derive(Clone, Copy, Hash, Eq, PartialEq)] + pub enum EventMetricWithExtraKeys { + AnExtraKey, + AnotherExtraKey, + } + + impl ExtraKeys for EventMetricWithExtraKeys { + const ALLOWED_KEYS: &'static [&'static str] = &["an_extra_key", "another_extra_key"]; + + fn index(self) -> i32 { + self as i32 + } + } + + /// Convert from an extra key's index to its variant. + impl std::convert::TryFrom<i32> for EventMetricWithExtraKeys { + type Error = EventRecordingError; + + fn try_from(value: i32) -> Result<Self, Self::Error> { + match value { + 0 => Ok(Self::AnExtraKey), + 1 => Ok(Self::AnotherExtraKey), + _ => Err(EventRecordingError::InvalidExtraKey), + } + } + } + + /// Convert from an extra key's string representation to its variant. + impl std::convert::TryFrom<&str> for EventMetricWithExtraKeys { + type Error = EventRecordingError; + + fn try_from(value: &str) -> Result<Self, Self::Error> { + match value { + "an_extra_key" => Ok(Self::AnExtraKey), + "another_extra_key" => Ok(Self::AnotherExtraKey), + _ => Err(EventRecordingError::InvalidExtraKey), + } + } + } + + #[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<EventMetricWithExtraKeys>> = Lazy::new(|| { + EventMetric::new(17.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(dead_code)] +pub(crate) mod __glean_metric_maps { + use std::collections::HashMap; + use std::convert::TryInto; + + 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<BooleanMetric>>>> = 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<CounterMetric>>>> = 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<StringMetric>>>> = 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 UUID_MAP: Lazy<HashMap<MetricId, &Lazy<UuidMetric>>> = Lazy::new(|| { + let mut map = HashMap::with_capacity(1); + map.insert(14.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(15.into(), &super::test_nested::datetime_metric); + map + }); + + + /// 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() + } + + /// Wrapper to record an event based on its metric ID. + /// + /// # Arguments + /// + /// * `metric_id` - The metric's ID to look up + /// * `extra` - An (optional) 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` the `extra` map could not be deserialized. + pub(crate) fn event_record_wrapper(metric_id: u32, extra: HashMap<i32, String>) -> Result<(), EventRecordingError> { + match metric_id { + 16 => { + assert!( + extra_keys_len(&super::test_nested::event_metric) != 0 || extra.is_empty(), + "No extra keys allowed, but some were passed" + ); + + // In case of `NoExtraKeys` the whole iterator is impossible, so rustc complains. + #[allow(unused_variables)] + let extra: HashMap<_, _> = extra + .into_iter() + .map(|(k, v)| k.try_into().map(|k| (k, v))) + .collect::<Result<HashMap<_, _>, _>>()?; + super::test_nested::event_metric.record(Some(extra)); + Ok(()) + } + 17 => { + assert!( + extra_keys_len(&super::test_nested::event_metric_with_extra) != 0 || extra.is_empty(), + "No extra keys allowed, but some were passed" + ); + + // In case of `NoExtraKeys` the whole iterator is impossible, so rustc complains. + #[allow(unused_variables)] + let extra: HashMap<_, _> = extra + .into_iter() + .map(|(k, v)| k.try_into().map(|k| (k, v))) + .collect::<Result<HashMap<_, _>, _>>()?; + super::test_nested::event_metric_with_extra.record(Some(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 (optional) 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` the `extra` map could not be deserialized. + pub(crate) fn event_record_wrapper_str(metric_id: u32, extra: HashMap<String, String>) -> Result<(), EventRecordingError> { + match metric_id { + 16 => { + assert!( + extra_keys_len(&super::test_nested::event_metric) != 0 || extra.is_empty(), + "No extra keys allowed, but some were passed" + ); + + // In case of `NoExtraKeys` the whole iterator is impossible, so rustc complains. + #[allow(unused_variables)] + let extra = extra + .into_iter() + .map(|(k, v)| (&*k).try_into().map(|k| (k, v))) + .collect::<Result<HashMap<_, _>, _>>()?; + super::test_nested::event_metric.record(Some(extra)); + Ok(()) + } + 17 => { + assert!( + extra_keys_len(&super::test_nested::event_metric_with_extra) != 0 || extra.is_empty(), + "No extra keys allowed, but some were passed" + ); + + // In case of `NoExtraKeys` the whole iterator is impossible, so rustc complains. + #[allow(unused_variables)] + let extra = extra + .into_iter() + .map(|(k, v)| (&*k).try_into().map(|k| (k, v))) + .collect::<Result<HashMap<_, _>, _>>()?; + super::test_nested::event_metric_with_extra.record(Some(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 + /// * `storage_name` - the storage name to look into. + /// + /// # 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, storage_name: &str) -> Option<Vec<RecordedEvent>> { + match metric_id { + 16 => super::test_nested::event_metric.test_get_value(storage_name), + 17 => super::test_nested::event_metric_with_extra.test_get_value(storage_name), + _ => panic!("No event for metric id {}", metric_id), + } + } +} + diff --git a/toolkit/components/glean/pytest/metrics_test_output_cpp b/toolkit/components/glean/pytest/metrics_test_output_cpp new file mode 100644 index 0000000000..73cfc76bc3 --- /dev/null +++ b/toolkit/components/glean/pytest/metrics_test_output_cpp @@ -0,0 +1,130 @@ +// -*- 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" + +namespace mozilla::glean { + +enum class NoExtraKeys {}; + + +namespace test { + /** + * generated from test.boolean_metric + */ + /** + * A multi-line + * description + */ + constexpr impl::BooleanMetric boolean_metric(1); + + /** + * generated from test.counter_metric + */ + /** + * A multi-line + * description + */ + constexpr impl::CounterMetric counter_metric(4); + + /** + * generated from test.string_metric + */ + /** + * A multi-line + * description + */ + constexpr impl::StringMetric string_metric(7); + + /** + * 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); + +} +namespace test_nested { + /** + * generated from test.nested.uuid_metric + */ + /** + * A multi-line + * description + */ + constexpr impl::UuidMetric uuid_metric(14); + + /** + * generated from test.nested.datetime_metric + */ + /** + * A multi-line + * description + */ + constexpr impl::DatetimeMetric datetime_metric(15); + + /** + * generated from test.nested.event_metric + */ + /** + * A multi-line + * description + */ + constexpr impl::EventMetric<uint32_t> event_metric(16); + + /** + * generated from test.nested.event_metric_with_extra + */ + enum class EventMetricWithExtraKeys : int32_t { + AnExtraKey, + AnotherExtraKey, + }; + + /** + * A multi-line + * description + */ + constexpr impl::EventMetric<EventMetricWithExtraKeys> event_metric_with_extra(17); + +} + +} // namespace mozilla::glean + +#endif // mozilla_Metrics_h diff --git a/toolkit/components/glean/pytest/metrics_test_output_js b/toolkit/components/glean/pytest/metrics_test_output_js new file mode 100644 index 0000000000..b9c351baa0 --- /dev/null +++ b/toolkit/components/glean/pytest/metrics_test_output_js @@ -0,0 +1,255 @@ +// -*- 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" + +#define GLEAN_INDEX_BITS (32) +#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 +using metric_entry_t = uint64_t; + +static_assert(GLEAN_INDEX_BITS + GLEAN_ID_BITS < sizeof(metric_entry_t) * 8, "Index and ID bits need to fit into an category_entry_t"); +static_assert(GLEAN_ID_BITS < sizeof(uint32_t) * 8, "Metric IDs need to fit into less than 32 bit"); +static_assert(2 < UINT32_MAX, "Too many metric categories generated."); +static_assert(17 < 134217728, "Too many metrics generated."); +static_assert(13 < 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 3: /* Counter */ + { + return MakeAndAddRef<GleanCounter>(metricId); + } + case 5: /* String */ + { + return MakeAndAddRef<GleanString>(metricId); + } + case 7: /* StringList */ + { + return MakeAndAddRef<GleanStringList>(metricId); + } + case 8: /* Timespan */ + { + return MakeAndAddRef<GleanTimespan>(metricId); + } + case 9: /* TimingDistribution */ + { + return MakeAndAddRef<GleanTimingDistribution>(metricId); + } + case 10: /* MemoryDistribution */ + { + return MakeAndAddRef<GleanMemoryDistribution>(metricId); + } + case 11: /* Uuid */ + { + return MakeAndAddRef<GleanUuid>(metricId); + } + case 12: /* Datetime */ + { + return MakeAndAddRef<GleanDatetime>(metricId); + } + case 13: /* Event */ + { + return MakeAndAddRef<GleanEvent>(metricId); + } + default: + MOZ_ASSERT_UNREACHABLE("Invalid type ID reached when trying to instantiate a new metric"); + 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[] = { + 5, + 0 +}; + + + +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 - "testNested.uuidMetric" */ 't', 'e', 's', 't', 'N', 'e', 's', 't', 'e', 'd', '.', 'u', 'u', 'i', 'd', 'M', 'e', 't', 'r', 'i', 'c', '\0', + /* 352 - "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', + /* 378 - "testNested.eventMetric" */ 't', 'e', 's', 't', 'N', 'e', 's', 't', 'e', 'd', '.', 'e', 'v', 'e', 'n', 't', 'M', 'e', 't', 'r', 'i', 'c', '\0', + /* 401 - "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', +}; + + +static_assert(sizeof(gMetricStringTable) < 4294967296, "Metric string table is too large."); + +const metric_entry_t sMetricByNameLookupEntries[] = { + 1729382274090139725, + 3458764552475246789, + 1152921513196781587, + 2305843030688530528, + 5188146822270419214, + 7493989848663982458, + 1152921517491748909, + 576460756598390784, + 4035225309073637604, + 6341068335467200842, + 6917529092065591648, + 4611686065672028410, + 7493989852958949777, + 2305843034983497850, + 5764607578868810028, + 3458764548180279468, + 2882303791581888666 +}; + + + +static Maybe<uint32_t> +MetricByNameLookup(const nsACString& aKey) +{ + static const uint8_t BASES[] = { + 0, 0, 0, 2, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, + 2, 0, 0, 0, 1, 0, 0, 1, 0, 5, 0, 0, 4, 0, 1, 0, + 2, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 2, 0, 6, 0, 0, + 0, 0, 20, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, + }; + + + 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/pytest/pings_test.yaml b/toolkit/components/glean/pytest/pings_test.yaml new file mode 100644 index 0000000000..efa6ba9e0a --- /dev/null +++ b/toolkit/components/glean/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/pytest/pings_test_output b/toolkit/components/glean/pytest/pings_test_output new file mode 100644 index 0000000000..df705967bc --- /dev/null +++ b/toolkit/components/glean/pytest/pings_test_output @@ -0,0 +1,93 @@ +// -*- 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>) { + 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/pytest/pings_test_output_cpp b/toolkit/components/glean/pytest/pings_test_output_cpp new file mode 100644 index 0000000000..7f9cc03947 --- /dev/null +++ b/toolkit/components/glean/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/pytest/pings_test_output_js b/toolkit/components/glean/pytest/pings_test_output_js new file mode 100644 index 0000000000..ecbc6ccf7c --- /dev/null +++ b/toolkit/components/glean/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/pytest/python.ini b/toolkit/components/glean/pytest/python.ini new file mode 100644 index 0000000000..d91d5bafa2 --- /dev/null +++ b/toolkit/components/glean/pytest/python.ini @@ -0,0 +1,6 @@ +[DEFAULT] +subsuite = fog + +[test_glean_parser_rust.py] +[test_glean_parser_cpp.py] +[test_glean_parser_js.py] diff --git a/toolkit/components/glean/pytest/test_glean_parser_cpp.py b/toolkit/components/glean/pytest/test_glean_parser_cpp.py new file mode 100644 index 0000000000..96bcd241a8 --- /dev/null +++ b/toolkit/components/glean/pytest/test_glean_parser_cpp.py @@ -0,0 +1,77 @@ +# 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 mozunit +from os import path +from pathlib import Path +import sys + + +# Shenanigans to import the cpp outputter extension +FOG_ROOT_PATH = path.abspath(path.join(path.dirname(__file__), path.pardir)) +sys.path.append(path.join(FOG_ROOT_PATH, "build_scripts", "glean_parser_ext")) +import cpp + +# 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_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 a new expected output file, copy the test yaml over the one in t/c/g, + run mach build, then copy the C++ output from objdir/t/c/g/. + """ + + options = {"allow_reserved": False} + input_files = [Path(path.join(path.dirname(__file__), "metrics_test.yaml"))] + + all_objs = parser.parse_objects(input_files, options) + assert not util.report_validation_errors(all_objs) + assert not lint.lint_metrics(all_objs.value, options) + + output_fd = io.StringIO() + cpp.output_cpp(all_objs.value, output_fd, options) + + with open( + path.join(path.dirname(__file__), "metrics_test_output_cpp"), "r" + ) as file: + EXPECTED_CPP = file.read() + assert output_fd.getvalue() == EXPECTED_CPP + + +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 a new expected output file, edit t/c/g/metrics_index.py, + comment out all other ping yamls, and add one for + t/c/g/pytest/pings_test.yaml. Run `mach build` (it'll fail). Copy + objdir/t/c/g/GleanPings.h over pings_test_output_cpp. + (Don't forget to undo your edits to t/c/g/metrics_index.py) + """ + + options = {"allow_reserved": False} + input_files = [Path(path.join(path.dirname(__file__), "pings_test.yaml"))] + + all_objs = parser.parse_objects(input_files, options) + assert not util.report_validation_errors(all_objs) + assert not lint.lint_metrics(all_objs.value, options) + + output_fd = io.StringIO() + cpp.output_cpp(all_objs.value, output_fd, options) + + with open(path.join(path.dirname(__file__), "pings_test_output_cpp"), "r") as file: + EXPECTED_CPP = file.read() + assert output_fd.getvalue() == EXPECTED_CPP + + +if __name__ == "__main__": + mozunit.main() diff --git a/toolkit/components/glean/pytest/test_glean_parser_js.py b/toolkit/components/glean/pytest/test_glean_parser_js.py new file mode 100644 index 0000000000..09ba623fcf --- /dev/null +++ b/toolkit/components/glean/pytest/test_glean_parser_js.py @@ -0,0 +1,75 @@ +# 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 mozunit +from os import path +from pathlib import Path +import sys + + +# Shenanigans to import the js outputter extension +FOG_ROOT_PATH = path.abspath(path.join(path.dirname(__file__), path.pardir)) +sys.path.append(path.join(FOG_ROOT_PATH, "build_scripts", "glean_parser_ext")) +import js + +# 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_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 a new expected output file, copy the test yaml over the one in t/c/g, + run mach build, then copy the C++ output from objdir/t/c/g/. + """ + + options = {"allow_reserved": False} + input_files = [Path(path.join(path.dirname(__file__), "metrics_test.yaml"))] + + all_objs = parser.parse_objects(input_files, options) + assert not util.report_validation_errors(all_objs) + assert not lint.lint_metrics(all_objs.value, options) + + output_fd = io.StringIO() + js.output_js(all_objs.value, output_fd, options) + + with open(path.join(path.dirname(__file__), "metrics_test_output_js"), "r") as file: + EXPECTED_JS = file.read() + assert output_fd.getvalue() == EXPECTED_JS + + +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 a new expected output file, edit t/c/g/metrics_index.py, + comment out all other ping yamls, and add one for + t/c/g/pytest/pings_test.yaml. Run `mach build` (it'll fail). Copy + objdir/t/c/g/GleanJSPingsLookup.h over pings_test_output_js. + (Don't forget to undo your edits to t/c/g/metrics_index.py) + """ + + options = {"allow_reserved": False} + input_files = [Path(path.join(path.dirname(__file__), "pings_test.yaml"))] + + all_objs = parser.parse_objects(input_files, options) + assert not util.report_validation_errors(all_objs) + assert not lint.lint_metrics(all_objs.value, options) + + output_fd = io.StringIO() + js.output_js(all_objs.value, output_fd, options) + + with open(path.join(path.dirname(__file__), "pings_test_output_js"), "r") as file: + EXPECTED_JS = file.read() + assert output_fd.getvalue() == EXPECTED_JS + + +if __name__ == "__main__": + mozunit.main() diff --git a/toolkit/components/glean/pytest/test_glean_parser_rust.py b/toolkit/components/glean/pytest/test_glean_parser_rust.py new file mode 100644 index 0000000000..6bf3fbe841 --- /dev/null +++ b/toolkit/components/glean/pytest/test_glean_parser_rust.py @@ -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 http://mozilla.org/MPL/2.0/. + +import io +import mozunit +from os import path +from pathlib import Path +import re +import sys + + +# Shenanigans to import the rust outputter extension +FOG_ROOT_PATH = path.abspath(path.join(path.dirname(__file__), 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")) +from glean_parser import lint, parser, util + + +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 a new expected output file, copy the test yaml over the one in t/c/g, + run mach build, then copy the rust output from objdir/t/c/g/api/src/. + """ + + options = {"allow_reserved": False} + input_files = [Path(path.join(path.dirname(__file__), "metrics_test.yaml"))] + + all_objs = parser.parse_objects(input_files, options) + assert not util.report_validation_errors(all_objs) + assert not lint.lint_metrics(all_objs.value, options) + + output_fd = io.StringIO() + rust.output_rust(all_objs.value, output_fd, options) + + with open(path.join(path.dirname(__file__), "metrics_test_output"), "r") as file: + EXPECTED_RUST = file.read() + assert output_fd.getvalue() == EXPECTED_RUST + + +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 a new expected output file, copy the test yaml over the one in t/c/g, + run mach build, then copy the rust output from objdir/t/c/g/api/src/. + """ + + options = {"allow_reserved": False} + input_files = [Path(path.join(path.dirname(__file__), "pings_test.yaml"))] + + all_objs = parser.parse_objects(input_files, options) + assert not util.report_validation_errors(all_objs) + assert not lint.lint_metrics(all_objs.value, options) + + output_fd = io.StringIO() + rust.output_rust(all_objs.value, output_fd, options) + + with open(path.join(path.dirname(__file__), "pings_test_output"), "r") as file: + EXPECTED_RUST = file.read() + assert output_fd.getvalue() == EXPECTED_RUST + + +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 = parser.parse_objects(input_files, options) + + assert not util.report_validation_errors(all_objs) + assert not lint.lint_metrics(all_objs.value, options) + + assert all_objs.value["test"]["expired1"].disabled is True + assert all_objs.value["test"]["expired2"].disabled is True + assert all_objs.value["test"]["unexpired"].disabled is False + + +def test_numeric_expires(): + """What if the expires value is a number, not a string? + 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. + """ + + # We'll never get to checking expires values, so this app version shouldn't matter. + options = run_glean_parser.get_parser_options("42.0a1") + input_files = [ + Path(path.join(path.dirname(__file__), "metrics_expires_number_test.yaml")) + ] + + all_objs = parser.parse_objects(input_files, options) + errors = list(all_objs) + assert len(errors) == 1 + assert re.search("99 is not of type 'string'", str(errors[0])) + + +if __name__ == "__main__": + mozunit.main() diff --git a/toolkit/components/glean/sphinx/glean.py b/toolkit/components/glean/sphinx/glean.py new file mode 100644 index 0000000000..fb11dacb6e --- /dev/null +++ b/toolkit/components/glean/sphinx/glean.py @@ -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 http://mozilla.org/MPL/2.0/. + +import os +from pathlib import Path +import sys + + +def setup(app): + from moztreedocs import manager + + # Import the list of metrics and ping files + glean_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), os.path.pardir)) + sys.path.append(glean_dir) + from metrics_index import metrics_yamls, pings_yamls + + # Import the custom version expiry code. + glean_parser_ext_dir = os.path.abspath( + Path(glean_dir) / "build_scripts" / "glean_parser_ext" + ) + sys.path.append(glean_parser_ext_dir) + from run_glean_parser import get_parser_options + + firefox_version = "4.0a1" # TODO: bug 1676416 - Get the real app version. + parser_config = get_parser_options(firefox_version) + + input_files = [Path(os.path.join(manager.topsrcdir, x)) for x in metrics_yamls] + input_files += [Path(os.path.join(manager.topsrcdir, x)) for x in pings_yamls] + + # Generate the autodocs. + from glean_parser import translate + + out_path = Path(os.path.join(manager.staging_dir, "metrics")) + translate.translate( + input_files, "markdown", out_path, {"project_title": "Firefox"}, parser_config + ) + + # Rename the generated docfile to index so Sphinx finds it + os.rename(os.path.join(out_path, "metrics.md"), os.path.join(out_path, "index.md")) diff --git a/toolkit/components/glean/src/lib.rs b/toolkit/components/glean/src/lib.rs new file mode 100644 index 0000000000..11bf1dd58c --- /dev/null +++ b/toolkit/components/glean/src/lib.rs @@ -0,0 +1,364 @@ +// 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; + +#[macro_use] +extern crate cstr; +#[macro_use] +extern crate xpcom; + +use std::ffi::CStr; +use std::os::raw::c_char; + +use nserror::{nsresult, NS_ERROR_FAILURE, NS_OK}; +use nsstring::{nsACString, nsAString, nsCStr, nsCString, nsStr, nsString}; +use xpcom::interfaces::{ + mozIViaduct, nsIFile, nsIObserver, nsIPrefBranch, nsIPropertyBag2, nsISupports, nsIXULAppInfo, +}; +use xpcom::{RefPtr, XpCom}; + +use glean::{ClientInfoMetrics, Configuration}; + +mod viaduct_uploader; + +use crate::viaduct_uploader::ViaductUploader; + +/// Project FOG's entry point. +/// +/// This assembles client information and the Glean configuration and then initializes the global +/// Glean instance. +#[no_mangle] +pub unsafe extern "C" fn fog_init() -> nsresult { + fog::metrics::fog::initialization.start(); + + log::debug!("Initializing FOG."); + + let data_path = match get_data_path() { + Ok(dp) => dp, + Err(e) => return e, + }; + + let (app_build, app_display_version, channel) = match get_app_info() { + Ok(ai) => ai, + Err(e) => return e, + }; + + let (os_version, _architecture) = match get_system_info() { + Ok(si) => si, + Err(e) => return e, + }; + + fog::metrics::fog_validation::os_version.set(os_version); + + let client_info = ClientInfoMetrics { + app_build, + app_display_version, + }; + log::debug!("Client Info: {:#?}", client_info); + + let pref_observer = UploadPrefObserver::allocate(InitUploadPrefObserver {}); + if let Err(e) = pref_observer.begin_observing() { + log::error!( + "Could not observe data upload pref. Abandoning FOG init due to {:?}", + e + ); + return e; + } + + 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) + }; + + let upload_enabled = static_prefs::pref!("datareporting.healthreport.uploadEnabled"); + let data_path = data_path.to_string(); + let configuration = Configuration { + upload_enabled, + data_path, + application_id: "firefox.desktop".to_string(), + max_events: None, + delay_ping_lifetime_io: false, + channel: Some(channel), + server_endpoint: Some(server), + uploader: Some(Box::new(crate::ViaductUploader) as Box<dyn glean::net::PingUploader>), + }; + + log::debug!("Configuration: {:#?}", configuration); + + // Ensure Viaduct is initialized for networking unconditionally so we don't + // need to check again if upload is later enabled. + if let Some(viaduct) = + xpcom::create_instance::<mozIViaduct>(cstr!("@mozilla.org/toolkit/viaduct;1")) + { + let result = viaduct.EnsureInitialized(); + if result.failed() { + log::error!("Failed to ensure viaduct was initialized due to {}. Ping upload may not be available.", result.error_name()); + } + } else { + log::error!("Failed to create Viaduct via XPCOM. Ping upload may not be available."); + } + + if configuration.data_path.len() > 0 { + glean::initialize(configuration, client_info); + + // Register all custom pings before we initialize. + fog::pings::register_pings(); + + fog::metrics::fog::initialization.stop(); + schedule_fog_validation_ping(); + } + + NS_OK +} + +#[no_mangle] +pub unsafe extern "C" fn fog_shutdown() { + glean::shutdown(); +} + +/// Construct and return the data_path from the profile dir, or return an error. +fn get_data_path() -> Result<String, nsresult> { + let dir_svc = match xpcom::services::get_DirectoryService() { + Some(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 = xpcom::services::get_XULRuntime().ok_or(NS_ERROR_FAILURE)?; + + let mut channel = nsCString::new(); + unsafe { + xul.GetDefaultUpdateChannel(&mut *channel).to_result()?; + } + + 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(), + )) +} + +/// Return a tuple of os_version and architecture, or an error. +fn get_system_info() -> Result<(String, String), nsresult> { + let info_service = xpcom::get_service::<nsIPropertyBag2>(cstr!("@mozilla.org/system-info;1")) + .ok_or(NS_ERROR_FAILURE)?; + + let os_version_key: Vec<u16> = "version".encode_utf16().collect(); + let os_version_key = &nsStr::from(&os_version_key) as &nsAString; + let mut os_version = nsCString::new(); + unsafe { + info_service + .GetPropertyAsACString(os_version_key, &mut *os_version) + .to_result()?; + } + + let arch_key: Vec<u16> = "arch".encode_utf16().collect(); + let arch_key = &nsStr::from(&arch_key) as &nsAString; + let mut arch = nsCString::new(); + unsafe { + info_service + .GetPropertyAsACString(arch_key, &mut *arch) + .to_result()?; + } + + Ok((os_version.to_string(), arch.to_string())) +} + +// Partially cargo-culted from https://searchfox.org/mozilla-central/rev/598e50d2c3cd81cd616654f16af811adceb08f9f/security/manager/ssl/cert_storage/src/lib.rs#1192 +#[derive(xpcom)] +#[xpimplements(nsIObserver)] +#[refcnt = "atomic"] +struct InitUploadPrefObserver {} + +#[allow(non_snake_case)] +impl UploadPrefObserver { + unsafe fn begin_observing(&self) -> Result<(), nsresult> { + let pref_service = xpcom::services::get_PrefService().ok_or(NS_ERROR_FAILURE)?; + let pref_branch: RefPtr<nsIPrefBranch> = + (*pref_service).query_interface().ok_or(NS_ERROR_FAILURE)?; + let pref_nscstr = &nsCStr::from("datareporting.healthreport.uploadEnabled") as &nsACString; + (*pref_branch) + .AddObserverImpl(pref_nscstr, self.coerce::<nsIObserver>(), false) + .to_result()?; + Ok(()) + } + + unsafe fn Observe( + &self, + _subject: *const nsISupports, + topic: *const c_char, + pref_name: *const i16, + ) -> nserror::nsresult { + let topic = CStr::from_ptr(topic).to_str().unwrap(); + // Conversion utf16 to utf8 is messy. + // We should only ever observe changes to the one pref 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 as *const u16, 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" && pref_name == "datareporting.healthreport.uploadEnabled" + ); + + let upload_enabled = static_prefs::pref!("datareporting.healthreport.uploadEnabled"); + glean::set_upload_enabled(upload_enabled); + NS_OK + } +} + +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 + } +} + +#[no_mangle] +/// 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. +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 +} + +#[no_mangle] +/// 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. +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); + } +} + +#[no_mangle] +/// Sets the debug tag for pings assembled in the future. +/// Returns an error result if the provided value is not a valid tag. +pub unsafe 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; + } +} + +#[no_mangle] +/// Submits a ping by name. +pub unsafe extern "C" fn fog_submit_ping(ping_name: &nsACString) -> nsresult { + glean::submit_ping_by_name(&ping_name.to_string(), None); + NS_OK +} + +#[no_mangle] +/// Turns ping logging on or off. +/// Returns an error if the logging failed to be configured. +pub unsafe extern "C" fn fog_set_log_pings(value: bool) -> nsresult { + glean::set_log_pings(value); + NS_OK +} + +fn schedule_fog_validation_ping() { + std::thread::spawn(|| { + loop { + // Sleep for an hour before and between submissions. + std::thread::sleep(std::time::Duration::from_secs(60 * 60)); + fog::pings::fog_validation.submit(None); + } + }); +} diff --git a/toolkit/components/glean/src/viaduct_uploader.rs b/toolkit/components/glean/src/viaduct_uploader.rs new file mode 100644 index 0000000000..3d01441cb4 --- /dev/null +++ b/toolkit/components/glean/src/viaduct_uploader.rs @@ -0,0 +1,54 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use glean::net::{PingUploader, UploadResult}; +use url::Url; +use viaduct::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 localhost_port = static_prefs::pref!("telemetry.fog.test.localhost_port"); + if localhost_port < 0 { + log::info!("FOG Ping uploader faking success"); + return Ok(UploadResult::HttpStatus(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::HttpStatus(res.status.into())) + })(); + log::trace!( + "FOG Ping Uploader completed uploading to {} (Result {:?})", + url, + result + ); + match result { + Ok(result) => result, + _ => UploadResult::UnrecoverableFailure, + } + } +} diff --git a/toolkit/components/glean/test_metrics.yaml b/toolkit/components/glean/test_metrics.yaml new file mode 100644 index 0000000000..db79622fcd --- /dev/null +++ b/toolkit/components/glean/test_metrics.yaml @@ -0,0 +1,353 @@ +# 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 for Internal FOG Test Use Only. + +--- +$schema: moz://mozilla.org/schemas/glean/metrics/1-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 + +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 + 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 + a_bool: + type: boolean + description: | + This is a test-only metric. + Just flagging things. + 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: + - store1 + no_lint: + - COMMON_PREFIX + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + an_event: + type: event + extra_keys: + extra1: + description: "Some extra data" + extra2: + 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 + 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 + 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 + 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 + 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 diff --git a/toolkit/components/glean/test_pings.yaml b/toolkit/components/glean/test_pings.yaml new file mode 100644 index 0000000000..dc65c17c5c --- /dev/null +++ b/toolkit/components/glean/test_pings.yaml @@ -0,0 +1,25 @@ +# 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/1-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 diff --git a/toolkit/components/glean/xpcom/FOG.cpp b/toolkit/components/glean/xpcom/FOG.cpp new file mode 100644 index 0000000000..fd530766eb --- /dev/null +++ b/toolkit/components/glean/xpcom/FOG.cpp @@ -0,0 +1,53 @@ +/* -*- 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 "nsIFOG.h" +#include "mozilla/FOG.h" +#include "mozilla/glean/fog_ffi_generated.h" + +#include "mozilla/ClearOnShutdown.h" + +namespace mozilla { + +static StaticRefPtr<FOG> gFOG; + +// static +already_AddRefed<FOG> FOG::GetSingleton() { + if (gFOG) { + return do_AddRef(gFOG); + } + + gFOG = new FOG(); + RunOnShutdown([&] { + gFOG->Shutdown(); + gFOG = nullptr; + }); + return do_AddRef(gFOG); +} + +void FOG::Shutdown() { glean::impl::fog_shutdown(); } + +NS_IMETHODIMP +FOG::InitializeFOG() { return glean::impl::fog_init(); } + +NS_IMETHODIMP +FOG::SetLogPings(bool aEnableLogPings) { + return glean::impl::fog_set_log_pings(aEnableLogPings); +} + +NS_IMETHODIMP +FOG::SetTagPings(const nsACString& aDebugTag) { + return glean::impl::fog_set_debug_view_tag(&aDebugTag); +} + +NS_IMETHODIMP +FOG::SendPing(const nsACString& aPingName) { + return glean::impl::fog_submit_ping(&aPingName); +} + +NS_IMPL_ISUPPORTS(FOG, nsIFOG) + +} // namespace mozilla diff --git a/toolkit/components/glean/xpcom/FOG.h b/toolkit/components/glean/xpcom/FOG.h new file mode 100644 index 0000000000..508865ad41 --- /dev/null +++ b/toolkit/components/glean/xpcom/FOG.h @@ -0,0 +1,27 @@ +/* -*- 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" + +namespace mozilla { +class FOG final : public nsIFOG { + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIFOG + + 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..db87b2f6a8 --- /dev/null +++ b/toolkit/components/glean/xpcom/components.conf @@ -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/. + +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', + } +] diff --git a/toolkit/components/glean/xpcom/moz.build b/toolkit/components/glean/xpcom/moz.build new file mode 100644 index 0000000000..cc4dd1496d --- /dev/null +++ b/toolkit/components/glean/xpcom/moz.build @@ -0,0 +1,28 @@ +# -*- 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 CONFIG["MOZ_GLEAN"]: + 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..80112c6a17 --- /dev/null +++ b/toolkit/components/glean/xpcom/nsIFOG.idl @@ -0,0 +1,50 @@ +/* -*- 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. + */ + void initializeFOG(); + + /** + * 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); +}; diff --git a/toolkit/components/glean/xpcom/nsIGleanMetrics.idl b/toolkit/components/glean/xpcom/nsIGleanMetrics.idl new file mode 100644 index 0000000000..a602200f2f --- /dev/null +++ b/toolkit/components/glean/xpcom/nsIGleanMetrics.idl @@ -0,0 +1,394 @@ +/* -*- 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. + * + * @param aValue The time value in milliseconds 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, 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 uint32_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 Nothing() if there is no value. + */ + [implicit_jscontext] + jsval testGetValue([optional] in ACString aPingName); +}; + +[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 Nothing() if there is no value. + */ + [implicit_jscontext] + jsval testGetValue([optional] in ACString aPingName); +}; + +[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); +}; + +[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(); + + /** + * **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. + */ + [implicit_jscontext] + jsval testGetValue([optional] in AUTF8String aPingName); +}; diff --git a/toolkit/components/glean/xpcshell/test_Glean.js b/toolkit/components/glean/xpcshell/test_Glean.js new file mode 100644 index 0000000000..de273634a3 --- /dev/null +++ b/toolkit/components/glean/xpcshell/test_Glean.js @@ -0,0 +1,221 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* FIXME: Remove these global markers. + * FOG doesn't follow the stricter naming patterns as expected by tool configuration yet. + * See https://searchfox.org/mozilla-central/source/.eslintrc.js#24 + * Reorganizing the directory structure will take this into account. + */ +/* global add_task, Assert, do_get_profile */ +"use strict"; + +Cu.importGlobalProperties(["Glean", "GleanPings"]); +const { MockRegistrar } = ChromeUtils.import( + "resource://testing-common/MockRegistrar.jsm" +); +const { ObjectUtils } = ChromeUtils.import( + "resource://gre/modules/ObjectUtils.jsm" +); +const { setTimeout } = ChromeUtils.import("resource://gre/modules/Timer.jsm"); + +/** + * Mock the SysInfo object used to read System data in Gecko. + */ +var SysInfo = { + overrides: {}, + + /** + * Checks if overrides are present and return them. + * + * @returns the overridden value or undefined if not present. + */ + _getOverridden(name) { + if (name in this.overrides) { + return this.overrides[name]; + } + + return undefined; + }, + + // To support nsIPropertyBag. + getProperty(name) { + let override = this._getOverridden(name); + return override !== undefined + ? override + : this._genuine.QueryInterface(Ci.nsIPropertyBag).getProperty(name); + }, + + // To support nsIPropertyBag2. + get(name) { + let override = this._getOverridden(name); + return override !== undefined + ? override + : this._genuine.QueryInterface(Ci.nsIPropertyBag2).get(name); + }, + + // To support nsIPropertyBag2. + getPropertyAsACString(name) { + return this.get(name); + }, + + QueryInterface: ChromeUtils.generateQI(["nsIPropertyBag2", "nsISystemInfo"]), +}; + +function sleep(ms) { + /* eslint-disable mozilla/no-arbitrary-setTimeout */ + return new Promise(resolve => setTimeout(resolve, ms)); +} + +add_task(function test_setup() { + // FOG needs a profile directory to put its data in. + do_get_profile(); + + // Mock SysInfo. + SysInfo.overrides = { + version: "1.2.3", + arc: "x64", + }; + MockRegistrar.register("@mozilla.org/system-info;1", SysInfo); + + // We need to initialize it once, otherwise operations will be stuck in the pre-init queue. + let FOG = Cc["@mozilla.org/toolkit/glean;1"].createInstance(Ci.nsIFOG); + FOG.initializeFOG(); +}); + +add_task(function test_osversion_is_set() { + Assert.equal( + "1.2.3", + Glean.fogValidation.osVersion.testGetValue("fog-validation") + ); +}); + +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() { + // 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_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")); +}); + +// Enable test after bug 1677448 is fixed. +add_task({ skip_if: () => true }, 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.ok(received.startsWith("2020-06-11T12:00:00")); +}); + +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(); + // FIXME(bug 1678567): Check that the value was recorded when we can. + // Assert.ok(Glean.testOnlyIpc.noExtraEvent.testGetValue("store1")); + + let extra = { extra1: "can set extras", extra2: "passing more data" }; + Glean.testOnlyIpc.anEvent.record(extra); + // FIXME(bug 1678567): Check that the value was recorded when we can. + // Assert.ok(Glean.testOnlyIpc.anEvent.testGetValue("store1")); +}); + +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(function test_fog_custom_pings() { + Assert.ok("onePingOnly" in GleanPings); + // Don't bother sending it, we'll test that in the integration suite. + // See also bug 1681742. +}); + +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; + + // Variance in timing makes getting the sum impossible to know. + Assert.greater(data.sum, 15 * NANOS_IN_MILLIS, "Total time elapsed: > 15ms"); + + // 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" + ); +}); diff --git a/toolkit/components/glean/xpcshell/xpcshell.ini b/toolkit/components/glean/xpcshell/xpcshell.ini new file mode 100644 index 0000000000..8bc1f67948 --- /dev/null +++ b/toolkit/components/glean/xpcshell/xpcshell.ini @@ -0,0 +1,4 @@ +[DEFAULT] +firefox-appdir = browser + +[test_Glean.js] |