From c12da13aff7ab8a07243f695ad673a7a172f7ce0 Mon Sep 17 00:00:00 2001 From: edevoogd Date: Tue, 26 Jan 2021 23:13:23 +0100 Subject: [PATCH 1/4] Add clock-specific capabilities to support non-arbitrary time representation as an offset to some non-arbitrary base --- src/TemporalContext.cs | 28 +++++++ .../assets/SystemClockTemporalContext.cs | 84 +++++++++++++++++++ 2 files changed, 112 insertions(+) diff --git a/src/TemporalContext.cs b/src/TemporalContext.cs index fd8ff38..9b67403 100644 --- a/src/TemporalContext.cs +++ b/src/TemporalContext.cs @@ -11,6 +11,34 @@ public interface ISystemClock /// The current system time in UTC. /// DateTimeOffset UtcNow { get; } + + /// + /// An offset (in ticks) representing the current system time in UTC. + /// + /// + /// Depending on implementation this may be an absolute value based on , a relative value based on etc. + /// + long UtcNowClockOffset { get; } + + /// Represents the number of offset units (ticks) in 1 millisecond + /// + /// + /// + long ClockOffsetUnitsPerMillisecond { get; } + + /// + /// Converts to a in UTC. + /// + /// The offset to convert + /// The corresponding + DateTimeOffset ClockOffsetToUtcDateTimeOffset(long offset); + + /// + /// Converts to a clock-specific offset. + /// + /// The offset to convert + /// The corresponding clock-specific offset + long DateTimeOffsetToClockOffset(DateTimeOffset offset); } /// diff --git a/tests/ClockQuantization.Tests/assets/SystemClockTemporalContext.cs b/tests/ClockQuantization.Tests/assets/SystemClockTemporalContext.cs index da994fc..c146cb8 100644 --- a/tests/ClockQuantization.Tests/assets/SystemClockTemporalContext.cs +++ b/tests/ClockQuantization.Tests/assets/SystemClockTemporalContext.cs @@ -1,4 +1,5 @@ using System; +using System.Runtime.CompilerServices; namespace ClockQuantization.Tests.Assets { @@ -10,8 +11,12 @@ class SystemClockTemporalContext : ISystemClock, ISystemClockTemporalContext private class ManualClock : ISystemClock { private DateTimeOffset _now; + /// public DateTimeOffset UtcNow { get => _now; } + /// + public long UtcNowClockOffset => UtcNow.UtcTicks; + public ManualClock(DateTimeOffset now) { _now = now; @@ -25,6 +30,14 @@ public void AdjustClock(DateTimeOffset now) { _now = now; } + + /// + public DateTimeOffset ClockOffsetToUtcDateTimeOffset(long offset) => new DateTimeOffset(offset, TimeSpan.Zero); + + /// + public long DateTimeOffsetToClockOffset(DateTimeOffset offset) => offset.UtcTicks; + + public long ClockOffsetUnitsPerMillisecond => TimeSpan.TicksPerMillisecond; } private ManualClock? _manual; @@ -78,6 +91,77 @@ public SystemClockTemporalContext(Func getUtcNow, MetronomeOptio GetUtcNow = getUtcNow; ProvidesMetronome = (_metronomeOptions = metronomeOptions) is not null; IsMetronomeRunning = ProvidesMetronome && ApplyMetronomeOptions(metronomeOptions!, Metronome_Ticked, out _metronome); + +#if NET5_0 + var utcNow = DateTimeOffset.UtcNow; + var milliSecondsSinceGenesis = Environment.TickCount64; // The number of milliseconds elapsed since the system started. + + UtcGenesis = utcNow - TimeSpan.FromMilliseconds(milliSecondsSinceGenesis); +#endif + } + +#if NET5_0 + public readonly DateTimeOffset UtcGenesis; +#endif + + + /// + public long UtcNowClockOffset + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#if NET5_0 + get => HasExternalClock ? Environment.TickCount64 : _manual!.UtcNowClockOffset; +#else + get => HasExternalClock ? UtcNow.UtcTicks : _manual!.UtcNowClockOffset; +#endif + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public DateTimeOffset ClockOffsetToUtcDateTimeOffset(long offset) + { + if (!HasExternalClock) + { + return _manual!.ClockOffsetToUtcDateTimeOffset(offset); + } + +#if NET5_0 + return UtcGenesis + TimeSpan.FromMilliseconds(offset); +#else + return new DateTimeOffset(offset, TimeSpan.Zero); +#endif + } + + /// + public long DateTimeOffsetToClockOffset(DateTimeOffset offset) + { + if (!HasExternalClock) + { + return _manual!.DateTimeOffsetToClockOffset(offset); + } + +#if NET5_0 + return (long) (offset.UtcDateTime - UtcGenesis).TotalMilliseconds; +#else + return offset.UtcTicks; +#endif + } + + public long ClockOffsetUnitsPerMillisecond + { + get + { + if (!HasExternalClock) + { + return _manual!.ClockOffsetUnitsPerMillisecond; + } + +#if NET5_0 + return 1; +#else + return TimeSpan.TicksPerMillisecond; +#endif + } } private bool ApplyMetronomeOptions(MetronomeOptions metronomeOptions, System.Threading.TimerCallback callback, out System.Threading.Timer? metronome) From a8b83649189734a524d345284b112d447da03fa2 Mon Sep 17 00:00:00 2001 From: edevoogd Date: Wed, 27 Jan 2021 00:02:01 +0100 Subject: [PATCH 2/4] Change underlying time representation from 'System.DateTimeOffset' to clock-specific offset and unit scale. --- src/ClockQuantization.xml | 161 +++++++++++++----- src/ClockQuantizer.cs | 133 +++++++++++---- src/Interval.cs | 53 +++--- ...on.cs => LazyClockOffsetSerialPosition.cs} | 44 ++--- .../ClockQuantizerTests.cs | 159 +++++++++-------- .../ConversionTests.cs | 64 +++++++ .../ClockQuantization.Tests/IntervalTests.cs | 24 +-- 7 files changed, 427 insertions(+), 211 deletions(-) rename src/{LazyTimeSerialPosition.cs => LazyClockOffsetSerialPosition.cs} (53%) create mode 100644 tests/ClockQuantization.Tests/ConversionTests.cs diff --git a/src/ClockQuantization.xml b/src/ClockQuantization.xml index 1e864fe..8a7d5f0 100644 --- a/src/ClockQuantization.xml +++ b/src/ClockQuantization.xml @@ -10,7 +10,7 @@ A so-called metronome is used to start a new every time when has passed. A may be cut short when an "out-of-cadance" advance operation is performed - such operation is triggered by calls, as well as by and events. - Under certain conditions, an advance operation may be incurred by calls. + Under certain conditions, an advance operation may be incurred by calls. @@ -21,10 +21,53 @@ The current in the 's temporal context. A starts in an inhibited state. Only after the first advance operation, will have a non- value. + + + Represents the clock-specific offset at which the next event is expected. + + + While uninitialized initially, will always have a value after the first advance operation. Basically, having + CurrentInterval.ClockOffset + TimeSpanToClockOffsetUnits(MaxIntervalTimeSpan) pre-calculated at the start of each metronomic interval, ammortizes the cost of this typical calculation during time-based decisions. + When an "out-of-cadance" (i.e. non-metronomic) advance operation is performed, (and its offset) will update, but not . + + Returns the value of the reference clock. Depending on the actual reference clock implementation, this may or may not incur an expensive system call. + + Returns the value of the reference clock. + + + + Converts a to an offset in clock-specific units (ticks). + + The to convert + An offset in clock-specific units. + + + + + Converts an offset in clock-specific units (ticks) to a . + + The clock-specific offset to convert + A in UTC. + + + + + Converts a to a count of clock-specific offset units (ticks). + + The to convert + The amount of clock-specific offset units covering the . + + + + Converts an amount of clock-specific offset units (ticks) to a . + + The amount of units to convert + A covering the specified number of . + Establishes a new lower bound on the "last seen" exact within the @@ -32,12 +75,12 @@ The newly started . - + - If does not have an exact yet, it will be initialized with one. In every + If does not have an exact yet, it will be initialized with one. In every situation where initialization is still required, this will incur a call into the reference clock's . - Reference to an (on-stack) which may or may not have been initialized. + Reference to an (on-stack) which may or may not have been initialized. Indicates if the should perform an advance operation. This is advised in situations where non-exact positions may still be acquired in the same and exact ordering (e.g. in a cache LRU eviction algorithm) might be adversely affected. @@ -45,15 +88,15 @@ Depending on the actual reference clock implementation, this may or may not incur an expensive system call. - + - If does not have a yet, it will be initialized with one. + If does not have an yet, it will be initialized with one. - Reference to an (on-stack) which may or may not have been initialized. + Reference to an (on-stack) which may or may not have been initialized. - If the did not perform a first advance operation yet, the result will be an exact position + If the had not performed a first advance operation yet, the result will be an exact position (incurring a call into the reference clock's ). Otherwise, returns a position bound to - 's , but with an incremented . + 's , but with an incremented . @@ -127,7 +170,7 @@ Within the reference frame of an , there is no notion of time; there is only notion of the order - in which s are issued. + in which s are issued. Whereas is always progressing with intervals of at most length, @@ -135,73 +178,73 @@ - + - The within the temporal context when the was started. + The offset within the temporal context when the was started. - + - If does not have a yet, it will be initialized with one, - based off 's and its monotonically increasing internal serial position. + If does not have an yet, it will be initialized with one, + based off 's and its monotonically increasing internal serial position. - The interval to create the off. - Reference to an (on-stack) which may or may not have been initialized. + The interval to initialize the off. + Reference to an (on-stack) which may or may not have been initialized. - + - Creates a new based off the 's and its monotonically increasing internal serial position. + Creates a new based off the 's and its monotonically increasing internal serial position. - A new + A new - A created at the time when a new is created (e.g. during - ) will have equal + A created at the time when a new is created (e.g. during + ) will have equal to . - + - Represents a point in time, expressed as a combination of and . Its value may be unitialized, - as indicated by its property. + Represents a point in time, expressed as a combination of a clock-specific and . Its value may be unitialized, + as indicated by its property. - When initialized (i.e. when equals ), the following rules apply: + When initialized (i.e. when equals ), the following rules apply: - Issuance of an "exact" can only occur at start. By definition, will equal - , will equal 1u and will equal . - Any issued off the same with N (N > 1u) was issued - at a later point in (continuous) time than the with equals N-1 and was issued at an earlier - point in (continuous) time than any with > N. + Issuance of an "exact" can only occur at start. By definition, will equal + , will equal 1u and will equal . + Any issued off the same with N (N > 1u) was issued + at a later point in (continuous) time than the with equals N-1 and was issued at an earlier + point in (continuous) time than any with > N. - With several methods available to lazily initialize a by reference, it is possible to create s + With several methods available to lazily initialize a by reference, it is possible to create s on-stack and initialize them as late as possible and only if deemed necessary for the operation/decision at hand. - - - + + + - - Returns the assigned to the current value. - When is . + + Returns the offset assigned to the current value. + When is . - + Returns the serial position assigned to the current value. - When is . + When is . - + Returns if a value is assigned, otherwise. - - Returns if a value is assigned and said value represents the first issued at . In other words, - the value was assigned exactly at . + + Returns if a value is assigned and said value represents the first issued at . In other words, + the value was assigned exactly at . - Abstracts the system clock to facilitate synthetic clocks (e.g. for testing). + Abstracts the system clock to facilitate synthetic clocks (e.g. for replay or testing). @@ -209,6 +252,34 @@ The current system time in UTC. + + + An offset (in ticks) representing the current system time in UTC. + + + Depending on implementation this may be an absolute value based on , a relative value based on etc. + + + + Represents the number of offset units (ticks) in 1 millisecond + + + + + + + Converts to a in UTC. + + The offset to convert + The corresponding + + + + Converts to a clock-specific offset. + + The offset to convert + The corresponding clock-specific offset + Represents traits of the temporal context diff --git a/src/ClockQuantizer.cs b/src/ClockQuantizer.cs index ee8eba2..099696b 100644 --- a/src/ClockQuantizer.cs +++ b/src/ClockQuantizer.cs @@ -8,27 +8,16 @@ namespace ClockQuantization /// A so-called metronome is used to start a new every time when has passed. A may be cut short when an "out-of-cadance" advance operation is performed - such operation is triggered by /// calls, as well as by and events. /// - /// Under certain conditions, an advance operation may be incurred by calls. + /// Under certain conditions, an advance operation may be incurred by calls. public class ClockQuantizer //: IAsyncDisposable, IDisposable { - private struct AdvancePreparationInfo - { - public Interval Interval; - public ClockQuantizer.NewIntervalEventArgs EventArgs; - - public AdvancePreparationInfo(Interval interval, ClockQuantizer.NewIntervalEventArgs eventArgs) - { - Interval = interval; - EventArgs = eventArgs; - } - } - private readonly ISystemClock _clock; private Interval? _currentInterval; private readonly System.Threading.Timer? _metronome; - // Properties + #region Fields & properties + /// /// The maximum of each , defined at construction. /// @@ -38,12 +27,66 @@ public AdvancePreparationInfo(Interval interval, ClockQuantizer.NewIntervalEvent /// A starts in an inhibited state. Only after the first advance operation, will have a non- value. public Interval? CurrentInterval { get => _currentInterval; } + /// + /// Represents the clock-specific offset at which the next event is expected. + /// + /// + /// While uninitialized initially, will always have a value after the first advance operation. Basically, having + /// CurrentInterval.ClockOffset + TimeSpanToClockOffsetUnits(MaxIntervalTimeSpan) pre-calculated at the start of each metronomic interval, ammortizes the cost of this typical calculation during time-based decisions. + /// When an "out-of-cadance" (i.e. non-metronomic) advance operation is performed, (and its offset) will update, but not . + /// + public long? NextMetronomicClockOffset { get; private set; } + + /// 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 => NewDisconnectedInterval().DateTimeOffset; } + public DateTimeOffset UtcNow { get => _clock.UtcNow; } + + /// Returns the value of the reference clock. + public long UtcNowClockOffset { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _clock.UtcNowClockOffset; } + + #endregion - // Basic quantizer operations + #region Time representation conversions + + /// + /// Converts a to an offset in clock-specific units (ticks). + /// + /// The to convert + /// An offset in clock-specific units. + /// + public long DateTimeOffsetToClockOffset(DateTimeOffset offset) => _clock.DateTimeOffsetToClockOffset(offset); + + /// + /// Converts an offset in clock-specific units (ticks) to a . + /// + /// The clock-specific offset to convert + /// A in UTC. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public DateTimeOffset ClockOffsetToUtcDateTimeOffset(long offset) => _clock.ClockOffsetToUtcDateTimeOffset(offset); + + /// + /// Converts a to a count of clock-specific offset units (ticks). + /// + /// 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); + + /// + /// 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); + + #endregion + + + #region Basic quantizer & clock-offset-serial position operations + /// /// Establishes a new lower bound on the "last seen" exact within the /// 's temporal context: the reference clock's . @@ -51,55 +94,57 @@ public AdvancePreparationInfo(Interval interval, ClockQuantizer.NewIntervalEvent /// The newly started . public Interval Advance() => Advance(metronomic: false); - - // Basic position operations /// - /// If does not have an exact yet, it will be initialized with one. In every + /// If does not have an exact yet, it will be initialized with one. In every /// situation where initialization is still required, this will incur a call into the reference clock's . /// - /// Reference to an (on-stack) which may or may not have been initialized. + /// Reference to an (on-stack) which may or may not have been initialized. /// Indicates if the should perform an advance operation. This is advised in situations where non-exact /// positions may still be acquired in the same and exact ordering (e.g. in a cache LRU eviction algorithm) might be adversely affected. /// /// An advance operation will incur an event. /// Depending on the actual reference clock implementation, this may or may not incur an expensive system call. /// - public void EnsureInitializedExactTimeSerialPosition(ref LazyTimeSerialPosition position, bool advance) + public void EnsureInitializedExactClockOffsetSerialPosition(ref LazyClockOffsetSerialPosition position, bool advance) { if (!position.IsExact) // test here as well to prevent unnecessary/unexpected Advance() if position was already initialzed { if (advance) { var preparation = PrepareAdvance(metronomic: false); - Interval.EnsureInitializedTimeSerialPosition(preparation.Interval, ref position); + Interval.EnsureInitializedClockOffsetSerialPosition(preparation.Interval, ref position); CommitAdvance(preparation); } else { - Interval.EnsureInitializedTimeSerialPosition(NewDisconnectedInterval(), ref position); + Interval.EnsureInitializedClockOffsetSerialPosition(NewDisconnectedInterval(), ref position); } } } /// - /// If does not have a yet, it will be initialized with one. + /// If does not have an yet, it will be initialized with one. /// - /// Reference to an (on-stack) which may or may not have been initialized. + /// Reference to an (on-stack) which may or may not have been initialized. /// - /// If the did not perform a first advance operation yet, the result will be an exact position + /// If the had not performed a first advance operation yet, the result will be an exact position /// (incurring a call into the reference clock's ). Otherwise, returns a position bound to - /// 's , but with an incremented . + /// 's , but with an incremented . /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void EnsureInitializedTimeSerialPosition(ref LazyTimeSerialPosition position) + public void EnsureInitializedClockOffsetSerialPosition(ref LazyClockOffsetSerialPosition position) { if (!position.HasValue) { - Interval.EnsureInitializedTimeSerialPosition(_currentInterval ?? NewDisconnectedInterval(), ref position); + Interval.EnsureInitializedClockOffsetSerialPosition(_currentInterval ?? NewDisconnectedInterval(), ref position); } } - // Events + #endregion + + + #region Events + /// /// Represents the ephemeral conditions at the time of an advance operation. /// @@ -160,6 +205,8 @@ internal NewIntervalEventArgs(DateTimeOffset offset, bool metronomic, TimeSpan? /// protected virtual void OnMetronomeTicked(NewIntervalEventArgs e) => MetronomeTicked?.Invoke(this, e); + #endregion + // Construction @@ -197,8 +244,20 @@ public ClockQuantizer(ISystemClock clock, TimeSpan maxIntervalTimeSpan) } } + private struct AdvancePreparationInfo + { + public Interval Interval; + public ClockQuantizer.NewIntervalEventArgs EventArgs; + + public AdvancePreparationInfo(Interval interval, ClockQuantizer.NewIntervalEventArgs eventArgs) + { + Interval = interval; + EventArgs = eventArgs; + } + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] - private Interval NewDisconnectedInterval() => new Interval(_clock.UtcNow); + private Interval NewDisconnectedInterval() => new Interval(UtcNowClockOffset); private Interval Advance(bool metronomic) { @@ -223,7 +282,7 @@ private AdvancePreparationInfo PrepareAdvance(bool metronomic) // Ignore potential *internal* metronome gap due to tiny clock jitter if (!metronomic || _metronome is null) { - var gap = interval.DateTimeOffset - (previousInterval.DateTimeOffset + MaxIntervalTimeSpan); + var gap = ClockOffsetUnitsToTimeSpan(interval.ClockOffset - previousInterval.ClockOffset) - MaxIntervalTimeSpan; if (gap > TimeSpan.Zero) { detectedGap = gap; @@ -231,15 +290,21 @@ private AdvancePreparationInfo PrepareAdvance(bool metronomic) } } - var e = new NewIntervalEventArgs(interval.DateTimeOffset, metronomic, detectedGap); + var e = new NewIntervalEventArgs(_clock.ClockOffsetToUtcDateTimeOffset(interval.ClockOffset), metronomic, detectedGap); return new AdvancePreparationInfo(interval, e); } private Interval CommitAdvance(AdvancePreparationInfo preparation) { - _currentInterval = preparation.Interval.Seal(); ; + _currentInterval = preparation.Interval.Seal(); + var e = preparation.EventArgs; + if (e.IsMetronomic) + { + NextMetronomicClockOffset = _clock.DateTimeOffsetToClockOffset(e.DateTimeOffset + MaxIntervalTimeSpan); + } + OnAdvanced(e); if (e.IsMetronomic) diff --git a/src/Interval.cs b/src/Interval.cs index 6f6b680..7e722e2 100644 --- a/src/Interval.cs +++ b/src/Interval.cs @@ -1,4 +1,3 @@ -using System; using System.Runtime.CompilerServices; using System.Threading; @@ -10,7 +9,7 @@ namespace ClockQuantization /// /// /// Within the reference frame of an , there is no notion of time; there is only notion of the order - /// in which s are issued. + /// in which s are issued. /// /// /// Whereas is always progressing with intervals of at most length, @@ -19,70 +18,70 @@ namespace ClockQuantization /// public class Interval { - internal struct SnapshotGenerator + internal struct SnapshotTracker { internal uint SerialPosition; - internal readonly DateTimeOffset DateTimeOffset; + internal readonly long ClockOffset; [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static ref readonly SnapshotGenerator WithNextSerialPosition(ref SnapshotGenerator generator) + internal static ref readonly SnapshotTracker WithNextSerialPosition(ref SnapshotTracker tracker) { #if NET5_0 - Interlocked.Increment(ref generator.SerialPosition); + Interlocked.Increment(ref tracker.SerialPosition); #else - Interlocked.Add(ref Unsafe.As(ref generator.SerialPosition), 1); + Interlocked.Add(ref Unsafe.As(ref tracker.SerialPosition), 1); #endif - return ref generator; + return ref tracker; } - internal SnapshotGenerator(in DateTimeOffset offset) { SerialPosition = 0u; DateTimeOffset = offset; } + internal SnapshotTracker(in long offset) { SerialPosition = 0u; ClockOffset = offset; } } - private SnapshotGenerator _generator; + private SnapshotTracker _tracker; /// - /// The within the temporal context when the was started. + /// The offset within the temporal context when the was started. /// - public DateTimeOffset DateTimeOffset { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _generator.DateTimeOffset; } + public long ClockOffset { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _tracker.ClockOffset; } - internal Interval(in DateTimeOffset offset) => _generator = new SnapshotGenerator(in offset); + internal Interval(in long offset) => _tracker = new SnapshotTracker(in offset); /// - /// If does not have a yet, it will be initialized with one, - /// based off 's and its monotonically increasing internal serial position. + /// If does not have an yet, it will be initialized with one, + /// based off 's and its monotonically increasing internal serial position. /// - /// The interval to create the off. - /// Reference to an (on-stack) which may or may not have been initialized. + /// The interval to initialize the off. + /// Reference to an (on-stack) which may or may not have been initialized. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void EnsureInitializedTimeSerialPosition(Interval interval, ref LazyTimeSerialPosition position) + public static void EnsureInitializedClockOffsetSerialPosition(Interval interval, ref LazyClockOffsetSerialPosition position) { - if (position.HasValue && interval._generator.SerialPosition > 0u) + if (position.HasValue && interval._tracker.SerialPosition > 0u) { return; } - LazyTimeSerialPosition.ApplySnapshot(ref position, in SnapshotGenerator.WithNextSerialPosition(ref interval._generator)); + LazyClockOffsetSerialPosition.ApplySnapshot(ref position, in SnapshotTracker.WithNextSerialPosition(ref interval._tracker)); } /// - /// Creates a new based off the 's and its monotonically increasing internal serial position. + /// Creates a new based off the 's and its monotonically increasing internal serial position. /// - /// A new + /// A new /// - /// A created at the time when a new is created (e.g. during - /// ) will have equal + /// A created at the time when a new is created (e.g. during + /// ) will have equal /// to . /// - public LazyTimeSerialPosition NewTimeSerialPosition() => new LazyTimeSerialPosition(in SnapshotGenerator.WithNextSerialPosition(ref _generator)); + public LazyClockOffsetSerialPosition NewClockOffsetSerialPosition() => new LazyClockOffsetSerialPosition(in SnapshotTracker.WithNextSerialPosition(ref _tracker)); internal Interval Seal() { // Prevent 'Exact' positions post initialization of the Interval; ensure SerialPosition > 0 #if NET5_0 - Interlocked.CompareExchange(ref _generator.SerialPosition, 1u, 0u); + Interlocked.CompareExchange(ref _tracker.SerialPosition, 1u, 0u); #else - Interlocked.CompareExchange(ref Unsafe.As(ref _generator.SerialPosition), 1, 0); + Interlocked.CompareExchange(ref Unsafe.As(ref _tracker.SerialPosition), 1, 0); #endif return this; diff --git a/src/LazyTimeSerialPosition.cs b/src/LazyClockOffsetSerialPosition.cs similarity index 53% rename from src/LazyTimeSerialPosition.cs rename to src/LazyClockOffsetSerialPosition.cs index 33a5333..4fcd54c 100644 --- a/src/LazyTimeSerialPosition.cs +++ b/src/LazyClockOffsetSerialPosition.cs @@ -6,28 +6,28 @@ namespace ClockQuantization { /// /// - /// Represents a point in time, expressed as a combination of and . Its value may be unitialized, + /// Represents a point in time, expressed as a combination of a clock-specific and . Its value may be unitialized, /// as indicated by its property. /// /// /// When initialized (i.e. when equals ), the following rules apply: /// - /// Issuance of an "exact" can only occur at start. By definition, will equal - /// , will equal 1u and will equal . - /// Any issued off the same with N (N > 1u) was issued - /// at a later point in (continuous) time than the with equals N-1 and was issued at an earlier - /// point in (continuous) time than any with > N. + /// Issuance of an "exact" can only occur at start. By definition, will equal + /// , will equal 1u and will equal . + /// Any issued off the same with N (N > 1u) was issued + /// at a later point in (continuous) time than the with equals N-1 and was issued at an earlier + /// point in (continuous) time than any with > N. /// /// /// /// - /// With several methods available to lazily initialize a by reference, it is possible to create s + /// With several methods available to lazily initialize a by reference, it is possible to create s /// on-stack and initialize them as late as possible and only if deemed necessary for the operation/decision at hand. - /// - /// - /// + /// + /// + /// /// - public struct LazyTimeSerialPosition + public struct LazyClockOffsetSerialPosition { private static class ThrowHelper @@ -38,23 +38,23 @@ private static class ThrowHelper private readonly struct Snapshot { - public readonly DateTimeOffset DateTimeOffset; + public readonly long ClockOffset; public readonly uint SerialPosition; [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal Snapshot(in Interval.SnapshotGenerator generator) + internal Snapshot(in Interval.SnapshotTracker tracker) { - SerialPosition = generator.SerialPosition; - DateTimeOffset = generator.DateTimeOffset; + SerialPosition = tracker.SerialPosition; + ClockOffset = tracker.ClockOffset; } } private Snapshot _snapshot; - /// Returns the assigned to the current value. + /// Returns the offset assigned to the current value. /// When is . [DebuggerBrowsable(DebuggerBrowsableState.Never)] - public readonly DateTimeOffset DateTimeOffset { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => HasValue ? _snapshot.DateTimeOffset : throw ThrowHelper.CreateInvalidOperationException(); } + public readonly long ClockOffset { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => HasValue ? _snapshot.ClockOffset : throw ThrowHelper.CreateInvalidOperationException(); } /// Returns the serial position assigned to the current value. /// When is . @@ -64,16 +64,16 @@ internal Snapshot(in Interval.SnapshotGenerator generator) /// Returns if a value is assigned, otherwise. public readonly bool HasValue { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _snapshot.SerialPosition > 0u; } - /// Returns if a value is assigned and said value represents the first issued at . In other words, - /// the value was assigned exactly at . + /// Returns if a value is assigned and said value represents the first issued at . In other words, + /// the value was assigned exactly at . public readonly bool IsExact { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _snapshot.SerialPosition == 1u; } - internal LazyTimeSerialPosition(in Interval.SnapshotGenerator generator) { _snapshot = new Snapshot(in generator); } + internal LazyClockOffsetSerialPosition(in Interval.SnapshotTracker tracker) { _snapshot = new Snapshot(in tracker); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static void ApplySnapshot(ref LazyTimeSerialPosition position, in Interval.SnapshotGenerator generator) + internal static void ApplySnapshot(ref LazyClockOffsetSerialPosition position, in Interval.SnapshotTracker tracker) { - position._snapshot = new Snapshot(in generator); + position._snapshot = new Snapshot(in tracker); } } } diff --git a/tests/ClockQuantization.Tests/ClockQuantizerTests.cs b/tests/ClockQuantization.Tests/ClockQuantizerTests.cs index b44896c..51918c9 100644 --- a/tests/ClockQuantization.Tests/ClockQuantizerTests.cs +++ b/tests/ClockQuantization.Tests/ClockQuantizerTests.cs @@ -27,7 +27,7 @@ public void ClockQuantizer_WithTestClock_AddWithGap_RaisesAdvanceEventWithDetect quantizer.Advance(); // Advance once to ensure that we have a CurrentInterval Assert.NotNull(quantizer.CurrentInterval); - Assert.Equal(now, quantizer.CurrentInterval!.DateTimeOffset); + Assert.Equal(context.DateTimeOffsetToClockOffset(now), quantizer.CurrentInterval!.ClockOffset); quantizer.Advanced += Quantizer_Advanced; @@ -70,7 +70,7 @@ public void ClockQuantizer_WithTestClock_AddWithoutGap_RaisesAdvanceEventWithout quantizer.Advance(); // Advance once to ensure that we have a CurrentInterval Assert.NotNull(quantizer.CurrentInterval); - Assert.Equal(now, quantizer.CurrentInterval!.DateTimeOffset); + Assert.Equal(context.DateTimeOffsetToClockOffset(now), quantizer.CurrentInterval!.ClockOffset); quantizer.Advanced += Quantizer_Advanced; @@ -112,7 +112,7 @@ public void ClockQuantizer_WithTestClock_AdjustClockWithGap_RaisesAdvanceEventWi quantizer.Advance(); // Advance once to ensure that we have a CurrentInterval Assert.NotNull(quantizer.CurrentInterval); - Assert.Equal(now, quantizer.CurrentInterval!.DateTimeOffset); + Assert.Equal(context.DateTimeOffsetToClockOffset(now), quantizer.CurrentInterval!.ClockOffset); quantizer.Advanced += Quantizer_Advanced; @@ -155,7 +155,7 @@ public void ClockQuantizer_WithTestClock_AdjustClockWithoutGap_RaisesAdvanceEven quantizer.Advance(); // Advance once to ensure that we have a CurrentInterval Assert.NotNull(quantizer.CurrentInterval); - Assert.Equal(now, quantizer.CurrentInterval!.DateTimeOffset); + Assert.Equal(context.DateTimeOffsetToClockOffset(now), quantizer.CurrentInterval!.ClockOffset); quantizer.Advanced += Quantizer_Advanced; @@ -330,6 +330,7 @@ void Quantizer_MetronomeTicked(object? sender, ClockQuantizer.NewIntervalEventAr public void ClockQuantizer_WithInternalMetronome_RaisesPeriodicMetronomeTickedEventsWithMetronomeJitterGapsIgnored() { // Juggling interval parameters to ensure we have an as little flaky as possible P95 test within approx. 1 second + const double targetP = 0.95; const int intervalCount = 25; var maxIntervalTimeSpan = TimeSpan.FromMilliseconds(42.5); var tolerance = TimeSpan.FromMilliseconds(15); // this is just about Timer precision? @@ -339,48 +340,64 @@ public void ClockQuantizer_WithInternalMetronome_RaisesPeriodicMetronomeTickedEv var considered = new System.Threading.Semaphore(intervalCount, intervalCount); var visited = new System.Threading.Semaphore(0, intervalCount); - var list = new List(intervalCount); + var list = new List(intervalCount); quantizer.MetronomeTicked += Quantizer_MetronomeTicked; // Kick off! - var jitters = 0; - var outliers = 0; - var start = quantizer.Advance().DateTimeOffset; + var start = quantizer.Advance().ClockOffset; for (int i = 0; i < intervalCount; i++) { - visited.WaitOne(maxIntervalTimeSpan + TimeSpan.FromMilliseconds(250)); + visited.WaitOne(maxIntervalTimeSpan + TimeSpan.FromMilliseconds(50)); } Assert.Equal(intervalCount, list.Count); + // Estimate weighed average skew + double weighedSkewSum = 0.0; + for (int i = 0; i < intervalCount; i++) + { + double skew = (double)list[i] - (i * maxIntervalTimeSpan.TotalMilliseconds * context.ClockOffsetUnitsPerMillisecond) - start; + var weighedSkewSumContribution = skew * (i + 1); + weighedSkewSum += weighedSkewSumContribution; + } + + var weighedAverageSkew = weighedSkewSum / (intervalCount * (intervalCount + 1) / 2.0); + + var jitters = 0; + var outliers = 0; for (int i = 0; i < intervalCount; i++) { - var offset = list[i] - start; - if (!(offset >= i * maxIntervalTimeSpan - tolerance && offset <= i * maxIntervalTimeSpan + tolerance)) + var delta = list[i] - (start + weighedAverageSkew); + var lower = (i * maxIntervalTimeSpan - tolerance).TotalMilliseconds * context.ClockOffsetUnitsPerMillisecond; + var upper = (i * maxIntervalTimeSpan + tolerance).TotalMilliseconds * context.ClockOffsetUnitsPerMillisecond; + + if (!(delta >= lower && (delta <= upper))) { outliers++; } - if (offset > i * maxIntervalTimeSpan) + if (delta > upper) { jitters++; } } +#if false // Ensure that we observed at least 1 jitter and validated that the gap was not registered in the event - Assert.True(jitters > 0); + Assert.True(jitters > 0, $"#jitters: {jitters}"); +#endif - // Ensure 95% of measurements within tolerance + // Ensure targetP % of measurements within tolerance var p = (double)outliers / (double)intervalCount; - Assert.True(p <= 0.05, $"P95 not achieved; actual: {1.0 - p}"); + Assert.True(p <= (1 - targetP), $"P{(int) (targetP * 100.0)} not achieved; actual: {1.0 - p}"); void Quantizer_MetronomeTicked(object? sender, ClockQuantizer.NewIntervalEventArgs e) { if (considered.WaitOne(0)) { - list.Add(e.DateTimeOffset); + list.Add(context.DateTimeOffsetToClockOffset(e.DateTimeOffset)); - // Even jitter should not be registered on *internal* metronome + // Jitter should not be administered on *internal* metronome events Assert.False(e.GapToPriorIntervalExpectedEnd.HasValue); visited.Release(); @@ -389,7 +406,7 @@ void Quantizer_MetronomeTicked(object? sender, ClockQuantizer.NewIntervalEventAr } [Fact] - public void ClockQuantizer_EnsureInitializedExactTimeSerialPosition_WithDefaultRef_ByDefinitionMustInitializeExactTimeSerialPosition() + public void ClockQuantizer_EnsureInitializedExactClockOffsetSerialPosition_WithDefaultRef_ByDefinitionMustInitializeExactClockOffsetSerialPosition() { var metronomeOptions = MetronomeOptions.Manual; var now = DateTimeOffset.UtcNow; @@ -402,8 +419,8 @@ public void ClockQuantizer_EnsureInitializedExactTimeSerialPosition_WithDefaultR var interval = quantizer.Advance(); // Execute - var position = default(LazyTimeSerialPosition); - quantizer.EnsureInitializedExactTimeSerialPosition(ref position, advance: false); + var position = default(LazyClockOffsetSerialPosition); + quantizer.EnsureInitializedExactClockOffsetSerialPosition(ref position, advance: false); // Test Assert.True(position.HasValue); @@ -411,7 +428,7 @@ public void ClockQuantizer_EnsureInitializedExactTimeSerialPosition_WithDefaultR } [Fact] - public void ClockQuantizer_EnsureInitializedExactTimeSerialPosition_WithoutAdvanceAndWithExactRef_ByDefinitionRemainsUntouched() + public void ClockQuantizer_EnsureInitializedExactClockOffsetSerialPosition_WithoutAdvanceAndWithExactRef_ByDefinitionRemainsUntouched() { var metronomeOptions = MetronomeOptions.Manual; var now = DateTimeOffset.UtcNow; @@ -423,8 +440,8 @@ public void ClockQuantizer_EnsureInitializedExactTimeSerialPosition_WithoutAdvan var quantizer = new ClockQuantizer(context, metronomeOptions.MaxIntervalTimeSpan); var interval = quantizer.Advance(); - var position = default(LazyTimeSerialPosition); - quantizer.EnsureInitializedExactTimeSerialPosition(ref position, advance: false); + var position = default(LazyClockOffsetSerialPosition); + quantizer.EnsureInitializedExactClockOffsetSerialPosition(ref position, advance: false); Assert.True(position.HasValue); Assert.True(position.IsExact); @@ -432,14 +449,14 @@ public void ClockQuantizer_EnsureInitializedExactTimeSerialPosition_WithoutAdvan var copy = position; // Execute - quantizer.EnsureInitializedExactTimeSerialPosition(ref position, advance: false); + quantizer.EnsureInitializedExactClockOffsetSerialPosition(ref position, advance: false); // Test Assert.Equal(copy, position); } [Fact] - public void ClockQuantizer_EnsureInitializedExactTimeSerialPosition_WithAdvanceAndWithExactRef_ByDefinitionRemainsUntouchedAndDoesNotAdvanceNorRaiseAdvancedEvent() + public void ClockQuantizer_EnsureInitializedExactClockOffsetSerialPosition_WithAdvanceAndWithExactRef_ByDefinitionRemainsUntouchedAndDoesNotAdvanceNorRaiseAdvancedEvent() { var metronomeOptions = MetronomeOptions.Manual; var now = DateTimeOffset.UtcNow; @@ -452,8 +469,8 @@ public void ClockQuantizer_EnsureInitializedExactTimeSerialPosition_WithAdvanceA var quantizer = new ClockQuantizer(context, metronomeOptions.MaxIntervalTimeSpan); var interval = quantizer.Advance(); - var position = default(LazyTimeSerialPosition); - quantizer.EnsureInitializedExactTimeSerialPosition(ref position, advance: false); + var position = default(LazyClockOffsetSerialPosition); + quantizer.EnsureInitializedExactClockOffsetSerialPosition(ref position, advance: false); Assert.True(position.HasValue); Assert.True(position.IsExact); @@ -463,7 +480,7 @@ public void ClockQuantizer_EnsureInitializedExactTimeSerialPosition_WithAdvanceA quantizer.Advanced += Quantizer_Advanced; // Execute - quantizer.EnsureInitializedExactTimeSerialPosition(ref position, advance: true); + quantizer.EnsureInitializedExactClockOffsetSerialPosition(ref position, advance: true); Assert.Same(interval, quantizer.CurrentInterval); // Test @@ -478,7 +495,7 @@ void Quantizer_Advanced(object? sender, ClockQuantizer.NewIntervalEventArgs e) } [Fact] - public void ClockQuantizer_EnsureInitializedExactTimeSerialPosition_WithNonExactRef_ByDefinitionMustReInitializeExactTimeSerialPosition() + public void ClockQuantizer_EnsureInitializedExactClockOffsetSerialPosition_WithNonExactRef_ByDefinitionMustReInitializeExactClockOffsetSerialPosition() { var metronomeOptions = MetronomeOptions.Manual; var now = DateTimeOffset.UtcNow; @@ -490,8 +507,8 @@ public void ClockQuantizer_EnsureInitializedExactTimeSerialPosition_WithNonExact var quantizer = new ClockQuantizer(context, metronomeOptions.MaxIntervalTimeSpan); var interval = quantizer.Advance(); - var position = default(LazyTimeSerialPosition); - quantizer.EnsureInitializedTimeSerialPosition(ref position); + var position = default(LazyClockOffsetSerialPosition); + quantizer.EnsureInitializedClockOffsetSerialPosition(ref position); Assert.True(position.HasValue); Assert.False(position.IsExact); @@ -499,7 +516,7 @@ public void ClockQuantizer_EnsureInitializedExactTimeSerialPosition_WithNonExact var copy = position; // Execute - quantizer.EnsureInitializedExactTimeSerialPosition(ref position, advance: false); + quantizer.EnsureInitializedExactClockOffsetSerialPosition(ref position, advance: false); // Test Assert.NotEqual(copy, position); @@ -507,7 +524,7 @@ public void ClockQuantizer_EnsureInitializedExactTimeSerialPosition_WithNonExact } [Fact] - public void ClockQuantizer_EnsureInitializedTimeSerialPosition_AfterFirstAdvanceWithDefaultRef_ByDefinitionMustInitializeNonExactTimeSerialPosition() + public void ClockQuantizer_EnsureInitializedClockOffsetSerialPosition_AfterFirstAdvanceWithDefaultRef_ByDefinitionMustInitializeNonExactClockOffsetSerialPosition() { var metronomeOptions = MetronomeOptions.Manual; var now = DateTimeOffset.UtcNow; @@ -520,17 +537,17 @@ public void ClockQuantizer_EnsureInitializedTimeSerialPosition_AfterFirstAdvance quantizer.Advance(); // Execute - var position = default(LazyTimeSerialPosition); - quantizer.EnsureInitializedTimeSerialPosition(ref position); + var position = default(LazyClockOffsetSerialPosition); + quantizer.EnsureInitializedClockOffsetSerialPosition(ref position); // Test Assert.True(position.HasValue); Assert.False(position.IsExact); - Assert.Equal(quantizer.CurrentInterval!.DateTimeOffset, position.DateTimeOffset); + Assert.Equal(quantizer.CurrentInterval!.ClockOffset, position.ClockOffset); } [Fact] - public void ClockQuantizer_EnsureInitializedTimeSerialPosition_BeforeFirstAdvanceWithDefaultRef_ByDefinitionMustInitializeExactTimeSerialPosition() + public void ClockQuantizer_EnsureInitializedClockOffsetSerialPosition_BeforeFirstAdvanceWithDefaultRef_ByDefinitionMustInitializeExactClockOffsetSerialPosition() { var metronomeOptions = MetronomeOptions.Manual; var now = DateTimeOffset.UtcNow; @@ -542,8 +559,8 @@ public void ClockQuantizer_EnsureInitializedTimeSerialPosition_BeforeFirstAdvanc var quantizer = new ClockQuantizer(context, metronomeOptions.MaxIntervalTimeSpan); // Execute - var position = default(LazyTimeSerialPosition); - quantizer.EnsureInitializedTimeSerialPosition(ref position); + var position = default(LazyClockOffsetSerialPosition); + quantizer.EnsureInitializedClockOffsetSerialPosition(ref position); // Test Assert.True(position.HasValue); @@ -552,7 +569,7 @@ public void ClockQuantizer_EnsureInitializedTimeSerialPosition_BeforeFirstAdvanc } [Fact] - public void ClockQuantizer_EnsureInitializedTimeSerialPosition_WithExactRef_ByDefinitionRemainsUntouched() + public void ClockQuantizer_EnsureInitializedClockOffsetSerialPosition_WithExactRef_ByDefinitionRemainsUntouched() { var metronomeOptions = MetronomeOptions.Manual; var now = DateTimeOffset.UtcNow; @@ -564,8 +581,8 @@ public void ClockQuantizer_EnsureInitializedTimeSerialPosition_WithExactRef_ByDe var quantizer = new ClockQuantizer(context, metronomeOptions.MaxIntervalTimeSpan); var interval = quantizer.Advance(); - var position = default(LazyTimeSerialPosition); - quantizer.EnsureInitializedExactTimeSerialPosition(ref position, advance: false); + var position = default(LazyClockOffsetSerialPosition); + quantizer.EnsureInitializedExactClockOffsetSerialPosition(ref position, advance: false); Assert.True(position.HasValue); Assert.True(position.IsExact); @@ -573,14 +590,14 @@ public void ClockQuantizer_EnsureInitializedTimeSerialPosition_WithExactRef_ByDe var copy = position; // Execute - quantizer.EnsureInitializedTimeSerialPosition(ref position); + quantizer.EnsureInitializedClockOffsetSerialPosition(ref position); // Test Assert.Equal(copy, position); } [Fact] - public void ClockQuantizer_EnsureInitializedTimeSerialPosition_WithNonExactRef_ByDefinitionRemainsUntouched() + public void ClockQuantizer_EnsureInitializedClockOffsetSerialPosition_WithNonExactRef_ByDefinitionRemainsUntouched() { var metronomeOptions = MetronomeOptions.Manual; var now = DateTimeOffset.UtcNow; @@ -592,8 +609,8 @@ public void ClockQuantizer_EnsureInitializedTimeSerialPosition_WithNonExactRef_B var quantizer = new ClockQuantizer(context, metronomeOptions.MaxIntervalTimeSpan); var interval = quantizer.Advance(); - var position = default(LazyTimeSerialPosition); - quantizer.EnsureInitializedTimeSerialPosition(ref position); + var position = default(LazyClockOffsetSerialPosition); + quantizer.EnsureInitializedClockOffsetSerialPosition(ref position); Assert.True(position.HasValue); Assert.False(position.IsExact); @@ -601,7 +618,7 @@ public void ClockQuantizer_EnsureInitializedTimeSerialPosition_WithNonExactRef_B var copy = position; // Execute - quantizer.EnsureInitializedTimeSerialPosition(ref position); + quantizer.EnsureInitializedClockOffsetSerialPosition(ref position); // Test Assert.Equal(copy, position); @@ -627,11 +644,11 @@ public void ClockQuantizer_Advance_YieldsNewSealedIntervalAndRaisesEvent() // Test Assert.NotSame(interval1, interval2); - Assert.True(interval1.DateTimeOffset < interval2.DateTimeOffset); + Assert.True(interval1.ClockOffset <= interval2.ClockOffset); // Ensure interval2 was sealed (and hence a new position can by definition not be "exact") - var position = default(LazyTimeSerialPosition); - Interval.EnsureInitializedTimeSerialPosition(interval2, ref position); + var position = default(LazyClockOffsetSerialPosition); + Interval.EnsureInitializedClockOffsetSerialPosition(interval2, ref position); Assert.False(position.IsExact); Assert.True(isAdvancedEventRaised); @@ -643,7 +660,7 @@ void Quantizer_Advanced(object? sender, ClockQuantizer.NewIntervalEventArgs e) } [Fact] - public void ClockQuantizer_EnsureInitializedExactTimeSerialPosition_WithoutAdvanceAndWithDefaultRef_DoesNotAdvanceCurrentInterval() + public void ClockQuantizer_EnsureInitializedExactClockOffsetSerialPosition_WithoutAdvanceAndWithDefaultRef_DoesNotAdvanceCurrentInterval() { var metronomeOptions = MetronomeOptions.Manual; var context = new SystemClockTemporalContext(() => DateTimeOffset.UtcNow, metronomeOptions); @@ -656,18 +673,18 @@ public void ClockQuantizer_EnsureInitializedExactTimeSerialPosition_WithoutAdvan Assert.Same(interval, quantizer.CurrentInterval); // Execute - var position = default(LazyTimeSerialPosition); - quantizer.EnsureInitializedExactTimeSerialPosition(ref position, advance:false); + var position = default(LazyClockOffsetSerialPosition); + quantizer.EnsureInitializedExactClockOffsetSerialPosition(ref position, advance: false); // Test Assert.True(position.HasValue); Assert.True(position.IsExact); Assert.Same(interval, quantizer.CurrentInterval); - Assert.True(quantizer.CurrentInterval!.DateTimeOffset < position.DateTimeOffset); + Assert.True(quantizer.CurrentInterval!.ClockOffset <= position.ClockOffset); } [Fact] - public void ClockQuantizer_EnsureInitializedExactTimeSerialPosition_WithAdvanceAndWithDefaultRef_AdvancesCurrentIntervalAndRaisesEvent() + public void ClockQuantizer_EnsureInitializedExactClockOffsetSerialPosition_WithAdvanceAndWithDefaultRef_AdvancesCurrentIntervalAndRaisesEvent() { var metronomeOptions = MetronomeOptions.Manual; var context = new SystemClockTemporalContext(() => DateTimeOffset.UtcNow, metronomeOptions); @@ -683,14 +700,14 @@ public void ClockQuantizer_EnsureInitializedExactTimeSerialPosition_WithAdvanceA quantizer.Advanced += Quantizer_Advanced; // Execute - var position = default(LazyTimeSerialPosition); - quantizer.EnsureInitializedExactTimeSerialPosition(ref position, advance: true); + var position = default(LazyClockOffsetSerialPosition); + quantizer.EnsureInitializedExactClockOffsetSerialPosition(ref position, advance: true); // Test Assert.True(position.HasValue); Assert.True(position.IsExact); Assert.NotSame(interval, quantizer.CurrentInterval); - Assert.True(quantizer.CurrentInterval!.DateTimeOffset == position.DateTimeOffset); + Assert.True(quantizer.CurrentInterval!.ClockOffset == position.ClockOffset); Assert.True(isAdvancedEventRaised); @@ -701,7 +718,7 @@ void Quantizer_Advanced(object? sender, ClockQuantizer.NewIntervalEventArgs e) } [Fact] - public void ClockQuantizer_EnsureInitializedExactTimeSerialPosition_WithAdvanceAndWithDefaultRef_DoesNotGetBrokenByInteractionInAdvancedEvent() + public void ClockQuantizer_EnsureInitializedExactClockOffsetSerialPosition_WithAdvanceAndWithDefaultRef_DoesNotGetBrokenByInteractionInAdvancedEvent() { var metronomeOptions = MetronomeOptions.Manual; var context = new SystemClockTemporalContext(() => DateTimeOffset.UtcNow, metronomeOptions); @@ -717,16 +734,16 @@ public void ClockQuantizer_EnsureInitializedExactTimeSerialPosition_WithAdvanceA quantizer.Advanced += Quantizer_Advanced; // Execute - var position = default(LazyTimeSerialPosition); - quantizer.EnsureInitializedExactTimeSerialPosition(ref position, advance: true); + var position = default(LazyClockOffsetSerialPosition); + quantizer.EnsureInitializedExactClockOffsetSerialPosition(ref position, advance: true); // Test Assert.True(position.HasValue); Assert.True(position.IsExact); Assert.NotSame(interval, quantizer.CurrentInterval); - // quantizer.CurrentInterval advanced once more in the event handler, so we cannot validate position.DateTimeOffset against it - Assert.False(quantizer.CurrentInterval!.DateTimeOffset == position.DateTimeOffset); + // quantizer.CurrentInterval advanced once more in the event handler, so we cannot validate position.Offset against it + Assert.True(quantizer.CurrentInterval!.ClockOffset >= position.ClockOffset); Assert.True(isAdvancedEventRaised); @@ -742,16 +759,16 @@ void Quantizer_Advanced(object? sender, ClockQuantizer.NewIntervalEventArgs e) Assert.NotNull(currentInterval); Assert.NotSame(interval, currentInterval); - var interferingTimeSerialPosition1 = default(LazyTimeSerialPosition); - Interval.EnsureInitializedTimeSerialPosition(interval, ref interferingTimeSerialPosition1); - Assert.True(interferingTimeSerialPosition1.HasValue); + var interferingClockOffsetSerialPosition1 = default(LazyClockOffsetSerialPosition); + Interval.EnsureInitializedClockOffsetSerialPosition(interval, ref interferingClockOffsetSerialPosition1); + Assert.True(interferingClockOffsetSerialPosition1.HasValue); - var interferingTimeSerialPosition2 = currentInterval!.NewTimeSerialPosition(); - Assert.True(interferingTimeSerialPosition2.HasValue); + var interferingClockOffsetSerialPosition2 = currentInterval!.NewClockOffsetSerialPosition(); + Assert.True(interferingClockOffsetSerialPosition2.HasValue); - var interferingTimeSerialPosition3 = default(LazyTimeSerialPosition); - quantizer.EnsureInitializedExactTimeSerialPosition(ref interferingTimeSerialPosition3, advance: recurse /* let's not recurse more than once... */); - Assert.True(interferingTimeSerialPosition3.IsExact); + var interferingClockOffsetSerialPosition3 = default(LazyClockOffsetSerialPosition); + quantizer.EnsureInitializedExactClockOffsetSerialPosition(ref interferingClockOffsetSerialPosition3, advance: recurse /* let's not recurse more than once... */); + Assert.True(interferingClockOffsetSerialPosition3.IsExact); } } } diff --git a/tests/ClockQuantization.Tests/ConversionTests.cs b/tests/ClockQuantization.Tests/ConversionTests.cs new file mode 100644 index 0000000..4b0c0b2 --- /dev/null +++ b/tests/ClockQuantization.Tests/ConversionTests.cs @@ -0,0 +1,64 @@ +using ClockQuantization.Tests.Assets; +using System; +using Xunit; + +namespace ClockQuantization.Tests +{ + public class ConversionTests + { + [Fact] + public void ClockQuantizer_TimeSpanToClockOffsetUnits_RoundTrips() + { + var metronomeOptions = MetronomeOptions.Manual; + var context = new SystemClockTemporalContext(() => DateTimeOffset.UtcNow, metronomeOptions); + var quantizer = new ClockQuantizer(context, metronomeOptions.MaxIntervalTimeSpan); + + var span = TimeSpan.FromMilliseconds(1); + + // Execute & test + Assert.Equal(span, quantizer.ClockOffsetUnitsToTimeSpan(quantizer.TimeSpanToClockOffsetUnits(span))); + } + + [Fact] + public void ClockQuantizer_ClockOffsetUnitsToTimeSpan_RoundTrips() + { + var metronomeOptions = MetronomeOptions.Manual; + var context = new SystemClockTemporalContext(() => DateTimeOffset.UtcNow, metronomeOptions); + var quantizer = new ClockQuantizer(context, metronomeOptions.MaxIntervalTimeSpan); + + var units = 1; + + // Execute & test + Assert.Equal(units, quantizer.TimeSpanToClockOffsetUnits(quantizer.ClockOffsetUnitsToTimeSpan(units))); + } + + [Fact] + public void ClockQuantizer_DateTimeOffsetToClockOffset_RoundTripsWithMillisecondPrecision() + { + var metronomeOptions = MetronomeOptions.Manual; + var context = new SystemClockTemporalContext(() => DateTimeOffset.UtcNow, metronomeOptions); + var quantizer = new ClockQuantizer(context, metronomeOptions.MaxIntervalTimeSpan); + + var now = quantizer.UtcNow; + + // Execute + var difference = quantizer.ClockOffsetToUtcDateTimeOffset(quantizer.DateTimeOffsetToClockOffset(now)) - now; + + // Test + Assert.True(difference > TimeSpan.FromMilliseconds(-1) && difference < TimeSpan.FromMilliseconds(1)); + } + + [Fact] + public void ClockQuantizer_ClockOffsetToUtcDateTimeOffset_RoundTrips() + { + var metronomeOptions = MetronomeOptions.Manual; + var context = new SystemClockTemporalContext(() => DateTimeOffset.UtcNow, metronomeOptions); + var quantizer = new ClockQuantizer(context, metronomeOptions.MaxIntervalTimeSpan); + + var offset = quantizer.UtcNowClockOffset; + + // Execute & test + Assert.Equal(offset, quantizer.DateTimeOffsetToClockOffset(quantizer.ClockOffsetToUtcDateTimeOffset(offset))); + } + } +} diff --git a/tests/ClockQuantization.Tests/IntervalTests.cs b/tests/ClockQuantization.Tests/IntervalTests.cs index 7528a25..9c980cc 100644 --- a/tests/ClockQuantization.Tests/IntervalTests.cs +++ b/tests/ClockQuantization.Tests/IntervalTests.cs @@ -11,7 +11,7 @@ namespace ClockQuantization.Tests public class IntervalTests { [Fact] - public void Interval_NewTimeSerialPosition_ByDefinitionCannotBeExact() + public void Interval_NewClockOffsetSerialPosition_ByDefinitionCannotBeExact() { var metronomeOptions = MetronomeOptions.Manual; var now = DateTimeOffset.UtcNow; @@ -24,7 +24,7 @@ public void Interval_NewTimeSerialPosition_ByDefinitionCannotBeExact() var interval = quantizer.Advance(); // Execute - var position = interval.NewTimeSerialPosition(); + var position = interval.NewClockOffsetSerialPosition(); // A position acquired after the creation of an interval *by definition* can never be exact @@ -34,7 +34,7 @@ public void Interval_NewTimeSerialPosition_ByDefinitionCannotBeExact() } [Fact] - public void Interval_EnsureInitializedTimeSerialPosition_ByDefinitionCannotBeExact() + public void Interval_EnsureInitializedClockOffsetSerialPosition_ByDefinitionCannotBeExact() { var metronomeOptions = MetronomeOptions.Manual; var now = DateTimeOffset.UtcNow; @@ -47,8 +47,8 @@ public void Interval_EnsureInitializedTimeSerialPosition_ByDefinitionCannotBeExa var interval = quantizer.Advance(); // Execute - var position = default(LazyTimeSerialPosition); - Interval.EnsureInitializedTimeSerialPosition(interval, ref position); + var position = default(LazyClockOffsetSerialPosition); + Interval.EnsureInitializedClockOffsetSerialPosition(interval, ref position); // A position acquired after the creation of an interval *by definition* can never be exact @@ -58,7 +58,7 @@ public void Interval_EnsureInitializedTimeSerialPosition_ByDefinitionCannotBeExa } [Fact] - public void Interval_ConsecutivelyAcquiredTimeSerialPositionsAreIssuedNonStrictlyMonotonically() + public void Interval_ConsecutivelyAcquiredClockOffsetSerialPositionsAreIssuedNonStrictlyMonotonically() { const int positionCount = 100; var metronomeOptions = MetronomeOptions.Manual; @@ -75,11 +75,11 @@ public void Interval_ConsecutivelyAcquiredTimeSerialPositionsAreIssuedNonStrictl var sequence = new List(positionCount); for (var i = 0; i < positionCount; i++) { - var position = default(LazyTimeSerialPosition); - Interval.EnsureInitializedTimeSerialPosition(interval, ref position); + var position = default(LazyClockOffsetSerialPosition); + Interval.EnsureInitializedClockOffsetSerialPosition(interval, ref position); sequence.Add(position.SerialPosition); - Assert.Equal(interval.DateTimeOffset, position.DateTimeOffset); + Assert.Equal(interval.ClockOffset, position.ClockOffset); } // Test @@ -91,7 +91,7 @@ public void Interval_ConsecutivelyAcquiredTimeSerialPositionsAreIssuedNonStrictl [Fact] - public void Interval_ConcurrentlyAcquiredTimeSerialPositionsAreIssuedNonStrictlyMonotonically() + public void Interval_ConcurrentlyAcquiredClockOffsetSerialPositionsAreIssuedNonStrictlyMonotonically() { const int positionPerPartitionCount = 16 * 1024; const int partitionCount = 16; @@ -123,8 +123,8 @@ public void Interval_ConcurrentlyAcquiredTimeSerialPositionsAreIssuedNonStrictly // Execute for (var i = range.Item1; i < range.Item2; i++) { - var position = default(LazyTimeSerialPosition); - Interval.EnsureInitializedTimeSerialPosition(interval, ref position); + var position = default(LazyClockOffsetSerialPosition); + Interval.EnsureInitializedClockOffsetSerialPosition(interval, ref position); stringOfPerRangeSequences[i] = position.SerialPosition; } From 96379403ed157d45e6443ecd12f256b82d28035c Mon Sep 17 00:00:00 2001 From: edevoogd Date: Wed, 27 Jan 2021 01:21:44 +0100 Subject: [PATCH 3/4] Update readme.md to reflect trigger and changes --- readme.md | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/readme.md b/readme.md index 69ec250..e2d94c1 100644 --- a/readme.md +++ b/readme.md @@ -3,10 +3,10 @@ Clock Quantization is proposed as a method to decrease the frequency of determining the exact time, while still being able to make time-dependent decisions. -Consider an interval defined as a time span for which we register a "start" `DateTimeOffset` and keep an internal +Consider an interval defined as a time span for which we register a "start" `ClockOffset` (not necessarily `DateTimeOffset`) and keep an internal serial position. The latter is incremented every time that we issue a new so-called time-serial position. Every time that such time-serial position is created off said interval, it will assume two properties: -1. A `DateTimeOffset` which is the copy of the interval's `DateTimeOffset` +1. A `ClockOffset` which is the copy of the interval's `ClockOffset` 2. A `SerialPosition` which is a copy of the interval's internal serial position at the time of issuance. An internal "metronome" ensures that a new interval is periodically started after `MaxIntervalTimeSpan` has passed. @@ -15,8 +15,8 @@ The result: * The continuous clock is quantized into regular intervals with known maximum length, allowing system calls (e.g. `DateTimeOffset.UtcNow`) to be required less frequent and amortized across multiple operations. * The status of "events" that occur outside of the interval can be reasoned about with absolute certainty. E.g., taking `interval` as a reference frame: - * A cache item that expires before `interval.DateTimeOffset` will definitely have expired - * A cache item that expires after `interval.DateTimeOffset + MaxIntervalTimeSpan` will definitely expire in the future (i.e. it has + * A cache item that expires before `ClockOffsetToUtcDateTimeOffset(interval.ClockOffset)` will definitely have expired + * A cache item that expires after `ClockOffsetToUtcDateTimeOffset(interval.ClockOffset) + MaxIntervalTimeSpan` will definitely expire in the future (i.e. it has not expired yet) * The status of "events" that occur inside the interval is in doubt, but policies could define how to deal with that. E.g., with the same example of cache item expiration, one of the following policies could be applied: @@ -28,7 +28,7 @@ The result: the exact time). * The time-serial positions within an interval can be ordered by their `SerialPosition` property, still allowing for e.g. strict LRU orderering. Alternatively, it also makes it possible to apply LRU ordering to a cluster of cache entries (all having - equal `LastAccessed.DateTimeOffset` - now also a time-serial position). + equal `LastAccessed.ClockOffset` - now also a time-serial position). # Context This repo stems from additional research after I posted a [comment](https://github.com/dotnet/runtime/pull/45842#issuecomment-742100677) in PR [dotnet/runtime#45842](https://github.com/dotnet/runtime/pull/45842). In that comment, I did not take into @@ -41,6 +41,26 @@ After some local experimentation with `Lazy` (as suchested in my initial comm I did get some promising results, but realized that it resulted in some quite convoluted code. Hence, I decided to first create and share an abstraction that reflects the idea behind a potential direction for further optimization. +##### 2021-01-27 +Triggered by [@filipnavara](https://github.com/dotnet/runtime/pull/45842#issuecomment-761235581), the latest incarnation of Clock Quantization caters to a non-arbitrary clock-specific representation of time-serial positions, allowing reference clocks to: +* Define the unit scale of clock offset ticks (`ISystemClock.ClockOffsetUnitsPerMillisecond`) +* Take full control off back-and-forth conversion between clock-specific time representation and `System.DateTimeOffset` + * `ISystemClock.ClockOffsetToUtcDateTimeOffset()` + * `ISystemClock.DateTimeOffsetToClockOffset()`. + +Under `#if NET5_0` we can now use `Environment.TickCount64` to implement `ISystemClock.UtcNowClockOffset`. This particular property is not available in +.NET Standard, where we can fall back to using (the less efficient) `DateTimeOffset.UtcNow.UtcTicks`. + +| TargetFramework | `UtcNowClockOffset` | `ClockOffsetUnitsPerMillisecond` | Underlying offset definition | +| ----------------- | ----------------------------------- | ------------------------------------------ | ----------------------------------------- | +| .NET 5 | `Environment.TickCount64` | 1 | Number of milliseconds elapsed since the system started | +| .NET Standard 2.0 | `DateTimeOffset.UtcNow.UtcTicks` | `TimeSpan.TicksPerMillisecond` (10,000) | Number of 100-nanosecond intervals that have elapsed since 1/1/0001 12:00AM | + +Obviously, custom reference clocks (e.g. for testing and/or replay) could define their own offset reference and unit scale. It is up to clock implementers to ensure that +their time representation doesn't overflow in realistic scenarios (such as would e.g. be the case with `Environment.TickCount` which wraps after 24.9 days and again every other approx. 49.8 days). + +Local experimentation shows further advantage to using `Environment.TickCount64` to speed up expiration check scenarios with a "Precise" policy being imposed on cache item expiration. + # Remarks * The `ISystemClockTemporalContext` abstraction introduced as part of this concept might be useful in other scenarios where a synthetic clock and/or timer must be imposed onto a subject/system (e.g. event replay, unit tests). From 5a406b8e6539a96d86e7374ef9eea9530851afb5 Mon Sep 17 00:00:00 2001 From: edevoogd Date: Wed, 27 Jan 2021 01:28:25 +0100 Subject: [PATCH 4/4] Fix typo --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index 0e0b6a5..fc09dcd 100644 --- a/readme.md +++ b/readme.md @@ -27,7 +27,7 @@ The result: the smaller the amount of uncertainty (and number of times that we still need to regress to calling onto the clock to determine the exact time). * The time-serial positions within an interval can be ordered by their `SerialPosition` property, still allowing for e.g. - strict LRU orderering. Alternatively, it also makes it possible to apply LRU ordering to a cluster of cache entries (all having + strict LRU ordering. Alternatively, it also makes it possible to apply LRU ordering to a cluster of cache entries (all having equal `LastAccessed.ClockOffset` - now also a time-serial position). # Context