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}