new simplified timer. and little other ops

This commit is contained in:
Youmu Liu
2025-11-18 20:39:03 +08:00
parent 81a60d1cfe
commit ece12f2ea7
28 changed files with 2254 additions and 853 deletions
@@ -1,6 +1,6 @@
//-----------------------------------------------------------------------
// -----------------------------------------------------------------------
// file="TimeProvider"
// file="StgSharpTime"
// Project: StgSharp
// AuthorGroup: Nitload
// Copyright (c) Nitload. All rights reserved.
@@ -27,12 +27,8 @@
// -----------------------------------------------------------------------
using StgSharp.Collections;
using StgSharp.Timing;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
namespace StgSharp
@@ -40,9 +36,7 @@ namespace StgSharp
public static partial class World
{
internal static StgSharpTime mainTimeProvider = new StgSharpTime();
internal static TimeSourceProviderBase MainTimeProvider => mainTimeProvider;
public static TimeSourceProviderBase MainTimeProvider => StgSharpTime.OnlyInstance;
}
@@ -50,40 +44,30 @@ namespace StgSharp
{
private static readonly LinearBag<TimeSpanProvider> _subscribers = new();
private static long accuracy;
private static readonly Stopwatch mainProvider = new();
private static Thread timeProvideThread;
internal StgSharpTime() { }
private StgSharpTime() { }
public static TimeSourceProviderBase OnlyInstance => World.MainTimeProvider;
// Use Stopwatch frequency as base tick frequency (ticks per second).
public override long Frequency => Stopwatch.Frequency;
public static StgSharpTime OnlyInstance { get; } = new StgSharpTime();
public override long GetCurrentTimeSpanTick() => mainProvider.ElapsedTicks;
public sealed override void StartProvidingTime()
{
if (mainProvider.IsRunning) {
return;
}
mainProvider.Start();
// speed test
long totalMS, internalFrequency = Stopwatch.Frequency;
for (int i = 0; i < 100; i++)
mainProvider.Start();
timeProvideThread = new Thread(ProvideTime)
{
totalMS = mainProvider.ElapsedTicks / (internalFrequency / (1000L * 1000L));
for (int t = 0; t < 10; t++)
{
if (totalMS < 1000) {
_subscribers.Add(
new TimeSpanProvider(100L, TimeSpanProvider.DefaultProvider));
}
}
}
mainProvider.Stop();
_subscribers.Clear();
totalMS = mainProvider.ElapsedTicks / (internalFrequency / (1000L * 1000L)) / 100L;
accuracy = totalMS * 2 / 3;
mainProvider.Reset();
timeProvideThread = new Thread(new ThreadStart(ProvideTime));
IsBackground = true,
Name = "StgSharpTime"
};
timeProvideThread.Start();
}
@@ -113,29 +97,36 @@ namespace StgSharp
private void ProvideTime()
{
long totalMS, internalFrequency = Stopwatch.Frequency;
long internalFrequency = Stopwatch.Frequency; // ticks per second
lock (mainProvider) {
mainProvider.Restart();
}
while (mainProvider.IsRunning)
{
totalMS = mainProvider.ElapsedTicks / (internalFrequency / (1000L * 1000L));
long elapsedTicks;
lock (mainProvider) {
elapsedTicks = mainProvider.ElapsedTicks;
}
if (_subscribers.Count == 0)
{
Thread.SpinWait(50);
continue;
}
lock (SubscribeLock)
{
for (int i = 0; i < _subscribers.Count; i++)
{
if (!_subscribers[i].CheckTime(totalMS))
if (!_subscribers[i].OnSpanTick(elapsedTicks))
{
_subscribers.RemoveAt(i);
DefaultLog.InternalWriteLog(
$"Time subscriber {_subscribers[i]} is to be removed.", LogType.Info);
i--; // adjust index after removal
}
}
}
// Optional: brief spin to avoid tight loop if frame lengths are large.
Thread.SpinWait(20);
}
}
-143
View File
@@ -1,143 +0,0 @@
//-----------------------------------------------------------------------
// -----------------------------------------------------------------------
// file="TimeFunction"
// Project: StgSharp
// AuthorGroup: Nitload
// Copyright (c) Nitload. All rights reserved.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
// -----------------------------------------------------------------------
// -----------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Text;
namespace StgSharp.Timing
{
public class TimeFunction
{
private float _beginning, _ending;
private TimeSpanProvider _timeProvider;
internal TimeFunction() { }
public TimeFunction(TimeSpanProvider timeSpanProvider, Func<float, float> timeRelatedCalculation, float range)
{
_timeProvider = timeSpanProvider;
TimeRelatedCalculation = timeRelatedCalculation;
if (timeSpanProvider == null) {
return;
}
_beginning = _timeProvider.CurrentSecond;
_ending = _timeProvider.CurrentSecond + range;
}
public virtual bool IsComplete
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => _timeProvider.CurrentSecond > _ending;
}
public Func<float, float> TimeRelatedCalculation { get; set; }
public static TimeFunction One
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => new TimeFunctionOne();
}
public TimeSpanProvider TimeSpanProvider
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => _timeProvider;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
set => _timeProvider = value;
}
public virtual float Calculate()
{
if (_timeProvider.CurrentSecond < _ending) {
return 0;
}
return TimeRelatedCalculation(_timeProvider.CurrentSecond - _beginning);
}
public static TimeFunction Proportional(TimeSpanProvider time, float begin, float range)
{
return new TimeFunction(time, (float t) => t + begin, range);
}
public virtual TimeFunction Renew((Func<float, float> func, float range) funcPair)
{
return new TimeFunction(_timeProvider, funcPair.func, funcPair.range);
}
public virtual TimeFunction Renew(Func<float, float> func, float range)
{
return new TimeFunction(_timeProvider, func, range);
}
public virtual void Reset()
{
float range = _ending - _beginning;
_beginning = _timeProvider.CurrentSecond;
_ending = _beginning + range;
}
public virtual TimeFunction Reverse()
{
float range = _ending - _beginning;
return new TimeFunction(
_timeProvider, p => TimeRelatedCalculation(range - p), range);
}
}
public sealed class TimeFunctionOne : TimeFunction
{
internal TimeFunctionOne() : base() { }
public override bool IsComplete => true;
public override float Calculate()
{
return 1;
}
public override TimeFunction Renew((Func<float, float> func, float range) funcPair)
{
throw new NotSupportedException();
}
public override TimeFunction Renew(Func<float, float> func, float range)
{
throw new NotSupportedException();
}
#pragma warning disable CA1822
public override void Reset()
{
}
#pragma warning restore CA1822
}
}
+160 -36
View File
@@ -25,69 +25,193 @@
//
// -----------------------------------------------------------------------
// -----------------------------------------------------------------------
using StgSharp.Mathematics;
using StgSharp.Timing;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
namespace StgSharp.Common.Timing
{
public abstract class TimeSequence
{
public ulong BeginningTick { get; protected set; }
public long BeginningTick { get; protected set; }
public ulong EndingTick { get; protected set; }
public long EndingTick { get; protected set; }
public ulong FrameLength { get; protected set; }
public long PreviousFrameTick { get; protected set; }
public static TimeSequence CreateLinear(ulong timeout, ulong length)
{
return new LinearTimeSequence(timeout, timeout + length, length);
}
public static TimeSequence CreateLinear(double totalSeconds, double frameSeconds) => new LinearTimeSequence(totalSeconds, frameSeconds);
public abstract bool IsNextFrameReady(ulong currentTick);
public static TimeSequence CreateLogarithmic(
double initialSliceSeconds,
double totalSeconds,
double growthMultiplier,
double minSliceSeconds) => new LogTimeSequence(initialSliceSeconds, totalSeconds,
growthMultiplier, minSliceSeconds);
}
public abstract bool EndsAt(long currentTick);
internal sealed class LogTimeSequence : TimeSequence
{
public abstract bool IsNextFrameReady(long currentTick);
private float _rate ;
public LogTimeSequence(ulong beginningTick, ulong endingTick, ulong tickPerDecade)
{
BeginningTick = beginningTick;
EndingTick = endingTick;
_rate = Scalar.Pow(10, 1 / tickPerDecade);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override bool IsNextFrameReady(ulong currentTick)
{
return (ulong)Math.Log(currentTick - BeginningTick + 1) >= FrameLength;
}
public abstract void StartFrom(TimeSourceProviderBase source);
}
internal sealed class LinearTimeSequence : TimeSequence
{
public LinearTimeSequence(ulong beginningTick, ulong endingTick, ulong tickPerFrame)
private readonly double _frameSeconds;
private readonly double _totalSeconds;
private long _frameLengthTicks;
public LinearTimeSequence(double totalSeconds, double frameSeconds)
{
BeginningTick = beginningTick;
EndingTick = endingTick;
FrameLength = tickPerFrame;
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(totalSeconds);
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(frameSeconds);
if (frameSeconds > totalSeconds) {
throw new ArgumentException("Frame length greater than total length.");
}
_totalSeconds = totalSeconds;
_frameSeconds = frameSeconds;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override bool IsNextFrameReady(ulong currentTick)
public override bool EndsAt(long currentTick) => currentTick >= EndingTick;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override bool IsNextFrameReady(long currentTick)
{
return currentTick - BeginningTick >= FrameLength;
long span = currentTick - PreviousFrameTick;
if (span >= _frameLengthTicks && currentTick < EndingTick)
{
PreviousFrameTick += _frameLengthTicks; // advance start of next frame
return true;
}
return false;
}
public override void StartFrom(TimeSourceProviderBase source)
{
long f = source?.Frequency ?? throw new ArgumentNullException(nameof(source));
_frameLengthTicks = (long)Math.Max(1, _frameSeconds * f);
long totalTicks = (long)Math.Max(_frameLengthTicks, Math.Round(_totalSeconds * f, MidpointRounding.AwayFromZero));
BeginningTick = source.GetCurrentTimeSpanTick();
EndingTick = BeginningTick + totalTicks;
EndingTick = EndingTick < 0 ? long.MaxValue : EndingTick;
PreviousFrameTick = BeginningTick;
}
}
/// <summary>
/// Logarithmic time sequence with growth factor. Boundaries (cumulative times since start)
/// follow initial * multiplier^k, but each slice duration (difference between adjusted
/// boundaries) is at least minSliceSeconds. First slice uses the exact initial length even if
/// below minSliceSeconds, subsequent slices enforce minimum. Example: initial=2,
/// multiplier=2, total>=8, min=3 => boundaries: 2,5,8 (durations:2,3,3).
/// </summary>
internal sealed class LogTimeSequence : TimeSequence
{
private readonly double _initialSeconds;
private double _lastBoundarySecondsAdjusted; // adjusted cumulative seconds of last emitted boundary (0 at start)
private readonly double _minSliceSeconds;
private readonly double _multiplier;
private double _nextBoundarySecondsAdjusted; // adjusted cumulative seconds for next boundary
private readonly double _totalSeconds;
private int _sliceIndex; // index of next boundary (0 => first boundary at initialSeconds)
private long _freq;
private long _nextBoundaryTick; // absolute tick for next boundary
private long _totalTicks;
public LogTimeSequence(
double initialSliceSeconds,
double totalSeconds,
double growthMultiplier,
double minSliceSeconds)
{
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(initialSliceSeconds);
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(totalSeconds);
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(growthMultiplier);
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(minSliceSeconds);
if (growthMultiplier < 1.0) {
throw new ArgumentException("growthMultiplier must be >= 1");
}
if (initialSliceSeconds > totalSeconds) {
throw new ArgumentException("Initial slice length greater than total length.");
}
_initialSeconds = initialSliceSeconds;
_totalSeconds = totalSeconds;
_multiplier = growthMultiplier;
_minSliceSeconds = minSliceSeconds;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override bool EndsAt(long currentTick) => currentTick >= EndingTick ||
_nextBoundarySecondsAdjusted >= _totalSeconds;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override bool IsNextFrameReady(long currentTick)
{
if (currentTick < _nextBoundaryTick || currentTick >= EndingTick) {
return false;
}
// Emit frame: update previous frame tick and prepare next boundary.
PreviousFrameTick = _nextBoundaryTick; // boundary just reached
_lastBoundarySecondsAdjusted = _nextBoundarySecondsAdjusted;
_sliceIndex++;
// Compute original (unadjusted) next cumulative boundary in seconds.
double originalNext = _initialSeconds * Math.Pow(_multiplier, _sliceIndex);
// Enforce total cap.
if (originalNext > _totalSeconds)
{
originalNext = _totalSeconds;
}
// Compute slice duration based on adjusted previous boundary.
double delta = originalNext - _lastBoundarySecondsAdjusted;
if (_sliceIndex > 0 && delta < _minSliceSeconds)
{
// Enforce minimum (except for first slice which uses initial value)
originalNext = _lastBoundarySecondsAdjusted + _minSliceSeconds;
if (originalNext > _totalSeconds) {
originalNext = _totalSeconds;
}
}
_nextBoundarySecondsAdjusted = originalNext;
if (_nextBoundarySecondsAdjusted >= _totalSeconds)
{
_nextBoundaryTick = EndingTick; // sequence will end after this boundary
} else
{
_nextBoundaryTick = BeginningTick + SecondsToTicks(_nextBoundarySecondsAdjusted);
}
return true;
}
public override void StartFrom(TimeSourceProviderBase source)
{
_freq = source?.Frequency ?? throw new ArgumentNullException(nameof(source));
BeginningTick = source.GetCurrentTimeSpanTick();
_totalTicks = SecondsToTicks(_totalSeconds);
EndingTick = BeginningTick + _totalTicks;
_sliceIndex = 0;
_lastBoundarySecondsAdjusted = 0.0;
_nextBoundarySecondsAdjusted = _initialSeconds; // first boundary uses exact initial length
_nextBoundaryTick = BeginningTick + SecondsToTicks(_nextBoundarySecondsAdjusted);
PreviousFrameTick = BeginningTick; // start point
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private long SecondsToTicks(double seconds) => (long)Math.Max(1L, Math.Round(seconds * _freq, MidpointRounding.AwayFromZero));
}
}
@@ -36,6 +36,8 @@ namespace StgSharp.Timing
public abstract class TimeSourceProviderBase
{
public abstract long Frequency { get; }
protected object SubscribeLock { get; } = new();
public void AddSubscriber(TimeSpanProvider subscriber)
@@ -45,6 +47,8 @@ namespace StgSharp.Timing
}
}
public abstract long GetCurrentTimeSpanTick();
public void RemoveSubscriber(TimeSpanProvider subscriber)
{
lock (SubscribeLock) {
@@ -1,147 +0,0 @@
//-----------------------------------------------------------------------
// -----------------------------------------------------------------------
// file="TimeSpanAwaitingToken"
// Project: StgSharp
// AuthorGroup: Nitload
// Copyright (c) Nitload. All rights reserved.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
// -----------------------------------------------------------------------
// -----------------------------------------------------------------------
using StgSharp.HighPerformance.Memory;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace StgSharp.Timing
{
public sealed class TimeSpanAwaitingToken : IDisposable
{
private static readonly ConcurrentBufferStack<int> s_unusedID = new(1);
private static int s_maxID;
private EventHandler? _refreshHandler,_missHandler;
private int _size, _id;
private readonly object _participantObject = new();
private SemaphoreSlim _awaitingSemaphore;
private TimeSpanProvider _provider;
internal TimeSpanAwaitingToken(TimeSpanProvider provider)
{
if (!s_unusedID.TryPop(out int id)) {
id = Interlocked.Increment(ref s_maxID);
}
TokenID = id;
_provider = provider;
_size = 1;
_awaitingSemaphore = new SemaphoreSlim(0, 1);
}
internal TimeSpanAwaitingToken(TimeSpanProvider provider, int count)
{
if (!s_unusedID.TryPop(out int id)) {
id = Interlocked.Increment(ref s_maxID);
}
_provider = provider;
_size = count;
_awaitingSemaphore = new SemaphoreSlim(0, _size);
}
public event EventHandler MissTimeSpanRefreshed
{
add => _missHandler += value;
remove => _missHandler -= value;
}
public event EventHandler TimeSpanRefreshed
{
add => _refreshHandler += value;
remove => _refreshHandler -= value;
}
public int TokenID
{
get => _id;
private set => _id = value;
}
internal SemaphoreSlim AwaitingSemaphoreSlim
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => _awaitingSemaphore;
}
public void Dispose()
{
_provider.QuitSpanAwaiting(this);
s_unusedID.Push(TokenID);
_awaitingSemaphore.Dispose();
GC.SuppressFinalize(this);
}
public override int GetHashCode()
{
return _id;
}
public void Participant()
{
lock (_participantObject)
{
_size++;
SemaphoreSlim newSemaphore = new(_awaitingSemaphore.CurrentCount, _size);
Interlocked.Exchange(ref _awaitingSemaphore, newSemaphore).Dispose();
}
}
public void WaitNextSpan()
{
_awaitingSemaphore.Wait();
}
internal void MissRefresh()
{
_missHandler?.Invoke(this, EventArgs.Empty);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal void Refresh()
{
_refreshHandler?.Invoke(this, EventArgs.Empty);
}
~TimeSpanAwaitingToken()
{
Dispose();
}
}
}
@@ -0,0 +1,198 @@
//-----------------------------------------------------------------------
// -----------------------------------------------------------------------
// file="TimeSpanProvider.Awaiting"
// Project: StgSharp
// AuthorGroup: Nitload
// Copyright (c) Nitload. All rights reserved.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
// -----------------------------------------------------------------------
// -----------------------------------------------------------------------
using StgSharp.HighPerformance.Memory;
using System;
using System.Collections.Concurrent;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
namespace StgSharp.Timing
{
public partial class TimeSpanProvider
{
private static readonly ConcurrentBufferStack<int> s_unusedID = new ConcurrentBufferStack<int>(8);
private static int s_maxID;
private EventHandler? _refreshHandler, _missHandler;
private int _pendingSpans; // Number of unconsumed span ticks since last Wait.
private int _size, _id;
private readonly ManualResetEventSlim _event = new();
private readonly object _participantObject = new();
public event EventHandler MissTimeSpanRefreshed
{
add => _missHandler += value;
remove => _missHandler -= value;
}
public event EventHandler TimeSpanRefreshed
{
add => _refreshHandler += value;
remove => _refreshHandler -= value;
}
/// <summary>
/// True if there is at least one unconsumed span tick (PendingSpanCount &gt; 0).
/// </summary>
public bool HasPendingSpan => PendingSpanCount > 0;
public int TokenID
{
get => _id;
private set => _id = value;
}
/// <summary>
/// Number of span ticks not yet consumed by Wait ( &gt; 0 means last tick not waited).
/// </summary>
public int PendingSpanCount => Volatile.Read(ref _pendingSpans);
internal ManualResetEventSlim AwaitingEvent
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => _event;
}
public void Dispose()
{
s_unusedID.Push(TokenID);
_event.Dispose();
GC.SuppressFinalize(this);
}
public override int GetHashCode() => _id;
public void Participant()
{
lock (_participantObject)
{
// Increase the maximum concurrent consumers for the next tick.
_size++;
}
}
public void WaitNextSpan()
{
_event.Wait();
// Consume one span slot (clamp at 0 so multiple subscribers won't make it negative).
while (true)
{
int cur = Volatile.Read(ref _pendingSpans);
if (cur == 0) {
return;
}
if (Interlocked.CompareExchange(ref _pendingSpans, cur - 1, cur) == cur)
{
if (cur - 1 == 0)
{
// Last consumer resets the gate for the next tick.
_event.Reset();
}
return;
}
}
}
public async Task WaitNextSpanAsync()
{
await Task.Run(() => _event.Wait()).ConfigureAwait(false);
while (true)
{
int cur = Volatile.Read(ref _pendingSpans);
if (cur == 0)
{
break;
}
if (Interlocked.CompareExchange(ref _pendingSpans, cur - 1, cur) == cur)
{
if (cur - 1 == 0) {
_event.Reset();
}
break;
}
}
}
internal void MissRefresh() => _missHandler?.Invoke(this, EventArgs.Empty); // Legacy compatibility.
/// <summary>
///
/// </summary>
/// <param name="currentTick">
/// Tick passed since binded time source.
/// </param>
/// <returns>
/// True if the span provider is working. False if time is over (the sequence ended).
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal bool OnSpanTick(long currentTick)
{
if (_sequence.EndsAt(currentTick))
{
Dispose();
return false;
}
if (!_sequence.IsNextFrameReady(currentTick)) {
return true;
}
Console.WriteLine("Frame hit!");
// Fast path: if no pending consumers, initialize the counter to current capacity (_size).
if (Volatile.Read(ref _pendingSpans) == 0)
{
int toIssue = Math.Max(1, Volatile.Read(ref _size));
if (Interlocked.CompareExchange(ref _pendingSpans, toIssue, 0) == 0)
{
// Open the gate for all current waiters (up to `_size`).
_event.Set();
_refreshHandler?.Invoke(this, EventArgs.Empty);
}
} else
{
// A tick is already pending (or was just set by another thread): mark as miss.
_missHandler?.Invoke(this, EventArgs.Empty);
}
return true;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal void Refresh() => _refreshHandler?.Invoke(this, EventArgs.Empty); // Legacy compatibility.
~TimeSpanProvider() => Dispose();
}
}
+21 -175
View File
@@ -25,207 +25,53 @@
//
// -----------------------------------------------------------------------
// -----------------------------------------------------------------------
using StgSharp.Common.Timing;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading;
namespace StgSharp.Timing
{
/// <summary>
/// A microsecond-accuracy timer. It will refresh every a certain time span.
/// TimeSpanProvider: uses a bound TimeSequence and TimeSourceProviderBase to publish span
/// ticks. Each OnSpanTick advances the sequence; when sequence ends provider unsubscribes.
/// </summary>
public class TimeSpanProvider
public partial class TimeSpanProvider
{
private bool _subscribed;
private readonly ConcurrentQueue<TimeSpanAwaitingToken> _tokenToAdd = new();
private readonly ConcurrentQueue<TimeSpanAwaitingToken> _tokenToQuit;
private Counter<int> timeSpanCount = new(-1, 1, 1);
private readonly HashSet<TimeSpanAwaitingToken> _awaitingTokens;
private long _maxSpanCount;
private long _spanBegin;
private readonly long _spanLength;
private int _emittedSpanCount; // legacy counter
private readonly TimeSequence _sequence;
private readonly TimeSourceProviderBase _timeSource;
/// <summary>
/// Create a new <see cref="TimeSpanProvider" /> and define the length of time span.
/// Length of a single time span is defined in microseconds.
/// </summary>
/// <param _label="spanMicroSeconds">
///
/// </param>
/// <param _label="provider">
///
/// </param>
public TimeSpanProvider(long spanMicroSeconds, TimeSourceProviderBase provider)
public TimeSpanProvider(TimeSequence sequence, TimeSourceProviderBase provider, int maxWaitCount = 1)
{
ArgumentNullException.ThrowIfNull(provider);
ArgumentNullException.ThrowIfNull(sequence);
_timeSource = provider;
_spanLength = spanMicroSeconds;
_maxSpanCount = int.MaxValue;
_sequence = sequence;
_sequence.StartFrom(provider);
provider.AddSubscriber(this);
_subscribed = true;
_awaitingTokens = [];
_tokenToQuit = new ConcurrentQueue<TimeSpanAwaitingToken>();
}
/// <summary>
/// Create a new <see cref="TimeSpanProvider" /> and define the length of time span.
/// Length of a single time span is defined in seconds.
/// </summary>
/// <param _label="spanSeconds">
///
/// </param>
/// <param _label="provider">
///
/// </param>
public TimeSpanProvider(double spanSeconds, TimeSourceProviderBase provider)
{
ArgumentNullException.ThrowIfNull(provider);
_timeSource = provider;
_spanLength = (long)(spanSeconds * 1000L * 1000L);
_spanBegin = -_spanLength;
_maxSpanCount = long.MaxValue;
provider.AddSubscriber(this);
_subscribed = true;
_awaitingTokens = [];
_tokenToQuit = new ConcurrentQueue<TimeSpanAwaitingToken>();
}
/// <summary>
/// Create a new <see cref="TimeSpanProvider" /> and define the length of time span.
/// Length of a single time span is defined in microseconds.
/// </summary>
/// <param _label="spanLength">
///
/// </param>
/// <param _label="maxSpan">
///
/// </param>
/// <param _label="provider">
///
/// </param>
public TimeSpanProvider(long spanLength, long maxSpan, TimeSourceProviderBase provider)
{
ArgumentNullException.ThrowIfNull(provider);
if (maxSpan <= 0) {
throw new ArgumentException("Sapn count cannot be smaller than zero");
if (!s_unusedID.TryPop(out int id)) {
id = Interlocked.Increment(ref s_maxID);
}
_timeSource = provider;
_spanLength = spanLength;
_spanBegin = -_spanLength;
provider.AddSubscriber(this);
_subscribed = true;
_awaitingTokens = new HashSet<TimeSpanAwaitingToken>();
_tokenToQuit = new ConcurrentQueue<TimeSpanAwaitingToken>();
TokenID = id;
_size = maxWaitCount > 0 ? maxWaitCount : 1;
_event = new ManualResetEventSlim(initialState:false, spinCount:0);
_pendingSpans = 0;
}
public double SpanLength
public double CurrentSecond
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => _spanLength * 1.0 / (1000L * 1000L);
get => (double)_sequence.PreviousFrameTick / _timeSource.Frequency;
}
public float CurrentSecond
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => timeSpanCount.Value * SpanLengthLowPrecession;
}
public int CurrentSpan => _emittedSpanCount;
public float SpanLengthLowPrecession
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => _spanLength * 1.0f / (1000 * 1000);
}
public static TimeSourceProviderBase DefaultProvider => World.MainTimeProvider;
public int CurrentSpan
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => timeSpanCount.Value;
}
/// <summary>
/// Time source provider provided in <see cref="World" /> internal framework.
/// </summary>
public static TimeSourceProviderBase DefaultProvider => StgSharpTime.OnlyInstance;
public TimeSpanAwaitingToken ParticipantSpanAwaiting(int count)
{
TimeSpanAwaitingToken token = new(this, count);
_tokenToAdd.Enqueue(token);
return token;
}
public void QuitSpanAwaiting(TimeSpanAwaitingToken token)
{
_tokenToQuit.Enqueue(token);
}
public void StopSpanProviding()
{
_ = Interlocked.Exchange(ref _maxSpanCount, 0);
}
public void SubscribeToTimeSource(TimeSourceProviderBase serviceHandler)
{
ArgumentNullException.ThrowIfNull(serviceHandler);
if (_subscribed) {
return;
}
_subscribed = true;
serviceHandler.AddSubscriber(this);
}
public int ToInt32()
{
return timeSpanCount.Value;
}
internal bool CheckTime(long microseconds)
{
if (microseconds - _spanBegin < _spanLength) {
return true;
}
foreach (TimeSpanAwaitingToken tokenAdd in _tokenToAdd) {
_ = _awaitingTokens.Add(tokenAdd);
}
_tokenToAdd.Clear();
foreach (TimeSpanAwaitingToken tokenQuit in _tokenToQuit)
{
_ = _awaitingTokens.Remove(tokenQuit);
DefaultLog.InternalWriteLog(
$"Awaiting token {tokenQuit!.TokenID} has been removed.", LogType.Info);
}
_tokenToQuit.Clear();
_spanBegin = microseconds;
timeSpanCount++;
foreach (TimeSpanAwaitingToken item in _awaitingTokens)
{
if (item.AwaitingSemaphoreSlim.CurrentCount == 0)
{
_ = item.AwaitingSemaphoreSlim.Release();
item.Refresh();
} else
{
item.MissRefresh();
}
}
return timeSpanCount < _maxSpanCount;
}
public static explicit operator int([NotNull]TimeSpanProvider provider)
{
return provider.ToInt32();
}
public static explicit operator int(TimeSpanProvider provider) => provider?._emittedSpanCount ?? 0;
}
}