Skip to content

Commit 37aa311

Browse files
Dispose all disposables in the Network screen (#8937)
* Dispose all disposables in the Network screen * rnotes * fix bugs and tests * rnotes * add back accidental deletion * comments * dcm lints
1 parent 657f821 commit 37aa311

12 files changed

Lines changed: 197 additions & 83 deletions

File tree

packages/devtools_app/lib/src/screens/inspector_v2/inspector_tree_controller.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1295,7 +1295,7 @@ class _RowPainter extends CustomPainter {
12951295
final width =
12961296
showExpandCollapse
12971297
? inspectorColumnIndent * 0.45
1298-
: inspectorColumnIndent * .6;
1298+
: inspectorColumnIndent * 0.6;
12991299
canvas.drawLine(
13001300
Offset(parentExpandCollapseX, 0.0),
13011301
Offset(parentExpandCollapseX, inspectorRowHeight * 0.5),

packages/devtools_app/lib/src/screens/memory/panes/chart/controller/chart_connection.dart

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,9 @@ class ChartVmConnection extends DisposableController
7979
),
8080
);
8181

82-
_polling = DebounceTimer.periodic(chartUpdateDelay, () async {
82+
_polling = DebounceTimer.periodic(chartUpdateDelay, ({
83+
DebounceCancelledCallback? cancelledCallback,
84+
}) async {
8385
if (!_isConnected) {
8486
_polling?.cancel();
8587
return;

packages/devtools_app/lib/src/screens/network/network_controller.dart

Lines changed: 56 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -45,24 +45,22 @@ enum NetworkResponseViewType {
4545

4646
enum _NetworkTrafficType { http, socket }
4747

48+
/// Screen controller for the Network screen.
49+
///
50+
/// This controller can be accessed from anywhere in DevTools, as long as it was
51+
/// first registered, by
52+
/// calling `screenControllers.lookup<NetworkController>()`.
53+
///
54+
/// The controller lifecycle is managed by the [ScreenControllers] class. The
55+
/// `init` method is called lazily upon the first controller access from
56+
/// `screenControllers`. The `dispose` method is called by `screenControllers`
57+
/// when DevTools is destroying a set of DevTools screen controllers.
4858
class NetworkController extends DevToolsScreenController
4959
with
5060
SearchControllerMixin<NetworkRequest>,
5161
FilterControllerMixin<NetworkRequest>,
5262
OfflineScreenControllerMixin,
5363
AutoDisposeControllerMixin {
54-
NetworkController() {
55-
_networkService = NetworkService(this);
56-
_currentNetworkRequests = CurrentNetworkRequests();
57-
_initHelper();
58-
addAutoDisposeListener(
59-
_currentNetworkRequests,
60-
_filterAndRefreshSearchMatches,
61-
);
62-
// TODO(https://github.com/flutter/devtools/issues/7727): add support for
63-
// persisting network filter.
64-
initFilterController();
65-
}
6664
List<DartIOHttpRequestData>? _httpRequests;
6765

6866
Future<String?> exportAsHarFile() async {
@@ -146,9 +144,7 @@ class NetworkController extends DevToolsScreenController
146144
ValueListenable<bool> get recordingNotifier => _recordingNotifier;
147145
final _recordingNotifier = ValueNotifier<bool>(false);
148146

149-
@visibleForTesting
150-
NetworkService get networkService => _networkService;
151-
late NetworkService _networkService;
147+
final networkService = NetworkService();
152148

153149
/// The timeline timestamps are relative to when the VM started.
154150
///
@@ -168,6 +164,32 @@ class NetworkController extends DevToolsScreenController
168164
@visibleForTesting
169165
bool get isPolling => _pollingTimer != null;
170166

167+
static const _pollingDuration = Duration(milliseconds: 2000);
168+
169+
@override
170+
void init() {
171+
super.init();
172+
_currentNetworkRequests = CurrentNetworkRequests();
173+
_initHelper();
174+
addAutoDisposeListener(
175+
_currentNetworkRequests,
176+
_filterAndRefreshSearchMatches,
177+
);
178+
initFilterController();
179+
}
180+
181+
@override
182+
void dispose() {
183+
// Cancel and dispose the polling timer before disposing anything else.
184+
_pollingTimer?.dispose();
185+
_pollingTimer = null;
186+
_currentResponseViewType.dispose();
187+
selectedRequest.dispose();
188+
_recordingNotifier.dispose();
189+
_currentNetworkRequests.dispose();
190+
super.dispose();
191+
}
192+
171193
void _initHelper() async {
172194
if (offlineDataController.showingOfflineData.value) {
173195
await maybeLoadOfflineData(
@@ -254,8 +276,8 @@ class NetworkController extends DevToolsScreenController
254276
_pollingTimer ??= DebounceTimer.periodic(
255277
// TODO(kenz): look into improving performance by caching more data.
256278
// Polling less frequently helps performance.
257-
const Duration(milliseconds: 2000),
258-
_networkService.refreshNetworkData,
279+
_pollingDuration,
280+
networkService.refreshNetworkData,
259281
);
260282
} else {
261283
_pollingTimer?.cancel();
@@ -286,10 +308,10 @@ class NetworkController extends DevToolsScreenController
286308
// Cancel existing polling timer before starting recording.
287309
_updatePollingState(false);
288310

289-
_networkService.updateLastHttpDataRefreshTime(
311+
networkService.updateLastHttpDataRefreshTime(
290312
alreadyRecordingHttp: alreadyRecordingHttp,
291313
);
292-
final timestamp = await _networkService.updateLastSocketDataRefreshTime(
314+
final timestamp = await networkService.updateLastSocketDataRefreshTime(
293315
alreadyRecordingSocketData: alreadyRecordingSocketData,
294316
);
295317

@@ -319,7 +341,9 @@ class NetworkController extends DevToolsScreenController
319341
}
320342

321343
Future<void> stopRecording() async {
322-
await togglePolling(false);
344+
if (!disposed) {
345+
await togglePolling(false);
346+
}
323347
}
324348

325349
Future<void> togglePolling(bool state) async {
@@ -339,8 +363,8 @@ class NetworkController extends DevToolsScreenController
339363
/// This will ensure that future fetches for http and socket requests will at
340364
/// most fetch requests since [updateLastRefreshTime] was called.
341365
Future<void> updateLastRefreshTime() async {
342-
_networkService.updateLastHttpDataRefreshTime();
343-
await _networkService.updateLastSocketDataRefreshTime();
366+
networkService.updateLastHttpDataRefreshTime();
367+
await networkService.updateLastSocketDataRefreshTime();
344368
}
345369

346370
Future<bool> _recordingNetworkTraffic({
@@ -370,12 +394,21 @@ class NetworkController extends DevToolsScreenController
370394
/// Clears the HTTP profile and socket profile from the vm, and resets the
371395
/// last refresh timestamp to the current time.
372396
Future<void> clear() async {
373-
await _networkService.clearData();
397+
await networkService.clearData();
374398
_currentNetworkRequests.clear();
375399
_filterAndRefreshSearchMatches();
376400
_updateSelection();
377401
}
378402

403+
@override
404+
void setActiveFilter({
405+
String? query,
406+
SettingFilters<NetworkRequest>? settingFilters,
407+
}) {
408+
super.setActiveFilter(query: query, settingFilters: settingFilters);
409+
_filterAndRefreshSearchMatches();
410+
}
411+
379412
void _filterAndRefreshSearchMatches() {
380413
filterData(activeFilter.value);
381414
refreshSearchMatches();

packages/devtools_app/lib/src/screens/network/network_screen.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,10 +143,10 @@ class _NetworkScreenBodyState extends State<NetworkScreenBody>
143143

144144
@override
145145
void dispose() {
146+
unawaited(controller.stopRecording());
146147
// TODO(kenz): this won't work well if we eventually have multiple clients
147148
// that want to listen to network data.
148149
super.dispose();
149-
unawaited(controller.stopRecording());
150150
}
151151

152152
@override

packages/devtools_app/lib/src/screens/network/network_service.dart

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@ import 'package:vm_service/vm_service.dart';
66

77
import '../../shared/globals.dart';
88
import '../../shared/primitives/utils.dart';
9+
import '../../shared/utils/utils.dart';
910
import 'network_controller.dart';
1011

1112
class NetworkService {
12-
NetworkService(this.networkController);
13-
14-
final NetworkController networkController;
13+
NetworkController get networkController =>
14+
screenControllers.lookup<NetworkController>();
1515

1616
/// Tracks the time (microseconds since epoch) that the HTTP profile was last
1717
/// retrieved for a given isolate ID.
@@ -61,15 +61,26 @@ class NetworkService {
6161

6262
/// Force refreshes the HTTP requests logged to the timeline as well as any
6363
/// recorded Socket traffic.
64-
Future<void> refreshNetworkData() async {
64+
///
65+
/// This method calls `cancelledCallback` after each async gap to ensure that
66+
/// this operation has not been cancelled during the async gap.
67+
Future<void> refreshNetworkData({
68+
DebounceCancelledCallback? cancelledCallback,
69+
}) async {
6570
if (serviceConnection.serviceManager.service == null) return;
6671
final timestampObj =
6772
await serviceConnection.serviceManager.service!.getVMTimelineMicros();
73+
if (cancelledCallback?.call() ?? false) return;
74+
6875
final timestamp = timestampObj.timestamp!;
6976
final sockets = await _refreshSockets();
77+
if (cancelledCallback?.call() ?? false) return;
78+
7079
networkController.lastSocketDataRefreshMicros = timestamp;
7180
List<HttpProfileRequest>? httpRequests;
7281
httpRequests = await _refreshHttpProfile();
82+
if (cancelledCallback?.call() ?? false) return;
83+
7384
networkController.processNetworkTraffic(
7485
sockets: sockets,
7586
httpRequests: httpRequests,

packages/devtools_app/lib/src/screens/vm_developer/object_inspector/vm_ic_data_display.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ class _VmICDataDisplayState extends State<VmICDataDisplay> {
3434
final entries = <ObjRef?>[];
3535

3636
Future<void> get _initialized => _initializingCompleter.future;
37-
final Completer<void> _initializingCompleter = Completer<void>();
37+
final _initializingCompleter = Completer<void>();
3838

3939
@override
4040
void initState() {

packages/devtools_app/lib/src/shared/utils/utils.dart

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,8 @@ extension IsKeyType on KeyEvent {
185185
bool get isKeyDownOrRepeat => this is KeyDownEvent || this is KeyRepeatEvent;
186186
}
187187

188+
typedef DebounceCancelledCallback = bool Function();
189+
188190
/// A helper class for [Timer] functionality, where the callbacks are debounced.
189191
class DebounceTimer {
190192
/// A periodic timer that ensures [callback] is only called at most once
@@ -193,8 +195,11 @@ class DebounceTimer {
193195
/// [callback] is triggered once immediately, and then every [duration] the
194196
/// timer checks to see if the previous [callback] call has finished running.
195197
/// If it has finished, then then next call to [callback] will begin.
196-
DebounceTimer.periodic(Duration duration, Future<void> Function() callback)
197-
: _callback = callback {
198+
DebounceTimer.periodic(
199+
Duration duration,
200+
Future<void> Function({DebounceCancelledCallback? cancelledCallback})
201+
callback,
202+
) : _callback = callback {
198203
// Start running the first call to the callback.
199204
_runCallback();
200205

@@ -210,23 +215,28 @@ class DebounceTimer {
210215
return;
211216
}
212217

218+
if (isCancelled) return;
219+
213220
try {
214221
_isRunning = true;
215-
await _callback();
222+
await _callback(cancelledCallback: () => isCancelled);
216223
} finally {
217224
_isRunning = false;
218225
}
219226
}
220227

221-
late final Timer _timer;
222-
final Future<void> Function() _callback;
228+
Timer? _timer;
229+
final Future<void> Function({DebounceCancelledCallback? cancelledCallback})
230+
_callback;
223231
bool _isRunning = false;
232+
bool _isCancelled = false;
224233

225234
void cancel() {
226-
_timer.cancel();
235+
_isCancelled = true;
236+
_timer?.cancel();
227237
}
228238

229-
bool get isCancelled => !_timer.isActive;
239+
bool get isCancelled => _isCancelled || (_timer != null && !_timer!.isActive);
230240

231241
void dispose() {
232242
cancel();

packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ To learn more about DevTools, check out the
2323
[#8932](https://github.com/flutter/devtools/pull/8932),
2424
[#8933](https://github.com/flutter/devtools/pull/8933),
2525
[#8934](https://github.com/flutter/devtools/pull/8934),
26-
[#8935](https://github.com/flutter/devtools/pull/8935)
26+
[#8935](https://github.com/flutter/devtools/pull/8935),
27+
[#8937](https://github.com/flutter/devtools/pull/8937)
2728

2829
## Inspector updates
2930

packages/devtools_app/test/screens/network/network_controller_test.dart

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ void main() {
2323

2424
setUp(() {
2525
setGlobal(OfflineDataController, OfflineDataController());
26+
setGlobal(ScreenControllers, ScreenControllers());
2627
socketProfile = loadSocketProfile();
2728
httpProfile = loadHttpProfile();
2829
fakeServiceConnection = FakeServiceConnectionManager(
@@ -33,7 +34,13 @@ void main() {
3334
);
3435
setGlobal(ServiceConnectionManager, fakeServiceConnection);
3536
setGlobal(PreferencesController, PreferencesController());
36-
controller = NetworkController();
37+
screenControllers.register<NetworkController>(() => NetworkController());
38+
// Lookup the controller immediately to force initialization.
39+
controller = screenControllers.lookup<NetworkController>();
40+
});
41+
42+
tearDown(() {
43+
screenControllers.disposeConnectedControllers();
3744
});
3845

3946
test('initialize recording state', () async {
@@ -48,11 +55,10 @@ void main() {
4855

4956
test('start and pause recording', () async {
5057
expect(controller.isPolling, false);
51-
final notifier = controller.recordingNotifier;
5258
await addListenerScope(
53-
listenable: notifier,
59+
listenable: controller.recordingNotifier,
5460
listener: () {
55-
expect(notifier.value, true);
61+
expect(controller.recordingNotifier.value, true);
5662
expect(controller.isPolling, true);
5763
},
5864
callback: () async {
@@ -62,16 +68,16 @@ void main() {
6268

6369
// Pause polling.
6470
await controller.togglePolling(false);
65-
expect(notifier.value, false);
71+
expect(controller.recordingNotifier.value, false);
6672
expect(controller.isPolling, false);
6773

6874
// Resume polling.
6975
await controller.togglePolling(true);
70-
expect(notifier.value, true);
76+
expect(controller.recordingNotifier.value, true);
7177
expect(controller.isPolling, true);
7278

7379
await controller.stopRecording();
74-
expect(notifier.value, false);
80+
expect(controller.recordingNotifier.value, false);
7581
expect(controller.isPolling, false);
7682
});
7783

packages/devtools_app/test/screens/network/network_model_test.dart

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,10 +87,17 @@ void main() {
8787
setGlobal(ServiceConnectionManager, fakeServiceConnection);
8888
setGlobal(PreferencesController, PreferencesController());
8989
setGlobal(OfflineDataController, OfflineDataController());
90-
controller = NetworkController();
90+
setGlobal(ScreenControllers, ScreenControllers());
91+
screenControllers.register<NetworkController>(() => NetworkController());
92+
// Lookup the controller immediately to force initialization.
93+
controller = screenControllers.lookup<NetworkController>();
9194
await controller.startRecording();
9295
});
9396

97+
tearDown(() {
98+
screenControllers.disposeConnectedControllers();
99+
});
100+
94101
test('method returns correct value', () {
95102
expect(httpGet.method, 'GET');
96103
expect(httpGetWithError.method, 'GET');

0 commit comments

Comments
 (0)