dart-lang / sdk

The Dart SDK, including the VM, JS and Wasm compilers, analysis, core libraries, and more.
https://dart.dev
BSD 3-Clause "New" or "Revised" License
10.04k stars 1.55k forks source link

Different async timing whether using vm, node, dart2js and dart2wasm #55952

Open alextekartik opened 2 months ago

alextekartik commented 2 months ago

Using dart 3.4.2 on Ubuntu 22.04

When using async/await, I noticed some difference in timing when methods are executed.

Consider the following unit test

import 'dart:async';

import 'package:test/test.dart';

void main() {
  test('timing', () async {
    var logs = <String>[];
    void log(String message) {
      // ignore: avoid_print
      print(message);
      logs.add(message);
    }

    unawaited(() async {
      await () async {
        await () async {}();
        log('async1 await1');
      }();
      log('async1 await2');
    }());

    unawaited(() async {
      await () async {
        await () async {}();
        log('async2 await1');
      }();
      log('async2 await2');
    }());

    await Future.delayed(Duration.zero, () {
      expect(logs,
          ['async1 await1', 'async1 await2', 'async2 await1', 'async2 await2']);
    });
  });
}

When using

dart test -p node
dart test -p chrome --compiler dart2js
dart test -p vm

it succeeds and properly displays:

async1 await1
async1 await2
async2 await1
async2 await2

However when using

dart test -p chrome --compiler dart2wasm

it fails and displays

async1 await1
async2 await1
async1 await2
async2 await2
00:08 +2 -1: [Chrome, Dart2Wasm] test/timing_min_test.dart: timing [E]                                                                                             
  Expected: ['async1 await1', 'async1 await2', 'async2 await1', 'async2 await2']
    Actual: ['async1 await1', 'async2 await1', 'async1 await2', 'async2 await2']
     Which: at location [1] is 'async2 await1' instead of 'async1 await2'

I noticed this while doing some indexeddb testing using the new js_interop where timing matters a lot during transaction.

Is the timing difference expected or should it be consistent across platforms?

lrhn commented 2 months ago

It doesn't have to be different across platforms, but it's allowed to.

Most likely all the different execution interleavings are within the specified behavior. The async/await specification is generally specified as "at a later time" or "in a later microtask", without specifying precisely how microtasks are ordered or scheduled.

The async/await implementations differ between platforms, and may do different optimizations. For example, an await asyncFunction() call can validly be implemented by inlining asyncFunction, which will definitely avoid introducing an extra await-delay. Even expecting the same platform to have exactly the same timing between releases, is to expect nothing to be optimized.

Generally, there is no guarantee about the order that completed futures call their callbacks in. It's mostly predictable only because nobody actively tried to make it unpredictable. To be blunt (and my opinion, not official policy): Code that breaks if timing of unrelated asynchronous operations change is bad code. Unrelated just means neither awaits the other.

If you manually call scheduleMicrotask or Timer, then you can somewhat predict the order of those callbacks, but futures are complicated objects with different optimizations depending on how they're used, and you should not expect to predict anything more complicated than calling complete on a Completer for a Future with a single listener.

That said, traditionally an await 0; has inserted a delay that allows other already scheduled microtasks to run. It is expected to introduce effectively await Future.microtask(()=>0) which schedules a microtask at the end of the queue, because the specification said it was equivalent to Future.value(0), and that was how Future.value worked (not how it is specified, just how it is implemented). The await 0 shouldn't even have specified in terms of a specific constructor, and we should fix that too.

The order that other platforms complete in is a consequence of the internal behavior of a function named _propagateToListeners which calls the listeners of a completed future, completes the futures returned by those callbacks and the continues calling those futures' callbacks in a loop. It propagates a future result as far as possible in a single step, effectively completing later futures synchronously. It's interesting that dart2wasm behaves differently, because it might mean that it's not as efficient as possible. That's worth looking into. But it's not wrong.

mkustermann commented 2 months ago

It's interesting that dart2wasm behaves differently, because it might mean that it's not as efficient as possible. That's worth looking into. But it's not wrong.

So I'll label this as performance improvement.

lrhn commented 2 months ago

I'm fine with that. If(/when) someone comes along and says that it makes dart2wasm unusable for their code that things are not timed exactly the same way, then we can consider whether to appease them, or if they'll have to fix their code. (If those people are web developers inside Google, and we want to switch them from dart2js to dart2wasm, we may have to fix their code.)