using System;
using System.Collections.Generic;
using System.Runtime.Versioning;
using System.Threading;
namespace Ryujinx.Common.PreciseSleep
{
///
/// A pool of threads used to allow "interruptable" nanosleep for a single target event.
///
[SupportedOSPlatform("macos")]
[SupportedOSPlatform("linux")]
[SupportedOSPlatform("android")]
[SupportedOSPlatform("ios")]
internal class NanosleepPool : IDisposable
{
public const int MaxThreads = 8;
///
/// A thread that nanosleeps and may signal an event on wake.
/// When a thread is assigned a nanosleep to perform, it also gets a signal ID.
/// The pool's target event is only signalled if this ID matches the latest dispatched one.
///
private class NanosleepThread : IDisposable
{
private static readonly long _timePointEpsilon;
static NanosleepThread()
{
_timePointEpsilon = PerformanceCounter.TicksPerMillisecond / 100; // 0.01ms
}
private readonly Thread _thread;
private readonly NanosleepPool _parent;
private readonly AutoResetEvent _newWaitEvent;
private bool _running = true;
private long _signalId;
private long _nanoseconds;
private long _timePoint;
public long SignalId => _signalId;
///
/// Creates a new NanosleepThread for a parent pool, with a specified thread ID.
///
/// Parent NanosleepPool
/// Thread ID
public NanosleepThread(NanosleepPool parent, int id)
{
_parent = parent;
_newWaitEvent = new(false);
_thread = new Thread(Loop)
{
Name = $"Common.Nanosleep.{id}",
Priority = ThreadPriority.AboveNormal,
IsBackground = true
};
_thread.Start();
}
///
/// Service requests to perform a nanosleep, signal parent pool when complete.
///
private void Loop()
{
_newWaitEvent.WaitOne();
while (_running)
{
Nanosleep.Sleep(_nanoseconds);
_parent.Signal(this);
_newWaitEvent.WaitOne();
}
_newWaitEvent.Dispose();
}
///
/// Assign a nanosleep for this thread to perform, then signal at the end.
///
/// Nanoseconds to sleep
/// Signal ID
/// Target timepoint
public void SleepAndSignal(long nanoseconds, long signalId, long timePoint)
{
_signalId = signalId;
_nanoseconds = nanoseconds;
_timePoint = timePoint;
_newWaitEvent.Set();
}
///
/// Resurrect an active nanosleep's signal if its target timepoint is a close enough match.
///
/// New signal id to assign the nanosleep
/// Target timepoint
/// True if resurrected, false otherwise
public bool Resurrect(long signalId, long timePoint)
{
if (Math.Abs(timePoint - _timePoint) < _timePointEpsilon)
{
_signalId = signalId;
return true;
}
return false;
}
///
/// Dispose the NanosleepThread, interrupting its worker loop.
///
public void Dispose()
{
if (_running)
{
_running = false;
_newWaitEvent.Set();
}
}
}
private readonly object _lock = new();
private readonly List _threads = new();
private readonly List _active = new();
private readonly Stack _free = new();
private readonly AutoResetEvent _signalTarget;
private long _signalId;
///
/// Creates a new NanosleepPool with a target event to signal when a nanosleep completes.
///
/// Event to signal when nanosleeps complete
public NanosleepPool(AutoResetEvent signalTarget)
{
_signalTarget = signalTarget;
}
///
/// Signal the target event (if the source sleep has not been superseded)
/// and free the nanosleep thread.
///
/// Nanosleep thread that completed
private void Signal(NanosleepThread thread)
{
lock (_lock)
{
_active.Remove(thread);
_free.Push(thread);
if (thread.SignalId == _signalId)
{
_signalTarget.Set();
}
}
}
///
/// Sleep for the given number of nanoseconds and signal the target event.
/// This does not block the caller thread.
///
/// Nanoseconds to sleep
/// Target timepoint
/// True if the signal will be set, false otherwise
public bool SleepAndSignal(long nanoseconds, long timePoint)
{
lock (_lock)
{
_signalId++;
// Check active sleeps, if any line up with the requested timepoint then resurrect that nanosleep.
foreach (NanosleepThread existing in _active)
{
if (existing.Resurrect(_signalId, timePoint))
{
return true;
}
}
if (!_free.TryPop(out NanosleepThread thread))
{
if (_threads.Count >= MaxThreads)
{
return false;
}
thread = new NanosleepThread(this, _threads.Count);
_threads.Add(thread);
}
_active.Add(thread);
thread.SleepAndSignal(nanoseconds, _signalId, timePoint);
return true;
}
}
///
/// Ignore the latest nanosleep.
///
public void IgnoreSignal()
{
_signalId++;
}
///
/// Dispose the NanosleepPool, disposing all of its active threads.
///
public void Dispose()
{
GC.SuppressFinalize(this);
foreach (NanosleepThread thread in _threads)
{
thread.Dispose();
}
_threads.Clear();
}
}
}