diff options
Diffstat (limited to 'devtools/client/performance/test/helpers')
13 files changed, 994 insertions, 0 deletions
diff --git a/devtools/client/performance/test/helpers/actions.js b/devtools/client/performance/test/helpers/actions.js new file mode 100644 index 000000000..e6c70e565 --- /dev/null +++ b/devtools/client/performance/test/helpers/actions.js @@ -0,0 +1,155 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +const { Constants } = require("devtools/client/performance/modules/constants"); +const { once, times } = require("devtools/client/performance/test/helpers/event-utils"); +const { waitUntil } = require("devtools/client/performance/test/helpers/wait-utils"); + +/** + * Starts a manual recording in the given performance tool panel and + * waits for it to finish starting. + */ +exports.startRecording = function (panel, options = {}) { + let controller = panel.panelWin.PerformanceController; + + return Promise.all([ + controller.startRecording(), + exports.waitForRecordingStartedEvents(panel, options) + ]); +}; + +/** + * Stops the latest recording in the given performance tool panel and + * waits for it to finish stopping. + */ +exports.stopRecording = function (panel, options = {}) { + let controller = panel.panelWin.PerformanceController; + + return Promise.all([ + controller.stopRecording(), + exports.waitForRecordingStoppedEvents(panel, options) + ]); +}; + +/** + * Waits for all the necessary events to be emitted after a recording starts. + */ +exports.waitForRecordingStartedEvents = function (panel, options = {}) { + options.expectedViewState = options.expectedViewState || /^(console-)?recording$/; + + let EVENTS = panel.panelWin.EVENTS; + let controller = panel.panelWin.PerformanceController; + let view = panel.panelWin.PerformanceView; + let overview = panel.panelWin.OverviewView; + + return Promise.all([ + options.skipWaitingForBackendReady + ? null + : once(controller, EVENTS.BACKEND_READY_AFTER_RECORDING_START), + options.skipWaitingForRecordingStarted + ? null + : once(controller, EVENTS.RECORDING_STATE_CHANGE, { + expectedArgs: { "1": "recording-started" } + }), + options.skipWaitingForViewState + ? null + : once(view, EVENTS.UI_STATE_CHANGED, { + expectedArgs: { "1": options.expectedViewState } + }), + options.skipWaitingForOverview + ? null + : once(overview, EVENTS.UI_OVERVIEW_RENDERED, { + expectedArgs: { "1": Constants.FRAMERATE_GRAPH_LOW_RES_INTERVAL } + }), + ]); +}; + +/** + * Waits for all the necessary events to be emitted after a recording finishes. + */ +exports.waitForRecordingStoppedEvents = function (panel, options = {}) { + options.expectedViewClass = options.expectedViewClass || "WaterfallView"; + options.expectedViewEvent = options.expectedViewEvent || "UI_WATERFALL_RENDERED"; + options.expectedViewState = options.expectedViewState || "recorded"; + + let EVENTS = panel.panelWin.EVENTS; + let controller = panel.panelWin.PerformanceController; + let view = panel.panelWin.PerformanceView; + let overview = panel.panelWin.OverviewView; + let subview = panel.panelWin[options.expectedViewClass]; + + return Promise.all([ + options.skipWaitingForBackendReady + ? null + : once(controller, EVENTS.BACKEND_READY_AFTER_RECORDING_STOP), + options.skipWaitingForRecordingStop + ? null + : once(controller, EVENTS.RECORDING_STATE_CHANGE, { + expectedArgs: { "1": "recording-stopping" } + }), + options.skipWaitingForRecordingStop + ? null + : once(controller, EVENTS.RECORDING_STATE_CHANGE, { + expectedArgs: { "1": "recording-stopped" } + }), + options.skipWaitingForViewState + ? null + : once(view, EVENTS.UI_STATE_CHANGED, { + expectedArgs: { "1": options.expectedViewState } + }), + options.skipWaitingForOverview + ? null + : once(overview, EVENTS.UI_OVERVIEW_RENDERED, { + expectedArgs: { "1": Constants.FRAMERATE_GRAPH_HIGH_RES_INTERVAL } + }), + options.skipWaitingForSubview + ? null + : once(subview, EVENTS[options.expectedViewEvent]), + ]); +}; + +/** + * Waits for rendering to happen once on all the performance tool's widgets. + */ +exports.waitForAllWidgetsRendered = (panel) => { + let { panelWin } = panel; + let { EVENTS } = panelWin; + + return Promise.all([ + once(panelWin.OverviewView, EVENTS.UI_MARKERS_GRAPH_RENDERED), + once(panelWin.OverviewView, EVENTS.UI_MEMORY_GRAPH_RENDERED), + once(panelWin.OverviewView, EVENTS.UI_FRAMERATE_GRAPH_RENDERED), + once(panelWin.OverviewView, EVENTS.UI_OVERVIEW_RENDERED), + once(panelWin.WaterfallView, EVENTS.UI_WATERFALL_RENDERED), + once(panelWin.JsCallTreeView, EVENTS.UI_JS_CALL_TREE_RENDERED), + once(panelWin.JsFlameGraphView, EVENTS.UI_JS_FLAMEGRAPH_RENDERED), + once(panelWin.MemoryCallTreeView, EVENTS.UI_MEMORY_CALL_TREE_RENDERED), + once(panelWin.MemoryFlameGraphView, EVENTS.UI_MEMORY_FLAMEGRAPH_RENDERED) + ]); +}; + +/** + * Waits for rendering to happen on the performance tool's overview graph, + * making sure some markers were also rendered. + */ +exports.waitForOverviewRenderedWithMarkers = (panel, minTimes = 3, minMarkers = 1) => { + let { EVENTS, OverviewView, PerformanceController } = panel.panelWin; + + return Promise.all([ + times(OverviewView, EVENTS.UI_OVERVIEW_RENDERED, minTimes, { + expectedArgs: { "1": Constants.FRAMERATE_GRAPH_LOW_RES_INTERVAL } + }), + waitUntil(() => + PerformanceController.getCurrentRecording().getMarkers().length >= minMarkers + ), + ]); +}; + +/** + * Reloads the given tab target. + */ +exports.reload = (target) => { + target.activeTab.reload(); + return once(target, "navigate"); +}; diff --git a/devtools/client/performance/test/helpers/dom-utils.js b/devtools/client/performance/test/helpers/dom-utils.js new file mode 100644 index 000000000..559b2b8d8 --- /dev/null +++ b/devtools/client/performance/test/helpers/dom-utils.js @@ -0,0 +1,30 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +const Services = require("Services"); +const { waitForMozAfterPaint } = require("devtools/client/performance/test/helpers/wait-utils"); + +/** + * Checks if a DOM node is considered visible. + */ +exports.isVisible = (element) => { + return !element.classList.contains("hidden") && !element.hidden; +}; + +/** + * Appends the provided element to the provided parent node. If run in e10s + * mode, will also wait for MozAfterPaint to make sure the tab is rendered. + * Should be reviewed if Bug 1240509 lands. + */ +exports.appendAndWaitForPaint = function (parent, element) { + let isE10s = Services.appinfo.browserTabsRemoteAutostart; + if (isE10s) { + let win = parent.ownerDocument.defaultView; + let onMozAfterPaint = waitForMozAfterPaint(win); + parent.appendChild(element); + return onMozAfterPaint; + } + parent.appendChild(element); + return null; +}; diff --git a/devtools/client/performance/test/helpers/event-utils.js b/devtools/client/performance/test/helpers/event-utils.js new file mode 100644 index 000000000..aa184accc --- /dev/null +++ b/devtools/client/performance/test/helpers/event-utils.js @@ -0,0 +1,114 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +/* globals dump */ + +const Services = require("Services"); + +const KNOWN_EE_APIS = [ + ["on", "off"], + ["addEventListener", "removeEventListener"], + ["addListener", "removeListener"] +]; + +/** + * Listens for any event for a single time on a target, no matter what kind of + * event emitter it is, returning a promise resolved with the passed arguments + * once the event is fired. + */ +exports.once = function (target, eventName, options = {}) { + return exports.times(target, eventName, 1, options); +}; + +/** + * Waits for any event to be fired a specified amount of times on a target, no + * matter what kind of event emitter. + * Possible options: `useCapture`, `spreadArgs`, `expectedArgs` + */ +exports.times = function (target, eventName, receiveCount, options = {}) { + let msg = `Waiting for event: '${eventName}' on ${target} for ${receiveCount} time(s)`; + if ("expectedArgs" in options) { + dump(`${msg} with arguments: ${JSON.stringify(options.expectedArgs)}.\n`); + } else { + dump(`${msg}.\n`); + } + + return new Promise((resolve, reject) => { + if (typeof eventName != "string") { + reject(new Error(`Unexpected event name: ${eventName}.`)); + } + + let API = KNOWN_EE_APIS.find(([a, r]) => (a in target) && (r in target)); + if (!API) { + reject(new Error("Target is not a supported event listener.")); + return; + } + + let [add, remove] = API; + + target[add](eventName, function onEvent(...args) { + if ("expectedArgs" in options) { + for (let index of Object.keys(options.expectedArgs)) { + if ( + // Expected argument matches this regexp. + (options.expectedArgs[index] instanceof RegExp && + !options.expectedArgs[index].exec(args[index])) || + // Expected argument is not a regexp and equal to the received arg. + (!(options.expectedArgs[index] instanceof RegExp) && + options.expectedArgs[index] != args[index]) + ) { + dump(`Ignoring event '${eventName}' with unexpected argument at index ` + + `${index}: ${args[index]}\n`); + return; + } + } + } + if (--receiveCount > 0) { + dump(`Event: '${eventName}' on ${target} needs to be fired ${receiveCount} ` + + `more time(s).\n`); + } else if (!receiveCount) { + dump(`Event: '${eventName}' on ${target} received.\n`); + target[remove](eventName, onEvent, options.useCapture); + resolve(options.spreadArgs ? args : args[0]); + } + }, options.useCapture); + }); +}; + +/** + * Like `once`, but for observer notifications. + */ +exports.observeOnce = function (notificationName, options = {}) { + return exports.observeTimes(notificationName, 1, options); +}; + +/** + * Like `times`, but for observer notifications. + * Possible options: `expectedSubject` + */ +exports.observeTimes = function (notificationName, receiveCount, options = {}) { + dump(`Waiting for notification: '${notificationName}' for ${receiveCount} time(s).\n`); + + return new Promise((resolve, reject) => { + if (typeof notificationName != "string") { + reject(new Error(`Unexpected notification name: ${notificationName}.`)); + } + + Services.obs.addObserver(function onObserve(subject, topic, data) { + if ("expectedSubject" in options && options.expectedSubject != subject) { + dump(`Ignoring notification '${notificationName}' with unexpected subject: ` + + `${subject}\n`); + return; + } + if (--receiveCount > 0) { + dump(`Notification: '${notificationName}' needs to be fired ${receiveCount} ` + + `more time(s).\n`); + } else if (!receiveCount) { + dump(`Notification: '${notificationName}' received.\n`); + Services.obs.removeObserver(onObserve, topic); + resolve(data); + } + }, notificationName, false); + }); +}; diff --git a/devtools/client/performance/test/helpers/input-utils.js b/devtools/client/performance/test/helpers/input-utils.js new file mode 100644 index 000000000..180091d07 --- /dev/null +++ b/devtools/client/performance/test/helpers/input-utils.js @@ -0,0 +1,75 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +exports.HORIZONTAL_AXIS = 1; +exports.VERTICAL_AXIS = 2; + +/** + * Simulates a command event on an element. + */ +exports.command = (node) => { + let ev = node.ownerDocument.createEvent("XULCommandEvent"); + ev.initCommandEvent("command", true, true, node.ownerDocument.defaultView, 0, false, + false, false, false, null); + node.dispatchEvent(ev); +}; + +/** + * Simulates a click event on a devtools canvas graph. + */ +exports.clickCanvasGraph = (graph, { x, y }) => { + x = x || 0; + y = y || 0; + x /= graph._window.devicePixelRatio; + y /= graph._window.devicePixelRatio; + graph._onMouseMove({ testX: x, testY: y }); + graph._onMouseDown({ testX: x, testY: y }); + graph._onMouseUp({ testX: x, testY: y }); +}; + +/** + * Simulates a drag start event on a devtools canvas graph. + */ +exports.dragStartCanvasGraph = (graph, { x, y }) => { + x = x || 0; + y = y || 0; + x /= graph._window.devicePixelRatio; + y /= graph._window.devicePixelRatio; + graph._onMouseMove({ testX: x, testY: y }); + graph._onMouseDown({ testX: x, testY: y }); +}; + +/** + * Simulates a drag stop event on a devtools canvas graph. + */ +exports.dragStopCanvasGraph = (graph, { x, y }) => { + x = x || 0; + y = y || 0; + x /= graph._window.devicePixelRatio; + y /= graph._window.devicePixelRatio; + graph._onMouseMove({ testX: x, testY: y }); + graph._onMouseUp({ testX: x, testY: y }); +}; + +/** + * Simulates a scroll event on a devtools canvas graph. + */ +exports.scrollCanvasGraph = (graph, { axis, wheel, x, y }) => { + x = x || 1; + y = y || 1; + x /= graph._window.devicePixelRatio; + y /= graph._window.devicePixelRatio; + graph._onMouseMove({ + testX: x, + testY: y + }); + graph._onMouseWheel({ + testX: x, + testY: y, + axis: axis, + detail: wheel, + HORIZONTAL_AXIS: exports.HORIZONTAL_AXIS, + VERTICAL_AXIS: exports.VERTICAL_AXIS + }); +}; diff --git a/devtools/client/performance/test/helpers/moz.build b/devtools/client/performance/test/helpers/moz.build new file mode 100644 index 000000000..b858530d6 --- /dev/null +++ b/devtools/client/performance/test/helpers/moz.build @@ -0,0 +1,20 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +DevToolsModules( + 'actions.js', + 'dom-utils.js', + 'event-utils.js', + 'input-utils.js', + 'panel-utils.js', + 'prefs.js', + 'profiler-mm-utils.js', + 'recording-utils.js', + 'synth-utils.js', + 'tab-utils.js', + 'urls.js', + 'wait-utils.js', +) diff --git a/devtools/client/performance/test/helpers/panel-utils.js b/devtools/client/performance/test/helpers/panel-utils.js new file mode 100644 index 000000000..468a86607 --- /dev/null +++ b/devtools/client/performance/test/helpers/panel-utils.js @@ -0,0 +1,106 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +/* globals dump */ + +const { gDevTools } = require("devtools/client/framework/devtools"); +const { TargetFactory } = require("devtools/client/framework/target"); +const { addTab, removeTab } = require("devtools/client/performance/test/helpers/tab-utils"); +const { once } = require("devtools/client/performance/test/helpers/event-utils"); + +/** + * Initializes a toolbox panel in a new tab. + */ +exports.initPanelInNewTab = function* ({ tool, url, win }, options = {}) { + let tab = yield addTab({ url, win }, options); + return (yield exports.initPanelInTab({ tool, tab })); +}; + +/** + * Initializes a toolbox panel in the specified tab. + */ +exports.initPanelInTab = function* ({ tool, tab }) { + dump(`Initializing a ${tool} panel.\n`); + + let target = TargetFactory.forTab(tab); + yield target.makeRemote(); + + // Open a toolbox and wait for the connection to the performance actors + // to be opened. This is necessary because of the WebConsole's + // `profile` and `profileEnd` methods. + let toolbox = yield gDevTools.showToolbox(target, tool); + yield toolbox.initPerformance(); + + let panel = toolbox.getCurrentPanel(); + return { target, toolbox, panel }; +}; + +/** + * Initializes a performance panel in a new tab. + */ +exports.initPerformanceInNewTab = function* ({ url, win }, options = {}) { + let tab = yield addTab({ url, win }, options); + return (yield exports.initPerformanceInTab({ tab })); +}; + +/** + * Initializes a performance panel in the specified tab. + */ +exports.initPerformanceInTab = function* ({ tab }) { + return (yield exports.initPanelInTab({ + tool: "performance", + tab: tab + })); +}; + +/** + * Initializes a webconsole panel in a new tab. + * Returns a console property that allows calls to `profile` and `profileEnd`. + */ +exports.initConsoleInNewTab = function* ({ url, win }, options = {}) { + let tab = yield addTab({ url, win }, options); + return (yield exports.initConsoleInTab({ tab })); +}; + +/** + * Initializes a webconsole panel in the specified tab. + * Returns a console property that allows calls to `profile` and `profileEnd`. + */ +exports.initConsoleInTab = function* ({ tab }) { + let { target, toolbox, panel } = yield exports.initPanelInTab({ + tool: "webconsole", + tab: tab + }); + + let consoleMethod = function* (method, label, event) { + let recordingEventReceived = once(toolbox.performance, event); + if (label === undefined) { + yield panel.hud.jsterm.execute(`console.${method}()`); + } else { + yield panel.hud.jsterm.execute(`console.${method}("${label}")`); + } + yield recordingEventReceived; + }; + + let profile = function* (label) { + return yield consoleMethod("profile", label, "recording-started"); + }; + + let profileEnd = function* (label) { + return yield consoleMethod("profileEnd", label, "recording-stopped"); + }; + + return { target, toolbox, panel, console: { profile, profileEnd } }; +}; + +/** + * Tears down a toolbox panel and removes an associated tab. + */ +exports.teardownToolboxAndRemoveTab = function* (panel, options) { + dump("Destroying panel.\n"); + + let tab = panel.target.tab; + yield panel.toolbox.destroy(); + yield removeTab(tab, options); +}; diff --git a/devtools/client/performance/test/helpers/prefs.js b/devtools/client/performance/test/helpers/prefs.js new file mode 100644 index 000000000..4d17afe12 --- /dev/null +++ b/devtools/client/performance/test/helpers/prefs.js @@ -0,0 +1,72 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +const Services = require("Services"); +const { Preferences } = require("resource://gre/modules/Preferences.jsm"); + +// Prefs to revert to default once tests finish. Keep these in sync with +// all the preferences defined in devtools/client/preferences/devtools.js. +exports.MEMORY_SAMPLE_PROB_PREF = "devtools.performance.memory.sample-probability"; +exports.MEMORY_MAX_LOG_LEN_PREF = "devtools.performance.memory.max-log-length"; +exports.PROFILER_BUFFER_SIZE_PREF = "devtools.performance.profiler.buffer-size"; +exports.PROFILER_SAMPLE_RATE_PREF = "devtools.performance.profiler.sample-frequency-khz"; + +exports.UI_EXPERIMENTAL_PREF = "devtools.performance.ui.experimental"; +exports.UI_INVERT_CALL_TREE_PREF = "devtools.performance.ui.invert-call-tree"; +exports.UI_INVERT_FLAME_PREF = "devtools.performance.ui.invert-flame-graph"; +exports.UI_FLATTEN_RECURSION_PREF = "devtools.performance.ui.flatten-tree-recursion"; +exports.UI_SHOW_PLATFORM_DATA_PREF = "devtools.performance.ui.show-platform-data"; +exports.UI_SHOW_IDLE_BLOCKS_PREF = "devtools.performance.ui.show-idle-blocks"; +exports.UI_ENABLE_FRAMERATE_PREF = "devtools.performance.ui.enable-framerate"; +exports.UI_ENABLE_MEMORY_PREF = "devtools.performance.ui.enable-memory"; +exports.UI_ENABLE_ALLOCATIONS_PREF = "devtools.performance.ui.enable-allocations"; +exports.UI_ENABLE_MEMORY_FLAME_CHART = "devtools.performance.ui.enable-memory-flame"; + +exports.DEFAULT_PREF_VALUES = [ + "devtools.debugger.log", + "devtools.performance.enabled", + "devtools.performance.timeline.hidden-markers", + exports.MEMORY_SAMPLE_PROB_PREF, + exports.MEMORY_MAX_LOG_LEN_PREF, + exports.PROFILER_BUFFER_SIZE_PREF, + exports.PROFILER_SAMPLE_RATE_PREF, + exports.UI_EXPERIMENTAL_PREF, + exports.UI_INVERT_CALL_TREE_PREF, + exports.UI_INVERT_FLAME_PREF, + exports.UI_FLATTEN_RECURSION_PREF, + exports.UI_SHOW_PLATFORM_DATA_PREF, + exports.UI_SHOW_IDLE_BLOCKS_PREF, + exports.UI_ENABLE_FRAMERATE_PREF, + exports.UI_ENABLE_MEMORY_PREF, + exports.UI_ENABLE_ALLOCATIONS_PREF, + exports.UI_ENABLE_MEMORY_FLAME_CHART, + "devtools.performance.ui.show-jit-optimizations", + "devtools.performance.ui.show-triggers-for-gc-types", +].reduce((prefValues, prefName) => { + prefValues[prefName] = Preferences.get(prefName); + return prefValues; +}, {}); + +/** + * Invokes callback when a pref which is not in the `DEFAULT_PREF_VALUES` store + * is changed. Returns a cleanup function. + */ +exports.whenUnknownPrefChanged = function (branch, callback) { + function onObserve(subject, topic, data) { + if (!(data in exports.DEFAULT_PREF_VALUES)) { + callback(data); + } + } + Services.prefs.addObserver(branch, onObserve, false); + return () => Services.prefs.removeObserver(branch, onObserve); +}; + +/** + * Reverts all known preferences to their default values. + */ +exports.rollbackPrefsToDefault = function () { + for (let prefName of Object.keys(exports.DEFAULT_PREF_VALUES)) { + Preferences.set(prefName, exports.DEFAULT_PREF_VALUES[prefName]); + } +}; diff --git a/devtools/client/performance/test/helpers/profiler-mm-utils.js b/devtools/client/performance/test/helpers/profiler-mm-utils.js new file mode 100644 index 000000000..bffebf818 --- /dev/null +++ b/devtools/client/performance/test/helpers/profiler-mm-utils.js @@ -0,0 +1,117 @@ +/* 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"; + +/** + * The following functions are used in testing to control and inspect + * the nsIProfiler in child process content. These should be called from + * the parent process. + */ + +const { Cc, Ci } = require("chrome"); +const { Task } = require("devtools/shared/task"); + +const FRAME_SCRIPT_UTILS_URL = "chrome://devtools/content/shared/frame-script-utils.js"; + +let gMM = null; + +/** + * Loads the relevant frame scripts into the provided browser's message manager. + */ +exports.pmmLoadFrameScripts = (gBrowser) => { + gMM = gBrowser.selectedBrowser.messageManager; + gMM.loadFrameScript(FRAME_SCRIPT_UTILS_URL, false); +}; + +/** + * Clears the cached message manager. + */ +exports.pmmClearFrameScripts = () => { + gMM = null; +}; + +/** + * Sends a message to the message listener, attaching an id to the payload data. + * Resolves a returned promise when the response is received from the message + * listener, with the same id as part of the response payload data. + */ +exports.pmmUniqueMessage = function (message, payload) { + if (!gMM) { + throw new Error("`pmmLoadFrameScripts()` must be called when using MessageManager."); + } + + let { generateUUID } = Cc["@mozilla.org/uuid-generator;1"] + .getService(Ci.nsIUUIDGenerator); + payload.id = generateUUID().toString(); + + return new Promise(resolve => { + gMM.addMessageListener(message + ":response", function onHandler({ data }) { + if (payload.id == data.id) { + gMM.removeMessageListener(message + ":response", onHandler); + resolve(data.data); + } + }); + gMM.sendAsyncMessage(message, payload); + }); +}; + +/** + * Checks if the nsProfiler module is active. + */ +exports.pmmIsProfilerActive = () => { + return exports.pmmSendProfilerCommand("IsActive"); +}; + +/** + * Starts the nsProfiler module. + */ +exports.pmmStartProfiler = Task.async(function* ({ entries, interval, features }) { + let isActive = (yield exports.pmmSendProfilerCommand("IsActive")).isActive; + if (!isActive) { + return exports.pmmSendProfilerCommand("StartProfiler", [entries, interval, features, + features.length]); + } + return null; +}); +/** + * Stops the nsProfiler module. + */ +exports.pmmStopProfiler = Task.async(function* () { + let isActive = (yield exports.pmmSendProfilerCommand("IsActive")).isActive; + if (isActive) { + return exports.pmmSendProfilerCommand("StopProfiler"); + } + return null; +}); + +/** + * Calls a method on the nsProfiler module. + */ +exports.pmmSendProfilerCommand = (method, args = []) => { + return exports.pmmUniqueMessage("devtools:test:profiler", { method, args }); +}; + +/** + * Evaluates a script in content, returning a promise resolved with the + * returned result. + */ +exports.pmmEvalInDebuggee = (script) => { + return exports.pmmUniqueMessage("devtools:test:eval", { script }); +}; + +/** + * Evaluates a console method in content. + */ +exports.pmmConsoleMethod = function (method, ...args) { + // Terrible ugly hack -- this gets stringified when it uses the + // message manager, so an undefined arg in `console.profileEnd()` + // turns into a stringified "null", which is terrible. This method + // is only used for test helpers, so swap out the argument if its undefined + // with an empty string. Differences between empty string and undefined are + // tested on the front itself. + if (args[0] == null) { + args[0] = ""; + } + return exports.pmmUniqueMessage("devtools:test:console", { method, args }); +}; diff --git a/devtools/client/performance/test/helpers/recording-utils.js b/devtools/client/performance/test/helpers/recording-utils.js new file mode 100644 index 000000000..e51e2d5dd --- /dev/null +++ b/devtools/client/performance/test/helpers/recording-utils.js @@ -0,0 +1,54 @@ +/* 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"; + +/** + * These utilities provide a functional interface for accessing the particulars + * about the recording's details. + */ + +/** + * Access the selected view from the panel's recording list. + * + * @param {object} panel - The current panel. + * @return {object} The recording model. + */ +exports.getSelectedRecording = function (panel) { + const view = panel.panelWin.RecordingsView; + return view.selected; +}; + +/** + * Set the selected index of the recording via the panel. + * + * @param {object} panel - The current panel. + * @return {number} index + */ +exports.setSelectedRecording = function (panel, index) { + const view = panel.panelWin.RecordingsView; + view.setSelectedByIndex(index); + return index; +}; + +/** + * Access the selected view from the panel's recording list. + * + * @param {object} panel - The current panel. + * @return {number} index + */ +exports.getSelectedRecordingIndex = function (panel) { + const view = panel.panelWin.RecordingsView; + return view.getSelectedIndex(); +}; + +exports.getDurationLabelText = function (panel, elementIndex) { + const { $$ } = panel.panelWin; + const elements = $$(".recording-list-item-duration", panel.panelWin.document); + return elements[elementIndex].innerHTML; +}; + +exports.getRecordingsCount = function (panel) { + const { $$ } = panel.panelWin; + return $$(".recording-list-item", panel.panelWin.document).length; +}; diff --git a/devtools/client/performance/test/helpers/synth-utils.js b/devtools/client/performance/test/helpers/synth-utils.js new file mode 100644 index 000000000..d4631a3f1 --- /dev/null +++ b/devtools/client/performance/test/helpers/synth-utils.js @@ -0,0 +1,99 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +/** + * Generates a generalized profile with some samples. + */ +exports.synthesizeProfile = () => { + const { CATEGORY_MASK } = require("devtools/client/performance/modules/categories"); + const RecordingUtils = require("devtools/shared/performance/recording-utils"); + + return RecordingUtils.deflateProfile({ + meta: { version: 2 }, + threads: [{ + samples: [{ + time: 1, + frames: [ + { category: CATEGORY_MASK("other"), location: "(root)" }, + { category: CATEGORY_MASK("other"), location: "A (http://foo/bar/baz:12)" }, + { category: CATEGORY_MASK("css"), location: "B (http://foo/bar/baz:34)" }, + { category: CATEGORY_MASK("js"), location: "C (http://foo/bar/baz:56)" } + ] + }, { + time: 1 + 1, + frames: [ + { category: CATEGORY_MASK("other"), location: "(root)" }, + { category: CATEGORY_MASK("other"), location: "A (http://foo/bar/baz:12)" }, + { category: CATEGORY_MASK("css"), location: "B (http://foo/bar/baz:34)" }, + { category: CATEGORY_MASK("gc", 1), location: "D (http://foo/bar/baz:78:9)" } + ] + }, { + time: 1 + 1 + 2, + frames: [ + { category: CATEGORY_MASK("other"), location: "(root)" }, + { category: CATEGORY_MASK("other"), location: "A (http://foo/bar/baz:12)" }, + { category: CATEGORY_MASK("css"), location: "B (http://foo/bar/baz:34)" }, + { category: CATEGORY_MASK("gc", 1), location: "D (http://foo/bar/baz:78:9)" } + ] + }, { + time: 1 + 1 + 2 + 3, + frames: [ + { category: CATEGORY_MASK("other"), location: "(root)" }, + { category: CATEGORY_MASK("other"), location: "A (http://foo/bar/baz:12)" }, + { category: CATEGORY_MASK("gc", 2), location: "E (http://foo/bar/baz:90)" }, + { category: CATEGORY_MASK("network"), location: "F (http://foo/bar/baz:99)" } + ] + }] + }] + }); +}; + +/** + * Generates a simple implementation for a tree class. + */ +exports.synthesizeCustomTreeClass = () => { + const { Cu } = require("chrome"); + const { AbstractTreeItem } = Cu.import("resource://devtools/client/shared/widgets/AbstractTreeItem.jsm", {}); + const { Heritage } = require("devtools/client/shared/widgets/view-helpers"); + + function MyCustomTreeItem(dataSrc, properties) { + AbstractTreeItem.call(this, properties); + this.itemDataSrc = dataSrc; + } + + MyCustomTreeItem.prototype = Heritage.extend(AbstractTreeItem.prototype, { + _displaySelf: function (document, arrowNode) { + let node = document.createElement("hbox"); + node.style.marginInlineStart = (this.level * 10) + "px"; + node.appendChild(arrowNode); + node.appendChild(document.createTextNode(this.itemDataSrc.label)); + return node; + }, + + _populateSelf: function (children) { + for (let childDataSrc of this.itemDataSrc.children) { + children.push(new MyCustomTreeItem(childDataSrc, { + parent: this, + level: this.level + 1 + })); + } + } + }); + + const myDataSrc = { + label: "root", + children: [{ + label: "foo", + children: [] + }, { + label: "bar", + children: [{ + label: "baz", + children: [] + }] + }] + }; + + return { MyCustomTreeItem, myDataSrc }; +}; diff --git a/devtools/client/performance/test/helpers/tab-utils.js b/devtools/client/performance/test/helpers/tab-utils.js new file mode 100644 index 000000000..3247faabf --- /dev/null +++ b/devtools/client/performance/test/helpers/tab-utils.js @@ -0,0 +1,85 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +/* globals dump */ + +const Services = require("Services"); +const tabs = require("sdk/tabs"); +const tabUtils = require("sdk/tabs/utils"); +const { viewFor } = require("sdk/view/core"); +const { waitForDelayedStartupFinished } = require("devtools/client/performance/test/helpers/wait-utils"); +const { gDevTools } = require("devtools/client/framework/devtools"); + +/** + * Gets a random integer in between an interval. Used to uniquely identify + * added tabs by augmenting the URL. + */ +function getRandomInt(min, max) { + return Math.floor(Math.random() * (max - min + 1)) + min; +} + +/** + * Adds a browser tab with the given url in the specified window and waits + * for it to load. + */ +exports.addTab = function ({ url, win }, options = {}) { + let id = getRandomInt(0, Number.MAX_SAFE_INTEGER - 1); + url += `#${id}`; + + dump(`Adding tab with url: ${url}.\n`); + + return new Promise(resolve => { + let tab; + + tabs.on("ready", function onOpen(model) { + if (tab != viewFor(model)) { + return; + } + dump(`Tab added and finished loading: ${model.url}.\n`); + tabs.off("ready", onOpen); + resolve(tab); + }); + + win.focus(); + tab = tabUtils.openTab(win, url); + + if (options.dontWaitForTabReady) { + resolve(tab); + } + }); +}; + +/** + * Removes a browser tab from the specified window and waits for it to close. + */ +exports.removeTab = function (tab, options = {}) { + dump(`Removing tab: ${tabUtils.getURI(tab)}.\n`); + + return new Promise(resolve => { + tabs.on("close", function onClose(model) { + if (tab != viewFor(model)) { + return; + } + dump(`Tab removed and finished closing: ${model.url}.\n`); + tabs.off("close", onClose); + resolve(tab); + }); + + tabUtils.closeTab(tab); + + if (options.dontWaitForTabClose) { + resolve(tab); + } + }); +}; + +/** + * Adds a browser window with the provided options. + */ +exports.addWindow = function* (options) { + let { OpenBrowserWindow } = Services.wm.getMostRecentWindow(gDevTools.chromeWindowType); + let win = OpenBrowserWindow(options); + yield waitForDelayedStartupFinished(win); + return win; +}; diff --git a/devtools/client/performance/test/helpers/urls.js b/devtools/client/performance/test/helpers/urls.js new file mode 100644 index 000000000..3bf1180b3 --- /dev/null +++ b/devtools/client/performance/test/helpers/urls.js @@ -0,0 +1,6 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +exports.EXAMPLE_URL = "http://example.com/browser/devtools/client/performance/test"; +exports.SIMPLE_URL = `${exports.EXAMPLE_URL}/doc_simple-test.html`; diff --git a/devtools/client/performance/test/helpers/wait-utils.js b/devtools/client/performance/test/helpers/wait-utils.js new file mode 100644 index 000000000..be654b7d8 --- /dev/null +++ b/devtools/client/performance/test/helpers/wait-utils.js @@ -0,0 +1,61 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +/* globals dump */ + +const { CC } = require("chrome"); +const { Task } = require("devtools/shared/task"); +const DevToolsUtils = require("devtools/shared/DevToolsUtils"); +const { once, observeOnce } = require("devtools/client/performance/test/helpers/event-utils"); + +/** + * Blocks the main thread for the specified amount of time. + */ +exports.busyWait = function (time) { + dump(`Busy waiting for: ${time} milliseconds.\n`); + let start = Date.now(); + /* eslint-disable no-unused-vars */ + let stack; + while (Date.now() - start < time) { + stack = CC.stack; + } + /* eslint-enable no-unused-vars */ +}; + +/** + * Idly waits for the specified amount of time. + */ +exports.idleWait = function (time) { + dump(`Idly waiting for: ${time} milliseconds.\n`); + return DevToolsUtils.waitForTime(time); +}; + +/** + * Waits until a predicate returns true. + */ +exports.waitUntil = function* (predicate, interval = 100, tries = 100) { + for (let i = 1; i <= tries; i++) { + if (yield Task.spawn(predicate)) { + dump(`Predicate returned true after ${i} tries.\n`); + return; + } + yield exports.idleWait(interval); + } + throw new Error(`Predicate returned false after ${tries} tries, aborting.\n`); +}; + +/** + * Waits for a `MozAfterPaint` event to be fired on the specified window. + */ +exports.waitForMozAfterPaint = function (window) { + return once(window, "MozAfterPaint"); +}; + +/** + * Waits for the `browser-delayed-startup-finished` observer notification + * to be fired on the specified window. + */ +exports.waitForDelayedStartupFinished = function (window) { + return observeOnce("browser-delayed-startup-finished", { expectedSubject: window }); +}; |