123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283 |
- using System;
- using System.Collections.Generic;
- using System.Runtime.CompilerServices;
- using System.Text;
- using Unity.PerformanceTesting.Runtime;
- using NUnit.Framework;
- using NUnit.Framework.Interfaces;
- using Unity.PerformanceTesting.Exceptions;
- using UnityEngine;
- using UnityEngine.TestRunner.NUnitExtensions;
-
- [assembly: InternalsVisibleTo("Unity.PerformanceTesting.Tests.Editor")]
- namespace Unity.PerformanceTesting
- {
- /// <summary>
- /// Represents active performance test as a singleton.
- /// </summary>
- [Serializable]
- public class PerformanceTest
- {
- /// <summary>
- /// Full name of the test.
- /// </summary>
- public string Name;
- /// <summary>
- /// Class name of the test.
- /// </summary>
- public string ClassName;
- /// <summary>
- /// Method name of the test.
- /// </summary>
- public string MethodName;
- /// <summary>
- /// Version of the test. Default "1".
- /// </summary>
- public string Version;
- /// <summary>
- /// List of categories assigned to the test.
- /// </summary>
- public List<string> Categories = new List<string>();
- /// <summary>
- /// List of sample groups assigned to the test.
- /// </summary>
- public List<SampleGroup> SampleGroups = new List<SampleGroup>();
- /// <summary>
- /// Singleton instance of active performance test.
- /// </summary>
- public static PerformanceTest Active { get; set; }
- private static List <IDisposable> m_Disposables = new List<IDisposable>(1024);
- internal static List<IDisposable> Disposables
- {
- get => m_Disposables;
- set => m_Disposables = value ?? new List<IDisposable>(1024);
- }
- PerformanceTestHelper m_PerformanceTestHelper;
-
- public static event Action OnTestEnded;
-
- /// <summary>
- /// Initializes a new performance test and assigns it as singleton.
- /// </summary>
- public PerformanceTest()
- {
- Active = this;
- }
-
- internal static void StartTest(ITest currentTest)
- {
- if (currentTest.IsSuite) return;
-
- var go = new GameObject("PerformanceTestHelper");
- go.hideFlags = HideFlags.HideAndDontSave;
- var performanceTestHelper = go.AddComponent<PerformanceTestHelper>();
-
- string methodName = currentTest.Name.Contains("(")
- ? currentTest.Name.Remove(currentTest.Name.IndexOf("(", StringComparison.Ordinal))
- : currentTest.Name;
-
- string className = currentTest.ClassName;
-
- var fullName = currentTest.MethodName != methodName ? $"{currentTest.ClassName}.{currentTest.MethodName}.{currentTest.Name}" : currentTest.FullName;
-
- var test = new PerformanceTest
- {
- Name = fullName,
- ClassName = className,
- MethodName = methodName,
- Categories = currentTest.GetAllCategoriesFromTest(),
- Version = GetVersion(currentTest),
- m_PerformanceTestHelper = performanceTestHelper
- };
-
- Active = test;
- performanceTestHelper.ActiveTest = test;
- }
-
- private static string GetVersion(ITest currentTest)
- {
- string version = "";
- var methodVersions = currentTest.Method.GetCustomAttributes<VersionAttribute>(false);
- var classVersion = currentTest.TypeInfo.Type.GetCustomAttributes(typeof(VersionAttribute), true);
-
- if (classVersion.Length > 0)
- version = ((VersionAttribute)classVersion[0]).Version + ".";
- if (methodVersions.Length > 0)
- version += methodVersions[0].Version;
- else
- version += "1";
-
- return version;
- }
-
- internal static void EndTest(ITest test)
- {
- if (test.IsSuite) return;
-
- if (Active.m_PerformanceTestHelper != null && Active.m_PerformanceTestHelper.gameObject != null)
- {
- UnityEngine.Object.DestroyImmediate(Active.m_PerformanceTestHelper.gameObject);
- }
-
- DisposeMeasurements();
- Active.CalculateStatisticalValues();
-
- try
- {
- // Notify subscribers that the test has ended by invoking OnTestEnded event
- OnTestEnded?.Invoke();
- }
- catch (Exception ex)
- {
- // An exception occurred while invoking the OnTestEnded event.
- // Log the error message, exception type, and stack trace for troubleshooting.
- Debug.LogError($"An exception occurred in OnTestEnd callback: {ex.GetType()}: {ex.Message}\n{ex.StackTrace}");
- }
- finally
- {
- // Regardless of whether the event invocation succeeded or not, perform cleanup
- // and finalize the test-related operations.
- PerformCleanupAndFinalization();
- }
- }
-
- internal static void PerformCleanupAndFinalization()
- {
- Active.LogOutput(); // Log test output
- TestContext.Out.WriteLine("##performancetestresult2:" + Active.Serialize()); // Log test result
- PlayerCallbacks.LogMetadata(); // Log metadata
- Active = null; // Clear active object
- GC.Collect(); // Trigger garbage collection to free resources
- }
-
- private static void DisposeMeasurements()
- {
- for (var i = 0; i < Disposables.Count; i++)
- {
- Disposables[i].Dispose();
- }
-
- Disposables.Clear();
- }
-
- /// <summary>
- /// Retrieves named sample group from active performance test.
- /// </summary>
- /// <param name="name">Name of sample group to retrieve.</param>
- /// <returns>Selected sample group.</returns>
- /// <exception cref="PerformanceTestException">Exception will be thrown if there is no active performance test.</exception>
- public static SampleGroup GetSampleGroup(string name)
- {
- if (Active == null) throw new PerformanceTestException("Trying to record samples but there is no active performance tests.");
- foreach (var sampleGroup in Active.SampleGroups)
- {
- if (sampleGroup.Name == name)
- return sampleGroup;
- }
-
- return null;
- }
-
- /// <summary>
- /// Adds sample group to active performance test.
- /// </summary>
- /// <param name="sampleGroup">Sample group to be added.</param>
- public static void AddSampleGroup(SampleGroup sampleGroup)
- {
- Active.SampleGroups.Add(sampleGroup);
- }
-
- internal string Serialize()
- {
- return JsonUtility.ToJson(Active);
- }
-
- /// <summary>
- /// Loops through sample groups and updates statistical values.
- /// </summary>
- public void CalculateStatisticalValues()
- {
- foreach (var sampleGroup in SampleGroups)
- {
- sampleGroup.UpdateStatistics();
- }
- }
-
- private void LogOutput()
- {
- TestContext.Out.WriteLine(ToString());
- }
-
- static void AppendVisualization(StringBuilder sb, IList<double> data, int n, double min, double max)
- {
- const string bars = "▁▂▃▄▅▆▇█";
- double range = max - min;
- for (int i = 0; i < n; i++)
- {
- var sample = data[i];
- int idx = Mathf.Clamp(Mathf.RoundToInt((float) ((sample - min) / range * (bars.Length - 1))), 0, bars.Length - 1);
- sb.Append(bars[idx]);
- }
- }
-
- private static double[] s_Buckets;
- static void AppendSampleHistogram(StringBuilder sb, SampleGroup s, int buckets)
- {
- if (s_Buckets == null || s_Buckets.Length < buckets)
- s_Buckets = new double[buckets];
- double maxInOneBucket = 0;
- double min = s.Min;
- double bucketsOverRange = (buckets - 1) / (s.Max - s.Min);
- for (int i = 0; i < s.Samples.Count; i++)
- {
- int bucket = Mathf.Clamp(Mathf.RoundToInt((float)((s.Samples[i] - min) * bucketsOverRange)), 0, buckets - 1);
- s_Buckets[bucket] += 1;
- if (s_Buckets[bucket] > maxInOneBucket)
- maxInOneBucket = s_Buckets[bucket];
- }
- AppendVisualization(sb, s_Buckets, s_Buckets.Length, 0, maxInOneBucket);
- }
-
- /// <summary>
- /// Returns performance test in a readable format.
- /// </summary>
- /// <returns>Readable representation of performance test.</returns>
- public override string ToString()
- {
- var logString = new StringBuilder();
-
- foreach (var s in SampleGroups)
- {
- logString.Append(s.Name);
-
- if (s.Samples.Count == 1)
- {
- logString.AppendLine($" {s.Samples[0]:0.00} {s.Unit}s");
- }
- else
- {
- string u = s.Unit.ShortName();
- 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}");
- logString.Append("First samples:\t");
- AppendVisualization(logString, s.Samples, Mathf.Min(s.Samples.Count, 100), s.Min, s.Max);
- logString.AppendLine();
- if (s.Samples.Count <= 512)
- {
- int numBuckets = Mathf.Min(10, s.Samples.Count / 4);
- if (numBuckets > 2)
- {
- logString.Append("Histogram:\t");
- AppendSampleHistogram(logString, s, numBuckets);
- logString.AppendLine();
- }
- else
- logString.Append("(not enough samples for histogram)\n");
- }
- logString.AppendLine();
- }
- }
-
- return logString.ToString();
- }
- }
- }
|