From cd12eef0c026346fb8becf052212c591718da7bb Mon Sep 17 00:00:00 2001 From: Michael Rumpler Date: Tue, 8 Oct 2024 14:10:43 +0200 Subject: [PATCH 1/3] add caching to DrawShapedText --- binding/Binding.Shared/HashCode.cs | 181 ++++++++++++++++++ .../SkiaSharp.HarfBuzz/CanvasExtensions.cs | 80 +++++++- tests/Tests/SkiaSharp/CanvasExtensionTest.cs | 149 ++++++++++++++ 3 files changed, 407 insertions(+), 3 deletions(-) create mode 100644 tests/Tests/SkiaSharp/CanvasExtensionTest.cs diff --git a/binding/Binding.Shared/HashCode.cs b/binding/Binding.Shared/HashCode.cs index 4fe4c85c12..50ef82cf31 100644 --- a/binding/Binding.Shared/HashCode.cs +++ b/binding/Binding.Shared/HashCode.cs @@ -145,5 +145,186 @@ public int ToHashCode () hash = MixFinal (hash); return (int)hash; } + + public static int Combine (T1 value1) + { + // Provide a way of diffusing bits from something with a limited + // input hash space. For example, many enums only have a few + // possible hashes, only using the bottom few bits of the code. Some + // collections are built on the assumption that hashes are spread + // over a larger space, so diffusing the bits may help the + // collection work more efficiently. + + uint hc1 = (uint)(value1?.GetHashCode () ?? 0); + + uint hash = MixEmptyState (); + hash += 4; + + hash = QueueRound (hash, hc1); + + hash = MixFinal (hash); + return (int)hash; + } + + public static int Combine (T1 value1, T2 value2) + { + uint hc1 = (uint)(value1?.GetHashCode () ?? 0); + uint hc2 = (uint)(value2?.GetHashCode () ?? 0); + + uint hash = MixEmptyState (); + hash += 8; + + hash = QueueRound (hash, hc1); + hash = QueueRound (hash, hc2); + + hash = MixFinal (hash); + return (int)hash; + } + + public static int Combine (T1 value1, T2 value2, T3 value3) + { + uint hc1 = (uint)(value1?.GetHashCode () ?? 0); + uint hc2 = (uint)(value2?.GetHashCode () ?? 0); + uint hc3 = (uint)(value3?.GetHashCode () ?? 0); + + uint hash = MixEmptyState (); + hash += 12; + + hash = QueueRound (hash, hc1); + hash = QueueRound (hash, hc2); + hash = QueueRound (hash, hc3); + + hash = MixFinal (hash); + return (int)hash; + } + + public static int Combine (T1 value1, T2 value2, T3 value3, T4 value4) + { + uint hc1 = (uint)(value1?.GetHashCode () ?? 0); + uint hc2 = (uint)(value2?.GetHashCode () ?? 0); + uint hc3 = (uint)(value3?.GetHashCode () ?? 0); + uint hc4 = (uint)(value4?.GetHashCode () ?? 0); + + Initialize (out uint v1, out uint v2, out uint v3, out uint v4); + + v1 = Round (v1, hc1); + v2 = Round (v2, hc2); + v3 = Round (v3, hc3); + v4 = Round (v4, hc4); + + uint hash = MixState (v1, v2, v3, v4); + hash += 16; + + hash = MixFinal (hash); + return (int)hash; + } + + public static int Combine (T1 value1, T2 value2, T3 value3, T4 value4, T5 value5) + { + uint hc1 = (uint)(value1?.GetHashCode () ?? 0); + uint hc2 = (uint)(value2?.GetHashCode () ?? 0); + uint hc3 = (uint)(value3?.GetHashCode () ?? 0); + uint hc4 = (uint)(value4?.GetHashCode () ?? 0); + uint hc5 = (uint)(value5?.GetHashCode () ?? 0); + + Initialize (out uint v1, out uint v2, out uint v3, out uint v4); + + v1 = Round (v1, hc1); + v2 = Round (v2, hc2); + v3 = Round (v3, hc3); + v4 = Round (v4, hc4); + + uint hash = MixState (v1, v2, v3, v4); + hash += 20; + + hash = QueueRound (hash, hc5); + + hash = MixFinal (hash); + return (int)hash; + } + + public static int Combine (T1 value1, T2 value2, T3 value3, T4 value4, T5 value5, T6 value6) + { + uint hc1 = (uint)(value1?.GetHashCode () ?? 0); + uint hc2 = (uint)(value2?.GetHashCode () ?? 0); + uint hc3 = (uint)(value3?.GetHashCode () ?? 0); + uint hc4 = (uint)(value4?.GetHashCode () ?? 0); + uint hc5 = (uint)(value5?.GetHashCode () ?? 0); + uint hc6 = (uint)(value6?.GetHashCode () ?? 0); + + Initialize (out uint v1, out uint v2, out uint v3, out uint v4); + + v1 = Round (v1, hc1); + v2 = Round (v2, hc2); + v3 = Round (v3, hc3); + v4 = Round (v4, hc4); + + uint hash = MixState (v1, v2, v3, v4); + hash += 24; + + hash = QueueRound (hash, hc5); + hash = QueueRound (hash, hc6); + + hash = MixFinal (hash); + return (int)hash; + } + + public static int Combine (T1 value1, T2 value2, T3 value3, T4 value4, T5 value5, T6 value6, T7 value7) + { + uint hc1 = (uint)(value1?.GetHashCode () ?? 0); + uint hc2 = (uint)(value2?.GetHashCode () ?? 0); + uint hc3 = (uint)(value3?.GetHashCode () ?? 0); + uint hc4 = (uint)(value4?.GetHashCode () ?? 0); + uint hc5 = (uint)(value5?.GetHashCode () ?? 0); + uint hc6 = (uint)(value6?.GetHashCode () ?? 0); + uint hc7 = (uint)(value7?.GetHashCode () ?? 0); + + Initialize (out uint v1, out uint v2, out uint v3, out uint v4); + + v1 = Round (v1, hc1); + v2 = Round (v2, hc2); + v3 = Round (v3, hc3); + v4 = Round (v4, hc4); + + uint hash = MixState (v1, v2, v3, v4); + hash += 28; + + hash = QueueRound (hash, hc5); + hash = QueueRound (hash, hc6); + hash = QueueRound (hash, hc7); + + hash = MixFinal (hash); + return (int)hash; + } + + public static int Combine (T1 value1, T2 value2, T3 value3, T4 value4, T5 value5, T6 value6, T7 value7, T8 value8) + { + uint hc1 = (uint)(value1?.GetHashCode () ?? 0); + uint hc2 = (uint)(value2?.GetHashCode () ?? 0); + uint hc3 = (uint)(value3?.GetHashCode () ?? 0); + uint hc4 = (uint)(value4?.GetHashCode () ?? 0); + uint hc5 = (uint)(value5?.GetHashCode () ?? 0); + uint hc6 = (uint)(value6?.GetHashCode () ?? 0); + uint hc7 = (uint)(value7?.GetHashCode () ?? 0); + uint hc8 = (uint)(value8?.GetHashCode () ?? 0); + + Initialize (out uint v1, out uint v2, out uint v3, out uint v4); + + v1 = Round (v1, hc1); + v2 = Round (v2, hc2); + v3 = Round (v3, hc3); + v4 = Round (v4, hc4); + + v1 = Round (v1, hc5); + v2 = Round (v2, hc6); + v3 = Round (v3, hc7); + v4 = Round (v4, hc8); + + uint hash = MixState (v1, v2, v3, v4); + hash += 32; + + hash = MixFinal (hash); + return (int)hash; + } } } diff --git a/source/SkiaSharp.HarfBuzz/SkiaSharp.HarfBuzz/CanvasExtensions.cs b/source/SkiaSharp.HarfBuzz/SkiaSharp.HarfBuzz/CanvasExtensions.cs index 8b39ae75d9..6f19eabb97 100644 --- a/source/SkiaSharp.HarfBuzz/SkiaSharp.HarfBuzz/CanvasExtensions.cs +++ b/source/SkiaSharp.HarfBuzz/SkiaSharp.HarfBuzz/CanvasExtensions.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Concurrent; +using System.Threading; namespace SkiaSharp.HarfBuzz { @@ -30,8 +32,10 @@ public static void DrawShapedText(this SKCanvas canvas, string text, float x, fl if (string.IsNullOrEmpty(text)) return; - using var shaper = new SKShaper(font.Typeface); + var shaper = GetShaper(font.Typeface); canvas.DrawShapedText(shaper, text, x, y, textAlign, font, paint); + if (cacheDuration == 0) + shaper.Dispose(); } [Obsolete("Use DrawShapedText(SKShaper shaper, string text, SKPoint p, SKTextAlign textAlign, SKFont font, SKPaint paint) instead.")] @@ -70,7 +74,7 @@ public static void DrawShapedText(this SKCanvas canvas, SKShaper shaper, string font.Typeface = shaper.Typeface; // shape the text - var result = shaper.Shape(text, x, y, font); + var result = GetShapeResult(shaper, text, font); // create the text blob using var builder = new SKTextBlobBuilder(); @@ -99,7 +103,77 @@ public static void DrawShapedText(this SKCanvas canvas, SKShaper shaper, string } // draw the text - canvas.DrawText(textBlob, xOffset, 0, paint); + canvas.DrawText(textBlob, x + xOffset, y, paint); + + if (clearCacheTimer is null && cacheDuration > 0) + clearCacheTimer = new Timer(_ => ClearCache(), null, 0, cacheDuration); + } + + private static uint cacheDuration = 0; + + public static void SetShaperCacheDuration(this SKCanvas canvas, uint milliseconds) => SetShaperCacheDuration(milliseconds); + public static void SetShaperCacheDuration(uint milliseconds) + { + cacheDuration = milliseconds; + + clearCacheTimer?.Change(0, cacheDuration); + } + + private static readonly ConcurrentDictionary shaperCache = new(); + private static readonly ConcurrentDictionary shapeResultCache = new(); + + private static SKShaper GetShaper(SKTypeface typeface) + { + var key = HashCode.Combine(typeface.FamilyName, typeface.IsBold, typeface.IsItalic); + + var shaper = shaperCache.TryGetValue(key, out var value) + ? value.shaper + : new SKShaper(typeface); + + if (cacheDuration > 0) + shaperCache[key] = (shaper, DateTime.Now); // update timestamp + return shaper; + } + + private static SKShaper.Result GetShapeResult(SKShaper shaper, string text, SKFont font) + { + var key = HashCode.Combine(font.Typeface.FamilyName, font.Size, font.Typeface.IsBold, font.Typeface.IsItalic, text); + + var result = shapeResultCache.TryGetValue(key, out var value) + ? value.shapeResult + : shaper.Shape(text, 0, 0, font); + + if (cacheDuration > 0) + shapeResultCache[key] = (result, DateTime.Now); // update timestamp + return result; + } + + private static Timer clearCacheTimer = null; + + private static void ClearCache() + { + var outdated = DateTime.Now - TimeSpan.FromMilliseconds(cacheDuration); + + foreach (var kv in shaperCache.ToArray()) + { + if (kv.Value.cachedAt < outdated) + { + if (shaperCache.TryRemove(kv.Key, out var entry)) + entry.shaper.Dispose(); + } + } + + foreach (var kv in shapeResultCache.ToArray()) + { + if (kv.Value.cachedAt < outdated) + shapeResultCache.TryRemove(kv.Key, out var _); + } + + if ((shaperCache.IsEmpty && shapeResultCache.IsEmpty) || cacheDuration == 0) + { + clearCacheTimer?.Dispose(); + clearCacheTimer = null; + } } } } diff --git a/tests/Tests/SkiaSharp/CanvasExtensionTest.cs b/tests/Tests/SkiaSharp/CanvasExtensionTest.cs new file mode 100644 index 0000000000..f6f374317f --- /dev/null +++ b/tests/Tests/SkiaSharp/CanvasExtensionTest.cs @@ -0,0 +1,149 @@ +using System.Collections.Concurrent; +using System; +using System.IO; +using System.Reflection; +using System.Threading.Tasks; +using SkiaSharp.Tests; +using Xunit; + +namespace SkiaSharp.HarfBuzz.Tests +{ + public class CanvasExtensionTest : SKTest + { + private SKCanvas canvas; + private SKFont font; + private SKPaint paint; + + #region Access private fields + + FieldInfo? shaperCacheField = null; + public ConcurrentDictionary ShaperCache + { + get + { + if (shaperCacheField is null) + shaperCacheField = typeof(CanvasExtensions).GetField("shaperCache", BindingFlags.NonPublic | BindingFlags.Static); + + return (ConcurrentDictionary)shaperCacheField!.GetValue(null)!; + } + } + + FieldInfo? shapeResultCacheField = null; + public ConcurrentDictionary ShapeResultCache + { + get + { + if (shapeResultCacheField is null) + shapeResultCacheField = typeof(CanvasExtensions).GetField("shapeResultCache", BindingFlags.NonPublic | BindingFlags.Static); + + return (ConcurrentDictionary)shapeResultCacheField!.GetValue(null)!; + } + } + + #endregion Access private fields + + + public CanvasExtensionTest() + { + canvas = new SKCanvas(new SKBitmap(320, 200)); + font = SKTypeface.FromFile(Path.Combine(PathToFonts, "content-font.ttf")).ToFont(20); + paint = new SKPaint() { Color = SKColors.LimeGreen, IsAntialias = true, IsStroke = false }; + } + + [SkippableFact] + public void CacheGetsFilled() + { + canvas.SetShaperCacheDuration(0); + canvas.SetShaperCacheDuration(30_000); + + Assert.Empty(ShaperCache); + Assert.Empty(ShapeResultCache); + + canvas.DrawShapedText("Hello world!", 0, 0, font, paint); + + Assert.Single(ShaperCache); + Assert.Single(ShapeResultCache); + + canvas.SetShaperCacheDuration(0); + } + + [SkippableFact] + public async Task CacheGetsClearedWhenSettingDurationToZero() + { + canvas.SetShaperCacheDuration(0); + canvas.SetShaperCacheDuration(30_000); + + canvas.DrawShapedText("Hello world!", 0, 0, font, paint); + + Assert.Single(ShaperCache); + Assert.Single(ShapeResultCache); + + canvas.SetShaperCacheDuration(0); + await Task.Delay(10); // give Timer a chance to run + + Assert.Empty(ShaperCache); + Assert.Empty(ShapeResultCache); + } + + [SkippableFact] + public async Task CacheGetsClearedAutomatically() + { + canvas.SetShaperCacheDuration(0); + canvas.SetShaperCacheDuration(30); + + canvas.DrawShapedText("Hello world!", 0, 0, font, paint); + + Assert.Single(ShaperCache); + Assert.Single(ShapeResultCache); + + await Task.Delay(50); + + Assert.Empty(ShaperCache); + Assert.Empty(ShapeResultCache); + + canvas.SetShaperCacheDuration(0); + } + + [SkippableFact] + public void TwoStringsGetBothCached() + { + canvas.SetShaperCacheDuration(0); + canvas.SetShaperCacheDuration(30_000); + + Assert.Empty(ShaperCache); + Assert.Empty(ShapeResultCache); + + canvas.DrawShapedText("Hello", 0, 0, font, paint); + canvas.DrawShapedText("world!", 100, 100, font, paint); + + Assert.Single(ShaperCache); + Assert.Equal(2, ShapeResultCache.Count); + + canvas.SetShaperCacheDuration(0); + } + + [SkippableFact] + public void ShaperGetsCachedForDifferentFonts() + { + var font2 = SKTypeface.FromFile(Path.Combine(PathToFonts, "segoeui.ttf")).ToFont(20); + + canvas.SetShaperCacheDuration(0); + canvas.SetShaperCacheDuration(30_000); + + Assert.Empty(ShaperCache); + Assert.Empty(ShapeResultCache); + + canvas.DrawShapedText("Hello", 0, 0, font, paint); + + Assert.Single(ShaperCache); + Assert.Single(ShapeResultCache); + + canvas.DrawShapedText("world!", 100, 0, font2, paint); + + Assert.Equal(2, ShaperCache.Count); + Assert.Equal(2, ShapeResultCache.Count); + + canvas.SetShaperCacheDuration(0); + } + } +} From e74a183acc759b3d7ac8776480d63487c6372a59 Mon Sep 17 00:00:00 2001 From: Michael Rumpler Date: Wed, 6 Nov 2024 15:30:43 +0100 Subject: [PATCH 2/3] use a Dictionary instead of a ConcurrentDictionary for the cache --- .../SkiaSharp.HarfBuzz/CanvasExtensions.cs | 47 ++++++++++++------- tests/Tests/SkiaSharp/CanvasExtensionTest.cs | 16 +++---- 2 files changed, 38 insertions(+), 25 deletions(-) diff --git a/source/SkiaSharp.HarfBuzz/SkiaSharp.HarfBuzz/CanvasExtensions.cs b/source/SkiaSharp.HarfBuzz/SkiaSharp.HarfBuzz/CanvasExtensions.cs index 6f19eabb97..b35dca991a 100644 --- a/source/SkiaSharp.HarfBuzz/SkiaSharp.HarfBuzz/CanvasExtensions.cs +++ b/source/SkiaSharp.HarfBuzz/SkiaSharp.HarfBuzz/CanvasExtensions.cs @@ -1,5 +1,6 @@ using System; -using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; using System.Threading; namespace SkiaSharp.HarfBuzz @@ -119,32 +120,44 @@ public static void SetShaperCacheDuration(uint milliseconds) clearCacheTimer?.Change(0, cacheDuration); } - private static readonly ConcurrentDictionary shaperCache = new(); - private static readonly ConcurrentDictionary shapeResultCache = new(); + private static readonly Dictionary shaperCache = new(); + private static readonly Dictionary shapeResultCache = new(); private static SKShaper GetShaper(SKTypeface typeface) { + if (cacheDuration == 0) + return new SKShaper(typeface); + var key = HashCode.Combine(typeface.FamilyName, typeface.IsBold, typeface.IsItalic); - var shaper = shaperCache.TryGetValue(key, out var value) - ? value.shaper - : new SKShaper(typeface); + SKShaper shaper; + lock (shaperCache) + { + shaper = shaperCache.TryGetValue(key, out var value) + ? value.shaper + : new SKShaper(typeface); - if (cacheDuration > 0) shaperCache[key] = (shaper, DateTime.Now); // update timestamp + } return shaper; } private static SKShaper.Result GetShapeResult(SKShaper shaper, string text, SKFont font) { + if (cacheDuration == 0) + return shaper.Shape(text, 0, 0, font); + var key = HashCode.Combine(font.Typeface.FamilyName, font.Size, font.Typeface.IsBold, font.Typeface.IsItalic, text); - var result = shapeResultCache.TryGetValue(key, out var value) - ? value.shapeResult - : shaper.Shape(text, 0, 0, font); + SKShaper.Result result; + lock (shapeResultCache) + { + result = shapeResultCache.TryGetValue(key, out var value) + ? value.shapeResult + : shaper.Shape(text, 0, 0, font); - if (cacheDuration > 0) shapeResultCache[key] = (result, DateTime.Now); // update timestamp + } return result; } @@ -154,22 +167,22 @@ private static void ClearCache() { var outdated = DateTime.Now - TimeSpan.FromMilliseconds(cacheDuration); - foreach (var kv in shaperCache.ToArray()) + foreach (var kv in shaperCache.AsEnumerable()) { if (kv.Value.cachedAt < outdated) { - if (shaperCache.TryRemove(kv.Key, out var entry)) - entry.shaper.Dispose(); + if (shaperCache.Remove(kv.Key)) + kv.Value.shaper.Dispose(); } } - foreach (var kv in shapeResultCache.ToArray()) + foreach (var kv in shapeResultCache.AsEnumerable()) { if (kv.Value.cachedAt < outdated) - shapeResultCache.TryRemove(kv.Key, out var _); + shapeResultCache.Remove(kv.Key); } - if ((shaperCache.IsEmpty && shapeResultCache.IsEmpty) || cacheDuration == 0) + if ((shaperCache.Count == 0 && shapeResultCache.Count == 0) || cacheDuration == 0) { clearCacheTimer?.Dispose(); clearCacheTimer = null; diff --git a/tests/Tests/SkiaSharp/CanvasExtensionTest.cs b/tests/Tests/SkiaSharp/CanvasExtensionTest.cs index f6f374317f..adab4e3d03 100644 --- a/tests/Tests/SkiaSharp/CanvasExtensionTest.cs +++ b/tests/Tests/SkiaSharp/CanvasExtensionTest.cs @@ -1,5 +1,5 @@ -using System.Collections.Concurrent; -using System; +using System; +using System.Collections.Generic; using System.IO; using System.Reflection; using System.Threading.Tasks; @@ -16,27 +16,27 @@ public class CanvasExtensionTest : SKTest #region Access private fields - FieldInfo? shaperCacheField = null; - public ConcurrentDictionary ShaperCache + FieldInfo shaperCacheField = null; + public Dictionary ShaperCache { get { if (shaperCacheField is null) shaperCacheField = typeof(CanvasExtensions).GetField("shaperCache", BindingFlags.NonPublic | BindingFlags.Static); - return (ConcurrentDictionary)shaperCacheField!.GetValue(null)!; + return (Dictionary)shaperCacheField!.GetValue(null)!; } } - FieldInfo? shapeResultCacheField = null; - public ConcurrentDictionary ShapeResultCache + FieldInfo shapeResultCacheField = null; + public Dictionary ShapeResultCache { get { if (shapeResultCacheField is null) shapeResultCacheField = typeof(CanvasExtensions).GetField("shapeResultCache", BindingFlags.NonPublic | BindingFlags.Static); - return (ConcurrentDictionary)shapeResultCacheField!.GetValue(null)!; + return (Dictionary)shapeResultCacheField!.GetValue(null)!; } } From 7842f572ed6ddd71e632ab6c55b287b0355b20d9 Mon Sep 17 00:00:00 2001 From: Michael Rumpler Date: Mon, 11 Nov 2024 16:51:17 +0100 Subject: [PATCH 3/3] add the missing lock in ClearCache() --- .../SkiaSharp.HarfBuzz/CanvasExtensions.cs | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/source/SkiaSharp.HarfBuzz/SkiaSharp.HarfBuzz/CanvasExtensions.cs b/source/SkiaSharp.HarfBuzz/SkiaSharp.HarfBuzz/CanvasExtensions.cs index b35dca991a..2a9d193440 100644 --- a/source/SkiaSharp.HarfBuzz/SkiaSharp.HarfBuzz/CanvasExtensions.cs +++ b/source/SkiaSharp.HarfBuzz/SkiaSharp.HarfBuzz/CanvasExtensions.cs @@ -167,19 +167,25 @@ private static void ClearCache() { var outdated = DateTime.Now - TimeSpan.FromMilliseconds(cacheDuration); - foreach (var kv in shaperCache.AsEnumerable()) + lock (shaperCache) { - if (kv.Value.cachedAt < outdated) + foreach (var kv in shaperCache.AsEnumerable()) { - if (shaperCache.Remove(kv.Key)) - kv.Value.shaper.Dispose(); + if (kv.Value.cachedAt < outdated) + { + if (shaperCache.Remove(kv.Key)) + kv.Value.shaper.Dispose(); + } } } - foreach (var kv in shapeResultCache.AsEnumerable()) + lock (shapeResultCache) { - if (kv.Value.cachedAt < outdated) - shapeResultCache.Remove(kv.Key); + foreach (var kv in shapeResultCache.AsEnumerable()) + { + if (kv.Value.cachedAt < outdated) + shapeResultCache.Remove(kv.Key); + } } if ((shaperCache.Count == 0 && shapeResultCache.Count == 0) || cacheDuration == 0)