-
Notifications
You must be signed in to change notification settings - Fork 28.9k
[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
[framework] inline casts on Element.widget getter to improve web performance #97822
Conversation
@@ -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; |
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.
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?
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.
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?
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.
It may also be helpful to add a note to this doc string describing the pattern.
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.
I think we could probably make this API mostly private too. I would need to update the doc with a better pattern
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.
I've removed the public API
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.
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(); |
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.
Maybe pull this out into a separate PR so we get separate benchmark runs for these two independent changes?
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.
Will do
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.
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; |
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.
I believe this cast can also be removed as follows since the code never relies on this being a StatelessWidget:
final StatelessWidget widget = element.widget as StatelessWidget; | |
final Widget widget = element.widget; |
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.
Ah nice
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.
Done
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.
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]. |
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.
Would be helpful to add a little color to this doc explaining when one should use widget
and when typedWidget
and why.
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.
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
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.
What do you mean by "just fix that" - do you mean making it generic? Or do you see another way to avoid the private typedWidget
s?
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.
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
Nice!
Would the breakage be contained in package:nested and could simply be fixed there? Or would it also require changes to its customers? |
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 |
could we type it |
No, that wouldn't work since the Element subtypes would need to tighten the constraint, |
Can we make it |
Unfortunately |
Is there a bug on that? It seems like |
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. |
I don't think its a bug, its just a cost of targeting a slow language |
In release builds there's no reason to add the test though right? That's what I mean by "free". |
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 |
@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. |
Late issue is here: dart-lang/sdk#43361 |
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 |
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:
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 withElement.widget
orBuildContext.widget
expecting only aWidget
- 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