Skip to content

Commit aac691b

Browse files
authored
Added size support to Network tab (#9744)
## Overview This PR adds support for displaying response payload size in the Network tab and fixes #6165. It introduces a new **"Size"** column in the network requests table and displays response size in the **Overview panel** of the request inspector. --- ## Changes ### 1. Data Model Updates **File:** `packages/devtools_app/lib/src/screens/network/network_model.dart` * Added two new getters to the `NetworkRequest` base class: * `requestBytes` * `responseBytes` * Implemented these getters in the `Socket` class using: * `writeBytes` : request size * `readBytes` : response size **Purpose:** Expose byte-level data in a unified way for all network request types. --- ### 2. HTTP Data Handling **File:** `packages/devtools_app/lib/src/shared/http/http_request_data.dart` * Added logic to extract response size using the `content-length` header * Handles both `String` and `List<String>` header formats **Purpose:** Provide response size for HTTP requests when available, without requiring changes to the Dart VM. --- ### 3. Shared Utility **File:** `packages/devtools_app/lib/src/screens/network/utils/http_utils.dart` * Moved `formatBytes` into a reusable utility function * Uses **decimal (base-10) units** (`kB`, `MB`) to align with Chrome DevTools * Handles null and negative values safely **Purpose:** Ensure consistent formatting across the network table and inspector views. --- ### 4. Network Table UI **File:** `packages/devtools_app/lib/src/screens/network/network_screen.dart` * Added a new column: **"Size"** * Displays formatted response size * Shows `-` when size is unavailable --- ### 5. Request Inspector (Overview Panel) **File:** `packages/devtools_app/lib/src/screens/network/network_request_inspector_views.dart` * Added a new row: * **Response Size** * Uses shared `formatBytes` utility --- ### 6. Tests * Added unit tests for: * `formatBytes` utility in `http_utils_test.dart` * `responseBytes` parsing logic in `http_request_data.dart` * Covers edge cases including: * string and list headers * invalid values * null handling --- ## Why request size is not included Request size is not reliably available for HTTP requests due to limitations in the current DevTools and VM service APIs: * The Dart VM does not expose request payload size in `HttpProfileRequest` * HTTP request bodies are not always accessible or fully captured * Headers such as `content-length` are often absent for outgoing requests * Streaming and chunked requests complicate accurate measurement While socket-level request size (`writeBytes`) is available, it is not consistently applicable to HTTP requests. Therefore, including request size would require changes in the Dart SDK / VM layer. This PR focuses on **response size**, which can be reliably determined using: * Socket `readBytes` * HTTP `content-length` header (when present) --- ## Screenshot <img width="1359" height="882" alt="Screenshot 2026-03-27 233804" src="https://github.com/user-attachments/assets/4ddce5eb-1a4b-4a9e-80b6-cd16fa226c13" /> --- ## Future Work * Add request size support when VM-level data becomes available * Introduce separate request/response size columns * Improve accuracy via VM instrumentation --- ### General checklist * [x] I read the Contributor Guide * [x] I read the Tree Hygiene guidelines * [x] I followed the Flutter Style Guide * [x] I signed the CLA * [x] I updated relevant documentation --- ### Issues checklist * [x] This PR fixes #6165 --- ### Tests checklist * [x] Added unit tests for new functionality --- ### AI-tooling checklist * [x] I used AI tooling responsibly and verified all generated content --- ### Feature-change checklist * [x] This PR changes DevTools UI * [x] Added entry to `NEXT_RELEASE_NOTES.md` * [x] Included screenshots * [x] Verified changes locally
1 parent 46811ca commit aac691b

8 files changed

Lines changed: 191 additions & 0 deletions

File tree

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ abstract class NetworkRequest
2929

3030
int? get port;
3131

32+
int? get requestBytes => null;
33+
int? get responseBytes => null;
34+
3235
bool get didFail;
3336

3437
/// True if the request hasn't completed yet.
@@ -160,6 +163,12 @@ class Socket extends NetworkRequest {
160163
@override
161164
int get port => _socket.port;
162165

166+
@override
167+
int get requestBytes => writeBytes;
168+
169+
@override
170+
int get responseBytes => readBytes;
171+
163172
// TODO(kenz): what determines a web socket request failure?
164173
@override
165174
bool get didFail => false;

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import '../../shared/ui/colors.dart';
1717
import '../../shared/ui/common_widgets.dart';
1818
import 'network_controller.dart';
1919
import 'network_model.dart';
20+
import 'utils/http_utils.dart';
2021

2122
// Approximately double the indent of the expandable tile's title.
2223
const _rowIndentPadding = 30.0;
@@ -625,6 +626,7 @@ class NetworkRequestOverviewView extends StatelessWidget {
625626
}
626627

627628
List<Widget> _buildGeneralRows(BuildContext context) {
629+
final bytes = data.responseBytes;
628630
return [
629631
// TODO(kenz): show preview for requests (png, response body, proto)
630632
_buildRow(
@@ -658,6 +660,14 @@ class NetworkRequestOverviewView extends StatelessWidget {
658660
),
659661
const SizedBox(height: defaultSpacing),
660662
],
663+
664+
_buildRow(
665+
context: context,
666+
title: 'Response Size',
667+
child: _valueText(bytes != null ? formatBytes(bytes) : '-'),
668+
),
669+
const SizedBox(height: defaultSpacing),
670+
661671
if (data.contentType != null) ...[
662672
_buildRow(
663673
context: context,

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import '../../shared/ui/utils.dart';
2929
import 'network_controller.dart';
3030
import 'network_model.dart';
3131
import 'network_request_inspector.dart';
32+
import 'utils/http_utils.dart';
3233

3334
class NetworkScreen extends Screen {
3435
NetworkScreen() : super.fromMetaData(ScreenMetaData.network);
@@ -352,6 +353,7 @@ class NetworkRequestsTable extends StatelessWidget {
352353
static const statusColumn = StatusColumn();
353354
static const typeColumn = TypeColumn();
354355
static const durationColumn = DurationColumn();
356+
static const responseSizeColumn = ResponseSizeColumn();
355357
static final timestampColumn = TimestampColumn();
356358
static const actionsColumn = ActionsColumn();
357359
static final columns = <ColumnData<NetworkRequest>>[
@@ -360,6 +362,7 @@ class NetworkRequestsTable extends StatelessWidget {
360362
statusColumn,
361363
typeColumn,
362364
durationColumn,
365+
responseSizeColumn,
363366
timestampColumn,
364367
actionsColumn,
365368
];
@@ -394,6 +397,20 @@ class NetworkRequestsTable extends StatelessWidget {
394397
}
395398
}
396399

400+
class ResponseSizeColumn extends ColumnData<NetworkRequest> {
401+
const ResponseSizeColumn()
402+
: super('Size', alignment: ColumnAlignment.right, fixedWidthPx: 90);
403+
404+
@override
405+
int? getValue(NetworkRequest dataObject) => dataObject.responseBytes;
406+
407+
@override
408+
String getDisplayValue(NetworkRequest dataObject) {
409+
final bytes = dataObject.responseBytes;
410+
return bytes != null ? formatBytes(bytes) : '-';
411+
}
412+
}
413+
397414
class AddressColumn extends ColumnData<NetworkRequest>
398415
implements ColumnRenderer<NetworkRequest> {
399416
AddressColumn()

packages/devtools_app/lib/src/screens/network/utils/http_utils.dart

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,18 @@ int calculateHeadersSize(Map<String, Object?>? headers) {
2929
// Calculate the byte length of the headers string
3030
return utf8.encode(headersString).length;
3131
}
32+
33+
// Output Formats:
34+
// - 512 → "512 B"
35+
// - 2000 → "2.0 kB"
36+
// - 1000000 → "1.0 MB"
37+
// Values are rounded to one decimal place for kB and MB.
38+
// Uses decimal (base-10) units to match Chrome DevTools.
39+
String formatBytes(int? bytes) {
40+
if (bytes == null || bytes < 0) return '-';
41+
if (bytes < 1000) return '$bytes B';
42+
if (bytes < 1000 * 1000) {
43+
return '${(bytes / 1000).toStringAsFixed(1)} kB';
44+
}
45+
return '${(bytes / (1000 * 1000)).toStringAsFixed(1)} MB';
46+
}

packages/devtools_app/lib/src/shared/http/http_request_data.dart

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,8 +251,28 @@ class DartIOHttpRequestData extends NetworkRequest {
251251
return connectionInfo != null ? connectionInfo[_localPortKey] : null;
252252
}
253253

254+
@override
255+
int? get responseBytes {
256+
final headers = _request.response?.headers;
257+
if (headers == null) return null;
258+
259+
final contentLength = headers['content-length'];
260+
261+
if (contentLength is String) {
262+
return int.tryParse(contentLength);
263+
}
264+
if (contentLength is List && contentLength.isNotEmpty) {
265+
final first = contentLength.first;
266+
267+
if (first is int) return first;
268+
if (first is String) return int.tryParse(first);
269+
}
270+
return null;
271+
}
272+
254273
/// True if the HTTP request hasn't completed yet, determined by
255274
/// `isRequestComplete` / `isResponseComplete` from the profile data.
275+
256276
@override
257277
bool get inProgress {
258278
if (_isCancelled) return false;

packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,14 @@ TODO: Remove this section if there are not any updates.
4040

4141
## Network profiler updates
4242

43+
- Added response size column to the Network tab and displayed response size in the request inspector overview.
44+
[#9744](https://github.com/flutter/devtools/pull/9744)
45+
4346
- Improved HTTP request status classification in the Network tab to better distinguish cancelled, completed, and in-flight requests (for example, avoiding some cases where cancelled requests appeared as pending). [#9683](https://github.com/flutter/devtools/pull/9683)
4447

48+
- Added a filter setting to hide HTTP-profiler socket data.
49+
[#9698](https://github.com/flutter/devtools/pull/9698)
50+
4551
## Logging updates
4652

4753
- Fixed an issue where log messages containing newline characters were incorrectly split into multiple separate entries in the Logging screen. [#9757](https://github.com/flutter/devtools/pull/9757)
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import 'package:devtools_app/src/shared/http/http_request_data.dart';
2+
import 'package:flutter_test/flutter_test.dart';
3+
4+
void main() {
5+
group('responseBytes', () {
6+
Map<String, Object?> baseJson(Map<String, Object?> responseHeaders) {
7+
return {
8+
'isolateId': 'isolate-1',
9+
'id': 'request-1',
10+
'method': 'GET',
11+
'uri': 'https://example.com',
12+
'events': <Object?>[],
13+
'startTime': DateTime.now().microsecondsSinceEpoch,
14+
'endTime': DateTime.now().microsecondsSinceEpoch,
15+
'request': {
16+
'headers': <String, Object?>{},
17+
'connectionInfo': null,
18+
'contentLength': null,
19+
'cookies': <Object?>[],
20+
'followRedirects': true,
21+
'maxRedirects': 5,
22+
'persistentConnection': true,
23+
},
24+
'response': {
25+
'headers': responseHeaders,
26+
'connectionInfo': null,
27+
'contentLength': null,
28+
'cookies': <Object?>[],
29+
'compressionState': 'ResponseBodyCompressionState.notCompressed',
30+
'isRedirect': false,
31+
'persistentConnection': true,
32+
'reasonPhrase': 'OK',
33+
'redirects': <Map<String, dynamic>>[],
34+
'statusCode': 200,
35+
'startTime': DateTime.now().microsecondsSinceEpoch,
36+
},
37+
};
38+
}
39+
40+
// Verifies parsing when content-length is a string value.
41+
test('parses content-length from string', () {
42+
final request = DartIOHttpRequestData.fromJson(
43+
baseJson({'content-length': '1234'}),
44+
null,
45+
null,
46+
);
47+
48+
expect(request.responseBytes, 1234);
49+
});
50+
51+
// Verifies parsing when content-length is a list of strings.
52+
test('parses content-length from list of strings', () {
53+
final request = DartIOHttpRequestData.fromJson(
54+
baseJson({
55+
'content-length': ['5678'],
56+
}),
57+
null,
58+
null,
59+
);
60+
61+
expect(request.responseBytes, 5678);
62+
});
63+
64+
// Ensures integer values inside a list are handled correctly.
65+
test('handles integer in list', () {
66+
final request = DartIOHttpRequestData.fromJson(
67+
baseJson({
68+
'content-length': [91011],
69+
}),
70+
null,
71+
null,
72+
);
73+
74+
expect(request.responseBytes, 91011);
75+
});
76+
77+
// Returns null when header is missing.
78+
test('returns null for missing header', () {
79+
final request = DartIOHttpRequestData.fromJson(baseJson({}), null, null);
80+
81+
expect(request.responseBytes, null);
82+
});
83+
84+
// Returns null when parsing fails.
85+
test('returns null for invalid value', () {
86+
final request = DartIOHttpRequestData.fromJson(
87+
baseJson({'content-length': 'invalid'}),
88+
null,
89+
null,
90+
);
91+
92+
expect(request.responseBytes, null);
93+
});
94+
});
95+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import 'package:devtools_app/src/screens/network/utils/http_utils.dart';
2+
import 'package:flutter_test/flutter_test.dart';
3+
4+
void main() {
5+
group('formatBytes', () {
6+
// Verifies correct formatting across different unit ranges.
7+
test('formats bytes correctly', () {
8+
expect(formatBytes(512), '512 B'); // bytes
9+
expect(formatBytes(2000), '2.0 kB'); // kilobytes (base-10)
10+
expect(formatBytes(1000000), '1.0 MB'); // megabytes (base-10)
11+
});
12+
13+
// Ensures handling of invalid or missing values.
14+
test('handles null and negative values', () {
15+
expect(formatBytes(null), '-');
16+
expect(formatBytes(-1), '-');
17+
});
18+
});
19+
}

0 commit comments

Comments
 (0)