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: limit JavaStringCache entries to deal with excessive GREF counts #18956

Merged
Merged
Changes from 1 commit
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
124 changes: 64 additions & 60 deletions src/Uno.UI/UI/Xaml/Controls/TextBlock/JavaStringCache.Android.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
/// <summary>
/// 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
/// </summary>
internal static class JavaStringCache
ramezgerges marked this conversation as resolved.
Show resolved Hide resolved
{
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 const int MaxEntryCount = 1000;
ramezgerges marked this conversation as resolved.
Show resolved Hide resolved
private static readonly Logger _log = typeof(JavaStringCache).Log();
private static readonly Stopwatch _watch = Stopwatch.StartNew();
private static readonly Dictionary<string, LinkedListNode<KeyEntry>> _table = new();
private static readonly LinkedList<KeyEntry> _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);
Expand All @@ -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();

/// <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;
}
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");
}
}
}

/// <summary>
Expand All @@ -62,64 +73,60 @@ static JavaStringCache()
/// <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 (_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<KeyEntry>(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)
{
return false;
}

var threshold = _platformProvider?.AppMemoryUsageLevel switch
var threshold = _platformProvider.AppMemoryUsageLevel switch
{
AppMemoryUsageLevel.Low => LowMemoryTrimInterval,
AppMemoryUsageLevel.Medium => MediumMemoryTrimInterval,
Expand All @@ -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);
Expand All @@ -154,26 +161,23 @@ private static void Trim(TimeSpan interval)
{
lock (_gate)
{
List<string>? 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
Expand Down
Loading