rksoftware

Visual Studio とか C# とかが好きです

タイマーの精度について

Xamarin.Forms で一定間隔の時間毎に何か処理をしたい場合、Xamarin.Forms.Device クラスの StartTimer メソッドが使えます。
しかし、精度はそれほど高くはありません。
※ Xamarin.Forms に限らず、他の環境でも普通精度は高くないです
というわけで、確認してみましょう。
次のコードは、1 秒( 1,000 ミリ秒)毎に経過ミリ秒をコンソールに表示するコードです。

■ コード

public MainPage()
{
    InitializeComponent();

    var count=0;
    var stopWatch = System.Diagnostics.Stopwatch.StartNew();
    var lastElapsed = 0L;

    Device.StartTimer(TimeSpan.FromSeconds(1), () =>
    {
        ++count;
        var millisec = stopWatch.ElapsedMilliseconds;
        System.Console.WriteLine($"{count.ToString("000")} {millisec.ToString("000,000")} +{(millisec-lastElapsed).ToString()}");
        lastElapsed = millisec;

        if (count > 100) return false;
        return true;
    });
}

■ 実行結果

────────────────────
回  経過   前回からの経過
────────────────────
001 001,019 +1019
002 002,108 +1089
003 003,114 +1006
004 004,120 +1006
005 005,125 +1005
006 006,131 +1006
007 007,137 +1006
008 008,141 +1004
009 009,143 +1002
010 010,149 +1006
011 011,154 +1005
012 012,159 +1005
013 013,165 +1006
014 014,171 +1006
015 015,177 +1006
016 016,183 +1006
017 017,188 +1005
018 018,194 +1006
019 019,199 +1005
020 020,204 +1005
021 021,209 +1005
022 022,215 +1006
023 023,219 +1004
024 024,225 +1006
025 025,230 +1005
026 026,234 +1004
027 027,240 +1006
028 028,246 +1006
029 029,252 +1006
030 030,258 +1006
031 031,264 +1006
032 032,270 +1006
033 033,276 +1006
034 034,280 +1004
035 035,283 +1003
036 036,287 +1004
037 037,290 +1003
038 038,295 +1005
039 039,300 +1005
040 040,305 +1005
041 041,311 +1006
042 042,316 +1005
043 043,322 +1006
044 044,327 +1005
045 045,331 +1004
046 046,337 +1006
047 047,343 +1006
048 048,349 +1006
049 049,354 +1005
050 050,360 +1006
051 051,366 +1006
052 052,371 +1005
053 053,377 +1006
054 054,382 +1005
055 055,388 +1006
056 056,394 +1006
057 057,400 +1006
058 058,406 +1006
059 059,411 +1005
060 060,416 +1005
061 061,420 +1004
062 062,425 +1005
063 063,430 +1005
064 064,436 +1006
065 065,441 +1005
066 066,445 +1004
067 067,451 +1006
068 068,457 +1006
069 069,461 +1004
070 070,467 +1006
071 071,473 +1006
072 072,477 +1004
073 073,481 +1004
074 074,484 +1003
075 075,488 +1004
076 076,494 +1006
077 077,499 +1005
078 078,503 +1004
079 079,508 +1005
080 080,513 +1005
081 081,519 +1006
082 082,524 +1005
083 083,529 +1005
084 084,535 +1006
085 085,540 +1005
086 086,544 +1004
087 087,548 +1004
088 088,554 +1006
089 089,560 +1006
090 090,565 +1005
091 091,569 +1004
092 092,574 +1005
093 093,579 +1005
094 094,584 +1005
095 095,617 +1033
096 096,619 +1002
097 097,622 +1003
098 098,625 +1003
099 099,631 +1006
100 100,637 +1006
101 101,640 +1003

毎回、1,000 ミリ秒ではなく、おおむね 5 ミリ秒程度の誤差が出ていることが確認できます。
それらの誤差がたまり続けて、最終的には実際の経過時間と 640 ミリ秒の誤差になっています。

というわけで、時間を正確に刻みたい場合は少し手をかけてやる必要があります。

■ コード

public MainPage()
{
    InitializeComponent();

    var count = 0;
    var stopWatch = System.Diagnostics.Stopwatch.StartNew();
    var lastElapsed = 0L;
    var lastSeconds = 0L;

    Device.StartTimer(TimeSpan.FromMilliseconds(33), () =>
    {
        try
        {
            var millisec = stopWatch.ElapsedMilliseconds;
            var seconds = (millisec / 1_000);
            if (seconds <= lastSeconds) return true;

            ++count;
            System.Console.WriteLine($"> {count.ToString("000")} {millisec.ToString("000,000")} +{(millisec - lastElapsed).ToString()}  ");
            lastElapsed = millisec;
            lastSeconds = seconds;

            if (millisec > 100_000) return false;
            return true;
        }
        catch (Exception ex)
        {
            System.Console.WriteLine(ex.Message);
            return false;
        }
    });
}

動作としては 1 秒ごとに画面更新をすればよいのですが、それより遥かに短い時間(ここでは 33 ミリ秒)でタイマーを動かしています。
そうして数多くのタイマー処理の中から経過秒が変わったいい感じのタイミングでだけ処理をすることで、総合のズレが少ない安定した時刻の刻みが得られます。

■ 実行結果

────────────────────
回  経過   前回からの経過
────────────────────
001 001,015 +1015
002 002,025 +1010
003 003,010 +985
004 004,029 +1019
005 005,014 +985
006 006,004 +990
007 007,026 +1022
008 008,017 +991
009 009,005 +988
010 010,026 +1021
011 011,012 +986
012 012,034 +1022
013 013,024 +990
014 014,013 +989
015 015,034 +1021
016 016,024 +990
017 017,016 +992
018 018,004 +988
019 019,029 +1025
020 020,024 +995
021 021,017 +993
022 022,007 +990
023 023,006 +999
024 024,033 +1027
025 025,022 +989
026 026,012 +990
027 027,002 +990
028 028,027 +1025
029 029,021 +994
030 030,011 +990
031 031,032 +1021
032 032,022 +990
033 033,009 +987
034 034,031 +1022
035 035,021 +990
036 036,012 +991
037 037,804 +1792
038 038,009 +205
039 039,017 +1008
040 040,028 +1011
041 041,015 +987
042 042,030 +1015
043 043,020 +990
044 044,028 +1008
045 045,006 +978
046 046,029 +1023
047 047,018 +989
048 048,004 +986
049 049,027 +1023
050 050,017 +990
051 051,034 +1017
052 052,020 +986
053 053,010 +990
054 054,032 +1022
055 055,025 +993
056 056,018 +993
057 057,005 +987
058 058,030 +1025
059 059,015 +985
060 060,007 +992
061 061,034 +1027
062 062,019 +985
063 063,022 +1003
064 064,012 +990
065 065,000 +988
066 066,025 +1025
067 067,016 +991
068 068,003 +987
069 069,023 +1020
070 070,016 +993
071 071,007 +991
072 072,033 +1026
073 073,021 +988
074 074,016 +995
075 075,000 +984
076 076,028 +1028
077 077,015 +987
078 078,034 +1019
079 079,022 +988
080 080,011 +989
081 081,000 +989
082 082,027 +1027
083 083,014 +987
084 084,001 +987
085 085,053 +1052
086 086,015 +962
087 087,035 +1020
088 088,024 +989
089 089,013 +989
090 090,004 +991
091 091,029 +1025
092 092,018 +989
093 093,009 +991
094 094,034 +1025
095 095,024 +990
096 096,009 +985
097 097,006 +997
098 098,018 +1012
099 099,024 +1006
100 100,010 +986

一回ずつの誤差はより大きくなっていますが、最終的には 10 ミリ秒の誤差ということでほぼ正しく時刻が刻まれています。
例えば今回は 37 回目で大きなズレが出ていますが、38 回目で復帰していることも確認できます。

■ デメリットと調整

上記例では、もともと必要な画面更新回数のおおよそ 30 倍の回数のイベント処理を行っています。
当然その分、負荷は上がっています。バッテリーの持ちにも影響があるかもしれません。
そのあたり、必要な精度を考えて最適な間隔を設定してください。例えば上記例でいえば 200 ミリ秒間隔程度でよさそうです。

■ もう一つの戦略

今回は大目に動かしていい感じのところを抽出する戦略をとりました。
しかし、実現方法は他にもあります。
概要としては毎回タイマー間隔を計算していい感じの間隔設定し続けるものです。
こちらはまた後日。