From 251972dad19cc2ed89aafb3de66c063ea756c003 Mon Sep 17 00:00:00 2001 From: "Ilya.Usov" Date: Wed, 29 Apr 2026 18:05:57 +0200 Subject: [PATCH] =?UTF-8?q?Add:=20FastStopwatch=20=E2=80=94=20low-overhead?= =?UTF-8?q?,=20low-resolution=20analogue=20of=20LocalStopwatch=20Backed=20?= =?UTF-8?q?by=20Environment=20TickCount=20on=20net472/netstandard2.0=20and?= =?UTF-8?q?=20Environment.TickCount64=20on=20.NET=205+.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rd-net/Lifetimes/Util/LocalStopwatch.cs | 53 ++++++++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/rd-net/Lifetimes/Util/LocalStopwatch.cs b/rd-net/Lifetimes/Util/LocalStopwatch.cs index ee3ea015a..6599ccb8d 100644 --- a/rd-net/Lifetimes/Util/LocalStopwatch.cs +++ b/rd-net/Lifetimes/Util/LocalStopwatch.cs @@ -56,4 +56,55 @@ private void AssertTimeStamp() if (Mode.IsAssertion) Assertion.Assert(myStartTimeStamp != 0, $"{nameof(LocalStopwatch)} must be created using `{nameof(StartNew)}` method"); } } -} \ No newline at end of file + + /// + /// Non-allocating, low-resolution stopwatch backed by on + /// .NET 5+ and by on older targets. Resolution is whatever + /// the OS tick granularity is (typically ~10–16 ms on Windows), which makes this much cheaper + /// than when sub-millisecond accuracy is not needed. + /// + /// + /// + /// Always create instances via ; a default-constructed value will trip an + /// assertion in . + /// + /// + /// Warning — wrap-around on pre-.NET 5 targets (net472 / netstandard2.0): + /// the underlying is a 32-bit counter. The unsigned + /// subtraction used here recovers the correct elapsed value across one wrap, giving an + /// unambiguous range of ~49.71 days (2^32 ms). If the stopwatch is read after a longer + /// interval the result silently aliases (true elapsed modulo ~49.71 days) — do not rely on + /// this type for measurements that may exceed that bound on pre-net5 targets. The .NET 5+ + /// branch uses a 64-bit counter and is not subject to this limit in practice. + /// + /// + public readonly struct FastStopwatch + { +#if NET5_0_OR_GREATER + private readonly long myStartTimeStamp; + private FastStopwatch(long startTimestamp) => myStartTimeStamp = startTimestamp; + private static long GetTicks() => Environment.TickCount64; +#else + private readonly uint myStartTimeStamp; + private FastStopwatch(uint startTimestamp) => myStartTimeStamp = startTimestamp; + private static uint GetTicks() => (uint)Environment.TickCount; +#endif + + public static FastStopwatch StartNew() => new(GetTicks()); + public long ElapsedMilliseconds + { + get + { + AssertTimeStamp(); + return unchecked(GetTicks() - myStartTimeStamp); + } + } + + public TimeSpan Elapsed => TimeSpan.FromMilliseconds(ElapsedMilliseconds); + + private void AssertTimeStamp() + { + if (Mode.IsAssertion) Assertion.Assert(myStartTimeStamp != 0, $"{nameof(FastStopwatch)} must be created using `{nameof(StartNew)}` method"); + } + } +}