-
Notifications
You must be signed in to change notification settings - Fork 29.8k
Feat: Add unused dependency cleanup #170104
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
base: master
Are you sure you want to change the base?
Conversation
|
Hi @rkishan516 do you still have plans for this change? |
|
This has come back around to stale PR triage. We don't review draft PRs, but have you reached out on Discord for the feedback you were seeking? |
Hey, last time we couldn't conclude, I will continue that discussion again today. |
3a852ae to
12e3249
Compare
justinmc
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.
@rkishan516 I really appreciate you opening this PR and getting a discussion going. Flutter needs more bold ideas like this.
It looks like there are two failing customer tests in the provider package, and they're kind of concerning. I wouldn't expect this change to cause them to fail, but maybe it's because I don't understand the inner workings of provider. Do you know what's causing the failure?
I think I agree with @rrousselGit in the original issue (#106549), who said that this should probably be opt-in. I really can't think of any reasonable situation where you would logically need the original behavior, can you? But I worry due to the (slight) performance effect.
Overall I agree that Flutter should have this feature, probably behind an opt-in flag.
| }()); | ||
| } | ||
| } | ||
| context.cleanupRemovedDependencies(); |
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 wondering about the performance implications of this. I believe this method will end up being O(n), where n is the number of elements in _dependencies.
- _dependencies!.difference is called, which I would guess is O(n). The docs don't say but Gemini agrees with me.
- Iterating the removed dependencies will just be another
noperations in the worst case. - Set.remove and Map.remove should be O(1).
- Set.clear should be O(1).
So my guess is that this is not a performance concern, because the number of dependencies is very unlikely to be large.
The test is incorrect as per Providers own documentation: https://pub.dev/documentation/provider/latest/provider/ChangeNotifierProvider-class.html#:~:text=DON%27T%20reuse%20an%20existing%20ChangeNotifier%20using%20the%20default%20constructor
Maybe tests could be run if this actually affects performance? Because I can also come up with some pathological cases where this should boost performance. IMO it would be too bad to have the desired / correct behaviour only available behind a flag / different API & having to wait for the entire ecosystem to migrate. |
|
Thanks @justinmc! I've investigated the failing provider tests and found the bug in my implementation. The issue was in where For the fix, I moved On the opt in choice, I am not really sure. I can see some advantage of opt in, so for now I am also leaning towards it. |
9d98a94 to
d0374cc
Compare
justinmc
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.
It looks like customer tests are still failing, but now it's because a class extends InheritedWidget and has its own dependOnInheritedElement implementation without the new parameter: https://github.com/rrousselGit/provider/blob/558bdcd26ddb2b922bec06e3fa0d91bc59479baf/packages/provider/lib/src/inherited_provider.dart#L606-L609
So now I think we'd have to create a new method(s) with the new functionality?
| /// If [trackForCleanup] is true, this dependency will be tracked and | ||
| /// automatically removed when the widget rebuilds and no longer depends on it. | ||
| /// This helps prevent unnecessary rebuilds but requires careful consideration | ||
| /// as it changes the default behavior. Defaults to false for backward compatibility. |
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 doc should not mention previous behavior. That will be confusing for 1st time reader.
Just state the behavior and the current default
| InheritedWidget dependOnInheritedElement( | ||
| InheritedElement ancestor, { | ||
| Object? aspect, | ||
| bool trackForCleanup = 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.
This API may introduce some bug that let's say there are two dependencies [A, B]
where B is called with trackForCleanup to false, and A is called with trackForCleanup to true.
B will end up get removed.
It feels like this API relies on all of the dependencies are called with trackForCleanup = true or all called with trackForCleanup = false.
Should we rewrite the mechanism to be per inherited element setting?
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.
Yaa, I was thinking about this. And rather than making both dependencies and currentBuildDependencies to HashMap. But I think having a setting per inherited element will be better idea.
0963a57 to
bf7806b
Compare
|
Since the current behavior (never unsubscribe) is intentional, I assume we'll never make this behavior the default. In this case, do you think it'll be easier to have another method to call than to configure this when defining the entire class? This allows the app developers to decide whether to adopt it, so that this feature can be applied to existing inherited models as well. I'm imagining a |
|
The very reason this feature is requested is that developers dont have a good place to unsubscribe. Folks can already override InheritedElement.removeDependent. (the alternative of providing Builder-like widgets has the same compatibility issue) |
These tests fail when Flutter automatically cleans up unused `InheritedWidget` dependencies as noted by @justinmc in flutter/flutter#170104. As per https://pub.dev/documentation/provider/latest/provider/ChangeNotifierProvider-class.html#:~:text=DON%27T%20reuse%20an%20existing%20ChangeNotifier%20using%20the%20default%20constructor already existing `ChangeNotifier` instances should not be passed into the `create` method of a `ChangeNotifierProvider` Widget (because it would then tie it to its own lifecycle). Instead instances can be passed through `ChangeNotifierProvider.value` and then disposable can be manually handled by the caller (in case of these tests via `addTearDown`).
|
The implementation most looks good to me, just left some suggestion to see if we can squeeze out as much performance as possible |
ce596e8 to
a981fc2
Compare
justinmc
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.
@rkishan516 Did you try this out on a low end device? I'm interested to see the performance impact.
As long as we're ok with any performance changes I'm on board with this approach.
| (d.widget as InheritedWidget).cleanupUnusedDependents && | ||
| !_currentBuildDependencies!.contains(d), | ||
| ) | ||
| .toList(); |
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 might be able to gain a bit of performance by omitting this toList. I think you can still loop over the Iterable as expected.
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.
Or another performance idea: Just loop over _dependencies once instead of using where.
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 initial implementation was with loop, but after suggestion we moved to this version.
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 agree with Justin. Widget rebuilds is a very hot path, I'd prefer performance over succinctness here.
where(...) will allocate a WhereIterable, and toList() will allocate a List and then add elements. This does more work and adds GC pressure. Let's avoid that if we can.
I have tried on iPhone 13 but i don't thats the low end, we are looking for. I will search for some old android phone and check. |
| } | ||
|
|
||
| return const SizedBox(); | ||
| } |
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.
Could we add a variant of this test where dependencies are used in the didChangeDependencies method instead of the build 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.
I believe this is currently broken, see: #170104 (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.
Yup, if dependency is established in didChangeDependency, that doesn't exist in build, get removed in build.
| } | ||
|
|
||
| return const SizedBox(); | ||
| } |
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.
Could we add a variant of this test where dependencies are used in a layout callback like LayoutBuilder.builder?
I believe the current implementation won't work in LayoutBuilder. The layout callback is called during the layout phase, which is after this build method has returned.
The inherited widget cleanup might need to happen in a post frame callback to correctly handle this case.
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're right. I have added tests for these. Which are marked as skip because they are failing.
| @override | ||
| @pragma('vm:notify-debugger-on-exception') | ||
| void performRebuild() { | ||
| _currentBuildDependencies?.clear(); |
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.
Does this line drop dependencies registered by didChangeDependencies?
Notice how StatefulElement.performRebuild calls didChangeDependencies before calling ComponentElement.performRebuild:
flutter/packages/flutter/lib/src/widgets/framework.dart
Lines 5976 to 5983 in 320c2eb
| @override | |
| void performRebuild() { | |
| if (_didChangeDependencies) { | |
| state.didChangeDependencies(); | |
| _didChangeDependencies = false; | |
| } | |
| super.performRebuild(); | |
| } |
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.
Yes, it does drop dependcies registered by didChangeDependencies.
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.
oh nice catch, yeah that is not something we should do. Can we do this in rebuild method instead? also, why do we do the clean up in ComponentElement given than the _dependencies are defined in 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.
I have moved _currentBuildDependencies?.clear(); to rebuild and _currentBuildDependencies to 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 I partially remember, _currentBuildDependencies was in Element and then we moved to ComponentElement for some reason but I couldn't find it.
|
Hello, thank you for the contribution and all the excellent work. While I am supportive of allowing widgets to clean up unused dependencies, I'm concerned with the current design and I don't think we should move forward with this implementation as-is:
I see that @goderbauer had expressed similar reservations on a previous pull request: #107112 (comment), #107112 (comment) I apologize for bringing this feedback late, months after you started this pull request. Thank you again for all the excellent work! |
a981fc2 to
01406c2
Compare
Hey @loic-sharma, thanks for your feedback. It's not require to land this PR in current state or current way, our main goal is to have cleanup for unused dependency. So, if you have a better idea, i can work on that. Current implementation is just result of initial implementation + feedback based iteration. For your concerns
|
01406c2 to
62eaa08
Compare
|
This is a very hard problem. What do you think of this idea instead? #106549 (comment)
This an easier path that would make this problem less bad. We'd still need the team to buy-off on the approach though. |
Agree. People just ignore issue like #163467 but if we solve this, it will give clarity.
I think we already have this with
Yup. |
62eaa08 to
5c1a25d
Compare
Feat: Add unused dependency cleanup
fixes: #106549
Before this PR
If any widget add dependency to other InheritedWidget, it never gets removed until widget is out of tree. e.g.
Once the button is tapped, any change in theme is applied to
ButtonDependsOnThemewidget.After this PR
If any widget add dependency to other InheritedWidget and in next rebuild it doesn't keep it as in example, it will be removed from dependency.
Pre-launch Checklist
///).