//----------------------------------------------------------------------- // ----------------------------------------------------------------------- // 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 s_unusedID = new ConcurrentBufferStack(8); private static int s_maxID; private EventHandler? _refreshHandler, _missHandler; private int _size, _id; private long _pendingSpans; // Number of unconsumed span ticks since last Wait. 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; } /// /// True if there is at least one unconsumed span tick (PendingSpanCount > 0). /// public bool HasPendingSpan => PendingSpanCount > 0; public int TokenID { get => _id; private set => _id = value; } /// /// Number of span ticks not yet consumed by Wait ( > 0 means last tick not waited). /// public long PendingSpanCount => Volatile.Read(ref _pendingSpans); internal ManualResetEventSlim AwaitingEvent { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _event; } public void Dispose() { // mark provider as ended so external users can observe terminal state Volatile.Write(ref _endedFlag, 1); s_unusedID.Push(TokenID); _event.Set(); 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) { long 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) { long 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; } } } /// /// Wait for the next timespan tick, but honor the provided cancellation token. Throws /// OperationCanceledException when token is cancelled. If the provider is disposed during /// wait an OperationCanceledException is thrown to indicate end. /// public Task WaitNextSpanAsync( CancellationToken ct ) { // Block the current thread until the event is signaled or cancellation requested. _event.Wait(ct); // consume one pending span (same logic as non-cancellable variant) while (true) { long 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; } } return Task.CompletedTask; } internal void MissRefresh() => _missHandler?.Invoke(this, EventArgs.Empty); // Legacy compatibility. /// /// /// /// /// Tick passed since binded time source. /// /// /// True if the span provider is working. False if time is over (the sequence ended). /// [MethodImpl(MethodImplOptions.AggressiveInlining)] internal bool OnSpanTick( long currentTick ) { if (_sequence.EndsAt(currentTick)) { // mark ended due to sequence timeout, then dispose Volatile.Write(ref _endedFlag, 1); // Console.WriteLine("Time span ended."); 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; } ~TimeSpanProvider() => Dispose(); } }