3
3
using System ;
4
4
using System . Collections . Generic ;
5
5
using System . Diagnostics ;
6
- using System . Threading ;
7
- using System . Text ;
8
- using Windows . Foundation ;
9
6
10
7
using Uno ;
11
- using Uno . Extensions ;
12
- using Uno . UI ;
13
8
using Uno . Foundation . Logging ;
14
- using Microsoft . UI . Xaml . Media ;
15
- using Uno . Collections ;
16
- using Android . Security . Keystore ;
17
- using Java . Security ;
18
9
using Uno . Buffers ;
19
10
using Windows . System ;
20
11
21
12
namespace Microsoft . UI . Xaml . Controls
22
13
{
23
14
/// <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
25
18
/// </summary>
26
19
internal static class JavaStringCache
27
20
{
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
+
31
32
private static TimeSpan _lastScavenge ;
32
- private static object _gate = new ( ) ;
33
33
34
34
internal static readonly TimeSpan LowMemoryTrimInterval = TimeSpan . FromMinutes ( 5 ) ;
35
35
internal static readonly TimeSpan MediumMemoryTrimInterval = TimeSpan . FromMinutes ( 3 ) ;
@@ -38,21 +38,32 @@ internal static class JavaStringCache
38
38
39
39
internal static readonly TimeSpan ScavengeInterval = TimeSpan . FromMinutes ( .5 ) ;
40
40
41
- private static DefaultArrayPoolPlatformProvider _platformProvider = new DefaultArrayPoolPlatformProvider ( ) ;
41
+ private static readonly DefaultArrayPoolPlatformProvider _platformProvider = new DefaultArrayPoolPlatformProvider ( ) ;
42
42
43
43
/// <summary>Determines if automatic memory management is enabled</summary>
44
44
private static readonly bool _automaticManagement ;
45
- /// <summary>Determines if GC trim callback has been registerd if non-zero</summary>
46
- private static int _trimCallbackCreated ;
47
45
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 ) ;
52
47
53
48
static JavaStringCache ( )
54
49
{
55
50
_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
+ }
56
67
}
57
68
58
69
/// <summary>
@@ -62,64 +73,60 @@ static JavaStringCache()
62
73
/// <returns></returns>
63
74
public static Java . Lang . String GetNativeString ( string value )
64
75
{
65
- TryInitializeMemoryManagement ( ) ;
66
-
67
76
Scavenge ( ) ;
68
77
69
78
lock ( _gate )
70
79
{
71
- if ( _table . TryGetValue ( value , out var result ) && result is KeyEntry entry )
80
+ if ( _table . TryGetValue ( value , out var result ) )
72
81
{
73
82
if ( _log . IsEnabled ( LogLevel . Trace ) )
74
83
{
75
84
_log . Trace ( $ "Reusing native string: [{ value } ]") ;
76
85
}
77
86
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 ;
80
93
}
81
94
else
82
95
{
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
+
83
108
if ( _log . IsEnabled ( LogLevel . Trace ) )
84
109
{
85
110
_log . Trace ( $ "Creating native string for [{ value } ]") ;
86
111
}
87
112
88
113
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 ;
90
117
return javaString ;
91
118
}
92
119
}
93
120
}
94
121
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
-
115
122
private static bool Trim ( )
116
123
{
117
124
if ( ! _automaticManagement )
118
125
{
119
126
return false ;
120
127
}
121
128
122
- var threshold = _platformProvider ? . AppMemoryUsageLevel switch
129
+ var threshold = _platformProvider . AppMemoryUsageLevel switch
123
130
{
124
131
AppMemoryUsageLevel . Low => LowMemoryTrimInterval ,
125
132
AppMemoryUsageLevel . Medium => MediumMemoryTrimInterval ,
@@ -130,7 +137,7 @@ private static bool Trim()
130
137
131
138
if ( _log . IsEnabled ( LogLevel . Trace ) )
132
139
{
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 } ") ;
134
141
}
135
142
136
143
Trim ( threshold ) ;
@@ -154,26 +161,23 @@ private static void Trim(TimeSpan interval)
154
161
{
155
162
lock ( _gate )
156
163
{
157
- List < string > ? entries = null ;
164
+ int trimmedCount = 0 ;
158
165
foreach ( var entry in _table . Values )
159
166
{
160
- if ( entry is KeyEntry keyEntry && keyEntry . LastUse + interval < _watch . Elapsed )
167
+ var node = entry . Value ;
168
+ if ( node . LastUse + interval < _watch . Elapsed )
161
169
{
162
- entries ??= new ( ) ;
163
- entries . Add ( keyEntry . Value ) ;
170
+ _table . Remove ( node . CsString ) ;
171
+ _queue . Remove ( node ) ;
172
+ trimmedCount ++ ;
164
173
}
165
174
}
166
175
167
- if ( entries is not null )
176
+ if ( trimmedCount > 0 )
168
177
{
169
178
if ( _log . IsEnabled ( LogLevel . Debug ) )
170
179
{
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 } ") ;
177
181
}
178
182
}
179
183
else
0 commit comments