diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt index 2d6fdb7f0aa..c4b1ee8494d 100644 --- a/.github/actions/spelling/expect.txt +++ b/.github/actions/spelling/expect.txt @@ -106,3 +106,4 @@ alreadyexists JsonRPC JsonRPCV2 Softpedia +img diff --git a/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj b/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj index b24f069c1ba..574f3686a6d 100644 --- a/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj +++ b/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj @@ -49,6 +49,7 @@ + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Flow.Launcher.Infrastructure/Image/ImageCache.cs b/Flow.Launcher.Infrastructure/Image/ImageCache.cs index 7a2b5763756..55545b9a732 100644 --- a/Flow.Launcher.Infrastructure/Image/ImageCache.cs +++ b/Flow.Launcher.Infrastructure/Image/ImageCache.cs @@ -4,13 +4,13 @@ using System.Linq; using System.Threading; using System.Windows.Media; +using FastCache; +using FastCache.Services; namespace Flow.Launcher.Infrastructure.Image { - [Serializable] public class ImageUsage { - public int usage; public ImageSource imageSource; @@ -23,16 +23,13 @@ public ImageUsage(int usage, ImageSource image) public class ImageCache { - private const int MaxCached = 50; - public ConcurrentDictionary<(string, bool), ImageUsage> Data { get; } = new(); - private const int permissibleFactor = 2; - private SemaphoreSlim semaphore = new(1, 1); + private const int MaxCached = 150; public void Initialize(Dictionary<(string, bool), int> usage) { foreach (var key in usage.Keys) { - Data[key] = new ImageUsage(usage[key], null); + Cached.Save(key, new ImageUsage(usage[key], null), TimeSpan.MaxValue, MaxCached); } } @@ -40,70 +37,48 @@ public void Initialize(Dictionary<(string, bool), int> usage) { get { - if (!Data.TryGetValue((path, isFullImage), out var value)) + if (!Cached.TryGet((path, isFullImage), out var value)) { return null; } - value.usage++; - return value.imageSource; + value.Value.usage++; + return value.Value.imageSource; } set { - Data.AddOrUpdate( - (path, isFullImage), - new ImageUsage(0, value), - (k, v) => - { - v.imageSource = value; - v.usage++; - return v; - } - ); - - SliceExtra(); - - async void SliceExtra() + if (Cached.TryGet((path, isFullImage), out var cached)) { - // To prevent the dictionary from drastically increasing in size by caching images, the dictionary size is not allowed to grow more than the permissibleFactor * maxCached size - // This is done so that we don't constantly perform this resizing operation and also maintain the image cache size at the same time - if (Data.Count > permissibleFactor * MaxCached) - { - await semaphore.WaitAsync().ConfigureAwait(false); - // To delete the images from the data dictionary based on the resizing of the Usage Dictionary - // Double Check to avoid concurrent remove - if (Data.Count > permissibleFactor * MaxCached) - foreach (var key in Data.OrderBy(x => x.Value.usage).Take(Data.Count - MaxCached).Select(x => x.Key)) - Data.TryRemove(key, out _); - semaphore.Release(); - } + cached.Value.imageSource = value; + cached.Value.usage++; } + + Cached.Save((path, isFullImage), new ImageUsage(0, value), TimeSpan.MaxValue, + MaxCached); } } public bool ContainsKey(string key, bool isFullImage) { - return key is not null && Data.ContainsKey((key, isFullImage)) && Data[(key, isFullImage)].imageSource != null; + return Cached.TryGet((key, isFullImage), out _); } public bool TryGetValue(string key, bool isFullImage, out ImageSource image) { - if (key is not null) - { - bool hasKey = Data.TryGetValue((key, isFullImage), out var imageUsage); - image = hasKey ? imageUsage.imageSource : null; - return hasKey; - } - else + if (Cached.TryGet((key, isFullImage), out var value)) { - image = null; - return false; + image = value.Value.imageSource; + value.Value.usage++; + return image != null; } + + image = null; + return false; } public int CacheSize() { - return Data.Count; + return CacheManager.TotalCount<(string, bool), ImageUsage>(); } /// @@ -111,7 +86,14 @@ public int CacheSize() /// public int UniqueImagesInCache() { - return Data.Values.Select(x => x.imageSource).Distinct().Count(); + return CacheManager.EnumerateEntries<(string, bool), ImageUsage>().Select(x => x.Value.imageSource) + .Distinct() + .Count(); + } + + public IEnumerable> EnumerateEntries() + { + return CacheManager.EnumerateEntries<(string, bool), ImageUsage>(); } } } diff --git a/Flow.Launcher.Infrastructure/Image/ImageLoader.cs b/Flow.Launcher.Infrastructure/Image/ImageLoader.cs index add6d4e9237..75c2a4ec989 100644 --- a/Flow.Launcher.Infrastructure/Image/ImageLoader.cs +++ b/Flow.Launcher.Infrastructure/Image/ImageLoader.cs @@ -49,7 +49,7 @@ public static async Task InitializeAsync() { await Stopwatch.NormalAsync("|ImageLoader.Initialize|Preload images cost", async () => { - foreach (var ((path, isFullImage), _) in ImageCache.Data) + foreach (var ((path, isFullImage), _) in usage) { await LoadAsync(path, isFullImage); } @@ -65,7 +65,7 @@ public static async Task Save() try { - _storage.SaveAsync(ImageCache.Data + await _storage.SaveAsync(ImageCache.EnumerateEntries() .ToDictionary( x => x.Key, x => x.Value.usage)); @@ -125,9 +125,12 @@ private static async ValueTask LoadInternalAsync(string path, bool return new ImageResult(MissingImage, ImageType.Error); } - if (ImageCache.ContainsKey(path, loadFullImage)) + // extra scope for use of same variable name { - return new ImageResult(ImageCache[path, loadFullImage], ImageType.Cache); + if (ImageCache.TryGetValue(path, loadFullImage, out var imageSource)) + { + return new ImageResult(imageSource, ImageType.Cache); + } } if (Uri.TryCreate(path, UriKind.RelativeOrAbsolute, out var uriResult)