diff --git a/BitFaster.Caching.Benchmarks/TimeBenchmarks.cs b/BitFaster.Caching.Benchmarks/TimeBenchmarks.cs index fc71231c..39df7903 100644 --- a/BitFaster.Caching.Benchmarks/TimeBenchmarks.cs +++ b/BitFaster.Caching.Benchmarks/TimeBenchmarks.cs @@ -1,5 +1,6 @@ using System; using System.Diagnostics; +using System.Runtime.InteropServices; using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Jobs; @@ -14,12 +15,22 @@ public class TimeBenchmarks { private static readonly Stopwatch sw = Stopwatch.StartNew(); + // .NET 8 onwards has TimeProvider.System + // https://learn.microsoft.com/en-us/dotnet/api/system.timeprovider.system?view=net-8.0 + // This is based on either Stopwatch (high perf timestamp) or UtcNow (time zone based on local) + [Benchmark(Baseline = true)] public DateTime DateTimeUtcNow() { return DateTime.UtcNow; } + [Benchmark()] + public DateTimeOffset DateTimeOffsetUtcNow() + { + return DateTimeOffset.UtcNow; + } + [Benchmark()] public int EnvironmentTickCount() { @@ -36,6 +47,12 @@ public long EnvironmentTickCount64() #endif } + [Benchmark()] + public long PInvokeTickCount64() + { + return TickCount64.Current; + } + [Benchmark()] public long StopWatchGetElapsed() { @@ -47,5 +64,19 @@ public long StopWatchGetTimestamp() { return Stopwatch.GetTimestamp(); } + + [Benchmark()] + public Duration DurationSinceEpoch() + { + return Duration.SinceEpoch(); + } + } + + public static class TickCount64 + { + public static long Current => GetTickCount64(); + + [DllImport("kernel32")] + private static extern long GetTickCount64(); } } diff --git a/BitFaster.Caching.UnitTests/DurationTests.cs b/BitFaster.Caching.UnitTests/DurationTests.cs index 7c5b4cf3..85bd09b9 100644 --- a/BitFaster.Caching.UnitTests/DurationTests.cs +++ b/BitFaster.Caching.UnitTests/DurationTests.cs @@ -1,23 +1,160 @@ using System; +using System.Diagnostics; +using System.Runtime.InteropServices; +using BitFaster.Caching.Lru; using FluentAssertions; using Xunit; +using Xunit.Abstractions; namespace BitFaster.Caching.UnitTests { public class DurationTests { + private readonly ITestOutputHelper testOutputHelper; + + public DurationTests(ITestOutputHelper testOutputHelper) + { + this.testOutputHelper = testOutputHelper; + } + + [Fact] + public void SinceEpoch() + { +#if NETCOREAPP3_0_OR_GREATER + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + // eps is 1/200 of a second + ulong eps = (ulong)(Stopwatch.Frequency / 200); + Duration.SinceEpoch().raw.Should().BeCloseTo(Stopwatch.GetTimestamp(), eps); + } + else + { + Duration.SinceEpoch().raw.Should().BeCloseTo(Environment.TickCount64, 15); + } +#else + // eps is 1/200 of a second + ulong eps = (ulong)(Stopwatch.Frequency / 200); + Duration.SinceEpoch().raw.Should().BeCloseTo(Stopwatch.GetTimestamp(), eps); +#endif + } + + [Fact] + public void ToTimeSpan() + { +#if NETCOREAPP3_0_OR_GREATER + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + new Duration(100).ToTimeSpan().Should().BeCloseTo(new TimeSpan(100), TimeSpan.FromMilliseconds(50)); + } + else + { + new Duration(1000).ToTimeSpan().Should().BeCloseTo(TimeSpan.FromMilliseconds(1000), TimeSpan.FromMilliseconds(10)); + } +#else + // for Stopwatch.GetTimestamp() this is number of ticks + new Duration(1 * Stopwatch.Frequency).ToTimeSpan().Should().BeCloseTo(TimeSpan.FromSeconds(1), TimeSpan.FromMilliseconds(10)); +#endif + } + + [Fact] + public void FromTimeSpan() + { +#if NETCOREAPP3_0_OR_GREATER + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + Duration.FromTimeSpan(TimeSpan.FromSeconds(1)).raw + .Should().Be(Stopwatch.Frequency); + } + else + { + Duration.FromTimeSpan(TimeSpan.FromSeconds(1)).raw + .Should().Be((long)TimeSpan.FromSeconds(1).TotalMilliseconds); + } +#else + Duration.FromTimeSpan(TimeSpan.FromSeconds(1)).raw + .Should().Be(Stopwatch.Frequency); +#endif + } + + [Fact] + public void RoundTripMilliseconds() + { + Duration.FromMilliseconds(2000) + .ToTimeSpan() + .Should().BeCloseTo(TimeSpan.FromMilliseconds(2000), TimeSpan.FromMilliseconds(50)); + } + + [Fact] + public void RoundTripSeconds() + { + Duration.FromSeconds(2) + .ToTimeSpan() + .Should().BeCloseTo(TimeSpan.FromSeconds(2), TimeSpan.FromMilliseconds(50)); + } + + [Fact] + public void RoundTripMinutes() + { + Duration.FromMinutes(2) + .ToTimeSpan() + .Should().BeCloseTo(TimeSpan.FromMinutes(2), TimeSpan.FromMilliseconds(100)); + } + [Fact] public void RoundTripHours() { - var d = Duration.FromHours(2); - d.ToTimeSpan().Should().BeCloseTo(TimeSpan.FromHours(2), TimeSpan.FromMilliseconds(100)); + Duration.FromHours(2) + .ToTimeSpan() + .Should().BeCloseTo(TimeSpan.FromHours(2), TimeSpan.FromMilliseconds(100)); } [Fact] public void RoundTripDays() { - var d = Duration.FromDays(2); - d.ToTimeSpan().Should().BeCloseTo(TimeSpan.FromDays(2), TimeSpan.FromMilliseconds(100)); + Duration.FromDays(2) + .ToTimeSpan() + .Should().BeCloseTo(TimeSpan.FromDays(2), TimeSpan.FromMilliseconds(100)); + } + + [Fact] + public void OperatorPlus() + { + (Duration.FromDays(2) + Duration.FromDays(2)) + .ToTimeSpan() + .Should().BeCloseTo(TimeSpan.FromDays(4), TimeSpan.FromMilliseconds(100)); + } + + [Fact] + public void OperatorMinus() + { + (Duration.FromDays(4) - Duration.FromDays(2)) + .ToTimeSpan() + .Should().BeCloseTo(TimeSpan.FromDays(2), TimeSpan.FromMilliseconds(100)); + } + + [Fact] + public void OperatorGreater() + { + (Duration.FromDays(4) > Duration.FromDays(2)) + .Should().BeTrue(); + } + + [Fact] + public void OperatorLess() + { + (Duration.FromDays(4) < Duration.FromDays(2)) + .Should().BeFalse(); + } + + // This is for diagnostic purposes when tests run on different operating systems. + [Fact] + public void OutputTimeParameters() + { + this.testOutputHelper.WriteLine($"Stopwatch.Frequency {Stopwatch.Frequency}"); + this.testOutputHelper.WriteLine($"TimeSpan.TicksPerSecond {TimeSpan.TicksPerSecond}"); + this.testOutputHelper.WriteLine($"stopwatchAdjustmentFactor {StopwatchTickConverter.stopwatchAdjustmentFactor}"); + var d = Duration.SinceEpoch(); + this.testOutputHelper.WriteLine($"Duration.SinceEpoch {d.raw} ({d.ToTimeSpan()})"); } } } diff --git a/BitFaster.Caching.UnitTests/Lfu/TimerWheelTests.cs b/BitFaster.Caching.UnitTests/Lfu/TimerWheelTests.cs index 80fced34..25bac835 100644 --- a/BitFaster.Caching.UnitTests/Lfu/TimerWheelTests.cs +++ b/BitFaster.Caching.UnitTests/Lfu/TimerWheelTests.cs @@ -155,7 +155,7 @@ public void WhenAdvanceThrowsCurrentTimeIsNotAdvanced() timerWheel.Schedule(AddNode(1, new DisposeThrows(), new Duration(clock.raw + TimerWheel.Spans[1]))); // This should expire the node, call evict, then throw via DisposeThrows.Dispose() - Action advance = () => timerWheel.Advance(ref cache, new Duration(clock.raw + int.MaxValue)); + Action advance = () => timerWheel.Advance(ref cache, new Duration(clock.raw + (2 * TimerWheel.Spans[1]))); advance.Should().Throw(); timerWheel.time.Should().Be(clock.raw); diff --git a/BitFaster.Caching.UnitTests/Lru/AfterAccessPolicyTests.cs b/BitFaster.Caching.UnitTests/Lru/AfterAccessPolicyTests.cs index b35d8f24..4925bd77 100644 --- a/BitFaster.Caching.UnitTests/Lru/AfterAccessPolicyTests.cs +++ b/BitFaster.Caching.UnitTests/Lru/AfterAccessPolicyTests.cs @@ -4,10 +4,6 @@ using System.Threading.Tasks; using Xunit; -#if NETFRAMEWORK -using System.Diagnostics; -#endif - namespace BitFaster.Caching.UnitTests.Lru { public class AfterAccessPolicyTests @@ -57,14 +53,7 @@ public void CreateItemInitializesTimestampToNow() { var item = this.policy.CreateItem(1, 2); -#if NETFRAMEWORK - var expected = Stopwatch.GetTimestamp(); - ulong epsilon = (ulong)(TimeSpan.FromMilliseconds(20).TotalSeconds * Stopwatch.Frequency); -#else - var expected = Environment.TickCount64; - ulong epsilon = 20; -#endif - item.TickCount.Should().BeCloseTo(expected, epsilon); + item.TickCount.Should().BeCloseTo(Duration.SinceEpoch().raw, Duration.epsilon); } [Fact] @@ -109,11 +98,7 @@ public void WhenItemIsExpiredShouldDiscardIsTrue() { var item = this.policy.CreateItem(1, 2); -#if NETFRAMEWORK - item.TickCount = Stopwatch.GetTimestamp() - StopwatchTickConverter.ToTicks(TimeSpan.FromSeconds(11)); -#else - item.TickCount = Environment.TickCount - (int)TimeSpan.FromSeconds(11).ToEnvTick64(); -#endif + item.TickCount = Duration.SinceEpoch().raw - Duration.FromSeconds(11).raw; this.policy.ShouldDiscard(item).Should().BeTrue(); } @@ -123,11 +108,7 @@ public void WhenItemIsNotExpiredShouldDiscardIsFalse() { var item = this.policy.CreateItem(1, 2); -#if NETFRAMEWORK - item.TickCount = Stopwatch.GetTimestamp() - StopwatchTickConverter.ToTicks(TimeSpan.FromSeconds(9)); -#else - item.TickCount = Environment.TickCount - (int)TimeSpan.FromSeconds(9).ToEnvTick64(); -#endif + item.TickCount = Duration.SinceEpoch().raw - Duration.FromSeconds(9).raw; this.policy.ShouldDiscard(item).Should().BeFalse(); } @@ -182,11 +163,7 @@ private LongTickCountLruItem CreateItem(bool wasAccessed, bool isExpir if (isExpired) { -#if NETFRAMEWORK - item.TickCount = Stopwatch.GetTimestamp() - StopwatchTickConverter.ToTicks(TimeSpan.FromSeconds(11)); -#else - item.TickCount = Environment.TickCount - TimeSpan.FromSeconds(11).ToEnvTick64(); -#endif + item.TickCount = Duration.SinceEpoch().raw - Duration.FromSeconds(11).raw; } return item; diff --git a/BitFaster.Caching.UnitTests/Lru/DiscretePolicyTests.cs b/BitFaster.Caching.UnitTests/Lru/DiscretePolicyTests.cs index 5e908ad2..c69bb00e 100644 --- a/BitFaster.Caching.UnitTests/Lru/DiscretePolicyTests.cs +++ b/BitFaster.Caching.UnitTests/Lru/DiscretePolicyTests.cs @@ -11,8 +11,6 @@ public class DiscretePolicyTests private readonly TestExpiryCalculator expiryCalculator; private readonly DiscretePolicy policy; - private static readonly ulong epsilon = (ulong)Duration.FromMilliseconds(20).raw; - public DiscretePolicyTests() { expiryCalculator = new TestExpiryCalculator(); @@ -50,7 +48,7 @@ public void CreateItemInitializesKeyValueAndTicks() item.Key.Should().Be(1); item.Value.Should().Be(2); - item.TickCount.Should().BeCloseTo(timeToExpire.raw + Duration.SinceEpoch().raw, epsilon); + item.TickCount.Should().BeCloseTo(timeToExpire.raw + Duration.SinceEpoch().raw, Duration.epsilon); } [Fact] @@ -95,11 +93,8 @@ public void WhenItemIsExpiredShouldDiscardIsTrue() { var item = this.policy.CreateItem(1, 2); -#if NETFRAMEWORK - item.TickCount = item.TickCount - StopwatchTickConverter.ToTicks(TimeSpan.FromMilliseconds(11)); -#else - item.TickCount = item.TickCount - TimeSpan.FromMilliseconds(11).ToEnvTick64(); -#endif + item.TickCount = item.TickCount - Duration.FromMilliseconds(11).raw; + this.policy.ShouldDiscard(item).Should().BeTrue(); } @@ -108,11 +103,7 @@ public void WhenItemIsNotExpiredShouldDiscardIsFalse() { var item = this.policy.CreateItem(1, 2); -#if NETFRAMEWORK - item.TickCount = item.TickCount - StopwatchTickConverter.ToTicks(TimeSpan.FromMilliseconds(9)); -#else - item.TickCount = item.TickCount - (int)TimeSpan.FromMilliseconds(9).ToEnvTick64(); -#endif + item.TickCount = item.TickCount - Duration.FromMilliseconds(9).raw; this.policy.ShouldDiscard(item).Should().BeFalse(); } @@ -168,11 +159,7 @@ private LongTickCountLruItem CreateItem(bool wasAccessed, bool isExpir if (isExpired) { -#if NETFRAMEWORK - item.TickCount = item.TickCount - StopwatchTickConverter.ToTicks(TimeSpan.FromMilliseconds(11)); -#else - item.TickCount = item.TickCount - TimeSpan.FromMilliseconds(11).ToEnvTick64(); -#endif + item.TickCount = item.TickCount - Duration.FromMilliseconds(11).raw; } return item; diff --git a/BitFaster.Caching.UnitTests/Lru/TLruTickCount64PolicyTests .cs b/BitFaster.Caching.UnitTests/Lru/TLruTickCount64PolicyTests .cs index 95bf7baf..197fba79 100644 --- a/BitFaster.Caching.UnitTests/Lru/TLruTickCount64PolicyTests .cs +++ b/BitFaster.Caching.UnitTests/Lru/TLruTickCount64PolicyTests .cs @@ -56,7 +56,7 @@ public void CreateItemInitializesTimestampToNow() { var item = this.policy.CreateItem(1, 2); - item.TickCount.Should().BeCloseTo(Environment.TickCount64, 20); + item.TickCount.Should().BeCloseTo(Duration.SinceEpoch().raw, Duration.epsilon); } [Fact] @@ -87,7 +87,7 @@ public async Task UpdateUpdatesTickCount() public void WhenItemIsExpiredShouldDiscardIsTrue() { var item = this.policy.CreateItem(1, 2); - item.TickCount = Environment.TickCount - (int)TimeSpan.FromSeconds(11).ToEnvTick64(); + item.TickCount = Duration.SinceEpoch().raw - Duration.FromSeconds(11).raw; this.policy.ShouldDiscard(item).Should().BeTrue(); } @@ -96,7 +96,7 @@ public void WhenItemIsExpiredShouldDiscardIsTrue() public void WhenItemIsNotExpiredShouldDiscardIsFalse() { var item = this.policy.CreateItem(1, 2); - item.TickCount = Environment.TickCount - (int)TimeSpan.FromSeconds(9).ToEnvTick64(); + item.TickCount = Duration.SinceEpoch().raw - Duration.FromSeconds(9).raw; this.policy.ShouldDiscard(item).Should().BeFalse(); } @@ -151,7 +151,7 @@ private LongTickCountLruItem CreateItem(bool wasAccessed, bool isExpir if (isExpired) { - item.TickCount = Environment.TickCount - TimeSpan.FromSeconds(11).ToEnvTick64(); + item.TickCount = Duration.SinceEpoch().raw - Duration.FromSeconds(11).raw; } return item; diff --git a/BitFaster.Caching/Duration.cs b/BitFaster.Caching/Duration.cs index 9281230e..51a090df 100644 --- a/BitFaster.Caching/Duration.cs +++ b/BitFaster.Caching/Duration.cs @@ -26,6 +26,8 @@ public readonly struct Duration internal static readonly Duration Zero = new Duration(0); + internal static readonly ulong epsilon = (ulong)Duration.FromMilliseconds(20).raw; + internal Duration(long raw) { this.raw = raw; @@ -39,7 +41,14 @@ internal Duration(long raw) public static Duration SinceEpoch() { #if NETCOREAPP3_0_OR_GREATER - return new Duration(Environment.TickCount64); + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + return new Duration(Stopwatch.GetTimestamp()); + } + else + { + return new Duration(Environment.TickCount64); + } #else return new Duration(Stopwatch.GetTimestamp()); #endif @@ -53,7 +62,14 @@ public static Duration SinceEpoch() public TimeSpan ToTimeSpan() { #if NETCOREAPP3_0_OR_GREATER - return TimeSpan.FromMilliseconds(raw); + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + return StopwatchTickConverter.FromTicks(raw); + } + else + { + return TimeSpan.FromMilliseconds(raw); + } #else return StopwatchTickConverter.FromTicks(raw); #endif @@ -68,7 +84,14 @@ public TimeSpan ToTimeSpan() public static Duration FromTimeSpan(TimeSpan timeSpan) { #if NETCOREAPP3_0_OR_GREATER - return new Duration((long)timeSpan.TotalMilliseconds); + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + return new Duration(StopwatchTickConverter.ToTicks(timeSpan)); + } + else + { + return new Duration((long)timeSpan.TotalMilliseconds); + } #else return new Duration(StopwatchTickConverter.ToTicks(timeSpan)); #endif diff --git a/BitFaster.Caching/Lru/StopwatchTickConverter.cs b/BitFaster.Caching/Lru/StopwatchTickConverter.cs index f309981f..770e8abb 100644 --- a/BitFaster.Caching/Lru/StopwatchTickConverter.cs +++ b/BitFaster.Caching/Lru/StopwatchTickConverter.cs @@ -7,7 +7,7 @@ namespace BitFaster.Caching.Lru internal static class StopwatchTickConverter { // On some platforms (e.g. MacOS), stopwatch and timespan have different resolution - private static readonly double stopwatchAdjustmentFactor = Stopwatch.Frequency / (double)TimeSpan.TicksPerSecond; + internal static readonly double stopwatchAdjustmentFactor = Stopwatch.Frequency / (double)TimeSpan.TicksPerSecond; [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static long ToTicks(TimeSpan timespan)