support_measurements.js (9335B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 /* eslint-env node */ 6 /* eslint-disable mozilla/avoid-Date-timing */ 7 8 const os = require("os"); 9 const path = require("path"); 10 const fs = require("fs"); 11 12 const usbPowerProfiler = require( 13 path.join( 14 process.env.BROWSERTIME_ROOT, 15 "node_modules", 16 "usb-power-profiling", 17 "usb-power-profiling.js" 18 ) 19 ); 20 21 const { 22 gatherWindowsPowerUsage, 23 getBrowsertimeResultsPath, 24 startWindowsPowerProfiling, 25 stopWindowsPowerProfiling, 26 } = require("./profiling"); 27 28 class SupportMeasurements { 29 constructor(context, commands, measureCPU, measurePower, measureTime) { 30 this.context = context; 31 this.commands = commands; 32 this.testTimes = []; 33 34 this.isWindows11 = 35 os.type() == "Windows_NT" && /10.0.2[2-9]/.test(os.release()); 36 37 this.isAndroid = 38 context.options.android && context.options.android.enabled == "true"; 39 this.application = context.options.browser; 40 41 if (this.isAndroid) { 42 if (this.application == "firefox") { 43 this.androidPackage = this.context.options.firefox.android.package; 44 } else if (this.application == "chrome") { 45 this.androidPackage = "com.android.chrome"; 46 } else { 47 this.androidPackage = this.context.options.chrome.android.package; 48 } 49 } 50 51 this.measurementMap = { 52 cpuTime: { 53 run: measureCPU, 54 initialize: null, 55 start: "_startMeasureCPU", 56 stop: "_stopMeasureCPU", 57 finalize: null, 58 }, 59 powerUsageSupport: { 60 run: measurePower, 61 initialize: "_initializeMeasurePower", 62 start: "_startMeasurePower", 63 stop: "_stopMeasurePower", 64 finalize: "_finalizeMeasurePower", 65 }, 66 "wallclock-for-tracking-only": { 67 run: measureTime, 68 initialize: null, 69 start: "_startMeasureTime", 70 stop: "_stopMeasureTime", 71 finalize: null, 72 }, 73 }; 74 } 75 76 async _gatherAndroidCPUTimes() { 77 this.processIDs = await this.commands.android.shell( 78 `pgrep -f "${this.androidPackage}"` 79 ); 80 81 let processTimes = {}; 82 for (let processID of this.processIDs.split("\n")) { 83 let processTimeInfo = ( 84 await this.commands.android.shell(`ps -p ${processID} -o name=,time=`) 85 ).trim(); 86 87 if (!processTimeInfo) { 88 // Sometimes a processID returns empty info 89 continue; 90 } 91 92 let nameAndTime = processTimeInfo.split(" "); 93 nameAndTime.forEach(el => el.trim()); 94 95 let hmsTime = nameAndTime[nameAndTime.length - 1].split(":"); 96 processTimes[nameAndTime[0]] = 97 parseInt(hmsTime[0], 10) * 60 * 60 + 98 parseInt(hmsTime[1], 10) * 60 + 99 parseInt(hmsTime[2], 10); 100 } 101 102 return processTimes; 103 } 104 105 async _startMeasureCPU() { 106 this.context.log.info("Starting CPU Time measurements"); 107 if (!this.isAndroid) { 108 this.startCPUTimes = os.cpus().map(c => c.times); 109 } else { 110 this.startCPUTimes = await this._gatherAndroidCPUTimes(); 111 } 112 } 113 114 async _stopMeasureCPU(measurementName) { 115 let totalTime = 0; 116 117 if (!this.isAndroid) { 118 let endCPUTimes = os.cpus().map(c => c.times); 119 totalTime = endCPUTimes 120 .map( 121 (times, i) => 122 times.user - 123 this.startCPUTimes[i].user + 124 (times.sys - this.startCPUTimes[i].sys) 125 ) 126 .reduce((currSum, val) => currSum + val, 0); 127 } else { 128 let endCPUTimes = await this._gatherAndroidCPUTimes(); 129 130 for (let processName in endCPUTimes) { 131 if (Object.hasOwn(this.startCPUTimes, processName)) { 132 totalTime += 133 endCPUTimes[processName] - this.startCPUTimes[processName]; 134 } else { 135 // Assumes that the process was started during the test 136 totalTime += endCPUTimes[processName]; 137 } 138 } 139 140 // Convert to ms 141 totalTime = totalTime * 1000; 142 } 143 144 this.context.log.info(`Total CPU time: ${totalTime}ms`); 145 this.commands.measure.addObject({ 146 [measurementName]: [totalTime], 147 }); 148 } 149 150 async _initializeMeasurePower() { 151 this.context.log.info("Initializing power usage measurements"); 152 if (this.isAndroid) { 153 await usbPowerProfiler.startSampling(); 154 } else if (this.isWindows11) { 155 await startWindowsPowerProfiling(this.context.index); 156 } 157 } 158 159 async _startMeasurePower() { 160 this.context.log.info("Starting power usage measurements"); 161 this.startPowerTime = Date.now(); 162 } 163 164 async _stopMeasurePower(measurementName) { 165 this.context.log.info("Taking power usage measurements"); 166 if (this.isAndroid) { 167 let powerUsageData = await usbPowerProfiler.getPowerData( 168 this.startPowerTime, 169 Date.now() 170 ); 171 let powerUsage = powerUsageData[0].samples.data.reduce( 172 (currSum, currVal) => currSum + Number.parseInt(currVal[1]), 173 0 174 ); 175 176 const powerProfile = await usbPowerProfiler.profileFromData(); 177 const browsertimeResultsPath = await getBrowsertimeResultsPath( 178 this.context, 179 this.commands, 180 true 181 ); 182 183 const data = JSON.stringify(powerProfile, undefined, 2); 184 await fs.promises.writeFile( 185 path.join( 186 browsertimeResultsPath, 187 `profile_power_${this.context.index}.json` 188 ), 189 data 190 ); 191 192 this.commands.measure.addObject({ 193 [measurementName]: [powerUsage], 194 }); 195 } else if (this.isWindows11) { 196 this.testTimes.push([this.startPowerTime, Date.now()]); 197 } 198 } 199 200 async _finalizeMeasurePower() { 201 this.context.log.info("Finalizing power usage measurements"); 202 if (this.isAndroid) { 203 await usbPowerProfiler.stopSampling(); 204 await usbPowerProfiler.resetPowerData(); 205 } else if (this.isWindows11) { 206 await stopWindowsPowerProfiling(); 207 208 let powerData = await gatherWindowsPowerUsage(this.testTimes); 209 powerData.forEach((powerUsage, ind) => { 210 if (!this.commands.measure.result[ind].extras.powerUsageSupport) { 211 this.commands.measure.result[ind].extras.powerUsageSupport = []; 212 } 213 this.commands.measure.result[ind].extras.powerUsageSupport.push({ 214 powerUsageSupport: powerUsage, 215 }); 216 }); 217 } 218 } 219 220 async _startMeasureTime() { 221 this.context.log.info("Starting wallclock measurement"); 222 this.startTime = performance.now(); 223 } 224 225 async _stopMeasureTime(measurementName) { 226 this.context.log.info("Taking wallclock measurement"); 227 this.commands.measure.addObject({ 228 [measurementName]: [ 229 parseFloat((performance.now() - this.startTime).toFixed(2)), 230 ], 231 }); 232 } 233 234 async reset(context, commands) { 235 this.testTimes = []; 236 this.context = context; 237 this.commands = commands; 238 } 239 240 async initialize() { 241 for (let measurementName in this.measurementMap) { 242 let measurementInfo = this.measurementMap[measurementName]; 243 if (!(measurementInfo.run && measurementInfo.initialize)) { 244 continue; 245 } 246 await this[measurementInfo.initialize](measurementName); 247 } 248 } 249 250 async start() { 251 for (let measurementName in this.measurementMap) { 252 let measurementInfo = this.measurementMap[measurementName]; 253 if (!(measurementInfo.run && measurementInfo.start)) { 254 continue; 255 } 256 await this[measurementInfo.start](measurementName); 257 } 258 } 259 260 async stop() { 261 for (let measurementName in this.measurementMap) { 262 let measurementInfo = this.measurementMap[measurementName]; 263 if (!(measurementInfo.run && measurementInfo.stop)) { 264 continue; 265 } 266 await this[measurementInfo.stop](measurementName); 267 } 268 } 269 270 async finalize() { 271 for (let measurementName in this.measurementMap) { 272 let measurementInfo = this.measurementMap[measurementName]; 273 if (!(measurementInfo.run && measurementInfo.finalize)) { 274 continue; 275 } 276 await this[measurementInfo.finalize](measurementName); 277 } 278 } 279 } 280 281 let supportMeasurementObj; 282 async function initializeMeasurements( 283 context, 284 commands, 285 measureCPU, 286 measurePower, 287 measureTime 288 ) { 289 if (!supportMeasurementObj) { 290 supportMeasurementObj = new SupportMeasurements( 291 context, 292 commands, 293 measureCPU, 294 measurePower, 295 measureTime 296 ); 297 } 298 299 await supportMeasurementObj.initialize(); 300 } 301 302 async function startMeasurements(context, commands) { 303 if (!supportMeasurementObj) { 304 throw new Error( 305 "initializeMeasurements must be called before startMeasurements" 306 ); 307 } 308 309 await supportMeasurementObj.reset(context, commands); 310 await supportMeasurementObj.start(); 311 } 312 313 async function stopMeasurements() { 314 if (!supportMeasurementObj) { 315 throw new Error( 316 "initializeMeasurements must be called before stopMeasurements" 317 ); 318 } 319 await supportMeasurementObj.stop(); 320 } 321 322 async function finalizeMeasurements() { 323 if (!supportMeasurementObj) { 324 throw new Error( 325 "initializeMeasurements must be called before finalizeMeasurements" 326 ); 327 } 328 await supportMeasurementObj.finalize(); 329 } 330 331 module.exports = { 332 SupportMeasurements, 333 initializeMeasurements, 334 startMeasurements, 335 stopMeasurements, 336 finalizeMeasurements, 337 };