tor-browser

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

browser_startup_mainthreadio.js (23418B)


      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 * If your code isn't strictly required to show the first browser window,
     17 * it shouldn't be loaded before we are done with first paint.
     18 * Finally, if your code isn't really needed during startup, it should not be
     19 * loaded before we have started handling user events.
     20 */
     21 
     22 "use strict";
     23 
     24 /* Set this to true only for debugging purpose; it makes the output noisy. */
     25 const kDumpAllStacks = false;
     26 
     27 // Shortcuts for conditions.
     28 const LINUX = AppConstants.platform == "linux";
     29 const WIN = AppConstants.platform == "win";
     30 const MAC = AppConstants.platform == "macosx";
     31 
     32 const kSharedFontList = SpecialPowers.getBoolPref("gfx.e10s.font-list.shared");
     33 
     34 /* This is an object mapping string phases of startup to lists of known cases
     35 * of IO happening on the main thread. Ideally, IO should not be on the main
     36 * thread, and should happen as late as possible (see above).
     37 *
     38 * Paths in the entries in these lists can:
     39 *  - be a full path, eg. "/etc/mime.types"
     40 *  - have a prefix which will be resolved using Services.dirsvc
     41 *    eg. "GreD:omni.ja"
     42 *    It's possible to have only a prefix, in thise case the directory will
     43 *    still be resolved, eg. "UAppData:"
     44 *  - use * at the begining and/or end as a wildcard
     45 * The folder separator is '/' even for Windows paths, where it'll be
     46 * automatically converted to '\'.
     47 *
     48 * Specifying 'ignoreIfUnused: true' will make the test ignore unused entries;
     49 * without this the test is strict and will fail if the described IO does not
     50 * happen.
     51 *
     52 * Each entry specifies the maximum number of times an operation is expected to
     53 * occur.
     54 * The operations currently reported by the I/O interposer are:
     55 *   create/open: only supported on Windows currently. The test currently
     56 *     ignores these markers to have a shorter initial list of IO operations.
     57 *     Adding Unix support is bug 1533779.
     58 *   stat: supported on all platforms when checking the last modified date or
     59 *     file size. Supported only on Windows when checking if a file exists;
     60 *     fixing this inconsistency is bug 1536109.
     61 *   read: supported on all platforms, but unix platforms will only report read
     62 *     calls going through NSPR.
     63 *   write: supported on all platforms, but Linux will only report write calls
     64 *     going through NSPR.
     65 *   close: supported only on Unix, and only for close calls going through NSPR.
     66 *     Adding Windows support is bug 1524574.
     67 *   fsync: supported only on Windows.
     68 *
     69 * If an entry specifies more than one operation, if at least one of them is
     70 * encountered, the test won't report a failure for the entry if other
     71 * operations are not encountered. This helps when listing cases where the
     72 * reported operations aren't the same on all platforms due to the I/O
     73 * interposer inconsistencies across platforms documented above.
     74 */
     75 const startupPhases = {
     76  // Anything done before or during app-startup must have a compelling reason
     77  // to run before we have even selected the user profile.
     78  "before profile selection": [
     79    {
     80      // bug 1541200
     81      path: "UAppData:Crash Reports/InstallTime20*",
     82      condition: AppConstants.MOZ_CRASHREPORTER,
     83      stat: 1, // only caught on Windows.
     84      read: 1,
     85      write: 2,
     86      close: 1,
     87    },
     88    {
     89      // bug 1541200
     90      path: "UAppData:Crash Reports/LastCrash",
     91      condition: WIN && AppConstants.MOZ_CRASHREPORTER,
     92      stat: 1, // only caught on Windows.
     93      read: 1,
     94    },
     95    {
     96      // bug 1541200
     97      path: "UAppData:Crash Reports/LastCrash",
     98      condition: !WIN && AppConstants.MOZ_CRASHREPORTER,
     99      ignoreIfUnused: true, // only if we ever crashed on this machine
    100      read: 1,
    101      close: 1,
    102    },
    103    {
    104      // At least the read seems unavoidable for a regular startup.
    105      path: "UAppData:profiles.ini",
    106      ignoreIfUnused: true,
    107      condition: MAC,
    108      stat: 1,
    109      read: 1,
    110      close: 1,
    111    },
    112    {
    113      // At least the read seems unavoidable for a regular startup.
    114      path: "UAppData:profiles.ini",
    115      condition: WIN,
    116      ignoreIfUnused: true, // only if a real profile exists on the system.
    117      read: 1,
    118      stat: 1,
    119    },
    120    {
    121      path: "ProfLD:.startup-incomplete",
    122      condition: !WIN, // Visible on Windows with an open marker
    123      close: 1,
    124    },
    125    {
    126      // bug 1541491 to stop using this file, bug 1541494 to write correctly.
    127      path: "ProfLD:compatibility.ini",
    128      write: 18,
    129      close: 1,
    130    },
    131    {
    132      path: "GreD:omni.ja",
    133      condition: !WIN, // Visible on Windows with an open marker
    134      stat: 1,
    135    },
    136    {
    137      // bug 1376994
    138      path: "XCurProcD:omni.ja",
    139      condition: !WIN, // Visible on Windows with an open marker
    140      stat: 1,
    141    },
    142    {
    143      path: "ProfD:parent.lock",
    144      condition: WIN,
    145      stat: 1,
    146    },
    147    {
    148      // bug 1541603
    149      path: "ProfD:minidumps",
    150      condition: WIN,
    151      stat: 1,
    152    },
    153    {
    154      // bug 1543746
    155      path: "XCurProcD:defaults/preferences",
    156      condition: WIN,
    157      stat: 1,
    158    },
    159    {
    160      // bug 1544034
    161      path: "ProfLDS:startupCache/scriptCache-child-current.bin",
    162      condition: WIN,
    163      stat: 1,
    164    },
    165    {
    166      // bug 1544034
    167      path: "ProfLDS:startupCache/scriptCache-child.bin",
    168      condition: WIN,
    169      stat: 1,
    170    },
    171    {
    172      // bug 1544034
    173      path: "ProfLDS:startupCache/scriptCache-current.bin",
    174      condition: WIN,
    175      stat: 1,
    176    },
    177    {
    178      // bug 1544034
    179      path: "ProfLDS:startupCache/scriptCache.bin",
    180      condition: WIN,
    181      stat: 1,
    182    },
    183    {
    184      // bug 1541601
    185      path: "PrfDef:channel-prefs.js",
    186      condition: !MAC,
    187      stat: 1,
    188      read: 1,
    189      close: 1,
    190    },
    191    {
    192      // At least the read seems unavoidable
    193      path: "PrefD:prefs.js",
    194      stat: 1,
    195      read: 1,
    196      close: 1,
    197    },
    198    {
    199      // bug 1543752
    200      path: "PrefD:user.js",
    201      stat: 1,
    202      read: 1,
    203      close: 1,
    204    },
    205    {
    206      // This is the startup lock used to restrict only one Firefox startup at a time.
    207      path: `TmpD:firefox-${AppConstants.MOZ_UPDATE_CHANNEL}/parent.lock`,
    208      condition: WIN,
    209      stat: 1,
    210    },
    211  ],
    212 
    213  "before opening first browser window": [
    214    {
    215      // bug 1541226
    216      path: "ProfD:",
    217      condition: WIN,
    218      ignoreIfUnused: true, // Sometimes happens in the next phase
    219      stat: 1,
    220    },
    221    {
    222      // bug 1534745
    223      path: "ProfD:cookies.sqlite-journal",
    224      condition: !LINUX,
    225      ignoreIfUnused: true, // Sometimes happens in the next phase
    226      stat: 3,
    227      write: 4,
    228    },
    229    {
    230      // bug 1534745
    231      path: "ProfD:cookies.sqlite",
    232      condition: !LINUX,
    233      ignoreIfUnused: true, // Sometimes happens in the next phase
    234      stat: 2,
    235      read: 3,
    236      write: 1,
    237    },
    238    {
    239      // bug 1534745
    240      path: "ProfD:cookies.sqlite-wal",
    241      ignoreIfUnused: true, // Sometimes happens in the next phase
    242      condition: WIN,
    243      stat: 2,
    244    },
    245    {
    246      // Seems done by OS X and outside of our control.
    247      path: "*.savedState/restorecount.plist",
    248      condition: MAC,
    249      ignoreIfUnused: true,
    250      write: 1,
    251    },
    252    {
    253      // Side-effect of bug 1412090, via sandboxing (but the real
    254      // problem there is main-thread CPU use; see bug 1439412)
    255      path: "*ld.so.conf*",
    256      condition: LINUX && !AppConstants.MOZ_CODE_COVERAGE && !kSharedFontList,
    257      read: 22,
    258      close: 11,
    259    },
    260    {
    261      // bug 1541246
    262      path: "ProfD:extensions",
    263      ignoreIfUnused: true, // bug 1649590
    264      condition: WIN,
    265      stat: 1,
    266    },
    267    {
    268      // bug 1541246
    269      path: "UAppData:",
    270      ignoreIfUnused: true, // sometimes before opening first browser window,
    271      // sometimes before first paint
    272      condition: WIN,
    273      stat: 1,
    274    },
    275    {
    276      // bug 1833104 has context - this is artifact-only so doesn't affect
    277      // any real users, will just show up for developer builds and
    278      // artifact trypushes so we include it here.
    279      path: "GreD:jogfile.json",
    280      condition:
    281        WIN && Services.prefs.getBoolPref("telemetry.fog.artifact_build"),
    282      stat: 1,
    283    },
    284  ],
    285 
    286  // We reach this phase right after showing the first browser window.
    287  // This means that any I/O at this point delayed first paint.
    288  "before first paint": [
    289    {
    290      // We only hit this for new profiles.
    291      path: "XREAppDist:distribution.ini",
    292      // check we're not msix to disambiguate from the next entry...
    293      condition: WIN && !Services.sysinfo.getProperty("hasWinPackageId"),
    294      stat: 1,
    295    },
    296    {
    297      // On MSIX, we actually read this file - bug 1833341.
    298      path: "XREAppDist:distribution.ini",
    299      condition: WIN && Services.sysinfo.getProperty("hasWinPackageId"),
    300      stat: 1,
    301      read: 1,
    302    },
    303    {
    304      // bug 1545139
    305      path: "*Fonts/StaticCache.dat",
    306      condition: WIN,
    307      ignoreIfUnused: true, // Only on Win7
    308      read: 1,
    309    },
    310    {
    311      // Bug 1626738
    312      path: "SysD:spool/drivers/color/*",
    313      condition: WIN,
    314      read: 1,
    315    },
    316    {
    317      // Sandbox policy construction
    318      path: "*ld.so.conf*",
    319      condition: LINUX && !AppConstants.MOZ_CODE_COVERAGE,
    320      read: 22,
    321      close: 11,
    322    },
    323    {
    324      // bug 1541246
    325      path: "UAppData:",
    326      ignoreIfUnused: true, // sometimes before opening first browser window,
    327      // sometimes before first paint
    328      condition: WIN,
    329      stat: 1,
    330    },
    331    {
    332      // Not in packaged builds; useful for artifact builds.
    333      path: "GreD:ScalarArtifactDefinitions.json",
    334      condition: WIN && !AppConstants.MOZILLA_OFFICIAL,
    335      stat: 1,
    336    },
    337    {
    338      // Not in packaged builds; useful for artifact builds.
    339      path: "GreD:EventArtifactDefinitions.json",
    340      condition: WIN && !AppConstants.MOZILLA_OFFICIAL,
    341      stat: 1,
    342    },
    343    {
    344      // bug 1541226
    345      path: "ProfD:",
    346      condition: WIN,
    347      ignoreIfUnused: true, // Usually happens in the previous phase
    348      stat: 1,
    349    },
    350    {
    351      // bug 1534745
    352      path: "ProfD:cookies.sqlite-journal",
    353      condition: WIN,
    354      ignoreIfUnused: true, // Usually happens in the previous phase
    355      stat: 3,
    356      write: 4,
    357    },
    358    {
    359      // bug 1534745
    360      path: "ProfD:cookies.sqlite",
    361      condition: WIN,
    362      ignoreIfUnused: true, // Usually happens in the previous phase
    363      stat: 2,
    364      read: 3,
    365      write: 1,
    366    },
    367    {
    368      // bug 1534745
    369      path: "ProfD:cookies.sqlite-wal",
    370      condition: WIN,
    371      ignoreIfUnused: true, // Usually happens in the previous phase
    372      stat: 2,
    373    },
    374  ],
    375 
    376  // We are at this phase once we are ready to handle user events.
    377  // Any IO at this phase or before gets in the way of the user
    378  // interacting with the first browser window.
    379  "before handling user events": [
    380    {
    381      path: "GreD:update.test",
    382      ignoreIfUnused: true,
    383      condition: LINUX,
    384      close: 1,
    385    },
    386    {
    387      // Bug 1660582 - access while running on windows10 hardware.
    388      path: "ProfD:wmfvpxvideo.guard",
    389      condition: WIN,
    390      ignoreIfUnused: true,
    391      stat: 1,
    392      close: 1,
    393    },
    394    {
    395      // Bug 1649590
    396      path: "ProfD:extensions",
    397      ignoreIfUnused: true,
    398      condition: WIN,
    399      stat: 1,
    400    },
    401  ],
    402 
    403  // Things that are expected to be completely out of the startup path
    404  // and loaded lazily when used for the first time by the user should
    405  // be listed here.
    406  "before becoming idle": [
    407    {
    408      // bug 1370516 - NSS should be initialized off main thread.
    409      path: `ProfD:cert9.db`,
    410      condition: WIN,
    411      read: 5,
    412      stat: AppConstants.NIGHTLY_BUILD ? 5 : 4,
    413    },
    414    {
    415      // bug 1370516 - NSS should be initialized off main thread.
    416      path: `ProfD:cert9.db-journal`,
    417      condition: WIN,
    418      stat: 3,
    419    },
    420    {
    421      // bug 1370516 - NSS should be initialized off main thread.
    422      path: `ProfD:cert9.db-wal`,
    423      condition: WIN,
    424      stat: 3,
    425    },
    426    {
    427      // bug 1370516 - NSS should be initialized off main thread.
    428      path: "ProfD:pkcs11.txt",
    429      condition: WIN,
    430      read: 2,
    431    },
    432    {
    433      // bug 1370516 - NSS should be initialized off main thread.
    434      path: `ProfD:key4.db`,
    435      condition: WIN,
    436      read: 10,
    437      stat: AppConstants.NIGHTLY_BUILD ? 5 : 4,
    438    },
    439    {
    440      // bug 1370516 - NSS should be initialized off main thread.
    441      path: `ProfD:key4.db-journal`,
    442      condition: WIN,
    443      stat: 7,
    444    },
    445    {
    446      // bug 1370516 - NSS should be initialized off main thread.
    447      path: `ProfD:key4.db-wal`,
    448      condition: WIN,
    449      stat: 7,
    450    },
    451    {
    452      // bug 1391590
    453      path: "ProfD:places.sqlite-journal",
    454      ignoreIfUnused: true,
    455      fsync: 1,
    456      stat: 4,
    457      read: 1,
    458      write: 2,
    459    },
    460    {
    461      // bug 1391590
    462      path: "ProfD:places.sqlite-wal",
    463      ignoreIfUnused: true,
    464      stat: 4,
    465      fsync: 3,
    466      read: 51,
    467      write: 178,
    468    },
    469    {
    470      // bug 1391590
    471      path: "ProfD:places.sqlite-shm",
    472      condition: WIN,
    473      ignoreIfUnused: true,
    474      stat: 1,
    475    },
    476    {
    477      // bug 1391590
    478      path: "ProfD:places.sqlite",
    479      ignoreIfUnused: true,
    480      fsync: 2,
    481      read: 4,
    482      stat: 3,
    483      write: 1324,
    484    },
    485    {
    486      // bug 1391590
    487      path: "ProfD:favicons.sqlite-journal",
    488      ignoreIfUnused: true,
    489      fsync: 2,
    490      stat: 7,
    491      read: 2,
    492      write: 7,
    493    },
    494    {
    495      // bug 1391590
    496      path: "ProfD:favicons.sqlite-wal",
    497      ignoreIfUnused: true,
    498      fsync: 2,
    499      stat: 7,
    500      read: 7,
    501      write: 15,
    502    },
    503    {
    504      // bug 1391590
    505      path: "ProfD:favicons.sqlite-shm",
    506      condition: WIN,
    507      ignoreIfUnused: true,
    508      stat: 2,
    509    },
    510    {
    511      // bug 1391590
    512      path: "ProfD:favicons.sqlite",
    513      ignoreIfUnused: true,
    514      fsync: 3,
    515      read: 8,
    516      stat: 4,
    517      write: 1300,
    518    },
    519    {
    520      path: "ProfD:",
    521      condition: WIN,
    522      ignoreIfUnused: true,
    523      stat: 3,
    524    },
    525  ],
    526 };
    527 
    528 for (let name of ["d3d11layers", "glcontext", "wmfvpxvideo"]) {
    529  startupPhases["before first paint"].push({
    530    path: `ProfD:${name}.guard`,
    531    ignoreIfUnused: true,
    532    stat: 1,
    533  });
    534 }
    535 
    536 function expandPathWithDirServiceKey(path) {
    537  if (path.includes(":")) {
    538    let [prefix, suffix] = path.split(":");
    539    let [key, property] = prefix.split(".");
    540    let dir = Services.dirsvc.get(key, Ci.nsIFile);
    541    if (property) {
    542      dir = dir[property];
    543    }
    544 
    545    // Resolve symLinks.
    546    let dirPath = dir.path;
    547    while (dir && !dir.isSymlink()) {
    548      dir = dir.parent;
    549    }
    550    if (dir) {
    551      dirPath = dirPath.replace(dir.path, dir.target);
    552    }
    553 
    554    path = dirPath;
    555 
    556    if (suffix) {
    557      path += "/" + suffix;
    558    }
    559  }
    560  if (AppConstants.platform == "win") {
    561    path = path.replace(/\//g, "\\");
    562  }
    563  return path;
    564 }
    565 
    566 function getStackFromProfile(profile, stack) {
    567  const stackPrefixCol = profile.stackTable.schema.prefix;
    568  const stackFrameCol = profile.stackTable.schema.frame;
    569  const frameLocationCol = profile.frameTable.schema.location;
    570 
    571  let result = [];
    572  while (stack) {
    573    let sp = profile.stackTable.data[stack];
    574    let frame = profile.frameTable.data[sp[stackFrameCol]];
    575    stack = sp[stackPrefixCol];
    576    frame = profile.stringTable[frame[frameLocationCol]];
    577    if (frame != "js::RunScript" && !frame.startsWith("next (self-hosted:")) {
    578      result.push(frame);
    579    }
    580  }
    581  return result;
    582 }
    583 
    584 function pathMatches(path, filename) {
    585  path = path.toLowerCase();
    586  return (
    587    path == filename || // Full match
    588    // Wildcard on both sides of the path
    589    (path.startsWith("*") &&
    590      path.endsWith("*") &&
    591      filename.includes(path.slice(1, -1))) ||
    592    // Wildcard suffix
    593    (path.endsWith("*") && filename.startsWith(path.slice(0, -1))) ||
    594    // Wildcard prefix
    595    (path.startsWith("*") && filename.endsWith(path.slice(1)))
    596  );
    597 }
    598 
    599 add_task(async function () {
    600  if (
    601    !AppConstants.NIGHTLY_BUILD &&
    602    !AppConstants.MOZ_DEV_EDITION &&
    603    !AppConstants.DEBUG
    604  ) {
    605    ok(
    606      !("@mozilla.org/test/startuprecorder;1" in Cc),
    607      "the startup recorder component shouldn't exist in this non-nightly/non-devedition/" +
    608        "non-debug build."
    609    );
    610    return;
    611  }
    612 
    613  TestUtils.assertPackagedBuild();
    614 
    615  let startupRecorder =
    616    Cc["@mozilla.org/test/startuprecorder;1"].getService().wrappedJSObject;
    617  await startupRecorder.done;
    618 
    619  // Check for main thread I/O markers in the startup profile.
    620  let profile = startupRecorder.data.profile.threads[0];
    621 
    622  let phases = {};
    623  {
    624    const nameCol = profile.markers.schema.name;
    625    const dataCol = profile.markers.schema.data;
    626 
    627    let markersForCurrentPhase = [];
    628    let foundIOMarkers = false;
    629 
    630    for (let m of profile.markers.data) {
    631      let markerName = profile.stringTable[m[nameCol]];
    632      if (markerName.startsWith("startupRecorder:")) {
    633        phases[markerName.split("startupRecorder:")[1]] =
    634          markersForCurrentPhase;
    635        markersForCurrentPhase = [];
    636        continue;
    637      }
    638 
    639      if (markerName != "FileIO") {
    640        continue;
    641      }
    642 
    643      let markerData = m[dataCol];
    644      if (markerData.source == "sqlite-mainthread") {
    645        continue;
    646      }
    647 
    648      let samples = markerData.stack.samples;
    649      let stack = samples.data[0][samples.schema.stack];
    650      markersForCurrentPhase.push({
    651        operation: markerData.operation,
    652        filename: markerData.filename,
    653        source: markerData.source,
    654        stackId: stack,
    655      });
    656      foundIOMarkers = true;
    657    }
    658 
    659    // The I/O interposer is disabled if !RELEASE_OR_BETA, so we expect to have
    660    // no I/O marker in that case, but it's good to keep the test running to check
    661    // that we are still able to produce startup profiles.
    662    is(
    663      foundIOMarkers,
    664      !AppConstants.RELEASE_OR_BETA,
    665      "The IO interposer should be enabled in builds that are not RELEASE_OR_BETA"
    666    );
    667    if (!foundIOMarkers) {
    668      // If a profile unexpectedly contains no I/O marker, it's better to return
    669      // early to avoid having a lot of of confusing "no main thread IO when we
    670      // expected some" failures.
    671      return;
    672    }
    673  }
    674 
    675  for (let phase in startupPhases) {
    676    startupPhases[phase] = startupPhases[phase].filter(
    677      entry => !("condition" in entry) || entry.condition
    678    );
    679    startupPhases[phase].forEach(entry => {
    680      entry.listedPath = entry.path;
    681      entry.path = expandPathWithDirServiceKey(entry.path);
    682    });
    683  }
    684 
    685  let tmpPath = expandPathWithDirServiceKey("TmpD:").toLowerCase();
    686  let shouldPass = true;
    687  for (let phase in phases) {
    688    let knownIOList = startupPhases[phase];
    689    info(
    690      `known main thread IO paths during ${phase}:\n` +
    691        knownIOList
    692          .map(e => {
    693            let operations = Object.keys(e)
    694              .filter(k => k != "path")
    695              .map(k => `${k}: ${e[k]}`);
    696            return `  ${e.path} - ${operations.join(", ")}`;
    697          })
    698          .join("\n")
    699    );
    700 
    701    let markers = phases[phase];
    702    for (let marker of markers) {
    703      if (marker.operation == "create/open") {
    704        // TODO: handle these I/O markers once they are supported on
    705        // non-Windows platforms.
    706        continue;
    707      }
    708 
    709      if (!marker.filename) {
    710        // We are still missing the filename on some mainthreadio markers,
    711        // these markers are currently useless for the purpose of this test.
    712        continue;
    713      }
    714 
    715      // Convert to lower case before comparing because the OS X test machines
    716      // have the 'Firefox' folder in 'Library/Application Support' created
    717      // as 'firefox' for some reason.
    718      let filename = marker.filename.toLowerCase();
    719 
    720      if (!WIN && filename == "/dev/urandom") {
    721        continue;
    722      }
    723 
    724      // /dev/shm is always tmpfs (a memory filesystem); this isn't
    725      // really I/O any more than mmap/munmap are.
    726      if (LINUX && filename.startsWith("/dev/shm/")) {
    727        continue;
    728      }
    729 
    730      // "Files" from memfd_create() are similar to tmpfs but never
    731      // exist in the filesystem; however, they have names which are
    732      // exposed in procfs, and the I/O interposer observes when
    733      // they're close()d.
    734      if (LINUX && filename.startsWith("/memfd:")) {
    735        continue;
    736      }
    737 
    738      // Shared memory uses temporary files on MacOS <= 10.11 to avoid
    739      // a kernel security bug that will never be patched (see
    740      // https://crbug.com/project-zero/1671 for details).  This can
    741      // be removed when we no longer support those OS versions.
    742      if (MAC && filename.startsWith(tmpPath + "/org.mozilla.ipc.")) {
    743        continue;
    744      }
    745 
    746      let expected = false;
    747      for (let entry of knownIOList) {
    748        if (pathMatches(entry.path, filename)) {
    749          entry[marker.operation] = (entry[marker.operation] || 0) - 1;
    750          entry._used = true;
    751          expected = true;
    752          break;
    753        }
    754      }
    755      if (!expected) {
    756        record(
    757          false,
    758          `unexpected ${marker.operation} on ${marker.filename} ${phase}`,
    759          undefined,
    760          "  " + getStackFromProfile(profile, marker.stackId).join("\n  ")
    761        );
    762        shouldPass = false;
    763      }
    764      info(`(${marker.source}) ${marker.operation} - ${marker.filename}`);
    765      if (kDumpAllStacks) {
    766        info(
    767          getStackFromProfile(profile, marker.stackId)
    768            .map(f => "  " + f)
    769            .join("\n")
    770        );
    771      }
    772    }
    773 
    774    for (let entry of knownIOList) {
    775      for (let op in entry) {
    776        if (
    777          [
    778            "listedPath",
    779            "path",
    780            "condition",
    781            "ignoreIfUnused",
    782            "_used",
    783          ].includes(op)
    784        ) {
    785          continue;
    786        }
    787        let message = `${op} on ${entry.path} `;
    788        if (entry[op] == 0) {
    789          message += "as many times as expected";
    790        } else if (entry[op] > 0) {
    791          message += `allowed ${entry[op]} more times`;
    792        } else {
    793          message += `${entry[op] * -1} more times than expected`;
    794        }
    795        Assert.greaterOrEqual(entry[op], 0, `${message} ${phase}`);
    796      }
    797      if (!("_used" in entry) && !entry.ignoreIfUnused) {
    798        ok(
    799          false,
    800          `no main thread IO when we expected some during ${phase}: ${entry.path} (${entry.listedPath})`
    801        );
    802        shouldPass = false;
    803      }
    804    }
    805  }
    806 
    807  if (shouldPass) {
    808    ok(shouldPass, "No unexpected main thread I/O during startup");
    809  } else {
    810    const filename = "profile_startup_mainthreadio.json";
    811    let path = Services.env.get("MOZ_UPLOAD_DIR");
    812    let profilePath = PathUtils.join(path, filename);
    813    await IOUtils.writeJSON(profilePath, startupRecorder.data.profile);
    814    ok(
    815      false,
    816      "Unexpected main thread I/O behavior during startup; open the " +
    817        `${filename} artifact in the Firefox Profiler to see what happened`
    818    );
    819  }
    820 });