Skip to content

Load images into ImageCache & Use non-async method in ImageCache #3898

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 35 additions & 46 deletions Flow.Launcher.Infrastructure/Image/ImageLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,14 @@ public static class ImageLoader
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;
Expand All @@ -36,20 +36,25 @@ public static class ImageLoader

public static async Task InitializeAsync()
{
_storage = new BinaryStorage<List<(string, bool)>>("Image");
_hashGenerator = new ImageHashGenerator();
var usage = await Task.Run(() =>
{
_storage = new BinaryStorage<List<(string, bool)>>("Image");
_hashGenerator = new ImageHashGenerator();

var usage = await LoadStorageToConcurrentDictionaryAsync();
_storage.ClearData();
var usage = LoadStorageToConcurrentDictionary();
_storage.ClearData();

ImageCache.Initialize(usage);
ImageCache.Initialize(usage);

foreach (var icon in new[] { Constant.DefaultIcon, Constant.MissingImgIcon })
{
ImageSource img = new BitmapImage(new Uri(icon));
img.Freeze();
ImageCache[icon, false] = img;
}
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 () =>
{
Expand All @@ -64,42 +69,26 @@ await Stopwatch.InfoAsync(ClassName, "Preload images cost", async () =>
});
}

public static async Task SaveAsync()
public static void Save()
{
await storageLock.WaitAsync();

try
{
await _storage.SaveAsync(ImageCache.EnumerateEntries()
.Select(x => x.Key)
.ToList());
}
catch (System.Exception e)
lock (storageLock)
{
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
{
return await _storage.TryLoadAsync(new List<(string, bool)>());
}
finally
lock (storageLock)
{
storageLock.Release();
return _storage.TryLoad([]);
}
}

Expand Down Expand Up @@ -174,7 +163,7 @@ private static async ValueTask<ImageResult> LoadInternalAsync(string path, bool
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);
}
Expand Down Expand Up @@ -273,7 +262,7 @@ private static ImageResult GetThumbnailResult(ref string path, bool loadFullImag
}
else
{
image = ImageCache[Constant.MissingImgIcon, false];
image = MissingImage;
path = Constant.MissingImgIcon;
}

Expand Down
70 changes: 56 additions & 14 deletions Flow.Launcher.Infrastructure/Storage/BinaryStorage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
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>
Expand Down Expand Up @@ -53,6 +53,45 @@ public BinaryStorage(string filename, string directoryPath = null!)
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;
Expand All @@ -79,26 +118,31 @@ public async ValueTask<T> TryLoadAsync(T defaultData)
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);
}

Expand All @@ -107,15 +151,6 @@ public async ValueTask SaveAsync()
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
Expand All @@ -124,5 +159,12 @@ public async ValueTask SaveAsync(T data)
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;
}
}
}
2 changes: 0 additions & 2 deletions Flow.Launcher/MainWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
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;
Expand Down Expand Up @@ -113,7 +112,7 @@
{
var handle = Win32Helper.GetWindowHandle(this, true);
_hwndSource = HwndSource.FromHwnd(handle);
_hwndSource.AddHook(WndProc);

Check warning on line 115 in Flow.Launcher/MainWindow.xaml.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`Wnd` is not a recognized word. (unrecognized-spelling)
Win32Helper.HideFromAltTab(this);
Win32Helper.DisableControlBox(this);
}
Expand Down Expand Up @@ -358,7 +357,6 @@
_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
Expand All @@ -371,7 +369,7 @@
{
try
{
_hwndSource.RemoveHook(WndProc);

Check warning on line 372 in Flow.Launcher/MainWindow.xaml.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`Wnd` is not a recognized word. (unrecognized-spelling)
}
catch (Exception)
{
Expand Down Expand Up @@ -584,9 +582,9 @@

#endregion

#region Window WndProc

Check warning on line 585 in Flow.Launcher/MainWindow.xaml.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`Wnd` is not a recognized word. (unrecognized-spelling)

private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)

Check warning on line 587 in Flow.Launcher/MainWindow.xaml.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`Wnd` is not a recognized word. (unrecognized-spelling)
{
switch (msg)
{
Expand Down Expand Up @@ -773,17 +771,17 @@
Header = App.API.GetTranslation("iconTrayOpen") + " (" + _settings.Hotkey + ")",
Icon = openIcon
};
var gamemodeIcon = new FontIcon { Glyph = "\ue7fc" };

Check warning on line 774 in Flow.Launcher/MainWindow.xaml.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`gamemode` is not a recognized word. (unrecognized-spelling)
var gamemode = new MenuItem

Check warning on line 775 in Flow.Launcher/MainWindow.xaml.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`gamemode` is not a recognized word. (unrecognized-spelling)
{
Header = App.API.GetTranslation("GameMode"),
Icon = gamemodeIcon

Check warning on line 778 in Flow.Launcher/MainWindow.xaml.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`gamemode` is not a recognized word. (unrecognized-spelling)
};
var positionresetIcon = new FontIcon { Glyph = "\ue73f" };
var positionreset = new MenuItem

Check warning on line 781 in Flow.Launcher/MainWindow.xaml.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`positionreset` is not a recognized word. (unrecognized-spelling)
{
Header = App.API.GetTranslation("PositionReset"),
Icon = positionresetIcon

Check warning on line 784 in Flow.Launcher/MainWindow.xaml.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`positionreset` is not a recognized word. (unrecognized-spelling)
};
var settingsIcon = new FontIcon { Glyph = "\ue713" };
var settings = new MenuItem
Expand All @@ -799,7 +797,7 @@
};

open.Click += (o, e) => _viewModel.ToggleFlowLauncher();
gamemode.Click += (o, e) => _viewModel.ToggleGameMode();

Check warning on line 800 in Flow.Launcher/MainWindow.xaml.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`gamemode` is not a recognized word. (unrecognized-spelling)
positionreset.Click += (o, e) => _ = PositionResetAsync();
settings.Click += (o, e) => App.API.OpenSettingDialog();
exit.Click += (o, e) => Close();
Expand All @@ -820,7 +818,7 @@

public void UpdatePosition()
{
// Initialize call twice to work around multi-display alignment issue- https://github.com/Flow-Launcher/Flow.Launcher/issues/2910

Check failure on line 821 in Flow.Launcher/MainWindow.xaml.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`work around` matches a line_forbidden.patterns entry: `\bwork[- ]arounds?\b`. (forbidden-pattern)
if (_viewModel.IsDialogJumpWindowUnderDialog())
{
InitializeDialogJumpPosition();
Expand All @@ -844,7 +842,7 @@

private void InitializePosition()
{
// Initialize call twice to work around multi-display alignment issue- https://github.com/Flow-Launcher/Flow.Launcher/issues/2910

Check failure on line 845 in Flow.Launcher/MainWindow.xaml.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`work around` matches a line_forbidden.patterns entry: `\bwork[- ]arounds?\b`. (forbidden-pattern)
InitializePositionInner();
InitializePositionInner();
return;
Expand Down
7 changes: 2 additions & 5 deletions Flow.Launcher/PublicAPIInstance.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ public void ChangeQuery(string query, bool requery = false)
}

[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD100:Avoid async void methods", Justification = "<Pending>")]
public async void RestartApp()
public void RestartApp()
{
_mainVM.Hide();

Expand All @@ -83,9 +83,6 @@ public async void RestartApp()
// 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.
Expand Down Expand Up @@ -115,8 +112,8 @@ public void SaveAppAllSettings()
_settings.Save();
PluginManager.Save();
_mainVM.Save();
ImageLoader.Save();
}
_ = ImageLoader.SaveAsync();
}

public Task ReloadAllPluginData() => PluginManager.ReloadDataAsync();
Expand Down
Loading