Bug 1408124 - Properly handle profiler state in the recording panel
MozReview-Commit-ID: KdrUebBBvNm
--- a/devtools/client/jar.mn
+++ b/devtools/client/jar.mn
@@ -164,16 +164,17 @@ devtools.jar:
skin/webconsole.css (themes/webconsole.css)
skin/images/webconsole.svg (themes/images/webconsole.svg)
skin/images/breadcrumbs-scrollbutton.png (themes/images/breadcrumbs-scrollbutton.png)
skin/images/breadcrumbs-scrollbutton@2x.png (themes/images/breadcrumbs-scrollbutton@2x.png)
skin/animation.css (themes/animation.css)
skin/animationinspector.css (themes/animationinspector.css)
skin/canvasdebugger.css (themes/canvasdebugger.css)
skin/debugger.css (themes/debugger.css)
+ skin/perf.css (themes/perf.css)
skin/performance.css (themes/performance.css)
skin/memory.css (themes/memory.css)
skin/scratchpad.css (themes/scratchpad.css)
skin/shadereditor.css (themes/shadereditor.css)
skin/storage.css (themes/storage.css)
skin/splitview.css (themes/splitview.css)
skin/styleeditor.css (themes/styleeditor.css)
skin/webaudioeditor.css (themes/webaudioeditor.css)
--- a/devtools/client/performance/new/initializer.js
+++ b/devtools/client/performance/new/initializer.js
@@ -1,25 +1,254 @@
/* 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 { render } = require("devtools/client/shared/vendor/react-dom");
+const { div, button } = DOM;
+
/**
- * Perform a simple initialization on the panel. Hook up event listeners.
- */
-function gInit(perfFront) {
- document.querySelector("#startRecording").addEventListener("click", () => {
- perfFront.startProfiler();
- });
+ * 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);
+ },
- document.querySelector("#stopRecording").addEventListener("click", async () => {
- const profile = await perfFront.stopProfiler();
+ 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
+ )
+ )
+ );
}
+
+/**
+ * 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 };
+ render(createElement(Perf, props), document.querySelector("#root"));
+}
--- a/devtools/client/performance/new/perf.xhtml
+++ b/devtools/client/performance/new/perf.xhtml
@@ -6,26 +6,20 @@
%htmlDTD;
]>
<!-- 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/. -->
<html xmlns="http://www.w3.org/1999/xhtml" dir="">
<head>
+ <link rel="stylesheet" href="chrome://devtools/skin/widgets.css" type="text/css"/>
+ <link rel="stylesheet" href="chrome://devtools/skin/perf.css" type="text/css"/>
+ </head>
+ <body class="theme-body">
+ <div id="root"></div>
<script type="application/javascript" src="initializer.js"></script>
<script type="application/javascript"
src="chrome://devtools/content/shared/theme-switching.js"
defer="true">
</script>
- <link rel="stylesheet" href="chrome://devtools/skin/widgets.css" type="text/css"/>
- <link rel="stylesheet" href="chrome://devtools/skin/memory.css" type="text/css"/>
- <link rel="stylesheet" href="chrome://devtools/skin/components-frame.css" type="text/css"/>
- <link rel="stylesheet" href="chrome://devtools/skin/components-h-split-box.css" type="text/css"/>
- </head>
- <body class="theme-body">
- <div>
- Recording panel
- <button id='startRecording'>Start Recording</button>
- <button id='stopRecording'>Stop Recording</button>
- </div>
</body>
</html>
new file mode 100644
--- /dev/null
+++ b/devtools/client/themes/perf.css
@@ -0,0 +1,23 @@
+/* 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/. */
+
+.perf {
+ width: 100%;
+ height: 100%;
+ position: absolute;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.devtools-button.perf-button {
+ padding: 5px;
+ margin: auto;
+ font-size: 120%;
+}
+
+.perf-additional-message {
+ margin: 10px;
+ margin-top: 65px;
+}
--- a/devtools/server/actors/perf.js
+++ b/devtools/server/actors/perf.js
@@ -2,29 +2,35 @@
* 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 protocol = require("devtools/shared/protocol");
const { ActorClassWithSpec, Actor } = protocol;
const { perfSpec } = require("devtools/shared/specs/perf");
const { Cc, Ci } = require("chrome");
+const Services = require("Services");
loader.lazyGetter(this, "geckoProfiler", () => {
return Cc["@mozilla.org/tools/profiler;1"].getService(Ci.nsIProfiler);
});
exports.PerfActor = ActorClassWithSpec(perfSpec, {
- typeName: "cssProperties",
-
initialize(conn) {
Actor.prototype.initialize.call(this, conn);
+ this._observer = {
+ observe: this.observe.bind(this)
+ };
+ Services.obs.addObserver(this._observer, "profiler-started");
+ Services.obs.addObserver(this._observer, "profiler-stopped");
},
destroy() {
+ Services.obs.removeObserver(this._observer, "profiler-started");
+ Services.obs.removeObserver(this._observer, "profiler-stopped");
Actor.prototype.destroy.call(this);
},
startProfiler() {
const settings = {
entries: 1000000,
interval: 1,
features: ["js", "stackwalk", "threads", "leaf"],
@@ -36,17 +42,33 @@ exports.PerfActor = ActorClassWithSpec(p
settings.interval,
settings.features,
settings.features.length,
settings.threads,
settings.threads.length
);
},
- async stopProfiler() {
+ stopProfilerAndDiscardProfile() {
+ geckoProfiler.StopProfiler();
+ },
+
+ async getProfileAndStopProfiler() {
const profile = await geckoProfiler.getProfileDataAsync();
geckoProfiler.StopProfiler();
if (Object.keys(profile).length === 0) {
throw new Error("Unable to capture a profile.");
}
return profile;
+ },
+
+ isActive() {
+ return geckoProfiler.IsActive();
+ },
+
+ observe(_subject, topic, _data) {
+ switch (topic) {
+ case "profiler-started":
+ case "profiler-stopped":
+ this.emit(topic);
+ }
}
});
--- a/devtools/shared/specs/perf.js
+++ b/devtools/shared/specs/perf.js
@@ -3,22 +3,41 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const { RetVal, generateActorSpec } = require("devtools/shared/protocol");
const perfSpec = generateActorSpec({
typeName: "perf",
+ events: {
+ "profiler-started": {
+ type: "profiler-started"
+ },
+ "profiler-stopped": {
+ type: "profiler-stopped"
+ }
+ },
+
methods: {
startProfiler: {
request: {},
response: {}
},
- stopProfiler: {
+ getProfileAndStopProfiler: {
request: {},
response: RetVal("json")
- }
+ },
+
+ stopProfilerAndDiscardProfile: {
+ request: {},
+ response: {}
+ },
+
+ isActive: {
+ request: {},
+ response: { value: RetVal("boolean") }
+ },
}
});
exports.perfSpec = perfSpec;