Merge pull request #3898 from Flow-Launcher/storage_api_method

Load images into ImageCache & Use non-async method in ImageCache
This commit is contained in:
Jeremy Wu 2025-08-31 15:31:01 +10:00 committed by GitHub
commit 72b4ff31ed
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 95 additions and 69 deletions

View file

@ -19,14 +19,14 @@ namespace Flow.Launcher.Infrastructure.Image
private static readonly string ClassName = nameof(ImageLoader);
private static readonly ImageCache ImageCache = new();
private static SemaphoreSlim storageLock { get; } = new SemaphoreSlim(1, 1);
private static Lock storageLock { get; } = new();
private static BinaryStorage<List<(string, bool)>> _storage;
private static readonly ConcurrentDictionary<string, string> GuidToKey = new();
private static IImageHashGenerator _hashGenerator;
private static readonly bool EnableImageHash = true;
public static ImageSource Image { get; } = new BitmapImage(new Uri(Constant.ImageIcon));
public static ImageSource MissingImage { get; } = new BitmapImage(new Uri(Constant.MissingImgIcon));
public static ImageSource LoadingImage { get; } = new BitmapImage(new Uri(Constant.LoadingImgIcon));
public static ImageSource Image => ImageCache[Constant.ImageIcon, false];
public static ImageSource MissingImage => ImageCache[Constant.MissingImgIcon, false];
public static ImageSource LoadingImage => ImageCache[Constant.LoadingImgIcon, false];
public const int SmallIconSize = 64;
public const int FullIconSize = 256;
public const int FullImageSize = 320;
@ -36,20 +36,25 @@ namespace Flow.Launcher.Infrastructure.Image
public static async Task InitializeAsync()
{
_storage = new BinaryStorage<List<(string, bool)>>("Image");
_hashGenerator = new ImageHashGenerator();
var usage = await LoadStorageToConcurrentDictionaryAsync();
_storage.ClearData();
ImageCache.Initialize(usage);
foreach (var icon in new[] { Constant.DefaultIcon, Constant.MissingImgIcon })
var usage = await Task.Run(() =>
{
ImageSource img = new BitmapImage(new Uri(icon));
img.Freeze();
ImageCache[icon, false] = img;
}
_storage = new BinaryStorage<List<(string, bool)>>("Image");
_hashGenerator = new ImageHashGenerator();
var usage = LoadStorageToConcurrentDictionary();
_storage.ClearData();
ImageCache.Initialize(usage);
foreach (var icon in new[] { Constant.DefaultIcon, Constant.ImageIcon, Constant.MissingImgIcon, Constant.LoadingImgIcon })
{
ImageSource img = new BitmapImage(new Uri(icon));
img.Freeze();
ImageCache[icon, false] = img;
}
return usage;
});
_ = Task.Run(async () =>
{
@ -64,42 +69,26 @@ namespace Flow.Launcher.Infrastructure.Image
});
}
public static async Task SaveAsync()
public static void Save()
{
await storageLock.WaitAsync();
try
lock (storageLock)
{
await _storage.SaveAsync(ImageCache.EnumerateEntries()
.Select(x => x.Key)
.ToList());
}
catch (System.Exception e)
{
Log.Exception(ClassName, "Failed to save image cache to file", e);
}
finally
{
storageLock.Release();
try
{
_storage.Save([.. ImageCache.EnumerateEntries().Select(x => x.Key)]);
}
catch (System.Exception e)
{
Log.Exception(ClassName, "Failed to save image cache to file", e);
}
}
}
public static async Task WaitSaveAsync()
private static List<(string, bool)> LoadStorageToConcurrentDictionary()
{
await storageLock.WaitAsync();
storageLock.Release();
}
private static async Task<List<(string, bool)>> LoadStorageToConcurrentDictionaryAsync()
{
await storageLock.WaitAsync();
try
lock (storageLock)
{
return await _storage.TryLoadAsync(new List<(string, bool)>());
}
finally
{
storageLock.Release();
return _storage.TryLoad([]);
}
}
@ -174,7 +163,7 @@ namespace Flow.Launcher.Infrastructure.Image
Log.Exception(ClassName, $"Failed to get thumbnail for {path} on first try", e);
Log.Exception(ClassName, $"Failed to get thumbnail for {path} on second try", e2);
ImageSource image = ImageCache[Constant.MissingImgIcon, false];
ImageSource image = MissingImage;
ImageCache[path, false] = image;
imageResult = new ImageResult(image, ImageType.Error);
}
@ -273,7 +262,7 @@ namespace Flow.Launcher.Infrastructure.Image
}
else
{
image = ImageCache[Constant.MissingImgIcon, false];
image = MissingImage;
path = Constant.MissingImgIcon;
}

View file

@ -12,7 +12,7 @@ using MemoryPack;
namespace Flow.Launcher.Infrastructure.Storage
{
/// <summary>
/// Stroage object using binary data
/// Storage object using binary data
/// Normally, it has better performance, but not readable
/// </summary>
/// <remarks>
@ -53,6 +53,45 @@ namespace Flow.Launcher.Infrastructure.Storage
FilePath = Path.Combine(DirectoryPath, $"{filename}{FileSuffix}");
}
public T TryLoad(T defaultData)
{
if (Data != null) return Data;
if (File.Exists(FilePath))
{
if (new FileInfo(FilePath).Length == 0)
{
Log.Error(ClassName, $"Zero length cache file <{FilePath}>");
Data = defaultData;
Save();
}
var bytes = File.ReadAllBytes(FilePath);
Data = Deserialize(bytes, defaultData);
}
else
{
Log.Info(ClassName, "Cache file not exist, load default data");
Data = defaultData;
Save();
}
return Data;
}
private T Deserialize(ReadOnlySpan<byte> bytes, T defaultData)
{
try
{
var t = MemoryPackSerializer.Deserialize<T>(bytes);
return t ?? defaultData;
}
catch (System.Exception e)
{
Log.Exception(ClassName, $"Deserialize error for file <{FilePath}>", e);
return defaultData;
}
}
public async ValueTask<T> TryLoadAsync(T defaultData)
{
if (Data != null) return Data;
@ -79,26 +118,31 @@ namespace Flow.Launcher.Infrastructure.Storage
return Data;
}
private static async ValueTask<T> DeserializeAsync(Stream stream, T defaultData)
private async ValueTask<T> DeserializeAsync(Stream stream, T defaultData)
{
try
{
var t = await MemoryPackSerializer.DeserializeAsync<T>(stream);
return t ?? defaultData;
}
catch (System.Exception)
catch (System.Exception e)
{
// Log.Exception($"|BinaryStorage.Deserialize|Deserialize error for file <{FilePath}>", e);
Log.Exception(ClassName, $"Deserialize error for file <{FilePath}>", e);
return defaultData;
}
}
public void Save()
{
Save(Data.NonNull());
}
public void Save(T data)
{
// User may delete the directory, so we need to check it
FilesFolders.ValidateDirectory(DirectoryPath);
var serialized = MemoryPackSerializer.Serialize(Data);
var serialized = MemoryPackSerializer.Serialize(data);
File.WriteAllBytes(FilePath, serialized);
}
@ -107,15 +151,6 @@ namespace Flow.Launcher.Infrastructure.Storage
await SaveAsync(Data.NonNull());
}
// ImageCache need to convert data into concurrent dictionary for usage,
// so we would better to clear the data
public void ClearData()
{
Data = default;
}
// ImageCache storages data in its class,
// so we need to pass it to SaveAsync
public async ValueTask SaveAsync(T data)
{
// User may delete the directory, so we need to check it
@ -124,5 +159,12 @@ namespace Flow.Launcher.Infrastructure.Storage
await using var stream = new FileStream(FilePath, FileMode.Create);
await MemoryPackSerializer.SerializeAsync(stream, data);
}
// ImageCache need to convert data into concurrent dictionary for usage,
// so we would better to clear the data
public void ClearData()
{
Data = default;
}
}
}

View file

@ -18,7 +18,6 @@ using Flow.Launcher.Core.Plugin;
using Flow.Launcher.Core.Resource;
using Flow.Launcher.Infrastructure;
using Flow.Launcher.Infrastructure.Hotkey;
using Flow.Launcher.Infrastructure.Image;
using Flow.Launcher.Infrastructure.DialogJump;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
@ -358,7 +357,6 @@ namespace Flow.Launcher
_notifyIcon.Visible = false;
App.API.SaveAppAllSettings();
e.Cancel = true;
await ImageLoader.WaitSaveAsync();
await PluginManager.DisposePluginsAsync();
Notification.Uninstall();
// After plugins are all disposed, we shutdown application to close app

View file

@ -75,7 +75,7 @@ namespace Flow.Launcher
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD100:Avoid async void methods", Justification = "<Pending>")]
public async void RestartApp()
public void RestartApp()
{
_mainVM.Hide();
@ -84,9 +84,6 @@ namespace Flow.Launcher
// which will cause ungraceful exit
SaveAppAllSettings();
// Wait for all image caches to be saved before restarting
await ImageLoader.WaitSaveAsync();
// Restart requires Squirrel's Update.exe to be present in the parent folder,
// it is only published from the project's release pipeline. When debugging without it,
// the project may not restart or just terminates. This is expected.
@ -116,8 +113,8 @@ namespace Flow.Launcher
_settings.Save();
PluginManager.Save();
_mainVM.Save();
ImageLoader.Save();
}
_ = ImageLoader.SaveAsync();
}
public Task ReloadAllPluginData() => PluginManager.ReloadDataAsync();