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

feat(ferry): add IsolateClient #405

Merged
merged 21 commits into from
Sep 18, 2022
Merged

feat(ferry): add IsolateClient #405

merged 21 commits into from
Sep 18, 2022

Conversation

knaeckeKami
Copy link
Collaborator

@knaeckeKami knaeckeKami commented Sep 14, 2022

added IsolateClient, which runs queries in another isolate in order to avoid jank on expensive normalization

This should solve all remaining issues regarding jank.

Also added example on how to use it in pokemon_explorer.

TODOs:

  • Features
    • ☑ request
    • ☑ readQuery
    • ☑ writeQuery
    • ☑ readFragment
    • ☑ writeFragment
    • ☑ evict dataId from cache
    • ☑ retain on cache
    • ☑ release from cache
    • ☑ cache garbage collection
    • ☑ enable custom message handling for features like token refresh, error reporting etc
  • Tests
    • ☑ IsolateClient integration test across isolates
    • ☑ handleCommand tests for unit testing the ferry isolate side logic
  • Documentation
    • ☑ Comments
    • ☑ Website
    • ☐ Better Naming?
    • ☑ Example on how to do token refresh over isolates
  • Correctness
    • ☐ Verify all ports and streams are properly closed when not needed anymore
      • ☑ Verified request() is cancelled on Isolate when main cancels stream
      • ☑ Verified request() is cancelled on isolate when main disposed link

This is almost ready to be merged now.

I'd appreciate comments, suggestions for improvements, and any feedback.

@GP4cK @2shrestha22 @ValentinVignal

@knaeckeKami knaeckeKami marked this pull request as ready for review September 18, 2022 00:40
@@ -0,0 +1,145 @@
import 'dart:async';
import 'dart:isolate';
Copy link
Contributor

Choose a reason for hiding this comment

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

Since dart:isolate is not supported on web, will this be an issue if we try to use this package in a web app? Should it be a separate package?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

It will fail if you try to use the IsolateClient, yes. The standard client will work though. But yes, it worth considering it moving to another package.

import 'package:ferry/ferry.dart';

abstract class IsolateCommand {
SendPort sendPort;
Copy link
Contributor

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 add a sendPort to each command because we could use the sendPort that we receive when the isolate is created, right? (_IsolateInit#sendPort)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Since there can be many multiple Streams and one-off request active at the same time, using just one SendPort would make the communication more complicated, since we would have to track which Message belongs to which request.

It would certainly be possible if we gave every IsolateCommand instance a unique ID for example, but still - this makes the code way simpler IMO. Also in cases where we need to send the information that the main isolate has stopped listening to a stream.

It would be possible by using request IDs for example. However, according to the the flutter team, creating ReceivePorts
is pretty cheap performance wise ( https://discord.com/channels/608014603317936148/608016344520196173/1019723108283850762 ). so I am not sure if it is worth it.

Copy link
Contributor

Choose a reason for hiding this comment

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

I see. Sounds fair.

Comment on lines 24 to 28
Future<Client> _initClientIsolate(Map<String, dynamic>? params, SendPort? sendPort) async {
// don't use Hive.initFlutter to avoid dealing with method channels in the isolate
// instead, call getApplicationDocumentsDirectory() on the main isolate
// and pass the result to the ferry isolate
Hive.init(params!["hivePath"]);
Copy link
Contributor

Choose a reason for hiding this comment

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

We may as well make params required if we expect a hivePath and a url.
Actually, it should be a class rather than a Map.

Copy link
Collaborator Author

@knaeckeKami knaeckeKami Sep 18, 2022

Choose a reason for hiding this comment

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

Changed to Map to be non-nullable now.

Good point. Currently, the signature of the function that creates a the client on the new Isolate must be

typedef InitClient = Future<TypedLinkWithCache> Function(
  Map<String, dynamic> params,
  SendPort? sendMessageToMessageHandler,
);

do you think we should make it generic, like

typedef InitClient<InitParams> = Future<TypedLinkWithCache> Function(
  InitParmams params,
  SendPort? sendMessageToMessageHandler,
);

and change the create() method of the IsolateClient to

 static Future<IsolateClient> create<InitParams>(InitClient<InitParams> initClient,
      {InitParams params,
      void Function(Object?)? messageHandler,
      IsolateSpawn? isolateSpawn})

This would be typesafe, which is definitely a big plus.

However, it could lead to users trying the send objects which cannot be serialized over isolates (like SharedPreferences, HiveBoxes, Closures... basically anything that is not just data), while Map<String, dynamic> is already typically used for json-like data.

Copy link
Contributor

Choose a reason for hiding this comment

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

Well, Isolate.spawn is generic so I'd say it's fair to do the same.
We can always add an API comment with /// to say "Make sure InitParams is serializable as it will be passed to the isolate".

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

True. I'll update it.

final disposeCommand = command as DisposeCommand;
disposeCommand.sendPort.send(null);
Isolate.exit(disposeCommand.sendPort);
void handleCommand(
Copy link
Contributor

Choose a reason for hiding this comment

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

A more SOLID approach would be to add an abstract method called handler to the IsolateCommand class.

This way you would force all the implementing classes to implement this method and you wouldn't need this big switch/case or the CommandType enum.
In the future, if we need to add a new command, we will just create a new class that extends IsolateCommand and we wouldn't have to touch other parts of the code.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I agree that this would be the right way to do it usually, not sure in this specific instance though.

I didn't really look into the serialization format the dart uses when sending objects through isolates, but wouldn't that lead to overhead? Since the commands are created on the main isolate, then serialized, sent to the ferry isolate, and then deserialized to an IsolateCommand object again, the commands should probably be as lightweight as possible.

So to achieve a SOLID approach without sacrificing performance we would need to maintain a parallel class structure on the ferry isolate side, which I feel is overkill as long as there are only a handful of commands.

Copy link
Contributor

Choose a reason for hiding this comment

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

You're right.

packages/ferry/test/isolate/isolate_client_test.dart Outdated Show resolved Hide resolved
Martin Kamleithner added 6 commits September 18, 2022 04:44
@knaeckeKami knaeckeKami merged commit 35a434c into master Sep 18, 2022
@knaeckeKami knaeckeKami deleted the feat/ferry_isolate branch September 18, 2022 19:30
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.

None yet

2 participants