ShortestPaths.js (5272B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 "use strict"; 6 7 const { 8 Component, 9 } = require("resource://devtools/client/shared/vendor/react.mjs"); 10 const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.mjs"); 11 const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); 12 const { isSavedFrame } = require("resource://devtools/shared/DevToolsUtils.js"); 13 const { 14 getSourceNames, 15 } = require("resource://devtools/client/shared/source-utils.js"); 16 const { L10N } = require("resource://devtools/client/memory/utils.js"); 17 18 const GRAPH_DEFAULTS = { 19 translate: [20, 20], 20 scale: 1, 21 }; 22 23 const NO_STACK = "noStack"; 24 const NO_FILENAME = "noFilename"; 25 const ROOT_LIST = "JS::ubi::RootList"; 26 27 function stringifyLabel(label, id) { 28 const sanitized = []; 29 30 for (let i = 0, length = label.length; i < length; i++) { 31 const piece = label[i]; 32 33 if (isSavedFrame(piece)) { 34 const { short } = getSourceNames(piece.source); 35 sanitized[i] = 36 `${piece.functionDisplayName} @ ` + 37 `${short}:${piece.line}:${piece.column}`; 38 } else if (piece === NO_STACK) { 39 sanitized[i] = L10N.getStr("tree-item.nostack"); 40 } else if (piece === NO_FILENAME) { 41 sanitized[i] = L10N.getStr("tree-item.nofilename"); 42 } else if (piece === ROOT_LIST) { 43 // Don't use the usual labeling machinery for root lists: replace it 44 // with the "GC Roots" string. 45 sanitized.splice(0, label.length); 46 sanitized.push(L10N.getStr("tree-item.rootlist")); 47 break; 48 } else { 49 sanitized[i] = "" + piece; 50 } 51 } 52 53 return `${sanitized.join(" › ")} @ 0x${id.toString(16)}`; 54 } 55 56 class ShortestPaths extends Component { 57 static get propTypes() { 58 return { 59 graph: PropTypes.shape({ 60 nodes: PropTypes.arrayOf(PropTypes.object), 61 edges: PropTypes.arrayOf(PropTypes.object), 62 }), 63 }; 64 } 65 66 constructor(props) { 67 super(props); 68 this.state = { zoom: null }; 69 this._renderGraph = this._renderGraph.bind(this); 70 } 71 72 componentDidMount() { 73 if (this.props.graph) { 74 this._renderGraph(this.refs.container, this.props.graph); 75 } 76 } 77 78 shouldComponentUpdate(nextProps) { 79 return this.props.graph != nextProps.graph; 80 } 81 82 componentDidUpdate() { 83 if (this.props.graph) { 84 this._renderGraph(this.refs.container, this.props.graph); 85 } 86 } 87 88 componentWillUnmount() { 89 if (this.state.zoom) { 90 this.state.zoom.on("zoom", null); 91 } 92 } 93 94 _renderGraph(container, { nodes, edges }) { 95 if (!container.firstChild) { 96 const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); 97 svg.setAttribute("id", "graph-svg"); 98 svg.setAttribute("xlink", "http://www.w3.org/1999/xlink"); 99 svg.style.width = "100%"; 100 svg.style.height = "100%"; 101 102 const target = document.createElementNS( 103 "http://www.w3.org/2000/svg", 104 "g" 105 ); 106 target.setAttribute("id", "graph-target"); 107 target.style.width = "100%"; 108 target.style.height = "100%"; 109 110 svg.appendChild(target); 111 container.appendChild(svg); 112 } 113 114 const graph = new dagreD3.Digraph(); 115 116 for (let i = 0; i < nodes.length; i++) { 117 graph.addNode(nodes[i].id, { 118 id: nodes[i].id, 119 label: stringifyLabel(nodes[i].label, nodes[i].id), 120 }); 121 } 122 123 for (let i = 0; i < edges.length; i++) { 124 graph.addEdge(null, edges[i].from, edges[i].to, { 125 label: edges[i].name, 126 }); 127 } 128 129 const renderer = new dagreD3.Renderer(); 130 renderer.drawNodes(); 131 renderer.drawEdgePaths(); 132 133 const svg = d3.select("#graph-svg"); 134 const target = d3.select("#graph-target"); 135 136 let zoom = this.state.zoom; 137 if (!zoom) { 138 zoom = d3.behavior.zoom().on("zoom", function () { 139 target.attr( 140 "transform", 141 `translate(${d3.event.translate}) scale(${d3.event.scale})` 142 ); 143 }); 144 svg.call(zoom); 145 this.setState({ zoom }); 146 } 147 148 const { translate, scale } = GRAPH_DEFAULTS; 149 zoom.scale(scale); 150 zoom.translate(translate); 151 target.attr("transform", `translate(${translate}) scale(${scale})`); 152 153 const layout = dagreD3.layout(); 154 renderer.layout(layout).run(graph, target); 155 } 156 157 render() { 158 let contents; 159 if (this.props.graph) { 160 // Let the componentDidMount or componentDidUpdate method draw the graph 161 // with DagreD3. We just provide the container for the graph here. 162 contents = dom.div({ 163 ref: "container", 164 style: { 165 flex: 1, 166 height: "100%", 167 width: "100%", 168 }, 169 }); 170 } else { 171 contents = dom.div( 172 { 173 id: "shortest-paths-select-node-msg", 174 }, 175 L10N.getStr("shortest-paths.select-node") 176 ); 177 } 178 179 return dom.div( 180 { 181 id: "shortest-paths", 182 className: "vbox", 183 }, 184 dom.label( 185 { 186 id: "shortest-paths-header", 187 className: "header", 188 }, 189 L10N.getStr("shortest-paths.header") 190 ), 191 contents 192 ); 193 } 194 } 195 196 module.exports = ShortestPaths;