Merge m-c to inbound. a=merge
authorRyan VanderMeulen <ryanvm@gmail.com>
Sun, 03 Apr 2016 21:40:20 -0400
changeset 291528 600f96d25f8c94c73849dc98f0ad36769f9b57d3
parent 291527 2effa07d02e114b9988cec11e443ce2ab79264c9 (current diff)
parent 291521 cfd51e67b26e1f969fb0956efb3ea27e12216491 (diff)
child 291529 6559f99e1805147da0fb0c56e52a09397d36cc3b
push id19656
push usergwagner@mozilla.com
push dateMon, 04 Apr 2016 13:43:23 +0000
treeherderb2g-inbound@e99061fde28a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone48.0a1
Merge m-c to inbound. a=merge
--- a/devtools/client/inspector/computed/test/browser_computed_search-filter_clear.js
+++ b/devtools/client/inspector/computed/test/browser_computed_search-filter_clear.js
@@ -5,16 +5,18 @@
 "use strict";
 
 // Tests that the search filter clear button works properly.
 
 const TEST_URI = `
   <style type="text/css">
     .matches {
       color: #F00;
+      background-color: #00F;
+      border-color: #0F0;
     }
   </style>
   <span id="matches" class="matches">Some styled text</span>
 `;
 
 add_task(function*() {
   yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
   let {inspector, view} = yield openComputedView();
@@ -24,21 +26,16 @@ add_task(function*() {
 });
 
 function* testAddTextInFilter(inspector, computedView) {
   info("Setting filter text to \"background-color\"");
 
   let win = computedView.styleWindow;
   let propertyViews = computedView.propertyViews;
   let searchField = computedView.searchField;
-  let checkbox = computedView.includeBrowserStylesCheckbox;
-
-  info("Include browser styles");
-  checkbox.click();
-  yield inspector.once("computed-view-refreshed");
 
   searchField.focus();
   synthesizeKeys("background-color", win);
   yield inspector.once("computed-view-refreshed");
 
   info("Check that the correct properties are visible");
 
   propertyViews.forEach((propView) => {
@@ -59,13 +56,12 @@ function* testClearSearchFilter(inspecto
 
   EventUtils.synthesizeMouseAtCenter(searchClearButton, {}, win);
   yield onRefreshed;
 
   info("Check that the correct properties are visible");
 
   ok(!searchField.value, "Search filter is cleared");
   propertyViews.forEach((propView) => {
-    let name = propView.name;
-    is(propView.visible, true,
-      "span " + name + " property is visible");
+    is(propView.visible, propView.hasMatchedSelectors,
+      "span " + propView.name + " property visibility check");
   });
 }
--- a/devtools/client/locales/en-US/memory.properties
+++ b/devtools/client/locales/en-US/memory.properties
@@ -72,16 +72,20 @@ censusDisplays.coarseType.tooltip=Group 
 # LOCALIZATION NOTE (censusDisplays.allocationStack.tooltip): The tooltip for
 # the "allocation stack" display option.
 censusDisplays.allocationStack.tooltip=Group items by the JavaScript stack recorded when the object was allocated
 
 # LOCALIZATION NOTE (censusDisplays.invertedAllocationStack.tooltip): The
 # tooltip for the "inverted allocation stack" display option.
 censusDisplays.invertedAllocationStack.tooltip=Group items by the inverted JavaScript call stack recorded when the object was created
 
+# LOCALIZATION NOTE (breakdowns.treeMap.tooltip): The tooltip for the "tree map"
+# breakdown option.
+censusDisplays.treeMap.tooltip=Visualize memory usage: larger blocks account for a larger percent of memory usage
+
 # LOCALIZATION NOTE (censusDisplays.objectClass.tooltip): The tooltip for the
 # "object class" display option.
 censusDisplays.objectClass.tooltip=Group items by their JavaScript Object [[class]] name
 
 # LOCALIZATION NOTE (censusDisplays.internalType.tooltip): The tooltip for the
 # "internal type" display option.
 censusDisplays.internalType.tooltip=Group items by their internal C++ type
 
@@ -100,16 +104,20 @@ dominatorTreeDisplays.coarseType.tooltip
 # LOCALIZATION NOTE (dominatorTreeDisplays.allocationStack.tooltip): The
 # tooltip for the "allocation stack" dominator tree display option.
 dominatorTreeDisplays.allocationStack.tooltip=Label objects by the JavaScript stack recorded when it was allocated
 
 # LOCALIZATION NOTE (dominatorTreeDisplays.internalType.tooltip): The
 # tooltip for the "internal type" dominator tree display option.
 dominatorTreeDisplays.internalType.tooltip=Label objects by their internal C++ type name
 
+# LOCALIZATION NOTE (treeMapDisplays.coarseType.tooltip): The tooltip for
+# the "coarse type" tree map display option.
+treeMapDisplays.coarseType.tooltip=Label objects by the broad categories they fit in
+
 # LOCALIZATION NOTE (toolbar.view): The label for the view selector in the
 # toolbar.
 toolbar.view=View:
 
 # LOCALIZATION NOTE (toolbar.view.tooltip): The tooltip for the label for the
 # view selector in the toolbar.
 toolbar.view.tooltip=Change the view of the heap snapshot
 
@@ -124,16 +132,24 @@ toolbar.view.census.tooltip=View a summary of the heap snapshot’s contents by aggregating objects into groups
 # LOCALIZATION NOTE (toolbar.view.dominators): The label for the dominators view
 # option in the toolbar.
 toolbar.view.dominators=Dominators
 
 # LOCALIZATION NOTE (toolbar.view.dominators.tooltip): The tooltip for the label
 # for the dominators view option in the toolbar.
 toolbar.view.dominators.tooltip=View the dominator tree and surface the largest structures in the heap snapshot
 
+# LOCALIZATION NOTE (toolbar.view.treemap): The label for the tree map option
+# in the toolbar.
+toolbar.view.treemap=Tree Map
+
+# LOCALIZATION NOTE (toolbar.view.treemap.tooltip): The tooltip for the label for
+# the tree map view option in the toolbar.
+toolbar.view.treemap.tooltip=Visualize memory usage: larger blocks account for a larger percent of memory usage
+
 # LOCALIZATION NOTE (take-snapshot): The label describing the button that
 # initiates taking a snapshot, either as the main label, or a tooltip.
 take-snapshot=Take snapshot
 
 # LOCALIZATION NOTE (import-snapshot): The label describing the button that
 # initiates importing a snapshot.
 import-snapshot=Import…
 
@@ -266,16 +282,20 @@ snapshot.state.importing.full=Importing…
 # snapshot state READING, and SAVED, due to these states being combined
 # visually, used in the main heap view.
 snapshot.state.reading.full=Reading snapshot…
 
 # LOCALIZATION NOTE (snapshot.state.saving-census.full): The label describing
 # the snapshot state SAVING, used in the main heap view.
 snapshot.state.saving-census.full=Saving census…
 
+# LOCALIZATION NOTE (snapshot.state.saving-tree-map.full): The label describing
+# the snapshot state SAVING, used in the main heap view.
+snapshot.state.saving-tree-map.full=Saving tree map…
+
 # LOCALIZATION NOTE (snapshot.state.error.full): The label describing the
 # snapshot state ERROR, used in the main heap view.
 snapshot.state.error.full=There was an error processing this snapshot.
 
 # LOCALIZATION NOTE (snapshot.state.saving): The label describing the snapshot
 # state SAVING, used in the snapshot list view
 snapshot.state.saving=Saving snapshot…
 
@@ -287,16 +307,20 @@ snapshot.state.importing=Importing snapshot…
 # state READING, and SAVED, due to these states being combined visually, used in
 # the snapshot list view.
 snapshot.state.reading=Reading snapshot…
 
 # LOCALIZATION NOTE (snapshot.state.saving-census): The label describing the
 # snapshot state SAVING, used in snapshot list view.
 snapshot.state.saving-census=Saving census…
 
+# LOCALIZATION NOTE (snapshot.state.saving-census): The label describing the
+# snapshot state SAVING, used in snapshot list view.
+snapshot.state.saving-tree-map=Saving tree map…
+
 # LOCALIZATION NOTE (snapshot.state.error): The label describing the snapshot
 # state ERROR, used in the snapshot list view.
 snapshot.state.error=Error
 
 # LOCALIZATION NOTE (heapview.no-difference): Message displayed when there is no
 # difference between two snapshots.
 heapview.no-difference=No difference between the baseline and comparison.
 
@@ -379,8 +403,12 @@ heapview.field.name.tooltip=The name of 
 
 # LOCALIZATION NOTE (shortest-paths.header): The header label for the shortest
 # paths pane.
 shortest-paths.header=Retaining Paths from GC Roots
 
 # LOCALIZATION NOTE (shortest-paths.select-node): The message displayed in the
 # shortest paths pane when a node is not yet selected.
 shortest-paths.select-node=Select a node to view its retaining paths
+
+# LOCALIZATION NOTE (tree-map.node-count): The label for the count value of a
+# node in the tree map
+tree-map.node-count=count
--- a/devtools/client/memory/actions/diffing.js
+++ b/devtools/client/memory/actions/diffing.js
@@ -6,16 +6,18 @@
 const { assert, reportException } = require("devtools/shared/DevToolsUtils");
 const { actions, diffingState, viewState } = require("../constants");
 const telemetry = require("../telemetry");
 const {
   getSnapshot,
   censusIsUpToDate,
   snapshotIsDiffable
 } = require("../utils");
+// This is a circular dependency, so do not destructure the needed properties.
+const snapshotActions = require("./snapshot");
 
 /**
  * Toggle diffing mode on or off.
  */
 const toggleDiffing = exports.toggleDiffing = function () {
   return function(dispatch, getState) {
     dispatch({
       type: actions.CHANGE_VIEW,
--- a/devtools/client/memory/actions/io.js
+++ b/devtools/client/memory/actions/io.js
@@ -1,20 +1,26 @@
 /* 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 { immutableUpdate, reportException, assert } = require("devtools/shared/DevToolsUtils");
-const { snapshotState: states, actions } = require("../constants");
+const { snapshotState: states, actions, viewState } = require("../constants");
 const { L10N, openFilePicker, createSnapshot } = require("../utils");
 const telemetry = require("../telemetry");
-const { selectSnapshot, computeSnapshotData, readSnapshot } = require("./snapshot");
 const { OS } = require("resource://gre/modules/osfile.jsm");
-const VALID_EXPORT_STATES = [states.SAVED, states.READ, states.SAVING_CENSUS, states.SAVED_CENSUS];
+const {
+  selectSnapshot,
+  computeSnapshotData,
+  readSnapshot,
+  takeCensus,
+  takeTreeMap
+} = require("./snapshot");
+const VALID_EXPORT_STATES = [states.SAVED, states.READ];
 
 exports.pickFileAndExportSnapshot = function (snapshot) {
   return function* (dispatch, getState) {
     let outputFile = yield openFilePicker({
       title: L10N.getFormatStr("snapshot.io.save.window"),
       defaultName: OS.Path.basename(snapshot.path),
       filters: [[L10N.getFormatStr("snapshot.io.filter"), "*.fxsnapshot"]],
       mode: "save",
--- a/devtools/client/memory/actions/moz.build
+++ b/devtools/client/memory/actions/moz.build
@@ -8,10 +8,11 @@ DevToolsModules(
     'census-display.js',
     'diffing.js',
     'dominator-tree-display.js',
     'filter.js',
     'io.js',
     'refresh.js',
     'sizes.js',
     'snapshot.js',
+    'tree-map-display.js',
     'view.js',
 )
--- a/devtools/client/memory/actions/refresh.js
+++ b/devtools/client/memory/actions/refresh.js
@@ -24,13 +24,17 @@ exports.refresh = function (heapWorker) 
       case viewState.CENSUS:
         yield dispatch(snapshot.refreshSelectedCensus(heapWorker));
         return;
 
       case viewState.DOMINATOR_TREE:
         yield dispatch(snapshot.refreshSelectedDominatorTree(heapWorker));
         return;
 
+      case viewState.TREE_MAP:
+        yield dispatch(snapshot.refreshSelectedTreeMap(heapWorker));
+        return;
+
       default:
         assert(false, `Unexpected view state: ${getState().view}`);
     }
   };
 };
--- a/devtools/client/memory/actions/snapshot.js
+++ b/devtools/client/memory/actions/snapshot.js
@@ -4,18 +4,26 @@
 "use strict";
 
 const { assert, reportException } = require("devtools/shared/DevToolsUtils");
 const {
   censusIsUpToDate,
   getSnapshot,
   createSnapshot,
   dominatorTreeIsComputed,
+  canTakeCensus
 } = require("../utils");
-const { actions, snapshotState: states, viewState, dominatorTreeState } = require("../constants");
+const {
+  actions,
+  snapshotState: states,
+  viewState,
+  censusState,
+  treeMapState,
+  dominatorTreeState
+} = require("../constants");
 const telemetry = require("../telemetry");
 const view = require("./view");
 const refresh = require("./refresh");
 const diffing = require("./diffing");
 
 /**
  * A series of actions are fired from this task to save, read and generate the
  * initial census from a snapshot.
@@ -45,17 +53,20 @@ const takeSnapshotAndCensus = exports.ta
  * @param {snapshotId} id
  */
 const computeSnapshotData = exports.computeSnapshotData = function(heapWorker, id) {
   return function* (dispatch, getState) {
     if (getSnapshot(getState(), id).state !== states.READ) {
       return;
     }
 
-    yield dispatch(takeCensus(heapWorker, id));
+    // Decide which type of census to take.
+    const censusTaker = getCurrentCensusTaker(getState().view);
+    yield dispatch(censusTaker(heapWorker, id));
+
     if (getState().view === viewState.DOMINATOR_TREE) {
       yield dispatch(computeAndFetchDominatorTree(heapWorker, id));
     }
   };
 };
 
 /**
  * Selects a snapshot and if the snapshot's census is using a different
@@ -133,107 +144,200 @@ const readSnapshot = exports.readSnapsho
       return;
     }
 
     dispatch({ type: actions.READ_SNAPSHOT_END, id, creationTime });
   };
 };
 
 /**
- * @param {HeapAnalysesClient} heapWorker
- * @param {snapshotId} id
+ * Census and tree maps both require snapshots. This function shares the logic
+ * of creating snapshots, but is configurable with specific actions for the
+ * individual census types.
  *
- * @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
+ * @param {getDisplay} Get the display object from the state.
+ * @param {getCensus} Get the census from the snapshot.
+ * @param {beginAction} Action to send at the beginning of a heap snapshot.
+ * @param {endAction} Action to send at the end of a heap snapshot.
+ * @param {errorAction} Action to send if a snapshot has an error.
  */
-const takeCensus = exports.takeCensus = function (heapWorker, id) {
-  return function *(dispatch, getState) {
-    const snapshot = getSnapshot(getState(), id);
-    assert([states.READ, states.SAVED_CENSUS].includes(snapshot.state),
-      `Can only take census of snapshots in READ or SAVED_CENSUS state, found ${snapshot.state}`);
+function makeTakeCensusTask({ getDisplay, getFilter, getCensus, beginAction,
+                              endAction, errorAction }) {
+  /**
+   * @param {HeapAnalysesClient} heapWorker
+   * @param {snapshotId} id
+   *
+   * @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
+   */
+  return function (heapWorker, id) {
+    return function *(dispatch, getState) {
+      const snapshot = getSnapshot(getState(), id);
+      // Assert that snapshot is in a valid state
+
+      assert(canTakeCensus(snapshot),
+        `Attempting to take a census when the snapshot is not in a ready state.`);
+
+      let report, parentMap;
+      let display = getDisplay(getState());
+      let filter = getFilter(getState());
 
-    let report, parentMap;
-    let display = getState().censusDisplay;
-    let filter = getState().filter;
+      // If display, filter and inversion haven't changed, don't do anything.
+      if (censusIsUpToDate(filter, display, getCensus(snapshot))) {
+        return;
+      }
+
+      // Keep taking a census if the display changes while our request is in
+      // flight. Recheck that the display used for the census is the same as the
+      // state's display.
+      do {
+        display = getDisplay(getState());
+        filter = getState().filter;
+
+        dispatch({
+          type: beginAction,
+          id,
+          filter,
+          display
+        });
 
-    // If display, filter and inversion haven't changed, don't do anything.
-    if (censusIsUpToDate(filter, display, snapshot.census)) {
-      return;
-    }
+        let opts = display.inverted
+          ? { asInvertedTreeNode: true }
+          : { asTreeNode: true };
+
+        opts.filter = filter || null;
 
-    // Keep taking a census if the display changes while our request is in
-    // flight. Recheck that the display used for the census is the same as the
-    // state's display.
-    do {
-      display = getState().censusDisplay;
-      filter = getState().filter;
+        try {
+          ({ report, parentMap } = yield heapWorker.takeCensus(
+            snapshot.path,
+            { breakdown: display.breakdown },
+            opts));
+        } catch (error) {
+          reportException("takeCensus", error);
+          dispatch({ type: errorAction, id, error });
+          return;
+        }
+      }
+      while (filter !== getState().filter ||
+             display !== getDisplay(getState()));
 
       dispatch({
-        type: actions.TAKE_CENSUS_START,
+        type: endAction,
         id,
+        display,
         filter,
-        display
+        report,
+        parentMap
       });
 
-      let opts = display.inverted
-        ? { asInvertedTreeNode: true }
-        : { asTreeNode: true };
-      opts.filter = filter || null;
+      telemetry.countCensus({ filter, display });
+    };
+  };
+}
+
+/**
+ * Take a census.
+ */
+const takeCensus = exports.takeCensus = makeTakeCensusTask({
+  getDisplay: (state) => state.censusDisplay,
+  getFilter: (state) => state.filter,
+  getCensus: (snapshot) => snapshot.census,
+  beginAction: actions.TAKE_CENSUS_START,
+  endAction: actions.TAKE_CENSUS_END,
+  errorAction: actions.TAKE_CENSUS_ERROR
+});
 
-      try {
-        ({ report, parentMap } = yield heapWorker.takeCensus(
-          snapshot.path,
-          { breakdown: display.breakdown },
-          opts));
-      } catch (error) {
-        reportException("takeCensus", error);
-        dispatch({ type: actions.SNAPSHOT_ERROR, id, error });
-        return;
-      }
-    }
-    while (filter !== getState().filter ||
-           display !== getState().censusDisplay);
+/**
+ * Take a census for the treemap.
+ */
+const takeTreeMap = exports.takeTreeMap = makeTakeCensusTask({
+  getDisplay: (state) => state.treeMapDisplay,
+  getFilter: () => null,
+  getCensus: (snapshot) => snapshot.treeMap,
+  beginAction: actions.TAKE_TREE_MAP_START,
+  endAction: actions.TAKE_TREE_MAP_END,
+  errorAction: actions.TAKE_TREE_MAP_ERROR
+});
+
+/**
+ * Define what should be the default mode for taking a census based on the
+ * default view of the tool.
+ */
+const defaultCensusTaker = takeTreeMap;
 
-    dispatch({
-      type: actions.TAKE_CENSUS_END,
-      id,
-      display,
-      filter,
-      report,
-      parentMap
-    });
-
-    telemetry.countCensus({ filter, display });
-  };
+/**
+ * Pick the default census taker when taking a snapshot. This should be
+ * determined by the current view. If the view doesn't include a census, then
+ * use the default one defined above. Some census information is always needed
+ * to display some basic information about a snapshot.
+ *
+ * @param {string} value from viewState
+ */
+const getCurrentCensusTaker = exports.getCurrentCensusTaker = function (currentView) {
+  switch (currentView) {
+    case viewState.TREE_MAP:
+      return takeTreeMap;
+    break;
+    case viewState.CENSUS:
+      return takeCensus;
+    break;
+  }
+  return defaultCensusTaker;
 };
 
 /**
  * Refresh the selected snapshot's census data, if need be (for example,
  * display 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
+    // orchestrating them. For example, if the snapshot census's state is
+    // SAVING, 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) {
+    if (snapshot &&
+        (snapshot.census && snapshot.census.state === censusState.SAVED) ||
+        !snapshot.census) {
       yield dispatch(takeCensus(heapWorker, snapshot.id));
     }
   };
 };
 
 /**
+ * Refresh the selected snapshot's tree map data, if need be (for example,
+ * display configuration changed).
+ *
+ * @param {HeapAnalysesClient} heapWorker
+ */
+const refreshSelectedTreeMap = exports.refreshSelectedTreeMap = 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 census's state is
+    // SAVING, 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.treeMap && snapshot.treeMap.state === treeMapState.SAVED) ||
+        !snapshot.treeMap) {
+      yield dispatch(takeTreeMap(heapWorker, snapshot.id));
+    }
+  };
+};
+
+/**
  * Request that the `HeapAnalysesWorker` compute the dominator tree for the
  * snapshot with the given `id`.
  *
  * @param {HeapAnalysesClient} heapWorker
  * @param {SnapshotId} id
  *
  * @returns {Promise<DominatorTreeId>}
  */
@@ -391,28 +495,23 @@ const refreshSelectedDominatorTree = exp
 
     if (snapshot.dominatorTree &&
         !(snapshot.dominatorTree.state === dominatorTreeState.COMPUTED ||
           snapshot.dominatorTree.state === dominatorTreeState.LOADED ||
           snapshot.dominatorTree.state === dominatorTreeState.INCREMENTAL_FETCHING)) {
       return;
     }
 
-    switch (snapshot.state) {
-      case states.READ:
-      case states.SAVING_CENSUS:
-      case states.SAVED_CENSUS:
-        if (snapshot.dominatorTree) {
-          yield dispatch(fetchDominatorTree(heapWorker, snapshot.id));
-        } else {
-          yield dispatch(computeAndFetchDominatorTree(heapWorker, snapshot.id));
-        }
-        return;
-
-      default:
+    if (snapshot.state === states.READ) {
+      if (snapshot.dominatorTree) {
+        yield dispatch(fetchDominatorTree(heapWorker, snapshot.id));
+      } else {
+        yield dispatch(computeAndFetchDominatorTree(heapWorker, snapshot.id));
+      }
+    } else {
         // If there was an error, we can't continue. If we are still saving or
         // reading the snapshot, then takeSnapshotAndCensus will finish the job
         // for us.
         return;
     }
   };
 };
 
@@ -425,24 +524,29 @@ const refreshSelectedDominatorTree = exp
 const selectSnapshot = exports.selectSnapshot = function (id) {
   return {
     type: actions.SELECT_SNAPSHOT,
     id
   };
 };
 
 /**
- * Delete all snapshots that are in the SAVED_CENSUS or ERROR state
+ * Delete all snapshots that are in the READ or ERROR state
  *
  * @param {HeapAnalysesClient} heapWorker
  */
 const clearSnapshots = exports.clearSnapshots = function (heapWorker) {
   return function*(dispatch, getState) {
-    let snapshots = getState().snapshots.filter(
-      s => s.state === states.SAVED_CENSUS || s.state === states.ERROR);
+    let snapshots = getState().snapshots.filter(s => {
+      let snapshotReady = s.state === states.READ || s.state === states.ERROR;
+      let censusReady = (s.treeMap && s.treeMap.state === treeMapState.SAVED) ||
+                        (s.census && s.census.state === censusState.SAVED);
+
+      return snapshotReady && censusReady
+    });
 
     let ids = snapshots.map(s => s.id);
 
     dispatch({ type: actions.DELETE_SNAPSHOTS_START, ids });
 
     if (getState().diffing) {
       dispatch(diffing.toggleDiffing());
     }
new file mode 100644
--- /dev/null
+++ b/devtools/client/memory/actions/tree-map-display.js
@@ -0,0 +1,37 @@
+/* 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 { assert } = require("devtools/shared/DevToolsUtils");
+const { actions } = require("../constants");
+const { refresh } = require("./refresh");
+/**
+ * Sets the tree map display as the current display and refreshes the tree map
+ * census.
+ */
+exports.setTreeMapAndRefresh = function(heapWorker, display) {
+  return function*(dispatch, getState) {
+    dispatch(setTreeMap(display));
+    yield dispatch(refresh(heapWorker));
+  };
+};
+
+/**
+ * Clears out all cached census data in the snapshots and sets new display data
+ * for tree maps.
+ *
+ * @param {treeMapModel} display
+ */
+const setTreeMap = exports.setTreeMap = function(display) {
+  assert(typeof display === "object"
+         && display
+         && display.breakdown
+         && display.breakdown.by,
+    `Breakdowns must be an object with a \`by\` property, attempted to set: ${uneval(display)}`);
+
+  return {
+    type: actions.SET_TREE_MAP_DISPLAY,
+    display,
+  };
+};
--- a/devtools/client/memory/app.js
+++ b/devtools/client/memory/app.js
@@ -1,23 +1,26 @@
 /* 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 { appinfo } = require("Services");
 const { DOM: dom, createClass, createFactory, PropTypes } = require("devtools/client/shared/vendor/react");
 const { connect } = require("devtools/client/shared/vendor/react-redux");
-const { censusDisplays, dominatorTreeDisplays, diffingState, viewState } = require("./constants");
+const { censusDisplays, dominatorTreeDisplays, treeMapDisplays, diffingState, viewState } = require("./constants");
 const { toggleRecordingAllocationStacks } = require("./actions/allocations");
 const { setCensusDisplayAndRefresh } = require("./actions/census-display");
 const { setDominatorTreeDisplayAndRefresh } = require("./actions/dominator-tree-display");
+const { setTreeMapDisplayAndRefresh } = require("./actions/tree-map-display");
+
 const {
   getCustomCensusDisplays,
   getCustomDominatorTreeDisplays,
+  getCustomTreeMapDisplays,
 } = require("devtools/client/memory/utils");
 const {
   selectSnapshotForDiffingAndRefresh,
   toggleDiffing,
   expandDiffingCensusNode,
   collapseDiffingCensusNode,
   focusDiffingCensusNode,
 } = require("./actions/diffing");
@@ -122,16 +125,28 @@ const MemoryApp = createClass({
     }, []);
 
     return [
       dominatorTreeDisplays.coarseType,
       dominatorTreeDisplays.allocationStack,
     ].concat(custom);
   },
 
+  _getTreeMapDisplays() {
+    const customDisplays = getCustomTreeMapDisplays();
+    const custom = Object.keys(customDisplays).reduce((arr, key) => {
+      arr.push(customDisplays[key]);
+      return arr;
+    }, []);
+
+    return [
+      treeMapDisplays.coarseType
+    ].concat(custom);
+  },
+
   render() {
     let {
       dispatch,
       snapshots,
       front,
       heapWorker,
       allocations,
       toolbox,
@@ -168,16 +183,19 @@ const MemoryApp = createClass({
           setFilterString: filterString =>
             dispatch(setFilterStringAndRefresh(filterString, heapWorker)),
           diffing,
           onToggleDiffing: () => dispatch(toggleDiffing()),
           view,
           dominatorTreeDisplays: this._getDominatorTreeDisplays(),
           onDominatorTreeDisplayChange: newDisplay =>
             dispatch(setDominatorTreeDisplayAndRefresh(heapWorker, newDisplay)),
+          treeMapDisplays: this._getTreeMapDisplays(),
+          onTreeMapDisplayChange: newDisplay =>
+            dispatch(setTreeMapDisplayAndRefresh(heapWorker, newDisplay)),
           onViewChange: v => dispatch(changeViewAndRefresh(v, heapWorker)),
         }),
 
         dom.div(
           {
             id: "memory-tool-container"
           },
 
--- a/devtools/client/memory/components/heap.js
+++ b/devtools/client/memory/components/heap.js
@@ -3,41 +3,56 @@
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 const { DOM: dom, createClass, PropTypes, createFactory } = require("devtools/client/shared/vendor/react");
 const { assert, safeErrorString } = require("devtools/shared/DevToolsUtils");
 const Census = createFactory(require("./census"));
 const CensusHeader = createFactory(require("./census-header"));
 const DominatorTree = createFactory(require("./dominator-tree"));
 const DominatorTreeHeader = createFactory(require("./dominator-tree-header"));
+const TreeMap = createFactory(require("./tree-map"));
 const HSplitBox = createFactory(require("devtools/client/shared/components/h-split-box"));
 const ShortestPaths = createFactory(require("./shortest-paths"));
 const { getStatusTextFull, L10N } = require("../utils");
-const { snapshotState: states, diffingState, viewState, dominatorTreeState } = require("../constants");
+const {
+  snapshotState: states,
+  diffingState,
+  viewState,
+  censusState,
+  treeMapState,
+  dominatorTreeState
+} = require("../constants");
 const { snapshot: snapshotModel, diffingModel } = require("../models");
 
 /**
  * Get the app state's current state atom.
  *
  * @see the relevant state string constants in `../constants.js`.
  *
  * @param {viewState} view
  * @param {snapshotModel} snapshot
  * @param {diffingModel} diffing
  *
  * @return {snapshotState|diffingState|dominatorTreeState}
  */
 function getState(view, snapshot, diffing) {
   switch (view) {
     case viewState.CENSUS:
-      return snapshot.state;
+      return snapshot.census
+        ? snapshot.census.state
+        : snapshot.state;
 
     case viewState.DIFFING:
       return diffing.state;
 
+    case viewState.TREE_MAP:
+    return snapshot.treeMap
+      ? snapshot.treeMap.state
+      : snapshot.state;
+
     case viewState.DOMINATOR_TREE:
       return snapshot.dominatorTree
         ? snapshot.dominatorTree.state
         : snapshot.state;
   }
 
   assert(false, `Unexpected view state: ${view}`);
   return null;
@@ -54,18 +69,18 @@ function getState(view, snapshot, diffin
  * @returns {Boolean}
  */
 function shouldDisplayStatus(state, view, snapshot) {
   switch (state) {
     case states.IMPORTING:
     case states.SAVING:
     case states.SAVED:
     case states.READING:
-    case states.READ:
-    case states.SAVING_CENSUS:
+    case censusState.SAVING:
+    case treeMapState.SAVING:
     case diffingState.SELECTING:
     case diffingState.TAKING_DIFF:
     case dominatorTreeState.COMPUTING:
     case dominatorTreeState.COMPUTED:
     case dominatorTreeState.FETCHING:
       return true;
   }
   return view === viewState.DOMINATOR_TREE && !snapshot.dominatorTree;
@@ -105,25 +120,38 @@ function shouldDisplayThrobber(diffing) 
  * Get the current state's error, or return null if there is none.
  *
  * @param {snapshotModel} snapshot
  * @param {diffingModel} diffing
  *
  * @returns {Error|null}
  */
 function getError(snapshot, diffing) {
-  if (diffing && diffing.state === diffingState.ERROR) {
-    return diffing.error;
+  if (diffing) {
+    if (diffing.state === diffingState.ERROR) {
+      return diffing.error;
+    }
+    if (diffing.census === censusState.ERROR) {
+      return diffing.census.error;
+    }
   }
 
   if (snapshot) {
     if (snapshot.state === states.ERROR) {
       return snapshot.error;
     }
 
+    if (snapshot.census === censusState.ERROR) {
+      return snapshot.census.error;
+    }
+
+    if (snapshot.treeMap === treeMapState.ERROR) {
+      return snapshot.treeMap.error;
+    }
+
     if (snapshot.dominatorTree &&
         snapshot.dominatorTree.state === dominatorTreeState.ERROR) {
       return snapshot.dominatorTree.error;
     }
   }
 
   return null;
 }
@@ -180,19 +208,26 @@ const Heap = module.exports = createClas
     if (error) {
       return this._renderError(state, statusText, error);
     }
 
     if (view === viewState.CENSUS || view === viewState.DIFFING) {
       const census = view === viewState.CENSUS
         ? snapshot.census
         : diffing.census;
+      if (!census) {
+        return this._renderStatus(state, statusText, diffing);
+      }
       return this._renderCensus(state, census, diffing, onViewSourceInDebugger);
     }
 
+    if (view === viewState.TREE_MAP) {
+      return this._renderTreeMap(state, snapshot.treeMap);
+    }
+
     assert(view === viewState.DOMINATOR_TREE,
            "If we aren't in progress, looking at a census, or diffing, then we " +
            "must be looking at a dominator tree");
     assert(!diffing, "Should not have diffing");
     assert(snapshot.dominatorTree, "Should have a dominator tree");
 
     return this._renderDominatorTree(state, onViewSourceInDebugger, snapshot.dominatorTree,
                                      onLoadMoreSiblings);
@@ -283,16 +318,23 @@ const Heap = module.exports = createClas
       onExpand: node => this.props.onCensusExpand(census, node),
       onCollapse: node => this.props.onCensusCollapse(census, node),
       onFocus: node => this.props.onCensusFocus(census, node),
     }));
 
     return this._renderHeapView(state, ...contents);
   },
 
+  _renderTreeMap(state, treeMap) {
+    return this._renderHeapView(
+      state,
+      TreeMap({ treeMap })
+    );
+  },
+
   _renderDominatorTree(state, onViewSourceInDebugger, dominatorTree, onLoadMoreSiblings) {
     const tree = dom.div(
       {
         className: "vbox",
         style: {
           overflowY: "auto"
         }
       },
--- a/devtools/client/memory/components/moz.build
+++ b/devtools/client/memory/components/moz.build
@@ -1,18 +1,23 @@
 # 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/.
 
+DIRS += [
+    'tree-map',
+]
+
 DevToolsModules(
     'census-header.js',
     'census-tree-item.js',
     'census.js',
     'dominator-tree-header.js',
     'dominator-tree-item.js',
     'dominator-tree.js',
     'heap.js',
     'list.js',
     'shortest-paths.js',
     'snapshot-list-item.js',
     'toolbar.js',
+    'tree-map.js',
 )
--- a/devtools/client/memory/components/snapshot-list-item.js
+++ b/devtools/client/memory/components/snapshot-list-item.js
@@ -4,19 +4,25 @@
 
 const { assert } = require("devtools/shared/DevToolsUtils");
 const { DOM: dom, createClass, PropTypes } = require("devtools/client/shared/vendor/react");
 const {
   L10N,
   getSnapshotTitle,
   getSnapshotTotals,
   getStatusText,
-  snapshotIsDiffable
+  snapshotIsDiffable,
+  getSavedCensus
 } = require("../utils");
-const { snapshotState: states, diffingState } = require("../constants");
+const {
+  snapshotState: states,
+  diffingState,
+  censusState,
+  treeMapState
+} = require("../constants");
 const { snapshot: snapshotModel } = require("../models");
 
 const SnapshotListItem = module.exports = createClass({
   displayName: "SnapshotListItem",
 
   propTypes: {
     onClick: PropTypes.func.isRequired,
     onSave: PropTypes.func.isRequired,
@@ -56,24 +62,31 @@ const SnapshotListItem = module.exports 
       }
 
       if (selectedForDiffing || diffing.state == diffingState.SELECTING) {
         checkbox = dom.input(checkboxAttrs);
       }
     }
 
     let details;
-    if (!selectedForDiffing && snapshot.state === states.SAVED_CENSUS) {
-      let { bytes } = getSnapshotTotals(snapshot.census);
-      let formatBytes = L10N.getFormatStr("aggregate.mb", L10N.numberWithDecimals(bytes / 1000000, 2));
+    if (!selectedForDiffing) {
+      // See if a tree map or census is in the read state.
+      let census = getSavedCensus(snapshot);
 
-      details = dom.span({ className: "snapshot-totals" },
-        dom.span({ className: "total-bytes" }, formatBytes)
-      );
-    } else {
+      // If there is census data, fill in the total bytes.
+      if (census) {
+        let { bytes } = getSnapshotTotals(census);
+        let formatBytes = L10N.getFormatStr("aggregate.mb", L10N.numberWithDecimals(bytes / 1000000, 2));
+
+        details = dom.span({ className: "snapshot-totals" },
+          dom.span({ className: "total-bytes" }, formatBytes)
+        );
+      }
+    }
+    if (!details) {
       details = dom.span({ className: "snapshot-state" }, statusText);
     }
 
     let saveLink = !snapshot.path ? void 0 : dom.a({
       onClick: () => onSave(snapshot),
       className: "save",
     }, L10N.getFormatStr("snapshot.io.save"));
 
--- a/devtools/client/memory/components/toolbar.js
+++ b/devtools/client/memory/components/toolbar.js
@@ -26,28 +26,34 @@ module.exports = createClass({
     diffing: models.diffingModel,
     onToggleDiffing: PropTypes.func.isRequired,
     view: PropTypes.string.isRequired,
     onViewChange: PropTypes.func.isRequired,
     dominatorTreeDisplays: PropTypes.arrayOf(PropTypes.shape({
       displayName: PropTypes.string.isRequired,
     })).isRequired,
     onDominatorTreeDisplayChange: PropTypes.func.isRequired,
+    treeMapDisplays: PropTypes.arrayOf(PropTypes.shape({
+      displayName: PropTypes.string.isRequired,
+    })).isRequired,
+    onTreeMapDisplayChange: PropTypes.func.isRequired,
     snapshots: PropTypes.arrayOf(models.snapshot).isRequired,
   },
 
   render() {
     let {
       onTakeSnapshotClick,
       onImportClick,
       onClearSnapshotsClick,
       onCensusDisplayChange,
       censusDisplays,
       dominatorTreeDisplays,
       onDominatorTreeDisplayChange,
+      treeMapDisplays,
+      onTreeMapDisplayChange,
       onToggleRecordAllocationStacks,
       allocations,
       filterString,
       setFilterString,
       snapshots,
       diffing,
       onToggleDiffing,
       view,
@@ -95,16 +101,54 @@ module.exports = createClass({
           type: "search",
           className: "devtools-searchinput",
           placeholder: L10N.getStr("filter.placeholder"),
           title: L10N.getStr("filter.tooltip"),
           onChange: event => setFilterString(event.target.value),
           value: filterString || undefined,
         })
       );
+    } else if (view == viewState.TREE_MAP) {
+      assert(treeMapDisplays.length >= 1,
+       "Should always have at least one tree map display");
+
+      // Only show the dropdown if there are multiple display options
+      viewToolbarOptions = treeMapDisplays.length > 1
+        ? dom.div(
+            {
+              className: "toolbar-group"
+            },
+
+            dom.label(
+              {
+                className: "display-by",
+                title: L10N.getStr("toolbar.displayBy.tooltip"),
+              },
+              L10N.getStr("toolbar.displayBy"),
+              dom.select(
+                {
+                  id: "select-tree-map-display",
+                  onChange: e => {
+                    const newDisplay =
+                      treeMapDisplays.find(b => b.displayName === e.target.value);
+                    onTreeMapDisplayChange(newDisplay);
+                  },
+                },
+                treeMapDisplays.map(({ tooltip, displayName }) => dom.option(
+                  {
+                    key: `tree-map-display-${displayName}`,
+                    value: displayName,
+                    title: tooltip,
+                  },
+                  displayName
+                ))
+              )
+            )
+          )
+        : null;
     } else {
       assert(view === viewState.DOMINATOR_TREE);
 
       viewToolbarOptions = dom.div(
         {
           className: "toolbar-group"
         },
 
@@ -142,20 +186,28 @@ module.exports = createClass({
         {
           title: L10N.getStr("toolbar.view.tooltip"),
         },
         L10N.getStr("toolbar.view"),
         dom.select(
           {
             id: "select-view",
             onChange: e => onViewChange(e.target.value),
-            defaultValue: viewState.CENSUS,
+            defaultValue: view,
           },
           dom.option(
             {
+              value: viewState.TREE_MAP,
+              title: L10N.getStr("toolbar.view.treemap.tooltip"),
+              selected: view
+            },
+            L10N.getStr("toolbar.view.treemap")
+          ),
+          dom.option(
+            {
               value: viewState.CENSUS,
               title: L10N.getStr("toolbar.view.census.tooltip"),
             },
             L10N.getStr("toolbar.view.census")
           ),
           dom.option(
             {
               value: viewState.DOMINATOR_TREE,
new file mode 100644
--- /dev/null
+++ b/devtools/client/memory/components/tree-map.js
@@ -0,0 +1,71 @@
+/* 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 { DOM: dom, createClass } = require("devtools/client/shared/vendor/react");
+const { treeMapModel } = require("../models");
+const startVisualization = require("./tree-map/start");
+
+module.exports = createClass({
+  propTypes: {
+    treeMap: treeMapModel
+  },
+
+  displayName: "TreeMap",
+
+  getInitialState() {
+    return {};
+  },
+
+  componentDidMount() {
+    const { treeMap } = this.props;
+    if (treeMap && treeMap.report) {
+      this._startVisualization();
+    }
+  },
+
+  shouldComponentUpdate(nextProps) {
+    const oldTreeMap = this.props.treeMap;
+    const newTreeMap = nextProps.treeMap;
+    return oldTreeMap !== newTreeMap;
+  },
+
+  componentDidUpdate(prevProps) {
+    this._stopVisualization();
+
+    if (this.props.treeMap && this.props.treeMap.report) {
+      this._startVisualization();
+    }
+  },
+
+  componentWillUnmount() {
+    if (this.state.stopVisualization) {
+      this.state.stopVisualization();
+    }
+  },
+
+  _stopVisualization() {
+    if (this.state.stopVisualization) {
+      this.state.stopVisualization();
+      this.setState({ stopVisualization: null });
+    }
+  },
+
+  _startVisualization() {
+    const { container } = this.refs;
+    const { report } = this.props.treeMap;
+    const stopVisualization = startVisualization(container, report);
+    this.setState({ stopVisualization });
+  },
+
+  render() {
+    return dom.div(
+      {
+        ref: "container",
+        className: "tree-map-container"
+      }
+    );
+  }
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/memory/components/tree-map/canvas-utils.js
@@ -0,0 +1,134 @@
+/* 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/. */
+
+/* eslint-env browser */
+
+"use strict";
+
+/**
+ * Create 2 canvases and contexts for drawing onto, 1 main canvas, and 1 zoom
+ * canvas. The main canvas dimensions match the parent div, but the CSS can be
+ * transformed to be zoomed and dragged around (potentially creating a blurry
+ * canvas once zoomed in). The zoom canvas is a zoomed in section that matches
+ * the parent div's dimensions and is kept in place through CSS. A zoomed in
+ * view of the visualization is drawn onto this canvas, providing a crisp zoomed
+ * in view of the tree map.
+ */
+const { debounce } = require("sdk/lang/functional");
+const EventEmitter = require("devtools/shared/event-emitter");
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const FULLSCREEN_STYLE = {
+  width: "100%",
+  height: "100%",
+  position: "absolute",
+};
+
+/**
+ * Create the canvases, resize handlers, and return references to them all
+ *
+ * @param  {HTMLDivElement} parentEl
+ * @param  {Number} debounceRate
+ * @return {Object}
+ */
+function Canvases(parentEl, debounceRate) {
+  EventEmitter.decorate(this)
+  this.container = createContainingDiv(parentEl);
+
+  // This canvas contains all of the treemap
+  this.main = createCanvas(this.container, "main");
+  // This canvas contains only the zoomed in portion, overlaying the main canvas
+  this.zoom = createCanvas(this.container, "zoom");
+
+  this.removeHandlers = handleResizes(this, debounceRate);
+}
+
+Canvases.prototype = {
+
+  /**
+   * Remove the handlers and elements
+   *
+   * @return {type}  description
+   */
+  destroy : function() {
+    this.removeHandlers();
+    this.container.removeChild(this.main.canvas);
+    this.container.removeChild(this.zoom.canvas);
+  }
+}
+
+module.exports = Canvases;
+
+/**
+ * Create the containing div
+ *
+ * @param  {HTMLDivElement} parentEl
+ * @return {HTMLDivElement}
+ */
+function createContainingDiv(parentEl) {
+  let div = parentEl.ownerDocument.createElementNS(HTML_NS, "div");
+  Object.assign(div.style, FULLSCREEN_STYLE);
+  parentEl.appendChild(div);
+  return div;
+}
+
+/**
+ * Create a canvas and context
+ *
+ * @param  {HTMLDivElement} container
+ * @param  {String} className
+ * @return {Object} { canvas, ctx }
+ */
+function createCanvas(container, className) {
+  let window = container.ownerDocument.defaultView;
+  let canvas = container.ownerDocument.createElementNS(HTML_NS, "canvas");
+  container.appendChild(canvas);
+  canvas.width = container.offsetWidth * window.devicePixelRatio;
+  canvas.height = container.offsetHeight * window.devicePixelRatio;
+  canvas.className = className;
+
+  Object.assign(canvas.style, FULLSCREEN_STYLE, {
+    pointerEvents: "none"
+  });
+
+  let ctx = canvas.getContext("2d");
+
+  return { canvas, ctx };
+}
+
+/**
+ * Resize the canvases' resolutions, and fires out the onResize callback
+ *
+ * @param  {HTMLDivElement} container
+ * @param  {Object} canvases
+ * @param  {Number} debounceRate
+ */
+function handleResizes(canvases, debounceRate) {
+  let { container, main, zoom } = canvases;
+  let window = container.ownerDocument.defaultView;
+
+  function resize() {
+    let width = container.offsetWidth * window.devicePixelRatio;
+    let height = container.offsetHeight * window.devicePixelRatio;
+
+    main.canvas.width = width;
+    main.canvas.height = height;
+    zoom.canvas.width = width;
+    zoom.canvas.height = height;
+
+    canvases.emit('resize');
+  }
+
+  // Tests may not need debouncing
+  let debouncedResize = debounceRate > 0
+    ? debounce(resize, debounceRate)
+    : resize;
+
+  window.addEventListener("resize", debouncedResize, false);
+  resize();
+
+  return function removeResizeHandlers() {
+    window.removeEventListener("resize", debouncedResize, false);
+  };
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/memory/components/tree-map/color-coarse-type.js
@@ -0,0 +1,70 @@
+/* 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";
+
+/**
+ * Color the boxes in the treemap
+ */
+
+const TYPES = [ "objects", "other", "strings", "scripts" ];
+
+// The factors determine how much the hue shifts
+const TYPE_FACTOR = TYPES.length * 3;
+const DEPTH_FACTOR = -10;
+const H = 0.5;
+const S = 0.6;
+const L = 0.9;
+
+/**
+ * Recursively find the index of the coarse type of a node
+ *
+ * @param  {Object} node
+ *         d3 treemap
+ * @return {Integer}
+ *         index
+ */
+function findCoarseTypeIndex(node) {
+  let index = TYPES.indexOf(node.name);
+
+  if (node.parent) {
+    return index === -1 ? findCoarseTypeIndex(node.parent) : index;
+  }
+
+  return TYPES.indexOf("other");
+}
+
+/**
+ * Decide a color value for depth to be used in the HSL computation
+ *
+ * @param  {Object} node
+ * @return {Number}
+ */
+function depthColorFactor(node) {
+  return Math.min(1, node.depth / DEPTH_FACTOR);
+}
+
+/**
+ * Decide a color value for type to be used in the HSL computation
+ *
+ * @param  {Object} node
+ * @return {Number}
+ */
+function typeColorFactor(node) {
+  return findCoarseTypeIndex(node) / TYPE_FACTOR;
+}
+
+/**
+ * Color a node
+ *
+ * @param  {Object} node
+ * @return {Array} HSL values ranged 0-1
+ */
+module.exports = function colorCoarseType(node) {
+  let h = Math.min(1, H + typeColorFactor(node));
+  let s = Math.min(1, S);
+  let l = Math.min(1, L + depthColorFactor(node));
+
+  return [h, s, l];
+};
new file mode 100644
--- /dev/null
+++ b/devtools/client/memory/components/tree-map/drag-zoom.js
@@ -0,0 +1,330 @@
+/* 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 { debounce } = require("sdk/lang/functional");
+const { lerp } = require("devtools/client/memory/utils");
+const EventEmitter = require("devtools/shared/event-emitter");
+
+const LERP_SPEED = 0.5;
+const ZOOM_SPEED = 0.01;
+const TRANSLATE_EPSILON = 1;
+const ZOOM_EPSILON = 0.001;
+const LINE_SCROLL_MODE = 1;
+const SCROLL_LINE_SIZE = 15;
+
+/**
+ * DragZoom is a constructor that contains the state of the current dragging and
+ * zooming behavior. It sets the scrolling and zooming behaviors.
+ *
+ * @param  {HTMLElement} container description
+ *         The container for the canvases
+ */
+function DragZoom(container, debounceRate, requestAnimationFrame) {
+  EventEmitter.decorate(this);
+
+  this.isDragging = false;
+
+  // The current mouse position
+  this.mouseX = container.offsetWidth / 2;
+  this.mouseY = container.offsetHeight / 2;
+
+  // The total size of the visualization after being zoomed, in pixels
+  this.zoomedWidth = container.offsetWidth;
+  this.zoomedHeight = container.offsetHeight;
+
+  // How much the visualization has been zoomed in
+  this.zoom = 0;
+
+  // The offset of visualization from the container. This is applied after
+  // the zoom, and the visualization by default is centered
+  this.translateX = 0;
+  this.translateY = 0;
+
+  // The size of the offset between the top/left of the container, and the
+  // top/left of the containing element. This value takes into account
+  // the devicePixelRatio for canvas draws.
+  this.offsetX = 0;
+  this.offsetY = 0;
+
+  // The smoothed values that are animated and eventually match the target
+  // values. The values are updated by the update loop
+  this.smoothZoom = 0;
+  this.smoothTranslateX = 0;
+  this.smoothTranslateY = 0;
+
+  // Add the constant values for testing purposes
+  this.ZOOM_SPEED = ZOOM_SPEED;
+  this.ZOOM_EPSILON = ZOOM_EPSILON;
+
+  let update = createUpdateLoop(container, this, requestAnimationFrame);
+
+  this.destroy = setHandlers(this, container, update, debounceRate);
+}
+
+module.exports = DragZoom;
+
+/**
+ * Returns an update loop. This loop smoothly updates the visualization when
+ * actions are performed. Once the animations have reached their target values
+ * the animation loop is stopped.
+ *
+ * Any value in the `dragZoom` object that starts with "smooth" is the
+ * smoothed version of a value that is interpolating toward the target value.
+ * For instance `dragZoom.smoothZoom` approaches `dragZoom.zoom` on each
+ * iteration of the update loop until it's sufficiently close as defined by
+ * the epsilon values.
+ *
+ * Only these smoothed values and the container CSS are updated by the loop.
+ *
+ * @param {HTMLDivElement} container
+ * @param {Object} dragZoom
+ *        The values that represent the current dragZoom state
+ * @param {Function} requestAnimationFrame
+ */
+function createUpdateLoop(container, dragZoom, requestAnimationFrame) {
+  let isLooping = false;
+
+  function update() {
+    let isScrollChanging = (
+      Math.abs(dragZoom.smoothZoom - dragZoom.zoom) > ZOOM_EPSILON
+    );
+    let isTranslateChanging = (
+      Math.abs(dragZoom.smoothTranslateX - dragZoom.translateX)
+      > TRANSLATE_EPSILON ||
+      Math.abs(dragZoom.smoothTranslateY - dragZoom.translateY)
+      > TRANSLATE_EPSILON
+    );
+
+    isLooping = isScrollChanging || isTranslateChanging;
+
+    if (isScrollChanging) {
+      dragZoom.smoothZoom = lerp(dragZoom.smoothZoom, dragZoom.zoom,
+                                 LERP_SPEED);
+    } else {
+      dragZoom.smoothZoom = dragZoom.zoom;
+    }
+
+    if (isTranslateChanging) {
+      dragZoom.smoothTranslateX = lerp(dragZoom.smoothTranslateX,
+                                       dragZoom.translateX, LERP_SPEED);
+      dragZoom.smoothTranslateY = lerp(dragZoom.smoothTranslateY,
+                                       dragZoom.translateY, LERP_SPEED);
+    } else {
+      dragZoom.smoothTranslateX = dragZoom.translateX;
+      dragZoom.smoothTranslateY = dragZoom.translateY;
+    }
+
+    let zoom = 1 + dragZoom.smoothZoom;
+    let x = dragZoom.smoothTranslateX;
+    let y = dragZoom.smoothTranslateY;
+    container.style.transform = `translate(${x}px, ${y}px) scale(${zoom})`;
+
+    if (isLooping) {
+      requestAnimationFrame(update);
+    }
+  }
+
+  // Go ahead and start the update loop
+  update();
+
+  return function restartLoopingIfStopped() {
+    if (!isLooping) {
+      update();
+    }
+  };
+}
+
+/**
+ * Set the various event listeners and return a function to remove them
+ *
+ * @param  {Object} dragZoom
+ * @param  {HTMLElement} container
+ * @param  {Function} update
+ * @return {Function}  The function to remove the handlers
+ */
+function setHandlers(dragZoom, container, update, debounceRate) {
+  let emitChanged = debounce(() => dragZoom.emit("change"), debounceRate);
+
+  let removeDragHandlers =
+    setDragHandlers(container, dragZoom, emitChanged, update);
+  let removeScrollHandlers =
+    setScrollHandlers(container, dragZoom, emitChanged, update);
+
+  return function removeHandlers() {
+    removeDragHandlers();
+    removeScrollHandlers();
+  };
+}
+
+/**
+ * Sets handlers for when the user drags on the canvas. It will update dragZoom
+ * object with new translate and offset values.
+ *
+ * @param  {HTMLElement} container
+ * @param  {Object} dragZoom
+ * @param  {Function} changed
+ * @param  {Function} update
+ */
+function setDragHandlers(container, dragZoom, emitChanged, update) {
+  let parentEl = container.parentElement;
+
+  function startDrag() {
+    dragZoom.isDragging = true;
+    container.style.cursor = "grabbing";
+  }
+
+  function stopDrag() {
+    dragZoom.isDragging = false;
+    container.style.cursor = "grab";
+  }
+
+  function drag(event) {
+    let prevMouseX = dragZoom.mouseX;
+    let prevMouseY = dragZoom.mouseY;
+
+    dragZoom.mouseX = event.clientX - parentEl.offsetLeft;
+    dragZoom.mouseY = event.clientY - parentEl.offsetTop;
+
+    if (!dragZoom.isDragging) {
+      return;
+    }
+
+    dragZoom.translateX += dragZoom.mouseX - prevMouseX;
+    dragZoom.translateY += dragZoom.mouseY - prevMouseY;
+
+    keepInView(container, dragZoom);
+
+    emitChanged();
+    update();
+  }
+
+  parentEl.addEventListener("mousedown", startDrag, false);
+  parentEl.addEventListener("mouseup", stopDrag, false);
+  parentEl.addEventListener("mouseout", stopDrag, false);
+  parentEl.addEventListener("mousemove", drag, false);
+
+  return function removeListeners() {
+    parentEl.removeEventListener("mousedown", startDrag, false);
+    parentEl.removeEventListener("mouseup", stopDrag, false);
+    parentEl.removeEventListener("mouseout", stopDrag, false);
+    parentEl.removeEventListener("mousemove", drag, false);
+  };
+}
+
+/**
+ * Sets the handlers for when the user scrolls. It updates the dragZoom object
+ * and keeps the canvases all within the view. After changing values update
+ * loop is called, and the changed event is emitted.
+ *
+ * @param  {HTMLDivElement} container
+ * @param  {Object} dragZoom
+ * @param  {Function} changed
+ * @param  {Function} update
+ */
+function setScrollHandlers(container, dragZoom, emitChanged, update) {
+  let window = container.ownerDocument.defaultView;
+
+  function handleWheel(event) {
+    event.preventDefault();
+
+    if (dragZoom.isDragging) {
+      return;
+    }
+
+    // Update the zoom level
+    let scrollDelta = getScrollDelta(event, window);
+    let prevZoom = dragZoom.zoom;
+    dragZoom.zoom = Math.max(0, dragZoom.zoom - scrollDelta * ZOOM_SPEED);
+    let deltaZoom = dragZoom.zoom - prevZoom;
+
+    // Calculate the updated width and height
+    let prevWidth = container.offsetWidth * (1 + prevZoom);
+    let prevHeight = container.offsetHeight * (1 + prevZoom);
+    dragZoom.zoomedWidth = container.offsetWidth * (1 + dragZoom.zoom);
+    dragZoom.height = container.offsetHeight * (1 + dragZoom.zoom);
+    let deltaWidth = dragZoom.zoomedWidth - prevWidth;
+    let deltaHeight = dragZoom.height - prevHeight;
+
+    // The ratio of where the center of the zoom is in regards to the total
+    // zoomed width/height
+    let ratioZoomX = (dragZoom.zoomedWidth / 2 - dragZoom.translateX)
+      / dragZoom.zoomedWidth;
+    let ratioZoomY = (dragZoom.height / 2 - dragZoom.translateY)
+      / dragZoom.height;
+
+    // Distribute the change in width and height based on the above ratio
+    dragZoom.translateX -= lerp(-deltaWidth / 2, deltaWidth / 2, ratioZoomX);
+    dragZoom.translateY -= lerp(-deltaHeight / 2, deltaHeight / 2, ratioZoomY);
+
+    // The ratio of mouse position to total zoomeed width/height, ranged [-1, 1]
+    let mouseRatioX, mouseRatioY;
+    if (deltaZoom > 0) {
+      // Zoom in towards the mouse
+      mouseRatioX = 2 * (dragZoom.mouseX - container.offsetWidth / 2)
+        / dragZoom.zoomedWidth;
+      mouseRatioY = 2 * (dragZoom.mouseY - container.offsetHeight / 2)
+        / dragZoom.height;
+    } else {
+      // Zoom out centering the screen
+      mouseRatioX = 0;
+      mouseRatioY = 0;
+    }
+    // Adjust the translate to zoom towards the mouse
+    dragZoom.translateX -= deltaWidth * mouseRatioX;
+    dragZoom.translateY -= deltaHeight * mouseRatioY;
+
+    // Keep the canvas in range of the container
+    keepInView(container, dragZoom);
+    emitChanged();
+    update();
+  }
+
+  window.addEventListener("wheel", handleWheel, false);
+
+  return function removeListener() {
+    window.removeEventListener("wheel", handleWheel, false);
+  };
+}
+
+/**
+ * Account for the various mouse wheel event types, per pixel or per line
+ *
+ * @param  {WheelEvent} event
+ * @param  {Window} window
+ * @return {Number} The scroll size in pixels
+ */
+function getScrollDelta(event, window) {
+  if (event.deltaMode === LINE_SCROLL_MODE) {
+    // Update by a fixed arbitrary value to normalize scroll types
+    return event.deltaY * SCROLL_LINE_SIZE;
+  }
+  return event.deltaY;
+}
+
+/**
+ * Keep the dragging and zooming within the view by updating the values in the
+ * `dragZoom` object.
+ *
+ * @param  {HTMLDivElement} container
+ * @param  {Object} dragZoom
+ */
+function keepInView(container, dragZoom) {
+  let { devicePixelRatio } = container.ownerDocument.defaultView;
+  let overdrawX = (dragZoom.zoomedWidth - container.offsetWidth) / 2;
+  let overdrawY = (dragZoom.height - container.offsetHeight) / 2;
+
+  dragZoom.translateX = Math.max(-overdrawX,
+                                 Math.min(overdrawX, dragZoom.translateX));
+  dragZoom.translateY = Math.max(-overdrawY,
+                                 Math.min(overdrawY, dragZoom.translateY));
+
+  dragZoom.offsetX = devicePixelRatio * (
+    (dragZoom.zoomedWidth - container.offsetWidth) / 2 - dragZoom.translateX
+  );
+  dragZoom.offsetY = devicePixelRatio * (
+    (dragZoom.height - container.offsetHeight) / 2 - dragZoom.translateY
+  );
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/memory/components/tree-map/draw.js
@@ -0,0 +1,285 @@
+/* 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";
+/**
+ * Draw the treemap into the provided canvases using the 2d context. The treemap
+ * layout is computed with d3. There are 2 canvases provided, each matching
+ * the resolution of the window. The main canvas is a fully drawn version of
+ * the treemap that is positioned and zoomed using css. It gets blurry the more
+ * you zoom in as it doesn't get redrawn when zooming. The zoom canvas is
+ * repositioned absolutely after every change in the dragZoom object, and then
+ * redrawn to provide a full-resolution (non-blurry) view of zoomed in segment
+ * of the treemap.
+ */
+
+const colorCoarseType = require("./color-coarse-type");
+const {
+  hslToStyle,
+  formatAbbreviatedBytes,
+  L10N
+} = require("devtools/client/memory/utils");
+
+// A constant fully zoomed out dragZoom object for the main canvas
+const NO_SCROLL = {
+  translateX: 0,
+  translateY: 0,
+  zoom: 0,
+  offsetX: 0,
+  offsetY: 0
+};
+
+// Drawing constants
+const ELLIPSIS = "...";
+const TEXT_MARGIN = 2;
+const TEXT_COLOR = "#000000";
+const TEXT_LIGHT_COLOR = "rgba(0,0,0,0.5)";
+const LINE_WIDTH = 1;
+const FONT_SIZE = 10;
+const FONT_LINE_HEIGHT = 2;
+const PADDING = [5, 5, 5, 5];
+const COUNT_LABEL = L10N.getStr("tree-map.node-count");
+
+/**
+ * Setup and start drawing the treemap visualization
+ *
+ * @param  {Object} report
+ * @param  {Object} canvases
+ *         A CanvasUtils object that contains references to the main and zoom
+ *         canvases and contexts
+ * @param  {Object} dragZoom
+ *         A DragZoom object representing the current state of the dragging
+ *         and zooming behavior
+ */
+exports.setupDraw = function(report, canvases, dragZoom) {
+  let getTreemap = configureD3Treemap.bind(null, canvases.main.canvas);
+
+  let treemap, nodes;
+
+  function drawFullTreemap() {
+    treemap = getTreemap();
+    nodes = treemap(report);
+    drawTreemap(canvases.main, nodes, NO_SCROLL);
+    drawTreemap(canvases.zoom, nodes, dragZoom);
+  }
+
+  function drawZoomedTreemap() {
+    drawTreemap(canvases.zoom, nodes, dragZoom);
+    positionZoomedCanvas(canvases.zoom.canvas, dragZoom);
+  }
+
+  drawFullTreemap();
+  canvases.on("resize", drawFullTreemap);
+  dragZoom.on("change", drawZoomedTreemap);
+};
+
+/**
+ * Returns a configured d3 treemap function
+ *
+ * @param  {HTMLCanvasElement} canvas
+ * @return {Function}
+ */
+const configureD3Treemap = exports.configureD3Treemap = function(canvas) {
+  let window = canvas.ownerDocument.defaultView;
+  let ratio = window.devicePixelRatio;
+  let treemap = window.d3.layout.treemap()
+    .size([canvas.width, canvas.height])
+    .sticky(true)
+    .padding([
+      (PADDING[0] + FONT_SIZE) * ratio,
+      PADDING[1] * ratio,
+      PADDING[2] * ratio,
+      PADDING[3] * ratio,
+    ])
+    .value(d => d.bytes);
+
+  /**
+   * Create treemap nodes from a census report that are sorted by depth
+   *
+   * @param  {Object} report
+   * @return {Array} An array of d3 treemap nodes
+   *         // https://github.com/mbostock/d3/wiki/Treemap-Layout
+   *         parent - the parent node, or null for the root.
+   *         children - the array of child nodes, or null for leaf nodes.
+   *         value - the node value, as returned by the value accessor.
+   *         depth - the depth of the node, starting at 0 for the root.
+   *         area - the computed pixel area of this node.
+   *         x - the minimum x-coordinate of the node position.
+   *         y - the minimum y-coordinate of the node position.
+   *         z - the orientation of this cell’s subdivision, if any.
+   *         dx - the x-extent of the node position.
+   *         dy - the y-extent of the node position.
+   */
+  return function depthSortedNodes(report) {
+    let nodes = treemap(report);
+    nodes.sort((a, b) => a.depth - b.depth);
+    return nodes;
+  };
+};
+
+/**
+ * Draw the text, cut it in half every time it doesn't fit until it fits or
+ * it's smaller than the "..." text.
+ *
+ * @param  {CanvasRenderingContext2D} ctx
+ * @param  {Number} x
+ *         the position of the text
+ * @param  {Number} y
+ *         the position of the text
+ * @param  {Number} innerWidth
+ *         the inner width of the containing treemap cell
+ * @param  {Text} name
+ */
+const drawTruncatedName = exports.drawTruncatedName = function(ctx, x, y,
+                                                               innerWidth,
+                                                               name) {
+  let truncated = name.substr(0, Math.floor(name.length / 2));
+  let formatted = truncated + ELLIPSIS;
+
+  if (ctx.measureText(formatted).width > innerWidth) {
+    drawTruncatedName(ctx, x, y, innerWidth, truncated);
+  } else {
+    ctx.fillText(formatted, x, y);
+  }
+};
+
+/**
+ * Fit and draw the text in a node with the following strategies to shrink
+ * down the text size:
+ *
+ * Function 608KB 9083 count
+ * Function
+ * Func...
+ * Fu...
+ * ...
+ *
+ * @param  {CanvasRenderingContext2D} ctx
+ * @param  {Object} node
+ * @param  {Number} borderWidth
+ * @param  {Number} ratio
+ * @param  {Object} dragZoom
+ */
+const drawText = exports.drawText = function(ctx, node, borderWidth, ratio,
+                                              dragZoom) {
+  let { dx, dy, name, totalBytes, totalCount } = node;
+  let scale = dragZoom.zoom + 1;
+  dx *= scale;
+  dy *= scale;
+
+  // Start checking to see how much text we can fit in, optimizing for the
+  // common case of lots of small leaf nodes
+  if (FONT_SIZE * FONT_LINE_HEIGHT < dy) {
+    let margin = borderWidth(node) * 1.5 + ratio * TEXT_MARGIN;
+    let x = margin + node.x * scale - dragZoom.offsetX;
+    let y = margin + node.y * scale - dragZoom.offsetY;
+    let innerWidth = dx - margin * 2;
+    let nameSize = ctx.measureText(name).width;
+
+    if (ctx.measureText(ELLIPSIS).width > innerWidth) {
+      return;
+    }
+
+    ctx.fillStyle = TEXT_COLOR;
+
+    if (nameSize > innerWidth) {
+      // The name is too long - halve the name as an expediant way to shorten it
+      drawTruncatedName(ctx, x, y, innerWidth, name);
+    } else {
+      let bytesFormatted = formatAbbreviatedBytes(totalBytes);
+      let countFormatted = `${totalCount} ${COUNT_LABEL}`;
+      let byteSize = ctx.measureText(bytesFormatted).width;
+      let countSize = ctx.measureText(countFormatted).width;
+      let spaceSize = ctx.measureText(" ").width;
+
+      if (nameSize + byteSize + countSize + spaceSize * 3 > innerWidth) {
+        // The full name will fit
+        ctx.fillText(`${name}`, x, y);
+      } else {
+        // The full name plus the byte information will fit
+        ctx.fillText(name, x, y);
+        ctx.fillStyle = TEXT_LIGHT_COLOR;
+        ctx.fillText(`${bytesFormatted} ${countFormatted}`,
+          x + nameSize + spaceSize, y);
+      }
+    }
+  }
+};
+
+/**
+ * Draw a box given a node
+ *
+ * @param  {CanvasRenderingContext2D} ctx
+ * @param  {Object} node
+ * @param  {Number} borderWidth
+ * @param  {Number} ratio
+ * @param  {Object} dragZoom
+ */
+const drawBox = exports.drawBox = function(ctx, node, borderWidth, dragZoom) {
+  let border = borderWidth(node);
+  let fillHSL = colorCoarseType(node);
+  let strokeHSL = [fillHSL[0], fillHSL[1], fillHSL[2] * 0.5];
+  let scale = 1 + dragZoom.zoom;
+
+  // Offset the draw so that box strokes don't overlap
+  let x = scale * node.x - dragZoom.offsetX + border / 2;
+  let y = scale * node.y - dragZoom.offsetY + border / 2;
+  let dx = scale * node.dx - border;
+  let dy = scale * node.dy - border;
+
+  ctx.fillStyle = hslToStyle(...fillHSL);
+  ctx.fillRect(x, y, dx, dy);
+
+  ctx.strokeStyle = hslToStyle(...strokeHSL);
+  ctx.lineWidth = border;
+  ctx.strokeRect(x, y, dx, dy);
+};
+
+/**
+ * Draw the overall treemap
+ *
+ * @param  {HTMLCanvasElement} canvas
+ * @param  {CanvasRenderingContext2D} ctx
+ * @param  {Array} nodes
+ * @param  {Objbect} dragZoom
+ */
+const drawTreemap = exports.drawTreemap = function({canvas, ctx}, nodes,
+                                                   dragZoom) {
+  let window = canvas.ownerDocument.defaultView;
+  let ratio = window.devicePixelRatio;
+  let canvasArea = canvas.width * canvas.height;
+  ctx.clearRect(0, 0, canvas.width, canvas.height);
+  ctx.font = `${FONT_SIZE * ratio}px sans-serif`;
+  ctx.textBaseline = "top";
+
+  function borderWidth(node) {
+    let areaRatio = Math.sqrt(node.area / canvasArea);
+    return ratio * Math.max(1, LINE_WIDTH * areaRatio);
+  }
+
+  for (let i = 0; i < nodes.length; i++) {
+    let node = nodes[i];
+    if (node.parent === undefined) {
+      continue;
+    }
+
+    drawBox(ctx, node, borderWidth, dragZoom);
+    drawText(ctx, node, borderWidth, ratio, dragZoom);
+  }
+};
+
+/**
+ * Set the position of the zoomed in canvas. It always take up 100% of the view
+ * window, but is transformed relative to the zoomed in containing element,
+ * essentially reversing the transform of the containing element.
+ *
+ * @param  {HTMLCanvasElement} canvas
+ * @param  {Object} dragZoom
+ */
+const positionZoomedCanvas = function(canvas, dragZoom) {
+  let scale = 1 / (1 + dragZoom.zoom);
+  let x = -dragZoom.translateX;
+  let y = -dragZoom.translateY;
+  canvas.style.transform = `scale(${scale}) translate(${x}px, ${y}px)`;
+};
+
+exports.positionZoomedCanvas = positionZoomedCanvas;
new file mode 100644
--- /dev/null
+++ b/devtools/client/memory/components/tree-map/moz.build
@@ -0,0 +1,12 @@
+# 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(
+    'canvas-utils.js',
+    'color-coarse-type.js',
+    'drag-zoom.js',
+    'draw.js',
+    'start.js',
+)
new file mode 100644
--- /dev/null
+++ b/devtools/client/memory/components/tree-map/start.js
@@ -0,0 +1,32 @@
+/* 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 { setupDraw } = require("./draw");
+const DragZoom = require("./drag-zoom");
+const CanvasUtils = require("./canvas-utils");
+
+/**
+ * Start the tree map visualization
+ *
+ * @param  {HTMLDivElement} container
+ * @param  {Object} report
+ *                  the report from a census
+ * @param  {Number} debounceRate
+ */
+module.exports = function startVisualization(parentEl, report,
+                                              debounceRate = 100) {
+  let window = parentEl.ownerDocument.defaultView;
+  let canvases = new CanvasUtils(parentEl, debounceRate);
+  let dragZoom = new DragZoom(canvases.container, debounceRate,
+                              window.requestAnimationFrame);
+
+  setupDraw(report, canvases, dragZoom);
+
+  return function stopVisualization() {
+    canvases.destroy();
+    dragZoom.destroy();
+  };
+};
--- a/devtools/client/memory/constants.js
+++ b/devtools/client/memory/constants.js
@@ -25,16 +25,22 @@ actions.TAKE_SNAPSHOT_END = "take-snapsh
 // When a heap snapshot is read into memory -- only fired
 // once per snapshot.
 actions.READ_SNAPSHOT_START = "read-snapshot-start";
 actions.READ_SNAPSHOT_END = "read-snapshot-end";
 
 // When a census is being performed on a heap snapshot
 actions.TAKE_CENSUS_START = "take-census-start";
 actions.TAKE_CENSUS_END = "take-census-end";
+actions.TAKE_CENSUS_ERROR = "take-census-error";
+
+// When a tree map is being calculated on a heap snapshot
+actions.TAKE_TREE_MAP_START = "take-tree-map-start";
+actions.TAKE_TREE_MAP_END = "take-tree-map-end";
+actions.TAKE_TREE_MAP_ERROR = "take-tree-map-error";
 
 // 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";
 
 // When a heap snapshot is being saved to a user-specified
 // location on disk.
 actions.EXPORT_SNAPSHOT_START = "export-snapshot-start";
@@ -66,16 +72,19 @@ actions.TAKE_CENSUS_DIFF_END = "take-cen
 actions.DIFFING_ERROR = "diffing-error";
 
 // Fired to set a new census display.
 actions.SET_CENSUS_DISPLAY = "set-census-display";
 
 // Fired to change the display that controls the dominator tree labels.
 actions.SET_DOMINATOR_TREE_DISPLAY = "set-dominator-tree-display";
 
+// Fired to set a tree map display
+actions.SET_TREEMAP_DISPLAY = "set-treemap-display";
+
 // Fired when changing between census or dominators view.
 actions.CHANGE_VIEW = "change-view";
 
 // Fired when there is an error processing a snapshot or taking a census.
 actions.SNAPSHOT_ERROR = "snapshot-error";
 
 // Fired when there is a new filter string set.
 actions.SET_FILTER_STRING = "set-filter-string";
@@ -104,38 +113,39 @@ actions.COLLAPSE_DOMINATOR_TREE_NODE = "
 actions.RESIZE_SHORTEST_PATHS = "resize-shortest-paths";
 
 /*** Census Displays ***************************************************************/
 
 const COUNT = Object.freeze({ by: "count", count: true, bytes: true });
 const INTERNAL_TYPE = Object.freeze({ by: "internalType", then: COUNT });
 const ALLOCATION_STACK = Object.freeze({ by: "allocationStack", then: COUNT, noStack: COUNT });
 const OBJECT_CLASS = Object.freeze({ by: "objectClass", then: COUNT, other: COUNT });
+const COARSE_TYPE = Object.freeze({
+  by: "coarseType",
+  objects: OBJECT_CLASS,
+  strings: COUNT,
+  scripts: {
+    by: "filename",
+    then: INTERNAL_TYPE,
+    noFilename: INTERNAL_TYPE
+  },
+  other: INTERNAL_TYPE,
+});
 
 exports.censusDisplays = Object.freeze({
   coarseType: Object.freeze({
     displayName: "Type",
     get tooltip() {
       // Importing down here is necessary because of the circular dependency
       // this introduces with `./utils.js`.
       const { L10N } = require("./utils");
       return L10N.getStr("censusDisplays.coarseType.tooltip");
     },
     inverted: true,
-    breakdown: Object.freeze({
-      by: "coarseType",
-      objects: OBJECT_CLASS,
-      strings: COUNT,
-      scripts: {
-        by: "filename",
-        then: INTERNAL_TYPE,
-        noFilename: INTERNAL_TYPE
-      },
-      other: INTERNAL_TYPE,
-    })
+    breakdown: COARSE_TYPE
   }),
 
   allocationStack: Object.freeze({
     displayName: "Call Stack",
     get tooltip() {
       const { L10N } = require("./utils");
       return L10N.getStr("censusDisplays.allocationStack.tooltip");
     },
@@ -188,49 +198,90 @@ exports.dominatorTreeDisplays = Object.f
     breakdown: Object.freeze({
       by: "allocationStack",
       then: DOMINATOR_TREE_LABEL_COARSE_TYPE,
       noStack: DOMINATOR_TREE_LABEL_COARSE_TYPE,
     }),
   }),
 });
 
+exports.treeMapDisplays = Object.freeze({
+  coarseType: Object.freeze({
+    displayName: "Type",
+    get tooltip() {
+      const { L10N } = require("./utils");
+      return L10N.getStr("treeMapDisplays.coarseType.tooltip");
+    },
+    breakdown: COARSE_TYPE,
+    inverted: false,
+  })
+});
+
 /*** View States **************************************************************/
 
 /**
  * The various main views that the tool can be in.
  */
 const viewState = exports.viewState = Object.create(null);
 viewState.CENSUS = "view-state-census";
 viewState.DIFFING = "view-state-diffing";
 viewState.DOMINATOR_TREE = "view-state-dominator-tree";
+viewState.TREE_MAP = "view-state-tree-map";
 
 /*** Snapshot States **********************************************************/
 
 const snapshotState = exports.snapshotState = Object.create(null);
 
 /**
  * Various states a snapshot can be in.
  * An FSM describing snapshot states:
  *
- *     SAVING -> SAVED -> READING -> READ     SAVED_CENSUS
- *                       ↗                ↘     ↑  ↓
- *              IMPORTING                   SAVING_CENSUS
+ *     SAVING -> SAVED -> READING -> READ
+ *                       ↗
+ *              IMPORTING
  *
  * Any of these states may go to the ERROR state, from which they can never
  * leave (mwah ha ha ha!)
  */
 snapshotState.ERROR = "snapshot-state-error";
 snapshotState.IMPORTING = "snapshot-state-importing";
 snapshotState.SAVING = "snapshot-state-saving";
 snapshotState.SAVED = "snapshot-state-saved";
 snapshotState.READING = "snapshot-state-reading";
 snapshotState.READ = "snapshot-state-read";
-snapshotState.SAVING_CENSUS = "snapshot-state-saving-census";
-snapshotState.SAVED_CENSUS = "snapshot-state-saved-census";
+
+/*
+ * Various states the census model can be in.
+ *
+ *     SAVING <-> SAVED
+ *       |
+ *       V
+ *     ERROR
+ */
+
+const censusState = exports.censusState = Object.create(null);
+
+censusState.SAVING = "census-state-saving";
+censusState.SAVED = "census-state-saved";
+censusState.ERROR = "census-state-error";
+
+/*
+ * Various states the tree map model can be in.
+ *
+ *     SAVING <-> SAVED
+ *       |
+ *       V
+ *     ERROR
+ */
+
+const treeMapState = exports.treeMapState = Object.create(null);
+
+treeMapState.SAVING = "tree-map-state-saving";
+treeMapState.SAVED = "tree-map-state-saved";
+treeMapState.ERROR = "tree-map-state-error";
 
 /*** Diffing States ***********************************************************/
 
 /*
  * Various states the diffing model can be in.
  *
  *     SELECTING --> TAKING_DIFF <---> TOOK_DIFF
  *                       |
--- a/devtools/client/memory/models.js
+++ b/devtools/client/memory/models.js
@@ -68,16 +68,60 @@ const censusDisplayModel = exports.censu
 const dominatorTreeDisplayModel = exports.dominatorTreeDisplay = PropTypes.shape({
   displayName: PropTypes.string.isRequired,
   tooltip: PropTypes.string.isRequired,
   breakdown: PropTypes.shape({
     by: PropTypes.string.isRequired,
   })
 });
 
+/**
+ * The data describing the tree map's shape, and its associated metadata.
+ *
+ * @see `js/src/doc/Debugger/Debugger.Memory.md`
+ */
+const treeMapDisplayModel = exports.treeMapDisplay = PropTypes.shape({
+  displayName: PropTypes.string.isRequired,
+  tooltip: PropTypes.string.isRequired,
+  inverted: PropTypes.bool.isRequired,
+  breakdown: PropTypes.shape({
+    by: PropTypes.string.isRequired,
+  })
+});
+
+/**
+ * Tree map model.
+ */
+const treeMapModel = exports.treeMapModel = PropTypes.shape({
+  // The current census report data.
+  report: PropTypes.object,
+  // The display data used to generate the current census.
+  display: treeMapDisplayModel,
+  // The current treeMapState this is in
+  state: catchAndIgnore(function (treeMap) {
+    switch (treeMap.state) {
+      case treeMapState.SAVING:
+        assert(!treeMap.report, "Should not have a report");
+        assert(!treeMap.error, "Should not have an error");
+        break;
+      case treeMapState.SAVED:
+        assert(treeMap.report, "Should have a report");
+        assert(!treeMap.error, "Should not have an error");
+        break;
+
+      case treeMapState.ERROR:
+        assert(treeMap.error, "Should have an error");
+        break;
+
+      default:
+        assert(false, `Unexpected treeMap state: ${treeMap.state}`);
+    }
+  })
+});
+
 let censusModel = exports.censusModel = PropTypes.shape({
   // The current census report data.
   report: PropTypes.object,
   // The parent map for the report.
   parentMap: PropTypes.object,
   // The display data used to generate the current census.
   display: censusDisplayModel,
   // If present, the currently cached report's filter string used for pruning
@@ -87,16 +131,42 @@ let censusModel = exports.censusModel = 
   expanded: catchAndIgnore(function (census) {
     if (census.report) {
       assert(census.expanded,
              "If we have a report, we should also have the set of expanded nodes");
     }
   }),
   // If a node is currently focused in the report tree, then this is it.
   focused: PropTypes.object,
+  // The censusModelState that this census is currently in.
+  state: catchAndIgnore(function (census) {
+    switch (census.state) {
+      case censusState.SAVING:
+        assert(!census.report, "Should not have a report");
+        assert(!census.parentMap, "Should not have a parent map");
+        assert(census.expanded, "Should not have an expanded set");
+        assert(!census.error, "Should not have an error");
+        break;
+
+      case censusState.SAVED:
+        assert(census.report, "Should have a report");
+        assert(census.parentMap, "Should have a parent map");
+        assert(census.expanded, "Should have an expanded set");
+        assert(!census.error, "Should not have an error");
+        break;
+
+      case censusState.ERROR:
+        assert(!census.report, "Should not have a report");
+        assert(census.error, "Should have an error");
+        break;
+
+      default:
+        assert(false, `Unexpected census state: ${census.state}`);
+    }
+  })
 });
 
 /**
  * Dominator tree model.
  */
 let dominatorTreeModel = exports.dominatorTreeModel = PropTypes.shape({
   // The id of this dominator tree.
   dominatorTreeId: PropTypes.number,
@@ -188,42 +258,39 @@ let snapshotModel = exports.snapshot = P
   selected: PropTypes.bool.isRequired,
   // Filesystem path to where the snapshot is stored; used to identify the
   // snapshot for HeapAnalysesClient.
   path: PropTypes.string,
   // Current census data for this snapshot.
   census: censusModel,
   // Current dominator tree data for this snapshot.
   dominatorTree: dominatorTreeModel,
+  // Current tree map data for this snapshot.
+  treeMap: treeMapModel,
   // If an error was thrown while processing this snapshot, the `Error` instance
   // is attached here.
   error: PropTypes.object,
   // Boolean indicating whether or not this snapshot was imported.
   imported: PropTypes.bool.isRequired,
   // The creation time of the snapshot; required after the snapshot has been
   // read.
   creationTime: PropTypes.number,
   // The current state the snapshot is in.
   // @see ./constants.js
   state: catchAndIgnore(function (snapshot, propName) {
     let current = snapshot.state;
-    let shouldHavePath = [states.IMPORTING, 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];
+    let shouldHavePath = [states.IMPORTING, states.SAVED, states.READ];
+    let shouldHaveCreationTime = [states.READ];
 
     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.census.display || !snapshot.census.display.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.
@@ -290,16 +357,20 @@ let appModel = exports.app = {
 
   // The display data describing how we want the census data to be.
   censusDisplay: censusDisplayModel.isRequired,
 
   // The display data describing how we want the dominator tree labels to be
   // computed.
   dominatorTreeDisplay: dominatorTreeDisplayModel.isRequired,
 
+  // The display data describing how we want the dominator tree labels to be
+  // computed.
+  treeMapDisplay: treeMapDisplayModel.isRequired,
+
   // List of reference to all snapshots taken
   snapshots: PropTypes.arrayOf(snapshotModel).isRequired,
 
   // If present, a filter string for pruning the tree items.
   filter: PropTypes.string,
 
   // If present, the current diffing state.
   diffing: diffingModel,
@@ -314,13 +385,17 @@ let appModel = exports.app = {
       case viewState.DIFFING:
         assert(app.diffing, "Should be diffing");
         break;
 
       case viewState.DOMINATOR_TREE:
         assert(!app.diffing, "Should not be diffing");
         break;
 
+      case viewState.TREE_MAP:
+        assert(!app.diffing, "Should not be diffing");
+        break;
+
       default:
         assert(false, `Unexpected type of view: ${app.view}`);
     }
   }),
 };
--- a/devtools/client/memory/reducers.js
+++ b/devtools/client/memory/reducers.js
@@ -2,13 +2,14 @@
  * 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.censusDisplay = require("./reducers/census-display");
 exports.diffing = require("./reducers/diffing");
 exports.dominatorTreeDisplay = require("./reducers/dominator-tree-display");
+exports.treeMapDisplay = require("./reducers/tree-map-display");
 exports.errors = require("./reducers/errors");
 exports.filter = require("./reducers/filter");
 exports.sizes = require("./reducers/sizes");
 exports.snapshots = require("./reducers/snapshots");
 exports.view = require("./reducers/view");
--- a/devtools/client/memory/reducers/moz.build
+++ b/devtools/client/memory/reducers/moz.build
@@ -7,10 +7,11 @@ DevToolsModules(
     'allocations.js',
     'census-display.js',
     'diffing.js',
     'dominator-tree-display.js',
     'errors.js',
     'filter.js',
     'sizes.js',
     'snapshots.js',
+    'tree-map-display.js',
     'view.js',
 )
--- a/devtools/client/memory/reducers/snapshots.js
+++ b/devtools/client/memory/reducers/snapshots.js
@@ -3,16 +3,18 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 "use strict";
 
 const Immutable = require("devtools/client/shared/vendor/immutable");
 const { immutableUpdate, assert } = require("devtools/shared/DevToolsUtils");
 const {
   actions,
   snapshotState: states,
+  censusState,
+  treeMapState,
   dominatorTreeState,
   viewState,
 } = require("../constants");
 const DominatorTreeNode = require("devtools/shared/heapsnapshot/DominatorTreeNode");
 
 const handlers = Object.create(null);
 
 handlers[actions.SNAPSHOT_ERROR] = function (snapshots, { id, error }) {
@@ -53,45 +55,110 @@ handlers[actions.READ_SNAPSHOT_END] = fu
   });
 };
 
 handlers[actions.TAKE_CENSUS_START] = function (snapshots, { id, display, filter }) {
   const census = {
     report: null,
     display,
     filter,
+    state: censusState.SAVING
   };
 
   return snapshots.map(snapshot => {
     return snapshot.id === id
-      ? immutableUpdate(snapshot, { state: states.SAVING_CENSUS, census })
+      ? immutableUpdate(snapshot, { census })
       : snapshot;
   });
 };
 
 handlers[actions.TAKE_CENSUS_END] = function (snapshots, { id,
                                                            report,
                                                            parentMap,
                                                            display,
                                                            filter }) {
   const census = {
     report,
     parentMap,
     expanded: Immutable.Set(),
     display,
     filter,
+    state: censusState.SAVED
   };
 
   return snapshots.map(snapshot => {
     return snapshot.id === id
-      ? immutableUpdate(snapshot, { state: states.SAVED_CENSUS, census })
+      ? immutableUpdate(snapshot, { census })
+      : snapshot;
+  });
+};
+
+handlers[actions.TAKE_CENSUS_ERROR] = function (snapshots, { id, error }) {
+  assert(error, "actions with TAKE_CENSUS_ERROR should have an error");
+
+  return snapshots.map(snapshot => {
+    if (snapshot.id !== id) {
+      return snapshot;
+    }
+
+    const census = Object.freeze({
+      state: censusState.ERROR,
+      error,
+    });
+
+    return immutableUpdate(snapshot, { census });
+  });
+};
+
+handlers[actions.TAKE_TREE_MAP_START] = function (snapshots, { id, display }) {
+  const treeMap = {
+    report: null,
+    display,
+    state: treeMapState.SAVING
+  };
+
+  return snapshots.map(snapshot => {
+    return snapshot.id === id
+      ? immutableUpdate(snapshot, { treeMap })
       : snapshot;
   });
 };
 
+handlers[actions.TAKE_TREE_MAP_END] = function (snapshots, action) {
+  const { id, report, display } = action;
+  const treeMap = {
+    report,
+    display,
+    state: treeMapState.SAVED
+  };
+
+  return snapshots.map(snapshot => {
+    return snapshot.id === id
+      ? immutableUpdate(snapshot, { treeMap })
+      : snapshot;
+  });
+};
+
+handlers[actions.TAKE_TREE_MAP_ERROR] = function (snapshots, { id, error }) {
+  assert(error, "actions with TAKE_TREE_MAP_ERROR should have an error");
+
+  return snapshots.map(snapshot => {
+    if (snapshot.id !== id) {
+      return snapshot;
+    }
+
+    const treeMap = Object.freeze({
+      state: treeMapState.ERROR,
+      error,
+    });
+
+    return immutableUpdate(snapshot, { treeMap });
+  });
+};
+
 handlers[actions.EXPAND_CENSUS_NODE] = function (snapshots, { id, node }) {
   return snapshots.map(snapshot => {
     if (snapshot.id !== id) {
       return snapshot;
     }
 
     assert(snapshot.census, "Should have a census");
     assert(snapshot.census.report, "Should have a census report");
new file mode 100644
--- /dev/null
+++ b/devtools/client/memory/reducers/tree-map-display.js
@@ -0,0 +1,19 @@
+/* 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, treeMapDisplays } = require("../constants");
+const DEFAULT_TREE_MAP_DISPLAY = treeMapDisplays.coarseType;
+
+const handlers = Object.create(null);
+
+handlers[actions.SET_TREE_MAP_DISPLAY] = function (_, { display }) {
+  return display;
+};
+
+module.exports = function (state = DEFAULT_TREE_MAP_DISPLAY, action) {
+  const handler = handlers[action.type];
+  return handler ? handler(state, action) : state;
+};
--- a/devtools/client/memory/reducers/view.js
+++ b/devtools/client/memory/reducers/view.js
@@ -6,12 +6,12 @@
 const { actions, viewState } = require("../constants");
 
 const handlers = Object.create(null);
 
 handlers[actions.CHANGE_VIEW] = function (_, { view }) {
   return view;
 };
 
-module.exports = function (view = viewState.CENSUS, action) {
+module.exports = function (view = viewState.TREE_MAP, action) {
   const handler = handlers[action.type];
   return handler ? handler(view, action) : view;
 };
--- a/devtools/client/memory/telemetry.js
+++ b/devtools/client/memory/telemetry.js
@@ -5,17 +5,17 @@
 // This module exports methods to record telemetry data for memory tool usage.
 //
 // NB: Ensure that *every* exported function is wrapped in `makeInfallible` so
 // that our probes don't accidentally break code that actually does productive
 // work for the user!
 
 const { telemetry } = require("Services");
 const { makeInfallible, immutableUpdate } = require("devtools/shared/DevToolsUtils");
-const { dominatorTreeDisplays, censusDisplays } = require("./constants");
+const { dominatorTreeDisplays, treeMapDisplays, censusDisplays } = require("./constants");
 
 exports.countTakeSnapshot = makeInfallible(function () {
   const histogram = telemetry.getHistogramById("DEVTOOLS_MEMORY_TAKE_SNAPSHOT_COUNT");
   histogram.add(1);
 }, "devtools/client/memory/telemetry#countTakeSnapshot");
 
 exports.countImportSnapshot = makeInfallible(function () {
   const histogram = telemetry.getHistogramById("DEVTOOLS_MEMORY_IMPORT_SNAPSHOT_COUNT");
--- a/devtools/client/memory/test/browser/browser.ini
+++ b/devtools/client/memory/test/browser/browser.ini
@@ -17,8 +17,10 @@ support-files =
 [browser_memory_keyboard.js]
 [browser_memory_keyboard-snapshot-list.js]
 [browser_memory_no_allocation_stacks.js]
 [browser_memory_no_auto_expand.js]
     skip-if = debug # bug 1219554
 [browser_memory_percents_01.js]
 [browser_memory_simple_01.js]
 [browser_memory_transferHeapSnapshot_e10s_01.js]
+[browser_memory_tree_map-01.js]
+[browser_memory_tree_map-02.js]
--- a/devtools/client/memory/test/browser/browser_memory_allocationStackDisplay_01.js
+++ b/devtools/client/memory/test/browser/browser_memory_allocationStackDisplay_01.js
@@ -4,25 +4,29 @@
 // Sanity test that we can show allocation stack displays in the tree.
 
 "use strict";
 
 const { waitForTime } = require("devtools/shared/DevToolsUtils");
 const { toggleRecordingAllocationStacks } = require("devtools/client/memory/actions/allocations");
 const { takeSnapshotAndCensus } = require("devtools/client/memory/actions/snapshot");
 const censusDisplayActions = require("devtools/client/memory/actions/census-display");
+const { viewState } = require("devtools/client/memory/constants");
+const { changeView } = require("devtools/client/memory/actions/view");
 
 const TEST_URL = "http://example.com/browser/devtools/client/memory/test/browser/doc_steady_allocation.html";
 
 this.test = makeMemoryTest(TEST_URL, function* ({ tab, panel }) {
   const heapWorker = panel.panelWin.gHeapAnalysesClient;
   const front = panel.panelWin.gFront;
   const { getState, dispatch } = panel.panelWin.gStore;
   const doc = panel.panelWin.document;
 
+  dispatch(changeView(viewState.CENSUS));
+
   dispatch(censusDisplayActions.setCensusDisplay(censusDisplays.invertedAllocationStack));
   is(getState().censusDisplay.breakdown.by, "allocationStack");
 
   yield dispatch(toggleRecordingAllocationStacks(front));
   ok(getState().allocations.recording);
 
   // Let some allocations build up.
   yield waitForTime(500);
--- a/devtools/client/memory/test/browser/browser_memory_clear_snapshots.js
+++ b/devtools/client/memory/test/browser/browser_memory_clear_snapshots.js
@@ -1,32 +1,34 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 /**
  * Tests taking and then clearing snapshots.
  */
 
+ const { treeMapState } = require("devtools/client/memory/constants");
 const TEST_URL = "http://example.com/browser/devtools/client/memory/test/browser/doc_steady_allocation.html";
 
 this.test = makeMemoryTest(TEST_URL, function* ({ tab, panel }) {
   const { gStore, document } = panel.panelWin;
   const { getState, dispatch } = gStore;
 
   let snapshotEls = document.querySelectorAll("#memory-tool-container .list li");
   is(getState().snapshots.length, 0, "Starts with no snapshots in store");
   is(snapshotEls.length, 0, "No snapshots visible");
 
   info("Take two snapshots");
   takeSnapshot(panel.panelWin);
   takeSnapshot(panel.panelWin);
   yield waitUntilState(gStore, state =>
     state.snapshots.length === 2 &&
-    state.snapshots[0].state === states.SAVED_CENSUS &&
-    state.snapshots[1].state === states.SAVED_CENSUS);
+    state.snapshots[0].treeMap && state.snapshots[1].treeMap &&
+    state.snapshots[0].treeMap.state === treeMapState.SAVED &&
+    state.snapshots[1].treeMap.state === treeMapState.SAVED);
 
   snapshotEls = document.querySelectorAll("#memory-tool-container .list li");
   is(snapshotEls.length, 2, "Two snapshots visible");
 
   info("Click on Clear Snapshots");
   yield clearSnapshots(panel.panelWin);
   is(getState().snapshots.length, 0, "No snapshots in store");
   snapshotEls = document.querySelectorAll("#memory-tool-container .list li");
--- a/devtools/client/memory/test/browser/browser_memory_diff_01.js
+++ b/devtools/client/memory/test/browser/browser_memory_diff_01.js
@@ -3,17 +3,18 @@
 
 // Test diffing.
 
 "use strict";
 
 const { waitForTime } = require("devtools/shared/DevToolsUtils");
 const {
   snapshotState,
-  diffingState
+  diffingState,
+  treeMapState
 } = require("devtools/client/memory/constants");
 
 const TEST_URL = "http://example.com/browser/devtools/client/memory/test/browser/doc_steady_allocation.html";
 
 this.test = makeMemoryTest(TEST_URL, function* ({ tab, panel }) {
   const heapWorker = panel.panelWin.gHeapAnalysesClient;
   const front = panel.panelWin.gFront;
   const store = panel.panelWin.gStore;
@@ -35,18 +36,19 @@ this.test = makeMemoryTest(TEST_URL, fun
                        state =>
                          !!state.diffing &&
                          state.diffing.state === diffingState.SELECTING);
   ok(true, "Clicking the diffing button put us into the diffing state.");
   is(getDisplayedSnapshotStatus(doc), "Select the baseline snapshot");
 
   yield waitUntilState(store, state =>
     state.snapshots.length === 2 &&
-    state.snapshots[0].state === snapshotState.SAVED_CENSUS &&
-    state.snapshots[1].state === snapshotState.SAVED_CENSUS);
+    state.snapshots[0].treeMap && state.snapshots[1].treeMap &&
+    state.snapshots[0].treeMap.state === treeMapState.SAVED &&
+    state.snapshots[1].treeMap.state === treeMapState.SAVED);
 
   const listItems = [...doc.querySelectorAll(".snapshot-list-item")];
   is(listItems.length, 2, "Should have two snapshot list items");
 
   // Select the first snapshot.
   EventUtils.synthesizeMouseAtCenter(listItems[0], {}, panel.panelWin);
   yield waitUntilState(store,
                        state =>
--- a/devtools/client/memory/test/browser/browser_memory_displays_01.js
+++ b/devtools/client/memory/test/browser/browser_memory_displays_01.js
@@ -2,28 +2,34 @@
    http://creativecommons.org/publicdomain/zero/1.0/ */
 "use strict";
 
 /**
  * Tests that the heap tree renders rows based on the display
  */
 
 const TEST_URL = "http://example.com/browser/devtools/client/memory/test/browser/doc_steady_allocation.html";
+const { viewState, censusState } = require("devtools/client/memory/constants");
+const { changeView } = require("devtools/client/memory/actions/view");
 
 this.test = makeMemoryTest(TEST_URL, function* ({ tab, panel }) {
   const { gStore, document } = panel.panelWin;
 
+  const { dispatch } = panel.panelWin.gStore;
+
   function $$(selector) {
     return [...document.querySelectorAll(selector)];
   }
+  dispatch(changeView(viewState.CENSUS));
 
   yield takeSnapshot(panel.panelWin);
 
   yield waitUntilState(gStore, state =>
-    state.snapshots[0].state === states.SAVED_CENSUS);
+    state.snapshots[0].census &&
+    state.snapshots[0].census.state === censusState.SAVED);
 
   info("Check coarse type heap view");
   ["Function", "js::Shape", "Object", "strings"].forEach(findNameCell);
 
   yield setCensusDisplay(panel.panelWin, censusDisplays.allocationStack);
   info("Check allocation stack heap view");
   [L10N.getStr("tree-item.nostack")].forEach(findNameCell);
 
--- a/devtools/client/memory/test/browser/browser_memory_filter_01.js
+++ b/devtools/client/memory/test/browser/browser_memory_filter_01.js
@@ -4,47 +4,53 @@
 // Sanity test that we can show allocation stack displays in the tree.
 
 "use strict";
 
 const {
   dominatorTreeState,
   snapshotState,
   viewState,
+  censusState,
 } = require("devtools/client/memory/constants");
-const { changeViewAndRefresh } = require("devtools/client/memory/actions/view");
+const { changeViewAndRefresh, changeView } = require("devtools/client/memory/actions/view");
 
 const TEST_URL = "http://example.com/browser/devtools/client/memory/test/browser/doc_steady_allocation.html";
 
 this.test = makeMemoryTest(TEST_URL, function* ({ tab, panel }) {
   const heapWorker = panel.panelWin.gHeapAnalysesClient;
   const front = panel.panelWin.gFront;
   const store = panel.panelWin.gStore;
   const { getState, dispatch } = store;
   const doc = panel.panelWin.document;
 
+  dispatch(changeView(viewState.CENSUS));
+
   const takeSnapshotButton = doc.getElementById("take-snapshot");
   EventUtils.synthesizeMouseAtCenter(takeSnapshotButton, {}, panel.panelWin);
 
   yield waitUntilState(store, state =>
     state.snapshots.length === 1 &&
-    state.snapshots[0].state === snapshotState.SAVED_CENSUS);
+    state.snapshots[0].census &&
+    state.snapshots[0].census.state === censusState.SAVING);
 
   let filterInput = doc.getElementById("filter");
   EventUtils.synthesizeMouseAtCenter(filterInput, {}, panel.panelWin);
   EventUtils.sendString("js::Shape", panel.panelWin);
 
   yield waitUntilState(store, state =>
     state.snapshots.length === 1 &&
-    state.snapshots[0].state === snapshotState.SAVING_CENSUS);
+    state.snapshots[0].census &&
+    state.snapshots[0].census.state === censusState.SAVING);
   ok(true, "adding a filter string should trigger census recompute");
 
   yield waitUntilState(store, state =>
     state.snapshots.length === 1 &&
-    state.snapshots[0].state === snapshotState.SAVED_CENSUS);
+    state.snapshots[0].census &&
+    state.snapshots[0].census.state === censusState.SAVED);
 
   let nameElem = doc.querySelector(".heap-tree-item-field.heap-tree-item-name");
   ok(nameElem, "Should get a tree item row with a name");
   is(nameElem.textContent.trim(), "js::Shape", "the tree item should be the one we filtered for");
   is(filterInput.value, "js::Shape",
     "and filter input contains the user value");
 
   // Now switch the dominator view, then switch back to census view
@@ -56,17 +62,18 @@ this.test = makeMemoryTest(TEST_URL, fun
   yield waitUntilDominatorTreeState(store, [dominatorTreeState.LOADED]);
   ok(true, "computed and fetched the dominator tree.");
 
   dispatch(changeViewAndRefresh(viewState.CENSUS, heapWorker));
   ok(true, "change view back to census");
 
   yield waitUntilState(store, state =>
     state.snapshots.length === 1 &&
-    state.snapshots[0].state === snapshotState.SAVED_CENSUS);
+    state.snapshots[0].census &&
+    state.snapshots[0].census.state === censusState.SAVED);
 
   nameElem = doc.querySelector(".heap-tree-item-field.heap-tree-item-name");
   filterInput = doc.getElementById("filter");
 
   ok(nameElem, "Should still get a tree item row with a name");
   is(nameElem.textContent.trim(), "js::Shape",
     "the tree item should still be the one we filtered for");
   is(filterInput.value, "js::Shape",
--- a/devtools/client/memory/test/browser/browser_memory_keyboard-snapshot-list.js
+++ b/devtools/client/memory/test/browser/browser_memory_keyboard-snapshot-list.js
@@ -1,44 +1,49 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Test that using ACCEL+UP/DOWN, the user can navigate between snapshots.
 
 "use strict";
 
 const {
-  snapshotState
+  snapshotState,
+  viewState,
+  censusState
 } = require("devtools/client/memory/constants");
 const {
   takeSnapshotAndCensus
 } = require("devtools/client/memory/actions/snapshot");
+const { changeView } = require("devtools/client/memory/actions/view");
 
 const TEST_URL = "http://example.com/browser/devtools/client/memory/test/browser/doc_steady_allocation.html";
 
 this.test = makeMemoryTest(TEST_URL, function* ({ panel }) {
   // Creating snapshots already takes ~25 seconds on linux 32 debug machines
   // which makes the test very likely to go over the allowed timeout
   requestLongerTimeout(2);
 
   const heapWorker = panel.panelWin.gHeapAnalysesClient;
   const front = panel.panelWin.gFront;
   const store = panel.panelWin.gStore;
   const { dispatch } = store;
   const doc = panel.panelWin.document;
 
+  dispatch(changeView(viewState.CENSUS));
+
   info("Take 3 snapshots");
   dispatch(takeSnapshotAndCensus(front, heapWorker));
   dispatch(takeSnapshotAndCensus(front, heapWorker));
   dispatch(takeSnapshotAndCensus(front, heapWorker));
 
   yield waitUntilState(store, state =>
     state.snapshots.length == 3 &&
-    state.snapshots.every(s => s.state === snapshotState.SAVED_CENSUS));
-  ok(true, "All snapshots are in SAVED_CENSUS state");
+    state.snapshots.every(s => s.census && s.census.state === censusState.SAVED));
+  ok(true, "All snapshots censuses are in SAVED state");
 
   yield waitUntilSnapshotSelected(store, 2);
   ok(true, "Third snapshot selected after creating all snapshots.");
 
   info("Press ACCEL+UP key, expect second snapshot selected.");
   EventUtils.synthesizeKey("VK_UP", { accelKey: true }, panel.panelWin);
   yield waitUntilSnapshotSelected(store, 1);
   ok(true, "Second snapshot selected after alt+UP.");
--- a/devtools/client/memory/test/browser/browser_memory_keyboard.js
+++ b/devtools/client/memory/test/browser/browser_memory_keyboard.js
@@ -2,29 +2,32 @@
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Bug 1246570 - Check that when pressing on LEFT arrow, the parent tree node
 // gets focused.
 
 "use strict";
 
 const {
-  snapshotState
+  snapshotState,
+  censusState,
+  viewState
 } = require("devtools/client/memory/constants");
 const {
   takeSnapshotAndCensus
 } = require("devtools/client/memory/actions/snapshot");
+const { changeView } = require("devtools/client/memory/actions/view");
 
 const TEST_URL = "http://example.com/browser/devtools/client/memory/test/browser/doc_steady_allocation.html";
 
 function waitUntilFocused(store, node) {
   return waitUntilState(store, state =>
       state.snapshots.length === 1 &&
-      state.snapshots[0].state === snapshotState.SAVED_CENSUS &&
       state.snapshots[0].census &&
+      state.snapshots[0].census.state === censusState.SAVED &&
       state.snapshots[0].census.focused &&
       state.snapshots[0].census.focused === node
   );
 }
 
 function waitUntilExpanded(store, node) {
   return waitUntilState(store, state =>
     state.snapshots[0] &&
@@ -34,17 +37,20 @@ function waitUntilExpanded(store, node) 
 
 this.test = makeMemoryTest(TEST_URL, function* ({ tab, panel }) {
   const heapWorker = panel.panelWin.gHeapAnalysesClient;
   const front = panel.panelWin.gFront;
   const store = panel.panelWin.gStore;
   const { getState, dispatch } = store;
   const doc = panel.panelWin.document;
 
+  dispatch(changeView(viewState.CENSUS));
+
   is(getState().censusDisplay.breakdown.by, "coarseType");
+
   yield dispatch(takeSnapshotAndCensus(front, heapWorker));
   let census = getState().snapshots[0].census;
   let root1 = census.report.children[0];
   let root2 = census.report.children[0];
   let root3 = census.report.children[0];
   let root4 = census.report.children[0];
   let child1 = root1.children[0];
 
--- a/devtools/client/memory/test/browser/browser_memory_no_allocation_stacks.js
+++ b/devtools/client/memory/test/browser/browser_memory_no_allocation_stacks.js
@@ -2,25 +2,29 @@
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Sanity test that we can show allocation stack displays in the tree.
 
 "use strict";
 
 const { takeSnapshotAndCensus } = require("devtools/client/memory/actions/snapshot");
 const censusDisplayActions = require("devtools/client/memory/actions/census-display");
+const { viewState } = require("devtools/client/memory/constants");
+const { changeView } = require("devtools/client/memory/actions/view");
 
 const TEST_URL = "http://example.com/browser/devtools/client/memory/test/browser/doc_steady_allocation.html";
 
 this.test = makeMemoryTest(TEST_URL, function* ({ tab, panel }) {
   const heapWorker = panel.panelWin.gHeapAnalysesClient;
   const front = panel.panelWin.gFront;
   const { getState, dispatch } = panel.panelWin.gStore;
   const doc = panel.panelWin.document;
 
+  dispatch(changeView(viewState.CENSUS));
+
   ok(!getState().allocations.recording,
      "Should not be recording allocagtions");
 
   yield dispatch(takeSnapshotAndCensus(front, heapWorker));
   yield dispatch(censusDisplayActions.setCensusDisplayAndRefresh(
     heapWorker,
     censusDisplays.allocationStack));
 
--- a/devtools/client/memory/test/browser/browser_memory_no_auto_expand.js
+++ b/devtools/client/memory/test/browser/browser_memory_no_auto_expand.js
@@ -2,25 +2,29 @@
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Bug 1221150 - Ensure that census trees do not accidentally auto expand
 // when clicking on the allocation stacks checkbox.
 
 "use strict";
 
 const { takeSnapshotAndCensus } = require("devtools/client/memory/actions/snapshot");
+const { viewState } = require("devtools/client/memory/constants");
+const { changeView } = require("devtools/client/memory/actions/view");
 
 const TEST_URL = "http://example.com/browser/devtools/client/memory/test/browser/doc_steady_allocation.html";
 
 this.test = makeMemoryTest(TEST_URL, function* ({ tab, panel }) {
   const heapWorker = panel.panelWin.gHeapAnalysesClient;
   const front = panel.panelWin.gFront;
   const { getState, dispatch } = panel.panelWin.gStore;
   const doc = panel.panelWin.document;
 
+  dispatch(changeView(viewState.CENSUS));
+
   yield dispatch(takeSnapshotAndCensus(front, heapWorker));
 
   is(getState().allocations.recording, false);
   const recordingCheckbox = doc.getElementById("record-allocation-stacks-checkbox");
   EventUtils.synthesizeMouseAtCenter(recordingCheckbox, {}, panel.panelWin);
   is(getState().allocations.recording, true);
 
   const nameElems = [...doc.querySelectorAll(".heap-tree-item-field.heap-tree-item-name")];
--- a/devtools/client/memory/test/browser/browser_memory_percents_01.js
+++ b/devtools/client/memory/test/browser/browser_memory_percents_01.js
@@ -1,16 +1,18 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Sanity test that we calculate percentages in the tree.
 
 "use strict";
 
 const { takeSnapshotAndCensus } = require("devtools/client/memory/actions/snapshot");
+const { viewState } = require("devtools/client/memory/constants");
+const { changeView } = require("devtools/client/memory/actions/view");
 
 const TEST_URL = "http://example.com/browser/devtools/client/memory/test/browser/doc_steady_allocation.html";
 
 function checkCells(cells) {
   ok(cells.length > 1, "Should have found some");
   // Ignore the first header cell.
   for (let cell of cells.slice(1)) {
     const percent = cell.querySelector(".heap-tree-percent");
@@ -20,16 +22,18 @@ function checkCells(cells) {
 }
 
 this.test = makeMemoryTest(TEST_URL, function* ({ tab, panel }) {
   const heapWorker = panel.panelWin.gHeapAnalysesClient;
   const front = panel.panelWin.gFront;
   const { getState, dispatch } = panel.panelWin.gStore;
   const doc = panel.panelWin.document;
 
+  dispatch(changeView(viewState.CENSUS));
+
   yield dispatch(takeSnapshotAndCensus(front, heapWorker));
   is(getState().censusDisplay.breakdown.by, "coarseType",
      "Should be using coarse type breakdown");
 
   const bytesCells = [...doc.querySelectorAll(".heap-tree-item-bytes")];
   checkCells(bytesCells);
 
   const totalBytesCells = [...doc.querySelectorAll(".heap-tree-item-total-bytes")];
--- a/devtools/client/memory/test/browser/browser_memory_simple_01.js
+++ b/devtools/client/memory/test/browser/browser_memory_simple_01.js
@@ -1,21 +1,25 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 /**
  * Tests taking snapshots and default states.
  */
 
 const TEST_URL = "http://example.com/browser/devtools/client/memory/test/browser/doc_steady_allocation.html";
+const { viewState, censusState } = require("devtools/client/memory/constants");
+const { changeView } = require("devtools/client/memory/actions/view");
 
 this.test = makeMemoryTest(TEST_URL, function* ({ tab, panel }) {
   const { gStore, document } = panel.panelWin;
   const { getState, dispatch } = gStore;
 
+  dispatch(changeView(viewState.CENSUS));
+
   let snapshotEls = document.querySelectorAll("#memory-tool-container .list li");
   is(getState().snapshots.length, 0, "Starts with no snapshots in store");
   is(snapshotEls.length, 0, "No snapshots rendered");
 
   yield takeSnapshot(panel.panelWin);
   snapshotEls = document.querySelectorAll("#memory-tool-container .list li");
   is(getState().snapshots.length, 1, "One snapshot was created in store");
   is(snapshotEls.length, 1, "One snapshot was rendered");
@@ -23,15 +27,14 @@ this.test = makeMemoryTest(TEST_URL, fun
 
   yield takeSnapshot(panel.panelWin);
   snapshotEls = document.querySelectorAll("#memory-tool-container .list li");
   is(getState().snapshots.length, 2, "Two snapshots created in store");
   is(snapshotEls.length, 2, "Two snapshots rendered");
   ok(!snapshotEls[0].classList.contains("selected"), "First snapshot no longer has `selected` class");
   ok(snapshotEls[1].classList.contains("selected"), "Second snapshot has `selected` class");
 
-  yield waitUntilState(gStore, state =>
-    state.snapshots[0].state === states.SAVED_CENSUS &&
-    state.snapshots[1].state === states.SAVED_CENSUS);
+  yield waitUntilCensusState(gStore, s => s.census, [censusState.SAVED,
+                                                     censusState.SAVED]);
 
   ok(document.querySelector(".heap-tree-item-name"),
     "Should have rendered some tree items");
 });
new file mode 100644
--- /dev/null
+++ b/devtools/client/memory/test/browser/browser_memory_tree_map-01.js
@@ -0,0 +1,102 @@
+/* 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/. */
+
+// Make sure the canvases are created correctly
+
+"use strict";
+
+const CanvasUtils = require("devtools/client/memory/components/tree-map/canvas-utils");
+const D3_SCRIPT = '<script type="application/javascript" ' +
+                  'src="chrome://devtools/content/shared/vendor/d3.js>';
+const TEST_URL = `data:text/html,<html><body>${D3_SCRIPT}</body></html>`;
+
+this.test = makeMemoryTest(TEST_URL, function* ({ tab, panel }) {
+  const document = panel.panelWin.document;
+  const window = panel.panelWin;
+  const div = document.createElement("div");
+
+  Object.assign(div.style, {
+    width: "100px",
+    height: "200px",
+    position: "absolute"
+  });
+
+  document.body.appendChild(div);
+
+  info("Create the canvases");
+
+  let canvases = new CanvasUtils(div, 0);
+
+  info("Test the shape of the returned object");
+
+  is(typeof canvases, "object", "Canvases create an object");
+  is(typeof canvases.emit, "function", "Decorated with an EventEmitter");
+  is(typeof canvases.on, "function", "Decorated with an EventEmitter");
+  is(div.children[0], canvases.container, "Div has the container");
+  ok(canvases.main.canvas instanceof window.HTMLCanvasElement,
+    "Creates the main canvas");
+  ok(canvases.zoom.canvas instanceof window.HTMLCanvasElement,
+    "Creates the zoom canvas");
+  ok(canvases.main.ctx instanceof window.CanvasRenderingContext2D,
+    "Creates the main canvas context");
+  ok(canvases.zoom.ctx instanceof window.CanvasRenderingContext2D,
+    "Creates the zoom canvas context");
+
+  info("Test resizing");
+
+  let timesResizeCalled = 0;
+  canvases.on('resize', function() {
+    timesResizeCalled++;
+  });
+
+  let main = canvases.main.canvas;
+  let zoom = canvases.zoom.canvas;
+  let ratio = window.devicePixelRatio;
+
+  is(main.width, 100 * ratio,
+    "Main canvas width is the same as the parent div");
+  is(main.height, 200 * ratio,
+    "Main canvas height is the same as the parent div");
+  is(zoom.width, 100 * ratio,
+    "Zoom canvas width is the same as the parent div");
+  is(zoom.height, 200 * ratio,
+    "Zoom canvas height is the same as the parent div");
+  is(timesResizeCalled, 0,
+    "Resize was not emitted");
+
+  div.style.width = "500px";
+  div.style.height = "700px";
+
+  window.dispatchEvent(new Event("resize"));
+
+  is(main.width, 500 * ratio,
+    "Main canvas width is resized to be the same as the parent div");
+  is(main.height, 700 * ratio,
+    "Main canvas height is resized to be the same as the parent div");
+  is(zoom.width, 500 * ratio,
+    "Zoom canvas width is resized to be the same as the parent div");
+  is(zoom.height, 700 * ratio,
+    "Zoom canvas height is resized to be the same as the parent div");
+  is(timesResizeCalled, 1,
+    "'resize' was emitted was emitted");
+
+  div.style.width = "1100px";
+  div.style.height = "1300px";
+
+  canvases.destroy();
+  window.dispatchEvent(new Event("resize"));
+
+  is(main.width, 500 * ratio,
+    "Main canvas width is not resized after destroy");
+  is(main.height, 700 * ratio,
+    "Main canvas height is not resized after destroy");
+  is(zoom.width, 500 * ratio,
+    "Zoom canvas width is not resized after destroy");
+  is(zoom.height, 700 * ratio,
+    "Zoom canvas height is not resized after destroy");
+  is(timesResizeCalled, 1,
+    "onResize was not called again");
+
+  document.body.removeChild(div);
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/memory/test/browser/browser_memory_tree_map-02.js
@@ -0,0 +1,129 @@
+/* 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/. */
+
+// Test the drag and zooming behavior
+
+"use strict";
+
+const CanvasUtils = require("devtools/client/memory/components/tree-map/canvas-utils");
+const DragZoom = require("devtools/client/memory/components/tree-map/drag-zoom");
+
+const TEST_URL = `data:text/html,<html><body></body></html>`;
+const PIXEL_SCROLL_MODE = 0;
+const PIXEL_DELTA = 10;
+const MAX_RAF_LOOP = 1000;
+
+this.test = makeMemoryTest(TEST_URL, function* ({ tab, panel }) {
+  const panelWin = panel.panelWin;
+  const panelDoc = panelWin.document;
+  const div = panelDoc.createElement("div");
+
+  Object.assign(div.style, {
+    width: "100px",
+    height: "200px",
+    position: "absolute",
+    left:0,
+    top:0
+  });
+
+  let rafMock = createRAFMock();
+
+  panelDoc.body.appendChild(div);
+
+  let canvases = new CanvasUtils(div, 0);
+  let dragZoom = new DragZoom(canvases.container, 0, rafMock.raf);
+
+  info("Check initial state of dragZoom");
+  {
+    is(dragZoom.zoom, 0, "Zooming starts at 0");
+    is(dragZoom.smoothZoom, 0, "Smoothed zooming starts at 0");
+    is(rafMock.timesCalled, 0, "No RAFs have been queued");
+
+    panelWin.dispatchEvent(new WheelEvent("wheel", {
+      deltaY: -PIXEL_DELTA,
+      deltaMode: PIXEL_SCROLL_MODE
+    }));
+
+    is(dragZoom.zoom, PIXEL_DELTA * dragZoom.ZOOM_SPEED,
+      "The zoom was increased");
+    ok(dragZoom.smoothZoom < dragZoom.zoom && dragZoom.smoothZoom > 0,
+      "The smooth zoom is between the initial value and the target");
+    is(rafMock.timesCalled, 1, "A RAF has been queued");
+  }
+
+  info("RAF will eventually stop once the smooth values approach the target");
+  {
+    let i;
+    let lastCallCount;
+    for (i = 0; i < MAX_RAF_LOOP; i++) {
+      if (lastCallCount === rafMock.timesCalled) {
+        break;
+      }
+      lastCallCount = rafMock.timesCalled;
+      rafMock.nextFrame();
+    }
+
+    is(dragZoom.zoom, dragZoom.smoothZoom,
+      "The smooth and target zoom values match");
+    isnot(MAX_RAF_LOOP, i,
+      "The RAF loop correctly stopped");
+  }
+
+  info("Dragging correctly translates the div");
+  {
+    let initialX = dragZoom.translateX;
+    let initialY = dragZoom.translateY;
+    div.dispatchEvent(new MouseEvent("mousemove", {
+      clientX: 10,
+      clientY: 10,
+    }));
+    div.dispatchEvent(new MouseEvent("mousedown"));
+    div.dispatchEvent(new MouseEvent("mousemove", {
+      clientX: 20,
+      clientY: 20,
+    }));
+    div.dispatchEvent(new MouseEvent("mouseup"));
+
+    ok(dragZoom.translateX - initialX > 0,
+      "Translate X moved by some pixel amount");
+    ok(dragZoom.translateY - initialY > 0,
+      "Translate Y moved by some pixel amount");
+  }
+
+  dragZoom.destroy();
+
+  info("Scroll isn't tracked after destruction");
+  {
+    let previousZoom = dragZoom.zoom;
+    let previousSmoothZoom = dragZoom.smoothZoom;
+
+    panelWin.dispatchEvent(new WheelEvent("wheel", {
+      deltaY: -PIXEL_DELTA,
+      deltaMode: PIXEL_SCROLL_MODE
+    }));
+
+    is(dragZoom.zoom, previousZoom,
+      "The zoom stayed the same");
+    is(dragZoom.smoothZoom, previousSmoothZoom,
+      "The smooth zoom stayed the same");
+  }
+
+  info("Translation isn't tracked after destruction");
+  {
+    let initialX = dragZoom.translateX;
+    let initialY = dragZoom.translateY;
+
+    div.dispatchEvent(new MouseEvent("mousedown"));
+    div.dispatchEvent(new MouseEvent("mousemove"), {
+      clientX: 40,
+      clientY: 40,
+    });
+    div.dispatchEvent(new MouseEvent("mouseup"));
+    is(dragZoom.translateX, initialX,
+      "The translationX didn't change");
+    is(dragZoom.translateY, initialY,
+      "The translationY didn't change");
+  }
+  panelDoc.body.removeChild(div);
+});
--- a/devtools/client/memory/test/browser/head.js
+++ b/devtools/client/memory/test/browser/head.js
@@ -107,17 +107,17 @@ function takeSnapshot (window) {
   document.querySelector(".devtools-toolbar .take-snapshot").click();
   return waitUntilState(gStore, () => gStore.getState().snapshots.length === snapshotCount + 1);
 }
 
 function clearSnapshots (window) {
   let { gStore, document } = window;
   document.querySelector(".devtools-toolbar .clear-snapshots").click();
   return waitUntilState(gStore, () => gStore.getState().snapshots.every(
-    (snapshot) => snapshot.state !== states.SAVED_CENSUS)
+    (snapshot) => snapshot.state !== states.READ)
   );
 }
 
 /**
  * Sets the current requested display and waits for the selected snapshot to use
  * it and complete the new census that entails.
  */
 function setCensusDisplay(window, display) {
@@ -126,17 +126,19 @@ function setCensusDisplay(window, displa
   // XXX: Should handle this via clicking the DOM, but React doesn't
   // fire the onChange event, so just change it in the store.
   // window.document.querySelector(`.select-display`).value = type;
   gStore.dispatch(require("devtools/client/memory/actions/census-display")
                          .setCensusDisplayAndRefresh(gHeapAnalysesClient, display));
 
   return waitUntilState(window.gStore, () => {
     let selected = window.gStore.getState().snapshots.find(s => s.selected);
-    return selected.state === states.SAVED_CENSUS &&
+    return selected.state === states.READ &&
+      selected.census &&
+      selected.census.state === censusState.SAVED &&
       selected.census.display === display;
   });
 }
 
 /**
  * Get the snapshot tatus text currently displayed, or null if none is
  * displayed.
  *
@@ -164,8 +166,62 @@ function getSelectedSnapshotIndex(store)
  *
  * @return {Promise}
  */
 function waitUntilSnapshotSelected(store, snapshotIndex) {
   return waitUntilState(store, state =>
     state.snapshots[snapshotIndex] &&
     state.snapshots[snapshotIndex].selected === true);
 }
+
+
+/**
+ * Wait until the state has censuses in a certain state.
+ *
+ * @return {Promise}
+ */
+function waitUntilCensusState (store, getCensus, expected) {
+  let predicate = () => {
+    let snapshots = store.getState().snapshots;
+
+    info('Current census state:' +
+             snapshots.map(x => getCensus(x) ? getCensus(x).state : null ));
+
+    return snapshots.length === expected.length &&
+           expected.every((state, i) => {
+             let census = getCensus(snapshots[i]);
+             return (state === "*") ||
+                    (!census && !state) ||
+                    (census && census.state === state);
+           });
+  };
+  info(`Waiting for snapshots' censuses to be of state: ${expected}`);
+  return waitUntilState(store, predicate);
+}
+/**
+ * Mock out the requestAnimationFrame.
+ *
+ * @return {Object}
+ * @function nextFrame
+ *           Call the last queued function
+ * @function raf
+ *           The mocked raf function
+ * @function timesCalled
+ *           How many times the RAF has been called
+ */
+function createRAFMock() {
+  let queuedFns = [];
+  let mock = { timesCalled: 0 };
+
+  mock.nextFrame = function() {
+    let thisQueue = queuedFns;
+    queuedFns = [];
+    for(var i = 0; i < thisQueue.length; i++) {
+      thisQueue[i]();
+    }
+  };
+
+  mock.raf = function(fn) {
+    mock.timesCalled++;
+    queuedFns.push(fn);
+  };
+  return mock;
+}
--- a/devtools/client/memory/test/chrome/chrome.ini
+++ b/devtools/client/memory/test/chrome/chrome.ini
@@ -10,8 +10,9 @@ support-files =
 [test_Heap_01.html]
 [test_Heap_02.html]
 [test_Heap_03.html]
 [test_Heap_04.html]
 [test_Heap_05.html]
 [test_ShortestPaths_01.html]
 [test_ShortestPaths_02.html]
 [test_Toolbar_01.html]
+[test_TreeMap_01.html]
--- a/devtools/client/memory/test/chrome/head.js
+++ b/devtools/client/memory/test/chrome/head.js
@@ -20,32 +20,34 @@ var { immutableUpdate } = DevToolsUtils;
 
 var constants = require("devtools/client/memory/constants");
 var {
   censusDisplays,
   diffingState,
   dominatorTreeDisplays,
   dominatorTreeState,
   snapshotState,
-  viewState
+  viewState,
+  censusState
 } = constants;
 
 const {
   L10N,
 } = require("devtools/client/memory/utils");
 
 var models = require("devtools/client/memory/models");
 
 var React = require("devtools/client/shared/vendor/react");
 var ReactDOM = require("devtools/client/shared/vendor/react-dom");
 var Heap = React.createFactory(require("devtools/client/memory/components/heap"));
 var CensusTreeItem = React.createFactory(require("devtools/client/memory/components/census-tree-item"));
 var DominatorTreeComponent = React.createFactory(require("devtools/client/memory/components/dominator-tree"));
 var DominatorTreeItem = React.createFactory(require("devtools/client/memory/components/dominator-tree-item"));
 var ShortestPaths = React.createFactory(require("devtools/client/memory/components/shortest-paths"));
+var TreeMap = React.createFactory(require("devtools/client/memory/components/tree-map"));
 var Toolbar = React.createFactory(require("devtools/client/memory/components/toolbar"));
 
 // All tests are asynchronous.
 SimpleTest.waitForExplicitFinish();
 
 var noop = () => {};
 
 var TEST_CENSUS_TREE_ITEM_PROPS = Object.freeze({
@@ -181,26 +183,28 @@ var TEST_HEAP_PROPS = Object.freeze({
         breakdown: Object.freeze({
           by: "coarseType",
           objects: Object.freeze({ by: "count", count: true, bytes: true }),
           scripts: Object.freeze({ by: "count", count: true, bytes: true }),
           strings: Object.freeze({ by: "count", count: true, bytes: true }),
           other: Object.freeze({ by: "count", count: true, bytes: true }),
         }),
       }),
+      state: censusState.SAVED,
       inverted: false,
       filter: null,
       expanded: new Set(),
       focused: null,
+      parentMap: Object.freeze(Object.create(null))
     }),
     dominatorTree: TEST_DOMINATOR_TREE,
     error: null,
     imported: false,
     creationTime: 0,
-    state: snapshotState.SAVED_CENSUS,
+    state: snapshotState.READ,
   }),
   sizes: Object.freeze({ shortestPathsSize: .5 }),
   onShortestPathsResize: noop,
 });
 
 var TEST_TOOLBAR_PROPS = Object.freeze({
   censusDisplays: [
     censusDisplays.coarseType,
@@ -223,16 +227,57 @@ var TEST_TOOLBAR_PROPS = Object.freeze({
   dominatorTreeDisplays: [
     dominatorTreeDisplays.coarseType,
     dominatorTreeDisplays.allocationStack,
   ],
   onDominatorTreeDisplayChange: noop,
   snapshots: [],
 });
 
+function makeTestCensusNode() {
+  return {
+    name: "Function",
+    bytes: 100,
+    totalBytes: 100,
+    count: 100,
+    totalCount: 100,
+    children: []
+  };
+}
+
+var TEST_TREE_MAP_PROPS = Object.freeze({
+  treeMap: Object.freeze({
+    report: {
+      name: null,
+      bytes: 0,
+      totalBytes: 400,
+      count: 0,
+      totalCount: 400,
+      children: [
+        {
+          name: "objects",
+          bytes: 0,
+          totalBytes: 200,
+          count: 0,
+          totalCount: 200,
+          children: [ makeTestCensusNode(), makeTestCensusNode() ]
+        },
+        {
+          name: "other",
+          bytes: 0,
+          totalBytes: 200,
+          count: 0,
+          totalCount: 200,
+          children: [ makeTestCensusNode(), makeTestCensusNode() ],
+        }
+      ]
+    }
+  })
+});
+
 function onNextAnimationFrame(fn) {
   return () =>
     requestAnimationFrame(() =>
       requestAnimationFrame(fn));
 }
 
 /**
  * Render the provided ReactElement in the provided HTML container.
--- a/devtools/client/memory/test/chrome/test_Heap_02.html
+++ b/devtools/client/memory/test/chrome/test_Heap_02.html
@@ -32,17 +32,17 @@ Test that the currently selected view is
                 "Should render the dominator tree.");
 
              // Census view.
 
              yield renderComponent(Heap(immutableUpdate(TEST_HEAP_PROPS, {
                view: viewState.CENSUS,
              })), container);
 
-             ok(container.querySelector(`[data-state=${snapshotState.SAVED_CENSUS}]`),
+             ok(container.querySelector(`[data-state=${censusState.SAVED}]`),
                 "Should render the census.");
 
              // Diffing view.
 
              yield renderComponent(Heap(immutableUpdate(TEST_HEAP_PROPS, {
                view: viewState.DIFFING,
                snapshot: null,
                diffing: {
new file mode 100644
--- /dev/null
+++ b/devtools/client/memory/test/chrome/test_TreeMap_01.html
@@ -0,0 +1,44 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test that the Tree Map correctly renders onto 2 managed canvases.
+-->
+<head>
+    <meta charset="utf-8">
+    <title>Tree component test</title>
+    <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+    <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+
+    <script type="application/javascript"
+            src="chrome://devtools/content/shared/vendor/d3.js">
+    </script>
+</head>
+<body>
+  <!-- Give the container height so that the whole tree is rendered. -->
+  <div id="container" style="height: 900px;"></div>
+
+  <pre id="test">
+    <script src="head.js" type="application/javascript;version=1.8"></script>
+    <script type="application/javascript;version=1.8">
+      window.onload = Task.async(function*() {
+        try {
+          const container = document.getElementById("container");
+
+          yield renderComponent(TreeMap(TEST_TREE_MAP_PROPS), container);
+
+          let treeMapContainer = container.querySelector(".tree-map-container");
+          ok(treeMapContainer, "Component creates a container");
+
+          let canvases = treeMapContainer.querySelectorAll("canvas");
+          is(canvases.length, 2, "Creates 2 canvases");
+
+        } catch(e) {
+          ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+        } finally {
+          SimpleTest.finish();
+        }
+      });
+    </script>
+  </pre>
+</body>
+</html>
--- a/devtools/client/memory/test/unit/head.js
+++ b/devtools/client/memory/test/unit/head.js
@@ -68,16 +68,35 @@ function waitUntilSnapshotState (store, 
     do_print(snapshots.map(x => x.state));
     return snapshots.length === expected.length &&
            expected.every((state, i) => state === "*" || snapshots[i].state === state);
   };
   do_print(`Waiting for snapshots to be of state: ${expected}`);
   return waitUntilState(store, predicate);
 }
 
+function waitUntilCensusState (store, getCensus, expected) {
+  let predicate = () => {
+    let snapshots = store.getState().snapshots;
+
+    do_print('Current census state:' +
+             snapshots.map(x => getCensus(x) ? getCensus(x).state : null ));
+
+    return snapshots.length === expected.length &&
+           expected.every((state, i) => {
+             let census = getCensus(snapshots[i]);
+             return (state === "*") ||
+                    (!census && !state) ||
+                    (census && census.state === state);
+           });
+  };
+  do_print(`Waiting for snapshots' censuses to be of state: ${expected}`);
+  return waitUntilState(store, predicate);
+}
+
 function *createTempFile () {
   let file = FileUtils.getFile("TmpD", ["tmp.fxsnapshot"]);
   file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
   let destPath = file.path;
   let stat = yield OS.File.stat(destPath);
   ok(stat.size === 0, "new file is 0 bytes at start");
   return destPath;
 }
--- a/devtools/client/memory/test/unit/test_action-clear-snapshots_01.js
+++ b/devtools/client/memory/test/unit/test_action-clear-snapshots_01.js
@@ -1,29 +1,30 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
-// Test clearSnapshots deletes snapshots with state SAVED_CENSUS
+// Test clearSnapshots deletes snapshots with READ censuses
 
 let { takeSnapshotAndCensus, clearSnapshots } = require("devtools/client/memory/actions/snapshot");
 let { snapshotState: states, actions } = require("devtools/client/memory/constants");
+const { treeMapState } = require("devtools/client/memory/constants");
 
 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;
 
   dispatch(takeSnapshotAndCensus(front, heapWorker));
-  yield waitUntilSnapshotState(store, [states.SAVED_CENSUS]);
+  yield waitUntilCensusState(store, s => s.treeMap, [treeMapState.SAVED]);
   ok(true, "snapshot created");
 
   ok(true, "dispatch clearSnapshots action");
   let deleteEvents = Promise.all([
     waitUntilAction(store, actions.DELETE_SNAPSHOTS_START),
     waitUntilAction(store, actions.DELETE_SNAPSHOTS_END)
   ]);
   dispatch(clearSnapshots(heapWorker));
--- a/devtools/client/memory/test/unit/test_action-clear-snapshots_02.js
+++ b/devtools/client/memory/test/unit/test_action-clear-snapshots_02.js
@@ -1,43 +1,47 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
-// Test clearSnapshots preserves snapshots with state != SAVED_CENSUS or ERROR
+// Test clearSnapshots preserves snapshots with state != READ or ERROR
 
 let { takeSnapshotAndCensus, clearSnapshots, takeSnapshot } = require("devtools/client/memory/actions/snapshot");
-let { snapshotState: states, actions } = require("devtools/client/memory/constants");
+let { snapshotState: states, treeMapState, actions } = require("devtools/client/memory/constants");
 
 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;
 
-  ok(true, "create a snapshot in SAVED_CENSUS state");
+  ok(true, "create a snapshot with a census in SAVED state");
   dispatch(takeSnapshotAndCensus(front, heapWorker));
   ok(true, "create a snapshot in SAVED state");
   dispatch(takeSnapshot(front));
-  yield waitUntilSnapshotState(store, [states.SAVED_CENSUS, states.SAVED]);
+  yield waitUntilSnapshotState(store, [states.SAVED, states.SAVED]);
+  yield waitUntilCensusState(store, snapshot => snapshot.treeMap,
+                             [treeMapState.SAVED, null]);
   ok(true, "snapshots created with expected states");
 
   ok(true, "dispatch clearSnapshots action");
   let deleteEvents = Promise.all([
     waitUntilAction(store, actions.DELETE_SNAPSHOTS_START),
     waitUntilAction(store, actions.DELETE_SNAPSHOTS_END)
   ]);
   dispatch(clearSnapshots(heapWorker));
   yield deleteEvents;
   ok(true, "received delete snapshots events");
 
   equal(getState().snapshots.length, 1, "one snapshot remaining");
   let remainingSnapshot = getState().snapshots[0];
-  notEqual(remainingSnapshot.state, states.SAVED_CENSUS,
-    "remaining snapshot doesn't have the SAVED_CENSUS state");
+  equal(remainingSnapshot.treeMap, undefined,
+    "remaining snapshot doesn't have a treeMap property");
+  equal(remainingSnapshot.census, undefined,
+    "remaining snapshot doesn't have a census property");
 
   heapWorker.destroy();
   yield front.detach();
 });
--- a/devtools/client/memory/test/unit/test_action-clear-snapshots_03.js
+++ b/devtools/client/memory/test/unit/test_action-clear-snapshots_03.js
@@ -1,31 +1,34 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Test clearSnapshots deletes snapshots with state ERROR
 
 let { takeSnapshotAndCensus, clearSnapshots } = require("devtools/client/memory/actions/snapshot");
-let { snapshotState: states, actions } = require("devtools/client/memory/constants");
+let { snapshotState: states, treeMapState, actions } = require("devtools/client/memory/constants");
 
 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;
 
-  ok(true, "create a snapshot with SAVED_CENSUS state");
+  ok(true, "create a snapshot with a treeMap");
   dispatch(takeSnapshotAndCensus(front, heapWorker));
-  yield waitUntilSnapshotState(store, [states.SAVED_CENSUS]);
-  ok(true, "snapshot created with SAVED_CENSUS state");
+  yield waitUntilSnapshotState(store, [states.SAVED]);
+  ok(true, "snapshot created with a SAVED state");
+  yield waitUntilCensusState(store, snapshot => snapshot.treeMap,
+                             [treeMapState.SAVED]);
+  ok(true, "treeMap created with a SAVED state");
 
   ok(true, "set snapshot state to error");
   let id = getState().snapshots[0].id;
   dispatch({ type: actions.SNAPSHOT_ERROR, id, error: new Error("_") });
   yield waitUntilSnapshotState(store, [states.ERROR]);
   ok(true, "snapshot set to error state");
 
   ok(true, "dispatch clearSnapshots action");
--- a/devtools/client/memory/test/unit/test_action-clear-snapshots_04.js
+++ b/devtools/client/memory/test/unit/test_action-clear-snapshots_04.js
@@ -1,40 +1,41 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Test clearSnapshots deletes several snapshots
 
 let { takeSnapshotAndCensus, clearSnapshots } = require("devtools/client/memory/actions/snapshot");
-let { snapshotState: states, actions } = require("devtools/client/memory/constants");
+let { snapshotState: states, actions, treeMapState } = require("devtools/client/memory/constants");
 
 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;
 
-  ok(true, "create 3 snapshots in SAVED_CENSUS state");
+  ok(true, "create 3 snapshots with a saved census");
   dispatch(takeSnapshotAndCensus(front, heapWorker));
   dispatch(takeSnapshotAndCensus(front, heapWorker));
   dispatch(takeSnapshotAndCensus(front, heapWorker));
-  ok(true, "snapshots created in SAVED_CENSUS state");
-  yield waitUntilSnapshotState(store,
-    [states.SAVED_CENSUS, states.SAVED_CENSUS, states.SAVED_CENSUS]);
+  yield waitUntilCensusState(store, snapshot => snapshot.treeMap,
+                             [treeMapState.SAVED, treeMapState.SAVED,
+                              treeMapState.SAVED]);
+  ok(true, "snapshots created with a saved census");
 
   ok(true, "set first snapshot state to error");
   let id = getState().snapshots[0].id;
   dispatch({ type: actions.SNAPSHOT_ERROR, id, error: new Error("_") });
   yield waitUntilSnapshotState(store,
-    [states.ERROR, states.SAVED_CENSUS, states.SAVED_CENSUS]);
+    [states.ERROR, states.READ, states.READ]);
   ok(true, "first snapshot set to error state");
 
   ok(true, "dispatch clearSnapshots action");
   let deleteEvents = Promise.all([
     waitUntilAction(store, actions.DELETE_SNAPSHOTS_START),
     waitUntilAction(store, actions.DELETE_SNAPSHOTS_END)
   ]);
   dispatch(clearSnapshots(heapWorker));
--- a/devtools/client/memory/test/unit/test_action-clear-snapshots_05.js
+++ b/devtools/client/memory/test/unit/test_action-clear-snapshots_05.js
@@ -1,32 +1,33 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Test clearSnapshots deletes several snapshots
 
 let { takeSnapshotAndCensus, clearSnapshots } = require("devtools/client/memory/actions/snapshot");
-let { snapshotState: states, actions } = require("devtools/client/memory/constants");
+let { snapshotState: states, actions, treeMapState } = require("devtools/client/memory/constants");
 
 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;
 
-  ok(true, "create 3 snapshots in SAVED_CENSUS state");
+  ok(true, "create 2 snapshots with a saved census");
   dispatch(takeSnapshotAndCensus(front, heapWorker));
   dispatch(takeSnapshotAndCensus(front, heapWorker));
-  ok(true, "snapshots created in SAVED_CENSUS state");
-  yield waitUntilSnapshotState(store, [states.SAVED_CENSUS, states.SAVED_CENSUS]);
+  ok(true, "snapshots created with a saved census");
+  yield waitUntilCensusState(store, snapshot => snapshot.treeMap,
+                             [treeMapState.SAVED, treeMapState.SAVED]);
 
   let errorHeapWorker = {
     deleteHeapSnapshot: function() {
       return Promise.reject("_");
     }
   };
 
   ok(true, "dispatch clearSnapshots action");
--- a/devtools/client/memory/test/unit/test_action-clear-snapshots_06.js
+++ b/devtools/client/memory/test/unit/test_action-clear-snapshots_06.js
@@ -2,42 +2,45 @@
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 // Test that clearSnapshots disables diffing when deleting snapshots
 
 const {
   takeSnapshotAndCensus,
-  clearSnapshots } = require("devtools/client/memory/actions/snapshot");
+  clearSnapshots
+} = require("devtools/client/memory/actions/snapshot");
 const {
   snapshotState: states,
-  actions } = require("devtools/client/memory/constants");
+  actions,
+  treeMapState
+} = require("devtools/client/memory/constants");
 const {
   toggleDiffing,
   selectSnapshotForDiffingAndRefresh
 } = require("devtools/client/memory/actions/diffing");
 
 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;
 
-  ok(true, "Create 2 snapshots in SAVED_CENSUS state");
+  ok(true, "create 2 snapshots with a saved census");
   dispatch(takeSnapshotAndCensus(front, heapWorker));
   dispatch(takeSnapshotAndCensus(front, heapWorker));
-  ok(true, "Snapshots created in SAVED_CENSUS state");
-  yield waitUntilSnapshotState(store,
-    [states.SAVED_CENSUS, states.SAVED_CENSUS]);
+  yield waitUntilCensusState(store, snapshot => snapshot.treeMap,
+                             [treeMapState.SAVED, treeMapState.SAVED]);
+  ok(true, "snapshots created with a saved census");
 
   dispatch(toggleDiffing());
   dispatch(selectSnapshotForDiffingAndRefresh(heapWorker,
                                               getState().snapshots[0]));
   dispatch(selectSnapshotForDiffingAndRefresh(heapWorker,
                                               getState().snapshots[1]));
 
   ok(getState().diffing, "We should be in diffing view");
--- a/devtools/client/memory/test/unit/test_action-export-snapshot.js
+++ b/devtools/client/memory/test/unit/test_action-export-snapshot.js
@@ -1,31 +1,32 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Test exporting a snapshot to a user specified location on disk.
 
 let { exportSnapshot } = require("devtools/client/memory/actions/io");
 let { takeSnapshotAndCensus } = require("devtools/client/memory/actions/snapshot");
-let { snapshotState: states, actions } = require("devtools/client/memory/constants");
+let { snapshotState: states, actions, treeMapState } = require("devtools/client/memory/constants");
 
 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;
 
   let destPath = yield createTempFile();
   dispatch(takeSnapshotAndCensus(front, heapWorker));
-  yield waitUntilSnapshotState(store, [states.SAVED_CENSUS]);
+  yield waitUntilCensusState(store, snapshot => snapshot.treeMap,
+                             [treeMapState.SAVED]);
 
   let exportEvents = Promise.all([
     waitUntilAction(store, actions.EXPORT_SNAPSHOT_START),
     waitUntilAction(store, actions.EXPORT_SNAPSHOT_END)
   ]);
   dispatch(exportSnapshot(getState().snapshots[0], destPath));
   yield exportEvents;
 
--- a/devtools/client/memory/test/unit/test_action-filter-02.js
+++ b/devtools/client/memory/test/unit/test_action-filter-02.js
@@ -1,65 +1,74 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 "use strict";
 
 // Test that changing filter state properly refreshes the selected census.
 
-let { snapshotState: states } = require("devtools/client/memory/constants");
+let { snapshotState: states, viewState, censusState } = require("devtools/client/memory/constants");
 let { setFilterStringAndRefresh } = require("devtools/client/memory/actions/filter");
 let { takeSnapshotAndCensus, selectSnapshotAndRefresh } = require("devtools/client/memory/actions/snapshot");
+let { setCensusDisplay } = require("devtools/client/memory/actions/census-display");
+let { changeView } = require("devtools/client/memory/actions/view");
 
 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(changeView(viewState.CENSUS));
+
   equal(getState().filter, null, "no filter 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]);
+  yield waitUntilCensusState(store, snapshot => snapshot.census,
+                             [censusState.SAVED,
+                              censusState.SAVED,
+                              censusState.SAVED]);
   ok(true, "saved 3 snapshots and took a census of each of them");
 
   dispatch(setFilterStringAndRefresh("str", heapWorker));
-  yield waitUntilSnapshotState(store, [states.SAVED_CENSUS,
-                                       states.SAVED_CENSUS,
-                                       states.SAVING_CENSUS]);
+  yield waitUntilCensusState(store, snapshot => snapshot.census,
+                             [censusState.SAVED,
+                              censusState.SAVED,
+                              censusState.SAVING]);
   ok(true, "setting filter string should recompute the selected snapshot's census");
 
   equal(getState().filter, "str", "now inverted");
 
-  yield waitUntilSnapshotState(store, [states.SAVED_CENSUS,
-                                       states.SAVED_CENSUS,
-                                       states.SAVED_CENSUS]);
+  yield waitUntilCensusState(store, snapshot => snapshot.census,
+                             [censusState.SAVED,
+                              censusState.SAVED,
+                              censusState.SAVED]);
 
   equal(getState().snapshots[0].census.filter, null);
   equal(getState().snapshots[1].census.filter, null);
   equal(getState().snapshots[2].census.filter, "str");
 
   dispatch(selectSnapshotAndRefresh(heapWorker, getState().snapshots[1].id));
-  yield waitUntilSnapshotState(store, [states.SAVED_CENSUS,
-                                       states.SAVING_CENSUS,
-                                       states.SAVED_CENSUS]);
+  yield waitUntilCensusState(store, snapshot => snapshot.census,
+                             [censusState.SAVED,
+                              censusState.SAVING,
+                              censusState.SAVED]);
   ok(true, "selecting non-inverted census should trigger a recompute");
 
-  yield waitUntilSnapshotState(store, [states.SAVED_CENSUS,
-                                       states.SAVED_CENSUS,
-                                       states.SAVED_CENSUS]);
+  yield waitUntilCensusState(store, snapshot => snapshot.census,
+                             [censusState.SAVED,
+                              censusState.SAVED,
+                              censusState.SAVED]);
 
   equal(getState().snapshots[0].census.filter, null);
   equal(getState().snapshots[1].census.filter, "str");
   equal(getState().snapshots[2].census.filter, "str");
 
   heapWorker.destroy();
   yield front.detach();
 });
--- a/devtools/client/memory/test/unit/test_action-filter-03.js
+++ b/devtools/client/memory/test/unit/test_action-filter-03.js
@@ -1,46 +1,52 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Test that changing filter state in the middle of taking a snapshot results in
 // the properly fitered census.
 
-let { snapshotState: states } = require("devtools/client/memory/constants");
+let { snapshotState: states, censusState, viewState } = require("devtools/client/memory/constants");
 let { setFilterString, setFilterStringAndRefresh } = require("devtools/client/memory/actions/filter");
 let { takeSnapshotAndCensus } = require("devtools/client/memory/actions/snapshot");
+let { changeView } = require("devtools/client/memory/actions/view");
 
 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(changeView(viewState.CENSUS));
+
   dispatch(takeSnapshotAndCensus(front, heapWorker));
   yield waitUntilSnapshotState(store, [states.SAVING]);
 
   dispatch(setFilterString("str"));
 
-  yield waitUntilSnapshotState(store, [states.SAVED_CENSUS]);
+  yield waitUntilCensusState(store, snapshot => snapshot.census,
+                             [censusState.SAVED]);
   equal(getState().filter, "str",
         "should want filtered trees");
   equal(getState().snapshots[0].census.filter, "str",
         "snapshot-we-were-in-the-middle-of-saving's census should be filtered");
 
   dispatch(setFilterStringAndRefresh("", heapWorker));
-  yield waitUntilSnapshotState(store, [states.SAVING_CENSUS]);
+  yield waitUntilCensusState(store, snapshot => snapshot.census,
+                             [censusState.SAVING]);
   ok(true, "changing filter string retriggers census");
   ok(!getState().filter, "no longer filtering");
 
   dispatch(setFilterString("obj"));
-  yield waitUntilSnapshotState(store, [states.SAVED_CENSUS]);
+  yield waitUntilCensusState(store, snapshot => snapshot.census,
+                             [censusState.SAVED]);
   equal(getState().filter, "obj", "filtering for obj now");
   equal(getState().snapshots[0].census.filter, "obj",
         "census-we-were-in-the-middle-of-recomputing should be filtered again");
 
   heapWorker.destroy();
   yield front.detach();
 });
--- a/devtools/client/memory/test/unit/test_action-import-snapshot-and-census.js
+++ b/devtools/client/memory/test/unit/test_action-import-snapshot-and-census.js
@@ -2,77 +2,90 @@
    http://creativecommons.org/publicdomain/zero/1.0/ */
 "use strict";
 
 /**
  * Tests the task creator `importSnapshotAndCensus()` for the whole flow of
  * importing a snapshot, and its sub-actions.
  */
 
-let { actions, snapshotState: states } = require("devtools/client/memory/constants");
+let { actions, snapshotState: states, treeMapState } = require("devtools/client/memory/constants");
 let { exportSnapshot, importSnapshotAndCensus } = require("devtools/client/memory/actions/io");
 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 { subscribe, dispatch, getState } = store;
 
   let destPath = yield createTempFile();
   dispatch(takeSnapshotAndCensus(front, heapWorker));
-  yield waitUntilSnapshotState(store, [states.SAVED_CENSUS]);
+  yield waitUntilCensusState(store, s => s.treeMap, [treeMapState.SAVED]);
 
   let exportEvents = Promise.all([
     waitUntilAction(store, actions.EXPORT_SNAPSHOT_START),
     waitUntilAction(store, actions.EXPORT_SNAPSHOT_END)
   ]);
   dispatch(exportSnapshot(getState().snapshots[0], destPath));
   yield exportEvents;
 
   // Now import our freshly exported snapshot
-  let i = 0;
-  let expected = ["IMPORTING", "READING", "READ", "SAVING_CENSUS", "SAVED_CENSUS"];
+  let snapshotI = 0;
+  let censusI = 0;
+  let snapshotStates = ["IMPORTING", "READING", "READ"];
+  let censusStates = ["SAVING", "SAVED"];
   let expectStates = () => {
     let snapshot = getState().snapshots[1];
     if (!snapshot) {
       return;
     }
-    let isCorrectState = snapshot.state === states[expected[i]];
-    if (isCorrectState) {
-      ok(true, `Found expected state ${expected[i]}`);
-      i++;
+    if (snapshotI < snapshotStates.length) {
+      let isCorrectState = snapshot.state === states[snapshotStates[snapshotI]];
+      if (isCorrectState) {
+        ok(true, `Found expected snapshot state ${snapshotStates[snapshotI]}`);
+        snapshotI++;
+      }
+    }
+    if (snapshot.treeMap && censusI < censusStates.length) {
+      if (snapshot.treeMap.state === treeMapState[censusStates[censusI]]) {
+        ok(true, `Found expected census state ${censusStates[censusI]}`);
+        censusI++;
+      }
     }
   };
 
   let unsubscribe = subscribe(expectStates);
   dispatch(importSnapshotAndCensus(heapWorker, destPath));
 
-  yield waitUntilState(store, () => i === expected.length);
+  yield waitUntilState(store, () => { return snapshotI === snapshotStates.length &&
+                                      censusI === censusStates.length });
   unsubscribe();
-  equal(i, expected.length, "importSnapshotAndCensus() produces the correct sequence of states in a snapshot");
-  equal(getState().snapshots[1].state, states.SAVED_CENSUS, "imported snapshot is in SAVED_CENSUS state");
+  equal(snapshotI, snapshotStates.length, "importSnapshotAndCensus() produces the correct sequence of states in a snapshot");
+  equal(getState().snapshots[1].state, states.READ, "imported snapshot is in READ state");
+  equal(censusI, censusStates.length, "importSnapshotAndCensus() produces the correct sequence of states in a census");
+  equal(getState().snapshots[1].treeMap.state, treeMapState.SAVED, "imported snapshot is in READ state");
   ok(getState().snapshots[1].selected, "imported snapshot is selected");
 
   // Check snapshot data
   let snapshot1 = getState().snapshots[0];
   let snapshot2 = getState().snapshots[1];
 
-  equal(snapshot1.census.display, snapshot2.census.display,
+  equal(snapshot1.treeMap.display, snapshot2.treeMap.display,
         "imported snapshot has correct display");
 
   // Clone the census data so we can destructively remove the ID/parents to compare
   // equal census data
-  let census1 = stripUnique(JSON.parse(JSON.stringify(snapshot1.census.report)));
-  let census2 = stripUnique(JSON.parse(JSON.stringify(snapshot2.census.report)));
+  let census1 = stripUnique(JSON.parse(JSON.stringify(snapshot1.treeMap.report)));
+  let census2 = stripUnique(JSON.parse(JSON.stringify(snapshot2.treeMap.report)));
 
   equal(JSON.stringify(census1), JSON.stringify(census2), "Imported snapshot has correct census");
 
   function stripUnique (obj) {
     let children = obj.children || [];
     for (let child of children) {
       delete child.id;
       delete child.parent;
--- a/devtools/client/memory/test/unit/test_action-import-snapshot-dominator-tree.js
+++ b/devtools/client/memory/test/unit/test_action-import-snapshot-dominator-tree.js
@@ -3,17 +3,17 @@
 "use strict";
 
 /**
  * Tests `importSnapshotAndCensus()` when importing snapshots from the dominator
  * tree view. The snapshot is expected to be loaded and its dominator tree
  * should be computed.
  */
 
-let { snapshotState, dominatorTreeState, viewState, } =
+let { snapshotState, dominatorTreeState, viewState, treeMapState } =
   require("devtools/client/memory/constants");
 let { importSnapshotAndCensus } = require("devtools/client/memory/actions/io");
 let { changeViewAndRefresh } = require("devtools/client/memory/actions/view");
 
 function run_test() {
   run_next_test();
 }
 
@@ -28,18 +28,18 @@ add_task(function* () {
   equal(getState().view, viewState.DOMINATOR_TREE,
         "We should now be in the DOMINATOR_TREE view");
 
   let i = 0;
   let expected = [
     "IMPORTING",
     "READING",
     "READ",
-    "SAVING_CENSUS",
-    "SAVED_CENSUS",
+    "treeMap:SAVING",
+    "treeMap:SAVED",
     "dominatorTree:COMPUTING",
     "dominatorTree:FETCHING",
     "dominatorTree:LOADED",
   ];
   let expectStates = () => {
     let snapshot = getState().snapshots[0];
     if (snapshot && hasExpectedState(snapshot, expected[i])) {
       ok(true, `Found expected state ${expected[i]}`);
@@ -68,11 +68,17 @@ add_task(function* () {
  */
 function hasExpectedState(snapshot, expectedState) {
   let isDominatorState = expectedState.indexOf("dominatorTree:") === 0;
   if (isDominatorState) {
     let state = dominatorTreeState[expectedState.replace("dominatorTree:", "")];
     return snapshot.dominatorTree && snapshot.dominatorTree.state === state;
   }
 
+  let isTreeMapState = expectedState.indexOf("treeMap:") === 0;
+  if (isTreeMapState) {
+    let state = treeMapState[expectedState.replace("treeMap:", "")];
+    return snapshot.treeMap && snapshot.treeMap.state === state;
+  }
+
   let state = snapshotState[expectedState];
   return snapshot.state === state;
 }
--- a/devtools/client/memory/test/unit/test_action-set-display-and-refresh-01.js
+++ b/devtools/client/memory/test/unit/test_action-set-display-and-refresh-01.js
@@ -4,31 +4,34 @@
 
 /**
  * Tests the task creator `setCensusDisplayAndRefreshAndRefresh()` for display
  * changing. We test this rather than `setCensusDisplayAndRefresh` directly, as
  * we use the refresh action in the app itself composed from
  * `setCensusDisplayAndRefresh`.
  */
 
-let { censusDisplays, snapshotState: states } = require("devtools/client/memory/constants");
+let { censusDisplays, snapshotState: states, censusState, viewState } = require("devtools/client/memory/constants");
 let { setCensusDisplayAndRefresh } = require("devtools/client/memory/actions/census-display");
 let { takeSnapshotAndCensus, selectSnapshotAndRefresh } = require("devtools/client/memory/actions/snapshot");
+const { changeView } = require("devtools/client/memory/actions/view");
 
 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(changeView(viewState.CENSUS));
+
   // Test default display with no snapshots
   equal(getState().censusDisplay.breakdown.by, "coarseType",
         "default coarseType display selected at start.");
   dispatch(setCensusDisplayAndRefresh(heapWorker,
                                       censusDisplays.allocationStack));
   equal(getState().censusDisplay.breakdown.by, "allocationStack",
         "display changed with no snapshots");
 
@@ -38,56 +41,70 @@ add_task(function *() {
   yield waitUntilState(store, () => getState().errors.length === 1);
   ok(true, "Emits an error action when passing in an invalid display object");
 
   equal(getState().censusDisplay.breakdown.by, "allocationStack",
     "current display unchanged when passing invalid display");
 
   // Test new snapshots
   dispatch(takeSnapshotAndCensus(front, heapWorker));
-  yield waitUntilSnapshotState(store, [states.SAVED_CENSUS]);
-
+  yield waitUntilCensusState(store, snapshot => snapshot.census,
+                             [censusState.SAVED]);
 
   // Updates when changing display during `SAVING`
   dispatch(takeSnapshotAndCensus(front, heapWorker));
-  yield waitUntilSnapshotState(store, [states.SAVED_CENSUS, states.SAVING]);
+  yield waitUntilCensusState(store, snapshot => snapshot.census,
+                             [censusState.SAVED, censusState.SAVING]);
   dispatch(setCensusDisplayAndRefresh(heapWorker, censusDisplays.coarseType));
-  yield waitUntilSnapshotState(store, [states.SAVED_CENSUS, states.SAVED_CENSUS]);
+  yield waitUntilCensusState(store, snapshot => snapshot.census,
+                             [censusState.SAVED, censusState.SAVED]);
 
 
   // Updates when changing display during `SAVING_CENSUS`
   dispatch(takeSnapshotAndCensus(front, heapWorker));
-  yield waitUntilSnapshotState(store, [states.SAVED_CENSUS, states.SAVED_CENSUS, states.SAVING_CENSUS]);
+  yield waitUntilCensusState(store, snapshot => snapshot.census,
+                             [censusState.SAVED,
+                              censusState.SAVED,
+                              censusState.SAVING]);
   dispatch(setCensusDisplayAndRefresh(heapWorker, censusDisplays.allocationStack));
-  yield waitUntilSnapshotState(store, [states.SAVED_CENSUS, states.SAVED_CENSUS, states.SAVED_CENSUS]);
+  yield waitUntilCensusState(store, snapshot => snapshot.census,
+                             [censusState.SAVED,
+                              censusState.SAVED,
+                              censusState.SAVED]);
 
   equal(getState().snapshots[2].census.display, censusDisplays.allocationStack,
         "Display can be changed while saving census, stores updated display in snapshot");
 
   // Updates census on currently selected snapshot when changing display
   ok(getState().snapshots[2].selected, "Third snapshot currently selected");
   dispatch(setCensusDisplayAndRefresh(heapWorker, censusDisplays.coarseType));
-  yield waitUntilState(store, state => state.snapshots[2].state === states.SAVED_CENSUS);
+  yield waitUntilState(store, state => state.snapshots[2].census.state === censusState.SAVED);
   equal(getState().snapshots[2].census.display, censusDisplays.coarseType,
         "Snapshot census updated when changing displays after already generating one census");
 
   dispatch(setCensusDisplayAndRefresh(heapWorker, censusDisplays.allocationStack));
-  yield waitUntilState(store, state => state.snapshots[2].state === states.SAVED_CENSUS);
+  yield waitUntilState(store, state => state.snapshots[2].census.state === censusState.SAVED);
   equal(getState().snapshots[2].census.display, censusDisplays.allocationStack,
         "Snapshot census updated when changing displays after already generating one census");
 
   // Does not update unselected censuses
   ok(!getState().snapshots[1].selected, "Second snapshot unselected currently");
   equal(getState().snapshots[1].census.display, censusDisplays.coarseType,
         "Second snapshot using `coarseType` display still and not yet updated to correct display");
 
   // Updates to current display when switching to stale snapshot
   dispatch(selectSnapshotAndRefresh(heapWorker, getState().snapshots[1].id));
-  yield waitUntilSnapshotState(store, [states.SAVED_CENSUS, states.SAVING_CENSUS, states.SAVED_CENSUS]);
-  yield waitUntilSnapshotState(store, [states.SAVED_CENSUS, states.SAVED_CENSUS, states.SAVED_CENSUS]);
+  yield waitUntilCensusState(store, snapshot => snapshot.census,
+                             [censusState.SAVED,
+                              censusState.SAVING,
+                              censusState.SAVED]);
+  yield waitUntilCensusState(store, snapshot => snapshot.census,
+                             [censusState.SAVED,
+                              censusState.SAVED,
+                              censusState.SAVED]);
 
   ok(getState().snapshots[1].selected, "Second snapshot selected currently");
   equal(getState().snapshots[1].census.display, censusDisplays.allocationStack,
         "Second snapshot using `allocationStack` display and updated to correct display");
 
   heapWorker.destroy();
   yield front.detach();
 });
--- a/devtools/client/memory/test/unit/test_action-set-display-and-refresh-02.js
+++ b/devtools/client/memory/test/unit/test_action-set-display-and-refresh-02.js
@@ -2,19 +2,20 @@
    http://creativecommons.org/publicdomain/zero/1.0/ */
 "use strict";
 
 /**
  * Tests the task creator `setCensusDisplayAndRefreshAndRefresh()` for custom
  * displays.
  */
 
-let { snapshotState: states } = require("devtools/client/memory/constants");
+let { snapshotState: states, censusState, viewState } = require("devtools/client/memory/constants");
 let { setCensusDisplayAndRefresh } = require("devtools/client/memory/actions/census-display");
 let { takeSnapshotAndCensus } = require("devtools/client/memory/actions/snapshot");
+let { changeView } = require("devtools/client/memory/actions/view");
 
 let CUSTOM = {
   displayName: "Custom",
   tooltip: "Custom tooltip",
   inverted: false,
   breakdown: {
     by: "internalType",
     then: { by: "count", bytes: true, count: false }
@@ -27,22 +28,23 @@ function run_test() {
 
 add_task(function *() {
   let front = new StubbedMemoryFront();
   let heapWorker = new HeapAnalysesClient();
   yield front.attach();
   let store = Store();
   let { getState, dispatch } = store;
 
+  dispatch(changeView(viewState.CENSUS));
   dispatch(setCensusDisplayAndRefresh(heapWorker, CUSTOM));
   equal(getState().censusDisplay, CUSTOM,
         "CUSTOM display stored in display state.");
 
   dispatch(takeSnapshotAndCensus(front, heapWorker));
-  yield waitUntilSnapshotState(store, [states.SAVED_CENSUS]);
+  yield waitUntilCensusState(store, s => s.census, [censusState.SAVED]);
 
   equal(getState().snapshots[0].census.display, CUSTOM,
   "New snapshot stored CUSTOM display when done taking census");
   ok(getState().snapshots[0].census.report.children.length, "Census has some children");
   // Ensure we don't have `count` in any results
   ok(getState().snapshots[0].census.report.children.every(c => !c.count),
      "Census used CUSTOM display without counts");
   // Ensure we do have `bytes` in the results
--- a/devtools/client/memory/test/unit/test_action-set-display.js
+++ b/devtools/client/memory/test/unit/test_action-set-display.js
@@ -3,31 +3,34 @@
 "use strict";
 
 /**
  * Tests the action creator `setCensusDisplay()` for display changing. Does not
  * test refreshing the census information, check `setCensusDisplayAndRefresh`
  * action for that.
  */
 
-let { censusDisplays, snapshotState: states } = require("devtools/client/memory/constants");
+let { censusDisplays, snapshotState: states, censusState, viewState } = require("devtools/client/memory/constants");
 let { setCensusDisplay } = require("devtools/client/memory/actions/census-display");
 let { takeSnapshotAndCensus } = require("devtools/client/memory/actions/snapshot");
+const { changeView } = require("devtools/client/memory/actions/view");
 
 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(changeView(viewState.CENSUS));
+
   // Test default display with no snapshots
   equal(getState().censusDisplay.breakdown.by, "coarseType",
         "default coarseType display selected at start.");
 
   dispatch(setCensusDisplay(censusDisplays.allocationStack));
   equal(getState().censusDisplay.breakdown.by, "allocationStack",
         "display changed with no snapshots");
 
@@ -38,12 +41,12 @@ add_task(function*() {
   } catch (e) {
     ok(true, "Throws when passing in an invalid display object");
   }
   equal(getState().censusDisplay.breakdown.by, "allocationStack",
     "current display unchanged when passing invalid display");
 
   // Test new snapshots
   dispatch(takeSnapshotAndCensus(front, heapWorker));
-  yield waitUntilSnapshotState(store, [states.SAVED_CENSUS]);
+  yield waitUntilCensusState(store, s => s.census, [censusState.SAVED]);
   equal(getState().snapshots[0].census.display, censusDisplays.allocationStack,
         "New snapshots use the current, non-default display");
 });
--- a/devtools/client/memory/test/unit/test_action-take-census.js
+++ b/devtools/client/memory/test/unit/test_action-take-census.js
@@ -1,28 +1,32 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 /**
  * Tests the async reducer responding to the action `takeCensus(heapWorker, snapshot)`
  */
 
-var { snapshotState: states, censusDisplays } = require("devtools/client/memory/constants");
+var { snapshotState: states, censusDisplays, censusState, censusState, viewState } = require("devtools/client/memory/constants");
 var actions = require("devtools/client/memory/actions/snapshot");
+var { changeView } = require("devtools/client/memory/actions/view");
+
 
 function run_test() {
   run_next_test();
 }
 
 add_task(function *() {
   let front = new StubbedMemoryFront();
   let heapWorker = new HeapAnalysesClient();
   yield front.attach();
   let store = Store();
 
+  store.dispatch(changeView(viewState.CENSUS));
+
   store.dispatch(actions.takeSnapshot(front));
   yield waitUntilState(store, () => {
     let snapshots = store.getState().snapshots;
     return snapshots.length === 1 && snapshots[0].state === states.SAVED;
   });
 
   let snapshot = store.getState().snapshots[0];
   equal(snapshot.census, null, "No census data exists yet on the snapshot.");
@@ -32,17 +36,18 @@ add_task(function *() {
   yield waitUntilState(store, () => store.getState().errors.length === 1);
   ok(/Assertion failure/.test(store.getState().errors[0]),
     "Error thrown when taking a census of a snapshot that has not been read.");
 
   store.dispatch(actions.readSnapshot(heapWorker, snapshot.id));
   yield waitUntilState(store, () => store.getState().snapshots[0].state === states.READ);
 
   store.dispatch(actions.takeCensus(heapWorker, snapshot.id));
-  yield waitUntilState(store, () => store.getState().snapshots[0].state === states.SAVING_CENSUS);
-  yield waitUntilState(store, () => store.getState().snapshots[0].state === states.SAVED_CENSUS);
+  yield waitUntilCensusState(store, s => s.census, [censusState.SAVING]);
+  yield waitUntilCensusState(store, s => s.census, [censusState.SAVED]);
 
   snapshot = store.getState().snapshots[0];
   ok(snapshot.census, "Snapshot has census after saved census");
   ok(snapshot.census.report.children.length, "Census is in tree node form");
   equal(snapshot.census.display, censusDisplays.coarseType,
         "Snapshot stored correct display used for the census");
+
 });
--- a/devtools/client/memory/test/unit/test_action-take-snapshot-and-census.js
+++ b/devtools/client/memory/test/unit/test_action-take-snapshot-and-census.js
@@ -1,46 +1,58 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 /**
  * Tests the task creator `takeSnapshotAndCensus()` for the whole flow of
  * taking a snapshot, and its sub-actions.
  */
 
-let { snapshotState: states } = require("devtools/client/memory/constants");
+let { snapshotState: states, treeMapState } = require("devtools/client/memory/constants");
 let actions = 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 i = 0;
-  let expected = ["SAVING", "SAVED", "READING", "READ", "SAVING_CENSUS", "SAVED_CENSUS"];
+  let snapshotI = 0;
+  let censusI = 0;
+  let snapshotStates = ["SAVING", "SAVED", "READING", "READ"];
+  let censusStates = ["SAVING", "SAVED"];
   let expectStates = () => {
-    if (i >= expected.length) { return false; }
+    let snapshot = store.getState().snapshots[0];
+    if (!snapshot) {
+      return;
+    }
+    if (snapshotI < snapshotStates.length) {
+      let isCorrectState = snapshot.state === states[snapshotStates[snapshotI]];
+      if (isCorrectState) {
+        ok(true, `Found expected snapshot state ${snapshotStates[snapshotI]}`);
+        snapshotI++;
+      }
+    }
+    if (snapshot.treeMap && censusI < censusStates.length) {
+      if (snapshot.treeMap.state === treeMapState[censusStates[censusI]]) {
+        ok(true, `Found expected census state ${censusStates[censusI]}`);
+        censusI++;
+      }
+    }
+  };
 
-    let snapshot = store.getState().snapshots[0] || {};
-    let isCorrectState = snapshot.state === states[expected[i]];
-    if (isCorrectState) {
-      ok(true, `Found expected state ${expected[i]}`);
-      i++;
-    }
-    return isCorrectState;
-  };
 
   let unsubscribe = store.subscribe(expectStates);
   store.dispatch(actions.takeSnapshotAndCensus(front, heapWorker));
 
-  yield waitUntilState(store, () => i === 6);
+  yield waitUntilState(store, () => { return snapshotI === snapshotStates.length &&
+                                      censusI === censusStates.length });
   unsubscribe();
 
   ok(true, "takeSnapshotAndCensus() produces the correct sequence of states in a snapshot");
   let snapshot = store.getState().snapshots[0];
-  ok(snapshot.census, "snapshot has census data");
+  ok(snapshot.treeMap, "snapshot has tree map census data");
   ok(snapshot.selected, "snapshot is selected");
 });
--- a/devtools/client/memory/test/unit/test_action-toggle-inverted-and-refresh-01.js
+++ b/devtools/client/memory/test/unit/test_action-toggle-inverted-and-refresh-01.js
@@ -2,76 +2,81 @@
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Test that changing displays with different inverted state properly
 // refreshes the selected census.
 
 const {
   censusDisplays,
   snapshotState: states,
+  censusState,
+  viewState
 } = require("devtools/client/memory/constants");
 const {
   setCensusDisplayAndRefresh
 } = require("devtools/client/memory/actions/census-display");
 const {
   takeSnapshotAndCensus,
   selectSnapshotAndRefresh,
 } = require("devtools/client/memory/actions/snapshot");
+const { changeView } = require("devtools/client/memory/actions/view");
 
 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(changeView(viewState.CENSUS));
+
   // Select a non-inverted display.
   dispatch(setCensusDisplayAndRefresh(heapWorker, censusDisplays.allocationStack));
   equal(getState().censusDisplay.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]);
+  yield waitUntilCensusState(store, s => s.census, [censusState.SAVED,
+                                                    censusState.SAVED,
+                                                    censusState.SAVED]);
   ok(true, "saved 3 snapshots and took a census of each of them");
 
   // Select an inverted display.
   dispatch(setCensusDisplayAndRefresh(heapWorker, censusDisplays.invertedAllocationStack));
 
-  yield waitUntilSnapshotState(store, [states.SAVED_CENSUS,
-                                       states.SAVED_CENSUS,
-                                       states.SAVING_CENSUS]);
+  yield waitUntilCensusState(store, s => s.census, [censusState.SAVED,
+                                                    censusState.SAVED,
+                                                    censusState.SAVING]);
   ok(true, "toggling inverted should recompute the selected snapshot's census");
 
   equal(getState().censusDisplay.inverted, true, "now inverted");
 
-  yield waitUntilSnapshotState(store, [states.SAVED_CENSUS,
-                                       states.SAVED_CENSUS,
-                                       states.SAVED_CENSUS]);
+  yield waitUntilCensusState(store, s => s.census, [censusState.SAVED,
+                                                    censusState.SAVED,
+                                                    censusState.SAVED]);
 
   equal(getState().snapshots[0].census.display.inverted, false);
   equal(getState().snapshots[1].census.display.inverted, false);
   equal(getState().snapshots[2].census.display.inverted, true);
 
   dispatch(selectSnapshotAndRefresh(heapWorker, getState().snapshots[1].id));
-  yield waitUntilSnapshotState(store, [states.SAVED_CENSUS,
-                                       states.SAVING_CENSUS,
-                                       states.SAVED_CENSUS]);
+  yield waitUntilCensusState(store, s => s.census, [censusState.SAVED,
+                                                    censusState.SAVING,
+                                                    censusState.SAVED]);
   ok(true, "selecting non-inverted census should trigger a recompute");
 
-  yield waitUntilSnapshotState(store, [states.SAVED_CENSUS,
-                                       states.SAVED_CENSUS,
-                                       states.SAVED_CENSUS]);
+  yield waitUntilCensusState(store, s => s.census, [censusState.SAVED,
+                                                    censusState.SAVED,
+                                                    censusState.SAVED]);
 
   equal(getState().snapshots[0].census.display.inverted, false);
   equal(getState().snapshots[1].census.display.inverted, true);
   equal(getState().snapshots[2].census.display.inverted, true);
 
   heapWorker.destroy();
   yield front.detach();
 });
--- a/devtools/client/memory/test/unit/test_action-toggle-inverted-and-refresh-02.js
+++ b/devtools/client/memory/test/unit/test_action-toggle-inverted-and-refresh-02.js
@@ -1,50 +1,54 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 "use strict";
 
 // Test that changing inverted state in the middle of taking a snapshot results
 // in an inverted census.
 
-const { censusDisplays, snapshotState: states } = require("devtools/client/memory/constants");
+const { censusDisplays, snapshotState: states, censusState, viewState } = require("devtools/client/memory/constants");
 const { takeSnapshotAndCensus } = require("devtools/client/memory/actions/snapshot");
 const { setCensusDisplayAndRefresh } = require("devtools/client/memory/actions/census-display");
+const { changeView } = require("devtools/client/memory/actions/view");
 
 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(changeView(viewState.CENSUS));
+
   dispatch(setCensusDisplayAndRefresh(heapWorker, censusDisplays.allocationStack));
   equal(getState().censusDisplay.inverted, false);
 
   dispatch(takeSnapshotAndCensus(front, heapWorker));
   yield waitUntilSnapshotState(store, [states.SAVING]);
 
   dispatch(setCensusDisplayAndRefresh(heapWorker, censusDisplays.invertedAllocationStack));
 
-  yield waitUntilSnapshotState(store, [states.SAVED_CENSUS]);
+  yield waitUntilCensusState(store, s => s.census, [censusState.SAVED]);
+
   ok(getState().censusDisplay.inverted,
      "should want inverted trees");
   ok(getState().snapshots[0].census.display.inverted,
      "snapshot-we-were-in-the-middle-of-saving's census should be inverted");
 
   dispatch(setCensusDisplayAndRefresh(heapWorker, censusDisplays.allocationStack));
-  yield waitUntilSnapshotState(store, [states.SAVING_CENSUS]);
+  yield waitUntilCensusState(store, s => s.census, [censusState.SAVING]);
   ok(true, "toggling inverted retriggers census");
   ok(!getState().censusDisplay.inverted, "no longer inverted");
 
   dispatch(setCensusDisplayAndRefresh(heapWorker, censusDisplays.invertedAllocationStack));
-  yield waitUntilSnapshotState(store, [states.SAVED_CENSUS]);
+  yield waitUntilCensusState(store, s => s.census, [censusState.SAVED]);
   ok(getState().censusDisplay.inverted, "inverted again");
   ok(getState().snapshots[0].census.display.inverted,
      "census-we-were-in-the-middle-of-recomputing should be inverted again");
 
   heapWorker.destroy();
   yield front.detach();
 });
--- a/devtools/client/memory/test/unit/test_action_diffing_02.js
+++ b/devtools/client/memory/test/unit/test_action_diffing_02.js
@@ -1,36 +1,39 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Test that toggling diffing unselects all snapshots.
 
-const { snapshotState } = require("devtools/client/memory/constants");
+const { snapshotState, censusState, viewState } = require("devtools/client/memory/constants");
 const { toggleDiffing } = require("devtools/client/memory/actions/diffing");
 const { takeSnapshotAndCensus } = require("devtools/client/memory/actions/snapshot");
+const { changeView } = require("devtools/client/memory/actions/view");
 
 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;
 
+  dispatch(changeView(viewState.CENSUS));
+
   equal(getState().diffing, null, "not diffing by default");
 
   dispatch(takeSnapshotAndCensus(front, heapWorker));
   dispatch(takeSnapshotAndCensus(front, heapWorker));
   dispatch(takeSnapshotAndCensus(front, heapWorker));
-  yield waitUntilSnapshotState(store, [snapshotState.SAVED_CENSUS,
-                                       snapshotState.SAVED_CENSUS,
-                                       snapshotState.SAVED_CENSUS]);
+  yield waitUntilCensusState(store, s => s.census, [censusState.SAVED,
+                                                    censusState.SAVED,
+                                                    censusState.SAVED]);
 
   ok(getState().snapshots.some(s => s.selected),
      "One of the new snapshots is selected");
 
   dispatch(toggleDiffing());
   ok(getState().diffing, "now diffing after toggling");
 
   for (let s of getState().snapshots) {
--- a/devtools/client/memory/test/unit/test_action_diffing_03.js
+++ b/devtools/client/memory/test/unit/test_action_diffing_03.js
@@ -1,36 +1,39 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Test selecting snapshots for diffing.
 
-const { diffingState, snapshotState } = require("devtools/client/memory/constants");
+const { diffingState, snapshotState, viewState } = require("devtools/client/memory/constants");
 const {
   toggleDiffing,
   selectSnapshotForDiffing
 } = require("devtools/client/memory/actions/diffing");
 const { takeSnapshot } = require("devtools/client/memory/actions/snapshot");
+const { changeView } = require("devtools/client/memory/actions/view");
 
 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;
 
+  dispatch(changeView(viewState.CENSUS));
   equal(getState().diffing, null, "not diffing by default");
 
   dispatch(takeSnapshot(front, heapWorker));
   dispatch(takeSnapshot(front, heapWorker));
   dispatch(takeSnapshot(front, heapWorker));
+
   yield waitUntilSnapshotState(store, [snapshotState.SAVED,
                                        snapshotState.SAVED,
                                        snapshotState.SAVED]);
   dispatch(takeSnapshot(front));
 
   // Start diffing.
   dispatch(toggleDiffing());
   ok(getState().diffing, "now diffing after toggling");
--- a/devtools/client/memory/test/unit/test_action_diffing_04.js
+++ b/devtools/client/memory/test/unit/test_action_diffing_04.js
@@ -1,36 +1,39 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Test that we compute census diffs.
 
 const {
   diffingState,
-  snapshotState
+  snapshotState,
+  viewState
 } = require("devtools/client/memory/constants");
 const {
   toggleDiffing,
   selectSnapshotForDiffingAndRefresh
 } = require("devtools/client/memory/actions/diffing");
 const {
   takeSnapshot,
   readSnapshot
 } = require("devtools/client/memory/actions/snapshot");
+const { changeView } = require("devtools/client/memory/actions/view");
 
 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;
+  dispatch(changeView(viewState.CENSUS));
 
   equal(getState().diffing, null, "not diffing by default");
 
   const s1 = yield dispatch(takeSnapshot(front, heapWorker));
   const s2 = yield dispatch(takeSnapshot(front, heapWorker));
   const s3 = yield dispatch(takeSnapshot(front, heapWorker));
   dispatch(readSnapshot(heapWorker, s1));
   dispatch(readSnapshot(heapWorker, s2));
--- a/devtools/client/memory/test/unit/test_action_diffing_05.js
+++ b/devtools/client/memory/test/unit/test_action_diffing_05.js
@@ -2,42 +2,45 @@
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Test that we recompute census diffs at the appropriate times.
 
 const {
   diffingState,
   snapshotState,
   censusDisplays,
+  viewState,
 } = require("devtools/client/memory/constants");
 const {
   setCensusDisplayAndRefresh,
 } = require("devtools/client/memory/actions/census-display");
 const {
   toggleDiffing,
   selectSnapshotForDiffingAndRefresh,
 } = require("devtools/client/memory/actions/diffing");
 const {
   setFilterStringAndRefresh,
 } = require("devtools/client/memory/actions/filter");
 const {
   takeSnapshot,
   readSnapshot,
 } = require("devtools/client/memory/actions/snapshot");
+const { changeView } = require("devtools/client/memory/actions/view");
 
 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;
+  dispatch(changeView(viewState.CENSUS));
 
   yield dispatch(setCensusDisplayAndRefresh(heapWorker,
                                         censusDisplays.allocationStack));
   equal(getState().censusDisplay.inverted, false,
         "not inverted at start");
 
   equal(getState().diffing, null, "not diffing by default");
 
--- a/devtools/client/memory/test/unit/test_dominator_trees_01.js
+++ b/devtools/client/memory/test/unit/test_dominator_trees_01.js
@@ -1,16 +1,17 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Test that we can compute and fetch the dominator tree for a snapshot.
 
 let {
   snapshotState: states,
   dominatorTreeState,
+  treeMapState,
 } = require("devtools/client/memory/constants");
 let {
   takeSnapshotAndCensus,
   computeAndFetchDominatorTree,
 } = require("devtools/client/memory/actions/snapshot");
 
 function run_test() {
   run_next_test();
@@ -19,17 +20,17 @@ function run_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]);
+  yield waitUntilCensusState(store, s => s.treeMap, [treeMapState.SAVED]);
   ok(!getState().snapshots[0].dominatorTree,
      "There shouldn't be a dominator tree model yet since it is not computed " +
      "until we switch to the dominators view.");
 
   // Change to the dominator tree view.
   dispatch(computeAndFetchDominatorTree(heapWorker, getState().snapshots[0].id));
   ok(getState().snapshots[0].dominatorTree,
      "Should now have a dominator tree model for the selected snapshot");
--- a/devtools/client/memory/test/unit/test_dominator_trees_02.js
+++ b/devtools/client/memory/test/unit/test_dominator_trees_02.js
@@ -3,16 +3,17 @@
 
 // Test that selecting the dominator tree view automatically kicks off fetching
 // and computing dominator trees.
 
 const {
   snapshotState: states,
   dominatorTreeState,
   viewState,
+  treeMapState,
 } = require("devtools/client/memory/constants");
 const {
   takeSnapshotAndCensus,
 } = require("devtools/client/memory/actions/snapshot");
 const {
   changeViewAndRefresh
 } = require("devtools/client/memory/actions/view");
 
@@ -23,17 +24,17 @@ function run_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]);
+  yield waitUntilCensusState(store, s => s.treeMap, [treeMapState.SAVED]);
   ok(!getState().snapshots[0].dominatorTree,
      "There shouldn't be a dominator tree model yet since it is not computed " +
      "until we switch to the dominators view.");
 
   dispatch(changeViewAndRefresh(viewState.DOMINATOR_TREE, heapWorker));
   ok(getState().snapshots[0].dominatorTree,
      "Should now have a dominator tree model for the selected snapshot");
 
--- a/devtools/client/memory/test/unit/test_dominator_trees_04.js
+++ b/devtools/client/memory/test/unit/test_dominator_trees_04.js
@@ -22,17 +22,17 @@ function run_test() {
 
 add_task(function *() {
   let front = new StubbedMemoryFront();
   let heapWorker = new HeapAnalysesClient();
   yield front.attach();
 
   for (let intermediateSnapshotState of [states.SAVING,
                                          states.READING,
-                                         states.SAVING_CENSUS]) {
+                                         states.READ]) {
     dumpn(`Testing switching to the DOMINATOR_TREE view in the middle of the ${intermediateSnapshotState} snapshot state`);
 
     let store = Store();
     let { getState, dispatch } = store;
 
     dispatch(takeSnapshotAndCensus(front, heapWorker));
     yield waitUntilSnapshotState(store, [intermediateSnapshotState]);
 
--- a/devtools/client/memory/test/unit/test_dominator_trees_05.js
+++ b/devtools/client/memory/test/unit/test_dominator_trees_05.js
@@ -3,16 +3,17 @@
 
 // Test that changing the currently selected snapshot to a snapshot that does
 // not have a dominator tree will automatically compute and fetch one for it.
 
 let {
   snapshotState: states,
   dominatorTreeState,
   viewState,
+  treeMapState,
 } = require("devtools/client/memory/constants");
 let {
   takeSnapshotAndCensus,
   selectSnapshotAndRefresh,
 } = require("devtools/client/memory/actions/snapshot");
 
 let { changeView } = require("devtools/client/memory/actions/view");
 
@@ -24,18 +25,18 @@ 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));
   dispatch(takeSnapshotAndCensus(front, heapWorker));
-  yield waitUntilSnapshotState(store, [states.SAVED_CENSUS,
-                                       states.SAVED_CENSUS]);
+  yield waitUntilCensusState(store, s => s.treeMap, [treeMapState.SAVED,
+                                                    treeMapState.SAVED]);
 
   ok(getState().snapshots[1].selected, "The second snapshot is selected");
 
   // Change to the dominator tree view.
   dispatch(changeView(viewState.DOMINATOR_TREE));
 
   // Wait for the dominator tree to finish being fetched.
   yield waitUntilState(store, state =>
--- a/devtools/client/memory/test/unit/test_dominator_trees_08.js
+++ b/devtools/client/memory/test/unit/test_dominator_trees_08.js
@@ -4,16 +4,17 @@
 // Test that we can change the display with which we describe a dominator tree
 // and that the dominator tree is re-fetched.
 
 const {
   snapshotState: states,
   dominatorTreeState,
   viewState,
   dominatorTreeDisplays,
+  treeMapState
 } = require("devtools/client/memory/constants");
 const {
   setDominatorTreeDisplayAndRefresh
 } = require("devtools/client/memory/actions/dominator-tree-display");
 const {
   changeView,
 } = require("devtools/client/memory/actions/view");
 const {
@@ -30,17 +31,17 @@ add_task(function *() {
   let heapWorker = new HeapAnalysesClient();
   yield front.attach();
   let store = Store();
   let { getState, dispatch } = store;
 
   dispatch(changeView(viewState.DOMINATOR_TREE));
 
   dispatch(takeSnapshotAndCensus(front, heapWorker));
-  yield waitUntilSnapshotState(store, [states.SAVED_CENSUS]);
+  yield waitUntilCensusState(store, s => s.treeMap, [treeMapState.SAVED]);
   ok(!getState().snapshots[0].dominatorTree,
      "There shouldn't be a dominator tree model yet since it is not computed " +
      "until we switch to the dominators view.");
 
   // Wait for the dominator tree to finish being fetched.
   yield waitUntilState(store, state =>
     state.snapshots[0] &&
     state.snapshots[0].dominatorTree &&
--- a/devtools/client/memory/test/unit/test_dominator_trees_09.js
+++ b/devtools/client/memory/test/unit/test_dominator_trees_09.js
@@ -4,16 +4,17 @@
 // Test that we can change the display with which we describe a dominator tree
 // while the dominator tree is in the middle of being fetched.
 
 const {
   snapshotState: states,
   dominatorTreeState,
   viewState,
   dominatorTreeDisplays,
+  treeMapState,
 } = require("devtools/client/memory/constants");
 const {
   setDominatorTreeDisplayAndRefresh
 } = require("devtools/client/memory/actions/dominator-tree-display");
 const {
   changeView,
 } = require("devtools/client/memory/actions/view");
 const {
@@ -30,17 +31,17 @@ add_task(function *() {
   let heapWorker = new HeapAnalysesClient();
   yield front.attach();
   let store = Store();
   let { getState, dispatch } = store;
 
   dispatch(changeView(viewState.DOMINATOR_TREE));
 
   dispatch(takeSnapshotAndCensus(front, heapWorker));
-  yield waitUntilSnapshotState(store, [states.SAVED_CENSUS]);
+  yield waitUntilCensusState(store, s => s.treeMap, [treeMapState.SAVED]);
   ok(!getState().snapshots[0].dominatorTree,
      "There shouldn't be a dominator tree model yet since it is not computed " +
      "until we switch to the dominators view.");
 
   // Wait for the dominator tree to start fetching.
   yield waitUntilState(store, state =>
     state.snapshots[0] &&
     state.snapshots[0].dominatorTree &&
new file mode 100644
--- /dev/null
+++ b/devtools/client/memory/test/unit/test_tree-map-01.js
@@ -0,0 +1,57 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { drawBox } = require("devtools/client/memory/components/tree-map/draw");
+
+function run_test() {
+  run_next_test();
+}
+
+add_task(function *() {
+  let fillRectValues, strokeRectValues;
+  let ctx = {
+    fillRect: (...args) => fillRectValues = args,
+    strokeRect: (...args) => strokeRectValues = args
+  };
+  let node = {
+    x: 20,
+    y: 30,
+    dx: 50,
+    dy: 70,
+    type: "other",
+    depth: 2
+  };
+  let borderWidth = () => 1;
+  let dragZoom = {
+    offsetX: 0,
+    offsetY: 0,
+    zoom: 0
+  };
+
+  drawBox(ctx, node, borderWidth, dragZoom);
+  ok(true, JSON.stringify([ctx, fillRectValues, strokeRectValues]));
+  equal(ctx.fillStyle, "hsl(210,60%,70%)", "The fillStyle is set");
+  equal(ctx.strokeStyle, "hsl(210,60%,35%)", "The strokeStyle is set");
+  equal(ctx.lineWidth, 1, "The lineWidth is set");
+  deepEqual(fillRectValues, [20.5,30.5,49,69], "Draws a filled rectangle");
+  deepEqual(strokeRectValues, [20.5,30.5,49,69], "Draws a stroked rectangle");
+
+
+  dragZoom.zoom = 0.5;
+
+  drawBox(ctx, node, borderWidth, dragZoom);
+  ok(true, JSON.stringify([ctx, fillRectValues, strokeRectValues]));
+  deepEqual(fillRectValues, [30.5,45.5,74,104],
+    "Draws a zoomed filled rectangle");
+  deepEqual(strokeRectValues, [30.5,45.5,74,104],
+    "Draws a zoomed stroked rectangle");
+
+  dragZoom.offsetX = 110;
+  dragZoom.offsetY = 130;
+
+  drawBox(ctx, node, borderWidth, dragZoom);
+  deepEqual(fillRectValues, [-79.5,-84.5,74,104],
+    "Draws a zoomed and offset filled rectangle");
+  deepEqual(strokeRectValues, [-79.5,-84.5,74,104],
+    "Draws a zoomed and offset stroked rectangle");
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/memory/test/unit/test_tree-map-02.js
@@ -0,0 +1,80 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { drawText } = require("devtools/client/memory/components/tree-map/draw");
+
+function run_test() {
+  run_next_test();
+}
+
+add_task(function*() {
+  // Mock out the Canvas2dContext
+  let ctx = {
+    fillText: (...args) => fillTextValues.push(args),
+    measureText: (text) => {
+      let width = text ? text.length * 10 : 0;
+      return { width };
+    }
+  };
+  let node = {
+    x: 20,
+    y: 30,
+    dx: 500,
+    dy: 70,
+    name: "Example Node",
+    totalBytes: 1200,
+    totalCount: 100
+  };
+  let ratio = 0;
+  let borderWidth = () => 1;
+  let dragZoom = {
+    offsetX: 0,
+    offsetY: 0,
+    zoom: 0
+  };
+  let fillTextValues = [];
+
+  drawText(ctx, node, borderWidth, ratio, dragZoom);
+  deepEqual(fillTextValues[0], ["Example Node", 21.5, 31.5],
+    "Fills in the full node name");
+  deepEqual(fillTextValues[1], ["1KiB 100 count", 151.5, 31.5],
+    "Includes the full byte and count information");
+
+  fillTextValues = [];
+  node.dx = 250;
+  drawText(ctx, node, borderWidth, ratio, dragZoom);
+
+  deepEqual(fillTextValues[0], ["Example Node", 21.5, 31.5],
+    "Fills in the full node name");
+  deepEqual(fillTextValues[1], undefined,
+    "Drops off the byte and count information if not enough room");
+
+  fillTextValues = [];
+  node.dx = 100;
+  drawText(ctx, node, borderWidth, ratio, dragZoom);
+
+  deepEqual(fillTextValues[0], ["Exampl...", 21.5, 31.5],
+    "Cuts the name with ellipsis");
+  deepEqual(fillTextValues[1], undefined,
+    "Drops off the byte and count information if not enough room");
+
+  fillTextValues = [];
+  node.dx = 40;
+  drawText(ctx, node, borderWidth, ratio, dragZoom);
+
+  deepEqual(fillTextValues[0], ["...", 21.5, 31.5],
+    "Shows only ellipsis when smaller");
+  deepEqual(fillTextValues[1], undefined,
+    "Drops off the byte and count information if not enough room");
+
+  fillTextValues = [];
+  node.dx = 20;
+  drawText(ctx, node, borderWidth, ratio, dragZoom);
+
+  deepEqual(fillTextValues[0], undefined,
+    "Draw nothing when not enough room");
+  deepEqual(fillTextValues[1], undefined,
+    "Drops off the byte and count information if not enough room");
+});
--- a/devtools/client/memory/test/unit/test_utils-get-snapshot-totals.js
+++ b/devtools/client/memory/test/unit/test_utils-get-snapshot-totals.js
@@ -2,37 +2,40 @@
    http://creativecommons.org/publicdomain/zero/1.0/ */
 "use strict";
 
 /**
  * Tests that we use the correct snapshot aggregate value
  * in `utils.getSnapshotTotals(snapshot)`
  */
 
-const { censusDisplays, snapshotState: states } = require("devtools/client/memory/constants");
+const { censusDisplays, snapshotState: states, viewState, censusState } = require("devtools/client/memory/constants");
 const { getSnapshotTotals } = require("devtools/client/memory/utils");
 const { takeSnapshotAndCensus } = require("devtools/client/memory/actions/snapshot");
 const { setCensusDisplayAndRefresh } = require("devtools/client/memory/actions/census-display");
+const { changeView } = require("devtools/client/memory/actions/view");
 
 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(changeView(viewState.CENSUS));
+
   yield dispatch(setCensusDisplayAndRefresh(heapWorker,
                                             censusDisplays.allocationStack));
 
   dispatch(takeSnapshotAndCensus(front, heapWorker));
-  yield waitUntilSnapshotState(store, [states.SAVED_CENSUS]);
+  yield waitUntilCensusState(store, s => s.census, [censusState.SAVED]);
 
   ok(!getState().snapshots[0].census.display.inverted, "Snapshot is not inverted");
 
   let census = getState().snapshots[0].census;
   let result = aggregate(census.report);
   let totalBytes = result.bytes;
   let totalCount = result.count;
 
@@ -40,18 +43,19 @@ add_task(function *() {
   ok(totalCount > 0, "counted up count in the census");
 
   result = getSnapshotTotals(getState().snapshots[0].census);
   equal(totalBytes, result.bytes, "getSnapshotTotals reuslted in correct bytes");
   equal(totalCount, result.count, "getSnapshotTotals reuslted in correct count");
 
   dispatch(setCensusDisplayAndRefresh(heapWorker,
                                       censusDisplays.invertedAllocationStack));
-  yield waitUntilSnapshotState(store, [states.SAVING_CENSUS]);
-  yield waitUntilSnapshotState(store, [states.SAVED_CENSUS]);
+
+  yield waitUntilCensusState(store, s => s.census, [censusState.SAVING]);
+  yield waitUntilCensusState(store, s => s.census, [censusState.SAVED]);
   ok(getState().snapshots[0].census.display.inverted, "Snapshot is inverted");
 
   result = getSnapshotTotals(getState().snapshots[0].census);
   equal(totalBytes, result.bytes,
         "getSnapshotTotals reuslted in correct bytes when inverted");
   equal(totalCount, result.count,
         "getSnapshotTotals reuslted in correct count when inverted");
 });
--- a/devtools/client/memory/test/unit/test_utils.js
+++ b/devtools/client/memory/test/unit/test_utils.js
@@ -46,9 +46,25 @@ add_task(function *() {
     "formatNumber can display number sign");
   equal(utils.formatNumber(-12, true), "-12",
     "formatNumber can display number sign (negative)");
 
   ok(true, "test formatPercent util functions");
   equal(utils.formatPercent(12), "12%", "formatPercent returns 12% for 12");
   equal(utils.formatPercent(12345), "12 345%",
     "formatPercent returns 12 345% for 12345");
+
+  equal(utils.formatAbbreviatedBytes(12), "12B", "Formats bytes");
+  equal(utils.formatAbbreviatedBytes(12345), "12KiB", "Formats kilobytes");
+  equal(utils.formatAbbreviatedBytes(12345678), "11MiB", "Formats megabytes");
+  equal(utils.formatAbbreviatedBytes(12345678912), "11GiB", "Formats gigabytes");
+
+  equal(utils.hslToStyle(0.5, 0.6, 0.7),
+    "hsl(180,60%,70%)", "hslToStyle converts an array to a style string");
+  equal(utils.hslToStyle(0, 0, 0),
+    "hsl(0,0%,0%)", "hslToStyle converts an array to a style string");
+  equal(utils.hslToStyle(1, 1, 1),
+    "hsl(360,100%,100%)", "hslToStyle converts an array to a style string");
+
+  equal(utils.lerp(5, 7, 0), 5, "lerp return first number for 0");
+  equal(utils.lerp(5, 7, 1), 7, "lerp return second number for 1");
+  equal(utils.lerp(5, 7, 0.5), 6, "lerp interpolates the numbers for 0.5");
 });
--- a/devtools/client/memory/test/unit/xpcshell.ini
+++ b/devtools/client/memory/test/unit/xpcshell.ini
@@ -37,10 +37,12 @@ skip-if = toolkit == 'android' || toolki
 [test_dominator_trees_02.js]
 [test_dominator_trees_03.js]
 [test_dominator_trees_04.js]
 [test_dominator_trees_05.js]
 [test_dominator_trees_06.js]
 [test_dominator_trees_07.js]
 [test_dominator_trees_08.js]
 [test_dominator_trees_09.js]
+[test_tree-map-01.js]
+[test_tree-map-02.js]
 [test_utils.js]
 [test_utils-get-snapshot-totals.js]
--- a/devtools/client/memory/utils.js
+++ b/devtools/client/memory/utils.js
@@ -8,20 +8,26 @@ const { LocalizationHelper } = require("
 const STRINGS_URI = "chrome://devtools/locale/memory.properties"
 const L10N = exports.L10N = new LocalizationHelper(STRINGS_URI);
 
 const { OS } = require("resource://gre/modules/osfile.jsm");
 const { assert } = require("devtools/shared/DevToolsUtils");
 const { Preferences } = require("resource://gre/modules/Preferences.jsm");
 const CUSTOM_CENSUS_DISPLAY_PREF = "devtools.memory.custom-census-displays";
 const CUSTOM_DOMINATOR_TREE_DISPLAY_PREF = "devtools.memory.custom-dominator-tree-displays";
+const CUSTOM_TREE_MAP_DISPLAY_PREF = "devtools.memory.custom-tree-map-displays";
+const BYTES = 1024;
+const KILOBYTES = Math.pow(BYTES, 2);
+const MEGABYTES = Math.pow(BYTES, 3);
 const DevToolsUtils = require("devtools/shared/DevToolsUtils");
 const {
   snapshotState: states,
   diffingState,
+  censusState,
+  treeMapState,
   censusDisplays,
   dominatorTreeDisplays,
   dominatorTreeState
 } = require("./constants");
 
 /**
  * Takes a snapshot object and returns the localized form of its timestamp to be
  * used as a title.
@@ -75,16 +81,26 @@ exports.getCustomCensusDisplays = functi
  *
  * @return {Object}
  */
 exports.getCustomDominatorTreeDisplays = function () {
   return getCustomDisplaysHelper(CUSTOM_DOMINATOR_TREE_DISPLAY_PREF);
 };
 
 /**
+ * Returns custom displays defined in
+ * `devtools.memory.custom-tree-map-displays` pref.
+ *
+ * @return {Object}
+ */
+exports.getCustomTreeMapDisplays = function () {
+  return getCustomDisplaysHelper(CUSTOM_TREE_MAP_DISPLAY_PREF);
+};
+
+/**
  * Returns a string representing a readable form of the snapshot's state. More
  * concise than `getStatusTextFull`.
  *
  * @param {snapshotState | diffingState} state
  * @return {String}
  */
 exports.getStatusText = function (state) {
   assert(state, "Must have a state");
@@ -101,19 +117,22 @@ exports.getStatusText = function (state)
 
     case states.IMPORTING:
       return L10N.getStr("snapshot.state.importing");
 
     case states.SAVED:
     case states.READING:
       return L10N.getStr("snapshot.state.reading");
 
-    case states.SAVING_CENSUS:
+    case censusState.SAVING:
       return L10N.getStr("snapshot.state.saving-census");
 
+    case treeMapState.SAVING:
+      return L10N.getStr("snapshot.state.saving-tree-map");
+
     case diffingState.TAKING_DIFF:
       return L10N.getStr("diffing.state.taking-diff");
 
     case diffingState.SELECTING:
       return L10N.getStr("diffing.state.selecting");
 
     case dominatorTreeState.COMPUTING:
       return L10N.getStr("dominatorTree.state.computing");
@@ -128,17 +147,18 @@ exports.getStatusText = function (state)
     case dominatorTreeState.ERROR:
       return L10N.getStr("dominatorTree.state.error");
 
     // These states do not have any message to show as other content will be
     // displayed.
     case dominatorTreeState.LOADED:
     case diffingState.TOOK_DIFF:
     case states.READ:
-    case states.SAVED_CENSUS:
+    case censusState.SAVED:
+    case treeMapState.SAVED:
       return "";
 
     default:
       assert(false, `Unexpected state: ${state}`);
       return "";
   }
 };
 
@@ -164,19 +184,22 @@ exports.getStatusTextFull = function (st
 
     case states.IMPORTING:
       return L10N.getStr("snapshot.state.importing");
 
     case states.SAVED:
     case states.READING:
       return L10N.getStr("snapshot.state.reading.full");
 
-    case states.SAVING_CENSUS:
+    case censusState.SAVING:
       return L10N.getStr("snapshot.state.saving-census.full");
 
+    case treeMapState.SAVING:
+      return L10N.getStr("snapshot.state.saving-tree-map.full");
+
     case diffingState.TAKING_DIFF:
       return L10N.getStr("diffing.state.taking-diff.full");
 
     case diffingState.SELECTING:
       return L10N.getStr("diffing.state.selecting.full");
 
     case dominatorTreeState.COMPUTING:
       return L10N.getStr("dominatorTree.state.computing.full");
@@ -191,34 +214,35 @@ exports.getStatusTextFull = function (st
     case dominatorTreeState.ERROR:
       return L10N.getStr("dominatorTree.state.error.full");
 
     // These states do not have any full message to show as other content will
     // be displayed.
     case dominatorTreeState.LOADED:
     case diffingState.TOOK_DIFF:
     case states.READ:
-    case states.SAVED_CENSUS:
+    case censusState.SAVED:
+    case treeMapState.SAVED:
       return "";
 
     default:
       assert(false, `Unexpected state: ${state}`);
       return "";
   }
 };
 
 /**
  * Return true if the snapshot is in a diffable state, false otherwise.
  *
  * @param {snapshotModel} snapshot
  * @returns {Boolean}
  */
 exports.snapshotIsDiffable = function snapshotIsDiffable(snapshot) {
-  return snapshot.state === states.SAVED_CENSUS
-    || snapshot.state === states.SAVING_CENSUS
+  return (snapshot.census && snapshot.census.state === censusState.SAVED)
+    || (snapshot.census && snapshot.census.state === censusState.SAVING)
     || snapshot.state === states.SAVED
     || snapshot.state === states.READ;
 };
 
 /**
  * Takes an array of snapshots and a snapshot and returns
  * the snapshot instance in `snapshots` that matches
  * the snapshot passed in.
@@ -251,16 +275,17 @@ exports.createSnapshot = function create
     });
   }
 
   return Object.freeze({
     id: ++ID_COUNTER,
     state: states.SAVING,
     dominatorTree,
     census: null,
+    treeMap: null,
     path: null,
     imported: false,
     selected: false,
     error: null,
   });
 };
 
 /**
@@ -270,35 +295,67 @@ exports.createSnapshot = function create
  * @param {String} filter
  * @param {censusDisplayModel} display
  * @param {censusModel} census
  *
  * @returns {Boolean}
  */
 exports.censusIsUpToDate = function (filter, display, census) {
   return census
-      && filter === census.filter
+      // Filter could be null == undefined so use loose equality.
+      && filter == census.filter
       && display === census.display;
 };
 
+
+/**
+ * Check to see if the snapshot is in a state that it can take a census.
+ *
+ * @param {SnapshotModel} A snapshot to check.
+ * @param {Boolean} Assert that the snapshot must be in a ready state.
+ * @returns {Boolean}
+ */
+exports.canTakeCensus = function (snapshot) {
+  return snapshot.state === states.READ &&
+    (!snapshot.census || snapshot.census.state === censusState.SAVED) &&
+    (!snapshot.treeMap || snapshot.treeMap.state === treeMapState.SAVED);
+};
+
 /**
  * Returns true if the given snapshot's dominator tree has been computed, false
  * otherwise.
  *
  * @param {SnapshotModel} snapshot
  * @returns {Boolean}
  */
 exports.dominatorTreeIsComputed = function (snapshot) {
   return snapshot.dominatorTree &&
     (snapshot.dominatorTree.state === dominatorTreeState.COMPUTED ||
      snapshot.dominatorTree.state === dominatorTreeState.LOADED ||
      snapshot.dominatorTree.state === dominatorTreeState.INCREMENTAL_FETCHING);
 };
 
 /**
+ * Find the first SAVED census, either from the tree map or the normal
+ * census.
+ *
+ * @param {SnapshotModel} snapshot
+ * @returns {Object|null} Either the census, or null if one hasn't completed
+ */
+exports.getSavedCensus = function (snapshot) {
+  if (snapshot.treeMap && snapshot.treeMap.state === treeMapState.SAVED) {
+    return snapshot.treeMap;
+  }
+  if (snapshot.census && snapshot.census.state === censusState.SAVED) {
+    return snapshot.census;
+  }
+  return null;
+};
+
+/**
  * Takes a snapshot and returns the total bytes and total count that this
  * snapshot represents.
  *
  * @param {CensusModel} census
  * @return {Object}
  */
 exports.getSnapshotTotals = function (census) {
   let bytes = 0;
@@ -389,8 +446,59 @@ exports.formatNumber = function(number, 
  *
  * @param {Number} percent
  * @param {Boolean} showSign (defaults to false)
  */
 exports.formatPercent = function(percent, showSign = false) {
   return exports.L10N.getFormatStr("tree-item.percent",
                            exports.formatNumber(percent, showSign));
 };
+
+/**
+ * Change an HSL color array with values ranged 0-1 to a properly formatted
+ * ctx.fillStyle string.
+ *
+ * @param  {Number} h
+ *         hue values ranged between [0 - 1]
+ * @param  {Number} s
+ *         hue values ranged between [0 - 1]
+ * @param  {Number} l
+ *         hue values ranged between [0 - 1]
+ * @return {type}
+ */
+exports.hslToStyle = function(h, s, l) {
+  h = parseInt(h * 360, 10);
+  s = parseInt(s * 100, 10);
+  l = parseInt(l * 100, 10);
+
+  return `hsl(${h},${s}%,${l}%)`;
+};
+
+/**
+ * Linearly interpolate between 2 numbers.
+ *
+ * @param {Number} a
+ * @param {Number} b
+ * @param {Number} t
+ *        A value of 0 returns a, and 1 returns b
+ * @return {Number}
+ */
+exports.lerp = function(a, b, t) {
+  return a * (1 - t) + b * t;
+};
+
+/**
+ * Format a number of bytes as human readable, e.g. 13434 => '13KiB'.
+ *
+ * @param  {Number} n
+ *         Number of bytes
+ * @return {String}
+ */
+exports.formatAbbreviatedBytes = function(n) {
+  if (n < BYTES) {
+    return n + "B";
+  } else if (n < KILOBYTES) {
+    return Math.floor(n / BYTES) + "KiB";
+  } else if (n < MEGABYTES) {
+    return Math.floor(n / KILOBYTES) + "MiB";
+  }
+  return Math.floor(n / MEGABYTES) + "GiB";
+};
--- a/devtools/client/preferences/devtools.js
+++ b/devtools/client/preferences/devtools.js
@@ -104,16 +104,17 @@ pref("devtools.debugger.ui.variables-sor
 pref("devtools.debugger.ui.variables-only-enum-visible", false);
 pref("devtools.debugger.ui.variables-searchbox-visible", false);
 
 // Enable the Memory tools
 pref("devtools.memory.enabled", false);
 
 pref("devtools.memory.custom-census-displays", "{}");
 pref("devtools.memory.custom-dominator-tree-displays", "{}");
+pref("devtools.memory.custom-tree-map-displays", "{}");
 
 // Enable the Performance tools
 pref("devtools.performance.enabled", true);
 
 // The default Performance UI settings
 pref("devtools.performance.memory.sample-probability", "0.05");
 // Can't go higher than this without causing internal allocation overflows while
 // serializing the allocations data over the RDP.
--- a/devtools/client/themes/memory.css
+++ b/devtools/client/themes/memory.css
@@ -490,16 +490,29 @@ html, body, #app, #memory-tool {
 }
 
 .heap-tree-item.focused .heap-tree-number,
 .heap-tree-item.focused .heap-tree-percent {
   color: inherit;
 }
 
 /**
+ * Tree map
+ */
+
+.tree-map-container {
+  width: 100%;
+  height: 100%;
+  position: relative;
+  overflow: hidden;
+  margin-top: -1em;
+  padding-bottom: 1em;
+}
+
+/**
  * Heap tree errors.
  */
 
 .error::before {
   content: "";
   display: inline-block;
   width: 12px;
   height: 12px;
--- a/devtools/docs/memory-panel.md
+++ b/devtools/docs/memory-panel.md
@@ -199,21 +199,25 @@ restricted to within these tasks.
 ### Snapshots State
 
 On the JavaScript side, the snapshots represent a reference to the underlying
 heap dump and the various analyses. The following diagram represents a finite
 state machine describing the snapshot states. Any of these states may go to the
 ERROR state, from which they can never leave.
 
 ```
-SAVING → SAVED → READING → READ    SAVED_CENSUS
-                  ↗             ↘      ↑  ↓
-         IMPORTING                SAVING_CENSUS
+SAVING → SAVED → READING → READ
+                  ↗
+         IMPORTING
 ```
 
+Each of the report types (census, diffing, tree maps, dominators) have their own states as well, and are documented at `devtools/client/memory/constants.js`.
+These report states are updated as the various filtering and selecting options
+are updated in the UI.
+
 ### Testing the Frontend
 
 Unit tests for React components are in `devtools/client/memory/test/chrome/*`.
 
 Unit tests for actions, reducers, and state changes are in
 `devtools/client/memory/test/unit/*`.
 
 Holistic integration tests for the frontend and the whole memory tool are in
--- a/python/mozbuild/mozbuild/artifacts.py
+++ b/python/mozbuild/mozbuild/artifacts.py
@@ -381,20 +381,19 @@ class WinArtifactJob(ArtifactJob):
                              'matched an archive path.'.format(
                                  patterns=self.artifact_patterns))
 
 # Keep the keys of this map in sync with the |mach artifact| --job
 # options.  The keys of this map correspond to entries at
 # https://tools.taskcluster.net/index/artifacts/#buildbot.branches.mozilla-central/buildbot.branches.mozilla-central.
 # The values correpsond to a pair of (<package regex>, <test archive regex>).
 JOB_DETAILS = {
-    # 'android-api-9': (AndroidArtifactJob, 'public/build/fennec-(.*)\.android-arm\.apk'),
-    'android-api-15': (AndroidArtifactJob, ('public/build/fennec-(.*)\.android-arm\.apk',
+    'android-api-15': (AndroidArtifactJob, ('public/build/fennec-(.*)-arm\.apk',
                                             None)),
-    'android-x86': (AndroidArtifactJob, ('public/build/fennec-(.*)\.android-i386\.apk',
+    'android-x86': (AndroidArtifactJob, ('public/build/fennec-(.*)-i386\.apk',
                                          None)),
     'linux': (LinuxArtifactJob, ('public/build/firefox-(.*)\.linux-i686\.tar\.bz2',
                                  'public/build/firefox-(.*)\.common\.tests\.zip')),
     'linux64': (LinuxArtifactJob, ('public/build/firefox-(.*)\.linux-x86_64\.tar\.bz2',
                                    'public/build/firefox-(.*)\.common\.tests\.zip')),
     'macosx64': (MacArtifactJob, ('public/build/firefox-(.*)\.mac\.dmg',
                                   'public/build/firefox-(.*)\.common\.tests\.zip')),
     'win32': (WinArtifactJob, ('public/build/firefox-(.*)\.win32.zip',