diff options
Diffstat (limited to 'toolkit/components/perfmonitoring/tests/browser')
18 files changed, 1253 insertions, 0 deletions
diff --git a/toolkit/components/perfmonitoring/tests/browser/.eslintrc.js b/toolkit/components/perfmonitoring/tests/browser/.eslintrc.js new file mode 100644 index 000000000..7c8021192 --- /dev/null +++ b/toolkit/components/perfmonitoring/tests/browser/.eslintrc.js @@ -0,0 +1,7 @@ +"use strict"; + +module.exports = { + "extends": [ + "../../../../../testing/mochitest/browser.eslintrc.js" + ] +}; diff --git a/toolkit/components/perfmonitoring/tests/browser/browser.ini b/toolkit/components/perfmonitoring/tests/browser/browser.ini new file mode 100644 index 000000000..7f4ac8514 --- /dev/null +++ b/toolkit/components/perfmonitoring/tests/browser/browser.ini @@ -0,0 +1,15 @@ +[DEFAULT] +head = head.js +tags = addons +support-files = + browser_Addons_sample.xpi + browser_compartments.html + browser_compartments_frame.html + browser_compartments_script.js + +[browser_AddonWatcher.js] +[browser_compartments.js] +skip-if = os == "linux" && !debug && e10s # Bug 1230018 +[browser_addonPerformanceAlerts.js] +[browser_addonPerformanceAlerts_2.js] +[browser_webpagePerformanceAlerts.js] diff --git a/toolkit/components/perfmonitoring/tests/browser/browser_AddonWatcher.js b/toolkit/components/perfmonitoring/tests/browser/browser_AddonWatcher.js new file mode 100644 index 000000000..b4e80faa7 --- /dev/null +++ b/toolkit/components/perfmonitoring/tests/browser/browser_AddonWatcher.js @@ -0,0 +1,151 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests for AddonWatcher.jsm + +"use strict"; + +requestLongerTimeout(2); + +Cu.import("resource://gre/modules/Promise.jsm", this); +Cu.import("resource://gre/modules/AddonManager.jsm", this); +Cu.import("resource://gre/modules/Services.jsm", this); + +const ADDON_URL = "http://example.com/browser/toolkit/components/perfmonitoring/tests/browser/browser_Addons_sample.xpi"; +const ADDON_ID = "addonwatcher-test@mozilla.com"; + +add_task(function* init() { + info("Installing test add-on"); + let installer = yield new Promise(resolve => AddonManager.getInstallForURL(ADDON_URL, resolve, "application/x-xpinstall")); + if (installer.error) { + throw installer.error; + } + let installed = new Promise((resolve, reject) => installer.addListener({ + onInstallEnded: (_, addon) => resolve(addon), + onInstallFailed: reject, + onDownloadFailed: reject + })); + + // We also need to wait for the add-on to report that it's ready + // to be used in the test. + let ready = TestUtils.topicObserved("test-addonwatcher-ready"); + installer.install(); + + info("Waiting for installation to terminate"); + let addon = yield installed; + + yield ready; + + registerCleanupFunction(() => { + info("Uninstalling test add-on"); + addon.uninstall() + }); + + Preferences.set("browser.addon-watch.warmup-ms", 0); + Preferences.set("browser.addon-watch.freeze-threshold-micros", 0); + Preferences.set("browser.addon-watch.jank-threshold-micros", 0); + Preferences.set("browser.addon-watch.occurrences-between-alerts", 0); + Preferences.set("browser.addon-watch.delay-between-alerts-ms", 0); + Preferences.set("browser.addon-watch.delay-between-freeze-alerts-ms", 0); + Preferences.set("browser.addon-watch.max-simultaneous-reports", 10000); + Preferences.set("browser.addon-watch.deactivate-after-idle-ms", 100000000); + registerCleanupFunction(() => { + for (let k of [ + "browser.addon-watch.warmup-ms", + "browser.addon-watch.freeze-threshold-micros", + "browser.addon-watch.jank-threshold-micros", + "browser.addon-watch.occurrences-between-alerts", + "browser.addon-watch.delay-between-alerts-ms", + "browser.addon-watch.delay-between-freeze-alerts-ms", + "browser.addon-watch.max-simultaneous-reports", + "browser.addon-watch.deactivate-after-idle-ms" + ]) { + Preferences.reset(k); + } + }); + + let oldCanRecord = Services.telemetry.canRecordExtended; + Services.telemetry.canRecordExtended = true; + AddonWatcher.init(); + + registerCleanupFunction(function () { + AddonWatcher.paused = true; + Services.telemetry.canRecordExtended = oldCanRecord; + }); +}); + +// Utility function to burn some resource, trigger a reaction of the add-on watcher +// and check both its notification and telemetry. +let burn_rubber = Task.async(function*({histogramName, topic, expectedMinSum}) { + let detected = false; + let observer = (_, topic, id) => { + Assert.equal(id, ADDON_ID, "The add-on watcher has detected the misbehaving addon"); + detected = true; + }; + + try { + info("Preparing add-on watcher"); + + Services.obs.addObserver(observer, AddonWatcher.TOPIC_SLOW_ADDON_DETECTED, false); + + let histogram = Services.telemetry.getKeyedHistogramById(histogramName); + histogram.clear(); + let snap1 = histogram.snapshot(ADDON_ID); + Assert.equal(snap1.sum, 0, `Histogram ${histogramName} is initially empty for the add-on`); + + let histogramUpdated = false; + do { + info(`Burning some CPU with ${topic}. This should cause an add-on watcher notification`); + yield new Promise(resolve => setTimeout(resolve, 100)); + Services.obs.notifyObservers(null, topic, ""); + yield new Promise(resolve => setTimeout(resolve, 100)); + + let snap2 = histogram.snapshot(ADDON_ID); + histogramUpdated = snap2.sum > 0; + info(`For the moment, histogram ${histogramName} shows ${snap2.sum} => ${histogramUpdated}`); + info(`For the moment, we have ${detected?"":"NOT "}detected the slow add-on`); + } while (!histogramUpdated || !detected); + + let snap3 = histogram.snapshot(ADDON_ID); + Assert.ok(snap3.sum >= expectedMinSum, `Histogram ${histogramName} recorded a gravity of ${snap3.sum}, expecting at least ${expectedMinSum}.`); + } finally { + Services.obs.removeObserver(observer, AddonWatcher.TOPIC_SLOW_ADDON_DETECTED); + } +}); + +// Test that burning CPU will cause the add-on watcher to notice that +// the add-on is misbehaving. +add_task(function* test_burn_CPU() { + yield burn_rubber({ + histogramName: "PERF_MONITORING_SLOW_ADDON_JANK_US", + topic: "test-addonwatcher-burn-some-cpu", + expectedMinSum: 7, + }); +}); + +// Test that burning content CPU will cause the add-on watcher to notice that +// the add-on is misbehaving. +/* +Blocked by bug 1227283. +add_task(function* test_burn_content_CPU() { + yield burn_rubber({ + histogramName: "PERF_MONITORING_SLOW_ADDON_JANK_US", + topic: "test-addonwatcher-burn-some-content-cpu", + expectedMinSum: 7, + }); +}); +*/ + +// Test that burning CPOW will cause the add-on watcher to notice that +// the add-on is misbehaving. +add_task(function* test_burn_CPOW() { + if (!gMultiProcessBrowser) { + info("This is a single-process Firefox, we can't test for CPOW"); + return; + } + yield burn_rubber({ + histogramName: "PERF_MONITORING_SLOW_ADDON_CPOW_US", + topic: "test-addonwatcher-burn-some-cpow", + expectedMinSum: 400, + }); +}); diff --git a/toolkit/components/perfmonitoring/tests/browser/browser_Addons_sample.xpi b/toolkit/components/perfmonitoring/tests/browser/browser_Addons_sample.xpi Binary files differnew file mode 100644 index 000000000..ae5bcc5ff --- /dev/null +++ b/toolkit/components/perfmonitoring/tests/browser/browser_Addons_sample.xpi diff --git a/toolkit/components/perfmonitoring/tests/browser/browser_Addons_sample/bootstrap.js b/toolkit/components/perfmonitoring/tests/browser/browser_Addons_sample/bootstrap.js new file mode 100644 index 000000000..9a5575827 --- /dev/null +++ b/toolkit/components/perfmonitoring/tests/browser/browser_Addons_sample/bootstrap.js @@ -0,0 +1,105 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Sample for browser_AddonWatcher.js + +"use strict"; + +const {utils: Cu, classes: Cc, interfaces: Ci} = Components; + +const TOPIC_BURNCPU = "test-addonwatcher-burn-some-cpu"; +const TOPIC_BURNCPOW = "test-addonwatcher-burn-some-cpow"; +const TOPIC_BURNCONTENTCPU = "test-addonwatcher-burn-some-content-cpu"; +const TOPIC_READY = "test-addonwatcher-ready"; + +const MESSAGE_BURNCPOW = "test-addonwatcher-cpow:init"; +const URL_FRAMESCRIPT = "chrome://addonwatcher-test/content/framescript.js"; + +Cu.import("resource://gre/modules/Services.jsm", this); +const { setTimeout } = Cu.import("resource://gre/modules/Timer.jsm", {}); +Cu.import("resource://gre/modules/Task.jsm", this); + +let globalMM = Cc["@mozilla.org/globalmessagemanager;1"] + .getService(Ci.nsIMessageListenerManager); + +/** + * Spend some time using CPU. + */ +function burnCPU() { + let ignored = []; + let start = Date.now(); + let i = 0; + while (Date.now() - start < 1000) { + ignored[i++ % 2] = i; + } +} + +/** + * Spend some time in CPOW. + */ +function burnCPOW() { + gBurnCPOW(); +} +let gBurnCPOW = null; + +function burnContentCPU() { + setTimeout(() => { try { + gBurnContentCPU() + } catch (ex) { + dump(`test-addon error: ${ex}\n`); + } }, 0); +} +let gBurnContentCPU = null; + +let gTab = null; +let gTabBrowser = null; + +function startup() { + Services.obs.addObserver(burnCPU, TOPIC_BURNCPU, false); + Services.obs.addObserver(burnCPOW, TOPIC_BURNCPOW, false); + Services.obs.addObserver(burnContentCPU, TOPIC_BURNCONTENTCPU, false); + + let windows = Services.wm.getEnumerator("navigator:browser"); + let win = windows.getNext(); + gTabBrowser = win.gBrowser; + gTab = gTabBrowser.addTab("about:robots"); + gBurnContentCPU = function() { + gTab.linkedBrowser.messageManager.sendAsyncMessage("test-addonwatcher-burn-some-content-cpu", {}); + } + + gTab.linkedBrowser.messageManager.loadFrameScript(URL_FRAMESCRIPT, false); + globalMM.loadFrameScript(URL_FRAMESCRIPT, false); + + if (Services.appinfo.browserTabsRemoteAutostart) { + // This profile has e10s enabled, which means we'll want to + // test CPOW traffic. + globalMM.addMessageListener("test-addonwatcher-cpow:init", function waitForCPOW(msg) { + if (Components.utils.isCrossProcessWrapper(msg.objects.burnCPOW)) { + gBurnCPOW = msg.objects.burnCPOW; + globalMM.removeMessageListener("test-addonwatcher-cpow:init", waitForCPOW); + Services.obs.notifyObservers(null, TOPIC_READY, null); + } else { + Cu.reportError("test-addonwatcher-cpow:init didn't give us a CPOW! Expect timeouts."); + } + }); + } else { + // e10s is not enabled, so a CPOW is not necessary - we can report ready + // right away. + Services.obs.notifyObservers(null, TOPIC_READY, null); + } +} + +function shutdown() { + Services.obs.removeObserver(burnCPU, TOPIC_BURNCPU); + Services.obs.removeObserver(burnCPOW, TOPIC_BURNCPOW); + Services.obs.removeObserver(burnContentCPU, TOPIC_BURNCONTENTCPU); + gTabBrowser.removeTab(gTab); +} + +function install() { + // Nothing to do +} + +function uninstall() { + // Nothing to do +} diff --git a/toolkit/components/perfmonitoring/tests/browser/browser_Addons_sample/build.sh b/toolkit/components/perfmonitoring/tests/browser/browser_Addons_sample/build.sh new file mode 100644 index 000000000..28d52ea3a --- /dev/null +++ b/toolkit/components/perfmonitoring/tests/browser/browser_Addons_sample/build.sh @@ -0,0 +1,4 @@ +echo "Rebuilding browser_Addons_sample.xpi..." +zip -r ../browser_Addons_sample.xpi . +echo " +Done! Don't forget to sign it: https://wiki.mozilla.org/EngineeringProductivity/HowTo/SignExtensions"
\ No newline at end of file diff --git a/toolkit/components/perfmonitoring/tests/browser/browser_Addons_sample/chrome.manifest b/toolkit/components/perfmonitoring/tests/browser/browser_Addons_sample/chrome.manifest new file mode 100644 index 000000000..9f53da861 --- /dev/null +++ b/toolkit/components/perfmonitoring/tests/browser/browser_Addons_sample/chrome.manifest @@ -0,0 +1 @@ +content addonwatcher-test content/ diff --git a/toolkit/components/perfmonitoring/tests/browser/browser_Addons_sample/content/framescript.js b/toolkit/components/perfmonitoring/tests/browser/browser_Addons_sample/content/framescript.js new file mode 100644 index 000000000..e7ebc2a61 --- /dev/null +++ b/toolkit/components/perfmonitoring/tests/browser/browser_Addons_sample/content/framescript.js @@ -0,0 +1,23 @@ +/* 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"; + +Components.utils.import("resource://gre/modules/Services.jsm"); + +function burnCPOW(msg) { + dump(`Addon: content burnCPU start ${Math.sin(Math.random())}\n`); + let start = content.performance.now(); + let ignored = []; + while (content.performance.now() - start < 5000) { + ignored[ignored.length % 2] = ignored.length; + } + dump(`Addon: content burnCPU done: ${content.performance.now() - start}\n`); +} + +if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) { + sendAsyncMessage("test-addonwatcher-cpow:init", {}, {burnCPOW}); +} + +addMessageListener("test-addonwatcher-burn-some-content-cpu", burnCPOW); diff --git a/toolkit/components/perfmonitoring/tests/browser/browser_Addons_sample/install.rdf b/toolkit/components/perfmonitoring/tests/browser/browser_Addons_sample/install.rdf new file mode 100644 index 000000000..cae10ace6 --- /dev/null +++ b/toolkit/components/perfmonitoring/tests/browser/browser_Addons_sample/install.rdf @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:em="http://www.mozilla.org/2004/em-rdf#"> + <Description about="urn:mozilla:install-manifest"> + <em:id>addonwatcher-test@mozilla.com</em:id> + <em:version>1.1</em:version> + + <em:targetApplication> + <Description> + <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id> + <em:minVersion>0.3</em:minVersion> + <em:maxVersion>*</em:maxVersion> + </Description> + </em:targetApplication> + + <em:targetApplication> + <Description> + <em:id>toolkit@mozilla.org</em:id> + <em:minVersion>1</em:minVersion> + <em:maxVersion>*</em:maxVersion> + </Description> + </em:targetApplication> + + <em:bootstrap>true</em:bootstrap> + + <em:name>Sample for browser_AddonWatcher.js</em:name> + + </Description> +</RDF> diff --git a/toolkit/components/perfmonitoring/tests/browser/browser_addonPerformanceAlerts.js b/toolkit/components/perfmonitoring/tests/browser/browser_addonPerformanceAlerts.js new file mode 100644 index 000000000..af78f0074 --- /dev/null +++ b/toolkit/components/perfmonitoring/tests/browser/browser_addonPerformanceAlerts.js @@ -0,0 +1,91 @@ +"use strict"; + +/** + * Tests for PerformanceWatcher watching slow addons. + */ + +add_task(function* init() { + // Get rid of buffering. + let service = Cc["@mozilla.org/toolkit/performance-stats-service;1"].getService( + Ci.nsIPerformanceStatsService); + let oldDelay = service.jankAlertBufferingDelay; + + service.jankAlertBufferingDelay = 0 /* ms */; + registerCleanupFunction(() => { + info("Cleanup"); + service.jankAlertBufferingDelay = oldDelay; + }); +}); + +add_task(function* test_install_addon_then_watch_it() { + for (let topic of ["burnCPU", "promiseBurnContentCPU", "promiseBurnCPOW"]) { + info(`Starting subtest ${topic}`); + info("Spawning fake add-on, making sure that the compartment is initialized"); + let addon = new AddonBurner(); + yield addon.promiseInitialized; + addon.burnCPU(); + + info(`Check that burning CPU triggers the real listener, but not the fake listener ${topic}`); + let realListener = new AddonListener(addon.addonId, (group, details) => { + if (group.addonId == addon.addonId) { + return details.highestJank; + } + throw new Error(`I shouldn't have been called with addon ${group.addonId}`); + }); + let fakeListener = new AddonListener(addon.addonId + "-fake-" + Math.random(), group => true); // This listener should never be triggered. + let universalListener = new AddonListener("*", alerts => { + info(`AddonListener: received alerts ${JSON.stringify(alerts)}`); + let alert = alerts.find(({source}) => { + return source.addonId == addon.addonId; + }); + if (alert) { + info(`AddonListener: I found an alert for ${addon.addonId}`); + return alert.details.highestJank; + } + info(`AddonListener: I didn't find any alert for ${addon.addonId}`); + return null; + }); + + // Waiting a little – listeners are buffered. + yield new Promise(resolve => setTimeout(resolve, 100)); + yield addon.run(topic, 10, realListener); + // Waiting a little – listeners are buffered. + yield new Promise(resolve => setTimeout(resolve, 100)); + + Assert.ok(realListener.triggered, `1. The real listener was triggered ${topic}`); + Assert.ok(universalListener.triggered, `1. The universal listener was triggered ${topic}`); + Assert.ok(!fakeListener.triggered, `1. The fake listener was not triggered ${topic}`); + Assert.ok(realListener.result >= addon.jankThreshold, `1. jank is at least ${addon.jankThreshold/1000}ms (${realListener.result/1000}ms) ${topic}`); + + info(`Attempting to remove a performance listener incorrectly, check that this does not hurt our real listener ${topic}`); + Assert.throws(() => PerformanceWatcher.removePerformanceListener({addonId: addon.addonId}, () => {})); + Assert.throws(() => PerformanceWatcher.removePerformanceListener({addonId: addon.addonId + "-unbound-id-" + Math.random()}, realListener.listener)); + + yield addon.run(topic, 10, realListener); + // Waiting a little – listeners are buffered. + yield new Promise(resolve => setTimeout(resolve, 300)); + + Assert.ok(realListener.triggered, `2. The real listener was triggered ${topic}`); + Assert.ok(universalListener.triggered, `2. The universal listener was triggered ${topic}`); + Assert.ok(!fakeListener.triggered, `2. The fake listener was not triggered ${topic}`); + Assert.ok(realListener.result >= 200000, `2. jank is at least 300ms (${realListener.result/1000}ms) ${topic}`); + + info(`Attempting to remove correctly, check if the listener is still triggered ${topic}`); + realListener.unregister(); + yield addon.run(topic, 3, realListener); + Assert.ok(!realListener.triggered, `3. After being unregistered, the real listener was not triggered ${topic}`); + Assert.ok(universalListener.triggered, `3. The universal listener is still triggered ${topic}`); + + info("Unregistering universal listener"); + // Waiting a little – listeners are buffered. + yield new Promise(resolve => setTimeout(resolve, 100)); + universalListener.unregister(); + // Waiting a little – listeners are buffered. + yield new Promise(resolve => setTimeout(resolve, 100)); + yield addon.run(topic, 3, realListener); + Assert.ok(!universalListener.triggered, `4. After being unregistered, the universal listener is not triggered ${topic}`); + + fakeListener.unregister(); + addon.dispose(); + } +}); diff --git a/toolkit/components/perfmonitoring/tests/browser/browser_addonPerformanceAlerts_2.js b/toolkit/components/perfmonitoring/tests/browser/browser_addonPerformanceAlerts_2.js new file mode 100644 index 000000000..d39c38b1f --- /dev/null +++ b/toolkit/components/perfmonitoring/tests/browser/browser_addonPerformanceAlerts_2.js @@ -0,0 +1,25 @@ +"use strict"; + +/** + * Tests for PerformanceWatcher watching slow addons. + */ + +add_task(function* test_watch_addon_then_install_it() { + for (let topic of ["burnCPU", "promiseBurnContentCPU", "promiseBurnCPOW"]) { + let addonId = "addon:test_watch_addons_before_installing" + Math.random(); + let realListener = new AddonListener(addonId, (group, details) => { + if (group.addonId == addonId) { + return details.highestJank; + } + throw new Error(`I shouldn't have been called with addon ${group.addonId}`); + }); + + info("Now install the add-on, *after* having installed the listener"); + let addon = new AddonBurner(addonId); + + Assert.ok((yield addon.run(topic, 10, realListener)), `5. The real listener was triggered ${topic}`); + Assert.ok(realListener.result >= addon.jankThreshold, `5. jank is at least ${addon.jankThreshold/1000}ms (${realListener.result}µs) ${topic}`); + realListener.unregister(); + addon.dispose(); + } +}); diff --git a/toolkit/components/perfmonitoring/tests/browser/browser_compartments.html b/toolkit/components/perfmonitoring/tests/browser/browser_compartments.html new file mode 100644 index 000000000..d7ee6c418 --- /dev/null +++ b/toolkit/components/perfmonitoring/tests/browser/browser_compartments.html @@ -0,0 +1,20 @@ +<!DOCTYPE html> +<html> +<head> + <title> + Main frame for test browser_compartments.js + </title> +</head> +<body> +Main frame. + +<iframe src="browser_compartments_frame.html?frame=1"> + Subframe 1 +</iframe> + +<iframe src="browser_compartments_frame.html?frame=2"> + Subframe 2. +</iframe> + +</body> +</html> 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}); +}); diff --git a/toolkit/components/perfmonitoring/tests/browser/browser_compartments_frame.html b/toolkit/components/perfmonitoring/tests/browser/browser_compartments_frame.html new file mode 100644 index 000000000..69edfe871 --- /dev/null +++ b/toolkit/components/perfmonitoring/tests/browser/browser_compartments_frame.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<html> +<head> + <title> + Subframe for test browser_compartments.html (do not change this title) + </title> + <script src="browser_compartments_script.js"></script> +</head> +<body> +Subframe loaded. +</body> +</html> diff --git a/toolkit/components/perfmonitoring/tests/browser/browser_compartments_script.js b/toolkit/components/perfmonitoring/tests/browser/browser_compartments_script.js new file mode 100644 index 000000000..3d5f7114f --- /dev/null +++ b/toolkit/components/perfmonitoring/tests/browser/browser_compartments_script.js @@ -0,0 +1,29 @@ + +var carryOn = true; + +window.addEventListener("message", e => { + console.log("frame content", "message", e); + if ("title" in e.data) { + document.title = e.data.title; + } + if ("stop" in e.data) { + carryOn = false; + } +}); + +// Use some CPU. +var interval = window.setInterval(() => { + if (!carryOn) { + window.clearInterval(interval); + return; + } + + // Compute an arbitrary value, print it out to make sure that the JS + // engine doesn't discard all our computation. + var date = Date.now(); + var array = []; + var i = 0; + while (Date.now() - date <= 100) { + array[i%2] = i++; + } +}, 300); diff --git a/toolkit/components/perfmonitoring/tests/browser/browser_webpagePerformanceAlerts.js b/toolkit/components/perfmonitoring/tests/browser/browser_webpagePerformanceAlerts.js new file mode 100644 index 000000000..eb908c8db --- /dev/null +++ b/toolkit/components/perfmonitoring/tests/browser/browser_webpagePerformanceAlerts.js @@ -0,0 +1,111 @@ +"use strict"; + +/** + * Tests for PerformanceWatcher watching slow web pages. + */ + + /** + * Simulate a slow webpage. + */ +function WebpageBurner() { + CPUBurner.call(this, "http://example.com/browser/toolkit/components/perfmonitoring/tests/browser/browser_compartments.html?test=" + Math.random(), 300000); +} +WebpageBurner.prototype = Object.create(CPUBurner.prototype); +WebpageBurner.prototype.promiseBurnContentCPU = function() { + return promiseContentResponse(this._browser, "test-performance-watcher:burn-content-cpu", {}); +}; + +function WebpageListener(windowId, accept) { + info(`Creating WebpageListener for ${windowId}`); + AlertListener.call(this, accept, { + register: () => PerformanceWatcher.addPerformanceListener({windowId}, this.listener), + unregister: () => PerformanceWatcher.removePerformanceListener({windowId}, this.listener) + }); +} +WebpageListener.prototype = Object.create(AlertListener.prototype); + +add_task(function* init() { + // Get rid of buffering. + let service = Cc["@mozilla.org/toolkit/performance-stats-service;1"].getService( + Ci.nsIPerformanceStatsService); + let oldDelay = service.jankAlertBufferingDelay; + + service.jankAlertBufferingDelay = 0 /* ms */; + registerCleanupFunction(() => { + info("Cleanup"); + service.jankAlertBufferingDelay = oldDelay; + }); +}); + +add_task(function* test_open_window_then_watch_it() { + let burner = new WebpageBurner(); + yield burner.promiseInitialized; + yield burner.promiseBurnContentCPU(); + + info(`Check that burning CPU triggers the real listener, but not the fake listener`); + let realListener = new WebpageListener(burner.windowId, (group, details) => { + info(`test: realListener for ${burner.tab.linkedBrowser.outerWindowID}: ${group}, ${details}\n`); + Assert.equal(group.windowId, burner.windowId, "We should not receive data meant for another group"); + return details; + }); // This listener should be triggered. + + info(`Creating fake burner`); + let otherTab = gBrowser.addTab(); + yield BrowserTestUtils.browserLoaded(otherTab.linkedBrowser); + info(`Check that burning CPU triggers the real listener, but not the fake listener`); + let fakeListener = new WebpageListener(otherTab.linkedBrowser.outerWindowID, group => group.windowId == burner.windowId); // This listener should never be triggered. + let universalListener = new WebpageListener(0, alerts => + alerts.find(alert => alert.source.windowId == burner.windowId) + ); + + // Waiting a little – listeners are buffered. + yield new Promise(resolve => setTimeout(resolve, 100)); + + yield burner.run("promiseBurnContentCPU", 20, realListener); + Assert.ok(realListener.triggered, `1. The real listener was triggered`); + Assert.ok(universalListener.triggered, `1. The universal listener was triggered`); + Assert.ok(!fakeListener.triggered, `1. The fake listener was not triggered`); + + if (realListener.result) { + Assert.ok(realListener.result.highestJank >= 300, `1. jank is at least 300ms (${realListener.result.highestJank}ms)`); + } + + info(`Attempting to remove a performance listener incorrectly, check that this does not hurt our real listener`); + Assert.throws(() => PerformanceWatcher.removePerformanceListener({addonId: addon.addonId}, () => {})); + Assert.throws(() => PerformanceWatcher.removePerformanceListener({addonId: addon.addonId + "-unbound-id-" + Math.random()}, realListener.listener)); + + // Waiting a little – listeners are buffered. + yield new Promise(resolve => setTimeout(resolve, 100)); + yield burner.run("promiseBurnContentCPU", 20, realListener); + // Waiting a little – listeners are buffered. + yield new Promise(resolve => setTimeout(resolve, 100)); + + Assert.ok(realListener.triggered, `2. The real listener was triggered`); + Assert.ok(universalListener.triggered, `2. The universal listener was triggered`); + Assert.ok(!fakeListener.triggered, `2. The fake listener was not triggered`); + if (realListener.result) { + Assert.ok(realListener.result.highestJank >= 300, `2. jank is at least 300ms (${realListener.jank}ms)`); + } + + info(`Attempting to remove correctly, check if the listener is still triggered`); + // Waiting a little – listeners are buffered. + yield new Promise(resolve => setTimeout(resolve, 100)); + realListener.unregister(); + + // Waiting a little – listeners are buffered. + yield new Promise(resolve => setTimeout(resolve, 100)); + yield burner.run("promiseBurnContentCPU", 3, realListener); + Assert.ok(!realListener.triggered, `3. After being unregistered, the real listener was not triggered`); + Assert.ok(universalListener.triggered, `3. The universal listener is still triggered`); + + universalListener.unregister(); + + // Waiting a little – listeners are buffered. + yield new Promise(resolve => setTimeout(resolve, 100)); + yield burner.run("promiseBurnContentCPU", 3, realListener); + Assert.ok(!universalListener.triggered, `4. After being unregistered, the universal listener is not triggered`); + + fakeListener.unregister(); + burner.dispose(); + gBrowser.removeTab(otherTab); +}); diff --git a/toolkit/components/perfmonitoring/tests/browser/head.js b/toolkit/components/perfmonitoring/tests/browser/head.js new file mode 100644 index 000000000..92258fd1b --- /dev/null +++ b/toolkit/components/perfmonitoring/tests/browser/head.js @@ -0,0 +1,287 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +var { utils: Cu, interfaces: Ci, classes: Cc } = Components; + +Cu.import("resource://gre/modules/Promise.jsm", this); +Cu.import("resource://gre/modules/AddonManager.jsm", this); +Cu.import("resource://gre/modules/AddonWatcher.jsm", this); +Cu.import("resource://gre/modules/PerformanceWatcher.jsm", this); +Cu.import("resource://gre/modules/Services.jsm", this); +Cu.import("resource://testing-common/ContentTaskUtils.jsm", this); + +/** + * Base class for simulating slow addons/webpages. + */ +function CPUBurner(url, jankThreshold) { + info(`CPUBurner: Opening tab for ${url}\n`); + this.url = url; + this.tab = gBrowser.addTab(url); + this.jankThreshold = jankThreshold; + let browser = this.tab.linkedBrowser; + this._browser = browser; + ContentTask.spawn(this._browser, null, CPUBurner.frameScript); + this.promiseInitialized = BrowserTestUtils.browserLoaded(browser); +} +CPUBurner.prototype = { + get windowId() { + return this._browser.outerWindowID; + }, + /** + * Burn CPU until it triggers a listener with the specified jank threshold. + */ + run: Task.async(function*(burner, max, listener) { + listener.reset(); + for (let i = 0; i < max; ++i) { + yield new Promise(resolve => setTimeout(resolve, 50)); + try { + yield this[burner](); + } catch (ex) { + return false; + } + if (listener.triggered && listener.result >= this.jankThreshold) { + return true; + } + } + return false; + }), + dispose: function() { + info(`CPUBurner: Closing tab for ${this.url}\n`); + gBrowser.removeTab(this.tab); + } +}; +// This function is injected in all frames +CPUBurner.frameScript = function() { + try { + "use strict"; + + const { utils: Cu, classes: Cc, interfaces: Ci } = Components; + let sandboxes = new Map(); + let getSandbox = function(addonId) { + let sandbox = sandboxes.get(addonId); + if (!sandbox) { + sandbox = Components.utils.Sandbox(Services.scriptSecurityManager.getSystemPrincipal(), { addonId }); + sandboxes.set(addonId, sandbox); + } + return sandbox; + }; + + let burnCPU = function() { + var start = Date.now(); + var ignored = []; + while (Date.now() - start < 500) { + ignored[ignored.length % 2] = ignored.length; + } + }; + let burnCPUInSandbox = function(addonId) { + let sandbox = getSandbox(addonId); + Cu.evalInSandbox(burnCPU.toSource() + "()", sandbox); + }; + + { + let topic = "test-performance-watcher:burn-content-cpu"; + addMessageListener(topic, function(msg) { + try { + if (msg.data && msg.data.addonId) { + burnCPUInSandbox(msg.data.addonId); + } else { + burnCPU(); + } + sendAsyncMessage(topic, {}); + } catch (ex) { + dump(`This is the content attempting to burn CPU: error ${ex}\n`); + dump(`${ex.stack}\n`); + } + }); + } + + // Bind the function to the global context or it might be GC'd during test + // causing failures (bug 1230027) + this.burnCPOWInSandbox = function(addonId) { + try { + burnCPUInSandbox(addonId); + } catch (ex) { + dump(`This is the addon attempting to burn CPOW: error ${ex}\n`); + dump(`${ex.stack}\n`); + } + } + + sendAsyncMessage("test-performance-watcher:cpow-init", {}, { + burnCPOWInSandbox: this.burnCPOWInSandbox + }); + + } catch (ex) { + Cu.reportError("This is the addon: error " + ex); + Cu.reportError(ex.stack); + } +}; + +/** + * Base class for listening to slow group alerts + */ +function AlertListener(accept, {register, unregister}) { + this.listener = (...args) => { + if (this._unregistered) { + throw new Error("Listener was unregistered"); + } + let result = accept(...args); + if (!result) { + return; + } + this.result = result; + this.triggered = true; + return; + }; + this.triggered = false; + this.result = null; + this._unregistered = false; + this._unregister = unregister; + registerCleanupFunction(() => { + this.unregister(); + }); + register(); +} +AlertListener.prototype = { + unregister: function() { + this.reset(); + if (this._unregistered) { + info(`head.js: No need to unregister, we're already unregistered.\n`); + return; + } + info(`head.js: Unregistering listener.\n`); + this._unregistered = true; + this._unregister(); + info(`head.js: Unregistration complete.\n`); + }, + reset: function() { + this.triggered = false; + this.result = null; + }, +}; + +/** + * Simulate a slow add-on. + */ +function AddonBurner(addonId = "fake add-on id: " + Math.random()) { + this.jankThreshold = 200000; + CPUBurner.call(this, `http://example.com/?uri=${addonId}`, this.jankThreshold); + this._addonId = addonId; + this._sandbox = Components.utils.Sandbox(Services.scriptSecurityManager.getSystemPrincipal(), { addonId: this._addonId }); + this._CPOWBurner = null; + + this._promiseCPOWBurner = new Promise(resolve => { + this._browser.messageManager.addMessageListener("test-performance-watcher:cpow-init", msg => { + // Note that we cannot resolve Promises with CPOWs now that they + // have been outlawed in bug 1233497, so we stash it in the + // AddonBurner instance instead. + this._CPOWBurner = msg.objects.burnCPOWInSandbox; + resolve(); + }); + }); +} +AddonBurner.prototype = Object.create(CPUBurner.prototype); +Object.defineProperty(AddonBurner.prototype, "addonId", { + get: function() { + return this._addonId; + } +}); + +/** + * Simulate slow code being executed by the add-on in the chrome. + */ +AddonBurner.prototype.burnCPU = function() { + Cu.evalInSandbox(AddonBurner.burnCPU.toSource() + "()", this._sandbox); +}; + +/** + * Simulate slow code being executed by the add-on in a CPOW. + */ +AddonBurner.prototype.promiseBurnCPOW = Task.async(function*() { + yield this._promiseCPOWBurner; + ok(this._CPOWBurner, "Got the CPOW burner"); + let burner = this._CPOWBurner; + info("Parent: Preparing to burn CPOW"); + try { + yield burner(this._addonId); + info("Parent: Done burning CPOW"); + } catch (ex) { + info(`Parent: Error burning CPOW: ${ex}\n`); + info(ex.stack + "\n"); + } +}); + +/** + * Simulate slow code being executed by the add-on in the content. + */ +AddonBurner.prototype.promiseBurnContentCPU = function() { + return promiseContentResponse(this._browser, "test-performance-watcher:burn-content-cpu", {addonId: this._addonId}); +}; +AddonBurner.burnCPU = function() { + var start = Date.now(); + var ignored = []; + while (Date.now() - start < 500) { + ignored[ignored.length % 2] = ignored.length; + } +}; + + +function AddonListener(addonId, accept) { + let target = {addonId}; + AlertListener.call(this, accept, { + register: () => { + info(`AddonListener: registering ${JSON.stringify(target, null, "\t")}`); + PerformanceWatcher.addPerformanceListener({addonId}, this.listener); + }, + unregister: () => { + info(`AddonListener: unregistering ${JSON.stringify(target, null, "\t")}`); + PerformanceWatcher.removePerformanceListener({addonId}, this.listener); + } + }); +} +AddonListener.prototype = Object.create(AlertListener.prototype); + +function promiseContentResponse(browser, name, message) { + let mm = browser.messageManager; + let promise = new Promise(resolve => { + function removeListener() { + mm.removeMessageListener(name, listener); + } + + function listener(msg) { + removeListener(); + resolve(msg.data); + } + + mm.addMessageListener(name, listener); + registerCleanupFunction(removeListener); + }); + mm.sendAsyncMessage(name, message); + return promise; +} +function promiseContentResponseOrNull(browser, name, message) { + if (!browser.messageManager) { + return null; + } + return promiseContentResponse(browser, name, message); +} + +/** + * `true` if we are running an OS in which the OS performance + * clock has a low precision and might unpredictably + * never be updated during the execution of the test. + */ +function hasLowPrecision() { + let [sysName, sysVersion] = [Services.sysinfo.getPropertyAsAString("name"), Services.sysinfo.getPropertyAsDouble("version")]; + info(`Running ${sysName} version ${sysVersion}`); + + if (sysName == "Windows_NT" && sysVersion < 6) { + info("Running old Windows, need to deactivate tests due to bad precision."); + return true; + } + if (sysName == "Linux" && sysVersion <= 2.6) { + info("Running old Linux, need to deactivate tests due to bad precision."); + return true; + } + info("This platform has good precision.") + return false; +} diff --git a/toolkit/components/perfmonitoring/tests/browser/install.rdf b/toolkit/components/perfmonitoring/tests/browser/install.rdf new file mode 100644 index 000000000..65add014f --- /dev/null +++ b/toolkit/components/perfmonitoring/tests/browser/install.rdf @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:em="http://www.mozilla.org/2004/em-rdf#"> + <Description about="urn:mozilla:install-manifest"> + <em:id>addonwatcher-test@mozilla.com</em:id> + <em:version>1.0</em:version> + + <em:targetApplication> + <Description> + <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id> + <em:minVersion>0.3</em:minVersion> + <em:maxVersion>*</em:maxVersion> + </Description> + </em:targetApplication> + + <em:targetApplication> + <Description> + <em:id>toolkit@mozilla.org</em:id> + <em:minVersion>1</em:minVersion> + <em:maxVersion>*</em:maxVersion> + </Description> + </em:targetApplication> + + <em:bootstrap>true</em:bootstrap> + + <em:name>Sample for browser_AddonWatcher.js</em:name> + + </Description> +</RDF> |