diff --git a/packages/devtools_app/lib/src/screens/memory/memory_analyzer.dart b/packages/devtools_app/lib/src/screens/memory/memory_analyzer.dart deleted file mode 100644 index 9b2d3105d91..00000000000 --- a/packages/devtools_app/lib/src/screens/memory/memory_analyzer.dart +++ /dev/null @@ -1,721 +0,0 @@ -// Copyright 2020 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:collection'; - -import 'package:flutter/material.dart'; -import 'package:vm_service/vm_service.dart'; - -import '../../primitives/auto_dispose_mixin.dart'; -import '../../primitives/utils.dart'; -import '../../shared/table/table.dart'; -import '../../shared/table/table_data.dart'; -import '../../shared/utils.dart'; -import 'memory_controller.dart'; -import 'memory_graph_model.dart'; -import 'memory_heap_tree_view.dart'; -import 'memory_snapshot_models.dart'; - -bool _classMatcher(HeapGraphClassLive liveClass) { - final regExp = RegExp(knownClassesRegExs); - return regExp.allMatches(liveClass.name).isNotEmpty; -} - -/// Returns a map of all datapoints collected: -/// -/// key: 'externals' value: List -/// key: 'filters' value: List -/// key: 'libraries' value: List -/// -Map> collect( - MemoryController controller, - Snapshot snapshot, -) { - final Map> result = {}; - - if (snapshot.libraryRoot == null) return {}; - - // Analyze the snapshot's heap memory information - final root = snapshot.libraryRoot!; - final heapGraph = controller.heapGraph; - - for (final library in root.children) { - if (library.isExternals) { - final externalsToAnalyze = []; - - final ExternalReferences externals = library as ExternalReferences; - for (final ExternalReference external - in externals.children.cast()) { - final liveExternal = external.liveExternal; - final size = liveExternal.externalProperty.externalSize; - final liveElement = liveExternal.live; - final HeapGraphClassLive liveClass = - liveElement.theClass as HeapGraphClassLive; - if (_classMatcher(liveClass)) { - final instances = liveClass.getInstances(heapGraph); - externalsToAnalyze.add(external); - debugLogger( - 'Regex external found ${liveClass.name} ' - 'instances=${instances.length} ' - 'allocated bytes=$size', - ); - } - } - result['externals'] = externalsToAnalyze; - } else if (library.isFiltered) { - final filtersToAnalyze = []; - for (final Reference libraryRef in library.children) { - for (final ClassReference classRef - in libraryRef.children.cast()) { - final HeapGraphClassLive liveClass = classRef.actualClass!; - if (_classMatcher(liveClass)) { - filtersToAnalyze.add(classRef); - final instances = liveClass.getInstances(heapGraph); - debugLogger( - 'Regex filtered found ${classRef.name} ' - 'instances=${instances.length}', - ); - } - } - result['filters'] = filtersToAnalyze; - } - } else if (library.isLibrary) { - final librariesToAnalyze = []; - for (final ClassReference classRef - in library.children.cast()) { - final HeapGraphClassLive liveClass = classRef.actualClass!; - if (_classMatcher(liveClass)) { - librariesToAnalyze.add(classRef); - final instances = liveClass.getInstances(heapGraph); - debugLogger( - 'Regex library found ${classRef.name} ' - 'instances=${instances.length}', - ); - } - } - result['libraries'] = librariesToAnalyze; - } else if (library.isAnalysis) { - // Nothing to do on anay analyses. - } - } - - return result; -} - -const bucket10K = '1..10K'; -const bucket50K = '10K..50K'; -const bucket100K = '50K..100K'; -const bucket500K = '100K..500K'; -const bucket1M = '500K..1M'; -const bucket10M = '1M..10M'; -const bucket50M = '10M..50M'; -const bucketBigM = '50M+'; - -class Bucket { - Bucket(this.totalCount, this.totalBytes); - - int totalCount; - int totalBytes; -} - -void imageAnalysis( - MemoryController controller, - AnalysisSnapshotReference analysisSnapshot, - Map> collectedData, -) { - // TODO(terry): Look at heap rate of growth (used, external, RSS). - - // TODO(terry): Any items with Reference.isEmpty need to be computed e.g., onExpand, - collectedData.forEach((key, value) { - switch (key) { - case 'externals': - final externalsNode = AnalysisReference('Externals'); - analysisSnapshot.addChild(externalsNode); - for (final ExternalReference ref in value.cast()) { - final HeapGraphExternalLive liveExternal = ref.liveExternal; - final HeapGraphElementLive liveElement = liveExternal.live; - - /// TODO(terry): Eliminate or show sentinels for total instances? - final objectNode = AnalysisReference( - '${ref.name}', - countNote: liveElement.theClass!.instancesCount, - ); - externalsNode.addChild(objectNode); - var childExternalSizes = 0; - final bucketSizes = SplayTreeMap(); - for (final ExternalObjectReference child - in ref.children.cast()) { - if (child.externalSize < 10000) { - bucketSizes.putIfAbsent(bucket10K, () => Bucket(0, 0)); - bucketSizes[bucket10K]!.totalCount += 1; - bucketSizes[bucket10K]!.totalBytes += child.externalSize; - } else if (child.externalSize < 50000) { - bucketSizes.putIfAbsent(bucket50K, () => Bucket(0, 0)); - bucketSizes[bucket50K]!.totalCount += 1; - bucketSizes[bucket50K]!.totalBytes += child.externalSize; - } else if (child.externalSize < 100000) { - bucketSizes.putIfAbsent(bucket100K, () => Bucket(0, 0)); - bucketSizes[bucket100K]!.totalCount += 1; - bucketSizes[bucket100K]!.totalBytes += child.externalSize; - } else if (child.externalSize < 500000) { - bucketSizes.putIfAbsent(bucket500K, () => Bucket(0, 0)); - bucketSizes[bucket500K]!.totalCount += 1; - bucketSizes[bucket500K]!.totalBytes += child.externalSize; - } else if (child.externalSize < 1000000) { - bucketSizes.putIfAbsent(bucket1M, () => Bucket(0, 0)); - bucketSizes[bucket1M]!.totalCount += 1; - bucketSizes[bucket1M]!.totalBytes += child.externalSize; - } else if (child.externalSize < 10000000) { - bucketSizes.putIfAbsent(bucket10M, () => Bucket(0, 0)); - bucketSizes[bucket10M]!.totalCount += 1; - bucketSizes[bucket10M]!.totalBytes += child.externalSize; - } else if (child.externalSize < 50000000) { - bucketSizes.putIfAbsent(bucket50M, () => Bucket(0, 0)); - bucketSizes[bucket50M]!.totalCount += 1; - bucketSizes[bucket50M]!.totalBytes += child.externalSize; - } else { - bucketSizes.putIfAbsent(bucketBigM, () => Bucket(0, 0)); - bucketSizes[bucketBigM]!.totalCount += 1; - bucketSizes[bucketBigM]!.totalBytes += child.externalSize; - } - - childExternalSizes += child.externalSize; - } - - final bucketNode = AnalysisReference( - 'Buckets', - sizeNote: childExternalSizes, - ); - bucketSizes.forEach((key, value) { - bucketNode.addChild( - AnalysisReference( - '$key', - countNote: value.totalCount, - sizeNote: value.totalBytes, - ), - ); - }); - objectNode.addChild(bucketNode); - } - break; - case 'filters': - case 'libraries': - final librariesNode = AnalysisReference('Library $key'); - - final matches = drillIn(controller, librariesNode, value); - - final imageCacheNode = processMatches(controller, matches); - if (imageCacheNode != null) { - librariesNode.addChild(imageCacheNode); - } - - analysisSnapshot.addChild(librariesNode); - } - }); -} - -AnalysisReference? processMatches( - MemoryController controller, - Map> matches, -) { - // Root __FIELDS__ is a container for children, the children - // are added, later, to a treenode - if the treenode should - // be created. - final AnalysisField pending = AnalysisField( - '__FIELDS__', - null, - ); - final AnalysisField cache = AnalysisField( - '__FIELDS__', - null, - ); - final AnalysisField live = AnalysisField( - '__FIELDS__', - null, - ); - - var countPending = 0; - var countCache = 0; - var countLive = 0; - bool imageCacheFound = false; - matches.forEach((key, values) { - final fields = key.split('.'); - imageCacheFound = fields[0] == imageCache; - - for (final value in values) { - switch (fields[1]) { - case '_pendingImages': - countPending++; - pending.addChild(AnalysisField('url', value)); - break; - case '_cache': - countCache++; - cache.addChild(AnalysisField('url', value)); - break; - case '_liveImages': - countLive++; - live.addChild(AnalysisField('url', value)); - break; - } - } - }); - - if (imageCacheFound) { - final imageCacheNode = AnalysisReference( - imageCache, - countNote: countPending + countCache + countLive, - ); - - final pendingNode = AnalysisReference( - 'Pending', - countNote: countPending, - ); - - final cacheNode = AnalysisReference( - 'Cache', - countNote: countCache, - ); - - final liveNode = AnalysisReference( - 'Live', - countNote: countLive, - ); - - final pendingInstance = AnalysisInstance( - controller, - 'Images', - pending, - ); - final cacheInstance = AnalysisInstance( - controller, - 'Images', - cache, - ); - final liveInstance = AnalysisInstance( - controller, - 'Images', - live, - ); - - pendingNode.addChild(pendingInstance); - imageCacheNode.addChild(pendingNode); - - cacheNode.addChild(cacheInstance); - imageCacheNode.addChild(cacheNode); - - liveNode.addChild(liveInstance); - imageCacheNode.addChild(liveNode); - - return imageCacheNode; - } - - return null; -} - -// TODO(terry): Add a test, insure debugMonitor output never seen before checkin. - -/// Enable monitoring. -bool _debugMonitorEnabled = false; - -// Name of classes to monitor then all field/object are followed with debug -// information during drill in, e.g., -final _debugMonitorClasses = ['ImageCache']; - -/// Class being monitored if its name is in the debugMonitorClasses. -String? _debugMonitorClass; - -void _debugMonitor(String msg) { - if (!_debugMonitorEnabled || _debugMonitorClass == null) return; - print('--> $_debugMonitorClass:$msg'); -} - -ClassFields fieldsStack = ClassFields(); - -Map> drillIn( - MemoryController? controller, - AnalysisReference librariesNode, - List references, { - createTreeNodes = false, -}) { - final Map> result = {}; - - final matcher = ObjectMatcher((className, fields, value) { - final key = '$className.${fields.join(".")}'; - result.putIfAbsent(key, () => []); - result[key]!.add(value); - }); - - for (final Reference classRef in references) { - if (classRef.name == null || !matcher.isClassMatched(classRef.name!)) { - continue; - } - - final name = classRef.name!; - - // Insure instances are realized (not Reference.empty). - computeInstanceForClassReference(controller, classRef); - final HeapGraphClassLive? liveClass = classRef.actualClass; - - late AnalysisReference objectNode; - if (createTreeNodes) { - objectNode = AnalysisReference( - '$name', - countNote: liveClass!.instancesCount, - ); - librariesNode.addChild(objectNode); - } - - if (_debugMonitorEnabled) { - _debugMonitorClass = _debugMonitorClasses.contains(name) ? name : ''; - } - - fieldsStack.push(name); - - var instanceIndex = 0; - _debugMonitor('Class $name Instance=$instanceIndex'); - for (final ObjectReference objRef - in classRef.children.cast()) { - final fields = objRef.instance.getFields(); - // Root __FIELDS__ is a container for children, the children - // are added, later, to a treenode - if the treenode should - // be created. - final AnalysisField fieldsRoot = AnalysisField( - '__FIELDS__', - null, - ); - - for (final field in fields) { - if (field.value.isSentinel) continue; - - final HeapGraphElementLive live = field.value as HeapGraphElementLive; - - if (live.references!.isNotEmpty) { - _debugMonitor('${field.key} OBJECT Start'); - - final fieldObjectNode = AnalysisField(field.key, ''); - - fieldsStack.push(field.key); - displayObject( - matcher, - fieldObjectNode, - live, - createTreeNodes: createTreeNodes, - ); - fieldsStack.pop(); - - if (createTreeNodes) { - fieldsRoot.addChild(fieldObjectNode); - } - _debugMonitor('${field.key} OBJECT End'); - } else { - final value = displayData(live); - if (value != null) { - fieldsStack.push(field.key); - matcher.findFieldMatch(fieldsStack, value); - fieldsStack.pop(); - - _debugMonitor('${field.key} = $value'); - if (createTreeNodes) { - final fieldNode = AnalysisField(field.key, value); - fieldsRoot.addChild(fieldNode); - } - } else { - _debugMonitor('${field.key} Skipped null'); - } - } - } - - if (createTreeNodes) { - final instanceNode = AnalysisInstance( - controller, - 'Instance $instanceIndex', - fieldsRoot, - ); - objectNode.addChild(instanceNode); - } - - instanceIndex++; - } - - fieldsStack.pop(); // Pop class name. - } - - return result; -} - -bool? displayObject( - ObjectMatcher matcher, - AnalysisField objectField, - HeapGraphElementLive live, { - depth = 0, - maxDepth = 4, - createTreeNodes = false, -}) { - if (depth >= maxDepth) return null; - if (live.references?.isEmpty ?? true) return true; - - final fields = live.getFields(); - for (final field in fields) { - if (field.value.isSentinel) continue; - - final HeapGraphElementLive liveField = field.value as HeapGraphElementLive; - for (final ref in liveField.references ?? []) { - if (ref.isSentinel) continue; - final HeapGraphElementLive liveRef = ref as HeapGraphElementLive; - final objectFields = liveRef.getFields(); - if (objectFields.isEmpty) continue; - - final newObject = AnalysisField(field.key, ''); - _debugMonitor('${field.key} OBJECT start [depth=$depth]'); - - depth++; - - fieldsStack.push(field.key); - final continueResult = displayObject( - matcher, - newObject, - liveRef, - depth: depth, - createTreeNodes: createTreeNodes, - ); - fieldsStack.pop(); - - depth--; - // Drilled in enough, stop. - if (continueResult == null) continue; - - if (createTreeNodes) { - objectField.addChild(newObject); - } - _debugMonitor('${field.key} OBJECT end [depth=$depth]'); - } - - final value = displayData(liveField); - if (value != null) { - fieldsStack.push(field.key); - matcher.findFieldMatch(fieldsStack, value); - fieldsStack.pop(); - - _debugMonitor('${field.key}=$value'); - if (createTreeNodes) { - final node = AnalysisField(field.key, value); - objectField.addChild(node); - } - } - } - - return true; -} - -String? displayData(instance) { - String? result; - - switch (instance.origin.data.runtimeType) { - case HeapSnapshotObjectNullData: - case HeapSnapshotObjectNoData: - case Null: - case TypeArguments: - break; - default: - result = '${instance.origin.data}'; - } - - return result; -} - -class AnalysisInstanceViewTable extends StatefulWidget { - @override - AnalysisInstanceViewState createState() => AnalysisInstanceViewState(); -} - -/// Table of the fields of an instance (type, name and value). -class AnalysisInstanceViewState extends State - with - AutoDisposeMixin, - ProvidedControllerMixin { - final TreeColumnData _treeColumn = _AnalysisFieldNameColumn(); - - final _columns = >[]; - - @override - void initState() { - super.initState(); - - // Setup the table columns. - _columns.addAll([ - _treeColumn, - _AnalysisFieldValueColumn(), - ]); - } - - @override - void didChangeDependencies() { - super.didChangeDependencies(); - - if (!initController()) return; - cancelListeners(); - - // Update the chart when the memorySource changes. - addAutoDisposeListener(controller.selectedSnapshotNotifier, () { - setState(() { - controller.computeAllLibraries(rebuild: true); - }); - }); - } - - @override - Widget build(BuildContext context) { - return TreeTable( - keyFactory: (typeRef) => PageStorageKey(typeRef.name ?? ''), - dataRoots: controller.analysisInstanceRoot!, - dataKey: 'instance-fields', - columns: _columns, - treeColumn: _treeColumn, - defaultSortColumn: _columns[0], - defaultSortDirection: SortDirection.ascending, - ); - } -} - -class _AnalysisFieldNameColumn extends TreeColumnData { - _AnalysisFieldNameColumn() : super('Name'); - - @override - dynamic getValue(AnalysisField dataObject) => dataObject.name; - - @override - String getDisplayValue(AnalysisField dataObject) => '${getValue(dataObject)}'; - - @override - bool get supportsSorting => true; - - @override - int compare(AnalysisField a, AnalysisField b) { - final Comparable valueA = getValue(a); - final Comparable valueB = getValue(b); - return valueA.compareTo(valueB); - } - - @override - double get fixedWidthPx => 250.0; -} - -class _AnalysisFieldValueColumn extends ColumnData { - _AnalysisFieldValueColumn() - : super( - 'Value', - fixedWidthPx: scaleByFontFactor(350.0), - ); - - @override - dynamic getValue(AnalysisField dataObject) => dataObject.value; - - @override - String getDisplayValue(AnalysisField dataObject) { - var value = getValue(dataObject); - if (value is String && value.length > 30) { - value = '${value.substring(0, 13)}…${value.substring(value.length - 17)}'; - } - return '$value'; - } - - @override - bool get supportsSorting => true; - - @override - int compare(AnalysisField a, AnalysisField b) { - final Comparable valueA = getValue(a); - final Comparable valueB = getValue(b); - return valueA.compareTo(valueB); - } -} - -class ClassFields { - final List _fields = []; - - void clear() { - _fields.clear(); - } - - int get length => _fields.length; - - void push(String name) { - _fields.add(name); - } - - String pop() => _fields.removeLast(); - - String elementAt(int index) => _fields.elementAt(index); -} - -const imageCache = 'ImageCache'; - -/// Callback function when an ObjectReference's class name and fields all match. -/// Parameters: -/// className that matched -/// fields that all matched -/// value of the matched objectReference -/// -typedef CompletedFunction = void Function( - String className, - List fields, - dynamic value, -); - -class ObjectMatcher { - ObjectMatcher(this._matchCompleted); - - static const Map>> matcherDrillIn = { - '$imageCache': [ - ['_pendingImages', 'data_', 'completer', 'context_', 'url'], - ['_cache', 'data_', 'completer', 'context_', 'url'], - ['_liveImages', 'data_', 'completer', 'context_', 'url'], - ] - }; - - final CompletedFunction _matchCompleted; - - bool isClassMatched(String className) => - matcherDrillIn.containsKey(className); - - List>? _findClassMatch(String className) => - matcherDrillIn[className]; - - // TODO(terry): Change to be less strict. Look for subclass or parentage - // relationships. If a new field or subclass is added we can - // still find what we're looking for. Maybe even consider the - // the type we're looking for - best to be loosey goosey so - // we're not brittle as the Framework or any code changes. - /// First field name match. - bool findFieldMatch(ClassFields classFields, dynamic value) { - bool matched = false; - - final className = classFields._fields.elementAt(0); - final listOfFieldsToMatch = _findClassMatch(className); - - if (listOfFieldsToMatch != null) { - for (final fieldsToMatch in listOfFieldsToMatch) { - final fieldsSize = fieldsToMatch.length; - if (fieldsSize == classFields._fields.length - 1) { - for (var index = 0; index < fieldsSize; index++) { - if (fieldsToMatch[index] == - classFields._fields.elementAt(index + 1)) { - matched = true; - } else { - matched = false; - break; - } - } - } - - if (matched) { - _matchCompleted(className, fieldsToMatch, value); - break; - } - } - } - - return matched; - } -} diff --git a/packages/devtools_app/lib/src/screens/memory/memory_controller.dart b/packages/devtools_app/lib/src/screens/memory/memory_controller.dart index 7b4543b2f1d..9c3c9c76699 100644 --- a/packages/devtools_app/lib/src/screens/memory/memory_controller.dart +++ b/packages/devtools_app/lib/src/screens/memory/memory_controller.dart @@ -4,7 +4,6 @@ import 'dart:async'; -import 'package:collection/collection.dart' show IterableExtension; import 'package:devtools_shared/devtools_shared.dart'; import 'package:flutter/foundation.dart'; import 'package:intl/intl.dart'; @@ -19,15 +18,10 @@ import '../../primitives/utils.dart'; import '../../service/service_extensions.dart'; import '../../service/service_manager.dart'; import '../../shared/globals.dart'; -import '../../shared/table/table.dart'; import '../../shared/utils.dart'; -import '../../ui/search.dart'; -import 'memory_graph_model.dart'; import 'memory_protocol.dart'; -import 'memory_snapshot_models.dart'; import 'panes/allocation_profile/allocation_profile_table_view_controller.dart'; import 'panes/diff/controller/diff_pane_controller.dart'; -import 'primitives/filter_config.dart'; import 'primitives/memory_timeline.dart'; import 'shared/heap/model.dart'; @@ -115,13 +109,10 @@ class OfflineFileException implements Exception { /// This class must not have direct dependencies on dart:html. This allows tests /// of the complicated logic in this class to run on the VM. class MemoryController extends DisposableController - with - AutoDisposeControllerMixin, - SearchControllerMixin, - AutoCompleteSearchControllerMixin { + with AutoDisposeControllerMixin { MemoryController({DiffPaneController? diffPaneController}) { memoryTimeline = MemoryTimeline(offline); - memoryLog = MemoryLog(this); + memoryLog = _MemoryLog(this); this.diffPaneController = diffPaneController ?? DiffPaneController(SnapshotTaker()); @@ -147,148 +138,14 @@ class MemoryController extends DisposableController static const logFilenamePrefix = 'memory_log_'; - final List snapshots = []; - - Snapshot? get lastSnapshot => snapshots.safeLast; - /// Root nodes names that contains nodes of either libraries or classes depending on /// group by library or group by class. static const libraryRootNode = '___LIBRARY___'; static const classRootNode = '___CLASSES___'; - /// Notifies that the source of the memory feed has changed. - ValueListenable get selectedSnapshotNotifier => - _selectedSnapshotNotifier; - final _shouldShowLeaksTab = ValueNotifier(false); ValueListenable get shouldShowLeaksTab => _shouldShowLeaksTab; - static String formattedTimestamp(DateTime? timestamp) => - timestamp != null ? DateFormat('MMM dd HH:mm:ss').format(timestamp) : ''; - - /// Stored value is pretty timestamp when the snapshot was done. - final _selectedSnapshotNotifier = ValueNotifier(null); - - set selectedSnapshotTimestamp(DateTime? snapshotTimestamp) { - _selectedSnapshotNotifier.value = snapshotTimestamp; - } - - DateTime? get selectedSnapshotTimestamp => _selectedSnapshotNotifier.value; - - HeapGraph? heapGraph; - - /// Leaf node of tabletree snapshot selected? If selected then the instance - /// view is displayed to view the fields of an instance. - final _leafSelectedNotifier = ValueNotifier(null); - - ValueListenable get leafSelectedNotifier => - _leafSelectedNotifier; - - HeapGraphElementLive? get selectedLeaf => _leafSelectedNotifier.value; - - set selectedLeaf(HeapGraphElementLive? selected) { - _leafSelectedNotifier.value = selected; - } - - bool get isLeafSelected => selectedLeaf != null; - - void computeRoot() { - if (selectedLeaf != null) { - final root = instanceToFieldNodes(this, selectedLeaf!); - _instanceRoot = root.isNotEmpty ? root : [FieldReference.empty]; - } else { - _instanceRoot = [FieldReference.empty]; - } - } - - List? _instanceRoot; - - List? get instanceRoot => _instanceRoot; - - /// Leaf node of analysis selected? If selected then the field - /// view is displayed to view an abbreviated fields of an instance. - final _leafAnalysisSelectedNotifier = ValueNotifier(null); - - ValueListenable get leafAnalysisSelectedNotifier => - _leafAnalysisSelectedNotifier; - - AnalysisInstance? get selectedAnalysisLeaf => - _leafAnalysisSelectedNotifier.value; - - set selectedAnalysisLeaf(AnalysisInstance? selected) { - _leafAnalysisSelectedNotifier.value = selected; - } - - bool get isAnalysisLeafSelected => selectedAnalysisLeaf != null; - - void computeAnalysisInstanceRoot() { - if (selectedAnalysisLeaf != null) { - final List analysisFields = - selectedAnalysisLeaf!.fieldsRoot.children; - _analysisInstanceRoot = - analysisFields.isNotEmpty ? analysisFields : [AnalysisField.empty]; - } else { - _analysisInstanceRoot = [AnalysisField.empty]; - } - } - - List? _analysisInstanceRoot; - - List? get analysisInstanceRoot => _analysisInstanceRoot; - - // List of completed Analysis of Snapshots. - final List completedAnalyses = []; - - /// Determine the snapshot to analyze - current active snapshot (selected or node - /// under snapshot selected), last snapshot or null (unknown). - Snapshot? get computeSnapshotToAnalyze { - // Any snapshots to analyze? - if (snapshots.isEmpty) return null; - - // Is a selected table row under a snapshot. - final nodeSelected = selectionSnapshotNotifier.value.node; - final snapshot = getSnapshot(nodeSelected); - if (snapshot != null) { - // Has the snapshot (with a selected row) been analyzed? - return _findSnapshotAnalyzed(snapshot); - } - - final snapshotsCount = snapshots.length; - final analysesCount = completedAnalyses.length; - - // Exactly one analysis is left? Ff the 'Analysis' button is pressed the - // snapshot that is left will be processed (usually the last one). More - // than one snapshots to analyze, the user must select the snapshot to - // analyze. - if (snapshotsCount > analysesCount && - snapshotsCount == (analysesCount + 1)) { - // Has the last snapshot been analyzed? - return _findSnapshotAnalyzed(lastSnapshot!); - } - - return null; - } - - /// Has the snapshot been analyzed, if not return the snapshot otherwise null. - Snapshot? _findSnapshotAnalyzed(Snapshot snapshot) { - final snapshotDateTime = snapshot.collectedTimestamp; - final foundMatch = completedAnalyses - .where((analysis) => analysis.dateTime == snapshotDateTime); - if (foundMatch.isEmpty) return snapshot; - - return null; - } - - ValueListenable get treeMapVisible => _treeMapVisible; - - final _treeMapVisible = ValueNotifier(false); - - void toggleTreeMapVisible(bool value) { - _treeMapVisible.value = value; - } - - bool isAnalyzeButtonEnabled() => computeSnapshotToAnalyze != null; - ValueListenable get legendVisibleNotifier => _legendVisibleNotifier; final _legendVisibleNotifier = ValueNotifier(false); @@ -300,7 +157,7 @@ class MemoryController extends DisposableController late MemoryTimeline memoryTimeline; - late MemoryLog memoryLog; + late _MemoryLog memoryLog; /// Source of memory heap samples. False live data, True loaded from a /// memory_log file. @@ -434,17 +291,6 @@ class MemoryController extends DisposableController final isAndroidChartVisibleNotifier = ValueNotifier(false); - final settings = SettingsModel(); - - final selectionSnapshotNotifier = - ValueNotifier>(Selection.empty()); - - /// Tree to view Libary/Class/Instance (grouped by) - late TreeTable groupByTreeTable; - - /// Tree to view fields of an instance. - TreeTable? instanceFieldsTreeTable; - final _updateClassStackTraces = ValueNotifier(0); ValueListenable get updateClassStackTraces => _updateClassStackTraces; @@ -453,104 +299,6 @@ class MemoryController extends DisposableController _updateClassStackTraces.value += 1; } - final FilterConfig filterConfig = FilterConfig( - filterZeroInstances: ValueNotifier(true), - filterLibraryNoInstances: ValueNotifier(true), - filterPrivateClasses: ValueNotifier(true), - libraryFilters: FilteredLibraries(), - ); - - /// All known libraries of the selected snapshot. - LibraryReference? get libraryRoot { - // Find the selected snapshot's libraryRoot. - final snapshot = getSnapshot(selectionSnapshotNotifier.value.node); - if (snapshot != null) return snapshot.libraryRoot; - - return null; - } - - /// Re-compute the libraries (possible filter change). - set libraryRoot(LibraryReference? newRoot) { - Snapshot? snapshot; - - // Use last snapshot. - if (snapshots.isNotEmpty) { - snapshot = snapshots.safeLast; - } - - // Find the selected snapshot's libraryRoot. - snapshot ??= getSnapshot(selectionSnapshotNotifier.value.node); - snapshot?.libraryRoot = newRoot; - } - - /// Using the tree table find the active snapshot (selected or last snapshot). - SnapshotReference get activeSnapshot { - for (final topLevel in groupByTreeTable.dataRoots) { - if (topLevel is SnapshotReference) { - final nodeSelected = selectionSnapshotNotifier.value.node; - final snapshot = getSnapshot(nodeSelected); - final SnapshotReference snapshotRef = topLevel; - if (snapshot != null && - snapshotRef.snapshot.collectedTimestamp == - snapshot.collectedTimestamp) { - return topLevel; - } - } - } - - // No selected snapshot so return the last snapshot. - final lastSnapshot = groupByTreeTable.dataRoots.safeLast!; - assert(lastSnapshot is SnapshotReference); - - return lastSnapshot as SnapshotReference; - } - - /// Given a node return its snapshot. - Snapshot? getSnapshot(Reference? reference) { - while (reference != null) { - if (reference is SnapshotReference) { - final SnapshotReference snapshotRef = reference; - return snapshotRef.snapshot; - } - reference = reference.parent; - } - - return null; - } - - /// Root node of all known analysis and snapshots. - LibraryReference? topNode; - - /// Root of known classes (used for group by class). - LibraryReference? classRoot; - - /// Notify that the filtering has changed. - ValueListenable get filterNotifier => _filterNotifier; - - final _filterNotifier = ValueNotifier(0); - - void updateFilter() { - _filterNotifier.value++; - } - - ValueListenable get filterZeroInstancesListenable => - filterConfig.filterZeroInstances; - - ValueListenable get filterPrivateClassesListenable => - filterConfig.filterPrivateClasses; - - ValueListenable get filterLibraryNoInstancesListenable => - filterConfig.filterLibraryNoInstances; - - /// Table ordered by library, class or instance - static const groupByLibrary = 'Library'; - static const groupByClass = 'Class'; - static const groupByInstance = 'Instance'; - - final groupingBy = ValueNotifier(groupByLibrary); - - ValueListenable get groupingByNotifier => groupingBy; - String? get _isolateId => serviceManager.isolateManager.selectedIsolate.value?.id; @@ -712,224 +460,6 @@ class MemoryController extends DisposableController // to line number of the source file when clicked is needed. static const packageName = '/packages/'; - /// When new snapshot occurs entire libraries should be rebuilt then rebuild should be true. - LibraryReference? computeAllLibraries({ - bool filtered = true, - bool rebuild = false, - HeapSnapshotGraph? graph, - }) { - final HeapSnapshotGraph? snapshotGraph = - graph != null ? graph : snapshots.safeLast?.snapshotGraph; - - if (snapshotGraph == null) return null; - - if (filtered && libraryRoot != null && !rebuild) return libraryRoot; - - // Group by library - final newLibraryRoot = LibraryReference(this, libraryRootNode, null); - - // Group by class (under root library __CLASSES__). - classRoot = LibraryReference(this, classRootNode, null); - - final externalReferences = - ExternalReferences(this, snapshotGraph.externalSize); - for (final liveExternal - in heapGraph?.externals ?? []) { - if (liveExternal == null) continue; - - final HeapGraphClassLive? classLive = - liveExternal.live.theClass as HeapGraphClassLive?; - - ExternalReference? externalReference; - - if (externalReferences.children.isNotEmpty) { - externalReference = externalReferences.children.singleWhereOrNull( - (knownClass) => knownClass.name == classLive?.name, - ) as ExternalReference?; - } - - if (externalReference == null) { - externalReference = - ExternalReference(this, classLive?.name ?? '', liveExternal); - externalReferences.addChild(externalReference); - } - - final classInstance = ExternalObjectReference( - this, - externalReference.children.length, - liveExternal.live, - liveExternal.externalProperty.externalSize, - ); - - // Sum up the externalSize of the children, under the externalReference group. - externalReference.sumExternalSizes += - liveExternal.externalProperty.externalSize; - - externalReference.addChild(classInstance); - } - - newLibraryRoot.addChild(externalReferences); - - // Add our filtered items under the 'Filtered' node. - if (filtered) { - final filteredReference = FilteredReference(this); - final filtered = heapGraph!.filteredLibraries; - addAllToNode(filteredReference, filtered); - - newLibraryRoot.addChild(filteredReference); - } - - // Compute all libraries. - final groupBy = - filtered ? heapGraph!.groupByLibrary : heapGraph!.rawGroupByLibrary; - - groupBy.forEach((libraryName, classes) { - LibraryReference? libReference = - newLibraryRoot.children.singleWhereOrNull((library) { - return libraryName == library.name; - }) as LibraryReference?; - - // Library not found add to list of children. - if (libReference == null) { - libReference = LibraryReference(this, libraryName, classes); - newLibraryRoot.addChild(libReference); - } - - for (var actualClass in libReference.actualClasses ?? {}) { - monitorClass( - className: actualClass!.name, - message: 'computeAllLibraries', - ); - final classRef = ClassReference(this, actualClass); - classRef.addChild(Reference.empty); - - libReference.addChild(classRef); - - // TODO(terry): Consider adding the ability to clear the table tree cache - // (root) to reset the level/depth values. - final classRefClassGroupBy = ClassReference(this, actualClass); - classRefClassGroupBy.addChild(Reference.empty); - classRoot!.addChild(classRefClassGroupBy); - } - }); - - // TODO(terry): Eliminate chicken and egg issue. - // This may not be set if snapshot is being computed, first-time. Returning - // newLibraryRoot allows new snapshot to store the libraryRoot. - libraryRoot = newLibraryRoot; - - return newLibraryRoot; - } - - // TODO(terry): Change to Set of known libraries so it's O(n) instead of O(n^2). - void addAllToNode( - Reference root, - Map> allItems, - ) { - allItems.forEach((libraryName, classes) { - LibraryReference? libReference = - root.children.singleWhereOrNull((library) { - return libraryName == library.name; - }) as LibraryReference?; - - // Library not found add to list of children. - libReference ??= LibraryReference(this, libraryName, classes); - root.addChild(libReference); - - for (var actualClass in libReference.actualClasses ?? {}) { - monitorClass( - className: actualClass!.name, - message: 'computeAllLibraries', - ); - final classRef = ClassReference(this, actualClass); - classRef.addChild(Reference.empty); - - libReference.addChild(classRef); - - // TODO(terry): Consider adding the ability to clear the table tree cache - // (root) to reset the level/depth values. - final classRefClassGroupBy = ClassReference(this, actualClass); - classRefClassGroupBy.addChild(Reference.empty); - classRoot!.addChild(classRefClassGroupBy); - } - }); - } - - AnalysesReference? findAnalysesNode() { - if (topNode == null) return null; - - for (final child in topNode!.children) { - if (child is AnalysesReference) { - return child; - } - } - return null; - } - - void createSnapshotEntries(Reference? parent) { - for (final snapshot in snapshots) { - final Reference? snaphotMatch = - parent!.children.firstWhereOrNull((element) { - var result = false; - if (element is SnapshotReference) { - final SnapshotReference node = element; - result = node.snapshot == snapshot; - } - - return result; - }); - if (snaphotMatch == null) { - // New snapshot add it. - final snapshotNode = SnapshotReference(snapshot); - parent.addChild(snapshotNode); - - final allLibraries = - computeAllLibraries(graph: snapshot.snapshotGraph)!; - snapshotNode.addAllChildren(allLibraries.children); - - return; - } - } - } - - final _treeChangedNotifier = ValueNotifier(false); - - ValueListenable get treeChangedNotifier => _treeChangedNotifier; - - bool get isTreeChanged => _treeChangedNotifier.value; - - void treeChanged({bool state = true}) { - if (_treeChangedNotifier.value) { - _treeChangedNotifier.value = false; - } - _treeChangedNotifier.value = state; - } - - Reference? buildTreeFromAllData() { - final List? oldChildren = topNode?.children; - if (isTreeChanged) topNode = null; - topNode ??= LibraryReference(this, libraryRootNode, null); - - if (isTreeChanged && oldChildren != null) { - topNode!.addAllChildren(oldChildren); - } - - final anyAnalyses = topNode!.children - .firstWhereOrNull((reference) => reference is AnalysesReference) != - null; - - if (snapshots.isNotEmpty && !anyAnalyses) { - // Create Analysis entry. - final analysesRoot = AnalysesReference(); - analysesRoot.addChild(AnalysisReference('')); - topNode!.addChild(analysesRoot); - } - - createSnapshotEntries(topNode); - - return topNode; - } - Future getObject(String objectRef) async => await serviceManager.service!.getObject( _isolateId!, @@ -952,35 +482,6 @@ class MemoryController extends DisposableController } } - List? snapshotByLibraryData; - - void createSnapshotByLibrary() { - snapshotByLibraryData ??= lastSnapshot?.librariesToList(); - } - - Snapshot storeSnapshot( - DateTime timestamp, - HeapSnapshotGraph graph, - LibraryReference libraryRoot, { - bool autoSnapshot = false, - }) { - final snapshot = Snapshot( - timestamp, - this, - graph, - libraryRoot, - autoSnapshot, - ); - snapshots.add(snapshot); - - return snapshot; - } - - void clearAllSnapshots() { - snapshots.clear(); - snapshotByLibraryData = null; - } - /// Detect stale isolates (sentinaled), may happen after a hot restart. Future isIsolateLive(String isolateId) async { try { @@ -1009,23 +510,6 @@ class MemoryController extends DisposableController } } -/// Settings dialog model. -class SettingsModel { - /// Pattern is of the form: - /// - empty string implies no matching. - /// - NNN* implies match anything starting with NNN. - /// - *NNN implies match anything ending with NNN. - String pattern = ''; - - /// If true hide Class names that begin with an underscore. - bool hidePrivateClasses = true; - - /// If true enable the memory experiment that following a object instance via - /// inbound references instances. Compares hashCodes (using eval causing - /// memory shaking). Only works in debug mode. - bool experiment = false; -} - /// Index in datasets to each dataset's list of Entry's. enum ChartDataSets { // Datapoint entries for each used heap value. @@ -1040,29 +524,9 @@ enum ChartDataSets { rasterPictureSet, } -/// Index in datasets to each dataset's list of Entry's. -enum EventDataSets { - // Datapoint entries for ghost trace to stop auto-scaling of Y-axis. - ghostsSet, - // Datapoint entries for each user initiated GC. - gcUserSet, - // Datapoint entries for a VM's GC. - gcVmSet, - // Datapoint entries for each user initiated snapshot event. - snapshotSet, - // Datapoint entries for an automatically initiated snapshot event. - snapshotAutoSet, - // Allocation Accumulator monitoring. - monitorStartSet, - // TODO(terry): Allocation Accumulator continues UX connector. - monitorContinuesSet, - // Reset all Allocation Accumulators. - monitorResetSet, -} - /// Supports saving and loading memory samples. -class MemoryLog { - MemoryLog(this.controller); +class _MemoryLog { + _MemoryLog(this.controller); /// Use in memory or local file system based on Flutter Web/Desktop. static final _fs = FileIO(); @@ -1128,6 +592,7 @@ class MemoryLog { } /// Load the memory profile data from a saved memory log file. + @visibleForTesting Future loadOffline(String filename) async { final jsonPayload = _fs.readStringFromFile(filename, isMemory: true)!; diff --git a/packages/devtools_app/lib/src/screens/memory/memory_filter.dart b/packages/devtools_app/lib/src/screens/memory/memory_filter.dart deleted file mode 100644 index 4962cd8f4aa..00000000000 --- a/packages/devtools_app/lib/src/screens/memory/memory_filter.dart +++ /dev/null @@ -1,218 +0,0 @@ -// Copyright 2020 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 'package:flutter/material.dart'; - -import '../../primitives/auto_dispose_mixin.dart'; -import '../../shared/common_widgets.dart'; -import '../../shared/dialogs.dart'; -import '../../shared/theme.dart'; -import 'memory_controller.dart'; -import 'memory_snapshot_models.dart'; -import 'primitives/filter_config.dart'; - -/// Name displayed in filter dialog, for wildcard groups. -const _prettyPrintDartAbbreviation = '$dartLibraryUriPrefix*'; -const _prettyPrintFlutterAbbreviation = '$flutterLibraryUriPrefix*'; - -/// State of the libraries and packages (hidden or not) for the filter dialog. -class LibraryFilter { - LibraryFilter(this.displayName, this.hide); - - /// Displayed library name. - final String displayName; - - /// Whether classes in this library hidden (filtered). - bool hide = false; -} - -class SnapshotFilterDialog extends StatefulWidget { - const SnapshotFilterDialog(this.controller); - - final MemoryController controller; - - @override - SnapshotFilterState createState() => SnapshotFilterState(); -} - -class SnapshotFilterState extends State - with AutoDisposeMixin { - bool _intitialized = false; - - late final bool oldFilterPrivateClassesValue; - - late final bool oldFilterZeroInstancesValue; - - late final bool oldFilterLibraryNoInstancesValue; - - final oldFilteredLibraries = {}; - - @override - void didChangeDependencies() { - super.didChangeDependencies(); - - if (_intitialized) return; - _intitialized = true; - - buildFilters(); - - final parameters = widget.controller.filterConfig; - oldFilterPrivateClassesValue = parameters.filterPrivateClasses.value; - oldFilterZeroInstancesValue = parameters.filterZeroInstances.value; - oldFilterLibraryNoInstancesValue = - parameters.filterLibraryNoInstances.value; - } - - void addLibrary(String libraryName, {bool hideState = false}) { - final filteredGroup = >{}; - final filters = widget.controller.filterConfig.libraryFilters; - - final isFiltered = filters.isLibraryFiltered(libraryName); - var groupedName = libraryName; - bool hide = hideState; - if (isFiltered) { - if (filters.isDartLibraryName(libraryName)) { - groupedName = _prettyPrintDartAbbreviation; - } else if (filters.isFlutterLibraryName(libraryName)) { - groupedName = _prettyPrintFlutterAbbreviation; - } - } - hide = isFiltered; - - // Used by checkboxes in dialog. - filteredGroup[groupedName] ??= []; - filteredGroup[groupedName]!.add(LibraryFilter(libraryName, hide)); - } - - void buildFilters() { - // First time filters created, populate with the default list of libraries - // to filters - - for (final library - in widget.controller.filterConfig.libraryFilters.librariesFiltered) { - addLibrary(library, hideState: true); - } - // If not snapshots, return no libraries to process. - if (widget.controller.snapshots.isEmpty) return; - - // No libraries to compute until a snapshot exist. - if (widget.controller.snapshots.isEmpty) return; - - final List libraries = widget.controller.libraryRoot == null - ? widget.controller.activeSnapshot.children - : widget.controller.libraryRoot!.children; - - libraries..sort((a, b) => a.name!.compareTo(b.name!)); - - for (final library in libraries) { - // Don't include external and filtered these are a composite and can't be filtered out. - if (library.name != externalLibraryName && - library.name != filteredLibrariesName) { - assert(library.name != null); - addLibrary(library.name!); - } - } - } - - Widget applyAndCancelButton() { - return Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - DialogApplyButton( - onPressed: () { - // Re-generate librariesFiltered - widget.controller.filterConfig.libraryFilters.clearFilters(); - // Re-filter the groups. - widget.controller.libraryRoot = null; - if (widget.controller.lastSnapshot != null) { - widget.controller.heapGraph!.computeFilteredGroups(); - widget.controller.computeAllLibraries( - graph: widget.controller.lastSnapshot!.snapshotGraph, - ); - } - - widget.controller.updateFilter(); - }, - ), - const SizedBox(width: defaultSpacing), - DialogCancelButton( - cancelAction: () { - widget.controller.filterConfig.filterPrivateClasses.value = - oldFilterPrivateClassesValue; - widget.controller.filterConfig.filterZeroInstances.value = - oldFilterZeroInstancesValue; - widget.controller.filterConfig.filterLibraryNoInstances.value = - oldFilterLibraryNoInstancesValue; - }, - ), - ], - ); - } - - @override - Widget build(BuildContext context) { - // Dialog has three main vertical sections: - // - three checkboxes - // - one list of libraries with at least 5 entries - // - one row of buttons Ok/Cancel - // For very tall app keep the dialog at a reasonable height w/o too much vertical whitespace. - // The listbox area is the area that grows to accommodate the list of known libraries. - - final theme = Theme.of(context); - - return DevToolsDialog( - title: const DialogTitleText('Memory Filter Libraries and Classes'), - includeDivider: false, - content: Container( - width: defaultDialogWidth, - child: Padding( - padding: const EdgeInsets.only(left: 15), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ...dialogSubHeader(theme, 'Snapshot Filters'), - Row( - children: [ - NotifierCheckbox( - notifier: - widget.controller.filterConfig.filterPrivateClasses, - ), - const DevToolsTooltip( - message: 'Hide class names beginning with ' - 'an underscore e.g., _className', - child: Text('Hide Private Classes'), - ), - ], - ), - Row( - children: [ - NotifierCheckbox( - notifier: - widget.controller.filterConfig.filterZeroInstances, - ), - const Text('Hide Classes with No Instances'), - ], - ), - Row( - children: [ - NotifierCheckbox( - notifier: widget - .controller.filterConfig.filterLibraryNoInstances, - ), - const Text('Hide Library with No Instances'), - ], - ), - applyAndCancelButton(), - ], - ), - ], - ), - ), - ), - ); - } -} diff --git a/packages/devtools_app/lib/src/screens/memory/memory_graph_model.dart b/packages/devtools_app/lib/src/screens/memory/memory_graph_model.dart deleted file mode 100644 index 9ffd11fecb8..00000000000 --- a/packages/devtools_app/lib/src/screens/memory/memory_graph_model.dart +++ /dev/null @@ -1,482 +0,0 @@ -// Copyright 2020 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 'package:vm_service/vm_service.dart'; - -// import '../config_specific/logger/logger.dart'; -import '../../primitives/utils.dart'; -import 'primitives/filter_config.dart'; -import 'primitives/predefined_classes.dart'; - -// TODO(terry): Investigate if class implements the Map interface? -bool isBuiltInMap(HeapGraphClassLive live) => - live.fullQualifiedName == predefinedMap || - live.fullQualifiedName == predefinedHashMap; - -/// Is it a built-in HashMap class (predefined). -bool isBuiltInHashMap(HeapGraphClassLive live) => - live.fullQualifiedName == predefinedHashMap; - -/// Is it a built-in List class (predefined). -bool isBuiltInList(HeapGraphClassLive live) => - live.fullQualifiedName == predefinedList; - -/// List of classes to monitor, helps to debug particular class structure. -final Map _monitorClasses = {}; - -/// Ensure the classId is zero based () -bool monitorClass({int? classId, String? className, String message = ''}) { - if (classId != null) { - if (_monitorClasses.containsKey(classId)) { - final className = _monitorClasses[classId]; - print('STOP: class $className [classId=$classId] $message'); - return true; - } - } else if (className != null) { - if (_monitorClasses.containsValue(className)) { - print('STOP: class $className $message'); - return true; - } - } else { - print('WARNING: Missing classId or className to monitor.'); - } - return false; -} - -HeapGraph convertHeapGraph( - FilterConfig filterConfig, - HeapSnapshotGraph graph, [ - List? classNamesToMonitor, -]) { - final Map builtInClasses = {}; - - if (classNamesToMonitor != null && classNamesToMonitor.isNotEmpty) { - print( - 'WARNING: Remove classNamesToMonitor before PR submission. ' - '$classNamesToMonitor', - ); - } - - // Construct all the classes in the snapshot. - final classes = List.filled(graph.classes.length, null); - for (int i = 0; i < graph.classes.length; i++) { - final HeapSnapshotClass c = graph.classes[i]; - - final className = c.name; - // Remember builtin classes classId e.g., bool, String (_OneByteString), etc. - // The classId is the index into the graph.classes list. - final libraryClass = LibraryClass('${c.libraryUri}', c.name); - // It's a exact match libraryName,className once we match the classId drives - // the class matching. - if (predefinedClasses.containsKey(libraryClass)) { - builtInClasses.putIfAbsent(libraryClass, () => i); - } - - // Debugging code to monitor a particular class. ClassesToMonitor should be - // empty before commiting PRs. - if (classNamesToMonitor != null && - classNamesToMonitor.isNotEmpty && - classNamesToMonitor.contains(className)) { - print('WARNING: class $className is monitored.'); - _monitorClasses.putIfAbsent(i, () => className); - } - - classes[i] = HeapGraphClassLive(c); - } - - // Pre-allocate the number of objects in the snapshot. - final elements = - List.filled(graph.objects.length, null); - - // Construct all objects. - for (int i = 0; i < graph.objects.length; i++) { - final HeapSnapshotObject o = graph.objects[i]; - elements[i] = HeapGraphElementLive(o); - } - - // Associate each object with a Class. - for (int i = 0; i < graph.objects.length; i++) { - final HeapSnapshotObject o = graph.objects[i]; - final HeapGraphElementLive converted = elements[i]!; - - // New style snapshot class index is zero based. - if (o.classId == 0) { - // classId of 0 is a sentinel. - converted.theClass = HeapGraph.classSentinel; - } else { - // Support for new snapshot class index is zero based (starts at - // zero) and the previous snapshot classId is 1-based index. - final classId = o.classId; - monitorClass(classId: classId); - converted.theClass = classes[classId]; - } - - // Lazily compute the references. - converted.referencesFiller = () { - for (int refId in o.references) { - HeapGraphElement ref; - if (refId == 0) { - ref = HeapGraph.elementSentinel; - } else { - ref = elements[refId]!; - } - converted.references!.add(ref); - } - }; - } - - final snapshotExternals = graph.externalProperties; - - // Pre-allocate the number of external objects in the snapshot. - final externals = - List.filled(snapshotExternals.length, null); - - // Construct all external objects and link to its live element. - for (int index = 0; index < snapshotExternals.length; index++) { - final snapshotObject = snapshotExternals[index]; - final liveElement = elements[snapshotObject.object]!; - externals[index] = HeapGraphExternalLive(snapshotObject, liveElement); - } - - return HeapGraph( - filterConfig, - builtInClasses, - classes, - elements, - externals, - ); -} - -class HeapGraph { - HeapGraph( - this.filterConfig, - this.builtInClasses, - this.classes, - this.elements, - this.externals, - ); - - final FilterConfig filterConfig; - - bool _instancesComputed = false; - - bool get instancesComputed => _instancesComputed; - - /// Known built-in classes. - final Map builtInClasses; - - /// Sentinel Class, all class sentinels point to this object. - static HeapGraphClassSentinel classSentinel = HeapGraphClassSentinel(); - - /// Indexed by classId. - final List classes; - - /// Sentinel Object, all object sentinels point to this object. - static HeapGraphElementSentinel elementSentinel = HeapGraphElementSentinel(); - - /// Index by objectId. - final List elements; - - /// Index by objectId of all external properties - List externals; - - /// Group all classes by libraries (key is library, value are classes). - /// This is the entire set of objects (no filter applied). - final Map> rawGroupByLibrary = {}; - - /// Group all classes by libraries (key is library, value is classes). - /// Filtering out objects that match a given filter. This is always a - /// subset of rawGroupByLibrary. - final Map> groupByLibrary = {}; - - /// Group all instances by class (key is class name, value are class - /// instances). This is the entire set of objects (no filter applied). - final Map> rawGroupByClass = {}; - - /// Group all instances by class (key is class name, value are class - /// instances). Filtering out objects that match a given filter. This - /// is always a subset of rawGroupByClass. - final Map> groupByClass = {}; - - /// Group of instances by filtered out classes (key is class name, value - /// are instances). These are the instances not in groupByClass, together - /// filteredElements and groupByClass are equivalent to rawGroupByClass. - final Map> filteredElements = {}; - - /// Group of libraries by filtered out classes (key is library name, value - /// are classes). These are the libraries not in groupByLibrary, together - /// filteredLibraries and groupByLibrary are equivalent to rawGroupByLibrary. - final Map> filteredLibraries = {}; - - /// Normalize the library name. Library is a Uri that contains - /// the schema e.g., 'dart' or 'package' and pathSegments. The - /// segments are paths to a dart file. Without simple normalization - /// 100s maybe 1000s of libraries would be displayed to the - /// developer. Normalizing takes the schema and the first part - /// of the path e.g., dart:core, package:flutter, etc. Hopefully, - /// this is not too chunky but better than no normalization. Also, - /// the empty library is returned as src e.g., lib/src - String normalizeLibraryName(HeapSnapshotClass theClass) { - final uri = theClass.libraryUri; - final scheme = uri.scheme; - - if (scheme == 'package' || scheme == 'dart') { - return '$scheme:${uri.pathSegments[0]}'; - } - - assert(theClass.libraryName.isEmpty); - return 'src'; - } - - void computeRawGroups() { - // Only compute once. - if (rawGroupByLibrary.isNotEmpty || rawGroupByClass.isNotEmpty) return; - - for (final c in classes) { - if (c == null) continue; - - final sb = StringBuffer(); - - final libraryKey = normalizeLibraryName(c.origin); - - // Collect classes for each library (group by library). - sb.write(libraryKey); - final librarySbToString = sb.toString(); - rawGroupByLibrary[librarySbToString] ??= {}; - rawGroupByLibrary[librarySbToString]!.add(c); - sb.clear(); - - // Collect instances for each class (group by class) - for (final instance in c.getInstances(this)) { - sb.write(c.name); - c.instancesTotalShallowSizes += instance.origin.shallowSize; - final classSbToString = sb.toString(); - rawGroupByClass[classSbToString] ??= {}; - rawGroupByClass[classSbToString]!.add(instance); - sb.clear(); - } - } - } - - void computeFilteredGroups() { - // Clone groupByClass from raw group. - groupByClass.clear(); - rawGroupByClass.forEach((key, value) { - groupByClass[key] = value.cast().toSet(); - }); - - // Prune classes that are private or have zero instances. - filteredElements.clear(); - groupByClass.removeWhere((className, instances) { - final remove = (filterConfig.filterZeroInstances.value && - instances.isEmpty) || - (filterConfig.filterPrivateClasses.value && isPrivate(className)) || - className == internalClassName; - if (remove) { - filteredElements.putIfAbsent(className, () => instances); - } - - return remove; - }); - - // Clone groupByLibrary from raw group. - groupByLibrary.clear(); - rawGroupByLibrary.forEach((key, value) { - groupByLibrary[key] = value.cast().toSet(); - }); - - // Prune libraries if all their classes are private or have zero instances. - filteredLibraries.clear(); - - groupByLibrary.removeWhere((libraryName, classes) { - classes.removeWhere((actual) { - final result = (filterConfig.filterZeroInstances.value && - actual.getInstances(this).isEmpty) || - (filterConfig.filterPrivateClasses.value && - isPrivate(actual.name)) || - actual.name == internalClassName; - return result; - }); - - final result = - (filterConfig.libraryFilters.isLibraryFiltered(libraryName)) || - filterConfig.filterLibraryNoInstances.value && classes.isEmpty; - if (result) { - filteredLibraries.putIfAbsent(libraryName, () => classes); - } - - return result; - }); - } - - // TODO(terry): Need dominator graph for flow. - /// Compute all instances, needed for retained space. - void computeInstancesForClasses() { - if (!instancesComputed) { - for (final instance in elements) { - instance?.theClass!.addInstance(instance); - } - - _instancesComputed = true; - } - } -} - -abstract class HeapGraphElement { - /// Outbound references, i.e. this element points to elements in this list. - List? _references; - - void Function()? referencesFiller; - - List? get references { - if (_references == null && referencesFiller != null) { - _references = []; - referencesFiller!(); - } - return _references; - } - - bool get isSentinel; -} - -/// Object marked for removal on next GC. -class HeapGraphElementSentinel extends HeapGraphElement { - @override - bool get isSentinel => true; - - @override - String toString() => 'HeapGraphElementSentinel'; -} - -/// Live element. -class HeapGraphElementLive extends HeapGraphElement { - HeapGraphElementLive(this.origin); - - final HeapSnapshotObject origin; - HeapGraphClass? theClass; - - @override - bool get isSentinel => false; - - HeapGraphElement? getField(String name) { - if (theClass is HeapGraphClassLive) { - final HeapGraphClassLive c = theClass as HeapGraphClassLive; - for (HeapSnapshotField field in c.origin.fields) { - if (field.name == name) { - return references![field.index]; - } - } - } - return null; - } - - List> getFields() { - final List> result = []; - if (theClass is HeapGraphClassLive) { - final HeapGraphClassLive c = theClass as HeapGraphClassLive; - for (final field in c.origin.fields) { - if (field.index < references!.length) { - result.add(MapEntry(field.name, references![field.index])); - } else { - // TODO(polinach): figure out what to do if index is out of range. - // See https://github.com/flutter/devtools/issues/3629 for details. - // log( - // 'ERROR Field Range: name=${field.name},index=${field.index}', - // LogLevel.error, - // ); - } - } - } - return result; - } - - @override - String toString() { - if (origin.data is HeapSnapshotObjectNoData) { - return 'Instance of $theClass'; - } - if (origin.data is HeapSnapshotObjectLengthData) { - final HeapSnapshotObjectLengthData data = origin.data; - return 'Instance of $theClass length = ${data.length}'; - } - return 'Instance of $theClass; data: \'${origin.data}\''; - } -} - -/// Live ExternalProperty. -class HeapGraphExternalLive extends HeapGraphElement { - HeapGraphExternalLive(this.externalProperty, this.live); - - final HeapSnapshotExternalProperty externalProperty; - final HeapGraphElementLive live; - - @override - bool get isSentinel => false; - - @override - String toString() { - if (live.origin.data is HeapSnapshotObjectNoData) { - return 'Instance of ${live.theClass}'; - } - if (live.origin.data is HeapSnapshotObjectLengthData) { - final HeapSnapshotObjectLengthData data = live.origin.data; - return 'Instance of ${live.theClass} length = ${data.length}'; - } - return 'Instance of ${live.theClass}; data: \'${live.origin.data}\''; - } -} - -abstract class HeapGraphClass { - final List _instances = []; - - int instancesTotalShallowSizes = 0; - - void addInstance(HeapGraphElementLive instance) { - _instances.add(instance); - } - - List getInstances(HeapGraph? graph) { - // TODO(polinach): this code never executes and should be reviewed and - // fixed. - // ignore: unnecessary_null_comparison - if (_instances == null) { - final len = graph?.elements.length ?? 0; - for (var i = 0; i < len; i++) { - final HeapGraphElementLive? converted = graph!.elements[i]; - if (converted != null && converted.theClass == this) { - _instances.add(converted); - } - } - } - return _instances; - } - - /// Quick short-circuit to return real size of null implies '--' yet to be - /// computed or N/A. - // TODO(polinach): _instances is never null. So, this code - // should be reviewed and fixed. - // ignore: unnecessary_null_comparison - int? get instancesCount => _instances == null ? null : _instances.length; -} - -class HeapGraphClassSentinel extends HeapGraphClass { - @override - String toString() => 'HeapGraphClassSentinel'; -} - -class HeapGraphClassLive extends HeapGraphClass { - HeapGraphClassLive(this.origin); - - final HeapSnapshotClass origin; - - String get name => origin.name; - - Uri get libraryUri => origin.libraryUri; - - LibraryClass get fullQualifiedName => - LibraryClass(libraryUri.toString(), name); - - @override - String toString() => name; -} diff --git a/packages/devtools_app/lib/src/screens/memory/memory_heap_tree_view.dart b/packages/devtools_app/lib/src/screens/memory/memory_heap_tree_view.dart deleted file mode 100644 index bb54ef5e776..00000000000 --- a/packages/devtools_app/lib/src/screens/memory/memory_heap_tree_view.dart +++ /dev/null @@ -1,1618 +0,0 @@ -// Copyright 2020 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:math'; - -import 'package:collection/collection.dart' show IterableExtension; -import 'package:flutter/material.dart'; -import 'package:intl/intl.dart'; - -import '../../analytics/analytics.dart' as ga; -import '../../analytics/constants.dart' as analytics_constants; -import '../../config_specific/logger/logger.dart' as logger; -import '../../primitives/auto_dispose_mixin.dart'; -import '../../primitives/utils.dart'; -import '../../shared/common_widgets.dart'; -import '../../shared/globals.dart'; -import '../../shared/split.dart'; -import '../../shared/table/table.dart'; -import '../../shared/table/table_data.dart'; -import '../../shared/theme.dart'; -import '../../shared/utils.dart'; -import '../../ui/search.dart'; -import '../../ui/tab.dart'; -import 'memory_analyzer.dart'; -import 'memory_controller.dart'; -import 'memory_filter.dart'; -import 'memory_graph_model.dart'; -import 'memory_heap_treemap.dart'; -import 'memory_instance_tree_view.dart'; -import 'memory_snapshot_models.dart'; -import 'panes/allocation_profile/allocation_profile_table_view.dart'; -import 'panes/allocation_tracing/allocation_profile_tracing_view.dart'; -import 'panes/diff/diff_pane.dart'; -import 'panes/leaks/leaks_pane.dart'; -import 'primitives/memory_utils.dart'; - -const memorySearchFieldKeyName = 'MemorySearchFieldKey'; - -@visibleForTesting -final memorySearchFieldKey = GlobalKey(debugLabel: memorySearchFieldKeyName); - -enum SnapshotStatus { - none, - streaming, - graphing, - grouping, - done, -} - -enum WildcardMatch { - exact, - startsWith, - endsWith, - contains, -} - -/// If no wildcard then exact match. -/// *NNN - ends with NNN -/// NNN* - starts with NNN -/// NNN*ZZZ - starts with NNN and ends with ZZZ -const knowClassesToAnalyzeForImages = >{ - // Anything that contains the phrase: - WildcardMatch.contains: [ - 'Image', - ], - - // Anything that starts with: - WildcardMatch.startsWith: [], - - // Anything that exactly matches: - WildcardMatch.exact: [ - '_Int32List', - // 32-bit devices e.g., emulators, Pixel 2, raw images as Int32List. - '_Int64List', - // 64-bit devices e.g., Pixel 3 XL, raw images as Int64List. - 'FrameInfos', - ], - - // Anything that ends with: - WildcardMatch.endsWith: [], -}; - -/// RegEx expressions to handle the WildcardMatches: -/// Ends with Image: \[_A-Za-z0-9_]*Image\$ -/// Starts with Image: ^Image -/// Contains Image: Image -/// Extact Image: ^Image$ -String buildRegExs(Map> matchingCriteria) { - final resultRegEx = StringBuffer(); - matchingCriteria.forEach((key, value) { - if (value.isNotEmpty) { - final name = value; - late String regEx; - // TODO(terry): Need to handle $ for identifier names e.g., - // $FOO is a valid identifier. - switch (key) { - case WildcardMatch.exact: - regEx = '^${name.join("\$|^")}\$'; - break; - case WildcardMatch.startsWith: - regEx = '^${name.join("|^")}'; - break; - case WildcardMatch.endsWith: - regEx = '^\[_A-Za-z0-9]*${name.join("\|[_A-Za-z0-9]*")}\$'; - break; - case WildcardMatch.contains: - regEx = '${name.join("|")}'; - break; - default: - assert(false, 'buildRegExs: Unhandled WildcardMatch'); - } - resultRegEx.write(resultRegEx.isEmpty ? '($regEx' : '|$regEx'); - } - }); - - resultRegEx.write(')'); - return resultRegEx.toString(); -} - -final String knownClassesRegExs = buildRegExs(knowClassesToAnalyzeForImages); - -@visibleForTesting -class MemoryScreenKeys { - static const searchButton = Key('Snapshot Search'); - static const filterButton = Key('Snapshot Filter'); - static const leaksTab = Key('Leaks Tab'); - static const dartHeapTableProfileTab = Key('Dart Heap Profile Tab'); - static const dartHeapAllocationTracingTab = - Key('Dart Heap Allocation Tracing Tab'); - static const diffTab = Key('Diff Tab'); -} - -class HeapTreeView extends StatefulWidget { - const HeapTreeView( - this.controller, - ); - - final MemoryController controller; - - @override - _HeapTreeViewState createState() => _HeapTreeViewState(); -} - -class _HeapTreeViewState extends State - with - AutoDisposeMixin, - ProvidedControllerMixin, - SearchFieldMixin, - TickerProviderStateMixin { - static const _gaPrefix = 'memoryTab'; - - late List _tabs; - late TabController _tabController; - late Set _searchableTabs; - final ValueNotifier _currentTab = ValueNotifier(0); - - Widget? snapshotDisplay; - - /// Used to detect a spike in memory usage. - MovingAverage heapMovingAverage = MovingAverage(averagePeriod: 100); - - /// Number of seconds between auto snapshots because RSS is exceeded. - static const maxRSSExceededDurationSecs = 30; - - static const minPercentIncrease = 30; - - /// Timestamp of HeapSample that caused auto snapshot. - int? spikeSnapshotTime; - - /// Timestamp when RSS exceeded auto snapshot. - int rssSnapshotTime = 0; - - /// Total memory that caused last snapshot. - int lastSnapshotMemoryTotal = 0; - - late bool treeMapVisible; - - late AnimationController _animation; - - @override - void initState() { - super.initState(); - - _animation = _setupBubbleAnimationController(); - } - - void _initTabs() { - _tabs = [ - DevToolsTab.create( - key: MemoryScreenKeys.dartHeapTableProfileTab, - tabName: 'Profile', - gaPrefix: _gaPrefix, - ), - DevToolsTab.create( - key: MemoryScreenKeys.dartHeapAllocationTracingTab, - tabName: 'Allocation Tracing', - gaPrefix: _gaPrefix, - ), - DevToolsTab.create( - key: MemoryScreenKeys.diffTab, - gaPrefix: _gaPrefix, - tabName: 'Diff', - ), - if (widget.controller.shouldShowLeaksTab.value) - DevToolsTab.create( - key: MemoryScreenKeys.leaksTab, - gaPrefix: _gaPrefix, - tabName: 'Leaks', - ), - ]; - - // TODO(polina-c): clean up analysis tab and search. - _searchableTabs = {}; - _tabController = TabController(length: _tabs.length, vsync: this); - _tabController.addListener(_onTabChanged); - } - - void _onTabChanged() => _currentTab.value = _tabController.index; - - @override - void didChangeDependencies() { - super.didChangeDependencies(); - if (!initController()) return; - - cancelListeners(); - - _initTabs(); - - addAutoDisposeListener(controller.shouldShowLeaksTab, () { - setState(() { - _initTabs(); - }); - }); - - addAutoDisposeListener(controller.selectedSnapshotNotifier, () { - setState(() { - controller.computeAllLibraries(rebuild: true); - }); - }); - - addAutoDisposeListener(controller.selectionSnapshotNotifier, () { - setState(() { - controller.computeAllLibraries(rebuild: true); - }); - }); - - addAutoDisposeListener(controller.filterNotifier, () { - setState(() { - controller.computeAllLibraries(rebuild: true); - }); - }); - - addAutoDisposeListener(controller.leafSelectedNotifier, () { - setState(() { - controller.computeRoot(); - }); - }); - - addAutoDisposeListener(controller.leafAnalysisSelectedNotifier, () { - setState(() { - controller.computeAnalysisInstanceRoot(); - }); - }); - - addAutoDisposeListener(controller.searchNotifier, () { - controller.closeAutoCompleteOverlay(); - controller..setCurrentHoveredIndexValue(0); - }); - - addAutoDisposeListener(controller.searchAutoCompleteNotifier, () { - controller.handleAutoCompleteOverlay( - context: context, - searchFieldKey: memorySearchFieldKey, - onTap: selectTheMatch, - ); - }); - - addAutoDisposeListener(controller.memoryTimeline.sampleAddedNotifier, () { - autoSnapshot(); - }); - - treeMapVisible = controller.treeMapVisible.value; - addAutoDisposeListener(controller.treeMapVisible, () { - setState(() { - treeMapVisible = controller.treeMapVisible.value; - }); - }); - } - - @override - void dispose() { - _animation.dispose(); - _tabController - ..removeListener(_onTabChanged) - ..dispose(); - - super.dispose(); - } - - /// Enable to output debugging information for auto-snapshot. - /// WARNING: Do not checkin with this flag set to true. - final debugSnapshots = false; - - /// Detect spike in memory usage if so do an automatic snapshot. - void autoSnapshot() { - if (!preferences.memory.autoSnapshotEnabled.value) return; - final heapSample = controller.memoryTimeline.sampleAddedNotifier.value!; - final heapSum = heapSample.external + heapSample.used; - heapMovingAverage.add(heapSum); - - final dateTimeFormat = DateFormat('HH:mm:ss.SSS'); - final startDateTime = dateTimeFormat - .format(DateTime.fromMillisecondsSinceEpoch(heapSample.timestamp)); - - if (debugSnapshots) { - debugLogger( - 'AutoSnapshot $startDateTime heapSum=$heapSum, ' - 'first=${heapMovingAverage.dataSet.first}, ' - 'mean=${heapMovingAverage.mean}', - ); - } - - bool takeSnapshot = false; - final sampleTime = Duration(milliseconds: heapSample.timestamp); - - final rssExceeded = heapSum > heapSample.rss; - if (rssExceeded) { - final increase = heapSum - lastSnapshotMemoryTotal; - final rssPercentIncrease = lastSnapshotMemoryTotal > 0 - ? increase / lastSnapshotMemoryTotal * 100 - : 0; - final rssTime = Duration(milliseconds: rssSnapshotTime); - // Number of seconds since last snapshot happens because of RSS exceeded. - // Reduce rapid fire snapshots. - final rssSnapshotPeriod = (sampleTime - rssTime).inSeconds; - if (rssSnapshotPeriod > maxRSSExceededDurationSecs) { - // minPercentIncrease larger than previous auto RSS snapshot then - // take another snapshot. - if (rssPercentIncrease > minPercentIncrease) { - rssSnapshotTime = heapSample.timestamp; - lastSnapshotMemoryTotal = heapSum; - - takeSnapshot = true; - debugLogger( - 'AutoSnapshot - RSS exceeded ' - '($rssPercentIncrease% increase) @ $startDateTime.', - ); - } - } - } - - if (!takeSnapshot && heapMovingAverage.hasSpike()) { - final snapshotTime = - Duration(milliseconds: spikeSnapshotTime ?? heapSample.timestamp); - spikeSnapshotTime = heapSample.timestamp; - takeSnapshot = true; - logger.log( - 'AutoSnapshot - memory spike @ $startDateTime} ' - 'last snapshot ${(sampleTime - snapshotTime).inSeconds} seconds ago.', - ); - debugLogger( - ' ' - 'heap @ last snapshot = $lastSnapshotMemoryTotal, ' - 'heap total=$heapSum, RSS=${heapSample.rss}', - ); - } - - if (takeSnapshot) { - assert(!heapMovingAverage.isDipping()); - // Reset moving average for next spike. - heapMovingAverage.clear(); - // TODO(terry): Should get the real sum of the snapshot not the current memory. - // Snapshot can take a bit and could be a lag. - lastSnapshotMemoryTotal = heapSum; - _takeHeapSnapshot(userGenerated: false); - } else if (heapMovingAverage.isDipping()) { - // Reset the two things we're tracking spikes and RSS exceeded. - heapMovingAverage.clear(); - lastSnapshotMemoryTotal = heapSum; - } - } - - SnapshotStatus snapshotState = SnapshotStatus.none; - - bool get _isSnapshotRunning => - snapshotState != SnapshotStatus.done && - snapshotState != SnapshotStatus.none; - - bool get _isSnapshotStreaming => snapshotState == SnapshotStatus.streaming; - - bool get _isSnapshotGraphing => snapshotState == SnapshotStatus.graphing; - - bool get _isSnapshotGrouping => snapshotState == SnapshotStatus.grouping; - - bool get _isSnapshotComplete => snapshotState == SnapshotStatus.done; - - @override - Widget build(BuildContext context) { - final themeData = Theme.of(context); - - if (_isSnapshotRunning) { - snapshotDisplay = Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const SizedBox(height: 50.0), - snapshotDisplay = const CircularProgressIndicator(), - const SizedBox(height: denseSpacing), - Text( - _isSnapshotStreaming - ? 'Processing...' - : _isSnapshotGraphing - ? 'Graphing...' - : _isSnapshotGrouping - ? 'Grouping...' - : _isSnapshotComplete - ? 'Done' - : '...', - ), - ], - ); - } else if (controller.snapshotByLibraryData != null) { - snapshotDisplay = - treeMapVisible ? const MemoryHeapTreemap() : MemoryHeapTable(); - } else { - snapshotDisplay = null; - } - - return Column( - children: [ - const SizedBox(height: defaultSpacing), - ValueListenableBuilder( - valueListenable: _currentTab, - builder: (context, index, _) => Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - TabBar( - labelColor: themeData.textTheme.bodyLarge!.color, - isScrollable: true, - controller: _tabController, - tabs: _tabs, - ), - if (_searchableTabs.contains(_tabs[index].key)) - _buildSearchFilterControls(), - ], - ), - ), - const Divider(), - Expanded( - child: TabBarView( - physics: defaultTabBarViewPhysics, - controller: _tabController, - children: [ - // Profile Tab - KeepAliveWrapper( - child: AllocationProfileTableView( - controller: controller.allocationProfileController, - ), - ), - const KeepAliveWrapper( - child: AllocationProfileTracingView(), - ), - // Diff tab. - KeepAliveWrapper( - child: DiffPane( - diffController: controller.diffPaneController, - ), - ), - // Leaks tab. - if (controller.shouldShowLeaksTab.value) - const KeepAliveWrapper(child: LeaksPane()), - ], - ), - ), - ], - ); - } - - Widget buildSnapshotTables(Widget? snapshotDisplay) { - if (snapshotDisplay == null) { - // Display help text about how to collect data. - return OutlineDecoration( - child: Center( - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: const [ - Text('Click the take heap snapshot button '), - Icon(Icons.camera), - Text(' to collect a graph of memory objects.'), - ], - ), - ), - ); - } - - final rightSideTable = controller.isLeafSelected - ? InstanceTreeView() - : controller.isAnalysisLeafSelected - ? Expanded(child: AnalysisInstanceViewTable()) - : helpScreen(); - - return treeMapVisible - ? snapshotDisplay - : Split( - initialFractions: const [0.5, 0.5], - minSizes: const [300, 300], - axis: Axis.horizontal, - children: [ - // TODO(terry): Need better focus handling between 2 tables & up/down - // arrows in the right-side field instance view table. - snapshotDisplay, - OutlineDecoration(child: rightSideTable), - ], - ); - } - - Widget tableExample(IconData? iconData, String entry) { - final themeData = Theme.of(context); - return Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - iconData == null - ? Text(' ', style: themeData.fixedFontStyle) - : Icon(iconData), - Text(entry, style: themeData.fixedFontStyle), - ], - ); - } - - Widget helpScreen() { - return Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text( - 'Click a leaf node instance of a class to\n' - 'inspect the fields of that instance e.g.,', - ), - const SizedBox(height: defaultSpacing), - tableExample(Icons.expand_more, 'dart:collection'), - tableExample(Icons.expand_more, 'SplayTreeMap'), - const SizedBox(height: denseRowSpacing), - tableExample(null, 'Instance 0'), - ], - ); - } - - @visibleForTesting - static const groupByMenuButtonKey = Key('Group By Menu Button'); - @visibleForTesting - static const groupByMenuItem = Key('Group By Menu Item'); - @visibleForTesting - static const groupByKey = Key('Group By'); - - Widget _groupByDropdown(TextTheme textTheme) { - final _groupByTypes = [ - MemoryController.groupByLibrary, - MemoryController.groupByClass, - MemoryController.groupByInstance, - ].map>( - ( - String value, - ) { - return DropdownMenuItem( - key: groupByMenuItem, - value: value, - child: Text( - 'Group by $value', - key: groupByKey, - ), - ); - }, - ).toList(); - - return DropdownButtonHideUnderline( - child: DropdownButton( - key: groupByMenuButtonKey, - style: textTheme.bodyMedium, - value: controller.groupingBy.value, - onChanged: (String? newValue) { - setState( - () { - controller.selectedLeaf = null; - controller.groupingBy.value = newValue!; - if (controller.snapshots.isNotEmpty) { - unawaited(doGroupBy()); - } - }, - ); - }, - items: _groupByTypes, - ), - ); - } - - // TODO(polina-c): cleanup unused code. - // ignore: unused_element - Widget _buildSnapshotControls(TextTheme textTheme) { - return SizedBox( - height: defaultButtonHeight, - child: Row( - children: [ - IconLabelButton( - tooltip: 'Take a memory profile snapshot', - icon: Icons.camera, - label: 'Take Heap Snapshot', - onPressed: _isSnapshotRunning ? null : _takeHeapSnapshot, - ), - const SizedBox(width: defaultSpacing), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Text('Treemap'), - Switch( - value: treeMapVisible, - onChanged: controller.snapshotByLibraryData != null - ? (value) { - controller.toggleTreeMapVisible(value); - } - : null, - ), - ], - ), - if (!treeMapVisible) ...[ - const SizedBox(width: defaultSpacing), - _groupByDropdown(textTheme), - const SizedBox(width: defaultSpacing), - // TODO(terry): Mechanism to handle expand/collapse on both tables - // objects/fields. Maybe notion in table? - ExpandAllButton( - onPressed: () { - ga.select( - analytics_constants.memory, - analytics_constants.expandAll, - ); - if (snapshotDisplay is MemoryHeapTable) { - controller.groupByTreeTable.dataRoots.every((element) { - element.expandCascading(); - return true; - }); - } - // All nodes expanded - signal tree state changed. - controller.treeChanged(); - }, - ), - const SizedBox(width: denseSpacing), - CollapseAllButton( - onPressed: () { - ga.select( - analytics_constants.memory, - analytics_constants.collapseAll, - ); - if (snapshotDisplay is MemoryHeapTable) { - controller.groupByTreeTable.dataRoots.every((element) { - element.collapseCascading(); - return true; - }); - if (controller.instanceFieldsTreeTable != null) { - // We're collapsing close the fields table. - controller.selectedLeaf = null; - } - // All nodes collapsed - signal tree state changed. - controller.treeChanged(); - } - }, - ), - ], - ], - ), - ); - } - - AnimationController _setupBubbleAnimationController() { - // Setup animation controller to handle the update bubble. - const animationDuration = Duration(milliseconds: 500); - final bubbleAnimation = AnimationController( - duration: animationDuration, - reverseDuration: animationDuration, - upperBound: 15.0, - vsync: this, - ); - - bubbleAnimation.addStatusListener(_animationStatusListener); - - return bubbleAnimation; - } - - void _animationStatusListener(AnimationStatus status) { - if (status == AnimationStatus.completed) { - // Reverse from larger bubble back to normal bubble radius. - _animation.reverse(); - } - } - - Timer? removeUpdateBubble; - - Widget textWidgetWithUpdateCircle( - String text, { - TextStyle? style, - double? size, - }) { - final textWidth = textWidgetWidth(text, style: style); - - return Stack( - children: [ - Positioned( - child: Container( - width: textWidth + 10, - child: Text(text, style: style), - ), - ), - Positioned( - right: 0, - child: Container( - alignment: Alignment.topRight, - width: size, - height: size, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: Colors.blue[400], - ), - child: const Icon(Icons.fiber_manual_record, size: 0), - ), - ), - ], - ); - } - - static const maxWidth = 800.0; - - double textWidgetWidth(String message, {TextStyle? style}) { - // Longest message must fit in this width. - const constraints = BoxConstraints( - maxWidth: maxWidth, - ); - - // TODO(terry): Is there a better (less heavyweight) way of computing text - // width than using the widget pipeline? - final richTextWidget = Text.rich(TextSpan(text: message), style: style) - .build(context) as RichText; - final renderObject = richTextWidget.createRenderObject(context); - renderObject.layout(constraints); - final boxes = renderObject.getBoxesForSelection( - TextSelection( - baseOffset: 0, - extentOffset: TextSpan(text: message).toPlainText().length, - ), - ); - - final textWidth = boxes.last.right; - - if (textWidth > maxWidth) { - // TODO(terry): If message > 800 pixels in width (not possible - // today) but could be more robust. - logger.log( - 'Computed text width > $maxWidth ($textWidth)\nmessage=$message.', - logger.LogLevel.warning, - ); - } - - return textWidth; - } - - /// Match, found, select it and process via ValueNotifiers. - void selectTheMatch(String foundName) { - setState(() { - if (snapshotDisplay is MemoryHeapTable) { - controller.groupByTreeTable.dataRoots.every((element) { - element.collapseCascading(); - return true; - }); - } - }); - - selectFromSearchField(controller, foundName); - clearSearchField(controller); - } - - Widget _buildSearchWidget(GlobalKey> key) => Container( - width: wideSearchTextWidth, - height: defaultTextFieldHeight, - child: buildAutoCompleteSearchField( - controller: controller, - searchFieldKey: key, - searchFieldEnabled: !treeMapVisible, - shouldRequestFocus: !treeMapVisible, - onSelection: selectTheMatch, - supportClearField: true, - ), - ); - - Widget _buildSearchFilterControls() => Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.end, - children: [ - _buildSearchWidget(memorySearchFieldKey), - const SizedBox(width: denseSpacing), - FilterButton( - key: MemoryScreenKeys.filterButton, - onPressed: _filter, - // TODO(kenz): implement isFilterActive - isFilterActive: false, - ), - ], - ); - - // TODO: Much of the logic for _takeHeapSnapshot() might want to move into the - // controller. - void _takeHeapSnapshot({bool userGenerated = true}) async { - // VmService not available (disconnected/crashed). - if (serviceManager.service == null) return; - - // Another snapshot in progress, don't stall the world. An auto-snapshot - // is probably in progress. - if (snapshotState != SnapshotStatus.none && - snapshotState != SnapshotStatus.done) { - debugLogger('Snapshot in progress - ignoring this request.'); - return; - } - - controller.memoryTimeline.addSnapshotEvent(auto: !userGenerated); - - setState(() { - snapshotState = SnapshotStatus.streaming; - }); - - final snapshotTimestamp = DateTime.now(); - - final graph = await snapshotMemory(); - - // No snapshot collected, disconnected/crash application. - if (graph == null) { - setState(() { - snapshotState = SnapshotStatus.done; - }); - controller.selectedSnapshotTimestamp = DateTime.now(); - return; - } - - final snapshotCollectionTime = DateTime.now(); - - setState(() { - snapshotState = SnapshotStatus.graphing; - }); - - // To debug particular classes add their names to the last - // parameter classNamesToMonitor e.g., ['AppStateModel', 'Terry', 'TerryEntry'] - controller.heapGraph = convertHeapGraph( - controller.filterConfig, - graph, - [], - ); - final snapshotGraphTime = DateTime.now(); - - setState(() { - snapshotState = SnapshotStatus.grouping; - }); - - await doGroupBy(); - - final root = controller.computeAllLibraries(graph: graph)!; - - final snapshot = controller.storeSnapshot( - snapshotTimestamp, - graph, - root, - autoSnapshot: !userGenerated, - ); - - final snapshotDoneTime = DateTime.now(); - - controller.selectedSnapshotTimestamp = snapshotTimestamp; - - debugLogger( - 'Total Snapshot completed in' - ' ${snapshotDoneTime.difference(snapshotTimestamp).inMilliseconds / 1000} seconds', - ); - debugLogger( - ' Snapshot collected in' - ' ${snapshotCollectionTime.difference(snapshotTimestamp).inMilliseconds / 1000} seconds', - ); - debugLogger( - ' Snapshot graph built in' - ' ${snapshotGraphTime.difference(snapshotCollectionTime).inMilliseconds / 1000} seconds', - ); - debugLogger( - ' Snapshot grouping/libraries computed in' - ' ${snapshotDoneTime.difference(snapshotGraphTime).inMilliseconds / 1000} seconds', - ); - - setState(() { - snapshotState = SnapshotStatus.done; - }); - - controller.buildTreeFromAllData(); - _analyze(snapshot: snapshot); - } - - Future doGroupBy() async { - controller.heapGraph! - ..computeInstancesForClasses() - ..computeRawGroups() - ..computeFilteredGroups(); - } - - void _filter() { - // TODO(terry): Remove barrierDismissble and make clicking outside - // dialog same as cancel. - // Dialog isn't dismissed by clicking outside the dialog (modal). - // Pressing either the Apply or Cancel button will dismiss. - unawaited( - showDialog( - context: context, - builder: (BuildContext context) => SnapshotFilterDialog(controller), - barrierDismissible: false, - ), - ); - } - - void _debugCheckAnalyses(DateTime currentSnapDateTime) { - // Debug only check. - assert( - () { - // Analysis already completed we're done. - final foundMatch = controller.completedAnalyses.firstWhereOrNull( - (element) => element.dateTime.compareTo(currentSnapDateTime) == 0, - ); - if (foundMatch != null) { - logger.log( - 'Analysis ' - '${MemoryController.formattedTimestamp(currentSnapDateTime)} ' - 'already computed.', - logger.LogLevel.warning, - ); - } - return true; - }(), - ); - } - - void _analyze({Snapshot? snapshot}) { - final AnalysesReference analysesNode = controller.findAnalysesNode()!; - - snapshot ??= controller.computeSnapshotToAnalyze!; - final DateTime currentSnapDateTime = snapshot.collectedTimestamp; - - _debugCheckAnalyses(currentSnapDateTime); - - // If there's an empty place holder than remove it. First analysis will - // exist shortly. - if (analysesNode.children.length == 1 && - analysesNode.children.first.name!.isEmpty) { - analysesNode.children.clear(); - } - - // Create analysis node to hold analysis results. - final analyzeSnapshot = AnalysisSnapshotReference(currentSnapDateTime); - analysesNode.addChild(analyzeSnapshot); - - // Analyze this snapshot. - final collectedData = collect(controller, snapshot); - - // Analyze the collected data. - - // 1. Analysis of memory image usage. - imageAnalysis(controller, analyzeSnapshot, collectedData); - - // Add to our list of completed analyses. - controller.completedAnalyses.add(analyzeSnapshot); - - // Expand the 'Analysis' node. - if (!analysesNode.isExpanded) { - analysesNode.expand(); - } - - // Select the snapshot just analyzed. - controller.selectionSnapshotNotifier.value = Selection( - node: analyzeSnapshot, - nodeIndex: analyzeSnapshot.index, - scrollIntoView: true, - ); - } -} - -/// Snapshot TreeTable -class MemoryHeapTable extends StatefulWidget { - @override - MemoryHeapTableState createState() => MemoryHeapTableState(); -} - -/// A table of the Memory graph class top-down call tree. -class MemoryHeapTableState extends State - with - AutoDisposeMixin, - ProvidedControllerMixin { - final TreeColumnData _treeColumn = _LibraryRefColumn(); - - final List> _columns = []; - - @override - void initState() { - super.initState(); - - // Setup the table columns. - _columns.addAll([ - _treeColumn, - _ClassOrInstanceCountColumn(), - _ShallowSizeColumn(), - // TODO(terry): Don't display until dominator is implemented - // Issue https://github.com/flutter/devtools/issues/2688 - // _RetainedSizeColumn(), - ]); - } - - @override - void didChangeDependencies() { - super.didChangeDependencies(); - if (!initController()) return; - - cancelListeners(); - - // Update the tree when the tree state changes e.g., expand, collapse, etc. - addAutoDisposeListener(controller.treeChangedNotifier, () { - if (controller.isTreeChanged) { - setState(() {}); - } - }); - - // Update the tree when the memorySource changes. - addAutoDisposeListener(controller.selectedSnapshotNotifier, () { - setState(() { - controller.computeAllLibraries(rebuild: true); - }); - }); - - addAutoDisposeListener(controller.filterNotifier, () { - setState(() { - controller.computeAllLibraries(rebuild: true); - }); - }); - - addAutoDisposeListener(controller.searchAutoCompleteNotifier); - - addAutoDisposeListener(controller.selectTheSearchNotifier, _handleSearch); - - addAutoDisposeListener(controller.searchNotifier, _handleSearch); - } - - void _handleSearch() { - final searchingValue = controller.search; - if (searchingValue.isNotEmpty) { - if (controller.selectTheSearch) { - // Found an exact match. - selectItemInTree(searchingValue); - controller.selectTheSearch = false; - controller.resetSearch(); - return; - } - - // No exact match, return the list of possible matches. - controller.clearSearchAutoComplete(); - - final matches = _snapshotMatches(searchingValue); - - // Remove duplicates and sort the matches. - final normalizedMatches = matches.toSet().toList()..sort(); - // Use the top 10 matches: - controller.searchAutoComplete.value = normalizedMatches - .sublist( - 0, - min( - topMatchesLimit, - normalizedMatches.length, - ), - ) - .map((match) => AutoCompleteMatch(match)) - .toList(); - } - } - - List _snapshotMatches(String searchingValue) { - final matches = []; - - final externalMatches = []; - final filteredMatches = []; - - switch (controller.groupingBy.value) { - case MemoryController.groupByLibrary: - final searchRoot = controller.activeSnapshot; - for (final reference in searchRoot.children) { - if (reference.isLibrary) { - matches.addAll( - matchesInLibrary(reference as LibraryReference, searchingValue), - ); - } else if (reference.isExternals) { - final refs = reference as ExternalReferences; - for (final ext in refs.children.cast()) { - final match = matchSearch(ext, searchingValue); - if (match != null) { - externalMatches.add(match); - } - } - } else if (reference.isFiltered) { - // Matches in the filtered nodes. - final filteredReference = reference as FilteredReference; - for (final library in filteredReference.children) { - filteredMatches.addAll( - matchesInLibrary( - library as LibraryReference, - searchingValue, - ), - ); - } - } - } - break; - case MemoryController.groupByClass: - matches.addAll( - matchClasses( - controller.groupByTreeTable.dataRoots, - searchingValue, - ), - ); - break; - case MemoryController.groupByInstance: - // TODO(terry): TBD - break; - } - - // Ordered in importance (matches, external, filtered). - matches.addAll(externalMatches); - matches.addAll(filteredMatches); - return matches; - } - - List _maybeAddMatch(Reference reference, String search) { - final matches = []; - - final match = matchSearch(reference, search); - if (match != null) { - matches.add(match); - } - - return matches; - } - - List matchesInLibrary( - LibraryReference libraryReference, - String searchingValue, - ) { - final matches = _maybeAddMatch(libraryReference, searchingValue); - - final List classes = libraryReference.children; - matches.addAll(matchClasses(classes, searchingValue)); - - return matches; - } - - List matchClasses( - List classReferences, - String searchingValue, - ) { - final matches = []; - - // Check the class names in the library - for (final ClassReference classReference - in classReferences.cast()) { - matches.addAll(_maybeAddMatch(classReference, searchingValue)); - } - - // Remove duplicates - return matches; - } - - /// Return null if no match, otherwise string. - String? matchSearch(Reference ref, String matchString) { - final knownName = ref.name!.toLowerCase(); - if (knownName.contains(matchString.toLowerCase())) { - return ref.name; - } - return null; - } - - /// This finds and selects an exact match in the tree. - /// Returns `true` if [searchingValue] is found in the tree. - bool selectItemInTree(String searchingValue) { - // Search the snapshots. - switch (controller.groupingBy.value) { - case MemoryController.groupByLibrary: - final searchRoot = controller.activeSnapshot; - if (controller.selectionSnapshotNotifier.value.node == null) { - // No selected node, then select the snapshot we're searching. - controller.selectionSnapshotNotifier.value = Selection( - node: searchRoot, - nodeIndex: searchRoot.index, - scrollIntoView: true, - ); - } - for (final reference in searchRoot.children) { - if (reference.isLibrary) { - final foundIt = _selectItemInTree(reference, searchingValue); - if (foundIt) { - return true; - } - } else if (reference.isFiltered) { - // Matches in the filtered nodes. - final filteredReference = reference as FilteredReference; - for (final library in filteredReference.children) { - final foundIt = _selectItemInTree(library, searchingValue); - if (foundIt) { - return true; - } - } - } else if (reference.isExternals) { - final refs = reference as ExternalReferences; - for (final external in refs.children.cast()) { - final foundIt = _selectItemInTree(external, searchingValue); - if (foundIt) { - return true; - } - } - } - } - break; - case MemoryController.groupByClass: - for (final reference in controller.groupByTreeTable.dataRoots) { - if (reference.isClass) { - return _selecteClassInTree(reference, searchingValue); - } - } - break; - case MemoryController.groupByInstance: - // TODO(terry): TBD - break; - } - - return false; - } - - bool _selectInTree(Reference reference, search) { - if (reference.name == search) { - controller.selectionSnapshotNotifier.value = Selection( - node: reference, - nodeIndex: reference.index, - scrollIntoView: true, - ); - controller.clearSearchAutoComplete(); - return true; - } - return false; - } - - bool _selectItemInTree(Reference reference, String searchingValue) { - // TODO(terry): Only finds first one. - if (_selectInTree(reference, searchingValue)) return true; - - // Check the class names in the library - return _selecteClassInTree(reference, searchingValue); - } - - bool _selecteClassInTree(Reference reference, String searchingValue) { - // Check the class names in the library - for (final Reference classReference in reference.children) { - // TODO(terry): Only finds first one. - if (_selectInTree(classReference, searchingValue)) return true; - } - - return false; - } - - @override - Widget build(BuildContext context) { - final root = controller.buildTreeFromAllData(); - Widget result; - if (root != null && root.children.isNotEmpty) { - // Snapshots and analyses exists display the trees. - controller.groupByTreeTable = TreeTable( - keyFactory: (libRef) => PageStorageKey(libRef.name), - dataRoots: root.children, - dataKey: 'memory-snapshot-tree', - columns: _columns, - treeColumn: _treeColumn, - defaultSortColumn: _columns[0], - defaultSortDirection: SortDirection.ascending, - selectionNotifier: controller.selectionSnapshotNotifier, - ); - - result = controller.groupByTreeTable; - } else { - // Nothing collected yet (snapshots/analyses) - return an empty area. - result = const SizedBox(); - } - return OutlineDecoration(child: result); - } -} - -class _LibraryRefColumn extends TreeColumnData { - _LibraryRefColumn() : super('Library or Class'); - - @override - String getValue(Reference dataObject) { - // Should never be empty when we're displaying in table. - assert(!dataObject.isEmptyReference); - - if (dataObject.isLibrary) { - final value = (dataObject as LibraryReference).name ?? ''; - final splitValues = value.split('/'); - - return splitValues.length > 1 - ? '${splitValues[0]}/${splitValues[1]}' - : splitValues[0]; - } - - return dataObject.name ?? ''; - } - - @override - String getDisplayValue(Reference dataObject) => getValue(dataObject); - - @override - bool get supportsSorting => true; - - @override - String getTooltip(Reference dataObject) { - return dataObject.name ?? ''; - } -} - -class _ClassOrInstanceCountColumn extends ColumnData { - _ClassOrInstanceCountColumn() - : super( - 'Count', - alignment: ColumnAlignment.right, - fixedWidthPx: scaleByFontFactor(75.0), - ); - - @override - int getValue(Reference dataObject) { - assert(!dataObject.isEmptyReference); - - // Although the method returns dynamic this implementatation only - // returns int. - - if (dataObject.name == MemoryController.libraryRootNode || - dataObject.name == MemoryController.classRootNode) return 0; - - if (dataObject.isExternal) { - return dataObject.children.length; - } - - if (dataObject.isAnalysis && dataObject is AnalysisReference) { - final AnalysisReference analysisReference = dataObject; - return analysisReference.countNote ?? 0; - } - - if (dataObject.isSnapshot && dataObject is SnapshotReference) { - int count = 0; - final SnapshotReference snapshotRef = dataObject; - for (final child in snapshotRef.children) { - count += _computeCount(child); - } - return count; - } - - return _computeCount(dataObject); - } - - /// Return a count based on the Reference type e.g., library, filtered, - /// class, externals, etc. Only compute once, store in the Reference. - int _computeCount(Reference ref) { - // Only compute the children counts once then store in the Reference. - if (ref.hasCount) return ref.count!; - - int count = 0; - - if (ref.isClass) { - final classRef = ref as ClassReference; - count = _computeClassInstances(classRef.actualClass!)!; - } else if (ref.isLibrary) { - // Return number of classes. - final LibraryReference libraryReference = ref as LibraryReference; - for (final heapClass in libraryReference.actualClasses!) { - count += _computeClassInstances(heapClass)!; - } - } else if (ref.isFiltered) { - final FilteredReference filteredRef = ref as FilteredReference; - for (final LibraryReference child - in filteredRef.children.cast()) { - for (final heapClass in child.actualClasses!) { - count += _computeClassInstances(heapClass)!; - } - } - } else if (ref.isExternals) { - for (ExternalReference externalRef - in ref.children.cast()) { - count += externalRef.children.length; - } - } else if (ref.isExternal) { - final ExternalReference externalRef = ref as ExternalReference; - count = externalRef.children.length; - } - - // Only compute once. - ref.count = count; - - return count; - } - - int? _computeClassInstances(HeapGraphClassLive liveClass) => - liveClass.instancesCount != null ? liveClass.instancesCount : null; - - /// Internal helper for all count values. - String _displayCount(int? count) => count == null ? '--' : '$count'; - - @override - String getDisplayValue(Reference dataObject) => - _displayCount(getValue(dataObject)); - - @override - bool get supportsSorting => true; - - @override - int compare(Reference a, Reference b) { - // Analysis is always before. - final Comparable valueA = a.isAnalysis ? 0 : getValue(a); - final Comparable valueB = b.isAnalysis ? 0 : getValue(b); - return valueA.compareTo(valueB); - } -} - -class _ShallowSizeColumn extends ColumnData { - _ShallowSizeColumn() - : super( - 'Shallow', - alignment: ColumnAlignment.right, - fixedWidthPx: scaleByFontFactor(100.0), - ); - - int sizeAllVisibleLibraries(List references) { - var sum = 0; - for (final ref in references) { - if (ref.isLibrary) { - final libraryReference = ref as LibraryReference; - for (final actualClass in libraryReference.actualClasses!) { - sum += actualClass.instancesTotalShallowSizes; - } - } - } - return sum; - } - - @override - dynamic getValue(Reference dataObject) { - // Should never be empty when we're displaying in table. - assert(!dataObject.isEmptyReference); - - if (dataObject.name == MemoryController.libraryRootNode || - dataObject.name == MemoryController.classRootNode) { - final snapshot = dataObject.controller!.getSnapshot(dataObject)!; - final snapshotGraph = snapshot.snapshotGraph; - return snapshotGraph.shallowSize + snapshotGraph.externalSize; - } - - if (dataObject.isAnalysis && dataObject is AnalysisReference) { - final AnalysisReference analysisReference = dataObject; - final size = analysisReference.sizeNote; - return size ?? ''; - } else if (dataObject.isSnapshot && dataObject is SnapshotReference) { - var sum = 0; - final SnapshotReference snapshotRef = dataObject; - for (final childRef in snapshotRef.children) { - sum += _sumShallowSize(childRef) ?? 0; - } - return sum; - } else { - final sum = _sumShallowSize(dataObject); - return sum ?? ''; - } - } - - int? _sumShallowSize(Reference ref) { - if (ref.isLibrary) { - // Return number of classes. - final libraryReference = ref as LibraryReference; - var sum = 0; - for (final actualClass in libraryReference.actualClasses!) { - sum += actualClass.instancesTotalShallowSizes; - } - return sum; - } else if (ref.isClass) { - final classReference = ref as ClassReference; - return classReference.actualClass!.instancesTotalShallowSizes; - } else if (ref.isObject) { - // Return number of instances. - final objectReference = ref as ObjectReference; - - var size = objectReference.instance.origin.shallowSize; - - // If it's an external object then return the externalSize too. - if (ref is ExternalObjectReference) { - final ExternalObjectReference externalRef = ref; - size += externalRef.externalSize; - } - - return size; - } else if (ref.isFiltered) { - final snapshot = ref.controller!.getSnapshot(ref)!; - final sum = sizeAllVisibleLibraries(snapshot.libraryRoot?.children ?? []); - final snapshotGraph = snapshot.snapshotGraph; - return snapshotGraph.shallowSize - sum; - } else if (ref.isExternals) { - final snapshot = ref.controller!.getSnapshot(ref)!; - return snapshot.snapshotGraph.externalSize; - } else if (ref.isExternal) { - return (ref as ExternalReference).sumExternalSizes; - } - - return null; - } - - @override - String getDisplayValue(Reference dataObject) { - final value = getValue(dataObject); - - // TODO(terry): Add percent to display too. -/* - final total = dataObject.controller.snapshots.last.snapshotGraph.capacity; - final percentage = (value / total) * 100; - final displayPercentage = percentage < .050 ? '<<1%' - : '${NumberFormat.compact().format(percentage)}%'; - print('$displayPercentage [${NumberFormat.compact().format(percentage)}%]'); -*/ - if ((dataObject.isAnalysis || - dataObject.isAllocations || - dataObject.isAllocation) && - value is! int) return ''; - - return prettyPrintBytes( - value as int, - kbFractionDigits: 1, - includeUnit: true, - )!; - } - - @override - bool get supportsSorting => true; - - @override - int compare(Reference a, Reference b) { - // Analysis is always before. - final Comparable valueA = a.isAnalysis ? 0 : getValue(a); - final Comparable valueB = b.isAnalysis ? 0 : getValue(b); - return valueA.compareTo(valueB); - } -} - -// TODO(terry): Remove ignore when dominator is implemented. -// ignore: unused_element -class _RetainedSizeColumn extends ColumnData { - _RetainedSizeColumn() - : super( - 'Retained', - alignment: ColumnAlignment.right, - fixedWidthPx: scaleByFontFactor(100.0), - ); - - @override - dynamic getValue(Reference dataObject) { - // Should never be empty when we're displaying in table. - assert(!dataObject.isEmptyReference); - - if (dataObject.name == MemoryController.libraryRootNode || - dataObject.name == MemoryController.classRootNode) return ''; - - if (dataObject.isLibrary) { - // Return number of classes. - final libraryReference = dataObject as LibraryReference; - var sum = 0; - for (final actualClass in libraryReference.actualClasses!) { - sum += actualClass.instancesTotalShallowSizes; - } - return sum; - } else if (dataObject.isClass) { - final classReference = dataObject as ClassReference; - return classReference.actualClass!.instancesTotalShallowSizes; - } else if (dataObject.isObject) { - // Return number of instances. - final objectReference = dataObject as ObjectReference; - return objectReference.instance.origin.shallowSize; - } else if (dataObject.isExternals) { - return dataObject.controller!.lastSnapshot!.snapshotGraph.externalSize; - } else if (dataObject.isExternal) { - return (dataObject as ExternalReference) - .liveExternal - .externalProperty - .externalSize; - } - - return ''; - } - - @override - String getDisplayValue(Reference dataObject) { - final value = getValue(dataObject); - return '$value'; - } - - @override - bool get supportsSorting => true; - - @override - int compare(Reference a, Reference b) { - // Analysis is always before. - final Comparable valueA = a.isAnalysis ? 0 : getValue(a); - final Comparable valueB = b.isAnalysis ? 0 : getValue(b); - return valueA.compareTo(valueB); - } -} diff --git a/packages/devtools_app/lib/src/screens/memory/memory_heap_treemap.dart b/packages/devtools_app/lib/src/screens/memory/memory_heap_treemap.dart deleted file mode 100644 index 88517d78cc9..00000000000 --- a/packages/devtools_app/lib/src/screens/memory/memory_heap_treemap.dart +++ /dev/null @@ -1,263 +0,0 @@ -// Copyright 2020 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 'package:flutter/material.dart' hide TextStyle; -import 'package:flutter/widgets.dart' hide TextStyle; - -import '../../charts/treemap.dart'; -import '../../primitives/auto_dispose_mixin.dart'; -import '../../shared/common_widgets.dart'; -import '../../shared/theme.dart'; -import '../../shared/utils.dart'; -import 'memory_controller.dart'; -import 'primitives/predefined_classes.dart'; - -class MemoryHeapTreemap extends StatefulWidget { - const MemoryHeapTreemap(); - - @override - MemoryHeapTreemapState createState() => MemoryHeapTreemapState(); -} - -class MemoryHeapTreemapState extends State - with - AutoDisposeMixin, - ProvidedControllerMixin { - InstructionsSize? _sizes; - - TreemapNode? root; - - @override - void didChangeDependencies() { - super.didChangeDependencies(); - - // TODO(terry): Unable to short-circuit need to investigate why? - if (!initController()) return; - - if (controller.heapGraph != null) { - _sizes = InstructionsSize.fromSnapshot(controller); - root = _sizes!.root; - } - - cancelListeners(); - - addAutoDisposeListener(controller.selectedSnapshotNotifier, () { - setState(() { - controller.computeAllLibraries(rebuild: true); - - _sizes = InstructionsSize.fromSnapshot(controller); - }); - }); - - // TODO(peterdjlee): Implement search and filter functionality for memory treemap. - // addAutoDisposeListener(controller.filterNotifier, () { - // setState(() { - // controller.computeAllLibraries(rebuild: true); - // }); - // }); - // addAutoDisposeListener(controller.selectTheSearchNotifier, () { - // setState(() { - // if (_trySelectItem()) { - // closeAutoCompleteOverlay(); - // } - // }); - // }); - - // addAutoDisposeListener(controller.searchNotifier, () { - // setState(() { - // if (_trySelectItem()) { - // closeAutoCompleteOverlay(); - // } - // }); - // }); - - // addAutoDisposeListener(controller.searchAutoCompleteNotifier, () { - // setState(autoCompleteOverlaySetState(controller, context)); - // }); - } - - void _onRootChanged(TreemapNode? newRoot) { - setState(() { - root = newRoot; - }); - } - - @override - Widget build(BuildContext context) { - Widget result; - if (_sizes == null) { - result = Column( - children: [ - const SizedBox(height: denseRowSpacing), - Expanded( - child: OutlineDecoration( - child: Row(children: const [SizedBox()]), - ), - ), - ], - ); - } else { - result = Padding( - padding: const EdgeInsets.only(top: denseRowSpacing), - child: LayoutBuilder( - builder: (context, constraints) { - return Treemap.fromRoot( - rootNode: root, - levelsVisible: 2, - isOutermostLevel: true, - width: constraints.maxWidth, - height: constraints.maxHeight, - onRootChangedCallback: _onRootChanged, - ); - }, - ), - ); - } - return OutlineDecoration(child: result); - } -} - -/// Definitions of exposed callback methods stored in callback Map the key -/// is the function name (String) and the value a callback function signature. - -/// matchNames callback name. -const matchNamesKey = 'matchNames'; - -/// matchNames callback signature. -typedef MatchNamesFunction = List Function(String); - -/// findNode callback name. -const findNodeKey = 'findNode'; - -/// findNode callback signature. -typedef FindNodeFunction = TreemapNode Function(String); - -/// selectNode callback name. -const selectNodeKey = 'selectNode'; - -/// selectNode callback signature. -typedef SelectNodeFunction = void Function(TreemapNode); - -class InstructionsSize { - const InstructionsSize(this.root); - - factory InstructionsSize.fromSnapshot(MemoryController controller) { - final rootChildren = {}; - final root = TreemapNode( - name: 'root', - childrenMap: rootChildren, - ); - TreemapNode? currentParent = root; - - // TODO(terry): Should treemap be all memory or just the filtered group? - // Using rawGroup not graph.groupByLibrary. - - (controller.heapGraph?.rawGroupByLibrary ?? {}).forEach( - (libraryGroup, value) { - final classes = value; - for (final theClass in classes) { - final shallowSize = theClass.instancesTotalShallowSizes; - var className = theClass.name; - if (shallowSize == 0 || className == '::') { - continue; - } - - // Ensure the empty library name is our group name e.g., '' -> 'src'. - String libraryName = theClass.libraryUri.toString(); - if (libraryName.isEmpty) { - libraryName = libraryGroup; - } - - // Map class names to familar user names. - final predefined = - predefinedClasses[LibraryClass(libraryName, className)]; - if (predefined != null) { - className = predefined.prettyName; - } - - final symbol = Symbol( - name: 'new $className', - size: shallowSize, - libraryUri: libraryName, - className: className, - ); - - Map currentChildren = rootChildren; - final parentReset = currentParent; - for (String pathPart in symbol.parts) { - currentChildren.putIfAbsent( - pathPart, - () { - final node = TreemapNode( - name: pathPart, - childrenMap: {}, - ); - currentParent!.addChild(node); - return node; - }, - ); - currentChildren[pathPart]!.byteSize += symbol.size; - currentParent = currentChildren[pathPart]; - currentChildren = currentChildren[pathPart]!.childrenMap; - } - currentParent = parentReset; - } - }, - ); - - // Get sum of children's sizes. - root.byteSize = root.childrenMap.values - .fold(0, (int current, TreemapNode node) => current + node.byteSize); - - final snapshotGraph = controller.snapshots.last.snapshotGraph; - // Add the external heap to the treemap. - root.childrenMap.putIfAbsent('External Heap', () { - final node = TreemapNode( - name: 'External Heap', - childrenMap: {}, - )..byteSize = snapshotGraph.externalSize; - root.addChild(node); - return node; - }); - - // TODO(peterdjlee): Add the filtered libraries/classes to the treemap. - - root.byteSize = snapshotGraph.shallowSize + snapshotGraph.externalSize; - - return InstructionsSize(root); - } - - final TreemapNode root; -} - -class Symbol { - const Symbol({ - required this.name, - required this.size, - this.libraryUri, - this.className, - }); - - static Symbol fromMap(Map json) { - return Symbol( - name: json['n'] as String, - size: json['s'] as int, - className: json['c'] as String?, - libraryUri: json['l'] as String?, - ); - } - - final String name; - final int size; - final String? libraryUri; - final String? className; - - List get parts { - return [ - if (libraryUri != null) ...libraryUri!.split('/') else '@stubs', - if (className?.isNotEmpty ?? false) className!, - name, - ]; - } -} diff --git a/packages/devtools_app/lib/src/screens/memory/memory_instance_tree_view.dart b/packages/devtools_app/lib/src/screens/memory/memory_instance_tree_view.dart deleted file mode 100644 index 44493dcbee3..00000000000 --- a/packages/devtools_app/lib/src/screens/memory/memory_instance_tree_view.dart +++ /dev/null @@ -1,171 +0,0 @@ -// Copyright 2020 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 'package:flutter/material.dart'; - -import '../../primitives/auto_dispose_mixin.dart'; -import '../../primitives/utils.dart'; -import '../../shared/table/table.dart'; -import '../../shared/table/table_data.dart'; -import '../../shared/utils.dart'; -import 'memory_controller.dart'; -import 'memory_snapshot_models.dart'; - -class InstanceTreeView extends StatefulWidget { - @override - InstanceTreeViewState createState() => InstanceTreeViewState(); -} - -/// Table of the fields of an instance (type, name and value). -class InstanceTreeViewState extends State - with - AutoDisposeMixin, - ProvidedControllerMixin { - final TreeColumnData treeColumn = _FieldTypeColumn(); - - final List> columns = []; - - @override - void initState() { - super.initState(); - - // Setup table column names for instance viewer. - columns.addAll([ - treeColumn, - _FieldNameColumn(), - _FieldValueColumn(), - ]); - } - - @override - void didChangeDependencies() { - super.didChangeDependencies(); - if (!initController()) return; - - cancelListeners(); - - // TODO(terry): setState should be called to set our state not change the - // controller. Have other ValueListenables on controller to - // listen to, so we don't need the setState calls. - // Update the chart when the memorySource changes. - addAutoDisposeListener(controller.selectedSnapshotNotifier, () { - setState(() { - controller.computeAllLibraries(rebuild: true); - }); - }); - } - - @override - Widget build(BuildContext context) { - controller.instanceFieldsTreeTable = TreeTable( - keyFactory: (typeRef) => PageStorageKey(typeRef.name), - dataRoots: controller.instanceRoot!, - dataKey: 'memory-instance-tree', - columns: columns, - treeColumn: treeColumn, - defaultSortColumn: columns[0], - defaultSortDirection: SortDirection.ascending, - ); - - return controller.instanceFieldsTreeTable!; - } -} - -class _FieldTypeColumn extends TreeColumnData { - _FieldTypeColumn() : super('Type'); - - @override - String getValue(FieldReference dataObject) => - dataObject.isEmptyReference || dataObject.isSentinelReference - ? '' - : dataObject.type ?? ''; - - @override - String getDisplayValue(FieldReference dataObject) => - '${getValue(dataObject)}'; - - @override - bool get supportsSorting => true; - - @override - int compare(FieldReference a, FieldReference b) { - final valueA = getValue(a); - final valueB = getValue(b); - return valueA.compareTo(valueB); - } - - @override - double get fixedWidthPx => 250.0; -} - -class _FieldNameColumn extends ColumnData { - _FieldNameColumn() - : super( - 'Name', - fixedWidthPx: scaleByFontFactor(150.0), - ); - - @override - String getValue(FieldReference dataObject) => - dataObject.isEmptyReference ? '' : dataObject.name; - - @override - String getDisplayValue(FieldReference dataObject) => - '${getValue(dataObject)}'; - - @override - bool get supportsSorting => true; - - @override - int compare(FieldReference a, FieldReference b) { - final valueA = getValue(a); - final valueB = getValue(b); - return valueA.compareTo(valueB); - } -} - -class _FieldValueColumn extends ColumnData { - _FieldValueColumn() - : super( - 'Value', - fixedWidthPx: scaleByFontFactor(350.0), - ); - - @override - String getValue(FieldReference dataObject) => - dataObject.isEmptyReference || dataObject.isSentinelReference - ? '' - : dataObject.value ?? ''; - - @override - String getDisplayValue(FieldReference dataObject) { - if (dataObject is ObjectFieldReference && !dataObject.isNull) { - // Real object that isn't Null value is empty string. - return ''; - } - - // TODO(terry): Can we use standard Flutter string truncation? - // Long variable names the beginning and end of the name is - // most significant the middle part of name less so. However, - // we need a nice hover/tooltip to show the entire name. - // If middle, '...' then we need a more robust computation w/ - // text measurement so we accurately truncate the correct # of - // characters instead guessing that we only show 30 chars. - var value = getValue(dataObject); - if (value.length > 30) { - value = '${value.substring(0, 13)}…${value.substring(value.length - 17)}'; - } - return '$value'; - } - - @override - bool get supportsSorting => true; - - @override - int compare(FieldReference a, FieldReference b) { - final valueA = getValue(a); - final valueB = getValue(b); - return valueA.compareTo(valueB); - } -} diff --git a/packages/devtools_app/lib/src/screens/memory/memory_screen.dart b/packages/devtools_app/lib/src/screens/memory/memory_screen.dart index 3cb313c2312..589b8cf40f2 100644 --- a/packages/devtools_app/lib/src/screens/memory/memory_screen.dart +++ b/packages/devtools_app/lib/src/screens/memory/memory_screen.dart @@ -14,7 +14,7 @@ import '../../shared/theme.dart'; import '../../shared/utils.dart'; import '../../ui/icons.dart'; import 'memory_controller.dart'; -import 'memory_heap_tree_view.dart'; +import 'memory_tabs.dart'; import 'panes/chart/chart_pane.dart'; import 'panes/chart/chart_pane_controller.dart'; import 'panes/chart/memory_android_chart.dart'; @@ -84,16 +84,6 @@ class MemoryBodyState extends State sharedLabels: vmChartController.labelTimestamps, ), ); - - // Update the chart when the memorySource changes. - addAutoDisposeListener(memoryController.selectedSnapshotNotifier, () { - setState(() { - // TODO(terry): Create the snapshot data to display by Library, - // by Class or by Objects. - // Create the snapshot data by Library. - memoryController.createSnapshotByLibrary(); - }); - }); } @override @@ -111,7 +101,7 @@ class MemoryBodyState extends State keyFocusNode: _focusNode, ), Expanded( - child: HeapTreeView(memoryController), + child: MemoryTabs(memoryController), ), ], ); diff --git a/packages/devtools_app/lib/src/screens/memory/memory_snapshot_models.dart b/packages/devtools_app/lib/src/screens/memory/memory_snapshot_models.dart deleted file mode 100644 index 638e15f536f..00000000000 --- a/packages/devtools_app/lib/src/screens/memory/memory_snapshot_models.dart +++ /dev/null @@ -1,1085 +0,0 @@ -// Copyright 2020 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 'package:collection/collection.dart'; -import 'package:vm_service/vm_service.dart'; - -import '../../primitives/trees.dart'; -import 'memory_controller.dart'; -import 'memory_graph_model.dart'; -import 'primitives/predefined_classes.dart'; - -/// Consolidated list of libraries. External is the external heap -/// and Filtered is the sum of all filtered (hidden) libraries. -const externalLibraryName = 'External'; -const filteredLibrariesName = 'Filtered'; - -/// Name for predefined Reference and FieldReference should never be -/// seen by user. -const emptyName = ''; -const sentinelName = ''; - -class Reference extends TreeNode { - factory Reference({ - MemoryController? argController, - HeapGraphClassLive? argActualClass, - String? argName, - bool argIsAllocation = false, - bool argIsAllocations = false, - bool argIsAnalysis = false, - bool argIsSnapshot = false, - bool argIsLibrary = false, - bool argIsExternals = false, - bool argIsExternal = false, - bool argIsFiltered = false, - bool argIsClass = false, - bool argIsObject = false, - Function? argOnExpand, - Function? argOnLeaf, - }) { - return Reference._internal( - controller: argController, - actualClass: argActualClass, - name: argName, - isAllocation: argIsAllocation, - isAllocations: argIsAllocations, - isAnalysis: argIsAnalysis, - isSnapshot: argIsSnapshot, - isLibrary: argIsLibrary, - isExternals: argIsExternals, - isFiltered: argIsFiltered, - isClass: argIsClass, - isObject: argIsObject, - onExpand: argOnExpand, - onLeaf: argOnLeaf, - ); - } - - Reference._internal({ - this.controller, - this.actualClass, - this.name, - this.isAllocation = false, - this.isAllocations = false, - this.isAnalysis = false, - this.isSnapshot = false, - this.isLibrary = false, - this.isExternals = false, - this.isExternal = false, - this.isFiltered = false, - this.isClass = false, - this.isObject = false, - this.onExpand, - this.onLeaf, - }); - - factory Reference._empty() => Reference._internal(name: emptyName); - - factory Reference._sentinel() => Reference._internal(name: sentinelName); - - Reference.allocationsMonitor( - String name, { - Function? onExpand, - Function? onLeaf, - }) : this._internal( - controller: null, - name: name, - isAllocations: true, - onExpand: onExpand, - onLeaf: onLeaf, - ); - - Reference.allocationMonitor( - MemoryController controller, - String name, { - Function? onExpand, - Function? onLeaf, - }) : this._internal( - controller: controller, - name: name, - isAllocation: true, - onExpand: onExpand, - onLeaf: onLeaf, - ); - - Reference.analysis( - MemoryController? controller, - String name, { - Function? onExpand, - Function? onLeaf, - }) : this._internal( - controller: controller, - name: name, - isAnalysis: true, - onExpand: onExpand, - onLeaf: onLeaf, - ); - - Reference.snapshot( - MemoryController? controller, - String name, { - Function? onExpand, - Function? onLeaf, - }) : this._internal( - controller: controller, - name: name, - isSnapshot: true, - onExpand: onExpand, - onLeaf: onLeaf, - ); - - Reference.library( - MemoryController controller, - String name, { - Function? onExpand, - Function? onLeaf, - }) : this._internal( - controller: controller, - name: name, - isLibrary: true, - onExpand: onExpand, - onLeaf: onLeaf, - ); - - Reference.aClass( - MemoryController controller, - HeapGraphClassLive actualClass, { - Function? onExpand, - Function? onLeaf, - }) : this._internal( - controller: controller, - actualClass: actualClass, - name: actualClass.name, - isClass: true, - onExpand: onExpand, - onLeaf: onLeaf, - ); - - /// Name is the object name. - Reference.object( - MemoryController? controller, - String name, { - Function? onExpand, - Function? onLeaf, - }) : this._internal( - controller: controller, - name: name, - isObject: true, - onExpand: onExpand, - onLeaf: onLeaf, - ); - - /// External heap - Reference.externals( - MemoryController controller, { - Function? onExpand, - }) : this._internal( - controller: controller, - name: externalLibraryName, - isExternals: true, - onExpand: onExpand, - ); - - /// External objects - Reference.external( - MemoryController controller, - String name, { - Function? onExpand, - }) : this._internal( - controller: controller, - name: name, - isExternal: true, - onExpand: onExpand, - ); - - /// All filtered libraries and classes - Reference.filtered(MemoryController controller) - : this._internal( - controller: controller, - name: filteredLibrariesName, - isFiltered: true, - ); - - static Reference empty = Reference._empty(); - - bool get isEmptyReference => this == empty; - - static Reference sentinel = Reference._sentinel(); - - bool get isSentinelReference => this == sentinel; - - final MemoryController? controller; - - final HeapGraphClassLive? actualClass; - - final String? name; - - /// Allocations are being monitored, user requested monitoring of any - /// allocated memory. Parent node containing all monitored classes. - final bool isAllocations; - - /// A monitored class. - final bool isAllocation; - - final bool isAnalysis; - - final bool isSnapshot; - - final bool isLibrary; - - /// The External heap (contains all external heap items). - final bool isExternals; - - /// An External heap item. - final bool isExternal; - - /// Class or Library is filtered. - final bool isFiltered; - - final bool isClass; - - final bool isObject; - - int? count; - - bool get hasCount => count != null; - - Function? onExpand; - - Function? onLeaf; - - @override - void expand() { - if (onExpand != null) { - onExpand!(this); - controller!.selectedLeaf = null; - } - super.expand(); - } - - @override - void leaf() { - if (isObject) { - final objectReference = this as ObjectReference; - if (controller!.selectedAnalysisLeaf != null) { - controller!.selectedAnalysisLeaf = null; - } - controller!.selectedLeaf = objectReference.instance; - } else if (isAnalysis && this is AnalysisInstance) { - final AnalysisInstance analysisInstance = this as AnalysisInstance; - if (controller!.selectedLeaf != null) { - controller!.selectedLeaf = null; - } - controller!.selectedAnalysisLeaf = analysisInstance; - } - - if (onLeaf != null) onLeaf!(this); - - super.leaf(); - } - - @override - Reference shallowCopy() { - throw UnimplementedError( - 'This method is not implemented. Implement if you ' - 'need to call `shallowCopy` on an instance of this class.', - ); - } -} - -/// Container of all snapshot analyses processed. -class AnalysesReference extends Reference { - AnalysesReference() - : super.analysis( - null, - 'Analysis', - ); -} - -/// Snapshot being analyzed. -class AnalysisSnapshotReference extends Reference { - AnalysisSnapshotReference( - this.dateTime, - ) : super.analysis( - null, - 'Analyzed ${MemoryController.formattedTimestamp(dateTime)}', - ); - - final DateTime dateTime; -} - -/// Analysis data. -class AnalysisReference extends Reference { - AnalysisReference( - String name, { - this.countNote, - this.sizeNote, - }) : super.analysis( - null, - name, - ); - - int? countNote; - int? sizeNote; -} - -/// Analysis instance. -class AnalysisInstance extends Reference { - AnalysisInstance( - MemoryController? controller, - String name, - this.fieldsRoot, - ) : super.analysis( - controller, - name, - ); - - /// quick view of fields analysis. - final AnalysisField fieldsRoot; -} - -/// Analysis instance. -class AnalysisField extends TreeNode { - AnalysisField( - this.name, - this.value, - ); - - AnalysisField._empty() - : name = null, - value = null; - - static AnalysisField empty = AnalysisField._empty(); - - bool get isEmptyReference => this == empty; - - final String? name; - final String? value; - - @override - AnalysisField shallowCopy() { - throw UnimplementedError( - 'This method is not implemented. Implement if you ' - 'need to call `shallowCopy` on an instance of this class.', - ); - } -} - -/// Snapshot being analyzed. -class SnapshotReference extends Reference { - SnapshotReference(this.snapshot) - : super.snapshot( - null, - title(snapshot), - ); - - static String title(Snapshot snapshot) { - final timestamp = snapshot.collectedTimestamp; - final displayTimestamp = MemoryController.formattedTimestamp(timestamp); - return snapshot.autoSnapshot - ? 'Snapshot (${snapshot.snapshotGraph.name}) $displayTimestamp Auto' - : 'Snapshot (${snapshot.snapshotGraph.name}) $displayTimestamp'; - } - - final Snapshot snapshot; -} - -class LibraryReference extends Reference { - LibraryReference( - MemoryController controller, - String libraryName, - this.actualClasses, - ) : super.library( - controller, - libraryName, - onExpand: (reference) { - assert(reference.isLibrary); - - // Need to construct the children. - if (reference.children.isNotEmpty && - reference.children.first.isEmptyReference) { - reference.children.clear(); - - final libraryReference = reference as LibraryReference; - for (final actualClass in libraryReference.actualClasses ?? {}) { - final classRef = ClassReference(controller, actualClass); - reference.addChild(classRef); - - // Add place holders for the instances. - final instances = - actualClass.getInstances(controller.heapGraph); - classRef.addAllChildren( - List.filled( - instances.length, - Reference.empty, - ), - ); - } - } - }, - ); - - Set? actualClasses; -} - -class ExternalReferences extends Reference { - ExternalReferences(MemoryController controller, int externalsSize) - : super.externals(controller); -} - -class ExternalReference extends Reference { - ExternalReference(MemoryController controller, String name, this.liveExternal) - : super.external( - controller, - name, - onExpand: (reference) { - assert(reference.isExternal); - - // Need to construct the children. - if (reference.children.isNotEmpty && - reference.children.first.isEmptyReference) { - reference.children.clear(); - - final externalReference = reference as ExternalReference; - final liveElement = externalReference.liveExternal.live; - externalReference.addChild( - ObjectReference( - controller, - 0, - liveElement, - ), - ); - } - }, - ); - - final HeapGraphExternalLive liveExternal; - int sumExternalSizes = 0; -} - -class FilteredReference extends Reference { - FilteredReference(MemoryController controller) : super.filtered(controller); -} - -void computeInstanceForClassReference( - MemoryController? controller, - Reference reference, -) { - // Need to construct the children if the first child is Reference.empty. - if (reference.children.isNotEmpty && - reference.children.first.isEmptyReference) { - reference.children.clear(); - - final classReference = reference as ClassReference; - - final instances = - classReference.actualClass!.getInstances(controller!.heapGraph); - - for (var index = 0; index < instances.length; index++) { - final instance = instances[index]; - final objectRef = ObjectReference(controller, index, instance); - classReference.addChild(objectRef); - } - } -} - -class ClassReference extends Reference { - ClassReference( - MemoryController controller, - HeapGraphClassLive actualClass, - ) : super.aClass( - controller, - actualClass, - onExpand: (reference) { - // Insure the children have been computed. - computeInstanceForClassReference(controller, reference); - }, - ); - - List get instances => - actualClass!.getInstances(controller!.heapGraph); -} - -class ObjectReference extends Reference { - ObjectReference( - MemoryController? controller, - int index, - this.instance, - ) : super.object(controller, 'Instance $index'); - - final HeapGraphElementLive instance; -} - -class ExternalObjectReference extends ObjectReference { - ExternalObjectReference( - MemoryController controller, - int index, - HeapGraphElementLive instance, - this.externalSize, - ) : super( - controller, - index, - instance, - ); - - final int externalSize; -} - -class Snapshot { - Snapshot( - this.collectedTimestamp, - this.controller, - this.snapshotGraph, - this.libraryRoot, - this.autoSnapshot, - ); - - final bool autoSnapshot; - final MemoryController controller; - final DateTime collectedTimestamp; - final HeapSnapshotGraph snapshotGraph; - LibraryReference? libraryRoot; - - final Map libraries = {}; - - List librariesToList() => - libraries.entries.map((entry) => entry.value).toList(); - - List get classes => snapshotGraph.classes; -} - -/// Base class for inspecting an instance field type, field name, and value. -class FieldReference extends TreeNode { - FieldReference._empty() - : controller = null, - instance = null, - name = emptyName, - isScaler = false, - isObject = false, - type = null, - value = null; - - FieldReference._sentinel() - : controller = null, - instance = null, - name = sentinelName, - isScaler = false, - isObject = false, - type = null, - value = null; - - FieldReference.createScaler( - this.controller, - this.instance, - this.type, - this.name, - this.value, - ) : isScaler = true, - isObject = false; - - FieldReference.createObject( - this.controller, - this.instance, - this.type, - this.name, { - this.onExpand, - }) : value = null, - isScaler = false, - isObject = true; - - static FieldReference empty = FieldReference._empty(); - - bool get isEmptyReference => this == empty; - - static FieldReference sentinel = FieldReference._sentinel(); - - bool get isSentinelReference => this == sentinel; - - final MemoryController? controller; - - final HeapGraphElementLive? instance; - - final String? type; - - final String name; - - final String? value; - - final bool isScaler; - - final bool isObject; - - Function? onExpand; - - @override - void expand() { - if (onExpand != null) { - onExpand!(this); - } - super.expand(); - } - - @override - FieldReference shallowCopy() { - throw UnimplementedError( - 'This method is not implemented. Implement if you ' - 'need to call `shallowCopy` on an instance of this class.', - ); - } -} - -class ScalarFieldReference extends FieldReference { - ScalarFieldReference( - MemoryController controller, - HeapGraphElementLive instance, - String type, - String name, - String value, - ) : super.createScaler( - controller, - instance, - type, - name, - value, - ); -} - -class ObjectFieldReference extends FieldReference { - ObjectFieldReference( - MemoryController? controller, - HeapGraphElementLive? instance, - String type, - String name, { - this.isNull = false, - }) : super.createObject( - controller, - instance, - type, - name, - ) { - onExpand = _expandObjectFieldRef; - } - - void _expandObjectFieldRef(reference) { - // Need to construct the children. - if (reference.children.isNotEmpty && - reference.children.first.isEmptyReference) { - // Remove empty entries compute the real values. - reference.children.clear(); - - // Null value nothing to expand. - if (reference.isScaler) return; - } - - assert(reference.isObject); - final ObjectFieldReference objectFieldReference = reference; - - var objFields = instanceToFieldNodes( - controller, - objectFieldReference.instance!, - ); - - if (objFields.isNotEmpty) { - final computedFields = []; - for (final ref in objFields) { - // Check if aready computed/expanded then skip. - final exist = objectFieldReference.children - .singleWhereOrNull((element) => element.name == ref.name); - if (exist == null) { - // Doesn't exist so add field. - if (ref is ObjectFieldReference && !ref.isNull) { - computedFields.add(ref); - } else { - final FieldReference fieldRef = ref; - final HeapGraphElementLive live = fieldRef.instance!; - final HeapGraphClassLive theClass = - live.theClass as HeapGraphClassLive; - final predefined = predefinedClasses[theClass.fullQualifiedName]; - if (predefined != null && predefined.isScalar) { - final scalarValue = createScalar( - controller, - fieldRef.name, - fieldRef.instance!, - ); - computedFields.add(scalarValue); - } - } - } - } - objFields = computedFields; - } - - objectFieldReference.addAllChildren(objFields); - } - - final bool isNull; -} - -/// Return list of FieldReference nodes (TableTree use) from the fields of an instance. -List instanceToFieldNodes( - MemoryController? controller, - HeapGraphElementLive instance, -) { - final List root = []; - final List> fields = instance.getFields(); - - var sentinelCount = 0; - var fieldIndex = 0; - for (var fieldElement in fields) { - // Ignore internal patching fields. - if (fieldElement.key != '__parts') { - if (fieldElement.value is! HeapGraphElementSentinel && - instance.references![fieldIndex] is! HeapGraphElementSentinel) { - root.add( - fieldToFieldReference( - controller, - instance, - fieldElement, - ), - ); - } else { - sentinelCount++; - root.add(FieldReference.sentinel); - } - } - fieldIndex++; - } - - if (root.isNotEmpty && sentinelCount > 0) { - root.removeWhere((e) => e.isSentinelReference); - } - - return root; -} - -/// Return a FieldReference node (TableTree use) from the field of an instance. -FieldReference fieldToFieldReference( - MemoryController? controller, - HeapGraphElementLive instance, - MapEntry fieldElement, -) { - if (fieldElement.value is HeapGraphElementSentinel) { - // TODO(terry): Debug for now, eliminate, user's don't need to know. - return FieldReference.sentinel; - } - - final theGraph = controller!.heapGraph; - - final actual = fieldElement.value as HeapGraphElementLive; - final HeapGraphClassLive? theClass = actual.theClass as HeapGraphClassLive?; - // Debugging a particular field displayed use fieldElement.key (field name) - // to break. - - // Handle change in VM service to use 0 based vs/ 1 based indexing. Need to - // support old semantics for old VMs. - final classId = actual.origin.classId; - final int indexIntoClass = classId; - if (!controller.heapGraph!.builtInClasses.containsValue(indexIntoClass)) { - return objectToFieldReference( - controller, - theGraph, - fieldElement, - actual, - ); - } else { - final data = actual.origin.data; - if (data.runtimeType == HeapSnapshotObjectLengthData) { - final isAMap = isBuiltInMap(theClass!); - if (isAMap || isBuiltInList(theClass)) { - return listToFieldEntries( - controller, - actual, - fieldElement.key, - data.length, - isHashMap: isAMap, - ); - } - } else if (isBuiltInHashMap(theClass!)) { - for (var ref in fieldElement.value!.references!) { - if (ref is! HeapGraphElementSentinel) { - final HeapGraphElementLive actual = ref as HeapGraphElementLive; - final HeapGraphClassLive theClass = - actual.theClass as HeapGraphClassLive; - if (isBuiltInList(theClass)) { - final hashMapData = actual.origin.data; - if (hashMapData.runtimeType == HeapSnapshotObjectLengthData) { - return listToFieldEntries( - controller, - ref, - fieldElement.key, - hashMapData.length ~/ 2, - isHashMap: true, - ); - } - } - } - } - } - return createScalar(controller, fieldElement.key, actual); - } -} - -/// Create a scalar field for display in the Table Tree. -FieldReference createScalar( - MemoryController? controller, - String fieldName, - HeapGraphElementLive actual, -) { - final data = actual.origin.data; - - String dataValue; - String dataType = ''; - switch (data.runtimeType) { - case HeapSnapshotObjectNoData: - dataValue = 'Object No Data'; - break; - case HeapSnapshotObjectNullData: - dataValue = ''; - dataType = 'Null'; - break; - case HeapSnapshotObjectLengthData: - dataValue = data.length.toString(); - break; - default: - final originClassId = actual.origin.classId; - final classId = originClassId; - dataValue = data.toString(); - final dataTypeClass = controller!.heapGraph!.classes[classId]; - if (dataTypeClass != null) { - final predefined = predefinedClasses[dataTypeClass.fullQualifiedName]!; - dataType = predefined.prettyName; - } - } - - return FieldReference.createScaler( - controller, - actual, - dataType, - fieldName, - dataValue, - ); -} - -/// Display a List. -FieldReference listToFieldEntries( - MemoryController? controller, - HeapGraphElement? reference, - String fieldName, - int? size, { - isHashMap = false, -}) { - bool isAMap = false; - HeapGraphElementLive? actualListElement; - late ObjectFieldReference listObjectReference; - if (reference is HeapGraphElementLive) { - final actualListClass = reference.theClass as HeapGraphClassLive; - if (isBuiltInList(actualListClass)) { - // Add the list entry. - actualListElement = reference; - listObjectReference = ObjectFieldReference( - controller, - actualListElement, - isHashMap ? 'HashMap' : 'List', - isHashMap ? '$fieldName {$size}' : '[$size]', - ); - } else if (isBuiltInMap(actualListClass)) { - // Add the Map field name and the key/value pairs. - actualListElement = reference; - listObjectReference = ObjectFieldReference( - controller, - actualListElement, - 'Map', - '$fieldName { ${size! ~/ 2} }', - ); - - // Look for list of Map values. - for (final reference in actualListElement.references!) { - if (reference is HeapGraphElementLive) { - final HeapGraphClassLive theClass = - reference.theClass as HeapGraphClassLive; - final fullClassName = theClass.fullQualifiedName; - if (fullClassName == predefinedList) { - actualListElement = reference; - isAMap = true; - break; - } - } - } - } - } - - var listIndex = 0; - final allEntryReferences = actualListElement!.references!; - final referencesLength = allEntryReferences.length; - - // Find all the Map key/value pairs (for integer keys the key maybe missing). - // TODO(terry): Need to verify. - final List realEntries = []; - for (var entryElementIndex = 0; - entryElementIndex < referencesLength; - entryElementIndex++) { - final entry = allEntryReferences[entryElementIndex]; - if (entry is HeapGraphElementLive) { - final HeapGraphElementLive entryElement = entry; - final HeapGraphClassLive actualClass = - entryElement.theClass as HeapGraphClassLive; - if (actualClass.fullQualifiedName != predefinedNull) { - realEntries.add(entryElement); - } - } - } - - // TODO(terry): Need to verify. - // Only value key if size != to number of real entries. - final hasKeyValues = isAMap && realEntries.length == size; - - for (var realEntryIndex = 0; - realEntryIndex < realEntries.length; - realEntryIndex++) { - final entryElement = realEntries[realEntryIndex]; - if (entryElement is HeapGraphElementLive) { - final entryClass = entryElement.theClass; - if (entryClass is HeapGraphClassLive && - entryClass.fullQualifiedName != predefinedNull) { - final predefined0 = predefinedClasses[entryClass.fullQualifiedName]; - FieldReference? listEntry; - if (predefined0 != null && predefined0.isScalar) { - if (isAMap) { - if (hasKeyValues) { - final HeapGraphElementLive valueElement = - realEntries[realEntryIndex + 1] as HeapGraphElementLive; - realEntryIndex++; - // The value entry is computed on key expansion. - final predefined1 = - predefinedClasses[entryClass.fullQualifiedName]!; - listEntry = ObjectFieldReference( - controller, - entryElement, - '${predefined1.prettyName}', - 'key \'${entryElement.origin.data}\'', - ); - - FieldReference valueEntry; - final HeapGraphClassLive valueClass = - valueElement.theClass as HeapGraphClassLive; - final predefined2 = - predefinedClasses[valueClass.fullQualifiedName]; - if (predefined2 != null && predefined2.isScalar) { - valueEntry = createScalar(controller, 'value', valueElement); - } else { - valueEntry = ObjectFieldReference( - controller, - valueElement, - valueClass.name, - 'value', - ); - // Compute the object's fields when onExpand hit. - valueEntry.addChild(FieldReference.empty); - } - listEntry.addChild(valueEntry); - } else { - // This is the value w/o any idea the index is the key. - listEntry = - createScalar(controller, 'value $listIndex', entryElement); - } - } else { - // Display the scalar entry. - listEntry = createScalar(controller, fieldName, entryElement); - } - } else { - listEntry ??= ObjectFieldReference( - controller, - entryElement, - entryClass.name, - '[$listIndex]', - ); - if (entryElement.references!.isNotEmpty) { - // Key of the Map is an object. - if (isAMap) { - final keyFields = entryElement.getFields(); - for (final keyField in keyFields) { - final key = keyField.key; - final value = keyField.value; - - // Skip sentinels and null values. - if (!value.isSentinel && - !dataIsNull(value as HeapGraphElementLive)) { - final HeapGraphElementLive live = value; - final HeapGraphClassLive theClass = - live.theClass as HeapGraphClassLive; - final className = theClass.fullQualifiedName; - final predefined = predefinedClasses[className]; - if (predefined != null && predefined.isScalar) { - final scalarEntry = createScalar(controller, key, live); - listEntry.addChild(scalarEntry); - } else { - final keyObjectRef = ObjectFieldReference( - controller, - live, - theClass.name, - '$key', - ); - - keyObjectRef.addChild(FieldReference.empty); - listEntry.addChild(keyObjectRef); - } - } - } - } - } - } - - // Entry type to expand later. - if (!isAMap) { - listEntry.addChild(FieldReference.empty); - } - - // Add our [n] entry. - listObjectReference.addChild(listEntry); - - // TODO(terry): Consider showing all entries - is it useful? - // Add each entry to the list, up to 100. - if (listIndex++ > 100) break; - } - } - } - - return listObjectReference; -} - -bool dataIsNull(HeapGraphElementLive live) => - live.origin.data.runtimeType == HeapSnapshotObjectNullData; - -/// Return a ObjectFieldReference node (TableTree use) from the field that -/// is an object (instance). This object's field will be computed when -/// the ObjectFieldReference node is expanded. -FieldReference objectToFieldReference( - MemoryController? controller, - HeapGraph? theGraph, - MapEntry objectEntry, - HeapGraphElementLive actual, -) { - final elementActual = objectEntry.value as HeapGraphElementLive; - final classActual = elementActual.theClass as HeapGraphClassLive; - - final isNullValue = actual.origin.data == null; - final reference = ObjectFieldReference( - controller, - actual, - classActual.name, - objectEntry.key, - isNull: isNullValue, - ); - - // If the object isn't null then add one fake empty child, so node - // can be expanded. The object's fields are computed on expansion. - reference.addAllChildren( - List.filled( - isNullValue ? 0 : 1, - FieldReference.empty, - ), - ); - - return reference; -} diff --git a/packages/devtools_app/lib/src/screens/memory/memory_tabs.dart b/packages/devtools_app/lib/src/screens/memory/memory_tabs.dart new file mode 100644 index 00000000000..4dd9d537027 --- /dev/null +++ b/packages/devtools_app/lib/src/screens/memory/memory_tabs.dart @@ -0,0 +1,171 @@ +// Copyright 2020 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 'package:flutter/material.dart'; + +import '../../primitives/auto_dispose_mixin.dart'; +import '../../shared/common_widgets.dart'; +import '../../shared/theme.dart'; +import '../../shared/utils.dart'; +import '../../ui/search.dart'; +import '../../ui/tab.dart'; +import 'memory_controller.dart'; +import 'panes/allocation_profile/allocation_profile_table_view.dart'; +import 'panes/allocation_tracing/allocation_profile_tracing_view.dart'; +import 'panes/diff/diff_pane.dart'; +import 'panes/leaks/leaks_pane.dart'; + +@visibleForTesting +class MemoryScreenKeys { + static const leaksTab = Key('Leaks Tab'); + static const dartHeapTableProfileTab = Key('Dart Heap Profile Tab'); + static const dartHeapAllocationTracingTab = + Key('Dart Heap Allocation Tracing Tab'); + static const diffTab = Key('Diff Tab'); +} + +class MemoryTabs extends StatefulWidget { + const MemoryTabs( + this.controller, + ); + + final MemoryController controller; + + @override + _MemoryTabsState createState() => _MemoryTabsState(); +} + +class _MemoryTabsState extends State + with + AutoDisposeMixin, + ProvidedControllerMixin, + SearchFieldMixin, + TickerProviderStateMixin { + static const _gaPrefix = 'memoryTab'; + + late List _tabs; + late TabController _tabController; + final ValueNotifier _currentTab = ValueNotifier(0); + + void _initTabs() { + _tabs = [ + DevToolsTab.create( + key: MemoryScreenKeys.dartHeapTableProfileTab, + tabName: 'Profile', + gaPrefix: _gaPrefix, + ), + DevToolsTab.create( + key: MemoryScreenKeys.dartHeapAllocationTracingTab, + tabName: 'Allocation Tracing', + gaPrefix: _gaPrefix, + ), + DevToolsTab.create( + key: MemoryScreenKeys.diffTab, + gaPrefix: _gaPrefix, + tabName: 'Diff', + ), + if (widget.controller.shouldShowLeaksTab.value) + DevToolsTab.create( + key: MemoryScreenKeys.leaksTab, + gaPrefix: _gaPrefix, + tabName: 'Leaks', + ), + ]; + + _tabController = TabController(length: _tabs.length, vsync: this); + _tabController.addListener(_onTabChanged); + } + + void _onTabChanged() => _currentTab.value = _tabController.index; + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + if (!initController()) return; + + cancelListeners(); + + _initTabs(); + + addAutoDisposeListener(controller.shouldShowLeaksTab, () { + setState(() { + _initTabs(); + }); + }); + } + + @override + void dispose() { + _tabController + ..removeListener(_onTabChanged) + ..dispose(); + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final themeData = Theme.of(context); + + return Column( + children: [ + const SizedBox(height: defaultSpacing), + ValueListenableBuilder( + valueListenable: _currentTab, + builder: (context, index, _) => Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + TabBar( + labelColor: themeData.textTheme.bodyLarge!.color, + isScrollable: true, + controller: _tabController, + tabs: _tabs, + ), + ], + ), + ), + const Divider(), + Expanded( + child: TabBarView( + physics: defaultTabBarViewPhysics, + controller: _tabController, + children: [ + // Profile Tab + KeepAliveWrapper( + child: AllocationProfileTableView( + controller: controller.allocationProfileController, + ), + ), + const KeepAliveWrapper( + child: AllocationProfileTracingView(), + ), + // Diff tab. + KeepAliveWrapper( + child: DiffPane( + diffController: controller.diffPaneController, + ), + ), + // Leaks tab. + if (controller.shouldShowLeaksTab.value) + const KeepAliveWrapper(child: LeaksPane()), + ], + ), + ), + ], + ); + } + + Widget tableExample(IconData? iconData, String entry) { + final themeData = Theme.of(context); + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + iconData == null + ? Text(' ', style: themeData.fixedFontStyle) + : Icon(iconData), + Text(entry, style: themeData.fixedFontStyle), + ], + ); + } +} diff --git a/packages/devtools_app/lib/src/screens/memory/panes/chart/chart_control_pane.dart b/packages/devtools_app/lib/src/screens/memory/panes/chart/chart_control_pane.dart index 16b71f74cd7..a7c322a2855 100644 --- a/packages/devtools_app/lib/src/screens/memory/panes/chart/chart_control_pane.dart +++ b/packages/devtools_app/lib/src/screens/memory/panes/chart/chart_control_pane.dart @@ -87,13 +87,6 @@ class _ChartControlPaneState extends State controller.memoryTimeline.reset(); - // Clear all analysis and snapshots collected too. - controller.clearAllSnapshots(); - controller.classRoot = null; - controller.topNode = null; - controller.selectedSnapshotTimestamp = null; - controller.selectedLeaf = null; - // Remove history of all plotted data in all charts. widget.chartController.resetAll(); } diff --git a/packages/devtools_app/lib/src/screens/memory/panes/control/settings_dialog.dart b/packages/devtools_app/lib/src/screens/memory/panes/control/settings_dialog.dart index 3d11fa3b072..39537e59b43 100644 --- a/packages/devtools_app/lib/src/screens/memory/panes/control/settings_dialog.dart +++ b/packages/devtools_app/lib/src/screens/memory/panes/control/settings_dialog.dart @@ -14,7 +14,6 @@ import '../../memory_controller.dart'; @visibleForTesting class MemorySettingDialogKeys { static const Key showAndroidChartCheckBox = ValueKey('showAndroidChart'); - static const Key autoSnapshotCheckbox = ValueKey('autoSnapshotCheckbox'); } class MemorySettingsDialog extends StatelessWidget { @@ -39,14 +38,6 @@ class MemorySettingsDialog extends StatelessWidget { 'Show Android memory chart in addition to Dart memory chart', checkboxKey: MemorySettingDialogKeys.showAndroidChartCheckBox, ), - const SizedBox( - height: defaultSpacing, - ), - CheckboxSetting( - notifier: preferences.memory.autoSnapshotEnabled, - title: 'Automatically take snapshot when memory usage spikes', - checkboxKey: MemorySettingDialogKeys.autoSnapshotCheckbox, - ), ], ), ), diff --git a/packages/devtools_app/lib/src/shared/preferences.dart b/packages/devtools_app/lib/src/shared/preferences.dart index 5d6025a4251..dbf648cd28c 100644 --- a/packages/devtools_app/lib/src/shared/preferences.dart +++ b/packages/devtools_app/lib/src/shared/preferences.dart @@ -306,9 +306,6 @@ class MemoryPreferencesController extends DisposableController static const _androidCollectionEnabledStorageId = 'memory.androidCollectionEnabled'; - final autoSnapshotEnabled = ValueNotifier(false); - static const _autoSnapshotEnabledStorageId = 'memory.autoSnapshotEnabled'; - final showChart = ValueNotifier(true); static const _showChartStorageId = 'memory.showChart'; @@ -331,24 +328,6 @@ class MemoryPreferencesController extends DisposableController androidCollectionEnabled.value = await storage.getValue(_androidCollectionEnabledStorageId) == 'true'; - addAutoDisposeListener( - autoSnapshotEnabled, - () { - storage.setValue( - _autoSnapshotEnabledStorageId, - autoSnapshotEnabled.value.toString(), - ); - if (autoSnapshotEnabled.value) { - ga.select( - analytics_constants.memory, - analytics_constants.MemoryEvent.autoSnapshot, - ); - } - }, - ); - autoSnapshotEnabled.value = - await storage.getValue(_autoSnapshotEnabledStorageId) == 'true'; - addAutoDisposeListener( showChart, () { diff --git a/packages/devtools_app/test/goldens/settings_dialog_default.png b/packages/devtools_app/test/goldens/settings_dialog_default.png index 609bf6c74d3..5334987746b 100644 Binary files a/packages/devtools_app/test/goldens/settings_dialog_default.png and b/packages/devtools_app/test/goldens/settings_dialog_default.png differ diff --git a/packages/devtools_app/test/goldens/settings_dialog_modified.png b/packages/devtools_app/test/goldens/settings_dialog_modified.png index fc69ac1df37..174a86a946b 100644 Binary files a/packages/devtools_app/test/goldens/settings_dialog_modified.png and b/packages/devtools_app/test/goldens/settings_dialog_modified.png differ diff --git a/packages/devtools_app/test/memory/allocation_profile/allocation_profile_table_view_test.dart b/packages/devtools_app/test/memory/allocation_profile/allocation_profile_table_view_test.dart index d2005925909..d3385e45297 100644 --- a/packages/devtools_app/test/memory/allocation_profile/allocation_profile_table_view_test.dart +++ b/packages/devtools_app/test/memory/allocation_profile/allocation_profile_table_view_test.dart @@ -3,8 +3,8 @@ // found in the LICENSE file. import 'package:devtools_app/src/screens/memory/memory_controller.dart'; -import 'package:devtools_app/src/screens/memory/memory_heap_tree_view.dart'; import 'package:devtools_app/src/screens/memory/memory_screen.dart'; +import 'package:devtools_app/src/screens/memory/memory_tabs.dart'; import 'package:devtools_app/src/screens/memory/panes/allocation_profile/allocation_profile_table_view_controller.dart'; import 'package:devtools_app/src/screens/memory/panes/allocation_profile/model.dart'; import 'package:devtools_app/src/shared/globals.dart'; diff --git a/packages/devtools_app/test/memory/allocation_tracing/allocation_profile_tracing_view_test.dart b/packages/devtools_app/test/memory/allocation_tracing/allocation_profile_tracing_view_test.dart index eb66897037c..e632f5b268e 100644 --- a/packages/devtools_app/test/memory/allocation_tracing/allocation_profile_tracing_view_test.dart +++ b/packages/devtools_app/test/memory/allocation_tracing/allocation_profile_tracing_view_test.dart @@ -9,8 +9,8 @@ import 'package:devtools_app/src/config_specific/ide_theme/ide_theme.dart'; import 'package:devtools_app/src/config_specific/import_export/import_export.dart'; import 'package:devtools_app/src/primitives/trees.dart'; import 'package:devtools_app/src/screens/memory/memory_controller.dart'; -import 'package:devtools_app/src/screens/memory/memory_heap_tree_view.dart'; import 'package:devtools_app/src/screens/memory/memory_screen.dart'; +import 'package:devtools_app/src/screens/memory/memory_tabs.dart'; import 'package:devtools_app/src/screens/memory/panes/allocation_tracing/allocation_profile_tracing_tree.dart'; import 'package:devtools_app/src/screens/memory/panes/allocation_tracing/allocation_profile_tracing_view.dart'; import 'package:devtools_app/src/screens/memory/panes/allocation_tracing/allocation_profile_tracing_view_controller.dart'; diff --git a/packages/devtools_app/test/memory/control/settings_dialog_test.dart b/packages/devtools_app/test/memory/control/settings_dialog_test.dart index 3bc89086a9c..3f7ba8e5101 100644 --- a/packages/devtools_app/test/memory/control/settings_dialog_test.dart +++ b/packages/devtools_app/test/memory/control/settings_dialog_test.dart @@ -53,13 +53,8 @@ void main() { preferences.memory.androidCollectionEnabled.value, isFalse, ); - expect( - preferences.memory.autoSnapshotEnabled.value, - isFalse, - ); await tester .tap(find.byKey(MemorySettingDialogKeys.showAndroidChartCheckBox)); - await tester.tap(find.byKey(MemorySettingDialogKeys.autoSnapshotCheckbox)); await tester.pumpAndSettle(); await expectLater( find.byType(MemorySettingsDialog), @@ -69,10 +64,6 @@ void main() { preferences.memory.androidCollectionEnabled.value, isTrue, ); - expect( - preferences.memory.autoSnapshotEnabled.value, - isTrue, - ); // Reopen the dialog and check the settings are not changed. await tester.tap(find.byType(DialogCloseButton)); diff --git a/packages/devtools_app/test/memory/diff/widgets/diff_pane_test.dart b/packages/devtools_app/test/memory/diff/widgets/diff_pane_test.dart index e8860048ce0..707b6c3c60f 100644 --- a/packages/devtools_app/test/memory/diff/widgets/diff_pane_test.dart +++ b/packages/devtools_app/test/memory/diff/widgets/diff_pane_test.dart @@ -2,8 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:devtools_app/src/screens/memory/memory_heap_tree_view.dart'; import 'package:devtools_app/src/screens/memory/memory_screen.dart'; +import 'package:devtools_app/src/screens/memory/memory_tabs.dart'; import 'package:devtools_app/src/screens/memory/panes/diff/diff_pane.dart'; import 'package:devtools_test/devtools_test.dart'; import 'package:flutter/material.dart'; diff --git a/packages/devtools_app/test/shared/preferences_controller_test.dart b/packages/devtools_app/test/shared/preferences_controller_test.dart index 3b9aa4ca497..1871ca75821 100644 --- a/packages/devtools_app/test/shared/preferences_controller_test.dart +++ b/packages/devtools_app/test/shared/preferences_controller_test.dart @@ -135,25 +135,18 @@ void main() { await controller.init(); }); - test('has expected default values', () async { - expect(controller.androidCollectionEnabled.value, isFalse); - expect(controller.autoSnapshotEnabled.value, isFalse); - }); - test('stores values and reads them on init', () async { storage.values.clear(); // Remember original values. final originalAndroidCollection = controller.androidCollectionEnabled.value; - final originalAutoSnapshot = controller.androidCollectionEnabled.value; // Flip the values in controller. controller.androidCollectionEnabled.value = !originalAndroidCollection; - controller.autoSnapshotEnabled.value = !originalAutoSnapshot; // Check the values are stored. - expect(storage.values, hasLength(2)); + expect(storage.values, hasLength(1)); // Reload the values from storage. await controller.init(); @@ -163,10 +156,6 @@ void main() { controller.androidCollectionEnabled.value, !originalAndroidCollection, ); - expect( - controller.autoSnapshotEnabled.value, - !originalAutoSnapshot, - ); // Flip the values in storage. for (var key in storage.values.keys) { @@ -181,10 +170,6 @@ void main() { controller.androidCollectionEnabled.value, originalAndroidCollection, ); - expect( - controller.autoSnapshotEnabled.value, - originalAutoSnapshot, - ); }); }); }