つくるの大好き。

つくるのが大好きな人の記録。

DartのStreamのバックグラウンド精度を調べてみた

ここまでのあらすじ

突然のブログ更新を思いたったのですが、久しぶりにも関わらず懲りずに 「Timerの精度」 という全く需要の無さそうな記事を書いてしまいました。

satoshi-maemoto.hatenablog.com

ただ書き始めるとすぐ次のアイディアが浮かんでしまうもので、再び「Streamの精度」というまた需要の無さそうな記事を書いてしまうのでした。

Streamを使用したバックグラウンド処理 その1

定期的な裏タスクを実現する場合、Streamを使用することができます。
Unityでコードを書く人ならばお馴染みの yield ですね。
yieldする際にに呼び出し元にcallbackがされる形で制御が渡り、また制御が戻ってきます。

まず安直なパターンとしてループ内で await Future.delayd(単位待ち時間) として単位待ち時間ごとに制御を返すパターンのテストコードを書きました。

https://github.com/satoshi-maemoto/studying_flutter/blob/e16195369fb0ec6ff7b2e885a6dc1bcbd4e6028a/integration_test/stream_test.dart#L7-L26

  Stream<int> runLoop(
      WidgetTester tester, double desiredFps, int testSeconds) async* {
    var duration = Duration(microseconds: (1000000.0 / desiredFps).truncate());
    var start = DateTime.now();
    tester.printToConsole(
        "START: ${start.toString()} desiredFps:$desiredFps duration:${duration.toString()}");

    var called = 0;
    var end = start.add(Duration(seconds: testSeconds));
    while (DateTime.now().compareTo(end) < 0) {
      ++called;
      await Future.delayed(duration);
      yield called;
    }

    var actualFps =
        called.toDouble() / (end.difference(start).inMicroseconds / 1000000.0);
    tester.printToConsole(
        "END:   ${end.toString()} called:$called difference:${end.difference(start)} actualFps:$actualFps");
  }

これは単位待ち時間分待つので、その他の処理が行われる時間の分だけyield回数が少なくなることは明白ですね。
動かしてみましょう。

  • iPhone 14 Pro
    START: 2023-02-05 19:47:54.229318 desiredFps:60.0 duration:0:00:00.016666
    END: 2023-02-05 19:48:04.229318 called:546 difference:0:00:10.000000 actualFps:54.6

  • Xperia1 II
    START: 2023-02-05 19:26:18.861561 desiredFps:60.0 duration:0:00:00.016666
    END: 2023-02-05 19:26:28.861561 called:535 difference:0:00:10.000000 actualFps:53.5

60回を期待しているところ54回程度の呼び出ししかされない模様。
これは予想通りちょっとダメでしたね。

Streamを使用したバックグラウンド処理 その2

次にループ内ではウェイトせず、単位待ち時間が経過したらyieldするという少し丁寧な処理にしたパターンです。

https://github.com/satoshi-maemoto/studying_flutter/blob/e16195369fb0ec6ff7b2e885a6dc1bcbd4e6028a/integration_test/stream_test.dart#L28-L44

  Stream<int> runLoop2(
      WidgetTester tester, double desiredFps, int testSeconds) async* {
    var duration = Duration(microseconds: (1000000.0 / desiredFps).truncate());
    var start = DateTime.now();
    tester.printToConsole(
        "START: ${start.toString()} desiredFps:$desiredFps duration:${duration.toString()}");

    var called = 0;
    var end = start.add(Duration(seconds: testSeconds));
    var prevCalled = start;
    while (DateTime.now().compareTo(end) < 0) {
      if (DateTime.now().difference(prevCalled) > duration) {
        prevCalled = DateTime.now();
        ++called;
        yield called;
      }
    }

    var actualFps =
        called.toDouble() / (end.difference(start).inMicroseconds / 1000000.0);
    tester.printToConsole(
        "END:   ${end.toString()} called:$called difference:${end.difference(start)} actualFps:$actualFps");
  }

さてどうなるでしょうか、動かしてみましょう。

  • iPhone 14 Pro
    START: 2023-02-05 19:48:06.290216 desiredFps:60.0 duration:0:00:00.016666
    END: 2023-02-05 19:48:16.290216 called:599 difference:0:00:10.000000 actualFps:59.9

  • Xperia1 II
    START: 2023-02-05 19:26:30.947065 desiredFps:60.0 duration:0:00:00.016666
    END: 2023-02-05 19:26:40.947065 called:599 difference:0:00:10.000000 actualFps:59.9

おお、60回を期待しているところにほぼ60回程度の呼び出しが行われました!
この方法が精度としては最も良さそうです。
ループ回数が多いのでCPU使用率= バッテリー消費量という面では若干不安がありますがそちらはまた追って調べてみたいと思っています。

まとめ

というわけでDartのバッググラウンド処理の時間的精度は、TimerよりStreamの方が高いということがわかりました。