Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Flutter 'Compute' memory leak - How do I retire heap variables used by compute instance?

Tags:

flutter

dart

I'm trying to use a compute instance in my mobile application to reduce jank when deserializing a bunch of downloaded JSON into objects.

When I implement the deserialization method using compute, the heap keeps both the passed JSON and the returned deserialized objects (in a list) INDEFINITELY. GC is triggering normally but is not removing the objects from the heap, even when the method has closed and the parent/calling objects had been retired. As such, when using the DevTools memory profiler, it shows runaway memory consumption - the heap just keeps getting bigger.

Normal memory profile Normal Memory Profile - memory usage hovers around 45MB when the deserialization method is called directly (but it causes jank in the app)

Runaway memory profile Runaway Memory Profile - memory usage increases linearly and is never retired when the deserialization method is called via compute (but it doesn't cause jank in the app)

static Stream<EventCommitInfoModel> getEventsAfterDate(DateTime date) async* {

    // variable defs for scope reuse

    while (count < maxCount && retryCount > 0) {
      try {
        json = await http.read(url);

        // currentEvents = await compute(EventModel.fromJsonArray, json);
        currentEvents = EventModel.fromJsonArray(json);

        db = await AppStateModel.database;
        await db.upsertEventModels(currentEvents);
        yield new InfoModel(maxCount, currentEvents.length);
      }
      catch (ex) {

        // try again or close

      }
    }

    print("stream is closing.");
  }

In the code above, the relevant lines begin with "currentEvents = ". The normal memory behavior is seen with:

  currentEvents = EventModel.fromJsonArray(json);

and the runaway memory behavior is seen with:

  currentEvents = await compute(EventModel.fromJsonArray, json);

PLEASE NOTE THAT CHANGING EventModel.fromJsonArray to an async method has NO impact on any of the profiling above. Nor does changing it to async cause the jank to go away. I have already considered that. I can add artificial delays into the code in order to introduce async splits in the mapping method, but that's NOT what I want to do here - I need the data to return as fast as possible, which is why using compute is ideal.

Even after "stream is closing" is printed, and the stream is closed, and the parent object is retired from the hierarchy and collected, any memory associated with the compute method is never retired.

How do I get the compute instance to retire memory properly? Is there something I'm doing wrong here?

like image 481
puargs Avatar asked Apr 15 '19 18:04

puargs


People also ask

How do you deal with memory leak in Flutter?

There are two solutions: After the run is complete, disconnect from the computer, and then it is best to restart the app. If the completed test package is installed on the mobile phone, the above problem does not exist, so this method is suitable for use by testers. It can be configured as follows in Android Studio .

What causes memory leaks in Flutter?

Once the object is referenced long-term by other objects due to reasons, such as circular reference, GC will not be able to release it. This will eventually lead to a memory leak.

What is garbage collection in Flutter?

Scheduling. To minimize the effects of garbage collection on app and UI performance, the garbage collector provides hooks to the Flutter engine that alerts it when the engine detects that the app is idle and there's no user interaction.


2 Answers

I see the same issue on flutter 2.2.3

All data I pass as argument to the compute are kept in memory - await compute(exampleGlobalFunction, data).

I checked it in Flutter DevTools / Memory tab and the instances of data are never released. If I call the function directly (without compute) then data are properly released.

EDIT: I think I found the solution - created a github issue https://github.com/flutter/flutter/issues/86470

like image 162
František Gažo Avatar answered Sep 22 '22 15:09

František Gažo


Without more code, what seems to be happening is that the stream you are creating is never having actually closing. So the stream remains open, and thus has hooks into the json objects which GC cannot then remove.

You imply in your problem statement that the stream is getting closed:

Even after "stream is closing" is printed, and the stream is closed, and ...

... But a print statement that the stream is closing doesn't close the stream. Just getting to the end of the code block does not close the stream either (especially for a broadcast stream), at least afaik.

Also per the dart documentation they don't recommend operating on streams directly; https://dart.dev/articles/libraries/creating-streams#final-hints for exactly this reason too. They recommend using a stream controller.

like image 29
M. Duane Avatar answered Sep 22 '22 15:09

M. Duane