37ed87b038
Port the service tests and Observatory to Dart 3. Change-Id: I8a8b20d8f90acd3b5f741c93f10ba99971aa0c52 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/154825 Commit-Queue: Ryan Macnak <rmacnak@google.com> Reviewed-by: Ben Konyi <bkonyi@google.com>
422 lines
11 KiB
Dart
422 lines
11 KiB
Dart
// Copyright (c) 2020, 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.
|
|
|
|
// A way to test heap snapshot loading and analysis outside of Observatory, and
|
|
// to handle snapshots that require more memory to analyze than is available in
|
|
// a web browser.
|
|
|
|
import 'dart:async';
|
|
import 'dart:convert';
|
|
import 'dart:io';
|
|
import 'dart:typed_data';
|
|
|
|
import 'package:observatory/object_graph.dart';
|
|
|
|
Future<SnapshotGraph> load(String uri) async {
|
|
final ws = await WebSocket.connect(uri,
|
|
compression: CompressionOptions.compressionOff);
|
|
|
|
final getVM = new Completer<String>();
|
|
final reader = new SnapshotReader();
|
|
|
|
reader.onProgress.listen(print);
|
|
|
|
ws.listen((dynamic dynResponse) {
|
|
if (dynResponse is String) {
|
|
final response = json.decode(dynResponse);
|
|
if (response['id'] == 1) {
|
|
getVM.complete(response['result']['isolates'][0]['id']);
|
|
}
|
|
} else if (dynResponse is List<int>) {
|
|
final response = new Uint8List.fromList(dynResponse);
|
|
final dataOffset =
|
|
new ByteData.view(response.buffer).getUint32(0, Endian.little);
|
|
dynamic metadata = new Uint8List.view(response.buffer, 4, dataOffset - 4);
|
|
final data = new Uint8List.view(
|
|
response.buffer, dataOffset, response.length - dataOffset);
|
|
metadata = utf8.decode(metadata);
|
|
metadata = json.decode(metadata);
|
|
var event = metadata['params']['event'];
|
|
if (event['kind'] == 'HeapSnapshot') {
|
|
bool last = event['last'] == true;
|
|
reader.add(data);
|
|
if (last) {
|
|
reader.close();
|
|
ws.close();
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
ws.add(json.encode({
|
|
'jsonrpc': '2.0',
|
|
'method': 'getVM',
|
|
'params': {},
|
|
'id': 1,
|
|
}));
|
|
|
|
final String isolateId = await getVM.future;
|
|
|
|
ws.add(json.encode({
|
|
'jsonrpc': '2.0',
|
|
'method': 'streamListen',
|
|
'params': {'streamId': 'HeapSnapshot'},
|
|
'id': 2,
|
|
}));
|
|
ws.add(json.encode({
|
|
'jsonrpc': '2.0',
|
|
'method': 'requestHeapSnapshot',
|
|
'params': {'isolateId': isolateId},
|
|
'id': 3,
|
|
}));
|
|
|
|
return reader.done;
|
|
}
|
|
|
|
String makeData(dynamic root) {
|
|
// 'root' can be arbitrarily deep, so we can't directly represent it as a
|
|
// JSON tree, which cause a stack overflow here encoding it and in the JS
|
|
// engine decoding it. Instead we flatten the tree into a list of tuples with
|
|
// a parent pointer and re-inflate it in JS.
|
|
final indices = <dynamic, int>{};
|
|
final preorder = <dynamic>[];
|
|
preorder.add(root);
|
|
|
|
for (var index = 0; index < preorder.length; index++) {
|
|
final object = preorder[index];
|
|
preorder.addAll(object.children);
|
|
}
|
|
|
|
final flattened = <dynamic>[];
|
|
for (var index = 0; index < preorder.length; index++) {
|
|
final object = preorder[index];
|
|
indices[object] = index;
|
|
|
|
flattened.add(object.description);
|
|
flattened.add(object.klass.name);
|
|
flattened.add(object.retainedSize);
|
|
if (index == 0) {
|
|
flattened.add(null);
|
|
} else {
|
|
flattened.add(indices[object.parent] as int);
|
|
}
|
|
}
|
|
|
|
return json.encode(flattened);
|
|
}
|
|
|
|
var css = '''
|
|
.treemapTile {
|
|
position: absolute;
|
|
box-sizing: border-box;
|
|
border: solid 1px;
|
|
font-size: 10;
|
|
text-align: center;
|
|
overflow: hidden;
|
|
white-space: nowrap;
|
|
cursor: default;
|
|
}
|
|
''';
|
|
|
|
var js = '''
|
|
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(v.type);
|
|
div.ondblclick = function(event) {
|
|
event.stopPropagation();
|
|
if (depth == 0) {
|
|
let dom = v.parent;
|
|
if (dom == undefined) {
|
|
// Already at root.
|
|
} else {
|
|
showDominatorTree(dom); // 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;
|
|
|
|
let label = v.name + " [" + prettySize(v.size) + "]";
|
|
div.title = label;
|
|
|
|
if (width < 10 || height < 10) {
|
|
// Too small: don't render label or children.
|
|
return div;
|
|
}
|
|
|
|
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.size > 0) {
|
|
children.push(c);
|
|
}
|
|
});
|
|
children.sort(function (a, b) {
|
|
return b.size - a.size;
|
|
});
|
|
|
|
const scale = width * height / v.size;
|
|
|
|
// 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].size * 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].size * 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.size * 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 setBody(div) {
|
|
let body = document.body;
|
|
while (body.firstChild) {
|
|
body.removeChild(body.firstChild);
|
|
}
|
|
body.appendChild(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.";
|
|
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 inflateData(flattened) {
|
|
// 'root' can be arbitrarily deep, so we need to use an explicit stack
|
|
// instead of the call stack.
|
|
let nodes = new Array();
|
|
let i = 0;
|
|
while (i < flattened.length) {
|
|
let node = {
|
|
"name": flattened[i++],
|
|
"type": flattened[i++],
|
|
"size": flattened[i++],
|
|
"children": [],
|
|
"parent": null
|
|
};
|
|
nodes.push(node);
|
|
|
|
let parentIndex = flattened[i++];
|
|
if (parentIndex != null) {
|
|
let parent = nodes[parentIndex];
|
|
parent.children.push(node);
|
|
node.parent = parent;
|
|
}
|
|
}
|
|
|
|
return nodes[0];
|
|
}
|
|
|
|
var root = __DATA__;
|
|
root = inflateData(root);
|
|
|
|
showDominatorTree(root);
|
|
''';
|
|
|
|
var html = '''
|
|
<html>
|
|
<head>
|
|
<title>Dart Heap Snapshot</title>
|
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
|
<style>$css</style>
|
|
</head>
|
|
<body>
|
|
<script>$js</script>
|
|
</body>
|
|
</html>
|
|
''';
|
|
|
|
main(List<String> args) async {
|
|
if (args.length < 1) {
|
|
print('Usage: heap_snapshot.dart <vm-service-uri>');
|
|
exitCode = 1;
|
|
return;
|
|
}
|
|
|
|
var uri = Uri.parse(args[0]);
|
|
if (uri.scheme == 'http') {
|
|
uri = uri.replace(scheme: 'ws');
|
|
} else if (uri.scheme == 'https') {
|
|
uri = uri.replace(scheme: 'wss');
|
|
}
|
|
if (!uri.path.endsWith('/ws')) {
|
|
uri = uri.resolve('ws');
|
|
}
|
|
|
|
final snapshot = await load(uri.toString());
|
|
|
|
final dir = await Directory.systemTemp.createTemp('heap-snapshot');
|
|
final path = dir.path + '/merged-dominator.html';
|
|
final file = await File(path).create();
|
|
final tree = makeData(snapshot.mergedRoot);
|
|
await file.writeAsString(html.replaceAll('__DATA__', tree));
|
|
print('Wrote file://' + path);
|
|
}
|