Imaginantia

思ったことを書きます

メモ GetServerTimeInSecondsの結果を引き算してはいけない

前からわかっていた話ですが念の為に調査しました。

udonsharp.docs.vrchat.com

Networking.GetServerTimeInSeconds() (以下 ServerTime) は各プレイヤーでほぼ*1等しいので共通の時間軸として使えて便利です。

しかしこの値はだんだん増えます。増えるということは (絶対時間じゃないから) 上限に達するということで、そうなったら巻き戻ると推測されています

よって、「ServerTime が単調増加する」ことに依存したコードは意図しない挙動を起こす可能性があります。

 

GetServerTimeInMilliseconds() が int 型なことから推測すると、範囲は -2147483.648 ~ 2147483.647 になってると思われます。

2147483 秒くらいになった次の瞬間には -2147483 秒ということになってる…というわけです。これでは困ります。

var c = Networking.GetServerTimeInSeconds();
var s = "";
s += $"{c}\n";
s += $"{Networking.CalculateServerDeltaTime(c, 0)}\n"; // 後を読むとわかりますが1行目と自明に一致します
s += $"{Networking.CalculateServerDeltaTime(c, firstFrame)}\n"; // こうすると大丈夫そうという話
text.text = s;

対応を考える

自前で繰り返しを処理しても一応動きそうですが、ここで、Networking.CalculateServerDeltaTime(double timeInSeconds, double previousTimeInSeconds) という関数があります (previous の方が第二引数なことに注意)。

これは ServerTime の差分を計算してくれる関数のようです。が、細かい仕様が結局わかりません。

適当な入力を与えて観察してみたところ、中身はこんな感じっぽいです:

CalculateServerDeltaTime = (time, prevTime) => {
  double delta = time - prevTime;
  if(delta < -2147483.6475) delta += 4294967.295;
  return delta;
}

結構思ってたのと違った。というか色々気になる*2。とは言え、目的には十分そうです。

つまりワールドに master が join したときの ServerTime を baseTime として記録・同期しておき、常に CalculateServerDeltaTime(GetServerTimeInSeconds(), baseTime) を、インスタンス内時間軸として使えばよい。

もしも ServerTime がオーバーフローして -2147483 秒になったとしても、概ね ↑ の if が機能して、期待通り連続かつ単調増加な値を返してくれることと思います。

ここで概ねというのは「baseTime0 未満なインスタンスでオーバーフローが発生したとき、if の条件を通らない」という話なんですが、これが発生するには 24.9 日インスタンスに滞在する必要があるので、まぁ気にしなくていいと思います。

一応丁寧に処理をすれば 49.8 日まで不具合を引き伸ばせると思いますが、コードがシンプルな方がいいような気がします*3

 

ちなみに、ClientSim だと単なる引き算で実装されているっぽいです。まぁ GetServerTimeInSeconds が 0 始まりの値を返すので大丈夫だろうということなんでしょう。

どれくらい起こり得るのか

ちょっと気になるのは、「これに起因したバグが発生する/している確率」です。

ServerTime は実際各サーバー?によってだいぶ異なるようです。インスタンスを作ろうとしたときに空いているサーバーがランダムに割り振られてるのかな?

rejoin を繰り返して ServerTime の分布を確認してみました。USE を 20 回確認し終える直前でアカウントが Too many requests で VRChat にログインできなくなりましたおすすめしません(30分くらいで解除されました)。EU/JP はテスト用アカウントで少なめに確認しました。

近い値のサーバーが複数ある*4のはサーバー起動タイミングが揃っているとかですかね。なんだかかわいいですね。

ともかく、値が不規則であること、リージョン毎の癖みたいなものはありそうだけどだからと言って何か確定するようなことを言えそうにないことがわかります。

実際独立して 49.8 日周期で繰り返しているとしたら -2147483.648 ~ 2147483.647 が一様に出現しうるはずです。

というわけで「ServerTime が一様出現するとき、1インスタンスd 時間過ごしたときに ServerTime がオーバーフローする確率」を考えますと、明らかに  d\times 60\times 60\div 4294967.296 \approx 0.0008382d です。

つまり「1時間過ごして ServerTime がオーバーフローする確率は 0.083%」ということです。

これは例えば100インスタンスの独立試行を考えると約8%の確率で起きているということです。そこそこありますね。

public instance とかだと長い事インスタンスが維持されることも多いと思うので、まぁ正直かなり起きていると思います。

 

うーん。うちのワールドのバグの原因が本当にこれだけだったらいいんですけどね……。

*1:誤差はありますが、気になることができない

*2:境界値は -2147483.648 で、差分量は 4294967.296 であるべきな気がする… 今の数値だと 1ms ズレそう

*3:あとundocumentedな挙動には基本的に依存しない方がよい

*4:上から順番に記入しているので、時間が巻き戻っているものは同一サーバーではない