Bug 1217500 - Add UI for inverting display tree in memory tool; r=jsantell
authorNick Fitzgerald <fitzgen@gmail.com>
Tue, 27 Oct 2015 09:11:05 -0700
changeset 269817 964ecb2b68a0aea3204e0310c65e8e09bd9fe6b7
parent 269754 f6d32a2fa56177e545400e655f3f57ebaa6b5a39
child 269818 c66c3620de8426242471640ad30e8b7a9f56ccd7
push id29592
push usercbook@mozilla.com
push dateWed, 28 Oct 2015 09:38:46 +0000
treeherdermozilla-central@872927368b0e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjsantell
bugs1217500
milestone44.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 1217500 - Add UI for inverting display tree in memory tool; r=jsantell
devtools/client/memory/actions/breakdown.js
devtools/client/memory/actions/inverted.js
devtools/client/memory/actions/moz.build
devtools/client/memory/actions/snapshot.js
devtools/client/memory/app.js
devtools/client/memory/components/toolbar.js
devtools/client/memory/constants.js
devtools/client/memory/models.js
devtools/client/memory/reducers.js
devtools/client/memory/reducers/inverted.js
devtools/client/memory/reducers/moz.build
devtools/client/memory/reducers/snapshots.js
devtools/client/memory/test/unit/test_action-toggle-inverted-and-refresh-01.js
devtools/client/memory/test/unit/test_action-toggle-inverted-and-refresh-02.js
devtools/client/memory/test/unit/test_action-toggle-inverted.js
devtools/client/memory/test/unit/xpcshell.ini
devtools/client/memory/utils.js
--- a/devtools/client/memory/actions/breakdown.js
+++ b/devtools/client/memory/actions/breakdown.js
@@ -1,43 +1,33 @@
 /* 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";
 
-// @TODO 1215606
-// Use this assert instead of utils when fixed.
-// const { assert } = require("devtools/shared/DevToolsUtils");
-const { breakdownEquals, createSnapshot, assert } = require("../utils");
+const { assert } = require("devtools/shared/DevToolsUtils");
+const { breakdownEquals, createSnapshot } = require("../utils");
 const { actions, snapshotState: states } = require("../constants");
-const { takeCensus } = require("./snapshot");
+const { refreshSelectedCensus } = require("./snapshot");
 
 const setBreakdownAndRefresh = exports.setBreakdownAndRefresh = function (heapWorker, breakdown) {
   return function *(dispatch, getState) {
-    // Clears out all stored census data and sets
-    // the breakdown
+    // Clears out all stored census data and sets the breakdown.
     dispatch(setBreakdown(breakdown));
-    let snapshot = getState().snapshots.find(s => s.selected);
-
-    // If selected snapshot does not have updated census if the breakdown
-    // changed, retake the census with new breakdown
-    if (snapshot && !breakdownEquals(snapshot.breakdown, breakdown)) {
-      yield dispatch(takeCensus(heapWorker, snapshot));
-    }
+    yield dispatch(refreshSelectedCensus(heapWorker));
   };
 };
 
 /**
  * Clears out all census data in the snapshots and sets
  * a new breakdown.
  *
  * @param {Breakdown} breakdown
  */
 const setBreakdown = exports.setBreakdown = function (breakdown) {
-  // @TODO 1215606
   assert(typeof breakdown === "object" && breakdown.by,
     `Breakdowns must be an object with a \`by\` property, attempted to set: ${uneval(breakdown)}`);
 
   return {
     type: actions.SET_BREAKDOWN,
     breakdown,
   }
 };
new file mode 100644
--- /dev/null
+++ b/devtools/client/memory/actions/inverted.js
@@ -0,0 +1,18 @@
+/* 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 { actions } = require("../constants");
+const { refreshSelectedCensus } = require("./snapshot");
+
+const toggleInverted = exports.toggleInverted = function () {
+  return { type: actions.TOGGLE_INVERTED };
+};
+
+exports.toggleInvertedAndRefresh = function (heapWorker) {
+  return function* (dispatch, getState) {
+    dispatch(toggleInverted());
+    yield dispatch(refreshSelectedCensus(heapWorker));
+  };
+};
--- a/devtools/client/memory/actions/moz.build
+++ b/devtools/client/memory/actions/moz.build
@@ -1,10 +1,11 @@
 # vim: set filetype=python:
 # 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/.
 
 DevToolsModules(
     'allocations.js',
     'breakdown.js',
+    'inverted.js',
     'snapshot.js',
 )
--- a/devtools/client/memory/actions/snapshot.js
+++ b/devtools/client/memory/actions/snapshot.js
@@ -1,17 +1,15 @@
 /* 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";
 
-// @TODO 1215606
-// Use this assert instead of utils when fixed.
-// const { assert } = require("devtools/shared/DevToolsUtils");
-const { getSnapshot, breakdownEquals, createSnapshot, assert } = require("../utils");
+const { assert } = require("devtools/shared/DevToolsUtils");
+const { getSnapshot, breakdownEquals, createSnapshot } = require("../utils");
 const { actions, snapshotState: states } = require("../constants");
 
 /**
  * A series of actions are fired from this task to save, read and generate the initial
  * census from a snapshot.
  *
  * @param {MemoryFront}
  * @param {HeapAnalysesClient}
@@ -62,17 +60,16 @@ const takeSnapshot = exports.takeSnapsho
  * Reads a snapshot into memory; necessary to do before taking
  * a census on the snapshot. May only be called once per snapshot.
  *
  * @param {HeapAnalysesClient}
  * @param {Snapshot} snapshot,
  */
 const readSnapshot = exports.readSnapshot = function readSnapshot (heapWorker, snapshot) {
   return function *(dispatch, getState) {
-    // @TODO 1215606
     assert(snapshot.state === states.SAVED,
       "Should only read a snapshot once");
 
     dispatch({ type: actions.READ_SNAPSHOT_START, snapshot });
     yield heapWorker.readHeapSnapshot(snapshot.path);
     dispatch({ type: actions.READ_SNAPSHOT_END, snapshot });
   };
 };
@@ -82,38 +79,63 @@ const readSnapshot = exports.readSnapsho
  * @param {Snapshot} snapshot,
  *
  * @see {Snapshot} model defined in devtools/client/memory/models.js
  * @see `devtools/shared/heapsnapshot/HeapAnalysesClient.js`
  * @see `js/src/doc/Debugger/Debugger.Memory.md` for breakdown details
  */
 const takeCensus = exports.takeCensus = function (heapWorker, snapshot) {
   return function *(dispatch, getState) {
-    // @TODO 1215606
     assert([states.READ, states.SAVED_CENSUS].includes(snapshot.state),
       "Can only take census of snapshots in READ or SAVED_CENSUS state");
 
     let census;
+    let inverted = getState().inverted;
     let breakdown = getState().breakdown;
 
-    // If breakdown hasn't changed, don't do anything
-    if (breakdownEquals(breakdown, snapshot.breakdown)) {
+    // If breakdown and inversion haven't changed, don't do anything.
+    if (inverted === snapshot.inverted && breakdownEquals(breakdown, snapshot.breakdown)) {
       return;
     }
 
     // Keep taking a census if the breakdown changes during. Recheck
     // that the breakdown used for the census is the same as
     // the state's breakdown.
     do {
+      inverted = getState().inverted;
       breakdown = getState().breakdown;
-      dispatch({ type: actions.TAKE_CENSUS_START, snapshot, breakdown });
-      census = yield heapWorker.takeCensus(snapshot.path, { breakdown }, { asTreeNode: true });
-    } while (!breakdownEquals(breakdown, getState().breakdown));
+      dispatch({ type: actions.TAKE_CENSUS_START, snapshot, inverted, breakdown });
+      let opts = inverted ? { asInvertedTreeNode: true } : { asTreeNode: true };
+      census = yield heapWorker.takeCensus(snapshot.path, { breakdown }, opts);
+    } while (inverted !== getState().inverted ||
+             !breakdownEquals(breakdown, getState().breakdown));
+
+    dispatch({ type: actions.TAKE_CENSUS_END, snapshot, breakdown, inverted, census });
+  };
+};
 
-    dispatch({ type: actions.TAKE_CENSUS_END, snapshot, breakdown, census });
+/**
+ * Refresh the selected snapshot's census data, if need be (for example,
+ * breakdown configuration changed).
+ *
+ * @param {HeapAnalysesClient} heapWorker
+ */
+const refreshSelectedCensus = exports.refreshSelectedCensus = function (heapWorker) {
+  return function*(dispatch, getState) {
+    let snapshot = getState().snapshots.find(s => s.selected);
+
+    // Intermediate snapshot states will get handled by the task action that is
+    // orchestrating them. For example, if the snapshot's state is
+    // SAVING_CENSUS, then the takeCensus action will keep taking a census until
+    // the inverted property matches the inverted state. If the snapshot is
+    // still in the process of being saved or read, the takeSnapshotAndCensus
+    // task action will follow through and ensure that a census is taken.
+    if (snapshot && snapshot.state === states.SAVED_CENSUS) {
+      yield dispatch(takeCensus(heapWorker, snapshot));
+    }
   };
 };
 
 /**
  * @param {Snapshot}
  * @see {Snapshot} model defined in devtools/client/memory/models.js
  */
 const selectSnapshot = exports.selectSnapshot = function (snapshot) {
--- a/devtools/client/memory/app.js
+++ b/devtools/client/memory/app.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/. */
 
 const { DOM: dom, createClass, createFactory, PropTypes } = require("devtools/client/shared/vendor/react");
 const { connect } = require("devtools/client/shared/vendor/react-redux");
 const { toggleRecordingAllocationStacks } = require("./actions/allocations");
 const { setBreakdownAndRefresh } = require("./actions/breakdown");
+const { toggleInvertedAndRefresh } = require("./actions/inverted");
 const { selectSnapshotAndRefresh, takeSnapshotAndCensus } = require("./actions/snapshot");
 const { breakdownNameToSpec, getBreakdownDisplayData } = require("./utils");
 const Toolbar = createFactory(require("./components/toolbar"));
 const List = createFactory(require("./components/list"));
 const SnapshotListItem = createFactory(require("./components/snapshot-list-item"));
 const HeapView = createFactory(require("./components/heap"));
 const { app: appModel } = require("./models");
 
@@ -34,31 +35,35 @@ const App = createClass({
   render() {
     let {
       dispatch,
       snapshots,
       front,
       heapWorker,
       breakdown,
       allocations,
+      inverted
     } = this.props;
 
     let selectedSnapshot = snapshots.find(s => s.selected);
 
     return (
       dom.div({ id: "memory-tool" }, [
 
         Toolbar({
           breakdowns: getBreakdownDisplayData(),
           onTakeSnapshotClick: () => dispatch(takeSnapshotAndCensus(front, heapWorker)),
           onBreakdownChange: breakdown =>
             dispatch(setBreakdownAndRefresh(heapWorker, breakdownNameToSpec(breakdown))),
           onToggleRecordAllocationStacks: () =>
             dispatch(toggleRecordingAllocationStacks(front)),
-          allocations
+          allocations,
+          inverted,
+          onToggleInverted: () =>
+            dispatch(toggleInvertedAndRefresh(heapWorker))
         }),
 
         dom.div({ id: "memory-tool-container" }, [
           List({
             itemComponent: SnapshotListItem,
             items: snapshots,
             onClick: snapshot => dispatch(selectSnapshotAndRefresh(heapWorker, snapshot))
           }),
--- a/devtools/client/memory/components/toolbar.js
+++ b/devtools/client/memory/components/toolbar.js
@@ -11,43 +11,57 @@ const Toolbar = module.exports = createC
   propTypes: {
     breakdowns: PropTypes.arrayOf(PropTypes.shape({
       name: PropTypes.string.isRequired,
       displayName: PropTypes.string.isRequired,
     })).isRequired,
     onTakeSnapshotClick: PropTypes.func.isRequired,
     onBreakdownChange: PropTypes.func.isRequired,
     onToggleRecordAllocationStacks: PropTypes.func.isRequired,
-    allocations: models.allocations
+    allocations: models.allocations,
+    onToggleInverted: PropTypes.func.isRequired,
+    inverted: PropTypes.bool.isRequired,
   },
 
   render() {
     let {
       onTakeSnapshotClick,
       onBreakdownChange,
       breakdowns,
       onToggleRecordAllocationStacks,
       allocations,
+      onToggleInverted,
+      inverted
     } = this.props;
 
     return (
       DOM.div({ className: "devtools-toolbar" }, [
         DOM.button({ className: `take-snapshot devtools-button`, onClick: onTakeSnapshotClick }),
 
         DOM.select({
           className: `select-breakdown`,
           onChange: e => onBreakdownChange(e.target.value),
         }, breakdowns.map(({ name, displayName }) => DOM.option({ value: name }, displayName))),
 
         DOM.label({}, [
           DOM.input({
             type: "checkbox",
+            checked: inverted,
+            onChange: onToggleInverted,
+          }),
+          // TODO bug 1214799
+          "Invert tree"
+        ]),
+
+        DOM.label({}, [
+          DOM.input({
+            type: "checkbox",
             checked: allocations.recording,
             disabled: allocations.togglingInProgress,
             onChange: onToggleRecordAllocationStacks,
           }),
           // TODO bug 1214799
           "Record allocation stacks"
         ])
-      ])
+])
     );
   }
 });
--- a/devtools/client/memory/constants.js
+++ b/devtools/client/memory/constants.js
@@ -21,16 +21,19 @@ actions.TAKE_CENSUS_END = "take-census-e
 
 // When requesting that the server start/stop recording allocation stacks.
 actions.TOGGLE_RECORD_ALLOCATION_STACKS_START = "toggle-record-allocation-stacks-start";
 actions.TOGGLE_RECORD_ALLOCATION_STACKS_END = "toggle-record-allocation-stacks-end";
 
 // Fired by UI to select a snapshot to view.
 actions.SELECT_SNAPSHOT = "select-snapshot";
 
+// Fired to toggle tree inversion on or off.
+actions.TOGGLE_INVERTED = "toggle-inverted";
+
 // Options passed to MemoryFront's startRecordingAllocations never change.
 exports.ALLOCATION_RECORDING_OPTIONS = {
   probability: 1,
   maxLogLength: 1
 };
 
 const COUNT = { by: "count", count: true, bytes: true };
 const INTERNAL_TYPE = { by: "internalType", then: COUNT };
--- a/devtools/client/memory/models.js
+++ b/devtools/client/memory/models.js
@@ -26,16 +26,18 @@ let snapshotModel = exports.snapshot = P
   selected: PropTypes.bool.isRequired,
   // fs path to where the snapshot is stored; used to
   // identify the snapshot for HeapAnalysesClient.
   path: PropTypes.string,
   // 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,
   // State the snapshot is in
   // @see ./constants.js
   state: function (props, propName) {
     let stateNames = Object.keys(states);
     let current = props.state;
     let shouldHavePath = [states.SAVED, states.READ, states.SAVING_CENSUS, states.SAVED_CENSUS];
     let shouldHaveCensus = [states.SAVED_CENSUS];
 
@@ -58,18 +60,20 @@ let allocationsModel = exports.allocatio
   // stacks on or off right now.
   togglingInProgress: PropTypes.bool.isRequired,
 });
 
 let appModel = exports.app = {
   // {MemoryFront} Used to communicate with platform
   front: PropTypes.instanceOf(MemoryFront),
   // Allocations recording related data.
-  allocations: allocationsModel,
+  allocations: allocationsModel.isRequired,
   // {HeapAnalysesClient} Used to interface with snapshots
   heapWorker: PropTypes.instanceOf(HeapAnalysesClient),
   // The breakdown object DSL describing how we want
   // the census data to be.
   // @see `js/src/doc/Debugger/Debugger.Memory.md`
   breakdown: breakdownModel.isRequired,
   // List of reference to all snapshots taken
   snapshots: PropTypes.arrayOf(snapshotModel).isRequired,
+  // True iff we want the tree displayed inverted.
+  inverted: PropTypes.bool.isRequired,
 };
--- a/devtools/client/memory/reducers.js
+++ b/devtools/client/memory/reducers.js
@@ -2,8 +2,9 @@
  * 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";
 
 exports.allocations = require("./reducers/allocations");
 exports.snapshots = require("./reducers/snapshots");
 exports.breakdown = require("./reducers/breakdown");
 exports.errors = require("./reducers/errors");
+exports.inverted = require("./reducers/inverted");
new file mode 100644
--- /dev/null
+++ b/devtools/client/memory/reducers/inverted.js
@@ -0,0 +1,10 @@
+/* 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 { actions } = require("../constants");
+
+module.exports = function (inverted = false, action) {
+  return action.type === actions.TOGGLE_INVERTED ? !inverted : inverted;
+};
--- a/devtools/client/memory/reducers/moz.build
+++ b/devtools/client/memory/reducers/moz.build
@@ -2,10 +2,11 @@
 # 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/.
 
 DevToolsModules(
     'allocations.js',
     'breakdown.js',
     'errors.js',
+    'inverted.js',
     'snapshots.js',
 )
--- a/devtools/client/memory/reducers/snapshots.js
+++ b/devtools/client/memory/reducers/snapshots.js
@@ -33,24 +33,26 @@ handlers[actions.READ_SNAPSHOT_END] = fu
   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;
+  snapshot.inverted = action.inverted;
   return [...snapshots];
 };
 
 handlers[actions.TAKE_CENSUS_END] = function (snapshots, action) {
   let snapshot = getSnapshot(snapshots, action.snapshot);
   snapshot.state = states.SAVED_CENSUS;
   snapshot.census = action.census;
   snapshot.breakdown = action.breakdown;
+  snapshot.inverted = action.inverted;
   return [...snapshots];
 };
 
 handlers[actions.SELECT_SNAPSHOT] = function (snapshots, action) {
   return snapshots.map(s => {
     s.selected = s.id === action.snapshot.id;
     return s;
   });
new file mode 100644
--- /dev/null
+++ b/devtools/client/memory/test/unit/test_action-toggle-inverted-and-refresh-01.js
@@ -0,0 +1,64 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that changing inverted state properly refreshes the selected census.
+
+let { breakdowns, snapshotState: states } = require("devtools/client/memory/constants");
+let { toggleInvertedAndRefresh } = require("devtools/client/memory/actions/inverted");
+let { takeSnapshotAndCensus, selectSnapshotAndRefresh } = 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;
+
+  equal(getState().inverted, false, "not inverted by default");
+
+  dispatch(takeSnapshotAndCensus(front, heapWorker));
+  dispatch(takeSnapshotAndCensus(front, heapWorker));
+  dispatch(takeSnapshotAndCensus(front, heapWorker));
+
+  yield waitUntilSnapshotState(store, [states.SAVED_CENSUS,
+                                       states.SAVED_CENSUS,
+                                       states.SAVED_CENSUS]);
+  ok(true, "saved 3 snapshots and took a census of each of them");
+
+  dispatch(toggleInvertedAndRefresh(heapWorker));
+  yield waitUntilSnapshotState(store, [states.SAVED_CENSUS,
+                                       states.SAVED_CENSUS,
+                                       states.SAVING_CENSUS]);
+  ok(true, "toggling inverted should recompute the selected snapshot's census");
+
+  equal(getState().inverted, true, "now inverted");
+
+  yield waitUntilSnapshotState(store, [states.SAVED_CENSUS,
+                                       states.SAVED_CENSUS,
+                                       states.SAVED_CENSUS]);
+
+  equal(getState().snapshots[0].inverted, false);
+  equal(getState().snapshots[1].inverted, false);
+  equal(getState().snapshots[2].inverted, true);
+
+  dispatch(selectSnapshotAndRefresh(heapWorker, getState().snapshots[1]));
+  yield waitUntilSnapshotState(store, [states.SAVED_CENSUS,
+                                       states.SAVING_CENSUS,
+                                       states.SAVED_CENSUS]);
+  ok(true, "selecting non-inverted census should trigger a recompute");
+
+  yield waitUntilSnapshotState(store, [states.SAVED_CENSUS,
+                                       states.SAVED_CENSUS,
+                                       states.SAVED_CENSUS]);
+
+  equal(getState().snapshots[0].inverted, false);
+  equal(getState().snapshots[1].inverted, true);
+  equal(getState().snapshots[2].inverted, true);
+
+  heapWorker.destroy();
+  yield front.detach();
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/memory/test/unit/test_action-toggle-inverted-and-refresh-02.js
@@ -0,0 +1,46 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that changing inverted state in the middle of taking a snapshot results
+// in an inverted census.
+
+let { snapshotState: states } = require("devtools/client/memory/constants");
+let { toggleInverted, toggleInvertedAndRefresh } = require("devtools/client/memory/actions/inverted");
+let { takeSnapshotAndCensus, selectSnapshotAndRefresh } = 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.SAVING]);
+
+  dispatch(toggleInverted());
+
+  yield waitUntilSnapshotState(store, [states.SAVED_CENSUS]);
+  ok(getState().inverted,
+     "should want inverted trees");
+  ok(getState().snapshots[0].inverted,
+     "snapshot-we-were-in-the-middle-of-saving's census should be inverted");
+
+  dispatch(toggleInvertedAndRefresh(heapWorker));
+  yield waitUntilSnapshotState(store, [states.SAVING_CENSUS]);
+  ok(true, "toggling inverted retriggers census");
+  ok(!getState().inverted, "no long inverted");
+
+  dispatch(toggleInverted());
+  yield waitUntilSnapshotState(store, [states.SAVED_CENSUS]);
+  ok(getState().inverted, "inverted again");
+  ok(getState().snapshots[0].inverted,
+     "census-we-were-in-the-middle-of-recomputing should be inverted again");
+
+  heapWorker.destroy();
+  yield front.detach();
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/memory/test/unit/test_action-toggle-inverted.js
@@ -0,0 +1,29 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test toggling the top level inversion state of the tree.
+
+let { toggleInverted } = require("devtools/client/memory/actions/inverted");
+
+function run_test() {
+  run_next_test();
+}
+
+add_task(function *() {
+  let front = new StubbedMemoryFront();
+  let heapWorker = new HeapAnalysesClient();
+  yield front.attach();
+  let store = Store();
+  const { getState, dispatch } = store;
+
+  equal(getState().inverted, false, "not inverted by default");
+
+  dispatch(toggleInverted());
+  equal(getState().inverted, true, "now inverted after toggling");
+
+  dispatch(toggleInverted());
+  equal(getState().inverted, false, "not inverted again after toggling again");
+
+  heapWorker.destroy();
+  yield front.detach();
+});
--- a/devtools/client/memory/test/unit/xpcshell.ini
+++ b/devtools/client/memory/test/unit/xpcshell.ini
@@ -1,15 +1,18 @@
 [DEFAULT]
 tags = devtools
 head = head.js
 tail =
 firefox-appdir = browser
 skip-if = toolkit == 'android' || toolkit == 'gonk'
 
+[test_action-toggle-inverted.js]
+[test_action-toggle-inverted-and-refresh-01.js]
+[test_action-toggle-inverted-and-refresh-02.js]
 [test_action-toggle-recording-allocations.js]
 [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]
--- a/devtools/client/memory/utils.js
+++ b/devtools/client/memory/utils.js
@@ -1,30 +1,21 @@
 /* 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 { 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");
 const SAVING_SNAPSHOT_TEXT = "Saving snapshot...";
 const READING_SNAPSHOT_TEXT = "Reading snapshot...";
 const SAVING_CENSUS_TEXT = "Taking heap census...";
 
-// @TODO 1215606
-// Use DevToolsUtils.assert when fixed.
-exports.assert = function (condition, message) {
-  if (!condition) {
-    const err = new Error("Assertion failure: " + message);
-    DevToolsUtils.reportException("DevToolsUtils.assert", err);
-    throw err;
-  }
-};
-
 /**
  * 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 => {
@@ -94,17 +85,17 @@ exports.breakdownNameToSpec = function (
 
 /**
  * Returns a string representing a readable form of the snapshot's state.
  *
  * @param {Snapshot} snapshot
  * @return {String}
  */
 exports.getSnapshotStatusText = function (snapshot) {
-  exports.assert((snapshot || {}).state,
+  assert((snapshot || {}).state,
     `Snapshot must have expected state, found ${(snapshot || {}).state}.`);
 
   switch (snapshot.state) {
     case states.SAVING:
       return SAVING_SNAPSHOT_TEXT;
     case states.SAVED:
     case states.READING:
       return READING_SNAPSHOT_TEXT;