// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. // TODO: // - starting dominator treemap from a group of nodes instead of only a // single node "use strict"; function Graph() { // Create all slots up front to prevent V8 map transitions. // We extensively use parallel arrays instead of objects like // // function Vertex { // this.size = 0; // this.successors = new Array(); // this.predecessors = new Array(); // this.semi = 0; // this.dom = 0; // this.label = 0; // this.parent = 0; // this.ancestor = 0; // } // // This avoids GC work in V8, and it allows us to release the memory for // intermediate values that are only needed while computing the dominators. // Inputs. this.N_ = 0; // Number of nodes. this.E_ = 0; // Number of edges. this.strings_ = null; this.name_ = null; this.class_ = null; this.shallowSize_ = null; this.firstSuccessor_ = null; this.successors_ = null; this.successorName_ = null; // Outputs. this.shallowSizeSum_ = 0; this.firstPredecessor_ = null; this.predecessors_ = null; this.predecessorName_ = null; this.retainedSize_ = null; this.dom_ = null; this.domHead_ = null; this.domNext_ = null; // Intermediates. this.Nconnected_ = 0; // Number of nodes reachable from root. this.vertex_ = null; this.semi_ = null; this.parent_ = null; this.ancestor_ = null; this.label_ = null; this.bucket_ = null; // Recycled memory. this.mark_ = null; this.stack_ = null; } // Load a graph in V8 heap profile format from `data`, then compute the graph's // dominator tree and the retained size of each vertex. // // If `rewriteForOwners` is true, for each vertex that has an "owner" edge, // replace all edges to the vertex with an edge from the owner to the vertex. // This can be the graph more hierachical and reveal more structure in the // dominator tree. Graph.prototype.loadV8Profile = function(data, rewriteForOwners) { console.log("Building successors..."); const N = data.snapshot.node_count; const E = data.snapshot.edge_count; const firstSuccessor = new Uint32Array(N + 2); const successors = new Uint32Array(E); const successorName = new Uint32Array(E); const name = new Uint32Array(N + 1); const clazz = new Array(N + 1); const shallowSize = new Uint32Array(N + 1); let shallowSizeSum = 0; const node_stride = data.snapshot.meta.node_fields.length; const node_type_offset = data.snapshot.meta.node_fields.indexOf("type"); const node_name_offset = data.snapshot.meta.node_fields.indexOf("name"); const node_id_offset = data.snapshot.meta.node_fields.indexOf("id"); const node_size_offset = data.snapshot.meta.node_fields.indexOf("self_size"); const node_edge_count_offset = data.snapshot.meta.node_fields.indexOf("edge_count"); const edge_stride = data.snapshot.meta.edge_fields.length; const edge_type_offset = data.snapshot.meta.edge_fields.indexOf("type"); const edge_name_or_index_offset = data.snapshot.meta.edge_fields.indexOf("name_or_index"); const edge_target_offset = data.snapshot.meta.edge_fields.indexOf("to_node"); const edge_type_property = data.snapshot.meta.edge_types[0].indexOf("property"); let nextSuccessorIndex = 0; let edge_cursor = 0; let i = 1; for (let node_cursor = 0; node_cursor < data.nodes.length; node_cursor += node_stride) { let type = data.nodes[node_cursor + node_type_offset]; let node_name = data.nodes[node_cursor + node_name_offset]; let id = data.nodes[node_cursor + node_id_offset]; let self_size = data.nodes[node_cursor + node_size_offset]; let edge_count = data.nodes[node_cursor + node_edge_count_offset]; name[i] = node_name; clazz[i] = data.snapshot.meta.node_types[0][type]; shallowSize[i] = self_size; shallowSizeSum += self_size; firstSuccessor[i] = nextSuccessorIndex; for (let j = 0; j < edge_count; j++) { let edge_type = data.edges[edge_cursor + edge_type_offset]; let edge_name_or_index = data.edges[edge_cursor + edge_name_or_index_offset]; let edge_to_node = (data.edges[edge_cursor + edge_target_offset] / node_stride) + 1; edge_cursor += edge_stride; if (edge_to_node < 1 || edge_to_node > N) { throw "Invalid edge target"; } successors[nextSuccessorIndex] = edge_to_node; if (edge_type == edge_type_property) { successorName[nextSuccessorIndex] = edge_name_or_index; } nextSuccessorIndex++; } i++; } firstSuccessor[N + 1] = nextSuccessorIndex; if (i != (N + 1)) { throw "Incorrect node_count!"; } if (nextSuccessorIndex != E) { throw "Incorrect edge_count!"; } // Free memory. data["nodes"] = null; data["edges"] = null; this.N_ = N; this.E_ = E; this.strings_ = data.strings; this.firstSuccessor_ = firstSuccessor; this.successorName_ = successorName; this.successors_ = successors; this.name_ = name; this.class_ = clazz; this.shallowSize_ = shallowSize; this.shallowSizeSum_ = shallowSizeSum; this.computePredecessors(); if (rewriteForOwners) { this.rewriteEdgesForOwners(); } this.computePreorder(1); this.computeDominators(); this.computeRetainedSizes(); this.linkDominatorChildren(); this.mark_ = new Uint8Array(N + 1); this.stack_ = new Uint32Array(E); }; Graph.prototype.computePredecessors = function() { console.log("Building predecessors..."); const N = this.N_; const E = this.E_; const firstSuccessor = this.firstSuccessor_; const successors = this.successors_; const successorName = this.successorName_; const firstPredecessor = new Uint32Array(N + 2); const predecessors = new Uint32Array(E); const predecessorName = new Uint32Array(E); const predecessorCount = new Uint32Array(N + 1); for (let i = 1; i <= N; i++) { let firstSuccessorIndex = firstSuccessor[i]; let lastSuccessorIndex = firstSuccessor[i + 1]; for (let successorIndex = firstSuccessorIndex; successorIndex < lastSuccessorIndex; successorIndex++) { let successor = successors[successorIndex]; predecessorCount[successor]++; } } let nextPredecessorIndex = 0; for (let i = 1; i <= N; i++) { firstPredecessor[i] = nextPredecessorIndex; nextPredecessorIndex += predecessorCount[i]; } firstPredecessor[N + 1] = nextPredecessorIndex; if (nextPredecessorIndex != E) { throw "Mismatched edges"; } for (let i = 1; i <= N; i++) { let firstSuccessorIndex = firstSuccessor[i]; let lastSuccessorIndex = firstSuccessor[i + 1]; for (let successorIndex = firstSuccessorIndex; successorIndex < lastSuccessorIndex; successorIndex++) { let successor = successors[successorIndex]; let count = --predecessorCount[successor]; let predecessorIndex = firstPredecessor[successor] + count; predecessors[predecessorIndex] = i; predecessorName[predecessorIndex] = successorName[successorIndex]; } } this.firstPredecessor_ = firstPredecessor; this.predecessors_ = predecessors; this.predecessorName_ = predecessorName; }; Graph.prototype.rewriteEdgesForOwners = function() { console.log("Rewriting edges for owners..."); // Rewrite some edges to make the graph more hierarchical. // If there is an edge A.owner -> B, // - remove all edges to A, and // - add edge B. -> A. const N = this.N_; const E = this.E_; const firstSuccessor = this.firstSuccessor_; const successors = this.successors_; const successorName = this.successorName_; const firstPredecessor = this.firstPredecessor_; const predecessors = this.predecessors_; const predecessorName = this.predecessorName_; const owners = new Uint32Array(N + 1); const owneeCount = new Uint32Array(N + 1); // Identify owner. for (let i = 1; i <= N; i++) { let cls = this.class_[i]; let ownerEdgeName; if (cls == "Class") { ownerEdgeName = "library_"; } else if (cls == "PatchClass") { ownerEdgeName = "patched_class_"; } else if (cls == "Function") { ownerEdgeName = "owner_"; } else if (cls == "Field") { ownerEdgeName = "owner_"; } else if (cls == "Code") { ownerEdgeName = "owner_"; } else if (cls == "ICData") { ownerEdgeName = "owner_"; } else { continue; } let firstSuccessorIndex = firstSuccessor[i]; let lastSuccessorIndex = firstSuccessor[i + 1]; for (let successorIndex = firstSuccessorIndex; successorIndex < lastSuccessorIndex; successorIndex++) { let edge = this.strings_[this.successorName_[successorIndex]]; if (edge == ownerEdgeName) { let owner = successors[successorIndex]; owners[i] = owner; owneeCount[owner]++; break; } } } // Remove successors if the target has an owner. // Allocate space for extra successors added to owners. const newSuccessors = new Uint32Array(E); const newSuccessorName = new Uint32Array(E); let newSuccessorIndex = 0; for (let i = 1; i <= N; i++) { let firstSuccessorIndex = firstSuccessor[i]; let lastSuccessorIndex = firstSuccessor[i + 1]; firstSuccessor[i] = newSuccessorIndex; for (let successorIndex = firstSuccessorIndex; successorIndex < lastSuccessorIndex; successorIndex++) { let successor = successors[successorIndex]; let name = successorName[successorIndex]; if (owners[successor] != 0) { // Drop successor. } else { newSuccessors[newSuccessorIndex] = successor; newSuccessorName[newSuccessorIndex] = name; newSuccessorIndex++; } } newSuccessorIndex += owneeCount[i]; } firstSuccessor[N + 1] = newSuccessorIndex; // Remove predecessors if the target has an owner. // Add the owner as a predecessor. // Add extra successors for owner. for (let i = 1; i <= N; i++) { let owner = owners[i]; if (owner == 0) { continue; } let firstPredecessorIndex = firstPredecessor[i]; let lastPredecessorIndex = firstPredecessor[i + 1]; for (let predecessorIndex = firstPredecessorIndex; predecessorIndex < lastPredecessorIndex; predecessorIndex++) { predecessors[predecessorIndex] = 0; } predecessors[firstPredecessorIndex] = owner; let nextSuccessorIndex = firstSuccessor[owner + 1] - owneeCount[owner]; newSuccessors[nextSuccessorIndex] = i; owneeCount[owner]--; } this.successors_ = newSuccessors; this.successorName_ = newSuccessorName; }; // Thomas Lengauer and Robert Endre Tarjan. 1979. A fast algorithm for finding // dominators in a flowgraph. ACM Trans. Program. Lang. Syst. 1, 1 (January // 1979), 121-141. DOI: https://doi.org/10.1145/357062.357071 Graph.prototype.computePreorder = function(root) { console.log("Computing preorder..."); // Lengauer and Tarjan Step 1. const N = this.N_; const firstSuccessor = this.firstSuccessor_; const successors = this.successors_; const semi = new Uint32Array(N + 1); const vertex = new Uint32Array(N + 1); const ancestor = new Uint32Array(N + 1); const parent = new Uint32Array(N + 1); const label = new Uint32Array(N + 1); let preorderNumber = 0; let stackNodes = new Uint32Array(N + 1); let stackEdges = new Uint32Array(N + 1); let stackTop = 0; // Push root. preorderNumber++; vertex[preorderNumber] = root; semi[root] = preorderNumber; label[root] = root; ancestor[root] = 0; stackNodes[stackTop] = root; stackEdges[stackTop] = firstSuccessor[root]; while (stackTop >= 0) { let v = stackNodes[stackTop]; let e = stackEdges[stackTop]; if (e < firstSuccessor[v + 1]) { // Visit next successor. let w = successors[e]; e++; stackEdges[stackTop] = e; if (semi[w] == 0) { parent[w] = v; preorderNumber++; vertex[preorderNumber] = w; semi[w] = preorderNumber; label[w] = w; ancestor[w] = 0; // Push successor. stackTop++; stackNodes[stackTop] = w; stackEdges[stackTop] = firstSuccessor[w]; } } else { // No more successors: pop. stackTop--; } } this.Nconnected_ = preorderNumber; if (this.Nconnected_ != N) { console.log("Graph is not fully connected: " + this.Nconnected_ + " nodes are reachable, but graph has " + this.N_ + " nodes"); } this.semi_ = semi; this.vertex_ = vertex; this.ancestor_ = ancestor; this.parent_ = parent; this.label_ = label; }; Graph.prototype.computeDominators = function() { console.log("Computing dominator tree..."); const N = this.N_; const Nconnected = this.Nconnected_; const firstPredecessor = this.firstPredecessor_; const predecessors = this.predecessors_; const vertex = this.vertex_; const semi = this.semi_; const bucket = new Array(N + 1); const parent = this.parent_; const dom = new Uint32Array(N + 1); for (let i = Nconnected; i > 1; i--) { let w = vertex[i]; // Lengauer and Tarjan Step 2. let firstPredecessorIndex = firstPredecessor[w]; let lastPredecessorIndex = firstPredecessor[w + 1]; for (let predecessorIndex = firstPredecessorIndex; predecessorIndex < lastPredecessorIndex; predecessorIndex++) { let v = predecessors[predecessorIndex]; if (semi[v] == 0) { // The predecessor was not reachable from the root: ignore // this edge. continue; } let u = this.forestEval(v); if (semi[u] < semi[w]) { semi[w] = semi[u] } } let z = vertex[semi[w]]; let b = bucket[z]; if (b == null) { b = new Array(); bucket[z] = b; } b.push(w); this.forestLink(parent[w], w); // Lengauer and Tarjan Step 3. z = parent[w]; b = bucket[z]; bucket[z] = null; if (b != null) { for (let j = 0; j < b.length; j++) { let v = b[j]; let u = this.forestEval(v); dom[v] = semi[u] < semi[v] ? u : parent[w]; } } } this.ancestor_ = null; this.label_ = null; this.parent_ = null; this.bucket_ = null; // Lengauer and Tarjan Step 4. for (let i = 2; i <= Nconnected; i++) { let w = vertex[i]; if (dom[w] != vertex[semi[w]]) { dom[w] = dom[dom[w]]; } } dom[vertex[1]] = 0; this.semi_ = null; this.dom_ = dom; }; Graph.prototype.forestCompress = function(v) { const ancestor = this.ancestor_; if (ancestor[ancestor[v]] != 0) { this.forestCompress(ancestor[v]); const semi = this.semi_; const label = this.label_; if (semi[label[ancestor[v]]] < semi[label[v]]) { label[v] = label[ancestor[v]]; } ancestor[v] = ancestor[ancestor[v]]; } }; Graph.prototype.forestEval = function(v) { if (this.ancestor_[v] == 0) { return v; } else { this.forestCompress(v); return this.label_[v]; } }; Graph.prototype.forestLink = function(v, w) { this.ancestor_[w] = v; }; Graph.prototype.computeRetainedSizes = function() { console.log("Computing retained sizes..."); const N = this.N_; const Nconnected = this.Nconnected_; const vertex = this.vertex_; const dom = this.dom_; const shallowSize = this.shallowSize_; const retainedSize = new Uint32Array(N + 1); let shallowSum = 0; for (let i = 1; i <= Nconnected; i++) { let w = vertex[i]; let shallow = shallowSize[w]; retainedSize[w] = shallow; shallowSum += shallow; } for (let i = Nconnected; i > 1; i--) { let w = vertex[i]; retainedSize[dom[w]] += retainedSize[w]; } if (retainedSize[vertex[1]] != shallowSum) { console.log("Retained size mismatch: root retains " + retainedSize[vertex[1]] + " but shallow sizes sum to " + shallowSum); } this.retainedSize_ = retainedSize; }; Graph.prototype.linkDominatorChildren = function() { console.log("Linking dominator tree children..."); const N = this.N_; const Nconnected = this.Nconnected_; const vertex = this.vertex_; const dom = this.dom_; const head = new Uint32Array(N + 1); const next = new Uint32Array(N + 1); for (let i = 2; i <= Nconnected; i++) { let child = vertex[i]; let parent = dom[child]; next[child] = head[parent]; head[parent] = child; } this.domHead_ = head; this.domNext_ = next; }; Graph.prototype.getTotalSize = function() { return this.shallowSizeSum_; }; Graph.prototype.getRoot = function() { return this.vertex_[1]; }; Graph.prototype.getAll = function() { let nodes = new Array(); for (let v = 1; v <= this.N_; v++) { nodes.push(v); } return nodes; }; function removeDuplicates(array) { let set = new Set(array); let result = new Array(); for (let element of set) { result.push(element); } return result } Graph.prototype.successorsOfDo = function(v, action) { let cls = this.class_[v]; let firstSuccessorIndex = this.firstSuccessor_[v]; let lastSuccessorIndex = this.firstSuccessor_[v + 1]; for (let successorIndex = firstSuccessorIndex; successorIndex < lastSuccessorIndex; successorIndex++) { let successor = this.successors_[successorIndex]; let edgeName = this.strings_[this.successorName_[successorIndex]]; action(successor, cls + "::" + edgeName); } } Graph.prototype.predecessorsOfDo = function(v, action) { let firstPredecessorIndex = this.firstPredecessor_[v]; let lastPredecessorIndex = this.firstPredecessor_[v + 1]; for (let predecessorIndex = firstPredecessorIndex; predecessorIndex < lastPredecessorIndex; predecessorIndex++) { let predecessor = this.predecessors_[predecessorIndex]; let cls = this.class_[predecessor]; let edgeName = this.strings_[this.predecessorName_[predecessorIndex]]; action(predecessor, cls + "::" + edgeName); } } Graph.prototype.dominatorChildrenOfDo = function(v, action) { for (let w = this.domHead_[v]; w != 0; w = this.domNext_[w]) { action(w); } }; Graph.prototype.nameOf = function(v) { return this.strings_[this.name_[v]]; }; Graph.prototype.classOf = function(v) { return this.class_[v]; }; Graph.prototype.shallowSizeOf = function(v) { return this.shallowSize_[v]; }; Graph.prototype.retainedSizeOf = function(v) { return this.retainedSize_[v]; }; Graph.prototype.shallowSizeOfSet = function(nodes) { let sum = 0; for (let i = 0; i < nodes.length; i++) { sum += this.shallowSize_[nodes[i]]; } return sum; }; Graph.prototype.retainedSizeOfSet = function(nodes) { const N = this.N_; const E = this.E_; const mark = this.mark_; const stack = this.stack_; for (let i = 1; i <= N; i++) { mark[i] = 0; } for (let i = 0; i < nodes.length; i++) { let v = nodes[i]; mark[v] = 1; } let scan = 0; let top = 0; stack[top++] = 1; while (scan < top) { let v = stack[scan++]; let firstSuccessorIndex = this.firstSuccessor_[v]; let lastSuccessorIndex = this.firstSuccessor_[v + 1]; for (let successorIndex = firstSuccessorIndex; successorIndex < lastSuccessorIndex; successorIndex++) { let successor = this.successors_[successorIndex]; if (mark[successor] == 0) { mark[successor] = 1; stack[top++] = successor; } } } for (let i = 0; i < nodes.length; i++) { let v = nodes[i]; mark[v] = 0; } for (let i = 0; i < nodes.length; i++) { let v = nodes[i]; if (mark[v] == 0) { mark[v] = 1; stack[top++] = v; } } let sum = 0; while (scan < top) { let v = stack[scan++]; sum += this.shallowSize_[v]; let firstSuccessorIndex = this.firstSuccessor_[v]; let lastSuccessorIndex = this.firstSuccessor_[v + 1]; for (let successorIndex = firstSuccessorIndex; successorIndex < lastSuccessorIndex; successorIndex++) { let successor = this.successors_[successorIndex]; if (mark[successor] == 0) { mark[successor] = 1; stack[top++] = successor; } } } return sum; }; function hash(string) { // Jenkin's one_at_a_time. let h = string.length; for (let i = 0; i < string.length; i++) { h += string.charCodeAt(i); h += h << 10; h ^= h >> 6; } h += h << 3; h ^= h >> 11; h += h << 15; return h; } function color(string) { let hue = hash(string) % 360; return "hsl(" + hue + ",60%,60%)"; } function prettySize(size) { if (size < 1024) return size + "B"; size /= 1024; if (size < 1024) return size.toFixed(1) + "KiB"; size /= 1024; if (size < 1024) return size.toFixed(1) + "MiB"; size /= 1024; return size.toFixed(1) + "GiB"; } function prettyPercent(fraction) { return (fraction * 100).toFixed(1); } function createTreemapTile(v, width, height, depth) { let div = document.createElement("div"); div.className = "treemapTile"; div.style["background-color"] = color(graph.classOf(v)); div.ondblclick = function(event) { event.stopPropagation(); if (depth == 0) { let dom = graph.dom_[v]; if (dom == 0) { // Already at root. } else { showDominatorTree(dom); // Zoom out. } } else { showDominatorTree(v); // Zoom in. } }; div.oncontextmenu = function(event) { event.stopPropagation(); event.preventDefault(); showTables([v]); }; let left = 0; let top = 0; const kPadding = 5; const kBorder = 1; left += kPadding - kBorder; top += kPadding - kBorder; width -= 2 * kPadding; height -= 2 * kPadding; div.title = graph.nameOf(v) + " \nclass: " + graph.classOf(v) + " \nretained: " + graph.retainedSizeOf(v) + " \nshallow: " + graph.shallowSizeOf(v); if (width < 10 || height < 10) { // Too small: don't render label or children. return div; } let label = graph.nameOf(v) + " [" + prettySize(graph.retainedSizeOf(v)) + "]"; div.appendChild(document.createTextNode(label)); const kLabelHeight = 9; top += kLabelHeight; height -= kLabelHeight; if (depth > 2) { // Too deep: don't render children. return div; } if (width < 4 || height < 4) { // Too small: don't render children. return div; } let children = new Array(); graph.dominatorChildrenOfDo(v, function(c) { // Size 0 children seem to confuse the layout algorithm (accumulating // rounding errors?). if (graph.retainedSizeOf(c) > 0) { children.push(c); } }); children.sort(function (a, b) { return graph.retainedSizeOf(b) - graph.retainedSizeOf(a); }); const scale = width * height / graph.retainedSizeOf(v); // Bruls M., Huizing K., van Wijk J.J. (2000) Squarified Treemaps. In: de // Leeuw W.C., van Liere R. (eds) Data Visualization 2000. Eurographics. // Springer, Vienna. for (let rowStart = 0; // Index of first child in the next row. rowStart < children.length;) { // Prefer wider rectangles, the better to fit text labels. const GOLDEN_RATIO = 1.61803398875; let verticalSplit = (width / height) > GOLDEN_RATIO; let space; if (verticalSplit) { space = height; } else { space = width; } let rowMin = graph.retainedSizeOf(children[rowStart]) * scale; let rowMax = rowMin; let rowSum = 0; let lastRatio = 0; let rowEnd; // One after index of last child in the next row. for (rowEnd = rowStart; rowEnd < children.length; rowEnd++) { let size = graph.retainedSizeOf(children[rowEnd]) * scale; if (size < rowMin) rowMin = size; if (size > rowMax) rowMax = size; rowSum += size; let ratio = Math.max((space * space * rowMax) / (rowSum * rowSum), (rowSum * rowSum) / (space * space * rowMin)); if ((lastRatio != 0) && (ratio > lastRatio)) { // Adding the next child makes the aspect ratios worse: remove it and // add the row. rowSum -= size; break; } lastRatio = ratio; } let rowLeft = left; let rowTop = top; let rowSpace = rowSum / space; for (let i = rowStart; i < rowEnd; i++) { let child = children[i]; let size = graph.retainedSizeOf(child) * scale; let childWidth; let childHeight; if (verticalSplit) { childWidth = rowSpace; childHeight = size / childWidth; } else { childHeight = rowSpace; childWidth = size / childHeight; } let childDiv = createTreemapTile(child, childWidth, childHeight, depth + 1); childDiv.style.left = rowLeft + "px"; childDiv.style.top = rowTop + "px"; // Oversize the final div by kBorder to make the borders overlap. childDiv.style.width = (childWidth + kBorder) + "px"; childDiv.style.height = (childHeight + kBorder) + "px"; div.appendChild(childDiv); if (verticalSplit) rowTop += childHeight; else rowLeft += childWidth; } if (verticalSplit) { left += rowSpace; width -= rowSpace; } else { top += rowSpace; height -= rowSpace; } rowStart = rowEnd; } return div; } function showDominatorTree(v) { let header = document.createElement("div"); header.textContent = "Dominator Tree"; header.title = "Double click a box to zoom in.\n" + "Double click the outermost box to zoom out.\n" + "Right click a box to view successor and predecessor tables."; header.className = "headerRow"; header.style["flex-grow"] = 0; header.style["padding"] = "5px"; header.style["border-bottom"] = "solid 1px"; let content = document.createElement("div"); content.style["flex-basis"] = 0; content.style["flex-grow"] = 1; let column = document.createElement("div"); column.style["width"] = "100%"; column.style["height"] = "100%"; column.style["border"] = "solid 2px"; column.style["display"] = "flex"; column.style["flex-direction"] = "column"; column.appendChild(header); column.appendChild(content); setBody(column); // Add the content div to the document first so the browser will calculate // the available width and height. let w = content.offsetWidth; let h = content.offsetHeight; let topTile = createTreemapTile(v, w, h, 0); topTile.style.width = w; topTile.style.height = h; topTile.style.border = "none"; content.appendChild(topTile); } function Group(edge, cls) { this.name = ""; this.edge = edge; this.cls = cls; this.shallowSize = 0; this.retainedSize = 0; this.nodes = new Array(); } Group.prototype.add = function(v, edge, cls) { this.nodes.push(v); if (this.edge != edge) { this.edge = ""; } if (this.cls != cls) { this.cls = "" } }; function asGroupsByClass(labeledNodes) { let map = new Map(); let groups = new Array(); for (let i = 0; i < labeledNodes.length; i++) { let v = labeledNodes[i].node; let edge = labeledNodes[i].name; let cls = graph.classOf(v); let group = map.get(cls); if (group === undefined) { group = new Group(edge, cls); groups.push(group); map.set(cls, group); } group.add(v, edge, cls); } for (let i = 0; i < groups.length; i++) { let group = groups[i]; group.shallowSize = graph.shallowSizeOfSet(group.nodes); group.retainedSize = graph.retainedSizeOfSet(group.nodes); group.name = group.nodes.length + " instances of " + group.cls; } return groups; } function LabeledNode(node, name) { this.node = node; this.name = name; } LabeledNode.prototype.addName = function(name) { if (this.name != name) { this.name = ""; } }; function Table(title, labeledNodes, byEdges) { const self = this; this.title = title; this.labeledNodes = labeledNodes; this.groupsByEdge = byEdges; this.groupsByClass = asGroupsByClass(labeledNodes); this.grouping = 0; this.nom = document.createElement("span"); this.nom.onclick = function() { self.grouping = (self.grouping + 1) % 3; self.refreshRows(); }; this.nom.className = "nameCell actionCell" this.nom.textContent = title; this.nom.title = "Toggle grouping by object, class or edge"; let edge = document.createElement("span"); edge.onclick = function() { self.sortByEdge(); }; edge.className = "edgeCell actionCell"; edge.textContent = "Edge"; edge.title = "Sort by edge"; let cls = document.createElement("span"); cls.onclick = function() { self.sortByClass(); }; cls.className = "classCell actionCell"; cls.textContent = "Class"; cls.title = "Sort by class"; let shallowSize = document.createElement("span"); shallowSize.onclick = function() { self.sortByShallowSize(); }; shallowSize.className = "sizeCell actionCell"; shallowSize.textContent = "Size"; shallowSize.title = "Sort by shallow size"; let shallowPercent = document.createElement("span"); shallowPercent.className = "sizePercentCell" let retainedSize = document.createElement("span"); retainedSize.onclick = function() { self.sortByRetainedSize(); }; retainedSize.className = "sizeCell actionCell"; retainedSize.textContent = "Retained Size"; retainedSize.title = "Sort by retained size"; let retainedPercent = document.createElement("span"); retainedPercent.className = "sizePercentCell"; let header = document.createElement("div"); header.className = "headerRow"; header.style["display"] = "flex"; header.style["flex-direction"] = "row"; header.style["border-bottom"] = "solid 1px"; header.appendChild(this.nom); header.appendChild(edge); header.appendChild(cls); header.appendChild(shallowSize); header.appendChild(shallowPercent); header.appendChild(retainedSize); header.appendChild(retainedPercent); this.listDiv = document.createElement("div"); this.listDiv.style["overflow-y"] = "scroll"; this.listDiv.style["flex-basis"] = "0px"; this.listDiv.style["flex-grow"] = 1; this.div = document.createElement("div"); this.div.style["border"] = "solid 2px"; this.div.style["display"] = "flex"; this.div.style["flex-direction"] = "column"; this.div.style["background-color"] = "#DDDDDD"; this.div.appendChild(header); this.div.appendChild(this.listDiv); this.refreshRows(); } Table.prototype.sortByEdge = function() { this.labeledNodes.sort(function (a, b) { return a.name.localeCompare(b.name); }); this.groupsByEdge.sort(function (a, b) { return a.edge.localeCompare(b.edge); }); this.groupsByClass.sort(function (a, b) { return a.edge.localeCompare(b.edge); }); this.refreshRows(); }; Table.prototype.sortByClass = function() { this.labeledNodes.sort(function (a, b) { return graph.classOf(a.node).localeCompare(graph.classOf(b.node)); }); this.groupsByEdge.sort(function (a, b) { return a.cls.localeCompare(b.cls); }); this.groupsByClass.sort(function (a, b) { return a.cls.localeCompare(b.cls); }); this.refreshRows(); }; Table.prototype.sortByShallowSize = function() { this.labeledNodes.sort(function (a, b) { return graph.shallowSizeOf(b.node) - graph.shallowSizeOf(a.node); }); this.groupsByEdge.sort(function (a, b) { return b.shallowSize - a.shallowSize; }); this.groupsByClass.sort(function (a, b) { return b.shallowSize - a.shallowSize; }); this.refreshRows(); }; Table.prototype.sortByRetainedSize = function() { this.labeledNodes.sort(function (a, b) { return graph.retainedSizeOf(b.node) - graph.retainedSizeOf(a.node); }); this.groupsByEdge.sort(function (a, b) { return b.retainedSize - a.retainedSize; }); this.groupsByClass.sort(function (a, b) { return b.retainedSize - a.retainedSize; }); this.refreshRows(); }; Table.prototype.refreshRows = function() { while (this.listDiv.firstChild) { this.listDiv.removeChild(this.listDiv.firstChild); } // To prevent rendering lag. const MAX_TABLE_ROWS = 500; if (this.grouping == 0) { let labeledNodes = this.labeledNodes; this.nom.textContent = this.title + " (" + labeledNodes.length + " objects)"; for (let i = 0; i < labeledNodes.length; i++) { if (i > MAX_TABLE_ROWS) { this.listDiv.appendChild(document.createTextNode((labeledNodes.length - i) + " more objects")); break; } this.listDiv.appendChild(createObjectRow(labeledNodes[i])); } } else if (this.grouping == 1) { let groups = this.groupsByClass; this.nom.textContent = this.title + " (" + groups.length + " classes)"; for (let i = 0; i < groups.length; i++) { if (i > MAX_TABLE_ROWS) { // Prevent rendering lag. this.listDiv.appendChild(document.createTextNode((groups.length - i) + " more classes")); break; } this.listDiv.appendChild(createGroupRow(groups[i])); } } else { let groups = this.groupsByEdge; this.nom.textContent = this.title + " (" + groups.length + " edges)"; for (let i = 0; i < groups.length; i++) { if (i > MAX_TABLE_ROWS) { // Prevent rendering lag. this.listDiv.appendChild(document.createTextNode((groups.length - i) + " more edges")); break; } this.listDiv.appendChild(createGroupRow(groups[i])); } } }; function createObjectRow(labeledNode) { const v = labeledNode.node; let nom = document.createElement("span"); nom.onclick = function() { showTables([v]); } nom.className = "nameCell actionCell"; nom.textContent = graph.nameOf(v); nom.title = "Select this object"; let edge = document.createElement("span"); edge.className = "edgeCell"; edge.textContent = labeledNode.name; let cls = document.createElement("span"); cls.className = "classCell"; cls.textContent = graph.classOf(v); let shallowSize = document.createElement("span"); shallowSize.className = "sizeCell"; shallowSize.textContent = graph.shallowSizeOf(v); let shallowPercent = document.createElement("span"); shallowPercent.className = "sizePercentCell"; shallowPercent.textContent = prettyPercent(graph.shallowSizeOf(v) / graph.getTotalSize()); let retainedSize = document.createElement("span"); retainedSize.onclick = function(event) { showDominatorTree(v); }; retainedSize.className = "sizeCell actionCell"; retainedSize.textContent = graph.retainedSizeOf(v); retainedSize.title = "Show dominator tree"; let retainedPercent = document.createElement("span"); retainedPercent.onclick = function(event) { showDominatorTree(v); }; retainedPercent.className = "sizePercentCell actionCell"; retainedPercent.textContent = prettyPercent(graph.retainedSizeOf(v) / graph.getTotalSize()); retainedPercent.title = "Show dominator tree"; let row = document.createElement("div"); row.style["display"] = "flex"; row.style["flex-direction"] = "row"; row.style["border-bottom"] = "solid 1px"; row.appendChild(nom); row.appendChild(edge); row.appendChild(cls); row.appendChild(shallowSize); row.appendChild(shallowPercent); row.appendChild(retainedSize); row.appendChild(retainedPercent); return row; } function createGroupRow(g) { let nom = document.createElement("span"); nom.onclick = function() { showTables(g.nodes); } nom.className = "nameCell actionCell"; nom.textContent = g.name; nom.title = "Select these objects"; let cls = document.createElement("span"); cls.className = "classCell"; cls.textContent = g.cls; let edge = document.createElement("span"); edge.className = "edgeCell"; edge.textContent = g.edge; let shallowSize = document.createElement("span"); shallowSize.className = "sizeCell"; shallowSize.textContent = g.shallowSize; let shallowPercent = document.createElement("span"); shallowPercent.className = "sizePercentCell"; shallowPercent.textContent = prettyPercent(g.shallowSize / graph.getTotalSize()); let retainedSize = document.createElement("span"); retainedSize.className = "sizeCell"; retainedSize.textContent = g.retainedSize; let retainedPercent = document.createElement("span"); retainedPercent.className = "sizePercentCell"; retainedPercent.textContent = prettyPercent(g.retainedSize / graph.getTotalSize()); let row = document.createElement("div"); row.style["display"] = "flex"; row.style["flex-direction"] = "row"; row.style["border-bottom"] = "solid 1px"; row.appendChild(nom); row.appendChild(edge); row.appendChild(cls); row.appendChild(shallowSize); row.appendChild(shallowPercent); row.appendChild(retainedSize); row.appendChild(retainedPercent); return row; } function mapValuesToArray(map) { let array = new Array(); for (let element of map.values()) { array.push(element); } return array; } function showTables(nodes) { let labeledNodes = new Array(); let successors = new Map(); let successorEdges = new Map(); let successorEdgeGroups = new Array(); let predecessors = new Map(); let predecessorEdges = new Map(); let predecessorEdgeGroups = new Array(); for (let i = 0; i < nodes.length; i++) { let n = nodes[i]; labeledNodes.push(new LabeledNode(n, "-")); graph.successorsOfDo(n, function (child, edgeName) { let e = successors.get(child); if (e) { e.addName(edgeName); } else { successors.set(child, new LabeledNode(child, edgeName)); } let cls = graph.classOf(child); let g = successorEdges.get(edgeName); if (!g) { g = new Group(edgeName, cls); successorEdgeGroups.push(g); successorEdges.set(edgeName, g); } g.add(child, edgeName, cls); }); graph.predecessorsOfDo(n, function (parent, edgeName) { let e = predecessors.get(parent); if (e) { e.addName(edgeName); } else { predecessors.set(parent, new LabeledNode(parent, edgeName)); } let cls = graph.classOf(parent); let g = predecessorEdges.get(edgeName); if (!g) { g = new Group(edgeName, cls); predecessorEdgeGroups.push(g); predecessorEdges.set(edgeName, g); } g.add(parent, edgeName, cls); }); } // Computing retained sizes here O((C + E) * N) where // N is the number of objects // C is the number of classes // E is the number of edges successors = mapValuesToArray(successors); for (let i = 0; i < successorEdgeGroups.length; i++) { let group = successorEdgeGroups[i]; group.nodes = removeDuplicates(group.nodes); group.shallowSize = graph.shallowSizeOfSet(group.nodes); group.retainedSize = graph.retainedSizeOfSet(group.nodes); group.name = group.nodes.length + " targets of " + group.edge; } predecessors = mapValuesToArray(predecessors); for (let i = 0; i < predecessorEdgeGroups.length; i++) { let group = predecessorEdgeGroups[i]; group.nodes = removeDuplicates(group.nodes); group.shallowSize = graph.shallowSizeOfSet(group.nodes); group.retainedSize = graph.retainedSizeOfSet(group.nodes); group.name = group.nodes.length + " sources of " + group.edge; } let rewrite = document.createElement("input"); rewrite.setAttribute("type", "checkbox"); let rewriteLabel = document.createElement("span"); rewriteLabel.textContent = "Owners "; let input = document.createElement("input"); input.setAttribute("type", "file"); input.setAttribute("multiple", false); input.onchange = function(event) { let file = event.target.files[0]; let reader = new FileReader(); reader.readAsText(file, 'UTF-8'); reader.onload = function(event) { let data = JSON.parse(event.target.result); document.title = file.name; graph = new Graph(); graph.loadV8Profile(data, rewrite.checked); data = null; // Release memory showTables([graph.getRoot()]); }; }; let selectRoot = document.createElement("button"); selectRoot.textContent = "Select Root"; selectRoot.onclick = function(event) { showTables([graph.getRoot()]); }; let selectAll = document.createElement("button"); selectAll.textContent = "Select All"; selectAll.onclick = function(event) { showTables(graph.getAll()); }; let filler = document.createElement("span"); filler.style["flex-grow"] = 1; let totalSize = document.createElement("span"); totalSize.className = "sizeCell"; totalSize.textContent = graph ? "" + graph.getTotalSize() : "0"; let totalPercent = document.createElement("span"); totalPercent.className = "sizePercentCell"; totalPercent.textContent = prettyPercent(1.0); let topBar = document.createElement("div"); topBar.className = "headerRow"; topBar.style["border"] = "solid 2px"; topBar.style["display"] = "flex"; topBar.style["flex-direction"] = "row"; topBar.style["align-items"] = "center"; topBar.appendChild(rewrite); topBar.appendChild(rewriteLabel); topBar.appendChild(input); if (graph) { topBar.appendChild(selectRoot); topBar.appendChild(selectAll); topBar.appendChild(filler); topBar.appendChild(totalSize); topBar.appendChild(totalPercent); } let selectionTable = new Table("Selection", labeledNodes, []).div; selectionTable.style["flex-basis"] = "0px"; selectionTable.style["flex-grow"] = 1; let successorsTable = new Table("Successors", successors, successorEdgeGroups).div; successorsTable.style["flex-basis"] = "0px"; successorsTable.style["flex-grow"] = 1; let predecessorsTable = new Table("Predecessors", predecessors, predecessorEdgeGroups).div; predecessorsTable.style["flex-basis"] = "0px"; predecessorsTable.style["flex-grow"] = 1; let help1 = document.createElement("p"); help1.textContent = "Create a snapshot profile by passing --write_v8_snapshot_profile_to=example.json to gen_snapshot." let help2 = document.createElement("p"); help2.textContent = "In Flutter, run flutter build aot --release --extra-gen-snapshot-options=--write-v8-snapshot-profile-to=example.json"; let column = document.createElement("div"); column.style["height"] = "100%"; column.style["display"] = "flex"; column.style["flex-direction"] = "column"; column.appendChild(topBar); column.appendChild(document.createElement("br")); if (graph) { column.appendChild(selectionTable); column.appendChild(document.createElement("br")); column.appendChild(successorsTable); column.appendChild(document.createElement("br")); column.appendChild(predecessorsTable); } else { column.appendChild(help1); column.appendChild(help2); } setBody(column); } function setBody(div) { let body = document.body; while (body.firstChild) { body.removeChild(body.firstChild); } body.appendChild(div); } let graph = null; showTables([]);