A game about forced loneliness, made by TACStudios
1using Unity.PerformanceTesting;
2using Unity.PerformanceTesting.Benchmark;
3using Unity.Burst;
4using Unity.Collections.LowLevel.Unsafe;
5using Unity.Jobs;
6using System.Runtime.InteropServices;
7
8namespace Unity.Collections.PerformanceTests
9{
10 /// <summary>
11 /// Interface to implement container performance tests which will run using <see cref="BenchmarkContainerRunnerParallel{T}.Run))"/>.
12 /// Deriving tests from this interface enables both Performance Test Framework and Benchmark Framework to generate and run
13 /// tests for the contexts described by <see cref="BenchmarkContainerType"/>.
14 /// </summary>
15 public interface IBenchmarkContainerParallel
16 {
17 /// <summary>
18 /// Override this to add extra int arguments to a performance test implementation as fields in the implementing type. These arguments
19 /// are optionally passed in through <see cref="BenchmarkContainerRunner{T}.Run(int, BenchmarkContainerType, int[])"/>.
20 /// </summary>
21 /// <param name="capacity">The initial capacity to requested for the container.</param>
22 /// <param name="args">A variable number of extra arguments to passed through to the test implementation</param>
23 public void SetParams(int capacity, params int[] args) { }
24
25 /// <summary>
26 /// Called during setup for each measurement in a sample set with the capacity to allocate to the native container
27 /// when the benchmark type is <see cref="BenchmarkContainerType.Native"/>, <see cref="BenchmarkContainerType.NativeBurstNoSafety"/>,
28 /// or <see cref="BenchmarkContainerType.NativeBurstSafety"/>.<para />
29 /// This is also called during teardown for each measurement in a sample set with '-1' to indicate freeing the container.
30 /// </summary>
31 /// <param name="capacity">The capacity to allocate for the managed container. Capacity of 0 will still create a container,
32 /// but it will be empty. A capacity of -1 will dispose the container and free associated allocation(s).</param>
33 public void AllocNativeContainer(int capacity);
34
35 /// <summary>
36 /// Called during setup for each measurement in a sample set with the capacity to allocate to the unsafe container
37 /// when the benchmark type is <see cref="BenchmarkContainerType.Unsafe"/>, <see cref="BenchmarkContainerType.UnsafeBurstNoSafety"/>,
38 /// or <see cref="BenchmarkContainerType.UnsafeBurstSafety"/>.<para />
39 /// This is also called during teardown for each measurement in a sample set with '-1' to indicate freeing the container.
40 /// </summary>
41 /// <param name="capacity">The capacity to allocate for the managed container. Capacity of 0 will still create a container,
42 /// but it will be empty. A capacity of -1 will dispose the container and free associated allocation(s).</param>
43 public void AllocUnsafeContainer(int capacity);
44
45 /// <summary>
46 /// Called during setup for each measurement in a sample set with the capacity to allocate to the managed container
47 /// when the benchmark type is <see cref="BenchmarkContainerConfig.BCL"/>.<para />
48 /// This is also called during teardown for each measurement in a sample set with '-1' to indicate freeing the container.
49 /// </summary>
50 /// <param name="capacity">The capacity to allocate for the managed container. Capacity of 0 will still create a container,
51 /// but it will be empty. A capacity of -1 will dispose the container and free associated allocation(s).</param>
52 /// <returns>A reference to the allocated container when capacity >= 0, and `null` when capacity < 0.</returns>
53 public object AllocBclContainer(int capacity);
54
55 /// <summary>
56 /// The code which will be executed during performance measurement. This should usually be general enough to
57 /// work with any native container.
58 /// </summary>
59 /// <param name="worker">The worker index out of the number of job workers requested for parallel benchmarking</param>
60 /// <param name="threadIndex">The job system thread index which must be specified in some cases for a container's ParallelWriter</param>
61 public void MeasureNativeContainer(int worker, int threadIndex);
62
63 /// <summary>
64 /// The code which will be executed during performance measurement. This should usually be general enough to
65 /// work with any unsafe container.
66 /// </summary>
67 /// <param name="worker">The worker index out of the number of job workers requested for parallel benchmarking</param>
68 /// <param name="threadIndex">The job system thread index which must be specified in some cases for a container's ParallelWriter</param>
69 public void MeasureUnsafeContainer(int worker, int threadIndex);
70
71 /// <summary>
72 /// The code which will be executed during performance measurement. This should usually be general enough to
73 /// work with any managed container provided by the Base Class Library (BCL).
74 /// </summary>
75 /// <param name="container">A reference to the managed container allocated in <see cref="AllocBclContainer(int)"/></param>
76 /// <param name="worker">The worker index out of the number of job workers requested for parallel benchmarking</param>
77 public void MeasureBclContainer(object container, int worker);
78 }
79
80 /// <summary>
81 /// Provides the API for running container based Performance Framework tests and Benchmark Framework measurements.
82 /// This will typically be the sole call from a performance test. See <see cref="Run(int, BenchmarkContainerType, int[])"/>
83 /// for more information.
84 /// </summary>
85 /// <typeparam name="T">An implementation conforming to the <see cref="IBenchmarkContainer"/> interface for running container performance tests and benchmarks.</typeparam>
86 [BurstCompile(CompileSynchronously = true)]
87 public static class BenchmarkContainerRunnerParallel<T> where T : unmanaged, IBenchmarkContainerParallel
88 {
89 [BurstCompile(CompileSynchronously = true, DisableSafetyChecks = true)]
90 unsafe struct NativeJobBurstST : IJob
91 {
92 [NativeDisableUnsafePtrRestriction] public T* methods;
93 public void Execute() => methods->MeasureNativeContainer(0, 0);
94 }
95 [BurstCompile(CompileSynchronously = true, DisableSafetyChecks = false)]
96 unsafe struct NativeJobSafetyBurstST : IJob
97 {
98 [NativeDisableUnsafePtrRestriction] public T* methods;
99 public void Execute() => methods->MeasureNativeContainer(0, 0);
100 }
101
102 [BurstCompile(CompileSynchronously = true, DisableSafetyChecks = true)]
103 unsafe struct UnsafeJobBurstST : IJob
104 {
105 [NativeDisableUnsafePtrRestriction] public T* methods;
106 public void Execute() => methods->MeasureUnsafeContainer(0, 0);
107 }
108 [BurstCompile(CompileSynchronously = true, DisableSafetyChecks = false)]
109 unsafe struct UnsafeJobSafetyBurstST : IJob
110 {
111 [NativeDisableUnsafePtrRestriction] public T* methods;
112 public void Execute() => methods->MeasureUnsafeContainer(0, 0);
113 }
114
115 unsafe struct NativeJobMT : IJobParallelFor
116 {
117 [NativeSetThreadIndex] int threadIndex;
118 [NativeDisableUnsafePtrRestriction] public T* methods;
119 public void Execute(int index) => methods->MeasureNativeContainer(index, threadIndex);
120 }
121 [BurstCompile(CompileSynchronously = true, DisableSafetyChecks = true)]
122 unsafe struct NativeJobBurstMT : IJobParallelFor
123 {
124 [NativeSetThreadIndex] int threadIndex;
125 [NativeDisableUnsafePtrRestriction] public T* methods;
126 public void Execute(int index) => methods->MeasureNativeContainer(index, threadIndex);
127 }
128 [BurstCompile(CompileSynchronously = true, DisableSafetyChecks = false)]
129 unsafe struct NativeJobSafetyBurstMT : IJobParallelFor
130 {
131 [NativeSetThreadIndex] int threadIndex;
132 [NativeDisableUnsafePtrRestriction] public T* methods;
133 public void Execute(int index) => methods->MeasureNativeContainer(index, threadIndex);
134 }
135
136 unsafe struct UnsafeJobMT : IJobParallelFor
137 {
138 [NativeSetThreadIndex] int threadIndex;
139 [NativeDisableUnsafePtrRestriction] public T* methods;
140 public void Execute(int index) => methods->MeasureUnsafeContainer(index, threadIndex);
141 }
142 [BurstCompile(CompileSynchronously = true, DisableSafetyChecks = true)]
143 unsafe struct UnsafeJobBurstMT : IJobParallelFor
144 {
145 [NativeSetThreadIndex] int threadIndex;
146 [NativeDisableUnsafePtrRestriction] public T* methods;
147 public void Execute(int index) => methods->MeasureUnsafeContainer(index, threadIndex);
148 }
149 [BurstCompile(CompileSynchronously = true, DisableSafetyChecks = false)]
150 unsafe struct UnsafeJobSafetyBurstMT : IJobParallelFor
151 {
152 [NativeSetThreadIndex] int threadIndex;
153 [NativeDisableUnsafePtrRestriction] public T* methods;
154 public void Execute(int index) => methods->MeasureUnsafeContainer(index, threadIndex);
155 }
156
157 unsafe struct BclJobMT : IJobParallelFor
158 {
159 [NativeDisableUnsafePtrRestriction] public T* methods;
160 [NativeDisableUnsafePtrRestriction] public GCHandle* gcHandle;
161 public void Execute(int index) => methods->MeasureBclContainer(gcHandle->Target, index);
162 }
163
164 static unsafe void RunMT(int workers, int capacity, BenchmarkContainerType type, params int[] args)
165 {
166 var methods = new T();
167 methods.SetParams(capacity, args);
168
169 switch (type)
170 {
171 case (BenchmarkContainerType)(BenchmarkContainerConfig.BCL):
172 object container = null;
173 GCHandle* gcHandle = default;
174 BenchmarkMeasure.MeasureParallel(typeof(T),
175 BenchmarkContainerConfig.kCountWarmup, BenchmarkContainerConfig.kCountMeasure,
176 () => new BclJobMT { methods = (T*)UnsafeUtility.AddressOf(ref methods), gcHandle = gcHandle }.Schedule(workers, 1).Complete(),
177 () =>
178 {
179 container = methods.AllocBclContainer(capacity);
180 gcHandle = (GCHandle*)UnsafeUtility.Malloc(sizeof(GCHandle), 0, Allocator.Persistent);
181 *gcHandle = GCHandle.Alloc(container);
182 },
183 () =>
184 {
185 gcHandle->Free();
186 UnsafeUtility.Free(gcHandle, Allocator.Persistent);
187 container = methods.AllocBclContainer(-1);
188 });
189 break;
190 case BenchmarkContainerType.Native:
191 BenchmarkMeasure.MeasureParallel(typeof(T),
192 BenchmarkContainerConfig.kCountWarmup, BenchmarkContainerConfig.kCountMeasure,
193 () => new NativeJobMT { methods = (T*)UnsafeUtility.AddressOf(ref methods) }.Schedule(workers, 1).Complete(),
194 () => methods.AllocNativeContainer(capacity), () => methods.AllocNativeContainer(-1));
195 break;
196 case BenchmarkContainerType.NativeBurstSafety:
197 BenchmarkMeasure.MeasureParallel(typeof(T),
198 BenchmarkContainerConfig.kCountWarmup, BenchmarkContainerConfig.kCountMeasure,
199 () => new NativeJobSafetyBurstMT { methods = (T*)UnsafeUtility.AddressOf(ref methods) }.Schedule(workers, 1).Complete(),
200 () => methods.AllocNativeContainer(capacity), () => methods.AllocNativeContainer(-1));
201 break;
202 case BenchmarkContainerType.NativeBurstNoSafety:
203 BenchmarkMeasure.MeasureParallel(typeof(T),
204 BenchmarkContainerConfig.kCountWarmup, BenchmarkContainerConfig.kCountMeasure,
205 () => new NativeJobBurstMT { methods = (T*)UnsafeUtility.AddressOf(ref methods) }.Schedule(workers, 1).Complete(),
206 () => methods.AllocNativeContainer(capacity), () => methods.AllocNativeContainer(-1));
207 break;
208 case BenchmarkContainerType.Unsafe:
209 BenchmarkMeasure.Measure(typeof(T),
210 BenchmarkContainerConfig.kCountWarmup, BenchmarkContainerConfig.kCountMeasure,
211 () => new UnsafeJobMT { methods = (T*)UnsafeUtility.AddressOf(ref methods) }.Schedule(workers, 1).Complete(),
212 () => methods.AllocUnsafeContainer(capacity), () => methods.AllocUnsafeContainer(-1));
213 break;
214 case BenchmarkContainerType.UnsafeBurstSafety:
215 BenchmarkMeasure.Measure(typeof(T),
216 BenchmarkContainerConfig.kCountWarmup, BenchmarkContainerConfig.kCountMeasure,
217 () => new UnsafeJobSafetyBurstMT { methods = (T*)UnsafeUtility.AddressOf(ref methods) }.Schedule(workers, 1).Complete(),
218 () => methods.AllocUnsafeContainer(capacity), () => methods.AllocUnsafeContainer(-1));
219 break;
220 case BenchmarkContainerType.UnsafeBurstNoSafety:
221 BenchmarkMeasure.MeasureParallel(typeof(T),
222 BenchmarkContainerConfig.kCountWarmup, BenchmarkContainerConfig.kCountMeasure,
223 () => new UnsafeJobBurstMT { methods = (T*)UnsafeUtility.AddressOf(ref methods) }.Schedule(workers, 1).Complete(),
224 () => methods.AllocUnsafeContainer(capacity), () => methods.AllocUnsafeContainer(-1));
225 break;
226 }
227 }
228
229 /// <summary>
230 /// Called from a typical performance test method to provide both Performance Framework measurements as well as
231 /// Benchmark Framework measurements. A typical usage is similar to:
232 /// <c>[Test, Performance]<br />
233 /// [Category("Performance")]<br />
234 /// public unsafe void ToNativeArray(<br />
235 /// [Values(100000, 1000000, 10000000)] int capacity,<br />
236 /// [Values] BenchmarkContainerType type)<br />
237 /// {<br />
238 /// BenchmarkContainerRunner<HashSetToNativeArray>.RunST(capacity, type);<br />
239 /// }</c>
240 /// </summary>
241 /// <param name="capacity">The capacity for the container(s) which will be passed to setup methods</param>
242 /// <param name="type">The benchmark or performance measurement type to run for containers i.e. <see cref="BenchmarkContainerType.Native"/> etc.</param>
243 /// <param name="args">Optional arguments that can be stored in a test implementation class.</param>
244 /// <remarks>This will run measurements with <see cref="IJob"/> or directly called on the main thread.</remarks>
245 public static unsafe void Run(int capacity, BenchmarkContainerType type, params int[] args)
246 {
247 var methods = new T();
248 methods.SetParams(capacity, args);
249
250 switch (type)
251 {
252 case (BenchmarkContainerType)(BenchmarkContainerConfig.BCL):
253 object container = null;
254 BenchmarkMeasure.Measure(typeof(T),
255 BenchmarkContainerConfig.kCountWarmup, BenchmarkContainerConfig.kCountMeasure,
256 () => methods.MeasureBclContainer(container, 0),
257 () => container = methods.AllocBclContainer(capacity), () => container = methods.AllocBclContainer(-1));
258 break;
259 case BenchmarkContainerType.Native:
260 BenchmarkMeasure.Measure(typeof(T),
261 BenchmarkContainerConfig.kCountWarmup, BenchmarkContainerConfig.kCountMeasure,
262 () => methods.MeasureNativeContainer(0, 0),
263 () => methods.AllocNativeContainer(capacity), () => methods.AllocNativeContainer(-1));
264 break;
265 case BenchmarkContainerType.NativeBurstSafety:
266 BenchmarkMeasure.Measure(typeof(T),
267 BenchmarkContainerConfig.kCountWarmup, BenchmarkContainerConfig.kCountMeasure,
268 () => new NativeJobSafetyBurstST { methods = (T*)UnsafeUtility.AddressOf(ref methods) }.Run(),
269 () => methods.AllocNativeContainer(capacity), () => methods.AllocNativeContainer(-1));
270 break;
271 case BenchmarkContainerType.NativeBurstNoSafety:
272 BenchmarkMeasure.Measure(typeof(T),
273 BenchmarkContainerConfig.kCountWarmup, BenchmarkContainerConfig.kCountMeasure,
274 () => new NativeJobBurstST { methods = (T*)UnsafeUtility.AddressOf(ref methods) }.Run(),
275 () => methods.AllocNativeContainer(capacity), () => methods.AllocNativeContainer(-1));
276 break;
277 case BenchmarkContainerType.Unsafe:
278 BenchmarkMeasure.Measure(typeof(T),
279 BenchmarkContainerConfig.kCountWarmup, BenchmarkContainerConfig.kCountMeasure,
280 () => methods.MeasureUnsafeContainer(0, 0),
281 () => methods.AllocUnsafeContainer(capacity), () => methods.AllocUnsafeContainer(-1));
282 break;
283 case BenchmarkContainerType.UnsafeBurstSafety:
284 BenchmarkMeasure.Measure(typeof(T),
285 BenchmarkContainerConfig.kCountWarmup, BenchmarkContainerConfig.kCountMeasure,
286 () => new UnsafeJobSafetyBurstST { methods = (T*)UnsafeUtility.AddressOf(ref methods) }.Run(),
287 () => methods.AllocUnsafeContainer(capacity), () => methods.AllocUnsafeContainer(-1));
288 break;
289 case BenchmarkContainerType.UnsafeBurstNoSafety:
290 BenchmarkMeasure.Measure(typeof(T),
291 BenchmarkContainerConfig.kCountWarmup, BenchmarkContainerConfig.kCountMeasure,
292 () => new UnsafeJobBurstST { methods = (T*)UnsafeUtility.AddressOf(ref methods) }.Run(),
293 () => methods.AllocUnsafeContainer(capacity), () => methods.AllocUnsafeContainer(-1));
294 break;
295 }
296 }
297
298 /// <summary>
299 /// Called from a typical performance test method to provide both Performance Framework measurements as well as
300 /// Benchmark Framework measurements. A typical usage is similar to:
301 /// <c>[Test, Performance]<br />
302 /// [Category("Performance")]<br />
303 /// public unsafe void ToNativeArray(<br />
304 /// [Values(1, 2, 4, 8)] int workers,<br />
305 /// [Values(100000, 1000000, 10000000)] int capacity,<br />
306 /// [Values] BenchmarkContainerType type)<br />
307 /// {<br />
308 /// BenchmarkContainerRunner<HashSetToNativeArray>.Run(workers, capacity, type);<br />
309 /// }</c>
310 /// </summary>
311 /// <param name="workers">The number of job workers to run performance tests on. These are duplicated across workers rather than split across workers.</param>
312 /// <param name="capacity">The capacity for the container(s) which will be passed to setup methods</param>
313 /// <param name="type">The benchmark or performance measurement type to run for containers i.e. <see cref="BenchmarkContainerType.Native"/> etc.</param>
314 /// <param name="args">Optional arguments that can be stored in a test implementation class.</param>
315 /// <remarks>This will run measurements with <see cref="IJob"/> or <see cref="IJobParallelFor"/> based on the number of workers being 1 or 2+, respectively.</remarks>
316 public static unsafe void Run(int workers, int capacity, BenchmarkContainerType type, params int[] args)
317 {
318 if (workers == 1)
319 Run(capacity, type, args);
320 else
321 RunMT(workers, capacity, type, args);
322 }
323 }
324}