Thanks to visit codestin.com
Credit goes to github.com

Skip to content

[framework] inline casts on Element.widget getter to improve web performance #97822

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

Merged
merged 11 commits into from
Feb 8, 2022

Conversation

jonahwilliams
Copy link
Contributor

@jonahwilliams jonahwilliams commented Feb 4, 2022

This is an alternative to the proposal to make Element generic. After investigation, I discovered that the majority of the casting was actually unnecessary: here is an explanation:

class A {
  Object get foo;
}

class B extends A {
  String get foo => super.foo as String;
}

void main() {
  A a = B();
  Object bar = a.foo;
}

Consider the snippet above. Despite the fact that we only need an Object, as long as the subtype B is used there will be a cast performed on B.foo. Consider the Element/BuildContext API: the vast majority of the framework interacts with Element.widget or BuildContext.widget expecting only a Widget - but since all Element subclasses override the type of widget and use a cast, almost all accesses have a cast inserted even if the more specific Widget type is not required by the call site.

This leaves an alternative optimization - introduce a different getter with a cast and avoid overriding Element.widget. Furthermore, this presents a chance to optimize Element.widget by making the field non-nullable and using _NullWidget as a sentinel. This null check is fairly expensive in dart2js.

To benchmark this I used my favorite benchmark from #95596

On the web the average time of this benchmark decreased from 1845 ms to 1688 ms (~9%). On my Pixel 4 there was essentially no difference, which may just represent the lower cost of casts/null checks in AOT

The change to make Element generic was #97249 and would cause a breaking change in package:nested

@flutter-dashboard flutter-dashboard bot added a: tests "flutter test", flutter_test, or one of our tests a: text input Entering text in a text field or keyboard related problems f: cupertino flutter/packages/flutter/cupertino repository f: material design flutter/packages/flutter/material repository. f: scrolling Viewports, list views, slivers, etc. framework flutter/packages/flutter repository. See also f: labels. labels Feb 4, 2022
@jonahwilliams jonahwilliams marked this pull request as ready for review February 4, 2022 19:26
@@ -3233,8 +3233,8 @@ abstract class Element extends DiagnosticableTree implements BuildContext {

/// The configuration for this element.
@override
Widget get widget => _widget!;
Widget? _widget;
Widget get widget => _widget;
Copy link
Member

Choose a reason for hiding this comment

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

I thought we had documentation somewhere that was specifically telling people to override this with a getter that type casts to the right type. Obviously, that would need to be updated. But I can't find it, so maybe we don't?

Copy link
Member

Choose a reason for hiding this comment

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

Ah! It's on RenderObjectElement: https://master-api.flutter.dev/flutter/widgets/RenderObjectElement-class.html

Can you update that doc there with the new practise?

Copy link
Member

Choose a reason for hiding this comment

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

It may also be helpful to add a note to this doc string describing the pattern.

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 think we could probably make this API mostly private too. I would need to update the doc with a better pattern

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've removed the public API

Copy link
Member

Choose a reason for hiding this comment

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

https://master-api.flutter.dev/flutter/widgets/RenderObjectElement-class.html still needs to be updated, though. It's telling people to do the casting pattern for new RenderObjectElements that we are now discouraging.

Widget get widget => _widget!;
Widget? _widget;
Widget get widget => _widget;
Widget _widget = const _NullWidget();
Copy link
Member

Choose a reason for hiding this comment

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

Maybe pull this out into a separate PR so we get separate benchmark runs for these two independent changes?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Will do

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

@@ -62,7 +62,7 @@ abstract class Notification {
@mustCallSuper
bool visitAncestor(Element element) {
if (element is StatelessElement) {
final StatelessWidget widget = element.widget;
final StatelessWidget widget = element.widget as StatelessWidget;
Copy link
Member

Choose a reason for hiding this comment

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

I believe this cast can also be removed as follows since the code never relies on this being a StatelessWidget:

Suggested change
final StatelessWidget widget = element.widget as StatelessWidget;
final Widget widget = element.widget;

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah nice

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
Contributor Author

Choose a reason for hiding this comment

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

actually this whole notification API is sus, I need to look at this more later

@@ -5571,8 +5572,8 @@ abstract class RenderObjectElement extends Element {
/// Creates an element that uses the given widget as its configuration.
RenderObjectElement(RenderObjectWidget widget) : super(widget);

@override
RenderObjectWidget get widget => super.widget as RenderObjectWidget;
/// A typed override of [Element.widget].
Copy link
Member

Choose a reason for hiding this comment

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

Would be helpful to add a little color to this doc explaining when one should use widget and when typedWidget and why.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Since I removed the public API, I wonder what we should do here. We could warn about casting, but ideally we'd just fix that and delete the typedWidget private API later

Copy link
Member

Choose a reason for hiding this comment

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

What do you mean by "just fix that" - do you mean making it generic? Or do you see another way to avoid the private typedWidgets?

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 mean ideally we would make casts faster, and then we wouldn't need to do the work around. I'll update the documentation to clarify the casting performance

@goderbauer
Copy link
Member

On the web the average time of this benchmark decreased from 1845 ms to 1688 ms (~9%).

Nice!

The change to make Element generic was https://github.com/flutter/flutter/pull/97249[](https://github.com/jonahwilliams) and would cause a breaking change in package:nested

Would the breakage be contained in package:nested and could simply be fixed there? Or would it also require changes to its customers?

@jonahwilliams
Copy link
Contributor Author

It could be fixed there, and I think it would be mostly self contained. The downside of making Element generic is that you can't make _widget non-nullable anymore, since it is typed T and we cannot create a const placeholder for an arbitrary T.

@Hixie
Copy link
Contributor

Hixie commented Feb 4, 2022

The downside of making Element generic is that you can't make _widget non-nullable anymore, since it is typed T and we cannot create a const placeholder for an arbitrary T.

could we type it T extends Widget and use your _NullWidget trick?

@jonahwilliams
Copy link
Contributor Author

No, that wouldn't work since the Element subtypes would need to tighten the constraint, StatelessElement<T extends StatelessWidget> extends ComponentElement<T>, and NullWidget would no longer satisfy that

@Hixie
Copy link
Contributor

Hixie commented Feb 4, 2022

Can we make it late and have a separate flag for when it's not set?

@jonahwilliams
Copy link
Contributor Author

Unfortunately late also has terrible performance in dart2js....

@Hixie
Copy link
Contributor

Hixie commented Feb 4, 2022

Is there a bug on that? It seems like late should be entirely free in release builds (where we can just fail with a JS error instead of a Dart error if it's not valid)...

@jonahwilliams
Copy link
Contributor Author

The nice part about making it non-nullable is that if you are using the API correctly there is no additional cost for accessing the field. The problem with late/nullable/a flag is that you need to insert a check on all accesses, and since JS is such a slower language than native machine code you pay an oversized effect.

@jonahwilliams
Copy link
Contributor Author

I don't think its a bug, its just a cost of targeting a slow language

@Hixie
Copy link
Contributor

Hixie commented Feb 4, 2022

In release builds there's no reason to add the test though right? That's what I mean by "free".

@jonahwilliams
Copy link
Contributor Author

Actually I sort of take that back. For reference, in a native build on the same machine this benchmark takes 747 ms, which is about 2.2X as fast as the web version. If you assume the same lines of code run, and each line ran in about the same speed with the same optimization, then I would expect that eliminating the cast would have about half the impact on native.

But I don't see that at all, instead the needle doesn't move at all on a native build - which implies that as casts are much more expensive on the web.

@jonahwilliams
Copy link
Contributor Author

@Hixie I believe dart2js is attempting to follow the spec which has specific kinds of behavior documented (i.e. LateInitializationError thrown) which is where the expense comes from.

@jonahwilliams
Copy link
Contributor Author

Late issue is here: dart-lang/sdk#43361

@jonahwilliams
Copy link
Contributor Author

I made all of the typedWidget getters private so there is no change to public API besides removing the more specific types from subclasses. I also removed the nullcheck - I did not observe a performance delta so I definitely overstated the impact of the nullcheck

engine-flutter-autoroll added a commit to engine-flutter-autoroll/plugins that referenced this pull request Feb 8, 2022
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Feb 8, 2022
engine-flutter-autoroll added a commit to engine-flutter-autoroll/plugins that referenced this pull request Feb 9, 2022
engine-flutter-autoroll added a commit to engine-flutter-autoroll/plugins that referenced this pull request Feb 9, 2022
engine-flutter-autoroll added a commit to engine-flutter-autoroll/plugins that referenced this pull request Feb 9, 2022
engine-flutter-autoroll added a commit to engine-flutter-autoroll/plugins that referenced this pull request Feb 9, 2022
engine-flutter-autoroll added a commit to engine-flutter-autoroll/plugins that referenced this pull request Feb 9, 2022
engine-flutter-autoroll added a commit to engine-flutter-autoroll/plugins that referenced this pull request Feb 9, 2022
engine-flutter-autoroll added a commit to engine-flutter-autoroll/plugins that referenced this pull request Feb 9, 2022
engine-flutter-autoroll added a commit to engine-flutter-autoroll/plugins that referenced this pull request Feb 9, 2022
engine-flutter-autoroll added a commit to engine-flutter-autoroll/plugins that referenced this pull request Feb 9, 2022
engine-flutter-autoroll added a commit to engine-flutter-autoroll/plugins that referenced this pull request Feb 9, 2022
engine-flutter-autoroll added a commit to engine-flutter-autoroll/plugins that referenced this pull request Feb 9, 2022
engine-flutter-autoroll added a commit to engine-flutter-autoroll/plugins that referenced this pull request Feb 9, 2022
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
a: tests "flutter test", flutter_test, or one of our tests a: text input Entering text in a text field or keyboard related problems f: cupertino flutter/packages/flutter/cupertino repository f: material design flutter/packages/flutter/material repository. f: scrolling Viewports, list views, slivers, etc. framework flutter/packages/flutter repository. See also f: labels.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants