summaryrefslogtreecommitdiffstats
path: root/toolkit/components/perfmonitoring/tests/browser/browser_compartments.js
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/perfmonitoring/tests/browser/browser_compartments.js')
-rw-r--r--toolkit/components/perfmonitoring/tests/browser/browser_compartments.js312
1 files changed, 312 insertions, 0 deletions
diff --git a/toolkit/components/perfmonitoring/tests/browser/browser_compartments.js b/toolkit/components/perfmonitoring/tests/browser/browser_compartments.js
new file mode 100644
index 000000000..f04fefb33
--- /dev/null
+++ b/toolkit/components/perfmonitoring/tests/browser/browser_compartments.js
@@ -0,0 +1,312 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Test that we see jank that takes place in a webpage,
+ * and that jank from several iframes are actually charged
+ * to the top window.
+ */
+Cu.import("resource://gre/modules/PerformanceStats.jsm", this);
+Cu.import("resource://gre/modules/Services.jsm", this);
+Cu.import("resource://testing-common/ContentTask.jsm", this);
+
+
+const URL = "http://example.com/browser/toolkit/components/perfmonitoring/tests/browser/browser_compartments.html?test=" + Math.random();
+const PARENT_TITLE = `Main frame for test browser_compartments.js ${Math.random()}`;
+const FRAME_TITLE = `Subframe for test browser_compartments.js ${Math.random()}`;
+
+const PARENT_PID = Services.appinfo.processID;
+
+// This function is injected as source as a frameScript
+function frameScript() {
+ try {
+ "use strict";
+
+ const { utils: Cu, classes: Cc, interfaces: Ci } = Components;
+ Cu.import("resource://gre/modules/PerformanceStats.jsm");
+ Cu.import("resource://gre/modules/Services.jsm");
+
+ // Make sure that the stopwatch is now active.
+ let monitor = PerformanceStats.getMonitor(["jank", "cpow", "ticks", "compartments"]);
+
+ addMessageListener("compartments-test:getStatistics", () => {
+ try {
+ monitor.promiseSnapshot().then(snapshot => {
+ sendAsyncMessage("compartments-test:getStatistics", {snapshot, pid: Services.appinfo.processID});
+ });
+ } catch (ex) {
+ Cu.reportError("Error in content (getStatistics): " + ex);
+ Cu.reportError(ex.stack);
+ }
+ });
+
+ addMessageListener("compartments-test:setTitles", titles => {
+ try {
+ content.document.title = titles.data.parent;
+ for (let i = 0; i < content.frames.length; ++i) {
+ content.frames[i].postMessage({title: titles.data.frames}, "*");
+ }
+ console.log("content", "Done setting titles", content.document.title);
+ sendAsyncMessage("compartments-test:setTitles");
+ } catch (ex) {
+ Cu.reportError("Error in content (setTitles): " + ex);
+ Cu.reportError(ex.stack);
+ }
+ });
+ } catch (ex) {
+ Cu.reportError("Error in content (setup): " + ex);
+ Cu.reportError(ex.stack);
+ }
+}
+
+// A variant of `Assert` that doesn't spam the logs
+// in case of success.
+var SilentAssert = {
+ equal: function(a, b, msg) {
+ if (a == b) {
+ return;
+ }
+ Assert.equal(a, b, msg);
+ },
+ notEqual: function(a, b, msg) {
+ if (a != b) {
+ return;
+ }
+ Assert.notEqual(a, b, msg);
+ },
+ ok: function(a, msg) {
+ if (a) {
+ return;
+ }
+ Assert.ok(a, msg);
+ },
+ leq: function(a, b, msg) {
+ this.ok(a <= b, `${msg}: ${a} <= ${b}`);
+ }
+};
+
+var isShuttingDown = false;
+function monotinicity_tester(source, testName) {
+ // In the background, check invariants:
+ // - numeric data can only ever increase;
+ // - the name, addonId, isSystem of a component never changes;
+ // - the name, addonId, isSystem of the process data;
+ // - there is at most one component with a combination of `name` and `addonId`;
+ // - types, etc.
+ let previous = {
+ processData: null,
+ componentsMap: new Map(),
+ };
+
+ let sanityCheck = function(prev, next) {
+ if (prev == null) {
+ return;
+ }
+ for (let k of ["groupId", "addonId", "isSystem"]) {
+ SilentAssert.equal(prev[k], next[k], `Sanity check (${testName}): ${k} hasn't changed (${prev.name}).`);
+ }
+ for (let [probe, k] of [
+ ["jank", "totalUserTime"],
+ ["jank", "totalSystemTime"],
+ ["cpow", "totalCPOWTime"],
+ ["ticks", "ticks"]
+ ]) {
+ SilentAssert.equal(typeof next[probe][k], "number", `Sanity check (${testName}): ${k} is a number.`);
+ SilentAssert.leq(prev[probe][k], next[probe][k], `Sanity check (${testName}): ${k} is monotonic.`);
+ SilentAssert.leq(0, next[probe][k], `Sanity check (${testName}): ${k} is >= 0.`)
+ }
+ SilentAssert.equal(prev.jank.durations.length, next.jank.durations.length);
+ for (let i = 0; i < next.jank.durations.length; ++i) {
+ SilentAssert.ok(typeof next.jank.durations[i] == "number" && next.jank.durations[i] >= 0,
+ `Sanity check (${testName}): durations[${i}] is a non-negative number.`);
+ SilentAssert.leq(prev.jank.durations[i], next.jank.durations[i],
+ `Sanity check (${testName}): durations[${i}] is monotonic.`);
+ }
+ for (let i = 0; i < next.jank.durations.length - 1; ++i) {
+ SilentAssert.leq(next.jank.durations[i + 1], next.jank.durations[i],
+ `Sanity check (${testName}): durations[${i}] >= durations[${i + 1}].`)
+ }
+ };
+ let iteration = 0;
+ let frameCheck = Task.async(function*() {
+ if (isShuttingDown) {
+ window.clearInterval(interval);
+ return;
+ }
+ let name = `${testName}: ${iteration++}`;
+ let result = yield source();
+ if (!result) {
+ // This can happen at the end of the test when we attempt
+ // to communicate too late with the content process.
+ window.clearInterval(interval);
+ return;
+ }
+ let {pid, snapshot} = result;
+
+ // Sanity check on the process data.
+ sanityCheck(previous.processData, snapshot.processData);
+ SilentAssert.equal(snapshot.processData.isSystem, true);
+ SilentAssert.equal(snapshot.processData.name, "<process>");
+ SilentAssert.equal(snapshot.processData.addonId, "");
+ SilentAssert.equal(snapshot.processData.processId, pid);
+ previous.procesData = snapshot.processData;
+
+ // Sanity check on components data.
+ let map = new Map();
+ for (let item of snapshot.componentsData) {
+ for (let [probe, k] of [
+ ["jank", "totalUserTime"],
+ ["jank", "totalSystemTime"],
+ ["cpow", "totalCPOWTime"]
+ ]) {
+ // Note that we cannot expect components data to be always smaller
+ // than process data, as `getrusage` & co are not monotonic.
+ SilentAssert.leq(item[probe][k], 3 * snapshot.processData[probe][k],
+ `Sanity check (${name}): ${k} of component is not impossibly larger than that of process`);
+ }
+
+ let isCorrectPid = (item.processId == pid && !item.isChildProcess)
+ || (item.processId != pid && item.isChildProcess);
+ SilentAssert.ok(isCorrectPid, `Pid check (${name}): the item comes from the right process`);
+
+ let key = item.groupId;
+ if (map.has(key)) {
+ let old = map.get(key);
+ Assert.ok(false, `Component ${key} has already been seen. Latest: ${item.addonId||item.name}, previous: ${old.addonId||old.name}`);
+ }
+ map.set(key, item);
+ }
+ for (let item of snapshot.componentsData) {
+ if (!item.parentId) {
+ continue;
+ }
+ let parent = map.get(item.parentId);
+ SilentAssert.ok(parent, `The parent exists ${item.parentId}`);
+
+ for (let [probe, k] of [
+ ["jank", "totalUserTime"],
+ ["jank", "totalSystemTime"],
+ ["cpow", "totalCPOWTime"]
+ ]) {
+ // Note that we cannot expect components data to be always smaller
+ // than parent data, as `getrusage` & co are not monotonic.
+ SilentAssert.leq(item[probe][k], 2 * parent[probe][k],
+ `Sanity check (${testName}): ${k} of component is not impossibly larger than that of parent`);
+ }
+ }
+ for (let [key, item] of map) {
+ sanityCheck(previous.componentsMap.get(key), item);
+ previous.componentsMap.set(key, item);
+ }
+ });
+ let interval = window.setInterval(frameCheck, 300);
+ registerCleanupFunction(() => {
+ window.clearInterval(interval);
+ });
+}
+
+add_task(function* test() {
+ let monitor = PerformanceStats.getMonitor(["jank", "cpow", "ticks"]);
+
+ info("Extracting initial state");
+ let stats0 = yield monitor.promiseSnapshot();
+ Assert.notEqual(stats0.componentsData.length, 0, "There is more than one component");
+ Assert.ok(!stats0.componentsData.find(stat => stat.name.indexOf(URL) != -1),
+ "The url doesn't appear yet");
+
+ let newTab = gBrowser.addTab();
+ let browser = newTab.linkedBrowser;
+ // Setup monitoring in the tab
+ info("Setting up monitoring in the tab");
+ yield ContentTask.spawn(newTab.linkedBrowser, null, frameScript);
+
+ info("Opening URL");
+ newTab.linkedBrowser.loadURI(URL);
+
+ if (Services.sysinfo.getPropertyAsAString("name") == "Windows_NT") {
+ info("Deactivating sanity checks under Windows (bug 1151240)");
+ } else {
+ info("Setting up sanity checks");
+ monotinicity_tester(() => monitor.promiseSnapshot().then(snapshot => ({snapshot, pid: PARENT_PID})), "parent process");
+ monotinicity_tester(() => promiseContentResponseOrNull(browser, "compartments-test:getStatistics", null), "content process" );
+ }
+
+ let skipTotalUserTime = hasLowPrecision();
+
+
+ while (true) {
+ yield new Promise(resolve => setTimeout(resolve, 100));
+
+ // We may have race conditions with DOM loading.
+ // Don't waste too much brainpower here, let's just ask
+ // repeatedly for the title to be changed, until this works.
+ info("Setting titles");
+ yield promiseContentResponse(browser, "compartments-test:setTitles", {
+ parent: PARENT_TITLE,
+ frames: FRAME_TITLE
+ });
+ info("Titles set");
+
+ let {snapshot: stats} = (yield promiseContentResponse(browser, "compartments-test:getStatistics", null));
+
+ // Attach titles to components.
+ let titles = [];
+ let map = new Map();
+ let windows = Services.wm.getEnumerator("navigator:browser");
+ while (windows.hasMoreElements()) {
+ let window = windows.getNext();
+ let tabbrowser = window.gBrowser;
+ for (let browser of tabbrowser.browsers) {
+ let id = browser.outerWindowID; // May be `null` if the browser isn't loaded yet
+ if (id != null) {
+ map.set(id, browser);
+ }
+ }
+ }
+ for (let stat of stats.componentsData) {
+ if (!stat.windowId) {
+ continue;
+ }
+ let browser = map.get(stat.windowId);
+ if (!browser) {
+ continue;
+ }
+ let title = browser.contentTitle;
+ if (title) {
+ stat.title = title;
+ titles.push(title);
+ }
+ }
+
+ // While the webpage consists in three compartments, we should see only
+ // one `PerformanceData` in `componentsData`. Its `name` is undefined
+ // (could be either the main frame or one of its subframes), but its
+ // `title` should be the title of the main frame.
+ info(`Searching for frame title '${FRAME_TITLE}' in ${JSON.stringify(titles)} (I hope not to find it)`);
+ Assert.ok(!titles.includes(FRAME_TITLE), "Searching by title, the frames don't show up in the list of components");
+
+ info(`Searching for window title '${PARENT_TITLE}' in ${JSON.stringify(titles)} (I hope to find it)`);
+ let parent = stats.componentsData.find(x => x.title == PARENT_TITLE);
+ if (!parent) {
+ info("Searching by title, we didn't find the main frame");
+ continue;
+ }
+ info("Found the main frame");
+
+ if (skipTotalUserTime) {
+ info("Not looking for total user time on this platform, we're done");
+ break;
+ } else if (parent.jank.totalUserTime > 1000) {
+ info("Enough CPU time detected, we're done");
+ break;
+ } else {
+ info(`Not enough CPU time detected: ${parent.jank.totalUserTime}`);
+ }
+ }
+ isShuttingDown = true;
+
+ // Cleanup
+ gBrowser.removeTab(newTab, {skipPermitUnload: true});
+});