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

Offline support for Network page #8332

Open
wants to merge 29 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
fe11d5b
added initial implementation
hrajwade96 Sep 21, 2024
6c140b4
adding constants file
hrajwade96 Sep 29, 2024
0c1f414
updated NEXT_RELEASE_NOTES.md
hrajwade96 Sep 30, 2024
e7eeecb
Revert "updated NEXT_RELEASE_NOTES.md"
hrajwade96 Sep 30, 2024
c5ec6f6
used constants, added util for header size calc, added params
hrajwade96 Oct 2, 2024
131cde6
Merge branch 'master' into network_screen_offline_support
hrajwade96 Oct 2, 2024
2a9c4cb
param renamed, used _log
hrajwade96 Oct 2, 2024
4f473be
Merge remote-tracking branch 'origin/network_screen_offline_support' …
hrajwade96 Oct 2, 2024
ae58738
added copyright header
hrajwade96 Oct 2, 2024
2bfbe1c
setup changes implemented
hrajwade96 Oct 3, 2024
857cfbe
put class name in square brackets
hrajwade96 Oct 3, 2024
7150436
added isNullOrEmpty check
hrajwade96 Oct 3, 2024
da11a67
added exit offline cta, hiding other controls, removed json string
hrajwade96 Oct 4, 2024
941ec88
fix for request selection
hrajwade96 Oct 4, 2024
852b144
comments resolved
hrajwade96 Oct 8, 2024
433af38
removing getFullRequestData section
hrajwade96 Oct 8, 2024
4644b00
using updateOrAddAll for setting data
hrajwade96 Oct 8, 2024
6fd98d0
Wrapped with OfflineAwareControls, removed if-else.
hrajwade96 Oct 8, 2024
665ea0d
removed addAutoDisposeListener as we are using OfflineAwareControls
hrajwade96 Oct 8, 2024
b6df572
removed try-catch
hrajwade96 Oct 8, 2024
be63db4
code refactoring
hrajwade96 Oct 9, 2024
91505b5
added enum
hrajwade96 Oct 9, 2024
0715379
code refactoring, reverting changes on network service
hrajwade96 Oct 10, 2024
ade8b5f
using values from enum
hrajwade96 Oct 10, 2024
f21a4f6
delegating toJson using extension methods, code refactoring
hrajwade96 Oct 23, 2024
454ca92
socket data de-serialisation changes
hrajwade96 Oct 27, 2024
b7b9679
Merge branch 'master' into network_screen_offline_support
hrajwade96 Oct 27, 2024
ac726e8
used factory constructor, made enum private
hrajwade96 Nov 4, 2024
2ea3044
reorder params, used isEmpty
hrajwade96 Nov 4, 2024
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 @@ -178,19 +178,25 @@ class NetworkController extends DisposableController
}

Future<void> loadOfflineData(OfflineNetworkData offlineData) async {
List<HttpProfileRequest> httpProfileData = [];
List<SocketStatistic> socketStatsData = [];

httpProfileData = offlineData.httpRequestData.mapToHttpProfileRequests;
socketStatsData = offlineData.socketData.mapToSocketStatistics;
hrajwade96 marked this conversation as resolved.
Show resolved Hide resolved

_currentNetworkRequests
..clear()
..updateOrAddAll(
requests: offlineData.currentRequests!,
sockets: offlineData.socketStats,
timelineMicrosOffset: DateTime.now().microsecondsSinceEpoch -
(networkService.timeStamp ?? 0),
requests: httpProfileData,
sockets: socketStatsData,
timelineMicrosOffset: DateTime.now().microsecondsSinceEpoch,
);
_filterAndRefreshSearchMatches();

// If a selectedRequestId is available, select it in offline mode.
if (offlineData.selectedRequestId != null) {
final selected = offlineData.getRequest(offlineData.selectedRequestId!);
final selected = _currentNetworkRequests
.getRequest(offlineData.selectedRequestId ?? '');
if (selected != null) {
selectedRequest.value = selected;
resetDropDown();
Expand Down Expand Up @@ -428,11 +434,20 @@ class NetworkController extends DisposableController

@override
OfflineScreenData prepareOfflineScreenData() {
final httpRequestData = <DartIOHttpRequestData>[];
final socketData = <Socket>[];
for (final request in _currentNetworkRequests.value) {
if (request is DartIOHttpRequestData) {
httpRequestData.add(request);
} else if (request is Socket) {
socketData.add(request);
}
}

final offlineData = OfflineNetworkData(
requests: filteredData.value.whereType<DartIOHttpRequestData>().toList(),
httpRequestData: httpRequestData,
socketData: socketData,
selectedRequestId: selectedRequest.value?.id,
currentRequests: _networkService.currentHttpRequests,
socketStats: _networkService.sockets ?? [],
);

return OfflineScreenData(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:devtools_shared/devtools_shared.dart';
import 'package:flutter/material.dart';
import 'package:vm_service/vm_service.dart';

import '../../shared/primitives/utils.dart';
import '../../shared/ui/search.dart';

abstract class NetworkRequest with ChangeNotifier, SearchableDataMixin {
abstract class NetworkRequest
with ChangeNotifier, SearchableDataMixin, Serializable {
String get method;

String get uri;
Expand Down Expand Up @@ -180,4 +182,60 @@ class Socket extends NetworkRequest {

@override
int get hashCode => id.hashCode;

SocketStatistic get socketData => _socket;

static Socket fromJson(Map<String, Object?> json) {
return Socket(
SocketStatistic.parse(json['socket'] as Map<String, dynamic>)!,
hrajwade96 marked this conversation as resolved.
Show resolved Hide resolved
json['timelineMicrosBase'] as int,
);
}

@override
Map<String, Object?> toJson() {
return {
SocketJsonKey.timelineMicrosBase.name: _timelineMicrosBase,
SocketJsonKey.socket.name: _socket.toJson(),
};
}
}

extension on SocketStatistic {
Map<String, Object?> toJson() {
return {
SocketJsonKey.id.name: id,
SocketJsonKey.startTime.name: startTime,
SocketJsonKey.endTime.name: endTime,
//TODO verify if these timings are in correct format
Copy link
Member

Choose a reason for hiding this comment

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

please address this TODO.

SocketJsonKey.lastReadTime.name: lastReadTime,
SocketJsonKey.lastWriteTime.name: lastWriteTime,
SocketJsonKey.socketType.name: socketType,
SocketJsonKey.address.name: address,
SocketJsonKey.port.name: port,
SocketJsonKey.readBytes.name: readBytes,
SocketJsonKey.writeBytes.name: writeBytes,
};
}
}

enum SocketJsonKey {
id,
startTime,
endTime,
lastReadTime,
lastWriteTime,
socketType,
address,
port,
readBytes,
writeBytes,
timelineMicrosBase,
socket,
}

extension SocketExtension on List<Socket> {
List<SocketStatistic> get mapToSocketStatistics {
return map((socket) => socket._socket).toList();
}
}
65 changes: 33 additions & 32 deletions packages/devtools_app/lib/src/screens/network/network_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import '../../shared/analytics/analytics.dart' as ga;
import '../../shared/analytics/constants.dart' as gac;
import '../../shared/common_widgets.dart';
import '../../shared/config_specific/copy_to_clipboard/copy_to_clipboard.dart';
import '../../shared/globals.dart';
import '../../shared/http/curl_command.dart';
import '../../shared/http/http_request_data.dart';
import '../../shared/primitives/utils.dart';
Expand Down Expand Up @@ -138,9 +137,9 @@ class _NetworkScreenBodyState extends State<NetworkScreenBody>
return Column(
children: [
OfflineAwareControls(
controlsBuilder: (_) => _NetworkProfilerControls(
controlsBuilder: (offline) => _NetworkProfilerControls(
controller: controller,
offline: offlineDataController.showingOfflineData.value,
offline: offline,
),
gaScreen: gac.network,
),
Expand Down Expand Up @@ -209,35 +208,37 @@ class _NetworkProfilerControlsState extends State<_NetworkProfilerControls>
final hasRequests = _filteredRequests.isNotEmpty;
return Row(
children: [
StartStopRecordingButton(
recording: _recording,
onPressed: () async =>
await widget.controller.togglePolling(!_recording),
tooltipOverride: _recording
? 'Stop recording network traffic'
: 'Resume recording network traffic',
minScreenWidthForTextBeforeScaling: double.infinity,
gaScreen: gac.network,
gaSelection: _recording ? gac.pause : gac.resume,
),
const SizedBox(width: denseSpacing),
ClearButton(
minScreenWidthForTextBeforeScaling:
_NetworkProfilerControls._includeTextWidth,
gaScreen: gac.network,
gaSelection: gac.clear,
onPressed: widget.controller.clear,
),
const SizedBox(width: defaultSpacing),
DownloadButton(
minScreenWidthForTextBeforeScaling:
_NetworkProfilerControls._includeTextWidth,
onPressed: widget.controller.exportAsHarFile,
gaScreen: gac.network,
gaSelection: gac.NetworkEvent.downloadAsHar.name,
),
const SizedBox(width: defaultSpacing),
const Expanded(child: SizedBox()),
if (!widget.offline) ...[
StartStopRecordingButton(
recording: _recording,
onPressed: () async =>
await widget.controller.togglePolling(!_recording),
tooltipOverride: _recording
? 'Stop recording network traffic'
: 'Resume recording network traffic',
minScreenWidthForTextBeforeScaling: double.infinity,
gaScreen: gac.network,
gaSelection: _recording ? gac.pause : gac.resume,
),
const SizedBox(width: denseSpacing),
ClearButton(
minScreenWidthForTextBeforeScaling:
_NetworkProfilerControls._includeTextWidth,
gaScreen: gac.network,
gaSelection: gac.clear,
onPressed: widget.controller.clear,
),
const SizedBox(width: defaultSpacing),
DownloadButton(
minScreenWidthForTextBeforeScaling:
_NetworkProfilerControls._includeTextWidth,
onPressed: widget.controller.exportAsHarFile,
gaScreen: gac.network,
gaSelection: gac.NetworkEvent.downloadAsHar.name,
),
const SizedBox(width: defaultSpacing),
],
const Spacer(),
// TODO(kenz): fix focus issue when state is refreshed
SearchField<NetworkController>(
searchController: widget.controller,
Expand Down
26 changes: 8 additions & 18 deletions packages/devtools_app/lib/src/screens/network/network_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,8 @@ import 'network_controller.dart';

class NetworkService {
NetworkService(this.networkController);
List<HttpProfileRequest>? _httpRequests;
List<SocketStatistic>? _sockets;

final NetworkController networkController;
List<HttpProfileRequest>? get currentHttpRequests => _httpRequests;
List<SocketStatistic>? get sockets => _sockets;
int? _timeStamp;
int? get timeStamp => _timeStamp;

/// Updates the last Socket data refresh time to the current time.
///
Expand Down Expand Up @@ -61,20 +56,15 @@ class NetworkService {
if (serviceConnection.serviceManager.service == null) return;
final timestampObj =
await serviceConnection.serviceManager.service!.getVMTimelineMicros();
_timeStamp = timestampObj.timestamp!;

// Refresh socket data
_sockets ??= await _refreshSockets();
_sockets?.addAll(await _refreshSockets());

// Refresh HTTP request data
_httpRequests ??= await _refreshHttpProfile();
_httpRequests?.addAll(await _refreshHttpProfile());

final timestamp = timestampObj.timestamp!;
final sockets = await _refreshSockets();
networkController.lastSocketDataRefreshMicros = timestamp;
List<HttpProfileRequest>? httpRequests;
httpRequests = await _refreshHttpProfile();
networkController.lastHttpDataRefreshTime = DateTime.now();
networkController.processNetworkTraffic(
sockets: _sockets ?? [],
httpRequests: _httpRequests,
sockets: sockets,
httpRequests: httpRequests,
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
// found in the LICENSE file.

import 'package:devtools_shared/devtools_shared.dart';
import 'package:vm_service/vm_service.dart';

import '../../shared/http/http_request_data.dart';
import '../../shared/primitives/utils.dart';
Expand All @@ -15,68 +14,49 @@ import 'network_model.dart';
/// It is responsible for serializing and deserializing offline network data.
class OfflineNetworkData with Serializable {
OfflineNetworkData({
required this.requests,
required this.httpRequestData,
this.selectedRequestId,
required this.currentRequests,
required this.socketStats,
required this.socketData,
hrajwade96 marked this conversation as resolved.
Show resolved Hide resolved
});

/// Creates an instance of [OfflineNetworkData] from a JSON map.
factory OfflineNetworkData.fromJson(Map<String, dynamic> json) {
final List<dynamic> requestsJson = json['requests'] ?? [];
final List<HttpProfileRequest>? currentReqData = json['currentRequests'];
final List<SocketStatistic>? socketStats = json['socketStats'];

final requests = requestsJson
.map(
(e) => DartIOHttpRequestData.fromJson(
e as Map<String, dynamic>,
null,
null,
),
)
.toList();
final httpRequestData = json[OfflineDataKeys.httpRequestData.name]
as List<DartIOHttpRequestData>;
Copy link
Member

Choose a reason for hiding this comment

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

cast this to the proper type instead:
final httpRequestData = (json[OfflineDataKeys.httpRequestData.name] as List?)?.cast<DartIOHttpRequestData>();

Copy link
Contributor Author

@hrajwade96 hrajwade96 Oct 23, 2024

Choose a reason for hiding this comment

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

now I have explicitly called fromJson on each object from list, will refactor it and make it into an extension.
BTW one observation is when I remove/comment all the toJson() of all the required classes, and pass the list of DartIOHttpRequestData directly

//To Json
  Map<String, dynamic> toJson() {
    return {
      OfflineDataKeys.httpRequestData.name: httpRequestData,
      OfflineDataKeys.selectedRequestId.name: selectedRequestId,
      OfflineDataKeys.socketData.name: socketData,
    };
// From Json    
     final httpRequestData = json[OfflineDataKeys.httpRequestData.name]
    as List<DartIOHttpRequestData>;
return OfflineNetworkData(
      httpRequestData: httpRequestData,
      selectedRequestId:
      json[OfflineDataKeys.selectedRequestId.name] as String?,
      socketData: socketReqData,
    )

This still works, as dart is preserving those object types (when we don't serialise them, hence no need for deserialisation).
Since this offline feature is only used in dart (not passed to any external system) and an in-memory thing, is this a good idea to not fully serialise & de-serialise it? and store the inner objects of DartIOHttpRequestData list and Socket and as they are and pick them? Or is it not recommended or safe to use?

Copy link
Member

Choose a reason for hiding this comment

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

In order to download the screen's data as a JSON file, the serialization is required. However, not serializing the data is acceptable for the 'review history' feature, which allows continuing to view recent data offline when the connected app disconnects. I wouldn't worry about distinguishing the two for this PR. This can be a clean up later if the serialization / deserialization for the 'review history' features causes performance issues.

List<Socket>? socketReqData = [];
socketReqData = json[OfflineDataKeys.socketData.name] as List<Socket>;
Copy link
Member

Choose a reason for hiding this comment

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

define in one step:
final socketRequestData =(json[OfflineDataKeys.socketData.name] as List?)?.cast<Socket>();

Also, prefer socketRequestData over socketReqData to avoid abbreviations in variable names


return OfflineNetworkData(
requests: requests,
selectedRequestId: json['selectedRequestId'] as String?,
currentRequests: currentReqData,
socketStats: socketStats!,
httpRequestData: httpRequestData,
selectedRequestId:
json[OfflineDataKeys.selectedRequestId.name] as String?,
socketData: socketReqData,
);
}
bool get isEmpty => requests.isNullOrEmpty;
bool get isEmpty => httpRequestData.isNullOrEmpty;
hrajwade96 marked this conversation as resolved.
Show resolved Hide resolved

/// List of current [DartIOHttpRequestData] network requests.
final List<DartIOHttpRequestData> requests;

/// Get a request by matching its `id` field.
// Temporarily added to check selection in the filtered requests data,
// until we have current requests data in place
NetworkRequest? getRequest(String id) {
// Search through the list of requests and return the one with the matching ID.
return requests.firstWhere(
(request) => request.id == id,
);
}
final List<DartIOHttpRequestData> httpRequestData;

/// The ID of the currently selected request, if any.
final String? selectedRequestId;

/// Current requests from network controller.

final List<HttpProfileRequest>? currentRequests;

/// Socket statistics
hrajwade96 marked this conversation as resolved.
Show resolved Hide resolved
final List<SocketStatistic> socketStats;
final List<Socket> socketData;

/// Converts the current offline data to a JSON format.
@override
Map<String, dynamic> toJson() {
return {
'requests': requests.map((request) => request.toJson()).toList(),
'selectedRequestId': selectedRequestId,
'currentRequests': currentRequests,
'socketStats': socketStats,
OfflineDataKeys.httpRequestData.name: httpRequestData,
OfflineDataKeys.selectedRequestId.name: selectedRequestId,
OfflineDataKeys.socketData.name: socketData,
};
}
}

enum OfflineDataKeys {
kenzieschmoll marked this conversation as resolved.
Show resolved Hide resolved
httpRequestData,
selectedRequestId,
socketData,
}
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@ class DartIOHttpRequestData extends NetworkRequest {
requestPostData?[HttpRequestDataKeys.text.name].toString();
}
//TODO go through all parameters, to check if they are correctly added.
hrajwade96 marked this conversation as resolved.
Show resolved Hide resolved
Map<String, Object>? toJson() {
@override
Map<String, Object?> toJson() {
try {
return {
HttpRequestDataKeys.startedDateTime.name:
Expand Down Expand Up @@ -173,7 +174,7 @@ class DartIOHttpRequestData extends NetworkRequest {
} catch (e) {
_log.shout('Error in toJson: $e');
}
return null;
return {};
}

static const _connectionInfoKey = 'connectionInfo';
Expand Down Expand Up @@ -453,3 +454,11 @@ class DartIOHttpRequestData extends NetworkRequest {
startTimestamp,
);
}

extension HttpRequestExtension on List<DartIOHttpRequestData> {
List<HttpProfileRequest> get mapToHttpProfileRequests {
return map(
(httpRequestData) => httpRequestData._request as HttpProfileRequest,
).toList();
}
}