diff --git a/src/Uno.UI/FeatureConfiguration.cs b/src/Uno.UI/FeatureConfiguration.cs
index 169a189dfec9..676398d97b50 100644
--- a/src/Uno.UI/FeatureConfiguration.cs
+++ b/src/Uno.UI/FeatureConfiguration.cs
@@ -503,6 +503,18 @@ 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.
+ /// This option must be set on application startup before the cache is initialized.
+ ///
+ 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 dfb23ea01454..97c3bbb294ef 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 deal with the Android-limited GREF counts.
///
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 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();
+ 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
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..db6e806bfa6b 100644
--- a/src/Uno.UI/UI/Xaml/Controls/TextBlock/TextBlock.Android.cs
+++ b/src/Uno.UI/UI/Xaml/Controls/TextBlock/TextBlock.Android.cs
@@ -274,7 +274,14 @@ private Java.Lang.ICharSequence GetTextFormatted()
}
else if (UseInlinesFastPath)
{
- return JavaStringCache.GetNativeString(Text);
+ if (FeatureConfiguration.TextBlock.IsJavaStringCachedEnabled)
+ {
+ return JavaStringCache.GetNativeString(Text);
+ }
+ else
+ {
+ return new Java.Lang.String(Text);
+ }
}
else
{