-
Notifications
You must be signed in to change notification settings - Fork 29.7k
Introduce caching mechanism during compile semantics tree #150394
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 looks like this pull request may not have tests. Please make sure to add tests before merging. If you need an exemption to this rule, contact "@test-exemption-reviewer" in the #hackers channel in Chat (don't just cc them here, they won't see it! Use Discord!). If you are not sure if you need tests, consider this rule of thumb: the purpose of a test is to make sure someone doesn't accidentally revert the fix. Ask yourself, is there anything in your PR that you feel it is important we not accidentally revert back to how it was before your fix? Reviewers: Read the Tree Hygiene page and make sure this patch meets those guidelines before LGTMing. |
7da6e2b to
7936617
Compare
OverlayPortal attaches its overlaychild's renderobject to overlay directly while keeps its semantics tree under overlayportal. This become a problem when the `overlaychild` markNeedsSemanticsUpdate that it propagate upward the rendering tree. This means instead of marking dirty upward to the OverlayPortal, it directly mark Overlay dirty instead and skip `OverlayPortal`. Currently this does not pose an issue other than unnecessary rebuilds, but it become a problem when I try to optimize the semantics tree compilation #150394. After the optimization it won't rebuild semantics node unless it is marked dirty. Since the OverlayPortal widget does not get marked dirty, it won't update the subtree.
b606224 to
fe69ce8
Compare
|
latest after |
|
some thoughts after working on reducing semantics compiling time. There are so many corner cases that ain't previously present since we used to recompile entire subtree, and each of them is very hard to debug because the code is too convoluted. There are somethings I noticed that are very hard to make changes to this part of the code.
for 1, The caches should probably have a more unify API for updating child fragments, and we can also think about changing how implicit merging work. This will be a big breaking change though. For 2, I think in the future we should probably do something to keep a single source of true and avoid mutating SemanticsConfiguration. Though this ties to 1 where some modification to SemanticsConfiguration is due to implicit merge. Fixing all these problem and adding caches seems too dramatic for a single change. so I decided to leave these problem for a later time. |
…#151688) OverlayPortal attaches its overlaychild's renderobject to overlay directly while keeps its semantics tree under overlayportal. This become a problem when the `overlaychild` markNeedsSemanticsUpdate that it propagate upward the rendering tree. This means instead of marking dirty upward to the OverlayPortal, it directly mark Overlay dirty instead and skip `OverlayPortal`. Currently this does not pose an issue other than unnecessary rebuilds, but it become a problem when I try to optimize the semantics tree compilation flutter#150394. After the optimization it won't rebuild semantics node unless it is marked dirty. Since the OverlayPortal widget does not get marked dirty, it won't update the subtree.
54b6333 to
bf0a7d8
Compare
791e2a7 to
93fa36e
Compare
b237acf to
d3fb5c1
Compare
6e2e0dc to
08a63d6
Compare
|
This pull request executed golden file tests, but it has not been updated in a while (20+ days). Test results from Gold expire after as many days, so this pull request will need to be updated with a fresh commit in order to get results from Gold. For more guidance, visit Writing a golden file test for Reviewers: Read the Tree Hygiene page and make sure this patch meets those guidelines before LGTMing. |
accdc52 to
b29f972
Compare
mdebbar
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.
I'm still going through the PR. Here's my first set of comments.
| // first, it may use dirty geometry in parent's semantics node to | ||
| // calculate the geometries in the subtree. | ||
| final List<RenderObject> nodesToProcess = _nodesNeedingSemantics | ||
| .where((RenderObject object) => !object._needsLayout && object.owner == this) |
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.
From the dartdocs above, the flushSemantics method is called after painting. Is it possible for a render object to need layout right after painting?
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 not possible, the flushpaint will ensure the layout is not dirty at the end
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.
Follow up question: why are we filtering out objects that need layout?
Isn't this the same as:
| .where((RenderObject object) => !object._needsLayout && object.owner == this) | |
| .where((RenderObject object) => object.owner == this) |
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.
IIrc widget that are offstage won't layout during the layout stage, those renderobject may still end up here if they markNeedsSemanticsUpdate. We skip those since they won't participate in semantics tree anyway.
| }); | ||
| } | ||
|
|
||
| final bool effectiveBlocksUserAction = newParentData.blocksUserActions || configProvider.original.isBlockingUserActions; |
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.
Related to the original vs effective comment:
Could you clarify why we are getting the effective "block user action" from original but then comparing it with the one in effective?
My understanding so far is that effective is either identical to original OR a writable copy of original. And by doing setProperty below, you are basically making sure that effective is a writable copy of original and then you update it.
If my understanding is correct, then I think it makes more sense to do:
| final bool effectiveBlocksUserAction = newParentData.blocksUserActions || configProvider.original.isBlockingUserActions; | |
| final bool effectiveBlocksUserAction = newParentData.blocksUserActions || configProvider.effective.isBlockingUserActions; |
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.
the effective may contain the stale data from previous update.
for example
frame1: parent blocks user actions = true, this configProvider.effective.isBlockingUserActions will be set to true
frame 2: parent blocks user actions updates to false, the configProvider.effective.isBlockingUserActions suppose to be updated to false, but effective still contains stale data so we have to check the original config instead
This is one of the reasons why I have to keep an original and effective config
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.
Thanks for clarifying.
Based on your description of effective and my understanding of this PR, I suggest the following changes to make it slightly more readable (IMHO):
SemanticsConfiguration get original {
if (_originalCache == null) {
// No need to set two variables here. Just one is enough.
_originalCache = SemanticsConfiguration();
// .....
}
return _originalCache;
}
SemanticsConfiguration? _originalCache;
// This is only used in this file, no need to make it public.
//
// In fact, since this is a mutable copy of the config, I strongly prefer
// that it remains private.
SemanticsConfiguration? _mutableConfig;
void updateConfig(callback) {
if (_mutableConfig == null) {
_mutableConfig = original.copy();
}
callback(_mutableConfig);
}This makes it a bit clearer from my perspective, but I'm also curious what the other reviewers think about this?
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.
Also, given that we are using .original and .effective in different parts of the code, it would be great if you could write a little comment explaining when one should be used vs the other.
For example, in ensureSemanticsNode() and some getters above, we are using .effective.isSemanticBoundary etc. Why not .original.isSemanticBoundary?
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.
The entire class is private so I dont think it really matter, but I try to maintain certain code style that property used outside of this class is public while internal property stay private
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.
also I think mutable doesn't convey the meaning well, it state the what happen to it but is not helpful that when it should be used.
23139cc to
b1c8fe2
Compare
|
This pr is ready for review |
| // first, it may use dirty geometry in parent's semantics node to | ||
| // calculate the geometries in the subtree. | ||
| final List<RenderObject> nodesToProcess = _nodesNeedingSemantics | ||
| .where((RenderObject object) => !object._needsLayout && object.owner == this) |
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.
Follow up question: why are we filtering out objects that need layout?
Isn't this the same as:
| .where((RenderObject object) => !object._needsLayout && object.owner == this) | |
| .where((RenderObject object) => object.owner == this) |
| }); | ||
| } | ||
|
|
||
| final bool effectiveBlocksUserAction = newParentData.blocksUserActions || configProvider.original.isBlockingUserActions; |
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.
Thanks for clarifying.
Based on your description of effective and my understanding of this PR, I suggest the following changes to make it slightly more readable (IMHO):
SemanticsConfiguration get original {
if (_originalCache == null) {
// No need to set two variables here. Just one is enough.
_originalCache = SemanticsConfiguration();
// .....
}
return _originalCache;
}
SemanticsConfiguration? _originalCache;
// This is only used in this file, no need to make it public.
//
// In fact, since this is a mutable copy of the config, I strongly prefer
// that it remains private.
SemanticsConfiguration? _mutableConfig;
void updateConfig(callback) {
if (_mutableConfig == null) {
_mutableConfig = original.copy();
}
callback(_mutableConfig);
}This makes it a bit clearer from my perspective, but I'm also curious what the other reviewers think about this?
| }); | ||
| } | ||
|
|
||
| final bool effectiveBlocksUserAction = newParentData.blocksUserActions || configProvider.original.isBlockingUserActions; |
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.
Also, given that we are using .original and .effective in different parts of the code, it would be great if you could write a little comment explaining when one should be used vs the other.
For example, in ensureSemanticsNode() and some getters above, we are using .effective.isSemanticBoundary etc. Why not .original.isSemanticBoundary?
| ); | ||
| } | ||
| } | ||
| mergeUp.addAll(children); |
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 feels backwards (and probably extra work) to add items to the mergeUp list and later do:
mergeUp.clear();
mergeUp.add(this);Why not something like this:
// mergeUp.addAll(children); // <-- remove this line
if (contributesToSemanticsTree) {
// ...
_marksExplicitInMergeGroup(children, isMergeUp: true);
// ...
mergeUp.add(this);
} else {
mergeUp.addAll(children);
}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.
mergeUp is used in the if case to get the mergeUpConfigs.
b63c4e0 to
bb89af2
Compare
b172af0 to
5e9e921
Compare
|
closing for #161195 |
…d formatted version (#161195) <!-- Thanks for filing a pull request! Reviewers are typically assigned within a week of filing a request. To learn more about code review, see our documentation on Tree Hygiene: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md --> old pr: #150394 ## Pre-launch Checklist - [ ] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [ ] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [ ] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [ ] I signed the [CLA]. - [ ] I listed at least one issue that this PR fixes in the description above. - [ ] I updated/added relevant documentation (doc comments with `///`). - [ ] I added new tests to check the change I am making, or this PR is [test-exempt]. - [ ] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [ ] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
…d formatted version (flutter#161195) <!-- Thanks for filing a pull request! Reviewers are typically assigned within a week of filing a request. To learn more about code review, see our documentation on Tree Hygiene: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md --> old pr: flutter#150394 ## Pre-launch Checklist - [ ] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [ ] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [ ] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [ ] I signed the [CLA]. - [ ] I listed at least one issue that this PR fixes in the description above. - [ ] I updated/added relevant documentation (doc comments with `///`). - [ ] I added new tests to check the change I am making, or this PR is [test-exempt]. - [ ] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [ ] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
toward #150234
There are two expansive part when updating a semantics subtree.
This pr introduced caching for both steps.
First, this pr make a _RenderObjectSemantics to replace _SemanticsFragment. It will be permanently cached on each RenderObject. It handles any semantics related compilation and move all the properties from RenderObject to this class. RenderObject is only responsible for provide semantics property through describeSemanticsConfiguration and assembleSemanticsNode.
to compile the semantics sub-tree,
The performance boost comes from caching from all steps. When the markNeedsSemanticsUpdate is called. it will only clear minimum caches from the
_RenderObjectSemanticsso only a small portion will be recalculated.before


after
getsemantics + compileChildren
530 + 470 ~= 900 before
34 + 187 ~= 200 after
about 78% reduce in compile time
Pre-launch Checklist
///).If you need help, consider asking for advice on the #hackers-new channel on Discord.