Dart future blocking main thread
Asked Answered
E

2

6

I'm working on an app that captures and processes an image. A simplified version of the code is:

build() {
   return FloatingActionButton(
        onPressed: processImage,
        child: Icon(
          Icons.camera_alt,
          color: color,
        ),
      ); 
}

processImage(Camera image) async {
   await image.process();   
}

And in another class:

Future<image> process() {
  return Future(() {
    for (int x = 0; x < width; x++) {
      for (int y = 0; y < height; y++) {
        //process image
      }
    }
  });
}

But when process() is running, the UI freezes.

Why is this happening? Isn't that function passed to Future constructor running in the background?

Epergne answered 25/10, 2020 at 3:17 Comment(10)
I'm not 100% sure, but I do know dart uses an event loop and everything will run on the main thread (just perhaps at some point in the future, in the case of asynchronous code). As such, that code will eventually be executed on the main thread, causing a block. Consider using Dart's isolates; that should fix the issue. Flutter has a built-in convenience method to do this for you, but I can't remember what it is called off the top of my head.Erena
I believe I found it: api.flutter.dev/flutter/foundation/compute.htmlErena
@GregoryConrad Thank you, that worked for me, is not a very elegant solution, but it works, if you want to make it an answer, I'll be happy to make it as the right answerEpergne
what is not "elegant" in compute() function?Apiarist
@Apiarist My function receives 6 parameters, and you cannot call a class method: "The callback argument must be a top-level function, not a closure or an instance or static method of a class", and you can only pass one parameter, so I had to convert all my params to a Map<String, dynamic>, create a top-level function that receives the map, and destructure to original variables.Epergne
this is how it works: the only solution for a background processing is compute() function or at lower level Isolate stuffApiarist
Yes, I now know that is how it works, compute receives the top level function handler and one extra parameter, so I had to make my code work with these constraints, as I had a class method with several parameters, so I had to refactor it to a top level function with only one Map as parameterEpergne
You can make your method take an instance of the class it is currently in as an argument. I can detail this more in my answer when I get around to creating it. This will make it cleaner than using a map.Erena
Oh I didn't know it is possible to pass objects, I read here that only primitives can be passed, found that link in compute() documentation: > The content of message can be: primitive values (null, num, bool, double, String), instances of SendPort, and lists and maps whose elements are any of these. List and maps are also allowed to be cyclic.Epergne
Whoops! I neglected to think about what types you can pass; you are correct.Erena
E
8

As Dart uses an event loop, all code (synchronous and asynchronous) will simply be run on the same isolate (think thread in other languages as an analogy), just at different points in time. As such, when your process method is dequeued and executed, it will block the thread and cause frames to be dropped due to a longer execution time, despite being asynchronous. The optimal solution to the problem is to spawn another isolate in a new thread, and carry out the computation there. Flutter provides a convenience method for this exact use case, called compute. It takes a top-level function (not in a class, nor anonymous) that can have a primitive type parameter (including Map and List) as an argument and will return at some point in the future. For more information on compute, see its documentation linked above.

If you have multiple parameters that you need to pass to compute, a common pattern (outside of just this use case) is making a method that serializes a class' fields to a Map<String, dynamic>, and a factory constructor that creates an object from a Map<String, dynamic>. This process would be easier with reflection, but Flutter disables it due to performance reasons.

For a full example on compute from the Flutter documentation, see here: https://flutter.dev/docs/cookbook/networking/background-parsing

Erena answered 25/10, 2020 at 19:35 Comment(0)
D
-1

You can insert the gap into the event loop.
Simple way:

Future<image> process2() {
  return Future(() async {
    for (var x = 0; x < width; x++) {
      for (var y = 0; y < height; y++) {        
        // process       
      }

      if (x % 100 == 0) {
        await Future.delayed(Duration(seconds: 100));
      }
    }
  });
}

Deliverance answered 25/10, 2020 at 5:49 Comment(2)
Wrong answer, this will not help. UI still freezes.Osseous
I found this article talking about using event loop to handle heavy tasks. I don't know if the same method can be used for this case (processing image). https://hackernoon.com/executing-heavy-tasks-without-blocking-the-main-thread-on-flutter-6mx31lhBridie

© 2022 - 2024 — McMap. All rights reserved.