Bug 1217243 - Display snapshot aggregate values and timestamp in the snapshot list view. r=fitzgen, a=blanket-sylvestre, l10n=blanket-sylvestre CLOSED TREE
authorJordan Santell <jsantell@mozilla.com>
Fri, 30 Oct 2015 13:31:41 -0700
changeset 305340 d0b21c37c1ab5c8b7db3750fc350ce9ca5761805
parent 305339 72f2d3449e06ea224d67f71301c0e8b8e571713e
child 305341 b8597247c87127845e2eda717a10f51a56a9ee8f
push id1001
push userraliiev@mozilla.com
push dateMon, 18 Jan 2016 19:06:03 +0000
treeherdermozilla-release@8b89261f3ac4 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersfitzgen, blanket-sylvestre
bugs1217243
milestone44.0a2
Bug 1217243 - Display snapshot aggregate values and timestamp in the snapshot list view. r=fitzgen, a=blanket-sylvestre, l10n=blanket-sylvestre CLOSED TREE
browser/locales/en-US/chrome/browser/devtools/memory.properties
devtools/client/memory/actions/snapshot.js
devtools/client/memory/components/snapshot-list-item.js
devtools/client/memory/models.js
devtools/client/memory/reducers/snapshots.js
devtools/client/memory/test/unit/test_utils-get-snapshot-totals.js
devtools/client/memory/test/unit/xpcshell.ini
devtools/client/memory/utils.js
devtools/client/themes/memory.css
devtools/shared/heapsnapshot/HeapAnalysesClient.js
devtools/shared/heapsnapshot/HeapAnalysesWorker.js
devtools/shared/heapsnapshot/tests/unit/test_HeapAnalyses_getCreationTime_01.js
devtools/shared/heapsnapshot/tests/unit/xpcshell.ini
--- a/browser/locales/en-US/chrome/browser/devtools/memory.properties
+++ b/browser/locales/en-US/chrome/browser/devtools/memory.properties
@@ -19,16 +19,24 @@ memory.label=Memory
 # This is used as the label for the toolbox panel.
 memory.panelLabel=Memory Panel
 
 # LOCALIZATION NOTE (memory.tooltip):
 # This string is displayed in the tooltip of the tab when the memory tool is
 # displayed inside the developer tools window.
 memory.tooltip=Memory
 
+# LOCALIZATION NOTE (aggregate.mb): The label annotating the number of bytes (in megabytes)
+# in a snapshot. %S represents the value, rounded to 2 decimal points.
+aggregate.mb=%S MB
+
+# LOCALIZATION NOTE (snapshot-title.loading): The title for a snapshot before
+# it has a creation time to display.
+snapshot-title.loading=Processing…
+
 # LOCALIZATION NOTE (checkbox.invertTree): The label describing the boolean
 # checkbox whether or not to invert the tree.
 checkbox.invertTree=Invert tree
 
 # LOCALIZATION NOTE (checkbox.recordAllocationStacks): The label describing the boolean
 # checkbox whether or not to record allocation stacks.
 checkbox.recordAllocationStacks=Record allocation stacks
 
--- a/devtools/client/memory/actions/snapshot.js
+++ b/devtools/client/memory/actions/snapshot.js
@@ -70,26 +70,29 @@ const takeSnapshot = exports.takeSnapsho
  * @param {HeapAnalysesClient}
  * @param {Snapshot} snapshot,
  */
 const readSnapshot = exports.readSnapshot = function readSnapshot (heapWorker, snapshot) {
   return function *(dispatch, getState) {
     assert(snapshot.state === states.SAVED,
       `Should only read a snapshot once. Found snapshot in state ${snapshot.state}`);
 
+    let creationTime;
+
     dispatch({ type: actions.READ_SNAPSHOT_START, snapshot });
     try {
       yield heapWorker.readHeapSnapshot(snapshot.path);
+      creationTime = yield heapWorker.getCreationTime(snapshot.path);
     } catch (error) {
       reportException("readSnapshot", error);
       dispatch({ type: actions.SNAPSHOT_ERROR, snapshot, error });
       return;
     }
 
-    dispatch({ type: actions.READ_SNAPSHOT_END, snapshot });
+    dispatch({ type: actions.READ_SNAPSHOT_END, snapshot, creationTime });
   };
 };
 
 /**
  * @param {HeapAnalysesClient} heapWorker
  * @param {Snapshot} snapshot,
  *
  * @see {Snapshot} model defined in devtools/client/memory/models.js
--- a/devtools/client/memory/components/snapshot-list-item.js
+++ b/devtools/client/memory/components/snapshot-list-item.js
@@ -1,34 +1,47 @@
 /* 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/. */
 
 const { DOM: dom, createClass, PropTypes } = require("devtools/client/shared/vendor/react");
-const { L10N, getSnapshotStatusText } = require("../utils");
+const { L10N, getSnapshotTitle, getSnapshotTotals, getSnapshotStatusText } = require("../utils");
+const { snapshotState: states } = require("../constants");
 const { snapshot: snapshotModel } = require("../models");
 
 const SnapshotListItem = module.exports = createClass({
   displayName: "snapshot-list-item",
 
   propTypes: {
     onClick: PropTypes.func,
     item: snapshotModel.isRequired,
     index: PropTypes.number.isRequired,
   },
 
   render() {
-    let { index, item, onClick } = this.props;
-    let className = `snapshot-list-item ${item.selected ? " selected" : ""}`;
-    let statusText = getSnapshotStatusText(item);
+    let { index, item: snapshot, onClick } = this.props;
+    let className = `snapshot-list-item ${snapshot.selected ? " selected" : ""}`;
+    let statusText = getSnapshotStatusText(snapshot);
+    let title = getSnapshotTitle(snapshot);
+
+    let details;
+    if (snapshot.state === states.SAVED_CENSUS) {
+      let { bytes } = getSnapshotTotals(snapshot);
+      let formatBytes = L10N.getFormatStr("aggregate.mb", L10N.numberWithDecimals(bytes / 1000000, 2));
+
+      details = dom.span({ className: "snapshot-totals" },
+        dom.span({ className: "total-bytes" }, formatBytes)
+      );
+    } else {
+      details = dom.span({ className: "snapshot-state" }, statusText);
+    }
 
     return (
       dom.li({ className, onClick },
         dom.span({
           className: `snapshot-title ${statusText ? " devtools-throbber" : ""}`
-        }, `Snapshot #${index}`),
-
-        statusText ? dom.span({ className: "snapshot-state" }, statusText) : void 0
+        }, title),
+        details
       )
     );
   }
 });
 
--- a/devtools/client/memory/models.js
+++ b/devtools/client/memory/models.js
@@ -31,32 +31,38 @@ let snapshotModel = exports.snapshot = P
   // Data of a census breakdown
   census: PropTypes.object,
   // The breakdown used to generate the current census
   breakdown: breakdownModel,
   // Whether the currently cached census tree is inverted or not.
   inverted: PropTypes.bool,
   // If an error was thrown while processing this snapshot, the `Error` instance is attached here.
   error: PropTypes.object,
+  // The creation time of the snapshot; required after the snapshot has been read.
+  creationTime: PropTypes.number,
   // State the snapshot is in
   // @see ./constants.js
   state: function (snapshot, propName) {
     let current = snapshot.state;
     let shouldHavePath = [states.SAVED, states.READ, states.SAVING_CENSUS, states.SAVED_CENSUS];
+    let shouldHaveCreationTime = [states.READ, states.SAVING_CENSUS, states.SAVED_CENSUS];
     let shouldHaveCensus = [states.SAVED_CENSUS];
 
     if (!stateKeys.includes(current)) {
       throw new Error(`Snapshot state must be one of ${stateKeys}.`);
     }
     if (shouldHavePath.includes(current) && !snapshot.path) {
       throw new Error(`Snapshots in state ${current} must have a snapshot path.`);
     }
     if (shouldHaveCensus.includes(current) && (!snapshot.census || !snapshot.breakdown)) {
       throw new Error(`Snapshots in state ${current} must have a census and breakdown.`);
     }
+    if (shouldHaveCreationTime.includes(current) && !snapshot.creationTime) {
+      throw new Error(`Snapshots in state ${current} must have a creation time.`);
+    }
   },
 });
 
 let allocationsModel = exports.allocations = PropTypes.shape({
   // True iff we are recording allocation stacks right now.
   recording: PropTypes.bool.isRequired,
   // True iff we are in the process of toggling the recording of allocation
   // stacks on or off right now.
--- a/devtools/client/memory/reducers/snapshots.js
+++ b/devtools/client/memory/reducers/snapshots.js
@@ -32,16 +32,17 @@ handlers[actions.READ_SNAPSHOT_START] = 
   let snapshot = getSnapshot(snapshots, action.snapshot);
   snapshot.state = states.READING;
   return [...snapshots];
 };
 
 handlers[actions.READ_SNAPSHOT_END] = function (snapshots, action) {
   let snapshot = getSnapshot(snapshots, action.snapshot);
   snapshot.state = states.READ;
+  snapshot.creationTime = action.creationTime;
   return [...snapshots];
 };
 
 handlers[actions.TAKE_CENSUS_START] = function (snapshots, action) {
   let snapshot = getSnapshot(snapshots, action.snapshot);
   snapshot.state = states.SAVING_CENSUS;
   snapshot.census = null;
   snapshot.breakdown = action.breakdown;
new file mode 100644
--- /dev/null
+++ b/devtools/client/memory/test/unit/test_utils-get-snapshot-totals.js
@@ -0,0 +1,63 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that we use the correct snapshot aggregate value
+ * in `utils.getSnapshotTotals(snapshot)`
+ */
+
+let { breakdowns, snapshotState: states } = require("devtools/client/memory/constants");
+let { getSnapshotTotals, breakdownEquals } = require("devtools/client/memory/utils");
+let { toggleInvertedAndRefresh } = require("devtools/client/memory/actions/inverted");
+let { takeSnapshotAndCensus } = require("devtools/client/memory/actions/snapshot");
+
+function run_test() {
+  run_next_test();
+}
+
+add_task(function *() {
+  let front = new StubbedMemoryFront();
+  let heapWorker = new HeapAnalysesClient();
+  yield front.attach();
+  let store = Store();
+  let { getState, dispatch } = store;
+
+  dispatch(takeSnapshotAndCensus(front, heapWorker));
+  yield waitUntilSnapshotState(store, [states.SAVED_CENSUS]);
+
+  ok(!getState().snapshots[0].inverted, "Snapshot is not inverted");
+  ok(isBreakdownType(getState().snapshots[0].census, "coarseType"),
+    "Snapshot using `coarseType` breakdown");
+
+  let census = getState().snapshots[0].census;
+  let result = aggregate(census);
+  let totalBytes = result.bytes;
+  let totalCount = result.count;
+
+  ok(totalBytes > 0, "counted up bytes in the census");
+  ok(totalCount > 0, "counted up count in the census");
+
+  result = getSnapshotTotals(getState().snapshots[0])
+  equal(totalBytes, result.bytes, "getSnapshotTotals reuslted in correct bytes");
+  equal(totalCount, result.count, "getSnapshotTotals reuslted in correct count");
+
+  dispatch(toggleInvertedAndRefresh(heapWorker));
+  yield waitUntilSnapshotState(store, [states.SAVING_CENSUS]);
+  yield waitUntilSnapshotState(store, [states.SAVED_CENSUS]);
+  ok(getState().snapshots[0].inverted, "Snapshot is inverted");
+
+  result = getSnapshotTotals(getState().snapshots[0])
+  equal(totalBytes, result.bytes, "getSnapshotTotals reuslted in correct bytes when inverted");
+  equal(totalCount, result.count, "getSnapshotTotals reuslted in correct count when inverted");
+});
+
+function aggregate (census) {
+  let totalBytes = census.bytes;
+  let totalCount = census.count;
+  for (let child of (census.children || [])) {
+    let { bytes, count } = aggregate(child);
+    totalBytes += bytes
+    totalCount += count;
+  }
+  return { bytes: totalBytes, count: totalCount };
+}
--- a/devtools/client/memory/test/unit/xpcshell.ini
+++ b/devtools/client/memory/test/unit/xpcshell.ini
@@ -12,8 +12,9 @@ skip-if = toolkit == 'android' || toolki
 [test_action-select-snapshot.js]
 [test_action-set-breakdown.js]
 [test_action-set-breakdown-and-refresh-01.js]
 [test_action-set-breakdown-and-refresh-02.js]
 [test_action-take-census.js]
 [test_action-take-snapshot.js]
 [test_action-take-snapshot-and-census.js]
 [test_utils.js]
+[test_utils-get-snapshot-totals.js]
--- a/devtools/client/memory/utils.js
+++ b/devtools/client/memory/utils.js
@@ -8,16 +8,37 @@ const STRINGS_URI = "chrome://browser/lo
 const L10N = exports.L10N = new ViewHelpers.L10N(STRINGS_URI);
 const { assert } = require("devtools/shared/DevToolsUtils");
 const { Preferences } = require("resource://gre/modules/Preferences.jsm");
 const CUSTOM_BREAKDOWN_PREF = "devtools.memory.custom-breakdowns";
 const DevToolsUtils = require("devtools/shared/DevToolsUtils");
 const { snapshotState: states, breakdowns } = require("./constants");
 
 /**
+ * Takes a snapshot object and returns the
+ * localized form of its timestamp to be used as a title.
+ *
+ * @param {Snapshot} snapshot
+ * @return {String}
+ */
+exports.getSnapshotTitle = function (snapshot) {
+  if (!snapshot.creationTime) {
+    return L10N.getStr("snapshot-title.loading");
+  }
+
+  let date = new Date(snapshot.creationTime / 1000);
+  return date.toLocaleTimeString(void 0, {
+    year: "2-digit",
+    month: "2-digit",
+    day: "2-digit",
+    hour12: false
+  });
+};
+
+/**
  * Returns an array of objects with the unique key `name`
  * and `displayName` for each breakdown.
  *
  * @return {Object{name, displayName}}
  */
 exports.getBreakdownDisplayData = function () {
   return exports.getBreakdownNames().map(name => {
     // If it's a preset use the display name value
@@ -211,8 +232,38 @@ exports.breakdownEquals = function (obj1
       return false;
     }
 
     return k1.every(k => exports.breakdownEquals(obj1[k], obj2[k]));
   }
 
   return false;
 };
+
+/**
+ * Takes a snapshot and returns the total bytes and
+ * total count that this snapshot represents.
+ *
+ * @param {Snapshot} snapshot
+ * @return {Object}
+ */
+exports.getSnapshotTotals = function (snapshot) {
+  let bytes, count;
+
+  let census = snapshot.census;
+
+  if (snapshot.inverted) {
+    while (census) {
+      bytes = census.totalBytes;
+      count = census.totalCount;
+      census = census.children && census.children[0];
+    }
+  } else {
+    bytes = census.totalBytes;
+    count = census.totalCount;
+  }
+
+  return {
+    bytes: bytes || 0,
+    count: count || 0,
+  };
+};
+
--- a/devtools/client/themes/memory.css
+++ b/devtools/client/themes/memory.css
@@ -124,26 +124,37 @@ html, .theme-body, #app, #memory-tool, #
   cursor: pointer;
 }
 
 .list > li.selected {
   background-color: var(--theme-selection-background);
   color: var(--theme-selection-color);
 }
 
+.snapshot-list-item {
+  position: relative;
+}
 .snapshot-list-item span {
   display: block;
 }
-
-.snapshot-list-item .snapshot-state {
+.snapshot-list-item .snapshot-state, .snapshot-list-item .snapshot-totals {
   font-size: 90%;
   color: var(--theme-body-color-alt);
+  position: absolute;
 }
-
-.snapshot-list-item.selected .snapshot-state {
+.snapshot-list-item .snapshot-state {
+  top: 38px;
+}
+.snapshot-list-item .snapshot-totals {
+  top: 38px;
+}
+.snapshot-list-item .total-bytes {
+  float: left;
+}
+.snapshot-list-item.selected .snapshot-state, .snapshot-list-item.selected .snapshot-totals {
   /* Text inside a selected item should not be custom colored. */
   color: inherit !important;
 }
 
 /**
  * Main panel
  */
 
--- a/devtools/shared/heapsnapshot/HeapAnalysesClient.js
+++ b/devtools/shared/heapsnapshot/HeapAnalysesClient.js
@@ -123,9 +123,23 @@ HeapAnalysesClient.prototype.takeCensusD
                                                         censusOptions,
                                                         requestOptions = {}) {
   return this._worker.performTask("takeCensusDiff", {
     firstSnapshotFilePath,
     secondSnapshotFilePath,
     censusOptions,
     requestOptions
   });
-}
+};
+
+/**
+ * Request the creation time given a snapshot file path. Returns `null`
+ * if snapshot does not exist.
+ *
+ * @param {String} snapshotFilePath
+ *        The path to the snapshot.
+ * @return {Number?}
+ *        The unix timestamp of the creation time of the snapshot, or null if
+ *        snapshot does not exist.
+ */
+HeapAnalysesClient.prototype.getCreationTime = function (snapshotFilePath) {
+  return this._worker.performTask("getCreationTime", snapshotFilePath);
+};
--- a/devtools/shared/heapsnapshot/HeapAnalysesWorker.js
+++ b/devtools/shared/heapsnapshot/HeapAnalysesWorker.js
@@ -73,8 +73,16 @@ workerHelper.createTask(self, "takeCensu
   if (requestOptions.asTreeNode) {
     return censusReportToCensusTreeNode(censusOptions.breakdown, delta);
   } else if (requestOptions.asInvertedTreeNode) {
     return censusReportToCensusTreeNode(censusOptions.breakdown, delta, { invert: true });
   } else {
     return delta;
   }
 });
+
+/**
+ * @see HeapAnalysesClient.prototype.getCreationTime
+ */
+workerHelper.createTask(self, "getCreationTime", (snapshotFilePath) => {
+  let snapshot = snapshots[snapshotFilePath];
+  return snapshot ? snapshot.creationTime : null;
+});
new file mode 100644
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/unit/test_HeapAnalyses_getCreationTime_01.js
@@ -0,0 +1,47 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that the HeapAnalyses{Client,Worker} can get a HeapSnapshot's
+// creation time.
+
+function waitForTenMilliseconds() {
+  const start = Date.now();
+  while (Date.now() - start < 10) ;
+}
+
+function run_test() {
+  run_next_test();
+}
+
+const BREAKDOWN = {
+  by: "internalType",
+  then: { by: "count", count: true, bytes: true }
+};
+
+add_task(function* () {
+  const client = new HeapAnalysesClient();
+  const start = Date.now() * 1000;
+
+  // Because Date.now() is less precise than the snapshot's time stamp, give it
+  // a little bit of head room.
+  waitForTenMilliseconds();
+  const snapshotFilePath = saveNewHeapSnapshot();
+  waitForTenMilliseconds();
+  const end = Date.now() * 1000;
+
+  yield client.readHeapSnapshot(snapshotFilePath);
+  ok(true, "Should have read the heap snapshot");
+
+  let time = yield client.getCreationTime("/not/a/real/path", {
+    breakdown: BREAKDOWN
+  });
+  equal(time, null, "getCreationTime returns `null` when snapshot does not exist");
+
+  time = yield client.getCreationTime(snapshotFilePath, {
+    breakdown: BREAKDOWN
+  });
+  ok(time >= start, "creation time occurred after start");
+  ok(time <= end, "creation time occurred before end");
+
+  client.destroy();
+});
--- a/devtools/shared/heapsnapshot/tests/unit/xpcshell.ini
+++ b/devtools/shared/heapsnapshot/tests/unit/xpcshell.ini
@@ -18,16 +18,17 @@ support-files =
 [test_census_diff_06.js]
 [test_census-tree-node-01.js]
 [test_census-tree-node-02.js]
 [test_census-tree-node-03.js]
 [test_census-tree-node-04.js]
 [test_census-tree-node-05.js]
 [test_census-tree-node-06.js]
 [test_census-tree-node-07.js]
+[test_HeapAnalyses_getCreationTime_01.js]
 [test_HeapAnalyses_readHeapSnapshot_01.js]
 [test_HeapAnalyses_takeCensusDiff_01.js]
 [test_HeapAnalyses_takeCensusDiff_02.js]
 [test_HeapAnalyses_takeCensus_01.js]
 [test_HeapAnalyses_takeCensus_02.js]
 [test_HeapAnalyses_takeCensus_03.js]
 [test_HeapAnalyses_takeCensus_04.js]
 [test_HeapAnalyses_takeCensus_05.js]