Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,57 @@ public sealed class PeriodicTimer : IDisposable
private readonly State _state;

/// <summary>Initializes the timer.</summary>
/// <param name="period">The time interval between invocations of callback..</param>
/// <param name="period">The period between ticks</param>
/// <exception cref="ArgumentOutOfRangeException"><paramref name="period"/> must represent a number of milliseconds equal to or larger than 1, and smaller than <see cref="uint.MaxValue"/>.</exception>
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);
}

/// <summary>Gets or sets the period between ticks.</summary>
/// <exception cref="ArgumentOutOfRangeException"><paramref name="value"/> must represent a number of milliseconds equal to or larger than 1, and smaller than <see cref="uint.MaxValue"/>.</exception>
/// <remarks>
/// All prior ticks of the timer, including any that may be waiting to be consumed by <see cref="WaitForNextTickAsync"/>,
/// are unaffected by changes to <see cref="Period"/>. Setting <see cref="Period"/> affects only and all subsequent times
/// at which the timer will tick.
/// </remarks>
public TimeSpan Period
{
get => TimeSpan.FromMilliseconds(_timer._period);
set
{
if (!TryGetMilliseconds(value, out uint ms))
{
throw new ArgumentOutOfRangeException(nameof(value));
Copy link
Member

Choose a reason for hiding this comment

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

Should this be a throw helper to allow Period to be inlined or are we thinking its not going to be called frequently enough to matter?

Copy link
Member Author

Choose a reason for hiding this comment

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

I was thinking it'd be called infrequently enough to not matter.

}

_timer.Change(ms, ms);
}
}

/// <summary>Tries to extract the number of milliseconds from <paramref name="value"/>.</summary>
/// <returns>
/// true if the number of milliseconds is extracted and stored into <paramref name="milliseconds"/>;
/// false if the number of milliseconds would be out of range of a timer.
/// </returns>
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;
}

/// <summary>Wait for the next tick of the timer, or for the timer to be stopped.</summary>
Expand Down
1 change: 1 addition & 0 deletions src/libraries/System.Runtime/ref/System.Runtime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<bool> WaitForNextTickAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
}
public static partial class Timeout
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<ArgumentOutOfRangeException>("value", () => timer.Period = TimeSpan.FromMilliseconds(-1));
AssertExtensions.Throws<ArgumentOutOfRangeException>("value", () => timer.Period = TimeSpan.Zero);
AssertExtensions.Throws<ArgumentOutOfRangeException>("value", () => timer.Period = TimeSpan.FromMilliseconds(uint.MaxValue));

timer.Dispose();
Assert.Throws<ObjectDisposedException>(() => 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<ArgumentOutOfRangeException>("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<bool> task = timer.WaitForNextTickAsync();
Assert.False(task.IsCompleted);

timer.Period = TimeSpan.FromMilliseconds(1);
await task;
}

[Fact]
public async Task Dispose_Idempotent()
{
Expand Down