new file mode 100644
--- /dev/null
+++ b/devtools/client/performance/new/components/Perf.js
@@ -0,0 +1,233 @@
+/* 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";
+
+const { createClass, PropTypes, DOM } = require("devtools/client/shared/vendor/react");
+const { div, button } = DOM;
+
+/**
+ * The recordingState is one of the following:
+ **/
+
+// The initial state before we've queried the PerfActor
+const NOT_YET_KNOWN = "not-yet-known";
+// The profiler is available, we haven't started recording yet.
+const AVAILABLE_TO_RECORD = "available-to-record";
+// An async request has been sent to start the profiler.
+const REQUEST_TO_START_RECORDING = "request-to-start-recording";
+// An async request has been sent to get the profile and stop the profiler.
+const REQUEST_TO_GET_PROFILE_AND_STOP_PROFILER =
+ "request-to-get-profile-and-stop-profiler";
+// An async request has been sent to stop the profiler.
+const REQUEST_TO_STOP_PROFILER = "request-to-stop-profiler";
+// The profiler notified us that our request to start it actually started it.
+const RECORDING = "recording";
+// Some other code with access to the profiler started it.
+const OTHER_IS_RECORDING = "other-is-recording";
+
+const Perf = createClass({
+ displayName: "Perf",
+
+ propTypes: {
+ perfFront: PropTypes.object.isRequired,
+ receiveProfile: PropTypes.func.isRequired
+ },
+
+ getInitialState() {
+ return {
+ recordingState: NOT_YET_KNOWN,
+ recordingUnexpectedlyStopped: false,
+ };
+ },
+
+ getRecordingStateForTesting() {
+ return this.state.recordingState;
+ },
+
+ componentDidMount() {
+ // We don't yet know what state the profile is in, find out.
+ this.props.perfFront.isActive().then(isActive => {
+ if (this.state.recordingState === NOT_YET_KNOWN) {
+ this.setState({
+ recordingState: isActive ? OTHER_IS_RECORDING : AVAILABLE_TO_RECORD
+ });
+ }
+ });
+
+ // Handle when the profiler changes state. It might be us, it might be someone else.
+ this.props.perfFront.on("profiler-started", this.handleProfilerStarting);
+ this.props.perfFront.on("profiler-stopped", this.handleProfilerStopping);
+ },
+
+ handleProfilerStarting() {
+ switch (this.state.recordingState) {
+ case NOT_YET_KNOWN:
+ // We couldn't have started it yet, so it must have been someone
+ // else. (fallthrough)
+ case AVAILABLE_TO_RECORD:
+ // We aren't recording, someone else started it up. (fallthrough)
+ case REQUEST_TO_STOP_PROFILER:
+ // We aren't recording, someone else started it up. (fallthrough)
+ case REQUEST_TO_GET_PROFILE_AND_STOP_PROFILER:
+ // Someone re-started the profiler while we were asking for the completed
+ // profile.
+
+ this.setState({
+ recordingState: OTHER_IS_RECORDING,
+ recordingUnexpectedlyStopped: false
+ });
+ break;
+
+ case REQUEST_TO_START_RECORDING:
+ // Wait for the profiler to tell us that it has started.
+ this.setState({
+ recordingState: RECORDING,
+ recordingUnexpectedlyStopped: false
+ });
+ break;
+
+ case OTHER_IS_RECORDING:
+ case RECORDING:
+ // These state cases don't make sense to happen, and means we have a logical
+ // fallacy somewhere.
+ throw new Error(
+ "The profiler started recording, when it shouldn't have " +
+ `been able to. Current state: "${this.state.recordingState}"`);
+ default:
+ throw new Error("Unhandled recording state");
+ }
+ },
+
+ handleProfilerStopping() {
+ switch (this.state.recordingState) {
+ case NOT_YET_KNOWN:
+ case REQUEST_TO_GET_PROFILE_AND_STOP_PROFILER:
+ case REQUEST_TO_STOP_PROFILER:
+ case OTHER_IS_RECORDING:
+ this.setState({
+ recordingState: AVAILABLE_TO_RECORD,
+ recordingUnexpectedlyStopped: false
+ });
+ break;
+
+ case REQUEST_TO_START_RECORDING:
+ // Highly unlikely, but someone stopped the recorder, this is fine. Do nothing.
+ break;
+
+ case RECORDING:
+ this.setState({
+ recordingState: AVAILABLE_TO_RECORD,
+ recordingUnexpectedlyStopped: true
+ });
+ break;
+
+ case AVAILABLE_TO_RECORD:
+ throw new Error(
+ "The profiler stopped recording, when it shouldn't have been able to.");
+ default:
+ throw new Error("Unhandled recording state");
+ }
+ },
+
+ startRecording() {
+ this.setState({
+ recordingState: REQUEST_TO_START_RECORDING,
+ // Reset this error state since it's no longer valid.
+ recordingUnexpectedlyStopped: false,
+ });
+ this.props.perfFront.startProfiler();
+ },
+
+ getProfileAndStopProfiler: async function () {
+ this.setState({ recordingState: REQUEST_TO_GET_PROFILE_AND_STOP_PROFILER });
+ const profile = await this.props.perfFront.getProfileAndStopProfiler();
+ this.setState({ recordingState: AVAILABLE_TO_RECORD });
+ console.log("getProfileAndStopProfiler");
+ this.props.receiveProfile(profile);
+ },
+
+ stopProfilerAndDiscardProfile: async function () {
+ this.setState({ recordingState: REQUEST_TO_STOP_PROFILER });
+ this.props.perfFront.stopProfilerAndDiscardProfile();
+ },
+
+ render() {
+ const { recordingState } = this.state;
+ // TODO - L10N all of the messages.
+ switch (recordingState) {
+ case NOT_YET_KNOWN:
+ return null;
+
+ case AVAILABLE_TO_RECORD:
+ return renderButton({
+ recordingState,
+ onClick: this.startRecording,
+ label: "Start recording",
+ additionalMessage: this.state.recordingUnexpectedlyStopped
+ ? div({}, "The recording was stopped by another tool.")
+ : null
+ });
+
+ case REQUEST_TO_STOP_PROFILER:
+ return renderButton({
+ recordingState,
+ label: "Stopping the recording",
+ disabled: true
+ });
+
+ case REQUEST_TO_GET_PROFILE_AND_STOP_PROFILER:
+ return renderButton({
+ recordingState,
+ label: "Stopping the recording, and capturing the profile",
+ disabled: true
+ });
+
+ case REQUEST_TO_START_RECORDING:
+ case RECORDING:
+ return renderButton({
+ recordingState,
+ label: "Stop and grab the recording",
+ onClick: this.getProfileAndStopProfiler,
+ disabled: this.state.recordingState === REQUEST_TO_START_RECORDING
+ });
+
+ case OTHER_IS_RECORDING:
+ return renderButton({
+ recordingState,
+ label: "Stop and discard the other recording",
+ onClick: this.stopProfilerAndDiscardProfile,
+ disabled: this.state.recordingState === REQUEST_TO_START_RECORDING,
+ additionalMessage: "Another tool is currently recording."
+ });
+
+ default:
+ throw new Error("Unhandled recording state");
+ }
+ }
+});
+
+module.exports = Perf;
+
+function renderButton(props) {
+ const { disabled, label, onClick, additionalMessage, recordingState } = props;
+ const nbsp = "\u00A0";
+
+ return div(
+ { className: "perf" },
+ div({ className: "perf-additional-message" }, additionalMessage || nbsp),
+ div(
+ {},
+ button(
+ {
+ className: "devtools-button perf-button",
+ "data-standalone": true,
+ "data-state": recordingState,
+ disabled,
+ onClick
+ },
+ label
+ )
+ )
+ );
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/performance/new/components/moz.build
@@ -0,0 +1,8 @@
+# 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(
+ 'Perf.js',
+)
--- a/devtools/client/performance/new/initializer.js
+++ b/devtools/client/performance/new/initializer.js
@@ -1,254 +1,39 @@
/* 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";
+
/* exported gInit */
const { utils: Cu } = Components;
const BrowserLoaderModule = {};
Cu.import("resource://devtools/client/shared/browser-loader.js", BrowserLoaderModule);
const { require } = BrowserLoaderModule.BrowserLoader({
baseURI: "resource://devtools/client/memory/",
window
});
-const { createClass, createElement, PropTypes, DOM } = require("devtools/client/shared/vendor/react");
+const Perf = require("devtools/client/performance/new/components/Perf");
const { render } = require("devtools/client/shared/vendor/react-dom");
-const { div, button } = DOM;
-
-/**
- * The recordingState is one of the following:
- **/
-
-// The initial state before we've queried the PerfActor
-const NOT_YET_KNOWN = "not-yet-known";
-// The profiler is available, we haven't started recording yet.
-const AVAILABLE_TO_RECORD = "available-to-record";
-// An async request has been sent to start the profiler.
-const REQUEST_TO_START_RECORDING = "request-to-start-recording";
-// An async request has been sent to get the profile and stop the profiler.
-const REQUEST_TO_GET_PROFILE_AND_STOP_PROFILER =
- "request-to-get-profile-and-stop-profiler";
-// An async request has been sent to stop the profiler.
-const REQUEST_TO_STOP_PROFILER = "request-to-stop-profiler";
-// The profiler notified us that our request to start it actually started it.
-const RECORDING = "recording";
-// Some other code with access to the profiler started it.
-const OTHER_IS_RECORDING = "other-is-recording";
-
-const Perf = createClass({
- displayName: "Perf",
-
- propTypes: {
- perfFront: PropTypes.object.isRequired,
- },
-
- getInitialState() {
- return {
- recordingState: NOT_YET_KNOWN,
- recordingUnexpectedlyStopped: false,
- };
- },
-
- componentDidUpdate() {
- console.log("!!! recordingState", this.state.recordingState);
- },
-
- componentDidMount() {
- // We don't yet know what state the profile is in, find out.
- this.props.perfFront.isActive().then(isActive => {
- if (this.state.recordingState === NOT_YET_KNOWN) {
- this.setState({
- recordingState: isActive ? OTHER_IS_RECORDING : AVAILABLE_TO_RECORD
- });
- }
- });
-
- // Handle when the profiler changes state. It might be us, it might be someone else.
- this.props.perfFront.on("profiler-started", this.handleProfilerStarting);
-
- // Handle when the profiler changes state. It might be us, it might be someone else.
- this.props.perfFront.on("profiler-stopped", this.handleProfilerStopping);
- },
-
- handleProfilerStarting() {
- switch (this.state.recordingState) {
- // Eslint doesn't allow comments between case statements:
- //
- // case NOT_YET_KNOWN:
- // We couldn't have started it yet, so it must have been someone else.
- // case AVAILABLE_TO_RECORD
- // We aren't recording, someone else started it up.
- // case REQUEST_TO_STOP_PROFILER
- // Someone re-started the profiler while we were asking for the completed
- // profile.
- // case REQUEST_TO_GET_PROFILE_AND_STOP_PROFILER
- // Someone re-started the profiler while we were asking for the completed
- // profile.
- case NOT_YET_KNOWN:
- case AVAILABLE_TO_RECORD:
- case REQUEST_TO_STOP_PROFILER:
- case REQUEST_TO_GET_PROFILE_AND_STOP_PROFILER:
- this.setState({
- recordingState: OTHER_IS_RECORDING,
- recordingUnexpectedlyStopped: false
- });
- break;
-
- // Wait for the profiler to tell us that it has started.
- case REQUEST_TO_START_RECORDING:
- this.setState({
- recordingState: RECORDING,
- recordingUnexpectedlyStopped: false
- });
- break;
-
- // These don't make sense to happen, and means we have a logical fallacy somewhere.
- case OTHER_IS_RECORDING:
- case RECORDING:
- throw new Error(
- "The profiler started recording, when it shouldn't have been able to.");
- default:
- throw new Error("Unhandled recording state");
- }
- },
-
- handleProfilerStopping() {
- switch (this.state.recordingState) {
- case NOT_YET_KNOWN:
- case REQUEST_TO_GET_PROFILE_AND_STOP_PROFILER:
- case REQUEST_TO_STOP_PROFILER:
- case OTHER_IS_RECORDING:
- this.setState({
- recordingState: AVAILABLE_TO_RECORD,
- recordingUnexpectedlyStopped: false
- });
- break;
-
- // Highly unlikely, but someone stopped the recorder, this is fine.
- case REQUEST_TO_START_RECORDING:
- // Do nothing.
- break;
-
- case RECORDING:
- this.setState({
- recordingState: AVAILABLE_TO_RECORD,
- recordingUnexpectedlyStopped: true
- });
- break;
-
- case AVAILABLE_TO_RECORD:
- throw new Error(
- "The profiler stopped recording, when it shouldn't have been able to.");
- default:
- throw new Error("Unhandled recording state");
- }
- },
-
- startRecording() {
- this.setState({
- recordingState: REQUEST_TO_START_RECORDING,
- // Reset this error state since it's no longer valid.
- recordingUnexpectedlyStopped: false,
- });
- this.props.perfFront.startProfiler();
- },
-
- getProfileAndStopProfiler: async function () {
- this.setState({ recordingState: REQUEST_TO_GET_PROFILE_AND_STOP_PROFILER });
- const profile = await this.props.perfFront.getProfileAndStopProfiler();
- this.setState({ recordingState: AVAILABLE_TO_RECORD });
-
- // Open up a new tab and send a message with the profile.
- const browser = top.gBrowser;
- const tab = browser.addTab("https://perf-html.io/from-addon");
- browser.selectedTab = tab;
- const mm = tab.linkedBrowser.messageManager;
- mm.loadFrameScript("chrome://devtools/content/performance/new/frame-script.js",
- false);
- mm.sendAsyncMessage("devtools:perf-html-transfer-profile", profile);
- },
-
- stopProfilerAndDiscardProfile: async function () {
- this.props.perfFront.stopProfilerAndDiscardProfile();
- },
-
- render() {
- // TODO - L10N all of the messages.
- switch (this.state.recordingState) {
- case NOT_YET_KNOWN:
- return null;
-
- case AVAILABLE_TO_RECORD:
- return renderButton({
- onClick: this.startRecording,
- label: "Start recording",
- additionalMessage: this.state.recordingUnexpectedlyStopped
- ? div({}, "The recording was stopped by another tool.")
- : null
- });
-
- case REQUEST_TO_STOP_PROFILER:
- return renderButton({
- label: "Stopping the recording",
- disabled: true
- });
-
- case REQUEST_TO_GET_PROFILE_AND_STOP_PROFILER:
- return renderButton({
- label: "Stopping the recording, and capturing the profile",
- disabled: true
- });
-
- case REQUEST_TO_START_RECORDING:
- case RECORDING:
- return renderButton({
- label: "Stop and grab the recording",
- onClick: this.getProfileAndStopProfiler,
- disabled: this.state.recordingState === REQUEST_TO_START_RECORDING
- });
-
- case OTHER_IS_RECORDING:
- return renderButton({
- label: "Stop and discard the other recording",
- onClick: this.stopProfilerAndDiscardProfile,
- disabled: this.state.recordingState === REQUEST_TO_START_RECORDING,
- additionalMessage: "Another tool is currently recording."
- });
-
- default:
- throw new Error("Unhandled recording state");
- }
- }
-});
-
-function renderButton(props) {
- const { disabled, label, onClick, additionalMessage } = props;
- const nbsp = "\u00A0";
-
- return div(
- { className: "perf" },
- div({ className: "perf-additional-message" }, additionalMessage || nbsp),
- div(
- {},
- button(
- {
- className: "devtools-button perf-button",
- "data-standalone": true,
- disabled,
- onClick
- },
- label
- )
- )
- );
-}
+const { createElement } = require("devtools/client/shared/vendor/react");
/**
* Perform a simple initialization on the panel. Hook up event listeners.
*
* @param perfFront - The Perf actor's front. Used to start and stop recordings.
*/
function gInit(perfFront) {
- const props = { perfFront };
+ const props = {
+ perfFront,
+ receiveProfile: profile => {
+ // Open up a new tab and send a message with the profile.
+ const browser = top.gBrowser;
+ const tab = browser.addTab("https://perf-html.io/from-addon");
+ browser.selectedTab = tab;
+ const mm = tab.linkedBrowser.messageManager;
+ mm.loadFrameScript("chrome://devtools/content/performance/new/frame-script.js",
+ false);
+ mm.sendAsyncMessage("devtools:perf-html-transfer-profile", profile);
+ }
+ };
render(createElement(Perf, props), document.querySelector("#root"));
}
--- a/devtools/client/performance/new/moz.build
+++ b/devtools/client/performance/new/moz.build
@@ -1,9 +1,18 @@
# 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/.
+DIRS += [
+ 'components',
+]
+
DevToolsModules(
'initializer.js',
'panel.js',
)
+
+BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
+
+with Files('**'):
+ BUG_COMPONENT = ('Firefox', 'Developer Tools: Performance Tools (Profiler/Timeline)')
new file mode 100644
--- /dev/null
+++ b/devtools/client/performance/new/test/.eslintrc.js
@@ -0,0 +1,6 @@
+"use strict";
+
+module.exports = {
+ // Extend from the shared list of defined globals for mochitests.
+ "extends": "../../../../.eslintrc.mochitests.js"
+};
new file mode 100644
--- /dev/null
+++ b/devtools/client/performance/new/test/browser.ini
@@ -0,0 +1,10 @@
+[DEFAULT]
+tags = devtools
+subsuite = devtools
+support-files =
+ head.js
+ doc_mount_react.html
+
+[browser_perf-state-01.js]
+[browser_perf-state-02.js]
+[browser_perf-state-03.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/performance/new/test/browser_perf-state-01.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/. */
+"use strict";
+
+/**
+ * Tests the normal workflow of starting and stopping the profiler through the Perf
+ * component.
+ */
+addPerfTest(async ({ document, browserRequire, mountElement, perfFront }) => {
+ const Perf = browserRequire("devtools/client/performance/new/components/Perf");
+ const React = browserRequire("devtools/client/shared/vendor/react");
+ const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+
+ // Inject a function which will allow us to receive the profile.
+ let profile;
+ function receiveProfile(profileIn) {
+ profile = profileIn;
+ }
+
+ const element = React.createElement(Perf, { perfFront, receiveProfile });
+ const perfComponent = ReactDOM.render(element, mountElement);
+
+ is(perfComponent.state.recordingState, "not-yet-known",
+ "The component at first is in an unknown state.");
+
+ await perfFront.flushAsyncQueue();
+ is(perfComponent.state.recordingState, "available-to-record",
+ "After talking to the actor, we're ready to record.");
+
+ document.querySelector("button").click();
+ is(perfComponent.state.recordingState, "request-to-start-recording",
+ "Sent in a request to start recording.");
+
+ await perfFront.flushAsyncQueue();
+ is(perfComponent.state.recordingState, "recording",
+ "The actor has started its recording");
+
+ document.querySelector("button").click();
+ is(perfComponent.state.recordingState, "request-to-get-profile-and-stop-profiler",
+ "We have requested to stop the profiler.");
+
+ await perfFront.flushAsyncQueue();
+ is(perfComponent.state.recordingState, "available-to-record",
+ "The profiler is available to record again.");
+
+ await perfFront.flushAsyncQueue();
+ is(typeof profile, "object", "Got a profile");
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/performance/new/test/browser_perf-state-02.js
@@ -0,0 +1,37 @@
+/* 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";
+
+/**
+ * Tests the perf component for when the profiler is already started.
+ */
+addPerfTest(async ({ document, browserRequire, mountElement, perfFront }) => {
+ const Perf = browserRequire("devtools/client/performance/new/components/Perf");
+ const React = browserRequire("devtools/client/shared/vendor/react");
+ const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+
+ const element = React.createElement(Perf, { perfFront });
+
+ ok(true, "Start the profiler before initiliazing the component, to simulate" +
+ "the profiler being controlled by another tool.");
+
+ perfFront.startProfiler();
+ await perfFront.flushAsyncQueue();
+
+ const perfComponent = ReactDOM.render(element, mountElement);
+ is(perfComponent.state.recordingState, "not-yet-known",
+ "The component at first is in an unknown state.");
+
+ await perfFront.flushAsyncQueue();
+ is(perfComponent.state.recordingState, "other-is-recording",
+ "The profiler is not available to record.");
+
+ document.querySelector("button").click();
+ is(perfComponent.state.recordingState, "request-to-stop-profiler",
+ "We can request to stop the profiler.");
+
+ await perfFront.flushAsyncQueue();
+ is(perfComponent.state.recordingState, "available-to-record",
+ "The profiler is now available to record.");
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/performance/new/test/browser_perf-state-03.js
@@ -0,0 +1,40 @@
+/* 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";
+
+/**
+ * Tests the workflow of what happens when a third party tool interrupts a recording.
+ */
+addPerfTest(async ({ document, browserRequire, mountElement, perfFront }) => {
+ const Perf = browserRequire("devtools/client/performance/new/components/Perf");
+ const React = browserRequire("devtools/client/shared/vendor/react");
+ const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+
+ const element = React.createElement(Perf, { perfFront });
+
+ const perfComponent = ReactDOM.render(element, mountElement);
+ is(perfComponent.state.recordingState, "not-yet-known",
+ "The component at first is in an unknown state.");
+
+ await perfFront.flushAsyncQueue();
+ is(perfComponent.state.recordingState, "available-to-record",
+ "After talking to the actor, we're ready to record.");
+
+ document.querySelector("button").click();
+ is(perfComponent.state.recordingState, "request-to-start-recording",
+ "Sent in a request to start recording.");
+
+ await perfFront.flushAsyncQueue();
+ is(perfComponent.state.recordingState, "recording",
+ "The actor has started its recording");
+
+ ok(true, "Simulate a third party stopping the profiler.");
+ perfFront.stopProfilerAndDiscardProfile();
+ await perfFront.flushAsyncQueue();
+
+ ok(perfComponent.state.recordingUnexpectedlyStopped,
+ "The profiler unexpectedly stopped.");
+ is(perfComponent.state.recordingState, "available-to-record",
+ "However, the profiler is available to record again.");
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/performance/new/test/doc_mount_react.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>Perf Test</title>
+ </head>
+ <body>
+ <div id="root"></div>
+ </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/devtools/client/performance/new/test/head.js
@@ -0,0 +1,140 @@
+/* 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";
+
+/* exported addPerfTest */
+/* globals URL_ROOT */
+
+// Load the shared test helpers into this compartment.
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/framework/test/shared-head.js",
+ this);
+
+const EventEmitter = require("devtools/shared/event-emitter");
+const { Hosts } = require("devtools/client/framework/toolbox-hosts");
+const { DOMHelpers } = Cu.import("resource://devtools/client/shared/DOMHelpers.jsm", {});
+const { BrowserLoader } = Cu.import("resource://devtools/client/shared/browser-loader.js", {});
+
+/**
+ * The perf tests require some common setup. This does that setup, and injects a few
+ * common requirements. It is a thin wrapper around add_task().
+ */
+function addPerfTest(asyncTestFn) {
+ add_task(async () => {
+ waitForExplicitFinish();
+
+ const [host, window, document] = await createHost(URL_ROOT + "doc_mount_react.html");
+ const mountElement = document.querySelector("#root");
+ const perfFront = new MockPerfFront();
+ const { require: browserRequire } = BrowserLoader({
+ baseURI: "res`ource://devtools/client/performance/new/test/",
+ window
+ });
+
+ // Run the test.
+ try {
+ await asyncTestFn({
+ browserRequire,
+ document,
+ mountElement,
+ perfFront
+ });
+ } catch (error) {
+ console.error(error);
+ ok(false, "The test threw an error.");
+ }
+
+ // Clean up.
+ host.destroy();
+ finish();
+ });
+}
+
+/**
+ * The Gecko Profiler is a rather heavy-handed component that uses a lot of resources.
+ * In order to get around that, and have quick component tests we provide a mock of
+ * the performance front. It also has a method called flushAsyncQueue() that will
+ * flush any queued async calls to deterministically run our tests.
+ */
+class MockPerfFront extends EventEmitter {
+ constructor() {
+ super();
+ this._isActive = false;
+ this._asyncQueue = [];
+
+ // Wrap all async methods in a flushable queue, so that tests can control
+ // when the responses come back.
+ this.isActive = this._wrapInAsyncQueue(this.isActive);
+ this.startProfiler = this._wrapInAsyncQueue(this.startProfiler);
+ this.stopProfilerAndDiscardProfile = this._wrapInAsyncQueue(
+ this.stopProfilerAndDiscardProfile);
+ this.getProfileAndStopProfiler = this._wrapInAsyncQueue(
+ this.getProfileAndStopProfiler);
+ }
+
+ /**
+ * Provide a flushable queue mechanism for all async work. The work piles up
+ * and then is evaluated at once when _flushPendingQueue is called.
+ */
+ _wrapInAsyncQueue(fn) {
+ if (typeof fn !== "function") {
+ throw new Error("_wrapInAsyncQueue requires a function");
+ }
+ return (...args) => {
+ return new Promise(resolve => {
+ this._asyncQueue.push(() => {
+ resolve(fn.apply(this, args));
+ });
+ });
+ };
+ }
+
+ flushAsyncQueue() {
+ const pending = this._asyncQueue;
+ this._asyncQueue = [];
+ pending.forEach(fn => fn());
+ return Promise.resolve();
+ }
+
+ isActive() {
+ return this._isActive;
+ }
+
+ startProfiler() {
+ this._isActive = true;
+ // Defer this so it doesn't happen immediately.
+ this.emit("profiler-started");
+ }
+
+ getProfileAndStopProfiler() {
+ this._isActive = false;
+ // Defer this so it doesn't happen immediately.
+ this.emit("profiler-stopped");
+ // Return a fake profile.
+ return {};
+ }
+
+ stopProfilerAndDiscardProfile() {
+ this._isActive = false;
+ // Defer this so it doesn't happen immediately.
+ this.emit("profiler-stopped");
+ }
+}
+
+/**
+ * Create a DevTools host on the bottom of the window with the given URL. This quickly
+ * gives us a document that the test can directly manipulate.
+ */
+async function createHost(url) {
+ const host = new Hosts.bottom(gBrowser.selectedTab); // eslint-disable-line new-cap
+ const iframe = await host.create();
+
+ await new Promise(resolve => {
+ const domHelper = new DOMHelpers(iframe.contentWindow);
+ iframe.setAttribute("src", url);
+ domHelper.onceDOMReady(resolve);
+ });
+
+ return [host, iframe.contentWindow, iframe.contentDocument];
+}