Skip to content

Conversation

@chingjun
Copy link
Contributor

@chingjun chingjun commented Dec 23, 2021

Also refactored DaemonStreams (that I wrote in a previous PR) for better reusability and testability, and added the ability to allow daemon commands to pass binary streams.

In my local tests passing a 100MB file in binary stream is ~50% faster than including a base64-encoded version in JSON.

Context: b/210724354

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 and followed the Flutter Style Guide, including Features we expect every widget to implement.
  • 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 this PR is test-exempt.
  • All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel on Discord.

@flutter-dashboard flutter-dashboard bot added the tool Affects the "flutter" command-line tool. See also t: labels. label Dec 23, 2021
@chingjun
Copy link
Contributor Author

chingjun commented Jan 5, 2022

Rebased this PR now that #95290 has been submitted.

Also reactored DaemonStreams (that I wrote in a previous PR) for better reusability and testability, and added the ability to allow daemon commands to pass binary streams.

In my local tests passing a 100MB file in binary stream is ~50% faster than including a base64-encoded version in JSON.

PTAL, thanks!

Copy link
Member

@jmagman jmagman left a comment

Choose a reason for hiding this comment

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

Minor nits. This review is huge. 🙂

/cc @zanderso @christopherfujino

Comment on lines 48 to 49
Copy link
Member

Choose a reason for hiding this comment

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

Lower camel https://dart.dev/guides/language/effective-dart/style#prefer-using-lowercamelcase-for-constant-names

Suggested change
JSON,
BINARY,
json,
binary,

Note: We initially used Java’s SCREAMING_CAPS style for constants. We changed for a few reasons:
SCREAMING_CAPS looks bad for many cases, particularly enum values for things like CSS colors.
Constants are often changed to final non-const variables, which would necessitate a name change.
The values property automatically defined on an enum type is const and lowercase.

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, changed to lower case

Copy link
Member

Choose a reason for hiding this comment

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

Maybe handle error case where message is malformed?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I preserved the original behavior for backward compatibility. In the original daemon, anything that is not JSON string are simply ignored.

Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
// final BuildInfo buildInfo;

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removed, thanks for noticing this :)

Copy link
Member

Choose a reason for hiding this comment

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

Consider adding a comment here explaining what this id is for.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Comment added

Copy link
Member

Choose a reason for hiding this comment

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

Consider adding a comment here. The comment should probably answer the question, "What's the difference between getDevices() and discoverDevices()?"

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

Copy link
Member

Choose a reason for hiding this comment

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

nit: else is unnecessary.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removed the else

Copy link
Member

Choose a reason for hiding this comment

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

nit: else is unnecessary.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removed the else

Copy link
Member

Choose a reason for hiding this comment

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

The binary parameter is only used in a few out of many instances of this callback type. Are there any drawbacks to using two callback tables for the two different callback types to avoid passing the binary parameter to many places it isn't used?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is a great idea! Just added a new callback table. Also added assertion so that the same method will not be registered on both callback tables. Thanks!

Copy link
Member

Choose a reason for hiding this comment

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

This seems like a programming error rather than an error that an end-user would be expected to address. In that case, this should be a normal exception so that it will hit crash logging and/or the end-user will see it as unusual and file a bug.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Changed to throw Exception.

Copy link
Member

Choose a reason for hiding this comment

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

commented code.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removed

Copy link
Member

Choose a reason for hiding this comment

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

Consider adding an error handling callback for the server socket.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added an error handler that prints the error with a logger to help diagnosing issues.

Copy link
Member

Choose a reason for hiding this comment

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

Not a blocker, but I'm curious to know whether these tests touch the global context. Could they be testWithoutContext?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The Daemon class in commands/daemon.dart is using the global context in several places, it errors out when I tried to use testWithoutContext.

Copy link
Member

Choose a reason for hiding this comment

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

Can these be testWithoutContext?

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, changed to testWithoutContext

@chingjun
Copy link
Contributor Author

Thanks for the extensive review Jenn, Zech and Chris! I've pushed a new commit based on the feedback, the biggest change is that I've refactored the convertInputStream async* function into a separate class and managed to remove the async* by using a StreamController instead. PTAL, thanks!

Copy link
Contributor

Choose a reason for hiding this comment

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

should this not be:

    assert(!_handlers.containsKey(name));

Copy link
Contributor Author

Choose a reason for hiding this comment

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

My intention is to check that the same method name is not registered in both command map.

But I think it doesn't harm to check both maps, and less error prone too. I'll update to check both maps.

Copy link
Contributor

@christopherfujino christopherfujino Jan 19, 2022

Choose a reason for hiding this comment

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

avoid throwing strings. is this an internal state error, or user error? Use a throwToolExit('device "$deviceId" not found'); if this is a user error.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I was following the other throw style in the same file. Should I change all the throw in this file to throwToolExit instead? If so can I do that in a follow-up PR?

Copy link
Member

Choose a reason for hiding this comment

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

Haven't looked at this code in awhile. I wonder if the strings get caught and sent as an error response back to the client. Even if that's the case though, we should wrap in a class to make that intent explicit at the point of the throw.

@chingjun chingjun force-pushed the proxied-device branch 2 times, most recently from 8498d31 to ad68ea7 Compare January 21, 2022 23:11
@chingjun
Copy link
Contributor Author

Friendly ping @zanderso if you want to take another look at this? Thanks!

Copy link
Member

@zanderso zanderso left a comment

Choose a reason for hiding this comment

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

Sorry for the slow review.

This change has low test-coverage of error cases. I think even after the error handling that has already been added, and the couple of places that I've suggested in the new round of comments, I'm not confident that all cases are covered. Sockets and streams in Dart are difficult to reason about, so we'll need you to be available to fix crashers in this new code as we discover them from crash logging data. If you are not signing up for that, we will have to back out this change, and reland only with much better test coverage of error cases.

Copy link
Member

Choose a reason for hiding this comment

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

Haven't looked at this code in awhile. I wonder if the strings get caught and sent as an error response back to the client. Even if that's the case though, we should wrap in a class to make that intent explicit at the point of the throw.

Future<String> connect(Map<String, dynamic> args) async {
final int targetPort = _getIntArg(args, 'port', required: true);
final String id = 'portForwarder_${targetPort}_${_id++}';
final Socket socket = await Socket.connect('127.0.0.1', targetPort);
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 '127.0.0.1', this should use InternetAddress.loopbackIPv4 and fall back on InternetAddress.loopbackIPv6 if that doesn't work. This call can also throw a SocketException, which should be handled here if it shouldn't propagate like that.

final String id = 'portForwarder_${targetPort}_${_id++}';
final Socket socket = await Socket.connect('127.0.0.1', targetPort);
_forwardedConnections[id] = socket;
socket.listen((List<int> data) {
Copy link
Member

Choose a reason for hiding this comment

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

You need an error handler on this listen() and possibly also an error handler on the done future or socket exceptions on remote disconnect may crash the tool.

@override
Future<void> dispose() async {
for (final Socket connection in _forwardedConnections.values) {
await connection.close();
Copy link
Member

Choose a reason for hiding this comment

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


int start = 0;
while (start < chunk.length) {
if (state == _InputStreamParseState.json) {
Copy link
Member

Choose a reason for hiding this comment

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

Since the state is in fields instead of locals, you can pull the handling of these cases out into helper methods.

Future<void> dispose() async {
await (await _socket).close();
/// Creates a [DaemonStreams] that uses stdin and stdout as the underlying streams.
static DaemonStreams fromStdio(Stdio stdio, { required Logger logger }) {
Copy link
Member

Choose a reason for hiding this comment

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

Dart style here would be to use a named forwarding factory constructor:

DaemonStreams.fromStdio(Stdio stdio, {required Logger logger})
  : this(stdio.stdin, stdio.stdout, logger: logger);

}

/// Creates a [DaemonStreams] that uses [Socket] as the underlying streams.
static DaemonStreams fromSocket(Socket socket, { required Logger logger }) {
Copy link
Member

Choose a reason for hiding this comment

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

Ditto:

DaemonStreams.fromSocket(Socket socket, { required Logger logger })
  : this(socket, socket, logger: logger);

}

/// Connects to a server and creates a [DaemonStreams] from the connection as the underlying streams.
static DaemonStreams connect(String host, int port, { required Logger logger }) {
Copy link
Member

Choose a reason for hiding this comment

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

And this should be a factory constructor:

factory DaemonStreams.connect(String host, int port, {required Logger logger}) {
  ...
}

final Future<Socket> socketFuture = Socket.connect(host, port);
final StreamController<List<int>> inputStreamController = StreamController<List<int>>();
final StreamController<List<int>> outputStreamController = StreamController<List<int>>();
socketFuture.then((Socket socket) {
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 future needs an error handler.

@override
String toString() => value;

static Category? fromString(String category) {
Copy link
Member

Choose a reason for hiding this comment

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

Here and below, using a const map lookup can help readability:

static Category? fromString(String category) {
  return <String, Category>{
    'web': web,
    ...
  }[category];
}

@chingjun
Copy link
Contributor Author

Sorry about that. The changes here will mostly not be used by default (especially the parts related to Sockets), and are only used in google3, so I think most of the changes here should be relatively safe for 3p users. The changes are submitted upstream only because it might be useful for some advanced 3p users.

And yes, I can fix upcoming issues or crashes if they come up, and I will send a separate PR to address the feedbacks you sent above.

Thanks.

chingjun added a commit to chingjun/flutter that referenced this pull request Jan 29, 2022
fluttergithubbot pushed a commit that referenced this pull request Feb 2, 2022
engine-flutter-autoroll added a commit to engine-flutter-autoroll/plugins that referenced this pull request Feb 4, 2022
clocksmith pushed a commit to clocksmith/flutter that referenced this pull request Mar 8, 2022
…daemon. (flutter#95738)

Also allow daemon commands to pass binary streams
clocksmith pushed a commit to clocksmith/flutter that referenced this pull request Mar 8, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

tool Affects the "flutter" command-line tool. See also t: labels.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants