diff options
Diffstat (limited to 'testing/talos/talos/tests/tabswitch')
9 files changed, 573 insertions, 0 deletions
diff --git a/testing/talos/talos/tests/tabswitch/actors/TalosTabSwitchChild.sys.mjs b/testing/talos/talos/tests/tabswitch/actors/TalosTabSwitchChild.sys.mjs new file mode 100644 index 0000000000..5b27d677e3 --- /dev/null +++ b/testing/talos/talos/tests/tabswitch/actors/TalosTabSwitchChild.sys.mjs @@ -0,0 +1,31 @@ +/* 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/. */ + +import { RemotePageChild } from "resource://gre/actors/RemotePageChild.sys.mjs"; + +export class TalosTabSwitchChild extends RemotePageChild { + actorCreated() { + // Ignore about:blank pages that can get here. + if (!String(this.document.location).startsWith("about:tabswitch")) { + return; + } + + // If an error occurs, it was probably already added by an earlier test run. + try { + this.addPage("about:tabswitch", { + RPMSendQuery: ["tabswitch-do-test"], + }); + } catch {} + + super.actorCreated(); + } + + handleEvent(event) {} + + receiveMessage(message) { + if (message.name == "GarbageCollect") { + this.contentWindow.windowUtils.garbageCollect(); + } + } +} diff --git a/testing/talos/talos/tests/tabswitch/actors/TalosTabSwitchParent.sys.mjs b/testing/talos/talos/tests/tabswitch/actors/TalosTabSwitchParent.sys.mjs new file mode 100644 index 0000000000..3b609af0a7 --- /dev/null +++ b/testing/talos/talos/tests/tabswitch/actors/TalosTabSwitchParent.sys.mjs @@ -0,0 +1,344 @@ +/* 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/. */ + +let TalosParentProfiler; + +export class TalosTabSwitchParent extends JSWindowActorParent { + receiveMessage(message) { + if (message.name == "tabswitch-do-test") { + let browser = this.browsingContext.top.embedderElement; + return this.test(browser.ownerGlobal); + } + + return undefined; + } + + /** + * Returns a Promise that resolves when browser-delayed-startup-finished + * fires for a given window + * + * @param win + * The window that we're waiting for the notification for. + * @returns Promise + */ + waitForDelayedStartup(win) { + return new Promise(resolve => { + const topic = "browser-delayed-startup-finished"; + Services.obs.addObserver(function onStartup(subject) { + if (win == subject) { + Services.obs.removeObserver(onStartup, topic); + resolve(); + } + }, topic); + }); + } + + /** + * For some <xul:tabbrowser>, loads a collection of URLs as new tabs + * in that browser. + * + * @param gBrowser (<xul:tabbrowser>) + * The <xul:tabbrowser> in which to load the new tabs. + * @param urls (Array) + * An array of URL strings to be loaded as new tabs. + * @returns Promise + * Resolves once all tabs have finished loading. + */ + loadTabs(gBrowser, urls) { + return new Promise(resolve => { + gBrowser.loadTabs(urls, { + inBackground: true, + triggeringPrincipal: + Services.scriptSecurityManager.getSystemPrincipal(), + }); + + let waitingToLoad = new Set(urls); + + let listener = { + QueryInterface: ChromeUtils.generateQI([ + "nsIWebProgressListener", + "nsISupportsWeakReference", + ]), + onStateChange(aBrowser, aWebProgress, aRequest, aStateFlags, aStatus) { + let loadedState = + Ci.nsIWebProgressListener.STATE_STOP | + Ci.nsIWebProgressListener.STATE_IS_NETWORK; + if ( + (aStateFlags & loadedState) == loadedState && + !aWebProgress.isLoadingDocument && + aWebProgress.isTopLevel && + Components.isSuccessCode(aStatus) + ) { + dump(`Loaded: ${aBrowser.currentURI.spec}\n`); + waitingToLoad.delete(aBrowser.currentURI.spec); + + if (!waitingToLoad.size) { + gBrowser.removeTabsProgressListener(listener); + dump("Loads complete - starting tab switches\n"); + resolve(); + } + } + }, + }; + + gBrowser.addTabsProgressListener(listener); + }); + } + + /** + * For some <xul:tab> in a browser window, have that window switch + * to that tab. Returns a Promise that resolves ones the tab content + * has been presented to the user. + */ + async switchToTab(tab) { + let browser = tab.linkedBrowser; + let gBrowser = tab.ownerGlobal.gBrowser; + + let start = Cu.now(); + + // We need to wait for the TabSwitchDone event to make sure + // that the async tab switcher has shut itself down. + let switchDone = this.waitForTabSwitchDone(browser); + // Set up our promise that will wait for the content to be + // presented. + let finishPromise = this.waitForContentPresented(browser); + // Finally, do the tab switch. + gBrowser.selectedTab = tab; + + await switchDone; + let finish = await finishPromise; + + return finish - start; + } + + /** + * For some <xul:browser>, find the <xul:tabbrowser> associated with it, + * and wait until that tabbrowser has finished a tab switch. This function + * assumes a tab switch has started, or is about to start. + * + * @param browser (<xul:browser>) + * The browser whose tabbrowser we expect to be involved in a tab + * switch. + * @returns Promise + * Resolves once the TabSwitchDone event is fired. + */ + waitForTabSwitchDone(browser) { + return new Promise(resolve => { + let gBrowser = browser.ownerGlobal.gBrowser; + gBrowser.addEventListener( + "TabSwitchDone", + function onTabSwitchDone() { + resolve(); + }, + { once: true } + ); + }); + } + + /** + * For some <xul:browser>, returns a Promise that resolves once its + * content has been presented to the user. + * + * @param browser (<xul:browser>) + * The browser we expect to be presented. + * + * @returns Promise + * Resolves once the content has been presented. Resolves to + * the system time that the presentation occurred at, in + * milliseconds since midnight 01 January, 1970 UTC. + */ + waitForContentPresented(browser) { + return new Promise(resolve => { + function onLayersReady() { + let now = Cu.now(); + TalosParentProfiler.mark("Browser layers seen by tabswitch"); + resolve(now); + } + if (browser.hasLayers) { + onLayersReady(); + return; + } + browser.addEventListener("MozLayerTreeReady", onLayersReady, { + once: true, + }); + }); + } + + /** + * Do a garbage collect in the parent, and then a garbage + * collection in the content process that the actor is + * running in. + * + * @returns Promise + * Resolves once garbage collection has been completed in the + * parent, and the content process for the actor. + */ + forceGC(win) { + win.windowUtils.garbageCollect(); + return this.sendQuery("GarbageCollect"); + } + + /** + * Given some host window, open a new window, browser its initial tab to + * about:blank, then load up our set of testing URLs. Once they've all finished + * loading, switch through each tab, recording their tab switch times. Finally, + * report the results. + * + * @param window + * A host window. Primarily, we just use this for the OpenBrowserWindow + * function defined in that window. + * @returns Promise + */ + async test(window) { + if (!window.gMultiProcessBrowser) { + dump( + "** The tabswitch Talos test does not support running in non-e10s mode " + + "anymore! Bailing out!\n" + ); + return null; + } + + TalosParentProfiler = ChromeUtils.importESModule( + "resource://talos-powers/TalosParentProfiler.sys.mjs" + ).TalosParentProfiler; + + let testURLs = []; + + let win = window.OpenBrowserWindow(); + try { + let prefFile = Services.prefs.getCharPref("addon.test.tabswitch.urlfile"); + if (prefFile) { + testURLs = handleFile(win, prefFile); + } + } catch (ex) { + /* error condition handled below */ + } + if (!testURLs || !testURLs.length) { + dump( + "no tabs to test, 'addon.test.tabswitch.urlfile' pref isn't set to page set path\n" + ); + return null; + } + + await this.waitForDelayedStartup(win); + + let gBrowser = win.gBrowser; + + // We don't want to catch scrolling the tabstrip in our tests + gBrowser.tabContainer.style.opacity = "0"; + + let initialTab = gBrowser.selectedTab; + await this.loadTabs(gBrowser, testURLs); + + // We'll switch back to about:blank after each tab switch + // in an attempt to put the graphics layer into a "steady" + // state before switching to the next tab. + initialTab.linkedBrowser.loadURI(Services.io.newURI("about:blank"), { + triggeringPrincipal: Services.scriptSecurityManager.createNullPrincipal( + {} + ), + }); + + let tabs = gBrowser.getTabsToTheEndFrom(initialTab); + let times = []; + + for (let tab of tabs) { + // Let's do an initial run to warm up any paint related caches + // (like glyph caches for text). In the next loop we will start with + // a GC before each switch so we don't need here. + dump(`${tab.linkedBrowser.currentURI.spec}: warm up begin\n`); + await this.switchToTab(tab); + dump(`${tab.linkedBrowser.currentURI.spec}: warm up end\n`); + + await this.switchToTab(initialTab); + } + + for (let tab of tabs) { + // Moving a tab causes expensive style/layout computations on the tab bar + // that are delayed using requestAnimationFrame, so wait for an animation + // frame callback + one tick to ensure we aren't measuring the time it + // takes to move a tab. + gBrowser.moveTabTo(tab, 1); + await new Promise(resolve => win.requestAnimationFrame(resolve)); + await new Promise(resolve => Services.tm.dispatchToMainThread(resolve)); + + await this.forceGC(win); + TalosParentProfiler.subtestStart(); + let time = await this.switchToTab(tab); + TalosParentProfiler.subtestEnd( + "TabSwitch Test: " + tab.linkedBrowser.currentURI.spec + ); + dump(`${tab.linkedBrowser.currentURI.spec}: ${time}ms\n`); + times.push(time); + await this.switchToTab(initialTab); + } + + let output = + "<!DOCTYPE html>" + + '<html lang="en">' + + "<head><title>Tab Switch Results</title></head>" + + "<body><h1>Tab switch times</h1>" + + "<table>"; + let time = 0; + for (let i in times) { + time += times[i]; + output += + "<tr><td>" + testURLs[i] + "</td><td>" + times[i] + "ms</td></tr>"; + } + output += "</table></body></html>"; + dump("total tab switch time:" + time + "\n"); + + let resultsTab = win.gBrowser.addTab( + "data:text/html;charset=utf-8," + encodeURIComponent(output), + { + triggeringPrincipal: + Services.scriptSecurityManager.getSystemPrincipal(), + } + ); + + win.gBrowser.selectedTab = resultsTab; + + TalosParentProfiler.afterProfileGathered().then(() => { + win.close(); + }); + + return { + times, + urls: testURLs, + }; + } +} + +// This just has to match up with the make_talos_domain function in talos.py +function makeTalosDomain(host) { + return host + "-talos"; +} + +function handleFile(win, file) { + let localFile = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); + localFile.initWithPath(file); + let localURI = Services.io.newFileURI(localFile); + let req = new win.XMLHttpRequest(); + req.open("get", localURI.spec, false); + req.send(null); + + let testURLs = []; + let maxurls = Services.prefs.getIntPref("addon.test.tabswitch.maxurls"); + let lines = req.responseText.split('<a href="'); + testURLs = []; + if (maxurls && maxurls > 0) { + lines.splice(maxurls, lines.length); + } + lines.forEach(function (a) { + let url = a.split('"')[0]; + if (url != "") { + let domain = url.split("/")[0]; + if (domain != "") { + testURLs.push(`http://${makeTalosDomain(domain)}/fis/tp5n/${url}`); + } + } + }); + + return testURLs; +} diff --git a/testing/talos/talos/tests/tabswitch/api.js b/testing/talos/talos/tests/tabswitch/api.js new file mode 100644 index 0000000000..8ebccdf411 --- /dev/null +++ b/testing/talos/talos/tests/tabswitch/api.js @@ -0,0 +1,62 @@ +// -*- Mode: js; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*- + +/* globals ExtensionAPI, Services */ + +ChromeUtils.defineESModuleGetters(this, { + AboutNewTab: "resource:///modules/AboutNewTab.sys.mjs", +}); + +this.tabswitch = class extends ExtensionAPI { + getAPI(context) { + return { + tabswitch: { + setup() { + AboutNewTab.newTabURL = "about:blank"; + + let uri = Services.io.newURI( + "actors/", + null, + context.extension.rootURI + ); + let resProto = Services.io + .getProtocolHandler("resource") + .QueryInterface(Ci.nsIResProtocolHandler); + resProto.setSubstitution("talos-tabswitch", uri); + + const processScriptURL = context.extension.baseURI.resolve( + "content/tabswitch-content-process.js" + ); + Services.ppmm.loadProcessScript(processScriptURL, true); + + let tabSwitchTalosActors = { + parent: { + esModuleURI: + "resource://talos-tabswitch/TalosTabSwitchParent.sys.mjs", + }, + child: { + esModuleURI: + "resource://talos-tabswitch/TalosTabSwitchChild.sys.mjs", + events: { + DOMDocElementInserted: { capture: true }, + }, + }, + }; + ChromeUtils.registerWindowActor( + "TalosTabSwitch", + tabSwitchTalosActors + ); + + return () => { + Services.ppmm.sendAsyncMessage("Tabswitch:Teardown"); + ChromeUtils.unregisterWindowActor( + "TalosTabSwitch", + tabSwitchTalosActors + ); + AboutNewTab.resetNewTabURL(); + resProto.setSubstitution("talos-tabswitch", null); + }; + }, + }, + }; + } +}; diff --git a/testing/talos/talos/tests/tabswitch/background.js b/testing/talos/talos/tests/tabswitch/background.js new file mode 100644 index 0000000000..6727db475f --- /dev/null +++ b/testing/talos/talos/tests/tabswitch/background.js @@ -0,0 +1,11 @@ +/* globals browser */ + +/** + * The tabswitch test is a Pageloader test, meaning that the tabswitch.manifest file + * tells Talos to load a particular page. The loading of that page signals + * the start of the test. It's also where results need to go, as the + * Talos gunk augments the loaded page with a special tpRecordTime + * function that is used to report results. + */ + +browser.tabswitch.setup(); diff --git a/testing/talos/talos/tests/tabswitch/content/tabswitch-content-process.js b/testing/talos/talos/tests/tabswitch/content/tabswitch-content-process.js new file mode 100644 index 0000000000..53c4935953 --- /dev/null +++ b/testing/talos/talos/tests/tabswitch/content/tabswitch-content-process.js @@ -0,0 +1,69 @@ +/* eslint-env mozilla/process-script */ + +const { ComponentUtils } = ChromeUtils.importESModule( + "resource://gre/modules/ComponentUtils.sys.mjs" +); + +const WEBEXTENSION_ID = "tabswitch-talos@mozilla.org"; +const ABOUT_PAGE_NAME = "tabswitch"; +const Registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar); +const UUID = "0f459ab4-b4ba-4741-ac89-ee47dea07adb"; +const ABOUT_PATH_PATH = "content/test.html"; + +const { WebExtensionPolicy } = Cu.getGlobalForObject(Services); + +const TPSProcessScript = { + init() { + let extensionPolicy = WebExtensionPolicy.getByID(WEBEXTENSION_ID); + let aboutPageURI = extensionPolicy.getURL(ABOUT_PATH_PATH); + + class TabSwitchAboutModule { + constructor() { + this.QueryInterface = ChromeUtils.generateQI(["nsIAboutModule"]); + } + newChannel(aURI, aLoadInfo) { + let uri = Services.io.newURI(aboutPageURI); + let chan = Services.io.newChannelFromURIWithLoadInfo(uri, aLoadInfo); + chan.originalURI = aURI; + return chan; + } + getURIFlags(aURI) { + return ( + Ci.nsIAboutModule.ALLOW_SCRIPT | + Ci.nsIAboutModule.URI_MUST_LOAD_IN_CHILD | + Ci.nsIAboutModule.URI_SAFE_FOR_UNTRUSTED_CONTENT + ); + } + } + + let factory = ComponentUtils.generateSingletonFactory(TabSwitchAboutModule); + this._factory = factory; + + Registrar.registerFactory( + Components.ID(UUID), + "", + `@mozilla.org/network/protocol/about;1?what=${ABOUT_PAGE_NAME}`, + factory + ); + + this._hasSetup = true; + }, + + teardown() { + if (!this._hasSetup) { + return; + } + + Registrar.unregisterFactory(Components.ID(UUID), this._factory); + this._hasSetup = false; + this._factory = null; + }, + + receiveMessage(msg) { + if (msg.name == "Tabswitch:Teardown") { + this.teardown(); + } + }, +}; + +TPSProcessScript.init(); diff --git a/testing/talos/talos/tests/tabswitch/content/test.html b/testing/talos/talos/tests/tabswitch/content/test.html new file mode 100644 index 0000000000..3cdc7a673c --- /dev/null +++ b/testing/talos/talos/tests/tabswitch/content/test.html @@ -0,0 +1,17 @@ +<html> + <head> + <script> + /* global RPMSendQuery */ + function do_test(override) { + if (override || document.location.hash.indexOf("#auto") == 0) { + RPMSendQuery("tabswitch-do-test", {}).then(results => { + tpRecordTime(results.times.join(","), 0, results.urls.join(",")); + }); + } + } + </script> + </head> + <body onload="do_test(false)"> + Hello Talos! + </body> +</html> diff --git a/testing/talos/talos/tests/tabswitch/manifest.json b/testing/talos/talos/tests/tabswitch/manifest.json new file mode 100644 index 0000000000..d8f4451c4e --- /dev/null +++ b/testing/talos/talos/tests/tabswitch/manifest.json @@ -0,0 +1,24 @@ +{ + "browser_specific_settings": { + "gecko": { + "id": "tabswitch-talos@mozilla.org" + } + }, + "manifest_version": 2, + "name": "Tabswitch Talos Test", + "version": "0.1", + "permissions": [], + "background": { + "scripts": ["background.js"] + }, + "experiment_apis": { + "tabswitch": { + "schema": "schema.json", + "parent": { + "scopes": ["addon_parent"], + "script": "api.js", + "paths": [["tabswitch"]] + } + } + } +} diff --git a/testing/talos/talos/tests/tabswitch/schema.json b/testing/talos/talos/tests/tabswitch/schema.json new file mode 100644 index 0000000000..6a8e54177e --- /dev/null +++ b/testing/talos/talos/tests/tabswitch/schema.json @@ -0,0 +1,14 @@ +[ + { + "namespace": "tabswitch", + "description": "Special powers for the tabswitch Talos test", + "functions": [ + { + "name": "setup", + "type": "function", + "description": "Prepares the tabswitch test to be run by the Talos framework.", + "parameters": [] + } + ] + } +] diff --git a/testing/talos/talos/tests/tabswitch/tabswitch.manifest b/testing/talos/talos/tests/tabswitch/tabswitch.manifest new file mode 100644 index 0000000000..cabe207e5d --- /dev/null +++ b/testing/talos/talos/tests/tabswitch/tabswitch.manifest @@ -0,0 +1 @@ +% about:tabswitch#auto |