|
| 1 | +// Licensed to the .NET Foundation under one or more agreements. |
| 2 | +// The .NET Foundation licenses this file to you under the MIT license. |
| 3 | + |
| 4 | +using System.Diagnostics; |
| 5 | +using System.Threading; |
| 6 | +using System.Threading.Tasks; |
| 7 | + |
| 8 | +namespace System |
| 9 | +{ |
| 10 | + /// <summary>Provides an abstraction for time.</summary> |
| 11 | + public abstract class TimeProvider |
| 12 | + { |
| 13 | + private readonly double _timeToTicksRatio; |
| 14 | + |
| 15 | + /// <summary> |
| 16 | + /// Gets a <see cref="TimeProvider"/> that provides a clock based on <see cref="DateTimeOffset.UtcNow"/>, |
| 17 | + /// a time zone based on <see cref="TimeZoneInfo.Local"/>, a high-performance time stamp based on <see cref="Stopwatch"/>, |
| 18 | + /// and a timer based on <see cref="Timer"/>. |
| 19 | + /// </summary> |
| 20 | + /// <remarks> |
| 21 | + /// If the <see cref="TimeZoneInfo.Local"/> changes after the object is returned, the change will be reflected in any subsequent operations that retrieve <see cref="TimeProvider.LocalNow"/>. |
| 22 | + /// </remarks> |
| 23 | + public static TimeProvider System { get; } = new SystemTimeProvider(null); |
| 24 | + |
| 25 | + /// <summary> |
| 26 | + /// Initializes the instance with the timestamp frequency. |
| 27 | + /// </summary> |
| 28 | + /// <exception cref="ArgumentOutOfRangeException">The value of <paramref name="timestampFrequency"/> is negative or zero.</exception> |
| 29 | + /// <param name="timestampFrequency">Frequency of the values returned from <see cref="GetTimestamp"/> method.</param> |
| 30 | + protected TimeProvider(long timestampFrequency) |
| 31 | + { |
| 32 | + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(timestampFrequency); |
| 33 | + TimestampFrequency = timestampFrequency; |
| 34 | + _timeToTicksRatio = (double)TimeSpan.TicksPerSecond / TimestampFrequency; |
| 35 | + } |
| 36 | + |
| 37 | + /// <summary> |
| 38 | + /// Gets a <see cref="DateTimeOffset"/> value whose date and time are set to the current |
| 39 | + /// Coordinated Universal Time (UTC) date and time and whose offset is Zero, |
| 40 | + /// all according to this <see cref="TimeProvider"/>'s notion of time. |
| 41 | + /// </summary> |
| 42 | + public abstract DateTimeOffset UtcNow { get; } |
| 43 | + |
| 44 | + /// <summary> |
| 45 | + /// Gets a <see cref="DateTimeOffset"/> value that is set to the current date and time according to this <see cref="TimeProvider"/>'s |
| 46 | + /// notion of time based on <see cref="UtcNow"/>, with the offset set to the <see cref="LocalTimeZone"/>'s offset from Coordinated Universal Time (UTC). |
| 47 | + /// </summary> |
| 48 | + public DateTimeOffset LocalNow |
| 49 | + { |
| 50 | + get |
| 51 | + { |
| 52 | + DateTime utcDateTime = UtcNow.UtcDateTime; |
| 53 | + TimeSpan offset = LocalTimeZone.GetUtcOffset(utcDateTime); |
| 54 | + |
| 55 | + long localTicks = utcDateTime.Ticks + offset.Ticks; |
| 56 | + if ((ulong)localTicks > DateTime.MaxTicks) |
| 57 | + { |
| 58 | + localTicks = localTicks < DateTime.MinTicks ? DateTime.MinTicks : DateTime.MaxTicks; |
| 59 | + } |
| 60 | + |
| 61 | + return new DateTimeOffset(localTicks, offset); |
| 62 | + } |
| 63 | + } |
| 64 | + |
| 65 | + /// <summary> |
| 66 | + /// Gets a <see cref="TimeZoneInfo"/> object that represents the local time zone according to this <see cref="TimeProvider"/>'s notion of time. |
| 67 | + /// </summary> |
| 68 | + public abstract TimeZoneInfo LocalTimeZone { get; } |
| 69 | + |
| 70 | + /// <summary> |
| 71 | + /// Gets the frequency of <see cref="GetTimestamp"/> of high-frequency value per second. |
| 72 | + /// </summary> |
| 73 | + public long TimestampFrequency { get; } |
| 74 | + |
| 75 | + /// <summary> |
| 76 | + /// Creates a <see cref="TimeProvider"/> that provides a clock based on <see cref="DateTimeOffset.UtcNow"/>, |
| 77 | + /// a time zone based on <paramref name="timeZone"/>, a high-performance time stamp based on <see cref="Stopwatch"/>, |
| 78 | + /// and a timer based on <see cref="Timer"/>. |
| 79 | + /// </summary> |
| 80 | + /// <param name="timeZone">The time zone to use in getting the local time using <see cref="LocalNow"/>. </param> |
| 81 | + /// <returns>A new instance of <see cref="TimeProvider"/>. </returns> |
| 82 | + /// <exception cref="ArgumentNullException"><paramref name="timeZone"/> is null.</exception> |
| 83 | + public static TimeProvider FromLocalTimeZone(TimeZoneInfo timeZone) |
| 84 | + { |
| 85 | + ArgumentNullException.ThrowIfNull(timeZone); |
| 86 | + return new SystemTimeProvider(timeZone); |
| 87 | + } |
| 88 | + |
| 89 | + /// <summary> |
| 90 | + /// Gets the current high-frequency value designed to measure small time intervals with high accuracy in the timer mechanism. |
| 91 | + /// </summary> |
| 92 | + /// <returns>A long integer representing the high-frequency counter value of the underlying timer mechanism. </returns> |
| 93 | + public abstract long GetTimestamp(); |
| 94 | + |
| 95 | + /// <summary> |
| 96 | + /// Gets the elapsed time between two timestamps retrieved using <see cref="GetTimestamp"/>. |
| 97 | + /// </summary> |
| 98 | + /// <param name="startingTimestamp">The timestamp marking the beginning of the time period.</param> |
| 99 | + /// <param name="endingTimestamp">The timestamp marking the end of the time period.</param> |
| 100 | + /// <returns>A <see cref="TimeSpan"/> for the elapsed time between the starting and ending timestamps.</returns> |
| 101 | + public TimeSpan GetElapsedTime(long startingTimestamp, long endingTimestamp) => |
| 102 | + new TimeSpan((long)((endingTimestamp - startingTimestamp) * _timeToTicksRatio)); |
| 103 | + |
| 104 | + /// <summary>Creates a new <see cref="ITimer"/> instance, using <see cref="TimeSpan"/> values to measure time intervals.</summary> |
| 105 | + /// <param name="callback"> |
| 106 | + /// A delegate representing a method to be executed when the timer fires. The method specified for callback should be reentrant, |
| 107 | + /// as it may be invoked simultaneously on two threads if the timer fires again before or while a previous callback is still being handled. |
| 108 | + /// </param> |
| 109 | + /// <param name="state">An object to be passed to the <paramref name="callback"/>. This may be null.</param> |
| 110 | + /// <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> |
| 111 | + /// <param name="period">The time interval between invocations of <paramref name="callback"/>. Specify <see cref="Timeout.InfiniteTimeSpan"/> to disable periodic signaling.</param> |
| 112 | + /// <returns> |
| 113 | + /// The newly created <see cref="ITimer"/> instance. |
| 114 | + /// </returns> |
| 115 | + /// <exception cref="ArgumentNullException"><paramref name="callback"/> is null.</exception> |
| 116 | + /// <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> |
| 117 | + /// <remarks> |
| 118 | + /// <para> |
| 119 | + /// 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. |
| 120 | + /// </para> |
| 121 | + /// <para> |
| 122 | + /// 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, |
| 123 | + /// but can be re-enabled by calling the <see cref="ITimer.Change"/> method. |
| 124 | + /// </para> |
| 125 | + /// <para> |
| 126 | + /// 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, |
| 127 | + /// but can be re-enabled using the <see cref="ITimer.Change"/> method. |
| 128 | + /// </para> |
| 129 | + /// <para> |
| 130 | + /// The return <see cref="ITimer"/> instance will be implicitly rooted while the timer is still scheduled. |
| 131 | + /// </para> |
| 132 | + /// <para> |
| 133 | + /// <see cref="CreateTimer"/> captures the <see cref="ExecutionContext"/> and stores that with the <see cref="ITimer"/> for use in invoking <paramref name="callback"/> |
| 134 | + /// each time it's called. That capture can be suppressed with <see cref="ExecutionContext.SuppressFlow"/>. |
| 135 | + /// </para> |
| 136 | + /// </remarks> |
| 137 | + public abstract ITimer CreateTimer(TimerCallback callback, object? state, TimeSpan dueTime, TimeSpan period); |
| 138 | + |
| 139 | + /// <summary> |
| 140 | + /// Provides a default implementation of <see cref="TimeProvider"/> based on <see cref="DateTimeOffset.UtcNow"/>, |
| 141 | + /// <see cref="TimeZoneInfo.Local"/>, <see cref="Stopwatch"/>, and <see cref="Timer"/>. |
| 142 | + /// </summary> |
| 143 | + private sealed class SystemTimeProvider : TimeProvider |
| 144 | + { |
| 145 | + /// <summary>The time zone to treat as local. If null, <see cref="TimeZoneInfo.Local"/> is used.</summary> |
| 146 | + private readonly TimeZoneInfo? _localTimeZone; |
| 147 | + |
| 148 | + /// <summary>Initializes the instance.</summary> |
| 149 | + /// <param name="localTimeZone">The time zone to treat as local. If null, <see cref="TimeZoneInfo.Local"/> is used.</param> |
| 150 | + internal SystemTimeProvider(TimeZoneInfo? localTimeZone) : base(Stopwatch.Frequency) => _localTimeZone = localTimeZone; |
| 151 | + |
| 152 | + /// <inheritdoc/> |
| 153 | + public override TimeZoneInfo LocalTimeZone => _localTimeZone ?? TimeZoneInfo.Local; |
| 154 | + |
| 155 | + /// <inheritdoc/> |
| 156 | + public override ITimer CreateTimer(TimerCallback callback, object? state, TimeSpan dueTime, TimeSpan period) |
| 157 | + { |
| 158 | + ArgumentNullException.ThrowIfNull(callback); |
| 159 | + return new SystemTimeProviderTimer(dueTime, period, callback, state); |
| 160 | + } |
| 161 | + |
| 162 | + /// <inheritdoc/> |
| 163 | + public override long GetTimestamp() => Stopwatch.GetTimestamp(); |
| 164 | + |
| 165 | + /// <inheritdoc/> |
| 166 | + public override DateTimeOffset UtcNow => DateTimeOffset.UtcNow; |
| 167 | + |
| 168 | + /// <summary>Thin wrapper for a <see cref="TimerQueueTimer"/>.</summary> |
| 169 | + /// <remarks> |
| 170 | + /// We don't return a TimerQueueTimer directly as it implements IThreadPoolWorkItem and we don't |
| 171 | + /// want it exposed in a way that user code could directly queue the timer to the thread pool. |
| 172 | + /// We also use this instead of Timer because CreateTimer needs to return a timer that's implicitly |
| 173 | + /// rooted while scheduled. |
| 174 | + /// </remarks> |
| 175 | + private sealed class SystemTimeProviderTimer : ITimer |
| 176 | + { |
| 177 | + private readonly TimerQueueTimer _timer; |
| 178 | + |
| 179 | + public SystemTimeProviderTimer(TimeSpan dueTime, TimeSpan period, TimerCallback callback, object? state) |
| 180 | + { |
| 181 | + (uint duration, uint periodTime) = CheckAndGetValues(dueTime, period); |
| 182 | + _timer = new TimerQueueTimer(callback, state, duration, periodTime, flowExecutionContext: true); |
| 183 | + } |
| 184 | + |
| 185 | + public bool Change(TimeSpan dueTime, TimeSpan period) |
| 186 | + { |
| 187 | + (uint duration, uint periodTime) = CheckAndGetValues(dueTime, period); |
| 188 | + return _timer.Change(duration, periodTime); |
| 189 | + } |
| 190 | + |
| 191 | + public void Dispose() => _timer.Dispose(); |
| 192 | + |
| 193 | + public ValueTask DisposeAsync() => _timer.DisposeAsync(); |
| 194 | + |
| 195 | + private static (uint duration, uint periodTime) CheckAndGetValues(TimeSpan dueTime, TimeSpan periodTime) |
| 196 | + { |
| 197 | + long dueTm = (long)dueTime.TotalMilliseconds; |
| 198 | + ArgumentOutOfRangeException.ThrowIfLessThan(dueTm, -1, nameof(dueTime)); |
| 199 | + ArgumentOutOfRangeException.ThrowIfGreaterThan(dueTm, Timer.MaxSupportedTimeout, nameof(dueTime)); |
| 200 | + |
| 201 | + long periodTm = (long)periodTime.TotalMilliseconds; |
| 202 | + ArgumentOutOfRangeException.ThrowIfLessThan(periodTm, -1, nameof(periodTime)); |
| 203 | + ArgumentOutOfRangeException.ThrowIfGreaterThan(periodTm, Timer.MaxSupportedTimeout, nameof(periodTime)); |
| 204 | + |
| 205 | + return ((uint)dueTm, (uint)periodTm); |
| 206 | + } |
| 207 | + } |
| 208 | + } |
| 209 | + } |
| 210 | +} |
0 commit comments