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;