diff --git a/src/Uno.Toolkit.Skia.WinUI/Controls/Shadows/ShadowsCache.cs b/src/Uno.Toolkit.Skia.WinUI/Controls/Shadows/ShadowsCache.cs index 0cc4ccd04..41338f058 100644 --- a/src/Uno.Toolkit.Skia.WinUI/Controls/Shadows/ShadowsCache.cs +++ b/src/Uno.Toolkit.Skia.WinUI/Controls/Shadows/ShadowsCache.cs @@ -1,16 +1,31 @@ using System; using System.Collections.Concurrent; +using System.Diagnostics; +using System.Linq; +using System.Threading.Tasks; + using Microsoft.Extensions.Logging; using SkiaSharp; using Uno.Extensions; using Uno.Logging; namespace Uno.Toolkit.UI; + public class ShadowsCache { + private const int CleanupInterval = 30; + private static readonly ILogger _logger = typeof(ShadowsCache).Log(); - private readonly ConcurrentDictionary _shadowsCache = new ConcurrentDictionary(); + private static readonly Func OneHitSinceAShortTime = + (bucket, time) => bucket.Hit == 1 && (time - bucket.LastHit).TotalMinutes > 1; + + private static readonly Func SeveralHitsSinceALongTime = + (bucket, time) => bucket.Hit > 1 && (time - bucket.LastHit).TotalMinutes > 3; + + private readonly ConcurrentDictionary _shadowsCache = new(); + + private DateTime _lastCleanupUtcTime = DateTime.UtcNow; public void AddOrUpdate(string key, SKImage image) { @@ -19,9 +34,9 @@ public void AddOrUpdate(string key, SKImage image) _logger.Trace($"[ShadowsCache] AddOrUpdate => key: {key}"); } - var bucket = _shadowsCache.AddOrUpdate( + _shadowsCache.AddOrUpdate( key, - (key) => + _ => { if (_logger.IsEnabled(LogLevel.Trace)) { @@ -29,7 +44,7 @@ public void AddOrUpdate(string key, SKImage image) } return new CacheBucket(image); }, - (key, existing) => + (_, existing) => { existing.AddHit(); if (_logger.IsEnabled(LogLevel.Trace)) @@ -38,6 +53,8 @@ public void AddOrUpdate(string key, SKImage image) } return existing; }); + + CleanupIfNeeded(); } public bool TryGetValue(string key, out SKImage? image) @@ -46,6 +63,7 @@ public bool TryGetValue(string key, out SKImage? image) { _logger.Trace($"[ShadowsCache] TryGet => key: {key}"); } + if (_shadowsCache.TryGetValue(key, out var bucket)) { bucket.AddHit(); @@ -101,4 +119,57 @@ public void AddHit() LastHit = DateTime.UtcNow; } } + + public async void CleanupIfNeeded() + { + if (!_shadowsCache.IsEmpty && (DateTime.UtcNow - _lastCleanupUtcTime).TotalSeconds > CleanupInterval) + { + try + { + await CleanupAsync().ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.Warn($"[ShadowsCache] Cleanup failed: {ex}"); + } + } + } + + /// + /// Removing old shadows based on their stats. + /// + private Task CleanupAsync() + { + _lastCleanupUtcTime = DateTime.UtcNow; + + return Task.Run(() => + { + var stopwatch = new Stopwatch(); + if (_logger.IsEnabled(LogLevel.Trace)) + { + stopwatch.Start(); + _logger.Trace($"[ShadowsCache] Cleanup starting"); + } + + DateTime utcNow = DateTime.UtcNow; + int removedShadows = 0; + foreach (var expiredBucket in _shadowsCache + .Where(x => OneHitSinceAShortTime(x.Value, utcNow) || SeveralHitsSinceALongTime(x.Value, utcNow))) + { + if (_shadowsCache.TryRemove(expiredBucket.Key, out _)) + { + removedShadows++; + } + } + + if (_logger.IsEnabled(LogLevel.Trace)) + { + stopwatch.Stop(); + if (_logger.IsEnabled(LogLevel.Trace)) + { + _logger.Trace($"[ShadowsCache] Cleanup done in {stopwatch.ElapsedMilliseconds} ms ({removedShadows} shadows removed)"); + } + } + }); + } }