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 8f48131f8d..145681f8df 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/AdapterUtil.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/AdapterUtil.cs @@ -582,6 +582,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 7a5e37198b..026a437cb7 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/FunctionalTests/Microsoft.Data.SqlClient.Tests.csproj b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/Microsoft.Data.SqlClient.Tests.csproj index 2336768391..89efdfc9be 100644 --- a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/Microsoft.Data.SqlClient.Tests.csproj +++ b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/Microsoft.Data.SqlClient.Tests.csproj @@ -62,6 +62,7 @@ + diff --git a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/TickCountElapsedTest.cs b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/TickCountElapsedTest.cs new file mode 100644 index 0000000000..33f5c660eb --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/TickCountElapsedTest.cs @@ -0,0 +1,70 @@ +// 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 System.Reflection; +using Microsoft.Data.Common; +using Xunit; + +namespace Microsoft.Data.SqlClient.Tests +{ + + /// + /// Tests for Environment.TickCount elapsed time calculation with wraparound handling. + /// + public sealed class TickCountElapsedTest + { + /// + /// Invokes the internal CalculateTickCountElapsed method to compute elapsed time between two tick counts. + /// + /// + /// + /// + internal static uint CalculateTickCountElapsed(long startTick, long endTick) { + var adpType = Assembly.GetAssembly(typeof(SqlConnection)).GetType("Microsoft.Data.Common.ADP"); + return (uint) adpType.GetMethod("CalculateTickCountElapsed", BindingFlags.Static | BindingFlags.NonPublic).Invoke(null, new object[] {startTick, endTick}); + } + + /// + /// Verifies that normal elapsed time calculation works correctly. + /// + [Fact] + public void CalculateTickCountElapsed_NormalCase_ReturnsCorrectElapsed() + { + uint elapsed = 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 = 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 = CalculateTickCountElapsed(start, end); + Assert.Equal(expected, elapsed); + } + + /// + /// Verifies that zero elapsed time returns zero. + /// + [Fact] + public void CalculateTickCountElapsed_ZeroElapsed_ReturnsZero() + { + uint elapsed = CalculateTickCountElapsed(1000, 1000); + Assert.Equal(0u, elapsed); + } + } +} 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"]);