Category: Performance

  • Object pooling in dart

    Object pooling in dart improves the memory performance of your Flutter app or game.

    When a large number of objects (such as bullets or enemies) are created over time in a Flutter game, dart has to do a lot of work allocating and then garbage collecting that memory.

    The garbage collector (GC) does not run continuously, so memory usage can increase dramatically between sweeps, resulting in high spikes of memory usage. Read more about dart’s garbage collection.

    Object pooling can help prevent fragmentation and reduce those peaks by re-using objects from a pool instead of creating and destroying them every time. This is especially useful on lower-memory devices such as smart watches or cheaper smart phones.

    Implementation of object pooling

    Here is a simple implementation of object pooling in dart. At it’s core, the pool is simply a list of the same type of object.

    // An object that is pooled should mix the Pooled mixin
    mixin Pooled {
      // Implement this to reset the object ready for re-use
      void reset();
    }
    
    // Object pool implementation
    class Pool<T extends Pooled> {
      // A list that holds the pooled objects
      final List<T> _pool = <T>[];
      // The method called to create a new pooled object
      final T Function() _creator;
    
      // Constructor
      Pool(this._creator);
    
      // Get an object from the pool, or create one if pool is empty
      T get() {
        if (_pool.isEmpty) {
          return _creator();
        } else {
          T obj = _pool.removeLast();
          obj.reset();
          return obj;
        }
      }
    
      // Add an object (back) to the pool
      void add(T obj) {
        _pool.add(obj);
      }
    
      // Clear the pool and release all the pooled objects for GC 
      void clear() {
        _pool.clear();
      }
    }
    Dart

    To use it, create a new pool for every different object type that you want to pool. Bullets are a good choice as they are often created and destroyed in large numbers.

    // Mix the Pooled mixin with the class that should be pooled
    class Bullet with Pooled {
      // Create a static pool, passing in the constructor of the object
      static Pool<Bullet> pool = Pool<Bullet>(Bullet.new);
      
      // These are some example properties on Bullet
      double damage = 1.0;
      double speed = 20.0;
      
      // Set the Bullet back to default values when re-used
      @override
      void reset() {
        damage = 1.0;
        speed = 20.0;
      }
    }
    Dart

    Then, whenever you need a new bullet get it from pool. Also remember to add it back to the pool once it is no longer needed.

    // Instead of this: Bullet b = Bullet();
    Bullet b = Bullet.pool.get();
    
    // Once the bullet is no longer required
    Bullet.pool.add(b);
    
    // When the game is over, free any pooled objects
    Bullet.pool.clear();
    Dart

    Here is the result – these are screengrabs of the memory profiler without and with object pooling enabled.

    Without object pooling, as many as 1000 bullets can remain in memory before the garbage collector sweep removed them.

    Memory usage without object pooling
    Memory usage without object pooling

    With object pooling enabled there are a maximum of 20 bullets in memory. Instead of creating new objects, recycled bullets are re-used from the pool.

    Memory usage with object pooling
    Memory usage with object pooling

  • FPS and battery life

    FPS and battery life

    , , ,

    I’ve been a fan of Flutter and Dart for some time. Recently I purchased a Wear OS smart watch and I’m interested in how well a Flutter game will perform on it.

    My watch is a Mobvoi Ticwatch Pro 5. It’s a beast, with 2GHz of Ram and a low-power 1.7GHz quad-core GPU. The display is a gorgeous 1.43″ 466*466 AMOLED screen. You can read the full specs here.

    My background is in game development for mobile – starting back in the day before iOS and Android existed – when devices had huge limitations in terms of CPU, ram, storage, network, screen size – you name it! I love working within limitations like these. They represent a special challenge for game design and development!

    I’m working on a prototype bullet-storm shooter with lots of bullets and particle effects to really try and see what the limitations of Flutter running on Wear OS are. I’m using a modified version of the Flame engine.

    Limiting the frame rate in Flame

    Flame works by calling an update function as often as possible. On desktop this tends to be around 120fps. On the Ticwatch it’s 60fps. To test the effect of different frame rates I modified the update function to limit the frame rate. Something like this:

    double _renderPeriod = 0.0;
    double _elapsedTime = 0.0;
    
    /// Set the maximum framerate for the game
    set maxFPS(double? fps) {
      if (fps == null) {
        _renderPeriod = 0.0;
        return;
      }
      _renderPeriod = 1.0 / fps;
    }
    
    /// Ensure the framerate doesn't exceed maximum
    @override
    void updateTree(double dt) {
      _elapsedTime += dt;
      if (_elapsedTime >= _renderPeriod) {
        super.updateTree(_elapsedTime);
        _elapsedTime = 0.0;
      }
    }
    Dart

    Testing the effects of different frame rates

    To perform the tests I repeated these steps:

    1. Charge the watch to 100%
    2. Set the framerate
    3. Run the game for exactly 30 minutes
    4. Check the battery level

    Here are the results:

    FPSBattery drain
    6011%
    3011%
    911%
    Effects on battery drain of changing FPS

    Analysis of results

    Changing the frame rate made no difference to the battery drain.

    I was initially surprised, but then I quickly realised that this made sense. My testing is flawed.

    My tests are not changing the fundamental framerate of the app. Under the hood, graphics are still being rendered to the screen at the same rate.

    It’s just my higher level update function that is being limited. It performs physics calculations and moves objects around, but it’s not responsible for the actual rendering. So while the animation is choppy and jumpy, the rendering is still happening at the same rate.

    Here’s an image to illustrate. It shows 10 frames being drawn while the ship is rotating:

    As you can see:

    • In both cases, the underlying code is updating the frames at the same rate
    • But when the fps is being limited (incorrectly), only the animation (rotation) is being limited.

    However, this next image shows what the intended effect of reducing frame rate should be:

    So back to the drawing board in this one.