diff --git a/src/Uno.Foundation/Metadata/ApiInformation.cs b/src/Uno.Foundation/Metadata/ApiInformation.cs
index a1831c6a64d5..e537a5285448 100644
--- a/src/Uno.Foundation/Metadata/ApiInformation.cs
+++ b/src/Uno.Foundation/Metadata/ApiInformation.cs
@@ -29,7 +29,10 @@ internal static void RegisterAssembly(Assembly assembly)
{
lock (_assemblies)
{
- _assemblies.Add(assembly);
+ if (!_assemblies.Contains(assembly))
+ {
+ _assemblies.Add(assembly);
+ }
}
}
diff --git a/src/Uno.UI.RuntimeTests/Tests/Windows_System/Given_Launcher.cs b/src/Uno.UI.RuntimeTests/Tests/Windows_System/Given_Launcher.cs
index 0b98b9ad58c2..5e892814dfc7 100644
--- a/src/Uno.UI.RuntimeTests/Tests/Windows_System/Given_Launcher.cs
+++ b/src/Uno.UI.RuntimeTests/Tests/Windows_System/Given_Launcher.cs
@@ -8,6 +8,7 @@
namespace Uno.UI.RuntimeTests.Tests.Windows_System
{
+
[TestClass]
public class Given_Launcher
{
diff --git a/src/Uno.UI.RuntimeTests/Tests/Windows_System/Given_MemoryManager.cs b/src/Uno.UI.RuntimeTests/Tests/Windows_System/Given_MemoryManager.cs
new file mode 100644
index 000000000000..625a34daa198
--- /dev/null
+++ b/src/Uno.UI.RuntimeTests/Tests/Windows_System/Given_MemoryManager.cs
@@ -0,0 +1,34 @@
+using System;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Windows.System;
+
+namespace Uno.UI.RuntimeTests.Tests.Windows_System
+{
+ [TestClass]
+ public class Given_MemoryManager
+ {
+ [TestMethod]
+ public void When_AppMemoryUsage()
+ {
+ EnsureApiAvailable("AppMemoryUsage");
+
+ Assert.AreNotEqual(0, MemoryManager.AppMemoryUsage);
+ }
+
+ [TestMethod]
+ public void When_AppMemoryUsageLimit()
+ {
+ EnsureApiAvailable("AppMemoryUsageLimit");
+
+ Assert.AreNotEqual(0, MemoryManager.AppMemoryUsageLimit);
+ }
+
+ private void EnsureApiAvailable(string propertyName)
+ {
+ if(!Windows.Foundation.Metadata.ApiInformation.IsPropertyPresent("Windows.System.MemoryManager", propertyName))
+ {
+ Assert.Inconclusive($"The Api {propertyName} is not implemented");
+ }
+ }
+ }
+}
diff --git a/src/Uno.UI/UI/Xaml/Application.cs b/src/Uno.UI/UI/Xaml/Application.cs
index 9b9f4a3dc42e..bbcf196dd551 100644
--- a/src/Uno.UI/UI/Xaml/Application.cs
+++ b/src/Uno.UI/UI/Xaml/Application.cs
@@ -62,6 +62,7 @@ static Application()
{
ApiInformation.RegisterAssembly(typeof(Application).Assembly);
ApiInformation.RegisterAssembly(typeof(Windows.Storage.ApplicationData).Assembly);
+ ApiInformation.RegisterAssembly(typeof(Windows.UI.Composition.Compositor).Assembly);
Uno.Helpers.DispatcherTimerProxy.SetDispatcherTimerGetter(() => new DispatcherTimer());
Uno.Helpers.VisualTreeHelperProxy.SetCloseAllFlyoutsAction(() => Media.VisualTreeHelper.CloseAllFlyouts());
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
{
diff --git a/src/Uno.UI/UI/Xaml/NativeApplication.cs b/src/Uno.UI/UI/Xaml/NativeApplication.cs
index 3fb6b022482c..0b8fb26fbdb3 100644
--- a/src/Uno.UI/UI/Xaml/NativeApplication.cs
+++ b/src/Uno.UI/UI/Xaml/NativeApplication.cs
@@ -9,6 +9,7 @@
using Windows.UI.StartScreen;
using Android.Content;
using Uno.Extensions;
+using Windows.Foundation.Metadata;
using System.ComponentModel;
using Uno.Foundation.Logging;
@@ -31,6 +32,12 @@ public class NativeApplication : Android.App.Application
public NativeApplication(AppBuilder appBuilder, IntPtr javaReference, Android.Runtime.JniHandleOwnership transfer)
: base(javaReference, transfer)
{
+ // Register assemblies earlier than Application itself, otherwise
+ // ApiInformation may return APIs as not implemented incorrectly.
+ ApiInformation.RegisterAssembly(typeof(Application).Assembly);
+ ApiInformation.RegisterAssembly(typeof(Windows.Storage.ApplicationData).Assembly);
+ ApiInformation.RegisterAssembly(typeof(Windows.UI.Composition.Compositor).Assembly);
+
// Delay create the Windows.UI.Xaml.Application in order to get the
// Android.App.Application.Context to be populated properly. This enables
// APIs such as Windows.Storage.ApplicationData.Current.LocalSettings to function properly.
diff --git a/src/Uno.UWP/System/MemoryManager.Android.cs b/src/Uno.UWP/System/MemoryManager.Android.cs
index 9deabd50bae9..053d6aecd803 100644
--- a/src/Uno.UWP/System/MemoryManager.Android.cs
+++ b/src/Uno.UWP/System/MemoryManager.Android.cs
@@ -15,9 +15,14 @@ public partial class MemoryManager
private static Debug.MemoryInfo? _mi;
private static ActivityManager.MemoryInfo? _memoryInfo;
private static global::System.Diagnostics.Stopwatch _updateWatch = global::System.Diagnostics.Stopwatch.StartNew();
- private static long _lastUpdate = long.MinValue;
+ private static TimeSpan _lastUpdate = TimeSpan.FromSeconds(-_updateInterval.TotalSeconds);
- private readonly static long _updateResolution = global::System.Diagnostics.Stopwatch.Frequency;
+ private readonly static TimeSpan _updateInterval = TimeSpan.FromSeconds(10);
+
+ static MemoryManager()
+ {
+ IsAvailable = true;
+ }
public static ulong AppMemoryUsage
{
@@ -41,8 +46,8 @@ public static ulong AppMemoryUsageLimit
private static void Update()
{
- var now = _updateWatch.ElapsedTicks;
- if (now - _lastUpdate > _updateResolution)
+ var now = _updateWatch.Elapsed;
+ if (_lastUpdate + _updateInterval < now)
{
_lastUpdate = now;