594 lines
16 KiB
JavaScript
594 lines
16 KiB
JavaScript
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
|
/* vim: set sts=2 sw=2 et tw=80: */
|
|
"use strict";
|
|
|
|
/* exported 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");
|
|
|
|
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",
|
|
"reset_parentapicall",
|
|
];
|
|
|
|
const GLEAN_EVENTPAGE_IDLE_RESULT_CATEGORIES = [
|
|
...HISTOGRAM_EVENTPAGE_IDLE_RESULT_CATEGORIES,
|
|
"__other__",
|
|
];
|
|
|
|
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() {
|
|
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 assertValidGleanMetric({
|
|
metricId,
|
|
gleanMetric,
|
|
gleanMetricConstructor,
|
|
msg,
|
|
}) {
|
|
const { GleanMetric } = globalThis;
|
|
if (!(gleanMetric instanceof GleanMetric)) {
|
|
throw new Error(
|
|
`gleanMetric "${metricId}" ${gleanMetric} should be an instance of GleanMetric ${msg}`
|
|
);
|
|
}
|
|
|
|
if (
|
|
gleanMetricConstructor &&
|
|
!(gleanMetric instanceof gleanMetricConstructor)
|
|
) {
|
|
throw new Error(
|
|
`gleanMetric "${metricId}" should be an instance of the given GleanMetric constructor: ${gleanMetric} not an instance of ${gleanMetricConstructor} ${msg}`
|
|
);
|
|
}
|
|
}
|
|
|
|
// TODO reuse this helper inside the DNR specific test helper which would be doing
|
|
// a similar assertion on DNR metrics.
|
|
function assertGleanMetricsNoSamples({
|
|
metricId,
|
|
gleanMetric,
|
|
gleanMetricConstructor,
|
|
message,
|
|
}) {
|
|
const msg = message ? `(${message})` : "";
|
|
assertValidGleanMetric({
|
|
metricId,
|
|
gleanMetric,
|
|
gleanMetricConstructor,
|
|
msg,
|
|
});
|
|
const gleanData = gleanMetric.testGetValue();
|
|
Assert.deepEqual(
|
|
gleanData,
|
|
undefined,
|
|
`Got no sample for Glean metric ${metricId} ${msg}`
|
|
);
|
|
}
|
|
|
|
// TODO reuse this helper inside the DNR specific test helper which would be doing
|
|
// a similar assertion on DNR metrics.
|
|
function assertGleanMetricsSamplesCount({
|
|
metricId,
|
|
gleanMetric,
|
|
gleanMetricConstructor,
|
|
expectedSamplesCount,
|
|
message,
|
|
}) {
|
|
const msg = message ? `(${message})` : "";
|
|
assertValidGleanMetric({
|
|
metricId,
|
|
gleanMetric,
|
|
gleanMetricConstructor,
|
|
msg,
|
|
});
|
|
const gleanData = gleanMetric.testGetValue();
|
|
Assert.notEqual(
|
|
gleanData,
|
|
undefined,
|
|
`Got some sample for Glean metric ${metricId} ${msg}`
|
|
);
|
|
Assert.equal(
|
|
valueSum(gleanData.values),
|
|
expectedSamplesCount,
|
|
`Got the expected number of samples for Glean metric ${metricId} ${msg}`
|
|
);
|
|
}
|
|
|
|
function assertGleanLabeledCounter({
|
|
metricId,
|
|
gleanMetric,
|
|
gleanMetricLabels,
|
|
expectedLabelsValue,
|
|
ignoreNonExpectedLabels,
|
|
ignoreUnknownLabels,
|
|
message,
|
|
}) {
|
|
const { GleanLabeled } = globalThis;
|
|
const msg = message ? `(${message})` : "";
|
|
if (!Array.isArray(gleanMetricLabels) || !gleanMetricLabels.length) {
|
|
throw new Error(
|
|
`Missing mandatory gleanMetricLabels property ${msg}: ${gleanMetricLabels}`
|
|
);
|
|
}
|
|
|
|
if (!(gleanMetric instanceof GleanLabeled)) {
|
|
throw new Error(
|
|
`Glean metric "${metricId}" should be an instance of GleanLabeled: ${gleanMetric} ${msg}`
|
|
);
|
|
}
|
|
|
|
for (const label of gleanMetricLabels) {
|
|
const expectedLabelValue = expectedLabelsValue[label];
|
|
if (ignoreNonExpectedLabels && !(label in expectedLabelsValue)) {
|
|
continue;
|
|
}
|
|
Assert.deepEqual(
|
|
gleanMetric[label].testGetValue(),
|
|
expectedLabelValue,
|
|
`Expect Glean "${metricId}" metric label "${label}" to be ${
|
|
expectedLabelValue > 0 ? expectedLabelValue : "empty"
|
|
}`
|
|
);
|
|
}
|
|
|
|
if (!ignoreUnknownLabels) {
|
|
Assert.deepEqual(
|
|
gleanMetric["__other__"].testGetValue(), // eslint-disable-line dot-notation
|
|
undefined,
|
|
`Expect Glean "${metricId}" metric label "__other__" to be empty.`
|
|
);
|
|
}
|
|
}
|
|
|
|
function assertGleanLabeledCounterEmpty({
|
|
metricId,
|
|
gleanMetric,
|
|
gleanMetricLabels,
|
|
message,
|
|
}) {
|
|
// All empty labels passed to the other helpers to make it
|
|
// assert that all labels are empty.
|
|
assertGleanLabeledCounter({
|
|
metricId,
|
|
gleanMetric,
|
|
gleanMetricLabels,
|
|
expectedLabelsValue: {},
|
|
message,
|
|
});
|
|
}
|
|
|
|
function assertGleanLabeledCounterNotEmpty({
|
|
metricId,
|
|
gleanMetric,
|
|
expectedNotEmptyLabels,
|
|
ignoreUnknownLabels,
|
|
message,
|
|
}) {
|
|
const { GleanLabeled } = globalThis;
|
|
const msg = message ? `(${message})` : "";
|
|
if (
|
|
!Array.isArray(expectedNotEmptyLabels) ||
|
|
!expectedNotEmptyLabels.length
|
|
) {
|
|
throw new Error(
|
|
`Missing mandatory expectedNotEmptyLabels property ${msg}: ${expectedNotEmptyLabels}`
|
|
);
|
|
}
|
|
|
|
if (!(gleanMetric instanceof GleanLabeled)) {
|
|
throw new Error(
|
|
`Glean metric "${metricId}" should be an instance of GleanLabeled: ${gleanMetric} ${msg}`
|
|
);
|
|
}
|
|
|
|
for (const label of expectedNotEmptyLabels) {
|
|
Assert.notEqual(
|
|
gleanMetric[label].testGetValue(),
|
|
undefined,
|
|
`Expect Glean "${metricId}" metric label "${label}" to not be empty`
|
|
);
|
|
}
|
|
|
|
if (!ignoreUnknownLabels) {
|
|
Assert.deepEqual(
|
|
gleanMetric["__other__"].testGetValue(), // eslint-disable-line dot-notation
|
|
undefined,
|
|
`Expect Glean "${metricId}" metric label "__other__" to be empty.`
|
|
);
|
|
}
|
|
}
|
|
|
|
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;
|
|
|
|
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;
|
|
|
|
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;
|
|
|
|
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,
|
|
});
|
|
}
|
|
}
|
|
}
|