diff options
Diffstat (limited to 'testing/talos/talos/pageloader/chrome')
-rw-r--r-- | testing/talos/talos/pageloader/chrome/MozillaFileLogger.js | 95 | ||||
-rw-r--r-- | testing/talos/talos/pageloader/chrome/Profiler.js | 206 | ||||
-rw-r--r-- | testing/talos/talos/pageloader/chrome/a11y.js | 66 | ||||
-rw-r--r-- | testing/talos/talos/pageloader/chrome/lh_dummy.js | 11 | ||||
-rw-r--r-- | testing/talos/talos/pageloader/chrome/lh_fnbpaint.js | 45 | ||||
-rw-r--r-- | testing/talos/talos/pageloader/chrome/lh_hero.js | 49 | ||||
-rw-r--r-- | testing/talos/talos/pageloader/chrome/lh_moz.js | 28 | ||||
-rw-r--r-- | testing/talos/talos/pageloader/chrome/lh_pdfpaint.js | 26 | ||||
-rw-r--r-- | testing/talos/talos/pageloader/chrome/pageloader.js | 1111 | ||||
-rw-r--r-- | testing/talos/talos/pageloader/chrome/pageloader.xhtml | 56 | ||||
-rw-r--r-- | testing/talos/talos/pageloader/chrome/quit.js | 72 | ||||
-rw-r--r-- | testing/talos/talos/pageloader/chrome/report.js | 195 | ||||
-rw-r--r-- | testing/talos/talos/pageloader/chrome/talos-content.js | 24 | ||||
-rw-r--r-- | testing/talos/talos/pageloader/chrome/tscroll.js | 324 | ||||
-rw-r--r-- | testing/talos/talos/pageloader/chrome/utils.js | 34 |
15 files changed, 2342 insertions, 0 deletions
diff --git a/testing/talos/talos/pageloader/chrome/MozillaFileLogger.js b/testing/talos/talos/pageloader/chrome/MozillaFileLogger.js new file mode 100644 index 0000000000..65338ee19e --- /dev/null +++ b/testing/talos/talos/pageloader/chrome/MozillaFileLogger.js @@ -0,0 +1,95 @@ +/* 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/. */ + +/** + * MozillaFileLogger, a log listener that can write to a local file. + */ + +// double logging to account for normal mode and ipc mode (mobile_profile only) +// Ideally we would remove the dump() and just do ipc logging +function dumpLog(msg) { + dump(msg); + MozillaFileLogger.log(msg); +} + +const FOSTREAM_CID = "@mozilla.org/network/file-output-stream;1"; +const LF_CID = "@mozilla.org/file/local;1"; + +// File status flags. It is a bitwise OR of the following bit flags. +// Only one of the first three flags below may be used. +const PR_READ_ONLY = 0x01; // Open for reading only. +const PR_WRITE_ONLY = 0x02; // Open for writing only. +const PR_READ_WRITE = 0x04; // Open for reading and writing. + +// If the file does not exist, the file is created. +// If the file exists, this flag has no effect. +const PR_CREATE_FILE = 0x08; + +// The file pointer is set to the end of the file prior to each write. +const PR_APPEND = 0x10; + +// If the file exists, its length is truncated to 0. +const PR_TRUNCATE = 0x20; + +// If set, each write will wait for both the file data +// and file status to be physically updated. +const PR_SYNC = 0x40; + +// If the file does not exist, the file is created. If the file already +// exists, no action and NULL is returned. +const PR_EXCL = 0x80; + +/** Init the file logger with the absolute path to the file. + It will create and append if the file already exists **/ +var MozillaFileLogger = {}; + +MozillaFileLogger.init = function(path) { + MozillaFileLogger._file = Cc[LF_CID].createInstance(Ci.nsIFile); + MozillaFileLogger._file.initWithPath(path); + MozillaFileLogger._foStream = Cc[FOSTREAM_CID].createInstance( + Ci.nsIFileOutputStream + ); + MozillaFileLogger._foStream.init( + this._file, + PR_WRITE_ONLY | PR_CREATE_FILE | PR_APPEND, + 0o664, + 0 + ); +}; + +MozillaFileLogger.getLogCallback = function() { + return function(msg) { + var data = msg.num + " " + msg.level + " " + msg.info.join(" ") + "\n"; + if (MozillaFileLogger._foStream) { + MozillaFileLogger._foStream.write(data, data.length); + } + + if (data.includes("SimpleTest FINISH")) { + MozillaFileLogger.close(); + } + }; +}; + +// This is only used from chrome space by the reftest harness +MozillaFileLogger.log = function(msg) { + try { + if (MozillaFileLogger._foStream) { + MozillaFileLogger._foStream.write(msg, msg.length); + } + } catch (ex) {} +}; + +MozillaFileLogger.close = function() { + if (MozillaFileLogger._foStream) { + MozillaFileLogger._foStream.close(); + } + + MozillaFileLogger._foStream = null; + MozillaFileLogger._file = null; +}; + +try { + var filename = Services.prefs.getCharPref("talos.logfile"); + MozillaFileLogger.init(filename); +} catch (ex) {} // pref does not exist, return empty string diff --git a/testing/talos/talos/pageloader/chrome/Profiler.js b/testing/talos/talos/pageloader/chrome/Profiler.js new file mode 100644 index 0000000000..a652c10c04 --- /dev/null +++ b/testing/talos/talos/pageloader/chrome/Profiler.js @@ -0,0 +1,206 @@ +/* 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/. */ + +/* eslint-env mozilla/frame-script */ + +// - NOTE: This file is duplicated verbatim at: +// - talos/pageloader/chrome/Profiler.js +// - talos/tests/tart/addon/content/Profiler.js +// - talos/startup_test/tresize/addon/content/Profiler.js +// +// - Please keep these copies in sync. +// - Please make sure your changes apply cleanly to all use cases. + +// Finer grained profiler control +// +// Use this object to pause and resume the profiler so that it only profiles the +// relevant parts of our tests. +var Profiler; + +(function() { + var _profiler; + + // If this script is loaded in a framescript context, there won't be a + // document object, so just use a fallback value in that case. + var test_name = this.document ? this.document.location.pathname : "unknown"; + + // Whether Profiler has been initialized. Until that happens, most calls + // will be ignored. + var enabled = false; + + // The subtest name that beginTest() was called with. + var currentTest = ""; + + // Profiling settings. + var profiler_interval, + profiler_entries, + profiler_threadsArray, + profiler_featuresArray, + profiler_dir; + + try { + // eslint-disable-next-line mozilla/use-services + _profiler = Cc["@mozilla.org/tools/profiler;1"].getService(Ci.nsIProfiler); + } catch (ex) { + (typeof dumpLog == "undefined" ? dump : dumpLog)(ex + "\n"); + } + + // Parses an url query string into a JS object. + function searchToObject(locationSearch) { + var pairs = locationSearch.substring(1).split("&"); + var result = {}; + + for (var i in pairs) { + if (pairs[i] !== "") { + var pair = pairs[i].split("="); + result[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1] || ""); + } + } + + return result; + } + + Profiler = { + /** + * Initialize the profiler using profiler settings supplied in a JS object. + * The following properties on the object are respected: + * - gecko_profile_interval + * - gecko_profile_entries + * - gecko_profile_features + * - gecko_profile_threads + * - gecko_profile_dir + */ + initFromObject: function Profiler__initFromObject(obj) { + if ( + obj && + "gecko_profile_dir" in obj && + typeof obj.gecko_profile_dir == "string" && + "gecko_profile_interval" in obj && + Number.isFinite(obj.gecko_profile_interval * 1) && + "gecko_profile_entries" in obj && + Number.isFinite(obj.gecko_profile_entries * 1) && + "gecko_profile_features" in obj && + typeof obj.gecko_profile_features == "string" && + "gecko_profile_threads" in obj && + typeof obj.gecko_profile_threads == "string" + ) { + profiler_interval = obj.gecko_profile_interval; + profiler_entries = obj.gecko_profile_entries; + profiler_featuresArray = obj.gecko_profile_features.split(","); + profiler_threadsArray = obj.gecko_profile_threads.split(","); + profiler_dir = obj.gecko_profile_dir; + enabled = true; + } + }, + initFromURLQueryParams: function Profiler__initFromURLQueryParams( + locationSearch + ) { + this.initFromObject(searchToObject(locationSearch)); + }, + beginTest: function Profiler__beginTest(testName) { + currentTest = testName; + if (_profiler && enabled) { + _profiler.StartProfiler( + profiler_entries, + profiler_interval, + profiler_featuresArray, + profiler_threadsArray + ); + _profiler.PauseSampling(); + } + }, + finishTest: function Profiler__finishTest() { + if (_profiler && enabled) { + _profiler.Pause(); + _profiler.dumpProfileToFile( + profiler_dir + "/" + currentTest + ".profile" + ); + _profiler.StopProfiler(); + } + }, + finishTestAsync: function Profiler__finishTest() { + if (!(_profiler && enabled)) { + return undefined; + } + return new Promise((resolve, reject) => { + Services.profiler.getProfileDataAsync().then( + profile => { + let profileFile = profiler_dir + "/" + currentTest + ".profile"; + + const { NetUtil } = ChromeUtils.import( + "resource://gre/modules/NetUtil.jsm" + ); + const { FileUtils } = ChromeUtils.importESModule( + "resource://gre/modules/FileUtils.sys.mjs" + ); + + var file = Cc["@mozilla.org/file/local;1"].createInstance( + Ci.nsIFile + ); + file.initWithPath(profileFile); + + var ostream = FileUtils.openSafeFileOutputStream(file); + + var converter = Cc[ + "@mozilla.org/intl/scriptableunicodeconverter" + ].createInstance(Ci.nsIScriptableUnicodeConverter); + converter.charset = "UTF-8"; + var istream = converter.convertToInputStream( + JSON.stringify(profile) + ); + + // The last argument (the callback) is optional. + NetUtil.asyncCopy(istream, ostream, function(status) { + if (!Components.isSuccessCode(status)) { + reject(); + return; + } + + resolve(); + }); + }, + error => { + console.error("Failed to gather profile: " + error); + reject(); + } + ); + }); + }, + finishStartupProfiling: function Profiler__finishStartupProfiling() { + if (_profiler && enabled) { + _profiler.Pause(); + _profiler.dumpProfileToFile(profiler_dir + "/startup.profile"); + _profiler.StopProfiler(); + } + }, + resume: function Profiler__resume(name, explicit) { + if (_profiler) { + if (_profiler.ResumeSampling) { + _profiler.ResumeSampling(); + } + ChromeUtils.addProfilerMarker( + explicit ? name : 'Start of test "' + (name || test_name) + '"', + { category: "Test" } + ); + } + }, + pause: function Profiler__pause(name, explicit) { + if (_profiler) { + ChromeUtils.addProfilerMarker( + explicit ? name : 'End of test "' + (name || test_name) + '"', + { category: "Test" } + ); + _profiler.PauseSampling(); + } + }, + mark: function Profiler__mark(marker, explicit) { + if (_profiler) { + ChromeUtils.addProfilerMarker( + explicit ? marker : 'Profiler: "' + (marker || test_name) + '"', + { category: "Test" } + ); + } + }, + }; +})(); diff --git a/testing/talos/talos/pageloader/chrome/a11y.js b/testing/talos/talos/pageloader/chrome/a11y.js new file mode 100644 index 0000000000..302ca0ed1f --- /dev/null +++ b/testing/talos/talos/pageloader/chrome/a11y.js @@ -0,0 +1,66 @@ +/* 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/. */ + +/* eslint-env mozilla/frame-script */ + +(function() { + let gAccService = 0; + + function initAccessibility() { + if (!gAccService) { + var service = Cc["@mozilla.org/accessibilityService;1"]; + if (service) { + // fails if build lacks accessibility module + gAccService = Cc["@mozilla.org/accessibilityService;1"].getService( + Ci.nsIAccessibilityService + ); + } + } + return !!gAccService; + } + + function getAccessible(aNode) { + try { + return gAccService.getAccessibleFor(aNode); + } catch (e) {} + + return null; + } + + function ensureAccessibleTreeForNode(aNode) { + var acc = getAccessible(aNode); + + ensureAccessibleTreeForAccessible(acc); + } + + function ensureAccessibleTreeForAccessible(aAccessible) { + var child = aAccessible.firstChild; + while (child) { + ensureAccessibleTreeForAccessible(child); + try { + child = child.nextSibling; + } catch (e) { + child = null; + } + } + } + + // Walk accessible tree of the given identifier to ensure tree creation + function ensureAccessibleTreeForId(aID) { + var node = content.document.getElementById(aID); + if (!node) { + return; + } + ensureAccessibleTreeForNode(node); + } + + addEventListener("DOMContentLoaded", e => { + Cu.exportFunction(initAccessibility, content, { + defineAs: "initAccessibility", + }); + Cu.exportFunction(ensureAccessibleTreeForId, content, { + defineAs: "ensureAccessibleTreeForId", + }); + }); +})(); diff --git a/testing/talos/talos/pageloader/chrome/lh_dummy.js b/testing/talos/talos/pageloader/chrome/lh_dummy.js new file mode 100644 index 0000000000..134efd4ef1 --- /dev/null +++ b/testing/talos/talos/pageloader/chrome/lh_dummy.js @@ -0,0 +1,11 @@ +/* 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/. */ + +/* eslint-env mozilla/frame-script */ + +function _dummy() { + sendAsyncMessage("PageLoader:LoadEvent", {}); +} + +addEventListener("load", contentLoadHandlerCallback(_dummy), true); // eslint-disable-line no-undef diff --git a/testing/talos/talos/pageloader/chrome/lh_fnbpaint.js b/testing/talos/talos/pageloader/chrome/lh_fnbpaint.js new file mode 100644 index 0000000000..5ef0066ad2 --- /dev/null +++ b/testing/talos/talos/pageloader/chrome/lh_fnbpaint.js @@ -0,0 +1,45 @@ +/* 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/. */ + +/* eslint-env mozilla/frame-script */ + +var gErr = + "Abort: firstNonBlankPaint value is not available after loading the page"; +var gRetryCounter = 0; + +function _contentFNBPaintHandler() { + var x = content.window.performance.timing.timeToNonBlankPaint; + if (typeof x == "undefined") { + sendAsyncMessage("PageLoader:Error", { msg: gErr }); + } + if (x > 0) { + dump("received fnbpaint value\n"); + sendAsyncMessage("PageLoader:LoadEvent", { time: x, name: "fnbpaint" }); + gRetryCounter = 0; + } else { + gRetryCounter += 1; + if (gRetryCounter <= 10) { + dump( + "\nfnbpaint is not yet available (0), retry number " + + gRetryCounter + + "...\n" + ); + content.setTimeout(_contentFNBPaintHandler, 100); + } else { + dump( + "\nunable to get a value for fnbpaint after " + + gRetryCounter + + " retries\n" + ); + sendAsyncMessage("PageLoader:Error", { msg: gErr }); + } + } +} + +addEventListener( + "load", + // eslint-disable-next-line no-undef + contentLoadHandlerCallback(_contentFNBPaintHandler), + true +); diff --git a/testing/talos/talos/pageloader/chrome/lh_hero.js b/testing/talos/talos/pageloader/chrome/lh_hero.js new file mode 100644 index 0000000000..caef53f374 --- /dev/null +++ b/testing/talos/talos/pageloader/chrome/lh_hero.js @@ -0,0 +1,49 @@ +/* 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/. */ + +/* eslint-env mozilla/frame-script */ + +function _contentHeroHandler(isload) { + var obs = null; + var el = content.window.document.querySelector("[elementtiming]"); + if (el) { + function callback(entries, observer) { + entries.forEach(entry => { + sendAsyncMessage("PageLoader:LoadEvent", { + time: content.window.performance.now(), + name: "tphero", + }); + obs.disconnect(); + }); + } + // we want the element 100% visible on the viewport + var options = { root: null, rootMargin: "0px", threshold: [1] }; + try { + obs = new content.window.IntersectionObserver(callback, options); + obs.observe(el); + } catch (err) { + sendAsyncMessage("PageLoader:Error", { msg: err.message }); + } + } else if (isload) { + // If the hero element is added from a settimeout handler, it might not run before 'load' + content.setTimeout(function() { + _contentHeroHandler(false); + }, 5000); + } else { + var err = "Could not find a tag with an elmenttiming attr on the page"; + sendAsyncMessage("PageLoader:Error", { msg: err }); + } + return obs; +} + +function _contentHeroLoadHandler() { + _contentHeroHandler(true); +} + +addEventListener( + "load", + // eslint-disable-next-line no-undef + contentLoadHandlerCallback(_contentHeroLoadHandler), + true +); diff --git a/testing/talos/talos/pageloader/chrome/lh_moz.js b/testing/talos/talos/pageloader/chrome/lh_moz.js new file mode 100644 index 0000000000..46d4a74a7c --- /dev/null +++ b/testing/talos/talos/pageloader/chrome/lh_moz.js @@ -0,0 +1,28 @@ +/* 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/. */ + +/* eslint-env mozilla/frame-script */ + +function _contentPaintHandler() { + var utils = content.windowUtils; + if (utils.isMozAfterPaintPending) { + addEventListener( + "MozAfterPaint", + function afterpaint(e) { + removeEventListener("MozAfterPaint", afterpaint, true); + sendAsyncMessage("PageLoader:LoadEvent", {}); + }, + true + ); + } else { + sendAsyncMessage("PageLoader:LoadEvent", {}); + } +} + +addEventListener( + "load", + // eslint-disable-next-line no-undef + contentLoadHandlerCallback(_contentPaintHandler), + true +); diff --git a/testing/talos/talos/pageloader/chrome/lh_pdfpaint.js b/testing/talos/talos/pageloader/chrome/lh_pdfpaint.js new file mode 100644 index 0000000000..0f1eff6da9 --- /dev/null +++ b/testing/talos/talos/pageloader/chrome/lh_pdfpaint.js @@ -0,0 +1,26 @@ +/* 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/. */ + +/* eslint-env mozilla/frame-script */ + +function _pdfPaintHandler() { + content.window.addEventListener( + "pagerendered", + e => { + if (e.detail.pageNumber !== 1) { + sendAsyncMessage("PageLoader:Error", { + msg: `Error: Expected page 1 got ${e.detail.pageNumber}`, + }); + return; + } + sendAsyncMessage("PageLoader:LoadEvent", { + time: e.detail.timestamp, + name: "pdfpaint", + }); + }, + { once: true } + ); +} + +addEventListener("DOMContentLoaded", _pdfPaintHandler, true); diff --git a/testing/talos/talos/pageloader/chrome/pageloader.js b/testing/talos/talos/pageloader/chrome/pageloader.js new file mode 100644 index 0000000000..e424a5c2ec --- /dev/null +++ b/testing/talos/talos/pageloader/chrome/pageloader.js @@ -0,0 +1,1111 @@ +/* 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-globals-from report.js */ + +var { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); +var { E10SUtils } = ChromeUtils.importESModule( + "resource://gre/modules/E10SUtils.sys.mjs" +); +ChromeUtils.defineESModuleGetters(this, { + TalosParentProfiler: "resource://talos-powers/TalosParentProfiler.sys.mjs", +}); + +var NUM_CYCLES = 5; +var numPageCycles = 1; + +var numRetries = 0; +var maxRetries = 3; + +var pageFilterRegexp = null; +var winWidth = 1024; +var winHeight = 768; + +var pages; +var pageIndex; +var start_time; +var cycle; +var pageCycle; +var report; +var timeout = -1; +var delay = 250; +var running = false; +var forceCC = true; + +var useMozAfterPaint = false; +var useFNBPaint = false; +var isFNBPaintPending = false; +var usePDFPaint = false; +var isPDFPaintPending = false; +var useHero = false; +var gPaintWindow = window; +var gPaintListener = false; +var loadNoCache = false; +var scrollTest = false; +var profilingInfo = false; +var baseVsRef = false; +var useBrowserChrome = false; +var useA11y = false; + +var isIdleCallbackPending = false; + +// when TEST_DOES_OWN_TIMING, we need to store the time from the page as MozAfterPaint can be slower than pageload +var gTime = -1; +var gStartTime = -1; +var gReference = -1; + +var content; + +// These are binary flags. Use 1/2/4/8/... +var TEST_DOES_OWN_TIMING = 1; +var EXECUTE_SCROLL_TEST = 2; + +var browserWindow = null; + +var recordedName = null; +var pageUrls; + +/** + * SingleTimeout class. Allow to register one and only one callback using + * setTimeout at a time. + */ +var SingleTimeout = function() { + this.timeoutEvent = undefined; +}; + +/** + * Register a callback with the given timeout. + * + * If timeout is < 0, this is a no-op. + * + * If a callback was previously registered and has not been called yet, it is + * first cleared with clear(). + */ +SingleTimeout.prototype.register = function(callback, timeout) { + if (timeout >= 0) { + if (this.timeoutEvent !== undefined) { + this.clear(); + } + var that = this; + this.timeoutEvent = setTimeout(function() { + that.timeoutEvent = undefined; + callback(); + }, timeout); + } +}; + +/** + * Clear a registered callback. + */ +SingleTimeout.prototype.clear = function() { + if (this.timeoutEvent !== undefined) { + clearTimeout(this.timeoutEvent); + this.timeoutEvent = undefined; + } +}; + +var failTimeout = new SingleTimeout(); + +async function plInit() { + if (running) { + return; + } + running = true; + + cycle = 0; + pageCycle = 1; + + try { + /* + * Desktop firefox: + * non-chrome talos runs - tp-cmdline will create and load pageloader + * into the main window of the app which displays and tests content. + * chrome talos runs - tp-cmdline does the same however pageloader + * creates a new chromed browser window below for content. + */ + + var manifestURI = Services.prefs.getCharPref("talos.tpmanifest", null); + if (manifestURI.length == null) { + dumpLine("tp abort: talos.tpmanifest browser pref is not set"); + plStop(true); + } + + NUM_CYCLES = Services.prefs.getIntPref("talos.tpcycles", 1); + numPageCycles = Services.prefs.getIntPref("talos.tppagecycles", 1); + timeout = Services.prefs.getIntPref("talos.tptimeout", -1); + useMozAfterPaint = Services.prefs.getBoolPref( + "talos.tpmozafterpaint", + false + ); + useHero = Services.prefs.getBoolPref("talos.tphero", false); + useFNBPaint = Services.prefs.getBoolPref("talos.fnbpaint", false); + usePDFPaint = Services.prefs.getBoolPref("talos.pdfpaint", false); + loadNoCache = Services.prefs.getBoolPref("talos.tploadnocache", false); + scrollTest = Services.prefs.getBoolPref("talos.tpscrolltest", false); + useBrowserChrome = Services.prefs.getBoolPref("talos.tpchrome", false); + useA11y = Services.prefs.getBoolPref("talos.a11y", false); + + // for pageloader tests the profiling info is found in an env variable + // because it is not available early enough to set it as a browser pref + if (Services.env.exists("TPPROFILINGINFO")) { + profilingInfo = Services.env.get("TPPROFILINGINFO"); + if (profilingInfo !== null) { + TalosParentProfiler.initFromObject(JSON.parse(profilingInfo)); + } + } + + if (forceCC && !window.windowUtils.garbageCollect) { + forceCC = false; + } + + var fileURI = Services.io.newURI(manifestURI); + pages = plLoadURLsFromURI(fileURI); + + if (!pages) { + dumpLine("tp: could not load URLs, quitting"); + plStop(true); + } + + if (!pages.length) { + dumpLine("tp: no pages to test, quitting"); + plStop(true); + } + + pageUrls = pages.map(function(p) { + return p.url.spec.toString(); + }); + report = new Report(); + + pageIndex = 0; + if (profilingInfo) { + TalosParentProfiler.beginTest(getCurrentPageShortName()); + } + + // Create a new chromed browser window for content + var blank = Cc["@mozilla.org/supports-string;1"].createInstance( + Ci.nsISupportsString + ); + blank.data = "about:blank"; + + let toolbars = "all"; + if (!useBrowserChrome) { + toolbars = "titlebar,resizable"; + } + + browserWindow = Services.ww.openWindow( + null, + AppConstants.BROWSER_CHROME_URL, + "_blank", + `chrome,${toolbars},dialog=no,width=${winWidth},height=${winHeight}`, + blank + ); + + gPaintWindow = browserWindow; + // get our window out of the way + window.resizeTo(10, 10); + + await new Promise(resolve => { + browserWindow.addEventListener("load", resolve, { + capture: true, + once: true, + }); + }); + + // do this half a second after load, because we need to be + // able to resize the window and not have it get clobbered + // by the persisted values + await new Promise(resolve => setTimeout(resolve, 500)); + + browserWindow.resizeTo(winWidth, winHeight); + browserWindow.moveTo(0, 0); + browserWindow.focus(); + content = browserWindow.gBrowser; + + // Since bug 1261842, the initial browser is remote unless it attempts + // to browse to a URI that should be non-remote (landed at bug 1047603). + // + // However, when it loads a URI that requires a different remote type, + // we lose the load listener and the injected tpRecordTime.remote, + // + // This listener will listen for when one of these process switches has + // happened, and re-install these listeners and injected methods into + // the new browser tab. + // + // It also probably means that per test (or, in fact, per pageloader browser + // instance which adds the load listener and injects tpRecordTime), all the + // pages should be able to load in the same mode as the initial page - due + // to this reinitialization on the switch. + let tab = content.selectedTab; + tab.addEventListener("TabRemotenessChange", function(evt) { + loadFrameScripts(tab.linkedBrowser); + }); + loadFrameScripts(tab.linkedBrowser); + + // Ensure that any webextensions that need to do setup have a chance + // to do so. e.g. the 'tabswitch' talos test registers a about:tabswitch + // handler during initialization, and if we don't wait for that, then + // attempting to load that URL will result in an error and hang the + // test. + for (let extension of WebExtensionPolicy.getActiveExtensions()) { + await extension.readyPromise; + } + plLoadPage(); + } catch (e) { + dumpLine("pageloader exception: " + e); + plStop(true); + } +} + +function plPageFlags() { + return pages[pageIndex].flags; +} + +var ContentListener = { + receiveMessage(message) { + switch (message.name) { + case "PageLoader:LoadEvent": + return plLoadHandlerMessage(message); + case "PageLoader:Error": + return plErrorMessage(message); + case "PageLoader:RecordTime": + return plRecordTimeMessage(message); + case "PageLoader:IdleCallbackSet": + return plIdleCallbackSet(); + case "PageLoader:IdleCallbackReceived": + return plIdleCallbackReceived(); + } + return undefined; + }, +}; + +// load the current page, start timing +var removeLastAddedMsgListener = null; +function plLoadPage() { + if (profilingInfo) { + TalosParentProfiler.beginTest( + getCurrentPageShortName() + "_pagecycle_" + pageCycle + ); + } + + var pageName = pages[pageIndex].url.spec; + + if (removeLastAddedMsgListener) { + removeLastAddedMsgListener(); + removeLastAddedMsgListener = null; + } + + let tab = content.selectedTab; + tab.addEventListener("TabRemotenessChange", evt => { + addMsgListeners(tab.linkedBrowser); + }); + addMsgListeners(tab.linkedBrowser); + + failTimeout.register(loadFail, timeout); + // record which page we are about to open + TalosParentProfiler.mark("Opening " + pages[pageIndex].url.pathQueryRef); + + if (useFNBPaint) { + isFNBPaintPending = true; + } + + if (usePDFPaint) { + isPDFPaintPending = true; + } + + startAndLoadURI(pageName); +} + +function addMsgListeners(browser) { + let mm = browser.messageManager; + // messages to watch for page load + mm.addMessageListener("PageLoader:LoadEvent", ContentListener); + mm.addMessageListener("PageLoader:RecordTime", ContentListener); + mm.addMessageListener("PageLoader:IdleCallbackSet", ContentListener); + mm.addMessageListener("PageLoader:IdleCallbackReceived", ContentListener); + mm.addMessageListener("PageLoader:Error", ContentListener); + + removeLastAddedMsgListener = function() { + mm.removeMessageListener("PageLoader:LoadEvent", ContentListener); + mm.removeMessageListener("PageLoader:RecordTime", ContentListener); + mm.removeMessageListener("PageLoader:IdleCallbackSet", ContentListener); + mm.removeMessageListener( + "PageLoader:IdleCallbackReceived", + ContentListener + ); + mm.removeMessageListener("PageLoader:Error", ContentListener); + }; +} + +function loadFrameScripts(browser) { + let mm = browser.messageManager; + + // Load our frame scripts. + mm.loadFrameScript("chrome://pageloader/content/utils.js", false, true); + + // pick the right load handler + if (useFNBPaint) { + mm.loadFrameScript( + "chrome://pageloader/content/lh_fnbpaint.js", + false, + true + ); + } else if (useMozAfterPaint) { + mm.loadFrameScript("chrome://pageloader/content/lh_moz.js", false, true); + } else if (useHero) { + mm.loadFrameScript("chrome://pageloader/content/lh_hero.js", false, true); + } else if (usePDFPaint) { + mm.loadFrameScript( + "chrome://pageloader/content/lh_pdfpaint.js", + false, + true + ); + } else { + mm.loadFrameScript("chrome://pageloader/content/lh_dummy.js", false, true); + } + mm.loadFrameScript("chrome://pageloader/content/talos-content.js", false); + mm.loadFrameScript( + "resource://talos-powers/TalosContentProfiler.js", + false, + true + ); + mm.loadFrameScript("chrome://pageloader/content/tscroll.js", false, true); + mm.loadFrameScript("chrome://pageloader/content/Profiler.js", false, true); + if (useA11y) { + mm.loadFrameScript("chrome://pageloader/content/a11y.js", false, true); + } +} + +function startAndLoadURI(pageName) { + if (!(plPageFlags() & TEST_DOES_OWN_TIMING)) { + // Resume the profiler because we're really measuring page load time. + // If the test is doing its own timing, it'll also need to do its own + // profiler pausing / resuming. + TalosParentProfiler.resume("Starting to load URI " + pageName); + } + + start_time = window.performance.now(); + if (loadNoCache) { + content.loadURI(pageName, { + triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), + flags: Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE, + }); + } else { + content.loadURI(pageName, { + triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), + }); + } +} + +function getTestName() { + // returns tp5n + var pageName = pages[pageIndex].url.spec; + let parts = pageName.split("/"); + if (parts.length > 4) { + return parts[4]; + } + return "pageloader"; +} + +function getCurrentPageShortName() { + // this is also used by gecko profiling for the profile + // file name; so ensure it is valid on Windows/Linux/OSX + var pageName = pages[pageIndex].url.spec; + let parts = pageName.split("/"); + if (parts.length > 5) { + // Tear off the first parts and concatenate the rest into a name. + let remainingParts = parts.slice(5); + let remainingAsString = remainingParts.join("_"); + if (remainingAsString.includes("?")) { + // page name is something like 'tpaint.html?auto=1' + remainingAsString = remainingAsString.split("?")[0]; + } + return remainingAsString; + } + return "page_" + pageIndex; +} + +function loadFail() { + var pageName = pages[pageIndex].url.spec; + numRetries++; + + if (numRetries >= maxRetries) { + dumpLine("__FAILTimeout in " + getTestName() + "__FAIL"); + dumpLine( + "__FAILTimeout (" + + numRetries + + "/" + + maxRetries + + ") exceeded on " + + pageName + + "__FAIL" + ); + TalosParentProfiler.finishTest().then(() => { + plStop(true); + }); + } else { + dumpLine( + "__WARNTimeout (" + + numRetries + + "/" + + maxRetries + + ") exceeded on " + + pageName + + "__WARN" + ); + // TODO: make this a cleaner cleanup + pageCycle--; + content.removeEventListener("load", plLoadHandler, true); + content.removeEventListener("load", plLoadHandlerCapturing, true); + content.removeEventListener("MozAfterPaint", plPaintedCapturing, true); + content.removeEventListener("MozAfterPaint", plPainted, true); + gPaintWindow.removeEventListener("MozAfterPaint", plPaintedCapturing, true); + gPaintWindow.removeEventListener("MozAfterPaint", plPainted, true); + removeLastAddedMsgListener = null; + gPaintListener = false; + + // TODO: consider adding a tab and removing the old tab?!? + setTimeout(plLoadPage, delay); + } +} + +var plNextPage = async function() { + var doNextPage = false; + + // ensure we've receive idle-callback before proceeding + if (isIdleCallbackPending) { + dumpLine("Waiting for idle-callback"); + await waitForIdleCallback(); + } + + if (useFNBPaint) { + // don't move to next page until we've received fnbpaint + if (isFNBPaintPending) { + dumpLine("Waiting for fnbpaint"); + await waitForFNBPaint(); + } + } + + if (usePDFPaint) { + // don't move to next page until we've received pdfpaint + if (isPDFPaintPending) { + dumpLine("Waiting for pdfpaint"); + await waitForPDFPaint(); + } + } + + if (profilingInfo) { + await TalosParentProfiler.finishTest(); + } + + if (pageCycle < numPageCycles) { + pageCycle++; + doNextPage = true; + } else if (pageIndex < pages.length - 1) { + pageIndex++; + recordedName = null; + pageCycle = 1; + doNextPage = true; + } + + if (doNextPage) { + if (forceCC) { + var tccstart = window.performance.now(); + window.windowUtils.garbageCollect(); + var tccend = window.performance.now(); + report.recordCCTime(tccend - tccstart); + + // Now asynchronously trigger GC / CC in the content process + await forceContentGC(); + } + + setTimeout(plLoadPage, delay); + } else { + plStop(false); + } +}; + +function waitForIdleCallback() { + return new Promise(resolve => { + function checkForIdleCallback() { + if (!isIdleCallbackPending) { + resolve(); + } else { + setTimeout(checkForIdleCallback, 5); + } + } + checkForIdleCallback(); + }); +} + +function plIdleCallbackSet() { + if (!scrollTest) { + isIdleCallbackPending = true; + } +} + +function plIdleCallbackReceived() { + isIdleCallbackPending = false; +} + +function waitForFNBPaint() { + return new Promise(resolve => { + function checkForFNBPaint() { + if (!isFNBPaintPending) { + resolve(); + } else { + setTimeout(checkForFNBPaint, 200); + } + } + checkForFNBPaint(); + }); +} + +function waitForPDFPaint() { + return new Promise(resolve => { + function checkForPDFPaint() { + if (!isPDFPaintPending) { + resolve(); + } else { + setTimeout(checkForPDFPaint, 200); + } + } + checkForPDFPaint(); + }); +} + +function forceContentGC() { + return new Promise(resolve => { + let mm = browserWindow.gBrowser.selectedBrowser.messageManager; + mm.addMessageListener("Talos:ForceGC:OK", function onTalosContentForceGC( + msg + ) { + mm.removeMessageListener("Talos:ForceGC:OK", onTalosContentForceGC); + resolve(); + }); + mm.sendAsyncMessage("Talos:ForceGC"); + }); +} + +function plRecordTime(time) { + var pageName = pages[pageIndex].url.spec; + var i = pageIndex; + if (i < pages.length - 1) { + i++; + } else { + i = 0; + } + var nextName = pages[i].url.spec; + if (!recordedName) { + // when doing base vs ref type of test, add pre 'base' or 'ref' to reported page name; + // this is necessary so that if multiple subtests use same reference page, results for + // each ref page run will be kept separate for each base vs ref run, and not grouped + // into just one set of results values for everytime that reference page was loaded + if (baseVsRef) { + recordedName = pages[pageIndex].pre + pageUrls[pageIndex]; + } else { + recordedName = pageUrls[pageIndex]; + } + } + if (typeof time == "string") { + var times = time.split(","); + var names = recordedName.split(","); + for (var t = 0; t < times.length; t++) { + if (names.length == 1) { + report.recordTime(names, times[t]); + } else { + report.recordTime(names[t], times[t]); + } + } + } else { + report.recordTime(recordedName, time); + } + dumpLine( + "Cycle " + + (cycle + 1) + + "(" + + pageCycle + + "): loaded " + + pageName + + " (next: " + + nextName + + ")" + ); +} + +function plLoadHandlerCapturing(evt) { + // make sure we pick up the right load event + if (evt.type != "load" || evt.originalTarget.defaultView.frameElement) { + return; + } + + // set the tpRecordTime function (called from test pages we load) to store a global time. + content.contentWindow.wrappedJSObject.tpRecordTime = function( + time, + startTime, + testName + ) { + gTime = time; + gStartTime = startTime; + recordedName = testName; + setTimeout(plWaitForPaintingCapturing, 0); + }; + + content.contentWindow.wrappedJSObject.plGarbageCollect = function() { + window.windowUtils.garbageCollect(); + }; + + content.removeEventListener("load", plLoadHandlerCapturing, true); + + setTimeout(plWaitForPaintingCapturing, 0); +} + +// Shim function this is really defined in tscroll.js +function sendScroll() { + const SCROLL_TEST_STEP_PX = 10; + const SCROLL_TEST_NUM_STEPS = 100; + // The page doesn't really use tpRecordTime. Instead, we trigger the scroll test, + // and the scroll test will call tpRecordTime which will take us to the next page + let details = { + target: "content", + stepSize: SCROLL_TEST_STEP_PX, + opt_numSteps: SCROLL_TEST_NUM_STEPS, + }; + let mm = content.selectedBrowser.messageManager; + mm.sendAsyncMessage("PageLoader:ScrollTest", { details }); +} + +function plWaitForPaintingCapturing() { + if (gPaintListener) { + return; + } + + var utils = gPaintWindow.windowUtils; + + if (utils.isMozAfterPaintPending && useMozAfterPaint) { + if (!gPaintListener) { + gPaintWindow.addEventListener("MozAfterPaint", plPaintedCapturing, true); + } + gPaintListener = true; + return; + } + + _loadHandlerCapturing(); +} + +function plPaintedCapturing() { + gPaintWindow.removeEventListener("MozAfterPaint", plPaintedCapturing, true); + gPaintListener = false; + _loadHandlerCapturing(); +} + +function _loadHandlerCapturing() { + failTimeout.clear(); + + if (!(plPageFlags() & TEST_DOES_OWN_TIMING)) { + dumpLine( + "tp: Capturing onload handler used with page that doesn't do its own timing?" + ); + plStop(true); + } + + if (useMozAfterPaint) { + if (gStartTime != null && gStartTime >= 0) { + gTime = + window.performance.timing.navigationStart + + window.performance.now() - + gStartTime; + gStartTime = -1; + } + } + + if (gTime !== -1) { + plRecordTime(gTime); + TalosParentProfiler.mark("Talos - capturing load handler fired"); + TalosParentProfiler.pause(); + gTime = -1; + recordedName = null; + setTimeout(plNextPage, delay); + } +} + +// the onload handler +function plLoadHandler(evt) { + // make sure we pick up the right load event + if (evt.type != "load" || evt.originalTarget.defaultView.frameElement) { + return; + } + + content.removeEventListener("load", plLoadHandler, true); + setTimeout(waitForPainted, 0); +} + +// This is called after we have received a load event, now we wait for painted +function waitForPainted() { + var utils = gPaintWindow.windowUtils; + + if (!utils.isMozAfterPaintPending || !useMozAfterPaint) { + _loadHandler(); + return; + } + + if (!gPaintListener) { + gPaintWindow.addEventListener("MozAfterPaint", plPainted, true); + } + gPaintListener = true; +} + +function plPainted() { + gPaintWindow.removeEventListener("MozAfterPaint", plPainted, true); + gPaintListener = false; + _loadHandler(); +} + +function _loadHandler(paint_time = 0) { + failTimeout.clear(); + var end_time = 0; + + if (paint_time !== 0) { + // window.performance.timing.timeToNonBlankPaint is a timestamp + // this may have a value for hero element (also a timestamp) + + let minDate = new Date("2001"); + + if (paint_time < minDate) { + //paint_time is a performance.now() value + end_time = paint_time; + } else { + //paint_time is a UNIX timestamp + end_time = paint_time - window.performance.timing.navigationStart; + } + } else { + end_time = window.performance.now(); + } + + var duration; + if (usePDFPaint) { + // PDF paint uses performance.now(), so the time does not need to be + // adjusted from the start time. + duration = end_time; + } else { + duration = end_time - start_time; + } + TalosParentProfiler.pause("Bubbling load handler fired."); + + // does this page want to do its own timing? + // if so, we shouldn't be here + if (plPageFlags() & TEST_DOES_OWN_TIMING) { + dumpLine( + "tp: Bubbling onload handler used with page that does its own timing?" + ); + plStop(true); + } + + plRecordTime(duration); + plNextPage(); +} + +// the core handler +function plLoadHandlerMessage(message) { + let paint_time = 0; + // XXX message.json.name contains the name + // of the load handler, so in future versions + // we can record several times per load. + if (message.json.time !== undefined) { + paint_time = message.json.time; + if (message.json.name == "fnbpaint") { + // we've received fnbpaint; no longer pending for this current pageload + isFNBPaintPending = false; + } else if (message.json.name == "pdfpaint") { + isPDFPaintPending = false; + } + } + + failTimeout.clear(); + + if (plPageFlags() & EXECUTE_SCROLL_TEST) { + // Let the page settle down after its load event, then execute the scroll test. + setTimeout(sendScroll, 500); + } else if (plPageFlags() & TEST_DOES_OWN_TIMING) { + var time; + + if (typeof gStartTime != "number") { + gStartTime = Date.parse(gStartTime); + } + if (gTime !== -1) { + if (useMozAfterPaint && gStartTime >= 0) { + time = window.performance.now() - gStartTime; + gStartTime = -1; + } else if (!useMozAfterPaint) { + time = gTime; + } + gTime = -1; + } + + if (time !== undefined) { + plRecordTime(time); + plNextPage(); + } + } else { + _loadHandler(paint_time); + } +} + +// the record time handler +function plRecordTimeMessage(message) { + gTime = message.json.time; + gStartTime = message.json.startTime; + recordedName = message.json.testName; + + if (useMozAfterPaint) { + gStartTime = message.json.startTime; + } + _loadHandlerCapturing(); +} + +// error +function plErrorMessage(message) { + if (message.json.msg) { + dumpLine(message.json.msg); + } + plStop(true); +} + +function plStop(force) { + plStopAll(force); +} + +function plStopAll(force) { + try { + if (!force) { + pageIndex = 0; + pageCycle = 1; + if (cycle < NUM_CYCLES - 1) { + cycle++; + recordedName = null; + setTimeout(plLoadPage, delay); + return; + } + + /* output report */ + dumpLine(report.getReport()); + dumpLine(report.getReportSummary()); + } + } catch (e) { + dumpLine(e); + } + + if (content) { + content.removeEventListener("load", plLoadHandlerCapturing, true); + content.removeEventListener("load", plLoadHandler, true); + + if (useMozAfterPaint) { + content.removeEventListener("MozAfterPaint", plPaintedCapturing, true); + content.removeEventListener("MozAfterPaint", plPainted, true); + } + + let mm = content.selectedBrowser.messageManager; + mm.removeMessageListener("PageLoader:LoadEvent", ContentListener); + mm.removeMessageListener("PageLoader:RecordTime", ContentListener); + mm.removeMessageListener("PageLoader:Error", ContentListener); + + if (isIdleCallbackPending) { + mm.removeMessageListener("PageLoader:IdleCallbackSet", ContentListener); + mm.removeMessageListener( + "PageLoader:IdleCallbackReceived", + ContentListener + ); + } + mm.loadFrameScript( + "data:,removeEventListener('load', _contentLoadHandler, true);", + false, + true + ); + } + + if (MozillaFileLogger && MozillaFileLogger._foStream) { + MozillaFileLogger.close(); + } + + goQuitApplication(); +} + +/* Returns array */ +function plLoadURLsFromURI(manifestUri) { + var fstream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance( + Ci.nsIFileInputStream + ); + + var uriFile = manifestUri.QueryInterface(Ci.nsIFileURL); + + if (uriFile.file.isFile() === false) { + dumpLine("tp: invalid file: %s" % uriFile.file); + return null; + } + + try { + fstream.init(uriFile.file, -1, 0, 0); + } catch (ex) { + dumpLine("tp: the file %s doesn't exist" % uriFile.file); + return null; + } + + var lstream = fstream.QueryInterface(Ci.nsILineInputStream); + + var url_array = []; + + var lineNo = 0; + var line = { value: null }; + var baseVsRefIndex = 0; + var more; + do { + lineNo++; + more = lstream.readLine(line); + var s = line.value; + + // strip comments (only leading ones) + s = s.replace(/^#.*/, ""); + + // strip leading and trailing whitespace + s = s.replace(/^\s*/, "").replace(/\s*$/, ""); + + if (!s) { + continue; + } + + var flags = 0; + var urlspec = s; + baseVsRefIndex += 1; + + // split on whitespace, and figure out if we have any flags + var items = s.split(/\s+/); + if (items[0] == "include") { + if (items.length != 2) { + dumpLine( + "tp: Error on line " + + lineNo + + " in " + + manifestUri.spec + + ": include must be followed by the manifest to include!" + ); + return null; + } + + var subManifest = Services.io.newURI(items[1], null, manifestUri); + if (subManifest == null) { + dumpLine( + "tp: invalid URI on line " + + manifestUri.spec + + ":" + + lineNo + + " : '" + + line.value + + "'" + ); + return null; + } + + var subItems = plLoadURLsFromURI(subManifest); + if (subItems == null) { + return null; + } + url_array = url_array.concat(subItems); + } else { + // For scrollTest flag, we accept "normal" pages but treat them as TEST_DOES_OWN_TIMING + // together with EXECUTE_SCROLL_TEST which makes us run the scroll test on load. + // We do this by artificially "injecting" the TEST_DOES_OWN_TIMING flag ("%") to the item + // and then let the default flow for this flag run without further modifications + // (other than calling the scroll test once the page is loaded). + // Note that if we have the scrollTest flag but the item already has "%", then we do + // nothing (the scroll test will not execute, and the page will report with its + // own tpRecordTime and not the one from the scroll test). + if (scrollTest && items.length == 1) { + // scroll enabled and no "%" + items.unshift("%"); + flags |= EXECUTE_SCROLL_TEST; + } + + if (items.length == 2) { + if (items[0].includes("%")) { + flags |= TEST_DOES_OWN_TIMING; + } + + urlspec = items[1]; + } else if (items.length == 3) { + // base vs ref type of talos test + // expect each manifest line to be in the format of: + // & http://localhost/tests/perf-reftest/base-page.html, http://localhost/tests/perf-reftest/reference-page.html + // test will run with the base page, then with the reference page; and ultimately the actual test results will + // be the comparison values of those two pages; more than one line will result in base vs ref subtests + if (items[0].includes("&")) { + baseVsRef = true; + flags |= TEST_DOES_OWN_TIMING; + // for the base, must remove the comma on the end of the actual url + var urlspecBase = items[1].slice(0, -1); + var urlspecRef = items[2]; + } else { + dumpLine( + "tp: Error on line " + + lineNo + + " in " + + manifestUri.spec + + ": unknown manifest format!" + ); + return null; + } + } else if (items.length != 1) { + dumpLine( + "tp: Error on line " + + lineNo + + " in " + + manifestUri.spec + + ": whitespace must be %-escaped!" + ); + return null; + } + + var url; + + if (!baseVsRef) { + url = Services.io.newURI(urlspec, null, manifestUri); + + if (pageFilterRegexp && !pageFilterRegexp.test(url.spec)) { + continue; + } + + url_array.push({ url, flags }); + } else { + // base vs ref type of talos test + // we add a 'pre' prefix here indicating that this particular page is a base page or a reference + // page; later on this 'pre' is used when recording the actual time value/result; because in + // the results we use the url as the results key; but we might use the same test page as a reference + // page in the same test suite, so we need to add a prefix so this results key is always unique + url = Services.io.newURI(urlspecBase, null, manifestUri); + if (pageFilterRegexp && !pageFilterRegexp.test(url.spec)) { + continue; + } + var pre = "base_page_" + baseVsRefIndex + "_"; + url_array.push({ url, flags, pre }); + + url = Services.io.newURI(urlspecRef, null, manifestUri); + if (pageFilterRegexp && !pageFilterRegexp.test(url.spec)) { + continue; + } + pre = "ref_page_" + baseVsRefIndex + "_"; + url_array.push({ url, flags, pre }); + } + } + } while (more); + + return url_array; +} + +function dumpLine(str) { + if (MozillaFileLogger && MozillaFileLogger._foStream) { + MozillaFileLogger.log(str + "\n"); + } + dump(str); + dump("\n"); +} diff --git a/testing/talos/talos/pageloader/chrome/pageloader.xhtml b/testing/talos/talos/pageloader/chrome/pageloader.xhtml new file mode 100644 index 0000000000..177502a57d --- /dev/null +++ b/testing/talos/talos/pageloader/chrome/pageloader.xhtml @@ -0,0 +1,56 @@ +<?xml version="1.0"?> +<!-- ***** BEGIN LICENSE BLOCK ***** + - Version: MPL 1.1/GPL 2.0/LGPL 2.1 + - + - The contents of this file are subject to the Mozilla Public License Version + - 1.1 (the "License"); you may not use this file except in compliance with + - the License. You may obtain a copy of the License at + - http://www.mozilla.org/MPL/ + - + - Software distributed under the License is distributed on an "AS IS" basis, + - WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + - for the specific language governing rights and limitations under the + - License. + - + - The Original Code is tp. + - + - The Initial Developer of the Original Code is the Mozilla Corporation. + - Portions created by the Initial Developer are Copyright (C) 2007 + - the Initial Developer. All Rights Reserved. + - + - Contributor(s): + - Rob Helmer <rhelmer@mozilla.com> + - Vladimir Vukicevic <vladimir@mozilla.com> + - + - Alternatively, the contents of this file may be used under the terms of + - either the GNU General Public License Version 2 or later (the "GPL"), or + - the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + - in which case the provisions of the GPL or the LGPL are applicable instead + - of those above. If you wish to allow use of your version of this file only + - under the terms of either the GPL or the LGPL, and not to allow others to + - use your version of this file under the terms of the MPL, indicate your + - decision by deleting the provisions above and replace them with the notice + - and other provisions required by the LGPL or the GPL. If you do not delete + - the provisions above, a recipient may use your version of this file under + - the terms of any one of the MPL, the GPL or the LGPL. + - + - ***** END LICENSE BLOCK ***** --> + +<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?> + +<window + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="pageloader" + screenX="0" screenY="0" + onload="plInit()"> + + <script type="application/x-javascript" + src="chrome://global/content/globalOverlay.js"/> + <script type="application/x-javascript" src="MozillaFileLogger.js"></script> + <script type="application/x-javascript" src="report.js"></script> + <script type="application/x-javascript" src="pageloader.js"></script> + <script type="application/x-javascript" src="quit.js"></script> + + <browser id="contentPageloader" src="about:blank" + type="content" flex="1"/> +</window> diff --git a/testing/talos/talos/pageloader/chrome/quit.js b/testing/talos/talos/pageloader/chrome/quit.js new file mode 100644 index 0000000000..0261295c1c --- /dev/null +++ b/testing/talos/talos/pageloader/chrome/quit.js @@ -0,0 +1,72 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; -*- */ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is The Original Code is Mozilla Automated Testing Code + * + * The Initial Developer of the Original Code is + * Mozilla Corporation. + * Portions created by the Initial Developer are Copyright (C) 2005 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): Bob Clary <bob@bclary.com> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +/* + From mozilla/toolkit/content + These files did not have a license +*/ + +/* import-globals-from pageloader.js */ + +function canQuitApplication() { + try { + var cancelQuit = Cc["@mozilla.org/supports-PRBool;1"].createInstance( + Ci.nsISupportsPRBool + ); + Services.obs.notifyObservers(cancelQuit, "quit-application-requested"); + + // Something aborted the quit process. + if (cancelQuit.data) { + return false; + } + } catch (ex) {} + return true; +} + +function goQuitApplication() { + if (!canQuitApplication()) { + return false; + } + + try { + Services.startup.quit(Ci.nsIAppStartup.eForceQuit); + } catch (ex) { + throw new Error("goQuitApplication: " + ex); + } + + return true; +} diff --git a/testing/talos/talos/pageloader/chrome/report.js b/testing/talos/talos/pageloader/chrome/report.js new file mode 100644 index 0000000000..1827aed36b --- /dev/null +++ b/testing/talos/talos/pageloader/chrome/report.js @@ -0,0 +1,195 @@ +/* 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/. */ + +// given an array of strings, finds the longest common prefix +function findCommonPrefixLength(strs) { + if (strs.length < 2) { + // only one page in the manifest + // i.e. http://localhost/tests/perf-reftest/bloom-basic.html + var place = strs[0].lastIndexOf("/"); + if (place < 0) { + place = 0; + } + return place; + } + + var len = 0; + do { + var newlen = len + 1; + var newprefix = null; + var failed = false; + for (var i = 0; i < strs.length; i++) { + if (newlen > strs[i].length) { + failed = true; + break; + } + + var s = strs[i].substr(0, newlen); + if (newprefix == null) { + newprefix = s; + } else if (newprefix != s) { + failed = true; + break; + } + } + + if (failed) { + break; + } + + len++; + } while (true); + return len; +} + +// Constructor +function Report() { + this.timeVals = {}; + this.totalCCTime = 0; + this.showTotalCCTime = false; +} + +Report.prototype.pageNames = function() { + var retval = []; + for (var page in this.timeVals) { + retval.push(page); + } + return retval; +}; + +Report.prototype.getReport = function() { + var report; + var pages = this.pageNames(); + var prefixLen = findCommonPrefixLength(pages); + + report = "__start_tp_report\n"; + report += "_x_x_mozilla_page_load\n"; + report += "_x_x_mozilla_page_load_details\n"; + report += "|i|pagename|runs|\n"; + + for (var i = 0; i < pages.length; i++) { + // don't report any measurements that were reported for about:blank + // some tests (like about-preferences) use it as a dummy test page + if (pages[i] == "about:blank") { + continue; + } + report += + "|" + + i + + ";" + + pages[i].substr(prefixLen) + + ";" + + this.timeVals[pages[i]].join(";") + + "\n"; + } + report += "__end_tp_report\n"; + + if (this.showTotalCCTime) { + report += "__start_cc_report\n"; + report += "_x_x_mozilla_cycle_collect," + this.totalCCTime + "\n"; + report += "__end_cc_report\n"; + } + var now = window.performance.now(); + report += "__startTimestamp" + now + "__endTimestamp\n"; // timestamp for determning shutdown time, used by talos + + return report; +}; + +Report.prototype.getReportSummary = function() { + function average(arr) { + var sum = 0; + for (var i in arr) { + sum += arr[i]; + } + return sum / (arr.length || 1); + } + + function median(arr) { + if (!arr.length) { + return 0; + } // As good indication for "not available" as any other value. + + var sorted = arr.slice(0).sort(); + var mid = Math.floor(arr.length / 2); + + if (sorted.length % 2) { + return sorted[mid]; + } + + return average(sorted.slice(mid, mid + 2)); + } + + // We use sample stddev and not population stddev because + // well.. it's a sample and we can't collect all/infinite number of values. + function stddev(arr) { + if (arr.length <= 1) { + return 0; + } + var avg = average(arr); + + var squareDiffArr = arr.map(function(v) { + return Math.pow(v - avg, 2); + }); + var sum = squareDiffArr.reduce(function(a, b) { + return a + b; + }); + var rv = Math.sqrt(sum / (arr.length - 1)); + return rv; + } + + var report = ""; + var pages = this.pageNames(); + var prefixLen = findCommonPrefixLength(pages); + + report += "------- Summary: start -------\n"; + report += "Number of tests: " + pages.length + "\n"; + + for (var i = 0; i < pages.length; i++) { + var results = this.timeVals[pages[i]].map(function(v) { + return Number(v); + }); + + report += + "\n[#" + + i + + "] " + + pages[i].substr(prefixLen) + + " Cycles:" + + results.length + + " Average:" + + average(results).toFixed(2) + + " Median:" + + median(results).toFixed(2) + + " stddev:" + + stddev(results).toFixed(2) + + " (" + + ((100 * stddev(results)) / median(results)).toFixed(1) + + "%)" + + (results.length < 5 + ? "" + : " stddev-sans-first:" + stddev(results.slice(1)).toFixed(2)) + + "\nValues: " + + results + .map(function(v) { + return v.toFixed(1); + }) + .join(" ") + + "\n"; + } + report += "-------- Summary: end --------\n"; + + return report; +}; + +Report.prototype.recordTime = function(pageName, ms) { + if (this.timeVals[pageName] == undefined) { + this.timeVals[pageName] = []; + } + this.timeVals[pageName].push(ms); +}; + +Report.prototype.recordCCTime = function(ms) { + this.totalCCTime += ms; + this.showTotalCCTime = true; +}; diff --git a/testing/talos/talos/pageloader/chrome/talos-content.js b/testing/talos/talos/pageloader/chrome/talos-content.js new file mode 100644 index 0000000000..ca21388c2c --- /dev/null +++ b/testing/talos/talos/pageloader/chrome/talos-content.js @@ -0,0 +1,24 @@ +/* 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/. */ + +/* eslint-env mozilla/frame-script */ + +const TalosContent = { + init() { + addMessageListener("Talos:ForceGC", this); + }, + + receiveMessage(msg) { + if (msg.name == "Talos:ForceGC") { + this.forceGC(); + } + }, + + forceGC() { + content.windowUtils.garbageCollect(); + sendAsyncMessage("Talos:ForceGC:OK"); + }, +}; + +TalosContent.init(); diff --git a/testing/talos/talos/pageloader/chrome/tscroll.js b/testing/talos/talos/pageloader/chrome/tscroll.js new file mode 100644 index 0000000000..03e039b583 --- /dev/null +++ b/testing/talos/talos/pageloader/chrome/tscroll.js @@ -0,0 +1,324 @@ +/* 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/. */ + +/* eslint-env mozilla/frame-script */ + +// Note: This file is used at both tscrollx and tp5o_scroll. With the former as +// unprivileged code. +// - Please make sure that any changes apply cleanly to all use cases. + +function testScroll(target, stepSize, opt_reportFunc, opt_numSteps) { + var win; + if (target == "content") { + target = content.wrappedJSObject; + win = content; + } else { + win = window; + } + + var result = { + names: [], + values: [], + }; + // We report multiple results, so we base the name on the path. + // Everything after '/tp5n/' if exists (for tp5o_scroll), or the file name at + // the path if non-empty (e.g. with tscrollx), or the last dir otherwise (e.g. + // 'mydir' for 'http://my.domain/dir1/mydir/'). + var href = win.location.href; + var testBaseName = + href.split("/tp5n/")[1] || + href.split("/").pop() || + href.split("/").splice(-2, 1)[0] || + "REALLY_WEIRD_URI"; + + // Verbatim copy from talos-powers/content/TalosPowersContent.js + // If the origin changes, this copy should be updated. + TalosPowersParent = { + replyId: 1, + + // dispatch an event to the framescript and register the result/callback event + exec(commandName, arg, callback, opt_custom_window) { + let win = opt_custom_window || window; + let replyEvent = "TalosPowers:ParentExec:ReplyEvent:" + this.replyId++; + if (callback) { + win.addEventListener( + replyEvent, + function(e) { + callback(e.detail); + }, + { once: true } + ); + } + win.dispatchEvent( + new win.CustomEvent("TalosPowers:ParentExec:QueryEvent", { + bubbles: true, + detail: { + command: { + name: commandName, + data: arg, + }, + listeningTo: replyEvent, + }, + }) + ); + }, + }; + // End of code from talos-powers + + var report; + /** + * Sets up the value of 'report' as a function for reporting the test result[s]. + * Chooses between the "usual" tpRecordTime which the pageloader addon injects + * to pages, or a custom function in case we're a framescript which pageloader + * added to the tested page, or a debug tpRecordTime from talos-debug.js if + * running in a plain browser. + * + * @returns Promise + */ + function P_setupReportFn() { + return new Promise(function(resolve) { + report = opt_reportFunc || win.tpRecordTime; + if (report == "PageLoader:RecordTime") { + report = function(duration, start, name) { + var msg = { time: duration, startTime: start, testName: name }; + sendAsyncMessage("PageLoader:RecordTime", msg); + }; + resolve(); + return; + } + + // Not part of the test and does nothing if we're within talos. + // Provides an alternative tpRecordTime (with some stats display) if running in a browser. + if (!report && document.head) { + var imported = document.createElement("script"); + imported.addEventListener("load", function() { + report = tpRecordTime; + resolve(); + }); + + imported.src = + "../../scripts/talos-debug.js?dummy=" + win.performance.now(); // For some browsers to re-read + document.head.appendChild(imported); + return; + } + + resolve(); + }); + } + + function FP_wait(ms) { + return function() { + return new Promise(function(resolve) { + win.setTimeout(resolve, ms); + }); + }; + } + + function rAF(fn) { + return win.requestAnimationFrame(fn); + } + + function P_rAF() { + return new Promise(function(resolve) { + rAF(resolve); + }); + } + + function P_MozAfterPaint() { + return new Promise(function(resolve) { + win.addEventListener("MozAfterPaint", () => resolve(), { once: true }); + }); + } + + var isWindow = target.self === target; + + var getPos = isWindow + ? function() { + return target.pageYOffset; + } + : function() { + return target.scrollTop; + }; + + var gotoTop = isWindow + ? function() { + target.scroll(0, 0); + ensureScroll(); + } + : function() { + target.scrollTop = 0; + ensureScroll(); + }; + + var doScrollTick = isWindow + ? function() { + target.scrollBy(0, stepSize); + ensureScroll(); + } + : function() { + target.scrollTop += stepSize; + ensureScroll(); + }; + + var setSmooth = isWindow + ? function() { + target.document.scrollingElement.style.scrollBehavior = "smooth"; + } + : function() { + target.style.scrollBehavior = "smooth"; + }; + + var gotoBottom = isWindow + ? function() { + target.scrollTo(0, target.scrollMaxY); + } + : function() { + target.scrollTop = target.scrollHeight; + }; + + function ensureScroll() { + // Ensure scroll by reading computed values. screenY is for X11. + if (!this.dummyEnsureScroll) { + this.dummyEnsureScroll = 1; + } + this.dummyEnsureScroll += win.screenY + getPos(); + } + + // For reference, rAF should fire on vsync, but Gecko currently doesn't use vsync. + // Instead, it uses 1000/layout.frame_rate + // (with 60 as default value when layout.frame_rate == -1). + function P_syncScrollTest() { + return new Promise(function(resolve) { + // We should be at the top of the page now. + var start = win.performance.now(); + var lastScrollPos = getPos(); + var lastScrollTime = start; + var durations = []; + + function tick() { + var now = win.performance.now(); + var duration = now - lastScrollTime; + lastScrollTime = now; + + durations.push(duration); + doScrollTick(); + + /* stop scrolling if we can't scroll more, or if we've reached requested number of steps */ + if ( + getPos() == lastScrollPos || + (opt_numSteps && durations.length >= opt_numSteps + 2) + ) { + let profilerPaused = Promise.resolve(); + if (typeof TalosContentProfiler !== "undefined") { + profilerPaused = TalosContentProfiler.pause(testBaseName, true); + } + + profilerPaused.then(() => { + // Note: The first (1-5) intervals WILL be longer than the rest. + // First interval might include initial rendering and be extra slow. + // Also requestAnimationFrame needs to sync (optimally in 1 frame) after long frames. + // Suggested: Ignore the first 5 intervals. + + durations.pop(); // Last step was 0. + durations.pop(); // and the prev one was shorter and with end-of-page logic, ignore both. + + if (win.talosDebug) { + win.talosDebug.displayData = true; + } // In a browser: also display all data points. + + // For analysis (otherwise, it's too many data points for talos): + var sum = 0; + for (var i = 0; i < durations.length; i++) { + sum += Number(durations[i]); + } + + // Report average interval or (failsafe) 0 if no intervls were recorded + result.values.push(durations.length ? sum / durations.length : 0); + result.names.push(testBaseName); + resolve(); + }); + return; + } + + lastScrollPos = getPos(); + P_MozAfterPaint().then(tick); + } + + if (typeof TalosContentProfiler !== "undefined") { + TalosContentProfiler.resume(testBaseName, true).then(() => { + rAF(tick); + }); + } + }); + } + + function P_testAPZScroll() { + var APZ_MEASURE_MS = 1000; + + function startFrameTimeRecording(cb) { + TalosPowersParent.exec("startFrameTimeRecording", null, cb, win); + } + + function stopFrameTimeRecording(handle, cb) { + TalosPowersParent.exec("stopFrameTimeRecording", handle, cb, win); + } + + return new Promise(function(resolve, reject) { + setSmooth(); + + var handle = -1; + startFrameTimeRecording(function(rv) { + handle = rv; + }); + + // Get the measurements after APZ_MEASURE_MS of scrolling + win.setTimeout(function() { + stopFrameTimeRecording(handle, function(intervals) { + function average(arr) { + var sum = 0; + for (var i = 0; i < arr.length; i++) { + sum += arr[i]; + } + return arr.length ? sum / arr.length : 0; + } + + // remove two frames on each side of the recording to get a cleaner result + result.values.push(average(intervals.slice(2, intervals.length - 2))); + result.names.push("CSSOM." + testBaseName); + + resolve(); + }); + }, APZ_MEASURE_MS); + + gotoBottom(); // trigger the APZ scroll + }); + } + + P_setupReportFn() + .then(FP_wait(260)) + .then(gotoTop) + .then(P_rAF) + .then(P_syncScrollTest) + .then(gotoTop) + .then(FP_wait(260)) + .then(P_testAPZScroll) + .then(function() { + report(result.values.join(","), 0, result.names.join(",")); + }); +} + +// This code below here is unique to tscroll.js inside of pageloader +try { + function handleMessageFromChrome(message) { + var payload = message.data.details; + testScroll( + payload.target, + payload.stepSize, + "PageLoader:RecordTime", + payload.opt_numSteps + ); + } + + addMessageListener("PageLoader:ScrollTest", handleMessageFromChrome); +} catch (e) {} diff --git a/testing/talos/talos/pageloader/chrome/utils.js b/testing/talos/talos/pageloader/chrome/utils.js new file mode 100644 index 0000000000..e326f6c6a4 --- /dev/null +++ b/testing/talos/talos/pageloader/chrome/utils.js @@ -0,0 +1,34 @@ +/* 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/. */ + +/* eslint-env mozilla/frame-script */ + +var idleCallbackHandle; + +function _idleCallbackHandler() { + content.window.cancelIdleCallback(idleCallbackHandle); + sendAsyncMessage("PageLoader:IdleCallbackReceived", {}); +} + +function setIdleCallback() { + idleCallbackHandle = content.window.requestIdleCallback(_idleCallbackHandler); + sendAsyncMessage("PageLoader:IdleCallbackSet", {}); +} + +function contentLoadHandlerCallback(cb) { + function _handler(e) { + if (e.originalTarget.defaultView == content) { + content.wrappedJSObject.tpRecordTime = Cu.exportFunction((t, s, n) => { + sendAsyncMessage("PageLoader:RecordTime", { + time: t, + startTime: s, + testName: n, + }); + }, content); + content.setTimeout(cb, 0); + content.setTimeout(setIdleCallback, 0); + } + } + return _handler; +} |