Bug 1297784 - Turn performance controls into a React component; r?jsantell draft
authorGreg Tatum <tatum.creative@gmail.com>
Thu, 18 Aug 2016 14:53:03 -0500
changeset 409446 4c0e9be30507f261db6e5292e7bb8dbd2ee73a56
parent 398620 a3fb4eb11fcf840b9e0d65a06be6cf64584add08
child 530341 da9e0e2771bda98dbfa9d3ea7471c31394ac9230
push id28471
push userbmo:gtatum@mozilla.com
push dateFri, 02 Sep 2016 20:53:26 +0000
reviewersjsantell
bugs1297784
milestone51.0a1
Bug 1297784 - Turn performance controls into a React component; r?jsantell MozReview-Commit-ID: 1R8ky0vW4fH
devtools/client/locales/en-US/performance.dtd
devtools/client/locales/en-US/performance.properties
devtools/client/performance/components/moz.build
devtools/client/performance/components/recording-button.js
devtools/client/performance/components/recording-controls.js
devtools/client/performance/modules/moz.build
devtools/client/performance/modules/utils.js
devtools/client/performance/performance-controller.js
devtools/client/performance/performance-view.js
devtools/client/performance/performance.xul
devtools/client/themes/performance.css
--- a/devtools/client/locales/en-US/performance.dtd
+++ b/devtools/client/locales/en-US/performance.dtd
@@ -6,21 +6,16 @@
 <!-- LOCALIZATION NOTE : FILE Do not translate commandkey -->
 
 <!-- LOCALIZATION NOTE : FILE The correct localization of this file might be to
   - keep it in English, or another language commonly spoken among web developers.
   - You want to make that choice consistent across the developer tools.
   - A good criteria is the language in which you'd find the best
   - documentation on web development on the web. -->
 
-<!-- LOCALIZATION NOTE (performanceUI.startRecording/performanceUI.stopRecording): These are
-  -  the labels shown on the main recording buttons to start/stop recording. -->
-<!ENTITY performanceUI.startRecording "Start Recording Performance">
-<!ENTITY performanceUI.stopRecording  "Stop Recording Performance">
-
 <!-- LOCALIZATION NOTE (performanceUI.bufferStatusTooltip): This string
   -  is displayed as the tooltip for the buffer capacity during a recording. -->
 <!ENTITY performanceUI.bufferStatusTooltip "The profiler stores samples in a circular buffer, and once the buffer reaches the limit for a recording, newer samples begin to overwrite samples at the beginning of the recording.">
 
 <!-- LOCALIZATION NOTE (performanceUI.disabledRealTime.nonE10SBuild): This string
   -  is displayed as a message for why the real time overview graph is disabled
   -  when running on a non-multiprocess build. -->
 <!ENTITY performanceUI.disabledRealTime.nonE10SBuild "Realtime recording data disabled on non-multiprocess Firefox.">
@@ -38,28 +33,16 @@
   -  in the details view while the profiler is unavailable, for example, while
   -  in Private Browsing mode. -->
 <!ENTITY performanceUI.unavailableNoticePB "Recording a profile is currently unavailable. Please close all private browsing windows and try again.">
 
 <!-- LOCALIZATION NOTE (performanceUI.loadingNotice): This is the label shown
   -  in the details view while loading a profile. -->
 <!ENTITY performanceUI.loadingNotice "Loading…">
 
-<!-- LOCALIZATION NOTE (performanceUI.recordButton): This string is displayed
-  -  on a button that starts a new profile. -->
-<!ENTITY performanceUI.recordButton.tooltip "Toggle the recording state of a performance recording.">
-
-<!-- LOCALIZATION NOTE (performanceUI.importButton): This string is displayed
-  -  on a button that opens a dialog to import a saved profile data file. -->
-<!ENTITY performanceUI.importButton "Import…">
-
-<!-- LOCALIZATION NOTE (performanceUI.clearButton): This string is displayed
-  -  on a button that removes all the recordings. -->
-<!ENTITY performanceUI.clearButton "Clear">
-
 <!-- LOCALIZATION NOTE (performanceUI.toolbar.*): These strings are displayed
   -  in the toolbar on buttons that select which view is currently shown. -->
 <!ENTITY performanceUI.toolbar.waterfall "Waterfall">
 <!ENTITY performanceUI.toolbar.waterfall.tooltiptext "Shows the different operations the browser is performing during the recording, laid out sequentially as a waterfall.">
 <!ENTITY performanceUI.toolbar.js-calltree "Call Tree">
 <!ENTITY performanceUI.toolbar.js-calltree.tooltiptext "Highlights JavaScript functions where the browser spent most time during the recording.">
 <!ENTITY performanceUI.toolbar.memory-calltree "Allocations">
 <!ENTITY performanceUI.toolbar.allocations.tooltiptext "Shows where memory was allocated during the recording.">
--- a/devtools/client/locales/en-US/performance.properties
+++ b/devtools/client/locales/en-US/performance.properties
@@ -152,8 +152,28 @@ timeline.records=RECORDS
 
 # LOCALIZATION NOTE (profiler.bufferFull):
 # This string is displayed when recording, indicating how much of the
 # buffer is currently be used.
 # %S is the percentage of the buffer used -- there are two "%"s after to escape
 # the % that is actually displayed.
 # Example: "Buffer 54% full"
 profiler.bufferFull=Buffer %S%% full
+
+# LOCALIZATION NOTE (recordings.start):
+# The label shown on the main recording buttons to start recording.
+recordings.start=Start Recording Performance
+
+# LOCALIZATION NOTE (recordings.stop):
+# The label shown on the main recording buttons to stop recording.
+recordings.stop=Stop Recording Performance
+
+# LOCALIZATION NOTE (recordings.start.tooltip):
+# This string is displayed as a tooltip on a button that starts a new profile.
+recordings.start.tooltip=Toggle the recording state of a performance recording.
+
+# LOCALIZATION NOTE (recordings.import.tooltip):
+# This string is displayed on a button that opens a dialog to import a saved profile data file.
+recordings.import.tooltip=Import…
+
+# LOCALIZATION NOTE (recordings.clear.tooltip):
+# This string is displayed on a button that removes all the recordings.
+recordings.clear.tooltip=Clear
--- a/devtools/client/performance/components/moz.build
+++ b/devtools/client/performance/components/moz.build
@@ -1,11 +1,13 @@
 # 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(
     'jit-optimizations-item.js',
     'jit-optimizations.js',
+    'recording-button.js',
+    'recording-controls.js',
 )
 
 MOCHITEST_CHROME_MANIFESTS += ['test/chrome.ini']
new file mode 100644
--- /dev/null
+++ b/devtools/client/performance/components/recording-button.js
@@ -0,0 +1,29 @@
+/* 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 {L10N} = require("devtools/client/performance/modules/global");
+const {DOM, createClass} = require("devtools/client/shared/vendor/react");
+const {button} = DOM;
+
+module.exports = createClass({
+  displayName: "Recording Button",
+
+  render() {
+    let {
+      onRecordButtonClick,
+      isRecording
+    } = this.props;
+
+    return button(
+      {
+        className: "devtools-toolbarbutton record-button",
+        onClick: onRecordButtonClick,
+        "data-standalone": "true",
+        "data-text-only": "true",
+      },
+      isRecording ? L10N.getStr("recordings.stop") : L10N.getStr("recordings.start")
+    );
+  }
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/performance/components/recording-controls.js
@@ -0,0 +1,70 @@
+/* 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 {L10N} = require("devtools/client/performance/modules/global");
+const {DOM, createClass} = require("devtools/client/shared/vendor/react");
+const {div, button} = DOM;
+
+module.exports = createClass({
+  displayName: "Recording Controls",
+
+  /**
+   * Manually handle the "checked" and "locked" attributes, as the DOM element won't
+   * change by just by changing the checked attribute through React.
+   */
+  componentDidUpdate() {
+    if (this.props.isRecording) {
+      this._recordButton.setAttribute("checked", true);
+    } else {
+      this._recordButton.removeAttribute("checked");
+    }
+
+    if (this.props.isLocked) {
+      this._recordButton.setAttribute("locked", true);
+    } else {
+      this._recordButton.removeAttribute("locked");
+    }
+  },
+
+  render() {
+    let {
+      onClearButtonClick,
+      onRecordButtonClick,
+      onImportButtonClick,
+      isRecording,
+      isLocked
+    } = this.props;
+
+    return (
+      div({ className: "devtools-toolbar" },
+        div({ className: "toolbar-group" },
+          button({
+            id: "clear-button",
+            className: "devtools-button",
+            title: L10N.getStr("recordings.clear.tooltip"),
+            onClick: onClearButtonClick
+          }),
+          button({
+            id: "main-record-button",
+            className: "devtools-button record-button",
+            title: L10N.getStr("recordings.start.tooltip"),
+            onClick: onRecordButtonClick,
+            checked: isRecording,
+            ref: (el) => {
+              this._recordButton = el;
+            },
+            locked: isLocked
+          }),
+          button({
+            id: "import-button",
+            className: "devtools-button",
+            title: L10N.getStr("recordings.import.tooltip"),
+            onClick: onImportButtonClick
+          })
+        )
+      )
+    );
+  }
+});
--- a/devtools/client/performance/modules/moz.build
+++ b/devtools/client/performance/modules/moz.build
@@ -12,9 +12,10 @@ DevToolsModules(
     'categories.js',
     'constants.js',
     'global.js',
     'io.js',
     'marker-blueprint-utils.js',
     'marker-dom-utils.js',
     'marker-formatters.js',
     'markers.js',
+    'utils.js',
 )
new file mode 100644
--- /dev/null
+++ b/devtools/client/performance/modules/utils.js
@@ -0,0 +1,21 @@
+/* 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";
+
+/* globals document */
+
+/**
+ * React components grab the namespace of the element they are mounting to. This function
+ * takes a XUL element, and makes sure to create a properly namespaced HTML element to
+ * avoid React creating XUL elements.
+ *
+ * {XULElement} xulElement
+ * return {HTMLElement} div
+ */
+
+exports.createHtmlMount = function (xulElement) {
+  let htmlElement = document.createElementNS("http://www.w3.org/1999/xhtml", "div");
+  xulElement.appendChild(htmlElement);
+  return htmlElement;
+};
--- a/devtools/client/performance/performance-controller.js
+++ b/devtools/client/performance/performance-controller.js
@@ -21,36 +21,41 @@ var { gDevTools } = require("devtools/cl
 // Events emitted by various objects in the panel.
 var EVENTS = require("devtools/client/performance/events");
 Object.defineProperty(this, "EVENTS", {
   value: EVENTS,
   enumerable: true,
   writable: false
 });
 
-/* exported React, ReactDOM, JITOptimizationsView, Services, promise, EventEmitter,
-   DevToolsUtils, system */
+/* exported React, ReactDOM, JITOptimizationsView, RecordingControls, RecordingButton,
+   Services, promise, EventEmitter, DevToolsUtils, system */
 var React = require("devtools/client/shared/vendor/react");
 var ReactDOM = require("devtools/client/shared/vendor/react-dom");
 var JITOptimizationsView = React.createFactory(require("devtools/client/performance/components/jit-optimizations"));
+var RecordingControls = React.createFactory(require("devtools/client/performance/components/recording-controls"));
+var RecordingButton = React.createFactory(require("devtools/client/performance/components/recording-button"));
+
 var Services = require("Services");
 var promise = require("promise");
 var EventEmitter = require("devtools/shared/event-emitter");
 var DevToolsUtils = require("devtools/shared/DevToolsUtils");
 var flags = require("devtools/shared/flags");
 var system = require("devtools/shared/system");
 
 // Logic modules
 /* exported L10N, PerformanceTelemetry, TIMELINE_BLUEPRINT, RecordingUtils,
-   OptimizationsGraph, GraphsController, WaterfallHeader, MarkerView, MarkerDetails,
-   MarkerBlueprintUtils, WaterfallUtils, FrameUtils, CallView, ThreadNode, FrameNode */
+   PerformanceUtils, OptimizationsGraph, GraphsController, WaterfallHeader, MarkerView,
+   MarkerDetails, MarkerBlueprintUtils, WaterfallUtils, FrameUtils, CallView, ThreadNode,
+   FrameNode */
 var { L10N } = require("devtools/client/performance/modules/global");
 var { PerformanceTelemetry } = require("devtools/client/performance/modules/logic/telemetry");
 var { TIMELINE_BLUEPRINT } = require("devtools/client/performance/modules/markers");
 var RecordingUtils = require("devtools/shared/performance/recording-utils");
+var PerformanceUtils = require("devtools/client/performance/modules/utils");
 var { OptimizationsGraph, GraphsController } = require("devtools/client/performance/modules/widgets/graphs");
 var { WaterfallHeader } = require("devtools/client/performance/modules/widgets/waterfall-ticks");
 var { MarkerView } = require("devtools/client/performance/modules/widgets/marker-view");
 var { MarkerDetails } = require("devtools/client/performance/modules/widgets/marker-details");
 var { MarkerBlueprintUtils } = require("devtools/client/performance/modules/marker-blueprint-utils");
 var WaterfallUtils = require("devtools/client/performance/modules/logic/waterfall-utils");
 var FrameUtils = require("devtools/client/performance/modules/logic/frame-utils");
 var { CallView } = require("devtools/client/performance/modules/widgets/tree-view");
--- a/devtools/client/performance/performance-view.js
+++ b/devtools/client/performance/performance-view.js
@@ -111,34 +111,24 @@ var PerformanceView = {
       },
     ]
   },
 
   /**
    * Sets up the view with event binding and main subviews.
    */
   initialize: Task.async(function* () {
-    this._recordButton = $("#main-record-button");
-    this._importButton = $("#import-button");
-    this._clearButton = $("#clear-button");
-
     this._onRecordButtonClick = this._onRecordButtonClick.bind(this);
     this._onImportButtonClick = this._onImportButtonClick.bind(this);
     this._onClearButtonClick = this._onClearButtonClick.bind(this);
     this._onRecordingSelected = this._onRecordingSelected.bind(this);
     this._onProfilerStatusUpdated = this._onProfilerStatusUpdated.bind(this);
     this._onRecordingStateChange = this._onRecordingStateChange.bind(this);
     this._onNewRecordingFailed = this._onNewRecordingFailed.bind(this);
 
-    for (let button of $$(".record-button")) {
-      button.addEventListener("click", this._onRecordButtonClick);
-    }
-    this._importButton.addEventListener("click", this._onImportButtonClick);
-    this._clearButton.addEventListener("click", this._onClearButtonClick);
-
     // Bind to controller events to unlock the record button
     PerformanceController.on(EVENTS.RECORDING_SELECTED, this._onRecordingSelected);
     PerformanceController.on(EVENTS.RECORDING_PROFILER_STATUS_UPDATE,
                              this._onProfilerStatusUpdated);
     PerformanceController.on(EVENTS.RECORDING_STATE_CHANGE, this._onRecordingStateChange);
     PerformanceController.on(EVENTS.RECORDING_ADDED, this._onRecordingStateChange);
     PerformanceController.on(EVENTS.BACKEND_FAILED_AFTER_RECORDING_START,
                              this._onNewRecordingFailed);
@@ -150,28 +140,49 @@ var PerformanceView = {
     }
 
     // Initialize the ToolbarView first, because other views may need access
     // to the OptionsView via the controller, to read prefs.
     yield ToolbarView.initialize();
     yield RecordingsView.initialize();
     yield OverviewView.initialize();
     yield DetailsView.initialize();
+
+    // DE-XUL: Begin migrating the toolbar to React. Temporarily hold state here.
+    this._recordingControlsState = {
+      onRecordButtonClick: this._onRecordButtonClick,
+      onImportButtonClick: this._onImportButtonClick,
+      onClearButtonClick: this._onClearButtonClick,
+      isRecording: false,
+      isDisabled: false
+    };
+    // Mount to an HTML element.
+    const {createHtmlMount} = PerformanceUtils;
+    this._recordingControlsMount = createHtmlMount($("#recording-controls-mount"));
+    this._recordingButtonsMounts = Array.from($$(".recording-button-mount"))
+                                        .map(createHtmlMount);
+
+    this._renderRecordingControls();
   }),
 
   /**
+   * DE-XUL: Render the recording controls and buttons using React.
+   */
+  _renderRecordingControls: function () {
+    ReactDOM.render(RecordingControls(this._recordingControlsState),
+                    this._recordingControlsMount);
+    for (let button of this._recordingButtonsMounts) {
+      ReactDOM.render(RecordingButton(this._recordingControlsState), button);
+    }
+  },
+
+  /**
    * Unbinds events and destroys subviews.
    */
   destroy: Task.async(function* () {
-    for (let button of $$(".record-button")) {
-      button.removeEventListener("click", this._onRecordButtonClick);
-    }
-    this._importButton.removeEventListener("click", this._onImportButtonClick);
-    this._clearButton.removeEventListener("click", this._onClearButtonClick);
-
     PerformanceController.off(EVENTS.RECORDING_SELECTED, this._onRecordingSelected);
     PerformanceController.off(EVENTS.RECORDING_PROFILER_STATUS_UPDATE,
                               this._onProfilerStatusUpdated);
     PerformanceController.off(EVENTS.RECORDING_STATE_CHANGE,
                               this._onRecordingStateChange);
     PerformanceController.off(EVENTS.RECORDING_ADDED, this._onRecordingStateChange);
     PerformanceController.off(EVENTS.BACKEND_FAILED_AFTER_RECORDING_START,
                               this._onNewRecordingFailed);
@@ -269,39 +280,29 @@ var PerformanceView = {
 
   /**
    * Toggles the `locked` attribute on the record buttons based
    * on `lock`.
    *
    * @param {boolean} lock
    */
   _lockRecordButtons: function (lock) {
-    for (let button of $$(".record-button")) {
-      if (lock) {
-        button.setAttribute("locked", "true");
-      } else {
-        button.removeAttribute("locked");
-      }
-    }
+    this._recordingControlsState.isLocked = lock;
+    this._renderRecordingControls();
   },
 
   /*
    * Toggles the `checked` attribute on the record buttons based
    * on `activate`.
    *
    * @param {boolean} activate
    */
   _toggleRecordButtons: function (activate) {
-    for (let button of $$(".record-button")) {
-      if (activate) {
-        button.setAttribute("checked", "true");
-      } else {
-        button.removeAttribute("checked");
-      }
-    }
+    this._recordingControlsState.isRecording = !!activate;
+    this._renderRecordingControls();
   },
 
   /**
    * When a recording has started.
    */
   _onRecordingStateChange: function () {
     let currentRecording = PerformanceController.getCurrentRecording();
     let recordings = PerformanceController.getRecordings();
@@ -334,17 +335,17 @@ var PerformanceView = {
   _onClearButtonClick: function (e) {
     this.emit(EVENTS.UI_CLEAR_RECORDINGS);
   },
 
   /**
    * Handler for clicking the record button.
    */
   _onRecordButtonClick: function (e) {
-    if (this._recordButton.hasAttribute("checked")) {
+    if (this._recordingControlsState.isRecording) {
       this.emit(EVENTS.UI_STOP_RECORDING);
     } else {
       this._lockRecordButtons(true);
       this._toggleRecordButtons(true);
       this.emit(EVENTS.UI_START_RECORDING);
     }
   },
 
--- a/devtools/client/performance/performance.xul
+++ b/devtools/client/performance/performance.xul
@@ -8,17 +8,18 @@
 <?xml-stylesheet href="chrome://devtools/skin/performance.css" type="text/css"?>
 <?xml-stylesheet href="chrome://devtools/skin/jit-optimizations.css" type="text/css"?>
 <?xml-stylesheet href="chrome://devtools/skin/components-frame.css" type="text/css"?>
 <!DOCTYPE window [
   <!ENTITY % performanceDTD SYSTEM "chrome://devtools/locale/performance.dtd">
   %performanceDTD;
 ]>
 
-<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+        xmlns:html="http://www.w3.org/1999/xhtml">
   <script src="chrome://devtools/content/shared/theme-switching.js"/>
   <script type="application/javascript" src="performance-controller.js"/>
   <script type="application/javascript" src="performance-view.js"/>
   <script type="application/javascript" src="views/overview.js"/>
   <script type="application/javascript" src="views/toolbar.js"/>
   <script type="application/javascript" src="views/details-abstract-subview.js"/>
   <script type="application/javascript" src="views/details-waterfall.js"/>
   <script type="application/javascript" src="views/details-js-call-tree.js"/>
@@ -71,35 +72,23 @@
       <menuitem id="option-flatten-tree-recursion"
                 type="checkbox"
                 data-pref="flatten-tree-recursion"
                 label="&performanceUI.flattenTreeRecursion;"
                 tooltiptext="&performanceUI.flattenTreeRecursion.tooltiptext;"/>
     </menupopup>
   </popupset>
 
-  <hbox id="body" class="theme-body" flex="1">
+  <hbox id="body" class="theme-body performance-tool" flex="1">
 
     <!-- Sidebar: controls and recording list -->
     <vbox id="recordings-pane">
-      <toolbar id="recordings-toolbar"
-               class="devtools-toolbar">
-        <hbox id="recordings-controls"
-              class="devtools-toolbarbutton-group">
-          <toolbarbutton id="clear-button"
-                         class="devtools-toolbarbutton devtools-clear-icon"
-                         tooltiptext="&performanceUI.clearButton;"/>
-          <toolbarbutton id="main-record-button"
-                         class="devtools-toolbarbutton record-button"
-                         tooltiptext="&performanceUI.recordButton.tooltip;"/>
-          <toolbarbutton id="import-button"
-                         class="devtools-toolbarbutton"
-                         tooltiptext="&performanceUI.importButton;"/>
-        </hbox>
-      </toolbar>
+      <hbox id="recordings-controls">
+        <html:div id='recording-controls-mount'/>
+      </hbox>
       <vbox id="recordings-list" class="theme-sidebar" flex="1"/>
     </vbox>
 
     <!-- Main panel content -->
     <vbox id="performance-pane" flex="1">
 
       <!-- Top toolbar controls -->
       <toolbar id="performance-toolbar"
@@ -165,38 +154,32 @@
 
         <!-- "Unavailable" notice, shown when the entire tool is disabled,
              for example, when in private browsing mode. -->
         <vbox id="unavailable-notice"
               class="notice-container"
               align="center"
               pack="center"
               flex="1">
-          <hbox class="devtools-toolbarbutton-group"
-                pack="center">
-            <toolbarbutton class="devtools-toolbarbutton record-button"
-                           label="&performanceUI.startRecording;"
-                           standalone="true"/>
+          <hbox pack="center">
+            <html:div class='recording-button-mount'/>
           </hbox>
           <description class="tool-disabled-message">
             &performanceUI.unavailableNoticePB;
           </description>
         </vbox>
 
         <!-- "Empty" notice, shown when there's no recordings available -->
         <hbox id="empty-notice"
               class="notice-container"
               align="center"
               pack="center"
               flex="1">
-          <hbox class="devtools-toolbarbutton-group"
-                pack="center">
-            <toolbarbutton class="devtools-toolbarbutton record-button"
-                           label="&performanceUI.startRecording;"
-                           standalone="true"/>
+          <hbox pack="center">
+            <html:div class='recording-button-mount'/>
           </hbox>
         </hbox>
 
         <!-- Recording contents -->
         <vbox id="performance-view-content" flex="1">
 
           <!-- Overview graphs -->
           <vbox id="overview-pane">
@@ -218,21 +201,18 @@
             </hbox>
 
             <!-- "Recording" notice, shown when a recording is in progress -->
             <vbox id="recording-notice"
                   class="notice-container"
                   align="center"
                   pack="center"
                   flex="1">
-              <hbox class="devtools-toolbarbutton-group"
-                    pack="center">
-                <toolbarbutton class="devtools-toolbarbutton record-button"
-                               label="&performanceUI.stopRecording;"
-                               standalone="true"/>
+              <hbox pack="center">
+                <html:div class='recording-button-mount'/>
               </hbox>
               <label class="realtime-disabled-message"
                      value="&performanceUI.disabledRealTime.nonE10SBuild;"/>
               <label class="realtime-disabled-on-e10s-message"
                      value="&performanceUI.disabledRealTime.disabledE10S;"/>
               <label class="buffer-status-message"
                      tooltiptext="&performanceUI.bufferStatusTooltip;"/>
               <label class="buffer-status-message-full"
--- a/devtools/client/themes/performance.css
+++ b/devtools/client/themes/performance.css
@@ -23,16 +23,24 @@
 .theme-firebug {
   --cell-border-color: rgba(0,0,0,0.15);
   --cell-border-color-light: rgba(0,0,0,0.1);
   --focus-cell-border-color: rgba(0,0,0,0.3);
   --row-alt-background-color: rgba(76,158,217,0.1);
   --row-hover-background-color: rgba(76,158,217,0.2);
 }
 
+/*
+ * DE-XUL: Set a sidebar width because inline XUL components will cause the flex
+ * to overflow if dynamically sized.
+ */
+.performance-tool {
+  --sidebar-width: 185px;
+}
+
 /**
  * A generic class to hide elements, replacing the `element.hidden` attribute
  * that we use to hide elements that can later be active
  */
 .hidden {
   display: none;
   width: 0px;
   height: 0px;
@@ -79,22 +87,26 @@
 }
 
 #select-optimizations-view {
   list-style-image: url(images/profiler-stopwatch.svg);
 }
 
 /* Recording buttons */
 
-#main-record-button {
-  list-style-image: url(images/profiler-stopwatch.svg);
+#clear-button::before {
+  background-image: var(--clear-icon-url);
 }
 
-#import-button {
-  list-style-image: url(images/import.svg);
+#main-record-button::before {
+  background-image: url(images/profiler-stopwatch.svg);
+}
+
+#import-button::before {
+  background-image: url(images/import.svg);
 }
 
 #main-record-button .button-icon, #import-button .button-icon {
   margin: 0;
 }
 
 #main-record-button .button-text, #import-button .button-text {
   display: none;
@@ -114,16 +126,32 @@
   pointer-events: none;
   opacity: 0.5;
 }
 
 /* Sidebar & recording items */
 
 #recordings-pane {
   border-inline-end: 1px solid var(--theme-splitter-color);
+  width: var(--sidebar-width);
+}
+
+#recording-controls-mount {
+  width: var(--sidebar-width);
+}
+
+#recording-controls-mount > div {
+  width: var(--sidebar-width);
+}
+
+/*
+ * DE-XUL: The height of the toolbar is not correct without tweaking the line-height.
+ */
+#recordings-pane .devtools-toolbar {
+  line-height: 0;
 }
 
 #recordings-list {
   max-width: 300px;
 }
 
 .recording-item {
   padding: 4px;