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 7de0daf62cc6c6..6df8877031a3ee 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Threading/PeriodicTimer.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Threading/PeriodicTimer.cs @@ -22,19 +22,57 @@ public sealed class PeriodicTimer : IDisposable private readonly State _state; /// Initializes the timer. - /// The time interval between invocations of callback.. + /// The period between ticks /// must represent a number of milliseconds equal to or larger than 1, and smaller than . public PeriodicTimer(TimeSpan period) { - long ms = (long)period.TotalMilliseconds; - if (ms < 1 || ms > Timer.MaxSupportedTimeout) + if (!TryGetMilliseconds(period, out uint ms)) { GC.SuppressFinalize(this); throw new ArgumentOutOfRangeException(nameof(period)); } _state = new State(); - _timer = new TimerQueueTimer(s => ((State)s!).Signal(), _state, (uint)ms, (uint)ms, flowExecutionContext: false); + _timer = new TimerQueueTimer(s => ((State)s!).Signal(), _state, ms, ms, flowExecutionContext: false); + } + + /// Gets or sets the period between ticks. + /// must represent a number of milliseconds equal to or larger than 1, and smaller than . + /// + /// All prior ticks of the timer, including any that may be waiting to be consumed by , + /// are unaffected by changes to . Setting affects only and all subsequent times + /// at which the timer will tick. + /// + public TimeSpan Period + { + get => TimeSpan.FromMilliseconds(_timer._period); + set + { + if (!TryGetMilliseconds(value, out uint ms)) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + + _timer.Change(ms, ms); + } + } + + /// Tries to extract the number of milliseconds from . + /// + /// true if the number of milliseconds is extracted and stored into ; + /// false if the number of milliseconds would be out of range of a timer. + /// + private static bool TryGetMilliseconds(TimeSpan value, out uint milliseconds) + { + long ms = (long)value.TotalMilliseconds; + if (ms >= 1 && ms <= Timer.MaxSupportedTimeout) + { + milliseconds = (uint)ms; + return true; + } + + milliseconds = 0; + return false; } /// Wait for the next tick of the timer, or for the timer to be stopped. diff --git a/src/libraries/System.Runtime/ref/System.Runtime.cs b/src/libraries/System.Runtime/ref/System.Runtime.cs index 2de21393bbc2ba..764d0120d93199 100644 --- a/src/libraries/System.Runtime/ref/System.Runtime.cs +++ b/src/libraries/System.Runtime/ref/System.Runtime.cs @@ -14476,6 +14476,7 @@ public sealed partial class PeriodicTimer : System.IDisposable public PeriodicTimer(System.TimeSpan period) { } public void Dispose() { } ~PeriodicTimer() { } + public System.TimeSpan Period { get { throw null; } set { } } public System.Threading.Tasks.ValueTask WaitForNextTickAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } } public static partial class Timeout diff --git a/src/libraries/System.Runtime/tests/System/Threading/PeriodicTimerTests.cs b/src/libraries/System.Runtime/tests/System/Threading/PeriodicTimerTests.cs index 05cdd8e9222286..f96b0b1e01fac9 100644 --- a/src/libraries/System.Runtime/tests/System/Threading/PeriodicTimerTests.cs +++ b/src/libraries/System.Runtime/tests/System/Threading/PeriodicTimerTests.cs @@ -25,6 +25,43 @@ public void Ctor_ValidArguments_Succeeds(uint milliseconds) using var timer = new PeriodicTimer(TimeSpan.FromMilliseconds(milliseconds)); } + [Fact] + public void Period_InvalidArguments_Throws() + { + PeriodicTimer timer = new PeriodicTimer(TimeSpan.FromMilliseconds(1)); + AssertExtensions.Throws("value", () => timer.Period = TimeSpan.FromMilliseconds(-1)); + AssertExtensions.Throws("value", () => timer.Period = TimeSpan.Zero); + AssertExtensions.Throws("value", () => timer.Period = TimeSpan.FromMilliseconds(uint.MaxValue)); + + timer.Dispose(); + Assert.Throws(() => timer.Period = TimeSpan.FromMilliseconds(100)); + } + + [Fact] + public void Period_Roundtrips() + { + using PeriodicTimer timer = new PeriodicTimer(TimeSpan.FromMilliseconds(1)); + Assert.Equal(TimeSpan.FromMilliseconds(1), timer.Period); + + timer.Period = TimeSpan.FromDays(1); + Assert.Equal(TimeSpan.FromDays(1), timer.Period); + + AssertExtensions.Throws("value", () => timer.Period = TimeSpan.Zero); + Assert.Equal(TimeSpan.FromDays(1), timer.Period); + } + + [Fact] + public async void Period_AffectsPendingWaits() + { + using PeriodicTimer timer = new PeriodicTimer(TimeSpan.FromDays(40)); + + ValueTask task = timer.WaitForNextTickAsync(); + Assert.False(task.IsCompleted); + + timer.Period = TimeSpan.FromMilliseconds(1); + await task; + } + [Fact] public async Task Dispose_Idempotent() {