diff --git a/packages/devtools_app/lib/src/screens/memory/DEPENDENCIES.md b/packages/devtools_app/lib/src/screens/memory/DEPENDENCIES.md index 64f82950ba2..6642a9585e1 100644 --- a/packages/devtools_app/lib/src/screens/memory/DEPENDENCIES.md +++ b/packages/devtools_app/lib/src/screens/memory/DEPENDENCIES.md @@ -6,6 +6,7 @@ Dependencies that create loops (inversions) are marked with `!`. ```mermaid flowchart TD; framework-->panes; +framework-->shared; panes-->shared; ``` diff --git a/packages/devtools_app/lib/src/screens/memory/framework/DEPENDENCIES.md b/packages/devtools_app/lib/src/screens/memory/framework/DEPENDENCIES.md index c5fd0663742..60293457965 100644 --- a/packages/devtools_app/lib/src/screens/memory/framework/DEPENDENCIES.md +++ b/packages/devtools_app/lib/src/screens/memory/framework/DEPENDENCIES.md @@ -5,6 +5,7 @@ Dependencies that create loops (inversions) are marked with `!`. ```mermaid flowchart TD; +memory_controller.dart-->offline_data; memory_screen.dart-->screen_body.dart; memory_tabs.dart-->memory_controller.dart; screen_body.dart-->memory_controller.dart; diff --git a/packages/devtools_app/lib/src/screens/memory/framework/memory_controller.dart b/packages/devtools_app/lib/src/screens/memory/framework/memory_controller.dart index 7ecda502402..39b30441628 100644 --- a/packages/devtools_app/lib/src/screens/memory/framework/memory_controller.dart +++ b/packages/devtools_app/lib/src/screens/memory/framework/memory_controller.dart @@ -2,43 +2,54 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:async'; + import 'package:devtools_app_shared/utils.dart'; import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; import '../../../shared/memory/class_name.dart'; import '../../../shared/memory/heap_graph_loader.dart'; +import '../../../shared/offline_data.dart'; import '../../../shared/primitives/simple_items.dart'; +import '../../../shared/screen.dart'; import '../../../shared/utils.dart'; import '../panes/chart/controller/chart_pane_controller.dart'; import '../panes/control/controller/control_pane_controller.dart'; import '../panes/diff/controller/diff_pane_controller.dart'; import '../panes/profile/profile_pane_controller.dart'; import '../panes/tracing/tracing_pane_controller.dart'; +import 'offline_data/offline_data.dart'; -/// This class contains the business logic for memory screen, for a connected -/// application. +/// This class contains the business logic for memory screen. /// /// This class must not have direct dependencies on web-only libraries. This /// allows tests of the complicated logic in this class to run on the VM. /// /// The controller should be recreated for every new connection. class MemoryController extends DisposableController - with AutoDisposeControllerMixin { + with + AutoDisposeControllerMixin, + OfflineScreenControllerMixin { MemoryController({ - @visibleForTesting DiffPaneController? diffPaneController, - @visibleForTesting ProfilePaneController? profilePaneController, + @visibleForTesting DiffPaneController? connectedDiff, + @visibleForTesting ProfilePaneController? connectedProfile, }) { - diff = diffPaneController ?? _createDiffController(); - profile = profilePaneController ?? ProfilePaneController(); - - shareClassFilterBetweenProfileAndDiff(); + if (connectedDiff != null || connectedProfile != null) { + _mode = DevToolsMode.connected; + } else { + _mode = devToolsMode; + } + unawaited(_init(connectedDiff, connectedProfile)); } + Future get initialized => _initialized.future; + final _initialized = Completer(); + /// DevTools mode at the time of creation of the controller. /// /// DevTools will recreate controller when the mode changes. - // ignore: unused_field, TODO(polina-c): https://github.com/flutter/devtools/issues/6972 - final DevToolsMode _mode = devToolsMode; + late final DevToolsMode _mode; /// Index of the selected feature tab. /// @@ -48,12 +59,15 @@ class MemoryController extends DisposableController /// instead of the widget state. int selectedFeatureTabIndex = 0; - late DiffPaneController diff; - late ProfilePaneController profile; - late MemoryChartPaneController chart = MemoryChartPaneController(); - TracingPaneController tracing = TracingPaneController(); - late final MemoryControlPaneController control = - MemoryControlPaneController(chart.memoryTimeline); + late final DiffPaneController diff; + + late final ProfilePaneController profile; + + late final MemoryChartPaneController chart; + + late final TracingPaneController tracing; + + late final MemoryControlPaneController control; @override void dispose() { @@ -65,26 +79,85 @@ class MemoryController extends DisposableController profile.dispose(); } - DiffPaneController _createDiffController() => - DiffPaneController(HeapGraphLoaderRuntime(chart.memoryTimeline)); + Future _init( + @visibleForTesting DiffPaneController? connectedDiff, + @visibleForTesting ProfilePaneController? connectedProfile, + ) async { + assert(!_initialized.isCompleted); + switch (_mode) { + case DevToolsMode.disconnected: + // TODO(polina-c): load memory screen in disconnected mode, https://github.com/flutter/devtools/issues/6972 + _initializeData(); + case DevToolsMode.connected: + _initializeData( + diffPaneController: connectedDiff, + profilePaneController: connectedProfile, + ); + case DevToolsMode.offlineData: + assert(connectedDiff == null && connectedProfile == null); + await maybeLoadOfflineData( + ScreenMetaData.memory.id, + createData: (json) => OfflineMemoryData.parse(json), + shouldLoad: (data) => true, + ); + // [maybeLoadOfflineData] will be a noop if there is no offline data for the memory screen, + // so ensure we still call [_initializedData] if it has not been called. + if (!_initialized.isCompleted) _initializeData(); + assert(_initialized.isCompleted); + } + assert(_initialized.isCompleted); + } - void reset() { - diff.dispose(); - diff = _createDiffController(); + void _initializeData({ + OfflineMemoryData? offlineData, + @visibleForTesting DiffPaneController? diffPaneController, + @visibleForTesting ProfilePaneController? profilePaneController, + }) { + assert(!_initialized.isCompleted); + + chart = offlineData?.chart ?? MemoryChartPaneController(_mode); + diff = diffPaneController ?? + offlineData?.diff ?? + DiffPaneController( + loader: HeapGraphLoaderRuntime(chart.memoryTimeline), + ); + profile = profilePaneController ?? + offlineData?.profile ?? + ProfilePaneController(); + control = MemoryControlPaneController( + chart.memoryTimeline, + isChartVisible: chart.isChartVisible, + exportData: exportData, + ); + tracing = TracingPaneController(); + selectedFeatureTabIndex = + offlineData?.selectedTab ?? selectedFeatureTabIndex; + if (offlineData != null) profile.setFilter(offlineData.filter); + _shareClassFilterBetweenProfileAndDiff(); - profile.dispose(); - profile = ProfilePaneController(); + _initialized.complete(); + } - tracing.dispose(); - tracing = TracingPaneController(); + @override + OfflineScreenData prepareOfflineScreenData() => OfflineScreenData( + screenId: ScreenMetaData.memory.id, + data: OfflineMemoryData( + diff, + profile, + chart, + profile.classFilter.value, + selectedTab: selectedFeatureTabIndex, + ).prepareForOffline(), + ); - chart.memoryTimeline.reset(); + @override + FutureOr processOfflineData(OfflineMemoryData offlineData) { + assert(!_initialized.isCompleted); + _initializeData(offlineData: offlineData); } - void shareClassFilterBetweenProfileAndDiff() { - diff.derived.applyFilter( - profile.classFilter.value, - ); + void _shareClassFilterBetweenProfileAndDiff() { + diff.derived.applyFilter(profile.classFilter.value); profile.classFilter.addListener(() { diff.derived.applyFilter(profile.classFilter.value); diff --git a/packages/devtools_app/lib/src/screens/memory/framework/offline_data/offline_data.dart b/packages/devtools_app/lib/src/screens/memory/framework/offline_data/offline_data.dart new file mode 100644 index 00000000000..6588ef67891 --- /dev/null +++ b/packages/devtools_app/lib/src/screens/memory/framework/offline_data/offline_data.dart @@ -0,0 +1,57 @@ +// Copyright 2024 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 '../../panes/chart/controller/chart_pane_controller.dart'; +import '../../panes/diff/controller/diff_pane_controller.dart'; +import '../../panes/profile/profile_pane_controller.dart'; +import '../../shared/heap/class_filter.dart'; + +class _Json { + static const selectedTab = 'selectedTab'; + static const classFilter = 'classFilter'; + static const diffData = 'diffData'; + static const profileData = 'profileData'; + static const chartData = 'chartData'; +} + +class OfflineMemoryData { + OfflineMemoryData( + this.diff, + this.profile, + this.chart, + this.filter, { + required this.selectedTab, + }); + + // TODO(polina-c): use an extension type for the Json parsing, https://github.com/flutter/devtools/issues/6972 + // https://github.com/flutter/devtools/pull/7572#discussion_r1563102256 + factory OfflineMemoryData.parse(Map json) { + Map item(String key) => + json[key] as Map? ?? {}; + return OfflineMemoryData( + DiffPaneController.parse(item(_Json.diffData)), + ProfilePaneController.parse(item(_Json.profileData)), + MemoryChartPaneController.parse(item(_Json.chartData)), + ClassFilter.parse(item(_Json.classFilter)), + selectedTab: json[_Json.selectedTab] as int? ?? 0, + ); + } + + final int selectedTab; + final ClassFilter filter; // filter is shared between tabs, so it's here + + final DiffPaneController diff; + final ProfilePaneController profile; + final MemoryChartPaneController chart; + + Map prepareForOffline() { + return { + _Json.selectedTab: selectedTab, + _Json.diffData: diff.prepareForOffline(), + _Json.profileData: profile.prepareForOffline(), + _Json.chartData: chart.prepareForOffline(), + _Json.classFilter: profile.classFilter.value.prepareForOffline(), + }; + } +} diff --git a/packages/devtools_app/lib/src/screens/memory/framework/screen_body.dart b/packages/devtools_app/lib/src/screens/memory/framework/screen_body.dart index 381c70ba7f7..4c67657cf69 100644 --- a/packages/devtools_app/lib/src/screens/memory/framework/screen_body.dart +++ b/packages/devtools_app/lib/src/screens/memory/framework/screen_body.dart @@ -7,6 +7,7 @@ import 'package:devtools_app_shared/utils.dart'; import 'package:flutter/material.dart'; import '../../../shared/banner_messages.dart'; +import '../../../shared/common_widgets.dart'; import '../../../shared/http/http_service.dart' as http_service; import '../../../shared/screen.dart'; import '../../../shared/utils.dart'; @@ -52,21 +53,30 @@ class _ConnectedMemoryBodyState extends State @override Widget build(BuildContext context) { - return Column( - key: MemoryChartPane.hoverKey, - children: [ - MemoryControlPane( - controller: controller.control, - ), - const SizedBox(height: intermediateSpacing), - MemoryChartPane( - chart: controller.chart, - keyFocusNode: _focusNode, - ), - Expanded( - child: MemoryTabView(memoryController), - ), - ], + return FutureBuilder( + future: memoryController.initialized, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + return Column( + key: MemoryChartPane.hoverKey, + children: [ + MemoryControlPane( + controller: controller.control, + ), + const SizedBox(height: intermediateSpacing), + MemoryChartPane( + chart: controller.chart, + keyFocusNode: _focusNode, + ), + Expanded( + child: MemoryTabView(memoryController), + ), + ], + ); + } else { + return const CenteredCircularProgressIndicator(); + } + }, ); } } diff --git a/packages/devtools_app/lib/src/screens/memory/panes/chart/controller/chart_pane_controller.dart b/packages/devtools_app/lib/src/screens/memory/panes/chart/controller/chart_pane_controller.dart index 8eb835c9a8c..e74e41d3c89 100644 --- a/packages/devtools_app/lib/src/screens/memory/panes/chart/controller/chart_pane_controller.dart +++ b/packages/devtools_app/lib/src/screens/memory/panes/chart/controller/chart_pane_controller.dart @@ -2,11 +2,13 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:async'; + import 'package:devtools_app_shared/utils.dart'; import 'package:flutter/foundation.dart'; -import 'package:vm_service/vm_service.dart'; import '../../../../../shared/globals.dart'; +import '../../../../../shared/primitives/simple_items.dart'; import '../../../shared/primitives/memory_timeline.dart'; import '../data/primitives.dart'; import 'android_chart_controller.dart'; @@ -14,9 +16,112 @@ import 'event_chart_controller.dart'; import 'memory_tracker.dart'; import 'vm_chart_controller.dart'; +/// Connection between chart and application. +/// +/// The connection consists of listeners to events from vm and +/// ongoing requests to vm service for current memory usage. +/// +/// When user pauses the chart, the data is still collected. +/// +/// Does not fail in case of accidental disconnect. +/// +/// All interactions between chart and vm are initiated by this class. +/// So, if this class is not instantiated, the interaction does not happen. +class _ChartConnection extends DisposableController + with AutoDisposeControllerMixin { + _ChartConnection(this.timeline, {required this.isAndroidChartVisible}); + + final MemoryTimeline timeline; + final ValueListenable isAndroidChartVisible; + + late final MemoryTracker _memoryTracker = MemoryTracker( + timeline, + isAndroidChartVisible: isAndroidChartVisible, + ); + + Timer? _pollingTimer; + bool _connected = false; + + late final isDeviceAndroid = + serviceConnection.serviceManager.vm?.operatingSystem == 'android'; + + Future maybeConnect() async { + if (_connected) return; + await serviceConnection.serviceManager.onServiceAvailable; + autoDisposeStreamSubscription( + serviceConnection.serviceManager.service!.onExtensionEvent + .listen(_memoryTracker.onMemoryData), + ); + autoDisposeStreamSubscription( + serviceConnection.serviceManager.service!.onGCEvent + .listen(_memoryTracker.onGCEvent), + ); + await _onPoll(); + _connected = true; + } + + Future _onPoll() async { + _pollingTimer = null; + await _memoryTracker.pollMemory(); + _pollingTimer = Timer(chartUpdateDelay, _onPoll); + } + + @override + void dispose() { + _pollingTimer?.cancel(); + super.dispose(); + } +} + class MemoryChartPaneController extends DisposableController with AutoDisposeControllerMixin { - MemoryChartPaneController(); + MemoryChartPaneController(this.mode, {this.isDeviceAndroid}) + : assert( + mode == DevToolsMode.connected || isDeviceAndroid != null, + 'If application is not connected, isDeviceAndroid must be provided.', + ) { + unawaited(_init()); + } + + factory MemoryChartPaneController.parse(Map map) { + // TODO(polina-c): implement, https://github.com/flutter/devtools/issues/6972 + return MemoryChartPaneController( + DevToolsMode.offlineData, + isDeviceAndroid: false, + ); + } + + DevToolsMode mode; + + /// Wether device is android, if [mode] is not [DevToolsMode.connected]. + /// + /// If [mode] is [DevToolsMode.connected], this value should be detected + /// by [_chartConnection]. + final bool? isDeviceAndroid; + + Future _init() async { + _updateAndroidChartVisibility(); + if (mode == DevToolsMode.connected && isChartVisible.value) { + await resume(); + } + addAutoDisposeListener( + preferences.memory.androidCollectionEnabled, + _updateAndroidChartVisibility, + ); + } + + Map prepareForOffline() { + // TODO(polina-c): implement, https://github.com/flutter/devtools/issues/6972 + return {}; + } + + late final _ChartConnection? _chartConnection = + (mode == DevToolsMode.connected) + ? _ChartConnection( + memoryTimeline, + isAndroidChartVisible: isAndroidChartVisible, + ) + : null; final MemoryTimeline memoryTimeline = MemoryTimeline(); @@ -31,12 +136,12 @@ class MemoryChartPaneController extends DisposableController ); ValueListenable get isLegendVisible => _legendVisibleNotifier; - final _legendVisibleNotifier = ValueNotifier(true); - bool toggleLegendVisibility() => _legendVisibleNotifier.value = !_legendVisibleNotifier.value; + ValueNotifier isChartVisible = preferences.memory.showChart; + void resetAll() { event.reset(); vm.reset(); @@ -55,139 +160,44 @@ class MemoryChartPaneController extends DisposableController android.dirty = true; } - ValueListenable get refreshCharts => _refreshCharts; - - final _refreshCharts = ValueNotifier(0); - /// Default is to display default tick width based on width of chart of the collected /// data in the chart. - final _displayIntervalNotifier = + final _displayInterval = ValueNotifier(ChartInterval.theDefault); set displayInterval(ChartInterval interval) { - _displayIntervalNotifier.value = interval; + _displayInterval.value = interval; } - ChartInterval get displayInterval => _displayIntervalNotifier.value; - - final _paused = ValueNotifier(false); + ChartInterval get displayInterval => _displayInterval.value; ValueListenable get paused => _paused; - - void pauseLiveFeed() { - _paused.value = true; - } - - void resumeLiveFeed() { + final _paused = ValueNotifier(true); + void pause() => _paused.value = true; + Future resume() async { + if (!_paused.value) return; + if (mode != DevToolsMode.connected) throw StateError('Not connected.'); + await _chartConnection!.maybeConnect(); _paused.value = false; } - bool get isPaused => _paused.value; - final isAndroidChartVisible = ValueNotifier(false); - void updateAndroidChartVisibility() { - final bool isConnectedToAndroidAndAndroidEnabled = - _isConnectedDeviceAndroid && - preferences.memory.androidCollectionEnabled.value; - - isAndroidChartVisible.value = isConnectedToAndroidAndAndroidEnabled; - } - - bool get _isConnectedDeviceAndroid { - return serviceConnection.serviceManager.vm?.operatingSystem == 'android'; - } - - MemoryTracker? memoryTracker; - - bool get hasStarted => memoryTracker != null; - - bool hasStopped = false; - - void stopTimeLine() { - memoryTracker?.stop(); - } - - void _handleConnectionStart() { - memoryTracker ??= MemoryTracker( - memoryTimeline, - isAndroidChartVisible: isAndroidChartVisible, - paused: paused, - )..start(); - - // Log Flutter extension events. - // Note: We do not need to listen to event history here because we do not - // have matching historical data about total memory usage. - autoDisposeStreamSubscription( - serviceConnection.serviceManager.service!.onExtensionEvent.listen( - (Event event) { - var extensionEventKind = event.extensionKind; - String? customEventKind; - if (MemoryTimeline.isCustomEvent(event.extensionKind!)) { - extensionEventKind = MemoryTimeline.devToolsExtensionEvent; - customEventKind = - MemoryTimeline.customEventName(event.extensionKind!); - } - final jsonData = event.extensionData!.data.cast(); - // TODO(terry): Display events enabled in a settings page for now only these events. - switch (extensionEventKind) { - case 'Flutter.ImageSizesForFrame': - memoryTimeline.addExtensionEvent( - event.timestamp, - event.extensionKind, - jsonData, - ); - break; - case MemoryTimeline.devToolsExtensionEvent: - memoryTimeline.addExtensionEvent( - event.timestamp, - MemoryTimeline.customDevToolsEvent, - jsonData, - customEventName: customEventKind, - ); - break; - } - }, - ), - ); - - updateAndroidChartVisibility(); - addAutoDisposeListener( - preferences.memory.androidCollectionEnabled, - updateAndroidChartVisibility, - ); - } - - void _handleConnectionStop() { - memoryTracker?.stop(); - memoryTimeline.reset(); - hasStopped = true; - } - - void startTimeline() { - addAutoDisposeListener(serviceConnection.serviceManager.connectedState, () { - if (serviceConnection.serviceManager.connectedState.value.connected) { - _handleConnectionStart(); - } else { - _handleConnectionStop(); - } - }); - - if (serviceConnection.serviceManager.connectedAppInitialized) { - _handleConnectionStart(); - } + void _updateAndroidChartVisibility() { + final isAndroid = isDeviceAndroid ?? _chartConnection!.isDeviceAndroid; + isAndroidChartVisible.value = + isAndroid && preferences.memory.androidCollectionEnabled.value; } @override void dispose() { super.dispose(); - memoryTracker?.dispose(); _legendVisibleNotifier.dispose(); - _displayIntervalNotifier.dispose(); - _refreshCharts.dispose(); + _displayInterval.dispose(); event.dispose(); vm.dispose(); android.dispose(); isAndroidChartVisible.dispose(); + _chartConnection?.dispose(); } } diff --git a/packages/devtools_app/lib/src/screens/memory/panes/chart/controller/memory_tracker.dart b/packages/devtools_app/lib/src/screens/memory/panes/chart/controller/memory_tracker.dart index 7bb71132f2e..10da1f77ea9 100644 --- a/packages/devtools_app/lib/src/screens/memory/panes/chart/controller/memory_tracker.dart +++ b/packages/devtools_app/lib/src/screens/memory/panes/chart/controller/memory_tracker.dart @@ -13,66 +13,42 @@ import 'package:vm_service/vm_service.dart'; import '../../../../../shared/globals.dart'; import '../../../../../shared/utils.dart'; import '../../../shared/primitives/memory_timeline.dart'; +import '../data/primitives.dart'; final _log = Logger('memory_protocol'); +enum _ContinuesState { + none, + stop, + next, +} + class MemoryTracker { MemoryTracker( this.timeline, { - required this.paused, required this.isAndroidChartVisible, }); final MemoryTimeline timeline; - final ValueListenable paused; - final ValueNotifier isAndroidChartVisible; - Timer? _pollingTimer; + final ValueListenable isAndroidChartVisible; + + _ContinuesState _monitorContinuesState = _ContinuesState.none; - final isolateHeaps = {}; + final _isolateHeaps = {}; /// Polled VM current RSS. - int processRss = 0; + int _processRss = 0; /// Polled adb dumpsys meminfo values. - AdbMemoryInfo? adbMemoryInfo; + AdbMemoryInfo? _adbMemoryInfo; /// Polled engine's RasterCache estimates. RasterCache? rasterCache; - Stream get onChange => _changeController.stream; - final _changeController = StreamController.broadcast(); - - StreamSubscription? _gcStreamListener; - Timer? _monitorContinues; - void start() { - _updateLiveDataPolling(); - paused.addListener(_updateLiveDataPolling); - } - - void _updateLiveDataPolling() { - _pollingTimer ??= Timer(MemoryTimeline.updateDelay, _pollMemory); - _gcStreamListener ??= serviceConnection.serviceManager.service?.onGCEvent - .listen(_handleGCEvent); - } - - void stop() { - _updateLiveDataPolling(); - _cleanListenersAndTimers(); - } - - void _cleanListenersAndTimers() { - paused.removeListener(_updateLiveDataPolling); - - _pollingTimer?.cancel(); - unawaited(_gcStreamListener?.cancel()); - _pollingTimer = null; - _gcStreamListener = null; - } - - void _handleGCEvent(Event event) { + void onGCEvent(Event event) { final HeapSpace newHeap = HeapSpace.parse(event.json!['new'])!; final HeapSpace oldHeap = HeapSpace.parse(event.json!['old'])!; @@ -85,9 +61,34 @@ class MemoryTracker { _updateGCEvent(event.isolate!.id!, memoryUsage); } - void _pollMemory() async { - _pollingTimer = null; + void onMemoryData(Event data) { + var extensionEventKind = data.extensionKind; + String? customEventKind; + if (MemoryTimeline.isCustomEvent(data.extensionKind!)) { + extensionEventKind = MemoryTimeline.devToolsExtensionEvent; + customEventKind = MemoryTimeline.customEventName(data.extensionKind!); + } + final jsonData = data.extensionData!.data.cast(); + switch (extensionEventKind) { + case 'Flutter.ImageSizesForFrame': + timeline.addExtensionEvent( + data.timestamp, + data.extensionKind, + jsonData, + ); + break; + case MemoryTimeline.devToolsExtensionEvent: + timeline.addExtensionEvent( + data.timestamp, + MemoryTimeline.customDevToolsEvent, + jsonData, + customEventName: customEventKind, + ); + break; + } + } + Future pollMemory() async { final isolateMemory = {}; for (IsolateRef isolateRef in serviceConnection.serviceManager.isolateManager.isolates.value) { @@ -100,7 +101,7 @@ class MemoryTracker { // Polls for current Android meminfo using: // > adb shell dumpsys meminfo -d - adbMemoryInfo = serviceConnection.serviceManager.hasConnection && + _adbMemoryInfo = serviceConnection.serviceManager.hasConnection && serviceConnection.serviceManager.vm!.operatingSystem == 'android' && isAndroidChartVisible.value ? await _fetchAdbInfo() @@ -112,11 +113,6 @@ class MemoryTracker { // Polls for current RSS size. final vm = await serviceConnection.serviceManager.service!.getVM(); _update(vm, isolateMemory); - - // TODO(terry): Is there a better way to detect an integration test running? - if (vm.json!.containsKey('_FAKE_VM')) return; - - _pollingTimer ??= Timer(MemoryTimeline.updateDelay, _pollMemory); } /// Detect stale isolates (sentineled), may happen after a hot restart. @@ -137,19 +133,19 @@ class MemoryTracker { } void _update(VM vm, Map isolateMemory) { - processRss = vm.json!['_currentRSS']; + _processRss = vm.json!['_currentRSS']; - isolateHeaps.clear(); + _isolateHeaps.clear(); for (IsolateRef isolateRef in isolateMemory.keys) { - isolateHeaps[isolateRef.id!] = isolateMemory[isolateRef]!; + _isolateHeaps[isolateRef.id!] = isolateMemory[isolateRef]!; } _recalculate(); } void _updateGCEvent(String isolateId, MemoryUsage memoryUsage) { - isolateHeaps[isolateId] = memoryUsage; + _isolateHeaps[isolateId] = memoryUsage; _recalculate(true); } @@ -187,11 +183,11 @@ class MemoryTracker { final keysToRemove = []; - final isolateCount = isolateHeaps.length; - final keys = isolateHeaps.keys.toList(); + final isolateCount = _isolateHeaps.length; + final keys = _isolateHeaps.keys.toList(); for (var index = 0; index < isolateCount; index++) { final isolateId = keys[index]; - var usage = isolateHeaps[isolateId]; + var usage = _isolateHeaps[isolateId]; // Check if the isolate is dead (sentinel), null implies sentinel. final checkIsolateUsage = await _isolateMemoryUsage(isolateId, usage); if (checkIsolateUsage == null && !keysToRemove.contains(isolateId)) { @@ -210,7 +206,7 @@ class MemoryTracker { } // Removes any isolate that is a sentinel. - isolateHeaps.removeWhere((key, value) => keysToRemove.contains(key)); + _isolateHeaps.removeWhere((key, value) => keysToRemove.contains(key)); int time = DateTime.now().millisecondsSinceEpoch; if (timeline.data.isNotEmpty) { @@ -218,14 +214,14 @@ class MemoryTracker { } // Process any memory events? - final eventSample = processEventSample(timeline, time); + final eventSample = _processEventSample(timeline, time); if (eventSample != null && eventSample.isEventAllocationAccumulator) { if (eventSample.allocationAccumulator!.isStart) { // Stop Continuous events being auto posted - a new start is beginning. - timeline.monitorContinuesState = ContinuesState.stop; + _monitorContinuesState = _ContinuesState.stop; } - } else if (timeline.monitorContinuesState == ContinuesState.next) { + } else if (_monitorContinuesState == _ContinuesState.next) { if (_monitorContinues != null) { _monitorContinues!.cancel(); _monitorContinues = null; @@ -238,28 +234,26 @@ class MemoryTracker { final HeapSample sample = HeapSample( time, - processRss, + _processRss, // Displaying capacity dashed line on top of stacked (used + external). capacity + external, used, external, fromGC, - adbMemoryInfo, + _adbMemoryInfo, eventSample, rasterCache, ); timeline.addSample(sample); - _changeController.add(null); - // Signal continues events are to be emitted. These events are hidden // until a reset event then the continuous events between last monitor // start/reset and latest reset are made visible. if (eventSample != null && eventSample.isEventAllocationAccumulator && eventSample.allocationAccumulator!.isStart) { - timeline.monitorContinuesState = ContinuesState.next; + _monitorContinuesState = _ContinuesState.next; } } @@ -270,10 +264,10 @@ class MemoryTracker { /// (time parameter)). /// /// Returns copy of events to associate with an existing HeapSample tick - /// (contained in the EventSample). See [processEventSample] it computes the + /// (contained in the EventSample). See [_processEventSample] it computes the /// events to aggregate to an existing HeapSample or delay associating those /// events until the next HeapSample (tick) received see [_recalculate]. - EventSample pullClone(MemoryTimeline memoryTimeline, int time) { + EventSample _pullClone(MemoryTimeline memoryTimeline, int time) { final pulledEvent = memoryTimeline.pullEventSample(); final extensionEvents = memoryTimeline.extensionEvents; final eventSample = pulledEvent.clone( @@ -287,20 +281,19 @@ class MemoryTracker { return eventSample; } - EventSample? processEventSample(MemoryTimeline memoryTimeline, int time) { + EventSample? _processEventSample(MemoryTimeline memoryTimeline, int time) { if (memoryTimeline.anyEvents) { final eventTime = memoryTimeline.peekEventTimestamp; final timeDuration = Duration(milliseconds: time); final eventDuration = Duration(milliseconds: eventTime); - // If the event is +/- _updateDelay (500 ms) of the current time then - // associate the EventSample with the current HeapSample. - const delay = MemoryTimeline.updateDelay; final compared = timeDuration.compareTo(eventDuration); if (compared < 0) { - if ((timeDuration + delay).compareTo(eventDuration) >= 0) { + // If the event is +/- _updateDelay (500 ms) of the current time then + // associate the EventSample with the current HeapSample. + if ((timeDuration + chartUpdateDelay).compareTo(eventDuration) >= 0) { // Currently, events are all UI events so duration < _updateDelay - return pullClone(memoryTimeline, time); + return _pullClone(memoryTimeline, time); } // Throw away event, missed attempt to attach to a HeapSample. final ignoreEvent = memoryTimeline.pullEventSample(); @@ -315,18 +308,18 @@ class MemoryTracker { if (compared > 0) { final msDiff = time - eventTime; - if (msDiff > MemoryTimeline.delayMs) { + if (msDiff > chartUpdateDelay.inMilliseconds) { // eventSample is in the future. - if ((timeDuration - delay).compareTo(eventDuration) >= 0) { + if ((timeDuration - chartUpdateDelay).compareTo(eventDuration) >= 0) { // Able to match event time to a heap sample. We will attach the // EventSample to this HeapSample. - return pullClone(memoryTimeline, time); + return _pullClone(memoryTimeline, time); } // Keep the event, its time hasn't caught up to the HeapSample time yet. return null; } // The almost exact eventSample we have. - return pullClone(memoryTimeline, time); + return _pullClone(memoryTimeline, time); } } @@ -337,8 +330,4 @@ class MemoryTracker { return null; } - - void dispose() { - _cleanListenersAndTimers(); - } } diff --git a/packages/devtools_app/lib/src/screens/memory/panes/chart/data/primitives.dart b/packages/devtools_app/lib/src/screens/memory/panes/chart/data/primitives.dart index ac1197e7fe8..ba282a9b003 100644 --- a/packages/devtools_app/lib/src/screens/memory/panes/chart/data/primitives.dart +++ b/packages/devtools_app/lib/src/screens/memory/panes/chart/data/primitives.dart @@ -23,3 +23,5 @@ enum ChartInterval { final String displayName; } + +const Duration chartUpdateDelay = Duration(milliseconds: 500); diff --git a/packages/devtools_app/lib/src/screens/memory/panes/chart/widgets/chart_control_pane.dart b/packages/devtools_app/lib/src/screens/memory/panes/chart/widgets/chart_control_pane.dart index aac699fbf45..b01bf845f7c 100644 --- a/packages/devtools_app/lib/src/screens/memory/panes/chart/widgets/chart_control_pane.dart +++ b/packages/devtools_app/lib/src/screens/memory/panes/chart/widgets/chart_control_pane.dart @@ -24,7 +24,8 @@ class ChartControlPane extends StatefulWidget { @visibleForTesting class ChartPaneTooltips { - static const String pauseTooltip = 'Pause the chart'; + static const String pauseTooltip = + 'Pause the chart. Data will be still collected and shown when you resume.'; static const String resumeTooltip = 'Resume the chart'; } @@ -32,12 +33,12 @@ class _ChartControlPaneState extends State with AutoDisposeMixin { void _onPause() { ga.select(gac.memory, gac.pause); - widget.chart.pauseLiveFeed(); + widget.chart.pause(); } - void _onResume() { + Future _onResume() async { ga.select(gac.memory, gac.resume); - widget.chart.resumeLiveFeed(); + await widget.chart.resume(); } void _clearTimeline() { diff --git a/packages/devtools_app/lib/src/screens/memory/panes/chart/widgets/chart_pane.dart b/packages/devtools_app/lib/src/screens/memory/panes/chart/widgets/chart_pane.dart index 602c5003895..977e7dcb9cb 100644 --- a/packages/devtools_app/lib/src/screens/memory/panes/chart/widgets/chart_pane.dart +++ b/packages/devtools_app/lib/src/screens/memory/panes/chart/widgets/chart_pane.dart @@ -136,26 +136,10 @@ class _MemoryChartPaneState extends State _addTapLocationListener(location, allLocations); } - addAutoDisposeListener(widget.chart.refreshCharts, () { - setState(() { - widget.chart.recomputeChartData(); - }); - }); - // There is no listener passed, so SetState will be invoked. addAutoDisposeListener( widget.chart.isAndroidChartVisible, ); - - _updateListeningState(); - } - - void _updateListeningState() async { - await serviceConnection.serviceManager.onServiceAvailable; - - if (!widget.chart.hasStarted) { - widget.chart.startTimeline(); - } } @override @@ -233,8 +217,7 @@ class _MemoryChartPaneState extends State @override void dispose() { - _hideHover(); // hover will leak if not hide - widget.chart.stopTimeLine(); + _hideHover(); super.dispose(); } diff --git a/packages/devtools_app/lib/src/screens/memory/panes/control/controller/control_pane_controller.dart b/packages/devtools_app/lib/src/screens/memory/panes/control/controller/control_pane_controller.dart index 1b21be2e56c..410ac52a85d 100644 --- a/packages/devtools_app/lib/src/screens/memory/panes/control/controller/control_pane_controller.dart +++ b/packages/devtools_app/lib/src/screens/memory/panes/control/controller/control_pane_controller.dart @@ -2,13 +2,22 @@ // 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/globals.dart'; import '../../../shared/primitives/memory_timeline.dart'; class MemoryControlPaneController { - MemoryControlPaneController(this.memoryTimeline); + MemoryControlPaneController( + this.memoryTimeline, { + required this.isChartVisible, + required this.exportData, + }); final MemoryTimeline memoryTimeline; + final VoidCallback exportData; + final ValueNotifier isChartVisible; + bool get isGcing => _gcing; bool _gcing = false; diff --git a/packages/devtools_app/lib/src/screens/memory/panes/control/widgets/control_pane.dart b/packages/devtools_app/lib/src/screens/memory/panes/control/widgets/control_pane.dart index 386e49350bd..9c959b36e89 100644 --- a/packages/devtools_app/lib/src/screens/memory/panes/control/widgets/control_pane.dart +++ b/packages/devtools_app/lib/src/screens/memory/panes/control/widgets/control_pane.dart @@ -21,7 +21,7 @@ class MemoryControlPane extends StatelessWidget { return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - const PrimaryControls(), + PrimaryControls(controller: controller), const Spacer(), SecondaryControls(controller: controller), ], diff --git a/packages/devtools_app/lib/src/screens/memory/panes/control/widgets/primary_controls.dart b/packages/devtools_app/lib/src/screens/memory/panes/control/widgets/primary_controls.dart index ebe7fa6c889..c6fcc053852 100644 --- a/packages/devtools_app/lib/src/screens/memory/panes/control/widgets/primary_controls.dart +++ b/packages/devtools_app/lib/src/screens/memory/panes/control/widgets/primary_controls.dart @@ -8,21 +8,25 @@ import '../../../../../shared/analytics/constants.dart' as gac; import '../../../../../shared/common_widgets.dart'; import '../../../../../shared/globals.dart'; import '../../../shared/primitives/simple_elements.dart'; +import '../controller/control_pane_controller.dart'; class PrimaryControls extends StatelessWidget { const PrimaryControls({ Key? key, + required this.controller, }) : super(key: key); @visibleForTesting static const memoryChartText = 'Memory chart'; + final MemoryControlPaneController controller; + @override Widget build(BuildContext context) { return VisibilityButton( show: preferences.memory.showChart, gaScreen: gac.memory, - onPressed: (show) => preferences.memory.showChart.value = show, + onPressed: (show) => controller.isChartVisible.value = show, minScreenWidthForTextBeforeScaling: memoryControlsMinVerboseWidth, label: memoryChartText, tooltip: 'Toggle visibility of the Memory usage chart', diff --git a/packages/devtools_app/lib/src/screens/memory/panes/control/widgets/secondary_controls.dart b/packages/devtools_app/lib/src/screens/memory/panes/control/widgets/secondary_controls.dart index 2d43a1881a8..85de97c8147 100644 --- a/packages/devtools_app/lib/src/screens/memory/panes/control/widgets/secondary_controls.dart +++ b/packages/devtools_app/lib/src/screens/memory/panes/control/widgets/secondary_controls.dart @@ -9,6 +9,8 @@ import 'package:flutter/material.dart'; import '../../../../../shared/analytics/constants.dart' as gac; import '../../../../../shared/common_widgets.dart'; +import '../../../../../shared/file_import.dart'; +import '../../../../../shared/screen.dart'; import '../../../shared/primitives/simple_elements.dart'; import '../controller/control_pane_controller.dart'; import 'settings_dialog.dart'; @@ -37,6 +39,11 @@ class SecondaryControls extends StatelessWidget { gaSelection: gac.MemoryEvent.gc, ), const SizedBox(width: denseSpacing), + OpenSaveButtonGroup( + screenId: ScreenMetaData.memory.id, + onSave: controller.exportData, + ), + const SizedBox(width: denseSpacing), SettingsOutlinedButton( gaScreen: gac.memory, gaSelection: gac.MemoryEvent.settings, 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 ca7138883ba..d46b75856a0 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 @@ -29,9 +29,19 @@ import 'class_data.dart'; import 'item_controller.dart'; class DiffPaneController extends DisposableController { - DiffPaneController(this._heapGraphLoader); + DiffPaneController({required this.loader}); - final HeapGraphLoader _heapGraphLoader; + factory DiffPaneController.parse(Map map) { + // TODO(polina-c): implement, https://github.com/flutter/devtools/issues/6972 + return DiffPaneController(loader: null); + } + + Map prepareForOffline() { + // TODO(polina-c): implement, https://github.com/flutter/devtools/issues/6972 + return {}; + } + + final HeapGraphLoader? loader; final retainingPathController = RetainingPathController(); @@ -47,13 +57,11 @@ class DiffPaneController extends DisposableController { gac.memory, gac.MemoryEvent.diffTakeSnapshotControlPane, ); - final item = SnapshotDataItem( displayNumber: _nextDisplayNumber(), defaultName: selectedIsolateName ?? '', ); - - await _addSnapshot(_heapGraphLoader, item); + await _addSnapshot(loader!, item); derived._updateValues(); } 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 28eb2dedd70..71b6abdf5e6 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 @@ -15,6 +15,18 @@ import 'model.dart'; class ProfilePaneController extends DisposableController with AutoDisposeControllerMixin { + ProfilePaneController(); + + factory ProfilePaneController.parse(Map map) { + // TODO(polina-c): implement, https://github.com/flutter/devtools/issues/6972 + return ProfilePaneController(); + } + + Map prepareForOffline() { + // TODO(polina-c): implement, https://github.com/flutter/devtools/issues/6972 + return {}; + } + final _exportController = ExportController(); /// The current profile being displayed. diff --git a/packages/devtools_app/lib/src/screens/memory/shared/heap/class_filter.dart b/packages/devtools_app/lib/src/screens/memory/shared/heap/class_filter.dart index 7ff9bd6bab0..9585ae05976 100644 --- a/packages/devtools_app/lib/src/screens/memory/shared/heap/class_filter.dart +++ b/packages/devtools_app/lib/src/screens/memory/shared/heap/class_filter.dart @@ -2,6 +2,7 @@ // 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:flutter/foundation.dart'; import '../../../../shared/globals.dart'; @@ -39,6 +40,14 @@ enum FilteringTask { typedef ApplyFilterCallback = void Function(ClassFilter); +class _Json { + static const except = 'except'; + static const only = 'only'; + static const type = 'type'; +} + +const _defaultFilterType = ClassFilterType.except; + @immutable class ClassFilter { ClassFilter({ @@ -50,11 +59,32 @@ class ClassFilter { ClassFilter.empty() : this( - filterType: ClassFilterType.except, + filterType: _defaultFilterType, except: defaultExceptString, only: null, ); + factory ClassFilter.parse(Map json) { + final type = json[_Json.type] as String?; + return ClassFilter( + filterType: + ClassFilterType.values.lastWhereOrNull((t) => t.name == type) ?? + _defaultFilterType, + except: json[_Json.except] as String? ?? defaultExceptString, + only: json[_Json.only] as String?, + ); + } + + // TODO: use an extension type for the Json parsing, https://github.com/flutter/devtools/issues/6972 + // https://github.com/flutter/devtools/pull/7572#discussion_r1563130198 + Map prepareForOffline() { + return { + _Json.type: filterType.name, + _Json.except: except, + _Json.only: only, + }; + } + @visibleForTesting static final defaultExceptString = '${ClassType.runtime.alias}\n${ClassType.sdk.alias}'; diff --git a/packages/devtools_app/lib/src/screens/memory/shared/primitives/memory_timeline.dart b/packages/devtools_app/lib/src/screens/memory/shared/primitives/memory_timeline.dart index 02f17c3e7aa..fa26c0a8cc8 100644 --- a/packages/devtools_app/lib/src/screens/memory/shared/primitives/memory_timeline.dart +++ b/packages/devtools_app/lib/src/screens/memory/shared/primitives/memory_timeline.dart @@ -9,18 +9,8 @@ import 'package:vm_service/vm_service.dart'; import '../../../../shared/primitives/utils.dart'; -enum ContinuesState { - none, - start, - stop, - next, -} - /// All Raw data received from the VM and offline data loaded from a memory log file. class MemoryTimeline { - static const delayMs = 500; - static const Duration updateDelay = Duration(milliseconds: delayMs); - /// This flag will be needed for offline mode implementation. final offline = false; @@ -60,8 +50,6 @@ class MemoryTimeline { /// List of events awaiting to be posted to HeapSample. final _eventSamples = []; - ContinuesState monitorContinuesState = ContinuesState.none; - void postEventSample(EventSample event) { /* final lastEvent = _eventSamples.safeLast; diff --git a/packages/devtools_app/lib/src/shared/charts/chart_controller.dart b/packages/devtools_app/lib/src/shared/charts/chart_controller.dart index e5cb250fa66..e765cdffa97 100644 --- a/packages/devtools_app/lib/src/shared/charts/chart_controller.dart +++ b/packages/devtools_app/lib/src/shared/charts/chart_controller.dart @@ -41,7 +41,6 @@ class ChartController extends DisposableController this.name, List? sharedLabelTimestamps, }) { - // TODO(terry): Compute dynamically based on X-axis labels text height. bottomPadding = !displayXLabels ? 0.0 : 40.0; if (sharedLabelTimestamps != null) { @@ -430,7 +429,7 @@ class ChartController extends DisposableController double yPositionToYCanvasCoord(double y) => -yPosition(y); Trace trace(int index) { - assert(index < traces.length); + assert(index < traces.length, '$index, ${traces.length}'); return traces[index]; } 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 2012880eb9f..65b61648556 100644 --- a/packages/devtools_app/test/memory/control/settings_dialog_test.dart +++ b/packages/devtools_app/test/memory/control/settings_dialog_test.dart @@ -2,7 +2,6 @@ // 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/framework/memory_screen.dart'; import 'package:devtools_app/src/screens/memory/panes/control/widgets/settings_dialog.dart'; import 'package:devtools_app/src/shared/common_widgets.dart'; import 'package:devtools_app/src/shared/globals.dart'; @@ -13,18 +12,10 @@ import 'package:flutter_test/flutter_test.dart'; import '../../test_infra/matchers/matchers.dart'; import '../../test_infra/scenes/memory/default.dart'; -import '../../test_infra/scenes/scene_test_extensions.dart'; void main() { late MemoryDefaultScene scene; - Future pumpMemoryScreen(WidgetTester tester) async { - await tester.pumpScene(scene); - // Delay to ensure the memory profiler has collected data. - await tester.pumpAndSettle(const Duration(seconds: 1)); - expect(find.byType(MemoryBody), findsOneWidget); - } - // Set a wide enough screen width that we do not run into overflow. const windowSize = Size(2225.0, 1000.0); @@ -41,7 +32,7 @@ void main() { 'settings update preferences', windowSize, (WidgetTester tester) async { - await pumpMemoryScreen(tester); + await scene.pump(tester); // Open the dialog. await tester.tap(find.byType(SettingsOutlinedButton)); 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 532955a01de..b61d21dbe7f 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 @@ -12,13 +12,9 @@ import 'package:flutter_test/flutter_test.dart'; import '../../../test_infra/matchers/matchers.dart'; import '../../../test_infra/scenes/memory/default.dart'; -import '../../../test_infra/scenes/scene_test_extensions.dart'; Future pumpScene(WidgetTester tester, MemoryDefaultScene scene) async { - await tester.pumpScene(scene); - // Delay to ensure the memory profiler has collected data. - await tester.pumpAndSettle(const Duration(seconds: 1)); - expect(find.byType(MemoryBody), findsOneWidget); + await scene.pump(tester); await tester.tap( find.byKey(MemoryScreenKeys.diffTab), ); diff --git a/packages/devtools_app/test/memory/framework/memory_controller_test.dart b/packages/devtools_app/test/memory/framework/memory_controller_test.dart index b9d426bd165..0bc4cecad4e 100644 --- a/packages/devtools_app/test/memory/framework/memory_controller_test.dart +++ b/packages/devtools_app/test/memory/framework/memory_controller_test.dart @@ -2,7 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:devtools_app/devtools_app.dart'; import 'package:devtools_app/src/screens/memory/framework/memory_tabs.dart'; import 'package:devtools_app/src/screens/memory/shared/heap/class_filter.dart'; import 'package:devtools_test/helpers.dart'; @@ -10,7 +9,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import '../../test_infra/scenes/memory/default.dart'; -import '../../test_infra/scenes/scene_test_extensions.dart'; final _filter1 = ClassFilter( except: 'filter1', @@ -25,10 +23,8 @@ final _filter2 = ClassFilter( ); Future pumpScene(WidgetTester tester, MemoryDefaultScene scene) async { - await tester.pumpScene(scene); - // Delay to ensure the memory profiler has collected data. - await tester.pumpAndSettle(const Duration(seconds: 1)); - expect(find.byType(MemoryBody), findsOneWidget); + await scene.pump(tester); + await tester.tap( find.byKey(MemoryScreenKeys.diffTab), ); diff --git a/packages/devtools_app/test/memory/framework/memory_screen_test.dart b/packages/devtools_app/test/memory/framework/memory_screen_test.dart index 43832ce5231..939471886bb 100644 --- a/packages/devtools_app/test/memory/framework/memory_screen_test.dart +++ b/packages/devtools_app/test/memory/framework/memory_screen_test.dart @@ -69,7 +69,8 @@ void main() { ); // Delay to ensure the memory profiler has collected data. - await tester.pumpAndSettle(const Duration(seconds: 1)); + await tester + .runAsync(() async => tester.pumpAndSettle(const Duration(seconds: 1))); expect(find.byType(MemoryBody), findsOneWidget); } @@ -101,15 +102,6 @@ void main() { expect(find.text('GC'), findsOneWidget); expect(find.byType(MemoryVMChart), findsOneWidget); - - expect( - controller.chart.memoryTimeline.liveData.isEmpty, - isTrue, - ); - expect( - controller.chart.memoryTimeline.offlineData.isEmpty, - isTrue, - ); }, ); }); diff --git a/packages/devtools_app/test/memory/framework/memory_service_test.dart b/packages/devtools_app/test/memory/framework/memory_service_test.dart index 94357cd10d4..e29a60faef3 100644 --- a/packages/devtools_app/test/memory/framework/memory_service_test.dart +++ b/packages/devtools_app/test/memory/framework/memory_service_test.dart @@ -29,7 +29,6 @@ void main() { env.afterNewSetup = () async { memoryController = MemoryController(); - memoryController.chart.startTimeline(); }; } } diff --git a/packages/devtools_app/test/memory/profile/allocation_profile_table_view_test.dart b/packages/devtools_app/test/memory/profile/allocation_profile_table_view_test.dart index 495cc66b163..5390f7f5d42 100644 --- a/packages/devtools_app/test/memory/profile/allocation_profile_table_view_test.dart +++ b/packages/devtools_app/test/memory/profile/allocation_profile_table_view_test.dart @@ -2,7 +2,6 @@ // 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/framework/memory_screen.dart'; import 'package:devtools_app/src/screens/memory/framework/memory_tabs.dart'; import 'package:devtools_app/src/screens/memory/panes/profile/model.dart'; import 'package:devtools_app/src/screens/memory/panes/profile/profile_pane_controller.dart'; @@ -17,7 +16,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import '../../test_infra/scenes/memory/default.dart'; -import '../../test_infra/scenes/scene_test_extensions.dart'; void main() { late MemoryDefaultScene scene; @@ -27,13 +25,6 @@ void main() { await scene.setUp(); }); - Future pumpMemoryScreen(WidgetTester tester) async { - await tester.pumpScene(scene); - // Delay to ensure the memory profiler has collected data. - await tester.pumpAndSettle(const Duration(seconds: 1)); - expect(find.byType(MemoryBody), findsOneWidget); - } - // Set a wide enough screen width that we do not run into overflow. const windowSize = Size(2225.0, 1200.0); //setGlobal(NotificationService, NotificationService()); @@ -57,7 +48,7 @@ void main() { 'respects VM Developer Mode setting', windowSize, (WidgetTester tester) async { - await pumpMemoryScreen(tester); + await scene.pump(tester); final allocationProfileController = scene.controller.profile; @@ -156,7 +147,7 @@ void main() { 'manually refreshes', windowSize, (WidgetTester tester) async { - await pumpMemoryScreen(tester); + await scene.pump(tester); final allocationProfileController = scene.controller.profile; await navigateToAllocationProfile(tester, allocationProfileController); @@ -185,7 +176,7 @@ void main() { 'refreshes on GC', windowSize, (WidgetTester tester) async { - await pumpMemoryScreen(tester); + await scene.pump(tester); final allocationProfileController = scene.controller.profile; @@ -227,7 +218,7 @@ void main() { 'sorts correctly', windowSize, (WidgetTester tester) async { - await pumpMemoryScreen(tester); + await scene.pump(tester); final table = find.byType(FlatTable); expect(table, findsOneWidget); diff --git a/packages/devtools_app/test/memory/tracing/tracing_view_test.dart b/packages/devtools_app/test/memory/tracing/tracing_view_test.dart index 52b0483468f..4a583e3327f 100644 --- a/packages/devtools_app/test/memory/tracing/tracing_view_test.dart +++ b/packages/devtools_app/test/memory/tracing/tracing_view_test.dart @@ -18,7 +18,6 @@ import 'package:mockito/mockito.dart'; import 'package:vm_service/vm_service.dart'; import '../../test_infra/scenes/memory/default.dart'; -import '../../test_infra/scenes/scene_test_extensions.dart'; import '../../test_infra/utils/test_utils.dart'; // TODO(bkonyi): add tests for multi-isolate support. @@ -59,10 +58,7 @@ void main() { late final CpuSamples allocationTracingProfile; Future pumpScene(WidgetTester tester) async { - await tester.pumpScene(scene); - // Delay to ensure the memory profiler has collected data. - await tester.pumpAndSettle(const Duration(seconds: 1)); - expect(find.byType(MemoryBody), findsOneWidget); + await scene.pump(tester); await tester.tap( find.byKey(MemoryScreenKeys.dartHeapAllocationTracingTab), ); diff --git a/packages/devtools_app/test/test_infra/scenes/memory/default.dart b/packages/devtools_app/test/test_infra/scenes/memory/default.dart index 2c0b8c7a98a..1d483cbf86b 100644 --- a/packages/devtools_app/test/test_infra/scenes/memory/default.dart +++ b/packages/devtools_app/test/test_infra/scenes/memory/default.dart @@ -21,6 +21,7 @@ import '../../../test_infra/test_data/memory.dart'; import '../../../test_infra/test_data/memory_allocation.dart'; import '../../test_data/memory/heap/heap_data.dart'; import '../../test_data/memory/heap/heap_graph_fakes.dart'; +import '../scene_test_extensions.dart'; // To run: // flutter run -t test/test_infra/scenes/memory/default.stager_app.g.dart -d macos @@ -79,6 +80,13 @@ class MemoryDefaultScene extends Scene { ); } + Future pump(WidgetTester tester) async { + await tester.pumpSceneAsync(this); + // Delay to ensure the memory profiler has collected data. + await tester.pumpAndSettle(const Duration(seconds: 1)); + expect(find.byType(MemoryBody), findsOneWidget); + } + @override /// Sets up the scene. @@ -136,14 +144,14 @@ class MemoryDefaultScene extends Scene { ); final diffController = - DiffPaneController(HeapGraphLoaderProvided(heapProviders)) + DiffPaneController(loader: HeapGraphLoaderProvided(heapProviders)) ..derived.applyFilter(showAllFilter); final profileController = ProfilePaneController()..setFilter(showAllFilter); controller = MemoryController( - diffPaneController: diffController, - profilePaneController: profileController, + connectedDiff: diffController, + connectedProfile: profileController, ) ..chart.memoryTimeline.offlineData.clear() ..chart.memoryTimeline.offlineData.addAll(memoryJson.data); diff --git a/packages/devtools_app/test/test_infra/scenes/memory/diff_snapshot.dart b/packages/devtools_app/test/test_infra/scenes/memory/diff_snapshot.dart index f99fae0a81b..f264c723c0b 100644 --- a/packages/devtools_app/test/test_infra/scenes/memory/diff_snapshot.dart +++ b/packages/devtools_app/test/test_infra/scenes/memory/diff_snapshot.dart @@ -46,7 +46,7 @@ class DiffSnapshotScene extends Scene { ); setGlobal(ServiceConnectionManager, fakeServiceConnection); - diffController = DiffPaneController(HeapGraphLoaderGoldens()); + diffController = DiffPaneController(loader: HeapGraphLoaderGoldens()); setClassFilterToShowAll(); await diffController.takeSnapshot(); diff --git a/packages/devtools_app/test/test_infra/scenes/scene_test_extensions.dart b/packages/devtools_app/test/test_infra/scenes/scene_test_extensions.dart index 0512d53e9aa..0ee0a18fff8 100644 --- a/packages/devtools_app/test/test_infra/scenes/scene_test_extensions.dart +++ b/packages/devtools_app/test/test_infra/scenes/scene_test_extensions.dart @@ -14,4 +14,14 @@ extension StagerTestExtensions on WidgetTester { ), ); } + + Future pumpSceneAsync(Scene scene) async { + await runAsync(() async { + await pumpWidget( + Builder( + builder: (BuildContext context) => scene.build(context), + ), + ); + }); + } }