diff --git a/RGB.NET.Core/Helper/TimerHelper.cs b/RGB.NET.Core/Helper/TimerHelper.cs new file mode 100644 index 0000000..fac9476 --- /dev/null +++ b/RGB.NET.Core/Helper/TimerHelper.cs @@ -0,0 +1,179 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Threading; + +namespace RGB.NET.Core; + +public static class TimerHelper +{ + #region DLL-Imports + + [DllImport("winmm.dll", EntryPoint = "timeBeginPeriod")] + private static extern void TimeBeginPeriod(int t); + + [DllImport("winmm.dll", EntryPoint = "timeEndPeriod")] + private static extern void TimeEndPeriod(int t); + + #endregion + + #region Properties & Fields + + private static readonly object HIGH_RESOLUTION_TIMER_LOCK = new(); + + private static bool _areHighResolutionTimersEnabled = false; + private static int _highResolutionTimerUsers = 0; + + private static bool _useHighResolutionTimers = true; + /// + /// Gets or sets if High Resolution Timers should be used. + /// + public static bool UseHighResolutionTimers + { + get => _useHighResolutionTimers; + set + { + lock (HIGH_RESOLUTION_TIMER_LOCK) + { + _useHighResolutionTimers = value; + CheckHighResolutionTimerUsage(); + } + } + } + + // ReSharper disable once InconsistentNaming + private static readonly HashSet _timerLeases = new(); + + #endregion + + #region Methods + + /// + /// Executes the provided action and blocks if needed until the the has passed. + /// + /// The action to execute. + /// The time in ms this method should block. default: 0 + /// The time in ms spent executing the . + public static double Execute(Action action, double targetExecuteTime = 0) + { + long preUpdateTicks = Stopwatch.GetTimestamp(); + + action(); + + double updateTime = GetElapsedTime(preUpdateTicks); + + if (targetExecuteTime > 0) + { + int sleep = (int)(targetExecuteTime - updateTime); + if (sleep > 0) + Thread.Sleep(sleep); + } + + return updateTime; + } + + /// + /// Calculates the ellapsed time in ms from the provided timestamp until now. + /// + /// The initial timestamp to calculate the time from. + /// The elapsed time in ms. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static double GetElapsedTime(long initialTimestamp) => ((Stopwatch.GetTimestamp() - initialTimestamp) / (double)TimeSpan.TicksPerMillisecond); + + /// + /// Requests to use to use High Resolution Timers if enabled. + /// IMPORTANT: Always dispose the returned disposable if High Resolution Timers are no longer needed for the caller. + /// + /// A disposable to remove the request. + public static IDisposable RequestHighResolutionTimer() + { + lock (HIGH_RESOLUTION_TIMER_LOCK) + { + _highResolutionTimerUsers++; + CheckHighResolutionTimerUsage(); + } + + HighResolutionTimerDisposable timerLease = new(); + _timerLeases.Add(timerLease); + return timerLease; + } + + private static void CheckHighResolutionTimerUsage() + { + if (UseHighResolutionTimers && (_highResolutionTimerUsers > 0)) + EnableHighResolutionTimers(); + else + DisableHighResolutionTimers(); + } + + private static void EnableHighResolutionTimers() + { + lock (HIGH_RESOLUTION_TIMER_LOCK) + { + if (_areHighResolutionTimersEnabled) return; + + // DarthAffe 06.05.2022: Linux should use 1ms timers by default + if (OperatingSystem.IsWindows()) + TimeBeginPeriod(1); + + _areHighResolutionTimersEnabled = true; + } + } + + private static void DisableHighResolutionTimers() + { + lock (HIGH_RESOLUTION_TIMER_LOCK) + { + if (!_areHighResolutionTimersEnabled) return; + + if (OperatingSystem.IsWindows()) + TimeEndPeriod(1); + + _areHighResolutionTimersEnabled = false; + } + } + + /// + /// Disposes all open High Resolution Timer Requests. + /// This should be called once when exiting the application to make sure nothing remains open and the application correctly unregisters itself on OS level. + /// Shouldn't be needed if everything is disposed, but better safe then sorry. + /// + public static void DisposeAllHighResolutionTimerRequests() + { + List timerLeases = _timerLeases.ToList(); + foreach (HighResolutionTimerDisposable timer in timerLeases) + timer.Dispose(); + } + + #endregion + + private class HighResolutionTimerDisposable : IDisposable + { + #region Properties & Fields + + private bool _isDisposed = false; + + #endregion + + #region Methods + + public void Dispose() + { + if (_isDisposed) return; + + _isDisposed = true; + + lock (HIGH_RESOLUTION_TIMER_LOCK) + { + _timerLeases.Remove(this); + _highResolutionTimerUsers--; + CheckHighResolutionTimerUsage(); + } + } + + #endregion + } +} \ No newline at end of file diff --git a/RGB.NET.Core/Update/Devices/DeviceUpdateTrigger.cs b/RGB.NET.Core/Update/Devices/DeviceUpdateTrigger.cs index d2ce849..6868a87 100644 --- a/RGB.NET.Core/Update/Devices/DeviceUpdateTrigger.cs +++ b/RGB.NET.Core/Update/Devices/DeviceUpdateTrigger.cs @@ -1,6 +1,5 @@ // ReSharper disable MemberCanBePrivate.Global -using System.Diagnostics; using System.Threading; using System.Threading.Tasks; @@ -142,25 +141,10 @@ public class DeviceUpdateTrigger : AbstractUpdateTrigger, IDeviceUpdateTrigger { OnStartup(); - while (!UpdateToken.IsCancellationRequested) - { - if (HasDataEvent.WaitOne(Timeout)) - { - long preUpdateTicks = Stopwatch.GetTimestamp(); - - OnUpdate(); - - double lastUpdateTime = ((Stopwatch.GetTimestamp() - preUpdateTicks) / 10000.0); - LastUpdateTime = lastUpdateTime; - - if (UpdateFrequency > 0) - { - int sleep = (int)((UpdateFrequency * 1000.0) - lastUpdateTime); - if (sleep > 0) - Thread.Sleep(sleep); - } - } - } + using (TimerHelper.RequestHighResolutionTimer()) + while (!UpdateToken.IsCancellationRequested) + if (HasDataEvent.WaitOne(Timeout)) + LastUpdateTime = TimerHelper.Execute(() => OnUpdate(), UpdateFrequency * 1000); } /// diff --git a/RGB.NET.Core/Update/ManualUpdateTrigger.cs b/RGB.NET.Core/Update/ManualUpdateTrigger.cs index f9b315c..67e8ca4 100644 --- a/RGB.NET.Core/Update/ManualUpdateTrigger.cs +++ b/RGB.NET.Core/Update/ManualUpdateTrigger.cs @@ -1,6 +1,5 @@ // ReSharper disable MemberCanBePrivate.Global -using System.Diagnostics; using System.Threading; using System.Threading.Tasks; @@ -86,11 +85,7 @@ public sealed class ManualUpdateTrigger : AbstractUpdateTrigger while (!UpdateToken.IsCancellationRequested) { if (_mutex.WaitOne(100)) - { - long preUpdateTicks = Stopwatch.GetTimestamp(); - OnUpdate(_customUpdateData); - LastUpdateTime = ((Stopwatch.GetTimestamp() - preUpdateTicks) / 10000.0); - } + LastUpdateTime = TimerHelper.Execute(() => OnUpdate(_customUpdateData)); } } diff --git a/RGB.NET.Core/Update/TimerUpdateTrigger.cs b/RGB.NET.Core/Update/TimerUpdateTrigger.cs index d15f049..14a7e9f 100644 --- a/RGB.NET.Core/Update/TimerUpdateTrigger.cs +++ b/RGB.NET.Core/Update/TimerUpdateTrigger.cs @@ -1,7 +1,6 @@ // ReSharper disable MemberCanBePrivate.Global using System; -using System.Diagnostics; using System.Threading; using System.Threading.Tasks; @@ -130,21 +129,10 @@ public class TimerUpdateTrigger : AbstractUpdateTrigger { OnStartup(); - while (!UpdateToken.IsCancellationRequested) - { - long preUpdateTicks = Stopwatch.GetTimestamp(); + using (TimerHelper.RequestHighResolutionTimer()) + while (!UpdateToken.IsCancellationRequested) + LastUpdateTime = TimerHelper.Execute(() => OnUpdate(_customUpdateData), UpdateFrequency * 1000); - OnUpdate(_customUpdateData); - - if (UpdateFrequency > 0) - { - double lastUpdateTime = ((Stopwatch.GetTimestamp() - preUpdateTicks) / 10000.0); - LastUpdateTime = lastUpdateTime; - int sleep = (int)((UpdateFrequency * 1000.0) - lastUpdateTime); - if (sleep > 0) - Thread.Sleep(sleep); - } - } } ///