blob: a0212f259f35ce6aa7709b5c4052e8c9b7f9a774 [file] [log] [blame]
// 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.
import 'dart:async';
import 'dart:convert';
import 'dart:js_interop';
import 'dart:math' as Math;
import 'package:web/web.dart';
import '../../models.dart' as M;
import 'containers/virtual_tree.dart';
import 'helpers/custom_element.dart';
import 'helpers/element_utils.dart';
import 'helpers/nav_bar.dart';
import 'helpers/nav_menu.dart';
import 'helpers/rendering_scheduler.dart';
import 'nav/notify.dart';
import 'nav/refresh.dart';
import 'nav/top_menu.dart';
import 'nav/vm_menu.dart';
import 'tree_map.dart';
import '../../utils.dart';
enum ProcessSnapshotTreeMode { treeMap, tree, treeMapDiff, treeDiff }
// Note the order of these lists is reflected in the UI, and the first option
// is the default.
const _viewModes = [
ProcessSnapshotTreeMode.treeMap,
ProcessSnapshotTreeMode.tree,
];
const _diffModes = [
ProcessSnapshotTreeMode.treeMapDiff,
ProcessSnapshotTreeMode.treeDiff,
];
class ProcessItemTreeMap extends NormalTreeMap<Map> {
ProcessSnapshotElement element;
ProcessItemTreeMap(this.element);
int getSize(Map node) => node["size"];
String getType(Map node) => node["name"];
String getName(Map node) => node["name"];
String getTooltip(Map node) => getLabel(node) + "\n" + node["description"];
Map getParent(Map node) => node["parent"];
Iterable<Map> getChildren(Map node) => new List<Map>.from(node["children"]);
void onSelect(Map node) {
element.selection = node;
element._r.dirty();
}
void onDetails(Map node) {}
}
class ProcessItemDiff {
Map? _a;
Map? _b;
ProcessItemDiff? parent;
List<ProcessItemDiff>? children;
int retainedGain = -1;
int retainedLoss = -1;
int retainedCommon = -1;
int get retainedSizeA => _a == null ? 0 : _a!["size"];
int get retainedSizeB => _b == null ? 0 : _b!["size"];
int get retainedSizeDiff => retainedSizeB - retainedSizeA;
int get shallowSizeA {
if (_a == null) return 0;
var s = _a!["size"];
for (var c in _a!["children"]) {
s -= c["size"];
}
return s;
}
int get shallowSizeB {
if (_b == null) return 0;
var s = _b!["size"];
for (var c in _b!["children"]) {
s -= c["size"];
}
return s;
}
int get shallowSizeDiff => shallowSizeB - shallowSizeA;
String get name => _a == null ? _b!["name"] : _a!["name"];
static ProcessItemDiff from(Map a, Map b) {
var root = new ProcessItemDiff();
root._a = a;
root._b = b;
// We must use an explicit stack instead of the call stack because the
// dominator tree can be arbitrarily deep. We need to compute the full
// tree to compute areas, so we do this eagerly to avoid having to
// repeatedly test for initialization.
var worklist = <ProcessItemDiff>[];
worklist.add(root);
// Compute children top-down.
for (var i = 0; i < worklist.length; i++) {
worklist[i]._computeChildren(worklist);
}
// Compute area bottom-up.
for (var i = worklist.length - 1; i >= 0; i--) {
worklist[i]._computeArea();
}
return root;
}
void _computeChildren(List<ProcessItemDiff> worklist) {
assert(children == null);
children = <ProcessItemDiff>[];
// Matching children by name.
final childrenB = <String, Map>{};
if (_b != null)
for (var childB in _b!["children"]) {
childrenB[childB["name"]] = childB;
}
if (_a != null)
for (var childA in _a!["children"]) {
var childDiff = new ProcessItemDiff();
childDiff.parent = this;
childDiff._a = childA;
var qualifiedName = childA["name"];
var childB = childrenB[qualifiedName];
if (childB != null) {
childrenB.remove(qualifiedName);
childDiff._b = childB;
}
children!.add(childDiff);
worklist.add(childDiff);
}
for (var childB in childrenB.values) {
var childDiff = new ProcessItemDiff();
childDiff.parent = this;
childDiff._b = childB;
children!.add(childDiff);
worklist.add(childDiff);
}
if (children!.length == 0) {
// Compress.
children = const <ProcessItemDiff>[];
}
}
void _computeArea() {
int g = 0;
int l = 0;
int c = 0;
for (var child in children!) {
g += child.retainedGain;
l += child.retainedLoss;
c += child.retainedCommon;
}
int d = shallowSizeDiff;
if (d > 0) {
g += d;
c += shallowSizeA;
} else {
l -= d;
c += shallowSizeB;
}
assert(retainedSizeA + g - l == retainedSizeB);
retainedGain = g;
retainedLoss = l;
retainedCommon = c;
}
}
class ProcessItemDiffTreeMap extends DiffTreeMap<ProcessItemDiff> {
ProcessSnapshotElement element;
ProcessItemDiffTreeMap(this.element);
int getSizeA(ProcessItemDiff node) => node.retainedSizeA;
int getSizeB(ProcessItemDiff node) => node.retainedSizeB;
int getGain(ProcessItemDiff node) => node.retainedGain;
int getLoss(ProcessItemDiff node) => node.retainedLoss;
int getCommon(ProcessItemDiff node) => node.retainedCommon;
String getType(ProcessItemDiff node) => node.name;
String getName(ProcessItemDiff node) => node.name;
ProcessItemDiff? getParent(ProcessItemDiff node) => node.parent;
Iterable<ProcessItemDiff> getChildren(ProcessItemDiff node) => node.children!;
void onSelect(ProcessItemDiff node) {
element.diffSelection = node;
element._r.dirty();
}
void onDetails(ProcessItemDiff node) {}
}
class ProcessSnapshotElement extends CustomElement implements Renderable {
late RenderingScheduler<ProcessSnapshotElement> _r;
Stream<RenderedEvent<ProcessSnapshotElement>> get onRendered => _r.onRendered;
late M.VM _vm;
late M.EventRepository _events;
late M.NotificationRepository _notifications;
M.NotificationRepository get notifications => _notifications;
M.VMRef get vm => _vm;
List<Map> _loadedSnapshots = <Map>[];
Map? selection;
ProcessItemDiff? diffSelection;
Map? _snapshotA;
Map? _snapshotB;
ProcessSnapshotTreeMode _mode = ProcessSnapshotTreeMode.treeMap;
factory ProcessSnapshotElement(
M.VM vm,
M.EventRepository events,
M.NotificationRepository notifications, {
RenderingQueue? queue,
}) {
ProcessSnapshotElement e = new ProcessSnapshotElement.created();
e._r = new RenderingScheduler<ProcessSnapshotElement>(e, queue: queue);
e._vm = vm;
e._events = events;
e._notifications = notifications;
return e;
}
ProcessSnapshotElement.created() : super.created('process-snapshot');
@override
attached() {
super.attached();
_r.enable();
_refresh();
}
@override
detached() {
super.detached();
_r.disable(notify: true);
removeChildren();
}
void render() {
final content = <HTMLElement>[
navBar(<HTMLElement>[
new NavTopMenuElement(queue: _r.queue).element,
new NavVMMenuElement(_vm, _events, queue: _r.queue).element,
navMenu('process snapshot'),
(new NavRefreshElement(queue: _r.queue)
..onRefresh.listen((e) {
_refresh();
}))
.element,
(new NavRefreshElement(label: 'save', queue: _r.queue)
..disabled = _snapshotA == null
..onRefresh.listen((e) {
_save();
}))
.element,
(new NavRefreshElement(label: 'load', queue: _r.queue)
..onRefresh.listen((e) {
_load();
}))
.element,
new NavNotifyElement(_notifications, queue: _r.queue).element,
]),
];
if (_snapshotA == null) {
// Loading
content.add(new HTMLSpanElement()..textContent = "Loading");
} else {
// Loaded
content.addAll(_createReport());
}
children = content;
}
_refresh() async {
Map snapshot = await (vm as dynamic).invokeRpcNoUpgrade(
"getProcessMemoryUsage",
{},
);
_snapshotLoaded(snapshot);
}
_save() {
var blob = new Blob(
[jsonEncode(_snapshotA).toJS].toJS,
BlobPropertyBag(type: 'application/json'),
);
var blobUrl = URL.createObjectURL(blob);
var link = new HTMLAnchorElement();
link.href = blobUrl;
var now = new DateTime.now();
link.download = 'dart-process-${now.year}-${now.month}-${now.day}.json';
link.click();
}
_load() {
var input = new HTMLInputElement();
input.type = 'file';
input.multiple = false;
input.onChange.listen((event) {
final file = input.files!.item(0);
final reader = FileReader();
reader
..onLoadEnd.first.then((_) async {
_snapshotLoaded(jsonDecode(reader.result as String));
})
..readAsText(file as Blob);
});
input.click();
}
_snapshotLoaded(Map snapshot) {
_loadedSnapshots.add(snapshot);
_snapshotA = snapshot;
_snapshotB = snapshot;
selection = null;
diffSelection = null;
_r.dirty();
}
void _createTreeMap<T>(List<HTMLElement> report, TreeMap<T> treemap, T root) {
final content = new HTMLDivElement();
content.style.border = '1px solid black';
content.style.width = '100%';
content.style.height = '100%';
content.textContent = 'Performing layout...';
Timer.run(() {
// Generate the treemap after the content div has been added to the
// document so that we can ask the browser how much space is
// available for treemap layout.
treemap.showIn(root, content);
});
final text =
'Double-click a tile to zoom in. Double-click the outermost tile to '
'zoom out. Process memory that is not further subdivided is non-Dart '
'memory not known to the VM.';
report.addAll([
new HTMLDivElement()
..className = 'content-centered-big explanation'
..textContent = text,
new HTMLDivElement()
..className = 'content-centered-big'
..style.width = '100%'
..style.height = '100%'
..appendChild(content),
]);
}
List<HTMLElement> _createReport() {
var report = <HTMLElement>[
new HTMLDivElement()
..className = 'content-centered-big'
..appendChildren(<HTMLElement>[
new HTMLDivElement()
..className = 'memberList'
..appendChildren(<HTMLElement>[
new HTMLDivElement()
..className = 'memberItem'
..appendChildren(<HTMLElement>[
new HTMLDivElement()
..className = 'memberName'
..textContent = 'Snapshot A',
new HTMLDivElement()
..className = 'memberName'
..appendChildren(_createSnapshotSelectA()),
]),
new HTMLDivElement()
..className = 'memberItem'
..appendChildren(<HTMLElement>[
new HTMLDivElement()
..className = 'memberName'
..textContent = 'Snapshot B',
new HTMLDivElement()
..className = 'memberName'
..appendChildren(_createSnapshotSelectB()),
]),
new HTMLDivElement()
..className = 'memberItem'
..appendChildren(<HTMLElement>[
new HTMLDivElement()
..className = 'memberName'
..textContent = (_snapshotA == _snapshotB)
? 'View '
: 'Compare ',
new HTMLDivElement()
..className = 'memberName'
..appendChildren(_createModeSelect()),
]),
]),
]),
];
switch (_mode) {
case ProcessSnapshotTreeMode.treeMap:
if (selection == null) {
selection = _snapshotA!["root"];
}
_createTreeMap(report, new ProcessItemTreeMap(this), selection);
break;
case ProcessSnapshotTreeMode.tree:
if (selection == null) {
selection = _snapshotA!["root"];
}
_tree = new VirtualTreeElement(
_createItem,
_updateItem,
_getChildrenItem,
items: [selection],
queue: _r.queue,
);
_tree!.expand(selection!);
report.add(_tree!.element);
break;
case ProcessSnapshotTreeMode.treeMapDiff:
if (diffSelection == null) {
diffSelection = ProcessItemDiff.from(
_snapshotA!["root"],
_snapshotB!["root"],
);
}
_createTreeMap(report, new ProcessItemDiffTreeMap(this), diffSelection);
break;
case ProcessSnapshotTreeMode.treeDiff:
var root = ProcessItemDiff.from(
_snapshotA!["root"],
_snapshotB!["root"],
);
_tree = new VirtualTreeElement(
_createItemDiff,
_updateItemDiff,
_getChildrenItemDiff,
items: [root],
queue: _r.queue,
);
_tree!.expand(root);
report.add(_tree!.element);
break;
}
return report;
}
VirtualTreeElement? _tree;
static HTMLElement _createItem(toggle) {
return new HTMLDivElement()
..className = 'tree-item'
..appendChildren(<HTMLElement>[
new HTMLSpanElement()
..className = 'percentage'
..title = 'percentage of total',
new HTMLSpanElement()
..className = 'size'
..title = 'retained size',
new HTMLSpanElement()..className = 'lines',
new HTMLButtonElement()
..className = 'expander'
..onClick.listen((_) => toggle(autoToggleSingleChildNodes: true)),
new HTMLSpanElement()..className = 'name',
]);
}
static HTMLElement _createItemDiff(toggle) {
return new HTMLDivElement()
..className = 'tree-item'
..appendChildren(<HTMLElement>[
new HTMLSpanElement()
..className = 'percentage'
..title = 'percentage of total',
new HTMLSpanElement()
..className = 'size'
..title = 'retained size A',
new HTMLSpanElement()
..className = 'percentage'
..title = 'percentage of total',
new HTMLSpanElement()
..className = 'size'
..title = 'retained size B',
new HTMLSpanElement()
..className = 'size'
..title = 'retained size change',
new HTMLSpanElement()..className = 'lines',
new HTMLButtonElement()
..className = 'expander'
..onClick.listen((_) => toggle(autoToggleSingleChildNodes: true)),
new HTMLSpanElement()..className = 'name',
]);
}
void _updateItem(HTMLElement element, node, int depth) {
var size = node["size"];
var rootSize = _snapshotA!["root"]["size"];
(element.children.item(0) as HTMLElement).textContent =
Utils.formatPercentNormalized(size * 1.0 / rootSize);
(element.children.item(1) as HTMLElement).textContent = Utils.formatSize(
size,
);
_updateLines(element.children.item(2) as HTMLElement, depth);
if (_getChildrenItem(node).isNotEmpty) {
(element.children.item(3) as HTMLElement).textContent =
_tree!.isExpanded(node) ? '▼' : '►';
} else {
(element.children.item(3) as HTMLElement).textContent = '';
}
(element.children.item(4) as HTMLElement).textContent = node["name"];
(element.children.item(4) as HTMLElement).title = node["description"];
}
void _updateItemDiff(HTMLElement element, nodeDynamic, int depth) {
ProcessItemDiff node = nodeDynamic;
(element.children.item(0) as HTMLElement).textContent =
Utils.formatPercentNormalized(
node.retainedSizeA * 1.0 / _snapshotA!["root"]["size"],
);
(element.children.item(1) as HTMLElement).textContent = Utils.formatSize(
node.retainedSizeA,
);
(element.children.item(2) as HTMLElement).textContent =
Utils.formatPercentNormalized(
node.retainedSizeB * 1.0 / _snapshotB!["root"]["size"],
);
(element.children.item(3) as HTMLElement).textContent = Utils.formatSize(
node.retainedSizeB,
);
(element.children.item(4) as HTMLElement).textContent =
(node.retainedSizeDiff > 0 ? '+' : '') +
Utils.formatSize(node.retainedSizeDiff);
(element.children.item(4) as HTMLElement).style.color =
node.retainedSizeDiff > 0 ? "red" : "green";
_updateLines(element.children.item(5) as HTMLElement, depth);
if (_getChildrenItemDiff(node).isNotEmpty) {
(element.children.item(6) as HTMLElement).textContent =
_tree!.isExpanded(node) ? '▼' : '►';
} else {
(element.children.item(6) as HTMLElement).textContent = '';
}
(element.children.item(7) as HTMLElement)..textContent = node.name;
}
static Iterable _getChildrenItem(node) {
return new List<Map>.from(node["children"]);
}
static Iterable _getChildrenItemDiff(nodeDynamic) {
ProcessItemDiff node = nodeDynamic;
final list = node.children!.toList();
list.sort((a, b) => b.retainedSizeDiff - a.retainedSizeDiff);
return list;
}
static _updateLines(HTMLElement element, int n) {
n = Math.max(0, n);
while (element.children.length > n) {
element.removeChild(element.lastChild!);
}
while (element.children.length < n) {
element.appendChild(HTMLSpanElement());
}
}
static String modeToString(ProcessSnapshotTreeMode mode) {
switch (mode) {
case ProcessSnapshotTreeMode.treeMap:
case ProcessSnapshotTreeMode.treeMapDiff:
return 'Tree Map';
case ProcessSnapshotTreeMode.tree:
case ProcessSnapshotTreeMode.treeDiff:
return 'Tree';
}
}
List<HTMLElement> _createModeSelect() {
var modes = _snapshotA == _snapshotB ? _viewModes : _diffModes;
if (!modes.contains(_mode)) {
_mode = modes[0];
_r.dirty();
}
final s = new HTMLSelectElement()
..className = 'analysis-select'
..value = modeToString(_mode)
..appendChildren(
modes.map(
(mode) => HTMLOptionElement()
..value = modeToString(mode)
..selected = _mode == mode
..textContent = modeToString(mode),
),
);
return [
s
..onChange.listen((_) {
_mode = modes[s.selectedIndex];
_r.dirty();
}),
];
}
String snapshotToString(snapshot) {
if (snapshot == null) return "None";
return snapshot["root"]["name"] +
" " +
Utils.formatSize(snapshot["root"]["size"]);
}
List<HTMLElement> _createSnapshotSelectA() {
final s = HTMLSelectElement()
..className = 'analysis-select'
..value = snapshotToString(_snapshotA)
..appendChildren(
_loadedSnapshots.map(
(snapshot) => HTMLOptionElement()
..value = snapshotToString(snapshot)
..selected = _snapshotA == snapshot
..textContent = snapshotToString(snapshot),
),
);
return [
s
..onChange.listen((_) {
_snapshotA = _loadedSnapshots[s.selectedIndex];
selection = null;
diffSelection = null;
_r.dirty();
}),
];
}
List<HTMLElement> _createSnapshotSelectB() {
final s = HTMLSelectElement()
..className = 'analysis-select'
..value = snapshotToString(_snapshotB)
..appendChildren(
_loadedSnapshots.map(
(snapshot) => HTMLOptionElement()
..value = snapshotToString(snapshot)
..selected = _snapshotB == snapshot
..textContent = snapshotToString(snapshot),
),
);
return [
s
..onChange.listen((_) {
_snapshotB = _loadedSnapshots[s.selectedIndex];
selection = null;
diffSelection = null;
_r.dirty();
}),
];
}
}