tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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 };