Files
FileTime2/src/AppCommon/FileTime.App.Core/Services/RefreshSmoothnessCalculator.cs

108 lines
4.0 KiB
C#

using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace FileTime.App.Core.Services;
public sealed class RefreshSmoothnessCalculator : IRefreshSmoothnessCalculator, INotifyPropertyChanged
{
private const int MaxSampleTimeInSeconds = 10;
private const int SampleWindowInMilliseconds = 1000;
private const int MaxDelayBetweenRefreshes = 600;
private const int MinDelayBetweenRefreshes = 10;
private readonly TimeSpan _maxDelay = TimeSpan.FromSeconds(MaxSampleTimeInSeconds);
private readonly TimeSpan _defaultRefreshDelay = TimeSpan.FromMilliseconds(200);
private readonly Queue<DateTime> _changeTimes = new();
private readonly object _lock = new();
private TimeSpan _refreshDelay;
public TimeSpan RefreshDelay
{
get => _refreshDelay;
private set
{
if (value.Equals(_refreshDelay)) return;
_refreshDelay = value;
OnPropertyChanged();
}
}
public RefreshSmoothnessCalculator()
{
_refreshDelay = _defaultRefreshDelay;
}
public void RegisterChange() => RegisterChange(DateTime.Now);
public void RegisterChange(DateTime changeTime)
{
lock (_lock)
{
if (_changeTimes.Count > 0 && Math.Abs((_changeTimes.Last() - changeTime).TotalMilliseconds) < 5) return;
CleanList(DateTime.Now);
_changeTimes.Enqueue(changeTime);
}
}
private void CleanList(DateTime now)
{
while (_changeTimes.Count > 0)
{
var item = _changeTimes.Peek();
if (now - item < _maxDelay) return;
_changeTimes.Dequeue();
}
}
public void RecalculateSmoothness()
{
lock (_lock)
{
if (_changeTimes.Count < 2)
{
RefreshDelay = _defaultRefreshDelay;
return;
}
var now = DateTime.Now;
CleanList(now);
var queue = new Queue<DateTime>(_changeTimes);
var segments = (int)Math.Ceiling((double)MaxSampleTimeInSeconds * 1000 / SampleWindowInMilliseconds);
Span<int> segmentElementCounts = stackalloc int[segments];
while (queue.Count > 0)
{
var item = queue.Dequeue();
var segment = segments - 1 - (int)((now - item).TotalMilliseconds / SampleWindowInMilliseconds);
segmentElementCounts[segment]++;
}
var weightSum = 0d;
var score = 0d;
//Note: This might not be the best algorithm to calculate the delay, but works okay.
//Note: I had an algorithm in mind that uses the delta between neighbour times and and delta between a time and now, but I couldn't implement it.
//Note: If you are good at math and have a better algorithm, please feel free to implement it/create an issue with it.
for (var i = 0; i < segments; i++)
{
var weight = Math.Pow(i, 2);
weightSum += weight;
var pressCount = segmentElementCounts[i];
//Note: we want the minimum delay even if the user pressed only once in a segment
if (pressCount > 0) pressCount--;
score += pressCount * weight;
}
const double fraction = (double)(MaxDelayBetweenRefreshes - MinDelayBetweenRefreshes) / 4;
var weightedAvg = weightSum == 0 ? _defaultRefreshDelay.TotalMilliseconds : Math.Round(score / weightSum);
var finalDelay = (weightedAvg * fraction) + MinDelayBetweenRefreshes;
finalDelay = finalDelay > MaxDelayBetweenRefreshes ? MaxDelayBetweenRefreshes : finalDelay;
RefreshDelay = TimeSpan.FromMilliseconds(finalDelay);
}
}
public event PropertyChangedEventHandler? PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string? propertyName = null)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}