/* 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 */ const os = require("os"); const path = require("path"); const fs = require("fs"); const usbPowerProfiler = require( path.join( process.env.BROWSERTIME_ROOT, "node_modules", "usb-power-profiling", "usb-power-profiling.js" ) ); const { gatherWindowsPowerUsage, getBrowsertimeResultsPath, startWindowsPowerProfiling, stopWindowsPowerProfiling, } = require("./profiling"); class SupportMeasurements { constructor(context, commands, measureCPU, measurePower, measureTime) { this.context = context; this.commands = commands; this.testTimes = []; this.isWindows11 = os.type() == "Windows_NT" && /10.0.2[2-9]/.test(os.release()); this.isAndroid = context.options.android && context.options.android.enabled == "true"; this.application = context.options.browser; if (this.isAndroid) { if (this.application == "firefox") { this.androidPackage = this.context.options.firefox.android.package; } else if (this.application == "chrome") { this.androidPackage = "com.android.chrome"; } else { this.androidPackage = this.context.options.chrome.android.package; } } this.measurementMap = { cpuTime: { run: measureCPU, initialize: null, start: "_startMeasureCPU", stop: "_stopMeasureCPU", finalize: null, }, powerUsageSupport: { run: measurePower, initialize: "_initializeMeasurePower", start: "_startMeasurePower", stop: "_stopMeasurePower", finalize: "_finalizeMeasurePower", }, "wallclock-for-tracking-only": { run: measureTime, initialize: null, start: "_startMeasureTime", stop: "_stopMeasureTime", finalize: null, }, }; } async _gatherAndroidCPUTimes() { this.processIDs = await this.commands.android.shell( `pgrep -f "${this.androidPackage}"` ); let processTimes = {}; for (let processID of this.processIDs.split("\n")) { let processTimeInfo = ( await this.commands.android.shell(`ps -p ${processID} -o name=,time=`) ).trim(); if (!processTimeInfo) { // Sometimes a processID returns empty info continue; } let nameAndTime = processTimeInfo.split(" "); nameAndTime.forEach(el => el.trim()); let hmsTime = nameAndTime[nameAndTime.length - 1].split(":"); processTimes[nameAndTime[0]] = parseInt(hmsTime[0], 10) * 60 * 60 + parseInt(hmsTime[1], 10) * 60 + parseInt(hmsTime[2], 10); } return processTimes; } async _startMeasureCPU() { this.context.log.info("Starting CPU Time measurements"); if (!this.isAndroid) { this.startCPUTimes = os.cpus().map(c => c.times); } else { this.startCPUTimes = await this._gatherAndroidCPUTimes(); } } async _stopMeasureCPU(measurementName) { let totalTime = 0; if (!this.isAndroid) { let endCPUTimes = os.cpus().map(c => c.times); totalTime = endCPUTimes .map( (times, i) => times.user - this.startCPUTimes[i].user + (times.sys - this.startCPUTimes[i].sys) ) .reduce((currSum, val) => currSum + val, 0); } else { let endCPUTimes = await this._gatherAndroidCPUTimes(); for (let processName in endCPUTimes) { if (Object.hasOwn(this.startCPUTimes, processName)) { totalTime += endCPUTimes[processName] - this.startCPUTimes[processName]; } else { // Assumes that the process was started during the test totalTime += endCPUTimes[processName]; } } // Convert to ms totalTime = totalTime * 1000; } this.context.log.info(`Total CPU time: ${totalTime}ms`); this.commands.measure.addObject({ [measurementName]: [totalTime], }); } async _initializeMeasurePower() { this.context.log.info("Initializing power usage measurements"); if (this.isAndroid) { await usbPowerProfiler.startSampling(); } else if (this.isWindows11) { await startWindowsPowerProfiling(this.context.index); } } async _startMeasurePower() { this.context.log.info("Starting power usage measurements"); this.startPowerTime = Date.now(); } async _stopMeasurePower(measurementName) { this.context.log.info("Taking power usage measurements"); if (this.isAndroid) { let powerUsageData = await usbPowerProfiler.getPowerData( this.startPowerTime, Date.now() ); let powerUsage = powerUsageData[0].samples.data.reduce( (currSum, currVal) => currSum + Number.parseInt(currVal[1]), 0 ); const powerProfile = await usbPowerProfiler.profileFromData(); const browsertimeResultsPath = await getBrowsertimeResultsPath( this.context, this.commands, true ); const data = JSON.stringify(powerProfile, undefined, 2); await fs.promises.writeFile( path.join( browsertimeResultsPath, `profile_power_${this.context.index}.json` ), data ); this.commands.measure.addObject({ [measurementName]: [powerUsage], }); } else if (this.isWindows11) { this.testTimes.push([this.startPowerTime, Date.now()]); } } async _finalizeMeasurePower() { this.context.log.info("Finalizing power usage measurements"); if (this.isAndroid) { await usbPowerProfiler.stopSampling(); await usbPowerProfiler.resetPowerData(); } else if (this.isWindows11) { await stopWindowsPowerProfiling(); let powerData = await gatherWindowsPowerUsage(this.testTimes); powerData.forEach((powerUsage, ind) => { if (!this.commands.measure.result[ind].extras.powerUsageSupport) { this.commands.measure.result[ind].extras.powerUsageSupport = []; } this.commands.measure.result[ind].extras.powerUsageSupport.push({ powerUsageSupport: powerUsage, }); }); } } async _startMeasureTime() { this.context.log.info("Starting wallclock measurement"); this.startTime = performance.now(); } async _stopMeasureTime(measurementName) { this.context.log.info("Taking wallclock measurement"); this.commands.measure.addObject({ [measurementName]: [ parseFloat((performance.now() - this.startTime).toFixed(2)), ], }); } async reset(context, commands) { this.testTimes = []; this.context = context; this.commands = commands; } async initialize() { for (let measurementName in this.measurementMap) { let measurementInfo = this.measurementMap[measurementName]; if (!(measurementInfo.run && measurementInfo.initialize)) { continue; } await this[measurementInfo.initialize](measurementName); } } async start() { for (let measurementName in this.measurementMap) { let measurementInfo = this.measurementMap[measurementName]; if (!(measurementInfo.run && measurementInfo.start)) { continue; } await this[measurementInfo.start](measurementName); } } async stop() { for (let measurementName in this.measurementMap) { let measurementInfo = this.measurementMap[measurementName]; if (!(measurementInfo.run && measurementInfo.stop)) { continue; } await this[measurementInfo.stop](measurementName); } } async finalize() { for (let measurementName in this.measurementMap) { let measurementInfo = this.measurementMap[measurementName]; if (!(measurementInfo.run && measurementInfo.finalize)) { continue; } await this[measurementInfo.finalize](measurementName); } } } let supportMeasurementObj; async function initializeMeasurements( context, commands, measureCPU, measurePower, measureTime ) { if (!supportMeasurementObj) { supportMeasurementObj = new SupportMeasurements( context, commands, measureCPU, measurePower, measureTime ); } await supportMeasurementObj.initialize(); } async function startMeasurements(context, commands) { if (!supportMeasurementObj) { throw new Error( "initializeMeasurements must be called before startMeasurements" ); } await supportMeasurementObj.reset(context, commands); await supportMeasurementObj.start(); } async function stopMeasurements() { if (!supportMeasurementObj) { throw new Error( "initializeMeasurements must be called before stopMeasurements" ); } await supportMeasurementObj.stop(); } async function finalizeMeasurements() { if (!supportMeasurementObj) { throw new Error( "initializeMeasurements must be called before finalizeMeasurements" ); } await supportMeasurementObj.finalize(); } module.exports = { SupportMeasurements, initializeMeasurements, startMeasurements, stopMeasurements, finalizeMeasurements, };