tor-browser

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

browser_startup_content_mainthreadio.js (14428B)


      1 /* Any copyright is dedicated to the Public Domain.
      2   http://creativecommons.org/publicdomain/zero/1.0/ */
      3 
      4 /* This test records I/O syscalls done on the main thread during startup.
      5 *
      6 * To run this test similar to try server, you need to run:
      7 *   ./mach package
      8 *   ./mach test --appname=dist <path to test>
      9 *
     10 * If you made changes that cause this test to fail, it's likely because you
     11 * are touching more files or directories during startup.
     12 * Most code has no reason to use main thread I/O.
     13 * If for some reason accessing the file system on the main thread is currently
     14 * unavoidable, consider defering the I/O as long as you can, ideally after
     15 * the end of startup.
     16 */
     17 
     18 "use strict";
     19 
     20 /* Set this to true only for debugging purpose; it makes the output noisy. */
     21 const kDumpAllStacks = false;
     22 
     23 // Shortcuts for conditions.
     24 const LINUX = AppConstants.platform == "linux";
     25 const WIN = AppConstants.platform == "win";
     26 const MAC = AppConstants.platform == "macosx";
     27 const FORK_SERVER = Services.prefs.getBoolPref(
     28  "dom.ipc.forkserver.enable",
     29  false
     30 );
     31 
     32 /* This is an object mapping string process types to lists of known cases
     33 * of IO happening on the main thread. Ideally, IO should not be on the main
     34 * thread, and should happen as late as possible (see above).
     35 *
     36 * Paths in the entries in these lists can:
     37 *  - be a full path, eg. "/etc/mime.types"
     38 *  - have a prefix which will be resolved using Services.dirsvc
     39 *    eg. "GreD:omni.ja"
     40 *    It's possible to have only a prefix, in thise case the directory will
     41 *    still be resolved, eg. "UAppData:"
     42 *  - use * at the begining and/or end as a wildcard
     43 * The folder separator is '/' even for Windows paths, where it'll be
     44 * automatically converted to '\'.
     45 *
     46 * Specifying 'ignoreIfUnused: true' will make the test ignore unused entries;
     47 * without this the test is strict and will fail if the described IO does not
     48 * happen.
     49 *
     50 * Each entry specifies the maximum number of times an operation is expected to
     51 * occur.
     52 * The operations currently reported by the I/O interposer are:
     53 *   create/open: only supported on Windows currently. The test currently
     54 *     ignores these markers to have a shorter initial list of IO operations.
     55 *     Adding Unix support is bug 1533779.
     56 *   stat: supported on all platforms when checking the last modified date or
     57 *     file size. Supported only on Windows when checking if a file exists;
     58 *     fixing this inconsistency is bug 1536109.
     59 *   read: supported on all platforms, but unix platforms will only report read
     60 *     calls going through NSPR.
     61 *   write: supported on all platforms, but Linux will only report write calls
     62 *     going through NSPR.
     63 *   close: supported only on Unix, and only for close calls going through NSPR.
     64 *     Adding Windows support is bug 1524574.
     65 *   fsync: supported only on Windows.
     66 *
     67 * If an entry specifies more than one operation, if at least one of them is
     68 * encountered, the test won't report a failure for the entry if other
     69 * operations are not encountered. This helps when listing cases where the
     70 * reported operations aren't the same on all platforms due to the I/O
     71 * interposer inconsistencies across platforms documented above.
     72 */
     73 const processes = {
     74  "Web Content": [
     75    {
     76      path: "GreD:omni.ja",
     77      // Visible on Windows with an open marker.
     78      // The fork server preloads the omnijars.
     79      condition: !WIN && !FORK_SERVER,
     80      stat: 1,
     81    },
     82    {
     83      // bug 1376994
     84      path: "XCurProcD:omni.ja",
     85      // Visible on Windows with an open marker.
     86      // The fork server preloads the omnijars.
     87      condition: !WIN && !FORK_SERVER,
     88      stat: 1,
     89    },
     90    {
     91      // Exists call in ScopedXREEmbed::SetAppDir
     92      path: "XCurProcD:",
     93      condition: WIN,
     94      stat: 1,
     95    },
     96    {
     97      path: "*ShaderCache*", // Bug 1660480 - seen on hardware
     98      condition: WIN,
     99      ignoreIfUnused: true,
    100      stat: 3,
    101    },
    102  ],
    103  "Privileged Content": [
    104    {
    105      path: "GreD:omni.ja",
    106      // Visible on Windows with an open marker.
    107      // The fork server preloads the omnijars.
    108      condition: !WIN && !FORK_SERVER,
    109      stat: 1,
    110    },
    111    {
    112      // bug 1376994
    113      path: "XCurProcD:omni.ja",
    114      // Visible on Windows with an open marker.
    115      // The fork server preloads the omnijars.
    116      condition: !WIN && !FORK_SERVER,
    117      stat: 1,
    118    },
    119    {
    120      // Exists call in ScopedXREEmbed::SetAppDir
    121      path: "XCurProcD:",
    122      condition: WIN,
    123      stat: 1,
    124    },
    125  ],
    126  WebExtensions: [
    127    {
    128      path: "GreD:omni.ja",
    129      // Visible on Windows with an open marker.
    130      // The fork server preloads the omnijars.
    131      condition: !WIN && !FORK_SERVER,
    132      stat: 1,
    133    },
    134    {
    135      // bug 1376994
    136      path: "XCurProcD:omni.ja",
    137      // Visible on Windows with an open marker.
    138      // The fork server preloads the omnijars.
    139      condition: !WIN && !FORK_SERVER,
    140      stat: 1,
    141    },
    142    {
    143      // Exists call in ScopedXREEmbed::SetAppDir
    144      path: "XCurProcD:",
    145      condition: WIN,
    146      stat: 1,
    147    },
    148  ],
    149 };
    150 
    151 function expandPathWithDirServiceKey(path) {
    152  if (path.includes(":")) {
    153    let [prefix, suffix] = path.split(":");
    154    let [key, property] = prefix.split(".");
    155    let dir = Services.dirsvc.get(key, Ci.nsIFile);
    156    if (property) {
    157      dir = dir[property];
    158    }
    159 
    160    // Resolve symLinks.
    161    let dirPath = dir.path;
    162    while (dir && !dir.isSymlink()) {
    163      dir = dir.parent;
    164    }
    165    if (dir) {
    166      dirPath = dirPath.replace(dir.path, dir.target);
    167    }
    168 
    169    path = dirPath;
    170 
    171    if (suffix) {
    172      path += "/" + suffix;
    173    }
    174  }
    175  if (AppConstants.platform == "win") {
    176    path = path.replace(/\//g, "\\");
    177  }
    178  return path;
    179 }
    180 
    181 function getStackFromProfile(profile, stack) {
    182  const stackPrefixCol = profile.stackTable.schema.prefix;
    183  const stackFrameCol = profile.stackTable.schema.frame;
    184  const frameLocationCol = profile.frameTable.schema.location;
    185 
    186  let result = [];
    187  while (stack) {
    188    let sp = profile.stackTable.data[stack];
    189    let frame = profile.frameTable.data[sp[stackFrameCol]];
    190    stack = sp[stackPrefixCol];
    191    frame = profile.stringTable[frame[frameLocationCol]];
    192    if (frame != "js::RunScript" && !frame.startsWith("next (self-hosted:")) {
    193      result.push(frame);
    194    }
    195  }
    196  return result;
    197 }
    198 
    199 function getIOMarkersFromProfile(profile) {
    200  const nameCol = profile.markers.schema.name;
    201  const dataCol = profile.markers.schema.data;
    202 
    203  let markers = [];
    204  for (let m of profile.markers.data) {
    205    let markerName = profile.stringTable[m[nameCol]];
    206 
    207    if (markerName != "FileIO") {
    208      continue;
    209    }
    210 
    211    let markerData = m[dataCol];
    212    if (markerData.source == "sqlite-mainthread") {
    213      continue;
    214    }
    215 
    216    let samples = markerData.stack.samples;
    217    let stack = samples.data[0][samples.schema.stack];
    218    markers.push({
    219      operation: markerData.operation,
    220      filename: markerData.filename,
    221      source: markerData.source,
    222      stackId: stack,
    223    });
    224  }
    225 
    226  return markers;
    227 }
    228 
    229 function pathMatches(path, filename) {
    230  path = path.toLowerCase();
    231  return (
    232    path == filename || // Full match
    233    // Wildcard on both sides of the path
    234    (path.startsWith("*") &&
    235      path.endsWith("*") &&
    236      filename.includes(path.slice(1, -1))) ||
    237    // Wildcard suffix
    238    (path.endsWith("*") && filename.startsWith(path.slice(0, -1))) ||
    239    // Wildcard prefix
    240    (path.startsWith("*") && filename.endsWith(path.slice(1)))
    241  );
    242 }
    243 
    244 add_task(async function () {
    245  if (
    246    !AppConstants.NIGHTLY_BUILD &&
    247    !AppConstants.MOZ_DEV_EDITION &&
    248    !AppConstants.DEBUG
    249  ) {
    250    ok(
    251      !("@mozilla.org/test/startuprecorder;1" in Cc),
    252      "the startup recorder component shouldn't exist in this non-nightly/non-devedition/" +
    253        "non-debug build."
    254    );
    255    return;
    256  }
    257 
    258  TestUtils.assertPackagedBuild();
    259 
    260  let startupRecorder =
    261    Cc["@mozilla.org/test/startuprecorder;1"].getService().wrappedJSObject;
    262  await startupRecorder.done;
    263 
    264  for (let process in processes) {
    265    processes[process] = processes[process].filter(
    266      entry => !("condition" in entry) || entry.condition
    267    );
    268    processes[process].forEach(entry => {
    269      entry.listedPath = entry.path;
    270      entry.path = expandPathWithDirServiceKey(entry.path);
    271    });
    272  }
    273 
    274  let tmpPath = expandPathWithDirServiceKey("TmpD:").toLowerCase();
    275  let shouldPass = true;
    276  for (let procName in processes) {
    277    let knownIOList = processes[procName];
    278    info(
    279      `known main thread IO paths for ${procName} process:\n` +
    280        knownIOList
    281          .map(e => {
    282            let operations = Object.keys(e)
    283              .filter(k => !["path", "condition"].includes(k))
    284              .map(k => `${k}: ${e[k]}`);
    285            return `  ${e.path} - ${operations.join(", ")}`;
    286          })
    287          .join("\n")
    288    );
    289 
    290    let profile;
    291    for (let process of startupRecorder.data.profile.processes) {
    292      if (process.threads[0].processName == procName) {
    293        profile = process.threads[0];
    294        break;
    295      }
    296    }
    297    if (procName == "Privileged Content" && !profile) {
    298      // The Privileged Content is started from an idle task that may not have
    299      // been executed yet at the time we captured the startup profile in
    300      // startupRecorder.
    301      todo(false, `profile for ${procName} process not found`);
    302    } else {
    303      ok(profile, `Found profile for ${procName} process`);
    304    }
    305    if (!profile) {
    306      continue;
    307    }
    308 
    309    let markers = getIOMarkersFromProfile(profile);
    310    for (let marker of markers) {
    311      if (marker.operation == "create/open") {
    312        // TODO: handle these I/O markers once they are supported on
    313        // non-Windows platforms.
    314        continue;
    315      }
    316 
    317      if (!marker.filename) {
    318        // We are still missing the filename on some mainthreadio markers,
    319        // these markers are currently useless for the purpose of this test.
    320        continue;
    321      }
    322 
    323      // Convert to lower case before comparing because the OS X test machines
    324      // have the 'Firefox' folder in 'Library/Application Support' created
    325      // as 'firefox' for some reason.
    326      let filename = marker.filename.toLowerCase();
    327 
    328      if (!WIN && filename == "/dev/urandom") {
    329        continue;
    330      }
    331 
    332      // /dev/shm is always tmpfs (a memory filesystem); this isn't
    333      // really I/O any more than mmap/munmap are.
    334      if (LINUX && filename.startsWith("/dev/shm/")) {
    335        continue;
    336      }
    337 
    338      // "Files" from memfd_create() are similar to tmpfs but never
    339      // exist in the filesystem; however, they have names which are
    340      // exposed in procfs, and the I/O interposer observes when
    341      // they're close()d.
    342      if (LINUX && filename.startsWith("/memfd:")) {
    343        continue;
    344      }
    345 
    346      // Shared memory uses temporary files on MacOS <= 10.11 to avoid
    347      // a kernel security bug that will never be patched (see
    348      // https://crbug.com/project-zero/1671 for details).  This can
    349      // be removed when we no longer support those OS versions.
    350      if (MAC && filename.startsWith(tmpPath + "/org.mozilla.ipc.")) {
    351        continue;
    352      }
    353 
    354      let expected = false;
    355      for (let entry of knownIOList) {
    356        if (pathMatches(entry.path, filename)) {
    357          entry[marker.operation] = (entry[marker.operation] || 0) - 1;
    358          entry._used = true;
    359          expected = true;
    360          break;
    361        }
    362      }
    363      if (!expected) {
    364        record(
    365          false,
    366          `unexpected ${marker.operation} on ${marker.filename} in ${procName} process`,
    367          undefined,
    368          "  " + getStackFromProfile(profile, marker.stackId).join("\n  ")
    369        );
    370        shouldPass = false;
    371      }
    372      info(`(${marker.source}) ${marker.operation} - ${marker.filename}`);
    373      if (kDumpAllStacks) {
    374        info(
    375          getStackFromProfile(profile, marker.stackId)
    376            .map(f => "  " + f)
    377            .join("\n")
    378        );
    379      }
    380    }
    381 
    382    if (!knownIOList.length) {
    383      continue;
    384    }
    385    if (knownIOList.some(io => !io.ignoreIfUnused)) {
    386      // The I/O interposer is disabled if RELEASE_OR_BETA, so we expect to have
    387      // no I/O marker in that case, but it's good to keep the test running to check
    388      // that we are still able to produce startup profiles.
    389      is(
    390        !!markers.length,
    391        !AppConstants.RELEASE_OR_BETA,
    392        procName +
    393          " startup profiles should have IO markers in builds that are not RELEASE_OR_BETA"
    394      );
    395      if (!markers.length) {
    396        // If a profile unexpectedly contains no I/O marker, it's better to return
    397        // early to avoid having a lot of confusing "no main thread IO when we
    398        // expected some" failures.
    399        continue;
    400      }
    401    }
    402 
    403    for (let entry of knownIOList) {
    404      for (let op in entry) {
    405        if (
    406          [
    407            "listedPath",
    408            "path",
    409            "condition",
    410            "ignoreIfUnused",
    411            "_used",
    412          ].includes(op)
    413        ) {
    414          continue;
    415        }
    416        let message = `${op} on ${entry.path} `;
    417        if (entry[op] == 0) {
    418          message += "as many times as expected";
    419        } else if (entry[op] > 0) {
    420          message += `allowed ${entry[op]} more times`;
    421        } else {
    422          message += `${entry[op] * -1} more times than expected`;
    423        }
    424        Assert.greaterOrEqual(
    425          entry[op],
    426          0,
    427          `${message} in ${procName} process`
    428        );
    429      }
    430      if (!("_used" in entry) && !entry.ignoreIfUnused) {
    431        ok(
    432          false,
    433          `no main thread IO when we expected some for process ${procName}: ${entry.path} (${entry.listedPath})`
    434        );
    435        shouldPass = false;
    436      }
    437    }
    438  }
    439 
    440  if (shouldPass) {
    441    ok(shouldPass, "No unexpected main thread I/O during startup");
    442  } else {
    443    const filename = "profile_startup_content_mainthreadio.json";
    444    let path = Services.env.get("MOZ_UPLOAD_DIR");
    445    let helpString;
    446    if (path) {
    447      let profilePath = PathUtils.join(path, filename);
    448      await IOUtils.writeJSON(profilePath, startupRecorder.data.profile);
    449      helpString = `open the ${filename} artifact in the Firefox Profiler to see what happened`;
    450    } else {
    451      helpString =
    452        "set the MOZ_UPLOAD_DIR environment variable to record a profile";
    453    }
    454    ok(
    455      false,
    456      "Unexpected main thread I/O behavior during child process startup; " +
    457        helpString
    458    );
    459  }
    460 });