profiling.js (8510B)
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 /* eslint-disable no-unsanitized/method */ 8 9 const fs = require("fs"); 10 const os = require("os"); 11 const path = require("path"); 12 const { exec } = require("node:child_process"); 13 14 async function getBrowsertimeResultsPath(context, commands, createDirectories) { 15 // Import needs to be done here because importing at the top-level 16 // requires a wrapped async function call, but that import can then 17 // only be used within the wrapped async call. Outside of it, the imported 18 // variable is undefined. 19 let pathToFolder; 20 if (os.type() == "Windows_NT") { 21 pathToFolder = await import( 22 `file://${process.env.BROWSERTIME_ROOT.replace( 23 "\\", 24 "/" 25 )}/node_modules/browsertime/lib/support/pathToFolder.js` 26 ); 27 } else { 28 pathToFolder = await import( 29 path.join( 30 process.env.BROWSERTIME_ROOT, 31 "node_modules", 32 "browsertime", 33 "lib", 34 "support", 35 "pathToFolder.js" 36 ) 37 ); 38 } 39 40 const browsertimeResultsPath = path.join( 41 context.options.resultDir, 42 await pathToFolder.pathToFolder( 43 commands.measure.result[0].browserScripts.pageinfo.url, 44 context.options 45 ) 46 ); 47 48 if (createDirectories) { 49 try { 50 await fs.promises.mkdir(browsertimeResultsPath, { recursive: true }); 51 } catch (err) { 52 context.log.info( 53 `Failed to create browsertime results path directories: ${err}` 54 ); 55 } 56 } 57 58 return browsertimeResultsPath; 59 } 60 61 async function moveToBrowsertimeResultsPath( 62 destFilename, 63 srcFilepath, 64 context, 65 commands 66 ) { 67 const browsertimeResultsPath = await getBrowsertimeResultsPath( 68 context, 69 commands, 70 true 71 ); 72 const destFilepath = path.join(browsertimeResultsPath, destFilename); 73 74 try { 75 await fs.promises.rename(srcFilepath, destFilepath); 76 } catch (err) { 77 context.log.info( 78 `Failed to rename/copy file into browsertime results: ${err}` 79 ); 80 } 81 82 return destFilepath; 83 } 84 85 function logCommands(commands, logger, command, printFirstArg) { 86 let object = commands; 87 let path = command.split("."); 88 while (path.length > 1) { 89 object = object[path.shift()]; 90 } 91 let methodName = path[0]; 92 let originalFun = object[methodName]; 93 object[methodName] = async function () { 94 let logString = ": " + command; 95 if (printFirstArg && arguments.length) { 96 logString += ": " + arguments[0]; 97 } 98 logger.info("BEGIN" + logString); 99 let rv = await originalFun.apply(object, arguments); 100 logger.info("END" + logString); 101 return rv; 102 }; 103 } 104 105 async function logTask(context, logString, task) { 106 context.log.info("BEGIN: " + logString); 107 let rv = await task(); 108 context.log.info("END: " + logString); 109 110 return rv; 111 } 112 113 let startedProfiling = false; 114 let childPromise, child, profilePath, profileFilename; 115 async function startWindowsPowerProfiling(iterationIndex) { 116 let canPowerProfile = 117 os.type() == "Windows_NT" && 118 /10.0.2[2-9]/.test(os.release()) && 119 process.env.XPCSHELL_PATH; 120 121 if (canPowerProfile && !startedProfiling) { 122 startedProfiling = true; 123 124 profileFilename = `profile_power_${iterationIndex}.json`; 125 profilePath = process.env.MOZ_UPLOAD_DIR + "\\" + profileFilename; 126 childPromise = new Promise(resolve => { 127 child = exec( 128 process.env.XPCSHELL_PATH, 129 { 130 env: { 131 MOZ_PROFILER_STARTUP: "1", 132 MOZ_PROFILER_STARTUP_FEATURES: 133 "power,nostacksampling,notimerresolutionchange", 134 MOZ_PROFILER_SHUTDOWN: profilePath, 135 }, 136 }, 137 (error, stdout, stderr) => { 138 if (error) { 139 console.log("DEBUG ERROR", error); 140 } 141 if (stderr) { 142 console.log("DEBUG stderr", error); 143 } 144 resolve(stdout); 145 } 146 ); 147 }); 148 } 149 } 150 151 async function stopWindowsPowerProfiling() { 152 if (startedProfiling) { 153 startedProfiling = false; 154 child.stdin.end("quit()"); 155 await childPromise; 156 } 157 } 158 159 async function gatherWindowsPowerUsage(testTimes) { 160 let powerDataEntries = []; 161 162 if (profilePath) { 163 let profile; 164 165 try { 166 profile = JSON.parse(await fs.readFileSync(profilePath, "utf8")); 167 } catch (err) { 168 throw Error(`Failed to read the profile file: ${err}`); 169 } 170 171 for (let [start, end] of testTimes) { 172 start -= profile.meta.startTime; 173 end -= profile.meta.startTime; 174 let powerData = { 175 cpu_cores: [], 176 cpu_package: [], 177 gpu: [], 178 }; 179 180 for (let counter of profile.counters) { 181 let field = ""; 182 if (counter.name == "Power: iGPU") { 183 field = "gpu"; 184 } else if (counter.name == "Power: CPU package") { 185 field = "cpu_package"; 186 } else if (counter.name == "Power: CPU cores") { 187 field = "cpu_cores"; 188 } else { 189 continue; 190 } 191 192 let accumulatedPower = 0; 193 for (let i = 0; i < counter.samples.data.length; ++i) { 194 let time = counter.samples.data[i][counter.samples.schema.time]; 195 if (time < start) { 196 continue; 197 } 198 if (time > end) { 199 break; 200 } 201 accumulatedPower += 202 counter.samples.data[i][counter.samples.schema.count]; 203 } 204 powerData[field].push(accumulatedPower); 205 } 206 207 powerDataEntries.push(powerData); 208 } 209 210 return powerDataEntries; 211 } 212 return null; 213 } 214 215 function logTest(name, test) { 216 return async function wrappedTest(context, commands) { 217 let testTimes = []; 218 219 let start; 220 let originalStart = commands.measure.start; 221 commands.measure.start = function () { 222 start = Date.now(); 223 return originalStart.apply(commands.measure, arguments); 224 }; 225 let originalStop = commands.measure.stop; 226 commands.measure.stop = function () { 227 testTimes.push([start, Date.now()]); 228 return originalStop.apply(commands.measure, arguments); 229 }; 230 231 for (let [commandName, printFirstArg] of [ 232 ["addText.bySelector", true], 233 ["android.shell", true], 234 ["click.byXpath", true], 235 ["click.byXpathAndWait", true], 236 ["js.run", false], 237 ["js.runAndWait", false], 238 ["js.runPrivileged", false], 239 ["measure.add", true], 240 ["measure.addObject", false], 241 ["measure.start", true], 242 ["measure.stop", false], 243 ["mouse.doubleClick.bySelector", true], 244 ["mouse.doubleClick.byXpath", true], 245 ["mouse.singleClick.bySelector", true], 246 ["navigate", true], 247 ["profiler.start", false], 248 ["profiler.stop", false], 249 ["trace.start", false], 250 ["trace.stop", false], 251 ["wait.byTime", true], 252 ]) { 253 logCommands(commands, context.log, commandName, printFirstArg); 254 } 255 256 if (context.options.browsertime.support_class) { 257 await startWindowsPowerProfiling(context.index); 258 } 259 260 let iterationName = "iteration"; 261 if ( 262 context.options.firefox.geckoProfiler || 263 context.options.browsertime.expose_profiler === "true" 264 ) { 265 iterationName = "profiling iteration"; 266 } 267 let logString = `: ${iterationName} ${context.index}: ${name}`; 268 context.log.info("BEGIN" + logString); 269 let rv = await test(context, commands); 270 context.log.info("END" + logString); 271 272 if (context.options.browsertime.support_class) { 273 await stopWindowsPowerProfiling(); 274 let powerData = await gatherWindowsPowerUsage(testTimes); 275 276 if (powerData?.length) { 277 // Move the profile to the appropriate location in the browsertime results folder 278 await moveToBrowsertimeResultsPath( 279 profileFilename, 280 profilePath, 281 context, 282 commands 283 ); 284 285 powerData.forEach((powerUsage, ind) => { 286 if (!commands.measure.result[ind].extras.powerUsage) { 287 commands.measure.result[ind].extras.powerUsagePageload = []; 288 } 289 commands.measure.result[ind].extras.powerUsagePageload.push({ 290 powerUsagePageload: powerUsage, 291 }); 292 }); 293 } 294 } 295 296 return rv; 297 }; 298 } 299 300 module.exports = { 301 logTest, 302 logTask, 303 gatherWindowsPowerUsage, 304 getBrowsertimeResultsPath, 305 moveToBrowsertimeResultsPath, 306 startWindowsPowerProfiling, 307 stopWindowsPowerProfiling, 308 };