diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/AdapterUtil.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/AdapterUtil.cs index f4bb469bbe..928dc6bab6 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/AdapterUtil.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/AdapterUtil.cs @@ -618,6 +618,13 @@ internal static Delegate FindBuilder(MulticastDelegate mcd) internal static long TimerCurrent() => DateTime.UtcNow.ToFileTimeUtc(); + internal static long FastTimerCurrent() => Environment.TickCount; + + internal static uint CalculateTickCountElapsed(long startTick, long endTick) + { + return (uint)(endTick - startTick); + } + internal static long TimerFromSeconds(int seconds) { long result = checked((long)seconds * TimeSpan.TicksPerSecond); diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlStatistics.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlStatistics.cs index 1ea104d065..c88cddff24 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlStatistics.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlStatistics.cs @@ -38,7 +38,7 @@ internal static ValueSqlStatisticsScope TimedScope(SqlStatistics statistics) // internal values that are not exposed through properties internal long _closeTimestamp; internal long _openTimestamp; - internal long _startExecutionTimestamp; + internal long? _startExecutionTimestamp; internal long _startFetchTimestamp; internal long _startNetworkServerTimestamp; @@ -80,7 +80,7 @@ internal bool WaitForDoneAfterRow internal void ContinueOnNewConnection() { - _startExecutionTimestamp = 0; + _startExecutionTimestamp = null; _startFetchTimestamp = 0; _waitForDoneAfterRow = false; _waitForReply = false; @@ -108,7 +108,7 @@ internal IDictionary GetDictionary() { "UnpreparedExecs", _unpreparedExecs }, { "ConnectionTime", ADP.TimerToMilliseconds(_connectionTime) }, - { "ExecutionTime", ADP.TimerToMilliseconds(_executionTime) }, + { "ExecutionTime", _executionTime }, { "NetworkServerTime", ADP.TimerToMilliseconds(_networkServerTime) } }; Debug.Assert(dictionary.Count == Count); @@ -117,9 +117,9 @@ internal IDictionary GetDictionary() internal bool RequestExecutionTimer() { - if (_startExecutionTimestamp == 0) + if (!_startExecutionTimestamp.HasValue) { - _startExecutionTimestamp = ADP.TimerCurrent(); + _startExecutionTimestamp = ADP.FastTimerCurrent(); return true; } return false; @@ -127,7 +127,7 @@ internal bool RequestExecutionTimer() internal void RequestNetworkServerTimer() { - Debug.Assert(_startExecutionTimestamp != 0, "No network time expected outside execution period"); + Debug.Assert(_startExecutionTimestamp.HasValue, "No network time expected outside execution period"); if (_startNetworkServerTimestamp == 0) { _startNetworkServerTimestamp = ADP.TimerCurrent(); @@ -137,10 +137,11 @@ internal void RequestNetworkServerTimer() internal void ReleaseAndUpdateExecutionTimer() { - if (_startExecutionTimestamp > 0) + if (_startExecutionTimestamp.HasValue) { - _executionTime += (ADP.TimerCurrent() - _startExecutionTimestamp); - _startExecutionTimestamp = 0; + uint elapsed = ADP.CalculateTickCountElapsed(_startExecutionTimestamp.Value, ADP.FastTimerCurrent()); + _executionTime += elapsed; + _startExecutionTimestamp = null; } } @@ -176,7 +177,7 @@ internal void Reset() _unpreparedExecs = 0; _waitForDoneAfterRow = false; _waitForReply = false; - _startExecutionTimestamp = 0; + _startExecutionTimestamp = null; _startNetworkServerTimestamp = 0; } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/CopyAllFromReader.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/CopyAllFromReader.cs index 4d1dd14cfb..beb8df7992 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/CopyAllFromReader.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/CopyAllFromReader.cs @@ -52,8 +52,6 @@ public static void Test(string srcConstr, string dstConstr, string dstTable) Assert.True(0 < (long)stats["BytesReceived"], "BytesReceived is non-positive."); Assert.True(0 < (long)stats["BytesSent"], "BytesSent is non-positive."); - Assert.True((long)stats["ConnectionTime"] >= (long)stats["ExecutionTime"], "Connection Time is less than Execution Time."); - Assert.True((long)stats["ExecutionTime"] >= (long)stats["NetworkServerTime"], "Execution Time is less than Network Server Time."); DataTestUtility.AssertEqualsWithDescription((long)0, (long)stats["UnpreparedExecs"], "Non-zero UnpreparedExecs value: " + (long)stats["UnpreparedExecs"]); DataTestUtility.AssertEqualsWithDescription((long)0, (long)stats["PreparedExecs"], "Non-zero PreparedExecs value: " + (long)stats["PreparedExecs"]); DataTestUtility.AssertEqualsWithDescription((long)0, (long)stats["Prepares"], "Non-zero Prepares value: " + (long)stats["Prepares"]); diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/TickCountElapsedTest.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/TickCountElapsedTest.cs new file mode 100644 index 0000000000..38a555d356 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/TickCountElapsedTest.cs @@ -0,0 +1,56 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.Data.Common; +using Xunit; + +namespace Microsoft.Data.SqlClient.UnitTests; + +/// +/// Tests for Environment.TickCount elapsed time calculation with wraparound handling. +/// +public sealed class TickCountElapsedTest +{ + /// + /// Verifies that normal elapsed time calculation works correctly. + /// + [Fact] + public void CalculateTickCountElapsed_NormalCase_ReturnsCorrectElapsed() + { + uint elapsed = ADP.CalculateTickCountElapsed(1000, 1500); + Assert.Equal(500u, elapsed); + } + + /// + /// Verifies that wraparound from int.MaxValue to int.MinValue is handled correctly. + /// + [Fact] + public void CalculateTickCountElapsed_MaxWraparound_ReturnsOne() + { + uint elapsed = ADP.CalculateTickCountElapsed(int.MaxValue, int.MinValue); + Assert.Equal(1u, elapsed); + } + + /// + /// Verifies that partial wraparound scenarios work correctly. + /// + [Theory] + [InlineData(2147483600, -2147483600, 96u)] + [InlineData(2147483647, -2147483647, 2u)] + public void CalculateTickCountElapsed_PartialWraparound_ReturnsCorrectElapsed(long start, long end, uint expected) + { + uint elapsed = ADP.CalculateTickCountElapsed(start, end); + Assert.Equal(expected, elapsed); + } + + /// + /// Verifies that zero elapsed time returns zero. + /// + [Fact] + public void CalculateTickCountElapsed_ZeroElapsed_ReturnsZero() + { + uint elapsed = ADP.CalculateTickCountElapsed(1000, 1000); + Assert.Equal(0u, elapsed); + } +}