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

Avoid type casts in Future implementation due to usage of untyped function #48225

Closed
mkustermann opened this issue Jan 26, 2022 · 4 comments
Closed
Labels
area-core-library SDK core library issues (core, async, ...); use area-vm or area-web for platform specific libraries. type-performance Issue relates to performance or code size

Comments

@mkustermann
Copy link
Member

The _FutureListener implementation looks like this:

class _FutureListener<S, T> {
  ...
  final Function? callback;
  ...
  _FutureListener.then(
      this.result, FutureOr<T> Function(S) onValue, Function? errorCallback)
      : callback = onValue ...;

  _FutureListener.thenAwait(
      this.result, _FutureOnValue<S, T> onValue, Function errorCallback)
      : callback = onValue ...;
  ...
  FutureOr<T> Function(S) get _onValue {
    assert(handlesValue);
    return callback as dynamic;
  }
  ...
  FutureOr<T> handleValue(S sourceResult) {
    return _zone.runUnary<FutureOr<T>, S>(_onValue, sourceResult);
  }
  ...
}

This causes a performance problem due to the runtime type check in _onValue from Function? to FutureOr<T> Function(S). The interesting thing is that we actually have correctly typed closures passed as arguments to the constructor, though then we "intentionally" loose those function types when assigning to the Function? callback.

Those function subtype checks are rather expensive in the VM and can cause apps to run over a performance cliff (once the size of a cache is exhausted). stc_overflow_in_future_impl.dart contains an example that causes a SubtypeTestCache overflow and makes the code call to runtime for every subsequent type check.

Even if we optimized those function type checks in the VM more - the fact that we have to do the runtime type checks is a performance issue for async heavy code.

@lrhn This is a big issue for async heavy code (including for some 1p customers). Do you think the code can be re-written to avoid the typechecks in _onValue, _errorTest, _whenCompleteAction?

(/cc @sstrickl This is a case of STC overflow for our 1p customer)

@mkustermann mkustermann added area-core-library SDK core library issues (core, async, ...); use area-vm or area-web for platform specific libraries. type-performance Issue relates to performance or code size labels Jan 26, 2022
@mkustermann
Copy link
Member Author

Given that the tests should succeed always, I think the easy solution would be to use unsafeCast<> there.
=> cl/230240

@rakudrama
Copy link
Member

There are 5 fields instead of 7 if callback was split three ways.
It does not seem like a big win.
The memory saving is an illusion if the type tests are causing a large cache to exist that otherwise would not.
Would a type-safe 3-field version avoid the type check, and cache, or would there still be checks due to parametric covariance?

Is there some unfortunate coupling with the VM (callback is annotated)?

@mkustermann
Copy link
Member Author

Would a type-safe 3-field version avoid the type check, and cache, or would there still be checks due to parametric covariance?

It would avoid the checks, the constructor would get the properly typed function, we would store into a field of the same type and don't have to cast when loading the field.

Though IMHO given how critical the future implementation is for perf (and possibly memory), I think it justifies using a unsafeCast<> - we do that in some other critical places as well.

@lrhn
Copy link
Member

lrhn commented Jan 31, 2022

It's hard to avoid type checks in the error handler, because it has to accept both unary and binary callbacks. (I really want to just force binary callbacks, we never succeeded at making the stack trace not be allocated when you don't use it. Since you can always add a .catchError((e, s) {}) to a future at any later point, we always have to have the stacktrace anyway.)
On the other hand, errors are probably not on the critical performance path anyway. If you need to optimize error handling, you have too many errors.

We could add just one more field, and keep the then and test callbacks typed.

  FutureOr<T> Function(S)? _onValueCallback;
  Function? _onErrorOrWhenCompleteCallback;
  bool Function(Object)? _errorTestCallback;

Then the then branch, which is the vast majority of cases in practice, will be well-typed.
The catchError/test case will be typed, but that's just because we need a third field for that when the on-value callback is typed.
The whenComplete will need a cast, to dynamic Function(), which I hope we can be somewhat efficient about.

We still need to check the results of the callbacks for being futures, including is Future<T> for the value and error callbacks (just Future<void> for whenComplete). There is no way around that.

This conflation of fields is only because we try to avoid polymorphism. If each kind of handler could have its own class, we could be completely typed and have no superfluous fields. How efficient would we be able to make that? That is, is one polymorhic call more expensive than what are doing here?

copybara-service bot pushed a commit that referenced this issue Feb 1, 2022
Avoids an extra function call and removes a casts that are
always expected to succeed from the compiled code in the
sdk async library. The motivating changes are introduced in
https://dart-review.googlesource.com/c/sdk/+/230240.

Change-Id: I4da07a54f025ee4e2d0a5a0b4fd4ce9c7e765454
Issue: #48225
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/230884
Reviewed-by: Sigmund Cherem <sigmund@google.com>
Reviewed-by: Stephen Adams <sra@google.com>
Commit-Queue: Nicholas Shahan <nshahan@google.com>
copybara-service bot pushed a commit that referenced this issue Feb 4, 2022
…ation

The `_FutureListener.callback` field is used for multiple purposes
(possibly to save memory by avoiding additional fields) so it cannot
have a specific function type.

This causes the `Function?` typed `_FutureListener.callback` field to be
casted to proper function types such as `T Function(S)`.
=> Those runtime type checks are very expensive.

To avoid introducing more fields we use an `unsafeCast<T>()` for those
casts, since the implementation guarantees they are going to suceed.

Issue #48225

TEST=ci

Change-Id: I6f0cc87600462c8ca5baceeb511ce8a06c61237e
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/230240
Reviewed-by: Nicholas Shahan <nshahan@google.com>
Reviewed-by: Lasse Nielsen <lrn@google.com>
Commit-Queue: Martin Kustermann <kustermann@google.com>
copybara-service bot pushed a commit that referenced this issue Jun 9, 2022
This reverts commit 69f32d6.

Reason for revert: We seem to have a number of tests failing with timeouts in CBUILD after this change, please see logs at
https://dart-in-g3-qa-prod.corp.google.com/dg3/Home#/cbuild/find/69f32d6ad7e724e3148cb2eb6601e63165e76ad3

Original change's description:
> Refactor `_Future`.
>
> This is a major rewrite of the `_Future` class,
> which is the default implementation of the `Future` interface.
>
> The main goal was to reduce the number of expensive type checks
> in the internal passing around of data.
> Expensive type checks are things like
> * `is _Future<T>` (more expensive than just `is _Future`, the latter
>   can be a single class-ID check.
> * Covariant generic parameter checks (using `T` covariantly in a
>   parameter forces a run-time type check).
>
> Also removed some plain unnecessary casts and turned some
> implicit casts from `dynamic` into `unsafeCast`s.
>
> This seems to be an success, at least on very primitive benchmarks, according to Golem:
> FutureCatchErrorTest    41.22% (1.9 noise)
> FutureValueTest         46.51% (2.8 noise)
> EmptyFutureTest         59.15% (3.1 noise)
> FutureWhenCompleteTest  51.10% (3.2 noise)
>
> A secondary goal was to clean up a very old and messy class,
> and make it clearer for other `dart:async` how to interact
> with the future.
>
> The change has a memory cost: The `_FutureListener<S,T>` class,
> which represents a `then`, `catchError` or `whenComplete`
> call on a `_Future`, now contains a reference to its source future,
> the one which provides the inputs to the callbacks,
> as well as the result future returned by the call.
> That's one extra memory slot per listener.
>
> In return, the `_FutureListener` now does not need to
> get its source future as an argument, which needs a covariant
> generic type check, and the methods of `_Future` can be written
> in a way which ignores the type parameters of both `_Future`
> and `_FutureListener`, which reduces complex type checks
> significantly.
>
> In general, typed code is in `_FutureListener`, which knows both
> the source and target types of the listener callbacks, and which
> contains the futures already at that type, so no extra type checking
> is needed.
> The `_Future` class is mostly untyped, except for its "public"
> API, called by other classes, which checks inputs,
> and code interacting with non-native futures.
> Invariants ensure that only correctly typed values
> are stored in the untyped shared `_resultOrListeners` field
> on `_Future`, as determined by its `_state` integer.
> (This was already partially true, and has simply been made
> more consistent.)
>
> Further, we now throw an error in a situation that was previously
> unhandled: When a `_Future` is completed with *itself*.
> That would ensure that the future would never complete
> (it waits for itself to complete before it can complete),
> and may potentially have caused weird loops in the representation.
> In practice, it probably never happens. Now it makes the error
> fail with an error.
> Currently a private `_FutureCyclicDependencyError` which presents
> as an `UnsupportedError`.
> That avoids code like
> ```dart
> import "dart:async";
> void main() {
>   var c = Completer();
>   c.complete(c.future); // bad.
>   print("well!");
>   var d = Completer();
>   d.complete(c.future);
>   print("shucks!");
> }
> ```
> from hanging the runtime by busily searching for the end of a cycle.
>
> See #48225
> Fixes #48225
>
> TEST= refactoring covered by existing tests, few new tests.
>
> Change-Id: Id9fc5af5fe011deb0af3e1e8a4ea3a91799f9da4
> Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/244241
> Reviewed-by: Martin Kustermann <kustermann@google.com>
> Commit-Queue: Lasse Nielsen <lrn@google.com>

TBR=lrn@google.com,kustermann@google.com,sra@google.com,sigmund@google.com,nshahan@google.com

Change-Id: I455be5a04b4c346df26d4ded0fa7388baccb0f8c
No-Presubmit: true
No-Tree-Checks: true
No-Try: true
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/247762
Reviewed-by: Siva Annamalai <asiva@google.com>
Reviewed-by: Alexander Aprelev <aam@google.com>
Commit-Queue: Alexander Aprelev <aam@google.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-core-library SDK core library issues (core, async, ...); use area-vm or area-web for platform specific libraries. type-performance Issue relates to performance or code size
Projects
None yet
Development

No branches or pull requests

3 participants