Bug 1302062 - Use React on performance recording list; r=jsantell draft
authorGreg Tatum <tatum.creative@gmail.com>
Tue, 20 Sep 2016 09:47:59 -0500
changeset 418094 b42c054a72082340a88df8cf75e857878539ffcb
parent 417914 66a77b9bfe5dcacd50eccf85de7c0e7e15ce0ffd
child 532263 bf9fb6dd8ef06b9c7b3678746bc3fac08bf5908f
push id30593
push userbmo:gtatum@mozilla.com
push dateTue, 27 Sep 2016 19:10:27 +0000
reviewersjsantell
bugs1302062
milestone52.0a1
Bug 1302062 - Use React on performance recording list; r=jsantell
devtools/client/performance/components/moz.build
devtools/client/performance/components/recording-list-item.js
devtools/client/performance/components/recording-list.js
devtools/client/performance/performance-controller.js
devtools/client/performance/performance.xul
devtools/client/performance/test/browser_perf-calltree-js-events.js
devtools/client/performance/test/browser_perf-console-record-01.js
devtools/client/performance/test/browser_perf-console-record-02.js
devtools/client/performance/test/browser_perf-console-record-03.js
devtools/client/performance/test/browser_perf-console-record-04.js
devtools/client/performance/test/browser_perf-console-record-05.js
devtools/client/performance/test/browser_perf-console-record-06.js
devtools/client/performance/test/browser_perf-console-record-07.js
devtools/client/performance/test/browser_perf-console-record-08.js
devtools/client/performance/test/browser_perf-details-03-without-allocations.js
devtools/client/performance/test/browser_perf-details-04-toolbar-buttons.js
devtools/client/performance/test/browser_perf-loading-01.js
devtools/client/performance/test/browser_perf-loading-02.js
devtools/client/performance/test/browser_perf-options-show-jit-optimizations.js
devtools/client/performance/test/browser_perf-overview-render-04.js
devtools/client/performance/test/browser_perf-recording-notices-02.js
devtools/client/performance/test/browser_perf-recording-notices-03.js
devtools/client/performance/test/browser_perf-recording-selected-01.js
devtools/client/performance/test/browser_perf-recording-selected-02.js
devtools/client/performance/test/browser_perf-recording-selected-03.js
devtools/client/performance/test/browser_perf-recording-selected-04.js
devtools/client/performance/test/browser_perf-recordings-clear-01.js
devtools/client/performance/test/browser_perf-recordings-clear-02.js
devtools/client/performance/test/browser_perf-tree-view-11.js
devtools/client/performance/test/helpers/moz.build
devtools/client/performance/test/helpers/recording-utils.js
devtools/client/performance/views/recordings.js
devtools/client/themes/performance.css
--- a/devtools/client/performance/components/moz.build
+++ b/devtools/client/performance/components/moz.build
@@ -3,15 +3,17 @@
 # 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',
+    'recording-list-item.js',
+    'recording-list.js',
     'waterfall-header.js',
     'waterfall-tree-row.js',
     'waterfall-tree.js',
     'waterfall.js',
 )
 
 MOCHITEST_CHROME_MANIFESTS += ['test/chrome.ini']
new file mode 100644
--- /dev/null
+++ b/devtools/client/performance/components/recording-list-item.js
@@ -0,0 +1,49 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const {DOM, createClass} = require("devtools/client/shared/vendor/react");
+const {div, li, span, button} = DOM;
+const {L10N} = require("devtools/client/performance/modules/global");
+
+module.exports = createClass({
+  displayName: "Recording List Item",
+
+  render() {
+    const {
+      label,
+      duration,
+      onSelect,
+      onSave,
+      isLoading,
+      isSelected,
+      isRecording
+    } = this.props;
+
+    const className = `recording-list-item ${isSelected ? "selected" : ""}`;
+
+    let durationText;
+    if (isLoading) {
+      durationText = L10N.getStr("recordingsList.loadingLabel");
+    } else if (isRecording) {
+      durationText = L10N.getStr("recordingsList.recordingLabel");
+    } else {
+      durationText = L10N.getFormatStr("recordingsList.durationLabel", duration);
+    }
+
+    return (
+      li({ className, onClick: onSelect },
+        div({ className: "recording-list-item-label" },
+          label
+        ),
+        div({ className: "recording-list-item-footer" },
+          span({ className: "recording-list-item-duration" }, durationText),
+          button({ className: "recording-list-item-save", onClick: onSave },
+            L10N.getStr("recordingsList.saveLabel")
+          )
+        )
+      )
+    );
+  }
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/performance/components/recording-list.js
@@ -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/. */
+"use strict";
+
+const {DOM, createClass} = require("devtools/client/shared/vendor/react");
+const {L10N} = require("devtools/client/performance/modules/global");
+const {ul, div} = DOM;
+
+module.exports = createClass({
+  displayName: "Recording List",
+
+  render() {
+    const {
+      items,
+      itemComponent: Item,
+    } = this.props;
+
+    return items.length > 0
+      ? ul({ className: "recording-list" }, ...items.map(Item))
+      : div({ className: "recording-list-empty" }, L10N.getStr("noRecordingsText"));
+  }
+});
--- a/devtools/client/performance/performance-controller.js
+++ b/devtools/client/performance/performance-controller.js
@@ -22,23 +22,26 @@ var { gDevTools } = require("devtools/cl
 var EVENTS = require("devtools/client/performance/events");
 Object.defineProperty(this, "EVENTS", {
   value: EVENTS,
   enumerable: true,
   writable: false
 });
 
 /* exported React, ReactDOM, JITOptimizationsView, RecordingControls, RecordingButton,
-   Waterfall, Services, promise, EventEmitter, DevToolsUtils, system */
+   RecordingList, RecordingListItem, Services, Waterfall, promise, EventEmitter,
+   DevToolsUtils, system */
 var React = require("devtools/client/shared/vendor/react");
 var ReactDOM = require("devtools/client/shared/vendor/react-dom");
 var Waterfall = React.createFactory(require("devtools/client/performance/components/waterfall"));
 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 RecordingList = React.createFactory(require("devtools/client/performance/components/recording-list"));
+var RecordingListItem = React.createFactory(require("devtools/client/performance/components/recording-list-item"));
 
 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");
 
--- a/devtools/client/performance/performance.xul
+++ b/devtools/client/performance/performance.xul
@@ -79,17 +79,19 @@
 
   <hbox id="body" class="theme-body performance-tool" flex="1">
 
     <!-- Sidebar: controls and recording list -->
     <vbox id="recordings-pane">
       <hbox id="recordings-controls">
         <html:div id='recording-controls-mount'/>
       </hbox>
-      <vbox id="recordings-list" class="theme-sidebar" flex="1"/>
+      <vbox id="recordings-list" class="theme-sidebar" flex="1">
+        <html:div id="recording-list-mount"/>
+      </vbox>
     </vbox>
 
     <!-- Main panel content -->
     <vbox id="performance-pane" flex="1">
 
       <!-- Top toolbar controls -->
       <toolbar id="performance-toolbar"
                class="devtools-toolbar">
--- a/devtools/client/performance/test/browser_perf-calltree-js-events.js
+++ b/devtools/client/performance/test/browser_perf-calltree-js-events.js
@@ -29,21 +29,28 @@ add_task(function* () {
   yield rendered;
 
   // Mock the profile used so we can get a deterministic tree created.
   let profile = synthesizeProfile();
   let threadNode = new ThreadNode(profile.threads[0], OverviewView.getTimeInterval());
   JsCallTreeView._populateCallTree(threadNode);
   JsCallTreeView.emit(EVENTS.UI_JS_CALL_TREE_RENDERED);
 
+  let firstTreeItem = $("#js-calltree-view .call-tree-item");
+
+  // DE-XUL: There are focus issues with XUL. Focus first, then synthesize the clicks
+  // so that keyboard events work correctly.
+  firstTreeItem.focus();
+
   let count = 0;
   let onFocus = () => count++;
   JsCallTreeView.on("focus", onFocus);
 
-  click($("#js-calltree-view .call-tree-item"));
+  click(firstTreeItem);
+
   key("VK_DOWN");
   key("VK_DOWN");
   key("VK_DOWN");
   key("VK_DOWN");
 
   JsCallTreeView.off("focus", onFocus);
   is(count, 4, "Several focus events are fired for the calltree.");
 
--- a/devtools/client/performance/test/browser_perf-console-record-01.js
+++ b/devtools/client/performance/test/browser_perf-console-record-01.js
@@ -5,36 +5,39 @@
 /**
  * Tests if the profiler is populated by console recordings that have finished
  * before it was opened.
  */
 
 const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
 const { initPerformanceInTab, initConsoleInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
 const { waitUntil } = require("devtools/client/performance/test/helpers/wait-utils");
+const { getSelectedRecording } = require("devtools/client/performance/test/helpers/recording-utils");
 
 add_task(function* () {
   let { target, console } = yield initConsoleInNewTab({
     url: SIMPLE_URL,
     win: window
   });
 
   yield console.profile("rust");
   yield console.profileEnd("rust");
 
   let { panel } = yield initPerformanceInTab({ tab: target.tab });
-  let { PerformanceController, RecordingsView, WaterfallView } = panel.panelWin;
+  let { PerformanceController, WaterfallView } = panel.panelWin;
 
   yield waitUntil(() => PerformanceController.getRecordings().length == 1);
   yield waitUntil(() => WaterfallView.wasRenderedAtLeastOnce);
 
   let recordings = PerformanceController.getRecordings();
   is(recordings.length, 1, "One recording found in the performance panel.");
   is(recordings[0].isConsole(), true, "Recording came from console.profile.");
   is(recordings[0].getLabel(), "rust", "Correct label in the recording model.");
 
-  is(RecordingsView.selectedItem.attachment, recordings[0],
+  const selected = getSelectedRecording(panel);
+
+  is(selected, recordings[0],
     "The profile from console should be selected as it's the only one.");
-  is(RecordingsView.selectedItem.attachment.getLabel(), "rust",
+  is(selected.getLabel(), "rust",
     "The profile label for the first recording is correct.");
 
   yield teardownToolboxAndRemoveTab(panel);
 });
--- a/devtools/client/performance/test/browser_perf-console-record-02.js
+++ b/devtools/client/performance/test/browser_perf-console-record-02.js
@@ -8,43 +8,45 @@
  */
 
 const { Constants } = require("devtools/client/performance/modules/constants");
 const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
 const { initPerformanceInTab, initConsoleInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
 const { waitForRecordingStoppedEvents } = require("devtools/client/performance/test/helpers/actions");
 const { waitUntil } = require("devtools/client/performance/test/helpers/wait-utils");
 const { times } = require("devtools/client/performance/test/helpers/event-utils");
+const { getSelectedRecording } = require("devtools/client/performance/test/helpers/recording-utils");
 
 add_task(function* () {
   let { target, console } = yield initConsoleInNewTab({
     url: SIMPLE_URL,
     win: window
   });
 
   yield console.profile("rust");
   yield console.profile("rust2");
 
   let { panel } = yield initPerformanceInTab({ tab: target.tab });
-  let { EVENTS, PerformanceController, OverviewView, RecordingsView } = panel.panelWin;
+  let { EVENTS, PerformanceController, OverviewView } = panel.panelWin;
 
   yield waitUntil(() => PerformanceController.getRecordings().length == 2);
 
   let recordings = PerformanceController.getRecordings();
   is(recordings.length, 2, "Two recordings found in the performance panel.");
   is(recordings[0].isConsole(), true, "Recording came from console.profile (1).");
   is(recordings[0].getLabel(), "rust", "Correct label in the recording model (1).");
   is(recordings[0].isRecording(), true, "Recording is still recording (1).");
   is(recordings[1].isConsole(), true, "Recording came from console.profile (2).");
   is(recordings[1].getLabel(), "rust2", "Correct label in the recording model (2).");
   is(recordings[1].isRecording(), true, "Recording is still recording (2).");
 
-  is(RecordingsView.selectedItem.attachment, recordings[0],
+  const selected = getSelectedRecording(panel);
+  is(selected, recordings[0],
     "The first console recording should be selected.");
-  is(RecordingsView.selectedItem.attachment.getLabel(), "rust",
+  is(selected.getLabel(), "rust",
     "The profile label for the first recording is correct.");
 
   // Ensure overview is still rendering.
   yield times(OverviewView, EVENTS.UI_OVERVIEW_RENDERED, 3, {
     expectedArgs: { "1": Constants.FRAMERATE_GRAPH_LOW_RES_INTERVAL }
   });
 
   let stopped = waitForRecordingStoppedEvents(panel, {
--- a/devtools/client/performance/test/browser_perf-console-record-03.js
+++ b/devtools/client/performance/test/browser_perf-console-record-03.js
@@ -6,45 +6,47 @@
  * Tests if the profiler is populated by in-progress console recordings, and
  * also console recordings that have finished before it was opened.
  */
 
 const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
 const { initPerformanceInTab, initConsoleInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
 const { waitForRecordingStoppedEvents } = require("devtools/client/performance/test/helpers/actions");
 const { waitUntil } = require("devtools/client/performance/test/helpers/wait-utils");
+const { getSelectedRecording } = require("devtools/client/performance/test/helpers/recording-utils");
 
 add_task(function* () {
   let { target, console } = yield initConsoleInNewTab({
     url: SIMPLE_URL,
     win: window
   });
 
   yield console.profile("rust");
   yield console.profileEnd("rust");
   yield console.profile("rust2");
 
   let { panel } = yield initPerformanceInTab({ tab: target.tab });
-  let { PerformanceController, RecordingsView, WaterfallView } = panel.panelWin;
+  let { PerformanceController, WaterfallView } = panel.panelWin;
 
   yield waitUntil(() => PerformanceController.getRecordings().length == 2);
   yield waitUntil(() => WaterfallView.wasRenderedAtLeastOnce);
 
   let recordings = PerformanceController.getRecordings();
   is(recordings.length, 2, "Two recordings found in the performance panel.");
   is(recordings[0].isConsole(), true, "Recording came from console.profile (1).");
   is(recordings[0].getLabel(), "rust", "Correct label in the recording model (1).");
   is(recordings[0].isRecording(), false, "Recording is still recording (1).");
   is(recordings[1].isConsole(), true, "Recording came from console.profile (2).");
   is(recordings[1].getLabel(), "rust2", "Correct label in the recording model (2).");
   is(recordings[1].isRecording(), true, "Recording is still recording (2).");
 
-  is(RecordingsView.selectedItem.attachment, recordings[0],
+  const selected = getSelectedRecording(panel);
+  is(selected, recordings[0],
     "The first console recording should be selected.");
-  is(RecordingsView.selectedItem.attachment.getLabel(), "rust",
+  is(selected.getLabel(), "rust",
     "The profile label for the first recording is correct.");
 
   let stopped = waitForRecordingStoppedEvents(panel, {
     // only emitted for manual recordings
     skipWaitingForBackendReady: true,
     // only emitted when a finished recording is selected
     skipWaitingForOverview: true,
     skipWaitingForSubview: true,
--- a/devtools/client/performance/test/browser_perf-console-record-04.js
+++ b/devtools/client/performance/test/browser_perf-console-record-04.js
@@ -7,42 +7,44 @@
  * after being opened.
  */
 
 const { Constants } = require("devtools/client/performance/modules/constants");
 const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
 const { initPerformanceInTab, initConsoleInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
 const { waitForRecordingStartedEvents, waitForRecordingStoppedEvents } = require("devtools/client/performance/test/helpers/actions");
 const { times } = require("devtools/client/performance/test/helpers/event-utils");
+const { getSelectedRecording } = require("devtools/client/performance/test/helpers/recording-utils");
 
 add_task(function* () {
   let { target, console } = yield initConsoleInNewTab({
     url: SIMPLE_URL,
     win: window
   });
 
   let { panel } = yield initPerformanceInTab({ tab: target.tab });
-  let { EVENTS, PerformanceController, OverviewView, RecordingsView } = panel.panelWin;
+  let { EVENTS, PerformanceController, OverviewView } = panel.panelWin;
 
   let started = waitForRecordingStartedEvents(panel, {
     // only emitted for manual recordings
     skipWaitingForBackendReady: true
   });
   yield console.profile("rust");
   yield started;
 
   let recordings = PerformanceController.getRecordings();
   is(recordings.length, 1, "One recording found in the performance panel.");
   is(recordings[0].isConsole(), true, "Recording came from console.profile.");
   is(recordings[0].getLabel(), "rust", "Correct label in the recording model.");
   is(recordings[0].isRecording(), true, "Recording is still recording.");
 
-  is(RecordingsView.selectedItem.attachment, recordings[0],
+  const selected = getSelectedRecording(panel);
+  is(selected, recordings[0],
     "The profile from console should be selected as it's the only one.");
-  is(RecordingsView.selectedItem.attachment.getLabel(), "rust",
+  is(selected.getLabel(), "rust",
     "The profile label for the first recording is correct.");
 
   // Ensure overview is still rendering.
   yield times(OverviewView, EVENTS.UI_OVERVIEW_RENDERED, 3, {
     expectedArgs: { "1": Constants.FRAMERATE_GRAPH_LOW_RES_INTERVAL }
   });
 
   let stopped = waitForRecordingStoppedEvents(panel, {
--- a/devtools/client/performance/test/browser_perf-console-record-05.js
+++ b/devtools/client/performance/test/browser_perf-console-record-05.js
@@ -7,42 +7,44 @@
  * in the recording list.
  */
 
 const { Constants } = require("devtools/client/performance/modules/constants");
 const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
 const { initPerformanceInTab, initConsoleInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
 const { waitForRecordingStartedEvents, waitForRecordingStoppedEvents } = require("devtools/client/performance/test/helpers/actions");
 const { times } = require("devtools/client/performance/test/helpers/event-utils");
+const { getSelectedRecording } = require("devtools/client/performance/test/helpers/recording-utils");
 
 add_task(function* () {
   let { target, console } = yield initConsoleInNewTab({
     url: SIMPLE_URL,
     win: window
   });
 
   let { panel } = yield initPerformanceInTab({ tab: target.tab });
-  let { EVENTS, PerformanceController, OverviewView, RecordingsView } = panel.panelWin;
+  let { EVENTS, PerformanceController, OverviewView } = panel.panelWin;
 
   let started = waitForRecordingStartedEvents(panel, {
     // only emitted for manual recordings
     skipWaitingForBackendReady: true
   });
   yield console.profile("rust");
   yield started;
 
   let recordings = PerformanceController.getRecordings();
   is(recordings.length, 1, "One recording found in the performance panel.");
   is(recordings[0].isConsole(), true, "Recording came from console.profile (1).");
   is(recordings[0].getLabel(), "rust", "Correct label in the recording model (1).");
   is(recordings[0].isRecording(), true, "Recording is still recording (1).");
 
-  is(RecordingsView.selectedItem.attachment, recordings[0],
+  let selected = getSelectedRecording(panel);
+  is(selected, recordings[0],
     "The profile from console should be selected as it's the only one.");
-  is(RecordingsView.selectedItem.attachment.getLabel(), "rust",
+  is(selected.getLabel(), "rust",
     "The profile label for the first recording is correct.");
 
   // Ensure overview is still rendering.
   yield times(OverviewView, EVENTS.UI_OVERVIEW_RENDERED, 3, {
     expectedArgs: { "1": Constants.FRAMERATE_GRAPH_LOW_RES_INTERVAL }
   });
 
   let stopped = waitForRecordingStoppedEvents(panel, {
@@ -65,19 +67,20 @@ add_task(function* () {
   yield started;
 
   recordings = PerformanceController.getRecordings();
   is(recordings.length, 2, "Two recordings found in the performance panel.");
   is(recordings[1].isConsole(), true, "Recording came from console.profile (2).");
   is(recordings[1].getLabel(), "rust", "Correct label in the recording model (2).");
   is(recordings[1].isRecording(), true, "Recording is still recording (2).");
 
-  is(RecordingsView.selectedItem.attachment, recordings[0],
+  selected = getSelectedRecording(panel);
+  is(selected, recordings[0],
     "The profile from console should still be selected");
-  is(RecordingsView.selectedItem.attachment.getLabel(), "rust",
+  is(selected.getLabel(), "rust",
     "The profile label for the first recording is correct.");
 
   stopped = waitForRecordingStoppedEvents(panel, {
     // only emitted for manual recordings
     skipWaitingForBackendReady: true,
     // only emitted when a finished recording is selected
     skipWaitingForOverview: true,
     skipWaitingForSubview: true,
--- a/devtools/client/performance/test/browser_perf-console-record-06.js
+++ b/devtools/client/performance/test/browser_perf-console-record-06.js
@@ -6,36 +6,37 @@
  * Tests that console recordings can overlap (not completely nested).
  */
 
 const { Constants } = require("devtools/client/performance/modules/constants");
 const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
 const { initPerformanceInTab, initConsoleInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
 const { waitForRecordingStartedEvents, waitForRecordingStoppedEvents } = require("devtools/client/performance/test/helpers/actions");
 const { times } = require("devtools/client/performance/test/helpers/event-utils");
+const { getSelectedRecording } = require("devtools/client/performance/test/helpers/recording-utils");
 
 add_task(function* () {
   let { target, console } = yield initConsoleInNewTab({
     url: SIMPLE_URL,
     win: window
   });
 
   let { panel } = yield initPerformanceInTab({ tab: target.tab });
-  let { EVENTS, PerformanceController, OverviewView, RecordingsView } = panel.panelWin;
+  let { EVENTS, PerformanceController, OverviewView } = panel.panelWin;
 
   let started = waitForRecordingStartedEvents(panel, {
     // only emitted for manual recordings
     skipWaitingForBackendReady: true
   });
   yield console.profile("rust");
   yield started;
 
   let recordings = PerformanceController.getRecordings();
   is(recordings.length, 1, "A recording found in the performance panel.");
-  is(RecordingsView.selectedItem.attachment, recordings[0],
+  is(getSelectedRecording(panel), recordings[0],
     "The first console recording should be selected.");
 
   // Ensure overview is still rendering.
   yield times(OverviewView, EVENTS.UI_OVERVIEW_RENDERED, 3, {
     expectedArgs: { "1": Constants.FRAMERATE_GRAPH_LOW_RES_INTERVAL }
   });
 
   started = waitForRecordingStartedEvents(panel, {
@@ -47,49 +48,49 @@ add_task(function* () {
     // in-progress recording is selected, which won't happen
     skipWaitingForViewState: true,
   });
   yield console.profile("golang");
   yield started;
 
   recordings = PerformanceController.getRecordings();
   is(recordings.length, 2, "Two recordings found in the performance panel.");
-  is(RecordingsView.selectedItem.attachment, recordings[0],
+  is(getSelectedRecording(panel), recordings[0],
     "The first console recording should still be selected.");
 
   // Ensure overview is still rendering.
   yield times(OverviewView, EVENTS.UI_OVERVIEW_RENDERED, 3, {
     expectedArgs: { "1": Constants.FRAMERATE_GRAPH_LOW_RES_INTERVAL }
   });
 
   let stopped = waitForRecordingStoppedEvents(panel, {
     // only emitted for manual recordings
     skipWaitingForBackendReady: true
   });
   yield console.profileEnd("rust");
   yield stopped;
 
   recordings = PerformanceController.getRecordings();
   is(recordings.length, 2, "Two recordings found in the performance panel.");
-  is(RecordingsView.selectedItem.attachment, recordings[0],
+  is(getSelectedRecording(panel), recordings[0],
     "The first console recording should still be selected.");
   is(recordings[0].isRecording(), false,
     "The first console recording should no longer be recording.");
 
   stopped = waitForRecordingStoppedEvents(panel, {
     // only emitted for manual recordings
     skipWaitingForBackendReady: true,
     // only emitted when a finished recording is selected
     skipWaitingForOverview: true,
     skipWaitingForSubview: true,
   });
   yield console.profileEnd("golang");
   yield stopped;
 
   recordings = PerformanceController.getRecordings();
   is(recordings.length, 2, "Two recordings found in the performance panel.");
-  is(RecordingsView.selectedItem.attachment, recordings[0],
+  is(getSelectedRecording(panel), recordings[0],
     "The first console recording should still be selected.");
   is(recordings[1].isRecording(), false,
     "The second console recording should no longer be recording.");
 
   yield teardownToolboxAndRemoveTab(panel);
 });
--- a/devtools/client/performance/test/browser_perf-console-record-07.js
+++ b/devtools/client/performance/test/browser_perf-console-record-07.js
@@ -7,25 +7,26 @@
  * most recent console recording, and console.profileEnd() with a label that
  * does not match any pending recordings does nothing.
  */
 
 const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
 const { initPerformanceInTab, initConsoleInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
 const { waitForRecordingStartedEvents, waitForRecordingStoppedEvents } = require("devtools/client/performance/test/helpers/actions");
 const { idleWait } = require("devtools/client/performance/test/helpers/wait-utils");
+const { getSelectedRecording } = require("devtools/client/performance/test/helpers/recording-utils");
 
 add_task(function* () {
   let { target, console } = yield initConsoleInNewTab({
     url: SIMPLE_URL,
     win: window
   });
 
   let { panel } = yield initPerformanceInTab({ tab: target.tab });
-  let { PerformanceController, RecordingsView } = panel.panelWin;
+  let { PerformanceController } = panel.panelWin;
 
   let started = waitForRecordingStartedEvents(panel, {
     // only emitted for manual recordings
     skipWaitingForBackendReady: true
   });
   yield console.profile();
   yield started;
 
@@ -49,21 +50,22 @@ add_task(function* () {
     // the view state won't switch to "console-recording" unless the new
     // in-progress recording is selected, which won't happen
     skipWaitingForViewState: true,
   });
   yield console.profile("2");
   yield started;
 
   let recordings = PerformanceController.getRecordings();
+  let selected = getSelectedRecording(panel);
   is(recordings.length, 3, "Three recordings found in the performance panel.");
   is(recordings[0].getLabel(), "", "Checking label of recording 1");
   is(recordings[1].getLabel(), "1", "Checking label of recording 2");
   is(recordings[2].getLabel(), "2", "Checking label of recording 3");
-  is(RecordingsView.selectedItem.attachment, recordings[0],
+  is(selected, recordings[0],
     "The first console recording should be selected.");
 
   is(recordings[0].isRecording(), true,
     "All recordings should now be started. (1)");
   is(recordings[1].isRecording(), true,
     "All recordings should now be started. (2)");
   is(recordings[2].isRecording(), true,
     "All recordings should now be started. (3)");
@@ -76,35 +78,37 @@ add_task(function* () {
     skipWaitingForSubview: true,
     // the view state won't switch to "recorded" unless the new
     // finished recording is selected, which won't happen
     skipWaitingForViewState: true,
   });
   yield console.profileEnd();
   yield stopped;
 
+  selected = getSelectedRecording(panel);
   recordings = PerformanceController.getRecordings();
   is(recordings.length, 3, "Three recordings found in the performance panel.");
-  is(RecordingsView.selectedItem.attachment, recordings[0],
+  is(selected, recordings[0],
     "The first console recording should still be selected.");
 
   is(recordings[0].isRecording(), true, "The not most recent recording should not stop " +
     "when calling console.profileEnd with no args.");
   is(recordings[1].isRecording(), true, "The not most recent recording should not stop " +
     "when calling console.profileEnd with no args.");
   is(recordings[2].isRecording(), false, "Only the most recent recording should stop " +
     "when calling console.profileEnd with no args.");
 
   info("Trying to `profileEnd` a non-existent console recording.");
   console.profileEnd("fxos");
   yield idleWait(1000);
 
+  selected = getSelectedRecording(panel);
   recordings = PerformanceController.getRecordings();
   is(recordings.length, 3, "Three recordings found in the performance panel.");
-  is(RecordingsView.selectedItem.attachment, recordings[0],
+  is(selected, recordings[0],
     "The first console recording should still be selected.");
 
   is(recordings[0].isRecording(), true,
     "The first recording should not be ended yet.");
   is(recordings[1].isRecording(), true,
     "The second recording should not be ended yet.");
   is(recordings[2].isRecording(), false,
     "The third recording should still be ended.");
@@ -117,38 +121,40 @@ add_task(function* () {
     skipWaitingForSubview: true,
     // the view state won't switch to "recorded" unless the new
     // finished recording is selected, which won't happen
     skipWaitingForViewState: true,
   });
   yield console.profileEnd();
   yield stopped;
 
+  selected = getSelectedRecording(panel);
   recordings = PerformanceController.getRecordings();
   is(recordings.length, 3, "Three recordings found in the performance panel.");
-  is(RecordingsView.selectedItem.attachment, recordings[0],
+  is(selected, recordings[0],
     "The first console recording should still be selected.");
 
   is(recordings[0].isRecording(), true,
     "The first recording should not be ended yet.");
   is(recordings[1].isRecording(), false,
     "The second recording should not be ended yet.");
   is(recordings[2].isRecording(), false,
     "The third recording should still be ended.");
 
   stopped = waitForRecordingStoppedEvents(panel, {
     // only emitted for manual recordings
     skipWaitingForBackendReady: true
   });
   yield console.profileEnd();
   yield stopped;
 
+  selected = getSelectedRecording(panel);
   recordings = PerformanceController.getRecordings();
   is(recordings.length, 3, "Three recordings found in the performance panel.");
-  is(RecordingsView.selectedItem.attachment, recordings[0],
+  is(selected, recordings[0],
     "The first console recording should be selected.");
 
   is(recordings[0].isRecording(), false,
     "All recordings should now be ended. (1)");
   is(recordings[1].isRecording(), false,
     "All recordings should now be ended. (2)");
   is(recordings[2].isRecording(), false,
     "All recordings should now be ended. (3)");
--- a/devtools/client/performance/test/browser_perf-console-record-08.js
+++ b/devtools/client/performance/test/browser_perf-console-record-08.js
@@ -8,104 +8,164 @@
  */
 
 const { Constants } = require("devtools/client/performance/modules/constants");
 const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
 const { initPerformanceInTab, initConsoleInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
 const { startRecording, stopRecording } = require("devtools/client/performance/test/helpers/actions");
 const { waitForRecordingStartedEvents, waitForRecordingStoppedEvents } = require("devtools/client/performance/test/helpers/actions");
 const { once, times } = require("devtools/client/performance/test/helpers/event-utils");
+const { setSelectedRecording } = require("devtools/client/performance/test/helpers/recording-utils");
+
+/**
+ * The following are bit flag constants that are used to represent the state of a
+ * recording.
+ */
+
+// Represents a manually recorded profile, if a user hit the record button.
+const MANUAL = 0;
+// Represents a recorded profile from console.profile().
+const CONSOLE = 1;
+// Represents a profile that is currently recording.
+const RECORDING = 2;
+// Represents a profile that is currently selected.
+const SELECTED = 4;
+
+/**
+ * Utility function to provide a meaningful inteface for testing that the bits
+ * match for the recording state.
+ * @param {integer} expected - The expected bit values packed in an integer.
+ * @param {integer} actual - The actual bit values packed in an integer.
+ */
+function hasBitFlag(expected, actual) {
+  return !!(expected & actual);
+}
 
 add_task(function* () {
   // This test seems to take a very long time to finish on Linux VMs.
   requestLongerTimeout(4);
 
   let { target, console } = yield initConsoleInNewTab({
     url: SIMPLE_URL,
     win: window
   });
 
   let { panel } = yield initPerformanceInTab({ tab: target.tab });
-  let { EVENTS, PerformanceController, RecordingsView, OverviewView } = panel.panelWin;
+  let { EVENTS, PerformanceController, OverviewView } = panel.panelWin;
 
-  info("Starting console.profile()...");
+  info("Recording 1 - Starting console.profile()...");
   let started = waitForRecordingStartedEvents(panel, {
     // only emitted for manual recordings
     skipWaitingForBackendReady: true
   });
   yield console.profile("rust");
   yield started;
-  testRecordings(PerformanceController, [C + S + R]);
+  testRecordings(PerformanceController, [
+    CONSOLE + SELECTED + RECORDING
+  ]);
 
-  info("Starting manual recording...");
+  info("Recording 2 - Starting manual recording...");
   yield startRecording(panel);
-  testRecordings(PerformanceController, [C + R, R + S]);
+  testRecordings(PerformanceController, [
+    CONSOLE + RECORDING,
+    MANUAL + RECORDING + SELECTED
+  ]);
 
-  info("Starting console.profile(\"3\")...");
+  info("Recording 3 - Starting console.profile(\"3\")...");
   started = waitForRecordingStartedEvents(panel, {
     // only emitted for manual recordings
     skipWaitingForBackendReady: true,
     // only emitted when an in-progress recording is selected
     skipWaitingForOverview: true,
     // the view state won't switch to "console-recording" unless the new
     // in-progress recording is selected, which won't happen
     skipWaitingForViewState: true,
   });
   yield console.profile("3");
   yield started;
-  testRecordings(PerformanceController, [C + R, R + S, C + R]);
+  testRecordings(PerformanceController, [
+    CONSOLE + RECORDING,
+    MANUAL + RECORDING + SELECTED,
+    CONSOLE + RECORDING
+  ]);
 
-  info("Starting console.profile(\"4\")...");
+  info("Recording 4 - Starting console.profile(\"4\")...");
   started = waitForRecordingStartedEvents(panel, {
     // only emitted for manual recordings
     skipWaitingForBackendReady: true,
     // only emitted when an in-progress  recording is selected
     skipWaitingForOverview: true,
     // the view state won't switch to "console-recording" unless the new
     // in-progress recording is selected, which won't happen
     skipWaitingForViewState: true,
   });
   yield console.profile("4");
   yield started;
-  testRecordings(PerformanceController, [C + R, R + S, C + R, C + R]);
+  testRecordings(PerformanceController, [
+    CONSOLE + RECORDING,
+    MANUAL + RECORDING + SELECTED,
+    CONSOLE + RECORDING,
+    CONSOLE + RECORDING
+  ]);
 
-  info("Ending console.profileEnd()...");
+  info("Recording 4 - Ending console.profileEnd()...");
   let stopped = waitForRecordingStoppedEvents(panel, {
     // only emitted for manual recordings
     skipWaitingForBackendReady: true,
     // only emitted when a finished recording is selected
     skipWaitingForOverview: true,
     skipWaitingForSubview: true,
     // the view state won't switch to "recorded" unless the new
     // finished recording is selected, which won't happen
     skipWaitingForViewState: true,
   });
   yield console.profileEnd();
   yield stopped;
-  testRecordings(PerformanceController, [C + R, R + S, C + R, C]);
+  testRecordings(PerformanceController, [
+    CONSOLE + RECORDING,
+    MANUAL + RECORDING + SELECTED,
+    CONSOLE + RECORDING,
+    CONSOLE
+  ]);
 
-  info("Select last recording...");
+  info("Recording 4 - Select last recording...");
   let recordingSelected = once(PerformanceController, EVENTS.RECORDING_SELECTED);
-  RecordingsView.selectedIndex = 3;
+  setSelectedRecording(panel, 3);
   yield recordingSelected;
-  testRecordings(PerformanceController, [C + R, R, C + R, C + S]);
+  testRecordings(PerformanceController, [
+    CONSOLE + RECORDING,
+    MANUAL + RECORDING,
+    CONSOLE + RECORDING,
+    CONSOLE + SELECTED
+  ]);
   ok(!OverviewView.isRendering(),
     "Stop rendering overview when a completed recording is selected.");
 
-  info("Stop manual recording...");
+  info("Recording 2 - Stop manual recording.");
+
   yield stopRecording(panel);
-  testRecordings(PerformanceController, [C + R, S, C + R, C]);
+  testRecordings(PerformanceController, [
+    CONSOLE + RECORDING,
+    MANUAL + SELECTED,
+    CONSOLE + RECORDING,
+    CONSOLE
+  ]);
   ok(!OverviewView.isRendering(),
     "Stop rendering overview when a completed recording is selected.");
 
-  info("Select first recording...");
+  info("Recording 1 - Select first recording.");
   recordingSelected = once(PerformanceController, EVENTS.RECORDING_SELECTED);
-  RecordingsView.selectedIndex = 0;
+  setSelectedRecording(panel, 0);
   yield recordingSelected;
-  testRecordings(PerformanceController, [C + R + S, 0, C + R, C]);
+  testRecordings(PerformanceController, [
+    CONSOLE + RECORDING + SELECTED,
+    MANUAL,
+    CONSOLE + RECORDING,
+    CONSOLE
+  ]);
   ok(OverviewView.isRendering(),
     "Should be rendering overview a recording in progress is selected.");
 
   // Ensure overview is still rendering.
   yield times(OverviewView, EVENTS.UI_OVERVIEW_RENDERED, 3, {
     expectedArgs: { "1": Constants.FRAMERATE_GRAPH_LOW_RES_INTERVAL }
   });
 
@@ -117,75 +177,92 @@ add_task(function* () {
     skipWaitingForOverview: true,
     skipWaitingForSubview: true,
     // the view state won't switch to "recorded" unless the new
     // finished recording is selected, which won't happen
     skipWaitingForViewState: true,
   });
   yield console.profileEnd();
   yield stopped;
-  testRecordings(PerformanceController, [C + R + S, 0, C, C]);
+  testRecordings(PerformanceController, [
+    CONSOLE + RECORDING + SELECTED,
+    MANUAL,
+    CONSOLE,
+    CONSOLE
+  ]);
   ok(OverviewView.isRendering(),
     "Should be rendering overview a recording in progress is selected.");
 
   // Ensure overview is still rendering.
   yield times(OverviewView, EVENTS.UI_OVERVIEW_RENDERED, 3, {
     expectedArgs: { "1": Constants.FRAMERATE_GRAPH_LOW_RES_INTERVAL }
   });
 
-  info("Start one more manual recording...");
+  info("Recording 5 - Start one more manual recording.");
   yield startRecording(panel);
-  testRecordings(PerformanceController, [C + R, 0, C, C, R + S]);
+  testRecordings(PerformanceController, [
+    CONSOLE + RECORDING,
+    MANUAL,
+    CONSOLE,
+    CONSOLE,
+    MANUAL + RECORDING + SELECTED
+  ]);
   ok(OverviewView.isRendering(),
     "Should be rendering overview a recording in progress is selected.");
 
   // Ensure overview is still rendering.
   yield times(OverviewView, EVENTS.UI_OVERVIEW_RENDERED, 3, {
     expectedArgs: { "1": Constants.FRAMERATE_GRAPH_LOW_RES_INTERVAL }
   });
 
-  info("Stop manual recording...");
+  info("Recording 5 - Stop manual recording.");
   yield stopRecording(panel);
-  testRecordings(PerformanceController, [C + R, 0, C, C, S]);
+  testRecordings(PerformanceController, [
+    CONSOLE + RECORDING,
+    MANUAL,
+    CONSOLE,
+    CONSOLE,
+    MANUAL + SELECTED
+  ]);
   ok(!OverviewView.isRendering(),
   "Stop rendering overview when a completed recording is selected.");
 
-  info("Ending console.profileEnd()...");
+  info("Recording 1 - Ending console.profileEnd()...");
   stopped = waitForRecordingStoppedEvents(panel, {
     // only emitted for manual recordings
     skipWaitingForBackendReady: true,
     // only emitted when a finished recording is selected
     skipWaitingForOverview: true,
     skipWaitingForSubview: true,
     // the view state won't switch to "recorded" unless the new
     // in-progress recording is selected, which won't happen
     skipWaitingForViewState: true,
   });
   yield console.profileEnd();
   yield stopped;
-  testRecordings(PerformanceController, [C, 0, C, C, S]);
+  testRecordings(PerformanceController, [
+    CONSOLE,
+    MANUAL,
+    CONSOLE,
+    CONSOLE,
+    MANUAL + SELECTED
+  ]);
   ok(!OverviewView.isRendering(),
     "Stop rendering overview when a completed recording is selected.");
 
   yield teardownToolboxAndRemoveTab(panel);
 });
 
-// is console
-const C = 1;
-// is recording
-const R = 2;
-// is selected
-const S = 4;
-
-function testRecordings(controller, expected) {
+function testRecordings(controller, expectedBitFlags) {
   let recordings = controller.getRecordings();
   let current = controller.getCurrentRecording();
-  is(recordings.length, expected.length, "Expected number of recordings.");
+  is(recordings.length, expectedBitFlags.length, "Expected number of recordings.");
 
   recordings.forEach((recording, i) => {
-    ok(recording.isConsole() == !!(expected[i] & C),
+    const expected = expectedBitFlags[i];
+    is(recording.isConsole(), hasBitFlag(expected, CONSOLE),
       `Recording ${i + 1} has expected console state.`);
-    ok(recording.isRecording() == !!(expected[i] & R),
+    is(recording.isRecording(), hasBitFlag(expected, RECORDING),
       `Recording ${i + 1} has expected console state.`);
-    ok((recording == current) == !!(expected[i] & S),
+    is((recording == current), hasBitFlag(expected, SELECTED),
       `Recording ${i + 1} has expected selected state.`);
   });
 }
--- a/devtools/client/performance/test/browser_perf-details-03-without-allocations.js
+++ b/devtools/client/performance/test/browser_perf-details-03-without-allocations.js
@@ -9,27 +9,27 @@
  * to a default panel instead.
  */
 
 const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
 const { UI_ENABLE_ALLOCATIONS_PREF } = require("devtools/client/performance/test/helpers/prefs");
 const { initPerformanceInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
 const { startRecording, stopRecording } = require("devtools/client/performance/test/helpers/actions");
 const { once } = require("devtools/client/performance/test/helpers/event-utils");
+const { setSelectedRecording } = require("devtools/client/performance/test/helpers/recording-utils");
 
 add_task(function* () {
   let { panel } = yield initPerformanceInNewTab({
     url: SIMPLE_URL,
     win: window
   });
 
   let {
     EVENTS,
     $,
-    RecordingsView,
     DetailsView,
     WaterfallView,
     MemoryCallTreeView,
     MemoryFlameGraphView
   } = panel.panelWin;
 
   let flameBtn = $("toolbarbutton[data-view='memory-flamegraph']");
   let callBtn = $("toolbarbutton[data-view='memory-calltree']");
@@ -75,31 +75,31 @@ add_task(function* () {
   yield rendered;
 
   ok(DetailsView.isViewSelected(MemoryFlameGraphView),
     "The memory flamegraph view can now be selected.");
 
   // Select the first recording with no memory data.
   selected = once(DetailsView, EVENTS.UI_DETAILS_VIEW_SELECTED);
   rendered = once(WaterfallView, EVENTS.UI_WATERFALL_RENDERED);
-  RecordingsView.selectedIndex = 0;
+  setSelectedRecording(panel, 0);
   yield selected;
   yield rendered;
 
   ok(DetailsView.isViewSelected(WaterfallView), "The waterfall view is now selected " +
     "when switching back to a recording that does not have memory data.");
 
   is(callBtn.hidden, true,
     "The `memory-calltree` button is hidden when recording has no memory data.");
   is(flameBtn.hidden, true,
     "The `memory-flamegraph` button is hidden when recording has no memory data.");
 
   // Go back to the recording with memory data.
   rendered = once(WaterfallView, EVENTS.UI_WATERFALL_RENDERED);
-  RecordingsView.selectedIndex = 1;
+  setSelectedRecording(panel, 1);
   yield rendered;
 
   ok(DetailsView.isViewSelected(WaterfallView),
     "The waterfall view is still selected in the details view.");
 
   is(callBtn.hidden, false,
     "The `memory-calltree` button is shown when recording has memory data.");
   is(flameBtn.hidden, false,
--- a/devtools/client/performance/test/browser_perf-details-04-toolbar-buttons.js
+++ b/devtools/client/performance/test/browser_perf-details-04-toolbar-buttons.js
@@ -6,28 +6,28 @@
  * Tests that the details view hides the toolbar buttons when a recording
  * doesn't exist or is in progress.
  */
 
 const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
 const { initPerformanceInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
 const { startRecording, stopRecording } = require("devtools/client/performance/test/helpers/actions");
 const { once } = require("devtools/client/performance/test/helpers/event-utils");
+const { setSelectedRecording, getSelectedRecordingIndex } = require("devtools/client/performance/test/helpers/recording-utils");
 
 add_task(function* () {
   let { panel } = yield initPerformanceInNewTab({
     url: SIMPLE_URL,
     win: window
   });
 
   let {
     EVENTS,
     $,
     PerformanceController,
-    RecordingsView,
     WaterfallView
   } = panel.panelWin;
 
   let waterfallBtn = $("toolbarbutton[data-view='waterfall']");
   let jsFlameBtn = $("toolbarbutton[data-view='js-flamegraph']");
   let jsCallBtn = $("toolbarbutton[data-view='js-calltree']");
   let memFlameBtn = $("toolbarbutton[data-view='memory-flamegraph']");
   let memCallBtn = $("toolbarbutton[data-view='memory-calltree']");
@@ -79,39 +79,41 @@ add_task(function* () {
     "The `js-calltree` button is hidden when another recording starts.");
   is(memFlameBtn.hidden, true,
     "The `memory-flamegraph` button is hidden when another recording starts.");
   is(memCallBtn.hidden, true,
     "The `memory-calltree` button is hidden when another recording starts.");
 
   let selected = once(PerformanceController, EVENTS.RECORDING_SELECTED);
   let rendered = once(WaterfallView, EVENTS.UI_WATERFALL_RENDERED);
-  RecordingsView.selectedIndex = 0;
+  setSelectedRecording(panel, 0);
   yield selected;
   yield rendered;
 
-  is(RecordingsView.selectedIndex, 0,
+  let selectedIndex = getSelectedRecordingIndex(panel);
+  is(selectedIndex, 0,
     "The first recording was selected again.");
 
   is(waterfallBtn.hidden, false,
     "The `waterfall` button is visible when first recording selected.");
   is(jsFlameBtn.hidden, false,
     "The `js-flamegraph` button is visible when first recording selected.");
   is(jsCallBtn.hidden, false,
     "The `js-calltree` button is visible when first recording selected.");
   is(memFlameBtn.hidden, true,
     "The `memory-flamegraph` button is hidden when first recording selected.");
   is(memCallBtn.hidden, true,
     "The `memory-calltree` button is hidden when first recording selected.");
 
   selected = once(PerformanceController, EVENTS.RECORDING_SELECTED);
-  RecordingsView.selectedIndex = 1;
+  setSelectedRecording(panel, 1);
   yield selected;
 
-  is(RecordingsView.selectedIndex, 1,
+  selectedIndex = getSelectedRecordingIndex(panel);
+  is(selectedIndex, 1,
     "The second recording was selected again.");
 
   is(waterfallBtn.hidden, true,
     "The `waterfall button` still is hidden when second recording selected.");
   is(jsFlameBtn.hidden, true,
     "The `js-flamegraph button` still is hidden when second recording selected.");
   is(jsCallBtn.hidden, true,
     "The `js-calltree button` still is hidden when second recording selected.");
@@ -119,17 +121,18 @@ add_task(function* () {
     "The `memory-flamegraph button` still is hidden when second recording selected.");
   is(memCallBtn.hidden, true,
     "The `memory-calltree button` still is hidden when second recording selected.");
 
   rendered = once(WaterfallView, EVENTS.UI_WATERFALL_RENDERED);
   yield stopRecording(panel);
   yield rendered;
 
-  is(RecordingsView.selectedIndex, 1,
+  selectedIndex = getSelectedRecordingIndex(panel);
+  is(selectedIndex, 1,
     "The second recording is still selected.");
 
   is(waterfallBtn.hidden, false,
     "The `waterfall` button is visible when second recording finished.");
   is(jsFlameBtn.hidden, false,
     "The `js-flamegraph` button is visible when second recording finished.");
   is(jsCallBtn.hidden, false,
     "The `js-calltree` button is visible when second recording finished.");
--- a/devtools/client/performance/test/browser_perf-loading-01.js
+++ b/devtools/client/performance/test/browser_perf-loading-01.js
@@ -6,46 +6,47 @@
  * Tests that the recordings view shows the right label while recording, after
  * recording, and once the record has loaded.
  */
 
 const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
 const { initPerformanceInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
 const { startRecording, stopRecording } = require("devtools/client/performance/test/helpers/actions");
 const { once } = require("devtools/client/performance/test/helpers/event-utils");
+const { getSelectedRecording, getDurationLabelText } = require("devtools/client/performance/test/helpers/recording-utils");
 
 add_task(function* () {
   let { panel } = yield initPerformanceInNewTab({
     url: SIMPLE_URL,
     win: window
   });
 
-  let { EVENTS, L10N, $, PerformanceController, RecordingsView } = panel.panelWin;
+  let { EVENTS, L10N, PerformanceController } = panel.panelWin;
 
   yield startRecording(panel);
 
-  let durationLabel = $(".recording-item-duration", RecordingsView.selectedItem.target);
-  is(durationLabel.getAttribute("value"),
+  is(getDurationLabelText(panel, 0),
     L10N.getStr("recordingsList.recordingLabel"),
     "The duration node should show the 'recording' message while recording");
 
   let recordingStopping = once(PerformanceController, EVENTS.RECORDING_STATE_CHANGE, {
     expectedArgs: { "1": "recording-stopping" }
   });
   let recordingStopped = once(PerformanceController, EVENTS.RECORDING_STATE_CHANGE, {
     expectedArgs: { "1": "recording-stopped" }
   });
   let everythingStopped = stopRecording(panel);
 
   yield recordingStopping;
-  is(durationLabel.getAttribute("value"),
+  is(getDurationLabelText(panel, 0),
     L10N.getStr("recordingsList.loadingLabel"),
     "The duration node should show the 'loading' message while stopping");
 
   yield recordingStopped;
-  is(durationLabel.getAttribute("value"),
+  const selected = getSelectedRecording(panel);
+  is(getDurationLabelText(panel, 0),
     L10N.getFormatStr("recordingsList.durationLabel",
-    RecordingsView.selectedItem.attachment.getDuration().toFixed(0)),
+    selected.getDuration().toFixed(0)),
     "The duration node should show the duration after the record has stopped");
 
   yield everythingStopped;
   yield teardownToolboxAndRemoveTab(panel);
 });
--- a/devtools/client/performance/test/browser_perf-loading-02.js
+++ b/devtools/client/performance/test/browser_perf-loading-02.js
@@ -8,24 +8,25 @@
  * Also test that the details view isn't locked if the recording that is being
  * stopped isn't the active one.
  */
 
 const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
 const { initPerformanceInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
 const { startRecording, stopRecording } = require("devtools/client/performance/test/helpers/actions");
 const { once } = require("devtools/client/performance/test/helpers/event-utils");
+const { getSelectedRecordingIndex, setSelectedRecording } = require("devtools/client/performance/test/helpers/recording-utils");
 
 add_task(function* () {
   let { panel } = yield initPerformanceInNewTab({
     url: SIMPLE_URL,
     win: window
   });
 
-  let { EVENTS, $, PerformanceController, RecordingsView } = panel.panelWin;
+  let { EVENTS, $, PerformanceController } = panel.panelWin;
   let detailsContainer = $("#details-pane-container");
   let recordingNotice = $("#recording-notice");
   let loadingNotice = $("#loading-notice");
   let detailsPane = $("#details-pane");
 
   yield startRecording(panel);
 
   is(detailsContainer.selectedPanel, recordingNotice,
@@ -47,35 +48,35 @@ add_task(function* () {
   is(detailsContainer.selectedPanel, detailsPane,
     "The details panel is shown after the record has stopped.");
 
   yield everythingStopped;
   yield startRecording(panel);
 
   info("While the 2nd record is still going, switch to the first one.");
   let recordingSelected = once(PerformanceController, EVENTS.RECORDING_SELECTED);
-  RecordingsView.selectedIndex = 0;
+  setSelectedRecording(panel, 0);
   yield recordingSelected;
 
   recordingStopping = once(PerformanceController, EVENTS.RECORDING_STATE_CHANGE, {
     expectedArgs: { "1": "recording-stopping" }
   });
   recordingStopped = once(PerformanceController, EVENTS.RECORDING_STATE_CHANGE, {
     expectedArgs: { "1": "recording-stopped" }
   });
   everythingStopped = stopRecording(panel);
 
   yield recordingStopping;
   is(detailsContainer.selectedPanel, detailsPane,
     "The details panel is still shown while the 2nd record is being stopped.");
-  is(RecordingsView.selectedIndex, 0,
+  is(getSelectedRecordingIndex(panel), 0,
     "The first record is still selected.");
 
   yield recordingStopped;
 
   is(detailsContainer.selectedPanel, detailsPane,
     "The details panel is still shown after the 2nd record has stopped.");
-  is(RecordingsView.selectedIndex, 1,
+  is(getSelectedRecordingIndex(panel), 1,
     "The second record is now selected.");
 
   yield everythingStopped;
   yield teardownToolboxAndRemoveTab(panel);
 });
--- a/devtools/client/performance/test/browser_perf-options-show-jit-optimizations.js
+++ b/devtools/client/performance/test/browser_perf-options-show-jit-optimizations.js
@@ -4,23 +4,23 @@
 /* eslint-disable */
 // Bug 1235788, increase time out of this test
 requestLongerTimeout(2);
 
 /**
  * Tests that the JIT Optimizations view renders optimization data
  * if on, and displays selected frames on focus.
  */
-
+ const { setSelectedRecording } = require("devtools/client/performance/test/helpers/recording-utils");
 Services.prefs.setBoolPref(INVERT_PREF, false);
 
 function* spawnTest() {
   let { panel } = yield initPerformance(SIMPLE_URL);
   let { EVENTS, $, $$, window, PerformanceController } = panel.panelWin;
-  let { OverviewView, DetailsView, OptimizationsListView, JsCallTreeView, RecordingsView } = panel.panelWin;
+  let { OverviewView, DetailsView, OptimizationsListView, JsCallTreeView } = panel.panelWin;
 
   let profilerData = { threads: [gThread] };
 
   is(Services.prefs.getBoolPref(JIT_PREF), false, "record JIT Optimizations pref off by default");
   Services.prefs.setBoolPref(JIT_PREF, true);
   is(Services.prefs.getBoolPref(JIT_PREF), true, "toggle on record JIT Optimizations");
 
   // Make two recordings, so we have one to switch to later, as the
@@ -53,24 +53,24 @@ function* spawnTest() {
   let rendered = once(JsCallTreeView, "focus");
   mousedown(window, $$(".call-tree-item")[2]);
   yield rendered;
   let isHidden = $("#jit-optimizations-view").classList.contains("hidden");
   ok(!isHidden, "opts view should be visible when selecting a frame with opts");
 
   let select = once(PerformanceController, EVENTS.RECORDING_SELECTED);
   rendered = once(JsCallTreeView, EVENTS.UI_JS_CALL_TREE_RENDERED);
-  RecordingsView.selectedIndex = 0;
+  setSelectedRecording(panel, 0);
   yield Promise.all([select, rendered]);
 
   isHidden = $("#jit-optimizations-view").classList.contains("hidden");
   ok(isHidden, "opts view is hidden when switching recordings");
 
   rendered = once(JsCallTreeView, EVENTS.UI_JS_CALL_TREE_RENDERED);
-  RecordingsView.selectedIndex = 1;
+  setSelectedRecording(panel, 1);
   yield rendered;
 
   rendered = once(JsCallTreeView, "focus");
   mousedown(window, $$(".call-tree-item")[2]);
   yield rendered;
   isHidden = $("#jit-optimizations-view").classList.contains("hidden");
   ok(!isHidden, "opts view should be visible when selecting a frame with opts");
 
--- a/devtools/client/performance/test/browser_perf-overview-render-04.js
+++ b/devtools/client/performance/test/browser_perf-overview-render-04.js
@@ -8,24 +8,25 @@
  */
 
 const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
 const { UI_ENABLE_MEMORY_PREF } = require("devtools/client/performance/test/helpers/prefs");
 const { initPerformanceInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
 const { startRecording, stopRecording } = require("devtools/client/performance/test/helpers/actions");
 const { waitUntil } = require("devtools/client/performance/test/helpers/wait-utils");
 const { isVisible } = require("devtools/client/performance/test/helpers/dom-utils");
+const { setSelectedRecording } = require("devtools/client/performance/test/helpers/recording-utils");
 
 add_task(function* () {
   let { panel } = yield initPerformanceInNewTab({
     url: SIMPLE_URL,
     win: window
   });
 
-  let { $, EVENTS, PerformanceController, RecordingsView, OverviewView } = panel.panelWin;
+  let { $, EVENTS, PerformanceController, OverviewView } = panel.panelWin;
 
   // Enable memory to test.
   Services.prefs.setBoolPref(UI_ENABLE_MEMORY_PREF, true);
 
   // Set realtime rendering off.
   OverviewView.isRealtimeRenderingEnabled = () => false;
 
   let updated = 0;
@@ -48,22 +49,22 @@ add_task(function* () {
   is(updated, 1, "Overview graphs rendered upon completion.");
 
   yield startRecording(panel, { skipWaitingForOverview: true });
 
   is(isVisible($("#overview-pane")), false,
      "Overview graphs hidden again when starting new recording.");
   is(updated, 1, "Overview graphs have not been updated again.");
 
-  RecordingsView.selectedIndex = 0;
+  setSelectedRecording(panel, 0);
   is(isVisible($("#overview-pane")), true,
      "Overview graphs no longer hidden when switching back to complete recording.");
   is(updated, 1, "Overview graphs have not been updated again.");
 
-  RecordingsView.selectedIndex = 1;
+  setSelectedRecording(panel, 1);
   is(isVisible($("#overview-pane")), false,
      "Overview graphs hidden again when going back to inprogress recording.");
   is(updated, 1, "Overview graphs have not been updated again.");
 
   yield stopRecording(panel);
 
   is(isVisible($("#overview-pane")), true,
      "overview graphs no longer hidden when recording finishes");
--- a/devtools/client/performance/test/browser_perf-recording-notices-02.js
+++ b/devtools/client/performance/test/browser_perf-recording-notices-02.js
@@ -6,29 +6,29 @@
  * Tests that the recording notice panes are toggled when going between
  * a completed recording and an in-progress recording.
  */
 
 const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
 const { initPerformanceInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
 const { startRecording, stopRecording } = require("devtools/client/performance/test/helpers/actions");
 const { once } = require("devtools/client/performance/test/helpers/event-utils");
+const { setSelectedRecording } = require("devtools/client/performance/test/helpers/recording-utils");
 
 add_task(function* () {
   let { panel } = yield initPerformanceInNewTab({
     url: SIMPLE_URL,
     win: window
   });
 
   let {
     EVENTS,
     $,
     PerformanceController,
     PerformanceView,
-    RecordingsView
   } = panel.panelWin;
 
   let MAIN_CONTAINER = $("#performance-view");
   let CONTENT = $("#performance-view-content");
   let DETAILS_CONTAINER = $("#details-pane-container");
   let RECORDING = $("#recording-notice");
   let DETAILS = $("#details-pane");
 
@@ -37,26 +37,26 @@ add_task(function* () {
 
   yield startRecording(panel);
 
   is(PerformanceView.getState(), "recording", "Correct state during recording.");
   is(MAIN_CONTAINER.selectedPanel, CONTENT, "Showing main view with timeline.");
   is(DETAILS_CONTAINER.selectedPanel, RECORDING, "Showing recording panel.");
 
   let selected = once(PerformanceController, EVENTS.RECORDING_SELECTED);
-  RecordingsView.selectedIndex = 0;
+  setSelectedRecording(panel, 0);
   yield selected;
 
   is(PerformanceView.getState(), "recorded",
      "Correct state during recording but selecting a completed recording.");
   is(MAIN_CONTAINER.selectedPanel, CONTENT, "Showing main view with timeline.");
   is(DETAILS_CONTAINER.selectedPanel, DETAILS, "Showing recorded panel.");
 
   selected = once(PerformanceController, EVENTS.RECORDING_SELECTED);
-  RecordingsView.selectedIndex = 1;
+  setSelectedRecording(panel, 1);
   yield selected;
 
   is(PerformanceView.getState(), "recording",
      "Correct state when switching back to recording in progress.");
   is(MAIN_CONTAINER.selectedPanel, CONTENT, "Showing main view with timeline.");
   is(DETAILS_CONTAINER.selectedPanel, RECORDING, "Showing recording panel.");
 
   yield stopRecording(panel);
--- a/devtools/client/performance/test/browser_perf-recording-notices-03.js
+++ b/devtools/client/performance/test/browser_perf-recording-notices-03.js
@@ -10,16 +10,17 @@
 
 const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
 const { PROFILER_BUFFER_SIZE_PREF } = require("devtools/client/performance/test/helpers/prefs");
 const { pmmLoadFrameScripts, pmmStopProfiler, pmmClearFrameScripts } = require("devtools/client/performance/test/helpers/profiler-mm-utils");
 const { initPerformanceInTab, initConsoleInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
 const { startRecording, stopRecording } = require("devtools/client/performance/test/helpers/actions");
 const { waitUntil } = require("devtools/client/performance/test/helpers/wait-utils");
 const { once } = require("devtools/client/performance/test/helpers/event-utils");
+const { setSelectedRecording } = require("devtools/client/performance/test/helpers/recording-utils");
 
 add_task(function* () {
   // Make sure the profiler module is stopped so we can set a new buffer limit.
   pmmLoadFrameScripts(gBrowser);
   yield pmmStopProfiler();
 
   // Keep the profiler's buffer large, but still get to 1% relatively quick.
   Services.prefs.setIntPref(PROFILER_BUFFER_SIZE_PREF, 1000000);
@@ -31,17 +32,16 @@ add_task(function* () {
 
   let { panel } = yield initPerformanceInTab({ tab: target.tab });
   let {
     gFront,
     EVENTS,
     $,
     PerformanceController,
     PerformanceView,
-    RecordingsView
   } = panel.panelWin;
 
   // Set a fast profiler-status update interval.
   yield gFront.setProfilerStatusInterval(10);
 
   let DETAILS_CONTAINER = $("#details-pane-container");
   let NORMAL_BUFFER_STATUS_MESSAGE = $("#recording-notice .buffer-status-message");
   let CONSOLE_BUFFER_STATUS_MESSAGE =
@@ -83,17 +83,17 @@ add_task(function* () {
     PerformanceController.getCurrentRecording());
   either(DETAILS_CONTAINER.getAttribute("buffer-status"), "in-progress", "full",
     "Container has [buffer-status=in-progress] or [buffer-status=full].");
   ok(NORMAL_BUFFER_STATUS_MESSAGE.value.indexOf(gPercent + "%") !== -1,
     "Buffer status text has correct percentage.");
 
   // Select the console recording.
   let selected = once(PerformanceController, EVENTS.RECORDING_SELECTED);
-  RecordingsView.selectedIndex = 1;
+  setSelectedRecording(panel, 1);
   yield selected;
 
   yield waitUntil(function* () {
     [, gPercent] = yield once(PerformanceView,
                               EVENTS.UI_RECORDING_PROFILER_STATUS_RENDERED,
                               { spreadArgs: true });
     return gPercent > 0;
   });
@@ -104,17 +104,17 @@ add_task(function* () {
     "Container has [buffer-status=in-progress] or [buffer-status=full].");
   ok(CONSOLE_BUFFER_STATUS_MESSAGE.value.indexOf(gPercent + "%") !== -1,
     "Buffer status text has correct percentage for console recording.");
 
   // Stop the console profile, then select the original manual recording.
   yield console.profileEnd("rust");
 
   selected = once(PerformanceController, EVENTS.RECORDING_SELECTED);
-  RecordingsView.selectedIndex = 0;
+  setSelectedRecording(panel, 0);
   yield selected;
 
   yield waitUntil(function* () {
     [, gPercent] = yield once(PerformanceView,
                               EVENTS.UI_RECORDING_PROFILER_STATUS_RENDERED,
                               { spreadArgs: true });
     return gPercent > Math.floor(bufferUsage * 100);
   });
--- a/devtools/client/performance/test/browser_perf-recording-selected-01.js
+++ b/devtools/client/performance/test/browser_perf-recording-selected-01.js
@@ -6,39 +6,40 @@
  * Tests if the profiler correctly handles multiple recordings and can
  * successfully switch between them.
  */
 
 const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
 const { initPerformanceInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
 const { startRecording, stopRecording } = require("devtools/client/performance/test/helpers/actions");
 const { once } = require("devtools/client/performance/test/helpers/event-utils");
+const { setSelectedRecording, getRecordingsCount, getSelectedRecordingIndex } = require("devtools/client/performance/test/helpers/recording-utils");
 
 add_task(function* () {
   let { panel } = yield initPerformanceInNewTab({
     url: SIMPLE_URL,
     win: window
   });
 
-  let { EVENTS, PerformanceController, RecordingsView } = panel.panelWin;
+  let { EVENTS, PerformanceController } = panel.panelWin;
 
   yield startRecording(panel);
   yield stopRecording(panel);
 
   yield startRecording(panel);
   yield stopRecording(panel);
 
-  is(RecordingsView.itemCount, 2,
+  is(getRecordingsCount(panel), 2,
     "There should be two recordings visible.");
-  is(RecordingsView.selectedIndex, 1,
+  is(getSelectedRecordingIndex(panel), 1,
     "The second recording item should be selected.");
 
   let selected = once(PerformanceController, EVENTS.RECORDING_SELECTED);
-  RecordingsView.selectedIndex = 0;
+  setSelectedRecording(panel, 0);
   yield selected;
 
-  is(RecordingsView.itemCount, 2,
+  is(getRecordingsCount(panel), 2,
     "There should still be two recordings visible.");
-  is(RecordingsView.selectedIndex, 0,
+  is(getSelectedRecordingIndex(panel), 0,
     "The first recording item should be selected.");
 
   yield teardownToolboxAndRemoveTab(panel);
 });
--- a/devtools/client/performance/test/browser_perf-recording-selected-02.js
+++ b/devtools/client/performance/test/browser_perf-recording-selected-02.js
@@ -6,53 +6,53 @@
  * Tests if the profiler correctly handles multiple recordings and can
  * successfully switch between them, even when one of them is in progress.
  */
 
 const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
 const { initPerformanceInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
 const { startRecording, stopRecording } = require("devtools/client/performance/test/helpers/actions");
 const { once } = require("devtools/client/performance/test/helpers/event-utils");
+const { getSelectedRecordingIndex, setSelectedRecording, getRecordingsCount } = require("devtools/client/performance/test/helpers/recording-utils");
 
 add_task(function* () {
   // This test seems to take a very long time to finish on Linux VMs.
   requestLongerTimeout(4);
 
   let { panel } = yield initPerformanceInNewTab({
     url: SIMPLE_URL,
     win: window
   });
 
-  let { EVENTS, PerformanceController, RecordingsView } = panel.panelWin;
+  let { EVENTS, PerformanceController } = panel.panelWin;
 
   yield startRecording(panel);
   yield stopRecording(panel);
 
   yield startRecording(panel);
 
-  is(RecordingsView.itemCount, 2,
+  is(getRecordingsCount(panel), 2,
     "There should be two recordings visible.");
-  is(RecordingsView.selectedIndex, 1,
+  is(getSelectedRecordingIndex(panel), 1,
     "The new recording item should be selected.");
 
   let selected = once(PerformanceController, EVENTS.RECORDING_SELECTED);
-  RecordingsView.selectedIndex = 0;
+  setSelectedRecording(panel, 0);
   yield selected;
 
-  is(RecordingsView.itemCount, 2,
+  is(getRecordingsCount(panel), 2,
     "There should still be two recordings visible.");
-  is(RecordingsView.selectedIndex, 0,
+  is(getSelectedRecordingIndex(panel), 0,
     "The first recording item should be selected now.");
 
   selected = once(PerformanceController, EVENTS.RECORDING_SELECTED);
-  RecordingsView.selectedIndex = 1;
+  setSelectedRecording(panel, 1);
   yield selected;
 
-  is(RecordingsView.itemCount, 2,
+  is(getRecordingsCount(panel), 2,
     "There should still be two recordings visible.");
-  is(RecordingsView.selectedIndex, 1,
+  is(getSelectedRecordingIndex(panel), 1,
     "The second recording item should be selected again.");
 
   yield stopRecording(panel);
 
   yield teardownToolboxAndRemoveTab(panel);
 });
-
--- a/devtools/client/performance/test/browser_perf-recording-selected-03.js
+++ b/devtools/client/performance/test/browser_perf-recording-selected-03.js
@@ -7,34 +7,35 @@
  * Tests if the profiler UI does not forget that recording is active when
  * selected recording changes.
  */
 
 const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
 const { initPerformanceInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
 const { startRecording, stopRecording } = require("devtools/client/performance/test/helpers/actions");
 const { once } = require("devtools/client/performance/test/helpers/event-utils");
+const { setSelectedRecording } = require("devtools/client/performance/test/helpers/recording-utils");
 
 add_task(function* () {
   let { panel } = yield initPerformanceInNewTab({
     url: SIMPLE_URL,
     win: window
   });
 
-  let { $, EVENTS, PerformanceController, RecordingsView } = panel.panelWin;
+  let { $, EVENTS, PerformanceController } = panel.panelWin;
 
   yield startRecording(panel);
   yield stopRecording(panel);
 
   yield startRecording(panel);
 
   info("Selecting recording #0 and waiting for it to be displayed.");
 
   let selected = once(PerformanceController, EVENTS.RECORDING_SELECTED);
-  RecordingsView.selectedIndex = 0;
+  setSelectedRecording(panel, 0);
   yield selected;
 
   ok($("#main-record-button").classList.contains("checked"),
     "Button is still checked after selecting another item.");
   ok(!$("#main-record-button").hasAttribute("disabled"),
     "Button is not locked after selecting another item.");
 
   yield stopRecording(panel);
--- a/devtools/client/performance/test/browser_perf-recording-selected-04.js
+++ b/devtools/client/performance/test/browser_perf-recording-selected-04.js
@@ -5,24 +5,25 @@
 /**
  * Tests that all components can get rerendered for a profile when switching.
  */
 
 const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
 const { UI_ENABLE_MEMORY_PREF, UI_ENABLE_ALLOCATIONS_PREF } = require("devtools/client/performance/test/helpers/prefs");
 const { initPerformanceInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
 const { startRecording, stopRecording, waitForAllWidgetsRendered } = require("devtools/client/performance/test/helpers/actions");
+const { setSelectedRecording } = require("devtools/client/performance/test/helpers/recording-utils");
 
 add_task(function* () {
   let { panel } = yield initPerformanceInNewTab({
     url: SIMPLE_URL,
     win: window
   });
 
-  let { DetailsView, DetailsSubview, RecordingsView } = panel.panelWin;
+  let { DetailsView, DetailsSubview } = panel.panelWin;
 
   // Enable memory to test the memory overview.
   Services.prefs.setBoolPref(UI_ENABLE_MEMORY_PREF, true);
 
   // Enable allocations to test the memory-calltree and memory-flamegraph.
   Services.prefs.setBoolPref(UI_ENABLE_ALLOCATIONS_PREF, true);
 
   yield startRecording(panel);
@@ -38,21 +39,21 @@ add_task(function* () {
   yield DetailsView.selectView("js-flamegraph");
   yield DetailsView.selectView("memory-calltree");
   yield DetailsView.selectView("memory-flamegraph");
 
   yield startRecording(panel);
   yield stopRecording(panel);
 
   let rerender = waitForAllWidgetsRendered(panel);
-  RecordingsView.selectedIndex = 0;
+  setSelectedRecording(panel, 0);
   yield rerender;
 
   ok(true, "All widgets were rendered when selecting the first recording.");
 
   rerender = waitForAllWidgetsRendered(panel);
-  RecordingsView.selectedIndex = 1;
+  setSelectedRecording(panel, 1);
   yield rerender;
 
   ok(true, "All widgets were rendered when selecting the second recording.");
 
   yield teardownToolboxAndRemoveTab(panel);
 });
--- a/devtools/client/performance/test/browser_perf-recordings-clear-01.js
+++ b/devtools/client/performance/test/browser_perf-recordings-clear-01.js
@@ -5,49 +5,50 @@
 /**
  * Tests that clearing recordings empties out the recordings list and toggles
  * the empty notice state.
  */
 
 const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
 const { initPanelInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
 const { startRecording, stopRecording } = require("devtools/client/performance/test/helpers/actions");
+const { getRecordingsCount } = require("devtools/client/performance/test/helpers/recording-utils");
 
 add_task(function* () {
   let { panel } = yield initPanelInNewTab({
     tool: "performance",
     url: SIMPLE_URL,
     win: window
   });
 
-  let { PerformanceController, PerformanceView, RecordingsView } = panel.panelWin;
+  let { PerformanceController, PerformanceView } = panel.panelWin;
 
   yield startRecording(panel);
   yield stopRecording(panel);
 
-  is(RecordingsView.itemCount, 1,
-    "RecordingsView should have one recording.");
+  is(getRecordingsCount(panel), 1,
+    "The recordings list should have one recording.");
   isnot(PerformanceView.getState(), "empty",
     "PerformanceView should not be in an empty state.");
   isnot(PerformanceController.getCurrentRecording(), null,
     "There should be a current recording.");
 
   yield startRecording(panel);
   yield stopRecording(panel);
 
-  is(RecordingsView.itemCount, 2,
-    "RecordingsView should have two recordings.");
+  is(getRecordingsCount(panel), 2,
+    "The recordings list should have two recordings.");
   isnot(PerformanceView.getState(), "empty",
     "PerformanceView should not be in an empty state.");
   isnot(PerformanceController.getCurrentRecording(), null,
     "There should be a current recording.");
 
   yield PerformanceController.clearRecordings();
 
-  is(RecordingsView.itemCount, 0,
-    "RecordingsView should be empty.");
+  is(getRecordingsCount(panel), 0,
+    "The recordings list should be empty.");
   is(PerformanceView.getState(), "empty",
     "PerformanceView should be in an empty state.");
   is(PerformanceController.getCurrentRecording(), null,
     "There should be no current recording.");
 
   yield teardownToolboxAndRemoveTab(panel);
 });
--- a/devtools/client/performance/test/browser_perf-recordings-clear-02.js
+++ b/devtools/client/performance/test/browser_perf-recordings-clear-02.js
@@ -6,63 +6,64 @@
  * Tests that clearing recordings empties out the recordings list and stops
  * a current recording if recording and can continue recording after.
  */
 
 const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
 const { initPanelInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
 const { startRecording, stopRecording } = require("devtools/client/performance/test/helpers/actions");
 const { times, once } = require("devtools/client/performance/test/helpers/event-utils");
+const { getRecordingsCount } = require("devtools/client/performance/test/helpers/recording-utils");
 
 add_task(function* () {
   let { panel } = yield initPanelInNewTab({
     tool: "performance",
     url: SIMPLE_URL,
     win: window
   });
 
-  let { EVENTS, PerformanceController, PerformanceView, RecordingsView } = panel.panelWin;
+  let { EVENTS, PerformanceController, PerformanceView } = panel.panelWin;
 
   yield startRecording(panel);
   yield stopRecording(panel);
 
-  is(RecordingsView.itemCount, 1,
-    "RecordingsView should have one recording.");
+  is(getRecordingsCount(panel), 1,
+    "The recordings list should have one recording.");
   isnot(PerformanceView.getState(), "empty",
     "PerformanceView should not be in an empty state.");
   isnot(PerformanceController.getCurrentRecording(), null,
     "There should be a current recording.");
 
   yield startRecording(panel);
 
-  is(RecordingsView.itemCount, 2,
-    "RecordingsView should have two recordings.");
+  is(getRecordingsCount(panel), 2,
+    "The recordings list should have two recordings.");
   isnot(PerformanceView.getState(), "empty",
     "PerformanceView should not be in an empty state.");
   isnot(PerformanceController.getCurrentRecording(), null,
     "There should be a current recording.");
 
   let recordingDeleted = times(PerformanceController, EVENTS.RECORDING_DELETED, 2);
   let recordingStopped = once(PerformanceController, EVENTS.RECORDING_STATE_CHANGE, {
     expectedArgs: { "1": "recording-stopped" }
   });
 
   PerformanceController.clearRecordings();
 
   yield recordingDeleted;
   yield recordingStopped;
 
-  is(RecordingsView.itemCount, 0,
-    "RecordingsView should be empty.");
+  is(getRecordingsCount(panel), 0,
+    "The recordings list should be empty.");
   is(PerformanceView.getState(), "empty",
     "PerformanceView should be in an empty state.");
   is(PerformanceController.getCurrentRecording(), null,
     "There should be no current recording.");
 
   // Bug 1169146: Try another recording after clearing mid-recording.
   yield startRecording(panel);
   yield stopRecording(panel);
 
-  is(RecordingsView.itemCount, 1,
-    "RecordingsView should have one recording.");
+  is(getRecordingsCount(panel), 1,
+    "The recordings list should have one recording.");
 
   yield teardownToolboxAndRemoveTab(panel);
 });
--- a/devtools/client/performance/test/browser_perf-tree-view-11.js
+++ b/devtools/client/performance/test/browser_perf-tree-view-11.js
@@ -7,17 +7,17 @@
  * icon is next to the frame with optimizations
  */
 
 var { CATEGORY_MASK } = require("devtools/client/performance/modules/categories");
 
 function* spawnTest() {
   let { panel } = yield initPerformance(SIMPLE_URL);
   let { EVENTS, $, $$, window, PerformanceController } = panel.panelWin;
-  let { OverviewView, DetailsView, JsCallTreeView, RecordingsView } = panel.panelWin;
+  let { OverviewView, DetailsView, JsCallTreeView } = panel.panelWin;
 
   let profilerData = { threads: [gThread] };
 
   Services.prefs.setBoolPref(JIT_PREF, true);
   Services.prefs.setBoolPref(PLATFORM_DATA_PREF, false);
   Services.prefs.setBoolPref(INVERT_PREF, false);
 
   // Make two recordings, so we have one to switch to later, as the
--- a/devtools/client/performance/test/helpers/moz.build
+++ b/devtools/client/performance/test/helpers/moz.build
@@ -7,13 +7,14 @@
 DevToolsModules(
     'actions.js',
     'dom-utils.js',
     'event-utils.js',
     'input-utils.js',
     'panel-utils.js',
     'prefs.js',
     'profiler-mm-utils.js',
+    'recording-utils.js',
     'synth-utils.js',
     'tab-utils.js',
     'urls.js',
     'wait-utils.js',
 )
new file mode 100644
--- /dev/null
+++ b/devtools/client/performance/test/helpers/recording-utils.js
@@ -0,0 +1,54 @@
+/* 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";
+
+/**
+ * These utilities provide a functional interface for accessing the particulars
+ * about the recording's details.
+ */
+
+/**
+ * Access the selected view from the panel's recording list.
+ *
+ * @param {object} panel - The current panel.
+ * @return {object} The recording model.
+ */
+exports.getSelectedRecording = function (panel) {
+  const view = panel.panelWin.RecordingsView;
+  return view.selected;
+};
+
+/**
+ * Set the selected index of the recording via the panel.
+ *
+ * @param {object} panel - The current panel.
+ * @return {number} index
+ */
+exports.setSelectedRecording = function (panel, index) {
+  const view = panel.panelWin.RecordingsView;
+  view.setSelectedByIndex(index);
+  return index;
+};
+
+/**
+ * Access the selected view from the panel's recording list.
+ *
+ * @param {object} panel - The current panel.
+ * @return {number} index
+ */
+exports.getSelectedRecordingIndex = function (panel) {
+  const view = panel.panelWin.RecordingsView;
+  return view.getSelectedIndex();
+};
+
+exports.getDurationLabelText = function (panel, elementIndex) {
+  const { $$ } = panel.panelWin;
+  const elements = $$(".recording-list-item-duration", panel.panelWin.document);
+  return elements[elementIndex].innerHTML;
+};
+
+exports.getRecordingsCount = function (panel) {
+  const { $$ } = panel.panelWin;
+  return $$(".recording-list-item", panel.panelWin.document).length;
+};
--- a/devtools/client/performance/views/recordings.js
+++ b/devtools/client/performance/views/recordings.js
@@ -4,98 +4,108 @@
 /* import-globals-from ../performance-controller.js */
 /* import-globals-from ../performance-view.js */
 /* globals document, window */
 "use strict";
 
 /**
  * Functions handling the recordings UI.
  */
-var RecordingsView = Heritage.extend(WidgetMethods, {
+var RecordingsView = {
   /**
    * Initialization function, called when the tool is started.
    */
   initialize: function () {
-    this.widget = new SideMenuWidget($("#recordings-list"));
-
     this._onSelect = this._onSelect.bind(this);
     this._onRecordingStateChange = this._onRecordingStateChange.bind(this);
     this._onNewRecording = this._onNewRecording.bind(this);
     this._onSaveButtonClick = this._onSaveButtonClick.bind(this);
     this._onRecordingDeleted = this._onRecordingDeleted.bind(this);
     this._onRecordingExported = this._onRecordingExported.bind(this);
 
-    this.emptyText = L10N.getStr("noRecordingsText");
-
     PerformanceController.on(EVENTS.RECORDING_STATE_CHANGE, this._onRecordingStateChange);
     PerformanceController.on(EVENTS.RECORDING_ADDED, this._onNewRecording);
     PerformanceController.on(EVENTS.RECORDING_DELETED, this._onRecordingDeleted);
     PerformanceController.on(EVENTS.RECORDING_EXPORTED, this._onRecordingExported);
-    this.widget.addEventListener("select", this._onSelect, false);
+
+    // DE-XUL: Begin migrating the recording sidebar to React. Temporarily hold state
+    // here.
+    this._listState = {
+      recordings: [],
+      labels: new WeakMap(),
+      selected: null,
+    };
+    this._listMount = PerformanceUtils.createHtmlMount($("#recording-list-mount"));
+    this._renderList();
+  },
+
+  /**
+   * Get the index of the currently selected recording. Only used by tests.
+   * @return {integer} index
+   */
+  getSelectedIndex() {
+    const { recordings, selected } = this._listState;
+    return recordings.indexOf(selected);
+  },
+
+  /**
+   * Set the currently selected recording via its index. Only used by tests.
+   * @param {integer} index
+   */
+  setSelectedByIndex(index) {
+    this._onSelect(this._listState.recordings[index]);
+    this._renderList();
+  },
+
+  /**
+   * DE-XUL: During the migration, this getter will access the selected recording from
+   * the private _listState object so that tests will continue to pass.
+   */
+  get selected() {
+    return this._listState.selected;
+  },
+
+  /**
+   * DE-XUL: During the migration, this getter will access the number of recordings.
+   */
+  get itemCount() {
+    return this._listState.recordings.length;
+  },
+
+  /**
+   * DE-XUL: Render the recording list using React.
+   */
+  _renderList: function () {
+    const {recordings, labels, selected} = this._listState;
+
+    const recordingList = RecordingList({
+      itemComponent: RecordingListItem,
+      items: recordings.map(recording => ({
+        onSelect: () => this._onSelect(recording),
+        onSave: () => this._onSaveButtonClick(recording),
+        isLoading: !recording.isRecording() && !recording.isCompleted(),
+        isRecording: recording.isRecording(),
+        isSelected: recording === selected,
+        duration: recording.getDuration().toFixed(0),
+        label: labels.get(recording),
+      }))
+    });
+
+    ReactDOM.render(recordingList, this._listMount);
   },
 
   /**
    * Destruction function, called when the tool is closed.
    */
   destroy: function () {
     PerformanceController.off(EVENTS.RECORDING_STATE_CHANGE,
                               this._onRecordingStateChange);
     PerformanceController.off(EVENTS.RECORDING_ADDED, this._onNewRecording);
     PerformanceController.off(EVENTS.RECORDING_DELETED, this._onRecordingDeleted);
     PerformanceController.off(EVENTS.RECORDING_EXPORTED, this._onRecordingExported);
-    this.widget.removeEventListener("select", this._onSelect, false);
-  },
-
-  /**
-   * Adds an empty recording to this container.
-   *
-   * @param RecordingModel recording
-   *        A model for the new recording item created.
-   */
-  addEmptyRecording: function (recording) {
-    let titleNode = document.createElement("label");
-    titleNode.className = "plain recording-item-title";
-    titleNode.setAttribute("crop", "end");
-    titleNode.setAttribute("value", recording.getLabel() ||
-      L10N.getFormatStr("recordingsList.itemLabel", this.itemCount + 1));
-
-    let durationNode = document.createElement("label");
-    durationNode.className = "plain recording-item-duration";
-    durationNode.setAttribute("value",
-      L10N.getStr("recordingsList.recordingLabel"));
-
-    let saveNode = document.createElement("label");
-    saveNode.className = "plain recording-item-save";
-    saveNode.addEventListener("click", this._onSaveButtonClick);
-
-    let hspacer = document.createElement("spacer");
-    hspacer.setAttribute("flex", "1");
-
-    let footerNode = document.createElement("hbox");
-    footerNode.className = "recording-item-footer";
-    footerNode.appendChild(durationNode);
-    footerNode.appendChild(hspacer);
-    footerNode.appendChild(saveNode);
-
-    let vspacer = document.createElement("spacer");
-    vspacer.setAttribute("flex", "1");
-
-    let contentsNode = document.createElement("vbox");
-    contentsNode.className = "recording-item";
-    contentsNode.setAttribute("flex", "1");
-    contentsNode.appendChild(titleNode);
-    contentsNode.appendChild(vspacer);
-    contentsNode.appendChild(footerNode);
-
-    // Append a recording item to this container.
-    return this.push([contentsNode], {
-      // Store the recording model that contains all the data to be
-      // rendered in the item.
-      attachment: recording
-    });
   },
 
   /**
    * Called when a new recording is stored in the UI. This handles
    * when recordings are lazily loaded (like a console.profile occurring
    * before the tool is loaded) or imported. In normal manual recording cases,
    * this will also be fired.
    */
@@ -107,119 +117,86 @@ var RecordingsView = Heritage.extend(Wid
    * Signals that a recording has changed state.
    *
    * @param string state
    *        Can be "recording-started", "recording-stopped", "recording-stopping"
    * @param RecordingModel recording
    *        Model of the recording that was started.
    */
   _onRecordingStateChange: function (_, state, recording) {
-    let recordingItem = this.getItemForPredicate(e => e.attachment === recording);
-    if (!recordingItem) {
-      recordingItem = this.addEmptyRecording(recording);
+    const { recordings, labels } = this._listState;
+
+    if (!recordings.includes(recording)) {
+      recordings.push(recording);
+      labels.set(recording, recording.getLabel() ||
+        L10N.getFormatStr("recordingsList.itemLabel", recordings.length));
 
       // If this is a manual recording, immediately select it, or
       // select a console profile if its the only one
-      if (!recording.isConsole() || this.selectedIndex === -1) {
-        this.selectedItem = recordingItem;
+      if (!recording.isConsole() || !this._listState.selected) {
+        this._onSelect(recording);
       }
     }
 
-    recordingItem.isRecording = recording.isRecording();
-
-    // This recording is in the process of stopping.
-    if (!recording.isRecording() && !recording.isCompleted()) {
-      // Mark the corresponding item as loading.
-      let durationNode = $(".recording-item-duration", recordingItem.target);
-      durationNode.setAttribute("value", L10N.getStr("recordingsList.loadingLabel"));
+    // Determine if the recording needs to be selected.
+    const isCompletedManualRecording = !recording.isConsole() && recording.isCompleted();
+    if (recording.isImported() || isCompletedManualRecording) {
+      this._onSelect(recording);
     }
 
-    // Render the recording item with finalized information (timing, etc)
-    if (recording.isCompleted() && !recordingItem.finalized) {
-      this.finalizeRecording(recordingItem);
-      // Select the recording if it was a manual recording only
-      if (!recording.isConsole()) {
-        this.forceSelect(recordingItem);
-      }
-    }
-
-    // Auto select imported items.
-    if (recording.isImported()) {
-      this.selectedItem = recordingItem;
-    }
+    this._renderList();
   },
 
   /**
    * Clears out all non-console recordings.
    */
   _onRecordingDeleted: function (_, recording) {
-    let recordingItem = this.getItemForPredicate(e => e.attachment === recording);
-    this.remove(recordingItem);
-  },
-
-  /**
-   * Adds recording data to a recording item in this container.
-   *
-   * @param Item recordingItem
-   *        An item inserted via `RecordingsView.addEmptyRecording`.
-   */
-  finalizeRecording: function (recordingItem) {
-    let model = recordingItem.attachment;
-    recordingItem.finalized = true;
-
-    let saveNode = $(".recording-item-save", recordingItem.target);
-    saveNode.setAttribute("value",
-      L10N.getStr("recordingsList.saveLabel"));
-
-    let durationMillis = model.getDuration().toFixed(0);
-    let durationNode = $(".recording-item-duration", recordingItem.target);
-    durationNode.setAttribute("value",
-      L10N.getFormatStr("recordingsList.durationLabel", durationMillis));
+    const { recordings } = this._listState;
+    const index = recordings.indexOf(recording);
+    if (index === -1) {
+      throw new Error("Attempting to remove a recording that doesn't exist.");
+    }
+    recordings.splice(index, 1);
+    this._renderList();
   },
 
   /**
    * The select listener for this container.
    */
-  _onSelect: Task.async(function* ({ detail: recordingItem }) {
-    if (!recordingItem) {
-      return;
-    }
-
-    let model = recordingItem.attachment;
-    this.emit(EVENTS.UI_RECORDING_SELECTED, model);
+  _onSelect: Task.async(function* (recording) {
+    this._listState.selected = recording;
+    this.emit(EVENTS.UI_RECORDING_SELECTED, recording);
+    this._renderList();
   }),
 
   /**
    * The click listener for the "save" button of each item in this container.
    */
-  _onSaveButtonClick: function (e) {
+  _onSaveButtonClick: function (recording) {
     let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
     fp.init(window, L10N.getStr("recordingsList.saveDialogTitle"),
             Ci.nsIFilePicker.modeSave);
     fp.appendFilter(L10N.getStr("recordingsList.saveDialogJSONFilter"), "*.json");
     fp.appendFilter(L10N.getStr("recordingsList.saveDialogAllFilter"), "*.*");
     fp.defaultString = "profile.json";
 
     fp.open({ done: result => {
       if (result == Ci.nsIFilePicker.returnCancel) {
         return;
       }
-      let recordingItem = this.getItemForElement(e.target);
-      this.emit(EVENTS.UI_EXPORT_RECORDING, recordingItem.attachment, fp.file);
+      this.emit(EVENTS.UI_EXPORT_RECORDING, recording, fp.file);
     }});
   },
 
   _onRecordingExported: function (_, recording, file) {
     if (recording.isConsole()) {
       return;
     }
-    let recordingItem = this.getItemForPredicate(e => e.attachment === recording);
-    let titleNode = $(".recording-item-title", recordingItem.target);
-    titleNode.setAttribute("value", file.leafName.replace(/\..+$/, ""));
-  },
-
-  toString: () => "[object RecordingsView]"
-});
+    const name = file.leafName.replace(/\..+$/, "");
+    this._listState.labels.set(recording, name);
+    this._renderList();
+  }
+};
 
 /**
  * Convenient way of emitting events from the RecordingsView.
  */
 EventEmitter.decorate(RecordingsView);
--- a/devtools/client/themes/performance.css
+++ b/devtools/client/themes/performance.css
@@ -139,43 +139,91 @@
 
 /*
  * 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;
+.theme-sidebar {
+  position: relative;
+}
+
+/**
+ * DE-XUL: This is probably only needed for the html:div inside of a vbox.
+ */
+#recordings-list > div {
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  overflow-y: auto;
+  overflow-x: hidden;
 }
 
-.recording-item {
-  padding: 4px;
+.recording-list {
+  width: var(--sidebar-width);
+  min-width: var(--sidebar-width);
+  margin: 0;
+  padding: 0;
+  background-color: var(--theme-sidebar-background);
+  border-inline-end: 1px solid var(--theme-splitter-color);
 }
 
-.recording-item-title {
+.recording-list-item {
+  display: flex;
+  flex-direction: column;
+  color: var(--theme-body-color);
+  border-bottom: 1px solid rgba(128,128,128,0.15);
+  padding: 8px;
+  cursor: default;
+}
+
+.recording-list-item.selected {
+  background-color: var(--theme-selection-background);
+  color: var(--theme-selection-color);
+}
+
+.recording-list-empty {
+  padding: 8px;
+}
+
+.recording-list-item-label {
   font-size: 110%;
 }
 
-.recording-item-footer {
+.recording-list-item-footer {
   padding-top: 4px;
   font-size: 90%;
+  display: flex;
+  justify-content: space-between;
 }
 
-.recording-item-save {
+.recording-list-item-save {
+  background: none;
+  border: none;
   text-decoration: underline;
   cursor: pointer;
+  font-size: 90%;
+  padding:0;
 }
 
-.recording-item-duration,
-.recording-item-save {
+.recording-list-item-duration,
+.recording-list-item-save {
   color: var(--theme-body-color-alt);
 }
 
+.recording-list-item.selected .recording-list-item-duration,
+.recording-list-item.selected .recording-list-item-save {
+  color: var(--theme-body-color-alt);
+  color: var(--theme-selection-color);
+}
+
 #recordings-list .selected label {
   /* Text inside a selected item should not be custom colored. */
   color: inherit !important;
 }
 
 /* Recording notices */
 
 .notice-container {