Skip to content

Conversation

@jonahwilliams
Copy link
Contributor

@jonahwilliams jonahwilliams commented Feb 14, 2022

Background

The notification API must traverse the element tree in order to locate a notification receiver. This can lead to some unfortunate performance characteristics:

/// Applied to each ancestor of the [dispatch] target.
///
/// The [Notification] class implementation of this method dispatches the
/// given [Notification] to each ancestor [NotificationListener] widget.
///
/// Subclasses can override this to apply additional filtering or to update
/// the notification as it is bubbled (for example, increasing a `depth` field
/// for each ancestor of a particular type).
@protected
@mustCallSuper
bool visitAncestor(Element element) {
if (element is StatelessElement) {
final Widget widget = element.widget;
if (widget is NotificationListener<Notification>) {
if (widget._dispatch(this, element)) // that function checks the type dynamically
return false;
}
}
return true;
}

  • If there is no receiver for a given notification type, the entire element tree above the notification will be traversed and type checked.
  • For multiple notifications in a given frame (which is common for scroll views) we can end up traversing the element tree multiple times
  • If there are multiple or nested scroll views on a given page, the situation is worsened significantly.

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.

image

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.

@override
bool visitAncestor(Element element) {
if (element is RenderObjectElement && element.renderObject is RenderAbstractViewport)
_depth += 1;
return super.visitAncestor(element);
}

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:

/// Mixin this class to Elements allow receiving [Notification] objects dispatched
/// by children.
///
/// 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.
 bool onNotification(Notification notification);

}

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

image

Fixes #97849

@flutter-dashboard flutter-dashboard bot added 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 14, 2022
@goderbauer
Copy link
Member

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 Viewport widget's custom element _ViewportElement just override dispatchNotification with the depth increasing logic?

@jonahwilliams
Copy link
Contributor Author

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....

@skia-gold
Copy link

Gold has detected about 1 new digest(s) on patchset 2.
View them at https://flutter-gold.skia.org/cl/github/98451

@jonahwilliams
Copy link
Contributor Author

Tried to do some stuff with mixins but it is quite tricky.

@jonahwilliams
Copy link
Contributor Author

OK!

Comment on lines 4408 to 4421
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;

Copy link
Contributor Author

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

Copy link
Member

@goderbauer goderbauer Feb 16, 2022

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._attachNotificationTree always sets _notificationTree = _parent?._notificationTree;
  • a new NotifiableElementMixin overrides _attachNotificationTree to set itself as the _notificationTree and it provides the abstract onNotification method.
  • Element.onNotification is 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.

Copy link
Contributor Author

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.

Copy link
Member

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();
}

Copy link
Member

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...

Copy link
Member

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...

Copy link
Contributor Author

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

@jonahwilliams jonahwilliams marked this pull request as ready for review February 16, 2022 16:46
@jonahwilliams
Copy link
Contributor Author

Need to fill out docs more obvs...

@jonahwilliams jonahwilliams changed the title [framework] prototype - improve Notification API performance [framework] improve Notification API performance by skipping full Element tree traversal Feb 16, 2022
Copy link
Member

@goderbauer goderbauer left a 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
Copy link
Member

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?

Copy link
Contributor Author

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.
Copy link
Member

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.

Copy link
Contributor Author

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;

Copy link
Member

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.

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

}

/// An element used to host [NotificationListener] elements.
@optionalTypeArgs
Copy link
Member

Choose a reason for hiding this comment

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

why the optionalTypeArgs annotation?

Copy link
Contributor Author

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
Copy link
Member

Choose a reason for hiding this comment

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

Viewport -> [Viewport]

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

/// See also:
/// * [Viewport], which creates a custom [MultiChildRenderObjectElement] that mixes
/// this in.
mixin ViewportElement on NotifiableElementMixin {
Copy link
Member

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?

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

/// See also:
/// * [Viewport], which creates a custom [MultiChildRenderObjectElement] that mixes
/// this in.
mixin ViewportElement on NotifiableElementMixin {
Copy link
Member

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.

Copy link
Contributor Author

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?

Copy link
Contributor Author

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

@jonahwilliams
Copy link
Contributor Author

I'm running a full presubmit right now 👯

/// for each ancestor of a particular type).
@protected
@mustCallSuper
bool visitAncestor(Element element) {
Copy link
Contributor Author

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

Copy link
Member

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?

Copy link
Contributor Author

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?

Copy link
Contributor Author

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

Copy link
Member

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.

Copy link
Member

@goderbauer goderbauer left a 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) {
Copy link
Member

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?

@jonahwilliams
Copy link
Contributor Author

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

engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Mar 11, 2022
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Mar 11, 2022
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Mar 11, 2022
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Mar 11, 2022
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Mar 11, 2022
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Mar 11, 2022
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Mar 11, 2022
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Mar 11, 2022
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Mar 12, 2022
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Mar 14, 2022
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Mar 14, 2022
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Mar 14, 2022
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Mar 14, 2022
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Mar 14, 2022
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Mar 14, 2022
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Mar 14, 2022
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Mar 14, 2022
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Mar 14, 2022
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Mar 14, 2022
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Mar 14, 2022
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Mar 14, 2022
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Mar 14, 2022
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Mar 14, 2022
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Mar 14, 2022
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Mar 15, 2022
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Mar 15, 2022
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Mar 15, 2022
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Mar 15, 2022
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Mar 15, 2022
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Mar 15, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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.

Notifcation API is expensive

3 participants