234 lines
5.6 KiB
C#
234 lines
5.6 KiB
C#
namespace Explorer.Application;
|
|
|
|
public readonly record struct ThumbnailKey(string Path, long ModifiedTicks, int PixelWidth)
|
|
{
|
|
public static ThumbnailKey From(string path, DateTimeOffset? modifiedUtc, int pixelWidth)
|
|
=> new(path, modifiedUtc?.UtcTicks ?? 0, pixelWidth);
|
|
}
|
|
|
|
public readonly record struct ThumbnailJob(
|
|
ThumbnailKey Key,
|
|
int Rank,
|
|
int Generation);
|
|
|
|
public readonly record struct ThumbnailSchedulerStats(
|
|
int Generation,
|
|
int Window,
|
|
int Pending,
|
|
int InFlight,
|
|
int Requested,
|
|
int CacheHits,
|
|
int Generated,
|
|
int Cancelled,
|
|
int Failed);
|
|
|
|
public sealed class ThumbnailScheduler
|
|
{
|
|
public const int DefaultConcurrency = 2;
|
|
public const int DefaultPixelWidth = 128;
|
|
public const int MaxPending = 96;
|
|
public const int PrefetchItems = 24;
|
|
|
|
private readonly object _gate = new();
|
|
private readonly List<ThumbnailJob> _window = [];
|
|
private readonly HashSet<ThumbnailKey> _windowKeys = [];
|
|
private readonly HashSet<ThumbnailKey> _inFlight = [];
|
|
private readonly HashSet<ThumbnailKey> _cached = [];
|
|
private int _generation;
|
|
private bool _enabled;
|
|
private int _requested;
|
|
private int _cacheHits;
|
|
private int _generated;
|
|
private int _cancelled;
|
|
private int _failed;
|
|
|
|
public int Generation
|
|
{
|
|
get { lock (_gate) return _generation; }
|
|
}
|
|
|
|
public int InFlight
|
|
{
|
|
get { lock (_gate) return _inFlight.Count; }
|
|
}
|
|
|
|
public void Reset(int generation)
|
|
{
|
|
lock (_gate)
|
|
{
|
|
_cancelled += _window.Count + _inFlight.Count;
|
|
_generation = generation;
|
|
_window.Clear();
|
|
_windowKeys.Clear();
|
|
_inFlight.Clear();
|
|
}
|
|
}
|
|
|
|
public void SetEnabled(bool enabled)
|
|
{
|
|
lock (_gate)
|
|
{
|
|
_enabled = enabled;
|
|
if (enabled)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_cancelled += _window.Count + _inFlight.Count;
|
|
_window.Clear();
|
|
_windowKeys.Clear();
|
|
_inFlight.Clear();
|
|
}
|
|
}
|
|
|
|
public bool IsCached(ThumbnailKey key)
|
|
{
|
|
lock (_gate)
|
|
{
|
|
return _cached.Contains(key);
|
|
}
|
|
}
|
|
|
|
public void MarkCached(ThumbnailKey key)
|
|
{
|
|
lock (_gate)
|
|
{
|
|
_cached.Add(key);
|
|
}
|
|
}
|
|
|
|
public void RecordCacheHit()
|
|
{
|
|
lock (_gate)
|
|
{
|
|
_cacheHits++;
|
|
_requested++;
|
|
}
|
|
}
|
|
|
|
public IReadOnlyList<ThumbnailJob> SetWindow(int generation, IReadOnlyList<ThumbnailJob> jobs)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(jobs);
|
|
lock (_gate)
|
|
{
|
|
if (generation != _generation || !_enabled)
|
|
{
|
|
_cancelled += jobs.Count;
|
|
return [];
|
|
}
|
|
|
|
var next = jobs
|
|
.Where(j => j.Generation == _generation)
|
|
.GroupBy(j => j.Key)
|
|
.Select(g => g.OrderBy(j => j.Rank).First())
|
|
.OrderBy(j => j.Rank)
|
|
.Take(MaxPending)
|
|
.ToList();
|
|
|
|
var nextKeys = next.Select(j => j.Key).ToHashSet();
|
|
foreach (var previous in _window)
|
|
{
|
|
if (!nextKeys.Contains(previous.Key))
|
|
{
|
|
_cancelled++;
|
|
}
|
|
}
|
|
|
|
foreach (var key in _inFlight.ToArray())
|
|
{
|
|
if (!nextKeys.Contains(key))
|
|
{
|
|
_inFlight.Remove(key);
|
|
_cancelled++;
|
|
}
|
|
}
|
|
|
|
_window.Clear();
|
|
_window.AddRange(next);
|
|
_windowKeys.Clear();
|
|
foreach (var job in next)
|
|
{
|
|
_windowKeys.Add(job.Key);
|
|
}
|
|
|
|
_requested += next.Count(j => !_cached.Contains(j.Key) && !_inFlight.Contains(j.Key));
|
|
return next;
|
|
}
|
|
}
|
|
|
|
public bool TryTake(out ThumbnailJob job)
|
|
{
|
|
lock (_gate)
|
|
{
|
|
if (!_enabled)
|
|
{
|
|
job = default;
|
|
return false;
|
|
}
|
|
|
|
foreach (var candidate in _window)
|
|
{
|
|
if (_cached.Contains(candidate.Key) || _inFlight.Contains(candidate.Key))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
_inFlight.Add(candidate.Key);
|
|
job = candidate;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
job = default;
|
|
return false;
|
|
}
|
|
|
|
public bool IsCurrent(ThumbnailJob job)
|
|
{
|
|
lock (_gate)
|
|
{
|
|
return _enabled
|
|
&& job.Generation == _generation
|
|
&& _windowKeys.Contains(job.Key);
|
|
}
|
|
}
|
|
|
|
public void Complete(ThumbnailJob job, bool generated, bool failed)
|
|
{
|
|
lock (_gate)
|
|
{
|
|
_inFlight.Remove(job.Key);
|
|
if (failed)
|
|
{
|
|
_failed++;
|
|
_cached.Add(job.Key);
|
|
return;
|
|
}
|
|
|
|
if (generated)
|
|
{
|
|
_generated++;
|
|
_cached.Add(job.Key);
|
|
}
|
|
}
|
|
}
|
|
|
|
public ThumbnailSchedulerStats Snapshot()
|
|
{
|
|
lock (_gate)
|
|
{
|
|
var pending = _window.Count(j => !_cached.Contains(j.Key) && !_inFlight.Contains(j.Key));
|
|
return new ThumbnailSchedulerStats(
|
|
_generation,
|
|
_window.Count,
|
|
pending,
|
|
_inFlight.Count,
|
|
_requested,
|
|
_cacheHits,
|
|
_generated,
|
|
_cancelled,
|
|
_failed);
|
|
}
|
|
}
|
|
}
|