From 8a10697780a4f820c4093e3196b9cb2033b5618a Mon Sep 17 00:00:00 2001 From: edevoogd Date: Sun, 18 Apr 2021 00:16:25 +0000 Subject: [PATCH 01/13] Initial extraction of temporal context massaging and metronome abstraction. --- src/ClockQuantizer.cs | 80 +++++++------------ src/ClockQuantizerDriver.cs | 150 ++++++++++++++++++++++++++++++++++++ 2 files changed, 178 insertions(+), 52 deletions(-) create mode 100644 src/ClockQuantizerDriver.cs diff --git a/src/ClockQuantizer.cs b/src/ClockQuantizer.cs index dcaffd8..594dd2f 100644 --- a/src/ClockQuantizer.cs +++ b/src/ClockQuantizer.cs @@ -1,6 +1,6 @@ using System; using System.Runtime.CompilerServices; -using System.Threading.Tasks; +//using System.Threading.Tasks; namespace ClockQuantization { @@ -10,11 +10,10 @@ namespace ClockQuantization /// calls, as well as by and events. /// /// Under certain conditions, an advance operation may be incurred by calls. - public class ClockQuantizer : IAsyncDisposable, IDisposable + public class ClockQuantizer //: IAsyncDisposable, IDisposable { - private readonly ISystemClock _clock; + private readonly ClockQuantizerDriver _driver; private Interval? _currentInterval; - private System.Threading.Timer? _metronome; #region Fields & properties @@ -41,10 +40,10 @@ public class ClockQuantizer : IAsyncDisposable, IDisposable /// Returns the value of the reference clock. /// Depending on the actual reference clock implementation, this may or may not incur an expensive system call. - public DateTimeOffset UtcNow { get => _clock.UtcNow; } + public DateTimeOffset UtcNow { get => _driver.UtcNow; } /// Returns the value of the reference clock. - public long UtcNowClockOffset { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _clock.UtcNowClockOffset; } + public long UtcNowClockOffset { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _driver.UtcNowClockOffset; } #endregion @@ -57,7 +56,7 @@ public class ClockQuantizer : IAsyncDisposable, IDisposable /// The to convert /// An offset in clock-specific units. /// - public long DateTimeOffsetToClockOffset(DateTimeOffset offset) => _clock.DateTimeOffsetToClockOffset(offset); + public long DateTimeOffsetToClockOffset(DateTimeOffset offset) => _driver.DateTimeOffsetToClockOffset(offset); /// /// Converts an offset in clock-specific units (ticks) to a . @@ -66,7 +65,7 @@ public class ClockQuantizer : IAsyncDisposable, IDisposable /// A in UTC. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public DateTimeOffset ClockOffsetToUtcDateTimeOffset(long offset) => _clock.ClockOffsetToUtcDateTimeOffset(offset); + public DateTimeOffset ClockOffsetToUtcDateTimeOffset(long offset) => _driver.ClockOffsetToUtcDateTimeOffset(offset); /// /// Converts a to a count of clock-specific offset units (ticks). @@ -74,14 +73,14 @@ public class ClockQuantizer : IAsyncDisposable, IDisposable /// The to convert /// The amount of clock-specific offset units covering the . [MethodImpl(MethodImplOptions.AggressiveInlining)] - public long TimeSpanToClockOffsetUnits(TimeSpan timeSpan) => (long)(timeSpan.TotalMilliseconds * _clock.ClockOffsetUnitsPerMillisecond); + public long TimeSpanToClockOffsetUnits(TimeSpan timeSpan) => (long)(timeSpan.TotalMilliseconds * _driver.ClockOffsetUnitsPerMillisecond); /// /// Converts an amount of clock-specific offset units (ticks) to a . /// /// The amount of units to convert /// A covering the specified number of . - public TimeSpan ClockOffsetUnitsToTimeSpan(long units) => TimeSpan.FromMilliseconds((double)units / _clock.ClockOffsetUnitsPerMillisecond); + public TimeSpan ClockOffsetUnitsToTimeSpan(long units) => TimeSpan.FromMilliseconds((double)units / _driver.ClockOffsetUnitsPerMillisecond); #endregion @@ -223,26 +222,11 @@ internal NewIntervalEventArgs(DateTimeOffset offset, bool metronomic, TimeSpan? /// public ClockQuantizer(ISystemClock clock, TimeSpan maxIntervalTimeSpan) { - _clock = clock; MaxIntervalTimeSpan = maxIntervalTimeSpan; - bool metronomic = true; - if (clock is ISystemClockTemporalContext context) - { - context.ClockAdjusted += Context_ClockAdjusted; - if (context.ProvidesMetronome) - { - // Allow external "pulse" on metronome ticks - context.MetronomeTicked += Context_MetronomeTicked; - metronomic = false; - } - } - - if (metronomic) - { - // Create a suspended timer. Timer will be started at first call to Advance(). - _metronome = new System.Threading.Timer(Metronome_TimerCallback, null, System.Threading.Timeout.InfiniteTimeSpan, maxIntervalTimeSpan); - } + _driver = new ClockQuantizerDriver(clock, MaxIntervalTimeSpan); + _driver.ClockAdjusted += Driver_ClockAdjusted; + _driver.MetronomeTicked += Driver_MetronomeTicked; } private struct AdvancePreparationInfo @@ -268,11 +252,12 @@ private Interval Advance(bool metronomic) private AdvancePreparationInfo PrepareAdvance(bool metronomic) { + bool starting = false; + // Start metronome (if not imposed externally) on first Advance and consider first Advance as a metronomic event. - if (_currentInterval is null && _metronome is not null) + if (_currentInterval is null && _driver.TryEnsureMetronomeRunning(out starting)) { - metronomic = true; - _metronome.Change(MaxIntervalTimeSpan, MaxIntervalTimeSpan); + metronomic |= starting; } var previousInterval = _currentInterval; @@ -281,7 +266,7 @@ private AdvancePreparationInfo PrepareAdvance(bool metronomic) if (previousInterval is not null) { // Ignore potential *internal* metronome gap due to tiny clock jitter - if (!metronomic || _metronome is null) + if (!metronomic || (metronomic && (starting || !_driver.HasInternalMetronome))) { var gap = ClockOffsetUnitsToTimeSpan(interval.ClockOffset - previousInterval.ClockOffset) - MaxIntervalTimeSpan; if (gap > TimeSpan.Zero) @@ -291,7 +276,7 @@ private AdvancePreparationInfo PrepareAdvance(bool metronomic) } } - var e = new NewIntervalEventArgs(_clock.ClockOffsetToUtcDateTimeOffset(interval.ClockOffset), metronomic, detectedGap); + var e = new NewIntervalEventArgs(_driver.ClockOffsetToUtcDateTimeOffset(interval.ClockOffset), metronomic, detectedGap); return new AdvancePreparationInfo(interval, e); } @@ -303,7 +288,7 @@ private Interval CommitAdvance(AdvancePreparationInfo preparation) var e = preparation.EventArgs; if (e.IsMetronomic) { - NextMetronomicClockOffset = _clock.DateTimeOffsetToClockOffset(e.DateTimeOffset + MaxIntervalTimeSpan); + NextMetronomicClockOffset = _driver.DateTimeOffsetToClockOffset(e.DateTimeOffset + MaxIntervalTimeSpan); } OnAdvanced(e); @@ -316,13 +301,13 @@ private Interval CommitAdvance(AdvancePreparationInfo preparation) return preparation.Interval; } - private void Metronome_TimerCallback(object? _) => Context_MetronomeTicked(null, EventArgs.Empty); + private void Driver_MetronomeTicked(object? _, EventArgs __) => Advance(metronomic: true); - private void Context_MetronomeTicked(object? _, EventArgs __) => Advance(metronomic: true); + private void Driver_ClockAdjusted(object? _, EventArgs __) => Advance(metronomic: false); - private void Context_ClockAdjusted(object? _, EventArgs __) => Advance(metronomic: false); #region IAsyncDisposable/IDisposable +/* /// public void Dispose() @@ -345,22 +330,15 @@ protected virtual void Dispose(bool disposing) { if (disposing) { - _metronome?.Dispose(); + _driver.Dispose(); } - - _metronome = null; } /// protected virtual async ValueTask DisposeAsyncCore() { - if (_metronome is null) - { - goto done; - } - #if NETSTANDARD2_1 || NETCOREAPP3_0 || NETCOREAPP3_1 || NET5_0 || NET5_0_OR_GREATER - if (_metronome is IAsyncDisposable asyncDisposable) + if (_driver is IAsyncDisposable asyncDisposable) { await asyncDisposable.DisposeAsync().ConfigureAwait(false); goto finish; @@ -368,16 +346,14 @@ protected virtual async ValueTask DisposeAsyncCore() #else await default(ValueTask).ConfigureAwait(false); #endif - _metronome!.Dispose(); + _driver.Dispose(); #if NETSTANDARD2_1 || NETCOREAPP3_0 || NETCOREAPP3_1 || NET5_0 || NET5_0_OR_GREATER finish: -#endif - _metronome = null; -done: ; +#endif } - -#endregion +*/ + #endregion } } diff --git a/src/ClockQuantizerDriver.cs b/src/ClockQuantizerDriver.cs new file mode 100644 index 0000000..f5121b8 --- /dev/null +++ b/src/ClockQuantizerDriver.cs @@ -0,0 +1,150 @@ +using System; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; + + +namespace ClockQuantization +{ + // Isolate some of the metronome madness from the core ClockQuantizer implementation + internal class ClockQuantizerDriver : ClockQuantization.ISystemClock, ISystemClockTemporalContext, IAsyncDisposable, IDisposable + { + private readonly ClockQuantization.ISystemClock _clock; + private readonly TimeSpan _metronomeIntervalTimeSpan; + private System.Threading.Timer? _metronome; + + + public ClockQuantizerDriver(ClockQuantization.ISystemClock clock, TimeSpan metronomeIntervalTimeSpan) + { + _clock = clock; + _metronomeIntervalTimeSpan = metronomeIntervalTimeSpan; + + var haveExternalMetronome = false; + if (clock is ISystemClockTemporalContext context) + { + context.ClockAdjusted += Context_ClockAdjusted; + if (haveExternalMetronome = context.ProvidesMetronome) + { + // Allow external "pulse" on metronome ticks + context.MetronomeTicked += Context_MetronomeTicked; + } + } + + if (!haveExternalMetronome) + { + // Create a suspended timer. Timer will be started at first call to Advance(). + _metronome = new Timer(Metronome_TimerCallback, null, Timeout.InfiniteTimeSpan, metronomeIntervalTimeSpan); + } + } + + public bool TryEnsureMetronomeRunning(out bool starting) + { + starting = false; + if (HasInternalMetronome) + { + starting = true; + _metronome!.Change(_metronomeIntervalTimeSpan, _metronomeIntervalTimeSpan); + } + + return true; + } + + public bool HasInternalMetronome { get => _metronome is not null; } + + protected virtual void OnClockAdjusted(EventArgs e) => ClockAdjusted?.Invoke(this, e); + protected virtual void OnMetronomeTicked(EventArgs e) => MetronomeTicked?.Invoke(this, e); + + private void Context_ClockAdjusted(object? _, EventArgs __) => OnClockAdjusted(EventArgs.Empty); + + private void Metronome_TimerCallback(object? _) => Context_MetronomeTicked(null, EventArgs.Empty); + + private void Context_MetronomeTicked(object? _, EventArgs __) => OnMetronomeTicked(EventArgs.Empty); + + + #region ISystemClock + + /// + public DateTimeOffset UtcNow { get => _clock.UtcNow; } + /// + public long UtcNowClockOffset { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _clock.UtcNowClockOffset; } + /// + public long ClockOffsetUnitsPerMillisecond { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _clock.ClockOffsetUnitsPerMillisecond; } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public DateTimeOffset ClockOffsetToUtcDateTimeOffset(long offset) => _clock.ClockOffsetToUtcDateTimeOffset(offset); + /// + public long DateTimeOffsetToClockOffset(DateTimeOffset offset) => _clock.DateTimeOffsetToClockOffset(offset); + + #endregion + + #region ISystemClockTemporalContext + + /// + public bool ProvidesMetronome { get; } = true; + /// + public event EventHandler? ClockAdjusted; + /// + public event EventHandler? MetronomeTicked; + + #endregion + + #region IAsyncDisposable/IDisposable + + /// + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + /// + public async ValueTask DisposeAsync() + { + await DisposeAsyncCore(); + + Dispose(disposing: false); + GC.SuppressFinalize(this); + } + + /// + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + _metronome?.Dispose(); + } + + _metronome = null; + } + + /// + protected virtual async ValueTask DisposeAsyncCore() + { + if (_metronome is null) + { + goto done; + } + +#if NETSTANDARD2_1 || NETCOREAPP3_0 || NETCOREAPP3_1 || NET5_0 || NET5_0_OR_GREATER + if (_metronome is IAsyncDisposable asyncDisposable) + { + await asyncDisposable.DisposeAsync().ConfigureAwait(false); + goto finish; + } +#else + await default(ValueTask).ConfigureAwait(false); +#endif + _metronome!.Dispose(); + +#if NETSTANDARD2_1 || NETCOREAPP3_0 || NETCOREAPP3_1 || NET5_0 || NET5_0_OR_GREATER +finish: +#endif + _metronome = null; +done: + ; + } + + #endregion + } +} \ No newline at end of file From f050204cded8173cfccbc6da22053b4ef7ef33f1 Mon Sep 17 00:00:00 2001 From: edevoogd Date: Sun, 18 Apr 2021 02:46:57 +0200 Subject: [PATCH 02/13] Missing EOL at EOF --- src/ClockQuantizerDriver.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ClockQuantizerDriver.cs b/src/ClockQuantizerDriver.cs index f5121b8..1a54ee9 100644 --- a/src/ClockQuantizerDriver.cs +++ b/src/ClockQuantizerDriver.cs @@ -147,4 +147,4 @@ protected virtual async ValueTask DisposeAsyncCore() #endregion } -} \ No newline at end of file +} From b8f132163d22ef47f4176510656c3b82aef78b0e Mon Sep 17 00:00:00 2001 From: edevoogd Date: Sun, 18 Apr 2021 02:48:06 +0200 Subject: [PATCH 03/13] XMLDOC updates --- src/ClockQuantization.xml | 34 +++++++++++++++++++++++++++++----- src/TemporalContext.cs | 2 +- 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/src/ClockQuantization.xml b/src/ClockQuantization.xml index aca3ac3..7f2b138 100644 --- a/src/ClockQuantization.xml +++ b/src/ClockQuantization.xml @@ -163,16 +163,40 @@ the will pick up on external events, instead of relying on an internal metronome. - + - + - + - + + + + + + + + + + + + + + + + + + + + + + + + + @@ -280,7 +304,7 @@ - Converts to a in UTC. + Converts clock-specific to a in UTC. The offset to convert The corresponding diff --git a/src/TemporalContext.cs b/src/TemporalContext.cs index 9b67403..0e4058b 100644 --- a/src/TemporalContext.cs +++ b/src/TemporalContext.cs @@ -27,7 +27,7 @@ public interface ISystemClock long ClockOffsetUnitsPerMillisecond { get; } /// - /// Converts to a in UTC. + /// Converts clock-specific to a in UTC. /// /// The offset to convert /// The corresponding From a47b9e33c232d536c007cab1a8151b4c31ef186d Mon Sep 17 00:00:00 2001 From: edevoogd Date: Sun, 18 Apr 2021 13:22:25 +0200 Subject: [PATCH 04/13] First stab at timer quiescing support --- src/ClockQuantizer.cs | 9 ++++ src/ClockQuantizerDriver.cs | 101 +++++++++++++++++++++++++++++++----- 2 files changed, 97 insertions(+), 13 deletions(-) diff --git a/src/ClockQuantizer.cs b/src/ClockQuantizer.cs index 594dd2f..4ae74eb 100644 --- a/src/ClockQuantizer.cs +++ b/src/ClockQuantizer.cs @@ -229,6 +229,15 @@ public ClockQuantizer(ISystemClock clock, TimeSpan maxIntervalTimeSpan) _driver.MetronomeTicked += Driver_MetronomeTicked; } + + // Quiescing + public void Quiesce() => _driver.Quiesce(); + + public void Unquiesce() => _driver.Unquiesce(); + + + // Advance operations + private struct AdvancePreparationInfo { public Interval Interval; diff --git a/src/ClockQuantizerDriver.cs b/src/ClockQuantizerDriver.cs index 1a54ee9..df33fa1 100644 --- a/src/ClockQuantizerDriver.cs +++ b/src/ClockQuantizerDriver.cs @@ -6,34 +6,51 @@ namespace ClockQuantization { - // Isolate some of the metronome madness from the core ClockQuantizer implementation + // Isolate some of the context/metronome madness from the core ClockQuantizer implementation internal class ClockQuantizerDriver : ClockQuantization.ISystemClock, ISystemClockTemporalContext, IAsyncDisposable, IDisposable { private readonly ClockQuantization.ISystemClock _clock; private readonly TimeSpan _metronomeIntervalTimeSpan; private System.Threading.Timer? _metronome; - + private EventArgs? _pendingClockAdjustedEventArgs; public ClockQuantizerDriver(ClockQuantization.ISystemClock clock, TimeSpan metronomeIntervalTimeSpan) { _clock = clock; _metronomeIntervalTimeSpan = metronomeIntervalTimeSpan; - var haveExternalMetronome = false; + AttachExternalTemporalContext(this, clock, out var haveExternalMetronome); + + if (haveExternalMetronome) + { + IsQuiescent = false; + } + } + + private static void AttachExternalTemporalContext(ClockQuantizerDriver driver, ClockQuantization.ISystemClock clock, out bool haveExternalMetronome) + { + haveExternalMetronome = false; if (clock is ISystemClockTemporalContext context) { - context.ClockAdjusted += Context_ClockAdjusted; + context.ClockAdjusted += driver.Context_ClockAdjusted; if (haveExternalMetronome = context.ProvidesMetronome) { // Allow external "pulse" on metronome ticks - context.MetronomeTicked += Context_MetronomeTicked; + context.MetronomeTicked += driver.Context_MetronomeTicked; } } + } - if (!haveExternalMetronome) + private static void DetachExternalTemporalContext(ClockQuantizerDriver driver, ClockQuantization.ISystemClock clock) + { + if (clock is ISystemClockTemporalContext context) { - // Create a suspended timer. Timer will be started at first call to Advance(). - _metronome = new Timer(Metronome_TimerCallback, null, Timeout.InfiniteTimeSpan, metronomeIntervalTimeSpan); + context.ClockAdjusted -= driver.Context_ClockAdjusted; + if (context.ProvidesMetronome) + { + // Allow external "pulse" on metronome ticks + context.MetronomeTicked -= driver.Context_MetronomeTicked; + } } } @@ -42,17 +59,75 @@ public bool TryEnsureMetronomeRunning(out bool starting) starting = false; if (HasInternalMetronome) { - starting = true; - _metronome!.Change(_metronomeIntervalTimeSpan, _metronomeIntervalTimeSpan); + starting = IsQuiescent; + Unquiesce(); } return true; } - public bool HasInternalMetronome { get => _metronome is not null; } + protected bool IsQuiescent { get; private set; } = true; + + internal void Quiesce() + { + IsQuiescent = true; + // TODO: Ditch internal metronome, free unmanaged timer resources + _metronome = null; + } + + internal void Unquiesce() + { + EventArgs? pendingClockAdjustedEventArgs = Interlocked.Exchange(ref _pendingClockAdjustedEventArgs, null); - protected virtual void OnClockAdjusted(EventArgs e) => ClockAdjusted?.Invoke(this, e); - protected virtual void OnMetronomeTicked(EventArgs e) => MetronomeTicked?.Invoke(this, e); + if (pendingClockAdjustedEventArgs is not null) + { + // Make sure that we briefly postpone any metronome event that may occur during the process of unquiescing + lock (this) + { + IsQuiescent = false; // Set to false already to make sure that the ClockAdjusted event fires + OnClockAdjusted(pendingClockAdjustedEventArgs); + } + } + + IsQuiescent = false; + // TODO: Restore internal metronome, re-acquire unmanaged timer resources + if (HasInternalMetronome) + { + // Create a suspended timer. Timer will be started at first call to Advance(). + _metronome = new Timer(Metronome_TimerCallback, null, Timeout.InfiniteTimeSpan, _metronomeIntervalTimeSpan); + _metronome!.Change(_metronomeIntervalTimeSpan, _metronomeIntervalTimeSpan); + } + } + + public bool HasInternalMetronome + { + get => !(_clock is ISystemClockTemporalContext context && context.ProvidesMetronome); + } + + protected virtual void OnClockAdjusted(EventArgs e) + { + if (!IsQuiescent) + { + ClockAdjusted?.Invoke(this, e); + } + else + { + // Retain the latest ClockAdjusted event until we unquiesce + Interlocked.Exchange(ref _pendingClockAdjustedEventArgs, e); + } + } + + protected virtual void OnMetronomeTicked(EventArgs e) + { + if (!IsQuiescent) + { + // Make sure that we briefly postpone a metronome event that occurs during the process of unquiescing + lock (this) + { + MetronomeTicked?.Invoke(this, e); + } + } + } private void Context_ClockAdjusted(object? _, EventArgs __) => OnClockAdjusted(EventArgs.Empty); From 31ae1bc08809b4472eaac935f3b871950756a8c6 Mon Sep 17 00:00:00 2001 From: edevoogd Date: Sun, 18 Apr 2021 18:24:18 +0200 Subject: [PATCH 05/13] Minor cleanup --- src/ClockQuantizer.cs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/ClockQuantizer.cs b/src/ClockQuantizer.cs index 4ae74eb..9e5abf1 100644 --- a/src/ClockQuantizer.cs +++ b/src/ClockQuantizer.cs @@ -222,10 +222,8 @@ internal NewIntervalEventArgs(DateTimeOffset offset, bool metronomic, TimeSpan? /// public ClockQuantizer(ISystemClock clock, TimeSpan maxIntervalTimeSpan) { - MaxIntervalTimeSpan = maxIntervalTimeSpan; - - _driver = new ClockQuantizerDriver(clock, MaxIntervalTimeSpan); - _driver.ClockAdjusted += Driver_ClockAdjusted; + _driver = new ClockQuantizerDriver(clock, MaxIntervalTimeSpan = maxIntervalTimeSpan); + _driver.ClockAdjusted += Driver_ClockAdjusted; _driver.MetronomeTicked += Driver_MetronomeTicked; } @@ -236,7 +234,7 @@ public ClockQuantizer(ISystemClock clock, TimeSpan maxIntervalTimeSpan) public void Unquiesce() => _driver.Unquiesce(); - // Advance operations + // Advance primitives private struct AdvancePreparationInfo { From 3108b3e38ea20311a34b9557d59fbaa04a6724a0 Mon Sep 17 00:00:00 2001 From: edevoogd Date: Sun, 18 Apr 2021 21:35:02 +0200 Subject: [PATCH 06/13] Renaming and initial refinement of internal resource management and (async) disposal --- ...izerDriver.cs => TemporalContextDriver.cs} | 114 +++++++++++------- 1 file changed, 73 insertions(+), 41 deletions(-) rename src/{ClockQuantizerDriver.cs => TemporalContextDriver.cs} (65%) diff --git a/src/ClockQuantizerDriver.cs b/src/TemporalContextDriver.cs similarity index 65% rename from src/ClockQuantizerDriver.cs rename to src/TemporalContextDriver.cs index df33fa1..5ea2344 100644 --- a/src/ClockQuantizerDriver.cs +++ b/src/TemporalContextDriver.cs @@ -7,14 +7,14 @@ namespace ClockQuantization { // Isolate some of the context/metronome madness from the core ClockQuantizer implementation - internal class ClockQuantizerDriver : ClockQuantization.ISystemClock, ISystemClockTemporalContext, IAsyncDisposable, IDisposable + internal class TemporalContextDriver : ClockQuantization.ISystemClock, ISystemClockTemporalContext, IDisposable, IAsyncDisposable { private readonly ClockQuantization.ISystemClock _clock; private readonly TimeSpan _metronomeIntervalTimeSpan; private System.Threading.Timer? _metronome; private EventArgs? _pendingClockAdjustedEventArgs; - public ClockQuantizerDriver(ClockQuantization.ISystemClock clock, TimeSpan metronomeIntervalTimeSpan) + public TemporalContextDriver(ClockQuantization.ISystemClock clock, TimeSpan metronomeIntervalTimeSpan) { _clock = clock; _metronomeIntervalTimeSpan = metronomeIntervalTimeSpan; @@ -27,7 +27,7 @@ public ClockQuantizerDriver(ClockQuantization.ISystemClock clock, TimeSpan metro } } - private static void AttachExternalTemporalContext(ClockQuantizerDriver driver, ClockQuantization.ISystemClock clock, out bool haveExternalMetronome) + private static void AttachExternalTemporalContext(TemporalContextDriver driver, ClockQuantization.ISystemClock clock, out bool haveExternalMetronome) { haveExternalMetronome = false; if (clock is ISystemClockTemporalContext context) @@ -41,7 +41,7 @@ private static void AttachExternalTemporalContext(ClockQuantizerDriver driver, C } } - private static void DetachExternalTemporalContext(ClockQuantizerDriver driver, ClockQuantization.ISystemClock clock) + private static void DetachExternalTemporalContext(TemporalContextDriver driver, ClockQuantization.ISystemClock clock) { if (clock is ISystemClockTemporalContext context) { @@ -54,13 +54,59 @@ private static void DetachExternalTemporalContext(ClockQuantizerDriver driver, C } } + private void EnsureInternalMetronome(out bool starting) + { + starting = false; + + if (_metronome is null) + { + // Create a paused metronome timer + var metronome = new Timer(Metronome_TimerCallback, null, Timeout.InfiniteTimeSpan, _metronomeIntervalTimeSpan); + if (Interlocked.CompareExchange(ref _metronome, metronome, null) is null) + { + // Resume the newly created metronome timer + _metronome!.Change(_metronomeIntervalTimeSpan, _metronomeIntervalTimeSpan); + starting = true; + } + else + { + // Wooops... another thread outpaced us... + metronome.Dispose(); + } + } + } + + private void DisposeInternalMetronome() + { + if (Interlocked.Exchange(ref _metronome, null) is Timer metronome) + { + metronome.Dispose(); + } + } + + private async ValueTask DisposeInternalMetronomeAsync() + { + if (Interlocked.Exchange(ref _metronome, null) is Timer metronome) + { +#if NETSTANDARD2_1 || NETCOREAPP3_0 || NETCOREAPP3_1 || NET5_0 || NET5_0_OR_GREATER + if (_metronome is IAsyncDisposable asyncDisposable) + { + await asyncDisposable.DisposeAsync().ConfigureAwait(continueOnCapturedContext: false); + return; + } +#else + await default(ValueTask).ConfigureAwait(continueOnCapturedContext: false); +#endif + metronome.Dispose(); + } + } + public bool TryEnsureMetronomeRunning(out bool starting) { starting = false; if (HasInternalMetronome) { - starting = IsQuiescent; - Unquiesce(); + Unquiesce(out starting); } return true; @@ -68,15 +114,19 @@ public bool TryEnsureMetronomeRunning(out bool starting) protected bool IsQuiescent { get; private set; } = true; - internal void Quiesce() + public void Quiesce() { IsQuiescent = true; - // TODO: Ditch internal metronome, free unmanaged timer resources - _metronome = null; + + // Dispose of internal metronome, if applicable. + DisposeInternalMetronome(); } - internal void Unquiesce() + public void Unquiesce() => Unquiesce(out var _); + + private void Unquiesce(out bool starting) { + starting = false; EventArgs? pendingClockAdjustedEventArgs = Interlocked.Exchange(ref _pendingClockAdjustedEventArgs, null); if (pendingClockAdjustedEventArgs is not null) @@ -93,15 +143,14 @@ internal void Unquiesce() // TODO: Restore internal metronome, re-acquire unmanaged timer resources if (HasInternalMetronome) { - // Create a suspended timer. Timer will be started at first call to Advance(). - _metronome = new Timer(Metronome_TimerCallback, null, Timeout.InfiniteTimeSpan, _metronomeIntervalTimeSpan); - _metronome!.Change(_metronomeIntervalTimeSpan, _metronomeIntervalTimeSpan); + // Create a ** suspended ?? ** timer. Timer will be started at first call to Advance(). + EnsureInternalMetronome(out starting); } } public bool HasInternalMetronome { - get => !(_clock is ISystemClockTemporalContext context && context.ProvidesMetronome); + get => _clock is not ISystemClockTemporalContext context || !context.ProvidesMetronome; } protected virtual void OnClockAdjusted(EventArgs e) @@ -112,7 +161,7 @@ protected virtual void OnClockAdjusted(EventArgs e) } else { - // Retain the latest ClockAdjusted event until we unquiesce + // Hold back the latest ClockAdjusted event until we unquiesce Interlocked.Exchange(ref _pendingClockAdjustedEventArgs, e); } } @@ -183,41 +232,24 @@ public async ValueTask DisposeAsync() } /// + private int _disposed; protected virtual void Dispose(bool disposing) { - if (disposing) + if (Interlocked.CompareExchange(ref _disposed, 1, 0) == 0) { - _metronome?.Dispose(); - } + if (disposing) + { + DisposeInternalMetronome(); + } - _metronome = null; + DetachExternalTemporalContext(this, _clock); + } } /// protected virtual async ValueTask DisposeAsyncCore() { - if (_metronome is null) - { - goto done; - } - -#if NETSTANDARD2_1 || NETCOREAPP3_0 || NETCOREAPP3_1 || NET5_0 || NET5_0_OR_GREATER - if (_metronome is IAsyncDisposable asyncDisposable) - { - await asyncDisposable.DisposeAsync().ConfigureAwait(false); - goto finish; - } -#else - await default(ValueTask).ConfigureAwait(false); -#endif - _metronome!.Dispose(); - -#if NETSTANDARD2_1 || NETCOREAPP3_0 || NETCOREAPP3_1 || NET5_0 || NET5_0_OR_GREATER -finish: -#endif - _metronome = null; -done: - ; + await DisposeInternalMetronomeAsync(); } #endregion From c97d5ba27101a0e08fa1d9323556d91d7e3382c2 Mon Sep 17 00:00:00 2001 From: edevoogd Date: Sun, 18 Apr 2021 22:22:30 +0200 Subject: [PATCH 07/13] Reinstate (async) disposal for ClockQuantizer --- src/ClockQuantizer.cs | 42 ++++++++++++++++-------------------- src/TemporalContextDriver.cs | 4 ++-- 2 files changed, 21 insertions(+), 25 deletions(-) diff --git a/src/ClockQuantizer.cs b/src/ClockQuantizer.cs index 9e5abf1..57ef2eb 100644 --- a/src/ClockQuantizer.cs +++ b/src/ClockQuantizer.cs @@ -1,6 +1,7 @@ using System; using System.Runtime.CompilerServices; -//using System.Threading.Tasks; +using System.Threading; +using System.Threading.Tasks; namespace ClockQuantization { @@ -10,9 +11,9 @@ namespace ClockQuantization /// calls, as well as by and events. /// /// Under certain conditions, an advance operation may be incurred by calls. - public class ClockQuantizer //: IAsyncDisposable, IDisposable + public class ClockQuantizer : IAsyncDisposable, IDisposable { - private readonly ClockQuantizerDriver _driver; + private readonly TemporalContextDriver _driver; private Interval? _currentInterval; @@ -222,7 +223,7 @@ internal NewIntervalEventArgs(DateTimeOffset offset, bool metronomic, TimeSpan? /// public ClockQuantizer(ISystemClock clock, TimeSpan maxIntervalTimeSpan) { - _driver = new ClockQuantizerDriver(clock, MaxIntervalTimeSpan = maxIntervalTimeSpan); + _driver = new TemporalContextDriver(clock, MaxIntervalTimeSpan = maxIntervalTimeSpan); _driver.ClockAdjusted += Driver_ClockAdjusted; _driver.MetronomeTicked += Driver_MetronomeTicked; } @@ -314,11 +315,13 @@ private Interval CommitAdvance(AdvancePreparationInfo preparation) #region IAsyncDisposable/IDisposable -/* /// public void Dispose() { +// _driver.ClockAdjusted -= Driver_ClockAdjusted; +// _driver.MetronomeTicked -= Driver_MetronomeTicked; + Dispose(disposing: true); GC.SuppressFinalize(this); } @@ -326,41 +329,34 @@ public void Dispose() /// public async ValueTask DisposeAsync() { +// _driver.ClockAdjusted -= Driver_ClockAdjusted; +// _driver.MetronomeTicked -= Driver_MetronomeTicked; + await DisposeAsyncCore(); Dispose(disposing: false); GC.SuppressFinalize(this); } + private int _disposed; /// protected virtual void Dispose(bool disposing) { - if (disposing) + if (Interlocked.CompareExchange(ref _disposed, 1, 0) == 0) { - _driver.Dispose(); + if (disposing) + { + _driver.Dispose(); + } } } /// protected virtual async ValueTask DisposeAsyncCore() { -#if NETSTANDARD2_1 || NETCOREAPP3_0 || NETCOREAPP3_1 || NET5_0 || NET5_0_OR_GREATER - if (_driver is IAsyncDisposable asyncDisposable) - { - await asyncDisposable.DisposeAsync().ConfigureAwait(false); - goto finish; - } -#else - await default(ValueTask).ConfigureAwait(false); -#endif - _driver.Dispose(); - -#if NETSTANDARD2_1 || NETCOREAPP3_0 || NETCOREAPP3_1 || NET5_0 || NET5_0_OR_GREATER -finish: - ; -#endif + await _driver.DisposeAsync().ConfigureAwait(false); } -*/ + #endregion } } diff --git a/src/TemporalContextDriver.cs b/src/TemporalContextDriver.cs index 5ea2344..a5e365b 100644 --- a/src/TemporalContextDriver.cs +++ b/src/TemporalContextDriver.cs @@ -58,7 +58,7 @@ private void EnsureInternalMetronome(out bool starting) { starting = false; - if (_metronome is null) + if (_metronome is null && HasInternalMetronome) { // Create a paused metronome timer var metronome = new Timer(Metronome_TimerCallback, null, Timeout.InfiniteTimeSpan, _metronomeIntervalTimeSpan); @@ -231,8 +231,8 @@ public async ValueTask DisposeAsync() GC.SuppressFinalize(this); } - /// private int _disposed; + /// protected virtual void Dispose(bool disposing) { if (Interlocked.CompareExchange(ref _disposed, 1, 0) == 0) From 1ce9c413031f94c2247e2083d6000f279b23fa00 Mon Sep 17 00:00:00 2001 From: edevoogd Date: Sun, 18 Apr 2021 23:47:59 +0200 Subject: [PATCH 08/13] Finalizing (async) disposal code for ClockQuantizer & TemporalContextDriver --- src/ClockQuantizer.cs | 33 +++++++++++++++++------ src/TemporalContextDriver.cs | 52 ++++++++++++++++++++---------------- 2 files changed, 54 insertions(+), 31 deletions(-) diff --git a/src/ClockQuantizer.cs b/src/ClockQuantizer.cs index 57ef2eb..72963ce 100644 --- a/src/ClockQuantizer.cs +++ b/src/ClockQuantizer.cs @@ -316,11 +316,21 @@ private Interval CommitAdvance(AdvancePreparationInfo preparation) #region IAsyncDisposable/IDisposable + private int _areHandlersDisposed; + private void DisposeHandlers() + { + if (Interlocked.CompareExchange(ref _areHandlersDisposed, 1, 0) == 0) + { + _driver.ClockAdjusted -= Driver_ClockAdjusted; + _driver.MetronomeTicked -= Driver_MetronomeTicked; + } + } + /// public void Dispose() { -// _driver.ClockAdjusted -= Driver_ClockAdjusted; -// _driver.MetronomeTicked -= Driver_MetronomeTicked; + // This method is re-entrant + DisposeHandlers(); Dispose(disposing: true); GC.SuppressFinalize(this); @@ -329,8 +339,8 @@ public void Dispose() /// public async ValueTask DisposeAsync() { -// _driver.ClockAdjusted -= Driver_ClockAdjusted; -// _driver.MetronomeTicked -= Driver_MetronomeTicked; + // This method is re-entrant + DisposeHandlers(); await DisposeAsyncCore(); @@ -338,15 +348,19 @@ public async ValueTask DisposeAsync() GC.SuppressFinalize(this); } - private int _disposed; + private int _isDisposed; + private int _areDisposableMembersDisposed; /// protected virtual void Dispose(bool disposing) { - if (Interlocked.CompareExchange(ref _disposed, 1, 0) == 0) + if (Interlocked.CompareExchange(ref _isDisposed, 1, 0) == 0) { if (disposing) { - _driver.Dispose(); + if (Interlocked.CompareExchange(ref _areDisposableMembersDisposed, 1, 0) == 0) + { + _driver.Dispose(); + } } } } @@ -354,7 +368,10 @@ protected virtual void Dispose(bool disposing) /// protected virtual async ValueTask DisposeAsyncCore() { - await _driver.DisposeAsync().ConfigureAwait(false); + if (Interlocked.CompareExchange(ref _areDisposableMembersDisposed, 1, 0) == 0) + { + await _driver.DisposeAsync().ConfigureAwait(false); + } } #endregion diff --git a/src/TemporalContextDriver.cs b/src/TemporalContextDriver.cs index a5e365b..7d6d491 100644 --- a/src/TemporalContextDriver.cs +++ b/src/TemporalContextDriver.cs @@ -25,31 +25,34 @@ public TemporalContextDriver(ClockQuantization.ISystemClock clock, TimeSpan metr { IsQuiescent = false; } - } - private static void AttachExternalTemporalContext(TemporalContextDriver driver, ClockQuantization.ISystemClock clock, out bool haveExternalMetronome) - { - haveExternalMetronome = false; - if (clock is ISystemClockTemporalContext context) + static void AttachExternalTemporalContext(TemporalContextDriver driver, ClockQuantization.ISystemClock clock, out bool haveExternalMetronome) { - context.ClockAdjusted += driver.Context_ClockAdjusted; - if (haveExternalMetronome = context.ProvidesMetronome) + haveExternalMetronome = false; + if (clock is ISystemClockTemporalContext context) { - // Allow external "pulse" on metronome ticks - context.MetronomeTicked += driver.Context_MetronomeTicked; + context.ClockAdjusted += driver.Context_ClockAdjusted; + if (haveExternalMetronome = context.ProvidesMetronome) + { + // Allow external "pulse" on metronome ticks + context.MetronomeTicked += driver.Context_MetronomeTicked; + } } } } - private static void DetachExternalTemporalContext(TemporalContextDriver driver, ClockQuantization.ISystemClock clock) + private int _isExternalTemporalContextDetached; + private void DetachExternalTemporalContext() { - if (clock is ISystemClockTemporalContext context) + if (Interlocked.CompareExchange(ref _isExternalTemporalContextDetached, 1, 0) == 0) { - context.ClockAdjusted -= driver.Context_ClockAdjusted; - if (context.ProvidesMetronome) + if (_clock is ISystemClockTemporalContext context) { - // Allow external "pulse" on metronome ticks - context.MetronomeTicked -= driver.Context_MetronomeTicked; + context.ClockAdjusted -= Context_ClockAdjusted; + if (context.ProvidesMetronome) + { + context.MetronomeTicked -= Context_MetronomeTicked; + } } } } @@ -218,6 +221,9 @@ protected virtual void OnMetronomeTicked(EventArgs e) /// public void Dispose() { + // This method is re-entrant + DetachExternalTemporalContext(); + Dispose(disposing: true); GC.SuppressFinalize(this); } @@ -225,30 +231,30 @@ public void Dispose() /// public async ValueTask DisposeAsync() { + // This method is re-entrant + DetachExternalTemporalContext(); + await DisposeAsyncCore(); Dispose(disposing: false); GC.SuppressFinalize(this); } - private int _disposed; + private int _isDisposed; /// protected virtual void Dispose(bool disposing) { - if (Interlocked.CompareExchange(ref _disposed, 1, 0) == 0) + if (disposing) { - if (disposing) - { - DisposeInternalMetronome(); - } - - DetachExternalTemporalContext(this, _clock); + // This method is re-entrant + DisposeInternalMetronome(); } } /// protected virtual async ValueTask DisposeAsyncCore() { + // This method is re-entrant await DisposeInternalMetronomeAsync(); } From fe66b33f951e0255ba4b19e7b6d4d4695a83f682 Mon Sep 17 00:00:00 2001 From: edevoogd Date: Mon, 19 Apr 2021 00:33:59 +0200 Subject: [PATCH 09/13] Simplification of (async) disposal logic - leverage re-entrancy --- src/ClockQuantizer.cs | 29 ++++++++++------------------- src/TemporalContextDriver.cs | 1 - 2 files changed, 10 insertions(+), 20 deletions(-) diff --git a/src/ClockQuantizer.cs b/src/ClockQuantizer.cs index 72963ce..34200a9 100644 --- a/src/ClockQuantizer.cs +++ b/src/ClockQuantizer.cs @@ -316,10 +316,10 @@ private Interval CommitAdvance(AdvancePreparationInfo preparation) #region IAsyncDisposable/IDisposable - private int _areHandlersDisposed; - private void DisposeHandlers() + private int _areEventHandlersDetached; + private void DetachEventHandlers() { - if (Interlocked.CompareExchange(ref _areHandlersDisposed, 1, 0) == 0) + if (Interlocked.CompareExchange(ref _areEventHandlersDetached, 1, 0) == 0) { _driver.ClockAdjusted -= Driver_ClockAdjusted; _driver.MetronomeTicked -= Driver_MetronomeTicked; @@ -330,7 +330,7 @@ private void DisposeHandlers() public void Dispose() { // This method is re-entrant - DisposeHandlers(); + DetachEventHandlers(); Dispose(disposing: true); GC.SuppressFinalize(this); @@ -340,7 +340,7 @@ public void Dispose() public async ValueTask DisposeAsync() { // This method is re-entrant - DisposeHandlers(); + DetachEventHandlers(); await DisposeAsyncCore(); @@ -348,30 +348,21 @@ public async ValueTask DisposeAsync() GC.SuppressFinalize(this); } - private int _isDisposed; - private int _areDisposableMembersDisposed; /// protected virtual void Dispose(bool disposing) { - if (Interlocked.CompareExchange(ref _isDisposed, 1, 0) == 0) + if (disposing) { - if (disposing) - { - if (Interlocked.CompareExchange(ref _areDisposableMembersDisposed, 1, 0) == 0) - { - _driver.Dispose(); - } - } + // This method is re-entrant and mutually co-existent with _driver.DisposeAsyncCore() + _driver.Dispose(); } } /// protected virtual async ValueTask DisposeAsyncCore() { - if (Interlocked.CompareExchange(ref _areDisposableMembersDisposed, 1, 0) == 0) - { - await _driver.DisposeAsync().ConfigureAwait(false); - } + // This method is re-entrant and mutually co-existent with _driver.Dispose() + await _driver.DisposeAsync().ConfigureAwait(false); } #endregion diff --git a/src/TemporalContextDriver.cs b/src/TemporalContextDriver.cs index 7d6d491..bf1c2b0 100644 --- a/src/TemporalContextDriver.cs +++ b/src/TemporalContextDriver.cs @@ -240,7 +240,6 @@ public async ValueTask DisposeAsync() GC.SuppressFinalize(this); } - private int _isDisposed; /// protected virtual void Dispose(bool disposing) { From 31a1206e378a15b2eed1496302661512db33b38a Mon Sep 17 00:00:00 2001 From: edevoogd Date: Mon, 19 Apr 2021 01:12:17 +0200 Subject: [PATCH 10/13] Improve TemporalContextDriver.DisposeInternalMetronomeAsync() --- src/TemporalContextDriver.cs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/TemporalContextDriver.cs b/src/TemporalContextDriver.cs index bf1c2b0..32c82dd 100644 --- a/src/TemporalContextDriver.cs +++ b/src/TemporalContextDriver.cs @@ -89,16 +89,18 @@ private void DisposeInternalMetronome() private async ValueTask DisposeInternalMetronomeAsync() { +#if !(NETSTANDARD2_1 || NETCOREAPP3_0 || NETCOREAPP3_1 || NET5_0 || NET5_0_OR_GREATER) + await default(ValueTask).ConfigureAwait(continueOnCapturedContext: false); +#endif + if (Interlocked.Exchange(ref _metronome, null) is Timer metronome) { #if NETSTANDARD2_1 || NETCOREAPP3_0 || NETCOREAPP3_1 || NET5_0 || NET5_0_OR_GREATER - if (_metronome is IAsyncDisposable asyncDisposable) + if (metronome is IAsyncDisposable asyncDisposable) { await asyncDisposable.DisposeAsync().ConfigureAwait(continueOnCapturedContext: false); return; } -#else - await default(ValueTask).ConfigureAwait(continueOnCapturedContext: false); #endif metronome.Dispose(); } @@ -164,7 +166,7 @@ protected virtual void OnClockAdjusted(EventArgs e) } else { - // Hold back the latest ClockAdjusted event until we unquiesce + // Retain the latest ClockAdjusted event until we unquiesce Interlocked.Exchange(ref _pendingClockAdjustedEventArgs, e); } } @@ -183,10 +185,10 @@ protected virtual void OnMetronomeTicked(EventArgs e) private void Context_ClockAdjusted(object? _, EventArgs __) => OnClockAdjusted(EventArgs.Empty); - private void Metronome_TimerCallback(object? _) => Context_MetronomeTicked(null, EventArgs.Empty); - private void Context_MetronomeTicked(object? _, EventArgs __) => OnMetronomeTicked(EventArgs.Empty); + private void Metronome_TimerCallback(object? _) => OnMetronomeTicked(EventArgs.Empty); + #region ISystemClock From bb2b04143711dc3598e1edfe87c758c8715ba463 Mon Sep 17 00:00:00 2001 From: edevoogd Date: Tue, 20 Apr 2021 01:21:40 +0200 Subject: [PATCH 11/13] - Update ISystemClockTemporalContext metronome feature - Small tweak to ClockQuantizer.PrepareAdvance() in light of quiescing --- src/ClockQuantizer.cs | 11 +-- src/TemporalContext.cs | 4 +- src/TemporalContextDriver.cs | 92 ++++++++++--------- .../assets/SystemClockTemporalContext.cs | 10 +- 4 files changed, 62 insertions(+), 55 deletions(-) diff --git a/src/ClockQuantizer.cs b/src/ClockQuantizer.cs index 34200a9..743c669 100644 --- a/src/ClockQuantizer.cs +++ b/src/ClockQuantizer.cs @@ -218,7 +218,7 @@ internal NewIntervalEventArgs(DateTimeOffset offset, bool metronomic, TimeSpan? /// The maximum of each /// /// If also implements , the will pick up on external - /// events. Also, if is , + /// events. Also, if is non-null, /// the will pick up on external events, instead of relying on an internal metronome. /// public ClockQuantizer(ISystemClock clock, TimeSpan maxIntervalTimeSpan) @@ -260,12 +260,11 @@ private Interval Advance(bool metronomic) private AdvancePreparationInfo PrepareAdvance(bool metronomic) { - bool starting = false; - // Start metronome (if not imposed externally) on first Advance and consider first Advance as a metronomic event. - if (_currentInterval is null && _driver.TryEnsureMetronomeRunning(out starting)) + bool unquiescing = _driver.Unquiesce(out var metronomeStarting); + if (_currentInterval is null) { - metronomic |= starting; + metronomic |= metronomeStarting; } var previousInterval = _currentInterval; @@ -274,7 +273,7 @@ private AdvancePreparationInfo PrepareAdvance(bool metronomic) if (previousInterval is not null) { // Ignore potential *internal* metronome gap due to tiny clock jitter - if (!metronomic || (metronomic && (starting || !_driver.HasInternalMetronome))) + if (!metronomic || (metronomic && (unquiescing || !_driver.HasInternalMetronome))) { var gap = ClockOffsetUnitsToTimeSpan(interval.ClockOffset - previousInterval.ClockOffset) - MaxIntervalTimeSpan; if (gap > TimeSpan.Zero) diff --git a/src/TemporalContext.cs b/src/TemporalContext.cs index 0e4058b..513a5dd 100644 --- a/src/TemporalContext.cs +++ b/src/TemporalContext.cs @@ -47,9 +47,9 @@ public interface ISystemClock public interface ISystemClockTemporalContext { /// - /// if the temporal context provides a metronome feature - i.e., if it fires events. + /// A non- value if the temporal context provides a metronome feature - i.e., if it fires events. /// - bool ProvidesMetronome { get; } + TimeSpan? MetronomeIntervalTimeSpan { get; } /// /// An event that can be raised to inform listeners that the was adjusted. diff --git a/src/TemporalContextDriver.cs b/src/TemporalContextDriver.cs index 32c82dd..57503ad 100644 --- a/src/TemporalContextDriver.cs +++ b/src/TemporalContextDriver.cs @@ -14,26 +14,39 @@ internal class TemporalContextDriver : ClockQuantization.ISystemClock, ISystemCl private System.Threading.Timer? _metronome; private EventArgs? _pendingClockAdjustedEventArgs; - public TemporalContextDriver(ClockQuantization.ISystemClock clock, TimeSpan metronomeIntervalTimeSpan) + public TemporalContextDriver(ClockQuantization.ISystemClock clock, TimeSpan? metronomeIntervalTimeSpan = default) { - _clock = clock; - _metronomeIntervalTimeSpan = metronomeIntervalTimeSpan; - - AttachExternalTemporalContext(this, clock, out var haveExternalMetronome); + AttachExternalTemporalContext(this, clock, out var externalMetronomeIntervalTimeSpan); - if (haveExternalMetronome) + if (externalMetronomeIntervalTimeSpan.HasValue) { IsQuiescent = false; + _metronomeIntervalTimeSpan = externalMetronomeIntervalTimeSpan.Value; } + else + { + if (!metronomeIntervalTimeSpan.HasValue) + { + // Need metronome interval timespan + DetachExternalTemporalContext(); + throw new ArgumentNullException(nameof(metronomeIntervalTimeSpan), "Must provide a valid metronome interval timespan or supply an external metronome."); + } - static void AttachExternalTemporalContext(TemporalContextDriver driver, ClockQuantization.ISystemClock clock, out bool haveExternalMetronome) + _metronomeIntervalTimeSpan = metronomeIntervalTimeSpan.Value; + } + + _clock = clock; + + static void AttachExternalTemporalContext(TemporalContextDriver driver, ClockQuantization.ISystemClock clock, out TimeSpan? externalMetronomeIntervalTimeSpan) { - haveExternalMetronome = false; + externalMetronomeIntervalTimeSpan = default; + if (clock is ISystemClockTemporalContext context) { context.ClockAdjusted += driver.Context_ClockAdjusted; - if (haveExternalMetronome = context.ProvidesMetronome) + if (context.MetronomeIntervalTimeSpan.HasValue) { + externalMetronomeIntervalTimeSpan = context.MetronomeIntervalTimeSpan.Value; // Allow external "pulse" on metronome ticks context.MetronomeTicked += driver.Context_MetronomeTicked; } @@ -49,7 +62,7 @@ private void DetachExternalTemporalContext() if (_clock is ISystemClockTemporalContext context) { context.ClockAdjusted -= Context_ClockAdjusted; - if (context.ProvidesMetronome) + if (context.MetronomeIntervalTimeSpan.HasValue) { context.MetronomeTicked -= Context_MetronomeTicked; } @@ -57,19 +70,19 @@ private void DetachExternalTemporalContext() } } - private void EnsureInternalMetronome(out bool starting) + private void EnsureInternalMetronome(out bool metronomeStarting) { - starting = false; + metronomeStarting = false; if (_metronome is null && HasInternalMetronome) { // Create a paused metronome timer var metronome = new Timer(Metronome_TimerCallback, null, Timeout.InfiniteTimeSpan, _metronomeIntervalTimeSpan); + metronomeStarting = true; if (Interlocked.CompareExchange(ref _metronome, metronome, null) is null) { // Resume the newly created metronome timer - _metronome!.Change(_metronomeIntervalTimeSpan, _metronomeIntervalTimeSpan); - starting = true; + metronome.Change(_metronomeIntervalTimeSpan, _metronomeIntervalTimeSpan); } else { @@ -106,18 +119,7 @@ private async ValueTask DisposeInternalMetronomeAsync() } } - public bool TryEnsureMetronomeRunning(out bool starting) - { - starting = false; - if (HasInternalMetronome) - { - Unquiesce(out starting); - } - - return true; - } - - protected bool IsQuiescent { get; private set; } = true; + public bool IsQuiescent { get; private set; } = true; public void Quiesce() { @@ -127,35 +129,37 @@ public void Quiesce() DisposeInternalMetronome(); } - public void Unquiesce() => Unquiesce(out var _); + public bool Unquiesce() => Unquiesce(out var _); - private void Unquiesce(out bool starting) + public bool Unquiesce(out bool metronomeStarting) { - starting = false; - EventArgs? pendingClockAdjustedEventArgs = Interlocked.Exchange(ref _pendingClockAdjustedEventArgs, null); + bool unquiescing = IsQuiescent; - if (pendingClockAdjustedEventArgs is not null) + if (unquiescing) { - // Make sure that we briefly postpone any metronome event that may occur during the process of unquiescing - lock (this) + EventArgs? pendingClockAdjustedEventArgs = Interlocked.Exchange(ref _pendingClockAdjustedEventArgs, null); + + if (pendingClockAdjustedEventArgs is not null) { - IsQuiescent = false; // Set to false already to make sure that the ClockAdjusted event fires - OnClockAdjusted(pendingClockAdjustedEventArgs); + // Make sure that we briefly postpone any metronome event that may occur during the process of unquiescing + lock (this) + { + IsQuiescent = false; // Set to false already to make sure that the ClockAdjusted event fires + OnClockAdjusted(pendingClockAdjustedEventArgs); + } } - } - IsQuiescent = false; - // TODO: Restore internal metronome, re-acquire unmanaged timer resources - if (HasInternalMetronome) - { - // Create a ** suspended ?? ** timer. Timer will be started at first call to Advance(). - EnsureInternalMetronome(out starting); + IsQuiescent = false; } + + EnsureInternalMetronome(out metronomeStarting); + + return unquiescing; } public bool HasInternalMetronome { - get => _clock is not ISystemClockTemporalContext context || !context.ProvidesMetronome; + get => _clock is not ISystemClockTemporalContext context || !context.MetronomeIntervalTimeSpan.HasValue; } protected virtual void OnClockAdjusted(EventArgs e) @@ -210,7 +214,7 @@ protected virtual void OnMetronomeTicked(EventArgs e) #region ISystemClockTemporalContext /// - public bool ProvidesMetronome { get; } = true; + public TimeSpan? MetronomeIntervalTimeSpan { get => _metronomeIntervalTimeSpan; } /// public event EventHandler? ClockAdjusted; /// diff --git a/tests/ClockQuantization.Tests/assets/SystemClockTemporalContext.cs b/tests/ClockQuantization.Tests/assets/SystemClockTemporalContext.cs index cf9d5fa..25abdf5 100644 --- a/tests/ClockQuantization.Tests/assets/SystemClockTemporalContext.cs +++ b/tests/ClockQuantization.Tests/assets/SystemClockTemporalContext.cs @@ -47,7 +47,7 @@ public void AdjustClock(DateTimeOffset now) private System.Threading.Timer? _metronome; private MetronomeOptions? _metronomeOptions; - public bool ProvidesMetronome { get; private set; } + public TimeSpan? MetronomeIntervalTimeSpan { get; private set; } = default; public bool IsMetronomeRunning { get; private set; } public event EventHandler? ClockAdjusted; @@ -89,8 +89,12 @@ public SystemClockTemporalContext(MetronomeOptions? metronomeOptions) : this(new public SystemClockTemporalContext(Func getUtcNow, MetronomeOptions? metronomeOptions) { GetUtcNow = getUtcNow; - ProvidesMetronome = (_metronomeOptions = metronomeOptions) is not null; - IsMetronomeRunning = ProvidesMetronome && ApplyMetronomeOptions(metronomeOptions!, Metronome_Ticked, out _metronome); + if ((_metronomeOptions = metronomeOptions) is not null) + { + MetronomeIntervalTimeSpan = metronomeOptions!.MaxIntervalTimeSpan; + } + + IsMetronomeRunning = MetronomeIntervalTimeSpan.HasValue && ApplyMetronomeOptions(metronomeOptions!, Metronome_Ticked, out _metronome); #if NET5_0 || NET5_0_OR_GREATER var utcNow = DateTimeOffset.UtcNow; From 2b99e1662ec09f07be1ac6ce15ef5b0ecdf14dc9 Mon Sep 17 00:00:00 2001 From: edevoogd Date: Tue, 20 Apr 2021 02:40:04 +0200 Subject: [PATCH 12/13] Final tweaks for quiscing support; updated XMLDOCs --- src/ClockQuantization.xml | 86 ++++++++++++++++++++++++------------ src/ClockQuantizer.cs | 23 ++++++++-- src/TemporalContextDriver.cs | 45 ++++++++++--------- 3 files changed, 100 insertions(+), 54 deletions(-) diff --git a/src/ClockQuantization.xml b/src/ClockQuantization.xml index 7f2b138..efd4e66 100644 --- a/src/ClockQuantization.xml +++ b/src/ClockQuantization.xml @@ -159,44 +159,38 @@ The maximum of each If also implements , the will pick up on external - events. Also, if is , + events. Also, if is non-null, the will pick up on external events, instead of relying on an internal metronome. - - - - - - - - - - - - - - - - - + + + Puts the into a quiescent state, effectively freeing any owned unmanaged resources. While in a quiescent state, the will not raise any events, nor perform metronomic advance operations. + + + Any externally initiated advance operation will automatically take the back into normal operation. + - - + + + Takes the out of a quiescent state into normal operation. + - - + + + Returns if the is in a quiescent state, otherwise. + - + - + - + - + @@ -321,9 +315,9 @@ Represents traits of the temporal context - + - if the temporal context provides a metronome feature - i.e., if it fires events. + A non- value if the temporal context provides a metronome feature - i.e., if it fires events. @@ -339,5 +333,41 @@ An event that can be raised to inform listeners that a metronome "tick" occurred. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/ClockQuantizer.cs b/src/ClockQuantizer.cs index 743c669..5d3eaa9 100644 --- a/src/ClockQuantizer.cs +++ b/src/ClockQuantizer.cs @@ -230,10 +230,25 @@ public ClockQuantizer(ISystemClock clock, TimeSpan maxIntervalTimeSpan) // Quiescing + + /// + /// Puts the into a quiescent state, effectively freeing any owned unmanaged resources. While in a quiescent state, the will not raise any events, nor perform metronomic advance operations. + /// + /// + /// Any externally initiated advance operation will automatically take the back into normal operation. + /// public void Quiesce() => _driver.Quiesce(); + /// + /// Takes the out of a quiescent state into normal operation. + /// public void Unquiesce() => _driver.Unquiesce(); + /// + /// Returns if the is in a quiescent state, otherwise. + /// + public bool IsQuiescent { get => _driver.IsQuiescent; } + // Advance primitives @@ -261,10 +276,10 @@ private Interval Advance(bool metronomic) private AdvancePreparationInfo PrepareAdvance(bool metronomic) { // Start metronome (if not imposed externally) on first Advance and consider first Advance as a metronomic event. - bool unquiescing = _driver.Unquiesce(out var metronomeStarting); - if (_currentInterval is null) + bool unquiescing = _driver.Unquiesce(); + if (unquiescing || _currentInterval is null) { - metronomic |= metronomeStarting; + metronomic = true; } var previousInterval = _currentInterval; @@ -273,7 +288,7 @@ private AdvancePreparationInfo PrepareAdvance(bool metronomic) if (previousInterval is not null) { // Ignore potential *internal* metronome gap due to tiny clock jitter - if (!metronomic || (metronomic && (unquiescing || !_driver.HasInternalMetronome))) + if (unquiescing || !metronomic || (metronomic && !_driver.HasInternalMetronome)) { var gap = ClockOffsetUnitsToTimeSpan(interval.ClockOffset - previousInterval.ClockOffset) - MaxIntervalTimeSpan; if (gap > TimeSpan.Zero) diff --git a/src/TemporalContextDriver.cs b/src/TemporalContextDriver.cs index 57503ad..a618388 100644 --- a/src/TemporalContextDriver.cs +++ b/src/TemporalContextDriver.cs @@ -70,15 +70,17 @@ private void DetachExternalTemporalContext() } } - private void EnsureInternalMetronome(out bool metronomeStarting) + public bool HasInternalMetronome { - metronomeStarting = false; + get => _clock is not ISystemClockTemporalContext context || !context.MetronomeIntervalTimeSpan.HasValue; + } + private void EnsureInternalMetronome() + { if (_metronome is null && HasInternalMetronome) { // Create a paused metronome timer var metronome = new Timer(Metronome_TimerCallback, null, Timeout.InfiniteTimeSpan, _metronomeIntervalTimeSpan); - metronomeStarting = true; if (Interlocked.CompareExchange(ref _metronome, metronome, null) is null) { // Resume the newly created metronome timer @@ -129,9 +131,8 @@ public void Quiesce() DisposeInternalMetronome(); } - public bool Unquiesce() => Unquiesce(out var _); - - public bool Unquiesce(out bool metronomeStarting) + private readonly object _quiescingLockObject = new object(); + public bool Unquiesce() { bool unquiescing = IsQuiescent; @@ -142,7 +143,7 @@ public bool Unquiesce(out bool metronomeStarting) if (pendingClockAdjustedEventArgs is not null) { // Make sure that we briefly postpone any metronome event that may occur during the process of unquiescing - lock (this) + lock (_quiescingLockObject) { IsQuiescent = false; // Set to false already to make sure that the ClockAdjusted event fires OnClockAdjusted(pendingClockAdjustedEventArgs); @@ -152,14 +153,22 @@ public bool Unquiesce(out bool metronomeStarting) IsQuiescent = false; } - EnsureInternalMetronome(out metronomeStarting); + // Ensure that we have an internal metronome, if applicable. + EnsureInternalMetronome(); return unquiescing; } - public bool HasInternalMetronome + protected virtual void OnMetronomeTicked(EventArgs e) { - get => _clock is not ISystemClockTemporalContext context || !context.MetronomeIntervalTimeSpan.HasValue; + if (!IsQuiescent) + { + // Make sure that we briefly postpone a metronome event that occurs during the process of unquiescing + lock (_quiescingLockObject) + { + MetronomeTicked?.Invoke(this, e); + } + } } protected virtual void OnClockAdjusted(EventArgs e) @@ -175,18 +184,6 @@ protected virtual void OnClockAdjusted(EventArgs e) } } - protected virtual void OnMetronomeTicked(EventArgs e) - { - if (!IsQuiescent) - { - // Make sure that we briefly postpone a metronome event that occurs during the process of unquiescing - lock (this) - { - MetronomeTicked?.Invoke(this, e); - } - } - } - private void Context_ClockAdjusted(object? _, EventArgs __) => OnClockAdjusted(EventArgs.Empty); private void Context_MetronomeTicked(object? _, EventArgs __) => OnMetronomeTicked(EventArgs.Empty); @@ -227,6 +224,8 @@ protected virtual void OnMetronomeTicked(EventArgs e) /// public void Dispose() { + IsQuiescent = true; + // This method is re-entrant DetachExternalTemporalContext(); @@ -237,6 +236,8 @@ public void Dispose() /// public async ValueTask DisposeAsync() { + IsQuiescent = true; + // This method is re-entrant DetachExternalTemporalContext(); From 20538358cc24b735b1e18ea88bf6cdd00dff455c Mon Sep 17 00:00:00 2001 From: edevoogd Date: Wed, 21 Apr 2021 00:48:33 +0200 Subject: [PATCH 13/13] Final tweaks to quiescing feature --- src/ClockQuantization.xml | 2 +- src/ClockQuantizer.cs | 31 +++++++++++++++++++++++++++---- src/TemporalContextDriver.cs | 6 +++--- 3 files changed, 31 insertions(+), 8 deletions(-) diff --git a/src/ClockQuantization.xml b/src/ClockQuantization.xml index efd4e66..4f982e7 100644 --- a/src/ClockQuantization.xml +++ b/src/ClockQuantization.xml @@ -159,7 +159,7 @@ The maximum of each If also implements , the will pick up on external - events. Also, if is non-null, + events. Also, if is non-, the will pick up on external events, instead of relying on an internal metronome. diff --git a/src/ClockQuantizer.cs b/src/ClockQuantizer.cs index 5d3eaa9..fa85d45 100644 --- a/src/ClockQuantizer.cs +++ b/src/ClockQuantizer.cs @@ -218,7 +218,7 @@ internal NewIntervalEventArgs(DateTimeOffset offset, bool metronomic, TimeSpan? /// The maximum of each /// /// If also implements , the will pick up on external - /// events. Also, if is non-null, + /// events. Also, if is non-, /// the will pick up on external events, instead of relying on an internal metronome. /// public ClockQuantizer(ISystemClock clock, TimeSpan maxIntervalTimeSpan) @@ -231,13 +231,22 @@ public ClockQuantizer(ISystemClock clock, TimeSpan maxIntervalTimeSpan) // Quiescing + private readonly object _quiescingLockObject = new object(); + /// /// Puts the into a quiescent state, effectively freeing any owned unmanaged resources. While in a quiescent state, the will not raise any events, nor perform metronomic advance operations. /// /// /// Any externally initiated advance operation will automatically take the back into normal operation. /// - public void Quiesce() => _driver.Quiesce(); + public void Quiesce() + { + // Ensure that quiesent state can be achieved without immediately being knocked out of it by a MetronomeTicked event that just happened to be in flight. + lock (_quiescingLockObject) + { + _driver.Quiesce(); + } + } /// /// Takes the out of a quiescent state into normal operation. @@ -323,9 +332,23 @@ private Interval CommitAdvance(AdvancePreparationInfo preparation) return preparation.Interval; } - private void Driver_MetronomeTicked(object? _, EventArgs __) => Advance(metronomic: true); + private void Driver_MetronomeTicked(object? _, EventArgs __) + { + lock (_quiescingLockObject) + { + // Ensure that any in-flight MetronomeTicked event during quiescing transition does not knock us out of a quiesent state that was juuuuuuust established. + if (!IsQuiescent) + { + Advance(metronomic: true); + } + } + } - private void Driver_ClockAdjusted(object? _, EventArgs __) => Advance(metronomic: false); + private void Driver_ClockAdjusted(object? _, EventArgs __) + { + // Allow clock adjustemts to take us out of quiesent state (race possible; small chance of in-flight event, as underlying driver is quisced as well). + Advance(metronomic: false); + } #region IAsyncDisposable/IDisposable diff --git a/src/TemporalContextDriver.cs b/src/TemporalContextDriver.cs index a618388..eb36b37 100644 --- a/src/TemporalContextDriver.cs +++ b/src/TemporalContextDriver.cs @@ -131,7 +131,7 @@ public void Quiesce() DisposeInternalMetronome(); } - private readonly object _quiescingLockObject = new object(); + private readonly object _unquiescingLockObject = new object(); public bool Unquiesce() { bool unquiescing = IsQuiescent; @@ -143,7 +143,7 @@ public bool Unquiesce() if (pendingClockAdjustedEventArgs is not null) { // Make sure that we briefly postpone any metronome event that may occur during the process of unquiescing - lock (_quiescingLockObject) + lock (_unquiescingLockObject) { IsQuiescent = false; // Set to false already to make sure that the ClockAdjusted event fires OnClockAdjusted(pendingClockAdjustedEventArgs); @@ -164,7 +164,7 @@ protected virtual void OnMetronomeTicked(EventArgs e) if (!IsQuiescent) { // Make sure that we briefly postpone a metronome event that occurs during the process of unquiescing - lock (_quiescingLockObject) + lock (_unquiescingLockObject) { MetronomeTicked?.Invoke(this, e); }