Bug 1077459 - Add flamechart view to Details view in new performance tool, r=jsantell
authorVictor Porof <vporof@mozilla.com>
Sun, 11 Jan 2015 11:45:23 -0500
changeset 249093 5032626927f25877c355a1e8e64f012fcb8e0937
parent 249092 3c5db371a6385189c4d2d7d23d8d3e6113a514d4
child 249094 3c5a88aaae96aa0f1cc464e0d019023057d89e11
push id4489
push userraliiev@mozilla.com
push dateMon, 23 Feb 2015 15:17:55 +0000
treeherdermozilla-beta@fd7c3dc24146 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjsantell
bugs1077459
milestone37.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1077459 - Add flamechart view to Details view in new performance tool, r=jsantell
browser/devtools/jar.mn
browser/devtools/performance/performance-controller.js
browser/devtools/performance/performance.xul
browser/devtools/performance/test/browser.ini
browser/devtools/performance/test/browser_perf-details-flamegraph-render-01.js
browser/devtools/performance/test/browser_perf-details.js
browser/devtools/performance/views/details-flamegraph.js
browser/devtools/performance/views/details.js
browser/devtools/shared/widgets/FlameGraph.jsm
browser/locales/en-US/chrome/browser/devtools/profiler.properties
browser/themes/shared/devtools/images/performance-icons.svg
browser/themes/shared/devtools/performance.inc.css
--- a/browser/devtools/jar.mn
+++ b/browser/devtools/jar.mn
@@ -92,16 +92,17 @@ browser.jar:
 #ifdef MOZ_DEVTOOLS_PERFTOOLS
     content/browser/devtools/performance.xul                           (performance/performance.xul)
     content/browser/devtools/performance/performance-controller.js     (performance/performance-controller.js)
     content/browser/devtools/performance/performance-view.js           (performance/performance-view.js)
     content/browser/devtools/performance/views/overview.js             (performance/views/overview.js)
     content/browser/devtools/performance/views/details.js              (performance/views/details.js)
     content/browser/devtools/performance/views/details-call-tree.js    (performance/views/details-call-tree.js)
     content/browser/devtools/performance/views/details-waterfall.js    (performance/views/details-waterfall.js)
+    content/browser/devtools/performance/views/details-flamegraph.js   (performance/views/details-flamegraph.js)
 #endif
     content/browser/devtools/responsivedesign/resize-commands.js       (responsivedesign/resize-commands.js)
     content/browser/devtools/commandline.css                           (commandline/commandline.css)
     content/browser/devtools/commandlineoutput.xhtml                   (commandline/commandlineoutput.xhtml)
     content/browser/devtools/commandlinetooltip.xhtml                  (commandline/commandlinetooltip.xhtml)
     content/browser/devtools/commandline/commands-index.js             (commandline/commands-index.js)
     content/browser/devtools/framework/toolbox-window.xul              (framework/toolbox-window.xul)
     content/browser/devtools/framework/toolbox-options.xul             (framework/toolbox-options.xul)
--- a/browser/devtools/performance/performance-controller.js
+++ b/browser/devtools/performance/performance-controller.js
@@ -12,40 +12,46 @@ Cu.import("resource:///modules/devtools/
 
 devtools.lazyRequireGetter(this, "Services");
 devtools.lazyRequireGetter(this, "promise");
 devtools.lazyRequireGetter(this, "EventEmitter",
   "devtools/toolkit/event-emitter");
 devtools.lazyRequireGetter(this, "DevToolsUtils",
   "devtools/toolkit/DevToolsUtils");
 
+devtools.lazyRequireGetter(this, "TIMELINE_BLUEPRINT",
+  "devtools/timeline/global", true);
 devtools.lazyRequireGetter(this, "L10N",
   "devtools/profiler/global", true);
 devtools.lazyRequireGetter(this, "PerformanceIO",
   "devtools/performance/io", true);
 devtools.lazyRequireGetter(this, "MarkersOverview",
   "devtools/timeline/markers-overview", true);
 devtools.lazyRequireGetter(this, "MemoryOverview",
   "devtools/timeline/memory-overview", true);
 devtools.lazyRequireGetter(this, "Waterfall",
   "devtools/timeline/waterfall", true);
 devtools.lazyRequireGetter(this, "MarkerDetails",
   "devtools/timeline/marker-details", true);
 devtools.lazyRequireGetter(this, "CallView",
   "devtools/profiler/tree-view", true);
 devtools.lazyRequireGetter(this, "ThreadNode",
   "devtools/profiler/tree-model", true);
-devtools.lazyRequireGetter(this, "TIMELINE_BLUEPRINT",
-  "devtools/timeline/global", true);
+
 
 devtools.lazyImporter(this, "CanvasGraphUtils",
   "resource:///modules/devtools/Graphs.jsm");
 devtools.lazyImporter(this, "LineGraphWidget",
   "resource:///modules/devtools/Graphs.jsm");
 
+devtools.lazyImporter(this, "FlameGraphUtils",
+  "resource:///modules/devtools/FlameGraph.jsm");
+devtools.lazyImporter(this, "FlameGraph",
+  "resource:///modules/devtools/FlameGraph.jsm");
+
 // Events emitted by various objects in the panel.
 const EVENTS = {
   // Emitted by the PerformanceView on record button click
   UI_START_RECORDING: "Performance:UI:StartRecording",
   UI_STOP_RECORDING: "Performance:UI:StopRecording",
 
   // Emitted by the PerformanceView on import or export button click
   UI_IMPORT_RECORDING: "Performance:UI:ImportRecording",
@@ -79,17 +85,20 @@ const EVENTS = {
   // Emitted by the CallTreeView when a call tree has been rendered
   CALL_TREE_RENDERED: "Performance:UI:CallTreeRendered",
 
   // When a source is shown in the JavaScript Debugger at a specific location.
   SOURCE_SHOWN_IN_JS_DEBUGGER: "Performance:UI:SourceShownInJsDebugger",
   SOURCE_NOT_FOUND_IN_JS_DEBUGGER: "Performance:UI:SourceNotFoundInJsDebugger",
 
   // Emitted by the WaterfallView when it has been rendered
-  WATERFALL_RENDERED: "Performance:UI:WaterfallRendered"
+  WATERFALL_RENDERED: "Performance:UI:WaterfallRendered",
+
+  // Emitted by the FlameGraphView when it has been rendered
+  FLAMEGRAPH_RENDERED: "Performance:UI:FlameGraphRendered"
 };
 
 // Constant defining the end time for a recording that hasn't finished
 // or is not yet available.
 const RECORDING_IN_PROGRESS = -1;
 const RECORDING_UNAVAILABLE = null;
 
 /**
--- a/browser/devtools/performance/performance.xul
+++ b/browser/devtools/performance/performance.xul
@@ -15,16 +15,17 @@
 <window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
   <script src="chrome://browser/content/devtools/theme-switching.js"/>
   <script type="application/javascript" src="performance/performance-controller.js"/>
   <script type="application/javascript" src="performance/performance-view.js"/>
   <script type="application/javascript" src="performance/views/overview.js"/>
   <script type="application/javascript" src="performance/views/details.js"/>
   <script type="application/javascript" src="performance/views/details-call-tree.js"/>
   <script type="application/javascript" src="performance/views/details-waterfall.js"/>
+  <script type="application/javascript" src="performance/views/details-flamegraph.js"/>
 
   <vbox class="theme-body" flex="1">
     <toolbar id="performance-toolbar" class="devtools-toolbar">
       <hbox id="performance-toolbar-controls-recordings" class="devtools-toolbarbutton-group">
         <toolbarbutton id="record-button"
                        class="devtools-toolbarbutton"
                        tooltiptext="&profilerUI.recordButton.tooltip;"/>
         <toolbarbutton id="clear-button"
@@ -47,36 +48,37 @@
       <hbox id="markers-overview"/>
       <hbox id="memory-overview"/>
     </vbox>
 
     <toolbar id="details-toolbar" class="devtools-toolbar">
       <hbox class="devtools-toolbarbutton-group">
         <toolbarbutton id="select-waterfall-view"
                        class="devtools-toolbarbutton"
-                       tooltiptext="waterfall"
                        data-view="waterfall" />
         <toolbarbutton id="select-calltree-view"
                        class="devtools-toolbarbutton"
-                       tooltiptext="calltree"
                        data-view="calltree" />
+        <toolbarbutton id="select-flamegraph-view"
+                       class="devtools-toolbarbutton"
+                       data-view="flamegraph" />
       </hbox>
     </toolbar>
 
     <deck id="details-pane" flex="1">
-      <hbox id="waterfall-view">
+      <hbox id="waterfall-view" flex="1">
         <vbox id="waterfall-graph" flex="1" />
         <splitter class="devtools-side-splitter"/>
         <vbox id="waterfall-details"
               class="theme-sidebar"
               width="150"
               height="150"/>
       </hbox>
 
-      <vbox id="calltree-view" class="call-tree" flex="1">
+      <vbox id="calltree-view" flex="1">
         <hbox class="call-tree-headers-container">
           <label class="plain call-tree-header"
                  type="duration"
                  crop="end"
                  value="&profilerUI.table.totalDuration;"/>
           <label class="plain call-tree-header"
                  type="percentage"
                  crop="end"
@@ -95,11 +97,14 @@
                  value="&profilerUI.table.samples;"/>
           <label class="plain call-tree-header"
                  type="function"
                  crop="end"
                  value="&profilerUI.table.function;"/>
         </hbox>
         <vbox class="call-tree-cells-container" flex="1"/>
       </vbox>
+
+      <hbox id="flamegraph-view" flex="1">
+      </hbox>
     </deck>
   </vbox>
 </window>
--- a/browser/devtools/performance/test/browser.ini
+++ b/browser/devtools/performance/test/browser.ini
@@ -8,16 +8,17 @@ support-files =
 # Commented out tests are profiler tests
 # that need to be moved over to performance tool
 
 [browser_perf-aaa-run-first-leaktest.js]
 [browser_perf-data-massaging-01.js]
 [browser_perf-data-samples.js]
 [browser_perf-details-calltree-render-01.js]
 [browser_perf-details-calltree-render-02.js]
+[browser_perf-details-flamegraph-render-01.js]
 [browser_perf-details-waterfall-render-01.js]
 [browser_perf-details.js]
 [browser_perf-front-basic-profiler-01.js]
 [browser_perf-front-basic-timeline-01.js]
 #[browser_perf-front-profiler-01.js] bug 1077464
 [browser_perf-front-profiler-02.js]
 [browser_perf-front-profiler-03.js]
 [browser_perf-front-profiler-04.js]
new file mode 100644
--- /dev/null
+++ b/browser/devtools/performance/test/browser_perf-details-flamegraph-render-01.js
@@ -0,0 +1,22 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that the flamegraph view renders content after recording.
+ */
+function spawnTest () {
+  let { panel } = yield initPerformance(SIMPLE_URL);
+  let { EVENTS, PerformanceController, FlameGraphView } = panel.panelWin;
+
+  yield startRecording(panel);
+  yield waitUntil(() => PerformanceController.getMarkers().length);
+
+  let rendered = once(FlameGraphView, EVENTS.FLAMEGRAPH_RENDERED);
+  yield stopRecording(panel);
+  yield rendered;
+
+  ok(true, "FlameGraphView rendered after recording is stopped.");
+
+  yield teardown(panel);
+  finish();
+}
--- a/browser/devtools/performance/test/browser_perf-details.js
+++ b/browser/devtools/performance/test/browser_perf-details.js
@@ -20,16 +20,23 @@ function spawnTest () {
 
   // Select waterfall view
   viewChanged = onceSpread(DetailsView, EVENTS.DETAILS_VIEW_SELECTED);
   command($("toolbarbutton[data-view='waterfall']"));
   [_, viewName] = yield viewChanged;
   is(viewName, "waterfall", "DETAILS_VIEW_SELECTED fired with view name");
   checkViews(DetailsView, doc, "waterfall");
 
+  // Select flamegraph view
+  viewChanged = onceSpread(DetailsView, EVENTS.DETAILS_VIEW_SELECTED);
+  command($("toolbarbutton[data-view='flamegraph']"));
+  [_, viewName] = yield viewChanged;
+  is(viewName, "flamegraph", "DETAILS_VIEW_SELECTED fired with view name");
+  checkViews(DetailsView, doc, "flamegraph");
+
   yield teardown(panel);
   finish();
 }
 
 function checkViews (DetailsView, doc, currentView) {
   for (let viewName in DetailsView.viewIndexes) {
     let button = doc.querySelector(`toolbarbutton[data-view="${viewName}"]`);
 
new file mode 100644
--- /dev/null
+++ b/browser/devtools/performance/views/details-flamegraph.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";
+
+/**
+ * FlameGraph view containing a pyramid-like visualization of a profile,
+ * controlled by DetailsView.
+ */
+let FlameGraphView = {
+  /**
+   * Sets up the view with event binding.
+   */
+  initialize: Task.async(function* () {
+    this._onRecordingStopped = this._onRecordingStopped.bind(this);
+    this._onRangeChange = this._onRangeChange.bind(this);
+
+    this.graph = new FlameGraph($("#flamegraph-view"));
+    this.graph.timelineTickUnits = L10N.getStr("graphs.ms");
+    yield this.graph.ready();
+
+    PerformanceController.on(EVENTS.RECORDING_STOPPED, this._onRecordingStopped);
+    OverviewView.on(EVENTS.OVERVIEW_RANGE_SELECTED, this._onRangeChange);
+    OverviewView.on(EVENTS.OVERVIEW_RANGE_CLEARED, this._onRangeChange);
+  }),
+
+  /**
+   * Unbinds events.
+   */
+  destroy: function () {
+    PerformanceController.off(EVENTS.RECORDING_STOPPED, this._onRecordingStopped);
+    OverviewView.off(EVENTS.OVERVIEW_RANGE_SELECTED, this._onRangeChange);
+    OverviewView.off(EVENTS.OVERVIEW_RANGE_CLEARED, this._onRangeChange);
+  },
+
+  /**
+   * Method for handling all the set up for rendering a new flamegraph.
+   */
+  render: function (profilerData) {
+    // Empty recordings might yield no profiler data.
+    if (profilerData.profile == null) {
+      return;
+    }
+    let samples = profilerData.profile.threads[0].samples;
+    let dataSrc = FlameGraphUtils.createFlameGraphDataFromSamples(samples);
+    this.graph.setData(dataSrc);
+    this.emit(EVENTS.FLAMEGRAPH_RENDERED);
+  },
+
+  /**
+   * Called when recording is stopped.
+   */
+  _onRecordingStopped: function () {
+    let profilerData = PerformanceController.getProfilerData();
+    this.render(profilerData);
+  },
+
+  /**
+   * Fired when a range is selected or cleared in the OverviewView.
+   */
+  _onRangeChange: function (_, params) {
+    // TODO bug 1105014
+  }
+};
+
+/**
+ * Convenient way of emitting events from the view.
+ */
+EventEmitter.decorate(FlameGraphView);
--- a/browser/devtools/performance/views/details.js
+++ b/browser/devtools/performance/views/details.js
@@ -10,64 +10,68 @@ const DEFAULT_DETAILS_SUBVIEW = "waterfa
  * subviews and toggles visibility between them.
  */
 let DetailsView = {
   /**
    * Name to index mapping of subviews, used by selecting view.
    */
   viewIndexes: {
     waterfall: 0,
-    calltree: 1
+    calltree: 1,
+    flamegraph: 2
   },
 
   /**
    * Sets up the view with event binding, initializes subviews.
    */
   initialize: Task.async(function *() {
     this.el = $("#details-pane");
 
     this._onViewToggle = this._onViewToggle.bind(this);
 
     for (let button of $$("toolbarbutton[data-view]", $("#details-toolbar"))) {
       button.addEventListener("command", this._onViewToggle);
     }
 
     yield CallTreeView.initialize();
     yield WaterfallView.initialize();
+    yield FlameGraphView.initialize();
 
     this.selectView(DEFAULT_DETAILS_SUBVIEW);
   }),
 
   /**
    * Unbinds events, destroys subviews.
    */
   destroy: Task.async(function *() {
     for (let button of $$("toolbarbutton[data-view]", $("#details-toolbar"))) {
       button.removeEventListener("command", this._onViewToggle);
     }
 
     yield CallTreeView.destroy();
     yield WaterfallView.destroy();
+    yield FlameGraphView.destroy();
   }),
 
   /**
    * Select one of the DetailView's subviews to be rendered,
    * hiding the others.
    *
    * @params {String} selectedView
    *         Name of the view to be shown.
    */
   selectView: function (selectedView) {
     this.el.selectedIndex = this.viewIndexes[selectedView];
 
     for (let button of $$("toolbarbutton[data-view]", $("#details-toolbar"))) {
-      if (button.getAttribute("data-view") === selectedView)
+      if (button.getAttribute("data-view") === selectedView) {
         button.setAttribute("checked", true);
-      else
+      } else {
         button.removeAttribute("checked");
+      }
     }
 
     this.emit(EVENTS.DETAILS_VIEW_SELECTED, selectedView);
   },
 
   /**
    * Called when a view button is clicked.
    */
--- a/browser/devtools/shared/widgets/FlameGraph.jsm
+++ b/browser/devtools/shared/widgets/FlameGraph.jsm
@@ -15,16 +15,18 @@ this.EXPORTED_SYMBOLS = [
   "FlameGraph",
   "FlameGraphUtils"
 ];
 
 const HTML_NS = "http://www.w3.org/1999/xhtml";
 const GRAPH_SRC = "chrome://browser/content/devtools/graphs-frame.xhtml";
 const L10N = new ViewHelpers.L10N();
 
+const GRAPH_RESIZE_EVENTS_DRAIN = 100; // ms
+
 const GRAPH_WHEEL_ZOOM_SENSITIVITY = 0.00035;
 const GRAPH_WHEEL_SCROLL_SENSITIVITY = 0.5;
 const GRAPH_MIN_SELECTION_WIDTH = 10; // ms
 
 const TIMELINE_TICKS_MULTIPLE = 5; // ms
 const TIMELINE_TICKS_SPACING_MIN = 75; // px
 
 const OVERVIEW_HEADER_HEIGHT = 18; // px
@@ -127,27 +129,32 @@ function FlameGraph(parent, sharpness) {
     this._textWidthsCache = {};
 
     let fontSize = FLAME_GRAPH_BLOCK_TEXT_FONT_SIZE * this._pixelRatio;
     let fontFamily = FLAME_GRAPH_BLOCK_TEXT_FONT_FAMILY;
     this._ctx.font = fontSize + "px " + fontFamily;
     this._averageCharWidth = this._calcAverageCharWidth();
     this._overflowCharWidth = this._getTextWidth(this.overflowChar);
 
+    this._onAnimationFrame = this._onAnimationFrame.bind(this);
     this._onMouseMove = this._onMouseMove.bind(this);
     this._onMouseDown = this._onMouseDown.bind(this);
     this._onMouseUp = this._onMouseUp.bind(this);
     this._onMouseWheel = this._onMouseWheel.bind(this);
-    this._onAnimationFrame = this._onAnimationFrame.bind(this);
+    this._onResize = this._onResize.bind(this);
+    this.refresh = this.refresh.bind(this);
 
     container.addEventListener("mousemove", this._onMouseMove);
     container.addEventListener("mousedown", this._onMouseDown);
     container.addEventListener("mouseup", this._onMouseUp);
     container.addEventListener("MozMousePixelScroll", this._onMouseWheel);
 
+    let ownerWindow = this._parent.ownerDocument.defaultView;
+    ownerWindow.addEventListener("resize", this._onResize);
+
     this._animationId = this._window.requestAnimationFrame(this._onAnimationFrame);
 
     this._ready.resolve(this);
     this.emit("ready", this);
   });
 }
 
 FlameGraph.prototype = {
@@ -174,16 +181,19 @@ FlameGraph.prototype = {
    */
   destroy: function() {
     let container = this._container;
     container.removeEventListener("mousemove", this._onMouseMove);
     container.removeEventListener("mousedown", this._onMouseDown);
     container.removeEventListener("mouseup", this._onMouseUp);
     container.removeEventListener("MozMousePixelScroll", this._onMouseWheel);
 
+    let ownerWindow = this._parent.ownerDocument.defaultView;
+    ownerWindow.removeEventListener("resize", this._onResize);
+
     this._window.cancelAnimationFrame(this._animationId);
     this._iframe.remove();
 
     this._selection = null;
     this._selectionDragger = null;
 
     this._data = null;
 
@@ -237,27 +247,61 @@ FlameGraph.prototype = {
    *         A promise resolved once the data is set.
    */
   setDataWhenReady: Task.async(function*(data) {
     yield this.ready();
     this.setData(data);
   }),
 
   /**
+   * Gets whether or not this graph has a data source.
+   * @return boolean
+   */
+  hasData: function() {
+    return !!this._data;
+  },
+
+  /**
    * Gets the start or end of this graph's selection, i.e. the 'data window'.
    * @return number
    */
   getDataWindowStart: function() {
     return this._selection.start;
   },
   getDataWindowEnd: function() {
     return this._selection.end;
   },
 
   /**
+   * Updates this graph to reflect the new dimensions of the parent node.
+   */
+  refresh: function() {
+    let bounds = this._parent.getBoundingClientRect();
+    let newWidth = this.fixedWidth || bounds.width;
+    let newHeight = this.fixedHeight || bounds.height;
+
+    // Prevent redrawing everything if the graph's width & height won't change.
+    if (this._width == newWidth * this._pixelRatio &&
+        this._height == newHeight * this._pixelRatio) {
+      this.emit("refresh-cancelled");
+      return;
+    }
+
+    bounds.width = newWidth;
+    bounds.height = newHeight;
+    this._iframe.setAttribute("width", bounds.width);
+    this._iframe.setAttribute("height", bounds.height);
+    this._width = this._canvas.width = bounds.width * this._pixelRatio;
+    this._height = this._canvas.height = bounds.height * this._pixelRatio;
+
+    this._shouldRedraw = true;
+    this.emit("refresh");
+  },
+
+  /**
    * The contents of this graph are redrawn only when something changed,
    * like the data source, or the selection bounds etc. This flag tracks
    * if the rendering is "dirty" and needs to be refreshed.
    */
   _shouldRedraw: false,
 
   /**
    * Animation frame callback, invoked on each tick of the refresh driver.
@@ -738,16 +782,25 @@ FlameGraph.prototype = {
     let y = 0;
 
     while ((node = node.offsetParent)) {
       x += node.offsetLeft;
       y += node.offsetTop;
     }
 
     return { left: x, top: y };
+  },
+
+  /**
+   * Listener for the "resize" event on the graph's parent node.
+   */
+  _onResize: function() {
+    if (this.hasData()) {
+      setNamedTimeout(this._uid, GRAPH_RESIZE_EVENTS_DRAIN, this.refresh);
+    }
   }
 };
 
 const FLAME_GRAPH_BLOCK_HEIGHT = 12; // px
 
 const PALLETTE_SIZE = 10;
 const PALLETTE_HUE_OFFSET = Math.random() * 90;
 const PALLETTE_HUE_RANGE = 270;
--- a/browser/locales/en-US/chrome/browser/devtools/profiler.properties
+++ b/browser/locales/en-US/chrome/browser/devtools/profiler.properties
@@ -59,16 +59,22 @@ recordingsList.saveLabel=Save
 profile.tab=%1$S ms → %2$S ms
 
 # LOCALIZATION NOTE (graphs.fps):
 # This string is displayed in the framerate graph of the Profiler,
 # as the unit used to measure frames per second. This label should be kept
 # AS SHORT AS POSSIBLE so it doesn't obstruct important parts of the graph.
 graphs.fps=fps
 
+# LOCALIZATION NOTE (graphs.ms):
+# This string is displayed in the flamegraph of the Profiler,
+# as the unit used to measure time (in milliseconds). This label should be kept
+# AS SHORT AS POSSIBLE so it doesn't obstruct important parts of the graph.
+graphs.ms=ms
+
 # LOCALIZATION NOTE (category.*):
 # These strings are displayed in the categories graph of the Profiler,
 # as the legend for each block in every bar. These labels should be kept
 # AS SHORT AS POSSIBLE so they don't obstruct important parts of the graph.
 category.other=Gecko
 category.css=Styles
 category.js=JIT
 category.gc=GC
@@ -97,8 +103,9 @@ recordingsList.saveDialogTitle=Save profile…
 
 # LOCALIZATION NOTE (recordingsList.saveDialogJSONFilter):
 # This string is displayed as a filter for saving a recording to disk.
 recordingsList.saveDialogJSONFilter=JSON Files
 
 # LOCALIZATION NOTE (recordingsList.saveDialogAllFilter):
 # This string is displayed as a filter for saving a recording to disk.
 recordingsList.saveDialogAllFilter=All Files
+
--- a/browser/themes/shared/devtools/images/performance-icons.svg
+++ b/browser/themes/shared/devtools/images/performance-icons.svg
@@ -2,40 +2,41 @@
 <style>
   g {
     fill: #edf0f1;
   }
   g:not(:target) {
     display: none;
   }
 </style>
-<g id="call-tree">
-  <rect x="1px" y="3.5px" width="14px" height="2px" rx="1" ry="1"/>
-  <rect x="1px" y="7.5px" width="7px" height="2px" rx="1" ry="1"/>
-  <rect x="11px" y="7.5px" width="4px" height="2px" rx="1" ry="1"/>
-  <rect x="4px" y="11.5px" width="4px" height="2px" rx="1" ry="1"/>
+<g id="overview-markers">
+  <rect x="0px" y="3px" width="5px" height="2.5px" rx="1" ry="1"/>
+  <rect x="7px" y="3px" width="9px" height="2.5px" rx="1" ry="1"/>
+  <rect x="0px" y="7px" width="9px" height="2.5px" rx="1" ry="1"/>
+  <rect x="10px" y="7px" width="6px" height="2.5px" rx="1" ry="1"/>
+  <rect x="4px" y="11px" width="5px" height="2.5px" rx="1" ry="1"/>
+  <rect x="12px" y="11px" width="4px" height="2.5px" rx="1" ry="1"/>
 </g>
-<g id="flamechart">
-  <rect x="1px" y="3px" width="14px" height="4px" style="shape-rendering: crispEdges"/>
-  <rect x="1px" y="5px" width="3px" height="5px" rx="1" ry="1"/>
-  <rect x="4px" y="5px" width="3px" height="10px" rx="1" ry="1"/>
-  <rect x="7px" y="5px" width="5px" height="3px" rx="1" ry="1"/>
-  <rect x="12px" y="5px" width="3px" height="7px" rx="1" ry="1"/>
+<g id="overview-frames">
+  <rect x="1px" y="4px" width="2px" height="12px" rx="1" ry="1"/>
+  <rect x="5px" y="12px" width="2px" height="4px" rx="1" ry="1"/>
+  <rect x="9px" y="9px" width="2px" height="7px" rx="1" ry="1"/>
+  <rect x="13px" y="7px" width="2px" height="9px" rx="1" ry="1"/>
 </g>
-<g id="frame">
-  <rect x="1px" y="4px" width="2px" height="13px" rx="1" ry="1"/>
-  <rect x="5px" y="12px" width="2px" height="5px" rx="1" ry="1"/>
-  <rect x="9px" y="9px" width="2px" height="8px" rx="1" ry="1"/>
-  <rect x="13px" y="7px" width="2px" height="10px" rx="1" ry="1"/>
+<g id="details-waterfall">
+  <rect x="0px" y="3px" width="9px" height="2.5px" rx="1" ry="1"/>
+  <rect x="5px" y="7px" width="8px" height="2.5px" rx="1" ry="1"/>
+  <rect x="7px" y="11px" width="9px" height="2.5px" rx="1" ry="1"/>
 </g>
-<g id="markers">
-  <path d="m2.1,2.1h9.6c.6,0 1.1,.5 1.1,1.1 0,.6-.5,1.1-1.1,1.1h-9.6c-.6,0-1.1-.5-1.1-1.1 .1-.6 .5-1.1 1.1-1.1z"/>
-  <path d="m7.4,5.3h7.4c.6,0 1.1,.5 1.1,1.1 0,.6-.5,1.1-1.1,1.1h-7.4c-.5-.1-1-.5-1-1.1 0-.6 .5-1.1 1-1.1z"/>
-  <path d="m5.3,8.5h3.2c.6,0 1.1,.5 1.1,1.1 0,.6-.5,1.1-1.1,1.1h-3.2c-.6,0-1.1-.5-1.1-1.1 .1-.6 .5-1.1 1.1-1.1z"/>
-  <path d="m4.3,11.7h2.1c.6,0 1.1,.5 1.1,1.1 0,.6-.5,1.1-1.1,1.1h-2.1c-.6,0-1.1-.5-1.1-1.1 0-.6 .5-1.1 1.1-1.1z"/>
-  <path d="m4.3,11.7h2.1c.6,0 1.1,.5 1.1,1.1 0,.6-.5,1.1-1.1,1.1h-2.1c-.6,0-1.1-.5-1.1-1.1 0-.6 .5-1.1 1.1-1.1z" style="transform: translateX(7px)"/>
+<g id="details-call-tree">
+  <rect x="0px" y="3px" width="16px" height="2px"/>
+  <rect x="3px" y="6px" width="7px" height="2px"/>
+  <rect x="6px" y="9px" width="6px" height="2px"/>
+  <rect x="9px" y="12px" width="5px" height="2px"/>
 </g>
-<g id="waterfall">
-  <rect x="1px" y="3px" width="8px" height="2.5px" rx="1" ry="1"/>
-  <rect x="5px" y="7px" width="8px" height="2.5px" rx="1" ry="1"/>
-  <rect x="7px" y="11.5px" width="8px" height="2.5px" rx="1" ry="1"/>
+<g id="details-flamegraph">
+  <rect x="0px" y="3px" width="16px" height="2px"/>
+  <rect x="0px" y="6px" width="8px" height="2px"/>
+  <rect x="10px" y="6px" width="6px" height="2px"/>
+  <rect x="2px" y="9px" width="6px" height="2px"/>
+  <rect x="5px" y="12px" width="3px" height="2px"/>
 </g>
-</svg>
\ No newline at end of file
+</svg>
--- a/browser/themes/shared/devtools/performance.inc.css
+++ b/browser/themes/shared/devtools/performance.inc.css
@@ -37,23 +37,26 @@
 
 #record-button[locked] {
   pointer-events: none;
 }
 
 /* Details Panel */
 
 #select-waterfall-view {
-  list-style-image: url(performance-icons.svg#waterfall);
+  list-style-image: url(performance-icons.svg#details-waterfall);
 }
 
 #select-calltree-view {
-  list-style-image: url(performance-icons.svg#call-tree);
+  list-style-image: url(performance-icons.svg#details-call-tree);
 }
 
+#select-flamegraph-view {
+  list-style-image: url(performance-icons.svg#details-flamegraph);
+}
 
 /* Profile call tree */
 
 .call-tree-cells-container {
   /* Hack: force hardware acceleration */
   transform: translateZ(1px);
   overflow: auto;
 }