summaryrefslogtreecommitdiffstats
path: root/devtools/client/performance/test/helpers
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/performance/test/helpers')
-rw-r--r--devtools/client/performance/test/helpers/actions.js155
-rw-r--r--devtools/client/performance/test/helpers/dom-utils.js30
-rw-r--r--devtools/client/performance/test/helpers/event-utils.js114
-rw-r--r--devtools/client/performance/test/helpers/input-utils.js75
-rw-r--r--devtools/client/performance/test/helpers/moz.build20
-rw-r--r--devtools/client/performance/test/helpers/panel-utils.js106
-rw-r--r--devtools/client/performance/test/helpers/prefs.js72
-rw-r--r--devtools/client/performance/test/helpers/profiler-mm-utils.js117
-rw-r--r--devtools/client/performance/test/helpers/recording-utils.js54
-rw-r--r--devtools/client/performance/test/helpers/synth-utils.js99
-rw-r--r--devtools/client/performance/test/helpers/tab-utils.js85
-rw-r--r--devtools/client/performance/test/helpers/urls.js6
-rw-r--r--devtools/client/performance/test/helpers/wait-utils.js61
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 });
+};