-
Notifications
You must be signed in to change notification settings - Fork 399
Fix Network tab not capturing requests after hot restart and clear #9856
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
a6a57ff
043132a
a4a7608
7d34604
08e070f
afdea78
2149f89
f37ac2a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -192,6 +192,15 @@ class NetworkController extends DevToolsScreenController | |
| _currentNetworkRequests, | ||
| _filterAndRefreshSearchMatches, | ||
| ); | ||
| autoDisposeStreamSubscription( | ||
| serviceConnection.serviceManager.isolateManager.onIsolateCreated.listen(( | ||
| _, | ||
| ) async { | ||
| if (_recordingNotifier.value) { | ||
| await allowedError(_enableNetworkTrafficRecordingOnAllIsolates()); | ||
| } | ||
| }), | ||
| ); | ||
| } | ||
|
|
||
| @override | ||
|
|
@@ -350,13 +359,16 @@ class NetworkController extends DevToolsScreenController | |
| ]), | ||
| ); | ||
|
|
||
| // TODO(kenz): only call these if http logging and socket profiling are not | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Did this TODO get addressed by this PR? If not, please add it back so that we don't lose track of this work. |
||
| // already enabled. Listen to service manager streams for this info. | ||
| await _enableNetworkTrafficRecordingOnAllIsolates(); | ||
| await togglePolling(true); | ||
| } | ||
|
|
||
| /// Enables HTTP timeline logging and socket profiling on all isolates. | ||
| Future<void> _enableNetworkTrafficRecordingOnAllIsolates() async { | ||
| await [ | ||
| http_service.toggleHttpRequestLogging(true), | ||
| networkService.toggleSocketProfiling(true), | ||
| ].wait; | ||
| await togglePolling(true); | ||
| } | ||
|
|
||
| Future<void> stopRecording() async { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -230,6 +230,7 @@ class _NetworkProfilerControlsState extends State<_NetworkProfilerControls> | |
| Expanded( | ||
| child: SearchField<NetworkController>( | ||
| searchController: controller, | ||
| searchFieldEnabled: _recording || hasRequests, | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This parameter was removed in #9855 so that the SearchField remained editable even when there were not requests. Please remove this parameter so that it always uses the default value of true. |
||
| searchFieldWidth: screenWidth <= MediaSize.xs | ||
| ? defaultSearchFieldWidth | ||
| : wideSearchFieldWidth, | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -202,9 +202,21 @@ class NetworkService { | |||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| Future<void> clearData() async { | ||||||||||||||||||||||||||||
| await updateLastSocketDataRefreshTime(); | ||||||||||||||||||||||||||||
| updateLastHttpDataRefreshTime(); | ||||||||||||||||||||||||||||
| await _clearSocketProfile(); | ||||||||||||||||||||||||||||
| await _clearHttpProfile(); | ||||||||||||||||||||||||||||
| final service = serviceConnection.serviceManager.service; | ||||||||||||||||||||||||||||
| if (service == null) return; | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||
| final timestamp = (await service.getVMTimelineMicros()).timestamp!; | ||||||||||||||||||||||||||||
| networkController.lastSocketDataRefreshMicros = timestamp; | ||||||||||||||||||||||||||||
| await service.forEachIsolate((isolate) async { | ||||||||||||||||||||||||||||
| lastHttpDataRefreshTimePerIsolate[isolate.id!] = timestamp; | ||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||
|
Comment on lines
+208
to
+213
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [MUST-FIX] There is a clock mismatch bug here. Setting You should use
Suggested change
Comment on lines
+209
to
+213
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why can't we use the |
||||||||||||||||||||||||||||
| await _clearSocketProfile(); | ||||||||||||||||||||||||||||
| await _clearHttpProfile(); | ||||||||||||||||||||||||||||
| } on RPCError catch (e) { | ||||||||||||||||||||||||||||
| if (!e.isServiceDisposedError) { | ||||||||||||||||||||||||||||
| rethrow; | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
muhammadkamel marked this conversation as resolved.
|
||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,312 @@ | ||
| // Copyright 2026 The Flutter Authors | ||
| // Use of this source code is governed by a BSD-style license that can be | ||
| // found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd. | ||
|
|
||
| @TestOn('vm') | ||
| library; | ||
|
|
||
| import 'package:devtools_app/devtools_app.dart'; | ||
| import 'package:devtools_test/devtools_test.dart'; | ||
| import 'package:flutter_test/flutter_test.dart'; | ||
|
|
||
| import 'utils/hot_restart_network_vm_service.dart'; | ||
| import 'utils/network_lifecycle_test_utils.dart'; | ||
| import 'utils/network_test_utils.dart'; | ||
|
|
||
| void main() { | ||
| group('Network View clear button', () { | ||
| late HotRestartNetworkVmService vmService; | ||
| late FakeServiceConnectionManager fakeServiceConnection; | ||
|
|
||
| setUp(() { | ||
| vmService = HotRestartNetworkVmService(); | ||
| fakeServiceConnection = FakeServiceConnectionManager(service: vmService); | ||
| }); | ||
|
|
||
| tearDown(disposeNetworkLifecycleControllers); | ||
|
|
||
| group('request visibility after clear', () { | ||
| test('displays new HTTP requests after clear while recording', () async { | ||
| final isolateId = vmService.currentIsolateId; | ||
| final controller = await initNetworkLifecycleController( | ||
| vmService: vmService, | ||
| fakeServiceConnection: fakeServiceConnection, | ||
| initialProfile: [ | ||
| createTestHttpRequest(id: 'before-clear', method: 'GET'), | ||
| ], | ||
| ); | ||
| await controller.networkService.refreshNetworkData(); | ||
| expect(controller.requests.value, hasLength(1)); | ||
|
|
||
| await controller.clear(); | ||
| expect(controller.requests.value, isEmpty); | ||
|
|
||
| vmService.appendHttpRequest( | ||
| isolateId, | ||
| createTestHttpRequest(id: 'after-clear', method: 'POST'), | ||
| ); | ||
| await controller.networkService.refreshNetworkData(); | ||
|
|
||
| expect( | ||
| controller.requests.value.whereType<DartIOHttpRequestData>().map( | ||
| (request) => request.method, | ||
| ), | ||
| contains('POST'), | ||
| reason: | ||
| 'Network View should display new requests after Clear while ' | ||
| 'recording is active.', | ||
| ); | ||
| }); | ||
|
|
||
| test('polling remains active after clear', () async { | ||
| final controller = await initNetworkLifecycleController( | ||
| vmService: vmService, | ||
| fakeServiceConnection: fakeServiceConnection, | ||
| initialProfile: [ | ||
| createTestHttpRequest(id: 'polling-test', method: 'GET'), | ||
| ], | ||
| ); | ||
| await controller.networkService.refreshNetworkData(); | ||
|
|
||
| await controller.clear(); | ||
|
|
||
| expect(controller.isPolling, isTrue); | ||
| expect(controller.recordingNotifier.value, isTrue); | ||
| }); | ||
|
|
||
| test('keeps HTTP logging enabled after clear', () async { | ||
| final isolateId = vmService.currentIsolateId; | ||
| final controller = await initNetworkLifecycleController( | ||
| vmService: vmService, | ||
| fakeServiceConnection: fakeServiceConnection, | ||
| initialProfile: [ | ||
| createTestHttpRequest(id: 'logging-test', method: 'GET'), | ||
| ], | ||
| ); | ||
| await controller.networkService.refreshNetworkData(); | ||
|
|
||
| await controller.clear(); | ||
|
|
||
| expect(vmService.isHttpLoggingEnabled(isolateId), isTrue); | ||
| }); | ||
|
|
||
| test('keeps socket profiling enabled after clear', () async { | ||
| final isolateId = vmService.currentIsolateId; | ||
| final controller = await initNetworkLifecycleController( | ||
| vmService: vmService, | ||
| fakeServiceConnection: fakeServiceConnection, | ||
| initialProfile: [ | ||
| createTestHttpRequest(id: 'socket-test', method: 'GET'), | ||
| ], | ||
| ); | ||
| await controller.networkService.refreshNetworkData(); | ||
|
|
||
| await controller.clear(); | ||
|
|
||
| expect(vmService.isSocketProfilingEnabled(isolateId), isTrue); | ||
| }); | ||
| }); | ||
|
|
||
| group('clear refresh timestamp tracking', () { | ||
| test( | ||
| 'resets HTTP refresh timestamps to the VM timeline on clear', | ||
| () async { | ||
| final isolateId = vmService.currentIsolateId; | ||
| final controller = await initNetworkLifecycleController( | ||
| vmService: vmService, | ||
| fakeServiceConnection: fakeServiceConnection, | ||
| ); | ||
| await controller.networkService.refreshNetworkData(); | ||
|
|
||
| controller | ||
| .networkService | ||
| .lastHttpDataRefreshTimePerIsolate[isolateId] = | ||
| 500_000; | ||
|
|
||
| await controller.clear(); | ||
|
|
||
| final timelineMicros = | ||
| (await vmService.getVMTimelineMicros()).timestamp!; | ||
| expect( | ||
| controller | ||
| .networkService | ||
| .lastHttpDataRefreshTimePerIsolate[isolateId], | ||
| timelineMicros, | ||
| ); | ||
| }, | ||
|
Comment on lines
+111
to
+136
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [CONCERN] This test asserts the incorrect behavior of resetting the HTTP refresh timestamps to the VM timeline instead of the wall-clock time. Once test(
'resets HTTP refresh timestamps to the wall-clock time on clear',
() async {
final isolateId = vmService.currentIsolateId;
final controller = await initNetworkLifecycleController(
vmService: vmService,
fakeServiceConnection: fakeServiceConnection,
);
await controller.networkService.refreshNetworkData();
controller
.networkService
.lastHttpDataRefreshTimePerIsolate[isolateId] =
500_000;
final beforeClear = DateTime.now().microsecondsSinceEpoch;
await controller.clear();
final afterClear = DateTime.now().microsecondsSinceEpoch;
final refreshTime = controller
.networkService
.lastHttpDataRefreshTimePerIsolate[isolateId]!;
expect(refreshTime, greaterThanOrEqualTo(beforeClear));
expect(refreshTime, lessThanOrEqualTo(afterClear));
}, |
||
| ); | ||
|
|
||
| test('does not show stale requests after clear', () async { | ||
| final controller = await initNetworkLifecycleController( | ||
| vmService: vmService, | ||
| fakeServiceConnection: fakeServiceConnection, | ||
| initialProfile: [ | ||
| createTestHttpRequest( | ||
| id: 'stale-request', | ||
| method: 'GET', | ||
| startTime: 1_500_000, | ||
| ), | ||
| ], | ||
| ); | ||
| await controller.networkService.refreshNetworkData(); | ||
| expect(controller.requests.value, hasLength(1)); | ||
|
|
||
| await controller.clear(); | ||
| await controller.networkService.refreshNetworkData(); | ||
|
|
||
| expect(controller.requests.value, isEmpty); | ||
| }); | ||
| }); | ||
|
|
||
| group('clear combined with hot restart', () { | ||
| test('clear then hot restart then new requests', () async { | ||
| final controller = await initNetworkLifecycleController( | ||
| vmService: vmService, | ||
| fakeServiceConnection: fakeServiceConnection, | ||
| initialProfile: [ | ||
| createTestHttpRequest(id: 'pre-clear', method: 'GET'), | ||
| ], | ||
| ); | ||
| await controller.networkService.refreshNetworkData(); | ||
| await controller.clear(); | ||
|
|
||
| final postRestartIsolateId = vmService.simulateHotRestart(); | ||
| notifyMainIsolateChanged(fakeServiceConnection, postRestartIsolateId); | ||
| await pumpEventQueue(); | ||
|
|
||
| vmService.appendHttpRequest( | ||
| postRestartIsolateId, | ||
| createTestHttpRequest( | ||
| id: 'after-clear-and-restart', | ||
| method: 'PUT', | ||
| startTime: 9_000_000, | ||
| ), | ||
| ); | ||
| await controller.networkService.refreshNetworkData(); | ||
|
|
||
| expect( | ||
| controller.requests.value.whereType<DartIOHttpRequestData>().map( | ||
| (request) => request.method, | ||
| ), | ||
| contains('PUT'), | ||
| ); | ||
| expect(vmService.isHttpLoggingEnabled(postRestartIsolateId), isTrue); | ||
| }); | ||
|
|
||
| test('hot restart then clear then new requests', () async { | ||
| final controller = await initNetworkLifecycleController( | ||
| vmService: vmService, | ||
| fakeServiceConnection: fakeServiceConnection, | ||
| initialProfile: [ | ||
| createTestHttpRequest(id: 'pre-restart', method: 'GET'), | ||
| ], | ||
| ); | ||
| await controller.networkService.refreshNetworkData(); | ||
|
|
||
| final postRestartIsolateId = vmService.simulateHotRestart(); | ||
| notifyMainIsolateChanged(fakeServiceConnection, postRestartIsolateId); | ||
| await pumpEventQueue(); | ||
| vmService.appendHttpRequest( | ||
| postRestartIsolateId, | ||
| createTestHttpRequest( | ||
| id: 'after-restart', | ||
| method: 'POST', | ||
| startTime: 8_000_000, | ||
| ), | ||
| ); | ||
| await controller.networkService.refreshNetworkData(); | ||
| expect(controller.requests.value, isNotEmpty); | ||
|
|
||
| await controller.clear(); | ||
| expect(controller.requests.value, isEmpty); | ||
|
|
||
| vmService.appendHttpRequest( | ||
| postRestartIsolateId, | ||
| createTestHttpRequest( | ||
| id: 'after-restart-and-clear', | ||
| method: 'DELETE', | ||
| startTime: 10_000_000, | ||
| ), | ||
| ); | ||
| await controller.networkService.refreshNetworkData(); | ||
|
|
||
| expect( | ||
| controller.requests.value.whereType<DartIOHttpRequestData>().map( | ||
| (request) => request.method, | ||
| ), | ||
| contains('DELETE'), | ||
| ); | ||
| }); | ||
| }); | ||
|
|
||
| group('state after clear', () { | ||
| test('clears selected request', () async { | ||
| final controller = await initNetworkLifecycleController( | ||
| vmService: vmService, | ||
| fakeServiceConnection: fakeServiceConnection, | ||
| initialProfile: [ | ||
| createTestHttpRequest(id: 'selected', method: 'GET'), | ||
| ], | ||
| ); | ||
| await controller.networkService.refreshNetworkData(); | ||
| controller.selectedRequest.value = controller.requests.value.first; | ||
|
|
||
| await controller.clear(); | ||
|
|
||
| expect(controller.selectedRequest.value, isNull); | ||
| }); | ||
|
|
||
| test('preserves active search text', () async { | ||
| final controller = await initNetworkLifecycleController( | ||
| vmService: vmService, | ||
| fakeServiceConnection: fakeServiceConnection, | ||
| initialProfile: [ | ||
| createTestHttpRequest( | ||
| id: 'searchable', | ||
| method: 'GET', | ||
| uri: 'https://example.com/api', | ||
| ), | ||
| ], | ||
| ); | ||
| await controller.networkService.refreshNetworkData(); | ||
| controller.search = 'example'; | ||
|
|
||
| await controller.clear(); | ||
|
|
||
| expect(controller.search, 'example'); | ||
| }); | ||
|
|
||
| test('supports multiple consecutive clears', () async { | ||
| final isolateId = vmService.currentIsolateId; | ||
| final controller = await initNetworkLifecycleController( | ||
| vmService: vmService, | ||
| fakeServiceConnection: fakeServiceConnection, | ||
| initialProfile: [ | ||
| createTestHttpRequest(id: 'multi-clear-1', method: 'GET'), | ||
| ], | ||
| ); | ||
| await controller.networkService.refreshNetworkData(); | ||
|
|
||
| await controller.clear(); | ||
| await controller.clear(); | ||
|
|
||
| vmService.appendHttpRequest( | ||
| isolateId, | ||
| createTestHttpRequest( | ||
| id: 'after-multi-clear', | ||
| method: 'PATCH', | ||
| startTime: 3_000_000, | ||
| ), | ||
| ); | ||
| await controller.networkService.refreshNetworkData(); | ||
|
|
||
| expect( | ||
| controller.requests.value.whereType<DartIOHttpRequestData>().map( | ||
| (request) => request.method, | ||
| ), | ||
| contains('PATCH'), | ||
| ); | ||
| }); | ||
| }); | ||
| }); | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.