Bug 1152441 - Part 2: Rewrite the marker view to use React and the Tree component r?gregtatum draft
authorJarda Snajdr <jsnajdr@gmail.com>
Fri, 09 Sep 2016 16:55:54 +0200
changeset 415511 0dd9639eede776902997d5ff0a38dc74e0ea66b1
parent 415510 216c3dfc4f96fa15ec5b7d4d303e1540da9c38e7
child 531622 133c59acb7fa7e40c7ad5e6d159bfae6d8cdf45b
push id29885
push userbmo:jsnajdr@gmail.com
push dateTue, 20 Sep 2016 12:37:58 +0000
reviewersgregtatum
bugs1152441
milestone52.0a1
Bug 1152441 - Part 2: Rewrite the marker view to use React and the Tree component r?gregtatum MozReview-Commit-ID: 5TNNsVSql46
devtools/client/performance/components/moz.build
devtools/client/performance/components/waterfall-header.js
devtools/client/performance/components/waterfall-tree-row.js
devtools/client/performance/components/waterfall-tree.js
devtools/client/performance/components/waterfall.js
devtools/client/performance/modules/logic/waterfall-utils.js
devtools/client/performance/modules/moz.build
devtools/client/performance/modules/waterfall-ticks.js
devtools/client/performance/modules/widgets/marker-view.js
devtools/client/performance/modules/widgets/markers-overview.js
devtools/client/performance/modules/widgets/moz.build
devtools/client/performance/modules/widgets/waterfall-ticks.js
devtools/client/performance/performance-controller.js
devtools/client/performance/performance.xul
devtools/client/performance/test/browser_timeline-waterfall-background.js
devtools/client/performance/test/browser_timeline-waterfall-generic.js
devtools/client/performance/views/details-waterfall.js
devtools/client/themes/performance.css
--- a/devtools/client/performance/components/moz.build
+++ b/devtools/client/performance/components/moz.build
@@ -3,11 +3,15 @@
 # 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',
+    '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/waterfall-header.js
@@ -0,0 +1,69 @@
+/* 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";
+
+/**
+ * The "waterfall ticks" view, a header for the markers displayed in the waterfall.
+ */
+
+const { DOM: dom, PropTypes } = require("devtools/client/shared/vendor/react");
+const { L10N } = require("../modules/global");
+const { TickUtils } = require("../modules/waterfall-ticks");
+
+// ms
+const WATERFALL_HEADER_TICKS_MULTIPLE = 5;
+// px
+const WATERFALL_HEADER_TICKS_SPACING_MIN = 50;
+// px
+const WATERFALL_HEADER_TEXT_PADDING = 3;
+
+function WaterfallHeader(props) {
+  let { startTime, dataScale, sidebarWidth, waterfallWidth } = props;
+
+  let tickInterval = TickUtils.findOptimalTickInterval({
+    ticksMultiple: WATERFALL_HEADER_TICKS_MULTIPLE,
+    ticksSpacingMin: WATERFALL_HEADER_TICKS_SPACING_MIN,
+    dataScale: dataScale
+  });
+
+  let ticks = [];
+  for (let x = 0; x < waterfallWidth; x += tickInterval) {
+    let left = x + WATERFALL_HEADER_TEXT_PADDING;
+    let time = Math.round(x / dataScale + startTime);
+    let label = L10N.getFormatStr("timeline.tick", time);
+
+    let node = dom.div({
+      className: "plain waterfall-header-tick",
+      style: { transform: `translateX(${left}px)` }
+    }, label);
+    ticks.push(node);
+  }
+
+  return dom.div(
+    { className: "waterfall-header" },
+    dom.div(
+      {
+        className: "waterfall-sidebar theme-sidebar waterfall-header-name",
+        style: { width: sidebarWidth + "px" }
+      },
+      L10N.getStr("timeline.records")
+    ),
+    dom.div(
+      { className: "waterfall-header-ticks waterfall-background-ticks" },
+      ticks
+    )
+  );
+}
+
+WaterfallHeader.displayName = "WaterfallHeader";
+
+WaterfallHeader.propTypes = {
+  startTime: PropTypes.number.isRequired,
+  dataScale: PropTypes.number.isRequired,
+  sidebarWidth: PropTypes.number.isRequired,
+  waterfallWidth: PropTypes.number.isRequired,
+};
+
+module.exports = WaterfallHeader;
new file mode 100644
--- /dev/null
+++ b/devtools/client/performance/components/waterfall-tree-row.js
@@ -0,0 +1,107 @@
+/* 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";
+
+/**
+ * A single row (node) in the waterfall tree
+ */
+
+const { DOM: dom, PropTypes } = require("devtools/client/shared/vendor/react");
+const { MarkerBlueprintUtils } = require("../modules/marker-blueprint-utils");
+
+// px
+const LEVEL_INDENT = 10;
+// px
+const ARROW_NODE_OFFSET = -14;
+// px
+const WATERFALL_MARKER_TIMEBAR_WIDTH_MIN = 5;
+
+function buildMarkerSidebar(blueprint, props) {
+  const { marker, level, sidebarWidth } = props;
+
+  let bullet = dom.div({
+    className: `waterfall-marker-bullet marker-color-${blueprint.colorName}`,
+    style: { transform: `translateX(${level * LEVEL_INDENT}px)` },
+    "data-type": marker.name
+  });
+
+  let label = MarkerBlueprintUtils.getMarkerLabel(marker);
+
+  let name = dom.div({
+    className: "plain waterfall-marker-name",
+    style: { transform: `translateX(${level * LEVEL_INDENT}px)` },
+    title: label
+  }, label);
+
+  return dom.div({
+    className: "waterfall-sidebar theme-sidebar",
+    style: { width: sidebarWidth + "px" }
+  }, bullet, name);
+}
+
+function buildMarkerTimebar(blueprint, props) {
+  const { marker, startTime, dataScale, arrow } = props;
+  const offset = (marker.start - startTime) * dataScale + ARROW_NODE_OFFSET;
+  const width = Math.max((marker.end - marker.start) * dataScale,
+                         WATERFALL_MARKER_TIMEBAR_WIDTH_MIN);
+
+  let bar = dom.div(
+    {
+      className: "waterfall-marker-wrap",
+      style: { transform: `translateX(${offset}px)` }
+    },
+    arrow,
+    dom.div({
+      className: `waterfall-marker-bar marker-color-${blueprint.colorName}`,
+      style: { width: `${width}px` },
+      "data-type": marker.name
+    })
+  );
+
+  return dom.div(
+    { className: "waterfall-marker waterfall-background-ticks" },
+    bar
+  );
+}
+
+function WaterfallTreeRow(props) {
+  const { marker, focused } = props;
+  const blueprint = MarkerBlueprintUtils.getBlueprintFor(marker);
+
+  let attrs = {
+    className: "waterfall-tree-item" + (focused ? " focused" : ""),
+    "data-otmt": marker.isOffMainThread
+  };
+
+  // Don't render an expando-arrow for leaf nodes.
+  let submarkers = marker.submarkers;
+  let hasDescendants = submarkers && submarkers.length > 0;
+  if (hasDescendants) {
+    attrs["data-expandable"] = "";
+  } else {
+    attrs["data-invisible"] = "";
+  }
+
+  return dom.div(
+    attrs,
+    buildMarkerSidebar(blueprint, props),
+    buildMarkerTimebar(blueprint, props)
+  );
+}
+
+WaterfallTreeRow.displayName = "WaterfallTreeRow";
+
+WaterfallTreeRow.propTypes = {
+  marker: PropTypes.object.isRequired,
+  level: PropTypes.number.isRequired,
+  arrow: PropTypes.element.isRequired,
+  expanded: PropTypes.bool.isRequired,
+  focused: PropTypes.bool.isRequired,
+  startTime: PropTypes.number.isRequired,
+  dataScale: PropTypes.number.isRequired,
+  sidebarWidth: PropTypes.number.isRequired,
+};
+
+module.exports = WaterfallTreeRow;
new file mode 100644
--- /dev/null
+++ b/devtools/client/performance/components/waterfall-tree.js
@@ -0,0 +1,167 @@
+/* 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 { createClass, createFactory, PropTypes } = require("devtools/client/shared/vendor/react");
+const Tree = createFactory(require("devtools/client/shared/components/tree"));
+const WaterfallTreeRow = createFactory(require("./waterfall-tree-row"));
+
+// px - keep in sync with var(--waterfall-tree-row-height) in performance.css
+const WATERFALL_TREE_ROW_HEIGHT = 15;
+
+/**
+ * Checks if a given marker is in the specified time range.
+ *
+ * @param object e
+ *        The marker containing the { start, end } timestamps.
+ * @param number start
+ *        The earliest allowed time.
+ * @param number end
+ *        The latest allowed time.
+ * @return boolean
+ *         True if the marker fits inside the specified time range.
+ */
+function isMarkerInRange(e, start, end) {
+  let mStart = e.start | 0;
+  let mEnd = e.end | 0;
+
+  return (
+    // bounds inside
+    (mStart >= start && mEnd <= end) ||
+    // bounds outside
+    (mStart < start && mEnd > end) ||
+    // overlap start
+    (mStart < start && mEnd >= start && mEnd <= end) ||
+    // overlap end
+    (mEnd > end && mStart >= start && mStart <= end)
+  );
+}
+
+const WaterfallTree = createClass({
+  displayName: "WaterfallTree",
+
+  propTypes: {
+    marker: PropTypes.object.isRequired,
+    startTime: PropTypes.number.isRequired,
+    endTime: PropTypes.number.isRequired,
+    dataScale: PropTypes.number.isRequired,
+    sidebarWidth: PropTypes.number.isRequired,
+    waterfallWidth: PropTypes.number.isRequired,
+    onFocus: PropTypes.func,
+  },
+
+  getInitialState() {
+    return {
+      focused: null,
+      expanded: new Set()
+    };
+  },
+
+  _getRoots(node) {
+    let roots = this.props.marker.submarkers || [];
+    return roots.filter(this._filter);
+  },
+
+  /**
+   * Find the parent node of 'node' with a depth-first search of the marker tree
+   */
+  _getParent(node) {
+    function findParent(marker) {
+      if (marker.submarkers) {
+        for (let submarker of marker.submarkers) {
+          if (submarker === node) {
+            return marker;
+          }
+
+          let parent = findParent(submarker);
+          if (parent) {
+            return parent;
+          }
+        }
+      }
+
+      return null;
+    }
+
+    let rootMarker = this.props.marker;
+    let parent = findParent(rootMarker);
+
+    // We are interested only in parent markers that are rendered,
+    // which rootMarker is not. Return null if the parent is rootMarker.
+    return parent !== rootMarker ? parent : null;
+  },
+
+  _getChildren(node) {
+    let submarkers = node.submarkers || [];
+    return submarkers.filter(this._filter);
+  },
+
+  _getKey(node) {
+    return `marker-${node.index}`;
+  },
+
+  _isExpanded(node) {
+    return this.state.expanded.has(node);
+  },
+
+  _onExpand(node) {
+    this.setState(state => {
+      let expanded = new Set(state.expanded);
+      expanded.add(node);
+      return { expanded };
+    });
+  },
+
+  _onCollapse(node) {
+    this.setState(state => {
+      let expanded = new Set(state.expanded);
+      expanded.delete(node);
+      return { expanded };
+    });
+  },
+
+  _onFocus(node) {
+    this.setState({ focused: node });
+    if (this.props.onFocus) {
+      this.props.onFocus(node);
+    }
+  },
+
+  _filter(node) {
+    let { startTime, endTime } = this.props;
+    return isMarkerInRange(node, startTime, endTime);
+  },
+
+  _renderItem(marker, level, focused, arrow, expanded) {
+    let { startTime, dataScale, sidebarWidth } = this.props;
+    return WaterfallTreeRow({
+      marker,
+      level,
+      arrow,
+      expanded,
+      focused,
+      startTime,
+      dataScale,
+      sidebarWidth
+    });
+  },
+
+  render() {
+    return Tree({
+      getRoots: this._getRoots,
+      getParent: this._getParent,
+      getChildren: this._getChildren,
+      getKey: this._getKey,
+      isExpanded: this._isExpanded,
+      onExpand: this._onExpand,
+      onCollapse: this._onCollapse,
+      onFocus: this._onFocus,
+      renderItem: this._renderItem,
+      focused: this.state.focused,
+      itemHeight: WATERFALL_TREE_ROW_HEIGHT
+    });
+  }
+});
+
+module.exports = WaterfallTree;
new file mode 100644
--- /dev/null
+++ b/devtools/client/performance/components/waterfall.js
@@ -0,0 +1,36 @@
+/* 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";
+
+/**
+ * This file contains the "waterfall" view, essentially a detailed list
+ * of all the markers in the timeline data.
+ */
+
+const { DOM: dom, createFactory, PropTypes } = require("devtools/client/shared/vendor/react");
+const WaterfallHeader = createFactory(require("./waterfall-header"));
+const WaterfallTree = createFactory(require("./waterfall-tree"));
+
+function Waterfall(props) {
+  return dom.div(
+    { className: "waterfall-markers" },
+    WaterfallHeader(props),
+    WaterfallTree(props)
+  );
+}
+
+Waterfall.displayName = "Waterfall";
+
+Waterfall.propTypes = {
+  marker: PropTypes.object.isRequired,
+  startTime: PropTypes.number.isRequired,
+  endTime: PropTypes.number.isRequired,
+  dataScale: PropTypes.number.isRequired,
+  sidebarWidth: PropTypes.number.isRequired,
+  waterfallWidth: PropTypes.number.isRequired,
+  onFocus: PropTypes.func,
+  onBlur: PropTypes.func,
+};
+
+module.exports = Waterfall;
--- a/devtools/client/performance/modules/logic/waterfall-utils.js
+++ b/devtools/client/performance/modules/logic/waterfall-utils.js
@@ -46,22 +46,22 @@ function collapseMarkersIntoNode({ rootN
     let parentNode = getCurrentParentNode();
     let blueprint = MarkerBlueprintUtils.getBlueprintFor(curr);
 
     let nestable = "nestable" in blueprint ? blueprint.nestable : true;
     let collapsible = "collapsible" in blueprint ? blueprint.collapsible : true;
 
     let finalized = false;
 
-    // If this marker is collapsible, turn it into a parent marker.
-    // If there are no children within it later, it will be turned
-    // back into a normal node.
+    // Extend the marker with extra properties needed in the marker tree
+    let extendedProps = { index: i };
     if (collapsible) {
-      curr = createParentNode(curr);
+      extendedProps.submarkers = [];
     }
+    curr = extend(curr, extendedProps);
 
     // If not nestible, just push it inside the root node. Additionally,
     // markers originating outside the main thread are considered to be
     // "never collapsible", to avoid confusion.
     // A beter solution would be to collapse every marker with its siblings
     // from the same thread, but that would require a thread id attached
     // to all markers, which is potentially expensive and rather useless at
     // the moment, since we don't really have that many OTMT markers.
--- a/devtools/client/performance/modules/moz.build
+++ b/devtools/client/performance/modules/moz.build
@@ -13,9 +13,10 @@ DevToolsModules(
     'constants.js',
     'global.js',
     'io.js',
     'marker-blueprint-utils.js',
     'marker-dom-utils.js',
     'marker-formatters.js',
     'markers.js',
     'utils.js',
+    'waterfall-ticks.js',
 )
new file mode 100644
--- /dev/null
+++ b/devtools/client/performance/modules/waterfall-ticks.js
@@ -0,0 +1,98 @@
+/* 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 HTML_NS = "http://www.w3.org/1999/xhtml";
+
+// ms
+const WATERFALL_BACKGROUND_TICKS_MULTIPLE = 5;
+const WATERFALL_BACKGROUND_TICKS_SCALES = 3;
+// px
+const WATERFALL_BACKGROUND_TICKS_SPACING_MIN = 10;
+const WATERFALL_BACKGROUND_TICKS_COLOR_RGB = [128, 136, 144];
+// byte
+const WATERFALL_BACKGROUND_TICKS_OPACITY_MIN = 32;
+// byte
+const WATERFALL_BACKGROUND_TICKS_OPACITY_ADD = 32;
+
+const FIND_OPTIMAL_TICK_INTERVAL_MAX_ITERS = 100;
+
+/**
+ * Creates the background displayed on the marker's waterfall.
+ */
+function drawWaterfallBackground(doc, dataScale, waterfallWidth) {
+  let canvas = doc.createElementNS(HTML_NS, "canvas");
+  let ctx = canvas.getContext("2d");
+
+  // Nuke the context.
+  let canvasWidth = canvas.width = waterfallWidth;
+  // Awww yeah, 1px, repeats on Y axis.
+  let canvasHeight = canvas.height = 1;
+
+  // Start over.
+  let imageData = ctx.createImageData(canvasWidth, canvasHeight);
+  let pixelArray = imageData.data;
+
+  let buf = new ArrayBuffer(pixelArray.length);
+  let view8bit = new Uint8ClampedArray(buf);
+  let view32bit = new Uint32Array(buf);
+
+  // Build new millisecond tick lines...
+  let [r, g, b] = WATERFALL_BACKGROUND_TICKS_COLOR_RGB;
+  let alphaComponent = WATERFALL_BACKGROUND_TICKS_OPACITY_MIN;
+  let tickInterval = findOptimalTickInterval({
+    ticksMultiple: WATERFALL_BACKGROUND_TICKS_MULTIPLE,
+    ticksSpacingMin: WATERFALL_BACKGROUND_TICKS_SPACING_MIN,
+    dataScale: dataScale
+  });
+
+  // Insert one pixel for each division on each scale.
+  for (let i = 1; i <= WATERFALL_BACKGROUND_TICKS_SCALES; i++) {
+    let increment = tickInterval * Math.pow(2, i);
+    for (let x = 0; x < canvasWidth; x += increment) {
+      let position = x | 0;
+      view32bit[position] = (alphaComponent << 24) | (b << 16) | (g << 8) | r;
+    }
+    alphaComponent += WATERFALL_BACKGROUND_TICKS_OPACITY_ADD;
+  }
+
+  // Flush the image data and cache the waterfall background.
+  pixelArray.set(view8bit);
+  ctx.putImageData(imageData, 0, 0);
+  doc.mozSetImageElement("waterfall-background", canvas);
+
+  return canvas;
+}
+
+/**
+ * Finds the optimal tick interval between time markers in this timeline.
+ *
+ * @param number ticksMultiple
+ * @param number ticksSpacingMin
+ * @param number dataScale
+ * @return number
+ */
+function findOptimalTickInterval({ ticksMultiple, ticksSpacingMin, dataScale }) {
+  let timingStep = ticksMultiple;
+  let maxIters = FIND_OPTIMAL_TICK_INTERVAL_MAX_ITERS;
+  let numIters = 0;
+
+  if (dataScale > ticksSpacingMin) {
+    return dataScale;
+  }
+
+  while (true) {
+    let scaledStep = dataScale * timingStep;
+    if (++numIters > maxIters) {
+      return scaledStep;
+    }
+    if (scaledStep < ticksSpacingMin) {
+      timingStep <<= 1;
+      continue;
+    }
+    return scaledStep;
+  }
+}
+
+exports.TickUtils = { findOptimalTickInterval, drawWaterfallBackground };
deleted file mode 100644
--- a/devtools/client/performance/modules/widgets/marker-view.js
+++ /dev/null
@@ -1,304 +0,0 @@
-/* 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";
-
-/**
- * This file contains the "marker" view, essentially a detailed list
- * of all the markers in the timeline data.
- */
-
-const { Heritage } = require("devtools/client/shared/widgets/view-helpers");
-const { AbstractTreeItem } = require("resource://devtools/client/shared/widgets/AbstractTreeItem.jsm");
-const { MarkerBlueprintUtils } = require("devtools/client/performance/modules/marker-blueprint-utils");
-
-// px
-const LEVEL_INDENT = 10;
-// px
-const ARROW_NODE_OFFSET = -15;
-// px
-const WATERFALL_MARKER_SIDEBAR_SAFE_BOUNDS = 20;
-// px
-const WATERFALL_MARKER_SIDEBAR_WIDTH = 175;
-// px
-const WATERFALL_MARKER_TIMEBAR_WIDTH_MIN = 5;
-
-/**
- * A detailed waterfall view for the timeline data.
- *
- * @param MarkerView owner
- *        The MarkerView considered the "owner" marker. This newly created
- *        instance will be represent the "submarker". Should be null for root nodes.
- * @param object marker
- *        Details about this marker, like { name, start, end, submarkers } etc.
- * @param number level [optional]
- *        The indentation level in the waterfall tree. The root node is at level 0.
- * @param boolean hidden [optional]
- *        Whether this node should be hidden and not contribute to depth/level
- *        calculations. Defaults to false.
- */
-function MarkerView({ owner, marker, level, hidden }) {
-  AbstractTreeItem.call(this, {
-    parent: owner,
-    level: level | 0 - (hidden ? 1 : 0)
-  });
-
-  this.marker = marker;
-  this.hidden = !!hidden;
-
-  this._onItemBlur = this._onItemBlur.bind(this);
-  this._onItemFocus = this._onItemFocus.bind(this);
-}
-
-MarkerView.prototype = Heritage.extend(AbstractTreeItem.prototype, {
-  /**
-   * Calculates and stores the available width for the waterfall.
-   * This should be invoked every time the container node is resized.
-   */
-  recalculateBounds: function () {
-    this.root._waterfallWidth = this.bounds.width
-      - WATERFALL_MARKER_SIDEBAR_WIDTH
-      - WATERFALL_MARKER_SIDEBAR_SAFE_BOUNDS;
-  },
-
-  /**
-   * Sets a list of marker types to be filtered out of this view.
-   * @param Array<String> filter
-   */
-  set filter(filter) {
-    this.root._filter = filter;
-  },
-  get filter() {
-    return this.root._filter;
-  },
-
-  /**
-   * Sets the { startTime, endTime }, in milliseconds.
-   * @param object interval
-   */
-  set interval(interval) {
-    this.root._interval = interval;
-  },
-  get interval() {
-    return this.root._interval;
-  },
-
-  /**
-   * Gets the current waterfall width.
-   * @return number
-   */
-  getWaterfallWidth: function () {
-    return this._waterfallWidth;
-  },
-
-  /**
-   * Gets the data scale amount for the current width and interval.
-   * @return number
-   */
-  getDataScale: function () {
-    let startTime = this.root._interval.startTime|0;
-    let endTime = this.root._interval.endTime|0;
-    return this.root._waterfallWidth / (endTime - startTime);
-  },
-
-  /**
-   * Creates the view for this waterfall node.
-   * @param nsIDOMNode document
-   * @param nsIDOMNode arrowNode
-   * @return nsIDOMNode
-   */
-  _displaySelf: function (document, arrowNode) {
-    let targetNode = document.createElement("hbox");
-    targetNode.className = "waterfall-tree-item";
-    targetNode.setAttribute("otmt", this.marker.isOffMainThread);
-
-    if (this == this.root) {
-      // Bounds are needed for properly positioning and scaling markers in
-      // the waterfall, but it's sufficient to make those calculations only
-      // for the root node.
-      this.root.recalculateBounds();
-      // The AbstractTreeItem propagates events to the root, so we don't
-      // need to listen them on descendant items in the tree.
-      this._addEventListeners();
-    } else {
-      // Root markers are an implementation detail and shouldn't be shown.
-      this._buildMarkerCells(document, targetNode, arrowNode);
-    }
-
-    if (this.hidden) {
-      targetNode.style.display = "none";
-    }
-
-    return targetNode;
-  },
-
-  /**
-   * Populates this node in the waterfall tree with the corresponding "markers".
-   * @param array:AbstractTreeItem children
-   */
-  _populateSelf: function (children) {
-    let submarkers = this.marker.submarkers;
-    if (!submarkers || !submarkers.length) {
-      return;
-    }
-    let startTime = this.root._interval.startTime;
-    let endTime = this.root._interval.endTime;
-    let newLevel = this.level + 1;
-
-    for (let i = 0, len = submarkers.length; i < len; i++) {
-      let marker = submarkers[i];
-
-      // Skip filtered markers
-      if (!MarkerBlueprintUtils.shouldDisplayMarker(marker, this.filter)) {
-        continue;
-      }
-
-      if (!isMarkerInRange(marker, startTime|0, endTime|0)) {
-        continue;
-      }
-
-      children.push(new MarkerView({
-        owner: this,
-        marker: marker,
-        level: newLevel,
-        inverted: this.inverted
-      }));
-    }
-  },
-
-  /**
-   * Builds all the nodes representing a marker in the waterfall.
-   * @param nsIDOMNode document
-   * @param nsIDOMNode targetNode
-   * @param nsIDOMNode arrowNode
-   */
-  _buildMarkerCells: function (doc, targetNode, arrowNode) {
-    let marker = this.marker;
-    let blueprint = MarkerBlueprintUtils.getBlueprintFor(marker);
-    let startTime = this.root._interval.startTime;
-    let endTime = this.root._interval.endTime;
-
-    let sidebarCell = this._buildMarkerSidebar(doc, blueprint, marker);
-    let timebarCell = this._buildMarkerTimebar(doc, blueprint, marker, startTime,
-                                               endTime, arrowNode);
-
-    targetNode.appendChild(sidebarCell);
-    targetNode.appendChild(timebarCell);
-
-    // Don't render an expando-arrow for leaf nodes.
-    let submarkers = this.marker.submarkers;
-    let hasDescendants = submarkers && submarkers.length > 0;
-    if (hasDescendants) {
-      targetNode.setAttribute("expandable", "");
-    } else {
-      arrowNode.setAttribute("invisible", "");
-    }
-
-    targetNode.setAttribute("level", this.level);
-  },
-
-  /**
-   * Functions creating each cell in this waterfall view.
-   * Invoked by `_displaySelf`.
-   */
-  _buildMarkerSidebar: function (doc, style, marker) {
-    let cell = doc.createElement("hbox");
-    cell.className = "waterfall-sidebar theme-sidebar";
-    cell.setAttribute("width", WATERFALL_MARKER_SIDEBAR_WIDTH);
-    cell.setAttribute("align", "center");
-
-    let bullet = doc.createElement("hbox");
-    bullet.className = `waterfall-marker-bullet marker-color-${style.colorName}`;
-    bullet.style.transform = `translateX(${this.level * LEVEL_INDENT}px)`;
-    bullet.setAttribute("type", marker.name);
-    cell.appendChild(bullet);
-
-    let name = doc.createElement("description");
-    let label = MarkerBlueprintUtils.getMarkerLabel(marker);
-    name.className = "plain waterfall-marker-name";
-    name.style.transform = `translateX(${this.level * LEVEL_INDENT}px)`;
-    name.setAttribute("crop", "end");
-    name.setAttribute("flex", "1");
-    name.setAttribute("value", label);
-    name.setAttribute("tooltiptext", label);
-    cell.appendChild(name);
-
-    return cell;
-  },
-  _buildMarkerTimebar: function (doc, style, marker, startTime, endTime, arrowNode) {
-    let cell = doc.createElement("hbox");
-    cell.className = "waterfall-marker waterfall-background-ticks";
-    cell.setAttribute("align", "center");
-    cell.setAttribute("flex", "1");
-
-    let dataScale = this.getDataScale();
-    let offset = (marker.start - startTime) * dataScale;
-    let width = (marker.end - marker.start) * dataScale;
-
-    arrowNode.style.transform = `translateX(${offset + ARROW_NODE_OFFSET}px)`;
-    cell.appendChild(arrowNode);
-
-    let bar = doc.createElement("hbox");
-    bar.className = `waterfall-marker-bar marker-color-${style.colorName}`;
-    bar.style.transform = `translateX(${offset}px)`;
-    bar.setAttribute("type", marker.name);
-    bar.setAttribute("width", Math.max(width, WATERFALL_MARKER_TIMEBAR_WIDTH_MIN));
-    cell.appendChild(bar);
-
-    return cell;
-  },
-
-  /**
-   * Adds the event listeners for this particular tree item.
-   */
-  _addEventListeners: function () {
-    this.on("focus", this._onItemFocus);
-    this.on("blur", this._onItemBlur);
-  },
-
-  /**
-   * Handler for the "blur" event on the root item.
-   */
-  _onItemBlur: function () {
-    this.root.emit("unselected");
-  },
-
-  /**
-   * Handler for the "mousedown" event on the root item.
-   */
-  _onItemFocus: function (e, item) {
-    this.root.emit("selected", item.marker);
-  }
-});
-
-/**
- * Checks if a given marker is in the specified time range.
- *
- * @param object e
- *        The marker containing the { start, end } timestamps.
- * @param number start
- *        The earliest allowed time.
- * @param number end
- *        The latest allowed time.
- * @return boolean
- *         True if the marker fits inside the specified time range.
- */
-function isMarkerInRange(e, start, end) {
-  let mStart = e.start|0;
-  let mEnd = e.end|0;
-
-  return (
-    // bounds inside
-    (mStart >= start && mEnd <= end) ||
-    // bounds outside
-    (mStart < start && mEnd > end) ||
-    // overlap start
-    (mStart < start && mEnd >= start && mEnd <= end) ||
-    // overlap end
-    (mEnd > end && mStart >= start && mStart <= end)
-  );
-}
-
-exports.MarkerView = MarkerView;
-exports.WATERFALL_MARKER_SIDEBAR_SAFE_BOUNDS = WATERFALL_MARKER_SIDEBAR_SAFE_BOUNDS;
-exports.WATERFALL_MARKER_SIDEBAR_WIDTH = WATERFALL_MARKER_SIDEBAR_WIDTH;
--- a/devtools/client/performance/modules/widgets/markers-overview.js
+++ b/devtools/client/performance/modules/widgets/markers-overview.js
@@ -11,17 +11,17 @@
 
 const { Heritage } = require("devtools/client/shared/widgets/view-helpers");
 const { AbstractCanvasGraph } = require("devtools/client/shared/widgets/Graphs");
 
 const { colorUtils } = require("devtools/shared/css/color");
 const { getColor } = require("devtools/client/shared/theme");
 const ProfilerGlobal = require("devtools/client/performance/modules/global");
 const { MarkerBlueprintUtils } = require("devtools/client/performance/modules/marker-blueprint-utils");
-const { TickUtils } = require("devtools/client/performance/modules/widgets/waterfall-ticks");
+const { TickUtils } = require("devtools/client/performance/modules/waterfall-ticks");
 const { TIMELINE_BLUEPRINT } = require("devtools/client/performance/modules/markers");
 
 // px
 const OVERVIEW_HEADER_HEIGHT = 14;
 // px
 const OVERVIEW_ROW_HEIGHT = 11;
 
 const OVERVIEW_SELECTION_LINE_COLOR = "#666";
--- a/devtools/client/performance/modules/widgets/moz.build
+++ b/devtools/client/performance/modules/widgets/moz.build
@@ -1,13 +1,11 @@
 # vim: set filetype=python:
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 DevToolsModules(
     'graphs.js',
     'marker-details.js',
-    'marker-view.js',
     'markers-overview.js',
     'tree-view.js',
-    'waterfall-ticks.js',
 )
deleted file mode 100644
--- a/devtools/client/performance/modules/widgets/waterfall-ticks.js
+++ /dev/null
@@ -1,193 +0,0 @@
-/* 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";
-
-/**
- * This file contains the "waterfall ticks" view, a header for the
- * markers displayed in the waterfall.
- */
-
-const { L10N } = require("devtools/client/performance/modules/global");
-const { WATERFALL_MARKER_SIDEBAR_WIDTH } = require("devtools/client/performance/modules/widgets/marker-view");
-
-const HTML_NS = "http://www.w3.org/1999/xhtml";
-
-const FIND_OPTIMAL_TICK_INTERVAL_MAX_ITERS = 100;
-
-// ms
-const WATERFALL_HEADER_TICKS_MULTIPLE = 5;
-// px
-const WATERFALL_HEADER_TICKS_SPACING_MIN = 50;
-// px
-const WATERFALL_HEADER_TEXT_PADDING = 3;
-
-// ms
-const WATERFALL_BACKGROUND_TICKS_MULTIPLE = 5;
-const WATERFALL_BACKGROUND_TICKS_SCALES = 3;
-// px
-const WATERFALL_BACKGROUND_TICKS_SPACING_MIN = 10;
-const WATERFALL_BACKGROUND_TICKS_COLOR_RGB = [128, 136, 144];
-// byte
-const WATERFALL_BACKGROUND_TICKS_OPACITY_MIN = 32;
-// byte
-const WATERFALL_BACKGROUND_TICKS_OPACITY_ADD = 32;
-
-/**
- * A header for a markers waterfall.
- *
- * @param MarkerView root
- *        The root item of the waterfall tree.
- */
-function WaterfallHeader(root) {
-  this.root = root;
-}
-
-WaterfallHeader.prototype = {
-  /**
-   * Creates and appends this header as the first element of the specified
-   * parent element.
-   *
-   * @param nsIDOMNode parentNode
-   *        The parent element for this header.
-   */
-  attachTo: function (parentNode) {
-    let document = parentNode.ownerDocument;
-    let startTime = this.root.interval.startTime;
-    let dataScale = this.root.getDataScale();
-    let waterfallWidth = this.root.getWaterfallWidth();
-
-    let header = this._buildNode(document, startTime, dataScale, waterfallWidth);
-    parentNode.insertBefore(header, parentNode.firstChild);
-
-    this._drawWaterfallBackground(document, dataScale, waterfallWidth);
-  },
-
-  /**
-   * Creates the node displaying this view.
-   */
-  _buildNode: function (doc, startTime, dataScale, waterfallWidth) {
-    let container = doc.createElement("hbox");
-    container.className = "waterfall-header-container";
-    container.setAttribute("flex", "1");
-
-    let sidebar = doc.createElement("hbox");
-    sidebar.className = "waterfall-sidebar theme-sidebar";
-    sidebar.setAttribute("width", WATERFALL_MARKER_SIDEBAR_WIDTH);
-    sidebar.setAttribute("align", "center");
-    container.appendChild(sidebar);
-
-    let name = doc.createElement("description");
-    name.className = "plain waterfall-header-name";
-    name.setAttribute("value", L10N.getStr("timeline.records"));
-    sidebar.appendChild(name);
-
-    let ticks = doc.createElement("hbox");
-    ticks.className = "waterfall-header-ticks waterfall-background-ticks";
-    ticks.setAttribute("align", "center");
-    ticks.setAttribute("flex", "1");
-    container.appendChild(ticks);
-
-    let tickInterval = findOptimalTickInterval({
-      ticksMultiple: WATERFALL_HEADER_TICKS_MULTIPLE,
-      ticksSpacingMin: WATERFALL_HEADER_TICKS_SPACING_MIN,
-      dataScale: dataScale
-    });
-
-    for (let x = 0; x < waterfallWidth; x += tickInterval) {
-      let left = x + WATERFALL_HEADER_TEXT_PADDING;
-      let time = Math.round(x / dataScale + startTime);
-      let label = L10N.getFormatStr("timeline.tick", time);
-
-      let node = doc.createElement("description");
-      node.className = "plain waterfall-header-tick";
-      node.style.transform = "translateX(" + left + "px)";
-      node.setAttribute("value", label);
-      ticks.appendChild(node);
-    }
-
-    return container;
-  },
-
-  /**
-   * Creates the background displayed on the marker's waterfall.
-   */
-  _drawWaterfallBackground: function (doc, dataScale, waterfallWidth) {
-    if (!this._canvas || !this._ctx) {
-      this._canvas = doc.createElementNS(HTML_NS, "canvas");
-      this._ctx = this._canvas.getContext("2d");
-    }
-    let canvas = this._canvas;
-    let ctx = this._ctx;
-
-    // Nuke the context.
-    let canvasWidth = canvas.width = waterfallWidth;
-    // Awww yeah, 1px, repeats on Y axis.
-    let canvasHeight = canvas.height = 1;
-
-    // Start over.
-    let imageData = ctx.createImageData(canvasWidth, canvasHeight);
-    let pixelArray = imageData.data;
-
-    let buf = new ArrayBuffer(pixelArray.length);
-    let view8bit = new Uint8ClampedArray(buf);
-    let view32bit = new Uint32Array(buf);
-
-    // Build new millisecond tick lines...
-    let [r, g, b] = WATERFALL_BACKGROUND_TICKS_COLOR_RGB;
-    let alphaComponent = WATERFALL_BACKGROUND_TICKS_OPACITY_MIN;
-    let tickInterval = findOptimalTickInterval({
-      ticksMultiple: WATERFALL_BACKGROUND_TICKS_MULTIPLE,
-      ticksSpacingMin: WATERFALL_BACKGROUND_TICKS_SPACING_MIN,
-      dataScale: dataScale
-    });
-
-    // Insert one pixel for each division on each scale.
-    for (let i = 1; i <= WATERFALL_BACKGROUND_TICKS_SCALES; i++) {
-      let increment = tickInterval * Math.pow(2, i);
-      for (let x = 0; x < canvasWidth; x += increment) {
-        let position = x | 0;
-        view32bit[position] = (alphaComponent << 24) | (b << 16) | (g << 8) | r;
-      }
-      alphaComponent += WATERFALL_BACKGROUND_TICKS_OPACITY_ADD;
-    }
-
-    // Flush the image data and cache the waterfall background.
-    pixelArray.set(view8bit);
-    ctx.putImageData(imageData, 0, 0);
-    doc.mozSetImageElement("waterfall-background", canvas);
-  }
-};
-
-/**
- * Finds the optimal tick interval between time markers in this timeline.
- *
- * @param number ticksMultiple
- * @param number ticksSpacingMin
- * @param number dataScale
- * @return number
- */
-function findOptimalTickInterval({ ticksMultiple, ticksSpacingMin, dataScale }) {
-  let timingStep = ticksMultiple;
-  let maxIters = FIND_OPTIMAL_TICK_INTERVAL_MAX_ITERS;
-  let numIters = 0;
-
-  if (dataScale > ticksSpacingMin) {
-    return dataScale;
-  }
-
-  while (true) {
-    let scaledStep = dataScale * timingStep;
-    if (++numIters > maxIters) {
-      return scaledStep;
-    }
-    if (scaledStep < ticksSpacingMin) {
-      timingStep <<= 1;
-      continue;
-    }
-    return scaledStep;
-  }
-}
-
-exports.WaterfallHeader = WaterfallHeader;
-exports.TickUtils = { findOptimalTickInterval };
--- a/devtools/client/performance/performance-controller.js
+++ b/devtools/client/performance/performance-controller.js
@@ -22,43 +22,42 @@ 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,
-   Services, promise, EventEmitter, DevToolsUtils, system */
+   Waterfall, Services, 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 Services = require("Services");
 var promise = require("promise");
 var EventEmitter = require("devtools/shared/event-emitter");
 var DevToolsUtils = require("devtools/shared/DevToolsUtils");
 var flags = require("devtools/shared/flags");
 var system = require("devtools/shared/system");
 
 // Logic modules
 /* exported L10N, PerformanceTelemetry, TIMELINE_BLUEPRINT, RecordingUtils,
-   PerformanceUtils, OptimizationsGraph, GraphsController, WaterfallHeader, MarkerView,
+   PerformanceUtils, OptimizationsGraph, GraphsController,
    MarkerDetails, MarkerBlueprintUtils, WaterfallUtils, FrameUtils, CallView, ThreadNode,
    FrameNode */
 var { L10N } = require("devtools/client/performance/modules/global");
 var { PerformanceTelemetry } = require("devtools/client/performance/modules/logic/telemetry");
 var { TIMELINE_BLUEPRINT } = require("devtools/client/performance/modules/markers");
 var RecordingUtils = require("devtools/shared/performance/recording-utils");
 var PerformanceUtils = require("devtools/client/performance/modules/utils");
 var { OptimizationsGraph, GraphsController } = require("devtools/client/performance/modules/widgets/graphs");
-var { WaterfallHeader } = require("devtools/client/performance/modules/widgets/waterfall-ticks");
-var { MarkerView } = require("devtools/client/performance/modules/widgets/marker-view");
 var { MarkerDetails } = require("devtools/client/performance/modules/widgets/marker-details");
 var { MarkerBlueprintUtils } = require("devtools/client/performance/modules/marker-blueprint-utils");
 var WaterfallUtils = require("devtools/client/performance/modules/logic/waterfall-utils");
 var FrameUtils = require("devtools/client/performance/modules/logic/frame-utils");
 var { CallView } = require("devtools/client/performance/modules/widgets/tree-view");
 var { ThreadNode } = require("devtools/client/performance/modules/logic/tree-model");
 var { FrameNode } = require("devtools/client/performance/modules/logic/tree-model");
 
--- a/devtools/client/performance/performance.xul
+++ b/devtools/client/performance/performance.xul
@@ -245,20 +245,17 @@
                      value="&performanceUI.bufferStatusFull;"/>
             </vbox>
 
             <!-- Detail views -->
             <deck id="details-pane" flex="1">
 
               <!-- Waterfall -->
               <hbox id="waterfall-view" flex="1">
-                <vbox flex="1">
-                  <hbox id="waterfall-header" />
-                  <vbox id="waterfall-breakdown" flex="1" />
-                </vbox>
+                <html:div xmlns="http://www.w3.org/1999/xhtml" id="waterfall-tree" />
                 <splitter class="devtools-side-splitter"/>
                 <vbox id="waterfall-details"
                       class="theme-sidebar"/>
               </hbox>
 
               <!-- JS Tree and JIT view -->
               <hbox id="js-profile-view" flex="1">
                 <vbox id="js-calltree-view" flex="1">
--- a/devtools/client/performance/test/browser_timeline-waterfall-background.js
+++ b/devtools/client/performance/test/browser_timeline-waterfall-background.js
@@ -25,21 +25,17 @@ add_task(function* () {
   // Ensure overview is rendering and some markers were received.
   yield waitForOverviewRenderedWithMarkers(panel);
 
   yield stopRecording(panel);
   ok(true, "Recording has ended.");
 
   // Test the waterfall background.
 
-  ok(WaterfallView._waterfallHeader._canvas,
-    "A canvas should be created after the recording ended.");
-  ok(WaterfallView._waterfallHeader._ctx,
-    "A 2d context should be created after the recording ended.");
+  ok(WaterfallView.canvas, "A canvas should be created after the recording ended.");
 
-  is(WaterfallView._waterfallHeader._canvas.width,
-    WaterfallView._markersRoot._waterfallWidth,
+  is(WaterfallView.canvas.width, WaterfallView.waterfallWidth,
     "The canvas width is correct.");
-  is(WaterfallView._waterfallHeader._canvas.height, 1,
+  is(WaterfallView.canvas.height, 1,
     "The canvas height is correct.");
 
   yield teardownToolboxAndRemoveTab(panel);
 });
--- a/devtools/client/performance/test/browser_timeline-waterfall-generic.js
+++ b/devtools/client/performance/test/browser_timeline-waterfall-generic.js
@@ -1,17 +1,16 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 "use strict";
 
 /**
  * Tests if the waterfall is properly built after finishing a recording.
  */
 
-const { WATERFALL_MARKER_SIDEBAR_SAFE_BOUNDS } = require("devtools/client/performance/modules/widgets/marker-view");
 const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
 const { initPerformanceInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
 const { startRecording, stopRecording, waitForOverviewRenderedWithMarkers } = require("devtools/client/performance/test/helpers/actions");
 const { once } = require("devtools/client/performance/test/helpers/event-utils");
 
 add_task(function* () {
   let { panel } = yield initPerformanceInNewTab({
     url: SIMPLE_URL,
@@ -26,25 +25,23 @@ add_task(function* () {
   // Ensure overview is rendering and some markers were received.
   yield waitForOverviewRenderedWithMarkers(panel);
 
   yield stopRecording(panel);
   ok(true, "Recording has ended.");
 
   // Test the header container.
 
-  ok($(".waterfall-header-container"),
+  ok($(".waterfall-header"),
     "A header container should have been created.");
 
   // Test the header sidebar (left).
 
-  ok($(".waterfall-header-container > .waterfall-sidebar"),
+  ok($(".waterfall-header > .waterfall-sidebar"),
     "A header sidebar node should have been created.");
-  ok($(".waterfall-header-container > .waterfall-sidebar > .waterfall-header-name"),
-    "A header name label should have been created inside the sidebar.");
 
   // Test the header ticks (right).
 
   ok($(".waterfall-header-ticks"),
     "A header ticks node should have been created.");
   ok($$(".waterfall-header-ticks > .waterfall-header-tick").length > 0,
     "Some header tick labels should have been created inside the tick node.");
 
@@ -56,55 +53,53 @@ add_task(function* () {
     "Some marker color bullets should have been created inside the sidebar.");
   ok($$(".waterfall-tree-item > .waterfall-sidebar > .waterfall-marker-name").length,
     "Some marker name labels should have been created inside the sidebar.");
 
   // Test the markers waterfall (right).
 
   ok($$(".waterfall-tree-item > .waterfall-marker").length,
     "Some marker waterfall nodes should have been created.");
-  ok($$(".waterfall-tree-item > .waterfall-marker > .waterfall-marker-bar").length,
+  ok($$(".waterfall-tree-item > .waterfall-marker .waterfall-marker-bar").length,
     "Some marker color bars should have been created inside the waterfall.");
 
   // Test the sidebar.
 
   let detailsView = WaterfallView.details;
-  let markersRoot = WaterfallView._markersRoot;
   // Make sure the bounds are up to date.
-  markersRoot.recalculateBounds();
+  WaterfallView._recalculateBounds();
 
   let parentWidthBefore = $("#waterfall-view").getBoundingClientRect().width;
   let sidebarWidthBefore = $(".waterfall-sidebar").getBoundingClientRect().width;
   let detailsWidthBefore = $("#waterfall-details").getBoundingClientRect().width;
 
   ok(detailsView.hidden,
     "The details view in the waterfall view is hidden by default.");
   is(detailsWidthBefore, 0,
     "The details view width should be 0 when hidden.");
-  is(markersRoot._waterfallWidth,
-     parentWidthBefore - sidebarWidthBefore - WATERFALL_MARKER_SIDEBAR_SAFE_BOUNDS,
+  is(WaterfallView.waterfallWidth,
+     parentWidthBefore - sidebarWidthBefore
+                       - WaterfallView.WATERFALL_MARKER_SIDEBAR_SAFE_BOUNDS,
      "The waterfall width is correct (1).");
 
-  let receivedFocusEvent = once(markersRoot, "focus");
   let waterfallRerendered = once(WaterfallView, EVENTS.UI_WATERFALL_RENDERED);
-  WaterfallView._markersRoot.getChild(0).focus();
-  yield receivedFocusEvent;
+  $$(".waterfall-tree-item")[0].click();
   yield waterfallRerendered;
 
   let parentWidthAfter = $("#waterfall-view").getBoundingClientRect().width;
   let sidebarWidthAfter = $(".waterfall-sidebar").getBoundingClientRect().width;
   let detailsWidthAfter = $("#waterfall-details").getBoundingClientRect().width;
 
   ok(!detailsView.hidden,
     "The details view in the waterfall view is now visible.");
   is(parentWidthBefore, parentWidthAfter,
     "The parent view's width should not have changed.");
   is(sidebarWidthBefore, sidebarWidthAfter,
     "The sidebar view's width should not have changed.");
   isnot(detailsWidthAfter, 0,
     "The details view width should not be 0 when visible.");
-  is(markersRoot._waterfallWidth,
+  is(WaterfallView.waterfallWidth,
      parentWidthAfter - sidebarWidthAfter - detailsWidthAfter
-                      - WATERFALL_MARKER_SIDEBAR_SAFE_BOUNDS,
+                      - WaterfallView.WATERFALL_MARKER_SIDEBAR_SAFE_BOUNDS,
      "The waterfall width is correct (2).");
 
   yield teardownToolboxAndRemoveTab(panel);
 });
--- a/devtools/client/performance/views/details-waterfall.js
+++ b/devtools/client/performance/views/details-waterfall.js
@@ -5,23 +5,29 @@
 /* import-globals-from ../performance-view.js */
 /* globals window, DetailsSubview */
 "use strict";
 
 const MARKER_DETAILS_WIDTH = 200;
 // Units are in milliseconds.
 const WATERFALL_RESIZE_EVENTS_DRAIN = 100;
 
+const { TickUtils } = require("devtools/client/performance/modules/waterfall-ticks");
+
 /**
  * Waterfall view containing the timeline markers, controlled by DetailsView.
  */
 var WaterfallView = Heritage.extend(DetailsSubview, {
 
   // Smallest unit of time between two markers. Larger by 10x^3 than Number.EPSILON.
   MARKER_EPSILON: 0.000000000001,
+  // px
+  WATERFALL_MARKER_SIDEBAR_WIDTH: 175,
+  // px
+  WATERFALL_MARKER_SIDEBAR_SAFE_BOUNDS: 20,
 
   observedPrefs: [
     "hidden-markers"
   ],
 
   rerenderPrefs: [
     "hidden-markers"
   ],
@@ -38,18 +44,17 @@ var WaterfallView = Heritage.extend(Deta
     this._cache = new WeakMap();
 
     this._onMarkerSelected = this._onMarkerSelected.bind(this);
     this._onResize = this._onResize.bind(this);
     this._onViewSource = this._onViewSource.bind(this);
     this._onShowAllocations = this._onShowAllocations.bind(this);
     this._hiddenMarkers = PerformanceController.getPref("hidden-markers");
 
-    this.headerContainer = $("#waterfall-header");
-    this.breakdownContainer = $("#waterfall-breakdown");
+    this.treeContainer = $("#waterfall-tree");
     this.detailsContainer = $("#waterfall-details");
     this.detailsSplitter = $("#waterfall-view > splitter");
 
     this.details = new MarkerDetails($("#waterfall-details"),
                                      $("#waterfall-view > splitter"));
     this.details.hidden = true;
 
     this.details.on("resize", this._onResize);
@@ -70,16 +75,18 @@ var WaterfallView = Heritage.extend(Deta
     clearNamedTimeout("waterfall-resize");
 
     this._cache = null;
 
     this.details.off("resize", this._onResize);
     this.details.off("view-source", this._onViewSource);
     this.details.off("show-allocations", this._onShowAllocations);
     window.removeEventListener("resize", this._onResize);
+
+    ReactDOM.unmountComponentAtNode(this.treeContainer);
   },
 
   /**
    * Method for handling all the set up for rendering a new waterfall.
    *
    * @param object interval [optional]
    *        The { startTime, endTime }, in milliseconds.
    */
@@ -104,29 +111,27 @@ var WaterfallView = Heritage.extend(Deta
   _onMarkerSelected: function (event, marker) {
     let recording = PerformanceController.getCurrentRecording();
     let frames = recording.getFrames();
     let allocations = recording.getConfiguration().withAllocations;
 
     if (event === "selected") {
       this.details.render({ marker, frames, allocations });
       this.details.hidden = false;
-      this._lastSelected = marker;
     }
     if (event === "unselected") {
       this.details.empty();
     }
   },
 
   /**
    * Called when the marker details view is resized.
    */
   _onResize: function () {
     setNamedTimeout("waterfall-resize", WATERFALL_RESIZE_EVENTS_DRAIN, () => {
-      this._markersRoot.recalculateBounds();
       this.render(OverviewView.getTimeInterval());
     });
   },
 
   /**
    * Called whenever an observed pref is changed.
    */
   _onObservedPrefChange: function (_, prefName) {
@@ -201,49 +206,47 @@ var WaterfallView = Heritage.extend(Deta
       filter: this._hiddenMarkers
     });
 
     this._cache.set(markers, rootMarkerNode);
     return rootMarkerNode;
   },
 
   /**
+   * Calculates the available width for the waterfall.
+   * This should be invoked every time the container node is resized.
+   */
+  _recalculateBounds: function () {
+    this.waterfallWidth = this.treeContainer.clientWidth
+      - this.WATERFALL_MARKER_SIDEBAR_WIDTH
+      - this.WATERFALL_MARKER_SIDEBAR_SAFE_BOUNDS;
+  },
+
+  /**
    * Renders the waterfall tree.
    */
   _populateWaterfallTree: function (rootMarkerNode, interval) {
-    let root = new MarkerView({
+    this._recalculateBounds();
+
+    let doc = this.treeContainer.ownerDocument;
+    let startTime = interval.startTime | 0;
+    let endTime = interval.endTime | 0;
+    let dataScale = this.waterfallWidth / (endTime - startTime);
+
+    this.canvas = TickUtils.drawWaterfallBackground(doc, dataScale, this.waterfallWidth);
+
+    let treeView = Waterfall({
       marker: rootMarkerNode,
-      // The root node is irrelevant in a waterfall tree.
-      hidden: true,
-      // The waterfall tree should not expand by default.
-      autoExpandDepth: 0
+      startTime,
+      endTime,
+      dataScale,
+      sidebarWidth: this.WATERFALL_MARKER_SIDEBAR_WIDTH,
+      waterfallWidth: this.waterfallWidth,
+      onFocus: node => this._onMarkerSelected("selected", node)
     });
 
-    let header = new WaterfallHeader(root);
-
-    this._markersRoot = root;
-    this._waterfallHeader = header;
-
-    root.filter = this._hiddenMarkers;
-    root.interval = interval;
-    root.on("selected", this._onMarkerSelected);
-    root.on("unselected", this._onMarkerSelected);
-
-    this.breakdownContainer.innerHTML = "";
-    root.attachTo(this.breakdownContainer);
-
-    this.headerContainer.innerHTML = "";
-    header.attachTo(this.headerContainer);
-
-    // If an item was previously selected in this view, attempt to
-    // re-select it by traversing the newly created tree.
-    if (this._lastSelected) {
-      let item = root.find(i => i.marker === this._lastSelected);
-      if (item) {
-        item.focus();
-      }
-    }
+    ReactDOM.render(treeView, this.treeContainer);
   },
 
   toString: () => "[object WaterfallView]"
 });
 
 EventEmitter.decorate(WaterfallView);
--- a/devtools/client/themes/performance.css
+++ b/devtools/client/themes/performance.css
@@ -393,20 +393,41 @@
 }
 
 .call-tree-category {
   transform: scale(0.75);
   transform-origin: center right;
 }
 
 /**
- * Waterfall ticks header
+ * Waterfall markers tree
  */
 
+#waterfall-tree {
+  /* DE-XUL: convert this to display: flex once performance.xul is converted to HTML */
+  display: -moz-box;
+  -moz-box-orient: vertical;
+  -moz-box-flex: 1;
+}
+
+.waterfall-markers {
+  /* DE-XUL: convert this to display: flex once performance.xul is converted to HTML */
+  display: -moz-box;
+  -moz-box-orient: vertical;
+  -moz-box-flex: 1;
+}
+
+.waterfall-header {
+  display: flex;
+}
+
 .waterfall-header-ticks {
+  display: flex;
+  flex: auto;
+  align-items: center;
   overflow: hidden;
 }
 
 .waterfall-header-name {
   padding: 2px 4px;
   font-size: 90%;
 }
 
@@ -418,151 +439,176 @@
 }
 
 .waterfall-header-tick:not(:first-child) {
   margin-inline-start: -100px !important; /* Don't affect layout. */
 }
 
 .waterfall-background-ticks {
   /* Background created on a <canvas> in js. */
-  /* @see devtools/client/timeline/widgets/waterfall.js */
+  /* @see devtools/client/performance/modules/widgets/waterfall-ticks.js */
   background-image: -moz-element(#waterfall-background);
   background-repeat: repeat-y;
   background-position: -1px center;
 }
 
 /**
  * Markers waterfall breakdown
  */
 
-#waterfall-breakdown {
+.waterfall-markers .tree {
+  /* DE-XUL: convert this to display: flex once performance.xul is converted to HTML */
+  display: -moz-box;
+  -moz-box-orient: vertical;
+  -moz-box-flex: 1;
   overflow-x: hidden;
   overflow-y: auto;
+  --waterfall-tree-row-height: 15px;
 }
 
-.theme-light .waterfall-tree-item:not([level="0"]) {
+.waterfall-markers .tree-node {
+  display: flex;
+  height: var(--waterfall-tree-row-height);
+  line-height: var(--waterfall-tree-row-height);
+}
+
+.waterfall-tree-item {
+  display: flex;
+  flex: auto;
+}
+
+.theme-light .waterfall-markers .tree-node:not([data-depth="0"]) {
   background-image: repeating-linear-gradient(
     -45deg,
     transparent 0px,
     transparent 2px,
     rgba(0,0,0,0.025) 2px,
     rgba(0,0,0,0.025) 4px
   );
 }
 
-.theme-dark .waterfall-tree-item:not([level="0"]) {
+.theme-dark .waterfall-markers .tree-node:not([data-depth="0"]) {
   background-image: repeating-linear-gradient(
     -45deg,
     transparent 0px,
     transparent 2px,
     rgba(255,255,255,0.05) 2px,
     rgba(255,255,255,0.05) 4px
   );
 }
 
-.theme-light .waterfall-tree-item[expandable] .waterfall-marker-bullet,
-.theme-light .waterfall-tree-item[expandable] .waterfall-marker-bar {
+.theme-light .waterfall-tree-item[data-expandable] .waterfall-marker-bullet,
+.theme-light .waterfall-tree-item[data-expandable] .waterfall-marker-bar {
   background-image: repeating-linear-gradient(
     -45deg,
     transparent 0px,
     transparent 5px,
     rgba(255,255,255,0.35) 5px,
     rgba(255,255,255,0.35) 10px
   );
 }
 
-.theme-dark .waterfall-tree-item[expandable] .waterfall-marker-bullet,
-.theme-dark .waterfall-tree-item[expandable] .waterfall-marker-bar {
+.theme-dark .waterfall-tree-item[data-expandable] .waterfall-marker-bullet,
+.theme-dark .waterfall-tree-item[data-expandable] .waterfall-marker-bar {
   background-image: repeating-linear-gradient(
     -45deg,
     transparent 0px,
     transparent 5px,
     rgba(0,0,0,0.35) 5px,
     rgba(0,0,0,0.35) 10px
   );
 }
 
-.waterfall-tree-item[expanded],
-.waterfall-tree-item:not([level="0"]) + .waterfall-tree-item[level="0"] {
+.waterfall-markers .tree-node[data-expanded],
+.waterfall-markers .tree-node:not([data-depth="0"]) + .tree-node[data-depth="0"] {
   box-shadow: 0 -1px var(--cell-border-color-light);
 }
 
-.waterfall-tree-item:nth-child(2n) > .waterfall-marker {
+.tree-node-odd .waterfall-marker {
   background-color: var(--row-alt-background-color);
 }
 
-.waterfall-tree-item:hover {
+.waterfall-markers .tree-node:hover {
   background-color: var(--row-hover-background-color);
 }
 
-.waterfall-tree-item:last-child {
+.waterfall-markers .tree-node-last {
   border-bottom: 1px solid var(--cell-border-color);
 }
 
-.waterfall-tree-item:focus {
+.waterfall-tree-item.focused {
   background-color: var(--theme-selection-background);
 }
 
-.waterfall-tree-item:focus description {
-  color: var(--theme-selection-color) !important;
-}
-
 /**
  * Marker left sidebar
  */
 
 .waterfall-sidebar {
+  display: flex;
+  align-items: center;
+  box-sizing: border-box;
   border-inline-end: 1px solid var(--cell-border-color);
 }
 
 .waterfall-tree-item > .waterfall-sidebar:hover,
 .waterfall-tree-item:hover > .waterfall-sidebar,
-.waterfall-tree-item:focus > .waterfall-sidebar {
+.waterfall-tree-item.focused > .waterfall-sidebar {
   background: transparent;
 }
 
+.waterfall-tree-item.focused > .waterfall-sidebar {
+  color: var(--theme-selection-color);
+}
+
 .waterfall-marker-bullet {
   width: 8px;
   height: 8px;
   margin-inline-start: 8px;
   margin-inline-end: 6px;
   border-radius: 1px;
+  box-sizing: border-box;
 }
 
 .waterfall-marker-name {
   font-size: 95%;
   padding-bottom: 1px !important;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
 }
 
 /**
  * Marker timebar
  */
 
 .waterfall-marker {
+  display: flex;
+  flex: auto;
   overflow: hidden;
 }
 
+.waterfall-marker-wrap {
+  display: flex;
+  align-items: center;
+  transform-origin: left center;
+}
+
 .waterfall-marker-bar {
   height: 9px;
-  transform-origin: left center;
   border-radius: 1px;
-}
-
-.waterfall-marker > .theme-twisty {
-  /* Don't affect layout. */
-  width: 14px;
-  margin-inline-end: -14px;
+  box-sizing: border-box;
 }
 
 /**
  * OTMT markers
  */
 
-.waterfall-tree-item[otmt=true] .waterfall-marker-bullet,
-.waterfall-tree-item[otmt=true] .waterfall-marker-bar {
+.waterfall-tree-item[data-otmt=true] .waterfall-marker-bullet,
+.waterfall-tree-item[data-otmt=true] .waterfall-marker-bar {
   background-color: transparent;
   border-width: 1px;
   border-style: solid;
 }
 
 /**
  * Marker details view
  */