diff --git a/devtools/client/definitions.js b/devtools/client/definitions.js index 263de7dc0ce0..fded481a139b 100644 --- a/devtools/client/definitions.js +++ b/devtools/client/definitions.js @@ -19,6 +19,7 @@ loader.lazyGetter(this, "CanvasDebuggerPanel", () => require("devtools/client/ca 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); @@ -253,29 +254,49 @@ Tools.canvasDebugger = { }; 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, - - isTargetSupported: function (target) { - return target.hasActor("performance"); - }, - - build: function (frame, target) { - return new PerformancePanel(frame, target); - } + 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, }; +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(); + +Services.prefs.addObserver( + "devtools.performance.new-panel-enabled", + { observe: switchPerformancePanel } +); + Tools.memory = { id: "memory", ordinal: 8, diff --git a/devtools/client/framework/toolbox-options.js b/devtools/client/framework/toolbox-options.js index 017d2772c9c8..05c47dba9435 100644 --- a/devtools/client/framework/toolbox-options.js +++ b/devtools/client/framework/toolbox-options.js @@ -328,6 +328,11 @@ OptionsPanel.prototype = { 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}) => { diff --git a/devtools/client/framework/toolbox-process-window.js b/devtools/client/framework/toolbox-process-window.js index 4eaea9b4655f..9234841a88c2 100644 --- a/devtools/client/framework/toolbox-process-window.js +++ b/devtools/client/framework/toolbox-process-window.js @@ -77,6 +77,7 @@ function setPrefDefaults() { 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"); diff --git a/devtools/client/jar.mn b/devtools/client/jar.mn index 3768b7041228..926c472e89d2 100644 --- a/devtools/client/jar.mn +++ b/devtools/client/jar.mn @@ -63,6 +63,9 @@ devtools.jar: 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) @@ -155,6 +158,7 @@ devtools.jar: 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) diff --git a/devtools/client/moz.build b/devtools/client/moz.build index bd2cf8233a3a..71224d2cd3fe 100644 --- a/devtools/client/moz.build +++ b/devtools/client/moz.build @@ -20,6 +20,7 @@ DIRS += [ 'memory', 'netmonitor', 'performance', + 'performance-new', 'preferences', 'responsive.html', 'scratchpad', diff --git a/devtools/client/performance-new/components/Perf.js b/devtools/client/performance-new/components/Perf.js new file mode 100644 index 000000000000..505a0ff29e48 --- /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 + ) + ) + ); +} diff --git a/devtools/client/performance-new/components/moz.build b/devtools/client/performance-new/components/moz.build new file mode 100644 index 000000000000..0db4d7f452de --- /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', +) diff --git a/devtools/client/performance-new/frame-script.js b/devtools/client/performance-new/frame-script.js new file mode 100644 index 000000000000..46f3a4ba70be --- /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; +} diff --git a/devtools/client/performance-new/initializer.js b/devtools/client/performance-new/initializer.js new file mode 100644 index 000000000000..e6a4a1efab29 --- /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")); +} diff --git a/devtools/client/performance-new/moz.build b/devtools/client/performance-new/moz.build new file mode 100644 index 000000000000..965bbf9e894e --- /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)') diff --git a/devtools/client/performance-new/panel.js b/devtools/client/performance-new/panel.js new file mode 100644 index 000000000000..5afa87188b1b --- /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; diff --git a/devtools/client/performance-new/perf.xhtml b/devtools/client/performance-new/perf.xhtml new file mode 100644 index 000000000000..c6bce09fe64b --- /dev/null +++ b/devtools/client/performance-new/perf.xhtml @@ -0,0 +1,25 @@ + + + %htmlDTD; +]> + + + + + + + + +
+ + + + diff --git a/devtools/client/performance-new/test/.eslintrc.js b/devtools/client/performance-new/test/.eslintrc.js new file mode 100644 index 000000000000..8d15a76d9b8c --- /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" +}; diff --git a/devtools/client/performance-new/test/chrome/chrome.ini b/devtools/client/performance-new/test/chrome/chrome.ini new file mode 100644 index 000000000000..953f87611dc1 --- /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] diff --git a/devtools/client/performance-new/test/chrome/head.js b/devtools/client/performance-new/test/chrome/head.js new file mode 100644 index 000000000000..3d75ee8d1382 --- /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."); + } +}); diff --git a/devtools/client/performance-new/test/chrome/test_perf-state-01.html b/devtools/client/performance-new/test/chrome/test_perf-state-01.html new file mode 100644 index 000000000000..f3cd18415bb7 --- /dev/null +++ b/devtools/client/performance-new/test/chrome/test_perf-state-01.html @@ -0,0 +1,70 @@ + + + + + + Perf component test + + + + +
+ +
+    
+    
+  
+ + diff --git a/devtools/client/performance-new/test/chrome/test_perf-state-02.html b/devtools/client/performance-new/test/chrome/test_perf-state-02.html new file mode 100644 index 000000000000..070c3fe92a4e --- /dev/null +++ b/devtools/client/performance-new/test/chrome/test_perf-state-02.html @@ -0,0 +1,59 @@ + + + + + + Perf component test + + + + +
+ +
+    
+    
+  
+ + diff --git a/devtools/client/performance-new/test/chrome/test_perf-state-03.html b/devtools/client/performance-new/test/chrome/test_perf-state-03.html new file mode 100644 index 000000000000..7fa385fe3981 --- /dev/null +++ b/devtools/client/performance-new/test/chrome/test_perf-state-03.html @@ -0,0 +1,61 @@ + + + + + + Perf component test + + + + +
+ +
+    
+    
+  
+ + diff --git a/devtools/client/performance-new/test/chrome/test_perf-state-04.html b/devtools/client/performance-new/test/chrome/test_perf-state-04.html new file mode 100644 index 000000000000..54b8b2449b2f --- /dev/null +++ b/devtools/client/performance-new/test/chrome/test_perf-state-04.html @@ -0,0 +1,64 @@ + + + + + + Perf component test + + + + +
+ +
+    
+    
+  
+ + diff --git a/devtools/client/preferences/devtools.js b/devtools/client/preferences/devtools.js index 62e69036267e..d1b7e398339c 100644 --- a/devtools/client/preferences/devtools.js +++ b/devtools/client/preferences/devtools.js @@ -305,6 +305,9 @@ 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); diff --git a/devtools/client/themes/perf.css b/devtools/client/themes/perf.css new file mode 100644 index 000000000000..efe7f399d8c7 --- /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; +} diff --git a/devtools/server/actors/moz.build b/devtools/server/actors/moz.build index 3407843c97dc..7f6737f657cf 100644 --- a/devtools/server/actors/moz.build +++ b/devtools/server/actors/moz.build @@ -42,6 +42,7 @@ DevToolsModules( 'memory.js', 'monitor.js', 'object.js', + 'perf.js', 'performance-recording.js', 'performance.js', 'preference.js', diff --git a/devtools/server/actors/perf.js b/devtools/server/actors/perf.js new file mode 100644 index 000000000000..d0b422e67fc7 --- /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; + } + } +}); diff --git a/devtools/server/main.js b/devtools/server/main.js index 6070f9aa8b47..1b7409affadb 100644 --- a/devtools/server/main.js +++ b/devtools/server/main.js @@ -443,6 +443,15 @@ var DebuggerServer = { 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 } + }); }, /** @@ -534,7 +543,8 @@ var DebuggerServer = { 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", diff --git a/devtools/server/tests/browser/browser.ini b/devtools/server/tests/browser/browser.ini index c94b78675c81..050b6ebd87d0 100644 --- a/devtools/server/tests/browser/browser.ini +++ b/devtools/server/tests/browser/browser.ini @@ -72,6 +72,9 @@ skip-if = e10s # Bug 1183605 - devtools/server/tests/browser/ tests are still di [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] diff --git a/devtools/server/tests/browser/browser_perf-01.js b/devtools/server/tests/browser/browser_perf-01.js new file mode 100644 index 000000000000..8249efc9ab7d --- /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(); +}); diff --git a/devtools/server/tests/browser/browser_perf-02.js b/devtools/server/tests/browser/browser_perf-02.js new file mode 100644 index 000000000000..60709426a274 --- /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(); +}); diff --git a/devtools/server/tests/browser/browser_perf-03.js b/devtools/server/tests/browser/browser_perf-03.js new file mode 100644 index 000000000000..b320722fd880 --- /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(); +}); diff --git a/devtools/server/tests/browser/head.js b/devtools/server/tests/browser/head.js index 1d656242352c..dfee614f45e5 100644 --- a/devtools/server/tests/browser/head.js +++ b/devtools/server/tests/browser/head.js @@ -108,6 +108,41 @@ function initDebuggerServer() { 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} diff --git a/devtools/shared/fronts/moz.build b/devtools/shared/fronts/moz.build index 5eadbccfc027..561159ae5c38 100644 --- a/devtools/shared/fronts/moz.build +++ b/devtools/shared/fronts/moz.build @@ -23,6 +23,7 @@ DevToolsModules( 'layout.js', 'memory.js', 'node.js', + 'perf.js', 'performance-recording.js', 'performance.js', 'preference.js', diff --git a/devtools/shared/fronts/perf.js b/devtools/shared/fronts/perf.js new file mode 100644 index 000000000000..0221f96b15a7 --- /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); + } +}); diff --git a/devtools/shared/specs/index.js b/devtools/shared/specs/index.js index 0a81a0da9bce..7085e009f879 100644 --- a/devtools/shared/specs/index.js +++ b/devtools/shared/specs/index.js @@ -132,6 +132,11 @@ const Types = exports.__TypesForTests = [ 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", diff --git a/devtools/shared/specs/moz.build b/devtools/shared/specs/moz.build index 3fd9911d6b0f..1b2f16610310 100644 --- a/devtools/shared/specs/moz.build +++ b/devtools/shared/specs/moz.build @@ -28,6 +28,7 @@ DevToolsModules( 'layout.js', 'memory.js', 'node.js', + 'perf.js', 'performance-recording.js', 'performance.js', 'preference.js', diff --git a/devtools/shared/specs/perf.js b/devtools/shared/specs/perf.js new file mode 100644 index 000000000000..f60904dd6397 --- /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;