com.wallstop-studios.unity-helpers 2.0.0-rc80.4 → 2.0.0-rc80.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -12,142 +12,60 @@ namespace WallstopStudios.UnityHelpers.Core.Extension
12
12
  public static class EnumNameCache<T>
13
13
  where T : struct, Enum
14
14
  {
15
- private const int MaxDenseRange = 1024;
16
-
17
- private static readonly bool UseDensePacking;
18
- private static readonly int Min;
19
- private static readonly string[] DenseNames;
20
- private static readonly Dictionary<int, string> SparseNames;
15
+ private static readonly Dictionary<T, string> Names;
21
16
 
22
17
  static EnumNameCache()
23
18
  {
24
- T[] values = Enum.GetValues(typeof(T)).Cast<T>().ToArray();
25
- int[] intValues = values.Select(v => Unsafe.As<T, int>(ref v)).ToArray();
26
- int min = intValues.Min();
27
- int max = intValues.Max();
28
- int range = max - min + 1;
29
-
30
- if (range <= MaxDenseRange)
31
- {
32
- UseDensePacking = true;
33
- Min = min;
34
- DenseNames = new string[range];
35
-
36
- for (int i = 0; i < values.Length; i++)
37
- {
38
- int key = intValues[i] - min;
39
- T value = values[i];
40
- DenseNames[key] = value.ToString("G");
41
- }
42
- }
43
- else
19
+ T[] values = Enum.GetValues(typeof(T)).OfType<T>().ToArray();
20
+ Names = new Dictionary<T, string>(values.Length);
21
+ for (int i = 0; i < values.Length; i++)
44
22
  {
45
- UseDensePacking = false;
46
- SparseNames = new Dictionary<int, string>();
47
- for (int i = 0; i < values.Length; i++)
48
- {
49
- int key = Unsafe.As<T, int>(ref values[i]);
50
- T value = values[i];
51
- string name = value.ToString("G");
52
- SparseNames.TryAdd(key, name);
53
- }
23
+ T value = values[i];
24
+ string name = value.ToString("G");
25
+ Names.TryAdd(value, name);
54
26
  }
55
27
  }
56
28
 
57
29
  [MethodImpl(MethodImplOptions.AggressiveInlining)]
58
30
  public static string ToCachedName(T value)
59
31
  {
60
- int key = Unsafe.As<T, int>(ref value);
61
- if (UseDensePacking)
32
+ if (Names.TryGetValue(value, out string name))
62
33
  {
63
- int idx = key - Min;
64
- if ((uint)idx < (uint)DenseNames!.Length)
65
- {
66
- return DenseNames[idx];
67
- }
68
- }
69
- else
70
- {
71
- if (SparseNames!.TryGetValue(key, out string name))
72
- {
73
- return name;
74
- }
34
+ return name;
75
35
  }
76
36
 
77
- return value.ToString();
37
+ return value.ToString("G");
78
38
  }
79
39
  }
80
40
 
81
41
  public static class EnumDisplayNameCache<T>
82
42
  where T : struct, Enum
83
43
  {
84
- private const int MaxDenseRange = 1024;
85
-
86
- private static readonly bool UseDensePacking;
87
- private static readonly int Min;
88
- private static readonly string[] DenseNames;
89
- private static readonly Dictionary<int, string> SparseNames;
44
+ private static readonly Dictionary<T, string> Names;
90
45
 
91
46
  static EnumDisplayNameCache()
92
47
  {
93
48
  Type type = typeof(T);
94
49
  FieldInfo[] fields = type.GetFields(BindingFlags.Public | BindingFlags.Static);
95
- T[] values = fields.Select(f => (T)f.GetValue(null)).ToArray();
96
- int[] intValues = values.Select(v => Unsafe.As<T, int>(ref v)).ToArray();
97
- int min = intValues.Min();
98
- int max = intValues.Max();
99
- int range = max - min + 1;
50
+ Names = new Dictionary<T, string>(fields.Length);
100
51
 
101
- if (range <= MaxDenseRange)
52
+ for (int i = 0; i < fields.Length; i++)
102
53
  {
103
- UseDensePacking = true;
104
- Min = min;
105
- DenseNames = new string[range];
106
-
107
- for (int i = 0; i < fields.Length; i++)
108
- {
109
- int key = intValues[i] - min;
110
- FieldInfo field = fields[i];
111
- string name = field.IsAttributeDefined(out EnumDisplayNameAttribute displayName)
112
- ? displayName.DisplayName
113
- : field.Name;
114
- DenseNames[key] = name;
115
- }
116
- }
117
- else
118
- {
119
- UseDensePacking = false;
120
- SparseNames = new Dictionary<int, string>(fields.Length);
121
- for (int i = 0; i < fields.Length; i++)
122
- {
123
- int key = Unsafe.As<T, int>(ref values[i]);
124
- FieldInfo field = fields[i];
125
- string name = field.IsAttributeDefined(out EnumDisplayNameAttribute displayName)
126
- ? displayName.DisplayName
127
- : field.Name;
128
- SparseNames.TryAdd(key, name);
129
- }
54
+ FieldInfo field = fields[i];
55
+ string name = field.IsAttributeDefined(out EnumDisplayNameAttribute displayName)
56
+ ? displayName.DisplayName
57
+ : field.Name;
58
+ T value = (T)field.GetValue(null);
59
+ Names.TryAdd(value, name);
130
60
  }
131
61
  }
132
62
 
133
63
  [MethodImpl(MethodImplOptions.AggressiveInlining)]
134
64
  public static string ToDisplayName(T value)
135
65
  {
136
- int key = Unsafe.As<T, int>(ref value);
137
- if (UseDensePacking)
66
+ if (Names.TryGetValue(value, out string name))
138
67
  {
139
- int idx = key - Min;
140
- if ((uint)idx < (uint)DenseNames!.Length)
141
- {
142
- return DenseNames[idx];
143
- }
144
- }
145
- else
146
- {
147
- if (SparseNames!.TryGetValue(key, out string name))
148
- {
149
- return name;
150
- }
68
+ return name;
151
69
  }
152
70
 
153
71
  return value.ToString();
@@ -160,9 +78,15 @@ namespace WallstopStudios.UnityHelpers.Core.Extension
160
78
  public static bool HasFlagNoAlloc<T>(this T value, T flag)
161
79
  where T : unmanaged, Enum
162
80
  {
163
- ulong valueUnderlying = GetUInt64(value);
164
- ulong flagUnderlying = GetUInt64(flag);
165
- return (valueUnderlying & flagUnderlying) == flagUnderlying;
81
+ ulong? valueUnderlying = GetUInt64(value);
82
+ ulong? flagUnderlying = GetUInt64(flag);
83
+ if (valueUnderlying == null || flagUnderlying == null)
84
+ {
85
+ // Fallback for unsupported enum sizes
86
+ return value.HasFlag(flag);
87
+ }
88
+
89
+ return (valueUnderlying.Value & flagUnderlying.Value) == flagUnderlying.Value;
166
90
  }
167
91
 
168
92
  public static string ToDisplayName<T>(this T value)
@@ -190,23 +114,24 @@ namespace WallstopStudios.UnityHelpers.Core.Extension
190
114
  }
191
115
 
192
116
  [MethodImpl(MethodImplOptions.AggressiveInlining)]
193
- private static unsafe ulong GetUInt64<T>(T value)
117
+ private static ulong? GetUInt64<T>(T value)
194
118
  where T : unmanaged
195
119
  {
196
- /*
197
- Works because T is constrained to unmanaged, so it's safe to reinterpret
198
- All enums are value types and have a fixed size
199
- */
200
- return sizeof(T) switch
120
+ try
201
121
  {
202
- 1 => *(byte*)&value,
203
- 2 => *(ushort*)&value,
204
- 4 => *(uint*)&value,
205
- 8 => *(ulong*)&value,
206
- _ => throw new ArgumentException(
207
- $"Unsupported enum size: {sizeof(T)} for type {typeof(T)}"
208
- ),
209
- };
122
+ return Unsafe.SizeOf<T>() switch
123
+ {
124
+ 1 => Unsafe.As<T, byte>(ref value),
125
+ 2 => Unsafe.As<T, ushort>(ref value),
126
+ 4 => Unsafe.As<T, uint>(ref value),
127
+ 8 => Unsafe.As<T, ulong>(ref value),
128
+ _ => null,
129
+ };
130
+ }
131
+ catch
132
+ {
133
+ return null;
134
+ }
210
135
  }
211
136
  }
212
137
  }
@@ -1,5 +1,6 @@
1
1
  namespace WallstopStudios.UnityHelpers.Core.Threading
2
2
  {
3
+ #if !SINGLE_THREADED
3
4
  using System;
4
5
  using System.Collections.Concurrent;
5
6
  using System.Threading;
@@ -223,4 +224,5 @@ namespace WallstopStudios.UnityHelpers.Core.Threading
223
224
  }
224
225
  }
225
226
  }
227
+ #endif
226
228
  }
@@ -1,9 +1,11 @@
1
1
  namespace WallstopStudios.UnityHelpers.Utils
2
2
  {
3
3
  using System;
4
+ using System.Collections.Concurrent;
4
5
  using System.Collections.Generic;
5
6
  using System.Reflection;
6
7
  using System.Text;
8
+ using System.Threading;
7
9
  using UnityEngine;
8
10
  using WallstopStudios.UnityHelpers.Core.Helper;
9
11
 
@@ -149,6 +151,200 @@ namespace WallstopStudios.UnityHelpers.Utils
149
151
  }
150
152
  }
151
153
 
154
+ #if SINGLE_THREADED
155
+ public static class WallstopFastArrayPool<T>
156
+ {
157
+ private static readonly List<List<T[]>> _pool = new();
158
+ private static readonly Action<T[]> _onDispose = Release;
159
+
160
+ public static PooledResource<T[]> Get(int size)
161
+ {
162
+ switch (size)
163
+ {
164
+ case < 0:
165
+ {
166
+ throw new ArgumentOutOfRangeException(
167
+ nameof(size),
168
+ size,
169
+ "Must be non-negative."
170
+ );
171
+ }
172
+ case 0:
173
+ {
174
+ return new PooledResource<T[]>(Array.Empty<T>(), _ => { });
175
+ }
176
+ }
177
+
178
+ while (_pool.Count <= size)
179
+ {
180
+ _pool.Add(null);
181
+ }
182
+
183
+ List<T[]> pool = _pool[size];
184
+ if (pool == null)
185
+ {
186
+ pool = new List<T[]>();
187
+ _pool[size] = pool;
188
+ }
189
+
190
+ if (pool.Count == 0)
191
+ {
192
+ return new PooledResource<T[]>(new T[size], _onDispose);
193
+ }
194
+
195
+ int lastIndex = pool.Count - 1;
196
+ T[] instance = pool[lastIndex];
197
+ pool.RemoveAt(lastIndex);
198
+ return new PooledResource<T[]>(instance, _onDispose);
199
+ }
200
+
201
+ private static void Release(T[] resource)
202
+ {
203
+ _pool[resource.Length].Add(resource);
204
+ }
205
+ }
206
+ #else
207
+
208
+ public static class WallstopFastArrayPool<T>
209
+ {
210
+ private static readonly ReaderWriterLockSlim _lock = new();
211
+ private static readonly List<ConcurrentStack<T[]>> _pool = new();
212
+ private static readonly Action<T[]> _onDispose = Release;
213
+
214
+ public static PooledResource<T[]> Get(int size)
215
+ {
216
+ switch (size)
217
+ {
218
+ case < 0:
219
+ {
220
+ throw new ArgumentOutOfRangeException(
221
+ nameof(size),
222
+ size,
223
+ "Must be non-negative."
224
+ );
225
+ }
226
+ case 0:
227
+ {
228
+ return new PooledResource<T[]>(Array.Empty<T>(), _ => { });
229
+ }
230
+ }
231
+
232
+ bool withinRange;
233
+ ConcurrentStack<T[]> pool = null;
234
+ _lock.EnterReadLock();
235
+ try
236
+ {
237
+ withinRange = size < _pool.Count;
238
+ if (withinRange)
239
+ {
240
+ pool = _pool[size];
241
+ }
242
+ }
243
+ finally
244
+ {
245
+ _lock.ExitReadLock();
246
+ }
247
+
248
+ if (withinRange)
249
+ {
250
+ if (pool == null)
251
+ {
252
+ _lock.EnterUpgradeableReadLock();
253
+ try
254
+ {
255
+ pool = _pool[size];
256
+ if (pool == null)
257
+ {
258
+ _lock.EnterWriteLock();
259
+ try
260
+ {
261
+ pool = _pool[size];
262
+ if (pool == null)
263
+ {
264
+ pool = new ConcurrentStack<T[]>();
265
+ _pool[size] = pool;
266
+ }
267
+ }
268
+ finally
269
+ {
270
+ _lock.ExitWriteLock();
271
+ }
272
+ }
273
+ }
274
+ finally
275
+ {
276
+ _lock.ExitUpgradeableReadLock();
277
+ }
278
+ }
279
+ }
280
+ else
281
+ {
282
+ _lock.EnterUpgradeableReadLock();
283
+ try
284
+ {
285
+ if (size < _pool.Count)
286
+ {
287
+ pool = _pool[size];
288
+ if (pool == null)
289
+ {
290
+ _lock.EnterWriteLock();
291
+ try
292
+ {
293
+ pool = _pool[size];
294
+ if (pool == null)
295
+ {
296
+ pool = new ConcurrentStack<T[]>();
297
+ _pool[size] = pool;
298
+ }
299
+ }
300
+ finally
301
+ {
302
+ _lock.ExitWriteLock();
303
+ }
304
+ }
305
+ }
306
+ else
307
+ {
308
+ _lock.EnterWriteLock();
309
+ try
310
+ {
311
+ while (_pool.Count <= size)
312
+ {
313
+ _pool.Add(null);
314
+ }
315
+ pool = _pool[size];
316
+ if (pool == null)
317
+ {
318
+ pool = new ConcurrentStack<T[]>();
319
+ _pool[size] = pool;
320
+ }
321
+ }
322
+ finally
323
+ {
324
+ _lock.ExitWriteLock();
325
+ }
326
+ }
327
+ }
328
+ finally
329
+ {
330
+ _lock.ExitUpgradeableReadLock();
331
+ }
332
+ }
333
+
334
+ if (pool.TryPop(out T[] instance))
335
+ {
336
+ return new PooledResource<T[]>(instance, _onDispose);
337
+ }
338
+ return new PooledResource<T[]>(new T[size], _onDispose);
339
+ }
340
+
341
+ private static void Release(T[] resource)
342
+ {
343
+ _pool[resource.Length].Push(resource);
344
+ }
345
+ }
346
+ #endif
347
+
152
348
  public readonly struct PooledResource<T> : IDisposable
153
349
  {
154
350
  public readonly T resource;
@@ -1,5 +1,6 @@
1
1
  namespace WallstopStudios.UnityHelpers.Tests.Core.Threading
2
2
  {
3
+ #if !SINGLE_THREADED
3
4
  using System.Collections;
4
5
  using System.Collections.Generic;
5
6
  using NUnit.Framework;
@@ -51,4 +52,5 @@ namespace WallstopStudios.UnityHelpers.Tests.Core.Threading
51
52
  }
52
53
  }
53
54
  }
55
+ #endif
54
56
  }
@@ -2,6 +2,7 @@ namespace WallstopStudios.UnityHelpers.Tests.Extensions
2
2
  {
3
3
  using NUnit.Framework;
4
4
  using UnityEngine.TestTools.Constraints;
5
+ using WallstopStudios.UnityHelpers.Core.Attributes;
5
6
  using WallstopStudios.UnityHelpers.Core.Extension;
6
7
  using Is = NUnit.Framework.Is;
7
8
 
@@ -26,8 +27,14 @@ namespace WallstopStudios.UnityHelpers.Tests.Extensions
26
27
  private enum SmallTestEnum : short
27
28
  {
28
29
  None = 0,
30
+
31
+ [EnumDisplayName("TestFirst")]
29
32
  First = 1 << 0,
33
+
34
+ [EnumDisplayName("TestSecond")]
30
35
  Second = 1 << 1,
36
+
37
+ [EnumDisplayName("TestThird")]
31
38
  Third = 1 << 2,
32
39
  }
33
40
 
@@ -39,6 +46,24 @@ namespace WallstopStudios.UnityHelpers.Tests.Extensions
39
46
  Third = 1 << 2,
40
47
  }
41
48
 
49
+ [Test]
50
+ public void DisplayName()
51
+ {
52
+ Assert.AreEqual("None", SmallTestEnum.None.ToDisplayName());
53
+ Assert.AreEqual("TestFirst", SmallTestEnum.First.ToDisplayName());
54
+ Assert.AreEqual("TestSecond", SmallTestEnum.Second.ToDisplayName());
55
+ Assert.AreEqual("TestThird", SmallTestEnum.Third.ToDisplayName());
56
+ }
57
+
58
+ [Test]
59
+ public void CachedName()
60
+ {
61
+ Assert.AreEqual("None", SmallTestEnum.None.ToCachedName());
62
+ Assert.AreEqual("First", SmallTestEnum.First.ToCachedName());
63
+ Assert.AreEqual("Second", SmallTestEnum.Second.ToCachedName());
64
+ Assert.AreEqual("Third", SmallTestEnum.Third.ToCachedName());
65
+ }
66
+
42
67
  [Test]
43
68
  public void HasFlagNoAlloc()
44
69
  {
@@ -1,9 +1,18 @@
1
1
  namespace WallstopStudios.UnityHelpers.Tests.Utils
2
2
  {
3
+ using System;
4
+ using System.Collections;
3
5
  using System.Collections.Generic;
6
+ using System.Linq;
4
7
  using NUnit.Framework;
8
+ using UnityEngine.TestTools;
9
+ using WallstopStudios.UnityHelpers.Core.Extension;
5
10
  using WallstopStudios.UnityHelpers.Core.Random;
6
11
  using WallstopStudios.UnityHelpers.Utils;
12
+ #if !SINGLETHREADED
13
+ using System.Threading;
14
+ using System.Threading.Tasks;
15
+ #endif
7
16
 
8
17
  public sealed class BuffersTests
9
18
  {
@@ -51,5 +60,661 @@ namespace WallstopStudios.UnityHelpers.Tests.Utils
51
60
  }
52
61
  }
53
62
  }
63
+
64
+ [Test]
65
+ public void WallstopFastArrayPoolGetNegativeSizeThrowsArgumentOutOfRangeException()
66
+ {
67
+ Assert.Throws<ArgumentOutOfRangeException>(() => WallstopFastArrayPool<int>.Get(-1));
68
+ }
69
+
70
+ [Test]
71
+ public void WallstopFastArrayPoolGetZeroSizeReturnsEmptyArrayWithNoOpDispose()
72
+ {
73
+ using PooledResource<int[]> pooled = WallstopFastArrayPool<int>.Get(0);
74
+ Assert.NotNull(pooled.resource);
75
+ Assert.AreEqual(0, pooled.resource.Length);
76
+ Assert.AreSame(Array.Empty<int>(), pooled.resource);
77
+ }
78
+
79
+ [Test]
80
+ public void WallstopFastArrayPoolGetPositiveSizeReturnsArrayWithCorrectLength()
81
+ {
82
+ const int size = 10;
83
+ using PooledResource<int[]> pooled = WallstopFastArrayPool<int>.Get(size);
84
+
85
+ Assert.NotNull(pooled.resource);
86
+ Assert.AreEqual(size, pooled.resource.Length);
87
+ }
88
+
89
+ [Test]
90
+ public void WallstopFastArrayPoolGetSameSizeReusesArrayAfterDispose()
91
+ {
92
+ const int size = 5;
93
+ int[] firstArray;
94
+
95
+ using (PooledResource<int[]> pooled = WallstopFastArrayPool<int>.Get(size))
96
+ {
97
+ firstArray = pooled.resource;
98
+ firstArray[0] = 42;
99
+ }
100
+
101
+ using PooledResource<int[]> pooledReused = WallstopFastArrayPool<int>.Get(size);
102
+ Assert.AreSame(firstArray, pooledReused.resource);
103
+ Assert.AreEqual(42, pooledReused.resource[0]);
104
+ }
105
+
106
+ [Test]
107
+ public void WallstopFastArrayPoolGetDifferentSizesReturnsCorrectArrays()
108
+ {
109
+ int[] sizes = { 1, 5, 10, 100, 1000 };
110
+ List<PooledResource<int[]>> pooledArrays = new();
111
+
112
+ try
113
+ {
114
+ foreach (int size in sizes)
115
+ {
116
+ PooledResource<int[]> pooled = WallstopFastArrayPool<int>.Get(size);
117
+ pooledArrays.Add(pooled);
118
+ Assert.AreEqual(size, pooled.resource.Length);
119
+ }
120
+ }
121
+ finally
122
+ {
123
+ foreach (PooledResource<int[]> pooled in pooledArrays)
124
+ {
125
+ pooled.Dispose();
126
+ }
127
+ }
128
+ }
129
+
130
+ [Test]
131
+ public void WallstopFastArrayPoolArraysNotClearedOnRelease()
132
+ {
133
+ const int size = 10;
134
+
135
+ using (PooledResource<string[]> pooled = WallstopFastArrayPool<string>.Get(size))
136
+ {
137
+ for (int i = 0; i < size; i++)
138
+ {
139
+ pooled.resource[i] = $"test{i}";
140
+ }
141
+ }
142
+
143
+ using PooledResource<string[]> pooledReused = WallstopFastArrayPool<string>.Get(size);
144
+ for (int i = 0; i < size; i++)
145
+ {
146
+ Assert.AreEqual($"test{i}", pooledReused.resource[i]);
147
+ }
148
+ }
149
+
150
+ [Test]
151
+ public void WallstopFastArrayPoolPoolGrowsAsNeeded()
152
+ {
153
+ const int size = 7;
154
+ const int count = 5;
155
+ List<PooledResource<int[]>> pooledArrays = new();
156
+
157
+ try
158
+ {
159
+ for (int i = 0; i < count; i++)
160
+ {
161
+ PooledResource<int[]> pooled = WallstopFastArrayPool<int>.Get(size);
162
+ pooledArrays.Add(pooled);
163
+ Assert.AreEqual(size, pooled.resource.Length);
164
+ }
165
+
166
+ HashSet<int[]> distinctArrays = pooledArrays.Select(p => p.resource).ToHashSet();
167
+ Assert.AreEqual(count, distinctArrays.Count);
168
+ }
169
+ finally
170
+ {
171
+ foreach (PooledResource<int[]> pooled in pooledArrays)
172
+ {
173
+ pooled.Dispose();
174
+ }
175
+ }
176
+ }
177
+
178
+ [Test]
179
+ public void WallstopFastArrayPoolDifferentTypesUseDifferentPools()
180
+ {
181
+ const int size = 10;
182
+
183
+ using PooledResource<int[]> intPooled = WallstopFastArrayPool<int>.Get(size);
184
+ using PooledResource<string[]> stringPooled = WallstopFastArrayPool<string>.Get(size);
185
+ using PooledResource<float[]> floatPooled = WallstopFastArrayPool<float>.Get(size);
186
+
187
+ Assert.AreEqual(size, intPooled.resource.Length);
188
+ Assert.AreEqual(size, stringPooled.resource.Length);
189
+ Assert.AreEqual(size, floatPooled.resource.Length);
190
+ }
191
+
192
+ [Test]
193
+ public void WallstopFastArrayPoolLargeArraysWork()
194
+ {
195
+ const int size = 100000;
196
+ using PooledResource<byte[]> pooled = WallstopFastArrayPool<byte>.Get(size);
197
+
198
+ Assert.AreEqual(size, pooled.resource.Length);
199
+ }
200
+
201
+ [Test]
202
+ public void WallstopFastArrayPoolNestedUsageWorks()
203
+ {
204
+ const int outerSize = 5;
205
+ const int innerSize = 3;
206
+
207
+ using PooledResource<int[]> outer = WallstopFastArrayPool<int>.Get(outerSize);
208
+ Assert.AreEqual(outerSize, outer.resource.Length);
209
+ Array.Clear(outer.resource, 0, outer.resource.Length);
210
+ outer.resource[0] = 1;
211
+
212
+ using (PooledResource<int[]> inner = WallstopFastArrayPool<int>.Get(innerSize))
213
+ {
214
+ inner.resource[0] = 2;
215
+ Assert.AreEqual(innerSize, inner.resource.Length);
216
+ Assert.AreEqual(1, outer.resource[0]);
217
+ Assert.AreEqual(2, inner.resource[0]);
218
+ }
219
+
220
+ Assert.AreEqual(outerSize, outer.resource.Length);
221
+ Assert.AreEqual(1, outer.resource[0]);
222
+ }
223
+
224
+ [UnityTest]
225
+ public IEnumerator WallstopFastArrayPoolStressTest()
226
+ {
227
+ const int iterations = 1000;
228
+ const int maxSize = 100;
229
+ Random random = new(42);
230
+
231
+ for (int i = 0; i < iterations; i++)
232
+ {
233
+ int size = random.Next(1, maxSize);
234
+ using PooledResource<int[]> pooled = WallstopFastArrayPool<int>.Get(size);
235
+
236
+ Assert.AreEqual(size, pooled.resource.Length);
237
+
238
+ for (int j = 0; j < Math.Min(10, size); j++)
239
+ {
240
+ pooled.resource[j] = random.Next();
241
+ }
242
+
243
+ if (i % 100 == 0)
244
+ {
245
+ yield return null;
246
+ }
247
+ }
248
+ }
249
+
250
+ #if !SINGLETHREADED
251
+ [Test]
252
+ public void WallstopFastArrayPoolConcurrentAccessDifferentSizes()
253
+ {
254
+ const int threadCount = 10;
255
+ const int operationsPerThread = 100;
256
+ Task[] tasks = new Task[threadCount];
257
+ List<Exception> exceptions = new();
258
+
259
+ for (int t = 0; t < threadCount; t++)
260
+ {
261
+ int threadId = t;
262
+ tasks[t] = Task.Run(async () =>
263
+ {
264
+ try
265
+ {
266
+ PcgRandom random = new(threadId);
267
+ foreach (int i in Enumerable.Range(0, operationsPerThread).Shuffled(random))
268
+ {
269
+ int size = random.Next(1, 50) + threadId;
270
+ using PooledResource<int[]> pooled = WallstopFastArrayPool<int>.Get(
271
+ size
272
+ );
273
+
274
+ Assert.AreEqual(size, pooled.resource.Length);
275
+
276
+ for (int j = 0; j < Math.Min(5, size); j++)
277
+ {
278
+ pooled.resource[j] = threadId * 1000 + i * 10 + j;
279
+ }
280
+
281
+ await Task.Delay(TimeSpan.FromMilliseconds(random.NextDouble()));
282
+ }
283
+ }
284
+ catch (Exception ex)
285
+ {
286
+ lock (exceptions)
287
+ {
288
+ exceptions.Add(ex);
289
+ }
290
+ }
291
+ });
292
+ }
293
+
294
+ Task.WaitAll(tasks);
295
+
296
+ if (exceptions.Count > 0)
297
+ {
298
+ throw new AggregateException(exceptions);
299
+ }
300
+ }
301
+
302
+ [Test]
303
+ public void WallstopFastArrayPoolConcurrentAccessSameSize()
304
+ {
305
+ const int threadCount = 8;
306
+ const int operationsPerThread = 200;
307
+ const int arraySize = 25;
308
+ Task[] tasks = new Task[threadCount];
309
+ List<Exception> exceptions = new();
310
+
311
+ for (int t = 0; t < threadCount; t++)
312
+ {
313
+ int threadId = t;
314
+ tasks[t] = Task.Run(() =>
315
+ {
316
+ try
317
+ {
318
+ PcgRandom random = new(threadId);
319
+ foreach (int i in Enumerable.Range(0, operationsPerThread).Shuffled(random))
320
+ {
321
+ using PooledResource<int[]> pooled = WallstopFastArrayPool<int>.Get(
322
+ arraySize
323
+ );
324
+
325
+ Array.Clear(pooled.resource, 0, pooled.resource.Length);
326
+ Assert.AreEqual(arraySize, pooled.resource.Length);
327
+
328
+ for (int j = 0; j < arraySize; j++)
329
+ {
330
+ Assert.AreEqual(0, pooled.resource[j]);
331
+ pooled.resource[j] = threadId * 1000 + i;
332
+ }
333
+ }
334
+ }
335
+ catch (Exception ex)
336
+ {
337
+ lock (exceptions)
338
+ {
339
+ exceptions.Add(ex);
340
+ }
341
+ }
342
+ });
343
+ }
344
+
345
+ Task.WaitAll(tasks);
346
+
347
+ if (exceptions.Count > 0)
348
+ {
349
+ throw new AggregateException(exceptions);
350
+ }
351
+ }
352
+
353
+ [Test]
354
+ public void WallstopFastArrayPoolConcurrentAccessMixedSizes()
355
+ {
356
+ const int threadCount = 6;
357
+ const int operationsPerThread = 150;
358
+ Task[] tasks = new Task[threadCount];
359
+ List<Exception> exceptions = new();
360
+ int[] sizes = { 1, 5, 10, 20, 50, 100 };
361
+
362
+ for (int t = 0; t < threadCount; t++)
363
+ {
364
+ int threadId = t;
365
+ tasks[t] = Task.Run(async () =>
366
+ {
367
+ try
368
+ {
369
+ PcgRandom random = new(threadId + 100);
370
+ foreach (int i in Enumerable.Range(0, operationsPerThread).Shuffled(random))
371
+ {
372
+ int size = random.NextOf(sizes);
373
+ using PooledResource<string[]> pooled =
374
+ WallstopFastArrayPool<string>.Get(size);
375
+ Array.Clear(pooled.resource, 0, pooled.resource.Length);
376
+
377
+ Assert.AreEqual(size, pooled.resource.Length);
378
+
379
+ for (int j = 0; j < size; j++)
380
+ {
381
+ Assert.IsNull(pooled.resource[j]);
382
+ pooled.resource[j] = $"T{threadId}-I{i}-J{j}";
383
+ }
384
+
385
+ if (i % 50 == 0)
386
+ {
387
+ await Task.Delay(TimeSpan.FromMilliseconds(random.NextDouble()));
388
+ }
389
+ }
390
+ }
391
+ catch (Exception ex)
392
+ {
393
+ lock (exceptions)
394
+ {
395
+ exceptions.Add(ex);
396
+ }
397
+ }
398
+ });
399
+ }
400
+
401
+ Task.WaitAll(tasks);
402
+
403
+ if (exceptions.Count > 0)
404
+ {
405
+ throw new AggregateException(exceptions);
406
+ }
407
+ }
408
+
409
+ [Test]
410
+ public void WallstopFastArrayPoolConcurrentAccessRapidAllocationDeallocation()
411
+ {
412
+ const int threadCount = 12;
413
+ const int operationsPerThread = 500;
414
+ Task[] tasks = new Task[threadCount];
415
+ List<Exception> exceptions = new();
416
+
417
+ for (int t = 0; t < threadCount; t++)
418
+ {
419
+ int threadId = t;
420
+ tasks[t] = Task.Run(() =>
421
+ {
422
+ try
423
+ {
424
+ PcgRandom random = new(threadId + 100);
425
+ foreach (int i in Enumerable.Range(0, operationsPerThread).Shuffled(random))
426
+ {
427
+ int size = random.Next(1, 30);
428
+ using PooledResource<byte[]> pooled = WallstopFastArrayPool<byte>.Get(
429
+ size
430
+ );
431
+
432
+ Assert.AreEqual(size, pooled.resource.Length);
433
+
434
+ for (int j = 0; j < size; j++)
435
+ {
436
+ pooled.resource[j] = (byte)(threadId + i + j);
437
+ }
438
+ }
439
+ }
440
+ catch (Exception ex)
441
+ {
442
+ lock (exceptions)
443
+ {
444
+ exceptions.Add(ex);
445
+ }
446
+ }
447
+ });
448
+ }
449
+
450
+ Task.WaitAll(tasks);
451
+
452
+ if (exceptions.Count > 0)
453
+ {
454
+ throw new AggregateException(exceptions);
455
+ }
456
+ }
457
+
458
+ [Test]
459
+ public void WallstopFastArrayPoolThreadSafetyPoolExpansion()
460
+ {
461
+ const int threadCount = 8;
462
+ const int maxPoolSize = 1000;
463
+ Task[] tasks = new Task[threadCount];
464
+ List<Exception> exceptions = new();
465
+ Barrier barrier = new(threadCount);
466
+
467
+ for (int t = 0; t < threadCount; t++)
468
+ {
469
+ int threadId = t;
470
+ tasks[t] = Task.Run(() =>
471
+ {
472
+ try
473
+ {
474
+ barrier.SignalAndWait();
475
+
476
+ for (int size = threadId + 1; size <= maxPoolSize; size += threadCount)
477
+ {
478
+ using PooledResource<int[]> pooled = WallstopFastArrayPool<int>.Get(
479
+ size
480
+ );
481
+ Assert.AreEqual(size, pooled.resource.Length);
482
+
483
+ pooled.resource[0] = threadId;
484
+ pooled.resource[size - 1] = threadId;
485
+ }
486
+ }
487
+ catch (Exception ex)
488
+ {
489
+ lock (exceptions)
490
+ {
491
+ exceptions.Add(ex);
492
+ }
493
+ }
494
+ });
495
+ }
496
+
497
+ Task.WaitAll(tasks);
498
+
499
+ if (exceptions.Count > 0)
500
+ {
501
+ throw new AggregateException(exceptions);
502
+ }
503
+ }
504
+
505
+ [UnityTest]
506
+ public IEnumerator WallstopFastArrayPoolConcurrentStressTest()
507
+ {
508
+ const int threadCount = 4;
509
+ const int operationsPerThread = 250;
510
+ Task[] tasks = new Task[threadCount];
511
+ List<Exception> exceptions = new();
512
+ int completedThreads = 0;
513
+
514
+ for (int t = 0; t < threadCount; t++)
515
+ {
516
+ int threadId = t;
517
+ tasks[t] = Task.Run(() =>
518
+ {
519
+ try
520
+ {
521
+ PcgRandom random = new(threadId + 100);
522
+ foreach (int i in Enumerable.Range(0, operationsPerThread).Shuffled(random))
523
+ {
524
+ int size = random.Next(1, 100);
525
+ using PooledResource<float[]> pooled = WallstopFastArrayPool<float>.Get(
526
+ size
527
+ );
528
+
529
+ Assert.AreEqual(size, pooled.resource.Length);
530
+
531
+ for (int j = 0; j < Math.Min(10, size); j++)
532
+ {
533
+ pooled.resource[j] = threadId * 1000.0f + i + j * 0.1f;
534
+ }
535
+ }
536
+ }
537
+ catch (Exception ex)
538
+ {
539
+ lock (exceptions)
540
+ {
541
+ exceptions.Add(ex);
542
+ }
543
+ }
544
+ finally
545
+ {
546
+ Interlocked.Increment(ref completedThreads);
547
+ }
548
+ });
549
+ }
550
+
551
+ while (completedThreads < threadCount)
552
+ {
553
+ yield return null;
554
+ }
555
+
556
+ Task.WaitAll(tasks);
557
+
558
+ if (exceptions.Count > 0)
559
+ {
560
+ throw new AggregateException(exceptions);
561
+ }
562
+ }
563
+ #endif
564
+
565
+ [Test]
566
+ public void WallstopFastArrayPoolEdgeCaseVeryLargeSize()
567
+ {
568
+ const int veryLargeSize = 1000000;
569
+ using PooledResource<int[]> pooled = WallstopFastArrayPool<int>.Get(veryLargeSize);
570
+
571
+ Assert.AreEqual(veryLargeSize, pooled.resource.Length);
572
+ Assert.AreEqual(0, pooled.resource[0]);
573
+ Assert.AreEqual(0, pooled.resource[veryLargeSize - 1]);
574
+ }
575
+
576
+ [Test]
577
+ public void WallstopFastArrayPoolEdgeCaseSizeOne()
578
+ {
579
+ using PooledResource<string[]> pooled = WallstopFastArrayPool<string>.Get(1);
580
+ Array.Clear(pooled.resource, 0, pooled.resource.Length);
581
+ Assert.AreEqual(1, pooled.resource.Length);
582
+ Assert.IsNull(pooled.resource[0]);
583
+
584
+ pooled.resource[0] = "test";
585
+ Assert.AreEqual("test", pooled.resource[0]);
586
+ }
587
+
588
+ [Test]
589
+ public void WallstopFastArrayPoolPoolingBehaviorLifo()
590
+ {
591
+ const int size = 15;
592
+ int[][] arrays = new int[3][];
593
+ {
594
+ using PooledResource<int[]> pooled1 = WallstopFastArrayPool<int>.Get(size);
595
+ arrays[0] = pooled1.resource;
596
+ using PooledResource<int[]> pooled2 = WallstopFastArrayPool<int>.Get(size);
597
+ arrays[1] = pooled2.resource;
598
+ using PooledResource<int[]> pooled3 = WallstopFastArrayPool<int>.Get(size);
599
+ arrays[2] = pooled3.resource;
600
+ }
601
+
602
+ using PooledResource<int[]> pooledReuse1 = WallstopFastArrayPool<int>.Get(size);
603
+ Assert.AreSame(arrays[0], pooledReuse1.resource);
604
+
605
+ using PooledResource<int[]> pooledReuse2 = WallstopFastArrayPool<int>.Get(size);
606
+ Assert.AreSame(arrays[1], pooledReuse2.resource);
607
+
608
+ using PooledResource<int[]> pooledReuse3 = WallstopFastArrayPool<int>.Get(size);
609
+ Assert.AreSame(arrays[2], pooledReuse3.resource);
610
+ }
611
+
612
+ [Test]
613
+ public void WallstopArrayPoolGetNegativeSizeThrowsArgumentOutOfRangeException()
614
+ {
615
+ Assert.Throws<ArgumentOutOfRangeException>(() => WallstopArrayPool<int>.Get(-1));
616
+ }
617
+
618
+ [Test]
619
+ public void WallstopArrayPoolGetZeroSizeReturnsEmptyArrayWithNoOpDispose()
620
+ {
621
+ using PooledResource<int[]> pooled = WallstopArrayPool<int>.Get(0);
622
+ Assert.NotNull(pooled.resource);
623
+ Assert.AreEqual(0, pooled.resource.Length);
624
+ Assert.AreSame(Array.Empty<int>(), pooled.resource);
625
+ }
626
+
627
+ [Test]
628
+ public void WallstopArrayPoolGetPositiveSizeReturnsArrayWithCorrectLength()
629
+ {
630
+ const int size = 10;
631
+ using PooledResource<int[]> pooled = WallstopArrayPool<int>.Get(size);
632
+
633
+ Assert.NotNull(pooled.resource);
634
+ Assert.AreEqual(size, pooled.resource.Length);
635
+ }
636
+
637
+ [Test]
638
+ public void WallstopArrayPoolGetSameSizeReusesArrayAfterDispose()
639
+ {
640
+ const int size = 5;
641
+ int[] firstArray;
642
+
643
+ using (PooledResource<int[]> pooled = WallstopArrayPool<int>.Get(size))
644
+ {
645
+ firstArray = pooled.resource;
646
+ firstArray[0] = 42;
647
+ }
648
+
649
+ using PooledResource<int[]> pooledReused = WallstopArrayPool<int>.Get(size);
650
+ Assert.AreSame(firstArray, pooledReused.resource);
651
+ Assert.AreEqual(0, pooledReused.resource[0]);
652
+ }
653
+
654
+ [Test]
655
+ public void WallstopGenericPoolGetReturnsValidPooledResource()
656
+ {
657
+ using PooledResource<List<int>> pooled = WallstopGenericPool<List<int>>.Get();
658
+
659
+ Assert.NotNull(pooled.resource);
660
+ Assert.AreEqual(0, pooled.resource.Count);
661
+ }
662
+
663
+ [Test]
664
+ public void WallstopGenericPoolGetReusesInstanceAfterDispose()
665
+ {
666
+ List<int> firstList;
667
+
668
+ using (PooledResource<List<int>> pooled = WallstopGenericPool<List<int>>.Get())
669
+ {
670
+ firstList = pooled.resource;
671
+ firstList.Add(42);
672
+ firstList.Add(100);
673
+ }
674
+
675
+ using PooledResource<List<int>> pooledReused = WallstopGenericPool<List<int>>.Get();
676
+ Assert.AreSame(firstList, pooledReused.resource);
677
+ Assert.AreEqual(0, pooledReused.resource.Count);
678
+ }
679
+
680
+ [Test]
681
+ public void WallstopGenericPoolClearActionWorksWithCustomType()
682
+ {
683
+ using PooledResource<HashSet<string>> pooled = WallstopGenericPool<
684
+ HashSet<string>
685
+ >.Get();
686
+
687
+ pooled.resource.Add("test1");
688
+ pooled.resource.Add("test2");
689
+ Assert.AreEqual(2, pooled.resource.Count);
690
+ }
691
+
692
+ [Test]
693
+ public void PooledResourceDisposeCallsOnDisposeAction()
694
+ {
695
+ bool disposeCalled = false;
696
+ WallstopGenericPool<List<int>>.clearAction ??= list => list.Clear();
697
+ WallstopGenericPool<List<int>>.clearAction += Callback;
698
+ try
699
+ {
700
+ using (PooledResource<List<int>> pooled = WallstopGenericPool<List<int>>.Get())
701
+ {
702
+ Assert.NotNull(pooled.resource);
703
+ Assert.IsFalse(disposeCalled);
704
+ }
705
+
706
+ Assert.IsTrue(disposeCalled);
707
+ }
708
+ finally
709
+ {
710
+ WallstopGenericPool<List<int>>.clearAction -= Callback;
711
+ }
712
+
713
+ return;
714
+ void Callback(List<int> list)
715
+ {
716
+ disposeCalled = true;
717
+ }
718
+ }
54
719
  }
55
720
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "com.wallstop-studios.unity-helpers",
3
- "version": "2.0.0-rc80.4",
3
+ "version": "2.0.0-rc80.6",
4
4
  "displayName": "Unity Helpers",
5
5
  "description": "Various Unity Helper Library",
6
6
  "dependencies": {},
@@ -55,6 +55,8 @@
55
55
 
56
56
 
57
57
 
58
+
59
+
58
60
 
59
61
 
60
62