Skip to content

Instantly share code, notes, and snippets.

@fitzgen
Last active April 26, 2024 20:00
Show Gist options
  • Save fitzgen/a143cf720ab89a65e91b to your computer and use it in GitHub Desktop.
Save fitzgen/a143cf720ab89a65e91b to your computer and use it in GitHub Desktop.
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* 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 ALLOCATIONS_TREE_INDENTATION = 16; // px
const AUTO_EXPAND_DEPTH = 3; // depth
module.exports = React => {
const { DOM: dom } = React;
/**
* An arrow that displays whether its node is expanded (▼) or collapsed
* (▶). When its node has no children, it is hidden.
*/
const ArrowExpander = React.createClass({
displayName: "ArrowExpander",
shouldComponentUpdate: function (nextProps, nextState) {
if (nextProps.expanded !== this.props.expanded) {
return true;
}
const oldHasChildren = !!this.props.data.children[this.props.id].length;
const newHasChildren = !!nextProps.data.children[this.props.id].length;
return oldHasChildren !== newHasChildren;
},
render: function () {
const attrs = {
className: "arrow theme-twisty",
onClick: this.props.expanded
? () => this.props.onCollapse(this.props.id)
: e => this.props.onExpand(this.props.id, e.altKey)
};
if (!this.props.data.children[this.props.id].length) {
attrs.style = {
visibility: "hidden"
};
}
return dom.div(attrs);
},
componentDidUpdate: function () {
// Unfortunately, React won't let you set arbitrary attributes unless they
// are prefixed with "data-". To take advantage of existing patterns and
// styles, we have to do add and remove our attribute manually after the
// component updates.
if (this.props.expanded) {
this.getDOMNode().setAttribute("open", "");
} else {
this.getDOMNode().removeAttribute("open");
}
}
});
/**
* The function name and frame location label for a node.
*/
const FrameLabel = React.createClass({
displayName: "FrameLabel",
shouldComponentUpdate: function (nextProps, nextState) {
return this.props.id !== nextProps.id;
},
render: function () {
const frame = this.props.data.frames[this.props.id];
const bits = [];
if (!frame) {
return dom.div(
{
className: "frame-label"
},
L10N.getStr("memory.root")
);
}
return dom.div(
{ className: "frame-label hbox" },
dom.span({ className: "frame-label-function-name" },
frame.functionDisplayName || "(anonymous)"),
dom.span({ className: "frame-label-at" }, "@"),
dom.span({ className: "frame-label-source" }, frame.source),
dom.span({ className: "frame-label-separator" }, ":"),
dom.span({ className: "frame-label-line" }, frame.line),
dom.span({ className: "frame-label-separator" }, ":"),
dom.span({ className: "frame-label-column" }, frame.column)
);
}
});
/**
* A node in the tree. Represents a frame that was on the stack when an
* allocation ocurred.
*/
const AllocationsTreeNode = React.createClass({
displayName: "AllocationsTreeNode",
shouldComponentUpdate: function (nextProps, nextState) {
if (this.props.id !== nextProps.id
|| this.props.focused !== nextProps.focused
|| this.props.expanded !== nextProps.expanded) {
return true;
}
const oldHasChildren = !!this.props.data.children[this.props.id].length;
const newHasChildren = !!nextProps.data.children[this.props.id].length;
return oldHasChildren !== newHasChildren;
},
render: function () {
console.log("Rendering " + this.props.id);
let className = "allocations-tree-item";
if (this.props.focused) {
className += " focused";
}
const focus = () => this.props.onFocus(this.props.id);
return dom.div(
{
className,
tabIndex: "0",
onBlur: () => this.props.onBlur(),
onFocus: focus,
onClick: focus,
},
// Total allocations cell
dom.div(
{ className: "tree-cell total-allocations" },
this.props.data.totalCounts[this.props.id]
),
// Self allocations cell
dom.div(
{ className: "tree-cell self-allocations" },
this.props.data.counts[this.props.id]
),
// Frame cell
dom.div(
{
className: "tree-cell frame",
style: {
MozMarginStart: (this.props.level * ALLOCATIONS_TREE_INDENTATION) + "px"
}
},
ArrowExpander(this.props),
FrameLabel(this.props)
)
);
},
componentDidUpdate: function () {
if (this.props.focused) {
this.getDOMNode().focus();
}
},
});
const isChromeSource = source =>
source.match(/\bchrome:\/\//) || source.match(/\bresource:\/\//);
const depth = (frames, id) => {
let depth = 0;
while (frames[id]) {
id = frames[id].parent;
depth++;
}
return depth;
}
/**
* A tree view of allocations. If we have three allocations that ocurred with
* the stacks ABC, ABD, and ABE respectively, render a tree that looks like
* this:
*
* +-------+------+--------------------+
* | Total | Self | Frame |
* +-------+------+--------------------+
* | 3 | 0 | ▼ A@foo.js:3:4 |
* | 3 | 0 | ▼ B@bar.js:6:7 |
* | 1 | 1 | C@baz.js:3:8 |
* | 1 | 1 | D@baz.js:4:3 |
* | 1 | 1 | E@baz.js:4:3 |
* +-------+------+--------------------+
*
*/
const AllocationsTree = React.createClass({
displayName: "AllocationsTree",
getInitialState: () => ({
expanded: new Set([0]),
focused: undefined
}),
componentWillReceiveProps: function (nextProps) {
// Automatically expand the first AUTO_EXPAND_DEPTH levels for new stacks.
for (var i = this.props.frames.length; i < nextProps.frames.length; i++) {
if (depth(nextProps.frames, i) < AUTO_EXPAND_DEPTH) {
this.state.expanded.add(i);
}
}
},
render: function () {
console.log("RENDERING TREE");
return dom.div(
{
className: "allocations-tree-container",
onKeyDown: this._onKeyDown
},
// Header
dom.div(
{ className: "allocations-tree-header" },
dom.div(
{
className: "tree-cell"
},
"Total Allocations"
),
dom.div(
{
className: "tree-cell"
},
"Self Allocations"
),
dom.div(
{
className: "tree-cell"
},
"Frames"
)
),
// Nodes
dom.div(
{
className: "allocations-tree"
},
[...this._dfs(0)].map(([id, level]) => {
console.log("Creating " + id);
return AllocationsTreeNode({
id,
key: "allocations-tree-item-" + id,
level,
data: this.props,
expanded: this.state.expanded.has(id),
focused: id === this.state.focused,
onExpand: this._onExpand,
onCollapse: this._onCollapse,
onBlur: this._onBlur,
onFocus: this._onFocus
});
})
)
);
},
/**
* Perform a depth-first-search.
*/
_dfs: function* (id, _level = 0, _seen = new Set()) {
if (!this.props.chrome
&& this.props.frames[id]
&& isChromeSource(this.props.frames[id].source)) {
return;
}
if (_seen.has(id)) {
throw new Error("Assertion failure: already seen id '" + id + "'");
}
console.log("Traversing " + id);
_seen.add(id);
yield [id, _level];
if (!this.state.expanded.has(id)) {
return;
}
for (let child of this.props.children[id]) {
yield* this._dfs(child, _level + 1, _seen);
}
},
});
return AllocationsTree;
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment