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

Optimize ContentLayers._isSubtreeDirty #1964

Merged
merged 3 commits into from
Apr 29, 2024

Conversation

knopp
Copy link
Contributor

@knopp knopp commented Apr 23, 2024

I was investigating excessive garbage collection when changing selection in documents with large number of tasks.

isSubtreeDirty - before

Much of the garbage seems to be generated from isSubtreeDirty, which is used to traverse, in this instance, a very large widget tree.

  bool _isSubtreeDirty(Element element) {
    contentLayersLog.finest("Finding dirty children for: $element");
    if (element.dirty) {
      contentLayersLog.finest("Found a dirty child: $element");
      return true;
    }

    bool isDirty = false;
    element.visitChildren((childElement) {
      isDirty = isDirty || _isSubtreeDirty(childElement);
    });
    return isDirty;
  }

There are two issues here. Interpolating the log string (and serializing the diagnosticable nodes) and the closure allocated for each recursive call. The string interpolation can be put behind a log level check and ideally only do for debug build, since two extra branches can give a lot of overhead and the closure allocation can be avoided by using a static method.

With this PR the garbage collection pretty much disappears from the profile:

Screenshot 2024-04-23 at 16 18 46

@knopp
Copy link
Contributor Author

knopp commented Apr 23, 2024

cc @matthew-carroll

isDirty = isDirty || _isSubtreeDirty(childElement);
});
return isDirty;
bool _isSubtreeDirty(Element element) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Please order method declarations in the order that they're used. In this case _isSubtreeDirty() before _isSubtreeDirtyVisitor().

PS - Typically we place all static members above all instance members. However, in this very unusual situation, we'll treat the static property and method as instance members because we're working around a language issue.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Where should the static variable go?

}
element.visitChildren(_isSubtreeDirtyVisitor);
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm surprised the compiler wasn't already in-lining this.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's an indirect call to a closure or function pointer passed to a virtual method. I don't think it's really possible to inline this. The call overhead is much less of a problem than the need to allocate closure that references local variable (which on top of it is modified from closure so itself needs to be boxed).

// This is intentionally static to prevent closure allocation during
// the traversal of the element tree.
static void _isSubtreeDirtyVisitor(Element element) {
// Can't use the () => message syntax because it allocates a closure.
Copy link
Contributor

Choose a reason for hiding this comment

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

If () => allocates a closure, and that's harmful to performance, then why did you use that syntax up above?

Copy link
Contributor Author

@knopp knopp Apr 24, 2024

Choose a reason for hiding this comment

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

Because there's several orders of magnitude less overlay / underlay layers than the widgets in document. In this case I think we have over 100000 elements in document all being traversed. If you want me to I can do the same thing above (for consistency).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Btw. I introduced the () => closure to prevent diagnosticable tree creates in debug mode during every call (closures in log are only evaluated when log lovel is reached), which is quite slow especially in debug mode.

@matthew-carroll
Copy link
Contributor

@knopp - Can you check the failing analysis? I'm guessing it's some new lint check that's only on master? I'm not sure.

@knopp
Copy link
Contributor Author

knopp commented Apr 24, 2024

@knopp - Can you check the failing analysis? I'm guessing it's some new lint check that's only on master? I'm not sure.

Will do.

@knopp
Copy link
Contributor Author

knopp commented Apr 24, 2024

@matthew-carroll, I moved the visitor method below and the PR to fix the analyser complaining is here.

@knopp
Copy link
Contributor Author

knopp commented Apr 29, 2024

Friendly ping, @matthew-carroll, does this need anything else?

Copy link
Contributor

@matthew-carroll matthew-carroll left a comment

Choose a reason for hiding this comment

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

LGTM

@matthew-carroll matthew-carroll merged commit bb92266 into superlistapp:main Apr 29, 2024
11 checks passed
@matthew-carroll
Copy link
Contributor

@angelosilvestre can you cherry pick this?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants