From a8b83649189734a524d345284b112d447da03fa2 Mon Sep 17 00:00:00 2001 From: edevoogd Date: Wed, 27 Jan 2021 00:02:01 +0100 Subject: [PATCH] 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; }