|  | #!/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> | 
|  | """; |