Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

perf: Reuse android java String instances in TextBlock #10428

Merged
merged 4 commits into from
Nov 17, 2022
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/Uno.Foundation/Metadata/ApiInformation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@ internal static void RegisterAssembly(Assembly assembly)
{
lock (_assemblies)
{
_assemblies.Add(assembly);
if (!_assemblies.Contains(assembly))
{
_assemblies.Add(assembly);
}
jeromelaban marked this conversation as resolved.
Show resolved Hide resolved
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

namespace Uno.UI.RuntimeTests.Tests.Windows_System
{

[TestClass]
public class Given_Launcher
{
Expand Down
Original file line number Diff line number Diff line change
@@ -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");
}
}
}
}
1 change: 1 addition & 0 deletions src/Uno.UI/UI/Xaml/Application.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down
189 changes: 189 additions & 0 deletions src/Uno.UI/UI/Xaml/Controls/TextBlock/JavaStringCache.android.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
#nullable enable
jeromelaban marked this conversation as resolved.
Show resolved Hide resolved

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
{
/// <summary>
/// A TextBlock measure cache for non-formatted text.
/// </summary>
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();

/// <summary>Determines if automatic memory management is enabled</summary>
private static readonly bool _automaticManagement;
/// <summary>Determines if GC trim callback has been registerd if non-zero</summary>
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;
jeromelaban marked this conversation as resolved.
Show resolved Hide resolved
jeromelaban marked this conversation as resolved.
Show resolved Hide resolved
}

/// <summary>
/// Gets a potentially cached native instance of a .NET string
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
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<string>? 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}");
}
}
}
}
}
}
2 changes: 1 addition & 1 deletion src/Uno.UI/UI/Xaml/Controls/TextBlock/TextBlock.Android.cs
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ private Java.Lang.ICharSequence GetTextFormatted()
}
else if (UseInlinesFastPath)
{
return new Java.Lang.String(Text);
return JavaStringCache.GetNativeString(Text);
}
else
{
Expand Down
7 changes: 7 additions & 0 deletions src/Uno.UI/UI/Xaml/NativeApplication.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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.
Expand Down
13 changes: 9 additions & 4 deletions src/Uno.UWP/System/MemoryManager.Android.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
jeromelaban marked this conversation as resolved.
Show resolved Hide resolved
}

public static ulong AppMemoryUsage
{
Expand All @@ -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;

Expand Down