summaryrefslogtreecommitdiffstats
path: root/toolkit/components/glean/tests/xpcshell/test_GIFFT.js
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/glean/tests/xpcshell/test_GIFFT.js')
-rw-r--r--toolkit/components/glean/tests/xpcshell/test_GIFFT.js523
1 files changed, 523 insertions, 0 deletions
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")
+ );
+});