summaryrefslogtreecommitdiffstats
path: root/toolkit/components/glean
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--toolkit/components/glean/Cargo.toml28
-rw-r--r--toolkit/components/glean/api/Cargo.toml27
-rw-r--r--toolkit/components/glean/api/src/common_test.rs58
-rw-r--r--toolkit/components/glean/api/src/factory.rs16
-rw-r--r--toolkit/components/glean/api/src/ffi/boolean.rs28
-rw-r--r--toolkit/components/glean/api/src/ffi/counter.rs28
-rw-r--r--toolkit/components/glean/api/src/ffi/custom_distribution.rs82
-rw-r--r--toolkit/components/glean/api/src/ffi/datetime.rs66
-rw-r--r--toolkit/components/glean/api/src/ffi/denominator.rs28
-rw-r--r--toolkit/components/glean/api/src/ffi/event.rs168
-rw-r--r--toolkit/components/glean/api/src/ffi/labeled.rs84
-rw-r--r--toolkit/components/glean/api/src/ffi/macros.rs289
-rw-r--r--toolkit/components/glean/api/src/ffi/memory_distribution.rs64
-rw-r--r--toolkit/components/glean/api/src/ffi/mod.rs28
-rw-r--r--toolkit/components/glean/api/src/ffi/numerator.rs35
-rw-r--r--toolkit/components/glean/api/src/ffi/ping.rs18
-rw-r--r--toolkit/components/glean/api/src/ffi/quantity.rs28
-rw-r--r--toolkit/components/glean/api/src/ffi/rate.rs40
-rw-r--r--toolkit/components/glean/api/src/ffi/string.rs33
-rw-r--r--toolkit/components/glean/api/src/ffi/string_list.rs42
-rw-r--r--toolkit/components/glean/api/src/ffi/text.rs29
-rw-r--r--toolkit/components/glean/api/src/ffi/timespan.rs48
-rw-r--r--toolkit/components/glean/api/src/ffi/timing_distribution.rs90
-rw-r--r--toolkit/components/glean/api/src/ffi/url.rs29
-rw-r--r--toolkit/components/glean/api/src/ffi/uuid.rs37
-rw-r--r--toolkit/components/glean/api/src/ipc.rs364
-rw-r--r--toolkit/components/glean/api/src/lib.rs27
-rw-r--r--toolkit/components/glean/api/src/metrics.rs20
-rw-r--r--toolkit/components/glean/api/src/pings.rs13
-rw-r--r--toolkit/components/glean/api/src/private/boolean.rs155
-rw-r--r--toolkit/components/glean/api/src/private/counter.rs204
-rw-r--r--toolkit/components/glean/api/src/private/custom_distribution.rs171
-rw-r--r--toolkit/components/glean/api/src/private/datetime.rs246
-rw-r--r--toolkit/components/glean/api/src/private/denominator.rs157
-rw-r--r--toolkit/components/glean/api/src/private/event.rs241
-rw-r--r--toolkit/components/glean/api/src/private/labeled.rs369
-rw-r--r--toolkit/components/glean/api/src/private/labeled_counter.rs179
-rw-r--r--toolkit/components/glean/api/src/private/memory_distribution.rs206
-rw-r--r--toolkit/components/glean/api/src/private/mod.rs76
-rw-r--r--toolkit/components/glean/api/src/private/numerator.rs152
-rw-r--r--toolkit/components/glean/api/src/private/ping.rs121
-rw-r--r--toolkit/components/glean/api/src/private/quantity.rs155
-rw-r--r--toolkit/components/glean/api/src/private/rate.rs186
-rw-r--r--toolkit/components/glean/api/src/private/string.rs185
-rw-r--r--toolkit/components/glean/api/src/private/string_list.rs212
-rw-r--r--toolkit/components/glean/api/src/private/text.rs179
-rw-r--r--toolkit/components/glean/api/src/private/timespan.rs180
-rw-r--r--toolkit/components/glean/api/src/private/timing_distribution.rs487
-rw-r--r--toolkit/components/glean/api/src/private/url.rs129
-rw-r--r--toolkit/components/glean/api/src/private/uuid.rs171
-rw-r--r--toolkit/components/glean/bindings/Category.cpp75
-rw-r--r--toolkit/components/glean/bindings/Category.h45
-rw-r--r--toolkit/components/glean/bindings/Glean.cpp111
-rw-r--r--toolkit/components/glean/bindings/Glean.h57
-rw-r--r--toolkit/components/glean/bindings/GleanMetric.cpp70
-rw-r--r--toolkit/components/glean/bindings/GleanMetric.h47
-rw-r--r--toolkit/components/glean/bindings/GleanPings.cpp92
-rw-r--r--toolkit/components/glean/bindings/GleanPings.h39
-rw-r--r--toolkit/components/glean/bindings/MetricTypes.h27
-rw-r--r--toolkit/components/glean/bindings/jog/Cargo.toml20
-rw-r--r--toolkit/components/glean/bindings/jog/JOG.cpp276
-rw-r--r--toolkit/components/glean/bindings/jog/JOG.h122
-rw-r--r--toolkit/components/glean/bindings/jog/cbindgen.toml26
-rw-r--r--toolkit/components/glean/bindings/jog/src/lib.rs267
-rw-r--r--toolkit/components/glean/bindings/private/Boolean.cpp71
-rw-r--r--toolkit/components/glean/bindings/private/Boolean.h80
-rw-r--r--toolkit/components/glean/bindings/private/Common.cpp33
-rw-r--r--toolkit/components/glean/bindings/private/Common.h24
-rw-r--r--toolkit/components/glean/bindings/private/Counter.cpp74
-rw-r--r--toolkit/components/glean/bindings/private/Counter.h77
-rw-r--r--toolkit/components/glean/bindings/private/CustomDistribution.cpp105
-rw-r--r--toolkit/components/glean/bindings/private/CustomDistribution.h95
-rw-r--r--toolkit/components/glean/bindings/private/Datetime.cpp121
-rw-r--r--toolkit/components/glean/bindings/private/Datetime.h79
-rw-r--r--toolkit/components/glean/bindings/private/Denominator.cpp64
-rw-r--r--toolkit/components/glean/bindings/private/Denominator.h77
-rw-r--r--toolkit/components/glean/bindings/private/DistributionData.h32
-rw-r--r--toolkit/components/glean/bindings/private/Event.cpp110
-rw-r--r--toolkit/components/glean/bindings/private/Event.h177
-rw-r--r--toolkit/components/glean/bindings/private/Labeled.cpp49
-rw-r--r--toolkit/components/glean/bindings/private/Labeled.h248
-rw-r--r--toolkit/components/glean/bindings/private/MemoryDistribution.cpp89
-rw-r--r--toolkit/components/glean/bindings/private/MemoryDistribution.h86
-rw-r--r--toolkit/components/glean/bindings/private/Numerator.cpp73
-rw-r--r--toolkit/components/glean/bindings/private/Numerator.h84
-rw-r--r--toolkit/components/glean/bindings/private/Ping.cpp85
-rw-r--r--toolkit/components/glean/bindings/private/Ping.h83
-rw-r--r--toolkit/components/glean/bindings/private/Quantity.cpp68
-rw-r--r--toolkit/components/glean/bindings/private/Quantity.h77
-rw-r--r--toolkit/components/glean/bindings/private/Rate.cpp86
-rw-r--r--toolkit/components/glean/bindings/private/Rate.h90
-rw-r--r--toolkit/components/glean/bindings/private/String.cpp75
-rw-r--r--toolkit/components/glean/bindings/private/String.h80
-rw-r--r--toolkit/components/glean/bindings/private/StringList.cpp80
-rw-r--r--toolkit/components/glean/bindings/private/StringList.h93
-rw-r--r--toolkit/components/glean/bindings/private/Text.cpp64
-rw-r--r--toolkit/components/glean/bindings/private/Text.h54
-rw-r--r--toolkit/components/glean/bindings/private/Timespan.cpp186
-rw-r--r--toolkit/components/glean/bindings/private/Timespan.h110
-rw-r--r--toolkit/components/glean/bindings/private/TimingDistribution.cpp242
-rw-r--r--toolkit/components/glean/bindings/private/TimingDistribution.h125
-rw-r--r--toolkit/components/glean/bindings/private/Url.cpp68
-rw-r--r--toolkit/components/glean/bindings/private/Url.h77
-rw-r--r--toolkit/components/glean/bindings/private/Uuid.cpp76
-rw-r--r--toolkit/components/glean/bindings/private/Uuid.h83
-rw-r--r--toolkit/components/glean/build_scripts/glean_parser_ext/cpp.py139
-rw-r--r--toolkit/components/glean/build_scripts/glean_parser_ext/jog.py207
-rw-r--r--toolkit/components/glean/build_scripts/glean_parser_ext/js.py308
-rw-r--r--toolkit/components/glean/build_scripts/glean_parser_ext/run_glean_parser.py234
-rw-r--r--toolkit/components/glean/build_scripts/glean_parser_ext/rust.py283
-rw-r--r--toolkit/components/glean/build_scripts/glean_parser_ext/string_table.py97
-rw-r--r--toolkit/components/glean/build_scripts/glean_parser_ext/templates/cpp.jinja2101
-rw-r--r--toolkit/components/glean/build_scripts/glean_parser_ext/templates/cpp_pings.jinja230
-rw-r--r--toolkit/components/glean/build_scripts/glean_parser_ext/templates/gifft.jinja275
-rw-r--r--toolkit/components/glean/build_scripts/glean_parser_ext/templates/gifft_events.jinja252
-rw-r--r--toolkit/components/glean/build_scripts/glean_parser_ext/templates/jog_factory.jinja2149
-rw-r--r--toolkit/components/glean/build_scripts/glean_parser_ext/templates/js.jinja2170
-rw-r--r--toolkit/components/glean/build_scripts/glean_parser_ext/templates/js_h.jinja277
-rw-r--r--toolkit/components/glean/build_scripts/glean_parser_ext/templates/js_pings.jinja267
-rw-r--r--toolkit/components/glean/build_scripts/glean_parser_ext/templates/js_pings_h.jinja235
-rw-r--r--toolkit/components/glean/build_scripts/glean_parser_ext/templates/rust.jinja2355
-rw-r--r--toolkit/components/glean/build_scripts/glean_parser_ext/templates/rust_pings.jinja277
-rw-r--r--toolkit/components/glean/build_scripts/glean_parser_ext/util.py102
-rw-r--r--toolkit/components/glean/build_scripts/mach_commands.py227
-rw-r--r--toolkit/components/glean/build_scripts/perf_data_review.py168
-rw-r--r--toolkit/components/glean/cbindgen.toml25
-rw-r--r--toolkit/components/glean/docs/dev/builtin_pings.md13
-rw-r--r--toolkit/components/glean/docs/dev/code_organization.md56
-rw-r--r--toolkit/components/glean/docs/dev/images/fog-modules.svg6
-rw-r--r--toolkit/components/glean/docs/dev/index.md15
-rw-r--r--toolkit/components/glean/docs/dev/ipc.md184
-rw-r--r--toolkit/components/glean/docs/dev/jog.md91
-rw-r--r--toolkit/components/glean/docs/dev/local_glean.md100
-rw-r--r--toolkit/components/glean/docs/dev/new_metric_types.md285
-rw-r--r--toolkit/components/glean/docs/dev/preferences.md105
-rw-r--r--toolkit/components/glean/docs/dev/storage.md14
-rw-r--r--toolkit/components/glean/docs/dev/style_guide.md49
-rw-r--r--toolkit/components/glean/docs/dev/testing.md240
-rw-r--r--toolkit/components/glean/docs/dev/updating_parser.md52
-rw-r--r--toolkit/components/glean/docs/dev/updating_sdk.md48
-rw-r--r--toolkit/components/glean/docs/index.md30
-rw-r--r--toolkit/components/glean/docs/user/geckoview_streaming_migration.md262
-rw-r--r--toolkit/components/glean/docs/user/getting_started.md97
-rw-r--r--toolkit/components/glean/docs/user/gifft.md241
-rw-r--r--toolkit/components/glean/docs/user/index.md17
-rw-r--r--toolkit/components/glean/docs/user/instrumentation_tests.md278
-rw-r--r--toolkit/components/glean/docs/user/migration.md909
-rw-r--r--toolkit/components/glean/docs/user/new_definitions_file.md116
-rw-r--r--toolkit/components/glean/ipc/FOGIPC.cpp575
-rw-r--r--toolkit/components/glean/ipc/FOGIPC.h102
-rw-r--r--toolkit/components/glean/ipc/Support.cpp76
-rw-r--r--toolkit/components/glean/metrics.yaml170
-rw-r--r--toolkit/components/glean/metrics_index.py180
-rw-r--r--toolkit/components/glean/moz.build209
-rw-r--r--toolkit/components/glean/pings.yaml13
-rw-r--r--toolkit/components/glean/src/init/mod.rs368
-rw-r--r--toolkit/components/glean/src/init/upload_pref.rs99
-rw-r--r--toolkit/components/glean/src/init/user_activity.rs129
-rw-r--r--toolkit/components/glean/src/init/viaduct_uploader.rs73
-rw-r--r--toolkit/components/glean/src/lib.rs210
-rw-r--r--toolkit/components/glean/tags.yaml553
-rw-r--r--toolkit/components/glean/tests/browser/browser.toml17
-rw-r--r--toolkit/components/glean/tests/browser/browser_event_leak.js26
-rw-r--r--toolkit/components/glean/tests/browser/browser_fog_gmp.js84
-rw-r--r--toolkit/components/glean/tests/browser/browser_fog_gpu.js34
-rw-r--r--toolkit/components/glean/tests/browser/browser_fog_rdd.js63
-rw-r--r--toolkit/components/glean/tests/browser/browser_fog_socket.js33
-rw-r--r--toolkit/components/glean/tests/browser/browser_fog_utility.js37
-rw-r--r--toolkit/components/glean/tests/browser/browser_labeled_gifft.js54
-rw-r--r--toolkit/components/glean/tests/browser/empty_file.html9
-rw-r--r--toolkit/components/glean/tests/browser/small-shot.oggbin0 -> 6416 bytes
-rw-r--r--toolkit/components/glean/tests/gtest/Cargo.toml14
-rw-r--r--toolkit/components/glean/tests/gtest/FOGFixture.h23
-rw-r--r--toolkit/components/glean/tests/gtest/TestFog.cpp470
-rw-r--r--toolkit/components/glean/tests/gtest/moz.build14
-rw-r--r--toolkit/components/glean/tests/gtest/test.rs55
-rw-r--r--toolkit/components/glean/tests/moz.build16
-rw-r--r--toolkit/components/glean/tests/pytest/expect_helper.py34
-rw-r--r--toolkit/components/glean/tests/pytest/gifft_output_Event67
-rw-r--r--toolkit/components/glean/tests/pytest/gifft_output_EventExtra35
-rw-r--r--toolkit/components/glean/tests/pytest/gifft_output_Histogram70
-rw-r--r--toolkit/components/glean/tests/pytest/gifft_output_Scalar102
-rw-r--r--toolkit/components/glean/tests/pytest/jogfile_output344
-rw-r--r--toolkit/components/glean/tests/pytest/metrics_expires_versions_test.yaml84
-rw-r--r--toolkit/components/glean/tests/pytest/metrics_test.yaml398
-rw-r--r--toolkit/components/glean/tests/pytest/metrics_test_output918
-rw-r--r--toolkit/components/glean/tests/pytest/metrics_test_output_cpp287
-rw-r--r--toolkit/components/glean/tests/pytest/metrics_test_output_js_cpp473
-rw-r--r--toolkit/components/glean/tests/pytest/metrics_test_output_js_h75
-rw-r--r--toolkit/components/glean/tests/pytest/pings_test.yaml116
-rw-r--r--toolkit/components/glean/tests/pytest/pings_test_output118
-rw-r--r--toolkit/components/glean/tests/pytest/pings_test_output_cpp62
-rw-r--r--toolkit/components/glean/tests/pytest/pings_test_output_js_cpp162
-rw-r--r--toolkit/components/glean/tests/pytest/pings_test_output_js_h33
-rw-r--r--toolkit/components/glean/tests/pytest/python.toml16
-rw-r--r--toolkit/components/glean/tests/pytest/test_gifft.py49
-rw-r--r--toolkit/components/glean/tests/pytest/test_glean_parser_cpp.py70
-rw-r--r--toolkit/components/glean/tests/pytest/test_glean_parser_js.py84
-rw-r--r--toolkit/components/glean/tests/pytest/test_glean_parser_rust.py89
-rw-r--r--toolkit/components/glean/tests/pytest/test_jogfile_output.py50
-rw-r--r--toolkit/components/glean/tests/pytest/test_no_expired_metrics.py46
-rw-r--r--toolkit/components/glean/tests/pytest/test_yaml_indices.py42
-rw-r--r--toolkit/components/glean/tests/test_metrics.yaml859
-rw-r--r--toolkit/components/glean/tests/test_pings.yaml42
-rw-r--r--toolkit/components/glean/tests/xpcshell/head.js6
-rw-r--r--toolkit/components/glean/tests/xpcshell/test_FOGIPCLimit.js51
-rw-r--r--toolkit/components/glean/tests/xpcshell/test_FOGInit.js49
-rw-r--r--toolkit/components/glean/tests/xpcshell/test_FOGPrefs.js47
-rw-r--r--toolkit/components/glean/tests/xpcshell/test_GIFFT.js523
-rw-r--r--toolkit/components/glean/tests/xpcshell/test_GIFFTIPC.js315
-rw-r--r--toolkit/components/glean/tests/xpcshell/test_Glean.js458
-rw-r--r--toolkit/components/glean/tests/xpcshell/test_GleanExperiments.js28
-rw-r--r--toolkit/components/glean/tests/xpcshell/test_GleanIPC.js157
-rw-r--r--toolkit/components/glean/tests/xpcshell/test_GleanServerKnobs.js164
-rw-r--r--toolkit/components/glean/tests/xpcshell/test_JOG.js728
-rw-r--r--toolkit/components/glean/tests/xpcshell/test_JOGIPC.js266
-rw-r--r--toolkit/components/glean/tests/xpcshell/test_MillionQ.js19
-rw-r--r--toolkit/components/glean/tests/xpcshell/xpcshell.toml32
-rw-r--r--toolkit/components/glean/xpcom/FOG.cpp433
-rw-r--r--toolkit/components/glean/xpcom/FOG.h29
-rw-r--r--toolkit/components/glean/xpcom/components.conf18
-rw-r--r--toolkit/components/glean/xpcom/moz.build27
-rw-r--r--toolkit/components/glean/xpcom/nsIFOG.idl190
-rw-r--r--toolkit/components/glean/xpcom/nsIGleanPing.idl51
224 files changed, 29240 insertions, 0 deletions
diff --git a/toolkit/components/glean/Cargo.toml b/toolkit/components/glean/Cargo.toml
new file mode 100644
index 0000000000..45423a183b
--- /dev/null
+++ b/toolkit/components/glean/Cargo.toml
@@ -0,0 +1,28 @@
+[package]
+name = "fog_control"
+version = "0.1.0"
+authors = ["Glean SDK team <glean-team@mozilla.com>"]
+edition = "2018"
+license = "MPL-2.0"
+
+[dependencies]
+glean = "57.0.0"
+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"
+firefox-on-glean = { path = "./api" }
+cstr = "0.2"
+viaduct = "0.1"
+url = "2.1"
+thin-vec = { version = "0.2.1", features = ["gecko-ffi"] }
+
+[features]
+# Leave data collection enabled, but disable upload.
+disable_upload = []
+# Letting us know we're compiling with Gecko symbols.
+with_gecko = ["firefox-on-glean/with_gecko"]
+# Opt into a larger preinit queue
+million_queue = ["glean/preinit_million_queue"]
diff --git a/toolkit/components/glean/api/Cargo.toml b/toolkit/components/glean/api/Cargo.toml
new file mode 100644
index 0000000000..403168fb90
--- /dev/null
+++ b/toolkit/components/glean/api/Cargo.toml
@@ -0,0 +1,27 @@
+[package]
+name = "firefox-on-glean"
+version = "0.1.0"
+authors = ["Glean SDK team <glean-team@mozilla.com>"]
+edition = "2018"
+publish = false
+license = "MPL-2.0"
+
+[dependencies]
+bincode = "1.0"
+chrono = "0.4.10"
+glean = "57.0.0"
+inherent = "1.0.0"
+log = "0.4"
+nsstring = { path = "../../../../xpcom/rust/nsstring", optional = true }
+once_cell = "1.2.0"
+serde = { version = "1.0", features = ["derive"] }
+uuid = { version = "1.0", features = ["v4"] }
+xpcom = { path = "../../../../xpcom/rust/xpcom", optional = true }
+thin-vec = { version = "0.2.1", features = ["gecko-ffi"] }
+mozbuild = "0.1"
+
+[dev-dependencies]
+tempfile = "3.1.0"
+
+[features]
+with_gecko = ["xpcom", "nsstring"]
diff --git a/toolkit/components/glean/api/src/common_test.rs b/toolkit/components/glean/api/src/common_test.rs
new file mode 100644
index 0000000000..3c8c2c71e1
--- /dev/null
+++ b/toolkit/components/glean/api/src/common_test.rs
@@ -0,0 +1,58 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
+
+use std::sync::{Mutex, MutexGuard};
+
+use once_cell::sync::Lazy;
+
+const GLOBAL_APPLICATION_ID: &str = "org.mozilla.firefox.test";
+
+/// UGLY HACK.
+/// We use a global lock to force synchronization of all tests, even if run multi-threaded.
+/// This allows us to run without `--test-threads 1`.`
+pub fn lock_test() -> (MutexGuard<'static, ()>, tempfile::TempDir) {
+ static GLOBAL_LOCK: Lazy<Mutex<()>> = Lazy::new(|| Mutex::new(()));
+
+ let lock = GLOBAL_LOCK.lock().unwrap();
+
+ let dir = setup_glean(None);
+ (lock, dir)
+}
+
+// Create a new instance of Glean with a temporary directory.
+// We need to keep the `TempDir` alive, so that it's not deleted before we stop using it.
+fn setup_glean(tempdir: Option<tempfile::TempDir>) -> tempfile::TempDir {
+ let dir = match tempdir {
+ Some(tempdir) => tempdir,
+ None => tempfile::tempdir().unwrap(),
+ };
+ let tmpname = dir.path().to_path_buf();
+
+ let cfg = glean::Configuration {
+ upload_enabled: true,
+ data_path: tmpname,
+ application_id: GLOBAL_APPLICATION_ID.into(),
+ max_events: None,
+ delay_ping_lifetime_io: false,
+ server_endpoint: None,
+ uploader: None,
+ use_core_mps: false,
+ trim_data_to_registered_pings: false,
+ log_level: None,
+ rate_limit: None,
+ enable_event_timestamps: false,
+ experimentation_id: None,
+ };
+
+ let client_info = glean::ClientInfoMetrics {
+ app_build: "test-build".into(),
+ app_display_version: "1.2.3".into(),
+ channel: None,
+ locale: None,
+ };
+
+ glean::test_reset_glean(cfg, client_info, true);
+
+ dir
+}
diff --git a/toolkit/components/glean/api/src/factory.rs b/toolkit/components/glean/api/src/factory.rs
new file mode 100644
index 0000000000..f7984f4c46
--- /dev/null
+++ b/toolkit/components/glean/api/src/factory.rs
@@ -0,0 +1,16 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
+
+//! This file contains the Generated JOG Factory for the runtime-registration
+//! of Glean metrics in Firefox on Glean.
+//! You probably should just ignore stuff in here and ask on the
+//! [#glean Matrix channel](https://chat.mozilla.org/#/room/#glean:mozilla.org)
+//! if you have questions.
+//!
+//! Most of the contents of this module are generated by
+//! `toolkit/components/glean/build_scripts/glean_parser_ext/run_glean_parser.py`
+
+include!(mozbuild::objdir_path!(
+ "toolkit/components/glean/api/src/factory.rs"
+));
diff --git a/toolkit/components/glean/api/src/ffi/boolean.rs b/toolkit/components/glean/api/src/ffi/boolean.rs
new file mode 100644
index 0000000000..9184b23c00
--- /dev/null
+++ b/toolkit/components/glean/api/src/ffi/boolean.rs
@@ -0,0 +1,28 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
+
+#![cfg(feature = "with_gecko")]
+
+use nsstring::nsACString;
+
+#[no_mangle]
+pub extern "C" fn fog_boolean_test_has_value(id: u32, ping_name: &nsACString) -> bool {
+ with_metric!(BOOLEAN_MAP, id, metric, test_has!(metric, ping_name))
+}
+
+#[no_mangle]
+pub extern "C" fn fog_boolean_test_get_value(id: u32, ping_name: &nsACString) -> bool {
+ with_metric!(BOOLEAN_MAP, id, metric, test_get!(metric, ping_name))
+}
+
+#[no_mangle]
+pub extern "C" fn fog_boolean_test_get_error(id: u32, error_str: &mut nsACString) -> bool {
+ let err = with_metric!(BOOLEAN_MAP, id, metric, test_get_errors!(metric));
+ err.map(|err_str| error_str.assign(&err_str)).is_some()
+}
+
+#[no_mangle]
+pub extern "C" fn fog_boolean_set(id: u32, value: bool) {
+ with_metric!(BOOLEAN_MAP, id, metric, metric.set(value));
+}
diff --git a/toolkit/components/glean/api/src/ffi/counter.rs b/toolkit/components/glean/api/src/ffi/counter.rs
new file mode 100644
index 0000000000..5fba7c0dea
--- /dev/null
+++ b/toolkit/components/glean/api/src/ffi/counter.rs
@@ -0,0 +1,28 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
+
+#![cfg(feature = "with_gecko")]
+
+use nsstring::nsACString;
+
+#[no_mangle]
+pub unsafe extern "C" fn fog_counter_add(id: u32, amount: i32) {
+ with_metric!(COUNTER_MAP, id, metric, metric.add(amount));
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn fog_counter_test_has_value(id: u32, ping_name: &nsACString) -> bool {
+ with_metric!(COUNTER_MAP, id, metric, test_has!(metric, ping_name))
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn fog_counter_test_get_value(id: u32, ping_name: &nsACString) -> i32 {
+ with_metric!(COUNTER_MAP, id, metric, test_get!(metric, ping_name))
+}
+
+#[no_mangle]
+pub extern "C" fn fog_counter_test_get_error(id: u32, error_str: &mut nsACString) -> bool {
+ let err = with_metric!(COUNTER_MAP, id, metric, test_get_errors!(metric));
+ err.map(|err_str| error_str.assign(&err_str)).is_some()
+}
diff --git a/toolkit/components/glean/api/src/ffi/custom_distribution.rs b/toolkit/components/glean/api/src/ffi/custom_distribution.rs
new file mode 100644
index 0000000000..853a6e9845
--- /dev/null
+++ b/toolkit/components/glean/api/src/ffi/custom_distribution.rs
@@ -0,0 +1,82 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
+
+#![cfg(feature = "with_gecko")]
+
+use nsstring::nsACString;
+use thin_vec::ThinVec;
+
+#[no_mangle]
+pub extern "C" fn fog_custom_distribution_test_has_value(id: u32, ping_name: &nsACString) -> bool {
+ with_metric!(
+ CUSTOM_DISTRIBUTION_MAP,
+ id,
+ metric,
+ test_has!(metric, ping_name)
+ )
+}
+
+#[no_mangle]
+pub extern "C" fn fog_custom_distribution_test_get_value(
+ id: u32,
+ ping_name: &nsACString,
+ sum: &mut u64,
+ buckets: &mut ThinVec<u64>,
+ counts: &mut ThinVec<u64>,
+) {
+ let val = with_metric!(
+ CUSTOM_DISTRIBUTION_MAP,
+ id,
+ metric,
+ test_get!(metric, ping_name)
+ );
+ // FIXME(bug 1771885): Glean should use `u64` where it can.
+ *sum = val.sum as _;
+ for (&bucket, &count) in val.values.iter() {
+ buckets.push(bucket as _);
+ counts.push(count as _);
+ }
+}
+
+#[no_mangle]
+pub extern "C" fn fog_custom_distribution_accumulate_samples(id: u32, samples: &ThinVec<u64>) {
+ // N.B.: Avoid reallocation here by making the underlying type take a slice.
+ let samples = samples.into_iter().map(|&i| i as i64).collect();
+ with_metric!(
+ CUSTOM_DISTRIBUTION_MAP,
+ id,
+ metric,
+ metric.accumulate_samples_signed(samples)
+ );
+}
+
+#[no_mangle]
+pub extern "C" fn fog_custom_distribution_accumulate_samples_signed(
+ id: u32,
+ samples: &ThinVec<i64>,
+) {
+ // N.B.: Avoid reallocation here by making the underlying type take a slice.
+ let samples = samples.to_vec();
+ with_metric!(
+ CUSTOM_DISTRIBUTION_MAP,
+ id,
+ metric,
+ metric.accumulate_samples_signed(samples)
+ );
+}
+
+#[no_mangle]
+pub extern "C" fn fog_custom_distribution_test_get_error(
+ id: u32,
+
+ error_str: &mut nsACString,
+) -> bool {
+ let err = with_metric!(
+ CUSTOM_DISTRIBUTION_MAP,
+ id,
+ metric,
+ test_get_errors!(metric)
+ );
+ err.map(|err_str| error_str.assign(&err_str)).is_some()
+}
diff --git a/toolkit/components/glean/api/src/ffi/datetime.rs b/toolkit/components/glean/api/src/ffi/datetime.rs
new file mode 100644
index 0000000000..7529a524e6
--- /dev/null
+++ b/toolkit/components/glean/api/src/ffi/datetime.rs
@@ -0,0 +1,66 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
+
+#![cfg(feature = "with_gecko")]
+
+use nsstring::nsACString;
+
+#[repr(C)]
+pub struct FogDatetime {
+ year: i32,
+ month: u32,
+ day: u32,
+ hour: u32,
+ minute: u32,
+ second: u32,
+ nano: u32,
+ offset_seconds: i32,
+}
+
+#[no_mangle]
+pub extern "C" fn fog_datetime_test_has_value(id: u32, ping_name: &nsACString) -> bool {
+ with_metric!(DATETIME_MAP, id, metric, test_has!(metric, ping_name))
+}
+
+#[no_mangle]
+pub extern "C" fn fog_datetime_test_get_value(
+ id: u32,
+ ping_name: &nsACString,
+ value: &mut FogDatetime,
+) {
+ let val = with_metric!(DATETIME_MAP, id, metric, test_get!(metric, ping_name));
+ value.year = val.year;
+ value.month = val.month;
+ value.day = val.day;
+ value.hour = val.hour;
+ value.minute = val.minute;
+ value.second = val.second;
+ value.nano = val.nanosecond;
+ value.offset_seconds = val.offset_seconds;
+}
+
+#[no_mangle]
+pub extern "C" fn fog_datetime_set(id: u32, dt: &FogDatetime) {
+ with_metric!(
+ DATETIME_MAP,
+ id,
+ metric,
+ metric.set_with_details(
+ dt.year,
+ dt.month,
+ dt.day,
+ dt.hour,
+ dt.minute,
+ dt.second,
+ dt.nano,
+ dt.offset_seconds
+ )
+ );
+}
+
+#[no_mangle]
+pub extern "C" fn fog_datetime_test_get_error(id: u32, error_str: &mut nsACString) -> bool {
+ let err = with_metric!(DATETIME_MAP, id, metric, test_get_errors!(metric));
+ err.map(|err_str| error_str.assign(&err_str)).is_some()
+}
diff --git a/toolkit/components/glean/api/src/ffi/denominator.rs b/toolkit/components/glean/api/src/ffi/denominator.rs
new file mode 100644
index 0000000000..ccb047f530
--- /dev/null
+++ b/toolkit/components/glean/api/src/ffi/denominator.rs
@@ -0,0 +1,28 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
+
+#![cfg(feature = "with_gecko")]
+
+use nsstring::nsACString;
+
+#[no_mangle]
+pub unsafe extern "C" fn fog_denominator_add(id: u32, amount: i32) {
+ with_metric!(DENOMINATOR_MAP, id, metric, metric.add(amount));
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn fog_denominator_test_has_value(id: u32, ping_name: &nsACString) -> bool {
+ with_metric!(DENOMINATOR_MAP, id, metric, test_has!(metric, ping_name))
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn fog_denominator_test_get_value(id: u32, ping_name: &nsACString) -> i32 {
+ with_metric!(DENOMINATOR_MAP, id, metric, test_get!(metric, ping_name))
+}
+
+#[no_mangle]
+pub extern "C" fn fog_denominator_test_get_error(id: u32, error_str: &mut nsACString) -> bool {
+ let err = with_metric!(DENOMINATOR_MAP, id, metric, test_get_errors!(metric));
+ err.map(|err_str| error_str.assign(&err_str)).is_some()
+}
diff --git a/toolkit/components/glean/api/src/ffi/event.rs b/toolkit/components/glean/api/src/ffi/event.rs
new file mode 100644
index 0000000000..bd167021d6
--- /dev/null
+++ b/toolkit/components/glean/api/src/ffi/event.rs
@@ -0,0 +1,168 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
+
+#![cfg(feature = "with_gecko")]
+
+use std::collections::HashMap;
+
+use nsstring::{nsACString, nsCString};
+use thin_vec::ThinVec;
+
+use crate::metrics::__glean_metric_maps as metric_maps;
+use crate::private::EventRecordingError;
+
+#[no_mangle]
+pub extern "C" fn fog_event_record(
+ id: u32,
+ extra_keys: &ThinVec<nsCString>,
+ extra_values: &ThinVec<nsCString>,
+) {
+ // If no extra keys are passed, we can shortcut here.
+ if extra_keys.is_empty() {
+ if id & (1 << crate::factory::DYNAMIC_METRIC_BIT) > 0 {
+ let map = crate::factory::__jog_metric_maps::EVENT_MAP
+ .read()
+ .expect("Read lock for dynamic metric map was poisoned");
+ match map.get(&id.into()) {
+ Some(m) => m.record_raw(Default::default()),
+ None => panic!("No (dynamic) metric for event with id {}", id),
+ }
+ return;
+ }
+
+ if metric_maps::record_event_by_id(id, Default::default()).is_err() {
+ panic!("No event for id {}", id);
+ }
+
+ return;
+ }
+
+ assert_eq!(
+ extra_keys.len(),
+ extra_values.len(),
+ "Extra keys and values differ in length. ID: {}",
+ id
+ );
+
+ // Otherwise we need to decode them and pass them along.
+ let extra = extra_keys
+ .iter()
+ .zip(extra_values.iter())
+ .map(|(k, v)| (k.to_string(), v.to_string()))
+ .collect();
+ if id & (1 << crate::factory::DYNAMIC_METRIC_BIT) > 0 {
+ let map = crate::factory::__jog_metric_maps::EVENT_MAP
+ .read()
+ .expect("Read lock for dynamic metric map was poisoned");
+ match map.get(&id.into()) {
+ Some(m) => m.record_raw(extra),
+ None => panic!("No (dynamic) metric for event with id {}", id),
+ }
+ return;
+ } else {
+ match metric_maps::record_event_by_id(id, extra) {
+ Ok(()) => {}
+ Err(EventRecordingError::InvalidId) => panic!("No event for id {}", id),
+ Err(_) => panic!("Unpossible!"),
+ }
+ }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn fog_event_test_has_value(id: u32, ping_name: &nsACString) -> bool {
+ let storage = if ping_name.is_empty() {
+ None
+ } else {
+ Some(ping_name.to_utf8().into_owned())
+ };
+ if id & (1 << crate::factory::DYNAMIC_METRIC_BIT) > 0 {
+ let map = crate::factory::__jog_metric_maps::EVENT_MAP
+ .read()
+ .expect("Read lock for dynamic metric map was poisoned");
+ match map.get(&id.into()) {
+ Some(m) => m.test_get_value(storage.as_deref()).is_some(),
+ None => panic!("No (dynamic) metric for event with id {}", id),
+ }
+ } else {
+ metric_maps::event_test_get_value_wrapper(id, storage).is_some()
+ }
+}
+
+#[no_mangle]
+pub extern "C" fn fog_event_test_get_error(id: u32, error_str: &mut nsACString) -> bool {
+ let err = if id & (1 << crate::factory::DYNAMIC_METRIC_BIT) > 0 {
+ let map = crate::factory::__jog_metric_maps::EVENT_MAP
+ .read()
+ .expect("Read lock for dynamic metric map was poisoned");
+ match map.get(&id.into()) {
+ Some(m) => test_get_errors!(m),
+ None => panic!("No (dynamic) metric for event with id {}", id),
+ }
+ } else {
+ metric_maps::event_test_get_error(id)
+ };
+ err.map(|err_str| error_str.assign(&err_str)).is_some()
+}
+
+/// FFI-compatible representation of recorded event data.
+#[repr(C)]
+pub struct FfiRecordedEvent {
+ timestamp: u64,
+ category: nsCString,
+ name: nsCString,
+
+ /// Array of extra data, keys and values are interleaved.
+ extras: ThinVec<nsCString>,
+}
+
+#[no_mangle]
+pub extern "C" fn fog_event_test_get_value(
+ id: u32,
+ ping_name: &nsACString,
+ out_events: &mut ThinVec<FfiRecordedEvent>,
+) {
+ let storage = if ping_name.is_empty() {
+ None
+ } else {
+ Some(ping_name.to_utf8().into_owned())
+ };
+
+ let events = if id & (1 << crate::factory::DYNAMIC_METRIC_BIT) > 0 {
+ let map = crate::factory::__jog_metric_maps::EVENT_MAP
+ .read()
+ .expect("Read lock for dynamic metric map was poisoned");
+ let events = match map.get(&id.into()) {
+ Some(m) => m.test_get_value(storage.as_deref()),
+ None => return,
+ };
+ match events {
+ Some(events) => events,
+ None => return,
+ }
+ } else {
+ match metric_maps::event_test_get_value_wrapper(id, storage) {
+ Some(events) => events,
+ None => return,
+ }
+ };
+
+ for event in events {
+ let extra = event.extra.unwrap_or_else(HashMap::new);
+ let extra_len = extra.len();
+ let mut extras = ThinVec::with_capacity(extra_len * 2);
+ for (k, v) in extra.into_iter() {
+ extras.push(nsCString::from(k));
+ extras.push(nsCString::from(v));
+ }
+
+ let event = FfiRecordedEvent {
+ timestamp: event.timestamp,
+ category: nsCString::from(event.category),
+ name: nsCString::from(event.name),
+ extras,
+ };
+
+ out_events.push(event);
+ }
+}
diff --git a/toolkit/components/glean/api/src/ffi/labeled.rs b/toolkit/components/glean/api/src/ffi/labeled.rs
new file mode 100644
index 0000000000..2cd61230f7
--- /dev/null
+++ b/toolkit/components/glean/api/src/ffi/labeled.rs
@@ -0,0 +1,84 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
+
+#![cfg(feature = "with_gecko")]
+
+use crate::metrics::__glean_metric_maps as metric_maps;
+use nsstring::nsACString;
+use std::sync::atomic::Ordering;
+
+#[no_mangle]
+pub extern "C" fn fog_labeled_enum_to_str(id: u32, label: u16, value: &mut nsACString) {
+ let val = metric_maps::labeled_enum_to_str(id, label);
+ value.assign(&val);
+}
+
+#[no_mangle]
+pub extern "C" fn fog_labeled_boolean_get(id: u32, label: &nsACString) -> u32 {
+ labeled_submetric_get!(
+ id,
+ label,
+ LABELED_BOOLEAN_MAP,
+ labeled_boolean_get,
+ BOOLEAN_MAP,
+ LabeledBooleanMetric
+ )
+}
+
+#[no_mangle]
+pub extern "C" fn fog_labeled_boolean_enum_get(id: u32, label: u16) -> u32 {
+ labeled_submetric_enum_get!(
+ id,
+ label,
+ labeled_boolean_enum_get,
+ BOOLEAN_MAP,
+ LabeledBooleanMetric
+ )
+}
+
+#[no_mangle]
+pub extern "C" fn fog_labeled_counter_get(id: u32, label: &nsACString) -> u32 {
+ labeled_submetric_get!(
+ id,
+ label,
+ LABELED_COUNTER_MAP,
+ labeled_counter_get,
+ COUNTER_MAP,
+ LabeledCounterMetric
+ )
+}
+
+#[no_mangle]
+pub extern "C" fn fog_labeled_counter_enum_get(id: u32, label: u16) -> u32 {
+ labeled_submetric_enum_get!(
+ id,
+ label,
+ labeled_counter_enum_get,
+ COUNTER_MAP,
+ LabeledCounterMetric
+ )
+}
+
+#[no_mangle]
+pub extern "C" fn fog_labeled_string_get(id: u32, label: &nsACString) -> u32 {
+ labeled_submetric_get!(
+ id,
+ label,
+ LABELED_STRING_MAP,
+ labeled_string_get,
+ STRING_MAP,
+ LabeledStringMetric
+ )
+}
+
+#[no_mangle]
+pub extern "C" fn fog_labeled_string_enum_get(id: u32, label: u16) -> u32 {
+ labeled_submetric_enum_get!(
+ id,
+ label,
+ labeled_string_enum_get,
+ STRING_MAP,
+ LabeledStringMetric
+ )
+}
diff --git a/toolkit/components/glean/api/src/ffi/macros.rs b/toolkit/components/glean/api/src/ffi/macros.rs
new file mode 100644
index 0000000000..3571ebd88b
--- /dev/null
+++ b/toolkit/components/glean/api/src/ffi/macros.rs
@@ -0,0 +1,289 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
+
+//! Helper macros for implementing the FFI API for metric types.
+
+/// Get a metric object by ID from the corresponding map, then
+/// execute the provided closure with it.
+///
+/// # Arguments
+///
+/// * `$map` - The name of the hash map within `metrics::__glean_metric_maps`
+/// (or `factory::__jog_metric_maps`)
+/// as generated by glean_parser.
+/// * `$id` - The ID of the metric to get.
+/// * `$m` - The identifier to use for the retrieved metric.
+/// The expression `$f` can use this identifier.
+/// * `$f` - The expression to execute with the retrieved metric `$m`.
+macro_rules! with_metric {
+ (BOOLEAN_MAP, $id:ident, $m:ident, $f:expr) => {
+ maybe_labeled_with_metric!(BOOLEAN_MAP, $id, $m, $f)
+ };
+ (COUNTER_MAP, $id:ident, $m:ident, $f:expr) => {
+ maybe_labeled_with_metric!(COUNTER_MAP, $id, $m, $f)
+ };
+ (STRING_MAP, $id:ident, $m:ident, $f:expr) => {
+ maybe_labeled_with_metric!(STRING_MAP, $id, $m, $f)
+ };
+ ($map:ident, $id:ident, $m:ident, $f:expr) => {
+ just_with_metric!($map, $id, $m, $f)
+ };
+}
+
+/// Get a dynamically-registered metric object by id from the corresponding map,
+/// then execute the provided closure with it.
+///
+/// Assumes `$id` is for a dynamic non-submetric metric.
+/// Will panic if it isn't.
+///
+/// # Arguments
+///
+/// * `$map` - The name of the hash map within `factory::__jog_metric_maps`
+/// as generated by glean_parser.
+/// * `$id` - The ID of the metric to get.
+/// * `$m` - The identifier to use for the retrieved metric.
+/// The expression `$f` can use this identifier.
+/// * `$f` - The expression to execute with the retrieved metric `$m`.
+macro_rules! just_with_jog_metric {
+ ($map:ident, $id:ident, $m:ident, $f:expr) => {{
+ let map = $crate::factory::__jog_metric_maps::$map
+ .read()
+ .expect("Read lock for dynamic metric map was poisoned");
+ match map.get(&$id.into()) {
+ Some($m) => $f,
+ None => panic!("No (dynamic) metric for id {}", $id),
+ }
+ }};
+}
+
+/// Get a metric object by id from the corresponding map, then
+/// execute the provided closure with it.
+///
+/// Ignores the possibility that the $id might be for a labeled submetric.
+///
+/// # Arguments
+///
+/// * `$map` - The name of the hash map within `metrics::__glean_metric_maps`
+/// (or `factory::__jog_metric_maps`)
+/// as generated by glean_parser.
+/// * `$id` - The ID of the metric to get.
+/// * `$m` - The identifier to use for the retrieved metric.
+/// The expression `$f` can use this identifier.
+/// * `$f` - The expression to execute with the retrieved metric `$m`.
+macro_rules! just_with_metric {
+ ($map:ident, $id:ident, $m:ident, $f:expr) => {
+ if $id & (1 << $crate::factory::DYNAMIC_METRIC_BIT) > 0 {
+ just_with_jog_metric!($map, $id, $m, $f)
+ } else {
+ match $crate::metrics::__glean_metric_maps::$map.get(&$id.into()) {
+ Some($m) => $f,
+ None => panic!("No metric for id {}", $id),
+ }
+ }
+ };
+}
+
+/// Get a metric object by id from the corresponding map, then
+/// execute the provided closure with it.
+///
+/// Requires that the provided $map be of a type that can be labeled, since it
+/// assumes the presence of a same-named map in
+/// `metrics::_glean_metrics_map::submetric_maps`.
+///
+/// # Arguments
+///
+/// * `$map` - The name of the hash map within `metrics::__glean_metric_maps`
+/// and `metrics::__glean_metric_maps::submetric_maps` as generated
+/// by glean_parser.
+/// * `$id` - The ID of the metric to get.
+/// * `$m` - The identifier to use for the retrieved metric.
+/// The expression `$f` can use this identifier.
+/// * `$f` - The expression to execute with the retrieved metric `$m`.
+macro_rules! maybe_labeled_with_metric {
+ ($map:ident, $id:ident, $m:ident, $f:expr) => {
+ if $id & (1 << $crate::metrics::__glean_metric_maps::submetric_maps::SUBMETRIC_BIT) > 0 {
+ let map = $crate::metrics::__glean_metric_maps::submetric_maps::$map
+ .read()
+ .expect("Read lock for labeled metric map was poisoned");
+ match map.get(&$id.into()) {
+ Some($m) => $f,
+ None => panic!("No submetric for id {}", $id),
+ }
+ } else {
+ just_with_metric!($map, $id, $m, $f)
+ }
+ };
+}
+
+/// Test whether a value is stored for the given metric.
+///
+/// # Arguments
+///
+/// * `$metric` - The metric to test.
+/// * `$storage` - the storage name to look into.
+macro_rules! test_has {
+ ($metric:ident, $storage:ident) => {{
+ let storage = if $storage.is_empty() {
+ None
+ } else {
+ Some($storage.to_utf8())
+ };
+ $metric.test_get_value(storage.as_deref()).is_some()
+ }};
+}
+
+/// Get the currently stored value for the given metric.
+///
+/// # Arguments
+///
+/// * `$metric` - The metric to test.
+/// * `$storage` - the storage name to look into.
+macro_rules! test_get {
+ ($metric:ident, $storage:ident) => {{
+ let storage = if $storage.is_empty() {
+ None
+ } else {
+ Some($storage.to_utf8())
+ };
+ $metric.test_get_value(storage.as_deref()).unwrap()
+ }};
+}
+
+/// Check the provided metric in the provided storage for errors.
+/// On finding one, return an error string.
+///
+/// # Arguments
+///
+/// * `$metric` - The metric to test.
+macro_rules! test_get_errors {
+ ($metric:path) => {{
+ let error_types = [
+ glean::ErrorType::InvalidValue,
+ glean::ErrorType::InvalidLabel,
+ glean::ErrorType::InvalidState,
+ glean::ErrorType::InvalidOverflow,
+ ];
+ let mut error_str = None;
+ for &error_type in error_types.iter() {
+ let num_errors = $metric.test_get_num_recorded_errors(error_type);
+ if num_errors > 0 {
+ error_str = Some(format!(
+ "Metric had {} error(s) of type {}!",
+ num_errors,
+ error_type.as_str()
+ ));
+ break;
+ }
+ }
+ error_str
+ }};
+}
+
+/// Get the submetric id for a given labeled metric and label.
+///
+/// # Arguments
+///
+/// * `$id` - The id of the labeled metric.
+/// * `$label` - The (string) label of the submetric.
+/// * `$labeled_map` - The name of the labeled metric's map for retrieval (JOG only).
+/// * `$labeled_get` - The name of the labeled metric's get fn for retrieval.
+/// * `$submetric_map`- The name of the submetrics' map for storage.
+/// * `$metric_type` - The submetric's type (needed for an internal closure).
+macro_rules! labeled_submetric_get {
+ ($id:ident, $label:ident, $labeled_map:ident, $labeled_get:ident, $submetric_map:ident, $metric_type:ty) => {{
+ let tuple = ($id, $label.to_utf8().into());
+ {
+ let map = $crate::metrics::__glean_metric_maps::submetric_maps::LABELED_METRICS_TO_IDS
+ .read()
+ .expect("read lock of submetric ids was poisoned");
+ if let Some(submetric_id) = map.get(&tuple) {
+ return *submetric_id;
+ }
+ }
+
+ // Gotta actually create a new submetric with a new id.
+ let submetric_id =
+ $crate::metrics::__glean_metric_maps::submetric_maps::NEXT_LABELED_SUBMETRIC_ID
+ .fetch_add(1, Ordering::SeqCst);
+ {
+ if $id & (1 << $crate::factory::DYNAMIC_METRIC_BIT) > 0 {
+ just_with_jog_metric!($labeled_map, $id, metric, {
+ let submetric = metric.get(&tuple.1);
+ let mut map =
+ $crate::metrics::__glean_metric_maps::submetric_maps::$submetric_map
+ .write()
+ .expect("write lock of submetric map was poisoned");
+ map.insert(submetric_id.into(), submetric);
+ });
+ } else {
+ let mut map = $crate::metrics::__glean_metric_maps::submetric_maps::$submetric_map
+ .write()
+ .expect("write lock of submetric map was poisoned");
+ map.insert(
+ submetric_id.into(),
+ $crate::metrics::__glean_metric_maps::$labeled_get($id, &tuple.1),
+ );
+ }
+ }
+
+ let mut map = $crate::metrics::__glean_metric_maps::submetric_maps::LABELED_METRICS_TO_IDS
+ .write()
+ .expect("write lock of submetric ids was poisoned");
+ map.insert(tuple, submetric_id);
+ submetric_id
+ }};
+}
+
+/// Get the submetric id for a given labeled metric and label enum.
+///
+/// # Arguments
+///
+/// * `$id` - The id of the labeled metric.
+/// * `$label` - The (enum) label of the submetric.
+/// * `$labeled_get` - The name of the labeled metric's get fn for retrieval.
+/// * `$submetric_map`- The name of the submetrics' map for storage.
+/// * `$metric_type` - The submetric's type (needed for an internal closure).
+macro_rules! labeled_submetric_enum_get {
+ ($id:ident, $label_enum:ident, $labeled_get:ident, $submetric_map:ident, $metric_type:ty) => {{
+ let tuple = ($id, $label_enum.into());
+ // First: Have we seen this enum before? If so, give out the same submetric id.
+ {
+ let map = $crate::metrics::__glean_metric_maps::submetric_maps::LABELED_ENUMS_TO_IDS
+ .read()
+ .expect("read lock of enum submetric ids was poisoned");
+ if let Some(submetric_id) = map.get(&tuple) {
+ return *submetric_id;
+ }
+ }
+
+ // Alas, this is the first time we've needed to handle this metric with this enum.
+ // Gotta actually create a new submetric with a new id.
+ let submetric_id =
+ $crate::metrics::__glean_metric_maps::submetric_maps::NEXT_LABELED_SUBMETRIC_ID
+ .fetch_add(1, Ordering::SeqCst);
+ {
+ // What if the dynamic bit is set?
+ // JOG only supports JS, and enum_get isn't (yet) supported in JS.
+ assert_eq!(
+ 0,
+ $id & (1 << $crate::factory::DYNAMIC_METRIC_BIT),
+ "No enum_get support for JOG"
+ );
+ let mut map = $crate::metrics::__glean_metric_maps::submetric_maps::$submetric_map
+ .write()
+ .expect("write lock of submetric map was poisoned");
+ map.insert(
+ submetric_id.into(),
+ $crate::metrics::__glean_metric_maps::$labeled_get($id, tuple.1),
+ );
+ }
+
+ // And now ensure we store the submetric so we need not create it on subsequent calls.
+ let mut map = $crate::metrics::__glean_metric_maps::submetric_maps::LABELED_ENUMS_TO_IDS
+ .write()
+ .expect("write lock of submetric ids was poisoned");
+ map.insert(tuple, submetric_id);
+ submetric_id
+ }};
+}
diff --git a/toolkit/components/glean/api/src/ffi/memory_distribution.rs b/toolkit/components/glean/api/src/ffi/memory_distribution.rs
new file mode 100644
index 0000000000..cf09d3f8de
--- /dev/null
+++ b/toolkit/components/glean/api/src/ffi/memory_distribution.rs
@@ -0,0 +1,64 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
+
+#![cfg(feature = "with_gecko")]
+
+use nsstring::nsACString;
+use thin_vec::ThinVec;
+
+#[no_mangle]
+pub extern "C" fn fog_memory_distribution_test_has_value(id: u32, ping_name: &nsACString) -> bool {
+ with_metric!(
+ MEMORY_DISTRIBUTION_MAP,
+ id,
+ metric,
+ test_has!(metric, ping_name)
+ )
+}
+
+#[no_mangle]
+pub extern "C" fn fog_memory_distribution_test_get_value(
+ id: u32,
+ ping_name: &nsACString,
+ sum: &mut u64,
+ buckets: &mut ThinVec<u64>,
+ counts: &mut ThinVec<u64>,
+) {
+ let val = with_metric!(
+ MEMORY_DISTRIBUTION_MAP,
+ id,
+ metric,
+ test_get!(metric, ping_name)
+ );
+ *sum = val.sum as _;
+ for (&bucket, &count) in val.values.iter() {
+ buckets.push(bucket as _);
+ counts.push(count as _);
+ }
+}
+
+#[no_mangle]
+pub extern "C" fn fog_memory_distribution_accumulate(id: u32, sample: u64) {
+ with_metric!(
+ MEMORY_DISTRIBUTION_MAP,
+ id,
+ metric,
+ metric.accumulate(sample)
+ );
+}
+
+#[no_mangle]
+pub extern "C" fn fog_memory_distribution_test_get_error(
+ id: u32,
+
+ error_str: &mut nsACString,
+) -> bool {
+ let err = with_metric!(
+ MEMORY_DISTRIBUTION_MAP,
+ id,
+ metric,
+ test_get_errors!(metric)
+ );
+ err.map(|err_str| error_str.assign(&err_str)).is_some()
+}
diff --git a/toolkit/components/glean/api/src/ffi/mod.rs b/toolkit/components/glean/api/src/ffi/mod.rs
new file mode 100644
index 0000000000..23235fc2f1
--- /dev/null
+++ b/toolkit/components/glean/api/src/ffi/mod.rs
@@ -0,0 +1,28 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
+
+#![cfg(feature = "with_gecko")]
+
+#[macro_use]
+mod macros;
+
+mod boolean;
+mod counter;
+mod custom_distribution;
+mod datetime;
+mod denominator;
+mod event;
+mod labeled;
+mod memory_distribution;
+mod numerator;
+mod ping;
+mod quantity;
+mod rate;
+mod string;
+mod string_list;
+mod text;
+mod timespan;
+mod timing_distribution;
+mod url;
+mod uuid;
diff --git a/toolkit/components/glean/api/src/ffi/numerator.rs b/toolkit/components/glean/api/src/ffi/numerator.rs
new file mode 100644
index 0000000000..e679e426c4
--- /dev/null
+++ b/toolkit/components/glean/api/src/ffi/numerator.rs
@@ -0,0 +1,35 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
+
+#![cfg(feature = "with_gecko")]
+
+use nsstring::nsACString;
+
+#[no_mangle]
+pub unsafe extern "C" fn fog_numerator_add_to_numerator(id: u32, amount: i32) {
+ with_metric!(NUMERATOR_MAP, id, metric, metric.add_to_numerator(amount));
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn fog_numerator_test_has_value(id: u32, ping_name: &nsACString) -> bool {
+ with_metric!(NUMERATOR_MAP, id, metric, test_has!(metric, ping_name))
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn fog_numerator_test_get_value(
+ id: u32,
+ ping_name: &nsACString,
+ num: &mut i32,
+ den: &mut i32,
+) {
+ let rate = with_metric!(NUMERATOR_MAP, id, metric, test_get!(metric, ping_name));
+ *num = rate.numerator;
+ *den = rate.denominator;
+}
+
+#[no_mangle]
+pub extern "C" fn fog_numerator_test_get_error(id: u32, error_str: &mut nsACString) -> bool {
+ let err = with_metric!(NUMERATOR_MAP, id, metric, test_get_errors!(metric));
+ err.map(|err_str| error_str.assign(&err_str)).is_some()
+}
diff --git a/toolkit/components/glean/api/src/ffi/ping.rs b/toolkit/components/glean/api/src/ffi/ping.rs
new file mode 100644
index 0000000000..2834655a03
--- /dev/null
+++ b/toolkit/components/glean/api/src/ffi/ping.rs
@@ -0,0 +1,18 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
+
+#![cfg(feature = "with_gecko")]
+
+use crate::pings;
+use nsstring::nsACString;
+
+#[no_mangle]
+pub extern "C" fn fog_submit_ping_by_id(id: u32, reason: &nsACString) {
+ let reason = if reason.is_empty() {
+ None
+ } else {
+ Some(reason.to_utf8())
+ };
+ pings::submit_ping_by_id(id, reason.as_deref());
+}
diff --git a/toolkit/components/glean/api/src/ffi/quantity.rs b/toolkit/components/glean/api/src/ffi/quantity.rs
new file mode 100644
index 0000000000..7f94bcff27
--- /dev/null
+++ b/toolkit/components/glean/api/src/ffi/quantity.rs
@@ -0,0 +1,28 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
+
+#![cfg(feature = "with_gecko")]
+
+use nsstring::nsACString;
+
+#[no_mangle]
+pub extern "C" fn fog_quantity_set(id: u32, value: i64) {
+ with_metric!(QUANTITY_MAP, id, metric, metric.set(value));
+}
+
+#[no_mangle]
+pub extern "C" fn fog_quantity_test_has_value(id: u32, ping_name: &nsACString) -> bool {
+ with_metric!(QUANTITY_MAP, id, metric, test_has!(metric, ping_name))
+}
+
+#[no_mangle]
+pub extern "C" fn fog_quantity_test_get_value(id: u32, ping_name: &nsACString) -> i64 {
+ with_metric!(QUANTITY_MAP, id, metric, test_get!(metric, ping_name))
+}
+
+#[no_mangle]
+pub extern "C" fn fog_quantity_test_get_error(id: u32, error_str: &mut nsACString) -> bool {
+ let err = with_metric!(QUANTITY_MAP, id, metric, test_get_errors!(metric));
+ err.map(|err_str| error_str.assign(&err_str)).is_some()
+}
diff --git a/toolkit/components/glean/api/src/ffi/rate.rs b/toolkit/components/glean/api/src/ffi/rate.rs
new file mode 100644
index 0000000000..c14b33f3ea
--- /dev/null
+++ b/toolkit/components/glean/api/src/ffi/rate.rs
@@ -0,0 +1,40 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
+
+#![cfg(feature = "with_gecko")]
+
+use nsstring::nsACString;
+
+#[no_mangle]
+pub unsafe extern "C" fn fog_rate_add_to_numerator(id: u32, amount: i32) {
+ with_metric!(RATE_MAP, id, metric, metric.add_to_numerator(amount));
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn fog_rate_add_to_denominator(id: u32, amount: i32) {
+ with_metric!(RATE_MAP, id, metric, metric.add_to_denominator(amount));
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn fog_rate_test_has_value(id: u32, ping_name: &nsACString) -> bool {
+ with_metric!(RATE_MAP, id, metric, test_has!(metric, ping_name))
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn fog_rate_test_get_value(
+ id: u32,
+ ping_name: &nsACString,
+ num: &mut i32,
+ den: &mut i32,
+) {
+ let rate = with_metric!(RATE_MAP, id, metric, test_get!(metric, ping_name));
+ *num = rate.numerator;
+ *den = rate.denominator;
+}
+
+#[no_mangle]
+pub extern "C" fn fog_rate_test_get_error(id: u32, error_str: &mut nsACString) -> bool {
+ let err = with_metric!(RATE_MAP, id, metric, test_get_errors!(metric));
+ err.map(|err_str| error_str.assign(&err_str)).is_some()
+}
diff --git a/toolkit/components/glean/api/src/ffi/string.rs b/toolkit/components/glean/api/src/ffi/string.rs
new file mode 100644
index 0000000000..fc28e03a38
--- /dev/null
+++ b/toolkit/components/glean/api/src/ffi/string.rs
@@ -0,0 +1,33 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
+
+#![cfg(feature = "with_gecko")]
+
+use nsstring::nsACString;
+
+#[no_mangle]
+pub extern "C" fn fog_string_test_has_value(id: u32, ping_name: &nsACString) -> bool {
+ with_metric!(STRING_MAP, id, metric, test_has!(metric, ping_name))
+}
+
+#[no_mangle]
+pub extern "C" fn fog_string_test_get_value(
+ id: u32,
+ ping_name: &nsACString,
+ value: &mut nsACString,
+) {
+ let val = with_metric!(STRING_MAP, id, metric, test_get!(metric, ping_name));
+ value.assign(&val);
+}
+
+#[no_mangle]
+pub extern "C" fn fog_string_set(id: u32, value: &nsACString) {
+ with_metric!(STRING_MAP, id, metric, metric.set(value.to_utf8()));
+}
+
+#[no_mangle]
+pub extern "C" fn fog_string_test_get_error(id: u32, error_str: &mut nsACString) -> bool {
+ let err = with_metric!(STRING_MAP, id, metric, test_get_errors!(metric));
+ err.map(|err_str| error_str.assign(&err_str)).is_some()
+}
diff --git a/toolkit/components/glean/api/src/ffi/string_list.rs b/toolkit/components/glean/api/src/ffi/string_list.rs
new file mode 100644
index 0000000000..42ebd9e445
--- /dev/null
+++ b/toolkit/components/glean/api/src/ffi/string_list.rs
@@ -0,0 +1,42 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
+
+#![cfg(feature = "with_gecko")]
+
+use nsstring::{nsACString, nsCString};
+use thin_vec::ThinVec;
+
+#[no_mangle]
+pub extern "C" fn fog_string_list_test_has_value(id: u32, ping_name: &nsACString) -> bool {
+ with_metric!(STRING_LIST_MAP, id, metric, test_has!(metric, ping_name))
+}
+
+#[no_mangle]
+pub extern "C" fn fog_string_list_test_get_value(
+ id: u32,
+ ping_name: &nsACString,
+ value: &mut ThinVec<nsCString>,
+) {
+ let val = with_metric!(STRING_LIST_MAP, id, metric, test_get!(metric, ping_name));
+ for v in val {
+ value.push(v.into());
+ }
+}
+
+#[no_mangle]
+pub extern "C" fn fog_string_list_add(id: u32, value: &nsACString) {
+ with_metric!(STRING_LIST_MAP, id, metric, metric.add(value.to_utf8()));
+}
+
+#[no_mangle]
+pub extern "C" fn fog_string_list_set(id: u32, value: &ThinVec<nsCString>) {
+ let value = value.iter().map(|s| s.to_utf8().into()).collect();
+ with_metric!(STRING_LIST_MAP, id, metric, metric.set(value));
+}
+
+#[no_mangle]
+pub extern "C" fn fog_string_list_test_get_error(id: u32, error_str: &mut nsACString) -> bool {
+ let err = with_metric!(STRING_LIST_MAP, id, metric, test_get_errors!(metric));
+ err.map(|err_str| error_str.assign(&err_str)).is_some()
+}
diff --git a/toolkit/components/glean/api/src/ffi/text.rs b/toolkit/components/glean/api/src/ffi/text.rs
new file mode 100644
index 0000000000..da46fb849f
--- /dev/null
+++ b/toolkit/components/glean/api/src/ffi/text.rs
@@ -0,0 +1,29 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
+
+#![cfg(feature = "with_gecko")]
+
+use nsstring::nsACString;
+
+#[no_mangle]
+pub extern "C" fn fog_text_test_has_value(id: u32, ping_name: &nsACString) -> bool {
+ with_metric!(TEXT_MAP, id, metric, test_has!(metric, ping_name))
+}
+
+#[no_mangle]
+pub extern "C" fn fog_text_test_get_value(id: u32, ping_name: &nsACString, value: &mut nsACString) {
+ let val = with_metric!(TEXT_MAP, id, metric, test_get!(metric, ping_name));
+ value.assign(&val);
+}
+
+#[no_mangle]
+pub extern "C" fn fog_text_set(id: u32, value: &nsACString) {
+ with_metric!(TEXT_MAP, id, metric, metric.set(value.to_utf8()));
+}
+
+#[no_mangle]
+pub extern "C" fn fog_text_test_get_error(id: u32, error_str: &mut nsACString) -> bool {
+ let err = with_metric!(TEXT_MAP, id, metric, test_get_errors!(metric));
+ err.map(|err_str| error_str.assign(&err_str)).is_some()
+}
diff --git a/toolkit/components/glean/api/src/ffi/timespan.rs b/toolkit/components/glean/api/src/ffi/timespan.rs
new file mode 100644
index 0000000000..5de411ffab
--- /dev/null
+++ b/toolkit/components/glean/api/src/ffi/timespan.rs
@@ -0,0 +1,48 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
+
+#![cfg(feature = "with_gecko")]
+
+use nsstring::nsACString;
+
+#[no_mangle]
+pub extern "C" fn fog_timespan_start(id: u32) {
+ with_metric!(TIMESPAN_MAP, id, metric, metric.start());
+}
+
+#[no_mangle]
+pub extern "C" fn fog_timespan_stop(id: u32) {
+ with_metric!(TIMESPAN_MAP, id, metric, metric.stop());
+}
+
+#[no_mangle]
+pub extern "C" fn fog_timespan_cancel(id: u32) {
+ with_metric!(TIMESPAN_MAP, id, metric, metric.cancel());
+}
+
+#[no_mangle]
+pub extern "C" fn fog_timespan_set_raw(id: u32, duration: u32) {
+ with_metric!(
+ TIMESPAN_MAP,
+ id,
+ metric,
+ metric.set_raw_unitless(duration.into())
+ );
+}
+
+#[no_mangle]
+pub extern "C" fn fog_timespan_test_has_value(id: u32, ping_name: &nsACString) -> bool {
+ with_metric!(TIMESPAN_MAP, id, metric, test_has!(metric, ping_name))
+}
+
+#[no_mangle]
+pub extern "C" fn fog_timespan_test_get_value(id: u32, ping_name: &nsACString) -> u64 {
+ with_metric!(TIMESPAN_MAP, id, metric, test_get!(metric, ping_name))
+}
+
+#[no_mangle]
+pub extern "C" fn fog_timespan_test_get_error(id: u32, error_str: &mut nsACString) -> bool {
+ let err = with_metric!(TIMESPAN_MAP, id, metric, test_get_errors!(metric));
+ err.map(|err_str| error_str.assign(&err_str)).is_some()
+}
diff --git a/toolkit/components/glean/api/src/ffi/timing_distribution.rs b/toolkit/components/glean/api/src/ffi/timing_distribution.rs
new file mode 100644
index 0000000000..4ac5d03986
--- /dev/null
+++ b/toolkit/components/glean/api/src/ffi/timing_distribution.rs
@@ -0,0 +1,90 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
+
+#![cfg(feature = "with_gecko")]
+
+use nsstring::nsACString;
+use std::time::Duration;
+use thin_vec::ThinVec;
+
+#[no_mangle]
+pub extern "C" fn fog_timing_distribution_start(id: u32) -> u64 {
+ with_metric!(TIMING_DISTRIBUTION_MAP, id, metric, metric.start().id)
+}
+
+#[no_mangle]
+pub extern "C" fn fog_timing_distribution_stop_and_accumulate(id: u32, timing_id: u64) {
+ with_metric!(
+ TIMING_DISTRIBUTION_MAP,
+ id,
+ metric,
+ metric.stop_and_accumulate(timing_id.into())
+ );
+}
+
+#[no_mangle]
+pub extern "C" fn fog_timing_distribution_accumulate_raw_nanos(id: u32, sample: u64) {
+ with_metric!(
+ TIMING_DISTRIBUTION_MAP,
+ id,
+ metric,
+ metric.accumulate_raw_duration(Duration::from_nanos(sample))
+ );
+}
+
+#[no_mangle]
+pub extern "C" fn fog_timing_distribution_cancel(id: u32, timing_id: u64) {
+ with_metric!(
+ TIMING_DISTRIBUTION_MAP,
+ id,
+ metric,
+ metric.cancel(timing_id.into())
+ );
+}
+
+#[no_mangle]
+pub extern "C" fn fog_timing_distribution_test_has_value(id: u32, ping_name: &nsACString) -> bool {
+ with_metric!(
+ TIMING_DISTRIBUTION_MAP,
+ id,
+ metric,
+ test_has!(metric, ping_name)
+ )
+}
+
+#[no_mangle]
+pub extern "C" fn fog_timing_distribution_test_get_value(
+ id: u32,
+ ping_name: &nsACString,
+ sum: &mut u64,
+ buckets: &mut ThinVec<u64>,
+ counts: &mut ThinVec<u64>,
+) {
+ let val = with_metric!(
+ TIMING_DISTRIBUTION_MAP,
+ id,
+ metric,
+ test_get!(metric, ping_name)
+ );
+ *sum = val.sum as _;
+ for (&bucket, &count) in val.values.iter() {
+ buckets.push(bucket as _);
+ counts.push(count as _);
+ }
+}
+
+#[no_mangle]
+pub extern "C" fn fog_timing_distribution_test_get_error(
+ id: u32,
+
+ error_str: &mut nsACString,
+) -> bool {
+ let err = with_metric!(
+ TIMING_DISTRIBUTION_MAP,
+ id,
+ metric,
+ test_get_errors!(metric)
+ );
+ err.map(|err_str| error_str.assign(&err_str)).is_some()
+}
diff --git a/toolkit/components/glean/api/src/ffi/url.rs b/toolkit/components/glean/api/src/ffi/url.rs
new file mode 100644
index 0000000000..b94915f7cf
--- /dev/null
+++ b/toolkit/components/glean/api/src/ffi/url.rs
@@ -0,0 +1,29 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
+
+#![cfg(feature = "with_gecko")]
+
+use nsstring::nsACString;
+
+#[no_mangle]
+pub extern "C" fn fog_url_test_has_value(id: u32, ping_name: &nsACString) -> bool {
+ with_metric!(URL_MAP, id, metric, test_has!(metric, ping_name))
+}
+
+#[no_mangle]
+pub extern "C" fn fog_url_test_get_value(id: u32, ping_name: &nsACString, value: &mut nsACString) {
+ let val = with_metric!(URL_MAP, id, metric, test_get!(metric, ping_name));
+ value.assign(&val);
+}
+
+#[no_mangle]
+pub extern "C" fn fog_url_set(id: u32, value: &nsACString) {
+ with_metric!(URL_MAP, id, metric, metric.set(value.to_utf8()));
+}
+
+#[no_mangle]
+pub extern "C" fn fog_url_test_get_error(id: u32, error_str: &mut nsACString) -> bool {
+ let err = with_metric!(URL_MAP, id, metric, test_get_errors!(metric));
+ err.map(|err_str| error_str.assign(&err_str)).is_some()
+}
diff --git a/toolkit/components/glean/api/src/ffi/uuid.rs b/toolkit/components/glean/api/src/ffi/uuid.rs
new file mode 100644
index 0000000000..e3101863ea
--- /dev/null
+++ b/toolkit/components/glean/api/src/ffi/uuid.rs
@@ -0,0 +1,37 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
+
+#![cfg(feature = "with_gecko")]
+
+use nsstring::nsACString;
+use uuid::Uuid;
+
+#[no_mangle]
+pub extern "C" fn fog_uuid_test_has_value(id: u32, ping_name: &nsACString) -> bool {
+ with_metric!(UUID_MAP, id, metric, test_has!(metric, ping_name))
+}
+
+#[no_mangle]
+pub extern "C" fn fog_uuid_test_get_value(id: u32, ping_name: &nsACString, value: &mut nsACString) {
+ let uuid = with_metric!(UUID_MAP, id, metric, test_get!(metric, ping_name)).to_string();
+ value.assign(&uuid);
+}
+
+#[no_mangle]
+pub extern "C" fn fog_uuid_set(id: u32, value: &nsACString) {
+ if let Ok(uuid) = Uuid::parse_str(&value.to_utf8()) {
+ with_metric!(UUID_MAP, id, metric, metric.set(uuid));
+ }
+}
+
+#[no_mangle]
+pub extern "C" fn fog_uuid_generate_and_set(id: u32) {
+ with_metric!(UUID_MAP, id, metric, metric.generate_and_set());
+}
+
+#[no_mangle]
+pub extern "C" fn fog_uuid_test_get_error(id: u32, error_str: &mut nsACString) -> bool {
+ let err = with_metric!(UUID_MAP, id, metric, test_get_errors!(metric));
+ err.map(|err_str| error_str.assign(&err_str)).is_some()
+}
diff --git a/toolkit/components/glean/api/src/ipc.rs b/toolkit/components/glean/api/src/ipc.rs
new file mode 100644
index 0000000000..9c23e1aa3b
--- /dev/null
+++ b/toolkit/components/glean/api/src/ipc.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/.
+
+//! IPC Implementation, Rust part
+
+use crate::private::MetricId;
+use once_cell::sync::Lazy;
+use serde::{Deserialize, Serialize};
+use std::collections::HashMap;
+#[cfg(not(feature = "with_gecko"))]
+use std::sync::atomic::AtomicBool;
+use std::sync::atomic::{AtomicUsize, Ordering};
+use std::sync::Mutex;
+#[cfg(feature = "with_gecko")]
+use {std::convert::TryInto, std::sync::atomic::AtomicU32, xpcom::interfaces::nsIXULRuntime};
+
+use super::metrics::__glean_metric_maps;
+
+type EventRecord = (u64, HashMap<String, String>);
+
+/// Contains all the information necessary to update the metrics on the main
+/// process.
+#[derive(Debug, Default, Deserialize, Serialize)]
+pub struct IPCPayload {
+ pub counters: HashMap<MetricId, i32>,
+ pub custom_samples: HashMap<MetricId, Vec<i64>>,
+ pub denominators: HashMap<MetricId, i32>,
+ pub events: HashMap<MetricId, Vec<EventRecord>>,
+ pub labeled_counters: HashMap<MetricId, HashMap<String, i32>>,
+ pub memory_samples: HashMap<MetricId, Vec<u64>>,
+ pub numerators: HashMap<MetricId, i32>,
+ pub rates: HashMap<MetricId, (i32, i32)>,
+ pub string_lists: HashMap<MetricId, Vec<String>>,
+ pub timing_samples: HashMap<MetricId, Vec<u64>>,
+}
+
+/// Global singleton: pending IPC payload.
+static PAYLOAD: Lazy<Mutex<IPCPayload>> = Lazy::new(|| Mutex::new(IPCPayload::default()));
+/// Global singleton: number of times the IPC payload was accessed.
+static PAYLOAD_ACCESS_COUNT: AtomicUsize = AtomicUsize::new(0);
+
+// The maximum size of an IPC message in Firefox Desktop is 256MB.
+// (See IPC::Channel::kMaximumMessageSize)
+// In `IPCPayload` the largest size can be attained in the fewest accesses via events.
+// Each event could be its own u64 id, u64 timestamp, and HashMap of ten i32 to ten 100-byte strings.
+// That's 1056B = 8 + 8 + 10(4 + 100)
+// In 256MB we can fit 254200 or so of these, not counting overhead.
+// Let's take a conservative estimate of 100000 to
+// 0) Account for overhead
+// 1) Not be greedy
+// 2) Allow time for the dispatch to main thread which will actually perform the flush
+// "Why the -1?" Because fetch_add returns the value before the addition.
+const PAYLOAD_ACCESS_WATERMARK: usize = 100000 - 1;
+
+pub fn with_ipc_payload<F, R>(f: F) -> R
+where
+ F: FnOnce(&mut IPCPayload) -> R,
+{
+ if PAYLOAD_ACCESS_COUNT.fetch_add(1, Ordering::SeqCst) > PAYLOAD_ACCESS_WATERMARK {
+ // We reset this before the actual flush to keep all the logic together.
+ // Otherwise the count reset would need to happen down in take_buf().
+ // This may overcount (resulting in undersized payloads) which is okay.
+ PAYLOAD_ACCESS_COUNT.store(0, Ordering::SeqCst);
+ handle_payload_filling();
+ }
+ let mut payload = PAYLOAD.lock().unwrap();
+ f(&mut payload)
+}
+
+/// Do we need IPC?
+///
+/// Thread-safe.
+#[cfg(feature = "with_gecko")]
+static PROCESS_TYPE: Lazy<AtomicU32> = Lazy::new(|| {
+ extern "C" {
+ fn FOG_GetProcessType() -> i32;
+ }
+ // SAFETY NOTE: Safe because it returns a primitive by value.
+ let process_type = unsafe { FOG_GetProcessType() };
+ // It's impossible for i32 to overflow u32, but maybe someone got clever
+ // and introduced a negative process type constant. Default to parent.
+ let process_type = process_type
+ .try_into()
+ .unwrap_or(nsIXULRuntime::PROCESS_TYPE_DEFAULT);
+ // We don't have process-specific init locations outside of the main
+ // process, so we introduce this side-effect to a global static init.
+ // This is the absolute first time we decide which process type we're
+ // treating this process as, so this is the earliest we can do this.
+ register_process_shutdown(process_type);
+ AtomicU32::new(process_type)
+});
+
+#[cfg(feature = "with_gecko")]
+pub fn need_ipc() -> bool {
+ PROCESS_TYPE.load(Ordering::Relaxed) != nsIXULRuntime::PROCESS_TYPE_DEFAULT
+}
+
+/// The first time we're used in a process,
+/// we'll need to start thinking about cleanup.
+///
+/// Please only call once per process.
+/// Multiple calls may register multiple handlers.
+#[cfg(feature = "with_gecko")]
+fn register_process_shutdown(process_type: u32) {
+ match process_type {
+ nsIXULRuntime::PROCESS_TYPE_DEFAULT => {
+ // Parent process shutdown is handled by the FOG XPCOM Singleton.
+ }
+ nsIXULRuntime::PROCESS_TYPE_CONTENT => {
+ // Content child shutdown is in C++ for access to RunOnShutdown().
+ extern "C" {
+ fn FOG_RegisterContentChildShutdown();
+ }
+ unsafe {
+ FOG_RegisterContentChildShutdown();
+ };
+ }
+ nsIXULRuntime::PROCESS_TYPE_GMPLUGIN => {
+ // GMP process shutdown is handled in GMPChild::ActorDestroy.
+ }
+ nsIXULRuntime::PROCESS_TYPE_GPU => {
+ // GPU process shutdown is handled in GPUParent::ActorDestroy.
+ }
+ nsIXULRuntime::PROCESS_TYPE_RDD => {
+ // RDD process shutdown is handled in RDDParent::ActorDestroy.
+ }
+ nsIXULRuntime::PROCESS_TYPE_SOCKET => {
+ // Socket process shutdown is handled in SocketProcessChild::ActorDestroy.
+ }
+ nsIXULRuntime::PROCESS_TYPE_UTILITY => {
+ // Utility process shutdown is handled in UtilityProcessChild::ActorDestroy.
+ }
+ _ => {
+ // We don't yet support other process types.
+ log::error!("Process type {} tried to use FOG, but isn't supported! (Process type constants are in nsIXULRuntime.rs)", process_type);
+ }
+ }
+}
+
+/// An RAII that, on drop, restores the value used to determine whether FOG
+/// needs IPC. Used in tests.
+/// ```rust,ignore
+/// #[test]
+/// fn test_need_ipc_raii() {
+/// assert!(false == ipc::need_ipc());
+/// {
+/// let _raii = ipc::test_set_need_ipc(true);
+/// assert!(ipc::need_ipc());
+/// }
+/// assert!(false == ipc::need_ipc());
+/// }
+/// ```
+#[cfg(not(feature = "with_gecko"))]
+pub struct TestNeedIpcRAII {
+ prev_value: bool,
+}
+
+#[cfg(not(feature = "with_gecko"))]
+impl Drop for TestNeedIpcRAII {
+ fn drop(&mut self) {
+ TEST_NEED_IPC.store(self.prev_value, Ordering::Relaxed);
+ }
+}
+
+#[cfg(not(feature = "with_gecko"))]
+static TEST_NEED_IPC: AtomicBool = AtomicBool::new(false);
+
+/// Test-only API for telling FOG to use IPC mechanisms even if the test has
+/// only the one process. See TestNeedIpcRAII for an example.
+#[cfg(not(feature = "with_gecko"))]
+pub fn test_set_need_ipc(need_ipc: bool) -> TestNeedIpcRAII {
+ TestNeedIpcRAII {
+ prev_value: TEST_NEED_IPC.swap(need_ipc, Ordering::Relaxed),
+ }
+}
+
+#[cfg(not(feature = "with_gecko"))]
+pub fn need_ipc() -> bool {
+ TEST_NEED_IPC.load(Ordering::Relaxed)
+}
+
+pub fn take_buf() -> Option<Vec<u8>> {
+ with_ipc_payload(move |payload| {
+ let buf = bincode::serialize(&payload).ok();
+ *payload = IPCPayload {
+ ..Default::default()
+ };
+ buf
+ })
+}
+
+#[cfg(not(feature = "with_gecko"))]
+fn handle_payload_filling() {
+ // Space intentionally left blank.
+ // Without Gecko IPC to drain the buffer, there's nothing we can do.
+}
+
+#[cfg(feature = "with_gecko")]
+fn handle_payload_filling() {
+ extern "C" {
+ fn FOG_IPCPayloadFull();
+ }
+ // SAFETY NOTE: Safe because it doesn't take or return values.
+ unsafe { FOG_IPCPayloadFull() };
+}
+
+#[cfg(not(feature = "with_gecko"))]
+pub fn is_in_automation() -> bool {
+ // Without Gecko IPC to drain the buffer, there's nothing we can do.
+ false
+}
+
+#[cfg(feature = "with_gecko")]
+pub fn is_in_automation() -> bool {
+ extern "C" {
+ fn FOG_IPCIsInAutomation() -> bool;
+ }
+ // SAFETY NOTE: Safe because it returns a primitive by value.
+ unsafe { FOG_IPCIsInAutomation() }
+}
+
+// Reason: We instrument the error counts,
+// but don't need more detailed error information at the moment.
+#[allow(clippy::result_unit_err)]
+pub fn replay_from_buf(buf: &[u8]) -> Result<(), ()> {
+ // TODO: Instrument failures to find metrics by id.
+ let ipc_payload: IPCPayload = bincode::deserialize(buf).map_err(|_| ())?;
+ for (id, value) in ipc_payload.counters.into_iter() {
+ if id.0 & (1 << crate::factory::DYNAMIC_METRIC_BIT) > 0 {
+ let map = crate::factory::__jog_metric_maps::COUNTER_MAP
+ .read()
+ .expect("Read lock for dynamic counter map was poisoned");
+ if let Some(metric) = map.get(&id) {
+ metric.add(value);
+ }
+ } else if let Some(metric) = __glean_metric_maps::COUNTER_MAP.get(&id) {
+ metric.add(value);
+ }
+ }
+ for (id, samples) in ipc_payload.custom_samples.into_iter() {
+ if id.0 & (1 << crate::factory::DYNAMIC_METRIC_BIT) > 0 {
+ let map = crate::factory::__jog_metric_maps::CUSTOM_DISTRIBUTION_MAP
+ .read()
+ .expect("Read lock for dynamic custom distribution map was poisoned");
+ if let Some(metric) = map.get(&id) {
+ metric.accumulate_samples_signed(samples);
+ }
+ } else if let Some(metric) = __glean_metric_maps::CUSTOM_DISTRIBUTION_MAP.get(&id) {
+ metric.accumulate_samples_signed(samples);
+ }
+ }
+ for (id, value) in ipc_payload.denominators.into_iter() {
+ if id.0 & (1 << crate::factory::DYNAMIC_METRIC_BIT) > 0 {
+ let map = crate::factory::__jog_metric_maps::DENOMINATOR_MAP
+ .read()
+ .expect("Read lock for dynamic denominator map was poisoned");
+ if let Some(metric) = map.get(&id) {
+ metric.add(value);
+ }
+ } else if let Some(metric) = __glean_metric_maps::DENOMINATOR_MAP.get(&id) {
+ metric.add(value);
+ }
+ }
+ for (id, records) in ipc_payload.events.into_iter() {
+ if id.0 & (1 << crate::factory::DYNAMIC_METRIC_BIT) > 0 {
+ let map = crate::factory::__jog_metric_maps::EVENT_MAP
+ .read()
+ .expect("Read lock for dynamic event map was poisoned");
+ if let Some(metric) = map.get(&id) {
+ for (timestamp, extra) in records.into_iter() {
+ metric.record_with_time(timestamp, extra);
+ }
+ }
+ } else {
+ for (timestamp, extra) in records.into_iter() {
+ let _ = __glean_metric_maps::record_event_by_id_with_time(id, timestamp, extra);
+ }
+ }
+ }
+ for (id, labeled_counts) in ipc_payload.labeled_counters.into_iter() {
+ if id.0 & (1 << crate::factory::DYNAMIC_METRIC_BIT) > 0 {
+ let map = crate::factory::__jog_metric_maps::LABELED_COUNTER_MAP
+ .read()
+ .expect("Read lock for dynamic labeled counter map was poisoned");
+ if let Some(metric) = map.get(&id) {
+ for (label, count) in labeled_counts.into_iter() {
+ metric.get(&label).add(count);
+ }
+ }
+ } else {
+ for (label, count) in labeled_counts.into_iter() {
+ __glean_metric_maps::labeled_counter_get(id.0, &label).add(count);
+ }
+ }
+ }
+ for (id, samples) in ipc_payload.memory_samples.into_iter() {
+ if id.0 & (1 << crate::factory::DYNAMIC_METRIC_BIT) > 0 {
+ let map = crate::factory::__jog_metric_maps::MEMORY_DISTRIBUTION_MAP
+ .read()
+ .expect("Read lock for dynamic memory dist map was poisoned");
+ if let Some(metric) = map.get(&id) {
+ samples
+ .into_iter()
+ .for_each(|sample| metric.accumulate(sample));
+ }
+ } else if let Some(metric) = __glean_metric_maps::MEMORY_DISTRIBUTION_MAP.get(&id) {
+ samples
+ .into_iter()
+ .for_each(|sample| metric.accumulate(sample));
+ }
+ }
+ for (id, value) in ipc_payload.numerators.into_iter() {
+ if id.0 & (1 << crate::factory::DYNAMIC_METRIC_BIT) > 0 {
+ let map = crate::factory::__jog_metric_maps::NUMERATOR_MAP
+ .read()
+ .expect("Read lock for dynamic numerator map was poisoned");
+ if let Some(metric) = map.get(&id) {
+ metric.add_to_numerator(value);
+ }
+ } else if let Some(metric) = __glean_metric_maps::NUMERATOR_MAP.get(&id) {
+ metric.add_to_numerator(value);
+ }
+ }
+ for (id, (n, d)) in ipc_payload.rates.into_iter() {
+ if id.0 & (1 << crate::factory::DYNAMIC_METRIC_BIT) > 0 {
+ let map = crate::factory::__jog_metric_maps::RATE_MAP
+ .read()
+ .expect("Read lock for dynamic rate map was poisoned");
+ if let Some(metric) = map.get(&id) {
+ metric.add_to_numerator(n);
+ metric.add_to_denominator(d);
+ }
+ } else if let Some(metric) = __glean_metric_maps::RATE_MAP.get(&id) {
+ metric.add_to_numerator(n);
+ metric.add_to_denominator(d);
+ }
+ }
+ for (id, strings) in ipc_payload.string_lists.into_iter() {
+ if id.0 & (1 << crate::factory::DYNAMIC_METRIC_BIT) > 0 {
+ let map = crate::factory::__jog_metric_maps::STRING_LIST_MAP
+ .read()
+ .expect("Read lock for dynamic string list map was poisoned");
+ if let Some(metric) = map.get(&id) {
+ strings.iter().for_each(|s| metric.add(s));
+ }
+ } else if let Some(metric) = __glean_metric_maps::STRING_LIST_MAP.get(&id) {
+ strings.iter().for_each(|s| metric.add(s));
+ }
+ }
+ for (id, samples) in ipc_payload.timing_samples.into_iter() {
+ if id.0 & (1 << crate::factory::DYNAMIC_METRIC_BIT) > 0 {
+ let map = crate::factory::__jog_metric_maps::TIMING_DISTRIBUTION_MAP
+ .read()
+ .expect("Read lock for dynamic timing distribution map was poisoned");
+ if let Some(metric) = map.get(&id) {
+ metric.accumulate_raw_samples_nanos(samples);
+ }
+ } else if let Some(metric) = __glean_metric_maps::TIMING_DISTRIBUTION_MAP.get(&id) {
+ metric.accumulate_raw_samples_nanos(samples);
+ }
+ }
+ Ok(())
+}
diff --git a/toolkit/components/glean/api/src/lib.rs b/toolkit/components/glean/api/src/lib.rs
new file mode 100644
index 0000000000..2f51b94917
--- /dev/null
+++ b/toolkit/components/glean/api/src/lib.rs
@@ -0,0 +1,27 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
+
+//! The public FOG APIs, for Rust consumers.
+
+// Re-exporting for later use in generated code.
+pub extern crate chrono;
+pub extern crate once_cell;
+pub extern crate uuid;
+
+// Re-exporting for use in user tests.
+pub use private::{DistributionData, ErrorType, RecordedEvent};
+
+// Must appear before `metrics` or its use of `ffi`'s macros will fail.
+#[macro_use]
+mod ffi;
+
+pub mod factory;
+pub mod metrics;
+pub mod pings;
+pub mod private;
+
+pub mod ipc;
+
+#[cfg(test)]
+mod common_test;
diff --git a/toolkit/components/glean/api/src/metrics.rs b/toolkit/components/glean/api/src/metrics.rs
new file mode 100644
index 0000000000..bca886e110
--- /dev/null
+++ b/toolkit/components/glean/api/src/metrics.rs
@@ -0,0 +1,20 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
+
+//! This file contains the Generated Glean Metrics API
+//!
+//! The contents of this module are generated by
+//! `toolkit/components/glean/build_scripts/glean_parser_ext/run_glean_parser.py`, from
+//! metrics files identified in 'toolkit/components/glean/metrics_index.py`.
+
+include!(mozbuild::objdir_path!(
+ "toolkit/components/glean/api/src/metrics.rs"
+));
+
+use crate::private::{EventMetric, ExtraKeys};
+
+/// Helper to get the number of allowed extra keys for a given event metric.
+fn extra_keys_len<K: ExtraKeys>(_event: &EventMetric<K>) -> usize {
+ K::ALLOWED_KEYS.len()
+}
diff --git a/toolkit/components/glean/api/src/pings.rs b/toolkit/components/glean/api/src/pings.rs
new file mode 100644
index 0000000000..f1d0332695
--- /dev/null
+++ b/toolkit/components/glean/api/src/pings.rs
@@ -0,0 +1,13 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
+
+//! This file contains the Generated Glean Metrics API (Ping portion)
+//!
+//! The contents of this module are generated by
+//! `toolkit/components/glean/build_scripts/glean_parser_ext/run_glean_parser.py`, from
+//! 'toolkit/components/glean/pings.yaml`.
+
+include!(mozbuild::objdir_path!(
+ "toolkit/components/glean/api/src/pings.rs"
+));
diff --git a/toolkit/components/glean/api/src/private/boolean.rs b/toolkit/components/glean/api/src/private/boolean.rs
new file mode 100644
index 0000000000..ea9ba6b6fe
--- /dev/null
+++ b/toolkit/components/glean/api/src/private/boolean.rs
@@ -0,0 +1,155 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
+
+use inherent::inherent;
+use std::sync::Arc;
+
+use glean::traits::Boolean;
+
+use super::CommonMetricData;
+
+use crate::ipc::need_ipc;
+use crate::private::MetricId;
+
+/// A boolean metric.
+///
+/// Records a simple true or false value.
+#[derive(Clone)]
+pub enum BooleanMetric {
+ Parent(Arc<glean::private::BooleanMetric>),
+ Child(BooleanMetricIpc),
+}
+#[derive(Clone, Debug)]
+pub struct BooleanMetricIpc;
+
+impl BooleanMetric {
+ /// Create a new boolean metric.
+ pub fn new(_id: MetricId, meta: CommonMetricData) -> Self {
+ if need_ipc() {
+ BooleanMetric::Child(BooleanMetricIpc)
+ } else {
+ BooleanMetric::Parent(Arc::new(glean::private::BooleanMetric::new(meta)))
+ }
+ }
+
+ #[cfg(test)]
+ pub(crate) fn child_metric(&self) -> Self {
+ match self {
+ BooleanMetric::Parent(_) => BooleanMetric::Child(BooleanMetricIpc),
+ BooleanMetric::Child(_) => panic!("Can't get a child metric from a child metric"),
+ }
+ }
+}
+
+#[inherent]
+impl Boolean for BooleanMetric {
+ /// Set to the specified boolean value.
+ ///
+ /// ## Arguments
+ ///
+ /// * `value` - the value to set.
+ pub fn set(&self, value: bool) {
+ match self {
+ BooleanMetric::Parent(p) => {
+ p.set(value);
+ }
+ BooleanMetric::Child(_) => {
+ log::error!("Unable to set boolean metric in non-main process. This operation will be ignored.");
+ // If we're in automation we can panic so the instrumentor knows they've gone wrong.
+ // This is a deliberate violation of Glean's "metric APIs must not throw" design.
+ assert!(!crate::ipc::is_in_automation(), "Attempted to set boolean metric in non-main process, which is forbidden. This panics in automation.");
+ // TODO: Record an error.
+ }
+ }
+ }
+
+ /// **Test-only API.**
+ ///
+ /// Get the currently stored value as a boolean.
+ /// This doesn't clear the stored value.
+ ///
+ /// ## Arguments
+ ///
+ /// * `ping_name` - the storage name to look into.
+ ///
+ /// ## Return value
+ ///
+ /// Returns the stored value or `None` if nothing stored.
+ pub fn test_get_value<'a, S: Into<Option<&'a str>>>(&self, ping_name: S) -> Option<bool> {
+ let ping_name = ping_name.into().map(|s| s.to_string());
+ match self {
+ BooleanMetric::Parent(p) => p.test_get_value(ping_name),
+ BooleanMetric::Child(_) => {
+ panic!("Cannot get test value for boolean metric in non-main process!",)
+ }
+ }
+ }
+
+ /// **Exported for test purposes.**
+ ///
+ /// Gets the number of recorded errors for the given metric and error type.
+ ///
+ /// # Arguments
+ ///
+ /// * `error` - The type of error
+ /// * `ping_name` - represents the optional name of the ping to retrieve the
+ /// metric for. Defaults to the first value in `send_in_pings`.
+ ///
+ /// # Returns
+ ///
+ /// The number of errors reported.
+ pub fn test_get_num_recorded_errors(&self, error: glean::ErrorType) -> i32 {
+ match self {
+ BooleanMetric::Parent(p) => p.test_get_num_recorded_errors(error),
+ BooleanMetric::Child(_) => panic!(
+ "Cannot get the number of recorded errors for boolean metric in non-main process!"
+ ),
+ }
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use crate::{common_test::*, ipc, metrics};
+
+ #[test]
+ fn sets_boolean_value() {
+ let _lock = lock_test();
+
+ let metric = &metrics::test_only_ipc::a_bool;
+ metric.set(true);
+
+ assert!(metric.test_get_value("store1").unwrap());
+ }
+
+ #[test]
+ fn boolean_ipc() {
+ // BooleanMetric doesn't support IPC.
+ let _lock = lock_test();
+
+ let parent_metric = &metrics::test_only_ipc::a_bool;
+
+ parent_metric.set(false);
+
+ {
+ let child_metric = parent_metric.child_metric();
+
+ // scope for need_ipc RAII
+ let _raii = ipc::test_set_need_ipc(true);
+
+ // Instrumentation calls do not panic.
+ child_metric.set(true);
+
+ // (They also shouldn't do anything,
+ // but that's not something we can inspect in this test)
+ }
+
+ assert!(ipc::replay_from_buf(&ipc::take_buf().unwrap()).is_ok());
+
+ assert!(
+ false == parent_metric.test_get_value("store1").unwrap(),
+ "Boolean metrics should only work in the parent process"
+ );
+ }
+}
diff --git a/toolkit/components/glean/api/src/private/counter.rs b/toolkit/components/glean/api/src/private/counter.rs
new file mode 100644
index 0000000000..67ad3204d6
--- /dev/null
+++ b/toolkit/components/glean/api/src/private/counter.rs
@@ -0,0 +1,204 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
+
+use inherent::inherent;
+use std::sync::Arc;
+
+use glean::traits::Counter;
+
+use super::{CommonMetricData, MetricId};
+use crate::ipc::{need_ipc, with_ipc_payload};
+
+/// A counter metric.
+///
+/// Used to count things.
+/// The value can only be incremented, not decremented.
+#[derive(Clone)]
+pub enum CounterMetric {
+ Parent {
+ /// The metric's ID.
+ ///
+ /// **TEST-ONLY** - Do not use unless gated with `#[cfg(test)]`.
+ id: MetricId,
+ inner: Arc<glean::private::CounterMetric>,
+ },
+ Child(CounterMetricIpc),
+}
+#[derive(Clone, Debug)]
+pub struct CounterMetricIpc(MetricId);
+
+impl CounterMetric {
+ /// Create a new counter metric.
+ pub fn new(id: MetricId, meta: CommonMetricData) -> Self {
+ if need_ipc() {
+ CounterMetric::Child(CounterMetricIpc(id))
+ } else {
+ let inner = Arc::new(glean::private::CounterMetric::new(meta));
+ CounterMetric::Parent { id, inner }
+ }
+ }
+
+ /// Special-purpose ctor for use by codegen.
+ /// Only useful if the metric is:
+ /// * not disabled
+ /// * lifetime: ping
+ /// * and is sent in precisely one ping.
+ pub fn codegen_new(id: u32, category: &str, name: &str, ping: &str) -> Self {
+ if need_ipc() {
+ CounterMetric::Child(CounterMetricIpc(id.into()))
+ } else {
+ let inner = Arc::new(glean::private::CounterMetric::new(CommonMetricData {
+ category: category.into(),
+ name: name.into(),
+ send_in_pings: vec![ping.into()],
+ ..Default::default()
+ }));
+ CounterMetric::Parent {
+ id: id.into(),
+ inner,
+ }
+ }
+ }
+
+ #[cfg(test)]
+ pub(crate) fn metric_id(&self) -> MetricId {
+ match self {
+ CounterMetric::Parent { id, .. } => *id,
+ CounterMetric::Child(c) => c.0,
+ }
+ }
+
+ #[cfg(test)]
+ pub(crate) fn child_metric(&self) -> Self {
+ match self {
+ CounterMetric::Parent { id, .. } => CounterMetric::Child(CounterMetricIpc(*id)),
+ CounterMetric::Child(_) => panic!("Can't get a child metric from a child metric"),
+ }
+ }
+}
+
+#[inherent]
+impl Counter for CounterMetric {
+ /// Increase the counter by `amount`.
+ ///
+ /// ## Arguments
+ ///
+ /// * `amount` - The amount to increase by. Should be positive.
+ ///
+ /// ## Notes
+ ///
+ /// Logs an error if the `amount` is 0 or negative.
+ pub fn add(&self, amount: i32) {
+ match self {
+ CounterMetric::Parent { inner, .. } => {
+ inner.add(amount);
+ }
+ CounterMetric::Child(c) => {
+ with_ipc_payload(move |payload| {
+ if let Some(v) = payload.counters.get_mut(&c.0) {
+ *v += amount;
+ } else {
+ payload.counters.insert(c.0, amount);
+ }
+ });
+ }
+ }
+ }
+
+ /// **Test-only API.**
+ ///
+ /// Get the currently stored value as an integer.
+ /// This doesn't clear the stored value.
+ ///
+ /// ## Arguments
+ ///
+ /// * `ping_name` - the storage name to look into.
+ ///
+ /// ## Return value
+ ///
+ /// Returns the stored value or `None` if nothing stored.
+ pub fn test_get_value<'a, S: Into<Option<&'a str>>>(&self, ping_name: S) -> Option<i32> {
+ let ping_name = ping_name.into().map(|s| s.to_string());
+ match self {
+ CounterMetric::Parent { inner, .. } => inner.test_get_value(ping_name),
+ CounterMetric::Child(c) => {
+ panic!("Cannot get test value for {:?} in non-parent process!", c.0)
+ }
+ }
+ }
+
+ /// **Test-only API.**
+ ///
+ /// Gets the number of recorded errors for the given metric and error type.
+ ///
+ /// # Arguments
+ ///
+ /// * `error` - The type of error
+ /// * `ping_name` - represents the optional name of the ping to retrieve the
+ /// metric for. Defaults to the first value in `send_in_pings`.
+ ///
+ /// # Returns
+ ///
+ /// The number of errors reported.
+ pub fn test_get_num_recorded_errors(&self, error: glean::ErrorType) -> i32 {
+ match self {
+ CounterMetric::Parent { inner, .. } => inner.test_get_num_recorded_errors(error),
+ CounterMetric::Child(c) => panic!(
+ "Cannot get the number of recorded errors for {:?} in non-parent process!",
+ c.0
+ ),
+ }
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use crate::{common_test::*, ipc, metrics};
+
+ #[test]
+ fn sets_counter_value_parent() {
+ let _lock = lock_test();
+
+ let metric = &metrics::test_only_ipc::a_counter;
+ metric.add(1);
+
+ assert_eq!(1, metric.test_get_value("store1").unwrap());
+ }
+
+ #[test]
+ fn sets_counter_value_child() {
+ let _lock = lock_test();
+
+ let parent_metric = &metrics::test_only_ipc::a_counter;
+ parent_metric.add(3);
+
+ {
+ let child_metric = parent_metric.child_metric();
+
+ // scope for need_ipc RAII
+ let _raii = ipc::test_set_need_ipc(true);
+ let metric_id = child_metric.metric_id();
+
+ child_metric.add(42);
+
+ ipc::with_ipc_payload(move |payload| {
+ assert!(
+ 42 == *payload.counters.get(&metric_id).unwrap(),
+ "Stored the correct value in the ipc payload"
+ );
+ });
+ }
+
+ assert!(
+ false == ipc::need_ipc(),
+ "RAII dropped, should not need ipc any more"
+ );
+ assert!(ipc::replay_from_buf(&ipc::take_buf().unwrap()).is_ok());
+
+ assert!(
+ 45 == parent_metric.test_get_value("store1").unwrap(),
+ "Values from the 'processes' should be summed"
+ );
+ }
+}
diff --git a/toolkit/components/glean/api/src/private/custom_distribution.rs b/toolkit/components/glean/api/src/private/custom_distribution.rs
new file mode 100644
index 0000000000..2114430898
--- /dev/null
+++ b/toolkit/components/glean/api/src/private/custom_distribution.rs
@@ -0,0 +1,171 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
+
+use inherent::inherent;
+
+use super::{CommonMetricData, MetricId};
+use glean::{DistributionData, ErrorType, HistogramType};
+
+use crate::ipc::{need_ipc, with_ipc_payload};
+use glean::traits::CustomDistribution;
+
+/// A custom distribution metric.
+///
+/// Custom distributions are used to record the distribution of arbitrary values.
+pub enum CustomDistributionMetric {
+ Parent {
+ /// The metric's ID.
+ ///
+ /// **TEST-ONLY** - Do not use unless gated with `#[cfg(test)]`.
+ id: MetricId,
+ inner: glean::private::CustomDistributionMetric,
+ },
+ Child(CustomDistributionMetricIpc),
+}
+#[derive(Debug)]
+pub struct CustomDistributionMetricIpc(MetricId);
+
+impl CustomDistributionMetric {
+ /// Create a new timing distribution metric.
+ pub fn new(
+ id: MetricId,
+ meta: CommonMetricData,
+ range_min: u64,
+ range_max: u64,
+ bucket_count: u64,
+ histogram_type: HistogramType,
+ ) -> Self {
+ if need_ipc() {
+ CustomDistributionMetric::Child(CustomDistributionMetricIpc(id))
+ } else {
+ debug_assert!(
+ range_min <= i64::MAX as u64,
+ "sensible limits enforced by glean_parser"
+ );
+ debug_assert!(
+ range_max <= i64::MAX as u64,
+ "sensible limits enforced by glean_parser"
+ );
+ debug_assert!(
+ bucket_count <= i64::MAX as u64,
+ "sensible limits enforced by glean_parser"
+ );
+ let inner = glean::private::CustomDistributionMetric::new(
+ meta,
+ range_min as i64,
+ range_max as i64,
+ bucket_count as i64,
+ histogram_type,
+ );
+ CustomDistributionMetric::Parent { id, inner }
+ }
+ }
+
+ #[cfg(test)]
+ pub(crate) fn child_metric(&self) -> Self {
+ match self {
+ CustomDistributionMetric::Parent { id, .. } => {
+ CustomDistributionMetric::Child(CustomDistributionMetricIpc(*id))
+ }
+ CustomDistributionMetric::Child(_) => {
+ panic!("Can't get a child metric from a child metric")
+ }
+ }
+ }
+}
+
+#[inherent]
+impl CustomDistribution for CustomDistributionMetric {
+ pub fn accumulate_samples_signed(&self, samples: Vec<i64>) {
+ match self {
+ CustomDistributionMetric::Parent { inner, .. } => inner.accumulate_samples(samples),
+ CustomDistributionMetric::Child(c) => {
+ with_ipc_payload(move |payload| {
+ if let Some(v) = payload.custom_samples.get_mut(&c.0) {
+ v.extend(samples);
+ } else {
+ payload.custom_samples.insert(c.0, samples);
+ }
+ });
+ }
+ }
+ }
+
+ pub fn test_get_value<'a, S: Into<Option<&'a str>>>(
+ &self,
+ ping_name: S,
+ ) -> Option<DistributionData> {
+ let ping_name = ping_name.into().map(|s| s.to_string());
+ match self {
+ CustomDistributionMetric::Parent { inner, .. } => inner.test_get_value(ping_name),
+ CustomDistributionMetric::Child(c) => {
+ panic!("Cannot get test value for {:?} in non-parent process!", c)
+ }
+ }
+ }
+
+ pub fn test_get_num_recorded_errors(&self, error: ErrorType) -> i32 {
+ match self {
+ CustomDistributionMetric::Parent { inner, .. } => {
+ inner.test_get_num_recorded_errors(error)
+ }
+ CustomDistributionMetric::Child(c) => panic!(
+ "Cannot get number of recorded errors for {:?} in non-parent process!",
+ c
+ ),
+ }
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use crate::{common_test::*, ipc, metrics};
+
+ #[test]
+ fn smoke_test_custom_distribution() {
+ let _lock = lock_test();
+
+ let metric = &metrics::test_only_ipc::a_custom_dist;
+
+ metric.accumulate_samples_signed(vec![1, 2, 3]);
+
+ assert!(metric.test_get_value("store1").is_some());
+ }
+
+ #[test]
+ fn custom_distribution_child() {
+ let _lock = lock_test();
+
+ let parent_metric = &metrics::test_only_ipc::a_custom_dist;
+ parent_metric.accumulate_samples_signed(vec![1, 268435458]);
+
+ {
+ let child_metric = parent_metric.child_metric();
+
+ // scope for need_ipc RAII
+ let _raii = ipc::test_set_need_ipc(true);
+
+ child_metric.accumulate_samples_signed(vec![4, 268435460]);
+ }
+
+ let buf = ipc::take_buf().unwrap();
+ assert!(buf.len() > 0);
+ assert!(ipc::replay_from_buf(&buf).is_ok());
+
+ let data = parent_metric
+ .test_get_value("store1")
+ .expect("should have some data");
+
+ assert_eq!(2, data.values[&1], "Low bucket has 2 values");
+ assert_eq!(
+ 2, data.values[&268435456],
+ "Next higher bucket has 2 values"
+ );
+ assert_eq!(
+ 1 + 4 + 268435458 + 268435460,
+ data.sum,
+ "Sum of all recorded values"
+ );
+ }
+}
diff --git a/toolkit/components/glean/api/src/private/datetime.rs b/toolkit/components/glean/api/src/private/datetime.rs
new file mode 100644
index 0000000000..f95b3c33fb
--- /dev/null
+++ b/toolkit/components/glean/api/src/private/datetime.rs
@@ -0,0 +1,246 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
+
+use inherent::inherent;
+
+use super::{CommonMetricData, MetricId};
+
+use super::TimeUnit;
+use crate::ipc::need_ipc;
+use chrono::{FixedOffset, TimeZone};
+use glean::traits::Datetime;
+
+/// A datetime metric of a certain resolution.
+///
+/// Datetimes are used to make record when something happened according to the
+/// client's clock.
+#[derive(Clone)]
+pub enum DatetimeMetric {
+ Parent(glean::private::DatetimeMetric),
+ Child(DatetimeMetricIpc),
+}
+#[derive(Debug, Clone)]
+pub struct DatetimeMetricIpc;
+
+impl DatetimeMetric {
+ /// Create a new datetime metric.
+ pub fn new(_id: MetricId, meta: CommonMetricData, time_unit: TimeUnit) -> Self {
+ if need_ipc() {
+ DatetimeMetric::Child(DatetimeMetricIpc)
+ } else {
+ DatetimeMetric::Parent(glean::private::DatetimeMetric::new(meta, time_unit))
+ }
+ }
+
+ #[cfg(test)]
+ pub(crate) fn child_metric(&self) -> Self {
+ match self {
+ DatetimeMetric::Parent { .. } => DatetimeMetric::Child(DatetimeMetricIpc),
+ DatetimeMetric::Child(_) => panic!("Can't get a child metric from a child metric"),
+ }
+ }
+
+ /// Sets the metric to a date/time including the timezone offset.
+ ///
+ /// # Arguments
+ ///
+ /// * `year` - the year to set the metric to.
+ /// * `month` - the month to set the metric to (1-12).
+ /// * `day` - the day to set the metric to (1-based).
+ /// * `hour` - the hour to set the metric to (0-23).
+ /// * `minute` - the minute to set the metric to.
+ /// * `second` - the second to set the metric to.
+ /// * `nano` - the nanosecond fraction to the last whole second.
+ /// * `offset_seconds` - the timezone difference, in seconds, for the Eastern
+ /// Hemisphere. Negative seconds mean Western Hemisphere.
+ #[cfg_attr(not(feature = "with-gecko"), allow(dead_code))]
+ #[allow(clippy::too_many_arguments)]
+ pub(crate) fn set_with_details(
+ &self,
+ year: i32,
+ month: u32,
+ day: u32,
+ hour: u32,
+ minute: u32,
+ second: u32,
+ nano: u32,
+ offset_seconds: i32,
+ ) {
+ match self {
+ DatetimeMetric::Parent(p) => {
+ let tz = FixedOffset::east_opt(offset_seconds);
+ if tz.is_none() {
+ log::error!(
+ "Unable to set datetime metric with invalid offset seconds {}",
+ offset_seconds
+ );
+ // TODO: Record an error
+ return;
+ }
+
+ let value = FixedOffset::east(offset_seconds)
+ .ymd_opt(year, month, day)
+ .and_hms_nano_opt(hour, minute, second, nano);
+ match value.single() {
+ Some(d) => p.set(Some(d.into())),
+ _ => {
+ log::error!("Unable to construct datetime")
+ // TODO: Record an error
+ }
+ }
+ }
+ DatetimeMetric::Child(_) => {
+ log::error!("Unable to set datetime metric in non-main process. This operation will be ignored.");
+ // If we're in automation we can panic so the instrumentor knows they've gone wrong.
+ // This is a deliberate violation of Glean's "metric APIs must not throw" design.
+ assert!(!crate::ipc::is_in_automation(), "Attempted to set datetime in non-main process, which is forbidden. This panics in automation.");
+ // TODO: Record an error.
+ }
+ }
+ }
+}
+
+#[inherent]
+impl Datetime for DatetimeMetric {
+ /// Sets the metric to a date/time which including the timezone offset.
+ ///
+ /// ## Arguments
+ ///
+ /// - `value` - The date and time and timezone value to set.
+ /// If None we use the current local time.
+ pub fn set(&self, value: Option<glean::Datetime>) {
+ match self {
+ DatetimeMetric::Parent(p) => {
+ p.set(value);
+ }
+ DatetimeMetric::Child(_) => {
+ log::error!(
+ "Unable to set datetime metric DatetimeMetric in non-main process. This operation will be ignored."
+ );
+ // If we're in automation we can panic so the instrumentor knows they've gone wrong.
+ // This is a deliberate violation of Glean's "metric APIs must not throw" design.
+ assert!(!crate::ipc::is_in_automation(), "Attempted to set datetime metric in non-main process, which is forbidden. This panics in automation.");
+ // TODO: Record an error.
+ }
+ }
+ }
+
+ /// **Exported for test purposes.**
+ ///
+ /// Gets the currently stored value as a Datetime.
+ ///
+ /// The precision of this value is truncated to the `time_unit` precision.
+ ///
+ /// This doesn't clear the stored value.
+ ///
+ /// # Arguments
+ ///
+ /// * `ping_name` - represents the optional name of the ping to retrieve the
+ /// metric for. Defaults to the first value in `send_in_pings`.
+ pub fn test_get_value<'a, S: Into<Option<&'a str>>>(
+ &self,
+ ping_name: S,
+ ) -> Option<glean::Datetime> {
+ let ping_name = ping_name.into().map(|s| s.to_string());
+ match self {
+ DatetimeMetric::Parent(p) => p.test_get_value(ping_name),
+ DatetimeMetric::Child(_) => {
+ panic!("Cannot get test value for DatetimeMetric in non-main process!")
+ }
+ }
+ }
+
+ /// **Exported for test purposes.**
+ ///
+ /// Gets the number of recorded errors for the given metric and error type.
+ ///
+ /// # Arguments
+ ///
+ /// * `error` - The type of error
+ /// * `ping_name` - represents the optional name of the ping to retrieve the
+ /// metric for. Defaults to the first value in `send_in_pings`.
+ ///
+ /// # Returns
+ ///
+ /// The number of errors reported.
+ pub fn test_get_num_recorded_errors(&self, error: glean::ErrorType) -> i32 {
+ match self {
+ DatetimeMetric::Parent(p) => p.test_get_num_recorded_errors(error),
+ DatetimeMetric::Child(_) => panic!(
+ "Cannot get the number of recorded errors for DatetimeMetric in non-main process!"
+ ),
+ }
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use chrono::{DateTime, FixedOffset, TimeZone};
+
+ use crate::{common_test::*, ipc, metrics};
+
+ #[test]
+ fn sets_datetime_value() {
+ let _lock = lock_test();
+
+ let metric = &metrics::test_only_ipc::a_date;
+
+ let a_datetime = FixedOffset::east(5 * 3600)
+ .ymd(2020, 05, 07)
+ .and_hms(11, 58, 00);
+ metric.set(Some(a_datetime.into()));
+
+ let expected: glean::Datetime = DateTime::parse_from_rfc3339("2020-05-07T11:58:00+05:00")
+ .unwrap()
+ .into();
+ assert_eq!(expected, metric.test_get_value("store1").unwrap());
+ }
+
+ #[test]
+ fn sets_datetime_value_with_details() {
+ let _lock = lock_test();
+
+ let metric = &metrics::test_only_ipc::a_date;
+
+ metric.set_with_details(2020, 05, 07, 11, 58, 0, 0, 5 * 3600);
+
+ let expected: glean::Datetime = DateTime::parse_from_rfc3339("2020-05-07T11:58:00+05:00")
+ .unwrap()
+ .into();
+ assert_eq!(expected, metric.test_get_value("store1").unwrap());
+ }
+
+ #[test]
+ fn datetime_ipc() {
+ // DatetimeMetric doesn't support IPC.
+ let _lock = lock_test();
+
+ let parent_metric = &metrics::test_only_ipc::a_date;
+
+ // Instrumentation calls do not panic.
+ let a_datetime = FixedOffset::east(5 * 3600)
+ .ymd(2020, 10, 13)
+ .and_hms(16, 41, 00);
+ parent_metric.set(Some(a_datetime.into()));
+
+ {
+ let child_metric = parent_metric.child_metric();
+
+ let _raii = ipc::test_set_need_ipc(true);
+
+ let a_datetime = FixedOffset::east(0).ymd(2018, 4, 7).and_hms(12, 01, 00);
+ child_metric.set(Some(a_datetime.into()));
+
+ // (They also shouldn't do anything,
+ // but that's not something we can inspect in this test)
+ }
+
+ assert!(ipc::replay_from_buf(&ipc::take_buf().unwrap()).is_ok());
+
+ let expected: glean::Datetime = DateTime::parse_from_rfc3339("2020-10-13T16:41:00+05:00")
+ .unwrap()
+ .into();
+ assert_eq!(expected, parent_metric.test_get_value("store1").unwrap());
+ }
+}
diff --git a/toolkit/components/glean/api/src/private/denominator.rs b/toolkit/components/glean/api/src/private/denominator.rs
new file mode 100644
index 0000000000..64982b5050
--- /dev/null
+++ b/toolkit/components/glean/api/src/private/denominator.rs
@@ -0,0 +1,157 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
+
+use inherent::inherent;
+
+use super::CommonMetricData;
+
+use glean::traits::Counter;
+
+use crate::ipc::{need_ipc, with_ipc_payload};
+use crate::private::MetricId;
+
+/// Developer-facing API for recording counter metrics that are acting as
+/// external denominators for rate metrics.
+///
+/// Instances of this class type are automatically generated by the parsers
+/// at build time, allowing developers to record values that were previously
+/// registered in the metrics.yaml file.
+#[derive(Clone)]
+pub enum DenominatorMetric {
+ Parent {
+ /// The metric's ID.
+ ///
+ /// **TEST-ONLY** - Do not use unless gated with `#[cfg(test)]`.
+ id: MetricId,
+ inner: glean::private::DenominatorMetric,
+ },
+ Child(DenominatorMetricIpc),
+}
+#[derive(Clone, Debug)]
+pub struct DenominatorMetricIpc(MetricId);
+
+impl DenominatorMetric {
+ /// The constructor used by automatically generated metrics.
+ pub fn new(id: MetricId, meta: CommonMetricData, numerators: Vec<CommonMetricData>) -> Self {
+ if need_ipc() {
+ DenominatorMetric::Child(DenominatorMetricIpc(id))
+ } else {
+ let inner = glean::private::DenominatorMetric::new(meta, numerators);
+ DenominatorMetric::Parent { id, inner }
+ }
+ }
+
+ #[cfg(test)]
+ pub(crate) fn metric_id(&self) -> MetricId {
+ match self {
+ DenominatorMetric::Parent { id, .. } => *id,
+ DenominatorMetric::Child(c) => c.0,
+ }
+ }
+
+ #[cfg(test)]
+ pub(crate) fn child_metric(&self) -> Self {
+ match self {
+ DenominatorMetric::Parent { id, .. } => {
+ DenominatorMetric::Child(DenominatorMetricIpc(*id))
+ }
+ DenominatorMetric::Child(_) => panic!("Can't get a child metric from a child metric"),
+ }
+ }
+}
+
+#[inherent]
+impl Counter for DenominatorMetric {
+ pub fn add(&self, amount: i32) {
+ match self {
+ DenominatorMetric::Parent { inner, .. } => {
+ inner.add(amount);
+ }
+ DenominatorMetric::Child(c) => {
+ with_ipc_payload(move |payload| {
+ if let Some(v) = payload.denominators.get_mut(&c.0) {
+ *v += amount;
+ } else {
+ payload.denominators.insert(c.0, amount);
+ }
+ });
+ }
+ }
+ }
+
+ pub fn test_get_value<'a, S: Into<Option<&'a str>>>(&self, ping_name: S) -> Option<i32> {
+ let ping_name = ping_name.into().map(|s| s.to_string());
+ match self {
+ DenominatorMetric::Parent { inner, .. } => inner.test_get_value(ping_name),
+ DenominatorMetric::Child(c) => {
+ panic!("Cannot get test value for {:?} in non-parent process!", c.0);
+ }
+ }
+ }
+
+ pub fn test_get_num_recorded_errors(&self, error: glean::ErrorType) -> i32 {
+ match self {
+ DenominatorMetric::Parent { inner, .. } => inner.test_get_num_recorded_errors(error),
+ DenominatorMetric::Child(c) => {
+ panic!(
+ "Cannot get the number of recorded errors for {:?} in non-parent process!",
+ c.0
+ );
+ }
+ }
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use crate::{common_test::*, ipc, metrics};
+
+ #[test]
+ fn sets_denominator_value_parent() {
+ let _lock = lock_test();
+
+ let metric = &metrics::test_only_ipc::an_external_denominator;
+ metric.add(1);
+
+ assert_eq!(1, metric.test_get_value("store1").unwrap());
+ }
+
+ #[test]
+ fn sets_denominator_value_child() {
+ let _lock = lock_test();
+
+ let parent_metric = &metrics::test_only_ipc::an_external_denominator;
+ parent_metric.add(3);
+
+ {
+ // scope for need_ipc RAII
+ let child_metric = parent_metric.child_metric();
+ let _raii = ipc::test_set_need_ipc(true);
+ let metric_id = child_metric.metric_id();
+
+ child_metric.add(42);
+
+ ipc::with_ipc_payload(move |payload| {
+ assert_eq!(
+ 42,
+ *payload.denominators.get(&metric_id).unwrap(),
+ "Stored the correct value in the ipc payload"
+ );
+ });
+ }
+
+ assert_eq!(
+ false,
+ ipc::need_ipc(),
+ "RAII dropped, should not need ipc any more"
+ );
+ assert!(ipc::replay_from_buf(&ipc::take_buf().unwrap()).is_ok());
+
+ assert_eq!(
+ 45,
+ parent_metric.test_get_value("store1").unwrap(),
+ "Values from the 'processes' should be summed"
+ );
+ }
+}
diff --git a/toolkit/components/glean/api/src/private/event.rs b/toolkit/components/glean/api/src/private/event.rs
new file mode 100644
index 0000000000..11f8213bef
--- /dev/null
+++ b/toolkit/components/glean/api/src/private/event.rs
@@ -0,0 +1,241 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
+
+use std::collections::HashMap;
+
+use inherent::inherent;
+
+use super::{CommonMetricData, MetricId, RecordedEvent};
+
+use crate::ipc::{need_ipc, with_ipc_payload};
+
+use glean::traits::Event;
+pub use glean::traits::{EventRecordingError, ExtraKeys, NoExtraKeys};
+
+/// An event metric.
+///
+/// Events allow recording of e.g. individual occurences of user actions, say
+/// every time a view was open and from where. Each time you record an event, it
+/// records a timestamp, the event's name and a set of custom values.
+pub enum EventMetric<K> {
+ Parent {
+ /// The metric's ID.
+ ///
+ /// **TEST-ONLY** - Do not use unless gated with `#[cfg(test)]`.
+ id: MetricId,
+ inner: glean::private::EventMetric<K>,
+ },
+ Child(EventMetricIpc),
+}
+
+#[derive(Debug)]
+pub struct EventMetricIpc(MetricId);
+
+impl<K: 'static + ExtraKeys + Send + Sync> EventMetric<K> {
+ /// Create a new event metric.
+ pub fn new(id: MetricId, meta: CommonMetricData) -> Self {
+ if need_ipc() {
+ EventMetric::Child(EventMetricIpc(id))
+ } else {
+ let inner = glean::private::EventMetric::new(meta);
+ EventMetric::Parent { id, inner }
+ }
+ }
+
+ pub fn with_runtime_extra_keys(
+ id: MetricId,
+ meta: CommonMetricData,
+ allowed_extra_keys: Vec<String>,
+ ) -> Self {
+ if need_ipc() {
+ EventMetric::Child(EventMetricIpc(id))
+ } else {
+ let inner =
+ glean::private::EventMetric::with_runtime_extra_keys(meta, allowed_extra_keys);
+ EventMetric::Parent { id, inner }
+ }
+ }
+
+ #[cfg(test)]
+ pub(crate) fn child_metric(&self) -> Self {
+ match self {
+ EventMetric::Parent { id, .. } => EventMetric::Child(EventMetricIpc(*id)),
+ EventMetric::Child(_) => panic!("Can't get a child metric from a child metric"),
+ }
+ }
+
+ /// Record a new event with the raw `extra key ID -> String` map.
+ ///
+ /// Should only be used when taking in data over FFI, where extra keys only exists as IDs.
+ pub(crate) fn record_raw(&self, extra: HashMap<String, String>) {
+ let now = glean::get_timestamp_ms();
+ self.record_with_time(now, extra);
+ }
+
+ /// Record a new event with the given timestamp and the raw `extra key ID -> String` map.
+ ///
+ /// Should only be used when applying previously recorded events, e.g. from IPC.
+ pub(crate) fn record_with_time(&self, timestamp: u64, extra: HashMap<String, String>) {
+ match self {
+ EventMetric::Parent { inner, .. } => {
+ inner.record_with_time(timestamp, extra);
+ }
+ EventMetric::Child(c) => {
+ with_ipc_payload(move |payload| {
+ if let Some(v) = payload.events.get_mut(&c.0) {
+ v.push((timestamp, extra));
+ } else {
+ let v = vec![(timestamp, extra)];
+ payload.events.insert(c.0, v);
+ }
+ });
+ }
+ }
+ }
+}
+
+#[inherent]
+impl<K: 'static + ExtraKeys + Send + Sync> Event for EventMetric<K> {
+ type Extra = K;
+
+ pub fn record<M: Into<Option<K>>>(&self, extra: M) {
+ match self {
+ EventMetric::Parent { inner, .. } => {
+ inner.record(extra);
+ }
+ EventMetric::Child(_) => {
+ let now = glean::get_timestamp_ms();
+ let extra = extra.into().map(|extra| extra.into_ffi_extra());
+ let extra = extra.unwrap_or_else(HashMap::new);
+ self.record_with_time(now, extra);
+ }
+ }
+ }
+
+ pub fn test_get_value<'a, S: Into<Option<&'a str>>>(
+ &self,
+ ping_name: S,
+ ) -> Option<Vec<RecordedEvent>> {
+ match self {
+ EventMetric::Parent { inner, .. } => inner.test_get_value(ping_name),
+ EventMetric::Child(_) => {
+ panic!("Cannot get test value for event metric in non-main process!",)
+ }
+ }
+ }
+
+ pub fn test_get_num_recorded_errors(&self, error: glean::ErrorType) -> i32 {
+ match self {
+ EventMetric::Parent { inner, .. } => inner.test_get_num_recorded_errors(error),
+ EventMetric::Child(c) => panic!(
+ "Cannot get the number of recorded errors for {:?} in non-main process!",
+ c.0
+ ),
+ }
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+ use crate::{common_test::*, ipc, metrics};
+
+ #[test]
+ fn smoke_test_event() {
+ let _lock = lock_test();
+
+ let metric = EventMetric::<NoExtraKeys>::new(
+ 0.into(),
+ CommonMetricData {
+ name: "event_metric".into(),
+ category: "telemetry".into(),
+ send_in_pings: vec!["store1".into()],
+ disabled: false,
+ ..Default::default()
+ },
+ );
+
+ // No extra keys
+ metric.record(None);
+
+ let recorded = metric.test_get_value("store1").unwrap();
+
+ assert!(recorded.iter().any(|e| e.name == "event_metric"));
+ }
+
+ #[test]
+ fn event_ipc() {
+ use metrics::test_only_ipc::AnEventExtra;
+
+ let _lock = lock_test();
+
+ let parent_metric = &metrics::test_only_ipc::an_event;
+
+ // No extra keys
+ parent_metric.record(None);
+
+ {
+ let child_metric = parent_metric.child_metric();
+
+ // scope for need_ipc RAII.
+ let _raii = ipc::test_set_need_ipc(true);
+
+ child_metric.record(None);
+
+ let extra = AnEventExtra {
+ extra1: Some("a-child-value".into()),
+ ..Default::default()
+ };
+ child_metric.record(extra);
+ }
+
+ // Record in the parent after the child.
+ let extra = AnEventExtra {
+ extra1: Some("a-valid-value".into()),
+ ..Default::default()
+ };
+ parent_metric.record(extra);
+
+ assert!(ipc::replay_from_buf(&ipc::take_buf().unwrap()).is_ok());
+
+ let events = parent_metric.test_get_value("store1").unwrap();
+ assert_eq!(events.len(), 4);
+
+ // Events from the child process are last, they might get sorted later by Glean.
+ assert_eq!(events[0].extra, None);
+ assert!(events[1].extra.as_ref().unwrap().get("extra1").unwrap() == "a-valid-value");
+ assert_eq!(events[2].extra, None);
+ assert!(events[3].extra.as_ref().unwrap().get("extra1").unwrap() == "a-child-value");
+ }
+
+ #[test]
+ fn events_with_typed_extras() {
+ use metrics::test_only_ipc::EventWithExtraExtra;
+ let _lock = lock_test();
+
+ let event = &metrics::test_only_ipc::event_with_extra;
+ // Record in the parent after the child.
+ let extra = EventWithExtraExtra {
+ extra1: Some("a-valid-value".into()),
+ extra2: Some(37),
+ extra3_longer_name: Some(false),
+ };
+ event.record(extra);
+
+ let recorded = event.test_get_value("store1").unwrap();
+
+ assert_eq!(recorded.len(), 1);
+ assert!(recorded[0].extra.as_ref().unwrap().get("extra1").unwrap() == "a-valid-value");
+ assert!(recorded[0].extra.as_ref().unwrap().get("extra2").unwrap() == "37");
+ assert!(
+ recorded[0]
+ .extra
+ .as_ref()
+ .unwrap()
+ .get("extra3_longer_name")
+ .unwrap()
+ == "false"
+ );
+ }
+}
diff --git a/toolkit/components/glean/api/src/private/labeled.rs b/toolkit/components/glean/api/src/private/labeled.rs
new file mode 100644
index 0000000000..60034ae5f1
--- /dev/null
+++ b/toolkit/components/glean/api/src/private/labeled.rs
@@ -0,0 +1,369 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
+
+use inherent::inherent;
+
+use super::{
+ CommonMetricData, ErrorType, LabeledBooleanMetric, LabeledCounterMetric, LabeledStringMetric,
+ MetricId,
+};
+use crate::ipc::need_ipc;
+use std::borrow::Cow;
+use std::marker::PhantomData;
+
+/// Sealed traits protect against downstream implementations.
+///
+/// We wrap it in a private module that is inaccessible outside of this module.
+mod private {
+ use super::{
+ need_ipc, LabeledBooleanMetric, LabeledCounterMetric, LabeledStringMetric, MetricId,
+ };
+ use crate::private::CounterMetric;
+ use std::sync::Arc;
+
+ /// The sealed trait.
+ ///
+ /// This allows us to define which FOG metrics can be used
+ /// as labeled types.
+ pub trait Sealed {
+ type GleanMetric: glean::private::AllowLabeled + Clone;
+ fn from_glean_metric(id: MetricId, metric: Arc<Self::GleanMetric>, label: &str) -> Self;
+ }
+
+ // `LabeledMetric<LabeledBooleanMetric>` is possible.
+ //
+ // See [Labeled Booleans](https://mozilla.github.io/glean/book/user/metrics/labeled_booleans.html).
+ impl Sealed for LabeledBooleanMetric {
+ type GleanMetric = glean::private::BooleanMetric;
+ fn from_glean_metric(_id: MetricId, metric: Arc<Self::GleanMetric>, _label: &str) -> Self {
+ if need_ipc() {
+ // TODO: Instrument this error.
+ LabeledBooleanMetric::Child(crate::private::boolean::BooleanMetricIpc)
+ } else {
+ LabeledBooleanMetric::Parent(metric)
+ }
+ }
+ }
+
+ // `LabeledMetric<LabeledStringMetric>` is possible.
+ //
+ // See [Labeled Strings](https://mozilla.github.io/glean/book/user/metrics/labeled_strings.html).
+ impl Sealed for LabeledStringMetric {
+ type GleanMetric = glean::private::StringMetric;
+ fn from_glean_metric(_id: MetricId, metric: Arc<Self::GleanMetric>, _label: &str) -> Self {
+ if need_ipc() {
+ // TODO: Instrument this error.
+ LabeledStringMetric::Child(crate::private::string::StringMetricIpc)
+ } else {
+ LabeledStringMetric::Parent(metric)
+ }
+ }
+ }
+
+ // `LabeledMetric<LabeledCounterMetric>` is possible.
+ //
+ // See [Labeled Counters](https://mozilla.github.io/glean/book/user/metrics/labeled_counters.html).
+ impl Sealed for LabeledCounterMetric {
+ type GleanMetric = glean::private::CounterMetric;
+ fn from_glean_metric(id: MetricId, metric: Arc<Self::GleanMetric>, label: &str) -> Self {
+ if need_ipc() {
+ LabeledCounterMetric::Child {
+ id,
+ label: label.to_string(),
+ }
+ } else {
+ LabeledCounterMetric::Parent(CounterMetric::Parent { id, inner: metric })
+ }
+ }
+ }
+}
+
+/// Marker trait for metrics that can be nested inside a labeled metric.
+///
+/// This trait is sealed and cannot be implemented for types outside this crate.
+pub trait AllowLabeled: private::Sealed {}
+
+// Implement the trait for everything we marked as allowed.
+impl<T> AllowLabeled for T where T: private::Sealed {}
+
+/// A labeled metric.
+///
+/// Labeled metrics allow to record multiple sub-metrics of the same type under different string labels.
+///
+/// ## Example
+///
+/// The following piece of code will be generated by `glean_parser`:
+///
+/// ```rust,ignore
+/// use glean::metrics::{LabeledMetric, BooleanMetric, CommonMetricData, Lifetime};
+/// use once_cell::sync::Lazy;
+///
+/// mod error {
+/// pub static seen_one: Lazy<LabeledMetric<BooleanMetric, DynamicLabel>> = Lazy::new(|| LabeledMetric::new(CommonMetricData {
+/// name: "seen_one".into(),
+/// category: "error".into(),
+/// send_in_pings: vec!["ping".into()],
+/// disabled: false,
+/// lifetime: Lifetime::Ping,
+/// ..Default::default()
+/// }, None));
+/// }
+/// ```
+///
+/// It can then be used with:
+///
+/// ```rust,ignore
+/// errro::seen_one.get("upload").set(true);
+/// ```
+pub struct LabeledMetric<T: AllowLabeled, E> {
+ /// The metric ID of the underlying metric.
+ id: MetricId,
+
+ /// Wrapping the underlying core metric.
+ ///
+ /// We delegate all functionality to this and wrap it up again in our own metric type.
+ core: glean::private::LabeledMetric<T::GleanMetric>,
+
+ label_enum: PhantomData<E>,
+}
+
+impl<T, E> LabeledMetric<T, E>
+where
+ T: AllowLabeled,
+{
+ /// Create a new labeled metric from the given metric instance and optional list of labels.
+ ///
+ /// See [`get`](#method.get) for information on how static or dynamic labels are handled.
+ pub fn new(
+ id: MetricId,
+ meta: CommonMetricData,
+ labels: Option<Vec<Cow<'static, str>>>,
+ ) -> LabeledMetric<T, E> {
+ let core = glean::private::LabeledMetric::new(meta, labels);
+ LabeledMetric {
+ id,
+ core,
+ label_enum: PhantomData,
+ }
+ }
+}
+
+#[inherent]
+impl<U, E> glean::traits::Labeled<U> for LabeledMetric<U, E>
+where
+ U: AllowLabeled + Clone,
+{
+ /// Gets a specific metric for a given label.
+ ///
+ /// If a set of acceptable labels were specified in the `metrics.yaml` file,
+ /// and the given label is not in the set, it will be recorded under the special `OTHER_LABEL` label.
+ ///
+ /// If a set of acceptable labels was not specified in the `metrics.yaml` file,
+ /// only the first 16 unique labels will be used.
+ /// After that, any additional labels will be recorded under the special `OTHER_LABEL` label.
+ ///
+ /// Labels must be `snake_case` and less than 30 characters.
+ /// If an invalid label is used, the metric will be recorded in the special `OTHER_LABEL` label.
+ pub fn get(&self, label: &str) -> U {
+ let metric = self.core.get(label);
+ U::from_glean_metric(self.id, metric, label)
+ }
+
+ /// **Exported for test purposes.**
+ ///
+ /// Gets the number of recorded errors for the given metric and error type.
+ ///
+ /// # Arguments
+ ///
+ /// * `error` - The type of error
+ /// * `ping_name` - represents the optional name of the ping to retrieve the
+ /// metric for. Defaults to the first value in `send_in_pings`.
+ ///
+ /// # Returns
+ ///
+ /// The number of errors reported.
+ pub fn test_get_num_recorded_errors(&self, error: ErrorType) -> i32 {
+ if need_ipc() {
+ panic!("Use of labeled metrics in IPC land not yet implemented!");
+ } else {
+ self.core.test_get_num_recorded_errors(error)
+ }
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use once_cell::sync::Lazy;
+
+ use super::*;
+ use crate::common_test::*;
+ use crate::metrics::DynamicLabel;
+
+ // Smoke test for what should be the generated code.
+ static GLOBAL_METRIC: Lazy<LabeledMetric<LabeledBooleanMetric, DynamicLabel>> =
+ Lazy::new(|| {
+ LabeledMetric::new(
+ 0.into(),
+ CommonMetricData {
+ name: "global".into(),
+ category: "metric".into(),
+ send_in_pings: vec!["ping".into()],
+ disabled: false,
+ ..Default::default()
+ },
+ None,
+ )
+ });
+
+ #[test]
+ fn smoke_test_global_metric() {
+ let _lock = lock_test();
+
+ GLOBAL_METRIC.get("a_value").set(true);
+ assert_eq!(
+ true,
+ GLOBAL_METRIC.get("a_value").test_get_value("ping").unwrap()
+ );
+ }
+
+ #[test]
+ fn sets_labeled_bool_metrics() {
+ let _lock = lock_test();
+ let store_names: Vec<String> = vec!["store1".into()];
+
+ let metric: LabeledMetric<LabeledBooleanMetric, DynamicLabel> = LabeledMetric::new(
+ 0.into(),
+ CommonMetricData {
+ name: "bool".into(),
+ category: "labeled".into(),
+ send_in_pings: store_names,
+ disabled: false,
+ ..Default::default()
+ },
+ None,
+ );
+
+ metric.get("upload").set(true);
+
+ assert!(metric.get("upload").test_get_value("store1").unwrap());
+ assert_eq!(None, metric.get("download").test_get_value("store1"));
+ }
+
+ #[test]
+ fn sets_labeled_string_metrics() {
+ let _lock = lock_test();
+ let store_names: Vec<String> = vec!["store1".into()];
+
+ let metric: LabeledMetric<LabeledStringMetric, DynamicLabel> = LabeledMetric::new(
+ 0.into(),
+ CommonMetricData {
+ name: "string".into(),
+ category: "labeled".into(),
+ send_in_pings: store_names,
+ disabled: false,
+ ..Default::default()
+ },
+ None,
+ );
+
+ metric.get("upload").set("Glean");
+
+ assert_eq!(
+ "Glean",
+ metric.get("upload").test_get_value("store1").unwrap()
+ );
+ assert_eq!(None, metric.get("download").test_get_value("store1"));
+ }
+
+ #[test]
+ fn sets_labeled_counter_metrics() {
+ let _lock = lock_test();
+ let store_names: Vec<String> = vec!["store1".into()];
+
+ let metric: LabeledMetric<LabeledCounterMetric, DynamicLabel> = LabeledMetric::new(
+ 0.into(),
+ CommonMetricData {
+ name: "counter".into(),
+ category: "labeled".into(),
+ send_in_pings: store_names,
+ disabled: false,
+ ..Default::default()
+ },
+ None,
+ );
+
+ metric.get("upload").add(10);
+
+ assert_eq!(10, metric.get("upload").test_get_value("store1").unwrap());
+ assert_eq!(None, metric.get("download").test_get_value("store1"));
+ }
+
+ #[test]
+ fn records_errors() {
+ let _lock = lock_test();
+ let store_names: Vec<String> = vec!["store1".into()];
+
+ let metric: LabeledMetric<LabeledBooleanMetric, DynamicLabel> = LabeledMetric::new(
+ 0.into(),
+ CommonMetricData {
+ name: "bool".into(),
+ category: "labeled".into(),
+ send_in_pings: store_names,
+ disabled: false,
+ ..Default::default()
+ },
+ None,
+ );
+
+ metric.get(&"1".repeat(72)).set(true);
+
+ assert_eq!(
+ 1,
+ metric.test_get_num_recorded_errors(ErrorType::InvalidLabel)
+ );
+ }
+
+ #[test]
+ fn predefined_labels() {
+ let _lock = lock_test();
+ let store_names: Vec<String> = vec!["store1".into()];
+
+ #[allow(dead_code)]
+ enum MetricLabels {
+ Label1 = 0,
+ Label2 = 1,
+ }
+ let metric: LabeledMetric<LabeledBooleanMetric, MetricLabels> = LabeledMetric::new(
+ 0.into(),
+ CommonMetricData {
+ name: "bool".into(),
+ category: "labeled".into(),
+ send_in_pings: store_names,
+ disabled: false,
+ ..Default::default()
+ },
+ Some(vec!["label1".into(), "label2".into()]),
+ );
+
+ metric.get("label1").set(true);
+ metric.get("label2").set(false);
+ metric.get("not_a_label").set(true);
+
+ assert_eq!(true, metric.get("label1").test_get_value("store1").unwrap());
+ assert_eq!(
+ false,
+ metric.get("label2").test_get_value("store1").unwrap()
+ );
+ // The label not in the predefined set is recorded to the `other` bucket.
+ assert_eq!(
+ true,
+ metric.get("__other__").test_get_value("store1").unwrap()
+ );
+
+ assert_eq!(
+ 0,
+ metric.test_get_num_recorded_errors(ErrorType::InvalidLabel)
+ );
+ }
+}
diff --git a/toolkit/components/glean/api/src/private/labeled_counter.rs b/toolkit/components/glean/api/src/private/labeled_counter.rs
new file mode 100644
index 0000000000..73a6697601
--- /dev/null
+++ b/toolkit/components/glean/api/src/private/labeled_counter.rs
@@ -0,0 +1,179 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
+
+use inherent::inherent;
+
+use glean::traits::Counter;
+
+use super::CommonMetricData;
+
+use crate::ipc::{need_ipc, with_ipc_payload};
+use crate::private::{CounterMetric, MetricId};
+use std::collections::HashMap;
+
+/// A counter metric that knows it's a labeled counter's submetric.
+///
+/// It has special work to do when in a non-parent process.
+/// When on the parent process, it dispatches calls to the normal CounterMetric.
+#[derive(Clone)]
+pub enum LabeledCounterMetric {
+ Parent(CounterMetric),
+ Child { id: MetricId, label: String },
+}
+
+impl LabeledCounterMetric {
+ /// Create a new labeled counter submetric.
+ pub fn new(id: MetricId, meta: CommonMetricData, label: String) -> Self {
+ if need_ipc() {
+ LabeledCounterMetric::Child { id, label }
+ } else {
+ LabeledCounterMetric::Parent(CounterMetric::new(id, meta))
+ }
+ }
+
+ #[cfg(test)]
+ pub(crate) fn metric_id(&self) -> MetricId {
+ match self {
+ LabeledCounterMetric::Parent(p) => p.metric_id(),
+ LabeledCounterMetric::Child { id, .. } => *id,
+ }
+ }
+}
+
+#[inherent]
+impl Counter for LabeledCounterMetric {
+ /// Increase the counter by `amount`.
+ ///
+ /// ## Arguments
+ ///
+ /// * `amount` - The amount to increase by. Should be positive.
+ ///
+ /// ## Notes
+ ///
+ /// Logs an error if the `amount` is 0 or negative.
+ pub fn add(&self, amount: i32) {
+ match self {
+ LabeledCounterMetric::Parent(p) => p.add(amount),
+ LabeledCounterMetric::Child { id, label } => {
+ with_ipc_payload(move |payload| {
+ if let Some(map) = payload.labeled_counters.get_mut(id) {
+ if let Some(v) = map.get_mut(label) {
+ *v += amount;
+ } else {
+ map.insert(label.to_string(), amount);
+ }
+ } else {
+ let mut map = HashMap::new();
+ map.insert(label.to_string(), amount);
+ payload.labeled_counters.insert(*id, map);
+ }
+ });
+ }
+ }
+ }
+
+ /// **Test-only API.**
+ ///
+ /// Get the currently stored value as an integer.
+ /// This doesn't clear the stored value.
+ ///
+ /// ## Arguments
+ ///
+ /// * `ping_name` - the storage name to look into.
+ ///
+ /// ## Return value
+ ///
+ /// Returns the stored value or `None` if nothing stored.
+ pub fn test_get_value<'a, S: Into<Option<&'a str>>>(&self, ping_name: S) -> Option<i32> {
+ match self {
+ LabeledCounterMetric::Parent(p) => p.test_get_value(ping_name),
+ LabeledCounterMetric::Child { id, .. } => {
+ panic!("Cannot get test value for {:?} in non-parent process!", id)
+ }
+ }
+ }
+
+ /// **Test-only API.**
+ ///
+ /// Gets the number of recorded errors for the given metric and error type.
+ ///
+ /// # Arguments
+ ///
+ /// * `error` - The type of error
+ /// * `ping_name` - represents the optional name of the ping to retrieve the
+ /// metric for. Defaults to the first value in `send_in_pings`.
+ ///
+ /// # Returns
+ ///
+ /// The number of errors reported.
+ pub fn test_get_num_recorded_errors(&self, error: glean::ErrorType) -> i32 {
+ match self {
+ LabeledCounterMetric::Parent(p) => p.test_get_num_recorded_errors(error),
+ LabeledCounterMetric::Child { id, .. } => panic!(
+ "Cannot get the number of recorded errors for {:?} in non-parent process!",
+ id
+ ),
+ }
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use crate::{common_test::*, ipc, metrics};
+
+ #[test]
+ fn sets_labeled_counter_value_parent() {
+ let _lock = lock_test();
+
+ let metric = &metrics::test_only_ipc::a_labeled_counter;
+ metric.get("a_label").add(1);
+
+ assert_eq!(1, metric.get("a_label").test_get_value("store1").unwrap());
+ }
+
+ #[test]
+ fn sets_labeled_counter_value_child() {
+ let _lock = lock_test();
+
+ let label = "some_label";
+
+ let parent_metric = &metrics::test_only_ipc::a_labeled_counter;
+ parent_metric.get(label).add(3);
+
+ {
+ // scope for need_ipc RAII
+ let _raii = ipc::test_set_need_ipc(true);
+ let child_metric = parent_metric.get(label);
+
+ let metric_id = child_metric.metric_id();
+
+ child_metric.add(42);
+
+ ipc::with_ipc_payload(move |payload| {
+ assert_eq!(
+ 42,
+ *payload
+ .labeled_counters
+ .get(&metric_id)
+ .unwrap()
+ .get(label)
+ .unwrap(),
+ "Stored the correct value in the ipc payload"
+ );
+ });
+ }
+
+ assert!(
+ false == ipc::need_ipc(),
+ "RAII dropped, should not need ipc any more"
+ );
+ assert!(ipc::replay_from_buf(&ipc::take_buf().unwrap()).is_ok());
+
+ assert_eq!(
+ 45,
+ parent_metric.get(label).test_get_value("store1").unwrap(),
+ "Values from the 'processes' should be summed"
+ );
+ }
+}
diff --git a/toolkit/components/glean/api/src/private/memory_distribution.rs b/toolkit/components/glean/api/src/private/memory_distribution.rs
new file mode 100644
index 0000000000..b529819562
--- /dev/null
+++ b/toolkit/components/glean/api/src/private/memory_distribution.rs
@@ -0,0 +1,206 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
+
+use inherent::inherent;
+use std::convert::TryInto;
+
+use super::{CommonMetricData, DistributionData, MemoryUnit, MetricId};
+
+use glean::traits::MemoryDistribution;
+
+use crate::ipc::{need_ipc, with_ipc_payload};
+
+/// A memory distribution metric.
+///
+/// Memory distributions are used to accumulate and store memory measurements for analyzing distributions of the memory data.
+#[derive(Clone)]
+pub enum MemoryDistributionMetric {
+ Parent {
+ /// The metric's ID.
+ ///
+ /// **TEST-ONLY** - Do not use unless gated with `#[cfg(test)]`.
+ id: MetricId,
+ inner: glean::private::MemoryDistributionMetric,
+ },
+ Child(MemoryDistributionMetricIpc),
+}
+#[derive(Clone, Debug)]
+pub struct MemoryDistributionMetricIpc(MetricId);
+
+impl MemoryDistributionMetric {
+ /// Create a new memory distribution metric.
+ pub fn new(id: MetricId, meta: CommonMetricData, memory_unit: MemoryUnit) -> Self {
+ if need_ipc() {
+ MemoryDistributionMetric::Child(MemoryDistributionMetricIpc(id))
+ } else {
+ let inner = glean::private::MemoryDistributionMetric::new(meta, memory_unit);
+ MemoryDistributionMetric::Parent { id, inner }
+ }
+ }
+
+ #[cfg(test)]
+ pub(crate) fn child_metric(&self) -> Self {
+ match self {
+ MemoryDistributionMetric::Parent { id, .. } => {
+ MemoryDistributionMetric::Child(MemoryDistributionMetricIpc(*id))
+ }
+ MemoryDistributionMetric::Child(_) => {
+ panic!("Can't get a child metric from a child metric")
+ }
+ }
+ }
+}
+
+#[inherent]
+impl MemoryDistribution for MemoryDistributionMetric {
+ /// Accumulates the provided sample in the metric.
+ ///
+ /// ## Arguments
+ ///
+ /// * `sample` - The sample to be recorded by the metric. The sample is assumed to be in the
+ /// configured memory unit of the metric.
+ ///
+ /// ## Notes
+ ///
+ /// Values bigger than 1 Terabyte (2<sup>40</sup> bytes) are truncated
+ /// and an `ErrorType::InvalidValue` error is recorded.
+ pub fn accumulate(&self, sample: u64) {
+ match self {
+ MemoryDistributionMetric::Parent { inner, .. } => {
+ // values are capped at 2**40.
+ // If the value doesn't fit into `i64` it's definitely to large
+ // and cause an error.
+ // glean-core handles that.
+ let sample = sample.try_into().unwrap_or_else(|_| {
+ log::warn!(
+ "Memory size too large to fit into into 64-bytes. Saturating at i64::MAX."
+ );
+ i64::MAX
+ });
+ inner.accumulate(sample);
+ }
+ MemoryDistributionMetric::Child(c) => {
+ with_ipc_payload(move |payload| {
+ if let Some(v) = payload.memory_samples.get_mut(&c.0) {
+ v.push(sample);
+ } else {
+ payload.memory_samples.insert(c.0, vec![sample]);
+ }
+ });
+ }
+ }
+ }
+
+ /// **Test-only API.**
+ ///
+ /// Get the currently-stored histogram as a DistributionData of the serialized value.
+ /// This doesn't clear the stored value.
+ ///
+ /// ## Arguments
+ ///
+ /// * `ping_name` - the storage name to look into.
+ ///
+ /// ## Return value
+ ///
+ /// Returns the stored value or `None` if nothing stored.
+ pub fn test_get_value<'a, S: Into<Option<&'a str>>>(
+ &self,
+ ping_name: S,
+ ) -> Option<DistributionData> {
+ let ping_name = ping_name.into().map(|s| s.to_string());
+ match self {
+ MemoryDistributionMetric::Parent { inner, .. } => inner.test_get_value(ping_name),
+ MemoryDistributionMetric::Child(c) => {
+ panic!("Cannot get test value for {:?} in non-parent process!", c.0)
+ }
+ }
+ }
+
+ /// **Exported for test purposes.**
+ ///
+ /// Gets the number of recorded errors for the given error type.
+ ///
+ /// # Arguments
+ ///
+ /// * `error` - The type of error
+ /// * `ping_name` - represents the optional name of the ping to retrieve the
+ /// metric for. Defaults to the first value in `send_in_pings`.
+ ///
+ /// # Returns
+ ///
+ /// The number of errors recorded.
+ pub fn test_get_num_recorded_errors(&self, error: glean::ErrorType) -> i32 {
+ match self {
+ MemoryDistributionMetric::Parent { inner, .. } => {
+ inner.test_get_num_recorded_errors(error)
+ }
+ MemoryDistributionMetric::Child(c) => panic!(
+ "Cannot get the number of recorded errors for {:?} in non-parent process!",
+ c.0
+ ),
+ }
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+ use crate::{common_test::*, ipc, metrics};
+
+ #[test]
+ fn smoke_test_memory_distribution() {
+ let _lock = lock_test();
+
+ let metric = MemoryDistributionMetric::new(
+ 0.into(),
+ CommonMetricData {
+ name: "memory_distribution_metric".into(),
+ category: "telemetry".into(),
+ send_in_pings: vec!["store1".into()],
+ disabled: false,
+ ..Default::default()
+ },
+ MemoryUnit::Kilobyte,
+ );
+
+ metric.accumulate(42);
+
+ let metric_data = metric.test_get_value("store1").unwrap();
+ assert_eq!(1, metric_data.values[&42494]);
+ assert_eq!(0, metric_data.values[&44376]);
+ assert_eq!(43008, metric_data.sum);
+ }
+
+ #[test]
+ fn memory_distribution_child() {
+ let _lock = lock_test();
+
+ let parent_metric = &metrics::test_only_ipc::a_memory_dist;
+ parent_metric.accumulate(42);
+
+ {
+ let child_metric = parent_metric.child_metric();
+
+ // scope for need_ipc RAII
+ let _raii = ipc::test_set_need_ipc(true);
+ child_metric.accumulate(13 * 9);
+ }
+
+ let metric_data = parent_metric.test_get_value("store1").unwrap();
+ assert_eq!(1, metric_data.values[&42494]);
+ assert_eq!(0, metric_data.values[&44376]);
+ assert_eq!(43008, metric_data.sum);
+
+ // Single-process IPC machine goes brrrrr...
+ let buf = ipc::take_buf().unwrap();
+ assert!(buf.len() > 0);
+ assert!(ipc::replay_from_buf(&buf).is_ok());
+
+ let data = parent_metric.test_get_value(None).expect("must have data");
+ assert_eq!(2, data.values.values().fold(0, |acc, count| acc + count));
+ assert_eq!(1, data.values[&42494]);
+ assert_eq!(1, data.values[&115097]);
+ assert_eq!(162816, data.sum);
+ }
+}
diff --git a/toolkit/components/glean/api/src/private/mod.rs b/toolkit/components/glean/api/src/private/mod.rs
new file mode 100644
index 0000000000..b0b1e11393
--- /dev/null
+++ b/toolkit/components/glean/api/src/private/mod.rs
@@ -0,0 +1,76 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
+
+//! The different metric types supported by the Glean SDK to handle data.
+
+use serde::{Deserialize, Serialize};
+
+// Re-export of `glean` types we can re-use.
+// That way a user only needs to depend on this crate, not on glean (and there can't be a
+// version mismatch).
+pub use glean::{
+ traits, CommonMetricData, DistributionData, ErrorType, Lifetime, MemoryUnit, RecordedEvent,
+ TimeUnit, TimerId,
+};
+
+mod boolean;
+mod counter;
+mod custom_distribution;
+mod datetime;
+mod denominator;
+mod event;
+mod labeled;
+mod labeled_counter;
+mod memory_distribution;
+mod numerator;
+mod ping;
+mod quantity;
+mod rate;
+pub(crate) mod string;
+mod string_list;
+mod text;
+mod timespan;
+mod timing_distribution;
+mod url;
+mod uuid;
+
+pub use self::boolean::BooleanMetric;
+pub use self::boolean::BooleanMetric as LabeledBooleanMetric;
+pub use self::counter::CounterMetric;
+pub use self::custom_distribution::CustomDistributionMetric;
+pub use self::datetime::DatetimeMetric;
+pub use self::denominator::DenominatorMetric;
+pub use self::event::{EventMetric, EventRecordingError, ExtraKeys, NoExtraKeys};
+pub use self::labeled::LabeledMetric;
+pub use self::labeled_counter::LabeledCounterMetric;
+pub use self::memory_distribution::MemoryDistributionMetric;
+pub use self::numerator::NumeratorMetric;
+pub use self::ping::Ping;
+pub use self::quantity::QuantityMetric;
+pub use self::rate::RateMetric;
+pub use self::string::StringMetric;
+pub use self::string::StringMetric as LabeledStringMetric;
+pub use self::string_list::StringListMetric;
+pub use self::text::TextMetric;
+pub use self::timespan::TimespanMetric;
+pub use self::timing_distribution::TimingDistributionMetric;
+pub use self::url::UrlMetric;
+pub use self::uuid::UuidMetric;
+
+/// Uniquely identifies a single metric within its metric type.
+#[derive(Debug, PartialEq, Eq, Hash, Copy, Clone, Deserialize, Serialize)]
+#[repr(transparent)]
+pub struct MetricId(pub(crate) u32);
+
+impl MetricId {
+ pub fn new(id: u32) -> Self {
+ Self(id)
+ }
+}
+
+impl From<u32> for MetricId {
+ fn from(id: u32) -> Self {
+ Self(id)
+ }
+}
diff --git a/toolkit/components/glean/api/src/private/numerator.rs b/toolkit/components/glean/api/src/private/numerator.rs
new file mode 100644
index 0000000000..0a22bf5bfc
--- /dev/null
+++ b/toolkit/components/glean/api/src/private/numerator.rs
@@ -0,0 +1,152 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
+
+use inherent::inherent;
+
+use super::CommonMetricData;
+
+use glean::traits::Numerator;
+use glean::Rate;
+
+use crate::ipc::{need_ipc, with_ipc_payload};
+use crate::private::MetricId;
+
+/// Developer-facing API for recording rate metrics with external denominators.
+///
+/// Instances of this class type are automatically generated by the parsers
+/// at build time, allowing developers to record values that were previously
+/// registered in the metrics.yaml file.
+#[derive(Clone)]
+pub enum NumeratorMetric {
+ Parent {
+ /// The metric's ID.
+ ///
+ /// **TEST-ONLY** - Do not use unless gated with `#[cfg(test)]`.
+ id: MetricId,
+ inner: glean::private::NumeratorMetric,
+ },
+ Child(NumeratorMetricIpc),
+}
+#[derive(Clone, Debug)]
+pub struct NumeratorMetricIpc(MetricId);
+
+impl NumeratorMetric {
+ /// The public constructor used by automatically generated metrics.
+ pub fn new(id: MetricId, meta: CommonMetricData) -> Self {
+ if need_ipc() {
+ NumeratorMetric::Child(NumeratorMetricIpc(id))
+ } else {
+ let inner = glean::private::NumeratorMetric::new(meta);
+ NumeratorMetric::Parent { id, inner }
+ }
+ }
+
+ #[cfg(test)]
+ pub(crate) fn metric_id(&self) -> MetricId {
+ match self {
+ NumeratorMetric::Parent { id, .. } => *id,
+ NumeratorMetric::Child(c) => c.0,
+ }
+ }
+
+ #[cfg(test)]
+ pub(crate) fn child_metric(&self) -> Self {
+ match self {
+ NumeratorMetric::Parent { id, .. } => NumeratorMetric::Child(NumeratorMetricIpc(*id)),
+ NumeratorMetric::Child(_) => panic!("Can't get a child metric from a child metric"),
+ }
+ }
+}
+
+#[inherent]
+impl Numerator for NumeratorMetric {
+ pub fn add_to_numerator(&self, amount: i32) {
+ match self {
+ NumeratorMetric::Parent { inner, .. } => {
+ inner.add_to_numerator(amount);
+ }
+ NumeratorMetric::Child(c) => {
+ with_ipc_payload(move |payload| {
+ if let Some(v) = payload.numerators.get_mut(&c.0) {
+ *v += amount;
+ } else {
+ payload.numerators.insert(c.0, amount);
+ }
+ });
+ }
+ }
+ }
+
+ pub fn test_get_value<'a, S: Into<Option<&'a str>>>(&self, ping_name: S) -> Option<Rate> {
+ let ping_name = ping_name.into().map(|s| s.to_string());
+ match self {
+ NumeratorMetric::Parent { inner, .. } => inner.test_get_value(ping_name),
+ NumeratorMetric::Child(c) => {
+ panic!("Cannot get test value for {:?} in non-parent process!", c.0);
+ }
+ }
+ }
+
+ pub fn test_get_num_recorded_errors(&self, error: glean::ErrorType) -> i32 {
+ match self {
+ NumeratorMetric::Parent { inner, .. } => inner.test_get_num_recorded_errors(error),
+ NumeratorMetric::Child(c) => {
+ panic!(
+ "Cannot get the number of recorded errors for {:?} in non-parent process!",
+ c.0
+ );
+ }
+ }
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use crate::{common_test::*, ipc, metrics};
+
+ #[test]
+ fn sets_numerator_value_parent() {
+ let _lock = lock_test();
+
+ let metric = &metrics::test_only_ipc::rate_with_external_denominator;
+ metric.add_to_numerator(1);
+
+ assert_eq!(1, metric.test_get_value("store1").unwrap().numerator);
+ }
+
+ #[test]
+ fn sets_numerator_value_child() {
+ let _lock = lock_test();
+
+ let parent_metric = &metrics::test_only_ipc::rate_with_external_denominator;
+ parent_metric.add_to_numerator(3);
+
+ {
+ // scope for need_ipc RAII
+ let child_metric = parent_metric.child_metric();
+ let _raii = ipc::test_set_need_ipc(true);
+ let metric_id = child_metric.metric_id();
+
+ child_metric.add_to_numerator(42);
+
+ ipc::with_ipc_payload(move |payload| {
+ assert!(
+ 42 == *payload.numerators.get(&metric_id).unwrap(),
+ "Stored the correct value in the ipc payload"
+ );
+ });
+ }
+
+ assert!(
+ false == ipc::need_ipc(),
+ "RAII dropped, should not need ipc any more"
+ );
+ assert!(ipc::replay_from_buf(&ipc::take_buf().unwrap()).is_ok());
+
+ assert!(
+ 45 == parent_metric.test_get_value("store1").unwrap().numerator,
+ "Values from the 'processes' should be summed"
+ );
+ }
+}
diff --git a/toolkit/components/glean/api/src/private/ping.rs b/toolkit/components/glean/api/src/private/ping.rs
new file mode 100644
index 0000000000..cc9585eea1
--- /dev/null
+++ b/toolkit/components/glean/api/src/private/ping.rs
@@ -0,0 +1,121 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
+
+use inherent::inherent;
+
+use crate::ipc::need_ipc;
+
+/// A Glean ping.
+///
+/// See [Glean Pings](https://mozilla.github.io/glean/book/user/pings/index.html).
+#[derive(Clone)]
+pub enum Ping {
+ Parent(glean::private::PingType),
+ Child,
+}
+
+impl Ping {
+ /// Create a new ping type for the given name, whether to include the client ID and whether to
+ /// send this ping empty.
+ ///
+ /// ## Arguments
+ ///
+ /// * `name` - The name of the ping.
+ /// * `include_client_id` - Whether to include the client ID in the assembled ping when submitting.
+ /// * `send_if_empty` - Whether the ping should be sent empty or not.
+ /// * `reason_codes` - The valid reason codes for this ping.
+ pub fn new<S: Into<String>>(
+ name: S,
+ include_client_id: bool,
+ send_if_empty: bool,
+ precise_timestamps: bool,
+ reason_codes: Vec<String>,
+ ) -> Self {
+ if need_ipc() {
+ Ping::Child
+ } else {
+ Ping::Parent(glean::private::PingType::new(
+ name,
+ include_client_id,
+ send_if_empty,
+ precise_timestamps,
+ reason_codes,
+ ))
+ }
+ }
+
+ /// **Test-only API**
+ ///
+ /// Attach a callback to be called right before a new ping is submitted.
+ /// The provided function is called exactly once before submitting a ping.
+ ///
+ /// Note: The callback will be called on any call to submit.
+ /// A ping might not be sent afterwards, e.g. if the ping is otherwise empty (and
+ /// `send_if_empty` is `false`).
+ pub fn test_before_next_submit(&self, cb: impl FnOnce(Option<&str>) + Send + 'static) {
+ match self {
+ Ping::Parent(p) => p.test_before_next_submit(cb),
+ Ping::Child => {
+ panic!("Cannot use ping test API from non-parent process!");
+ }
+ };
+ }
+}
+
+#[inherent]
+impl glean::traits::Ping for Ping {
+ /// Submits the ping for eventual uploading
+ ///
+ /// # Arguments
+ ///
+ /// * `reason` - the reason the ping was triggered. Included in the
+ /// `ping_info.reason` part of the payload.
+ pub fn submit(&self, reason: Option<&str>) {
+ match self {
+ Ping::Parent(p) => {
+ p.submit(reason);
+ }
+ Ping::Child => {
+ log::error!(
+ "Unable to submit ping in non-main process. This operation will be ignored."
+ );
+ // If we're in automation we can panic so the instrumentor knows they've gone wrong.
+ // This is a deliberate violation of Glean's "metric APIs must not throw" design.
+ assert!(!crate::ipc::is_in_automation(), "Attempted to submit a ping in non-main process, which is forbidden. This panics in automation.");
+ // TODO: Record an error.
+ }
+ };
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use once_cell::sync::Lazy;
+
+ use super::*;
+ use crate::common_test::*;
+
+ use std::sync::{
+ atomic::{AtomicBool, Ordering},
+ Arc,
+ };
+
+ // Smoke test for what should be the generated code.
+ static PROTOTYPE_PING: Lazy<Ping> =
+ Lazy::new(|| Ping::new("prototype", false, true, true, vec![]));
+
+ #[test]
+ fn smoke_test_custom_ping() {
+ let _lock = lock_test();
+
+ let called = Arc::new(AtomicBool::new(false));
+ let rcalled = Arc::clone(&called);
+ PROTOTYPE_PING.test_before_next_submit(move |reason| {
+ (*rcalled).store(true, Ordering::Relaxed);
+ assert_eq!(None, reason);
+ });
+ PROTOTYPE_PING.submit(None);
+ assert!((*called).load(Ordering::Relaxed));
+ }
+}
diff --git a/toolkit/components/glean/api/src/private/quantity.rs b/toolkit/components/glean/api/src/private/quantity.rs
new file mode 100644
index 0000000000..ee7021890e
--- /dev/null
+++ b/toolkit/components/glean/api/src/private/quantity.rs
@@ -0,0 +1,155 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
+
+use inherent::inherent;
+
+use glean::traits::Quantity;
+
+use super::CommonMetricData;
+
+use crate::ipc::need_ipc;
+use crate::private::MetricId;
+
+/// A quantity metric.
+///
+/// Records a single numeric value of a specific unit.
+#[derive(Clone)]
+pub enum QuantityMetric {
+ Parent(glean::private::QuantityMetric),
+ Child(QuantityMetricIpc),
+}
+#[derive(Clone, Debug)]
+pub struct QuantityMetricIpc;
+
+impl QuantityMetric {
+ /// Create a new quantity metric.
+ pub fn new(_id: MetricId, meta: CommonMetricData) -> Self {
+ if need_ipc() {
+ QuantityMetric::Child(QuantityMetricIpc)
+ } else {
+ QuantityMetric::Parent(glean::private::QuantityMetric::new(meta))
+ }
+ }
+
+ #[cfg(test)]
+ pub(crate) fn child_metric(&self) -> Self {
+ match self {
+ QuantityMetric::Parent(_) => QuantityMetric::Child(QuantityMetricIpc),
+ QuantityMetric::Child(_) => panic!("Can't get a child metric from a child metric"),
+ }
+ }
+}
+
+#[inherent]
+impl Quantity for QuantityMetric {
+ /// Set the value. Must be non-negative.
+ ///
+ /// # Arguments
+ ///
+ /// * `value` - The value. Must be non-negative.
+ ///
+ /// ## Notes
+ ///
+ /// Logs an error if the `value` is negative.
+ pub fn set(&self, value: i64) {
+ match self {
+ QuantityMetric::Parent(p) => {
+ p.set(value);
+ }
+ QuantityMetric::Child(_) => {
+ log::error!("Unable to set quantity metric in non-main process. This operation will be ignored.");
+ // If we're in automation we can panic so the instrumentor knows they've gone wrong.
+ // This is a deliberate violation of Glean's "metric APIs must not throw" design.
+ assert!(!crate::ipc::is_in_automation(), "Attempted to set quantity metric in non-main process, which is forbidden. This panics in automation.");
+ // TODO: Record an error.
+ }
+ }
+ }
+
+ /// **Test-only API.**
+ ///
+ /// Get the currently stored value.
+ /// This doesn't clear the stored value.
+ ///
+ /// ## Arguments
+ ///
+ /// * `ping_name` - the storage name to look into.
+ ///
+ /// ## Return value
+ ///
+ /// Returns the stored value or `None` if nothing stored.
+ pub fn test_get_value<'a, S: Into<Option<&'a str>>>(&self, ping_name: S) -> Option<i64> {
+ let ping_name = ping_name.into().map(|s| s.to_string());
+ match self {
+ QuantityMetric::Parent(p) => p.test_get_value(ping_name),
+ QuantityMetric::Child(_) => {
+ panic!("Cannot get test value for quantity metric in non-main process!",)
+ }
+ }
+ }
+
+ /// **Test-only API.**
+ ///
+ /// Gets the number of recorded errors for the given metric and error type.
+ ///
+ /// # Arguments
+ ///
+ /// * `error` - The type of error
+ /// * `ping_name` - represents the optional name of the ping to retrieve the
+ /// metric for. Defaults to the first value in `send_in_pings`.
+ ///
+ /// # Returns
+ ///
+ /// The number of errors reported.
+ pub fn test_get_num_recorded_errors(&self, error: glean::ErrorType) -> i32 {
+ match self {
+ QuantityMetric::Parent(p) => p.test_get_num_recorded_errors(error),
+ QuantityMetric::Child(_) => panic!(
+ "Cannot get the number of recorded errors for quantity metric in non-main process!"
+ ),
+ }
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use crate::{common_test::*, ipc, metrics};
+
+ #[test]
+ fn sets_quantity_metric() {
+ let _lock = lock_test();
+
+ let metric = &metrics::test_only_ipc::a_quantity;
+ metric.set(14);
+
+ assert_eq!(14, metric.test_get_value("store1").unwrap());
+ }
+
+ #[test]
+ fn quantity_ipc() {
+ // QuantityMetric doesn't support IPC.
+ let _lock = lock_test();
+
+ let parent_metric = &metrics::test_only_ipc::a_quantity;
+
+ parent_metric.set(15);
+
+ {
+ let child_metric = parent_metric.child_metric();
+
+ // scope for need_ipc RAII
+ let _raii = ipc::test_set_need_ipc(true);
+
+ // Instrumentation calls do not panic.
+ child_metric.set(30);
+
+ // (They also shouldn't do anything,
+ // but that's not something we can inspect in this test)
+ }
+
+ assert!(ipc::replay_from_buf(&ipc::take_buf().unwrap()).is_ok());
+
+ assert_eq!(15, parent_metric.test_get_value(None).unwrap());
+ }
+}
diff --git a/toolkit/components/glean/api/src/private/rate.rs b/toolkit/components/glean/api/src/private/rate.rs
new file mode 100644
index 0000000000..39f04db767
--- /dev/null
+++ b/toolkit/components/glean/api/src/private/rate.rs
@@ -0,0 +1,186 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
+
+use inherent::inherent;
+
+use super::CommonMetricData;
+
+use glean::traits::Rate;
+
+use crate::ipc::{need_ipc, with_ipc_payload};
+use crate::private::MetricId;
+
+/// Developer-facing API for recording rate metrics.
+///
+/// Instances of this class type are automatically generated by the parsers
+/// at build time, allowing developers to record values that were previously
+/// registered in the metrics.yaml file.
+#[derive(Clone)]
+pub enum RateMetric {
+ Parent {
+ /// The metric's ID.
+ ///
+ /// **TEST-ONLY** - Do not use unless gated with `#[cfg(test)]`.
+ id: MetricId,
+ inner: glean::private::RateMetric,
+ },
+ Child(RateMetricIpc),
+}
+#[derive(Clone, Debug)]
+pub struct RateMetricIpc(MetricId);
+
+impl RateMetric {
+ /// The public constructor used by automatically generated metrics.
+ pub fn new(id: MetricId, meta: CommonMetricData) -> Self {
+ if need_ipc() {
+ RateMetric::Child(RateMetricIpc(id))
+ } else {
+ let inner = glean::private::RateMetric::new(meta);
+ RateMetric::Parent { id, inner }
+ }
+ }
+
+ #[cfg(test)]
+ pub(crate) fn metric_id(&self) -> MetricId {
+ match self {
+ RateMetric::Parent { id, .. } => *id,
+ RateMetric::Child(c) => c.0,
+ }
+ }
+
+ #[cfg(test)]
+ pub(crate) fn child_metric(&self) -> Self {
+ match self {
+ RateMetric::Parent { id, .. } => RateMetric::Child(RateMetricIpc(*id)),
+ RateMetric::Child(_) => panic!("Can't get a child metric from a child metric"),
+ }
+ }
+}
+
+#[inherent]
+impl Rate for RateMetric {
+ pub fn add_to_numerator(&self, amount: i32) {
+ match self {
+ RateMetric::Parent { inner, .. } => {
+ inner.add_to_numerator(amount);
+ }
+ RateMetric::Child(c) => {
+ with_ipc_payload(move |payload| {
+ if let Some(r) = payload.rates.get_mut(&c.0) {
+ r.0 += amount;
+ } else {
+ payload.rates.insert(c.0, (amount, 0));
+ }
+ });
+ }
+ }
+ }
+
+ pub fn add_to_denominator(&self, amount: i32) {
+ match self {
+ RateMetric::Parent { inner, .. } => {
+ inner.add_to_denominator(amount);
+ }
+ RateMetric::Child(c) => {
+ with_ipc_payload(move |payload| {
+ if let Some(r) = payload.rates.get_mut(&c.0) {
+ r.1 += amount;
+ } else {
+ payload.rates.insert(c.0, (0, amount));
+ }
+ });
+ }
+ }
+ }
+
+ pub fn test_get_value<'a, S: Into<Option<&'a str>>>(
+ &self,
+ ping_name: S,
+ ) -> Option<glean::Rate> {
+ let ping_name = ping_name.into().map(|s| s.to_string());
+ match self {
+ RateMetric::Parent { inner, .. } => inner.test_get_value(ping_name),
+ RateMetric::Child(c) => {
+ panic!("Cannot get test value for {:?} in non-parent process!", c.0);
+ }
+ }
+ }
+
+ pub fn test_get_num_recorded_errors(&self, error: glean::ErrorType) -> i32 {
+ match self {
+ RateMetric::Parent { inner, .. } => inner.test_get_num_recorded_errors(error),
+ RateMetric::Child(c) => {
+ panic!(
+ "Cannot get the number of recorded errors for {:?} in non-parent process!",
+ c.0
+ );
+ }
+ }
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use crate::{common_test::*, ipc, metrics};
+ use glean::Rate;
+
+ #[test]
+ fn sets_rate_value_parent() {
+ let _lock = lock_test();
+
+ let metric = &metrics::test_only_ipc::irate;
+ metric.add_to_numerator(1);
+ metric.add_to_denominator(100);
+
+ assert_eq!(
+ Rate {
+ numerator: 1,
+ denominator: 100
+ },
+ metric.test_get_value("store1").unwrap()
+ );
+ }
+
+ #[test]
+ fn sets_rate_value_child() {
+ let _lock = lock_test();
+
+ let parent_metric = &metrics::test_only_ipc::irate;
+ parent_metric.add_to_numerator(3);
+ parent_metric.add_to_denominator(9);
+
+ {
+ // scope for need_ipc RAII
+ let child_metric = parent_metric.child_metric();
+ let _raii = ipc::test_set_need_ipc(true);
+ let metric_id = child_metric.metric_id();
+
+ child_metric.add_to_numerator(42);
+ child_metric.add_to_denominator(24);
+
+ ipc::with_ipc_payload(move |payload| {
+ assert_eq!(
+ (42, 24),
+ *payload.rates.get(&metric_id).unwrap(),
+ "Stored the correct value in the ipc payload"
+ );
+ });
+ }
+
+ assert!(
+ false == ipc::need_ipc(),
+ "RAII dropped, should not need ipc any more"
+ );
+ assert!(ipc::replay_from_buf(&ipc::take_buf().unwrap()).is_ok());
+
+ assert_eq!(
+ Rate {
+ numerator: 45,
+ denominator: 33
+ },
+ parent_metric.test_get_value("store1").unwrap(),
+ "Values from the 'processes' should be summed"
+ );
+ }
+}
diff --git a/toolkit/components/glean/api/src/private/string.rs b/toolkit/components/glean/api/src/private/string.rs
new file mode 100644
index 0000000000..e61064c3d5
--- /dev/null
+++ b/toolkit/components/glean/api/src/private/string.rs
@@ -0,0 +1,185 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
+
+use inherent::inherent;
+use std::sync::Arc;
+
+use super::{CommonMetricData, MetricId};
+use crate::ipc::need_ipc;
+
+/// A string metric.
+///
+/// Record an Unicode string value with arbitrary content.
+/// Strings are length-limited to `MAX_LENGTH_VALUE` bytes.
+///
+/// # Example
+///
+/// The following piece of code will be generated by `glean_parser`:
+///
+/// ```rust,ignore
+/// use glean::metrics::{StringMetric, CommonMetricData, Lifetime};
+/// use once_cell::sync::Lazy;
+///
+/// mod browser {
+/// pub static search_engine: Lazy<StringMetric> = Lazy::new(|| StringMetric::new(CommonMetricData {
+/// name: "search_engine".into(),
+/// category: "browser".into(),
+/// lifetime: Lifetime::Ping,
+/// disabled: false,
+/// dynamic_label: None
+/// }));
+/// }
+/// ```
+///
+/// It can then be used with:
+///
+/// ```rust,ignore
+/// browser::search_engine.set("websearch");
+/// ```
+#[derive(Clone)]
+pub enum StringMetric {
+ Parent(Arc<glean::private::StringMetric>),
+ Child(StringMetricIpc),
+}
+#[derive(Clone, Debug)]
+pub struct StringMetricIpc;
+
+impl StringMetric {
+ /// Create a new string metric.
+ pub fn new(_id: MetricId, meta: CommonMetricData) -> Self {
+ if need_ipc() {
+ StringMetric::Child(StringMetricIpc)
+ } else {
+ StringMetric::Parent(Arc::new(glean::private::StringMetric::new(meta)))
+ }
+ }
+
+ #[cfg(test)]
+ pub(crate) fn child_metric(&self) -> Self {
+ match self {
+ StringMetric::Parent(_) => StringMetric::Child(StringMetricIpc),
+ StringMetric::Child(_) => panic!("Can't get a child metric from a child metric"),
+ }
+ }
+}
+
+#[inherent]
+impl glean::traits::String for StringMetric {
+ /// Sets to the specified value.
+ ///
+ /// # Arguments
+ ///
+ /// * `value` - The string to set the metric to.
+ ///
+ /// ## Notes
+ ///
+ /// Truncates the value if it is longer than `MAX_STRING_LENGTH` bytes and logs an error.
+ pub fn set<S: Into<std::string::String>>(&self, value: S) {
+ match self {
+ StringMetric::Parent(p) => {
+ p.set(value.into());
+ }
+ StringMetric::Child(_) => {
+ log::error!("Unable to set string metric in non-main process. This operation will be ignored.");
+ // If we're in automation we can panic so the instrumentor knows they've gone wrong.
+ // This is a deliberate violation of Glean's "metric APIs must not throw" design.
+ assert!(!crate::ipc::is_in_automation(), "Attempted to set string metric in non-main process, which is forbidden. This panics in automation.");
+ // TODO: Record an error.
+ }
+ };
+ }
+
+ /// **Exported for test purposes.**
+ ///
+ /// Gets the currently stored value as a string.
+ ///
+ /// This doesn't clear the stored value.
+ ///
+ /// # Arguments
+ ///
+ /// * `ping_name` - represents the optional name of the ping to retrieve the
+ /// metric for. Defaults to the first value in `send_in_pings`.
+ pub fn test_get_value<'a, S: Into<Option<&'a str>>>(
+ &self,
+ ping_name: S,
+ ) -> Option<std::string::String> {
+ let ping_name = ping_name.into().map(|s| s.to_string());
+ match self {
+ StringMetric::Parent(p) => p.test_get_value(ping_name),
+ StringMetric::Child(_) => {
+ panic!("Cannot get test value for string metric in non-main process!")
+ }
+ }
+ }
+
+ /// **Exported for test purposes.**
+ ///
+ /// Gets the number of recorded errors for the given metric and error type.
+ ///
+ /// # Arguments
+ ///
+ /// * `error` - The type of error
+ /// * `ping_name` - represents the optional name of the ping to retrieve the
+ /// metric for. Defaults to the first value in `send_in_pings`.
+ ///
+ /// # Returns
+ ///
+ /// The number of errors reported.
+ pub fn test_get_num_recorded_errors(&self, error: glean::ErrorType) -> i32 {
+ match self {
+ StringMetric::Parent(p) => p.test_get_num_recorded_errors(error),
+ StringMetric::Child(_) => panic!(
+ "Cannot get the number of recorded errors for string metric in non-main process!"
+ ),
+ }
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use crate::{common_test::*, ipc, metrics};
+
+ #[test]
+ fn sets_string_value() {
+ let _lock = lock_test();
+
+ let metric = &metrics::test_only_ipc::a_string;
+
+ metric.set("test_string_value");
+
+ assert_eq!(
+ "test_string_value",
+ metric.test_get_value("store1").unwrap()
+ );
+ }
+
+ #[test]
+ fn string_ipc() {
+ // StringMetric doesn't support IPC.
+ let _lock = lock_test();
+
+ let parent_metric = &metrics::test_only_ipc::a_string;
+
+ parent_metric.set("test_parent_value");
+
+ {
+ let child_metric = parent_metric.child_metric();
+
+ let _raii = ipc::test_set_need_ipc(true);
+
+ // Instrumentation calls do not panic.
+ child_metric.set("test_string_value");
+
+ // (They also shouldn't do anything,
+ // but that's not something we can inspect in this test)
+ }
+
+ assert!(ipc::replay_from_buf(&ipc::take_buf().unwrap()).is_ok());
+
+ assert!(
+ "test_parent_value" == parent_metric.test_get_value("store1").unwrap(),
+ "String metrics should only work in the parent process"
+ );
+ }
+}
diff --git a/toolkit/components/glean/api/src/private/string_list.rs b/toolkit/components/glean/api/src/private/string_list.rs
new file mode 100644
index 0000000000..7edd4e89d4
--- /dev/null
+++ b/toolkit/components/glean/api/src/private/string_list.rs
@@ -0,0 +1,212 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
+
+use inherent::inherent;
+
+use super::{CommonMetricData, MetricId};
+
+use glean::traits::StringList;
+
+use crate::ipc::{need_ipc, with_ipc_payload};
+
+/// A string list metric.
+///
+/// This allows appending a string value with arbitrary content to a list.
+#[derive(Clone)]
+pub enum StringListMetric {
+ Parent {
+ /// The metric's ID.
+ ///
+ /// **TEST-ONLY** - Do not use unless gated with `#[cfg(test)]`.
+ id: MetricId,
+ inner: glean::private::StringListMetric,
+ },
+ Child(StringListMetricIpc),
+}
+#[derive(Clone, Debug)]
+pub struct StringListMetricIpc(MetricId);
+
+impl StringListMetric {
+ /// Create a new string list metric.
+ pub fn new(id: MetricId, meta: CommonMetricData) -> Self {
+ if need_ipc() {
+ StringListMetric::Child(StringListMetricIpc(id))
+ } else {
+ let inner = glean::private::StringListMetric::new(meta);
+ StringListMetric::Parent { id, inner }
+ }
+ }
+
+ #[cfg(test)]
+ pub(crate) fn child_metric(&self) -> Self {
+ match self {
+ StringListMetric::Parent { id, .. } => {
+ StringListMetric::Child(StringListMetricIpc(*id))
+ }
+ StringListMetric::Child(_) => panic!("Can't get a child metric from a child metric"),
+ }
+ }
+}
+
+#[inherent]
+impl StringList for StringListMetric {
+ /// Add a new string to the list.
+ ///
+ /// ## Arguments
+ ///
+ /// * `value` - The string to add.
+ ///
+ /// ## Notes
+ ///
+ /// Truncates the value if it is longer than `MAX_STRING_LENGTH` bytes and logs an error.
+ /// See [String list metric limits](https://mozilla.github.io/glean/book/user/metrics/string_list.html#limits).
+ pub fn add<S: Into<String>>(&self, value: S) {
+ match self {
+ StringListMetric::Parent { inner, .. } => {
+ inner.add(value.into());
+ }
+ StringListMetric::Child(c) => {
+ with_ipc_payload(move |payload| {
+ if let Some(v) = payload.string_lists.get_mut(&c.0) {
+ v.push(value.into());
+ } else {
+ let v = vec![value.into()];
+ payload.string_lists.insert(c.0, v);
+ }
+ });
+ }
+ }
+ }
+
+ /// Set to a specific list of strings.
+ ///
+ /// ## Arguments
+ ///
+ /// * `value` - The list of string to set the metric to.
+ ///
+ /// ## Notes
+ ///
+ /// If passed an empty list, records an error and returns.
+ /// Truncates the list if it is longer than `MAX_LIST_LENGTH` and logs an error.
+ /// Truncates any value in the list if it is longer than `MAX_STRING_LENGTH` and logs an error.
+ pub fn set(&self, value: Vec<String>) {
+ match self {
+ StringListMetric::Parent { inner, .. } => {
+ inner.set(value);
+ }
+ StringListMetric::Child(c) => {
+ log::error!(
+ "Unable to set string list metric {:?} in non-main process. This operation will be ignored.",
+ c.0
+ );
+ // If we're in automation we can panic so the instrumentor knows they've gone wrong.
+ // This is a deliberate violation of Glean's "metric APIs must not throw" design.assert!(!crate::ipc::is_in_automation());
+ assert!(!crate::ipc::is_in_automation(), "Attempted to set string list metric in non-main process, which is forbidden. This panics in automation.");
+ // TODO: Record an error.
+ }
+ }
+ }
+
+ /// **Test-only API.**
+ ///
+ /// Get the currently stored values.
+ /// This doesn't clear the stored value.
+ ///
+ /// ## Arguments
+ ///
+ /// * `storage_name` - the storage name to look into.
+ ///
+ /// ## Return value
+ ///
+ /// Returns the stored value or `None` if nothing stored.
+ pub fn test_get_value<'a, S: Into<Option<&'a str>>>(
+ &self,
+ ping_name: S,
+ ) -> Option<Vec<String>> {
+ let ping_name = ping_name.into().map(|s| s.to_string());
+ match self {
+ StringListMetric::Parent { inner, .. } => inner.test_get_value(ping_name),
+ StringListMetric::Child(c) => {
+ panic!("Cannot get test value for {:?} in non-parent process!", c.0)
+ }
+ }
+ }
+
+ /// **Exported for test purposes.**
+ ///
+ /// Gets the number of recorded errors for the given error type.
+ ///
+ /// # Arguments
+ ///
+ /// * `error` - The type of error
+ /// * `ping_name` - represents the optional name of the ping to retrieve the
+ /// metric for. Defaults to the first value in `send_in_pings`.
+ ///
+ /// # Returns
+ ///
+ /// The number of errors recorded.
+ pub fn test_get_num_recorded_errors(&self, error: glean::ErrorType) -> i32 {
+ match self {
+ StringListMetric::Parent { inner, .. } => inner.test_get_num_recorded_errors(error),
+ StringListMetric::Child(c) => panic!(
+ "Cannot get the number of recorded errors for {:?} in non-parent process!",
+ c.0
+ ),
+ }
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use crate::{common_test::*, ipc, metrics};
+
+ #[test]
+ #[ignore] // TODO: Enable them back when bug 1677454 lands.
+ fn sets_string_list_value() {
+ let _lock = lock_test();
+
+ let metric = &metrics::test_only_ipc::a_string_list;
+
+ metric.set(vec!["test_string_value".to_string()]);
+ metric.add("another test value");
+
+ assert_eq!(
+ vec!["test_string_value", "another test value"],
+ metric.test_get_value("store1").unwrap()
+ );
+ }
+
+ #[test]
+ #[ignore] // TODO: Enable them back when bug 1677454 lands.
+ fn string_list_ipc() {
+ // StringListMetric supports IPC only for `add`, not `set`.
+ let _lock = lock_test();
+
+ let parent_metric = &metrics::test_only_ipc::a_string_list;
+
+ parent_metric.set(vec!["test_string_value".to_string()]);
+ parent_metric.add("another test value");
+
+ {
+ let child_metric = parent_metric.child_metric();
+
+ // scope for need_ipc RAII
+ let _raii = ipc::test_set_need_ipc(true);
+
+ // Recording APIs do not panic, even when they don't work.
+ child_metric.set(vec!["not gonna be set".to_string()]);
+
+ child_metric.add("child_value");
+ assert!(ipc::take_buf().unwrap().len() > 0);
+ }
+
+ // TODO: implement replay. See bug 1646165.
+ // Then perform the replay and assert we have the values from both "processes".
+ assert!(ipc::replay_from_buf(&ipc::take_buf().unwrap()).is_ok());
+ assert_eq!(
+ vec!["test_string_value", "another test value"],
+ parent_metric.test_get_value("store1").unwrap()
+ );
+ }
+}
diff --git a/toolkit/components/glean/api/src/private/text.rs b/toolkit/components/glean/api/src/private/text.rs
new file mode 100644
index 0000000000..970ac419eb
--- /dev/null
+++ b/toolkit/components/glean/api/src/private/text.rs
@@ -0,0 +1,179 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
+
+use inherent::inherent;
+use std::sync::Arc;
+
+use super::{CommonMetricData, MetricId};
+use crate::ipc::need_ipc;
+
+/// A text metric.
+///
+/// Record a string value with arbitrary content. Supports non-ASCII
+/// characters.
+///
+/// # Example
+///
+/// The following piece of code will be generated by `glean_parser`:
+///
+/// ```rust,ignore
+/// use glean::metrics::{TextMetric, CommonMetricData, Lifetime};
+/// use once_cell::sync::Lazy;
+///
+/// mod browser {
+/// pub static bread_recipe: Lazy<TextMetric> = Lazy::new(|| TextMetric::new(CommonMetricData {
+/// name: "bread_recipe".into(),
+/// category: "browser".into(),
+/// lifetime: Lifetime::Ping,
+/// disabled: false,
+/// dynamic_label: None
+/// }));
+/// }
+/// ```
+///
+/// It can then be used with:
+///
+/// ```rust,ignore
+/// browser::bread_recipe.set("The 'baguette de tradition française' is made from wheat flour, water, yeast, and common salt. It may contain up to 2% broad bean flour, up to 0.5% soya flour, and up to 0.3% wheat malt flour.");
+/// ```
+
+#[derive(Clone)]
+pub enum TextMetric {
+ Parent(Arc<glean::private::TextMetric>),
+ Child(TextMetricIpc),
+}
+
+#[derive(Clone, Debug)]
+pub struct TextMetricIpc;
+
+impl TextMetric {
+ /// Create a new text metric.
+ pub fn new(_id: MetricId, meta: CommonMetricData) -> Self {
+ if need_ipc() {
+ TextMetric::Child(TextMetricIpc)
+ } else {
+ TextMetric::Parent(Arc::new(glean::private::TextMetric::new(meta)))
+ }
+ }
+
+ #[cfg(test)]
+ pub(crate) fn child_metric(&self) -> Self {
+ match self {
+ TextMetric::Parent(_) => TextMetric::Child(TextMetricIpc),
+ TextMetric::Child(_) => panic!("Can't get a child metric from a child process"),
+ }
+ }
+}
+
+#[inherent]
+impl glean::traits::Text for TextMetric {
+ /// Sets to the specified value.
+ ///
+ /// # Arguments
+ ///
+ /// * `value` - The text to set the metric to.
+ pub fn set<S: Into<std::string::String>>(&self, value: S) {
+ match self {
+ TextMetric::Parent(p) => {
+ p.set(value.into());
+ }
+ TextMetric::Child(_) => {
+ log::error!("Unable to set text metric in non-main process. This operation will be ignored.");
+ // If we're in automation we can panic so the instrumentor knows they've gone wrong.
+ // This is a deliberate violation of Glean's "metric APIs must not throw" design.
+ assert!(!crate::ipc::is_in_automation(), "Attempted to set text metric in non-main process, which is forbidden. This panics in automation.");
+ }
+ }
+ }
+
+ /// **Exported for test purposes.**
+ ///
+ /// Gets the currently stored value as a string.
+ ///
+ /// This doesn't clear the stored value.
+ ///
+ /// # Arguments
+ ///
+ /// * `ping_name` - represents the optional name of the ping to retrieve the
+ /// metric for. Defaults to the first value in `send_in_pings`.
+ pub fn test_get_value<'a, S: Into<Option<&'a str>>>(
+ &self,
+ ping_name: S,
+ ) -> Option<std::string::String> {
+ let ping_name = ping_name.into().map(|s| s.to_string());
+ match self {
+ TextMetric::Parent(p) => p.test_get_value(ping_name),
+ TextMetric::Child(_) => {
+ panic!("Cannot get test value for text metric in non-main process!")
+ }
+ }
+ }
+
+ /// **Exported for test purposes.**
+ ///
+ /// Gets the number of recorded errors for the given metric and error type.
+ ///
+ /// # Arguments
+ ///
+ /// * `error` - The type of error
+ /// * `ping_name` - represents the optional name of the ping to retrieve the
+ /// metric for. Defaults to the first value in `send_in_pings`.
+ ///
+ /// # Returns
+ ///
+ /// The number of errors reported.
+ pub fn test_get_num_recorded_errors(&self, error: glean::ErrorType) -> i32 {
+ match self {
+ TextMetric::Parent(p) => p.test_get_num_recorded_errors(error),
+ TextMetric::Child(_) => panic!(
+ "Cannot get the number of recorded errors for text metric in non-main process!"
+ ),
+ }
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use crate::{common_test::*, ipc, metrics};
+
+ #[test]
+ fn sets_text_value() {
+ let _lock = lock_test();
+
+ let metric = &metrics::test_only_ipc::a_text;
+
+ metric.set("test_text_value");
+
+ assert_eq!("test_text_value", metric.test_get_value("store1").unwrap());
+ }
+
+ #[test]
+ fn text_ipc() {
+ // TextMetric doesn't support IPC.
+ let _lock = lock_test();
+
+ let parent_metric = &metrics::test_only_ipc::a_text;
+
+ parent_metric.set("test_parent_value");
+
+ {
+ let child_metric = parent_metric.child_metric();
+
+ let _raii = ipc::test_set_need_ipc(true);
+
+ // Instrumentation calls do not panic.
+ child_metric.set("test_child_value");
+
+ // (They also shouldn't do anything,
+ // but that's not something we can inspect in this test)
+ }
+
+ assert!(ipc::replay_from_buf(&ipc::take_buf().unwrap()).is_ok());
+
+ assert!(
+ "test_parent_value" == parent_metric.test_get_value("store1").unwrap(),
+ "Text metrics should only work in the parent process"
+ );
+ }
+}
diff --git a/toolkit/components/glean/api/src/private/timespan.rs b/toolkit/components/glean/api/src/private/timespan.rs
new file mode 100644
index 0000000000..f817d6996f
--- /dev/null
+++ b/toolkit/components/glean/api/src/private/timespan.rs
@@ -0,0 +1,180 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
+
+use inherent::inherent;
+
+use super::{CommonMetricData, MetricId, TimeUnit};
+use std::convert::TryInto;
+use std::time::Duration;
+
+use glean::traits::Timespan;
+
+use crate::ipc::need_ipc;
+
+/// A timespan metric.
+///
+/// Timespans are used to make a measurement of how much time is spent in a particular task.
+pub enum TimespanMetric {
+ Parent(glean::private::TimespanMetric, TimeUnit),
+ Child,
+}
+
+impl TimespanMetric {
+ /// Create a new timespan metric.
+ pub fn new(_id: MetricId, meta: CommonMetricData, time_unit: TimeUnit) -> Self {
+ if need_ipc() {
+ TimespanMetric::Child
+ } else {
+ TimespanMetric::Parent(
+ glean::private::TimespanMetric::new(meta, time_unit),
+ time_unit,
+ )
+ }
+ }
+
+ /// Only to be called from the MLA FFI.
+ /// If you don't know what that is, don't call this.
+ pub fn set_raw_unitless(&self, duration: u64) {
+ match self {
+ TimespanMetric::Parent(p, time_unit) => {
+ p.set_raw(Duration::from_nanos(time_unit.as_nanos(duration)));
+ }
+ TimespanMetric::Child => {
+ log::error!(
+ "Unable to set_raw_unitless on timespan in non-main process. This operation will be ignored."
+ );
+ // If we're in automation we can panic so the instrumentor knows they've gone wrong.
+ // This is a deliberate violation of Glean's "metric APIs must not throw" design.
+ assert!(!crate::ipc::is_in_automation(), "Attempted to set_raw_unitless on timespan metric in non-main process, which is forbidden. This panics in automation.");
+ // TODO: Record an error. bug 1704504.
+ }
+ }
+ }
+}
+
+#[inherent]
+impl Timespan for TimespanMetric {
+ pub fn start(&self) {
+ match self {
+ TimespanMetric::Parent(p, _) => p.start(),
+ TimespanMetric::Child => {
+ log::error!("Unable to start timespan metric in non-main process. This operation will be ignored.");
+ // If we're in automation we can panic so the instrumentor knows they've gone wrong.
+ // This is a deliberate violation of Glean's "metric APIs must not throw" design.assert!(!crate::ipc::is_in_automation());
+ assert!(!crate::ipc::is_in_automation(), "Attempted to start timespan metric in non-main process, which is forbidden. This panics in automation.");
+ // TODO: Record an error. bug 1704504.
+ }
+ }
+ }
+
+ pub fn stop(&self) {
+ match self {
+ TimespanMetric::Parent(p, _) => p.stop(),
+ TimespanMetric::Child => {
+ log::error!("Unable to stop timespan metric in non-main process. This operation will be ignored.");
+ // If we're in automation we can panic so the instrumentor knows they've gone wrong.
+ // This is a deliberate violation of Glean's "metric APIs must not throw" design.assert!(!crate::ipc::is_in_automation());
+ assert!(!crate::ipc::is_in_automation(), "Attempted to stop timespan metric in non-main process, which is forbidden. This panics in automation.");
+ // TODO: Record an error. bug 1704504.
+ }
+ }
+ }
+
+ pub fn cancel(&self) {
+ match self {
+ TimespanMetric::Parent(p, _) => p.cancel(),
+ TimespanMetric::Child => {
+ log::error!("Unable to cancel timespan metric in non-main process. This operation will be ignored.");
+ // If we're in automation we can panic so the instrumentor knows they've gone wrong.
+ // This is a deliberate violation of Glean's "metric APIs must not throw" design.assert!(!crate::ipc::is_in_automation());
+ assert!(!crate::ipc::is_in_automation(), "Attempted to cancel timespan metric in non-main process, which is forbidden. This panics in automation.");
+ // TODO: Record an error. bug 1704504.
+ }
+ }
+ }
+
+ pub fn set_raw(&self, elapsed: Duration) {
+ let elapsed = elapsed.as_nanos().try_into().unwrap_or(i64::MAX);
+ match self {
+ TimespanMetric::Parent(p, _) => p.set_raw_nanos(elapsed),
+ TimespanMetric::Child => {
+ log::error!("Unable to set_raw on timespan in non-main process. This operation will be ignored.");
+ // If we're in automation we can panic so the instrumentor knows they've gone wrong.
+ // This is a deliberate violation of Glean's "metric APIs must not throw" design.assert!(!crate::ipc::is_in_automation());
+ assert!(!crate::ipc::is_in_automation(), "Attempted to set_raw on timespan metric in non-main process, which is forbidden. This panics in automation.");
+ // TODO: Record an error. bug 1704504.
+ }
+ }
+ }
+
+ pub fn test_get_value<'a, S: Into<Option<&'a str>>>(&self, ping_name: S) -> Option<u64> {
+ let ping_name = ping_name.into().map(|s| s.to_string());
+ match self {
+ // Conversion is ok here:
+ // Timespans are really tricky to set to excessive values with the pleasant APIs.
+ TimespanMetric::Parent(p, _) => p.test_get_value(ping_name).map(|i| i as u64),
+ TimespanMetric::Child => {
+ panic!("Cannot get test value for in non-main process!");
+ }
+ }
+ }
+
+ pub fn test_get_num_recorded_errors(&self, error: glean::ErrorType) -> i32 {
+ match self {
+ TimespanMetric::Parent(p, _) => p.test_get_num_recorded_errors(error),
+ TimespanMetric::Child => {
+ panic!("Cannot get the number of recorded errors for timespan metric in non-main process!");
+ }
+ }
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+ use crate::{common_test::*, ipc, metrics};
+
+ #[test]
+ fn smoke_test_timespan() {
+ let _lock = lock_test();
+
+ let metric = TimespanMetric::new(
+ 0.into(),
+ CommonMetricData {
+ name: "timespan_metric".into(),
+ category: "telemetry".into(),
+ send_in_pings: vec!["store1".into()],
+ disabled: false,
+ ..Default::default()
+ },
+ TimeUnit::Nanosecond,
+ );
+
+ metric.start();
+ // Stopping right away might not give us data, if the underlying clock source is not precise
+ // enough.
+ // So let's cancel and make sure nothing blows up.
+ metric.cancel();
+
+ assert_eq!(None, metric.test_get_value("store1"));
+ }
+
+ #[test]
+ fn timespan_ipc() {
+ let _lock = lock_test();
+ let _raii = ipc::test_set_need_ipc(true);
+
+ let child_metric = &metrics::test_only::can_we_time_it;
+
+ // Instrumentation calls do not panic.
+ child_metric.start();
+ // Stopping right away might not give us data,
+ // if the underlying clock source is not precise enough.
+ // So let's cancel and make sure nothing blows up.
+ child_metric.cancel();
+
+ // (They also shouldn't do anything,
+ // but that's not something we can inspect in this test)
+ }
+}
diff --git a/toolkit/components/glean/api/src/private/timing_distribution.rs b/toolkit/components/glean/api/src/private/timing_distribution.rs
new file mode 100644
index 0000000000..0ab25cc900
--- /dev/null
+++ b/toolkit/components/glean/api/src/private/timing_distribution.rs
@@ -0,0 +1,487 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
+
+use inherent::inherent;
+use std::collections::HashMap;
+use std::convert::TryInto;
+use std::sync::{
+ atomic::{AtomicUsize, Ordering},
+ RwLock,
+};
+use std::time::{Duration, Instant};
+
+use super::{CommonMetricData, MetricId, TimeUnit};
+use glean::{DistributionData, ErrorType, TimerId};
+
+use crate::ipc::{need_ipc, with_ipc_payload};
+use glean::traits::TimingDistribution;
+
+/// A timing distribution metric.
+///
+/// Timing distributions are used to accumulate and store time measurements for analyzing distributions of the timing data.
+pub enum TimingDistributionMetric {
+ Parent {
+ /// The metric's ID.
+ ///
+ /// No longer test-only, is also used for GIFFT.
+ id: MetricId,
+ inner: glean::private::TimingDistributionMetric,
+ },
+ Child(TimingDistributionMetricIpc),
+}
+#[derive(Debug)]
+pub struct TimingDistributionMetricIpc {
+ metric_id: MetricId,
+ next_timer_id: AtomicUsize,
+ instants: RwLock<HashMap<u64, Instant>>,
+}
+
+impl TimingDistributionMetric {
+ /// Create a new timing distribution metric.
+ pub fn new(id: MetricId, meta: CommonMetricData, time_unit: TimeUnit) -> Self {
+ if need_ipc() {
+ TimingDistributionMetric::Child(TimingDistributionMetricIpc {
+ metric_id: id,
+ next_timer_id: AtomicUsize::new(0),
+ instants: RwLock::new(HashMap::new()),
+ })
+ } else {
+ let inner = glean::private::TimingDistributionMetric::new(meta, time_unit);
+ TimingDistributionMetric::Parent { id, inner }
+ }
+ }
+
+ #[cfg(test)]
+ pub(crate) fn child_metric(&self) -> Self {
+ match self {
+ TimingDistributionMetric::Parent { id, .. } => {
+ TimingDistributionMetric::Child(TimingDistributionMetricIpc {
+ metric_id: *id,
+ next_timer_id: AtomicUsize::new(0),
+ instants: RwLock::new(HashMap::new()),
+ })
+ }
+ TimingDistributionMetric::Child(_) => {
+ panic!("Can't get a child metric from a child metric")
+ }
+ }
+ }
+
+ /// Accumulates a time duration sample for the provided metric.
+ ///
+ /// Adds a count to the corresponding bucket in the timing distribution.
+ /// Saturates at u64::MAX nanoseconds.
+ ///
+ /// Prefer start() and stop_and_accumulate() where possible.
+ ///
+ /// Users of this API are responsible for ensuring the timing source used
+ /// to calculate the duration is monotonic and consistent across platforms.
+ ///
+ /// # Arguments
+ ///
+ /// * `duration` - The [`Duration`] of the accumulated sample.
+ pub fn accumulate_raw_duration(&self, duration: Duration) {
+ let sample = duration.as_nanos().try_into().unwrap_or_else(|_| {
+ // TODO: Instrument this error
+ log::warn!(
+ "Elapsed nanoseconds larger than fits into 64-bytes. Saturating at u64::MAX."
+ );
+ u64::MAX
+ });
+ // May be unused in builds without gecko.
+ let _sample_ms = duration.as_millis().try_into().unwrap_or_else(|_| {
+ // TODO: Instrument this error
+ log::warn!(
+ "Elapsed milliseconds larger than fits into 32-bytes. Saturating at u32::MAX."
+ );
+ u32::MAX
+ });
+ match self {
+ TimingDistributionMetric::Parent {
+ id: _metric_id,
+ inner,
+ } => {
+ #[cfg(feature = "with_gecko")]
+ {
+ extern "C" {
+ fn GIFFT_TimingDistributionAccumulateRawMillis(metric_id: u32, sample: u32);
+ }
+ // SAFETY: using only primitives, no return value.
+ unsafe {
+ GIFFT_TimingDistributionAccumulateRawMillis(_metric_id.0, _sample_ms);
+ }
+ }
+ inner.accumulate_raw_samples_nanos(vec![sample]);
+ }
+ TimingDistributionMetric::Child(c) => {
+ #[cfg(feature = "with_gecko")]
+ {
+ extern "C" {
+ fn GIFFT_TimingDistributionAccumulateRawMillis(metric_id: u32, sample: u32);
+ }
+ // SAFETY: using only primitives, no return value.
+ unsafe {
+ GIFFT_TimingDistributionAccumulateRawMillis(c.metric_id.0, _sample_ms);
+ }
+ }
+ with_ipc_payload(move |payload| {
+ if let Some(v) = payload.timing_samples.get_mut(&c.metric_id) {
+ v.push(sample);
+ } else {
+ payload.timing_samples.insert(c.metric_id, vec![sample]);
+ }
+ });
+ }
+ }
+ }
+}
+
+#[inherent]
+impl TimingDistribution for TimingDistributionMetric {
+ /// Starts tracking time for the provided metric.
+ ///
+ /// This records an error if it’s already tracking time (i.e.
+ /// [`start`](TimingDistribution::start) was already called with no corresponding
+ /// [`stop_and_accumulate`](TimingDistribution::stop_and_accumulate)): in that case the
+ /// original start time will be preserved.
+ ///
+ /// # Returns
+ ///
+ /// A unique [`TimerId`] for the new timer.
+ pub fn start(&self) -> TimerId {
+ match self {
+ TimingDistributionMetric::Parent { id: _id, inner } => {
+ let timer_id = inner.start();
+ #[cfg(feature = "with_gecko")]
+ {
+ extern "C" {
+ fn GIFFT_TimingDistributionStart(metric_id: u32, timer_id: u64);
+ }
+ // SAFETY: using only primitives, no return value.
+ unsafe {
+ GIFFT_TimingDistributionStart(_id.0, timer_id.id);
+ }
+ }
+ timer_id.into()
+ }
+ TimingDistributionMetric::Child(c) => {
+ // There is no glean-core on this process to give us a TimerId,
+ // so we'll have to make our own and do our own bookkeeping.
+ let id = c
+ .next_timer_id
+ .fetch_add(1, Ordering::SeqCst)
+ .try_into()
+ .unwrap();
+ let mut map = c
+ .instants
+ .write()
+ .expect("lock of instants map was poisoned");
+ if let Some(_v) = map.insert(id, Instant::now()) {
+ // TODO: report an error and find a different TimerId.
+ }
+ #[cfg(feature = "with_gecko")]
+ {
+ extern "C" {
+ fn GIFFT_TimingDistributionStart(metric_id: u32, timer_id: u64);
+ }
+ // SAFETY: using only primitives, no return value.
+ unsafe {
+ GIFFT_TimingDistributionStart(c.metric_id.0, id);
+ }
+ }
+ id.into()
+ }
+ }
+ }
+
+ /// Stops tracking time for the provided metric and associated timer id.
+ ///
+ /// Adds a count to the corresponding bucket in the timing distribution.
+ /// This will record an error if no [`start`](TimingDistribution::start) was
+ /// called.
+ ///
+ /// # Arguments
+ ///
+ /// * `id` - The [`TimerId`] to associate with this timing. This allows
+ /// for concurrent timing of events associated with different ids to the
+ /// same timespan metric.
+ pub fn stop_and_accumulate(&self, id: TimerId) {
+ match self {
+ TimingDistributionMetric::Parent {
+ id: _metric_id,
+ inner,
+ } => {
+ #[cfg(feature = "with_gecko")]
+ {
+ extern "C" {
+ fn GIFFT_TimingDistributionStopAndAccumulate(metric_id: u32, timer_id: u64);
+ }
+ // SAFETY: using only primitives, no return value.
+ unsafe {
+ GIFFT_TimingDistributionStopAndAccumulate(_metric_id.0, id.id);
+ }
+ }
+ inner.stop_and_accumulate(id);
+ }
+ TimingDistributionMetric::Child(c) => {
+ #[cfg(feature = "with_gecko")]
+ {
+ extern "C" {
+ fn GIFFT_TimingDistributionStopAndAccumulate(metric_id: u32, timer_id: u64);
+ }
+ // SAFETY: using only primitives, no return value.
+ unsafe {
+ GIFFT_TimingDistributionStopAndAccumulate(c.metric_id.0, id.id);
+ }
+ }
+ let mut map = c
+ .instants
+ .write()
+ .expect("Write lock must've been poisoned.");
+ if let Some(start) = map.remove(&id.id) {
+ let now = Instant::now();
+ let sample = now
+ .checked_duration_since(start)
+ .map(|s| s.as_nanos().try_into());
+ let sample = match sample {
+ Some(Ok(sample)) => sample,
+ Some(Err(_)) => {
+ log::warn!("Elapsed time larger than fits into 64-bytes. Saturating at u64::MAX.");
+ u64::MAX
+ }
+ None => {
+ log::warn!("Time went backwards. Not recording.");
+ // TODO: report an error (timer id for stop was started, but time went backwards).
+ return;
+ }
+ };
+ with_ipc_payload(move |payload| {
+ if let Some(v) = payload.timing_samples.get_mut(&c.metric_id) {
+ v.push(sample);
+ } else {
+ payload.timing_samples.insert(c.metric_id, vec![sample]);
+ }
+ });
+ } else {
+ // TODO: report an error (timer id for stop wasn't started).
+ }
+ }
+ }
+ }
+
+ /// Aborts a previous [`start`](TimingDistribution::start) call. No
+ /// error is recorded if no [`start`](TimingDistribution::start) was
+ /// called.
+ ///
+ /// # Arguments
+ ///
+ /// * `id` - The [`TimerId`] to associate with this timing. This allows
+ /// for concurrent timing of events associated with different ids to the
+ /// same timing distribution metric.
+ pub fn cancel(&self, id: TimerId) {
+ match self {
+ TimingDistributionMetric::Parent {
+ id: _metric_id,
+ inner,
+ } => {
+ #[cfg(feature = "with_gecko")]
+ {
+ extern "C" {
+ fn GIFFT_TimingDistributionCancel(metric_id: u32, timer_id: u64);
+ }
+ // SAFETY: using only primitives, no return value.
+ unsafe {
+ GIFFT_TimingDistributionCancel(_metric_id.0, id.id);
+ }
+ }
+ inner.cancel(id);
+ }
+ TimingDistributionMetric::Child(c) => {
+ let mut map = c
+ .instants
+ .write()
+ .expect("Write lock must've been poisoned.");
+ if map.remove(&id.id).is_none() {
+ // TODO: report an error (cancelled a non-started id).
+ }
+ #[cfg(feature = "with_gecko")]
+ {
+ extern "C" {
+ fn GIFFT_TimingDistributionCancel(metric_id: u32, timer_id: u64);
+ }
+ // SAFETY: using only primitives, no return value.
+ unsafe {
+ GIFFT_TimingDistributionCancel(c.metric_id.0, id.id);
+ }
+ }
+ }
+ }
+ }
+
+ /// Accumulates the provided signed samples in the metric.
+ ///
+ /// This is required so that the platform-specific code can provide us with
+ /// 64 bit signed integers if no `u64` comparable type is available. This
+ /// will take care of filtering and reporting errors for any provided negative
+ /// sample.
+ ///
+ /// Please note that this assumes that the provided samples are already in
+ /// the "unit" declared by the instance of the metric type (e.g. if the
+ /// instance this method was called on is using [`crate::TimeUnit::Second`], then
+ /// `samples` are assumed to be in that unit).
+ ///
+ /// # Arguments
+ ///
+ /// * `samples` - The vector holding the samples to be recorded by the metric.
+ ///
+ /// ## Notes
+ ///
+ /// Discards any negative value in `samples` and report an [`ErrorType::InvalidValue`]
+ /// for each of them. Reports an [`ErrorType::InvalidOverflow`] error for samples that
+ /// are longer than `MAX_SAMPLE_TIME`.
+ pub fn accumulate_samples(&self, samples: Vec<i64>) {
+ match self {
+ TimingDistributionMetric::Parent { id: _id, inner } => {
+ inner.accumulate_samples(samples)
+ }
+ TimingDistributionMetric::Child(_c) => {
+ // TODO: Instrument this error
+ log::error!("Can't record samples for a timing distribution from a child metric");
+ }
+ }
+ }
+
+ /// Accumulates the provided samples in the metric.
+ ///
+ /// # Arguments
+ ///
+ /// * `samples` - A list of samples recorded by the metric.
+ /// Samples must be in nanoseconds.
+ /// ## Notes
+ ///
+ /// Reports an [`ErrorType::InvalidOverflow`] error for samples that
+ /// are longer than `MAX_SAMPLE_TIME`.
+ pub fn accumulate_raw_samples_nanos(&self, samples: Vec<u64>) {
+ match self {
+ TimingDistributionMetric::Parent { id: _id, inner } => {
+ inner.accumulate_raw_samples_nanos(samples)
+ }
+ TimingDistributionMetric::Child(_c) => {
+ // TODO: Instrument this error
+ log::error!("Can't record samples for a timing distribution from a child metric");
+ }
+ }
+ }
+
+ /// **Exported for test purposes.**
+ ///
+ /// Gets the currently stored value of the metric.
+ ///
+ /// This doesn't clear the stored value.
+ ///
+ /// # Arguments
+ ///
+ /// * `ping_name` - represents the optional name of the ping to retrieve the
+ /// metric for. Defaults to the first value in `send_in_pings`.
+ pub fn test_get_value<'a, S: Into<Option<&'a str>>>(
+ &self,
+ ping_name: S,
+ ) -> Option<DistributionData> {
+ let ping_name = ping_name.into().map(|s| s.to_string());
+ match self {
+ TimingDistributionMetric::Parent { inner, .. } => inner.test_get_value(ping_name),
+ TimingDistributionMetric::Child(c) => {
+ panic!("Cannot get test value for {:?} in non-parent process!", c)
+ }
+ }
+ }
+
+ /// **Exported for test purposes.**
+ ///
+ /// Gets the number of recorded errors for the given error type.
+ ///
+ /// # Arguments
+ ///
+ /// * `error` - The type of error
+ /// * `ping_name` - represents the optional name of the ping to retrieve the
+ /// metric for. Defaults to the first value in `send_in_pings`.
+ ///
+ /// # Returns
+ ///
+ /// The number of errors recorded.
+ pub fn test_get_num_recorded_errors(&self, error: ErrorType) -> i32 {
+ match self {
+ TimingDistributionMetric::Parent { inner, .. } => {
+ inner.test_get_num_recorded_errors(error)
+ }
+ TimingDistributionMetric::Child(c) => panic!(
+ "Cannot get number of recorded errors for {:?} in non-parent process!",
+ c
+ ),
+ }
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use crate::{common_test::*, ipc, metrics};
+
+ #[test]
+ fn smoke_test_timing_distribution() {
+ let _lock = lock_test();
+
+ let metric = &metrics::test_only_ipc::a_timing_dist;
+
+ let id = metric.start();
+ // Stopping right away might not give us data, if the underlying clock source is not precise
+ // enough.
+ // So let's cancel and make sure nothing blows up.
+ metric.cancel(id);
+
+ // We can't inspect the values yet.
+ assert!(metric.test_get_value("store1").is_none());
+ }
+
+ #[test]
+ fn timing_distribution_child() {
+ let _lock = lock_test();
+
+ let parent_metric = &metrics::test_only_ipc::a_timing_dist;
+ let id = parent_metric.start();
+ std::thread::sleep(std::time::Duration::from_millis(10));
+ parent_metric.stop_and_accumulate(id);
+
+ {
+ let child_metric = parent_metric.child_metric();
+
+ // scope for need_ipc RAII
+ let _raii = ipc::test_set_need_ipc(true);
+
+ let id = child_metric.start();
+ let id2 = child_metric.start();
+ assert_ne!(id, id2);
+ std::thread::sleep(std::time::Duration::from_millis(10));
+ child_metric.stop_and_accumulate(id);
+
+ child_metric.cancel(id2);
+ }
+
+ let buf = ipc::take_buf().unwrap();
+ assert!(buf.len() > 0);
+ assert!(ipc::replay_from_buf(&buf).is_ok());
+
+ let data = parent_metric
+ .test_get_value("store1")
+ .expect("should have some data");
+
+ // No guarantees from timers means no guarantees on buckets.
+ // But we can guarantee it's only two samples.
+ assert_eq!(
+ 2,
+ data.values.values().fold(0, |acc, count| acc + count),
+ "record 2 values, one parent, one child measurement"
+ );
+ assert!(0 < data.sum, "record some time");
+ }
+}
diff --git a/toolkit/components/glean/api/src/private/url.rs b/toolkit/components/glean/api/src/private/url.rs
new file mode 100644
index 0000000000..09f21dad5e
--- /dev/null
+++ b/toolkit/components/glean/api/src/private/url.rs
@@ -0,0 +1,129 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
+
+use inherent::inherent;
+
+use super::{CommonMetricData, MetricId};
+
+use crate::ipc::need_ipc;
+
+/// Developer-facing API for recording URL metrics.
+///
+/// Instances of this class type are automatically generated by the parsers
+/// at build time, allowing developers to record values that were previously
+/// registered in the metrics.yaml file.
+#[derive(Clone)]
+pub enum UrlMetric {
+ Parent(glean::private::UrlMetric),
+ Child(UrlMetricIpc),
+}
+#[derive(Clone, Debug)]
+pub struct UrlMetricIpc;
+
+impl UrlMetric {
+ /// Create a new Url metric.
+ pub fn new(_id: MetricId, meta: CommonMetricData) -> Self {
+ if need_ipc() {
+ UrlMetric::Child(UrlMetricIpc)
+ } else {
+ UrlMetric::Parent(glean::private::UrlMetric::new(meta))
+ }
+ }
+
+ #[cfg(test)]
+ pub(crate) fn child_metric(&self) -> Self {
+ match self {
+ UrlMetric::Parent(_) => UrlMetric::Child(UrlMetricIpc),
+ UrlMetric::Child(_) => panic!("Can't get a child metric from a child metric"),
+ }
+ }
+}
+
+#[inherent]
+impl glean::traits::Url for UrlMetric {
+ pub fn set<S: Into<std::string::String>>(&self, value: S) {
+ match self {
+ UrlMetric::Parent(p) => p.set(value),
+ UrlMetric::Child(_) => {
+ log::error!(
+ "Unable to set Url metric in non-main process. This operation will be ignored."
+ );
+ // If we're in automation we can panic so the instrumentor knows they've gone wrong.
+ // This is a deliberate violation of Glean's "metric APIs must not throw" design.
+ assert!(!crate::ipc::is_in_automation(), "Attempted to set URL metric in non-main process, which is forbidden. This panics in automation.");
+ // TODO: Record an error.
+ }
+ };
+ }
+
+ pub fn test_get_value<'a, S: Into<Option<&'a str>>>(
+ &self,
+ ping_name: S,
+ ) -> Option<std::string::String> {
+ let ping_name = ping_name.into().map(|s| s.to_string());
+ match self {
+ UrlMetric::Parent(p) => p.test_get_value(ping_name),
+ UrlMetric::Child(_) => {
+ panic!("Cannot get test value for Url metric in non-main process!")
+ }
+ }
+ }
+
+ pub fn test_get_num_recorded_errors(&self, error: glean::ErrorType) -> i32 {
+ match self {
+ UrlMetric::Parent(p) => p.test_get_num_recorded_errors(error),
+ UrlMetric::Child(_) => panic!(
+ "Cannot get the number of recorded errors for Url metric in non-main process!"
+ ),
+ }
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use crate::{common_test::*, ipc, metrics};
+
+ #[test]
+ fn sets_url_value() {
+ let _lock = lock_test();
+
+ let metric = &metrics::test_only_ipc::a_url;
+
+ metric.set("https://example.com");
+
+ assert_eq!(
+ "https://example.com",
+ metric.test_get_value("store1").unwrap()
+ );
+ }
+
+ #[test]
+ fn url_ipc() {
+ // UrlMetric doesn't support IPC.
+ let _lock = lock_test();
+
+ let parent_metric = &metrics::test_only_ipc::a_url;
+
+ parent_metric.set("https://example.com/parent");
+
+ {
+ let child_metric = parent_metric.child_metric();
+
+ let _raii = ipc::test_set_need_ipc(true);
+
+ // Instrumentation calls do not panic.
+ child_metric.set("https://example.com/child");
+
+ // (They also shouldn't do anything,
+ // but that's not something we can inspect in this test)
+ }
+
+ assert!(ipc::replay_from_buf(&ipc::take_buf().unwrap()).is_ok());
+
+ assert!(
+ "https://example.com/parent" == parent_metric.test_get_value("store1").unwrap(),
+ "Url metrics should only work in the parent process"
+ );
+ }
+}
diff --git a/toolkit/components/glean/api/src/private/uuid.rs b/toolkit/components/glean/api/src/private/uuid.rs
new file mode 100644
index 0000000000..101a1a9875
--- /dev/null
+++ b/toolkit/components/glean/api/src/private/uuid.rs
@@ -0,0 +1,171 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
+
+use inherent::inherent;
+
+use uuid::Uuid;
+
+use super::{CommonMetricData, MetricId};
+
+use crate::ipc::need_ipc;
+
+/// A UUID metric.
+///
+/// Stores UUID values.
+pub enum UuidMetric {
+ Parent(glean::private::UuidMetric),
+ Child(UuidMetricIpc),
+}
+
+#[derive(Debug)]
+pub struct UuidMetricIpc;
+
+impl UuidMetric {
+ /// Create a new UUID metric.
+ pub fn new(_id: MetricId, meta: CommonMetricData) -> Self {
+ if need_ipc() {
+ UuidMetric::Child(UuidMetricIpc)
+ } else {
+ UuidMetric::Parent(glean::private::UuidMetric::new(meta))
+ }
+ }
+
+ #[cfg(test)]
+ pub(crate) fn child_metric(&self) -> Self {
+ match self {
+ UuidMetric::Parent(_) => UuidMetric::Child(UuidMetricIpc),
+ UuidMetric::Child(_) => panic!("Can't get a child metric from a child metric"),
+ }
+ }
+}
+
+#[inherent]
+impl glean::traits::Uuid for UuidMetric {
+ /// Set to the specified value.
+ ///
+ /// ## Arguments
+ ///
+ /// * `value` - The UUID to set the metric to.
+ pub fn set(&self, value: Uuid) {
+ match self {
+ UuidMetric::Parent(p) => p.set(value.to_string()),
+ UuidMetric::Child(_c) => {
+ log::error!("Unable to set the uuid metric in non-main process. This operation will be ignored.");
+ // If we're in automation we can panic so the instrumentor knows they've gone wrong.
+ // This is a deliberate violation of Glean's "metric APIs must not throw" design.
+ assert!(!crate::ipc::is_in_automation(), "Attempted to set uuid metric in non-main process, which is forbidden. This panics in automation.");
+ // TODO: Record an error.
+ }
+ };
+ }
+
+ /// Generate a new random UUID and set the metric to it.
+ ///
+ /// ## Return value
+ ///
+ /// Returns the stored UUID value or `Uuid::nil` if called from
+ /// a non-main process.
+ pub fn generate_and_set(&self) -> Uuid {
+ match self {
+ UuidMetric::Parent(p) => Uuid::parse_str(&p.generate_and_set()).unwrap(),
+ UuidMetric::Child(_c) => {
+ log::error!("Unable to set the uuid metric in non-main process. This operation will be ignored.");
+ // If we're in automation we can panic so the instrumentor knows they've gone wrong.
+ // This is a deliberate violation of Glean's "metric APIs must not throw" design.
+ assert!(!crate::ipc::is_in_automation(), "Attempted to set uuid metric in non-main process, which is forbidden. This panics in automation.");
+ // TODO: Record an error.
+ Uuid::nil()
+ }
+ }
+ }
+
+ /// **Test-only API.**
+ ///
+ /// Get the stored UUID value.
+ /// This doesn't clear the stored value.
+ ///
+ /// ## Arguments
+ ///
+ /// * `storage_name` - the storage name to look into.
+ ///
+ /// ## Return value
+ ///
+ /// Returns the stored value or `None` if nothing stored.
+ pub fn test_get_value<'a, S: Into<Option<&'a str>>>(&self, storage_name: S) -> Option<Uuid> {
+ let storage_name = storage_name.into().map(|s| s.to_string());
+ match self {
+ UuidMetric::Parent(p) => p
+ .test_get_value(storage_name)
+ .and_then(|s| Uuid::parse_str(&s).ok()),
+ UuidMetric::Child(_c) => panic!("Cannot get test value for in non-main process!"),
+ }
+ }
+
+ /// **Exported for test purposes.**
+ ///
+ /// Gets the number of recorded errors for the given metric and error type.
+ ///
+ /// # Arguments
+ ///
+ /// * `error` - The type of error
+ /// * `ping_name` - represents the optional name of the ping to retrieve the
+ /// metric for. Defaults to the first value in `send_in_pings`.
+ ///
+ /// # Returns
+ ///
+ /// The number of errors reported.
+ pub fn test_get_num_recorded_errors(&self, error: glean::ErrorType) -> i32 {
+ match self {
+ UuidMetric::Parent(p) => p.test_get_num_recorded_errors(error),
+ UuidMetric::Child(_c) => {
+ panic!("Cannot get test value for UuidMetric in non-main process!")
+ }
+ }
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+ use crate::{common_test::*, ipc, metrics};
+
+ #[test]
+ fn sets_uuid_value() {
+ let _lock = lock_test();
+
+ let metric = &metrics::test_only_ipc::a_uuid;
+ let expected = Uuid::new_v4();
+ metric.set(expected.clone());
+
+ assert_eq!(expected, metric.test_get_value("store1").unwrap());
+ }
+
+ #[test]
+ fn uuid_ipc() {
+ // UuidMetric doesn't support IPC.
+ let _lock = lock_test();
+
+ let parent_metric = &metrics::test_only_ipc::a_uuid;
+ let expected = Uuid::new_v4();
+ parent_metric.set(expected.clone());
+
+ {
+ let child_metric = parent_metric.child_metric();
+
+ // Instrumentation calls do not panic.
+ child_metric.set(Uuid::new_v4());
+
+ // (They also shouldn't do anything,
+ // but that's not something we can inspect in this test)
+ }
+
+ assert!(ipc::replay_from_buf(&ipc::take_buf().unwrap()).is_ok());
+
+ assert_eq!(
+ expected,
+ parent_metric.test_get_value("store1").unwrap(),
+ "UUID metrics should only work in the parent process"
+ );
+ }
+}
diff --git a/toolkit/components/glean/bindings/Category.cpp b/toolkit/components/glean/bindings/Category.cpp
new file mode 100644
index 0000000000..1931b244a1
--- /dev/null
+++ b/toolkit/components/glean/bindings/Category.cpp
@@ -0,0 +1,75 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/GleanBinding.h"
+#include "mozilla/glean/bindings/Glean.h"
+#include "mozilla/glean/bindings/Category.h"
+#include "mozilla/glean/bindings/GleanJSMetricsLookup.h"
+#include "mozilla/glean/bindings/jog/JOG.h"
+
+namespace mozilla::glean {
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(Category, mParent)
+
+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<GleanMetric> Category::NamedGetter(const nsAString& aName,
+ bool& aFound) {
+ aFound = false;
+
+ nsCString metricName;
+ metricName.AppendASCII(mName);
+ metricName.AppendLiteral(".");
+ AppendUTF16toUTF8(aName, metricName);
+
+ Maybe<uint32_t> metricIdx = JOG::GetMetric(metricName);
+ if (metricIdx.isNothing() && !JOG::AreRuntimeMetricsComprehensive()) {
+ metricIdx = MetricByNameLookup(metricName);
+ }
+
+ if (metricIdx.isNothing()) {
+ aFound = false;
+ return nullptr;
+ }
+
+ aFound = true; // Should always be true (MOZ_ASSERT_UNREACHABLE-guarded).
+ return NewMetricFromId(metricIdx.value(), mParent);
+}
+
+bool Category::NameIsEnumerable(const nsAString& aName) { return false; }
+
+void Category::GetSupportedNames(nsTArray<nsString>& aNames) {
+ JOG::GetMetricNames(mName, aNames);
+ if (!JOG::AreRuntimeMetricsComprehensive()) {
+ for (metric_entry_t entry : sMetricByNameLookupEntries) {
+ const char* identifierBuf = GetMetricIdentifier(entry);
+ nsDependentCString identifier(identifierBuf);
+
+ // We're iterating all metrics,
+ // so we need to check for the ones in the right category.
+ //
+ // We need to ensure that we found _only_ the exact category by checking
+ // it is followed by a dot.
+ if (StringBeginsWith(identifier, mName) &&
+ identifier.CharAt(mName.Length()) == '.') {
+ const char* metricName = &identifierBuf[mName.Length() + 1];
+ aNames.AppendElement()->AssignASCII(metricName);
+ }
+ }
+ }
+}
+
+} // namespace mozilla::glean
diff --git a/toolkit/components/glean/bindings/Category.h b/toolkit/components/glean/bindings/Category.h
new file mode 100644
index 0000000000..f173912f32
--- /dev/null
+++ b/toolkit/components/glean/bindings/Category.h
@@ -0,0 +1,45 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_glean_Category_h
+#define mozilla_glean_Category_h
+
+#include "js/TypeDecls.h"
+#include "mozilla/glean/bindings/GleanMetric.h"
+#include "nsISupports.h"
+#include "nsTArrayForwardDeclare.h"
+#include "nsWrapperCache.h"
+
+namespace mozilla::glean {
+
+class Category final : public nsISupports, public nsWrapperCache {
+ public:
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(Category)
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+ nsISupports* GetParentObject() { return mParent; };
+
+ explicit Category(nsCString&& aName, nsISupports* aParent)
+ : mName(aName), mParent(aParent) {}
+
+ already_AddRefed<GleanMetric> NamedGetter(const nsAString& aName,
+ bool& aFound);
+ bool NameIsEnumerable(const nsAString& aName);
+ void GetSupportedNames(nsTArray<nsString>& aNames);
+
+ private:
+ nsCString mName;
+ nsCOMPtr<nsISupports> mParent;
+
+ 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..4f77f3a775
--- /dev/null
+++ b/toolkit/components/glean/bindings/Glean.cpp
@@ -0,0 +1,111 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/DOMJSClass.h"
+#include "mozilla/dom/GleanBinding.h"
+#include "mozilla/dom/BindingUtils.h"
+#include "mozilla/glean/bindings/Glean.h"
+#include "mozilla/glean/bindings/Category.h"
+#include "mozilla/glean/bindings/GleanJSMetricsLookup.h"
+#include "mozilla/glean/bindings/jog/jog_ffi_generated.h"
+#include "mozilla/glean/bindings/jog/JOG.h"
+#include "MainThreadUtils.h"
+#include "js/PropertyAndElement.h" // JS_DefineProperty
+
+namespace mozilla::glean {
+
+// Whether the runtime-registered metrics should be treated as comprehensive,
+// or additive. If comprehensive, a metric not registered at runtime is a
+// metric that doesn't exist. If additive, a metric not registered at runtime
+// may still exist if it was registered at compile time.
+// If we're supporting Artefact Builds, we treat them as comprehensive.
+// Threading: Must only be read or written to on the main thread.
+static bool gRuntimeMetricsComprehensive = false;
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(Glean, mParent)
+
+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>(global);
+ if (!dom::GetOrCreateDOMReflector(aCx, impl.get(), &glean)) {
+ return false;
+ }
+
+ return JS_DefineProperty(aCx, aGlobal, "Glean", glean, JSPROP_ENUMERATE);
+}
+
+already_AddRefed<Category> Glean::NamedGetter(const nsAString& aName,
+ bool& aFound) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ JOG::EnsureRuntimeMetricsRegistered();
+
+ NS_ConvertUTF16toUTF8 categoryName(aName);
+ if (JOG::HasCategory(categoryName)) {
+ aFound = true;
+ return MakeAndAddRef<Category>(std::move(categoryName), mParent);
+ }
+
+ if (gRuntimeMetricsComprehensive) {
+ // This category might be built-in, but since the runtime metrics are
+ // comprehensive, that just signals that the category was removed locally.
+ aFound = false;
+ return nullptr;
+ }
+
+ Maybe<uint32_t> categoryIdx = CategoryByNameLookup(categoryName);
+ if (categoryIdx.isNothing()) {
+ aFound = false;
+ return nullptr;
+ }
+
+ aFound = true;
+ nsDependentCString name(GetCategoryName(categoryIdx.value()));
+ return MakeAndAddRef<Category>(std::move(name), mParent);
+}
+
+bool Glean::NameIsEnumerable(const nsAString& aName) { return false; }
+
+void Glean::GetSupportedNames(nsTArray<nsString>& aNames) {
+ JOG::GetCategoryNames(aNames);
+ if (!JOG::AreRuntimeMetricsComprehensive()) {
+ for (category_entry_t entry : sCategoryByNameLookupEntries) {
+ const char* categoryName = GetCategoryName(entry);
+ aNames.AppendElement()->AssignASCII(categoryName);
+ }
+ }
+}
+
+// static
+void Glean::TestSetRuntimeMetricsComprehensive(bool aIsComprehensive) {
+ MOZ_ASSERT(NS_IsMainThread());
+ gRuntimeMetricsComprehensive = aIsComprehensive;
+}
+
+} // namespace mozilla::glean
diff --git a/toolkit/components/glean/bindings/Glean.h b/toolkit/components/glean/bindings/Glean.h
new file mode 100644
index 0000000000..d9ffc97918
--- /dev/null
+++ b/toolkit/components/glean/bindings/Glean.h
@@ -0,0 +1,57 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_glean_Glean_h
+#define mozilla_glean_Glean_h
+
+#include "js/TypeDecls.h"
+#include "nsGlobalWindowInner.h"
+#include "nsISupports.h"
+#include "nsTArrayForwardDeclare.h"
+#include "nsWrapperCache.h"
+
+namespace mozilla::glean {
+
+class Category;
+
+class Glean final : public nsISupports, public nsWrapperCache {
+ public:
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(Glean)
+
+ explicit Glean(nsIGlobalObject* aGlobal) : mParent(aGlobal) {}
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+ nsISupports* GetParentObject() { return mParent; }
+
+ static bool DefineGlean(JSContext* aCx, JS::Handle<JSObject*> aGlobal);
+
+ already_AddRefed<Category> NamedGetter(const nsAString& aName, bool& aFound);
+ bool NameIsEnumerable(const nsAString& aName);
+ void GetSupportedNames(nsTArray<nsString>& aNames);
+
+ /*
+ * Test-only method.
+ *
+ * Set whether we should treat runtime-registered metrics as the
+ * comprehensive list of all metrics, or whether compile-time-registered
+ * metrics are allowed to count too.
+ *
+ * Allows us to test Artifact Build support flexibly.
+ */
+ static void TestSetRuntimeMetricsComprehensive(bool aIsComprehensive);
+
+ private:
+ nsCOMPtr<nsISupports> mParent;
+
+ protected:
+ virtual ~Glean() = default;
+};
+
+} // namespace mozilla::glean
+
+#endif /* mozilla_glean_Glean */
diff --git a/toolkit/components/glean/bindings/GleanMetric.cpp b/toolkit/components/glean/bindings/GleanMetric.cpp
new file mode 100644
index 0000000000..cb15432913
--- /dev/null
+++ b/toolkit/components/glean/bindings/GleanMetric.cpp
@@ -0,0 +1,70 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/glean/bindings/GleanMetric.h"
+
+#include "nsThreadUtils.h"
+#include "nsWrapperCache.h"
+#include "mozilla/AppShutdown.h"
+#include "mozilla/ClearOnShutdown.h"
+
+namespace mozilla::glean {
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(GleanMetric, mParent)
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(GleanMetric)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(GleanMetric)
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(GleanMetric)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+Maybe<SubmetricToMirrorMutex::AutoLock> GetLabeledMirrorLock() {
+ static SubmetricToMirrorMutex sLabeledMirrors("sLabeledMirrors");
+ auto lock = sLabeledMirrors.Lock();
+ // GIFFT will work up to the end of AppShutdownTelemetry.
+ if (AppShutdown::IsInOrBeyond(ShutdownPhase::XPCOMWillShutdown)) {
+ return Nothing();
+ }
+ if (!*lock) {
+ *lock = MakeUnique<SubmetricToLabeledMirrorMapType>();
+ RefPtr<nsIRunnable> cleanupFn = NS_NewRunnableFunction(__func__, [&] {
+ if (AppShutdown::IsInOrBeyond(ShutdownPhase::XPCOMWillShutdown)) {
+ auto lock = sLabeledMirrors.Lock();
+ *lock = nullptr; // deletes, see UniquePtr.h
+ return;
+ }
+ RunOnShutdown(
+ [&] {
+ auto lock = sLabeledMirrors.Lock();
+ *lock = nullptr; // deletes, see UniquePtr.h
+ },
+ ShutdownPhase::XPCOMWillShutdown);
+ });
+ // Both getting the main thread and dispatching to it can fail.
+ // In that event we leak. Grab a pointer so we have something to NS_RELEASE
+ // in that case.
+ nsIRunnable* temp = cleanupFn.get();
+ nsCOMPtr<nsIThread> mainThread;
+ if (NS_FAILED(NS_GetMainThread(getter_AddRefs(mainThread))) ||
+ NS_FAILED(mainThread->Dispatch(cleanupFn.forget(),
+ nsIThread::DISPATCH_NORMAL))) {
+ // Failed to dispatch cleanup routine.
+ // First, un-leak the runnable (but only if we actually attempted
+ // dispatch)
+ if (!cleanupFn) {
+ NS_RELEASE(temp);
+ }
+ // Next, cleanup immediately, and allow metrics to try again later.
+ *lock = nullptr;
+ return Nothing();
+ }
+ }
+
+ return Some(std::move(lock));
+}
+
+} // namespace mozilla::glean
diff --git a/toolkit/components/glean/bindings/GleanMetric.h b/toolkit/components/glean/bindings/GleanMetric.h
new file mode 100644
index 0000000000..65ac75191d
--- /dev/null
+++ b/toolkit/components/glean/bindings/GleanMetric.h
@@ -0,0 +1,47 @@
+/* -*- 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_GleanMetric_h
+#define mozilla_glean_GleanMetric_h
+
+#include "js/TypeDecls.h"
+#include "nsIGlobalObject.h"
+#include "nsWrapperCache.h"
+#include "nsClassHashtable.h"
+#include "nsTHashMap.h"
+#include "mozilla/DataMutex.h"
+
+namespace mozilla::Telemetry {
+enum class ScalarID : uint32_t;
+} // namespace mozilla::Telemetry
+
+namespace mozilla::glean {
+
+class GleanMetric : public nsISupports, public nsWrapperCache {
+ public:
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS;
+ NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(GleanMetric);
+
+ nsISupports* GetParentObject() const { return mParent; }
+
+ protected:
+ GleanMetric(nsISupports* aParent) : mParent(aParent) {}
+ virtual ~GleanMetric() = default;
+ nsCOMPtr<nsISupports> mParent;
+};
+
+typedef nsUint32HashKey SubmetricIdHashKey;
+typedef nsTHashMap<SubmetricIdHashKey,
+ std::tuple<Telemetry::ScalarID, nsString>>
+ SubmetricToLabeledMirrorMapType;
+typedef StaticDataMutex<UniquePtr<SubmetricToLabeledMirrorMapType>>
+ SubmetricToMirrorMutex;
+
+Maybe<SubmetricToMirrorMutex::AutoLock> GetLabeledMirrorLock();
+
+} // namespace mozilla::glean
+
+#endif /* mozilla_glean_GleanMetric_h */
diff --git a/toolkit/components/glean/bindings/GleanPings.cpp b/toolkit/components/glean/bindings/GleanPings.cpp
new file mode 100644
index 0000000000..eab84192ad
--- /dev/null
+++ b/toolkit/components/glean/bindings/GleanPings.cpp
@@ -0,0 +1,92 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/glean/bindings/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/jog/JOG.h"
+#include "mozilla/glean/bindings/Ping.h"
+#include "MainThreadUtils.h"
+#include "js/PropertyAndElement.h" // JS_DefineProperty
+
+namespace mozilla::glean {
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_0(GleanPings)
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(GleanPings)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(GleanPings)
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(GleanPings)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+JSObject* GleanPings::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return dom::GleanPingsImpl_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+// static
+bool GleanPings::DefineGleanPings(JSContext* aCx,
+ JS::Handle<JSObject*> aGlobal) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(JS::GetClass(aGlobal)->flags & JSCLASS_DOM_GLOBAL,
+ "Passed object is not a global object!");
+
+ nsIGlobalObject* global = xpc::CurrentNativeGlobal(aCx);
+ if (NS_WARN_IF(!global)) {
+ return false;
+ }
+
+ JS::Rooted<JS::Value> gleanPings(aCx);
+ js::AssertSameCompartment(aCx, aGlobal);
+
+ auto impl = MakeRefPtr<GleanPings>();
+ if (!dom::GetOrCreateDOMReflector(aCx, impl.get(), &gleanPings)) {
+ return false;
+ }
+
+ return JS_DefineProperty(aCx, aGlobal, "GleanPings", gleanPings,
+ JSPROP_ENUMERATE);
+}
+
+already_AddRefed<GleanPing> GleanPings::NamedGetter(const nsAString& aName,
+ bool& aFound) {
+ aFound = false;
+
+ NS_ConvertUTF16toUTF8 pingName(aName);
+
+ JOG::EnsureRuntimeMetricsRegistered();
+
+ Maybe<uint32_t> pingId = JOG::GetPing(pingName);
+ if (pingId.isNothing() && !JOG::AreRuntimeMetricsComprehensive()) {
+ pingId = PingByNameLookup(pingName);
+ }
+
+ if (pingId.isNothing()) {
+ aFound = false;
+ return nullptr;
+ }
+
+ aFound = true;
+ return MakeAndAddRef<GleanPing>(pingId.value());
+}
+
+bool GleanPings::NameIsEnumerable(const nsAString& aName) { return false; }
+
+void GleanPings::GetSupportedNames(nsTArray<nsString>& aNames) {
+ JOG::GetPingNames(aNames);
+ if (!JOG::AreRuntimeMetricsComprehensive()) {
+ 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..b4a88bfc64
--- /dev/null
+++ b/toolkit/components/glean/bindings/GleanPings.h
@@ -0,0 +1,39 @@
+/* -*- 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 "nsGlobalWindowInner.h"
+#include "nsISupports.h"
+#include "nsWrapperCache.h"
+
+namespace mozilla::glean {
+
+class GleanPings final : public nsISupports, public nsWrapperCache {
+ public:
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(GleanPings)
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+ nsISupports* GetParentObject() { return nullptr; }
+
+ static bool DefineGleanPings(JSContext* aCx, JS::Handle<JSObject*> aGlobal);
+
+ already_AddRefed<GleanPing> NamedGetter(const nsAString& aName, bool& aFound);
+ bool NameIsEnumerable(const nsAString& aName);
+ void GetSupportedNames(nsTArray<nsString>& aNames);
+
+ protected:
+ virtual ~GleanPings() = default;
+};
+
+} // namespace mozilla::glean
+
+#endif /* mozilla_glean_GleanPings */
diff --git a/toolkit/components/glean/bindings/MetricTypes.h b/toolkit/components/glean/bindings/MetricTypes.h
new file mode 100644
index 0000000000..a7ae09fe19
--- /dev/null
+++ b/toolkit/components/glean/bindings/MetricTypes.h
@@ -0,0 +1,27 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_Glean_MetricTypes_h
+#define mozilla_Glean_MetricTypes_h
+
+#include "mozilla/glean/bindings/Boolean.h"
+#include "mozilla/glean/bindings/Counter.h"
+#include "mozilla/glean/bindings/CustomDistribution.h"
+#include "mozilla/glean/bindings/Datetime.h"
+#include "mozilla/glean/bindings/Denominator.h"
+#include "mozilla/glean/bindings/Event.h"
+#include "mozilla/glean/bindings/Labeled.h"
+#include "mozilla/glean/bindings/MemoryDistribution.h"
+#include "mozilla/glean/bindings/Numerator.h"
+#include "mozilla/glean/bindings/Quantity.h"
+#include "mozilla/glean/bindings/Rate.h"
+#include "mozilla/glean/bindings/String.h"
+#include "mozilla/glean/bindings/StringList.h"
+#include "mozilla/glean/bindings/Text.h"
+#include "mozilla/glean/bindings/Timespan.h"
+#include "mozilla/glean/bindings/TimingDistribution.h"
+#include "mozilla/glean/bindings/Url.h"
+#include "mozilla/glean/bindings/Uuid.h"
+
+#endif // mozilla_Glean_MetricTypes_h
diff --git a/toolkit/components/glean/bindings/jog/Cargo.toml b/toolkit/components/glean/bindings/jog/Cargo.toml
new file mode 100644
index 0000000000..de2d8dce5a
--- /dev/null
+++ b/toolkit/components/glean/bindings/jog/Cargo.toml
@@ -0,0 +1,20 @@
+[package]
+name = "jog"
+version = "0.1.0"
+authors = ["Glean SDK team <glean-team@mozilla.com>"]
+edition = "2021"
+publish = false
+license = "MPL-2.0"
+
+[dependencies]
+firefox-on-glean = { path = "../../api" }
+log = "0.4"
+mozbuild = "0.1"
+nsstring = { path = "../../../../../xpcom/rust/nsstring", optional = true }
+once_cell = "1.2.0"
+serde = { version = "1.0", features = ["derive"] }
+serde_json = "1.0"
+thin-vec = { version = "0.2.1", features = ["gecko-ffi"] }
+
+[features]
+with_gecko = [ "nsstring" ]
diff --git a/toolkit/components/glean/bindings/jog/JOG.cpp b/toolkit/components/glean/bindings/jog/JOG.cpp
new file mode 100644
index 0000000000..56119cef0e
--- /dev/null
+++ b/toolkit/components/glean/bindings/jog/JOG.cpp
@@ -0,0 +1,276 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/glean/bindings/jog/JOG.h"
+
+#include <locale>
+
+#include "mozilla/AppShutdown.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/glean/bindings/jog/jog_ffi_generated.h"
+#include "mozilla/Logging.h"
+#include "mozilla/StaticPrefs_telemetry.h"
+#include "mozilla/AppShutdown.h"
+#include "nsDirectoryServiceDefs.h"
+#include "nsDirectoryServiceUtils.h"
+#include "nsThreadUtils.h"
+#include "nsTHashMap.h"
+#include "nsTHashSet.h"
+
+namespace mozilla::glean {
+
+using mozilla::LogLevel;
+static mozilla::LazyLogModule sLog("jog");
+
+// Storage
+// Thread Safety: Only used on the main thread.
+StaticAutoPtr<nsTHashSet<nsCString>> gCategories;
+StaticAutoPtr<nsTHashMap<nsCString, uint32_t>> gMetrics;
+StaticAutoPtr<nsTHashMap<uint32_t, nsCString>> gMetricNames;
+StaticAutoPtr<nsTHashMap<nsCString, uint32_t>> gPings;
+
+// static
+bool JOG::HasCategory(const nsACString& aCategoryName) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ return gCategories && gCategories->Contains(aCategoryName);
+}
+
+static Maybe<bool> sFoundAndLoadedJogfile;
+
+// static
+bool JOG::EnsureRuntimeMetricsRegistered(bool aForce) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (sFoundAndLoadedJogfile) {
+ return sFoundAndLoadedJogfile.value();
+ }
+ sFoundAndLoadedJogfile = Some(false);
+
+ MOZ_LOG(sLog, LogLevel::Debug, ("Determining whether there's JOG for you."));
+
+ if (!mozilla::StaticPrefs::telemetry_fog_artifact_build()) {
+ // Supporting Artifact Builds is a developer-only thing.
+ // We're on the main thread here.
+ // Let's not spend any more time than we need to.
+ MOZ_LOG(sLog, LogLevel::Debug,
+ ("!telemetry.fog.artifact_build. No JOG for you."));
+ return false;
+ }
+ // The metrics we need to process were placed in GreD in jogfile.json
+ // That file was generated by
+ // toolkit/components/glean/build_scripts/glean_parser_ext/jog.py
+ nsCOMPtr<nsIFile> jogfile;
+ if (NS_WARN_IF(NS_FAILED(
+ NS_GetSpecialDirectory(NS_GRE_DIR, getter_AddRefs(jogfile))))) {
+ return false;
+ }
+ if (NS_WARN_IF(NS_FAILED(jogfile->Append(u"jogfile.json"_ns)))) {
+ return false;
+ }
+ bool jogfileExists = false;
+ if (NS_WARN_IF(NS_FAILED(jogfile->Exists(&jogfileExists))) ||
+ !jogfileExists) {
+ return false;
+ }
+
+ // We _could_ register everything here in C++ land,
+ // but let's use Rust because (among other reasons) it's more fun.
+ nsAutoString jogfileString;
+ if (NS_WARN_IF(NS_FAILED(jogfile->GetPath(jogfileString)))) {
+ return false;
+ }
+ sFoundAndLoadedJogfile = Some(jog::jog_load_jogfile(&jogfileString));
+ MOZ_LOG(sLog, LogLevel::Debug,
+ ("%s", sFoundAndLoadedJogfile.value()
+ ? "Found and loaded jogfile. Yes! JOG for you!"
+ : "Couldn't find and load jogfile. No JOG for you."));
+ return sFoundAndLoadedJogfile.value();
+}
+
+// static
+bool JOG::AreRuntimeMetricsComprehensive() {
+ MOZ_ASSERT(NS_IsMainThread());
+ return sFoundAndLoadedJogfile && sFoundAndLoadedJogfile.value();
+}
+
+// static
+void JOG::GetCategoryNames(nsTArray<nsString>& aNames) {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (!gCategories) {
+ return;
+ }
+ for (const auto& category : *gCategories) {
+ aNames.EmplaceBack(NS_ConvertUTF8toUTF16(category));
+ }
+}
+
+// static
+Maybe<uint32_t> JOG::GetMetric(const nsACString& aMetricName) {
+ MOZ_ASSERT(NS_IsMainThread());
+ return !gMetrics ? Nothing() : gMetrics->MaybeGet(aMetricName);
+}
+
+// static
+Maybe<nsCString> JOG::GetMetricName(uint32_t aMetricId) {
+ MOZ_ASSERT(NS_IsMainThread());
+ return !gMetricNames ? Nothing() : gMetricNames->MaybeGet(aMetricId);
+}
+
+// static
+void JOG::GetMetricNames(const nsACString& aCategoryName,
+ nsTArray<nsString>& aNames) {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (!gMetricNames) {
+ return;
+ }
+ for (const auto& identifier : gMetricNames->Values()) {
+ if (StringBeginsWith(identifier, aCategoryName) &&
+ identifier.CharAt(aCategoryName.Length()) == '.') {
+ const char* metricName = &identifier.Data()[aCategoryName.Length() + 1];
+ aNames.AppendElement()->AssignASCII(metricName);
+ }
+ }
+}
+
+// static
+Maybe<uint32_t> JOG::GetPing(const nsACString& aPingName) {
+ MOZ_ASSERT(NS_IsMainThread());
+ return !gPings ? Nothing() : gPings->MaybeGet(aPingName);
+}
+
+// static
+void JOG::GetPingNames(nsTArray<nsString>& aNames) {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (!gPings) {
+ return;
+ }
+ for (const auto& ping : gPings->Keys()) {
+ aNames.EmplaceBack(NS_ConvertUTF8toUTF16(ping));
+ }
+}
+
+} // namespace mozilla::glean
+
+// static
+nsCString dottedSnakeToCamel(const nsACString& aSnake) {
+ nsCString camel;
+ bool first = true;
+ for (const nsACString& segment : aSnake.Split('_')) {
+ for (const nsACString& part : segment.Split('.')) {
+ if (first) {
+ first = false;
+ camel.Append(part);
+ } else if (part.Length()) {
+ char lower = part.CharAt(0);
+ if ('a' <= lower && lower <= 'z') {
+ camel.Append(
+ std::toupper(lower, std::locale())); // append the Capital.
+ camel.Append(part.BeginReading() + 1,
+ part.Length() - 1); // append the rest.
+ } else {
+ // Not gonna try to capitalize anything outside a->z.
+ camel.Append(part);
+ }
+ }
+ }
+ }
+ return camel;
+}
+
+// static
+nsCString kebabToCamel(const nsACString& aKebab) {
+ nsCString camel;
+ bool first = true;
+ for (const nsACString& segment : aKebab.Split('-')) {
+ if (first) {
+ first = false;
+ camel.Append(segment);
+ } else if (segment.Length()) {
+ char lower = segment.CharAt(0);
+ if ('a' <= lower && lower <= 'z') {
+ camel.Append(
+ std::toupper(lower, std::locale())); // append the Capital.
+ camel.Append(segment.BeginReading() + 1,
+ segment.Length() - 1); // append the rest.
+ } else {
+ // Not gonna try to capitalize anything outside a->z.
+ camel.Append(segment);
+ }
+ }
+ }
+ return camel;
+}
+
+using mozilla::AppShutdown;
+using mozilla::ShutdownPhase;
+using mozilla::glean::gCategories;
+using mozilla::glean::gMetricNames;
+using mozilla::glean::gMetrics;
+using mozilla::glean::gPings;
+
+extern "C" NS_EXPORT void JOG_RegisterMetric(
+ const nsACString& aCategory, const nsACString& aName,
+ uint32_t aMetric, // includes type.
+ uint32_t aMetricId) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (AppShutdown::IsInOrBeyond(ShutdownPhase::XPCOMWillShutdown)) {
+ return;
+ }
+
+ MOZ_LOG(mozilla::glean::sLog, mozilla::LogLevel::Verbose,
+ ("Registering metric %s.%s id %" PRIu32 " id+type %" PRIu32 "",
+ PromiseFlatCString(aCategory).get(), PromiseFlatCString(aName).get(),
+ aMetricId, aMetric));
+
+ // aCategory is dotted.snake_case. aName is snake_case.
+ auto categoryCamel = dottedSnakeToCamel(aCategory);
+ auto nameCamel = dottedSnakeToCamel(aName);
+
+ // Register the category
+ if (!gCategories) {
+ gCategories = new nsTHashSet<nsCString>();
+ RunOnShutdown([&] { gCategories = nullptr; },
+ ShutdownPhase::XPCOMWillShutdown);
+ }
+ gCategories->Insert(categoryCamel);
+
+ // Register the metric
+ if (!gMetrics) {
+ gMetrics = new nsTHashMap<nsCString, uint32_t>();
+ RunOnShutdown([&] { gMetrics = nullptr; },
+ ShutdownPhase::XPCOMWillShutdown);
+ }
+ gMetrics->InsertOrUpdate(categoryCamel + "."_ns + nameCamel, aMetric);
+
+ // Register the metric name (for GIFFT)
+ if (!gMetricNames) {
+ gMetricNames = new nsTHashMap<uint32_t, nsCString>();
+ RunOnShutdown([&] { gMetricNames = nullptr; },
+ ShutdownPhase::XPCOMWillShutdown);
+ }
+ gMetricNames->InsertOrUpdate(aMetricId, categoryCamel + "."_ns + nameCamel);
+}
+
+extern "C" NS_EXPORT void JOG_RegisterPing(const nsACString& aPingName,
+ uint32_t aPingId) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (AppShutdown::IsInOrBeyond(ShutdownPhase::XPCOMWillShutdown)) {
+ return;
+ }
+
+ // aPingName is kebab-case. JS expects camelCase.
+ auto pingCamel = kebabToCamel(aPingName);
+
+ // Register the ping
+ if (!gPings) {
+ gPings = new nsTHashMap<nsCString, uint32_t>();
+ RunOnShutdown([&] { gPings = nullptr; }, ShutdownPhase::XPCOMWillShutdown);
+ }
+ gPings->InsertOrUpdate(pingCamel, aPingId);
+}
diff --git a/toolkit/components/glean/bindings/jog/JOG.h b/toolkit/components/glean/bindings/jog/JOG.h
new file mode 100644
index 0000000000..ce51249d00
--- /dev/null
+++ b/toolkit/components/glean/bindings/jog/JOG.h
@@ -0,0 +1,122 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_glean_JOG_h
+#define mozilla_glean_JOG_h
+
+#include "nsStringFwd.h"
+#include "nsTArray.h"
+#include "mozilla/Maybe.h"
+
+namespace mozilla::glean {
+
+class JOG {
+ public:
+ /**
+ * Returns whether JOG knows about a category by this name
+ *
+ * @param aCategoryName The category name to check.
+ *
+ * @returns true if JOG is aware of a category by the given name at this time
+ */
+ static bool HasCategory(const nsACString& aCategoryName);
+
+ /**
+ * Runs the runtime registrar.
+ *
+ * Locates the runtime metrics file and, if present, loads and processes it.
+ *
+ * Only does any work at all if !mozilla::IsPackagedBuild()
+ *
+ * **Note:** When this function does something, it is expensive, running
+ * synchronous file I/O to ensure that the registration is complete when this
+ * call returns.
+ *
+ * @param aForce Set to `true` if you want to force the I/O to run. Defaults
+ * to `false`, which doesn't run the I/O if it's already run and
+ * returns the previous return value.
+ * @returns whether it found the runtime metrics file and succesfully loaded,
+ * processed, and registered the described metrics.
+ */
+ static bool EnsureRuntimeMetricsRegistered(bool aForce = false);
+
+ /**
+ * Returns whether, if a metric is absent in the runtime-registered metrics,
+ * you should check the compile-time-registered metrics.
+ *
+ * Runtime-registered metrics can either replace all compile-time-registered
+ * metrics (like in artefact builds) or just be supplementing compile-time-
+ * registered metrics (like addons/dynamic telemetry/etc).
+ *
+ * This is tied to the current state of runtime metric registration. So it
+ * may return false at one time and true later (e.g. if RuntimeRegistrar is
+ * run in between).
+ *
+ * @return true if you should treat the runtime-registered metrics as
+ * authoritative and comprehensive.
+ */
+ static bool AreRuntimeMetricsComprehensive();
+
+ /**
+ * Adds the runtime-registered metrics' categories to `aNames`.
+ *
+ * @param aNames The list to add the categories' names to.
+ */
+ static void GetCategoryNames(nsTArray<nsString>& aNames);
+
+ /**
+ * Get the metric id+type in a u32 for a named runtime-registered metric.
+ *
+ * Return value's only useful to GleanJSMetricsLookup.h
+ *
+ * @param aMetricName The `myCategory.myName` dotted.camelCase metric name.
+ * @return Nothing() if no metric by that name was registered at runtime.
+ * Otherwise, the encoded u32 with metric id and metric type id for
+ * the runtime-registered metric.
+ */
+ static Maybe<uint32_t> GetMetric(const nsACString& aMetricName);
+
+ /**
+ * Get the metric name for an identified runtime-registered metric.
+ *
+ * @param aMetricId The id of the runtime-registered metric.
+ * @return Nothing() if no metric by that id has been registered.
+ * Otherwise, the `myCategory.myName` dotted.camelCase metric name of
+ * the runtime-registered metric.
+ */
+ static Maybe<nsCString> GetMetricName(uint32_t aMetricId);
+
+ /**
+ * Adds `aCategoryName`'s runtime-registered metrics' names to `aNames`.
+ *
+ * @param aCategoryName The name of the category we want the metric names for.
+ * @param aNames The list to add the metrics' names to.
+ */
+ static void GetMetricNames(const nsACString& aCategoryName,
+ nsTArray<nsString>& aNames);
+
+ /**
+ * Get the ping id in a u32 for a named runtime-registered ping.
+ *
+ * Return value's only useful to GleanJSPingsLookup.h
+ *
+ * @param aPingName The ping name.
+ * @return Nothing() if no ping by that name was registered at runtime.
+ * Otherwise, the id for the runtime-registered ping.
+ */
+ static Maybe<uint32_t> GetPing(const nsACString& aPingName);
+
+ /**
+ * Adds the runtime-registered pings' names to `aNames`.
+ *
+ * @param aNames The list to add the pings' names to.
+ */
+ static void GetPingNames(nsTArray<nsString>& aNames);
+};
+
+} // namespace mozilla::glean
+
+#endif /* mozilla_glean_JOG_h */
diff --git a/toolkit/components/glean/bindings/jog/cbindgen.toml b/toolkit/components/glean/bindings/jog/cbindgen.toml
new file mode 100644
index 0000000000..62139cc6c6
--- /dev/null
+++ b/toolkit/components/glean/bindings/jog/cbindgen.toml
@@ -0,0 +1,26 @@
+header = """/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#ifndef mozilla_glean_jog_ffi_generated_h
+#define mozilla_glean_jog_ffi_generated_h
+"""
+trailer = """
+#endif // mozilla_glean_jog_ffi_generated_h
+"""
+autogen_warning = """/* DO NOT MODIFY THIS MANUALLY! This file was generated using cbindgen. */"""
+include_version = true
+braces = "SameLine"
+line_length = 100
+tab_width = 2
+language = "C++"
+namespaces = ["mozilla::glean::jog"]
+includes = ["nsTArray.h", "nsString.h"]
+
+[export.rename]
+"ThinVec" = "nsTArray"
+#"nsCStringRepr" = "nsCString"
+
+[parse]
+#parse_deps = true
+#include = ["fog"]
+#extra_bindings = ["fog"]
diff --git a/toolkit/components/glean/bindings/jog/src/lib.rs b/toolkit/components/glean/bindings/jog/src/lib.rs
new file mode 100644
index 0000000000..4f2d439d80
--- /dev/null
+++ b/toolkit/components/glean/bindings/jog/src/lib.rs
@@ -0,0 +1,267 @@
+/* This Source Code Form is subject to the terms of 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 firefox_on_glean::factory;
+use firefox_on_glean::private::traits::HistogramType;
+use firefox_on_glean::private::{CommonMetricData, Lifetime, MemoryUnit, TimeUnit};
+#[cfg(feature = "with_gecko")]
+use nsstring::{nsACString, nsAString, nsCString};
+use serde::Deserialize;
+use std::borrow::Cow;
+use std::collections::BTreeMap;
+use std::fs::File;
+use std::io::BufReader;
+use thin_vec::ThinVec;
+
+#[derive(Default, Deserialize)]
+struct ExtraMetricArgs {
+ time_unit: Option<TimeUnit>,
+ memory_unit: Option<MemoryUnit>,
+ allowed_extra_keys: Option<Vec<String>>,
+ range_min: Option<u64>,
+ range_max: Option<u64>,
+ bucket_count: Option<u64>,
+ histogram_type: Option<HistogramType>,
+ numerators: Option<Vec<CommonMetricData>>,
+ ordered_labels: Option<Vec<Cow<'static, str>>>,
+}
+
+/// Test-only method.
+///
+/// Registers a metric.
+/// Doesn't check to see if it's been registered before.
+/// Doesn't check that it would pass schema validation if it were a real metric.
+///
+/// `extra_args` is a JSON-encoded string in a form that serde can read into an ExtraMetricArgs.
+///
+/// No effort has been made to make this pleasant to use, since it's for
+/// internal testing only (ie, the testing of JOG itself).
+#[cfg(feature = "with_gecko")]
+#[no_mangle]
+pub extern "C" fn jog_test_register_metric(
+ metric_type: &nsACString,
+ category: &nsACString,
+ name: &nsACString,
+ send_in_pings: &ThinVec<nsCString>,
+ lifetime: &nsACString,
+ disabled: bool,
+ extra_args: &nsACString,
+) -> u32 {
+ log::warn!("Type: {:?}, Category: {:?}, Name: {:?}, SendInPings: {:?}, Lifetime: {:?}, Disabled: {}, ExtraArgs: {}",
+ metric_type, category, name, send_in_pings, lifetime, disabled, extra_args);
+ let metric_type = &metric_type.to_utf8();
+ let category = category.to_string();
+ let name = name.to_string();
+ let send_in_pings = send_in_pings.iter().map(|ping| ping.to_string()).collect();
+ let lifetime = serde_json::from_str(&lifetime.to_utf8())
+ .expect("Lifetime didn't deserialize happily. Is it valid JSON?");
+
+ let extra_args: ExtraMetricArgs = if extra_args.is_empty() {
+ Default::default()
+ } else {
+ serde_json::from_str(&extra_args.to_utf8())
+ .expect("Extras didn't deserialize happily. Are they valid JSON?")
+ };
+ create_and_register_metric(
+ metric_type,
+ category,
+ name,
+ send_in_pings,
+ lifetime,
+ disabled,
+ extra_args,
+ )
+ .expect("Creation/Registration of metric failed") // ok to panic in test-only method
+ .0
+}
+
+fn create_and_register_metric(
+ metric_type: &str,
+ category: String,
+ name: String,
+ send_in_pings: Vec<String>,
+ lifetime: Lifetime,
+ disabled: bool,
+ extra_args: ExtraMetricArgs,
+) -> Result<(u32, u32), Box<dyn std::error::Error>> {
+ let ns_name = nsCString::from(&name);
+ let ns_category = nsCString::from(&category);
+ let metric = factory::create_and_register_metric(
+ metric_type,
+ category,
+ name,
+ send_in_pings,
+ lifetime,
+ disabled,
+ extra_args.time_unit,
+ extra_args.memory_unit,
+ extra_args.allowed_extra_keys.or_else(|| Some(Vec::new())),
+ extra_args.range_min,
+ extra_args.range_max,
+ extra_args.bucket_count,
+ extra_args.histogram_type,
+ extra_args.numerators,
+ extra_args.ordered_labels,
+ );
+ extern "C" {
+ fn JOG_RegisterMetric(
+ category: &nsACString,
+ name: &nsACString,
+ metric: u32,
+ metric_id: u32,
+ );
+ }
+ if let Ok((metric, metric_id)) = metric {
+ unsafe {
+ // Safety: We're loaning to C++ data we don't later use.
+ JOG_RegisterMetric(&ns_category, &ns_name, metric, metric_id);
+ }
+ } else {
+ log::warn!(
+ "Could not register metric {}.{} due to {:?}",
+ ns_category,
+ ns_name,
+ metric
+ );
+ }
+ metric
+}
+
+/// Test-only method.
+///
+/// Registers a ping. Doesn't check to see if it's been registered before.
+/// Doesn't check that it would pass schema validation if it were a real ping.
+#[no_mangle]
+pub extern "C" fn jog_test_register_ping(
+ name: &nsACString,
+ include_client_id: bool,
+ send_if_empty: bool,
+ precise_timestamps: bool,
+ reason_codes: &ThinVec<nsCString>,
+) -> u32 {
+ let ping_name = name.to_string();
+ let reason_codes = reason_codes
+ .iter()
+ .map(|reason| reason.to_string())
+ .collect();
+ create_and_register_ping(
+ ping_name,
+ include_client_id,
+ send_if_empty,
+ precise_timestamps,
+ reason_codes,
+ )
+ .expect("Creation or registration of ping failed.") // permitted to panic in test-only method.
+}
+
+fn create_and_register_ping(
+ ping_name: String,
+ include_client_id: bool,
+ send_if_empty: bool,
+ precise_timestamps: bool,
+ reason_codes: Vec<String>,
+) -> Result<u32, Box<dyn std::error::Error>> {
+ let ns_name = nsCString::from(&ping_name);
+ let ping_id = factory::create_and_register_ping(
+ ping_name,
+ include_client_id,
+ send_if_empty,
+ precise_timestamps,
+ reason_codes,
+ );
+ extern "C" {
+ fn JOG_RegisterPing(name: &nsACString, ping_id: u32);
+ }
+ if let Ok(ping_id) = ping_id {
+ unsafe {
+ // Safety: We're loaning to C++ data we don't later use.
+ JOG_RegisterPing(&ns_name, ping_id);
+ }
+ } else {
+ log::warn!("Could not register ping {} due to {:?}", ns_name, ping_id);
+ }
+ ping_id
+}
+
+/// Test-only method.
+///
+/// Clears all runtime registration storage of registered metrics and pings.
+#[no_mangle]
+pub extern "C" fn jog_test_clear_registered_metrics_and_pings() {}
+
+#[derive(Default, Deserialize)]
+struct Jogfile {
+ // Using BTreeMap to ensure stable iteration ordering.
+ metrics: BTreeMap<String, Vec<MetricDefinitionData>>,
+ pings: Vec<PingDefinitionData>,
+}
+
+#[derive(Default, Deserialize)]
+struct MetricDefinitionData {
+ metric_type: String,
+ name: String,
+ send_in_pings: Vec<String>,
+ lifetime: Lifetime,
+ disabled: bool,
+ #[serde(default)]
+ extra_args: Option<ExtraMetricArgs>,
+}
+
+#[derive(Default, Deserialize)]
+struct PingDefinitionData {
+ name: String,
+ include_client_id: bool,
+ send_if_empty: bool,
+ precise_timestamps: bool,
+ reason_codes: Option<Vec<String>>,
+}
+
+/// Read the file at the provided location, interpret it as a jogfile,
+/// and register those pings and metrics.
+/// Returns true if we successfully parsed the jogfile. Does not mean
+/// all or any metrics and pings successfully registered,
+/// just that serde managed to deserialize it into metrics and pings and we tried to register them all.
+#[no_mangle]
+pub extern "C" fn jog_load_jogfile(jogfile_path: &nsAString) -> bool {
+ let f = match File::open(jogfile_path.to_string()) {
+ Ok(f) => f,
+ _ => {
+ log::error!("Boo, couldn't open jogfile at {}", jogfile_path.to_string());
+ return false;
+ }
+ };
+ let reader = BufReader::new(f);
+
+ let j: Jogfile = match serde_json::from_reader(reader) {
+ Ok(j) => j,
+ Err(e) => {
+ log::error!("Boo, couldn't read jogfile because of: {:?}", e);
+ return false;
+ }
+ };
+ log::trace!("Loaded jogfile. Registering metrics+pings.");
+ for (category, metrics) in j.metrics.into_iter() {
+ for metric in metrics.into_iter() {
+ let _ = create_and_register_metric(
+ &metric.metric_type,
+ category.to_string(),
+ metric.name,
+ metric.send_in_pings,
+ metric.lifetime,
+ metric.disabled,
+ metric.extra_args.unwrap_or_else(Default::default),
+ );
+ }
+ }
+ for ping in j.pings.into_iter() {
+ let _ = create_and_register_ping(
+ ping.name,
+ ping.include_client_id,
+ ping.send_if_empty,
+ ping.precise_timestamps,
+ ping.reason_codes.unwrap_or_else(Vec::new),
+ );
+ }
+ true
+}
diff --git a/toolkit/components/glean/bindings/private/Boolean.cpp b/toolkit/components/glean/bindings/private/Boolean.cpp
new file mode 100644
index 0000000000..8300990b49
--- /dev/null
+++ b/toolkit/components/glean/bindings/private/Boolean.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/glean/bindings/Boolean.h"
+
+#include "nsString.h"
+#include "mozilla/ResultVariant.h"
+#include "mozilla/dom/GleanMetricsBinding.h"
+#include "mozilla/glean/bindings/ScalarGIFFTMap.h"
+#include "mozilla/glean/fog_ffi_generated.h"
+
+namespace mozilla::glean {
+
+namespace impl {
+
+void BooleanMetric::Set(bool aValue) const {
+ auto scalarId = ScalarIdForMetric(mId);
+ if (scalarId) {
+ Telemetry::ScalarSet(scalarId.extract(), aValue);
+ } else if (IsSubmetricId(mId)) {
+ GetLabeledMirrorLock().apply([&](auto& lock) {
+ auto tuple = lock.ref()->MaybeGet(mId);
+ if (tuple) {
+ Telemetry::ScalarSet(std::get<0>(tuple.ref()), std::get<1>(tuple.ref()),
+ aValue);
+ }
+ });
+ }
+ fog_boolean_set(mId, int(aValue));
+}
+
+Result<Maybe<bool>, nsCString> BooleanMetric::TestGetValue(
+ const nsACString& aPingName) const {
+ nsCString err;
+ if (fog_boolean_test_get_error(mId, &err)) {
+ return Err(err);
+ }
+ if (!fog_boolean_test_has_value(mId, &aPingName)) {
+ return Maybe<bool>();
+ }
+ return Some(fog_boolean_test_get_value(mId, &aPingName));
+}
+
+} // namespace impl
+
+JSObject* GleanBoolean::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return dom::GleanBoolean_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+void GleanBoolean::Set(bool aValue) { mBoolean.Set(aValue); }
+
+dom::Nullable<bool> GleanBoolean::TestGetValue(const nsACString& aPingName,
+ ErrorResult& aRv) {
+ dom::Nullable<bool> ret;
+ auto result = mBoolean.TestGetValue(aPingName);
+ if (result.isErr()) {
+ aRv.ThrowDataError(result.unwrapErr());
+ return ret;
+ }
+ auto optresult = result.unwrap();
+ if (!optresult.isNothing()) {
+ ret.SetValue(optresult.value());
+ }
+ return ret;
+}
+
+} // namespace mozilla::glean
diff --git a/toolkit/components/glean/bindings/private/Boolean.h b/toolkit/components/glean/bindings/private/Boolean.h
new file mode 100644
index 0000000000..6947e1b713
--- /dev/null
+++ b/toolkit/components/glean/bindings/private/Boolean.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_GleanBoolean_h
+#define mozilla_glean_GleanBoolean_h
+
+#include "mozilla/dom/BindingDeclarations.h"
+#include "mozilla/glean/bindings/GleanMetric.h"
+#include "mozilla/Result.h"
+#include "nsString.h"
+#include "nsWrapperCache.h"
+
+namespace mozilla {
+namespace glean {
+
+namespace impl {
+
+class BooleanMetric {
+ public:
+ constexpr explicit BooleanMetric(uint32_t id) : mId(id) {}
+
+ /**
+ * Set to the specified boolean value.
+ *
+ * @param aValue the value to set.
+ */
+ void Set(bool aValue) const;
+
+ /**
+ * **Test-only API**
+ *
+ * Gets the currently stored value as a boolean.
+ *
+ * This function will attempt to await the last parent-process task (if any)
+ * writing to the the metric's storage engine before returning a value.
+ * This function will not wait for data from child processes.
+ *
+ * This doesn't clear the stored value.
+ * Parent process only. Panics in child processes.
+ *
+ * @param aPingName The (optional) name of the ping to retrieve the metric
+ * for. Defaults to the first value in `send_in_pings`.
+ *
+ * @return value of the stored metric.
+ */
+ Result<Maybe<bool>, nsCString> TestGetValue(
+ const nsACString& aPingName = nsCString()) const;
+
+ private:
+ const uint32_t mId;
+};
+
+} // namespace impl
+
+class GleanBoolean final : public GleanMetric {
+ public:
+ explicit GleanBoolean(uint32_t id, nsISupports* aParent)
+ : GleanMetric(aParent), mBoolean(id) {}
+
+ virtual JSObject* WrapObject(
+ JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override final;
+
+ void Set(bool aValue);
+
+ dom::Nullable<bool> TestGetValue(const nsACString& aPingName,
+ ErrorResult& aRv);
+
+ private:
+ virtual ~GleanBoolean() = default;
+
+ const impl::BooleanMetric mBoolean;
+};
+
+} // namespace glean
+} // namespace mozilla
+
+#endif /* mozilla_glean_GleanBoolean.h */
diff --git a/toolkit/components/glean/bindings/private/Common.cpp b/toolkit/components/glean/bindings/private/Common.cpp
new file mode 100644
index 0000000000..f84c39c05d
--- /dev/null
+++ b/toolkit/components/glean/bindings/private/Common.cpp
@@ -0,0 +1,33 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "Common.h"
+#include "nsComponentManagerUtils.h"
+#include "nsIConsoleService.h"
+#include "nsIScriptError.h"
+#include "nsServiceManagerUtils.h"
+
+namespace mozilla::glean {
+
+// This is copied from TelemetryCommons.cpp (and modified because consoleservice
+// handles threading), but that one is not exported.
+// There's _at least_ a third instance of `LogToBrowserConsole`,
+// but that one is slightly different.
+void LogToBrowserConsole(uint32_t aLogLevel, const nsAString& aMsg) {
+ nsCOMPtr<nsIConsoleService> console(
+ do_GetService("@mozilla.org/consoleservice;1"));
+ if (!console) {
+ NS_WARNING("Failed to log message to console.");
+ return;
+ }
+
+ nsCOMPtr<nsIScriptError> error(do_CreateInstance(NS_SCRIPTERROR_CONTRACTID));
+ error->Init(aMsg, u""_ns, u""_ns, 0, 0, aLogLevel, "chrome javascript"_ns,
+ false /* from private window */, true /* from chrome context */);
+ console->LogMessage(error);
+}
+
+} // namespace mozilla::glean
diff --git a/toolkit/components/glean/bindings/private/Common.h b/toolkit/components/glean/bindings/private/Common.h
new file mode 100644
index 0000000000..e3dd7a0a47
--- /dev/null
+++ b/toolkit/components/glean/bindings/private/Common.h
@@ -0,0 +1,24 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_glean_Common_h
+#define mozilla_glean_Common_h
+
+#include "nsIScriptError.h"
+
+namespace mozilla::glean {
+
+/**
+ * Dumps a log message to the Browser Console using the provided level.
+ *
+ * @param aLogLevel The level to use when displaying the message in the browser
+ * console (e.g. nsIScriptError::warningFlag, ...).
+ * @param aMsg The text message to print to the console.
+ */
+void LogToBrowserConsole(uint32_t aLogLevel, const nsAString& aMsg);
+
+} // namespace mozilla::glean
+
+#endif /* mozilla_glean_Common_h */
diff --git a/toolkit/components/glean/bindings/private/Counter.cpp b/toolkit/components/glean/bindings/private/Counter.cpp
new file mode 100644
index 0000000000..f7f70f29eb
--- /dev/null
+++ b/toolkit/components/glean/bindings/private/Counter.cpp
@@ -0,0 +1,74 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/glean/bindings/Counter.h"
+
+#include "nsString.h"
+#include "mozilla/ResultVariant.h"
+#include "mozilla/dom/GleanMetricsBinding.h"
+#include "mozilla/glean/bindings/ScalarGIFFTMap.h"
+#include "mozilla/glean/fog_ffi_generated.h"
+
+namespace mozilla::glean {
+
+namespace impl {
+
+void CounterMetric::Add(int32_t aAmount) const {
+ auto scalarId = ScalarIdForMetric(mId);
+ if (aAmount >= 0) {
+ if (scalarId) {
+ Telemetry::ScalarAdd(scalarId.extract(), aAmount);
+ } else if (IsSubmetricId(mId)) {
+ GetLabeledMirrorLock().apply([&](auto& lock) {
+ auto tuple = lock.ref()->MaybeGet(mId);
+ if (tuple && aAmount > 0) {
+ Telemetry::ScalarAdd(std::get<0>(tuple.ref()),
+ std::get<1>(tuple.ref()), (uint32_t)aAmount);
+ }
+ });
+ }
+ }
+ fog_counter_add(mId, aAmount);
+}
+
+Result<Maybe<int32_t>, nsCString> CounterMetric::TestGetValue(
+ const nsACString& aPingName) const {
+ nsCString err;
+ if (fog_counter_test_get_error(mId, &err)) {
+ return Err(err);
+ }
+ if (!fog_counter_test_has_value(mId, &aPingName)) {
+ return Maybe<int32_t>(); // can't use Nothing() or templates will fail.
+ }
+ return Some(fog_counter_test_get_value(mId, &aPingName));
+}
+
+} // namespace impl
+
+/* virtual */
+JSObject* GleanCounter::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return dom::GleanCounter_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+void GleanCounter::Add(int32_t aAmount) { mCounter.Add(aAmount); }
+
+dom::Nullable<int32_t> GleanCounter::TestGetValue(const nsACString& aPingName,
+ ErrorResult& aRv) {
+ dom::Nullable<int32_t> ret;
+ auto result = mCounter.TestGetValue(aPingName);
+ if (result.isErr()) {
+ aRv.ThrowDataError(result.unwrapErr());
+ return ret;
+ }
+ auto optresult = result.unwrap();
+ if (!optresult.isNothing()) {
+ ret.SetValue(optresult.value());
+ }
+ return ret;
+}
+
+} // 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..c378df8e5d
--- /dev/null
+++ b/toolkit/components/glean/bindings/private/Counter.h
@@ -0,0 +1,77 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_glean_GleanCounter_h
+#define mozilla_glean_GleanCounter_h
+
+#include "mozilla/dom/BindingDeclarations.h"
+#include "mozilla/glean/bindings/GleanMetric.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/Result.h"
+#include "nsString.h"
+
+namespace mozilla::glean {
+
+namespace impl {
+
+class CounterMetric {
+ public:
+ constexpr explicit CounterMetric(uint32_t aId) : mId(aId) {}
+
+ /*
+ * Increases the counter by `amount`.
+ *
+ * @param aAmount The amount to increase by. Should be positive.
+ */
+ void Add(int32_t aAmount = 1) const;
+
+ /**
+ * **Test-only API**
+ *
+ * Gets the currently stored value as an integer.
+ *
+ * This function will attempt to await the last parent-process task (if any)
+ * writing to the the metric's storage engine before returning a value.
+ * This function will not wait for data from child processes.
+ *
+ * This doesn't clear the stored value.
+ * Parent process only. Panics in child processes.
+ *
+ * @param aPingName The (optional) name of the ping to retrieve the metric
+ * for. Defaults to the first value in `send_in_pings`.
+ *
+ * @return value of the stored metric, or Nothing() if there is no value.
+ */
+ Result<Maybe<int32_t>, nsCString> TestGetValue(
+ const nsACString& aPingName = nsCString()) const;
+
+ private:
+ const uint32_t mId;
+};
+} // namespace impl
+
+class GleanCounter final : public GleanMetric {
+ public:
+ explicit GleanCounter(uint32_t id, nsISupports* aParent)
+ : GleanMetric(aParent), mCounter(id) {}
+
+ virtual JSObject* WrapObject(
+ JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override final;
+
+ void Add(int32_t aAmount);
+
+ dom::Nullable<int32_t> TestGetValue(const nsACString& aPingName,
+ ErrorResult& aRv);
+
+ private:
+ virtual ~GleanCounter() = default;
+
+ const impl::CounterMetric mCounter;
+};
+
+} // namespace mozilla::glean
+
+#endif /* mozilla_glean_GleanCounter_h */
diff --git a/toolkit/components/glean/bindings/private/CustomDistribution.cpp b/toolkit/components/glean/bindings/private/CustomDistribution.cpp
new file mode 100644
index 0000000000..2f0226cb58
--- /dev/null
+++ b/toolkit/components/glean/bindings/private/CustomDistribution.cpp
@@ -0,0 +1,105 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/glean/bindings/CustomDistribution.h"
+
+#include "mozilla/ResultVariant.h"
+#include "mozilla/dom/GleanMetricsBinding.h"
+#include "mozilla/glean/bindings/HistogramGIFFTMap.h"
+#include "mozilla/glean/fog_ffi_generated.h"
+#include "nsJSUtils.h"
+#include "nsPrintfCString.h"
+#include "nsString.h"
+#include "js/PropertyAndElement.h" // JS_DefineProperty
+
+namespace mozilla::glean {
+
+namespace impl {
+
+void CustomDistributionMetric::AccumulateSamples(
+ const nsTArray<uint64_t>& aSamples) const {
+ auto hgramId = HistogramIdForMetric(mId);
+ if (hgramId) {
+ auto id = hgramId.extract();
+ // N.B.: There is an `Accumulate(nsTArray<T>)`, but `T` is `uint32_t` and
+ // we got `uint64_t`s here.
+ for (auto sample : aSamples) {
+ Telemetry::Accumulate(id, sample);
+ }
+ }
+ fog_custom_distribution_accumulate_samples(mId, &aSamples);
+}
+
+void CustomDistributionMetric::AccumulateSamplesSigned(
+ const nsTArray<int64_t>& aSamples) const {
+ auto hgramId = HistogramIdForMetric(mId);
+ if (hgramId) {
+ auto id = hgramId.extract();
+ // N.B.: There is an `Accumulate(nsTArray<T>)`, but `T` is `uint32_t` and
+ // we got `int64_t`s here.
+ for (auto sample : aSamples) {
+ Telemetry::Accumulate(id, sample);
+ }
+ }
+ fog_custom_distribution_accumulate_samples_signed(mId, &aSamples);
+}
+
+Result<Maybe<DistributionData>, nsCString>
+CustomDistributionMetric::TestGetValue(const nsACString& aPingName) const {
+ nsCString err;
+ if (fog_custom_distribution_test_get_error(mId, &err)) {
+ return Err(err);
+ }
+ if (!fog_custom_distribution_test_has_value(mId, &aPingName)) {
+ return Maybe<DistributionData>();
+ }
+ nsTArray<uint64_t> buckets;
+ nsTArray<uint64_t> counts;
+ uint64_t sum;
+ fog_custom_distribution_test_get_value(mId, &aPingName, &sum, &buckets,
+ &counts);
+ return Some(DistributionData(buckets, counts, sum));
+}
+
+} // namespace impl
+
+/* virtual */
+JSObject* GleanCustomDistribution::WrapObject(
+ JSContext* aCx, JS::Handle<JSObject*> aGivenProto) {
+ return dom::GleanCustomDistribution_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+void GleanCustomDistribution::AccumulateSamples(
+ const dom::Sequence<int64_t>& aSamples) {
+ mCustomDist.AccumulateSamplesSigned(aSamples);
+}
+
+void GleanCustomDistribution::TestGetValue(
+ const nsACString& aPingName,
+ dom::Nullable<dom::GleanDistributionData>& aRetval, ErrorResult& aRv) {
+ auto result = mCustomDist.TestGetValue(aPingName);
+ if (result.isErr()) {
+ aRv.ThrowDataError(result.unwrapErr());
+ return;
+ }
+ auto optresult = result.unwrap();
+ if (optresult.isNothing()) {
+ return;
+ }
+
+ dom::GleanDistributionData ret;
+ ret.mSum = optresult.ref().sum;
+ auto& data = optresult.ref().values;
+ for (const auto& entry : data) {
+ dom::binding_detail::RecordEntry<nsCString, uint64_t> bucket;
+ bucket.mKey = nsPrintfCString("%" PRIu64, entry.GetKey());
+ bucket.mValue = entry.GetData();
+ ret.mValues.Entries().EmplaceBack(std::move(bucket));
+ }
+ aRetval.SetValue(std::move(ret));
+}
+
+} // namespace mozilla::glean
diff --git a/toolkit/components/glean/bindings/private/CustomDistribution.h b/toolkit/components/glean/bindings/private/CustomDistribution.h
new file mode 100644
index 0000000000..8227b024ad
--- /dev/null
+++ b/toolkit/components/glean/bindings/private/CustomDistribution.h
@@ -0,0 +1,95 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_glean_GleanCustomDistribution_h
+#define mozilla_glean_GleanCustomDistribution_h
+
+#include "mozilla/dom/BindingDeclarations.h"
+#include "mozilla/glean/bindings/GleanMetric.h"
+#include "mozilla/glean/bindings/DistributionData.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/Result.h"
+#include "nsTArray.h"
+
+namespace mozilla::dom {
+struct GleanDistributionData;
+} // namespace mozilla::dom
+
+namespace mozilla::glean {
+
+namespace impl {
+
+class CustomDistributionMetric {
+ public:
+ constexpr explicit CustomDistributionMetric(uint32_t aId) : mId(aId) {}
+
+ /**
+ * Accumulates the provided samples in the metric.
+ *
+ * @param aSamples The vector holding the samples to be recorded by the
+ * metric.
+ */
+ void AccumulateSamples(const nsTArray<uint64_t>& aSamples) const;
+
+ /**
+ * Accumulates the provided samples in the metric.
+ *
+ * @param aSamples The vector holding the samples to be recorded by the
+ * metric.
+ *
+ * Notes: Discards any negative value in `samples`
+ * and reports an `InvalidValue` error for each of them.
+ */
+ void AccumulateSamplesSigned(const nsTArray<int64_t>& aSamples) const;
+
+ /**
+ * **Test-only API**
+ *
+ * Gets the currently stored value as a DistributionData.
+ *
+ * This function will attempt to await the last parent-process task (if any)
+ * writing to the the metric's storage engine before returning a value.
+ * This function will not wait for data from child processes.
+ *
+ * This doesn't clear the stored value.
+ * Parent process only. Panics in child processes.
+ *
+ * @param aPingName The (optional) name of the ping to retrieve the metric
+ * for. Defaults to the first value in `send_in_pings`.
+ *
+ * @return value of the stored metric, or Nothing() if there is no value.
+ */
+ Result<Maybe<DistributionData>, nsCString> TestGetValue(
+ const nsACString& aPingName = nsCString()) const;
+
+ private:
+ const uint32_t mId;
+};
+} // namespace impl
+
+class GleanCustomDistribution final : public GleanMetric {
+ public:
+ explicit GleanCustomDistribution(uint64_t aId, nsISupports* aParent)
+ : GleanMetric(aParent), mCustomDist(aId) {}
+
+ virtual JSObject* WrapObject(
+ JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override final;
+
+ void AccumulateSamples(const dom::Sequence<int64_t>& aSamples);
+
+ void TestGetValue(const nsACString& aPingName,
+ dom::Nullable<dom::GleanDistributionData>& aRetval,
+ ErrorResult& aRv);
+
+ private:
+ virtual ~GleanCustomDistribution() = default;
+
+ const impl::CustomDistributionMetric mCustomDist;
+};
+
+} // namespace mozilla::glean
+
+#endif /* mozilla_glean_GleanCustomDistribution_h */
diff --git a/toolkit/components/glean/bindings/private/Datetime.cpp b/toolkit/components/glean/bindings/private/Datetime.cpp
new file mode 100644
index 0000000000..ac88c67426
--- /dev/null
+++ b/toolkit/components/glean/bindings/private/Datetime.cpp
@@ -0,0 +1,121 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/glean/bindings/Datetime.h"
+
+#include "jsapi.h"
+#include "js/Date.h"
+#include "nsString.h"
+#include "mozilla/ResultVariant.h"
+#include "mozilla/dom/GleanMetricsBinding.h"
+#include "mozilla/glean/bindings/ScalarGIFFTMap.h"
+#include "mozilla/glean/fog_ffi_generated.h"
+#include "prtime.h"
+
+namespace mozilla::glean {
+
+namespace impl {
+
+void DatetimeMetric::Set(const PRExplodedTime* aValue) const {
+ PRExplodedTime exploded;
+ if (!aValue) {
+ PR_ExplodeTime(PR_Now(), PR_LocalTimeParameters, &exploded);
+ } else {
+ exploded = *aValue;
+ }
+
+ auto id = ScalarIdForMetric(mId);
+ if (id) {
+ const uint32_t buflen = 64; // More than enough for now.
+ char buf[buflen];
+ uint32_t written = PR_FormatTime(buf, buflen, "%FT%T%z", &exploded);
+ if (written > 2 && written < 64) {
+ // Format's still not quite there. Gotta put a `:` between timezone
+ // hours and minutes
+ buf[written] = '\0';
+ buf[written - 1] = buf[written - 2];
+ buf[written - 2] = buf[written - 3];
+ buf[written - 3] = ':';
+ Telemetry::ScalarSet(id.extract(), NS_ConvertASCIItoUTF16(buf));
+ }
+ }
+
+ int32_t offset =
+ exploded.tm_params.tp_gmt_offset + exploded.tm_params.tp_dst_offset;
+ FogDatetime dt{exploded.tm_year,
+ static_cast<uint32_t>(exploded.tm_month + 1),
+ static_cast<uint32_t>(exploded.tm_mday),
+ static_cast<uint32_t>(exploded.tm_hour),
+ static_cast<uint32_t>(exploded.tm_min),
+ static_cast<uint32_t>(exploded.tm_sec),
+ static_cast<uint32_t>(exploded.tm_usec * 1000),
+ offset};
+ fog_datetime_set(mId, &dt);
+}
+
+Result<Maybe<PRExplodedTime>, nsCString> DatetimeMetric::TestGetValue(
+ const nsACString& aPingName) const {
+ nsCString err;
+ if (fog_datetime_test_get_error(mId, &err)) {
+ return Err(err);
+ }
+ if (!fog_datetime_test_has_value(mId, &aPingName)) {
+ return Maybe<PRExplodedTime>();
+ }
+ FogDatetime ret{0};
+ fog_datetime_test_get_value(mId, &aPingName, &ret);
+ PRExplodedTime pret{0};
+ pret.tm_year = static_cast<PRInt16>(ret.year);
+ pret.tm_month = static_cast<PRInt32>(ret.month - 1);
+ pret.tm_mday = static_cast<PRInt32>(ret.day);
+ pret.tm_hour = static_cast<PRInt32>(ret.hour);
+ pret.tm_min = static_cast<PRInt32>(ret.minute);
+ pret.tm_sec = static_cast<PRInt32>(ret.second);
+ pret.tm_usec = static_cast<PRInt32>(ret.nano / 1000); // truncated is fine
+ pret.tm_params.tp_gmt_offset = static_cast<PRInt32>(ret.offset_seconds);
+ return Some(std::move(pret));
+}
+
+} // namespace impl
+
+/* virtual */
+JSObject* GleanDatetime::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return dom::GleanDatetime_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+void GleanDatetime::Set(const dom::Optional<int64_t>& aValue) {
+ if (aValue.WasPassed()) {
+ PRExplodedTime exploded;
+ PR_ExplodeTime(aValue.Value(), PR_LocalTimeParameters, &exploded);
+ mDatetime.Set(&exploded);
+ } else {
+ mDatetime.Set();
+ }
+}
+
+void GleanDatetime::TestGetValue(JSContext* aCx, const nsACString& aPingName,
+ JS::MutableHandle<JS::Value> aResult,
+ ErrorResult& aRv) {
+ auto result = mDatetime.TestGetValue(aPingName);
+ if (result.isErr()) {
+ aResult.set(JS::UndefinedValue());
+ aRv.ThrowDataError(result.unwrapErr());
+ return;
+ }
+ auto optresult = result.unwrap();
+ if (optresult.isNothing()) {
+ aResult.set(JS::UndefinedValue());
+ } else {
+ double millis =
+ static_cast<double>(PR_ImplodeTime(optresult.ptr())) / PR_USEC_PER_MSEC;
+ JS::Rooted<JSObject*> root(aCx,
+ JS::NewDateObject(aCx, JS::TimeClip(millis)));
+ aResult.setObject(*root);
+ }
+}
+
+} // 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..1dc4dfbe70
--- /dev/null
+++ b/toolkit/components/glean/bindings/private/Datetime.h
@@ -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/. */
+
+#ifndef mozilla_glean_GleanDatetime_h
+#define mozilla_glean_GleanDatetime_h
+
+#include "mozilla/dom/BindingDeclarations.h"
+#include "mozilla/glean/bindings/GleanMetric.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/Result.h"
+#include "nsString.h"
+#include "nsWrapperCache.h"
+#include "prtime.h"
+
+namespace mozilla::glean {
+
+namespace impl {
+
+class DatetimeMetric {
+ public:
+ constexpr explicit DatetimeMetric(uint32_t aId) : mId(aId) {}
+
+ /*
+ * Set the datetime to the provided value, or the local now.
+ *
+ * @param amount The date value to set.
+ */
+ void Set(const PRExplodedTime* aValue = nullptr) const;
+
+ /**
+ * **Test-only API**
+ *
+ * Gets the currently stored value as a PRExplodedTime.
+ *
+ * This function will attempt to await the last parent-process task (if any)
+ * writing to the the metric's storage engine before returning a value.
+ * This function will not wait for data from child processes.
+ *
+ * This doesn't clear the stored value.
+ * Parent process only. Panics in child processes.
+ *
+ * @param aPingName The (optional) name of the ping to retrieve the metric
+ * for. Defaults to the first value in `send_in_pings`.
+ *
+ * @return value of the stored metric, or Nothing() if there is no value.
+ */
+ Result<Maybe<PRExplodedTime>, nsCString> TestGetValue(
+ const nsACString& aPingName = nsCString()) const;
+
+ private:
+ const uint32_t mId;
+};
+} // namespace impl
+
+class GleanDatetime final : public GleanMetric {
+ public:
+ explicit GleanDatetime(uint32_t aId, nsISupports* aParent)
+ : GleanMetric(aParent), mDatetime(aId) {}
+
+ virtual JSObject* WrapObject(
+ JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override final;
+
+ void Set(const dom::Optional<int64_t>& aValue);
+
+ void TestGetValue(JSContext* aCx, const nsACString& aPingName,
+ JS::MutableHandle<JS::Value> aResult, ErrorResult& aRv);
+
+ private:
+ virtual ~GleanDatetime() = default;
+
+ const impl::DatetimeMetric mDatetime;
+};
+
+} // namespace mozilla::glean
+
+#endif /* mozilla_glean_GleanDatetime_h */
diff --git a/toolkit/components/glean/bindings/private/Denominator.cpp b/toolkit/components/glean/bindings/private/Denominator.cpp
new file mode 100644
index 0000000000..459bb002a7
--- /dev/null
+++ b/toolkit/components/glean/bindings/private/Denominator.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/Denominator.h"
+
+#include "nsString.h"
+#include "mozilla/ResultVariant.h"
+#include "mozilla/dom/GleanMetricsBinding.h"
+#include "mozilla/glean/bindings/ScalarGIFFTMap.h"
+#include "mozilla/glean/fog_ffi_generated.h"
+
+namespace mozilla::glean {
+
+namespace impl {
+
+void DenominatorMetric::Add(int32_t aAmount) const {
+ auto scalarId = ScalarIdForMetric(mId);
+ if (scalarId && aAmount >= 0) {
+ Telemetry::ScalarAdd(scalarId.extract(), aAmount);
+ }
+ fog_denominator_add(mId, aAmount);
+}
+
+Result<Maybe<int32_t>, nsCString> DenominatorMetric::TestGetValue(
+ const nsACString& aPingName) const {
+ nsCString err;
+ if (fog_denominator_test_get_error(mId, &err)) {
+ return Err(err);
+ }
+ if (!fog_denominator_test_has_value(mId, &aPingName)) {
+ return Maybe<int32_t>();
+ }
+ return Some(fog_denominator_test_get_value(mId, &aPingName));
+}
+
+} // namespace impl
+
+/* virtual */
+JSObject* GleanDenominator::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return dom::GleanDenominator_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+void GleanDenominator::Add(int32_t aAmount) { mDenominator.Add(aAmount); }
+
+dom::Nullable<int32_t> GleanDenominator::TestGetValue(
+ const nsACString& aPingName, ErrorResult& aRv) {
+ dom::Nullable<int32_t> ret;
+ auto result = mDenominator.TestGetValue(aPingName);
+ if (result.isErr()) {
+ aRv.ThrowDataError(result.unwrapErr());
+ return ret;
+ }
+ auto optresult = result.unwrap();
+ if (!optresult.isNothing()) {
+ ret.SetValue(optresult.value());
+ }
+ return ret;
+}
+
+} // namespace mozilla::glean
diff --git a/toolkit/components/glean/bindings/private/Denominator.h b/toolkit/components/glean/bindings/private/Denominator.h
new file mode 100644
index 0000000000..c52b0cb7ad
--- /dev/null
+++ b/toolkit/components/glean/bindings/private/Denominator.h
@@ -0,0 +1,77 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_glean_GleanDenominator_h
+#define mozilla_glean_GleanDenominator_h
+
+#include "mozilla/dom/BindingDeclarations.h"
+#include "mozilla/glean/bindings/GleanMetric.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/Result.h"
+#include "nsString.h"
+
+namespace mozilla::glean {
+
+namespace impl {
+
+class DenominatorMetric {
+ public:
+ constexpr explicit DenominatorMetric(uint32_t aId) : mId(aId) {}
+
+ /*
+ * Increases the counter by `amount`.
+ *
+ * @param aAmount The amount to increase by. Should be positive.
+ */
+ void Add(int32_t aAmount = 1) const;
+
+ /**
+ * **Test-only API**
+ *
+ * Gets the currently stored value as an integer.
+ *
+ * This function will attempt to await the last parent-process task (if any)
+ * writing to the the metric's storage engine before returning a value.
+ * This function will not wait for data from child processes.
+ *
+ * This doesn't clear the stored value.
+ * Parent process only. Panics in child processes.
+ *
+ * @param aPingName The (optional) name of the ping to retrieve the metric
+ * for. Defaults to the first value in `send_in_pings`.
+ *
+ * @return value of the stored metric, or Nothing() if there is no value.
+ */
+ Result<Maybe<int32_t>, nsCString> TestGetValue(
+ const nsACString& aPingName = nsCString()) const;
+
+ private:
+ const uint32_t mId;
+};
+} // namespace impl
+
+class GleanDenominator final : public GleanMetric {
+ public:
+ explicit GleanDenominator(uint32_t id, nsISupports* aParent)
+ : GleanMetric(aParent), mDenominator(id) {}
+
+ virtual JSObject* WrapObject(
+ JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override final;
+
+ void Add(int32_t aAmount);
+
+ dom::Nullable<int32_t> TestGetValue(const nsACString& aPingName,
+ ErrorResult& aRv);
+
+ private:
+ virtual ~GleanDenominator() = default;
+
+ const impl::DenominatorMetric mDenominator;
+};
+
+} // namespace mozilla::glean
+
+#endif /* mozilla_glean_GleanDenominator_h */
diff --git a/toolkit/components/glean/bindings/private/DistributionData.h b/toolkit/components/glean/bindings/private/DistributionData.h
new file mode 100644
index 0000000000..6ff995f222
--- /dev/null
+++ b/toolkit/components/glean/bindings/private/DistributionData.h
@@ -0,0 +1,32 @@
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_glean_DistributionData_h
+#define mozilla_glean_DistributionData_h
+
+#include "nsTHashMap.h"
+
+namespace mozilla::glean {
+
+struct DistributionData final {
+ uint64_t sum;
+ nsTHashMap<nsUint64HashKey, uint64_t> values;
+
+ /**
+ * Create distribution data from the buckets, counts and sum,
+ * as returned by `fog_*_distribution_test_get_value`.
+ */
+ DistributionData(const nsTArray<uint64_t>& aBuckets,
+ const nsTArray<uint64_t>& aCounts, uint64_t aSum)
+ : sum(aSum) {
+ for (size_t i = 0; i < aBuckets.Length(); ++i) {
+ this->values.InsertOrUpdate(aBuckets[i], aCounts[i]);
+ }
+ }
+};
+
+} // namespace mozilla::glean
+
+#endif /* mozilla_glean_DistributionData_h */
diff --git a/toolkit/components/glean/bindings/private/Event.cpp b/toolkit/components/glean/bindings/private/Event.cpp
new file mode 100644
index 0000000000..6d4065947d
--- /dev/null
+++ b/toolkit/components/glean/bindings/private/Event.cpp
@@ -0,0 +1,110 @@
+/* -*- 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 "nsString.h"
+#include "mozilla/dom/GleanMetricsBinding.h"
+#include "mozilla/dom/ToJSValue.h"
+#include "mozilla/glean/bindings/Common.h"
+#include "jsapi.h"
+#include "js/PropertyAndElement.h" // JS_DefineElement, JS_DefineProperty, JS_Enumerate, JS_GetProperty, JS_GetPropertyById
+
+namespace mozilla::glean {
+
+/* virtual */
+JSObject* GleanEvent::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return dom::GleanEvent_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+// Convert all capital letters to "_x" where "x" is the corresponding lowercase.
+nsCString camelToSnake(const nsACString& aCamel) {
+ nsCString snake;
+ const auto* start = aCamel.BeginReading();
+ const auto* end = aCamel.EndReading();
+ for (; start != end; ++start) {
+ if ('A' <= *start && *start <= 'Z') {
+ snake.AppendLiteral("_");
+ snake.Append(static_cast<char>(std::tolower(*start)));
+ } else {
+ snake.Append(*start);
+ }
+ }
+ return snake;
+}
+
+void GleanEvent::Record(
+ const dom::Optional<dom::Record<nsCString, nsCString>>& aExtra) {
+ if (!aExtra.WasPassed()) {
+ mEvent.Record();
+ return;
+ }
+
+ nsTArray<nsCString> extraKeys;
+ nsTArray<nsCString> extraValues;
+ CopyableTArray<Telemetry::EventExtraEntry> telExtras;
+ for (const auto& entry : aExtra.Value().Entries()) {
+ if (entry.mValue.IsVoid()) {
+ // Someone passed undefined/null for this value.
+ // Pretend it wasn't here.
+ continue;
+ }
+ // We accept camelCase extra keys, but Glean requires snake_case.
+ auto snakeKey = camelToSnake(entry.mKey);
+
+ extraKeys.AppendElement(snakeKey);
+ extraValues.AppendElement(entry.mValue);
+ telExtras.EmplaceBack(Telemetry::EventExtraEntry{entry.mKey, entry.mValue});
+ }
+
+ // Since this calls the implementation directly, we need to implement GIFFT
+ // here as well as in EventMetric::Record.
+ auto id = EventIdForMetric(mEvent.mId);
+ if (id) {
+ Telemetry::RecordEvent(id.extract(), Nothing(),
+ telExtras.IsEmpty() ? Nothing() : Some(telExtras));
+ }
+
+ // Calling the implementation directly, because we have a `string->string`
+ // map, not a `T->string` map the C++ API expects.
+ impl::fog_event_record(mEvent.mId, &extraKeys, &extraValues);
+}
+
+void GleanEvent::TestGetValue(
+ const nsACString& aPingName,
+ dom::Nullable<nsTArray<dom::GleanEventRecord>>& aResult, ErrorResult& aRv) {
+ auto resEvents = mEvent.TestGetValue(aPingName);
+ if (resEvents.isErr()) {
+ aRv.ThrowDataError(resEvents.unwrapErr());
+ return;
+ }
+ auto optEvents = resEvents.unwrap();
+ if (optEvents.isNothing()) {
+ return;
+ }
+
+ nsTArray<dom::GleanEventRecord> ret;
+ for (auto& event : optEvents.extract()) {
+ dom::GleanEventRecord record;
+ if (!event.mExtra.IsEmpty()) {
+ record.mExtra.Construct();
+ for (auto& extraEntry : event.mExtra) {
+ dom::binding_detail::RecordEntry<nsCString, nsCString> extra;
+ extra.mKey = std::get<0>(extraEntry);
+ extra.mValue = std::get<1>(extraEntry);
+ record.mExtra.Value().Entries().EmplaceBack(std::move(extra));
+ }
+ }
+ record.mCategory = event.mCategory;
+ record.mName = event.mName;
+ record.mTimestamp = event.mTimestamp;
+ ret.EmplaceBack(std::move(record));
+ }
+ aResult.SetValue(std::move(ret));
+}
+
+} // 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..72fd70410c
--- /dev/null
+++ b/toolkit/components/glean/bindings/private/Event.h
@@ -0,0 +1,177 @@
+/* -*- 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 "mozilla/dom/BindingDeclarations.h"
+#include "mozilla/dom/Record.h"
+#include "mozilla/glean/bindings/EventGIFFTMap.h"
+#include "mozilla/glean/bindings/GleanMetric.h"
+#include "mozilla/glean/fog_ffi_generated.h"
+#include "mozilla/ResultVariant.h"
+
+#include "nsString.h"
+#include "nsTArray.h"
+
+namespace mozilla::dom {
+// forward declaration
+struct GleanEventRecord;
+} // namespace mozilla::dom
+
+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<std::tuple<nsCString, nsCString>> mExtra;
+};
+
+template <class T>
+class EventMetric {
+ friend class mozilla::glean::GleanEvent;
+
+ public:
+ constexpr explicit EventMetric(uint32_t id) : mId(id) {}
+
+ /**
+ * Record an event.
+ *
+ * @param aExtras The list of (extra key, value) pairs. Allowed extra keys are
+ * defined in the metric definition.
+ * If the wrong keys are used or values are too large
+ * an error is report and no event is recorded.
+ */
+ void Record(const Maybe<T>& aExtras = Nothing()) const {
+ auto id = EventIdForMetric(mId);
+ if (id) {
+ // NB. In case `aExtras` is filled we call `ToFfiExtra`, causing
+ // twice the required allocation. We could be smarter and reuse the data.
+ // But this is GIFFT-only allocation, so wait to be told it's a problem.
+ Maybe<CopyableTArray<Telemetry::EventExtraEntry>> telExtras;
+ if (aExtras) {
+ CopyableTArray<Telemetry::EventExtraEntry> extras;
+ auto serializedExtras = aExtras->ToFfiExtra();
+ auto keys = std::move(std::get<0>(serializedExtras));
+ auto values = std::move(std::get<1>(serializedExtras));
+ for (size_t i = 0; i < keys.Length(); i++) {
+ extras.EmplaceBack(Telemetry::EventExtraEntry{keys[i], values[i]});
+ }
+ telExtras = Some(extras);
+ }
+ Telemetry::RecordEvent(id.extract(), Nothing(), telExtras);
+ }
+ if (aExtras) {
+ auto extra = aExtras->ToFfiExtra();
+ fog_event_record(mId, &std::get<0>(extra), &std::get<1>(extra));
+ } else {
+ nsTArray<nsCString> keys;
+ nsTArray<nsCString> vals;
+ fog_event_record(mId, &keys, &vals);
+ }
+ }
+
+ /**
+ * **Test-only API**
+ *
+ * Get a list of currently stored events for this event metric.
+ *
+ * This function will attempt to await the last parent-process task (if any)
+ * writing to the the metric's storage engine before returning a value.
+ * This function will not wait for data from child processes.
+ *
+ * This doesn't clear the stored value.
+ * Parent process only. Panics in child processes.
+ *
+ * @param aPingName The (optional) name of the ping to retrieve the metric
+ * for. Defaults to the first value in `send_in_pings`.
+ *
+ * @return value of the stored metric, or Nothing() if there is no value.
+ */
+ Result<Maybe<nsTArray<RecordedEvent>>, nsCString> TestGetValue(
+ const nsACString& aPingName = nsCString()) const {
+ nsCString err;
+ if (fog_event_test_get_error(mId, &err)) {
+ return Err(err);
+ }
+
+ if (!fog_event_test_has_value(mId, &aPingName)) {
+ return Maybe<nsTArray<RecordedEvent>>();
+ }
+
+ nsTArray<FfiRecordedEvent> events;
+ fog_event_test_get_value(mId, &aPingName, &events);
+
+ nsTArray<RecordedEvent> result;
+ for (const auto& event : events) {
+ auto ev = result.AppendElement();
+ ev->mTimestamp = event.timestamp;
+ ev->mCategory.Append(event.category);
+ ev->mName.Assign(event.name);
+
+ MOZ_ASSERT(event.extras.Length() % 2 == 0);
+ ev->mExtra.SetCapacity(event.extras.Length() / 2);
+ for (unsigned int i = 0; i < event.extras.Length(); i += 2) {
+ // keys & values are interleaved.
+ nsCString key = std::move(event.extras[i]);
+ nsCString value = std::move(event.extras[i + 1]);
+ ev->mExtra.AppendElement(
+ std::make_tuple(std::move(key), std::move(value)));
+ }
+ }
+ return Some(std::move(result));
+ }
+
+ private:
+ static const nsCString ExtraStringForKey(uint32_t aKey);
+
+ const uint32_t mId;
+};
+
+} // namespace impl
+
+struct NoExtraKeys {
+ std::tuple<nsTArray<nsCString>, nsTArray<nsCString>> ToFfiExtra() const {
+ nsTArray<nsCString> extraKeys;
+ nsTArray<nsCString> extraValues;
+ return std::make_tuple(std::move(extraKeys), std::move(extraValues));
+ }
+};
+
+class GleanEvent final : public GleanMetric {
+ public:
+ explicit GleanEvent(uint32_t id, nsISupports* aParent)
+ : GleanMetric(aParent), mEvent(id) {}
+
+ virtual JSObject* WrapObject(
+ JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override final;
+
+ void Record(const dom::Optional<dom::Record<nsCString, nsCString>>& aExtra);
+
+ void TestGetValue(const nsACString& aPingName,
+ dom::Nullable<nsTArray<dom::GleanEventRecord>>& aResult,
+ ErrorResult& aRv);
+
+ private:
+ virtual ~GleanEvent() = default;
+
+ const impl::EventMetric<NoExtraKeys> mEvent;
+};
+
+} // namespace mozilla::glean
+
+#endif /* mozilla_glean_GleanEvent.h */
diff --git a/toolkit/components/glean/bindings/private/Labeled.cpp b/toolkit/components/glean/bindings/private/Labeled.cpp
new file mode 100644
index 0000000000..23527708e0
--- /dev/null
+++ b/toolkit/components/glean/bindings/private/Labeled.cpp
@@ -0,0 +1,49 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/glean/bindings/Labeled.h"
+
+#include "mozilla/dom/GleanBinding.h"
+#include "mozilla/glean/fog_ffi_generated.h"
+#include "mozilla/glean/bindings/GleanJSMetricsLookup.h"
+#include "mozilla/glean/bindings/MetricTypes.h"
+#include "mozilla/glean/bindings/ScalarGIFFTMap.h"
+#include "nsString.h"
+
+namespace mozilla::glean {
+
+JSObject* GleanLabeled::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return dom::GleanLabeled_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+already_AddRefed<GleanMetric> GleanLabeled::NamedGetter(const nsAString& aName,
+ bool& aFound) {
+ auto label = NS_ConvertUTF16toUTF8(aName);
+ // All strings will map to a label. Either a valid one or `__other__`.
+ aFound = true;
+ uint32_t submetricId = 0;
+ already_AddRefed<GleanMetric> submetric =
+ NewSubMetricFromIds(mTypeId, mId, label, &submetricId, mParent);
+
+ auto mirrorId = ScalarIdForMetric(mId);
+ if (mirrorId) {
+ GetLabeledMirrorLock().apply([&](auto& lock) {
+ auto tuple = std::make_tuple<Telemetry::ScalarID, nsString>(
+ mirrorId.extract(), nsString(aName));
+ lock.ref()->InsertOrUpdate(submetricId, std::move(tuple));
+ });
+ }
+ return submetric;
+}
+
+bool GleanLabeled::NameIsEnumerable(const nsAString& aName) { return false; }
+
+void GleanLabeled::GetSupportedNames(nsTArray<nsString>& aNames) {
+ // We really don't know, so don't do anything.
+}
+
+} // namespace mozilla::glean
diff --git a/toolkit/components/glean/bindings/private/Labeled.h b/toolkit/components/glean/bindings/private/Labeled.h
new file mode 100644
index 0000000000..65e31bd2bd
--- /dev/null
+++ b/toolkit/components/glean/bindings/private/Labeled.h
@@ -0,0 +1,248 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_glean_Labeled_h
+#define mozilla_glean_Labeled_h
+
+#include "nsISupports.h"
+#include "nsWrapperCache.h"
+#include "mozilla/dom/BindingDeclarations.h"
+#include "mozilla/glean/bindings/Boolean.h"
+#include "mozilla/glean/bindings/Counter.h"
+#include "mozilla/glean/bindings/GleanMetric.h"
+#include "mozilla/glean/bindings/ScalarGIFFTMap.h"
+#include "mozilla/glean/bindings/String.h"
+#include "mozilla/glean/fog_ffi_generated.h"
+
+enum class DynamicLabel : uint16_t;
+
+namespace mozilla::glean {
+
+namespace impl {
+
+template <typename T, typename E>
+class Labeled {
+ public:
+ constexpr explicit Labeled<T, E>(uint32_t id) : mId(id) {}
+
+ /**
+ * Gets a specific metric for a given label.
+ *
+ * If a set of acceptable labels were specified in the `metrics.yaml` file,
+ * and the given label is not in the set, it will be recorded under the
+ * special `OTHER_LABEL` label.
+ *
+ * If a set of acceptable labels was not specified in the `metrics.yaml` file,
+ * only the first 16 unique labels will be used.
+ * After that, any additional labels will be recorded under the special
+ * `OTHER_LABEL` label.
+ *
+ * @param aLabel - a snake_case string under 30 characters in length,
+ * otherwise the metric will be recorded under the special
+ * `OTHER_LABEL` label and an error will be recorded.
+ */
+ T Get(const nsACString& aLabel) const;
+
+ /**
+ * Gets a specific metric for a given label, using the label's enum variant.
+ *
+ * @param aLabel - a variant of this label's label enum.
+ */
+ T EnumGet(E aLabel) const;
+
+ private:
+ const uint32_t mId;
+};
+
+static inline void UpdateLabeledMirror(Telemetry::ScalarID aMirrorId,
+ uint32_t aSubmetricId,
+ const nsACString& aLabel) {
+ GetLabeledMirrorLock().apply([&](auto& lock) {
+ auto tuple = std::make_tuple<Telemetry::ScalarID, nsString>(
+ std::move(aMirrorId), NS_ConvertUTF8toUTF16(aLabel));
+ lock.ref()->InsertOrUpdate(aSubmetricId, std::move(tuple));
+ });
+}
+
+template <typename E>
+class Labeled<BooleanMetric, E> {
+ public:
+ constexpr explicit Labeled(uint32_t id) : mId(id) {}
+
+ BooleanMetric Get(const nsACString& aLabel) const {
+ auto submetricId = fog_labeled_boolean_get(mId, &aLabel);
+ // If this labeled metric is mirrored, we need to map the submetric id back
+ // to the label string and mirrored scalar so we can mirror its operations.
+ auto mirrorId = ScalarIdForMetric(mId);
+ if (mirrorId) {
+ UpdateLabeledMirror(mirrorId.extract(), submetricId, aLabel);
+ }
+ return BooleanMetric(submetricId);
+ }
+
+ BooleanMetric EnumGet(E aLabel) const {
+ auto submetricId =
+ fog_labeled_boolean_enum_get(mId, static_cast<uint16_t>(aLabel));
+ auto mirrorId = ScalarIdForMetric(mId);
+ if (mirrorId) {
+ // Telemetry's keyed scalars operate on (16-bit) strings,
+ // so we're going to need the string for this enum.
+ nsCString label;
+ fog_labeled_enum_to_str(mId, static_cast<uint16_t>(aLabel), &label);
+ UpdateLabeledMirror(mirrorId.extract(), submetricId, label);
+ }
+ return BooleanMetric(submetricId);
+ }
+
+ private:
+ const uint32_t mId;
+};
+
+template <typename E>
+class Labeled<CounterMetric, E> {
+ public:
+ constexpr explicit Labeled(uint32_t id) : mId(id) {}
+
+ CounterMetric Get(const nsACString& aLabel) const {
+ auto submetricId = fog_labeled_counter_get(mId, &aLabel);
+ // If this labeled metric is mirrored, we need to map the submetric id back
+ // to the label string and mirrored scalar so we can mirror its operations.
+ auto mirrorId = ScalarIdForMetric(mId);
+ if (mirrorId) {
+ UpdateLabeledMirror(mirrorId.extract(), submetricId, aLabel);
+ }
+ return CounterMetric(submetricId);
+ }
+
+ CounterMetric EnumGet(E aLabel) const {
+ auto submetricId =
+ fog_labeled_counter_enum_get(mId, static_cast<uint16_t>(aLabel));
+ auto mirrorId = ScalarIdForMetric(mId);
+ if (mirrorId) {
+ // Telemetry's keyed scalars operate on (16-bit) strings,
+ // so we're going to need the string for this enum.
+ nsCString label;
+ fog_labeled_enum_to_str(mId, static_cast<uint16_t>(aLabel), &label);
+ UpdateLabeledMirror(mirrorId.extract(), submetricId, label);
+ }
+ return CounterMetric(submetricId);
+ }
+
+ private:
+ const uint32_t mId;
+};
+
+template <typename E>
+class Labeled<StringMetric, E> {
+ public:
+ constexpr explicit Labeled(uint32_t id) : mId(id) {}
+
+ StringMetric Get(const nsACString& aLabel) const {
+ auto submetricId = fog_labeled_string_get(mId, &aLabel);
+ // Why no GIFFT map here?
+ // Labeled Strings can't be mirrored. Telemetry has no compatible probe.
+ return StringMetric(submetricId);
+ }
+
+ StringMetric EnumGet(E aLabel) const {
+ auto submetricId =
+ fog_labeled_string_enum_get(mId, static_cast<uint16_t>(aLabel));
+ // Why no GIFFT map here?
+ // Labeled Strings can't be mirrored. Telemetry has no compatible probe.
+ return StringMetric(submetricId);
+ }
+
+ private:
+ const uint32_t mId;
+};
+
+template <>
+class Labeled<BooleanMetric, DynamicLabel> {
+ public:
+ constexpr explicit Labeled(uint32_t id) : mId(id) {}
+
+ BooleanMetric Get(const nsACString& aLabel) const {
+ auto submetricId = fog_labeled_boolean_get(mId, &aLabel);
+ // If this labeled metric is mirrored, we need to map the submetric id back
+ // to the label string and mirrored scalar so we can mirror its operations.
+ auto mirrorId = ScalarIdForMetric(mId);
+ if (mirrorId) {
+ UpdateLabeledMirror(mirrorId.extract(), submetricId, aLabel);
+ }
+ return BooleanMetric(submetricId);
+ }
+
+ BooleanMetric EnumGet(DynamicLabel aLabel) const = delete;
+
+ private:
+ const uint32_t mId;
+};
+
+template <>
+class Labeled<CounterMetric, DynamicLabel> {
+ public:
+ constexpr explicit Labeled(uint32_t id) : mId(id) {}
+
+ CounterMetric Get(const nsACString& aLabel) const {
+ auto submetricId = fog_labeled_counter_get(mId, &aLabel);
+ // If this labeled metric is mirrored, we need to map the submetric id back
+ // to the label string and mirrored scalar so we can mirror its operations.
+ auto mirrorId = ScalarIdForMetric(mId);
+ if (mirrorId) {
+ UpdateLabeledMirror(mirrorId.extract(), submetricId, aLabel);
+ }
+ return CounterMetric(submetricId);
+ }
+
+ CounterMetric EnumGet(DynamicLabel aLabel) const = delete;
+
+ private:
+ const uint32_t mId;
+};
+
+template <>
+class Labeled<StringMetric, DynamicLabel> {
+ public:
+ constexpr explicit Labeled(uint32_t id) : mId(id) {}
+
+ StringMetric Get(const nsACString& aLabel) const {
+ auto submetricId = fog_labeled_string_get(mId, &aLabel);
+ // Why no GIFFT map here?
+ // Labeled Strings can't be mirrored. Telemetry has no compatible probe.
+ return StringMetric(submetricId);
+ }
+
+ StringMetric EnumGet(DynamicLabel aLabel) const = delete;
+
+ private:
+ const uint32_t mId;
+};
+
+} // namespace impl
+
+class GleanLabeled final : public GleanMetric {
+ public:
+ explicit GleanLabeled(uint32_t aId, uint32_t aTypeId, nsISupports* aParent)
+ : GleanMetric(aParent), mId(aId), mTypeId(aTypeId) {}
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override final;
+
+ already_AddRefed<GleanMetric> NamedGetter(const nsAString& aName,
+ bool& aFound);
+ bool NameIsEnumerable(const nsAString& aName);
+ void GetSupportedNames(nsTArray<nsString>& aNames);
+
+ private:
+ virtual ~GleanLabeled() = default;
+
+ const uint32_t mId;
+ const uint32_t mTypeId;
+};
+
+} // namespace mozilla::glean
+
+#endif /* mozilla_glean_Labeled_h */
diff --git a/toolkit/components/glean/bindings/private/MemoryDistribution.cpp b/toolkit/components/glean/bindings/private/MemoryDistribution.cpp
new file mode 100644
index 0000000000..a580c5df3c
--- /dev/null
+++ b/toolkit/components/glean/bindings/private/MemoryDistribution.cpp
@@ -0,0 +1,89 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/glean/bindings/MemoryDistribution.h"
+
+#include "mozilla/Components.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/ResultVariant.h"
+#include "mozilla/dom/GleanMetricsBinding.h"
+#include "mozilla/glean/bindings/HistogramGIFFTMap.h"
+#include "mozilla/glean/fog_ffi_generated.h"
+#include "nsIClassInfoImpl.h"
+#include "nsJSUtils.h"
+#include "nsPrintfCString.h"
+#include "nsString.h"
+#include "js/PropertyAndElement.h" // JS_DefineProperty
+
+namespace mozilla::glean {
+
+namespace impl {
+
+void MemoryDistributionMetric::Accumulate(size_t aSample) const {
+ auto hgramId = HistogramIdForMetric(mId);
+ if (hgramId) {
+ Telemetry::Accumulate(hgramId.extract(), aSample);
+ }
+ static_assert(sizeof(size_t) <= sizeof(uint64_t),
+ "Memory distribution samples might overflow.");
+ fog_memory_distribution_accumulate(mId, aSample);
+}
+
+Result<Maybe<DistributionData>, nsCString>
+MemoryDistributionMetric::TestGetValue(const nsACString& aPingName) const {
+ nsCString err;
+ if (fog_memory_distribution_test_get_error(mId, &err)) {
+ return Err(err);
+ }
+ if (!fog_memory_distribution_test_has_value(mId, &aPingName)) {
+ return Maybe<DistributionData>();
+ }
+ nsTArray<uint64_t> buckets;
+ nsTArray<uint64_t> counts;
+ uint64_t sum;
+ fog_memory_distribution_test_get_value(mId, &aPingName, &sum, &buckets,
+ &counts);
+ return Some(DistributionData(buckets, counts, sum));
+}
+
+} // namespace impl
+
+/* virtual */
+JSObject* GleanMemoryDistribution::WrapObject(
+ JSContext* aCx, JS::Handle<JSObject*> aGivenProto) {
+ return dom::GleanMemoryDistribution_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+void GleanMemoryDistribution::Accumulate(uint64_t aSample) {
+ mMemoryDist.Accumulate(aSample);
+}
+
+void GleanMemoryDistribution::TestGetValue(
+ const nsACString& aPingName,
+ dom::Nullable<dom::GleanDistributionData>& aRetval, ErrorResult& aRv) {
+ auto result = mMemoryDist.TestGetValue(aPingName);
+ if (result.isErr()) {
+ aRv.ThrowDataError(result.unwrapErr());
+ return;
+ }
+ auto optresult = result.unwrap();
+ if (optresult.isNothing()) {
+ return;
+ }
+
+ dom::GleanDistributionData ret;
+ ret.mSum = optresult.ref().sum;
+ auto& data = optresult.ref().values;
+ for (const auto& entry : data) {
+ dom::binding_detail::RecordEntry<nsCString, uint64_t> bucket;
+ bucket.mKey = nsPrintfCString("%" PRIu64, entry.GetKey());
+ bucket.mValue = entry.GetData();
+ ret.mValues.Entries().EmplaceBack(std::move(bucket));
+ }
+ aRetval.SetValue(std::move(ret));
+}
+
+} // 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..6b58fc7f75
--- /dev/null
+++ b/toolkit/components/glean/bindings/private/MemoryDistribution.h
@@ -0,0 +1,86 @@
+/* -*- 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/dom/BindingDeclarations.h"
+#include "mozilla/glean/bindings/DistributionData.h"
+#include "mozilla/glean/bindings/GleanMetric.h"
+#include "mozilla/Maybe.h"
+#include "nsTArray.h"
+
+namespace mozilla::dom {
+struct GleanDistributionData;
+} // namespace mozilla::dom
+
+namespace mozilla::glean {
+
+namespace impl {
+
+class MemoryDistributionMetric {
+ public:
+ constexpr explicit MemoryDistributionMetric(uint32_t aId) : mId(aId) {}
+
+ /*
+ * Accumulates the provided sample in the metric.
+ *
+ * @param aSample The sample to be recorded by the metric. The sample is
+ * assumed to be in the confgured memory unit of the metric.
+ *
+ * Notes: Values bigger than 1 Terabyte (2^40 bytes) are truncated and an
+ * InvalidValue error is recorded.
+ */
+ void Accumulate(size_t aSample) const;
+
+ /**
+ * **Test-only API**
+ *
+ * Gets the currently stored value as a DistributionData.
+ *
+ * This function will attempt to await the last parent-process task (if any)
+ * writing to the the metric's storage engine before returning a value.
+ * This function will not wait for data from child processes.
+ *
+ * This doesn't clear the stored value.
+ * Parent process only. Panics in child processes.
+ *
+ * @param aPingName The (optional) name of the ping to retrieve the metric
+ * for. Defaults to the first value in `send_in_pings`.
+ *
+ * @return value of the stored metric, or Nothing() if there is no value.
+ */
+ Result<Maybe<DistributionData>, nsCString> TestGetValue(
+ const nsACString& aPingName = nsCString()) const;
+
+ private:
+ const uint32_t mId;
+};
+} // namespace impl
+
+class GleanMemoryDistribution final : public GleanMetric {
+ public:
+ explicit GleanMemoryDistribution(uint64_t aId, nsISupports* aParent)
+ : GleanMetric(aParent), mMemoryDist(aId) {}
+
+ virtual JSObject* WrapObject(
+ JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override final;
+
+ void Accumulate(uint64_t aSample);
+
+ void TestGetValue(const nsACString& aPingName,
+ dom::Nullable<dom::GleanDistributionData>& aRetval,
+ ErrorResult& aRv);
+
+ private:
+ virtual ~GleanMemoryDistribution() = default;
+
+ const impl::MemoryDistributionMetric mMemoryDist;
+};
+
+} // namespace mozilla::glean
+
+#endif /* mozilla_glean_GleanMemoryDistribution_h */
diff --git a/toolkit/components/glean/bindings/private/Numerator.cpp b/toolkit/components/glean/bindings/private/Numerator.cpp
new file mode 100644
index 0000000000..87f931e53d
--- /dev/null
+++ b/toolkit/components/glean/bindings/private/Numerator.cpp
@@ -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/. */
+
+#include "mozilla/glean/bindings/Numerator.h"
+
+#include "nsString.h"
+#include "mozilla/ResultVariant.h"
+#include "mozilla/dom/GleanMetricsBinding.h"
+#include "mozilla/glean/bindings/ScalarGIFFTMap.h"
+#include "mozilla/glean/fog_ffi_generated.h"
+#include "jsapi.h"
+
+namespace mozilla::glean {
+
+namespace impl {
+
+void NumeratorMetric::AddToNumerator(int32_t aAmount) const {
+ auto scalarId = ScalarIdForMetric(mId);
+ if (scalarId && aAmount >= 0) {
+ Telemetry::ScalarAdd(scalarId.extract(), u"numerator"_ns, aAmount);
+ }
+ fog_numerator_add_to_numerator(mId, aAmount);
+}
+
+Result<Maybe<std::pair<int32_t, int32_t>>, nsCString>
+NumeratorMetric::TestGetValue(const nsACString& aPingName) const {
+ nsCString err;
+ if (fog_numerator_test_get_error(mId, &err)) {
+ return Err(err);
+ }
+ if (!fog_numerator_test_has_value(mId, &aPingName)) {
+ return Maybe<std::pair<int32_t, int32_t>>();
+ }
+ int32_t num = 0;
+ int32_t den = 0;
+ fog_numerator_test_get_value(mId, &aPingName, &num, &den);
+ return Some(std::make_pair(num, den));
+}
+
+} // namespace impl
+
+/* virtual */
+JSObject* GleanNumerator::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return dom::GleanNumerator_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+void GleanNumerator::AddToNumerator(int32_t aAmount) {
+ mNumerator.AddToNumerator(aAmount);
+}
+
+void GleanNumerator::TestGetValue(const nsACString& aPingName,
+ dom::Nullable<dom::GleanRateData>& aResult,
+ ErrorResult& aRv) {
+ auto result = mNumerator.TestGetValue(aPingName);
+ if (result.isErr()) {
+ aRv.ThrowDataError(result.unwrapErr());
+ return;
+ }
+ auto optresult = result.unwrap();
+ if (!optresult.isNothing()) {
+ dom::GleanRateData ret;
+ auto pair = optresult.extract();
+ ret.mNumerator = pair.first;
+ ret.mDenominator = pair.second;
+ aResult.SetValue(std::move(ret));
+ }
+}
+
+} // namespace mozilla::glean
diff --git a/toolkit/components/glean/bindings/private/Numerator.h b/toolkit/components/glean/bindings/private/Numerator.h
new file mode 100644
index 0000000000..a2ea02936a
--- /dev/null
+++ b/toolkit/components/glean/bindings/private/Numerator.h
@@ -0,0 +1,84 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_glean_GleanNumerator_h
+#define mozilla_glean_GleanNumerator_h
+
+#include "mozilla/dom/BindingDeclarations.h"
+#include "mozilla/glean/bindings/GleanMetric.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/Result.h"
+#include "nsString.h"
+
+namespace mozilla::dom {
+struct GleanRateData;
+} // namespace mozilla::dom
+
+namespace mozilla::glean {
+
+namespace impl {
+
+// Actually a RateMetric, but one whose denominator is a CounterMetric external
+// to the RateMetric.
+class NumeratorMetric {
+ public:
+ constexpr explicit NumeratorMetric(uint32_t aId) : mId(aId) {}
+
+ /*
+ * Increases the numerator by `amount`.
+ *
+ * @param aAmount The amount to increase by. Should be positive.
+ */
+ void AddToNumerator(int32_t aAmount = 1) const;
+
+ /**
+ * **Test-only API**
+ *
+ * Gets the currently stored value as a pair of integers.
+ *
+ * This function will attempt to await the last parent-process task (if any)
+ * writing to the the metric's storage engine before returning a value.
+ * This function will not wait for data from child processes.
+ *
+ * This doesn't clear the stored value.
+ * Parent process only. Panics in child processes.
+ *
+ * @param aPingName The (optional) name of the ping to retrieve the metric
+ * for. Defaults to the first value in `send_in_pings`.
+ *
+ * @return value of the stored metric, or Nothing() if there is no value.
+ */
+ Result<Maybe<std::pair<int32_t, int32_t>>, nsCString> TestGetValue(
+ const nsACString& aPingName = nsCString()) const;
+
+ private:
+ const uint32_t mId;
+};
+} // namespace impl
+
+class GleanNumerator final : public GleanMetric {
+ public:
+ explicit GleanNumerator(uint32_t id, nsISupports* aParent)
+ : GleanMetric(aParent), mNumerator(id) {}
+
+ virtual JSObject* WrapObject(
+ JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override final;
+
+ void AddToNumerator(int32_t aAmount);
+
+ void TestGetValue(const nsACString& aPingName,
+ dom::Nullable<dom::GleanRateData>& aResult,
+ ErrorResult& aRv);
+
+ private:
+ virtual ~GleanNumerator() = default;
+
+ const impl::NumeratorMetric mNumerator;
+};
+
+} // namespace mozilla::glean
+
+#endif /* mozilla_glean_GleanNumerator_h */
diff --git a/toolkit/components/glean/bindings/private/Ping.cpp b/toolkit/components/glean/bindings/private/Ping.cpp
new file mode 100644
index 0000000000..19f4fb5f77
--- /dev/null
+++ b/toolkit/components/glean/bindings/private/Ping.cpp
@@ -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/. */
+
+#include "mozilla/glean/bindings/Ping.h"
+
+#include "mozilla/AppShutdown.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/Components.h"
+#include "nsIClassInfoImpl.h"
+#include "nsTHashMap.h"
+#include "nsString.h"
+
+namespace mozilla::glean {
+
+namespace impl {
+
+using CallbackMapType = nsTHashMap<uint32_t, PingTestCallback>;
+using MetricIdToCallbackMutex = StaticDataMutex<UniquePtr<CallbackMapType>>;
+static Maybe<MetricIdToCallbackMutex::AutoLock> GetCallbackMapLock() {
+ static MetricIdToCallbackMutex sCallbacks("sCallbacks");
+ auto lock = sCallbacks.Lock();
+ // Test callbacks will continue to work until the end of AppShutdownTelemetry
+ if (AppShutdown::IsInOrBeyond(ShutdownPhase::XPCOMWillShutdown)) {
+ return Nothing();
+ }
+ if (!*lock) {
+ *lock = MakeUnique<CallbackMapType>();
+ RunOnShutdown(
+ [&] {
+ auto lock = sCallbacks.Lock();
+ *lock = nullptr; // deletes, see UniquePtr.h
+ },
+ ShutdownPhase::XPCOMWillShutdown);
+ }
+ return Some(std::move(lock));
+}
+
+void Ping::Submit(const nsACString& aReason) const {
+ {
+ auto callback = Maybe<PingTestCallback>();
+ GetCallbackMapLock().apply(
+ [&](auto& lock) { callback = lock.ref()->Extract(mId); });
+ // Calling the callback outside of the lock allows it to register a new
+ // callback itself.
+ if (callback) {
+ callback.extract()(aReason);
+ }
+ }
+ fog_submit_ping_by_id(mId, &aReason);
+}
+
+void Ping::TestBeforeNextSubmit(PingTestCallback&& aCallback) const {
+ {
+ GetCallbackMapLock().apply(
+ [&](auto& lock) { lock.ref()->InsertOrUpdate(mId, aCallback); });
+ }
+}
+
+} // namespace impl
+
+NS_IMPL_CLASSINFO(GleanPing, nullptr, 0, {0})
+NS_IMPL_ISUPPORTS_CI(GleanPing, nsIGleanPing)
+
+NS_IMETHODIMP
+GleanPing::Submit(const nsACString& aReason) {
+ mPing.Submit(aReason);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+GleanPing::TestBeforeNextSubmit(nsIGleanPingTestCallback* aCallback) {
+ if (NS_WARN_IF(!aCallback)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ // Throw the bare ptr into a COM ptr to keep it around in the lambda.
+ nsCOMPtr<nsIGleanPingTestCallback> callback = aCallback;
+ mPing.TestBeforeNextSubmit(
+ [callback](const nsACString& aReason) { callback->Call(aReason); });
+ return NS_OK;
+}
+
+} // namespace mozilla::glean
diff --git a/toolkit/components/glean/bindings/private/Ping.h b/toolkit/components/glean/bindings/private/Ping.h
new file mode 100644
index 0000000000..2a92d87995
--- /dev/null
+++ b/toolkit/components/glean/bindings/private/Ping.h
@@ -0,0 +1,83 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_glean_Ping_h
+#define mozilla_glean_Ping_h
+
+#include "mozilla/DataMutex.h"
+#include "mozilla/glean/fog_ffi_generated.h"
+#include "mozilla/Maybe.h"
+#include "nsIGleanPing.h"
+#include "nsString.h"
+
+namespace mozilla::glean {
+
+typedef std::function<void(const nsACString& aReason)> PingTestCallback;
+
+namespace impl {
+
+class Ping {
+ public:
+ constexpr explicit Ping(uint32_t aId) : mId(aId) {}
+
+ /**
+ * Collect and submit the ping for eventual upload.
+ *
+ * This will collect all stored data to be included in the ping.
+ * Data with lifetime `ping` will then be reset.
+ *
+ * If the ping is configured with `send_if_empty = false`
+ * and the ping currently contains no content,
+ * it will not be queued for upload.
+ * If the ping is configured with `send_if_empty = true`
+ * it will be queued for upload even if empty.
+ *
+ * Pings always contain the `ping_info` and `client_info` sections.
+ * See [ping
+ * sections](https://mozilla.github.io/glean/book/user/pings/index.html#ping-sections)
+ * for details.
+ *
+ * @param aReason - Optional. The reason the ping is being submitted.
+ * Must match one of the configured `reason_codes`.
+ */
+ void Submit(const nsACString& aReason = nsCString()) const;
+
+ /**
+ * **Test-only API**
+ *
+ * Register a callback to be called right before this ping is next submitted.
+ * The provided function is called exactly once before submitting.
+ *
+ * Note: The callback will be called on any call to submit.
+ * A ping may not be sent afterwards, e.g. if the ping is empty and
+ * `send_if_empty` is `false`
+ *
+ * @param aCallback - The callback to call on the next submit.
+ */
+ void TestBeforeNextSubmit(PingTestCallback&& aCallback) const;
+
+ private:
+ const uint32_t mId;
+};
+
+} // namespace impl
+
+class GleanPing final : public nsIGleanPing {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIGLEANPING
+
+ explicit GleanPing(uint32_t aId) : mPing(aId) {}
+
+ private:
+ virtual ~GleanPing() = default;
+
+ const impl::Ping mPing;
+};
+
+} // namespace mozilla::glean
+
+#endif /* mozilla_glean_Ping_h */
diff --git a/toolkit/components/glean/bindings/private/Quantity.cpp b/toolkit/components/glean/bindings/private/Quantity.cpp
new file mode 100644
index 0000000000..9b7034e013
--- /dev/null
+++ b/toolkit/components/glean/bindings/private/Quantity.cpp
@@ -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/. */
+
+#include "mozilla/glean/bindings/Quantity.h"
+
+#include "mozilla/ResultVariant.h"
+#include "mozilla/dom/GleanMetricsBinding.h"
+#include "mozilla/glean/bindings/ScalarGIFFTMap.h"
+#include "mozilla/glean/fog_ffi_generated.h"
+#include "nsString.h"
+
+namespace mozilla::glean {
+
+namespace impl {
+
+void QuantityMetric::Set(int64_t aValue) const {
+ auto scalarId = ScalarIdForMetric(mId);
+ if (scalarId && aValue >= 0) {
+ uint32_t theValue = static_cast<uint32_t>(aValue);
+ if (aValue > std::numeric_limits<uint32_t>::max()) {
+ theValue = std::numeric_limits<uint32_t>::max();
+ }
+ Telemetry::ScalarSet(scalarId.extract(), theValue);
+ }
+ fog_quantity_set(mId, aValue);
+}
+
+Result<Maybe<int64_t>, nsCString> QuantityMetric::TestGetValue(
+ const nsACString& aPingName) const {
+ nsCString err;
+ if (fog_quantity_test_get_error(mId, &err)) {
+ return Err(err);
+ }
+ if (!fog_quantity_test_has_value(mId, &aPingName)) {
+ return Maybe<int64_t>();
+ }
+ return Some(fog_quantity_test_get_value(mId, &aPingName));
+}
+
+} // namespace impl
+
+/* virtual */
+JSObject* GleanQuantity::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return dom::GleanQuantity_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+void GleanQuantity::Set(int64_t aValue) { mQuantity.Set(aValue); }
+
+dom::Nullable<int64_t> GleanQuantity::TestGetValue(const nsACString& aPingName,
+ ErrorResult& aRv) {
+ dom::Nullable<int64_t> ret;
+ auto result = mQuantity.TestGetValue(aPingName);
+ if (result.isErr()) {
+ aRv.ThrowDataError(result.unwrapErr());
+ return ret;
+ }
+ auto optresult = result.unwrap();
+ if (!optresult.isNothing()) {
+ ret.SetValue(optresult.value());
+ }
+ return ret;
+}
+
+} // namespace mozilla::glean
diff --git a/toolkit/components/glean/bindings/private/Quantity.h b/toolkit/components/glean/bindings/private/Quantity.h
new file mode 100644
index 0000000000..bf7485a33c
--- /dev/null
+++ b/toolkit/components/glean/bindings/private/Quantity.h
@@ -0,0 +1,77 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_glean_GleanQuantity_h
+#define mozilla_glean_GleanQuantity_h
+
+#include "mozilla/dom/BindingDeclarations.h"
+#include "mozilla/glean/bindings/GleanMetric.h"
+#include "nsTString.h"
+#include "nsIScriptError.h"
+
+namespace mozilla::glean {
+
+namespace impl {
+
+class QuantityMetric {
+ public:
+ constexpr explicit QuantityMetric(uint32_t id) : mId(id) {}
+
+ /**
+ * Set to the specified value.
+ *
+ * @param aValue the value to set.
+ */
+ void Set(int64_t aValue) const;
+
+ /**
+ * **Test-only API**
+ *
+ * Gets the currently stored value.
+ *
+ * This function will attempt to await the last parent-process task (if any)
+ * writing to the the metric's storage engine before returning a value.
+ * This function will not wait for data from child processes.
+ *
+ * This doesn't clear the stored value.
+ * Parent process only. Panics in child processes.
+ *
+ * @param aPingName The (optional) name of the ping to retrieve the metric
+ * for. Defaults to the first value in `send_in_pings`.
+ *
+ * @return value of the stored metric.
+ */
+ Result<Maybe<int64_t>, nsCString> TestGetValue(
+ const nsACString& aPingName = nsCString()) const;
+
+ private:
+ const uint32_t mId;
+};
+
+} // namespace impl
+
+class GleanQuantity final : public GleanMetric {
+ public:
+ explicit GleanQuantity(uint32_t id, nsISupports* aParent)
+ : GleanMetric(aParent), mQuantity(id) {}
+
+ virtual JSObject* WrapObject(
+ JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override final;
+
+ void Set(int64_t aValue);
+
+ dom::Nullable<int64_t> TestGetValue(const nsACString& aPingName,
+ ErrorResult& aRv);
+
+ private:
+ virtual ~GleanQuantity() = default;
+
+ const impl::QuantityMetric mQuantity;
+};
+
+} // namespace mozilla::glean
+
+#endif /* mozilla_glean_GleanQuantity.h */
diff --git a/toolkit/components/glean/bindings/private/Rate.cpp b/toolkit/components/glean/bindings/private/Rate.cpp
new file mode 100644
index 0000000000..d317b5ad7d
--- /dev/null
+++ b/toolkit/components/glean/bindings/private/Rate.cpp
@@ -0,0 +1,86 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/glean/bindings/Rate.h"
+
+#include "jsapi.h"
+#include "nsString.h"
+#include "mozilla/ErrorResult.h"
+#include "mozilla/ResultVariant.h"
+#include "mozilla/dom/GleanMetricsBinding.h"
+#include "mozilla/glean/bindings/ScalarGIFFTMap.h"
+#include "mozilla/glean/fog_ffi_generated.h"
+
+namespace mozilla::glean {
+
+namespace impl {
+
+void RateMetric::AddToNumerator(int32_t aAmount) const {
+ auto scalarId = ScalarIdForMetric(mId);
+ if (scalarId && aAmount >= 0) {
+ Telemetry::ScalarAdd(scalarId.extract(), u"numerator"_ns, aAmount);
+ }
+ fog_rate_add_to_numerator(mId, aAmount);
+}
+
+void RateMetric::AddToDenominator(int32_t aAmount) const {
+ auto scalarId = ScalarIdForMetric(mId);
+ if (scalarId && aAmount >= 0) {
+ Telemetry::ScalarAdd(scalarId.extract(), u"denominator"_ns, aAmount);
+ }
+ fog_rate_add_to_denominator(mId, aAmount);
+}
+
+Result<Maybe<std::pair<int32_t, int32_t>>, nsCString> RateMetric::TestGetValue(
+ const nsACString& aPingName) const {
+ nsCString err;
+ if (fog_rate_test_get_error(mId, &err)) {
+ return Err(err);
+ }
+ if (!fog_rate_test_has_value(mId, &aPingName)) {
+ return Maybe<std::pair<int32_t, int32_t>>();
+ }
+ int32_t num = 0;
+ int32_t den = 0;
+ fog_rate_test_get_value(mId, &aPingName, &num, &den);
+ return Some(std::make_pair(num, den));
+}
+
+} // namespace impl
+
+/* virtual */
+JSObject* GleanRate::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return dom::GleanRate_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+void GleanRate::AddToNumerator(int32_t aAmount) {
+ mRate.AddToNumerator(aAmount);
+}
+
+void GleanRate::AddToDenominator(int32_t aAmount) {
+ mRate.AddToDenominator(aAmount);
+}
+
+void GleanRate::TestGetValue(const nsACString& aPingName,
+ dom::Nullable<dom::GleanRateData>& aResult,
+ ErrorResult& aRv) {
+ auto result = mRate.TestGetValue(aPingName);
+ if (result.isErr()) {
+ aRv.ThrowDataError(result.unwrapErr());
+ return;
+ }
+ auto optresult = result.unwrap();
+ if (!optresult.isNothing()) {
+ dom::GleanRateData ret;
+ auto pair = optresult.extract();
+ ret.mNumerator = pair.first;
+ ret.mDenominator = pair.second;
+ aResult.SetValue(std::move(ret));
+ }
+}
+
+} // namespace mozilla::glean
diff --git a/toolkit/components/glean/bindings/private/Rate.h b/toolkit/components/glean/bindings/private/Rate.h
new file mode 100644
index 0000000000..227372bdd2
--- /dev/null
+++ b/toolkit/components/glean/bindings/private/Rate.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_GleanRate_h
+#define mozilla_glean_GleanRate_h
+
+#include "mozilla/dom/BindingDeclarations.h"
+#include "mozilla/glean/bindings/GleanMetric.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/Result.h"
+#include "nsString.h"
+
+namespace mozilla::dom {
+struct GleanRateData;
+} // namespace mozilla::dom
+
+namespace mozilla::glean {
+
+namespace impl {
+
+class RateMetric {
+ public:
+ constexpr explicit RateMetric(uint32_t aId) : mId(aId) {}
+
+ /*
+ * Increases the numerator by `amount`.
+ *
+ * @param aAmount The amount to increase by. Should be positive.
+ */
+ void AddToNumerator(int32_t aAmount = 1) const;
+
+ /*
+ * Increases the denominator by `amount`.
+ *
+ * @param aAmount The amount to increase by. Should be positive.
+ */
+ void AddToDenominator(int32_t aAmount = 1) const;
+
+ /**
+ * **Test-only API**
+ *
+ * Gets the currently stored value as a pair of integers.
+ *
+ * This function will attempt to await the last parent-process task (if any)
+ * writing to the the metric's storage engine before returning a value.
+ * This function will not wait for data from child processes.
+ *
+ * This doesn't clear the stored value.
+ * Parent process only. Panics in child processes.
+ *
+ * @param aPingName The (optional) name of the ping to retrieve the metric
+ * for. Defaults to the first value in `send_in_pings`.
+ *
+ * @return value of the stored metric, or Nothing() if there is no value.
+ */
+ Result<Maybe<std::pair<int32_t, int32_t>>, nsCString> TestGetValue(
+ const nsACString& aPingName = nsCString()) const;
+
+ private:
+ const uint32_t mId;
+};
+} // namespace impl
+
+class GleanRate final : public GleanMetric {
+ public:
+ explicit GleanRate(uint32_t id, nsISupports* aParent)
+ : GleanMetric(aParent), mRate(id) {}
+
+ virtual JSObject* WrapObject(
+ JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override final;
+
+ void AddToNumerator(int32_t aAmount);
+ void AddToDenominator(int32_t aAmount);
+
+ void TestGetValue(const nsACString& aPingName,
+ dom::Nullable<dom::GleanRateData>& aResult,
+ ErrorResult& aRv);
+
+ private:
+ virtual ~GleanRate() = default;
+
+ const impl::RateMetric mRate;
+};
+
+} // namespace mozilla::glean
+
+#endif /* mozilla_glean_GleanRate_h */
diff --git a/toolkit/components/glean/bindings/private/String.cpp b/toolkit/components/glean/bindings/private/String.cpp
new file mode 100644
index 0000000000..f012884c8c
--- /dev/null
+++ b/toolkit/components/glean/bindings/private/String.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/glean/bindings/String.h"
+
+#include "jsapi.h"
+#include "js/String.h"
+#include "nsString.h"
+#include "mozilla/ErrorResult.h"
+#include "mozilla/ResultVariant.h"
+#include "mozilla/dom/GleanMetricsBinding.h"
+#include "mozilla/glean/bindings/ScalarGIFFTMap.h"
+#include "mozilla/glean/fog_ffi_generated.h"
+
+namespace mozilla::glean {
+
+namespace impl {
+
+void StringMetric::Set(const nsACString& aValue) const {
+ auto scalarId = ScalarIdForMetric(mId);
+ if (scalarId) {
+ Telemetry::ScalarSet(scalarId.extract(), NS_ConvertUTF8toUTF16(aValue));
+ }
+ fog_string_set(mId, &aValue);
+}
+
+Result<Maybe<nsCString>, nsCString> StringMetric::TestGetValue(
+ const nsACString& aPingName) const {
+ nsCString err;
+ if (fog_string_test_get_error(mId, &err)) {
+ return Err(err);
+ }
+ if (!fog_string_test_has_value(mId, &aPingName)) {
+ return Maybe<nsCString>();
+ }
+ nsCString ret;
+ fog_string_test_get_value(mId, &aPingName, &ret);
+ return Some(ret);
+}
+
+} // namespace impl
+
+/* virtual */
+JSObject* GleanString::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return dom::GleanString_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+void GleanString::Set(const nsACString& aValue) {
+ if (aValue.IsVoid()) {
+ // TODO: Instrument this error (bug 1691073)
+ return;
+ }
+ mString.Set(aValue);
+}
+
+void GleanString::TestGetValue(const nsACString& aPingName, nsCString& aResult,
+ ErrorResult& aRv) {
+ auto result = mString.TestGetValue(aPingName);
+ if (result.isErr()) {
+ aRv.ThrowDataError(result.unwrapErr());
+ return;
+ }
+ auto optresult = result.unwrap();
+ if (!optresult.isNothing()) {
+ aResult.Assign(optresult.extract());
+ } else {
+ aResult.SetIsVoid(true);
+ }
+}
+
+} // 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..b4aadb63f9
--- /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/dom/BindingDeclarations.h"
+#include "mozilla/glean/bindings/GleanMetric.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/Result.h"
+#include "nsString.h"
+
+namespace mozilla::glean {
+
+namespace impl {
+
+class StringMetric {
+ public:
+ constexpr explicit StringMetric(uint32_t aId) : mId(aId) {}
+
+ /*
+ * Set to the specified value.
+ *
+ * Truncates the value if it is longer than 100 bytes and logs an error.
+ * See https://mozilla.github.io/glean/book/user/metrics/string.html#limits.
+ *
+ * @param aValue The string to set the metric to.
+ */
+ void Set(const nsACString& aValue) const;
+
+ /**
+ * **Test-only API**
+ *
+ * Gets the currently stored value as a string.
+ *
+ * This function will attempt to await the last parent-process task (if any)
+ * writing to the the metric's storage engine before returning a value.
+ * This function will not wait for data from child processes.
+ *
+ * This doesn't clear the stored value.
+ * Parent process only. Panics in child processes.
+ *
+ * @param aPingName The (optional) name of the ping to retrieve the metric
+ * for. Defaults to the first value in `send_in_pings`.
+ *
+ * @return value of the stored metric, or Nothing() if there is no value.
+ */
+ Result<Maybe<nsCString>, nsCString> TestGetValue(
+ const nsACString& aPingName = nsCString()) const;
+
+ private:
+ const uint32_t mId;
+};
+} // namespace impl
+
+class GleanString final : public GleanMetric {
+ public:
+ explicit GleanString(uint32_t aId, nsISupports* aParent)
+ : GleanMetric(aParent), mString(aId) {}
+
+ virtual JSObject* WrapObject(
+ JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override final;
+
+ void Set(const nsACString& aValue);
+
+ void TestGetValue(const nsACString& aPingName, nsCString& aResult,
+ ErrorResult& aRv);
+
+ virtual ~GleanString() = default;
+
+ private:
+ 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..b2875807a1
--- /dev/null
+++ b/toolkit/components/glean/bindings/private/StringList.cpp
@@ -0,0 +1,80 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/glean/bindings/StringList.h"
+
+#include "mozilla/ResultVariant.h"
+#include "mozilla/dom/ToJSValue.h"
+#include "mozilla/dom/GleanMetricsBinding.h"
+#include "mozilla/glean/bindings/ScalarGIFFTMap.h"
+#include "mozilla/glean/fog_ffi_generated.h"
+#include "nsString.h"
+#include "nsTArray.h"
+
+namespace mozilla::glean {
+
+namespace impl {
+
+void StringListMetric::Add(const nsACString& aValue) const {
+ auto scalarId = ScalarIdForMetric(mId);
+ if (scalarId) {
+ Telemetry::ScalarSet(scalarId.extract(), NS_ConvertUTF8toUTF16(aValue),
+ true);
+ }
+ fog_string_list_add(mId, &aValue);
+}
+
+void StringListMetric::Set(const nsTArray<nsCString>& aValue) const {
+ // Calling `Set` on a mirrored labeled_string is likely an error.
+ // We can't remove keys from the mirror scalar and handle this 'properly',
+ // so you shouldn't use this operation at all.
+ (void)NS_WARN_IF(ScalarIdForMetric(mId).isSome());
+ fog_string_list_set(mId, &aValue);
+}
+
+Result<Maybe<nsTArray<nsCString>>, nsCString> StringListMetric::TestGetValue(
+ const nsACString& aPingName) const {
+ nsCString err;
+ if (fog_string_list_test_get_error(mId, &err)) {
+ return Err(err);
+ }
+ if (!fog_string_list_test_has_value(mId, &aPingName)) {
+ return Maybe<nsTArray<nsCString>>();
+ }
+ nsTArray<nsCString> ret;
+ fog_string_list_test_get_value(mId, &aPingName, &ret);
+ return Some(std::move(ret));
+}
+
+} // namespace impl
+
+/* virtual */
+JSObject* GleanStringList::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return dom::GleanStringList_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+void GleanStringList::Add(const nsACString& aValue) { mStringList.Add(aValue); }
+
+void GleanStringList::Set(const dom::Sequence<nsCString>& aValue) {
+ mStringList.Set(aValue);
+}
+
+void GleanStringList::TestGetValue(const nsACString& aPingName,
+ dom::Nullable<nsTArray<nsCString>>& aResult,
+ ErrorResult& aRv) {
+ auto result = mStringList.TestGetValue(aPingName);
+ if (result.isErr()) {
+ aRv.ThrowDataError(result.unwrapErr());
+ return;
+ }
+ auto optresult = result.unwrap();
+ if (!optresult.isNothing()) {
+ aResult.SetValue(optresult.extract());
+ }
+}
+
+} // 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..94cb830cf6
--- /dev/null
+++ b/toolkit/components/glean/bindings/private/StringList.h
@@ -0,0 +1,93 @@
+/* -*- 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 "mozilla/dom/BindingDeclarations.h"
+#include "mozilla/glean/bindings/GleanMetric.h"
+#include "nsString.h"
+#include "nsTArray.h"
+
+namespace mozilla::glean {
+
+namespace impl {
+
+class StringListMetric {
+ public:
+ constexpr explicit StringListMetric(uint32_t aId) : mId(aId) {}
+
+ /*
+ * Adds a new string to the list.
+ *
+ * Truncates the value if it is longer than 50 bytes and logs an error.
+ *
+ * @param aValue The string to add.
+ */
+ void Add(const nsACString& aValue) const;
+
+ /*
+ * Set to a specific list of strings.
+ *
+ * Truncates any values longer than 50 bytes and logs an error.
+ * Truncates the list if it is over 20 items long.
+ * See
+ * https://mozilla.github.io/glean/book/user/metrics/string_list.html#limits.
+ *
+ * @param aValue The list of strings to set the metric to.
+ */
+ void Set(const nsTArray<nsCString>& aValue) const;
+
+ /**
+ * **Test-only API**
+ *
+ * Gets the currently stored value.
+ *
+ * This function will attempt to await the last parent-process task (if any)
+ * writing to the the metric's storage engine before returning a value.
+ * This function will not wait for data from child processes.
+ *
+ * This doesn't clear the stored value.
+ * Parent process only. Panics in child processes.
+ *
+ * @param aPingName The (optional) name of the ping to retrieve the metric
+ * for. Defaults to the first value in `send_in_pings`.
+ *
+ * @return value of the stored metric, or Nothing() if there is no value.
+ */
+ Result<Maybe<nsTArray<nsCString>>, nsCString> TestGetValue(
+ const nsACString& aPingName = nsCString()) const;
+
+ private:
+ const uint32_t mId;
+};
+} // namespace impl
+
+class GleanStringList final : public GleanMetric {
+ public:
+ explicit GleanStringList(uint32_t aId, nsISupports* aParent)
+ : GleanMetric(aParent), mStringList(aId) {}
+
+ virtual JSObject* WrapObject(
+ JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override final;
+
+ void Add(const nsACString& aValue);
+ void Set(const dom::Sequence<nsCString>& aValue);
+
+ void TestGetValue(const nsACString& aPingName,
+ dom::Nullable<nsTArray<nsCString>>& aResult,
+ ErrorResult& aRv);
+
+ private:
+ virtual ~GleanStringList() = default;
+
+ const impl::StringListMetric mStringList;
+};
+
+} // namespace mozilla::glean
+
+#endif /* mozilla_glean_GleanStringList_h */
diff --git a/toolkit/components/glean/bindings/private/Text.cpp b/toolkit/components/glean/bindings/private/Text.cpp
new file mode 100644
index 0000000000..fd36f66136
--- /dev/null
+++ b/toolkit/components/glean/bindings/private/Text.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/Text.h"
+
+#include "jsapi.h"
+#include "js/String.h"
+#include "nsString.h"
+#include "mozilla/ResultVariant.h"
+#include "mozilla/dom/GleanMetricsBinding.h"
+#include "mozilla/glean/bindings/ScalarGIFFTMap.h"
+#include "mozilla/glean/fog_ffi_generated.h"
+
+namespace mozilla::glean {
+
+namespace impl {
+
+void TextMetric::Set(const nsACString& aValue) const {
+ fog_text_set(mId, &aValue);
+}
+
+Result<Maybe<nsCString>, nsCString> TextMetric::TestGetValue(
+ const nsACString& aPingName) const {
+ nsCString err;
+ if (fog_text_test_get_error(mId, &err)) {
+ return Err(err);
+ }
+ if (!fog_text_test_has_value(mId, &aPingName)) {
+ return Maybe<nsCString>();
+ }
+ nsCString ret;
+ fog_text_test_get_value(mId, &aPingName, &ret);
+ return Some(ret);
+}
+
+} // namespace impl
+
+/* virtual */
+JSObject* GleanText::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return dom::GleanText_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+void GleanText::Set(const nsACString& aValue) { mText.Set(aValue); }
+
+void GleanText::TestGetValue(const nsACString& aPingName, nsCString& aResult,
+ ErrorResult& aRv) {
+ auto result = mText.TestGetValue(aPingName);
+ if (result.isErr()) {
+ aRv.ThrowDataError(result.unwrapErr());
+ return;
+ }
+ auto optresult = result.unwrap();
+ if (!optresult.isNothing()) {
+ aResult.Assign(optresult.extract());
+ } else {
+ aResult.SetIsVoid(true);
+ }
+}
+
+} // namespace mozilla::glean
diff --git a/toolkit/components/glean/bindings/private/Text.h b/toolkit/components/glean/bindings/private/Text.h
new file mode 100644
index 0000000000..545b1e68a0
--- /dev/null
+++ b/toolkit/components/glean/bindings/private/Text.h
@@ -0,0 +1,54 @@
+/* -*- 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_GleanText_h
+#define mozilla_glean_GleanText_h
+
+#include "mozilla/dom/BindingDeclarations.h"
+#include "mozilla/glean/bindings/GleanMetric.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/Result.h"
+#include "nsString.h"
+
+namespace mozilla::glean {
+
+namespace impl {
+
+class TextMetric {
+ public:
+ constexpr explicit TextMetric(uint32_t aId) : mId(aId) {}
+
+ void Set(const nsACString& aValue) const;
+
+ Result<Maybe<nsCString>, nsCString> TestGetValue(
+ const nsACString& aPingName = nsCString()) const;
+
+ private:
+ const uint32_t mId;
+};
+
+} // namespace impl
+
+class GleanText final : public GleanMetric {
+ public:
+ explicit GleanText(uint32_t aId, nsISupports* aParent)
+ : GleanMetric(aParent), mText(aId) {}
+
+ virtual JSObject* WrapObject(
+ JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override final;
+
+ void Set(const nsACString& aValue);
+
+ void TestGetValue(const nsACString& aPingName, nsCString& aResult,
+ ErrorResult& aRv);
+
+ private:
+ virtual ~GleanText() = default;
+
+ const impl::TextMetric mText;
+};
+} // namespace mozilla::glean
+#endif
diff --git a/toolkit/components/glean/bindings/private/Timespan.cpp b/toolkit/components/glean/bindings/private/Timespan.cpp
new file mode 100644
index 0000000000..13e57317fa
--- /dev/null
+++ b/toolkit/components/glean/bindings/private/Timespan.cpp
@@ -0,0 +1,186 @@
+/* -*- 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 "mozilla/ResultVariant.h"
+#include "mozilla/dom/GleanMetricsBinding.h"
+#include "mozilla/glean/bindings/ScalarGIFFTMap.h"
+#include "mozilla/glean/fog_ffi_generated.h"
+
+namespace mozilla::glean {
+
+namespace impl {
+
+namespace {
+class ScalarIDHashKey : public PLDHashEntryHdr {
+ public:
+ using KeyType = const ScalarID&;
+ using KeyTypePointer = const ScalarID*;
+
+ explicit ScalarIDHashKey(KeyTypePointer aKey) : mValue(*aKey) {}
+ ScalarIDHashKey(ScalarIDHashKey&& aOther)
+ : PLDHashEntryHdr(std::move(aOther)), mValue(std::move(aOther.mValue)) {}
+ ~ScalarIDHashKey() = default;
+
+ KeyType GetKey() const { return mValue; }
+ bool KeyEquals(KeyTypePointer aKey) const { return *aKey == mValue; }
+
+ static KeyTypePointer KeyToPointer(KeyType aKey) { return &aKey; }
+ static PLDHashNumber HashKey(KeyTypePointer aKey) {
+ return static_cast<std::underlying_type<ScalarID>::type>(*aKey);
+ }
+ enum { ALLOW_MEMMOVE = true };
+
+ private:
+ const ScalarID mValue;
+};
+} // namespace
+
+using TimesToStartsMutex =
+ StaticDataMutex<UniquePtr<nsTHashMap<ScalarIDHashKey, TimeStamp>>>;
+static Maybe<TimesToStartsMutex::AutoLock> GetTimesToStartsLock() {
+ static TimesToStartsMutex sTimespanStarts("sTimespanStarts");
+ auto lock = sTimespanStarts.Lock();
+ // GIFFT will work up to the end of AppShutdownTelemetry.
+ if (AppShutdown::IsInOrBeyond(ShutdownPhase::XPCOMWillShutdown)) {
+ return Nothing();
+ }
+ if (!*lock) {
+ *lock = MakeUnique<nsTHashMap<ScalarIDHashKey, TimeStamp>>();
+ RefPtr<nsIRunnable> cleanupFn = NS_NewRunnableFunction(__func__, [&] {
+ if (AppShutdown::IsInOrBeyond(ShutdownPhase::XPCOMWillShutdown)) {
+ auto lock = sTimespanStarts.Lock();
+ *lock = nullptr; // deletes, see UniquePtr.h
+ return;
+ }
+ RunOnShutdown(
+ [&] {
+ auto lock = sTimespanStarts.Lock();
+ *lock = nullptr; // deletes, see UniquePtr.h
+ },
+ ShutdownPhase::XPCOMWillShutdown);
+ });
+ // Both getting the main thread and dispatching to it can fail.
+ // In that event we leak. Grab a pointer so we have something to NS_RELEASE
+ // in that case.
+ nsIRunnable* temp = cleanupFn.get();
+ nsCOMPtr<nsIThread> mainThread;
+ if (NS_FAILED(NS_GetMainThread(getter_AddRefs(mainThread))) ||
+ NS_FAILED(mainThread->Dispatch(cleanupFn.forget(),
+ nsIThread::DISPATCH_NORMAL))) {
+ // Failed to dispatch cleanup routine.
+ // First, un-leak the runnable (but only if we actually attempted
+ // dispatch)
+ if (!cleanupFn) {
+ NS_RELEASE(temp);
+ }
+ // Next, cleanup immediately, and allow metrics to try again later.
+ *lock = nullptr;
+ return Nothing();
+ }
+ }
+ return Some(std::move(lock));
+}
+
+void TimespanMetric::Start() const {
+ auto optScalarId = ScalarIdForMetric(mId);
+ if (optScalarId) {
+ auto scalarId = optScalarId.extract();
+ GetTimesToStartsLock().apply([&](auto& lock) {
+ (void)NS_WARN_IF(lock.ref()->Remove(scalarId));
+ lock.ref()->InsertOrUpdate(scalarId, TimeStamp::Now());
+ });
+ }
+ fog_timespan_start(mId);
+}
+
+void TimespanMetric::Stop() const {
+ auto optScalarId = ScalarIdForMetric(mId);
+ if (optScalarId) {
+ auto scalarId = optScalarId.extract();
+ GetTimesToStartsLock().apply([&](auto& lock) {
+ auto optStart = lock.ref()->Extract(scalarId);
+ if (!NS_WARN_IF(!optStart)) {
+ double delta = (TimeStamp::Now() - optStart.extract()).ToMilliseconds();
+ uint32_t theDelta = static_cast<uint32_t>(delta);
+ if (delta > std::numeric_limits<uint32_t>::max()) {
+ theDelta = std::numeric_limits<uint32_t>::max();
+ } else if (MOZ_UNLIKELY(delta < 0)) {
+ theDelta = 0;
+ }
+ Telemetry::ScalarSet(scalarId, theDelta);
+ }
+ });
+ }
+ fog_timespan_stop(mId);
+}
+
+void TimespanMetric::Cancel() const {
+ auto optScalarId = ScalarIdForMetric(mId);
+ if (optScalarId) {
+ auto scalarId = optScalarId.extract();
+ GetTimesToStartsLock().apply(
+ [&](auto& lock) { lock.ref()->Remove(scalarId); });
+ }
+ fog_timespan_cancel(mId);
+}
+
+void TimespanMetric::SetRaw(uint32_t aDuration) const {
+ auto optScalarId = ScalarIdForMetric(mId);
+ if (optScalarId) {
+ auto scalarId = optScalarId.extract();
+ Telemetry::ScalarSet(scalarId, aDuration);
+ }
+ fog_timespan_set_raw(mId, aDuration);
+}
+
+Result<Maybe<uint64_t>, nsCString> TimespanMetric::TestGetValue(
+ const nsACString& aPingName) const {
+ nsCString err;
+ if (fog_timespan_test_get_error(mId, &err)) {
+ return Err(err);
+ }
+ if (!fog_timespan_test_has_value(mId, &aPingName)) {
+ return Maybe<uint64_t>();
+ }
+ return Some(fog_timespan_test_get_value(mId, &aPingName));
+}
+
+} // namespace impl
+
+/* virtual */
+JSObject* GleanTimespan::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return dom::GleanTimespan_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+void GleanTimespan::Start() { mTimespan.Start(); }
+
+void GleanTimespan::Stop() { mTimespan.Stop(); }
+
+void GleanTimespan::Cancel() { mTimespan.Cancel(); }
+
+void GleanTimespan::SetRaw(uint32_t aDuration) { mTimespan.SetRaw(aDuration); }
+
+dom::Nullable<uint64_t> GleanTimespan::TestGetValue(const nsACString& aPingName,
+ ErrorResult& aRv) {
+ dom::Nullable<uint64_t> ret;
+ auto result = mTimespan.TestGetValue(aPingName);
+ if (result.isErr()) {
+ aRv.ThrowDataError(result.unwrapErr());
+ return ret;
+ }
+ auto optresult = result.unwrap();
+ if (!optresult.isNothing()) {
+ ret.SetValue(optresult.value());
+ }
+ return ret;
+}
+
+} // 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..296e1cb28f
--- /dev/null
+++ b/toolkit/components/glean/bindings/private/Timespan.h
@@ -0,0 +1,110 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_glean_GleanTimespan_h
+#define mozilla_glean_GleanTimespan_h
+
+#include "mozilla/Maybe.h"
+#include "mozilla/Result.h"
+#include "mozilla/dom/BindingDeclarations.h"
+#include "mozilla/glean/bindings/GleanMetric.h"
+#include "nsTString.h"
+
+namespace mozilla::glean {
+
+namespace impl {
+
+class TimespanMetric {
+ public:
+ constexpr explicit TimespanMetric(uint32_t aId) : mId(aId) {}
+
+ /**
+ * Start tracking time for the provided metric.
+ *
+ * This records an error if it’s already tracking time (i.e. start was already
+ * called with no corresponding [stop]): in that case the original
+ * start time will be preserved.
+ */
+ void Start() const;
+
+ /**
+ * Stop tracking time for the provided metric.
+ *
+ * Sets the metric to the elapsed time, but does not overwrite an already
+ * existing value.
+ * This will record an error if no [start] was called or there is an already
+ * existing value.
+ */
+ void Stop() const;
+
+ /**
+ * Abort a previous Start.
+ *
+ * No error will be recorded if no Start was called.
+ */
+ void Cancel() const;
+
+ /**
+ * Explicitly sets the timespan value
+ *
+ * This API should only be used if you cannot make use of
+ * `start`/`stop`/`cancel`.
+ *
+ * @param aDuration The duration of this timespan, in units matching the
+ * `time_unit` of this metric's definition.
+ */
+ void SetRaw(uint32_t aDuration) const;
+
+ /**
+ * **Test-only API**
+ *
+ * Gets the currently stored value as an integer.
+ *
+ * This function will attempt to await the last parent-process task (if any)
+ * writing to the the metric's storage engine before returning a value.
+ * This function will not wait for data from child processes.
+ *
+ * This doesn't clear the stored value.
+ * Parent process only. Panics in child processes.
+ *
+ * @param aPingName The (optional) name of the ping to retrieve the metric
+ * for. Defaults to the first value in `send_in_pings`.
+ *
+ * @return value of the stored metric, or Nothing() if there is no value.
+ */
+ Result<Maybe<uint64_t>, nsCString> TestGetValue(
+ const nsACString& aPingName = nsCString()) const;
+
+ private:
+ const uint32_t mId;
+};
+} // namespace impl
+
+class GleanTimespan final : public GleanMetric {
+ public:
+ explicit GleanTimespan(uint32_t aId, nsISupports* aParent)
+ : GleanMetric(aParent), mTimespan(aId) {}
+
+ virtual JSObject* WrapObject(
+ JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override final;
+
+ void Start();
+ void Stop();
+ void Cancel();
+ void SetRaw(uint32_t aDuration);
+
+ dom::Nullable<uint64_t> TestGetValue(const nsACString& aPingName,
+ ErrorResult& aRv);
+
+ 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..f7a78165ae
--- /dev/null
+++ b/toolkit/components/glean/bindings/private/TimingDistribution.cpp
@@ -0,0 +1,242 @@
+/* -*- 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/AppShutdown.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/ResultVariant.h"
+#include "mozilla/dom/GleanMetricsBinding.h"
+#include "mozilla/dom/ToJSValue.h"
+#include "mozilla/glean/bindings/HistogramGIFFTMap.h"
+#include "mozilla/glean/fog_ffi_generated.h"
+#include "nsJSUtils.h"
+#include "nsPrintfCString.h"
+#include "nsString.h"
+#include "js/PropertyAndElement.h" // JS_DefineProperty
+
+namespace mozilla::glean {
+
+using MetricId = uint32_t; // Same type as in api/src/private/mod.rs
+using MetricTimerTuple = std::tuple<MetricId, TimerId>;
+class MetricTimerTupleHashKey : public PLDHashEntryHdr {
+ public:
+ using KeyType = const MetricTimerTuple&;
+ using KeyTypePointer = const MetricTimerTuple*;
+
+ explicit MetricTimerTupleHashKey(KeyTypePointer aKey) : mValue(*aKey) {}
+ MetricTimerTupleHashKey(MetricTimerTupleHashKey&& aOther)
+ : PLDHashEntryHdr(std::move(aOther)), mValue(aOther.mValue) {}
+ ~MetricTimerTupleHashKey() = default;
+
+ KeyType GetKey() const { return mValue; }
+ bool KeyEquals(KeyTypePointer aKey) const {
+ return std::get<0>(*aKey) == std::get<0>(mValue) &&
+ std::get<1>(*aKey) == std::get<1>(mValue);
+ }
+
+ static KeyTypePointer KeyToPointer(KeyType aKey) { return &aKey; }
+ static PLDHashNumber HashKey(KeyTypePointer aKey) {
+ // Chosen because this is how nsIntegralHashKey does it.
+ return HashGeneric(std::get<0>(*aKey), std::get<1>(*aKey));
+ }
+ enum { ALLOW_MEMMOVE = true };
+
+ private:
+ const MetricTimerTuple mValue;
+};
+
+using TimerToStampMutex =
+ StaticDataMutex<UniquePtr<nsTHashMap<MetricTimerTupleHashKey, TimeStamp>>>;
+static Maybe<TimerToStampMutex::AutoLock> GetTimerIdToStartsLock() {
+ static TimerToStampMutex sTimerIdToStarts("sTimerIdToStarts");
+ auto lock = sTimerIdToStarts.Lock();
+ // GIFFT will work up to the end of AppShutdownTelemetry.
+ if (AppShutdown::IsInOrBeyond(ShutdownPhase::XPCOMWillShutdown)) {
+ return Nothing();
+ }
+ if (!*lock) {
+ *lock = MakeUnique<nsTHashMap<MetricTimerTupleHashKey, TimeStamp>>();
+ RefPtr<nsIRunnable> cleanupFn = NS_NewRunnableFunction(__func__, [&] {
+ if (AppShutdown::IsInOrBeyond(ShutdownPhase::XPCOMWillShutdown)) {
+ auto lock = sTimerIdToStarts.Lock();
+ *lock = nullptr; // deletes, see UniquePtr.h
+ return;
+ }
+ RunOnShutdown(
+ [&] {
+ auto lock = sTimerIdToStarts.Lock();
+ *lock = nullptr; // deletes, see UniquePtr.h
+ },
+ ShutdownPhase::XPCOMWillShutdown);
+ });
+ // Both getting the main thread and dispatching to it can fail.
+ // In that event we leak. Grab a pointer so we have something to NS_RELEASE
+ // in that case.
+ nsIRunnable* temp = cleanupFn.get();
+ nsCOMPtr<nsIThread> mainThread;
+ if (NS_FAILED(NS_GetMainThread(getter_AddRefs(mainThread))) ||
+ NS_FAILED(mainThread->Dispatch(cleanupFn.forget(),
+ nsIThread::DISPATCH_NORMAL))) {
+ // Failed to dispatch cleanup routine.
+ // First, un-leak the runnable (but only if we actually attempted
+ // dispatch)
+ if (!cleanupFn) {
+ NS_RELEASE(temp);
+ }
+ // Next, cleanup immediately, and allow metrics to try again later.
+ *lock = nullptr;
+ return Nothing();
+ }
+ }
+ return Some(std::move(lock));
+}
+
+} // namespace mozilla::glean
+
+// Called from within FOG's Rust impl.
+extern "C" NS_EXPORT void GIFFT_TimingDistributionStart(
+ uint32_t aMetricId, mozilla::glean::TimerId aTimerId) {
+ auto mirrorId = mozilla::glean::HistogramIdForMetric(aMetricId);
+ if (mirrorId) {
+ mozilla::glean::GetTimerIdToStartsLock().apply([&](auto& lock) {
+ auto tuple = std::make_tuple(aMetricId, aTimerId);
+ // It should be all but impossible for anyone to have already inserted
+ // this timer for this metric given the monotonicity of timer ids.
+ (void)NS_WARN_IF(lock.ref()->Remove(tuple));
+ lock.ref()->InsertOrUpdate(tuple, mozilla::TimeStamp::Now());
+ });
+ }
+}
+
+// Called from within FOG's Rust impl.
+extern "C" NS_EXPORT void GIFFT_TimingDistributionStopAndAccumulate(
+ uint32_t aMetricId, mozilla::glean::TimerId aTimerId) {
+ auto mirrorId = mozilla::glean::HistogramIdForMetric(aMetricId);
+ if (mirrorId) {
+ mozilla::glean::GetTimerIdToStartsLock().apply([&](auto& lock) {
+ auto optStart = lock.ref()->Extract(std::make_tuple(aMetricId, aTimerId));
+ // The timer might not be in the map to be removed if it's already been
+ // cancelled or stop_and_accumulate'd.
+ if (!NS_WARN_IF(!optStart)) {
+ AccumulateTimeDelta(mirrorId.extract(), optStart.extract());
+ }
+ });
+ }
+}
+
+// Called from within FOG's Rust impl.
+extern "C" NS_EXPORT void GIFFT_TimingDistributionAccumulateRawMillis(
+ uint32_t aMetricId, uint32_t aMS) {
+ auto mirrorId = mozilla::glean::HistogramIdForMetric(aMetricId);
+ if (mirrorId) {
+ Accumulate(mirrorId.extract(), aMS);
+ }
+}
+
+// Called from within FOG's Rust impl.
+extern "C" NS_EXPORT void GIFFT_TimingDistributionCancel(
+ uint32_t aMetricId, mozilla::glean::TimerId aTimerId) {
+ auto mirrorId = mozilla::glean::HistogramIdForMetric(aMetricId);
+ if (mirrorId) {
+ mozilla::glean::GetTimerIdToStartsLock().apply([&](auto& lock) {
+ // The timer might not be in the map to be removed if it's already been
+ // cancelled or stop_and_accumulate'd.
+ (void)NS_WARN_IF(
+ !lock.ref()->Remove(std::make_tuple(aMetricId, aTimerId)));
+ });
+ }
+}
+
+namespace mozilla::glean {
+
+namespace impl {
+
+TimerId TimingDistributionMetric::Start() const {
+ return fog_timing_distribution_start(mId);
+}
+
+void TimingDistributionMetric::StopAndAccumulate(const TimerId&& aId) const {
+ fog_timing_distribution_stop_and_accumulate(mId, aId);
+}
+
+// Intentionally not exposed to JS for lack of use case and a time duration
+// type.
+void TimingDistributionMetric::AccumulateRawDuration(
+ const TimeDuration& aDuration) const {
+ fog_timing_distribution_accumulate_raw_nanos(
+ mId, uint64_t(aDuration.ToMicroseconds() * 1000.00));
+}
+
+void TimingDistributionMetric::Cancel(const TimerId&& aId) const {
+ fog_timing_distribution_cancel(mId, aId);
+}
+
+Result<Maybe<DistributionData>, nsCString>
+TimingDistributionMetric::TestGetValue(const nsACString& aPingName) const {
+ nsCString err;
+ if (fog_timing_distribution_test_get_error(mId, &err)) {
+ return Err(err);
+ }
+ if (!fog_timing_distribution_test_has_value(mId, &aPingName)) {
+ return Maybe<DistributionData>();
+ }
+ nsTArray<uint64_t> buckets;
+ nsTArray<uint64_t> counts;
+ uint64_t sum;
+ fog_timing_distribution_test_get_value(mId, &aPingName, &sum, &buckets,
+ &counts);
+ return Some(DistributionData(buckets, counts, sum));
+}
+
+} // namespace impl
+
+/* virtual */
+JSObject* GleanTimingDistribution::WrapObject(
+ JSContext* aCx, JS::Handle<JSObject*> aGivenProto) {
+ return dom::GleanTimingDistribution_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+uint64_t GleanTimingDistribution::Start() { return mTimingDist.Start(); }
+
+void GleanTimingDistribution::StopAndAccumulate(uint64_t aId) {
+ mTimingDist.StopAndAccumulate(std::move(aId));
+}
+
+void GleanTimingDistribution::Cancel(uint64_t aId) {
+ mTimingDist.Cancel(std::move(aId));
+}
+
+void GleanTimingDistribution::TestGetValue(
+ const nsACString& aPingName,
+ dom::Nullable<dom::GleanDistributionData>& aRetval, ErrorResult& aRv) {
+ auto result = mTimingDist.TestGetValue(aPingName);
+ if (result.isErr()) {
+ aRv.ThrowDataError(result.unwrapErr());
+ return;
+ }
+ auto optresult = result.unwrap();
+ if (optresult.isNothing()) {
+ return;
+ }
+
+ dom::GleanDistributionData ret;
+ ret.mSum = optresult.ref().sum;
+ auto& data = optresult.ref().values;
+ for (const auto& entry : data) {
+ dom::binding_detail::RecordEntry<nsCString, uint64_t> bucket;
+ bucket.mKey = nsPrintfCString("%" PRIu64, entry.GetKey());
+ bucket.mValue = entry.GetData();
+ ret.mValues.Entries().EmplaceBack(std::move(bucket));
+ }
+ aRetval.SetValue(std::move(ret));
+}
+
+void GleanTimingDistribution::TestAccumulateRawMillis(uint64_t aSample) {
+ mTimingDist.AccumulateRawDuration(TimeDuration::FromMilliseconds(aSample));
+}
+
+} // 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..ae4a796279
--- /dev/null
+++ b/toolkit/components/glean/bindings/private/TimingDistribution.h
@@ -0,0 +1,125 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_glean_GleanTimingDistribution_h
+#define mozilla_glean_GleanTimingDistribution_h
+
+#include "mozilla/dom/BindingDeclarations.h"
+#include "mozilla/glean/bindings/DistributionData.h"
+#include "mozilla/glean/bindings/GleanMetric.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/Result.h"
+#include "mozilla/TimeStamp.h"
+#include "nsTArray.h"
+
+namespace mozilla::dom {
+struct GleanDistributionData;
+} // namespace mozilla::dom
+
+namespace mozilla::glean {
+
+typedef uint64_t TimerId;
+
+namespace impl {
+class TimingDistributionMetric {
+ public:
+ constexpr explicit TimingDistributionMetric(uint32_t aId) : mId(aId) {}
+
+ /*
+ * Starts tracking time for the provided metric.
+ *
+ * @returns A unique TimerId for the new timer
+ */
+ TimerId Start() const;
+
+ /*
+ * Stops tracking time for the provided metric and associated timer id.
+ *
+ * Adds a count to the corresponding bucket in the timing distribution.
+ * This will record an error if no `Start` was called on this TimerId or
+ * if this TimerId was used to call `Cancel`.
+ *
+ * @param aId The TimerId to associate with this timing. This allows for
+ * concurrent timing of events associated with different ids.
+ */
+ void StopAndAccumulate(const TimerId&& aId) const;
+
+ /*
+ * Adds a duration sample to a timing distribution metric.
+ *
+ * Adds a count to the corresponding bucket in the timing distribution.
+ * Prefer Start() and StopAndAccumulate() where possible.
+ * Users of this API are responsible for ensuring the timing source used
+ * to calculate the TimeDuration is monotonic and consistent accross
+ * platforms.
+ *
+ * NOTE: Negative durations are not handled and will saturate to INT64_MAX
+ * nanoseconds.
+ *
+ * @param aDuration The duration of the sample to add to the distribution.
+ */
+ void AccumulateRawDuration(const TimeDuration& aDuration) const;
+
+ /*
+ * Aborts a previous `Start` call. No error is recorded if no `Start` was
+ * called.
+ *
+ * @param aId The TimerId whose `Start` you wish to abort.
+ */
+ void Cancel(const TimerId&& aId) const;
+
+ /**
+ * **Test-only API**
+ *
+ * Gets the currently stored value as a DistributionData.
+ *
+ * This function will attempt to await the last parent-process task (if any)
+ * writing to the the metric's storage engine before returning a value.
+ * This function will not wait for data from child processes.
+ *
+ * This doesn't clear the stored value.
+ * Parent process only. Panics in child processes.
+ *
+ * @param aPingName The (optional) name of the ping to retrieve the metric
+ * for. Defaults to the first value in `send_in_pings`.
+ *
+ * @return value of the stored metric, or Nothing() if there is no value.
+ */
+ Result<Maybe<DistributionData>, nsCString> TestGetValue(
+ const nsACString& aPingName = nsCString()) const;
+
+ private:
+ const uint32_t mId;
+};
+} // namespace impl
+
+class GleanTimingDistribution final : public GleanMetric {
+ public:
+ explicit GleanTimingDistribution(uint64_t aId, nsISupports* aParent)
+ : GleanMetric(aParent), mTimingDist(aId) {}
+
+ virtual JSObject* WrapObject(
+ JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override final;
+
+ uint64_t Start();
+ void StopAndAccumulate(uint64_t aId);
+ void Cancel(uint64_t aId);
+
+ void TestGetValue(const nsACString& aPingName,
+ dom::Nullable<dom::GleanDistributionData>& aRetval,
+ ErrorResult& aRv);
+
+ void TestAccumulateRawMillis(uint64_t aSample);
+
+ private:
+ virtual ~GleanTimingDistribution() = default;
+
+ const impl::TimingDistributionMetric mTimingDist;
+};
+
+} // namespace mozilla::glean
+
+#endif /* mozilla_glean_GleanTimingDistribution_h */
diff --git a/toolkit/components/glean/bindings/private/Url.cpp b/toolkit/components/glean/bindings/private/Url.cpp
new file mode 100644
index 0000000000..060b27f125
--- /dev/null
+++ b/toolkit/components/glean/bindings/private/Url.cpp
@@ -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/. */
+
+#include "mozilla/glean/bindings/Url.h"
+
+#include "jsapi.h"
+#include "js/String.h"
+#include "nsString.h"
+#include "mozilla/ResultVariant.h"
+#include "mozilla/dom/GleanMetricsBinding.h"
+#include "mozilla/glean/bindings/ScalarGIFFTMap.h"
+#include "mozilla/glean/fog_ffi_generated.h"
+
+namespace mozilla::glean {
+
+namespace impl {
+
+void UrlMetric::Set(const nsACString& aValue) const {
+ auto scalarId = ScalarIdForMetric(mId);
+ if (scalarId) {
+ Telemetry::ScalarSet(scalarId.extract(), NS_ConvertUTF8toUTF16(aValue));
+ }
+ fog_url_set(mId, &aValue);
+}
+
+Result<Maybe<nsCString>, nsCString> UrlMetric::TestGetValue(
+ const nsACString& aPingName) const {
+ nsCString err;
+ if (fog_url_test_get_error(mId, &err)) {
+ return Err(err);
+ }
+ if (!fog_url_test_has_value(mId, &aPingName)) {
+ return Maybe<nsCString>();
+ }
+ nsCString ret;
+ fog_url_test_get_value(mId, &aPingName, &ret);
+ return Some(ret);
+}
+
+} // namespace impl
+
+/* virtual */
+JSObject* GleanUrl::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return dom::GleanUrl_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+void GleanUrl::Set(const nsACString& aValue) { mUrl.Set(aValue); }
+
+void GleanUrl::TestGetValue(const nsACString& aPingName, nsCString& aResult,
+ ErrorResult& aRv) {
+ auto result = mUrl.TestGetValue(aPingName);
+ if (result.isErr()) {
+ aRv.ThrowDataError(result.unwrapErr());
+ return;
+ }
+ auto optresult = result.unwrap();
+ if (!optresult.isNothing()) {
+ aResult.Assign(optresult.extract());
+ } else {
+ aResult.SetIsVoid(true);
+ }
+}
+
+} // namespace mozilla::glean
diff --git a/toolkit/components/glean/bindings/private/Url.h b/toolkit/components/glean/bindings/private/Url.h
new file mode 100644
index 0000000000..26ebdafdd0
--- /dev/null
+++ b/toolkit/components/glean/bindings/private/Url.h
@@ -0,0 +1,77 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_glean_GleanUrl_h
+#define mozilla_glean_GleanUrl_h
+
+#include "mozilla/dom/BindingDeclarations.h"
+#include "mozilla/glean/bindings/GleanMetric.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/Result.h"
+#include "nsString.h"
+
+namespace mozilla::glean {
+
+namespace impl {
+
+class UrlMetric {
+ public:
+ constexpr explicit UrlMetric(uint32_t aId) : mId(aId) {}
+
+ /*
+ * Set to the specified value.
+ *
+ * @param aValue The stringified Url to set the metric to.
+ */
+ void Set(const nsACString& aValue) const;
+
+ /**
+ * **Test-only API**
+ *
+ * Gets the currently stored value as a string.
+ *
+ * This function will attempt to await the last parent-process task (if any)
+ * writing to the the metric's storage engine before returning a value.
+ * This function will not wait for data from child processes.
+ *
+ * This doesn't clear the stored value.
+ * Parent process only. Panics in child processes.
+ *
+ * @param aPingName The (optional) name of the ping to retrieve the metric
+ * for. Defaults to the first value in `send_in_pings`.
+ *
+ * @return value of the stored metric, or Nothing() if there is no value.
+ */
+ Result<Maybe<nsCString>, nsCString> TestGetValue(
+ const nsACString& aPingName = nsCString()) const;
+
+ private:
+ const uint32_t mId;
+};
+} // namespace impl
+
+class GleanUrl final : public GleanMetric {
+ public:
+ explicit GleanUrl(uint32_t aId, nsISupports* aParent)
+ : GleanMetric(aParent), mUrl(aId) {}
+
+ virtual JSObject* WrapObject(
+ JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override final;
+
+ void Set(const nsACString& aValue);
+
+ void TestGetValue(const nsACString& aPingName, nsCString& aResult,
+ ErrorResult& aRv);
+
+ private:
+ virtual ~GleanUrl() = default;
+
+ const impl::UrlMetric mUrl;
+};
+
+} // namespace mozilla::glean
+
+#endif /* mozilla_glean_GleanUrl_h */
diff --git a/toolkit/components/glean/bindings/private/Uuid.cpp b/toolkit/components/glean/bindings/private/Uuid.cpp
new file mode 100644
index 0000000000..6503b5ffd8
--- /dev/null
+++ b/toolkit/components/glean/bindings/private/Uuid.cpp
@@ -0,0 +1,76 @@
+/* -*- 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 "jsapi.h"
+#include "mozilla/ResultVariant.h"
+#include "mozilla/dom/GleanMetricsBinding.h"
+#include "mozilla/glean/bindings/ScalarGIFFTMap.h"
+#include "mozilla/glean/fog_ffi_generated.h"
+#include "nsString.h"
+
+namespace mozilla::glean {
+
+namespace impl {
+
+void UuidMetric::Set(const nsACString& aValue) const {
+ auto scalarId = ScalarIdForMetric(mId);
+ if (scalarId) {
+ Telemetry::ScalarSet(scalarId.extract(), NS_ConvertUTF8toUTF16(aValue));
+ }
+ fog_uuid_set(mId, &aValue);
+}
+
+void UuidMetric::GenerateAndSet() const {
+ // We don't have the generated value to mirror to the scalar,
+ // so calling this function on a mirrored metric is likely an error.
+ (void)NS_WARN_IF(ScalarIdForMetric(mId).isSome());
+ fog_uuid_generate_and_set(mId);
+}
+
+Result<Maybe<nsCString>, nsCString> UuidMetric::TestGetValue(
+ const nsACString& aPingName) const {
+ nsCString err;
+ if (fog_uuid_test_get_error(mId, &err)) {
+ return Err(err);
+ }
+ if (!fog_uuid_test_has_value(mId, &aPingName)) {
+ return Maybe<nsCString>();
+ }
+ nsCString ret;
+ fog_uuid_test_get_value(mId, &aPingName, &ret);
+ return Some(ret);
+}
+
+} // namespace impl
+
+/* virtual */
+JSObject* GleanUuid::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return dom::GleanUuid_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+void GleanUuid::Set(const nsACString& aValue) { mUuid.Set(aValue); }
+
+void GleanUuid::GenerateAndSet() { mUuid.GenerateAndSet(); }
+
+void GleanUuid::TestGetValue(const nsACString& aPingName, nsCString& aResult,
+ ErrorResult& aRv) {
+ auto result = mUuid.TestGetValue(aPingName);
+ if (result.isErr()) {
+ aRv.ThrowDataError(result.unwrapErr());
+ return;
+ }
+ auto optresult = result.unwrap();
+ if (!optresult.isNothing()) {
+ aResult.Assign(optresult.extract());
+ } else {
+ aResult.SetIsVoid(true);
+ }
+}
+
+} // 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..b4c5e2bc32
--- /dev/null
+++ b/toolkit/components/glean/bindings/private/Uuid.h
@@ -0,0 +1,83 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_glean_GleanUuid_h
+#define mozilla_glean_GleanUuid_h
+
+#include "mozilla/Maybe.h"
+#include "mozilla/Result.h"
+#include "mozilla/dom/BindingDeclarations.h"
+#include "mozilla/glean/bindings/GleanMetric.h"
+#include "nsString.h"
+
+namespace mozilla::glean {
+
+namespace impl {
+
+class UuidMetric {
+ public:
+ constexpr explicit UuidMetric(uint32_t aId) : mId(aId) {}
+
+ /*
+ * Sets to the specified value.
+ *
+ * @param aValue The UUID to set the metric to.
+ */
+ void Set(const nsACString& aValue) const;
+
+ /*
+ * Generate a new random UUID and set the metric to it.
+ */
+ void GenerateAndSet() const;
+
+ /**
+ * **Test-only API**
+ *
+ * Gets the currently stored value as a hyphenated string.
+ *
+ * This function will attempt to await the last parent-process task (if any)
+ * writing to the the metric's storage engine before returning a value.
+ * This function will not wait for data from child processes.
+ *
+ * This doesn't clear the stored value.
+ * Parent process only. Panics in child processes.
+ *
+ * @param aPingName The (optional) name of the ping to retrieve the metric
+ * for. Defaults to the first value in `send_in_pings`.
+ *
+ * @return value of the stored metric, or Nothing() if there is no value.
+ */
+ Result<Maybe<nsCString>, nsCString> TestGetValue(
+ const nsACString& aPingName = nsCString()) const;
+
+ private:
+ const uint32_t mId;
+};
+} // namespace impl
+
+class GleanUuid final : public GleanMetric {
+ public:
+ explicit GleanUuid(uint32_t aId, nsISupports* aParent)
+ : GleanMetric(aParent), mUuid(aId) {}
+
+ virtual JSObject* WrapObject(
+ JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override final;
+
+ void Set(const nsACString& aValue);
+ void GenerateAndSet();
+
+ void TestGetValue(const nsACString& aPingName, nsCString& aResult,
+ ErrorResult& aRv);
+
+ 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..003ae4997c
--- /dev/null
+++ b/toolkit/components/glean/build_scripts/glean_parser_ext/cpp.py
@@ -0,0 +1,139 @@
+# -*- coding: utf-8 -*-
+
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+"""
+Outputter to generate C++ code for metrics.
+"""
+
+import json
+
+import jinja2
+from glean_parser import metrics, util
+from util import generate_metric_ids, generate_ping_ids, get_metrics
+
+
+def cpp_datatypes_filter(value):
+ """
+ A Jinja2 filter that renders C++ literals.
+
+ Based on Python's JSONEncoder, but overrides:
+ - lists to array literals {}
+ - strings to "value"
+ """
+
+ class CppEncoder(json.JSONEncoder):
+ def iterencode(self, value):
+ if isinstance(value, list):
+ yield "{"
+ first = True
+ for subvalue in list(value):
+ if not first:
+ yield ", "
+ yield from self.iterencode(subvalue)
+ first = False
+ yield "}"
+ elif isinstance(value, str):
+ yield '"' + value + '"'
+ else:
+ yield from super().iterencode(value)
+
+ return "".join(CppEncoder().iterencode(value))
+
+
+def type_name(obj):
+ """
+ Returns the C++ type to use for a given metric object.
+ """
+
+ if getattr(obj, "labeled", False):
+ class_name = util.Camelize(obj.type[8:]) # strips "labeled_" off the front.
+ label_enum = "DynamicLabel"
+ if obj.labels and len(obj.labels):
+ label_enum = f"{util.Camelize(obj.name)}Label"
+ return f"Labeled<impl::{class_name}Metric, {label_enum}>"
+ generate_enums = getattr(obj, "_generate_enums", []) # Extra Keys? Reasons?
+ if len(generate_enums):
+ for name, _ in generate_enums:
+ if not len(getattr(obj, name)) and isinstance(obj, metrics.Event):
+ return util.Camelize(obj.type) + "Metric<NoExtraKeys>"
+ else:
+ # we always use the `extra` suffix,
+ # because we only expose the new event API
+ suffix = "Extra"
+ return "{}Metric<{}>".format(
+ util.Camelize(obj.type), util.Camelize(obj.name) + suffix
+ )
+ return util.Camelize(obj.type) + "Metric"
+
+
+def extra_type_name(typ: str) -> str:
+ """
+ Returns the corresponding Rust type for event's extra key types.
+ """
+
+ if typ == "boolean":
+ return "bool"
+ elif typ == "string":
+ return "nsCString"
+ elif typ == "quantity":
+ return "uint32_t"
+ else:
+ return "UNSUPPORTED"
+
+
+def output_cpp(objs, output_fd, options={}):
+ """
+ Given a tree of objects, output C++ code to the file-like object `output_fd`.
+
+ :param objs: A tree of objects (metrics and pings) as returned from
+ `parser.parse_objects`.
+ :param output_fd: Writeable file to write the output to.
+ :param options: options dictionary.
+ """
+
+ # Monkeypatch util.snake_case for the templates to use
+ util.snake_case = lambda value: value.replace(".", "_").replace("-", "_")
+ # Monkeypatch util.get_jinja2_template to find templates nearby
+
+ def get_local_template(template_name, filters=()):
+ env = jinja2.Environment(
+ loader=jinja2.PackageLoader("cpp", "templates"),
+ trim_blocks=True,
+ lstrip_blocks=True,
+ )
+ env.filters["camelize"] = util.camelize
+ env.filters["Camelize"] = util.Camelize
+ for filter_name, filter_func in filters:
+ env.filters[filter_name] = filter_func
+ return env.get_template(template_name)
+
+ util.get_jinja2_template = get_local_template
+ get_metric_id = generate_metric_ids(objs)
+ get_ping_id = generate_ping_ids(objs)
+
+ if "pings" in objs:
+ template_filename = "cpp_pings.jinja2"
+ if objs.get("tags"):
+ del objs["tags"]
+ else:
+ template_filename = "cpp.jinja2"
+ objs = get_metrics(objs)
+
+ template = util.get_jinja2_template(
+ template_filename,
+ filters=(
+ ("cpp", cpp_datatypes_filter),
+ ("snake_case", util.snake_case),
+ ("type_name", type_name),
+ ("extra_type_name", extra_type_name),
+ ("metric_id", get_metric_id),
+ ("ping_id", get_ping_id),
+ ("Camelize", util.Camelize),
+ ),
+ )
+
+ output_fd.write(template.render(all_objs=objs))
+ output_fd.write("\n")
diff --git a/toolkit/components/glean/build_scripts/glean_parser_ext/jog.py b/toolkit/components/glean/build_scripts/glean_parser_ext/jog.py
new file mode 100644
index 0000000000..81d92b0f5d
--- /dev/null
+++ b/toolkit/components/glean/build_scripts/glean_parser_ext/jog.py
@@ -0,0 +1,207 @@
+# -*- coding: utf-8 -*-
+
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+"""
+Outputter to generate Rust code for metrics.
+"""
+
+import enum
+import json
+import sys
+
+import jinja2
+from glean_parser import util
+from glean_parser.metrics import Rate
+from util import type_ids_and_categories
+
+from js import ID_BITS, PING_INDEX_BITS
+
+RUNTIME_METRIC_BIT = ID_BITS - 1
+RUNTIME_PING_BIT = PING_INDEX_BITS - 1
+
+# The list of all args to CommonMetricData.
+# No particular order is required, but I have these in common_metric_data.rs
+# order just to be organized.
+# Note that this is util.common_metric_args + "dynamic_label"
+common_metric_data_args = [
+ "name",
+ "category",
+ "send_in_pings",
+ "lifetime",
+ "disabled",
+ "dynamic_label",
+]
+
+# List of all metric-type-specific args that JOG understands.
+known_extra_args = [
+ "time_unit",
+ "memory_unit",
+ "allowed_extra_keys",
+ "reason_codes",
+ "range_min",
+ "range_max",
+ "bucket_count",
+ "histogram_type",
+ "numerators",
+ "ordered_labels",
+]
+
+# List of all ping-specific args that JOG undertsands.
+known_ping_args = [
+ "name",
+ "include_client_id",
+ "send_if_empty",
+ "precise_timestamps",
+ "reason_codes",
+]
+
+
+def ensure_jog_support_for_args():
+ """
+ glean_parser or the Glean SDK might add new metric/ping args.
+ To ensure JOG doesn't fall behind in support,
+ we check the list of JOG-supported args vs glean_parser's.
+ We fail the build if glean_parser has one or more we haven't seen before.
+ """
+
+ unknown_args = set(util.extra_metric_args) - set(known_extra_args)
+
+ unknown_args |= set(util.ping_args) - set(known_ping_args)
+
+ if len(unknown_args):
+ print(f"Unknown glean_parser args {unknown_args}")
+ print("JOG must be updated to support the new args")
+ sys.exit(1)
+
+
+def load_monkeypatches():
+ """
+ Monkeypatch jinja template loading because we're not glean_parser.
+ We're glean_parser_ext.
+ """
+
+ # Monkeypatch util.get_jinja2_template to find templates nearby
+ def get_local_template(template_name, filters=()):
+ env = jinja2.Environment(
+ loader=jinja2.PackageLoader("rust", "templates"),
+ trim_blocks=True,
+ lstrip_blocks=True,
+ )
+ env.filters["camelize"] = util.camelize
+ env.filters["Camelize"] = util.Camelize
+ for filter_name, filter_func in filters:
+ env.filters[filter_name] = filter_func
+ return env.get_template(template_name)
+
+ util.get_jinja2_template = get_local_template
+
+
+def output_factory(objs, output_fd, options={}):
+ """
+ Given a tree of objects, output Rust code to the file-like object `output_fd`.
+ Specifically, Rust code that can generate Rust metrics instances.
+
+ :param objs: A tree of objects (metrics and pings) as returned from
+ `parser.parse_objects`.
+ :param output_fd: Writeable file to write the output to.
+ :param options: options dictionary, presently unused.
+ """
+
+ ensure_jog_support_for_args()
+ load_monkeypatches()
+
+ # Get the metric type ids. Must be the same ids generated in js.py
+ metric_types, categories = type_ids_and_categories(objs)
+
+ template = util.get_jinja2_template(
+ "jog_factory.jinja2",
+ filters=(("snake_case", util.snake_case),),
+ )
+
+ output_fd.write(
+ template.render(
+ all_objs=objs,
+ common_metric_data_args=common_metric_data_args,
+ extra_args=util.extra_args,
+ metric_types=metric_types,
+ runtime_metric_bit=RUNTIME_METRIC_BIT,
+ runtime_ping_bit=RUNTIME_PING_BIT,
+ ID_BITS=ID_BITS,
+ )
+ )
+ output_fd.write("\n")
+
+
+def camel_to_snake(s):
+ assert "_" not in s, "JOG doesn't encode metric typenames with underscores"
+ return "".join(["_" + c.lower() if c.isupper() else c for c in s]).lstrip("_")
+
+
+def output_file(objs, output_fd, options={}):
+ """
+ Given a tree of objects, output them to the file-like object `output_fd`.
+ Specifically, in a format that describes all the metrics and pings defined in objs.
+
+ :param objs: A tree of objects (metrics and pings) as returned from
+ `parser.parse_objects`.
+ Presently a dictionary with keys of literals "pings" and "tags"
+ as well as one key per metric category mapped to lists of
+ pings, tags, and metrics (respecitvely)
+ :param output_fd: Writeable file to write the output to.
+ :param options: options dictionary, presently unused.
+ """
+
+ ensure_jog_support_for_args()
+
+ jog_data = {"pings": [], "metrics": {}}
+
+ if "tags" in objs:
+ del objs["tags"] # JOG has no use for tags.
+
+ pings = objs["pings"]
+ del objs["pings"]
+ for ping in pings.values():
+ ping_arg_list = []
+ for arg in known_ping_args:
+ if hasattr(ping, arg):
+ ping_arg_list.append(getattr(ping, arg))
+ jog_data["pings"].append(ping_arg_list)
+
+ def encode(value):
+ if isinstance(value, enum.Enum):
+ return value.name
+ if isinstance(value, Rate): # `numerators` for an external Denominator metric
+ args = []
+ for arg_name in common_metric_data_args[:-1]:
+ args.append(getattr(value, arg_name))
+
+ # These are deserialized as CommonMetricData.
+ # CMD have a final param JOG never uses: `dynamic_label`
+ # It's optional, so we should be able to omit it, but we'd need to
+ # annotate it with #[serde(default)]... so here we add the sixth
+ # param as None.
+ args.append(None)
+ return args
+ return json.dumps(value)
+
+ for category, metrics in objs.items():
+ dict_cat = jog_data["metrics"].setdefault(category, [])
+ for metric in metrics.values():
+ metric_arg_list = [camel_to_snake(metric.__class__.__name__)]
+ for arg in common_metric_data_args[:-1]:
+ if arg in ["category"]:
+ continue # We don't include the category in each metric.
+ metric_arg_list.append(getattr(metric, arg))
+ extra = {}
+ for arg in known_extra_args:
+ if hasattr(metric, arg):
+ extra[arg] = getattr(metric, arg)
+ if len(extra):
+ metric_arg_list.append(extra)
+ dict_cat.append(metric_arg_list)
+
+ # TODO: Measure the speed gain of removing `indent=2`
+ json.dump(jog_data, output_fd, sort_keys=True, default=encode, indent=2)
diff --git a/toolkit/components/glean/build_scripts/glean_parser_ext/js.py b/toolkit/components/glean/build_scripts/glean_parser_ext/js.py
new file mode 100644
index 0000000000..aabae636f9
--- /dev/null
+++ b/toolkit/components/glean/build_scripts/glean_parser_ext/js.py
@@ -0,0 +1,308 @@
+# -*- coding: utf-8 -*-
+
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+"""
+Outputter to generate C++ code for the JavaScript API for metrics.
+
+The code for the JavaScript API is a bit special in that we only generate C++ code,
+string tables and mapping functions.
+The rest is handled by the WebIDL and XPIDL implementation
+that uses this code to look up metrics by name.
+"""
+
+import jinja2
+from glean_parser import util
+from perfecthash import PerfectHash
+from string_table import StringTable
+from util import generate_metric_ids, generate_ping_ids, get_metrics
+
+"""
+We need to store several bits of information in the Perfect Hash Map Entry:
+
+1. An index into the string table to check for string equality with a search key
+ The perfect hash function will give false-positive for non-existent keys,
+ so we need to verify these ourselves.
+2. Type information to instantiate the correct C++ class
+3. The metric's actual ID to lookup the underlying instance.
+4. Whether the metric is a "submetric" (generated per-label for labeled_* metrics)
+5. Whether the metric was registered at runtime
+
+We have 64 bits to play with, so we dedicate:
+
+1. 32 bit to the string table offset. More than enough for a large string table (~60M metrics).
+2. 5 bit for the type. That allows for 32 metric types. We're not even close to that yet.
+3. 25 bit for the metric ID. That allows for 33.5 million metrics. Let's not go there.
+4. 1 bit for signifying that this metric is a submetric
+5. 1 bit for signifying that this metric was registered at runtime
+
+These values are interpolated into the template as well, so changing them here
+ensures the generated C++ code follows.
+If we ever need more bits for a part (e.g. when we add the 33rd metric type),
+we figure out if either the string table indices or the range of possible IDs can be reduced
+and adjust the constants below.
+"""
+ENTRY_WIDTH = 64
+INDEX_BITS = 32
+ID_BITS = 27 # Includes ID_SIGNAL_BITS
+ID_SIGNAL_BITS = 2
+TYPE_BITS = 5
+
+PING_INDEX_BITS = 16
+
+# Size of the PHF intermediate table.
+# This ensures the algorithm finds empty slots in the buckets
+# with the number of metrics we now have in-tree.
+# toolkit/components/telemetry uses 1024, some others 512.
+# See https://bugzilla.mozilla.org/show_bug.cgi?id=1822477
+PHF_SIZE = 1024
+
+
+def ping_entry(ping_id, ping_string_index):
+ """
+ The 2 pieces of information of a ping encoded into a single 32-bit integer.
+ """
+ assert ping_id < 2 ** (32 - PING_INDEX_BITS)
+ assert ping_string_index < 2**PING_INDEX_BITS
+ return ping_id << PING_INDEX_BITS | ping_string_index
+
+
+def create_entry(metric_id, type_id, idx):
+ """
+ The 3 pieces of information of a metric encoded into a single 64-bit integer.
+ """
+ return metric_id << INDEX_BITS | type_id << (INDEX_BITS + ID_BITS) | idx
+
+
+def metric_identifier(category, metric_name):
+ """
+ The metric's unique identifier, including the category and name
+ """
+ return f"{category}.{util.camelize(metric_name)}"
+
+
+def type_name(obj):
+ """
+ Returns the C++ type to use for a given metric object.
+ """
+
+ if getattr(obj, "labeled", False):
+ return "GleanLabeled"
+ return "Glean" + util.Camelize(obj.type)
+
+
+def subtype_name(obj):
+ """
+ Returns the subtype name for labeled metrics.
+ (e.g. 'boolean' for 'labeled_boolean').
+ Returns "" for non-labeled metrics.
+ """
+ if getattr(obj, "labeled", False):
+ type = obj.type[8:] # strips "labeled_" off the front
+ return "Glean" + util.Camelize(type)
+ return ""
+
+
+def output_js(objs, output_fd_h, output_fd_cpp, options={}):
+ """
+ Given a tree of objects, output code for the JS API to the file-like object `output_fd`.
+
+ :param objs: A tree of objects (metrics and pings) as returned from
+ `parser.parse_objects`.
+ :param output_fd: Writeable file to write the output to.
+ :param options: options dictionary.
+ """
+
+ # Monkeypatch util.get_jinja2_template to find templates nearby
+
+ def get_local_template(template_name, filters=()):
+ env = jinja2.Environment(
+ loader=jinja2.PackageLoader("js", "templates"),
+ trim_blocks=True,
+ lstrip_blocks=True,
+ )
+ env.filters["Camelize"] = util.Camelize
+ for filter_name, filter_func in filters:
+ env.filters[filter_name] = filter_func
+ return env.get_template(template_name)
+
+ util.get_jinja2_template = get_local_template
+
+ if "pings" in objs:
+ write_pings(
+ {"pings": objs["pings"]},
+ output_fd_cpp,
+ "js_pings.jinja2",
+ output_fd_h,
+ "js_pings_h.jinja2",
+ )
+ else:
+ write_metrics(
+ get_metrics(objs), output_fd_cpp, "js.jinja2", output_fd_h, "js_h.jinja2"
+ )
+
+
+def write_metrics(objs, output_fd, template_filename, output_fd_h, template_filename_h):
+ """
+ Given a tree of objects `objs`, output metrics-only code for the JS API to the
+ file-like object `output_fd` using template `template_filename`
+ """
+
+ template = util.get_jinja2_template(
+ template_filename,
+ )
+
+ assert (
+ INDEX_BITS + TYPE_BITS + ID_BITS <= ENTRY_WIDTH
+ ), "INDEX_BITS, TYPE_BITS, or ID_BITS are larger than allowed"
+
+ get_metric_id = generate_metric_ids(objs)
+ # Mapping from a metric's identifier to the entry (metric ID | type id | index)
+ metric_id_mapping = {}
+ categories = []
+
+ category_string_table = StringTable()
+ metric_string_table = StringTable()
+ # Mapping from a type name to its ID
+ metric_type_ids = {}
+
+ for category_name, objs in get_metrics(objs).items():
+ category_camel = util.camelize(category_name)
+ id = category_string_table.stringIndex(category_camel)
+ categories.append((category_camel, id))
+
+ for metric in objs.values():
+ identifier = metric_identifier(category_camel, metric.name)
+ metric_type_tuple = (type_name(metric), subtype_name(metric))
+ if metric_type_tuple in metric_type_ids:
+ type_id, _ = metric_type_ids[metric_type_tuple]
+ else:
+ type_id = len(metric_type_ids) + 1
+ metric_type_ids[metric_type_tuple] = (type_id, metric.type)
+
+ idx = metric_string_table.stringIndex(identifier)
+ metric_id = get_metric_id(metric)
+ entry = create_entry(metric_id, type_id, idx)
+ metric_id_mapping[identifier] = entry
+
+ # Create a lookup table for the metric categories only
+ category_string_table = category_string_table.writeToString("gCategoryStringTable")
+ category_map = [(bytearray(category, "ascii"), id) for (category, id) in categories]
+ name_phf = PerfectHash(category_map, PHF_SIZE)
+ category_by_name_lookup = name_phf.cxx_codegen(
+ name="CategoryByNameLookup",
+ entry_type="category_entry_t",
+ lower_entry=lambda x: str(x[1]) + "ul",
+ key_type="const nsACString&",
+ key_bytes="aKey.BeginReading()",
+ key_length="aKey.Length()",
+ return_type="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, PHF_SIZE)
+ metric_by_name_lookup = metric_phf.cxx_codegen(
+ name="MetricByNameLookup",
+ entry_type="metric_entry_t",
+ lower_entry=lambda x: str(x[1]) + "ull",
+ key_type="const nsACString&",
+ key_bytes="aKey.BeginReading()",
+ key_length="aKey.Length()",
+ return_type="Maybe<uint32_t>",
+ return_entry="return metric_result_check(aKey, entry);",
+ )
+
+ output_fd.write(
+ template.render(
+ categories=categories,
+ metric_id_mapping=metric_id_mapping,
+ metric_type_ids=metric_type_ids,
+ entry_width=ENTRY_WIDTH,
+ index_bits=INDEX_BITS,
+ id_bits=ID_BITS,
+ type_bits=TYPE_BITS,
+ id_signal_bits=ID_SIGNAL_BITS,
+ category_string_table=category_string_table,
+ category_by_name_lookup=category_by_name_lookup,
+ metric_string_table=metric_string_table,
+ metric_by_name_lookup=metric_by_name_lookup,
+ )
+ )
+ output_fd.write("\n")
+
+ output_fd_h.write(
+ util.get_jinja2_template(template_filename_h).render(
+ index_bits=INDEX_BITS,
+ id_bits=ID_BITS,
+ type_bits=TYPE_BITS,
+ id_signal_bits=ID_SIGNAL_BITS,
+ num_categories=len(categories),
+ num_metrics=len(metric_id_mapping.items()),
+ )
+ )
+ output_fd_h.write("\n")
+
+
+def write_pings(objs, output_fd, template_filename, output_fd_h, template_filename_h):
+ """
+ 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_camel = util.camelize(ping_name)
+ pings[ping_camel] = ping_entry(
+ ping_id, ping_string_table.stringIndex(ping_camel)
+ )
+
+ 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, PHF_SIZE)
+ 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="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")
+
+ output_fd_h.write(
+ util.get_jinja2_template(template_filename_h).render(
+ num_pings=len(pings.items()),
+ )
+ )
+ output_fd_h.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..1d7d97cf73
--- /dev/null
+++ b/toolkit/components/glean/build_scripts/glean_parser_ext/run_glean_parser.py
@@ -0,0 +1,234 @@
+# -*- coding: utf-8 -*-
+
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import os
+import sys
+from pathlib import Path
+
+import cpp
+import jinja2
+import jog
+import rust
+from glean_parser import lint, parser, translate, util
+from mozbuild.util import FileAvoidWrite, memoize
+from util import generate_metric_ids
+
+import js
+
+
+@memoize
+def get_deps():
+ # Any imported python module is added as a dep automatically,
+ # so we only need the index and the templates.
+ return {
+ *[str(p) for p in (Path(os.path.dirname(__file__)) / "templates").iterdir()],
+ }
+
+
+class ParserError(Exception):
+ """Thrown from parse if something goes wrong"""
+
+ pass
+
+
+GIFFT_TYPES = {
+ "Event": ["event"],
+ "Histogram": ["timing_distribution", "memory_distribution", "custom_distribution"],
+ "Scalar": [
+ "boolean",
+ "labeled_boolean",
+ "counter",
+ "labeled_counter",
+ "string",
+ "string_list",
+ "timespan",
+ "uuid",
+ "datetime",
+ "quantity",
+ "rate",
+ "url",
+ ],
+}
+
+
+def get_parser_options(moz_app_version):
+ app_version_major = moz_app_version.split(".", 1)[0]
+ return {
+ "allow_reserved": False,
+ "expire_by_version": int(app_version_major),
+ }
+
+
+def parse(args):
+ """
+ Parse and lint the input files,
+ then return the parsed objects for further processing.
+ """
+
+ # Unfortunately, GeneratedFile appends `flags` directly after `inputs`
+ # instead of listifying either, so we need to pull stuff from a *args.
+ yaml_array = args[:-1]
+ moz_app_version = args[-1]
+
+ input_files = [Path(x) for x in yaml_array]
+
+ options = get_parser_options(moz_app_version)
+
+ return parse_with_options(input_files, options)
+
+
+def parse_with_options(input_files, options):
+ # Derived heavily from glean_parser.translate.translate.
+ # Adapted to how mozbuild sends us a fd, and to expire on versions not dates.
+
+ all_objs = parser.parse_objects(input_files, options)
+ if util.report_validation_errors(all_objs):
+ raise ParserError("found validation errors during parse")
+
+ nits = lint.lint_metrics(all_objs.value, options)
+ if nits is not None and any(nit.check_name != "EXPIRED" for nit in nits):
+ # Treat Warnings as Errors in FOG.
+ # But don't fail the whole build on expired metrics (it blocks testing).
+ raise ParserError("glinter nits found during parse")
+
+ objects = all_objs.value
+
+ translate.transform_metrics(objects)
+
+ return objects, options
+
+
+def main(cpp_fd, *args):
+ def open_output(filename):
+ return FileAvoidWrite(os.path.join(os.path.dirname(cpp_fd.name), filename))
+
+ [js_h_path, js_cpp_path, rust_path] = args[-3:]
+ args = args[:-3]
+ all_objs, options = parse(args)
+
+ cpp.output_cpp(all_objs, cpp_fd, options)
+
+ with open_output(js_h_path) as js_fd:
+ with open_output(js_cpp_path) as js_cpp_fd:
+ js.output_js(all_objs, js_fd, js_cpp_fd, options)
+
+ # We only need this info if we're dealing with pings.
+ ping_names_by_app_id = {}
+ if "pings" in all_objs:
+ import sys
+ from os import path
+
+ from buildconfig import topsrcdir
+
+ sys.path.append(path.join(path.dirname(__file__), path.pardir, path.pardir))
+ from metrics_index import pings_by_app_id
+
+ for app_id, ping_yamls in pings_by_app_id.items():
+ input_files = [Path(path.join(topsrcdir, x)) for x in ping_yamls]
+ ping_objs, _ = parse_with_options(input_files, options)
+ ping_names_by_app_id[app_id] = ping_objs["pings"].keys()
+
+ with open_output(rust_path) as rust_fd:
+ rust.output_rust(all_objs, rust_fd, ping_names_by_app_id, options)
+
+ return get_deps()
+
+
+def gifft_map(output_fd, *args):
+ probe_type = args[-1]
+ args = args[:-1]
+ all_objs, options = parse(args)
+
+ # Events also need to output maps from event extra enum to strings.
+ # Sadly we need to generate code for all possible events, not just mirrored.
+ # Otherwise we won't compile.
+ if probe_type == "Event":
+ output_path = Path(os.path.dirname(output_fd.name))
+ with FileAvoidWrite(output_path / "EventExtraGIFFTMaps.cpp") as cpp_fd:
+ output_gifft_map(output_fd, probe_type, all_objs, cpp_fd)
+ else:
+ output_gifft_map(output_fd, probe_type, all_objs, None)
+
+ return get_deps()
+
+
+def output_gifft_map(output_fd, probe_type, all_objs, cpp_fd):
+ get_metric_id = generate_metric_ids(all_objs)
+ ids_to_probes = {}
+ for category_name, objs in all_objs.items():
+ for metric in objs.values():
+ if (
+ hasattr(metric, "telemetry_mirror")
+ and metric.telemetry_mirror is not None
+ ):
+ info = (metric.telemetry_mirror, f"{category_name}.{metric.name}")
+ if metric.type in GIFFT_TYPES[probe_type]:
+ if any(
+ metric.telemetry_mirror == value[0]
+ for value in ids_to_probes.values()
+ ):
+ print(
+ f"Telemetry mirror {metric.telemetry_mirror} already registered",
+ file=sys.stderr,
+ )
+ sys.exit(1)
+ ids_to_probes[get_metric_id(metric)] = info
+ # If we don't support a mirror for this metric type: build error.
+ elif not any(
+ [
+ metric.type in types_for_probe
+ for types_for_probe in GIFFT_TYPES.values()
+ ]
+ ):
+ print(
+ f"Glean metric {category_name}.{metric.name} is of type {metric.type}"
+ " which can't be mirrored (we don't know how).",
+ file=sys.stderr,
+ )
+ sys.exit(1)
+
+ env = jinja2.Environment(
+ loader=jinja2.PackageLoader("run_glean_parser", "templates"),
+ trim_blocks=True,
+ lstrip_blocks=True,
+ )
+ env.filters["snake_case"] = lambda value: value.replace(".", "_").replace("-", "_")
+ env.filters["Camelize"] = util.Camelize
+ template = env.get_template("gifft.jinja2")
+ output_fd.write(
+ template.render(
+ ids_to_probes=ids_to_probes,
+ probe_type=probe_type,
+ id_bits=js.ID_BITS,
+ id_signal_bits=js.ID_SIGNAL_BITS,
+ runtime_metric_bit=jog.RUNTIME_METRIC_BIT,
+ )
+ )
+ output_fd.write("\n")
+
+ # Events also need to output maps from event extra enum to strings.
+ # Sadly we need to generate code for all possible events, not just mirrored.
+ # Otherwise we won't compile.
+ if probe_type == "Event":
+ template = env.get_template("gifft_events.jinja2")
+ cpp_fd.write(template.render(all_objs=all_objs))
+ cpp_fd.write("\n")
+
+
+def jog_factory(output_fd, *args):
+ all_objs, options = parse(args)
+ jog.output_factory(all_objs, output_fd, options)
+ return get_deps()
+
+
+def jog_file(output_fd, *args):
+ all_objs, options = parse(args)
+ jog.output_file(all_objs, output_fd, options)
+ return get_deps()
+
+
+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..615784b481
--- /dev/null
+++ b/toolkit/components/glean/build_scripts/glean_parser_ext/rust.py
@@ -0,0 +1,283 @@
+# -*- coding: utf-8 -*-
+
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+"""
+Outputter to generate Rust code for metrics.
+"""
+
+import enum
+import json
+
+import jinja2
+from glean_parser import util
+from glean_parser.metrics import CowString, Event, Rate
+from util import generate_metric_ids, generate_ping_ids, get_metrics
+
+from js import ID_BITS, ID_SIGNAL_BITS
+
+# The list of all args to CommonMetricData.
+# No particular order is required, but I have these in common_metric_data.rs
+# order just to be organized.
+common_metric_data_args = [
+ "name",
+ "category",
+ "send_in_pings",
+ "lifetime",
+ "disabled",
+ "dynamic_label",
+]
+
+
+def rust_datatypes_filter(value):
+ """
+ A Jinja2 filter that renders Rust literals.
+
+ Based on Python's JSONEncoder, but overrides:
+ - dicts and sets to raise an error
+ - sets to vec![] (used in labels)
+ - enums to become Class::Value
+ - lists to vec![] (used in send_in_pings)
+ - null to None
+ - strings to "value".into()
+ - Rate objects to a CommonMetricData initializer
+ (for external Denominators' Numerators lists)
+ """
+
+ class RustEncoder(json.JSONEncoder):
+ def iterencode(self, value):
+ if isinstance(value, dict):
+ raise ValueError("RustEncoder doesn't know dicts {}".format(str(value)))
+ elif isinstance(value, enum.Enum):
+ yield (value.__class__.__name__ + "::" + util.Camelize(value.name))
+ elif isinstance(value, set):
+ yield from self.iterencode(sorted(list(value)))
+ elif isinstance(value, list):
+ 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"
+ # CowString is also a 'str' but is a special case.
+ # Ensure its case is handled before str's (below).
+ elif isinstance(value, CowString):
+ yield f'::std::borrow::Cow::from("{value.inner}")'
+ elif isinstance(value, str):
+ yield '"' + value + '".into()'
+ elif isinstance(value, Rate):
+ yield "CommonMetricData {"
+ for arg_name in common_metric_data_args:
+ if hasattr(value, arg_name):
+ yield f"{arg_name}: "
+ yield from self.iterencode(getattr(value, arg_name))
+ yield ", "
+ yield " ..Default::default()}"
+ else:
+ yield from super().iterencode(value)
+
+ return "".join(RustEncoder().iterencode(value))
+
+
+def ctor(obj):
+ """
+ Returns the scope and name of the constructor to use for a metric object.
+ Necessary because LabeledMetric<T> is constructed using LabeledMetric::new
+ not LabeledMetric<T>::new
+ """
+ if getattr(obj, "labeled", False):
+ return "LabeledMetric::new"
+ return class_name(obj.type) + "::new"
+
+
+def type_name(obj):
+ """
+ Returns the Rust type to use for a given metric or ping object.
+ """
+
+ if getattr(obj, "labeled", False):
+ label_enum = "super::DynamicLabel"
+ if obj.labels and len(obj.labels):
+ label_enum = f"{util.Camelize(obj.name)}Label"
+ return f"LabeledMetric<Labeled{class_name(obj.type)}, {label_enum}>"
+ generate_enums = getattr(obj, "_generate_enums", []) # Extra Keys? Reasons?
+ if len(generate_enums):
+ for name, _ in generate_enums:
+ if not len(getattr(obj, name)) and isinstance(obj, Event):
+ return class_name(obj.type) + "<NoExtraKeys>"
+ else:
+ # we always use the `extra` suffix,
+ # because we only expose the new event API
+ suffix = "Extra"
+ return "{}<{}>".format(
+ class_name(obj.type), util.Camelize(obj.name) + suffix
+ )
+ return class_name(obj.type)
+
+
+def extra_type_name(typ: str) -> str:
+ """
+ Returns the corresponding Rust type for event's extra key types.
+ """
+
+ if typ == "boolean":
+ return "bool"
+ elif typ == "string":
+ return "String"
+ elif typ == "quantity":
+ return "u32"
+ else:
+ return "UNSUPPORTED"
+
+
+def class_name(obj_type):
+ """
+ Returns the Rust class name for a given metric or ping type.
+ """
+ if obj_type == "ping":
+ return "Ping"
+ if obj_type.startswith("labeled_"):
+ obj_type = obj_type[8:]
+ return util.Camelize(obj_type) + "Metric"
+
+
+def extra_keys(allowed_extra_keys):
+ """
+ Returns the &'static [&'static str] ALLOWED_EXTRA_KEYS for impl ExtraKeys
+ """
+ return "&[" + ", ".join(map(lambda key: '"' + key + '"', allowed_extra_keys)) + "]"
+
+
+def output_rust(objs, output_fd, ping_names_by_app_id, 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 ping_names_by_app_id: A map of app_ids to lists of ping names.
+ Used to determine which custom pings to register.
+ :param options: options dictionary, presently unused.
+ """
+
+ # Monkeypatch util.snake_case 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 = {}
+
+ # Map from a labeled type (e.g. "counter") to a map from metric ID to the
+ # fully qualified path of the labeled metric object in Rust paired with
+ # whether the labeled metric has an enum.
+ # Required for the special handling of labeled metric lookups.
+ #
+ # Example:
+ #
+ # "counter" -> 42 -> ("test_only::mabels_kitchen_counters", false)
+ labeleds_by_id_by_type = {}
+
+ if "pings" in objs:
+ template_filename = "rust_pings.jinja2"
+ objs = {"pings": objs["pings"]}
+ else:
+ template_filename = "rust.jinja2"
+ objs = get_metrics(objs)
+ for category_name, category_value in objs.items():
+ for metric in category_value.values():
+ # The constant is all uppercase and suffixed by `_MAP`
+ const_name = util.snake_case(metric.type).upper() + "_MAP"
+ typ = type_name(metric)
+ key = (const_name, typ)
+
+ metric_name = util.snake_case(metric.name)
+ category_snake = util.snake_case(category_name)
+ full_path = f"{category_snake}::{metric_name}"
+
+ if metric.type == "event":
+ events_by_id[get_metric_id(metric)] = full_path
+ continue
+
+ if getattr(metric, "labeled", False):
+ labeled_type = metric.type[8:]
+ if labeled_type not in labeleds_by_id_by_type:
+ labeleds_by_id_by_type[labeled_type] = {}
+ labeleds_by_id_by_type[labeled_type][get_metric_id(metric)] = (
+ full_path,
+ metric.labels and len(metric.labels),
+ )
+ continue
+
+ if key not in objs_by_type:
+ objs_by_type[key] = []
+ objs_by_type[key].append((get_metric_id(metric), full_path))
+
+ # Now for the modules for each category.
+ template = util.get_jinja2_template(
+ template_filename,
+ filters=(
+ ("rust", rust_datatypes_filter),
+ ("snake_case", util.snake_case),
+ ("type_name", type_name),
+ ("extra_type_name", extra_type_name),
+ ("ctor", ctor),
+ ("extra_keys", extra_keys),
+ ("metric_id", get_metric_id),
+ ("ping_id", get_ping_id),
+ ),
+ )
+
+ output_fd.write(
+ template.render(
+ all_objs=objs,
+ common_metric_data_args=common_metric_data_args,
+ metric_by_type=objs_by_type,
+ extra_args=util.extra_args,
+ events_by_id=events_by_id,
+ labeleds_by_id_by_type=labeleds_by_id_by_type,
+ submetric_bit=ID_BITS - ID_SIGNAL_BITS,
+ ping_names_by_app_id=ping_names_by_app_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..2cf5cb68e7
--- /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..2a4e40d6ac
--- /dev/null
+++ b/toolkit/components/glean/build_scripts/glean_parser_ext/templates/cpp.jinja2
@@ -0,0 +1,101 @@
+// -*- mode: C++ -*-
+
+// AUTOGENERATED BY glean_parser. DO NOT EDIT.
+{# The rendered source is autogenerated, but this
+Jinja2 template is not. Please file bugs! #}
+
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_Metrics_h
+#define mozilla_Metrics_h
+
+#include "mozilla/glean/bindings/MetricTypes.h"
+#include "mozilla/Maybe.h"
+#include "nsTArray.h"
+#include "nsPrintfCString.h"
+
+#include <tuple>
+
+namespace mozilla::glean {
+
+{%- macro generate_extra_keys(obj) -%}
+{% for name, suffix in obj["_generate_enums"] %}
+{# we always use the `extra` suffix, because we only expose the new event API #}
+{% set suffix = "Extra" %}
+{% if obj|attr(name)|length %}
+ {{ extra_keys_with_types(obj, name, suffix)|indent }}
+{% endif %}
+{% endfor %}
+{%- endmacro -%}
+
+{%- macro extra_keys_with_types(obj, name, suffix) -%}
+struct {{ obj.name|Camelize }}{{ suffix }} {
+ {% for item, type in obj|attr(name) %}
+ mozilla::Maybe<{{type|extra_type_name}}> {{ item|camelize }};
+ {% endfor %}
+
+ std::tuple<nsTArray<nsCString>, nsTArray<nsCString>> ToFfiExtra() const {
+ nsTArray<nsCString> extraKeys;
+ nsTArray<nsCString> extraValues;
+ {% for item, type in obj|attr(name) %}
+ if ({{item|camelize}}) {
+ extraKeys.AppendElement()->AssignASCII("{{item}}");
+ {% if type == "string" %}
+ extraValues.EmplaceBack({{item|camelize}}.value());
+ {% elif type == "boolean" %}
+ extraValues.AppendElement()->AssignASCII({{item|camelize}}.value() ? "true" : "false");
+ {% elif type == "quantity" %}
+ extraValues.EmplaceBack(nsPrintfCString("%d", {{item|camelize}}.value()));
+ {% else %}
+#error "Glean: Invalid extra key type for metric {{obj.category}}.{{obj.name}}, defined in: {{obj.defined_in['filepath']}}:{{obj.defined_in['line']}})"
+ {% endif %}
+ }
+ {% endfor %}
+ return std::make_tuple(std::move(extraKeys), std::move(extraValues));
+ }
+};
+{%- endmacro -%}
+
+{# Okay, so though `enum class` means we can't pollute the namespace with the
+ enum variants' identifiers, there's no guarantee there isn't some
+ preprocessor #define lying in wait to collide with us. Using CamelCase
+ helps, but isn't foolproof (X11/X.h has `#define Success 0`).
+ So we prefix it. I chose `e` (for `enum`) for the prefix. #}
+{%- macro generate_label_enum(obj) -%}
+enum class {{ obj.name|Camelize }}Label: uint16_t {
+ {% for label in obj.ordered_labels %}
+ e{{ label|Camelize }} = {{loop.index0}},
+ {% endfor %}
+ e__Other__,
+};
+{%- endmacro %}
+
+struct NoExtraKeys;
+enum class DynamicLabel: uint16_t { };
+
+{% for category_name, objs in all_objs.items() %}
+namespace {{ category_name|snake_case }} {
+ {% for obj in objs.values() %}
+ /**
+ * generated from {{ category_name }}.{{ obj.name }}
+ */
+ {% if obj|attr("_generate_enums") %}
+{{ generate_extra_keys(obj) }}
+ {%- endif %}
+ {% if obj.labeled and obj.labels and obj.labels|length %}
+ {{ generate_label_enum(obj)|indent }}
+ {% endif %}
+ /**
+ * {{ obj.description|wordwrap() | replace('\n', '\n * ') }}
+ */
+ constexpr impl::{{ obj|type_name }} {{obj.name|snake_case }}({{obj|metric_id}});
+
+ {% endfor %}
+}
+{% endfor %}
+
+} // namespace mozilla::glean
+
+#endif // mozilla_Metrics_h
diff --git a/toolkit/components/glean/build_scripts/glean_parser_ext/templates/cpp_pings.jinja2 b/toolkit/components/glean/build_scripts/glean_parser_ext/templates/cpp_pings.jinja2
new file mode 100644
index 0000000000..f877cb9685
--- /dev/null
+++ b/toolkit/components/glean/build_scripts/glean_parser_ext/templates/cpp_pings.jinja2
@@ -0,0 +1,30 @@
+// -*- mode: C++ -*-
+
+// AUTOGENERATED BY glean_parser. DO NOT EDIT.
+{# The rendered source is autogenerated, but this
+Jinja2 template is not. Please file bugs! #}
+
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_glean_Pings_h
+#define mozilla_glean_Pings_h
+
+#include "mozilla/glean/bindings/Ping.h"
+
+namespace mozilla::glean_pings {
+
+{% for obj in all_objs['pings'].values() %}
+/*
+ * Generated from {{ obj.name }}.
+ *
+ * {{ obj.description|wordwrap() | replace('\n', '\n * ') }}
+ */
+constexpr glean::impl::Ping {{ obj.name|Camelize }}({{ obj.name|ping_id }});
+
+{% endfor %}
+
+} // namespace mozilla::glean_pings
+
+#endif // mozilla_glean_Pings_h
diff --git a/toolkit/components/glean/build_scripts/glean_parser_ext/templates/gifft.jinja2 b/toolkit/components/glean/build_scripts/glean_parser_ext/templates/gifft.jinja2
new file mode 100644
index 0000000000..4cfb5f242d
--- /dev/null
+++ b/toolkit/components/glean/build_scripts/glean_parser_ext/templates/gifft.jinja2
@@ -0,0 +1,75 @@
+// -*- mode: C++ -*-
+
+/* This file is auto-generated by run_glean_parser.py.
+ It is only for internal use by types in
+ toolkit/components/glean/bindings/private */
+{# The rendered source is autogenerated, but this
+Jinja2 template is not. Please file bugs! #}
+
+#include "mozilla/AppShutdown.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/glean/bindings/GleanJSMetricsLookup.h"
+#include "mozilla/glean/bindings/jog/JOG.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/Telemetry.h"
+#include "nsIThread.h"
+#include "nsThreadUtils.h"
+
+#ifndef mozilla_glean_{{ probe_type }}GifftMap_h
+#define mozilla_glean_{{ probe_type }}GifftMap_h
+
+#define DYNAMIC_METRIC_BIT ({{runtime_metric_bit}})
+#define GLEAN_METRIC_ID(id) ((id) & ((1ULL << {{id_bits}}) - 1))
+
+namespace mozilla::glean {
+
+using Telemetry::{{ probe_type }}ID;
+
+{% if probe_type == "Scalar" %}
+static inline bool IsSubmetricId(uint32_t aId) {
+ // Submetrics have the 2^{{id_bits - id_signal_bits}} bit set.
+ // (ID_BITS - ID_SIGNAL_BITS, keep it in sync with js.py).
+ return (aId & (1 << {{id_bits - id_signal_bits}})) > 0;
+}
+{% endif %}
+
+static{% if probe_type == "Event" or probe_type == "Scalar" %} inline{% endif %} Maybe<{{ probe_type }}ID> {{ probe_type }}IdForMetric(uint32_t aId) {
+ switch(aId) {
+{% for id, (mirror, metric_name) in ids_to_probes.items() %}
+ case {{ id }}: { // {{ metric_name }}
+ return Some({{ probe_type }}ID::{{ mirror }});
+ }
+{% endfor %}
+ default: {
+ if (MOZ_UNLIKELY(aId & (1 << DYNAMIC_METRIC_BIT))) {
+ // Dynamic (runtime-registered) metric. Use its static (compiletime-
+ // registered) metric's telemetry_mirror mapping.
+ // ...if applicable.
+
+ // Only JS can use dynamic (runtime-registered) metric ids.
+ MOZ_ASSERT(NS_IsMainThread());
+
+ auto metricName = JOG::GetMetricName(aId);
+ // All of these should have names, but the storage only lasts until
+ // XPCOMWillShutdown, so it might return `Nothing()`.
+ if (metricName.isSome()) {
+ auto maybeMetric = MetricByNameLookup(metricName.ref());
+ if (maybeMetric.isSome()) {
+ uint32_t staticId = GLEAN_METRIC_ID(maybeMetric.value());
+ // Let's ensure we don't infinite loop, huh.
+ MOZ_ASSERT(!(staticId & (1 << DYNAMIC_METRIC_BIT)));
+ return {{ probe_type }}IdForMetric(staticId);
+ }
+ }
+ }
+ return Nothing();
+ }
+ }
+}
+
+} // namespace mozilla::glean
+
+#undef GLEAN_METRIC_ID
+#undef DYNAMIC_METRIC_BIT
+
+#endif // mozilla_glean_{{ probe_type }}GifftMaps_h
diff --git a/toolkit/components/glean/build_scripts/glean_parser_ext/templates/gifft_events.jinja2 b/toolkit/components/glean/build_scripts/glean_parser_ext/templates/gifft_events.jinja2
new file mode 100644
index 0000000000..f32640fc19
--- /dev/null
+++ b/toolkit/components/glean/build_scripts/glean_parser_ext/templates/gifft_events.jinja2
@@ -0,0 +1,52 @@
+// -*- mode: C++ -*-
+
+/* This file is auto-generated by run_glean_parser.py.
+ It is only for internal use by types in
+ toolkit/components/glean/bindings/private */
+{# The rendered source is autogenerated, but this
+Jinja2 template is not. Pleas file bugs! #}
+
+#include "mozilla/glean/bindings/Event.h"
+#include "mozilla/glean/GleanMetrics.h"
+
+namespace mozilla::glean {
+
+template <>
+/*static*/ const nsCString impl::EventMetric<NoExtraKeys>::ExtraStringForKey(uint32_t aKey) {
+ MOZ_ASSERT_UNREACHABLE("What are you doing here? No extra keys!");
+ return ""_ns;
+}
+
+{% for category_name, objs in all_objs.items() %}
+{% for obj in objs.values() %}
+{% if obj|attr("_generate_enums") %}
+{# we always use the `extra` suffix, because we only expose the new event API #}
+{% set suffix = "Extra" %}
+{% for name, _ in obj["_generate_enums"] %}
+{% if obj|attr(name)|length %}
+{% set ns %}{{ category_name|snake_case }}{% endset %}
+{% set type %}{{ obj.name|Camelize }}{{ suffix }}{% endset %}
+template <>
+/*static*/ const nsCString impl::EventMetric<{{ ns }}::{{ type }}>::ExtraStringForKey(uint32_t aKey) {
+ using {{ ns }}::{{ type }};
+ switch (aKey) {
+{% if obj|attr("telemetry_mirror") %}{# Optimization: Do not generate switch if not mirrored #}
+{% for key, _ in obj|attr(name) %}
+ case {{loop.index-1}}: {
+ return "{{ key }}"_ns;
+ }
+{% endfor %}
+{% endif %}
+ default: {
+ MOZ_ASSERT_UNREACHABLE("Impossible event key reached.");
+ return ""_ns;
+ }
+ }
+}
+
+{% endif %}
+{% endfor %}
+{% endif %}
+{% endfor %}
+{% endfor %}
+}; // namespace mozilla::glean
diff --git a/toolkit/components/glean/build_scripts/glean_parser_ext/templates/jog_factory.jinja2 b/toolkit/components/glean/build_scripts/glean_parser_ext/templates/jog_factory.jinja2
new file mode 100644
index 0000000000..4b7838a47d
--- /dev/null
+++ b/toolkit/components/glean/build_scripts/glean_parser_ext/templates/jog_factory.jinja2
@@ -0,0 +1,149 @@
+// -*- mode: Rust -*-
+
+// AUTOGENERATED BY glean_parser. DO NOT EDIT.
+{# The rendered source is autogenerated, but this
+Jinja2 template is not. Please file bugs! #}
+
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/// This file contains factory implementation information for the
+/// JOG Runtime Registration module.
+/// It is responsible for being able to build metrics and pings described at runtime.
+/// It is generated to keep it in sync with how the runtime definitions are defined.
+
+use std::borrow::Cow;
+use std::sync::atomic::{AtomicU32, Ordering};
+use crate::private::{
+ CommonMetricData,
+ Lifetime,
+ MemoryUnit,
+ TimeUnit,
+ Ping,
+ LabeledMetric,
+{% for metric_type_name in metric_types.keys() if not metric_type_name.startswith('labeled_') %}
+ {{ metric_type_name|Camelize }}Metric,
+{% endfor %}};
+use crate::private::traits::HistogramType;
+
+pub(crate) static DYNAMIC_METRIC_BIT: u32 = {{runtime_metric_bit}};
+// 2**DYNAMIC_METRIC_BIT + 1 (+1 because we reserve the 0 metric id)
+static NEXT_METRIC_ID: AtomicU32 = AtomicU32::new({{2**runtime_metric_bit + 1}});
+#[cfg(feature = "with_gecko")] // only used in submit_ping_by_id, which is gecko-only.
+pub(crate) static DYNAMIC_PING_BIT: u32 = {{runtime_ping_bit}};
+// 2**DYNAMIC_PING_BIT + 1 (+1 because we reserve the 0 ping id)
+static NEXT_PING_ID: AtomicU32 = AtomicU32::new({{2**runtime_ping_bit + 1}});
+
+pub(crate) mod __jog_metric_maps {
+ use crate::metrics::DynamicLabel;
+ use crate::private::MetricId;
+ use crate::private::{
+ Ping,
+ LabeledMetric,
+ NoExtraKeys,
+ {% for metric_type_name in metric_types.keys() %}
+ {{ metric_type_name|Camelize }}Metric,
+ {% endfor %}
+ };
+ use once_cell::sync::Lazy;
+ use std::collections::HashMap;
+ use std::sync::{Arc, RwLock};
+
+{% for metric_type_name in metric_types.keys() if metric_type_name != "event" and not metric_type_name.startswith('labeled_') %}
+ pub static {{ metric_type_name.upper() }}_MAP: Lazy<Arc<RwLock<HashMap<MetricId, {{ metric_type_name|Camelize }}Metric>>>> =
+ Lazy::new(|| Arc::new(RwLock::new(HashMap::new())));
+
+{% endfor %}
+{# Labeled metrics are special because they're LabeledMetric<Labeled{Counter|Boolean|...}Metric> #}
+{% for metric_type_name in metric_types.keys() if metric_type_name.startswith('labeled_') %}
+ pub static {{ metric_type_name.upper() }}_MAP: Lazy<Arc<RwLock<HashMap<MetricId, LabeledMetric<{{ metric_type_name|Camelize }}Metric, DynamicLabel>>>>> =
+ Lazy::new(|| Arc::new(RwLock::new(HashMap::new())));
+
+{% endfor %}
+ pub static PING_MAP: Lazy<Arc<RwLock<HashMap<u32, Ping>>>> =
+ Lazy::new(|| Arc::new(RwLock::new(HashMap::new())));
+
+{# Event metrics are special because they're EventMetric<K> #}
+ pub static EVENT_MAP: Lazy<Arc<RwLock<HashMap<MetricId, EventMetric<NoExtraKeys>>>>> =
+ Lazy::new(|| Arc::new(RwLock::new(HashMap::new())));
+}
+
+#[derive(Debug)]
+struct MetricTypeNotFoundError(String);
+impl std::error::Error for MetricTypeNotFoundError {}
+impl std::fmt::Display for MetricTypeNotFoundError {
+ fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ write!(f, "Metric type {} not found", self.0)
+ }
+}
+
+/// Creates and registers a metric, returning its type+id.
+pub fn create_and_register_metric(
+ metric_type: &str,
+{# The rest of these are handrolled because it proved easier than maintaining a
+map of argument name to argument type. I may regret this if I need it again. #}
+{# In order of util.common_metric_args and util.extra_metric_args, because why not. #}
+ category: String,
+ name: String,
+ send_in_pings: Vec<String>,
+ lifetime: Lifetime,
+ disabled: bool,
+ time_unit: Option<TimeUnit>,
+ memory_unit: Option<MemoryUnit>,
+ allowed_extra_keys: Option<Vec<String>>,
+{# Skipping reason_codes since that's a ping thing. #}
+ range_min: Option<u64>,
+ range_max: Option<u64>,
+ bucket_count: Option<u64>,
+ histogram_type: Option<HistogramType>,
+ numerators: Option<Vec<CommonMetricData>>,
+{# And, don't forget the list of acceptable labels for a labeled metric. #}
+ labels: Option<Vec<Cow<'static, str>>>,
+) -> Result<(u32, u32), Box<dyn std::error::Error>> {
+ let metric_id = NEXT_METRIC_ID.fetch_add(1, Ordering::SeqCst);
+ let metric32 = match metric_type {
+{% for metric_type_name, metric_type in metric_types.items() %}
+ "{{ metric_type_name }}" => {
+ let metric = {{ metric_type_name|Camelize if not metric_type_name.startswith('labeled_') else "Labeled"}}Metric::{% if metric_type_name == 'event' %}with_runtime_extra_keys{% else %}new{% endif %}(metric_id.into(), CommonMetricData {
+ {% for arg_name in common_metric_data_args if arg_name in metric_type.args %}
+ {{ arg_name }},
+ {% endfor %}
+ ..Default::default()
+ }
+ {%- for arg_name in metric_type.args if arg_name not in common_metric_data_args -%}
+ , {{ arg_name }}.unwrap()
+ {%- endfor -%}
+ {%- if metric_type_name.startswith('labeled_') -%}
+ , labels
+ {%- endif -%}
+ );
+ let metric32: u32 = ({{metric_type.id}} << {{ID_BITS}}) | metric_id;
+ assert!(
+ __jog_metric_maps::{{metric_type_name.upper()}}_MAP.write()?.insert(metric_id.into(), metric).is_none(),
+ "We should never insert a runtime metric with an already-used id."
+ );
+ metric32
+ }
+{% endfor %}
+ _ => return Err(Box::new(MetricTypeNotFoundError(metric_type.to_string())))
+ };
+ Ok((metric32, metric_id))
+}
+
+/// Creates and registers a ping, returning its id.
+pub fn create_and_register_ping(
+ ping_name: String,
+ include_client_id: bool,
+ send_if_empty: bool,
+ precise_timestamps: bool,
+ reason_codes: Vec<String>,
+) -> Result<u32, Box<dyn std::error::Error>> {
+ let ping_id = NEXT_PING_ID.fetch_add(1, Ordering::SeqCst);
+ let ping = Ping::new(ping_name, include_client_id, send_if_empty, precise_timestamps, reason_codes);
+ assert!(
+ __jog_metric_maps::PING_MAP.write()?.insert(ping_id.into(), ping).is_none(),
+ "We should never insert a runtime ping with an already-used id."
+ );
+ Ok(ping_id)
+}
diff --git a/toolkit/components/glean/build_scripts/glean_parser_ext/templates/js.jinja2 b/toolkit/components/glean/build_scripts/glean_parser_ext/templates/js.jinja2
new file mode 100644
index 0000000000..aa0d741083
--- /dev/null
+++ b/toolkit/components/glean/build_scripts/glean_parser_ext/templates/js.jinja2
@@ -0,0 +1,170 @@
+// -*- 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/. */
+
+#include "mozilla/glean/bindings/GleanJSMetricsLookup.h"
+
+#include "mozilla/PerfectHash.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/glean/bindings/MetricTypes.h"
+#include "mozilla/glean/fog_ffi_generated.h"
+#include "nsString.h"
+
+#define GLEAN_INDEX_BITS ({{index_bits}})
+#define GLEAN_TYPE_BITS ({{type_bits}})
+#define GLEAN_ID_BITS ({{id_bits}})
+#define GLEAN_TYPE_ID(id) ((id) >> GLEAN_ID_BITS)
+#define GLEAN_METRIC_ID(id) ((id) & ((1ULL << GLEAN_ID_BITS) - 1))
+#define GLEAN_OFFSET(entry) (entry & ((1ULL << GLEAN_INDEX_BITS) - 1))
+
+namespace mozilla::glean {
+
+// The category lookup table's entry type
+using category_entry_t = uint32_t;
+// The metric lookup table's entry type
+// This is a bitpacked type with {{index_bits}} bits available to index into
+// the string table, {{type_bits}} bits available to signify the metric type,
+// and the remaining {{id_bits}} bits devoted to {{id_signal_bits}} "signal"
+// bits to signify important characteristics (metric's a labeled metric's
+// submetric, metric's been registered at runtime) and {{id_bits - id_signal_bits}} bits
+// for built-in metric ids.
+// Gives room for {{2 ** (id_bits - id_signal_bits)}} of each combination of
+// characteristics (which hopefully will prove to be enough).
+using metric_entry_t = uint64_t;
+
+static_assert(GLEAN_INDEX_BITS + GLEAN_TYPE_BITS + GLEAN_ID_BITS == sizeof(metric_entry_t) * 8, "Index, Type, and ID bits need to fit into a metric_entry_t");
+static_assert(GLEAN_TYPE_BITS + GLEAN_ID_BITS <= sizeof(uint32_t) * 8, "Metric Types and IDs need to fit into at most 32 bits");
+static_assert({{ categories|length }} < UINT32_MAX, "Too many metric categories generated.");
+static_assert({{ metric_id_mapping|length }} < {{2 ** (id_bits - id_signal_bits)}}, "Too many metrics generated. Need room for {{id_signal_bits}} signal bits.");
+static_assert({{ metric_type_ids|length }} < {{2 ** type_bits}}, "Too many different metric types.");
+
+already_AddRefed<GleanMetric> NewMetricFromId(uint32_t id, nsISupports* aParent) {
+ uint32_t typeId = GLEAN_TYPE_ID(id);
+ uint32_t metricId = GLEAN_METRIC_ID(id);
+
+ switch (typeId) {
+ {% for (type_name, subtype_name), (type_id, original_type) in metric_type_ids.items() %}
+ case {{ type_id }}: /* {{ original_type }} */
+ {
+ return MakeAndAddRef<{{type_name}}>(metricId{% if subtype_name|length > 0 %}, {{ type_id }}{% endif %}, aParent);
+ }
+ {% endfor %}
+ default:
+ MOZ_ASSERT_UNREACHABLE("Invalid type ID reached when trying to instantiate a new metric");
+ return nullptr;
+ }
+}
+
+/**
+ * Create a submetric instance for a labeled metric of the provided type and id for the given label.
+ * Assigns or retrieves an id for the submetric from the SDK.
+ *
+ * @param aParentTypeId - The type of the parent labeled metric identified as a number generated during codegen.
+ * Only used to identify which X of LabeledX you are so that X can be created here.
+ * @param aParentMetricId - The metric id for the parent labeled metric.
+ * @param aLabel - The label for the submetric. Might not adhere to the SDK label format.
+ * @param aSubmetricId - an outparam which is assigned the submetric's SDK-generated submetric id.
+ * Used only by GIFFT.
+ */
+already_AddRefed<GleanMetric> NewSubMetricFromIds(uint32_t aParentTypeId,
+ uint32_t aParentMetricId,
+ const nsACString& aLabel,
+ uint32_t* aSubmetricId,
+ nsISupports* aParent) {
+ switch (aParentTypeId) {
+ {% for (type_name, subtype_name), (type_id, original_type) in metric_type_ids.items() %}
+ {# TODO: Remove the subtype inclusion clause when we suport the rest of labeled_* #}
+ {% if subtype_name|length > 0 and original_type in ['labeled_boolean', 'labeled_counter', 'labeled_string'] %}
+ case {{ type_id }}: { /* {{ original_type }} */
+ auto id = impl::fog_{{original_type}}_get(aParentMetricId, &aLabel);
+ *aSubmetricId = id;
+ return MakeAndAddRef<{{subtype_name}}>(id, aParent);
+ }
+ {% endif %}
+ {% endfor %}
+ default: {
+ MOZ_ASSERT_UNREACHABLE("Invalid type ID for submetric.");
+ return nullptr;
+ }
+ }
+}
+
+static Maybe<uint32_t> category_result_check(const nsACString& aKey, category_entry_t entry);
+static Maybe<uint32_t> metric_result_check(const nsACString& aKey, metric_entry_t entry);
+
+{{ category_string_table }}
+static_assert(sizeof(gCategoryStringTable) < UINT32_MAX, "Category string table is too large.");
+
+{{ category_by_name_lookup }}
+
+{{ metric_string_table }}
+static_assert(sizeof(gMetricStringTable) < {{2 ** index_bits}}, "Metric string table is too large.");
+
+{{ metric_by_name_lookup }}
+
+/**
+ * Get a category's name from the string table.
+ */
+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.
+ */
+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
diff --git a/toolkit/components/glean/build_scripts/glean_parser_ext/templates/js_h.jinja2 b/toolkit/components/glean/build_scripts/glean_parser_ext/templates/js_h.jinja2
new file mode 100644
index 0000000000..1de790be10
--- /dev/null
+++ b/toolkit/components/glean/build_scripts/glean_parser_ext/templates/js_h.jinja2
@@ -0,0 +1,77 @@
+// -*- 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 <cstdint>
+
+#include "mozilla/AlreadyAddRefed.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/glean/bindings/GleanMetric.h"
+#include "nsStringFwd.h"
+
+class nsISupports;
+
+namespace mozilla::glean {
+
+// The category lookup table's entry type
+using category_entry_t = uint32_t;
+// The metric lookup table's entry type
+// This is a bitpacked type with {{index_bits}} bits available to index into
+// the string table, {{type_bits}} bits available to signify the metric type,
+// and the remaining {{id_bits}} bits devoted to {{id_signal_bits}} "signal"
+// bits to signify important characteristics (metric's a labeled metric's
+// submetric, metric's been registered at runtime) and {{id_bits - id_signal_bits}} bits
+// for built-in metric ids.
+// Gives room for {{2 ** (id_bits - id_signal_bits)}} of each combination of
+// characteristics (which hopefully will prove to be enough).
+using metric_entry_t = uint64_t;
+
+already_AddRefed<GleanMetric> NewMetricFromId(uint32_t id, nsISupports* aParent);
+
+/**
+ * Create a submetric instance for a labeled metric of the provided type and id for the given label.
+ * Assigns or retrieves an id for the submetric from the SDK.
+ *
+ * @param aParentTypeId - The type of the parent labeled metric identified as a number generated during codegen.
+ * Only used to identify which X of LabeledX you are so that X can be created here.
+ * @param aParentMetricId - The metric id for the parent labeled metric.
+ * @param aLabel - The label for the submetric. Might not adhere to the SDK label format.
+ * @param aSubmetricId - an outparam which is assigned the submetric's SDK-generated submetric id.
+ * Used only by GIFFT.
+ */
+already_AddRefed<GleanMetric> NewSubMetricFromIds(uint32_t aParentTypeId, uint32_t aParentMetricId, const nsACString& aLabel, uint32_t* aSubmetricId, nsISupports* aParent);
+
+/**
+ * Get a category's name from the string table.
+ */
+const char* GetCategoryName(category_entry_t entry);
+
+/**
+ * Get a metric's identifier from the string table.
+ */
+const char* GetMetricIdentifier(metric_entry_t entry);
+
+/**
+ * Get a metric's id given its name.
+ */
+Maybe<uint32_t> MetricByNameLookup(const nsACString&);
+
+/**
+ * Get a category's id given its name.
+ */
+Maybe<uint32_t> CategoryByNameLookup(const nsACString&);
+
+extern const category_entry_t sCategoryByNameLookupEntries[{{num_categories}}];
+extern const metric_entry_t sMetricByNameLookupEntries[{{num_metrics}}];
+
+} // 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..4fa43832a6
--- /dev/null
+++ b/toolkit/components/glean/build_scripts/glean_parser_ext/templates/js_pings.jinja2
@@ -0,0 +1,67 @@
+// -*- 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/. */
+
+#include "mozilla/glean/bindings/GleanJSPingsLookup.h"
+
+#include "mozilla/PerfectHash.h"
+#include "nsString.h"
+
+#include "mozilla/PerfectHash.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;
+
+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.
+ */
+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.
+ */
+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
diff --git a/toolkit/components/glean/build_scripts/glean_parser_ext/templates/js_pings_h.jinja2 b/toolkit/components/glean/build_scripts/glean_parser_ext/templates/js_pings_h.jinja2
new file mode 100644
index 0000000000..3052b8549b
--- /dev/null
+++ b/toolkit/components/glean/build_scripts/glean_parser_ext/templates/js_pings_h.jinja2
@@ -0,0 +1,35 @@
+// -*- 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
+
+#include <cstdint>
+#include "mozilla/Maybe.h"
+#include "nsStringFwd.h"
+
+namespace mozilla::glean {
+
+// Contains the ping id and the index into the ping string table.
+using ping_entry_t = uint32_t;
+
+/**
+ * Get a ping's name given its entry in the PHF.
+ */
+const char* GetPingName(ping_entry_t aEntry);
+
+/**
+ * Get a ping's id given its name.
+ */
+Maybe<uint32_t> PingByNameLookup(const nsACString&);
+
+extern const ping_entry_t sPingByNameLookupEntries[{{num_pings}}];
+} // 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..cc29805099
--- /dev/null
+++ b/toolkit/components/glean/build_scripts/glean_parser_ext/templates/rust.jinja2
@@ -0,0 +1,355 @@
+// -*- mode: Rust -*-
+
+// AUTOGENERATED BY glean_parser. DO NOT EDIT.
+{# The rendered source is autogenerated, but this
+Jinja2 template is not. Please file bugs! #}
+
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+{% macro generate_extra_keys(obj) -%}
+{% for name, _ in obj["_generate_enums"] %}
+{# we always use the `extra` suffix, because we only expose the new event API #}
+{% set suffix = "Extra" %}
+{% if obj|attr(name)|length %}
+ {{ extra_keys_with_types(obj, name, suffix)|indent }}
+{% endif %}
+{% endfor %}
+{%- endmacro -%}
+
+{%- macro extra_keys_with_types(obj, name, suffix) -%}
+#[derive(Default, Debug, Clone, Hash, Eq, PartialEq)]
+pub struct {{ obj.name|Camelize }}{{ suffix }} {
+ {% for item, type in obj|attr(name) %}
+ pub {{ item|snake_case }}: Option<{{type|extra_type_name}}>,
+ {% endfor %}
+}
+
+impl ExtraKeys for {{ obj.name|Camelize }}{{ suffix }} {
+ const ALLOWED_KEYS: &'static [&'static str] = {{ obj.allowed_extra_keys|extra_keys }};
+
+ fn into_ffi_extra(self) -> ::std::collections::HashMap<String, String> {
+ let mut map = ::std::collections::HashMap::new();
+ {% for key, _ in obj|attr(name) %}
+ self.{{key|snake_case}}.and_then(|val| map.insert("{{key|snake_case}}".into(), val.to_string()));
+ {% endfor %}
+ map
+ }
+}
+{%- endmacro -%}
+
+{% macro generate_label_enum(obj) %}
+#[repr(u16)]
+pub enum {{ obj.name|Camelize }}Label {
+ {% for label in obj.ordered_labels %}
+ {# Specifically _not_ using r# as C++ doesn't have it #}
+ {{ label|Camelize }} = {{loop.index0}},
+ {% endfor %}
+ __Other__,
+}
+impl From<u16> for {{ obj.name|Camelize }}Label {
+ fn from(v: u16) -> Self {
+ match v {
+ {% for label in obj.ordered_labels %}
+ {{ loop.index0 }} => Self::{{ label|Camelize }},
+ {% endfor %}
+ _ => Self::__Other__,
+ }
+ }
+}
+impl Into<&'static str> for {{ obj.name|Camelize }}Label {
+ fn into(self) -> &'static str {
+ match self {
+ {% for label in obj.ordered_labels %}
+ Self::{{ label| Camelize }} => "{{label}}",
+ {% endfor %}
+ Self::__Other__ => "__other__",
+ }
+ }
+}
+{%- endmacro -%}
+
+pub enum DynamicLabel { }
+
+{% for category_name, objs in all_objs.items() %}
+pub mod {{ category_name|snake_case }} {
+ use crate::private::*;
+ #[allow(unused_imports)] // CommonMetricData might be unused, let's avoid warnings
+ use glean::CommonMetricData;
+ #[allow(unused_imports)] // HistogramType might be unusued, let's avoid warnings
+ use glean::HistogramType;
+ use once_cell::sync::Lazy;
+
+ {% for obj in objs.values() %}
+ {% if obj|attr("_generate_enums") %}
+{{ generate_extra_keys(obj) }}
+ {%- endif %}
+ {% if obj.labeled and obj.labels and obj.labels|length %}
+ {{ generate_label_enum(obj)|indent }}
+ {% endif %}
+ #[allow(non_upper_case_globals)]
+ /// generated from {{ category_name }}.{{ obj.name }}
+ ///
+ /// {{ obj.description|wordwrap() | replace('\n', '\n /// ') }}
+ {% if obj.type == "counter" and obj.send_in_pings|length == 1 and not obj.disabled and obj.lifetime|rust == "Lifetime::Ping" %}
+ {# Use optimized CounterMetric ctor in a common case (esp. for Use Counters) #}
+ pub static {{ obj.name|snake_case }}: Lazy<{{ obj|type_name }}> = Lazy::new(|| {
+ CounterMetric::codegen_new(
+ {{obj|metric_id}},
+ "{{obj.category}}",
+ "{{obj.name}}",
+ "{{obj.send_in_pings[0]}}"
+ )
+ });
+ {% else %}
+ 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 %}
+ });
+ {% endif %}
+
+ {% endfor %}
+}
+{% endfor %}
+
+{% if metric_by_type|length > 0 %}
+#[allow(dead_code)]
+pub(crate) mod __glean_metric_maps {
+ use std::collections::HashMap;
+
+ use crate::metrics::extra_keys_len;
+ use crate::private::*;
+ use once_cell::sync::Lazy;
+
+{% for typ, metrics in metric_by_type.items() %}
+ pub static {{typ.0}}: Lazy<HashMap<MetricId, &Lazy<{{typ.1}}>>> = Lazy::new(|| {
+ let mut map = HashMap::with_capacity({{metrics|length}});
+ {% for metric in metrics %}
+ map.insert({{metric.0}}.into(), &super::{{metric.1}});
+ {% endfor %}
+ map
+ });
+
+{% endfor %}
+
+ /// Wrapper to record an event based on its metric ID.
+ ///
+ /// # Arguments
+ ///
+ /// * `metric_id` - The metric's ID to look up
+ /// * `extra` - An map of (extra key id, string) pairs.
+ /// The map will be decoded into the appropriate `ExtraKeys` type.
+ /// # Returns
+ ///
+ /// Returns `Ok(())` if the event was found and `record` was called with the given `extra`,
+ /// or an `EventRecordingError::InvalidId` if no event by that ID exists
+ /// or an `EventRecordingError::InvalidExtraKey` if the `extra` map could not be deserialized.
+ pub(crate) fn record_event_by_id(metric_id: u32, extra: HashMap<String, String>) -> Result<(), EventRecordingError> {
+ match metric_id {
+{% for metric_id, event in events_by_id.items() %}
+ {{metric_id}} => {
+ assert!(
+ extra_keys_len(&super::{{event}}) != 0 || extra.is_empty(),
+ "No extra keys allowed, but some were passed"
+ );
+
+ super::{{event}}.record_raw(extra);
+ Ok(())
+ }
+{% endfor %}
+ _ => Err(EventRecordingError::InvalidId),
+ }
+ }
+
+ /// Wrapper to record an event based on its metric ID, with a provided timestamp.
+ ///
+ /// # Arguments
+ ///
+ /// * `metric_id` - The metric's ID to look up
+ /// * `timestamp` - The time at which this event was recorded.
+ /// * `extra` - An map of (extra key id, string) pairs.
+ /// The map will be decoded into the appropriate `ExtraKeys` type.
+ /// # Returns
+ ///
+ /// Returns `Ok(())` if the event was found and `record` was called with the given `extra`,
+ /// or an `EventRecordingError::InvalidId` if no event by that ID exists
+ /// or an `EventRecordingError::InvalidExtraKey` if the event doesn't take extra pairs,
+ /// but some are passed in.
+ pub(crate) fn record_event_by_id_with_time(metric_id: MetricId, timestamp: u64, extra: HashMap<String, String>) -> Result<(), EventRecordingError> {
+ match metric_id {
+{% for metric_id, event in events_by_id.items() %}
+ MetricId({{metric_id}}) => {
+ if extra_keys_len(&super::{{event}}) == 0 && !extra.is_empty() {
+ return Err(EventRecordingError::InvalidExtraKey);
+ }
+
+ super::{{event}}.record_with_time(timestamp, extra);
+ Ok(())
+ }
+{% endfor %}
+ _ => Err(EventRecordingError::InvalidId),
+ }
+ }
+
+ /// Wrapper to get the currently stored events for event metric.
+ ///
+ /// # Arguments
+ ///
+ /// * `metric_id` - The metric's ID to look up
+ /// * `ping_name` - (Optional) The ping name to look into.
+ /// Defaults to the first value in `send_in_pings`.
+ ///
+ /// # Returns
+ ///
+ /// Returns the recorded events or `None` if nothing stored.
+ ///
+ /// # Panics
+ ///
+ /// Panics if no event by the given metric ID could be found.
+ pub(crate) fn event_test_get_value_wrapper(metric_id: u32, ping_name: Option<String>) -> Option<Vec<RecordedEvent>> {
+ match metric_id {
+{% for metric_id, event in events_by_id.items() %}
+ {{metric_id}} => super::{{event}}.test_get_value(ping_name.as_deref()),
+{% endfor %}
+ _ => panic!("No event for metric id {}", metric_id),
+ }
+ }
+
+ /// Check the provided event for errors.
+ ///
+ /// # Arguments
+ ///
+ /// * `metric_id` - The metric's ID to look up
+ /// * `ping_name` - (Optional) The ping name to look into.
+ /// Defaults to the first value in `send_in_pings`.
+ ///
+ /// # Returns
+ ///
+ /// Returns a string for the recorded error or `None`.
+ ///
+ /// # Panics
+ ///
+ /// Panics if no event by the given metric ID could be found.
+ #[allow(unused_variables)]
+ pub(crate) fn event_test_get_error(metric_id: u32) -> Option<String> {
+ #[cfg(feature = "with_gecko")]
+ match metric_id {
+{% for metric_id, event in events_by_id.items() %}
+ {{metric_id}} => test_get_errors!(super::{{event}}),
+{% endfor %}
+ _ => panic!("No event for metric id {}", metric_id),
+ }
+
+ #[cfg(not(feature = "with_gecko"))]
+ {
+ return None;
+ }
+ }
+
+{% for labeled_type, labeleds_by_id in labeleds_by_id_by_type.items() %}
+ /// Gets the submetric from the specified labeled_{{labeled_type}} metric.
+ ///
+ /// # Arguments
+ ///
+ /// * `metric_id` - The metric's ID to look up
+ /// * `label` - The label identifying the {{labeled_type}} submetric.
+ ///
+ /// # Returns
+ ///
+ /// Returns the {{labeled_type}} submetric.
+ ///
+ /// # Panics
+ ///
+ /// Panics if no labeled_{{labeled_type}} by the given metric ID could be found.
+ #[allow(unused_variables)]
+ pub(crate) fn labeled_{{labeled_type}}_get(metric_id: u32, label: &str) -> Labeled{{labeled_type|Camelize}}Metric {
+ match metric_id {
+{% for metric_id, (labeled, _) in labeleds_by_id.items() %}
+ {{metric_id}} => super::{{labeled}}.get(label),
+{% endfor %}
+ _ => panic!("No labeled_{{labeled_type}} for metric id {}", metric_id),
+ }
+ }
+
+ /// Gets the submetric from the specified labeled_{{labeled_type}} metric, by enum.
+ ///
+ /// # Arguments
+ ///
+ /// * `metric_id` - The metric's ID to look up
+ /// * `label_enum` - The label enum identifying the {{labeled_type}} submetric.
+ ///
+ /// # Returns
+ ///
+ /// Returns the {{labeled_type}} submetric.
+ ///
+ /// # Panics
+ ///
+ /// Panics if no labeled_{{labeled_type}} by the given metric ID could be found.
+ #[allow(unused_variables)]
+ pub(crate) fn labeled_{{labeled_type}}_enum_get(metric_id: u32, label_enum: u16) -> Labeled{{labeled_type|Camelize}}Metric {
+ match metric_id {
+{% for metric_id, (labeled, has_enum) in labeleds_by_id.items() %}
+{% if has_enum %}
+ {{metric_id}} => super::{{labeled}}.get(labeled_enum_to_str(metric_id, label_enum)),
+{% endif %}
+{% endfor %}
+ _ => panic!("No labeled_{{labeled_type}} for metric id {}", metric_id),
+ }
+ }
+{% endfor %}
+
+ pub(crate) fn labeled_enum_to_str(metric_id: u32, label: u16) -> &'static str {
+ match metric_id {
+{% for category_name, objs in all_objs.items() %}
+{% for obj in objs.values() %}
+{% if obj.labeled and obj.labels and obj.labels|length %}
+ {{obj|metric_id}} => super::{{category_name|snake_case}}::{{obj.name|Camelize}}Label::from(label).into(),
+{% endif %}
+{% endfor %}
+{% endfor %}
+ _ => panic!("Can't turn label enum to string for metric {} which isn't a labeled metric with static labels", metric_id),
+ }
+ }
+
+ pub(crate) mod submetric_maps {
+ use std::sync::{
+ atomic::AtomicU32,
+ RwLock,
+ };
+ use super::*;
+
+ pub(crate) const SUBMETRIC_BIT: u32 = {{submetric_bit}};
+ pub(crate) static NEXT_LABELED_SUBMETRIC_ID: AtomicU32 = AtomicU32::new((1 << SUBMETRIC_BIT) + 1);
+ pub(crate) static LABELED_METRICS_TO_IDS: Lazy<RwLock<HashMap<(u32, String), u32>>> = Lazy::new(||
+ RwLock::new(HashMap::new())
+ );
+ pub(crate) static LABELED_ENUMS_TO_IDS: Lazy<RwLock<HashMap<(u32, u16), u32>>> = Lazy::new(||
+ RwLock::new(HashMap::new())
+ );
+
+{% for typ, metrics in metric_by_type.items() %}
+{% if typ.0 in ('BOOLEAN_MAP', 'COUNTER_MAP', 'STRING_MAP') %}
+ pub static {{typ.0}}: Lazy<RwLock<HashMap<MetricId, Labeled{{typ.1}}>>> = Lazy::new(||
+ RwLock::new(HashMap::new())
+ );
+{% endif %}
+{% endfor%}
+ }
+}
+{% endif %}
diff --git a/toolkit/components/glean/build_scripts/glean_parser_ext/templates/rust_pings.jinja2 b/toolkit/components/glean/build_scripts/glean_parser_ext/templates/rust_pings.jinja2
new file mode 100644
index 0000000000..c041f663a6
--- /dev/null
+++ b/toolkit/components/glean/build_scripts/glean_parser_ext/templates/rust_pings.jinja2
@@ -0,0 +1,77 @@
+// -*- 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.precise_timestamps|rust }},
+ {{ obj.reason_codes|rust }},
+ )
+});
+
+{% endfor %}
+
+/// Instantiate custom pings once to trigger registration.
+///
+/// # Arguments
+///
+/// application_id: If present, limit to only registering custom pings
+/// assigned to the identified application.
+#[doc(hidden)]
+pub fn register_pings(application_id: Option<&str>) {
+ match application_id {
+ {% for id, ping_names in ping_names_by_app_id.items() %}
+ Some("{{id}}") => {
+ log::info!("Registering pings {{ ping_names|join(', ') }} for {{id}}");
+ {% for ping_name in ping_names %}
+ let _ = &*{{ ping_name|snake_case }};
+ {% endfor %}
+ },
+ {% endfor %}
+ _ => {
+ {% for obj in all_objs['pings'].values() %}
+ let _ = &*{{ obj.name|snake_case }};
+ {% endfor %}
+ }
+ }
+}
+
+#[cfg(feature = "with_gecko")]
+pub(crate) fn submit_ping_by_id(id: u32, reason: Option<&str>) {
+ if id & (1 << crate::factory::DYNAMIC_PING_BIT) > 0 {
+ let map = crate::factory::__jog_metric_maps::PING_MAP
+ .read()
+ .expect("Read lock for dynamic ping map was poisoned!");
+ if let Some(ping) = map.get(&id) {
+ ping.submit(reason);
+ } else {
+ // TODO: instrument this error.
+ log::error!("Cannot submit unknown dynamic ping {} by id.", id);
+ }
+ return;
+ }
+ match id {
+{% for obj in all_objs['pings'].values() %}
+ {{ obj.name|ping_id }} => {{ obj.name | snake_case }}.submit(reason),
+{% endfor %}
+ _ => {
+ // TODO: instrument this error.
+ log::error!("Cannot submit unknown ping {} by id.", id);
+ }
+ }
+}
diff --git a/toolkit/components/glean/build_scripts/glean_parser_ext/util.py b/toolkit/components/glean/build_scripts/glean_parser_ext/util.py
new file mode 100644
index 0000000000..722a3e409c
--- /dev/null
+++ b/toolkit/components/glean/build_scripts/glean_parser_ext/util.py
@@ -0,0 +1,102 @@
+# -*- coding: utf-8 -*-
+
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+"""
+Utility functions for the glean_parser-based code generator
+"""
+import copy
+from typing import Dict, List, Tuple
+
+from glean_parser import util
+
+
+def generate_ping_ids(objs):
+ """
+ Return a lookup function for ping IDs per ping name.
+
+ :param objs: A tree of objects as returned from `parser.parse_objects`.
+ """
+
+ if "pings" not in objs:
+
+ def no_ping_ids_for_you():
+ assert False
+
+ return no_ping_ids_for_you
+
+ # Ping ID 0 is reserved (but unused) right now.
+ ping_id = 1
+
+ ping_id_mapping = {}
+ for ping_name in objs["pings"].keys():
+ ping_id_mapping[ping_name] = ping_id
+ ping_id += 1
+
+ return lambda ping_name: ping_id_mapping[ping_name]
+
+
+def generate_metric_ids(objs):
+ """
+ Return a lookup function for metric IDs per metric object.
+
+ :param objs: A tree of metrics as returned from `parser.parse_objects`.
+ """
+
+ # Metric ID 0 is reserved (but unused) right now.
+ metric_id = 1
+
+ # Mapping from a tuple of (category name, metric name) to the metric's numeric ID
+ metric_id_mapping = {}
+ for category_name, metrics in objs.items():
+ for metric in metrics.values():
+ metric_id_mapping[(category_name, metric.name)] = metric_id
+ metric_id += 1
+
+ return lambda metric: metric_id_mapping[(metric.category, metric.name)]
+
+
+def get_metrics(objs):
+ """
+ Returns *just* the metrics in a set of Glean objects
+ """
+ ret = copy.copy(objs)
+ for category in ["pings", "tags"]:
+ if ret.get(category):
+ del ret[category]
+ return ret
+
+
+def type_ids_and_categories(objs) -> Tuple[Dict[str, Tuple[int, List[str]]], List[str]]:
+ """
+ Iterates over the metrics in objs, constructing two metadata structures:
+ - metric_types: Dict[str, Tuple[int, List[str]]] - map from a metric
+ type (snake_case) to its metric type id and ordered list of arguments.
+ - categories: List[str] - category names (snake_case)
+
+ Is stable across invocations: Will generate same ids for same objs.
+ (If it doesn't, JOG's factory disagreeing with GleanJSMetricsLookup
+ will break the build).
+ Uses the same order of metric args set out in glean_parser.util's
+ common_metric_args and extra_metric_args.
+ (If it didn't, it would supply args in the wrong order to metric type
+ constructors with multiple extra args (e.g. custom_distribution)).
+ """
+ metric_type_ids = {}
+ categories = []
+
+ for category_name, objs in get_metrics(objs).items():
+ categories.append(category_name)
+
+ for metric in objs.values():
+ if metric.type not in metric_type_ids:
+ type_id = len(metric_type_ids) + 1
+ args = util.common_metric_args.copy()
+ for arg_name in util.extra_metric_args:
+ if hasattr(metric, arg_name):
+ args.append(arg_name)
+ metric_type_ids[metric.type] = {"id": type_id, "args": args}
+
+ return (metric_type_ids, categories)
diff --git a/toolkit/components/glean/build_scripts/mach_commands.py b/toolkit/components/glean/build_scripts/mach_commands.py
new file mode 100644
index 0000000000..d385e53605
--- /dev/null
+++ b/toolkit/components/glean/build_scripts/mach_commands.py
@@ -0,0 +1,227 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+from mach.decorators import Command, CommandArgument
+
+LICENSE_HEADER = """# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+"""
+
+GENERATED_HEADER = """
+### This file was AUTOMATICALLY GENERATED by `./mach update-glean-tags`
+### DO NOT edit it by hand.
+"""
+
+
+@Command(
+ "data-review",
+ category="misc",
+ description="Generate a skeleton data review request form for a given bug's data",
+)
+@CommandArgument(
+ "bug", default=None, nargs="?", type=str, help="bug number or search pattern"
+)
+def data_review(command_context, bug=None):
+ # Get the metrics_index's list of metrics indices
+ # by loading the index as a module.
+ import sys
+ from os import path
+
+ sys.path.append(path.join(path.dirname(__file__), path.pardir))
+ from pathlib import Path
+
+ from glean_parser import data_review
+ from metrics_index import metrics_yamls
+
+ return data_review.generate(
+ bug, [Path(command_context.topsrcdir) / x for x in metrics_yamls]
+ )
+
+
+@Command(
+ "perf-data-review",
+ category="misc",
+ description="Generate a skeleton performance data review request form for a given bug's data",
+)
+@CommandArgument(
+ "bug", default=None, nargs="?", type=str, help="bug number or search pattern"
+)
+def perf_data_review(command_context, bug=None):
+ # Get the metrics_index's list of metrics indices
+ # by loading the index as a module.
+ import sys
+ from os import path
+
+ sys.path.append(path.join(path.dirname(__file__), path.pardir))
+ from metrics_index import metrics_yamls
+
+ sys.path.append(path.dirname(__file__))
+ from pathlib import Path
+
+ import perf_data_review
+
+ return perf_data_review.generate(
+ bug, [Path(command_context.topsrcdir) / x for x in metrics_yamls]
+ )
+
+
+@Command(
+ "update-glean-tags",
+ category="misc",
+ description=(
+ "Creates a list of valid glean tags based on in-tree bugzilla component definitions"
+ ),
+)
+def update_glean_tags(command_context):
+ from pathlib import Path
+
+ import yaml
+ from mozbuild.backend.configenvironment import ConfigEnvironment
+ from mozbuild.frontend.reader import BuildReader
+
+ config = ConfigEnvironment(
+ command_context.topsrcdir,
+ command_context.topobjdir,
+ defines=command_context.defines,
+ substs=command_context.substs,
+ )
+
+ reader = BuildReader(config)
+ bug_components = set()
+ for p in reader.read_topsrcdir():
+ if p.get("BUG_COMPONENT"):
+ bug_components.add(p["BUG_COMPONENT"])
+
+ tags_filename = (Path(__file__).parent / "../tags.yaml").resolve()
+
+ tags = {"$schema": "moz://mozilla.org/schemas/glean/tags/1-0-0"}
+ for bug_component in bug_components:
+ product = bug_component.product.strip()
+ component = bug_component.component.strip()
+ tags["{} :: {}".format(product, component)] = {
+ "description": "The Bugzilla component which applies to this object."
+ }
+
+ open(tags_filename, "w").write(
+ "{}\n{}\n\n".format(LICENSE_HEADER, GENERATED_HEADER)
+ + yaml.dump(tags, width=78, explicit_start=True)
+ )
+
+
+def replace_in_file(path, pattern, replace):
+ """
+ Replace `pattern` with `replace` in the file `path`.
+ The file is modified on disk.
+
+ Returns `True` if exactly one replacement happened.
+ `False` otherwise.
+ """
+
+ import re
+
+ with open(path, "r+") as file:
+ data = file.read()
+ data, subs_made = re.subn(pattern, replace, data, flags=re.MULTILINE)
+
+ file.seek(0)
+ file.write(data)
+ file.truncate()
+
+ if subs_made != 1:
+ return False
+
+ return True
+
+
+def replace_in_file_or_die(path, pattern, replace):
+ """
+ Replace `pattern` with `replace` in the file `path`.
+ The file is modified on disk.
+
+ If not exactly one occurrence of `pattern` was replaced it will exit with exit code 1.
+ """
+
+ import sys
+
+ success = replace_in_file(path, pattern, replace)
+ if not success:
+ print(f"ERROR: Failed to replace one occurrence in {path}")
+ print(f" Pattern: {pattern}")
+ print(f" Replace: {replace}")
+ print("File was modified. Check the diff.")
+ sys.exit(1)
+
+
+@Command(
+ "update-glean",
+ category="misc",
+ description="Update Glean to the given version",
+)
+@CommandArgument("version", help="Glean version to upgrade to")
+def update_glean(command_context, version):
+ import textwrap
+ from pathlib import Path
+
+ topsrcdir = Path(command_context.topsrcdir)
+
+ replace_in_file_or_die(
+ topsrcdir / "build.gradle",
+ r'gleanVersion = "[0-9.]+"',
+ f'gleanVersion = "{version}"',
+ )
+ replace_in_file_or_die(
+ topsrcdir / "toolkit" / "components" / "glean" / "Cargo.toml",
+ r'^glean = "[0-9.]+"',
+ f'glean = "{version}"',
+ )
+ replace_in_file_or_die(
+ topsrcdir / "toolkit" / "components" / "glean" / "api" / "Cargo.toml",
+ r'^glean = "[0-9.]+"',
+ f'glean = "{version}"',
+ )
+ replace_in_file_or_die(
+ topsrcdir / "gfx" / "wr" / "webrender" / "Cargo.toml",
+ r'^glean = "[0-9.]+"',
+ f'glean = "{version}"',
+ )
+ replace_in_file_or_die(
+ topsrcdir / "python" / "sites" / "mach.txt",
+ r"glean-sdk==[0-9.]+",
+ f"glean-sdk=={version}",
+ )
+
+ instructions = f"""
+ We've edited most of the necessary files to require Glean SDK {version}.
+
+ You will have to edit the following files yourself:
+
+ gfx/wr/wr_glyph_rasterizer/Cargo.toml
+
+ Then, to ensure Glean and Firefox's other Rust dependencies are appropriately vendored,
+ please run the following commands:
+
+ cargo update -p glean
+ ./mach vendor rust --ignore-modified
+
+ `./mach vendor rust` may identify version mismatches.
+ Please consult the Updating the Glean SDK docs for assistance:
+ https://firefox-source-docs.mozilla.org/toolkit/components/glean/dev/updating_sdk.html
+
+ The Glean SDK is already vetted and no additional vetting for it is necessary.
+ To prune the configuration file after vendoring run:
+
+ ./mach cargo vet prune
+
+ Then, to update webrender which independently relies on the Glean SDK, run:
+
+ cd gfx/wr
+ cargo update -p glean
+
+ Then, to ensure all is well, build Firefox and run the FOG tests.
+ Instructions can be found here:
+ https://firefox-source-docs.mozilla.org/toolkit/components/glean/dev/testing.html
+ """
+
+ print(textwrap.dedent(instructions))
diff --git a/toolkit/components/glean/build_scripts/perf_data_review.py b/toolkit/components/glean/build_scripts/perf_data_review.py
new file mode 100644
index 0000000000..8c84249a2a
--- /dev/null
+++ b/toolkit/components/glean/build_scripts/perf_data_review.py
@@ -0,0 +1,168 @@
+# -*- coding: utf-8 -*-
+
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+"""
+Produce skeleton Performance Data Review Requests.
+
+This was mostly copies from glean_parser, and should be kept in sync.
+"""
+
+import re
+from pathlib import Path
+from typing import Sequence
+
+from glean_parser import parser, util
+
+
+def generate(
+ bug: str,
+ metrics_files: Sequence[Path],
+) -> int:
+ """
+ Commandline helper for Data Review Request template generation.
+
+ :param bug: pattern to match in metrics' bug_numbers lists.
+ :param metrics_files: List of Path objects to load metrics from.
+ :return: Non-zero if there were any errors.
+ """
+
+ metrics_files = util.ensure_list(metrics_files)
+
+ # Accept any value of expires.
+ parser_options = {
+ "allow_reserved": True,
+ "custom_is_expired": lambda expires: False,
+ "custom_validate_expires": lambda expires: True,
+ }
+ all_objects = parser.parse_objects(metrics_files, parser_options)
+
+ if util.report_validation_errors(all_objects):
+ return 1
+
+ # I tried [\W\Z] but it complained. So `|` it is.
+ reobj = re.compile(f"\\W{bug}\\W|\\W{bug}$")
+ durations = set()
+ responsible_emails = set()
+ metrics_table = ""
+ for category_name, metrics in all_objects.value.items():
+ for metric in metrics.values():
+ if not any([len(reobj.findall(bug)) == 1 for bug in metric.bugs]):
+ continue
+
+ metric_name = util.snake_case(metric.name)
+ category_name = util.snake_case(category_name)
+ one_line_desc = metric.description.replace("\n", " ")
+ sensitivity = ", ".join([s.name for s in metric.data_sensitivity])
+ last_bug = metric.bugs[-1]
+ metrics_table += f"`{category_name}.{metric_name}` | "
+ metrics_table += f"{one_line_desc} | {sensitivity} | {last_bug}\n"
+ if metric.type == "event" and len(metric.allowed_extra_keys):
+ for extra_name, extra_detail in metric.extra_keys.items():
+ extra_one_line_desc = extra_detail["description"].replace("\n", " ")
+ metrics_table += f"`{category_name}.{metric_name}#{extra_name}` | "
+ metrics_table += (
+ f"{extra_one_line_desc} | {sensitivity} | {last_bug}\n"
+ )
+
+ durations.add(metric.expires)
+
+ if metric.expires == "never":
+ responsible_emails.update(metric.notification_emails)
+
+ if len(durations) == 1:
+ duration = next(iter(durations))
+ if duration == "never":
+ collection_duration = "This collection will be collected permanently."
+ else:
+ collection_duration = f"This collection has expiry '{duration}'"
+ else:
+ collection_duration = "Parts of this collection expire at different times: "
+ collection_duration += f"{durations}"
+
+ if "never" in durations:
+ collection_duration += "\n" + ", ".join(responsible_emails) + " "
+ collection_duration += "will be responsible for the permanent collections."
+
+ if len(durations) == 0:
+ print(f"I'm sorry, I couldn't find metrics matching the bug number {bug}.")
+ return 1
+
+ # This template is pulled from
+ # https://github.com/mozilla/data-review/blob/main/request.md
+ print(
+ """
+!! Reminder: it is your responsibility to complete and check the correctness of
+!! this automatically-generated request skeleton before requesting Data
+!! Collection Review. See https://wiki.mozilla.org/Data_Collection for details.
+
+DATA REVIEW REQUEST
+1. What questions will you answer with this data?
+
+TODO: Fill this in.
+
+2. Why does Mozilla need to answer these questions? Are there benefits for users?
+ Do we need this information to address product or business requirements?
+
+In order to guarantee the performance of our products, it is vital to monitor
+real-world installs used by real-world users.
+
+3. What alternative methods did you consider to answer these questions?
+ Why were they not sufficient?
+
+Our ability to measure the practical performance impact of changes through CI
+and manual testing is limited. Monitoring the performance of our products in
+the wild among real users is the only way to be sure we have an accurate
+picture.
+
+4. Can current instrumentation answer these questions?
+
+No.
+
+5. List all proposed measurements and indicate the category of data collection for each
+ measurement, using the Firefox data collection categories found on the Mozilla wiki.
+
+Measurement Name | Measurement Description | Data Collection Category | Tracking Bug
+---------------- | ----------------------- | ------------------------ | ------------"""
+ )
+ print(metrics_table)
+ print(
+ """
+6. Please provide a link to the documentation for this data collection which
+ describes the ultimate data set in a public, complete, and accurate way.
+
+This collection is Glean so is documented
+[in the Glean Dictionary](https://dictionary.telemetry.mozilla.org).
+
+7. How long will this data be collected?
+"""
+ )
+ print(collection_duration)
+ print(
+ """
+8. What populations will you measure?
+
+All channels, countries, and locales. No filters.
+
+9. If this data collection is default on, what is the opt-out mechanism for users?
+
+These collections are Glean. The opt-out can be found in the product's preferences.
+
+10. Please provide a general description of how you will analyze this data.
+
+This will be continuously monitored for regression and improvement detection.
+
+11. Where do you intend to share the results of your analysis?
+
+Internal monitoring (GLAM, Redash, Looker, etc.).
+
+12. Is there a third-party tool (i.e. not Telemetry) that you
+ are proposing to use for this data collection?
+
+No.
+"""
+ )
+
+ return 0
diff --git a/toolkit/components/glean/cbindgen.toml b/toolkit/components/glean/cbindgen.toml
new file mode 100644
index 0000000000..fd3af2220c
--- /dev/null
+++ b/toolkit/components/glean/cbindgen.toml
@@ -0,0 +1,25 @@
+header = """/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#ifndef mozilla_glean_fog_ffi_generated_h
+#define mozilla_glean_fog_ffi_generated_h
+"""
+trailer = """
+#endif // mozilla_glean_fog_ffi_generated_h
+"""
+autogen_warning = """/* DO NOT MODIFY THIS MANUALLY! This file was generated using cbindgen. */"""
+include_version = true
+braces = "SameLine"
+line_length = 100
+tab_width = 2
+language = "C++"
+namespaces = ["mozilla::glean::impl"]
+includes = ["nsTArray.h", "nsString.h"]
+
+[export.rename]
+"ThinVec" = "nsTArray"
+
+[parse]
+parse_deps = true
+include = ["firefox-on-glean"]
+extra_bindings = ["firefox-on-glean"]
diff --git a/toolkit/components/glean/docs/dev/builtin_pings.md b/toolkit/components/glean/docs/dev/builtin_pings.md
new file mode 100644
index 0000000000..cbfff8c559
--- /dev/null
+++ b/toolkit/components/glean/docs/dev/builtin_pings.md
@@ -0,0 +1,13 @@
+# Built-in Pings
+
+FOG embeds the Glean SDK so
+[its documentation on pings is authoritative](https://mozilla.github.io/glean/book/user/pings/index.html).
+The only detail FOG adds is to clarify
+[the "baseline" ping's schedule](https://mozilla.github.io/glean/book/user/pings/baseline.html#scheduling).
+Specifically, in Firefox Desktop, the application is considered
+* "active" when started,
+ or when a user interacts with Firefox after a 20min period of inactivity,
+* "inactive" after the user stops interacting with Firefox after 2min of activity.
+
+For more details about why, see the bug tree around
+[bug 1635242](https://bugzilla.mozilla.org/show_bug.cgi?id=1635242).
diff --git a/toolkit/components/glean/docs/dev/code_organization.md b/toolkit/components/glean/docs/dev/code_organization.md
new file mode 100644
index 0000000000..e943dbfd50
--- /dev/null
+++ b/toolkit/components/glean/docs/dev/code_organization.md
@@ -0,0 +1,56 @@
+# FOG code organization
+
+![Modules of Project FOG](images/fog-modules.svg)
+
+The diagram shows the different modules of Project FOG.
+
+## FOG control
+
+This module is the glue between Firefox and Glean.
+
+* The code lives in `toolkit/components/glean/src`.
+* It is written in Rust.
+* The crate is named `fog_control`.
+* It is not published to crates.io.
+* It is not consumed by other Rust crates inside mozilla-central.
+
+This module is responsible for
+
+* collecting and assembling the [client information](https://mozilla.github.io/glean/book/user/pings/index.html#the-client_info-section)
+* configuring the Glean SDK via the Rust Language Binding
+* watching the Firefox Telemetry data upload preference (`datareporting.healthreport.uploadEnabled`)
+* scheduling builtin pings
+* controling ping upload workers
+* passing IPC buffers
+
+It calls into `glean` (the Glean SDK Rust Language Binding) to:
+
+* configure and initialize Glean
+* toggle `upload_enabled`
+* get upload tasks
+
+It calls into `fog` to:
+
+* pass IPC buffers
+* record to its own metrics
+
+## FOG API
+
+This module provides the user-facing API for Glean inside mozilla-central.
+
+* The code lives in `toolkit/components/glean/api`.
+* It is written in Rust.
+* The crate is named `fog`.
+* It is not published to crates.io.
+* It can be consumed by other Rust crates inside mozilla-central for their Glean usage.
+
+This module is responsible for
+
+* exposing a specific metric API in Rust
+* wrapping metric implementations for handling IPC
+* exposing FFI functionality to implement other language APIs on top.
+ See also [Adding a New Metric Type](new_metric_types.md).
+
+It calls into `glean` (the Glean SDK Rust Language Binding) for:
+
+* metric types (including pings)
diff --git a/toolkit/components/glean/docs/dev/images/fog-modules.svg b/toolkit/components/glean/docs/dev/images/fog-modules.svg
new file mode 100644
index 0000000000..c0271e9f6e
--- /dev/null
+++ b/toolkit/components/glean/docs/dev/images/fog-modules.svg
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg xmlns="http://www.w3.org/2000/svg" style="background-color: rgb(255, 255, 255);" version="1.1" width="601px" height="511px" viewBox="-0.5 -0.5 601 511" content="&lt;mxfile host=&quot;app.diagrams.net&quot; modified=&quot;2020-03-27T09:36:49.445Z&quot; agent=&quot;Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:76.0) Gecko/20100101 Firefox/76.0&quot; etag=&quot;_te5ruI7nKv5hlBG0WM7&quot; version=&quot;12.9.3&quot; type=&quot;device&quot;&gt;&lt;diagram id=&quot;02tFoor6QwIjLFNeosYU&quot; name=&quot;Page-1&quot;&gt;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=&lt;/diagram&gt;&lt;/mxfile&gt;"><defs/><g><rect x="0" y="60" width="600" height="120" fill="#fff2cc" stroke="#d6b656" pointer-events="all"/><rect x="0" y="180" width="600" height="330" fill="#d5e8d4" stroke="#82b366" pointer-events="all"/><rect x="460" y="60" width="140" height="30" fill="none" stroke="none" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe flex-end; width: 138px; height: 1px; padding-top: 75px; margin-left: 460px;"><div style="box-sizing: border-box; font-size: 0; text-align: right; "><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; "><div align="right"><font style="font-size: 16px">Developer facing</font></div></div></div></div></foreignObject><text x="598" y="79" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="end">Developer facing</text></switch></g><rect x="470" y="180" width="130" height="30" fill="none" stroke="none" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe flex-end; width: 128px; height: 1px; padding-top: 195px; margin-left: 470px;"><div style="box-sizing: border-box; font-size: 0; text-align: right; "><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; "><div style="font-size: 16px" align="right"><font style="font-size: 16px">Internals</font></div></div></div></div></foreignObject><text x="598" y="199" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="end">Internals</text></switch></g><path d="M 250 150 L 250 190 L 120 190 L 120 203.63" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 120 208.88 L 116.5 201.88 L 120 203.63 L 123.5 201.88 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><rect x="190" y="90" width="120" height="60" fill="#f8cecc" stroke="#b85450" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 118px; height: 1px; padding-top: 120px; margin-left: 191px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; "><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">C++</div></div></div></foreignObject><text x="250" y="124" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">C++</text></switch></g><rect x="360" y="90" width="120" height="60" fill="#f8cecc" stroke="#b85450" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 118px; height: 1px; padding-top: 120px; margin-left: 361px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; "><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">JavaScript</div></div></div></foreignObject><text x="420" y="124" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">JavaScript</text></switch></g><path d="M 420 150 L 420 190 L 120 190 L 120 203.63" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 120 208.88 L 116.5 201.88 L 120 203.63 L 123.5 201.88 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><rect x="360" y="90" width="120" height="60" fill="#ffffff" stroke="#000000" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 118px; height: 1px; padding-top: 120px; margin-left: 361px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; "><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">JavaScript</div></div></div></foreignObject><text x="420" y="124" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">JavaScript</text></switch></g><path d="M 80 150 L 80 170 L 80 223.63" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 80 228.88 L 76.5 221.88 L 80 223.63 L 83.5 221.88 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><rect x="20" y="90" width="120" height="60" fill="#dae8fc" stroke="#6c8ebf" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 118px; height: 1px; padding-top: 120px; margin-left: 21px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; "><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; "><div>Rust</div></div></div></div></foreignObject><text x="80" y="124" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">Rust</text></switch></g><rect x="20" y="230" width="120" height="60" fill="#dae8fc" stroke="#6c8ebf" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 118px; height: 1px; padding-top: 260px; margin-left: 21px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; "><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">FOG API   </div></div></div></foreignObject><text x="80" y="264" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">FOG API   </text></switch></g><path d="M 140 470 L 160 470 L 150 470 L 163.63 470" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 168.88 470 L 161.88 473.5 L 163.63 470 L 161.88 466.5 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><rect x="20" y="440" width="120" height="60" fill="#dae8fc" stroke="#6c8ebf" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 118px; height: 1px; padding-top: 470px; margin-left: 21px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; "><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">FOG Control</div></div></div></foreignObject><text x="80" y="474" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">FOG Control</text></switch></g><rect x="170" y="440" width="120" height="60" fill="#dae8fc" stroke="#6c8ebf" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 118px; height: 1px; padding-top: 470px; margin-left: 171px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; "><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">Glean SDK</div></div></div></foreignObject><text x="230" y="474" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">Glean SDK</text></switch></g><rect x="480" y="0" width="120" height="20" fill="#dae8fc" stroke="#6c8ebf" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 118px; height: 1px; padding-top: 10px; margin-left: 481px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; "><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">Rust</div></div></div></foreignObject><text x="540" y="14" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">Rust</text></switch></g><rect x="480" y="30" width="120" height="20" fill="#f8cecc" stroke="#b85450" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 118px; height: 1px; padding-top: 40px; margin-left: 481px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; "><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">C++</div></div></div></foreignObject><text x="540" y="44" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">C++</text></switch></g><path d="M 230 220 L 270 260 L 230 300 L 190 260 Z" fill="#ffffff" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 78px; height: 1px; padding-top: 260px; margin-left: 191px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; "><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">IPC parent?</div></div></div></foreignObject><text x="230" y="264" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">IPC parent?</text></switch></g><path d="M 230 300 L 230 433.63" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 230 438.88 L 226.5 431.88 L 230 433.63 L 233.5 431.88 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><path d="M 140 260 L 183.63 260" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 188.88 260 L 181.88 263.5 L 183.63 260 L 181.88 256.5 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><rect x="230" y="310" width="40" height="20" fill="none" stroke="none" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 38px; height: 1px; padding-top: 320px; margin-left: 231px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; "><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">YES</div></div></div></foreignObject><text x="250" y="324" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">YES</text></switch></g><rect x="270" y="240" width="40" height="20" fill="none" stroke="none" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 38px; height: 1px; padding-top: 250px; margin-left: 271px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; "><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">NO</div></div></div></foreignObject><text x="290" y="254" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">NO</text></switch></g><rect x="360" y="230" width="230" height="200" fill="#f5f5f5" stroke="#666666" pointer-events="all"/><path d="M 270 260 L 460 260 Q 470 260 470 261.82 L 470 263.63" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 470 268.88 L 466.5 261.88 L 470 263.63 L 473.5 261.88 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><path d="M 470 290 L 470 310 L 470 290 L 470 303.63" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 470 308.88 L 466.5 301.88 L 470 303.63 L 473.5 301.88 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><rect x="410" y="270" width="120" height="20" fill="#dae8fc" stroke="#6c8ebf" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 118px; height: 1px; padding-top: 280px; margin-left: 411px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; "><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">transfer encoding</div></div></div></foreignObject><text x="470" y="284" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">transfer encoding</text></switch></g><path d="M 470 330 L 470 353.63" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 470 358.88 L 466.5 351.88 L 470 353.63 L 473.5 351.88 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><rect x="410" y="310" width="120" height="20" fill="#f8cecc" stroke="#b85450" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 118px; height: 1px; padding-top: 320px; margin-left: 411px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; "><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">IPC send</div></div></div></foreignObject><text x="470" y="324" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">IPC send</text></switch></g><path d="M 470 380 L 470 393.63" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 470 398.88 L 466.5 391.88 L 470 393.63 L 473.5 391.88 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><rect x="410" y="360" width="120" height="20" fill="#f8cecc" stroke="#b85450" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 118px; height: 1px; padding-top: 370px; margin-left: 411px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; "><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">IPC recv</div></div></div></foreignObject><text x="470" y="374" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">IPC recv</text></switch></g><path d="M 470 420 L 470 470 L 296.37 470" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 291.12 470 L 298.12 466.5 L 296.37 470 L 298.12 473.5 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><rect x="410" y="400" width="120" height="20" fill="#dae8fc" stroke="#6c8ebf" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 118px; height: 1px; padding-top: 410px; margin-left: 411px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; "><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">transfer decoding</div></div></div></foreignObject><text x="470" y="414" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">transfer decoding</text></switch></g><rect x="520" y="230" width="70" height="20" fill="none" stroke="none" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 68px; height: 1px; padding-top: 240px; margin-left: 521px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; "><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">IPC layer</div></div></div></foreignObject><text x="555" y="244" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">IPC layer</text></switch></g><rect x="100" y="210" width="40" height="20" fill="#f8cecc" stroke="#b85450" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 38px; height: 1px; padding-top: 220px; margin-left: 101px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; "><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; "><font style="font-size: 11px">C API</font></div></div></div></foreignObject><text x="120" y="224" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">C API</text></switch></g></g><switch><g requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"/><a transform="translate(0,-5)" href="https://desk.draw.io/support/solutions/articles/16000042487" target="_blank"><text text-anchor="middle" font-size="10px" x="50%" y="100%">Viewer does not support full SVG 1.1</text></a></switch></svg> \ No newline at end of file
diff --git a/toolkit/components/glean/docs/dev/index.md b/toolkit/components/glean/docs/dev/index.md
new file mode 100644
index 0000000000..334f040ed9
--- /dev/null
+++ b/toolkit/components/glean/docs/dev/index.md
@@ -0,0 +1,15 @@
+# Developing Firefox on Glean
+
+This section of docs is designed to be helpful to people developing FOG.
+If you're not touching code, tests, or documentation in `toolkit/components/glean`,
+and you're not interested in implementation details, you probably want
+[the user docs instead](../user/index).
+
+```{toctree}
+:titlesonly:
+:maxdepth: 1
+:glob:
+
+*
+Glean SDK Source <https://github.com/mozilla/glean/>
+```
diff --git a/toolkit/components/glean/docs/dev/ipc.md b/toolkit/components/glean/docs/dev/ipc.md
new file mode 100644
index 0000000000..1d6bf5366d
--- /dev/null
+++ b/toolkit/components/glean/docs/dev/ipc.md
@@ -0,0 +1,184 @@
+# Inter-process Communication (IPC)
+
+Firefox Desktop is a multi-process desktop application.
+Code requiring instrumentation may be on any of its processes,
+so FOG provide facilities to do just that.
+
+## Design
+
+The IPC Design of FOG was worked out in
+[bug 1618253](https://bugzilla.mozilla.org/show_bug.cgi?id=1618253).
+
+It centred around a few specific concepts:
+
+### Forbidding Non-Commutative Operations
+
+Because we cannot nicely impose a canonical ordering of metric operations across all processes,
+FOG forbids non-[commutative](https://en.wikipedia.org/wiki/Commutative_property)
+metric operations in some circumstances.
+
+For example,
+`Add()`-ing to a Counter metric works from multiple processes because the order doesn't matter.
+However, given a String metric being `Set()` from multiple processes simultaneously,
+which value should it take?
+
+This ambiguity is not a good foundation to build trust on,
+so we forbid setting a String metric from multiple processes.
+
+#### List of Forbidden Operations
+
+* Boolean's `set` (this is the metric type's only operation)
+* Labeled Boolean's `set` (this is the metric type's only operation)
+* String's `set` (this is the metric type's only operation)
+* Labeled String's `set` (this is the metric type's only operation)
+* String List's `set`
+ * `add` is permitted (order and uniqueness are not guaranteed)
+* Timespan's `start`, `stop`, and `cancel` (these are the metric type's only operations)
+* UUID's `set` and `generateAndSet` (these are the metric type's only operations)
+* Datetime's `set` (this is the metric type's only operation)
+* Quantity's `set` (this is the metric type's only operation)
+
+This list may grow over time as new metric types are added.
+If there's an operation/metric type on this list that you need to use in a non-parent process,
+please reach out
+[on the #glean channel](https://chat.mozilla.org/#/room/#glean:mozilla.org)
+and we'll help you out.
+
+### Process Agnosticism
+
+For metric types that can be used cross-process,
+FOG provides no facility for identifying which process the instrumentation is on.
+
+What this means is that if you accumulate to a
+[Timing Distribution](https://mozilla.github.io/glean/book/user/metrics/timing_distribution.html)
+in multiple processes,
+all the samples from all the processes will be combined in the same metric.
+
+If you wish to distinguish samples from different process types,
+you will need multiple metrics and inline code to select the proper one for the given process.
+For example:
+
+```C++
+if (XRE_GetProcessType() == GeckoProcessType_Default) {
+ mozilla::glean::performance::cache_size.Accumulate(numBytes / 1024);
+} else {
+ mozilla::glean::performance::non_main_process_cache_size.Accumulate(numBytes / 1024);
+}
+```
+
+### Scheduling
+
+FOG makes no guarantee about when non-main-process metric values are sent across IPC.
+FOG will try its best to schedule opportunistically in idle moments,
+and during orderly shutdowns.
+
+There are a few cases where we provide more firm guarantees:
+
+#### Tests
+
+There are test-only APIs in Rust, C++, and Javascript.
+These do not await a flush of child process metric values.
+You can use the test-only method `testFlushAllChildren` on the `FOG`
+XPCOM component to await child data's arrival:
+```js
+await Services.fog.testFlushAllChildren();
+```
+See [the test documentation](testing.md) for more details on testing FOG.
+For writing tests about instrumentation, see
+[the instrumentation test documentation](../user/instrumentation_tests).
+
+#### Pings
+
+We do not guarantee that non-main-process data has made it into a specific ping.
+
+[Built-in pings](https://mozilla.github.io/glean/book/user/pings/index.html)
+are submitted by the Rust Glean SDK at times FOG doesn't directly control,
+so there may be data not present in the parent process when a built-in ping is submitted.
+We don't anticipate this causing a problem since child-process data that
+"misses" a given ping will be included in the next one.
+
+At this time,
+[Custom Pings](https://mozilla.github.io/glean/book/user/pings/custom.html)
+must be sent in the parent process and have no mechanism
+to schedule their submission for after child-process data arrives in the parent process.
+[bug 1732118](https://bugzilla.mozilla.org/show_bug.cgi?id=1732118)
+tracks the addition of such a mechanism or guarantee.
+
+#### Shutdown
+
+We will make a best effort during an orderly shutdown to flush all pending data in child processes.
+This means a disorderly shutdown (usually a crash)
+may result in child process data being lost.
+
+#### Size
+
+We don't measure or keep an up-to-date calculation of the size of the IPC Payload.
+We do, however, keep a count of the number of times the IPC Payload has been accessed.
+This is used as a (very) conservative estimate of the size of the IPC Payload so we do not exceed the
+[IPC message size limit](https://searchfox.org/mozilla-central/search?q=kMaximumMessageSize).
+
+See [bug 1745660](https://bugzilla.mozilla.org/show_bug.cgi?id=1745660).
+
+### Mechanics
+
+The rough design is that the parent process can request an immediate flush of pending data,
+and each child process can decide to flush its pending data whenever it wishes.
+The former is via `FlushFOGData() returns (ByteBuf)` and the latter via `FOGData(ByteBuf)`.
+
+Pending Data is a buffer of bytes generated by `bincode` in Rust in the Child,
+handed off to C++, passed over IPC,
+then given back to `bincode` in Rust on the Parent.
+
+Rust is then responsible for turning the pending data into
+[metrics API][glean-metrics] calls on the metrics in the parent process.
+
+#### Supported Process Types
+
+FOG supports messaging between the following types of child process and the parent process:
+* content children (via `PContent`
+ (for now. See [bug 1641989](https://bugzilla.mozilla.org/show_bug.cgi?id=1641989))
+* gmp children (via `PGMP`)
+* gpu children (via `PGPU`)
+* rdd children (via `PRDD`)
+* socket children (via `PSocketProcess`)
+* utility children (via `PUtilityProcess`)
+
+See
+[the process model docs](/dom/ipc/process_model.rst)
+for more information about what that means.
+
+### Adding Support for a new Process Type
+
+Adding support for a new process type is a matter of extending the two messages
+mentioned above in "Mechanics" to another process type's protocol (ipdl file).
+
+1. Add two messages to the appropriate sections in `P<ProcessType>.ipdl`
+ * (( **Note:** `PGPU` _should_ be the only ipdl where `parent`
+ means the non-parent/-main/-UI process,
+ but double-check that you get this correct.))
+ * Add `async FOGData(ByteBuf&& aBuf);` to the parent/main/UI process side of things
+ (most often `parent:`).
+ * Add `async FlushFOGData() returns (ByteBuf buf);` to the non-parent/-main/-UI side
+ (most often `child:`).
+2. Implement the protocol endpoints in `P<ProcessType>{Child|Parent}.{h|cpp}`
+ * The message added to the `parent: ` section goes in
+ `P<ProcessType>Parent.{h|cpp}` and vice versa.
+3. Add to `FOGIPC.cpp`'s `FlushAllChildData` code that
+ 1. Enumerates all processes of the newly-supported type (there may only be one),
+ 2. Calls `SendFlushFOGData on each, and
+ 3. Adds the resulting promise to the array.
+4. Add to `FOGIPC.cpp`'s `SendFOGData` the correct `GeckoProcessType_*`
+ enum value, and appropriate code for getting the parent process singleton and calling
+ `SendFOGData` on it.
+5. Add to the fog crate's `register_process_shutdown` function
+ handling for at-shutdown flushing of IPC data.
+ If this isn't added, we will log (but not panic)
+ on the first use of Glean APIs on an unsupported process type.
+ * "Handling" might be an empty block with a comment explaining where to find it
+ (like how `PROCESS_TYPE_DEFAULT` is handled)
+ * Or it might be custom code
+ (like `PROCESS_TYPE_CONTENT`'s)
+6. Add to the documented [list of supported process types](#supported-process-types)
+ the process type you added support for.
+
+[glean-metrics]: https://mozilla.github.io/glean/book/reference/metrics/index.html
diff --git a/toolkit/components/glean/docs/dev/jog.md b/toolkit/components/glean/docs/dev/jog.md
new file mode 100644
index 0000000000..c8d0549cd4
--- /dev/null
+++ b/toolkit/components/glean/docs/dev/jog.md
@@ -0,0 +1,91 @@
+# Runtime Metric Definition Subsystem: JOG
+
+```{admonition} I'm Sorry
+Why is it caled JOG? Because it's concerned with... run... time.
+```
+
+The normal mechanism for registering metrics,
+for reasons as varied from ease-of-impl to performance,
+happens at compile time.
+However, this doesn't support use cases like
+* [Artifact Builds][artifact-build]
+ (Where only the JavaScript of Firefox Desktop is repackaged at build time,
+ so there is no compile environment)
+* Dynamic Telemetry
+ (A theorized system for instrumenting Firefox Desktop without shipping code)
+* Web Extensions
+ (Or at least the kind that can't or won't use
+ [the Glean JS SDK][glean-js])
+
+Thus we need a subsystem that supports the runtime registration of metrics.
+We call it JOG and it was implemented in [bug 1698184][impl-bug].
+
+## JavaScript Only
+
+Metrics in C++ and Rust are identified by identifiers which we can't swap out at runtime.
+Thus, in order for changes to metrics to be visible to instrumented systems in C++ or Rust, you must compile.
+
+JavaScript, on the other hand, we supply instances to on-demand.
+It not only supports the specific use cases driving this project,
+it's the only environment that can benefit from runtime metric definition in Firefox Desktop.
+
+## Design
+
+The original design was done as part of
+[bug 1662863][design-bug].
+Things have mostly just been refined from there.
+
+## Architecture
+
+We silo as much of the subsystem as we can into the
+`jog` module located in `toolkit/components/glean/bindings/jog/`.
+This includes the metrics construction factory and storage for metrics instances and their names and ids.
+
+Unfortunately, so that the metrics instances can be accessed by FFI,
+the Rust metrics instances created by the `jog` crate are stored within the `fog` crate.
+
+Speaking of FFI, the `jog` crate is using cbindgen to be accessible to C++.
+
+If necessary or pleasant, it is probably possible to do away with the C++ storage,
+moving the category set and metrics id map to Rust and moving information over FFI as needed.
+
+Test methods are run from `nsIFOG` (so we can use them in JS in xpcshell)
+to static `JOG::` functions.
+
+### Build Integration
+
+If JOG detects we're an artifact build (by checking `MOZ_ARTIFACT_BUILDS`),
+it generates `jogfile.json` and ensures it is placed in `GreD`
+(next to the `firefox` binary).
+
+`jogfile.json` includes only the metric and ping information necessary to register them at runtime.
+(It doesn't know about tags or descriptions, just the shapes and names of things)
+
+This file is read the first time JS tries to get a metric category from the
+`Glean` global or a ping from the `GleanPings` global.
+
+Yes, this is on the main thread. Yes, this is synchronous. Yes, this is file I/O.
+
+Since this is a developer build, we think this is worth it to support Artifact Builds.
+
+If we're wrong about this and there are additional conditions we should place JOG under,
+please [contact us][glean-channel].
+
+#### If things get weird, delete `objdir/dist/bin/jogfile.json`
+
+Sometimes, metrics or pings you've added may not appear when you run Firefox.
+For these and other odd cases, the solution is the same:
+delete `jogfile.json` from the `dist/bin` directory of your objdir, then try again.
+
+This shouldn't happen if you keep your artifact and non-artifact objdirs segregated
+(as is good practice).
+
+If, despite doing things properly you still see this or something else odd, then that's a bug.
+Please [file it in Toolkit :: Telemetry][file-bug]
+
+[artifact-build]: https://firefox-source-docs.mozilla.org/contributing/build/artifact_builds.html
+[glean-js]: https://mozilla.github.io/glean/book/user/adding-glean-to-your-project/javascript.html
+[impl-bug]: https://bugzilla.mozilla.org/show_bug.cgi?id=1698184
+[design-bug]: https://bugzilla.mozilla.org/show_bug.cgi?id=1662863
+[glean-channel]: https://chat.mozilla.org/#/room/#glean:mozilla.org
+[file-bug]: https://bugzilla.mozilla.org/enter_bug.cgi?assigned_to=nobody%40mozilla.org&bug_ignored=0&bug_severity=--&bug_status=NEW&bug_type=defect&cf_a11y_review_project_flag=---&cf_fx_iteration=---&cf_fx_points=---&cf_performance_impact=---&cf_status_firefox106=---&cf_status_firefox107=---&cf_status_firefox108=---&cf_status_firefox_esr102=---&cf_status_thunderbird_esr102=---&cf_status_thunderbird_esr91=---&cf_tracking_firefox106=---&cf_tracking_firefox107=---&cf_tracking_firefox108=---&cf_tracking_firefox_esr102=---&cf_tracking_firefox_relnote=---&cf_tracking_thunderbird_esr102=---&cf_tracking_thunderbird_esr91=---&cf_webcompat_priority=---&component=Telemetry&contenttypemethod=list&contenttypeselection=text%2Fplain&defined_groups=1&filed_via=standard_form&flag_type-203=X&flag_type-37=X&flag_type-41=X&flag_type-607=X&flag_type-721=X&flag_type-737=X&flag_type-787=X&flag_type-799=X&flag_type-800=X&flag_type-803=X&flag_type-846=X&flag_type-855=X&flag_type-864=X&flag_type-930=X&flag_type-936=X&flag_type-937=X&flag_type-952=X&form_name=enter_bug&maketemplate=Remember%20values%20as%20bookmarkable%20template&op_sys=Unspecified&priority=--&product=Toolkit&rep_platform=Unspecified&short_desc=Problem%20with%20JOG%3A%20&target_milestone=---&version=unspecified
diff --git a/toolkit/components/glean/docs/dev/local_glean.md b/toolkit/components/glean/docs/dev/local_glean.md
new file mode 100644
index 0000000000..046cf6e79c
--- /dev/null
+++ b/toolkit/components/glean/docs/dev/local_glean.md
@@ -0,0 +1,100 @@
+# Developing with a local Glean build
+
+FOG uses a release version of Glean, as published on [crates.io][cratesio-glean].
+
+For local development and try runs you can replace this Glean implementation with a local or remote version.
+
+1. To tell `mach` where to find your Glean, patch the [top-level `Cargo.toml`][cargo-toml]. E.g. like this:
+
+ ```toml
+ [patches.crates-io]
+ glean = { git = "https://github.com/myfork/glean", branch = "my-feature-branch" }
+ glean-core = { git = "https://github.com/myfork/glean", branch = "my-feature-branch" }
+ ```
+
+ Both crates are required to ensure they are in sync.
+
+ You can specify the exact code to use by `branch`, `tag` or `rev` (Git commit).
+ See the [cargo documentation for details][cargo-doc].
+
+ You can also use a path dependency:
+
+ ```toml
+ [patches.crates-io]
+ glean = { path = "../glean/glean-core/rlb" }
+ glean-core = { path = "../glean/glean-core" }
+ ```
+
+2. If the crate version in the patched repository is not
+ [semver]-compatible with the version required by the
+ `fog` and `fog_control` crates,
+ you need to change the version in the following files to match the ones in your
+ `glean` repo:
+
+ ```
+ toolkit/components/glean/Cargo.toml
+ toolkit/components/glean/api/Cargo.toml
+ ```
+
+ This tells FOG's crates that it needs your local Glean's version.
+
+3. Update the Cargo lockfile:
+
+ ```
+ cargo update -p glean
+ ```
+
+4. Mozilla's supply-chain management policy requires that third-party software
+ (which includes the Glean SDK because it is distributed as though it is third-party)
+ be audited and certified as safe.
+ Your local Glean SDK probably hasn't been vetted. If you try to vendor right now,
+ `./mach vendor rust` will complain something like:
+
+ ```
+ Vet error: There are some issues with your policy.audit-as-crates-io entries
+ ```
+
+ This is because your local Glean SDK is neither of a version nor is from a source that has been vetted.
+ To allow your local Glean crates to be treated as crates.io-sourced crates for vetting,
+ add the following sections to the top of `supply-chain/config.toml`:
+
+ ```toml
+ [policy.glean]
+ audit-as-crates-io = true
+
+ [policy.glean-core]
+ audit-as-crates-io = true
+ ```
+
+ If your local Glean is of a non-vetted version, you can update `glean` and
+ `glean-core`'s entries in `supply-chain/audits.toml` to the version you're using.
+ If you don't, `./mach vendor rust` will complain and not complete.
+
+ **Note:** Do not attempt to check these changes in.
+ These changes bypass supply chain defenses.
+ `@supply-chain-reviewers` may become cross as they `r-` your patch.
+
+5. Vendor the changed crates:
+
+ ```
+ ./mach vendor rust
+ ```
+
+ **Note:** If you're using a path dependency, `mach vendor rust` doesn't actually change files.
+ Instead it pulls the files directly from the location on disk you specify.
+
+6. Finally, build Firefox:
+
+ ```
+ ./mach build
+ ```
+
+A remote reference works for try runs as well,
+but a path dependency will not.
+
+Please ensure to not land a non-release version of Glean.
+
+[cratesio-glean]: https://crates.io/crates/glean
+[cargo-toml]: https://searchfox.org/mozilla-central/rev/f07a609a76136ef779c65185165ff5ac513cc172/Cargo.toml#76
+[cargo-doc]: https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#specifying-dependencies-from-git-repositories
+[semver]: https://semver.org/
diff --git a/toolkit/components/glean/docs/dev/new_metric_types.md b/toolkit/components/glean/docs/dev/new_metric_types.md
new file mode 100644
index 0000000000..a3bcdc8b54
--- /dev/null
+++ b/toolkit/components/glean/docs/dev/new_metric_types.md
@@ -0,0 +1,285 @@
+# Adding a New Metric Type
+
+This document covers how to add a new metric type to FOG.
+You should only have to do this if a new metric type is added to the
+[Glean SDK](https://mozilla.github.io/glean/book/user/metrics/index.html)
+and it is needed in Firefox Desktop.
+
+## IPC
+
+For detailed information about the IPC design,
+including a list of forbidden operations,
+please consult
+[the FOG IPC documentation](ipc.md).
+
+When adding a new metric type, the main IPC considerations are:
+* Which operations are forbidden because they are not commutative?
+ * Most `set`-style operations cannot be reconciled sensibly across multiple processes.
+* If there are non-forbidden operations,
+what partial representation will this metric have in non-main processes?
+Put another way, what shape of storage will this take up in the
+[IPC Payload](https://hg.mozilla.org/mozilla-central/file/tip/toolkit/components/glean/api/src/ipc.rs)?
+ * For example, Counters can aggregate all partial counts together to a single
+ "partial sum". So
+ [its representation](https://searchfox.org/mozilla-central/rev/803b368879fa332e8e2c1840bf1ec164f7ed2c32/toolkit/components/glean/api/src/ipc.rs#45)
+ in the IPC Payload is just a single number per Counter.
+ * In contrast, Timing Distributions' bucket arrangements are known only to the core,
+ so it can't combine sample counts in child processes.
+ Instead we record durations in the highest resolution (nanos),
+ and send a stream of high-precision samples across IPC.
+
+To implement IPC support in a metric type,
+we split FOG's Rust implementation of the metric into three pieces:
+1. An umbrella `enum` with the name `MetricTypeMetric`.
+ * It has a `Child` and a `Parent` variant.
+ * It is IPC-aware and is responsible for
+ * If on a non-parent-process,
+ either storing partial representations in the IPC Payload,
+ or logging errors if forbidden non-test APIs are called.
+ (Or panicking if test APIs are called.)
+ * If on the parent process, dispatching API calls on its inner Rust Language Binding metric.
+2. The parent-process implementation is supplied by
+ [the RLB](https://crates.io/crates/glean/).
+ * For testing, it stores the `MetricId` that identifies this particular metric in a cross-process fashion.
+ * For testing, it exposes a `child_metric()` function to create its `Child` equivalent.
+ * For testing and if it supports operations in a non-parent-process,
+ it exposes a `metric_id()` function to access the stored `MetricId`.
+3. The `MetricTypeIpc` is the non-parent-process implementation.
+ * If it does support operations in non-parent processes it stores the
+ `MetricId` that identifies this particular metric in a cross-process fashion.
+
+## Mirrors
+
+FOG can mirror Glean metrics to Telemetry probes via the
+[Glean Interface For Firefox Telemetry](../user/gifft.md).
+
+Can this metric type be mirrored?
+Should it be mirrored?
+
+If so, add an appropriate Telemetry probe for it to mirror to,
+documenting the compatibility in
+[the GIFFT docs](../user/gifft.md).
+
+### GIFFT Tests
+
+If you add a GIFFT mirror, don't forget to test that the mirror works.
+You should be able to do this by adding a task to
+[`toolkit/components/glean/tests/xpcshell/test_GIFFT.js`](https://hg.mozilla.org/mozilla-central/file/tip/toolkit/components/glean/tests/xpcshell/test_GIFFT.js).
+
+### GIFFT C++ State: Typical Locking and Shutdown
+
+Some metric types (`labeled_*`, `timespan`, `timing_distribution`)
+require holding state in C++ to make GIFFT work.
+Pings also hold state to support `testBeforeNextSubmit()`.
+If your new metric type requires state in C++,
+the current state-of-the-art is a `StaticDataMutex`-locked `UniquePtr` to a `nsTHashTable`.
+Access to the inner map is guarded by the lock and is controlled and lazily-instantiated through a single access function.
+[See Ping's `GetCallbackMapLock()`](https://searchfox.org/mozilla-central/source/toolkit/components/glean/bindings/private/Ping.cpp)
+for example.
+
+It is important to clear this state to avoid leaks.
+(See [bug 1752417](https://bugzilla.mozilla.org/show_bug.cgi?id=1752417).)
+However, instrumentation may call metrics APIs at any time.
+
+Therefore, GIFFT explicitly stops supporting these state-requiring operations after the
+[`AppShutdownTelemetry` shutdown phase](https://searchfox.org/mozilla-central/source/xpcom/base/ShutdownPhase.h).
+This is because during the next phase (`XPCOMWillShutdown`) we clear the state.
+
+## Rust
+
+FOG uses the Rust Language Binding APIs (the `glean` crate) with a layer of IPC on top.
+
+The IPC additions and glean-core trait implementations are in the
+[`private` module of the `fog` crate](https://hg.mozilla.org/mozilla-central/file/tip/toolkit/components/glean/api/src/metrics).
+
+Each metric type gets its own file, mimicking the structure in
+[`glean_core`](https://github.com/mozilla/glean/tree/main/glean-core/src/metrics)
+and [`glean`](https://github.com/mozilla/glean/tree/main/glean-core/rlb/src/private).
+Unless, of course, that metric is a labeled metric type.
+Then the sub metric type gets its own file,
+and you need to add "Labeledness" to it by implementing
+`Sealed` for your new type following the pattern in
+[`api/src/private/labeled.rs`](https://hg.mozilla.org/mozilla-central/file/tip/toolkit/components/glean/api/src/private/labeled.rs).
+
+Every method on the metric type is public for now,
+including test methods,
+and is at least all the methods exposed via the
+[metric traits](https://github.com/mozilla/glean/tree/main/glean-core/src/traits).
+
+To support IPC and the MLA FFI (see below)
+we identify metric instances by MetricId and store them in maps in
+[the `__glean_metric_maps` mod of `metrics.rs`](https://hg.mozilla.org/mozilla-central/toolkit/components/glean/api/src/metrics.rs).
+This work is done by the `rust.py` and `rust(_pings).jinja2` extensions to `glean_parser` found
+[in the `build_scripts/glean_parser_ext/` folder](https://searchfox.org/mozilla-central/source/toolkit/components/glean/build_scripts/glean_parser_ext).
+
+You shouldn't have to edit these files for new metric types,
+as the original modifications to `glean_parser` for this type should already be generating correct code.
+
+### Rust Tests
+
+You should be able to smoke test the basic functionality in Rust unit tests.
+You can do this within the metric type implementation file directly.
+
+## C++ and JS
+
+The C++ and JS APIs are implemented [atop the Rust API](code_organization.md).
+We treat them both together since, though they're different languages,
+they're both implemented in C++ and share much of their implementation.
+
+The overall design is to build the C++ API atop the Multi-Language Architecture's
+(MLA's) FFI, then build the JS API atop the C++ API.
+This allows features like the
+[Glean Interface For Firefox Telemetry (GIFFT)](../user/gifft.md)
+that target only C++ and JS to be more simply implemented in the C++ layer.
+Exceptions to this (where the JS uses the FFI directly) are discouraged.
+
+Each metric type has six pieces you'll need to cover:
+
+### 1. MLA FFI
+
+- Using our convenient macros,
+ define the metric type's Multi-Language Architecture FFI layer above the Rust API in
+ [`api/src/ffi/`](https://hg.mozilla.org/mozilla-central/file/tip/toolkit/components/glean/api/src/ffi/).
+
+### 2. C++ Impl
+
+- Implement a type called `XMetric` (e.g. `CounterMetric`) in `mozilla::glean::impl` in
+ [`bindings/private/`](https://hg.mozilla.org/mozilla-central/file/tip/toolkit/components/glean/bindings/private/).
+ - Its methods should be named the same as the ones in the Rust API, transformed to `CamelCase`.
+ - They should all be public.
+ - Multiplex the FFI's `test_have` and `test_get` functions into a single
+ `TestGetValue` function that returns a
+ `mozilla::Maybe` wrapping the C++ type that best fits the metric type.
+- Include the new metric type in
+ [`bindings/MetricTypes.h`](https://hg.mozilla.org/mozilla-central/file/tip/toolkit/components/glean/bindings/MetricTypes.h).
+- Include the new files in
+ [`moz.build`](https://hg.mozilla.org/mozilla-central/file/tip/toolkit/components/glean/moz.build).
+ The header file should be added to `EXPORTS.mozilla.glean.bindings` and the
+ `.cpp` file should be added to `UNIFIED_SOURCES`.
+
+### 3. IDL
+
+- Duplicate the public API (including its docs) to
+ {searchfox}`dom/webidl/GleanMetrics.webidl`
+ with the name `GleanX` (e.g. `GleanCounter`).
+ - Inherit from `GleanMetric`.
+ - The naming style for methods here is `lowerCamelCase`.
+ - If the metric method is a reserved word, prepend it with a `_`.
+ - Web IDL bindings use
+ [their own mapping for types](/dom/webIdlBindings/index.md).
+ If you choose ones that most closely resemble the C++ types,
+ you'll make your life easier.
+- Add a new mapping in `dom/bindings/Bindings.conf`:
+ ```idl
+ 'GleanX': {
+ 'nativeType': 'mozilla::glean::GleanX',
+ 'headerFile': 'mozilla/glean/bindings/X.h',
+ },
+ ```
+ - If you don't, you will get a build error complaining `fatal error: 'mozilla/dom/GleanX.h' file not found`.
+
+### 4. JS Impl
+
+- Implement the `GleanX` (e.g. `GleanCounter`) type
+ in the same header and `.cpp` as `XMetric` in
+ {searchfox}`toolkit/components/glean/bindings/private/`
+ - It should own an instance of and delegate method implementations to `XMetric`.
+ - In the definition of `GleanX`, member identifiers are back to
+ `CamelCase`.
+ - Test-only methods can throw `DataError` on failure.
+ - Review the [Web IDL Bindings documentation](/dom/webIdlBindings/index.html)
+ for help with optional, nullable, and non-primitive types.
+
+### 6. Tests
+
+Two languages means two test suites.
+
+- Add a never-expiring test-only metric of your type to
+ [`test_metrics.yaml`](https://hg.mozilla.org/mozilla-central/file/tip/toolkit/components/glean/tests/test_metrics.yaml).
+ - Feel free to be clever with the name,
+ but be sure to make clear that it is test-only.
+- **C++ Tests (GTest)** - Add a small test case to
+ [`gtest/TestFog.cpp`](https://hg.mozilla.org/mozilla-central/file/tip/toolkit/components/glean/tests/gtest/TestFog.cpp).
+ - For more details, peruse the [testing docs](testing.md).
+- **JS Tests (xpcshell)** - Add a small test case to
+ [`xpcshell/test_Glean.js`](https://hg.mozilla.org/mozilla-central/file/tip/toolkit/components/glean/tests/xpcshell/test_Glean.js)
+ and
+ [`xpcshell/test_JOG.js`](https://hg.mozilla.org/mozilla-central/file/tip/toolkit/components/glean/tests/xpcshell/test_JOG.js).
+ If your metric type has supported IPC operations, also add cases to the `IPC` variants of these test files.
+ - For more details, peruse the [testing docs](testing.md).
+
+### 7. API Documentation
+
+Metric API Documentation is centralized in
+[the Glean SDK Book](https://mozilla.github.io/glean/book/user/metrics/index.html).
+
+You will need to craft a Pull Request against
+[the SDK](https://github.com/mozilla/glean/)
+adding a C++ and JS example to the specific metric type's API docs.
+
+Add a notice at the top of both examples that these APIs are only available in Firefox Desktop:
+````md
+<div data-lang="C++" class="tab">
+
+> **Note**: C++ APIs are only available in Firefox Desktop.
+
+```cpp
+#include "mozilla/glean/GleanMetrics.h"
+
+mozilla::glean::category_name::metric_name.Api(args);
+```
+
+There are test APIs available too:
+
+```cpp
+#include "mozilla/glean/GleanMetrics.h"
+
+ASSERT_EQ(value, mozilla::glean::category_name::metric_name.TestGetValue().ref());
+```
+</div>
+
+// and again for <div data-lang="JS">
+````
+
+If you're lucky, the Rust API will have already been added.
+Otherwise you'll need to write an example for that one too.
+
+### 8. Labeled metrics (if necessary)
+
+If your new metric type is Labeled, you have more work to do.
+I'm assuming you've already implemented the non-labeled sub metric type following the steps above.
+Now you must add "Labeledness" to it.
+
+There are four pieces to this:
+
+#### FFI
+
+- To add the writeable storage Rust will use to store the dynamically-generated sub metric instances,
+ add your sub metric type's map as a list item in the `submetric_maps` `mod` of
+ [`rust.jinja2`](https://hg.mozilla.org/mozilla-central/file/tip/toolkit/components/glean/build_scripts/glean_parser_ext/templates/rust.jinja2).
+- Following the pattern of the others, add a `fog_{your labeled metric name here}_get()` FFI API to
+ `api/src/ffi/mod.rs`.
+ This is what C++ and JS will use to allocate and retrieve sub metric instances by id.
+
+#### C++
+
+- Following the pattern of the others, add a template specialiation for `Labeled<YourSubMetric>::Get` to
+ [`bindings/private/Labeled.cpp`](https://hg.mozilla.org/mozilla-central/file/tip/toolkit/components/glean/bindings/private/Labeled.cpp).
+ This will ensure C++ consumers can fetch or create sub metric instances.
+
+#### JS
+
+- Already handled for you since the JS types all inherit from `GleanMetric`
+ and the JS template knows to add your new type to `NewSubMetricFromIds(...)`
+ (see `GleanLabeled::NamedGetter` if you're curious).
+
+#### Tests
+
+- The labeled variant will need tests the same as Step #6.
+ A tip: be sure to test two labels with different values.
+
+## Python Tests
+
+We have a suite of tests for ensuring code generation generates appropriate code.
+You should add a metric to [that suite](testing.md) for your new metric type.
+You will need to regenerate the expected files.
diff --git a/toolkit/components/glean/docs/dev/preferences.md b/toolkit/components/glean/docs/dev/preferences.md
new file mode 100644
index 0000000000..732e25c5c9
--- /dev/null
+++ b/toolkit/components/glean/docs/dev/preferences.md
@@ -0,0 +1,105 @@
+# Preferences and Defines
+
+## User Preferences
+
+`datareporting.healthreport.uploadEnabled`
+
+This determines whether the Glean SDK is enabled.
+It can be controlled by users via `about:preferences#privacy`.
+If this is set to false from true, we send a
+["deletion-request" ping](https://mozilla.github.io/glean/book/user/pings/deletion_request.html)
+and no data collections will be persisted or reported from that point.
+
+## Test-only Preferences
+
+`telemetry.fog.test.localhost_port`
+
+If set to a value `port` which is greater than 0, pings will be sent to
+`http://localhost:port` instead of `https://incoming.telemetry.mozilla.org`.
+If set to a value `port` which is less than 0, FOG will:
+1) Tell Glean that upload is enabled, even if it isn't.
+2) Take all pings scheduled for upload and drop them on the floor,
+ telling the Glean SDK that it was sent successfully.
+
+This is how you emulate "recording enabled but upload disabled"
+like developer builds have in Firefox Telemetry.
+When switching from `port < 0` to `port >= 0`,
+Glean will be told (if just temporarily) that upload is disabled.
+This clears the stores of recorded-but-not-reported data.
+Defaults to 0.
+
+`telemetry.fog.test.activity_limit`
+`telemetry.fog.test.inactivity_limit`
+
+This pair of prefs control the length of time of activity before inactivity
+(or vice versa)
+needed before FOG informs the SDK's Client Activity API that the client was (in)active.
+Present to allow testing without figuring out how to mock Rust's clock.
+Their values are integer seconds.
+Defaults to 120 (activity), 1200 (inactivity).
+
+## Internal Preferences
+
+`telemetry.fog.artifact_build`
+
+Read-only. This pref is `true` only if `MOZ_ARTIFACT_BUILDS` was set during configure.
+If true, [JOG](./jog) is enabled so that artifact builds will exhibit changes to their Glean metrics.
+
+## Defines
+
+`MOZ_AUTOMATION`
+
+If set, and `GLEAN_SOURCE_TAGS` isn't set, FOG will set a
+[Glean source tag](https://mozilla.github.io/glean/book/reference/debug/sourceTags.html)
+of `automation`.
+
+If `GLEAN_SOURCE_TAGS` is set, the `automation` source tag will not be added automatically.
+
+If not set, any tags set by `GLEAN_SOURCE_TAGS` will be present.
+
+`MOZ_GLEAN_ANDROID`
+
+If set, the Glean SDK is assumed to be managed by something other than FOG, meaning:
+* [GIFFT][gifft] is disabled.
+* FOG doesn't initialize Glean
+* FOG doesn't relay (in)activity or experiment annotations to Glean
+
+`MOZILLA_OFFICIAL`
+
+If unset, we set a `glean_disable_upload` Rust feature in
+`gkrust` and `gkrust-shared` which is forwarded to `fog_control` as `disable_upload`.
+This feature defaults FOG to an "upload disabled"
+mode where collection on the client proceeds as normal but no ping is sent.
+This mode can be overridden at runtime in two ways:
+* If the ping has a
+ [Debug Tag](https://mozilla.github.io/glean/book/user/debugging/index.html)
+ then it is sent so that it may be inspected in the
+ [Glean Debug Ping Viewer](https://debug-ping-preview.firebaseapp.com/).
+* If the preference `telemetry.fog.test.localhost_port` is set to a value greater than 0,
+ then pings are sent to a server operating locally at that port
+ (even if the ping has a Debug Tag), to enable testing.
+
+`MOZILLA_OFFICIAL` tends to be set on most builds released to users,
+including builds distributed by Linux distributions.
+It tends to not be set on local developer builds.
+See [bug 1680025](https://bugzilla.mozilla.org/show_bug.cgi?id=1680025) for details.
+
+`MOZ_ARTIFACT_BUILDS`
+
+If `MOZ_ARTIFACT_BUILDS` is set in the build config,
+[JOG](./jog) will generate a file for the runtime-registration of metrics and pings.
+This is to support [Artifact Builds](/contributing/build/artifact_builds).
+
+See also `telemetry.fog.artifact_build`.
+
+`OS_TARGET`
+
+If not set to `'Android'` we set a `glean_million_queue` Rust feature
+([see gkrust-features.mozbuild][gkrust-features])
+which, when passed to the Glean SDK,
+opts us into a preinit queue that doesn't discard tasks until there are 10^6 of them.
+
+See [bug 1797494](https://bugzilla.mozilla.org/show_bug.cgi?id=1797494) for details.
+
+[gkrust-features]: https://searchfox.org/mozilla-central/source/toolkit/library/rust/gkrust-features.mozbuild
+[gifft]: ../user/gifft
diff --git a/toolkit/components/glean/docs/dev/storage.md b/toolkit/components/glean/docs/dev/storage.md
new file mode 100644
index 0000000000..00464c6173
--- /dev/null
+++ b/toolkit/components/glean/docs/dev/storage.md
@@ -0,0 +1,14 @@
+# Storage
+
+Both FOG and the Glean SDK require some storage in the
+[Firefox Profile Directory](https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Multiple_profiles).
+
+## FOG
+
+At present FOG's storage is limited to its [preferences](preferences.md).
+
+## Glean SDK
+
+The Glean SDK stores things in the
+[Glean Data Directory](https://mozilla.github.io/glean/book/dev/core/internal/directory-structure.html)
+which can be found at `<profile_dir>/datareporting/glean`.
diff --git a/toolkit/components/glean/docs/dev/style_guide.md b/toolkit/components/glean/docs/dev/style_guide.md
new file mode 100644
index 0000000000..d96766da9f
--- /dev/null
+++ b/toolkit/components/glean/docs/dev/style_guide.md
@@ -0,0 +1,49 @@
+# FOG Documentation Style Guide
+
+FOG's Documentation is written in Markdown.
+You can find its source at `toolkit/components/glean/docs`.
+
+## Line breaks
+
+We will use [semantic linefeeds]:
+* Break anywhere before 80-100 characters
+* Break after any punctuation when it makes sense
+* Break before or after any markdown when it makes sense
+
+**Tip:** To keep lines narrow, use markdown's [reference link]
+feature whenever it makes sense (or all the time. Up to you.).
+
+## Linking to other documentation
+
+Linking to other external documentation is [easy][reference link].
+Linking to other pieces of documentation in the source docs requires a
+link to the source file in the sphinx tree.
+
+Links can be relative e.g. to link to the [preferences] docs:
+
+```md
+[preferences](preferences.md)
+```
+
+Or they can be absolute e.g. to link to the [Telemetry] docs:
+```md
+[Telemetry](/toolkit/components/telemetry/index.rst)
+```
+
+Sphinx will automagically transform that to an
+appropriately-base-url'd url with a `.html` suffix.
+
+
+## Use of "Main" versus "Parent" for processes
+
+When talking about Glean, it is helpeful to distinguish when we are on the main
+process specifically. This is because there is only one main process, and there is only one Glean.
+However, "parent" is a relative term, and it may refer to a process
+that is the parent of another process, but is not itself the main process.
+Prefer use of "main" when it is accurate.
+
+[semantic linefeeds]: https://rhodesmill.org/brandon/2012/one-sentence-per-line/
+[reference link]: https://spec.commonmark.org/0.29/#reference-link
+[Telemetry]: /toolkit/components/telemetry/index.rst
+[#firefox-source-docs:mozilla.org]: https://chat.mozilla.org/#/room/#firefox-source-docs:mozilla.org
+[bug 1621950]: https://bugzilla.mozilla.org/show_bug.cgi?id=1621950
diff --git a/toolkit/components/glean/docs/dev/testing.md b/toolkit/components/glean/docs/dev/testing.md
new file mode 100644
index 0000000000..fe6c3e0fe7
--- /dev/null
+++ b/toolkit/components/glean/docs/dev/testing.md
@@ -0,0 +1,240 @@
+# Testing
+
+```{admonition} This documentation is about testing FOG itself
+This document contains information about how FOG tests itself,
+how to add new tests, how and what to log, and stuff like that.
+If you're interested in learning how to test instrumentation you added,
+you'll want to read
+[the instrumetnation testing docs](../user/instrumentation_tests) instead.
+```
+
+Given the multiple API languages, processes, and dependencies,
+testing FOG is a matter of choosing the right tool for the situation.
+
+## One Big Command
+
+To run all the things, here's the tl;dr:
+
+`./mach build && ./mach lint -W -w -o --fix
+&& ./mach rusttests && ./mach gtest "FOG*"
+&& python3 ./mach python-test toolkit/components/glean/tests/pytest
+&& ./mach test toolkit/components/glean/tests/xpcshell
+&& ./mach telemetry-tests-client toolkit/components/telemetry/tests/marionette/tests/client/test_fog* --gecko-log "-"
+&& ./mach test toolkit/components/glean/tests/browser
+`
+
+## Logging
+
+An often-overlooked first line of testing is "what do the logs say?".
+To turn on logging for FOG, use any of the following:
+* Run Firefox with `RUST_LOG="fog_control,fog,glean_core"`.
+ * On some platforms this will use terminal colours to indicate log level.
+* Run Firefox with `MOZ_LOG="timestamp,sync,glean::*:5,fog::*:5,fog_control::*:5,glean_core::*:5"`.
+* Set the following prefs:
+ * `logging.config.timestamp` to `true`
+ * `logging.config.sync` to `true`
+ * `logging.fog_control::*` to `5`
+ * `logging.fog::*` to `5`
+ * `logging.glean::*` to `5`
+ * `logging.glean_core::*` to `5`
+ * `logging.config.clear_on_startup` to `false` (or all these prefs will be cleared on startup)
+
+For more information on logging in Gecko, see the
+[Gecko Logging docs](/xpcom/logging).
+
+User-destined logs (of the "You did something wrong" variety) might output to the
+[Browser Console](/devtools-user/browser_console/index)
+if they originate in JS land. Open via
+<kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>J</kbd>, or
+<kbd>Cmd</kbd>+<kbd>Shift</kbd>+<kbd>J</kbd>.
+
+```{admonition} Note
+At present, Rust logging in non-main processes just doesn't work.
+```
+
+### What to log, and to where?
+
+FOG covers a lot a ground (languages, layers):
+where you are determines what logging you have available.
+
+Here are some common situtations for logging:
+
+#### JS to C++
+
+If your logging is aimed at the user using the JS API
+(e.g. because the type provided isn't convertable to the necessary C++ type)
+then use the Browser Console via
+[FOG's Common's `LogToBrowserConsole`](https://searchfox.org/mozilla-central/rev/d107bc8aeadcc816ba85cb21c1a6a1aac1d4ef9f/toolkit/components/glean/bindings/private/Common.cpp#19).
+
+#### C++
+
+If you are in C++ and didn't come from JS, use `MOZ_LOG` with module `fog`.
+
+#### Rust
+
+Use the logging macros from `log`, e.g. `log::info!` or `log::error!`.
+Remember that, no matter the log level, `log::debug!` and `log::trace!`
+[will not appear in non-debug builds](/testing-rust-code/index.md#gecko-logging)
+
+If you are logging due to a situation caused by and fixable by a developer using the API,
+use `log::error!(...)`. Otherwise, use a quieter level.
+
+## `about:glean`
+
+`about:glean` is a page in a running Firefox that allows you to
+[debug the Glean SDK](https://mozilla.github.io/glean/book/user/debugging/index.html)
+in Firefox Desktop.
+It does this through the displayed user interface (just follow the instructions).
+
+## Linting
+
+To keep in accordance with Mozilla's various and several Coding Styles,
+we rely on `mach lint`.
+
+To lint the code in the "usual" way, automatically fixing where possible, run:
+`./mach lint -W -w -o --fix`
+
+This should keep you from checking in code that will automatically be backed out.
+
+## Rust
+
+Not all of our Rust code can be tested in a single fashion, unfortunately.
+
+### Using `rusttests` (Treeherder symbol `Br` (a build task))
+
+If the crate you're testing has no Gecko symbols you can write standard
+[Rust tests](https://doc.rust-lang.org/book/ch11-01-writing-tests.html).
+
+This supports both unit tests
+(inline in the file under test) and integration tests
+(in the `tests/` folder in the crate root).
+Metric type tests are currently written as unit tests inline in the file,
+as they require access to the metric ID, which should only be exposed in tests.
+
+To run FOG's `rusttests` suite use `mach rusttests`
+
+If the crate uses only a few Gecko symbols, they may use the
+`with_gecko` feature to conditionally use them.
+This allows the crate to test its non-Gecko-adjacent code using Rust tests.
+(You will need to cover the Gecko-adjacent code via another means.)
+
+**Note:** Some FOG rusttests panic on purpose. They print stack traces to stdout.
+If the rusttests fail and you see a stack trace,
+double-check it isn't from a purposefully-panicking test.
+
+**Note:** If a test fails, it is very likely they'll poison the test lock.
+This will cause all subsequent tests that attempt to take the test lock
+(which is all of them)
+to also fail due to `PoisonError`s. They can be safely ignored.
+
+### Using `gtest` (Treeherder symbol `GTest` (a build task))
+
+Because Gecko symbols aren't built for the
+`rusttests` build,
+any test that is written for code that uses Gecko symbols should be written as a
+[`gtest`](https://github.com/google/googletest)
+in `toolkit/components/glean/tests/gtest/`.
+You can write the actual test code in Rust.
+It needs to be accompanied by a C++ GTest that calls a C FFI-exported Rust function.
+See [Testing & Debugging Rust Code](/testing-rust-code/index.md) for more.
+See [`toolkit/components/glean/tests/gtest/TestFog.cpp`](https://searchfox.org/mozilla-central/source/toolkit/components/glean/tests/gtest/TestFog.cpp)
+and [`toolkit/components/glean/tests/gtest/test.rs`](https://searchfox.org/mozilla-central/source/toolkit/components/glean/tests/gtest/test.rs)
+for an example.
+
+By necessity these can only be integration tests against the compiled crate.
+
+**Note:** When adding a new test file, don't forget to add it to
+`toolkit/components/glean/tests/gtest/moz.build` and use the
+`FOG` prefix in your test names
+(e.g. `TEST(FOG, YourTestName) { ... }`).
+
+To run FOG's Rust `gtest` suite use `mach gtest FOG.*`
+
+## Python (Treeherder symbol `py3(fp)` aka `source-test-python-fog`)
+
+The [Glean Parser](https://github.com/mozilla/glean_parser/)
+has been augmented to generate FOG-specific APIs for Glean metrics.
+This augmentation is tested by running:
+
+`mach test toolkit/components/glean/tests/pytest`
+
+These tests require Python 3+.
+If your default Python is Python 2, you may need to instead run:
+
+`python3 mach python-test toolkit/components/glean/tests/pytest`
+
+These tests check the code generator output against known good file contents.
+If you change the code generator the files will need an update.
+Run the test suite with the `UPDATE_EXPECT` environment variable set to do that automatically:
+
+`UPDATE_EXPECT=1 mach test toolkit/components/glean/tests/pytest`
+
+## C++ (Treeherder symbol `GTest` (a build task))
+
+To test the C++ parts of FOG's implementation
+(like metric types)
+you should use `gtest`.
+FOG's `gtest` tests are in
+[`gtest/`](https://hg.mozilla.org/mozilla-central/file/tip/toolkit/components/glean/tests/gtest/).
+
+You can either add a test case to an existing file or add a new file.
+If you add a new file, remember to add it to the
+[`moz.build`](https://hg.mozilla.org/mozilla-central/file/tip/toolkit/components/glean/tests/gtest/moz.build))
+or the test runner won't be able to find it.
+
+All tests should start with `FOG` so that all tests are run with
+`./mach gtest FOG*`.
+
+## JS (Treeherder symbol `X(Xn)` for some number `n`)
+
+To test the JS parts of FOG's implementation
+(like metric types)
+you should use `xpcshell`.
+FOG's `xpcshell` tests are in
+[`xpcshell/`](https://hg.mozilla.org/mozilla-central/file/tip/toolkit/components/glean/tests/xpcshell).
+
+You can either add a test case to an existing file or add a new file.
+If you add a new file, remember to add it to the
+[`xpcshell.ini`](https://hg.mozilla.org/mozilla-central/file/tip/toolkit/components/glean/tests/xpcshell/xpcshell.ini)
+or the test runner will not be able to find it.
+
+To run FOG's JS tests, run:
+`./mach test toolkit/components/glean/tests/xpcshell`
+
+## Non-content-process multiprocess (Browser Chrome Mochitests with Treeherder symbol `M(bcN)` for some number `N`)
+
+To test e.g. the GPU process support you need a full Firefox browser:
+xpcshell doesn't have the flexibility.
+To test that and have access to privileged JS (i.e. `Glean` and `FOG` APIs),
+we use browser-chrome-flavoured mochitests you can find in
+[`browser/`](https://hg.mozilla.org/mozilla-central/file/tip/toolkit/components/glean/tests/browser).
+
+If you need to add a new test file, remember to add it to the
+[`browser.ini`](https://hg.mozilla.org/mozilla-central/file/tip/toolkit/components/glean/tests/browser/browser.ini)
+manifest, or the test runner will not be able to find it.
+
+To run FOG's browser chrome tests, run:
+`./mach test toolkit/components/glean/tests/browser`
+
+## Integration (Marionette, borrowing `telemetry-tests-client` Treeherder symbol `tt(c)`)
+
+To test pings (See [bug 1681742](https://bugzilla.mozilla.org/show_bug.cgi?id=1681742))
+or anything that requires one or more full browsers running,
+we use the `telemetry-tests-client` suite in
+[`toolkit/components/telemetry/tests/marionette/tests/client/`](https://hg.mozilla.org/mozilla-central/file/tip/toolkit/components/telemetry/tests/marionette/tests/client/).
+
+For more information on this suite, look to
+[Firefox Telemetry's Test Documentation](https://firefox-source-docs.mozilla.org/toolkit/components/telemetry/internals/tests.html#integration-tests-telemetry-tests-client-and-telemetry-integration-tests)
+and
+[Marionette's Documentation](/testing/marionette/Testing.md).
+
+To run these integration tests, run:
+`./mach telemetry-tests-client toolkit/components/telemetry/tests/marionette/tests/client/`
+
+To capture the Firefox under test's logs, use the `--gecko-log` parameter.
+For example, to echo to stdout:
+`./mach telemetry-tests-client toolkit/components/telemetry/tests/marionette/tests/client/test_fog* --gecko-log "-"`
+
+**Note:** Running the `tt(c)` suite in this way ignored skip directives in the manifest.
+This means that you might run tests that are not expected to succeed on your platform.
+Check `toolkit/components/telemetry/tests/marionette/tests/client/manifest.ini` for details.
diff --git a/toolkit/components/glean/docs/dev/updating_parser.md b/toolkit/components/glean/docs/dev/updating_parser.md
new file mode 100644
index 0000000000..4609f76a80
--- /dev/null
+++ b/toolkit/components/glean/docs/dev/updating_parser.md
@@ -0,0 +1,52 @@
+# Updating glean_parser
+
+Project FOG uses the `glean_parser` to generate code from metric definitions.
+It depends on [glean-parser] from pypi.org
+
+[glean-parser]: https://pypi.org/project/glean-parser/
+
+To update the in-tree glean-parser change the version in `third_party/python/requirements.in`,
+then run
+
+```
+./mach vendor python
+```
+
+```{note}
+**Important**: the glean_parser and all of its dependencies must support Python 3.5, as discussed here.
+This is the minimum version supported by mach and installed on the CI images for running tests.
+This is enforced by the version ranges declared in the Python installation manifest.
+```
+
+## Version mismatch of the Python dependencies
+
+The logic for handling version mismatches is very similar to the one for the Rust crates.
+See [Updating the Glean SDK](updating_sdk.md) for details.
+However, updating Python packages also requires to think about Python 3.5 (and Python 2, still) compatibility.
+
+## Keeping versions in sync
+
+The Glean SDK and `glean_parser` are currently released as separate projects.
+However each Glean SDK release requires a specific `glean_parser` version.
+When updating one or the other ensure versions stay compatible.
+You can find the currently used `glean_parser` version in the Glean SDK source tree, e.g. in [sdk_generator.sh].
+
+[sdk_generator.sh]: https://github.com/mozilla/glean/blob/main/glean-core/ios/sdk_generator.sh#L28
+
+## Using a local `glean_parser` development version
+
+To test out a new `glean_parser` in mozilla-central follow these steps:
+
+1. Remove `glean_parser` from the user-wide virtual environment.
+ This can be found in a path like `~/.mozbuild/srcdirs/gecko-f5e3b9c6ded5/_virtualenvs/mach/lib/python3.10/site-packages/glean_parser`
+ Note that the `gecko-f5e3b9c6ded5` part will be different depending on your local checkout.
+ Remove all directories and files mentioning `glean_parser`
+2. Remove `glean_parser` from the build virtual enviromment.
+ This can be found in `$MOZ_OBJDIR/_virtualenvs/common/lib/python3.6/site-packages/glean_parser`.
+ Note that `$MOZ_OBJDIR` depends on your local mozconfig configuration.
+ Remove all directories and files mentioning `glean_parser`
+3. Copy the local `glean_parser` checkout into `third_party/python/glean_parser`.
+ E.g. `cp ~/code/glean_parser $GECKO/third_party/python/glean_parser`.
+
+You should now be able to build `mozilla-central` and it will use the modified `glean_parser`.
+You can make further edits in `$GECKO/third_party/python/glean_parser`.
diff --git a/toolkit/components/glean/docs/dev/updating_sdk.md b/toolkit/components/glean/docs/dev/updating_sdk.md
new file mode 100644
index 0000000000..fafec08d39
--- /dev/null
+++ b/toolkit/components/glean/docs/dev/updating_sdk.md
@@ -0,0 +1,48 @@
+# Updating the Glean SDK
+
+Project FOG uses the Glean SDK published as the [`glean`][glean-crate]
+and [`glean-core`][glean-core] crates on crates.io.
+
+[glean-crate]: https://crates.io/crates/glean
+[glean-core]: https://crates.io/crates/glean-core
+
+These two crates' versions are included in several places in mozilla-central.
+To update them all, you should use the command
+`mach update-glean <version, like "54.1.0">`.
+
+This is a semi-manual process.
+Please pay attention to the output of `mach update-glean` for instructions,
+and follow them closely.
+
+## Version mismatches of Rust dependencies
+
+Other crates that are already vendored might require a different version of the same dependencies that the Glean SDK requires.
+The general strategy for Rust dependencies is to keep one single version of the dependency in-tree
+(see [comment #8 in bug 1591555](https://bugzilla.mozilla.org/show_bug.cgi?id=1591555#c8)).
+This might be hard to do in reality since some dependencies might require tweaks in order to work.
+The following strategy can be followed to decide on version mismatches:
+
+* If the versions only **differ by the patch version**, Cargo will keep the vendored version,
+ unless some other dependency pinned specific patch versions;
+ assuming it doesn’t break the Glean SDK;
+ if it does, follow the next steps;
+* If the version of the **vendored dependency is newer** (greater major or minor version) than the version required by the Glean SDK,
+ [file a bug in the Glean SDK component][glean-bug] to get Glean to require the same version;
+ * You will have to abandon updating the Glean SDK to this version.
+ You will have to wait for Glean SDK to update its dependency and for a new Glean SDK release.
+ Then you will have to update to that new Glean SDK version.
+* If the version of the **vendored dependency is older** (lower major or minor version), consider updating the vendored version to the newer one;
+ seek review from the person who vendored that dependency in the first place;
+ if that is not possible or breaks mozilla-central build, then consider keeping both versions vendored in-tree; please note that this option will probably only be approved for small crates,
+ and will require updating the `TOLERATED_DUPES` list in `mach vendor`
+ (instructions are provided as you go).
+
+## Keeping versions in sync
+
+The Glean SDK and `glean_parser` are currently released as separate projects.
+However each Glean SDK release requires a specific `glean_parser` version.
+When updating one or the other ensure versions stay compatible.
+You can find the currently used `glean_parser` version in the Glean SDK source tree, e.g. in [sdk_generator.sh].
+
+[sdk_generator.sh]: https://github.com/mozilla/glean/blob/main/glean-core/ios/sdk_generator.sh#L28
+[glean-bug]: https://bugzilla.mozilla.org/enter_bug.cgi?product=Data+Platform+and+Tools&component=Glean%3A+SDK&priority=P3&status_whiteboard=%5Btelemetry%3Aglean-rs%3Am%3F%5D
diff --git a/toolkit/components/glean/docs/index.md b/toolkit/components/glean/docs/index.md
new file mode 100644
index 0000000000..c9d8f42eb4
--- /dev/null
+++ b/toolkit/components/glean/docs/index.md
@@ -0,0 +1,30 @@
+# Firefox on Glean (FOG)
+
+Firefox on Glean (FOG) is the name of the layer that integrates the
+[Glean SDK][glean-sdk] into
+[Firefox Desktop](https://www.firefox.com/).
+
+The [Glean SDK][glean-sdk]
+is a data collection library built by Mozilla for use in its products.
+Like [Telemetry][telemetry], it can be used to
+(in accordance with our [Privacy Policy][privacy-policy])
+send anonymous usage statistics to Mozilla in order to make better decisions.
+
+If you have any questions,
+please reach out to the team on
+[#glean:mozilla.org][glean-matrix].
+
+```{toctree}
+:titlesonly:
+:maxdepth: 2
+:glob:
+
+user/index
+dev/index
+```
+
+[telemetry]: ../telemetry/index
+[glean-sdk]: https://github.com/mozilla/glean/
+[book-of-glean]: https://mozilla.github.io/glean/book/index.html
+[privacy-policy]: https://www.mozilla.org/privacy/
+[glean-matrix]: https://chat.mozilla.org/#/room/#glean:mozilla.org
diff --git a/toolkit/components/glean/docs/user/geckoview_streaming_migration.md b/toolkit/components/glean/docs/user/geckoview_streaming_migration.md
new file mode 100644
index 0000000000..6dede3e90e
--- /dev/null
+++ b/toolkit/components/glean/docs/user/geckoview_streaming_migration.md
@@ -0,0 +1,262 @@
+# Migrating Telemetry Collected via Geckoview Streaming to Glean
+
+With Geckoview Streaming (GVST) having been deprecated,
+this is a guide to migrating collections to [Glean][book-of-glean]
+via [Firefox on Glean](../index.md).
+
+```{contents}
+```
+
+## Before we Begin
+
+You should familiarize yourself with the guide on
+[Adding New Metrics to Firefox Desktop](./new_definitions_file.md).
+
+You should also read through the guide for
+[Migrating Metrics from Legacy Telemetry to Glean](./migration.md).
+
+This guide assumes some basic familiarity with the above.
+The [Glean book][book-of-glean] has a full API reference, as well.
+
+## Process
+
+There are 3 main steps:
+
+1. Move the metric definition and make necessary updates
+2. Update the call to use the Glean API
+3. Update the tests to use the Glean test API
+
+### Move and Update the Metric Definition
+
+Existing metrics that make use of the GVST will already have a fully specified YAML
+entry that we will use as a starting point.
+This is convenient, but we want to make some minor updates rather than take it fully as is.
+At a minimum we need to move the definition out of the
+[Geckoview metrics.yaml][gv-metrics-yaml] file and change from GVST to [GIFFT](./gifft.md).
+It can go into whichever metrics.yaml file you feel is most appropriate.
+If an appropriate one does not exist, create a new one [following this guide][new-yaml].
+Completely remove the metric definition from the Geckoview `metrics.yaml`.
+
+For all metric types other than `labeled counters` the first step is to change the key
+of the `gecko_datapoint` entry to `telemetry_mirror`.
+Next, update the value as per the rules outlined in the [GIFFT guide][telemetry-mirror-doc].
+This change is required to keep data flowing in the Legacy Telemetry version of the metric.
+Doing so will ensure that downstream analyses do not break unintentionally.
+It is not necessary to modify the [Histograms.json][histograms-json] or
+[Scalars.yaml][scalars-yaml] file.
+
+To migrate `labeled counters` instead fully remove the `gecko_datapoint` entry.
+Note that our overall treatment of this type is slightly different.
+
+Next add the bug that covers the migration to the `bugs` field.
+Update the `description` field as well to indicate the metric used to be collected
+via the Geckoview Streaming API. Other fields should be updated as makes sense.
+For example, you may need to update the field for `notification_emails`.
+Since you are not changing the collection a new data review is not necessary.
+However, if you notice a metric is set to expire soon and it should continue to be collected,
+complete a [data review renewal][dr-renewal] form.
+
+Do not change the name or metric type.
+**If you need to change or one both you are creating a new collection.**
+
+### Update Calls to Use the Glean API
+
+The next step is to update the metric calls to the Glean API.
+Fortunately, for the vast matjority of metricsthis is a 1:1 swapout,
+or for `labeled counters` (which are `categorical histograms` in legacy) we add a second call.
+We identify the Glean API, remove the old call, and put in its place the Glean call.
+You can find a full API reference in the [Glean][book-of-glean], but we'll look at how to record
+values for the types that have existing GVST metrics.
+
+One way to mentally organize the metrics is to break them into two groups, those that are set,
+and those that are accumulated to. As when you use the Legacy Telemetry API for GVST,
+they are invoked slightly differently.
+
+To record in C++, you need to include `#include "mozilla/glean/GleanMetrics.h"`.
+In Javascript, it is extremely unlikely that you will not already have access to `Glean`.
+If you do not, please reach out to a Data Collection Tools team member on
+[the #glean:mozilla.org Matrix channel](https://chat.mozilla.org/#/room/#glean:mozilla.org).
+
+Let's try a few examples.
+
+#### Migrating a Set Value (string) in C++
+
+Let's look at the case of the Graphics Adaptor Vendor ID.
+This is a String,
+and it's recorded via GVST in C++
+
+GVST YAML entry (slightly truncated):
+
+```YAML
+geckoview:
+ version:
+ description: >
+ The version of the Gecko engine, example: 74.0a1
+ type: string
+ gecko_datapoint: gecko.version
+```
+
+And this is recorded:
+
+```CPP
+Telemetry::ScalarSet(Telemetry::ScalarID::GECKO_VERSION,
+ NS_ConvertASCIItoUTF16(gAppData->version));
+```
+
+To migrate this, let's update our YAML entry, again moving it out of the GVST
+metrics.yaml into the most appropriate one:
+
+```YAML
+geckoview:
+ version:
+ description: >
+ The version of the Gecko engine, example: 74.0a1
+ (Migrated from geckoview.gfx.adapter.primary)
+ type: string
+ telemetry_mirror: GECKO_VERSION
+```
+
+Notice how we've checked all of our boxes:
+
+* Made sure our category makes sense
+* Changed gecko_datapoint to telemetry_mirror
+* Updated the description to note that it was migrated from another collection
+* Kept the type identical.
+
+Now we can update our call:
+
+```CPP
+mozilla::glean::geckoview::version.Set(nsDependentCString(gAppData->version));
+```
+
+#### Migrating a Labeled Counter in C++
+
+Let's look at probably the most complicated scenario, one where we need to accumulate
+to a labeled collection. Because the glean `labeled counter` and legacy `categorical histogram`
+type do not support GIFFT, we will add a second call.
+Let's take a look at an elided version of how this would be done with GVST:
+
+```CPP
+switch (aResult.as<NonDecoderResult>()) {
+ case NonDecoderResult::SizeOverflow:
+ AccumulateCategorical(LABELS_AVIF_DECODE_RESULT::size_overflow);
+ return;
+ case NonDecoderResult::OutOfMemory:
+ AccumulateCategorical(LABELS_AVIF_DECODE_RESULT::out_of_memory);
+ return;
+ case NonDecoderResult::PipeInitError:
+ AccumulateCategorical(LABELS_AVIF_DECODE_RESULT::pipe_init_error);
+ return;
+}
+```
+
+And we update it by adding a call with the FOG API:
+
+```CPP
+switch (aResult.as<NonDecoderResult>()) {
+ case NonDecoderResult::SizeOverflow:
+ AccumulateCategorical(LABELS_AVIF_DECODE_RESULT::size_overflow);
+ mozilla::glean::avif::decode_result.EnumGet(avif::DecodeResult::eSizeOverflow).Add();
+ return;
+ case NonDecoderResult::OutOfMemory:
+ AccumulateCategorical(LABELS_AVIF_DECODE_RESULT::out_of_memory);
+ mozilla::glean::avif::decode_result.EnumGet(avif::DecodeResult::eOutOfMemory).Add();
+ return;
+ case NonDecoderResult::PipeInitError:
+ AccumulateCategorical(LABELS_AVIF_DECODE_RESULT::pipe_init_error);
+ mozilla::glean::avif::decode_result.EnumGet(avif::DecodeResult::ePipeInitError).Add();
+ return;
+}
+```
+
+#### Migrating an Accumulated Value (Histogram) in Javascript
+
+Javascript follows the same pattern. Consider the case when want to record the number
+of uniqiue site origins. Here's the original JS implementation:
+
+```Javascript
+let originCount = this.computeSiteOriginCount(aWindows, aIsGeckoView);
+let histogram = Services.telemetry.getHistogramById(
+ "FX_NUMBER_OF_UNIQUE_SITE_ORIGINS_ALL_TABS",
+);
+
+if (!this._lastRecordSiteOrigin) {
+ this._lastRecordSiteOrigin = currentTime;
+} else if (currentTime >= this._lastRecordSiteOrigin + this.min_interval) {
+ this._lastRecordSiteOrigin = currentTime;
+
+ histogram.add(originCount);
+}
+```
+
+And here is the direct Glean version
+
+```Javascript
+let originCount = this.computeSiteOriginCount(aWindows, aIsGeckoView);
+
+if (!this._lastRecordSiteOrigin) {
+ this._lastRecordSiteOrigin = currentTime;
+} else if (currentTime >= this._lastRecordSiteOrigin + this.min_interval) {
+ this._lastRecordSiteOrigin = currentTime;
+
+ Glean.tabs.uniqueSiteOriginsAllTabs.accumulateSamples([originCount]);
+}
+
+```
+
+Note that we don't have to call into Services to get the histogram object.
+
+### Update the tests to use the Glean test API
+
+The last piece is updating tests. If tests don't exist
+(which is often the case since testing metrics collected via GVST can be challenging),
+we recommend that you write them as the
+[the Glean test API is quite straightforward](./instrumentation_tests.md).
+
+The main test method is `testGetValue()`. Returning to our earlier example of
+Number of Unique Site Origins, in a Javascript test we can invoke:
+
+```Javascript
+let result = Glean.tabs.uniqueSiteOriginsAllTabs.testGetValue();
+
+// This collection is a histogram, we can check the sum for this test
+Assert.equal(result.sum, 144);
+```
+
+If your collection is in a child process, it can be helpful to invoke
+`await Services.fog.testFlushAllChildren();`
+
+If you wish to write a C++ test, `testGetValue()` is also our main method:
+
+```CPP
+#include "mozilla/glean/GleanMetrics.h"
+
+ASSERT_EQ(1,
+ mozilla::glean::avif::image_decode_result
+ .EnumGet(avif::DecodeResult::eSizeOverflow)
+ .TestGetValue()
+ .unwrap()
+ .ref());
+
+ASSERT_EQ(3,
+ mozilla::glean::avif::image_decode_result
+ .EnumGet(avif::DecodeResult::eOutOfMemory)
+ .TestGetValue()
+ .unwrap()
+ .ref());
+
+ASSERT_EQ(0,
+ mozilla::glean::avif::image_decode_result
+ .EnumGet(avif::DecodeResult::ePipeInitError)
+ .TestGetValue()
+ .unwrap()
+ .ref());
+```
+
+[book-of-glean]: https://mozilla.github.io/glean/book/index.html
+[gv-metrics-yaml]: https://searchfox.org/mozilla-central/source/toolkit/components/telemetry/geckoview/streaming/metrics.yaml
+[histograms-json]: https://searchfox.org/mozilla-central/source/toolkit/components/telemetry/Histograms.json
+[scalars-yaml]: https://searchfox.org/mozilla-central/source/toolkit/components/telemetry/Scalars.yaml
+[new-yaml]: ./new_definitions_file.md#where-do-i-define-new-metrics-and-pings
+[dr-renewal]: https://github.com/mozilla/data-review/blob/main/renewal_request.md
+[telemetry-mirror-doc]: https://firefox-source-docs.mozilla.org/toolkit/components/glean/user/gifft.html#the-telemetry-mirror-property-in-metrics-yaml
diff --git a/toolkit/components/glean/docs/user/getting_started.md b/toolkit/components/glean/docs/user/getting_started.md
new file mode 100644
index 0000000000..50b955bced
--- /dev/null
+++ b/toolkit/components/glean/docs/user/getting_started.md
@@ -0,0 +1,97 @@
+# Getting Started with Firefox on Glean (FOG)
+
+This documentation is designed to be helpful to those who are
+* New to data collection in Firefox Desktop,
+* Experienced with data collection in Firefox Desktop, but not the Glean kind
+* Those who are just interested in a refresher.
+
+## What is FOG?
+
+Firefox on Glean (FOG) is the library that brings
+[the Glean SDK](https://mozilla.github.io/glean/book/index.html),
+Mozilla's modern data collection system,
+to Firefox Desktop.
+
+FOG's code is in `toolkit/components/glean` and is considered part of the
+`Toolkit :: Telemetry` [module][modules].
+Bugs against FOG can be [filed][file-fog-bugs]
+in Bugzilla in the `Toolkit` product and the `Telemetry` component.
+(No bugs about adding new instrumentation, please.
+You can file those in the components that you want instrumented.)
+You can find folks who can help answer your questions about FOG in
+* [#glean:mozilla.org](https://chat.mozilla.org/#/room/#glean:mozilla.org)
+* [#telemetry:mozilla.org](https://chat.mozilla.org/#/room/#telemetry:mozilla.org)
+* Slack#data-help
+
+On top of the usual things Glean embedders supply
+(user engagement monitoring, network upload configuration, data upload preference watching, ...)
+FOG supplies Firefox-Desktop-specific things:
+* Privileged JS API
+* C++ API
+* IPC
+* Test Preferences
+* Support for `xpcshell`, browser-chrome mochitests, GTests, and rusttests
+* `about:glean`
+* ...and more.
+
+## What do I need to know about Glean?
+
+You use the APIs supplied by the Glean SDK to instrument Mozilla projects.
+
+The unit of instrumentation is the **metric**.
+Recording the number of times a user opens a new tab? That's a metric.
+Timing how long each JS garbage collector pass takes? Also a metric.
+
+Glean has documentation about
+[how to add a new metric][add-a-metric]
+that you should follow to learn how to add a metric to instrument Firefox Desktop.
+There are some [peculiarities specific to Firefox Desktop](new_definitions_file)
+that you'll wish to review as well.
+Don't forget to get [Data Collection Review][data-review]
+for any new or expanded data collections in mozilla projects.
+
+By adding a metric you've told the Glean SDK what shape of instrumentation you want.
+And by using the metric's APIs to instrument your code,
+you've put your interesting data into that metric.
+But how does the data leave Firefox Desktop and make it to Mozilla's Data Pipeline?
+
+Batches of related metrics are collected into **pings**
+which are submitted according to their specific schedules.
+If you don't say otherwise, any non-`event`-metric will be sent in the
+[built-in Glean "metrics" ping][metrics-ping] about once a day.
+(`event` metrics are sent in [the "events" ping][events-ping]
+more frequently than that).
+
+With data being sent to Mozilla's Data Pipeline, how do you analyse it?
+
+That's an impossible question to answer completely without knowing a _lot_ about what questions you want to answer.
+However, in general, if you want to see what data is being collected by your instrumentation,
+[go to its page in Glean Dictionary][glean-dictionary]
+and you'll find links and information there about how to proceed.
+
+## Where do I learn more?
+
+Here in the [FOG User Documentation](./index) you will find FOG-specific details like
+[how to write instrumentation tests](instrumentation_tests), or
+[how to use Glean APIs to mirror data to Telemetry](gifft).
+
+Most of what you should have to concern yourself with, as an instrumentor,
+is documented in [the Book of Glean](https://mozilla.github.io/glean/book/index.html).
+Such as its [illuminating glossary][glean-glossary],
+the [list of all metric types][metrics-types],
+or the index of our long-running blog series [This Week in Glean][twig-index].
+
+And for anything else you need help with, please find us in
+[#glean:mozilla.org](https://chat.mozilla.org/#/room/#glean:mozilla.org).
+We'll be happy to help you learn more about FOG and Glean.
+
+[add-a-metric]: https://mozilla.github.io/glean/book/user/metrics/adding-new-metrics.html
+[metrics-ping]: https://mozilla.github.io/glean/book/user/pings/metrics.html
+[events-ping]: https://mozilla.github.io/glean/book/user/pings/events.html
+[modules]: https://wiki.mozilla.org/Modules/All
+[data-review]: https://wiki.mozilla.org/Data_Collection
+[glean-dictionary]: https://dictionary.telemetry.mozilla.org/
+[glean-glossary]: https://mozilla.github.io/glean/book/appendix/glossary.html
+[twig-index]: https://mozilla.github.io/glean/book/appendix/twig.html
+[metrics-types]: https://mozilla.github.io/glean/book/reference/metrics/index.html
+[file-fog-bugs]: https://bugzilla.mozilla.org/enter_bug.cgi?product=Toolkit&component=Telemetry
diff --git a/toolkit/components/glean/docs/user/gifft.md b/toolkit/components/glean/docs/user/gifft.md
new file mode 100644
index 0000000000..4c173884ce
--- /dev/null
+++ b/toolkit/components/glean/docs/user/gifft.md
@@ -0,0 +1,241 @@
+# Glean Interface For Firefox Telemetry (GIFFT)
+
+To make Migration from Firefox Telemetry to Glean easier,
+the C++ and JS Glean API can be configured
+(on a metric-by-metric basis)
+to mirror data collection to both the Glean metric and a Telemetry probe.
+
+GIFFT should ideally be used only when the data you require for analysis still mostly lives in Telemetry,
+and should be removed promptly when no longer needed.
+Instrumentors are encouraged to have the Telemetry mirror probe expire within six versions.
+(As always you can renew an expiring probe if you're still using it,
+but this will help us get closer to the time when we eventually turn Telemetry off.)
+
+**Note:** GIFFT only works for data provided via C++ or JS.
+Rust Glean metrics APIs will not mirror to Telemetry as Telemetry does not have a Rust API.
+
+**Note:** Using the Glean API replaces the Telemetry API.
+Do not use any mix of the two APIs for the same probe.
+
+## How to Mirror a Glean Metric to a Firefox Telemetry Probe
+
+For the mirror to work, you need three things:
+* A compatible Glean metric (defined in a `metrics.yaml`)
+* A compatible Telemetry probe
+ (defined in `Histograms.json`, `Scalars.yaml`, or `Events.yaml`)
+* A `telemetry_mirror` property on the Glean metric definition identifying the Telemetry probe
+
+### Compatibility
+
+This compatibility table explains which Telemetry probe types can be mirrors for which Glean metric types:
+
+| Glean Metric Type | Telementry Probe Type |
+| ----------------- | --------------------- |
+| [boolean](https://mozilla.github.io/glean/book/reference/metrics/boolean.html) | [Scalar of kind: boolean](/toolkit/components/telemetry/collection/scalars.html) |
+| [labeled_boolean](https://mozilla.github.io/glean/book/reference/metrics/labeled_booleans.html) | [Keyed scalar of kind: boolean](/toolkit/components/telemetry/collection/scalars.html) |
+| [counter](https://mozilla.github.io/glean/book/reference/metrics/counter.html) | [Scalar of kind: uint](/toolkit/components/telemetry/collection/scalars.html) |
+| [labeled_counter](https://mozilla.github.io/glean/book/reference/metrics/labeled_counters.html) | [Keyed Scalar of kind: uint](/toolkit/components/telemetry/collection/scalars.html) |
+| [string](https://mozilla.github.io/glean/book/reference/metrics/string.html) | [Scalar of kind: string](/toolkit/components/telemetry/collection/scalars.html) |
+| [labeled_string](https://mozilla.github.io/glean/book/reference/metrics/labeled_strings.html) | *No Supported Telemetry Type* |
+| [string_list](https://mozilla.github.io/glean/book/reference/metrics/string_list.html) | [Keyed Scalar of kind: boolean](/toolkit/components/telemetry/collection/scalars.html). The keys are the strings. The values are all `true`. Calling `Set` on the labeled_string is not mirrored (since there's no way to remove keys from a keyed scalar of kind boolean). Doing so will log a warning. |
+| [timespan](https://mozilla.github.io/glean/book/reference/metrics/timespan.html) | [Scalar of kind: uint](/toolkit/components/telemetry/collection/scalars.html). The value is in units of milliseconds. |
+| [timing_distribution](https://mozilla.github.io/glean/book/reference/metrics/timing_distribution.html) | [Histogram of kind "linear" or "exponential"](/toolkit/components/telemetry/collection/histograms.html#exponential). Samples will be in units of milliseconts. |
+| [memory_distribution](https://mozilla.github.io/glean/book/reference/metrics/memory_distribution.html) | [Histogram of kind "linear" or "exponential"](/toolkit/components/telemetry/collection/histograms.html#exponential). Samples will be in `memory_unit` units. |
+| [custom_distribution](https://mozilla.github.io/glean/book/reference/metrics/custom_distribution.html) | [Histogram of kind "linear" or "exponential"](/toolkit/components/telemetry/collection/histograms.html#exponential). Samples will be used as is. Ensure the bucket count and range match. |
+| [uuid](https://mozilla.github.io/glean/book/reference/metrics/uuid.html) | [Scalar of kind: string](/toolkit/components/telemetry/collection/scalars.html). Value will be in canonical 8-4-4-4-12 format. Value is not guaranteed to be valid, and invalid values may be present in the mirrored scalar while the uuid metric remains empty. Calling `GenerateAndSet` on the uuid is not mirrored, and will log a warning. |
+| [url](https://mozilla.github.io/glean/book/reference/metrics/url.html) | [Scalar of kind: string](/toolkit/components/telemetry/collection/scalars.html). The stringified Url will be cropped to the maximum length allowed by the legacy type. |
+| [datetime](https://mozilla.github.io/glean/book/reference/metrics/datetime.html) | [Scalar of kind: string](/toolkit/components/telemetry/collection/scalars.html). Value will be in ISO8601 format. |
+| [events](https://mozilla.github.io/glean/book/reference/metrics/event.html) | [Events](/toolkit/components/telemetry/collection/events.html). The `value` field will be left empty. |
+| [quantity](https://mozilla.github.io/glean/book/reference/metrics/quantity.html) | [Scalar of kind: uint](/toolkit/components/telemetry/collection/scalars.html) |
+| [rate](https://mozilla.github.io/glean/book/reference/metrics/rate.html) | [Keyed Scalar of kind: uint](/toolkit/components/telemetry/collection/scalars.html). The keys are "numerator" and "denominator". Does not work for `rate` metrics with external denominators. |
+| [text](https://mozilla.github.io/glean/book/reference/metrics/text.html) | *No Supported Telemetry Type* |
+
+### The `telemetry_mirror` property in `metrics.yaml`
+
+You must use the C++ enum identifier of the Histogram, Scalar, or Event being mirrored to:
+* For Histograms, the Telemetry C++ enum identifier is the histogram's name
+ * e.g. The C++ enum identifier for `WR_RENDERER_TIME` is
+ `WR_RENDERER_TIME` (see {searchfox}`gfx/metrics.yaml`)
+* For Scalars, the Telemetry C++ enum identifier is the Scalar category and name in
+ `SCREAMING_SNAKE_CASE` with any `.` replaced with `_`
+ * e.g. The enum identifier for `extensions.startupCache.load_time` is
+ `EXTENSIONS_STARTUPCACHE_LOAD_TIME` (see {searchfox}`toolkit/components/extensions/metrics.yaml`)
+* For Events, the Telemetry C++ enum identifier is the Event category, method, and object
+ rendered in `Snakey_CamelCase`.
+ * e.g. The enum identifier for `page_load.toplevel#content` is
+ `Page_load_Toplevel_Content` (see {searchfox}`dom/metrics.yaml`)
+
+If you use the wrong enum identifier, this will manifest as a build error.
+
+If you are having trouble finding the correct conjugation for the mirror Telemetry probe,
+you can find the specific value in the list of all Telemetry C++ enum identifiers in
+`<objdir>/toolkit/components/telemetry/Telemetry{Histogram|Scalar|Event}Enums.h`.
+(Choose the file appropriate to the type of the Telemetry mirror.)
+
+## Artifact Build Support
+
+Sadly, GIFFT does not support Artifact builds.
+You must build Firefox when you add the mirrored metric so the C++ enum value is present,
+even if you only use the metric from Javascript.
+
+## Analysis Gotchas
+
+Firefox Telemetry and the Glean SDK are very different.
+Though GIFFT bridges the differences as best it can,
+there are many things it cannot account for.
+
+These are a few of the ways that differences between Firefox Telemetry and the Glean SDK might manifest as anomalies during analysis.
+
+### Processes, Products, and Channels
+
+Like Firefox on Glean itself,
+GIFFT doesn't know what process, product, or channel it is recording in.
+Telemetry does, and imposes restrictions on which probes can be recorded to and when.
+
+Ensure that the following fields in any Telemetry mirror's definition aren't too restrictive for your use:
+* `record_in_processes`
+* `products`
+* `release_channel_collection`/`releaseChannelCollection`
+
+A mismatch won't result in an error.
+If you, for example,
+record to a Glean metric in a release channel that the Telemetry mirror probe doesn't permit,
+then the Glean metric will have a value and the Telemetry mirror probe won't.
+
+Also recall that Telemetry probes split their values across processes.
+[Glean metrics do not](../dev/ipc.md).
+This may manifest as curious anomalies when comparing the Glean metric to its Telemetry mirror probe.
+Ensure your analyses are aggregating Telemetry values from all processes,
+or define and use process-specific Glean metrics and Telemetry mirror probes to keep things separate.
+
+### Pings
+
+Glean and Telemetry both send their built-in pings on their own schedules.
+This means the values present in these pings may not agree since they reflect state at different time.
+
+For example, if you are measuring "Number of Monitors" with a
+[`quantity`](https://mozilla.github.io/glean/book/reference/metrics/quantity.html)
+sent by default in the Glean "metrics" ping mirrored to a
+[Scalar of kind: uint](/toolkit/components/telemetry/collection/scalars.rst)
+sent by default in the Telemetry "main" ping,
+then if the user plugs in a second monitor between midnight
+(when Telemetry "main" pings with reason "daily" are sent) and 4AM
+(when Glean "metrics" pings with reason "today" are sent),
+the value in the `quantity` will be `2`
+while the value in the Scalar of kind: uint will be `1`.
+
+If the metric or mirrored probe are sent in Custom pings,
+the schedules could line up exactly or be entirely unrelated.
+
+### Labels
+
+Labeled metrics supported by GIFFT
+(`labeled_boolean` and `labeled_counter`)
+adhere to the Glean SDK's
+[label format](https://mozilla.github.io/glean/book/reference/metrics/index.html#label-format).
+
+Keyed Scalars, on the other hand, do not have a concept of an "Invalid key".
+Firefox Telemetry will accept just about any sequence of bytes as a key.
+
+This means that a label deemed invalid by the Glean SDK may appear in the mirrored probe's data.
+For example, using 72 "1" characters as a label that doesn't conform to the format
+(it is longer than 71 printable ASCII characters).
+See that the `labeled_boolean` metric
+[correctly ascribes it to `__other__`](https://mozilla.github.io/glean/book/reference/metrics/index.html#labeled-metrics)
+whereas the mirrored Keyed Scalar with kind boolean stores and retrieves it without change:
+```js
+Glean.testOnly.mirrorsForLabeledBools["1".repeat(72)].set(true);
+Assert.equal(true, Glean.testOnly.mirrorsForLabeledBools.__other__.testGetValue());
+// The above actually throws NS_ERROR_LOSS_OF_SIGNIFICANT_DATA because it also records
+// an invalid_label error. But you get the idea.
+let snapshot = Services.telemetry.getSnapshotForKeyedScalars().parent;
+Assert.equal(true, snapshot["telemetry.test.mirror_for_labeled_bool"]["1".repeat(72)]);
+```
+
+### Telemetry Events
+
+A Glean event can be mirrored to a Telemetry Event.
+Telemetry Events must be enabled before they can be recorded to via the API
+`Telemetry.setEventRecordingEnabled(category, enable);`.
+If the Telemetry Event isn't enabled,
+recording to the Glean event will still work,
+and the event will be Summarized in Telemetry as all disabled events are.
+
+See
+[the Telemetry Event docs](/toolkit/components/telemetry/collection/events.rst)
+for details on how disabled Telemetry Events behave.
+
+### Numeric Values
+
+The arguments and storage formats for Glean's numeric types
+(`counter`, `labeled_counter`, `quantity`, `rate`, and `timespan`)
+are different from Telemetry's numeric type
+(Scalar of kind `uint`).
+
+This results in a few notable differences.
+
+#### Saturation and Overflow
+
+`counter`, `labeled_counter`, and `rate` metrics are stored as 32-bit signed values.
+`quantity` metrics are stored as 64-bit signed values.
+`timing_distribution` samples can be 64-bit signed values.
+All of these Glean numeric metric types saturate at their maximum representable value,
+or according to the Limits section of the Glean metric type documentation.
+
+Scalars of kind `uint` are stored as 32-bit unsigned values.
+They will overflow if they exceed the value $2^{32} - 1$.
+
+If a Glean numeric type saturates, it will record an error of type `invalid_overflow`.
+In your analyses please check for these errors.
+
+#### Quantity Value Over-size
+
+Values greater than $2^{32} - 1$ passed to a `quantity` metric's
+`set()` method will be clamped to $2^{32} - 1$ before being passed to the metric's Telemetry mirror.
+
+#### Negative Values
+
+Values less than 0 passed to any numeric metric type's API will not be passed on to the Telemetry mirror.
+This avoids small negative numbers being cast into a stunningly large numbers,
+and keeps the Telemetry mirror's value closer to that of the Glean metric.
+
+#### Long Time Spans
+
+If the number of milliseconds between calls to a
+`timespan` metric's `start()` and `stop()` methods exceeds $2^{32} - 1$,
+the value passed to the metric's Telemetry mirror will be clamped to $2^{32} - 1$.
+
+The same happens for samples in `timing_distribution` metrics:
+values passed to the Telemetry mirror histogram will saturate at $2^{32} - 1$
+until they get past $2^{64}$ when they'll overflow.
+
+#### `timing_distribution` mirrors: Samples and Sums might be Different
+
+A specific value in a `timing_distribution` metric will not always agree with
+the corresponding value in its mirrored-to histogram.
+Though the calls to the clock are very close together in the code in Telemetry and Glean,
+Telemetry's are not on the exact same instruction as Glean's _and_
+Telemetry uses a different clock source (`TimeStamp::Now()`) than Glean (`time::precise_time_ns()`).
+
+Also, if these slight drifts happen to cross the boundary of a bucket in either system,
+samples might end up looking more different than you'd expect.
+
+This shouldn't affect analysis, but it can affect testing, so please
+[bear this difference in mind](./instrumentation_tests.md#general-things-to-bear-in-mind)
+in testing.
+
+### App Shutdown
+
+Telemetry only works up to
+[`ShutdownPhase::AppShutdownTelemetry` aka `profile-before-change-telemetry`][app-shutdown].
+Telemetry data recorded after that phase just aren't persisted.
+
+FOG _presently_ shuts down Glean in a later phase,
+and so is able to collect data deeper into shutdown.
+(The particular phase is not presently something anyone's asked us to guarantee,
+so that's why I'm not being precise.)
+
+What this means is that, for data recorded later in shutdown,
+Glean will report more complete information than Telemetry will.
+
+[app-shutdown]: https://searchfox.org/mozilla-central/source/xpcom/base/AppShutdown.cpp#57
diff --git a/toolkit/components/glean/docs/user/index.md b/toolkit/components/glean/docs/user/index.md
new file mode 100644
index 0000000000..f5ddf6d9e3
--- /dev/null
+++ b/toolkit/components/glean/docs/user/index.md
@@ -0,0 +1,17 @@
+# Using Firefox on Glean
+
+This section of docs is designed to be helpful to people instrumenting Firefox Desktop.
+You may wish to begin with the [Getting Started](getting_started.md) docs.
+Or, if you're already acquainted with Glean concepts and what FOG is,
+you might want to know [how to migrate a piece of Firefox Telemetry to Glean](migration.md).
+
+```{toctree}
+:titlesonly:
+:maxdepth: 1
+:glob:
+
+getting_started
+new_definitions_file
+*
+Glean SDK Documentation <https://mozilla.github.io/glean/book/index.html>
+```
diff --git a/toolkit/components/glean/docs/user/instrumentation_tests.md b/toolkit/components/glean/docs/user/instrumentation_tests.md
new file mode 100644
index 0000000000..e457e9fd39
--- /dev/null
+++ b/toolkit/components/glean/docs/user/instrumentation_tests.md
@@ -0,0 +1,278 @@
+# Writing Instrumentation Tests
+
+```{admonition} Old Glean Proverb
+If it's important enough to be instrumented, it's important enough to be tested.
+```
+
+All metrics and pings in the Glean SDK have [well-documented APIs for testing][glean-metrics-apis].
+You'll want to familiarize yourself with `TestGetValue()`
+(here's [an example JS (xpcshell) test of some metrics][metrics-xpcshell-test])
+for metrics and
+[`TestBeforeNextSubmit()`][test-before-next-submit]
+(here's [an example C++ (gtest) test of a custom ping][ping-gtest])
+for pings.
+
+All test APIs are available in all three of FOG's supported languages:
+Rust, C++, and JavaScript.
+
+But how do you get into a position where you can even call these test APIs?
+How do they fit in with Firefox Desktop's testing frameworks?
+
+## Manual Testing and Debugging
+
+The Glean SDK has [debugging capabilities][glean-debug]
+for manually verifying that instrumentation makes it to Mozilla's Data Pipeline.
+Firefox Desktop supports these via environment variables _and_
+via the interface on `about:glean`.
+
+This is all well and good for getting a good sense check that things are going well _now_,
+but in order to check that everything stays good through the future,
+you're going to want to write some automated tests.
+
+## General Things To Bear In Mind
+
+* You may see values from previous tests persist across tests because the profile directory was shared between test cases.
+ * You can reset Glean before your test by calling
+ `Services.fog.testResetFOG()` (in JS).
+ * If your instrumentation isn't on the parent process,
+ you should call `await Services.fog.testFlushAllChildren()` before `testResetFOG`.
+ That will ensure all pending data makes it to the parent process to be cleared.
+ * You shouldn't have to do this in C++ or Rust since there you should use the
+ `FOGFixture` test fixture.
+* If your metric is based on timing (`timespan`, `timing_distribution`),
+ do not expect to be able to assert the correct timing value.
+ Glean does a lot of timing for you deep in the SDK, so unless you mock the system's monotonic clock,
+ do not expect the values to be predictable.
+ * Instead, check that a value is `> 0` or that the number of samples is expected.
+ * You might be able to assert that the value is at least as much as a known, timed value,
+ but beware of rounding.
+ * If your metric is a `timing_distribution` mirroring to a Telemetry probe via [GIFFT](./gifft.md),
+ there may be [small observed differences between systems](./gifft.md#timing-distribution-mirrors-samples-and-sums-might-be-different)
+ that can cause equality assertions to fail.
+* Errors in instrumentation APIs do not panic, throw, or crash.
+ But Glean remembers that the errors happened.
+ * Test APIs, on the other hand, are permitted
+ (some may say "encouraged")
+ to panic, throw, or crash on bad behaviour.
+ * If you call a test API and it panics, throws, or crashes,
+ that means your instrumentation did something wrong.
+ Check your test logs for details about what went awry.
+
+### Tests and Artifact Builds
+
+Artifact build support is provided by [the JOG subsystem](../dev/jog).
+It is able to register the latest versions of all metrics and pings at runtime.
+However, the compiled code is still running against the
+version of those metrics and pings that was current at the time the artifacts were compiled.
+
+This isn't a problem unless:
+* You are changing a metric or ping that is used in instrumentation in the compiled code, or
+* You are using `testBeforeNextSubmit` in JavaScript for a ping submitted in the compiled code.
+
+When in doubt, simply test your new test in artifact mode
+(by e.g. passing `--enable-artifact-builds` to `mach try`)
+before submitting it.
+If it doesn't pass in artifact mode because of one of these two cases,
+you may need to skip your test whenever FOG's artifact build support is enabled:
+* xpcshell:
+```js
+add_task(
+ { skip_if: () => Services.prefs.getBoolPref("telemetry.fog.artifact_build", false) },
+ function () {
+ // ... your test ...
+ }
+);
+```
+* mochitest:
+```js
+add_task(function () {
+ if (Services.prefs.getBoolPref("telemetry.fog.artifact_build", false)) {
+ Assert.ok(true, "Test skipped in artifact mode.");
+ return;
+ }
+ // ... your test ...
+});
+```
+
+## The Usual Test Format
+
+Instrumentation tests tend to follow the same three-part format:
+1) Assert no value in the metric
+2) Express behaviour
+3) Assert correct value in the metric
+
+Your choice of test suite will depend on how the instrumented behaviour can be expressed.
+
+
+## `xpcshell` Tests
+
+If the instrumented behaviour is on the main or content process and can be called from privileged JS,
+`xpcshell` is an excellent choice.
+
+`xpcshell` is so minimal an environment, however, that
+(pending [bug 1756055](https://bugzilla.mozilla.org/show_bug.cgi?id=1756055))
+you'll need to manually tell it you need two things:
+1) A profile directory
+2) An initialized FOG
+
+```js
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_setup(function test_setup() {
+ // FOG needs a profile directory to put its data in.
+ do_get_profile();
+
+ // FOG needs to be initialized in order for data to flow.
+ Services.fog.initializeFOG();
+});
+```
+
+From there, just follow The Usual Test Format:
+
+```js
+add_task(function test_instrumentation() {
+ // 1) Assert no value
+ Assert.equal(undefined, Glean.myMetricCategory.myMetricName.testGetValue());
+
+ // 2) Express behaviour
+ // ...<left as an exercise to the reader>...
+
+ // 3) Assert correct value
+ Assert.equal(kValue, Glean.myMetricCategory.myMetricName.testGetValue());
+});
+```
+
+If your new instrumentation includes a new custom ping,
+there are two small additions to The Usual Test Format:
+
+* 1.1) Call `testBeforeNextSubmit` _before_ your ping is submitted.
+ The callback you register in `testBeforeNextSubmit`
+ is called synchronously with the call to the ping's `submit()`.
+* 3.1) Check that the ping actually was submitted.
+ If all your Asserts are inside `testBeforeNextSubmit`'s closure,
+ another way this test could pass is by not running any of them.
+
+```js
+add_task(function test_custom_ping() {
+ // 1) Assert no value
+ Assert.equal(undefined, Glean.myMetricCategory.myMetricName.testGetValue());
+
+ // 1.1) Set up Step 3.
+ let submitted = false;
+ GleanPings.myPing.testBeforeNextSubmit(reason => {
+ submitted = true;
+ // 3) Assert correct value
+ Assert.equal(kExpectedReason, reason, "Reason of submitted ping must match.");
+ Assert.equal(kExpectedMetricValue, Glean.myMetricCategory.myMetricName.testGetValue());
+ });
+
+ // 2) Express behaviour that sends a ping with expected reason and contents
+ // ...<left as an exercise to the reader>...
+
+ // 3.1) Check that the ping actually was submitted.
+ Assert.ok(submitted, "Ping was submitted, callback was called.");
+});
+```
+
+(( We acknowledge that this isn't the most ergonomic form.
+Please follow
+[bug 1756637](https://bugzilla.mozilla.org/show_bug.cgi?id=1756637)
+for updates on a better design and implementation for ping tests. ))
+
+## mochitest
+
+`browser-chrome`-flavoured mochitests can be tested very similarly to `xpcshell`,
+though you do not need to request a profile or initialize FOG.
+`plain`-flavoured mochitests aren't yet supported (follow
+[bug 1799977](https://bugzilla.mozilla.org/show_bug.cgi?id=1799977)
+for updates and a workaround).
+
+If you're testing in `mochitest`, your instrumentation (or your test)
+might not be running in the parent process.
+This means you get to learn the IPC test APIs.
+
+### IPC
+
+All test APIs must be called on the main process
+(they'll assert otherwise).
+But your instrumentation might be on any process, so how do you test it?
+
+In this case there's a slight addition to the Usual Test Format:
+1) Assert no value in the metric
+2) Express behaviour
+3) _Flush all pending FOG IPC operations with `await Services.fog.testFlushAllChildren()`_
+4) Assert correct value in the metric.
+
+**NOTE:** We learned in
+[bug 1843178](https://bugzilla.mozilla.org/show_bug.cgi?id=1843178)
+that the list of all content processes that `Services.fog.testFlushAllChildren()`
+uses is very quickly updated after the end of a call to `BrowserUtils.withNewTab(...)`.
+If you are using `withNewTab`, you should consider calling `testFlushAllChildren()`
+_within_ the callback.
+
+## GTests/Google Tests
+
+Please make use of the `FOGFixture` fixture when writing your tests, like:
+
+```cpp
+TEST_F(FOGFixture, MyTestCase) {
+ // 1) Assert no value
+ ASSERT_EQ(mozilla::Nothing(),
+ my_metric_category::my_metric_name.TestGetValue());
+
+ // 2) Express behaviour
+ // ...<left as an exercise to the reader>...
+
+ // 3) Assert correct value
+ ASSERT_EQ(kValue,
+ my_metric_category::my_metric_name.TestGetValue().unwrap().ref());
+}
+```
+
+The fixture will take care of ensuring storage is reset between tests.
+
+## Rust `rusttests`
+
+The general-purpose
+[Testing & Debugging Rust Code in Firefox](/testing-rust-code/index)
+is a good thing to review first.
+
+Unfortunately, FOG requires gecko
+(to tell it where the profile dir is, and other things),
+which means we need to use the
+[GTest + FFI approach](/testing-rust-code/index.md#gtests)
+where GTest is the runner and Rust is just the language the test is written in.
+
+This means your test will look like a GTest like this:
+
+```cpp
+extern "C" void Rust_MyRustTest();
+TEST_F(FOGFixture, MyRustTest) { Rust_MyRustTest(); }
+```
+
+Plus a Rust test like this:
+
+```rust
+#[no_mangle]
+pub extern "C" fn Rust_MyRustTest() {
+ // 1) Assert no value
+ assert_eq!(None,
+ fog::metrics::my_metric_category::my_metric_name.test_get_value(None));
+
+ // 2) Express behaviour
+ // ...<left as an exercise to the reader>...
+
+ // 3) Assert correct value
+ assert_eq!(Some(value),
+ fog::metrics::my_metric_category::my_metric_name.test_get_value(None));
+}
+```
+
+[glean-metrics-apis]: https://mozilla.github.io/glean/book/reference/metrics/index.html
+[metrics-xpcshell-test]: https://searchfox.org/mozilla-central/rev/66e59131c1c76fe486424dc37f0a8a399ca874d4/toolkit/mozapps/update/tests/unit_background_update/test_backgroundupdate_glean.js#28
+[ping-gtest]: https://searchfox.org/mozilla-central/rev/66e59131c1c76fe486424dc37f0a8a399ca874d4/toolkit/components/glean/tests/gtest/TestFog.cpp#232
+[test-before-next-submit]: https://mozilla.github.io/glean/book/reference/pings/index.html#testbeforenextsubmit
+[glean-debug]: https://mozilla.github.io/glean/book/reference/debug/index.html
diff --git a/toolkit/components/glean/docs/user/migration.md b/toolkit/components/glean/docs/user/migration.md
new file mode 100644
index 0000000000..f3c86a182b
--- /dev/null
+++ b/toolkit/components/glean/docs/user/migration.md
@@ -0,0 +1,909 @@
+# Migrating Firefox Telemetry to Glean
+
+This guide aims to help you migrate individual data collections from
+[Firefox Telemetry](/toolkit/components/telemetry/index.rst)
+to
+[Glean][book-of-glean] via [Firefox on Glean](../index.md).
+
+This is intended to be a reference to help you fill out your
+[migration worksheet][migration-worksheet],
+or for mentally translating Telemetry concepts to Glean ones.
+
+```{contents}
+```
+
+## General Things To Bear In Mind
+
+You should familiarize yourself with
+[the guide on adding new metrics to Firefox Desktop](new_definitions_file.md).
+Its advice stacks with the advice included in this guide as
+(once you've figured out what kind) you will indeed be adding new metrics.
+
+There are some other broad topics specific to migrating Firefox Telemetry stuff to Glean stuff:
+
+### Process-Agnosticism: No more `record_in_processes` field
+
+Glean (and thus FOG) [doesn't know anything about processes][ipc-dev-doc]
+except what it has to in order to ensure all the data makes it to the parent process.
+Firefox Telemetry cared very much about which process was collecting which specific data,
+keeping them separate.
+
+If you collect data in multiple processes and wish to keep data from each process type separate,
+you will need to provide this separation yourself.
+
+Please see [this dev doc][ipc-dev-doc] for an example of how to do that.
+
+### Channel-Agnosticism: No more `release_channel_collection: opt-out`
+
+FOG doesn't make a differentiation between pre-release Firefox and release Firefox,
+except inasmuch as is necessary to put the correct channel in `client_info.app_channel`.
+
+This means all data is collected in all build configurations.
+
+If you wish or are required to only collect your data in pre-release Firefox,
+please avail yourself of the `EARLY_BETA_OR_EARLIER` `#define` or `AppConstant`.
+
+### File-level Product Inclusion/Exclusion: No more `products` field
+
+Glean determines which metrics are recorded in which products via
+[a dependency tree][repositories-yaml].
+This means FOG doesn't distinguish between products at the per-product level.
+
+If some of your metrics are recorded in different sets of products
+(e.g. some of your metrics are collected in both Firefox Desktop _and_ Firefox for Android,
+but others are Firefox Desktop-specific)
+you must separate them into separate [definitions files](new_definitions_file.md).
+
+### Many Definitions Files
+
+Each component is expected to own and care for its own
+[metrics definitions files](new_definitions_file.md).
+There is no centralized `Histograms.json` or `Scalars.yaml` or `Events.yaml`.
+
+Instead the component being instrumented will have its own `metrics.yaml`
+(and `pings.yaml` for any [Custom Pings][custom-pings])
+in which you will define the data.
+
+See [this guide](new_definitions_file.md) for details.
+
+### Testing
+
+Firefox Telemetry had very uneven support for testing instrumentation code.
+FOG has much better support. Anywhere you can instrument is someplace you can test.
+
+It's as simple as calling `testGetValue`.
+
+All migrated collections are expected to be tested.
+If you can't test them, then you'd better have an exceptionally good reason why not.
+
+For more details, please peruse the
+[instrumentation testing docs](instrumentation_tests).
+
+## Which Glean Metric Type Should I Use?
+
+Glean uses higher-level metric types than Firefox Telemetry does.
+This complicates migration as something that is "just a number"
+in Firefox Telemetry might map to any number of Glean metric types.
+
+Please choose the most specific metric type that solves your problem.
+This'll make analysis easier as
+1. Others will know more about how to analyse the metric from more specific types.
+2. Tooling will be able to present only relevant operations for more specific types.
+
+Example:
+> In Firefox Telemetry I record the number of monitors attached to the computer that Firefox Desktop is running on.
+> I could record this number as a [`string`][string-metric], a [`counter`][counter-metric],
+> or a [`quantity`][quantity-metric].
+> The `string` is an obvious trap. It doesn't even have the correct data type (string vs number).
+> But is it a `counter` or `quantity`?
+> If you pay attention to this guide you'll learn that `counter`s are used to accumulate sums of information,
+> whereas `quantity` metrics are used to record specific values.
+> The "sum" of monitors over time doesn't make sense, so `counter` is out.
+> `quantity` is the correct choice.
+
+## Histograms
+
+[Histograms][telemetry-histograms]
+are the oldest Firefox Telemetry data type, and as such they've accumulated
+([ha!][histogram-accumulate]) the most ways of being used.
+
+### Scalar Values in Histograms: kind `flag` and `count`
+
+If you have a Histogram that records exactly one value,
+please scroll down and look at the migration guide for the relevant Scalar:
+* For Histograms of kind `flag` see "Scalars of kind `bool`"
+* For Histograms of kind `count` see "Scalars of kind `uint`"
+
+### Continuous Distributions: kind `linear` and `exponential`
+
+If the Histogram you wish to migrate is formed of multiple buckets that together form a single continuous range
+(like you have buckets 1-5, 6-10, 11-19, and 20-50 - they form the range 1-50),
+then you will want a "distribution" metric type in Glean.
+Which kind of "distribution" metric type depends on what the samples are.
+
+#### Timing samples - Use Glean's `timing_distribution`
+
+The most common type of continuous distribution in Firefox Telemetry is a histogram of timing samples like
+[`GC_MS`][gc-ms].
+
+In Glean this sort of data is recorded using a
+[`timing-distribution`][timing-distribution-metric] metric type.
+
+You will no longer need to worry about the range of values or number or distribution of buckets
+(represented by the `low`, `high`, `n_buckets`, or `kind` in your Histogram's definition).
+Glean uses a [clever automatic bucketing algorithm][timing-distribution-metric] instead.
+
+So for a Histogram that records timing samples like this:
+
+```
+ "GC_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "geckoview_streaming"],
+ "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org", "jcoppeard@mozilla.com"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "bug_numbers": [1636419],
+ "description": "Time spent running JS GC (ms)"
+ },
+```
+
+You will migrate to a `timing_distibution` metric type like this:
+
+```yaml
+js:
+ gc:
+ type: timing_distribution
+ time_unit: millisecond
+ description: |
+ Time spent running the Javascript Garbage Collector.
+ Migrated from Firefox Telemetry's `GC_MS`.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1636419
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1636419#c8
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - dev-telemetry-gc-alerts@mozilla.org
+ - jcoppeard@mozilla.com
+ expires: never
+```
+
+**GIFFT:** This type of collection is mirrorable back to Firefox Telemetry via the
+[Glean Interface For Firefox Telemetry][gifft].
+See [the guide][gifft] for instructions.
+
+#### Memory Samples - Use Glean's `memory_distribution`
+
+Another common content of `linear` or `exponential`
+Histograms in Firefox Telemetry is memory samples.
+For example, [`MEMORY_TOTAL`][memory-total]'s samples are in kilobytes.
+
+In Glean this sort of data is recorded using a
+[`memory-distribution`][memory-distribution-metric] metric type.
+
+You will no longer need to worry about the range of values or number or distribution of buckets
+(represented by the `low`, `high`, `n_buckets`, or `kind` in your Histogram's definition).
+Glean uses a [clever automatic bucketing algorithm][memory-distribution-metric] instead.
+
+So for a Histogram that records memory samples like this:
+
+```
+ "MEMORY_TOTAL": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "thunderbird"],
+ "alert_emails": ["memshrink-telemetry-alerts@mozilla.com", "amccreight@mozilla.com"],
+ "bug_numbers": [1198209, 1511918],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 32768,
+ "high": 16777216,
+ "n_buckets": 100,
+ "description": "Total Memory Across All Processes (KB)",
+ "releaseChannelCollection": "opt-out"
+ },
+```
+
+You will migrate to a `memory_distribution` metric type like this:
+
+```yaml
+memory:
+ total:
+ type: memory_distribution
+ memory_unit: kilobyte
+ description: |
+ The total memory allocated across all processes.
+ Migrated from Telemetry's `MEMORY_TOTAL`.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1198209
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1511918
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1511918#c9
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - memshrink-telemetry-alerts@mozilla.com
+ - amccreight@mozilla.com
+ expires: never
+```
+
+**GIFFT:** This type of collection is mirrorable back to Firefox Telemetry via the
+[Glean Interface For Firefox Telemetry][gifft].
+See [the guide][gifft] for instructions.
+
+#### Percentage Samples - Comment on bug 1657467
+
+A very common Histogram in Firefox Desktop is a distribution of percentage samples.
+[For example, `GC_SLICE_DURING_IDLE`][gc-idle].
+
+Glean doesn't currently have a good metric type for this.
+But we [intend to add one][new-metric-percent].
+If you are migrating a collection of this type,
+please add a comment to the bug detailing which probe you are migrating,
+and when you need it migrated by.
+We'll prioritize adding this metric type accordingly.
+
+#### Other - Use Glean's `custom_distribution`
+
+Continuous Distribution Histograms have been around long enough to have gotten weird.
+If you're migrating one of those histograms with units like
+["square root of pixels times milliseconds"][checkerboard-severity],
+we have a "catch all" metric type for you: [Custom Distribution][custom-distribution-metric].
+
+Sadly, you'll have to care about the bucketing algorithm and bucket ranges for this one.
+So for a Histogram with artisinal samples like:
+
+```
+ "CHECKERBOARD_SEVERITY": {
+ "record_in_processes": ["main", "content", "gpu"],
+ "products": ["firefox", "fennec", "geckoview_streaming"],
+ "alert_emails": ["gfx-telemetry-alerts@mozilla.com", "botond@mozilla.com"],
+ "bug_numbers": [1238040, 1539309, 1584109],
+ "releaseChannelCollection": "opt-out",
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1073741824,
+ "n_buckets": 50,
+ "description": "Opaque measure of the severity of a checkerboard event"
+ },
+```
+
+You will migrate it to a `custom_distribution` like:
+
+```yaml
+gfx.checkerboard:
+ severity:
+ type: custom_distribution
+ range_max: 1073741824
+ bucket_count: 50
+ histogram_type: exponential
+ unit: Opaque unit
+ description: >
+ An opaque measurement of the severity of a checkerboard event.
+ This doesn't have units, it's just useful for comparing two checkerboard
+ events to see which one is worse, for some implementation-specific
+ definition of "worse". The larger the value, the worse the
+ checkerboarding.
+ Migrated from Telemetry's `CHECKERBOARD_SEVERITY`.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1238040
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1539309
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1584109
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1584109#c1
+ notification_emails:
+ - gfx-telemetry-alerts@mozilla.com
+ - botond@mozilla.com
+ data_sensitivity:
+ - technical
+ expires: never
+```
+
+**TODO [Bug 1677447](https://bugzilla.mozilla.org/show_bug.cgi?id=1677447):**
+Custom Distributions aren't yet implemented in FOG. We're working on it.
+When they're done we'll see if they'll support GIFFT like the other distributions.
+
+#### Keyed Histograms with Continuous Sample Distributions - Ask on #glean:mozilla.org for assistance
+
+Glean doesn't currently have a good metric type for keyed continuous distributions
+like video play time keyed by codec.
+Please [reach out to us][glean-matrix] to explain your use-case.
+We will help you either work within what Glean currently affords or
+[design a new metric type for you][new-metric-type].
+
+### Discrete Distributions: kind `categorical`, `enumerated`, or `boolean` - Use Glean's `labeled_counter`
+
+If the samples don't fall in a continuous range and instead fall into a known number of buckets,
+Glean provides the [Labeled Counter][labeled-counter-metric] for these cases.
+
+Simply enumerate the discrete categories as `labels` in the `labeled_counter`.
+
+For example, for a Histogram of kind `categorical` like:
+
+```
+ "AVIF_DECODE_RESULT": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "geckoview_streaming"],
+ "alert_emails": ["cchang@mozilla.com", "jbauman@mozilla.com"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "categorical",
+ "labels": [
+ "success",
+ "parse_error",
+ "no_primary_item",
+ "decode_error",
+ "size_overflow",
+ "out_of_memory",
+ "pipe_init_error",
+ "write_buffer_error",
+ "alpha_y_sz_mismatch",
+ "alpha_y_bpc_mismatch"
+ ],
+ "description": "Decode result of AVIF image",
+ "bug_numbers": [1670827]
+ },
+```
+
+You would migrate to a `labeled_counter` like:
+
+```yaml
+avif:
+ decode_result:
+ type: labeled_counter,
+ description: |
+ Each AVIF image's decode result.
+ Migrated from Telemetry's `AVIF_DECODE_RESULT`.
+ labels:
+ - success
+ - parse_error
+ - no_primary_item
+ - decode_error
+ - size_overflow
+ - out_of_memory
+ - pipe_init_error
+ - write_buffer_error
+ - alpha_y_sz_mismatch
+ - alpha_y_bpc_mismatch
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1670827
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1670827#c9
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - cchang@mozilla.com
+ - jbauman@mozilla.com
+ expires: never
+```
+
+**N.B:** Glean Labels have a strict regex.
+You may have to transform some categories to
+`snake_case` so that they're safe for the data pipeline.
+
+**GIFFT:** This type of collection is mirrorable back to Firefox Telemetry via the
+[Glean Interface For Firefox Telemetry][gifft].
+See [the guide][gifft] for instructions.
+**N.B.:** This will mirror back as a Keyed Scalar of kind `uint`,
+not as any kind of Histogram,
+so your original un-migrated histogram cannot be used as the mirror.
+
+#### Keyed Histograms with Discrete Sample Distributions: `"keyed": true` and kind `categorical`, `enumerated`, or `boolean` - Comment on bug 1657470
+
+Glean doesn't currently have a good metric type for this.
+But we [intend to add one][new-metric-keyed-categorical].
+If you are migrating a collection of this type,
+please add a comment to the bug detailing which probe you are migrating,
+and when you need it migrated by.
+We'll prioritize adding this metric type accordingly.
+
+## Scalars
+
+[Scalars][telemetry-scalars] are low-level individual data collections with a variety of uses.
+
+### Scalars of `kind: uint` that you call `scalarAdd` on - Use Glean's `counter`
+
+The most common kind of Scalar is of `kind: uint`.
+The most common use of such a scalar is to repeatedly call `scalarAdd`
+on it as countable things happen.
+
+The Glean metric type for countable things is [the `counter` metric type][counter-metric].
+
+So for a Scalar like this:
+
+```yaml
+script.preloader:
+ mainthread_recompile:
+ bug_numbers:
+ - 1364235
+ description:
+ How many times we ended up recompiling a script from the script preloader
+ on the main thread.
+ expires: "100"
+ keyed: false
+ kind: uint
+ notification_emails:
+ - dothayer@mozilla.com
+ - plawless@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'main'
+ - 'content'
+```
+
+You will migrate to a `counter` metric type like this:
+
+```yaml
+script.preloader:
+ mainthread_recompile:
+ type: counter
+ description: |
+ How many times we ended up recompiling a script from the script preloader
+ on the main thread.
+ Migrated from Telemetry's `script.preloader.mainthread_recompile`.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1364235
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1364235#c25
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - dothayer@mozilla.com
+ - plawless@mozilla.com
+ expires: "100"
+```
+
+**GIFFT:** This type of collection is mirrorable back to Firefox Telemetry via the
+[Glean Interface For Firefox Telemetry][gifft].
+See [the guide][gifft] for instructions.
+
+#### Keyed Scalars of `kind: uint` that you call `scalarAdd` on - Use Glean's `labeled_counter`
+
+Another very common use of Scalars is to have a Keyed Scalar of
+`kind: uint`. This was often used to track UI usage.
+
+This is supported by the [Glean `labeled_counter` metric type][labeled-counter-metric].
+
+So for a Keyed Scalar of `kind: uint` like this:
+
+```yaml
+urlbar:
+ tips:
+ bug_numbers:
+ - 1608461
+ description: >
+ A keyed uint recording how many times particular tips are shown in the
+ Urlbar and how often their confirm and help buttons are pressed.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - email@example.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+```
+
+You would migrate it to a `labeled_counter` like this:
+
+```yaml
+urlbar:
+ tips:
+ type: labeled_counter
+ description: >
+ A keyed uint recording how many times particular tips are shown in the
+ Urlbar and how often their confirm and help buttons are pressed.
+ Migrated from Telemetry's `urlbar.tips`.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1608461
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1608461#c42
+ data_sensitivity:
+ - interaction
+ expires: never
+ notification_emails:
+ - email@example.com
+```
+
+Now, if your Keyed Scalar has a list of known keys,
+you should provide it to the `labeled_counter` using the `labels` property like so:
+
+```yaml
+urlbar:
+ tips:
+ type: labeled_counter
+ labels:
+ - tabtosearch_onboard_shown
+ - tabtosearch_shown
+ - searchtip_onboard_shown
+ ...
+```
+
+**N.B:** Glean Labels have a strict regex.
+You may have to transform some categories to
+`snake_case` so that they're safe for the data pipeline.
+
+**GIFFT:** This type of collection is mirrorable back to Firefox Telemetry via the
+[Glean Interface For Firefox Telemetry][gifft].
+See [the guide][gifft] for instructions.
+
+### Scalars of `kind: uint` that you call `scalarSet` on - Use Glean's `quantity`
+
+Distinct from counts which are partial sums,
+Scalars of `kind: uint` that you _set_ could contain just about anything.
+The best metric type depends on the type of data you're setting
+(See "Other Scalar-ish types" for some possibilities).
+
+If it's a numerical value you are setting, chances are you will be best served by
+[Glean's `quantity` metric type][quantity-metric].
+
+For a such a quantitative Scalar like:
+
+```yaml
+gfx.display:
+ primary_height:
+ bug_numbers:
+ - 1594145
+ description: >
+ Height of the primary display, takes device rotation into account.
+ expires: never
+ kind: uint
+ notification_emails:
+ - gfx-telemetry-alerts@mozilla.com
+ - ktaeleman@mozilla.com
+ products:
+ - 'geckoview_streaming'
+ record_in_processes:
+ - 'main'
+ release_channel_collection: opt-out
+```
+
+You would migrate it to a `quantity` like:
+
+```yaml
+gfx.display:
+ primary_height:
+ type: quantity
+ unit: pixels
+ description: >
+ Height of the primary display, takes device rotation into account.
+ Migrated from Telemetry's `gfx.display.primary_height`.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1594145
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1687219
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1594145#c4
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - gfx-telemetry-alerts@mozilla.com
+ expires: never
+```
+
+Note the required `unit` property.
+
+**GIFFT:** This type of collection is mirrorable back to Firefox Telemetry via the
+[Glean Interface For Firefox Telemetry][gifft].
+See [the guide][gifft] for instructions.
+
+**IPC Note:** Due to `set` not being a [commutative operation][ipc-docs], using `quantity`
+on non-parent processes is forbidden.
+This is a restriction that favours correctness over friendliness,
+which we may revisit if enough use cases require it.
+Please [contact us][glean-matrix] if you'd like us to do so.
+
+#### Keyed Scalars of `kind: uint` that you call `scalarSet` on - Ask on #glean:mozilla.org for assistance
+
+Glean doesn't currently have a good metric type for keyed quantities.
+Please [reach out to us][glean-matrix] to explain your use-case.
+We will help you either work within what Glean currently affords or
+[design a new metric type for you][new-metric-type].
+
+### Scalars of `kind: uint` that you call `scalarSetMaximum` or some combination of operations on - Ask on #glean:mozilla.org for assistance
+
+Glean doesn't currently have a good metric type for dealing with maximums,
+or for dealing with values you both count and set.
+Please [reach out to us][glean-matrix] to explain your use-case.
+We will help you either work within what Glean currently affords or
+[design a new metric type for you][new-metric-type].
+
+### Scalars of `kind: string` - Use Glean's `string`
+
+If your string value is a unique identifier, then consider
+[Glean's `uuid` metric type][uuid-metric] first.
+
+If the string scalar value doesn't fit that or any other more specific metric type,
+then [Glean's `string` metric type][string-metric] will do.
+
+For a Scalar of `kind: string` like:
+
+```yaml
+widget:
+ gtk_version:
+ bug_numbers:
+ - 1670145
+ description: >
+ The version of Gtk 3 in use.
+ kind: string
+ expires: never
+ notification_emails:
+ - layout-telemetry-alerts@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+```
+
+You will migrate it to a `string` metric like:
+
+```yaml
+widget:
+ gtk_version:
+ type: string
+ description: >
+ The version of Gtk 3 in use.
+ Migrated from Telemetry's `widget.gtk_version`.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1670145
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1670145#c7
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - layout-telemetry-alerts@mozilla.com
+ expires: never
+```
+
+**GIFFT:** This type of collection is mirrorable back to Firefox Telemetry via the
+[Glean Interface For Firefox Telemetry][gifft].
+See [the guide][gifft] for instructions.
+
+**IPC Note:** Due to `set` not being a [commutative operation][ipc-docs], using `string`
+on non-parent processes is forbidden.
+This is a restriction that favours correctness over friendliness,
+which we may revisit if enough use cases require it.
+Please [contact us][glean-matrix] if you'd like us to do so.
+
+### Scalars of `kind: boolean` - Use Glean's `boolean`
+
+If you need to store a simple true/false,
+[Glean's `boolean` metric type][boolean-metric] is likely best.
+
+If you have more that just `true` and `false` to store,
+you may prefer a `labeled_counter`.
+
+For a Scalar of `kind: boolean` like:
+
+```yaml
+widget:
+ dark_mode:
+ bug_numbers:
+ - 1601846
+ description: >
+ Whether the OS theme is dark.
+ expires: never
+ kind: boolean
+ notification_emails:
+ - layout-telemetry-alerts@mozilla.com
+ - cmccormack@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'main'
+```
+
+You would migrate to a `boolean` metric type like:
+
+```yaml
+widget:
+ dark_mode:
+ type: boolean
+ description: >
+ Whether the OS theme is dark.
+ Migrated from Telemetry's `widget.dark_mode`.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1601846
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1601846#c5
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - layout-telemetry-alerts@mozilla.com
+ - cmccormack@mozilla.com
+ expires: never
+```
+
+**GIFFT:** This type of collection is mirrorable back to Firefox Telemetry via the
+[Glean Interface For Firefox Telemetry][gifft].
+See [the guide][gifft] for instructions.
+
+**IPC Note:** Due to `set` not being a [commutative operation][ipc-docs], using `boolean`
+on non-parent processes is forbidden.
+This is a restriction that favours correctness over friendliness,
+which we may revisit if enough use cases require it.
+Please [contact us][glean-matrix] if you'd like us to do so.
+
+#### Keyed Scalars of `kind: boolean` - Use Glean's `labeled_boolean`
+
+If you have multiple related true/false values, you may have put them in a
+Keyed Scalar of `kind: boolean`.
+
+The best match for this is
+[Glean's `labeled_boolean` metric type][labeled-boolean-metric].
+
+For a Keyed Scalar of `kind: boolean` like:
+
+```yaml
+devtools.tool:
+ registered:
+ bug_numbers:
+ - 1447302
+ - 1503568
+ - 1587985
+ description: >
+ Recorded on enable tool checkbox check/uncheck in Developer Tools options
+ panel. Boolean stating if the tool was enabled or disabled by the user.
+ Keyed by tool id. Current default tools with their id's are defined in
+ https://searchfox.org/mozilla-central/source/devtools/client/definitions.js
+ expires: never
+ kind: boolean
+ keyed: true
+ notification_emails:
+ - dev-developer-tools@lists.mozilla.org
+ - accessibility@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'main'
+```
+
+You would migrate to a `labeled_boolean` like:
+
+```yaml
+devtools.tool:
+ registered:
+ type: labeled_boolean
+ description: >
+ Recorded on enable tool checkbox check/uncheck in Developer Tools options
+ panel. Boolean stating if the tool was enabled or disabled by the user.
+ Migrated from Telemetry's `devtools.tool`.
+ labels:
+ - options
+ - inspector
+ - webconsole
+ - jsdebugger
+ - styleeditor
+ - performance
+ - memory
+ - netmonitor
+ - storage
+ - dom
+ - accessibility
+ - application
+ - dark
+ - light
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1447302
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1503568
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1587985
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1447302#c17
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1503568#c3
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1587985#c5
+ data_sensitivity:
+ - interaction
+ notification_emails:
+ - dev-developer-tools@lists.mozilla.org
+ - accessibility@mozilla.com
+ expires: never
+```
+
+**N.B:** Glean Labels have a strict regex.
+You may have to transform some categories to
+`snake_case` so that they're safe for the data pipeline.
+
+**GIFFT:** This type of collection is mirrorable back to Firefox Telemetry via the
+[Glean Interface For Firefox Telemetry][gifft].
+See [the guide][gifft] for instructions.
+
+**IPC Note:** Due to `set` not being a [commutative operation][ipc-docs], using `labeled_boolean`
+on non-parent processes is forbidden.
+This is a restriction that favours correctness over friendliness,
+which we may revisit if enough use cases require it.
+Please [contact us][glean-matrix] if you'd like us to do so.
+
+### Other Scalar-ish types: `rate`, `timespan`, `datetime`, `uuid`
+
+The Glean SDK provides some very handy higher-level metric types for specific data.
+If your data
+* Is two or more numbers that are related (like failure count vs total count),
+ then consider the [Glean `rate` metric type][rate-metric].
+* Is a single duration or span of time (like how long Firefox takes to start),
+ then consider the [Glean `timespan` metric type][timespan-metric].
+* Is a single point in time (like the most recent sync time),
+ then consider the [Glean `datetime` metric type][datetime-metric].
+* Is a unique identifier (like a session id),
+ then consider the [Glean `uuid` metric type][uuid-metric].
+
+**GIFFT:** These types of collection are mirrorable back to Firefox Telemetry via the
+[Glean Interface For Firefox Telemetry][gifft].
+See [the guide][gifft] for instructions.
+
+## Events - Use Glean's `event`
+
+[Telemetry Events][telemetry-events]
+are a lesser-used form of data collection in Firefox Desktop.
+Glean aimed to remove some of the stumbling blocks facing instrumentors when using events
+in the [Glean `event` metric type][event-metric]:
+
+* Don't worry about enabling event categories.
+ In Glean all `events` are always on.
+* No more event `name`.
+ Events in Glean follow the same `category.name.metric_name`
+ naming structure that other metrics do.
+* No more `method`/`object`/`value`.
+ Events in Glean are just their identifier and an `extras` key/value dictionary.
+
+Since the two Event types aren't that analogous you will need to decide if your event
+* Prefers to put its `method`/`object`/`value` in the `extras` dictionary
+* Prefers to fold its `method`/`object`/`value` into its identifier
+
+**GIFFT:** Events are mirrorable back to Firefox Telemetry via the
+[Glean Interface For Firefox Telemetry][gifft].
+See [the guide][gifft] for instructions.
+
+## Other: Environment, Crash Annotations, Use Counters, Etc - Ask on #glean:mozilla.org for assistance
+
+Telemetry has a lot of collection subsystems build adjacent to those already mentioned.
+We have solutions for the common ones,
+but they are entirely dependent on the specific use case.
+Please [reach out to us][glean-matrix] to explain it to us so we can help you either
+work within what Glean currently affords or
+[design a new metric type for you][new-metric-type].
+
+[book-of-glean]: https://mozilla.github.io/glean/book/index.html
+[gc-ms]: https://glam.telemetry.mozilla.org/firefox/probe/gc_ms/explore
+[histogram-accumulate]: https://searchfox.org/mozilla-central/rev/d59bdea4956040e16113b05296c56867f761735b/toolkit/components/telemetry/core/Telemetry.h#61
+[ipc-docs]: ../dev/ipc.md
+[gifft]: gifft.md
+[memory-total]: https://glam.telemetry.mozilla.org/firefox/probe/memory_total/explore
+[migration-worksheet]: https://docs.google.com/spreadsheets/d/1uEK7zSIJDcGGmof9NywP5AwaovVQCv_Bm3iNqibtESI/edit#gid=0
+[boolean-metric]: https://mozilla.github.io/glean/book/reference/metrics/boolean.html
+[labeled-boolean-metric]: https://mozilla.github.io/glean/book/reference/metrics/labeled_booleans.html
+[counter-metric]: https://mozilla.github.io/glean/book/reference/metrics/counter.html
+[labeled-counter-metric]: https://mozilla.github.io/glean/book/reference/metrics/labeled_counters.html
+[string-metric]: https://mozilla.github.io/glean/book/reference/metrics/string.html
+[labeled-string-metric]: https://mozilla.github.io/glean/book/reference/metrics/labeled_strings.html
+[timespan-metric]: https://mozilla.github.io/glean/book/reference/metrics/timespan.html
+[timing-distribution-metric]: https://mozilla.github.io/glean/book/reference/metrics/timing_distribution.html
+[memory-distribution-metric]: https://mozilla.github.io/glean/book/reference/metrics/memory_distribution.html
+[uuid-metric]: https://mozilla.github.io/glean/book/reference/metrics/uuid.html
+[datetime-metric]: https://mozilla.github.io/glean/book/reference/metrics/datetime.html
+[event-metric]: https://mozilla.github.io/glean/book/reference/metrics/event.html
+[custom-distribution-metric]: https://mozilla.github.io/glean/book/reference/metrics/custom_distribution.html
+[quantity-metric]: https://mozilla.github.io/glean/book/reference/metrics/quantity.html
+[rate-metric]: https://mozilla.github.io/glean/book/reference/metrics/rate.html
+[ipc-dev-doc]: ../dev/ipc.md
+[gc-idle]: https://glam.telemetry.mozilla.org/firefox/probe/gc_slice_during_idle/explore
+[new-metric-keyed-categorical]: https://bugzilla.mozilla.org/show_bug.cgi?id=1657470
+[new-metric-percent]: https://bugzilla.mozilla.org/show_bug.cgi?id=1657467
+[new-metric-type]: https://wiki.mozilla.org/Glean/Adding_or_changing_Glean_metric_types
+[glean-matrix]: https://chat.mozilla.org/#/room/#glean:mozilla.org
+[checkerboard-severity]: https://searchfox.org/mozilla-central/rev/d59bdea4956040e16113b05296c56867f761735b/gfx/layers/apz/src/CheckerboardEvent.cpp#44
+[telemetry-events]: /toolkit/components/telemetry/collection/events.rst
+[telemetry-scalars]: /toolkit/components/telemetry/collection/scalars.rst
+[telemetry-histograms]: /toolkit/components/telemetry/collection/histograms.rst
+[repositories-yaml]: https://github.com/mozilla/probe-scraper/blob/main/repositories.yaml
diff --git a/toolkit/components/glean/docs/user/new_definitions_file.md b/toolkit/components/glean/docs/user/new_definitions_file.md
new file mode 100644
index 0000000000..b9d1ada8f3
--- /dev/null
+++ b/toolkit/components/glean/docs/user/new_definitions_file.md
@@ -0,0 +1,116 @@
+# New Metrics and Pings
+
+To add a new metric or ping to Firefox Desktop you should follow the
+[Glean SDK documentation on the subject](https://mozilla.github.io/glean/book/user/adding-new-metrics.html),
+with some few twists we detail herein:
+
+## Testing
+
+Instrumentation, being code, should be tested.
+Firefox on Glean [supports a wide variety of Firefox Desktop test suites][instrumentation-tests]
+in addition to [Glean's own debugging mechanisms][glean-debug].
+
+## IPC
+
+Firefox Desktop is made of multiple processes.
+You can record data from any process in Firefox Desktop
+[subject to certain conditions](../dev/ipc.md).
+
+If you will be recording data to this metric in multiple processes,
+you should make yourself aware of those conditions.
+
+## Where do I Define new Metrics and Pings?
+
+Metrics and pings are defined in their definitions files
+(`metrics.yaml` or `pings.yaml`, respectively).
+But where can you find `metrics.yaml` or `pings.yaml`?
+
+If you're not the first person in your component to ask that question,
+the answer is likely "in the root of your component".
+Look for the definitions files near to where you are instrumenting your code.
+Or you can look in
+`toolkit/components/glean/metrics_index.py`
+to see the list of all currently-known definitions files.
+
+If you _are_ the first person in your component to ask that question,
+you get to choose where to start them!
+We recommend adding them in the root of your component, next to a `moz.build`.
+Be sure to link to this document at the top of the file!
+It contains many useful tidbits of information that anyone adding new metrics should know.
+Preferably, use this blank template to get started,
+substituting your component's `product :: component` tag from
+[the list](https://searchfox.org/mozilla-central/source/toolkit/components/glean/tags.yaml):
+
+```yaml
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+# Adding a new metric? We have docs for that!
+# https://firefox-source-docs.mozilla.org/toolkit/components/glean/user/new_definitions_file.html
+
+---
+$schema: moz://mozilla.org/schemas/glean/metrics/2-0-0
+$tags:
+ - 'Your Product :: Your Component'
+
+```
+
+If you add a new definitions file, be sure to edit
+`toolkit/components/glean/metrics_index.py`,
+adding your definitions files to the Python lists therein.
+If you don't, no API will be generated for your metrics and your build will fail.
+You will have to decide which products your metrics will be used in.
+For code that's also used in other Gecko-based products (Firefox Desktop, Firefox for Android, Focus for Android), use `gecko_metrics`.
+For Desktop-only instrumentation use `firefox_desktop_metrics`.
+For other products use their respective lists.
+
+Changes to `metrics_index.py` are automatically reflected in the data pipeline once a day
+using the [fog-updater automation in probe-scraper](https://github.com/mozilla/probe-scraper/tree/main/fog-updater).
+Data will not show up in datasets and tools until this happens.
+If something is unclear or data is not showing up in time you will need to file a bug in
+`Data Platform and Tools :: General`.
+
+If you have any questions, be sure to ask on
+[the #glean channel](https://chat.mozilla.org/#/room/#glean:mozilla.org).
+
+**Note:** Do _not_ use `toolkit/components/glean/metrics.yaml`
+or `toolkit/components/glean/pings.yaml`.
+These are for metrics instrumenting the code under `toolkit/components/glean`
+and are not general-purpose locations for adding metrics and pings.
+
+## How does Expiry Work?
+
+In FOG,
+unlike in other Glean-SDK-using projects,
+metrics expire based on Firefox application version.
+This is to allow metrics to be valid over the entire life of an application version,
+whether that is the 4-6 weeks of usual releases or the 13 months of ESR releases.
+
+There are three values accepted in the `expires` field of `metrics.yaml`s for FOG:
+* `"X"` (where `X` is the major portion of a Firefox Desktop version) -
+ The metric will be expired when the `MOZ_APP_VERSION` reaches or exceeds `X`.
+ (For example, when the Firefox Version is `88.0a1`,
+ all metrics marked with `expires: "88"` or lower will be expired.)
+ This is the recommended form for all new metrics to ensure they stop recording when they stop being relevant.
+* `expired` - For marking a metric as manually expired.
+ Not usually used, but sometimes helpful for internal tests.
+* `never` - For marking a metric as part of a permanent data collection.
+ Metrics marked with `never` must have
+ [instrumentation tests](instrumentation_tests).
+
+For more information on what expiry means and the
+`metrics.yaml` format, see
+[the Glean SDK docs](https://mozilla.github.io/glean/book/user/metric-parameters.html)
+on this subject. Some quick facts:
+
+* Data collected to expired metrics is not recorded or sent.
+* Recording to expired metrics is not an error at runtime.
+* Expired metrics being in a `metrics.yaml` is a linting error in `glean_parser`.
+* Expired (and non-expired) metrics that are no longer useful should be promptly removed from your `metrics.yaml`.
+ This reduces the size and improves the performance of Firefox
+ (and speeds up the Firefox build process)
+ by decreasing the amount of code that needs to be generated.
+
+[instrumentation-tests]: ./instrumentation_tests
+[glean-debug]: https://mozilla.github.io/glean/book/reference/debug/index.html
diff --git a/toolkit/components/glean/ipc/FOGIPC.cpp b/toolkit/components/glean/ipc/FOGIPC.cpp
new file mode 100644
index 0000000000..e44cf3e863
--- /dev/null
+++ b/toolkit/components/glean/ipc/FOGIPC.cpp
@@ -0,0 +1,575 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "FOGIPC.h"
+
+#include <limits>
+#include "mozilla/glean/fog_ffi_generated.h"
+#include "mozilla/glean/GleanMetrics.h"
+#include "mozilla/dom/BrowsingContextGroup.h"
+#include "mozilla/dom/ContentChild.h"
+#include "mozilla/dom/ContentParent.h"
+#include "mozilla/dom/DocGroup.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/gfx/GPUChild.h"
+#include "mozilla/gfx/GPUParent.h"
+#include "mozilla/gfx/GPUProcessManager.h"
+#include "mozilla/Hal.h"
+#include "mozilla/MozPromise.h"
+#include "mozilla/net/SocketProcessChild.h"
+#include "mozilla/net/SocketProcessParent.h"
+#include "mozilla/ProcInfo.h"
+#include "mozilla/RDDChild.h"
+#include "mozilla/RDDParent.h"
+#include "mozilla/RDDProcessManager.h"
+#include "mozilla/ipc/UtilityProcessChild.h"
+#include "mozilla/ipc/UtilityProcessManager.h"
+#include "mozilla/ipc/UtilityProcessParent.h"
+#include "mozilla/ipc/UtilityProcessSandboxing.h"
+#include "mozilla/Unused.h"
+#include "GMPPlatform.h"
+#include "GMPServiceParent.h"
+#include "nsIClassifiedChannel.h"
+#include "nsIXULRuntime.h"
+#include "nsTArray.h"
+#include "nsThreadUtils.h"
+
+using mozilla::dom::ContentParent;
+using mozilla::gfx::GPUChild;
+using mozilla::gfx::GPUProcessManager;
+using mozilla::ipc::ByteBuf;
+using mozilla::ipc::UtilityProcessChild;
+using mozilla::ipc::UtilityProcessManager;
+using mozilla::ipc::UtilityProcessParent;
+using FlushFOGDataPromise = mozilla::dom::ContentParent::FlushFOGDataPromise;
+
+namespace geckoprofiler::markers {
+
+using namespace mozilla;
+
+struct ProcessingTimeMarker {
+ static constexpr Span<const char> MarkerTypeName() {
+ return MakeStringSpan("ProcessingTime");
+ }
+ static void StreamJSONMarkerData(baseprofiler::SpliceableJSONWriter& aWriter,
+ int64_t aDiffMs,
+ const ProfilerString8View& aType,
+ const ProfilerString8View& aTrackerType) {
+ aWriter.IntProperty("time", aDiffMs);
+ aWriter.StringProperty("label", aType);
+ if (aTrackerType.Length() > 0) {
+ aWriter.StringProperty("tracker", aTrackerType);
+ }
+ }
+ static MarkerSchema MarkerTypeDisplay() {
+ using MS = MarkerSchema;
+ MS schema{MS::Location::MarkerChart, MS::Location::MarkerTable};
+ schema.AddKeyLabelFormat("time", "Recorded Time", MS::Format::Milliseconds);
+ schema.AddKeyLabelFormat("tracker", "Tracker Type", MS::Format::String);
+ schema.SetTooltipLabel("{marker.name} - {marker.data.label}");
+ schema.SetTableLabel(
+ "{marker.name} - {marker.data.label}: {marker.data.time}");
+ return schema;
+ }
+};
+
+} // namespace geckoprofiler::markers
+
+namespace mozilla::glean {
+
+// Echoes processtools/metrics.yaml's power.wakeups_per_thread
+enum ProcessType {
+ eParentActive,
+ eParentInactive,
+ eContentForeground,
+ eContentBackground,
+ eGpuProcess,
+ eUnknown,
+};
+
+// This static global is set within RecordPowerMetrics on the main thread,
+// using information only gettable on the main thread, and is read within
+// RecordThreadCpuUse on any thread.
+static Atomic<ProcessType> gThisProcessType(eUnknown);
+
+#ifdef NIGHTLY_BUILD
+// It is fine to call RecordThreadCpuUse during startup before the first
+// RecordPowerMetrics call. In that case the parent process will be recorded
+// as inactive, and other processes will be ignored (content processes start
+// in the 'prealloc' type for which we don't record per-thread CPU use data).
+
+void RecordThreadCpuUse(const nsACString& aThreadName, uint64_t aCpuTimeMs,
+ uint64_t aWakeCount) {
+ ProcessType processType = gThisProcessType;
+
+ if (processType == ProcessType::eUnknown) {
+ if (XRE_IsParentProcess()) {
+ // During startup we might not have gotten a RecordPowerMetrics call.
+ // That's fine. Default to eParentInactive.
+ processType = eParentInactive;
+ } else {
+ // We are not interested in per-thread CPU use data for the current
+ // process type.
+ return;
+ }
+ }
+
+ nsAutoCString threadName(aThreadName);
+ for (size_t i = 0; i < threadName.Length(); ++i) {
+ const char c = threadName.CharAt(i);
+
+ // Valid characters.
+ if ((c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || c == '-' ||
+ c == '_') {
+ continue;
+ }
+
+ // Should only use lower case characters
+ if (c >= 'A' && c <= 'Z') {
+ threadName.SetCharAt(c + ('a' - 'A'), i);
+ continue;
+ }
+
+ // Replace everything else with _
+ threadName.SetCharAt('_', i);
+ }
+
+ if (aCpuTimeMs != 0 &&
+ MOZ_LIKELY(aCpuTimeMs < std::numeric_limits<int32_t>::max())) {
+ switch (processType) {
+ case eParentActive:
+ power_cpu_ms_per_thread::parent_active.Get(threadName)
+ .Add(int32_t(aCpuTimeMs));
+ break;
+ case eParentInactive:
+ power_cpu_ms_per_thread::parent_inactive.Get(threadName)
+ .Add(int32_t(aCpuTimeMs));
+ break;
+ case eContentForeground:
+ power_cpu_ms_per_thread::content_foreground.Get(threadName)
+ .Add(int32_t(aCpuTimeMs));
+ break;
+ case eContentBackground:
+ power_cpu_ms_per_thread::content_background.Get(threadName)
+ .Add(int32_t(aCpuTimeMs));
+ break;
+ case eGpuProcess:
+ power_cpu_ms_per_thread::gpu_process.Get(threadName)
+ .Add(int32_t(aCpuTimeMs));
+ break;
+ case eUnknown:
+ // Nothing to do.
+ break;
+ }
+ }
+
+ if (aWakeCount != 0 &&
+ MOZ_LIKELY(aWakeCount < std::numeric_limits<int32_t>::max())) {
+ switch (processType) {
+ case eParentActive:
+ power_wakeups_per_thread::parent_active.Get(threadName)
+ .Add(int32_t(aWakeCount));
+ break;
+ case eParentInactive:
+ power_wakeups_per_thread::parent_inactive.Get(threadName)
+ .Add(int32_t(aWakeCount));
+ break;
+ case eContentForeground:
+ power_wakeups_per_thread::content_foreground.Get(threadName)
+ .Add(int32_t(aWakeCount));
+ break;
+ case eContentBackground:
+ power_wakeups_per_thread::content_background.Get(threadName)
+ .Add(int32_t(aWakeCount));
+ break;
+ case eGpuProcess:
+ power_wakeups_per_thread::gpu_process.Get(threadName)
+ .Add(int32_t(aWakeCount));
+ break;
+ case eUnknown:
+ // Nothing to do.
+ break;
+ }
+ }
+}
+#endif
+
+void GetTrackerType(nsAutoCString& aTrackerType) {
+ using namespace mozilla::dom;
+ uint32_t trackingFlags =
+ (nsIClassifiedChannel::CLASSIFIED_CRYPTOMINING |
+ nsIClassifiedChannel::CLASSIFIED_FINGERPRINTING |
+ nsIClassifiedChannel::CLASSIFIED_TRACKING |
+ nsIClassifiedChannel::CLASSIFIED_TRACKING_AD |
+ nsIClassifiedChannel::CLASSIFIED_TRACKING_ANALYTICS |
+ nsIClassifiedChannel::CLASSIFIED_TRACKING_SOCIAL);
+ AutoTArray<RefPtr<BrowsingContextGroup>, 5> bcGroups;
+ BrowsingContextGroup::GetAllGroups(bcGroups);
+ for (auto& bcGroup : bcGroups) {
+ AutoTArray<DocGroup*, 5> docGroups;
+ bcGroup->GetDocGroups(docGroups);
+ for (auto* docGroup : docGroups) {
+ for (Document* doc : *docGroup) {
+ nsCOMPtr<nsIClassifiedChannel> classifiedChannel =
+ do_QueryInterface(doc->GetChannel());
+ if (classifiedChannel) {
+ uint32_t classificationFlags =
+ classifiedChannel->GetThirdPartyClassificationFlags();
+ trackingFlags &= classificationFlags;
+ if (!trackingFlags) {
+ return;
+ }
+ }
+ }
+ }
+ }
+
+ // The if-elseif-else chain works because the tracker types listed here are
+ // currently mutually exclusive and should be maintained that way by policy.
+ if (trackingFlags == nsIClassifiedChannel::CLASSIFIED_TRACKING_AD) {
+ aTrackerType = "ad";
+ } else if (trackingFlags ==
+ nsIClassifiedChannel::CLASSIFIED_TRACKING_ANALYTICS) {
+ aTrackerType = "analytics";
+ } else if (trackingFlags ==
+ nsIClassifiedChannel::CLASSIFIED_TRACKING_SOCIAL) {
+ aTrackerType = "social";
+ } else if (trackingFlags == nsIClassifiedChannel::CLASSIFIED_CRYPTOMINING) {
+ aTrackerType = "cryptomining";
+ } else if (trackingFlags == nsIClassifiedChannel::CLASSIFIED_FINGERPRINTING) {
+ aTrackerType = "fingerprinting";
+ } else if (trackingFlags == nsIClassifiedChannel::CLASSIFIED_TRACKING) {
+ // CLASSIFIED_TRACKING means we were not able to identify the type of
+ // classification.
+ aTrackerType = "unknown";
+ }
+}
+
+void RecordPowerMetrics() {
+ static uint64_t previousCpuTime = 0, previousGpuTime = 0;
+
+ uint64_t cpuTime, newCpuTime = 0;
+ if (NS_SUCCEEDED(GetCpuTimeSinceProcessStartInMs(&cpuTime)) &&
+ cpuTime > previousCpuTime) {
+ newCpuTime = cpuTime - previousCpuTime;
+ }
+
+ uint64_t gpuTime, newGpuTime = 0;
+ // Avoid loading gdi32.dll for the Socket process where the GPU is never used.
+ if (!XRE_IsSocketProcess() &&
+ NS_SUCCEEDED(GetGpuTimeSinceProcessStartInMs(&gpuTime)) &&
+ gpuTime > previousGpuTime) {
+ newGpuTime = gpuTime - previousGpuTime;
+ }
+
+ if (!newCpuTime && !newGpuTime) {
+ // Nothing to record.
+ return;
+ }
+
+ // Compute the process type string.
+ nsAutoCString type(XRE_GetProcessTypeString());
+ nsAutoCString trackerType;
+ if (XRE_IsContentProcess()) {
+ auto* cc = dom::ContentChild::GetSingleton();
+ if (cc) {
+ type.Assign(dom::RemoteTypePrefix(cc->GetRemoteType()));
+ if (StringBeginsWith(type, WEB_REMOTE_TYPE)) {
+ type.AssignLiteral("web");
+ switch (cc->GetProcessPriority()) {
+ case hal::PROCESS_PRIORITY_BACKGROUND:
+ type.AppendLiteral(".background");
+ gThisProcessType = ProcessType::eContentBackground;
+ break;
+ case hal::PROCESS_PRIORITY_FOREGROUND:
+ type.AppendLiteral(".foreground");
+ gThisProcessType = ProcessType::eContentForeground;
+ break;
+ case hal::PROCESS_PRIORITY_BACKGROUND_PERCEIVABLE:
+ type.AppendLiteral(".background-perceivable");
+ gThisProcessType = ProcessType::eUnknown;
+ break;
+ default:
+ gThisProcessType = ProcessType::eUnknown;
+ break;
+ }
+ }
+ GetTrackerType(trackerType);
+ } else {
+ gThisProcessType = ProcessType::eUnknown;
+ }
+ } else if (XRE_IsParentProcess()) {
+ if (nsContentUtils::GetUserIsInteracting()) {
+ type.AssignLiteral("parent.active");
+ gThisProcessType = ProcessType::eParentActive;
+ } else {
+ type.AssignLiteral("parent.inactive");
+ gThisProcessType = ProcessType::eParentInactive;
+ }
+ hal::WakeLockInformation info;
+ GetWakeLockInfo(u"video-playing"_ns, &info);
+ if (info.numLocks() != 0 && info.numHidden() < info.numLocks()) {
+ type.AppendLiteral(".playing-video");
+ } else {
+ GetWakeLockInfo(u"audio-playing"_ns, &info);
+ if (info.numLocks()) {
+ type.AppendLiteral(".playing-audio");
+ }
+ }
+ } else if (XRE_IsGPUProcess()) {
+ gThisProcessType = ProcessType::eGpuProcess;
+ } else {
+ gThisProcessType = ProcessType::eUnknown;
+ }
+
+ if (newCpuTime) {
+ // The counters are reset at least once a day. Assuming all cores are used
+ // continuously, an int32 can hold the data for 24.85 cores.
+ // This should be fine for now, but may overflow in the future.
+ // Bug 1751277 tracks a newer, bigger counter.
+ int32_t nNewCpuTime = int32_t(newCpuTime);
+ if (newCpuTime < std::numeric_limits<int32_t>::max()) {
+ power::total_cpu_time_ms.Add(nNewCpuTime);
+ power::cpu_time_per_process_type_ms.Get(type).Add(nNewCpuTime);
+ if (!trackerType.IsEmpty()) {
+ power::cpu_time_per_tracker_type_ms.Get(trackerType).Add(nNewCpuTime);
+ }
+ } else {
+ power::cpu_time_bogus_values.Add(1);
+ }
+ PROFILER_MARKER("Process CPU Time", OTHER, {}, ProcessingTimeMarker,
+ nNewCpuTime, type, trackerType);
+ previousCpuTime += newCpuTime;
+ }
+
+ if (newGpuTime) {
+ int32_t nNewGpuTime = int32_t(newGpuTime);
+ if (newGpuTime < std::numeric_limits<int32_t>::max()) {
+ power::total_gpu_time_ms.Add(nNewGpuTime);
+ power::gpu_time_per_process_type_ms.Get(type).Add(nNewGpuTime);
+ } else {
+ power::gpu_time_bogus_values.Add(1);
+ }
+ PROFILER_MARKER("Process GPU Time", OTHER, {}, ProcessingTimeMarker,
+ nNewGpuTime, type, trackerType);
+ previousGpuTime += newGpuTime;
+ }
+
+ profiler_record_wakeup_count(type);
+}
+
+/**
+ * Flush your data ASAP, either because the parent process is asking you to
+ * or because the process is about to shutdown.
+ *
+ * @param aResolver - The function you need to call with the bincoded,
+ * serialized payload that the Rust impl hands you.
+ */
+void FlushFOGData(std::function<void(ipc::ByteBuf&&)>&& aResolver) {
+ // Record power metrics right before data is sent to the parent.
+ RecordPowerMetrics();
+
+ ByteBuf buf;
+ uint32_t ipcBufferSize = impl::fog_serialize_ipc_buf();
+ bool ok = buf.Allocate(ipcBufferSize);
+ if (!ok) {
+ return;
+ }
+ uint32_t writtenLen = impl::fog_give_ipc_buf(buf.mData, buf.mLen);
+ if (writtenLen != ipcBufferSize) {
+ return;
+ }
+ aResolver(std::move(buf));
+}
+
+/**
+ * Called by FOG on the parent process when it wants to flush all its
+ * children's data.
+ * @param aResolver - The function that'll be called with the results.
+ */
+void FlushAllChildData(
+ std::function<void(nsTArray<ipc::ByteBuf>&&)>&& aResolver) {
+ auto timerId = fog_ipc::flush_durations.Start();
+
+ nsTArray<ContentParent*> parents;
+ ContentParent::GetAll(parents);
+ nsTArray<RefPtr<FlushFOGDataPromise>> promises;
+ for (auto* parent : parents) {
+ promises.EmplaceBack(parent->SendFlushFOGData());
+ }
+
+ if (GPUProcessManager* gpuManager = GPUProcessManager::Get()) {
+ if (GPUChild* gpuChild = gpuManager->GetGPUChild()) {
+ promises.EmplaceBack(gpuChild->SendFlushFOGData());
+ }
+ }
+
+ if (RDDProcessManager* rddManager = RDDProcessManager::Get()) {
+ if (RDDChild* rddChild = rddManager->GetRDDChild()) {
+ promises.EmplaceBack(rddChild->SendFlushFOGData());
+ }
+ }
+
+ if (net::SocketProcessParent* socketParent =
+ net::SocketProcessParent::GetSingleton()) {
+ promises.EmplaceBack(socketParent->SendFlushFOGData());
+ }
+
+ if (RefPtr<mozilla::gmp::GeckoMediaPluginServiceParent> gmps =
+ mozilla::gmp::GeckoMediaPluginServiceParent::GetSingleton()) {
+ // There can be multiple Gecko Media Plugin processes, but iterating
+ // through them requires locking a mutex and the IPCs need to be sent
+ // from a different thread, so it's better to let the
+ // GeckoMediaPluginServiceParent code do it for us.
+ gmps->SendFlushFOGData(promises);
+ }
+
+ if (RefPtr<UtilityProcessManager> utilityManager =
+ UtilityProcessManager::GetIfExists()) {
+ for (RefPtr<UtilityProcessParent>& parent :
+ utilityManager->GetAllProcessesProcessParent()) {
+ promises.EmplaceBack(parent->SendFlushFOGData());
+ }
+ }
+
+ if (promises.Length() == 0) {
+ // No child processes at the moment. Resolve synchronously.
+ fog_ipc::flush_durations.Cancel(std::move(timerId));
+ nsTArray<ipc::ByteBuf> results;
+ aResolver(std::move(results));
+ return;
+ }
+
+ // If fog.ipc.flush_failures ever gets too high:
+ // TODO: Don't throw away resolved data if some of the promises reject.
+ // (not sure how, but it'll mean not using ::All... maybe a custom copy of
+ // AllPromiseHolder? Might be impossible outside MozPromise.h)
+ FlushFOGDataPromise::All(GetCurrentSerialEventTarget(), promises)
+ ->Then(GetCurrentSerialEventTarget(), __func__,
+ [aResolver = std::move(aResolver), timerId](
+ FlushFOGDataPromise::AllPromiseType::ResolveOrRejectValue&&
+ aValue) {
+ fog_ipc::flush_durations.StopAndAccumulate(std::move(timerId));
+ if (aValue.IsResolve()) {
+ aResolver(std::move(aValue.ResolveValue()));
+ } else {
+ fog_ipc::flush_failures.Add(1);
+ nsTArray<ipc::ByteBuf> results;
+ aResolver(std::move(results));
+ }
+ });
+}
+
+/**
+ * A child process has sent you this buf as a treat.
+ * @param buf - a bincoded serialized payload that the Rust impl understands.
+ */
+void FOGData(ipc::ByteBuf&& buf) {
+ JOG::EnsureRuntimeMetricsRegistered();
+ fog_ipc::buffer_sizes.Accumulate(buf.mLen);
+ impl::fog_use_ipc_buf(buf.mData, buf.mLen);
+}
+
+/**
+ * Called by FOG on a child process when it wants to send a buf to the parent.
+ * @param buf - a bincoded serialized payload that the Rust impl understands.
+ */
+void SendFOGData(ipc::ByteBuf&& buf) {
+ switch (XRE_GetProcessType()) {
+ case GeckoProcessType_Content:
+ mozilla::dom::ContentChild::GetSingleton()->SendFOGData(std::move(buf));
+ break;
+ case GeckoProcessType_GMPlugin: {
+ mozilla::gmp::SendFOGData(std::move(buf));
+ } break;
+ case GeckoProcessType_GPU:
+ Unused << mozilla::gfx::GPUParent::GetSingleton()->SendFOGData(
+ std::move(buf));
+ break;
+ case GeckoProcessType_RDD:
+ Unused << mozilla::RDDParent::GetSingleton()->SendFOGData(std::move(buf));
+ break;
+ case GeckoProcessType_Socket:
+ Unused << net::SocketProcessChild::GetSingleton()->SendFOGData(
+ std::move(buf));
+ break;
+ case GeckoProcessType_Utility:
+ Unused << ipc::UtilityProcessChild::GetSingleton()->SendFOGData(
+ std::move(buf));
+ break;
+ default:
+ MOZ_ASSERT_UNREACHABLE("Unsuppored process type");
+ }
+}
+
+/**
+ * Called on the parent process to ask all child processes for data,
+ * sending it all down into Rust to be used.
+ */
+RefPtr<GenericPromise> FlushAndUseFOGData() {
+ // Record power metrics on the parent before sending requests to child
+ // processes.
+ RecordPowerMetrics();
+
+ RefPtr<GenericPromise::Private> ret = new GenericPromise::Private(__func__);
+ std::function<void(nsTArray<ByteBuf>&&)> resolver =
+ [ret](nsTArray<ByteBuf>&& bufs) {
+ for (ByteBuf& buf : bufs) {
+ FOGData(std::move(buf));
+ }
+ ret->Resolve(true, __func__);
+ };
+ FlushAllChildData(std::move(resolver));
+ return ret;
+}
+
+void TestTriggerMetrics(uint32_t aProcessType,
+ const RefPtr<dom::Promise>& promise) {
+ switch (aProcessType) {
+ case nsIXULRuntime::PROCESS_TYPE_GMPLUGIN: {
+ RefPtr<mozilla::gmp::GeckoMediaPluginServiceParent> gmps(
+ mozilla::gmp::GeckoMediaPluginServiceParent::GetSingleton());
+ gmps->TestTriggerMetrics()->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [promise]() { promise->MaybeResolveWithUndefined(); },
+ [promise]() { promise->MaybeRejectWithUndefined(); });
+ } break;
+ case nsIXULRuntime::PROCESS_TYPE_GPU:
+ mozilla::gfx::GPUProcessManager::Get()->TestTriggerMetrics()->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [promise]() { promise->MaybeResolveWithUndefined(); },
+ [promise]() { promise->MaybeRejectWithUndefined(); });
+ break;
+ case nsIXULRuntime::PROCESS_TYPE_RDD:
+ RDDProcessManager::Get()->TestTriggerMetrics()->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [promise]() { promise->MaybeResolveWithUndefined(); },
+ [promise]() { promise->MaybeRejectWithUndefined(); });
+ break;
+ case nsIXULRuntime::PROCESS_TYPE_SOCKET:
+ Unused << net::SocketProcessParent::GetSingleton()
+ ->SendTestTriggerMetrics()
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [promise]() { promise->MaybeResolveWithUndefined(); },
+ [promise]() { promise->MaybeRejectWithUndefined(); });
+ break;
+ case nsIXULRuntime::PROCESS_TYPE_UTILITY:
+ Unused << ipc::UtilityProcessManager::GetSingleton()
+ ->GetProcessParent(ipc::SandboxingKind::GENERIC_UTILITY)
+ ->SendTestTriggerMetrics()
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [promise]() { promise->MaybeResolveWithUndefined(); },
+ [promise]() { promise->MaybeRejectWithUndefined(); });
+ break;
+ default:
+ promise->MaybeRejectWithUndefined();
+ break;
+ }
+}
+
+} // namespace mozilla::glean
diff --git a/toolkit/components/glean/ipc/FOGIPC.h b/toolkit/components/glean/ipc/FOGIPC.h
new file mode 100644
index 0000000000..3701f71838
--- /dev/null
+++ b/toolkit/components/glean/ipc/FOGIPC.h
@@ -0,0 +1,102 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef FOGIPC_h__
+#define FOGIPC_h__
+
+#include <functional>
+
+#include "mozilla/Maybe.h"
+#include "mozilla/MozPromise.h"
+#include "nsTArrayForwardDeclare.h"
+
+namespace mozilla {
+namespace ipc {
+class ByteBuf;
+} // namespace ipc
+} // namespace mozilla
+
+// This module provides the interface for FOG to communicate between processes.
+
+namespace mozilla {
+namespace glean {
+
+/**
+ * The parent process is asking you to flush your data ASAP.
+ *
+ * @param aResolver - The function you need to call with the bincoded,
+ * serialized payload that the Rust impl hands you.
+ */
+void FlushFOGData(std::function<void(ipc::ByteBuf&&)>&& aResolver);
+
+/**
+ * Called by FOG on the parent process when it wants to flush all its
+ * children's data.
+ * @param aResolver - The function that'll be called with the results.
+ */
+void FlushAllChildData(
+ std::function<void(nsTArray<ipc::ByteBuf>&&)>&& aResolver);
+
+/**
+ * A child process has sent you this buf as a treat.
+ * @param buf - a bincoded serialized payload that the Rust impl understands.
+ */
+void FOGData(ipc::ByteBuf&& buf);
+
+/**
+ * Called by FOG on a child process when it wants to send a buf to the parent.
+ * @param buf - a bincoded serialized payload that the Rust impl understands.
+ */
+void SendFOGData(ipc::ByteBuf&& buf);
+
+/**
+ * Called on the parent process to ask all child processes for data,
+ * sending it all down into Rust to be used.
+ *
+ * @returns a Promise that resolves when the data has made it to the parent.
+ */
+RefPtr<GenericPromise> FlushAndUseFOGData();
+
+/**
+ * ** Test-only Method **
+ *
+ * Trigger GMP, GPU, RDD or Socket process test instrumentation.
+ *
+ * @param processType - one of the PROCESS_TYPE_* constants from nsIXULRuntime.
+ * @param promise - a promise that will be resolved when the data has made it to
+ * the target process.
+ */
+void TestTriggerMetrics(uint32_t processType,
+ const RefPtr<dom::Promise>& promise);
+
+#ifdef NIGHTLY_BUILD
+/**
+ * This function records the CPU activity (CPU time used and wakeup count)
+ * of a specific thread. It is called only by profiler code, either multiple
+ * times in a row when RecordPowerMetrics asks the profiler to record
+ * the wakeup counts of all threads, or once when a thread is unregistered.
+ *
+ * @param aThreadName The name of the thread for which the CPU data is being
+ * recorded.
+ * The name will be converted to lower case, and characters
+ * that are not valid for glean labels will be replaced with
+ * '_'. The resulting name should be part of the
+ * per_thread_labels static list of labels defined in
+ * toolkit/components/processtools/metrics.yaml.
+ * @param aCpuTimeMs CPU time in miliseconds since the last time CPU use data
+ * was recorded for this thread.
+ * @param aWakeCount How many times the thread woke up since the previous time
+ * CPU use data was recorded for this thread.
+ */
+void RecordThreadCpuUse(const nsACString& aThreadName, uint64_t aCpuTimeMs,
+ uint64_t aWakeCount);
+#endif
+
+void RecordPowerMetrics();
+
+} // namespace glean
+} // namespace mozilla
+
+#endif // FOGIPC_h__
diff --git a/toolkit/components/glean/ipc/Support.cpp b/toolkit/components/glean/ipc/Support.cpp
new file mode 100644
index 0000000000..263c370cae
--- /dev/null
+++ b/toolkit/components/glean/ipc/Support.cpp
@@ -0,0 +1,76 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// This file is for support functions for the Rust IPC module.
+// Some information just isn't available to Rust and must be made available over
+// FFI.
+#include "FOGIPC.h"
+#include "mozilla/AppShutdown.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/glean/GleanMetrics.h"
+#include "mozilla/ipc/ByteBuf.h"
+#include "mozilla/Unused.h"
+#include "nsThreadUtils.h"
+
+using mozilla::AppShutdown;
+using mozilla::RunOnShutdown;
+using mozilla::ShutdownPhase;
+using mozilla::Unused;
+using mozilla::glean::FlushFOGData;
+using mozilla::glean::SendFOGData;
+using mozilla::ipc::ByteBuf;
+
+extern "C" {
+void FOG_RegisterContentChildShutdown() {
+ if (AppShutdown::IsInOrBeyond(ShutdownPhase::AppShutdownConfirmed)) {
+ return;
+ }
+
+ // If there is no main thread (too early in startup or too late in shutdown),
+ // there's nothing we can do but log.
+ bool failed =
+ NS_FAILED(NS_DispatchToMainThread(NS_NewRunnableFunction(__func__, [] {
+ // By the time the main thread dispatched this, it may already be too
+ // late.
+ if (AppShutdown::IsInOrBeyond(ShutdownPhase::AppShutdownConfirmed)) {
+ return;
+ }
+ RunOnShutdown(
+ [] {
+ FlushFOGData(
+ [](ByteBuf&& aBuf) { SendFOGData(std::move(aBuf)); });
+ },
+ ShutdownPhase::AppShutdownConfirmed);
+ })));
+ if (failed) {
+ NS_WARNING(
+ "Failed to register FOG content child shutdown flush. "
+ "Will lose shutdown data and leak a runnable.");
+ mozilla::glean::fog_ipc::shutdown_registration_failures.Add(1);
+ }
+}
+
+int FOG_GetProcessType() { return XRE_GetProcessType(); }
+
+/**
+ * Called from FOG IPC in Rust when the IPC Payload might be getting full.
+ * We should probably flush before we reach the max IPC message size.
+ */
+void FOG_IPCPayloadFull() {
+ // NS_DispatchToMainThread can leak the runnable (bug 1787589), so let's be
+ // sure not to create it too late in shutdown.
+ // We choose XPCOMShutdown to match gFOG->Shutdown().
+ if (AppShutdown::IsInOrBeyond(ShutdownPhase::XPCOMShutdown)) {
+ return;
+ }
+ // FOG IPC must happen on the main thread until bug 1641989.
+ // If there is no main thread (too early in startup or too late in shutdown),
+ // there's nothing we can do but log.
+ Unused << NS_WARN_IF(NS_FAILED(NS_DispatchToMainThread(
+ NS_NewRunnableFunction("FOG IPC Payload getting full", [] {
+ FlushFOGData([](ByteBuf&& aBuf) { SendFOGData(std::move(aBuf)); });
+ }))));
+}
+}
diff --git a/toolkit/components/glean/metrics.yaml b/toolkit/components/glean/metrics.yaml
new file mode 100644
index 0000000000..5ba2ac969f
--- /dev/null
+++ b/toolkit/components/glean/metrics.yaml
@@ -0,0 +1,170 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+# This file is for Internal FOG Use Only.
+# Please don't add anything here unless you have the permission of a
+# Telemetry Module Peer.
+
+---
+$schema: moz://mozilla.org/schemas/glean/metrics/2-0-0
+$tags:
+ - 'Toolkit :: Telemetry'
+
+fog:
+ initialization:
+ type: timespan
+ time_unit: nanosecond
+ description: |
+ Time the FOG initialization takes.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1662123
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1662123#c3
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - jrediger@mozilla.com
+ - glean-team@mozilla.com
+ expires: never
+
+ failed_idle_registration:
+ type: boolean
+ lifetime: application
+ description: |
+ True if we failed to register with the idle service. Absent otherwise.
+ Means IPC probably isn't working well.
+ Child-process data will likely be absent, or incomplete.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1694739
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1694739#c5
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - chutten@mozilla.com
+ - glean-team@mozilla.com
+ expires: never
+
+ inits_during_shutdown:
+ type: counter
+ lifetime: ping
+ description: |
+ Counts the number of times init had to be called during shutdown.
+ Should never have a value for any session long enough to grow idle.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1839433
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1839433
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - chutten@mozilla.com
+ - glean-team@mozilla.com
+ expires: never
+
+ max_pings_per_minute:
+ type: quantity
+ unit: int
+ description: |
+ Value of the configurable rate limit, in pings per minute, for Glean pings as set at init.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1834792
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1834792
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - pmcmanis@mozilla.com
+ - glean-team@mozilla.com
+ expires: never
+
+fog.ipc:
+ replay_failures:
+ type: counter
+ description: |
+ The number of times the ipc buffer failed to be replayed in the
+ parent process.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1664461
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1664461
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - chutten@mozilla.com
+ - glean-team@mozilla.com
+ expires: never
+
+ buffer_sizes:
+ type: memory_distribution
+ memory_unit: byte
+ description: |
+ The number and size of the IPC buffers being received over FOG IPC.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1694739
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1694739#c5
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - chutten@mozilla.com
+ - glean-team@mozilla.com
+ expires: never
+
+ flush_durations:
+ type: timing_distribution
+ time_unit: microsecond
+ description: |
+ The length of time between asking the child processes for their
+ IPC buffers and all of them being received by the parent.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1694739
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1694739#c5
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - chutten@mozilla.com
+ - glean-team@mozilla.com
+ expires: never
+
+ flush_failures:
+ type: counter
+ description: |
+ The number of times we failed to flush all non-parent-process data,
+ throwing even partial results into the trash.
+ If this number is high, we might consider writing custom `MozPromise`-
+ handling code instead of using `MozPromise::All`.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1729026
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1729026
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - chutten@mozilla.com
+ - glean-team@mozilla.com
+ expires: never
+
+ shutdown_registration_failures:
+ type: counter
+ description: |
+ The number of times we tried to register shutdown flush routines for
+ content child processes, and failed (probably because there was no main
+ thread).
+ As a result there may be data loss from content child processes.
+ Large or rising number of clients experiencing this indicates we should
+ perhaps refactor content child shutdown in FOG to try harder to register
+ flush operations.
+ Will likely be obsoleted by bug 1641989.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1766977
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1766977
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - chutten@mozilla.com
+ - glean-team@mozilla.com
+ expires: never
diff --git a/toolkit/components/glean/metrics_index.py b/toolkit/components/glean/metrics_index.py
new file mode 100644
index 0000000000..aa7eea6645
--- /dev/null
+++ b/toolkit/components/glean/metrics_index.py
@@ -0,0 +1,180 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+# ATTENTION: Changes to this file will need to be reflected in probe-scraper[1].
+# This should happen automatically once a day.
+# If something is unclear or data is not showing up in time
+# you will need to file a bug in Data Platform and Tools :: General.
+#
+# [1] https://github.com/mozilla/probe-scraper
+
+# Metrics that are sent by Gecko and everyone using Gecko
+# (Firefox Desktop, Firefox for Android, Focus for Android, etc.).
+# Order is lexicographical, enforced by t/c/glean/tests/pytest/test_yaml_indices.py
+gecko_metrics = [
+ "browser/base/content/metrics.yaml",
+ "docshell/base/metrics.yaml",
+ "dom/base/use_counter_metrics.yaml",
+ "dom/media/metrics.yaml",
+ "dom/media/webrtc/metrics.yaml",
+ "dom/metrics.yaml",
+ "dom/performance/metrics.yaml",
+ "gfx/metrics.yaml",
+ "image/decoders/metrics.yaml",
+ "js/xpconnect/metrics.yaml",
+ "layout/base/metrics.yaml",
+ "mobile/android/actors/metrics.yaml",
+ "mobile/android/modules/geckoview/metrics.yaml",
+ "netwerk/metrics.yaml",
+ "netwerk/protocol/http/metrics.yaml",
+ "security/manager/ssl/metrics.yaml",
+ "toolkit/components/cookiebanners/metrics.yaml",
+ "toolkit/components/extensions/metrics.yaml",
+ "toolkit/components/formautofill/metrics.yaml",
+ "toolkit/components/glean/metrics.yaml",
+ "toolkit/components/passwordmgr/metrics.yaml",
+ "toolkit/components/pdfjs/metrics.yaml",
+ "toolkit/components/processtools/metrics.yaml",
+ "toolkit/components/reportbrokensite/metrics.yaml",
+ "toolkit/components/resistfingerprinting/metrics.yaml",
+ "toolkit/components/translations/metrics.yaml",
+ "toolkit/mozapps/extensions/metrics.yaml",
+ "toolkit/mozapps/handling/metrics.yaml",
+ "toolkit/xre/metrics.yaml",
+ "xpcom/metrics.yaml",
+]
+
+# Metrics that are sent by Firefox Desktop
+# Order is lexicographical, enforced by t/c/glean/tests/pytest/test_yaml_indices.py
+firefox_desktop_metrics = [
+ "browser/components/metrics.yaml",
+ "browser/components/migration/metrics.yaml",
+ "browser/components/newtab/metrics.yaml",
+ "browser/components/pocket/metrics.yaml",
+ "browser/components/preferences/metrics.yaml",
+ "browser/components/privatebrowsing/metrics.yaml",
+ "browser/components/protocolhandler/metrics.yaml",
+ "browser/components/search/metrics.yaml",
+ "browser/components/shopping/metrics.yaml",
+ "browser/components/urlbar/metrics.yaml",
+ "browser/modules/metrics.yaml",
+ "dom/media/platforms/wmf/metrics.yaml",
+ "toolkit/components/crashes/metrics.yaml",
+ "toolkit/components/nimbus/metrics.yaml",
+ "toolkit/components/search/metrics.yaml",
+ "toolkit/components/shopping/metrics.yaml",
+ "toolkit/components/telemetry/dap/metrics.yaml",
+ "toolkit/components/telemetry/metrics.yaml",
+ "toolkit/modules/metrics.yaml",
+ "widget/cocoa/metrics.yaml",
+ "widget/windows/metrics.yaml",
+]
+
+# Metrics that are sent by the Firefox Desktop Background Update Task
+# Order is lexicographical, enforced by t/c/glean/tests/pytest/test_yaml_indices.py
+background_update_metrics = [
+ "toolkit/components/crashes/metrics.yaml",
+ "toolkit/mozapps/update/metrics.yaml",
+]
+
+# Metrics that are sent by the Firefox Desktop Background Tasks
+# Order is lexicographical, enforced by t/c/glean/tests/pytest/test_yaml_indices.py
+background_tasks_metrics = [
+ "browser/components/metrics.yaml",
+ "toolkit/components/backgroundtasks/metrics.yaml",
+ "toolkit/components/crashes/metrics.yaml",
+ "toolkit/mozapps/defaultagent/metrics.yaml",
+]
+
+# Test metrics
+# Order is lexicographical, enforced by t/c/glean/tests/pytest/test_yaml_indices.py
+test_metrics = [
+ "toolkit/components/glean/tests/test_metrics.yaml",
+]
+
+# The list of all Glean metrics.yaml files, relative to the top src dir.
+# ONLY TO BE MODIFIED BY FOG PEERS!
+metrics_yamls = sorted(
+ set(
+ gecko_metrics
+ + firefox_desktop_metrics
+ + background_update_metrics
+ + background_tasks_metrics
+ + test_metrics
+ )
+)
+
+# Pings that are sent by Gecko and everyone using Gecko
+# (Firefox Desktop, Firefox for Android, Focus for Android, etc.).
+# Order is lexicographical, enforced by t/c/glean/tests/pytest/test_yaml_indices.py
+gecko_pings = [
+ "dom/pings.yaml",
+ "toolkit/components/glean/pings.yaml",
+ "toolkit/components/reportbrokensite/pings.yaml",
+]
+
+# Pings that are sent by Firefox Desktop.
+# Order is lexicographical, enforced by t/c/glean/tests/pytest/test_yaml_indices.py
+firefox_desktop_pings = [
+ "browser/components/newtab/pings.yaml",
+ "browser/components/pocket/pings.yaml",
+ "browser/components/search/pings.yaml",
+ "browser/components/urlbar/pings.yaml",
+ "toolkit/components/crashes/pings.yaml",
+ "toolkit/components/telemetry/pings.yaml",
+ "toolkit/modules/pings.yaml",
+]
+
+# Pings that are sent by the Firefox Desktop Background Update Task
+# Order is lexicographical, enforced by t/c/glean/tests/pytest/test_yaml_indices.py
+background_update_pings = [
+ "toolkit/components/crashes/pings.yaml",
+ "toolkit/mozapps/update/pings.yaml",
+]
+
+# Pings that are sent by the Firefox Desktop Background Tasks
+# Order is lexicographical, enforced by t/c/glean/tests/pytest/test_yaml_indices.py
+background_tasks_pings = [
+ "toolkit/components/backgroundtasks/pings.yaml",
+ "toolkit/components/crashes/pings.yaml",
+ "toolkit/mozapps/defaultagent/pings.yaml",
+]
+
+# Test pings
+# Order is lexicographical, enforced by t/c/glean/tests/pytest/test_yaml_indices.py
+test_pings = [
+ "toolkit/components/glean/tests/test_pings.yaml",
+]
+
+# Map of app ids to lists of pings files for that app.
+# Necessary to ensure different apps don't store data for unsent pings.
+# Use the app id conjugation passed to initializeFOG (dotted.case).
+pings_by_app_id = {
+ "firefox.desktop": gecko_pings + firefox_desktop_pings + test_pings,
+ "firefox.desktop.background.update": gecko_pings
+ + background_update_pings
+ + test_pings,
+}
+
+# The list of all Glean pings.yaml files, relative to the top src dir.
+# ONLY TO BE MODIFIED BY FOG PEERS!
+pings_yamls = sorted(
+ set(
+ gecko_pings
+ + firefox_desktop_pings
+ + background_update_pings
+ + background_tasks_pings
+ + test_pings
+ )
+)
+
+# The list of tags that are allowed in the above to files, and their
+# descriptions. Currently we restrict to a set scraped from bugzilla
+# (via `./mach update-glean-tags`)
+# Order is lexicographical, enforced by t/c/glean/tests/pytest/test_yaml_indices.py
+tags_yamls = [
+ "toolkit/components/glean/tags.yaml",
+]
diff --git a/toolkit/components/glean/moz.build b/toolkit/components/glean/moz.build
new file mode 100644
index 0000000000..d6876167e6
--- /dev/null
+++ b/toolkit/components/glean/moz.build
@@ -0,0 +1,209 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+SPHINX_TREES["/toolkit/components/glean"] = "docs"
+
+# Needed so that we can include IPC things.
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "xul"
+
+EXPORTS.mozilla += [
+ "ipc/FOGIPC.h",
+]
+
+EXPORTS.mozilla.glean += [
+ "!GleanMetrics.h",
+ "!GleanPings.h",
+]
+
+EXPORTS.mozilla.glean.bindings += [
+ "!EventGIFFTMap.h",
+ "!GleanJSMetricsLookup.h",
+ "!GleanJSPingsLookup.h",
+ "!HistogramGIFFTMap.h",
+ "!ScalarGIFFTMap.h",
+ "bindings/Category.h",
+ "bindings/Glean.h",
+ "bindings/GleanMetric.h",
+ "bindings/GleanPings.h",
+ "bindings/MetricTypes.h",
+ "bindings/private/Boolean.h",
+ "bindings/private/Common.h",
+ "bindings/private/Counter.h",
+ "bindings/private/CustomDistribution.h",
+ "bindings/private/Datetime.h",
+ "bindings/private/Denominator.h",
+ "bindings/private/DistributionData.h",
+ "bindings/private/Event.h",
+ "bindings/private/Labeled.h",
+ "bindings/private/MemoryDistribution.h",
+ "bindings/private/Numerator.h",
+ "bindings/private/Ping.h",
+ "bindings/private/Quantity.h",
+ "bindings/private/Rate.h",
+ "bindings/private/String.h",
+ "bindings/private/StringList.h",
+ "bindings/private/Text.h",
+ "bindings/private/Timespan.h",
+ "bindings/private/TimingDistribution.h",
+ "bindings/private/Url.h",
+ "bindings/private/Uuid.h",
+]
+
+EXPORTS.mozilla.glean.bindings.jog += [
+ "bindings/jog/JOG.h",
+]
+
+if CONFIG["COMPILE_ENVIRONMENT"]:
+ EXPORTS.mozilla.glean += [
+ "!fog_ffi_generated.h",
+ ]
+
+ EXPORTS.mozilla.glean.bindings.jog += [
+ "!jog_ffi_generated.h",
+ ]
+
+ CbindgenHeader("fog_ffi_generated.h", inputs=["/toolkit/components/glean"])
+ CbindgenHeader(
+ "jog_ffi_generated.h", inputs=["/toolkit/components/glean/bindings/jog"]
+ )
+
+UNIFIED_SOURCES += [
+ "bindings/Category.cpp",
+ "bindings/Glean.cpp",
+ "bindings/GleanMetric.cpp",
+ "bindings/GleanPings.cpp",
+ "bindings/jog/JOG.cpp",
+ "bindings/private/Boolean.cpp",
+ "bindings/private/Common.cpp",
+ "bindings/private/Counter.cpp",
+ "bindings/private/CustomDistribution.cpp",
+ "bindings/private/Datetime.cpp",
+ "bindings/private/Denominator.cpp",
+ "bindings/private/Event.cpp",
+ "bindings/private/Labeled.cpp",
+ "bindings/private/MemoryDistribution.cpp",
+ "bindings/private/Numerator.cpp",
+ "bindings/private/Ping.cpp",
+ "bindings/private/Quantity.cpp",
+ "bindings/private/Rate.cpp",
+ "bindings/private/String.cpp",
+ "bindings/private/StringList.cpp",
+ "bindings/private/Text.cpp",
+ "bindings/private/Timespan.cpp",
+ "bindings/private/TimingDistribution.cpp",
+ "bindings/private/Url.cpp",
+ "bindings/private/Uuid.cpp",
+ "ipc/FOGIPC.cpp",
+ "ipc/Support.cpp",
+]
+
+SOURCES += [
+ "!EventExtraGIFFTMaps.cpp",
+ "!GleanJSMetricsLookup.cpp",
+ "!GleanJSPingsLookup.cpp",
+]
+
+# Provides us the list of dependent metrics|pings.yaml.
+include("metrics_index.py")
+# GeneratedFile's `inputs` are relative to our dir.
+# The yamls arrays are relative to topsrcdir, so we need to transform:
+metrics_yamls = ["../../../" + x for x in metrics_yamls]
+pings_yamls = ["../../../" + x for x in pings_yamls]
+tags_yamls = ["../../../" + x for x in tags_yamls]
+
+GeneratedFile(
+ "GleanMetrics.h",
+ "GleanJSMetricsLookup.h",
+ "GleanJSMetricsLookup.cpp",
+ "api/src/metrics.rs",
+ script="build_scripts/glean_parser_ext/run_glean_parser.py",
+ flags=[
+ CONFIG["MOZ_APP_VERSION"],
+ "GleanJSMetricsLookup.h",
+ "GleanJSMetricsLookup.cpp",
+ "api/src/metrics.rs",
+ ],
+ inputs=metrics_yamls + tags_yamls,
+)
+
+GeneratedFile(
+ "GleanPings.h",
+ "GleanJSPingsLookup.h",
+ "GleanJSPingsLookup.cpp",
+ "api/src/pings.rs",
+ script="build_scripts/glean_parser_ext/run_glean_parser.py",
+ flags=[
+ CONFIG["MOZ_APP_VERSION"],
+ "GleanJSPingsLookup.h",
+ "GleanJSPingsLookup.cpp",
+ "api/src/pings.rs",
+ ],
+ inputs=pings_yamls + tags_yamls,
+)
+
+# Glean Interface For Firefox Telemetry Maps from Glean MetricId to Telemetry ProbeId
+# We split it one map per header to avoid unused function warnings on build.
+GeneratedFile(
+ "EventGIFFTMap.h",
+ script="build_scripts/glean_parser_ext/run_glean_parser.py",
+ entry_point="gifft_map",
+ flags=[CONFIG["MOZ_APP_VERSION"], "Event"],
+ inputs=metrics_yamls + tags_yamls,
+)
+
+GeneratedFile(
+ "HistogramGIFFTMap.h",
+ script="build_scripts/glean_parser_ext/run_glean_parser.py",
+ entry_point="gifft_map",
+ flags=[CONFIG["MOZ_APP_VERSION"], "Histogram"],
+ inputs=metrics_yamls + tags_yamls,
+)
+
+GeneratedFile(
+ "ScalarGIFFTMap.h",
+ script="build_scripts/glean_parser_ext/run_glean_parser.py",
+ entry_point="gifft_map",
+ flags=[CONFIG["MOZ_APP_VERSION"], "Scalar"],
+ inputs=metrics_yamls + tags_yamls,
+)
+
+# JOG provides both the Rust factory for building runtime-registered metrics
+# and pings _and_ the YAML file used at runtime to register those metrics and
+# pings for Artifact Builds.
+# The factory lives inside the `fog` crate to avoid a circular dependency.
+GeneratedFile(
+ "api/src/factory.rs",
+ script="build_scripts/glean_parser_ext/run_glean_parser.py",
+ entry_point="jog_factory",
+ flags=[CONFIG["MOZ_APP_VERSION"]],
+ inputs=pings_yamls + metrics_yamls + tags_yamls,
+)
+
+# Only generate jogfile.json in Artifact Builds since
+# its presence triggers main-thread I/O.
+if CONFIG["MOZ_ARTIFACT_BUILDS"]:
+ GeneratedFile(
+ "jogfile.json",
+ script="build_scripts/glean_parser_ext/run_glean_parser.py",
+ entry_point="jog_file",
+ flags=[CONFIG["MOZ_APP_VERSION"]],
+ inputs=pings_yamls + metrics_yamls + tags_yamls,
+ )
+ # Once generated, it needs to be placed in GreD so it can be found.
+ FINAL_TARGET_FILES += ["!jogfile.json"]
+
+DIRS += [
+ "tests", # Must be in DIRS, not TEST_DIRS or python-test won't find it.
+ "xpcom",
+]
+
+with Files("docs/**"):
+ SCHEDULES.exclusive = ["docs"]
+
+with Files("**"):
+ BUG_COMPONENT = ("Toolkit", "Telemetry")
diff --git a/toolkit/components/glean/pings.yaml b/toolkit/components/glean/pings.yaml
new file mode 100644
index 0000000000..d4710a47c2
--- /dev/null
+++ b/toolkit/components/glean/pings.yaml
@@ -0,0 +1,13 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+# This file defines the pings that are recorded by the Glean SDK. They are
+# automatically converted to platform-specific code at build time using the
+# `glean_parser` PyPI package.
+
+# This file is presently for Internal FOG Use Only.
+# You should not add pings here until probably about January of 2021.
+
+---
+$schema: moz://mozilla.org/schemas/glean/pings/2-0-0
diff --git a/toolkit/components/glean/src/init/mod.rs b/toolkit/components/glean/src/init/mod.rs
new file mode 100644
index 0000000000..f430cd7384
--- /dev/null
+++ b/toolkit/components/glean/src/init/mod.rs
@@ -0,0 +1,368 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
+
+use std::env;
+use std::ffi::CString;
+use std::ops::DerefMut;
+use std::path::PathBuf;
+
+use firefox_on_glean::{metrics, pings};
+use nserror::{nsresult, NS_ERROR_FAILURE};
+use nsstring::{nsACString, nsCString, nsString};
+use xpcom::interfaces::{
+ mozILocaleService, nsIFile, nsIPrefService, nsIProperties, nsIXULAppInfo, nsIXULRuntime,
+};
+use xpcom::{RefPtr, XpCom};
+
+use glean::{ClientInfoMetrics, Configuration};
+
+#[cfg(not(target_os = "android"))]
+mod upload_pref;
+#[cfg(not(target_os = "android"))]
+mod user_activity;
+mod viaduct_uploader;
+
+#[cfg(not(target_os = "android"))]
+use upload_pref::UploadPrefObserver;
+#[cfg(not(target_os = "android"))]
+use user_activity::UserActivityObserver;
+use viaduct_uploader::ViaductUploader;
+
+/// Project FOG's entry point.
+///
+/// This assembles client information and the Glean configuration and then initializes the global
+/// Glean instance.
+#[cfg(not(target_os = "android"))]
+#[no_mangle]
+pub extern "C" fn fog_init(
+ data_path_override: &nsACString,
+ app_id_override: &nsACString,
+) -> nsresult {
+ let upload_enabled = static_prefs::pref!("datareporting.healthreport.uploadEnabled");
+ let recording_enabled = static_prefs::pref!("telemetry.fog.test.localhost_port") < 0;
+ let uploader = Some(Box::new(ViaductUploader) as Box<dyn glean::net::PingUploader>);
+
+ fog_init_internal(
+ data_path_override,
+ app_id_override,
+ upload_enabled || recording_enabled,
+ uploader,
+ )
+ .into()
+}
+
+/// Project FOG's entry point on Android.
+///
+/// This assembles client information and the Glean configuration and then initializes the global
+/// Glean instance.
+/// It always enables upload and set no uploader.
+/// This should only be called in test scenarios.
+/// In normal use Glean should be initialized and controlled by the Glean Kotlin SDK.
+#[cfg(target_os = "android")]
+#[no_mangle]
+pub extern "C" fn fog_init(
+ data_path_override: &nsACString,
+ app_id_override: &nsACString,
+) -> nsresult {
+ // On Android always enable Glean upload.
+ let upload_enabled = true;
+ // Don't set up an uploader.
+ let uploader = None;
+
+ fog_init_internal(
+ data_path_override,
+ app_id_override,
+ upload_enabled,
+ uploader,
+ )
+ .into()
+}
+
+fn fog_init_internal(
+ data_path_override: &nsACString,
+ app_id_override: &nsACString,
+ upload_enabled: bool,
+ uploader: Option<Box<dyn glean::net::PingUploader>>,
+) -> Result<(), nsresult> {
+ metrics::fog::initialization.start();
+
+ log::debug!("Initializing FOG.");
+
+ setup_observers()?;
+
+ let (mut conf, client_info) = build_configuration(data_path_override, app_id_override)?;
+
+ conf.upload_enabled = upload_enabled;
+ conf.uploader = uploader;
+
+ // If we're operating in automation without any specific source tags to set,
+ // set the tag "automation" so any pings that escape don't clutter the tables.
+ // See https://mozilla.github.io/glean/book/user/debugging/index.html#enabling-debugging-features-through-environment-variables
+ if env::var("MOZ_AUTOMATION").is_ok() && env::var("GLEAN_SOURCE_TAGS").is_err() {
+ log::info!("In automation, setting 'automation' source tag.");
+ glean::set_source_tags(vec!["automation".to_string()]);
+ log::info!("In automation, disabling MPS to avoid 4am issues.");
+ conf.use_core_mps = false;
+ }
+
+ log::debug!("Configuration: {:#?}", conf);
+
+ // Register all custom pings before we initialize.
+ pings::register_pings(Some(&conf.application_id));
+
+ glean::initialize(conf, client_info);
+
+ metrics::fog::initialization.stop();
+
+ Ok(())
+}
+
+fn build_configuration(
+ data_path_override: &nsACString,
+ app_id_override: &nsACString,
+) -> Result<(Configuration, ClientInfoMetrics), nsresult> {
+ let data_path_str = if data_path_override.is_empty() {
+ get_data_path()?
+ } else {
+ data_path_override.to_utf8().to_string()
+ };
+ let data_path = PathBuf::from(&data_path_str);
+
+ let (app_build, app_display_version, channel, locale) = get_app_info()?;
+
+ let client_info = ClientInfoMetrics {
+ app_build,
+ app_display_version,
+ channel: Some(channel),
+ locale: Some(locale),
+ };
+ log::debug!("Client Info: {:#?}", client_info);
+
+ const SERVER: &str = "https://incoming.telemetry.mozilla.org";
+ let localhost_port = static_prefs::pref!("telemetry.fog.test.localhost_port");
+ let server = if localhost_port > 0 {
+ format!("http://localhost:{}", localhost_port)
+ } else {
+ String::from(SERVER)
+ };
+
+ let application_id = if app_id_override.is_empty() {
+ "firefox.desktop".to_string()
+ } else {
+ app_id_override.to_utf8().to_string()
+ };
+
+ extern "C" {
+ fn FOG_MaxPingLimit() -> u32;
+ fn FOG_EventTimestampsEnabled() -> bool;
+ }
+
+ // SAFETY NOTE: Safe because it returns a primitive by value.
+ let pings_per_interval = unsafe { FOG_MaxPingLimit() };
+ metrics::fog::max_pings_per_minute.set(pings_per_interval.into());
+
+ // SAFETY NOTE: Safe because it returns a primitive by value.
+ let enable_event_timestamps = unsafe { FOG_EventTimestampsEnabled() };
+
+ let rate_limit = Some(glean::PingRateLimit {
+ seconds_per_interval: 60,
+ pings_per_interval,
+ });
+
+ let configuration = Configuration {
+ upload_enabled: false,
+ data_path,
+ application_id,
+ max_events: None,
+ delay_ping_lifetime_io: true,
+ server_endpoint: Some(server),
+ uploader: None,
+ use_core_mps: true,
+ trim_data_to_registered_pings: true,
+ log_level: None,
+ rate_limit,
+ enable_event_timestamps,
+ experimentation_id: None,
+ };
+
+ Ok((configuration, client_info))
+}
+
+#[cfg(not(target_os = "android"))]
+fn setup_observers() -> Result<(), nsresult> {
+ if let Err(e) = UploadPrefObserver::begin_observing() {
+ log::error!(
+ "Could not observe data upload pref. Abandoning FOG init due to {:?}",
+ e
+ );
+ return Err(e);
+ }
+
+ if let Err(e) = UserActivityObserver::begin_observing() {
+ log::error!(
+ "Could not observe user activity. Abandoning FOG init due to {:?}",
+ e
+ );
+ return Err(e);
+ }
+
+ Ok(())
+}
+
+#[cfg(target_os = "android")]
+fn setup_observers() -> Result<(), nsresult> {
+ // No observers are set up on Android.
+ Ok(())
+}
+
+/// Construct and return the data_path from the profile dir, or return an error.
+fn get_data_path() -> Result<String, nsresult> {
+ let dir_svc: RefPtr<nsIProperties> = match xpcom::components::Directory::service() {
+ Ok(ds) => ds,
+ _ => return Err(NS_ERROR_FAILURE),
+ };
+ let mut profile_dir = xpcom::GetterAddrefs::<nsIFile>::new();
+ unsafe {
+ dir_svc
+ .Get(
+ cstr!("ProfD").as_ptr(),
+ &nsIFile::IID,
+ profile_dir.void_ptr(),
+ )
+ .to_result()?;
+ }
+ let profile_dir = profile_dir.refptr().ok_or(NS_ERROR_FAILURE)?;
+ let mut profile_path = nsString::new();
+ unsafe {
+ (*profile_dir).GetPath(&mut *profile_path).to_result()?;
+ }
+ let profile_path = String::from_utf16(&profile_path[..]).map_err(|_| NS_ERROR_FAILURE)?;
+ let data_path = profile_path + "/datareporting/glean";
+ Ok(data_path)
+}
+
+/// Return a tuple of the build_id, app version, build channel, and locale.
+/// 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, String), nsresult> {
+ let xul: RefPtr<nsIXULRuntime> =
+ xpcom::components::XULRuntime::service().map_err(|_| NS_ERROR_FAILURE)?;
+
+ let pref_service: RefPtr<nsIPrefService> =
+ xpcom::components::Preferences::service().map_err(|_| NS_ERROR_FAILURE)?;
+ let locale_service: RefPtr<mozILocaleService> =
+ xpcom::components::Locale::service().map_err(|_| NS_ERROR_FAILURE)?;
+ let branch = xpcom::getter_addrefs(|p| {
+ // Safe because:
+ // * `null` is explicitly allowed per documentation
+ // * `p` is a valid outparam guaranteed by `getter_addrefs`
+ unsafe { pref_service.GetDefaultBranch(std::ptr::null(), p) }
+ })?;
+ let pref_name = CString::new("app.update.channel").map_err(|_| NS_ERROR_FAILURE)?;
+ let mut channel = nsCString::new();
+ // Safe because:
+ // * `branch` is non-null (otherwise `getter_addrefs` would've been `Err`
+ // * `pref_name` exists so a pointer to it is valid for the life of the function
+ // * `channel` exists so a pointer to it is valid, and it can be written to
+ unsafe {
+ if (*branch)
+ .GetCharPref(pref_name.as_ptr(), channel.deref_mut() as *mut nsACString)
+ .to_result()
+ .is_err()
+ {
+ channel = "unknown".into();
+ }
+ }
+
+ let app_info = match xul.query_interface::<nsIXULAppInfo>() {
+ Some(ai) => ai,
+ // In e.g. xpcshell the XULRuntime isn't XULAppInfo.
+ // We still want to return sensible values so tests don't explode.
+ _ => {
+ return Ok((
+ "unknown".to_owned(),
+ "unknown".to_owned(),
+ channel.to_string(),
+ "unknown".to_owned(),
+ ))
+ }
+ };
+
+ 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()?;
+ }
+
+ let mut locale = nsCString::new();
+ unsafe {
+ locale_service
+ .GetAppLocaleAsBCP47(&mut *locale)
+ .to_result()?;
+ }
+
+ Ok((
+ build_id.to_string(),
+ version.to_string(),
+ channel.to_string(),
+ locale.to_string(),
+ ))
+}
+
+/// **TEST-ONLY METHOD**
+/// Resets FOG and the underlying Glean SDK, clearing stores.
+#[no_mangle]
+pub extern "C" fn fog_test_reset(
+ data_path_override: &nsACString,
+ app_id_override: &nsACString,
+) -> nsresult {
+ fog_test_reset_internal(data_path_override, app_id_override).into()
+}
+
+// Split out into its own function so I could use `?`
+#[cfg(not(target_os = "android"))]
+fn fog_test_reset_internal(
+ data_path_override: &nsACString,
+ app_id_override: &nsACString,
+) -> Result<(), nsresult> {
+ let (mut conf, client_info) = build_configuration(data_path_override, app_id_override)?;
+
+ let upload_enabled = static_prefs::pref!("datareporting.healthreport.uploadEnabled");
+ let recording_enabled = static_prefs::pref!("telemetry.fog.test.localhost_port") < 0;
+ conf.upload_enabled = upload_enabled || recording_enabled;
+
+ // Don't accidentally send "main" pings during tests.
+ conf.use_core_mps = false;
+
+ // I'd prefer to reuse the uploader, but it gets moved into Glean so we build anew.
+ conf.uploader = Some(Box::new(ViaductUploader) as Box<dyn glean::net::PingUploader>);
+
+ glean::test_reset_glean(conf, client_info, true);
+ Ok(())
+}
+
+#[cfg(target_os = "android")]
+fn fog_test_reset_internal(
+ data_path_override: &nsACString,
+ app_id_override: &nsACString,
+) -> Result<(), nsresult> {
+ let (mut conf, client_info) = build_configuration(data_path_override, app_id_override)?;
+
+ // On Android always enable Glean upload.
+ conf.upload_enabled = true;
+
+ // Don't accidentally send "main" pings during tests.
+ conf.use_core_mps = false;
+
+ // Same as before, would prefer to reuse, but it gets moved into Glean so we build anew.
+ conf.uploader = Some(Box::new(ViaductUploader) as Box<dyn glean::net::PingUploader>);
+
+ glean::test_reset_glean(conf, client_info, true);
+ Ok(())
+}
diff --git a/toolkit/components/glean/src/init/upload_pref.rs b/toolkit/components/glean/src/init/upload_pref.rs
new file mode 100644
index 0000000000..737230c16c
--- /dev/null
+++ b/toolkit/components/glean/src/init/upload_pref.rs
@@ -0,0 +1,99 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
+
+use std::ffi::CStr;
+use std::os::raw::c_char;
+use std::sync::atomic::{AtomicBool, Ordering};
+
+use nserror::{nsresult, NS_ERROR_FAILURE, NS_OK};
+use nsstring::{nsACString, nsCStr};
+use xpcom::{
+ interfaces::{nsIPrefBranch, nsISupports},
+ RefPtr,
+};
+
+/// Whether the current value of the localhost testing pref is permitting
+/// metric recording (even if upload is disabled).
+static RECORDING_ENABLED: AtomicBool = AtomicBool::new(false);
+
+// Partially cargo-culted from https://searchfox.org/mozilla-central/rev/598e50d2c3cd81cd616654f16af811adceb08f9f/security/manager/ssl/cert_storage/src/lib.rs#1192
+#[xpcom(implement(nsIObserver), atomic)]
+pub(crate) struct UploadPrefObserver {}
+
+#[allow(non_snake_case)]
+impl UploadPrefObserver {
+ pub(crate) fn begin_observing() -> Result<(), nsresult> {
+ // Ensure we begin with the correct current value of RECORDING_ENABLED.
+ let recording_enabled = static_prefs::pref!("telemetry.fog.test.localhost_port") < 0;
+ RECORDING_ENABLED.store(recording_enabled, Ordering::SeqCst);
+
+ // SAFETY: Everything here is self-contained.
+ //
+ // * We allocate the pref observer, created by the xpcom macro
+ // * We query the pref service and bail out if it doesn't exist.
+ // * We create a nsCStr from a static string.
+ // * We control all input to `AddObserverImpl`
+ unsafe {
+ let pref_obs = Self::allocate(InitUploadPrefObserver {});
+ let pref_branch: RefPtr<nsIPrefBranch> =
+ xpcom::components::Preferences::service().map_err(|_| NS_ERROR_FAILURE)?;
+ let pref_nscstr =
+ &nsCStr::from("datareporting.healthreport.uploadEnabled") as &nsACString;
+ (*pref_branch)
+ .AddObserverImpl(pref_nscstr, pref_obs.coerce(), false)
+ .to_result()?;
+ let pref_nscstr = &nsCStr::from("telemetry.fog.test.localhost_port") as &nsACString;
+ (*pref_branch)
+ .AddObserverImpl(pref_nscstr, pref_obs.coerce(), false)
+ .to_result()?;
+ }
+
+ Ok(())
+ }
+
+ unsafe fn Observe(
+ &self,
+ _subject: *const nsISupports,
+ topic: *const c_char,
+ pref_name: *const u16,
+ ) -> nserror::nsresult {
+ let topic = CStr::from_ptr(topic).to_str().unwrap();
+ // Conversion utf16 to utf8 is messy.
+ // We should only ever observe changes to one of the two prefs we want,
+ // but just to be on the safe side let's assert.
+
+ // cargo-culted from https://searchfox.org/mozilla-central/rev/598e50d2c3cd81cd616654f16af811adceb08f9f/security/manager/ssl/cert_storage/src/lib.rs#1606-1612
+ // (with a little transformation)
+ let len = (0..).take_while(|&i| *pref_name.offset(i) != 0).count(); // find NUL.
+ let slice = std::slice::from_raw_parts(pref_name, len);
+ let pref_name = match String::from_utf16(slice) {
+ Ok(name) => name,
+ Err(_) => return NS_ERROR_FAILURE,
+ };
+ log::info!("Observed {:?}, {:?}", topic, pref_name);
+ debug_assert!(topic == "nsPref:changed");
+ debug_assert!(
+ pref_name == "datareporting.healthreport.uploadEnabled"
+ || pref_name == "telemetry.fog.test.localhost_port"
+ );
+
+ let upload_enabled = static_prefs::pref!("datareporting.healthreport.uploadEnabled");
+ let recording_enabled = static_prefs::pref!("telemetry.fog.test.localhost_port") < 0;
+ log::info!(
+ "New upload_enabled {}, recording_enabled {}",
+ upload_enabled,
+ recording_enabled
+ );
+ if RECORDING_ENABLED.load(Ordering::SeqCst) && !recording_enabled {
+ // Whenever the test pref goes from permitting recording to forbidding it,
+ // ensure Glean is told to wipe the stores.
+ // This may send a "deletion-request" ping for a client_id that's never sent
+ // any other pings.
+ glean::set_upload_enabled(false);
+ }
+ RECORDING_ENABLED.store(recording_enabled, Ordering::SeqCst);
+ glean::set_upload_enabled(upload_enabled || recording_enabled);
+ NS_OK
+ }
+}
diff --git a/toolkit/components/glean/src/init/user_activity.rs b/toolkit/components/glean/src/init/user_activity.rs
new file mode 100644
index 0000000000..dbafeca4e8
--- /dev/null
+++ b/toolkit/components/glean/src/init/user_activity.rs
@@ -0,0 +1,129 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
+
+use std::ffi::CStr;
+use std::os::raw::c_char;
+use std::sync::{
+ atomic::{AtomicBool, Ordering},
+ RwLock,
+};
+use std::time::{Duration, Instant};
+
+use nserror::{nsresult, NS_ERROR_FAILURE, NS_OK};
+use xpcom::{
+ interfaces::{nsIObserverService, nsISupports},
+ RefPtr,
+};
+
+// Partially cargo-culted from UploadPrefObserver.
+#[xpcom(implement(nsIObserver), atomic)]
+pub(crate) struct UserActivityObserver {
+ last_edge: RwLock<Instant>,
+ was_active: AtomicBool,
+}
+
+/// Listens to Firefox Desktop's `user-interaction-(in)active` topics,
+/// debouncing them before calling into the Glean SDK Client Activity API.
+/// See
+/// [the docs](https://firefox-source-docs.mozilla.org/toolkit/components/glean/builtin_pings.html)
+/// for more info.
+#[allow(non_snake_case)]
+impl UserActivityObserver {
+ pub(crate) fn begin_observing() -> Result<(), nsresult> {
+ // First and foremost, even if we can't get the ObserverService,
+ // init always means client activity.
+ glean::handle_client_active();
+
+ // SAFETY: Everything here is self-contained.
+ //
+ // * We allocate the activity observer, created by the xpcom macro
+ // * We create cstr from a static string.
+ // * We control all input to `AddObserver`
+ unsafe {
+ let activity_obs = Self::allocate(InitUserActivityObserver {
+ last_edge: RwLock::new(Instant::now()),
+ was_active: AtomicBool::new(false),
+ });
+ let obs_service: RefPtr<nsIObserverService> =
+ xpcom::components::Observer::service().map_err(|_| NS_ERROR_FAILURE)?;
+ let rv = obs_service.AddObserver(
+ activity_obs.coerce(),
+ cstr!("user-interaction-active").as_ptr(),
+ false,
+ );
+ if !rv.succeeded() {
+ return Err(rv);
+ }
+ let rv = obs_service.AddObserver(
+ activity_obs.coerce(),
+ cstr!("user-interaction-inactive").as_ptr(),
+ false,
+ );
+ if !rv.succeeded() {
+ return Err(rv);
+ }
+ }
+ Ok(())
+ }
+
+ unsafe fn Observe(
+ &self,
+ _subject: *const nsISupports,
+ topic: *const c_char,
+ _data: *const u16,
+ ) -> nserror::nsresult {
+ match CStr::from_ptr(topic).to_str() {
+ Ok("user-interaction-active") => self.handle_active(),
+ Ok("user-interaction-inactive") => self.handle_inactive(),
+ _ => NS_OK,
+ }
+ }
+
+ fn handle_active(&self) -> nserror::nsresult {
+ let was_active = self.was_active.swap(true, Ordering::SeqCst);
+ if !was_active {
+ let inactivity = self
+ .last_edge
+ .read()
+ .expect("Edge lock poisoned.")
+ .elapsed();
+ // We only care after a certain period of inactivity (default 20min).
+ let limit = static_prefs::pref!("telemetry.fog.test.inactivity_limit");
+ if inactivity >= Duration::from_secs(limit.into()) {
+ log::info!(
+ "User triggers core activity after {}s!",
+ inactivity.as_secs()
+ );
+ glean::handle_client_active();
+ }
+ let mut edge = self.last_edge.write().expect("Edge lock poisoned.");
+ *edge = Instant::now();
+ }
+ NS_OK
+ }
+
+ fn handle_inactive(&self) -> nserror::nsresult {
+ let was_active = self.was_active.swap(false, Ordering::SeqCst);
+ // This is actually always so. Inactivity is only notified once.
+ if was_active {
+ let activity = self
+ .last_edge
+ .read()
+ .expect("Edge lock poisoned.")
+ .elapsed();
+ // We only care after a certain period of activity (default 2min).
+ let limit = static_prefs::pref!("telemetry.fog.test.activity_limit");
+ if activity >= Duration::from_secs(limit.into()) {
+ log::info!(
+ "User triggers core inactivity after {}s!",
+ activity.as_secs()
+ );
+ glean::handle_client_inactive();
+ }
+ let mut edge = self.last_edge.write().expect("Edge lock poisoned.");
+ *edge = Instant::now();
+ }
+ NS_OK
+ }
+}
diff --git a/toolkit/components/glean/src/init/viaduct_uploader.rs b/toolkit/components/glean/src/init/viaduct_uploader.rs
new file mode 100644
index 0000000000..d9ce4e0488
--- /dev/null
+++ b/toolkit/components/glean/src/init/viaduct_uploader.rs
@@ -0,0 +1,73 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
+
+use glean::net::{PingUploader, UploadResult};
+use url::Url;
+use viaduct::{Error::*, Request};
+
+extern "C" {
+ fn FOG_TooLateToSend() -> bool;
+}
+
+/// 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 || {
+ // SAFETY NOTE: Safe because it returns a primitive by value.
+ if unsafe { FOG_TooLateToSend() } {
+ log::trace!("Attempted to send ping too late into shutdown.");
+ return Ok(UploadResult::done());
+ }
+ let debug_tagged = headers.iter().any(|(name, _)| name == "X-Debug-ID");
+ let localhost_port = static_prefs::pref!("telemetry.fog.test.localhost_port");
+ if localhost_port < 0
+ || (localhost_port == 0 && !debug_tagged && cfg!(feature = "disable_upload"))
+ {
+ log::info!("FOG Ping uploader faking success");
+ return Ok(UploadResult::http_status(200));
+ }
+ let parsed_url = Url::parse(&url_clone)?;
+
+ log::info!("FOG Ping uploader uploading to {:?}", parsed_url);
+
+ let mut req = Request::post(parsed_url.clone()).body(body.clone());
+ for (header_key, header_value) in &headers {
+ req = req.header(header_key.to_owned(), header_value)?;
+ }
+
+ log::trace!("FOG Ping Uploader sending ping to {}", parsed_url);
+ let res = req.send()?;
+ Ok(UploadResult::http_status(res.status as i32))
+ })();
+ log::trace!(
+ "FOG Ping Uploader completed uploading to {} (Result {:?})",
+ url,
+ result
+ );
+ match result {
+ Ok(result) => result,
+ Err(NonTlsUrl | UrlError(_)) => UploadResult::unrecoverable_failure(),
+ Err(
+ RequestHeaderError(_)
+ | BackendError(_)
+ | NetworkError(_)
+ | BackendNotInitialized
+ | SetBackendError,
+ ) => UploadResult::recoverable_failure(),
+ }
+ }
+}
diff --git a/toolkit/components/glean/src/lib.rs b/toolkit/components/glean/src/lib.rs
new file mode 100644
index 0000000000..79f3258bb7
--- /dev/null
+++ b/toolkit/components/glean/src/lib.rs
@@ -0,0 +1,210 @@
+// This Source Code Form is subject to the terms of 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/
+
+use firefox_on_glean::{ipc, metrics, pings};
+use nserror::{nsresult, NS_ERROR_FAILURE, NS_OK};
+use nsstring::{nsACString, nsCString};
+use thin_vec::ThinVec;
+
+#[macro_use]
+extern crate cstr;
+#[cfg_attr(not(target_os = "android"), macro_use)]
+extern crate xpcom;
+
+mod init;
+
+pub use init::fog_init;
+
+#[no_mangle]
+pub extern "C" fn fog_shutdown() {
+ glean::shutdown();
+}
+
+#[no_mangle]
+pub extern "C" fn fog_register_pings() {
+ pings::register_pings(None);
+}
+
+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) = ipc::take_buf() {
+ PENDING_BUF = buf;
+ PENDING_BUF.len()
+ } else {
+ PENDING_BUF = vec![];
+ 0
+ }
+}
+
+/// Only safe if called on a single thread (the same single thread you call
+/// fog_serialize_ipc_buf on), and if buf points to an allocated buffer of at
+/// least buf_len bytes.
+#[no_mangle]
+pub unsafe extern "C" fn fog_give_ipc_buf(buf: *mut u8, buf_len: usize) -> usize {
+ let pending_len = PENDING_BUF.len();
+ if buf.is_null() || buf_len < pending_len {
+ return 0;
+ }
+ std::ptr::copy_nonoverlapping(PENDING_BUF.as_ptr(), buf, pending_len);
+ PENDING_BUF = Vec::new();
+ pending_len
+}
+
+/// Only safe if buf points to an allocated buffer of at least buf_len bytes.
+/// No ownership is transfered to Rust by this method: caller owns the memory at
+/// buf before and after this call.
+#[no_mangle]
+pub unsafe extern "C" fn fog_use_ipc_buf(buf: *const u8, buf_len: usize) {
+ let slice = std::slice::from_raw_parts(buf, buf_len);
+ let res = ipc::replay_from_buf(slice);
+ if res.is_err() {
+ log::warn!("Unable to replay ipc buffer. This will result in data loss.");
+ metrics::fog_ipc::replay_failures.add(1);
+ }
+}
+
+/// Sets the debug tag for pings assembled in the future.
+/// Returns an error result if the provided value is not a valid tag.
+#[no_mangle]
+pub extern "C" fn fog_set_debug_view_tag(value: &nsACString) -> nsresult {
+ let result = glean::set_debug_view_tag(&value.to_string());
+ if result {
+ return NS_OK;
+ } else {
+ return NS_ERROR_FAILURE;
+ }
+}
+
+/// Submits a ping by name.
+#[no_mangle]
+pub extern "C" fn fog_submit_ping(ping_name: &nsACString) -> nsresult {
+ glean::submit_ping_by_name(&ping_name.to_string(), None);
+ NS_OK
+}
+
+/// Turns ping logging on or off.
+/// Returns an error if the logging failed to be configured.
+#[no_mangle]
+pub extern "C" fn fog_set_log_pings(value: bool) -> nsresult {
+ glean::set_log_pings(value);
+ NS_OK
+}
+
+/// Flushes ping-lifetime data to the db when delay_ping_lifetime_io is true.
+#[no_mangle]
+pub extern "C" fn fog_persist_ping_lifetime_data() -> nsresult {
+ glean::persist_ping_lifetime_data();
+ NS_OK
+}
+
+/// Indicate that an experiment is running.
+/// Glean will add an experiment annotation which is sent with pings.
+/// This information is not persisted between runs.
+///
+/// See [`glean_core::Glean::set_experiment_active`].
+#[no_mangle]
+pub extern "C" fn fog_set_experiment_active(
+ experiment_id: &nsACString,
+ branch: &nsACString,
+ extra_keys: &ThinVec<nsCString>,
+ extra_values: &ThinVec<nsCString>,
+) {
+ assert_eq!(
+ extra_keys.len(),
+ extra_values.len(),
+ "Experiment extra keys and values differ in length."
+ );
+ let extra = if extra_keys.len() == 0 {
+ None
+ } else {
+ Some(
+ extra_keys
+ .iter()
+ .zip(extra_values.iter())
+ .map(|(k, v)| (k.to_string(), v.to_string()))
+ .collect(),
+ )
+ };
+ glean::set_experiment_active(experiment_id.to_string(), branch.to_string(), extra);
+}
+
+/// Indicate that an experiment is no longer running.
+///
+/// See [`glean_core::Glean::set_experiment_inactive`].
+#[no_mangle]
+pub extern "C" fn fog_set_experiment_inactive(experiment_id: &nsACString) {
+ glean::set_experiment_inactive(experiment_id.to_string());
+}
+
+/// TEST ONLY FUNCTION
+///
+/// Returns true if the identified experiment is active.
+#[no_mangle]
+pub extern "C" fn fog_test_is_experiment_active(experiment_id: &nsACString) -> bool {
+ glean::test_is_experiment_active(experiment_id.to_string())
+}
+
+/// TEST ONLY FUNCTION
+///
+/// Fills `branch`, `extra_keys`, and `extra_values` with the identified experiment's data.
+/// Panics if the identified experiment isn't active.
+#[no_mangle]
+pub extern "C" fn fog_test_get_experiment_data(
+ experiment_id: &nsACString,
+ branch: &mut nsACString,
+ extra_keys: &mut ThinVec<nsCString>,
+ extra_values: &mut ThinVec<nsCString>,
+) {
+ let data = glean::test_get_experiment_data(experiment_id.to_string());
+ if let Some(data) = data {
+ branch.assign(&data.branch);
+ if let Some(extra) = data.extra {
+ let (data_keys, data_values): (Vec<_>, Vec<_>) = extra.iter().unzip();
+ extra_keys.extend(data_keys.into_iter().map(|key| key.into()));
+ extra_values.extend(data_values.into_iter().map(|value| value.into()));
+ }
+ }
+}
+
+/// Sets the remote feature configuration.
+///
+/// See [`glean_core::Glean::set_metrics_disabled_config`].
+#[no_mangle]
+pub extern "C" fn fog_set_metrics_feature_config(config_json: &nsACString) {
+ // Normalize null and empty strings to a stringified empty map
+ if config_json == "null" || config_json.is_empty() {
+ glean::glean_set_metrics_enabled_config("{}".to_owned());
+ }
+ glean::glean_set_metrics_enabled_config(config_json.to_string());
+}
+
+/// Performs Glean tasks when client state changes to inactive
+///
+/// See [`glean_core::Glean::handle_client_inactive`].
+#[no_mangle]
+pub extern "C" fn fog_internal_glean_handle_client_inactive() {
+ glean::handle_client_inactive();
+}
diff --git a/toolkit/components/glean/tags.yaml b/toolkit/components/glean/tags.yaml
new file mode 100644
index 0000000000..9c6c09bb9c
--- /dev/null
+++ b/toolkit/components/glean/tags.yaml
@@ -0,0 +1,553 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+
+### This file was AUTOMATICALLY GENERATED by `./mach update-glean-tags`
+### DO NOT edit it by hand.
+
+
+---
+$schema: moz://mozilla.org/schemas/glean/tags/1-0-0
+'Cloud Services :: Firefox: Common':
+ description: The Bugzilla component which applies to this object.
+'Conduit :: mots':
+ description: The Bugzilla component which applies to this object.
+'Core :: Audio/Video':
+ description: The Bugzilla component which applies to this object.
+'Core :: Audio/Video: GMP':
+ description: The Bugzilla component which applies to this object.
+'Core :: Audio/Video: MediaStreamGraph':
+ description: The Bugzilla component which applies to this object.
+'Core :: Audio/Video: Playback':
+ description: The Bugzilla component which applies to this object.
+'Core :: Audio/Video: Recording':
+ description: The Bugzilla component which applies to this object.
+'Core :: Audio/Video: Web Codecs':
+ description: The Bugzilla component which applies to this object.
+'Core :: Audio/Video: cubeb':
+ description: The Bugzilla component which applies to this object.
+'Core :: AutoConfig (Mission Control Desktop)':
+ description: The Bugzilla component which applies to this object.
+'Core :: CSS Parsing and Computation':
+ description: The Bugzilla component which applies to this object.
+'Core :: CSS Transitions and Animations':
+ description: The Bugzilla component which applies to this object.
+'Core :: Cycle Collector':
+ description: The Bugzilla component which applies to this object.
+'Core :: DOM: Animation':
+ description: The Bugzilla component which applies to this object.
+'Core :: DOM: Bindings (WebIDL)':
+ description: The Bugzilla component which applies to this object.
+'Core :: DOM: CSS Object Model':
+ description: The Bugzilla component which applies to this object.
+'Core :: DOM: Content Processes':
+ description: The Bugzilla component which applies to this object.
+'Core :: DOM: Copy & Paste and Drag & Drop':
+ description: The Bugzilla component which applies to this object.
+'Core :: DOM: Core & HTML':
+ description: The Bugzilla component which applies to this object.
+'Core :: DOM: Credential Management':
+ description: The Bugzilla component which applies to this object.
+'Core :: DOM: Device Interfaces':
+ description: The Bugzilla component which applies to this object.
+'Core :: DOM: Editor':
+ description: The Bugzilla component which applies to this object.
+'Core :: DOM: Events':
+ description: The Bugzilla component which applies to this object.
+'Core :: DOM: File':
+ description: The Bugzilla component which applies to this object.
+'Core :: DOM: Forms':
+ description: The Bugzilla component which applies to this object.
+'Core :: DOM: Geolocation':
+ description: The Bugzilla component which applies to this object.
+'Core :: DOM: HTML Parser':
+ description: The Bugzilla component which applies to this object.
+'Core :: DOM: Navigation':
+ description: The Bugzilla component which applies to this object.
+'Core :: DOM: Networking':
+ description: The Bugzilla component which applies to this object.
+'Core :: DOM: Notifications':
+ description: The Bugzilla component which applies to this object.
+'Core :: DOM: Performance':
+ description: The Bugzilla component which applies to this object.
+'Core :: DOM: Security':
+ description: The Bugzilla component which applies to this object.
+'Core :: DOM: Selection':
+ description: The Bugzilla component which applies to this object.
+'Core :: DOM: Serializers':
+ description: The Bugzilla component which applies to this object.
+'Core :: DOM: Service Workers':
+ description: The Bugzilla component which applies to this object.
+'Core :: DOM: Streams':
+ description: The Bugzilla component which applies to this object.
+'Core :: DOM: UI Events & Focus Handling':
+ description: The Bugzilla component which applies to this object.
+'Core :: DOM: Web Authentication':
+ description: The Bugzilla component which applies to this object.
+'Core :: DOM: Web Crypto':
+ description: The Bugzilla component which applies to this object.
+'Core :: DOM: Web Payments':
+ description: The Bugzilla component which applies to this object.
+'Core :: DOM: Workers':
+ description: The Bugzilla component which applies to this object.
+'Core :: DOM: postMessage':
+ description: The Bugzilla component which applies to this object.
+'Core :: Disability Access APIs':
+ description: The Bugzilla component which applies to this object.
+'Core :: Document Navigation':
+ description: The Bugzilla component which applies to this object.
+'Core :: Gecko Profiler':
+ description: The Bugzilla component which applies to this object.
+'Core :: General':
+ description: The Bugzilla component which applies to this object.
+'Core :: Graphics':
+ description: The Bugzilla component which applies to this object.
+'Core :: Graphics: Canvas2D':
+ description: The Bugzilla component which applies to this object.
+'Core :: Graphics: CanvasWebGL':
+ description: The Bugzilla component which applies to this object.
+'Core :: Graphics: ImageLib':
+ description: The Bugzilla component which applies to this object.
+'Core :: Graphics: Text':
+ description: The Bugzilla component which applies to this object.
+'Core :: Graphics: WebGPU':
+ description: The Bugzilla component which applies to this object.
+'Core :: Graphics: WebRender':
+ description: The Bugzilla component which applies to this object.
+'Core :: Hardware Abstraction Layer (HAL)':
+ description: The Bugzilla component which applies to this object.
+'Core :: IPC':
+ description: The Bugzilla component which applies to this object.
+'Core :: Internationalization':
+ description: The Bugzilla component which applies to this object.
+'Core :: JavaScript Engine':
+ description: The Bugzilla component which applies to this object.
+'Core :: JavaScript Engine: JIT':
+ description: The Bugzilla component which applies to this object.
+'Core :: JavaScript: GC':
+ description: The Bugzilla component which applies to this object.
+'Core :: JavaScript: Internationalization API':
+ description: The Bugzilla component which applies to this object.
+'Core :: JavaScript: Standard Library':
+ description: The Bugzilla component which applies to this object.
+'Core :: JavaScript: WebAssembly':
+ description: The Bugzilla component which applies to this object.
+'Core :: Javascript: Web Assembly':
+ description: The Bugzilla component which applies to this object.
+'Core :: Javascript: WebAssembly':
+ description: The Bugzilla component which applies to this object.
+'Core :: Layout':
+ description: The Bugzilla component which applies to this object.
+'Core :: Layout: Block and Inline':
+ description: The Bugzilla component which applies to this object.
+'Core :: Layout: Columns':
+ description: The Bugzilla component which applies to this object.
+'Core :: Layout: Flexbox':
+ description: The Bugzilla component which applies to this object.
+'Core :: Layout: Floats':
+ description: The Bugzilla component which applies to this object.
+'Core :: Layout: Form Controls':
+ description: The Bugzilla component which applies to this object.
+'Core :: Layout: Generated Content, Lists, and Counters':
+ description: The Bugzilla component which applies to this object.
+'Core :: Layout: Grid':
+ description: The Bugzilla component which applies to this object.
+'Core :: Layout: Images, Video, and HTML Frames':
+ description: The Bugzilla component which applies to this object.
+'Core :: Layout: Positioned':
+ description: The Bugzilla component which applies to this object.
+'Core :: Layout: Ruby':
+ description: The Bugzilla component which applies to this object.
+'Core :: Layout: Scrolling and Overflow':
+ description: The Bugzilla component which applies to this object.
+'Core :: Layout: Tables':
+ description: The Bugzilla component which applies to this object.
+'Core :: Layout: Text and Fonts':
+ description: The Bugzilla component which applies to this object.
+'Core :: Localization':
+ description: The Bugzilla component which applies to this object.
+'Core :: MFBT':
+ description: The Bugzilla component which applies to this object.
+'Core :: MathML':
+ description: The Bugzilla component which applies to this object.
+'Core :: Memory Allocator':
+ description: The Bugzilla component which applies to this object.
+'Core :: Networking':
+ description: The Bugzilla component which applies to this object.
+'Core :: Networking: Cache':
+ description: The Bugzilla component which applies to this object.
+'Core :: Networking: Cookies':
+ description: The Bugzilla component which applies to this object.
+'Core :: Networking: DNS':
+ description: The Bugzilla component which applies to this object.
+'Core :: Networking: File':
+ description: The Bugzilla component which applies to this object.
+'Core :: Networking: HTTP':
+ description: The Bugzilla component which applies to this object.
+'Core :: Networking: JAR':
+ description: The Bugzilla component which applies to this object.
+'Core :: Networking: WebSockets':
+ description: The Bugzilla component which applies to this object.
+'Core :: Networking: WebTransport':
+ description: The Bugzilla component which applies to this object.
+'Core :: Panning and Zooming':
+ description: The Bugzilla component which applies to this object.
+'Core :: Permission Manager':
+ description: The Bugzilla component which applies to this object.
+'Core :: Preferences: Backend':
+ description: The Bugzilla component which applies to this object.
+'Core :: Printing: Output':
+ description: The Bugzilla component which applies to this object.
+'Core :: Privacy: Anti-Tracking':
+ description: The Bugzilla component which applies to this object.
+'Core :: SVG':
+ description: The Bugzilla component which applies to this object.
+'Core :: Security':
+ description: The Bugzilla component which applies to this object.
+'Core :: Security: CAPS':
+ description: The Bugzilla component which applies to this object.
+'Core :: Security: PSM':
+ description: The Bugzilla component which applies to this object.
+'Core :: Security: Process Sandboxing':
+ description: The Bugzilla component which applies to this object.
+'Core :: Spelling checker':
+ description: The Bugzilla component which applies to this object.
+'Core :: Storage: Cache API':
+ description: The Bugzilla component which applies to this object.
+'Core :: Storage: IndexedDB':
+ description: The Bugzilla component which applies to this object.
+'Core :: Storage: Quota Manager':
+ description: The Bugzilla component which applies to this object.
+'Core :: Storage: localStorage & sessionStorage':
+ description: The Bugzilla component which applies to this object.
+'Core :: String':
+ description: The Bugzilla component which applies to this object.
+'Core :: Web Audio':
+ description: The Bugzilla component which applies to this object.
+'Core :: Web Painting':
+ description: The Bugzilla component which applies to this object.
+'Core :: Web Speech':
+ description: The Bugzilla component which applies to this object.
+'Core :: WebRTC':
+ description: The Bugzilla component which applies to this object.
+'Core :: WebRTC: Audio/Video':
+ description: The Bugzilla component which applies to this object.
+'Core :: WebRTC: Networking':
+ description: The Bugzilla component which applies to this object.
+'Core :: WebRTC: Signaling':
+ description: The Bugzilla component which applies to this object.
+'Core :: WebVR':
+ description: The Bugzilla component which applies to this object.
+'Core :: Widget':
+ description: The Bugzilla component which applies to this object.
+'Core :: Widget: Cocoa':
+ description: The Bugzilla component which applies to this object.
+'Core :: Widget: Gtk':
+ description: The Bugzilla component which applies to this object.
+'Core :: Widget: Win32':
+ description: The Bugzilla component which applies to this object.
+'Core :: Window Management':
+ description: The Bugzilla component which applies to this object.
+'Core :: XML':
+ description: The Bugzilla component which applies to this object.
+'Core :: XPCOM':
+ description: The Bugzilla component which applies to this object.
+'Core :: XPConnect':
+ description: The Bugzilla component which applies to this object.
+'Core :: XSLT':
+ description: The Bugzilla component which applies to this object.
+'Core :: XUL':
+ description: The Bugzilla component which applies to this object.
+'Core :: js-ctypes':
+ description: The Bugzilla component which applies to this object.
+'Core :: mozglue':
+ description: The Bugzilla component which applies to this object.
+'DevTools :: Accessibility Tools':
+ description: The Bugzilla component which applies to this object.
+'DevTools :: Console':
+ description: The Bugzilla component which applies to this object.
+'DevTools :: DOM':
+ description: The Bugzilla component which applies to this object.
+'DevTools :: Debugger':
+ description: The Bugzilla component which applies to this object.
+'DevTools :: Framework':
+ description: The Bugzilla component which applies to this object.
+'DevTools :: General':
+ description: The Bugzilla component which applies to this object.
+'DevTools :: Inspector':
+ description: The Bugzilla component which applies to this object.
+'DevTools :: Inspector: Animations':
+ description: The Bugzilla component which applies to this object.
+'DevTools :: Inspector: Changes':
+ description: The Bugzilla component which applies to this object.
+'DevTools :: Inspector: Compatibility':
+ description: The Bugzilla component which applies to this object.
+'DevTools :: Inspector: Layout':
+ description: The Bugzilla component which applies to this object.
+'DevTools :: Inspector: Rules':
+ description: The Bugzilla component which applies to this object.
+'DevTools :: JSON Viewer':
+ description: The Bugzilla component which applies to this object.
+'DevTools :: Memory':
+ description: The Bugzilla component which applies to this object.
+'DevTools :: Netmonitor':
+ description: The Bugzilla component which applies to this object.
+'DevTools :: Performance Tools (Profiler/Timeline)':
+ description: The Bugzilla component which applies to this object.
+'DevTools :: Responsive Design Mode':
+ description: The Bugzilla component which applies to this object.
+'DevTools :: Shared Components':
+ description: The Bugzilla component which applies to this object.
+'DevTools :: Source Editor':
+ description: The Bugzilla component which applies to this object.
+'DevTools :: Storage Inspector':
+ description: The Bugzilla component which applies to this object.
+'DevTools :: Style Editor':
+ description: The Bugzilla component which applies to this object.
+'DevTools :: about:debugging':
+ description: The Bugzilla component which applies to this object.
+'Developer Infrastructure :: Lint and Formatting':
+ description: The Bugzilla component which applies to this object.
+'Developer Infrastructure :: Source Documentation':
+ description: The Bugzilla component which applies to this object.
+'Developer Infrastructure :: Try':
+ description: The Bugzilla component which applies to this object.
+'Firefox :: Address Bar':
+ description: The Bugzilla component which applies to this object.
+'Firefox :: Bookmarks & History':
+ description: The Bugzilla component which applies to this object.
+'Firefox :: Distributions':
+ description: The Bugzilla component which applies to this object.
+'Firefox :: Downloads Panel':
+ description: The Bugzilla component which applies to this object.
+'Firefox :: Enterprise Policies':
+ description: The Bugzilla component which applies to this object.
+'Firefox :: File Handling':
+ description: The Bugzilla component which applies to this object.
+'Firefox :: Firefox Accounts':
+ description: The Bugzilla component which applies to this object.
+'Firefox :: Firefox View':
+ description: The Bugzilla component which applies to this object.
+'Firefox :: General':
+ description: The Bugzilla component which applies to this object.
+'Firefox :: Headless':
+ description: The Bugzilla component which applies to this object.
+'Firefox :: Installer':
+ description: The Bugzilla component which applies to this object.
+'Firefox :: Keyboard Navigation':
+ description: The Bugzilla component which applies to this object.
+'Firefox :: Menus':
+ description: The Bugzilla component which applies to this object.
+'Firefox :: Messaging System':
+ description: The Bugzilla component which applies to this object.
+'Firefox :: Migration':
+ description: The Bugzilla component which applies to this object.
+'Firefox :: New Tab Page':
+ description: The Bugzilla component which applies to this object.
+'Firefox :: Nimbus Desktop Client':
+ description: The Bugzilla component which applies to this object.
+'Firefox :: Normandy Client':
+ description: The Bugzilla component which applies to this object.
+'Firefox :: PDF Viewer':
+ description: The Bugzilla component which applies to this object.
+'Firefox :: Page Info Window':
+ description: The Bugzilla component which applies to this object.
+'Firefox :: Pocket':
+ description: The Bugzilla component which applies to this object.
+'Firefox :: Private Browsing':
+ description: The Bugzilla component which applies to this object.
+'Firefox :: Protections UI':
+ description: The Bugzilla component which applies to this object.
+'Firefox :: Remote Settings Client':
+ description: The Bugzilla component which applies to this object.
+'Firefox :: Screenshots':
+ description: The Bugzilla component which applies to this object.
+'Firefox :: Search':
+ description: The Bugzilla component which applies to this object.
+'Firefox :: Security':
+ description: The Bugzilla component which applies to this object.
+'Firefox :: Session Restore':
+ description: The Bugzilla component which applies to this object.
+'Firefox :: Settings UI':
+ description: The Bugzilla component which applies to this object.
+'Firefox :: Shell Integration':
+ description: The Bugzilla component which applies to this object.
+'Firefox :: Shopping':
+ description: The Bugzilla component which applies to this object.
+'Firefox :: Site Identity':
+ description: The Bugzilla component which applies to this object.
+'Firefox :: Site Permissions':
+ description: The Bugzilla component which applies to this object.
+'Firefox :: Sync':
+ description: The Bugzilla component which applies to this object.
+'Firefox :: Tabbed Browser':
+ description: The Bugzilla component which applies to this object.
+'Firefox :: Theme':
+ description: The Bugzilla component which applies to this object.
+'Firefox :: Toolbars and Customization':
+ description: The Bugzilla component which applies to this object.
+'Firefox :: Tours':
+ description: The Bugzilla component which applies to this object.
+'Firefox :: Translation':
+ description: The Bugzilla component which applies to this object.
+'Firefox :: about:logins':
+ description: The Bugzilla component which applies to this object.
+'Firefox Build System :: Bootstrap Configuration':
+ description: The Bugzilla component which applies to this object.
+'Firefox Build System :: General':
+ description: The Bugzilla component which applies to this object.
+'Firefox Build System :: Mach Core':
+ description: The Bugzilla component which applies to this object.
+'Firefox Build System :: Source Code Analysis':
+ description: The Bugzilla component which applies to this object.
+'Firefox Build System :: Task Configuration':
+ description: The Bugzilla component which applies to this object.
+'GeckoView :: General':
+ description: The Bugzilla component which applies to this object.
+'Localization Infrastructure and Tools :: Fluent Migration':
+ description: The Bugzilla component which applies to this object.
+'Localization Infrastructure and Tools :: General':
+ description: The Bugzilla component which applies to this object.
+'Localization Infrastructure and Tools :: compare-locales':
+ description: The Bugzilla component which applies to this object.
+'NSPR :: NSPR':
+ description: The Bugzilla component which applies to this object.
+'NSS :: Libraries':
+ description: The Bugzilla component which applies to this object.
+'Release Engineering :: General':
+ description: The Bugzilla component which applies to this object.
+'Release Engineering :: Release Automation: Updates':
+ description: The Bugzilla component which applies to this object.
+'Remote Protocol :: Agent':
+ description: The Bugzilla component which applies to this object.
+'Remote Protocol :: CDP':
+ description: The Bugzilla component which applies to this object.
+'Remote Protocol :: Marionette':
+ description: The Bugzilla component which applies to this object.
+'Remote Protocol :: WebDriver BiDi':
+ description: The Bugzilla component which applies to this object.
+'Taskcluster :: General':
+ description: The Bugzilla component which applies to this object.
+'Taskcluster :: Platform Libraries':
+ description: The Bugzilla component which applies to this object.
+'Testing :: CPPUnitTest':
+ description: The Bugzilla component which applies to this object.
+'Testing :: Code Coverage':
+ description: The Bugzilla component which applies to this object.
+'Testing :: Firefox UI Tests':
+ description: The Bugzilla component which applies to this object.
+'Testing :: GTest':
+ description: The Bugzilla component which applies to this object.
+'Testing :: General':
+ description: The Bugzilla component which applies to this object.
+'Testing :: Marionette Client and Harness':
+ description: The Bugzilla component which applies to this object.
+'Testing :: Mochitest':
+ description: The Bugzilla component which applies to this object.
+'Testing :: Mozbase':
+ description: The Bugzilla component which applies to this object.
+'Testing :: Mozbase Rust':
+ description: The Bugzilla component which applies to this object.
+'Testing :: Python Test':
+ description: The Bugzilla component which applies to this object.
+'Testing :: Reftest':
+ description: The Bugzilla component which applies to this object.
+'Testing :: XPCShell Harness':
+ description: The Bugzilla component which applies to this object.
+'Testing :: geckodriver':
+ description: The Bugzilla component which applies to this object.
+'Testing :: mozperftest':
+ description: The Bugzilla component which applies to this object.
+'Testing :: web-platform-tests':
+ description: The Bugzilla component which applies to this object.
+'Toolkit :: Add-ons Manager':
+ description: The Bugzilla component which applies to this object.
+'Toolkit :: Alerts Service':
+ description: The Bugzilla component which applies to this object.
+'Toolkit :: Application Update':
+ description: The Bugzilla component which applies to this object.
+'Toolkit :: Async Tooling':
+ description: The Bugzilla component which applies to this object.
+'Toolkit :: Autocomplete':
+ description: The Bugzilla component which applies to this object.
+'Toolkit :: Background Tasks':
+ description: The Bugzilla component which applies to this object.
+'Toolkit :: Blocklist Implementation':
+ description: The Bugzilla component which applies to this object.
+'Toolkit :: Content Prompts':
+ description: The Bugzilla component which applies to this object.
+'Toolkit :: Crash Reporting':
+ description: The Bugzilla component which applies to this object.
+'Toolkit :: Data Sanitization':
+ description: The Bugzilla component which applies to this object.
+'Toolkit :: Default Browser Agent':
+ description: The Bugzilla component which applies to this object.
+'Toolkit :: Downloads API':
+ description: The Bugzilla component which applies to this object.
+'Toolkit :: FeatureGate':
+ description: The Bugzilla component which applies to this object.
+'Toolkit :: Find Toolbar':
+ description: The Bugzilla component which applies to this object.
+'Toolkit :: Form Autofill':
+ description: The Bugzilla component which applies to this object.
+'Toolkit :: Form Manager':
+ description: The Bugzilla component which applies to this object.
+'Toolkit :: General':
+ description: The Bugzilla component which applies to this object.
+'Toolkit :: IOUtils and PathUtils':
+ description: The Bugzilla component which applies to this object.
+'Toolkit :: NSIS Installer':
+ description: The Bugzilla component which applies to this object.
+'Toolkit :: Password Manager':
+ description: The Bugzilla component which applies to this object.
+'Toolkit :: Performance Monitoring':
+ description: The Bugzilla component which applies to this object.
+'Toolkit :: Picture-in-Picture':
+ description: The Bugzilla component which applies to this object.
+'Toolkit :: Places':
+ description: The Bugzilla component which applies to this object.
+'Toolkit :: PopupNotifications and Notification Bars':
+ description: The Bugzilla component which applies to this object.
+'Toolkit :: Preferences':
+ description: The Bugzilla component which applies to this object.
+'Toolkit :: Printing':
+ description: The Bugzilla component which applies to this object.
+'Toolkit :: Reader Mode':
+ description: The Bugzilla component which applies to this object.
+'Toolkit :: Safe Browsing':
+ description: The Bugzilla component which applies to this object.
+'Toolkit :: Startup and Profile System':
+ description: The Bugzilla component which applies to this object.
+'Toolkit :: Storage':
+ description: The Bugzilla component which applies to this object.
+'Toolkit :: Telemetry':
+ description: The Bugzilla component which applies to this object.
+'Toolkit :: Themes':
+ description: The Bugzilla component which applies to this object.
+'Toolkit :: UniFFI Bindings':
+ description: The Bugzilla component which applies to this object.
+'Toolkit :: Video/Audio Controls':
+ description: The Bugzilla component which applies to this object.
+'Toolkit :: View Source':
+ description: The Bugzilla component which applies to this object.
+'Toolkit :: UI Widgets':
+ description: The Bugzilla component which applies to this object.
+'Toolkit :: about:memory':
+ description: The Bugzilla component which applies to this object.
+'Web Compatibility :: Tooling & Investigations':
+ description: The Bugzilla component which applies to this object.
+'WebExtensions :: General':
+ description: The Bugzilla component which applies to this object.
+'WebExtensions :: Request Handling':
+ description: The Bugzilla component which applies to this object.
+'WebExtensions :: Storage':
+ description: The Bugzilla component which applies to this object.
+'WebExtensions :: Themes':
+ description: The Bugzilla component which applies to this object.
+'WebExtensions :: Untriaged':
+ description: The Bugzilla component which applies to this object.
+'mozilla.org :: Licensing':
+ description: The Bugzilla component which applies to this object.
+'mozilla.org :: MozillaBuild':
+ description: The Bugzilla component which applies to this object.
diff --git a/toolkit/components/glean/tests/browser/browser.toml b/toolkit/components/glean/tests/browser/browser.toml
new file mode 100644
index 0000000000..70c4afc8b1
--- /dev/null
+++ b/toolkit/components/glean/tests/browser/browser.toml
@@ -0,0 +1,17 @@
+[DEFAULT]
+
+["browser_event_leak.js"]
+
+["browser_fog_gmp.js"]
+support-files = ["empty_file.html"]
+
+["browser_fog_gpu.js"]
+
+["browser_fog_rdd.js"]
+support-files = ["small-shot.ogg"]
+
+["browser_fog_socket.js"]
+
+["browser_fog_utility.js"]
+
+["browser_labeled_gifft.js"]
diff --git a/toolkit/components/glean/tests/browser/browser_event_leak.js b/toolkit/components/glean/tests/browser/browser_event_leak.js
new file mode 100644
index 0000000000..1e69f17064
--- /dev/null
+++ b/toolkit/components/glean/tests/browser/browser_event_leak.js
@@ -0,0 +1,26 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async () => {
+ Services.fog.testResetFOG(); // Needed for TV which reuses profiles on repeat
+ Assert.equal(
+ undefined,
+ Glean.testOnlyIpc.eventWithExtra.testGetValue(),
+ "Nothing to begin with"
+ );
+ Glean.testOnlyIpc.eventWithExtra.record({
+ extra1: "Some extra string",
+ extra2: 42,
+ extra3_longer_name: false,
+ });
+ Assert.equal(
+ 1,
+ Glean.testOnlyIpc.eventWithExtra.testGetValue().length,
+ "One event? One event."
+ );
+
+ // AND NOW, FOR THE TRUE TEST:
+ // Will this leak memory all over the place?
+});
diff --git a/toolkit/components/glean/tests/browser/browser_fog_gmp.js b/toolkit/components/glean/tests/browser/browser_fog_gmp.js
new file mode 100644
index 0000000000..f61c3806f1
--- /dev/null
+++ b/toolkit/components/glean/tests/browser/browser_fog_gmp.js
@@ -0,0 +1,84 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Return a web-based URL for a given file based on the testing directory.
+ * @param {String} fileName
+ * file that caller wants its web-based url
+ */
+function GetTestWebBasedURL(fileName) {
+ return (
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.org"
+ ) + fileName
+ );
+}
+
+add_task(async () => {
+ await SpecialPowers.pushPrefEnv({
+ set: [["media.eme.enabled", true]],
+ });
+
+ await BrowserTestUtils.withNewTab(
+ GetTestWebBasedURL("empty_file.html"),
+ async function (browser) {
+ await SpecialPowers.spawn(browser, [], async function () {
+ try {
+ let config = [
+ {
+ initDataTypes: ["webm"],
+ videoCapabilities: [{ contentType: 'video/webm; codecs="vp9"' }],
+ },
+ ];
+ let access = await content.navigator.requestMediaKeySystemAccess(
+ "org.w3.clearkey",
+ config
+ );
+
+ content.mediaKeys = await access.createMediaKeys();
+ info("got media keys, which should ensure a GMP process exists");
+ } catch (ex) {
+ ok(false, ex.toString());
+ }
+ });
+
+ ok(
+ (await ChromeUtils.requestProcInfo()).children.some(
+ p => p.type == "gmpPlugin"
+ ),
+ "Found the GMP process."
+ );
+
+ Services.fog.testResetFOG();
+
+ is(
+ null,
+ Glean.testOnlyIpc.aCounter.testGetValue(),
+ "Ensure we begin without value."
+ );
+
+ await TestUtils.waitForCondition(async () => {
+ try {
+ await Services.fog.testTriggerMetrics(
+ Ci.nsIXULRuntime.PROCESS_TYPE_GMPLUGIN
+ );
+ return true;
+ } catch (e) {
+ info(e);
+ return false;
+ }
+ }, "waiting until we can successfully send the TestTriggerMetrics IPC.");
+
+ await Services.fog.testFlushAllChildren();
+
+ is(
+ Glean.testOnlyIpc.aCounter.testGetValue(),
+ Ci.nsIXULRuntime.PROCESS_TYPE_GMPLUGIN,
+ "Ensure the GMP-process-set value shows up in the parent process."
+ );
+ }
+ );
+});
diff --git a/toolkit/components/glean/tests/browser/browser_fog_gpu.js b/toolkit/components/glean/tests/browser/browser_fog_gpu.js
new file mode 100644
index 0000000000..6911ae8026
--- /dev/null
+++ b/toolkit/components/glean/tests/browser/browser_fog_gpu.js
@@ -0,0 +1,34 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async () => {
+ if (
+ !(await ChromeUtils.requestProcInfo()).children.some(p => p.type == "gpu")
+ ) {
+ ok(
+ true,
+ 'No GPU process? No test. Try again with --setpref "layers.gpu-process.force-enabled=true".'
+ );
+ return;
+ }
+ ok(true, "GPU Process found: Let's test.");
+
+ Services.fog.testResetFOG();
+
+ is(
+ null,
+ Glean.testOnlyIpc.aCounter.testGetValue(),
+ "Ensure we begin without value."
+ );
+
+ await Services.fog.testTriggerMetrics(Ci.nsIXULRuntime.PROCESS_TYPE_GPU);
+ await Services.fog.testFlushAllChildren();
+
+ is(
+ Glean.testOnlyIpc.aCounter.testGetValue(),
+ Ci.nsIXULRuntime.PROCESS_TYPE_GPU,
+ "Ensure the GPU-process-set value shows up in the parent process."
+ );
+});
diff --git a/toolkit/components/glean/tests/browser/browser_fog_rdd.js b/toolkit/components/glean/tests/browser/browser_fog_rdd.js
new file mode 100644
index 0000000000..51c10bbf31
--- /dev/null
+++ b/toolkit/components/glean/tests/browser/browser_fog_rdd.js
@@ -0,0 +1,63 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Return a web-based URL for a given file based on the testing directory.
+ * @param {String} fileName
+ * file that caller wants its web-based url
+ */
+function GetTestWebBasedURL(fileName) {
+ return (
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "http://example.org"
+ ) + fileName
+ );
+}
+
+add_task(async () => {
+ await SpecialPowers.pushPrefEnv({
+ set: [["media.rdd-process.enabled", true]],
+ });
+
+ let url = GetTestWebBasedURL("small-shot.ogg");
+ info(
+ `Opening ${url} in a new tab to trigger the creation of the RDD process`
+ );
+ let tab = BrowserTestUtils.addTab(gBrowser, url);
+
+ await TestUtils.waitForCondition(
+ async () =>
+ (await ChromeUtils.requestProcInfo()).children.some(p => p.type == "rdd"),
+ "waiting to find RDD process."
+ );
+
+ Services.fog.testResetFOG();
+
+ is(
+ null,
+ Glean.testOnlyIpc.aCounter.testGetValue(),
+ "Ensure we begin without value."
+ );
+
+ await TestUtils.waitForCondition(async () => {
+ try {
+ await Services.fog.testTriggerMetrics(Ci.nsIXULRuntime.PROCESS_TYPE_RDD);
+ return true;
+ } catch (e) {
+ return false;
+ }
+ }, "waiting until we can successfully send the TestTriggerMetrics IPC.");
+
+ await Services.fog.testFlushAllChildren();
+
+ is(
+ Glean.testOnlyIpc.aCounter.testGetValue(),
+ Ci.nsIXULRuntime.PROCESS_TYPE_RDD,
+ "Ensure the RDD-process-set value shows up in the parent process."
+ );
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/toolkit/components/glean/tests/browser/browser_fog_socket.js b/toolkit/components/glean/tests/browser/browser_fog_socket.js
new file mode 100644
index 0000000000..5ccf59dabb
--- /dev/null
+++ b/toolkit/components/glean/tests/browser/browser_fog_socket.js
@@ -0,0 +1,33 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async () => {
+ if (
+ !(await ChromeUtils.requestProcInfo()).children.some(
+ p => p.type == "socket"
+ )
+ ) {
+ ok(true, "No Socket process? No test.");
+ return;
+ }
+ ok(true, "Socket process found: Let's test.");
+
+ Services.fog.testResetFOG();
+
+ is(
+ null,
+ Glean.testOnlyIpc.aCounter.testGetValue(),
+ "Ensure we begin without value."
+ );
+
+ await Services.fog.testTriggerMetrics(Ci.nsIXULRuntime.PROCESS_TYPE_SOCKET);
+ await Services.fog.testFlushAllChildren();
+
+ is(
+ Glean.testOnlyIpc.aCounter.testGetValue(),
+ Ci.nsIXULRuntime.PROCESS_TYPE_SOCKET,
+ "Ensure the Socket-process-set value shows up in the parent process."
+ );
+});
diff --git a/toolkit/components/glean/tests/browser/browser_fog_utility.js b/toolkit/components/glean/tests/browser/browser_fog_utility.js
new file mode 100644
index 0000000000..2f6913eefc
--- /dev/null
+++ b/toolkit/components/glean/tests/browser/browser_fog_utility.js
@@ -0,0 +1,37 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async () => {
+ const utilityProcessTest = Cc[
+ "@mozilla.org/utility-process-test;1"
+ ].createInstance(Ci.nsIUtilityProcessTest);
+ await utilityProcessTest
+ .startProcess()
+ .then(async () => {
+ Services.fog.testResetFOG();
+
+ is(
+ null,
+ Glean.testOnlyIpc.aCounter.testGetValue(),
+ "Ensure we begin without value."
+ );
+
+ await Services.fog.testTriggerMetrics(
+ Ci.nsIXULRuntime.PROCESS_TYPE_UTILITY
+ );
+ await Services.fog.testFlushAllChildren();
+
+ is(
+ Glean.testOnlyIpc.aCounter.testGetValue(),
+ Ci.nsIXULRuntime.PROCESS_TYPE_UTILITY,
+ "Ensure the utility-process-set value shows up in the parent process."
+ );
+ })
+ .catch(async () => {
+ ok(false, "Cannot start Utility process?");
+ });
+
+ await utilityProcessTest.stopProcess();
+});
diff --git a/toolkit/components/glean/tests/browser/browser_labeled_gifft.js b/toolkit/components/glean/tests/browser/browser_labeled_gifft.js
new file mode 100644
index 0000000000..a6b5761c98
--- /dev/null
+++ b/toolkit/components/glean/tests/browser/browser_labeled_gifft.js
@@ -0,0 +1,54 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+function keyedScalarValue(aScalarName) {
+ let snapshot = Services.telemetry.getSnapshotForKeyedScalars();
+ return "parent" in snapshot ? snapshot.parent[aScalarName] : undefined;
+}
+
+add_task(async () => {
+ // Ensure we're starting with a clean slate. test-verify can be tricky.
+ Services.fog.testResetFOG();
+ Services.telemetry.clearScalars();
+
+ Assert.equal(
+ undefined,
+ Glean.testOnlyIpc.aLabeledCounter.a_label.testGetValue(),
+ "New labels with no values should return undefined"
+ );
+ Glean.testOnlyIpc.aLabeledCounter.a_label.add(1);
+ Glean.testOnlyIpc.aLabeledCounter.another_label.add(2);
+ Assert.equal(1, Glean.testOnlyIpc.aLabeledCounter.a_label.testGetValue());
+ Assert.equal(
+ 2,
+ Glean.testOnlyIpc.aLabeledCounter.another_label.testGetValue()
+ );
+ // What about invalid/__other__?
+ Assert.equal(
+ undefined,
+ Glean.testOnlyIpc.aLabeledCounter.__other__.testGetValue()
+ );
+ Glean.testOnlyIpc.aLabeledCounter["1".repeat(72)].add(3);
+ Assert.throws(
+ () => Glean.testOnlyIpc.aLabeledCounter.__other__.testGetValue(),
+ /DataError/,
+ "Can't get the value when you're error'd"
+ );
+
+ let value = keyedScalarValue(
+ "telemetry.test.another_mirror_for_labeled_counter"
+ );
+ Assert.deepEqual(
+ {
+ a_label: 1,
+ another_label: 2,
+ ["1".repeat(72)]: 3,
+ },
+ value
+ );
+
+ // AND NOW, FOR THE TRUE TEST:
+ // Will this leak memory all over the place?
+});
diff --git a/toolkit/components/glean/tests/browser/empty_file.html b/toolkit/components/glean/tests/browser/empty_file.html
new file mode 100644
index 0000000000..af8440ac16
--- /dev/null
+++ b/toolkit/components/glean/tests/browser/empty_file.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="UTF-8">
+ </head>
+ <body>
+ This page is intentionally left blank.
+ </body>
+</html>
diff --git a/toolkit/components/glean/tests/browser/small-shot.ogg b/toolkit/components/glean/tests/browser/small-shot.ogg
new file mode 100644
index 0000000000..1a41623f81
--- /dev/null
+++ b/toolkit/components/glean/tests/browser/small-shot.ogg
Binary files differ
diff --git a/toolkit/components/glean/tests/gtest/Cargo.toml b/toolkit/components/glean/tests/gtest/Cargo.toml
new file mode 100644
index 0000000000..a744517e56
--- /dev/null
+++ b/toolkit/components/glean/tests/gtest/Cargo.toml
@@ -0,0 +1,14 @@
+[package]
+name = "fog-gtest"
+version = "0.1.0"
+authors = ["glean-team@mozilla.com"]
+license = "MPL-2.0"
+description = "Tests for the FOG crate"
+
+[dependencies]
+firefox-on-glean = { path = "../../api" }
+jog = { path = "../../bindings/jog" }
+nsstring = { path = "../../../../../xpcom/rust/nsstring" }
+
+[lib]
+path = "test.rs"
diff --git a/toolkit/components/glean/tests/gtest/FOGFixture.h b/toolkit/components/glean/tests/gtest/FOGFixture.h
new file mode 100644
index 0000000000..da3c6fc123
--- /dev/null
+++ b/toolkit/components/glean/tests/gtest/FOGFixture.h
@@ -0,0 +1,23 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+#ifndef FOGFixture_h_
+#define FOGFixture_h_
+
+#include "gtest/gtest.h"
+#include "mozilla/glean/fog_ffi_generated.h"
+#include "nsString.h"
+
+using namespace mozilla::glean::impl;
+
+class FOGFixture : public ::testing::Test {
+ protected:
+ FOGFixture() = default;
+ virtual void SetUp() {
+ nsCString empty;
+ ASSERT_EQ(NS_OK, fog_test_reset(&empty, &empty));
+ }
+};
+
+#endif // FOGFixture_h_
diff --git a/toolkit/components/glean/tests/gtest/TestFog.cpp b/toolkit/components/glean/tests/gtest/TestFog.cpp
new file mode 100644
index 0000000000..2de8f9ba4d
--- /dev/null
+++ b/toolkit/components/glean/tests/gtest/TestFog.cpp
@@ -0,0 +1,470 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "FOGFixture.h"
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "mozilla/glean/GleanMetrics.h"
+#include "mozilla/glean/GleanPings.h"
+#include "mozilla/glean/fog_ffi_generated.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/Result.h"
+#include "mozilla/ResultVariant.h"
+
+#include "nsTArray.h"
+
+#include "mozilla/Preferences.h"
+#include "mozilla/Unused.h"
+#include "nsString.h"
+#include "prtime.h"
+
+using mozilla::Preferences;
+using namespace mozilla::glean;
+using namespace mozilla::glean::impl;
+
+#define DATA_PREF "datareporting.healthreport.uploadEnabled"
+
+extern "C" {
+// This function is called by the rust code in test.rs if a non-fatal test
+// failure occurs.
+void GTest_FOG_ExpectFailure(const char* aMessage) {
+ EXPECT_STREQ(aMessage, "");
+}
+}
+
+TEST_F(FOGFixture, BuiltinPingsRegistered) {
+ Preferences::SetInt("telemetry.fog.test.localhost_port", -1);
+ nsAutoCString metricsPingName("metrics");
+ nsAutoCString baselinePingName("baseline");
+ nsAutoCString eventsPingName("events");
+ ASSERT_EQ(NS_OK, fog_submit_ping(&metricsPingName));
+ ASSERT_EQ(NS_OK, fog_submit_ping(&baselinePingName));
+ ASSERT_EQ(NS_OK, fog_submit_ping(&eventsPingName));
+}
+
+TEST_F(FOGFixture, TestCppCounterWorks) {
+ mozilla::glean::test_only::bad_code.Add(42);
+
+ ASSERT_EQ(42, mozilla::glean::test_only::bad_code.TestGetValue("test-ping"_ns)
+ .unwrap()
+ .value());
+ // And test that the ping name's optional, while you're at it:
+ ASSERT_EQ(42, test_only::bad_code.TestGetValue().unwrap().value());
+}
+
+TEST_F(FOGFixture, TestCppStringWorks) {
+ auto kValue = "cheez!"_ns;
+ mozilla::glean::test_only::cheesy_string.Set(kValue);
+
+ ASSERT_STREQ(kValue.get(), mozilla::glean::test_only::cheesy_string
+ .TestGetValue("test-ping"_ns)
+ .unwrap()
+ .value()
+ .get());
+}
+
+TEST_F(FOGFixture, TestCppTimespanWorks) {
+ mozilla::glean::test_only::can_we_time_it.Start();
+ PR_Sleep(PR_MillisecondsToInterval(10));
+ mozilla::glean::test_only::can_we_time_it.Stop();
+
+ ASSERT_TRUE(
+ mozilla::glean::test_only::can_we_time_it.TestGetValue("test-ping"_ns)
+ .unwrap()
+ .value() > 0);
+}
+
+TEST_F(FOGFixture, TestCppUuidWorks) {
+ nsCString kTestUuid("decafdec-afde-cafd-ecaf-decafdecafde");
+ test_only::what_id_it.Set(kTestUuid);
+ ASSERT_STREQ(kTestUuid.get(),
+ test_only::what_id_it.TestGetValue("test-ping"_ns)
+ .unwrap()
+ .value()
+ .get());
+
+ test_only::what_id_it.GenerateAndSet();
+ // Since we generate v4 UUIDs, and the first character of the third group
+ // isn't 4, this won't ever collide with kTestUuid.
+ ASSERT_STRNE(kTestUuid.get(),
+ test_only::what_id_it.TestGetValue("test-ping"_ns)
+ .unwrap()
+ .value()
+ .get());
+}
+
+TEST_F(FOGFixture, TestCppBooleanWorks) {
+ mozilla::glean::test_only::can_we_flag_it.Set(false);
+
+ ASSERT_EQ(false, mozilla::glean::test_only::can_we_flag_it
+ .TestGetValue("test-ping"_ns)
+ .unwrap()
+ .value());
+}
+
+MATCHER_P(BitEq, x, "bit equal") {
+ static_assert(sizeof(x) == sizeof(arg));
+ return std::memcmp(&arg, &x, sizeof(x)) == 0;
+}
+
+TEST_F(FOGFixture, TestCppDatetimeWorks) {
+ PRExplodedTime date{0, 35, 10, 12, 6, 10, 2020, 0, 0, {5 * 60 * 60, 0}};
+ test_only::what_a_date.Set(&date);
+
+ auto received = test_only::what_a_date.TestGetValue("test-ping"_ns).unwrap();
+ ASSERT_THAT(received.value(), BitEq(date));
+}
+
+using mozilla::Some;
+using mozilla::glean::test_only_ipc::AnEventExtra;
+using mozilla::glean::test_only_ipc::EventWithExtraExtra;
+using std::tuple;
+
+TEST_F(FOGFixture, TestCppEventWorks) {
+ test_only_ipc::no_extra_event.Record();
+ ASSERT_TRUE(test_only_ipc::no_extra_event.TestGetValue("store1"_ns)
+ .unwrap()
+ .isSome());
+
+ AnEventExtra extra = {.extra1 = Some("can set extras"_ns)};
+ test_only_ipc::an_event.Record(Some(extra));
+ auto optEvents = test_only_ipc::an_event.TestGetValue("store1"_ns).unwrap();
+ ASSERT_TRUE(optEvents.isSome());
+
+ auto events = optEvents.extract();
+ ASSERT_EQ(1UL, events.Length());
+ ASSERT_STREQ("test_only.ipc", events[0].mCategory.get());
+ ASSERT_STREQ("an_event", events[0].mName.get());
+ ASSERT_EQ(1UL, events[0].mExtra.Length());
+ ASSERT_STREQ("extra1", std::get<0>(events[0].mExtra[0]).get());
+ ASSERT_STREQ("can set extras", std::get<1>(events[0].mExtra[0]).get());
+}
+
+TEST_F(FOGFixture, TestCppEventsWithDifferentExtraTypes) {
+ EventWithExtraExtra extra = {.extra1 = Some("can set extras"_ns),
+ .extra2 = Some(37),
+ .extra3LongerName = Some(false)};
+ test_only_ipc::event_with_extra.Record(Some(extra));
+ auto optEvents =
+ test_only_ipc::event_with_extra.TestGetValue("store1"_ns).unwrap();
+ ASSERT_TRUE(optEvents.isSome());
+
+ auto events = optEvents.extract();
+ ASSERT_EQ(1UL, events.Length());
+
+ // The list of extra key/value pairs can be in any order.
+ ASSERT_EQ(3UL, events[0].mExtra.Length());
+ for (auto extra : events[0].mExtra) {
+ auto key = std::get<0>(extra);
+ auto value = std::get<1>(extra);
+
+ if (key == "extra1"_ns) {
+ ASSERT_STREQ("can set extras", value.get());
+ } else if (key == "extra2"_ns) {
+ ASSERT_STREQ("37", value.get());
+ } else if (key == "extra3_longer_name"_ns) {
+ ASSERT_STREQ("false", value.get());
+ } else {
+ ASSERT_TRUE(false)
+ << "Invalid extra item recorded.";
+ }
+ }
+}
+
+TEST_F(FOGFixture, TestCppMemoryDistWorks) {
+ test_only::do_you_remember.Accumulate(7);
+ test_only::do_you_remember.Accumulate(17);
+
+ DistributionData data =
+ test_only::do_you_remember.TestGetValue("test-ping"_ns).unwrap().ref();
+ // Sum is in bytes, test_only::do_you_remember is in megabytes. So
+ // multiplication ahoy!
+ ASSERT_EQ(data.sum, 24UL * 1024 * 1024);
+ for (const auto& entry : data.values) {
+ const uint64_t bucket = entry.GetKey();
+ const uint64_t count = entry.GetData();
+ ASSERT_TRUE(count == 0 ||
+ (count == 1 && (bucket == 17520006 || bucket == 7053950)))
+ << "Only two occupied buckets";
+ }
+}
+
+TEST_F(FOGFixture, TestCppCustomDistWorks) {
+ test_only_ipc::a_custom_dist.AccumulateSamples({7, 268435458});
+
+ DistributionData data =
+ test_only_ipc::a_custom_dist.TestGetValue("store1"_ns).unwrap().ref();
+ ASSERT_EQ(data.sum, 7UL + 268435458);
+ for (const auto& entry : data.values) {
+ const uint64_t bucket = entry.GetKey();
+ const uint64_t count = entry.GetData();
+ ASSERT_TRUE(count == 0 ||
+ (count == 1 && (bucket == 1 || bucket == 268435456)))
+ << "Only two occupied buckets";
+ }
+}
+
+TEST_F(FOGFixture, TestCppPings) {
+ test_only::one_ping_one_bool.Set(false);
+ const auto& ping = mozilla::glean_pings::OnePingOnly;
+ bool submitted = false;
+ ping.TestBeforeNextSubmit([&submitted](const nsACString& aReason) {
+ submitted = true;
+ ASSERT_EQ(false,
+ test_only::one_ping_one_bool.TestGetValue().unwrap().ref());
+ });
+ ping.Submit();
+ ASSERT_TRUE(submitted)
+ << "Must have actually called the lambda.";
+}
+
+TEST_F(FOGFixture, TestCppStringLists) {
+ auto kValue = "cheez!"_ns;
+ auto kValue2 = "cheezier!"_ns;
+ auto kValue3 = "cheeziest."_ns;
+
+ nsTArray<nsCString> cheezList;
+ cheezList.EmplaceBack(kValue);
+ cheezList.EmplaceBack(kValue2);
+
+ test_only::cheesy_string_list.Set(cheezList);
+
+ auto val = test_only::cheesy_string_list.TestGetValue().unwrap().value();
+ // Note: This is fragile if the order is ever not preserved.
+ ASSERT_STREQ(kValue.get(), val[0].get());
+ ASSERT_STREQ(kValue2.get(), val[1].get());
+
+ test_only::cheesy_string_list.Add(kValue3);
+
+ val = test_only::cheesy_string_list.TestGetValue().unwrap().value();
+ ASSERT_STREQ(kValue3.get(), val[2].get());
+}
+
+TEST_F(FOGFixture, TestCppTimingDistWorks) {
+ auto id1 = test_only::what_time_is_it.Start();
+ auto id2 = test_only::what_time_is_it.Start();
+ PR_Sleep(PR_MillisecondsToInterval(5));
+ auto id3 = test_only::what_time_is_it.Start();
+ test_only::what_time_is_it.Cancel(std::move(id1));
+ PR_Sleep(PR_MillisecondsToInterval(5));
+ test_only::what_time_is_it.StopAndAccumulate(std::move(id2));
+ test_only::what_time_is_it.StopAndAccumulate(std::move(id3));
+
+ DistributionData data =
+ test_only::what_time_is_it.TestGetValue().unwrap().ref();
+ const uint64_t NANOS_IN_MILLIS = 1e6;
+
+ // bug 1701847 - Sleeps don't necessarily round up as you'd expect.
+ // Give ourselves a 200000ns (0.2ms) window to be off on fast machines.
+ const uint64_t EPSILON = 200000;
+
+ // We don't know exactly how long those sleeps took, only that it was at
+ // least 15ms total.
+ ASSERT_GT(data.sum, (uint64_t)(15 * NANOS_IN_MILLIS) - EPSILON);
+
+ // We also can't guarantee the buckets, but we can guarantee two samples.
+ uint64_t sampleCount = 0;
+ for (const auto& value : data.values.Values()) {
+ sampleCount += value;
+ }
+ ASSERT_EQ(sampleCount, (uint64_t)2);
+}
+
+TEST_F(FOGFixture, TestLabeledBooleanWorks) {
+ ASSERT_EQ(mozilla::Nothing(),
+ test_only::mabels_like_balloons.Get("hot_air"_ns)
+ .TestGetValue()
+ .unwrap());
+ test_only::mabels_like_balloons.Get("hot_air"_ns).Set(true);
+ test_only::mabels_like_balloons.Get("helium"_ns).Set(false);
+ ASSERT_EQ(true, test_only::mabels_like_balloons.Get("hot_air"_ns)
+ .TestGetValue()
+ .unwrap()
+ .ref());
+ ASSERT_EQ(false, test_only::mabels_like_balloons.Get("helium"_ns)
+ .TestGetValue()
+ .unwrap()
+ .ref());
+}
+
+TEST_F(FOGFixture, TestLabeledBooleanWithLabelsWorks) {
+ ASSERT_EQ(mozilla::Nothing(),
+ test_only::mabels_like_labeled_balloons
+ .EnumGet(test_only::MabelsLikeLabeledBalloonsLabel::eWater)
+ .TestGetValue()
+ .unwrap());
+ test_only::mabels_like_labeled_balloons
+ .EnumGet(test_only::MabelsLikeLabeledBalloonsLabel::eWater)
+ .Set(true);
+ test_only::mabels_like_labeled_balloons
+ .EnumGet(test_only::MabelsLikeLabeledBalloonsLabel::eBirthdayParty)
+ .Set(false);
+ ASSERT_EQ(true,
+ test_only::mabels_like_labeled_balloons
+ .EnumGet(test_only::MabelsLikeLabeledBalloonsLabel::eWater)
+ .TestGetValue()
+ .unwrap()
+ .ref());
+ ASSERT_EQ(
+ false,
+ test_only::mabels_like_labeled_balloons
+ .EnumGet(test_only::MabelsLikeLabeledBalloonsLabel::eBirthdayParty)
+ .TestGetValue()
+ .unwrap()
+ .ref());
+}
+
+TEST_F(FOGFixture, TestLabeledCounterWorks) {
+ ASSERT_EQ(mozilla::Nothing(),
+ test_only::mabels_kitchen_counters.Get("marble"_ns)
+ .TestGetValue()
+ .unwrap());
+ test_only::mabels_kitchen_counters.Get("marble"_ns).Add(1);
+ test_only::mabels_kitchen_counters.Get("laminate"_ns).Add(2);
+ ASSERT_EQ(1, test_only::mabels_kitchen_counters.Get("marble"_ns)
+ .TestGetValue()
+ .unwrap()
+ .ref());
+ ASSERT_EQ(2, test_only::mabels_kitchen_counters.Get("laminate"_ns)
+ .TestGetValue()
+ .unwrap()
+ .ref());
+}
+
+TEST_F(FOGFixture, TestLabeledCounterWithLabelsWorks) {
+ ASSERT_EQ(
+ mozilla::Nothing(),
+ test_only::mabels_labeled_counters
+ .EnumGet(test_only::MabelsLabeledCountersLabel::eNextToTheFridge)
+ .TestGetValue()
+ .unwrap());
+ test_only::mabels_labeled_counters
+ .EnumGet(test_only::MabelsLabeledCountersLabel::eNextToTheFridge)
+ .Add(1);
+ test_only::mabels_labeled_counters
+ .EnumGet(test_only::MabelsLabeledCountersLabel::eClean)
+ .Add(2);
+ ASSERT_EQ(
+ 1, test_only::mabels_labeled_counters
+ .EnumGet(test_only::MabelsLabeledCountersLabel::eNextToTheFridge)
+ .TestGetValue()
+ .unwrap()
+ .ref());
+ ASSERT_EQ(2, test_only::mabels_labeled_counters
+ .EnumGet(test_only::MabelsLabeledCountersLabel::eClean)
+ .TestGetValue()
+ .unwrap()
+ .ref());
+}
+
+TEST_F(FOGFixture, TestLabeledStringWorks) {
+ ASSERT_EQ(mozilla::Nothing(),
+ test_only::mabels_balloon_strings.Get("twine"_ns)
+ .TestGetValue()
+ .unwrap());
+ test_only::mabels_balloon_strings.Get("twine"_ns).Set("seems acceptable"_ns);
+ test_only::mabels_balloon_strings.Get("parachute_cord"_ns)
+ .Set("preferred"_ns);
+ ASSERT_STREQ("seems acceptable",
+ test_only::mabels_balloon_strings.Get("twine"_ns)
+ .TestGetValue()
+ .unwrap()
+ .ref()
+ .get());
+ ASSERT_STREQ("preferred",
+ test_only::mabels_balloon_strings.Get("parachute_cord"_ns)
+ .TestGetValue()
+ .unwrap()
+ .ref()
+ .get());
+}
+
+TEST_F(FOGFixture, TestLabeledStringWithLabelsWorks) {
+ ASSERT_EQ(mozilla::Nothing(),
+ test_only::mabels_balloon_labels
+ .EnumGet(test_only::MabelsBalloonLabelsLabel::eCelebratory)
+ .TestGetValue()
+ .unwrap());
+ test_only::mabels_balloon_labels
+ .EnumGet(test_only::MabelsBalloonLabelsLabel::eCelebratory)
+ .Set("for birthdays, etc."_ns);
+ test_only::mabels_balloon_labels
+ .EnumGet(test_only::MabelsBalloonLabelsLabel::eCelebratoryAndSnarky)
+ .Set("for retirements and bridal showers"_ns);
+ ASSERT_STREQ("for birthdays, etc.",
+ test_only::mabels_balloon_labels
+ .EnumGet(test_only::MabelsBalloonLabelsLabel::eCelebratory)
+ .TestGetValue()
+ .unwrap()
+ .ref()
+ .get());
+ ASSERT_STREQ(
+ "for retirements and bridal showers",
+ test_only::mabels_balloon_labels
+ .EnumGet(test_only::MabelsBalloonLabelsLabel::eCelebratoryAndSnarky)
+ .TestGetValue()
+ .unwrap()
+ .ref()
+ .get());
+}
+
+TEST_F(FOGFixture, TestCppQuantityWorks) {
+ // This joke only works in base 13.
+ const uint32_t kValue = 6 * 9;
+ mozilla::glean::test_only::meaning_of_life.Set(kValue);
+
+ ASSERT_EQ(kValue, mozilla::glean::test_only::meaning_of_life.TestGetValue()
+ .unwrap()
+ .value());
+}
+
+TEST_F(FOGFixture, TestCppRateWorks) {
+ // 1) Standard rate with internal denominator
+ const int32_t kNum = 22;
+ const int32_t kDen = 7; // because I like pi, even just approximately.
+
+ test_only_ipc::irate.AddToNumerator(kNum);
+ test_only_ipc::irate.AddToDenominator(kDen);
+ auto value = test_only_ipc::irate.TestGetValue().unwrap();
+ ASSERT_EQ(kNum, value.ref().first);
+ ASSERT_EQ(kDen, value.ref().second);
+
+ // 2) Rate with external denominator
+ test_only_ipc::rate_with_external_denominator.AddToNumerator(kNum);
+ test_only_ipc::an_external_denominator.Add(kDen);
+ value = test_only_ipc::rate_with_external_denominator.TestGetValue().unwrap();
+ ASSERT_EQ(kNum, value.ref().first);
+ ASSERT_EQ(kDen, value.ref().second);
+ ASSERT_EQ(
+ kDen,
+ test_only_ipc::an_external_denominator.TestGetValue().unwrap().extract());
+}
+
+TEST_F(FOGFixture, TestCppUrlWorks) {
+ auto kValue = "https://example.com/fog/gtest"_ns;
+ mozilla::glean::test_only_ipc::a_url.Set(kValue);
+
+ ASSERT_STREQ(kValue.get(),
+ mozilla::glean::test_only_ipc::a_url.TestGetValue("store1"_ns)
+ .unwrap()
+ .value()
+ .get());
+}
+
+TEST_F(FOGFixture, TestCppTextWorks) {
+ auto kValue =
+ "République is a French-inspired bakery, café, bar and formal dining room located in the Mid-city neighborhood of Los Angeles, set in a historic 1928 space ..."_ns;
+ mozilla::glean::test_only_ipc::a_text.Set(kValue);
+ ASSERT_STREQ(kValue.get(),
+ mozilla::glean::test_only_ipc::a_text.TestGetValue()
+ .unwrap()
+ .value()
+ .get());
+}
+
+extern "C" void Rust_TestRustInGTest();
+TEST_F(FOGFixture, TestRustInGTest) { Rust_TestRustInGTest(); }
+
+extern "C" void Rust_TestJogfile();
+TEST_F(FOGFixture, TestJogfile) { Rust_TestJogfile(); }
diff --git a/toolkit/components/glean/tests/gtest/moz.build b/toolkit/components/glean/tests/gtest/moz.build
new file mode 100644
index 0000000000..d17ee0e0dd
--- /dev/null
+++ b/toolkit/components/glean/tests/gtest/moz.build
@@ -0,0 +1,14 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+if not CONFIG["MOZ_GLEAN_ANDROID"]:
+ UNIFIED_SOURCES += [
+ "TestFog.cpp",
+ ]
+
+ TEST_HARNESS_FILES.gtest += ["../pytest/jogfile_output"]
+
+FINAL_LIBRARY = "xul-gtest"
diff --git a/toolkit/components/glean/tests/gtest/test.rs b/toolkit/components/glean/tests/gtest/test.rs
new file mode 100644
index 0000000000..90ad67aebb
--- /dev/null
+++ b/toolkit/components/glean/tests/gtest/test.rs
@@ -0,0 +1,55 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+extern crate firefox_on_glean;
+use firefox_on_glean::metrics;
+
+extern crate nsstring;
+use nsstring::nsString;
+
+fn nonfatal_fail(msg: String) {
+ extern "C" {
+ fn GTest_FOG_ExpectFailure(message: *const ::std::os::raw::c_char);
+ }
+ unsafe {
+ let msg = ::std::ffi::CString::new(msg).unwrap();
+ GTest_FOG_ExpectFailure(msg.as_ptr());
+ }
+}
+
+/// This macro checks if the expression evaluates to true,
+/// and causes a non-fatal GTest test failure if it doesn't.
+macro_rules! expect {
+ ($x:expr) => {
+ match (&$x) {
+ true => {}
+ false => nonfatal_fail(format!(
+ "check failed: (`{}`) at {}:{}",
+ stringify!($x),
+ file!(),
+ line!()
+ )),
+ }
+ };
+}
+
+#[no_mangle]
+pub extern "C" fn Rust_TestRustInGTest() {
+ // Just a smoke test, we show here how tests might work that both
+ // a) Are in Rust, and
+ // b) Require Gecko
+ // This demonstration doesn't actually require Gecko. But we pretend it
+ // does so we remember how to do this rust-in-gtest pattern.
+ metrics::test_only::bad_code.add(12);
+ expect!(metrics::test_only::bad_code.test_get_value(None) == Some(12));
+}
+
+#[no_mangle]
+pub extern "C" fn Rust_TestJogfile() {
+ // Ensure that the generated jogfile in t/c/g/tests/pytest
+ // (which is installed nearby using TEST_HARNESS_FILES)
+ // can be consumed by JOG's inner workings
+ //
+ // If it can't, that's perhaps a sign that the inner workings need to be updated.
+ expect!(jog::jog_load_jogfile(&nsString::from("jogfile_output")));
+}
diff --git a/toolkit/components/glean/tests/moz.build b/toolkit/components/glean/tests/moz.build
new file mode 100644
index 0000000000..7b7a654b8f
--- /dev/null
+++ b/toolkit/components/glean/tests/moz.build
@@ -0,0 +1,16 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+PYTHON_UNITTEST_MANIFESTS += ["pytest/python.toml"]
+
+TEST_DIRS += ["gtest"]
+
+XPCSHELL_TESTS_MANIFESTS += ["xpcshell/xpcshell.toml"]
+
+BROWSER_CHROME_MANIFESTS += ["browser/browser.toml"]
+
+with Files("**"):
+ BUG_COMPONENT = ("Toolkit", "Telemetry")
diff --git a/toolkit/components/glean/tests/pytest/expect_helper.py b/toolkit/components/glean/tests/pytest/expect_helper.py
new file mode 100644
index 0000000000..543ef04026
--- /dev/null
+++ b/toolkit/components/glean/tests/pytest/expect_helper.py
@@ -0,0 +1,34 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import inspect
+import os
+
+
+def expect(path, actual):
+ """
+ Assert that the content of the file at `path` contains `actual`.
+
+ If the environment variable `UPDATE_EXPECT` is set, the path content is updated to `actual`.
+ This allows to update the file contents easily.
+ """
+
+ callerframerecord = inspect.stack()[1]
+ frame = callerframerecord[0]
+ info = inspect.getframeinfo(frame)
+ msg = f"""
+Unexpected content in {path} (at {info.filename}:{info.lineno})
+
+If the code generation was changed,
+run the test suite again with `UPDATE_EXPECT=1` set to update the test files.
+""".strip()
+
+ if "UPDATE_EXPECT" in os.environ:
+ with open(path, "w") as file:
+ file.write(actual)
+
+ expected = None
+ with open(path, "r") as file:
+ expected = file.read()
+ assert actual == expected, msg
diff --git a/toolkit/components/glean/tests/pytest/gifft_output_Event b/toolkit/components/glean/tests/pytest/gifft_output_Event
new file mode 100644
index 0000000000..ba80e3300b
--- /dev/null
+++ b/toolkit/components/glean/tests/pytest/gifft_output_Event
@@ -0,0 +1,67 @@
+// -*- mode: C++ -*-
+
+/* This file is auto-generated by run_glean_parser.py.
+ It is only for internal use by types in
+ toolkit/components/glean/bindings/private */
+
+#include "mozilla/AppShutdown.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/glean/bindings/GleanJSMetricsLookup.h"
+#include "mozilla/glean/bindings/jog/JOG.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/Telemetry.h"
+#include "nsIThread.h"
+#include "nsThreadUtils.h"
+
+#ifndef mozilla_glean_EventGifftMap_h
+#define mozilla_glean_EventGifftMap_h
+
+#define DYNAMIC_METRIC_BIT (26)
+#define GLEAN_METRIC_ID(id) ((id) & ((1ULL << 27) - 1))
+
+namespace mozilla::glean {
+
+using Telemetry::EventID;
+
+
+static inline Maybe<EventID> EventIdForMetric(uint32_t aId) {
+ switch(aId) {
+ case 17: { // test.nested.event_metric
+ return Some(EventID::EventMetric_EnumNames_AreStrange);
+ }
+ case 18: { // test.nested.event_metric_with_extra
+ return Some(EventID::EventMetric_EnumName_WithExtra);
+ }
+ default: {
+ if (MOZ_UNLIKELY(aId & (1 << DYNAMIC_METRIC_BIT))) {
+ // Dynamic (runtime-registered) metric. Use its static (compiletime-
+ // registered) metric's telemetry_mirror mapping.
+ // ...if applicable.
+
+ // Only JS can use dynamic (runtime-registered) metric ids.
+ MOZ_ASSERT(NS_IsMainThread());
+
+ auto metricName = JOG::GetMetricName(aId);
+ // All of these should have names, but the storage only lasts until
+ // XPCOMWillShutdown, so it might return `Nothing()`.
+ if (metricName.isSome()) {
+ auto maybeMetric = MetricByNameLookup(metricName.ref());
+ if (maybeMetric.isSome()) {
+ uint32_t staticId = GLEAN_METRIC_ID(maybeMetric.value());
+ // Let's ensure we don't infinite loop, huh.
+ MOZ_ASSERT(!(staticId & (1 << DYNAMIC_METRIC_BIT)));
+ return EventIdForMetric(staticId);
+ }
+ }
+ }
+ return Nothing();
+ }
+ }
+}
+
+} // namespace mozilla::glean
+
+#undef GLEAN_METRIC_ID
+#undef DYNAMIC_METRIC_BIT
+
+#endif // mozilla_glean_EventGifftMaps_h
diff --git a/toolkit/components/glean/tests/pytest/gifft_output_EventExtra b/toolkit/components/glean/tests/pytest/gifft_output_EventExtra
new file mode 100644
index 0000000000..f5f2671d99
--- /dev/null
+++ b/toolkit/components/glean/tests/pytest/gifft_output_EventExtra
@@ -0,0 +1,35 @@
+// -*- mode: C++ -*-
+
+/* This file is auto-generated by run_glean_parser.py.
+ It is only for internal use by types in
+ toolkit/components/glean/bindings/private */
+
+#include "mozilla/glean/bindings/Event.h"
+#include "mozilla/glean/GleanMetrics.h"
+
+namespace mozilla::glean {
+
+template <>
+/*static*/ const nsCString impl::EventMetric<NoExtraKeys>::ExtraStringForKey(uint32_t aKey) {
+ MOZ_ASSERT_UNREACHABLE("What are you doing here? No extra keys!");
+ return ""_ns;
+}
+
+template <>
+/*static*/ const nsCString impl::EventMetric<test_nested::EventMetricWithExtraExtra>::ExtraStringForKey(uint32_t aKey) {
+ using test_nested::EventMetricWithExtraExtra;
+ switch (aKey) {
+ case 0: {
+ return "an_extra_key"_ns;
+ }
+ case 1: {
+ return "another_extra_key"_ns;
+ }
+ default: {
+ MOZ_ASSERT_UNREACHABLE("Impossible event key reached.");
+ return ""_ns;
+ }
+ }
+}
+
+}; // namespace mozilla::glean
diff --git a/toolkit/components/glean/tests/pytest/gifft_output_Histogram b/toolkit/components/glean/tests/pytest/gifft_output_Histogram
new file mode 100644
index 0000000000..40a33dd27f
--- /dev/null
+++ b/toolkit/components/glean/tests/pytest/gifft_output_Histogram
@@ -0,0 +1,70 @@
+// -*- mode: C++ -*-
+
+/* This file is auto-generated by run_glean_parser.py.
+ It is only for internal use by types in
+ toolkit/components/glean/bindings/private */
+
+#include "mozilla/AppShutdown.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/glean/bindings/GleanJSMetricsLookup.h"
+#include "mozilla/glean/bindings/jog/JOG.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/Telemetry.h"
+#include "nsIThread.h"
+#include "nsThreadUtils.h"
+
+#ifndef mozilla_glean_HistogramGifftMap_h
+#define mozilla_glean_HistogramGifftMap_h
+
+#define DYNAMIC_METRIC_BIT (26)
+#define GLEAN_METRIC_ID(id) ((id) & ((1ULL << 27) - 1))
+
+namespace mozilla::glean {
+
+using Telemetry::HistogramID;
+
+
+static Maybe<HistogramID> HistogramIdForMetric(uint32_t aId) {
+ switch(aId) {
+ case 3: { // test.custom_distribution_metric
+ return Some(HistogramID::SOME_LINEAR_HISTOGRAM);
+ }
+ case 10: { // test.memory_distribution_metric
+ return Some(HistogramID::SOME_MEM_HISTOGRAM_KB);
+ }
+ case 15: { // test.timing_distribution_metric
+ return Some(HistogramID::SOME_TIME_HISTOGRAM_MS);
+ }
+ default: {
+ if (MOZ_UNLIKELY(aId & (1 << DYNAMIC_METRIC_BIT))) {
+ // Dynamic (runtime-registered) metric. Use its static (compiletime-
+ // registered) metric's telemetry_mirror mapping.
+ // ...if applicable.
+
+ // Only JS can use dynamic (runtime-registered) metric ids.
+ MOZ_ASSERT(NS_IsMainThread());
+
+ auto metricName = JOG::GetMetricName(aId);
+ // All of these should have names, but the storage only lasts until
+ // XPCOMWillShutdown, so it might return `Nothing()`.
+ if (metricName.isSome()) {
+ auto maybeMetric = MetricByNameLookup(metricName.ref());
+ if (maybeMetric.isSome()) {
+ uint32_t staticId = GLEAN_METRIC_ID(maybeMetric.value());
+ // Let's ensure we don't infinite loop, huh.
+ MOZ_ASSERT(!(staticId & (1 << DYNAMIC_METRIC_BIT)));
+ return HistogramIdForMetric(staticId);
+ }
+ }
+ }
+ return Nothing();
+ }
+ }
+}
+
+} // namespace mozilla::glean
+
+#undef GLEAN_METRIC_ID
+#undef DYNAMIC_METRIC_BIT
+
+#endif // mozilla_glean_HistogramGifftMaps_h
diff --git a/toolkit/components/glean/tests/pytest/gifft_output_Scalar b/toolkit/components/glean/tests/pytest/gifft_output_Scalar
new file mode 100644
index 0000000000..854455b584
--- /dev/null
+++ b/toolkit/components/glean/tests/pytest/gifft_output_Scalar
@@ -0,0 +1,102 @@
+// -*- mode: C++ -*-
+
+/* This file is auto-generated by run_glean_parser.py.
+ It is only for internal use by types in
+ toolkit/components/glean/bindings/private */
+
+#include "mozilla/AppShutdown.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/glean/bindings/GleanJSMetricsLookup.h"
+#include "mozilla/glean/bindings/jog/JOG.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/Telemetry.h"
+#include "nsIThread.h"
+#include "nsThreadUtils.h"
+
+#ifndef mozilla_glean_ScalarGifftMap_h
+#define mozilla_glean_ScalarGifftMap_h
+
+#define DYNAMIC_METRIC_BIT (26)
+#define GLEAN_METRIC_ID(id) ((id) & ((1ULL << 27) - 1))
+
+namespace mozilla::glean {
+
+using Telemetry::ScalarID;
+
+static inline bool IsSubmetricId(uint32_t aId) {
+ // Submetrics have the 2^25 bit set.
+ // (ID_BITS - ID_SIGNAL_BITS, keep it in sync with js.py).
+ return (aId & (1 << 25)) > 0;
+}
+
+static inline Maybe<ScalarID> ScalarIdForMetric(uint32_t aId) {
+ switch(aId) {
+ case 1: { // test.boolean_metric
+ return Some(ScalarID::SOME_BOOL_SCALAR);
+ }
+ case 2: { // test.counter_metric
+ return Some(ScalarID::SOME_UINT_SCALAR);
+ }
+ case 4: { // test.labeled_boolean_metric
+ return Some(ScalarID::SOME_KEYED_BOOL_SCALAR);
+ }
+ case 5: { // test.labeled_boolean_metric_labels
+ return Some(ScalarID::SOME_OTHER_KEYED_BOOL_SCALAR);
+ }
+ case 6: { // test.labeled_counter_metric
+ return Some(ScalarID::SOME_KEYED_UINT_SCALAR);
+ }
+ case 7: { // test.labeled_counter_metric_labels
+ return Some(ScalarID::SOME_OTHER_KEYED_UINT_SCALAR);
+ }
+ case 11: { // test.string_list_metric
+ return Some(ScalarID::YET_ANOTHER_KEYED_BOOL_SCALAR);
+ }
+ case 12: { // test.string_metric
+ return Some(ScalarID::SOME_STRING_SCALAR);
+ }
+ case 14: { // test.timespan_metric
+ return Some(ScalarID::SOME_OTHER_UINT_SCALAR);
+ }
+ case 16: { // test.nested.datetime_metric
+ return Some(ScalarID::SOME_STILL_OTHER_STRING_SCALAR);
+ }
+ case 21: { // test.nested.quantity_metric
+ return Some(ScalarID::TELEMETRY_TEST_MIRROR_FOR_QUANTITY);
+ }
+ case 24: { // test.nested.uuid_metric
+ return Some(ScalarID::SOME_OTHER_STRING_SCALAR);
+ }
+ default: {
+ if (MOZ_UNLIKELY(aId & (1 << DYNAMIC_METRIC_BIT))) {
+ // Dynamic (runtime-registered) metric. Use its static (compiletime-
+ // registered) metric's telemetry_mirror mapping.
+ // ...if applicable.
+
+ // Only JS can use dynamic (runtime-registered) metric ids.
+ MOZ_ASSERT(NS_IsMainThread());
+
+ auto metricName = JOG::GetMetricName(aId);
+ // All of these should have names, but the storage only lasts until
+ // XPCOMWillShutdown, so it might return `Nothing()`.
+ if (metricName.isSome()) {
+ auto maybeMetric = MetricByNameLookup(metricName.ref());
+ if (maybeMetric.isSome()) {
+ uint32_t staticId = GLEAN_METRIC_ID(maybeMetric.value());
+ // Let's ensure we don't infinite loop, huh.
+ MOZ_ASSERT(!(staticId & (1 << DYNAMIC_METRIC_BIT)));
+ return ScalarIdForMetric(staticId);
+ }
+ }
+ }
+ return Nothing();
+ }
+ }
+}
+
+} // namespace mozilla::glean
+
+#undef GLEAN_METRIC_ID
+#undef DYNAMIC_METRIC_BIT
+
+#endif // mozilla_glean_ScalarGifftMaps_h
diff --git a/toolkit/components/glean/tests/pytest/jogfile_output b/toolkit/components/glean/tests/pytest/jogfile_output
new file mode 100644
index 0000000000..d75c08c8ba
--- /dev/null
+++ b/toolkit/components/glean/tests/pytest/jogfile_output
@@ -0,0 +1,344 @@
+{
+ "metrics": {
+ "test": [
+ [
+ "boolean",
+ "boolean_metric",
+ [
+ "metrics"
+ ],
+ "application",
+ false
+ ],
+ [
+ "counter",
+ "counter_metric",
+ [
+ "metrics"
+ ],
+ "application",
+ false
+ ],
+ [
+ "custom_distribution",
+ "custom_distribution_metric",
+ [
+ "metrics"
+ ],
+ "application",
+ false,
+ {
+ "bucket_count": 100,
+ "histogram_type": "linear",
+ "range_max": 100,
+ "range_min": 0
+ }
+ ],
+ [
+ "labeled_boolean",
+ "labeled_boolean_metric",
+ [
+ "metrics"
+ ],
+ "application",
+ false,
+ {
+ "ordered_labels": null
+ }
+ ],
+ [
+ "labeled_boolean",
+ "labeled_boolean_metric_labels",
+ [
+ "metrics"
+ ],
+ "application",
+ false,
+ {
+ "ordered_labels": [
+ "one_label",
+ "two_labels",
+ "three_labels",
+ "four_labels",
+ "five_labels",
+ "six_labels",
+ "seven_labels",
+ "eight_labels",
+ "nine_labels",
+ "ten_labels"
+ ]
+ }
+ ],
+ [
+ "labeled_counter",
+ "labeled_counter_metric",
+ [
+ "metrics"
+ ],
+ "application",
+ false,
+ {
+ "ordered_labels": null
+ }
+ ],
+ [
+ "labeled_counter",
+ "labeled_counter_metric_labels",
+ [
+ "metrics"
+ ],
+ "application",
+ false,
+ {
+ "ordered_labels": [
+ "one_label",
+ "two_labels"
+ ]
+ }
+ ],
+ [
+ "labeled_string",
+ "labeled_string_metric",
+ [
+ "metrics"
+ ],
+ "application",
+ false,
+ {
+ "ordered_labels": null
+ }
+ ],
+ [
+ "labeled_string",
+ "labeled_string_metric_labels",
+ [
+ "metrics"
+ ],
+ "application",
+ false,
+ {
+ "ordered_labels": [
+ "one_label",
+ "two_labels"
+ ]
+ }
+ ],
+ [
+ "memory_distribution",
+ "memory_distribution_metric",
+ [
+ "metrics"
+ ],
+ "application",
+ false,
+ {
+ "memory_unit": "kilobyte"
+ }
+ ],
+ [
+ "string_list",
+ "string_list_metric",
+ [
+ "metrics"
+ ],
+ "application",
+ false
+ ],
+ [
+ "string",
+ "string_metric",
+ [
+ "metrics"
+ ],
+ "application",
+ false
+ ],
+ [
+ "text",
+ "text_metric",
+ [
+ "metrics"
+ ],
+ "application",
+ false
+ ],
+ [
+ "timespan",
+ "timespan_metric",
+ [
+ "metrics"
+ ],
+ "application",
+ false,
+ {
+ "time_unit": "millisecond"
+ }
+ ],
+ [
+ "timing_distribution",
+ "timing_distribution_metric",
+ [
+ "metrics"
+ ],
+ "application",
+ false,
+ {
+ "time_unit": "nanosecond"
+ }
+ ]
+ ],
+ "test.nested": [
+ [
+ "datetime",
+ "datetime_metric",
+ [
+ "metrics"
+ ],
+ "application",
+ false,
+ {
+ "time_unit": "millisecond"
+ }
+ ],
+ [
+ "event",
+ "event_metric",
+ [
+ "events"
+ ],
+ "ping",
+ false,
+ {
+ "allowed_extra_keys": []
+ }
+ ],
+ [
+ "event",
+ "event_metric_with_extra",
+ [
+ "events"
+ ],
+ "ping",
+ false,
+ {
+ "allowed_extra_keys": [
+ "an_extra_key",
+ "another_extra_key"
+ ]
+ }
+ ],
+ [
+ "denominator",
+ "external_denominator",
+ [
+ "metrics"
+ ],
+ "ping",
+ false,
+ {
+ "numerators": [
+ [
+ "rate_with_external_denominator",
+ "test.nested",
+ [
+ "metrics"
+ ],
+ "ping",
+ false,
+ null
+ ]
+ ]
+ }
+ ],
+ [
+ "counter",
+ "optimizable_counter_metric",
+ [
+ "metrics"
+ ],
+ "ping",
+ false
+ ],
+ [
+ "quantity",
+ "quantity_metric",
+ [
+ "metrics"
+ ],
+ "ping",
+ false
+ ],
+ [
+ "rate",
+ "rate_metric",
+ [
+ "metrics"
+ ],
+ "ping",
+ false
+ ],
+ [
+ "rate",
+ "rate_with_external_denominator",
+ [
+ "metrics"
+ ],
+ "ping",
+ false
+ ],
+ [
+ "uuid",
+ "uuid_metric",
+ [
+ "metrics"
+ ],
+ "application",
+ false
+ ]
+ ]
+ },
+ "pings": [
+ [
+ "not-baseline",
+ true,
+ false,
+ true,
+ [
+ "background",
+ "dirty_startup",
+ "foreground"
+ ]
+ ],
+ [
+ "not-deletion-request",
+ true,
+ true,
+ true,
+ []
+ ],
+ [
+ "not-events",
+ true,
+ false,
+ true,
+ [
+ "background",
+ "max_capacity",
+ "startup"
+ ]
+ ],
+ [
+ "not-metrics",
+ true,
+ false,
+ true,
+ [
+ "overdue",
+ "reschedule",
+ "today",
+ "tomorrow",
+ "upgrade"
+ ]
+ ]
+ ]
+} \ No newline at end of file
diff --git a/toolkit/components/glean/tests/pytest/metrics_expires_versions_test.yaml b/toolkit/components/glean/tests/pytest/metrics_expires_versions_test.yaml
new file mode 100644
index 0000000000..ea73a6465e
--- /dev/null
+++ b/toolkit/components/glean/tests/pytest/metrics_expires_versions_test.yaml
@@ -0,0 +1,84 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+# This file is FOR TESTING PURPOSES ONLY.
+
+---
+$schema: moz://mozilla.org/schemas/glean/metrics/2-0-0
+
+test:
+ expired1:
+ type: boolean
+ expires: 41
+ description: |
+ A multi-line
+ description
+ lifetime: application
+ notification_emails:
+ - glean-team@mozilla.com
+ bugs:
+ - https://bugzilla.mozilla.org/1664306
+ data_reviews:
+ - https://example.com
+ no_lint:
+ - EXPIRED
+
+ expired2:
+ type: labeled_boolean
+ expires: 42
+ description: |
+ A multi-line
+ description
+ lifetime: application
+ notification_emails:
+ - glean-team@mozilla.com
+ bugs:
+ - https://bugzilla.mozilla.org/1664306
+ data_reviews:
+ - https://example.com
+ no_lint:
+ - EXPIRED
+
+ unexpired:
+ type: labeled_boolean
+ expires: 100
+ description: |
+ A multi-line
+ description
+ lifetime: application
+ notification_emails:
+ - glean-team@mozilla.com
+ bugs:
+ - https://bugzilla.mozilla.org/1664306
+ data_reviews:
+ - https://example.com
+ labels:
+ - one_label
+ - two_labels
+
+ never:
+ type: string
+ expires: never
+ description: A never-expiring metric
+ lifetime: application
+ notification_emails:
+ - glean-team@mozilla.com
+ bugs:
+ - https://bugzilla.mozilla.org/1664306
+ data_reviews:
+ - https://example.com
+
+ always:
+ type: string
+ expires: expired
+ description: An already-expired metric
+ lifetime: application
+ notification_emails:
+ - glean-team@mozilla.com
+ bugs:
+ - https://bugzilla.mozilla.org/1664306
+ data_reviews:
+ - https://example.com
+ no_lint:
+ - EXPIRED
diff --git a/toolkit/components/glean/tests/pytest/metrics_test.yaml b/toolkit/components/glean/tests/pytest/metrics_test.yaml
new file mode 100644
index 0000000000..cbbb0220b5
--- /dev/null
+++ b/toolkit/components/glean/tests/pytest/metrics_test.yaml
@@ -0,0 +1,398 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+# This file defines the metrics that are recorded by the Glean SDK. They are
+# automatically converted to platform-specific code at build time using the
+# `glean_parser` PyPI package.
+
+# This file is presently for Internal FOG Use Only.
+# You should not add metrics here until probably about January of 2021.
+# If you're looking for the metrics.yaml for Geckoveiw Streaming Telemetry,
+# you can find that one in toolkit/components/telemetry/geckoview/streaming.
+
+---
+$schema: moz://mozilla.org/schemas/glean/metrics/2-0-0
+
+test:
+ boolean_metric:
+ type: boolean
+ expires: never
+ description: |
+ A multi-line
+ description
+ lifetime: application
+ notification_emails:
+ - glean-team@mozilla.com
+ bugs:
+ - https://bugzilla.mozilla.org/1635260/
+ data_reviews:
+ - https://example.com
+ telemetry_mirror: SOME_BOOL_SCALAR
+
+ labeled_boolean_metric:
+ type: labeled_boolean
+ expires: never
+ description: |
+ A multi-line
+ description
+ lifetime: application
+ notification_emails:
+ - glean-team@mozilla.com
+ bugs:
+ - https://bugzilla.mozilla.org/1635260/
+ data_reviews:
+ - https://example.com
+ telemetry_mirror: SOME_KEYED_BOOL_SCALAR
+
+ labeled_boolean_metric_labels:
+ type: labeled_boolean
+ expires: never
+ description: |
+ A multi-line
+ description
+ lifetime: application
+ notification_emails:
+ - glean-team@mozilla.com
+ bugs:
+ - https://bugzilla.mozilla.org/1635260/
+ data_reviews:
+ - https://example.com
+ labels:
+ - one_label
+ - two_labels
+ - three_labels
+ - four_labels
+ - five_labels
+ - six_labels
+ - seven_labels
+ - eight_labels
+ - nine_labels
+ - ten_labels
+ telemetry_mirror: SOME_OTHER_KEYED_BOOL_SCALAR
+
+ counter_metric:
+ type: counter
+ expires: never
+ description: |
+ A multi-line
+ description
+ lifetime: application
+ notification_emails:
+ - glean-team@mozilla.com
+ bugs:
+ - https://bugzilla.mozilla.org/1635260/
+ data_reviews:
+ - https://example.com
+ telemetry_mirror: SOME_UINT_SCALAR
+
+ labeled_counter_metric:
+ type: labeled_counter
+ expires: never
+ description: |
+ A multi-line
+ description
+ lifetime: application
+ notification_emails:
+ - glean-team@mozilla.com
+ bugs:
+ - https://bugzilla.mozilla.org/1635260/
+ data_reviews:
+ - https://example.com
+ telemetry_mirror: SOME_KEYED_UINT_SCALAR
+
+ labeled_counter_metric_labels:
+ type: labeled_counter
+ expires: never
+ description: |
+ A multi-line
+ description
+ lifetime: application
+ notification_emails:
+ - glean-team@mozilla.com
+ bugs:
+ - https://bugzilla.mozilla.org/1635260/
+ data_reviews:
+ - https://example.com
+ labels:
+ - one_label
+ - two_labels
+ telemetry_mirror: SOME_OTHER_KEYED_UINT_SCALAR
+
+ string_metric:
+ type: string
+ expires: never
+ description: |
+ A multi-line
+ description
+ lifetime: application
+ notification_emails:
+ - glean-team@mozilla.com
+ bugs:
+ - https://bugzilla.mozilla.org/1635260/
+ data_reviews:
+ - https://example.com
+ telemetry_mirror: SOME_STRING_SCALAR
+
+ labeled_string_metric:
+ type: labeled_string
+ expires: never
+ description: |
+ A multi-line
+ description
+ lifetime: application
+ notification_emails:
+ - glean-team@mozilla.com
+ bugs:
+ - https://bugzilla.mozilla.org/1635260/
+ data_reviews:
+ - https://example.com
+
+ labeled_string_metric_labels:
+ type: labeled_string
+ expires: never
+ description: |
+ A multi-line
+ description
+ lifetime: application
+ notification_emails:
+ - glean-team@mozilla.com
+ bugs:
+ - https://bugzilla.mozilla.org/1635260/
+ data_reviews:
+ - https://example.com
+ labels:
+ - one_label
+ - two_labels
+
+ string_list_metric:
+ type: string_list
+ expires: never
+ description: |
+ A multi-line
+ description
+ lifetime: application
+ notification_emails:
+ - glean-team@mozilla.com
+ bugs:
+ - https://bugzilla.mozilla.org/1635260/
+ data_reviews:
+ - https://example.com
+ telemetry_mirror: YET_ANOTHER_KEYED_BOOL_SCALAR
+
+ text_metric:
+ type: text
+ expires: never
+ description: |
+ A multi-line
+ description
+ lifetime: application
+ notification_emails:
+ - glean-team@mozilla.com
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1828528
+ 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
+ telemetry_mirror: SOME_OTHER_UINT_SCALAR
+
+ timing_distribution_metric:
+ type: timing_distribution
+ expires: never
+ description: |
+ A multi-line
+ description
+ lifetime: application
+ notification_emails:
+ - glean-team@mozilla.com
+ bugs:
+ - https://bugzilla.mozilla.org/1635260/
+ data_reviews:
+ - https://example.com
+ telemetry_mirror: SOME_TIME_HISTOGRAM_MS
+
+ memory_distribution_metric:
+ type: memory_distribution
+ expires: never
+ description: |
+ A multi-line
+ description
+ lifetime: application
+ notification_emails:
+ - glean-team@mozilla.com
+ bugs:
+ - https://bugzilla.mozilla.org/1635260/
+ data_reviews:
+ - https://example.com
+ memory_unit: kilobyte
+ telemetry_mirror: SOME_MEM_HISTOGRAM_KB
+
+ custom_distribution_metric:
+ type: custom_distribution
+ expires: never
+ description: |
+ A multi-line
+ description
+ lifetime: application
+ notification_emails:
+ - glean-team@mozilla.com
+ bugs:
+ - https://bugzilla.mozilla.org/1635260/
+ data_reviews:
+ - https://example.com
+ range_min: 0
+ range_max: 100
+ bucket_count: 100
+ histogram_type: linear
+ telemetry_mirror: SOME_LINEAR_HISTOGRAM
+
+test.nested:
+ uuid_metric:
+ type: uuid
+ expires: never
+ description: |
+ A multi-line
+ description
+ lifetime: application
+ notification_emails:
+ - glean-team@mozilla.com
+ bugs:
+ - https://bugzilla.mozilla.org/1635260/
+ data_reviews:
+ - https://example.com
+ telemetry_mirror: SOME_OTHER_STRING_SCALAR
+
+ datetime_metric:
+ type: datetime
+ expires: never
+ description: |
+ A multi-line
+ description
+ lifetime: application
+ notification_emails:
+ - glean-team@mozilla.com
+ bugs:
+ - https://bugzilla.mozilla.org/1635260/
+ data_reviews:
+ - https://example.com
+ telemetry_mirror: SOME_STILL_OTHER_STRING_SCALAR
+
+ event_metric:
+ type: event
+ expires: never
+ description: |
+ A multi-line
+ description
+ lifetime: ping
+ notification_emails:
+ - glean-team@mozilla.com
+ bugs:
+ - https://bugzilla.mozilla.org/1635260/
+ data_reviews:
+ - https://example.com
+ telemetry_mirror: EventMetric_EnumNames_AreStrange
+
+ event_metric_with_extra:
+ type: event
+ expires: never
+ description: |
+ A multi-line
+ description
+ lifetime: ping
+ notification_emails:
+ - glean-team@mozilla.com
+ bugs:
+ - https://bugzilla.mozilla.org/1635260/
+ data_reviews:
+ - https://example.com
+ extra_keys:
+ an_extra_key:
+ type: string
+ description: An extra key description
+ another_extra_key:
+ type: string
+ description: Another extra key description
+ telemetry_mirror: EventMetric_EnumName_WithExtra
+
+ quantity_metric:
+ type: quantity
+ unit: someunit
+ expires: never
+ description: |
+ A multi-line
+ description
+ notification_emails:
+ - glean-team@mozilla.com
+ bugs:
+ - https://bugzilla.mozilla.org/1704846/
+ data_reviews:
+ - https://example.com
+ telemetry_mirror: TELEMETRY_TEST_MIRROR_FOR_QUANTITY
+
+ rate_metric:
+ type: rate
+ expires: never
+ description: |
+ A multi-line
+ description
+ notification_emails:
+ - glean-team@mozilla.com
+ bugs:
+ - https://bugzilla.mozilla.org/1704846/
+ data_reviews:
+ - https://example.com
+
+ rate_with_external_denominator:
+ type: rate
+ denominator_metric: test.nested.external_denominator
+ expires: never
+ description: |
+ A multi-line
+ description
+ notification_emails:
+ - glean-team@mozilla.com
+ bugs:
+ - https://bugzilla.mozilla.org/1704846/
+ data_reviews:
+ - https://example.com
+
+ external_denominator:
+ type: counter
+ expires: never
+ description: |
+ A multi-line
+ description
+ notification_emails:
+ - glean-team@mozilla.com
+ bugs:
+ - https://bugzilla.mozilla.org/1704846/
+ data_reviews:
+ - https://example.com
+
+ optimizable_counter_metric:
+ type: counter
+ 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
diff --git a/toolkit/components/glean/tests/pytest/metrics_test_output b/toolkit/components/glean/tests/pytest/metrics_test_output
new file mode 100644
index 0000000000..76808cc984
--- /dev/null
+++ b/toolkit/components/glean/tests/pytest/metrics_test_output
@@ -0,0 +1,918 @@
+// -*- 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 enum DynamicLabel { }
+
+pub mod test {
+ use crate::private::*;
+ #[allow(unused_imports)] // CommonMetricData might be unused, let's avoid warnings
+ use glean::CommonMetricData;
+ #[allow(unused_imports)] // HistogramType might be unusued, let's avoid warnings
+ use glean::HistogramType;
+ use once_cell::sync::Lazy;
+
+ #[allow(non_upper_case_globals)]
+ /// generated from test.boolean_metric
+ ///
+ /// A multi-line
+ /// description
+ pub static boolean_metric: Lazy<BooleanMetric> = Lazy::new(|| {
+ BooleanMetric::new(1.into(), CommonMetricData {
+ name: "boolean_metric".into(),
+ category: "test".into(),
+ send_in_pings: vec!["metrics".into()],
+ lifetime: Lifetime::Application,
+ disabled: false,
+ ..Default::default()
+ })
+ });
+
+ #[allow(non_upper_case_globals)]
+ /// generated from test.counter_metric
+ ///
+ /// A multi-line
+ /// description
+ pub static counter_metric: Lazy<CounterMetric> = Lazy::new(|| {
+ CounterMetric::new(2.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.custom_distribution_metric
+ ///
+ /// A multi-line
+ /// description
+ pub static custom_distribution_metric: Lazy<CustomDistributionMetric> = Lazy::new(|| {
+ CustomDistributionMetric::new(3.into(), CommonMetricData {
+ name: "custom_distribution_metric".into(),
+ category: "test".into(),
+ send_in_pings: vec!["metrics".into()],
+ lifetime: Lifetime::Application,
+ disabled: false,
+ ..Default::default()
+ }, 0, 100, 100, HistogramType::Linear)
+ });
+
+ #[allow(non_upper_case_globals)]
+ /// generated from test.labeled_boolean_metric
+ ///
+ /// A multi-line
+ /// description
+ pub static labeled_boolean_metric: Lazy<LabeledMetric<LabeledBooleanMetric, super::DynamicLabel>> = Lazy::new(|| {
+ LabeledMetric::new(4.into(), CommonMetricData {
+ name: "labeled_boolean_metric".into(),
+ category: "test".into(),
+ send_in_pings: vec!["metrics".into()],
+ lifetime: Lifetime::Application,
+ disabled: false,
+ ..Default::default()
+ }, None)
+ });
+
+ #[repr(u16)]
+ pub enum LabeledBooleanMetricLabelsLabel {
+ OneLabel = 0,
+ TwoLabels = 1,
+ ThreeLabels = 2,
+ FourLabels = 3,
+ FiveLabels = 4,
+ SixLabels = 5,
+ SevenLabels = 6,
+ EightLabels = 7,
+ NineLabels = 8,
+ TenLabels = 9,
+ __Other__,
+ }
+ impl From<u16> for LabeledBooleanMetricLabelsLabel {
+ fn from(v: u16) -> Self {
+ match v {
+ 0 => Self::OneLabel,
+ 1 => Self::TwoLabels,
+ 2 => Self::ThreeLabels,
+ 3 => Self::FourLabels,
+ 4 => Self::FiveLabels,
+ 5 => Self::SixLabels,
+ 6 => Self::SevenLabels,
+ 7 => Self::EightLabels,
+ 8 => Self::NineLabels,
+ 9 => Self::TenLabels,
+ _ => Self::__Other__,
+ }
+ }
+ }
+ impl Into<&'static str> for LabeledBooleanMetricLabelsLabel {
+ fn into(self) -> &'static str {
+ match self {
+ Self::OneLabel => "one_label",
+ Self::TwoLabels => "two_labels",
+ Self::ThreeLabels => "three_labels",
+ Self::FourLabels => "four_labels",
+ Self::FiveLabels => "five_labels",
+ Self::SixLabels => "six_labels",
+ Self::SevenLabels => "seven_labels",
+ Self::EightLabels => "eight_labels",
+ Self::NineLabels => "nine_labels",
+ Self::TenLabels => "ten_labels",
+ Self::__Other__ => "__other__",
+ }
+ }
+ }
+ #[allow(non_upper_case_globals)]
+ /// generated from test.labeled_boolean_metric_labels
+ ///
+ /// A multi-line
+ /// description
+ pub static labeled_boolean_metric_labels: Lazy<LabeledMetric<LabeledBooleanMetric, LabeledBooleanMetricLabelsLabel>> = Lazy::new(|| {
+ LabeledMetric::new(5.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![::std::borrow::Cow::from("eight_labels"), ::std::borrow::Cow::from("five_labels"), ::std::borrow::Cow::from("four_labels"), ::std::borrow::Cow::from("nine_labels"), ::std::borrow::Cow::from("one_label"), ::std::borrow::Cow::from("seven_labels"), ::std::borrow::Cow::from("six_labels"), ::std::borrow::Cow::from("ten_labels"), ::std::borrow::Cow::from("three_labels"), ::std::borrow::Cow::from("two_labels")]))
+ });
+
+ #[allow(non_upper_case_globals)]
+ /// generated from test.labeled_counter_metric
+ ///
+ /// A multi-line
+ /// description
+ pub static labeled_counter_metric: Lazy<LabeledMetric<LabeledCounterMetric, super::DynamicLabel>> = Lazy::new(|| {
+ LabeledMetric::new(6.into(), CommonMetricData {
+ name: "labeled_counter_metric".into(),
+ category: "test".into(),
+ send_in_pings: vec!["metrics".into()],
+ lifetime: Lifetime::Application,
+ disabled: false,
+ ..Default::default()
+ }, None)
+ });
+
+ #[repr(u16)]
+ pub enum LabeledCounterMetricLabelsLabel {
+ OneLabel = 0,
+ TwoLabels = 1,
+ __Other__,
+ }
+ impl From<u16> for LabeledCounterMetricLabelsLabel {
+ fn from(v: u16) -> Self {
+ match v {
+ 0 => Self::OneLabel,
+ 1 => Self::TwoLabels,
+ _ => Self::__Other__,
+ }
+ }
+ }
+ impl Into<&'static str> for LabeledCounterMetricLabelsLabel {
+ fn into(self) -> &'static str {
+ match self {
+ Self::OneLabel => "one_label",
+ Self::TwoLabels => "two_labels",
+ Self::__Other__ => "__other__",
+ }
+ }
+ }
+ #[allow(non_upper_case_globals)]
+ /// generated from test.labeled_counter_metric_labels
+ ///
+ /// A multi-line
+ /// description
+ pub static labeled_counter_metric_labels: Lazy<LabeledMetric<LabeledCounterMetric, LabeledCounterMetricLabelsLabel>> = Lazy::new(|| {
+ LabeledMetric::new(7.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![::std::borrow::Cow::from("one_label"), ::std::borrow::Cow::from("two_labels")]))
+ });
+
+ #[allow(non_upper_case_globals)]
+ /// generated from test.labeled_string_metric
+ ///
+ /// A multi-line
+ /// description
+ pub static labeled_string_metric: Lazy<LabeledMetric<LabeledStringMetric, super::DynamicLabel>> = 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)
+ });
+
+ #[repr(u16)]
+ pub enum LabeledStringMetricLabelsLabel {
+ OneLabel = 0,
+ TwoLabels = 1,
+ __Other__,
+ }
+ impl From<u16> for LabeledStringMetricLabelsLabel {
+ fn from(v: u16) -> Self {
+ match v {
+ 0 => Self::OneLabel,
+ 1 => Self::TwoLabels,
+ _ => Self::__Other__,
+ }
+ }
+ }
+ impl Into<&'static str> for LabeledStringMetricLabelsLabel {
+ fn into(self) -> &'static str {
+ match self {
+ Self::OneLabel => "one_label",
+ Self::TwoLabels => "two_labels",
+ Self::__Other__ => "__other__",
+ }
+ }
+ }
+ #[allow(non_upper_case_globals)]
+ /// generated from test.labeled_string_metric_labels
+ ///
+ /// A multi-line
+ /// description
+ pub static labeled_string_metric_labels: Lazy<LabeledMetric<LabeledStringMetric, LabeledStringMetricLabelsLabel>> = 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![::std::borrow::Cow::from("one_label"), ::std::borrow::Cow::from("two_labels")]))
+ });
+
+ #[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(10.into(), CommonMetricData {
+ name: "memory_distribution_metric".into(),
+ category: "test".into(),
+ send_in_pings: vec!["metrics".into()],
+ lifetime: Lifetime::Application,
+ disabled: false,
+ ..Default::default()
+ }, MemoryUnit::Kilobyte)
+ });
+
+ #[allow(non_upper_case_globals)]
+ /// generated from test.string_list_metric
+ ///
+ /// A multi-line
+ /// description
+ pub static string_list_metric: Lazy<StringListMetric> = Lazy::new(|| {
+ StringListMetric::new(11.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.string_metric
+ ///
+ /// A multi-line
+ /// description
+ pub static string_metric: Lazy<StringMetric> = Lazy::new(|| {
+ StringMetric::new(12.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.text_metric
+ ///
+ /// A multi-line
+ /// description
+ pub static text_metric: Lazy<TextMetric> = Lazy::new(|| {
+ TextMetric::new(13.into(), CommonMetricData {
+ name: "text_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(14.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(15.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)
+ });
+
+}
+pub mod test_nested {
+ use crate::private::*;
+ #[allow(unused_imports)] // CommonMetricData might be unused, let's avoid warnings
+ use glean::CommonMetricData;
+ #[allow(unused_imports)] // HistogramType might be unusued, let's avoid warnings
+ use glean::HistogramType;
+ use once_cell::sync::Lazy;
+
+ #[allow(non_upper_case_globals)]
+ /// generated from test.nested.datetime_metric
+ ///
+ /// A multi-line
+ /// description
+ pub static datetime_metric: Lazy<DatetimeMetric> = Lazy::new(|| {
+ DatetimeMetric::new(16.into(), CommonMetricData {
+ name: "datetime_metric".into(),
+ category: "test.nested".into(),
+ send_in_pings: vec!["metrics".into()],
+ lifetime: Lifetime::Application,
+ disabled: false,
+ ..Default::default()
+ }, TimeUnit::Millisecond)
+ });
+
+ #[allow(non_upper_case_globals)]
+ /// generated from test.nested.event_metric
+ ///
+ /// A multi-line
+ /// description
+ pub static event_metric: Lazy<EventMetric<NoExtraKeys>> = Lazy::new(|| {
+ EventMetric::new(17.into(), CommonMetricData {
+ name: "event_metric".into(),
+ category: "test.nested".into(),
+ send_in_pings: vec!["events".into()],
+ lifetime: Lifetime::Ping,
+ disabled: false,
+ ..Default::default()
+ })
+ });
+
+ #[derive(Default, Debug, Clone, Hash, Eq, PartialEq)]
+ pub struct EventMetricWithExtraExtra {
+ pub an_extra_key: Option<String>,
+ pub another_extra_key: Option<String>,
+ }
+
+ impl ExtraKeys for EventMetricWithExtraExtra {
+ const ALLOWED_KEYS: &'static [&'static str] = &["an_extra_key", "another_extra_key"];
+
+ fn into_ffi_extra(self) -> ::std::collections::HashMap<String, String> {
+ let mut map = ::std::collections::HashMap::new();
+ self.an_extra_key.and_then(|val| map.insert("an_extra_key".into(), val.to_string()));
+ self.another_extra_key.and_then(|val| map.insert("another_extra_key".into(), val.to_string()));
+ map
+ }
+ }
+ #[allow(non_upper_case_globals)]
+ /// generated from test.nested.event_metric_with_extra
+ ///
+ /// A multi-line
+ /// description
+ pub static event_metric_with_extra: Lazy<EventMetric<EventMetricWithExtraExtra>> = Lazy::new(|| {
+ EventMetric::new(18.into(), CommonMetricData {
+ name: "event_metric_with_extra".into(),
+ category: "test.nested".into(),
+ send_in_pings: vec!["events".into()],
+ lifetime: Lifetime::Ping,
+ disabled: false,
+ ..Default::default()
+ })
+ });
+
+ #[allow(non_upper_case_globals)]
+ /// generated from test.nested.external_denominator
+ ///
+ /// A multi-line
+ /// description
+ pub static external_denominator: Lazy<DenominatorMetric> = Lazy::new(|| {
+ DenominatorMetric::new(19.into(), CommonMetricData {
+ name: "external_denominator".into(),
+ category: "test.nested".into(),
+ send_in_pings: vec!["metrics".into()],
+ lifetime: Lifetime::Ping,
+ disabled: false,
+ ..Default::default()
+ }, vec![CommonMetricData {name: "rate_with_external_denominator".into(), category: "test.nested".into(), send_in_pings: vec!["metrics".into()], lifetime: Lifetime::Ping, disabled: false, ..Default::default()}])
+ });
+
+ #[allow(non_upper_case_globals)]
+ /// generated from test.nested.optimizable_counter_metric
+ ///
+ /// A multi-line
+ /// description
+ pub static optimizable_counter_metric: Lazy<CounterMetric> = Lazy::new(|| {
+ CounterMetric::codegen_new(
+ 20,
+ "test.nested",
+ "optimizable_counter_metric",
+ "metrics"
+ )
+ });
+
+ #[allow(non_upper_case_globals)]
+ /// generated from test.nested.quantity_metric
+ ///
+ /// A multi-line
+ /// description
+ pub static quantity_metric: Lazy<QuantityMetric> = Lazy::new(|| {
+ QuantityMetric::new(21.into(), CommonMetricData {
+ name: "quantity_metric".into(),
+ category: "test.nested".into(),
+ send_in_pings: vec!["metrics".into()],
+ lifetime: Lifetime::Ping,
+ disabled: false,
+ ..Default::default()
+ })
+ });
+
+ #[allow(non_upper_case_globals)]
+ /// generated from test.nested.rate_metric
+ ///
+ /// A multi-line
+ /// description
+ pub static rate_metric: Lazy<RateMetric> = Lazy::new(|| {
+ RateMetric::new(22.into(), CommonMetricData {
+ name: "rate_metric".into(),
+ category: "test.nested".into(),
+ send_in_pings: vec!["metrics".into()],
+ lifetime: Lifetime::Ping,
+ disabled: false,
+ ..Default::default()
+ })
+ });
+
+ #[allow(non_upper_case_globals)]
+ /// generated from test.nested.rate_with_external_denominator
+ ///
+ /// A multi-line
+ /// description
+ pub static rate_with_external_denominator: Lazy<NumeratorMetric> = Lazy::new(|| {
+ NumeratorMetric::new(23.into(), CommonMetricData {
+ name: "rate_with_external_denominator".into(),
+ category: "test.nested".into(),
+ send_in_pings: vec!["metrics".into()],
+ lifetime: Lifetime::Ping,
+ disabled: false,
+ ..Default::default()
+ })
+ });
+
+ #[allow(non_upper_case_globals)]
+ /// generated from test.nested.uuid_metric
+ ///
+ /// A multi-line
+ /// description
+ pub static uuid_metric: Lazy<UuidMetric> = Lazy::new(|| {
+ UuidMetric::new(24.into(), CommonMetricData {
+ name: "uuid_metric".into(),
+ category: "test.nested".into(),
+ send_in_pings: vec!["metrics".into()],
+ lifetime: Lifetime::Application,
+ disabled: false,
+ ..Default::default()
+ })
+ });
+
+}
+
+#[allow(dead_code)]
+pub(crate) mod __glean_metric_maps {
+ use std::collections::HashMap;
+
+ use crate::metrics::extra_keys_len;
+ use crate::private::*;
+ use once_cell::sync::Lazy;
+
+ pub static BOOLEAN_MAP: Lazy<HashMap<MetricId, &Lazy<BooleanMetric>>> = Lazy::new(|| {
+ let mut map = HashMap::with_capacity(1);
+ map.insert(1.into(), &super::test::boolean_metric);
+ map
+ });
+
+ pub static COUNTER_MAP: Lazy<HashMap<MetricId, &Lazy<CounterMetric>>> = Lazy::new(|| {
+ let mut map = HashMap::with_capacity(2);
+ map.insert(2.into(), &super::test::counter_metric);
+ map.insert(20.into(), &super::test_nested::optimizable_counter_metric);
+ map
+ });
+
+ pub static CUSTOM_DISTRIBUTION_MAP: Lazy<HashMap<MetricId, &Lazy<CustomDistributionMetric>>> = Lazy::new(|| {
+ let mut map = HashMap::with_capacity(1);
+ map.insert(3.into(), &super::test::custom_distribution_metric);
+ map
+ });
+
+ pub static MEMORY_DISTRIBUTION_MAP: Lazy<HashMap<MetricId, &Lazy<MemoryDistributionMetric>>> = Lazy::new(|| {
+ let mut map = HashMap::with_capacity(1);
+ map.insert(10.into(), &super::test::memory_distribution_metric);
+ map
+ });
+
+ pub static STRING_LIST_MAP: Lazy<HashMap<MetricId, &Lazy<StringListMetric>>> = Lazy::new(|| {
+ let mut map = HashMap::with_capacity(1);
+ map.insert(11.into(), &super::test::string_list_metric);
+ map
+ });
+
+ pub static STRING_MAP: Lazy<HashMap<MetricId, &Lazy<StringMetric>>> = Lazy::new(|| {
+ let mut map = HashMap::with_capacity(1);
+ map.insert(12.into(), &super::test::string_metric);
+ map
+ });
+
+ pub static TEXT_MAP: Lazy<HashMap<MetricId, &Lazy<TextMetric>>> = Lazy::new(|| {
+ let mut map = HashMap::with_capacity(1);
+ map.insert(13.into(), &super::test::text_metric);
+ map
+ });
+
+ pub static TIMESPAN_MAP: Lazy<HashMap<MetricId, &Lazy<TimespanMetric>>> = Lazy::new(|| {
+ let mut map = HashMap::with_capacity(1);
+ map.insert(14.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(15.into(), &super::test::timing_distribution_metric);
+ map
+ });
+
+ pub static DATETIME_MAP: Lazy<HashMap<MetricId, &Lazy<DatetimeMetric>>> = Lazy::new(|| {
+ let mut map = HashMap::with_capacity(1);
+ map.insert(16.into(), &super::test_nested::datetime_metric);
+ map
+ });
+
+ pub static DENOMINATOR_MAP: Lazy<HashMap<MetricId, &Lazy<DenominatorMetric>>> = Lazy::new(|| {
+ let mut map = HashMap::with_capacity(1);
+ map.insert(19.into(), &super::test_nested::external_denominator);
+ map
+ });
+
+ pub static QUANTITY_MAP: Lazy<HashMap<MetricId, &Lazy<QuantityMetric>>> = Lazy::new(|| {
+ let mut map = HashMap::with_capacity(1);
+ map.insert(21.into(), &super::test_nested::quantity_metric);
+ map
+ });
+
+ pub static RATE_MAP: Lazy<HashMap<MetricId, &Lazy<RateMetric>>> = Lazy::new(|| {
+ let mut map = HashMap::with_capacity(1);
+ map.insert(22.into(), &super::test_nested::rate_metric);
+ map
+ });
+
+ pub static NUMERATOR_MAP: Lazy<HashMap<MetricId, &Lazy<NumeratorMetric>>> = Lazy::new(|| {
+ let mut map = HashMap::with_capacity(1);
+ map.insert(23.into(), &super::test_nested::rate_with_external_denominator);
+ map
+ });
+
+ pub static UUID_MAP: Lazy<HashMap<MetricId, &Lazy<UuidMetric>>> = Lazy::new(|| {
+ let mut map = HashMap::with_capacity(1);
+ map.insert(24.into(), &super::test_nested::uuid_metric);
+ map
+ });
+
+
+ /// Wrapper to record an event based on its metric ID.
+ ///
+ /// # Arguments
+ ///
+ /// * `metric_id` - The metric's ID to look up
+ /// * `extra` - An map of (extra key id, string) pairs.
+ /// The map will be decoded into the appropriate `ExtraKeys` type.
+ /// # Returns
+ ///
+ /// Returns `Ok(())` if the event was found and `record` was called with the given `extra`,
+ /// or an `EventRecordingError::InvalidId` if no event by that ID exists
+ /// or an `EventRecordingError::InvalidExtraKey` if the `extra` map could not be deserialized.
+ pub(crate) fn record_event_by_id(metric_id: u32, extra: HashMap<String, String>) -> Result<(), EventRecordingError> {
+ match metric_id {
+ 17 => {
+ assert!(
+ extra_keys_len(&super::test_nested::event_metric) != 0 || extra.is_empty(),
+ "No extra keys allowed, but some were passed"
+ );
+
+ super::test_nested::event_metric.record_raw(extra);
+ Ok(())
+ }
+ 18 => {
+ assert!(
+ extra_keys_len(&super::test_nested::event_metric_with_extra) != 0 || extra.is_empty(),
+ "No extra keys allowed, but some were passed"
+ );
+
+ super::test_nested::event_metric_with_extra.record_raw(extra);
+ Ok(())
+ }
+ _ => Err(EventRecordingError::InvalidId),
+ }
+ }
+
+ /// Wrapper to record an event based on its metric ID, with a provided timestamp.
+ ///
+ /// # Arguments
+ ///
+ /// * `metric_id` - The metric's ID to look up
+ /// * `timestamp` - The time at which this event was recorded.
+ /// * `extra` - An map of (extra key id, string) pairs.
+ /// The map will be decoded into the appropriate `ExtraKeys` type.
+ /// # Returns
+ ///
+ /// Returns `Ok(())` if the event was found and `record` was called with the given `extra`,
+ /// or an `EventRecordingError::InvalidId` if no event by that ID exists
+ /// or an `EventRecordingError::InvalidExtraKey` if the event doesn't take extra pairs,
+ /// but some are passed in.
+ pub(crate) fn record_event_by_id_with_time(metric_id: MetricId, timestamp: u64, extra: HashMap<String, String>) -> Result<(), EventRecordingError> {
+ match metric_id {
+ MetricId(17) => {
+ if extra_keys_len(&super::test_nested::event_metric) == 0 && !extra.is_empty() {
+ return Err(EventRecordingError::InvalidExtraKey);
+ }
+
+ super::test_nested::event_metric.record_with_time(timestamp, extra);
+ Ok(())
+ }
+ MetricId(18) => {
+ if extra_keys_len(&super::test_nested::event_metric_with_extra) == 0 && !extra.is_empty() {
+ return Err(EventRecordingError::InvalidExtraKey);
+ }
+
+ super::test_nested::event_metric_with_extra.record_with_time(timestamp, extra);
+ Ok(())
+ }
+ _ => Err(EventRecordingError::InvalidId),
+ }
+ }
+
+ /// Wrapper to get the currently stored events for event metric.
+ ///
+ /// # Arguments
+ ///
+ /// * `metric_id` - The metric's ID to look up
+ /// * `ping_name` - (Optional) The ping name to look into.
+ /// Defaults to the first value in `send_in_pings`.
+ ///
+ /// # Returns
+ ///
+ /// Returns the recorded events or `None` if nothing stored.
+ ///
+ /// # Panics
+ ///
+ /// Panics if no event by the given metric ID could be found.
+ pub(crate) fn event_test_get_value_wrapper(metric_id: u32, ping_name: Option<String>) -> Option<Vec<RecordedEvent>> {
+ match metric_id {
+ 17 => super::test_nested::event_metric.test_get_value(ping_name.as_deref()),
+ 18 => super::test_nested::event_metric_with_extra.test_get_value(ping_name.as_deref()),
+ _ => panic!("No event for metric id {}", metric_id),
+ }
+ }
+
+ /// Check the provided event for errors.
+ ///
+ /// # Arguments
+ ///
+ /// * `metric_id` - The metric's ID to look up
+ /// * `ping_name` - (Optional) The ping name to look into.
+ /// Defaults to the first value in `send_in_pings`.
+ ///
+ /// # Returns
+ ///
+ /// Returns a string for the recorded error or `None`.
+ ///
+ /// # Panics
+ ///
+ /// Panics if no event by the given metric ID could be found.
+ #[allow(unused_variables)]
+ pub(crate) fn event_test_get_error(metric_id: u32) -> Option<String> {
+ #[cfg(feature = "with_gecko")]
+ match metric_id {
+ 17 => test_get_errors!(super::test_nested::event_metric),
+ 18 => test_get_errors!(super::test_nested::event_metric_with_extra),
+ _ => panic!("No event for metric id {}", metric_id),
+ }
+
+ #[cfg(not(feature = "with_gecko"))]
+ {
+ return None;
+ }
+ }
+
+ /// Gets the submetric from the specified labeled_boolean metric.
+ ///
+ /// # Arguments
+ ///
+ /// * `metric_id` - The metric's ID to look up
+ /// * `label` - The label identifying the boolean submetric.
+ ///
+ /// # Returns
+ ///
+ /// Returns the boolean submetric.
+ ///
+ /// # Panics
+ ///
+ /// Panics if no labeled_boolean by the given metric ID could be found.
+ #[allow(unused_variables)]
+ pub(crate) fn labeled_boolean_get(metric_id: u32, label: &str) -> LabeledBooleanMetric {
+ match metric_id {
+ 4 => super::test::labeled_boolean_metric.get(label),
+ 5 => super::test::labeled_boolean_metric_labels.get(label),
+ _ => panic!("No labeled_boolean for metric id {}", metric_id),
+ }
+ }
+
+ /// Gets the submetric from the specified labeled_boolean metric, by enum.
+ ///
+ /// # Arguments
+ ///
+ /// * `metric_id` - The metric's ID to look up
+ /// * `label_enum` - The label enum identifying the boolean submetric.
+ ///
+ /// # Returns
+ ///
+ /// Returns the boolean submetric.
+ ///
+ /// # Panics
+ ///
+ /// Panics if no labeled_boolean by the given metric ID could be found.
+ #[allow(unused_variables)]
+ pub(crate) fn labeled_boolean_enum_get(metric_id: u32, label_enum: u16) -> LabeledBooleanMetric {
+ match metric_id {
+ 5 => super::test::labeled_boolean_metric_labels.get(labeled_enum_to_str(metric_id, label_enum)),
+ _ => panic!("No labeled_boolean for metric id {}", metric_id),
+ }
+ }
+ /// Gets the submetric from the specified labeled_counter metric.
+ ///
+ /// # Arguments
+ ///
+ /// * `metric_id` - The metric's ID to look up
+ /// * `label` - The label identifying the counter submetric.
+ ///
+ /// # Returns
+ ///
+ /// Returns the counter submetric.
+ ///
+ /// # Panics
+ ///
+ /// Panics if no labeled_counter by the given metric ID could be found.
+ #[allow(unused_variables)]
+ pub(crate) fn labeled_counter_get(metric_id: u32, label: &str) -> LabeledCounterMetric {
+ match metric_id {
+ 6 => super::test::labeled_counter_metric.get(label),
+ 7 => super::test::labeled_counter_metric_labels.get(label),
+ _ => panic!("No labeled_counter for metric id {}", metric_id),
+ }
+ }
+
+ /// Gets the submetric from the specified labeled_counter metric, by enum.
+ ///
+ /// # Arguments
+ ///
+ /// * `metric_id` - The metric's ID to look up
+ /// * `label_enum` - The label enum identifying the counter submetric.
+ ///
+ /// # Returns
+ ///
+ /// Returns the counter submetric.
+ ///
+ /// # Panics
+ ///
+ /// Panics if no labeled_counter by the given metric ID could be found.
+ #[allow(unused_variables)]
+ pub(crate) fn labeled_counter_enum_get(metric_id: u32, label_enum: u16) -> LabeledCounterMetric {
+ match metric_id {
+ 7 => super::test::labeled_counter_metric_labels.get(labeled_enum_to_str(metric_id, label_enum)),
+ _ => panic!("No labeled_counter for metric id {}", metric_id),
+ }
+ }
+ /// Gets the submetric from the specified labeled_string metric.
+ ///
+ /// # Arguments
+ ///
+ /// * `metric_id` - The metric's ID to look up
+ /// * `label` - The label identifying the string submetric.
+ ///
+ /// # Returns
+ ///
+ /// Returns the string submetric.
+ ///
+ /// # Panics
+ ///
+ /// Panics if no labeled_string by the given metric ID could be found.
+ #[allow(unused_variables)]
+ pub(crate) fn labeled_string_get(metric_id: u32, label: &str) -> LabeledStringMetric {
+ match metric_id {
+ 8 => super::test::labeled_string_metric.get(label),
+ 9 => super::test::labeled_string_metric_labels.get(label),
+ _ => panic!("No labeled_string for metric id {}", metric_id),
+ }
+ }
+
+ /// Gets the submetric from the specified labeled_string metric, by enum.
+ ///
+ /// # Arguments
+ ///
+ /// * `metric_id` - The metric's ID to look up
+ /// * `label_enum` - The label enum identifying the string submetric.
+ ///
+ /// # Returns
+ ///
+ /// Returns the string submetric.
+ ///
+ /// # Panics
+ ///
+ /// Panics if no labeled_string by the given metric ID could be found.
+ #[allow(unused_variables)]
+ pub(crate) fn labeled_string_enum_get(metric_id: u32, label_enum: u16) -> LabeledStringMetric {
+ match metric_id {
+ 9 => super::test::labeled_string_metric_labels.get(labeled_enum_to_str(metric_id, label_enum)),
+ _ => panic!("No labeled_string for metric id {}", metric_id),
+ }
+ }
+
+ pub(crate) fn labeled_enum_to_str(metric_id: u32, label: u16) -> &'static str {
+ match metric_id {
+ 5 => super::test::LabeledBooleanMetricLabelsLabel::from(label).into(),
+ 7 => super::test::LabeledCounterMetricLabelsLabel::from(label).into(),
+ 9 => super::test::LabeledStringMetricLabelsLabel::from(label).into(),
+ _ => panic!("Can't turn label enum to string for metric {} which isn't a labeled metric with static labels", metric_id),
+ }
+ }
+
+ pub(crate) mod submetric_maps {
+ use std::sync::{
+ atomic::AtomicU32,
+ RwLock,
+ };
+ use super::*;
+
+ pub(crate) const SUBMETRIC_BIT: u32 = 25;
+ pub(crate) static NEXT_LABELED_SUBMETRIC_ID: AtomicU32 = AtomicU32::new((1 << SUBMETRIC_BIT) + 1);
+ pub(crate) static LABELED_METRICS_TO_IDS: Lazy<RwLock<HashMap<(u32, String), u32>>> = Lazy::new(||
+ RwLock::new(HashMap::new())
+ );
+ pub(crate) static LABELED_ENUMS_TO_IDS: Lazy<RwLock<HashMap<(u32, u16), u32>>> = Lazy::new(||
+ RwLock::new(HashMap::new())
+ );
+
+ pub static BOOLEAN_MAP: Lazy<RwLock<HashMap<MetricId, LabeledBooleanMetric>>> = Lazy::new(||
+ RwLock::new(HashMap::new())
+ );
+ pub static COUNTER_MAP: Lazy<RwLock<HashMap<MetricId, LabeledCounterMetric>>> = Lazy::new(||
+ RwLock::new(HashMap::new())
+ );
+ pub static STRING_MAP: Lazy<RwLock<HashMap<MetricId, LabeledStringMetric>>> = Lazy::new(||
+ RwLock::new(HashMap::new())
+ );
+ }
+}
+
diff --git a/toolkit/components/glean/tests/pytest/metrics_test_output_cpp b/toolkit/components/glean/tests/pytest/metrics_test_output_cpp
new file mode 100644
index 0000000000..5b9c7bfae4
--- /dev/null
+++ b/toolkit/components/glean/tests/pytest/metrics_test_output_cpp
@@ -0,0 +1,287 @@
+// -*- mode: C++ -*-
+
+// AUTOGENERATED BY glean_parser. DO NOT EDIT.
+
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_Metrics_h
+#define mozilla_Metrics_h
+
+#include "mozilla/glean/bindings/MetricTypes.h"
+#include "mozilla/Maybe.h"
+#include "nsTArray.h"
+#include "nsPrintfCString.h"
+
+#include <tuple>
+
+namespace mozilla::glean {
+struct NoExtraKeys;
+enum class DynamicLabel: uint16_t { };
+
+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(2);
+
+ /**
+ * generated from test.custom_distribution_metric
+ */
+ /**
+ * A multi-line
+ * description
+ */
+ constexpr impl::CustomDistributionMetric custom_distribution_metric(3);
+
+ /**
+ * generated from test.labeled_boolean_metric
+ */
+ /**
+ * A multi-line
+ * description
+ */
+ constexpr impl::Labeled<impl::BooleanMetric, DynamicLabel> labeled_boolean_metric(4);
+
+ /**
+ * generated from test.labeled_boolean_metric_labels
+ */
+ enum class LabeledBooleanMetricLabelsLabel: uint16_t {
+ eOneLabel = 0,
+ eTwoLabels = 1,
+ eThreeLabels = 2,
+ eFourLabels = 3,
+ eFiveLabels = 4,
+ eSixLabels = 5,
+ eSevenLabels = 6,
+ eEightLabels = 7,
+ eNineLabels = 8,
+ eTenLabels = 9,
+ e__Other__,
+ };
+ /**
+ * A multi-line
+ * description
+ */
+ constexpr impl::Labeled<impl::BooleanMetric, LabeledBooleanMetricLabelsLabel> labeled_boolean_metric_labels(5);
+
+ /**
+ * generated from test.labeled_counter_metric
+ */
+ /**
+ * A multi-line
+ * description
+ */
+ constexpr impl::Labeled<impl::CounterMetric, DynamicLabel> labeled_counter_metric(6);
+
+ /**
+ * generated from test.labeled_counter_metric_labels
+ */
+ enum class LabeledCounterMetricLabelsLabel: uint16_t {
+ eOneLabel = 0,
+ eTwoLabels = 1,
+ e__Other__,
+ };
+ /**
+ * A multi-line
+ * description
+ */
+ constexpr impl::Labeled<impl::CounterMetric, LabeledCounterMetricLabelsLabel> labeled_counter_metric_labels(7);
+
+ /**
+ * generated from test.labeled_string_metric
+ */
+ /**
+ * A multi-line
+ * description
+ */
+ constexpr impl::Labeled<impl::StringMetric, DynamicLabel> labeled_string_metric(8);
+
+ /**
+ * generated from test.labeled_string_metric_labels
+ */
+ enum class LabeledStringMetricLabelsLabel: uint16_t {
+ eOneLabel = 0,
+ eTwoLabels = 1,
+ e__Other__,
+ };
+ /**
+ * A multi-line
+ * description
+ */
+ constexpr impl::Labeled<impl::StringMetric, LabeledStringMetricLabelsLabel> labeled_string_metric_labels(9);
+
+ /**
+ * generated from test.memory_distribution_metric
+ */
+ /**
+ * A multi-line
+ * description
+ */
+ constexpr impl::MemoryDistributionMetric memory_distribution_metric(10);
+
+ /**
+ * generated from test.string_list_metric
+ */
+ /**
+ * A multi-line
+ * description
+ */
+ constexpr impl::StringListMetric string_list_metric(11);
+
+ /**
+ * generated from test.string_metric
+ */
+ /**
+ * A multi-line
+ * description
+ */
+ constexpr impl::StringMetric string_metric(12);
+
+ /**
+ * generated from test.text_metric
+ */
+ /**
+ * A multi-line
+ * description
+ */
+ constexpr impl::TextMetric text_metric(13);
+
+ /**
+ * generated from test.timespan_metric
+ */
+ /**
+ * A multi-line
+ * description
+ */
+ constexpr impl::TimespanMetric timespan_metric(14);
+
+ /**
+ * generated from test.timing_distribution_metric
+ */
+ /**
+ * A multi-line
+ * description
+ */
+ constexpr impl::TimingDistributionMetric timing_distribution_metric(15);
+
+}
+namespace test_nested {
+ /**
+ * generated from test.nested.datetime_metric
+ */
+ /**
+ * A multi-line
+ * description
+ */
+ constexpr impl::DatetimeMetric datetime_metric(16);
+
+ /**
+ * generated from test.nested.event_metric
+ */
+ /**
+ * A multi-line
+ * description
+ */
+ constexpr impl::EventMetric<NoExtraKeys> event_metric(17);
+
+ /**
+ * generated from test.nested.event_metric_with_extra
+ */
+ struct EventMetricWithExtraExtra {
+ mozilla::Maybe<nsCString> anExtraKey;
+ mozilla::Maybe<nsCString> anotherExtraKey;
+
+ std::tuple<nsTArray<nsCString>, nsTArray<nsCString>> ToFfiExtra() const {
+ nsTArray<nsCString> extraKeys;
+ nsTArray<nsCString> extraValues;
+ if (anExtraKey) {
+ extraKeys.AppendElement()->AssignASCII("an_extra_key");
+ extraValues.EmplaceBack(anExtraKey.value());
+ }
+ if (anotherExtraKey) {
+ extraKeys.AppendElement()->AssignASCII("another_extra_key");
+ extraValues.EmplaceBack(anotherExtraKey.value());
+ }
+ return std::make_tuple(std::move(extraKeys), std::move(extraValues));
+ }
+ };
+ /**
+ * A multi-line
+ * description
+ */
+ constexpr impl::EventMetric<EventMetricWithExtraExtra> event_metric_with_extra(18);
+
+ /**
+ * generated from test.nested.external_denominator
+ */
+ /**
+ * A multi-line
+ * description
+ */
+ constexpr impl::DenominatorMetric external_denominator(19);
+
+ /**
+ * generated from test.nested.optimizable_counter_metric
+ */
+ /**
+ * A multi-line
+ * description
+ */
+ constexpr impl::CounterMetric optimizable_counter_metric(20);
+
+ /**
+ * generated from test.nested.quantity_metric
+ */
+ /**
+ * A multi-line
+ * description
+ */
+ constexpr impl::QuantityMetric quantity_metric(21);
+
+ /**
+ * generated from test.nested.rate_metric
+ */
+ /**
+ * A multi-line
+ * description
+ */
+ constexpr impl::RateMetric rate_metric(22);
+
+ /**
+ * generated from test.nested.rate_with_external_denominator
+ */
+ /**
+ * A multi-line
+ * description
+ */
+ constexpr impl::NumeratorMetric rate_with_external_denominator(23);
+
+ /**
+ * generated from test.nested.uuid_metric
+ */
+ /**
+ * A multi-line
+ * description
+ */
+ constexpr impl::UuidMetric uuid_metric(24);
+
+}
+
+} // namespace mozilla::glean
+
+#endif // mozilla_Metrics_h
diff --git a/toolkit/components/glean/tests/pytest/metrics_test_output_js_cpp b/toolkit/components/glean/tests/pytest/metrics_test_output_js_cpp
new file mode 100644
index 0000000000..ba4619bb57
--- /dev/null
+++ b/toolkit/components/glean/tests/pytest/metrics_test_output_js_cpp
@@ -0,0 +1,473 @@
+// -*- 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/. */
+
+#include "mozilla/glean/bindings/GleanJSMetricsLookup.h"
+
+#include "mozilla/PerfectHash.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/glean/bindings/MetricTypes.h"
+#include "mozilla/glean/fog_ffi_generated.h"
+#include "nsString.h"
+
+#define GLEAN_INDEX_BITS (32)
+#define GLEAN_TYPE_BITS (5)
+#define GLEAN_ID_BITS (27)
+#define GLEAN_TYPE_ID(id) ((id) >> GLEAN_ID_BITS)
+#define GLEAN_METRIC_ID(id) ((id) & ((1ULL << GLEAN_ID_BITS) - 1))
+#define GLEAN_OFFSET(entry) (entry & ((1ULL << GLEAN_INDEX_BITS) - 1))
+
+namespace mozilla::glean {
+
+// The category lookup table's entry type
+using category_entry_t = uint32_t;
+// The metric lookup table's entry type
+// This is a bitpacked type with 32 bits available to index into
+// the string table, 5 bits available to signify the metric type,
+// and the remaining 27 bits devoted to 2 "signal"
+// bits to signify important characteristics (metric's a labeled metric's
+// submetric, metric's been registered at runtime) and 25 bits
+// for built-in metric ids.
+// Gives room for 33554432 of each combination of
+// characteristics (which hopefully will prove to be enough).
+using metric_entry_t = uint64_t;
+
+static_assert(GLEAN_INDEX_BITS + GLEAN_TYPE_BITS + GLEAN_ID_BITS == sizeof(metric_entry_t) * 8, "Index, Type, and ID bits need to fit into a metric_entry_t");
+static_assert(GLEAN_TYPE_BITS + GLEAN_ID_BITS <= sizeof(uint32_t) * 8, "Metric Types and IDs need to fit into at most 32 bits");
+static_assert(2 < UINT32_MAX, "Too many metric categories generated.");
+static_assert(24 < 33554432, "Too many metrics generated. Need room for 2 signal bits.");
+static_assert(19 < 32, "Too many different metric types.");
+
+already_AddRefed<GleanMetric> NewMetricFromId(uint32_t id, nsISupports* aParent) {
+ uint32_t typeId = GLEAN_TYPE_ID(id);
+ uint32_t metricId = GLEAN_METRIC_ID(id);
+
+ switch (typeId) {
+ case 1: /* boolean */
+ {
+ return MakeAndAddRef<GleanBoolean>(metricId, aParent);
+ }
+ case 2: /* counter */
+ {
+ return MakeAndAddRef<GleanCounter>(metricId, aParent);
+ }
+ case 3: /* custom_distribution */
+ {
+ return MakeAndAddRef<GleanCustomDistribution>(metricId, aParent);
+ }
+ case 4: /* labeled_boolean */
+ {
+ return MakeAndAddRef<GleanLabeled>(metricId, 4, aParent);
+ }
+ case 5: /* labeled_counter */
+ {
+ return MakeAndAddRef<GleanLabeled>(metricId, 5, aParent);
+ }
+ case 6: /* labeled_string */
+ {
+ return MakeAndAddRef<GleanLabeled>(metricId, 6, aParent);
+ }
+ case 7: /* memory_distribution */
+ {
+ return MakeAndAddRef<GleanMemoryDistribution>(metricId, aParent);
+ }
+ case 8: /* string_list */
+ {
+ return MakeAndAddRef<GleanStringList>(metricId, aParent);
+ }
+ case 9: /* string */
+ {
+ return MakeAndAddRef<GleanString>(metricId, aParent);
+ }
+ case 10: /* text */
+ {
+ return MakeAndAddRef<GleanText>(metricId, aParent);
+ }
+ case 11: /* timespan */
+ {
+ return MakeAndAddRef<GleanTimespan>(metricId, aParent);
+ }
+ case 12: /* timing_distribution */
+ {
+ return MakeAndAddRef<GleanTimingDistribution>(metricId, aParent);
+ }
+ case 13: /* datetime */
+ {
+ return MakeAndAddRef<GleanDatetime>(metricId, aParent);
+ }
+ case 14: /* event */
+ {
+ return MakeAndAddRef<GleanEvent>(metricId, aParent);
+ }
+ case 15: /* denominator */
+ {
+ return MakeAndAddRef<GleanDenominator>(metricId, aParent);
+ }
+ case 16: /* quantity */
+ {
+ return MakeAndAddRef<GleanQuantity>(metricId, aParent);
+ }
+ case 17: /* rate */
+ {
+ return MakeAndAddRef<GleanRate>(metricId, aParent);
+ }
+ case 18: /* numerator */
+ {
+ return MakeAndAddRef<GleanNumerator>(metricId, aParent);
+ }
+ case 19: /* uuid */
+ {
+ return MakeAndAddRef<GleanUuid>(metricId, aParent);
+ }
+ default:
+ MOZ_ASSERT_UNREACHABLE("Invalid type ID reached when trying to instantiate a new metric");
+ return nullptr;
+ }
+}
+
+/**
+ * Create a submetric instance for a labeled metric of the provided type and id for the given label.
+ * Assigns or retrieves an id for the submetric from the SDK.
+ *
+ * @param aParentTypeId - The type of the parent labeled metric identified as a number generated during codegen.
+ * Only used to identify which X of LabeledX you are so that X can be created here.
+ * @param aParentMetricId - The metric id for the parent labeled metric.
+ * @param aLabel - The label for the submetric. Might not adhere to the SDK label format.
+ * @param aSubmetricId - an outparam which is assigned the submetric's SDK-generated submetric id.
+ * Used only by GIFFT.
+ */
+already_AddRefed<GleanMetric> NewSubMetricFromIds(uint32_t aParentTypeId,
+ uint32_t aParentMetricId,
+ const nsACString& aLabel,
+ uint32_t* aSubmetricId,
+ nsISupports* aParent) {
+ switch (aParentTypeId) {
+ case 4: { /* labeled_boolean */
+ auto id = impl::fog_labeled_boolean_get(aParentMetricId, &aLabel);
+ *aSubmetricId = id;
+ return MakeAndAddRef<GleanBoolean>(id, aParent);
+ }
+ case 5: { /* labeled_counter */
+ auto id = impl::fog_labeled_counter_get(aParentMetricId, &aLabel);
+ *aSubmetricId = id;
+ return MakeAndAddRef<GleanCounter>(id, aParent);
+ }
+ case 6: { /* labeled_string */
+ auto id = impl::fog_labeled_string_get(aParentMetricId, &aLabel);
+ *aSubmetricId = id;
+ return MakeAndAddRef<GleanString>(id, aParent);
+ }
+ default: {
+ MOZ_ASSERT_UNREACHABLE("Invalid type ID for submetric.");
+ return nullptr;
+ }
+ }
+}
+
+static Maybe<uint32_t> category_result_check(const nsACString& aKey, category_entry_t entry);
+static Maybe<uint32_t> metric_result_check(const nsACString& aKey, metric_entry_t entry);
+
+#if defined(_MSC_VER) && !defined(__clang__)
+const char gCategoryStringTable[] = {
+#else
+constexpr char gCategoryStringTable[] = {
+#endif
+ /* 0 - "test" */ 't', 'e', 's', 't', '\0',
+ /* 5 - "testNested" */ 't', 'e', 's', 't', 'N', 'e', 's', 't', 'e', 'd', '\0',
+};
+
+
+static_assert(sizeof(gCategoryStringTable) < UINT32_MAX, "Category string table is too large.");
+
+const category_entry_t sCategoryByNameLookupEntries[] = {
+ 5ul,
+ 0ul
+};
+
+
+
+Maybe<uint32_t>
+CategoryByNameLookup(const nsACString& aKey)
+{
+ static const uint8_t BASES[] = {
+ 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, 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, 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, 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, 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, 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, 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, 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,
+ 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, 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, 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, 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, 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, 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, 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, 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,
+ 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, 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, 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,
+ 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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.counterMetric" */ 't', 'e', 's', 't', '.', 'c', 'o', 'u', 'n', 't', 'e', 'r', 'M', 'e', 't', 'r', 'i', 'c', '\0',
+ /* 38 - "test.customDistributionMetric" */ 't', 'e', 's', 't', '.', 'c', 'u', 's', 't', 'o', 'm', 'D', 'i', 's', 't', 'r', 'i', 'b', 'u', 't', 'i', 'o', 'n', 'M', 'e', 't', 'r', 'i', 'c', '\0',
+ /* 68 - "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',
+ /* 94 - "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',
+ /* 126 - "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',
+ /* 152 - "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',
+ /* 184 - "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',
+ /* 209 - "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',
+ /* 240 - "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',
+ /* 270 - "test.stringListMetric" */ 't', 'e', 's', 't', '.', 's', 't', 'r', 'i', 'n', 'g', 'L', 'i', 's', 't', 'M', 'e', 't', 'r', 'i', 'c', '\0',
+ /* 292 - "test.stringMetric" */ 't', 'e', 's', 't', '.', 's', 't', 'r', 'i', 'n', 'g', 'M', 'e', 't', 'r', 'i', 'c', '\0',
+ /* 310 - "test.textMetric" */ 't', 'e', 's', 't', '.', 't', 'e', 'x', 't', 'M', 'e', 't', 'r', 'i', 'c', '\0',
+ /* 326 - "test.timespanMetric" */ 't', 'e', 's', 't', '.', 't', 'i', 'm', 'e', 's', 'p', 'a', 'n', 'M', 'e', 't', 'r', 'i', 'c', '\0',
+ /* 346 - "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',
+ /* 376 - "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',
+ /* 402 - "testNested.eventMetric" */ 't', 'e', 's', 't', 'N', 'e', 's', 't', 'e', 'd', '.', 'e', 'v', 'e', 'n', 't', 'M', 'e', 't', 'r', 'i', 'c', '\0',
+ /* 425 - "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',
+ /* 457 - "testNested.externalDenominator" */ 't', 'e', 's', 't', 'N', 'e', 's', 't', 'e', 'd', '.', 'e', 'x', 't', 'e', 'r', 'n', 'a', 'l', 'D', 'e', 'n', 'o', 'm', 'i', 'n', 'a', 't', 'o', 'r', '\0',
+ /* 488 - "testNested.optimizableCounterMetric" */ 't', 'e', 's', 't', 'N', 'e', 's', 't', 'e', 'd', '.', 'o', 'p', 't', 'i', 'm', 'i', 'z', 'a', 'b', 'l', 'e', 'C', 'o', 'u', 'n', 't', 'e', 'r', 'M', 'e', 't', 'r', 'i', 'c', '\0',
+ /* 524 - "testNested.quantityMetric" */ 't', 'e', 's', 't', 'N', 'e', 's', 't', 'e', 'd', '.', 'q', 'u', 'a', 'n', 't', 'i', 't', 'y', 'M', 'e', 't', 'r', 'i', 'c', '\0',
+ /* 550 - "testNested.rateMetric" */ 't', 'e', 's', 't', 'N', 'e', 's', 't', 'e', 'd', '.', 'r', 'a', 't', 'e', 'M', 'e', 't', 'r', 'i', 'c', '\0',
+ /* 572 - "testNested.rateWithExternalDenominator" */ 't', 'e', 's', 't', 'N', 'e', 's', 't', 'e', 'd', '.', 'r', 'a', 't', 'e', 'W', 'i', 't', 'h', 'E', 'x', 't', 'e', 'r', 'n', 'a', 'l', 'D', 'e', 'n', 'o', 'm', 'i', 'n', 'a', 't', 'o', 'r', '\0',
+ /* 611 - "testNested.uuidMetric" */ 't', 'e', 's', 't', 'N', 'e', 's', 't', 'e', 'd', '.', 'u', 'u', 'i', 'd', 'M', 'e', 't', 'r', 'i', 'c', '\0',
+};
+
+
+static_assert(sizeof(gMetricStringTable) < 4294967296, "Metric string table is too large.");
+
+const metric_entry_t sMetricByNameLookupEntries[] = {
+ 6341068335467200838ull,
+ 3458764548180279480ull,
+ 1152921590506193384ull,
+ 576460756598390784ull,
+ 2882303787286921342ull,
+ 6917529092065591642ull,
+ 9799832883647480358ull,
+ 2305843026393563204ull,
+ 7493989848663982456ull,
+ 1152921513196781587ull,
+ 8070450609557340585ull,
+ 2305843030688530526ull,
+ 5188146822270419236ull,
+ 8646911366155731401ull,
+ 1729382269795172390ull,
+ 10952754396844261987ull,
+ 4035225309073637616ull,
+ 10376293640245871164ull,
+ 9223372127049089548ull,
+ 5764607578868810038ull,
+ 8070450605262373266ull,
+ 2882303791581888664ull,
+ 4611686065672028430ull,
+ 3458764552475246801ull
+};
+
+
+
+Maybe<uint32_t>
+MetricByNameLookup(const nsACString& aKey)
+{
+ static const uint8_t BASES[] = {
+ 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 1, 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, 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, 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, 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, 3, 0, 4, 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,
+ 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, 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, 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, 0, 0,
+ 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 6, 0, 12, 0, 0, 0, 0, 0, 0, 0,
+ 11, 0, 0, 0, 3, 0, 0, 3, 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, 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,
+ sMetricByNameLookupEntries);
+ return metric_result_check(aKey, entry);
+}
+
+
+/**
+ * Get a category's name from the string table.
+ */
+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.
+ */
+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
diff --git a/toolkit/components/glean/tests/pytest/metrics_test_output_js_h b/toolkit/components/glean/tests/pytest/metrics_test_output_js_h
new file mode 100644
index 0000000000..6ce9833732
--- /dev/null
+++ b/toolkit/components/glean/tests/pytest/metrics_test_output_js_h
@@ -0,0 +1,75 @@
+// -*- 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 <cstdint>
+
+#include "mozilla/AlreadyAddRefed.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/glean/bindings/GleanMetric.h"
+#include "nsStringFwd.h"
+
+class nsISupports;
+
+namespace mozilla::glean {
+
+// The category lookup table's entry type
+using category_entry_t = uint32_t;
+// The metric lookup table's entry type
+// This is a bitpacked type with 32 bits available to index into
+// the string table, 5 bits available to signify the metric type,
+// and the remaining 27 bits devoted to 2 "signal"
+// bits to signify important characteristics (metric's a labeled metric's
+// submetric, metric's been registered at runtime) and 25 bits
+// for built-in metric ids.
+// Gives room for 33554432 of each combination of
+// characteristics (which hopefully will prove to be enough).
+using metric_entry_t = uint64_t;
+
+already_AddRefed<GleanMetric> NewMetricFromId(uint32_t id, nsISupports* aParent);
+
+/**
+ * Create a submetric instance for a labeled metric of the provided type and id for the given label.
+ * Assigns or retrieves an id for the submetric from the SDK.
+ *
+ * @param aParentTypeId - The type of the parent labeled metric identified as a number generated during codegen.
+ * Only used to identify which X of LabeledX you are so that X can be created here.
+ * @param aParentMetricId - The metric id for the parent labeled metric.
+ * @param aLabel - The label for the submetric. Might not adhere to the SDK label format.
+ * @param aSubmetricId - an outparam which is assigned the submetric's SDK-generated submetric id.
+ * Used only by GIFFT.
+ */
+already_AddRefed<GleanMetric> NewSubMetricFromIds(uint32_t aParentTypeId, uint32_t aParentMetricId, const nsACString& aLabel, uint32_t* aSubmetricId, nsISupports* aParent);
+
+/**
+ * Get a category's name from the string table.
+ */
+const char* GetCategoryName(category_entry_t entry);
+
+/**
+ * Get a metric's identifier from the string table.
+ */
+const char* GetMetricIdentifier(metric_entry_t entry);
+
+/**
+ * Get a metric's id given its name.
+ */
+Maybe<uint32_t> MetricByNameLookup(const nsACString&);
+
+/**
+ * Get a category's id given its name.
+ */
+Maybe<uint32_t> CategoryByNameLookup(const nsACString&);
+
+extern const category_entry_t sCategoryByNameLookupEntries[2];
+extern const metric_entry_t sMetricByNameLookupEntries[24];
+
+} // namespace mozilla::glean
+#endif // mozilla_GleanJSMetricsLookup_h
diff --git a/toolkit/components/glean/tests/pytest/pings_test.yaml b/toolkit/components/glean/tests/pytest/pings_test.yaml
new file mode 100644
index 0000000000..efa6ba9e0a
--- /dev/null
+++ b/toolkit/components/glean/tests/pytest/pings_test.yaml
@@ -0,0 +1,116 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+# This file defines the built-in pings that are recorded by the Glean SDK. They
+# are automatically converted to Kotlin code at build time using the
+# `glean_parser` PyPI package.
+
+---
+$schema: moz://mozilla.org/schemas/glean/pings/1-0-0
+
+not-baseline:
+ description: >
+ This ping is intended to provide metrics that are managed by the library
+ itself, and not explicitly set by the application or included in the
+ application's `metrics.yaml` file.
+ The `baseline` ping is automatically sent when the application is moved to
+ the background.
+ include_client_id: true
+ bugs:
+ - https://bugzilla.mozilla.org/1512938
+ - https://bugzilla.mozilla.org/1599877
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1512938#c3
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1599877#c25
+ notification_emails:
+ - glean-team@mozilla.com
+ reasons:
+ dirty_startup: |
+ The ping was submitted at startup, because the application process was
+ killed before the Glean SDK had the chance to generate this ping, when
+ going to background, in the last session.
+
+ *Note*: this ping will not contain the `glean.baseline.duration` metric.
+ background: |
+ The ping was submitted before going to background.
+ foreground: |
+ The ping was submitted when the application went to foreground, which
+ includes when the application starts.
+
+ *Note*: this ping will not contain the `glean.baseline.duration` metric.
+
+not-metrics:
+ description: >
+ The `metrics` ping is intended for all of the metrics that are explicitly
+ set by the application or are included in the application's `metrics.yaml`
+ file (except events).
+ The reported data is tied to the ping's *measurement window*, which is the
+ time between the collection of two `metrics` ping. Ideally, this window is
+ expected to be about 24 hours, given that the collection is scheduled daily
+ at 4AM. Data in the `ping_info` section of the ping can be used to infer the
+ length of this window.
+ include_client_id: true
+ bugs:
+ - https://bugzilla.mozilla.org/1512938
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1512938#c3
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1557048#c13
+ notification_emails:
+ - glean-team@mozilla.com
+ reasons:
+ overdue: |
+ The last ping wasn't submitted on the current calendar day, but it's after
+ 4am, so this ping submitted immediately
+ today: |
+ The last ping wasn't submitted on the current calendar day, but it is
+ still before 4am, so schedule to send this ping on the current calendar
+ day at 4am.
+ tomorrow: |
+ The last ping was already submitted on the current calendar day, so
+ schedule this ping for the next calendar day at 4am.
+ upgrade: |
+ This ping was submitted at startup because the application was just
+ upgraded.
+ reschedule: |
+ A ping was just submitted. This ping was rescheduled for the next calendar
+ day at 4am.
+
+not-events:
+ description: >
+ The events ping's purpose is to transport all of the event metric
+ information. The `events` ping is automatically sent when the application is
+ moved to the background.
+ include_client_id: true
+ bugs:
+ - https://bugzilla.mozilla.org/1512938
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1512938#c3
+ notification_emails:
+ - glean-team@mozilla.com
+ reasons:
+ startup: |
+ The ping was submitted at startup. The events ping is always sent if there
+ are any pending events at startup, because event timestamps can not be
+ mixed across runs of the application.
+ background: |
+ The ping was submitted before going to background.
+ max_capacity: |
+ The maximum number of events was reached (default 500 events).
+
+not-deletion-request:
+ description: >
+ This ping is submitted when a user opts out of
+ sending technical and interaction data to Mozilla.
+ This ping is intended to communicate to the Data Pipeline
+ that the user wishes to have their reported Telemetry data deleted.
+ As such it attempts to send itself at the moment the user
+ opts out of data collection.
+ include_client_id: true
+ send_if_empty: true
+ bugs:
+ - https://bugzilla.mozilla.org/1587095
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1587095#c6
+ notification_emails:
+ - glean-team@mozilla.com
diff --git a/toolkit/components/glean/tests/pytest/pings_test_output b/toolkit/components/glean/tests/pytest/pings_test_output
new file mode 100644
index 0000000000..9e037eb22a
--- /dev/null
+++ b/toolkit/components/glean/tests/pytest/pings_test_output
@@ -0,0 +1,118 @@
+// -*- 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,
+ true,
+ vec!["background".into(), "dirty_startup".into(), "foreground".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,
+ true,
+ vec![],
+ )
+});
+
+#[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,
+ true,
+ vec!["background".into(), "max_capacity".into(), "startup".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,
+ true,
+ vec!["overdue".into(), "reschedule".into(), "today".into(), "tomorrow".into(), "upgrade".into()],
+ )
+});
+
+
+/// Instantiate custom pings once to trigger registration.
+///
+/// # Arguments
+///
+/// application_id: If present, limit to only registering custom pings
+/// assigned to the identified application.
+#[doc(hidden)]
+pub fn register_pings(application_id: Option<&str>) {
+ match application_id {
+ _ => {
+ let _ = &*not_baseline;
+ let _ = &*not_deletion_request;
+ let _ = &*not_events;
+ let _ = &*not_metrics;
+ }
+ }
+}
+
+#[cfg(feature = "with_gecko")]
+pub(crate) fn submit_ping_by_id(id: u32, reason: Option<&str>) {
+ if id & (1 << crate::factory::DYNAMIC_PING_BIT) > 0 {
+ let map = crate::factory::__jog_metric_maps::PING_MAP
+ .read()
+ .expect("Read lock for dynamic ping map was poisoned!");
+ if let Some(ping) = map.get(&id) {
+ ping.submit(reason);
+ } else {
+ // TODO: instrument this error.
+ log::error!("Cannot submit unknown dynamic ping {} by id.", id);
+ }
+ return;
+ }
+ match id {
+ 1 => not_baseline.submit(reason),
+ 2 => not_deletion_request.submit(reason),
+ 3 => not_events.submit(reason),
+ 4 => not_metrics.submit(reason),
+ _ => {
+ // TODO: instrument this error.
+ log::error!("Cannot submit unknown ping {} by id.", id);
+ }
+ }
+}
diff --git a/toolkit/components/glean/tests/pytest/pings_test_output_cpp b/toolkit/components/glean/tests/pytest/pings_test_output_cpp
new file mode 100644
index 0000000000..289529b118
--- /dev/null
+++ b/toolkit/components/glean/tests/pytest/pings_test_output_cpp
@@ -0,0 +1,62 @@
+// -*- mode: C++ -*-
+
+// AUTOGENERATED BY glean_parser. DO NOT EDIT.
+
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_glean_Pings_h
+#define mozilla_glean_Pings_h
+
+#include "mozilla/glean/bindings/Ping.h"
+
+namespace mozilla::glean_pings {
+
+/*
+ * Generated from not-baseline.
+ *
+ * This ping is intended to provide metrics that are managed by the library
+ * itself, and not explicitly set by the application or included in the
+ * application's `metrics.yaml` file. The `baseline` ping is automatically sent
+ * when the application is moved to the background.
+ */
+constexpr glean::impl::Ping NotBaseline(1);
+
+/*
+ * Generated from not-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(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-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(4);
+
+
+} // namespace mozilla::glean_pings
+
+#endif // mozilla_glean_Pings_h
diff --git a/toolkit/components/glean/tests/pytest/pings_test_output_js_cpp b/toolkit/components/glean/tests/pytest/pings_test_output_js_cpp
new file mode 100644
index 0000000000..139eb29148
--- /dev/null
+++ b/toolkit/components/glean/tests/pytest/pings_test_output_js_cpp
@@ -0,0 +1,162 @@
+// -*- 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/. */
+
+#include "mozilla/glean/bindings/GleanJSPingsLookup.h"
+
+#include "mozilla/PerfectHash.h"
+#include "nsString.h"
+
+#include "mozilla/PerfectHash.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;
+
+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 - "notDeletionRequest" */ 'n', 'o', 't', 'D', 'e', 'l', 'e', 't', 'i', 'o', 'n', 'R', 'e', 'q', 'u', 'e', 's', 't', '\0',
+ /* 31 - "notEvents" */ 'n', 'o', 't', 'E', 'v', 'e', 'n', 't', 's', '\0',
+ /* 41 - "notMetrics" */ 'n', 'o', 't', 'M', 'e', 't', 'r', 'i', 'c', 's', '\0',
+};
+
+
+
+const ping_entry_t sPingByNameLookupEntries[] = {
+ 65536,
+ 131084,
+ 196639,
+ 262185
+};
+
+
+
+Maybe<uint32_t>
+PingByNameLookup(const nsACString& aKey)
+{
+ static const uint8_t BASES[] = {
+ 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, 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, 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, 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, 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, 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, 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, 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, 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,
+ 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, 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, 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, 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, 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, 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, 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, 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,
+ 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, 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, 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, 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,
+ 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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,
+ sPingByNameLookupEntries);
+ return ping_result_check(aKey, entry);
+}
+
+
+/**
+ * Get a ping's name given its entry from the PHF.
+ */
+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.
+ */
+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
diff --git a/toolkit/components/glean/tests/pytest/pings_test_output_js_h b/toolkit/components/glean/tests/pytest/pings_test_output_js_h
new file mode 100644
index 0000000000..0c89de93ae
--- /dev/null
+++ b/toolkit/components/glean/tests/pytest/pings_test_output_js_h
@@ -0,0 +1,33 @@
+// -*- 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
+
+#include <cstdint>
+#include "mozilla/Maybe.h"
+#include "nsStringFwd.h"
+
+namespace mozilla::glean {
+
+// Contains the ping id and the index into the ping string table.
+using ping_entry_t = uint32_t;
+
+/**
+ * Get a ping's name given its entry in the PHF.
+ */
+const char* GetPingName(ping_entry_t aEntry);
+
+/**
+ * Get a ping's id given its name.
+ */
+Maybe<uint32_t> PingByNameLookup(const nsACString&);
+
+extern const ping_entry_t sPingByNameLookupEntries[4];
+} // namespace mozilla::glean
+#endif // mozilla_GleanJSPingsLookup_h
diff --git a/toolkit/components/glean/tests/pytest/python.toml b/toolkit/components/glean/tests/pytest/python.toml
new file mode 100644
index 0000000000..86c0f55cfa
--- /dev/null
+++ b/toolkit/components/glean/tests/pytest/python.toml
@@ -0,0 +1,16 @@
+[DEFAULT]
+subsuite = "fog"
+
+["test_gifft.py"]
+
+["test_glean_parser_cpp.py"]
+
+["test_glean_parser_js.py"]
+
+["test_glean_parser_rust.py"]
+
+["test_jogfile_output.py"]
+
+["test_no_expired_metrics.py"]
+
+["test_yaml_indices.py"]
diff --git a/toolkit/components/glean/tests/pytest/test_gifft.py b/toolkit/components/glean/tests/pytest/test_gifft.py
new file mode 100644
index 0000000000..5de0640bca
--- /dev/null
+++ b/toolkit/components/glean/tests/pytest/test_gifft.py
@@ -0,0 +1,49 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import io
+import sys
+from os import path
+from pathlib import Path
+
+import mozunit
+from expect_helper import expect
+
+# Shenanigans to import the FOG glean_parser runner
+FOG_ROOT_PATH = path.abspath(
+ path.join(path.dirname(__file__), path.pardir, path.pardir)
+)
+sys.path.append(path.join(FOG_ROOT_PATH, "build_scripts", "glean_parser_ext"))
+import run_glean_parser
+
+
+def test_gifft_codegen():
+ """
+ A regression test. Very fragile.
+ It generates C++ for GIFFT for metrics_test.yaml and compares it
+ byte-for-byte with expected output C++ files.
+ To generate new expected output files, set `UPDATE_EXPECT=1` when running the test suite:
+
+ UPDATE_EXPECT=1 mach test toolkit/components/glean/pytest
+ """
+
+ options = {"allow_reserved": False}
+ here_path = Path(path.dirname(__file__))
+ input_files = [here_path / "metrics_test.yaml"]
+
+ all_objs, options = run_glean_parser.parse_with_options(input_files, options)
+
+ for probe_type in ("Event", "Histogram", "Scalar"):
+ output_fd = io.StringIO()
+ cpp_fd = io.StringIO()
+ run_glean_parser.output_gifft_map(output_fd, probe_type, all_objs, cpp_fd)
+
+ expect(here_path / f"gifft_output_{probe_type}", output_fd.getvalue())
+
+ if probe_type == "Event":
+ expect(here_path / "gifft_output_EventExtra", cpp_fd.getvalue())
+
+
+if __name__ == "__main__":
+ mozunit.main()
diff --git a/toolkit/components/glean/tests/pytest/test_glean_parser_cpp.py b/toolkit/components/glean/tests/pytest/test_glean_parser_cpp.py
new file mode 100644
index 0000000000..05fd691782
--- /dev/null
+++ b/toolkit/components/glean/tests/pytest/test_glean_parser_cpp.py
@@ -0,0 +1,70 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import io
+import sys
+from os import path
+from pathlib import Path
+
+import mozunit
+from expect_helper import expect
+
+# Shenanigans to import the cpp outputter extension
+FOG_ROOT_PATH = path.abspath(
+ path.join(path.dirname(__file__), path.pardir, path.pardir)
+)
+sys.path.append(path.join(FOG_ROOT_PATH, "build_scripts", "glean_parser_ext"))
+import cpp
+import run_glean_parser
+
+
+def test_all_metric_types():
+ """Honestly, this is a pretty bad test.
+ It generates C++ for a given test metrics.yaml and compares it byte-for-byte
+ with an expected output C++ file.
+ Expect it to be fragile.
+ To generate new expected output files, set `UPDATE_EXPECT=1` when running the test suite:
+
+ UPDATE_EXPECT=1 mach test toolkit/components/glean/pytest
+ """
+
+ options = {"allow_reserved": False}
+ input_files = [Path(path.join(path.dirname(__file__), "metrics_test.yaml"))]
+
+ all_objs, options = run_glean_parser.parse_with_options(input_files, options)
+
+ output_fd = io.StringIO()
+ cpp.output_cpp(all_objs, output_fd, options)
+
+ expect(
+ path.join(path.dirname(__file__), "metrics_test_output_cpp"),
+ output_fd.getvalue(),
+ )
+
+
+def test_fake_pings():
+ """Another similarly-fragile test.
+ It generates C++ for pings_test.yaml, comparing it byte-for-byte
+ with an expected output C++ file `pings_test_output_cpp`.
+ Expect it to be fragile.
+ To generate new expected output files, set `UPDATE_EXPECT=1` when running the test suite:
+
+ UPDATE_EXPECT=1 mach test toolkit/components/glean/pytest
+ """
+
+ options = {"allow_reserved": False}
+ input_files = [Path(path.join(path.dirname(__file__), "pings_test.yaml"))]
+
+ all_objs, options = run_glean_parser.parse_with_options(input_files, options)
+
+ output_fd = io.StringIO()
+ cpp.output_cpp(all_objs, output_fd, options)
+
+ expect(
+ path.join(path.dirname(__file__), "pings_test_output_cpp"), output_fd.getvalue()
+ )
+
+
+if __name__ == "__main__":
+ mozunit.main()
diff --git a/toolkit/components/glean/tests/pytest/test_glean_parser_js.py b/toolkit/components/glean/tests/pytest/test_glean_parser_js.py
new file mode 100644
index 0000000000..3f4a37e797
--- /dev/null
+++ b/toolkit/components/glean/tests/pytest/test_glean_parser_js.py
@@ -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/.
+
+import io
+import sys
+from os import path
+from pathlib import Path
+
+import mozunit
+from expect_helper import expect
+
+# Shenanigans to import the js outputter extension
+FOG_ROOT_PATH = path.abspath(
+ path.join(path.dirname(__file__), path.pardir, path.pardir)
+)
+sys.path.append(path.join(FOG_ROOT_PATH, "build_scripts", "glean_parser_ext"))
+import run_glean_parser
+
+import js
+
+
+def test_all_metric_types():
+ """Honestly, this is a pretty bad test.
+ It generates C++ for a given test metrics.yaml and compares it byte-for-byte
+ with an expected output C++ file.
+ Expect it to be fragile.
+ To generate new expected output files, set `UPDATE_EXPECT=1` when running the test suite:
+
+ UPDATE_EXPECT=1 mach test toolkit/components/glean/tests/pytest
+ """
+
+ options = {"allow_reserved": False}
+ input_files = [Path(path.join(path.dirname(__file__), "metrics_test.yaml"))]
+
+ all_objs, options = run_glean_parser.parse_with_options(input_files, options)
+
+ output_fd_h = io.StringIO()
+ output_fd_cpp = io.StringIO()
+ js.output_js(all_objs, output_fd_h, output_fd_cpp, options)
+
+ expect(
+ path.join(path.dirname(__file__), "metrics_test_output_js_h"),
+ output_fd_h.getvalue(),
+ )
+
+ expect(
+ path.join(path.dirname(__file__), "metrics_test_output_js_cpp"),
+ output_fd_cpp.getvalue(),
+ )
+
+
+def test_fake_pings():
+ """Another similarly-fragile test.
+ It generates C++ for pings_test.yaml, comparing it byte-for-byte
+ with an expected output C++ file `pings_test_output_js`.
+ Expect it to be fragile.
+ To generate new expected output files, set `UPDATE_EXPECT=1` when running the test suite:
+
+ UPDATE_EXPECT=1 mach test toolkit/components/glean/tests/pytest
+ """
+
+ options = {"allow_reserved": False}
+ input_files = [Path(path.join(path.dirname(__file__), "pings_test.yaml"))]
+
+ all_objs, options = run_glean_parser.parse_with_options(input_files, options)
+
+ output_fd_h = io.StringIO()
+ output_fd_cpp = io.StringIO()
+ js.output_js(all_objs, output_fd_h, output_fd_cpp, options)
+
+ expect(
+ path.join(path.dirname(__file__), "pings_test_output_js_h"),
+ output_fd_h.getvalue(),
+ )
+
+ expect(
+ path.join(path.dirname(__file__), "pings_test_output_js_cpp"),
+ output_fd_cpp.getvalue(),
+ )
+
+
+if __name__ == "__main__":
+ mozunit.main()
diff --git a/toolkit/components/glean/tests/pytest/test_glean_parser_rust.py b/toolkit/components/glean/tests/pytest/test_glean_parser_rust.py
new file mode 100644
index 0000000000..329c7ed348
--- /dev/null
+++ b/toolkit/components/glean/tests/pytest/test_glean_parser_rust.py
@@ -0,0 +1,89 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import io
+import sys
+from os import path
+from pathlib import Path
+
+import mozunit
+from expect_helper import expect
+
+# Shenanigans to import the rust outputter extension
+FOG_ROOT_PATH = path.abspath(
+ path.join(path.dirname(__file__), path.pardir, path.pardir)
+)
+sys.path.append(path.join(FOG_ROOT_PATH, "build_scripts", "glean_parser_ext"))
+import run_glean_parser
+import rust
+
+# Shenanigans to import the in-tree glean_parser
+GECKO_PATH = path.join(FOG_ROOT_PATH, path.pardir, path.pardir, path.pardir)
+sys.path.append(path.join(GECKO_PATH, "third_party", "python", "glean_parser"))
+
+
+def test_all_metric_types():
+ """Honestly, this is a pretty bad test.
+ It generates Rust for a given test metrics.yaml and compares it byte-for-byte
+ with an expected output Rust file.
+ Expect it to be fragile.
+ To generate new expected output files, set `UPDATE_EXPECT=1` when running the test suite:
+
+ UPDATE_EXPECT=1 mach test toolkit/components/glean/pytest
+ """
+
+ options = {"allow_reserved": False}
+ input_files = [Path(path.join(path.dirname(__file__), "metrics_test.yaml"))]
+
+ all_objs, options = run_glean_parser.parse_with_options(input_files, options)
+
+ output_fd = io.StringIO()
+ rust.output_rust(all_objs, output_fd, {}, options)
+
+ expect(
+ path.join(path.dirname(__file__), "metrics_test_output"), output_fd.getvalue()
+ )
+
+
+def test_fake_pings():
+ """Another similarly-bad test.
+ It generates Rust for pings_test.yaml, comparing it byte-for-byte
+ with an expected output Rust file.
+ Expect it to be fragile.
+ To generate new expected output files, set `UPDATE_EXPECT=1` when running the test suite:
+
+ UPDATE_EXPECT=1 mach test toolkit/components/glean/pytest
+ """
+
+ options = {"allow_reserved": False}
+ input_files = [Path(path.join(path.dirname(__file__), "pings_test.yaml"))]
+
+ all_objs, options = run_glean_parser.parse_with_options(input_files, options)
+
+ output_fd = io.StringIO()
+ rust.output_rust(all_objs, output_fd, {}, options)
+
+ expect(path.join(path.dirname(__file__), "pings_test_output"), output_fd.getvalue())
+
+
+def test_expires_version():
+ """This test relies on the intermediary object format output by glean_parser.
+ Expect it to be fragile on glean_parser updates that change that format.
+ """
+
+ # The test file has 41, 42, 100. Use 42.0a1 here to ensure "expires == version" means expired.
+ options = run_glean_parser.get_parser_options("42.0a1")
+ input_files = [
+ Path(path.join(path.dirname(__file__), "metrics_expires_versions_test.yaml"))
+ ]
+
+ all_objs, options = run_glean_parser.parse_with_options(input_files, options)
+
+ assert all_objs["test"]["expired1"].disabled is True
+ assert all_objs["test"]["expired2"].disabled is True
+ assert all_objs["test"]["unexpired"].disabled is False
+
+
+if __name__ == "__main__":
+ mozunit.main()
diff --git a/toolkit/components/glean/tests/pytest/test_jogfile_output.py b/toolkit/components/glean/tests/pytest/test_jogfile_output.py
new file mode 100644
index 0000000000..801c0967ff
--- /dev/null
+++ b/toolkit/components/glean/tests/pytest/test_jogfile_output.py
@@ -0,0 +1,50 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import io
+import sys
+from os import path
+from pathlib import Path
+
+import mozunit
+from expect_helper import expect
+
+# Shenanigans to import the FOG glean_parser runner
+FOG_ROOT_PATH = path.abspath(
+ path.join(path.dirname(__file__), path.pardir, path.pardir)
+)
+sys.path.append(path.join(FOG_ROOT_PATH, "build_scripts", "glean_parser_ext"))
+import jog
+import run_glean_parser
+
+
+def test_jogfile_output():
+ """
+ A regression test. Very fragile.
+ It generates a jogfile for metrics_test.yaml and compares it
+ byte-for-byte with an expected output file.
+
+ Also, part one of a two-part test.
+ The generated jogfile is consumed in Rust_TestJogfile in t/c/g/tests/gtest/test.rs
+ This is to ensure that the jogfile we generate in Python can be consumed in Rust.
+
+ To generate new expected output files, set `UPDATE_EXPECT=1` when running the test suite:
+
+ UPDATE_EXPECT=1 mach test toolkit/components/glean/pytest
+ """
+
+ options = {"allow_reserved": False}
+ here_path = Path(path.dirname(__file__))
+ input_files = [here_path / "metrics_test.yaml", here_path / "pings_test.yaml"]
+
+ all_objs, options = run_glean_parser.parse_with_options(input_files, options)
+
+ output_fd = io.StringIO()
+ jog.output_file(all_objs, output_fd, options)
+
+ expect(here_path / "jogfile_output", output_fd.getvalue())
+
+
+if __name__ == "__main__":
+ mozunit.main()
diff --git a/toolkit/components/glean/tests/pytest/test_no_expired_metrics.py b/toolkit/components/glean/tests/pytest/test_no_expired_metrics.py
new file mode 100644
index 0000000000..9783acaf0a
--- /dev/null
+++ b/toolkit/components/glean/tests/pytest/test_no_expired_metrics.py
@@ -0,0 +1,46 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import sys
+from os import path
+from pathlib import Path
+
+import mozunit
+
+# Shenanigans to import the metrics index's list of metrics.yamls
+FOG_ROOT_PATH = path.abspath(
+ path.join(path.dirname(__file__), path.pardir, path.pardir)
+)
+sys.path.append(FOG_ROOT_PATH)
+from metrics_index import metrics_yamls, tags_yamls
+
+# Shenanigans to import run_glean_parser
+sys.path.append(path.join(FOG_ROOT_PATH, "build_scripts", "glean_parser_ext"))
+import run_glean_parser
+
+# Shenanigans to import the in-tree glean_parser
+GECKO_PATH = path.join(FOG_ROOT_PATH, path.pardir, path.pardir, path.pardir)
+sys.path.append(path.join(GECKO_PATH, "third_party", "python", "glean_parser"))
+from glean_parser import lint, parser, util
+
+
+def test_no_metrics_expired():
+ """
+ Of all the metrics included in this build, are any expired?
+ If so, they must be removed or renewed.
+
+ (This also checks other lints, as a treat.)
+ """
+ with open("browser/config/version.txt", "r") as version_file:
+ app_version = version_file.read().strip()
+
+ options = run_glean_parser.get_parser_options(app_version)
+ paths = [Path(x) for x in metrics_yamls] + [Path(x) for x in tags_yamls]
+ all_objs = parser.parse_objects(paths, options)
+ assert not util.report_validation_errors(all_objs)
+ assert not lint.lint_metrics(all_objs.value, options)
+
+
+if __name__ == "__main__":
+ mozunit.main()
diff --git a/toolkit/components/glean/tests/pytest/test_yaml_indices.py b/toolkit/components/glean/tests/pytest/test_yaml_indices.py
new file mode 100644
index 0000000000..9ccde409e5
--- /dev/null
+++ b/toolkit/components/glean/tests/pytest/test_yaml_indices.py
@@ -0,0 +1,42 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import sys
+from os import path
+
+import mozunit
+
+# Shenanigans to import the metrics index's lists of yamls
+FOG_ROOT_PATH = path.abspath(
+ path.join(path.dirname(__file__), path.pardir, path.pardir)
+)
+sys.path.append(FOG_ROOT_PATH)
+import metrics_index
+
+
+def test_yamls_sorted():
+ """
+ Ensure the yamls indices are sorted lexicographically.
+ """
+ # Ignore lists that are the concatenation of others.
+ to_ignore = ["metrics_yamls", "pings_yamls"]
+
+ # Fetch names of all variables defined in the `metrics_index` module.
+ yaml_lists = [
+ item
+ for item in dir(metrics_index)
+ if isinstance(getattr(metrics_index, item), list) and not item.startswith("__")
+ ]
+ for name in yaml_lists:
+ if name in to_ignore:
+ continue
+
+ yamls_to_test = metrics_index.__dict__[name]
+ assert (
+ sorted(yamls_to_test) == yamls_to_test
+ ), f"{name} must be be lexicographically sorted."
+
+
+if __name__ == "__main__":
+ mozunit.main()
diff --git a/toolkit/components/glean/tests/test_metrics.yaml b/toolkit/components/glean/tests/test_metrics.yaml
new file mode 100644
index 0000000000..bc290d470f
--- /dev/null
+++ b/toolkit/components/glean/tests/test_metrics.yaml
@@ -0,0 +1,859 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+# This file is for Internal FOG Test Use Only.
+
+---
+$schema: moz://mozilla.org/schemas/glean/metrics/2-0-0
+
+test_only:
+ bad_code:
+ type: counter
+ description: |
+ Number of times we encountered bad code.
+ This is a test-only metric.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1646165
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1646165#c1
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - glean-team@mozilla.com
+ expires: never
+ send_in_pings:
+ - test-ping
+
+ can_we_time_it:
+ type: timespan
+ time_unit: nanosecond
+ description: |
+ Test metric for a timespan.
+ This is a test-only metric.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1646165
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1646165#c1
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - glean-team@mozilla.com
+ expires: never
+ send_in_pings:
+ - test-ping
+
+ cheesy_string:
+ type: string
+ description: |
+ Only the cheesiest of strings.
+ This is a test-only metric.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1673662
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1673662#c1
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - glean-team@mozilla.com
+ expires: never
+ send_in_pings:
+ - test-ping
+
+ cheesy_string_list:
+ type: string_list
+ description: |
+ Only the cheesiest of strings. In list form!
+ This is a test-only metric.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1682960
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1682960#c1
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - glean-team@mozilla.com
+ expires: never
+ send_in_pings:
+ - test-ping
+
+ what_a_date:
+ type: datetime
+ time_unit: second
+ description: >
+ ...To be writing FOG code.
+ This is a test-only metric.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1646165
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1646165#c1
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - glean-team@mozilla.com
+ expires: never
+ send_in_pings:
+ - test-ping
+
+ what_id_it: # Using a different metrics yaml style for fun.
+ type: uuid
+ description: |
+ Just a UUID.
+ This is a test-only metric.
+ bugs: ["https://bugzilla.mozilla.org/show_bug.cgi?id=1673664"]
+ data_reviews: ["https://bugzilla.mozilla.org/show_bug.cgi?id=1673664#c1"]
+ data_sensitivity: ["technical"]
+ notification_emails: ["glean-team@mozilla.com"]
+ expires: never
+ send_in_pings: ["test-ping"]
+
+ can_we_flag_it:
+ type: boolean
+ description: |
+ Test metric for a boolean.
+ This is a test-only metric.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1646165
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1646165#c1
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - glean-team@mozilla.com
+ expires: never
+ send_in_pings:
+ - test-ping
+
+ do_you_remember:
+ type: memory_distribution
+ memory_unit: megabyte
+ description: |
+ They say it's the second thing to go.
+ This is a test-only metric.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1673648
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1673648#c1
+ data_sensitivity:
+ - technical
+ expires: never
+ notification_emails:
+ - glean-team@mozilla.com
+ send_in_pings:
+ - test-ping
+
+ what_time_is_it:
+ type: timing_distribution
+ time_unit: microsecond
+ description: |
+ Adheres to at least two of the top ten fallacies programmers believe
+ about time.
+ This is a test-only metric.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1673663
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1673663#c1
+ data_sensitivity:
+ - technical
+ expires: never
+ notification_emails:
+ - glean-team@mozilla.com
+ send_in_pings:
+ - test-ping
+ telemetry_mirror: TELEMETRY_TEST_MIRROR_FOR_TIMING
+
+ mabels_kitchen_counters:
+ type: labeled_counter
+ description: |
+ Counts Mabels labeled by their kitchen counters.
+ This is a test-only metric.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1675277
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1675277#c1
+ data_sensitivity:
+ - technical
+ expires: never
+ notification_emails:
+ - glean-team@mozilla.com
+ send_in_pings:
+ - test-ping
+
+ mabels_labeled_counters:
+ type: labeled_counter
+ description: |
+ Counts Mabels labeled by their kitchen counters.
+ Now with static labels.
+ This is a test-only metric.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1672273
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1672273
+ data_sensitivity:
+ - technical
+ expires: never
+ notification_emails:
+ - glean-team@mozilla.com
+ send_in_pings:
+ - test-ping
+ labels:
+ - next_to_the_fridge
+ - clean
+
+ mabels_bathroom_counters:
+ type: labeled_counter
+ description: |
+ Counts Mabels labeled by their bathroom counters.
+ This is a test-only metric.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1683171
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1683171#c1
+ data_sensitivity:
+ - technical
+ expires: never
+ notification_emails:
+ - glean-team@mozilla.com
+ send_in_pings:
+ - test-ping
+
+ mabels_like_balloons:
+ type: labeled_boolean
+ description: |
+ Does the labeled Mabel like balloons?
+ This is a test-only metric.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1675277
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1675277#c1
+ data_sensitivity:
+ - technical
+ expires: never
+ notification_emails:
+ - glean-team@mozilla.com
+ send_in_pings:
+ - test-ping
+
+ mabels_like_labeled_balloons:
+ type: labeled_boolean
+ description: |
+ Does the labeled Mabel like balloons?
+ This is a test-only metric.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1672273
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1672273
+ data_sensitivity:
+ - technical
+ expires: never
+ notification_emails:
+ - glean-team@mozilla.com
+ send_in_pings:
+ - test-ping
+ labels:
+ - water
+ - birthday_party
+
+ mabels_balloon_strings:
+ type: labeled_string
+ description: |
+ What do the labeled Mabel's liked balloons' strings say?
+ This is a test-only metric.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1675277
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1675277#c1
+ data_sensitivity:
+ - technical
+ expires: never
+ notification_emails:
+ - glean-team@mozilla.com
+ send_in_pings:
+ - test-ping
+
+ mabels_balloon_labels:
+ type: labeled_string
+ description: |
+ How does Mabel label her balloons?
+ This is a test-only metric.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1672273
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1672273
+ data_sensitivity:
+ - technical
+ expires: never
+ notification_emails:
+ - glean-team@mozilla.com
+ send_in_pings:
+ - test-ping
+ labels:
+ - celebratory
+ - celebratory_and_snarky
+
+ mabels_label_maker:
+ type: labeled_string
+ description: |
+ Mabel just got a label maker and wants to party like it's
+ 1999.
+ This is a test-only metric.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1696388
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1696388
+ data_sensitivity:
+ - technical
+ expires: never
+ notification_emails:
+ - glean-team@mozilla.com
+ send_in_pings:
+ - test-ping
+
+ mirror_time:
+ type: timespan
+ time_unit: nanosecond
+ description: |
+ Mirrored metric for a timespan.
+ This is a test-only metric.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1685406
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1685406#c1
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - glean-team@mozilla.com
+ expires: never
+ send_in_pings:
+ - test-ping
+ telemetry_mirror: TELEMETRY_TEST_MIRROR_FOR_TIMESPAN
+
+ mirror_time_nanos:
+ type: timespan
+ time_unit: nanosecond
+ description: |
+ Mirrored metric for a timespan.
+ This is a test-only metric.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1704106
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1704106#c1
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - glean-team@mozilla.com
+ expires: never
+ send_in_pings:
+ - test-ping
+ telemetry_mirror: TELEMETRY_TEST_MIRROR_FOR_TIMESPAN_NANOS
+
+ mirrors_for_labeled_bools:
+ type: labeled_boolean
+ description: |
+ Mirrored metric.
+ This is a test-only metric.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1675277
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1675277#c1
+ data_sensitivity:
+ - technical
+ expires: never
+ notification_emails:
+ - glean-team@mozilla.com
+ send_in_pings:
+ - test-ping
+ telemetry_mirror: TELEMETRY_TEST_MIRROR_FOR_LABELED_BOOL
+
+ one_ping_one_bool:
+ type: boolean
+ description: |
+ One bool for one ping only.
+ This is a test-only metric.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1685402
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1685402#c1
+ data_sensitivity:
+ - technical
+ expires: never
+ notification_emails:
+ - glean-team@mozilla.com
+ send_in_pings:
+ - one-ping-only
+
+ meaning_of_life:
+ type: quantity
+ unit: unfathomable
+ description: |
+ Measures the one true answer to the Ultimate Question of Life,
+ the Universe, and Everything.
+ Approximately.
+ This is a test-only metric.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1704846
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1704846#c1
+ data_sensitivity:
+ - technical
+ expires: never
+ notification_emails:
+ - glean-team@mozilla.com
+ send_in_pings:
+ - test-ping
+ telemetry_mirror: TELEMETRY_TEST_MIRROR_FOR_QUANTITY
+
+
+test_only.ipc:
+ a_counter:
+ type: counter
+ description: |
+ This is a test-only metric.
+ Just counting things.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1646165
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1685406
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1646165#c1
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - glean-team@mozilla.com
+ expires: never
+ send_in_pings:
+ - store1
+ no_lint:
+ - COMMON_PREFIX
+ telemetry_mirror: TELEMETRY_TEST_MIRROR_FOR_COUNTER
+ a_bool:
+ type: boolean
+ description: |
+ This is a test-only metric.
+ Just flagging things.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1646165
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1685406
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1646165#c1
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - glean-team@mozilla.com
+ expires: never
+ send_in_pings:
+ - store1
+ no_lint:
+ - COMMON_PREFIX
+ telemetry_mirror: TELEMETRY_TEST_BOOLEAN_KIND
+ a_date:
+ type: datetime
+ time_unit: second
+ description: |
+ This is a test-only metric.
+ Just putting things on the calendar.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1646165
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1685406
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1646165#c1
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - glean-team@mozilla.com
+ expires: never
+ send_in_pings:
+ - store1
+ no_lint:
+ - COMMON_PREFIX
+ telemetry_mirror: TELEMETRY_TEST_MIRROR_FOR_DATE
+ a_string:
+ type: string
+ description: |
+ This is a test-only metric.
+ Just setting some strings.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1646165
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1685406
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1646165#c1
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - glean-team@mozilla.com
+ expires: never
+ send_in_pings:
+ - store1
+ no_lint:
+ - COMMON_PREFIX
+ telemetry_mirror: TELEMETRY_TEST_MULTIPLE_STORES_STRING
+ a_text:
+ type: text
+ description: |
+ This is a test-only metric.
+ For holding Text data.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1828528
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1688281#c1
+ data_sensitivity:
+ - web_activity
+ 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
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1685406
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1646165#c1
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - glean-team@mozilla.com
+ expires: never
+ send_in_pings:
+ - store1
+ no_lint:
+ - COMMON_PREFIX
+ telemetry_mirror: TELEMETRY_TEST_LINEAR
+ a_timing_dist:
+ type: timing_distribution
+ description: |
+ This is a test-only metric.
+ Just measuring time.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1646165
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1685406
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1646165#c1
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - glean-team@mozilla.com
+ expires: never
+ send_in_pings:
+ - store1
+ no_lint:
+ - COMMON_PREFIX
+ telemetry_mirror: TELEMETRY_TEST_EXPONENTIAL
+ a_custom_dist:
+ type: custom_distribution
+ description: |
+ This is a test-only metric.
+ Just measuring samples.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1713398
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1713398#c1
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - glean-team@mozilla.com
+ expires: never
+ send_in_pings:
+ - store1
+ no_lint:
+ - COMMON_PREFIX
+ range_min: 1
+ range_max: 2147483646
+ bucket_count: 10
+ histogram_type: linear
+ telemetry_mirror: TELEMETRY_TEST_MIRROR_FOR_CUSTOM
+ a_string_list:
+ type: string_list
+ description: |
+ This is a test-only metric.
+ Just appending some strings.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1646165
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1685406
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1646165#c1
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - glean-team@mozilla.com
+ expires: never
+ send_in_pings:
+ - store1
+ no_lint:
+ - COMMON_PREFIX
+ telemetry_mirror: TELEMETRY_TEST_KEYED_BOOLEAN_KIND
+ an_event:
+ type: event
+ extra_keys:
+ extra1:
+ type: string
+ description: "Some extra data"
+ extra2:
+ type: string
+ description: "Some extra data again"
+ description: |
+ This is a test-only metric.
+ Just recording some events.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1646165
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1685406
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1646165#c1
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - glean-team@mozilla.com
+ expires: never
+ send_in_pings:
+ - store1
+ no_lint:
+ - COMMON_PREFIX
+ telemetry_mirror: TelemetryTest_MirrorWithExtra_Object1
+ event_with_extra:
+ type: event
+ extra_keys:
+ extra1:
+ type: string
+ description: "Some extra data"
+ extra2:
+ type: quantity
+ description: "Some extra data again"
+ extra3_longer_name:
+ type: boolean
+ description: "Some extra data again. Also tests extras with underscores"
+ description: |
+ This is a test-only metric.
+ Just recording some events.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1646165
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1685406
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1646165#c1
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - glean-team@mozilla.com
+ expires: never
+ send_in_pings:
+ - store1
+ no_lint:
+ - COMMON_PREFIX
+ no_extra_event:
+ type: event
+ description: |
+ This is a test-only metric.
+ Just recording some events without the extra fuss.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1646165
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1685406
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1646165#c1
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - glean-team@mozilla.com
+ expires: never
+ send_in_pings:
+ - store1
+ telemetry_mirror: TelemetryTest_NotExpiredOptout_Object1
+ a_uuid:
+ type: uuid
+ description: |
+ This is a test-only metric.
+ Just recording some unique identifiers.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1646165
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1685406
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1646165#c1
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - glean-team@mozilla.com
+ expires: never
+ send_in_pings:
+ - store1
+ no_lint:
+ - COMMON_PREFIX
+ telemetry_mirror: TELEMETRY_TEST_STRING_KIND
+ a_labeled_counter:
+ type: labeled_counter
+ description: |
+ This is a test-only metric.
+ Just counting labeled things.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1688281
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1685406
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1688281#c1
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - glean-team@mozilla.com
+ expires: never
+ send_in_pings:
+ - store1
+ no_lint:
+ - COMMON_PREFIX
+ telemetry_mirror: TELEMETRY_TEST_ANOTHER_MIRROR_FOR_LABELED_COUNTER
+ another_labeled_counter:
+ type: labeled_counter
+ description: |
+ This is a test-only metric.
+ Just another metric counting labeled things.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1688281
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1685406
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1758795
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1688281#c1
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - glean-team@mozilla.com
+ expires: never
+ send_in_pings:
+ - store1
+ no_lint:
+ - COMMON_PREFIX
+ telemetry_mirror: TELEMETRY_TEST_MIRROR_FOR_LABELED_COUNTER
+ a_quantity:
+ type: quantity
+ unit: squad
+ description: |
+ This is a test-only metric.
+ Just quantifying things.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1704846
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1704846#c1
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - glean-team@mozilla.com
+ expires: never
+ send_in_pings:
+ - store1
+ no_lint:
+ - COMMON_PREFIX
+ irate:
+ type: rate
+ description: |
+ This is a test-only metric.
+ A rate that isn't happy about it.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1694496
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1694496#c1
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - glean-team@mozilla.com
+ expires: never
+ send_in_pings:
+ - store1
+ telemetry_mirror: TELEMETRY_TEST_MIRROR_FOR_RATE
+ rate_with_external_denominator:
+ type: rate
+ denominator_metric: test_only.ipc.an_external_denominator
+ description: |
+ This is a test-only metric.
+ A rate with a denominator that is Out There.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1694496
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1694496#c1
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - glean-team@mozilla.com
+ expires: never
+ send_in_pings:
+ - store1
+ an_external_denominator:
+ type: counter
+ description: |
+ This is a test-only metric.
+ A denominator not from around here.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1694496
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1694496#c1
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - glean-team@mozilla.com
+ expires: never
+ send_in_pings:
+ - store1
+ no_lint:
+ - COMMON_PREFIX
+ a_url:
+ type: url
+ description: |
+ This is a test-only metric.
+ Just setting some Urls.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1766980
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1646165#c1
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - glean-team@mozilla.com
+ expires: never
+ send_in_pings:
+ - store1
+ no_lint:
+ - COMMON_PREFIX
+ telemetry_mirror: TELEMETRY_TEST_MIRROR_FOR_URL
+
+test_only.jog:
+ a_counter:
+ type: counter
+ description: |
+ This is a test-only metric.
+ Just counting things.
+ Tied closely to test_jog_name_collision.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1767039
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1767039
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - glean-team@mozilla.com
+ expires: never
+ send_in_pings:
+ - store1
+
+ an_event:
+ type: event
+ extra_keys:
+ extra1:
+ type: string
+ description: "Some extra data"
+ extra2:
+ type: string
+ description: "Some extra data again"
+ description: |
+ This is a test-only metric.
+ Just recording some events.
+ Tied closely to test_jog_name_collision.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1767039
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1767039
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - glean-team@mozilla.com
+ expires: never
+ send_in_pings:
+ - store1
diff --git a/toolkit/components/glean/tests/test_pings.yaml b/toolkit/components/glean/tests/test_pings.yaml
new file mode 100644
index 0000000000..d62f682109
--- /dev/null
+++ b/toolkit/components/glean/tests/test_pings.yaml
@@ -0,0 +1,42 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+# This file defines the pings that are recorded by the Glean SDK. They are
+# automatically converted to platform-specific code at build time using the
+# `glean_parser` PyPI package.
+
+# This file is presently for Internal FOG Test Use Only.
+
+---
+$schema: moz://mozilla.org/schemas/glean/pings/2-0-0
+
+one-ping-only:
+ description: |
+ This ping is for tests only.
+ include_client_id: false
+ send_if_empty: true
+ bugs:
+ - https://bugzilla.mozilla.org/1673660
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1673660#c1
+ notification_emails:
+ - chutten@mozilla.com
+ - glean-team@mozilla.com
+ no_lint:
+ - REDUNDANT_PING
+
+test-ping:
+ description: |
+ This ping is for tests only.
+ include_client_id: false
+ send_if_empty: true
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1737157
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1737157#c1
+ notification_emails:
+ - chutten@mozilla.com
+ - glean-team@mozilla.com
+ no_lint:
+ - REDUNDANT_PING
diff --git a/toolkit/components/glean/tests/xpcshell/head.js b/toolkit/components/glean/tests/xpcshell/head.js
new file mode 100644
index 0000000000..f42bd02822
--- /dev/null
+++ b/toolkit/components/glean/tests/xpcshell/head.js
@@ -0,0 +1,6 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
diff --git a/toolkit/components/glean/tests/xpcshell/test_FOGIPCLimit.js b/toolkit/components/glean/tests/xpcshell/test_FOGIPCLimit.js
new file mode 100644
index 0000000000..10ab9f5bc3
--- /dev/null
+++ b/toolkit/components/glean/tests/xpcshell/test_FOGIPCLimit.js
@@ -0,0 +1,51 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+
+const { ContentTaskUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/ContentTaskUtils.sys.mjs"
+);
+
+add_setup(
+ /* on Android FOG is set up through head.js */
+ { skip_if: () => !runningInParent || AppConstants.platform == "android" },
+ function test_setup() {
+ // Give FOG a temp profile to init within.
+ do_get_profile();
+
+ // We need to initialize it once, otherwise operations will be stuck in the pre-init queue.
+ Services.fog.initializeFOG();
+ }
+);
+
+// Keep in sync with ipc.rs.
+// "Why no -1?" Because the limit's 100k. The -1 is because of atomic ops.
+const FOG_IPC_PAYLOAD_ACCESS_LIMIT = 100000;
+
+add_task({ skip_if: () => runningInParent }, async function run_child_stuff() {
+ for (let i = 0; i < FOG_IPC_PAYLOAD_ACCESS_LIMIT + 1; i++) {
+ Glean.testOnly.badCode.add(1);
+ }
+});
+
+add_task(
+ { skip_if: () => !runningInParent },
+ async function test_fog_ipc_limit() {
+ await run_test_in_child("test_FOGIPCLimit.js");
+
+ await ContentTaskUtils.waitForCondition(() => {
+ return !!Glean.testOnly.badCode.testGetValue();
+ }, "Waiting for IPC.");
+
+ // The child exceeded the number of accesses to trigger an IPC flush.
+ Assert.greater(
+ Glean.testOnly.badCode.testGetValue(),
+ FOG_IPC_PAYLOAD_ACCESS_LIMIT
+ );
+ }
+);
diff --git a/toolkit/components/glean/tests/xpcshell/test_FOGInit.js b/toolkit/components/glean/tests/xpcshell/test_FOGInit.js
new file mode 100644
index 0000000000..e5aaced074
--- /dev/null
+++ b/toolkit/components/glean/tests/xpcshell/test_FOGInit.js
@@ -0,0 +1,49 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+
+add_setup(
+ /* on Android FOG is set up through head.js */
+ { skip_if: () => AppConstants.platform == "android" },
+ function test_setup() {
+ // FOG needs a profile directory to put its data in.
+ do_get_profile();
+
+ // Glean init (via `chrono`) gets the timezone via unprotected write.
+ // This is being worked around:
+ // https://github.com/chronotope/chrono/pull/677
+ // Until that reaches a release and we update to it (bug 1780401), ensure
+ // local time has been loaded by JS before we kick of Glean init.
+ new Date().getHours(); // used for its side effect.
+
+ // We need to initialize it once, otherwise operations will be stuck in the pre-init queue.
+ Services.fog.initializeFOG();
+ }
+);
+
+add_task(function test_fog_init_works() {
+ if (new Date().getHours() >= 3 && new Date().getHours() <= 4) {
+ // We skip this test if it's too close to 4AM, when we might send a
+ // "metrics" ping between init and this test being run.
+ Assert.ok(true, "Too close to 'metrics' ping send window. Skipping test.");
+ return;
+ }
+ Assert.greater(
+ Glean.fog.initialization.testGetValue(),
+ 0,
+ "FOG init happened, and its time was measured."
+ );
+});
+
+add_task(function test_fog_initialized_with_correct_rate_limit() {
+ Assert.greater(
+ Glean.fog.maxPingsPerMinute.testGetValue(),
+ 0,
+ "FOG has been initialized with a ping rate limit of greater than 0."
+ );
+});
diff --git a/toolkit/components/glean/tests/xpcshell/test_FOGPrefs.js b/toolkit/components/glean/tests/xpcshell/test_FOGPrefs.js
new file mode 100644
index 0000000000..943ffb3186
--- /dev/null
+++ b/toolkit/components/glean/tests/xpcshell/test_FOGPrefs.js
@@ -0,0 +1,47 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TELEMETRY_SERVER_PREF = "toolkit.telemetry.server";
+const UPLOAD_PREF = "datareporting.healthreport.uploadEnabled";
+const LOCALHOST_PREF = "telemetry.fog.test.localhost_port";
+
+// FOG needs a profile directory to put its data in.
+do_get_profile();
+
+// We want Glean to use a localhost server so we can be SURE not to send data to the outside world.
+// Yes, the port spells GLEAN on a T9 keyboard, why do you ask?
+Services.prefs.setIntPref(LOCALHOST_PREF, 45326);
+// We need to initialize it once, otherwise operations will be stuck in the pre-init queue.
+Services.fog.initializeFOG();
+
+add_task(function test_fog_upload_only() {
+ // Don't forget to point the telemetry server to localhost, or Telemetry
+ // might make a non-local connection during the test run.
+ Services.prefs.setStringPref(
+ TELEMETRY_SERVER_PREF,
+ "http://localhost/telemetry-fake/"
+ );
+ // Be sure to set port=-1 for faking success _before_ enabling upload.
+ // Or else there's a short window where we might send something.
+ Services.prefs.setIntPref(LOCALHOST_PREF, -1);
+ Services.prefs.setBoolPref(UPLOAD_PREF, true);
+
+ const value = 42;
+ Glean.testOnly.meaningOfLife.set(value);
+ // We specifically look at the custom ping's value because we know it
+ // won't be reset by being sent.
+ Assert.equal(value, Glean.testOnly.meaningOfLife.testGetValue("test-ping"));
+
+ // Despite upload being disabled, we keep the old values around.
+ Services.prefs.setBoolPref(UPLOAD_PREF, false);
+ Assert.equal(value, Glean.testOnly.meaningOfLife.testGetValue("test-ping"));
+
+ // Now, when we turn the fake upload off, we clear the stores
+ Services.prefs.setIntPref(LOCALHOST_PREF, 0);
+ Assert.equal(
+ undefined,
+ Glean.testOnly.meaningOfLife.testGetValue("test-ping")
+ );
+});
diff --git a/toolkit/components/glean/tests/xpcshell/test_GIFFT.js b/toolkit/components/glean/tests/xpcshell/test_GIFFT.js
new file mode 100644
index 0000000000..015b4d4e38
--- /dev/null
+++ b/toolkit/components/glean/tests/xpcshell/test_GIFFT.js
@@ -0,0 +1,523 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+const { setTimeout } = ChromeUtils.importESModule(
+ "resource://gre/modules/Timer.sys.mjs"
+);
+const { TelemetryTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TelemetryTestUtils.sys.mjs"
+);
+
+const Telemetry = Services.telemetry;
+
+function sleep(ms) {
+ /* eslint-disable mozilla/no-arbitrary-setTimeout */
+ return new Promise(resolve => setTimeout(resolve, ms));
+}
+
+function scalarValue(aScalarName) {
+ let snapshot = Telemetry.getSnapshotForScalars();
+ return "parent" in snapshot ? snapshot.parent[aScalarName] : undefined;
+}
+
+function keyedScalarValue(aScalarName) {
+ let snapshot = Telemetry.getSnapshotForKeyedScalars();
+ return "parent" in snapshot ? snapshot.parent[aScalarName] : undefined;
+}
+
+add_setup(function test_setup() {
+ // FOG needs a profile directory to put its data in.
+ do_get_profile();
+
+ Services.prefs.setBoolPref(
+ "toolkit.telemetry.testing.overrideProductsCheck",
+ true
+ );
+
+ // We need to initialize it once, otherwise operations will be stuck in the pre-init queue.
+ // On Android FOG is set up through head.js.
+ if (AppConstants.platform != "android") {
+ Services.fog.initializeFOG();
+ }
+});
+
+add_task(function test_gifft_counter() {
+ Glean.testOnlyIpc.aCounter.add(20);
+ Assert.equal(20, Glean.testOnlyIpc.aCounter.testGetValue());
+ Assert.equal(20, scalarValue("telemetry.test.mirror_for_counter"));
+});
+
+add_task(function test_gifft_boolean() {
+ Glean.testOnlyIpc.aBool.set(false);
+ Assert.equal(false, Glean.testOnlyIpc.aBool.testGetValue());
+ Assert.equal(false, scalarValue("telemetry.test.boolean_kind"));
+});
+
+add_task(function test_gifft_datetime() {
+ const dateStr = "2021-03-22T16:06:00";
+ const value = new Date(dateStr);
+ Glean.testOnlyIpc.aDate.set(value.getTime() * 1000);
+
+ let received = Glean.testOnlyIpc.aDate.testGetValue();
+ Assert.equal(value.getTime(), received.getTime());
+ Assert.ok(scalarValue("telemetry.test.mirror_for_date").startsWith(dateStr));
+});
+
+add_task(function test_gifft_string() {
+ const value = "a string!";
+ Glean.testOnlyIpc.aString.set(value);
+
+ Assert.equal(value, Glean.testOnlyIpc.aString.testGetValue());
+ Assert.equal(value, scalarValue("telemetry.test.multiple_stores_string"));
+});
+
+add_task(function test_gifft_memory_dist() {
+ Glean.testOnlyIpc.aMemoryDist.accumulate(7);
+ Glean.testOnlyIpc.aMemoryDist.accumulate(17);
+
+ let data = Glean.testOnlyIpc.aMemoryDist.testGetValue();
+ // `data.sum` is in bytes, but the metric is in KB.
+ Assert.equal(24 * 1024, data.sum, "Sum's correct");
+ for (let [bucket, count] of Object.entries(data.values)) {
+ Assert.ok(
+ count == 0 || (count == 1 && (bucket == 6888 || bucket == 17109)),
+ `Only two buckets have a sample ${bucket} ${count}`
+ );
+ }
+
+ data = Telemetry.getHistogramById("TELEMETRY_TEST_LINEAR").snapshot();
+ Telemetry.getHistogramById("TELEMETRY_TEST_LINEAR").clear();
+ Assert.equal(24, data.sum, "Histogram's in `memory_unit` units");
+ Assert.equal(2, data.values["1"], "Both samples in a low bucket");
+
+ // MemoryDistribution's Accumulate method to takes
+ // a platform specific type (size_t).
+ // Glean's, however, is i64, and, glean_memory_dist is uint64_t
+ // What happens when we give accumulate dubious values?
+ // This may occur on some uncommon platforms.
+ // Note: there are issues in JS with numbers above 2**53
+ Glean.testOnlyIpc.aMemoryDist.accumulate(36893488147419103232);
+ let dubiousValue = Object.entries(
+ Glean.testOnlyIpc.aMemoryDist.testGetValue().values
+ )[0][1];
+ Assert.equal(
+ dubiousValue,
+ 1,
+ "Greater than 64-Byte number did not accumulate correctly"
+ );
+
+ // Values lower than the out-of-range value are not clamped
+ // resulting in an exception being thrown from the glean side
+ // when the value exceeds the glean maximum allowed value
+ Glean.testOnlyIpc.aMemoryDist.accumulate(Math.pow(2, 31));
+ Assert.throws(
+ () => Glean.testOnlyIpc.aMemoryDist.testGetValue(),
+ /DataError/,
+ "Did not accumulate correctly"
+ );
+});
+
+add_task(function test_gifft_custom_dist() {
+ Glean.testOnlyIpc.aCustomDist.accumulateSamples([7, 268435458]);
+
+ let data = Glean.testOnlyIpc.aCustomDist.testGetValue();
+ Assert.equal(7 + 268435458, data.sum, "Sum's correct");
+ for (let [bucket, count] of Object.entries(data.values)) {
+ Assert.ok(
+ count == 0 || (count == 1 && (bucket == 1 || bucket == 268435456)),
+ `Only two buckets have a sample ${bucket} ${count}`
+ );
+ }
+
+ data = Telemetry.getHistogramById(
+ "TELEMETRY_TEST_MIRROR_FOR_CUSTOM"
+ ).snapshot();
+ Telemetry.getHistogramById("TELEMETRY_TEST_MIRROR_FOR_CUSTOM").clear();
+ Assert.equal(7 + 268435458, data.sum, "Sum in histogram is correct");
+ Assert.equal(1, data.values["1"], "One sample in the low bucket");
+ // Yes, the bucket is off-by-one compared to Glean.
+ Assert.equal(1, data.values["268435457"], "One sample in the next bucket");
+});
+
+add_task(async function test_gifft_timing_dist() {
+ let t1 = Glean.testOnlyIpc.aTimingDist.start();
+ // Interleave some other metric's samples. bug 1768636.
+ let ot1 = Glean.testOnly.whatTimeIsIt.start();
+ let t2 = Glean.testOnlyIpc.aTimingDist.start();
+ let ot2 = Glean.testOnly.whatTimeIsIt.start();
+ Glean.testOnly.whatTimeIsIt.cancel(ot1);
+ Glean.testOnly.whatTimeIsIt.cancel(ot2);
+
+ await sleep(5);
+
+ let t3 = Glean.testOnlyIpc.aTimingDist.start();
+ Glean.testOnlyIpc.aTimingDist.cancel(t1);
+
+ await sleep(5);
+
+ Glean.testOnlyIpc.aTimingDist.stopAndAccumulate(t2); // 10ms
+ Glean.testOnlyIpc.aTimingDist.stopAndAccumulate(t3); // 5ms
+
+ let data = Glean.testOnlyIpc.aTimingDist.testGetValue();
+ const NANOS_IN_MILLIS = 1e6;
+ // bug 1701949 - Sleep gets close, but sometimes doesn't wait long enough.
+ const EPSILON = 40000;
+
+ // Variance in timing makes getting the sum impossible to know.
+ // 10 and 5 input value can be trunacted to 4. + 9. >= 13. from cast
+ Assert.greater(data.sum, 13 * NANOS_IN_MILLIS - EPSILON);
+
+ // No guarantees from timers means no guarantees on buckets.
+ // But we can guarantee it's only two samples.
+ Assert.equal(
+ 2,
+ Object.entries(data.values).reduce(
+ (acc, [bucket, count]) => acc + count,
+ 0
+ ),
+ "Only two buckets with samples"
+ );
+
+ data = Telemetry.getHistogramById("TELEMETRY_TEST_EXPONENTIAL").snapshot();
+ // Suffers from same cast truncation issue of 9.... and 4.... values
+ Assert.greaterOrEqual(data.sum, 13, "Histogram's in milliseconds");
+ Assert.equal(
+ 2,
+ Object.entries(data.values).reduce(
+ (acc, [bucket, count]) => acc + count,
+ 0
+ ),
+ "Only two samples"
+ );
+});
+
+add_task(function test_gifft_string_list_works() {
+ const value = "a string!";
+ const value2 = "another string!";
+ const value3 = "yet another string.";
+
+ // `set` doesn't work in the mirror, so use `add`
+ Glean.testOnlyIpc.aStringList.add(value);
+ Glean.testOnlyIpc.aStringList.add(value2);
+ Glean.testOnlyIpc.aStringList.add(value3);
+
+ let val = Glean.testOnlyIpc.aStringList.testGetValue();
+ // Note: This is incredibly fragile and will break if we ever rearrange items
+ // in the string list.
+ Assert.deepEqual([value, value2, value3], val);
+
+ val = keyedScalarValue("telemetry.test.keyed_boolean_kind");
+ // This too may be fragile.
+ Assert.deepEqual(
+ {
+ [value]: true,
+ [value2]: true,
+ [value3]: true,
+ },
+ val
+ );
+});
+
+add_task(function test_gifft_events() {
+ Telemetry.setEventRecordingEnabled("telemetry.test", true);
+
+ Glean.testOnlyIpc.noExtraEvent.record();
+ var events = Glean.testOnlyIpc.noExtraEvent.testGetValue();
+ Assert.equal(1, events.length);
+ Assert.equal("test_only.ipc", events[0].category);
+ Assert.equal("no_extra_event", events[0].name);
+
+ let extra = { extra1: "can set extras", extra2: "passing more data" };
+ Glean.testOnlyIpc.anEvent.record(extra);
+ events = Glean.testOnlyIpc.anEvent.testGetValue();
+ Assert.equal(1, events.length);
+ Assert.equal("test_only.ipc", events[0].category);
+ Assert.equal("an_event", events[0].name);
+ Assert.deepEqual(extra, events[0].extra);
+
+ TelemetryTestUtils.assertEvents(
+ [
+ ["telemetry.test", "not_expired_optout", "object1", undefined, undefined],
+ ["telemetry.test", "mirror_with_extra", "object1", null, extra],
+ ],
+ { category: "telemetry.test" }
+ );
+});
+
+add_task(function test_gifft_uuid() {
+ const kTestUuid = "decafdec-afde-cafd-ecaf-decafdecafde";
+ Glean.testOnlyIpc.aUuid.set(kTestUuid);
+ Assert.equal(kTestUuid, Glean.testOnlyIpc.aUuid.testGetValue());
+ Assert.equal(kTestUuid, scalarValue("telemetry.test.string_kind"));
+});
+
+add_task(function test_gifft_labeled_counter() {
+ Assert.equal(
+ undefined,
+ Glean.testOnlyIpc.aLabeledCounter.a_label.testGetValue(),
+ "New labels with no values should return undefined"
+ );
+ Glean.testOnlyIpc.aLabeledCounter.a_label.add(1);
+ Glean.testOnlyIpc.aLabeledCounter.another_label.add(2);
+ Glean.testOnlyIpc.aLabeledCounter.a_label.add(3);
+ Assert.equal(4, Glean.testOnlyIpc.aLabeledCounter.a_label.testGetValue());
+ Assert.equal(
+ 2,
+ Glean.testOnlyIpc.aLabeledCounter.another_label.testGetValue()
+ );
+ // What about invalid/__other__?
+ Assert.equal(
+ undefined,
+ Glean.testOnlyIpc.aLabeledCounter.__other__.testGetValue()
+ );
+ Glean.testOnlyIpc.aLabeledCounter["1".repeat(72)].add(3);
+ Assert.throws(
+ () => Glean.testOnlyIpc.aLabeledCounter.__other__.testGetValue(),
+ /DataError/,
+ "Can't get the value when you're error'd"
+ );
+
+ let value = keyedScalarValue(
+ "telemetry.test.another_mirror_for_labeled_counter"
+ );
+ Assert.deepEqual(
+ {
+ a_label: 4,
+ another_label: 2,
+ ["1".repeat(72)]: 3,
+ },
+ value
+ );
+});
+
+add_task(async function test_gifft_timespan() {
+ // We start, briefly sleep and then stop.
+ // That guarantees some time to measure.
+ Glean.testOnly.mirrorTime.start();
+ await sleep(10);
+ Glean.testOnly.mirrorTime.stop();
+
+ const NANOS_IN_MILLIS = 1e6;
+ // bug 1701949 - Sleep gets close, but sometimes doesn't wait long enough.
+ const EPSILON = 40000;
+ Assert.greater(
+ Glean.testOnly.mirrorTime.testGetValue(),
+ 10 * NANOS_IN_MILLIS - EPSILON
+ );
+ // Mirrored to milliseconds.
+ Assert.greaterOrEqual(scalarValue("telemetry.test.mirror_for_timespan"), 9);
+});
+
+add_task(async function test_gifft_timespan_raw() {
+ Glean.testOnly.mirrorTimeNanos.setRaw(15 /*ns*/);
+
+ Assert.equal(15, Glean.testOnly.mirrorTimeNanos.testGetValue());
+ // setRaw, unlike start/stop, mirrors the raw value directly.
+ Assert.equal(scalarValue("telemetry.test.mirror_for_timespan_nanos"), 15);
+});
+
+add_task(async function test_gifft_labeled_boolean() {
+ Assert.equal(
+ undefined,
+ Glean.testOnly.mirrorsForLabeledBools.a_label.testGetValue(),
+ "New labels with no values should return undefined"
+ );
+ Glean.testOnly.mirrorsForLabeledBools.a_label.set(true);
+ Glean.testOnly.mirrorsForLabeledBools.another_label.set(false);
+ Assert.equal(
+ true,
+ Glean.testOnly.mirrorsForLabeledBools.a_label.testGetValue()
+ );
+ Assert.equal(
+ false,
+ Glean.testOnly.mirrorsForLabeledBools.another_label.testGetValue()
+ );
+ // What about invalid/__other__?
+ Assert.equal(
+ undefined,
+ Glean.testOnly.mirrorsForLabeledBools.__other__.testGetValue()
+ );
+ Glean.testOnly.mirrorsForLabeledBools["1".repeat(72)].set(true);
+ Assert.throws(
+ () => Glean.testOnly.mirrorsForLabeledBools.__other__.testGetValue(),
+ /DataError/,
+ "Should throw because of a recording error."
+ );
+
+ // In Telemetry there is no invalid label
+ let value = keyedScalarValue("telemetry.test.mirror_for_labeled_bool");
+ Assert.deepEqual(
+ {
+ a_label: true,
+ another_label: false,
+ ["1".repeat(72)]: true,
+ },
+ value
+ );
+});
+
+add_task(function test_gifft_boolean() {
+ Glean.testOnly.meaningOfLife.set(42);
+ Assert.equal(42, Glean.testOnly.meaningOfLife.testGetValue());
+ Assert.equal(42, scalarValue("telemetry.test.mirror_for_quantity"));
+});
+
+add_task(function test_gifft_rate() {
+ Glean.testOnlyIpc.irate.addToNumerator(22);
+ Glean.testOnlyIpc.irate.addToDenominator(7);
+ Assert.deepEqual(
+ { numerator: 22, denominator: 7 },
+ Glean.testOnlyIpc.irate.testGetValue()
+ );
+ Assert.deepEqual(
+ { numerator: 22, denominator: 7 },
+ keyedScalarValue("telemetry.test.mirror_for_rate")
+ );
+});
+
+add_task(function test_gifft_numeric_limits() {
+ // Glean and Telemetry don't share the same storage sizes or signedness.
+ // Check the edges.
+
+ // 0) Reset everything
+ Services.fog.testResetFOG();
+ Services.telemetry.getSnapshotForHistograms("main", true /* aClearStore */);
+ Services.telemetry.getSnapshotForScalars("main", true /* aClearStore */);
+ Services.telemetry.getSnapshotForKeyedScalars("main", true /* aClearStore */);
+
+ // 1) Counter: i32 (saturates), mirrored to uint Scalar: u32 (overflows)
+ // 1.1) Negative parameters refused.
+ Glean.testOnlyIpc.aCounter.add(-20);
+ // Unfortunately we can't check what the error was, due to API design.
+ // (chutten blames chutten for his shortsightedness)
+ Assert.throws(
+ () => Glean.testOnlyIpc.aCounter.testGetValue(),
+ /DataError/,
+ "Can't get the value when you're error'd"
+ );
+ Assert.equal(undefined, scalarValue("telemetry.test.mirror_for_counter"));
+ // Clear the error state
+ Services.fog.testResetFOG();
+
+ // 1.2) Values that sum larger than u32::max saturate (counter) and overflow (Scalar)
+ // Sums to 2^32 + 1
+ Glean.testOnlyIpc.aCounter.add(Math.pow(2, 31) - 1);
+ Glean.testOnlyIpc.aCounter.add(1);
+ Glean.testOnlyIpc.aCounter.add(Math.pow(2, 31) - 1);
+ Glean.testOnlyIpc.aCounter.add(2);
+ // Glean doesn't actually throw on saturation (bug 1751469),
+ // so we can just check the saturation value.
+ Assert.equal(Math.pow(2, 31) - 1, Glean.testOnlyIpc.aCounter.testGetValue());
+ // Telemetry will have wrapped around to 1
+ Assert.equal(1, scalarValue("telemetry.test.mirror_for_counter"));
+
+ // 2) Quantity: i64 (saturates), mirrored to uint Scalar: u32 (overflows)
+ // 2.1) Negative parameters refused.
+ Glean.testOnly.meaningOfLife.set(-42);
+ // Glean will error on this.
+ Assert.throws(
+ () => Glean.testOnly.meaningOfLife.testGetValue(),
+ /DataError/,
+ "Can't get the value when you're error'd"
+ );
+ // GIFFT doesn't tell Telemetry about the weird value at all.
+ Assert.equal(undefined, scalarValue("telemetry.test.mirror_for_quantity"));
+ // Clear the error state
+ Services.fog.testResetFOG();
+
+ // 2.2) A parameter larger than u32::max is passed to Glean unchanged,
+ // but is clamped to u32::max before being passed to Telemetry.
+ Glean.testOnly.meaningOfLife.set(Math.pow(2, 32));
+ Assert.equal(Math.pow(2, 32), Glean.testOnly.meaningOfLife.testGetValue());
+ Assert.equal(
+ Math.pow(2, 32) - 1,
+ scalarValue("telemetry.test.mirror_for_quantity")
+ );
+
+ // 3) Rate: two i32 (saturates), mirrored to keyed uint Scalar: u32s (overflow)
+ // 3.1) Negative parameters refused.
+ Glean.testOnlyIpc.irate.addToNumerator(-22);
+ Glean.testOnlyIpc.irate.addToDenominator(7);
+ Assert.throws(
+ () => Glean.testOnlyIpc.irate.testGetValue(),
+ /DataError/,
+ "Can't get the value when you're error'd"
+ );
+ Assert.deepEqual(
+ { denominator: 7 },
+ keyedScalarValue("telemetry.test.mirror_for_rate")
+ );
+ // Clear the error state
+ Services.fog.testResetFOG();
+ // Clear the partial Telemetry value
+ Services.telemetry.getSnapshotForKeyedScalars("main", true /* aClearStore */);
+
+ // Now the denominator:
+ Glean.testOnlyIpc.irate.addToNumerator(22);
+ Glean.testOnlyIpc.irate.addToDenominator(-7);
+ Assert.throws(
+ () => Glean.testOnlyIpc.irate.testGetValue(),
+ /DataError/,
+ "Can't get the value when you're error'd"
+ );
+ Assert.deepEqual(
+ { numerator: 22 },
+ keyedScalarValue("telemetry.test.mirror_for_rate")
+ );
+
+ // 4) Timespan
+ // ( Can't overflow time without finding a way to get TimeStamp to think
+ // we're 2^32 milliseconds later without waiting a month )
+
+ // 5) TimingDistribution
+ // ( Can't overflow time with start() and stopAndAccumulate() without
+ // waiting for ages. But we _do_ have a test-only raw API...)
+ // The max sample for timing_distribution is 600000000000.
+ // The type for timing_distribution samples is i64.
+ // This means when we explore the edges of GIFFT's limits, we're well past
+ // Glean's limits. All we can get out of Glean is errors.
+ // (Which is good for data, difficult for tests.)
+ // But GIFFT should properly saturate in Telemetry at i32::max,
+ // so we shall test that.
+ Glean.testOnlyIpc.aTimingDist.testAccumulateRawMillis(Math.pow(2, 31) + 1);
+ Glean.testOnlyIpc.aTimingDist.testAccumulateRawMillis(Math.pow(2, 32) + 1);
+ Assert.throws(
+ () => Glean.testOnlyIpc.aTimingDist.testGetValue(),
+ /DataError/,
+ "Can't get the value when you're error'd"
+ );
+ let snapshot = Telemetry.getHistogramById(
+ "TELEMETRY_TEST_EXPONENTIAL"
+ ).snapshot();
+ Assert.equal(
+ snapshot.values["2147483646"],
+ 2,
+ "samples > i32::max should end up in the top bucket"
+ );
+});
+
+add_task(function test_gifft_url() {
+ const value = "https://www.example.com";
+ Glean.testOnlyIpc.aUrl.set(value);
+
+ Assert.equal(value, Glean.testOnlyIpc.aUrl.testGetValue());
+ Assert.equal(value, scalarValue("telemetry.test.mirror_for_url"));
+});
+
+add_task(function test_gifft_url_cropped() {
+ const value = `https://example.com${"/test".repeat(47)}`;
+ Glean.testOnlyIpc.aUrl.set(value);
+
+ Assert.equal(value, Glean.testOnlyIpc.aUrl.testGetValue());
+ // We expect the mirrored URL to be truncated at the maximum
+ // length supported by string scalars.
+ Assert.equal(
+ value.substring(0, 50),
+ scalarValue("telemetry.test.mirror_for_url")
+ );
+});
diff --git a/toolkit/components/glean/tests/xpcshell/test_GIFFTIPC.js b/toolkit/components/glean/tests/xpcshell/test_GIFFTIPC.js
new file mode 100644
index 0000000000..a318d57b9c
--- /dev/null
+++ b/toolkit/components/glean/tests/xpcshell/test_GIFFTIPC.js
@@ -0,0 +1,315 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+const { ContentTaskUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/ContentTaskUtils.sys.mjs"
+);
+const { setTimeout } = ChromeUtils.importESModule(
+ "resource://gre/modules/Timer.sys.mjs"
+);
+const Telemetry = Services.telemetry;
+const { TelemetryTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TelemetryTestUtils.sys.mjs"
+);
+
+function sleep(ms) {
+ /* eslint-disable mozilla/no-arbitrary-setTimeout */
+ return new Promise(resolve => setTimeout(resolve, ms));
+}
+
+function scalarValue(aScalarName, aProcessName) {
+ let snapshot = Telemetry.getSnapshotForScalars();
+ return aProcessName in snapshot
+ ? snapshot[aProcessName][aScalarName]
+ : undefined;
+}
+
+function keyedScalarValue(aScalarName, aProcessName) {
+ let snapshot = Telemetry.getSnapshotForKeyedScalars();
+ return aProcessName in snapshot
+ ? snapshot[aProcessName][aScalarName]
+ : undefined;
+}
+
+add_setup({ skip_if: () => !runningInParent }, function test_setup() {
+ // Give FOG a temp profile to init within.
+ do_get_profile();
+
+ // Allows these tests to properly run on e.g. Thunderbird
+ Services.prefs.setBoolPref(
+ "toolkit.telemetry.testing.overrideProductsCheck",
+ true
+ );
+
+ // We need to initialize it once, otherwise operations will be stuck in the pre-init queue.
+ // on Android FOG is set up through head.js
+ if (AppConstants.platform != "android") {
+ Services.fog.initializeFOG();
+ }
+});
+
+const COUNT = 42;
+const CHEESY_STRING = "a very cheesy string!";
+const CHEESIER_STRING = "a much cheesier string!";
+const CUSTOM_SAMPLES = [3, 4];
+const EVENT_EXTRA = { extra1: "so very extra" };
+const MEMORIES = [13, 31];
+const MEMORY_BUCKETS = ["13193", "31378"]; // buckets are strings : |
+const A_LABEL_COUNT = 3;
+const ANOTHER_LABEL_COUNT = 5;
+const INVALID_COUNTERS = 7;
+const IRATE_NUMERATOR = 44;
+const IRATE_DENOMINATOR = 14;
+
+add_task({ skip_if: () => runningInParent }, async function run_child_stuff() {
+ let oldCanRecordBase = Telemetry.canRecordBase;
+ Telemetry.canRecordBase = true; // Ensure we're able to record things.
+
+ Glean.testOnlyIpc.aCounter.add(COUNT);
+ Glean.testOnlyIpc.aStringList.add(CHEESY_STRING);
+ Glean.testOnlyIpc.aStringList.add(CHEESIER_STRING);
+
+ Glean.testOnlyIpc.noExtraEvent.record();
+ Glean.testOnlyIpc.anEvent.record(EVENT_EXTRA);
+
+ for (let memory of MEMORIES) {
+ Glean.testOnlyIpc.aMemoryDist.accumulate(memory);
+ }
+
+ let t1 = Glean.testOnlyIpc.aTimingDist.start();
+ let t2 = Glean.testOnlyIpc.aTimingDist.start();
+
+ await sleep(5);
+
+ let t3 = Glean.testOnlyIpc.aTimingDist.start();
+ Glean.testOnlyIpc.aTimingDist.cancel(t1);
+
+ await sleep(5);
+
+ Glean.testOnlyIpc.aTimingDist.stopAndAccumulate(t2); // 10ms
+ Glean.testOnlyIpc.aTimingDist.stopAndAccumulate(t3); // 5ms
+
+ Glean.testOnlyIpc.aCustomDist.accumulateSamples(CUSTOM_SAMPLES);
+
+ Glean.testOnlyIpc.aLabeledCounter.a_label.add(A_LABEL_COUNT);
+ Glean.testOnlyIpc.aLabeledCounter.another_label.add(ANOTHER_LABEL_COUNT);
+
+ // Has to be different from aLabeledCounter so the error we record doesn't
+ // get in the way.
+ Glean.testOnlyIpc.anotherLabeledCounter["1".repeat(72)].add(INVALID_COUNTERS);
+
+ Glean.testOnlyIpc.irate.addToNumerator(IRATE_NUMERATOR);
+ Glean.testOnlyIpc.irate.addToDenominator(IRATE_DENOMINATOR);
+ Telemetry.canRecordBase = oldCanRecordBase;
+});
+
+add_task(
+ { skip_if: () => !runningInParent },
+ async function test_child_metrics() {
+ Telemetry.setEventRecordingEnabled("telemetry.test", true);
+
+ // Clear any stray Telemetry data
+ Telemetry.clearScalars();
+ Telemetry.getSnapshotForHistograms("main", true);
+ Telemetry.clearEvents();
+
+ await run_test_in_child("test_GIFFTIPC.js");
+
+ // Wait for both IPC mechanisms to flush.
+ await Services.fog.testFlushAllChildren();
+ await ContentTaskUtils.waitForCondition(() => {
+ let snapshot = Telemetry.getSnapshotForKeyedScalars();
+ return (
+ "content" in snapshot &&
+ "telemetry.test.mirror_for_rate" in snapshot.content
+ );
+ }, "failed to find content telemetry in parent");
+
+ // boolean
+ // Doesn't work over IPC
+
+ // counter
+ Assert.equal(Glean.testOnlyIpc.aCounter.testGetValue(), COUNT);
+ Assert.equal(
+ scalarValue("telemetry.test.mirror_for_counter", "content"),
+ COUNT,
+ "content-process Scalar has expected count"
+ );
+
+ // custom_distribution
+ const customSampleSum = CUSTOM_SAMPLES.reduce((acc, a) => acc + a, 0);
+ const customData = Glean.testOnlyIpc.aCustomDist.testGetValue("store1");
+ Assert.equal(customSampleSum, customData.sum, "Sum's correct");
+ for (let [bucket, count] of Object.entries(customData.values)) {
+ Assert.ok(
+ count == 0 || (count == CUSTOM_SAMPLES.length && bucket == 1), // both values in the low bucket
+ `Only two buckets have a sample ${bucket} ${count}`
+ );
+ }
+ const histSnapshot = Telemetry.getSnapshotForHistograms(
+ "main",
+ false,
+ false
+ );
+ const histData = histSnapshot.content.TELEMETRY_TEST_MIRROR_FOR_CUSTOM;
+ Assert.equal(customSampleSum, histData.sum, "Sum in histogram's correct");
+ Assert.equal(2, histData.values["1"], "Two samples in the first bucket");
+
+ // datetime
+ // Doesn't work over IPC
+
+ // event
+ var events = Glean.testOnlyIpc.noExtraEvent.testGetValue();
+ Assert.equal(1, events.length);
+ Assert.equal("test_only.ipc", events[0].category);
+ Assert.equal("no_extra_event", events[0].name);
+
+ events = Glean.testOnlyIpc.anEvent.testGetValue();
+ Assert.equal(1, events.length);
+ Assert.equal("test_only.ipc", events[0].category);
+ Assert.equal("an_event", events[0].name);
+ Assert.deepEqual(EVENT_EXTRA, events[0].extra);
+
+ TelemetryTestUtils.assertEvents(
+ [
+ [
+ "telemetry.test",
+ "not_expired_optout",
+ "object1",
+ undefined,
+ undefined,
+ ],
+ ["telemetry.test", "mirror_with_extra", "object1", null, EVENT_EXTRA],
+ ],
+ { category: "telemetry.test" },
+ { process: "content" }
+ );
+
+ // labeled_boolean
+ // Doesn't work over IPC
+
+ // labeled_counter
+ const counters = Glean.testOnlyIpc.aLabeledCounter;
+ Assert.equal(counters.a_label.testGetValue(), A_LABEL_COUNT);
+ Assert.equal(counters.another_label.testGetValue(), ANOTHER_LABEL_COUNT);
+
+ Assert.throws(
+ () => Glean.testOnlyIpc.anotherLabeledCounter.__other__.testGetValue(),
+ /DataError/,
+ "Invalid labels record errors, which throw"
+ );
+
+ let value = keyedScalarValue(
+ "telemetry.test.another_mirror_for_labeled_counter",
+ "content"
+ );
+ Assert.deepEqual(
+ {
+ a_label: A_LABEL_COUNT,
+ another_label: ANOTHER_LABEL_COUNT,
+ },
+ value
+ );
+ value = keyedScalarValue(
+ "telemetry.test.mirror_for_labeled_counter",
+ "content"
+ );
+ Assert.deepEqual(
+ {
+ ["1".repeat(72)]: INVALID_COUNTERS,
+ },
+ value
+ );
+
+ // labeled_string
+ // Doesn't work over IPC
+
+ // memory_distribution
+ const memoryData = Glean.testOnlyIpc.aMemoryDist.testGetValue();
+ const memorySum = MEMORIES.reduce((acc, a) => acc + a, 0);
+ // The sum's in bytes, but the metric's in KB
+ Assert.equal(memorySum * 1024, memoryData.sum);
+ for (let [bucket, count] of Object.entries(memoryData.values)) {
+ // We could assert instead, but let's skip to save the logspam.
+ if (count == 0) {
+ continue;
+ }
+ Assert.ok(count == 1 && MEMORY_BUCKETS.includes(bucket));
+ }
+
+ const memoryHist = histSnapshot.content.TELEMETRY_TEST_LINEAR;
+ Assert.equal(
+ memorySum,
+ memoryHist.sum,
+ "Histogram's in `memory_unit` units"
+ );
+ Assert.equal(2, memoryHist.values["1"], "Samples are in the right bucket");
+
+ // quantity
+ // Doesn't work over IPC
+
+ // rate
+ Assert.deepEqual(
+ { numerator: IRATE_NUMERATOR, denominator: IRATE_DENOMINATOR },
+ Glean.testOnlyIpc.irate.testGetValue()
+ );
+ Assert.deepEqual(
+ { numerator: IRATE_NUMERATOR, denominator: IRATE_DENOMINATOR },
+ keyedScalarValue("telemetry.test.mirror_for_rate", "content")
+ );
+
+ // string
+ // Doesn't work over IPC
+
+ // string_list
+ // Note: this will break if string list ever rearranges its items.
+ const cheesyStrings = Glean.testOnlyIpc.aStringList.testGetValue();
+ Assert.deepEqual(cheesyStrings, [CHEESY_STRING, CHEESIER_STRING]);
+ // Note: this will break if keyed scalars rearrange their items.
+ Assert.deepEqual(
+ {
+ [CHEESY_STRING]: true,
+ [CHEESIER_STRING]: true,
+ },
+ keyedScalarValue("telemetry.test.keyed_boolean_kind", "content")
+ );
+
+ // timespan
+ // Doesn't work over IPC
+
+ // timing_distribution
+ const NANOS_IN_MILLIS = 1e6;
+ const EPSILON = 40000; // bug 1701949
+ const times = Glean.testOnlyIpc.aTimingDist.testGetValue();
+ Assert.greater(times.sum, 15 * NANOS_IN_MILLIS - EPSILON);
+ // We can't guarantee any specific time values (thank you clocks),
+ // but we can assert there are only two samples.
+ Assert.equal(
+ 2,
+ Object.entries(times.values).reduce(
+ (acc, [bucket, count]) => acc + count,
+ 0
+ )
+ );
+ const timingHist = histSnapshot.content.TELEMETRY_TEST_EXPONENTIAL;
+ Assert.greaterOrEqual(timingHist.sum, 13, "Histogram's in milliseconds.");
+ // Both values, 10 and 5, are truncated by a cast in AccumulateTimeDelta
+ // Minimally downcast 9. + 4. could realistically result in 13.
+ Assert.equal(
+ 2,
+ Object.entries(timingHist.values).reduce(
+ (acc, [bucket, count]) => acc + count,
+ 0
+ ),
+ "Only two samples"
+ );
+
+ // uuid
+ // Doesn't work over IPC
+ }
+);
diff --git a/toolkit/components/glean/tests/xpcshell/test_Glean.js b/toolkit/components/glean/tests/xpcshell/test_Glean.js
new file mode 100644
index 0000000000..8375c22a3e
--- /dev/null
+++ b/toolkit/components/glean/tests/xpcshell/test_Glean.js
@@ -0,0 +1,458 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+const { setTimeout } = ChromeUtils.importESModule(
+ "resource://gre/modules/Timer.sys.mjs"
+);
+
+function sleep(ms) {
+ /* eslint-disable mozilla/no-arbitrary-setTimeout */
+ return new Promise(resolve => setTimeout(resolve, ms));
+}
+
+add_setup(
+ /* on Android FOG is set up through head.js */
+ { skip_if: () => AppConstants.platform == "android" },
+ function test_setup() {
+ // FOG needs a profile directory to put its data in.
+ do_get_profile();
+
+ // We need to initialize it once, otherwise operations will be stuck in the pre-init queue.
+ Services.fog.initializeFOG();
+ }
+);
+
+add_task(function test_fog_counter_works() {
+ Glean.testOnly.badCode.add(31);
+ Assert.equal(31, Glean.testOnly.badCode.testGetValue("test-ping"));
+});
+
+add_task(async function test_fog_string_works() {
+ Assert.equal(null, Glean.testOnly.cheesyString.testGetValue());
+
+ // Setting `undefined` will be ignored.
+ Glean.testOnly.cheesyString.set(undefined);
+ Assert.equal(null, Glean.testOnly.cheesyString.testGetValue());
+
+ const value = "a cheesy string!";
+ Glean.testOnly.cheesyString.set(value);
+
+ Assert.equal(value, Glean.testOnly.cheesyString.testGetValue("test-ping"));
+});
+
+add_task(async function test_fog_string_list_works() {
+ const value = "a cheesy string!";
+ const value2 = "a cheesier string!";
+ const value3 = "the cheeziest of strings.";
+
+ const cheeseList = [value, value2];
+ Glean.testOnly.cheesyStringList.set(cheeseList);
+
+ let val = Glean.testOnly.cheesyStringList.testGetValue();
+ // Note: This is incredibly fragile and will break if we ever rearrange items
+ // in the string list.
+ Assert.deepEqual(cheeseList, val);
+
+ Glean.testOnly.cheesyStringList.add(value3);
+ Assert.ok(Glean.testOnly.cheesyStringList.testGetValue().includes(value3));
+});
+
+add_task(async function test_fog_timespan_works() {
+ Glean.testOnly.canWeTimeIt.start();
+ Glean.testOnly.canWeTimeIt.cancel();
+ Assert.equal(undefined, Glean.testOnly.canWeTimeIt.testGetValue());
+
+ // We start, briefly sleep and then stop.
+ // That guarantees some time to measure.
+ Glean.testOnly.canWeTimeIt.start();
+ await sleep(10);
+ Glean.testOnly.canWeTimeIt.stop();
+
+ Assert.ok(Glean.testOnly.canWeTimeIt.testGetValue("test-ping") > 0);
+});
+
+add_task(async function test_fog_timespan_throws_on_stop_wout_start() {
+ Glean.testOnly.canWeTimeIt.stop();
+ Assert.throws(
+ () => Glean.testOnly.canWeTimeIt.testGetValue(),
+ /DataError/,
+ "Should throw because stop was called without start."
+ );
+});
+
+add_task(async function test_fog_uuid_works() {
+ const kTestUuid = "decafdec-afde-cafd-ecaf-decafdecafde";
+ Glean.testOnly.whatIdIt.set(kTestUuid);
+ Assert.equal(kTestUuid, Glean.testOnly.whatIdIt.testGetValue("test-ping"));
+
+ Glean.testOnly.whatIdIt.generateAndSet();
+ // Since we generate v4 UUIDs, and the first character of the third group
+ // isn't 4, this won't ever collide with kTestUuid.
+ Assert.notEqual(kTestUuid, Glean.testOnly.whatIdIt.testGetValue("test-ping"));
+});
+
+add_task(function test_fog_datetime_works() {
+ const value = new Date("2020-06-11T12:00:00");
+
+ Glean.testOnly.whatADate.set(value.getTime() * 1000);
+
+ const received = Glean.testOnly.whatADate.testGetValue("test-ping");
+ Assert.equal(received.getTime(), value.getTime());
+});
+
+add_task(function test_fog_boolean_works() {
+ Glean.testOnly.canWeFlagIt.set(false);
+ Assert.equal(false, Glean.testOnly.canWeFlagIt.testGetValue("test-ping"));
+ // While you're here, might as well test that the ping name's optional.
+ Assert.equal(false, Glean.testOnly.canWeFlagIt.testGetValue());
+});
+
+add_task(async function test_fog_event_works() {
+ Glean.testOnlyIpc.noExtraEvent.record();
+ var events = Glean.testOnlyIpc.noExtraEvent.testGetValue();
+ Assert.equal(1, events.length);
+ Assert.equal("test_only.ipc", events[0].category);
+ Assert.equal("no_extra_event", events[0].name);
+
+ let extra = { extra1: "can set extras", extra2: "passing more data" };
+ Glean.testOnlyIpc.anEvent.record(extra);
+ events = Glean.testOnlyIpc.anEvent.testGetValue();
+ Assert.equal(1, events.length);
+ Assert.equal("test_only.ipc", events[0].category);
+ Assert.equal("an_event", events[0].name);
+ Assert.deepEqual(extra, events[0].extra);
+
+ // Corner case: Event with extra with `undefined` value.
+ // Should pretend that extra key isn't there.
+ extra = { extra1: undefined, extra2: "defined" };
+ Glean.testOnlyIpc.anEvent.record(extra);
+ events = Glean.testOnlyIpc.anEvent.testGetValue();
+ Assert.equal(2, events.length);
+ Assert.deepEqual({ extra2: "defined" }, events[1].extra);
+
+ let extra2 = {
+ extra1: "can set extras",
+ extra2: 37,
+ extra3_longer_name: false,
+ };
+ Glean.testOnlyIpc.eventWithExtra.record(extra2);
+ events = Glean.testOnlyIpc.eventWithExtra.testGetValue();
+ Assert.equal(1, events.length);
+ Assert.equal("test_only.ipc", events[0].category);
+ Assert.equal("event_with_extra", events[0].name);
+ let expectedExtra = {
+ extra1: "can set extras",
+ extra2: "37",
+ extra3_longer_name: "false",
+ };
+ Assert.deepEqual(expectedExtra, events[0].extra);
+
+ // camelCase extras work.
+ let extra5 = {
+ extra3LongerName: false,
+ };
+ Glean.testOnlyIpc.eventWithExtra.record(extra5);
+ events = Glean.testOnlyIpc.eventWithExtra.testGetValue();
+ Assert.equal(2, events.length, "Recorded one event too many.");
+ expectedExtra = {
+ extra3_longer_name: "false",
+ };
+ Assert.deepEqual(expectedExtra, events[1].extra);
+
+ // Invalid extra keys don't crash, the event is not recorded,
+ // but an error is recorded.
+ let extra3 = {
+ extra1_nonexistent_extra: "this does not crash",
+ };
+ Glean.testOnlyIpc.eventWithExtra.record(extra3);
+ Assert.throws(
+ () => Glean.testOnlyIpc.eventWithExtra.testGetValue(),
+ /DataError/,
+ "Should throw because of a recording error."
+ );
+});
+
+add_task(async function test_fog_memory_distribution_works() {
+ Glean.testOnly.doYouRemember.accumulate(7);
+ Glean.testOnly.doYouRemember.accumulate(17);
+
+ let data = Glean.testOnly.doYouRemember.testGetValue("test-ping");
+ // `data.sum` is in bytes, but the metric is in MB.
+ Assert.equal(24 * 1024 * 1024, data.sum, "Sum's correct");
+ for (let [bucket, count] of Object.entries(data.values)) {
+ Assert.ok(
+ count == 0 || (count == 1 && (bucket == 17520006 || bucket == 7053950)),
+ "Only two buckets have a sample"
+ );
+ }
+});
+
+add_task(async function test_fog_custom_distribution_works() {
+ Glean.testOnlyIpc.aCustomDist.accumulateSamples([7, 268435458]);
+
+ let data = Glean.testOnlyIpc.aCustomDist.testGetValue("store1");
+ Assert.equal(7 + 268435458, data.sum, "Sum's correct");
+ for (let [bucket, count] of Object.entries(data.values)) {
+ Assert.ok(
+ count == 0 || (count == 1 && (bucket == 1 || bucket == 268435456)),
+ `Only two buckets have a sample ${bucket} ${count}`
+ );
+ }
+
+ // Negative values will not be recorded, instead an error is recorded.
+ Glean.testOnlyIpc.aCustomDist.accumulateSamples([-7]);
+ Assert.throws(
+ () => Glean.testOnlyIpc.aCustomDist.testGetValue(),
+ /DataError/
+ );
+});
+
+add_task(function test_fog_custom_pings() {
+ Assert.ok("onePingOnly" in GleanPings);
+ let submitted = false;
+ Glean.testOnly.onePingOneBool.set(false);
+ GleanPings.onePingOnly.testBeforeNextSubmit(reason => {
+ submitted = true;
+ Assert.equal(false, Glean.testOnly.onePingOneBool.testGetValue());
+ });
+ GleanPings.onePingOnly.submit();
+ Assert.ok(submitted, "Ping was submitted, callback was called.");
+});
+
+add_task(function test_recursive_testBeforeNextSubmit() {
+ Assert.ok("onePingOnly" in GleanPings);
+ let submitted = 0;
+ let rec = reason => {
+ submitted++;
+ GleanPings.onePingOnly.testBeforeNextSubmit(rec);
+ };
+ GleanPings.onePingOnly.testBeforeNextSubmit(rec);
+ GleanPings.onePingOnly.submit();
+ GleanPings.onePingOnly.submit();
+ GleanPings.onePingOnly.submit();
+ Assert.equal(3, submitted, "Ping was submitted 3 times");
+ // Be kind and remove the callback.
+ GleanPings.onePingOnly.testBeforeNextSubmit(() => {});
+});
+
+add_task(async function test_fog_timing_distribution_works() {
+ let t1 = Glean.testOnly.whatTimeIsIt.start();
+ let t2 = Glean.testOnly.whatTimeIsIt.start();
+
+ await sleep(5);
+
+ let t3 = Glean.testOnly.whatTimeIsIt.start();
+ Glean.testOnly.whatTimeIsIt.cancel(t1);
+
+ await sleep(5);
+
+ Glean.testOnly.whatTimeIsIt.stopAndAccumulate(t2); // 10ms
+ Glean.testOnly.whatTimeIsIt.stopAndAccumulate(t3); // 5ms
+
+ let data = Glean.testOnly.whatTimeIsIt.testGetValue();
+ const NANOS_IN_MILLIS = 1e6;
+ // bug 1701949 - Sleep gets close, but sometimes doesn't wait long enough.
+ const EPSILON = 40000;
+
+ // Variance in timing makes getting the sum impossible to know.
+ Assert.greater(data.sum, 15 * NANOS_IN_MILLIS - EPSILON);
+
+ // No guarantees from timers means no guarantees on buckets.
+ // But we can guarantee it's only two samples.
+ Assert.equal(
+ 2,
+ Object.entries(data.values).reduce(
+ (acc, [bucket, count]) => acc + count,
+ 0
+ ),
+ "Only two buckets with samples"
+ );
+});
+
+add_task(async function test_fog_labels_conform() {
+ Glean.testOnly.mabelsLabelMaker.singleword.set("portmanteau");
+ Assert.equal(
+ "portmanteau",
+ Glean.testOnly.mabelsLabelMaker.singleword.testGetValue()
+ );
+ Glean.testOnly.mabelsLabelMaker.snake_case.set("snek");
+ Assert.equal(
+ "snek",
+ Glean.testOnly.mabelsLabelMaker.snake_case.testGetValue()
+ );
+ Glean.testOnly.mabelsLabelMaker["dash-character"].set("Dash Rendar");
+ Assert.equal(
+ "Dash Rendar",
+ Glean.testOnly.mabelsLabelMaker["dash-character"].testGetValue()
+ );
+ Glean.testOnly.mabelsLabelMaker["dot.separated"].set("dot product");
+ Assert.equal(
+ "dot product",
+ Glean.testOnly.mabelsLabelMaker["dot.separated"].testGetValue()
+ );
+ Glean.testOnly.mabelsLabelMaker.camelCase.set("wednesday");
+ Assert.equal(
+ "wednesday",
+ Glean.testOnly.mabelsLabelMaker.camelCase.testGetValue()
+ );
+ const veryLong = "1".repeat(72);
+ Glean.testOnly.mabelsLabelMaker[veryLong].set("seventy-two");
+ Assert.throws(
+ () => Glean.testOnly.mabelsLabelMaker[veryLong].testGetValue(),
+ /DataError/,
+ "Should throw because of an invalid label."
+ );
+ // This test should _now_ throw because we are calling data after an invalid
+ // label has been set.
+ Assert.throws(
+ () => Glean.testOnly.mabelsLabelMaker["dot.separated"].testGetValue(),
+ /DataError/,
+ "Should throw because of an invalid label."
+ );
+});
+
+add_task(async function test_fog_labeled_boolean_works() {
+ Assert.equal(
+ undefined,
+ Glean.testOnly.mabelsLikeBalloons.at_parties.testGetValue(),
+ "New labels with no values should return undefined"
+ );
+ Glean.testOnly.mabelsLikeBalloons.at_parties.set(true);
+ Glean.testOnly.mabelsLikeBalloons.at_funerals.set(false);
+ Assert.equal(
+ true,
+ Glean.testOnly.mabelsLikeBalloons.at_parties.testGetValue()
+ );
+ Assert.equal(
+ false,
+ Glean.testOnly.mabelsLikeBalloons.at_funerals.testGetValue()
+ );
+ // What about invalid/__other__?
+ Assert.equal(
+ undefined,
+ Glean.testOnly.mabelsLikeBalloons.__other__.testGetValue()
+ );
+ Glean.testOnly.mabelsLikeBalloons["1".repeat(72)].set(true);
+ Assert.throws(
+ () => Glean.testOnly.mabelsLikeBalloons.__other__.testGetValue(),
+ /DataError/,
+ "Should throw because of a recording error."
+ );
+});
+
+add_task(async function test_fog_labeled_counter_works() {
+ Assert.equal(
+ undefined,
+ Glean.testOnly.mabelsKitchenCounters.near_the_sink.testGetValue(),
+ "New labels with no values should return undefined"
+ );
+ Glean.testOnly.mabelsKitchenCounters.near_the_sink.add(1);
+ Glean.testOnly.mabelsKitchenCounters.with_junk_on_them.add(2);
+ Assert.equal(
+ 1,
+ Glean.testOnly.mabelsKitchenCounters.near_the_sink.testGetValue()
+ );
+ Assert.equal(
+ 2,
+ Glean.testOnly.mabelsKitchenCounters.with_junk_on_them.testGetValue()
+ );
+ // What about invalid/__other__?
+ Assert.equal(
+ undefined,
+ Glean.testOnly.mabelsKitchenCounters.__other__.testGetValue()
+ );
+ Glean.testOnly.mabelsKitchenCounters["1".repeat(72)].add(1);
+ Assert.throws(
+ () => Glean.testOnly.mabelsKitchenCounters.__other__.testGetValue(),
+ /DataError/,
+ "Should throw because of a recording error."
+ );
+});
+
+add_task(async function test_fog_labeled_string_works() {
+ Assert.equal(
+ undefined,
+ Glean.testOnly.mabelsBalloonStrings.colour_of_99.testGetValue(),
+ "New labels with no values should return undefined"
+ );
+ Glean.testOnly.mabelsBalloonStrings.colour_of_99.set("crimson");
+ Glean.testOnly.mabelsBalloonStrings.string_lengths.set("various");
+ Assert.equal(
+ "crimson",
+ Glean.testOnly.mabelsBalloonStrings.colour_of_99.testGetValue()
+ );
+ Assert.equal(
+ "various",
+ Glean.testOnly.mabelsBalloonStrings.string_lengths.testGetValue()
+ );
+ // What about invalid/__other__?
+ Assert.equal(
+ undefined,
+ Glean.testOnly.mabelsBalloonStrings.__other__.testGetValue()
+ );
+ Glean.testOnly.mabelsBalloonStrings["1".repeat(72)].set("valid");
+ Assert.throws(
+ () => Glean.testOnly.mabelsBalloonStrings.__other__.testGetValue(),
+ /DataError/
+ );
+});
+
+add_task(function test_fog_quantity_works() {
+ Glean.testOnly.meaningOfLife.set(42);
+ Assert.equal(42, Glean.testOnly.meaningOfLife.testGetValue());
+});
+
+add_task(function test_fog_rate_works() {
+ // 1) Standard rate with internal denominator
+ Glean.testOnlyIpc.irate.addToNumerator(22);
+ Glean.testOnlyIpc.irate.addToDenominator(7);
+ Assert.deepEqual(
+ { numerator: 22, denominator: 7 },
+ Glean.testOnlyIpc.irate.testGetValue()
+ );
+
+ // 2) Rate with external denominator
+ Glean.testOnlyIpc.anExternalDenominator.add(11);
+ Glean.testOnlyIpc.rateWithExternalDenominator.addToNumerator(121);
+ Assert.equal(11, Glean.testOnlyIpc.anExternalDenominator.testGetValue());
+ Assert.deepEqual(
+ { numerator: 121, denominator: 11 },
+ Glean.testOnlyIpc.rateWithExternalDenominator.testGetValue()
+ );
+});
+
+add_task(async function test_fog_url_works() {
+ const value = "https://www.example.com/fog";
+ Glean.testOnlyIpc.aUrl.set(value);
+
+ Assert.equal(value, Glean.testOnlyIpc.aUrl.testGetValue("store1"));
+});
+
+add_task(async function test_fog_text_works() {
+ const value =
+ "Before the risin' sun, we fly, So many roads to choose, We'll start out walkin' and learn to run, (We've only just begun)";
+ Glean.testOnlyIpc.aText.set(value);
+
+ let rslt = Glean.testOnlyIpc.aText.testGetValue();
+
+ Assert.equal(value, rslt);
+
+ Assert.equal(121, rslt.length);
+});
+
+add_task(async function test_fog_text_works_unusual_character() {
+ const value =
+ "The secret to Dominique Ansel's viennoiserie is the use of Isigny Sainte-Mère butter and Les Grands Moulins de Paris flour";
+ Glean.testOnlyIpc.aText.set(value);
+
+ let rslt = Glean.testOnlyIpc.aText.testGetValue();
+
+ Assert.equal(value, rslt);
+
+ Assert.greater(rslt.length, 100);
+});
diff --git a/toolkit/components/glean/tests/xpcshell/test_GleanExperiments.js b/toolkit/components/glean/tests/xpcshell/test_GleanExperiments.js
new file mode 100644
index 0000000000..cf8871e9d1
--- /dev/null
+++ b/toolkit/components/glean/tests/xpcshell/test_GleanExperiments.js
@@ -0,0 +1,28 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// FOG needs a profile directory to put its data in.
+do_get_profile();
+
+// We need to initialize it once, otherwise operations will be stuck in the pre-init queue.
+Services.fog.initializeFOG();
+
+add_task(function test_fog_experiment_annotations() {
+ const id = "my-experiment-id";
+ const branch = "my-branch";
+ const extra = { extra_key: "extra_value" };
+ Services.fog.setExperimentActive(id, branch, extra);
+
+ let data = Services.fog.testGetExperimentData(id);
+ Assert.equal(data.branch, branch);
+ Assert.deepEqual(data.extra, extra);
+
+ // Unknown id gets nothing.
+ Assert.equal(undefined, Services.fog.testGetExperimentData(id + id));
+
+ // Inactive id gets nothing.
+ Services.fog.setExperimentInactive(id);
+ Assert.equal(undefined, Services.fog.testGetExperimentData(id));
+});
diff --git a/toolkit/components/glean/tests/xpcshell/test_GleanIPC.js b/toolkit/components/glean/tests/xpcshell/test_GleanIPC.js
new file mode 100644
index 0000000000..3d665c23b9
--- /dev/null
+++ b/toolkit/components/glean/tests/xpcshell/test_GleanIPC.js
@@ -0,0 +1,157 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+const { setTimeout } = ChromeUtils.importESModule(
+ "resource://gre/modules/Timer.sys.mjs"
+);
+
+function sleep(ms) {
+ /* eslint-disable mozilla/no-arbitrary-setTimeout */
+ return new Promise(resolve => setTimeout(resolve, ms));
+}
+
+add_setup(
+ /* on Android FOG is set up through head.js */
+ { skip_if: () => !runningInParent || AppConstants.platform == "android" },
+ function test_setup() {
+ // Give FOG a temp profile to init within.
+ do_get_profile();
+
+ // We need to initialize it once, otherwise operations will be stuck in the pre-init queue.
+ Services.fog.initializeFOG();
+ }
+);
+
+const BAD_CODE_COUNT = 42;
+const CHEESY_STRING = "a very cheesy string!";
+const CHEESIER_STRING = "a much cheesier string!";
+const EVENT_EXTRA = { extra1: "so very extra" };
+const MEMORIES = [13, 31];
+const MEMORY_BUCKETS = ["13509772", "32131834"]; // buckets are strings : |
+const COUNTERS_NEAR_THE_SINK = 3;
+const COUNTERS_WITH_JUNK_ON_THEM = 5;
+const INVALID_COUNTERS = 7;
+
+add_task({ skip_if: () => runningInParent }, async function run_child_stuff() {
+ Glean.testOnly.badCode.add(BAD_CODE_COUNT);
+ Glean.testOnly.cheesyStringList.add(CHEESY_STRING);
+ Glean.testOnly.cheesyStringList.add(CHEESIER_STRING);
+
+ Glean.testOnlyIpc.noExtraEvent.record();
+ Glean.testOnlyIpc.anEvent.record(EVENT_EXTRA);
+
+ for (let memory of MEMORIES) {
+ Glean.testOnly.doYouRemember.accumulate(memory);
+ }
+
+ let t1 = Glean.testOnly.whatTimeIsIt.start();
+ let t2 = Glean.testOnly.whatTimeIsIt.start();
+
+ await sleep(5);
+
+ let t3 = Glean.testOnly.whatTimeIsIt.start();
+ Glean.testOnly.whatTimeIsIt.cancel(t1);
+
+ await sleep(5);
+
+ Glean.testOnly.whatTimeIsIt.stopAndAccumulate(t2); // 10ms
+ Glean.testOnly.whatTimeIsIt.stopAndAccumulate(t3); // 5ms
+
+ Glean.testOnlyIpc.aCustomDist.accumulateSamples([3, 4]);
+
+ Glean.testOnly.mabelsKitchenCounters.near_the_sink.add(
+ COUNTERS_NEAR_THE_SINK
+ );
+ Glean.testOnly.mabelsKitchenCounters.with_junk_on_them.add(
+ COUNTERS_WITH_JUNK_ON_THEM
+ );
+
+ Glean.testOnly.mabelsBathroomCounters["1".repeat(72)].add(INVALID_COUNTERS);
+
+ Glean.testOnlyIpc.irate.addToNumerator(44);
+ Glean.testOnlyIpc.irate.addToDenominator(14);
+});
+
+add_task(
+ { skip_if: () => !runningInParent },
+ async function test_child_metrics() {
+ await run_test_in_child("test_GleanIPC.js");
+ await Services.fog.testFlushAllChildren();
+
+ Assert.equal(Glean.testOnly.badCode.testGetValue(), BAD_CODE_COUNT);
+
+ // Note: this will break if string list ever rearranges its items.
+ const cheesyStrings = Glean.testOnly.cheesyStringList.testGetValue();
+ Assert.deepEqual(cheesyStrings, [CHEESY_STRING, CHEESIER_STRING]);
+
+ const data = Glean.testOnly.doYouRemember.testGetValue();
+ Assert.equal(MEMORIES.reduce((a, b) => a + b, 0) * 1024 * 1024, data.sum);
+ for (let [bucket, count] of Object.entries(data.values)) {
+ // We could assert instead, but let's skip to save the logspam.
+ if (count == 0) {
+ continue;
+ }
+ Assert.ok(count == 1 && MEMORY_BUCKETS.includes(bucket));
+ }
+
+ const customData = Glean.testOnlyIpc.aCustomDist.testGetValue("store1");
+ Assert.equal(3 + 4, customData.sum, "Sum's correct");
+ for (let [bucket, count] of Object.entries(customData.values)) {
+ Assert.ok(
+ count == 0 || (count == 2 && bucket == 1), // both values in the low bucket
+ `Only two buckets have a sample ${bucket} ${count}`
+ );
+ }
+
+ var events = Glean.testOnlyIpc.noExtraEvent.testGetValue();
+ Assert.equal(1, events.length);
+ Assert.equal("test_only.ipc", events[0].category);
+ Assert.equal("no_extra_event", events[0].name);
+
+ events = Glean.testOnlyIpc.anEvent.testGetValue();
+ Assert.equal(1, events.length);
+ Assert.equal("test_only.ipc", events[0].category);
+ Assert.equal("an_event", events[0].name);
+ Assert.deepEqual(EVENT_EXTRA, events[0].extra);
+
+ const NANOS_IN_MILLIS = 1e6;
+ const EPSILON = 40000; // bug 1701949
+ const times = Glean.testOnly.whatTimeIsIt.testGetValue();
+ Assert.greater(times.sum, 15 * NANOS_IN_MILLIS - EPSILON);
+ // We can't guarantee any specific time values (thank you clocks),
+ // but we can assert there are only two samples.
+ Assert.equal(
+ 2,
+ Object.entries(times.values).reduce(
+ (acc, [bucket, count]) => acc + count,
+ 0
+ )
+ );
+
+ const mabelsCounters = Glean.testOnly.mabelsKitchenCounters;
+ Assert.equal(
+ mabelsCounters.near_the_sink.testGetValue(),
+ COUNTERS_NEAR_THE_SINK
+ );
+ Assert.equal(
+ mabelsCounters.with_junk_on_them.testGetValue(),
+ COUNTERS_WITH_JUNK_ON_THEM
+ );
+
+ Assert.throws(
+ () => Glean.testOnly.mabelsBathroomCounters.__other__.testGetValue(),
+ /DataError/,
+ "Invalid labels record errors, which throw"
+ );
+
+ Assert.deepEqual(
+ { numerator: 44, denominator: 14 },
+ Glean.testOnlyIpc.irate.testGetValue()
+ );
+ }
+);
diff --git a/toolkit/components/glean/tests/xpcshell/test_GleanServerKnobs.js b/toolkit/components/glean/tests/xpcshell/test_GleanServerKnobs.js
new file mode 100644
index 0000000000..31ed262798
--- /dev/null
+++ b/toolkit/components/glean/tests/xpcshell/test_GleanServerKnobs.js
@@ -0,0 +1,164 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_setup(function test_setup() {
+ // Give FOG a temp profile to init within.
+ do_get_profile();
+
+ // We need to initialize it once, otherwise operations will be stuck in
+ // the pre-init queue.
+ Services.fog.initializeFOG();
+});
+
+add_task(function test_fog_metrics_disabled_remotely() {
+ // Set a cheesy string in the test metric. This should record because the
+ // metric has `disabled: false` by default.
+ const str1 = "a cheesy string!";
+ Glean.testOnly.cheesyString.set(str1);
+ Assert.equal(str1, Glean.testOnly.cheesyString.testGetValue("test-ping"));
+
+ // Create and set a feature configuration that disables the test metric.
+ const feature_config = {
+ "test_only.cheesy_string": false,
+ };
+ Services.fog.setMetricsFeatureConfig(JSON.stringify(feature_config));
+
+ // Attempt to set another cheesy string in the test metric. This should not
+ // record because of the override to the metric's default value in the
+ // feature configuration.
+ const str2 = "another cheesy string!";
+ Glean.testOnly.cheesyString.set(str2);
+ Assert.equal(str1, Glean.testOnly.cheesyString.testGetValue("test-ping"));
+
+ // Reset everything so it doesn't interfere with other tests.
+ Services.fog.testResetFOG();
+});
+
+add_task(function test_fog_multiple_metrics_disabled_remotely() {
+ // Set some test metrics. This should record because the metrics are
+ // `disabled: false` by default.
+ const str1 = "yet another a cheesy string!";
+ Glean.testOnly.cheesyString.set(str1);
+ Assert.equal(str1, Glean.testOnly.cheesyString.testGetValue("test-ping"));
+ const qty1 = 42;
+ Glean.testOnly.meaningOfLife.set(qty1);
+ Assert.equal(qty1, Glean.testOnly.meaningOfLife.testGetValue("test-ping"));
+
+ // Create and set a feature configuration that disables multiple test
+ // metrics.
+ var feature_config = {
+ "test_only.cheesy_string": false,
+ "test_only.meaning_of_life": false,
+ };
+ Services.fog.setMetricsFeatureConfig(JSON.stringify(feature_config));
+
+ // Attempt to set the metrics again. This should not record because of the
+ // override to the metrics' default value in the feature configuration.
+ const str2 = "another cheesy string v2!";
+ Glean.testOnly.cheesyString.set(str2);
+ Assert.equal(str1, Glean.testOnly.cheesyString.testGetValue("test-ping"));
+ const qty2 = 52;
+ Glean.testOnly.meaningOfLife.set(qty2);
+ Assert.equal(qty1, Glean.testOnly.meaningOfLife.testGetValue("test-ping"));
+
+ // Change the feature configuration to re-enable the `cheesy_string` metric.
+ feature_config = {
+ "test_only.cheesy_string": true,
+ "test_only.meaning_of_life": false,
+ };
+ Services.fog.setMetricsFeatureConfig(JSON.stringify(feature_config));
+
+ // Attempt to set the metrics again. This should only record `cheesy_string`
+ // because of the most recent feature configuration.
+ const str3 = "another cheesy string v3!";
+ Glean.testOnly.cheesyString.set(str3);
+ Assert.equal(str3, Glean.testOnly.cheesyString.testGetValue("test-ping"));
+ const qty3 = 62;
+ Glean.testOnly.meaningOfLife.set(qty3);
+ Assert.equal(qty1, Glean.testOnly.meaningOfLife.testGetValue("test-ping"));
+
+ // Reset everything so it doesn't interfere with other tests.
+ Services.fog.testResetFOG();
+
+ // Set some final metrics. This should record in both metrics because they
+ // are both `disabled: false` by default.
+ const str4 = "another a cheesy string v4";
+ Glean.testOnly.cheesyString.set(str4);
+ Assert.equal(str4, Glean.testOnly.cheesyString.testGetValue("test-ping"));
+ const qty4 = 72;
+ Glean.testOnly.meaningOfLife.set(qty4);
+ Assert.equal(qty4, Glean.testOnly.meaningOfLife.testGetValue("test-ping"));
+});
+
+add_task(function test_fog_metrics_feature_config_api_handles_null_values() {
+ // Set a cheesy string in the test metric. This should record because the
+ // metric has `disabled: false` by default.
+ const str1 = "a cheesy string!";
+ Glean.testOnly.cheesyString.set(str1);
+ Assert.equal(str1, Glean.testOnly.cheesyString.testGetValue("test-ping"));
+
+ // Create and set a feature configuration that disables the test metric.
+ const feature_config = {
+ "test_only.cheesy_string": false,
+ };
+ Services.fog.setMetricsFeatureConfig(JSON.stringify(feature_config));
+
+ // Attempt to set another cheesy string in the test metric. This should not
+ // record because of the override to the metric's default value in the
+ // feature configuration.
+ const str2 = "another cheesy string v2";
+ Glean.testOnly.cheesyString.set(str2);
+ Assert.equal(str1, Glean.testOnly.cheesyString.testGetValue("test-ping"));
+
+ // Set the configuration to `null`.
+ Services.fog.setMetricsFeatureConfig(null);
+
+ // Attempt to set another cheesy string in the test metric. This should now
+ // record because `null` doesn't change already existing configuration.
+ Glean.testOnly.cheesyString.set(str2);
+ Assert.equal(str1, Glean.testOnly.cheesyString.testGetValue("test-ping"));
+
+ // Set the configuration to `""` to replicate getting an empty string from
+ // Nimbus.
+ Services.fog.setMetricsFeatureConfig("");
+
+ // Attempt to set another cheesy string in the test metric. This should now
+ // record again because `""` doesn't change already existing configuration.
+ const str3 = "another cheesy string v3";
+ Glean.testOnly.cheesyString.set(str3);
+ Assert.equal(str1, Glean.testOnly.cheesyString.testGetValue("test-ping"));
+});
+
+add_task(function test_fog_metrics_disabled_reset_fog_behavior() {
+ // Set a cheesy string in the test metric. This should record because the
+ // metric has `disabled: false` by default.
+ const str1 = "a cheesy string!";
+ Glean.testOnly.cheesyString.set(str1);
+ Assert.equal(str1, Glean.testOnly.cheesyString.testGetValue("test-ping"));
+
+ // Create and set a feature configuration that disables the test metric.
+ const feature_config = {
+ "test_only.cheesy_string": false,
+ };
+ Services.fog.setMetricsFeatureConfig(JSON.stringify(feature_config));
+
+ // Attempt to set another cheesy string in the test metric. This should not
+ // record because of the override to the metric's default value in the
+ // feature configuration.
+ const str2 = "another cheesy string!";
+ Glean.testOnly.cheesyString.set(str2);
+ Assert.equal(str1, Glean.testOnly.cheesyString.testGetValue("test-ping"));
+
+ // Now reset FOG to ensure that the feature configuration is also reset.
+ Services.fog.testResetFOG();
+
+ // Attempt to set the string again in the test metric. This should now
+ // record normally because we reset FOG.
+ Glean.testOnly.cheesyString.set(str2);
+ Assert.equal(str2, Glean.testOnly.cheesyString.testGetValue("test-ping"));
+
+ // Reset everything so it doesn't interfere with other tests.
+ Services.fog.testResetFOG();
+});
diff --git a/toolkit/components/glean/tests/xpcshell/test_JOG.js b/toolkit/components/glean/tests/xpcshell/test_JOG.js
new file mode 100644
index 0000000000..53b1d25962
--- /dev/null
+++ b/toolkit/components/glean/tests/xpcshell/test_JOG.js
@@ -0,0 +1,728 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+const { setTimeout } = ChromeUtils.importESModule(
+ "resource://gre/modules/Timer.sys.mjs"
+);
+
+function sleep(ms) {
+ /* eslint-disable mozilla/no-arbitrary-setTimeout */
+ return new Promise(resolve => setTimeout(resolve, ms));
+}
+
+add_task(
+ /* on Android FOG is set up through head.js */
+ { skip_if: () => AppConstants.platform == "android" },
+ function test_setup() {
+ // FOG needs a profile directory to put its data in.
+ do_get_profile();
+
+ // We need to initialize it once, otherwise operations will be stuck in the pre-init queue.
+ Services.fog.initializeFOG();
+ }
+);
+
+add_task(function test_jog_counter_works() {
+ Services.fog.testRegisterRuntimeMetric(
+ "counter",
+ "jog_cat",
+ "jog_counter",
+ ["test-only"],
+ `"ping"`,
+ false
+ );
+ Glean.jogCat.jogCounter.add(53);
+ Assert.equal(53, Glean.jogCat.jogCounter.testGetValue());
+});
+
+add_task(async function test_jog_string_works() {
+ const value = "an active string!";
+ Services.fog.testRegisterRuntimeMetric(
+ "string",
+ "jog_cat",
+ "jog_string",
+ ["test-only"],
+ `"ping"`,
+ false
+ );
+ Glean.jogCat.jogString.set(value);
+
+ Assert.equal(value, Glean.jogCat.jogString.testGetValue());
+});
+
+add_task(async function test_jog_string_list_works() {
+ const value = "an active string!";
+ const value2 = "a more active string!";
+ const value3 = "the most active of strings.";
+ Services.fog.testRegisterRuntimeMetric(
+ "string_list",
+ "jog_cat",
+ "jog_string_list",
+ ["test-only"],
+ `"ping"`,
+ false
+ );
+
+ const jogList = [value, value2];
+ Glean.jogCat.jogStringList.set(jogList);
+
+ let val = Glean.jogCat.jogStringList.testGetValue();
+ // Note: This is incredibly fragile and will break if we ever rearrange items
+ // in the string list.
+ Assert.deepEqual(jogList, val);
+
+ Glean.jogCat.jogStringList.add(value3);
+ Assert.ok(Glean.jogCat.jogStringList.testGetValue().includes(value3));
+});
+
+add_task(async function test_jog_timespan_works() {
+ Services.fog.testRegisterRuntimeMetric(
+ "timespan",
+ "jog_cat",
+ "jog_timespan",
+ ["test-only"],
+ `"ping"`,
+ false,
+ JSON.stringify({ time_unit: "millisecond" })
+ );
+ Glean.jogCat.jogTimespan.start();
+ Glean.jogCat.jogTimespan.cancel();
+ Assert.equal(undefined, Glean.jogCat.jogTimespan.testGetValue());
+
+ // We start, briefly sleep and then stop.
+ // That guarantees some time to measure.
+ Glean.jogCat.jogTimespan.start();
+ await sleep(10);
+ Glean.jogCat.jogTimespan.stop();
+
+ Assert.ok(Glean.jogCat.jogTimespan.testGetValue() > 0);
+});
+
+add_task(async function test_jog_uuid_works() {
+ const kTestUuid = "decafdec-afde-cafd-ecaf-decafdecafde";
+ Services.fog.testRegisterRuntimeMetric(
+ "uuid",
+ "jog_cat",
+ "jog_uuid",
+ ["test-only"],
+ `"ping"`,
+ false
+ );
+ Glean.jogCat.jogUuid.set(kTestUuid);
+ Assert.equal(kTestUuid, Glean.jogCat.jogUuid.testGetValue());
+
+ Glean.jogCat.jogUuid.generateAndSet();
+ // Since we generate v4 UUIDs, and the first character of the third group
+ // isn't 4, this won't ever collide with kTestUuid.
+ Assert.notEqual(kTestUuid, Glean.jogCat.jogUuid.testGetValue());
+});
+
+add_task(function test_jog_datetime_works() {
+ const value = new Date("2020-06-11T12:00:00");
+ Services.fog.testRegisterRuntimeMetric(
+ "datetime",
+ "jog_cat",
+ "jog_datetime",
+ ["test-only"],
+ `"ping"`,
+ false,
+ JSON.stringify({ time_unit: "nanosecond" })
+ );
+
+ Glean.jogCat.jogDatetime.set(value.getTime() * 1000);
+
+ const received = Glean.jogCat.jogDatetime.testGetValue();
+ Assert.equal(received.getTime(), value.getTime());
+});
+
+add_task(function test_jog_boolean_works() {
+ Services.fog.testRegisterRuntimeMetric(
+ "boolean",
+ "jog_cat",
+ "jog_bool",
+ ["test-only"],
+ `"ping"`,
+ false
+ );
+ Glean.jogCat.jogBool.set(false);
+ Assert.equal(false, Glean.jogCat.jogBool.testGetValue());
+});
+
+add_task(async function test_jog_event_works() {
+ Services.fog.testRegisterRuntimeMetric(
+ "event",
+ "jog_cat",
+ "jog_event_no_extra",
+ ["test-only"],
+ `"ping"`,
+ false
+ );
+ Glean.jogCat.jogEventNoExtra.record();
+ var events = Glean.jogCat.jogEventNoExtra.testGetValue();
+ Assert.equal(1, events.length);
+ Assert.equal("jog_cat", events[0].category);
+ Assert.equal("jog_event_no_extra", events[0].name);
+
+ Services.fog.testRegisterRuntimeMetric(
+ "event",
+ "jog_cat",
+ "jog_event",
+ ["test-only"],
+ `"ping"`,
+ false,
+ JSON.stringify({ allowed_extra_keys: ["extra1", "extra2"] })
+ );
+ let extra = { extra1: "can set extras", extra2: "passing more data" };
+ Glean.jogCat.jogEvent.record(extra);
+ events = Glean.jogCat.jogEvent.testGetValue();
+ Assert.equal(1, events.length);
+ Assert.equal("jog_cat", events[0].category);
+ Assert.equal("jog_event", events[0].name);
+ Assert.deepEqual(extra, events[0].extra);
+
+ Services.fog.testRegisterRuntimeMetric(
+ "event",
+ "jog_cat",
+ "jog_event_with_extra",
+ ["test-only"],
+ `"ping"`,
+ false,
+ JSON.stringify({
+ allowed_extra_keys: ["extra1", "extra2", "extra3_longer_name"],
+ })
+ );
+ let extra2 = {
+ extra1: "can set extras",
+ extra2: 37,
+ extra3_longer_name: false,
+ };
+ Glean.jogCat.jogEventWithExtra.record(extra2);
+ events = Glean.jogCat.jogEventWithExtra.testGetValue();
+ Assert.equal(1, events.length);
+ Assert.equal("jog_cat", events[0].category);
+ Assert.equal("jog_event_with_extra", events[0].name);
+ let expectedExtra = {
+ extra1: "can set extras",
+ extra2: "37",
+ extra3_longer_name: "false",
+ };
+ Assert.deepEqual(expectedExtra, events[0].extra);
+
+ // Invalid extra keys don't crash, the event is not recorded.
+ let extra3 = {
+ extra1_nonexistent_extra: "this does not crash",
+ };
+ Glean.jogCat.jogEventWithExtra.record(extra3);
+ // And test methods throw appropriately
+ Assert.throws(
+ () => Glean.jogCat.jogEventWithExtra.testGetValue(),
+ /DataError/
+ );
+});
+
+add_task(async function test_jog_memory_distribution_works() {
+ Services.fog.testRegisterRuntimeMetric(
+ "memory_distribution",
+ "jog_cat",
+ "jog_memory_dist",
+ ["test-only"],
+ `"ping"`,
+ false,
+ JSON.stringify({ memory_unit: "megabyte" })
+ );
+ Glean.jogCat.jogMemoryDist.accumulate(7);
+ Glean.jogCat.jogMemoryDist.accumulate(17);
+
+ let data = Glean.jogCat.jogMemoryDist.testGetValue();
+ // `data.sum` is in bytes, but the metric is in MB.
+ Assert.equal(24 * 1024 * 1024, data.sum, "Sum's correct");
+ for (let [bucket, count] of Object.entries(data.values)) {
+ Assert.ok(
+ count == 0 || (count == 1 && (bucket == 17520006 || bucket == 7053950)),
+ "Only two buckets have a sample"
+ );
+ }
+});
+
+add_task(async function test_jog_custom_distribution_works() {
+ Services.fog.testRegisterRuntimeMetric(
+ "custom_distribution",
+ "jog_cat",
+ "jog_custom_dist",
+ ["test-only"],
+ `"ping"`,
+ false,
+ JSON.stringify({
+ range_min: 1,
+ range_max: 2147483646,
+ bucket_count: 10,
+ histogram_type: "linear",
+ })
+ );
+ Glean.jogCat.jogCustomDist.accumulateSamples([7, 268435458]);
+
+ let data = Glean.jogCat.jogCustomDist.testGetValue();
+ Assert.equal(7 + 268435458, data.sum, "Sum's correct");
+ for (let [bucket, count] of Object.entries(data.values)) {
+ Assert.ok(
+ count == 0 || (count == 1 && (bucket == 1 || bucket == 268435456)),
+ `Only two buckets have a sample ${bucket} ${count}`
+ );
+ }
+
+ // Negative values will not be recorded, instead an error is recorded.
+ Glean.jogCat.jogCustomDist.accumulateSamples([-7]);
+ Assert.throws(() => Glean.jogCat.jogCustomDist.testGetValue(), /DataError/);
+});
+
+add_task(async function test_jog_custom_pings() {
+ Services.fog.testRegisterRuntimeMetric(
+ "boolean",
+ "jog_cat",
+ "jog_ping_bool",
+ ["jog-ping"],
+ `"ping"`,
+ false
+ );
+ Services.fog.testRegisterRuntimePing("jog-ping", true, true, true, []);
+ Assert.ok("jogPing" in GleanPings);
+ let submitted = false;
+ Glean.jogCat.jogPingBool.set(false);
+ GleanPings.jogPing.testBeforeNextSubmit(reason => {
+ submitted = true;
+ Assert.equal(false, Glean.jogCat.jogPingBool.testGetValue());
+ });
+ GleanPings.jogPing.submit();
+ Assert.ok(submitted, "Ping was submitted, callback was called.");
+ // ping-lifetime value was cleared.
+ Assert.equal(undefined, Glean.jogCat.jogPingBool.testGetValue());
+});
+
+add_task(async function test_jog_timing_distribution_works() {
+ Services.fog.testRegisterRuntimeMetric(
+ "timing_distribution",
+ "jog_cat",
+ "jog_timing_dist",
+ ["test-only"],
+ `"ping"`,
+ false,
+ JSON.stringify({ time_unit: "microsecond" })
+ );
+ let t1 = Glean.jogCat.jogTimingDist.start();
+ let t2 = Glean.jogCat.jogTimingDist.start();
+
+ await sleep(5);
+
+ let t3 = Glean.jogCat.jogTimingDist.start();
+ Glean.jogCat.jogTimingDist.cancel(t1);
+
+ await sleep(5);
+
+ Glean.jogCat.jogTimingDist.stopAndAccumulate(t2); // 10ms
+ Glean.jogCat.jogTimingDist.stopAndAccumulate(t3); // 5ms
+
+ let data = Glean.jogCat.jogTimingDist.testGetValue();
+ const NANOS_IN_MILLIS = 1e6;
+ // bug 1701949 - Sleep gets close, but sometimes doesn't wait long enough.
+ const EPSILON = 40000;
+
+ // Variance in timing makes getting the sum impossible to know.
+ Assert.greater(data.sum, 15 * NANOS_IN_MILLIS - EPSILON);
+
+ // No guarantees from timers means no guarantees on buckets.
+ // But we can guarantee it's only two samples.
+ Assert.equal(
+ 2,
+ Object.entries(data.values).reduce(
+ (acc, [bucket, count]) => acc + count,
+ 0
+ ),
+ "Only two buckets with samples"
+ );
+});
+
+add_task(async function test_jog_labeled_boolean_works() {
+ Services.fog.testRegisterRuntimeMetric(
+ "labeled_boolean",
+ "jog_cat",
+ "jog_labeled_bool",
+ ["test-only"],
+ `"ping"`,
+ false
+ );
+ Assert.equal(
+ undefined,
+ Glean.jogCat.jogLabeledBool.label_1.testGetValue(),
+ "New labels with no values should return undefined"
+ );
+ Glean.jogCat.jogLabeledBool.label_1.set(true);
+ Glean.jogCat.jogLabeledBool.label_2.set(false);
+ Assert.equal(true, Glean.jogCat.jogLabeledBool.label_1.testGetValue());
+ Assert.equal(false, Glean.jogCat.jogLabeledBool.label_2.testGetValue());
+ // What about invalid/__other__?
+ Assert.equal(undefined, Glean.jogCat.jogLabeledBool.__other__.testGetValue());
+ Glean.jogCat.jogLabeledBool.NowValidLabel.set(true);
+ Assert.ok(Glean.jogCat.jogLabeledBool.NowValidLabel.testGetValue());
+ Glean.jogCat.jogLabeledBool["1".repeat(72)].set(true);
+ Assert.throws(
+ () => Glean.jogCat.jogLabeledBool.__other__.testGetValue(),
+ /DataError/,
+ "Should throw because of a recording error."
+ );
+});
+
+add_task(async function test_jog_labeled_boolean_with_static_labels_works() {
+ Services.fog.testRegisterRuntimeMetric(
+ "labeled_boolean",
+ "jog_cat",
+ "jog_labeled_bool_with_labels",
+ ["test-only"],
+ `"ping"`,
+ false,
+ JSON.stringify({ ordered_labels: ["label_1", "label_2"] })
+ );
+ Assert.equal(
+ undefined,
+ Glean.jogCat.jogLabeledBoolWithLabels.label_1.testGetValue(),
+ "New labels with no values should return undefined"
+ );
+ Glean.jogCat.jogLabeledBoolWithLabels.label_1.set(true);
+ Glean.jogCat.jogLabeledBoolWithLabels.label_2.set(false);
+ Assert.equal(
+ true,
+ Glean.jogCat.jogLabeledBoolWithLabels.label_1.testGetValue()
+ );
+ Assert.equal(
+ false,
+ Glean.jogCat.jogLabeledBoolWithLabels.label_2.testGetValue()
+ );
+ // What about invalid/__other__?
+ Assert.equal(
+ undefined,
+ Glean.jogCat.jogLabeledBoolWithLabels.__other__.testGetValue()
+ );
+ Glean.jogCat.jogLabeledBoolWithLabels.label_3.set(true);
+ Assert.equal(
+ true,
+ Glean.jogCat.jogLabeledBoolWithLabels.__other__.testGetValue()
+ );
+ // TODO: Test that we have the right number and type of errors (bug 1683171)
+});
+
+add_task(async function test_jog_labeled_counter_works() {
+ Services.fog.testRegisterRuntimeMetric(
+ "labeled_counter",
+ "jog_cat",
+ "jog_labeled_counter",
+ ["test-only"],
+ `"ping"`,
+ false
+ );
+ Assert.equal(
+ undefined,
+ Glean.jogCat.jogLabeledCounter.label_1.testGetValue(),
+ "New labels with no values should return undefined"
+ );
+ Glean.jogCat.jogLabeledCounter.label_1.add(1);
+ Glean.jogCat.jogLabeledCounter.label_2.add(2);
+ Assert.equal(1, Glean.jogCat.jogLabeledCounter.label_1.testGetValue());
+ Assert.equal(2, Glean.jogCat.jogLabeledCounter.label_2.testGetValue());
+ // What about invalid/__other__?
+ Assert.equal(
+ undefined,
+ Glean.jogCat.jogLabeledCounter.__other__.testGetValue()
+ );
+ Glean.jogCat.jogLabeledCounter["1".repeat(72)].add(1);
+ Assert.throws(
+ () => Glean.jogCat.jogLabeledCounter.__other__.testGetValue(),
+ /DataError/,
+ "Should throw because of a recording error."
+ );
+});
+
+add_task(async function test_jog_labeled_counter_with_static_labels_works() {
+ Services.fog.testRegisterRuntimeMetric(
+ "labeled_counter",
+ "jog_cat",
+ "jog_labeled_counter_with_labels",
+ ["test-only"],
+ `"ping"`,
+ false,
+ JSON.stringify({ ordered_labels: ["label_1", "label_2"] })
+ );
+ Assert.equal(
+ undefined,
+ Glean.jogCat.jogLabeledCounterWithLabels.label_1.testGetValue(),
+ "New labels with no values should return undefined"
+ );
+ Glean.jogCat.jogLabeledCounterWithLabels.label_1.add(1);
+ Glean.jogCat.jogLabeledCounterWithLabels.label_2.add(2);
+ Assert.equal(
+ 1,
+ Glean.jogCat.jogLabeledCounterWithLabels.label_1.testGetValue()
+ );
+ Assert.equal(
+ 2,
+ Glean.jogCat.jogLabeledCounterWithLabels.label_2.testGetValue()
+ );
+ // What about invalid/__other__?
+ Assert.equal(
+ undefined,
+ Glean.jogCat.jogLabeledCounterWithLabels.__other__.testGetValue()
+ );
+ Glean.jogCat.jogLabeledCounterWithLabels["1".repeat(72)].add(1);
+ // TODO:(bug 1766515) - This should throw.
+ /*Assert.throws(
+ () => Glean.jogCat.jogLabeledCounterWithLabels.__other__.testGetValue(),
+ /DataError/,
+ "Should throw because of a recording error."
+ );*/
+ Assert.equal(
+ 1,
+ Glean.jogCat.jogLabeledCounterWithLabels.__other__.testGetValue()
+ );
+});
+
+add_task(async function test_jog_labeled_string_works() {
+ Services.fog.testRegisterRuntimeMetric(
+ "labeled_string",
+ "jog_cat",
+ "jog_labeled_string",
+ ["test-only"],
+ `"ping"`,
+ false
+ );
+ Assert.equal(
+ undefined,
+ Glean.jogCat.jogLabeledString.label_1.testGetValue(),
+ "New labels with no values should return undefined"
+ );
+ Glean.jogCat.jogLabeledString.label_1.set("crimson");
+ Glean.jogCat.jogLabeledString.label_2.set("various");
+ Assert.equal("crimson", Glean.jogCat.jogLabeledString.label_1.testGetValue());
+ Assert.equal("various", Glean.jogCat.jogLabeledString.label_2.testGetValue());
+ // What about invalid/__other__?
+ Assert.equal(
+ undefined,
+ Glean.jogCat.jogLabeledString.__other__.testGetValue()
+ );
+ Glean.jogCat.jogLabeledString["1".repeat(72)].set("valid");
+ Assert.throws(
+ () => Glean.jogCat.jogLabeledString.__other__.testGetValue(),
+ /DataError/
+ );
+});
+
+add_task(async function test_jog_labeled_string_with_labels_works() {
+ Services.fog.testRegisterRuntimeMetric(
+ "labeled_string",
+ "jog_cat",
+ "jog_labeled_string_with_labels",
+ ["test-only"],
+ `"ping"`,
+ false,
+ JSON.stringify({ ordered_labels: ["label_1", "label_2"] })
+ );
+ Assert.equal(
+ undefined,
+ Glean.jogCat.jogLabeledStringWithLabels.label_1.testGetValue(),
+ "New labels with no values should return undefined"
+ );
+ Glean.jogCat.jogLabeledStringWithLabels.label_1.set("crimson");
+ Glean.jogCat.jogLabeledStringWithLabels.label_2.set("various");
+ Assert.equal(
+ "crimson",
+ Glean.jogCat.jogLabeledStringWithLabels.label_1.testGetValue()
+ );
+ Assert.equal(
+ "various",
+ Glean.jogCat.jogLabeledStringWithLabels.label_2.testGetValue()
+ );
+ // What about invalid/__other__?
+ Assert.equal(
+ undefined,
+ Glean.jogCat.jogLabeledStringWithLabels.__other__.testGetValue()
+ );
+ Glean.jogCat.jogLabeledStringWithLabels["1".repeat(72)].set("valid");
+ // TODO:(bug 1766515) - This should throw.
+ /*Assert.throws(
+ () => Glean.jogCat.jogLabeledStringWithLabels.__other__.testGetValue(),
+ /DataError/
+ );*/
+ Assert.equal(
+ "valid",
+ Glean.jogCat.jogLabeledStringWithLabels.__other__.testGetValue()
+ );
+});
+
+add_task(function test_jog_quantity_works() {
+ Services.fog.testRegisterRuntimeMetric(
+ "quantity",
+ "jog_cat",
+ "jog_quantity",
+ ["test-only"],
+ `"ping"`,
+ false
+ );
+ Glean.jogCat.jogQuantity.set(42);
+ Assert.equal(42, Glean.jogCat.jogQuantity.testGetValue());
+});
+
+add_task(function test_jog_rate_works() {
+ Services.fog.testRegisterRuntimeMetric(
+ "rate",
+ "jog_cat",
+ "jog_rate",
+ ["test-only"],
+ `"ping"`,
+ false
+ );
+ // 1) Standard rate with internal denominator
+ Glean.jogCat.jogRate.addToNumerator(22);
+ Glean.jogCat.jogRate.addToDenominator(7);
+ Assert.deepEqual(
+ { numerator: 22, denominator: 7 },
+ Glean.jogCat.jogRate.testGetValue()
+ );
+
+ Services.fog.testRegisterRuntimeMetric(
+ "denominator",
+ "jog_cat",
+ "jog_denominator",
+ ["test-only"],
+ `"ping"`,
+ false,
+ JSON.stringify({
+ numerators: [
+ {
+ name: "jog_rate_ext",
+ category: "jog_cat",
+ send_in_pings: ["test-only"],
+ lifetime: "ping",
+ disabled: false,
+ },
+ ],
+ })
+ );
+ Services.fog.testRegisterRuntimeMetric(
+ "rate",
+ "jog_cat",
+ "jog_rate_ext",
+ ["test-only"],
+ `"ping"`,
+ false
+ );
+ // 2) Rate with external denominator
+ Glean.jogCat.jogDenominator.add(11);
+ Glean.jogCat.jogRateExt.addToNumerator(121);
+ Assert.equal(11, Glean.jogCat.jogDenominator.testGetValue());
+ Assert.deepEqual(
+ { numerator: 121, denominator: 11 },
+ Glean.jogCat.jogRateExt.testGetValue()
+ );
+});
+
+add_task(function test_jog_dotted_categories_work() {
+ Services.fog.testRegisterRuntimeMetric(
+ "counter",
+ "jog_cat.dotted",
+ "jog_counter",
+ ["test-only"],
+ `"ping"`,
+ false
+ );
+ Glean.jogCatDotted.jogCounter.add(314);
+ Assert.equal(314, Glean.jogCatDotted.jogCounter.testGetValue());
+});
+
+add_task(async function test_jog_ping_works() {
+ const kReason = "reason-1";
+ Services.fog.testRegisterRuntimePing("my-ping", true, true, true, [kReason]);
+ let submitted = false;
+ GleanPings.myPing.testBeforeNextSubmit(reason => {
+ submitted = true;
+ Assert.equal(kReason, reason);
+ });
+ GleanPings.myPing.submit("reason-1");
+ Assert.ok(submitted, "Ping must have been submitted");
+});
+
+add_task(function test_jog_name_collision() {
+ Assert.ok("aCounter" in Glean.testOnlyJog);
+ Assert.equal(undefined, Glean.testOnlyJog.aCounter.testGetValue());
+ const kValue = 42;
+ Glean.testOnlyJog.aCounter.add(kValue);
+ Assert.equal(kValue, Glean.testOnlyJog.aCounter.testGetValue());
+
+ // Let's overwrite the test_only.jog.a_counter counter.
+ Services.fog.testRegisterRuntimeMetric(
+ "counter",
+ "test_only.jog",
+ "a_counter",
+ ["store1"],
+ `"ping"`,
+ true // changing the metric to disabled.
+ );
+
+ Assert.ok("aCounter" in Glean.testOnlyJog);
+ Assert.equal(kValue, Glean.testOnlyJog.aCounter.testGetValue());
+ Glean.testOnlyJog.aCounter.add(kValue);
+ Assert.equal(
+ kValue,
+ Glean.testOnlyJog.aCounter.testGetValue(),
+ "value of now-disabled metric remains unchanged."
+ );
+
+ // Now let's mess with events:
+ Assert.ok("anEvent" in Glean.testOnlyJog);
+ Assert.equal(undefined, Glean.testOnlyJog.anEvent.testGetValue());
+ const extra12 = {
+ extra1: "a value",
+ extra2: "another value",
+ };
+ Glean.testOnlyJog.anEvent.record(extra12);
+ Assert.deepEqual(extra12, Glean.testOnlyJog.anEvent.testGetValue()[0].extra);
+ Services.fog.testRegisterRuntimeMetric(
+ "event",
+ "test_only.jog",
+ "an_event",
+ ["store1"],
+ `"ping"`,
+ false,
+ JSON.stringify({ allowed_extra_keys: ["extra1", "extra2", "extra3"] }) // New extra key just dropped
+ );
+ const extra123 = {
+ extra1: "different value",
+ extra2: "another different value",
+ extra3: 42,
+ };
+ Glean.testOnlyJog.anEvent.record(extra123);
+ Assert.deepEqual(extra123, Glean.testOnlyJog.anEvent.testGetValue()[1].extra);
+});
+
+add_task(function test_enumerable_names() {
+ Assert.ok(Object.keys(Glean).includes("testOnlyJog"));
+ Assert.ok(Object.keys(Glean.testOnlyJog).includes("aCounter"));
+ Assert.ok(Object.keys(GleanPings).includes("testPing"));
+});
+
+add_task(async function test_jog_text_works() {
+ const kValue =
+ "In the heart of the Opéra district in Paris, the Cédric Grolet Opéra bakery-pastry shop is a veritable temple of gourmet delights.";
+ Services.fog.testRegisterRuntimeMetric(
+ "text",
+ "test_only.jog",
+ "a_text",
+ ["test-only"],
+ `"ping"`,
+ false
+ );
+ Glean.testOnlyJog.aText.set(kValue);
+
+ Assert.equal(kValue, Glean.testOnlyJog.aText.testGetValue());
+});
diff --git a/toolkit/components/glean/tests/xpcshell/test_JOGIPC.js b/toolkit/components/glean/tests/xpcshell/test_JOGIPC.js
new file mode 100644
index 0000000000..505dba6825
--- /dev/null
+++ b/toolkit/components/glean/tests/xpcshell/test_JOGIPC.js
@@ -0,0 +1,266 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+const { setTimeout } = ChromeUtils.importESModule(
+ "resource://gre/modules/Timer.sys.mjs"
+);
+
+function sleep(ms) {
+ /* eslint-disable mozilla/no-arbitrary-setTimeout */
+ return new Promise(resolve => setTimeout(resolve, ms));
+}
+
+add_task(
+ /* on Android FOG is set up through head.js */
+ { skip_if: () => !runningInParent || AppConstants.platform == "android" },
+ function test_setup() {
+ // Give FOG a temp profile to init within.
+ do_get_profile();
+
+ // We need to initialize it once, otherwise operations will be stuck in the pre-init queue.
+ Services.fog.initializeFOG();
+ }
+);
+
+const COUNT = 42;
+const STRING = "a string!";
+const ANOTHER_STRING = "another string!";
+const EVENT_EXTRA = { extra1: "so very extra" };
+const MEMORIES = [13, 31];
+const MEMORY_BUCKETS = ["13509772", "32131834"]; // buckets are strings : |
+const COUNTERS_1 = 3;
+const COUNTERS_2 = 5;
+const INVALID_COUNTERS = 7;
+
+// It is CRUCIAL that we register metrics in the same order in the parent and
+// in the child or their metric ids will not line up and ALL WILL EXPLODE.
+const METRICS = [
+ ["counter", "jog_ipc", "jog_counter", ["test-only"], `"ping"`, false],
+ ["string_list", "jog_ipc", "jog_string_list", ["test-only"], `"ping"`, false],
+ ["event", "jog_ipc", "jog_event_no_extra", ["test-only"], `"ping"`, false],
+ [
+ "event",
+ "jog_ipc",
+ "jog_event",
+ ["test-only"],
+ `"ping"`,
+ false,
+ JSON.stringify({ allowed_extra_keys: ["extra1"] }),
+ ],
+ [
+ "memory_distribution",
+ "jog_ipc",
+ "jog_memory_dist",
+ ["test-only"],
+ `"ping"`,
+ false,
+ JSON.stringify({ memory_unit: "megabyte" }),
+ ],
+ [
+ "timing_distribution",
+ "jog_ipc",
+ "jog_timing_dist",
+ ["test-only"],
+ `"ping"`,
+ false,
+ JSON.stringify({ time_unit: "nanosecond" }),
+ ],
+ [
+ "custom_distribution",
+ "jog_ipc",
+ "jog_custom_dist",
+ ["test-only"],
+ `"ping"`,
+ false,
+ JSON.stringify({
+ range_min: 1,
+ range_max: 2147483646,
+ bucket_count: 10,
+ histogram_type: "linear",
+ }),
+ ],
+ [
+ "labeled_counter",
+ "jog_ipc",
+ "jog_labeled_counter",
+ ["test-only"],
+ `"ping"`,
+ false,
+ ],
+ [
+ "labeled_counter",
+ "jog_ipc",
+ "jog_labeled_counter_err",
+ ["test-only"],
+ `"ping"`,
+ false,
+ ],
+ [
+ "labeled_counter",
+ "jog_ipc",
+ "jog_labeled_counter_with_labels",
+ ["test-only"],
+ `"ping"`,
+ false,
+ JSON.stringify({ ordered_labels: ["label_1", "label_2"] }),
+ ],
+ [
+ "labeled_counter",
+ "jog_ipc",
+ "jog_labeled_counter_with_labels_err",
+ ["test-only"],
+ `"ping"`,
+ false,
+ JSON.stringify({ ordered_labels: ["label_1", "label_2"] }),
+ ],
+ ["rate", "jog_ipc", "jog_rate", ["test-only"], `"ping"`, false],
+];
+
+add_task({ skip_if: () => runningInParent }, async function run_child_stuff() {
+ // Ensure any _actual_ runtime metrics are registered first.
+ // Otherwise the jog_ipc.* ones will have incorrect ids.
+ Glean.testOnly.badCode;
+ for (let metric of METRICS) {
+ Services.fog.testRegisterRuntimeMetric(...metric);
+ }
+ Glean.jogIpc.jogCounter.add(COUNT);
+ Glean.jogIpc.jogStringList.add(STRING);
+ Glean.jogIpc.jogStringList.add(ANOTHER_STRING);
+
+ Glean.jogIpc.jogEventNoExtra.record();
+ Glean.jogIpc.jogEvent.record(EVENT_EXTRA);
+
+ for (let memory of MEMORIES) {
+ Glean.jogIpc.jogMemoryDist.accumulate(memory);
+ }
+
+ let t1 = Glean.jogIpc.jogTimingDist.start();
+ let t2 = Glean.jogIpc.jogTimingDist.start();
+
+ await sleep(5);
+
+ let t3 = Glean.jogIpc.jogTimingDist.start();
+ Glean.jogIpc.jogTimingDist.cancel(t1);
+
+ await sleep(5);
+
+ Glean.jogIpc.jogTimingDist.stopAndAccumulate(t2); // 10ms
+ Glean.jogIpc.jogTimingDist.stopAndAccumulate(t3); // 5ms
+
+ Glean.jogIpc.jogCustomDist.accumulateSamples([3, 4]);
+
+ Glean.jogIpc.jogLabeledCounter.label_1.add(COUNTERS_1);
+ Glean.jogIpc.jogLabeledCounter.label_2.add(COUNTERS_2);
+
+ Glean.jogIpc.jogLabeledCounterErr["1".repeat(72)].add(INVALID_COUNTERS);
+
+ Glean.jogIpc.jogLabeledCounterWithLabels.label_1.add(COUNTERS_1);
+ Glean.jogIpc.jogLabeledCounterWithLabels.label_2.add(COUNTERS_2);
+
+ Glean.jogIpc.jogLabeledCounterWithLabelsErr["1".repeat(72)].add(
+ INVALID_COUNTERS
+ );
+
+ Glean.jogIpc.jogRate.addToNumerator(44);
+ Glean.jogIpc.jogRate.addToDenominator(14);
+});
+
+add_task(
+ { skip_if: () => !runningInParent },
+ async function test_child_metrics() {
+ // Ensure any _actual_ runtime metrics are registered first.
+ // Otherwise the jog_ipc.* ones will have incorrect ids.
+ Glean.testOnly.badCode;
+ for (let metric of METRICS) {
+ Services.fog.testRegisterRuntimeMetric(...metric);
+ }
+ await run_test_in_child("test_JOGIPC.js");
+ await Services.fog.testFlushAllChildren();
+
+ Assert.equal(Glean.jogIpc.jogCounter.testGetValue(), COUNT);
+
+ // Note: this will break if string list ever rearranges its items.
+ const strings = Glean.jogIpc.jogStringList.testGetValue();
+ Assert.deepEqual(strings, [STRING, ANOTHER_STRING]);
+
+ const data = Glean.jogIpc.jogMemoryDist.testGetValue();
+ Assert.equal(MEMORIES.reduce((a, b) => a + b, 0) * 1024 * 1024, data.sum);
+ for (let [bucket, count] of Object.entries(data.values)) {
+ // We could assert instead, but let's skip to save the logspam.
+ if (count == 0) {
+ continue;
+ }
+ Assert.ok(count == 1 && MEMORY_BUCKETS.includes(bucket));
+ }
+
+ const customData = Glean.jogIpc.jogCustomDist.testGetValue();
+ Assert.equal(3 + 4, customData.sum, "Sum's correct");
+ for (let [bucket, count] of Object.entries(customData.values)) {
+ Assert.ok(
+ count == 0 || (count == 2 && bucket == 1), // both values in the low bucket
+ `Only two buckets have a sample ${bucket} ${count}`
+ );
+ }
+
+ let events = Glean.jogIpc.jogEventNoExtra.testGetValue();
+ Assert.equal(1, events.length);
+ Assert.equal("jog_ipc", events[0].category);
+ Assert.equal("jog_event_no_extra", events[0].name);
+
+ events = Glean.jogIpc.jogEvent.testGetValue();
+ Assert.equal(1, events.length);
+ Assert.equal("jog_ipc", events[0].category);
+ Assert.equal("jog_event", events[0].name);
+ Assert.deepEqual(EVENT_EXTRA, events[0].extra);
+
+ const NANOS_IN_MILLIS = 1e6;
+ const EPSILON = 40000; // bug 1701949
+ const times = Glean.jogIpc.jogTimingDist.testGetValue();
+ Assert.greater(times.sum, 15 * NANOS_IN_MILLIS - EPSILON);
+ // We can't guarantee any specific time values (thank you clocks),
+ // but we can assert there are only two samples.
+ Assert.equal(
+ 2,
+ Object.entries(times.values).reduce(
+ (acc, [bucket, count]) => acc + count,
+ 0
+ )
+ );
+
+ const labeledCounter = Glean.jogIpc.jogLabeledCounter;
+ Assert.equal(labeledCounter.label_1.testGetValue(), COUNTERS_1);
+ Assert.equal(labeledCounter.label_2.testGetValue(), COUNTERS_2);
+
+ Assert.throws(
+ () => Glean.jogIpc.jogLabeledCounterErr.__other__.testGetValue(),
+ /DataError/,
+ "Invalid labels record errors, which throw"
+ );
+
+ const labeledCounterWLabels = Glean.jogIpc.jogLabeledCounterWithLabels;
+ Assert.equal(labeledCounterWLabels.label_1.testGetValue(), COUNTERS_1);
+ Assert.equal(labeledCounterWLabels.label_2.testGetValue(), COUNTERS_2);
+
+ // TODO:(bug 1766515) - This should throw.
+ /*Assert.throws(
+ () =>
+ Glean.jogIpc.jogLabeledCounterWithLabelsErr.__other__.testGetValue(),
+ /DataError/,
+ "Invalid labels record errors, which throw"
+ );*/
+ Assert.equal(
+ Glean.jogIpc.jogLabeledCounterWithLabelsErr.__other__.testGetValue(),
+ INVALID_COUNTERS
+ );
+
+ Assert.deepEqual(
+ { numerator: 44, denominator: 14 },
+ Glean.jogIpc.jogRate.testGetValue()
+ );
+ }
+);
diff --git a/toolkit/components/glean/tests/xpcshell/test_MillionQ.js b/toolkit/components/glean/tests/xpcshell/test_MillionQ.js
new file mode 100644
index 0000000000..d98e73b451
--- /dev/null
+++ b/toolkit/components/glean/tests/xpcshell/test_MillionQ.js
@@ -0,0 +1,19 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(function test_queue_longer_than_1k() {
+ // FOG needs a profile directory to put its data in.
+ do_get_profile();
+
+ // Before init, try and fill the preinit queue with > 1000 tasks.
+ const kIterations = 2000;
+ for (let _i = 0; _i < kIterations; _i++) {
+ Glean.testOnly.badCode.add(1);
+ }
+
+ Services.fog.initializeFOG();
+
+ Assert.equal(kIterations, Glean.testOnly.badCode.testGetValue());
+});
diff --git a/toolkit/components/glean/tests/xpcshell/xpcshell.toml b/toolkit/components/glean/tests/xpcshell/xpcshell.toml
new file mode 100644
index 0000000000..40b1a22bf4
--- /dev/null
+++ b/toolkit/components/glean/tests/xpcshell/xpcshell.toml
@@ -0,0 +1,32 @@
+[DEFAULT]
+firefox-appdir = "browser"
+head = "head.js"
+
+["test_FOGIPCLimit.js"]
+
+["test_FOGInit.js"]
+
+["test_FOGPrefs.js"]
+skip-if = ["os == 'android'"] # FOG isn't responsible for monitoring prefs and controlling upload on Android
+
+["test_GIFFT.js"]
+run-sequentially = "very high failure rate in parallel"
+
+["test_GIFFTIPC.js"]
+
+["test_Glean.js"]
+
+["test_GleanExperiments.js"]
+skip-if = ["os == 'android'"] # FOG isn't responsible for experiment annotations on Android
+
+["test_GleanIPC.js"]
+
+["test_GleanServerKnobs.js"]
+skip-if = ["os == 'android'"] # Server Knobs on mobile will be handled by the specific app
+
+["test_JOG.js"]
+
+["test_JOGIPC.js"]
+
+["test_MillionQ.js"]
+skip-if = ["os == 'android'"] # Android inits its own FOG, so the test won't work.
diff --git a/toolkit/components/glean/xpcom/FOG.cpp b/toolkit/components/glean/xpcom/FOG.cpp
new file mode 100644
index 0000000000..3673b23707
--- /dev/null
+++ b/toolkit/components/glean/xpcom/FOG.cpp
@@ -0,0 +1,433 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/FOG.h"
+
+#include "mozilla/AppShutdown.h"
+#include "mozilla/Atomics.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/FOGIPC.h"
+#include "mozilla/browser/NimbusFeatures.h"
+#include "mozilla/glean/bindings/Common.h"
+#include "mozilla/glean/bindings/jog/jog_ffi_generated.h"
+#include "mozilla/glean/fog_ffi_generated.h"
+#include "mozilla/glean/GleanMetrics.h"
+#include "mozilla/Logging.h"
+#include "mozilla/MozPromise.h"
+#include "mozilla/ShutdownPhase.h"
+#include "mozilla/Unused.h"
+#include "nsContentUtils.h"
+#include "nsIFOG.h"
+#include "nsIUserIdleService.h"
+#include "nsServiceManagerUtils.h"
+#include "xpcpublic.h"
+
+namespace mozilla {
+
+using mozilla::LogLevel;
+static mozilla::LazyLogModule sLog("fog");
+
+using glean::LogToBrowserConsole;
+
+#ifdef MOZ_GLEAN_ANDROID
+// Defined by `glean-core`. We reexport it here for later use.
+extern "C" NS_EXPORT void glean_enable_logging(void);
+
+// Workaround to force a re-export of the `no_mangle` symbols from `glean-core`
+//
+// Due to how linking works and hides symbols the symbols from `glean-core`
+// might not be re-exported and thus not usable. By forcing use of _at least
+// one_ symbol in an exported function the functions will also be rexported.
+//
+// See also https://github.com/rust-lang/rust/issues/50007
+extern "C" NS_EXPORT void _fog_force_reexport_donotcall(void) {
+ glean_enable_logging();
+}
+#endif
+
+static StaticRefPtr<FOG> gFOG;
+static mozilla::Atomic<bool> gInitializeCalled(false);
+
+// We wait for 5s of idle before dumping IPC and flushing ping data to disk.
+// This number hasn't been tuned, so if you have a reason to change it,
+// please by all means do.
+const uint32_t kIdleSecs = 5;
+
+// static
+already_AddRefed<FOG> FOG::GetSingleton() {
+ if (gFOG) {
+ return do_AddRef(gFOG);
+ }
+
+ MOZ_LOG(sLog, LogLevel::Debug, ("FOG::GetSingleton()"));
+
+ gFOG = new FOG();
+
+ if (XRE_IsParentProcess()) {
+ nsresult rv;
+ nsCOMPtr<nsIUserIdleService> idleService =
+ do_GetService("@mozilla.org/widget/useridleservice;1", &rv);
+ NS_ENSURE_SUCCESS(rv, nullptr);
+ MOZ_ASSERT(idleService);
+ if (NS_WARN_IF(NS_FAILED(idleService->AddIdleObserver(gFOG, kIdleSecs)))) {
+ glean::fog::failed_idle_registration.Set(true);
+ }
+
+ RunOnShutdown(
+ [&] {
+ nsresult rv;
+ nsCOMPtr<nsIUserIdleService> idleService =
+ do_GetService("@mozilla.org/widget/useridleservice;1", &rv);
+ if (NS_SUCCEEDED(rv)) {
+ MOZ_ASSERT(idleService);
+ Unused << idleService->RemoveIdleObserver(gFOG, kIdleSecs);
+ }
+ if (!gInitializeCalled) {
+ gInitializeCalled = true;
+ // Assuming default data path and application id.
+ // Consumers using non defaults _must_ initialize FOG explicitly.
+ MOZ_LOG(sLog, LogLevel::Debug,
+ ("Init not called. Init-ing in shutdown"));
+ glean::fog::inits_during_shutdown.Add(1);
+ // It's enough to call init before shutting down.
+ // We don't need to (and can't) wait for it to complete.
+ glean::impl::fog_init(&VoidCString(), &VoidCString());
+ }
+ gFOG->Shutdown();
+ gFOG = nullptr;
+ },
+ ShutdownPhase::XPCOMShutdown);
+ }
+ return do_AddRef(gFOG);
+}
+
+void FOG::Shutdown() {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ glean::impl::fog_shutdown();
+}
+
+// This allows us to know it's too late to submit a ping in Rust.
+extern "C" bool FOG_TooLateToSend(void) {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ return AppShutdown::IsInOrBeyond(ShutdownPhase::AppShutdownNetTeardown);
+}
+
+// This allows us to pass the configurable maximum ping limit (in pings per
+// minute) to Rust. Default value is 15.
+extern "C" uint32_t FOG_MaxPingLimit(void) {
+ return NimbusFeatures::GetInt("gleanInternalSdk"_ns,
+ "gleanMaxPingsPerMinute"_ns, 15);
+}
+
+// This allows us to pass whether to enable precise event timestamps to Rust.
+// Default is false.
+extern "C" bool FOG_EventTimestampsEnabled(void) {
+ return NimbusFeatures::GetBool("gleanInternalSdk"_ns,
+ "enableEventTimestamps"_ns, false);
+}
+
+// Called when knowing if we're in automation is necessary.
+extern "C" bool FOG_IPCIsInAutomation(void) { return xpc::IsInAutomation(); }
+
+NS_IMETHODIMP
+FOG::InitializeFOG(const nsACString& aDataPathOverride,
+ const nsACString& aAppIdOverride) {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ gInitializeCalled = true;
+ RunOnShutdown(
+ [&] {
+ if (NimbusFeatures::GetBool("gleanInternalSdk"_ns, "finalInactive"_ns,
+ false)) {
+ glean::impl::fog_internal_glean_handle_client_inactive();
+ }
+ },
+ ShutdownPhase::AppShutdownConfirmed);
+
+ return glean::impl::fog_init(&aDataPathOverride, &aAppIdOverride);
+}
+
+NS_IMETHODIMP
+FOG::RegisterCustomPings() {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ glean::impl::fog_register_pings();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+FOG::SetLogPings(bool aEnableLogPings) {
+#ifdef MOZ_GLEAN_ANDROID
+ return NS_OK;
+#else
+ MOZ_ASSERT(XRE_IsParentProcess());
+ return glean::impl::fog_set_log_pings(aEnableLogPings);
+#endif
+}
+
+NS_IMETHODIMP
+FOG::SetTagPings(const nsACString& aDebugTag) {
+#ifdef MOZ_GLEAN_ANDROID
+ return NS_OK;
+#else
+ MOZ_ASSERT(XRE_IsParentProcess());
+ return glean::impl::fog_set_debug_view_tag(&aDebugTag);
+#endif
+}
+
+NS_IMETHODIMP
+FOG::SendPing(const nsACString& aPingName) {
+#ifdef MOZ_GLEAN_ANDROID
+ return NS_OK;
+#else
+ MOZ_ASSERT(XRE_IsParentProcess());
+ return glean::impl::fog_submit_ping(&aPingName);
+#endif
+}
+
+NS_IMETHODIMP
+FOG::SetExperimentActive(const nsACString& aExperimentId,
+ const nsACString& aBranch, JS::HandleValue aExtra,
+ JSContext* aCx) {
+#ifdef MOZ_GLEAN_ANDROID
+ NS_WARNING("Don't set experiments from Gecko in Android. Ignoring.");
+ return NS_OK;
+#else
+ MOZ_ASSERT(XRE_IsParentProcess());
+ nsTArray<nsCString> extraKeys;
+ nsTArray<nsCString> extraValues;
+ if (!aExtra.isNullOrUndefined()) {
+ JS::RootedObject obj(aCx, &aExtra.toObject());
+ JS::Rooted<JS::IdVector> keys(aCx, JS::IdVector(aCx));
+ if (!JS_Enumerate(aCx, obj, &keys)) {
+ LogToBrowserConsole(nsIScriptError::warningFlag,
+ u"Failed to enumerate experiment extras object."_ns);
+ return NS_OK;
+ }
+
+ for (size_t i = 0, n = keys.length(); i < n; i++) {
+ nsAutoJSCString jsKey;
+ if (!jsKey.init(aCx, keys[i])) {
+ LogToBrowserConsole(
+ nsIScriptError::warningFlag,
+ u"Extra dictionary should only contain string keys."_ns);
+ return NS_OK;
+ }
+
+ JS::Rooted<JS::Value> value(aCx);
+ if (!JS_GetPropertyById(aCx, obj, keys[i], &value)) {
+ LogToBrowserConsole(nsIScriptError::warningFlag,
+ u"Failed to get experiment extra property."_ns);
+ return NS_OK;
+ }
+
+ nsAutoJSCString jsValue;
+ if (!value.isString()) {
+ LogToBrowserConsole(
+ nsIScriptError::warningFlag,
+ u"Experiment extra properties must have string values."_ns);
+ return NS_OK;
+ }
+
+ if (!jsValue.init(aCx, value)) {
+ LogToBrowserConsole(nsIScriptError::warningFlag,
+ u"Can't extract experiment extra property"_ns);
+ return NS_OK;
+ }
+
+ extraKeys.AppendElement(jsKey);
+ extraValues.AppendElement(jsValue);
+ }
+ }
+ glean::impl::fog_set_experiment_active(&aExperimentId, &aBranch, &extraKeys,
+ &extraValues);
+ return NS_OK;
+#endif
+}
+
+NS_IMETHODIMP
+FOG::SetExperimentInactive(const nsACString& aExperimentId) {
+#ifdef MOZ_GLEAN_ANDROID
+ NS_WARNING("Don't unset experiments from Gecko in Android. Ignoring.");
+ return NS_OK;
+#else
+ MOZ_ASSERT(XRE_IsParentProcess());
+ glean::impl::fog_set_experiment_inactive(&aExperimentId);
+ return NS_OK;
+#endif
+}
+
+NS_IMETHODIMP
+FOG::TestGetExperimentData(const nsACString& aExperimentId, JSContext* aCx,
+ JS::MutableHandleValue aResult) {
+#ifdef MOZ_GLEAN_ANDROID
+ NS_WARNING("Don't test experiments from Gecko in Android. Throwing.");
+ aResult.set(JS::UndefinedValue());
+ return NS_ERROR_FAILURE;
+#else
+ MOZ_ASSERT(XRE_IsParentProcess());
+ if (!glean::impl::fog_test_is_experiment_active(&aExperimentId)) {
+ aResult.set(JS::UndefinedValue());
+ return NS_OK;
+ }
+
+ // We could struct-up the branch and extras and do what
+ // EventMetric::TestGetValue does... but keeping allocation on this side feels
+ // cleaner to me at the moment.
+ nsCString branch;
+ nsTArray<nsCString> extraKeys;
+ nsTArray<nsCString> extraValues;
+
+ glean::impl::fog_test_get_experiment_data(&aExperimentId, &branch, &extraKeys,
+ &extraValues);
+ MOZ_ASSERT(extraKeys.Length() == extraValues.Length());
+
+ JS::RootedObject jsExperimentDataObj(aCx, JS_NewPlainObject(aCx));
+ if (NS_WARN_IF(!jsExperimentDataObj)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ JS::RootedValue jsBranchStr(aCx);
+ if (!dom::ToJSValue(aCx, branch, &jsBranchStr) ||
+ !JS_DefineProperty(aCx, jsExperimentDataObj, "branch", jsBranchStr,
+ JSPROP_ENUMERATE)) {
+ NS_WARNING("Failed to define branch for experiment data object.");
+ return NS_ERROR_FAILURE;
+ }
+
+ JS::RootedObject jsExtraObj(aCx, JS_NewPlainObject(aCx));
+ if (!JS_DefineProperty(aCx, jsExperimentDataObj, "extra", jsExtraObj,
+ JSPROP_ENUMERATE)) {
+ NS_WARNING("Failed to define extra for experiment data object.");
+ return NS_ERROR_FAILURE;
+ }
+
+ for (unsigned int i = 0; i < extraKeys.Length(); i++) {
+ JS::RootedValue jsValueStr(aCx);
+ if (!dom::ToJSValue(aCx, extraValues[i], &jsValueStr) ||
+ !JS_DefineProperty(aCx, jsExtraObj, extraKeys[i].Data(), jsValueStr,
+ JSPROP_ENUMERATE)) {
+ NS_WARNING("Failed to define extra property for experiment data object.");
+ return NS_ERROR_FAILURE;
+ }
+ }
+ aResult.setObject(*jsExperimentDataObj);
+ return NS_OK;
+#endif
+}
+
+NS_IMETHODIMP
+FOG::SetMetricsFeatureConfig(const nsACString& aJsonConfig) {
+#ifdef MOZ_GLEAN_ANDROID
+ NS_WARNING(
+ "Don't set metric feature configs from Gecko in Android. Ignoring.");
+ return NS_OK;
+#else
+ MOZ_ASSERT(XRE_IsParentProcess());
+ glean::impl::fog_set_metrics_feature_config(&aJsonConfig);
+ return NS_OK;
+#endif
+}
+
+NS_IMETHODIMP
+FOG::TestFlushAllChildren(JSContext* aCx, mozilla::dom::Promise** aOutPromise) {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ NS_ENSURE_ARG(aOutPromise);
+ *aOutPromise = nullptr;
+ nsIGlobalObject* global = xpc::CurrentNativeGlobal(aCx);
+ if (NS_WARN_IF(!global)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ ErrorResult erv;
+ RefPtr<dom::Promise> promise = dom::Promise::Create(global, erv);
+ if (NS_WARN_IF(erv.Failed())) {
+ return erv.StealNSResult();
+ }
+
+ glean::FlushAndUseFOGData()->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [promise]() { promise->MaybeResolveWithUndefined(); });
+
+ promise.forget(aOutPromise);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+FOG::Observe(nsISupports* aSubject, const char* aTopic, const char16_t* aData) {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ MOZ_ASSERT(NS_IsMainThread());
+
+ // On idle, opportunistically flush child process data to the parent,
+ // then persist ping-lifetime data to the db.
+ if (!strcmp(aTopic, OBSERVER_TOPIC_IDLE)) {
+ glean::FlushAndUseFOGData();
+#ifndef MOZ_GLEAN_ANDROID
+ Unused << glean::impl::fog_persist_ping_lifetime_data();
+#endif
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+FOG::TestResetFOG(const nsACString& aDataPathOverride,
+ const nsACString& aAppIdOverride) {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ return glean::impl::fog_test_reset(&aDataPathOverride, &aAppIdOverride);
+}
+
+NS_IMETHODIMP
+FOG::TestTriggerMetrics(uint32_t aProcessType, JSContext* aCx,
+ mozilla::dom::Promise** aOutPromise) {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ NS_ENSURE_ARG(aOutPromise);
+ *aOutPromise = nullptr;
+ nsIGlobalObject* global = xpc::CurrentNativeGlobal(aCx);
+ if (NS_WARN_IF(!global)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ ErrorResult erv;
+ RefPtr<dom::Promise> promise = dom::Promise::Create(global, erv);
+ if (NS_WARN_IF(erv.Failed())) {
+ return erv.StealNSResult();
+ }
+
+ glean::TestTriggerMetrics(aProcessType, promise);
+
+ promise.forget(aOutPromise);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+FOG::TestRegisterRuntimeMetric(
+ const nsACString& aType, const nsACString& aCategory,
+ const nsACString& aName, const nsTArray<nsCString>& aPings,
+ const nsACString& aLifetime, const bool aDisabled,
+ const nsACString& aExtraArgs, uint32_t* aMetricIdOut) {
+ *aMetricIdOut = 0;
+ *aMetricIdOut = glean::jog::jog_test_register_metric(
+ &aType, &aCategory, &aName, &aPings, &aLifetime, aDisabled, &aExtraArgs);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+FOG::TestRegisterRuntimePing(const nsACString& aName,
+ const bool aIncludeClientId,
+ const bool aSendIfEmpty,
+ const bool aPreciseTimestamps,
+ const nsTArray<nsCString>& aReasonCodes,
+ uint32_t* aPingIdOut) {
+ *aPingIdOut = 0;
+ *aPingIdOut =
+ glean::jog::jog_test_register_ping(&aName, aIncludeClientId, aSendIfEmpty,
+ aPreciseTimestamps, &aReasonCodes);
+ return NS_OK;
+}
+
+NS_IMPL_ISUPPORTS(FOG, nsIFOG, nsIObserver)
+
+} // namespace mozilla
diff --git a/toolkit/components/glean/xpcom/FOG.h b/toolkit/components/glean/xpcom/FOG.h
new file mode 100644
index 0000000000..55e155052e
--- /dev/null
+++ b/toolkit/components/glean/xpcom/FOG.h
@@ -0,0 +1,29 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_FOG_h
+#define mozilla_FOG_h
+
+#include "nsIFOG.h"
+#include "nsIObserver.h"
+
+namespace mozilla {
+class FOG final : public nsIFOG, public nsIObserver {
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIFOG
+ NS_DECL_NSIOBSERVER
+
+ public:
+ FOG() = default;
+ static already_AddRefed<FOG> GetSingleton();
+
+ private:
+ ~FOG() = default;
+ void Shutdown();
+};
+
+}; // namespace mozilla
+
+#endif // mozilla_FOG_h
diff --git a/toolkit/components/glean/xpcom/components.conf b/toolkit/components/glean/xpcom/components.conf
new file mode 100644
index 0000000000..b5a86537c0
--- /dev/null
+++ b/toolkit/components/glean/xpcom/components.conf
@@ -0,0 +1,18 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+Classes = [
+ {
+ 'cid': '{98d0e975-9cad-4ce3-ae2f-f878b8be6307}',
+ 'contract_ids': ['@mozilla.org/toolkit/glean;1'],
+ 'singleton': True,
+ 'type': 'mozilla::FOG',
+ 'headers': ['mozilla/FOG.h'],
+ 'constructor': 'mozilla::FOG::GetSingleton',
+ 'js_name': 'fog',
+ 'interfaces': ['nsIFOG'],
+ }
+]
diff --git a/toolkit/components/glean/xpcom/moz.build b/toolkit/components/glean/xpcom/moz.build
new file mode 100644
index 0000000000..0d967ff564
--- /dev/null
+++ b/toolkit/components/glean/xpcom/moz.build
@@ -0,0 +1,27 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+FINAL_LIBRARY = "xul"
+
+EXPORTS.mozilla += [
+ "FOG.h",
+]
+
+UNIFIED_SOURCES += [
+ "FOG.cpp",
+]
+
+XPCOM_MANIFESTS += ["components.conf"]
+
+XPIDL_MODULE = "fog"
+
+XPIDL_SOURCES += [
+ "nsIFOG.idl",
+ "nsIGleanPing.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..682df85f1a
--- /dev/null
+++ b/toolkit/components/glean/xpcom/nsIFOG.idl
@@ -0,0 +1,190 @@
+/* -*- 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.
+ *
+ * Call it eventually. Metric data recorded beforehand will be buffered.
+ *
+ * @param aDataPathOverride - The path of a custom Glean data path to use
+ * instead of the profile dir.
+ * @param aAppIdOverride - The application_id to use instead of
+ * "firefox.desktop".
+ */
+ void initializeFOG([optional] in AUTF8String aDataPathOverride, [optional] in AUTF8String aAppIdOverride);
+
+ /**
+ * Register custom pings.
+ *
+ * Ensure all custom pings are registered with Glean.
+ */
+ void registerCustomPings();
+
+ /**
+ * Enable or Disable the logging of pings in the Glean SDK.
+ * See https://firefox-source-docs.mozilla.org/toolkit/components/glean/dev/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/dev/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/dev/testing.html
+ * for details.
+ *
+ * @param aPingName - The name of the ping to send. If no ping of that name
+ * exists, or the ping is known but cannot be assembled
+ * (e.g if it is empty), no ping will be sent.
+ */
+ void sendPing(in ACString aPingName);
+
+ /**
+ * Indicate that an experiment is running.
+ * Glean will add an experiment annotation which is sent with pings.
+ * This information is not persisted between runs.
+ *
+ * See `glean_core::Glean::set_experiment_active`.
+ *
+ * Logs on error, but does not throw.
+ *
+ * @param aExperimentId - The id/slug of the experiment.
+ * @param aBranch - The name of the active branch of the experiment.
+ * @param aExtra - Optional string -> string dictionary of extra information.
+ */
+ [implicit_jscontext]
+ void setExperimentActive(in ACString aExperimentId, in ACString aBranch, [optional] in jsval aExtra);
+
+ /**
+ * Indicate that an experiment is no longer running.
+ *
+ * See `glean_core::Glean::set_experiment_inactive`.
+ *
+ * Logs on error, but does not throw.
+ *
+ * @param aExperimentId - The id/slug of the experiment from setExperimentActive.
+ */
+ void setExperimentInactive(in ACString aExperimentId);
+
+ /**
+ * **Test-only API**
+ *
+ * If the identified experiment was set active and hasn't been set inactive,
+ * this will give you the active branch and extra information.
+ *
+ * @param aExperimentId - The id/slug of the experiment from setExperimentActive.
+ *
+ * @return an object of the form
+ * {branch: "branch-name", extra: {extra_key1: extra_value1, ...}}
+ * if there is an active experiment. Undefined, otherwise.
+ */
+ [implicit_jscontext]
+ jsval testGetExperimentData(in ACString aExperimentId);
+
+ /**
+ * Set remote-configuration for metrics' disabled property.
+ *
+ * See [`glean_core::Glean::set_metrics_enabled_config`]
+ *
+ * Logs on error, but does not throw.
+ *
+ * @param aJsonConfig - The stringified JSON object in the form
+ * {metric_base_identifier: boolean,}
+ * which may contain multiple metric object entries.
+ */
+ void setMetricsFeatureConfig(in ACString aJsonConfig);
+
+ /**
+ * ** Test-only Method **
+ *
+ * Flush all data from all child processes.
+ *
+ * @returns A promise that resolves when the data's been stored.
+ */
+ [implicit_jscontext]
+ Promise testFlushAllChildren();
+
+ /**
+ * ** Test-only Method **
+ *
+ * Reset FOG and the Glean SDK, clearing storage.
+ */
+ void testResetFOG([optional] in AUTF8String aDataPathOverride, [optional] in AUTF8String aAppIdOverride);
+
+ /**
+ * ** Test-only Method **
+ *
+ * Trigger test metric instrumentation on the GPU, RDD or Socket process.
+ *
+ * @param aProcessType - A PROCESS_TYPE_* value from the constants defined
+ * in the nsIXULRuntime interface.
+ *
+ * @returns A promise that resolves when the test data has been added.
+ * The promise will be rejected if the process type is not supported
+ * or if sending the IPC to the child process fails.
+ */
+ [implicit_jscontext]
+ Promise testTriggerMetrics(in unsigned long aProcessType);
+
+ /**
+ * ** Test-only Method **
+ *
+ * Register a metric.
+ *
+ * This function is deliberately not too friendly to use. You probably aren't
+ * supposed to use it unless you're testing metric registration itself.
+ *
+ * @param aType - The metric's type.
+ * @param aCategory - The metric's category.
+ * @param aName - The metric's name.
+ * @param aPings - The pings to send it in.
+ * @param aLifetime - The metric's lifetime.
+ * @param aDisabled - Whether the metric, though existing, isn't enabled.
+ * @param aExtraArgs - Optional JSON string of extra args.
+ */
+ uint32_t testRegisterRuntimeMetric(in ACString aType,
+ in ACString aCategory,
+ in ACString aName,
+ in Array<ACString> aPings,
+ in ACString aLifetime,
+ in boolean aDisabled,
+ [optional] in ACString aExtraArgs);
+
+ /**
+ * ** Test-only Method **
+ *
+ * Register a ping.
+ *
+ * This function is deliberately not too friendly to use. You probably aren't
+ * supposed to use it unless you're testing ping registration itself.
+ *
+ * @param aName - The ping's name.
+ * @param aIncludeClientId - Whether the ping should include the client_id.
+ * @param aSendIfEmpty - Whether the ping should send even if empty.
+ * @param aReasonCodes - The list of valid reasons for ping submission.
+ */
+ uint32_t testRegisterRuntimePing(in ACString aName,
+ in boolean aIncludeClientId,
+ in boolean aSendIfEmpty,
+ in boolean aPreciseTimestamps,
+ in Array<ACString> aReasonCodes);
+};
diff --git a/toolkit/components/glean/xpcom/nsIGleanPing.idl b/toolkit/components/glean/xpcom/nsIGleanPing.idl
new file mode 100644
index 0000000000..59d1aa8453
--- /dev/null
+++ b/toolkit/components/glean/xpcom/nsIGleanPing.idl
@@ -0,0 +1,51 @@
+/* -*- 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, function, uuid(e5447f62-4b03-497c-81e9-6ab683d20380)]
+interface nsIGleanPingTestCallback : nsISupports
+{
+ void call(in ACString aReason);
+};
+
+[scriptable, uuid(5223a48b-687d-47ff-a629-fd4a72d1ecfa)]
+interface nsIGleanPing : nsISupports
+{
+ /**
+ * Collect and submit the ping for eventual upload.
+ *
+ * This will collect all stored data to be included in the ping.
+ * Data with lifetime `ping` will then be reset.
+ *
+ * If the ping is configured with `send_if_empty = false`
+ * and the ping currently contains no content,
+ * it will not be queued for upload.
+ * If the ping is configured with `send_if_empty = true`
+ * it will be queued for upload even if empty.
+ *
+ * Pings always contain the `ping_info` and `client_info` sections.
+ * See [ping sections](https://mozilla.github.io/glean/book/user/pings/index.html#ping-sections)
+ * for details.
+ *
+ * @param aReason - Optional. The reason the ping is being submitted.
+ * Must match one of the configured `reason_codes`.
+ */
+ void submit([optional] in ACString aReason);
+
+ /**
+ * **Test-only API**
+ *
+ * Register a callback to be called right before this ping is next submitted.
+ * The provided function is called exactly once before submitting.
+ *
+ * Note: The callback will be called on any call to submit.
+ * A ping might not be sent afterwards, e.g. if the ping is empty and
+ * `send_if_empty` is `false`.
+ *
+ * @param aCallback - The callback to call on the next submit.
+ */
+ void testBeforeNextSubmit(in nsIGleanPingTestCallback aCallback);
+};