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 24 commits into
base: master
Choose a base branch
from

Conversation

hrajwade96
Copy link
Contributor

@hrajwade96 hrajwade96 commented Sep 21, 2024

Adding offline support to Network page.

issue link - #4470

List which issues are fixed by this PR.

Please add a note to packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md if your change requires release notes. Otherwise, add the 'release-notes-not-required' label to the PR.

Pre-launch Checklist

  • I read the Contributor Guide and followed the process outlined there for submitting PRs.
  • I read the Tree Hygiene wiki page, which explains my responsibilities.
  • I read the Flutter Style Guide recently, and have followed its advice.
  • I signed the CLA.
  • I listed at least one issue that this PR fixes in the description above.
  • I updated/added relevant documentation (doc comments with ///).
  • I added new tests to check the change I am making, or there is a reason for not adding tests.

build.yaml badge

If you need help, consider asking for help on Discord.

@hrajwade96 hrajwade96 marked this pull request as draft September 21, 2024 09:19
@hrajwade96 hrajwade96 changed the title added initial implementation Offline support for Network page Sep 21, 2024
@kenzieschmoll
Copy link
Member

@hrajwade96 is this PR ready for review or is this still a work in progress?

@hrajwade96
Copy link
Contributor Author

@hrajwade96 is this PR ready for review or is this still a work in progress?

@kenzieschmoll just finishing up few things, I will mark it ready soon

@hrajwade96 hrajwade96 marked this pull request as ready for review October 2, 2024 17:19
unawaited(controller.startRecording());

cancelListeners();
try {
Copy link
Member

Choose a reason for hiding this comment

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

instead of doing this logic in the screen, we want to do this in the screen's controller NetworkController. Checkout the guide in the dart doc for OfflineScreenControllerMixin. This provides pretty thorough documentation on how offline support for a screen should be set up: https://github.com/flutter/devtools/blob/master/packages/devtools_app/lib/src/shared/offline_data.dart/#L66-L124.

You can also use the ProfilerScreenController as an example: https://github.com/flutter/devtools/blob/master/packages/devtools_app/lib/src/screens/profiler/profiler_screen_controller.dart/#L81

Copy link
Contributor Author

Choose a reason for hiding this comment

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

not sure how I missed it, apologies. I have fixed it now and scanning through if I have missed anything.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Can you confirm if the changes are correct?

filteredData.value.whereType<DartIOHttpRequestData>().toList();
return OfflineScreenData(
screenId: NetworkScreen.id,
data: serializeRequestDataForOfflineMode(requests),
Copy link
Member

Choose a reason for hiding this comment

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

I recommend creating a class OfflineNetworkData with Serializable that packages up all of the data stored on the Network controller for serialization. This includes the list of requests as well as other information like the currently selected request. In the future we may want to include the current filter, but let's leave that out for now.

Creating a new class OfflineNetworkData will make it easier to test the toJson and fromJson methods that should be implemented on the class. See other examples of classes that mixin Serializable in DevTools for an example.

Then here, you'd create an instance of OfflineNetworkData from the current data on the network controller, and call toJson() to get the value for the data parameter of OfflineScreenData in this return statement.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yup, sure!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

@@ -160,6 +167,58 @@ class NetworkController extends DisposableController
@visibleForTesting
bool get isPolling => _pollingTimer != null;

void _initHelper() async {
try {
Copy link
Member

Choose a reason for hiding this comment

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

why do we need the try / catch here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done, removed it.

shouldLoad: (data) => !data.isEmpty,
loadData: (data) => loadOfflineData(data),
);
}
Copy link
Member

Choose a reason for hiding this comment

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

the code after this if statement should be put in an else block

Copy link
Contributor Author

@hrajwade96 hrajwade96 Oct 8, 2024

Choose a reason for hiding this comment

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

not using else was an oversight, added else block now, and also now there is no code outside.

loadData: (data) => loadOfflineData(data),
);
}
cancelListeners();
Copy link
Member

Choose a reason for hiding this comment

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

you can remove this line since this initialization code should only be run once. When this was in the widget code, we needed to call cancelListeners because didChangeDependencies may be called more than once.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok got it, removed

@@ -187,7 +180,7 @@ class _NetworkProfilerControlsState extends State<_NetworkProfilerControls>
@override
void initState() {
super.initState();

addAutoDisposeListener(offlineDataController.showingOfflineData);
Copy link
Member

Choose a reason for hiding this comment

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

you can remove this if you use OfflineAwareControls instead

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ok done

Comment on lines 22 to 23
final List<dynamic> requestsJson = json['requests'] ?? [];
final requests = requestsJson
Copy link
Member

Choose a reason for hiding this comment

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

we need to track the socket statistics as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes sure, will add it, I knew it is required but thought of first doing it for http requests to start with, and later add the implementation for socket reqs.

Comment on lines 64 to 65
'requests': requests.map((request) => request.toJson()).toList(),
'selectedRequestId': selectedRequestId,
Copy link
Member

Choose a reason for hiding this comment

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

store these strings as static consts on this class or as an enum in this file

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ok

Comment on lines 186 to 187
timelineMicrosOffset: DateTime.now().microsecondsSinceEpoch -
(networkService.timeStamp ?? 0),
Copy link
Member

Choose a reason for hiding this comment

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

You can just use DateTime.now().microsecondsSinceEpoch for this. This is arbitrary for offline data since we won't be updating the sockets like we do when we have a live connection.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

OfflineAwareControls(
controlsBuilder: (_) => _NetworkProfilerControls(
controller: controller,
offline: offlineDataController.showingOfflineData.value,
Copy link
Member

Choose a reason for hiding this comment

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

use the value from the controlsBuilder instead of looking this up from the notifier again:

controlsBuilder: (offline) => _NetworkProfilerControls(
  controller: controller,
  offline: offline,
),

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

Comment on lines 242 to 253
SearchField<NetworkController>(
searchController: widget.controller,
searchFieldEnabled: hasRequests,
searchFieldWidth: screenWidth <= MediaSize.xs
? defaultSearchFieldWidth
: wideSearchFieldWidth,
),
const SizedBox(width: denseSpacing),
DevToolsFilterButton(
onPressed: _showFilterDialog,
isFilterActive: _filteredRequests.length != _requests.length,
),
Copy link
Member

Choose a reason for hiding this comment

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

only include these two controls in offline mode.

children: [
  if(!widget.offline) ...[
    StartStopRecordingButton(...)
    const SizedBox(width: denseSpacing),
    ClearButton(...)
    const SizedBox(width: denseSpacing),
    DownloadButton(...)
    const SizedBox(width: denseSpacing),
  ]
  Spacer(), // this is a helper widget that replaces Expanded(child: SizedBox()),
  const SizedBox(width: denseSpacing),
  SearchField<NetworkController>(...)
  const SizedBox(width: denseSpacing),
  DevToolsFilterButton(...),
],

Copy link
Contributor Author

@hrajwade96 hrajwade96 Oct 9, 2024

Choose a reason for hiding this comment

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

Ok reverting this and adding Spacer.
Actually I had changed it again thinking you meant keeping all the controls, which I got clarified later.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

Comment on lines 434 to 435
currentRequests: _networkService.currentHttpRequests,
socketStats: _networkService.sockets ?? [],
Copy link
Member

@kenzieschmoll kenzieschmoll Oct 8, 2024

Choose a reason for hiding this comment

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

We shouldn't have to make any changes to the _networkService to do this. I think what you want is this:

mixin Serializable to the NetworkRequest class. This will force subclasses of NetworkRequest to implement a toJson method.

Then implement toJson for the NetworkRequest subclasses DartIOHttpRequestData and Socket. You will also need to implement the deserialization logic fromJson on these classes as well if they are not already implemented.

Then here you can create offline data as follows:

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(
  httpRequestData: httpRequestData,
  socketData: socketData,
  selectedRequestId: selectedRequest.value?.id,
);

In OfflineNetworkData toJson and fromJson methods, you can use the serialization methods you implemented on the Socket and DartIORequestData classes.

Let me know if you have any questions on this.

Copy link
Contributor Author

@hrajwade96 hrajwade96 Oct 9, 2024

Choose a reason for hiding this comment

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

This will give the reqs lists of - DartIOHttpRequestData and Socket classes.
I had implemented this for DartIOHttpRequestData, similarly I can do for socket, by using Serializable mixin on their parent abstract class.

However, you suggested to use the updateOrAddAll function

  void updateOrAddAll({
    required List<HttpProfileRequest> requests,
    required List<SocketStatistic> sockets,
    required int timelineMicrosOffset,
  })

Which requires below lists :
List<HttpProfileRequest> & List<SocketStatistic>.

And I think this data is only available in NetworkService, also there are no getters for this data in DartIOHttpRequestData and Socket classes.
Or I should create those getters and retrieve it from there, is that what you were suggesting?

Copy link
Member

Choose a reason for hiding this comment

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

The Socket class wraps a SocketStatistic object, so you can get the List<SocketStatistic> from the List<Socket>. I recommend adding this extension method to the bottom of the network_model.dart file so that you don't have to change the visibility of Socket._socket.

extension SocketExtension on List<Socket> {
  List<SocketStatistic> get mapToSocketStatistics {
    return map((socket) => socket._socket).toList();
  }
}

Copy link
Member

@kenzieschmoll kenzieschmoll Oct 9, 2024

Choose a reason for hiding this comment

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

Also SocketStatistic already has a fromJson method you can leverage. I expect the serialization logic for Socket would be something simple like:

Map<String, Object?> toJson() {
  return {
    'timelineMicrosBase': _timelineMicrosBase,
    'socket': _socket.toJson() // you'll need to implement this, perhaps as an extension method on SocketStatistic
  };
}

Socket fromJson(Map<String, Object?> json) {
  return Socket(
    SocketStatistic.fromJson((json['socket'] as Map).cast<String, Object?>()),
    json['timelineMicrosBase'] as int,
  );
}

Copy link
Contributor Author

@hrajwade96 hrajwade96 Oct 9, 2024

Choose a reason for hiding this comment

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

Oh ok, I think visibility can remain intact even with :

  SocketStatistic get socketStatistic => _socket;

But yes extension adds more utility here, will use it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants