/* 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, };