Introducing Time abstraction Part1 (#83604)
authorTarek Mahmoud Sayed <tarekms@microsoft.com>
Sat, 25 Mar 2023 19:34:08 +0000 (12:34 -0700)
committerGitHub <noreply@github.com>
Sat, 25 Mar 2023 19:34:08 +0000 (12:34 -0700)
15 files changed:
src/libraries/Common/src/System/ITimer.cs [new file with mode: 0644]
src/libraries/Common/src/System/TimeProvider.cs [new file with mode: 0644]
src/libraries/Common/tests/Tests/System/TimeProviderTests.cs [new file with mode: 0644]
src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPoolManager.cs
src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems
src/libraries/System.Private.CoreLib/src/System/Threading/CancellationTokenSource.cs
src/libraries/System.Private.CoreLib/src/System/Threading/PeriodicTimer.cs
src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Future.cs
src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs
src/libraries/System.Private.CoreLib/src/System/Threading/Timer.cs
src/libraries/System.Runtime/ref/System.Runtime.cs
src/libraries/System.Runtime/tests/System.Runtime.Tests.csproj
src/libraries/System.Threading.Timer/tests/TimerChangeTests.cs
src/libraries/System.Threading.Timer/tests/TimerDisposeTests.cs
src/libraries/System.Threading.Timer/tests/TimerFiringTests.cs

diff --git a/src/libraries/Common/src/System/ITimer.cs b/src/libraries/Common/src/System/ITimer.cs
new file mode 100644 (file)
index 0000000..2dde864
--- /dev/null
@@ -0,0 +1,29 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace System.Threading
+{
+    /// <summary>Represents a timer that can have its due time and period changed.</summary>
+    /// <remarks>
+    /// Implementations of <see cref="Change"/>, <see cref="IDisposable.Dispose"/>, and <see cref="IAsyncDisposable.DisposeAsync"/>
+    /// must all be thread-safe such that the timer instance may be accessed concurrently from multiple threads.
+    /// </remarks>
+    public interface ITimer : IDisposable, IAsyncDisposable
+    {
+        /// <summary>Changes the start time and the interval between method invocations for a timer, using <see cref="TimeSpan"/> values to measure time intervals.</summary>
+        /// <param name="dueTime">
+        /// A <see cref="TimeSpan"/> representing the amount of time to delay before invoking the callback method specified when the <see cref="ITimer"/> was constructed.
+        /// Specify <see cref="Timeout.InfiniteTimeSpan"/> to prevent the timer from restarting. Specify <see cref="TimeSpan.Zero"/> to restart the timer immediately.
+        /// </param>
+        /// <param name="period">
+        /// The time interval between invocations of the callback method specified when the Timer was constructed.
+        /// Specify <see cref="Timeout.InfiniteTimeSpan"/> to disable periodic signaling.
+        /// </param>
+        /// <returns><see langword="true"/> if the timer was successfully updated; otherwise, <see langword="false"/>.</returns>
+        /// <exception cref="ArgumentOutOfRangeException">The <paramref name="dueTime"/> or <paramref name="period"/> parameter, in milliseconds, is less than -1 or greater than 4294967294.</exception>
+        /// <remarks>
+        /// It is the responsibility of the implementer of the ITimer interface to ensure thread safety.
+        /// </remarks>
+        bool Change(TimeSpan dueTime, TimeSpan period);
+    }
+}
diff --git a/src/libraries/Common/src/System/TimeProvider.cs b/src/libraries/Common/src/System/TimeProvider.cs
new file mode 100644 (file)
index 0000000..316cde1
--- /dev/null
@@ -0,0 +1,210 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Diagnostics;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace System
+{
+    /// <summary>Provides an abstraction for time.</summary>
+    public abstract class TimeProvider
+    {
+        private readonly double _timeToTicksRatio;
+
+        /// <summary>
+        /// Gets a <see cref="TimeProvider"/> that provides a clock based on <see cref="DateTimeOffset.UtcNow"/>,
+        /// a time zone based on <see cref="TimeZoneInfo.Local"/>, a high-performance time stamp based on <see cref="Stopwatch"/>,
+        /// and a timer based on <see cref="Timer"/>.
+        /// </summary>
+        /// <remarks>
+        /// If the <see cref="TimeZoneInfo.Local"/> changes after the object is returned, the change will be reflected in any subsequent operations that retrieve <see cref="TimeProvider.LocalNow"/>.
+        /// </remarks>
+        public static TimeProvider System { get; } = new SystemTimeProvider(null);
+
+        /// <summary>
+        /// Initializes the instance with the timestamp frequency.
+        /// </summary>
+        /// <exception cref="ArgumentOutOfRangeException">The value of <paramref name="timestampFrequency"/> is negative or zero.</exception>
+        /// <param name="timestampFrequency">Frequency of the values returned from <see cref="GetTimestamp"/> method.</param>
+        protected TimeProvider(long timestampFrequency)
+        {
+            ArgumentOutOfRangeException.ThrowIfNegativeOrZero(timestampFrequency);
+            TimestampFrequency = timestampFrequency;
+            _timeToTicksRatio = (double)TimeSpan.TicksPerSecond / TimestampFrequency;
+        }
+
+        /// <summary>
+        /// Gets a <see cref="DateTimeOffset"/> value whose date and time are set to the current
+        /// Coordinated Universal Time (UTC) date and time and whose offset is Zero,
+        /// all according to this <see cref="TimeProvider"/>'s notion of time.
+        /// </summary>
+        public abstract DateTimeOffset UtcNow { get; }
+
+        /// <summary>
+        /// Gets a <see cref="DateTimeOffset"/> value that is set to the current date and time according to this <see cref="TimeProvider"/>'s
+        /// notion of time based on <see cref="UtcNow"/>, with the offset set to the <see cref="LocalTimeZone"/>'s offset from Coordinated Universal Time (UTC).
+        /// </summary>
+        public DateTimeOffset LocalNow
+        {
+            get
+            {
+                DateTime utcDateTime = UtcNow.UtcDateTime;
+                TimeSpan offset = LocalTimeZone.GetUtcOffset(utcDateTime);
+
+                long localTicks = utcDateTime.Ticks + offset.Ticks;
+                if ((ulong)localTicks > DateTime.MaxTicks)
+                {
+                    localTicks = localTicks < DateTime.MinTicks ? DateTime.MinTicks : DateTime.MaxTicks;
+                }
+
+                return new DateTimeOffset(localTicks, offset);
+            }
+        }
+
+        /// <summary>
+        /// Gets a <see cref="TimeZoneInfo"/> object that represents the local time zone according to this <see cref="TimeProvider"/>'s notion of time.
+        /// </summary>
+        public abstract TimeZoneInfo LocalTimeZone { get; }
+
+        /// <summary>
+        /// Gets the frequency of <see cref="GetTimestamp"/> of high-frequency value per second.
+        /// </summary>
+        public long TimestampFrequency { get; }
+
+        /// <summary>
+        /// Creates a <see cref="TimeProvider"/> that provides a clock based on <see cref="DateTimeOffset.UtcNow"/>,
+        /// a time zone based on <paramref name="timeZone"/>, a high-performance time stamp based on <see cref="Stopwatch"/>,
+        /// and a timer based on <see cref="Timer"/>.
+        /// </summary>
+        /// <param name="timeZone">The time zone to use in getting the local time using <see cref="LocalNow"/>. </param>
+        /// <returns>A new instance of <see cref="TimeProvider"/>. </returns>
+        /// <exception cref="ArgumentNullException"><paramref name="timeZone"/> is null.</exception>
+        public static TimeProvider FromLocalTimeZone(TimeZoneInfo timeZone)
+        {
+            ArgumentNullException.ThrowIfNull(timeZone);
+            return new SystemTimeProvider(timeZone);
+        }
+
+        /// <summary>
+        /// Gets the current high-frequency value designed to measure small time intervals with high accuracy in the timer mechanism.
+        /// </summary>
+        /// <returns>A long integer representing the high-frequency counter value of the underlying timer mechanism. </returns>
+        public abstract long GetTimestamp();
+
+        /// <summary>
+        /// Gets the elapsed time between two timestamps retrieved using <see cref="GetTimestamp"/>.
+        /// </summary>
+        /// <param name="startingTimestamp">The timestamp marking the beginning of the time period.</param>
+        /// <param name="endingTimestamp">The timestamp marking the end of the time period.</param>
+        /// <returns>A <see cref="TimeSpan"/> for the elapsed time between the starting and ending timestamps.</returns>
+        public TimeSpan GetElapsedTime(long startingTimestamp, long endingTimestamp) =>
+            new TimeSpan((long)((endingTimestamp - startingTimestamp) * _timeToTicksRatio));
+
+        /// <summary>Creates a new <see cref="ITimer"/> instance, using <see cref="TimeSpan"/> values to measure time intervals.</summary>
+        /// <param name="callback">
+        /// A delegate representing a method to be executed when the timer fires. The method specified for callback should be reentrant,
+        /// as it may be invoked simultaneously on two threads if the timer fires again before or while a previous callback is still being handled.
+        /// </param>
+        /// <param name="state">An object to be passed to the <paramref name="callback"/>. This may be null.</param>
+        /// <param name="dueTime">The amount of time to delay before <paramref name="callback"/> is invoked. Specify <see cref="Timeout.InfiniteTimeSpan"/> to prevent the timer from starting. Specify <see cref="TimeSpan.Zero"/> to start the timer immediately.</param>
+        /// <param name="period">The time interval between invocations of <paramref name="callback"/>. Specify <see cref="Timeout.InfiniteTimeSpan"/> to disable periodic signaling.</param>
+        /// <returns>
+        /// The newly created <see cref="ITimer"/> instance.
+        /// </returns>
+        /// <exception cref="ArgumentNullException"><paramref name="callback"/> is null.</exception>
+        /// <exception cref="ArgumentOutOfRangeException">The number of milliseconds in the value of <paramref name="dueTime"/> or <paramref name="period"/> is negative and not equal to <see cref="Timeout.Infinite"/>, or is greater than <see cref="int.MaxValue"/>.</exception>
+        /// <remarks>
+        /// <para>
+        /// The delegate specified by the callback parameter is invoked once after <paramref name="dueTime"/> elapses, and thereafter each time the <paramref name="period"/> time interval elapses.
+        /// </para>
+        /// <para>
+        /// If <paramref name="dueTime"/> is zero, the callback is invoked immediately. If <paramref name="dueTime"/> is -1 milliseconds, <paramref name="callback"/> is not invoked; the timer is disabled,
+        /// but can be re-enabled by calling the <see cref="ITimer.Change"/> method.
+        /// </para>
+        /// <para>
+        /// If <paramref name="period"/> is 0 or -1 milliseconds and <paramref name="dueTime"/> is positive, <paramref name="callback"/> is invoked once; the periodic behavior of the timer is disabled,
+        /// but can be re-enabled using the <see cref="ITimer.Change"/> method.
+        /// </para>
+        /// <para>
+        /// The return <see cref="ITimer"/> instance will be implicitly rooted while the timer is still scheduled.
+        /// </para>
+        /// <para>
+        /// <see cref="CreateTimer"/> captures the <see cref="ExecutionContext"/> and stores that with the <see cref="ITimer"/> for use in invoking <paramref name="callback"/>
+        /// each time it's called. That capture can be suppressed with <see cref="ExecutionContext.SuppressFlow"/>.
+        /// </para>
+        /// </remarks>
+        public abstract ITimer CreateTimer(TimerCallback callback, object? state, TimeSpan dueTime, TimeSpan period);
+
+        /// <summary>
+        /// Provides a default implementation of <see cref="TimeProvider"/> based on <see cref="DateTimeOffset.UtcNow"/>,
+        /// <see cref="TimeZoneInfo.Local"/>, <see cref="Stopwatch"/>, and <see cref="Timer"/>.
+        /// </summary>
+        private sealed class SystemTimeProvider : TimeProvider
+        {
+            /// <summary>The time zone to treat as local.  If null, <see cref="TimeZoneInfo.Local"/> is used.</summary>
+            private readonly TimeZoneInfo? _localTimeZone;
+
+            /// <summary>Initializes the instance.</summary>
+            /// <param name="localTimeZone">The time zone to treat as local.  If null, <see cref="TimeZoneInfo.Local"/> is used.</param>
+            internal SystemTimeProvider(TimeZoneInfo? localTimeZone) : base(Stopwatch.Frequency) => _localTimeZone = localTimeZone;
+
+            /// <inheritdoc/>
+            public override TimeZoneInfo LocalTimeZone => _localTimeZone ?? TimeZoneInfo.Local;
+
+            /// <inheritdoc/>
+            public override ITimer CreateTimer(TimerCallback callback, object? state, TimeSpan dueTime, TimeSpan period)
+            {
+                ArgumentNullException.ThrowIfNull(callback);
+                return new SystemTimeProviderTimer(dueTime, period, callback, state);
+            }
+
+            /// <inheritdoc/>
+            public override long GetTimestamp() => Stopwatch.GetTimestamp();
+
+            /// <inheritdoc/>
+            public override DateTimeOffset UtcNow => DateTimeOffset.UtcNow;
+
+            /// <summary>Thin wrapper for a <see cref="TimerQueueTimer"/>.</summary>
+            /// <remarks>
+            /// We don't return a TimerQueueTimer directly as it implements IThreadPoolWorkItem and we don't
+            /// want it exposed in a way that user code could directly queue the timer to the thread pool.
+            /// We also use this instead of Timer because CreateTimer needs to return a timer that's implicitly
+            /// rooted while scheduled.
+            /// </remarks>
+            private sealed class SystemTimeProviderTimer : ITimer
+            {
+                private readonly TimerQueueTimer _timer;
+
+                public SystemTimeProviderTimer(TimeSpan dueTime, TimeSpan period, TimerCallback callback, object? state)
+                {
+                    (uint duration, uint periodTime) = CheckAndGetValues(dueTime, period);
+                    _timer = new TimerQueueTimer(callback, state, duration, periodTime, flowExecutionContext: true);
+                }
+
+                public bool Change(TimeSpan dueTime, TimeSpan period)
+                {
+                    (uint duration, uint periodTime) = CheckAndGetValues(dueTime, period);
+                    return _timer.Change(duration, periodTime);
+                }
+
+                public void Dispose() => _timer.Dispose();
+
+                public ValueTask DisposeAsync() => _timer.DisposeAsync();
+
+                private static (uint duration, uint periodTime) CheckAndGetValues(TimeSpan dueTime, TimeSpan periodTime)
+                {
+                    long dueTm = (long)dueTime.TotalMilliseconds;
+                    ArgumentOutOfRangeException.ThrowIfLessThan(dueTm, -1, nameof(dueTime));
+                    ArgumentOutOfRangeException.ThrowIfGreaterThan(dueTm, Timer.MaxSupportedTimeout, nameof(dueTime));
+
+                    long periodTm = (long)periodTime.TotalMilliseconds;
+                    ArgumentOutOfRangeException.ThrowIfLessThan(periodTm, -1, nameof(periodTime));
+                    ArgumentOutOfRangeException.ThrowIfGreaterThan(periodTm, Timer.MaxSupportedTimeout, nameof(periodTime));
+
+                    return ((uint)dueTm, (uint)periodTm);
+                }
+            }
+        }
+    }
+}
diff --git a/src/libraries/Common/tests/Tests/System/TimeProviderTests.cs b/src/libraries/Common/tests/Tests/System/TimeProviderTests.cs
new file mode 100644 (file)
index 0000000..8b00756
--- /dev/null
@@ -0,0 +1,420 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Threading;
+using System.Threading.Tasks;
+
+using Xunit;
+
+namespace Tests.System
+{
+    public class TimeProviderTests
+    {
+        [Fact]
+        public void TestUtcSystemTime()
+        {
+            DateTimeOffset dto1 = DateTimeOffset.UtcNow;
+            DateTimeOffset providerDto = TimeProvider.System.UtcNow;
+            DateTimeOffset dto2 = DateTimeOffset.UtcNow;
+
+            Assert.InRange(providerDto.Ticks, dto1.Ticks, dto2.Ticks);
+            Assert.Equal(TimeSpan.Zero, providerDto.Offset);
+        }
+
+        [Fact]
+        public void TestLocalSystemTime()
+        {
+            DateTimeOffset dto1 = DateTimeOffset.Now;
+            DateTimeOffset providerDto = TimeProvider.System.LocalNow;
+            DateTimeOffset dto2 = DateTimeOffset.Now;
+
+            // Ensure there was no daylight saving shift during the test execution.
+            if (dto1.Offset == dto2.Offset)
+            {
+                Assert.InRange(providerDto.Ticks, dto1.Ticks, dto2.Ticks);
+                Assert.Equal(dto1.Offset, providerDto.Offset);
+            }
+        }
+
+        [Fact]
+        public void TestSystemProviderWithTimeZone()
+        {
+            Assert.Equal(TimeZoneInfo.Local.Id, TimeProvider.System.LocalTimeZone.Id);
+
+            TimeZoneInfo tzi = TimeZoneInfo.FindSystemTimeZoneById(OperatingSystem.IsWindows() ? "Pacific Standard Time" : "America/Los_Angeles");
+
+            TimeProvider tp = TimeProvider.FromLocalTimeZone(tzi);
+            Assert.Equal(tzi.Id, tp.LocalTimeZone.Id);
+
+            DateTimeOffset utcDto1 = DateTimeOffset.UtcNow;
+            DateTimeOffset localDto = tp.LocalNow;
+            DateTimeOffset utcDto2 = DateTimeOffset.UtcNow;
+
+            DateTimeOffset utcConvertedDto = TimeZoneInfo.ConvertTime(localDto, TimeZoneInfo.Utc);
+            Assert.InRange(utcConvertedDto.Ticks, utcDto1.Ticks, utcDto2.Ticks);
+        }
+
+        [Fact]
+        public void TestSystemTimestamp()
+        {
+            long timestamp1 = Stopwatch.GetTimestamp();
+            long providerTimestamp1 = TimeProvider.System.GetTimestamp();
+            long timestamp2 = Stopwatch.GetTimestamp();
+            Thread.Sleep(100);
+            long providerTimestamp2 = TimeProvider.System.GetTimestamp();
+
+            Assert.InRange(providerTimestamp1, timestamp1, timestamp2);
+            Assert.True(providerTimestamp2 > timestamp2);
+            Assert.Equal(Stopwatch.GetElapsedTime(providerTimestamp1, providerTimestamp2), TimeProvider.System.GetElapsedTime(providerTimestamp1, providerTimestamp2));
+
+            Assert.Equal(Stopwatch.Frequency, TimeProvider.System.TimestampFrequency);
+        }
+
+        public static IEnumerable<object[]> TimersProvidersData()
+        {
+            yield return new object[] { TimeProvider.System, 6000 };
+            yield return new object[] { new FastClock(),     3000 };
+        }
+
+        [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))]
+        [MemberData(nameof(TimersProvidersData))]
+        public void TestProviderTimer(TimeProvider provider, int MaxMilliseconds)
+        {
+            TimerState state = new TimerState();
+
+            state.Timer = provider.CreateTimer(
+                            stat =>
+                                {
+                                    TimerState s = (TimerState)stat;
+                                    s.Counter++;
+
+                                    s.TotalTicks += DateTimeOffset.UtcNow.Ticks - s.UtcNow.Ticks;
+
+                                    switch (s.Counter)
+                                    {
+                                        case 2:
+                                            s.Period = 400;
+                                            s.Timer.Change(TimeSpan.FromMilliseconds(s.Period), TimeSpan.FromMilliseconds(s.Period));
+                                            break;
+
+                                        case 4:
+                                            s.TokenSource.Cancel();
+                                            s.Timer.Dispose();
+                                            break;
+                                    }
+
+                                    s.UtcNow = DateTimeOffset.UtcNow;
+                                },
+                            state,
+                            TimeSpan.FromMilliseconds(state.Period), TimeSpan.FromMilliseconds(state.Period));
+
+            state.TokenSource.Token.WaitHandle.WaitOne(30000);
+            state.TokenSource.Dispose();
+
+            Assert.Equal(4, state.Counter);
+            Assert.Equal(400, state.Period);
+            Assert.True(MaxMilliseconds >= state.TotalTicks / TimeSpan.TicksPerMillisecond, $"The total fired periods {state.TotalTicks / TimeSpan.TicksPerMillisecond}ms expected not exceeding the expected max {MaxMilliseconds}");
+        }
+
+        [Fact]
+        public void FastClockTest()
+        {
+            FastClock fastClock = new FastClock();
+
+            for (int i = 0; i < 20; i++)
+            {
+                DateTimeOffset fastNow = fastClock.UtcNow;
+                DateTimeOffset now = DateTimeOffset.UtcNow;
+
+                Assert.True(fastNow > now, $"Expected {fastNow} > {now}");
+
+                fastNow = fastClock.LocalNow;
+                now = DateTimeOffset.Now;
+
+                Assert.True(fastNow > now, $"Expected {fastNow} > {now}");
+            }
+
+            Assert.Equal(TimeSpan.TicksPerSecond, fastClock.TimestampFrequency);
+
+            long stamp1 = fastClock.GetTimestamp();
+            long stamp2 = fastClock.GetTimestamp();
+
+            Assert.Equal(stamp2 - stamp1, fastClock.GetElapsedTime(stamp1, stamp2).Ticks);
+        }
+
+        public static IEnumerable<object[]> TimersProvidersListData()
+        {
+            yield return new object[] { TimeProvider.System };
+            yield return new object[] { new FastClock() };
+        }
+
+        [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))]
+        [MemberData(nameof(TimersProvidersListData))]
+        public static void CancellationTokenSourceWithTimer(TimeProvider provider)
+        {
+            //
+            // Test out some int-based timeout logic
+            //
+            CancellationTokenSource cts = new CancellationTokenSource(Timeout.InfiniteTimeSpan, provider); // should be an infinite timeout
+            CancellationToken token = cts.Token;
+            ManualResetEventSlim mres = new ManualResetEventSlim(false);
+            CancellationTokenRegistration ctr = token.Register(() => mres.Set());
+
+            Assert.False(token.IsCancellationRequested,
+               "CancellationTokenSourceWithTimer:  Cancellation signaled on infinite timeout (int)!");
+
+            cts.CancelAfter(1000000);
+
+            Assert.False(token.IsCancellationRequested,
+               "CancellationTokenSourceWithTimer:  Cancellation signaled on super-long timeout (int) !");
+
+            cts.CancelAfter(1);
+
+            Debug.WriteLine("CancellationTokenSourceWithTimer: > About to wait on cancellation that should occur soon (int)... if we hang, something bad happened");
+
+            mres.Wait();
+
+            cts.Dispose();
+
+            //
+            // Test out some TimeSpan-based timeout logic
+            //
+            TimeSpan prettyLong = new TimeSpan(1, 0, 0);
+            cts = new CancellationTokenSource(prettyLong, provider);
+            token = cts.Token;
+            mres = new ManualResetEventSlim(false);
+            ctr = token.Register(() => mres.Set());
+
+            Assert.False(token.IsCancellationRequested,
+               "CancellationTokenSourceWithTimer:  Cancellation signaled on super-long timeout (TimeSpan,1)!");
+
+            cts.CancelAfter(prettyLong);
+
+            Assert.False(token.IsCancellationRequested,
+               "CancellationTokenSourceWithTimer:  Cancellation signaled on super-long timeout (TimeSpan,2) !");
+
+            cts.CancelAfter(new TimeSpan(1000));
+
+            Debug.WriteLine("CancellationTokenSourceWithTimer: > About to wait on cancellation that should occur soon (TimeSpan)... if we hang, something bad happened");
+
+            mres.Wait();
+
+            cts.Dispose();
+        }
+
+        [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))]
+        [MemberData(nameof(TimersProvidersListData))]
+        public static void RunDelayTests(TimeProvider provider)
+        {
+            CancellationTokenSource cts = new CancellationTokenSource();
+            CancellationToken token = cts.Token;
+
+            // These should all complete quickly, with RAN_TO_COMPLETION status.
+            Task task1 = Task.Delay(new TimeSpan(0), provider);
+            Task task2 = Task.Delay(new TimeSpan(0), provider, token);
+
+            Debug.WriteLine("RunDelayTests:    > Waiting for 0-delayed uncanceled tasks to complete.  If we hang, something went wrong.");
+            try
+            {
+                Task.WaitAll(task1, task2);
+            }
+            catch (Exception e)
+            {
+                Assert.True(false, string.Format("RunDelayTests:    > FAILED.  Unexpected exception on WaitAll(simple tasks): {0}", e));
+            }
+
+            Assert.True(task1.Status == TaskStatus.RanToCompletion, "    > FAILED.  Expected Delay(TimeSpan(0), timeProvider) to run to completion");
+            Assert.True(task2.Status == TaskStatus.RanToCompletion, "    > FAILED.  Expected Delay(TimeSpan(0), timeProvider, uncanceledToken) to run to completion");
+
+            // This should take some time
+            Task task3 = Task.Delay(TimeSpan.FromMilliseconds(20000), provider);
+            Assert.False(task3.IsCompleted, "RunDelayTests:    > FAILED.  Delay(20000) appears to have completed too soon(1).");
+            Task t2 = Task.Delay(TimeSpan.FromMilliseconds(10));
+            Assert.False(task3.IsCompleted, "RunDelayTests:    > FAILED.  Delay(10000) appears to have completed too soon(2).");
+        }
+
+        [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))]
+        [MemberData(nameof(TimersProvidersListData))]
+        public static async void RunWaitAsyncTests(TimeProvider provider)
+        {
+            CancellationTokenSource cts = new CancellationTokenSource();
+
+            var tcs1 = new TaskCompletionSource();
+            Task task1 = tcs1.Task.WaitAsync(TimeSpan.FromDays(1), provider);
+            Assert.False(task1.IsCompleted);
+            tcs1.SetResult();
+            await task1;
+
+            var tcs2 = new TaskCompletionSource();
+            Task task2 = tcs2.Task.WaitAsync(TimeSpan.FromDays(1), provider, cts.Token);
+            Assert.False(task2.IsCompleted);
+            tcs2.SetResult();
+            await task2;
+
+            var tcs3 = new TaskCompletionSource<int>();
+            Task<int> task3 = tcs3.Task.WaitAsync(TimeSpan.FromDays(1), provider);
+            Assert.False(task3.IsCompleted);
+            tcs3.SetResult(42);
+            Assert.Equal(42, await task3);
+
+            var tcs4 = new TaskCompletionSource<int>();
+            Task<int> task4 = tcs4.Task.WaitAsync(TimeSpan.FromDays(1), provider, cts.Token);
+            Assert.False(task4.IsCompleted);
+            tcs4.SetResult(42);
+            Assert.Equal(42, await task4);
+
+            using CancellationTokenSource cts1 = new CancellationTokenSource();
+            Task task5 = Task.Run(() => { while (!cts1.Token.IsCancellationRequested) { Thread.Sleep(10); } });
+            await Assert.ThrowsAsync<TimeoutException>(() => task5.WaitAsync(TimeSpan.FromMilliseconds(10), provider));
+            cts1.Cancel();
+            await task5;
+
+            using CancellationTokenSource cts2 = new CancellationTokenSource();
+            Task task6 = Task.Run(() => { while (!cts2.Token.IsCancellationRequested) { Thread.Sleep(10); } });
+            await Assert.ThrowsAsync<TimeoutException>(() => task6.WaitAsync(TimeSpan.FromMilliseconds(10), provider, cts2.Token));
+            cts1.Cancel();
+            await task5;
+
+            using CancellationTokenSource cts3 = new CancellationTokenSource();
+            Task<int> task7 = Task<int>.Run(() => { while (!cts3.Token.IsCancellationRequested) { Thread.Sleep(10); } return 100; });
+            await Assert.ThrowsAsync<TimeoutException>(() => task7.WaitAsync(TimeSpan.FromMilliseconds(10), provider));
+            cts3.Cancel();
+            Assert.Equal(100, await task7);
+
+            using CancellationTokenSource cts4 = new CancellationTokenSource();
+            Task<int> task8 = Task<int>.Run(() => { while (!cts4.Token.IsCancellationRequested) { Thread.Sleep(10); } return 200; });
+            await Assert.ThrowsAsync<TimeoutException>(() => task8.WaitAsync(TimeSpan.FromMilliseconds(10), provider, cts4.Token));
+            cts4.Cancel();
+            Assert.Equal(200, await task8);
+        }
+
+        [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))]
+        [MemberData(nameof(TimersProvidersListData))]
+        public static async void PeriodicTimerTests(TimeProvider provider)
+        {
+            var timer = new PeriodicTimer(TimeSpan.FromMilliseconds(1), provider);
+            Assert.True(await timer.WaitForNextTickAsync());
+
+            timer.Dispose();
+            Assert.False(timer.WaitForNextTickAsync().Result);
+
+            timer.Dispose();
+            Assert.False(timer.WaitForNextTickAsync().Result);
+        }
+
+        [Fact]
+        public static void NegativeTests()
+        {
+            Assert.Throws<ArgumentOutOfRangeException>(() => new FastClock(-1)); // negative frequency
+            Assert.Throws<ArgumentOutOfRangeException>(() => new FastClock(0)); // zero frequency
+            Assert.Throws<ArgumentNullException>(() => TimeProvider.FromLocalTimeZone(null));
+
+            Assert.Throws<ArgumentNullException>(() => TimeProvider.System.CreateTimer(null, null, Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan));
+            Assert.Throws<ArgumentOutOfRangeException>(() => TimeProvider.System.CreateTimer(obj => { }, null, TimeSpan.FromMilliseconds(-2), Timeout.InfiniteTimeSpan));
+            Assert.Throws<ArgumentOutOfRangeException>(() => TimeProvider.System.CreateTimer(obj => { }, null, Timeout.InfiniteTimeSpan, TimeSpan.FromMilliseconds(-2)));
+
+            Assert.Throws<ArgumentNullException>(() => new CancellationTokenSource(Timeout.InfiniteTimeSpan, null));
+
+            Assert.Throws<ArgumentNullException>(() => new PeriodicTimer(TimeSpan.FromMilliseconds(1), null));
+        }
+
+        class TimerState
+        {
+            public TimerState()
+            {
+                Counter = 0;
+                Period = 300;
+                TotalTicks = 0;
+                UtcNow = DateTimeOffset.UtcNow;
+                TokenSource = new CancellationTokenSource();
+            }
+
+            public CancellationTokenSource TokenSource { get; set; }
+            public int Counter { get; set; }
+            public int Period { get; set; }
+            public DateTimeOffset UtcNow { get; set; }
+            public ITimer Timer { get; set; }
+            public long TotalTicks { get; set; }
+        };
+
+        // Clock that speeds up the reported time
+        class FastClock : TimeProvider
+        {
+            private long _minutesToAdd;
+            private TimeZoneInfo _zone;
+
+            public FastClock(long timestampFrequency = TimeSpan.TicksPerSecond, TimeZoneInfo? zone = null) : base(timestampFrequency)
+            {
+                _zone = zone ?? TimeZoneInfo.Local;
+            }
+
+            public override DateTimeOffset UtcNow
+            {
+                get
+                {
+                    DateTimeOffset now = DateTimeOffset.UtcNow;
+
+                    _minutesToAdd++;
+                    long remainingTicks = (DateTimeOffset.MaxValue.Ticks - now.Ticks);
+
+                    if (_minutesToAdd * TimeSpan.TicksPerMinute > remainingTicks)
+                    {
+                        _minutesToAdd = 0;
+                        return now;
+                    }
+
+                    return now.AddMinutes(_minutesToAdd);
+                }
+            }
+
+            public override TimeZoneInfo LocalTimeZone => _zone;
+
+            public override long GetTimestamp() => UtcNow.Ticks;
+
+            public override ITimer CreateTimer(TimerCallback callback, object? state, TimeSpan dueTime, TimeSpan period) =>
+                new FastTimer(callback, state, dueTime, period);
+        }
+
+        // Timer that fire faster
+        class FastTimer : ITimer
+        {
+            private Timer _timer;
+
+            public FastTimer(TimerCallback callback, object? state, TimeSpan dueTime, TimeSpan period)
+            {
+                if (dueTime != Timeout.InfiniteTimeSpan)
+                {
+                    dueTime = new TimeSpan(dueTime.Ticks / 2);
+                }
+
+                if (period != Timeout.InfiniteTimeSpan)
+                {
+                    period = new TimeSpan(period.Ticks / 2);
+                }
+
+                _timer = new Timer(callback, state, dueTime, period);
+            }
+
+            public bool Change(TimeSpan dueTime, TimeSpan period)
+            {
+                if (dueTime != Timeout.InfiniteTimeSpan)
+                {
+                    dueTime = new TimeSpan(dueTime.Ticks / 2);
+                }
+
+                if (period != Timeout.InfiniteTimeSpan)
+                {
+                    period = new TimeSpan(period.Ticks / 2);
+                }
+
+                return _timer.Change(dueTime, period);
+            }
+
+            public void Dispose() => _timer.Dispose();
+            public ValueTask DisposeAsync() => _timer.DisposeAsync();
+        }
+    }
+}
index 9bc1af96d883553b096b06433ac65b4615646da7..db54690e4ac40dca70c06f9f88571ae5c1690196 100644 (file)
@@ -443,18 +443,10 @@ namespace System.Net.Http
         /// <summary>Sets <see cref="_cleaningTimer"/> and <see cref="_timerIsRunning"/> based on the specified timeout.</summary>
         private void SetCleaningTimer(TimeSpan timeout)
         {
-            try
+            if (_cleaningTimer!.Change(timeout, Timeout.InfiniteTimeSpan))
             {
-                _cleaningTimer!.Change(timeout, Timeout.InfiniteTimeSpan);
                 _timerIsRunning = timeout != Timeout.InfiniteTimeSpan;
             }
-            catch (ObjectDisposedException)
-            {
-                // In a rare race condition where the timer callback was queued
-                // or executed and then the pool manager was disposed, the timer
-                // would be disposed and then calling Change on it could result
-                // in an ObjectDisposedException.  We simply eat that.
-            }
         }
 
         /// <summary>Removes unusable connections from each pool, and removes stale pools entirely.</summary>
index cd44b2e9e5c639506a6807c5533fff4d5ead6385..bb3a70a516cfdc030341bf4b410d050ef42c394d 100644 (file)
     <Compile Include="$(CommonPath)SkipLocalsInit.cs">
       <Link>Common\SkipLocalsInit.cs</Link>
     </Compile>
+    <Compile Include="$(CommonPath)System\ITimer.cs">
+      <Link>Common\System\ITimer.cs</Link>
+    </Compile>
+    <Compile Include="$(CommonPath)System\TimeProvider.cs">
+      <Link>Common\System\TimeProvider.cs</Link>
+    </Compile>
     <Compile Include="$(CommonPath)System\LocalAppContextSwitches.Common.cs">
       <Link>Common\System\LocalAppContextSwitches.Common.cs</Link>
     </Compile>
index b40c9033f865b94c182ba1b31b128a709f2d4902..d6277386bd1c784515e3d60e8503a4ac9ff59735 100644 (file)
@@ -38,8 +38,8 @@ namespace System.Threading
         private volatile int _state;
         /// <summary>Whether this <see cref="CancellationTokenSource"/> has been disposed.</summary>
         private bool _disposed;
-        /// <summary>TimerQueueTimer used by CancelAfter and Timer-related ctors. Used instead of Timer to avoid extra allocations and because the rooted behavior is desired.</summary>
-        private volatile TimerQueueTimer? _timer;
+        /// <summary>ITimer used by CancelAfter and Timer-related ctors. Used instead of Timer to avoid extra allocations and because the rooted behavior is desired.</summary>
+        private volatile ITimer? _timer;
         /// <summary><see cref="System.Threading.WaitHandle"/> lazily initialized and returned from <see cref="WaitHandle"/>.</summary>
         private volatile ManualResetEvent? _kernelEvent;
         /// <summary>Registration state for the source.</summary>
@@ -137,15 +137,31 @@ namespace System.Threading
         /// canceled already.
         /// </para>
         /// </remarks>
-        public CancellationTokenSource(TimeSpan delay)
+        public CancellationTokenSource(TimeSpan delay) : this(delay, TimeProvider.System)
         {
+        }
+
+        /// <summary>Initializes a new instance of the <see cref="CancellationTokenSource"/> class that will be canceled after the specified <see cref="TimeSpan"/>.</summary>
+        /// <param name="delay">The time interval to wait before canceling this <see cref="CancellationTokenSource"/>.</param>
+        /// <param name="timeProvider">The <see cref="TimeProvider"/> with which to interpret the <paramref name="delay"/>.</param>
+        /// <exception cref="ArgumentOutOfRangeException"><paramref name="delay"/>'s <see cref="TimeSpan.TotalMilliseconds"/> is less than -1 or greater than <see cref="uint.MaxValue"/> - 1.</exception>
+        /// <exception cref="ArgumentNullException"><paramref name="timeProvider"/> is null.</exception>
+        /// <remarks>
+        /// The countdown for the delay starts during the call to the constructor.  When the delay expires,
+        /// the constructed <see cref="CancellationTokenSource"/> is canceled, if it has
+        /// not been canceled already. Subsequent calls to CancelAfter will reset the delay for the constructed
+        /// <see cref="CancellationTokenSource"/>, if it has not been canceled already.
+        /// </remarks>
+        public CancellationTokenSource(TimeSpan delay, TimeProvider timeProvider)
+        {
+            ArgumentNullException.ThrowIfNull(timeProvider);
             long totalMilliseconds = (long)delay.TotalMilliseconds;
             if (totalMilliseconds < -1 || totalMilliseconds > Timer.MaxSupportedTimeout)
             {
                 ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.delay);
             }
 
-            InitializeWithTimer((uint)totalMilliseconds);
+            InitializeWithTimer((uint)totalMilliseconds, timeProvider);
         }
 
         /// <summary>
@@ -174,14 +190,14 @@ namespace System.Threading
                 ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.millisecondsDelay);
             }
 
-            InitializeWithTimer((uint)millisecondsDelay);
+            InitializeWithTimer((uint)millisecondsDelay, TimeProvider.System);
         }
 
         /// <summary>
         /// Common initialization logic when constructing a CTS with a delay parameter.
         /// A zero delay will result in immediate cancellation.
         /// </summary>
-        private void InitializeWithTimer(uint millisecondsDelay)
+        private void InitializeWithTimer(uint millisecondsDelay, TimeProvider timeProvider)
         {
             if (millisecondsDelay == 0)
             {
@@ -189,8 +205,17 @@ namespace System.Threading
             }
             else
             {
-                _timer = new TimerQueueTimer(s_timerCallback, this, millisecondsDelay, Timeout.UnsignedInfinite, flowExecutionContext: false);
-
+                if (timeProvider == TimeProvider.System)
+                {
+                    _timer = new TimerQueueTimer(s_timerCallback, this, millisecondsDelay, Timeout.UnsignedInfinite, flowExecutionContext: false);
+                }
+                else
+                {
+                    using (ExecutionContext.SuppressFlow())
+                    {
+                        _timer = timeProvider.CreateTimer(s_timerCallback, this, TimeSpan.FromMilliseconds(millisecondsDelay), Timeout.InfiniteTimeSpan);
+                    }
+                }
                 // The timer roots this CTS instance while it's scheduled.  That is by design, so
                 // that code like:
                 //     new CancellationTokenSource(timeout).Token.Register(() => ...);
@@ -404,7 +429,7 @@ namespace System.Threading
             // expired and Disposed itself).  But this would be considered bad behavior, as
             // Dispose() is not thread-safe and should not be called concurrently with CancelAfter().
 
-            TimerQueueTimer? timer = _timer;
+            ITimer? timer = _timer;
             if (timer == null)
             {
                 // Lazily initialize the timer in a thread-safe fashion.
@@ -412,16 +437,16 @@ namespace System.Threading
                 // chance on a timer "losing" the initialization and then
                 // cancelling the token before it (the timer) can be disposed.
                 timer = new TimerQueueTimer(s_timerCallback, this, Timeout.UnsignedInfinite, Timeout.UnsignedInfinite, flowExecutionContext: false);
-                TimerQueueTimer? currentTimer = Interlocked.CompareExchange(ref _timer, timer, null);
+                ITimer? currentTimer = Interlocked.CompareExchange(ref _timer, timer, null);
                 if (currentTimer != null)
                 {
                     // We did not initialize the timer.  Dispose the new timer.
-                    timer.Close();
+                    timer.Dispose();
                     timer = currentTimer;
                 }
             }
 
-            timer.Change(millisecondsDelay, Timeout.UnsignedInfinite, throwIfDisposed: false);
+            timer.Change(TimeSpan.FromMilliseconds(millisecondsDelay), Timeout.InfiniteTimeSpan);
         }
 
         /// <summary>
@@ -455,9 +480,8 @@ namespace System.Threading
                 // to reset it to be infinite so that it won't fire, and then recognize that it could have already
                 // fired by the time we successfully changed it, and so check to see whether that's possibly the case.
                 // If we successfully reset it and it never fired, then we can be sure it won't trigger cancellation.
-                bool reset =
-                    _timer is not TimerQueueTimer timer ||
-                    (timer.Change(Timeout.UnsignedInfinite, Timeout.UnsignedInfinite, throwIfDisposed: false) && !timer._everQueued);
+                bool reset = _timer is null ||
+                    (_timer is TimerQueueTimer timer && timer.Change(Timeout.UnsignedInfinite, Timeout.UnsignedInfinite) && !timer._everQueued);
 
                 if (reset)
                 {
@@ -509,11 +533,11 @@ namespace System.Threading
                 // internal source of cancellation, then Disposes of that linked source, which could
                 // happen at the same time the external entity is requesting cancellation).
 
-                TimerQueueTimer? timer = _timer;
+                ITimer? timer = _timer;
                 if (timer != null)
                 {
                     _timer = null;
-                    timer.Close(); // TimerQueueTimer.Close is thread-safe
+                    timer.Dispose(); // ITimer.Dispose is thread-safe
                 }
 
                 _registrations = null; // allow the GC to clean up registrations
@@ -676,12 +700,12 @@ namespace System.Threading
             if (!IsCancellationRequested &&
                 Interlocked.CompareExchange(ref _state, NotifyingState, NotCanceledState) == NotCanceledState)
             {
-                // Dispose of the timer, if any.  Dispose may be running concurrently here, but TimerQueueTimer.Close is thread-safe.
-                TimerQueueTimer? timer = _timer;
+                // Dispose of the timer, if any.  Dispose may be running concurrently here, but ITimer.Dispose is thread-safe.
+                ITimer? timer = _timer;
                 if (timer != null)
                 {
                     _timer = null;
-                    timer.Close();
+                    timer.Dispose();
                 }
 
                 // Set the event if it's been lazily initialized and hasn't yet been disposed of.  Dispose may
index 27bba5ebfca30cef4682b63f162281dca29cfe21..b784d11935bec327a6209f91072573457ddc6e32 100644 (file)
@@ -18,9 +18,11 @@ namespace System.Threading
     public sealed class PeriodicTimer : IDisposable
     {
         /// <summary>The underlying timer.</summary>
-        private readonly TimerQueueTimer _timer;
+        private readonly ITimer _timer;
         /// <summary>All state other than the _timer, so that the rooted timer's callback doesn't indirectly root itself by referring to _timer.</summary>
         private readonly State _state;
+        /// <summary>The timer's current period.</summary>
+        private TimeSpan _period;
 
         /// <summary>Initializes the timer.</summary>
         /// <param name="period">The period between ticks</param>
@@ -33,10 +35,48 @@ namespace System.Threading
                 throw new ArgumentOutOfRangeException(nameof(period));
             }
 
+            _period = period;
             _state = new State();
+
             _timer = new TimerQueueTimer(s => ((State)s!).Signal(), _state, ms, ms, flowExecutionContext: false);
         }
 
+        /// <summary>Initializes the timer.</summary>
+        /// <param name="period">The period between ticks</param>
+        /// <param name="timeProvider">The <see cref="TimeProvider"/> used to interpret <paramref name="period"/>.</param>
+        /// <exception cref="ArgumentOutOfRangeException"><paramref name="period"/> must be <see cref="Timeout.InfiniteTimeSpan"/> or represent a number of milliseconds equal to or larger than 1 and smaller than <see cref="uint.MaxValue"/>.</exception>
+        /// <exception cref="ArgumentNullException"><paramref name="timeProvider"/> is null</exception>
+        public PeriodicTimer(TimeSpan period, TimeProvider timeProvider)
+        {
+            if (!TryGetMilliseconds(period, out uint ms))
+            {
+                GC.SuppressFinalize(this);
+                throw new ArgumentOutOfRangeException(nameof(period));
+            }
+
+            if (timeProvider is null)
+            {
+                GC.SuppressFinalize(this);
+                throw new ArgumentNullException(nameof(timeProvider));
+            }
+
+            _period = period;
+            _state = new State();
+            TimerCallback callback = s => ((State)s!).Signal();
+
+            if (timeProvider == TimeProvider.System)
+            {
+                _timer = new TimerQueueTimer(callback, _state, ms, ms, flowExecutionContext: false);
+            }
+            else
+            {
+                using (ExecutionContext.SuppressFlow())
+                {
+                    _timer = timeProvider.CreateTimer(callback, _state, period, period);
+                }
+            }
+        }
+
         /// <summary>Gets or sets the period between ticks.</summary>
         /// <exception cref="ArgumentOutOfRangeException"><paramref name="value"/> must be <see cref="Timeout.InfiniteTimeSpan"/> or represent a number of milliseconds equal to or larger than 1 and smaller than <see cref="uint.MaxValue"/>.</exception>
         /// <remarks>
@@ -46,15 +86,19 @@ namespace System.Threading
         /// </remarks>
         public TimeSpan Period
         {
-            get => _timer._period == Timeout.UnsignedInfinite ? Timeout.InfiniteTimeSpan : TimeSpan.FromMilliseconds(_timer._period);
+            get => _period;
             set
             {
-                if (!TryGetMilliseconds(value, out uint ms))
+                if (!TryGetMilliseconds(value, out _))
                 {
                     throw new ArgumentOutOfRangeException(nameof(value));
                 }
 
-                _timer.Change(ms, ms);
+                _period = value;
+                if (!_timer.Change(value, value))
+                {
+                    ThrowHelper.ThrowObjectDisposedException(this);
+                }
             }
         }
 
@@ -98,7 +142,7 @@ namespace System.Threading
         public void Dispose()
         {
             GC.SuppressFinalize(this);
-            _timer.Close();
+            _timer.Dispose();
             _state.Signal(stopping: true);
         }
 
index 57b4e9d36db242b431ee399c873899d095472a3d..6cce3006175c0c84dbcaf06204be77804cd5f809 100644 (file)
@@ -536,22 +536,47 @@ namespace System.Threading.Tasks
         /// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for a cancellation request.</param>
         /// <returns>The <see cref="Task{TResult}"/> representing the asynchronous wait.  It may or may not be the same instance as the current instance.</returns>
         public new Task<TResult> WaitAsync(CancellationToken cancellationToken) =>
-            WaitAsync(Timeout.UnsignedInfinite, cancellationToken);
+            WaitAsync(Timeout.UnsignedInfinite, TimeProvider.System, cancellationToken);
 
         /// <summary>Gets a <see cref="Task{TResult}"/> that will complete when this <see cref="Task{TResult}"/> completes or when the specified timeout expires.</summary>
         /// <param name="timeout">The timeout after which the <see cref="Task"/> should be faulted with a <see cref="TimeoutException"/> if it hasn't otherwise completed.</param>
         /// <returns>The <see cref="Task{TResult}"/> representing the asynchronous wait.  It may or may not be the same instance as the current instance.</returns>
         public new Task<TResult> WaitAsync(TimeSpan timeout) =>
-            WaitAsync(ValidateTimeout(timeout, ExceptionArgument.timeout), default);
+            WaitAsync(ValidateTimeout(timeout, ExceptionArgument.timeout), TimeProvider.System, default);
+
+        /// <summary>
+        /// Gets a <see cref="Task{TResult}"/> that will complete when this <see cref="Task{TResult}"/> completes or when the specified timeout expires.
+        /// </summary>
+        /// <param name="timeout">The timeout after which the <see cref="Task"/> should be faulted with a <see cref="TimeoutException"/> if it hasn't otherwise completed.</param>
+        /// <param name="timeProvider">The <see cref="TimeProvider"/> with which to interpret <paramref name="timeout"/>.</param>
+        /// <returns>The <see cref="Task{TResult}"/> representing the asynchronous wait.  It may or may not be the same instance as the current instance.</returns>
+        public new Task<TResult> WaitAsync(TimeSpan timeout, TimeProvider timeProvider)
+        {
+            ArgumentNullException.ThrowIfNull(timeProvider);
+            return WaitAsync(ValidateTimeout(timeout, ExceptionArgument.timeout), timeProvider, default);
+        }
 
         /// <summary>Gets a <see cref="Task{TResult}"/> that will complete when this <see cref="Task{TResult}"/> completes, when the specified timeout expires, or when the specified <see cref="CancellationToken"/> has cancellation requested.</summary>
         /// <param name="timeout">The timeout after which the <see cref="Task"/> should be faulted with a <see cref="TimeoutException"/> if it hasn't otherwise completed.</param>
         /// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for a cancellation request.</param>
         /// <returns>The <see cref="Task{TResult}"/> representing the asynchronous wait.  It may or may not be the same instance as the current instance.</returns>
         public new Task<TResult> WaitAsync(TimeSpan timeout, CancellationToken cancellationToken) =>
-            WaitAsync(ValidateTimeout(timeout, ExceptionArgument.timeout), cancellationToken);
+            WaitAsync(ValidateTimeout(timeout, ExceptionArgument.timeout), TimeProvider.System, cancellationToken);
+
+        /// <summary>
+        /// Gets a <see cref="Task{TResult}"/> that will complete when this <see cref="Task{TResult}"/> completes, when the specified timeout expires, or when the specified <see cref="CancellationToken"/> has cancellation requested.
+        /// </summary>
+        /// <param name="timeout">The timeout after which the <see cref="Task"/> should be faulted with a <see cref="TimeoutException"/> if it hasn't otherwise completed.</param>
+        /// <param name="timeProvider">The <see cref="TimeProvider"/> with which to interpret <paramref name="timeout"/>.</param>
+        /// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for a cancellation request.</param>
+        /// <returns>The <see cref="Task{TResult}"/> representing the asynchronous wait.  It may or may not be the same instance as the current instance.</returns>
+        public new Task<TResult> WaitAsync(TimeSpan timeout, TimeProvider timeProvider, CancellationToken cancellationToken)
+        {
+            ArgumentNullException.ThrowIfNull(timeProvider);
+            return WaitAsync(ValidateTimeout(timeout, ExceptionArgument.timeout), timeProvider, cancellationToken);
+        }
 
-        private Task<TResult> WaitAsync(uint millisecondsTimeout, CancellationToken cancellationToken)
+        private Task<TResult> WaitAsync(uint millisecondsTimeout, TimeProvider timeProvider, CancellationToken cancellationToken)
         {
             if (IsCompleted || (!cancellationToken.CanBeCanceled && millisecondsTimeout == Timeout.UnsignedInfinite))
             {
@@ -568,7 +593,7 @@ namespace System.Threading.Tasks
                 return FromException<TResult>(new TimeoutException());
             }
 
-            return new CancellationPromise<TResult>(this, millisecondsTimeout, cancellationToken);
+            return new CancellationPromise<TResult>(this, millisecondsTimeout, timeProvider, cancellationToken);
         }
         #endregion
 
index 668bb0fdf078f28e33202e846dbeca8a93d75922..8b79ac9235f5cfa49332e24f304a04c94ccd19cb 100644 (file)
@@ -2777,21 +2777,44 @@ namespace System.Threading.Tasks
         /// <summary>Gets a <see cref="Task"/> that will complete when this <see cref="Task"/> completes or when the specified <see cref="CancellationToken"/> has cancellation requested.</summary>
         /// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for a cancellation request.</param>
         /// <returns>The <see cref="Task"/> representing the asynchronous wait.  It may or may not be the same instance as the current instance.</returns>
-        public Task WaitAsync(CancellationToken cancellationToken) => WaitAsync(Timeout.UnsignedInfinite, cancellationToken);
+        public Task WaitAsync(CancellationToken cancellationToken) => WaitAsync(Timeout.UnsignedInfinite, TimeProvider.System, cancellationToken);
 
         /// <summary>Gets a <see cref="Task"/> that will complete when this <see cref="Task"/> completes or when the specified timeout expires.</summary>
         /// <param name="timeout">The timeout after which the <see cref="Task"/> should be faulted with a <see cref="TimeoutException"/> if it hasn't otherwise completed.</param>
         /// <returns>The <see cref="Task"/> representing the asynchronous wait.  It may or may not be the same instance as the current instance.</returns>
-        public Task WaitAsync(TimeSpan timeout) => WaitAsync(ValidateTimeout(timeout, ExceptionArgument.timeout), default);
+        public Task WaitAsync(TimeSpan timeout) => WaitAsync(ValidateTimeout(timeout, ExceptionArgument.timeout), TimeProvider.System, default);
+
+        /// <summary>Gets a <see cref="Task"/> that will complete when this <see cref="Task"/> completes or when the specified timeout expires.</summary>
+        /// <param name="timeout">The timeout after which the <see cref="Task"/> should be faulted with a <see cref="TimeoutException"/> if it hasn't otherwise completed.</param>
+        /// <param name="timeProvider">The <see cref="TimeProvider"/> with which to interpret <paramref name="timeout"/>.</param>
+        /// <returns>The <see cref="Task"/> representing the asynchronous wait.  It may or may not be the same instance as the current instance.</returns>
+        /// <exception cref="System.ArgumentNullException">The <paramref name="timeProvider"/> argument is null.</exception>
+        public Task WaitAsync(TimeSpan timeout, TimeProvider timeProvider)
+        {
+            ArgumentNullException.ThrowIfNull(timeProvider);
+            return WaitAsync(ValidateTimeout(timeout, ExceptionArgument.timeout), timeProvider, default);
+        }
 
         /// <summary>Gets a <see cref="Task"/> that will complete when this <see cref="Task"/> completes, when the specified timeout expires, or when the specified <see cref="CancellationToken"/> has cancellation requested.</summary>
         /// <param name="timeout">The timeout after which the <see cref="Task"/> should be faulted with a <see cref="TimeoutException"/> if it hasn't otherwise completed.</param>
         /// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for a cancellation request.</param>
         /// <returns>The <see cref="Task"/> representing the asynchronous wait.  It may or may not be the same instance as the current instance.</returns>
         public Task WaitAsync(TimeSpan timeout, CancellationToken cancellationToken) =>
-            WaitAsync(ValidateTimeout(timeout, ExceptionArgument.timeout), cancellationToken);
+            WaitAsync(ValidateTimeout(timeout, ExceptionArgument.timeout), TimeProvider.System, cancellationToken);
 
-        private Task WaitAsync(uint millisecondsTimeout, CancellationToken cancellationToken)
+        /// <summary>Gets a <see cref="Task"/> that will complete when this <see cref="Task"/> completes, when the specified timeout expires, or when the specified <see cref="CancellationToken"/> has cancellation requested.</summary>
+        /// <param name="timeout">The timeout after which the <see cref="Task"/> should be faulted with a <see cref="TimeoutException"/> if it hasn't otherwise completed.</param>
+        /// <param name="timeProvider">The <see cref="TimeProvider"/> with which to interpret <paramref name="timeout"/>.</param>
+        /// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for a cancellation request.</param>
+        /// <returns>The <see cref="Task"/> representing the asynchronous wait.  It may or may not be the same instance as the current instance.</returns>
+        /// <exception cref="System.ArgumentNullException">The <paramref name="timeProvider"/> argument is null.</exception>
+        public Task WaitAsync(TimeSpan timeout, TimeProvider timeProvider, CancellationToken cancellationToken)
+        {
+            ArgumentNullException.ThrowIfNull(timeProvider);
+            return WaitAsync(ValidateTimeout(timeout, ExceptionArgument.timeout), timeProvider, cancellationToken);
+        }
+
+        private Task WaitAsync(uint millisecondsTimeout, TimeProvider timeProvider, CancellationToken cancellationToken)
         {
             if (IsCompleted || (!cancellationToken.CanBeCanceled && millisecondsTimeout == Timeout.UnsignedInfinite))
             {
@@ -2808,7 +2831,7 @@ namespace System.Threading.Tasks
                 return FromException(new TimeoutException());
             }
 
-            return new CancellationPromise<VoidTaskResult>(this, millisecondsTimeout, cancellationToken);
+            return new CancellationPromise<VoidTaskResult>(this, millisecondsTimeout, timeProvider, cancellationToken);
         }
 
         /// <summary>Task that's completed when another task, timeout, or cancellation token triggers.</summary>
@@ -2819,9 +2842,9 @@ namespace System.Threading.Tasks
             /// <summary>Cancellation registration used to unregister from the token source upon timeout or the task completing.</summary>
             private readonly CancellationTokenRegistration _registration;
             /// <summary>The timer used to implement the timeout.  It's stored so that it's rooted and so that we can dispose it upon cancellation or the task completing.</summary>
-            private readonly TimerQueueTimer? _timer;
+            private readonly ITimer? _timer;
 
-            internal CancellationPromise(Task source, uint millisecondsDelay, CancellationToken token)
+            internal CancellationPromise(Task source, uint millisecondsDelay, TimeProvider timeProvider, CancellationToken token)
             {
                 Debug.Assert(source != null);
                 Debug.Assert(millisecondsDelay != 0);
@@ -2833,14 +2856,26 @@ namespace System.Threading.Tasks
                 // Register with a timer if it's needed.
                 if (millisecondsDelay != Timeout.UnsignedInfinite)
                 {
-                    _timer = new TimerQueueTimer(static state =>
+                    TimerCallback callback = static state =>
                     {
                         var thisRef = (CancellationPromise<TResult>)state!;
                         if (thisRef.TrySetException(new TimeoutException()))
                         {
                             thisRef.Cleanup();
                         }
-                    }, this, millisecondsDelay, Timeout.UnsignedInfinite, flowExecutionContext: false);
+                    };
+
+                    if (timeProvider == TimeProvider.System)
+                    {
+                        _timer = new TimerQueueTimer(callback, this, millisecondsDelay, Timeout.UnsignedInfinite, flowExecutionContext: false);
+                    }
+                    else
+                    {
+                        using (ExecutionContext.SuppressFlow())
+                        {
+                            _timer = timeProvider.CreateTimer(callback, this, TimeSpan.FromMilliseconds(millisecondsDelay), Timeout.InfiniteTimeSpan);
+                        }
+                    }
                 }
 
                 // Register with the cancellation token.
@@ -2884,7 +2919,7 @@ namespace System.Threading.Tasks
             private void Cleanup()
             {
                 _registration.Dispose();
-                _timer?.Close();
+                _timer?.Dispose();
                 _task.RemoveContinuation(this);
             }
         }
@@ -5510,7 +5545,16 @@ namespace System.Threading.Tasks
         /// <remarks>
         /// After the specified time delay, the Task is completed in RanToCompletion state.
         /// </remarks>
-        public static Task Delay(TimeSpan delay) => Delay(delay, default);
+        public static Task Delay(TimeSpan delay) => Delay(delay, TimeProvider.System, default);
+
+        /// <summary>Creates a task that completes after a specified time interval.</summary>
+        /// <param name="delay">The <see cref="TimeSpan"/> to wait before completing the returned task, or <see cref="Timeout.InfiniteTimeSpan"/> to wait indefinitely.</param>
+        /// <param name="timeProvider">The <see cref="TimeProvider"/> with which to interpret <paramref name="delay"/>.</param>
+        /// <returns>A task that represents the time delay.</returns>
+        /// <exception cref="ArgumentOutOfRangeException"><paramref name="delay"/> represents a negative time interval other than <see cref="Timeout.InfiniteTimeSpan"/>.</exception>
+        /// <exception cref="ArgumentOutOfRangeException"><paramref name="delay"/>'s <see cref="TimeSpan.TotalMilliseconds"/> property is greater than 4294967294.</exception>
+        /// <exception cref="System.ArgumentNullException">The <paramref name="timeProvider"/> argument is null.</exception>
+        public static Task Delay(TimeSpan delay, TimeProvider timeProvider) => Delay(delay, timeProvider, default);
 
         /// <summary>
         /// Creates a Task that will complete after a time delay.
@@ -5531,7 +5575,21 @@ namespace System.Threading.Tasks
         /// delay has expired.
         /// </remarks>
         public static Task Delay(TimeSpan delay, CancellationToken cancellationToken) =>
-            Delay(ValidateTimeout(delay, ExceptionArgument.delay), cancellationToken);
+            Delay(delay, TimeProvider.System, cancellationToken);
+
+        /// <summary>Creates a cancellable task that completes after a specified time interval.</summary>
+        /// <param name="delay">The <see cref="TimeSpan"/> to wait before completing the returned task, or <see cref="Timeout.InfiniteTimeSpan"/> to wait indefinitely.</param>
+        /// <param name="timeProvider">The <see cref="TimeProvider"/> with which to interpret <paramref name="delay"/>.</param>
+        /// <param name="cancellationToken">A cancellation token to observe while waiting for the task to complete.</param>
+        /// <returns>A task that represents the time delay.</returns>
+        /// <exception cref="ArgumentOutOfRangeException"><paramref name="delay"/> represents a negative time interval other than <see cref="Timeout.InfiniteTimeSpan"/>.</exception>
+        /// <exception cref="ArgumentOutOfRangeException"><paramref name="delay"/>'s <see cref="TimeSpan.TotalMilliseconds"/> property is greater than 4294967294.</exception>
+        /// <exception cref="System.ArgumentNullException">The <paramref name="timeProvider"/> argument is null.</exception>
+        public static Task Delay(TimeSpan delay, TimeProvider timeProvider, CancellationToken cancellationToken)
+        {
+            ArgumentNullException.ThrowIfNull(timeProvider);
+            return Delay(ValidateTimeout(delay, ExceptionArgument.delay), timeProvider, cancellationToken);
+        }
 
         /// <summary>
         /// Creates a Task that will complete after a time delay.
@@ -5572,14 +5630,14 @@ namespace System.Threading.Tasks
                 ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.millisecondsDelay, ExceptionResource.Task_Delay_InvalidMillisecondsDelay);
             }
 
-            return Delay((uint)millisecondsDelay, cancellationToken);
+            return Delay((uint)millisecondsDelay, TimeProvider.System, cancellationToken);
         }
 
-        private static Task Delay(uint millisecondsDelay, CancellationToken cancellationToken) =>
+        private static Task Delay(uint millisecondsDelay, TimeProvider timeProvider, CancellationToken cancellationToken) =>
             cancellationToken.IsCancellationRequested ? FromCanceled(cancellationToken) :
             millisecondsDelay == 0 ? CompletedTask :
-            cancellationToken.CanBeCanceled ? new DelayPromiseWithCancellation(millisecondsDelay, cancellationToken) :
-            new DelayPromise(millisecondsDelay);
+            cancellationToken.CanBeCanceled ? new DelayPromiseWithCancellation(millisecondsDelay, timeProvider, cancellationToken) :
+            new DelayPromise(millisecondsDelay, timeProvider);
 
         internal static uint ValidateTimeout(TimeSpan timeout, ExceptionArgument argument)
         {
@@ -5596,9 +5654,9 @@ namespace System.Threading.Tasks
         private class DelayPromise : Task
         {
             private static readonly TimerCallback s_timerCallback = TimerCallback;
-            private readonly TimerQueueTimer? _timer;
+            private readonly ITimer? _timer;
 
-            internal DelayPromise(uint millisecondsDelay)
+            internal DelayPromise(uint millisecondsDelay, TimeProvider timeProvider)
             {
                 Debug.Assert(millisecondsDelay != 0);
 
@@ -5610,13 +5668,24 @@ namespace System.Threading.Tasks
 
                 if (millisecondsDelay != Timeout.UnsignedInfinite) // no need to create the timer if it's an infinite timeout
                 {
-                    _timer = new TimerQueueTimer(s_timerCallback, this, millisecondsDelay, Timeout.UnsignedInfinite, flowExecutionContext: false);
+                    if (timeProvider == TimeProvider.System)
+                    {
+                        _timer = new TimerQueueTimer(s_timerCallback, this, millisecondsDelay, Timeout.UnsignedInfinite, flowExecutionContext: false);
+                    }
+                    else
+                    {
+                        using (ExecutionContext.SuppressFlow())
+                        {
+                            _timer = timeProvider.CreateTimer(s_timerCallback, this, TimeSpan.FromMilliseconds(millisecondsDelay), Timeout.InfiniteTimeSpan);
+                        }
+                    }
+
                     if (IsCompleted)
                     {
                         // Handle rare race condition where the timer fires prior to our having stored it into the field, in which case
                         // the timer won't have been cleaned up appropriately.  This call to close might race with the Cleanup call to Close,
                         // but Close is thread-safe and will be a nop if it's already been closed.
-                        _timer.Close();
+                        _timer.Dispose();
                     }
                 }
             }
@@ -5638,7 +5707,7 @@ namespace System.Threading.Tasks
                 }
             }
 
-            protected virtual void Cleanup() => _timer?.Close();
+            protected virtual void Cleanup() => _timer?.Dispose();
         }
 
         /// <summary>DelayPromise that also supports cancellation.</summary>
@@ -5646,7 +5715,7 @@ namespace System.Threading.Tasks
         {
             private readonly CancellationTokenRegistration _registration;
 
-            internal DelayPromiseWithCancellation(uint millisecondsDelay, CancellationToken token) : base(millisecondsDelay)
+            internal DelayPromiseWithCancellation(uint millisecondsDelay, TimeProvider timeProvider, CancellationToken token) : base(millisecondsDelay, timeProvider)
             {
                 Debug.Assert(token.CanBeCanceled);
 
index 40ecd77c61b418bb1dd4bd73bd4c23e3a065fafd..5cd51ae4086fa397f72572138dfd3e50a0639861 100644 (file)
@@ -5,6 +5,7 @@ using System.Collections.Generic;
 using System.Diagnostics;
 using System.Diagnostics.CodeAnalysis;
 using System.Diagnostics.Tracing;
+using System.Runtime.CompilerServices;
 using System.Threading.Tasks;
 
 namespace System.Threading
@@ -444,7 +445,7 @@ namespace System.Threading
     // A timer in our TimerQueue.
     [DebuggerDisplay("{DisplayString,nq}")]
     [DebuggerTypeProxy(typeof(TimerDebuggerTypeProxy))]
-    internal sealed class TimerQueueTimer : IThreadPoolWorkItem
+    internal sealed class TimerQueueTimer : ITimer, IThreadPoolWorkItem
     {
         // The associated timer queue.
         private readonly TimerQueue _associatedTimerQueue;
@@ -484,6 +485,19 @@ namespace System.Threading
         internal bool _everQueued;
         private object? _notifyWhenNoCallbacksRunning; // may be either WaitHandle or Task
 
+        internal TimerQueueTimer(TimerCallback timerCallback, object? state, TimeSpan dueTime, TimeSpan period, bool flowExecutionContext) :
+            this(timerCallback, state, GetMilliseconds(dueTime), GetMilliseconds(period), flowExecutionContext)
+        {
+        }
+
+        private static uint GetMilliseconds(TimeSpan time, [CallerArgumentExpression("time")] string? parameter = null)
+        {
+            long tm = (long)time.TotalMilliseconds;
+            ArgumentOutOfRangeException.ThrowIfLessThan(tm, -1, parameter);
+            ArgumentOutOfRangeException.ThrowIfGreaterThan(tm, Timer.MaxSupportedTimeout, parameter);
+            return (uint)tm;
+        }
+
         internal TimerQueueTimer(TimerCallback timerCallback, object? state, uint dueTime, uint period, bool flowExecutionContext)
         {
             _timerCallback = timerCallback;
@@ -519,7 +533,10 @@ namespace System.Threading
             }
         }
 
-        internal bool Change(uint dueTime, uint period, bool throwIfDisposed = true)
+        public bool Change(TimeSpan dueTime, TimeSpan period) =>
+            Change(GetMilliseconds(dueTime), GetMilliseconds(period));
+
+        internal bool Change(uint dueTime, uint period)
         {
             bool success;
 
@@ -527,7 +544,6 @@ namespace System.Threading
             {
                 if (_canceled)
                 {
-                    ObjectDisposedException.ThrowIf(throwIfDisposed, this);
                     return false;
                 }
 
@@ -549,8 +565,7 @@ namespace System.Threading
             return success;
         }
 
-
-        public void Close()
+        public void Dispose()
         {
             lock (_associatedTimerQueue)
             {
@@ -562,8 +577,7 @@ namespace System.Threading
             }
         }
 
-
-        public bool Close(WaitHandle toSignal)
+        public bool Dispose(WaitHandle toSignal)
         {
             Debug.Assert(toSignal != null);
 
@@ -592,7 +606,7 @@ namespace System.Threading
             return success;
         }
 
-        public ValueTask CloseAsync()
+        public ValueTask DisposeAsync()
         {
             lock (_associatedTimerQueue)
             {
@@ -779,25 +793,25 @@ namespace System.Threading
 
         ~TimerHolder()
         {
-            _timer.Close();
+            _timer.Dispose();
         }
 
-        public void Close()
+        public void Dispose()
         {
-            _timer.Close();
+            _timer.Dispose();
             GC.SuppressFinalize(this);
         }
 
-        public bool Close(WaitHandle notifyObject)
+        public bool Dispose(WaitHandle notifyObject)
         {
-            bool result = _timer.Close(notifyObject);
+            bool result = _timer.Dispose(notifyObject);
             GC.SuppressFinalize(this);
             return result;
         }
 
-        public ValueTask CloseAsync()
+        public ValueTask DisposeAsync()
         {
-            ValueTask result = _timer.CloseAsync();
+            ValueTask result = _timer.DisposeAsync();
             GC.SuppressFinalize(this);
             return result;
         }
@@ -805,7 +819,7 @@ namespace System.Threading
 
     [DebuggerDisplay("{DisplayString,nq}")]
     [DebuggerTypeProxy(typeof(TimerQueueTimer.TimerDebuggerTypeProxy))]
-    public sealed class Timer : MarshalByRefObject, IDisposable, IAsyncDisposable
+    public sealed class Timer : MarshalByRefObject, IDisposable, IAsyncDisposable, ITimer
     {
         internal const uint MaxSupportedTimeout = 0xfffffffe;
 
@@ -899,10 +913,8 @@ namespace System.Threading
             return _timer._timer.Change((uint)dueTime, (uint)period);
         }
 
-        public bool Change(TimeSpan dueTime, TimeSpan period)
-        {
-            return Change((long)dueTime.TotalMilliseconds, (long)period.TotalMilliseconds);
-        }
+        public bool Change(TimeSpan dueTime, TimeSpan period) =>
+            _timer._timer.Change(dueTime, period);
 
         [CLSCompliant(false)]
         public bool Change(uint dueTime, uint period)
@@ -944,17 +956,17 @@ namespace System.Threading
         {
             ArgumentNullException.ThrowIfNull(notifyObject);
 
-            return _timer.Close(notifyObject);
+            return _timer.Dispose(notifyObject);
         }
 
         public void Dispose()
         {
-            _timer.Close();
+            _timer.Dispose();
         }
 
         public ValueTask DisposeAsync()
         {
-            return _timer.CloseAsync();
+            return _timer.DisposeAsync();
         }
 
         private string DisplayString => _timer._timer.DisplayString;
index 6a18583fdb46dac1137ad2d6211d37152736bcea..bf955c2f418a77c3f73c530a571e887e6529018b 100644 (file)
@@ -1849,6 +1849,19 @@ namespace System
         Friday = 5,
         Saturday = 6,
     }
+    public abstract class TimeProvider
+    {
+        public static TimeProvider System { get; }
+        protected TimeProvider(long timestampFrequency) { throw null; }
+        public abstract System.DateTimeOffset UtcNow { get; }
+        public System.DateTimeOffset LocalNow  { get; }
+        public abstract System.TimeZoneInfo LocalTimeZone { get; }
+        public long TimestampFrequency { get; }
+        public static TimeProvider FromLocalTimeZone(System.TimeZoneInfo timeZone) { throw null; }
+        public abstract long GetTimestamp();
+        public TimeSpan GetElapsedTime(long startingTimestamp, long endingTimestamp) { throw null; }
+        public abstract System.Threading.ITimer CreateTimer(System.Threading.TimerCallback callback, object? state, System.TimeSpan dueTime, System.TimeSpan period);
+    }
     public sealed partial class DBNull : System.IConvertible, System.Runtime.Serialization.ISerializable
     {
         internal DBNull() { }
@@ -14466,6 +14479,7 @@ namespace System.Threading
     public partial class CancellationTokenSource : System.IDisposable
     {
         public CancellationTokenSource() { }
+        public CancellationTokenSource(TimeSpan delay, TimeProvider timeProvider) { }
         public CancellationTokenSource(int millisecondsDelay) { }
         public CancellationTokenSource(System.TimeSpan delay) { }
         public bool IsCancellationRequested { get { throw null; } }
@@ -14491,6 +14505,7 @@ namespace System.Threading
     public sealed partial class PeriodicTimer : System.IDisposable
     {
         public PeriodicTimer(System.TimeSpan period) { }
+        public PeriodicTimer(TimeSpan period, TimeProvider timeProvider) { }
         public void Dispose() { }
         ~PeriodicTimer() { }
         public System.TimeSpan Period { get { throw null; } set { } }
@@ -14501,7 +14516,11 @@ namespace System.Threading
         public const int Infinite = -1;
         public static readonly System.TimeSpan InfiniteTimeSpan;
     }
-    public sealed partial class Timer : System.MarshalByRefObject, System.IAsyncDisposable, System.IDisposable
+    public interface ITimer : System.IDisposable, System.IAsyncDisposable
+    {
+        bool Change(System.TimeSpan dueTime, System.TimeSpan period);
+    }
+    public sealed partial class Timer : System.MarshalByRefObject, System.IAsyncDisposable, System.IDisposable, ITimer
     {
         public Timer(System.Threading.TimerCallback callback) { }
         public Timer(System.Threading.TimerCallback callback, object? state, int dueTime, int period) { }
@@ -14620,6 +14639,8 @@ namespace System.Threading.Tasks
         public static System.Threading.Tasks.Task Delay(int millisecondsDelay, System.Threading.CancellationToken cancellationToken) { throw null; }
         public static System.Threading.Tasks.Task Delay(System.TimeSpan delay) { throw null; }
         public static System.Threading.Tasks.Task Delay(System.TimeSpan delay, System.Threading.CancellationToken cancellationToken) { throw null; }
+        public static System.Threading.Tasks.Task Delay(System.TimeSpan delay, System.TimeProvider timeProvider) { throw null; }
+        public static System.Threading.Tasks.Task Delay(System.TimeSpan delay, System.TimeProvider timeProvider, System.Threading.CancellationToken cancellationToken) { throw null; }
         public void Dispose() { }
         protected virtual void Dispose(bool disposing) { }
         public static System.Threading.Tasks.Task FromCanceled(System.Threading.CancellationToken cancellationToken) { throw null; }
@@ -14664,6 +14685,8 @@ namespace System.Threading.Tasks
         public System.Threading.Tasks.Task WaitAsync(System.Threading.CancellationToken cancellationToken) { throw null; }
         public System.Threading.Tasks.Task WaitAsync(System.TimeSpan timeout) { throw null; }
         public System.Threading.Tasks.Task WaitAsync(System.TimeSpan timeout, System.Threading.CancellationToken cancellationToken) { throw null; }
+        public System.Threading.Tasks.Task WaitAsync(System.TimeSpan timeout, System.TimeProvider timeProvider) { throw null; }
+        public System.Threading.Tasks.Task WaitAsync(System.TimeSpan timeout, System.TimeProvider timeProvider, System.Threading.CancellationToken cancellationToken) { throw null; }
         public static System.Threading.Tasks.Task WhenAll(System.Collections.Generic.IEnumerable<System.Threading.Tasks.Task> tasks) { throw null; }
         public static System.Threading.Tasks.Task WhenAll(params System.Threading.Tasks.Task[] tasks) { throw null; }
         public static System.Threading.Tasks.Task<TResult[]> WhenAll<TResult>(System.Collections.Generic.IEnumerable<System.Threading.Tasks.Task<TResult>> tasks) { throw null; }
@@ -14965,6 +14988,8 @@ namespace System.Threading.Tasks
         public new System.Threading.Tasks.Task<TResult> WaitAsync(System.Threading.CancellationToken cancellationToken) { throw null; }
         public new System.Threading.Tasks.Task<TResult> WaitAsync(System.TimeSpan timeout) { throw null; }
         public new System.Threading.Tasks.Task<TResult> WaitAsync(System.TimeSpan timeout, System.Threading.CancellationToken cancellationToken) { throw null; }
+        public new System.Threading.Tasks.Task<TResult> WaitAsync(System.TimeSpan timeout, System.TimeProvider timeProvider) { throw null; }
+        public new System.Threading.Tasks.Task<TResult> WaitAsync(System.TimeSpan timeout, System.TimeProvider timeProvider, System.Threading.CancellationToken cancellationToken) { throw null; }
     }
     public static partial class TaskToAsyncResult
     {
index 0bb0ae98902317e50f6db4d219ed307ad12776b9..266ac502fa531921878b219af98456d4fd1440af 100644 (file)
 
      <!-- some tests require full ICU data, force it -->
     <WasmIncludeFullIcuData>true</WasmIncludeFullIcuData>
-    
+
     <!-- The test is looking for debugger attributes we would have stripped with NativeAOT -->
     <IlcKeepManagedDebuggerSupport>true</IlcKeepManagedDebuggerSupport>
   </PropertyGroup>
 
-  <!-- 
+  <!--
     Add special trait for running local timezone validation tests on certain devices e.g. in CI to make sure the local timezone is correct and not UTC. Disable these tests otherwise.
   -->
   <PropertyGroup>
@@ -42,6 +42,7 @@
     <Compile Include="$(CommonTestPath)System\Collections\TestBase.NonGeneric.cs" Link="Common\System\Collections\TestBase.NonGeneric.cs" />
     <Compile Include="$(CommonTestPath)System\Diagnostics\DebuggerAttributes.cs" Link="Common\System\Diagnostics\DebuggerAttributes.cs" />
     <Compile Include="$(CommonTestPath)Tests\System\StringTests.cs" Link="Common\System\StringTests.cs" />
+    <Compile Include="$(CommonTestPath)Tests\System\TimeProviderTests.cs" Link="Common\System\TimeProviderTests.cs" />
     <Compile Include="$(CommonTestPath)System\Collections\IDictionary.NonGeneric.Tests.cs" Link="Common\System\Collections\IDictionary.NonGeneric.Tests.cs" />
     <Compile Include="$(CommonTestPath)System\Collections\IList.NonGeneric.Tests.cs" Link="Common\System\Collections\IList.NonGeneric.Tests.cs" />
     <Compile Include="$(CommonTestPath)System\Collections\ICollection.NonGeneric.Tests.cs" Link="Common\System\Collections\ICollection.NonGeneric.Tests.cs" />
 
   <ItemGroup>
     <ProjectReference Include="$(LibrariesProjectRoot)System.Text.RegularExpressions\gen\System.Text.RegularExpressions.Generator.csproj" ReferenceOutputAssembly="false" SetTargetFramework="TargetFramework=netstandard2.0" OutputItemType="Analyzer" />
-    
+
     <PackageReference Include="Moq" Version="$(MoqVersion)" />
     <PackageReference Include="System.Runtime.Numerics.TestData" Version="$(SystemRuntimeNumericsTestDataVersion)" GeneratePathProperty="true" />
 
index 6de015852a8d03e9df765fb7673397e1cf23ec59..fdd62265a1680d682b5d5e202262911c4b85706f 100644 (file)
@@ -38,14 +38,15 @@ namespace System.Threading.Tests
         }
 
         [Fact]
-        public void Timer_Change_AfterDispose_Throws()
+        public void Timer_Change_AfterDispose_Test()
         {
             var t = new Timer(new TimerCallback(EmptyTimerTarget), null, 1, 1);
+            Assert.True(t.Change(1, 1));
             t.Dispose();
-            Assert.Throws<ObjectDisposedException>(() => t.Change(1, 1));
-            Assert.Throws<ObjectDisposedException>(() => t.Change(1L, 1L));
-            Assert.Throws<ObjectDisposedException>(() => t.Change(1u, 1u));
-            Assert.Throws<ObjectDisposedException>(() => t.Change(TimeSpan.FromMilliseconds(1), TimeSpan.FromMilliseconds(1)));
+            Assert.False(t.Change(1, 1));
+            Assert.False(t.Change(1L, 1L));
+            Assert.False(t.Change(1u, 1u));
+            Assert.False(t.Change(TimeSpan.FromMilliseconds(1), TimeSpan.FromMilliseconds(1)));
         }
 
         [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))]
index 4159f97c6928db41cb3cb03719af3d15a3213118..81cf68b4f2c6d898eae5b5ac19aca14e80874686 100644 (file)
@@ -28,7 +28,7 @@ namespace System.Threading.Tests
         {
             var t = new Timer(_ => { });
             await t.DisposeAsync();
-            Assert.Throws<ObjectDisposedException>(() => t.Change(-1, -1));
+            Assert.False(t.Change(-1, -1));
         }
 
         [Fact]
index c01c8b4e64419622766b34424608e6c8f2896ef3..cf6b97e0be20fbe69cabfd6015fb6686b3dbef25 100644 (file)
@@ -236,7 +236,7 @@ namespace System.Threading.Tests
             t.Dispose(allTicksCompleted);
             Assert.True(allTicksCompleted.WaitOne(MaxPositiveTimeoutInMs));
             Assert.Equal(0, tickCount);
-            Assert.Throws<ObjectDisposedException>(() => t.Change(0, 0));
+            Assert.False(t.Change(0, 0));
         }
 
         [OuterLoop("Incurs seconds delay to wait for events that should never happen")]