Description
The following flutter application runs an animation. When building each frame we do new memory allocations and make older objects unreachable. The live memory at any given point in time should be <100 MB, though the process will eventually OOM due to unbounded memory growth:
import 'package:flutter/animation.dart';
import 'package:flutter/material.dart';
main() {
final sim = AllocationSimulation();
runApp(LogoApp(sim));
}
class LogoApp extends StatefulWidget {
final AllocationSimulation sim;
LogoApp(this.sim);
LogoAppState createState() => LogoAppState(sim);
}
class LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin {
final AllocationSimulation sim;
Animation<double> animation;
AnimationController controller;
LogoAppState(this.sim);
void initState() {
super.initState();
controller =
AnimationController(duration: const Duration(seconds: 2), vsync: this);
animation = CurvedAnimation(parent: controller, curve: Curves.easeIn)
..addStatusListener((status) {
if (status == AnimationStatus.completed) {
controller.reverse();
} else if (status == AnimationStatus.dismissed) {
controller.forward();
}
});
controller.forward();
}
Widget build(BuildContext context) => AnimatedLogo(sim, animation: animation);
void dispose() {
controller.dispose();
super.dispose();
}
}
class AnimatedLogo extends AnimatedWidget {
static final _sizeTween = Tween<double>(begin: 0, end: 300);
final AllocationSimulation sim;
AnimatedLogo(this.sim, {Key key, Animation<double> animation})
: super(key: key, listenable: animation);
Widget build(BuildContext context) {
// Produce some garbage.
sim.onFrameBuild();
final animation = listenable as Animation<double>;
return Center(
child: Container(
margin: EdgeInsets.symmetric(vertical: 10),
height: _sizeTween.evaluate(animation),
width: _sizeTween.evaluate(animation),
child: FlutterLogo(),
),
);
}
}
class AllocationSimulation {
// Array of this size is around 1 kb.
static const int chunkSize = 128;
// Make enough arrays to exhaust new-space limit.
static const int chunks = 17 * 1024;
// For each frame: Replace this many old arrays with new arrays.
static const int iterateCount = 8 * 1024;
List root, current;
AllocationSimulation() {
root = List(chunkSize);
var last = root;
for (int i = 0; i < chunks; ++i) {
final nc = List(chunkSize);
last[0] = nc;
last = nc;
}
current = root;
}
void onFrameBuild() {
for (int i = 0; i < iterateCount; ++i) {
if (current[0] == null) current = root;
final old = current[0];
final replacement = List(chunkSize)..[0] = old[0];
current[0] = replacement;
current = replacement;
}
}
}
Most likely this happens due to missing start of old space collections during the Dart_NotifyIdle
calls from flutter engine:
Here we can see that every Dart_NotifyIdle
will cause a scavenge that almost exhausts the 16 ms limit - which might be why we fail to start old space collection.
=> We should ensure to have tests for our heuristics - effectively adding regression tests for such bugs.
/cc @rmacnak-google Maybe you could take a look?
/cc @dnfield Since it's related to idle notification. Do you have any suggestions how to add memory tests on flutter side?
Activity