browser_gc_schedule.js (10781B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 "use strict"; 6 7 const TEST_PAGE = 8 "http://mochi.test:8888/browser/dom/ipc/tests/file_dummy.html"; 9 10 async function waitForGCBegin() { 11 var waitTopic = "garbage-collector-begin"; 12 var observer = {}; 13 14 info("Waiting for " + waitTopic); 15 // This fixes a ReferenceError for Date, it's weird. 16 ok(Date.now(), "Date.now()"); 17 var when = await new Promise(resolve => { 18 observer.observe = function () { 19 resolve(Date.now()); 20 }; 21 22 Services.obs.addObserver(observer, waitTopic); 23 }); 24 25 Services.obs.removeObserver(observer, waitTopic); 26 27 // This delay attempts to make the time stamps unique. 28 do { 29 var now = Date.now(); 30 } while (when + 5 > now); 31 32 return when; 33 } 34 35 async function waitForGCEnd() { 36 var waitTopic = "garbage-collector-end"; 37 var observer = {}; 38 39 info("Waiting for " + waitTopic); 40 // This fixes a ReferenceError for Date, it's weird. 41 ok(Date.now(), "Date.now()"); 42 let when = await new Promise(resolve => { 43 observer.observe = function () { 44 resolve(Date.now()); 45 }; 46 47 Services.obs.addObserver(observer, waitTopic); 48 }); 49 50 Services.obs.removeObserver(observer, waitTopic); 51 52 do { 53 var now = Date.now(); 54 } while (when + 5 > now); 55 56 return when; 57 } 58 59 function getProcessID() { 60 return Services.appinfo.processID; 61 } 62 63 async function resolveInOrder(promisesAndStates) { 64 var order = []; 65 var promises = []; 66 67 for (let p of promisesAndStates) { 68 promises.push( 69 p.promise.then(when => { 70 info(`Tab: ${p.tab} did ${p.state}`); 71 order.push({ tab: p.tab, state: p.state, when }); 72 }) 73 ); 74 } 75 76 await Promise.all(promises); 77 78 return order; 79 } 80 81 // Check that the list of events returned by resolveInOrder are in a 82 // sensible order. 83 function checkOneAtATime(events) { 84 var cur = null; 85 var lastWhen = null; 86 87 info("Checking order of events"); 88 for (const e of events) { 89 ok(e.state === "begin" || e.state === "end", "event.state is good"); 90 Assert.notStrictEqual(e.tab, undefined, "event.tab exists"); 91 92 if (lastWhen) { 93 // We need these in sorted order so that the other checks here make 94 // sense. 95 Assert.lessOrEqual( 96 lastWhen, 97 e.when, 98 `Unsorted events, last: ${lastWhen}, this: ${e.when}` 99 ); 100 } 101 lastWhen = e.when; 102 103 if (e.state === "begin") { 104 is(cur, null, `GC can begin on tab ${e.tab}`); 105 cur = e.tab; 106 } else { 107 is(e.tab, cur, `GC can end on tab ${e.tab}`); 108 cur = null; 109 } 110 } 111 112 is(cur, null, "No GC left running"); 113 } 114 115 function checkAllCompleted(events, expectTabsCompleted) { 116 var tabsCompleted = events.filter(e => e.state === "end").map(e => e.tab); 117 118 for (var t of expectTabsCompleted) { 119 ok(tabsCompleted.includes(t), `Tab ${t} did a GC`); 120 } 121 } 122 123 async function setupTabsAndOneForForeground(num_tabs) { 124 ++num_tabs; 125 var pids = []; 126 127 const parent_pid = getProcessID(); 128 info("Parent process PID is " + parent_pid); 129 130 const tabs = await Promise.all( 131 Array(num_tabs) 132 .fill() 133 .map(_ => { 134 return BrowserTestUtils.openNewForegroundTab({ 135 gBrowser, 136 opening: TEST_PAGE, 137 forceNewProcess: true, 138 }); 139 }) 140 ); 141 142 for (const [i, tab] of Object.entries(tabs)) { 143 const tab_pid = await SpecialPowers.spawn( 144 tab.linkedBrowser, 145 [], 146 getProcessID 147 ); 148 149 info(`Tab ${i} pid is ${tab_pid}`); 150 isnot(parent_pid, tab_pid, `Tab ${i} is in content process`); 151 ok(!pids.includes(tab_pid), `Tab ${i} is in a distinct process`); 152 153 pids.push(tab_pid); 154 } 155 156 // Since calling openNewForegroundTab several times in a row doesn't update 157 // process priorities correctly, we need to explicitly switch tabs. 158 for (let tab of tabs) { 159 await BrowserTestUtils.switchTab(gBrowser, tab); 160 } 161 162 return tabs; 163 } 164 165 function doContentRunNextCollectionTimer() { 166 content.windowUtils.pokeGC("PAGE_HIDE"); 167 content.windowUtils.runNextCollectorTimer("PAGE_HIDE"); 168 } 169 170 function startNextCollection( 171 tab, 172 tab_num, 173 waits, 174 fn = doContentRunNextCollectionTimer 175 ) { 176 var browser = tab.linkedBrowser; 177 178 // Finish any currently running GC. 179 SpecialPowers.spawn(browser, [], () => { 180 SpecialPowers.Cu.getJSTestingFunctions().finishgc(); 181 }); 182 183 if (tab.selected) { 184 // One isn't expected to use the return value with foreground tab! 185 return {}; 186 } 187 188 var waitBegin = SpecialPowers.spawn(browser, [], waitForGCBegin); 189 var waitEnd = SpecialPowers.spawn(browser, [], waitForGCEnd); 190 waits.push({ promise: waitBegin, tab: tab_num, state: "begin" }); 191 waits.push({ promise: waitEnd, tab: tab_num, state: "end" }); 192 193 SpecialPowers.spawn(browser, [], fn); 194 195 // Return these so that the abort GC test can wait for the begin. 196 return { waitBegin, waitEnd }; 197 } 198 199 add_task(async function gcOneAtATime() { 200 SpecialPowers.pushPrefEnv({ 201 set: [["javascript.options.concurrent_multiprocess_gcs.max", 1]], 202 }); 203 204 const num_tabs = 12; 205 var tabs = await setupTabsAndOneForForeground(num_tabs); 206 207 info("Tabs ready, Asking for GCs"); 208 var waits = []; 209 for (var i = 0; i < num_tabs; i++) { 210 startNextCollection(tabs[i], i, waits); 211 } 212 213 let order = await resolveInOrder(waits); 214 // We need these in the order they actually occurred, so far that's how 215 // they're returned, but we'll sort them to be sure. 216 order.sort((e1, e2) => e1.when - e2.when); 217 checkOneAtATime(order); 218 checkAllCompleted( 219 order, 220 Array.from({ length: num_tabs }, (_, n) => n) 221 ); 222 223 for (var tab of tabs) { 224 BrowserTestUtils.removeTab(tab); 225 } 226 227 SpecialPowers.popPrefEnv(); 228 }); 229 230 add_task(async function gcAbort() { 231 SpecialPowers.pushPrefEnv({ 232 set: [["javascript.options.concurrent_multiprocess_gcs.max", 1]], 233 }); 234 235 const num_tabs = 2; 236 var tabs = await setupTabsAndOneForForeground(num_tabs); 237 238 info("Tabs ready, Asking for GCs"); 239 var waits = []; 240 241 var tab0Waits = startNextCollection(tabs[0], 0, waits, () => { 242 SpecialPowers.Cu.getJSTestingFunctions().gcslice(1); 243 }); 244 await tab0Waits.waitBegin; 245 246 // Tab 0 has started a GC. Now we schedule a GC in tab one. It must not 247 // begin yet (but we don't check that, gcOneAtATime is assumed to check 248 // this. 249 startNextCollection(tabs[1], 1, waits); 250 251 // Request that tab 0 abort, this test checks that tab 1 can now begin. 252 SpecialPowers.spawn(tabs[0].linkedBrowser, [], () => { 253 SpecialPowers.Cu.getJSTestingFunctions().abortgc(); 254 }); 255 256 let order = await resolveInOrder(waits); 257 // We need these in the order they actually occurred, so far that's how 258 // they're returned, but we'll sort them to be sure. 259 order.sort((e1, e2) => e1.when - e2.when); 260 checkOneAtATime(order); 261 checkAllCompleted( 262 order, 263 Array.from({ length: num_tabs }, (_, n) => n) 264 ); 265 266 for (var tab of tabs) { 267 BrowserTestUtils.removeTab(tab); 268 } 269 270 SpecialPowers.popPrefEnv(); 271 }); 272 273 add_task(async function gcJSInitiatedDuring() { 274 SpecialPowers.pushPrefEnv({ 275 set: [["javascript.options.concurrent_multiprocess_gcs.max", 1]], 276 }); 277 278 const num_tabs = 3; 279 var tabs = await setupTabsAndOneForForeground(num_tabs); 280 281 let completed; 282 let retry = 0; 283 const maxRetries = 3; 284 do { 285 completed = true; 286 retry++; 287 288 info("Tabs ready, Asking for GCs"); 289 var waits = []; 290 291 // Start a GC on tab 0 to consume the scheduler's "token". 292 var tab0Waits = startNextCollection(tabs[0], 0, waits, () => { 293 SpecialPowers.Cu.getJSTestingFunctions().gcslice(1); 294 }); 295 await tab0Waits.waitBegin; 296 info("GC on tab 0 has begun"); 297 298 // Request a GC in tab 1, this will be blocked by the ongoing GC in tab 0. 299 var tab1Waits = startNextCollection(tabs[1], 1, waits); 300 301 // Force a GC to start in tab 1. This won't wait for tab 0. 302 SpecialPowers.spawn(tabs[1].linkedBrowser, [], () => { 303 SpecialPowers.Cu.getJSTestingFunctions().gcslice(1); 304 }); 305 306 await tab1Waits.waitBegin; 307 info("GC on tab 1 has begun"); 308 309 // The GC in tab 0 should still be running. 310 var state = await SpecialPowers.spawn(tabs[0].linkedBrowser, [], () => { 311 return SpecialPowers.Cu.getJSTestingFunctions().gcstate(); 312 }); 313 314 info("State of Tab 0 GC is " + state); 315 if (state == "NotActive") { 316 // The GC finished earlier than expected. The test must be retried. 317 info("GC finished in tab 0"); 318 completed = false; 319 } 320 321 // Let the GCs complete, verify that a GC in a 3rd tab can acquire a token. 322 startNextCollection(tabs[2], 2, waits); 323 324 let order = await resolveInOrder(waits); 325 info("All GCs finished"); 326 checkAllCompleted( 327 order, 328 Array.from({ length: num_tabs }, (_, n) => n) 329 ); 330 } while (!completed && retry <= maxRetries); 331 332 ok(completed, "GC in tab 0 finished sooner than expected"); 333 334 for (var tab of tabs) { 335 BrowserTestUtils.removeTab(tab); 336 } 337 338 SpecialPowers.popPrefEnv(); 339 }); 340 341 add_task(async function gcJSInitiatedBefore() { 342 SpecialPowers.pushPrefEnv({ 343 set: [["javascript.options.concurrent_multiprocess_gcs.max", 1]], 344 }); 345 346 const num_tabs = 8; 347 var tabs = await setupTabsAndOneForForeground(num_tabs); 348 349 let completed; 350 let retry = 0; 351 const maxRetries = 3; 352 do { 353 completed = true; 354 retry++; 355 356 info("Tabs ready"); 357 var waits = []; 358 359 // Start a GC on tab 0 to consume the scheduler's first "token". 360 info("Force a JS-initiated GC in tab 0"); 361 var tab0Waits = startNextCollection(tabs[0], 0, waits, () => { 362 SpecialPowers.Cu.getJSTestingFunctions().gcslice(1); 363 }); 364 await tab0Waits.waitBegin; 365 366 info("Request GCs in remaining tabs"); 367 for (var i = 1; i < num_tabs; i++) { 368 startNextCollection(tabs[i], i, waits); 369 } 370 371 // The GC in tab 0 should still be running. 372 var state = await SpecialPowers.spawn(tabs[0].linkedBrowser, [], () => { 373 return SpecialPowers.Cu.getJSTestingFunctions().gcstate(); 374 }); 375 376 info("State is " + state); 377 if (state == "NotActive") { 378 // The GC finished earlier than expected. The test must be retried. 379 info("GC finished in tab 0"); 380 completed = false; 381 } 382 383 let order = await resolveInOrder(waits); 384 // We need these in the order they actually occurred, so far that's how 385 // they're returned, but we'll sort them to be sure. 386 order.sort((e1, e2) => e1.when - e2.when); 387 checkOneAtATime(order); 388 checkAllCompleted( 389 order, 390 Array.from({ length: num_tabs }, (_, n) => n) 391 ); 392 } while (!completed && retry <= maxRetries); 393 394 ok(completed, "GC in tab 0 finished sooner than expected"); 395 396 for (var tab of tabs) { 397 BrowserTestUtils.removeTab(tab); 398 } 399 400 SpecialPowers.popPrefEnv(); 401 });