-
Notifications
You must be signed in to change notification settings - Fork 29.7k
[framework] improve Notification API performance by skipping full Element tree traversal #98451
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
Conversation
|
It would be cool if ViewportBoundary could just be an implementation detail of the Viewport widget (without the need to expose something like ViewportInternal)... As an idea: could the |
|
dispatchNotification would skip any Element that isn't a notification element by design. We'd have to expose some APIs for overriding that allow other element types to participate in the notification tree. I think you're right though, this is probably the way to go and involves fewer breaking changes. I'll think about this one some more.... |
|
Gold has detected about 1 new digest(s) on patchset 2. |
This reverts commit e888e05.
|
Tried to do some stuff with mixins but it is quite tricky. |
|
OK! |
| void dispatchNotification(Notification notification) { | ||
| _notificationTree?.dispatchNotification(notification); | ||
| } | ||
|
|
||
| @override | ||
| bool onNotification(Notification notification) { | ||
| return false; | ||
| } | ||
|
|
||
| /// Whether this element can handle [Notifications]. | ||
| /// | ||
| /// The value of this getter must be unchanging for the lifetime of the Element. | ||
| bool get handlesNotification => false; | ||
|
|
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.
@goderbauer this is basically the new API I added to make this work. I'm not 100% how I feel about it, but it does allow the viewport elements to just mixin a class to support the scroll notifications
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.
You mentioned you tried mixin's (so maybe this doesn't work), but could we have something like this:
Element._attachNotificationTreealways sets_notificationTree = _parent?._notificationTree;- a new
NotifiableElementMixinoverrides_attachNotificationTreeto set itself as the_notificationTreeand it provides the abstractonNotificationmethod. Element.onNotificationis removed
That way we wouldn't need the awkward handlesNotification thing...
Users would just create custom elements with NotifiableElementMixin mixed in and implement the onNotification method.
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.
You can't apply a mixin to a type that overrides a private method. For example, the _attachNotificationTree override defined by the mixin will conflict with the _attachNotificationTree that the NotificationElement inherits from Element.
The only way around this is to make the method public. We could use a protected annotation though. I do like this API better than what I have.
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.
You can't apply a mixin to a type that overrides a private method.
This seems to work in dartpad:
class Foo {
foo() {
_attachNotificationTree();
}
_attachNotificationTree() {
print("hello");
}
}
mixin FooMixin on Foo {
@override
_attachNotificationTree() {
print("Hell from mixin");
}
}
class Ha extends Foo with FooMixin {
}
main() {
Ha().foo();
}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.
Ugh, never mind. This only works because Ha is in the same file...
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.
Making it protected doesn't sound like the worst idea then...
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.
Yeah, its not so bad - and its not the only element API that is protected too
|
Need to fill out docs more obvs... |
goderbauer
left a comment
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.
now the big question: Does this break anything in e.g. google3?
| } | ||
| } | ||
|
|
||
| /// Mixin this class to Elements allow receiving [Notification] objects dispatched |
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.
nit: somehow this sentence sounds grammatically incorrect?
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.
fixed
| /// See also: | ||
| /// * [NotificationListener], for a widget that allows consuming notifications. | ||
| mixin NotifiableElementMixin on Element { | ||
| /// Return true to consume the notification or false to let it continue bubbling. |
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.
add documentation explaining when this is called.
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.
borrowed some existing docs
| DebugReassembleConfig? _debugReassembleConfig; | ||
|
|
||
| _NotificationNode? _notificationTree; | ||
|
|
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.
nit: remove extra white space.
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
| } | ||
|
|
||
| /// An element used to host [NotificationListener] elements. | ||
| @optionalTypeArgs |
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.
why the optionalTypeArgs annotation?
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.
outdated, removed
| } | ||
| } | ||
|
|
||
| /// A mixin that allows [Element]s containing Viewport like widgets to correctly |
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.
Viewport -> [Viewport]
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
| /// See also: | ||
| /// * [Viewport], which creates a custom [MultiChildRenderObjectElement] that mixes | ||
| /// this in. | ||
| mixin ViewportElement on NotifiableElementMixin { |
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 rename to ViewportElementMixin to keep the naming scheme?
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
| /// See also: | ||
| /// * [Viewport], which creates a custom [MultiChildRenderObjectElement] that mixes | ||
| /// this in. | ||
| mixin ViewportElement on NotifiableElementMixin { |
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.
We should probably link to this from some other docs as well so people implementing custom viewports find it.
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 is a good place where we describe this - on Viewport itself?
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.
added a See More section
|
I'm running a full presubmit right now 👯 |
| /// for each ancestor of a particular type). | ||
| @protected | ||
| @mustCallSuper | ||
| bool visitAncestor(Element element) { |
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.
Even though nothing broke in customer tests or g3, do you think we should leave this and mark it deprecated? I could also leave the old dispatch path and instead add a new method to do a fast dispatch
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.
leaving this and the old dispatch in as deprecated sounds reasonable to me. I just wonder if we can find a reasonable new name for the new dispatch. fastDispatch sounds strange after we remove the regular dispatch - and what if we find an even faster dispatch in the future?
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 I could just leave the BuildContext dispatch and deprecate the dispatch on Notification?
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.
that is, BuildContext would have the new dispatch and Notification would keep the old
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.
That sounds reasonable. We'd just have to check all docs pointing to it on the Notification and update them to point to BuildContext instead.
goderbauer
left a comment
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
| /// for each ancestor of a particular type). | ||
| @protected | ||
| @mustCallSuper | ||
| bool visitAncestor(Element element) { |
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.
leaving this and the old dispatch in as deprecated sounds reasonable to me. I just wonder if we can find a reasonable new name for the new dispatch. fastDispatch sounds strange after we remove the regular dispatch - and what if we find an even faster dispatch in the future?
|
I did spend some time going through the migration guide today. I guess I'm sort of concerned that for folks that don't override visitAncestor on Notification ... we're going to create a bunch of churn for no reason. Maybe I need to do a package survey to see if anyone actually does |
…full Element tree traversal (flutter/flutter#98451)
…full Element tree traversal (flutter/flutter#98451)
…full Element tree traversal (flutter/flutter#98451)
…full Element tree traversal (flutter/flutter#98451)
…full Element tree traversal (flutter/flutter#98451)
…full Element tree traversal (flutter/flutter#98451)
…full Element tree traversal (flutter/flutter#98451)
…full Element tree traversal (flutter/flutter#98451)
…full Element tree traversal (flutter/flutter#98451)
…full Element tree traversal (flutter/flutter#98451)
…full Element tree traversal (flutter/flutter#98451)
…full Element tree traversal (flutter/flutter#98451)
…full Element tree traversal (flutter/flutter#98451)
…full Element tree traversal (flutter/flutter#98451)
…full Element tree traversal (flutter/flutter#98451)
…full Element tree traversal (flutter/flutter#98451)
…full Element tree traversal (flutter/flutter#98451)
…full Element tree traversal (flutter/flutter#98451)
…full Element tree traversal (flutter/flutter#98451)
…full Element tree traversal (flutter/flutter#98451)
…full Element tree traversal (flutter/flutter#98451)
…full Element tree traversal (flutter/flutter#98451)
…full Element tree traversal (flutter/flutter#98451)
…full Element tree traversal (flutter/flutter#98451)
…full Element tree traversal (flutter/flutter#98451)
…full Element tree traversal (flutter/flutter#98451)
…full Element tree traversal (flutter/flutter#98451)
…full Element tree traversal (flutter/flutter#98451)
…full Element tree traversal (flutter/flutter#98451)
…full Element tree traversal (flutter/flutter#98451)
Background
The notification API must traverse the element tree in order to locate a notification receiver. This can lead to some unfortunate performance characteristics:
flutter/packages/flutter/lib/src/widgets/notification_listener.dart
Lines 53 to 73 in 9407700
What is the impact of these performance problems? If we look at devtools as an example app we find that about 30% CPU time while scrolling the flame graph is spent dispatching notifications. Less severely, in flutter/gallery Crane demo on mobile we spend 2-3% CPU dispatching notifications for scrollbars that are entirely unused.
Detailed Design
In order to reduce the cost of dispatching notifications, we must traverse fewer elements. This can be done by creating a new Tree (Notification Tree) hosted in the element tree that only contains Elements which can respond to notifications. Since there are many times fewer notification listeners than elements, this is significantly faster.
However, the old notification system exposed the fact that it traversed each element as part of its API. This was used by certain scroll notifications to check for Viewport render objects in order to increment a depth property. If we skip most elements, then we would also skip the corresponding RenderObjectElements.
flutter/packages/flutter/lib/src/widgets/scroll_notification.dart
Lines 27 to 33 in 9407700
To support this, we need to not only create an Element type for NotificationListeners, but provide an API that allows different Element subtypes to subscribe to notifications. The following is the new Element API added:
Improvements
The result of this change is that notification dispatch drops to about 2-3% CPU on devtools and no longer appears in Crane. This is likely to be a breaking change, though I have not yet run google3 tests to confirm. Regardless, we may need to consider a migrating strategy where for a period of time both dispatching mechanisms are supported
Fixes #97849