Kit.Core/LibExternal/System.Reactive/Linq/Observable/Timer.cs

303 lines
13 KiB
C#

// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT License.
// See the LICENSE file in the project root for more information.
using System.Reactive.Concurrency;
using System.Reactive.Disposables;
using System.Threading;
namespace System.Reactive.Linq.ObservableImpl
{
internal static class Timer
{
internal abstract class Single : Producer<long, Single._>
{
private readonly IScheduler _scheduler;
protected Single(IScheduler scheduler)
{
_scheduler = scheduler;
}
internal sealed class Relative : Single
{
private readonly TimeSpan _dueTime;
public Relative(TimeSpan dueTime, IScheduler scheduler)
: base(scheduler)
{
_dueTime = dueTime;
}
protected override _ CreateSink(IObserver<long> observer) => new(observer);
protected override void Run(_ sink) => sink.Run(this, _dueTime);
}
internal sealed class Absolute : Single
{
private readonly DateTimeOffset _dueTime;
public Absolute(DateTimeOffset dueTime, IScheduler scheduler)
: base(scheduler)
{
_dueTime = dueTime;
}
protected override _ CreateSink(IObserver<long> observer) => new(observer);
protected override void Run(_ sink) => sink.Run(this, _dueTime);
}
internal sealed class _ : IdentitySink<long>
{
public _(IObserver<long> observer)
: base(observer)
{
}
public void Run(Single parent, DateTimeOffset dueTime)
{
SetUpstream(parent._scheduler.ScheduleAction(this, dueTime, state => state.Invoke()));
}
public void Run(Single parent, TimeSpan dueTime)
{
SetUpstream(parent._scheduler.ScheduleAction(this, dueTime, state => state.Invoke()));
}
private void Invoke()
{
ForwardOnNext(0);
ForwardOnCompleted();
}
}
}
internal abstract class Periodic : Producer<long, Periodic._>
{
private readonly TimeSpan _period;
private readonly IScheduler _scheduler;
protected Periodic(TimeSpan period, IScheduler scheduler)
{
_period = period;
_scheduler = scheduler;
}
internal sealed class Relative : Periodic
{
private readonly TimeSpan _dueTime;
public Relative(TimeSpan dueTime, TimeSpan period, IScheduler scheduler)
: base(period, scheduler)
{
_dueTime = dueTime;
}
protected override _ CreateSink(IObserver<long> observer) => new(_period, observer);
protected override void Run(_ sink) => sink.Run(this, _dueTime);
}
internal sealed class Absolute : Periodic
{
private readonly DateTimeOffset _dueTime;
public Absolute(DateTimeOffset dueTime, TimeSpan period, IScheduler scheduler)
: base(period, scheduler)
{
_dueTime = dueTime;
}
protected override _ CreateSink(IObserver<long> observer) => new(_period, observer);
protected override void Run(_ sink) => sink.Run(this, _dueTime);
}
internal sealed class _ : IdentitySink<long>
{
private readonly TimeSpan _period;
private long _index;
public _(TimeSpan period, IObserver<long> observer)
: base(observer)
{
_period = period;
}
public void Run(Periodic parent, DateTimeOffset dueTime)
{
SetUpstream(parent._scheduler.Schedule(this, dueTime, static (innerScheduler, @this) => @this.InvokeStart(innerScheduler)));
}
public void Run(Periodic parent, TimeSpan dueTime)
{
//
// Optimize for the case of Observable.Interval.
//
if (dueTime == _period)
{
SetUpstream(parent._scheduler.SchedulePeriodic(this, _period, static @this => @this.Tick()));
}
else
{
SetUpstream(parent._scheduler.Schedule(this, dueTime, static (innerScheduler, @this) => @this.InvokeStart(innerScheduler)));
}
}
//
// BREAKING CHANGE v2 > v1.x - No more correction for time drift based on absolute time. This
// didn't work for large period values anyway; the fractional
// error exceeded corrections. Also complicated dealing with system
// clock change conditions and caused numerous bugs.
//
// - For more precise scheduling, use a custom scheduler that measures TimeSpan values in a
// better way, e.g. spinning to make up for the last part of the period. Whether or not the
// values of the TimeSpan period match NT time or wall clock time is up to the scheduler.
//
// - For more accurate scheduling wrt the system clock, use Generate with DateTimeOffset time
// selectors. When the system clock changes, intervals will not be the same as diffs between
// consecutive absolute time values. The precision will be low (1s range by default).
//
private void Tick()
{
var count = _index;
_index = unchecked(count + 1);
ForwardOnNext(count);
}
private int _pendingTickCount;
private IDisposable? _periodic;
private IDisposable InvokeStart(IScheduler self)
{
//
// Notice the first call to OnNext will introduce skew if it takes significantly long when
// using the following naive implementation:
//
// Code: base._observer.OnNext(0L);
// return self.SchedulePeriodicEmulated(1L, _period, (Func<long, long>)Tick);
//
// What we're saying here is that Observable.Timer(dueTime, period) is pretty much the same
// as writing Observable.Timer(dueTime).Concat(Observable.Interval(period)).
//
// Expected: dueTime
// |
// 0--period--1--period--2--period--3--period--4--...
// |
// +-OnNext(0L)-|
//
// Actual: dueTime
// |
// 0------------#--period--1--period--2--period--3--period--4--...
// |
// +-OnNext(0L)-|
//
// Different solutions for this behavior have different problems:
//
// 1. Scheduling the periodic job first and using an AsyncLock to serialize the OnNext calls
// has the drawback that InvokeStart may never return. This happens when every callback
// doesn't meet the period's deadline, hence the periodic job keeps queueing stuff up. In
// this case, InvokeStart stays the owner of the AsyncLock and the call to Wait will never
// return, thus not allowing any interleaving of work on this scheduler's logical thread.
//
// 2. Scheduling the periodic job first and using a (blocking) synchronization primitive to
// signal completion of the OnNext(0L) call to the Tick call requires quite a bit of state
// and careful handling of the case when OnNext(0L) throws. What's worse is the blocking
// behavior inside Tick.
//
// In order to avoid blocking behavior, we need a scheme much like SchedulePeriodic emulation
// where work to dispatch OnNext(n + 1) is delegated to a catch up loop in case OnNext(n) was
// still running. Because SchedulePeriodic emulation exhibits such behavior in all cases, we
// only need to deal with the overlap of OnNext(0L) with future periodic OnNext(n) dispatch
// jobs. In the worst case where every callback takes longer than the deadline implied by the
// period, the periodic job will just queue up work that's dispatched by the tail-recursive
// catch up loop. In the best case, all work will be dispatched on the periodic scheduler.
//
//
// We start with one tick pending because we're about to start doing OnNext(0L).
//
_pendingTickCount = 1;
var d = new SingleAssignmentDisposable();
_periodic = d;
_index = 1;
d.Disposable = self.SchedulePeriodic(this, _period, static @this => @this.Tock());
try
{
ForwardOnNext(0L);
}
catch (Exception e)
{
d.Dispose();
e.Throw();
}
//
// If the periodic scheduling job already ran before we finished dispatching the OnNext(0L)
// call, we'll find pendingTickCount to be > 1. In this case, we need to catch up by dispatching
// subsequent calls to OnNext as fast as possible, but without running a loop in order to ensure
// fair play with the scheduler. So, we run a tail-recursive loop in CatchUp instead.
//
if (Interlocked.Decrement(ref _pendingTickCount) > 0)
{
var c = self.Schedule((@this: this, index: 1L), static (tuple, action) => tuple.@this.CatchUp(tuple.index, action));
return StableCompositeDisposable.Create(d, c);
}
return d;
}
private void Tock()
{
//
// Notice the handler for (emulated) periodic scheduling is non-reentrant.
//
// When there's no overlap with the OnNext(0L) call, the following code will cycle through
// pendingTickCount 0 -> 1 -> 0 for the remainder of the timer's execution.
//
// If there's overlap with the OnNext(0L) call, pendingTickCount will increase to record
// the number of catch up OnNext calls required, which will be dispatched by the recursive
// scheduling loop in CatchUp (which quits when it reaches 0 pending ticks).
//
if (Interlocked.Increment(ref _pendingTickCount) == 1)
{
var count = _index;
_index = unchecked(count + 1);
ForwardOnNext(count);
Interlocked.Decrement(ref _pendingTickCount);
}
}
private void CatchUp(long count, Action<(_, long)> recurse)
{
try
{
ForwardOnNext(count);
}
catch (Exception e)
{
_periodic!.Dispose(); // NB: _periodic is assigned before this runs.
e.Throw();
}
//
// We can simply bail out if we decreased the tick count to 0. In that case, the Tock
// method will take over when it sees the 0 -> 1 transition.
//
if (Interlocked.Decrement(ref _pendingTickCount) > 0)
{
recurse((this, unchecked(count + 1)));
}
}
}
}
}
}