-
Notifications
You must be signed in to change notification settings - Fork 27k
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
Refractor compute
#99527
Refractor compute
#99527
Conversation
Send all events to one ReceivePort. This is both simpler and more efficient. To correctly identify which event was sent the result returned by the callback passed by the user is wrapped in a List. This way the following is true. When the first event received from the port is: 1. null; it is an exit event i. e. the isolate exited prematurely 2. a single element list; it is a success event i. e. the isolate and computed were successfully completed 3. a double element list; it is an error event i. e. the isolate or user provided callback failed Any other type of event is impossible to receive. The first and third options are guaranteed by the Dart SDK. While the second we guarantee ourselves. We have to wrap the result in a List, because the type of the user callback could also be a List. Meaning, a simple `result is R` could return true for an error event. (Which is specified by the Dart SDK)
It looks like this pull request may not have tests. Please make sure to add tests before merging. If you need an exemption to this rule, contact Hixie on the #hackers channel in Chat (don't just cc him here, he won't see it! He's on Discord!). If you are not sure if you need tests, consider this rule of thumb: the purpose of a test is to make sure someone doesn't accidentally revert the fix. Ask yourself, is there anything in your PR that you feel it is important we not accidentally revert back to how it was before your fix? Reviewers: Read the Tree Hygiene page and make sure this patch meets those guidelines before LGTMing. |
compute
to use single ReceivePort for all eventscompute
to use single ReceivePort
for all events
There is also an edge case here that also wasn't handled in the previous iteration of the code, which is that no error will be reported if the entry point fails after the sending the result back to the main This is because after receiving the data we close all the ports as such no additional handlers are called. However, this is only an issue for the development team not the clients / consumers. I've been thinking a great deal about this and there is no simple or secure way of doing this that would be 100% future-proof. Also it's also impossible to test this because it would not something the caller is responsible for but the developer. To illustrate when this issue would occur: // if this were to be changed
Isolate.exit(configuration.resultPort, <R>[result]);
// to something like this
configuration.resultPort.send(<R>[result]);
configuration.resultPort.send(<R>[result]); // This send event would be ignored
// or
configuration.resultPort.send(<R>[result]);
throw Error(); // This error would not be reported The previous implementation also ignored additional success / result events, while reported an additional error. But because To actually force the handlers to be called one would have to delay returning and do something like configuration.resultPort.send(<R>[result]);
await Future.delay(Duration(seconds: 1), () => throw Error()); However, as long as we call Am I overthinking this? For now I'll just add a comment and if anyone has a better solution in mind, please let me know. |
@dnfield Would you mind reviewing this? |
The API we're currently using doesn't require the full interface the ReceivePort provides. Additionally we are not restricted / affected by the low-levelness of the RawReceivePort. After all, we are simply scheduling a callback which closes the port on the first message received.
I also added additional tests:
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@dnfield from the looks of it in Dart 2.17 a new method The differences I see in the implementation is:
For now, I will address 3. and 4. How should we proceed?
|
After taking a second look at the new The Which means the transition for end users wouldn't be just replacing the call but still relatively trivial if we chose to deprecate Anyways, awaiting additional feedback. |
I think it would make sense to do the following:
@goderbauer @Piinks wdyt? |
If we want to keep timing exactly intact, we need to actually implement the function ourselves. Since the implementation timed the If we're OK with simply timing the user callback then the new implementation is essentially: // Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'dart:developer';
import 'dart:isolate';
import 'constants.dart';
import 'isolates.dart' as isolates;
/// The dart:io implementation of [isolate.compute].
Future<R> compute<Q, R>(isolates.ComputeCallback<Q, R> callback, Q message, { String? debugLabel }) {
debugLabel ??= kReleaseMode ? 'compute' : callback.toString();
return Isolate.run(() => Timeline.timeSync(
debugLabel!,
() => callback(message),
));
} or // Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'dart:developer';
import 'dart:isolate';
import 'constants.dart';
import 'isolates.dart' as isolates;
void _void() {}
/// The dart:io implementation of [isolate.compute].
Future<R> compute<Q, R>(isolates.ComputeCallback<Q, R> callback, Q message, { String? debugLabel }) async {
debugLabel ??= kReleaseMode ? 'compute' : callback.toString();
final Flow flow = Flow.begin();
final int flowId = flow.id;
Timeline.timeSync('$debugLabel: start', _void, flow: flow);
final R result = await Isolate.run(_fn(callback, message, flowId, debugLabel));
Timeline.timeSync('$debugLabel: end', _void, flow: Flow.end(flowId));
return result;
}
FutureOr<R> Function() _fn<Q, R>(isolates.ComputeCallback<Q, R> callback, Q message, int flowId, String debugLabel) {
return () {
return Timeline.timeSync(
debugLabel,
() => callback(message),
flow: Flow.step(flowId)
);
};
} if we wish to fake the starts and ends. However, should we take the approach of wrapping the Of course, regardless of which wrapping we might chose. Test expectations will need to be changed (because of changed error reporting) and wait for the raised issue in the Dart SDK to be resolved. |
My preferred way would be to keep the compute functionality, but implement it in terms of the new Dart API. The tracing that compute adds over the raw Dart API has been useful. The important bits to keep for tracing here are the flow events (going from the point the computation was kicked of to the computation to the point where the result of the computation is received) and the duration of the computation. Measuring how long it takes to instantiate and close the ports is not important, IMO. I believe that was just done in the current implementation to get the flow events inserted properly. I don't think we should deprecate Is the switch from Exception to RemoteError actually breaking anything? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Still LGTM.
Thanks for sticking with this. It may not be apparent but your work here has helped improve things around the Isolate.run
API and identify some key areas needed to improve that.
This still needs a second LGTM - @goderbauer? |
Gold has detected about 1 new digest(s) on patchset 24. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
@dnfield Thank you very much. It was a fun exercise. |
All tests are green, and I just wanted to add one last note for documentation purposes of the changes. User facing changes (tiny):
User facing changes (minor):
Internal:
|
@mit-mit I don't know if you're the right person to tag. I just want to make sure users aren't forgotten to be informed about changes in my previous post (mostly exception handling). |
(About the assert: Until all programs are soundly null-safe, |
Aha!!! Thank you for the explanation. |
This PR fixes #90693 and makes the
compute
method more efficient by using a singleReceivePort
instead of 3.The code changes were inspired by @irhn 's comment in a Dart SDK issue.
Fixes #90693
Pre-launch Checklist
///
).