A game about forced loneliness, made by TACStudios
1using System;
2using System.Collections.Generic;
3using Unity.PerformanceTesting.Data;
4using Unity.PerformanceTesting.Runtime;
5using Unity.PerformanceTesting.Exceptions;
6using Unity.PerformanceTesting.Meters;
7using Unity.PerformanceTesting.Statistics;
8using UnityEngine;
9using UnityEngine.Profiling;
10
11namespace Unity.PerformanceTesting.Measurements
12{
13 /// <summary>
14 /// Used as a helper class to sample execution time of methods. Uses fluent pattern to build and needs to be executed with Run method.
15 /// </summary>
16 public class MethodMeasurement
17 {
18 internal const int k_MeasurementCount = 9;
19 private const int k_MinMeasurementTimeMs = 100;
20 private const int k_MinWarmupTimeMs = 100;
21 private const int k_ProbingMultiplier = 4;
22 private const int k_MaxIterations = 10000;
23 internal const int k_MaxDynamicMeasurements = 1000;
24 private const double k_DefaultMaxRelativeError = 0.02;
25 private const ConfidenceLevel k_DefaultConfidenceLevel = ConfidenceLevel.L99;
26 private const OutlierMode k_DefaultOutlierMode = OutlierMode.Remove;
27 private readonly Action m_Action;
28 private readonly List<SampleGroup> m_SampleGroups = new List<SampleGroup>();
29 private readonly Recorder m_GCRecorder;
30
31 private Action m_Setup;
32 private Action m_Cleanup;
33 private SampleGroup m_SampleGroup = new SampleGroup("Time", SampleUnit.Millisecond, false);
34 private SampleGroup m_SampleGroupGC = new SampleGroup("Time.GC()", SampleUnit.Undefined, false);
35 private int m_WarmupCount;
36 private int m_MeasurementCount;
37 internal bool m_DynamicMeasurementCount;
38 private double m_MaxRelativeError = k_DefaultMaxRelativeError;
39 private ConfidenceLevel m_ConfidenceLevel = k_DefaultConfidenceLevel;
40 private OutlierMode m_OutlierMode = k_DefaultOutlierMode;
41 private int m_IterationCount = 1;
42 private bool m_GC;
43 private IStopWatch m_Watch;
44
45 /// <summary>
46 /// Initializes a method measurement.
47 /// </summary>
48 /// <param name="action">Method to be measured.</param>
49 public MethodMeasurement(Action action)
50 {
51 m_Action = action;
52 m_GCRecorder = Recorder.Get("GC.Alloc");
53 m_GCRecorder.enabled = false;
54 if (m_Watch == null) m_Watch = new StopWatch();
55 }
56
57 internal MethodMeasurement StopWatch(IStopWatch watch)
58 {
59 m_Watch = watch;
60
61 return this;
62 }
63
64 /// <summary>
65 /// Will record provided profiler markers once per frame.
66 /// </summary>
67 /// <param name="profilerMarkerNames">Profiler marker names as in profiler window.</param>
68 /// <returns></returns>
69 public MethodMeasurement ProfilerMarkers(params string[] profilerMarkerNames)
70 {
71 if (profilerMarkerNames == null) return this;
72 foreach (var marker in profilerMarkerNames)
73 {
74 var sampleGroup = new SampleGroup(marker, SampleUnit.Nanosecond, false);
75 sampleGroup.GetRecorder();
76 sampleGroup.Recorder.enabled = false;
77 m_SampleGroups.Add(sampleGroup);
78 }
79
80 return this;
81 }
82
83 /// <summary>
84 /// Will record provided profiler markers once per frame with additional control over the SampleUnit.
85 /// </summary>
86 /// <param name="sampleGroups">List of SampleGroups where a name matches the profiler marker and desired SampleUnit.</param>
87 /// <returns></returns>
88 public MethodMeasurement ProfilerMarkers(params SampleGroup[] sampleGroups)
89 {
90 if (sampleGroups == null){ return this;}
91 foreach (var sampleGroup in sampleGroups)
92 {
93 sampleGroup.GetRecorder();
94 sampleGroup.Recorder.enabled = false;
95 m_SampleGroups.Add(sampleGroup);
96 }
97
98 return this;
99 }
100
101 /// <summary>
102 /// Overrides the default SampleGroup of "Time".
103 /// </summary>
104 /// <param name="name">Desired name for measurement SampleGroup.</param>
105 /// <returns></returns>
106 public MethodMeasurement SampleGroup(string name)
107 {
108 m_SampleGroup = new SampleGroup(name, SampleUnit.Millisecond, false);
109 m_SampleGroupGC = new SampleGroup(name + ".GC()", SampleUnit.Undefined, false);
110 return this;
111 }
112
113 /// <summary>
114 /// Overrides the default SampleGroup.
115 /// </summary>
116 /// <param name="sampleGroup">SampleGroup with your desired name and unit.</param>
117 /// <returns></returns>
118 public MethodMeasurement SampleGroup(SampleGroup sampleGroup)
119 {
120 m_SampleGroup = sampleGroup;
121 m_SampleGroupGC = new SampleGroup(sampleGroup.Name + ".GC()", SampleUnit.Undefined, false);
122 return this;
123 }
124
125 /// <summary>
126 /// Count of times to execute before measurements are collected. If unspecified, a default warmup will be assigned.
127 /// </summary>
128 /// <param name="count">Count of warmup iterations to execute.</param>
129 /// <returns></returns>
130 public MethodMeasurement WarmupCount(int count)
131 {
132 m_WarmupCount = count;
133 return this;
134 }
135
136 /// <summary>
137 /// Specifies the amount of method executions for a single measurement.
138 /// </summary>
139 /// <param name="count">Count of method executions.</param>
140 /// <returns></returns>
141 public MethodMeasurement IterationsPerMeasurement(int count)
142 {
143 m_IterationCount = count;
144 return this;
145 }
146
147 /// <summary>
148 /// Specifies the number of measurements to take.
149 /// </summary>
150 /// <param name="count">Count of measurements to take.</param>
151 /// <returns></returns>
152 public MethodMeasurement MeasurementCount(int count)
153 {
154 m_MeasurementCount = count;
155 return this;
156 }
157
158 /// <summary>
159 /// Dynamically find a suitable measurement count based on the margin of error of the samples.
160 /// The measurements will stop once a certain amount of samples (specified by a confidence interval)
161 /// falls within an acceptable error range from the result (defined by a relative error of the mean).
162 /// A default margin of error range of 2% and a default confidence interval of 99% will be used.
163 /// </summary>
164 /// <param name="outlierMode">Outlier mode allows to include or exclude outliers when evaluating the stop criterion.</param>
165 /// <returns></returns>
166 public MethodMeasurement DynamicMeasurementCount(OutlierMode outlierMode = k_DefaultOutlierMode)
167 {
168 m_DynamicMeasurementCount = true;
169 m_OutlierMode = outlierMode;
170 return this;
171 }
172
173 /// <summary>
174 /// Dynamically find a suitable measurement count based on the margin of error of the samples.
175 /// The measurements will stop once a certain amount of samples (specified by a confidence interval)
176 /// falls within an acceptable error range from the result (defined by a relative error of the mean).
177 /// </summary>
178 /// <param name="maxRelativeError">The maximum relative error of the mean that the margin of error must fall into.</param>
179 /// <param name="confidenceLevel">The confidence interval which will be used to calculate the margin of error.</param>
180 /// <param name="outlierMode">Outlier mode allows to include or exclude outliers when evaluating the stop criterion.</param>
181 /// <returns></returns>
182 public MethodMeasurement DynamicMeasurementCount(double maxRelativeError, ConfidenceLevel confidenceLevel = k_DefaultConfidenceLevel,
183 OutlierMode outlierMode = k_DefaultOutlierMode)
184 {
185 m_MaxRelativeError = maxRelativeError;
186 m_ConfidenceLevel = confidenceLevel;
187 m_DynamicMeasurementCount = true;
188 m_OutlierMode = outlierMode;
189 return this;
190 }
191
192 /// <summary>
193 /// Used to provide a cleanup method which will not be measured.
194 /// </summary>
195 /// <param name="action">Cleanup method to execute.</param>
196 /// <returns></returns>
197 public MethodMeasurement CleanUp(Action action)
198 {
199 m_Cleanup = action;
200 return this;
201 }
202
203 /// <summary>
204 /// Used to provide a setup method which will run before the measurement.
205 /// </summary>
206 /// <param name="action">Setup method to execute.</param>
207 /// <returns></returns>
208 public MethodMeasurement SetUp(Action action)
209 {
210 m_Setup = action;
211 return this;
212 }
213
214 /// <summary>
215 /// Enables recording of garbage collector calls.
216 /// </summary>
217 /// <returns></returns>
218 public MethodMeasurement GC()
219 {
220 m_GC = true;
221 return this;
222 }
223
224 /// <summary>
225 /// Executes the measurement with given parameters. When MeasurementCount is not provided, a probing method will run to determine desired measurement counts.
226 /// </summary>
227 public void Run()
228 {
229 ValidateCorrectDynamicMeasurementCountUsage();
230 SettingsOverride();
231 var settingsCount = RunSettings.Instance.MeasurementCount;
232
233 if (m_MeasurementCount > 0 || settingsCount > -1)
234 {
235 Warmup(m_WarmupCount);
236 RunForIterations(m_IterationCount, m_MeasurementCount, useAverage: false);
237 return;
238 }
239
240 if (m_DynamicMeasurementCount)
241 {
242 Warmup(m_WarmupCount);
243 RunForIterations(m_IterationCount);
244 return;
245 }
246
247 var iterations = Probing();
248 RunForIterations(iterations, k_MeasurementCount, useAverage: true);
249 }
250
251 private void ValidateCorrectDynamicMeasurementCountUsage()
252 {
253 if (!m_DynamicMeasurementCount)
254 return;
255
256 if (m_MeasurementCount > 0)
257 {
258 m_DynamicMeasurementCount = false;
259 Debug.LogWarning("DynamicMeasurementCount will be ignored because MeasurementCount was specified.");
260 }
261 }
262
263 /// <summary>
264 /// Overrides measurement count based on performance run settings
265 /// </summary>
266 private void SettingsOverride()
267 {
268 var count = RunSettings.Instance.MeasurementCount;
269 if (count < 0) { return; }
270 m_MeasurementCount = count;
271 m_WarmupCount = m_WarmupCount > 0 ? count : 0;
272 m_DynamicMeasurementCount = false;
273 }
274
275 private void RunForIterations(int iterations, int measurements, bool useAverage)
276 {
277 EnableMarkers();
278 for (var j = 0; j < measurements; j++)
279 {
280 var executionTime = iterations == 1 ? ExecuteSingleIteration() : ExecuteForIterations(iterations);
281 if (useAverage) executionTime /= iterations;
282 var delta = Utils.ConvertSample(SampleUnit.Millisecond, m_SampleGroup.Unit, executionTime);
283 Measure.Custom(m_SampleGroup, delta);
284 }
285
286 DisableAndMeasureMarkers();
287 }
288
289 private void RunForIterations(int iterations)
290 {
291 EnableMarkers();
292
293 while(true)
294 {
295 var executionTime = iterations == 1 ? ExecuteSingleIteration() : ExecuteForIterations(iterations);
296 var delta = Utils.ConvertSample(SampleUnit.Millisecond, m_SampleGroup.Unit, executionTime);
297 Measure.Custom(m_SampleGroup, delta);
298
299 if (SampleCountFulfillsRequirements())
300 break;
301 }
302
303 DisableAndMeasureMarkers();
304 }
305
306 private void EnableMarkers()
307 {
308 foreach (var sampleGroup in m_SampleGroups)
309 {
310 sampleGroup.Recorder.enabled = true;
311 }
312 }
313
314 private void DisableAndMeasureMarkers()
315 {
316 foreach (var sampleGroup in m_SampleGroups)
317 {
318 sampleGroup.Recorder.enabled = false;
319 var sample = sampleGroup.Recorder.elapsedNanoseconds;
320 var blockCount = sampleGroup.Recorder.sampleBlockCount;
321 if(blockCount == 0) continue;
322 var delta = Utils.ConvertSample(SampleUnit.Nanosecond, sampleGroup.Unit, sample);
323 Measure.Custom(sampleGroup, delta / blockCount);
324 }
325 }
326
327 private bool SampleCountFulfillsRequirements()
328 {
329 var samples = m_SampleGroup.Samples;
330 var sampleCount = samples.Count;
331 var statistics = MeasurementsStatistics.Calculate(samples, m_OutlierMode, m_ConfidenceLevel);
332 var actualError = statistics.MarginOfError;
333 var maxError = m_MaxRelativeError * statistics.Mean;
334
335 if (sampleCount >= k_MeasurementCount && actualError < maxError)
336 return true;
337
338 if (sampleCount >= k_MaxDynamicMeasurements)
339 return true;
340
341 return false;
342 }
343
344 private int Probing()
345 {
346 var executionTime = 0.0D;
347 var iterations = 1;
348
349 if (m_WarmupCount > 0)
350 throw new PerformanceTestException(
351 "Please provide MeasurementCount or remove WarmupCount in your usage of Measure.Method");
352
353 while (executionTime < k_MinWarmupTimeMs)
354 {
355 executionTime = m_Watch.Split();
356 Warmup(iterations);
357 executionTime = m_Watch.Split() - executionTime;
358
359 if (executionTime < k_MinWarmupTimeMs)
360 {
361 iterations *= k_ProbingMultiplier;
362 }
363 }
364
365 if (iterations == 1)
366 {
367 ExecuteActionWithCleanupSetup();
368 ExecuteActionWithCleanupSetup();
369
370 return 1;
371 }
372
373 var deisredIterationsCount =
374 Mathf.Clamp((int) (k_MinMeasurementTimeMs * iterations / executionTime), 1, k_MaxIterations);
375
376 return deisredIterationsCount;
377 }
378
379 private void Warmup(int iterations)
380 {
381 for (var i = 0; i < iterations; i++)
382 {
383 ExecuteForIterations(m_IterationCount);
384 }
385 }
386
387 private double ExecuteActionWithCleanupSetup()
388 {
389 m_Setup?.Invoke();
390
391 var executionTime = m_Watch.Split();
392 m_Action.Invoke();
393 executionTime = m_Watch.Split() - executionTime;
394
395 m_Cleanup?.Invoke();
396
397 return executionTime;
398 }
399
400 private double ExecuteSingleIteration()
401 {
402 if (m_GC) StartGCRecorder();
403 m_Setup?.Invoke();
404
405 var executionTime = m_Watch.Split();
406 m_Action.Invoke();
407 executionTime = m_Watch.Split() - executionTime;
408
409 m_Cleanup?.Invoke();
410 if (m_GC) EndGCRecorderAndMeasure(1);
411 return executionTime;
412 }
413
414 private double ExecuteForIterations(int iterations)
415 {
416 if (m_GC) StartGCRecorder();
417 var executionTime = 0.0D;
418
419 if (m_Cleanup != null || m_Setup != null)
420 {
421 for (var i = 0; i < iterations; i++)
422 {
423 executionTime += ExecuteActionWithCleanupSetup();
424 }
425 }
426 else
427 {
428 executionTime = m_Watch.Split();
429 for (var i = 0; i < iterations; i++)
430 {
431 m_Action.Invoke();
432 }
433
434 executionTime = m_Watch.Split() - executionTime;
435 }
436
437 if (m_GC) EndGCRecorderAndMeasure(iterations);
438 return executionTime;
439 }
440
441 private void StartGCRecorder()
442 {
443 System.GC.Collect();
444
445 m_GCRecorder.enabled = false;
446 m_GCRecorder.enabled = true;
447 }
448
449 private void EndGCRecorderAndMeasure(int iterations)
450 {
451 m_GCRecorder.enabled = false;
452
453 Measure.Custom(m_SampleGroupGC, (double) m_GCRecorder.sampleBlockCount / iterations);
454 }
455 }
456}