optimize Cache performance

This commit is contained in:
DismissedLight
2022-07-24 00:05:57 +08:00
parent a163e00907
commit 7fcb255a21
13 changed files with 243 additions and 250 deletions

View File

@@ -6,6 +6,7 @@ using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Media.Imaging;
using Snap.Hutao.Core.Caching;
using Snap.Hutao.Extension;
using Windows.Storage;
namespace Snap.Hutao.Control.Image;
@@ -14,15 +15,11 @@ namespace Snap.Hutao.Control.Image;
/// </summary>
public class CachedImage : ImageEx
{
private readonly IImageCache imageCache;
/// <summary>
/// 构造一个新的缓存图像
/// </summary>
public CachedImage()
{
imageCache = Ioc.Default.GetRequiredService<IImageCache>();
IsCacheEnabled = true;
EnableLazyLoading = true;
}
@@ -30,10 +27,15 @@ public class CachedImage : ImageEx
/// <inheritdoc/>
protected override async Task<ImageSource> ProvideCachedResourceAsync(Uri imageUri, CancellationToken token)
{
BitmapImage? image;
IImageCache imageCache = Ioc.Default.GetRequiredService<IImageCache>();
try
{
image = await imageCache.GetFromCacheAsync(imageUri, true);
StorageFile file = await imageCache.GetFileFromCacheAsync(imageUri);
// check token state to determine whether the operation should be canceled.
Must.TryThrowOnCanceled(token, "Image source has changed.");
return new BitmapImage(new(file.Path));
}
catch (TaskCanceledException)
{
@@ -43,18 +45,8 @@ public class CachedImage : ImageEx
catch
{
// maybe the image is corrupted, remove it.
await imageCache.RemoveAsync(imageUri.Enumerate());
await imageCache.RemoveAsync(imageUri.Enumerate()).ConfigureAwait(false);
throw;
}
// check token state to determine whether the operation should be canceled.
if (token.IsCancellationRequested)
{
throw new TaskCanceledException("Image source has changed.");
}
else
{
return Must.NotNull(image!);
}
}
}

View File

@@ -60,7 +60,7 @@ public class Gradient : Microsoft.UI.Xaml.Controls.Control
{
Ioc.Default
.GetRequiredService<IInfoBarService>()
.Error(exception, "应用渐变背景时发生异常");
.Error(exception, "应用渐变图像时发生异常");
}
private static Task<StorageFile?> GetCachedFileAsync(string url)

View File

@@ -0,0 +1,149 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.WinUI.UI.Animations;
using Microsoft.Graphics.Canvas.Effects;
using Microsoft.UI;
using Microsoft.UI.Composition;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Hosting;
using Microsoft.UI.Xaml.Media;
using Snap.Hutao.Core;
using Snap.Hutao.Core.Caching;
using Snap.Hutao.Core.Threading;
using Snap.Hutao.Extension;
using Snap.Hutao.Service.Abstraction;
using Windows.Graphics.Imaging;
using Windows.Storage;
using Windows.Storage.Streams;
namespace Snap.Hutao.Control.Image;
/// <summary>
/// 支持单色的图像
/// </summary>
public class MonoChrome : Microsoft.UI.Xaml.Controls.Control
{
private static readonly DependencyProperty SourceProperty = Property<MonoChrome>.Depend(nameof(Source), string.Empty, OnSourceChanged);
private static readonly ConcurrentCancellationTokenSource<MonoChrome> LoadingTokenSource = new();
private SpriteVisual? spriteVisual;
private CompositionColorBrush? backgroundBrush;
/// <summary>
/// 构造一个新的单色图像
/// </summary>
public MonoChrome()
{
SizeChanged += OnSizeChanged;
ActualThemeChanged += OnActualThemeChanged;
}
/// <summary>
/// 源
/// </summary>
public string Source
{
get => (string)GetValue(SourceProperty);
set => SetValue(SourceProperty, value);
}
private static void OnSourceChanged(DependencyObject sender, DependencyPropertyChangedEventArgs arg)
{
MonoChrome monoChrome = (MonoChrome)sender;
string url = (string)arg.NewValue;
ILogger<MonoChrome> logger = Ioc.Default.GetRequiredService<ILogger<MonoChrome>>();
monoChrome.ApplyImageAsync(url, LoadingTokenSource.Register(monoChrome)).SafeForget(logger, OnApplyImageFailed);
}
private static void OnApplyImageFailed(Exception exception)
{
Ioc.Default
.GetRequiredService<IInfoBarService>()
.Error(exception, "应用单色背景时发生异常");
}
private static async Task<LoadedImageSurface> LoadImageSurfaceAsync(StorageFile storageFile, CancellationToken token)
{
using (IRandomAccessStream imageStream = await storageFile.OpenAsync(FileAccessMode.Read).AsTask(token))
{
BitmapDecoder decoder = await BitmapDecoder.CreateAsync(imageStream).AsTask(token);
return LoadedImageSurface.StartLoadFromStream(imageStream);
}
}
private static Task<StorageFile?> GetCachedFileAsync(string url)
{
return Ioc.Default.GetRequiredService<IImageCache>().GetFileFromCacheAsync(new(url));
}
private void OnSizeChanged(object sender, SizeChangedEventArgs e)
{
if (e.NewSize != e.PreviousSize && spriteVisual is not null)
{
UpdateVisual(spriteVisual);
}
}
private void OnActualThemeChanged(FrameworkElement sender, object args)
{
if (backgroundBrush != null)
{
SetBackgroundColor(backgroundBrush);
}
}
private void UpdateVisual(SpriteVisual spriteVisual)
{
if (spriteVisual is not null)
{
spriteVisual.Size = ActualSize;
}
}
private void SetBackgroundColor(CompositionColorBrush backgroundBrush)
{
ApplicationTheme theme = ActualTheme switch
{
ElementTheme.Light => ApplicationTheme.Light,
ElementTheme.Dark => ApplicationTheme.Dark,
_ => App.Current.RequestedTheme,
};
backgroundBrush.Color = theme switch
{
ApplicationTheme.Light => Colors.Black,
ApplicationTheme.Dark => Colors.White,
_ => throw Must.NeverHappen(),
};
}
private async Task ApplyImageAsync(string url, CancellationToken token)
{
await AnimationBuilder.Create().Opacity(0d).StartAsync(this, token);
StorageFile? storageFile = Must.NotNull((await GetCachedFileAsync(url))!);
Compositor compositor = ElementCompositionPreview.GetElementVisual(this).Compositor;
LoadedImageSurface imageSurface = await LoadImageSurfaceAsync(storageFile, token);
CompositionColorBrush blackLayerBursh = compositor.CreateColorBrush(Colors.Black);
CompositionSurfaceBrush imageSurfaceBrush = compositor.CompositeSurfaceBrush(imageSurface, stretch: CompositionStretch.Uniform, vRatio: 0f);
CompositionEffectBrush overlayBrush = compositor.CompositeBlendEffectBrush(blackLayerBursh, imageSurfaceBrush, BlendEffectMode.Overlay);
CompositionEffectBrush opacityBrush = compositor.CompositeLuminanceToAlphaEffectBrush(overlayBrush);
backgroundBrush = compositor.CreateColorBrush();
SetBackgroundColor(backgroundBrush);
CompositionEffectBrush alphaMaskEffectBrush = compositor.CompositeAlphaMaskEffectBrush(backgroundBrush, opacityBrush);
spriteVisual = compositor.CompositeSpriteVisual(alphaMaskEffectBrush);
UpdateVisual(spriteVisual);
ElementCompositionPreview.SetElementChildVisual(this, spriteVisual);
await AnimationBuilder.Create().Opacity(1d).StartAsync(this, token);
}
}

View File

@@ -3,7 +3,6 @@
// See the LICENSE file in the project root for more information.
using Snap.Hutao.Core.Logging;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
@@ -23,7 +22,6 @@ public abstract class CacheBase<T>
where T : class
{
private readonly SemaphoreSlim cacheFolderSemaphore = new(1);
private readonly ConcurrentDictionary<string, Task<T?>> concurrentTasks = new();
private readonly ILogger logger;
private readonly HttpClient httpClient;
@@ -48,12 +46,12 @@ public abstract class CacheBase<T>
/// <summary>
/// Gets or sets the life duration of every cache entry.
/// </summary>
public TimeSpan CacheDuration { get; set; }
public TimeSpan CacheDuration { get; }
/// <summary>
/// Gets or sets the number of retries trying to ensure the file is cached.
/// </summary>
public uint RetryCount { get; set; }
public uint RetryCount { get; }
/// <summary>
/// Clears all files in the cache
@@ -76,12 +74,12 @@ public abstract class CacheBase<T>
{
TimeSpan expiryDuration = duration ?? CacheDuration;
StorageFolder? folder = await GetCacheFolderAsync().ConfigureAwait(false);
IReadOnlyList<StorageFile>? files = await folder.GetFilesAsync().AsTask().ConfigureAwait(false);
StorageFolder folder = await GetCacheFolderAsync().ConfigureAwait(false);
IReadOnlyList<StorageFile> files = await folder.GetFilesAsync().AsTask().ConfigureAwait(false);
List<StorageFile>? filesToDelete = new();
List<StorageFile> filesToDelete = new();
foreach (StorageFile? file in files)
foreach (StorageFile file in files)
{
if (file == null)
{
@@ -135,24 +133,14 @@ public abstract class CacheBase<T>
await InternalClearAsync(filesToDelete).ConfigureAwait(false);
}
/// <summary>
/// Retrieves item represented by Uri from the cache. If the item is not found in the cache, it will try to downloaded and saved before returning it to the caller.
/// </summary>
/// <param name="uri">Uri of the item.</param>
/// <param name="throwOnError">Indicates whether or not exception should be thrown if item cannot be found / downloaded.</param>
/// <returns>an instance of Generic type</returns>
public Task<T?> GetFromCacheAsync(Uri uri, bool throwOnError = false)
{
return GetItemAsync(uri, throwOnError);
}
/// <summary>
/// Gets the StorageFile containing cached item for given Uri
/// </summary>
/// <param name="uri">Uri of the item.</param>
/// <returns>a StorageFile</returns>
public async Task<StorageFile?> GetFileFromCacheAsync(Uri uri)
public async Task<StorageFile> GetFileFromCacheAsync(Uri uri)
{
logger.LogInformation(EventIds.FileCaching, "Begin caching for [{uri}]", uri);
StorageFolder folder = await GetCacheFolderAsync().ConfigureAwait(false);
string fileName = GetCacheFileName(uri);
@@ -161,28 +149,14 @@ public abstract class CacheBase<T>
if (item == null)
{
StorageFile? baseFile = await folder.CreateFileAsync(fileName, CreationCollisionOption.ReplaceExisting).AsTask().ConfigureAwait(false);
StorageFile baseFile = await folder.CreateFileAsync(fileName, CreationCollisionOption.ReplaceExisting).AsTask().ConfigureAwait(false);
await DownloadFileAsync(uri, baseFile).ConfigureAwait(false);
item = await folder.TryGetItemAsync(fileName).AsTask().ConfigureAwait(false);
}
return item as StorageFile;
return Must.NotNull((item as StorageFile)!);
}
/// <summary>
/// Cache specific hooks to process items from HTTP response
/// </summary>
/// <param name="stream">input stream</param>
/// <returns>awaitable task</returns>
protected abstract Task<T> InitializeTypeAsync(Stream stream);
/// <summary>
/// Cache specific hooks to process items from HTTP response
/// </summary>
/// <param name="baseFile">storage file</param>
/// <returns>awaitable task</returns>
protected abstract Task<T> InitializeTypeAsync(StorageFile baseFile);
/// <summary>
/// Override-able method that checks whether file is valid or not.
/// </summary>
@@ -220,121 +194,16 @@ public abstract class CacheBase<T>
return value;
}
private async Task<T?> GetItemAsync(Uri uri, bool throwOnError)
private async Task DownloadFileAsync(Uri uri, StorageFile baseFile)
{
T? instance = default(T);
string fileName = GetCacheFileName(uri);
concurrentTasks.TryGetValue(fileName, out Task<T?>? request);
// complete previous task first
if (request != null)
using (Stream httpStream = await httpClient.GetStreamAsync(uri))
{
await request.ConfigureAwait(false);
request = null;
}
if (request == null)
{
request = GetFromCacheOrDownloadAsync(uri, fileName);
concurrentTasks[fileName] = request;
}
try
{
instance = await request.ConfigureAwait(false);
}
catch (Exception ex)
{
logger.LogError(EventIds.CacheException, ex, "Exception happened when caching.");
if (throwOnError)
using (Stream fs = await baseFile.OpenStreamForWriteAsync())
{
throw;
await httpStream.CopyToAsync(fs);
await fs.FlushAsync();
}
}
finally
{
concurrentTasks.TryRemove(fileName, out _);
}
return instance;
}
private async Task<T?> GetFromCacheOrDownloadAsync(Uri uri, string fileName)
{
T? instance = default;
StorageFolder folder = await GetCacheFolderAsync().ConfigureAwait(false);
StorageFile? baseFile = await folder.TryGetItemAsync(fileName).AsTask().ConfigureAwait(false) as StorageFile;
bool downloadDataFile = baseFile == null || await IsFileOutOfDateAsync(baseFile, CacheDuration).ConfigureAwait(false);
baseFile ??= await folder.CreateFileAsync(fileName, CreationCollisionOption.ReplaceExisting).AsTask().ConfigureAwait(false);
if (downloadDataFile)
{
uint retries = 0;
try
{
while (retries < RetryCount)
{
try
{
instance = await DownloadFileAsync(uri, baseFile).ConfigureAwait(false);
if (instance != null)
{
break;
}
}
catch (FileNotFoundException)
{
}
retries++;
}
}
catch (Exception)
{
await baseFile.DeleteAsync().AsTask().ConfigureAwait(false);
throw; // re-throwing the exception changes the stack trace. just throw
}
}
if (EqualityComparer<T>.Default.Equals(instance, default(T)))
{
instance = await InitializeTypeAsync(baseFile).ConfigureAwait(false);
}
return instance;
}
private async Task<T?> DownloadFileAsync(Uri uri, StorageFile baseFile)
{
T? instance = default;
using (MemoryStream memory = new())
{
using (Stream httpStream = await httpClient.GetStreamAsync(uri))
{
await httpStream.CopyToAsync(memory);
await memory.FlushAsync();
memory.Position = 0;
using (Stream fs = await baseFile.OpenStreamForWriteAsync())
{
await memory.CopyToAsync(fs);
await fs.FlushAsync();
memory.Position = 0;
}
}
instance = await InitializeTypeAsync(memory).ConfigureAwait(false);
}
return instance;
}
[SuppressMessage("", "CA1822")]

View File

@@ -18,15 +18,7 @@ internal interface IImageCache
/// </summary>
/// <param name="uri">Uri of the item.</param>
/// <returns>a StorageFile</returns>
Task<StorageFile?> GetFileFromCacheAsync(Uri uri);
/// <summary>
/// Retrieves item represented by Uri from the cache. If the item is not found in the cache, it will try to downloaded and saved before returning it to the caller.
/// </summary>
/// <param name="uri">Uri of the item.</param>
/// <param name="throwOnError">Indicates whether or not exception should be thrown if item cannot be found / downloaded.</param>
/// <returns>an instance of Generic type</returns>
Task<BitmapImage?> GetFromCacheAsync(Uri uri, bool throwOnError = false);
Task<StorageFile> GetFileFromCacheAsync(Uri uri);
/// <summary>
/// Removed items based on uri list passed
@@ -34,4 +26,11 @@ internal interface IImageCache
/// <param name="uriForCachedItems">Enumerable uri list</param>
/// <returns>awaitable Task</returns>
Task RemoveAsync(IEnumerable<Uri> uriForCachedItems);
/// <summary>
/// Removes cached files that have expired
/// </summary>
/// <param name="duration">Optional timespan to compute whether file has expired or not. If no value is supplied, <see cref="CacheDuration"/> is used.</param>
/// <returns>awaitable task</returns>
Task RemoveExpiredAsync(TimeSpan? duration = null);
}

View File

@@ -1,11 +1,8 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.WinUI;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml.Media.Imaging;
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using Windows.Storage;
using Windows.Storage.FileProperties;
@@ -21,10 +18,7 @@ public class ImageCache : CacheBase<BitmapImage>, IImageCache
{
private const string DateAccessedProperty = "System.DateAccessed";
private readonly List<string> extendedPropertyNames = new()
{
DateAccessedProperty,
};
private readonly List<string> extendedPropertyNames = new() { DateAccessedProperty };
/// <summary>
/// Initializes a new instance of the <see cref="ImageCache"/> class.
@@ -34,48 +28,6 @@ public class ImageCache : CacheBase<BitmapImage>, IImageCache
public ImageCache(ILogger<ImageCache> logger, HttpClient httpClient)
: base(logger, httpClient)
{
DispatcherQueue = Program.UIDispatcherQueue;
}
/// <summary>
/// Gets or sets which DispatcherQueue is used to dispatch UI updates.
/// </summary>
private DispatcherQueue DispatcherQueue { get; }
/// <summary>
/// Cache specific hooks to process items from HTTP response
/// </summary>
/// <param name="stream">input stream</param>
/// <returns>awaitable task</returns>
protected override Task<BitmapImage> InitializeTypeAsync(Stream stream)
{
if (stream.Length == 0)
{
throw new FileNotFoundException();
}
return DispatcherQueue.EnqueueAsync(async () =>
{
BitmapImage image = new();
// This action will run on the UI thread, no need to care which thread to continue with
await image.SetSourceAsync(stream.AsRandomAccessStream()).AsTask().ConfigureAwait(false);
return image;
});
}
/// <summary>
/// Cache specific hooks to process items from HTTP response
/// </summary>
/// <param name="baseFile">storage file</param>
/// <returns>awaitable task</returns>
protected override async Task<BitmapImage> InitializeTypeAsync(StorageFile baseFile)
{
using (Stream stream = await baseFile.OpenStreamForReadAsync().ConfigureAwait(false))
{
return await InitializeTypeAsync(stream).ConfigureAwait(false);
}
}
/// <summary>

View File

@@ -57,6 +57,11 @@ internal static class EventIds
/// </summary>
public static readonly EventId MetadataFileMD5Check = 100111;
/// <summary>
/// 文件缓存
/// </summary>
public static readonly EventId FileCaching = 100120;
// 杂项
/// <summary>

View File

@@ -84,4 +84,20 @@ public static class Must
#pragma warning restore CS8763 // 不应返回标记为 [DoesNotReturn] 的方法。
}
}
/// <summary>
/// 尝试抛出任务取消异常
/// </summary>
/// <param name="token">取消令牌</param>
/// <param name="message">取消消息</param>
/// <exception cref="TaskCanceledException">任务被取消</exception>
[DebuggerStepThrough]
[SuppressMessage("", "CA1068")]
public static void TryThrowOnCanceled(CancellationToken token, string message)
{
if (token.IsCancellationRequested)
{
throw new TaskCanceledException("Image source has changed.");
}
}
}

View File

@@ -54,6 +54,26 @@ internal static class CompositionExtensions
return brush;
}
/// <summary>
/// 创建灰阶效果画刷
/// </summary>
/// <param name="compositor">合成器</param>
/// <param name="source">源</param>
/// <returns>合成效果画刷</returns>
public static CompositionEffectBrush CompositeGrayScaleEffectBrush(this Compositor compositor, CompositionBrush source)
{
GrayscaleEffect effect = new()
{
Source = new CompositionEffectSourceParameter("Source"),
};
CompositionEffectBrush brush = compositor.CreateEffectFactory(effect).CreateBrush();
brush.SetSourceParameter("Source", source);
return brush;
}
/// <summary>
/// 创建亮度转不透明度效果画刷
/// </summary>

View File

@@ -68,14 +68,6 @@ public sealed partial class MainWindow : Window
if (!rect.Size.IsEmpty)
{
WINDOWPLACEMENT windowPlacement = WINDOWPLACEMENT.Create(new POINT(-1, -1), rect, ShowWindowCommand.Normal);
//WINDOWPLACEMENT windowPlacement = new()
//{
// Length = Marshal.SizeOf<WINDOWPLACEMENT>(),
// MaxPosition = new POINT(-1, -1),
// NormalPosition = rect,
// ShowCmd = ShowWindowCommand.Normal,
//};
User32.SetWindowPlacement(handle, ref windowPlacement);
}

View File

@@ -44,20 +44,7 @@ internal class DescParamDescriptor : IValueConverter
throw Must.NeverHappen();
}
private IList<string> GetFormattedParameters(IEnumerable<DescFormat> formats, IList<double> param)
{
List<string> results = new();
foreach (DescFormat descFormat in formats)
{
string format = descFormat.Format;
string resultFormatted = Regex.Replace(format, @"{param\d+.*?}", match => EvaluateMatch(match, param));
results.Add(resultFormatted);
}
return results;
}
private string EvaluateMatch(Match match, IList<double> param)
private static string EvaluateMatch(Match match, IList<double> param)
{
if (match.Success)
{
@@ -87,6 +74,19 @@ internal class DescParamDescriptor : IValueConverter
}
}
private IList<string> GetFormattedParameters(IEnumerable<DescFormat> formats, IList<double> param)
{
List<string> results = new();
foreach (DescFormat descFormat in formats)
{
string format = descFormat.Format;
string resultFormatted = Regex.Replace(format, @"{param\d+.*?}", match => EvaluateMatch(match, param));
results.Add(resultFormatted);
}
return results;
}
private class DescFormat
{
public DescFormat(string description, string format)

View File

@@ -25,17 +25,17 @@
<NavigationViewItem
Content="活动"
shvh:NavHelper.NavigateTo="shvp:AnnouncementPage"
Icon="{cwu:BitmapIcon Source=ms-appx:///Resource/Icon/UI_BtnIcon_ActivityEntry.png}"/>
Icon="{cwu:BitmapIcon ShowAsMonochrome=True,Source=ms-appx:///Resource/Icon/UI_BtnIcon_ActivityEntry.png}"/>
<NavigationViewItem
Content="成就"
shvh:NavHelper.NavigateTo="shvp:AchievementPage"
Icon="{cwu:BitmapIcon Source=ms-appx:///Resource/Icon/UI_Icon_Achievement.png}"/>
Icon="{cwu:BitmapIcon ShowAsMonochrome=True,Source=ms-appx:///Resource/Icon/UI_Icon_Achievement.png}"/>
<NavigationViewItem
Content="角色"
shvh:NavHelper.NavigateTo="shvp:WikiAvatarPage"
Icon="{cwu:BitmapIcon Source=ms-appx:///Resource/Icon/UI_BagTabIcon_Avatar.png}"/>
Icon="{cwu:BitmapIcon ShowAsMonochrome=True,Source=ms-appx:///Resource/Icon/UI_BagTabIcon_Avatar.png}"/>
</NavigationView.MenuItems>
<NavigationView.PaneFooter>

View File

@@ -5,7 +5,6 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:cwuc="using:CommunityToolkit.WinUI.UI.Controls"
xmlns:cwum="using:CommunityToolkit.WinUI.UI.Media"
xmlns:mxi="using:Microsoft.Xaml.Interactivity"
xmlns:shc="using:Snap.Hutao.Control"
xmlns:shcb="using:Snap.Hutao.Control.Behavior"
@@ -200,12 +199,12 @@
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<shci:CachedImage
<shci:MonoChrome
Grid.Column="0"
Width="27.2"
Height="27.2"
Source="{Binding Selected.FetterInfo.VisionBefore,Converter={StaticResource ElementNameIconConverter}}"/>
<shci:CachedImage
<shci:MonoChrome
Grid.Column="1"
Width="27.2"
Height="27.2"