diff --git a/packages/devtools_app/lib/src/screens/memory/panes/diff/controller/diff_pane_controller.dart b/packages/devtools_app/lib/src/screens/memory/panes/diff/controller/diff_pane_controller.dart index 5e3d2cc13ed..13428e7cda6 100644 --- a/packages/devtools_app/lib/src/screens/memory/panes/diff/controller/diff_pane_controller.dart +++ b/packages/devtools_app/lib/src/screens/memory/panes/diff/controller/diff_pane_controller.dart @@ -10,13 +10,16 @@ import 'package:flutter/foundation.dart'; import '../../../../../primitives/utils.dart'; import '../../../primitives/memory_utils.dart'; import '../../../shared/heap/model.dart'; -import 'model.dart'; +import 'heap_diff.dart'; +import 'item_controller.dart'; class DiffPaneController { DiffPaneController(this.snapshotTaker); final SnapshotTaker snapshotTaker; + final diffStore = HeapDiffStore(); + /// The list contains one item that show information and all others /// are snapshots. ValueListenable> get snapshots => _snapshots; @@ -29,7 +32,12 @@ class DiffPaneController { ValueListenable get isProcessing => _isProcessing; final _isProcessing = ValueNotifier(false); - DiffListItem get selected => snapshots.value[selectedIndex.value]; + DiffListItem get selectedItem => snapshots.value[selectedIndex.value]; + + /// Full name for the selected class. + ValueListenable get selectedClass => _selectedClass; + final _selectedClass = ValueNotifier(null); + void setSelectedClass(String? value) => _selectedClass.value = value; /// True, if the list contains snapshots, i.e. items beyond the first /// informational item. @@ -43,6 +51,8 @@ class DiffPaneController { future, _nextDisplayNumber(), currentIsolateName ?? '', + diffStore, + selectedClass, ), ); await future; @@ -52,6 +62,9 @@ class DiffPaneController { } Future clearSnapshots() async { + for (var i = 1; i < snapshots.value.length; i++) { + snapshots.value[i].dispose(); + } _snapshots.removeRange(1, snapshots.value.length); _selectedIndex.value = 0; } @@ -63,7 +76,8 @@ class DiffPaneController { } void deleteCurrentSnapshot() { - assert(selected is SnapshotListItem); + assert(selectedItem is SnapshotListItem); + selectedItem.dispose(); _snapshots.removeRange(selectedIndex.value, selectedIndex.value + 1); // We must change the selectedIndex, because otherwise the content will // not be re-rendered. diff --git a/packages/devtools_app/lib/src/screens/memory/panes/diff/controller/heap_diff.dart b/packages/devtools_app/lib/src/screens/memory/panes/diff/controller/heap_diff.dart new file mode 100644 index 00000000000..0eaa2f498f4 --- /dev/null +++ b/packages/devtools_app/lib/src/screens/memory/panes/diff/controller/heap_diff.dart @@ -0,0 +1,102 @@ +// Copyright 2022 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/foundation.dart'; + +import '../../../shared/heap/heap.dart'; +import '../../../shared/heap/model.dart'; + +/// Stores already calculated comparisons for heap couples. +class HeapDiffStore { + final _store = <_HeapCouple, HeapComparison>{}; + + HeapComparison compare(AdaptedHeap heap1, AdaptedHeap heap2) { + final couple = _HeapCouple(heap1, heap2); + return _store.putIfAbsent(couple, () => HeapComparison(couple)); + } +} + +@immutable +class _HeapCouple { + _HeapCouple(AdaptedHeap heap1, AdaptedHeap heap2) { + older = _older(heap1, heap2); + younger = older == heap1 ? heap2 : heap1; + } + + late final AdaptedHeap older; + late final AdaptedHeap younger; + + /// Finds most deterministic way to declare earliest heap. + /// + /// If earliest heap cannot be identified, returns first argument. + static AdaptedHeap _older(AdaptedHeap heap1, AdaptedHeap heap2) { + assert(heap1.data != heap2.data); + if (heap1.data.created.isBefore(heap2.data.created)) return heap1; + if (heap2.data.created.isBefore(heap1.data.created)) return heap2; + if (identityHashCode(heap1) < identityHashCode(heap2)) return heap1; + if (identityHashCode(heap2) < identityHashCode(heap1)) return heap2; + if (identityHashCode(heap1.data) < identityHashCode(heap2.data)) + return heap1; + if (identityHashCode(heap2.data) < identityHashCode(heap1.data)) + return heap2; + return heap1; + } + + @override + bool operator ==(Object other) { + if (other.runtimeType != runtimeType) { + return false; + } + return other is _HeapCouple && + other.older == older && + other.younger == younger; + } + + @override + int get hashCode => Object.hash(older, younger); +} + +class HeapComparison { + HeapComparison(this.heapCouple); + + late final HeapStatistics stats = _stats(); + + final _HeapCouple heapCouple; + + HeapStatistics _stats() { + final result = {}; + + final older = heapCouple.older.stats.recordsByClass; + final younger = heapCouple.younger.stats.recordsByClass; + + final unionOfKeys = older.keys.toSet().union(younger.keys.toSet()); + + for (var key in unionOfKeys) { + final olderRecord = older[key]; + final youngerRecord = younger[key]; + + if (olderRecord != null && youngerRecord != null) { + final diff = _diffStatsRecords(olderRecord, youngerRecord); + if (!diff.isZero) result[key] = diff; + } else if (youngerRecord != null) { + result[key] = youngerRecord; + } else { + result[key] = olderRecord!.negative(); + } + } + + return HeapStatistics(result); + } + + static HeapStatsRecord _diffStatsRecords( + HeapStatsRecord older, + HeapStatsRecord younger, + ) { + assert(older.heapClass.fullName == younger.heapClass.fullName); + return HeapStatsRecord(older.heapClass) + ..instanceCount = younger.instanceCount - older.instanceCount + ..shallowSize = younger.shallowSize - older.shallowSize + ..retainedSize = younger.retainedSize - older.retainedSize; + } +} diff --git a/packages/devtools_app/lib/src/screens/memory/panes/diff/controller/item_controller.dart b/packages/devtools_app/lib/src/screens/memory/panes/diff/controller/item_controller.dart new file mode 100644 index 00000000000..0a982835cd7 --- /dev/null +++ b/packages/devtools_app/lib/src/screens/memory/panes/diff/controller/item_controller.dart @@ -0,0 +1,97 @@ +// Copyright 2022 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/foundation.dart'; + +import '../../../../../primitives/auto_dispose.dart'; +import '../../../../../primitives/utils.dart'; +import '../../../shared/heap/heap.dart'; +import '../../../shared/heap/model.dart'; +import 'heap_diff.dart'; + +abstract class DiffListItem extends DisposableController { + /// Number, that if shown in name, should be unique in the list. + /// + /// If the number is not expected to be shown in UI, it should be 0. + int get displayNumber; + + ValueListenable get isProcessing => _isProcessing; + final _isProcessing = ValueNotifier(false); + + /// If true, the item contains data, that can be compared and analyzed. + bool get hasData; +} + +class InformationListItem extends DiffListItem { + @override + int get displayNumber => 0; + + @override + bool get hasData => false; +} + +class SnapshotListItem extends DiffListItem with AutoDisposeControllerMixin { + SnapshotListItem( + Future receiver, + this.displayNumber, + this._isolateName, + this.diffStore, + this.selectedClassName, + ) { + _isProcessing.value = true; + receiver.whenComplete(() async { + final data = await receiver; + if (data != null) { + heap = AdaptedHeap(data); + updateSelectedRecord(); + addAutoDisposeListener(selectedClassName, () => updateSelectedRecord()); + } + _isProcessing.value = false; + }); + } + + final String _isolateName; + + final HeapDiffStore diffStore; + + AdaptedHeap? heap; + + @override + final int displayNumber; + + String get name => '$_isolateName-$displayNumber'; + + var sorting = ColumnSorting(); + + ValueListenable get diffWith => _diffWith; + final _diffWith = ValueNotifier(null); + void setDiffWith(SnapshotListItem? value) { + _diffWith.value = value; + updateSelectedRecord(); + } + + final ValueListenable selectedClassName; + + ValueListenable get selectedRecord => _selectedRecord; + final _selectedRecord = ValueNotifier(null); + + @override + bool get hasData => heap != null; + + HeapStatistics get statsToShow { + final theHeap = heap!; + final itemToDiffWith = diffWith.value; + if (itemToDiffWith == null) return theHeap.stats; + return diffStore.compare(theHeap, itemToDiffWith.heap!).stats; + } + + void updateSelectedRecord() => _selectedRecord.value = + statsToShow.recordsByClass[selectedClassName.value]; +} + +class ColumnSorting { + bool initialized = false; + SortDirection direction = SortDirection.ascending; + int columnIndex = 0; +} diff --git a/packages/devtools_app/lib/src/screens/memory/panes/diff/controller/model.dart b/packages/devtools_app/lib/src/screens/memory/panes/diff/controller/model.dart deleted file mode 100644 index e5d092c6726..00000000000 --- a/packages/devtools_app/lib/src/screens/memory/panes/diff/controller/model.dart +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright 2022 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/foundation.dart'; - -import '../../../../../primitives/utils.dart'; -import '../../../shared/heap/heap.dart'; -import '../../../shared/heap/model.dart'; - -abstract class DiffListItem { - /// Number, that, if shown in name, should be unique in the list. - /// - /// If the number is not shown, it should be 0. - int get displayNumber; - - ValueListenable get isProcessing => _isProcessing; - final _isProcessing = ValueNotifier(false); -} - -class InformationListItem extends DiffListItem { - @override - int get displayNumber => 0; -} - -class SnapshotListItem extends DiffListItem { - SnapshotListItem( - Future receiver, - this.displayNumber, - this._isolateName, - ) { - _isProcessing.value = true; - receiver.whenComplete(() async { - final data = await receiver; - if (data != null) heap = AdaptedHeap(data); - _isProcessing.value = false; - }); - } - - final String _isolateName; - - final selectedRecord = ValueNotifier(null); - - AdaptedHeap? heap; - - @override - final int displayNumber; - - String get name => '$_isolateName-$displayNumber'; - - var sorting = ColumnSorting(); -} - -class ColumnSorting { - bool initialized = false; - SortDirection direction = SortDirection.ascending; - int columnIndex = 0; -} diff --git a/packages/devtools_app/lib/src/screens/memory/panes/diff/diff_pane.dart b/packages/devtools_app/lib/src/screens/memory/panes/diff/diff_pane.dart index 943b6bd7897..8936f32571c 100644 --- a/packages/devtools_app/lib/src/screens/memory/panes/diff/diff_pane.dart +++ b/packages/devtools_app/lib/src/screens/memory/panes/diff/diff_pane.dart @@ -6,8 +6,9 @@ import 'package:flutter/material.dart'; import '../../../../shared/common_widgets.dart'; import '../../../../shared/split.dart'; +import '../../../../shared/theme.dart'; import 'controller/diff_pane_controller.dart'; -import 'controller/model.dart'; +import 'controller/item_controller.dart'; import 'widgets/snapshot_control_pane.dart'; import 'widgets/snapshot_list.dart'; import 'widgets/snapshot_view.dart'; @@ -22,7 +23,7 @@ class DiffPane extends StatelessWidget { final Widget itemContent = ValueListenableBuilder( valueListenable: controller.selectedIndex, builder: (_, index, __) { - final item = controller.selected; + final item = controller.selectedItem; if (item is InformationListItem) { return const _SnapshotDoc(); @@ -68,7 +69,7 @@ class _SnapshotDoc extends StatelessWidget { class _SnapshotContent extends StatelessWidget { _SnapshotContent({Key? key, required this.item, required this.controller}) - : assert(controller.selected == item), + : assert(controller.selectedItem == item), super(key: key); final SnapshotListItem item; @@ -78,9 +79,11 @@ class _SnapshotContent extends StatelessWidget { Widget build(BuildContext context) { return Column( children: [ + const SizedBox(height: denseRowSpacing), SnapshotControlPane(controller: controller), + const SizedBox(height: denseRowSpacing), Expanded( - child: SnapshotView(item: item), + child: SnapshotView(controller: controller), ), ], ); diff --git a/packages/devtools_app/lib/src/screens/memory/panes/diff/widgets/class_details.dart b/packages/devtools_app/lib/src/screens/memory/panes/diff/widgets/class_details.dart new file mode 100644 index 00000000000..cfd9b5cd8f4 --- /dev/null +++ b/packages/devtools_app/lib/src/screens/memory/panes/diff/widgets/class_details.dart @@ -0,0 +1,24 @@ +// Copyright 2022 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/widgets.dart'; + +import '../../../shared/heap/model.dart'; + +class HeapClassDetails extends StatelessWidget { + const HeapClassDetails({Key? key, required this.heapClass}) : super(key: key); + + final HeapClass? heapClass; + + @override + Widget build(BuildContext context) { + final theClass = heapClass; + if (theClass == null) { + return const Center( + child: Text('Select class to see details here.'), + ); + } + return Center(child: Text('Details for ${theClass.fullName} will be here')); + } +} diff --git a/packages/devtools_app/lib/src/screens/memory/panes/diff/widgets/snapshot_control_pane.dart b/packages/devtools_app/lib/src/screens/memory/panes/diff/widgets/snapshot_control_pane.dart index 10f07dc25f8..654e2f80f8b 100644 --- a/packages/devtools_app/lib/src/screens/memory/panes/diff/widgets/snapshot_control_pane.dart +++ b/packages/devtools_app/lib/src/screens/memory/panes/diff/widgets/snapshot_control_pane.dart @@ -2,10 +2,13 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import '../../../../../shared/common_widgets.dart'; +import '../../../../../shared/theme.dart'; import '../controller/diff_pane_controller.dart'; +import '../controller/item_controller.dart'; class SnapshotControlPane extends StatelessWidget { const SnapshotControlPane({Key? key, required this.controller}) @@ -17,12 +20,77 @@ class SnapshotControlPane extends StatelessWidget { Widget build(BuildContext context) { return ValueListenableBuilder( valueListenable: controller.isProcessing, - builder: (_, isProcessing, __) => Row( + builder: (_, isProcessing, __) { + final current = controller.selectedItem as SnapshotListItem; + + return Row( + children: [ + const SizedBox(width: defaultSpacing), + if (!isProcessing && current.heap != null) ...[ + _DiffDropdown( + current: current, + list: controller.snapshots, + ), + const SizedBox(width: defaultSpacing), + ], + ToolbarAction( + icon: Icons.clear, + tooltip: 'Delete snapshot', + onPressed: isProcessing ? null : controller.deleteCurrentSnapshot, + ), + ], + ); + }, + ); + } +} + +class _DiffDropdown extends StatelessWidget { + _DiffDropdown({ + Key? key, + required this.list, + required this.current, + }) : super(key: key) { + final diffWith = current.diffWith.value; + // Check if diffWith was deleted from list. + if (diffWith != null && !list.value.contains(diffWith)) { + current.setDiffWith(null); + } + } + + final ValueListenable> list; + final SnapshotListItem current; + + List> items() => + list.value.where((item) => item.hasData).cast().map( + (item) { + return DropdownMenuItem( + value: item, + child: Text(item == current ? '-' : item.name), + ); + }, + ).toList(); + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: current.diffWith, + builder: (_, diffWith, __) => Row( children: [ - ToolbarAction( - icon: Icons.clear, - tooltip: 'Delete snapshot', - onPressed: isProcessing ? null : controller.deleteCurrentSnapshot, + const Text('Diff with:'), + const SizedBox(width: defaultSpacing), + RoundedDropDownButton( + isDense: true, + style: Theme.of(context).textTheme.bodyText2, + value: current.diffWith.value ?? current, + onChanged: (SnapshotListItem? value) { + if ((value ?? current) == current) { + current.setDiffWith(null); + } else { + current.setDiffWith(value); + } + }, + items: items(), ), ], ), diff --git a/packages/devtools_app/lib/src/screens/memory/panes/diff/widgets/snapshot_list.dart b/packages/devtools_app/lib/src/screens/memory/panes/diff/widgets/snapshot_list.dart index 8d0821ef6d6..1f9d4578c2f 100644 --- a/packages/devtools_app/lib/src/screens/memory/panes/diff/widgets/snapshot_list.dart +++ b/packages/devtools_app/lib/src/screens/memory/panes/diff/widgets/snapshot_list.dart @@ -9,7 +9,7 @@ import '../../../../../shared/common_widgets.dart'; import '../../../../../shared/table.dart'; import '../../../../../shared/theme.dart'; import '../controller/diff_pane_controller.dart'; -import '../controller/model.dart'; +import '../controller/item_controller.dart'; class SnapshotList extends StatelessWidget { const SnapshotList({Key? key, required this.controller}) : super(key: key); diff --git a/packages/devtools_app/lib/src/screens/memory/panes/diff/widgets/snapshot_view.dart b/packages/devtools_app/lib/src/screens/memory/panes/diff/widgets/snapshot_view.dart index 6bf528f0f86..8bfb27f3d31 100644 --- a/packages/devtools_app/lib/src/screens/memory/panes/diff/widgets/snapshot_view.dart +++ b/packages/devtools_app/lib/src/screens/memory/panes/diff/widgets/snapshot_view.dart @@ -4,32 +4,63 @@ import 'package:flutter/material.dart'; -import '../controller/model.dart'; +import '../../../../../shared/common_widgets.dart'; +import '../../../../../shared/split.dart'; +import '../../../shared/heap/model.dart'; +import '../controller/diff_pane_controller.dart'; +import '../controller/item_controller.dart'; +import 'class_details.dart'; import 'stats_table.dart'; class SnapshotView extends StatelessWidget { - const SnapshotView({Key? key, required this.item}) : super(key: key); + const SnapshotView({Key? key, required this.controller}) : super(key: key); - final SnapshotListItem item; + final DiffPaneController controller; @override Widget build(BuildContext context) { + final item = controller.selectedItem as SnapshotListItem; return ValueListenableBuilder( valueListenable: item.isProcessing, builder: (_, isProcessing, __) { if (isProcessing) return const SizedBox.shrink(); - final stats = item.heap?.stats; + late HeapStatistics? stats; + if (item.diffWith.value == null) { + stats = item.heap?.stats; + } else { + final heap1 = item.heap!; + final heap2 = item.diffWith.value!.heap!; + + // TODO(polina-c): make comparison async. + stats = controller.diffStore.compare(heap1, heap2).stats; + } + if (stats == null) { return const Center(child: Text('Could not take snapshot.')); } - return StatsTable( - // The key is passed to persist state. - key: ObjectKey(item), - data: stats, - sorting: item.sorting, - selectedRecord: item.selectedRecord, + return ValueListenableBuilder( + valueListenable: item.diffWith, + builder: (_, diffWith, __) { + return Split( + axis: Axis.horizontal, + initialFractions: const [0.5, 0.5], + minSizes: const [80, 80], + children: [ + OutlineDecoration( + child: StatsTable( + // The key is passed to persist state. + key: ObjectKey(item), + controller: controller, + ), + ), + const OutlineDecoration( + child: HeapClassDetails(heapClass: null), + ), + ], + ); + }, ); }, ); diff --git a/packages/devtools_app/lib/src/screens/memory/panes/diff/widgets/stats_table.dart b/packages/devtools_app/lib/src/screens/memory/panes/diff/widgets/stats_table.dart index 4c56f909aca..3856c09ebff 100644 --- a/packages/devtools_app/lib/src/screens/memory/panes/diff/widgets/stats_table.dart +++ b/packages/devtools_app/lib/src/screens/memory/panes/diff/widgets/stats_table.dart @@ -4,12 +4,14 @@ import 'package:flutter/material.dart'; +import '../../../../../primitives/auto_dispose_mixin.dart'; import '../../../../../primitives/utils.dart'; import '../../../../../shared/table.dart'; import '../../../../../shared/table_data.dart'; import '../../../../../shared/utils.dart'; import '../../../shared/heap/model.dart'; -import '../controller/model.dart'; +import '../controller/diff_pane_controller.dart'; +import '../controller/item_controller.dart'; class _ClassNameColumn extends ColumnData { _ClassNameColumn() @@ -34,7 +36,7 @@ class _InstanceColumn extends ColumnData { _InstanceColumn() : super( 'Non GC-able\nInstances', - titleTooltip: 'Number of instances of the class, ' + titleTooltip: 'Number of instances of the class ' 'that have a retaining path from the root.', fixedWidthPx: scaleByFontFactor(110.0), alignment: ColumnAlignment.right, @@ -111,28 +113,25 @@ class _RetainedSizeColumn extends ColumnData { class StatsTable extends StatefulWidget { const StatsTable({ Key? key, - required this.data, - required this.sorting, - required this.selectedRecord, + required this.controller, }) : super(key: key); - final HeapStatistics data; - - final ValueNotifier selectedRecord; - - final ColumnSorting sorting; + final DiffPaneController controller; @override State createState() => _StatsTableState(); } -class _StatsTableState extends State { +class _StatsTableState extends State with AutoDisposeMixin { late final List> _columns; + late final SnapshotListItem _item; @override void initState() { super.initState(); + _item = widget.controller.selectedItem as SnapshotListItem; + final _shallowSizeColumn = _ShallowSizeColumn(); _columns = >[ @@ -142,8 +141,8 @@ class _StatsTableState extends State { _RetainedSizeColumn(), ]; - if (!widget.sorting.initialized) { - widget.sorting + if (!_item.sorting.initialized) { + _item.sorting ..direction = SortDirection.descending ..columnIndex = _columns.indexOf(_shallowSizeColumn) ..initialized = true; @@ -154,20 +153,21 @@ class _StatsTableState extends State { Widget build(BuildContext context) { return FlatTable( columns: _columns, - data: widget.data.records, + data: _item.statsToShow.records, keyFactory: (e) => Key(e.heapClass.fullName), - onItemSelected: (r) => widget.selectedRecord.value = r, - selectionNotifier: widget.selectedRecord, - sortColumn: _columns[widget.sorting.columnIndex], - sortDirection: widget.sorting.direction, + onItemSelected: (r) => + widget.controller.setSelectedClass(r.heapClass.fullName), + selectionNotifier: _item.selectedRecord, + sortColumn: _columns[_item.sorting.columnIndex], + sortDirection: _item.sorting.direction, onSortChanged: ( sortColumn, direction, { secondarySortColumn, }) => setState(() { - widget.sorting.columnIndex = _columns.indexOf(sortColumn); - widget.sorting.direction = direction; + _item.sorting.columnIndex = _columns.indexOf(sortColumn); + _item.sorting.direction = direction; }), ); } diff --git a/packages/devtools_app/lib/src/screens/memory/shared/heap/heap.dart b/packages/devtools_app/lib/src/screens/memory/shared/heap/heap.dart index 39859299d2d..69ddbaef83e 100644 --- a/packages/devtools_app/lib/src/screens/memory/shared/heap/heap.dart +++ b/packages/devtools_app/lib/src/screens/memory/shared/heap/heap.dart @@ -16,13 +16,16 @@ class AdaptedHeap { HeapStatistics _heapStatistics(AdaptedHeapData data) { final result = {}; if (!data.isSpanningTreeBuilt) buildSpanningTree(data); + for (var object in data.objects) { + final heapClass = object.heapClass; + // We do not show objects that will be garbage collected soon. - if (object.retainedSize == null || object.heapClass.isSentinel) continue; + if (object.retainedSize == null || heapClass.isSentinel) continue; - final fullName = object.heapClass.fullName; + final fullName = heapClass.fullName; if (!result.containsKey(fullName)) { - result[fullName] = HeapStatsRecord(object.heapClass); + result[fullName] = HeapStatsRecord(heapClass); } final stats = result[fullName]!; stats.retainedSize += object.retainedSize ?? 0; diff --git a/packages/devtools_app/lib/src/screens/memory/shared/heap/model.dart b/packages/devtools_app/lib/src/screens/memory/shared/heap/model.dart index 238def16ae6..de742cb479f 100644 --- a/packages/devtools_app/lib/src/screens/memory/shared/heap/model.dart +++ b/packages/devtools_app/lib/src/screens/memory/shared/heap/model.dart @@ -15,24 +15,32 @@ class _JsonFields { static const String library = 'library'; static const String shallowSize = 'shallowSize'; static const String rootIndex = 'rootIndex'; + static const String created = 'created'; } /// Contains information from [HeapSnapshotGraph], /// needed for memory screen. class AdaptedHeapData { - /// Default value for rootIndex is taken from the doc: - /// https://github.com/dart-lang/sdk/blob/main/runtime/vm/service/heap_snapshot.md#object-ids - AdaptedHeapData(this.objects, {this.rootIndex = _defaultRootIndex}) - : assert(objects.isNotEmpty), - assert(objects.length > rootIndex); + AdaptedHeapData( + this.objects, { + this.rootIndex = _defaultRootIndex, + DateTime? created, + }) : assert(objects.isNotEmpty), + assert(objects.length > rootIndex) { + this.created = created ?? DateTime.now(); + } - factory AdaptedHeapData.fromJson(Map json) => - AdaptedHeapData( - (json[_JsonFields.objects] as List) - .map((e) => AdaptedHeapObject.fromJson(e)) - .toList(), - rootIndex: json[_JsonFields.rootIndex] ?? _defaultRootIndex, - ); + factory AdaptedHeapData.fromJson(Map json) { + final createdJson = json[_JsonFields.created]; + + return AdaptedHeapData( + (json[_JsonFields.objects] as List) + .map((e) => AdaptedHeapObject.fromJson(e)) + .toList(), + created: createdJson == null ? null : DateTime.parse(createdJson), + rootIndex: json[_JsonFields.rootIndex] ?? _defaultRootIndex, + ); + } factory AdaptedHeapData.fromHeapSnapshot(HeapSnapshotGraph graph) => AdaptedHeapData( @@ -41,15 +49,19 @@ class AdaptedHeapData { .toList(), ); + /// Default value for rootIndex is taken from the doc: + /// https://github.com/dart-lang/sdk/blob/main/runtime/vm/service/heap_snapshot.md#object-ids static const int _defaultRootIndex = 1; final int rootIndex; + AdaptedHeapObject get root => objects[rootIndex]; + final List objects; bool isSpanningTreeBuilt = false; - AdaptedHeapObject get root => objects[rootIndex]; + late DateTime created; /// Heap objects by identityHashCode. late final Map _objectsByCode = Map.fromIterable( @@ -61,6 +73,7 @@ class AdaptedHeapData { Map toJson() => { _JsonFields.objects: objects.map((e) => e.toJson()).toList(), _JsonFields.rootIndex: rootIndex, + _JsonFields.created: created.toIso8601String(), }; HeapPath? _retainingPath(IdentityHashCode code) { @@ -93,7 +106,7 @@ class AdaptedHeapData { } } -/// Result of invocation of [inentityHashCode()]. +/// Result of invocation of [identityHashCode]. typedef IdentityHashCode = int; /// Sequence of ids of objects in the heap. @@ -165,12 +178,20 @@ class HeapStatsRecord { HeapStatsRecord(this.heapClass); final HeapClass heapClass; + int instanceCount = 0; int shallowSize = 0; int retainedSize = 0; - int instanceCount = 0; + + HeapStatsRecord negative() => HeapStatsRecord(heapClass) + ..instanceCount = -instanceCount + ..shallowSize = -shallowSize + ..retainedSize = -retainedSize; + + bool get isZero => + shallowSize == 0 && retainedSize == 0 && instanceCount == 0; } -/// This class is needed to make snapshot taking mockable. +/// This class is needed to make the snapshot taking operation mockable. class SnapshotTaker { Future take() async { final snapshot = await snapshotMemory(); @@ -214,9 +235,10 @@ class HeapClass { } class HeapStatistics { - HeapStatistics(this.map); + HeapStatistics(this.recordsByClass); /// Maps full class name to stats record of this class. - final Map map; - late final List records = map.values.toList(growable: false); + final Map recordsByClass; + late final List records = + recordsByClass.values.toList(growable: false); } diff --git a/packages/devtools_app/test/goldens/memory_diff_three_snapshots.png b/packages/devtools_app/test/goldens/memory_diff_three_snapshots.png index bec68ebb233..14da38ecf15 100644 Binary files a/packages/devtools_app/test/goldens/memory_diff_three_snapshots.png and b/packages/devtools_app/test/goldens/memory_diff_three_snapshots.png differ diff --git a/packages/devtools_app/test/memory/diff/heap_diff_test.dart b/packages/devtools_app/test/memory/diff/heap_diff_test.dart new file mode 100644 index 00000000000..d91f9c47cab --- /dev/null +++ b/packages/devtools_app/test/memory/diff/heap_diff_test.dart @@ -0,0 +1,41 @@ +// Copyright 2022 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:devtools_app/src/screens/memory/panes/diff/controller/heap_diff.dart'; +import 'package:devtools_app/src/screens/memory/shared/heap/heap.dart'; +import 'package:devtools_app/src/screens/memory/shared/heap/model.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('$HeapDiffStore does not create new $HeapComparison for the same couple', + () { + final heap1 = _createHeap(); + final heap2 = _createHeap(); + + expect(heap1 == heap2, false); + + final store = HeapDiffStore(); + + final couple1 = identityHashCode(store.compare(heap1, heap2)); + final couple2 = identityHashCode(store.compare(heap1, heap2)); + final couple3 = identityHashCode(store.compare(heap2, heap1)); + + expect(couple1, couple2); + expect(couple1, couple3); + }); +} + +AdaptedHeap _createHeap() => AdaptedHeap( + AdaptedHeapData( + [ + AdaptedHeapObject( + code: 0, + references: [], + heapClass: HeapClass(className: 'className', library: 'library'), + shallowSize: 1, + ) + ], + rootIndex: 0, + ), + ); diff --git a/packages/devtools_app/test/memory/shared/model_test.dart b/packages/devtools_app/test/memory/shared/model_test.dart index 5fa7a4934c6..5208e17dec6 100644 --- a/packages/devtools_app/test/memory/shared/model_test.dart +++ b/packages/devtools_app/test/memory/shared/model_test.dart @@ -20,6 +20,7 @@ void main() { ) ], rootIndex: 0, + created: DateTime(2000), ).toJson(); expect(json, AdaptedHeapData.fromJson(json).toJson());