Source code

Revision control

Copy as Markdown

Other Tools

/* This Source Code Form is subject to the terms of 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 strict";
var HandshakeTelemetryHelpers = {
FLAVORS: ["", "_FIRST_TRY", "_CONSERVATIVE", "_ECH", "_ECH_GREASE"],
_baseline: null,
_fogInitialized: false,
/**
* Returns the Glean ssl_handshake.result_* metric for a given flavor.
*/
resultMetric(flavor) {
switch (flavor) {
case "":
return Glean.sslHandshake.result;
case "_FIRST_TRY":
return Glean.sslHandshake.resultFirstTry;
case "_CONSERVATIVE":
return Glean.sslHandshake.resultConservative;
case "_ECH":
return Glean.sslHandshake.resultEch;
case "_ECH_GREASE":
return Glean.sslHandshake.resultEchGrease;
default:
Assert.ok(false, `Unknown result flavor: ${flavor}`);
return null;
}
},
/**
* Returns the Glean ssl.time_until_ready_* metric for a given flavor.
*/
timingMetric(flavor) {
switch (flavor) {
case "":
return Glean.ssl.timeUntilReady;
case "_FIRST_TRY":
return Glean.ssl.timeUntilReadyFirstTry;
case "_CONSERVATIVE":
return Glean.ssl.timeUntilReadyConservative;
case "_ECH":
return Glean.ssl.timeUntilReadyEch;
case "_ECH_GREASE":
return Glean.ssl.timeUntilReadyEchGrease;
default:
Assert.ok(false, `Unknown timing flavor: ${flavor}`);
return null;
}
},
/**
* Computes the per-bucket difference between two distribution snapshots.
* Returns a values object containing only non-zero deltas.
*/
_deltaValues(current, baseline) {
let result = {};
let currentValues = current?.values ?? {};
let baselineValues = baseline?.values ?? {};
let allKeys = new Set([
...Object.keys(currentValues),
...Object.keys(baselineValues),
]);
for (let k of allKeys) {
let delta = (currentValues[k] ?? 0) - (baselineValues[k] ?? 0);
if (delta !== 0) {
result[k] = delta;
}
}
return result;
},
/**
* Returns a delta snapshot for a result metric since the last
* resetHistograms() call. The returned object has a .values property
* compatible with assertHistogramMap.
*/
resultDelta(flavor) {
let current = this.resultMetric(flavor).testGetValue();
let baseline = this._baseline?.[`result${flavor}`];
return { values: this._deltaValues(current, baseline) };
},
/**
* Returns the delta sample count for a timing metric since the last
* resetHistograms() call.
*/
timingDeltaCount(flavor) {
let current = this.timingMetric(flavor).testGetValue();
let baseline = this._baseline?.[`timing${flavor}`];
return (current?.count ?? 0) - (baseline?.count ?? 0);
},
/**
* Assert that the distribution values match expected entries exactly.
* All non-expected bucket entries must be zero.
*
* @param {object} snapshot A distribution snapshot with a .values property.
* @param {Map} expectedEntries Map of bucket index (string) to expected count.
*/
assertHistogramMap(snapshot, expectedEntries) {
Assert.ok(
!mozinfo.socketprocess_networking,
"Metrics don't populate on network process"
);
let values = JSON.parse(JSON.stringify(snapshot));
for (let [Tk, Tv] of expectedEntries.entries()) {
let found = false;
for (let [i, val] of Object.entries(values.values)) {
if (i == Tk) {
found = true;
Assert.equal(val, Tv, `expected counts should match at index ${i}`);
values.values[i] = 0; // Reset the value
}
}
Assert.ok(found, `Should have found an entry at index ${Tk}`);
}
for (let k in values.values) {
Assert.equal(
values.values[k],
0,
`Should NOT have found an entry at index ${k} of value ${values.values[k]}`
);
}
},
/**
* Snapshots current metric values as a baseline. Subsequent check methods
* compare against this baseline, measuring only what changed.
*/
resetHistograms() {
if (!this._fogInitialized) {
Services.fog.initializeFOG();
this._fogInitialized = true;
}
info("Snapshotting TLS handshake metric baseline");
this._baseline = {};
for (let f of this.FLAVORS) {
this._baseline[`result${f}`] = this.resultMetric(f).testGetValue();
this._baseline[`timing${f}`] = this.timingMetric(f).testGetValue();
}
},
/**
* Checks that all TLS Handshake metrics of a particular flavor have
* exactly resultCount new entries for the resultCode and no other new
* entries since the last resetHistograms() call.
*
* @param {Array} flavors An array of strings corresponding to which types
* of metrics should have entries. See
* HandshakeTelemetryHelpers.FLAVORS.
* @param {number} resultCode The expected result code, see sslerr.h. 0 is success, all others are errors.
* @param {number} resultCount The number of handshake results expected.
*/
checkEntry(flavors, resultCode, resultCount) {
Assert.ok(
!mozinfo.socketprocess_networking,
"Metrics don't populate on network process"
);
for (let f of flavors) {
let delta = this.resultDelta(f);
info(
`checkEntry: result${f} expecting ${resultCount}x code ${resultCode}, ` +
`got delta: ${JSON.stringify(delta.values)}`
);
this.assertHistogramMap(
delta,
new Map([[String(resultCode), resultCount]])
);
}
// Timing metrics should only contain values if we expected success.
for (let f of flavors) {
let deltaCount = this.timingDeltaCount(f);
let expectedCount = resultCode === 0 ? resultCount : 0;
info(
`checkEntry: timing${f} expecting delta count=${expectedCount}, ` +
`got delta count=${deltaCount}`
);
Assert.strictEqual(
deltaCount,
expectedCount,
resultCode === 0
? "Timing entry count correct"
: "No timing entries expected"
);
}
},
checkSuccess(flavors, resultCount = 1) {
this.checkEntry(flavors, 0, resultCount);
},
checkEmpty(flavors) {
for (let f of flavors) {
let resultDelta = this.resultDelta(f);
let resultDeltaCount = Object.values(resultDelta.values).reduce(
(a, b) => a + b,
0
);
info(
`checkEmpty: result${f} expecting no new entries, ` +
`got delta count=${resultDeltaCount}`
);
Assert.strictEqual(
resultDeltaCount,
0,
`No new result entries expected for '${f}'. Delta: ${JSON.stringify(
resultDelta.values
)}`
);
let timingDeltaCount = this.timingDeltaCount(f);
info(
`checkEmpty: timing${f} expecting no new entries, ` +
`got delta count=${timingDeltaCount}`
);
Assert.strictEqual(
timingDeltaCount,
0,
`No new timing entries expected for '${f}'.`
);
}
},
};