diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 01:47:29 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 01:47:29 +0000 |
commit | 0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d (patch) | |
tree | a31f07c9bcca9d56ce61e9a1ffd30ef350d513aa /toolkit/components/extensions/test/xpcshell/head_telemetry.js | |
parent | Initial commit. (diff) | |
download | firefox-esr-0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d.tar.xz firefox-esr-0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d.zip |
Adding upstream version 115.8.0esr.upstream/115.8.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/components/extensions/test/xpcshell/head_telemetry.js')
-rw-r--r-- | toolkit/components/extensions/test/xpcshell/head_telemetry.js | 435 |
1 files changed, 435 insertions, 0 deletions
diff --git a/toolkit/components/extensions/test/xpcshell/head_telemetry.js b/toolkit/components/extensions/test/xpcshell/head_telemetry.js new file mode 100644 index 0000000000..b3aa5e84a8 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/head_telemetry.js @@ -0,0 +1,435 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +/* exported IS_ANDROID_BUILD, IS_OOP, valueSum, clearHistograms, getSnapshots, promiseTelemetryRecorded, + assertDNRTelemetryMetricsDefined, assertDNRTelemetryMetricsNoSamples, assertDNRTelemetryMetricsGetValueEq, + assertDNRTelemetryMetricsSamplesCount, resetTelemetryData, setupTelemetryForTests */ + +ChromeUtils.defineESModuleGetters(this, { + ContentTaskUtils: "resource://testing-common/ContentTaskUtils.sys.mjs", +}); + +// Allows to run xpcshell telemetry test also on products (e.g. Thunderbird) where +// that telemetry wouldn't be actually collected in practice (but to be sure +// that it will work on those products as well by just adding the product in +// the telemetry metric definitions if it turns out we want to). +Services.prefs.setBoolPref( + "toolkit.telemetry.testing.overrideProductsCheck", + true +); + +const IS_OOP = Services.prefs.getBoolPref("extensions.webextensions.remote"); + +// Initializing and asserting the expected telemetry is currently conditioned +// on this const. +// TODO(Bug 1752139) remove this along with initializing and asserting the expected +// telemetry also for android build, once `Services.fog.testResetFOG()` is implemented +// for Android builds. +const IS_ANDROID_BUILD = AppConstants.platform === "android"; + +const WEBEXT_EVENTPAGE_RUNNING_TIME_MS = "WEBEXT_EVENTPAGE_RUNNING_TIME_MS"; +const WEBEXT_EVENTPAGE_RUNNING_TIME_MS_BY_ADDONID = + "WEBEXT_EVENTPAGE_RUNNING_TIME_MS_BY_ADDONID"; +const WEBEXT_EVENTPAGE_IDLE_RESULT_COUNT = "WEBEXT_EVENTPAGE_IDLE_RESULT_COUNT"; +const WEBEXT_EVENTPAGE_IDLE_RESULT_COUNT_BY_ADDONID = + "WEBEXT_EVENTPAGE_IDLE_RESULT_COUNT_BY_ADDONID"; + +// Keep this in sync with the order in Histograms.json for "WEBEXT_EVENTPAGE_IDLE_RESULT_COUNT": +// the position of the category string determines the index of the values collected in the categorial +// histogram and so the existing labels should be kept in the exact same order and any new category +// to be added in the future should be appended to the existing ones. +const HISTOGRAM_EVENTPAGE_IDLE_RESULT_CATEGORIES = [ + "suspend", + "reset_other", + "reset_event", + "reset_listeners", + "reset_nativeapp", + "reset_streamfilter", +]; + +function valueSum(arr) { + return Object.values(arr).reduce((a, b) => a + b, 0); +} + +function clearHistograms() { + Services.telemetry.getSnapshotForHistograms("main", true /* clear */); + Services.telemetry.getSnapshotForKeyedHistograms("main", true /* clear */); +} + +function clearScalars() { + Services.telemetry.getSnapshotForScalars("main", true /* clear */); + Services.telemetry.getSnapshotForKeyedScalars("main", true /* clear */); +} + +function getSnapshots(process) { + return Services.telemetry.getSnapshotForHistograms("main", false /* clear */)[ + process + ]; +} + +function getKeyedSnapshots(process) { + return Services.telemetry.getSnapshotForKeyedHistograms( + "main", + false /* clear */ + )[process]; +} + +// TODO Bug 1357509: There is no good way to make sure that the parent received +// the histogram entries from the extension and content processes. Let's stick +// to the ugly, spinning the event loop until we have a good approach. +function promiseTelemetryRecorded(id, process, expectedCount) { + let condition = () => { + let snapshot = Services.telemetry.getSnapshotForHistograms( + "main", + false /* clear */ + )[process][id]; + return snapshot && valueSum(snapshot.values) >= expectedCount; + }; + return ContentTaskUtils.waitForCondition(condition); +} + +function promiseKeyedTelemetryRecorded( + id, + process, + expectedKey, + expectedCount +) { + let condition = () => { + let snapshot = Services.telemetry.getSnapshotForKeyedHistograms( + "main", + false /* clear */ + )[process][id]; + return ( + snapshot && + snapshot[expectedKey] && + valueSum(snapshot[expectedKey].values) >= expectedCount + ); + }; + return ContentTaskUtils.waitForCondition(condition); +} + +function assertHistogramSnapshot( + histogramId, + { keyed, processSnapshot, expectedValue }, + msg +) { + let histogram; + + if (keyed) { + histogram = Services.telemetry.getKeyedHistogramById(histogramId); + } else { + histogram = Services.telemetry.getHistogramById(histogramId); + } + + let res = processSnapshot(histogram.snapshot()); + Assert.deepEqual(res, expectedValue, msg); + return res; +} + +function assertHistogramEmpty(histogramId) { + assertHistogramSnapshot( + histogramId, + { + processSnapshot: snapshot => snapshot.sum, + expectedValue: 0, + }, + `No data recorded for histogram: ${histogramId}.` + ); +} + +function assertKeyedHistogramEmpty(histogramId) { + assertHistogramSnapshot( + histogramId, + { + keyed: true, + processSnapshot: snapshot => Object.keys(snapshot).length, + expectedValue: 0, + }, + `No data recorded for histogram: ${histogramId}.` + ); +} + +function assertHistogramCategoryNotEmpty( + histogramId, + { category, categories, keyed, key }, + msg +) { + let message = msg; + + if (!msg) { + message = `Data recorded for histogram: ${histogramId}, category "${category}"`; + if (keyed) { + message += `, key "${key}"`; + } + } + + assertHistogramSnapshot( + histogramId, + { + keyed, + processSnapshot: snapshot => { + const categoryIndex = categories.indexOf(category); + if (keyed) { + return { + [key]: snapshot[key] + ? snapshot[key].values[categoryIndex] > 0 + : null, + }; + } + return snapshot.values[categoryIndex] > 0; + }, + expectedValue: keyed ? { [key]: true } : true, + }, + message + ); +} + +function setupTelemetryForTests() { + // 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(); +} + +function resetTelemetryData() { + if (IS_ANDROID_BUILD) { + info("Skip testResetFOG on android builds"); + return; + } + Services.fog.testResetFOG(); + + // Clear histograms data recorded in the unified telemetry + // (needed to make sure we can keep asserting that the same + // amount of samples collected by Glean should also be found + // in the related mirrored unified telemetry probe after we + // have reset Glean metrics data using testResetFOG). + clearHistograms(); + clearScalars(); +} + +function assertDNRTelemetryMetricsDefined(metrics) { + const metricsNotFound = metrics.filter(metricDetails => { + const { metric, label } = metricDetails; + if (!Glean.extensionsApisDnr[metric]) { + return true; + } + if (label) { + return !Glean.extensionsApisDnr[metric][label]; + } + return false; + }); + Assert.deepEqual( + metricsNotFound, + [], + `All expected extensionsApisDnr Glean metrics should be found` + ); +} + +function assertDNRTelemetryMirrored({ + gleanMetric, + gleanLabel, + unifiedName, + unifiedType, +}) { + assertDNRTelemetryMetricsDefined([ + { metric: gleanMetric, label: gleanLabel }, + ]); + const gleanData = gleanLabel + ? Glean.extensionsApisDnr[gleanMetric][gleanLabel].testGetValue() + : Glean.extensionsApisDnr[gleanMetric].testGetValue(); + + if (!unifiedName) { + Assert.ok( + false, + `Unexpected missing unifiedName parameter on call to assertDNRTelemetryMirrored` + ); + return; + } + + let unifiedData; + + switch (unifiedType) { + case "histogram": { + let found = false; + try { + const histogram = Services.telemetry.getHistogramById(unifiedName); + found = !!histogram; + } catch (err) { + Cu.reportError(err); + } + Assert.ok(found, `Expect an histogram named ${unifiedName} to be found`); + unifiedData = Services.telemetry.getSnapshotForHistograms("main", false) + .parent[unifiedName]; + break; + } + case "keyedScalar": { + const snapshot = Services.telemetry.getSnapshotForKeyedScalars( + "main", + false + ); + if (unifiedName in (snapshot?.parent || {})) { + unifiedData = snapshot.parent[unifiedName][gleanLabel]; + } + break; + } + case "scalar": { + const snapshot = Services.telemetry.getSnapshotForScalars("main", false); + if (unifiedName in (snapshot?.parent || {})) { + unifiedData = snapshot.parent[unifiedName]; + } + break; + } + default: + Assert.ok( + false, + `Unexpected unifiedType ${unifiedType} on call to assertDNRTelemetryMirrored` + ); + return; + } + + if (gleanData == undefined) { + Assert.deepEqual( + unifiedData, + undefined, + `Expect mirrored unified telemetry ${unifiedType} ${unifiedName} has no samples as Glean ${gleanMetric}` + ); + } else { + switch (unifiedType) { + case "histogram": { + Assert.deepEqual( + valueSum(unifiedData.values), + valueSum(gleanData.values), + `Expect mirrored unified telemetry ${unifiedType} ${unifiedName} has samples mirrored from Glean ${gleanMetric}` + ); + break; + } + case "scalar": + case "keyedScalar": { + Assert.deepEqual( + unifiedData, + gleanData, + `Expect mirrored unified telemetry ${unifiedType} ${unifiedName} has samples mirrored from Glean ${gleanMetric}` + ); + break; + } + } + } +} + +function assertDNRTelemetryMetricsNoSamples(metrics, msg) { + assertDNRTelemetryMetricsDefined(metrics); + for (const metricDetails of metrics) { + const { metric, label } = metricDetails; + if (IS_ANDROID_BUILD) { + info( + `Skip assertions on collected samples for extensionsApisDnr.${metric} on android builds (${msg})` + ); + return; + } + const gleanData = label + ? Glean.extensionsApisDnr[metric][label].testGetValue() + : Glean.extensionsApisDnr[metric].testGetValue(); + Assert.deepEqual( + gleanData, + undefined, + `Expect no sample for Glean metric extensionApisDnr.${metric} (${msg}): ${gleanData}` + ); + + if (metricDetails.mirroredName) { + const { mirroredName, mirroredType } = metricDetails; + assertDNRTelemetryMirrored({ + gleanMetric: metric, + gleanLabel: label, + unifiedName: mirroredName, + unifiedType: mirroredType, + }); + } + } +} + +function assertDNRTelemetryMetricsGetValueEq(metrics, msg) { + assertDNRTelemetryMetricsDefined(metrics); + for (const metricDetails of metrics) { + const { metric, label, expectedGetValue } = metricDetails; + if (IS_ANDROID_BUILD) { + info( + `Skip assertions on collected samples for extensionsApisDnr.${metric} on android builds` + ); + return; + } + const gleanData = label + ? Glean.extensionsApisDnr[metric][label].testGetValue() + : Glean.extensionsApisDnr[metric].testGetValue(); + Assert.deepEqual( + gleanData, + expectedGetValue, + `Got expected value set on Glean metric extensionApisDnr.${metric}${ + label ? `.${label}` : "" + } (${msg})` + ); + + if (metricDetails.mirroredName) { + const { mirroredName, mirroredType } = metricDetails; + assertDNRTelemetryMirrored({ + gleanMetric: metric, + gleanLabel: label, + unifiedName: mirroredName, + unifiedType: mirroredType, + }); + } + } +} + +function assertDNRTelemetryMetricsSamplesCount(metrics, msg) { + assertDNRTelemetryMetricsDefined(metrics); + + // This assertion helpers doesn't currently handle labeled metrics, + // raise an explicit error to catch if one is included by mistake. + const labeledMetricsFound = metrics.filter(metric => !!metric.label); + if (labeledMetricsFound.length) { + throw new Error( + `Unexpected labeled metrics in call to assertDNRTelemetryMetricsSamplesCount: ${labeledMetricsFound}` + ); + } + + for (const metricDetails of metrics) { + const { metric, expectedSamplesCount } = metricDetails; + if (IS_ANDROID_BUILD) { + info( + `Skip assertions on collected samples for extensionsApisDnr.${metric} on android builds` + ); + return; + } + const gleanData = Glean.extensionsApisDnr[metric].testGetValue(); + Assert.notEqual( + gleanData, + undefined, + `Got some sample for Glean metric extensionApisDnr.${metric}: ${ + gleanData && JSON.stringify(gleanData) + }` + ); + Assert.equal( + valueSum(gleanData.values), + expectedSamplesCount, + `Got the expected number of samples for Glean metric extensionsApisDnr.${metric} (${msg})` + ); + // Make sure we are accumulating meaningfull values in the sample, + // if we do have samples for the bucket "0" it likely means we have + // not been collecting the value correctly (e.g. typo in the property + // name being collected). + Assert.ok( + !gleanData.values["0"], + `No sample for Glean metric extensionsApisDnr.${metric} should be collected for the bucket "0"` + ); + + if (metricDetails.mirroredName) { + const { mirroredName, mirroredType } = metricDetails; + assertDNRTelemetryMirrored({ + gleanMetric: metric, + unifiedName: mirroredName, + unifiedType: mirroredType, + }); + } + } +} |