Skip to content
Open
Show file tree
Hide file tree
Changes from 8 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

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch — it was not addressed by this PR. I restored the TODO above _enableNetworkTrafficRecordingOnAllIsolates().

// 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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done — removed searchFieldEnabled so the field always uses the default (true), matching #9855.

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;
});

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;
});

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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

You're right — we should use the helpers here.

There was a real bug in updateLastHttpDataRefreshTime: it stamped refresh times with DateTime.now() (wall clock), but getHttpProfile's updatedSince expects the VM timeline clock. That meant pause/resume could filter out all subsequent requests.

I updated updateLastHttpDataRefreshTime to fetch (await service.getVMTimelineMicros()).timestamp!, and clearData() now calls updateLastSocketDataRefreshTime() / updateLastHttpDataRefreshTime() again instead of writing the maps directly.

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 +137

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