Skip to content
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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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 {
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,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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,
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;
});
Comment on lines +208 to +213

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

[MUST-FIX] There is a clock mismatch bug here. lastHttpDataRefreshTimePerIsolate tracks timestamps in wall-clock microseconds since epoch (as seen in updateLastHttpDataRefreshTime and _refreshHttpProfile). However, getVMTimelineMicros() returns monotonic VM timeline microseconds, which is completely different from wall-clock time on real devices.

Setting lastHttpDataRefreshTimePerIsolate to the VM timeline timestamp on clear will cause subsequent HTTP profile requests to use a 1970-era timestamp for updatedSince, resulting in all historical HTTP requests being fetched again on every poll after a clear.

You should use DateTime.now().microsecondsSinceEpoch for the HTTP refresh timestamps, while keeping the VM timeline timestamp for lastSocketDataRefreshMicros.

Suggested change
try {
final timestamp = (await service.getVMTimelineMicros()).timestamp!;
networkController.lastSocketDataRefreshMicros = timestamp;
await service.forEachIsolate((isolate) async {
lastHttpDataRefreshTimePerIsolate[isolate.id!] = timestamp;
});
try {
final timestamp = (await service.getVMTimelineMicros()).timestamp!;
networkController.lastSocketDataRefreshMicros = timestamp;
final wallClockTimestamp = DateTime.now().microsecondsSinceEpoch;
await service.forEachIsolate((isolate) async {
lastHttpDataRefreshTimePerIsolate[isolate.id!] = wallClockTimestamp;
});

Comment on lines +209 to +213

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why can't we use the updateLastSocketDataRefreshTime and updateLastHttpDataRefreshTime helpers here? Is there a bug in those methods that needs to be fixed rather than updating these variables in a different way here?

await _clearSocketProfile();
await _clearHttpProfile();
} on RPCError catch (e) {
if (!e.isServiceDisposedError) {
rethrow;
}
}
}
Comment thread
muhammadkamel marked this conversation as resolved.
}
7 changes: 6 additions & 1 deletion packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,12 @@ 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)

## 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,
);
},
Comment on lines +111 to +136

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

[CONCERN] This test asserts the incorrect behavior of resetting the HTTP refresh timestamps to the VM timeline instead of the wall-clock time. Once NetworkService.clearData() is corrected to use DateTime.now().microsecondsSinceEpoch, this test should be updated to verify that the timestamp is reset to the wall-clock time.

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