1 /* Any copyright is dedicated to the Public Domain.
2 http://creativecommons.org/publicdomain/zero/1.0/ */
4 /* This test records I/O syscalls done on the main thread during startup.
6 * To run this test similar to try server, you need to run:
8 * ./mach test --appname=dist <path to test>
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
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.
24 /* Set this to true only for debugging purpose; it makes the output noisy. */
25 const kDumpAllStacks = false;
27 // Shortcuts for conditions.
28 const LINUX = AppConstants.platform == "linux";
29 const WIN = AppConstants.platform == "win";
30 const MAC = AppConstants.platform == "macosx";
32 const kSharedFontList = SpecialPowers.getBoolPref("gfx.e10s.font-list.shared");
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).
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
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 '\'.
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
52 * Each entry specifies the maximum number of times an operation is expected to
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
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.
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.
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": [
81 path: "UAppData:Crash Reports/InstallTime20*",
82 condition: AppConstants.MOZ_CRASHREPORTER,
83 stat: 1, // only caught on Windows.
90 path: "UAppData:Crash Reports/LastCrash",
91 condition: WIN && AppConstants.MOZ_CRASHREPORTER,
92 stat: 1, // only caught on Windows.
97 path: "UAppData:Crash Reports/LastCrash",
98 condition: !WIN && AppConstants.MOZ_CRASHREPORTER,
99 ignoreIfUnused: true, // only if we ever crashed on this machine
104 // At least the read seems unavoidable for a regular startup.
105 path: "UAppData:profiles.ini",
106 ignoreIfUnused: true,
113 // At least the read seems unavoidable for a regular startup.
114 path: "UAppData:profiles.ini",
116 ignoreIfUnused: true, // only if a real profile exists on the system.
121 // bug 1541226, bug 1363586, bug 1541593
127 path: "ProfLD:.startup-incomplete",
128 condition: !WIN, // Visible on Windows with an open marker
132 // bug 1541491 to stop using this file, bug 1541494 to write correctly.
133 path: "ProfLD:compatibility.ini",
138 path: "GreD:omni.ja",
139 condition: !WIN, // Visible on Windows with an open marker
144 path: "XCurProcD:omni.ja",
145 condition: !WIN, // Visible on Windows with an open marker
149 path: "ProfD:parent.lock",
155 path: "ProfD:minidumps",
161 path: "XCurProcD:defaults/preferences",
167 path: "ProfLDS:startupCache/scriptCache-child-current.bin",
173 path: "ProfLDS:startupCache/scriptCache-child.bin",
179 path: "ProfLDS:startupCache/scriptCache-current.bin",
185 path: "ProfLDS:startupCache/scriptCache.bin",
191 path: "PrfDef:channel-prefs.js",
197 // At least the read seems unavoidable
198 path: "PrefD:prefs.js",
205 path: "PrefD:user.js",
212 "before opening first browser window": [
217 ignoreIfUnused: true, // Sometimes happens in the next phase
222 path: "ProfD:cookies.sqlite-journal",
224 ignoreIfUnused: true, // Sometimes happens in the next phase
230 path: "ProfD:cookies.sqlite",
232 ignoreIfUnused: true, // Sometimes happens in the next phase
239 path: "ProfD:cookies.sqlite-wal",
240 ignoreIfUnused: true, // Sometimes happens in the next phase
245 // Seems done by OS X and outside of our control.
246 path: "*.savedState/restorecount.plist",
248 ignoreIfUnused: true,
252 // Side-effect of bug 1412090, via sandboxing (but the real
253 // problem there is main-thread CPU use; see bug 1439412)
254 path: "*ld.so.conf*",
255 condition: LINUX && !AppConstants.MOZ_CODE_COVERAGE && !kSharedFontList,
261 path: "ProfD:extensions",
262 ignoreIfUnused: true, // bug 1649590
269 ignoreIfUnused: true, // sometimes before opening first browser window,
270 // sometimes before first paint
275 // bug 1833104 has context - this is artifact-only so doesn't affect
276 // any real users, will just show up for developer builds and
277 // artifact trypushes so we include it here.
278 path: "GreD:jogfile.json",
280 WIN && Services.prefs.getBoolPref("telemetry.fog.artifact_build"),
285 // We reach this phase right after showing the first browser window.
286 // This means that any I/O at this point delayed first paint.
287 "before first paint": [
290 path: "OldUpdRootD:",
296 path: "UpdRootD:updates/0/update.status",
301 path: "XREAppFeat:formautofill@mozilla.org.xpi",
307 path: "XREAppFeat:webcompat@mozilla.org.xpi",
309 ignoreIfUnused: true, // Sometimes happens in the previous phase
313 // We only hit this for new profiles.
314 path: "XREAppDist:distribution.ini",
315 // check we're not msix to disambiguate from the next entry...
316 condition: WIN && !Services.sysinfo.getProperty("hasWinPackageId"),
320 // On MSIX, we actually read this file - bug 1833341.
321 path: "XREAppDist:distribution.ini",
322 condition: WIN && Services.sysinfo.getProperty("hasWinPackageId"),
328 path: "*Fonts/StaticCache.dat",
330 ignoreIfUnused: true, // Only on Win7
335 path: "SysD:spool/drivers/color/*",
340 // Sandbox policy construction
341 path: "*ld.so.conf*",
342 condition: LINUX && !AppConstants.MOZ_CODE_COVERAGE,
349 ignoreIfUnused: true, // sometimes before opening first browser window,
350 // sometimes before first paint
355 // Not in packaged builds; useful for artifact builds.
356 path: "GreD:ScalarArtifactDefinitions.json",
357 condition: WIN && !AppConstants.MOZILLA_OFFICIAL,
361 // Not in packaged builds; useful for artifact builds.
362 path: "GreD:EventArtifactDefinitions.json",
363 condition: WIN && !AppConstants.MOZILLA_OFFICIAL,
370 ignoreIfUnused: true, // Usually happens in the previous phase
375 path: "ProfD:cookies.sqlite-journal",
377 ignoreIfUnused: true, // Usually happens in the previous phase
383 path: "ProfD:cookies.sqlite",
385 ignoreIfUnused: true, // Usually happens in the previous phase
392 path: "ProfD:cookies.sqlite-wal",
394 ignoreIfUnused: true, // Usually happens in the previous phase
399 // We are at this phase once we are ready to handle user events.
400 // Any IO at this phase or before gets in the way of the user
401 // interacting with the first browser window.
402 "before handling user events": [
404 path: "GreD:update.test",
405 ignoreIfUnused: true,
410 path: "XREAppFeat:webcompat-reporter@mozilla.org.xpi",
412 ignoreIfUnused: true,
417 // Bug 1660582 - access while running on windows10 hardware.
418 path: "ProfD:wmfvpxvideo.guard",
420 ignoreIfUnused: true,
426 path: "ProfD:extensions",
427 ignoreIfUnused: true,
433 // Things that are expected to be completely out of the startup path
434 // and loaded lazily when used for the first time by the user should
436 "before becoming idle": [
438 // bug 1370516 - NSS should be initialized off main thread.
439 path: `ProfD:cert9.db`,
442 stat: AppConstants.NIGHTLY_BUILD ? 5 : 4,
445 // bug 1370516 - NSS should be initialized off main thread.
446 path: `ProfD:cert9.db-journal`,
451 // bug 1370516 - NSS should be initialized off main thread.
452 path: `ProfD:cert9.db-wal`,
457 // bug 1370516 - NSS should be initialized off main thread.
458 path: "ProfD:pkcs11.txt",
463 // bug 1370516 - NSS should be initialized off main thread.
464 path: `ProfD:key4.db`,
467 stat: AppConstants.NIGHTLY_BUILD ? 5 : 4,
470 // bug 1370516 - NSS should be initialized off main thread.
471 path: `ProfD:key4.db-journal`,
476 // bug 1370516 - NSS should be initialized off main thread.
477 path: `ProfD:key4.db-wal`,
482 path: "XREAppFeat:screenshots@mozilla.org.xpi",
483 ignoreIfUnused: true,
487 path: "XREAppFeat:webcompat-reporter@mozilla.org.xpi",
488 ignoreIfUnused: true,
494 path: "ProfD:places.sqlite-journal",
495 ignoreIfUnused: true,
503 path: "ProfD:places.sqlite-wal",
504 ignoreIfUnused: true,
512 path: "ProfD:places.sqlite-shm",
514 ignoreIfUnused: true,
519 path: "ProfD:places.sqlite",
520 ignoreIfUnused: true,
528 path: "ProfD:favicons.sqlite-journal",
529 ignoreIfUnused: true,
537 path: "ProfD:favicons.sqlite-wal",
538 ignoreIfUnused: true,
546 path: "ProfD:favicons.sqlite-shm",
548 ignoreIfUnused: true,
553 path: "ProfD:favicons.sqlite",
554 ignoreIfUnused: true,
563 ignoreIfUnused: true,
569 for (let name of ["d3d11layers", "glcontext", "wmfvpxvideo"]) {
570 startupPhases["before first paint"].push({
571 path: `ProfD:${name}.guard`,
572 ignoreIfUnused: true,
577 function expandPathWithDirServiceKey(path) {
578 if (path.includes(":")) {
579 let [prefix, suffix] = path.split(":");
580 let [key, property] = prefix.split(".");
581 let dir = Services.dirsvc.get(key, Ci.nsIFile);
587 let dirPath = dir.path;
588 while (dir && !dir.isSymlink()) {
592 dirPath = dirPath.replace(dir.path, dir.target);
598 path += "/" + suffix;
601 if (AppConstants.platform == "win") {
602 path = path.replace(/\//g, "\\");
607 function getStackFromProfile(profile, stack) {
608 const stackPrefixCol = profile.stackTable.schema.prefix;
609 const stackFrameCol = profile.stackTable.schema.frame;
610 const frameLocationCol = profile.frameTable.schema.location;
614 let sp = profile.stackTable.data[stack];
615 let frame = profile.frameTable.data[sp[stackFrameCol]];
616 stack = sp[stackPrefixCol];
617 frame = profile.stringTable[frame[frameLocationCol]];
618 if (frame != "js::RunScript" && !frame.startsWith("next (self-hosted:")) {
625 function pathMatches(path, filename) {
626 path = path.toLowerCase();
628 path == filename || // Full match
629 // Wildcard on both sides of the path
630 (path.startsWith("*") &&
631 path.endsWith("*") &&
632 filename.includes(path.slice(1, -1))) ||
634 (path.endsWith("*") && filename.startsWith(path.slice(0, -1))) ||
636 (path.startsWith("*") && filename.endsWith(path.slice(1)))
640 add_task(async function() {
642 !AppConstants.NIGHTLY_BUILD &&
643 !AppConstants.MOZ_DEV_EDITION &&
647 !("@mozilla.org/test/startuprecorder;1" in Cc),
648 "the startup recorder component shouldn't exist in this non-nightly/non-devedition/" +
654 TestUtils.assertPackagedBuild();
656 let startupRecorder = Cc["@mozilla.org/test/startuprecorder;1"].getService()
658 await startupRecorder.done;
660 // Add system add-ons to the list of known IO dynamically.
661 // They should go in the omni.ja file (bug 1357205).
663 let addons = await AddonManager.getAddonsByTypes(["extension"]);
664 for (let addon of addons) {
665 if (addon.isSystem) {
666 startupPhases["before opening first browser window"].push({
667 path: `XREAppFeat:${addon.id}.xpi`,
671 startupPhases["before handling user events"].push({
672 path: `XREAppFeat:${addon.id}.xpi`,
680 // Check for main thread I/O markers in the startup profile.
681 let profile = startupRecorder.data.profile.threads[0];
685 const nameCol = profile.markers.schema.name;
686 const dataCol = profile.markers.schema.data;
688 let markersForCurrentPhase = [];
689 let foundIOMarkers = false;
691 for (let m of profile.markers.data) {
692 let markerName = profile.stringTable[m[nameCol]];
693 if (markerName.startsWith("startupRecorder:")) {
695 markerName.split("startupRecorder:")[1]
696 ] = markersForCurrentPhase;
697 markersForCurrentPhase = [];
701 if (markerName != "FileIO") {
705 let markerData = m[dataCol];
706 if (markerData.source == "sqlite-mainthread") {
710 let samples = markerData.stack.samples;
711 let stack = samples.data[0][samples.schema.stack];
712 markersForCurrentPhase.push({
713 operation: markerData.operation,
714 filename: markerData.filename,
715 source: markerData.source,
718 foundIOMarkers = true;
721 // The I/O interposer is disabled if !RELEASE_OR_BETA, so we expect to have
722 // no I/O marker in that case, but it's good to keep the test running to check
723 // that we are still able to produce startup profiles.
726 !AppConstants.RELEASE_OR_BETA,
727 "The IO interposer should be enabled in builds that are not RELEASE_OR_BETA"
729 if (!foundIOMarkers) {
730 // If a profile unexpectedly contains no I/O marker, it's better to return
731 // early to avoid having a lot of of confusing "no main thread IO when we
732 // expected some" failures.
737 for (let phase in startupPhases) {
738 startupPhases[phase] = startupPhases[phase].filter(
739 entry => !("condition" in entry) || entry.condition
741 startupPhases[phase].forEach(entry => {
742 entry.listedPath = entry.path;
743 entry.path = expandPathWithDirServiceKey(entry.path);
747 let tmpPath = expandPathWithDirServiceKey("TmpD:").toLowerCase();
748 let shouldPass = true;
749 for (let phase in phases) {
750 let knownIOList = startupPhases[phase];
752 `known main thread IO paths during ${phase}:\n` +
755 let operations = Object.keys(e)
756 .filter(k => k != "path")
757 .map(k => `${k}: ${e[k]}`);
758 return ` ${e.path} - ${operations.join(", ")}`;
763 let markers = phases[phase];
764 for (let marker of markers) {
765 if (marker.operation == "create/open") {
766 // TODO: handle these I/O markers once they are supported on
767 // non-Windows platforms.
771 if (!marker.filename) {
772 // We are still missing the filename on some mainthreadio markers,
773 // these markers are currently useless for the purpose of this test.
777 // Convert to lower case before comparing because the OS X test machines
778 // have the 'Firefox' folder in 'Library/Application Support' created
779 // as 'firefox' for some reason.
780 let filename = marker.filename.toLowerCase();
782 if (!WIN && filename == "/dev/urandom") {
786 // /dev/shm is always tmpfs (a memory filesystem); this isn't
787 // really I/O any more than mmap/munmap are.
788 if (LINUX && filename.startsWith("/dev/shm/")) {
792 // "Files" from memfd_create() are similar to tmpfs but never
793 // exist in the filesystem; however, they have names which are
794 // exposed in procfs, and the I/O interposer observes when
796 if (LINUX && filename.startsWith("/memfd:")) {
800 // Shared memory uses temporary files on MacOS <= 10.11 to avoid
801 // a kernel security bug that will never be patched (see
802 // https://crbug.com/project-zero/1671 for details). This can
803 // be removed when we no longer support those OS versions.
804 if (MAC && filename.startsWith(tmpPath + "/org.mozilla.ipc.")) {
808 let expected = false;
809 for (let entry of knownIOList) {
810 if (pathMatches(entry.path, filename)) {
811 entry[marker.operation] = (entry[marker.operation] || 0) - 1;
820 `unexpected ${marker.operation} on ${marker.filename} ${phase}`,
822 " " + getStackFromProfile(profile, marker.stackId).join("\n ")
826 info(`(${marker.source}) ${marker.operation} - ${marker.filename}`);
827 if (kDumpAllStacks) {
829 getStackFromProfile(profile, marker.stackId)
836 for (let entry of knownIOList) {
837 for (let op in entry) {
849 let message = `${op} on ${entry.path} `;
850 if (entry[op] == 0) {
851 message += "as many times as expected";
852 } else if (entry[op] > 0) {
853 message += `allowed ${entry[op]} more times`;
855 message += `${entry[op] * -1} more times than expected`;
857 ok(entry[op] >= 0, `${message} ${phase}`);
859 if (!("_used" in entry) && !entry.ignoreIfUnused) {
862 `no main thread IO when we expected some during ${phase}: ${entry.path} (${entry.listedPath})`
870 ok(shouldPass, "No unexpected main thread I/O during startup");
872 const filename = "profile_startup_mainthreadio.json";
873 let path = Services.env.get("MOZ_UPLOAD_DIR");
874 let profilePath = PathUtils.join(path, filename);
875 await IOUtils.writeJSON(profilePath, startupRecorder.data.profile);
878 "Unexpected main thread I/O behavior during startup; open the " +
879 `${filename} artifact in the Firefox Profiler to see what happened`