0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Scratch3 『1秒待つ』は何秒待つのか

Last updated at Posted at 2022-07-12

はじめに

image.png

Scratch3 の『1秒待つ』は本当に1秒待つのだろうかと、調べてみた。

※ Scratch3 とは
https://scratch.mit.edu/

計測する

タイマー機能を使う。
『1秒待つ』の前で『タイマーをリセット』、『1秒待つ』の後で『タイマー値』を表示する。

コード

image.png

1秒待った結果

image.png

1秒よりも23msだけ余計に待っている。意外と誤差が大きい。

今度は『0秒』待ってみる

image.png

image.png

33ms 待っている。『1秒待つ』よりも誤差が大きい。

いろんな秒数で待ってみる

〇秒待つの秒数 タイマー値 誤差  誤差の大きさ
0 秒 33 ms 33 ms
1 秒 1023 ms 23 ms
2 秒 2013 ms 13 ms
3 秒 3003 ms 3 ms
4 秒 4026 ms 26 ms
5 秒 5016 ms 16 ms
6 秒 6006 ms 6 ms
7 秒 7028 ms 28 ms
8 秒 8018 ms 18 ms
9 秒 9009 ms 9 ms

結果

  • 0 秒待つのとき、33 ms 待つ
  • 3の倍数でないときは 誤差が 大き目
  • 3の倍数の秒のときは 誤差が 小さめ (不思議だね)

〇秒待つの仕組みを紐解く

Scratch3 における 33ms の呪縛

『待つ秒数』により誤差が発生するのは『33msの呪縛』のためである。

簡単にいうと、『〇秒待つ』は 33ms ごとに 下記の判定が行われて

  現在時刻 - 『待つ』を開始した時刻 < 待つ秒数

上が成立する間『待つ』ことになるのですが、『33 ms』ごとの判定になります。
33 ms × 判定回数(整数) が 待つ時間を超えたときの時間 = 『待つ』時間になります。

〇秒待つの秒数 判定の回数 待つ時間の式 待つ時間
0 秒 1 回 33 ms × 回数(1) = 33 ms
1 秒 31 回 33 ms × 回数(31) = 1023 ms
2 秒 61 回 33 ms × 回数(61) = 2013 ms
3 秒 91 回 33 ms × 回数(91) = 3003 ms
以下略

33 ms の単位なので 3の倍数の秒数のときは、ちょうどいい感じに誤差が小さくなるようです。

実装(Scratch-vm)を見てみる

runtime.js タイマー処理( 33ms )

  • 33ms ごとに タイマー処理が動き _step() を呼び出します。
  • intervalには 標準では 1000/30 (= 33 ms )が入ります。
  • _step() の中では、sequencer.stepThreads()を呼び出しします。
  • sequencer.stepThreads()の中では、runtime.updateCurrentMSecs()を呼び出しします。
  • runtime.updateCurrentMSecs() の中では、runtimeのインスタンス変数 currentMSecs へ現在時刻( ms )を入れます。
  • つまり runtime.currentMSecs は 33ms ごとに 時刻(ms)が変化するわけです。
scratch-vm/src/engine/runtime.js
    start () {
       ~
        let interval = Runtime.THREAD_STEP_INTERVAL;
        if (this.compatibilityMode) {
            interval = Runtime.THREAD_STEP_INTERVAL_COMPATIBILITY;
        }
        this.currentStepTime = interval;
        this._steppingInterval = setInterval(() => {
            this._step();
        }, interval);
       ~
    }
       ~
   _step () {
       ~
        const doneThreads = this.sequencer.stepThreads();
       ~
    }
scratch-vm/src/engine/sequencer.js
    stepThreads () {
       ~
        this.runtime.updateCurrentMSecs();
       ~
    }
scratch-vm/src/engine/runtime.js
   updateCurrentMSecs () {
        this.currentMSecs = Date.now();
    }

wait処理の実装( scratch-vm )

wait処理が呼び出されると

  • 最初の呼び出しのときは、(1) 開始時刻を記録し (2) 描画処理を実行し (3) 他スレッドに処理を譲る(=yield)
  • 2回目以降で最後の呼び出しでないときは、(1) 何もせずに他スレッドに処理を譲る(=yield)

の動きになります。他スレッドに処理を譲る(=yield)ことにより waitを実現します。何度も繰り返し waitが呼び出されますが、〇秒経過するまでは 他スレッドに処理を譲る(=yield)を続けます。yield()の後に(33ms後に)再度waitが呼ばれます。これが wait の実装です。

(33ms後に)と書きましたが、33ms の間隔をあけずに繰返してwait() を呼び出す場合もあります。このお話はこことは直接関係ないので後日に説明したいと思います。ここでは 33ms の間隔で wait() が呼ばれるものだと思ってください。

scratch-vm/src/blocks/scratch3_control.js
    wait (args, util) {
        if (util.stackTimerNeedsInit()) {
            const duration = Math.max(0, 1000 * Cast.toNumber(args.DURATION));

            util.startStackTimer(duration);
            this.runtime.requestRedraw();
            util.yield();
        } else if (!util.stackTimerFinished()) {
            util.yield();
        }
    }

util.stackTimerFinished()は下記の実装です
経過時間(timeElapsed) < 待つ時間( duration ) のときは 待つは終了しない(false)です。

scratch-vm/src/engine/block-utility.js
    stackTimerFinished () {
        const timeElapsed = this.stackFrame.timer.timeElapsed();
        if (timeElapsed < this.stackFrame.duration) {
            return false;
        }
        return true;
    }

timer.timeElapsed()は下記の実装で経過時間を得ることができます。
現在時刻(= this.nowObj.now() ) - 開始時刻( = this.startTime )で経過時間を計算します。

scratch-vm/src/util/timer.js
    timeElapsed () {
        return this.nowObj.now() - this.startTime;
    }

ここで注意して欲しいのが 現在時刻(= this.nowObj.now() )は 厳密な現在時刻ではありません。block-utility.js を見たらわかります。

  • block-utility.owObj.now() は、runtime.currentMSecs です。
  • runtime.currentMSecs は、runtimes.js 内の タイマー処理(33ms)で更新される値ですので 33ms の倍数です。
scratch-vm/src/engine/block-utility.js
    constructor (sequencer = null, thread = null) {
        ~
        this._nowObj = {
            now: () => this.sequencer.runtime.currentMSecs
        };
    }

    get nowObj () {
        if (this.runtime) {
            return this._nowObj;
        }
        return null;
    }

実装(scratch-vm)を見ると、『〇秒待つ』は 33msの倍数の時間分待つのだ!と理解できると思います。

『〇秒待つ』のときの経過時間がぴったり〇秒になるのは?

33ms の倍数 であるのなら、『3.3 秒待つ』のときは 経過時間= 3300ms になるはずですよね。試してみましょう。

image.png

image.png

予想どおりでした。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?