Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,15 @@ class NetworkController extends DevToolsScreenController
_currentNetworkRequests,
_filterAndRefreshSearchMatches,
);
autoDisposeStreamSubscription(
serviceConnection.serviceManager.isolateManager.onIsolateCreated.listen((
_,
) async {
if (_recordingNotifier.value) {
await allowedError(_enableNetworkTrafficRecordingOnAllIsolates());
Comment thread
muhammadkamel marked this conversation as resolved.
}
}),
);
}

@override
Expand Down Expand Up @@ -350,13 +359,16 @@ class NetworkController extends DevToolsScreenController
]),
);

// TODO(kenz): only call these if http logging and socket profiling are not
// 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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@ class _NetworkProfilerControlsState extends State<_NetworkProfilerControls>
Expanded(
child: SearchField<NetworkController>(
searchController: controller,
searchFieldEnabled: _recording || hasRequests,
searchFieldWidth: screenWidth <= MediaSize.xs
? defaultSearchFieldWidth
: wideSearchFieldWidth,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
});
await _clearSocketProfile();
await _clearHttpProfile();
} on RPCError catch (e) {
if (!e.isServiceDisposedError) {
rethrow;
}
}
}
Comment thread
muhammadkamel marked this conversation as resolved.
}
10 changes: 9 additions & 1 deletion packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,15 @@ TODO: Remove this section if there are not any updates.

## Network profiler updates

TODO: Remove this section if there are not any updates.
* Fixed an issue where the Network tab would stop capturing HTTP requests after
a hot restart. -
[#9856](https://github.com/flutter/devtools/pull/9856)
* Fixed an issue where the Network tab would stop capturing new HTTP requests
after pressing Clear while recording. -
[#9856](https://github.com/flutter/devtools/pull/9856)
* Fixed an issue where the Network tab search field was disabled after pressing
Clear while recording. -
[#9856](https://github.com/flutter/devtools/pull/9856)

## Logging updates

Expand Down
312 changes: 312 additions & 0 deletions packages/devtools_app/test/screens/network/network_clear_test.dart
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,
);
},
);

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'),
);
});
});
});
}
Loading