Bug 1168125 - Add marker folding logic, r=jsantell
authorVictor Porof <vporof@mozilla.com>
Wed, 27 May 2015 17:23:53 -0400
changeset 245995 37227fbe07f7d7660e60a57860210f5d665dabe7
parent 245994 f24a709545686e11e175ab27229e2e9210918c70
child 245996 c3594996ddb6d98c9272ec5119761d12d1e2fcf5
push id60333
push userryanvm@gmail.com
push dateThu, 28 May 2015 14:20:47 +0000
treeherdermozilla-inbound@8225a3b75df6 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjsantell
bugs1168125
milestone41.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 1168125 - Add marker folding logic, r=jsantell
browser/devtools/performance/modules/global.js
browser/devtools/performance/modules/logic/waterfall-utils.js
browser/devtools/performance/modules/widgets/markers-overview.js
browser/devtools/performance/moz.build
browser/devtools/performance/performance-controller.js
browser/devtools/performance/views/details-waterfall.js
--- a/browser/devtools/performance/modules/global.js
+++ b/browser/devtools/performance/modules/global.js
@@ -99,16 +99,31 @@ const CATEGORY_MAPPINGS = {
  *          If you use a function for a label, it *must* handle the case where
  *          no marker is provided for a main label to describe all markers of
  *          this type.
  * - colorName: The label of the DevTools color used for this marker. If
  *              adding a new color, be sure to check that there's an entry
  *              for `.marker-details-bullet.{COLORNAME}` for the equivilent
  *              entry in ./browser/themes/shared/devtools/performance.inc.css
  *              https://developer.mozilla.org/en-US/docs/Tools/DevToolsColors
+ * - collapseFunc: A function determining how markers are collapsed together.
+ *                 Invoked with 3 arguments: the current parent marker, the
+ *                 current marker and a method for peeking i markers ahead. If
+ *                 nothing is returned, the marker is added as a standalone entry
+ *                 in the waterfall. Otherwise, an object needs to be returned
+ *                 with the following properties:
+ *                 - toParent: The parent marker name (needs to be an entry in
+ *                             the `TIMELINE_BLUEPRINT` itself).
+ *                 - withData: An object containing some properties to staple
+ *                             on the parent marker.
+ *                 - forceNew: True if a new parent marker needs to be created
+ *                             even though there is one currently available
+ *                             with the same name.
+ *                 - forceEnd: True if the current parent marker is full after
+ *                             this collapse operation and should be finalized.
  * - fields: An optional array of marker properties you wish to display in the
  *           marker details view. For example, a field in the array such as
  *           { property: "aCauseName", label: "Cause" } would render a string
  *           like `Cause: ${marker.aCauseName}` in the marker details view.
  *           Each `field` item may take the following properties:
  *           - property: The property that must exist on the marker to render,
  *                       and the value of the property will be displayed.
  *           - label: The name of the property that should be displayed.
@@ -122,85 +137,171 @@ const CATEGORY_MAPPINGS = {
  * Whenever this is changed, browser_timeline_waterfall-styles.js *must* be
  * updated as well.
  */
 const TIMELINE_BLUEPRINT = {
   /* Group 0 - Reflow and Rendering pipeline */
   "Styles": {
     group: 0,
     colorName: "graphs-purple",
+    collapseFunc: collapseConsecutiveIdentical,
     label: L10N.getStr("timeline.label.styles2"),
     fields: getStylesFields,
   },
   "Reflow": {
     group: 0,
     colorName: "graphs-purple",
-    label: L10N.getStr("timeline.label.reflow2")
+    collapseFunc: collapseConsecutiveIdentical,
+    label: L10N.getStr("timeline.label.reflow2"),
   },
   "Paint": {
     group: 0,
     colorName: "graphs-green",
-    label: L10N.getStr("timeline.label.paint")
+    collapseFunc: collapseConsecutiveIdentical,
+    label: L10N.getStr("timeline.label.paint"),
   },
 
   /* Group 1 - JS */
   "DOMEvent": {
     group: 1,
     colorName: "graphs-yellow",
+    collapseFunc: collapseDOMIntoDOMJS,
     label: L10N.getStr("timeline.label.domevent"),
     fields: getDOMEventFields,
   },
   "Javascript": {
     group: 1,
     colorName: "graphs-yellow",
+    collapseFunc: either(collapseJSIntoDOMJS, collapseConsecutiveIdentical),
     label: getJSLabel,
     fields: getJSFields,
   },
+  "meta::DOMEvent+JS": {
+    colorName: "graphs-yellow",
+    label: getDOMJSLabel,
+    fields: getDOMEventFields,
+  },
   "Parse HTML": {
     group: 1,
     colorName: "graphs-yellow",
-    label: L10N.getStr("timeline.label.parseHTML")
+    collapseFunc: collapseConsecutiveIdentical,
+    label: L10N.getStr("timeline.label.parseHTML"),
   },
   "Parse XML": {
     group: 1,
     colorName: "graphs-yellow",
-    label: L10N.getStr("timeline.label.parseXML")
+    collapseFunc: collapseConsecutiveIdentical,
+    label: L10N.getStr("timeline.label.parseXML"),
   },
   "GarbageCollection": {
     group: 1,
     colorName: "graphs-red",
+    collapseFunc: collapseAdjacentGC,
     label: getGCLabel,
     fields: [
       { property: "causeName", label: "Reason:" },
       { property: "nonincrementalReason", label: "Non-incremental Reason:" }
-    ]
+    ],
   },
 
   /* Group 2 - User Controlled */
   "ConsoleTime": {
     group: 2,
     colorName: "graphs-grey",
     label: sublabelForProperty(L10N.getStr("timeline.label.consoleTime"), "causeName"),
     fields: [{
       property: "causeName",
       label: L10N.getStr("timeline.markerDetail.consoleTimerName")
-    }]
+    }],
   },
   "TimeStamp": {
     group: 2,
     colorName: "graphs-blue",
     label: sublabelForProperty(L10N.getStr("timeline.label.timestamp"), "causeName"),
     fields: [{
       property: "causeName",
       label: "Label:"
-    }]
+    }],
   },
 };
 
 /**
+ * Helper for creating a function that returns the first defined result from
+ * a list of functions passed in as params, in order.
+ * @param ...function fun
+ * @return any
+ */
+function either(...fun) {
+  return function() {
+    for (let f of fun) {
+      let result = f.apply(null, arguments);
+      if (result !== undefined) return result;
+    }
+  }
+}
+
+/**
+ * A series of collapsers used by the blueprint. These functions are
+ * consecutively invoked on a moving window of two markers.
+ */
+
+function collapseConsecutiveIdentical(parent, curr, peek) {
+  // If there is a parent marker currently being filled and the current marker
+  // should go into the parent marker, make it so.
+  if (parent && parent.name == curr.name) {
+    return { toParent: parent.name };
+  }
+  // Otherwise if the current marker is the same type as the next marker type,
+  // create a new parent marker containing the current marker.
+  let next = peek(1);
+  if (next && curr.name == next.name) {
+    return { toParent: curr.name };
+  }
+}
+
+function collapseAdjacentGC(parent, curr, peek) {
+  let next = peek(1);
+  if (next && (next.start < curr.end || next.start - curr.end <= 10 /* ms */)) {
+    return collapseConsecutiveIdentical(parent, curr, peek);
+  }
+}
+
+function collapseDOMIntoDOMJS(parent, curr, peek) {
+  // If the next marker is a JavaScript marker, create a new meta parent marker
+  // containing the current marker.
+  let next = peek(1);
+  if (next && next.name == "Javascript") {
+    return {
+      forceNew: true,
+      toParent: "meta::DOMEvent+JS",
+      withData: {
+        type: curr.type,
+        eventPhase: curr.eventPhase
+      },
+    };
+  }
+}
+
+function collapseJSIntoDOMJS(parent, curr, peek) {
+  // If there is a parent marker currently being filled, and it's the one
+  // created from a `DOMEvent` via `collapseDOMIntoDOMJS`, then the current
+  // marker has to go into that one.
+  if (parent && parent.name == "meta::DOMEvent+JS") {
+    return {
+      forceEnd: true,
+      toParent: "meta::DOMEvent+JS",
+      withData: {
+        stack: curr.stack,
+        endStack: curr.endStack
+      },
+    };
+  }
+}
+
+/**
  * A series of formatters used by the blueprint.
  */
 
 function getGCLabel (marker={}) {
   let label = L10N.getStr("timeline.label.garbageCollection");
   // Only if a `nonincrementalReason` exists, do we want to label
   // this as a non incremental GC event.
   if ("nonincrementalReason" in marker) {
@@ -231,16 +332,20 @@ const JS_MARKER_MAP = {
 function getJSLabel (marker={}) {
   let generic = L10N.getStr("timeline.label.javascript2");
   if ("causeName" in marker) {
     return JS_MARKER_MAP[marker.causeName] || generic;
   }
   return generic;
 }
 
+function getDOMJSLabel (marker={}) {
+  return `Event (${marker.type})`;
+}
+
 /**
  * Returns a hash for computing a fields object for a JS marker. If the cause
  * is considered content (so an entry exists in the JS_MARKER_MAP), do not display it
  * since it's redundant with the label. Otherwise for Gecko code, either display
  * the cause, or "(Gecko)", depending on if "show-platform-data" is set.
  */
 function getJSFields (marker) {
   if ("causeName" in marker && !JS_MARKER_MAP[marker.causeName]) {
new file mode 100644
--- /dev/null
+++ b/browser/devtools/performance/modules/logic/waterfall-utils.js
@@ -0,0 +1,122 @@
+/* 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";
+
+/**
+ * Utility functions for collapsing markers into a waterfall.
+ */
+
+loader.lazyRequireGetter(this, "TIMELINE_BLUEPRINT",
+  "devtools/performance/global", true);
+
+/**
+ * Collapses markers into a tree-like structure. Currently, this only goes
+ * one level deep.
+ * @param object markerNode
+ * @param array markersList
+ */
+function collapseMarkersIntoNode({ markerNode, markersList }) {
+  let [getOrCreateParentNode, getCurrentParentNode, clearParentNode] = makeParentNodeFactory();
+
+  for (let i = 0, len = markersList.length; i < len; i++) {
+    let curr = markersList[i];
+    let blueprint = TIMELINE_BLUEPRINT[curr.name];
+
+    let parentNode = getCurrentParentNode();
+    let collapse = blueprint.collapseFunc || (() => null);
+    let peek = distance => markersList[i + distance];
+    let collapseInfo = collapse(parentNode, curr, peek);
+
+    if (collapseInfo) {
+      let { toParent, withData, forceNew, forceEnd } = collapseInfo;
+
+      // If the `forceNew` prop is set on the collapse info, then a new parent
+      // marker needs to be created even if there is one already available.
+      if (forceNew) {
+        clearParentNode();
+      }
+      // If the `toParent` prop is set on the collapse info, then this marker
+      // can be collapsed into a higher-level parent marker.
+      if (toParent) {
+        let parentNode = getOrCreateParentNode(markerNode, toParent, curr.start);
+        parentNode.end = curr.end;
+        parentNode.submarkers.push(curr);
+        for (let key in withData) {
+          parentNode[key] = withData[key];
+        }
+      }
+      // If the `forceEnd` prop is set on the collapse info, then the higher-level
+      // parent marker is full and should be finalized.
+      if (forceEnd) {
+        clearParentNode();
+      }
+    } else {
+      clearParentNode();
+      markerNode.submarkers.push(curr);
+    }
+  }
+}
+
+/**
+ * Creates an empty parent marker, which functions like a regular marker,
+ * but is able to hold additional child markers.
+ * @param string name
+ * @param number start [optional]
+ * @param number end [optional]
+ * @return object
+ */
+function makeEmptyMarkerNode(name, start, end) {
+  return {
+    name: name,
+    start: start,
+    end: end,
+    submarkers: []
+  };
+}
+
+/**
+ * Creates a factory for markers containing other markers.
+ * @return array[function]
+ */
+function makeParentNodeFactory() {
+  let marker;
+
+  return [
+    /**
+     * Gets the current parent marker for the given marker name. If it doesn't
+     * exist, it creates it and appends it to another parent marker.
+     * @param object owner
+     * @param string name
+     * @param number start
+     * @return object
+     */
+    function getOrCreateParentNode(owner, name, start) {
+      if (marker && marker.name == name) {
+        return marker;
+      } else {
+        marker = makeEmptyMarkerNode(name, start);
+        owner.submarkers.push(marker);
+        return marker;
+      }
+    },
+
+    /**
+     * Gets the current marker marker.
+     * @return object
+     */
+    function getCurrentParentNode() {
+      return marker;
+    },
+
+    /**
+     * Clears the current marker marker.
+     */
+    function clearParentNode() {
+      marker = null;
+    }
+  ];
+}
+
+exports.makeEmptyMarkerNode = makeEmptyMarkerNode;
+exports.collapseMarkersIntoNode = collapseMarkersIntoNode;
--- a/browser/devtools/performance/modules/widgets/markers-overview.js
+++ b/browser/devtools/performance/modules/widgets/markers-overview.js
@@ -72,17 +72,17 @@ MarkersOverview.prototype = Heritage.ext
    * @see TIMELINE_BLUEPRINT in timeline/widgets/global.js
    */
   setBlueprint: function(blueprint) {
     this._paintBatches = new Map();
     this._lastGroup = 0;
 
     for (let type in blueprint) {
       this._paintBatches.set(type, { style: blueprint[type], batch: [] });
-      this._lastGroup = Math.max(this._lastGroup, blueprint[type].group);
+      this._lastGroup = Math.max(this._lastGroup, blueprint[type].group || 0);
     }
   },
 
   /**
    * Disables selection and empties this graph.
    */
   clearView: function() {
     this.selectionEnabled = false;
--- a/browser/devtools/performance/moz.build
+++ b/browser/devtools/performance/moz.build
@@ -10,16 +10,17 @@ EXTRA_JS_MODULES.devtools.performance +=
     'modules/logic/frame-utils.js',
     'modules/logic/front.js',
     'modules/logic/io.js',
     'modules/logic/jit.js',
     'modules/logic/marker-utils.js',
     'modules/logic/recording-model.js',
     'modules/logic/recording-utils.js',
     'modules/logic/tree-model.js',
+    'modules/logic/waterfall-utils.js',
     'modules/widgets/graphs.js',
     'modules/widgets/marker-details.js',
     'modules/widgets/marker-view.js',
     'modules/widgets/markers-overview.js',
     'modules/widgets/tree-view.js',
     'modules/widgets/waterfall-ticks.js',
     'panel.js'
 ]
--- a/browser/devtools/performance/performance-controller.js
+++ b/browser/devtools/performance/performance-controller.js
@@ -28,16 +28,18 @@ loader.lazyRequireGetter(this, "GraphsCo
 loader.lazyRequireGetter(this, "WaterfallHeader",
   "devtools/performance/waterfall-ticks", true);
 loader.lazyRequireGetter(this, "MarkerView",
   "devtools/performance/marker-view", true);
 loader.lazyRequireGetter(this, "MarkerDetails",
   "devtools/performance/marker-details", true);
 loader.lazyRequireGetter(this, "MarkerUtils",
   "devtools/performance/marker-utils");
+loader.lazyRequireGetter(this, "WaterfallUtils",
+  "devtools/performance/waterfall-utils");
 loader.lazyRequireGetter(this, "CallView",
   "devtools/performance/tree-view", true);
 loader.lazyRequireGetter(this, "ThreadNode",
   "devtools/performance/tree-model", true);
 loader.lazyRequireGetter(this, "FrameNode",
   "devtools/performance/tree-model", true);
 loader.lazyRequireGetter(this, "JITOptimizations",
   "devtools/performance/jit", true);
--- a/browser/devtools/performance/views/details-waterfall.js
+++ b/browser/devtools/performance/views/details-waterfall.js
@@ -109,16 +109,24 @@ let WaterfallView = Heritage.extend(Deta
     gToolbox.viewSourceInDebugger(file, line);
   },
 
   /**
    * Called when the recording is stopped and prepares data to
    * populate the waterfall tree.
    */
   _prepareWaterfallTree: function(markers) {
+    let rootMarkerNode = WaterfallUtils.makeEmptyMarkerNode("(root)");
+
+    WaterfallUtils.collapseMarkersIntoNode({
+      markerNode: rootMarkerNode,
+      markersList: markers
+    });
+
+    return rootMarkerNode;
   },
 
   /**
    * Renders the waterfall tree.
    */
   _populateWaterfallTree: function(rootMarkerNode, interval) {
     let root = new MarkerView({
       marker: rootMarkerNode,