From deff1757509cf1faf4c80559f5869ce43916e661 Mon Sep 17 00:00:00 2001 From: Ramez Ragaa Date: Thu, 28 Nov 2024 23:40:12 +0200 Subject: [PATCH 1/3] perf: limit JavaStringCache entries to deal with excessive GREF counts --- .../TextBlock/JavaStringCache.Android.cs | 124 +++++++++--------- 1 file changed, 64 insertions(+), 60 deletions(-) diff --git a/src/Uno.UI/UI/Xaml/Controls/TextBlock/JavaStringCache.Android.cs b/src/Uno.UI/UI/Xaml/Controls/TextBlock/JavaStringCache.Android.cs index dfb23ea01454..94008676aab4 100644 --- a/src/Uno.UI/UI/Xaml/Controls/TextBlock/JavaStringCache.Android.cs +++ b/src/Uno.UI/UI/Xaml/Controls/TextBlock/JavaStringCache.Android.cs @@ -3,33 +3,33 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.Threading; -using System.Text; -using Windows.Foundation; using Uno; -using Uno.Extensions; -using Uno.UI; using Uno.Foundation.Logging; -using Microsoft.UI.Xaml.Media; -using Uno.Collections; -using Android.Security.Keystore; -using Java.Security; using Uno.Buffers; using Windows.System; namespace Microsoft.UI.Xaml.Controls { /// - /// A TextBlock measure cache for non-formatted text. + /// A cache for native java strings. This cache periodically evicts entries that haven't been used + /// in a while. Additionally, it also evicts the least recently used entries when adding new entries beyond a certain + /// capacity. Limiting the total capacity is necessary to /// internal static class JavaStringCache { - private static Logger _log = typeof(JavaStringCache).Log(); - private static Stopwatch _watch = Stopwatch.StartNew(); - private static HashtableEx _table = new(); + // Xamarin.Android uses Android global references to provide mappings between Java instances and the associated managed instances, as when invoking a Java method a Java instance needs to be provided to Java. + // Unfortunately, Android emulators only allow 2000 global references to exist at a time. Hardware has a much higher limit of 52000 global references. The lower limit can be problematic when running applications on the emulator, so knowing where the instance came from can be very useful. + // https://github.com/MicrosoftDocs/xamarin-docs/blob/live/docs/android/troubleshooting/troubleshooting.md + // https://github.com/unoplatform/uno/issues/18951 + private const int MaxEntryCount = 1000; + private static readonly Logger _log = typeof(JavaStringCache).Log(); + private static readonly Stopwatch _watch = Stopwatch.StartNew(); + private static readonly Dictionary> _table = new(); + private static readonly LinkedList _queue = new(); + private static readonly object _gate = new(); + private static TimeSpan _lastScavenge; - private static object _gate = new(); internal static readonly TimeSpan LowMemoryTrimInterval = TimeSpan.FromMinutes(5); internal static readonly TimeSpan MediumMemoryTrimInterval = TimeSpan.FromMinutes(3); @@ -38,21 +38,32 @@ internal static class JavaStringCache internal static readonly TimeSpan ScavengeInterval = TimeSpan.FromMinutes(.5); - private static DefaultArrayPoolPlatformProvider _platformProvider = new DefaultArrayPoolPlatformProvider(); + private static readonly DefaultArrayPoolPlatformProvider _platformProvider = new DefaultArrayPoolPlatformProvider(); /// Determines if automatic memory management is enabled private static readonly bool _automaticManagement; - /// Determines if GC trim callback has been registerd if non-zero - private static int _trimCallbackCreated; - private record KeyEntry(string Value, Java.Lang.String NativeValue) - { - public TimeSpan LastUse { get; set; } = _watch.Elapsed; - } + private readonly record struct KeyEntry(string CsString, Java.Lang.String JavaString, TimeSpan LastUse); static JavaStringCache() { _automaticManagement = WinRTFeatureConfiguration.ArrayPool.EnableAutomaticMemoryManagement && _platformProvider.CanUseMemoryManager; + if (_automaticManagement) + { + if (_log.IsEnabled(LogLevel.Debug)) + { + _log.Debug($"Using automatic memory management"); + } + + _platformProvider.RegisterTrimCallback(_ => Trim(), _gate); + } + else + { + if (_log.IsEnabled(LogLevel.Debug)) + { + _log.Debug($"Using manual memory management"); + } + } } /// @@ -62,56 +73,52 @@ static JavaStringCache() /// public static Java.Lang.String GetNativeString(string value) { - TryInitializeMemoryManagement(); - Scavenge(); lock (_gate) { - if (_table.TryGetValue(value, out var result) && result is KeyEntry entry) + if (_table.TryGetValue(value, out var result)) { if (_log.IsEnabled(LogLevel.Trace)) { _log.Trace($"Reusing native string: [{value}]"); } - entry.LastUse = _watch.Elapsed; - return entry.NativeValue; + var entry = result.Value; + result.Value = entry with { LastUse = _watch.Elapsed }; + _queue.Remove(result); + _queue.AddFirst(result); + + return entry.JavaString; } else { + if (_queue.Count == MaxEntryCount) + { + var last = _queue.Last!.Value.CsString; + _table.Remove(last); + _queue.RemoveLast(); + + if (_log.IsEnabled(LogLevel.Trace)) + { + _log.Trace($"{nameof(JavaStringCache)} is full. Evicting [{last}]"); + } + } + if (_log.IsEnabled(LogLevel.Trace)) { _log.Trace($"Creating native string for [{value}]"); } var javaString = new Java.Lang.String(value); - _table[value] = new KeyEntry(value, javaString); + var node = new LinkedListNode(new KeyEntry(value, javaString, _watch.Elapsed)); + _queue.AddFirst(node); + _table[value] = node; return javaString; } } } - private static void TryInitializeMemoryManagement() - { - if (_automaticManagement && Interlocked.Exchange(ref _trimCallbackCreated, 1) == 0) - { - if (_log.IsEnabled(LogLevel.Debug)) - { - _log.Debug($"Using automatic memory management"); - } - - _platformProvider.RegisterTrimCallback(_ => Trim(), _gate); - } - else - { - if (_log.IsEnabled(LogLevel.Debug)) - { - _log.Debug($"Using manual memory management"); - } - } - } - private static bool Trim() { if (!_automaticManagement) @@ -119,7 +126,7 @@ private static bool Trim() return false; } - var threshold = _platformProvider?.AppMemoryUsageLevel switch + var threshold = _platformProvider.AppMemoryUsageLevel switch { AppMemoryUsageLevel.Low => LowMemoryTrimInterval, AppMemoryUsageLevel.Medium => MediumMemoryTrimInterval, @@ -130,7 +137,7 @@ private static bool Trim() if (_log.IsEnabled(LogLevel.Trace)) { - _log.Trace($"Memory pressure is {_platformProvider?.AppMemoryUsageLevel}, using trim interval of {threshold}"); + _log.Trace($"Memory pressure is {_platformProvider.AppMemoryUsageLevel}, using trim interval of {threshold}"); } Trim(threshold); @@ -154,26 +161,23 @@ private static void Trim(TimeSpan interval) { lock (_gate) { - List? entries = null; + int trimmedCount = 0; foreach (var entry in _table.Values) { - if (entry is KeyEntry keyEntry && keyEntry.LastUse + interval < _watch.Elapsed) + var node = entry.Value; + if (node.LastUse + interval < _watch.Elapsed) { - entries ??= new(); - entries.Add(keyEntry.Value); + _table.Remove(node.CsString); + _queue.Remove(node); + trimmedCount++; } } - if (entries is not null) + if (trimmedCount > 0) { if (_log.IsEnabled(LogLevel.Debug)) { - _log.Debug($"Trimming {entries.Count} native strings unused since {interval}"); - } - - foreach (var entry in entries) - { - _table.Remove(entry); + _log.Debug($"Trimming {trimmedCount} native strings unused since {interval}"); } } else From 3fc4fec90562c87622675b0518a465f788021716 Mon Sep 17 00:00:00 2001 From: Ramez Ragaa Date: Fri, 29 Nov 2024 15:51:44 +0200 Subject: [PATCH 2/3] chore: add TextBlock FeatureConfiguration options --- src/Uno.UI/FeatureConfiguration.cs | 11 +++++++++++ .../Controls/TextBlock/JavaStringCache.Android.cs | 4 ++-- .../UI/Xaml/Controls/TextBlock/TextBlock.Android.cs | 2 +- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/Uno.UI/FeatureConfiguration.cs b/src/Uno.UI/FeatureConfiguration.cs index 169a189dfec9..1e09357e6c96 100644 --- a/src/Uno.UI/FeatureConfiguration.cs +++ b/src/Uno.UI/FeatureConfiguration.cs @@ -503,6 +503,17 @@ public static class TextBlock /// [WebAssembly Only] Determines if the measure cache is enabled. /// public static bool IsMeasureCacheEnabled { get; set; } = true; + + /// + /// [Android Only] Determines if the Java string-cache is enabled. + /// + public static bool IsJavaStringCachedEnabled { get; set; } = true; + + /// + /// [Android Only] Determines the maximum capacity of the Java string-cache. + /// This option must be set on application startup before the cache is initialized. + /// + public static int JavaStringCachedCapacity { get; set; } = 1000; } public static class TextBox diff --git a/src/Uno.UI/UI/Xaml/Controls/TextBlock/JavaStringCache.Android.cs b/src/Uno.UI/UI/Xaml/Controls/TextBlock/JavaStringCache.Android.cs index 94008676aab4..f2539eebb5d5 100644 --- a/src/Uno.UI/UI/Xaml/Controls/TextBlock/JavaStringCache.Android.cs +++ b/src/Uno.UI/UI/Xaml/Controls/TextBlock/JavaStringCache.Android.cs @@ -14,7 +14,7 @@ namespace Microsoft.UI.Xaml.Controls /// /// A cache for native java strings. This cache periodically evicts entries that haven't been used /// in a while. Additionally, it also evicts the least recently used entries when adding new entries beyond a certain - /// capacity. Limiting the total capacity is necessary to + /// capacity. Limiting the total capacity is necessary to deal with the Android-limited GREF counts. /// internal static class JavaStringCache { @@ -22,7 +22,7 @@ internal static class JavaStringCache // Unfortunately, Android emulators only allow 2000 global references to exist at a time. Hardware has a much higher limit of 52000 global references. The lower limit can be problematic when running applications on the emulator, so knowing where the instance came from can be very useful. // https://github.com/MicrosoftDocs/xamarin-docs/blob/live/docs/android/troubleshooting/troubleshooting.md // https://github.com/unoplatform/uno/issues/18951 - private const int MaxEntryCount = 1000; + private static readonly int MaxEntryCount = Uno.UI.FeatureConfiguration.JavaStringCachedCapacity; private static readonly Logger _log = typeof(JavaStringCache).Log(); private static readonly Stopwatch _watch = Stopwatch.StartNew(); private static readonly Dictionary> _table = new(); diff --git a/src/Uno.UI/UI/Xaml/Controls/TextBlock/TextBlock.Android.cs b/src/Uno.UI/UI/Xaml/Controls/TextBlock/TextBlock.Android.cs index 1946204c78a0..2f8a7d4954f2 100644 --- a/src/Uno.UI/UI/Xaml/Controls/TextBlock/TextBlock.Android.cs +++ b/src/Uno.UI/UI/Xaml/Controls/TextBlock/TextBlock.Android.cs @@ -272,7 +272,7 @@ private Java.Lang.ICharSequence GetTextFormatted() { return EmptyString; } - else if (UseInlinesFastPath) + else if (UseInlinesFastPath && FeatureConfiguration.IsJavaStringCachedEnabled) { return JavaStringCache.GetNativeString(Text); } From cce9701855fed8320f55aec96ac37981afdf9ac5 Mon Sep 17 00:00:00 2001 From: Ramez Ragaa Date: Fri, 29 Nov 2024 16:53:52 +0200 Subject: [PATCH 3/3] chore: keep using fast path even when !IsJavaStringCachedEnabled --- src/Uno.UI/FeatureConfiguration.cs | 1 + .../Controls/TextBlock/JavaStringCache.Android.cs | 4 ++-- .../UI/Xaml/Controls/TextBlock/TextBlock.Android.cs | 11 +++++++++-- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/Uno.UI/FeatureConfiguration.cs b/src/Uno.UI/FeatureConfiguration.cs index 1e09357e6c96..676398d97b50 100644 --- a/src/Uno.UI/FeatureConfiguration.cs +++ b/src/Uno.UI/FeatureConfiguration.cs @@ -506,6 +506,7 @@ public static class TextBlock /// /// [Android Only] Determines if the Java string-cache is enabled. + /// This option must be set on application startup before the cache is initialized. /// public static bool IsJavaStringCachedEnabled { get; set; } = true; diff --git a/src/Uno.UI/UI/Xaml/Controls/TextBlock/JavaStringCache.Android.cs b/src/Uno.UI/UI/Xaml/Controls/TextBlock/JavaStringCache.Android.cs index f2539eebb5d5..97c3bbb294ef 100644 --- a/src/Uno.UI/UI/Xaml/Controls/TextBlock/JavaStringCache.Android.cs +++ b/src/Uno.UI/UI/Xaml/Controls/TextBlock/JavaStringCache.Android.cs @@ -22,7 +22,7 @@ internal static class JavaStringCache // Unfortunately, Android emulators only allow 2000 global references to exist at a time. Hardware has a much higher limit of 52000 global references. The lower limit can be problematic when running applications on the emulator, so knowing where the instance came from can be very useful. // https://github.com/MicrosoftDocs/xamarin-docs/blob/live/docs/android/troubleshooting/troubleshooting.md // https://github.com/unoplatform/uno/issues/18951 - private static readonly int MaxEntryCount = Uno.UI.FeatureConfiguration.JavaStringCachedCapacity; + private static readonly int _maxEntryCount = Uno.UI.FeatureConfiguration.TextBlock.JavaStringCachedCapacity; private static readonly Logger _log = typeof(JavaStringCache).Log(); private static readonly Stopwatch _watch = Stopwatch.StartNew(); private static readonly Dictionary> _table = new(); @@ -93,7 +93,7 @@ public static Java.Lang.String GetNativeString(string value) } else { - if (_queue.Count == MaxEntryCount) + if (_queue.Count == _maxEntryCount) { var last = _queue.Last!.Value.CsString; _table.Remove(last); diff --git a/src/Uno.UI/UI/Xaml/Controls/TextBlock/TextBlock.Android.cs b/src/Uno.UI/UI/Xaml/Controls/TextBlock/TextBlock.Android.cs index 2f8a7d4954f2..db6e806bfa6b 100644 --- a/src/Uno.UI/UI/Xaml/Controls/TextBlock/TextBlock.Android.cs +++ b/src/Uno.UI/UI/Xaml/Controls/TextBlock/TextBlock.Android.cs @@ -272,9 +272,16 @@ private Java.Lang.ICharSequence GetTextFormatted() { return EmptyString; } - else if (UseInlinesFastPath && FeatureConfiguration.IsJavaStringCachedEnabled) + else if (UseInlinesFastPath) { - return JavaStringCache.GetNativeString(Text); + if (FeatureConfiguration.TextBlock.IsJavaStringCachedEnabled) + { + return JavaStringCache.GetNativeString(Text); + } + else + { + return new Java.Lang.String(Text); + } } else {