--- a/devtools/client/definitions.js
+++ b/devtools/client/definitions.js
@@ -14,16 +14,17 @@ loader.lazyGetter(this, "WebConsolePanel
loader.lazyGetter(this, "DebuggerPanel", () => require("devtools/client/debugger/panel").DebuggerPanel);
loader.lazyGetter(this, "NewDebuggerPanel", () => require("devtools/client/debugger/new/panel").DebuggerPanel);
loader.lazyGetter(this, "StyleEditorPanel", () => require("devtools/client/styleeditor/styleeditor-panel").StyleEditorPanel);
loader.lazyGetter(this, "ShaderEditorPanel", () => require("devtools/client/shadereditor/panel").ShaderEditorPanel);
loader.lazyGetter(this, "CanvasDebuggerPanel", () => require("devtools/client/canvasdebugger/panel").CanvasDebuggerPanel);
loader.lazyGetter(this, "WebAudioEditorPanel", () => require("devtools/client/webaudioeditor/panel").WebAudioEditorPanel);
loader.lazyGetter(this, "MemoryPanel", () => require("devtools/client/memory/panel").MemoryPanel);
loader.lazyGetter(this, "PerformancePanel", () => require("devtools/client/performance/panel").PerformancePanel);
+loader.lazyGetter(this, "NewPerformancePanel", () => require("devtools/client/performance-new/panel").PerformancePanel);
loader.lazyGetter(this, "NetMonitorPanel", () => require("devtools/client/netmonitor/panel").NetMonitorPanel);
loader.lazyGetter(this, "StoragePanel", () => require("devtools/client/storage/panel").StoragePanel);
loader.lazyGetter(this, "ScratchpadPanel", () => require("devtools/client/scratchpad/scratchpad-panel").ScratchpadPanel);
loader.lazyGetter(this, "DomPanel", () => require("devtools/client/dom/dom-panel").DomPanel);
// Other dependencies
loader.lazyRequireGetter(this, "CommandUtils", "devtools/client/shared/developer-toolbar", true);
loader.lazyRequireGetter(this, "CommandState", "devtools/shared/gcli/command-state", true);
@@ -248,38 +249,58 @@ Tools.canvasDebugger = {
},
build: function (iframeWindow, toolbox) {
return new CanvasDebuggerPanel(iframeWindow, toolbox);
}
};
Tools.performance = {
- id: "performance",
- ordinal: 7,
- icon: "chrome://devtools/skin/images/tool-profiler.svg",
- url: "chrome://devtools/content/performance/performance.xul",
- visibilityswitch: "devtools.performance.enabled",
- label: l10n("performance.label"),
- panelLabel: l10n("performance.panelLabel"),
- get tooltip() {
- return l10n("performance.tooltip", "Shift+" +
- functionkey(l10n("performance.commandkey")));
- },
- accesskey: l10n("performance.accesskey"),
- inMenu: true,
+ id: "performance",
+ ordinal: 7,
+ icon: "chrome://devtools/skin/images/tool-profiler.svg",
+ visibilityswitch: "devtools.performance.enabled",
+ label: l10n("performance.label"),
+ panelLabel: l10n("performance.panelLabel"),
+ get tooltip() {
+ return l10n("performance.tooltip", "Shift+" +
+ functionkey(l10n("performance.commandkey")));
+ },
+ accesskey: l10n("performance.accesskey"),
+ inMenu: true,
+};
- isTargetSupported: function (target) {
- return target.hasActor("performance");
- },
+function switchPerformancePanel() {
+ if (Services.prefs.getBoolPref("devtools.performance.new-panel-enabled", false)) {
+ Tools.performance.url = "chrome://devtools/content/performance-new/perf.xhtml";
+ Tools.performance.build = function (frame, target) {
+ return new NewPerformancePanel(frame, target);
+ };
+ Tools.performance.isTargetSupported = function (target) {
+ // Root actors are lazily initialized, so we can't check if the target has
+ // the perf actor yet. Also this function is not async, so we can't initialize
+ // the actor yet.
+ return true;
+ };
+ } else {
+ Tools.performance.url = "chrome://devtools/content/performance/performance.xul";
+ Tools.performance.build = function (frame, target) {
+ return new PerformancePanel(frame, target);
+ };
+ Tools.performance.isTargetSupported = function (target) {
+ return target.hasActor("performance");
+ };
+ }
+}
+switchPerformancePanel();
- build: function (frame, target) {
- return new PerformancePanel(frame, target);
- }
-};
+Services.prefs.addObserver(
+ "devtools.performance.new-panel-enabled",
+ { observe: switchPerformancePanel }
+);
Tools.memory = {
id: "memory",
ordinal: 8,
icon: "chrome://devtools/skin/images/tool-memory.svg",
url: "chrome://devtools/content/memory/memory.xhtml",
visibilityswitch: "devtools.memory.enabled",
label: l10n("memory.label"),
--- a/devtools/client/framework/toolbox-options.js
+++ b/devtools/client/framework/toolbox-options.js
@@ -323,16 +323,21 @@ OptionsPanel.prototype = {
label: L10N.getStr("toolbox.options.enableNewConsole.label"),
id: "devtools-new-webconsole",
parentId: "webconsole-options"
}, {
pref: "devtools.debugger.new-debugger-frontend",
label: L10N.getStr("toolbox.options.enableNewDebugger.label"),
id: "devtools-new-debugger",
parentId: "debugger-options"
+ }, {
+ pref: "devtools.performance.new-panel-enabled",
+ label: "Enable new performance recorder (then re-open DevTools)",
+ id: "devtools-new-performance",
+ parentId: "context-options"
}];
let createPreferenceOption = ({pref, label, id}) => {
let inputLabel = this.panelDoc.createElement("label");
let checkbox = this.panelDoc.createElement("input");
checkbox.setAttribute("type", "checkbox");
if (GetPref(pref)) {
checkbox.setAttribute("checked", "checked");
--- a/devtools/client/framework/toolbox-process-window.js
+++ b/devtools/client/framework/toolbox-process-window.js
@@ -72,16 +72,17 @@ function setPrefDefaults() {
Services.prefs.setBoolPref("devtools.inspector.showAllAnonymousContent", true);
Services.prefs.setBoolPref("browser.dom.window.dump.enabled", true);
Services.prefs.setBoolPref("devtools.command-button-noautohide.enabled", true);
Services.prefs.setBoolPref("devtools.scratchpad.enabled", true);
// Bug 1225160 - Using source maps with browser debugging can lead to a crash
Services.prefs.setBoolPref("devtools.debugger.source-maps-enabled", false);
Services.prefs.setBoolPref("devtools.debugger.new-debugger-frontend", true);
Services.prefs.setBoolPref("devtools.webconsole.new-frontend-enabled", false);
+ Services.prefs.setBoolPref("devtools.preference.new-panel-enabled", false);
}
window.addEventListener("load", function () {
let cmdClose = document.getElementById("toolbox-cmd-close");
cmdClose.addEventListener("command", onCloseCommand);
setPrefDefaults();
connect().catch(e => {
let errorMessageContainer = document.getElementById("error-message-container");
let errorMessage = document.getElementById("error-message");
--- a/devtools/client/jar.mn
+++ b/devtools/client/jar.mn
@@ -58,16 +58,19 @@ devtools.jar:
content/webaudioeditor/views/utils.js (webaudioeditor/views/utils.js)
content/webaudioeditor/views/context.js (webaudioeditor/views/context.js)
content/webaudioeditor/views/inspector.js (webaudioeditor/views/inspector.js)
content/webaudioeditor/views/properties.js (webaudioeditor/views/properties.js)
content/webaudioeditor/views/automation.js (webaudioeditor/views/automation.js)
content/performance/performance.xul (performance/performance.xul)
content/performance/performance-controller.js (performance/performance-controller.js)
content/performance/performance-view.js (performance/performance-view.js)
+ content/performance-new/perf.xhtml (performance-new/perf.xhtml)
+ content/performance-new/frame-script.js (performance-new/frame-script.js)
+ content/performance-new/initializer.js (performance-new/initializer.js)
content/performance/views/overview.js (performance/views/overview.js)
content/performance/views/toolbar.js (performance/views/toolbar.js)
content/performance/views/details.js (performance/views/details.js)
content/performance/views/details-abstract-subview.js (performance/views/details-abstract-subview.js)
content/performance/views/details-waterfall.js (performance/views/details-waterfall.js)
content/performance/views/details-js-call-tree.js (performance/views/details-js-call-tree.js)
content/performance/views/details-js-flamegraph.js (performance/views/details-js-flamegraph.js)
content/performance/views/details-memory-call-tree.js (performance/views/details-memory-call-tree.js)
@@ -150,16 +153,17 @@ devtools.jar:
skin/images/breakpoint.svg (themes/images/breakpoint.svg)
skin/webconsole.css (themes/webconsole.css)
skin/images/webconsole.svg (themes/images/webconsole.svg)
skin/images/breadcrumbs-scrollbutton.svg (themes/images/breadcrumbs-scrollbutton.svg)
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/moz.build
+++ b/devtools/client/moz.build
@@ -15,16 +15,17 @@ DIRS += [
'dom',
'framework',
'inspector',
'jsonview',
'locales',
'memory',
'netmonitor',
'performance',
+ 'performance-new',
'preferences',
'responsive.html',
'scratchpad',
'shadereditor',
'shared',
'sourceeditor',
'storage',
'styleeditor',
new file mode 100644
--- /dev/null
+++ b/devtools/client/performance-new/components/Perf.js
@@ -0,0 +1,356 @@
+/* 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 { PureComponent } = require("devtools/client/shared/vendor/react");
+const { div, button } = require("devtools/client/shared/vendor/react-dom-factories");
+const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+
+/**
+ * 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";
+// Profiling is not available when in private browsing mode.
+const LOCKED_BY_PRIVATE_BROWSING = "locked-by-private-browsing";
+
+class Perf extends PureComponent {
+ static get propTypes() {
+ return {
+ perfFront: PropTypes.object.isRequired,
+ receiveProfile: PropTypes.func.isRequired
+ };
+ }
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ recordingState: NOT_YET_KNOWN,
+ recordingUnexpectedlyStopped: false,
+ // The following is either "null" for unknown, or a boolean value.
+ isSupportedPlatform: null
+ };
+ this.startRecording = this.startRecording.bind(this);
+ this.getProfileAndStopProfiler = this.getProfileAndStopProfiler.bind(this);
+ this.stopProfilerAndDiscardProfile = this.stopProfilerAndDiscardProfile.bind(this);
+ this.handleProfilerStarting = this.handleProfilerStarting.bind(this);
+ this.handleProfilerStopping = this.handleProfilerStopping.bind(this);
+ this.handlePrivateBrowsingStarting = this.handlePrivateBrowsingStarting.bind(this);
+ this.handlePrivateBrowsingEnding = this.handlePrivateBrowsingEnding.bind(this);
+ }
+
+ componentDidMount() {
+ const { perfFront } = this.props;
+
+ // Ask for the initial state of the profiler.
+ Promise.all([
+ perfFront.isActive(),
+ perfFront.isSupportedPlatform(),
+ perfFront.isLockedForPrivateBrowsing(),
+ ]).then((results) => {
+ const [
+ isActive,
+ isSupportedPlatform,
+ isLockedForPrivateBrowsing
+ ] = results;
+
+ let recordingState = this.state.recordingState;
+ // It's theoretically possible we got an event that already let us know about
+ // the current state of the profiler.
+ if (recordingState === NOT_YET_KNOWN && isSupportedPlatform) {
+ if (isLockedForPrivateBrowsing) {
+ recordingState = LOCKED_BY_PRIVATE_BROWSING;
+ } else {
+ recordingState = isActive
+ ? OTHER_IS_RECORDING
+ : AVAILABLE_TO_RECORD;
+ }
+ }
+ this.setState({ isSupportedPlatform, recordingState });
+ });
+
+ // 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);
+ this.props.perfFront.on("profile-locked-by-private-browsing",
+ this.handlePrivateBrowsingStarting);
+ this.props.perfFront.on("profile-unlocked-from-private-browsing",
+ this.handlePrivateBrowsingEnding);
+ }
+
+ componentWillUnmount() {
+ switch (this.state.recordingState) {
+ case NOT_YET_KNOWN:
+ case AVAILABLE_TO_RECORD:
+ case REQUEST_TO_STOP_PROFILER:
+ case REQUEST_TO_GET_PROFILE_AND_STOP_PROFILER:
+ case LOCKED_BY_PRIVATE_BROWSING:
+ case OTHER_IS_RECORDING:
+ // Do nothing for these states.
+ break;
+
+ case RECORDING:
+ case REQUEST_TO_START_RECORDING:
+ this.props.perfFront.stopProfilerAndDiscardProfile();
+ break;
+
+ default:
+ throw new Error("Unhandled recording state.");
+ }
+ }
+
+ getRecordingStateForTesting() {
+ return this.state.recordingState;
+ }
+
+ 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 requested to stop the profiler, but someone else already 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 LOCKED_BY_PRIVATE_BROWSING:
+ 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 (fallthrough).
+ case LOCKED_BY_PRIVATE_BROWSING:
+ // The profiler is already locked, so we know about this already.
+ 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");
+ }
+ }
+
+ handlePrivateBrowsingStarting() {
+ switch (this.state.recordingState) {
+ case REQUEST_TO_GET_PROFILE_AND_STOP_PROFILER:
+ // This one is a tricky case. Go ahead and act like nothing went wrong, maybe
+ // it will resolve correctly? (fallthrough)
+ case REQUEST_TO_STOP_PROFILER:
+ case AVAILABLE_TO_RECORD:
+ case OTHER_IS_RECORDING:
+ case NOT_YET_KNOWN:
+ this.setState({
+ recordingState: LOCKED_BY_PRIVATE_BROWSING,
+ recordingUnexpectedlyStopped: false
+ });
+ break;
+
+ case REQUEST_TO_START_RECORDING:
+ case RECORDING:
+ this.setState({
+ recordingState: LOCKED_BY_PRIVATE_BROWSING,
+ recordingUnexpectedlyStopped: true
+ });
+ break;
+
+ case LOCKED_BY_PRIVATE_BROWSING:
+ // Do nothing
+ break;
+
+ default:
+ throw new Error("Unhandled recording state");
+ }
+ }
+
+ handlePrivateBrowsingEnding() {
+ // No matter the state, go ahead and set this as ready to record. This should
+ // be the only logical state to go into.
+ this.setState({
+ recordingState: AVAILABLE_TO_RECORD,
+ recordingUnexpectedlyStopped: false
+ });
+ }
+
+ startRecording() {
+ this.setState({
+ recordingState: REQUEST_TO_START_RECORDING,
+ // Reset this error state since it's no longer valid.
+ recordingUnexpectedlyStopped: false,
+ });
+ this.props.perfFront.startProfiler();
+ }
+
+ async getProfileAndStopProfiler() {
+ 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() {
+ this.setState({ recordingState: REQUEST_TO_STOP_PROFILER });
+ this.props.perfFront.stopProfilerAndDiscardProfile();
+ }
+
+ render() {
+ const { recordingState, isSupportedPlatform } = this.state;
+
+ // Handle the cases of platform support.
+ switch (isSupportedPlatform) {
+ case null:
+ // We don't know yet if this is a supported platform, wait for a response.
+ return null;
+ case false:
+ return renderButton({
+ label: "Start recording",
+ disabled: true,
+ additionalMessage: "Your platform is not supported. The Gecko Profiler only " +
+ "supports Tier-1 platforms."
+ });
+ case true:
+ // Continue on and render the panel.
+ break;
+ }
+
+ // TODO - L10N all of the messages. Bug 1418056
+ switch (recordingState) {
+ case NOT_YET_KNOWN:
+ return null;
+
+ case AVAILABLE_TO_RECORD:
+ return renderButton({
+ onClick: this.startRecording,
+ label: "Start recording",
+ additionalMessage: this.state.recordingUnexpectedlyStopped
+ ? div(null, "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,
+ additionalMessage: "Another tool is currently recording."
+ });
+
+ case LOCKED_BY_PRIVATE_BROWSING:
+ return renderButton({
+ label: "Start recording",
+ disabled: true,
+ additionalMessage: `The profiler is disabled when Private Browsing is enabled.
+ Close all Private Windows to re-enable the profiler`
+ });
+
+ default:
+ throw new Error("Unhandled recording state");
+ }
+ }
+}
+
+module.exports = Perf;
+
+function renderButton(props) {
+ const { disabled, label, onClick, additionalMessage } = props;
+ const nbsp = "\u00A0";
+
+ return div(
+ { className: "perf" },
+ div({ className: "perf-additional-message" }, additionalMessage || nbsp),
+ div(
+ null,
+ button(
+ {
+ className: "devtools-button perf-button",
+ "data-standalone": true,
+ 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',
+)
new file mode 100644
--- /dev/null
+++ b/devtools/client/performance-new/frame-script.js
@@ -0,0 +1,106 @@
+/* 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";
+/* global addMessageListener, addEventListener, content */
+
+/**
+ * This frame script injects itself into perf-html.io and injects the profile
+ * into the page. It is mostly taken from the Gecko Profiler Addon implementation.
+ */
+
+const TRANSFER_EVENT = "devtools:perf-html-transfer-profile";
+
+let gProfile = null;
+
+addMessageListener(TRANSFER_EVENT, e => {
+ gProfile = e.data;
+ // Eagerly try and see if the framescript was evaluated after perf loaded its scripts.
+ connectToPage();
+ // If not try again at DOMContentLoaded which should be called after the script
+ // tag was synchronously loaded in.
+ addEventListener("DOMContentLoaded", connectToPage);
+});
+
+function connectToPage() {
+ const unsafeWindow = content.wrappedJSObject;
+ if (unsafeWindow.connectToGeckoProfiler) {
+ unsafeWindow.connectToGeckoProfiler(makeAccessibleToPage({
+ getProfile: () => Promise.resolve(gProfile),
+ getSymbolTable: (debugName, breakpadId) => getSymbolTable(debugName, breakpadId),
+ }, unsafeWindow));
+ }
+}
+
+/**
+ * For now, do not try to symbolicate. Reject any attempt.
+ */
+function getSymbolTable(debugName, breakpadId) {
+ // Errors will not properly clone into the content page as they bring privileged
+ // stacks information into the page. In this case provide a mock object to maintain
+ // the Error type object shape.
+ const error = {
+ message: `The DevTools' "perf" actor does not support symbolication.`
+ };
+ return Promise.reject(error);
+}
+
+// The following functions handle the security of cloning the object into the page.
+// The code was taken from the original Gecko Profiler Add-on to maintain
+// compatibility with the existing profile importing mechanism:
+// See: https://github.com/devtools-html/Gecko-Profiler-Addon/blob/78138190b42565f54ce4022a5b28583406489ed2/data/tab-framescript.js
+
+/**
+ * Create a promise that can be used in the page.
+ */
+function createPromiseInPage(fun, contentGlobal) {
+ function funThatClonesObjects(resolve, reject) {
+ return fun(result => resolve(Components.utils.cloneInto(result, contentGlobal)),
+ error => reject(Components.utils.cloneInto(error, contentGlobal)));
+ }
+ return new contentGlobal.Promise(Components.utils.exportFunction(funThatClonesObjects,
+ contentGlobal));
+}
+
+/**
+ * Returns a function that calls the original function and tries to make the
+ * return value available to the page.
+ */
+function wrapFunction(fun, contentGlobal) {
+ return function () {
+ let result = fun.apply(this, arguments);
+ if (typeof result === "object") {
+ if (("then" in result) && (typeof result.then === "function")) {
+ // fun returned a promise.
+ return createPromiseInPage((resolve, reject) =>
+ result.then(resolve, reject), contentGlobal);
+ }
+ return Components.utils.cloneInto(result, contentGlobal);
+ }
+ return result;
+ };
+}
+
+/**
+ * Pass a simple object containing values that are objects or functions.
+ * The objects or functions are wrapped in such a way that they can be
+ * consumed by the page.
+ */
+function makeAccessibleToPage(obj, contentGlobal) {
+ let result = Components.utils.createObjectIn(contentGlobal);
+ for (let field in obj) {
+ switch (typeof obj[field]) {
+ case "function":
+ Components.utils.exportFunction(
+ wrapFunction(obj[field], contentGlobal), result, { defineAs: field });
+ break;
+ case "object":
+ Components.utils.cloneInto(obj[field], result, { defineAs: field });
+ break;
+ default:
+ result[field] = obj[field];
+ break;
+ }
+ }
+ return result;
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/performance-new/initializer.js
@@ -0,0 +1,44 @@
+/* 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, gDestroy */
+
+const BrowserLoaderModule = {};
+Components.utils.import("resource://devtools/client/shared/browser-loader.js", BrowserLoaderModule);
+const { require } = BrowserLoaderModule.BrowserLoader({
+ baseURI: "resource://devtools/client/memory/",
+ window
+});
+const Perf = require("devtools/client/performance-new/components/Perf");
+const { render, unmountComponentAtNode } = require("devtools/client/shared/vendor/react-dom");
+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,
+ 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"));
+}
+
+function gDestroy() {
+ unmountComponentAtNode(document.querySelector("#root"));
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/performance-new/moz.build
@@ -0,0 +1,17 @@
+# 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(
+ 'panel.js',
+)
+
+MOCHITEST_CHROME_MANIFESTS += ['test/chrome/chrome.ini']
+
+with Files('**'):
+ BUG_COMPONENT = ('Firefox', 'Developer Tools: Performance Tools (Profiler/Timeline)')
new file mode 100644
--- /dev/null
+++ b/devtools/client/performance-new/panel.js
@@ -0,0 +1,59 @@
+/* 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 { PerfFront } = require("devtools/shared/fronts/perf");
+
+loader.lazyRequireGetter(this, "EventEmitter",
+ "devtools/shared/old-event-emitter");
+
+class PerformancePanel {
+ constructor(iframeWindow, toolbox) {
+ this.panelWin = iframeWindow;
+ this.toolbox = toolbox;
+
+ EventEmitter.decorate(this);
+ }
+
+ /**
+ * Open is effectively an asynchronous constructor.
+ * @return {Promise} Resolves when the Perf tool completes opening.
+ */
+ open() {
+ if (!this._opening) {
+ this._opening = this._doOpen();
+ }
+ return this._opening;
+ }
+
+ async _doOpen() {
+ this.panelWin.gToolbox = this.toolbox;
+ this.panelWin.gTarget = this.target;
+
+ const rootForm = await this.target.root;
+ const perfFront = new PerfFront(this.target.client, rootForm);
+
+ this.isReady = true;
+ this.emit("ready");
+ this.panelWin.gInit(perfFront);
+ return this;
+ }
+
+ // DevToolPanel API:
+
+ get target() {
+ return this.toolbox.target;
+ }
+
+ async destroy() {
+ // Make sure this panel is not already destroyed.
+ if (this._destroyed) {
+ return;
+ }
+ this.panelWin.gDestroy();
+ this.emit("destroyed");
+ this._destroyed = true;
+ }
+}
+exports.PerformancePanel = PerformancePanel;
new file mode 100644
--- /dev/null
+++ b/devtools/client/performance-new/perf.xhtml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html [
+ <!ENTITY % htmlDTD
+ PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+ "DTD/xhtml1-strict.dtd">
+ %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>
+ </body>
+</html>
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/chrome/chrome.ini
@@ -0,0 +1,8 @@
+[DEFAULT]
+support-files =
+ head.js
+
+[test_perf-state-01.html]
+[test_perf-state-02.html]
+[test_perf-state-03.html]
+[test_perf-state-04.html]
new file mode 100644
--- /dev/null
+++ b/devtools/client/performance-new/test/chrome/head.js
@@ -0,0 +1,133 @@
+/* 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, MockPerfFront */
+/* globals URL_ROOT */
+
+const { BrowserLoader } = Components.utils.import("resource://devtools/client/shared/browser-loader.js", {});
+var { require } = BrowserLoader({
+ baseURI: "resource://devtools/client/performance-new/",
+ window
+});
+
+const EventEmitter = require("devtools/shared/event-emitter");
+const { perfDescription } = require("devtools/shared/specs/perf");
+const DevToolsUtils = require("devtools/shared/DevToolsUtils");
+const flags = require("devtools/shared/flags");
+
+flags.testing = true;
+let EXPECTED_DTU_ASSERT_FAILURE_COUNT = 0;
+SimpleTest.registerCleanupFunction(function () {
+ if (DevToolsUtils.assertionFailureCount !== EXPECTED_DTU_ASSERT_FAILURE_COUNT) {
+ ok(false, "Should have had the expected number of DevToolsUtils.assert() failures." +
+ "Expected " + EXPECTED_DTU_ASSERT_FAILURE_COUNT +
+ ", got " + DevToolsUtils.assertionFailureCount);
+ }
+});
+
+/**
+ * Handle test setup and teardown while catching errors.
+ */
+function addPerfTest(asyncTest) {
+ window.onload = async () => {
+ try {
+ await asyncTest();
+ } catch (e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.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 = [];
+
+ // Tests can update these two values directly as needed.
+ this.mockIsSupported = true;
+ this.mockIsLocked = false;
+
+ // 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());
+ // Ensure this is async.
+ return new Promise(resolve => setTimeout(resolve, 0));
+ }
+
+ startProfiler() {
+ this._isActive = true;
+ this.emit("profiler-started");
+ }
+
+ getProfileAndStopProfiler() {
+ this._isActive = false;
+ this.emit("profiler-stopped");
+ // Return a fake profile.
+ return {};
+ }
+
+ stopProfilerAndDiscardProfile() {
+ this._isActive = false;
+ this.emit("profiler-stopped");
+ }
+
+ isActive() {
+ return this._isActive;
+ }
+
+ isSupportedPlatform() {
+ return this.mockIsSupported;
+ }
+
+ isLockedForPrivateBrowsing() {
+ return this.mockIsLocked;
+ }
+}
+
+// Do a quick validation to make sure that our Mock has the same methods as a spec.
+const mockKeys = Object.getOwnPropertyNames(MockPerfFront.prototype);
+Object.getOwnPropertyNames(perfDescription.methods).forEach(methodName => {
+ if (!mockKeys.includes(methodName)) {
+ throw new Error(`The MockPerfFront is missing the method "${methodName}" from the ` +
+ "actor's spec. It should be added to the mock.");
+ }
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/performance-new/test/chrome/test_perf-state-01.html
@@ -0,0 +1,70 @@
+<!DOCTYPE HTML>
+<html>
+<!-- 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/. -->
+<head>
+ <meta charset="utf-8">
+ <title>Perf component test</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+ <div id="container"></div>
+
+ <pre id="test">
+ <script src="head.js" type="application/javascript"></script>
+ <script type="application/javascript">
+ "use strict";
+
+ /**
+ * Test the normal workflow of starting and stopping the profiler through the
+ * Perf component.
+ */
+ addPerfTest(async () => {
+ const Perf = require("devtools/client/performance-new/components/Perf");
+ const React = require("devtools/client/shared/vendor/react");
+ const ReactDOM = require("devtools/client/shared/vendor/react-dom");
+ const perfFront = new MockPerfFront();
+ const container = document.querySelector("#container");
+
+ // 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, container);
+ 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.");
+
+ const button = container.querySelector("button");
+ ok(button, "Selected the button to click.");
+ 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");
+
+ 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");
+ });
+ </script>
+ </pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/devtools/client/performance-new/test/chrome/test_perf-state-02.html
@@ -0,0 +1,59 @@
+<!DOCTYPE HTML>
+<html>
+<!-- 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/. -->
+<head>
+ <meta charset="utf-8">
+ <title>Perf component test</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+ <div id="container"></div>
+
+ <pre id="test">
+ <script src="head.js" type="application/javascript"></script>
+ <script type="application/javascript">
+ "use strict";
+
+ /**
+ * Test the perf component when the profiler is already started.
+ */
+ addPerfTest(async () => {
+ const Perf = require("devtools/client/performance-new/components/Perf");
+ const React = require("devtools/client/shared/vendor/react");
+ const ReactDOM = require("devtools/client/shared/vendor/react-dom");
+ const perfFront = new MockPerfFront();
+ const container = document.querySelector("#container");
+
+ ok(true, "Start the profiler before initiliazing the component, to simulate" +
+ "the profiler being controlled by another tool.");
+
+ perfFront.startProfiler();
+ await perfFront.flushAsyncQueue();
+
+ const receiveProfile = () => {};
+ const element = React.createElement(Perf, { perfFront, receiveProfile });
+ const perfComponent = ReactDOM.render(element, container);
+ 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.");
+
+ const button = container.querySelector("button");
+ ok(button, "Selected a button on the component");
+ 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.");
+ });
+ </script>
+ </pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/devtools/client/performance-new/test/chrome/test_perf-state-03.html
@@ -0,0 +1,61 @@
+<!DOCTYPE HTML>
+<html>
+<!-- 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/. -->
+<head>
+ <meta charset="utf-8">
+ <title>Perf component test</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+ <div id="container"></div>
+
+ <pre id="test">
+ <script src="head.js" type="application/javascript"></script>
+ <script type="application/javascript">
+ "use strict";
+
+ /**
+ * Test the perf component for when the profiler is already started.
+ */
+ addPerfTest(async () => {
+ const Perf = require("devtools/client/performance-new/components/Perf");
+ const React = require("devtools/client/shared/vendor/react");
+ const ReactDOM = require("devtools/client/shared/vendor/react-dom");
+ const perfFront = new MockPerfFront();
+ const container = document.querySelector("#container");
+
+ const receiveProfile = () => {};
+ const element = React.createElement(Perf, { perfFront, receiveProfile });
+ const perfComponent = ReactDOM.render(element, container);
+
+ 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.");
+ });
+ </script>
+ </pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/devtools/client/performance-new/test/chrome/test_perf-state-04.html
@@ -0,0 +1,64 @@
+<!DOCTYPE HTML>
+<html>
+<!-- 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/. -->
+<head>
+ <meta charset="utf-8">
+ <title>Perf component test</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+ <div id="container"></div>
+
+ <pre id="test">
+ <script src="head.js" type="application/javascript"></script>
+ <script type="application/javascript">
+ "use strict";
+
+ /**
+ * Test that the profiler gets disabled during private browsing.
+ */
+ addPerfTest(async () => {
+ const Perf = require("devtools/client/performance-new/components/Perf");
+ const React = require("devtools/client/shared/vendor/react");
+ const ReactDOM = require("devtools/client/shared/vendor/react-dom");
+ const perfFront = new MockPerfFront();
+ const container = document.querySelector("#container");
+
+ perfFront.mockIsLocked = true;
+
+ const receiveProfile = () => {};
+ const element = React.createElement(Perf, { perfFront, receiveProfile });
+ const perfComponent = ReactDOM.render(element, container);
+
+ is(perfComponent.state.recordingState, "not-yet-known",
+ "The component at first is in an unknown state.");
+
+ await perfFront.flushAsyncQueue();
+ is(perfComponent.state.recordingState, "locked-by-private-browsing",
+ "After talking to the actor, it's locked for private browsing.");
+
+ perfFront.mockIsLocked = false;
+ perfFront.emit("profile-unlocked-from-private-browsing");
+
+ await perfFront.flushAsyncQueue();
+ is(perfComponent.state.recordingState, "available-to-record",
+ "After the profiler is unlocked, it's available to record.");
+
+ document.querySelector("button").click();
+ await perfFront.flushAsyncQueue();
+ is(perfComponent.state.recordingState, "recording",
+ "The actor has started its recording");
+
+ perfFront.mockIsLocked = true;
+ perfFront.emit("profile-locked-by-private-browsing");
+ await perfFront.flushAsyncQueue();
+ is(perfComponent.state.recordingState, "locked-by-private-browsing",
+ "The recording stops when going into private browsing mode.");
+ });
+ </script>
+ </pre>
+</body>
+</html>
--- a/devtools/client/preferences/devtools.js
+++ b/devtools/client/preferences/devtools.js
@@ -300,16 +300,19 @@ pref("devtools.webconsole.timestampMessa
pref("devtools.webconsole.autoMultiline", true);
// Enable the new webconsole frontend
pref("devtools.webconsole.new-frontend-enabled", true);
// Enable the webconsole sidebar toggle
pref("devtools.webconsole.sidebarToggle", false);
+// Disable the new performance recording panel by default
+pref("devtools.performance.new-panel-enabled", false);
+
// Enable client-side mapping service for source maps
pref("devtools.source-map.client-service.enabled", true);
// The number of lines that are displayed in the web console.
pref("devtools.hud.loglimit", 10000);
// The number of lines that are displayed in the old web console for the Net,
// CSS, JS and Web Developer categories. These defaults should be kept in sync
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/moz.build
+++ b/devtools/server/actors/moz.build
@@ -37,16 +37,17 @@ DevToolsModules(
'heap-snapshot-file.js',
'highlighters.css',
'highlighters.js',
'inspector.js',
'layout.js',
'memory.js',
'monitor.js',
'object.js',
+ 'perf.js',
'performance-recording.js',
'performance.js',
'preference.js',
'pretty-print-worker.js',
'process.js',
'promises.js',
'reflow.js',
'root.js',
new file mode 100644
--- /dev/null
+++ b/devtools/server/actors/perf.js
@@ -0,0 +1,153 @@
+/* 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 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);
+});
+
+loader.lazyImporter(this, "PrivateBrowsingUtils",
+ "resource://gre/modules/PrivateBrowsingUtils.jsm");
+
+// Some platforms are built without the Gecko Profiler.
+const IS_SUPPORTED_PLATFORM = "nsIProfiler" in Ci;
+
+/**
+ * The PerfActor wraps the Gecko Profiler interface
+ */
+exports.PerfActor = ActorClassWithSpec(perfSpec, {
+ initialize(conn) {
+ Actor.prototype.initialize.call(this, conn);
+
+ // Only setup the observers on a supported platform.
+ if (IS_SUPPORTED_PLATFORM) {
+ this._observer = {
+ observe: this._observe.bind(this)
+ };
+ Services.obs.addObserver(this._observer, "profiler-started");
+ Services.obs.addObserver(this._observer, "profiler-stopped");
+ Services.obs.addObserver(this._observer, "chrome-document-global-created");
+ Services.obs.addObserver(this._observer, "last-pb-context-exited");
+ }
+ },
+
+ destroy() {
+ if (!IS_SUPPORTED_PLATFORM) {
+ return;
+ }
+ Services.obs.removeObserver(this._observer, "profiler-started");
+ Services.obs.removeObserver(this._observer, "profiler-stopped");
+ Services.obs.removeObserver(this._observer, "chrome-document-global-created");
+ Services.obs.removeObserver(this._observer, "last-pb-context-exited");
+ Actor.prototype.destroy.call(this);
+ },
+
+ startProfiler() {
+ if (!IS_SUPPORTED_PLATFORM) {
+ return false;
+ }
+
+ // For a quick implementation, decide on some default values. These may need
+ // to be tweaked or made configurable as needed.
+ const settings = {
+ entries: 1000000,
+ interval: 1,
+ features: ["js", "stackwalk", "threads", "leaf"],
+ threads: ["GeckoMain", "Compositor"]
+ };
+
+ try {
+ // This can throw an error if the profiler is in the wrong state.
+ geckoProfiler.StartProfiler(
+ settings.entries,
+ settings.interval,
+ settings.features,
+ settings.features.length,
+ settings.threads,
+ settings.threads.length
+ );
+ } catch (e) {
+ // In case any errors get triggered, bailout with a false.
+ return false;
+ }
+
+ return true;
+ },
+
+ stopProfilerAndDiscardProfile() {
+ if (!IS_SUPPORTED_PLATFORM) {
+ return;
+ }
+ geckoProfiler.StopProfiler();
+ },
+
+ async getProfileAndStopProfiler() {
+ if (!IS_SUPPORTED_PLATFORM) {
+ return null;
+ }
+ let profile;
+ try {
+ // Attempt to pull out the data.
+ profile = await geckoProfiler.getProfileDataAsync();
+
+ // Stop and discard the buffers.
+ geckoProfiler.StopProfiler();
+ } catch (e) {
+ // If there was any kind of error, bailout with no profile.
+ return null;
+ }
+
+ // Gecko Profiler errors can return an empty object, return null for this case
+ // as well.
+ if (Object.keys(profile).length === 0) {
+ return null;
+ }
+ return profile;
+ },
+
+ isActive() {
+ if (!IS_SUPPORTED_PLATFORM) {
+ return false;
+ }
+ return geckoProfiler.IsActive();
+ },
+
+ isSupportedPlatform() {
+ return IS_SUPPORTED_PLATFORM;
+ },
+
+ isLockedForPrivateBrowsing() {
+ if (!IS_SUPPORTED_PLATFORM) {
+ return false;
+ }
+ return !geckoProfiler.CanProfile();
+ },
+
+ /**
+ * Watch for events that happen within the browser. These can affect the current
+ * availability and state of the Gecko Profiler.
+ */
+ _observe(subject, topic, _data) {
+ switch (topic) {
+ case "chrome-document-global-created":
+ if (PrivateBrowsingUtils.isWindowPrivate(subject)) {
+ this.emit("profile-locked-by-private-browsing");
+ }
+ break;
+ case "last-pb-context-exited":
+ this.emit("profile-unlocked-from-private-browsing");
+ break;
+ case "profiler-started":
+ case "profiler-stopped":
+ this.emit(topic);
+ break;
+ }
+ }
+});
--- a/devtools/server/main.js
+++ b/devtools/server/main.js
@@ -438,16 +438,25 @@ var DebuggerServer = {
constructor: "DeviceActor",
type: { global: true }
});
this.registerModule("devtools/server/actors/heap-snapshot-file", {
prefix: "heapSnapshotFile",
constructor: "HeapSnapshotFileActor",
type: { global: true }
});
+ // Always register this as a global module, even while there is a pref turning
+ // on and off the other performance actor. This actor shouldn't conflict with
+ // the other one. These are also lazily loaded so there shouldn't be a performance
+ // impact.
+ this.registerModule("devtools/server/actors/perf", {
+ prefix: "perf",
+ constructor: "PerfActor",
+ type: { global: true }
+ });
},
/**
* Install tab actors.
*/
_addTabActors() {
this.registerModule("devtools/server/actors/webconsole", {
prefix: "console",
@@ -529,17 +538,18 @@ var DebuggerServer = {
constructor: "MonitorActor",
type: { tab: true }
});
this.registerModule("devtools/server/actors/timeline", {
prefix: "timeline",
constructor: "TimelineActor",
type: { tab: true }
});
- if ("nsIProfiler" in Ci) {
+ if ("nsIProfiler" in Ci &&
+ !Services.prefs.getBoolPref("devtools.performance.new-panel-enabled", false)) {
this.registerModule("devtools/server/actors/performance", {
prefix: "performance",
constructor: "PerformanceActor",
type: { tab: true }
});
}
this.registerModule("devtools/server/actors/animation", {
prefix: "animations",
--- a/devtools/server/tests/browser/browser.ini
+++ b/devtools/server/tests/browser/browser.ini
@@ -67,16 +67,19 @@ skip-if = e10s # Bug 1183605 - devtools/
[browser_markers-docloading-03.js]
[browser_markers-gc.js]
[browser_markers-minor-gc.js]
[browser_markers-parse-html.js]
[browser_markers-styles.js]
[browser_markers-timestamp.js]
[browser_navigateEvents.js]
skip-if = e10s # Bug 1183605 - devtools/server/tests/browser/ tests are still disabled in E10S
+[browser_perf-01.js]
+[browser_perf-02.js]
+[browser_perf-03.js]
[browser_perf-allocation-data.js]
skip-if = e10s # Bug 1183605 - devtools/server/tests/browser/ tests are still disabled in E10S
[browser_perf-profiler-01.js]
[browser_perf-profiler-02.js]
skip-if = true # Needs to be updated for async actor destruction
[browser_perf-profiler-03.js]
skip-if = true # Needs to be updated for async actor destruction
[browser_perf-realtime-markers.js]
new file mode 100644
--- /dev/null
+++ b/devtools/server/tests/browser/browser_perf-01.js
@@ -0,0 +1,47 @@
+/* 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";
+
+/**
+ * Run through a series of basic recording actions for the perf actor.
+ */
+add_task(async function () {
+ const {front, client} = await initPerfFront();
+
+ // Assert the initial state.
+ is(await front.isSupportedPlatform(), true,
+ "This test only runs on supported platforms.");
+ is(await front.isLockedForPrivateBrowsing(), false,
+ "The browser is not in private browsing mode.");
+ is(await front.isActive(), false,
+ "The profiler is not active yet.");
+
+ // Start the profiler.
+ const profilerStarted = once(front, "profiler-started");
+ await front.startProfiler();
+ await profilerStarted;
+ is(await front.isActive(), true, "The profiler was started.");
+
+ // Stop the profiler and assert the results.
+ const profilerStopped1 = once(front, "profiler-stopped");
+ const profile = await front.getProfileAndStopProfiler();
+ await profilerStopped1;
+ is(await front.isActive(), false, "The profiler was stopped.");
+ ok("threads" in profile, "The actor was used to record a profile.");
+
+ // Restart the profiler.
+ await front.startProfiler();
+ is(await front.isActive(), true, "The profiler was re-started.");
+
+ // Stop and discard.
+ const profilerStopped2 = once(front, "profiler-stopped");
+ await front.stopProfilerAndDiscardProfile();
+ await profilerStopped2;
+ is(await front.isActive(), false,
+ "The profiler was stopped and the profile discarded.");
+
+ // Clean up.
+ await front.destroy();
+ await client.close();
+});
new file mode 100644
--- /dev/null
+++ b/devtools/server/tests/browser/browser_perf-02.js
@@ -0,0 +1,30 @@
+/* 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";
+
+/**
+ * Test what happens when other tools control the profiler.
+ */
+add_task(async function () {
+ const {front, client} = await initPerfFront();
+
+ // Simulate other tools by getting an independent handle on the Gecko Profiler.
+ const geckoProfiler = Cc["@mozilla.org/tools/profiler;1"].getService(Ci.nsIProfiler);
+
+ is(await front.isActive(), false, "The profiler hasn't been started yet.");
+
+ // Start the profiler.
+ await front.startProfiler();
+ is(await front.isActive(), true, "The profiler was started.");
+
+ // Stop the profiler manually through the Gecko Profiler interface.
+ const profilerStopped = once(front, "profiler-stopped");
+ geckoProfiler.StopProfiler();
+ await profilerStopped;
+ is(await front.isActive(), false, "The profiler was stopped by another tool.");
+
+ // Clean up.
+ await front.destroy();
+ await client.close();
+});
new file mode 100644
--- /dev/null
+++ b/devtools/server/tests/browser/browser_perf-03.js
@@ -0,0 +1,33 @@
+/* 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";
+
+/**
+ * Test that the profiler emits events when private browsing windows are opened
+ * and closed.
+ */
+add_task(async function () {
+ const {front, client} = await initPerfFront();
+
+ is(await front.isLockedForPrivateBrowsing(), false,
+ "The profiler is not locked for private browsing.");
+
+ // Open up a new private browser window, and assert the correct events are fired.
+ const profilerLocked = once(front, "profile-locked-by-private-browsing");
+ const privateWindow = await BrowserTestUtils.openNewBrowserWindow({private: true});
+ await profilerLocked;
+ is(await front.isLockedForPrivateBrowsing(), true,
+ "The profiler is now locked because of private browsing.");
+
+ // Close the private browser window, and assert the correct events are fired.
+ const profilerUnlocked = once(front, "profile-unlocked-from-private-browsing");
+ await BrowserTestUtils.closeWindow(privateWindow);
+ await profilerUnlocked;
+ is(await front.isLockedForPrivateBrowsing(), false,
+ "The profiler is available again after closing the private browsing window.");
+
+ // Clean up.
+ await front.destroy();
+ await client.close();
+});
--- a/devtools/server/tests/browser/head.js
+++ b/devtools/server/tests/browser/head.js
@@ -103,16 +103,51 @@ function initDebuggerServer() {
DebuggerServer.destroy();
} catch (e) {
info(`DebuggerServer destroy error: ${e}\n${e.stack}`);
}
DebuggerServer.init();
DebuggerServer.registerAllActors();
}
+async function initPerfFront() {
+ const {PerfFront} = require("devtools/shared/fronts/perf");
+
+ initDebuggerServer();
+ let client = new DebuggerClient(DebuggerServer.connectPipe());
+ await waitUntilClientConnected(client);
+ const rootForm = await getRootForm(client);
+ const front = PerfFront(client, rootForm);
+ return {front, client};
+}
+
+/**
+ * Gets the RootActor form from a DebuggerClient.
+ * @param {DebuggerClient} client
+ * @return {RootActor} Resolves when connected.
+ */
+function getRootForm(client) {
+ return new Promise(resolve => {
+ client.listTabs(rootForm => {
+ resolve(rootForm);
+ });
+ });
+}
+
+/**
+ * Wait until a DebuggerClient is connected.
+ * @param {DebuggerClient} client
+ * @return {Promise} Resolves when connected.
+ */
+function waitUntilClientConnected(client) {
+ return new Promise(resolve => {
+ client.addOneTimeListener("connected", resolve);
+ });
+}
+
/**
* Connect a debugger client.
* @param {DebuggerClient}
* @return {Promise} Resolves to the selected tabActor form when the client is
* connected.
*/
function connectDebuggerClient(client) {
return client.connect()
--- a/devtools/shared/fronts/moz.build
+++ b/devtools/shared/fronts/moz.build
@@ -18,16 +18,17 @@ DevToolsModules(
'eventlooplag.js',
'framerate.js',
'gcli.js',
'highlighters.js',
'inspector.js',
'layout.js',
'memory.js',
'node.js',
+ 'perf.js',
'performance-recording.js',
'performance.js',
'preference.js',
'promises.js',
'reflow.js',
'storage.js',
'string.js',
'styles.js',
new file mode 100644
--- /dev/null
+++ b/devtools/shared/fronts/perf.js
@@ -0,0 +1,15 @@
+/* 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 { FrontClassWithSpec, Front } = require("devtools/shared/protocol");
+const { perfSpec } = require("devtools/shared/specs/perf");
+
+exports.PerfFront = FrontClassWithSpec(perfSpec, {
+ initialize: function (client, form) {
+ Front.prototype.initialize.call(this, client, form);
+ this.actorID = form.perfActor;
+ this.manage(this);
+ }
+});
--- a/devtools/shared/specs/index.js
+++ b/devtools/shared/specs/index.js
@@ -128,16 +128,21 @@ const Types = exports.__TypesForTests =
front: null,
},
{
types: ["domnode", "domnodelist"],
spec: "devtools/shared/specs/node",
front: "devtools/shared/fronts/node",
},
{
+ types: ["perf"],
+ spec: "devtools/shared/specs/perf",
+ front: "devtools/shared/fronts/perf",
+ },
+ {
types: ["performance"],
spec: "devtools/shared/specs/performance",
front: "devtools/shared/fronts/performance",
},
{
types: ["performance-recording"],
spec: "devtools/shared/specs/performance-recording",
front: "devtools/shared/fronts/performance-recording",
--- a/devtools/shared/specs/moz.build
+++ b/devtools/shared/specs/moz.build
@@ -23,16 +23,17 @@ DevToolsModules(
'gcli.js',
'heap-snapshot-file.js',
'highlighters.js',
'index.js',
'inspector.js',
'layout.js',
'memory.js',
'node.js',
+ 'perf.js',
'performance-recording.js',
'performance.js',
'preference.js',
'promises.js',
'reflow.js',
'script.js',
'source.js',
'storage.js',
new file mode 100644
--- /dev/null
+++ b/devtools/shared/specs/perf.js
@@ -0,0 +1,66 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const { RetVal, generateActorSpec } = require("devtools/shared/protocol");
+
+const perfDescription = {
+ typeName: "perf",
+
+ events: {
+ "profiler-started": {
+ type: "profiler-started"
+ },
+ "profiler-stopped": {
+ type: "profiler-stopped"
+ },
+ "profile-locked-by-private-browsing": {
+ type: "profile-locked-by-private-browsing"
+ },
+ "profile-unlocked-from-private-browsing": {
+ type: "profile-unlocked-from-private-browsing"
+ }
+ },
+
+ methods: {
+ startProfiler: {
+ request: {},
+ response: { value: RetVal("boolean") }
+ },
+
+ /**
+ * Returns null when unable to return the profile.
+ */
+ getProfileAndStopProfiler: {
+ request: {},
+ response: RetVal("nullable:json")
+ },
+
+ stopProfilerAndDiscardProfile: {
+ request: {},
+ response: {}
+ },
+
+ isActive: {
+ request: {},
+ response: { value: RetVal("boolean") }
+ },
+
+ isSupportedPlatform: {
+ request: {},
+ response: { value: RetVal("boolean") }
+ },
+
+ isLockedForPrivateBrowsing: {
+ request: {},
+ response: { value: RetVal("boolean") }
+ }
+ }
+};
+
+exports.perfDescription = perfDescription;
+
+const perfSpec = generateActorSpec(perfDescription);
+
+exports.perfSpec = perfSpec;