-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
Dart VM closures are not safe-for-space (share the same context if defined within the same scope - leading to overcapturing) #36983
Comments
I'm assuming that
Currently, I haven't been able to simplify it past (1) I'm guessing that all closures created within a function share a common context object, and since one of the functions needed Edit: Further evidence:
|
@mkustermann we should probably introduce a liveness analysis which compute which variables should be promoted into the context across yield points. This should alleviate this a bit - because we are over-promoting right now. Additionally it is probably worth taking a look at alternative strategies for implementing closures - so that a closure does not retain any state it does not reference. My concern would be that solutions that adress the memory leak might have negative impact on code size - but we don't really know unless we try. Maybe something that @cskau-g could take a look at? |
Condensed example of a leak in pkg/frontend_server/lib/frontend_server.dart:
run like If one uncomments the line |
This is great! Happy to see this explored.
Given the code size concerns (and given the fact that closure identity is observable in Dart) I wonder whether that would suggest an environment passing approach despite the slightly larger memory usage? I don't know if that makes sense in our existing implementations since I don't know what our closure representations look like, but separating the environment should give you more freedom to share environments (and hence eliminate some of the code required to construct them). |
That's more or less what we use now. We have a variation of what in classical literature is called "linked closures": each closure object has a pointer to a context object and each context object can have a parent context - this is just a straightforward representation of syntactical scopes. Context objects store variables directly. |
I have just discovered something similar in the dart2js source. Q: How do I work around a leak like this? Is nulling-out a local guaranteed to be sufficient or would I need to manually extract the code into a method and force it to never be inlined? What I see: The local variable The state of the _FutureListerner is I think there are two leaks here.
(the references from |
This is causing hard to debug (and understand) memory leaks in flutter applications that use closures for gesture handlers and route builders, see flutter/flutter#79605 for an example. |
Totally agree with @goderbauer. I believe we can It'd be great to have something like Memory Graph in Xcode for Dart/Flutter dev tools. |
…d over As a side-effect of making closures without captured variables have an empty context, we not only avoid a memory leak, but also allow such closures to be sent across [SendPort]s. Issue #36983 Issue #45603 TEST=vm/dart{_2,}/isolates/closures_without_captured_variables_test Change-Id: I5e8845203059ff625f7cff3f072d845b5e48ba36 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/212297 Commit-Queue: Martin Kustermann <kustermann@google.com> Reviewed-by: Slava Egorov <vegorov@google.com>
In https://dart-review.googlesource.com/c/sdk/+/214020 incremental_compiler_leak_test was renamed to incremental_compiler_leak_tester temporarily because it started saying we had possible leaks. Looking into it now, a number of puzzeling this were found: * Many SourceLibraryBuilders, SourceClassBuilders etc was alive despite having been converted to DillLibraryBuilders (etc). They shoudn't be. * Several Librarys (from kernels AST) came and went between invalidations. This all was caused by the VM sometimes keeping things around longer than it should (probably a variation of #36983). This CL fixes it by explicitley nulling things out (and similar). It re-enasbles the test and updates it to explicitley expecting a certain number of instances for some classes (e.g. it now expect 0 SourceLibraryBuilders). To be clear I don't think this has caused any "real" leaks, but: * having things clear up too late is very confusing. * we will probably use less memory now. Change-Id: I3a439f23fc7ef26b156c6164a2c37f6352bc57b4 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/229964 Reviewed-by: Johnni Winther <johnniwinther@google.com> Commit-Queue: Jens Johansen <jensj@google.com>
Another example of this problem was found in combination with |
New async/await implementation which is based on copying stack frames (developed to address #48378) might be able to fix this bug because it doesn't capture local variables in async methods. |
This bug is about over-capturing of state in closures in general - which we should fix. |
The new implementation of async/await landed, so The problem related to multiple closures sharing the same context still remains. |
In my case, the problem with overcapturing is that it blocks communication of state from Fluter's root isolate to a background isolate via a closure since it was erroneously capturing @override
void initState() {
super.initState();
final int index = widget.index;
_getAgent().then((agent) async {
final String? decoded =
await agent.query((state) => state.decodedMessages[index]);
if (mounted) {
setState(() {
_text = decoded;
});
}
});
}
That looks like a novel observation about this bug. I don't believe there is a workaround (short of writing the data to pipe and reading it back =P). More details in #50593 |
One can always work around the issues by restructuring code, would e.g. this work for you: void initState() {
super.initState();
final int index = widget.index;
_getAgent().then((agent) async {
final String? decoded = await _sendQuery(agent, index);
if (mounted) {
setState(() {
_text = decoded;
});
}
});
}
Future _sendQuery(agent, index) => agent.query((state) => state.decodedMessages[index]); ? |
@mkustermann I was planning on suggesting that restructuring in the documentation |
Maybe in a it very cautionary way / as a side-note, because we shouldn't tell our users to re-write code in a more ugly / hard to maintain way if they wouldn't actually benefit from it. (With infinite resources we could even highlight in the IDE directly what state is retaine & we could find a solution for the over-capturing issue in the first place). |
I just bumped into this today. This can lead to some very non-obvious bugs when using isolates and providers. |
Running this program:
gives this result when run via /usr/bin/time:
I'll mark the important part here: Maximum resident set size (kbytes): 7908452 --- i.e. it uses 7GB+ memory.
Via observatory I can see that all 100 Foo's are alive, and picking a semi-arbitrary one of them, and clicking the "Retaining path" thing gets me this:
so it seems to me that the current
Foo
(which should be alive --- it's in variableresult
) has the closurefoo
in it (so far so good), which then has a context which has the previousFoo
in it and on and on.Also notice that
newResult.foo = foo;
line the problem goes away.The text was updated successfully, but these errors were encountered: