diff --git a/src/Uno.UI/UI/Xaml/Controls/TextBlock/JavaStringCache.android.cs b/src/Uno.UI/UI/Xaml/Controls/TextBlock/JavaStringCache.android.cs new file mode 100644 index 000000000000..1f219bdcf107 --- /dev/null +++ b/src/Uno.UI/UI/Xaml/Controls/TextBlock/JavaStringCache.android.cs @@ -0,0 +1,189 @@ +#nullable enable + +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 Windows.UI.Xaml.Media; +using Uno.Collections; +using Android.Security.Keystore; +using Java.Security; +using Uno.Buffers; +using Windows.System; + +namespace Windows.UI.Xaml.Controls +{ + /// + /// A TextBlock measure cache for non-formatted text. + /// + internal static class JavaStringCache + { + private static Logger _log = typeof(JavaStringCache).Log(); + private static Stopwatch _watch = Stopwatch.StartNew(); + private static HashtableEx _table = 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); + internal static readonly TimeSpan HighMemoryTrimInterval = TimeSpan.FromMinutes(1); + internal static readonly TimeSpan OverLimitMemoryTrimInterval = TimeSpan.FromMinutes(.5); + + internal static readonly TimeSpan ScavengeInterval = TimeSpan.FromMinutes(.5); + + private static 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; + } + + static JavaStringCache() + { + _automaticManagement = WinRTFeatureConfiguration.ArrayPool.EnableAutomaticMemoryManagement && _platformProvider.CanUseMemoryManager; + } + + /// + /// Gets a potentially cached native instance of a .NET string + /// + /// + /// + public static Java.Lang.String GetNativeString(string value) + { + TryInitializeMemoryManagement(); + + Scavenge(); + + lock (_gate) + { + if (_table.TryGetValue(value, out var result) && result is KeyEntry entry) + { + if (_log.IsEnabled(LogLevel.Trace)) + { + _log.Trace($"Reusing native string: [{value}]"); + } + + entry.LastUse = _watch.Elapsed; + return entry.NativeValue; + } + else + { + 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); + 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) + { + return false; + } + + var threshold = _platformProvider?.AppMemoryUsageLevel switch + { + AppMemoryUsageLevel.Low => LowMemoryTrimInterval, + AppMemoryUsageLevel.Medium => MediumMemoryTrimInterval, + AppMemoryUsageLevel.High => HighMemoryTrimInterval, + AppMemoryUsageLevel.OverLimit => OverLimitMemoryTrimInterval, + _ => LowMemoryTrimInterval + }; + + if (_log.IsEnabled(LogLevel.Trace)) + { + _log.Trace($"Memory pressure is {_platformProvider?.AppMemoryUsageLevel}, using trim interval of {threshold}"); + } + + Trim(threshold); + + return true; + } + + private static void Scavenge() + { + if (!_automaticManagement) + { + if (_lastScavenge + ScavengeInterval < _watch.Elapsed) + { + _lastScavenge = _watch.Elapsed; + Trim(LowMemoryTrimInterval); + } + } + } + + private static void Trim(TimeSpan interval) + { + lock (_gate) + { + List? entries = null; + foreach (var entry in _table.Values) + { + if (entry is KeyEntry keyEntry && keyEntry.LastUse + interval < _watch.Elapsed) + { + entries ??= new(); + entries.Add(keyEntry.Value); + } + } + + if (entries is not null) + { + if (_log.IsEnabled(LogLevel.Debug)) + { + _log.Debug($"Trimming {entries.Count} native strings unused since {interval}"); + } + + foreach (var entry in entries) + { + _table.Remove(entry); + } + } + else + { + if (_log.IsEnabled(LogLevel.Trace)) + { + _log.Trace($"Nothing to trim for the past {interval}"); + } + } + } + } + } +} 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 e47fa7b02b23..4d76ecd3c33f 100644 --- a/src/Uno.UI/UI/Xaml/Controls/TextBlock/TextBlock.Android.cs +++ b/src/Uno.UI/UI/Xaml/Controls/TextBlock/TextBlock.Android.cs @@ -263,7 +263,7 @@ private Java.Lang.ICharSequence GetTextFormatted() } else if (UseInlinesFastPath) { - return new Java.Lang.String(Text); + return JavaStringCache.GetNativeString(Text); } else {