summaryrefslogtreecommitdiffstats
path: root/devtools/client/memory/components
diff options
context:
space:
mode:
authorMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
committerMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
commit5f8de423f190bbb79a62f804151bc24824fa32d8 (patch)
tree10027f336435511475e392454359edea8e25895d /devtools/client/memory/components
parent49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff)
downloadUXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.lz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.xz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.zip
Add m-esr52 at 52.6.0
Diffstat (limited to 'devtools/client/memory/components')
-rw-r--r--devtools/client/memory/components/census-header.js72
-rw-r--r--devtools/client/memory/components/census-tree-item.js134
-rw-r--r--devtools/client/memory/components/census.js79
-rw-r--r--devtools/client/memory/components/dominator-tree-header.js44
-rw-r--r--devtools/client/memory/components/dominator-tree-item.js142
-rw-r--r--devtools/client/memory/components/dominator-tree.js216
-rw-r--r--devtools/client/memory/components/heap.js455
-rw-r--r--devtools/client/memory/components/individuals-header.js44
-rw-r--r--devtools/client/memory/components/individuals.js61
-rw-r--r--devtools/client/memory/components/list.js35
-rw-r--r--devtools/client/memory/components/moz.build25
-rw-r--r--devtools/client/memory/components/shortest-paths.js184
-rw-r--r--devtools/client/memory/components/snapshot-list-item.js114
-rw-r--r--devtools/client/memory/components/toolbar.js300
-rw-r--r--devtools/client/memory/components/tree-map.js71
-rw-r--r--devtools/client/memory/components/tree-map/canvas-utils.js134
-rw-r--r--devtools/client/memory/components/tree-map/color-coarse-type.js70
-rw-r--r--devtools/client/memory/components/tree-map/drag-zoom.js316
-rw-r--r--devtools/client/memory/components/tree-map/draw.js295
-rw-r--r--devtools/client/memory/components/tree-map/moz.build12
-rw-r--r--devtools/client/memory/components/tree-map/start.js32
21 files changed, 2835 insertions, 0 deletions
diff --git a/devtools/client/memory/components/census-header.js b/devtools/client/memory/components/census-header.js
new file mode 100644
index 000000000..d897ed132
--- /dev/null
+++ b/devtools/client/memory/components/census-header.js
@@ -0,0 +1,72 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const { DOM: dom, createClass } = require("devtools/client/shared/vendor/react");
+const { L10N } = require("../utils");
+const models = require("../models");
+
+const CensusHeader = module.exports = createClass({
+ displayName: "CensusHeader",
+
+ propTypes: {
+ diffing: models.diffingModel,
+ },
+
+ render() {
+ let individualsCell;
+ if (!this.props.diffing) {
+ individualsCell = dom.span({
+ className: "heap-tree-item-field heap-tree-item-individuals"
+ });
+ }
+
+ return dom.div(
+ {
+ className: "header"
+ },
+
+ dom.span(
+ {
+ className: "heap-tree-item-bytes",
+ title: L10N.getStr("heapview.field.bytes.tooltip"),
+ },
+ L10N.getStr("heapview.field.bytes")
+ ),
+
+ dom.span(
+ {
+ className: "heap-tree-item-count",
+ title: L10N.getStr("heapview.field.count.tooltip"),
+ },
+ L10N.getStr("heapview.field.count")
+ ),
+
+ dom.span(
+ {
+ className: "heap-tree-item-total-bytes",
+ title: L10N.getStr("heapview.field.totalbytes.tooltip"),
+ },
+ L10N.getStr("heapview.field.totalbytes")
+ ),
+
+ dom.span(
+ {
+ className: "heap-tree-item-total-count",
+ title: L10N.getStr("heapview.field.totalcount.tooltip"),
+ },
+ L10N.getStr("heapview.field.totalcount")
+ ),
+
+ individualsCell,
+
+ dom.span(
+ {
+ className: "heap-tree-item-name",
+ title: L10N.getStr("heapview.field.name.tooltip"),
+ },
+ L10N.getStr("heapview.field.name")
+ )
+ );
+ }
+});
diff --git a/devtools/client/memory/components/census-tree-item.js b/devtools/client/memory/components/census-tree-item.js
new file mode 100644
index 000000000..c5d08cefc
--- /dev/null
+++ b/devtools/client/memory/components/census-tree-item.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/. */
+"use strict";
+
+const { isSavedFrame } = require("devtools/shared/DevToolsUtils");
+const { DOM: dom, createClass, createFactory } = require("devtools/client/shared/vendor/react");
+const { L10N, formatNumber, formatPercent } = require("../utils");
+const Frame = createFactory(require("devtools/client/shared/components/frame"));
+const { TREE_ROW_HEIGHT } = require("../constants");
+
+const CensusTreeItem = module.exports = createClass({
+ displayName: "CensusTreeItem",
+
+ shouldComponentUpdate(nextProps, nextState) {
+ return this.props.item != nextProps.item
+ || this.props.depth != nextProps.depth
+ || this.props.expanded != nextProps.expanded
+ || this.props.focused != nextProps.focused
+ || this.props.diffing != nextProps.diffing;
+ },
+
+ render() {
+ let {
+ item,
+ depth,
+ arrow,
+ focused,
+ getPercentBytes,
+ getPercentCount,
+ diffing,
+ onViewSourceInDebugger,
+ onViewIndividuals,
+ inverted,
+ } = this.props;
+
+ const bytes = formatNumber(item.bytes, !!diffing);
+ const percentBytes = formatPercent(getPercentBytes(item.bytes), !!diffing);
+
+ const count = formatNumber(item.count, !!diffing);
+ const percentCount = formatPercent(getPercentCount(item.count), !!diffing);
+
+ const totalBytes = formatNumber(item.totalBytes, !!diffing);
+ const percentTotalBytes = formatPercent(getPercentBytes(item.totalBytes), !!diffing);
+
+ const totalCount = formatNumber(item.totalCount, !!diffing);
+ const percentTotalCount = formatPercent(getPercentCount(item.totalCount), !!diffing);
+
+ let pointer;
+ if (inverted && depth > 0) {
+ pointer = dom.span({ className: "children-pointer" }, "↖");
+ } else if (!inverted && item.children && item.children.length) {
+ pointer = dom.span({ className: "children-pointer" }, "↘");
+ }
+
+ let individualsCell;
+ if (!diffing) {
+ let individualsButton;
+ if (item.reportLeafIndex !== undefined) {
+ individualsButton = dom.button(
+ {
+ key: `individuals-button-${item.id}`,
+ title: L10N.getStr("tree-item.view-individuals.tooltip"),
+ className: "devtools-button individuals-button",
+ onClick: e => {
+ // Don't let the event bubble up to cause this item to focus after
+ // we have switched views, which would lead to assertion failures.
+ e.preventDefault();
+ e.stopPropagation();
+
+ onViewIndividuals(item);
+ },
+ },
+ "⁂"
+ );
+ }
+ individualsCell = dom.span(
+ { className: "heap-tree-item-field heap-tree-item-individuals" },
+ individualsButton
+ );
+ }
+
+ return dom.div(
+ { className: `heap-tree-item ${focused ? "focused" : ""}` },
+ dom.span({ className: "heap-tree-item-field heap-tree-item-bytes" },
+ dom.span({ className: "heap-tree-number" }, bytes),
+ dom.span({ className: "heap-tree-percent" }, percentBytes)),
+ dom.span({ className: "heap-tree-item-field heap-tree-item-count" },
+ dom.span({ className: "heap-tree-number" }, count),
+ dom.span({ className: "heap-tree-percent" }, percentCount)),
+ dom.span({ className: "heap-tree-item-field heap-tree-item-total-bytes" },
+ dom.span({ className: "heap-tree-number" }, totalBytes),
+ dom.span({ className: "heap-tree-percent" }, percentTotalBytes)),
+ dom.span({ className: "heap-tree-item-field heap-tree-item-total-count" },
+ dom.span({ className: "heap-tree-number" }, totalCount),
+ dom.span({ className: "heap-tree-percent" }, percentTotalCount)),
+ individualsCell,
+ dom.span(
+ {
+ className: "heap-tree-item-field heap-tree-item-name",
+ style: { marginInlineStart: depth * TREE_ROW_HEIGHT }
+ },
+ arrow,
+ pointer,
+ this.toLabel(item.name, onViewSourceInDebugger)
+ )
+ );
+ },
+
+ toLabel(name, linkToDebugger) {
+ if (isSavedFrame(name)) {
+ return Frame({
+ frame: name,
+ onClick: () => linkToDebugger(name),
+ showFunctionName: true,
+ showHost: true,
+ });
+ }
+
+ if (name === null) {
+ return L10N.getStr("tree-item.root");
+ }
+
+ if (name === "noStack") {
+ return L10N.getStr("tree-item.nostack");
+ }
+
+ if (name === "noFilename") {
+ return L10N.getStr("tree-item.nofilename");
+ }
+
+ return String(name);
+ },
+});
diff --git a/devtools/client/memory/components/census.js b/devtools/client/memory/components/census.js
new file mode 100644
index 000000000..3274b26bb
--- /dev/null
+++ b/devtools/client/memory/components/census.js
@@ -0,0 +1,79 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const { DOM: dom, createClass, PropTypes, createFactory } = require("devtools/client/shared/vendor/react");
+const Tree = createFactory(require("devtools/client/shared/components/tree"));
+const CensusTreeItem = createFactory(require("./census-tree-item"));
+const { createParentMap } = require("../utils");
+const { TREE_ROW_HEIGHT } = require("../constants");
+const { censusModel, diffingModel } = require("../models");
+
+const Census = module.exports = createClass({
+ displayName: "Census",
+
+ propTypes: {
+ census: censusModel,
+ onExpand: PropTypes.func.isRequired,
+ onCollapse: PropTypes.func.isRequired,
+ onFocus: PropTypes.func.isRequired,
+ onViewSourceInDebugger: PropTypes.func.isRequired,
+ onViewIndividuals: PropTypes.func.isRequired,
+ diffing: diffingModel,
+ },
+
+ render() {
+ let {
+ census,
+ onExpand,
+ onCollapse,
+ onFocus,
+ diffing,
+ onViewSourceInDebugger,
+ onViewIndividuals,
+ } = this.props;
+
+ const report = census.report;
+ let parentMap = census.parentMap;
+ const { totalBytes, totalCount } = report;
+
+ const getPercentBytes = totalBytes === 0
+ ? _ => 0
+ : bytes => (bytes / totalBytes) * 100;
+
+ const getPercentCount = totalCount === 0
+ ? _ => 0
+ : count => (count / totalCount) * 100;
+
+ return Tree({
+ autoExpandDepth: 0,
+ focused: census.focused,
+ getParent: node => {
+ const parent = parentMap[node.id];
+ return parent === report ? null : parent;
+ },
+ getChildren: node => node.children || [],
+ isExpanded: node => census.expanded.has(node.id),
+ onExpand,
+ onCollapse,
+ onFocus,
+ renderItem: (item, depth, focused, arrow, expanded) =>
+ new CensusTreeItem({
+ onViewSourceInDebugger,
+ item,
+ depth,
+ focused,
+ arrow,
+ expanded,
+ getPercentBytes,
+ getPercentCount,
+ diffing,
+ inverted: census.display.inverted,
+ onViewIndividuals,
+ }),
+ getRoots: () => report.children || [],
+ getKey: node => node.id,
+ itemHeight: TREE_ROW_HEIGHT,
+ });
+ }
+});
diff --git a/devtools/client/memory/components/dominator-tree-header.js b/devtools/client/memory/components/dominator-tree-header.js
new file mode 100644
index 000000000..ae1c7520e
--- /dev/null
+++ b/devtools/client/memory/components/dominator-tree-header.js
@@ -0,0 +1,44 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const { DOM: dom, createClass, PropTypes } = require("devtools/client/shared/vendor/react");
+const { L10N } = require("../utils");
+
+const DominatorTreeHeader = module.exports = createClass({
+ displayName: "DominatorTreeHeader",
+
+ propTypes: { },
+
+ render() {
+ return dom.div(
+ {
+ className: "header"
+ },
+
+ dom.span(
+ {
+ className: "heap-tree-item-bytes",
+ title: L10N.getStr("heapview.field.retainedSize.tooltip"),
+ },
+ L10N.getStr("heapview.field.retainedSize")
+ ),
+
+ dom.span(
+ {
+ className: "heap-tree-item-bytes",
+ title: L10N.getStr("heapview.field.shallowSize.tooltip"),
+ },
+ L10N.getStr("heapview.field.shallowSize")
+ ),
+
+ dom.span(
+ {
+ className: "heap-tree-item-name",
+ title: L10N.getStr("dominatortree.field.label.tooltip"),
+ },
+ L10N.getStr("dominatortree.field.label")
+ )
+ );
+ }
+});
diff --git a/devtools/client/memory/components/dominator-tree-item.js b/devtools/client/memory/components/dominator-tree-item.js
new file mode 100644
index 000000000..bf76ee9b4
--- /dev/null
+++ b/devtools/client/memory/components/dominator-tree-item.js
@@ -0,0 +1,142 @@
+/* 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, isSavedFrame } = require("devtools/shared/DevToolsUtils");
+const { DOM: dom, createClass, createFactory, PropTypes } = require("devtools/client/shared/vendor/react");
+const { L10N, formatNumber, formatPercent } = require("../utils");
+const Frame = createFactory(require("devtools/client/shared/components/frame"));
+const { TREE_ROW_HEIGHT } = require("../constants");
+
+const Separator = createFactory(createClass({
+ displayName: "Separator",
+
+ render() {
+ return dom.span({ className: "separator" }, "›");
+ }
+}));
+
+const DominatorTreeItem = module.exports = createClass({
+ displayName: "DominatorTreeItem",
+
+ propTypes: {
+ item: PropTypes.object.isRequired,
+ depth: PropTypes.number.isRequired,
+ arrow: PropTypes.object,
+ focused: PropTypes.bool.isRequired,
+ getPercentSize: PropTypes.func.isRequired,
+ onViewSourceInDebugger: PropTypes.func.isRequired,
+ },
+
+ shouldComponentUpdate(nextProps, nextState) {
+ return this.props.item != nextProps.item
+ || this.props.depth != nextProps.depth
+ || this.props.expanded != nextProps.expanded
+ || this.props.focused != nextProps.focused;
+ },
+
+ render() {
+ let {
+ item,
+ depth,
+ arrow,
+ focused,
+ getPercentSize,
+ onViewSourceInDebugger,
+ } = this.props;
+
+ const retainedSize = formatNumber(item.retainedSize);
+ const percentRetainedSize = formatPercent(getPercentSize(item.retainedSize));
+
+ const shallowSize = formatNumber(item.shallowSize);
+ const percentShallowSize = formatPercent(getPercentSize(item.shallowSize));
+
+ // Build up our label UI as an array of each label piece, which is either a
+ // string or a frame, and separators in between them.
+
+ assert(item.label.length > 0,
+ "Our label should not be empty");
+ const label = Array(item.label.length * 2 - 1);
+ label.fill(undefined);
+
+ for (let i = 0, length = item.label.length; i < length; i++) {
+ const piece = item.label[i];
+ const key = `${item.nodeId}-label-${i}`;
+
+ // `i` is the index of the label piece we are rendering, `label[i*2]` is
+ // where the rendered label piece belngs, and `label[i*2+1]` (if it isn't
+ // out of bounds) is where the separator belongs.
+
+ if (isSavedFrame(piece)) {
+ label[i * 2] = Frame({
+ key,
+ onClick: () => onViewSourceInDebugger(piece),
+ frame: piece,
+ showFunctionName: true
+ });
+ } else if (piece === "noStack") {
+ label[i * 2] = dom.span({ key, className: "not-available" },
+ L10N.getStr("tree-item.nostack"));
+ } else if (piece === "noFilename") {
+ label[i * 2] = dom.span({ key, className: "not-available" },
+ L10N.getStr("tree-item.nofilename"));
+ } else if (piece === "JS::ubi::RootList") {
+ // Don't use the usual labeling machinery for root lists: replace it
+ // with the "GC Roots" string.
+ label.splice(0, label.length);
+ label.push(L10N.getStr("tree-item.rootlist"));
+ break;
+ } else {
+ label[i * 2] = piece;
+ }
+
+ // If this is not the last piece of the label, add a separator.
+ if (i < length - 1) {
+ label[i * 2 + 1] = Separator({ key: `${item.nodeId}-separator-${i}` });
+ }
+ }
+
+ return dom.div(
+ {
+ className: `heap-tree-item ${focused ? "focused" : ""} node-${item.nodeId}`
+ },
+
+ dom.span(
+ {
+ className: "heap-tree-item-field heap-tree-item-bytes"
+ },
+ dom.span(
+ {
+ className: "heap-tree-number"
+ },
+ retainedSize
+ ),
+ dom.span({ className: "heap-tree-percent" }, percentRetainedSize)
+ ),
+
+ dom.span(
+ {
+ className: "heap-tree-item-field heap-tree-item-bytes"
+ },
+ dom.span(
+ {
+ className: "heap-tree-number"
+ },
+ shallowSize
+ ),
+ dom.span({ className: "heap-tree-percent" }, percentShallowSize)
+ ),
+
+ dom.span(
+ {
+ className: "heap-tree-item-field heap-tree-item-name",
+ style: { marginInlineStart: depth * TREE_ROW_HEIGHT }
+ },
+ arrow,
+ label,
+ dom.span({ className: "heap-tree-item-address" },
+ `@ 0x${item.nodeId.toString(16)}`)
+ )
+ );
+ },
+});
diff --git a/devtools/client/memory/components/dominator-tree.js b/devtools/client/memory/components/dominator-tree.js
new file mode 100644
index 000000000..a1105a4fc
--- /dev/null
+++ b/devtools/client/memory/components/dominator-tree.js
@@ -0,0 +1,216 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const { DOM: dom, createClass, PropTypes, createFactory } = require("devtools/client/shared/vendor/react");
+const { assert, safeErrorString } = require("devtools/shared/DevToolsUtils");
+const { createParentMap } = require("devtools/shared/heapsnapshot/CensusUtils");
+const Tree = createFactory(require("devtools/client/shared/components/tree"));
+const DominatorTreeItem = createFactory(require("./dominator-tree-item"));
+const { L10N } = require("../utils");
+const { TREE_ROW_HEIGHT, dominatorTreeState } = require("../constants");
+const { dominatorTreeModel } = require("../models");
+const DominatorTreeLazyChildren = require("../dominator-tree-lazy-children");
+
+const DOMINATOR_TREE_AUTO_EXPAND_DEPTH = 3;
+
+/**
+ * A throbber that represents a subtree in the dominator tree that is actively
+ * being incrementally loaded and fetched from the `HeapAnalysesWorker`.
+ */
+const DominatorTreeSubtreeFetching = createFactory(createClass({
+ displayName: "DominatorTreeSubtreeFetching",
+
+ shouldComponentUpdate(nextProps, nextState) {
+ return this.props.depth !== nextProps.depth
+ || this.props.focused !== nextProps.focused;
+ },
+
+ propTypes: {
+ depth: PropTypes.number.isRequired,
+ focused: PropTypes.bool.isRequired,
+ },
+
+ render() {
+ let {
+ depth,
+ focused,
+ } = this.props;
+
+ return dom.div(
+ {
+ className: `heap-tree-item subtree-fetching ${focused ? "focused" : ""}`
+ },
+ dom.span({ className: "heap-tree-item-field heap-tree-item-bytes" }),
+ dom.span({ className: "heap-tree-item-field heap-tree-item-bytes" }),
+ dom.span({
+ className: "heap-tree-item-field heap-tree-item-name devtools-throbber",
+ style: { marginInlineStart: depth * TREE_ROW_HEIGHT }
+ })
+ );
+ }
+}));
+
+/**
+ * A link to fetch and load more siblings in the dominator tree, when there are
+ * already many loaded above.
+ */
+const DominatorTreeSiblingLink = createFactory(createClass({
+ displayName: "DominatorTreeSiblingLink",
+
+ propTypes: {
+ depth: PropTypes.number.isRequired,
+ focused: PropTypes.bool.isRequired,
+ item: PropTypes.instanceOf(DominatorTreeLazyChildren).isRequired,
+ onLoadMoreSiblings: PropTypes.func.isRequired,
+ },
+
+ shouldComponentUpdate(nextProps, nextState) {
+ return this.props.depth !== nextProps.depth
+ || this.props.focused !== nextProps.focused;
+ },
+
+ render() {
+ let {
+ depth,
+ focused,
+ item,
+ onLoadMoreSiblings,
+ } = this.props;
+
+ return dom.div(
+ {
+ className: `heap-tree-item more-children ${focused ? "focused" : ""}`
+ },
+ dom.span({ className: "heap-tree-item-field heap-tree-item-bytes" }),
+ dom.span({ className: "heap-tree-item-field heap-tree-item-bytes" }),
+ dom.span(
+ {
+ className: "heap-tree-item-field heap-tree-item-name",
+ style: { marginInlineStart: depth * TREE_ROW_HEIGHT }
+ },
+ dom.a(
+ {
+ onClick: () => onLoadMoreSiblings(item)
+ },
+ L10N.getStr("tree-item.load-more")
+ )
+ )
+ );
+ }
+}));
+
+/**
+ * The actual dominator tree rendered as an expandable and collapsible tree.
+ */
+const DominatorTree = module.exports = createClass({
+ displayName: "DominatorTree",
+
+ propTypes: {
+ dominatorTree: dominatorTreeModel.isRequired,
+ onLoadMoreSiblings: PropTypes.func.isRequired,
+ onViewSourceInDebugger: PropTypes.func.isRequired,
+ onExpand: PropTypes.func.isRequired,
+ onCollapse: PropTypes.func.isRequired,
+ },
+
+ shouldComponentUpdate(nextProps, nextState) {
+ // Safe to use referential equality here because all of our mutations on
+ // dominator tree models use immutableUpdate in a persistent manner. The
+ // exception to the rule are mutations of the expanded set, however we take
+ // care that the dominatorTree model itself is still re-allocated when
+ // mutations to the expanded set occur. Because of the re-allocations, we
+ // can continue using referential equality here.
+ return this.props.dominatorTree !== nextProps.dominatorTree;
+ },
+
+ render() {
+ const { dominatorTree, onViewSourceInDebugger, onLoadMoreSiblings } = this.props;
+
+ const parentMap = createParentMap(dominatorTree.root, node => node.nodeId);
+
+ return Tree({
+ key: "dominator-tree-tree",
+ autoExpandDepth: DOMINATOR_TREE_AUTO_EXPAND_DEPTH,
+ focused: dominatorTree.focused,
+ getParent: node =>
+ node instanceof DominatorTreeLazyChildren
+ ? parentMap[node.parentNodeId()]
+ : parentMap[node.nodeId],
+ getChildren: node => {
+ const children = node.children ? node.children.slice() : [];
+ if (node.moreChildrenAvailable) {
+ children.push(new DominatorTreeLazyChildren(node.nodeId, children.length));
+ }
+ return children;
+ },
+ isExpanded: node => {
+ return node instanceof DominatorTreeLazyChildren
+ ? false
+ : dominatorTree.expanded.has(node.nodeId);
+ },
+ onExpand: item => {
+ if (item instanceof DominatorTreeLazyChildren) {
+ return;
+ }
+
+ if (item.moreChildrenAvailable && (!item.children || !item.children.length)) {
+ const startIndex = item.children ? item.children.length : 0;
+ onLoadMoreSiblings(new DominatorTreeLazyChildren(item.nodeId, startIndex));
+ }
+
+ this.props.onExpand(item);
+ },
+ onCollapse: item => {
+ if (item instanceof DominatorTreeLazyChildren) {
+ return;
+ }
+
+ this.props.onCollapse(item);
+ },
+ onFocus: item => {
+ if (item instanceof DominatorTreeLazyChildren) {
+ return;
+ }
+
+ this.props.onFocus(item);
+ },
+ renderItem: (item, depth, focused, arrow, expanded) => {
+ if (item instanceof DominatorTreeLazyChildren) {
+ if (item.isFirstChild()) {
+ assert(dominatorTree.state === dominatorTreeState.INCREMENTAL_FETCHING,
+ "If we are displaying a throbber for loading a subtree, " +
+ "then we should be INCREMENTAL_FETCHING those children right now");
+ return DominatorTreeSubtreeFetching({
+ key: item.key(),
+ depth,
+ focused,
+ });
+ }
+
+ return DominatorTreeSiblingLink({
+ key: item.key(),
+ item,
+ depth,
+ focused,
+ onLoadMoreSiblings,
+ });
+ }
+
+ return DominatorTreeItem({
+ item,
+ depth,
+ focused,
+ arrow,
+ expanded,
+ getPercentSize: size => (size / dominatorTree.root.retainedSize) * 100,
+ onViewSourceInDebugger,
+ });
+ },
+ getRoots: () => [dominatorTree.root],
+ getKey: node =>
+ node instanceof DominatorTreeLazyChildren ? node.key() : node.nodeId,
+ itemHeight: TREE_ROW_HEIGHT,
+ });
+ }
+});
diff --git a/devtools/client/memory/components/heap.js b/devtools/client/memory/components/heap.js
new file mode 100644
index 000000000..786f37ae1
--- /dev/null
+++ b/devtools/client/memory/components/heap.js
@@ -0,0 +1,455 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const { DOM: dom, createClass, PropTypes, 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 Individuals = createFactory(require("./individuals"));
+const IndividualsHeader = createFactory(require("./individuals-header"));
+const ShortestPaths = createFactory(require("./shortest-paths"));
+const { getStatusTextFull, L10N } = require("../utils");
+const {
+ snapshotState: states,
+ diffingState,
+ viewState,
+ censusState,
+ treeMapState,
+ dominatorTreeState,
+ individualsState,
+} = require("../constants");
+const models = require("../models");
+const { snapshot: snapshotModel, diffingModel } = models;
+
+/**
+ * Get the app state's current state atom.
+ *
+ * @see the relevant state string constants in `../constants.js`.
+ *
+ * @param {models.view} view
+ * @param {snapshotModel} snapshot
+ * @param {diffingModel} diffing
+ * @param {individualsModel} individuals
+ *
+ * @return {snapshotState|diffingState|dominatorTreeState}
+ */
+function getState(view, snapshot, diffing, individuals) {
+ switch (view.state) {
+ case viewState.CENSUS:
+ 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;
+
+ case viewState.INDIVIDUALS:
+ return individuals.state;
+ }
+
+ assert(false, `Unexpected view state: ${view.state}`);
+ return null;
+}
+
+/**
+ * Return true if we should display a status message when we are in the given
+ * state. Return false otherwise.
+ *
+ * @param {snapshotState|diffingState|dominatorTreeState} state
+ * @param {models.view} view
+ * @param {snapshotModel} snapshot
+ *
+ * @returns {Boolean}
+ */
+function shouldDisplayStatus(state, view, snapshot) {
+ switch (state) {
+ case states.IMPORTING:
+ case states.SAVING:
+ case states.SAVED:
+ case states.READING:
+ case censusState.SAVING:
+ case treeMapState.SAVING:
+ case diffingState.SELECTING:
+ case diffingState.TAKING_DIFF:
+ case dominatorTreeState.COMPUTING:
+ case dominatorTreeState.COMPUTED:
+ case dominatorTreeState.FETCHING:
+ case individualsState.COMPUTING_DOMINATOR_TREE:
+ case individualsState.FETCHING:
+ return true;
+ }
+ return view.state === viewState.DOMINATOR_TREE && !snapshot.dominatorTree;
+}
+
+/**
+ * Get the status text to display for the given state.
+ *
+ * @param {snapshotState|diffingState|dominatorTreeState} state
+ * @param {diffingModel} diffing
+ *
+ * @returns {String}
+ */
+function getStateStatusText(state, diffing) {
+ if (state === diffingState.SELECTING) {
+ return L10N.getStr(diffing.firstSnapshotId === null
+ ? "diffing.prompt.selectBaseline"
+ : "diffing.prompt.selectComparison");
+ }
+
+ return getStatusTextFull(state);
+}
+
+/**
+ * Given that we should display a status message, return true if we should also
+ * display a throbber along with the status message. Return false otherwise.
+ *
+ * @param {diffingModel} diffing
+ *
+ * @returns {Boolean}
+ */
+function shouldDisplayThrobber(diffing) {
+ return !diffing || diffing.state !== diffingState.SELECTING;
+}
+
+/**
+ * Get the current state's error, or return null if there is none.
+ *
+ * @param {snapshotModel} snapshot
+ * @param {diffingModel} diffing
+ * @param {individualsModel} individuals
+ *
+ * @returns {Error|null}
+ */
+function getError(snapshot, diffing, individuals) {
+ 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;
+ }
+ }
+
+ if (individuals && individuals.state === individualsState.ERROR) {
+ return individuals.error;
+ }
+
+ return null;
+}
+
+/**
+ * Main view for the memory tool.
+ *
+ * The Heap component contains several panels for different states; an initial
+ * state of only a button to take a snapshot, loading states, the census view
+ * tree, the dominator tree, etc.
+ */
+const Heap = module.exports = createClass({
+ displayName: "Heap",
+
+ propTypes: {
+ onSnapshotClick: PropTypes.func.isRequired,
+ onLoadMoreSiblings: PropTypes.func.isRequired,
+ onCensusExpand: PropTypes.func.isRequired,
+ onCensusCollapse: PropTypes.func.isRequired,
+ onDominatorTreeExpand: PropTypes.func.isRequired,
+ onDominatorTreeCollapse: PropTypes.func.isRequired,
+ onCensusFocus: PropTypes.func.isRequired,
+ onDominatorTreeFocus: PropTypes.func.isRequired,
+ onShortestPathsResize: PropTypes.func.isRequired,
+ snapshot: snapshotModel,
+ onViewSourceInDebugger: PropTypes.func.isRequired,
+ onPopView: PropTypes.func.isRequired,
+ individuals: models.individuals,
+ onViewIndividuals: PropTypes.func.isRequired,
+ onFocusIndividual: PropTypes.func.isRequired,
+ diffing: diffingModel,
+ view: models.view.isRequired,
+ sizes: PropTypes.object.isRequired,
+ },
+
+ render() {
+ let {
+ snapshot,
+ diffing,
+ onSnapshotClick,
+ onLoadMoreSiblings,
+ onViewSourceInDebugger,
+ onViewIndividuals,
+ individuals,
+ view,
+ } = this.props;
+
+
+ if (!diffing && !snapshot && !individuals) {
+ return this._renderInitial(onSnapshotClick);
+ }
+
+ const state = getState(view, snapshot, diffing, individuals);
+ const statusText = getStateStatusText(state, diffing);
+
+ if (shouldDisplayStatus(state, view, snapshot)) {
+ return this._renderStatus(state, statusText, diffing);
+ }
+
+ const error = getError(snapshot, diffing, individuals);
+ if (error) {
+ return this._renderError(state, statusText, error);
+ }
+
+ if (view.state === viewState.CENSUS || view.state === viewState.DIFFING) {
+ const census = view.state === viewState.CENSUS
+ ? snapshot.census
+ : diffing.census;
+ if (!census) {
+ return this._renderStatus(state, statusText, diffing);
+ }
+ return this._renderCensus(state, census, diffing, onViewSourceInDebugger,
+ onViewIndividuals);
+ }
+
+ if (view.state === viewState.TREE_MAP) {
+ return this._renderTreeMap(state, snapshot.treeMap);
+ }
+
+ if (view.state === viewState.INDIVIDUALS) {
+ assert(individuals.state === individualsState.FETCHED,
+ "Should have fetched the individuals -- other states are rendered as statuses");
+ return this._renderIndividuals(state, individuals,
+ individuals.dominatorTree,
+ onViewSourceInDebugger);
+ }
+
+ assert(view.state === 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);
+ },
+
+ /**
+ * Render the heap view's container panel with the given contents inside of
+ * it.
+ *
+ * @param {snapshotState|diffingState|dominatorTreeState} state
+ * @param {...Any} contents
+ */
+ _renderHeapView(state, ...contents) {
+ return dom.div(
+ {
+ id: "heap-view",
+ "data-state": state
+ },
+ dom.div(
+ {
+ className: "heap-view-panel",
+ "data-state": state,
+ },
+ ...contents
+ )
+ );
+ },
+
+ _renderInitial(onSnapshotClick) {
+ return this._renderHeapView("initial", dom.button(
+ {
+ className: "devtools-toolbarbutton take-snapshot",
+ onClick: onSnapshotClick,
+ "data-standalone": true,
+ "data-text-only": true,
+ },
+ L10N.getStr("take-snapshot")
+ ));
+ },
+
+ _renderStatus(state, statusText, diffing) {
+ let throbber = "";
+ if (shouldDisplayThrobber(diffing)) {
+ throbber = "devtools-throbber";
+ }
+
+ return this._renderHeapView(state, dom.span(
+ {
+ className: `snapshot-status ${throbber}`
+ },
+ statusText
+ ));
+ },
+
+ _renderError(state, statusText, error) {
+ return this._renderHeapView(
+ state,
+ dom.span({ className: "snapshot-status error" }, statusText),
+ dom.pre({}, safeErrorString(error))
+ );
+ },
+
+ _renderCensus(state, census, diffing, onViewSourceInDebugger, onViewIndividuals) {
+ assert(census.report, "Should not render census that does not have a report");
+
+ if (!census.report.children) {
+ const msg = diffing ? L10N.getStr("heapview.no-difference")
+ : census.filter ? L10N.getStr("heapview.none-match")
+ : L10N.getStr("heapview.empty");
+ return this._renderHeapView(state, dom.div({ className: "empty" }, msg));
+ }
+
+ const contents = [];
+
+ if (census.display.breakdown.by === "allocationStack"
+ && census.report.children
+ && census.report.children.length === 1
+ && census.report.children[0].name === "noStack") {
+ contents.push(dom.div({ className: "error no-allocation-stacks" },
+ L10N.getStr("heapview.noAllocationStacks")));
+ }
+
+ contents.push(CensusHeader({ diffing }));
+ contents.push(Census({
+ onViewSourceInDebugger,
+ onViewIndividuals,
+ diffing,
+ census,
+ 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 })
+ );
+ },
+
+ _renderIndividuals(state, individuals, dominatorTree, onViewSourceInDebugger) {
+ assert(individuals.state === individualsState.FETCHED,
+ "Should have fetched individuals");
+ assert(dominatorTree && dominatorTree.root,
+ "Should have a dominator tree and its root");
+
+ const tree = dom.div(
+ {
+ className: "vbox",
+ style: {
+ overflowY: "auto"
+ }
+ },
+ IndividualsHeader(),
+ Individuals({
+ individuals,
+ dominatorTree,
+ onViewSourceInDebugger,
+ onFocus: this.props.onFocusIndividual
+ })
+ );
+
+ const shortestPaths = ShortestPaths({
+ graph: individuals.focused
+ ? individuals.focused.shortestPaths
+ : null
+ });
+
+ return this._renderHeapView(
+ state,
+ dom.div(
+ { className: "hbox devtools-toolbar" },
+ dom.label(
+ { id: "pop-view-button-label" },
+ dom.button(
+ {
+ id: "pop-view-button",
+ className: "devtools-button",
+ onClick: this.props.onPopView,
+ },
+ L10N.getStr("toolbar.pop-view")
+ ),
+ L10N.getStr("toolbar.pop-view.label")
+ ),
+ L10N.getStr("toolbar.viewing-individuals")
+ ),
+ HSplitBox({
+ start: tree,
+ end: shortestPaths,
+ startWidth: this.props.sizes.shortestPathsSize,
+ onResize: this.props.onShortestPathsResize,
+ })
+ );
+ },
+
+ _renderDominatorTree(state, onViewSourceInDebugger, dominatorTree, onLoadMoreSiblings) {
+ const tree = dom.div(
+ {
+ className: "vbox",
+ style: {
+ overflowY: "auto"
+ }
+ },
+ DominatorTreeHeader(),
+ DominatorTree({
+ onViewSourceInDebugger,
+ dominatorTree,
+ onLoadMoreSiblings,
+ onExpand: this.props.onDominatorTreeExpand,
+ onCollapse: this.props.onDominatorTreeCollapse,
+ onFocus: this.props.onDominatorTreeFocus,
+ })
+ );
+
+ const shortestPaths = ShortestPaths({
+ graph: dominatorTree.focused
+ ? dominatorTree.focused.shortestPaths
+ : null
+ });
+
+ return this._renderHeapView(
+ state,
+ HSplitBox({
+ start: tree,
+ end: shortestPaths,
+ startWidth: this.props.sizes.shortestPathsSize,
+ onResize: this.props.onShortestPathsResize,
+ })
+ );
+ },
+});
diff --git a/devtools/client/memory/components/individuals-header.js b/devtools/client/memory/components/individuals-header.js
new file mode 100644
index 000000000..7cacd163e
--- /dev/null
+++ b/devtools/client/memory/components/individuals-header.js
@@ -0,0 +1,44 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const { DOM: dom, createClass, PropTypes } = require("devtools/client/shared/vendor/react");
+const { L10N } = require("../utils");
+
+const IndividualsHeader = module.exports = createClass({
+ displayName: "IndividualsHeader",
+
+ propTypes: { },
+
+ render() {
+ return dom.div(
+ {
+ className: "header"
+ },
+
+ dom.span(
+ {
+ className: "heap-tree-item-bytes",
+ title: L10N.getStr("heapview.field.retainedSize.tooltip"),
+ },
+ L10N.getStr("heapview.field.retainedSize")
+ ),
+
+ dom.span(
+ {
+ className: "heap-tree-item-bytes",
+ title: L10N.getStr("heapview.field.shallowSize.tooltip"),
+ },
+ L10N.getStr("heapview.field.shallowSize")
+ ),
+
+ dom.span(
+ {
+ className: "heap-tree-item-name",
+ title: L10N.getStr("individuals.field.node.tooltip"),
+ },
+ L10N.getStr("individuals.field.node")
+ )
+ );
+ }
+});
diff --git a/devtools/client/memory/components/individuals.js b/devtools/client/memory/components/individuals.js
new file mode 100644
index 000000000..56c784820
--- /dev/null
+++ b/devtools/client/memory/components/individuals.js
@@ -0,0 +1,61 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const { DOM: dom, createClass, PropTypes, createFactory } = require("devtools/client/shared/vendor/react");
+const { assert } = require("devtools/shared/DevToolsUtils");
+const { createParentMap } = require("devtools/shared/heapsnapshot/CensusUtils");
+const Tree = createFactory(require("devtools/client/shared/components/tree"));
+const DominatorTreeItem = createFactory(require("./dominator-tree-item"));
+const { L10N } = require("../utils");
+const { TREE_ROW_HEIGHT } = require("../constants");
+const models = require("../models");
+
+/**
+ * The list of individuals in a census group.
+ */
+const Individuals = module.exports = createClass({
+ displayName: "Individuals",
+
+ propTypes: {
+ onViewSourceInDebugger: PropTypes.func.isRequired,
+ onFocus: PropTypes.func.isRequired,
+ individuals: models.individuals,
+ dominatorTree: models.dominatorTreeModel,
+ },
+
+ render() {
+ const {
+ individuals,
+ dominatorTree,
+ onViewSourceInDebugger,
+ onFocus,
+ } = this.props;
+
+ return Tree({
+ key: "individuals-tree",
+ autoExpandDepth: 0,
+ focused: individuals.focused,
+ getParent: node => null,
+ getChildren: node => [],
+ isExpanded: node => false,
+ onExpand: () => {},
+ onCollapse: () => {},
+ onFocus,
+ renderItem: (item, depth, focused, _, expanded) => {
+ return DominatorTreeItem({
+ item,
+ depth,
+ focused,
+ arrow: undefined,
+ expanded,
+ getPercentSize: size => (size / dominatorTree.root.retainedSize) * 100,
+ onViewSourceInDebugger,
+ });
+ },
+ getRoots: () => individuals.nodes,
+ getKey: node => node.nodeId,
+ itemHeight: TREE_ROW_HEIGHT,
+ });
+ }
+});
diff --git a/devtools/client/memory/components/list.js b/devtools/client/memory/components/list.js
new file mode 100644
index 000000000..3aa27da69
--- /dev/null
+++ b/devtools/client/memory/components/list.js
@@ -0,0 +1,35 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const { DOM: dom, createClass, PropTypes } = require("devtools/client/shared/vendor/react");
+
+/**
+ * Generic list component that takes another react component to represent
+ * the children nodes as `itemComponent`, and a list of items to render
+ * as that component with a click handler.
+ */
+const List = module.exports = createClass({
+ displayName: "List",
+
+ propTypes: {
+ itemComponent: PropTypes.any.isRequired,
+ onClick: PropTypes.func,
+ items: PropTypes.array.isRequired,
+ },
+
+ render() {
+ let { items, onClick, itemComponent: Item } = this.props;
+
+ return (
+ dom.ul({ className: "list" }, ...items.map((item, index) => {
+ return Item(Object.assign({}, this.props, {
+ key: index,
+ item,
+ index,
+ onClick: () => onClick(item),
+ }));
+ }))
+ );
+ }
+});
diff --git a/devtools/client/memory/components/moz.build b/devtools/client/memory/components/moz.build
new file mode 100644
index 000000000..529454dd9
--- /dev/null
+++ b/devtools/client/memory/components/moz.build
@@ -0,0 +1,25 @@
+# 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',
+ 'individuals-header.js',
+ 'individuals.js',
+ 'list.js',
+ 'shortest-paths.js',
+ 'snapshot-list-item.js',
+ 'toolbar.js',
+ 'tree-map.js',
+)
diff --git a/devtools/client/memory/components/shortest-paths.js b/devtools/client/memory/components/shortest-paths.js
new file mode 100644
index 000000000..23cb8134b
--- /dev/null
+++ b/devtools/client/memory/components/shortest-paths.js
@@ -0,0 +1,184 @@
+/* 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,
+ PropTypes,
+} = require("devtools/client/shared/vendor/react");
+const { isSavedFrame } = require("devtools/shared/DevToolsUtils");
+const { getSourceNames } = require("devtools/client/shared/source-utils");
+const { L10N } = require("../utils");
+
+const GRAPH_DEFAULTS = {
+ translate: [20, 20],
+ scale: 1
+};
+
+const NO_STACK = "noStack";
+const NO_FILENAME = "noFilename";
+const ROOT_LIST = "JS::ubi::RootList";
+
+function stringifyLabel(label, id) {
+ const sanitized = [];
+
+ for (let i = 0, length = label.length; i < length; i++) {
+ const piece = label[i];
+
+ if (isSavedFrame(piece)) {
+ const { short } = getSourceNames(piece.source);
+ sanitized[i] = `${piece.functionDisplayName} @ ${short}:${piece.line}:${piece.column}`;
+ } else if (piece === NO_STACK) {
+ sanitized[i] = L10N.getStr("tree-item.nostack");
+ } else if (piece === NO_FILENAME) {
+ sanitized[i] = L10N.getStr("tree-item.nofilename");
+ } else if (piece === ROOT_LIST) {
+ // Don't use the usual labeling machinery for root lists: replace it
+ // with the "GC Roots" string.
+ sanitized.splice(0, label.length);
+ sanitized.push(L10N.getStr("tree-item.rootlist"));
+ break;
+ } else {
+ sanitized[i] = "" + piece;
+ }
+ }
+
+ return `${sanitized.join(" › ")} @ 0x${id.toString(16)}`;
+}
+
+module.exports = createClass({
+ displayName: "ShortestPaths",
+
+ propTypes: {
+ graph: PropTypes.shape({
+ nodes: PropTypes.arrayOf(PropTypes.object),
+ edges: PropTypes.arrayOf(PropTypes.object),
+ }),
+ },
+
+ getInitialState() {
+ return { zoom: null };
+ },
+
+ shouldComponentUpdate(nextProps) {
+ return this.props.graph != nextProps.graph;
+ },
+
+ componentDidMount() {
+ if (this.props.graph) {
+ this._renderGraph(this.refs.container, this.props.graph);
+ }
+ },
+
+ componentDidUpdate() {
+ if (this.props.graph) {
+ this._renderGraph(this.refs.container, this.props.graph);
+ }
+ },
+
+ componentWillUnmount() {
+ if (this.state.zoom) {
+ this.state.zoom.on("zoom", null);
+ }
+ },
+
+ render() {
+ let contents;
+ if (this.props.graph) {
+ // Let the componentDidMount or componentDidUpdate method draw the graph
+ // with DagreD3. We just provide the container for the graph here.
+ contents = dom.div({
+ ref: "container",
+ style: {
+ flex: 1,
+ height: "100%",
+ width: "100%",
+ }
+ });
+ } else {
+ contents = dom.div(
+ {
+ id: "shortest-paths-select-node-msg"
+ },
+ L10N.getStr("shortest-paths.select-node")
+ );
+ }
+
+ return dom.div(
+ {
+ id: "shortest-paths",
+ className: "vbox",
+ },
+ dom.label(
+ {
+ id: "shortest-paths-header",
+ className: "header",
+ },
+ L10N.getStr("shortest-paths.header")
+ ),
+ contents
+ );
+ },
+
+ _renderGraph(container, { nodes, edges }) {
+ if (!container.firstChild) {
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
+ svg.setAttribute("id", "graph-svg");
+ svg.setAttribute("xlink", "http://www.w3.org/1999/xlink");
+ svg.style.width = "100%";
+ svg.style.height = "100%";
+
+ const target = document.createElementNS("http://www.w3.org/2000/svg", "g");
+ target.setAttribute("id", "graph-target");
+ target.style.width = "100%";
+ target.style.height = "100%";
+
+ svg.appendChild(target);
+ container.appendChild(svg);
+ }
+
+ const graph = new dagreD3.Digraph();
+
+ for (let i = 0; i < nodes.length; i++) {
+ graph.addNode(nodes[i].id, {
+ id: nodes[i].id,
+ label: stringifyLabel(nodes[i].label, nodes[i].id),
+ });
+ }
+
+ for (let i = 0; i < edges.length; i++) {
+ graph.addEdge(null, edges[i].from, edges[i].to, {
+ label: edges[i].name
+ });
+ }
+
+ const renderer = new dagreD3.Renderer();
+ renderer.drawNodes();
+ renderer.drawEdgePaths();
+
+ const svg = d3.select("#graph-svg");
+ const target = d3.select("#graph-target");
+
+ let zoom = this.state.zoom;
+ if (!zoom) {
+ zoom = d3.behavior.zoom().on("zoom", function () {
+ target.attr(
+ "transform",
+ `translate(${d3.event.translate}) scale(${d3.event.scale})`
+ );
+ });
+ svg.call(zoom);
+ this.setState({ zoom });
+ }
+
+ const { translate, scale } = GRAPH_DEFAULTS;
+ zoom.scale(scale);
+ zoom.translate(translate);
+ target.attr("transform", `translate(${translate}) scale(${scale})`);
+
+ const layout = dagreD3.layout();
+ renderer.layout(layout).run(graph, target);
+ },
+});
diff --git a/devtools/client/memory/components/snapshot-list-item.js b/devtools/client/memory/components/snapshot-list-item.js
new file mode 100644
index 000000000..37db81d13
--- /dev/null
+++ b/devtools/client/memory/components/snapshot-list-item.js
@@ -0,0 +1,114 @@
+/* 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 { DOM: dom, createClass, PropTypes } = require("devtools/client/shared/vendor/react");
+const {
+ L10N,
+ getSnapshotTitle,
+ getSnapshotTotals,
+ getStatusText,
+ snapshotIsDiffable,
+ getSavedCensus
+} = require("../utils");
+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,
+ onDelete: PropTypes.func.isRequired,
+ item: snapshotModel.isRequired,
+ index: PropTypes.number.isRequired,
+ },
+
+ render() {
+ let { index, item: snapshot, onClick, onSave, onDelete, diffing } = this.props;
+ let className = `snapshot-list-item ${snapshot.selected ? " selected" : ""}`;
+ let statusText = getStatusText(snapshot.state);
+ let wantThrobber = !!statusText;
+ let title = getSnapshotTitle(snapshot);
+
+ const selectedForDiffing = diffing
+ && (diffing.firstSnapshotId === snapshot.id
+ || diffing.secondSnapshotId === snapshot.id);
+
+ let checkbox;
+ if (diffing && snapshotIsDiffable(snapshot)) {
+ if (diffing.state === diffingState.SELECTING) {
+ wantThrobber = false;
+ }
+
+ const checkboxAttrs = {
+ type: "checkbox",
+ checked: false,
+ };
+
+ if (selectedForDiffing) {
+ checkboxAttrs.checked = true;
+ checkboxAttrs.disabled = true;
+ className += " selected";
+ statusText = L10N.getStr(diffing.firstSnapshotId === snapshot.id
+ ? "diffing.baseline"
+ : "diffing.comparison");
+ }
+
+ if (selectedForDiffing || diffing.state == diffingState.SELECTING) {
+ checkbox = dom.input(checkboxAttrs);
+ }
+ }
+
+ let details;
+ if (!selectedForDiffing) {
+ // See if a tree map or census is in the read state.
+ let census = getSavedCensus(snapshot);
+
+ // 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.getStr("snapshot.io.save"));
+
+ let deleteButton = !snapshot.path ? void 0 : dom.button({
+ onClick: () => onDelete(snapshot),
+ className: "devtools-button delete",
+ title: L10N.getStr("snapshot.io.delete")
+ });
+
+ return (
+ dom.li({ className, onClick },
+ dom.span({ className: `snapshot-title ${wantThrobber ? " devtools-throbber" : ""}` },
+ checkbox,
+ title,
+ deleteButton
+ ),
+ dom.span({ className: "snapshot-info" },
+ details,
+ saveLink
+ )
+ )
+ );
+ }
+});
diff --git a/devtools/client/memory/components/toolbar.js b/devtools/client/memory/components/toolbar.js
new file mode 100644
index 000000000..de60b2af9
--- /dev/null
+++ b/devtools/client/memory/components/toolbar.js
@@ -0,0 +1,300 @@
+/* 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 { DOM: dom, createClass, PropTypes } = require("devtools/client/shared/vendor/react");
+const { L10N } = require("../utils");
+const models = require("../models");
+const { viewState } = require("../constants");
+
+module.exports = createClass({
+ displayName: "Toolbar",
+
+ propTypes: {
+ censusDisplays: PropTypes.arrayOf(PropTypes.shape({
+ displayName: PropTypes.string.isRequired,
+ })).isRequired,
+ censusDisplay: PropTypes.shape({
+ displayName: PropTypes.string.isRequired,
+ }).isRequired,
+ onTakeSnapshotClick: PropTypes.func.isRequired,
+ onImportClick: PropTypes.func.isRequired,
+ onClearSnapshotsClick: PropTypes.func.isRequired,
+ onCensusDisplayChange: PropTypes.func.isRequired,
+ onToggleRecordAllocationStacks: PropTypes.func.isRequired,
+ allocations: models.allocations,
+ filterString: PropTypes.string,
+ setFilterString: PropTypes.func.isRequired,
+ diffing: models.diffingModel,
+ onToggleDiffing: PropTypes.func.isRequired,
+ view: models.view.isRequired,
+ onViewChange: PropTypes.func.isRequired,
+ labelDisplays: PropTypes.arrayOf(PropTypes.shape({
+ displayName: PropTypes.string.isRequired,
+ })).isRequired,
+ labelDisplay: PropTypes.shape({
+ displayName: PropTypes.string.isRequired,
+ }).isRequired,
+ onLabelDisplayChange: 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,
+ censusDisplay,
+ labelDisplays,
+ labelDisplay,
+ onLabelDisplayChange,
+ treeMapDisplays,
+ onTreeMapDisplayChange,
+ onToggleRecordAllocationStacks,
+ allocations,
+ filterString,
+ setFilterString,
+ snapshots,
+ diffing,
+ onToggleDiffing,
+ view,
+ onViewChange,
+ } = this.props;
+
+ let viewToolbarOptions;
+ if (view.state == viewState.CENSUS || view.state === viewState.DIFFING) {
+ viewToolbarOptions = dom.div(
+ {
+ className: "toolbar-group"
+ },
+
+ dom.label(
+ {
+ className: "display-by",
+ title: L10N.getStr("toolbar.displayBy.tooltip"),
+ },
+ L10N.getStr("toolbar.displayBy"),
+ dom.select(
+ {
+ id: "select-display",
+ className: "select-display",
+ onChange: e => {
+ const newDisplay =
+ censusDisplays.find(b => b.displayName === e.target.value);
+ onCensusDisplayChange(newDisplay);
+ },
+ value: censusDisplay.displayName,
+ },
+ censusDisplays.map(({ tooltip, displayName }) => dom.option(
+ {
+ key: `display-${displayName}`,
+ value: displayName,
+ title: tooltip,
+ },
+ displayName
+ ))
+ )
+ ),
+
+ dom.div({ id: "toolbar-spacer", className: "spacer" }),
+
+ dom.input({
+ id: "filter",
+ type: "search",
+ className: "devtools-filterinput",
+ placeholder: L10N.getStr("filter.placeholder"),
+ title: L10N.getStr("filter.tooltip"),
+ onChange: event => setFilterString(event.target.value),
+ value: filterString || undefined,
+ })
+ );
+ } else if (view.state == 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.state === viewState.DOMINATOR_TREE ||
+ view.state === viewState.INDIVIDUALS);
+
+ viewToolbarOptions = dom.div(
+ {
+ className: "toolbar-group"
+ },
+
+ dom.label(
+ {
+ className: "label-by",
+ title: L10N.getStr("toolbar.labelBy.tooltip"),
+ },
+ L10N.getStr("toolbar.labelBy"),
+ dom.select(
+ {
+ id: "select-label-display",
+ onChange: e => {
+ const newDisplay =
+ labelDisplays.find(b => b.displayName === e.target.value);
+ onLabelDisplayChange(newDisplay);
+ },
+ value: labelDisplay.displayName,
+ },
+ labelDisplays.map(({ tooltip, displayName }) => dom.option(
+ {
+ key: `label-display-${displayName}`,
+ value: displayName,
+ title: tooltip,
+ },
+ displayName
+ ))
+ )
+ )
+ );
+ }
+
+ let viewSelect;
+ if (view.state !== viewState.DIFFING && view.state !== viewState.INDIVIDUALS) {
+ viewSelect = dom.label(
+ {
+ title: L10N.getStr("toolbar.view.tooltip"),
+ },
+ L10N.getStr("toolbar.view"),
+ dom.select(
+ {
+ id: "select-view",
+ onChange: e => onViewChange(e.target.value),
+ defaultValue: view,
+ value: view.state,
+ },
+ dom.option(
+ {
+ value: viewState.TREE_MAP,
+ title: L10N.getStr("toolbar.view.treemap.tooltip"),
+ },
+ 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,
+ title: L10N.getStr("toolbar.view.dominators.tooltip"),
+ },
+ L10N.getStr("toolbar.view.dominators")
+ )
+ )
+ );
+ }
+
+ return (
+ dom.div(
+ {
+ className: "devtools-toolbar"
+ },
+
+ dom.div(
+ {
+ className: "toolbar-group"
+ },
+
+ dom.button({
+ id: "clear-snapshots",
+ className: "clear-snapshots devtools-button",
+ disabled: !snapshots.length,
+ onClick: onClearSnapshotsClick,
+ title: L10N.getStr("clear-snapshots.tooltip")
+ }),
+
+ dom.button({
+ id: "take-snapshot",
+ className: "take-snapshot devtools-button",
+ onClick: onTakeSnapshotClick,
+ title: L10N.getStr("take-snapshot")
+ }),
+
+ dom.button(
+ {
+ id: "diff-snapshots",
+ className: "devtools-button devtools-monospace" + (!!diffing ? " checked" : ""),
+ disabled: snapshots.length < 2,
+ onClick: onToggleDiffing,
+ title: L10N.getStr("diff-snapshots.tooltip"),
+ }
+ ),
+
+ dom.button(
+ {
+ id: "import-snapshot",
+ className: "devtools-toolbarbutton import-snapshot devtools-button",
+ onClick: onImportClick,
+ title: L10N.getStr("import-snapshot"),
+ }
+ )
+ ),
+
+ dom.label(
+ {
+ id: "record-allocation-stacks-label",
+ title: L10N.getStr("checkbox.recordAllocationStacks.tooltip"),
+ },
+ dom.input({
+ id: "record-allocation-stacks-checkbox",
+ type: "checkbox",
+ checked: allocations.recording,
+ disabled: allocations.togglingInProgress,
+ onChange: onToggleRecordAllocationStacks,
+ }),
+ L10N.getStr("checkbox.recordAllocationStacks")
+ ),
+
+ viewSelect,
+ viewToolbarOptions
+ )
+ );
+ }
+});
diff --git a/devtools/client/memory/components/tree-map.js b/devtools/client/memory/components/tree-map.js
new file mode 100644
index 000000000..b6764605e
--- /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"
+ }
+ );
+ }
+});
diff --git a/devtools/client/memory/components/tree-map/canvas-utils.js b/devtools/client/memory/components/tree-map/canvas-utils.js
new file mode 100644
index 000000000..c7d67a0bf
--- /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);
+ };
+}
diff --git a/devtools/client/memory/components/tree-map/color-coarse-type.js b/devtools/client/memory/components/tree-map/color-coarse-type.js
new file mode 100644
index 000000000..5f033ea26
--- /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];
+};
diff --git a/devtools/client/memory/components/tree-map/drag-zoom.js b/devtools/client/memory/components/tree-map/drag-zoom.js
new file mode 100644
index 000000000..3de970725
--- /dev/null
+++ b/devtools/client/memory/components/tree-map/drag-zoom.js
@@ -0,0 +1,316 @@
+/* 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 prevZoomedWidth = container.offsetWidth * (1 + prevZoom);
+ let prevZoomedHeight = container.offsetHeight * (1 + prevZoom);
+ dragZoom.zoomedWidth = container.offsetWidth * (1 + dragZoom.zoom);
+ dragZoom.zoomedHeight = container.offsetHeight * (1 + dragZoom.zoom);
+ let deltaWidth = dragZoom.zoomedWidth - prevZoomedWidth;
+ let deltaHeight = dragZoom.zoomedHeight - prevZoomedHeight;
+
+ let mouseOffsetX = dragZoom.mouseX - container.offsetWidth / 2;
+ let mouseOffsetY = dragZoom.mouseY - container.offsetHeight / 2;
+
+ // The ratio of where the center of the mouse is in regards to the total
+ // zoomed width/height
+ let ratioZoomX = (prevZoomedWidth / 2 + mouseOffsetX - dragZoom.translateX)
+ / prevZoomedWidth;
+ let ratioZoomY = (prevZoomedHeight / 2 + mouseOffsetY - dragZoom.translateY)
+ / prevZoomedHeight;
+
+ // 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);
+
+ // Keep the canvas in range of the container
+ keepInView(container, dragZoom);
+ emitChanged();
+ update();
+ }
+
+ container.addEventListener("wheel", handleWheel, false);
+
+ return function removeListener() {
+ container.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.zoomedHeight - 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.zoomedHeight - container.offsetHeight) / 2 - dragZoom.translateY
+ );
+}
diff --git a/devtools/client/memory/components/tree-map/draw.js b/devtools/client/memory/components/tree-map/draw.js
new file mode 100644
index 000000000..a55c1eae6
--- /dev/null
+++ b/devtools/client/memory/components/tree-map/draw.js
@@ -0,0 +1,295 @@
+/* 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 + FONT_SIZE, 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([
+ // The d3 layout includes the padding around everything, add some
+ // extra padding to the size to compensate for thi
+ canvas.width + (PADDING[1] + PADDING[3]) * ratio,
+ canvas.height + (PADDING[0] + PADDING[2]) * ratio
+ ])
+ .sticky(true)
+ .padding([
+ PADDING[0] * 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 {Object} dragZoom
+ * @param {Array} padding
+ */
+const drawText = exports.drawText = function (ctx, node, borderWidth, ratio,
+ dragZoom, padding) {
+ 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 - padding[0]) * scale - dragZoom.offsetX;
+ let y = margin + (node.y - padding[1]) * 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
+ * @param {Array} padding
+ */
+const drawBox = exports.drawBox = function (ctx, node, borderWidth, dragZoom,
+ padding) {
+ 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 - padding[0]) - dragZoom.offsetX + border / 2;
+ let y = scale * (node.y - padding[1]) - 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;
+ // Subtract the outer padding from the tree map layout.
+ let padding = [PADDING[3] * ratio, PADDING[0] * ratio];
+
+ 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, padding);
+ drawText(ctx, node, borderWidth, ratio, dragZoom, padding);
+ }
+};
+
+/**
+ * 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;
diff --git a/devtools/client/memory/components/tree-map/moz.build b/devtools/client/memory/components/tree-map/moz.build
new file mode 100644
index 000000000..aab193191
--- /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',
+)
diff --git a/devtools/client/memory/components/tree-map/start.js b/devtools/client/memory/components/tree-map/start.js
new file mode 100644
index 000000000..9cebe2a9d
--- /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 = 60) {
+ 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();
+ };
+};