Search code examples
asynchronousdarttimeoutwait

Why does Future.timeout not work in dart?


I want to run a computation that might take too long. If it doesn't finish in a specific time, abort the computation and return a different value. For this problem I found Future.timeout, it would almost do the same thing that I want, except it doesn't work for this code.

Future<String> timeoutTest() async
{
  return await longComputation().timeout(
    Duration(milliseconds: 10),
    onTimeout: () => "Took too long"
  );
}

Future<String> longComputation() async
{
  int startTime = DateTime.now().millisecondsSinceEpoch;

  Rational n = Rational.one;
  for(int i = 1; i < 2000; i++)
  {
    n *= Rational.fromInt(i);
  }
  String result = n.toDecimalString();

  print("Time took: ${DateTime.now().millisecondsSinceEpoch - startTime} ms");
  return result;
}

When I call print(await timeoutTest()) I either expect a string of digits that took maximum 10ms to calculate OR the "Took too long" string if it took more than 10ms. But I get the string of digits, and the message in the console: "Time took: 877 ms". So the timeout didn't work.

If I fake the computation with Future.delay, the timeout works as expected. It returns a different value, because the longComputation took at least 100ms. (I still get the message in the console: "Time took: 103ms", but this is not the main problem.)

Future<String> longComputation() async
{
  int startTime = DateTime.now().millisecondsSinceEpoch;

  String result = await Future.delayed(
    Duration(milliseconds: 100),
    () => "Fake computation result"
  );

  print("Time took: ${DateTime.now().millisecondsSinceEpoch - startTime} ms");
  return result;
}

I'm assuming I messed up something in the longComputation, but what? There were no un-awaited Futures.


Solution

  • This behavior can be confusing but it is important to remember that your Dart code are only executed in a single thread (unless you are using isolates).

    The problem is that the logic behind .timeout needs to run in the same single thread and Dart can't just stop the execution of your own code. So if you are running a CPU intensive calculation without any pauses you are stopping the Dart VM from running any other events on the event queue.

    What the implementation of .timeout actually does, is creating an internal Timer which are going to be triggered in the future unless you get a result before the timeout value. This Timer event are going on top on the event queue like any other event in the Dart VM.

    But in your first example, we are actually never going to execute any other event on the event queue before you are giving the result. So from the Future's point of view, you are returning a result before the deadline.

    This is going to look like .timeout is kind of pointless but what it really are for is when you are making some IO operations like API requests where the Dart VM are actually waiting for some answer.

    If you are going to use it for heavy calculations, you can either spawn an Isolate so your main isolate instance can wait on the other isolate. Alternative, you can insert some pauses in your calculation which makes space for the Dart VM to execute other events on the event queue. An example could be inserting await Future<void>(() => null); which are going to spawn a new event on top of the event queue. When we wait for all events on the queue to be executed before our own empty calculation.

    It would then also make sense to add some logic so your own code can see if the timeout value has been reached so you can stop the calculation if that is the case.