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 240005 5032626927f25877c355a1e8e64f012fcb8e0937
parent 240004 3c5db371a6385189c4d2d7d23d8d3e6113a514d4
child 240006 3c5a88aaae96aa0f1cc464e0d019023057d89e11
push id7472
push userraliiev@mozilla.com
push dateMon, 12 Jan 2015 20:36:27 +0000
treeherdermozilla-aurora@300ca104f8fb [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjsantell
bugs1077459
milestone37.0a1
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;
 }