blob: 7b72a62f9757ef25793ad8cf1ff529655dd0a0ad [file] [log] [blame]
// Copyright (c) 2015, 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.
// Copyright (c) 2013, 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:js_interop';
import 'dart:math' as Math;
import 'dart:typed_data';
import 'package:web/web.dart';
import '../../models.dart' as M;
import '../../object_graph.dart';
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/isolate_menu.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 HeapSnapshotTreeMode {
classesTable,
classesTableDiff,
classesTreeMap,
classesTreeMapDiff,
dominatorTree,
dominatorTreeMap,
mergedDominatorTree,
mergedDominatorTreeDiff,
mergedDominatorTreeMap,
mergedDominatorTreeMapDiff,
ownershipTable,
ownershipTableDiff,
ownershipTreeMap,
ownershipTreeMapDiff,
predecessors,
successors,
}
// Note the order of these lists is reflected in the UI, and the first option
// is the default.
const viewModes = [
HeapSnapshotTreeMode.mergedDominatorTreeMap,
HeapSnapshotTreeMode.mergedDominatorTree,
HeapSnapshotTreeMode.dominatorTreeMap,
HeapSnapshotTreeMode.dominatorTree,
HeapSnapshotTreeMode.ownershipTreeMap,
HeapSnapshotTreeMode.ownershipTable,
HeapSnapshotTreeMode.classesTreeMap,
HeapSnapshotTreeMode.classesTable,
HeapSnapshotTreeMode.successors,
HeapSnapshotTreeMode.predecessors,
];
const diffModes = [
HeapSnapshotTreeMode.mergedDominatorTreeMapDiff,
HeapSnapshotTreeMode.mergedDominatorTreeDiff,
HeapSnapshotTreeMode.ownershipTreeMapDiff,
HeapSnapshotTreeMode.ownershipTableDiff,
HeapSnapshotTreeMode.classesTreeMapDiff,
HeapSnapshotTreeMode.classesTableDiff,
];
class DominatorTreeMap extends NormalTreeMap<SnapshotObject> {
HeapSnapshotElement element;
DominatorTreeMap(this.element);
int getSize(SnapshotObject node) => node.retainedSize;
String getType(SnapshotObject node) => node.klass.name;
String getName(SnapshotObject node) => node.description;
SnapshotObject getParent(SnapshotObject node) => node.parent;
Iterable<SnapshotObject> getChildren(SnapshotObject node) => node.children;
void onSelect(SnapshotObject node) {
element.selection = List.from(node.objects);
element._r.dirty();
}
void onDetails(SnapshotObject node) {
element.selection = List.from(node.objects);
element._mode = HeapSnapshotTreeMode.successors;
element._r.dirty();
}
}
class MergedDominatorTreeMap extends NormalTreeMap<SnapshotMergedDominator> {
HeapSnapshotElement element;
MergedDominatorTreeMap(this.element);
int getSize(SnapshotMergedDominator node) => node.retainedSize;
String getType(SnapshotMergedDominator node) => node.klass.name;
String getName(SnapshotMergedDominator node) => node.description;
SnapshotMergedDominator getParent(SnapshotMergedDominator node) =>
node.parent;
Iterable<SnapshotMergedDominator> getChildren(SnapshotMergedDominator node) =>
node.children;
void onSelect(SnapshotMergedDominator node) {
element.mergedSelection = node;
element._r.dirty();
}
void onDetails(SnapshotMergedDominator node) {
element.selection = List.from(node.objects);
element._mode = HeapSnapshotTreeMode.successors;
element._r.dirty();
}
}
class MergedDominatorDiffTreeMap extends DiffTreeMap<MergedDominatorDiff> {
HeapSnapshotElement element;
MergedDominatorDiffTreeMap(this.element);
int getSizeA(MergedDominatorDiff node) => node.retainedSizeA;
int getSizeB(MergedDominatorDiff node) => node.retainedSizeB;
int getGain(MergedDominatorDiff node) => node.retainedGain;
int getLoss(MergedDominatorDiff node) => node.retainedLoss;
int getCommon(MergedDominatorDiff node) => node.retainedCommon;
String getType(MergedDominatorDiff node) => node.name;
String getName(MergedDominatorDiff node) => "instances of ${node.name}";
MergedDominatorDiff? getParent(MergedDominatorDiff node) => node.parent;
Iterable<MergedDominatorDiff> getChildren(MergedDominatorDiff node) =>
node.children!;
void onSelect(MergedDominatorDiff node) {
element.mergedDiffSelection = node;
element._r.dirty();
}
void onDetails(MergedDominatorDiff node) {
element._snapshotA = element._snapshotB;
element.selection = node.objectsB;
element._mode = HeapSnapshotTreeMode.successors;
element._r.dirty();
}
}
// Using `null` to represent the root.
class ClassesShallowTreeMap extends NormalTreeMap<SnapshotClass> {
HeapSnapshotElement element;
SnapshotGraph snapshot;
ClassesShallowTreeMap(this.element, this.snapshot);
int getSize(SnapshotClass node) => node.shallowSize;
String getType(SnapshotClass node) => node.name;
String getName(SnapshotClass node) =>
"${node.instanceCount} instances of ${node.name}";
SnapshotClass? getParent(SnapshotClass node) => null;
Iterable<SnapshotClass> getChildren(SnapshotClass node) => [];
void onSelect(SnapshotClass node) {}
void onDetails(SnapshotClass node) {
element.selection = List.from(node.instances);
element._mode = HeapSnapshotTreeMode.successors;
element._r.dirty();
}
}
// Using `null` to represent the root.
class ClassesShallowDiffTreeMap extends DiffTreeMap<SnapshotClassDiff> {
HeapSnapshotElement element;
List<SnapshotClassDiff> classes;
ClassesShallowDiffTreeMap(this.element, this.classes);
int getSizeA(SnapshotClassDiff node) {
return node.shallowSizeA;
}
int getSizeB(SnapshotClassDiff node) {
return node.shallowSizeB;
}
int getGain(SnapshotClassDiff node) {
return node.shallowSizeGain;
}
int getLoss(SnapshotClassDiff node) {
return node.shallowSizeLoss;
}
int getCommon(SnapshotClassDiff node) {
return node.shallowSizeCommon;
}
String getType(SnapshotClassDiff node) => node.name;
String getName(SnapshotClassDiff node) => "instances of ${node.name}";
SnapshotClassDiff? getParent(SnapshotClassDiff node) => null;
Iterable<SnapshotClassDiff> getChildren(SnapshotClassDiff node) => [];
void onSelect(SnapshotClassDiff node) {}
void onDetails(SnapshotClassDiff node) {
element._snapshotA = element._snapshotB;
element.selection = node.objectsB;
element._mode = HeapSnapshotTreeMode.successors;
element._r.dirty();
}
}
// Using `null` to represent the root.
class ClassesOwnershipTreeMap extends NormalTreeMap<SnapshotClass> {
HeapSnapshotElement element;
SnapshotGraph snapshot;
ClassesOwnershipTreeMap(this.element, this.snapshot);
int getSize(SnapshotClass node) => node.ownedSize;
String getType(SnapshotClass node) => node.name;
String getName(SnapshotClass node) =>
"${node.instanceCount} instances of ${node.name}";
SnapshotClass? getParent(SnapshotClass node) => null;
Iterable<SnapshotClass> getChildren(SnapshotClass node) => [];
void onSelect(SnapshotClass node) {}
void onDetails(SnapshotClass node) {
element.selection = List.from(node.instances);
element._mode = HeapSnapshotTreeMode.successors;
element._r.dirty();
}
}
// Using `null` to represent the root.
class ClassesOwnershipDiffTreeMap extends DiffTreeMap<SnapshotClassDiff> {
HeapSnapshotElement element;
List<SnapshotClassDiff> classes;
ClassesOwnershipDiffTreeMap(this.element, this.classes);
int getSizeA(SnapshotClassDiff node) {
return node.ownedSizeA;
}
int getSizeB(SnapshotClassDiff node) {
return node.ownedSizeB;
}
int getGain(SnapshotClassDiff node) {
return node.ownedSizeGain;
}
int getLoss(SnapshotClassDiff node) {
return node.ownedSizeLoss;
}
int getCommon(SnapshotClassDiff node) {
return node.ownedSizeCommon;
}
String getType(SnapshotClassDiff node) => node.name;
String getName(SnapshotClassDiff node) => "instances of ${node.name}";
SnapshotClassDiff? getParent(SnapshotClassDiff node) => null;
Iterable<SnapshotClassDiff> getChildren(SnapshotClassDiff node) => [];
void onSelect(SnapshotClassDiff node) {}
void onDetails(SnapshotClassDiff node) {
element._snapshotA = element._snapshotB;
element.selection = node.objectsB;
element._mode = HeapSnapshotTreeMode.successors;
element._r.dirty();
}
}
class SnapshotClassDiff {
SnapshotClass? _a;
SnapshotClass? _b;
int get shallowSizeA => _a == null ? 0 : _a!.shallowSize;
int get ownedSizeA => _a == null ? 0 : _a!.ownedSize;
int get instanceCountA => _a == null ? 0 : _a!.instanceCount;
int get shallowSizeB => _b == null ? 0 : _b!.shallowSize;
int get ownedSizeB => _b == null ? 0 : _b!.ownedSize;
int get instanceCountB => _b == null ? 0 : _b!.instanceCount;
int get shallowSizeDiff => shallowSizeB - shallowSizeA;
int get ownedSizeDiff => ownedSizeB - ownedSizeA;
int get instanceCountDiff => instanceCountB - instanceCountA;
int get shallowSizeGain =>
shallowSizeB > shallowSizeA ? shallowSizeB - shallowSizeA : 0;
int get ownedSizeGain =>
ownedSizeB > ownedSizeA ? ownedSizeB - ownedSizeA : 0;
int get shallowSizeLoss =>
shallowSizeA > shallowSizeB ? shallowSizeA - shallowSizeB : 0;
int get ownedSizeLoss =>
ownedSizeA > ownedSizeB ? ownedSizeA - ownedSizeB : 0;
int get shallowSizeCommon =>
shallowSizeB > shallowSizeA ? shallowSizeA : shallowSizeB;
int get ownedSizeCommon => ownedSizeB > ownedSizeA ? ownedSizeA : ownedSizeB;
String get name => _a == null ? _b!.name : _a!.name;
List<SnapshotObject> get objectsA =>
_a == null ? const <SnapshotObject>[] : _a!.instances.toList();
List<SnapshotObject> get objectsB =>
_b == null ? const <SnapshotObject>[] : _b!.instances.toList();
static List<SnapshotClassDiff> from(
SnapshotGraph graphA,
SnapshotGraph graphB,
) {
// Matching classes by SnapshotClass.qualifiedName.
var classesB = new Map<String, SnapshotClass>();
var classesDiff = <SnapshotClassDiff>[];
for (var classB in graphB.classes) {
classesB[classB.qualifiedName] = classB;
}
for (var classA in graphA.classes) {
var classDiff = new SnapshotClassDiff();
var qualifiedName = classA.qualifiedName;
classDiff._a = classA;
var classB = classesB[qualifiedName];
if (classB != null) {
classesB.remove(qualifiedName);
classDiff._b = classB;
}
classesDiff.add(classDiff);
}
for (var classB in classesB.values) {
var classDiff = new SnapshotClassDiff();
classDiff._b = classB;
classesDiff.add(classDiff);
}
return classesDiff;
}
}
class MergedDominatorDiff {
SnapshotMergedDominator? _a;
SnapshotMergedDominator? _b;
MergedDominatorDiff? parent;
List<MergedDominatorDiff>? children;
int retainedGain = -1;
int retainedLoss = -1;
int retainedCommon = -1;
int get shallowSizeA => _a == null ? 0 : _a!.shallowSize;
int get retainedSizeA => _a == null ? 0 : _a!.retainedSize;
int get instanceCountA => _a == null ? 0 : _a!.instanceCount;
int get shallowSizeB => _b == null ? 0 : _b!.shallowSize;
int get retainedSizeB => _b == null ? 0 : _b!.retainedSize;
int get instanceCountB => _b == null ? 0 : _b!.instanceCount;
int get shallowSizeDiff => shallowSizeB - shallowSizeA;
int get retainedSizeDiff => retainedSizeB - retainedSizeA;
int get instanceCountDiff => instanceCountB - instanceCountA;
String get name => _a == null ? _b!.klass.name : _a!.klass.name;
List<SnapshotObject> get objectsA =>
_a == null ? const <SnapshotObject>[] : _a!.objects.toList();
List<SnapshotObject> get objectsB =>
_b == null ? const <SnapshotObject>[] : _b!.objects.toList();
static MergedDominatorDiff from(
SnapshotMergedDominator a,
SnapshotMergedDominator b,
) {
var root = new MergedDominatorDiff();
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 = <MergedDominatorDiff>[];
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<MergedDominatorDiff> worklist) {
assert(children == null);
children = <MergedDominatorDiff>[];
// Matching children by MergedObjectVertex.klass.qualifiedName.
final childrenB = <String, SnapshotMergedDominator>{};
if (_b != null)
for (var childB in _b!.children) {
childrenB[childB.klass.qualifiedName] = childB;
}
if (_a != null)
for (var childA in _a!.children) {
var childDiff = new MergedDominatorDiff();
childDiff.parent = this;
childDiff._a = childA;
var qualifiedName = childA.klass.qualifiedName;
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 MergedDominatorDiff();
childDiff.parent = this;
childDiff._b = childB;
children!.add(childDiff);
worklist.add(childDiff);
}
if (children!.length == 0) {
// Compress.
children = const <MergedDominatorDiff>[];
}
}
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 HeapSnapshotElement extends CustomElement implements Renderable {
late RenderingScheduler<HeapSnapshotElement> _r;
Stream<RenderedEvent<HeapSnapshotElement>> get onRendered => _r.onRendered;
late M.VM _vm;
late M.IsolateRef _isolate;
late M.EventRepository _events;
late M.NotificationRepository _notifications;
late M.HeapSnapshotRepository _snapshots;
SnapshotReader? _reader;
String? _status;
List<SnapshotGraph> _loadedSnapshots = <SnapshotGraph>[];
SnapshotGraph? _snapshotA;
SnapshotGraph? _snapshotB;
HeapSnapshotTreeMode _mode = HeapSnapshotTreeMode.mergedDominatorTreeMap;
M.IsolateRef get isolate => _isolate;
M.NotificationRepository get notifications => _notifications;
M.HeapSnapshotRepository get profiles => _snapshots;
M.VMRef get vm => _vm;
List<SnapshotObject>? selection;
SnapshotMergedDominator? mergedSelection;
MergedDominatorDiff? mergedDiffSelection;
factory HeapSnapshotElement(
M.VM vm,
M.IsolateRef isolate,
M.EventRepository events,
M.NotificationRepository notifications,
M.HeapSnapshotRepository snapshots,
M.ObjectRepository objects, {
RenderingQueue? queue,
}) {
HeapSnapshotElement e = new HeapSnapshotElement.created();
e._r = new RenderingScheduler<HeapSnapshotElement>(e, queue: queue);
e._vm = vm;
e._isolate = isolate;
e._events = events;
e._notifications = notifications;
e._snapshots = snapshots;
return e;
}
HeapSnapshotElement.created() : super.created('heap-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,
new NavIsolateMenuElement(_isolate, _events, queue: _r.queue).element,
navMenu('heap snapshot'),
(new NavRefreshElement(queue: _r.queue)
..disabled = _reader != null
..onRefresh.listen((e) {
_refresh();
}))
.element,
(new NavRefreshElement(label: 'save', queue: _r.queue)
..disabled = _reader != null
..onRefresh.listen((e) {
_save();
}))
.element,
(new NavRefreshElement(label: 'load', queue: _r.queue)
..disabled = _reader != null
..onRefresh.listen((e) {
_load();
}))
.element,
new NavNotifyElement(_notifications, queue: _r.queue).element,
]),
];
if (_reader != null) {
// Loading
content.addAll(
_createStatusMessage(
'Loading snapshot...',
description: _status!,
progress: 1,
),
);
} else if (_snapshotA != null) {
// Loaded
content.addAll(_createReport());
}
setChildren(content);
}
_refresh() {
_reader = null;
_snapshotLoading(_snapshots.get(isolate));
}
_save() {
final blob = Blob(
_snapshotA!.chunks as JSArray<JSAny>,
BlobPropertyBag(type: 'application/octet-stream'),
);
var blobUrl = URL.createObjectURL(blob);
var link = new HTMLAnchorElement();
link.href = blobUrl;
var now = new DateTime.now();
link.download = 'dart-heap-${now.year}-${now.month}-${now.day}.bin';
link.click();
}
_load() {
var input = new HTMLInputElement();
input.type = 'file';
input.multiple = false;
input.onChange.listen((_) {
File file = input.files!.item(0) as File;
final reader = FileReader();
reader
..onLoadEnd.first.then((_) async {
final snapshotReader = new SnapshotReader();
_snapshotLoading(snapshotReader);
snapshotReader.add((reader.result as ByteBuffer).asUint8List());
snapshotReader.close();
})
..readAsArrayBuffer(file);
});
input.click();
}
_snapshotLoading(SnapshotReader reader) async {
_status = '';
_reader = reader;
reader.onProgress.listen((String status) {
_status = status;
_r.dirty();
});
_snapshotLoaded(await reader.done);
}
_snapshotLoaded(SnapshotGraph snapshot) {
_reader = null;
_loadedSnapshots.add(snapshot);
_snapshotA = snapshot;
_snapshotB = snapshot;
selection = null;
mergedSelection = null;
mergedDiffSelection = null;
_r.dirty();
}
static List<HTMLElement> _createStatusMessage(
String message, {
String description = '',
double progress = 0.0,
}) {
return [
new HTMLDivElement()
..className = 'content-centered-big'
..appendChildren(<HTMLElement>[
new HTMLDivElement()
..className = 'statusBox shadow center'
..appendChildren(<HTMLElement>[
new HTMLDivElement()
..className = 'statusMessage'
..textContent = message,
new HTMLDivElement()
..className = 'statusDescription'
..textContent = description,
new HTMLDivElement()
..style.background = '#0489c3'
..style.width = '$progress%'
..style.height = '15px'
..style.borderRadius = '4px',
]),
]),
];
}
VirtualTreeElement? _tree;
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. Right-click a tile to inspect its objects.';
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 HeapSnapshotTreeMode.dominatorTree:
if (selection == null) {
selection = List.from(_snapshotA!.extendedRoot.objects);
}
_tree = new VirtualTreeElement(
_createDominator,
_updateDominator,
_getChildrenDominator,
items: selection!,
queue: _r.queue,
);
if (selection!.length == 1) {
_tree!.expand(selection!.first);
}
final text =
'In a heap dominator tree, an object X is a parent of '
'object Y if every path from the root to Y goes through '
'X. This allows you to find "choke points" that are '
'holding onto a lot of memory. If an object becomes '
'garbage, all its children in the dominator tree become '
'garbage as well.';
report.addAll([
new HTMLDivElement()
..className = 'content-centered-big explanation'
..textContent = text,
_tree!.element,
]);
break;
case HeapSnapshotTreeMode.dominatorTreeMap:
if (selection == null) {
selection = List.from(_snapshotA!.extendedRoot.objects);
}
_createTreeMap(report, new DominatorTreeMap(this), selection!.first);
break;
case HeapSnapshotTreeMode.mergedDominatorTree:
_tree = new VirtualTreeElement(
_createMergedDominator,
_updateMergedDominator,
_getChildrenMergedDominator,
items: _getChildrenMergedDominator(_snapshotA!.extendedMergedRoot),
queue: _r.queue,
);
_tree!.expand(_snapshotA!.extendedMergedRoot);
final text =
'A heap dominator tree, where siblings with the same class'
' have been merged into a single node.';
report.addAll([
new HTMLDivElement()
..className = 'content-centered-big explanation'
..textContent = text,
_tree!.element,
]);
break;
case HeapSnapshotTreeMode.mergedDominatorTreeDiff:
var root = MergedDominatorDiff.from(
_snapshotA!.extendedMergedRoot,
_snapshotB!.extendedMergedRoot,
);
_tree = new VirtualTreeElement(
_createMergedDominatorDiff,
_updateMergedDominatorDiff,
_getChildrenMergedDominatorDiff,
items: _getChildrenMergedDominatorDiff(root),
queue: _r.queue,
);
_tree!.expand(root);
final text =
'A heap dominator tree, where siblings with the same class'
' have been merged into a single node.';
report.addAll([
new HTMLDivElement()
..className = 'content-centered-big explanation'
..textContent = text,
_tree!.element,
]);
break;
case HeapSnapshotTreeMode.mergedDominatorTreeMap:
if (mergedSelection == null) {
mergedSelection = _snapshotA!.extendedMergedRoot;
}
_createTreeMap(
report,
new MergedDominatorTreeMap(this),
mergedSelection,
);
break;
case HeapSnapshotTreeMode.mergedDominatorTreeMapDiff:
if (mergedDiffSelection == null) {
mergedDiffSelection = MergedDominatorDiff.from(
_snapshotA!.extendedMergedRoot,
_snapshotB!.extendedMergedRoot,
);
}
_createTreeMap(
report,
new MergedDominatorDiffTreeMap(this),
mergedDiffSelection,
);
break;
case HeapSnapshotTreeMode.ownershipTable:
final items = _snapshotA!.classes
.where((c) => c.ownedSize > 0)
.toList();
items.sort((a, b) => b.ownedSize - a.ownedSize);
_tree = new VirtualTreeElement(
_createOwnership,
_updateOwnership,
_getChildrenOwnership,
items: items,
queue: _r.queue,
);
_tree!.expand(_snapshotA!.root);
final text =
'An object X is said to "own" object Y if X is the only '
'object that references Y, or X owns the only object that '
'references Y. In particular, objects "own" the space of any '
'unshared lists or maps they reference.';
report.addAll([
new HTMLDivElement()
..className = 'content-centered-big explanation'
..textContent = text,
_tree!.element,
]);
break;
case HeapSnapshotTreeMode.ownershipTableDiff:
final items = SnapshotClassDiff.from(_snapshotA!, _snapshotB!);
items.sort((a, b) => b.ownedSizeB - a.ownedSizeB);
items.sort((a, b) => b.ownedSizeA - a.ownedSizeA);
items.sort((a, b) => b.ownedSizeDiff - a.ownedSizeDiff);
_tree = new VirtualTreeElement(
_createOwnershipDiff,
_updateOwnershipDiff,
_getChildrenOwnershipDiff,
items: items,
queue: _r.queue,
);
_tree!.expand(_snapshotA!.root);
final text =
'An object X is said to "own" object Y if X is the only '
'object that references Y, or X owns the only object that '
'references Y. In particular, objects "own" the space of any '
'unshared lists or maps they reference.';
report.addAll([
new HTMLDivElement()
..className = 'content-centered-big explanation'
..textContent = text,
_tree!.element,
]);
break;
case HeapSnapshotTreeMode.ownershipTreeMap:
_createTreeMap(
report,
new ClassesOwnershipTreeMap(this, _snapshotA!),
null,
);
break;
case HeapSnapshotTreeMode.ownershipTreeMapDiff:
final items = SnapshotClassDiff.from(_snapshotA!, _snapshotB!);
_createTreeMap(
report,
new ClassesOwnershipDiffTreeMap(this, items),
null,
);
break;
case HeapSnapshotTreeMode.successors:
if (selection == null) {
selection = List.from(_snapshotA!.root.objects);
}
_tree = new VirtualTreeElement(
_createSuccessor,
_updateSuccessor,
_getChildrenSuccessor,
items: selection!,
queue: _r.queue,
);
if (selection!.length == 1) {
_tree!.expand(selection!.first);
}
final text = '';
report.addAll([
new HTMLDivElement()
..className = 'content-centered-big explanation'
..textContent = text,
_tree!.element,
]);
break;
case HeapSnapshotTreeMode.predecessors:
if (selection == null) {
selection = List.from(_snapshotA!.root.objects);
}
_tree = new VirtualTreeElement(
_createPredecessor,
_updatePredecessor,
_getChildrenPredecessor,
items: selection!,
queue: _r.queue,
);
if (selection!.length == 1) {
_tree!.expand(selection!.first);
}
final text = '';
report.addAll([
new HTMLDivElement()
..className = 'content-centered-big explanation'
..textContent = text,
_tree!.element,
]);
break;
case HeapSnapshotTreeMode.classesTable:
final items = _snapshotA!.classes.toList();
items.sort((a, b) => b.shallowSize - a.shallowSize);
_tree = new VirtualTreeElement(
_createClass,
_updateClass,
_getChildrenClass,
items: items,
queue: _r.queue,
);
report.add(_tree!.element);
break;
case HeapSnapshotTreeMode.classesTableDiff:
final items = SnapshotClassDiff.from(_snapshotA!, _snapshotB!);
items.sort((a, b) => b.shallowSizeB - a.shallowSizeB);
items.sort((a, b) => b.shallowSizeA - a.shallowSizeA);
items.sort((a, b) => b.shallowSizeDiff - a.shallowSizeDiff);
_tree = new VirtualTreeElement(
_createClassDiff,
_updateClassDiff,
_getChildrenClassDiff,
items: items,
queue: _r.queue,
);
report.add(_tree!.element);
break;
case HeapSnapshotTreeMode.classesTreeMap:
_createTreeMap(
report,
new ClassesShallowTreeMap(this, _snapshotA!),
null,
);
break;
case HeapSnapshotTreeMode.classesTreeMapDiff:
final items = SnapshotClassDiff.from(_snapshotA!, _snapshotB!);
_createTreeMap(
report,
new ClassesShallowDiffTreeMap(this, items),
null,
);
break;
}
return report;
}
static HTMLElement _createDominator(toggle) {
return new HTMLDivElement()
..className = 'tree-item'
..appendChildren(<HTMLElement>[
new HTMLSpanElement()
..className = 'percentage'
..title = 'percentage of heap being retained',
new HTMLSpanElement()
..className = 'size'
..title = 'retained size',
new HTMLSpanElement()..className = 'lines',
new HTMLButtonElement()
..className = 'expander'
..onClick.listen((_) => toggle(autoToggleSingleChildNodes: true)),
new HTMLSpanElement()..className = 'name',
new HTMLAnchorElement()
..className = 'link'
..textContent = "[inspect]",
new HTMLAnchorElement()
..className = 'link'
..textContent = "[incoming]",
new HTMLAnchorElement()
..className = 'link'
..textContent = "[dominator-map]",
]);
}
static HTMLElement _createSuccessor(toggle) {
return new HTMLDivElement()
..className = 'tree-item'
..appendChildren(<HTMLElement>[
new HTMLSpanElement()..className = 'lines',
new HTMLButtonElement()
..className = 'expander'
..onClick.listen((_) => toggle(autoToggleSingleChildNodes: true)),
new HTMLSpanElement()
..className = 'size'
..title = 'retained size',
new HTMLSpanElement()
..className = 'edge'
..title = 'name of outgoing field',
new HTMLSpanElement()..className = 'name',
new HTMLAnchorElement()
..className = 'link'
..textContent = "[incoming]",
new HTMLAnchorElement()
..className = 'link'
..textContent = "[dominator-tree]",
new HTMLAnchorElement()
..className = 'link'
..textContent = "[dominator-map]",
]);
}
static HTMLElement _createPredecessor(toggle) {
return new HTMLDivElement()
..className = 'tree-item'
..appendChildren(<HTMLElement>[
new HTMLSpanElement()..className = 'lines',
new HTMLButtonElement()
..className = 'expander'
..onClick.listen((_) => toggle(autoToggleSingleChildNodes: true)),
new HTMLSpanElement()
..className = 'size'
..title = 'retained size',
new HTMLSpanElement()
..className = 'edge'
..title = 'name of incoming field',
new HTMLSpanElement()..className = 'name',
new HTMLSpanElement()
..className = 'link'
..textContent = "[inspect]",
new HTMLAnchorElement()
..className = 'link'
..textContent = "[dominator-tree]",
new HTMLAnchorElement()
..className = 'link'
..textContent = "[dominator-map]",
]);
}
static HTMLElement _createMergedDominator(toggle) {
return new HTMLDivElement()
..className = 'tree-item'
..appendChildren(<HTMLElement>[
new HTMLSpanElement()
..className = 'percentage'
..title = 'percentage of heap being retained',
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 _createMergedDominatorDiff(toggle) {
return new HTMLDivElement()
..className = 'tree-item'
..appendChildren(<HTMLElement>[
new HTMLSpanElement()
..className = 'percentage'
..title = 'percentage of heap being retained',
new HTMLSpanElement()
..className = 'size'
..title = 'retained size A',
new HTMLSpanElement()
..className = 'percentage'
..title = 'percentage of heap being retained',
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',
]);
}
static HTMLElement _createOwnership(toggle) {
return new HTMLDivElement()
..className = 'tree-item'
..appendChildren(<HTMLElement>[
new HTMLSpanElement()
..className = 'percentage'
..title = 'percentage of heap owned',
new HTMLSpanElement()
..className = 'size'
..title = 'owned size',
new HTMLSpanElement()..className = 'name',
]);
}
static HTMLElement _createOwnershipDiff(toggle) {
return new HTMLDivElement()
..className = 'tree-item'
..appendChildren(<HTMLElement>[
new HTMLSpanElement()
..className = 'percentage'
..title = 'percentage of heap owned A',
new HTMLSpanElement()
..className = 'size'
..title = 'owned size A',
new HTMLSpanElement()
..className = 'percentage'
..title = 'percentage of heap owned B',
new HTMLSpanElement()
..className = 'size'
..title = 'owned size B',
new HTMLSpanElement()
..className = 'size'
..title = 'owned size change',
new HTMLSpanElement()..className = 'name',
]);
}
static HTMLElement _createClass(toggle) {
return new HTMLDivElement()
..className = 'tree-item'
..appendChildren(<HTMLElement>[
new HTMLSpanElement()
..className = 'percentage'
..title = 'percentage of heap owned',
new HTMLSpanElement()
..className = 'size'
..title = 'shallow size',
new HTMLSpanElement()
..className = 'size'
..title = 'instance count',
new HTMLSpanElement()..className = 'name',
]);
}
static HTMLElement _createClassDiff(toggle) {
return new HTMLDivElement()
..className = 'tree-item'
..appendChildren(<HTMLElement>[
new HTMLSpanElement()
..className = 'size'
..title = 'shallow size A',
new HTMLSpanElement()
..className = 'size'
..title = 'instance count A',
new HTMLSpanElement()
..className = 'size'
..title = 'shallow size B',
new HTMLSpanElement()
..className = 'size'
..title = 'instance count B',
new HTMLSpanElement()
..className = 'size'
..title = 'shallow size diff',
new HTMLSpanElement()
..className = 'size'
..title = 'instance count diff',
new HTMLSpanElement()..className = 'name',
]);
}
static const int kMaxChildren = 100;
static const int kMinRetainedSize = 4096;
static Iterable _getChildrenDominator(nodeDynamic) {
SnapshotObject node = nodeDynamic;
final list = node.children
.where((child) => child.retainedSize >= kMinRetainedSize)
.toList();
list.sort((a, b) => b.retainedSize - a.retainedSize);
return list.take(kMaxChildren).toList();
}
static Iterable _getChildrenSuccessor(nodeDynamic) {
SnapshotObject node = nodeDynamic;
return node.successors.toList();
}
static Iterable _getChildrenPredecessor(nodeDynamic) {
SnapshotObject node = nodeDynamic;
return node.predecessors.take(kMaxChildren).toList();
}
static Iterable _getChildrenMergedDominator(nodeDynamic) {
SnapshotMergedDominator node = nodeDynamic;
final list = node.children
.where((child) => child.retainedSize >= kMinRetainedSize)
.toList();
list.sort((a, b) => b.retainedSize - a.retainedSize);
return list.take(kMaxChildren).toList();
}
static Iterable _getChildrenMergedDominatorDiff(nodeDynamic) {
MergedDominatorDiff node = nodeDynamic;
final list = node.children!
.where(
(child) =>
child.retainedSizeA >= kMinRetainedSize ||
child.retainedSizeB >= kMinRetainedSize,
)
.toList();
list.sort((a, b) => b.retainedSizeDiff - a.retainedSizeDiff);
return list.take(kMaxChildren).toList();
}
static Iterable _getChildrenOwnership(item) {
return const [];
}
static Iterable _getChildrenOwnershipDiff(item) {
return const [];
}
static Iterable _getChildrenClass(item) {
return const [];
}
static Iterable _getChildrenClassDiff(item) {
return const [];
}
void _updateDominator(HTMLElement element, nodeDynamic, int depth) {
SnapshotObject node = nodeDynamic;
(element.children.item(0) as HTMLElement).textContent =
Utils.formatPercentNormalized(
node.retainedSize * 1.0 / _snapshotA!.size,
);
(element.children.item(1) as HTMLElement).textContent = Utils.formatSize(
node.retainedSize,
);
_updateLines(element.children.item(2) as HTMLElement, depth);
if (_getChildrenDominator(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.description;
(element.children.item(5) as HTMLElement).onClick.listen((_) {
selection = List.from(node.objects);
_mode = HeapSnapshotTreeMode.successors;
_r.dirty();
});
(element.children.item(6) as HTMLElement).onClick.listen((_) {
selection = List.from(node.objects);
_mode = HeapSnapshotTreeMode.predecessors;
_r.dirty();
});
(element.children.item(7) as HTMLElement).onClick.listen((_) {
selection = List.from(node.objects);
_mode = HeapSnapshotTreeMode.dominatorTreeMap;
_r.dirty();
});
}
void _updateSuccessor(HTMLElement element, nodeDynamic, int depth) {
SnapshotObject node = nodeDynamic;
_updateLines(element.children.item(0) as HTMLElement, depth);
if (_getChildrenSuccessor(node).isNotEmpty) {
(element.children.item(1) as HTMLElement).textContent =
_tree!.isExpanded(node) ? 'â–¼' : 'â–º';
} else {
(element.children.item(1) as HTMLElement).textContent = '';
}
(element.children.item(2) as HTMLElement).textContent = Utils.formatSize(
node.retainedSize,
);
(element.children.item(3) as HTMLElement).textContent = node.label;
(element.children.item(4) as HTMLElement).textContent = node.description;
(element.children.item(5) as HTMLElement).onClick.listen((_) {
selection = List.from(node.objects);
_mode = HeapSnapshotTreeMode.predecessors;
_r.dirty();
});
(element.children.item(6) as HTMLElement).onClick.listen((_) {
selection = List.from(node.objects);
_mode = HeapSnapshotTreeMode.dominatorTree;
_r.dirty();
});
(element.children.item(7) as HTMLElement).onClick.listen((_) {
selection = List.from(node.objects);
_mode = HeapSnapshotTreeMode.dominatorTreeMap;
_r.dirty();
});
}
void _updatePredecessor(HTMLElement element, nodeDynamic, int depth) {
SnapshotObject node = nodeDynamic;
_updateLines(element.children.item(0) as HTMLElement, depth);
if (_getChildrenSuccessor(node).isNotEmpty) {
(element.children.item(1) as HTMLElement).textContent =
_tree!.isExpanded(node) ? 'â–¼' : 'â–º';
} else {
(element.children.item(1) as HTMLElement).textContent = '';
}
(element.children.item(2) as HTMLElement).textContent = Utils.formatSize(
node.retainedSize,
);
(element.children.item(3) as HTMLElement).textContent = node.label;
(element.children.item(4) as HTMLElement).textContent = node.description;
(element.children.item(5) as HTMLElement).onClick.listen((_) {
selection = List.from(node.objects);
_mode = HeapSnapshotTreeMode.successors;
_r.dirty();
});
(element.children.item(6) as HTMLElement).onClick.listen((_) {
selection = List.from(node.objects);
_mode = HeapSnapshotTreeMode.dominatorTree;
_r.dirty();
});
(element.children.item(7) as HTMLElement).onClick.listen((_) {
selection = List.from(node.objects);
_mode = HeapSnapshotTreeMode.dominatorTreeMap;
_r.dirty();
});
}
void _updateMergedDominator(HTMLElement element, nodeDynamic, int depth) {
SnapshotMergedDominator node = nodeDynamic;
(element.children.item(0) as HTMLElement).textContent =
Utils.formatPercentNormalized(
node.retainedSize * 1.0 / _snapshotA!.size,
);
(element.children.item(1) as HTMLElement).textContent = Utils.formatSize(
node.retainedSize,
);
_updateLines(element.children.item(2) as HTMLElement, depth);
if (_getChildrenMergedDominator(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.instanceCount} instances of ${node.klass.name}';
}
void _updateMergedDominatorDiff(HTMLElement element, nodeDynamic, int depth) {
MergedDominatorDiff node = nodeDynamic;
(element.children.item(0) as HTMLElement).textContent =
Utils.formatPercentNormalized(
node.retainedSizeA * 1.0 / _snapshotA!.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!.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 (_getChildrenMergedDominatorDiff(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.instanceCountA} → ${node.instanceCountB} instances of ${node.name}';
}
void _updateOwnership(HTMLElement element, nodeDynamic, int depth) {
SnapshotClass node = nodeDynamic;
(element.children.item(0) as HTMLElement).textContent =
Utils.formatPercentNormalized(node.ownedSize * 1.0 / _snapshotA!.size);
(element.children.item(1) as HTMLElement).textContent = Utils.formatSize(
node.ownedSize,
);
(element.children.item(2) as HTMLElement).textContent = node.name;
}
void _updateOwnershipDiff(HTMLElement element, nodeDynamic, int depth) {
SnapshotClassDiff node = nodeDynamic;
(element.children.item(0) as HTMLElement).textContent =
Utils.formatPercentNormalized(node.ownedSizeA * 1.0 / _snapshotA!.size);
(element.children.item(1) as HTMLElement).textContent = Utils.formatSize(
node.ownedSizeA,
);
(element.children.item(2) as HTMLElement).textContent =
Utils.formatPercentNormalized(node.ownedSizeB * 1.0 / _snapshotB!.size);
(element.children.item(3) as HTMLElement).textContent = Utils.formatSize(
node.ownedSizeB,
);
(element.children.item(4) as HTMLElement).textContent =
(node.ownedSizeDiff > 0 ? "+" : "") +
Utils.formatSize(node.ownedSizeDiff);
(element.children.item(4) as HTMLElement).style.color =
node.ownedSizeDiff > 0 ? "red" : "green";
(element.children.item(5) as HTMLElement).textContent = node.name;
}
void _updateClass(HTMLElement element, nodeDynamic, int depth) {
SnapshotClass node = nodeDynamic;
(element.children.item(0) as HTMLElement).textContent =
Utils.formatPercentNormalized(
node.shallowSize * 1.0 / _snapshotA!.size,
);
(element.children.item(1) as HTMLElement).textContent = Utils.formatSize(
node.shallowSize,
);
(element.children.item(2) as HTMLElement).textContent = node.instanceCount
.toString();
(element.children.item(3) as HTMLElement).textContent = node.name;
}
void _updateClassDiff(HTMLElement element, nodeDynamic, int depth) {
SnapshotClassDiff node = nodeDynamic;
(element.children.item(0) as HTMLElement).textContent = Utils.formatSize(
node.shallowSizeA,
);
(element.children.item(1) as HTMLElement).textContent = node.instanceCountA
.toString();
(element.children.item(2) as HTMLElement).textContent = Utils.formatSize(
node.shallowSizeB,
);
(element.children.item(3) as HTMLElement).textContent = node.instanceCountB
.toString();
(element.children.item(4) as HTMLElement).textContent =
(node.shallowSizeDiff > 0 ? "+" : "") +
Utils.formatSize(node.shallowSizeDiff);
(element.children.item(4) as HTMLElement).style.color =
node.shallowSizeDiff > 0 ? "red" : "green";
(element.children.item(5) as HTMLElement).textContent =
(node.instanceCountDiff > 0 ? "+" : "") +
node.instanceCountDiff.toString();
(element.children.item(5) as HTMLElement).style.color =
node.instanceCountDiff > 0 ? "red" : "green";
(element.children.item(6) as HTMLElement).textContent = node.name;
}
static _updateLines(HTMLElement element, int n) {
n = Math.max(0, n);
while (element.childNodes.length > n) {
element.removeChild(
element.childNodes.item(element.childNodes.length - 1)!,
);
}
while (element.childNodes.length < n) {
element.appendChild(new HTMLSpanElement());
}
}
static String modeToString(HeapSnapshotTreeMode mode) {
switch (mode) {
case HeapSnapshotTreeMode.dominatorTree:
return 'Dominators (tree)';
case HeapSnapshotTreeMode.dominatorTreeMap:
return 'Dominators (treemap)';
case HeapSnapshotTreeMode.mergedDominatorTree:
case HeapSnapshotTreeMode.mergedDominatorTreeDiff:
return 'Dominators (tree, siblings merged by class)';
case HeapSnapshotTreeMode.mergedDominatorTreeMap:
case HeapSnapshotTreeMode.mergedDominatorTreeMapDiff:
return 'Dominators (treemap, siblings merged by class)';
case HeapSnapshotTreeMode.ownershipTable:
case HeapSnapshotTreeMode.ownershipTableDiff:
return 'Ownership (table)';
case HeapSnapshotTreeMode.ownershipTreeMap:
case HeapSnapshotTreeMode.ownershipTreeMapDiff:
return 'Ownership (treemap)';
case HeapSnapshotTreeMode.classesTable:
case HeapSnapshotTreeMode.classesTableDiff:
return 'Classes (table)';
case HeapSnapshotTreeMode.classesTreeMap:
case HeapSnapshotTreeMode.classesTreeMapDiff:
return 'Classes (treemap)';
case HeapSnapshotTreeMode.successors:
return 'Successors / outgoing references';
case HeapSnapshotTreeMode.predecessors:
return 'Predecessors / incoming references';
}
}
List<HTMLElement> _createModeSelect() {
var modes = _snapshotA == _snapshotB ? viewModes : diffModes;
if (!modes.contains(_mode)) {
_mode = modes[0];
_r.dirty();
}
final s = 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.description +
" " +
Utils.formatSize(snapshot.capacity + snapshot.externalSize);
}
List<HTMLElement> _createSnapshotSelectA() {
final s = new HTMLSelectElement()
..className = 'analysis-select'
..value = snapshotToString(_snapshotA)
..appendChildren(
_loadedSnapshots.map((snapshot) {
return HTMLOptionElement()
..value = snapshotToString(snapshot)
..selected = _snapshotA == snapshot
..textContent = snapshotToString(snapshot);
}),
);
return [
s
..onChange.listen((_) {
_snapshotA = _loadedSnapshots[s.selectedIndex];
selection = null;
mergedSelection = null;
mergedDiffSelection = null;
_r.dirty();
}),
];
}
List<HTMLElement> _createSnapshotSelectB() {
var s;
return [
s = new HTMLSelectElement()
..className = 'analysis-select'
..value = snapshotToString(_snapshotB)
..appendChildren(
_loadedSnapshots.map(
(snapshot) => HTMLOptionElement()
..value = snapshotToString(snapshot)
..selected = _snapshotB == snapshot
..textContent = snapshotToString(snapshot),
),
)
..onChange.listen((_) {
_snapshotB = _loadedSnapshots[s.selectedIndex];
selection = null;
mergedSelection = null;
mergedDiffSelection = null;
_r.dirty();
}),
];
}
}