Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introducing Time abstraction Part2 (down-level support) #84235

Merged
merged 5 commits into from
Apr 7, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
75 changes: 70 additions & 5 deletions src/libraries/Common/src/System/TimeProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,15 @@ public abstract class TimeProvider
/// <param name="timestampFrequency">Frequency of the values returned from <see cref="GetTimestamp"/> method.</param>
protected TimeProvider(long timestampFrequency)
{
#if SYSTEM_PRIVATE_CORELIB
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(timestampFrequency);
#else
if (timestampFrequency <= 0)
{
throw new ArgumentOutOfRangeException(nameof(timestampFrequency), timestampFrequency, SR.Format(SR.ArgumentOutOfRange_Generic_MustBeNonNegativeNonZero, nameof(timestampFrequency)));
}
#endif // SYSTEM_PRIVATE_CORELIB

TimestampFrequency = timestampFrequency;
_timeToTicksRatio = (double)TimeSpan.TicksPerSecond / TimestampFrequency;
}
Expand All @@ -53,9 +61,9 @@ public DateTimeOffset LocalNow
TimeSpan offset = LocalTimeZone.GetUtcOffset(utcDateTime);

long localTicks = utcDateTime.Ticks + offset.Ticks;
if ((ulong)localTicks > DateTime.MaxTicks)
if ((ulong)localTicks > (ulong)DateTime.MaxValue.Ticks)
{
localTicks = localTicks < DateTime.MinTicks ? DateTime.MinTicks : DateTime.MaxTicks;
localTicks = localTicks < DateTime.MinValue.Ticks ? DateTime.MinValue.Ticks : DateTime.MaxValue.Ticks;
}
tarekgh marked this conversation as resolved.
Show resolved Hide resolved

return new DateTimeOffset(localTicks, offset);
Expand All @@ -82,7 +90,15 @@ public DateTimeOffset LocalNow
/// <exception cref="ArgumentNullException"><paramref name="timeZone"/> is null.</exception>
public static TimeProvider FromLocalTimeZone(TimeZoneInfo timeZone)
{
#if SYSTEM_PRIVATE_CORELIB
ArgumentNullException.ThrowIfNull(timeZone);
#else
if (timeZone is null)
{
throw new ArgumentNullException(nameof(timeZone));
}
#endif // SYSTEM_PRIVATE_CORELIB

return new SystemTimeProvider(timeZone);
}

Expand Down Expand Up @@ -155,7 +171,15 @@ private sealed class SystemTimeProvider : TimeProvider
/// <inheritdoc/>
public override ITimer CreateTimer(TimerCallback callback, object? state, TimeSpan dueTime, TimeSpan period)
{
#if SYSTEM_PRIVATE_CORELIB
ArgumentNullException.ThrowIfNull(callback);
#else
if (callback is null)
{
throw new ArgumentNullException(nameof(callback));
}
#endif // SYSTEM_PRIVATE_CORELIB

return new SystemTimeProviderTimer(dueTime, period, callback, state);
}

Expand All @@ -165,7 +189,7 @@ public override ITimer CreateTimer(TimerCallback callback, object? state, TimeSp
/// <inheritdoc/>
public override DateTimeOffset UtcNow => DateTimeOffset.UtcNow;

/// <summary>Thin wrapper for a <see cref="TimerQueueTimer"/>.</summary>
/// <summary>Thin wrapper for a <see cref="Timer"/>.</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.
Expand All @@ -174,12 +198,19 @@ public override ITimer CreateTimer(TimerCallback callback, object? state, TimeSp
/// </remarks>
private sealed class SystemTimeProviderTimer : ITimer
tarekgh marked this conversation as resolved.
Show resolved Hide resolved
{
#if SYSTEM_PRIVATE_CORELIB
private readonly TimerQueueTimer _timer;

#else
private readonly Timer _timer;
#endif // SYSTEM_PRIVATE_CORELIB
public SystemTimeProviderTimer(TimeSpan dueTime, TimeSpan period, TimerCallback callback, object? state)
{
(uint duration, uint periodTime) = CheckAndGetValues(dueTime, period);
#if SYSTEM_PRIVATE_CORELIB
_timer = new TimerQueueTimer(callback, state, duration, periodTime, flowExecutionContext: true);
#else
_timer = new Timer(callback, state, duration, periodTime);
tarekgh marked this conversation as resolved.
Show resolved Hide resolved
#endif // SYSTEM_PRIVATE_CORELIB
tarekgh marked this conversation as resolved.
Show resolved Hide resolved
}

public bool Change(TimeSpan dueTime, TimeSpan period)
Expand All @@ -190,17 +221,51 @@ public bool Change(TimeSpan dueTime, TimeSpan period)

public void Dispose() => _timer.Dispose();


#if SYSTEM_PRIVATE_CORELIB
public ValueTask DisposeAsync() => _timer.DisposeAsync();
#else
public ValueTask DisposeAsync()
{
_timer.Dispose();
return default;
}
#endif // SYSTEM_PRIVATE_CORELIB

private static (uint duration, uint periodTime) CheckAndGetValues(TimeSpan dueTime, TimeSpan periodTime)
{
long dueTm = (long)dueTime.TotalMilliseconds;
long periodTm = (long)periodTime.TotalMilliseconds;

#if SYSTEM_PRIVATE_CORELIB
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));
#else
const uint MaxSupportedTimeout = 0xfffffffe;

if (dueTm < -1)
{
throw new ArgumentOutOfRangeException(nameof(dueTime), dueTm, SR.Format(SR.ArgumentOutOfRange_Generic_MustBeGreaterOrEqual, nameof(dueTime), -1));
}

if (dueTm > MaxSupportedTimeout)
{
throw new ArgumentOutOfRangeException(nameof(dueTime), dueTm, SR.Format(SR.ArgumentOutOfRange_Generic_MustBeLessOrEqual, nameof(dueTime), MaxSupportedTimeout));
}

if (periodTm < -1)
{
throw new ArgumentOutOfRangeException(nameof(periodTm), periodTm, SR.Format(SR.ArgumentOutOfRange_Generic_MustBeGreaterOrEqual, nameof(periodTm), -1));
}

if (periodTm > MaxSupportedTimeout)
{
throw new ArgumentOutOfRangeException(nameof(periodTm), periodTm, SR.Format(SR.ArgumentOutOfRange_Generic_MustBeLessOrEqual, nameof(periodTm), MaxSupportedTimeout));
}
#endif // SYSTEM_PRIVATE_CORELIB

return ((uint)dueTm, (uint)periodTm);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,11 @@ public void TestSystemProviderWithTimeZone()
{
Assert.Equal(TimeZoneInfo.Local.Id, TimeProvider.System.LocalTimeZone.Id);

#if NETFRAMEWORK
TimeZoneInfo tzi = TimeZoneInfo.FindSystemTimeZoneById("Pacific Standard Time");
#else
TimeZoneInfo tzi = TimeZoneInfo.FindSystemTimeZoneById(OperatingSystem.IsWindows() ? "Pacific Standard Time" : "America/Los_Angeles");
#endif // NETFRAMEWORK

TimeProvider tp = TimeProvider.FromLocalTimeZone(tzi);
Assert.Equal(tzi.Id, tp.LocalTimeZone.Id);
Expand All @@ -57,6 +61,15 @@ public void TestSystemProviderWithTimeZone()
Assert.InRange(utcConvertedDto.Ticks, utcDto1.Ticks, utcDto2.Ticks);
}

#if NETFRAMEWORK
public static double s_tickFrequency = (double)TimeSpan.TicksPerSecond / Stopwatch.Frequency;
public static TimeSpan GetElapsedTime(long startingTimestamp, long endingTimestamp) =>
new TimeSpan((long)((endingTimestamp - startingTimestamp) * s_tickFrequency));
#else
public static TimeSpan GetElapsedTime(long startingTimestamp, long endingTimestamp) =>
Stopwatch.GetElapsedTime(startingTimestamp, endingTimestamp);
#endif // NETFRAMEWORK

[Fact]
public void TestSystemTimestamp()
{
Expand All @@ -68,7 +81,7 @@ public void TestSystemTimestamp()

Assert.InRange(providerTimestamp1, timestamp1, timestamp2);
Assert.True(providerTimestamp2 > timestamp2);
Assert.Equal(Stopwatch.GetElapsedTime(providerTimestamp1, providerTimestamp2), TimeProvider.System.GetElapsedTime(providerTimestamp1, providerTimestamp2));
Assert.Equal(GetElapsedTime(providerTimestamp1, providerTimestamp2), TimeProvider.System.GetElapsedTime(providerTimestamp1, providerTimestamp2));

Assert.Equal(Stopwatch.Frequency, TimeProvider.System.TimestampFrequency);
}
Expand Down Expand Up @@ -151,27 +164,54 @@ public static IEnumerable<object[]> TimersProvidersListData()
yield return new object[] { new FastClock() };
}

#if NETFRAMEWORK
private static void CancelAfter(TimeProvider provider, CancellationTokenSource cts, TimeSpan delay)
{
if (provider == TimeProvider.System)
{
cts.CancelAfter(delay);
}
else
{
ITimer timer = provider.CreateTimer(s => ((CancellationTokenSource)s).Cancel(), cts, delay, Timeout.InfiniteTimeSpan);
cts.Token.Register(t => ((ITimer)t).Dispose(), timer);
}
}
#endif // NETFRAMEWORK

[ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))]
[MemberData(nameof(TimersProvidersListData))]
public static void CancellationTokenSourceWithTimer(TimeProvider provider)
{
//
// Test out some int-based timeout logic
//
#if NETFRAMEWORK
CancellationTokenSource cts = new CancellationTokenSource(Timeout.InfiniteTimeSpan); // should be an infinite timeout
#else
CancellationTokenSource cts = new CancellationTokenSource(Timeout.InfiniteTimeSpan, provider); // should be an infinite timeout
#endif // NETFRAMEWORK
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)!");

#if NETFRAMEWORK
CancelAfter(provider, cts, TimeSpan.FromMilliseconds(1000000));
#else
cts.CancelAfter(1000000);
#endif // NETFRAMEWORK

Assert.False(token.IsCancellationRequested,
"CancellationTokenSourceWithTimer: Cancellation signaled on super-long timeout (int) !");

#if NETFRAMEWORK
CancelAfter(provider, cts, TimeSpan.FromMilliseconds(1));
#else
cts.CancelAfter(1);
#endif // NETFRAMEWORK

Debug.WriteLine("CancellationTokenSourceWithTimer: > About to wait on cancellation that should occur soon (int)... if we hang, something bad happened");

Expand All @@ -183,20 +223,33 @@ public static void CancellationTokenSourceWithTimer(TimeProvider provider)
// Test out some TimeSpan-based timeout logic
//
TimeSpan prettyLong = new TimeSpan(1, 0, 0);
#if NETFRAMEWORK
cts = new CancellationTokenSource(prettyLong);
#else
cts = new CancellationTokenSource(prettyLong, provider);
#endif // NETFRAMEWORK

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)!");

#if NETFRAMEWORK
CancelAfter(provider, cts, prettyLong);
#else
cts.CancelAfter(prettyLong);
#endif // NETFRAMEWORK

Assert.False(token.IsCancellationRequested,
"CancellationTokenSourceWithTimer: Cancellation signaled on super-long timeout (TimeSpan,2) !");

#if NETFRAMEWORK
CancelAfter(provider, cts, TimeSpan.FromMilliseconds(1000));
#else
cts.CancelAfter(new TimeSpan(1000));
#endif // NETFRAMEWORK

Debug.WriteLine("CancellationTokenSourceWithTimer: > About to wait on cancellation that should occur soon (TimeSpan)... if we hang, something bad happened");

Expand All @@ -213,8 +266,13 @@ public static void RunDelayTests(TimeProvider provider)
CancellationToken token = cts.Token;

// These should all complete quickly, with RAN_TO_COMPLETION status.
#if NETFRAMEWORK
Task task1 = provider.Delay(new TimeSpan(0));
Task task2 = provider.Delay(new TimeSpan(0), token);
#else
Task task1 = Task.Delay(new TimeSpan(0), provider);
Task task2 = Task.Delay(new TimeSpan(0), provider, token);
#endif // NETFRAMEWORK

Debug.WriteLine("RunDelayTests: > Waiting for 0-delayed uncanceled tasks to complete. If we hang, something went wrong.");
try
Expand All @@ -230,7 +288,11 @@ public static void RunDelayTests(TimeProvider provider)
Assert.True(task2.Status == TaskStatus.RanToCompletion, " > FAILED. Expected Delay(TimeSpan(0), timeProvider, uncanceledToken) to run to completion");

// This should take some time
#if NETFRAMEWORK
Task task3 = provider.Delay(TimeSpan.FromMilliseconds(20000));
#else
Task task3 = Task.Delay(TimeSpan.FromMilliseconds(20000), provider);
#endif // NETFRAMEWORK
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).");
Expand All @@ -242,16 +304,16 @@ public static async void RunWaitAsyncTests(TimeProvider provider)
{
CancellationTokenSource cts = new CancellationTokenSource();

var tcs1 = new TaskCompletionSource();
var tcs1 = new TaskCompletionSource<bool>();
Task task1 = tcs1.Task.WaitAsync(TimeSpan.FromDays(1), provider);
Assert.False(task1.IsCompleted);
tcs1.SetResult();
tcs1.SetResult(true);
await task1;

var tcs2 = new TaskCompletionSource();
var tcs2 = new TaskCompletionSource<bool>();
Task task2 = tcs2.Task.WaitAsync(TimeSpan.FromDays(1), provider, cts.Token);
Assert.False(task2.IsCompleted);
tcs2.SetResult();
tcs2.SetResult(true);
await task2;

var tcs3 = new TaskCompletionSource<int>();
Expand Down Expand Up @@ -291,6 +353,7 @@ public static async void RunWaitAsyncTests(TimeProvider provider)
Assert.Equal(200, await task8);
}

#if !NETFRAMEWORK
[ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))]
[MemberData(nameof(TimersProvidersListData))]
public static async void PeriodicTimerTests(TimeProvider provider)
Expand All @@ -304,6 +367,7 @@ public static async void PeriodicTimerTests(TimeProvider provider)
timer.Dispose();
Assert.False(timer.WaitForNextTickAsync().Result);
}
#endif // !NETFRAMEWORK

[Fact]
public static void NegativeTests()
Expand All @@ -316,9 +380,11 @@ public static void NegativeTests()
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)));

#if !NETFRAMEWORK
Assert.Throws<ArgumentNullException>(() => new CancellationTokenSource(Timeout.InfiniteTimeSpan, null));

Assert.Throws<ArgumentNullException>(() => new PeriodicTimer(TimeSpan.FromMilliseconds(1), null));
#endif // !NETFRAMEWORK
}

class TimerState
Expand Down Expand Up @@ -414,7 +480,16 @@ public bool Change(TimeSpan dueTime, TimeSpan period)
}

public void Dispose() => _timer.Dispose();

#if NETFRAMEWORK
public ValueTask DisposeAsync()
{
_timer.Dispose();
return default;
}
#else
public ValueTask DisposeAsync() => _timer.DisposeAsync();
#endif // NETFRAMEWORK
}
}
}