No Description
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

PerformanceTest.cs 10KB

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