From 52f1cb9a8f4faaa0c86d94d35fe6cbfa0cb32bec Mon Sep 17 00:00:00 2001 From: Mayuki Sawatari Date: Fri, 6 Sep 2024 12:47:05 +0900 Subject: [PATCH 1/3] Add a callback when an Ack message is received from the client --- .../Features/IMagicOnionHeartbeatFeature.cs | 8 ++ .../Hubs/StreamingHubHeartbeatManager.cs | 55 ++++++++---- .../StreamingHubHeartbeatManagerTest.cs | 89 ++++++++++++------- 3 files changed, 105 insertions(+), 47 deletions(-) diff --git a/src/MagicOnion.Server/Features/IMagicOnionHeartbeatFeature.cs b/src/MagicOnion.Server/Features/IMagicOnionHeartbeatFeature.cs index 208b786b0..c5c3f84e2 100644 --- a/src/MagicOnion.Server/Features/IMagicOnionHeartbeatFeature.cs +++ b/src/MagicOnion.Server/Features/IMagicOnionHeartbeatFeature.cs @@ -22,6 +22,12 @@ public interface IMagicOnionHeartbeatFeature /// CancellationToken TimeoutToken { get; } + /// + /// Sets the callback action to be performed when an Ack message is received from the client. + /// + /// + void SetAckCallback(Action? callbackAction); + /// /// Unregister the current StreamingHub connection from the HeartbeatManager. /// @@ -37,4 +43,6 @@ internal sealed class MagicOnionHeartbeatFeature(StreamingHubHeartbeatHandle han public CancellationToken TimeoutToken => handle.TimeoutToken; public void Unregister() => handle.Unregister(); + + public void SetAckCallback(Action? callbackAction) => handle.SetAckCallback(callbackAction); } diff --git a/src/MagicOnion.Server/Hubs/StreamingHubHeartbeatManager.cs b/src/MagicOnion.Server/Hubs/StreamingHubHeartbeatManager.cs index eb2e95ef2..eb7be239a 100644 --- a/src/MagicOnion.Server/Hubs/StreamingHubHeartbeatManager.cs +++ b/src/MagicOnion.Server/Hubs/StreamingHubHeartbeatManager.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using MagicOnion.Internal; using MagicOnion.Server.Diagnostics; +using MagicOnion.Server.Internal; using Microsoft.Extensions.Logging; namespace MagicOnion.Server.Hubs; @@ -17,26 +18,26 @@ internal interface IStreamingHubHeartbeatManager : IDisposable internal class StreamingHubHeartbeatHandle : IDisposable { + readonly object gate = new(); readonly IStreamingHubHeartbeatManager manager; readonly CancellationTokenSource timeoutToken; readonly TimeSpan timeoutDuration; bool disposed; - short waitingSequence; + short waitingSequence = -1; bool timeoutTimerIsRunning; DateTimeOffset lastSentAt; - DateTimeOffset lastReceivedAt; + long lastSentAtTimestamp; + Action? onAckCallback; /// /// Gets the last received time. /// - public DateTimeOffset LastReceivedAt => lastReceivedAt; + public DateTimeOffset LastReceivedAt { get; private set; } /// /// Gets the latency between client and server. Returns if not sent or received. /// - public TimeSpan Latency => (lastSentAt == default || lastReceivedAt == default) - ? TimeSpan.Zero - : lastReceivedAt - lastSentAt; + public TimeSpan Latency { get; private set; } public IStreamingServiceContext ServiceContext { get; } public CancellationToken TimeoutToken => timeoutToken.Token; @@ -53,16 +54,21 @@ public StreamingHubHeartbeatHandle(IStreamingHubHeartbeatManager manager, IStrea ); } - public void RestartTimeoutTimer(short sequence, DateTimeOffset sentAt) + public void RestartTimeoutTimer(short sequence, DateTimeOffset sentAt, long sentAtTimestamp) { if (disposed || timeoutDuration == Timeout.InfiniteTimeSpan) return; - waitingSequence = sequence; - lastSentAt = sentAt; - if (!timeoutTimerIsRunning) + lock (gate) { - timeoutToken.CancelAfter(timeoutDuration); - timeoutTimerIsRunning = true; + waitingSequence = sequence; + lastSentAt = sentAt; + lastSentAtTimestamp = sentAtTimestamp; + + if (!timeoutTimerIsRunning) + { + timeoutToken.CancelAfter(timeoutDuration); + timeoutTimerIsRunning = true; + } } } @@ -71,9 +77,24 @@ public void Ack(short sequence) if (disposed || timeoutDuration == Timeout.InfiniteTimeSpan) return; if (waitingSequence != sequence) return; - lastReceivedAt = manager.TimeProvider.GetUtcNow(); - timeoutToken.CancelAfter(Timeout.InfiniteTimeSpan); - timeoutTimerIsRunning = false; + + lock (gate) + { + var receivedAtTimestamp = manager.TimeProvider.GetTimestamp(); + var elapsed = StopwatchHelper.GetElapsedTime(lastSentAtTimestamp, receivedAtTimestamp); + + LastReceivedAt = lastSentAt.Add(elapsed); + Latency = elapsed; + timeoutToken.CancelAfter(Timeout.InfiniteTimeSpan); + timeoutTimerIsRunning = false; + + onAckCallback?.Invoke(Latency); + } + } + + public void SetAckCallback(Action? callbackAction) + { + this.onAckCallback = callbackAction; } public void Unregister() @@ -87,6 +108,7 @@ public void Dispose() { if (disposed) return; disposed = true; + onAckCallback = null; manager.Unregister(ServiceContext); timeoutToken.Dispose(); } @@ -172,6 +194,7 @@ async Task StartHeartbeatAsync(PeriodicTimer runningTimer, string method) while (await runningTimer.WaitForNextTickAsync()) { var now = TimeProvider.GetUtcNow(); + var timestamp = TimeProvider.GetTimestamp(); StreamingHubMessageWriter.WriteServerHeartbeatMessageHeader(writer, sequence, now); if (!(heartbeatMetadataProvider?.TryWriteMetadata(writer) ?? false)) { @@ -183,7 +206,7 @@ async Task StartHeartbeatAsync(PeriodicTimer runningTimer, string method) { foreach (var (contextId, handle) in contexts) { - handle.RestartTimeoutTimer(sequence, now); + handle.RestartTimeoutTimer(sequence, now, timestamp); handle.ServiceContext.QueueResponseStreamWrite(StreamingHubPayloadPool.Shared.RentOrCreate(writer.WrittenSpan)); } } diff --git a/tests/MagicOnion.Server.Tests/StreamingHubHeartbeat/StreamingHubHeartbeatManagerTest.cs b/tests/MagicOnion.Server.Tests/StreamingHubHeartbeat/StreamingHubHeartbeatManagerTest.cs index 71f7226cd..4b14ce3e0 100644 --- a/tests/MagicOnion.Server.Tests/StreamingHubHeartbeat/StreamingHubHeartbeatManagerTest.cs +++ b/tests/MagicOnion.Server.Tests/StreamingHubHeartbeat/StreamingHubHeartbeatManagerTest.cs @@ -58,12 +58,12 @@ public async Task Latency() // Act using var handle = manager.Register(context); timeProvider.Advance(TimeSpan.FromMilliseconds(350)); - await Task.Delay(1); + await Task.Delay(16); // Simulate to send heartbeat responses from clients. timeProvider.Advance(TimeSpan.FromMilliseconds(50)); handle.Ack(0); - await Task.Delay(1); + await Task.Delay(16); // Assert Assert.Equal(TimeSpan.FromMilliseconds(50), handle.Latency); @@ -99,7 +99,7 @@ public async Task Latency_Before_Ack() // Act using var handle = manager.Register(context); timeProvider.Advance(TimeSpan.FromMilliseconds(350)); - await Task.Delay(1); + await Task.Delay(16); // Assert Assert.Equal(TimeSpan.Zero, handle.Latency); @@ -124,11 +124,11 @@ public async Task Interval_Disable_Timeout() using var handle2 = manager.Register(context2); using var handle3 = manager.Register(context3); timeProvider.Advance(TimeSpan.FromMilliseconds(100)); - await Task.Delay(1); + await Task.Delay(16); timeProvider.Advance(TimeSpan.FromMilliseconds(100)); - await Task.Delay(1); + await Task.Delay(16); timeProvider.Advance(TimeSpan.FromMilliseconds(100)); - await Task.Delay(1); + await Task.Delay(16); // Assert Assert.Equal(3, context1.Responses.Count); @@ -162,7 +162,7 @@ public async Task Interval_Keep() using var handle2 = manager.Register(context2); using var handle3 = manager.Register(context3); timeProvider.Advance(TimeSpan.FromMilliseconds(350)); - await Task.Delay(1); + await Task.Delay(16); var isCanceled1 = handle1.TimeoutToken.IsCancellationRequested; var isCanceled2 = handle2.TimeoutToken.IsCancellationRequested; var isCanceled3 = handle3.TimeoutToken.IsCancellationRequested; @@ -171,7 +171,7 @@ public async Task Interval_Keep() handle2.Ack(0); handle3.Ack(0); timeProvider.Advance(TimeSpan.FromMilliseconds(250)); - await Task.Delay(1); + await Task.Delay(16); // Assert Assert.False(isCanceled1); @@ -199,12 +199,12 @@ public async Task Interval_With_Timeout() using var handle2 = manager.Register(context2); using var handle3 = manager.Register(context3); timeProvider.Advance(TimeSpan.FromMilliseconds(350)); - await Task.Delay(1); + await Task.Delay(16); var isCanceled1 = handle1.TimeoutToken.IsCancellationRequested; var isCanceled2 = handle2.TimeoutToken.IsCancellationRequested; var isCanceled3 = handle3.TimeoutToken.IsCancellationRequested; timeProvider.Advance(TimeSpan.FromMilliseconds(250)); // No responses from clients and timeouts are reached. - await Task.Delay(1); + await Task.Delay(16); // Assert Assert.False(isCanceled1); @@ -234,20 +234,20 @@ public async Task Interval_Stop_After_HandleDisposed() var handle2 = manager.Register(context2); var handle3 = manager.Register(context3); timeProvider.Advance(TimeSpan.FromMilliseconds(100)); - await Task.Delay(1); + await Task.Delay(16); timeProvider.Advance(TimeSpan.FromMilliseconds(100)); - await Task.Delay(1); + await Task.Delay(16); timeProvider.Advance(TimeSpan.FromMilliseconds(100)); - await Task.Delay(1); + await Task.Delay(16); handle1.Dispose(); handle2.Dispose(); handle3.Dispose(); timeProvider.Advance(TimeSpan.FromMilliseconds(100)); - await Task.Delay(1); + await Task.Delay(16); timeProvider.Advance(TimeSpan.FromMilliseconds(100)); - await Task.Delay(1); + await Task.Delay(16); timeProvider.Advance(TimeSpan.FromMilliseconds(100)); - await Task.Delay(1); + await Task.Delay(16); // Assert Assert.Equal(3, context1.Responses.Count); @@ -283,11 +283,11 @@ public async Task CustomMetadataProvider() using var handle2 = manager.Register(context2); using var handle3 = manager.Register(context3); timeProvider.Advance(TimeSpan.FromMilliseconds(100)); - await Task.Delay(1); + await Task.Delay(16); timeProvider.Advance(TimeSpan.FromMilliseconds(100)); - await Task.Delay(1); + await Task.Delay(16); timeProvider.Advance(TimeSpan.FromMilliseconds(100)); - await Task.Delay(1); + await Task.Delay(16); // Assert Assert.Equal(3, context1.Responses.Count); @@ -317,21 +317,21 @@ public async Task Timeout_Longer_Than_Interval_Keep() // Act & Assert using var handle = manager.Register(context); timeProvider.Advance(TimeSpan.FromSeconds(1)); - await Task.Delay(1); + await Task.Delay(16); Assert.False(handle.TimeoutToken.IsCancellationRequested); Assert.Single(context.Responses); timeProvider.Advance(TimeSpan.FromSeconds(1)); - await Task.Delay(1); + await Task.Delay(16); Assert.False(handle.TimeoutToken.IsCancellationRequested); Assert.Equal(2, context.Responses.Count); timeProvider.Advance(TimeSpan.FromSeconds(1)); - await Task.Delay(1); + await Task.Delay(16); Assert.False(handle.TimeoutToken.IsCancellationRequested); timeProvider.Advance(TimeSpan.FromMilliseconds(900)); - await Task.Delay(1); + await Task.Delay(16); Assert.False(handle.TimeoutToken.IsCancellationRequested); handle.Ack(0); @@ -339,7 +339,7 @@ public async Task Timeout_Longer_Than_Interval_Keep() handle.Ack(2); timeProvider.Advance(TimeSpan.FromMilliseconds(100)); - await Task.Delay(1); + await Task.Delay(16); Assert.False(handle.TimeoutToken.IsCancellationRequested); Assert.Equal(4, context.Responses.Count); } @@ -357,21 +357,21 @@ public async Task Timeout_Longer_Than_Interval_Lost() // Act & Assert using var handle = manager.Register(context); timeProvider.Advance(TimeSpan.FromSeconds(1)); - await Task.Delay(1); + await Task.Delay(16); Assert.False(handle.TimeoutToken.IsCancellationRequested); Assert.Single(context.Responses); timeProvider.Advance(TimeSpan.FromSeconds(1)); // 1s has elapsed since the first message. - await Task.Delay(1); + await Task.Delay(16); Assert.False(handle.TimeoutToken.IsCancellationRequested); Assert.Equal(2, context.Responses.Count); timeProvider.Advance(TimeSpan.FromSeconds(1)); // 2s has elapsed since the first message. - await Task.Delay(1); + await Task.Delay(16); Assert.False(handle.TimeoutToken.IsCancellationRequested); timeProvider.Advance(TimeSpan.FromMilliseconds(900)); // 2.9s has elapsed since the first message. - await Task.Delay(1); + await Task.Delay(16); Assert.False(handle.TimeoutToken.IsCancellationRequested); // Only returns a response to the first message. @@ -380,7 +380,7 @@ public async Task Timeout_Longer_Than_Interval_Lost() //handle.Ack(2); timeProvider.Advance(TimeSpan.FromMilliseconds(100)); // 3s has elapsed since the first message. - await Task.Delay(1); + await Task.Delay(16); Assert.True(handle.TimeoutToken.IsCancellationRequested); // The client should be disconnected. Assert.Equal(4, context.Responses.Count); } @@ -399,12 +399,12 @@ public async Task Sequence() // Act using var handle1 = manager.Register(context1); timeProvider.Advance(TimeSpan.FromMilliseconds(350)); - await Task.Delay(1); + await Task.Delay(16); // Simulate to send heartbeat responses from clients. handle1.Ack(0); timeProvider.Advance(TimeSpan.FromMilliseconds(350)); - await Task.Delay(1); + await Task.Delay(16); handle1.Ack(1); // Assert @@ -428,6 +428,33 @@ public void Writer_WriteServerHeartbeatMessageHeader() Assert.Equal(BuildMessageHeader(1, DateTimeOffset.FromUnixTimeMilliseconds(456789)), bufferWriter2.WrittenSpan.ToArray()); } + [Fact] + public async Task AckCallback() + { + // Arrange + var collector = FakeLogCollector.Create(new FakeLogCollectorOptions()); + var logger = new FakeLogger(collector); + var timeProvider = new FakeTimeProvider(); + var manager = new StreamingHubHeartbeatManager(TimeSpan.FromMilliseconds(300), TimeSpan.FromMilliseconds(200), null, timeProvider, logger); + var context = CreateFakeStreamingServiceContext(); + + // Act + var ackCalled = default(TimeSpan?); + using var handle = manager.Register(context); + handle.SetAckCallback(x => ackCalled = x); + timeProvider.Advance(TimeSpan.FromMilliseconds(350)); // SentAt = 00:00.0350 + await Task.Delay(16); + + // Simulate to send heartbeat responses from clients. + timeProvider.Advance(TimeSpan.FromMilliseconds(50)); // ReceivedAt = 00:00.0400 + handle.Ack(0); + await Task.Delay(16); + + // Assert + Assert.True(ackCalled.HasValue); + Assert.Equal(TimeSpan.FromMilliseconds(50), ackCalled.Value); + } + static byte[] BuildMessageHeader(byte sequence, DateTimeOffset serverSentAt) { var bufferWriter = new ArrayBufferWriter(); From f70c2d0591314ca041a4db6a6c51c8d9036f9d98 Mon Sep 17 00:00:00 2001 From: Mayuki Sawatari Date: Fri, 6 Sep 2024 12:56:18 +0900 Subject: [PATCH 2/3] Allow CancellationToken to be used within OnDisconnected --- src/MagicOnion.Server/Hubs/StreamingHub.cs | 5 ++++- .../Hubs/StreamingHubHeartbeatManager.cs | 10 +++++++++- .../StreamingHubServerHeartbeatTest.cs | 5 +++++ 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/MagicOnion.Server/Hubs/StreamingHub.cs b/src/MagicOnion.Server/Hubs/StreamingHub.cs index cbf9d22bd..303b91e3a 100644 --- a/src/MagicOnion.Server/Hubs/StreamingHub.cs +++ b/src/MagicOnion.Server/Hubs/StreamingHub.cs @@ -134,10 +134,13 @@ internal async Task? callbackAction) public void Unregister() { + if (unregistered) return; + manager.Unregister(ServiceContext); timeoutToken.CancelAfter(Timeout.InfiniteTimeSpan); timeoutTimerIsRunning = false; + unregistered = true; } public void Dispose() { if (disposed) return; + disposed = true; onAckCallback = null; - manager.Unregister(ServiceContext); + if (!unregistered) + { + manager.Unregister(ServiceContext); + } timeoutToken.Dispose(); } } diff --git a/tests/MagicOnion.Server.Tests/StreamingHubHeartbeat/StreamingHubServerHeartbeatTest.cs b/tests/MagicOnion.Server.Tests/StreamingHubHeartbeat/StreamingHubServerHeartbeatTest.cs index 18fc82533..b21b86517 100644 --- a/tests/MagicOnion.Server.Tests/StreamingHubHeartbeat/StreamingHubServerHeartbeatTest.cs +++ b/tests/MagicOnion.Server.Tests/StreamingHubHeartbeat/StreamingHubServerHeartbeatTest.cs @@ -134,6 +134,7 @@ public async Task Timeout() // Assert Assert.True((bool)Fixture.Items.GetValueOrDefault("Disconnected")); + Assert.True((bool)Fixture.Items.GetValueOrDefault("Heartbeat/TimeoutToken/IsCancellationRequested")); Assert.True(client.WaitForDisconnect().IsCompleted); } @@ -269,7 +270,11 @@ public class StreamingHubServerHeartbeatTestHub_TimeoutBehavior([FromKeyedServic { protected override ValueTask OnDisconnected() { + var httpContext = Context.CallContext.GetHttpContext(); + var heartbeatFeature = httpContext.Features.GetRequiredFeature(); + items["Disconnected"] = true; + items["Heartbeat/TimeoutToken/IsCancellationRequested"] = heartbeatFeature.TimeoutToken.IsCancellationRequested; return base.OnDisconnected(); } } From 0fbd6ba8a9e6eeb82226b6f8e78970866ff4a1d8 Mon Sep 17 00:00:00 2001 From: Mayuki Sawatari Date: Fri, 6 Sep 2024 14:45:30 +0900 Subject: [PATCH 3/3] Use TimeProvider --- .../Diagnostics/MagicOnionMetrics.cs | 2 +- src/MagicOnion.Server/Hubs/StreamingHub.cs | 14 ++++++------- .../Hubs/StreamingHubHeartbeatManager.cs | 2 +- .../Internal/StopwatchHelper.cs | 20 ------------------- 4 files changed, 9 insertions(+), 29 deletions(-) delete mode 100644 src/MagicOnion.Server/Internal/StopwatchHelper.cs diff --git a/src/MagicOnion.Server/Diagnostics/MagicOnionMetrics.cs b/src/MagicOnion.Server/Diagnostics/MagicOnionMetrics.cs index da167fa4a..6525dc18b 100644 --- a/src/MagicOnion.Server/Diagnostics/MagicOnionMetrics.cs +++ b/src/MagicOnion.Server/Diagnostics/MagicOnionMetrics.cs @@ -63,7 +63,7 @@ public void StreamingHubMethodCompleted(in MetricsContext context, StreamingHubH var tags = InitializeTagListForStreamingHub(handler.HubName); tags.Add("rpc.method", handler.MethodInfo.Name); tags.Add("magiconion.streaminghub.is_error", isErrorOrInterrupted ? BoxedTrue : BoxedFalse); - streamingHubMethodDuration.Record((long)StopwatchHelper.GetElapsedTime(startingTimestamp, endingTimestamp).TotalMilliseconds, tags); + streamingHubMethodDuration.Record((long)TimeProvider.System.GetElapsedTime(startingTimestamp, endingTimestamp).TotalMilliseconds, tags); streamingHubMethodCompletedCounter.Add(1, tags); } } diff --git a/src/MagicOnion.Server/Hubs/StreamingHub.cs b/src/MagicOnion.Server/Hubs/StreamingHub.cs index 303b91e3a..ef94212ea 100644 --- a/src/MagicOnion.Server/Hubs/StreamingHub.cs +++ b/src/MagicOnion.Server/Hubs/StreamingHub.cs @@ -1,8 +1,6 @@ using System.Buffers; -using System.Diagnostics; using System.Runtime.CompilerServices; using System.Threading.Channels; -using Cysharp.Runtime.Multicast; using Cysharp.Runtime.Multicast.Remoting; using Grpc.Core; using MagicOnion.Internal; @@ -23,6 +21,7 @@ public abstract class StreamingHubBase : ServiceBase NilTask = Task.FromResult(Nil.Default); protected static readonly ValueTask CompletedTask = new ValueTask(); @@ -88,10 +87,11 @@ internal async Task>().Value; + timeProvider = magicOnionOptions.TimeProvider ?? TimeProvider.System; var remoteProxyFactory = serviceProvider.GetRequiredService(); var remoteSerializer = serviceProvider.GetRequiredService(); - this.remoteClientResultPendingTasks = new RemoteClientResultPendingTaskRegistry(magicOnionOptions.ClientResultsDefaultTimeout, magicOnionOptions.TimeProvider ?? TimeProvider.System); + this.remoteClientResultPendingTasks = new RemoteClientResultPendingTaskRegistry(magicOnionOptions.ClientResultsDefaultTimeout, timeProvider); this.Client = remoteProxyFactory.CreateDirect(new MagicOnionRemoteReceiverWriter(StreamingServiceContext), remoteSerializer, remoteClientResultPendingTasks); var handlerRepository = serviceProvider.GetRequiredService(); @@ -270,10 +270,10 @@ async ValueTask ProcessRequestAsync(UniqueHashDictionary ha hubInstance: this, request: body, messageId: messageId, - timestamp: DateTime.UtcNow + timestamp: timeProvider.GetUtcNow().UtcDateTime ); - var methodStartingTimestamp = Stopwatch.GetTimestamp(); + var methodStartingTimestamp = timeProvider.GetTimestamp(); var isErrorOrInterrupted = false; MagicOnionServerLog.BeginInvokeHubMethod(Context.MethodHandler.Logger, context, context.Request, handler.RequestType); try @@ -300,8 +300,8 @@ async ValueTask ProcessRequestAsync(UniqueHashDictionary ha } finally { - var methodEndingTimestamp = Stopwatch.GetTimestamp(); - MagicOnionServerLog.EndInvokeHubMethod(Context.MethodHandler.Logger, context, context.ResponseSize, context.ResponseType, StopwatchHelper.GetElapsedTime(methodStartingTimestamp, methodEndingTimestamp).TotalMilliseconds, isErrorOrInterrupted); + var methodEndingTimestamp = timeProvider.GetTimestamp(); + MagicOnionServerLog.EndInvokeHubMethod(Context.MethodHandler.Logger, context, context.ResponseSize, context.ResponseType, timeProvider.GetElapsedTime(methodStartingTimestamp, methodEndingTimestamp).TotalMilliseconds, isErrorOrInterrupted); Metrics.StreamingHubMethodCompleted(Context.Metrics, handler, methodStartingTimestamp, methodEndingTimestamp, isErrorOrInterrupted); StreamingHubContextPool.Shared.Return(context); diff --git a/src/MagicOnion.Server/Hubs/StreamingHubHeartbeatManager.cs b/src/MagicOnion.Server/Hubs/StreamingHubHeartbeatManager.cs index ea2796fbb..8e7b5b862 100644 --- a/src/MagicOnion.Server/Hubs/StreamingHubHeartbeatManager.cs +++ b/src/MagicOnion.Server/Hubs/StreamingHubHeartbeatManager.cs @@ -82,7 +82,7 @@ public void Ack(short sequence) lock (gate) { var receivedAtTimestamp = manager.TimeProvider.GetTimestamp(); - var elapsed = StopwatchHelper.GetElapsedTime(lastSentAtTimestamp, receivedAtTimestamp); + var elapsed = manager.TimeProvider.GetElapsedTime(lastSentAtTimestamp, receivedAtTimestamp); LastReceivedAt = lastSentAt.Add(elapsed); Latency = elapsed; diff --git a/src/MagicOnion.Server/Internal/StopwatchHelper.cs b/src/MagicOnion.Server/Internal/StopwatchHelper.cs deleted file mode 100644 index 558e80d09..000000000 --- a/src/MagicOnion.Server/Internal/StopwatchHelper.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.Diagnostics; - -namespace MagicOnion.Server.Internal; - -internal static class StopwatchHelper -{ -#if NET7_0_OR_GREATER - public static TimeSpan GetElapsedTime(long startingTimestamp, long endingTimestamp) - => Stopwatch.GetElapsedTime(startingTimestamp, endingTimestamp); -#else -#pragma warning disable IDE1006 // Naming Styles - const long TicksPerSecond = TicksPerMillisecond * 1000; - const long TicksPerMillisecond = 10000; - static readonly double tickFrequency = (double)TicksPerSecond / Stopwatch.Frequency; -#pragma warning restore IDE1006 // Naming Styles - - public static TimeSpan GetElapsedTime(long startingTimestamp, long endingTimestamp) - => new TimeSpan((long)((endingTimestamp - startingTimestamp) * tickFrequency)); -#endif -}