Skip to content

Commit 0ab7dc6

Browse files
authored
Merge pull request #133 from Flow-Launcher/address_high_memory_issue
Limiting the number of ImageSources cached to address memory issue
2 parents 44654e5 + 26285ab commit 0ab7dc6

File tree

2 files changed

+89
-44
lines changed

2 files changed

+89
-44
lines changed

Flow.Launcher.Infrastructure/Image/ImageCache.cs

Lines changed: 64 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,53 +2,100 @@
22
using System.Collections.Concurrent;
33
using System.Collections.Generic;
44
using System.Linq;
5+
using System.Threading.Tasks;
56
using System.Windows.Media;
67

78
namespace Flow.Launcher.Infrastructure.Image
89
{
910
[Serializable]
11+
public class ImageUsage
12+
{
13+
14+
public int usage;
15+
public ImageSource imageSource;
16+
17+
public ImageUsage(int usage, ImageSource image)
18+
{
19+
this.usage = usage;
20+
imageSource = image;
21+
}
22+
}
23+
1024
public class ImageCache
1125
{
12-
private const int MaxCached = 5000;
13-
public ConcurrentDictionary<string, int> Usage = new ConcurrentDictionary<string, int>();
14-
private readonly ConcurrentDictionary<string, ImageSource> _data = new ConcurrentDictionary<string, ImageSource>();
26+
private const int MaxCached = 50;
27+
public ConcurrentDictionary<string, ImageUsage> Data { get; private set; } = new ConcurrentDictionary<string, ImageUsage>();
28+
private const int permissibleFactor = 2;
1529

30+
public void Initialization(Dictionary<string, int> usage)
31+
{
32+
foreach (var key in usage.Keys)
33+
{
34+
Data[key] = new ImageUsage(usage[key], null);
35+
}
36+
}
1637

1738
public ImageSource this[string path]
1839
{
1940
get
2041
{
21-
Usage.AddOrUpdate(path, 1, (k, v) => v + 1);
22-
var i = _data[path];
23-
return i;
42+
if (Data.TryGetValue(path, out var value))
43+
{
44+
value.usage++;
45+
return value.imageSource;
46+
}
47+
48+
return null;
2449
}
25-
set { _data[path] = value; }
26-
}
50+
set
51+
{
52+
Data.AddOrUpdate(
53+
path,
54+
new ImageUsage(0, value),
55+
(k, v) =>
56+
{
57+
v.imageSource = value;
58+
v.usage++;
59+
return v;
60+
}
61+
);
2762

28-
public Dictionary<string, int> CleanupAndToDictionary()
29-
=> Usage
30-
.OrderByDescending(o => o.Value)
31-
.Take(MaxCached)
32-
.ToDictionary(i => i.Key, i => i.Value);
63+
// 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
64+
// This is done so that we don't constantly perform this resizing operation and also maintain the image cache size at the same time
65+
if (Data.Count > permissibleFactor * MaxCached)
66+
{
67+
// To delete the images from the data dictionary based on the resizing of the Usage Dictionary.
68+
69+
70+
foreach (var key in Data.OrderBy(x => x.Value.usage).Take(Data.Count - MaxCached).Select(x => x.Key))
71+
{
72+
if (!(key.Equals(Constant.ErrorIcon) || key.Equals(Constant.DefaultIcon)))
73+
{
74+
Data.TryRemove(key, out _);
75+
}
76+
}
77+
}
78+
}
79+
}
3380

3481
public bool ContainsKey(string key)
3582
{
36-
var contains = _data.ContainsKey(key);
83+
var contains = Data.ContainsKey(key);
3784
return contains;
3885
}
3986

4087
public int CacheSize()
4188
{
42-
return _data.Count;
89+
return Data.Count;
4390
}
4491

4592
/// <summary>
4693
/// return the number of unique images in the cache (by reference not by checking images content)
4794
/// </summary>
4895
public int UniqueImagesInCache()
4996
{
50-
return _data.Values.Distinct().Count();
97+
return Data.Values.Select(x => x.imageSource).Distinct().Count();
5198
}
5299
}
53100

54-
}
101+
}

Flow.Launcher.Infrastructure/Image/ImageLoader.cs

Lines changed: 25 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,11 @@ namespace Flow.Launcher.Infrastructure.Image
1313
{
1414
public static class ImageLoader
1515
{
16-
private static readonly ImageCache _imageCache = new ImageCache();
17-
private static readonly ConcurrentDictionary<string, string> _guidToKey = new ConcurrentDictionary<string, string>();
18-
private static readonly bool _enableHashImage = true;
19-
16+
private static readonly ImageCache ImageCache = new ImageCache();
2017
private static BinaryStorage<Dictionary<string, int>> _storage;
18+
private static readonly ConcurrentDictionary<string, string> GuidToKey = new ConcurrentDictionary<string, string>();
2119
private static IImageHashGenerator _hashGenerator;
20+
private static bool EnableImageHash = true;
2221

2322
private static readonly string[] ImageExtensions =
2423
{
@@ -36,39 +35,39 @@ public static void Initialize()
3635
_storage = new BinaryStorage<Dictionary<string, int>>("Image");
3736
_hashGenerator = new ImageHashGenerator();
3837

39-
_imageCache.Usage = LoadStorageToConcurrentDictionary();
38+
var usage = LoadStorageToConcurrentDictionary();
4039

4140
foreach (var icon in new[] { Constant.DefaultIcon, Constant.MissingImgIcon })
4241
{
4342
ImageSource img = new BitmapImage(new Uri(icon));
4443
img.Freeze();
45-
_imageCache[icon] = img;
44+
ImageCache[icon] = img;
4645
}
4746

4847
Task.Run(() =>
4948
{
5049
Stopwatch.Normal("|ImageLoader.Initialize|Preload images cost", () =>
5150
{
52-
_imageCache.Usage.AsParallel().ForAll(x =>
51+
ImageCache.Data.AsParallel().ForAll(x =>
5352
{
5453
Load(x.Key);
5554
});
5655
});
57-
Log.Info($"|ImageLoader.Initialize|Number of preload images is <{_imageCache.Usage.Count}>, Images Number: {_imageCache.CacheSize()}, Unique Items {_imageCache.UniqueImagesInCache()}");
56+
Log.Info($"|ImageLoader.Initialize|Number of preload images is <{ImageCache.CacheSize()}>, Images Number: {ImageCache.CacheSize()}, Unique Items {ImageCache.UniqueImagesInCache()}");
5857
});
5958
}
6059

6160
public static void Save()
6261
{
6362
lock (_storage)
6463
{
65-
_storage.Save(_imageCache.CleanupAndToDictionary());
64+
_storage.Save(ImageCache.Data.Select(x => (x.Key, x.Value.usage)).ToDictionary(x => x.Key, y => y.usage));
6665
}
6766
}
6867

6968
private static ConcurrentDictionary<string, int> LoadStorageToConcurrentDictionary()
7069
{
71-
lock(_storage)
70+
lock (_storage)
7271
{
7372
var loaded = _storage.TryLoad(new Dictionary<string, int>());
7473

@@ -106,11 +105,11 @@ private static ImageResult LoadInternal(string path, bool loadFullImage = false)
106105
{
107106
if (string.IsNullOrEmpty(path))
108107
{
109-
return new ImageResult(_imageCache[Constant.MissingImgIcon], ImageType.Error);
108+
return new ImageResult(ImageCache[Constant.MissingImgIcon], ImageType.Error);
110109
}
111-
if (_imageCache.ContainsKey(path))
110+
if (ImageCache.ContainsKey(path))
112111
{
113-
return new ImageResult(_imageCache[path], ImageType.Cache);
112+
return new ImageResult(ImageCache[path], ImageType.Cache);
114113
}
115114

116115
if (path.StartsWith("data:", StringComparison.OrdinalIgnoreCase))
@@ -139,8 +138,8 @@ private static ImageResult LoadInternal(string path, bool loadFullImage = false)
139138
Log.Exception($"|ImageLoader.Load|Failed to get thumbnail for {path} on first try", e);
140139
Log.Exception($"|ImageLoader.Load|Failed to get thumbnail for {path} on second try", e2);
141140

142-
ImageSource image = _imageCache[Constant.MissingImgIcon];
143-
_imageCache[path] = image;
141+
ImageSource image = ImageCache[Constant.MissingImgIcon];
142+
ImageCache[path] = image;
144143
imageResult = new ImageResult(image, ImageType.Error);
145144
}
146145
}
@@ -191,7 +190,7 @@ private static ImageResult GetThumbnailResult(ref string path, bool loadFullImag
191190
}
192191
else
193192
{
194-
image = _imageCache[Constant.MissingImgIcon];
193+
image = ImageCache[Constant.MissingImgIcon];
195194
path = Constant.MissingImgIcon;
196195
}
197196

@@ -218,27 +217,26 @@ public static ImageSource Load(string path, bool loadFullImage = false)
218217

219218
var img = imageResult.ImageSource;
220219
if (imageResult.ImageType != ImageType.Error && imageResult.ImageType != ImageType.Cache)
221-
{
222-
// we need to get image hash
223-
string hash = _enableHashImage ? _hashGenerator.GetHashFromImage(img) : null;
220+
{ // we need to get image hash
221+
string hash = EnableImageHash ? _hashGenerator.GetHashFromImage(img) : null;
224222
if (hash != null)
225223
{
226-
if (_guidToKey.TryGetValue(hash, out string key))
227-
{
228-
// image already exists
229-
img = _imageCache[key];
224+
225+
if (GuidToKey.TryGetValue(hash, out string key))
226+
{ // image already exists
227+
img = ImageCache[key] ?? img;
230228
}
231229
else
232-
{
233-
// new guid
234-
_guidToKey[hash] = path;
230+
{ // new guid
231+
GuidToKey[hash] = path;
235232
}
236233
}
237234

238235
// update cache
239-
_imageCache[path] = img;
236+
ImageCache[path] = img;
240237
}
241238

239+
242240
return img;
243241
}
244242

0 commit comments

Comments
 (0)