Bug 1077455 - Link the details and overview views together in the new performance tool. r=vp
authorJordan Santell <jsantell@gmail.com>
Tue, 25 Nov 2014 15:01:00 +0100
changeset 217674 9e9df8e2e7e57a861c478a055ba900537d7eba96
parent 217673 b8d490a607e7bebddbca7dd9dc473ebc86c247ec
child 217675 81b55d99c4228f488a7f21b44af34d3695a20735
push id27887
push userryanvm@gmail.com
push dateThu, 27 Nov 2014 02:08:38 +0000
treeherdermozilla-central@c63e741bca2e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersvp
bugs1077455
milestone36.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 1077455 - Link the details and overview views together in the new performance tool. r=vp
browser/devtools/performance/controller.js
browser/devtools/performance/test/browser.ini
browser/devtools/performance/test/browser_perf-details-calltree-render-02.js
browser/devtools/performance/test/browser_perf-overview-selection.js
browser/devtools/performance/test/head.js
browser/devtools/performance/views/call-tree.js
browser/devtools/performance/views/overview.js
--- a/browser/devtools/performance/controller.js
+++ b/browser/devtools/performance/controller.js
@@ -3,16 +3,17 @@
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 "use strict";
 
 const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
 
 Cu.import("resource://gre/modules/Task.jsm");
 Cu.import("resource://gre/modules/devtools/Loader.jsm");
 Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
+Cu.import("resource://gre/modules/devtools/Console.jsm");
 
 let require = devtools.require;
 devtools.lazyRequireGetter(this, "Services");
 devtools.lazyRequireGetter(this, "promise");
 devtools.lazyRequireGetter(this, "EventEmitter",
   "devtools/toolkit/event-emitter");
 devtools.lazyRequireGetter(this, "DevToolsUtils",
   "devtools/toolkit/DevToolsUtils");
@@ -36,16 +37,20 @@ const EVENTS = {
   TIMELINE_DATA: "Performance:TimelineData",
 
   // Emitted by the PerformanceView on record button click
   UI_START_RECORDING: "Performance:UI:StartRecording",
   UI_STOP_RECORDING: "Performance:UI:StopRecording",
 
   // Emitted by the OverviewView when more data has been rendered
   OVERVIEW_RENDERED: "Performance:UI:OverviewRendered",
+  // Emitted by the OverviewView when a range has been selected in the graphs
+  OVERVIEW_RANGE_SELECTED: "Performance:UI:OverviewRangeSelected",
+  // Emitted by the OverviewView when a selection range has been removed
+  OVERVIEW_RANGE_CLEARED: "Performance:UI:OverviewRangeCleared",
 
   // Emitted by the CallTreeView when a call tree has been rendered
   CALL_TREE_RENDERED: "Performance:UI:CallTreeRendered"
 };
 
 /**
  * The current target and the profiler connection, set by this tool's host.
  */
--- a/browser/devtools/performance/test/browser.ini
+++ b/browser/devtools/performance/test/browser.ini
@@ -26,9 +26,11 @@ support-files =
 [browser_perf-shared-connection-03.js]
 # bug 1077464
 #[browser_perf-shared-connection-04.js]
 [browser_perf-data-samples.js]
 [browser_perf-data-massaging-01.js]
 [browser_perf-ui-recording.js]
 [browser_perf-overview-render-01.js]
 [browser_perf-overview-render-02.js]
+[browser_perf-overview-selection.js]
 [browser_perf-details-calltree-render-01.js]
+[browser_perf-details-calltree-render-02.js]
new file mode 100644
--- /dev/null
+++ b/browser/devtools/performance/test/browser_perf-details-calltree-render-02.js
@@ -0,0 +1,37 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that the call tree view renders after recording.
+ */
+function spawnTest () {
+  let { panel } = yield initPerformance(SIMPLE_URL);
+  let { EVENTS, CallTreeView, OverviewView } = panel.panelWin;
+
+  let updated = 0;
+  CallTreeView.on(EVENTS.CALL_TREE_RENDERED, () => updated++);
+
+  let rendered = once(CallTreeView, EVENTS.CALL_TREE_RENDERED);
+
+  yield startRecording(panel);
+  yield busyWait(100);
+  yield stopRecording(panel);
+  yield rendered;
+
+  rendered = once(CallTreeView, EVENTS.CALL_TREE_RENDERED);
+  OverviewView.emit(EVENTS.OVERVIEW_RANGE_SELECTED, { beginAt: 0, endAt: 10 });
+  yield rendered;
+
+  ok(true, "Call tree rerenders when a range in the overview graph is selected.");
+
+  rendered = once(CallTreeView, EVENTS.CALL_TREE_RENDERED);
+  OverviewView.emit(EVENTS.OVERVIEW_RANGE_CLEARED);
+  yield rendered;
+
+  ok(true, "Call tree rerenders when a range in the overview graph is removed.");
+
+  is(updated, 3, "CallTreeView rerendered 3 times.");
+
+  yield teardown(panel);
+  finish();
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/performance/test/browser_perf-overview-selection.js
@@ -0,0 +1,45 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that events are fired from OverviewView from selection manipulation.
+ */
+function spawnTest () {
+  let { panel } = yield initPerformance(SIMPLE_URL);
+  let { EVENTS, OverviewView } = panel.panelWin;
+  let beginAt, endAt, params, _;
+
+  yield startRecording(panel);
+
+  yield once(OverviewView, EVENTS.OVERVIEW_RENDERED);
+
+  yield stopRecording(panel);
+
+  let graph = OverviewView.framerateGraph;
+  let MAX = graph.width;
+
+  // Select the first half of the graph
+  let results = onceSpread(OverviewView, EVENTS.OVERVIEW_RANGE_SELECTED);
+  dragStart(graph, 0);
+  dragStop(graph, MAX / 2);
+
+  [_, { beginAt, endAt }] = yield results;
+
+  let actual = graph.getMappedSelection();
+  ise(beginAt, actual.min, "OVERVIEW_RANGE_SELECTED fired with beginAt value on click.");
+  ise(endAt, actual.max, "OVERVIEW_RANGE_SELECTED fired with endAt value on click.");
+
+  // Listen to deselection
+  results = onceSpread(OverviewView, EVENTS.OVERVIEW_RANGE_CLEARED);
+  dropSelection(graph);
+  [_, params] = yield results;
+
+  is(graph.hasSelection(), false, "selection no longer on graph.");
+  is(params, undefined, "OVERVIEW_RANGE_CLEARED fired with no additional arguments.");
+
+  results = beginAt = endAt = graph = OverviewView = null;
+
+  panel.panelWin.clearNamedTimeout("graph-scroll");
+  yield teardown(panel);
+  finish();
+}
--- a/browser/devtools/performance/test/head.js
+++ b/browser/devtools/performance/test/head.js
@@ -96,38 +96,46 @@ function removeTab(aTab, aWindow) {
   return deferred.promise;
 }
 
 function handleError(aError) {
   ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
   finish();
 }
 
-function once(aTarget, aEventName, aUseCapture = false) {
+function once(aTarget, aEventName, aUseCapture = false, spread = false) {
   info("Waiting for event: '" + aEventName + "' on " + aTarget + ".");
 
   let deferred = Promise.defer();
 
   for (let [add, remove] of [
     ["on", "off"], // Use event emitter before DOM events for consistency
     ["addEventListener", "removeEventListener"],
     ["addListener", "removeListener"]
   ]) {
     if ((add in aTarget) && (remove in aTarget)) {
       aTarget[add](aEventName, function onEvent(...aArgs) {
         aTarget[remove](aEventName, onEvent, aUseCapture);
-        deferred.resolve(...aArgs);
+        deferred.resolve(spread ? aArgs : aArgs[0]);
       }, aUseCapture);
       break;
     }
   }
 
   return deferred.promise;
 }
 
+/**
+ * Like `once`, except returns an array so we can
+ * access all arguments fired by the event.
+ */
+function onceSpread(aTarget, aEventName, aUseCapture) {
+  return once(aTarget, aEventName, aUseCapture, true);
+}
+
 function test () {
   Task.spawn(spawnTest).then(finish, handleError);
 }
 
 function initBackend(aUrl) {
   info("Initializing a performance front.");
 
   if (!DebuggerServer.initialized) {
@@ -277,8 +285,29 @@ function waitUntil(predicate, interval =
     return Promise.resolve(true);
   }
   let deferred = Promise.defer();
   setTimeout(function() {
     waitUntil(predicate).then(() => deferred.resolve(true));
   }, interval);
   return deferred.promise;
 }
+
+// EventUtils just doesn't work!
+
+function dragStart(graph, x, y = 1) {
+  x /= window.devicePixelRatio;
+  y /= window.devicePixelRatio;
+  graph._onMouseMove({ clientX: x, clientY: y });
+  graph._onMouseDown({ clientX: x, clientY: y });
+}
+
+function dragStop(graph, x, y = 1) {
+  x /= window.devicePixelRatio;
+  y /= window.devicePixelRatio;
+  graph._onMouseMove({ clientX: x, clientY: y });
+  graph._onMouseUp({ clientX: x, clientY: y });
+}
+
+function dropSelection(graph) {
+  graph.dropSelection();
+  graph.emit("mouseup");
+}
--- a/browser/devtools/performance/views/call-tree.js
+++ b/browser/devtools/performance/views/call-tree.js
@@ -8,46 +8,75 @@
  */
 let CallTreeView = {
   /**
    * Sets up the view with event binding.
    */
   initialize: function () {
     this.el = $(".call-tree");
     this._graphEl = $(".call-tree-cells-container");
+    this._onRangeChange = this._onRangeChange.bind(this);
     this._stop = this._stop.bind(this);
 
+    OverviewView.on(EVENTS.OVERVIEW_RANGE_SELECTED, this._onRangeChange);
+    OverviewView.on(EVENTS.OVERVIEW_RANGE_CLEARED, this._onRangeChange);
     PerformanceController.on(EVENTS.RECORDING_STOPPED, this._stop);
   },
 
   /**
    * Unbinds events.
    */
   destroy: function () {
+    OverviewView.off(EVENTS.OVERVIEW_RANGE_SELECTED, this._onRangeChange);
+    OverviewView.off(EVENTS.OVERVIEW_RANGE_CLEARED, this._onRangeChange);
     PerformanceController.off(EVENTS.RECORDING_STOPPED, this._stop);
   },
 
+  /**
+   * Method for handling all the set up for rendering a new
+   * call tree.
+   */
+  render: function (profilerData, beginAt, endAt, options={}) {
+    let threadNode = this._prepareCallTree(profilerData, beginAt, endAt, options);
+    this._populateCallTree(threadNode, options);
+    this.emit(EVENTS.CALL_TREE_RENDERED);
+  },
+
+  /**
+   * Called when recording is stopped.
+   */
   _stop: function (_, { profilerData }) {
-    this._prepareCallTree(profilerData);
+    this._profilerData = profilerData;
+    this.render(profilerData);
+  },
+
+  /**
+   * Fired when a range is selected or cleared in the OverviewView.
+   */
+  _onRangeChange: function (_, params) {
+    // When a range is cleared, we'll have no beginAt/endAt data,
+    // so the rebuild will just render all the data again.
+    let { beginAt, endAt } = params || {};
+    this.render(this._profilerData, beginAt, endAt);
   },
 
   /**
    * Called when the recording is stopped and prepares data to
    * populate the call tree.
    */
-  _prepareCallTree: function (profilerData, beginAt, endAt, options={}) {
+  _prepareCallTree: function (profilerData, beginAt, endAt, options) {
     let threadSamples = profilerData.profile.threads[0].samples;
     let contentOnly = !Prefs.showPlatformData;
     // TODO handle inverted tree bug 1102347
     let invertTree = false;
 
     let threadNode = new ThreadNode(threadSamples, contentOnly, beginAt, endAt, invertTree);
     options.inverted = invertTree && threadNode.samples > 0;
 
-    this._populateCallTree(threadNode, options);
+    return threadNode;
   },
 
   /**
    * Renders the call tree.
    */
   _populateCallTree: function (frameNode, options={}) {
     let root = new CallView({
       autoExpandDepth: options.inverted ? 0 : undefined,
@@ -57,17 +86,15 @@ let CallTreeView = {
     });
 
     // Clear out other graphs
     this._graphEl.innerHTML = "";
     root.attachTo(this._graphEl);
 
     let contentOnly = !Prefs.showPlatformData;
     root.toggleCategories(!contentOnly);
-
-    this.emit(EVENTS.CALL_TREE_RENDERED);
   }
 };
 
 /**
  * Convenient way of emitting events from the view.
  */
 EventEmitter.decorate(CallTreeView);
--- a/browser/devtools/performance/views/overview.js
+++ b/browser/devtools/performance/views/overview.js
@@ -1,16 +1,17 @@
 /* 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 OVERVIEW_UPDATE_INTERVAL = 100;
 const FRAMERATE_CALC_INTERVAL = 16; // ms
 const FRAMERATE_GRAPH_HEIGHT = 60; // px
+const GRAPH_SCROLL_EVENTS_DRAIN = 50; // ms
 
 /**
  * View handler for the overview panel's time view, displaying
  * framerate over time.
  */
 let OverviewView = {
 
   /**
@@ -19,44 +20,83 @@ let OverviewView = {
   initialize: function () {
     this._framerateEl = $("#time-framerate");
     this._ticksData = [];
 
     this._start = this._start.bind(this);
     this._stop = this._stop.bind(this);
     this._onTimelineData = this._onTimelineData.bind(this);
     this._onRecordingTick = this._onRecordingTick.bind(this);
+    this._onGraphMouseUp = this._onGraphMouseUp.bind(this);
+    this._onGraphScroll = this._onGraphScroll.bind(this);
 
     this._initializeFramerateGraph();
 
+    this.framerateGraph.on("mouseup", this._onGraphMouseUp);
+    this.framerateGraph.on("scroll", this._onGraphScroll);
     PerformanceController.on(EVENTS.RECORDING_STARTED, this._start);
     PerformanceController.on(EVENTS.RECORDING_STOPPED, this._stop);
     PerformanceController.on(EVENTS.TIMELINE_DATA, this._onTimelineData);
   },
 
   /**
    * Unbinds events.
    */
   destroy: function () {
+    this.framerateGraph.off("mouseup", this._onGraphMouseUp);
+    this.framerateGraph.off("scroll", this._onGraphScroll);
+    clearNamedTimeout("graph-scroll");
     PerformanceController.off(EVENTS.RECORDING_STARTED, this._start);
     PerformanceController.off(EVENTS.RECORDING_STOPPED, this._stop);
+    PerformanceController.off(EVENTS.TIMELINE_DATA, this._onTimelineData);
   },
 
   /**
    * Called at most every OVERVIEW_UPDATE_INTERVAL milliseconds
    * and uses data fetched from `_onTimelineData` to render
    * data into all the corresponding overview graphs.
    */
   _onRecordingTick: Task.async(function *() {
     yield this.framerateGraph.setDataWhenReady(this._ticksData);
     this.emit(EVENTS.OVERVIEW_RENDERED);
     this._draw();
   }),
 
   /**
+   * Fired when the graph selection has changed. Called by
+   * mouseup and scroll events.
+   */
+  _onSelectionChange: function () {
+    if (this.framerateGraph.hasSelection()) {
+      let { min: beginAt, max: endAt } = this.framerateGraph.getMappedSelection();
+      this.emit(EVENTS.OVERVIEW_RANGE_SELECTED, { beginAt, endAt });
+    } else {
+      this.emit(EVENTS.OVERVIEW_RANGE_CLEARED);
+    }
+  },
+
+  /**
+   * Listener handling the "mouseup" event for the framerate graph.
+   * Fires an event to be handled elsewhere.
+   */
+  _onGraphMouseUp: function () {
+    this._onSelectionChange();
+  },
+
+  /**
+   * Listener handling the "scroll" event for the framerate graph.
+   * Fires an event to be handled elsewhere.
+   */
+  _onGraphScroll: function () {
+    setNamedTimeout("graph-scroll", GRAPH_SCROLL_EVENTS_DRAIN, () => {
+      this._onSelectionChange();
+    });
+  },
+
+  /**
    * Sets up the framerate graph.
    */
   _initializeFramerateGraph: function () {
     let graph = new LineGraphWidget(this._framerateEl, L10N.getStr("graphs.fps"));
     graph.minDistanceBetweenPoints = 1;
     graph.fixedHeight = FRAMERATE_GRAPH_HEIGHT;
     graph.selectionEnabled = false;
     this.framerateGraph = graph;