diff --git a/src/libraries/Common/src/System/ITimer.cs b/src/libraries/Common/src/System/ITimer.cs
new file mode 100644
index 0000000000000..2dde8645f7b1c
--- /dev/null
+++ b/src/libraries/Common/src/System/ITimer.cs
@@ -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
+{
+ /// Represents a timer that can have its due time and period changed.
+ ///
+ /// Implementations of , , and
+ /// must all be thread-safe such that the timer instance may be accessed concurrently from multiple threads.
+ ///
+ public interface ITimer : IDisposable, IAsyncDisposable
+ {
+ /// Changes the start time and the interval between method invocations for a timer, using values to measure time intervals.
+ ///
+ /// A representing the amount of time to delay before invoking the callback method specified when the was constructed.
+ /// Specify to prevent the timer from restarting. Specify to restart the timer immediately.
+ ///
+ ///
+ /// The time interval between invocations of the callback method specified when the Timer was constructed.
+ /// Specify to disable periodic signaling.
+ ///
+ /// if the timer was successfully updated; otherwise, .
+ /// The or parameter, in milliseconds, is less than -1 or greater than 4294967294.
+ ///
+ /// It is the responsibility of the implementer of the ITimer interface to ensure thread safety.
+ ///
+ 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
index 0000000000000..316cde1dc3768
--- /dev/null
+++ b/src/libraries/Common/src/System/TimeProvider.cs
@@ -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
+{
+ /// Provides an abstraction for time.
+ public abstract class TimeProvider
+ {
+ private readonly double _timeToTicksRatio;
+
+ ///
+ /// Gets a that provides a clock based on ,
+ /// a time zone based on , a high-performance time stamp based on ,
+ /// and a timer based on .
+ ///
+ ///
+ /// If the changes after the object is returned, the change will be reflected in any subsequent operations that retrieve .
+ ///
+ public static TimeProvider System { get; } = new SystemTimeProvider(null);
+
+ ///
+ /// Initializes the instance with the timestamp frequency.
+ ///
+ /// The value of is negative or zero.
+ /// Frequency of the values returned from method.
+ protected TimeProvider(long timestampFrequency)
+ {
+ ArgumentOutOfRangeException.ThrowIfNegativeOrZero(timestampFrequency);
+ TimestampFrequency = timestampFrequency;
+ _timeToTicksRatio = (double)TimeSpan.TicksPerSecond / TimestampFrequency;
+ }
+
+ ///
+ /// Gets a 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 's notion of time.
+ ///
+ public abstract DateTimeOffset UtcNow { get; }
+
+ ///
+ /// Gets a value that is set to the current date and time according to this 's
+ /// notion of time based on , with the offset set to the 's offset from Coordinated Universal Time (UTC).
+ ///
+ 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);
+ }
+ }
+
+ ///
+ /// Gets a object that represents the local time zone according to this 's notion of time.
+ ///
+ public abstract TimeZoneInfo LocalTimeZone { get; }
+
+ ///
+ /// Gets the frequency of of high-frequency value per second.
+ ///
+ public long TimestampFrequency { get; }
+
+ ///
+ /// Creates a that provides a clock based on ,
+ /// a time zone based on , a high-performance time stamp based on ,
+ /// and a timer based on .
+ ///
+ /// The time zone to use in getting the local time using .
+ /// A new instance of .
+ /// is null.
+ public static TimeProvider FromLocalTimeZone(TimeZoneInfo timeZone)
+ {
+ ArgumentNullException.ThrowIfNull(timeZone);
+ return new SystemTimeProvider(timeZone);
+ }
+
+ ///
+ /// Gets the current high-frequency value designed to measure small time intervals with high accuracy in the timer mechanism.
+ ///
+ /// A long integer representing the high-frequency counter value of the underlying timer mechanism.
+ public abstract long GetTimestamp();
+
+ ///
+ /// Gets the elapsed time between two timestamps retrieved using .
+ ///
+ /// The timestamp marking the beginning of the time period.
+ /// The timestamp marking the end of the time period.
+ /// A for the elapsed time between the starting and ending timestamps.
+ public TimeSpan GetElapsedTime(long startingTimestamp, long endingTimestamp) =>
+ new TimeSpan((long)((endingTimestamp - startingTimestamp) * _timeToTicksRatio));
+
+ /// Creates a new instance, using values to measure time intervals.
+ ///
+ /// 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.
+ ///
+ /// An object to be passed to the . This may be null.
+ /// The amount of time to delay before is invoked. Specify to prevent the timer from starting. Specify to start the timer immediately.
+ /// The time interval between invocations of . Specify to disable periodic signaling.
+ ///
+ /// The newly created instance.
+ ///
+ /// is null.
+ /// The number of milliseconds in the value of or is negative and not equal to , or is greater than .
+ ///
+ ///
+ /// The delegate specified by the callback parameter is invoked once after elapses, and thereafter each time the time interval elapses.
+ ///
+ ///
+ /// If is zero, the callback is invoked immediately. If is -1 milliseconds, is not invoked; the timer is disabled,
+ /// but can be re-enabled by calling the method.
+ ///
+ ///
+ /// If is 0 or -1 milliseconds and is positive, is invoked once; the periodic behavior of the timer is disabled,
+ /// but can be re-enabled using the method.
+ ///
+ ///
+ /// The return instance will be implicitly rooted while the timer is still scheduled.
+ ///
+ ///
+ /// captures the and stores that with the for use in invoking
+ /// each time it's called. That capture can be suppressed with .
+ ///
+ ///
+ public abstract ITimer CreateTimer(TimerCallback callback, object? state, TimeSpan dueTime, TimeSpan period);
+
+ ///
+ /// Provides a default implementation of based on ,
+ /// , , and .
+ ///
+ private sealed class SystemTimeProvider : TimeProvider
+ {
+ /// The time zone to treat as local. If null, is used.
+ private readonly TimeZoneInfo? _localTimeZone;
+
+ /// Initializes the instance.
+ /// The time zone to treat as local. If null, is used.
+ internal SystemTimeProvider(TimeZoneInfo? localTimeZone) : base(Stopwatch.Frequency) => _localTimeZone = localTimeZone;
+
+ ///
+ public override TimeZoneInfo LocalTimeZone => _localTimeZone ?? TimeZoneInfo.Local;
+
+ ///
+ public override ITimer CreateTimer(TimerCallback callback, object? state, TimeSpan dueTime, TimeSpan period)
+ {
+ ArgumentNullException.ThrowIfNull(callback);
+ return new SystemTimeProviderTimer(dueTime, period, callback, state);
+ }
+
+ ///
+ public override long GetTimestamp() => Stopwatch.GetTimestamp();
+
+ ///
+ public override DateTimeOffset UtcNow => DateTimeOffset.UtcNow;
+
+ /// Thin wrapper for a .
+ ///
+ /// 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.
+ ///
+ 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
index 0000000000000..8b00756367d88
--- /dev/null
+++ b/src/libraries/Common/tests/Tests/System/TimeProviderTests.cs
@@ -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 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 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();
+ Task task3 = tcs3.Task.WaitAsync(TimeSpan.FromDays(1), provider);
+ Assert.False(task3.IsCompleted);
+ tcs3.SetResult(42);
+ Assert.Equal(42, await task3);
+
+ var tcs4 = new TaskCompletionSource();
+ Task 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(() => 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(() => task6.WaitAsync(TimeSpan.FromMilliseconds(10), provider, cts2.Token));
+ cts1.Cancel();
+ await task5;
+
+ using CancellationTokenSource cts3 = new CancellationTokenSource();
+ Task task7 = Task.Run(() => { while (!cts3.Token.IsCancellationRequested) { Thread.Sleep(10); } return 100; });
+ await Assert.ThrowsAsync(() => task7.WaitAsync(TimeSpan.FromMilliseconds(10), provider));
+ cts3.Cancel();
+ Assert.Equal(100, await task7);
+
+ using CancellationTokenSource cts4 = new CancellationTokenSource();
+ Task task8 = Task.Run(() => { while (!cts4.Token.IsCancellationRequested) { Thread.Sleep(10); } return 200; });
+ await Assert.ThrowsAsync(() => 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(() => new FastClock(-1)); // negative frequency
+ Assert.Throws(() => new FastClock(0)); // zero frequency
+ Assert.Throws(() => TimeProvider.FromLocalTimeZone(null));
+
+ Assert.Throws(() => TimeProvider.System.CreateTimer(null, null, Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan));
+ Assert.Throws(() => TimeProvider.System.CreateTimer(obj => { }, null, TimeSpan.FromMilliseconds(-2), Timeout.InfiniteTimeSpan));
+ Assert.Throws(() => TimeProvider.System.CreateTimer(obj => { }, null, Timeout.InfiniteTimeSpan, TimeSpan.FromMilliseconds(-2)));
+
+ Assert.Throws(() => new CancellationTokenSource(Timeout.InfiniteTimeSpan, null));
+
+ Assert.Throws(() => 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();
+ }
+ }
+}
diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPoolManager.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPoolManager.cs
index 9bc1af96d8835..db54690e4ac40 100644
--- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPoolManager.cs
+++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPoolManager.cs
@@ -443,18 +443,10 @@ public void Dispose()
/// Sets and based on the specified timeout.
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.
- }
}
/// Removes unusable connections from each pool, and removes stale pools entirely.
diff --git a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems
index 6f2d52f360447..7804a67acb277 100644
--- a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems
+++ b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems
@@ -1317,6 +1317,12 @@
Common\SkipLocalsInit.cs
+
+ Common\System\ITimer.cs
+
+
+ Common\System\TimeProvider.cs
+
Common\System\LocalAppContextSwitches.Common.cs
diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/CancellationTokenSource.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/CancellationTokenSource.cs
index b40c9033f865b..d6277386bd1c7 100644
--- a/src/libraries/System.Private.CoreLib/src/System/Threading/CancellationTokenSource.cs
+++ b/src/libraries/System.Private.CoreLib/src/System/Threading/CancellationTokenSource.cs
@@ -38,8 +38,8 @@ private static void TimerCallback(object? state) => // separated out into a name
private volatile int _state;
/// Whether this has been disposed.
private bool _disposed;
- /// TimerQueueTimer used by CancelAfter and Timer-related ctors. Used instead of Timer to avoid extra allocations and because the rooted behavior is desired.
- private volatile TimerQueueTimer? _timer;
+ /// ITimer used by CancelAfter and Timer-related ctors. Used instead of Timer to avoid extra allocations and because the rooted behavior is desired.
+ private volatile ITimer? _timer;
/// lazily initialized and returned from .
private volatile ManualResetEvent? _kernelEvent;
/// Registration state for the source.
@@ -137,15 +137,31 @@ public CancellationTokenSource() { }
/// canceled already.
///
///
- public CancellationTokenSource(TimeSpan delay)
+ public CancellationTokenSource(TimeSpan delay) : this(delay, TimeProvider.System)
{
+ }
+
+ /// Initializes a new instance of the class that will be canceled after the specified .
+ /// The time interval to wait before canceling this .
+ /// The with which to interpret the .
+ /// 's is less than -1 or greater than - 1.
+ /// is null.
+ ///
+ /// The countdown for the delay starts during the call to the constructor. When the delay expires,
+ /// the constructed is canceled, if it has
+ /// not been canceled already. Subsequent calls to CancelAfter will reset the delay for the constructed
+ /// , if it has not been canceled already.
+ ///
+ 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);
}
///
@@ -174,14 +190,14 @@ public CancellationTokenSource(int millisecondsDelay)
ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.millisecondsDelay);
}
- InitializeWithTimer((uint)millisecondsDelay);
+ InitializeWithTimer((uint)millisecondsDelay, TimeProvider.System);
}
///
/// Common initialization logic when constructing a CTS with a delay parameter.
/// A zero delay will result in immediate cancellation.
///
- private void InitializeWithTimer(uint millisecondsDelay)
+ private void InitializeWithTimer(uint millisecondsDelay, TimeProvider timeProvider)
{
if (millisecondsDelay == 0)
{
@@ -189,8 +205,17 @@ private void InitializeWithTimer(uint millisecondsDelay)
}
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 @@ private void CancelAfter(uint millisecondsDelay)
// 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 @@ private void CancelAfter(uint millisecondsDelay)
// 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);
}
///
@@ -455,9 +480,8 @@ public bool TryReset()
// 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 @@ protected virtual void Dispose(bool disposing)
// 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 @@ private bool TransitionToCancellationRequested()
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
diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/PeriodicTimer.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/PeriodicTimer.cs
index 27bba5ebfca30..b784d11935bec 100644
--- a/src/libraries/System.Private.CoreLib/src/System/Threading/PeriodicTimer.cs
+++ b/src/libraries/System.Private.CoreLib/src/System/Threading/PeriodicTimer.cs
@@ -18,9 +18,11 @@ namespace System.Threading
public sealed class PeriodicTimer : IDisposable
{
/// The underlying timer.
- private readonly TimerQueueTimer _timer;
+ private readonly ITimer _timer;
/// All state other than the _timer, so that the rooted timer's callback doesn't indirectly root itself by referring to _timer.
private readonly State _state;
+ /// The timer's current period.
+ private TimeSpan _period;
/// Initializes the timer.
/// The period between ticks
@@ -33,10 +35,48 @@ public PeriodicTimer(TimeSpan period)
throw new ArgumentOutOfRangeException(nameof(period));
}
+ _period = period;
_state = new State();
+
_timer = new TimerQueueTimer(s => ((State)s!).Signal(), _state, ms, ms, flowExecutionContext: false);
}
+ /// Initializes the timer.
+ /// The period between ticks
+ /// The used to interpret .
+ /// must be or represent a number of milliseconds equal to or larger than 1 and smaller than .
+ /// is null
+ 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);
+ }
+ }
+ }
+
/// Gets or sets the period between ticks.
/// must be or represent a number of milliseconds equal to or larger than 1 and smaller than .
///
@@ -46,15 +86,19 @@ public PeriodicTimer(TimeSpan period)
///
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 @@ public ValueTask WaitForNextTickAsync(CancellationToken cancellationToken
public void Dispose()
{
GC.SuppressFinalize(this);
- _timer.Close();
+ _timer.Dispose();
_state.Signal(stopping: true);
}
diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Future.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Future.cs
index a23260dfe9844..f893e8f5332b5 100644
--- a/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Future.cs
+++ b/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Future.cs
@@ -538,22 +538,47 @@ internal override void InnerInvoke()
/// The to monitor for a cancellation request.
/// The representing the asynchronous wait. It may or may not be the same instance as the current instance.
public new Task WaitAsync(CancellationToken cancellationToken) =>
- WaitAsync(Timeout.UnsignedInfinite, cancellationToken);
+ WaitAsync(Timeout.UnsignedInfinite, TimeProvider.System, cancellationToken);
/// Gets a that will complete when this completes or when the specified timeout expires.
/// The timeout after which the should be faulted with a if it hasn't otherwise completed.
/// The representing the asynchronous wait. It may or may not be the same instance as the current instance.
public new Task WaitAsync(TimeSpan timeout) =>
- WaitAsync(ValidateTimeout(timeout, ExceptionArgument.timeout), default);
+ WaitAsync(ValidateTimeout(timeout, ExceptionArgument.timeout), TimeProvider.System, default);
+
+ ///
+ /// Gets a that will complete when this completes or when the specified timeout expires.
+ ///
+ /// The timeout after which the should be faulted with a if it hasn't otherwise completed.
+ /// The with which to interpret .
+ /// The representing the asynchronous wait. It may or may not be the same instance as the current instance.
+ public new Task WaitAsync(TimeSpan timeout, TimeProvider timeProvider)
+ {
+ ArgumentNullException.ThrowIfNull(timeProvider);
+ return WaitAsync(ValidateTimeout(timeout, ExceptionArgument.timeout), timeProvider, default);
+ }
/// Gets a that will complete when this completes, when the specified timeout expires, or when the specified has cancellation requested.
/// The timeout after which the should be faulted with a if it hasn't otherwise completed.
/// The to monitor for a cancellation request.
/// The representing the asynchronous wait. It may or may not be the same instance as the current instance.
public new Task WaitAsync(TimeSpan timeout, CancellationToken cancellationToken) =>
- WaitAsync(ValidateTimeout(timeout, ExceptionArgument.timeout), cancellationToken);
+ WaitAsync(ValidateTimeout(timeout, ExceptionArgument.timeout), TimeProvider.System, cancellationToken);
+
+ ///
+ /// Gets a that will complete when this completes, when the specified timeout expires, or when the specified has cancellation requested.
+ ///
+ /// The timeout after which the should be faulted with a if it hasn't otherwise completed.
+ /// The with which to interpret .
+ /// The to monitor for a cancellation request.
+ /// The representing the asynchronous wait. It may or may not be the same instance as the current instance.
+ public new Task WaitAsync(TimeSpan timeout, TimeProvider timeProvider, CancellationToken cancellationToken)
+ {
+ ArgumentNullException.ThrowIfNull(timeProvider);
+ return WaitAsync(ValidateTimeout(timeout, ExceptionArgument.timeout), timeProvider, cancellationToken);
+ }
- private Task WaitAsync(uint millisecondsTimeout, CancellationToken cancellationToken)
+ private Task WaitAsync(uint millisecondsTimeout, TimeProvider timeProvider, CancellationToken cancellationToken)
{
if (IsCompleted || (!cancellationToken.CanBeCanceled && millisecondsTimeout == Timeout.UnsignedInfinite))
{
@@ -570,7 +595,7 @@ private Task WaitAsync(uint millisecondsTimeout, CancellationToken canc
return FromException(new TimeoutException());
}
- return new CancellationPromise(this, millisecondsTimeout, cancellationToken);
+ return new CancellationPromise(this, millisecondsTimeout, timeProvider, cancellationToken);
}
#endregion
diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs
index a306088726e9c..0ed3f5c72cd00 100644
--- a/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs
+++ b/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs
@@ -2781,21 +2781,44 @@ public bool Wait(int millisecondsTimeout, CancellationToken cancellationToken)
/// Gets a that will complete when this completes or when the specified has cancellation requested.
/// The to monitor for a cancellation request.
/// The representing the asynchronous wait. It may or may not be the same instance as the current instance.
- public Task WaitAsync(CancellationToken cancellationToken) => WaitAsync(Timeout.UnsignedInfinite, cancellationToken);
+ public Task WaitAsync(CancellationToken cancellationToken) => WaitAsync(Timeout.UnsignedInfinite, TimeProvider.System, cancellationToken);
/// Gets a that will complete when this completes or when the specified timeout expires.
/// The timeout after which the should be faulted with a if it hasn't otherwise completed.
/// The representing the asynchronous wait. It may or may not be the same instance as the current instance.
- public Task WaitAsync(TimeSpan timeout) => WaitAsync(ValidateTimeout(timeout, ExceptionArgument.timeout), default);
+ public Task WaitAsync(TimeSpan timeout) => WaitAsync(ValidateTimeout(timeout, ExceptionArgument.timeout), TimeProvider.System, default);
+
+ /// Gets a that will complete when this completes or when the specified timeout expires.
+ /// The timeout after which the should be faulted with a if it hasn't otherwise completed.
+ /// The with which to interpret .
+ /// The representing the asynchronous wait. It may or may not be the same instance as the current instance.
+ /// The argument is null.
+ public Task WaitAsync(TimeSpan timeout, TimeProvider timeProvider)
+ {
+ ArgumentNullException.ThrowIfNull(timeProvider);
+ return WaitAsync(ValidateTimeout(timeout, ExceptionArgument.timeout), timeProvider, default);
+ }
/// Gets a that will complete when this completes, when the specified timeout expires, or when the specified has cancellation requested.
/// The timeout after which the should be faulted with a if it hasn't otherwise completed.
/// The to monitor for a cancellation request.
/// The representing the asynchronous wait. It may or may not be the same instance as the current instance.
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)
+ /// Gets a that will complete when this completes, when the specified timeout expires, or when the specified has cancellation requested.
+ /// The timeout after which the should be faulted with a if it hasn't otherwise completed.
+ /// The with which to interpret .
+ /// The to monitor for a cancellation request.
+ /// The representing the asynchronous wait. It may or may not be the same instance as the current instance.
+ /// The argument is null.
+ 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))
{
@@ -2812,7 +2835,7 @@ private Task WaitAsync(uint millisecondsTimeout, CancellationToken cancellationT
return FromException(new TimeoutException());
}
- return new CancellationPromise(this, millisecondsTimeout, cancellationToken);
+ return new CancellationPromise(this, millisecondsTimeout, timeProvider, cancellationToken);
}
/// Task that's completed when another task, timeout, or cancellation token triggers.
@@ -2823,9 +2846,9 @@ private protected sealed class CancellationPromise : Task, ITa
/// Cancellation registration used to unregister from the token source upon timeout or the task completing.
private readonly CancellationTokenRegistration _registration;
/// 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.
- 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);
@@ -2837,14 +2860,26 @@ internal CancellationPromise(Task source, uint millisecondsDelay, CancellationTo
// Register with a timer if it's needed.
if (millisecondsDelay != Timeout.UnsignedInfinite)
{
- _timer = new TimerQueueTimer(static state =>
+ TimerCallback callback = static state =>
{
var thisRef = (CancellationPromise)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.
@@ -2888,7 +2923,7 @@ void ITaskCompletionAction.Invoke(Task completingTask)
private void Cleanup()
{
_registration.Dispose();
- _timer?.Close();
+ _timer?.Dispose();
_task.RemoveContinuation(this);
}
}
@@ -5519,7 +5554,16 @@ public static Task Run(Func?> function, Cancella
///
/// After the specified time delay, the Task is completed in RanToCompletion state.
///
- public static Task Delay(TimeSpan delay) => Delay(delay, default);
+ public static Task Delay(TimeSpan delay) => Delay(delay, TimeProvider.System, default);
+
+ /// Creates a task that completes after a specified time interval.
+ /// The to wait before completing the returned task, or to wait indefinitely.
+ /// The with which to interpret .
+ /// A task that represents the time delay.
+ /// represents a negative time interval other than .
+ /// 's property is greater than 4294967294.
+ /// The argument is null.
+ public static Task Delay(TimeSpan delay, TimeProvider timeProvider) => Delay(delay, timeProvider, default);
///
/// Creates a Task that will complete after a time delay.
@@ -5540,7 +5584,21 @@ public static Task Run(Func?> function, Cancella
/// delay has expired.
///
public static Task Delay(TimeSpan delay, CancellationToken cancellationToken) =>
- Delay(ValidateTimeout(delay, ExceptionArgument.delay), cancellationToken);
+ Delay(delay, TimeProvider.System, cancellationToken);
+
+ /// Creates a cancellable task that completes after a specified time interval.
+ /// The to wait before completing the returned task, or to wait indefinitely.
+ /// The with which to interpret .
+ /// A cancellation token to observe while waiting for the task to complete.
+ /// A task that represents the time delay.
+ /// represents a negative time interval other than .
+ /// 's property is greater than 4294967294.
+ /// The argument is null.
+ public static Task Delay(TimeSpan delay, TimeProvider timeProvider, CancellationToken cancellationToken)
+ {
+ ArgumentNullException.ThrowIfNull(timeProvider);
+ return Delay(ValidateTimeout(delay, ExceptionArgument.delay), timeProvider, cancellationToken);
+ }
///
/// Creates a Task that will complete after a time delay.
@@ -5581,14 +5639,14 @@ public static Task Delay(int millisecondsDelay, CancellationToken cancellationTo
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)
{
@@ -5605,9 +5663,9 @@ internal static uint ValidateTimeout(TimeSpan timeout, ExceptionArgument argumen
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);
@@ -5619,13 +5677,24 @@ internal DelayPromise(uint millisecondsDelay)
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();
}
}
}
@@ -5647,7 +5716,7 @@ private void CompleteTimedOut()
}
}
- protected virtual void Cleanup() => _timer?.Close();
+ protected virtual void Cleanup() => _timer?.Dispose();
}
/// DelayPromise that also supports cancellation.
@@ -5655,7 +5724,7 @@ private sealed class DelayPromiseWithCancellation : DelayPromise
{
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);
diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/Timer.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/Timer.cs
index 40ecd77c61b41..5cd51ae4086fa 100644
--- a/src/libraries/System.Private.CoreLib/src/System/Threading/Timer.cs
+++ b/src/libraries/System.Private.CoreLib/src/System/Threading/Timer.cs
@@ -5,6 +5,7 @@
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 @@ public void DeleteTimer(TimerQueueTimer timer)
// 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 @@ internal sealed class TimerQueueTimer : IThreadPoolWorkItem
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 @@ internal string DisplayString
}
}
- 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 @@ internal bool Change(uint dueTime, uint period, bool throwIfDisposed = true)
{
if (_canceled)
{
- ObjectDisposedException.ThrowIf(throwIfDisposed, this);
return false;
}
@@ -549,8 +565,7 @@ internal bool Change(uint dueTime, uint period, bool throwIfDisposed = true)
return success;
}
-
- public void Close()
+ public void Dispose()
{
lock (_associatedTimerQueue)
{
@@ -562,8 +577,7 @@ public void Close()
}
}
-
- public bool Close(WaitHandle toSignal)
+ public bool Dispose(WaitHandle toSignal)
{
Debug.Assert(toSignal != null);
@@ -592,7 +606,7 @@ public bool Close(WaitHandle toSignal)
return success;
}
- public ValueTask CloseAsync()
+ public ValueTask DisposeAsync()
{
lock (_associatedTimerQueue)
{
@@ -779,25 +793,25 @@ public TimerHolder(TimerQueueTimer timer)
~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 @@ public ValueTask CloseAsync()
[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 @@ public bool Change(int dueTime, int period)
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 @@ public bool Dispose(WaitHandle notifyObject)
{
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;
diff --git a/src/libraries/System.Runtime/ref/System.Runtime.cs b/src/libraries/System.Runtime/ref/System.Runtime.cs
index 53f5e047b232a..2a8e2960ab786 100644
--- a/src/libraries/System.Runtime/ref/System.Runtime.cs
+++ b/src/libraries/System.Runtime/ref/System.Runtime.cs
@@ -1849,6 +1849,19 @@ public enum DayOfWeek
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() { }
@@ -14459,6 +14472,7 @@ public void Dispose() { }
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; } }
@@ -14484,6 +14498,7 @@ public enum LazyThreadSafetyMode
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 { } }
@@ -14494,7 +14509,11 @@ public static partial class Timeout
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) { }
@@ -14613,6 +14632,8 @@ public Task(System.Action action, object? state, 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; }
@@ -14657,6 +14678,8 @@ public static void WaitAll(System.Threading.Tasks.Task[] tasks, System.Threading
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 tasks) { throw null; }
public static System.Threading.Tasks.Task WhenAll(params System.Threading.Tasks.Task[] tasks) { throw null; }
public static System.Threading.Tasks.Task WhenAll(System.Collections.Generic.IEnumerable> tasks) { throw null; }
@@ -14958,6 +14981,8 @@ public Task(System.Func function, System.Threading.Tasks.TaskCreationOp
public new System.Threading.Tasks.Task WaitAsync(System.Threading.CancellationToken cancellationToken) { throw null; }
public new System.Threading.Tasks.Task WaitAsync(System.TimeSpan timeout) { throw null; }
public new System.Threading.Tasks.Task WaitAsync(System.TimeSpan timeout, System.Threading.CancellationToken cancellationToken) { throw null; }
+ public new System.Threading.Tasks.Task WaitAsync(System.TimeSpan timeout, System.TimeProvider timeProvider) { throw null; }
+ public new System.Threading.Tasks.Task WaitAsync(System.TimeSpan timeout, System.TimeProvider timeProvider, System.Threading.CancellationToken cancellationToken) { throw null; }
}
public static partial class TaskToAsyncResult
{
diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests.csproj b/src/libraries/System.Runtime/tests/System.Runtime.Tests.csproj
index 91dbd5cc20545..0b474b78b91c6 100644
--- a/src/libraries/System.Runtime/tests/System.Runtime.Tests.csproj
+++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests.csproj
@@ -10,12 +10,12 @@
true
-
+
true
-
@@ -41,6 +41,7 @@
+
@@ -339,7 +340,7 @@
-
+
diff --git a/src/libraries/System.Threading.Timer/tests/TimerChangeTests.cs b/src/libraries/System.Threading.Timer/tests/TimerChangeTests.cs
index 6de015852a8d0..fdd62265a1680 100644
--- a/src/libraries/System.Threading.Timer/tests/TimerChangeTests.cs
+++ b/src/libraries/System.Threading.Timer/tests/TimerChangeTests.cs
@@ -38,14 +38,15 @@ public void Timer_Change_Period_OutOfRange_Throws()
}
[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(() => t.Change(1, 1));
- Assert.Throws(() => t.Change(1L, 1L));
- Assert.Throws(() => t.Change(1u, 1u));
- Assert.Throws(() => 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))]
diff --git a/src/libraries/System.Threading.Timer/tests/TimerDisposeTests.cs b/src/libraries/System.Threading.Timer/tests/TimerDisposeTests.cs
index 4159f97c6928d..81cf68b4f2c6d 100644
--- a/src/libraries/System.Threading.Timer/tests/TimerDisposeTests.cs
+++ b/src/libraries/System.Threading.Timer/tests/TimerDisposeTests.cs
@@ -28,7 +28,7 @@ public async Task DisposeAsync_DisposesTimer()
{
var t = new Timer(_ => { });
await t.DisposeAsync();
- Assert.Throws(() => t.Change(-1, -1));
+ Assert.False(t.Change(-1, -1));
}
[Fact]
diff --git a/src/libraries/System.Threading.Timer/tests/TimerFiringTests.cs b/src/libraries/System.Threading.Timer/tests/TimerFiringTests.cs
index c01c8b4e64419..cf6b97e0be20f 100644
--- a/src/libraries/System.Threading.Timer/tests/TimerFiringTests.cs
+++ b/src/libraries/System.Threading.Timer/tests/TimerFiringTests.cs
@@ -236,7 +236,7 @@ public void Timer_Dispose_WaitHandle()
t.Dispose(allTicksCompleted);
Assert.True(allTicksCompleted.WaitOne(MaxPositiveTimeoutInMs));
Assert.Equal(0, tickCount);
- Assert.Throws(() => t.Change(0, 0));
+ Assert.False(t.Change(0, 0));
}
[OuterLoop("Incurs seconds delay to wait for events that should never happen")]