com.wallstop-studios.unity-helpers 2.0.0-rc80.5 → 2.0.0-rc80.7

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