From 44f225f1f22359f28352771827a674f77b65dec8 Mon Sep 17 00:00:00 2001 From: khanak0509 Date: Fri, 15 May 2026 14:34:04 +0530 Subject: [PATCH 1/4] Add pin rows to Profile Memory table --- .../screens/memory/panes/profile/model.dart | 70 +++++++- .../profile/profile_pane_controller.dart | 69 +++++++- .../memory/panes/profile/profile_view.dart | 155 +++++++++++++----- .../constants/_memory_constants.dart | 1 + .../release_notes/NEXT_RELEASE_NOTES.md | 2 +- .../allocation_profile_table_view_test.dart | 44 +++++ 6 files changed, 287 insertions(+), 54 deletions(-) diff --git a/packages/devtools_app/lib/src/screens/memory/panes/profile/model.dart b/packages/devtools_app/lib/src/screens/memory/panes/profile/model.dart index b1c412b5724..7bdb02e31bb 100644 --- a/packages/devtools_app/lib/src/screens/memory/panes/profile/model.dart +++ b/packages/devtools_app/lib/src/screens/memory/panes/profile/model.dart @@ -34,13 +34,19 @@ class AdaptedProfile with Serializable { factory AdaptedProfile.fromAllocationProfile( AllocationProfile profile, ClassFilter filter, - String? rootPackage, - ) { + String? rootPackage, { + Set pinnedClassFullNames = const {}, + }) { final adaptedProfile = AdaptedProfile._( total: ProfileRecord.total(profile), items: (profile.members ?? []) .where((e) => (e.instancesCurrent ?? 0) > 0) - .map((e) => ProfileRecord.fromClassHeapStats(e)) + .map((e) => ProfileRecord.fromClassHeapStats( + e, + userPinned: pinnedClassFullNames.contains( + HeapClassName.fromClassRef(e.classRef).fullName, + ), + )) .toList(), newSpaceGCStats: profile.newSpaceGCStats, oldSpaceGCStats: profile.oldSpaceGCStats, @@ -70,6 +76,29 @@ class AdaptedProfile with Serializable { ); } + /// Returns a copy of [profile] with [pinnedClassFullNames] applied to items. + factory AdaptedProfile.withPinnedClasses( + AdaptedProfile profile, + Set pinnedClassFullNames, + String? rootPackage, + ) { + final itemsWithPins = profile._items + .map( + (record) => record.copyWith( + userPinned: pinnedClassFullNames.contains(record.heapClass.fullName), + ), + ) + .toList(); + final updated = AdaptedProfile._( + total: profile._total, + items: itemsWithPins, + newSpaceGCStats: profile.newSpaceGCStats, + oldSpaceGCStats: profile.oldSpaceGCStats, + totalGCStats: profile.totalGCStats, + ); + return AdaptedProfile.withNewFilter(updated, profile.filter, rootPackage); + } + factory AdaptedProfile.fromJson(Map json) { return AdaptedProfile._( total: ProfileRecord.fromJson(json[_ProfileJson.total]), @@ -117,6 +146,7 @@ class AdaptedProfile with Serializable { class _RecordJson { static const isTotal = 'it'; static const heapClass = 'c'; + static const userPinned = 'p'; static const totalInstances = 'ti'; static const totalSize = 'ts'; static const totalDartHeapSize = 'tds'; @@ -135,6 +165,7 @@ class ProfileRecord with PinnableListEntry, Serializable { ProfileRecord._({ required this.isTotal, required this.heapClass, + this.userPinned = false, required this.totalInstances, required this.totalSize, required this.totalDartHeapSize, @@ -151,7 +182,10 @@ class ProfileRecord with PinnableListEntry, Serializable { _verifyIntegrity(); } - factory ProfileRecord.fromClassHeapStats(ClassHeapStats stats) { + factory ProfileRecord.fromClassHeapStats( + ClassHeapStats stats, { + bool userPinned = false, + }) { assert( stats.bytesCurrent! == stats.newSpace.size + stats.oldSpace.size, '${stats.bytesCurrent}, ${stats.newSpace.size}, ${stats.oldSpace.size}', @@ -159,6 +193,7 @@ class ProfileRecord with PinnableListEntry, Serializable { return ProfileRecord._( isTotal: false, heapClass: HeapClassName.fromClassRef(stats.classRef), + userPinned: userPinned, totalInstances: stats.instancesCurrent ?? 0, totalSize: stats.bytesCurrent! + @@ -180,6 +215,7 @@ class ProfileRecord with PinnableListEntry, Serializable { ProfileRecord.total(AllocationProfile profile) : isTotal = true, + userPinned = false, heapClass = HeapClassName.fromPath(className: 'All Classes', library: ''), totalInstances = null, totalSize = @@ -202,6 +238,7 @@ class ProfileRecord with PinnableListEntry, Serializable { return ProfileRecord._( isTotal: json[_RecordJson.isTotal] as bool, heapClass: HeapClassName.fromJson(json[_RecordJson.heapClass]), + userPinned: json[_RecordJson.userPinned] as bool? ?? false, totalInstances: json[_RecordJson.totalInstances] as int?, totalSize: json[_RecordJson.totalSize] as int, totalDartHeapSize: json[_RecordJson.totalDartHeapSize] as int, @@ -217,11 +254,32 @@ class ProfileRecord with PinnableListEntry, Serializable { ); } + ProfileRecord copyWith({bool? userPinned}) { + return ProfileRecord._( + isTotal: isTotal, + heapClass: heapClass, + userPinned: userPinned ?? this.userPinned, + totalInstances: totalInstances, + totalSize: totalSize, + totalDartHeapSize: totalDartHeapSize, + totalExternalSize: totalExternalSize, + newSpaceInstances: newSpaceInstances, + newSpaceSize: newSpaceSize, + newSpaceDartHeapSize: newSpaceDartHeapSize, + newSpaceExternalSize: newSpaceExternalSize, + oldSpaceInstances: oldSpaceInstances, + oldSpaceSize: oldSpaceSize, + oldSpaceDartHeapSize: oldSpaceDartHeapSize, + oldSpaceExternalSize: oldSpaceExternalSize, + ); + } + @override Map toJson() { return { _RecordJson.isTotal: isTotal, _RecordJson.heapClass: heapClass, + _RecordJson.userPinned: userPinned, _RecordJson.totalInstances: totalInstances, _RecordJson.totalSize: totalSize, _RecordJson.totalDartHeapSize: totalDartHeapSize, @@ -239,6 +297,8 @@ class ProfileRecord with PinnableListEntry, Serializable { final bool isTotal; + final bool userPinned; + final HeapClassName heapClass; final int? totalInstances; @@ -257,7 +317,7 @@ class ProfileRecord with PinnableListEntry, Serializable { final int? oldSpaceExternalSize; @override - bool get pinToTop => isTotal; + bool get pinToTop => isTotal || userPinned; void _verifyIntegrity() { assert(() { diff --git a/packages/devtools_app/lib/src/screens/memory/panes/profile/profile_pane_controller.dart b/packages/devtools_app/lib/src/screens/memory/panes/profile/profile_pane_controller.dart index d469c23a8ab..eade6c301d8 100644 --- a/packages/devtools_app/lib/src/screens/memory/panes/profile/profile_pane_controller.dart +++ b/packages/devtools_app/lib/src/screens/memory/panes/profile/profile_pane_controller.dart @@ -11,20 +11,32 @@ import 'package:vm_service/vm_service.dart'; import '../../../../shared/config_specific/import_export/import_export.dart'; import '../../../../shared/globals.dart'; +import '../../../../shared/memory/class_name.dart'; import '../../shared/heap/class_filter.dart'; import 'model.dart'; @visibleForTesting -enum Json { profile, rootPackage } +enum Json { profile, rootPackage, pinnedClasses } class ProfilePaneController extends DisposableController with AutoDisposeControllerMixin, Serializable { - ProfilePaneController({required this.rootPackage, AdaptedProfile? profile}) { + ProfilePaneController({ + required this.rootPackage, + AdaptedProfile? profile, + Set? pinnedClassFullNames, + }) { + if (pinnedClassFullNames != null) { + _pinnedClassFullNames.addAll(pinnedClassFullNames); + } // [profile] should only be non-null when loading offline data. if (profile != null) { - _currentAllocationProfile.value = AdaptedProfile.withNewFilter( - profile, - classFilter.value, + _currentAllocationProfile.value = AdaptedProfile.withPinnedClasses( + AdaptedProfile.withNewFilter( + profile, + classFilter.value, + rootPackage, + ), + _pinnedClassFullNames, rootPackage, ); } @@ -34,6 +46,9 @@ class ProfilePaneController extends DisposableController return ProfilePaneController( profile: deserialize(json[Json.profile.name], AdaptedProfile.fromJson), rootPackage: json[Json.rootPackage.name], + pinnedClassFullNames: (json[Json.pinnedClasses.name] as List?) + ?.cast() + .toSet(), ); } @@ -42,11 +57,43 @@ class ProfilePaneController extends DisposableController return { Json.profile.name: _currentAllocationProfile.value, Json.rootPackage.name: rootPackage, + Json.pinnedClasses.name: _pinnedClassFullNames.toList(), }; } bool _initialized = false; + final _pinnedClassFullNames = {}; + + /// Classes pinned to the top of the Profile Memory table. + ValueListenable> get pinnedClassFullNames => + _pinnedClassFullNamesListenable; + final _pinnedClassFullNamesListenable = ValueNotifier>({}); + + bool isPinned(HeapClassName heapClass) => + _pinnedClassFullNames.contains(heapClass.fullName); + + void togglePin(HeapClassName heapClass) { + final key = heapClass.fullName; + if (_pinnedClassFullNames.contains(key)) { + _pinnedClassFullNames.remove(key); + } else { + _pinnedClassFullNames.add(key); + } + _pinnedClassFullNamesListenable.value = Set.of(_pinnedClassFullNames); + _reapplyPinnedState(); + } + + void _reapplyPinnedState() { + final currentProfile = _currentAllocationProfile.value; + if (currentProfile == null) return; + _currentAllocationProfile.value = AdaptedProfile.withPinnedClasses( + currentProfile, + _pinnedClassFullNames, + rootPackage, + ); + } + /// Initializes the controller if it is not initialized yet. @override void init() { @@ -84,6 +131,7 @@ class ProfilePaneController extends DisposableController profile, classFilter.value, rootPackage, + pinnedClassFullNames: _pinnedClassFullNames, ); _initializeSelection(); } @@ -105,9 +153,13 @@ class ProfilePaneController extends DisposableController _classFilter.value = filter; final currentProfile = _currentAllocationProfile.value; if (currentProfile == null) return; - _currentAllocationProfile.value = AdaptedProfile.withNewFilter( - currentProfile, - classFilter.value, + _currentAllocationProfile.value = AdaptedProfile.withPinnedClasses( + AdaptedProfile.withNewFilter( + currentProfile, + classFilter.value, + rootPackage, + ), + _pinnedClassFullNames, rootPackage, ); } @@ -210,6 +262,7 @@ class ProfilePaneController extends DisposableController _currentAllocationProfile.dispose(); _classFilter.dispose(); _refreshOnGc.dispose(); + _pinnedClassFullNamesListenable.dispose(); selection.dispose(); super.dispose(); } diff --git a/packages/devtools_app/lib/src/screens/memory/panes/profile/profile_view.dart b/packages/devtools_app/lib/src/screens/memory/panes/profile/profile_view.dart index 7325b420604..f7d81cd3d9c 100644 --- a/packages/devtools_app/lib/src/screens/memory/panes/profile/profile_view.dart +++ b/packages/devtools_app/lib/src/screens/memory/panes/profile/profile_view.dart @@ -32,6 +32,56 @@ import 'profile_pane_controller.dart'; /// instances, memory). const _defaultNumberFieldWidth = 80.0; +class _PinColumn extends ColumnData + implements ColumnRenderer { + _PinColumn({required this.controller}) + : super( + 'Pin', + titleTooltip: 'Pin class to the top of the table', + fixedWidthPx: 40.0, + alignment: ColumnAlignment.left, + ); + + final ProfilePaneController controller; + + @override + bool get supportsSorting => false; + + @override + Widget build( + BuildContext context, + ProfileRecord item, { + bool isRowSelected = false, + bool isRowHovered = false, + }) { + if (item.isTotal) return const SizedBox.shrink(); + + final pinned = item.userPinned; + return IconButton( + visualDensity: VisualDensity.compact, + padding: EdgeInsets.zero, + constraints: const BoxConstraints(minWidth: 32, minHeight: 32), + iconSize: 18, + icon: Icon(pinned ? Icons.push_pin : Icons.push_pin_outlined), + tooltip: pinned ? 'Unpin class' : 'Pin class to top', + onPressed: () { + ga.select( + gac.memory, + '${gac.MemoryEvents.profilePinClass.name}-$pinned', + ); + controller.togglePin(item.heapClass); + }, + ); + } + + @override + bool? getValue(ProfileRecord _) => null; + + @override + int compare(ProfileRecord a, ProfileRecord b) => + a.userPinned.boolCompare(b.userPinned); +} + class _FieldClassNameColumn extends ColumnData implements ColumnRenderer, @@ -473,46 +523,47 @@ class AllocationProfileTableViewState ); }, ), - Expanded(child: _AllocationProfileTable(controller: widget.controller)), + Expanded( + child: _AllocationProfileTable(controller: widget.controller), + ), ], ); } } -class _AllocationProfileTable extends StatelessWidget { - _AllocationProfileTable({required this.controller}); +class _AllocationProfileTable extends StatefulWidget { + const _AllocationProfileTable({required this.controller}); + + final ProfilePaneController controller; + + @override + State<_AllocationProfileTable> createState() => + _AllocationProfileTableState(); +} +class _AllocationProfileTableState extends State<_AllocationProfileTable> { /// List of columns displayed in advanced developer mode state. static final _vmModeColumnGroups = [ ColumnGroup.fromText(title: '', range: const Range(0, 1)), + ColumnGroup.fromText(title: '', range: const Range(1, 2)), ColumnGroup.fromText( title: HeapGeneration.total.toString(), - range: const Range(1, 5), + range: const Range(2, 6), ), ColumnGroup.fromText( title: HeapGeneration.newSpace.toString(), - range: const Range(5, 9), + range: const Range(6, 10), ), ColumnGroup.fromText( title: HeapGeneration.oldSpace.toString(), - range: const Range(9, 13), + range: const Range(10, 14), ), ]; static const _fieldSizeColumn = _FieldSizeColumn(heap: HeapGeneration.total); - late final _columns = >[ - _FieldClassNameColumn( - ClassFilterData( - filter: controller.classFilter, - onChanged: controller.setFilter, - rootPackage: controller.rootPackage, - ), - ), - const _FieldInstanceCountColumn(heap: HeapGeneration.total), - _fieldSizeColumn, - _FieldDartHeapSizeColumn(heap: HeapGeneration.total), - ]; + late final _PinColumn _pinColumn; + late final List> _columns; late final _advancedDeveloperModeColumns = [ const _FieldExternalSizeColumn(heap: HeapGeneration.total), @@ -526,12 +577,29 @@ class _AllocationProfileTable extends StatelessWidget { const _FieldExternalSizeColumn(heap: HeapGeneration.oldSpace), ]; - final ProfilePaneController controller; + @override + void initState() { + super.initState(); + _pinColumn = _PinColumn(controller: widget.controller); + _columns = >[ + _pinColumn, + _FieldClassNameColumn( + ClassFilterData( + filter: widget.controller.classFilter, + onChanged: widget.controller.setFilter, + rootPackage: widget.controller.rootPackage, + ), + ), + const _FieldInstanceCountColumn(heap: HeapGeneration.total), + _fieldSizeColumn, + _FieldDartHeapSizeColumn(heap: HeapGeneration.total), + ]; + } @override Widget build(BuildContext context) { return ValueListenableBuilder( - valueListenable: controller.currentAllocationProfile, + valueListenable: widget.controller.currentAllocationProfile, builder: (context, profile, _) { // TODO(bkonyi): make this an overlay so the table doesn't // disappear when we're retrieving new data, especially since the @@ -539,26 +607,33 @@ class _AllocationProfileTable extends StatelessWidget { if (profile == null) { return const CenteredCircularProgressIndicator(); } - return ValueListenableBuilder( - valueListenable: preferences.advancedDeveloperModeEnabled, - builder: (context, advancedDeveloperModeEnabled, _) { - return FlatTable( - keyFactory: (element) => Key(element.heapClass.fullName), - data: profile.records, - dataKey: 'allocation-profile', - columnGroups: advancedDeveloperModeEnabled - ? _AllocationProfileTable._vmModeColumnGroups - : null, - columns: [ - ..._columns, - if (advancedDeveloperModeEnabled) - ..._advancedDeveloperModeColumns, - ], - defaultSortColumn: _AllocationProfileTable._fieldSizeColumn, - defaultSortDirection: SortDirection.descending, - pinBehavior: FlatTablePinBehavior.pinOriginalToTop, - includeColumnGroupHeaders: false, - selectionNotifier: controller.selection, + return ValueListenableBuilder>( + valueListenable: widget.controller.pinnedClassFullNames, + builder: (context, pinnedClassFullNames, _) { + return ValueListenableBuilder( + valueListenable: preferences.advancedDeveloperModeEnabled, + builder: (context, advancedDeveloperModeEnabled, _) { + return FlatTable( + keyFactory: (element) => Key(element.heapClass.fullName), + data: profile.records, + dataKey: + 'allocation-profile-${pinnedClassFullNames.length}-' + '${pinnedClassFullNames.toList()..sort()}', + columnGroups: advancedDeveloperModeEnabled + ? _AllocationProfileTableState._vmModeColumnGroups + : null, + columns: [ + ..._columns, + if (advancedDeveloperModeEnabled) + ..._advancedDeveloperModeColumns, + ], + defaultSortColumn: _AllocationProfileTableState._fieldSizeColumn, + defaultSortDirection: SortDirection.descending, + pinBehavior: FlatTablePinBehavior.pinOriginalToTop, + includeColumnGroupHeaders: false, + selectionNotifier: widget.controller.selection, + ); + }, ); }, ); diff --git a/packages/devtools_app/lib/src/shared/analytics/constants/_memory_constants.dart b/packages/devtools_app/lib/src/shared/analytics/constants/_memory_constants.dart index 6549ec693aa..bce8a49ae29 100644 --- a/packages/devtools_app/lib/src/shared/analytics/constants/_memory_constants.dart +++ b/packages/devtools_app/lib/src/shared/analytics/constants/_memory_constants.dart @@ -53,6 +53,7 @@ enum MemoryEvents { profileHelp, profileRefreshManual, profileRefreshOnGc, + profilePinClass, // 'Tracing' tab events tracingClear, diff --git a/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md b/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md index 1f6fafdf6fd..ed5e391462b 100644 --- a/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md +++ b/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md @@ -34,7 +34,7 @@ TODO: Remove this section if there are not any updates. ## Memory updates -TODO: Remove this section if there are not any updates. +* Added the ability to pin classes to the top of the Profile Memory table. [#8898](https://github.com/flutter/devtools/issues/8898) ## Debugger updates diff --git a/packages/devtools_app/test/screens/memory/profile/allocation_profile_table_view_test.dart b/packages/devtools_app/test/screens/memory/profile/allocation_profile_table_view_test.dart index c3824f0e1ae..455351e1a20 100644 --- a/packages/devtools_app/test/screens/memory/profile/allocation_profile_table_view_test.dart +++ b/packages/devtools_app/test/screens/memory/profile/allocation_profile_table_view_test.dart @@ -319,5 +319,49 @@ void main() { lastValue = internalSize; } }); + + testWidgetsWithWindowSize('pins class to top of table', windowSize, ( + WidgetTester tester, + ) async { + await scene.pump(tester); + + final allocationProfileController = scene.controller.profile!; + await navigateToAllocationProfile(tester, allocationProfileController); + + final table = find.byType(FlatTable); + expect(table, findsOneWidget); + + final state = tester.state>(table.first); + + // "All Classes" is always pinned via [ProfileRecord.isTotal]. + expect(state.tableController.pinnedData, isNotEmpty); + expect( + state.tableController.pinnedData.every((record) => !record.userPinned), + isTrue, + ); + + final pinButtons = find.byIcon(Icons.push_pin_outlined); + expect(pinButtons, findsWidgets); + + await tester.tap(pinButtons.first); + await tester.pumpAndSettle(); + + expect(find.byIcon(Icons.push_pin), findsWidgets); + expect( + state.tableController.pinnedData.any((record) => record.userPinned), + isTrue, + ); + expect( + allocationProfileController.pinnedClassFullNames.value, + isNotEmpty, + ); + + // Pinned class stays at the top after sorting by class name. + await tester.tap(find.text('Class')); + await tester.pumpAndSettle(); + + final pinnedData = state.tableController.pinnedData; + expect(pinnedData.any((record) => record.userPinned), isTrue); + }); }); } From e9ae7b8bfe48f18c6a26d36844cfbc3617013b63 Mon Sep 17 00:00:00 2001 From: Khanak Khandelwal Date: Fri, 15 May 2026 16:04:23 +0530 Subject: [PATCH 2/4] Update packages/devtools_app/lib/src/screens/memory/panes/profile/profile_pane_controller.dart Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- .../screens/memory/panes/profile/profile_pane_controller.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/devtools_app/lib/src/screens/memory/panes/profile/profile_pane_controller.dart b/packages/devtools_app/lib/src/screens/memory/panes/profile/profile_pane_controller.dart index eade6c301d8..c709c283d5f 100644 --- a/packages/devtools_app/lib/src/screens/memory/panes/profile/profile_pane_controller.dart +++ b/packages/devtools_app/lib/src/screens/memory/panes/profile/profile_pane_controller.dart @@ -27,6 +27,7 @@ class ProfilePaneController extends DisposableController }) { if (pinnedClassFullNames != null) { _pinnedClassFullNames.addAll(pinnedClassFullNames); + _pinnedClassFullNamesListenable.value = pinnedClassFullNames; } // [profile] should only be non-null when loading offline data. if (profile != null) { From e3544f20ec66a02a85472e3755ca1b0c1588cec3 Mon Sep 17 00:00:00 2001 From: Khanak Khandelwal Date: Fri, 15 May 2026 16:04:34 +0530 Subject: [PATCH 3/4] Update packages/devtools_app/lib/src/screens/memory/panes/profile/profile_view.dart Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- .../lib/src/screens/memory/panes/profile/profile_view.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/devtools_app/lib/src/screens/memory/panes/profile/profile_view.dart b/packages/devtools_app/lib/src/screens/memory/panes/profile/profile_view.dart index f7d81cd3d9c..55e8aa25efc 100644 --- a/packages/devtools_app/lib/src/screens/memory/panes/profile/profile_view.dart +++ b/packages/devtools_app/lib/src/screens/memory/panes/profile/profile_view.dart @@ -562,8 +562,8 @@ class _AllocationProfileTableState extends State<_AllocationProfileTable> { static const _fieldSizeColumn = _FieldSizeColumn(heap: HeapGeneration.total); - late final _PinColumn _pinColumn; - late final List> _columns; + late _PinColumn _pinColumn; + late List> _columns; late final _advancedDeveloperModeColumns = [ const _FieldExternalSizeColumn(heap: HeapGeneration.total), From 1ba1b5c989da1b8fd2199df14e2944c6db36a5c8 Mon Sep 17 00:00:00 2001 From: khanak0509 Date: Fri, 15 May 2026 16:13:04 +0530 Subject: [PATCH 4/4] fix issues --- .../profile/profile_pane_controller.dart | 1 + .../memory/panes/profile/profile_view.dart | 59 ++++++++++--------- 2 files changed, 33 insertions(+), 27 deletions(-) diff --git a/packages/devtools_app/lib/src/screens/memory/panes/profile/profile_pane_controller.dart b/packages/devtools_app/lib/src/screens/memory/panes/profile/profile_pane_controller.dart index c709c283d5f..6e51caf8a39 100644 --- a/packages/devtools_app/lib/src/screens/memory/panes/profile/profile_pane_controller.dart +++ b/packages/devtools_app/lib/src/screens/memory/panes/profile/profile_pane_controller.dart @@ -29,6 +29,7 @@ class ProfilePaneController extends DisposableController _pinnedClassFullNames.addAll(pinnedClassFullNames); _pinnedClassFullNamesListenable.value = pinnedClassFullNames; } + _pinnedClassFullNamesListenable.value = Set.of(_pinnedClassFullNames); // [profile] should only be non-null when loading offline data. if (profile != null) { _currentAllocationProfile.value = AdaptedProfile.withPinnedClasses( diff --git a/packages/devtools_app/lib/src/screens/memory/panes/profile/profile_view.dart b/packages/devtools_app/lib/src/screens/memory/panes/profile/profile_view.dart index 55e8aa25efc..93f8a8aed23 100644 --- a/packages/devtools_app/lib/src/screens/memory/panes/profile/profile_view.dart +++ b/packages/devtools_app/lib/src/screens/memory/panes/profile/profile_view.dart @@ -580,6 +580,18 @@ class _AllocationProfileTableState extends State<_AllocationProfileTable> { @override void initState() { super.initState(); + _initColumns(); + } + + @override + void didUpdateWidget(covariant _AllocationProfileTable oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.controller != widget.controller) { + _initColumns(); + } + } + + void _initColumns() { _pinColumn = _PinColumn(controller: widget.controller); _columns = >[ _pinColumn, @@ -607,33 +619,26 @@ class _AllocationProfileTableState extends State<_AllocationProfileTable> { if (profile == null) { return const CenteredCircularProgressIndicator(); } - return ValueListenableBuilder>( - valueListenable: widget.controller.pinnedClassFullNames, - builder: (context, pinnedClassFullNames, _) { - return ValueListenableBuilder( - valueListenable: preferences.advancedDeveloperModeEnabled, - builder: (context, advancedDeveloperModeEnabled, _) { - return FlatTable( - keyFactory: (element) => Key(element.heapClass.fullName), - data: profile.records, - dataKey: - 'allocation-profile-${pinnedClassFullNames.length}-' - '${pinnedClassFullNames.toList()..sort()}', - columnGroups: advancedDeveloperModeEnabled - ? _AllocationProfileTableState._vmModeColumnGroups - : null, - columns: [ - ..._columns, - if (advancedDeveloperModeEnabled) - ..._advancedDeveloperModeColumns, - ], - defaultSortColumn: _AllocationProfileTableState._fieldSizeColumn, - defaultSortDirection: SortDirection.descending, - pinBehavior: FlatTablePinBehavior.pinOriginalToTop, - includeColumnGroupHeaders: false, - selectionNotifier: widget.controller.selection, - ); - }, + return ValueListenableBuilder( + valueListenable: preferences.advancedDeveloperModeEnabled, + builder: (context, advancedDeveloperModeEnabled, _) { + return FlatTable( + keyFactory: (element) => Key(element.heapClass.fullName), + data: profile.records, + dataKey: 'allocation-profile', + columnGroups: advancedDeveloperModeEnabled + ? _AllocationProfileTableState._vmModeColumnGroups + : null, + columns: [ + ..._columns, + if (advancedDeveloperModeEnabled) + ..._advancedDeveloperModeColumns, + ], + defaultSortColumn: _AllocationProfileTableState._fieldSizeColumn, + defaultSortDirection: SortDirection.descending, + pinBehavior: FlatTablePinBehavior.pinOriginalToTop, + includeColumnGroupHeaders: false, + selectionNotifier: widget.controller.selection, ); }, );