| #!/usr/bin/env dart | |
| // Copyright (c) 2025, 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. | |
| import "dart:io"; | |
| class Symbol { | |
| String? name; | |
| String? type; | |
| int? shallowSize; | |
| int? retainedSize; | |
| List<Symbol> children = <Symbol>[]; | |
| Symbol compressTrivialPaths() { | |
| for (var i = 0; i < children.length; i++) { | |
| children[i] = children[i].compressTrivialPaths(); | |
| } | |
| if ((type == "path") && (children.length == 1) && (children[0].type == "path")) { | |
| return children[0]; | |
| } | |
| return this; | |
| } | |
| int computeRetainedSize() { | |
| var s = shallowSize!; | |
| for (var child in children) { | |
| s += child.computeRetainedSize(); | |
| } | |
| retainedSize = s; | |
| return s; | |
| } | |
| writeJson(StringBuffer out) { | |
| out.write("{"); | |
| out.write('"name": "$name",'); | |
| out.write('"shallowSize": $shallowSize,'); | |
| out.write('"retainedSize": $retainedSize,'); | |
| out.write('"type": "$type",'); | |
| out.write('"children": ['); | |
| bool first = true; | |
| for (var child in children) { | |
| if (first) { | |
| first = false; | |
| } else { | |
| out.write(","); | |
| } | |
| child.writeJson(out); | |
| } | |
| out.write("]}"); | |
| } | |
| } | |
| const filteredPathComponents = <String>[ | |
| "", | |
| ]; | |
| var cwd = Directory.current.path; | |
| String prettyPath(String path) { | |
| if (path.startsWith(cwd)) { | |
| path = path.substring(cwd.length); | |
| } | |
| return path | |
| .split("/") | |
| .where((component) => !filteredPathComponents.contains(component)) | |
| .join("/"); | |
| } | |
| main(List<String> args) { | |
| if (args.length != 1) { | |
| print("Usage: zip_size <archive>"); | |
| exit(1); | |
| } | |
| var path = args[0]; | |
| var unzipExec = "unzip"; | |
| var unzipArgs = ["-l", path]; | |
| var unzipResult = Process.runSync(unzipExec, unzipArgs); | |
| if (unzipResult.exitCode != 0) { | |
| print("+ ${unzipExec} ${unzipArgs.join(' ')}"); | |
| print(unzipResult.exitCode); | |
| print(unzipResult.stdout); | |
| print(unzipResult.stderr); | |
| exit(1); | |
| } | |
| var root = new Symbol(); | |
| root.name = path; | |
| root.type = "archive"; | |
| root.shallowSize = 0; | |
| var paths = new Map<String, Symbol>(); | |
| paths[""] = root; | |
| addToPath(Symbol s, String path) { | |
| Symbol? p = paths[path]; | |
| if (p == null) { | |
| p = new Symbol(); | |
| p.name = path; | |
| p.type = "path"; | |
| p.shallowSize = 0; | |
| paths[path] = p; | |
| var i = path.lastIndexOf("/"); | |
| if (i != -1) { | |
| p.name = path.substring(i + 1); | |
| addToPath(p, path.substring(0, i)); | |
| } else { | |
| root.children.add(p); | |
| } | |
| } | |
| p.children.add(s); | |
| } | |
| var lines = unzipResult.stdout.split("\n"); | |
| var regexp = new RegExp(r"\s*(\d+)\s+\d+-\d+-\d+\s+\d+:\d+\s+(.*)"); | |
| for (var line in lines) { | |
| var match = regexp.firstMatch(line); | |
| if (match == null) { | |
| continue; | |
| } | |
| var name = match[2]!; | |
| var size = int.parse(match[1]!); | |
| var path = ""; | |
| if (name.contains("/")) { | |
| path = name.substring(0, name.lastIndexOf("/")); | |
| name = name.substring(name.lastIndexOf("/") + 1); | |
| } | |
| path = prettyPath(path); | |
| var s = new Symbol(); | |
| s.name = name; | |
| s.type = "file"; | |
| s.shallowSize = size; | |
| addToPath(s, path); | |
| } | |
| root.compressTrivialPaths(); | |
| root.computeRetainedSize(); | |
| var json = new StringBuffer(); | |
| root.writeJson(json); | |
| var html = viewer.replaceAll("__DATA__", json.toString()); | |
| new File("${path}.html").writeAsStringSync(html); | |
| // This written as a URL instead of path because some terminals will | |
| // automatically recognize it and make it a link. | |
| var url = Directory.current.uri.resolve("${path}.html"); | |
| print("Wrote $url"); | |
| } | |
| var viewer = """ | |
| <html lang="en"> | |
| <head> | |
| <style> | |
| .treemapTile { | |
| position: absolute; | |
| box-sizing: border-box; | |
| border: solid 1px; | |
| font-size: 13px; | |
| text-align: center; | |
| overflow: hidden; | |
| white-space: nowrap; | |
| cursor: default; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <script> | |
| var root = __DATA__; | |
| 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 + ",90%,80%)"; | |
| } | |
| 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 createTreemapTile(v, width, height, depth) { | |
| let div = document.createElement("div"); | |
| div.className = "treemapTile"; | |
| div.style["background-color"] = color(v.type); | |
| div.ondblclick = function(event) { | |
| event.stopPropagation(); | |
| if (depth == 0) { | |
| let parent = v.parent; | |
| if (parent === undefined) { | |
| // Already at root. | |
| } else { | |
| showDominatorTree(parent); // Zoom out. | |
| } | |
| } else { | |
| showDominatorTree(v); // Zoom in. | |
| } | |
| }; | |
| 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 = | |
| v.name + | |
| " \\ntype: " + v.type + | |
| " \\nretained: " + v.retainedSize + | |
| " \\nshallow: " + v.shallowSize; | |
| if (width < 10 || height < 10) { | |
| // Too small: don't render label or children. | |
| return div; | |
| } | |
| let label = v.name + " [" + prettySize(v.retainedSize) + "]"; | |
| div.appendChild(document.createTextNode(label)); | |
| const kLabelHeight = 13; | |
| 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(); | |
| v.children.forEach(function(c) { | |
| // Size 0 children seem to confuse the layout algorithm (accumulating | |
| // rounding errors?). | |
| if (c.retainedSize > 0) { | |
| children.push(c); | |
| } | |
| }); | |
| children.sort(function (a, b) { | |
| return b.retainedSize - a.retainedSize; | |
| }); | |
| const scale = width * height / v.retainedSize; | |
| // 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 = children[rowStart].retainedSize * 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 = children[rowEnd].retainedSize * 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 = child.retainedSize * 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) { | |
| // Add a filler div to the document first so the browser will calculate | |
| // the available width and height. | |
| let fill = document.createElement("div"); | |
| fill.style.width = "100%"; | |
| fill.style.height = "100%"; | |
| setBody(fill); | |
| let w = document.body.offsetWidth; | |
| let h = document.body.offsetHeight; | |
| let topTile = createTreemapTile(v, w, h, 0); | |
| topTile.style.width = w + "px"; | |
| topTile.style.height = h + "px"; | |
| setBody(topTile); | |
| // Encode the current view into the URL fragment. | |
| if (v == root) { | |
| window.location.hash = ""; | |
| } else { | |
| var fragment = v.name; | |
| v = v.parent; | |
| while (v != root) { | |
| fragment = v.name + "," + fragment; | |
| v = v.parent; | |
| } | |
| window.location.hash = fragment; | |
| } | |
| } | |
| function setBody(div) { | |
| let body = document.body; | |
| while (body.firstChild) { | |
| body.removeChild(body.firstChild); | |
| } | |
| body.appendChild(div); | |
| } | |
| function setParents(v) { | |
| v.children.forEach(function (child) { | |
| child.parent = v; | |
| setParents(child); | |
| }); | |
| } | |
| setParents(root); | |
| let v = root; | |
| let fragments = window.location.hash.substring(1).split(","); | |
| for (let i = 0; i < fragments.length; i++) { | |
| let fragment = fragments[i]; | |
| let foundChild = null; | |
| for (let j = 0; j < v.children.length; j++) { | |
| let child = v.children[j]; | |
| if (child.name == fragment) { | |
| foundChild = child; | |
| break; | |
| } | |
| } | |
| if (foundChild == null) break; // Not found, end search. | |
| v = foundChild; | |
| } | |
| showDominatorTree(v); | |
| </script> | |
| </body> | |
| </html> | |
| """; |