summaryrefslogtreecommitdiffstats
path: root/dom/ipc/tests/browser_gc_schedule.js
diff options
context:
space:
mode:
Diffstat (limited to 'dom/ipc/tests/browser_gc_schedule.js')
-rw-r--r--dom/ipc/tests/browser_gc_schedule.js366
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..10776e8bd6
--- /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();
+});