Skip to content

Unbounded memory growth in Flutter application - due to missing old-space GC #43770

Closed
@mkustermann

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:
Screen Shot 2020-10-13 at 14 09 35

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Assignees

Labels

P1A high priority bug; for example, a single project is unusable or has many test failuresarea-vmUse area-vm for VM related issues, including code coverage, and the AOT and JIT backends.type-performanceIssue relates to performance or code size

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions