A game about forced loneliness, made by TACStudios
1using System; 2using System.Collections.Generic; 3using System.Runtime.CompilerServices; 4using System.Text; 5using Unity.PerformanceTesting.Runtime; 6using NUnit.Framework; 7using NUnit.Framework.Interfaces; 8using Unity.PerformanceTesting.Exceptions; 9using UnityEngine; 10using UnityEngine.TestRunner.NUnitExtensions; 11 12[assembly: InternalsVisibleTo("Unity.PerformanceTesting.Tests.Editor")] 13namespace Unity.PerformanceTesting 14{ 15 /// <summary> 16 /// Represents active performance test as a singleton. 17 /// </summary> 18 [Serializable] 19 public class PerformanceTest 20 { 21 /// <summary> 22 /// Full name of the test. 23 /// </summary> 24 public string Name; 25 /// <summary> 26 /// Class name of the test. 27 /// </summary> 28 public string ClassName; 29 /// <summary> 30 /// Method name of the test. 31 /// </summary> 32 public string MethodName; 33 /// <summary> 34 /// Version of the test. Default "1". 35 /// </summary> 36 public string Version; 37 /// <summary> 38 /// List of categories assigned to the test. 39 /// </summary> 40 public List<string> Categories = new List<string>(); 41 /// <summary> 42 /// List of sample groups assigned to the test. 43 /// </summary> 44 public List<SampleGroup> SampleGroups = new List<SampleGroup>(); 45 /// <summary> 46 /// Singleton instance of active performance test. 47 /// </summary> 48 public static PerformanceTest Active { get; set; } 49 private static List <IDisposable> m_Disposables = new List<IDisposable>(1024); 50 internal static List<IDisposable> Disposables 51 { 52 get => m_Disposables; 53 set => m_Disposables = value ?? new List<IDisposable>(1024); 54 } 55 PerformanceTestHelper m_PerformanceTestHelper; 56 57 public static event Action OnTestEnded; 58 59 /// <summary> 60 /// Initializes a new performance test and assigns it as singleton. 61 /// </summary> 62 public PerformanceTest() 63 { 64 Active = this; 65 } 66 67 internal static void StartTest(ITest currentTest) 68 { 69 if (currentTest.IsSuite) return; 70 71 var go = new GameObject("PerformanceTestHelper"); 72 go.hideFlags = HideFlags.HideAndDontSave; 73 var performanceTestHelper = go.AddComponent<PerformanceTestHelper>(); 74 75 string methodName = currentTest.Name.Contains("(") 76 ? currentTest.Name.Remove(currentTest.Name.IndexOf("(", StringComparison.Ordinal)) 77 : currentTest.Name; 78 79 string className = currentTest.ClassName; 80 81 var fullName = currentTest.MethodName != methodName ? $"{currentTest.ClassName}.{currentTest.MethodName}.{currentTest.Name}" : currentTest.FullName; 82 83 var test = new PerformanceTest 84 { 85 Name = fullName, 86 ClassName = className, 87 MethodName = methodName, 88 Categories = currentTest.GetAllCategoriesFromTest(), 89 Version = GetVersion(currentTest), 90 m_PerformanceTestHelper = performanceTestHelper 91 }; 92 93 Active = test; 94 performanceTestHelper.ActiveTest = test; 95 } 96 97 private static string GetVersion(ITest currentTest) 98 { 99 string version = ""; 100 var methodVersions = currentTest.Method.GetCustomAttributes<VersionAttribute>(false); 101 var classVersion = currentTest.TypeInfo.Type.GetCustomAttributes(typeof(VersionAttribute), true); 102 103 if (classVersion.Length > 0) 104 version = ((VersionAttribute)classVersion[0]).Version + "."; 105 if (methodVersions.Length > 0) 106 version += methodVersions[0].Version; 107 else 108 version += "1"; 109 110 return version; 111 } 112 113 internal static void EndTest(ITest test) 114 { 115 if (test.IsSuite) return; 116 117 if (Active.m_PerformanceTestHelper != null && Active.m_PerformanceTestHelper.gameObject != null) 118 { 119 UnityEngine.Object.DestroyImmediate(Active.m_PerformanceTestHelper.gameObject); 120 } 121 122 DisposeMeasurements(); 123 Active.CalculateStatisticalValues(); 124 125 try 126 { 127 // Notify subscribers that the test has ended by invoking OnTestEnded event 128 OnTestEnded?.Invoke(); 129 } 130 catch (Exception ex) 131 { 132 // An exception occurred while invoking the OnTestEnded event. 133 // Log the error message, exception type, and stack trace for troubleshooting. 134 Debug.LogError($"An exception occurred in OnTestEnd callback: {ex.GetType()}: {ex.Message}\n{ex.StackTrace}"); 135 } 136 finally 137 { 138 // Regardless of whether the event invocation succeeded or not, perform cleanup 139 // and finalize the test-related operations. 140 PerformCleanupAndFinalization(); 141 } 142 } 143 144 internal static void PerformCleanupAndFinalization() 145 { 146 Active.LogOutput(); // Log test output 147 TestContext.Out.WriteLine("##performancetestresult2:" + Active.Serialize()); // Log test result 148 PlayerCallbacks.LogMetadata(); // Log metadata 149 Active = null; // Clear active object 150 GC.Collect(); // Trigger garbage collection to free resources 151 } 152 153 private static void DisposeMeasurements() 154 { 155 for (var i = 0; i < Disposables.Count; i++) 156 { 157 Disposables[i].Dispose(); 158 } 159 160 Disposables.Clear(); 161 } 162 163 /// <summary> 164 /// Retrieves named sample group from active performance test. 165 /// </summary> 166 /// <param name="name">Name of sample group to retrieve.</param> 167 /// <returns>Selected sample group.</returns> 168 /// <exception cref="PerformanceTestException">Exception will be thrown if there is no active performance test.</exception> 169 public static SampleGroup GetSampleGroup(string name) 170 { 171 if (Active == null) throw new PerformanceTestException("Trying to record samples but there is no active performance tests."); 172 foreach (var sampleGroup in Active.SampleGroups) 173 { 174 if (sampleGroup.Name == name) 175 return sampleGroup; 176 } 177 178 return null; 179 } 180 181 /// <summary> 182 /// Adds sample group to active performance test. 183 /// </summary> 184 /// <param name="sampleGroup">Sample group to be added.</param> 185 public static void AddSampleGroup(SampleGroup sampleGroup) 186 { 187 Active.SampleGroups.Add(sampleGroup); 188 } 189 190 internal string Serialize() 191 { 192 return JsonUtility.ToJson(Active); 193 } 194 195 /// <summary> 196 /// Loops through sample groups and updates statistical values. 197 /// </summary> 198 public void CalculateStatisticalValues() 199 { 200 foreach (var sampleGroup in SampleGroups) 201 { 202 sampleGroup.UpdateStatistics(); 203 } 204 } 205 206 private void LogOutput() 207 { 208 TestContext.Out.WriteLine(ToString()); 209 } 210 211 static void AppendVisualization(StringBuilder sb, IList<double> data, int n, double min, double max) 212 { 213 const string bars = "▁▂▃▄▅▆▇█"; 214 double range = max - min; 215 for (int i = 0; i < n; i++) 216 { 217 var sample = data[i]; 218 int idx = Mathf.Clamp(Mathf.RoundToInt((float) ((sample - min) / range * (bars.Length - 1))), 0, bars.Length - 1); 219 sb.Append(bars[idx]); 220 } 221 } 222 223 private static double[] s_Buckets; 224 static void AppendSampleHistogram(StringBuilder sb, SampleGroup s, int buckets) 225 { 226 if (s_Buckets == null || s_Buckets.Length < buckets) 227 s_Buckets = new double[buckets]; 228 double maxInOneBucket = 0; 229 double min = s.Min; 230 double bucketsOverRange = (buckets - 1) / (s.Max - s.Min); 231 for (int i = 0; i < s.Samples.Count; i++) 232 { 233 int bucket = Mathf.Clamp(Mathf.RoundToInt((float)((s.Samples[i] - min) * bucketsOverRange)), 0, buckets - 1); 234 s_Buckets[bucket] += 1; 235 if (s_Buckets[bucket] > maxInOneBucket) 236 maxInOneBucket = s_Buckets[bucket]; 237 } 238 AppendVisualization(sb, s_Buckets, s_Buckets.Length, 0, maxInOneBucket); 239 } 240 241 /// <summary> 242 /// Returns performance test in a readable format. 243 /// </summary> 244 /// <returns>Readable representation of performance test.</returns> 245 public override string ToString() 246 { 247 var logString = new StringBuilder(); 248 249 foreach (var s in SampleGroups) 250 { 251 logString.Append(s.Name); 252 253 if (s.Samples.Count == 1) 254 { 255 logString.AppendLine($" {s.Samples[0]:0.00} {s.Unit}s"); 256 } 257 else 258 { 259 string u = s.Unit.ShortName(); 260 logString.AppendLine($" in {s.Unit}s\nMin:\t\t{s.Min:0.00} {u}\nMedian:\t\t{s.Median:0.00} {u}\nMax:\t\t{s.Max:0.00} {u}\nAvg:\t\t{s.Average:0.00} {u}\nStdDev:\t\t{s.StandardDeviation:0.00} {u}\nSampleCount:\t{s.Samples.Count}\nSum:\t\t{s.Sum:0.00} {u}"); 261 logString.Append("First samples:\t"); 262 AppendVisualization(logString, s.Samples, Mathf.Min(s.Samples.Count, 100), s.Min, s.Max); 263 logString.AppendLine(); 264 if (s.Samples.Count <= 512) 265 { 266 int numBuckets = Mathf.Min(10, s.Samples.Count / 4); 267 if (numBuckets > 2) 268 { 269 logString.Append("Histogram:\t"); 270 AppendSampleHistogram(logString, s, numBuckets); 271 logString.AppendLine(); 272 } 273 else 274 logString.Append("(not enough samples for histogram)\n"); 275 } 276 logString.AppendLine(); 277 } 278 } 279 280 return logString.ToString(); 281 } 282 } 283}