Last active
April 26, 2024 20:00
-
-
Save fitzgen/a143cf720ab89a65e91b to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* -*- 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 | ▼ [email protected]:3:4 | | |
* | 3 | 0 | ▼ [email protected]:6:7 | | |
* | 1 | 1 | [email protected]:3:8 | | |
* | 1 | 1 | [email protected]:4:3 | | |
* | 1 | 1 | [email protected]: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