blob: 3cffa6c542348770f8cf854cbecc6908f0ee6655 [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 'dart:collection';
import 'dart:html' as html;
import 'package:meta/meta.dart';
import 'package:vm_service/vm_service.dart';
import '../framework/framework.dart';
import '../globals.dart';
import '../popup.dart';
import '../table_data.dart';
import '../tables.dart';
import '../ui/analytics.dart' as ga;
import '../ui/analytics_platform.dart' as ga_platform;
import '../ui/custom.dart';
import '../ui/elements.dart';
import '../ui/icons.dart';
import '../ui/primer.dart';
import '../ui/ui_utils.dart';
import '../utils.dart';
import 'memory_chart.dart';
import 'memory_controller.dart';
import 'memory_data_view.dart';
import 'memory_detail.dart';
import 'memory_inbounds.dart';
import 'memory_protocol.dart';
import 'memory_service.dart';
const memoryScreenId = 'memory';
class MemoryScreen extends Screen with SetStateMixin {
MemoryScreen({bool disabled, String disabledTooltip})
: super(
name: 'Memory',
id: memoryScreenId,
iconClass: 'octicon-package',
disabled: disabled,
disabledTooltip: disabledTooltip,
) {
// Hookup for memory UI short-cut keys.
shortcutCallback = memoryShortcuts;
classCountStatus = StatusItem();
addStatusItem(classCountStatus);
objectCountStatus = StatusItem();
addStatusItem(objectCountStatus);
experimentStatus = StatusItem();
addStatusItem(experimentStatus);
}
final MemoryController memoryController = MemoryController();
StatusItem classCountStatus;
StatusItem objectCountStatus;
StatusItem experimentStatus;
PButton pauseButton;
PButton resumeButton;
// The autocomplete view manages the textfield and popup list.
CoreElement vmSearchField;
PopupListView<String> heapPopupList;
PopupAutoCompleteView heapAutoCompletePopup;
// Hover card shows where allocation occurred and references to instance.
final CoreElement hoverPopup = div(c: 'allocation-hover-card');
PButton vmMemorySearchButton;
PButton vmMemorySnapshotButton;
PButton resetAccumulatorsButton;
PButton filterLibrariesButton;
PButton gcNowButton;
ListQueue<Table<dynamic>> tableStack = ListQueue<Table<dynamic>>();
MemoryChart memoryChart;
CoreElement tableContainer;
InboundsTree _inboundTree;
// Memory navigation history. Driven from selecting items in the list of
// known classes, instances of a particular class and clicking on the class
// and field that allocated the instance (holds the reference).
// This list is displayed as a set of hyperlinks e.g.,
//
// class1 (instance) > class2.extra > class3.mainHolder
// ----------------- ------------ -----------------
//
// Clicking on one of the above links would select the class and instance that
// was associated with that hover navigation. In this case:
// [class3.mainHolder] - class3 called class2 constructor storing the
// reference to class2 in the field mainHolder.
// [class2.extra] - class2 called class1 constructor and stored the
// reference to class1 in field extra.
CoreElement history;
// This remembers how memory was navigated using the hover card to render the
// links in the history element (see above).
NavigationPath memoryPath = NavigationPath();
// Signals if navigation is happening as a result of clicking in a hover card.
// If true, keep recording the navigation instead of resetting history.
bool fromMemoryHover = false;
MemoryDataView memoryDataView;
MemoryTracker memoryTracker;
ProgressElement progressElement;
// TODO(terry): Remove experiment after binary snapshot is added.
bool get isMemoryExperiment => _memoryExperiment;
bool _memoryExperiment = false;
// Handle shortcut keys
bool memoryShortcuts(bool ctrlKey, bool shiftKey, bool altKey, String key) {
if (ctrlKey && key == 'f') {
_search();
return true;
}
return false;
}
@override
void entering() {
_updateListeningState();
}
void updateResumeButton({@required bool disabled}) {
resumeButton.disabled = disabled;
}
void updatePauseButton({@required bool disabled}) {
pauseButton.disabled = disabled;
}
@override
CoreElement createContent(Framework framework) {
ga_platform.setupDimensions();
final CoreElement screenDiv = div(c: 'custom-scrollbar')..layoutVertical();
resumeButton = PButton.icon('Resume', FlutterIcons.resume_white_disabled_2x)
..primary()
..small()
..disabled = true;
pauseButton = PButton.icon('Pause', FlutterIcons.pause_black_2x)..small();
heapPopupList = PopupListView<String>();
vmSearchField = CoreElement('input', classes: 'search-text')
..setAttribute('type', 'text')
..setAttribute('placeholder', 'search')
..id = 'popup_search_memory';
vmMemorySearchButton =
PButton.icon('', FlutterIcons.search, title: 'Memory Search')
..small()
..click(_search)
..disabled = true;
// TODO(terry): Need to correctly handle enabled and disabled.
vmMemorySnapshotButton = PButton.icon('Snapshot', FlutterIcons.snapshot,
title: 'Memory Snapshot')
..clazz('margin-left')
..small()
..click(
_loadAllocationProfile,
() {
// TODO(terry): Disable when real binary snapshot is exposed.
// Shift key pressed while clicking on Snapshot button enables live
// memory inspection.
_loadAllocationProfile(memoryExperiment: true);
},
)
..disabled = true;
resetAccumulatorsButton = PButton.icon(
'Reset', FlutterIcons.resetAccumulators,
title: 'Reset Accumulators')
..small()
..click(_resetAllocatorCounts)
..disabled = true;
filterLibrariesButton =
PButton.icon('Filter', FlutterIcons.filter, title: 'Filter')
..small()
..disabled = true;
heapAutoCompletePopup = PopupAutoCompleteView(
heapPopupList,
screenDiv,
vmSearchField,
_callbackPopupSelectClass,
);
gcNowButton =
PButton.icon('GC', FlutterIcons.gcNow, title: 'Manual Garbage Collect')
..small()
..click(_gcNow)
..disabled = true;
resumeButton.click(() {
ga.select(ga.memory, ga.resume);
updateResumeButton(disabled: true);
updatePauseButton(disabled: false);
memoryChart.resume();
});
pauseButton.click(() {
ga.select(ga.memory, ga.pause);
updatePauseButton(disabled: true);
updateResumeButton(disabled: false);
memoryChart.pause();
});
// Handle keeping card active while mouse in the hover card.
hoverPopup.onMouseOver.listen((html.MouseEvent evt) {
_mouseInHover(evt);
});
// Handle hiding card once mouse is outside of the hover card.
hoverPopup.onMouseLeave.listen((html.MouseEvent evt) {
_mouseOutHover(evt);
});
history = div(c: 'history-navigation section', a: 'hidden');
screenDiv.add(<CoreElement>[
div(c: 'section')
..add(<CoreElement>[
form()
..layoutHorizontal()
..clazz('align-items-center')
..add(<CoreElement>[
div(c: 'btn-group collapsible-885 flex-no-wrap')
..add(<CoreElement>[
pauseButton,
resumeButton,
]),
div()..flex(),
div(
c: 'btn-group collapsible-785 nowrap margin-left '
'memory-buttons')
..flex()
..add(<CoreElement>[
vmSearchField,
vmMemorySearchButton,
vmMemorySnapshotButton,
resetAccumulatorsButton,
filterLibrariesButton,
gcNowButton,
]),
]),
]),
memoryChart = MemoryChart(memoryController)..disabled = true,
tableContainer = div(c: 'section overflow-auto')
..layoutHorizontal()
..flex(),
history,
heapAutoCompletePopup,
hoverPopup, // Hover card
]);
memoryController.onDisconnect.listen((__) {
serviceDisconnect();
});
maybeAddDebugMessage(framework, memoryScreenId);
_pushNextTable(null, _createHeapStatsTableView());
_updateStatus(null);
return screenDiv;
}
ClassHeapDetailStats findClass(String className) {
final List<ClassHeapDetailStats> classesData = tableStack.first.model.data;
return classesData.firstWhere(
(stat) => stat.classRef.name == className,
orElse: () => null,
);
}
Future<List<InstanceSummary>> findInstances(ClassHeapDetailStats row) async {
try {
final List<InstanceSummary> instances =
await memoryController.getInstances(
row.classRef.id,
row.classRef.name,
row.instancesCurrent,
);
return instances;
} catch (e) {
// TODO(terry): Cleanup error.
print('findInstances ERROR: $e');
return [];
}
}
ClassHeapDetailStats findClassDetails(String classRefId) {
final List<ClassHeapDetailStats> classesData = tableStack.first.model.data;
return classesData.firstWhere(
(stat) => stat.classRef.id == classRefId,
orElse: () => null,
);
}
void _selectClass(String className, {bool record = true}) {
final List<ClassHeapDetailStats> classesData = tableStack.first.model.data;
int row = 0;
for (ClassHeapDetailStats stat in classesData) {
if (stat.classRef.name == className) {
tableStack.first.selectByIndex(row, scrollBehavior: 'auto');
if (record) {
memoryPath.add(NavigationState.classSelect(className));
}
return;
}
row++;
}
framework.toast('Unable to find class $className', title: 'Error');
}
Future<int> _selectInstanceInFieldHashCode(
String fieldName, int instanceHashCode) async {
final Table<Object> instanceTable = tableStack.elementAt(1);
final spinner = Spinner.centered();
instanceTable.element.add(spinner);
// There's an instances table up.
// TODO(terry): Need more efficient way to match ObjectRefs than hashCodes.
final List<InstanceSummary> instances = instanceTable.model.data;
int row = 0;
for (InstanceSummary instance in instances) {
// Check the field in each instance looking to find the object being held
// (the hashCode passed in matches the particular field's hashCode)
// TODO(terry): Enable below once expressions accessing private fields
// TODO(terry): e.g., _extra.hashCode works again. Better yet code that
// TODO(terry): is more efficient that allows objectRef identity.
//
// final evalResult = await evaluate(instance.objectRef, '$fieldName.hashCode');
// int fieldHashCode =
// evalResult != null ? int.parse(evalResult.valueAsString) : null;
//
// if (fieldHashCode == instanceHashCode) {
// // Found the object select the instance.
// instanceTable.selectByIndex(row, scrollBehavior: 'auto');
// spinner.remove();
// return row;
// }
// TODO(terry): Temporary workaround since evaluate fails on expressions
// TODO(terry): accessing a private field e.g., _extra.hashcode.
if (await memoryController.matchObject(
instance.objectRef, fieldName, instanceHashCode)) {
instanceTable.selectByIndex(row, scrollBehavior: 'auto');
spinner.remove();
return row;
}
row++;
}
spinner.remove();
framework.toast(
'Unable to find instance for field $fieldName [$hashCode]',
title: 'Error',
);
return -1;
}
void _resetHistory() {
history.hidden(true);
history.clear();
memoryPath = NavigationPath();
}
/// Finish callback from search class selected (auto-complete).
void _callbackPopupSelectClass([bool cancel]) {
if (cancel) {
heapAutoCompletePopup.matcher.reset();
heapPopupList.reset();
} else {
// Reset memory history selecting a class.
_resetHistory();
// Highlighted class is the class to select.
final String selectedClass = heapPopupList.highlightedItem;
if (selectedClass != null) _selectClass(selectedClass);
}
// Done with the popup.
heapAutoCompletePopup.hide();
}
void _selectInstanceByObjectRef(String objectRefToFind) {
removeInstanceView();
// There's an instances table up.
final Table<Object> instanceTable = tableStack.last;
final List<InboundsTreeNode> nodes = instanceTable.model.data;
final foundNode = nodes.firstWhere(
(node) => node.instance?.objectRef == objectRefToFind,
orElse: () => null,
);
if (foundNode != null) {
instanceTable.selectByIndex(
nodes.indexOf(foundNode),
scrollBehavior: 'auto',
);
}
}
Future<void> _selectInstanceByHashCode(int instanceHashCode) async {
// There's an instances table up.
final Table<Object> instanceTable = tableStack.last;
final List<InstanceSummary> instances = instanceTable.model.data;
int row = 0;
for (InstanceSummary instance in instances) {
// Check each instance looking to find a particular object.
// TODO(terry): Is there something faster for objectRef identity check?
final eval = await evaluate(instance.objectRef, 'hashCode');
final int evalHashCode = int.parse(eval?.valueAsString);
if (evalHashCode == instanceHashCode) {
// Found the object select the instance.
instanceTable.selectByIndex(row, scrollBehavior: 'auto');
return;
}
row++;
}
framework.toast('Unable to find instance [$instanceHashCode]',
title: 'Error');
}
bool get _isClassSelectedAndInstancesReady =>
tableStack.first.model.hasSelection &&
tableStack.length == 2 &&
tableStack.last.model.data.isNotEmpty;
void selectClassInstance(String className, int instanceHashCode) {
// Remove selection in class list.
tableStack.first.clearSelection();
// TODO(terry): Better solution is to await a Table event that tells us.
Timer.periodic(const Duration(milliseconds: 100), (Timer timer) {
if (!tableStack.first.model.hasSelection) {
// Wait until the class list has no selection.
timer.cancel();
}
});
// Select the class (don't record this select in memory history). The
// memoryPath will be added by NavigationState.inboundSelect - see below.
_selectClass(className, record: false);
// TODO(terry): Better solution is to await a Table event that tells us.
Timer.periodic(const Duration(milliseconds: 100), (Timer timer) async {
// Wait until the class has been selected, 2 lists (class and instances
// for the class exist) and the instances list has data.
if (_isClassSelectedAndInstancesReady) {
timer.cancel();
await _selectInstanceByHashCode(instanceHashCode);
}
});
}
void selectClassAndInstanceInField(
String className,
String field,
int instanceHashCode,
) async {
fromMemoryHover = true;
// Remove selection in class list.
tableStack.first.clearSelection();
// TODO(terry): Better solution is to await a Table event that tells us.
Timer.periodic(const Duration(milliseconds: 100), (Timer timer) {
if (!tableStack.first.model.hasSelection) {
// Wait until the class list has no selection.
timer.cancel();
}
});
// Select the class (don't record this select in memory history). The
// memoryPath will be added by NavigationState.inboundSelect - see below.
_selectClass(className, record: false);
// TODO(terry): Better solution is to await a Table event that tells us.
Timer.periodic(const Duration(milliseconds: 100), (Timer timer) async {
// Wait until the class has been selected, 2 lists (class and instances
// for the class exist) and the instances list has data.
if (_isClassSelectedAndInstancesReady) {
timer.cancel();
final int rowToSelect =
await _selectInstanceInFieldHashCode(field, instanceHashCode);
if (rowToSelect != -1) {
// Found the instance that refs the object (hashCode passed). Mark the
// field name (fieldReference). When the next instance memory path is
// added (in select) the field ill be stored in the NavigationState.
memoryPath.fieldReference = field;
}
// Wait for instance table, element 1, to have registered the selection.
// TODO(terry): Better solution is to await a Table event that tells us.
Timer.periodic(const Duration(milliseconds: 100), (Timer timer) async {
if (tableStack.length == 2 &&
tableStack.elementAt(1).model.hasSelection) {
timer.cancel();
// Done simulating all user UI actions as we navigate via hover thru
// classes, instances and fields.
fromMemoryHover = false;
}
});
}
});
}
void _pushNextTable(
Table<dynamic> current,
Table<dynamic> next, [
InboundsTree inboundTree,
]) {
// Remove any tables to the right of current from the DOM and the stack.
while (tableStack.length > 1 && tableStack.last != current) {
// TODO(terry): Hacky need to manage tables better.
if (tableStack.length == 2) {
_inboundTree = null;
}
tableStack.removeLast()
..element.element.remove()
..dispose();
}
// Push the new table on to the stack and to the right of current.
if (next != null) {
final bool isFirst = tableStack.isEmpty;
tableStack.addLast(next);
tableContainer.add(next.element);
// TODO(terry): Hacky need to manage tables better.
if (inboundTree != null) _inboundTree = inboundTree;
if (!isFirst) {
next.element.clazz('margin-left');
}
tableContainer.element.scrollTo(<String, dynamic>{
'left': tableContainer.element.scrollWidth,
'top': 0,
'behavior': 'smooth',
});
}
}
Future<void> _resetAllocatorCounts() async {
ga.select(ga.memory, ga.reset);
memoryChart.plotReset();
resetAccumulatorsButton.disabled = true;
tableStack.first.element.display = null;
final Spinner spinner = tableStack.first.element.add(Spinner.centered());
try {
final List<ClassHeapDetailStats> heapStats =
await memoryController.resetAllocationProfile();
tableStack.first.model.setRows(heapStats);
_updateStatus(heapStats);
spinner.remove();
} catch (e) {
framework.toast('Reset failed ${e.toString()}', title: 'Error');
} finally {
resetAccumulatorsButton.disabled = false;
}
}
final List<String> _knownSnapshotClasses = [];
List<String> getKnownSnapshotClasses() {
if (_knownSnapshotClasses.isEmpty) {
final List<ClassHeapDetailStats> classesData =
tableStack.first.model.data;
for (ClassHeapDetailStats stat in classesData) {
_knownSnapshotClasses.add(stat.classRef.name);
}
}
return _knownSnapshotClasses;
}
Future<void> _search() async {
ga.select(ga.memory, ga.search);
// Subsequent snapshots will reset heapPopupList to empty.
if (heapPopupList.isEmpty) {
// Only fetch once between snapshots.
heapPopupList.setList(getKnownSnapshotClasses());
}
if (!vmSearchField.isVisible) {
vmSearchField.element.style.visibility = 'visible';
vmSearchField.element.focus();
heapAutoCompletePopup.show();
} else {
heapAutoCompletePopup.matcher.finish(false); // Cancel popup auto-complete
}
}
Future<void> _loadAllocationProfile({
bool reset = false,
bool memoryExperiment = false,
}) async {
ga.select(ga.memory, ga.snapshot);
_memoryExperiment = memoryExperiment;
memoryChart.plotSnapshot();
// Empty the popup list - we'll repopulated from new snapshot.
heapPopupList.setList([]);
vmMemorySnapshotButton.disabled = true;
tableStack.first.element.display = null;
final Spinner spinner = tableStack.first.element.add(Spinner.centered());
try {
final List<ClassHeapDetailStats> heapStats =
await memoryController.getAllocationProfile();
// Reset known snapshot classes, just changed.
_knownSnapshotClasses.clear();
tableStack.first.model.setRows(heapStats);
_updateStatus(heapStats);
spinner.remove();
} catch (e) {
framework.toast('Snapshot failed ${e.toString()}', title: 'Error');
} finally {
vmMemorySnapshotButton.disabled = false;
vmMemorySearchButton.disabled = false;
}
}
Future<void> _gcNow() async {
ga.select(ga.memory, ga.gC);
gcNowButton.disabled = true;
try {
await memoryController.gc();
} catch (e) {
framework.toast('Unable to GC ${e.toString()}', title: 'Error');
} finally {
gcNowButton.disabled = false;
}
}
void _updateListeningState() async {
await serviceManager.serviceAvailable.future;
final bool shouldBeRunning = isCurrentScreen;
if (shouldBeRunning && !memoryController.hasStarted) {
await memoryController.startTimeline();
pauseButton.disabled = false;
resumeButton.disabled = true;
vmMemorySnapshotButton.disabled = false;
resetAccumulatorsButton.disabled = false;
gcNowButton.disabled = false;
memoryChart.disabled = false;
}
}
// VM Service has stopped (disconnected).
void serviceDisconnect() {
pauseButton.disabled = true;
resumeButton.disabled = true;
vmMemorySnapshotButton.disabled = true;
resetAccumulatorsButton.disabled = true;
filterLibrariesButton.disabled = true;
gcNowButton.disabled = true;
memoryChart.disabled = true;
}
void removeInstanceView() {
if (tableContainer.element.children.length == 3) {
tableContainer.element.children.removeLast();
}
}
Table<ClassHeapDetailStats> _createHeapStatsTableView() {
final table = Table<ClassHeapDetailStats>.virtual()
..element.display = 'none'
..element.clazz('memory-table');
table.model
..addColumn(MemoryColumnSize())
..addColumn(MemoryColumnInstanceCount())
..addColumn(MemoryColumnInstanceAccumulatedCount())
..addColumn(MemoryColumnClassName());
table.model.sortColumn = table.model.columns.first;
table.model.onSelect.listen((ClassHeapDetailStats row) async {
ga.select(ga.memory, ga.inspectClass);
// User selected a new class from the list of classes so the instance view
// which would be the third child needs to be removed.
removeInstanceView();
final InboundsTree inboundTree =
row == null ? null : await displayInboundReferences(row);
final TreeTable<InboundsTreeNode> tree = inboundTree.referencesTable;
_pushNextTable(table, tree, inboundTree);
});
return table;
}
Future<InboundsTree> displayInboundReferences(
ClassHeapDetailStats row) async {
final treeData = InboundsTreeData()..data = InboundsTreeNode.root();
final List<InstanceSummary> instanceRows =
await memoryController.getInstances(
row.classRef.id,
row.classRef.name,
row.instancesCurrent,
);
for (var instance in instanceRows) {
final instanceNode = InboundsTreeNode.instance(instance);
treeData.data.addChild(instanceNode);
// Place holder to lazily compute next child when parent node is expanded.
// Place holder to lazily compute next child when parent node is expanded.
instanceNode.addChild(InboundsTreeNode.empty());
}
final inboundsTreeTable = InboundsTree(this, treeData, row.classRef.name);
return inboundsTreeTable..update();
}
Future<String> computeInboundReference(
String objectRef,
InboundsTreeNode instanceNode,
) async {
final refs = await getInboundReferences(objectRef, 1000);
String instanceHashCode;
if (isMemoryExperiment) {
// TODO(terry): Expensive need better VMService identity for objectRef.
// Get hashCode identity object id changes but hashCode is our identity.
final hashCodeResult = await evaluate(objectRef, 'hashCode');
instanceHashCode = hashCodeResult?.valueAsString;
}
final List<ClassHeapDetailStats> allClasses = tableStack.first.model.data;
computeInboundRefs(allClasses, refs, (
String referenceName,
String owningAllocator,
bool owningAllocatorIsAbstract,
) async {
if (!owningAllocatorIsAbstract && owningAllocator.isNotEmpty) {
final inboundNode =
InboundsTreeNode(owningAllocator, referenceName, instanceHashCode);
instanceNode.addChild(inboundNode);
if (_memoryExperiment) inboundNode.addChild(InboundsTreeNode.empty());
}
});
return instanceHashCode;
}
Future<InstanceSummary> findLostObjectRef(
String classRef,
int instanceHashCode,
) async {
final classDetails = findClassDetails(classRef);
if (classDetails != null) {
final List<InstanceSummary> instances =
await memoryController.getInstances(
classDetails.classRef.id,
classDetails.classRef.name,
classDetails.instancesCurrent,
);
for (var instance in instances) {
final InstanceRef eval = await evaluate(instance.objectRef, 'hashCode');
final int evalResult = int.parse(eval?.valueAsString);
if (evalResult == instanceHashCode) {
// Found the instance.
return instance;
}
}
}
return null;
}
Future<Instance> getInstance(String objectRef) async {
Instance instance;
try {
final dynamic theObject = await memoryController.getObject(objectRef);
if (theObject is Instance) {
instance = theObject;
} else if (theObject is Sentinel) {
instance = null;
// TODO(terry): Tracking Sentinel's to be removed.
framework.toast('Sentinel $objectRef', title: 'Warning');
}
} catch (e) {
// Log this problem not sure how it can really happen.
ga.error('Memory select (getInstance): $e', false);
instance = null; // Signal a problem
}
return instance;
}
void updateInstancesTree() {
_inboundTree.update();
}
void select(InboundsTreeNode rowNode) async {
ga.select(ga.memory, ga.inspectInstance);
// User selected a new instance from the list of class instances so the
// instance view which would be the third child needs to be removed.
removeInstanceView();
if (rowNode?.instance == null) return;
Instance instance = await getInstance(rowNode.instance.objectRef);
if (instance == null) {
// TODO(terry): Eliminate for eval
// Eval objectRef ids have changed re-fetch objectRef ids.
final newInstance = await findLostObjectRef(
rowNode.instance.classRef,
int.parse(rowNode.instanceHashCode),
);
framework.toast(
'Re-computed ${rowNode.instance.objectRef} -> ${newInstance.objectRef}',
title: 'Message',
);
// Update to the new objectRef id.
rowNode.setInstance(newInstance, rowNode.instanceHashCode, true);
instance = await getInstance(rowNode.instance.objectRef);
_inboundTree.update();
// Re-computing could cause instance in TableTree to move (change row).
// Find it and select it again.
_selectInstanceByObjectRef(rowNode.instance.objectRef);
}
tableContainer.add(_createInstanceView(
instance != null
? rowNode.instance.objectRef
: 'Unable to fetch instance ${rowNode.name}',
rowNode.instance.className,
));
tableContainer.element.scrollTo(<String, dynamic>{
'left': tableContainer.element.scrollWidth,
'top': 0,
'behavior': 'smooth',
});
// Allow inspection of the memory object.
memoryDataView.showFields(instance != null ? instance.fields : []);
}
// TD element used to simulate hover state when hover card is visible. When
// not null the mouse is actively in the hover card.
CoreElement _tdCellHover;
// InstanceSummary of the visible hover card.
HoverCell<InstanceSummary> _currentHoverSummary;
// This is the listener for the hover card (hoverPopup's) onMouseOver, it's
// designed to keep the hover state (background-color for the TD same as the
// CSS :hover) as the mouse slides to the hover card. It gives the appearance
// that hover is still active in the TD.
void _mouseInHover(html.MouseEvent evt) {
final CoreElement cell = _currentHoverSummary?.cell;
if (cell != null) _tdCellHover = cell;
// Simulate the :hover when the mouse in hover card.
_tdCellHover?.clazz('allocation-hover', removeOthers: true);
_tdCellHover?.clazz('left');
}
// This is the listener for the hover card (hoverPopup's) onMouseLeave, it's
// designed to end the hover state (background-color for the TD same as the
// CSS :hover) as the mouse slides out of the hover card. It gives the
// appearance that the hover is not active.
void _mouseOutHover(html.MouseEvent evt) {
// Done simulating hover, hover card is closing. Reset to CSS handling the
// :hover for the allocation class.
_tdCellHover?.clazz('allocation', removeOthers: true);
_tdCellHover?.clazz('left');
if (_tdCellHover != null) _tdCellHover = null;
_currentHoverSummary = null;
// We're really leaving hover so close it.
hoverPopup.clear(); // Remove all children.
hoverPopup.display = 'none';
}
void _closeHover(HoverCell<InstanceSummary> newCurrent) {
// We're really leaving hover so close it.
hoverPopup.clear(); // Remove all children.
hoverPopup.display = 'none';
_currentHoverSummary = newCurrent;
}
void _maybeCloseHover() {
final String hoverToClose = _currentHoverSummary?.data?.objectRef;
Timer(const Duration(milliseconds: 50), () {
if (_tdCellHover == null &&
hoverToClose == _currentHoverSummary?.data?.objectRef) {
// We're really leaving hover so close it.
_closeHover(null);
}
});
}
static const String dataHashCode = 'data-hashcode';
static const String dataOwningClass = 'data-owning-class';
static const String dataRef = 'data-ref';
void hoverInstanceAllocations(HoverCellData<InstanceSummary> data) async {
final HoverCell<InstanceSummary> hover = data;
if (hover.cell == null) {
// Hover out of the cell.
_maybeCloseHover();
return;
}
// Hover in the cell.
if (hover.data != _currentHoverSummary?.data) {
// Selecting a different instance then what's current.
_closeHover(hover);
}
// Entering Hover again?
if (hoverPopup.element.children.isNotEmpty) return;
final CoreElement ulElem = ul();
final refs = await getInboundReferences(hover.data.objectRef, 1000);
if (refs == null) {
framework.toast(
'Instance ${hover.data.objectRef} - Sentinel/Expired.',
);
return;
}
ulElem.add(li(c: 'allocation-li-title')
..add([
span(text: 'Allocated', c: 'allocated-by-class-title'),
span(text: 'Referenced', c: 'ref-by-title')
]));
final List<ClassHeapDetailStats> allClasses = tableStack.first.model.data;
computeInboundRefs(
allClasses,
refs,
(
String referenceName,
String owningAllocator,
bool owningAllocatorIsAbstract,
) async {
// Callback function to build each item in the hover card.
final classAllocation = owningAllocatorIsAbstract
? 'allocation-abstract allocated-by-class'
: 'allocated-by-class';
final fieldAllocation =
owningAllocatorIsAbstract ? 'allocation-abstract ref-by' : 'ref-by';
final CoreElement liElem = li(c: 'allocation-li')
..add([
span(
text: 'class $owningAllocator',
c: classAllocation,
),
span(
text: 'field $referenceName',
c: fieldAllocation,
),
]);
if (owningAllocatorIsAbstract) {
// Mark as grayed/italic
liElem.clazz('li-allocation-abstract');
}
if (!owningAllocatorIsAbstract && owningAllocator.isNotEmpty) {
// TODO(terry): Expensive need better VMService identity for objectRef.
// Get hashCode identity object id changes but hashCode is our identity.
final hashCodeResult =
await evaluate(hover.data.objectRef, 'hashCode');
liElem.setAttribute(dataHashCode, hashCodeResult?.valueAsString);
liElem.setAttribute(dataOwningClass, owningAllocator);
liElem.setAttribute(dataRef, referenceName);
}
liElem.onClick.listen((evt) {
final html.Element e = evt.currentTarget;
String className = e.getAttribute(dataOwningClass);
if (className == null || className.isEmpty) {
className = e.parent.getAttribute(dataOwningClass);
}
String refName = e.getAttribute(dataRef);
if (refName == null || refName.isEmpty) {
refName = e.parent.getAttribute(dataRef);
}
String objectHashCode = e.getAttribute(dataHashCode);
if (objectHashCode == null || objectHashCode.isEmpty) {
objectHashCode = e.parent.getAttribute(dataHashCode);
}
final int instanceHashCode = int.parse(objectHashCode);
// Done with the hover - close it down.
_closeHover(null);
// Make sure its a known class (not abstract).
if (className.isNotEmpty &&
refName.isNotEmpty &&
instanceHashCode != null) {
// Display just the instances of classes with ref
selectClassAndInstanceInField(className, refName, instanceHashCode);
}
});
ulElem.add(liElem);
},
);
if (hover.cell != null && hover.cell.hasClass('allocation')) {
// Hover over
final int top = hover.cell.top + 10;
final int left = hover.cell.left + 21;
hoverPopup.clear(); // TODO(terry): Workaround multiple ULs?
hoverPopup.add(ulElem);
// Display the popup.
hoverPopup
..display = 'block'
..element.style.top = '${top}px'
..element.style.left = '${left}px'
..element.style.height = '';
}
}
CoreElement _createInstanceView(String objectRef, String className) {
final MemoryDescriber describer = (BoundField field) async {
if (field == null) {
return null;
}
final dynamic value = field.value;
// TODO(terry): Replace two if's with switch (value.runtimeType)
if (value is Sentinel) {
return value.valueAsString;
}
if (value is TypeArgumentsRef) {
return value.name;
}
final InstanceRef ref = value;
if (ref?.valueAsString != null && !ref.valueAsStringIsTruncated) {
return ref.valueAsString;
} else {
// Shouldn't happen but want to check - log to analytics.
ga.error(
'Memory _createInstanceView: UNKNOWN BoundField $objectRef', false);
}
return null;
};
memoryDataView = MemoryDataView(memoryController, describer);
return div(
c: 'table-border table-virtual memory-table margin-left debugger-menu')
..layoutVertical()
..add(<CoreElement>[
div(
text: '$className instance $objectRef',
c: 'memory-inspector',
),
memoryDataView.element,
]);
}
void _updateStatus(List<ClassHeapDetailStats> data) {
if (data == null) {
classCountStatus.element.text = '';
objectCountStatus.element.text = '';
} else {
classCountStatus.element.text = '${nf.format(data.length)} classes';
int objectCount = 0;
for (ClassHeapDetailStats stats in data) {
objectCount += stats.instancesCurrent;
}
objectCountStatus.element.text = '${nf.format(objectCount)} objects';
}
experimentStatus.element.text =
isMemoryExperiment ? 'Experiment' : 'Memory';
}
}
/// Path consists of:
/// Class selected (from Class list):
/// _className
/// _hashCode = empty
/// field = empty
///
/// Instance selected (from Instance list):
/// _className
/// _hashCode [hashCode of instance]
/// field = empty
///
/// Hover (from inboundReferences) parent allocations:
/// _className [class name of parent class that allocated object]
/// _hashCode [hashCode of instance]
/// field [field of parent class that has ref]
class NavigationState {
NavigationState._() : _className = '';
NavigationState.classSelect(this._className);
NavigationState.instanceSelect(this._className, this._hashCode);
// data attribute names.
static const String dataIndex = 'data-index';
static const String dataClass = 'data-class';
static const String dataField = 'data-field';
static const String dataHashCode = 'data-hashcode';
String field = '';
String get className => _className;
final String _className;
int get instanceHashCode => _hashCode;
int _hashCode;
bool get isClass =>
_className.isNotEmpty && field.isEmpty && _hashCode == null;
bool get isInstance =>
_className.isNotEmpty && field.isEmpty && _hashCode != null;
bool get isInbound =>
_className.isNotEmpty && field.isNotEmpty && _hashCode != null;
// Create a span with all information to navigate through the class list and
// instance list. The span element will look like:
//
// <span class=N data-index=# data-class=N data-field=N data-hashcode=N>
// class[.field]
// </span>
//
// where:
// class=N is the css class for styling
// index=# is the index of this Navigation link in the NavigationPath list
// data-class=N is the class selected in the memory class list
// data-field=N if specified, references previous history hashcode (object)
// data-hashcode=N if specified, object referenced in this data-class field
CoreElement link(int index, [bool last = false]) {
final String spanText = field.isNotEmpty
? '$className.$field'
: isInstance ? '$className (instance)' : className;
final CoreElement spanElem =
span(text: spanText, c: last ? 'history-link-last' : 'history-link');
spanElem.setAttribute(dataIndex, '$index');
spanElem.setAttribute(dataClass, className);
if (field.isNotEmpty) spanElem.setAttribute(dataField, field);
if (instanceHashCode != null) {
spanElem.setAttribute(dataHashCode, instanceHashCode.toString());
}
return spanElem;
}
CoreElement get separator => span(text: '>', c: 'history-separator');
}
// Used to manage all memory navigation from user clicks or hover card
// navigation so user can visually understand the relationship of the current
// memory object being displayed.
class NavigationPath {
final List<NavigationState> _path = [];
// Global field name next add if state object isInstance then store the field
// name in the state.
String _inboundFieldName = '';
set fieldReference(String field) => _inboundFieldName = field;
bool get isEmpty => _path.isEmpty;
bool get isNotEmpty => _path.isNotEmpty;
void add(NavigationState state) {
if (state.isInbound) {
throw Exception('Inbound use not valid here.');
}
// If adding a state and the global inbound is set, then record this field
// with the state.
if (state.isInstance && _inboundFieldName.isNotEmpty) {
state.field = _inboundFieldName;
}
_inboundFieldName = '';
if (_path.isNotEmpty) {
final lastState = _path.last;
// if last state in path and same state we're to push, ignore - class
// being set by a click in history navigation.
if (lastState.isClass &&
state.isClass &&
lastState.className == state.className) return;
}
_path.add(state);
}
NavigationState get(int index) => _path[index];
void remove(NavigationState stateToRemove) {
for (int row = 0; row < _path.length; row++) {
final NavigationState state = _path[row];
if (stateToRemove == state) {
assert(state.instanceHashCode == stateToRemove.instanceHashCode &&
state.className == stateToRemove.className &&
state.field == stateToRemove.field);
_path.removeRange(row, _path.length);
return;
}
}
}
/// Is the last item in the path an inBound NavigationState.
bool get isLastInBound => _path.isNotEmpty ? _path.last.isInbound : false;
bool get isLastInstance => _path.isNotEmpty ? _path.last.isInstance : false;
// Display all the NavigationStates in our _path as UI links.
void displayPathsAsLinks(
CoreElement parent, {
void Function(CoreElement) clickHandler,
}) {
for (int index = 0; index < _path.length; index++) {
final NavigationState state = _path[index];
final bool lastLink = _path.length - 1 == index; // Last item in path?
final CoreElement link = state.link(index, lastLink);
if (clickHandler != null) {
link.click(() {
final CoreElement element = link;
clickHandler(element);
});
}
parent.add(link);
if (!lastLink) parent.add(state.separator);
}
}
}