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}