diff options
Diffstat (limited to 'devtools/client/memory/components/ShortestPaths.js')
-rw-r--r-- | devtools/client/memory/components/ShortestPaths.js | 196 |
1 files changed, 196 insertions, 0 deletions
diff --git a/devtools/client/memory/components/ShortestPaths.js b/devtools/client/memory/components/ShortestPaths.js new file mode 100644 index 0000000000..0dd6e22c33 --- /dev/null +++ b/devtools/client/memory/components/ShortestPaths.js @@ -0,0 +1,196 @@ +/* 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 { + Component, +} = require("resource://devtools/client/shared/vendor/react.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const { isSavedFrame } = require("resource://devtools/shared/DevToolsUtils.js"); +const { + getSourceNames, +} = require("resource://devtools/client/shared/source-utils.js"); +const { L10N } = require("resource://devtools/client/memory/utils.js"); + +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)}`; +} + +class ShortestPaths extends Component { + static get propTypes() { + return { + graph: PropTypes.shape({ + nodes: PropTypes.arrayOf(PropTypes.object), + edges: PropTypes.arrayOf(PropTypes.object), + }), + }; + } + + constructor(props) { + super(props); + this.state = { zoom: null }; + this._renderGraph = this._renderGraph.bind(this); + } + + componentDidMount() { + if (this.props.graph) { + this._renderGraph(this.refs.container, this.props.graph); + } + } + + shouldComponentUpdate(nextProps) { + return this.props.graph != nextProps.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); + } + } + + _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); + } + + 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 + ); + } +} + +module.exports = ShortestPaths; |