/* 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/. */
/* eslint-env node */
/* eslint-disable mozilla/avoid-Date-timing */
/* eslint-disable no-unsanitized/method */
const fs = require(
"fs");
const os = require(
"os");
const path = require(
"path");
const { exec } = require(
"node:child_process");
async
function getBrowsertimeResultsPath(context, commands, createDirectories) {
// Import needs to be done here because importing at the top-level
// requires a wrapped async function call, but that import can then
// only be used within the wrapped async call. Outside of it, the imported
// variable is undefined.
let pathToFolder;
if (os.type() ==
"Windows_NT") {
pathToFolder = await
import(
`file:
//${process.env.BROWSERTIME_ROOT.replace(
"\\",
"/"
)}/node_modules/browsertime/lib/support/pathToFolder.js`
);
}
else {
pathToFolder = await
import(
path.join(
process.env.BROWSERTIME_ROOT,
"node_modules",
"browsertime",
"lib",
"support",
"pathToFolder.js"
)
);
}
const browsertimeResultsPath = path.join(
context.options.resultDir,
await pathToFolder.pathToFolder(
commands.measure.result[0].browserScripts.pageinfo.url,
context.options
)
);
if (createDirectories) {
try {
await fs.promises.mkdir(browsertimeResultsPath, { recursive:
true });
}
catch (err) {
context.log.info(
`Failed to create browsertime results path directories: ${err}`
);
}
}
return browsertimeResultsPath;
}
async
function moveToBrowsertimeResultsPath(
destFilename,
srcFilepath,
context,
commands
) {
const browsertimeResultsPath = await getBrowsertimeResultsPath(
context,
commands,
true
);
const destFilepath = path.join(browsertimeResultsPath, destFilename);
try {
await fs.promises.rename(srcFilepath, destFilepath);
}
catch (err) {
context.log.info(
`Failed to rename/copy file into browsertime results: ${err}`
);
}
return destFilepath;
}
function logCommands(commands, logger, command, printFirstArg) {
let object = commands;
let path = command.split(
".");
while (path.length > 1) {
object = object[path.shift()];
}
let methodName = path[0];
let originalFun = object[methodName];
object[methodName] = async
function () {
let logString =
": " + command;
if (printFirstArg && arguments.length) {
logString +=
": " + arguments[0];
}
logger.info(
"BEGIN" + logString);
let rv = await originalFun.apply(object, arguments);
logger.info(
"END" + logString);
return rv;
};
}
async
function logTask(context, logString, task) {
context.log.info(
"BEGIN: " + logString);
let rv = await task();
context.log.info(
"END: " + logString);
return rv;
}
let startedProfiling =
false;
let childPromise, child, profilePath, profileFilename;
async
function startWindowsPowerProfiling(iterationIndex) {
let canPowerProfile =
os.type() ==
"Windows_NT" &&
/10.0.2[2-9]/.test(os.release()) &&
process.env.XPCSHELL_PATH;
if (canPowerProfile && !startedProfiling) {
startedProfiling =
true;
profileFilename = `profile_power_${iterationIndex}.json`;
profilePath = process.env.MOZ_UPLOAD_DIR +
"\\" + profileFilename;
childPromise =
new Promise(resolve => {
child = exec(
process.env.XPCSHELL_PATH,
{
env: {
MOZ_PROFILER_STARTUP:
"1",
MOZ_PROFILER_STARTUP_FEATURES:
"power,nostacksampling,notimerresolutionchange",
MOZ_PROFILER_SHUTDOWN: profilePath,
},
},
(error, stdout, stderr) => {
if (error) {
console.log(
"DEBUG ERROR", error);
}
if (stderr) {
console.log(
"DEBUG stderr", error);
}
resolve(stdout);
}
);
});
}
}
async
function stopWindowsPowerProfiling() {
if (startedProfiling) {
startedProfiling =
false;
child.stdin.end(
"quit()");
await childPromise;
}
}
async
function gatherWindowsPowerUsage(testTimes) {
let powerDataEntries = [];
if (profilePath) {
let profile;
try {
profile = JSON.parse(await fs.readFileSync(profilePath,
"utf8"));
}
catch (err) {
throw Error(`Failed to read the profile file: ${err}`);
}
for (let [start, end] of testTimes) {
start -= profile.meta.startTime;
end -= profile.meta.startTime;
let powerData = {
cpu_cores: [],
cpu_package: [],
gpu: [],
};
for (let counter of profile.counters) {
let field =
"";
if (counter.name ==
"Power: iGPU") {
field =
"gpu";
}
else if (counter.name ==
"Power: CPU package") {
field =
"cpu_package";
}
else if (counter.name ==
"Power: CPU cores") {
field =
"cpu_cores";
}
else {
continue;
}
let accumulatedPower = 0;
for (let i = 0; i < counter.samples.data.length; ++i) {
let time = counter.samples.data[i][counter.samples.schema.time];
if (time < start) {
continue;
}
if (time > end) {
break;
}
accumulatedPower +=
counter.samples.data[i][counter.samples.schema.count];
}
powerData[field].push(accumulatedPower);
}
powerDataEntries.push(powerData);
}
return powerDataEntries;
}
return null;
}
function logTest(name, test) {
return async
function wrappedTest(context, commands) {
let testTimes = [];
let start;
let originalStart = commands.measure.start;
commands.measure.start =
function () {
start = Date.now();
return originalStart.apply(commands.measure, arguments);
};
let originalStop = commands.measure.stop;
commands.measure.stop =
function () {
testTimes.push([start, Date.now()]);
return originalStop.apply(commands.measure, arguments);
};
for (let [commandName, printFirstArg] of [
[
"addText.bySelector",
true],
[
"android.shell",
true],
[
"click.byXpath",
true],
[
"click.byXpathAndWait",
true],
[
"js.run",
false],
[
"js.runAndWait",
false],
[
"js.runPrivileged",
false],
[
"measure.add",
true],
[
"measure.addObject",
false],
[
"measure.start",
true],
[
"measure.stop",
false],
[
"mouse.doubleClick.bySelector",
true],
[
"mouse.doubleClick.byXpath",
true],
[
"mouse.singleClick.bySelector",
true],
[
"navigate",
true],
[
"profiler.start",
false],
[
"profiler.stop",
false],
[
"trace.start",
false],
[
"trace.stop",
false],
[
"wait.byTime",
true],
]) {
logCommands(commands, context.log, commandName, printFirstArg);
}
if (context.options.browsertime.support_class) {
await startWindowsPowerProfiling(context.index);
}
let iterationName =
"iteration";
if (
context.options.firefox.geckoProfiler ||
context.options.browsertime.expose_profiler ===
"true"
) {
iterationName =
"profiling iteration";
}
let logString = `: ${iterationName} ${context.index}: ${name}`;
context.log.info(
"BEGIN" + logString);
let rv = await test(context, commands);
context.log.info(
"END" + logString);
if (context.options.browsertime.support_class) {
await stopWindowsPowerProfiling();
let powerData = await gatherWindowsPowerUsage(testTimes);
if (powerData?.length) {
// Move the profile to the appropriate location in the browsertime results folder
await moveToBrowsertimeResultsPath(
profileFilename,
profilePath,
context,
commands
);
powerData.forEach((powerUsage, ind) => {
if (!commands.measure.result[ind].extras.powerUsage) {
commands.measure.result[ind].extras.powerUsagePageload = [];
}
commands.measure.result[ind].extras.powerUsagePageload.push({
powerUsagePageload: powerUsage,
});
});
}
}
return rv;
};
}
module.exports = {
logTest,
logTask,
gatherWindowsPowerUsage,
getBrowsertimeResultsPath,
moveToBrowsertimeResultsPath,
startWindowsPowerProfiling,
stopWindowsPowerProfiling,
};