From 6bf0a5cb5034a7e684dcc3500e841785237ce2dd Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 19:32:43 +0200 Subject: Adding upstream version 1:115.7.0. Signed-off-by: Daniel Baumann --- .../browser_startup_content_mainthreadio.js | 438 +++++++++++++++++++++ 1 file changed, 438 insertions(+) create mode 100644 browser/base/content/test/performance/browser_startup_content_mainthreadio.js (limited to 'browser/base/content/test/performance/browser_startup_content_mainthreadio.js') diff --git a/browser/base/content/test/performance/browser_startup_content_mainthreadio.js b/browser/base/content/test/performance/browser_startup_content_mainthreadio.js new file mode 100644 index 0000000000..bf200b940d --- /dev/null +++ b/browser/base/content/test/performance/browser_startup_content_mainthreadio.js @@ -0,0 +1,438 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* This test records I/O syscalls done on the main thread during startup. + * + * To run this test similar to try server, you need to run: + * ./mach package + * ./mach test --appname=dist + * + * If you made changes that cause this test to fail, it's likely because you + * are touching more files or directories during startup. + * Most code has no reason to use main thread I/O. + * If for some reason accessing the file system on the main thread is currently + * unavoidable, consider defering the I/O as long as you can, ideally after + * the end of startup. + */ + +"use strict"; + +/* Set this to true only for debugging purpose; it makes the output noisy. */ +const kDumpAllStacks = false; + +// Shortcuts for conditions. +const LINUX = AppConstants.platform == "linux"; +const WIN = AppConstants.platform == "win"; +const MAC = AppConstants.platform == "macosx"; + +/* This is an object mapping string process types to lists of known cases + * of IO happening on the main thread. Ideally, IO should not be on the main + * thread, and should happen as late as possible (see above). + * + * Paths in the entries in these lists can: + * - be a full path, eg. "/etc/mime.types" + * - have a prefix which will be resolved using Services.dirsvc + * eg. "GreD:omni.ja" + * It's possible to have only a prefix, in thise case the directory will + * still be resolved, eg. "UAppData:" + * - use * at the begining and/or end as a wildcard + * The folder separator is '/' even for Windows paths, where it'll be + * automatically converted to '\'. + * + * Specifying 'ignoreIfUnused: true' will make the test ignore unused entries; + * without this the test is strict and will fail if the described IO does not + * happen. + * + * Each entry specifies the maximum number of times an operation is expected to + * occur. + * The operations currently reported by the I/O interposer are: + * create/open: only supported on Windows currently. The test currently + * ignores these markers to have a shorter initial list of IO operations. + * Adding Unix support is bug 1533779. + * stat: supported on all platforms when checking the last modified date or + * file size. Supported only on Windows when checking if a file exists; + * fixing this inconsistency is bug 1536109. + * read: supported on all platforms, but unix platforms will only report read + * calls going through NSPR. + * write: supported on all platforms, but Linux will only report write calls + * going through NSPR. + * close: supported only on Unix, and only for close calls going through NSPR. + * Adding Windows support is bug 1524574. + * fsync: supported only on Windows. + * + * If an entry specifies more than one operation, if at least one of them is + * encountered, the test won't report a failure for the entry if other + * operations are not encountered. This helps when listing cases where the + * reported operations aren't the same on all platforms due to the I/O + * interposer inconsistencies across platforms documented above. + */ +const processes = { + "Web Content": [ + { + path: "GreD:omni.ja", + condition: !WIN, // Visible on Windows with an open marker + stat: 1, + }, + { + // bug 1376994 + path: "XCurProcD:omni.ja", + condition: !WIN, // Visible on Windows with an open marker + stat: 1, + }, + { + // Exists call in ScopedXREEmbed::SetAppDir + path: "XCurProcD:", + condition: WIN, + stat: 1, + }, + { + // bug 1357205 + path: "XREAppFeat:formautofill@mozilla.org.xpi", + condition: !WIN, + ignoreIfUnused: true, + stat: 1, + }, + { + path: "*ShaderCache*", // Bug 1660480 - seen on hardware + condition: WIN, + ignoreIfUnused: true, + stat: 3, + }, + ], + "Privileged Content": [ + { + path: "GreD:omni.ja", + condition: !WIN, // Visible on Windows with an open marker + stat: 1, + }, + { + // bug 1376994 + path: "XCurProcD:omni.ja", + condition: !WIN, // Visible on Windows with an open marker + stat: 1, + }, + { + // Exists call in ScopedXREEmbed::SetAppDir + path: "XCurProcD:", + condition: WIN, + stat: 1, + }, + ], + WebExtensions: [ + { + path: "GreD:omni.ja", + condition: !WIN, // Visible on Windows with an open marker + stat: 1, + }, + { + // bug 1376994 + path: "XCurProcD:omni.ja", + condition: !WIN, // Visible on Windows with an open marker + stat: 1, + }, + { + // Exists call in ScopedXREEmbed::SetAppDir + path: "XCurProcD:", + condition: WIN, + stat: 1, + }, + ], +}; + +function expandPathWithDirServiceKey(path) { + if (path.includes(":")) { + let [prefix, suffix] = path.split(":"); + let [key, property] = prefix.split("."); + let dir = Services.dirsvc.get(key, Ci.nsIFile); + if (property) { + dir = dir[property]; + } + + // Resolve symLinks. + let dirPath = dir.path; + while (dir && !dir.isSymlink()) { + dir = dir.parent; + } + if (dir) { + dirPath = dirPath.replace(dir.path, dir.target); + } + + path = dirPath; + + if (suffix) { + path += "/" + suffix; + } + } + if (AppConstants.platform == "win") { + path = path.replace(/\//g, "\\"); + } + return path; +} + +function getStackFromProfile(profile, stack) { + const stackPrefixCol = profile.stackTable.schema.prefix; + const stackFrameCol = profile.stackTable.schema.frame; + const frameLocationCol = profile.frameTable.schema.location; + + let result = []; + while (stack) { + let sp = profile.stackTable.data[stack]; + let frame = profile.frameTable.data[sp[stackFrameCol]]; + stack = sp[stackPrefixCol]; + frame = profile.stringTable[frame[frameLocationCol]]; + if (frame != "js::RunScript" && !frame.startsWith("next (self-hosted:")) { + result.push(frame); + } + } + return result; +} + +function getIOMarkersFromProfile(profile) { + const nameCol = profile.markers.schema.name; + const dataCol = profile.markers.schema.data; + + let markers = []; + for (let m of profile.markers.data) { + let markerName = profile.stringTable[m[nameCol]]; + + if (markerName != "FileIO") { + continue; + } + + let markerData = m[dataCol]; + if (markerData.source == "sqlite-mainthread") { + continue; + } + + let samples = markerData.stack.samples; + let stack = samples.data[0][samples.schema.stack]; + markers.push({ + operation: markerData.operation, + filename: markerData.filename, + source: markerData.source, + stackId: stack, + }); + } + + return markers; +} + +function pathMatches(path, filename) { + path = path.toLowerCase(); + return ( + path == filename || // Full match + // Wildcard on both sides of the path + (path.startsWith("*") && + path.endsWith("*") && + filename.includes(path.slice(1, -1))) || + // Wildcard suffix + (path.endsWith("*") && filename.startsWith(path.slice(0, -1))) || + // Wildcard prefix + (path.startsWith("*") && filename.endsWith(path.slice(1))) + ); +} + +add_task(async function () { + if ( + !AppConstants.NIGHTLY_BUILD && + !AppConstants.MOZ_DEV_EDITION && + !AppConstants.DEBUG + ) { + ok( + !("@mozilla.org/test/startuprecorder;1" in Cc), + "the startup recorder component shouldn't exist in this non-nightly/non-devedition/" + + "non-debug build." + ); + return; + } + + TestUtils.assertPackagedBuild(); + + let startupRecorder = + Cc["@mozilla.org/test/startuprecorder;1"].getService().wrappedJSObject; + await startupRecorder.done; + + for (let process in processes) { + processes[process] = processes[process].filter( + entry => !("condition" in entry) || entry.condition + ); + processes[process].forEach(entry => { + entry.listedPath = entry.path; + entry.path = expandPathWithDirServiceKey(entry.path); + }); + } + + let tmpPath = expandPathWithDirServiceKey("TmpD:").toLowerCase(); + let shouldPass = true; + for (let procName in processes) { + let knownIOList = processes[procName]; + info( + `known main thread IO paths for ${procName} process:\n` + + knownIOList + .map(e => { + let operations = Object.keys(e) + .filter(k => !["path", "condition"].includes(k)) + .map(k => `${k}: ${e[k]}`); + return ` ${e.path} - ${operations.join(", ")}`; + }) + .join("\n") + ); + + let profile; + for (let process of startupRecorder.data.profile.processes) { + if (process.threads[0].processName == procName) { + profile = process.threads[0]; + break; + } + } + if (procName == "Privileged Content" && !profile) { + // The Privileged Content is started from an idle task that may not have + // been executed yet at the time we captured the startup profile in + // startupRecorder. + todo(false, `profile for ${procName} process not found`); + } else { + ok(profile, `Found profile for ${procName} process`); + } + if (!profile) { + continue; + } + + let markers = getIOMarkersFromProfile(profile); + for (let marker of markers) { + if (marker.operation == "create/open") { + // TODO: handle these I/O markers once they are supported on + // non-Windows platforms. + continue; + } + + if (!marker.filename) { + // We are still missing the filename on some mainthreadio markers, + // these markers are currently useless for the purpose of this test. + continue; + } + + // Convert to lower case before comparing because the OS X test machines + // have the 'Firefox' folder in 'Library/Application Support' created + // as 'firefox' for some reason. + let filename = marker.filename.toLowerCase(); + + if (!WIN && filename == "/dev/urandom") { + continue; + } + + // /dev/shm is always tmpfs (a memory filesystem); this isn't + // really I/O any more than mmap/munmap are. + if (LINUX && filename.startsWith("/dev/shm/")) { + continue; + } + + // "Files" from memfd_create() are similar to tmpfs but never + // exist in the filesystem; however, they have names which are + // exposed in procfs, and the I/O interposer observes when + // they're close()d. + if (LINUX && filename.startsWith("/memfd:")) { + continue; + } + + // Shared memory uses temporary files on MacOS <= 10.11 to avoid + // a kernel security bug that will never be patched (see + // https://crbug.com/project-zero/1671 for details). This can + // be removed when we no longer support those OS versions. + if (MAC && filename.startsWith(tmpPath + "/org.mozilla.ipc.")) { + continue; + } + + let expected = false; + for (let entry of knownIOList) { + if (pathMatches(entry.path, filename)) { + entry[marker.operation] = (entry[marker.operation] || 0) - 1; + entry._used = true; + expected = true; + break; + } + } + if (!expected) { + record( + false, + `unexpected ${marker.operation} on ${marker.filename} in ${procName} process`, + undefined, + " " + getStackFromProfile(profile, marker.stackId).join("\n ") + ); + shouldPass = false; + } + info(`(${marker.source}) ${marker.operation} - ${marker.filename}`); + if (kDumpAllStacks) { + info( + getStackFromProfile(profile, marker.stackId) + .map(f => " " + f) + .join("\n") + ); + } + } + + if (!knownIOList.length) { + continue; + } + // The I/O interposer is disabled if !RELEASE_OR_BETA, so we expect to have + // no I/O marker in that case, but it's good to keep the test running to check + // that we are still able to produce startup profiles. + is( + !!markers.length, + !AppConstants.RELEASE_OR_BETA, + procName + + " startup profiles should have IO markers in builds that are not RELEASE_OR_BETA" + ); + if (!markers.length) { + // If a profile unexpectedly contains no I/O marker, it's better to return + // early to avoid having a lot of of confusing "no main thread IO when we + // expected some" failures. + continue; + } + + for (let entry of knownIOList) { + for (let op in entry) { + if ( + [ + "listedPath", + "path", + "condition", + "ignoreIfUnused", + "_used", + ].includes(op) + ) { + continue; + } + let message = `${op} on ${entry.path} `; + if (entry[op] == 0) { + message += "as many times as expected"; + } else if (entry[op] > 0) { + message += `allowed ${entry[op]} more times`; + } else { + message += `${entry[op] * -1} more times than expected`; + } + ok(entry[op] >= 0, `${message} in ${procName} process`); + } + if (!("_used" in entry) && !entry.ignoreIfUnused) { + ok( + false, + `no main thread IO when we expected some for process ${procName}: ${entry.path} (${entry.listedPath})` + ); + shouldPass = false; + } + } + } + + if (shouldPass) { + ok(shouldPass, "No unexpected main thread I/O during startup"); + } else { + const filename = "profile_startup_content_mainthreadio.json"; + let path = Services.env.get("MOZ_UPLOAD_DIR"); + let profilePath = PathUtils.join(path, filename); + await IOUtils.writeJSON(profilePath, startupRecorder.data.profile); + ok( + false, + "Unexpected main thread I/O behavior during child process startup; " + + `open the ${filename} artifact in the Firefox Profiler to see what happened` + ); + } +}); -- cgit v1.2.3