blob: 5621bf5aa8a767ad9228023db340b80258f7764a [file] [log] [blame]
#!/usr/bin/env dart
// Copyright (c) 2022, 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)) {
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>[
"",
".",
"..",
"out",
"src",
"source",
"third_party",
"DebugX64",
"ReleaseX64",
"ProductX64",
];
String prettyPath(String path) {
return path
.split("/")
.where((component) => !filteredPathComponents.contains(component))
.join("/");
}
main(List<String> args) {
if (args.isEmpty) {
print("Usage: binary_size <binaries>");
exit(1);
}
for (var arg in args) {
analyze(arg);
}
}
analyze(String binaryPath) {
var nmExec = "nm";
var nmArgs = ["--demangle", "--line-numbers", "--print-size", binaryPath];
var nmResult = Process.runSync(nmExec, nmArgs);
if (nmResult.exitCode != 0) {
print("+ ${nmExec} ${nmArgs.join(' ')}");
print(nmResult.exitCode);
print(nmResult.stdout);
print(nmResult.stderr);
exit(1);
}
var root = new Symbol();
root.name = binaryPath;
root.type = "path";
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 = nmResult.stdout.split("\n");
var regex = new RegExp("([0-9a-f]+) ([0-9a-f]+) ([a-zA-z]) (.*)");
for (var line in lines) {
var match = regex.firstMatch(line);
if (match == null) {
continue;
}
var address = int.parse(match[1]!, radix: 16);
var size = int.parse(match[2]!, radix: 16);
var type = match[3];
if (type == "b" || type == "B") {
// Uninitialized data does not contribute to file size.
continue;
}
var nameAndPath = match[4]!.split("\t");
var name = nameAndPath[0].trim();
var path = nameAndPath.length == 1
? ""
: prettyPath(nameAndPath[1].trim().split(":")[0]);
var s = new Symbol();
s.name = name;
s.type = type;
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("${binaryPath}.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("${binaryPath}.html");
print("Wrote $url");
}
var viewer = """
<html>
<head>
<style>
.treemapTile {
position: absolute;
box-sizing: border-box;
border: solid 1px;
font-size: 10;
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 + ",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 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 = 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();
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 the content div to the document first so the browser will calculate
// the available width and height.
let w = document.body.offsetWidth;
let h = document.body.offsetHeight;
let topTile = createTreemapTile(v, w, h, 0);
topTile.style.width = w;
topTile.style.height = h;
setBody(topTile);
}
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);
showDominatorTree(root);
</script>
</body>
</html>
""";