From fdf8c5cf230cf8b8006f117734949bfa7748c4a8 Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Wed, 9 Apr 2025 16:46:53 -0700 Subject: [PATCH] fix: Fixes critical bug in TickCount calculation. (#2151) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Summary On some operating systems (like hosted Linux VMs), the `TimeStamp.GetTimeStamp()` CPU tick count will wrap around. This is generally not an issue, except for legacy reasons the TickCount is returned in milliseconds instead of ticks. This means when the values wrap around, they are already divided by the CPU Frequency (usually 1million) and then converted to milliseconds. That means the delta between the tick count before and after wrapping is off by a magnitude of (Frequency / 1000). Example: TimeStamp A = 9223372036654775807 Some time has passed: TimeStamp B = -9223372036654775809 The raw delta is 400_000_000 (400ms) when you do `unchecked(A - B)`. If we do the calculation AFTER converting it to milliseconds, then: TickCount A = 9223372036654 TickCount B = -9223372036654 The raw delta is -18446744073308 instead of 400_000_000. To fix this the calculation was changed so `long` -> `ulong`, then divided, then converted back to `long`, effectively bypassing wrap-around issue. The new TickCount values in our example become: TickCount A = 9223372037054 TickCount B = 9223372036654 The delta is 400 (in milliseconds). 🎉 --- Projects/Server/Main.cs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/Projects/Server/Main.cs b/Projects/Server/Main.cs index d25827f6a..d5eae2b32 100644 --- a/Projects/Server/Main.cs +++ b/Projects/Server/Main.cs @@ -346,8 +346,20 @@ public static class Core } } + private static readonly bool UseFastTimestampMath = Stopwatch.Frequency % 1000 == 0; + private static readonly ulong FrequencyInMilliseconds = (ulong)Stopwatch.Frequency / 1000; + [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static long GetTimestamp() => 1000L * Stopwatch.GetTimestamp() / Stopwatch.Frequency; + public static long GetTimestamp() + { + if (UseFastTimestampMath) + { + return (long)((ulong)Stopwatch.GetTimestamp() / FrequencyInMilliseconds); + } + + // Fast calculation will be lossy, fallback to slower but accurate calculation + return (long)((UInt128)Stopwatch.GetTimestamp() * 1000 / (ulong)Stopwatch.Frequency); + } public static void Setup(Assembly applicationAssembly, Process process) {