Skip to content

Commit deff175

Browse files
committed
perf: limit JavaStringCache entries to deal with excessive GREF counts
1 parent facfb7f commit deff175

File tree

1 file changed

+64
-60
lines changed

1 file changed

+64
-60
lines changed

src/Uno.UI/UI/Xaml/Controls/TextBlock/JavaStringCache.Android.cs

Lines changed: 64 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -3,33 +3,33 @@
33
using System;
44
using System.Collections.Generic;
55
using System.Diagnostics;
6-
using System.Threading;
7-
using System.Text;
8-
using Windows.Foundation;
96

107
using Uno;
11-
using Uno.Extensions;
12-
using Uno.UI;
138
using Uno.Foundation.Logging;
14-
using Microsoft.UI.Xaml.Media;
15-
using Uno.Collections;
16-
using Android.Security.Keystore;
17-
using Java.Security;
189
using Uno.Buffers;
1910
using Windows.System;
2011

2112
namespace Microsoft.UI.Xaml.Controls
2213
{
2314
/// <summary>
24-
/// A TextBlock measure cache for non-formatted text.
15+
/// A cache for native java strings. This cache periodically evicts entries that haven't been used
16+
/// in a while. Additionally, it also evicts the least recently used entries when adding new entries beyond a certain
17+
/// capacity. Limiting the total capacity is necessary to
2518
/// </summary>
2619
internal static class JavaStringCache
2720
{
28-
private static Logger _log = typeof(JavaStringCache).Log();
29-
private static Stopwatch _watch = Stopwatch.StartNew();
30-
private static HashtableEx _table = new();
21+
// 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.
22+
// 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.
23+
// https://github.com/MicrosoftDocs/xamarin-docs/blob/live/docs/android/troubleshooting/troubleshooting.md
24+
// https://github.com/unoplatform/uno/issues/18951
25+
private const int MaxEntryCount = 1000;
26+
private static readonly Logger _log = typeof(JavaStringCache).Log();
27+
private static readonly Stopwatch _watch = Stopwatch.StartNew();
28+
private static readonly Dictionary<string, LinkedListNode<KeyEntry>> _table = new();
29+
private static readonly LinkedList<KeyEntry> _queue = new();
30+
private static readonly object _gate = new();
31+
3132
private static TimeSpan _lastScavenge;
32-
private static object _gate = new();
3333

3434
internal static readonly TimeSpan LowMemoryTrimInterval = TimeSpan.FromMinutes(5);
3535
internal static readonly TimeSpan MediumMemoryTrimInterval = TimeSpan.FromMinutes(3);
@@ -38,21 +38,32 @@ internal static class JavaStringCache
3838

3939
internal static readonly TimeSpan ScavengeInterval = TimeSpan.FromMinutes(.5);
4040

41-
private static DefaultArrayPoolPlatformProvider _platformProvider = new DefaultArrayPoolPlatformProvider();
41+
private static readonly DefaultArrayPoolPlatformProvider _platformProvider = new DefaultArrayPoolPlatformProvider();
4242

4343
/// <summary>Determines if automatic memory management is enabled</summary>
4444
private static readonly bool _automaticManagement;
45-
/// <summary>Determines if GC trim callback has been registerd if non-zero</summary>
46-
private static int _trimCallbackCreated;
4745

48-
private record KeyEntry(string Value, Java.Lang.String NativeValue)
49-
{
50-
public TimeSpan LastUse { get; set; } = _watch.Elapsed;
51-
}
46+
private readonly record struct KeyEntry(string CsString, Java.Lang.String JavaString, TimeSpan LastUse);
5247

5348
static JavaStringCache()
5449
{
5550
_automaticManagement = WinRTFeatureConfiguration.ArrayPool.EnableAutomaticMemoryManagement && _platformProvider.CanUseMemoryManager;
51+
if (_automaticManagement)
52+
{
53+
if (_log.IsEnabled(LogLevel.Debug))
54+
{
55+
_log.Debug($"Using automatic memory management");
56+
}
57+
58+
_platformProvider.RegisterTrimCallback(_ => Trim(), _gate);
59+
}
60+
else
61+
{
62+
if (_log.IsEnabled(LogLevel.Debug))
63+
{
64+
_log.Debug($"Using manual memory management");
65+
}
66+
}
5667
}
5768

5869
/// <summary>
@@ -62,64 +73,60 @@ static JavaStringCache()
6273
/// <returns></returns>
6374
public static Java.Lang.String GetNativeString(string value)
6475
{
65-
TryInitializeMemoryManagement();
66-
6776
Scavenge();
6877

6978
lock (_gate)
7079
{
71-
if (_table.TryGetValue(value, out var result) && result is KeyEntry entry)
80+
if (_table.TryGetValue(value, out var result))
7281
{
7382
if (_log.IsEnabled(LogLevel.Trace))
7483
{
7584
_log.Trace($"Reusing native string: [{value}]");
7685
}
7786

78-
entry.LastUse = _watch.Elapsed;
79-
return entry.NativeValue;
87+
var entry = result.Value;
88+
result.Value = entry with { LastUse = _watch.Elapsed };
89+
_queue.Remove(result);
90+
_queue.AddFirst(result);
91+
92+
return entry.JavaString;
8093
}
8194
else
8295
{
96+
if (_queue.Count == MaxEntryCount)
97+
{
98+
var last = _queue.Last!.Value.CsString;
99+
_table.Remove(last);
100+
_queue.RemoveLast();
101+
102+
if (_log.IsEnabled(LogLevel.Trace))
103+
{
104+
_log.Trace($"{nameof(JavaStringCache)} is full. Evicting [{last}]");
105+
}
106+
}
107+
83108
if (_log.IsEnabled(LogLevel.Trace))
84109
{
85110
_log.Trace($"Creating native string for [{value}]");
86111
}
87112

88113
var javaString = new Java.Lang.String(value);
89-
_table[value] = new KeyEntry(value, javaString);
114+
var node = new LinkedListNode<KeyEntry>(new KeyEntry(value, javaString, _watch.Elapsed));
115+
_queue.AddFirst(node);
116+
_table[value] = node;
90117
return javaString;
91118
}
92119
}
93120
}
94121

95-
private static void TryInitializeMemoryManagement()
96-
{
97-
if (_automaticManagement && Interlocked.Exchange(ref _trimCallbackCreated, 1) == 0)
98-
{
99-
if (_log.IsEnabled(LogLevel.Debug))
100-
{
101-
_log.Debug($"Using automatic memory management");
102-
}
103-
104-
_platformProvider.RegisterTrimCallback(_ => Trim(), _gate);
105-
}
106-
else
107-
{
108-
if (_log.IsEnabled(LogLevel.Debug))
109-
{
110-
_log.Debug($"Using manual memory management");
111-
}
112-
}
113-
}
114-
115122
private static bool Trim()
116123
{
117124
if (!_automaticManagement)
118125
{
119126
return false;
120127
}
121128

122-
var threshold = _platformProvider?.AppMemoryUsageLevel switch
129+
var threshold = _platformProvider.AppMemoryUsageLevel switch
123130
{
124131
AppMemoryUsageLevel.Low => LowMemoryTrimInterval,
125132
AppMemoryUsageLevel.Medium => MediumMemoryTrimInterval,
@@ -130,7 +137,7 @@ private static bool Trim()
130137

131138
if (_log.IsEnabled(LogLevel.Trace))
132139
{
133-
_log.Trace($"Memory pressure is {_platformProvider?.AppMemoryUsageLevel}, using trim interval of {threshold}");
140+
_log.Trace($"Memory pressure is {_platformProvider.AppMemoryUsageLevel}, using trim interval of {threshold}");
134141
}
135142

136143
Trim(threshold);
@@ -154,26 +161,23 @@ private static void Trim(TimeSpan interval)
154161
{
155162
lock (_gate)
156163
{
157-
List<string>? entries = null;
164+
int trimmedCount = 0;
158165
foreach (var entry in _table.Values)
159166
{
160-
if (entry is KeyEntry keyEntry && keyEntry.LastUse + interval < _watch.Elapsed)
167+
var node = entry.Value;
168+
if (node.LastUse + interval < _watch.Elapsed)
161169
{
162-
entries ??= new();
163-
entries.Add(keyEntry.Value);
170+
_table.Remove(node.CsString);
171+
_queue.Remove(node);
172+
trimmedCount++;
164173
}
165174
}
166175

167-
if (entries is not null)
176+
if (trimmedCount > 0)
168177
{
169178
if (_log.IsEnabled(LogLevel.Debug))
170179
{
171-
_log.Debug($"Trimming {entries.Count} native strings unused since {interval}");
172-
}
173-
174-
foreach (var entry in entries)
175-
{
176-
_table.Remove(entry);
180+
_log.Debug($"Trimming {trimmedCount} native strings unused since {interval}");
177181
}
178182
}
179183
else

0 commit comments

Comments
 (0)