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 Part1 #83604

Merged
merged 7 commits into from
Mar 25, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions src/libraries/Common/src/System/ITimer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// 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="ObjectDisposedException">The timer has already been disposed.</exception>
tarekgh marked this conversation as resolved.
Show resolved Hide resolved
/// <exception cref="ArgumentOutOfRangeException">The <paramref name="dueTime"/> or <paramref name="period"/> parameter, in milliseconds, is less than -1 or greater than 4294967294.</exception>
bool Change(TimeSpan dueTime, TimeSpan period);
tarekgh marked this conversation as resolved.
Show resolved Hide resolved
}
}
208 changes: 208 additions & 0 deletions src/libraries/Common/src/System/TimeProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
// 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 _tickFrequency;
tarekgh marked this conversation as resolved.
Show resolved Hide resolved

/// <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"/>,
tarekgh marked this conversation as resolved.
Show resolved Hide resolved
/// and a timer based on <see cref="Timer"/>.
/// </summary>
public static TimeProvider System { get; } = new SystemTimeProvider(null);

/// <summary>
/// Initializes the instance with the timestamp frequency.
/// </summary>
/// <param name="timestampFrequency">Frequency of the values returned from <see cref="GetTimestamp"/> method. </param>
tarekgh marked this conversation as resolved.
Show resolved Hide resolved
tarekgh marked this conversation as resolved.
Show resolved Hide resolved
protected TimeProvider(long timestampFrequency)
{
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(timestampFrequency);
TimestampFrequency = timestampFrequency;
_tickFrequency = (double)TimeSpan.TicksPerSecond / TimestampFrequency;
stephentoub marked this conversation as resolved.
Show resolved Hide resolved
}

/// <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);
tarekgh marked this conversation as resolved.
Show resolved Hide resolved
}
}

/// <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>
tarekgh marked this conversation as resolved.
Show resolved Hide resolved
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) * _tickFrequency));
stephentoub marked this conversation as resolved.
Show resolved Hide resolved

/// <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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to consider some kind of out parameter that would allow this to communicate back whether all work has quiesced? Or are you thinking we'll investigate that as a follow-up? This is to allow, for example, CancellationTokenSource.TryReset to work with other timer implementations.

Copy link
Member Author

@tarekgh tarekgh Mar 23, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's have a follow up on that to see if there are any more things we need to consider. I'll create an issue for that.


/// <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 const uint MaxSupportedTimeout = 0xfffffffe;
tarekgh marked this conversation as resolved.
Show resolved Hide resolved

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)
stephentoub marked this conversation as resolved.
Show resolved Hide resolved
{
long dueTm = (long)dueTime.TotalMilliseconds;
ArgumentOutOfRangeException.ThrowIfLessThan(dueTm, -1, nameof(dueTime));
ArgumentOutOfRangeException.ThrowIfGreaterThan(dueTm, MaxSupportedTimeout, nameof(dueTime));

long periodTm = (long)periodTime.TotalMilliseconds;
ArgumentOutOfRangeException.ThrowIfLessThan(periodTm, -1, nameof(periodTime));
ArgumentOutOfRangeException.ThrowIfGreaterThan(periodTm, MaxSupportedTimeout, nameof(periodTime));

return ((uint)dueTm, (uint)periodTm);

tarekgh marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
}
}
Loading