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