diff options
Diffstat (limited to 'dom/ipc/tests/browser_gc_schedule.js')
-rw-r--r-- | dom/ipc/tests/browser_gc_schedule.js | 366 |
1 files changed, 366 insertions, 0 deletions
diff --git a/dom/ipc/tests/browser_gc_schedule.js b/dom/ipc/tests/browser_gc_schedule.js new file mode 100644 index 0000000000..53f8a70ab2 --- /dev/null +++ b/dom/ipc/tests/browser_gc_schedule.js @@ -0,0 +1,366 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const TEST_PAGE = + "http://mochi.test:8888/browser/dom/ipc/tests/file_dummy.html"; + +async function waitForGCBegin() { + var waitTopic = "garbage-collector-begin"; + var observer = {}; + + info("Waiting for " + waitTopic); + // This fixes a ReferenceError for Date, it's weird. + ok(Date.now(), "Date.now()"); + var when = await new Promise(resolve => { + observer.observe = function (subject, topic, data) { + resolve(Date.now()); + }; + + Services.obs.addObserver(observer, waitTopic); + }); + + Services.obs.removeObserver(observer, waitTopic); + + // This delay attempts to make the time stamps unique. + do { + var now = Date.now(); + } while (when + 5 > now); + + return when; +} + +async function waitForGCEnd() { + var waitTopic = "garbage-collector-end"; + var observer = {}; + + info("Waiting for " + waitTopic); + // This fixes a ReferenceError for Date, it's weird. + ok(Date.now(), "Date.now()"); + let when = await new Promise(resolve => { + observer.observe = function (subject, topic, data) { + resolve(Date.now()); + }; + + Services.obs.addObserver(observer, waitTopic); + }); + + Services.obs.removeObserver(observer, waitTopic); + + do { + var now = Date.now(); + } while (when + 5 > now); + + return when; +} + +function getProcessID() { + return Services.appinfo.processID; +} + +async function resolveInOrder(promisesAndStates) { + var order = []; + var promises = []; + + for (let p of promisesAndStates) { + promises.push( + p.promise.then(when => { + info(`Tab: ${p.tab} did ${p.state}`); + order.push({ tab: p.tab, state: p.state, when }); + }) + ); + } + + await Promise.all(promises); + + return order; +} + +// Check that the list of events returned by resolveInOrder are in a +// sensible order. +function checkOneAtATime(events) { + var cur = null; + var lastWhen = null; + + info("Checking order of events"); + for (const e of events) { + ok(e.state === "begin" || e.state === "end", "event.state is good"); + ok(e.tab !== undefined, "event.tab exists"); + + if (lastWhen) { + // We need these in sorted order so that the other checks here make + // sense. + ok( + lastWhen <= e.when, + `Unsorted events, last: ${lastWhen}, this: ${e.when}` + ); + } + lastWhen = e.when; + + if (e.state === "begin") { + is(cur, null, `GC can begin on tab ${e.tab}`); + cur = e.tab; + } else { + is(e.tab, cur, `GC can end on tab ${e.tab}`); + cur = null; + } + } + + is(cur, null, "No GC left running"); +} + +function checkAllCompleted(events, expectTabsCompleted) { + var tabsCompleted = events.filter(e => e.state === "end").map(e => e.tab); + + for (var t of expectTabsCompleted) { + ok(tabsCompleted.includes(t), `Tab ${t} did a GC`); + } +} + +async function setupTabs(num_tabs) { + var pids = []; + + const parent_pid = getProcessID(); + info("Parent process PID is " + parent_pid); + + const tabs = await Promise.all( + Array(num_tabs) + .fill() + .map(_ => { + return BrowserTestUtils.openNewForegroundTab({ + gBrowser, + opening: TEST_PAGE, + forceNewProcess: true, + }); + }) + ); + + for (const [i, tab] of Object.entries(tabs)) { + const tab_pid = await SpecialPowers.spawn( + tab.linkedBrowser, + [], + getProcessID + ); + + info(`Tab ${i} pid is ${tab_pid}`); + isnot(parent_pid, tab_pid, `Tab ${i} is in content process`); + ok(!pids.includes(tab_pid), `Tab ${i} is in a distinct process`); + + pids.push(tab_pid); + } + + return tabs; +} + +function doContentRunNextCollectionTimer() { + content.windowUtils.pokeGC("PAGE_HIDE"); + content.windowUtils.runNextCollectorTimer("PAGE_HIDE"); +} + +function startNextCollection( + tab, + tab_num, + waits, + fn = doContentRunNextCollectionTimer +) { + var browser = tab.linkedBrowser; + + // Finish any currently running GC. + SpecialPowers.spawn(browser, [], () => { + SpecialPowers.Cu.getJSTestingFunctions().finishgc(); + }); + + var waitBegin = SpecialPowers.spawn(browser, [], waitForGCBegin); + var waitEnd = SpecialPowers.spawn(browser, [], waitForGCEnd); + waits.push({ promise: waitBegin, tab: tab_num, state: "begin" }); + waits.push({ promise: waitEnd, tab: tab_num, state: "end" }); + + SpecialPowers.spawn(browser, [], fn); + + // Return these so that the abort GC test can wait for the begin. + return { waitBegin, waitEnd }; +} + +add_task(async function gcOneAtATime() { + SpecialPowers.pushPrefEnv({ + set: [["javascript.options.concurrent_multiprocess_gcs.max", 1]], + }); + + const num_tabs = 12; + var tabs = await setupTabs(num_tabs); + + info("Tabs ready, Asking for GCs"); + var waits = []; + for (var i = 0; i < num_tabs; i++) { + startNextCollection(tabs[i], i, waits); + } + + let order = await resolveInOrder(waits); + // We need these in the order they actually occurred, so far that's how + // they're returned, but we'll sort them to be sure. + order.sort((e1, e2) => e1.when - e2.when); + checkOneAtATime(order); + checkAllCompleted( + order, + Array.from({ length: num_tabs }, (_, n) => n) + ); + + for (var tab of tabs) { + BrowserTestUtils.removeTab(tab); + } + + SpecialPowers.popPrefEnv(); +}); + +add_task(async function gcAbort() { + SpecialPowers.pushPrefEnv({ + set: [["javascript.options.concurrent_multiprocess_gcs.max", 1]], + }); + + const num_tabs = 2; + var tabs = await setupTabs(num_tabs); + + info("Tabs ready, Asking for GCs"); + var waits = []; + + var tab0Waits = startNextCollection(tabs[0], 0, waits, () => { + SpecialPowers.Cu.getJSTestingFunctions().gcslice(1); + }); + await tab0Waits.waitBegin; + + // Tab 0 has started a GC. Now we schedule a GC in tab one. It must not + // begin yet (but we don't check that, gcOneAtATime is assumed to check + // this. + startNextCollection(tabs[1], 1, waits); + + // Request that tab 0 abort, this test checks that tab 1 can now begin. + SpecialPowers.spawn(tabs[0].linkedBrowser, [], () => { + SpecialPowers.Cu.getJSTestingFunctions().abortgc(); + }); + + let order = await resolveInOrder(waits); + // We need these in the order they actually occurred, so far that's how + // they're returned, but we'll sort them to be sure. + order.sort((e1, e2) => e1.when - e2.when); + checkOneAtATime(order); + checkAllCompleted( + order, + Array.from({ length: num_tabs }, (_, n) => n) + ); + + for (var tab of tabs) { + BrowserTestUtils.removeTab(tab); + } + + SpecialPowers.popPrefEnv(); +}); + +add_task(async function gcJSInitiatedDuring() { + SpecialPowers.pushPrefEnv({ + set: [["javascript.options.concurrent_multiprocess_gcs.max", 1]], + }); + + const num_tabs = 3; + var tabs = await setupTabs(num_tabs); + + info("Tabs ready, Asking for GCs"); + var waits = []; + + // Start a GC on tab 0 to consume the scheduler's "token". Zeal mode 10 + // will cause it to run in many slices. + var tab0Waits = startNextCollection(tabs[0], 0, waits, () => { + if (SpecialPowers.Cu.getJSTestingFunctions().gczeal) { + SpecialPowers.Cu.getJSTestingFunctions().gczeal(10); + } + SpecialPowers.Cu.getJSTestingFunctions().gcslice(1); + }); + await tab0Waits.waitBegin; + info("GC on tab 0 has begun"); + + // Request a GC in tab 1, this will be blocked by the ongoing GC in tab 0. + var tab1Waits = startNextCollection(tabs[1], 1, waits); + + // Force a GC to start in tab 1. This won't wait for tab 0. + SpecialPowers.spawn(tabs[1].linkedBrowser, [], () => { + SpecialPowers.Cu.getJSTestingFunctions().gcslice(1); + }); + + await tab1Waits.waitBegin; + info("GC on tab 1 has begun"); + + // The GC in tab 0 should still be running. + var state = await SpecialPowers.spawn(tabs[0].linkedBrowser, [], () => { + return SpecialPowers.Cu.getJSTestingFunctions().gcstate(); + }); + info("State of Tab 0 GC is " + state); + isnot(state, "NotActive", "GC is active in tab 0"); + + // Let the GCs complete, verify that a GC in a 3rd tab can acquire a token. + startNextCollection(tabs[2], 2, waits); + + let order = await resolveInOrder(waits); + info("All GCs finished"); + checkAllCompleted( + order, + Array.from({ length: num_tabs }, (_, n) => n) + ); + + for (var tab of tabs) { + BrowserTestUtils.removeTab(tab); + } + + SpecialPowers.popPrefEnv(); +}); + +add_task(async function gcJSInitiatedBefore() { + SpecialPowers.pushPrefEnv({ + set: [["javascript.options.concurrent_multiprocess_gcs.max", 1]], + }); + + const num_tabs = 8; + var tabs = await setupTabs(num_tabs); + + info("Tabs ready"); + var waits = []; + + // Start a GC on tab 0 to consume the scheduler's first "token". Zeal mode 10 + // will cause it to run in many slices. + info("Force a JS-initiated GC in tab 0"); + var tab0Waits = startNextCollection(tabs[0], 0, waits, () => { + if (SpecialPowers.Cu.getJSTestingFunctions().gczeal) { + SpecialPowers.Cu.getJSTestingFunctions().gczeal(10); + } + SpecialPowers.Cu.getJSTestingFunctions().gcslice(1); + }); + await tab0Waits.waitBegin; + + info("Request GCs in remaining tabs"); + for (var i = 1; i < num_tabs; i++) { + startNextCollection(tabs[i], i, waits); + } + + // The GC in tab 0 should still be running. + var state = await SpecialPowers.spawn(tabs[0].linkedBrowser, [], () => { + return SpecialPowers.Cu.getJSTestingFunctions().gcstate(); + }); + info("State is " + state); + isnot(state, "NotActive", "GC is active in tab 0"); + + let order = await resolveInOrder(waits); + // We need these in the order they actually occurred, so far that's how + // they're returned, but we'll sort them to be sure. + order.sort((e1, e2) => e1.when - e2.when); + checkOneAtATime(order); + checkAllCompleted( + order, + Array.from({ length: num_tabs }, (_, n) => n) + ); + + for (var tab of tabs) { + BrowserTestUtils.removeTab(tab); + } + + SpecialPowers.popPrefEnv(); +}); |