blob: 42216553c0579f79bc84ebb3318f5d3a6cb97b30 [file] [log] [blame]
// Copyright 2019 The Chromium Authors. 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 'package:vm_service/vm_service.dart';
import '../table_data.dart';
import '../tables.dart';
import '../trees.dart';
import '../ui/custom.dart';
import '../ui/elements.dart';
import 'memory.dart';
import 'memory_protocol.dart';
import 'memory_service.dart';
class InboundsTree extends InstanceRefsView {
InboundsTree(
this._memoryScreen,
InboundsTreeData inboundsTree,
String className,
) : super(inboundsTree) {
flex();
layoutVertical();
_init(className);
}
final MemoryScreen _memoryScreen;
TreeTable<InboundsTreeNode> referencesTable;
Spinner spinner;
void _init(String className) {
final title =
'${inboundsTree.data.children.length} Instances of $className';
final classNameColumn = ClassNameColumn(title)
..onNodeExpanded.listen((inboundNode) async {
// TODO(terry): Fix need to support simultaneous expansions.
if (spinner != null) return;
if (inboundNode.children.length == 1 &&
inboundNode.children[0].isEmpty) {
inboundNode.children.removeLast();
// Make sure it's a known class (not abstract).
if (!inboundNode.isEmpty) {
spinner = Spinner.centered();
referencesTable.element.add(spinner);
if (inboundNode.instanceHashCode == null &&
inboundNode.instance != null) {
// Need the hashCode. It's slow - do it when needed.
// TODO(terry): Needs to be used with real snapshot.
final String instanceHashCode =
await _memoryScreen.computeInboundReference(
inboundNode.instance.objectRef,
inboundNode,
);
inboundNode.instanceHashCode = instanceHashCode;
}
final instanceHashCode = _memoryScreen.isMemoryExperiment
? int.parse(inboundNode.instanceHashCode)
: -1;
final ClassHeapDetailStats classStats =
_memoryScreen.findClass(inboundNode.name);
if (_memoryScreen.isMemoryExperiment) {
// All instances of a class.
final List<InstanceSummary> instances =
await _memoryScreen.findInstances(classStats);
int instanceIndex = 1;
for (InstanceSummary instance in instances) {
// Give feedback on what is happening node name appended with
// ' (N of NNN)' instances of total instances being processed.
inboundNode.working(instanceIndex++, instances.length);
_memoryScreen.updateInstancesTree();
// Found the instance.
final refs =
await getInboundReferences(instance.objectRef, 1000);
// TODO(terry): Temporary workaround since evaluate fails on expressions
// TODO(terry): accessing a private field e.g., _extra.hashcode.
if (await _memoryScreen.memoryController.matchObject(
instance.objectRef,
inboundNode.fieldName,
instanceHashCode,
)) {
// TODO(terry): Expensive need better VMService identity for objectRef.
// Get hashCode identity object id changes but hashCode is our identity.
InstanceRef hashCodeResult;
hashCodeResult = await evaluate(
instance.objectRef,
'hashCode',
);
// Record we have a real instance too.
inboundNode.setInstance(
instance,
hashCodeResult?.valueAsString,
);
final List<ClassHeapDetailStats> allClasses =
_memoryScreen.tableStack.first.model.data;
computeInboundRefs(
allClasses,
refs,
(
String referenceName,
String owningAllocator,
bool owningAllocatorIsAbstract,
) async {
if (!owningAllocatorIsAbstract &&
owningAllocator.isNotEmpty) {
final newRefNode = InboundsTreeNode(
owningAllocator,
referenceName,
hashCodeResult?.valueAsString,
);
inboundNode.addChild(newRefNode);
newRefNode.addChild(InboundsTreeNode.empty());
}
},
);
break;
}
}
}
spinner.remove();
// TODO(terry): Make spinner local using as a sentry.
spinner = null;
}
}
referencesTable.model.expandNode(inboundNode);
})
..onNodeCollapsed.listen(
(inboundNode) => referencesTable.model.collapseNode(inboundNode));
referencesTable = TreeTable<InboundsTreeNode>.virtual()
..element.clazz('memory-table');
referencesTable.model
..addColumn(classNameColumn)
..addColumn(FieldNameColumn())
..setRows(<InboundsTreeNode>[]);
referencesTable.model.onSelect.listen(_memoryScreen.select);
add(referencesTable.element);
}
@override
void rebuildView() {
final InboundsTreeData providerData = inboundsTree;
final List<InboundsTreeNode> rows = providerData.data.root.children.cast();
// TODO(terry): Work around bug if children have a parent (which they do
// TODO(terry): the TreeTable won't render.
for (InboundsTreeNode row in rows) row.parent = null;
referencesTable.model.setRows(rows);
}
@override
void reset() => referencesTable.model.setRows(<InboundsTreeNode>[]);
}
class InboundsTreeData {
InboundsTreeData();
InboundsTreeData.test() {
final treeNode00 = InboundsTreeNode('class_0_0', 'field_0');
final treeNode01 = InboundsTreeNode('class_0_1', 'field_1');
final treeNode02 = InboundsTreeNode('class_0_2', 'field_2');
final treeNode03 = InboundsTreeNode('class_0_3', 'field_3');
final treeNode04 = InboundsTreeNode('class_0_4', 'field_4');
final treeNode05 = InboundsTreeNode('class_0_5', 'field_5');
final treeNode10 = InboundsTreeNode('class_1', 'field_a');
final treeNode11 = InboundsTreeNode('class_1_1', 'field_b');
final treeNode12 = InboundsTreeNode('class_1_2', 'field_c');
final treeNode13 = InboundsTreeNode('class_1_3', 'field_d');
final treeNode14 = InboundsTreeNode('class_4_4', 'field_e');
final treeNode15 = InboundsTreeNode('class_1_5', 'field_f');
final terryStuff = InboundsTreeNode(
'TerryStuff allocated TerryExtra', 'extra [object/14752]');
final shrineAppState1 = InboundsTreeNode('_ShrineAppState', '_stuff');
final shrineAppState2 = InboundsTreeNode('_ShrineAppState', '_stuff2');
final statefulElement1 = InboundsTreeNode('StatefulElement', 'state');
final hashmapEntry1 = InboundsTreeNode('_HashMapEntry', '_key');
final singleChildrenObjectElement1 =
InboundsTreeNode('SingleChildrenObjectElement', '_child');
final statefulElement2 = InboundsTreeNode('StatefulElement', 'state');
final hashmapEntry2 = InboundsTreeNode('_HashMapEntry', '_key');
final singleChildrenObjectElement2 =
InboundsTreeNode('SingleChildrenObjectElement', '_child');
data = InboundsTreeNode.root()
..addChild(treeNode00
..addChild(treeNode01)
..addChild(treeNode02
..addChild(treeNode03)
..addChild(treeNode04)
..addChild(treeNode05)))
..addChild(treeNode10
..addChild(treeNode11)
..addChild(treeNode12)
..addChild(treeNode13)
..addChild(treeNode14)
..addChild(treeNode15))
..addChild(terryStuff
..addChild(shrineAppState1
..addChild(statefulElement1
..addChild(hashmapEntry1..addChild(singleChildrenObjectElement1))))
..addChild(shrineAppState2
..addChild(statefulElement2
..addChild(
hashmapEntry2..addChild(singleChildrenObjectElement2)))));
}
InboundsTreeNode data;
}
class InboundsTreeNode extends TreeNode<InboundsTreeNode> {
InboundsTreeNode(this._name, this.fieldName, [this.instanceHashCode]);
InboundsTreeNode.instance(this._instance, [this.instanceHashCode])
: _name = _instance.objectRef,
fieldName = '';
InboundsTreeNode.root()
: _name = 'Instances',
fieldName = '',
instanceHashCode = null;
InboundsTreeNode.empty()
: _name = null,
fieldName = null,
instanceHashCode = null;
String get name => _name;
String _name;
InstanceSummary get instance => _instance;
/// Replaces the node's [instance], [instanceHashCode] and [name]. This is
/// can happen as a result of the objectRef id changing (as known by the VM).
/// It's matched to the propery objectRef (instance) by comparing hashCodes.
///
/// [isNew] signals the objectRef (e.g., objects/123) changed to something
/// different (e.g., objects/245).
void setInstance(
InstanceSummary theInstance,
String hashCode, [
bool isNew = false,
]) {
_instance = theInstance;
instanceHashCode = hashCode;
_name = _name.split(' ')[0]; // Throw away instance objectRef name.
_name = (isNew && !isInboundEntry)
? _instance.objectRef
: '$name (${instance.objectRef})';
}
void working(int index, int total) {
_name = _name.split(' ')[0]; // Throw away instance objectRef name.
_name = '$name ($index of $total)';
}
InstanceSummary _instance;
final String fieldName;
bool get isInboundEntry => fieldName?.isNotEmpty;
String instanceHashCode;
bool get isEmpty =>
_name == null && fieldName == null && instanceHashCode == null;
}
abstract class InstanceRefsView extends CoreElement {
InstanceRefsView(this.inboundsTree) : super('div', classes: 'memory-table');
final InboundsTreeData inboundsTree;
bool viewNeedsRebuild = false;
void rebuildView();
void reset();
void update({bool showLoadingSpinner = false}) async {
if (inboundsTree == null) return;
// Update the view if it is visible. Otherwise, mark the view as needing a
// rebuild.
if (!isHidden) {
if (showLoadingSpinner) {
final spinner = Spinner.centered();
add(spinner);
// Awaiting this future ensures the spinner pops up in between switching
// table views. Without this, the UI is laggy and the spinner never
// appears.
await Future.delayed(const Duration(microseconds: 1));
rebuildView();
spinner.remove();
} else {
rebuildView();
}
} else {
viewNeedsRebuild = true;
}
}
void show() {
hidden(false);
if (viewNeedsRebuild) {
viewNeedsRebuild = false;
update(showLoadingSpinner: true);
}
}
void hide() => hidden(true);
}
class ClassNameColumn extends TreeColumnData<InboundsTreeNode> {
ClassNameColumn(String title) : super(title);
static const maxClassNameLength = 75;
@override
dynamic getValue(InboundsTreeNode dataObject) => dataObject.name;
@override
String getDisplayValue(InboundsTreeNode dataObject) {
final String name = dataObject.name;
if (name.length > maxClassNameLength) {
return name.substring(0, maxClassNameLength) + '...';
}
return name;
}
@override
bool get supportsSorting => true;
@override
String getTooltip(InboundsTreeNode dataObject) =>
'${dataObject.name} . ${dataObject.fieldName}';
}
//class FieldNameColumn extends TreeColumn<InboundsTreeNode> {
class FieldNameColumn extends ColumnData<InboundsTreeNode> {
FieldNameColumn() : super('Field Reference');
static const maxFieldNameLength = 25;
@override
dynamic getValue(InboundsTreeNode dataObject) => dataObject.fieldName;
@override
String getDisplayValue(InboundsTreeNode dataObject) {
final String fieldName = dataObject.fieldName;
if (fieldName.length > maxFieldNameLength) {
return fieldName.substring(0, maxFieldNameLength) + '...';
}
return fieldName;
}
@override
bool get supportsSorting => false;
@override
String getTooltip(InboundsTreeNode dataObject) =>
'${dataObject.fieldName} OF ${dataObject.name}';
}