精度の高い時計を求めて

Clockのdocには[Advanced Clock Details][doc_clock_adv]という項目がある。これまであんまりこの部分には興味が無かったのですが最近ちょっと興味が湧いたので調べてみた。

# 既定のClockの精度

KivyのClockは通常は最大fpsの影響を受ける。以下のcodeは

- 最大fpsを10にして(A行)
- 0.001秒毎に関数を実行するように予約している(B行)

のですが

```python3
from kivy.config import Config
Config.set('graphics', 'maxfps', 10)  # A
from kivy.app import App
from kivy.lang import Builder
from kivy.clock import Clock

KV_CODE = '''
Label:
    text: 'Label'
    font_size: 40
    canvas.before:
        Callback:
            callback: lambda __: print('描画')
'''

class SampleApp(App):
    def build(self):
        return Builder.load_string(KV_CODE)
    def on_start(self):
        Clock.schedule_interval(self._update_opacity, 0.001)  # B
    def _update_opacity(self, dt):
        print(f'Clock dt={dt:.03f}')
        label = self.root
        label.opacity -= .3
        if label.opacity <= 0:
            return False  # 予約を解除

if __name__ == '__main__':
    SampleApp().run()
```

結果は以下のように0.001秒とはかけ離れたdtになっているのが分かる。

```
Clock dt=0.955
描画
Clock dt=0.074
描画
Clock dt=0.074
描画
Clock dt=0.074
描画
```

最初が0.955とかなり遅いのはKivyの色んな初期化処理による遅れだと思うので、後ろに続く0.074が正しい値だ。ここでA行を`Config.set('graphics', 'maxfps', 60)`と書き換えるとどうなるかというと

```
Clock dt=0.940
描画
Clock dt=0.020
描画
Clock dt=0.013
描画
Clock dt=0.013
描画
```

となる。まだ全然希望の間隔とはほど遠いがそれでもだいぶ近づいた。数値を見ると分かる通りdtは描画間隔に近い値である。

| 最大fps | 描画間隔 | dt |
| :---: | :---: | :---: |
| 10 | 0.1 | 0.074 |
| 60 | 0.016 | 0.013 |

さらにfpsの上限を無くす(A行を`Config.set('graphics', 'maxfps', 0)`と書き換える)と以下のようになった。

```
Clock dt=0.944
描画
Clock dt=0.023
描画
Clock dt=0.004
描画
Clock dt=0.005
描画
```

これでも0.001秒の4倍5倍かかっているのだから、おせじにも精度が良いとは言えない。

# なぜこうなっているのか？

KivyがGUI libraryである事を考えればこうなるのには納得できる。例えば10fpsで描画しているのにkeyframe animationの補間の計算を0.01秒間隔で行ってもCPUの無駄遣いだ。描画が行われる直前に一回だけ計算を行えば十分だろう。他にもwidgetのlayoutの計算などもだけど、とにかく描画に関わる計算はふつう描画前に一度だけ行えば十分なはずである。なのでそれ以上の精度が要らないということなのだろう。

# fps以上の精度が欲しい場面

私自身はそうなったことは無いですが描画間隔以上の精度が要る場面というのは容易に想像できる。例えばRaspberryPiを使っていて取り付けた何かのcensorの状態を極めて短い間隔で読み取りたい場合などだ。もちろん別のthread上でやってしまえばいいと言えばそれまでですが、一応KivyのClockを用いた場合でもそれが可能だと分かったのでこれから書いていく。

# 4種のClock

[冒頭に貼ったlink][doc_clock_adv]にはKivyでは4種のClockが使えると書いてある。どうやらそれそれ

- Default Clock ... 精度が最大fpsに縛られる
- Interruptible Clock ... 精度が最大fpsに縛られない。なので上で述べた"CPUの無駄遣い"が起こりうるように思える。
- Free Only Clock ... 上二つのいいとこ取り。`schedule_xxx_free()`を使って予約した場合は精度が最大fpsに縛られず、`schedule_xxx()`を使った場合は縛られる。
- Free Clock ... `schedule_xxx_free()`で予約された関数が一つでもある場合は全ての関数が最大fpsに縛られないが、無い場合は縛られる。

のようになっている。説明を読んだ感じFree Only Clockがわかりやすそうなので使ってみることにする。

# Free Only Clock

必要な変更は

- Configを使ってClockの種類を指定
- `schedule_interval()`の代わりに`schedule_interval_free()`を使う

だ。

```python3
from kivy.config import Config
Config.set('graphics', 'maxfps', 60)  # なんでもいいと思うが60にしといた
Config.set('kivy', 'kivy_clock', 'free_only')  # 変更点
from kivy.app import App
from kivy.lang import Builder
from kivy.clock import Clock

KV_CODE = '''
Label:
    text: 'Label'
    font_size: 40
    canvas.before:
        Callback:
            callback: lambda __: print('描画')
'''

class SampleApp(App):
    def build(self):
        return Builder.load_string(KV_CODE)
    def on_start(self):
        Clock.schedule_interval_free(self._update_opacity, 0.001)  # 変更点
    def _update_opacity(self, dt):
        print(f'Clock dt={dt:.03f}')  # A
        label = self.root
        label.opacity -= .3  # B
        if label.opacity <= 0:
            return False

if __name__ == '__main__':
    SampleApp().run()
```

```
Clock dt=0.191
描画
Clock dt=0.025
描画
Clock dt=0.003
Clock dt=0.000
描画
```

さっきまでは`Clock dt=...`と`描画`が交互に出力されていたのに今回は`Clock dt=...`が連続している。描画のrhythmに関係なく`_update_opacity()`が実行されたと見ていいだろう。ただ小数点以下3桁だと足りないのと、dtの値がちゃんと安定しているか見たいのでA行とB行を書き換える。

```python3
    def _update_opacity(self, dt):
        print(f'Clock dt={dt:.06f}')  # A
        label = self.root
        label.opacity -= .1  # B
        if label.opacity <= 0:
            return False
```

```
Clock dt=0.190404
描画
Clock dt=0.024428
Clock dt=0.000239
Clock dt=0.000139
Clock dt=0.000121
Clock dt=0.000115
Clock dt=0.000110
Clock dt=0.000108
Clock dt=0.000200
Clock dt=0.000108
Clock dt=0.000102
描画
```

dtの値がえらく小さくなってしまった。0.001より小さいということは早すぎるか希望通りになっているかのどちらかだろう。遅いよりかはマシだ。とにかくこのClockを用いることで描画のrhythmに縛られずにClockを使える事が確認できた。

[doc_clock_adv]:https://kivy.org/doc/master/api-kivy.clock.html#advanced-clock-details