Open
Description
Running this program:
$ cat memoryleak.dart
main() async {
var result;
for(int i = 0; i < 100; i++) {
result = await calc(result);
}
print("done");
}
calc(var oldResult) async {
var newResult = calcInternal(oldResult);
foo() {
print("hello world");
}
newResult.foo = foo;
return newResult;
}
calcInternal(var oldResult) {
return new Foo(new List<int>(10000000));
}
class Foo {
var foo;
final List<int> data;
Foo(this.data);
}
gives this result when run via /usr/bin/time:
$ /usr/bin/time -v out/ReleaseX64/dart memoryleak.dart
done
Command being timed: "out/ReleaseX64/dart memoryleak.dart"
User time (seconds): 10.01
System time (seconds): 2.04
Percent of CPU this job got: 167%
Elapsed (wall clock) time (h:mm:ss or m:ss): 0:07.19
Average shared text size (kbytes): 0
Average unshared data size (kbytes): 0
Average stack size (kbytes): 0
Average total size (kbytes): 0
Maximum resident set size (kbytes): 7908452
Average resident set size (kbytes): 0
Major (requiring I/O) page faults: 0
Minor (reclaiming a frame) page faults: 1573005
Voluntary context switches: 2635
Involuntary context switches: 42
Swaps: 0
File system inputs: 0
File system outputs: 0
Socket messages sent: 0
Socket messages received: 0
Signals delivered: 0
Page size (bytes): 4096
Exit status: 0
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:
Retaining path { ⊟
Foo { ⊞ }
retained by offset 3 of Context (7) { ⊞ }
retained by var _context of Closure (foo) { ⊞ }
retained by var foo of Foo { ⊞ }
retained by offset 3 of Context (7) { ⊞ }
retained by var _context of Closure (foo) { ⊞ }
retained by var foo of Foo { ⊞ }
retained by offset 3 of Context (7) { ⊞ }
retained by var _context of Closure (foo) { ⊞ }
retained by var foo of Foo { ⊞ }
retained by offset 3 of Context (7) { ⊞ }
retained by var _context of Closure (foo) { ⊞ }
retained by var foo of Foo { ⊞ }
retained by offset 3 of Context (7) { ⊞ }
retained by var _context of Closure (foo) { ⊞ }
retained by var foo of Foo { ⊞ }
retained by offset 3 of Context (7) { ⊞ }
retained by var _context of Closure (foo) { ⊞ }
retained by var foo of Foo { ⊞ }
retained by offset 3 of Context (7) { ⊞ }
retained by var _context of Closure (foo) { ⊞ }
retained by var foo of Foo { ⊞ }
retained by offset 3 of Context (7) { ⊞ }
retained by var _context of Closure (foo) { ⊞ }
retained by var foo of Foo { ⊞ }
retained by a GC root
so it seems to me that the current Foo
(which should be alive --- it's in variable result
) has the closure foo
in it (so far so good), which then has a context which has the previous Foo
in it and on and on.
Also notice that
- if I don't have the
newResult.foo = foo;
line the problem goes away. - if I remove the async/await stuff the problem goes away.