Initial project commit

This commit is contained in:
2026-01-08 16:50:20 +00:00
commit f0c5a8b267
29596 changed files with 4861782 additions and 0 deletions

View File

@@ -0,0 +1,6 @@
using System.Reflection;
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Unity.PerformanceTesting.Tests.Editor")]
[assembly: AssemblyVersion("3.2.0")]

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: fd4abc6933ef4db296c17e31d92d800b
timeCreated: 1617350583

View File

@@ -0,0 +1,61 @@
using System;
using System.IO;
using UnityEditor.TestTools.TestRunner.Api;
using UnityEngine;
namespace Unity.PerformanceTesting.Editor
{
[Serializable]
class CmdLineResultsSavingCallbacks : ScriptableObject, ICallbacks
{
[SerializeField]
string resultsLocation;
void ICallbacks.RunStarted(ITestAdaptor testsToRun)
{
PerformanceTest.Active = null;
}
void ICallbacks.RunFinished(ITestResultAdaptor result)
{
PlayerCallbacks.Saved = false;
try
{
var performanceTestRun = TestResultsParser.GetPerformanceTestRunData(result);
if (performanceTestRun == null)
{
return;
}
Debug.LogFormat(LogType.Log, LogOption.NoStacktrace, null, "Saving performance results to: {0}", resultsLocation);
var jsonContents = JsonUtility.ToJson(performanceTestRun, true);
CreateDirectoryIfNecessary(resultsLocation);
File.WriteAllText(resultsLocation, jsonContents);
}
catch (Exception e)
{
Debug.LogError("Saving performance results file failed.");
Debug.LogException(e);
}
}
static void CreateDirectoryIfNecessary(string filePath)
{
var directoryPath = Path.GetDirectoryName(filePath);
if (!string.IsNullOrEmpty(directoryPath) && !Directory.Exists(directoryPath))
{
Directory.CreateDirectory(directoryPath);
}
}
void ICallbacks.TestStarted(ITestAdaptor test) { }
void ICallbacks.TestFinished(ITestResultAdaptor result) { }
public void SetResultsLocation(string perfTestResultsPath)
{
resultsLocation = perfTestResultsPath;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 65a435b219724f1c9e25b4f8d4564bb3
timeCreated: 1742825901

View File

@@ -0,0 +1,24 @@
using System.Linq;
using UnityEditor;
using UnityEditor.Build;
namespace Unity.PerformanceTesting.Editor
{
internal class PerformanceTestBuildAssemblyFilter : IFilterBuildAssemblies
{
private const string unityTestRunnerAssemblyName = "Unity.PerformanceTesting";
public int callbackOrder { get; }
public string[] OnFilterAssemblies(BuildOptions buildOptions, string[] assemblies)
{
if ((buildOptions & BuildOptions.IncludeTestAssemblies) == BuildOptions.IncludeTestAssemblies)
{
return assemblies;
}
return assemblies.Where(x => !x.Contains(unityTestRunnerAssemblyName))
.ToArray();
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6b7e8a2b3c6739c44a35905b9331e9b4
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,43 @@
using System;
using System.IO;
using Unity.PerformanceTesting.Runtime;
using UnityEngine;
using UnityEditor.TestTools.TestRunner.Api;
using UnityEditor.TestTools.TestRunner.CommandLineTest;
namespace Unity.PerformanceTesting.Editor
{
[Serializable]
internal class PerformanceTestRunSaver : ScriptableObject, ICallbacks
{
void ICallbacks.RunStarted(ITestAdaptor testsToRun)
{
PerformanceTest.Active = null;
}
void ICallbacks.RunFinished(ITestResultAdaptor result)
{
PlayerCallbacks.Saved = false;
try
{
var resultWriter = new ResultsWriter();
var xmlPath = Path.Combine(Application.persistentDataPath, "TestResults.xml");
var jsonPath = Path.Combine(Application.persistentDataPath, "PerformanceTestResults.json");
resultWriter.WriteResultToFile(result, xmlPath);
var xmlParser = new TestResultXmlParser();
var run = xmlParser.GetPerformanceTestRunFromXml(xmlPath);
if (run == null) return;
File.WriteAllText(jsonPath, JsonUtility.ToJson(run, true));
}
catch (Exception e)
{
Console.WriteLine(e.Message + "\n" + e.InnerException);
}
}
void ICallbacks.TestStarted(ITestAdaptor test) { }
void ICallbacks.TestFinished(ITestResultAdaptor result) { }
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 62eec8a012ecc7c4a8b5a38b5dc130e4
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 30bb01dc09cb89b4faf344490fee76a7
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,11 @@
namespace Unity.PerformanceTesting.Editor
{
internal struct SampleGroupAdditionalData
{
public float min;
public float lowerQuartile;
public float median;
public float upperQuartile;
public float max;
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c6efe3fc0e07c46468863dfd0a15978d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,21 @@
using System;
namespace Unity.PerformanceTesting.Editor
{
internal struct SamplePoint : IComparable<SamplePoint>
{
public double sample;
public int index;
public SamplePoint(double _sample, int _index)
{
sample = _sample;
index = _index;
}
public int CompareTo(SamplePoint other)
{
return sample.CompareTo(other.sample);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: dd6eec692697e8f4bbd715f4d546a329
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,457 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEditor.IMGUI.Controls;
using UnityEngine;
using UnityEngine.Assertions;
using Unity.PerformanceTesting.Data;
#if UNITY_6000_2_OR_NEWER
using TreeView = UnityEditor.IMGUI.Controls.TreeView<int>;
using TreeViewItem = UnityEditor.IMGUI.Controls.TreeViewItem<int>;
using TreeViewState = UnityEditor.IMGUI.Controls.TreeViewState<int>;
#endif
namespace Unity.PerformanceTesting.Editor
{
internal class TestListTableItem : TreeViewItem
{
public PerformanceTestResult performanceTest;
public double deviation;
public double standardDeviation;
public double median;
public double min;
public double max;
public TestListTableItem(int id, int depth, string displayName, PerformanceTestResult performanceTest)
: base(id, depth,
displayName)
{
this.performanceTest = performanceTest;
deviation = 0f;
if (performanceTest != null)
{
foreach (var sampleGroup in performanceTest.SampleGroups)
{
var thisDeviation =
sampleGroup.Median == 0 ? 0 : sampleGroup.StandardDeviation / sampleGroup.Median;
if (sampleGroup.Name == "Time" || sampleGroup.Samples.Count <= 1 || thisDeviation > deviation)
{
standardDeviation = sampleGroup.StandardDeviation;
median = sampleGroup.Median;
min = sampleGroup.Min;
max = sampleGroup.Max;
deviation = thisDeviation;
break;
}
}
}
}
}
internal class TestListTable : TreeView
{
TestReportWindow m_testReportWindow;
const float kRowHeights = 20f;
readonly List<TreeViewItem> m_Rows = new List<TreeViewItem>(100);
// All columns
public enum MyColumns
{
Name,
SampleCount,
StandardDeviation,
Deviation,
Median,
Min,
Max,
}
public enum SortOption
{
Name,
SampleCount,
StandardDeviation,
Deviation,
Median,
Min,
Max,
}
// Sort options per column
SortOption[] m_SortOptions =
{
SortOption.Name,
SortOption.SampleCount,
SortOption.StandardDeviation,
SortOption.Deviation,
SortOption.Median,
SortOption.Min,
SortOption.Max,
};
public TestListTable(TreeViewState state, MultiColumnHeader multicolumnHeader,
TestReportWindow testReportWindow)
: base(state, multicolumnHeader)
{
m_testReportWindow = testReportWindow;
Assert.AreEqual(m_SortOptions.Length, Enum.GetValues(typeof(MyColumns)).Length,
"Ensure number of sort options are in sync with number of MyColumns enum values");
// Custom setup
rowHeight = kRowHeights;
showAlternatingRowBackgrounds = true;
showBorder = true;
customFoldoutYOffset =
(kRowHeights - EditorGUIUtility.singleLineHeight) *
0.5f; // center foldout in the row since we also center content. See RowGUI
// extraSpaceBeforeIconAndLabel = 0;
multicolumnHeader.sortingChanged += OnSortingChanged;
Reload();
}
protected override TreeViewItem BuildRoot()
{
var root = new TestListTableItem(-1, -1, "root", null);
var results = m_testReportWindow.GetResults();
var searchText = m_testReportWindow.searchString?.ToLower();
var methodItemId = 0;
if (results == null) return root;
var classItemId = results.Results.Count(); // Class ID will always be bigger than the count of results (from methods) we get
var classGroups = results.Results.GroupBy(r => r.ClassName);
foreach (var classGroup in classGroups)
{
var classItem = new TestListTableItem(classItemId++, 0, classGroup.Key, null);
root.AddChild(classItem);
// Create method items and add them to the class item if they match the search text
// Currently Name provides both MethodName and Test parameter - example would be - ValueSource(Cube)
// We are using Name and simply removing the ClassPart as we need to keep that parameter
var methodItems = classGroup
.Select(r =>
{
// Calculate the starting index for the substring to extract methodname from full name
int startIndex = r.Name.IndexOf(classGroup.Key, StringComparison.Ordinal) + classGroup.Key.Length + 1;
// Check if the calculated startIndex is within the bounds of the string
// If it's within bounds, extract the substring; otherwise, return full name
string methodName = startIndex < r.Name.Length ? r.Name.Substring(startIndex) : r.Name;
// Create a TestListTableItem using the extracted methodName and other parameters
return new TestListTableItem(methodItemId++, 1, methodName, r);
});
foreach (var methodItem in methodItems)
{
var matchesSearchText = string.IsNullOrEmpty(searchText) ||
methodItem.displayName.ToLower().Contains(searchText);
if (matchesSearchText) classItem.AddChild(methodItem);
}
}
// Remove class items with no children
root.children?.RemoveAll(c => !c.hasChildren);
return root;
}
protected override IList<TreeViewItem> BuildRows(TreeViewItem root)
{
m_Rows.Clear();
if (rootItem != null && rootItem.children != null)
{
m_Rows.AddRange(base.BuildRows(root));
}
SortIfNeeded(m_Rows);
return m_Rows;
}
void OnSortingChanged(MultiColumnHeader _multiColumnHeader)
{
SortIfNeeded(GetRows());
}
void SortIfNeeded(IList<TreeViewItem> rows)
{
if (rows.Count <= 1)
{
return;
}
if (multiColumnHeader.sortedColumnIndex == -1)
{
return; // No column to sort for (just use the order the data are in)
}
// Sort the roots of the existing tree items
SortByMultipleColumns();
// Update the data with the sorted content
rows.Clear();
AddExpandedRows(rootItem, rows);
Repaint();
}
void SortByMultipleColumns()
{
var sortedColumns = multiColumnHeader.state.sortedColumns;
if (sortedColumns.Length == 0)
{
return;
}
var classItems = rootItem.children;
foreach (var classItem in classItems)
{
var myTypes = classItem.children.Cast<TestListTableItem>();
var orderedQuery = InitialOrder(myTypes, sortedColumns);
for (int i = 1; i < sortedColumns.Length; i++)
{
SortOption sortOption = m_SortOptions[sortedColumns[i]];
bool ascending = multiColumnHeader.IsSortedAscending(sortedColumns[i]);
switch (sortOption)
{
case SortOption.Name:
orderedQuery = orderedQuery.ThenBy(l => l.displayName, ascending);
break;
case SortOption.SampleCount:
orderedQuery = orderedQuery.ThenBy(l => l.performanceTest.SampleGroups.Count, ascending);
break;
case SortOption.Deviation:
orderedQuery = orderedQuery.ThenBy(l => l.deviation, ascending);
break;
case SortOption.StandardDeviation:
orderedQuery = orderedQuery.ThenBy(l => l.standardDeviation, ascending);
break;
case SortOption.Median:
orderedQuery = orderedQuery.ThenBy(l => l.median, ascending);
break;
case SortOption.Min:
orderedQuery = orderedQuery.ThenBy(l => l.min, ascending);
break;
case SortOption.Max:
orderedQuery = orderedQuery.ThenBy(l => l.max, ascending);
break;
}
}
classItem.children = orderedQuery.Cast<TreeViewItem>().ToList();
}
}
IOrderedEnumerable<TestListTableItem> InitialOrder(IEnumerable<TestListTableItem> myTypes, int[] history)
{
SortOption sortOption = m_SortOptions[history[0]];
bool ascending = multiColumnHeader.IsSortedAscending(history[0]);
switch (sortOption)
{
case SortOption.Name:
return myTypes.Order(l => l.displayName, ascending);
case SortOption.SampleCount:
return myTypes.Order(l => l.performanceTest.SampleGroups.Count, ascending);
case SortOption.Deviation:
return myTypes.Order(l => l.deviation, ascending);
case SortOption.StandardDeviation:
return myTypes.Order(l => l.standardDeviation, ascending);
case SortOption.Median:
return myTypes.Order(l => l.median, ascending);
case SortOption.Min:
return myTypes.Order(l => l.min, ascending);
case SortOption.Max:
return myTypes.Order(l => l.max, ascending);
default:
Assert.IsTrue(false, "Unhandled enum");
break;
}
// default
return myTypes.Order(l => l.displayName, ascending);
}
protected override void RowGUI(RowGUIArgs args)
{
var item = (TestListTableItem)args.item;
var nameRect = args.GetCellRect(0);
nameRect.x += GetContentIndent(item);
nameRect.xMax -= GetContentIndent(item);
if (item.depth == 0)
{
EditorGUI.LabelField(nameRect, item.displayName);
}
if (item.depth == 1)
{
for (var i = 0; i < args.GetNumVisibleColumns(); ++i)
{
var column = (MyColumns)args.GetColumn(i);
if (column == MyColumns.Name)
{
EditorGUI.LabelField(nameRect, item.displayName);
}
else
{
CellGUI(args.GetCellRect(i), item, column);
}
}
}
}
void CellGUI(Rect cellRect, TestListTableItem item, MyColumns column)
{
// Center cell rect vertically (makes it easier to place controls, icons etc in the cells)
CenterRectUsingSingleLineHeight(ref cellRect);
switch (column)
{
case MyColumns.SampleCount:
EditorGUI.LabelField(cellRect, $"{item.performanceTest.SampleGroups.Count}");
break;
case MyColumns.Deviation:
EditorGUI.LabelField(cellRect, $"{item.deviation:f2}");
break;
case MyColumns.StandardDeviation:
EditorGUI.LabelField(cellRect, $"{item.standardDeviation:f2}");
break;
case MyColumns.Median:
EditorGUI.LabelField(cellRect, $"{item.median:f2}");
break;
case MyColumns.Min:
EditorGUI.LabelField(cellRect, $"{item.min:f2}");
break;
case MyColumns.Max:
EditorGUI.LabelField(cellRect, $"{item.max:f2}");
break;
}
}
// Misc
//--------
protected override bool CanMultiSelect(TreeViewItem item)
{
return false;
}
struct HeaderData
{
public readonly GUIContent content;
public readonly float width;
public readonly float minWidth;
public readonly bool autoResize;
public readonly bool allowToggleVisibility;
public readonly bool ascending;
public HeaderData(string name, string tooltip = "", float width = 100, float minWidth = 50, bool autoResize = true, bool allowToggleVisibility = true, bool ascending = false)
{
content = new GUIContent(name, tooltip);
this.width = width;
this.minWidth = minWidth;
this.autoResize = autoResize;
this.allowToggleVisibility = allowToggleVisibility;
this.ascending = ascending;
}
}
public static MultiColumnHeaderState CreateDefaultMultiColumnHeaderState(float treeViewWidth)
{
var columnList = new List<MultiColumnHeaderState.Column>();
HeaderData[] headerData = new HeaderData[]
{
new HeaderData("Name", "Name of test", width : 500, minWidth : 100, autoResize : false, allowToggleVisibility : false, ascending : true),
new HeaderData("Groups", "Number of Sample Groups", width : 60, minWidth : 50),
new HeaderData("SD", "Standard Deviation"), // (of sample group with largest deviation)
new HeaderData("Deviation", "Standard Deviation / Median"),
new HeaderData("Median", "Median value"),
new HeaderData("Min", "Min value"),
new HeaderData("Max", "Max value"),
};
foreach (var header in headerData)
{
columnList.Add(new MultiColumnHeaderState.Column
{
headerContent = header.content,
headerTextAlignment = TextAlignment.Left,
sortedAscending = header.ascending,
sortingArrowAlignment = TextAlignment.Left,
width = header.width,
minWidth = header.minWidth,
autoResize = header.autoResize,
allowToggleVisibility = header.allowToggleVisibility
});
};
var columns = columnList.ToArray();
Assert.AreEqual(columns.Length, Enum.GetValues(typeof(MyColumns)).Length,
"Number of columns should match number of enum values: You probably forgot to update one of them.");
var state = new MultiColumnHeaderState(columns);
state.visibleColumns = new int[]
{
(int)MyColumns.Name,
(int)MyColumns.SampleCount,
(int)MyColumns.Deviation,
(int)MyColumns.StandardDeviation,
(int)MyColumns.Median,
(int)MyColumns.Min,
(int)MyColumns.Max,
};
return state;
}
protected override void SelectionChanged(IList<int> selectedIds)
{
base.SelectionChanged(selectedIds);
if (selectedIds.Count > 0)
m_testReportWindow.SelectTest(selectedIds[0]);
}
}
internal static class MyExtensionMethods
{
public static IOrderedEnumerable<T> Order<T, TKey>(this IEnumerable<T> source, Func<T, TKey> selector,
bool ascending)
{
if (ascending)
{
return source.OrderBy(selector);
}
else
{
return source.OrderByDescending(selector);
}
}
public static IOrderedEnumerable<T> ThenBy<T, TKey>(this IOrderedEnumerable<T> source, Func<T, TKey> selector,
bool ascending)
{
if (ascending)
{
return source.ThenBy(selector);
}
else
{
return source.ThenByDescending(selector);
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 242480b4ab9580c44b35d75dfa073192
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,47 @@
Shader "Unlit/TestReportShader"
{
Properties
{
}
SubShader
{
Tags { "RenderType"="Transparent" }
LOD 100
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
Pass
{
CGPROGRAM
#pragma target 2.0
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
fixed4 color : COLOR;
};
struct v2f
{
float4 vertex : SV_POSITION;
fixed4 color : COLOR;
};
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.color.rgba = v.color;
return o;
}
fixed4 frag (v2f i) : SV_Target { return i.color; }
ENDCG
}
}
}

View File

@@ -0,0 +1,9 @@
fileFormatVersion: 2
guid: c7715b638c6f5c849a6ec12234ae264c
ShaderImporter:
externalObjects: {}
defaultTextures: []
nonModifiableTextures: []
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,906 @@
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using UnityEditorInternal;
using System;
using UnityEditor.IMGUI.Controls;
using System.IO;
using System.Linq;
using System.Text;
using Unity.PerformanceTesting.Data;
using Unity.PerformanceTesting.Editor.UIElements;
using Unity.PerformanceTesting.Runtime;
#if UNITY_6000_2_OR_NEWER
using TreeViewState = UnityEditor.IMGUI.Controls.TreeViewState<int>;
#endif
namespace Unity.PerformanceTesting.Editor
{
internal class TestReportWindow : EditorWindow
{
static int s_windowWidth = 800;
static int s_windowHeight = 600;
private GUIStyle m_glStyle = null;
private GUIStyle m_boldStyle = null;
private Material m_material;
public Color m_colorWhite = new Color(1.0f, 1.0f, 1.0f);
public Color m_colorBarBackground = new Color(0.5f, 0.5f, 0.5f);
public Color m_colorBoxAndWhiskerBackground = new Color(0.4f, 0.4f, 0.4f);
public Color m_colorBar = new Color(0.95f, 0.95f, 0.95f);
public Color m_colorStandardLine = new Color(1.0f, 1.0f, 1.0f);
public Color m_colorMedianLine = new Color(0.2f, 0.5f, 1.0f, 0.5f);
public Color m_colorMedianText = new Color(0.4f, 0.7f, 1.0f, 1.0f);
public Color m_colorWarningText = Color.red;
public Color m_toolbarSeparator = new Color (0.15f, 0.15f, 0.15f, 1.0f);
private Run m_resultsData = null;
private string m_selectedTest;
private DateTime m_lastResultsDateTime = new DateTime(0);
private bool m_NewerFileExists = false;
private double m_LastFileCheckTime = 0f;
private float m_FileCheckFrequencySeconds = 3f;
private bool m_AutoRefresh;
private List<string> m_sampleGroups = new List<string>();
//private int m_selectedSampleGroupIndex;
private bool m_showTests = true;
private bool m_showSamples = true;
private int[] m_columnWidth = new int[4];
private bool m_isResizing;
private float m_testListHeight = (float)s_windowHeight / 4;
private Rect m_splitterRect;
private float m_windowHeight;
[SerializeField]
TreeViewState m_testListTreeViewState;
[SerializeField]
MultiColumnHeaderState m_testListMulticolumnHeaderState;
TestListTable m_testListTable;
Vector2 m_sampleGroupScroll = new Vector2(0, 0);
private readonly Dictionary<Tuple<string, string>, SampleGroupAdditionalData> m_sampleGroupAdditionalData = new Dictionary<Tuple<string, string>, SampleGroupAdditionalData>();
private bool m_isResultsCountPositive;
private ToolbarWithSearch m_toolbarWithSearch;
public string searchString;
private const int m_chartLimit = 1000;
private void CreateTestListTable()
{
if (m_testListTreeViewState == null)
m_testListTreeViewState = new TreeViewState();
//if (m_profileMulticolumnHeaderState==null)
m_testListMulticolumnHeaderState = TestListTable.CreateDefaultMultiColumnHeaderState(700);
var multiColumnHeader = new MultiColumnHeader(m_testListMulticolumnHeaderState);
multiColumnHeader.SetSorting((int)TestListTable.MyColumns.Name, true);
multiColumnHeader.ResizeToFit();
m_testListTable = new TestListTable(m_testListTreeViewState, multiColumnHeader, this);
m_testListTable.ExpandAll();
}
public Run GetResults()
{
return m_resultsData;
}
public void SelectTest(int index)
{
if (index < 0 || index >= m_resultsData.Results.Count)
{
m_sampleGroups.Clear();
return;
}
var result = m_resultsData.Results[index];
SelectTest(result);
}
public void SelectTest(string name)
{
foreach (var result in m_resultsData.Results)
{
if (result.Name == name)
{
SelectTest(result);
return;
}
}
}
public void SelectTest(PerformanceTestResult result)
{
m_selectedTest = result.Name;
m_sampleGroups.Clear();
foreach (var sampleGroup in result.SampleGroups)
{
m_sampleGroups.Add(sampleGroup.Name);
}
}
[MenuItem("Window/General/Performance Test Report", false, priority = 202)]
private static void Init()
{
var window = GetWindow<TestReportWindow>("Test Report");
window.minSize = new Vector2(640, 480);
window.position.size.Set(s_windowWidth, s_windowHeight);
window.Show();
}
public void SetupMaterial()
{
// Create a new material
m_material = new Material(Shader.Find("Unlit/TestReportShader"));
m_material.SetPass(0);
}
private string GetResultsPath()
{
return Path.Combine(Application.persistentDataPath, "PerformanceTestResults.json");
}
private bool NewerFileExists()
{
m_LastFileCheckTime = EditorApplication.timeSinceStartup;
string filePath = GetResultsPath();
if (!File.Exists(filePath))
return false;
if (m_resultsData == null)
return true;
DateTime dateTime = File.GetLastWriteTime(GetResultsPath());
if ((dateTime - m_lastResultsDateTime).TotalMilliseconds > 0)
return true;
return false;
}
private void ResetFileCheck()
{
m_lastResultsDateTime = File.GetLastWriteTime(GetResultsPath());
m_NewerFileExists = false;
m_LastFileCheckTime = EditorApplication.timeSinceStartup;
}
private bool CheckIfNewerFileExists()
{
if (EditorApplication.timeSinceStartup - m_LastFileCheckTime < m_FileCheckFrequencySeconds)
return m_NewerFileExists;
m_NewerFileExists = NewerFileExists();
return m_NewerFileExists;
}
private void LoadData()
{
m_toolbarWithSearch?.ClearSearchString();
string filePath = GetResultsPath();
if (!File.Exists(filePath)) return;
string json = File.ReadAllText(filePath);
m_resultsData = JsonUtility.FromJson<Run>(json);
ResetFileCheck();
List<SamplePoint> samplePoints = new List<SamplePoint>();
m_sampleGroupAdditionalData.Clear();
foreach (var result in m_resultsData.Results)
{
foreach (var sampleGroup in result.SampleGroups)
{
samplePoints.Clear();
for (int index = 0; index < sampleGroup.Samples.Count; index++)
{
var sample = sampleGroup.Samples[index];
samplePoints.Add(new SamplePoint(sample, index));
}
samplePoints.Sort();
int discard;
SampleGroupAdditionalData data = new SampleGroupAdditionalData();
data.min = (float)GetPercentageOffset(samplePoints, 0, out discard);
data.lowerQuartile = (float)GetPercentageOffset(samplePoints, 25, out discard);
data.median = (float)sampleGroup.Median;
data.upperQuartile = (float)GetPercentageOffset(samplePoints, 75, out discard);
data.max = (float)GetPercentageOffset(samplePoints, 100, out discard);
var key = Tuple.Create(result.Name, sampleGroup.Name);
m_sampleGroupAdditionalData[key] = data;
}
}
CreateTestListTable();
}
private void OnEnable()
{
SetupMaterial();
LoadData();
}
private void OnFocus()
{
CheckIfNewerFileExists();
}
private void OnGUI()
{
if (m_glStyle == null)
{
m_glStyle = new GUIStyle(GUI.skin.box);
m_glStyle.padding = new RectOffset(0, 0, 0, 0);
m_glStyle.margin = new RectOffset(0, 0, 0, 0);
}
if (m_boldStyle == null)
{
m_boldStyle = new GUIStyle(GUI.skin.label);
m_boldStyle.fontStyle = FontStyle.Bold;
}
if (m_toolbarWithSearch == null)
{
m_toolbarWithSearch = new ToolbarWithSearch();
m_toolbarWithSearch.SearchTextChanged += s =>
{
searchString = s;
CreateTestListTable();
};
}
m_isResultsCountPositive = m_resultsData?.Results.Count > 0;
DrawToolbar();
if (!DrawPerformanceDataStatusLabel()) return;
m_toolbarWithSearch.Draw();
DrawTestView();
DrawSampleView();
}
private void Update()
{
CheckIfNewerFileExists();
if (m_NewerFileExists && m_AutoRefresh)
Refresh();
}
private double GetPercentageOffset(List<SamplePoint> samplePoint, float percent, out int outputFrameIndex)
{
int index = (int)((samplePoint.Count - 1) * percent / 100);
outputFrameIndex = samplePoint[index].index;
// True median is half of the sum of the middle 2 frames for an even count. However this would be a value never recorded so we avoid that.
return samplePoint[index].sample;
}
private bool BoldFoldout(bool toggle, string text)
{
GUIStyle foldoutStyle = new GUIStyle(EditorStyles.foldout);
foldoutStyle.fontStyle = FontStyle.Bold;
return EditorGUILayout.Foldout(toggle, text, foldoutStyle);
/*
EditorGUILayout.LabelField(text, EditorStyles.boldLabel, GUILayout.Width(100));
return true;
*/
}
private void SetColumnSizes(int a, int b, int c, int d)
{
m_columnWidth[0] = a;
m_columnWidth[1] = b;
m_columnWidth[2] = c;
m_columnWidth[3] = d;
}
private void DrawColumn(int n, string col)
{
if (m_columnWidth[n] > 0)
EditorGUILayout.LabelField(col, GUILayout.Width(m_columnWidth[n]));
else
EditorGUILayout.LabelField(col);
}
private void DrawColumn(int n, float value)
{
DrawColumn(n, string.Format("{0:f2}", value));
}
private void Draw2Column(string label, float value)
{
EditorGUILayout.BeginHorizontal();
DrawColumn(0, label);
DrawColumn(1, value);
EditorGUILayout.EndHorizontal();
}
private void Refresh()
{
LoadData();
CreateTestListTable();
if (m_resultsData == null) return;
if (m_resultsData.Results == null) return;
if (m_resultsData.Results.Any(result => result.Name == m_selectedTest))
SelectTest(m_selectedTest);
else
SelectTest(0);
Repaint();
}
private void Export()
{
if (m_resultsData == null || m_resultsData.Results.Count <= 0) return;
var path = EditorUtility.SaveFilePanel("Save CSV data", "", "PerformanceTestResults.csv", "csv");
if (string.IsNullOrEmpty(path)) return;
var sb = new StringBuilder();
const string header =
"Index,Test Name,Version,Sample Group Name,Unit,Increase Is Better,Min,Max,Median,Average,Standard Deviation,Sum,Values\n";
sb.Append(header);
var index = 0;
foreach (var result in m_resultsData.Results)
foreach (var sampleGroup in result.SampleGroups)
{
var sampleValues = string.Join(", ", sampleGroup.Samples);
var increaseIsBetter = sampleGroup.IncreaseIsBetter ? "Yes" : "No";
var line =
$"{index},\"{result.Name}\",{result.Version},\"{sampleGroup.Name}\",\"{sampleGroup.Unit}\",{increaseIsBetter},{sampleGroup.Min},{sampleGroup.Max},{sampleGroup.Median},{sampleGroup.Average},{sampleGroup.StandardDeviation},{sampleGroup.Sum},{sampleValues}\n";
sb.Append(line);
index++;
}
File.WriteAllText(path, sb.ToString());
}
private void ClearResults()
{
File.Delete(GetResultsPath());
m_resultsData = null;
GUILayout.Label(string.Empty);
}
private void DrawToolbar()
{
GUILayout.BeginHorizontal();
m_AutoRefresh = EditorPrefs.GetBool("ToggleState", false);
m_AutoRefresh = GUILayout.Toggle(m_AutoRefresh, "Auto Refresh");
EditorPrefs.SetBool("ToggleState", m_AutoRefresh);
if (GUILayout.Button("Refresh", EditorStyles.toolbarButton)) Refresh();
GUILayout.FlexibleSpace();
if (m_isResultsCountPositive) GUILayout.Label($"Last results {m_lastResultsDateTime}");
GUI.enabled = m_isResultsCountPositive;
if (GUILayout.Button("Clear Results", EditorStyles.toolbarButton))
{
if (EditorUtility.DisplayDialog("Clear Results",
"Are you sure you want to clear all Performance test results?", "Clear", "Cancel"))
{
ClearResults();
}
}
if (GUILayout.Button("Export", EditorStyles.toolbarButton)) Export();
GUI.enabled = true;
DrawHelpIcon();
GUILayout.EndHorizontal();
EditorGUI.DrawRect(new Rect(0, GUILayoutUtility.GetLastRect().yMax, position.width, 1), m_toolbarSeparator);
EditorGUILayout.Space();
}
private static void DrawHelpIcon()
{
if (!GUILayout.Button(EditorGUIUtility.TrIconContent("_Help", "Open Documentation"),
EditorStyles.toolbarButton)) return;
var packageInfo = UnityEditor.PackageManager.PackageInfo.FindForAssetPath("Packages/com.unity.test-framework.performance");
var shortVersion = packageInfo.version.Substring(0,
packageInfo.version.IndexOf('.', packageInfo.version.IndexOf('.') + 1));
var documentationUrl = $"https://docs.unity3d.com/Packages/{packageInfo.name}@{shortVersion}";
Help.ShowHelpPage(documentationUrl);
}
private bool DrawPerformanceDataStatusLabel()
{
var isDataMissing = m_resultsData == null || !m_isResultsCountPositive;
var shouldRefreshData = m_NewerFileExists && !m_AutoRefresh;
if (isDataMissing || shouldRefreshData)
{
var content = new GUIContent
{
image = EditorGUIUtility.IconContent("console.infoicon").image,
text = shouldRefreshData
? "New performance <b>data is available</b>. Click <b>\"Refresh\"</b> or <b>\"Auto Refresh\"</b> to view it"
: "No performance data found"
};
var style = new GUIStyle(GUI.skin.GetStyle("HelpBox"))
{
richText = true,
alignment = TextAnchor.MiddleLeft
};
GUILayout.Label(content, style);
if (isDataMissing) return false;
}
return true;
}
private void DrawTestView()
{
m_showTests = BoldFoldout(m_showTests, "Test View");
if (m_showTests)
{
if (m_testListTable != null)
{
var r = GUILayoutUtility.GetRect(position.width, m_testListHeight, GUI.skin.box,
GUILayout.ExpandWidth(true));
m_testListTable.OnGUI(r);
Resize(r.y);
}
}
if (!string.IsNullOrEmpty(m_selectedTest))
{
var profileDataFile = Path.Combine(Application.persistentDataPath,
Utils.RemoveIllegalCharacters(m_selectedTest) + ".raw");
if (File.Exists(profileDataFile))
{
if (GUILayout.Button($"Load profiler data for test: {m_selectedTest}"))
{
ProfilerDriver.LoadProfile(profileDataFile, false);
}
}
}
}
private void DrawSampleView()
{
m_showSamples = BoldFoldout(m_showSamples, "Sample Group View");
if (m_showSamples)
{
if (m_sampleGroups?.Count <= 0)
{
EditorGUILayout.LabelField("This Selection Has No Sample Groups", m_boldStyle);
return;
}
SetColumnSizes(50, 50, 50, 50);
var boxStyle = GUI.skin.box;
var boxWidth = position.width - GUI.skin.verticalScrollbar.fixedWidth -
(boxStyle.padding.horizontal + boxStyle.margin.horizontal);
var graphWidth = position.width - 200;
EditorGUILayout.BeginVertical();
m_sampleGroupScroll = EditorGUILayout.BeginScrollView(m_sampleGroupScroll, false, true);
foreach (var result in m_resultsData.Results)
{
if (result.Name != m_selectedTest) continue;
EditorGUILayout.LabelField(result.Name, m_boldStyle);
foreach (var sampleGroup in result.SampleGroups)
{
var key = Tuple.Create(result.Name, sampleGroup.Name);
if (m_sampleGroupAdditionalData.TryGetValue(key, out var data))
{
var samplesToDraw = sampleGroup.Samples;
var min = data.min;
var lowerQuartile = data.lowerQuartile;
var median = data.median;
var upperQuartile = data.upperQuartile;
var max = data.max;
var graphMin = min > 0.0f ? 0.0f : min;
EditorGUILayout.BeginVertical(boxStyle, GUILayout.Width(boxWidth),
GUILayout.ExpandHeight(false));
EditorGUILayout.LabelField(sampleGroup.Name, m_boldStyle);
EditorGUILayout.LabelField($"Sample Unit: {sampleGroup.Unit}");
if (samplesToDraw.Count > m_chartLimit)
{
EditorGUILayout.HelpBox(
$"Sample Group has more than {m_chartLimit} Samples. The first {m_chartLimit} Samples will be displayed. However, calculations are done according to all samples received from the test run.",
MessageType.Warning, true);
samplesToDraw = samplesToDraw.Take(m_chartLimit).ToList();
}
EditorGUILayout.BeginHorizontal(GUILayout.Height(100), GUILayout.ExpandHeight(false));
EditorGUILayout.BeginVertical(GUILayout.Width(100), GUILayout.ExpandHeight(true));
Draw2Column("Max", max);
GUILayout.FlexibleSpace();
var oldColor = GUI.contentColor;
GUI.contentColor = median < 0.01f ? m_colorWarningText : m_colorMedianText;
Draw2Column("Median", median);
GUI.contentColor = oldColor;
GUILayout.FlexibleSpace();
Draw2Column("Min", min);
EditorGUILayout.EndVertical();
DrawBarGraph(graphWidth, 100, samplesToDraw, graphMin, max, median);
DrawBoxAndWhiskerPlot(50, 100, min, lowerQuartile, median, upperQuartile, max, min, max,
(float)sampleGroup.StandardDeviation, m_colorWhite, m_colorBoxAndWhiskerBackground);
EditorGUILayout.EndHorizontal();
EditorGUILayout.EndVertical();
}
}
}
EditorGUILayout.EndScrollView();
EditorGUILayout.EndVertical();
}
}
private void Resize(float headerHeight)
{
m_splitterRect = new Rect(0, headerHeight + m_testListHeight, position.width, 5f);
EditorGUIUtility.AddCursorRect(m_splitterRect, MouseCursor.ResizeVertical);
if (Event.current.type == EventType.MouseDown)
m_isResizing = m_splitterRect.Contains(Event.current.mousePosition);
const float minListHeight = 80f;
var maxListHeight = position.height - 240f;
var needMoveSplitterAndRepaint = false;
if (m_isResizing && Event.current.type == EventType.MouseDrag)
{
m_testListHeight = Mathf.Clamp(Event.current.mousePosition.y - headerHeight, minListHeight, maxListHeight);
needMoveSplitterAndRepaint = true;
}
else if (Math.Abs(m_windowHeight - position.height) > float.Epsilon)
{
m_windowHeight = position.height;
m_testListHeight = Mathf.Clamp(m_testListHeight, minListHeight, maxListHeight);
needMoveSplitterAndRepaint = true;
}
if (needMoveSplitterAndRepaint)
{
m_splitterRect.Set(m_splitterRect.x, m_testListHeight, m_splitterRect.width, m_splitterRect.height);
Repaint();
}
if (Event.current.type == EventType.MouseUp)
m_isResizing = false;
}
private void DrawBarGraph(float width, float height, List<double> samples, float min, float max, float median)
{
if (DrawStart(width, height))
{
Rect rect = GUILayoutUtility.GetLastRect();
float clipH = Math.Max(m_sampleGroupScroll.y - rect.y, 0);
float maxH = Math.Max(height - clipH, 0);
int xAxisDivisions = samples.Count;
float xAxisInc = width / xAxisDivisions;
float yRange = max - min;
float x = 0;
float y = 0;
float spacing = 2;
float w = xAxisInc - spacing;
if (w < 1) spacing = 0;
float h = 0;
for (int i = 0; i < samples.Count; i++)
{
float sample = (float)samples[i];
w = xAxisInc - spacing;
h = ((sample - min) * height) / yRange;
if (h > maxH)
h = maxH;
DrawBar(x, y + (height - h), w, h, m_colorBar);
x += xAxisInc;
}
h = ((median - min) * height) / yRange;
if (h <= maxH)
{
//DrawLine(0, (height - h), width, (height - h), m_colorMedianLine);
DrawBar(0, (height - h), width, 3, m_colorMedianLine);
}
x = 0;
for (int i = 0; i < samples.Count; i++)
{
float sample = (float)samples[i];
string tooltip = string.Format("{0} (at sample {1} of {2})", sample, i+1, samples.Count);
GUI.Label(new Rect(rect.x + x, rect.y + y, xAxisInc, height), new GUIContent("", tooltip));
x += xAxisInc;
}
DrawEnd();
}
}
public bool DrawStart(Rect r)
{
if (Event.current.type != EventType.Repaint)
return false;
GL.PushMatrix();
SetupMaterial();
Matrix4x4 matrix = new Matrix4x4();
matrix.SetTRS(new Vector3(r.x, r.y, 0), Quaternion.identity, Vector3.one);
GL.MultMatrix(matrix);
return true;
}
public bool DrawStart(float w, float h, GUIStyle style = null)
{
Rect r = GUILayoutUtility.GetRect(w, h, style == null ? m_glStyle : style);
return DrawStart(r);
}
public void DrawEnd()
{
GL.PopMatrix();
}
public void DrawBar(float x, float y, float w, float h, Color col)
{
GL.Begin(GL.TRIANGLE_STRIP);
GL.Color(col);
GL.Vertex3(x, y, 0);
GL.Vertex3(x + w, y, 0);
GL.Vertex3(x, y + h, 0);
GL.Vertex3(x + w, y + h, 0);
GL.End();
}
void DrawBar(float x, float y, float w, float h, float r, float g, float b)
{
DrawBar(x, y, w, h, new Color(r, g, b));
}
void DrawLine(float x, float y, float x2, float y2, Color col)
{
GL.Begin(GL.LINES);
GL.Color(col);
GL.Vertex3(x, y, 0);
GL.Vertex3(x2, y2, 0);
GL.End();
}
void DrawLine(float x, float y, float x2, float y2, float r, float g, float b)
{
DrawLine(x, y, x2, y2, new Color(r, g, b));
}
void DrawBox(float x, float y, float w, float h, Color col)
{
GL.Begin(GL.LINE_STRIP);
GL.Color(col);
GL.Vertex3(x, y, 0);
GL.Vertex3(x + w, y, 0);
GL.Vertex3(x + w, y + h, 0);
GL.Vertex3(x, y + h, 0);
GL.Vertex3(x, y, 0);
GL.End();
}
void DrawBox(float x, float y, float w, float h, float r, float g, float b)
{
DrawBox(x, y, w, h, new Color(r, g, b));
}
private float ClampToRange(float value, float min, float max)
{
return Math.Max(min, Math.Min(value, max));
}
private void DrawHistogramStart(float width)
{
EditorGUILayout.BeginHorizontal(GUILayout.Width(width + 10));
//GUILayoutUtility.GetRect(GUI.skin.box.margin.left, 1);
EditorGUILayout.BeginVertical();
}
private void DrawHistogramEnd(float width, float min, float max, float spacing)
{
EditorGUILayout.BeginHorizontal();
float lastBar = width - 50;
GUIStyle rightAlignStyle = new GUIStyle(GUI.skin.label);
rightAlignStyle.alignment = TextAnchor.MiddleCenter;
EditorGUILayout.LabelField(string.Format("{0:f2}", min), GUILayout.Width(lastBar));
EditorGUILayout.LabelField(string.Format("{0:f2}", max), rightAlignStyle, GUILayout.Width(50));
EditorGUILayout.EndHorizontal();
EditorGUILayout.EndVertical();
EditorGUILayout.EndHorizontal();
}
private void DrawHistogramBackground(float width, float height, int bucketCount, float spacing)
{
float x = (spacing / 2);
float y = 0;
float w = ((width + spacing) / bucketCount) - spacing;
float h = height;
for (int i = 0; i < bucketCount; i++)
{
DrawBar(x, y, w, h, m_colorBarBackground);
x += w;
x += spacing;
}
}
private void DrawHistogramData(float width, float height, int[] buckets, int totalFrameCount, float min,
float max, Color barColor, float spacing)
{
float x = (spacing / 2);
float y = 0;
float w = ((width + spacing) / buckets.Length) - spacing;
float h = height;
Rect rect = GUILayoutUtility.GetLastRect();
int bucketCount = buckets.Length;
float bucketWidth = ((max - min) / bucketCount);
for (int bucketAt = 0; bucketAt < bucketCount; bucketAt++)
{
var count = buckets[bucketAt];
float barHeight = (h * count) / totalFrameCount;
if (barHeight > rect.height)
barHeight = rect.height;
DrawBar(x, y + (h - barHeight), w, barHeight, barColor);
float bucketStart = min + (bucketAt * bucketWidth);
float bucketEnd = bucketStart + bucketWidth;
GUI.Label(new Rect(rect.x + x, rect.y + y, w, h),
new GUIContent("", string.Format("{0:f2}-{1:f2}ms\n{2} frames", bucketStart, bucketEnd, count)));
x += w;
x += spacing;
}
}
private void DrawHistogram(float width, float height, int[] buckets, int totalFrameCount, float min, float max,
Color barColor)
{
DrawHistogramStart(width);
float spacing = 2;
if (DrawStart(width, height))
{
Rect rect = GUILayoutUtility.GetLastRect();
float clipH = Math.Max(m_sampleGroupScroll.y - rect.y, 0);
float maxH = Math.Max(height - clipH, 0);
DrawHistogramBackground(width, maxH, buckets.Length, spacing);
DrawHistogramData(width, height, buckets, totalFrameCount, min, max, barColor, spacing);
DrawEnd();
}
DrawHistogramEnd(width, min, max, spacing);
}
private void DrawBoxAndWhiskerPlot(float width, float height, float min, float lowerQuartile, float median,
float upperQuartile, float max, float yAxisStart, float yAxisEnd, float standardDeviation, Color color,
Color colorBackground)
{
if (DrawStart(width, height))
{
Rect rect = GUILayoutUtility.GetLastRect();
float clipH = Math.Max(m_sampleGroupScroll.y - rect.y, 0);
float maxH = Math.Max(height - clipH, 0);
if (maxH > 0)
{
float x = 0;
float y = 0;
float w = width;
float h = height;
DrawBoxAndWhiskerPlot(rect, x, y, w, h, min, lowerQuartile, median, upperQuartile, max, yAxisStart,
yAxisEnd, standardDeviation, color, colorBackground);
}
DrawEnd();
}
}
private void DrawBoxAndWhiskerPlot(Rect rect, float x, float y, float w, float h, float min,
float lowerQuartile, float median, float upperQuartile, float max, float yAxisStart, float yAxisEnd,
float standardDeviation, Color color, Color colorBackground)
{
string tooltip = string.Format(
"Max :\t\t{0:f2}\n\nUpper Quartile :\t{1:f2}\nMedian :\t\t{2:f2}\nLower Quartile :\t{3:f2}\n\nMin :\t\t{4:f2}\n\nStandard Deviation:\t{5:f2}",
max, upperQuartile, median, lowerQuartile, min,
standardDeviation);
GUI.Label(rect, new GUIContent("", tooltip));
float clipH = Math.Max(m_sampleGroupScroll.y - rect.y, 0);
float maxH = Math.Max(h - clipH, 0);
DrawBar(x, y + (h - maxH), w, maxH, m_colorBoxAndWhiskerBackground);
float first = yAxisStart;
float last = yAxisEnd;
float range = last - first;
bool startCap = (min >= first) ? true : false;
bool endCap = (max <= last) ? true : false;
// Range clamping
min = ClampToRange(min, first, last);
lowerQuartile = ClampToRange(lowerQuartile, first, last);
median = ClampToRange(median, first, last);
upperQuartile = ClampToRange(upperQuartile, first, last);
max = ClampToRange(max, first, last);
float yMin = h * (min - first) / range;
float yLowerQuartile = h * (lowerQuartile - first) / range;
float yMedian = h * (median - first) / range;
float yUpperQuartile = h * (upperQuartile - first) / range;
float yMax = h * (max - first) / range;
// Clamp to scroll area
yMin = Math.Min(yMin, maxH);
yLowerQuartile = Math.Min(yLowerQuartile, maxH);
yMedian = Math.Min(yMedian, maxH);
yUpperQuartile = Math.Min(yUpperQuartile, maxH);
yMax = Math.Min(yMax, maxH);
// Min to max line
//DrawLine(x + (w / 2), y + (h - yMin), x + (w / 2), y + (h - yMax), color);
DrawLine(x + (w / 2), y + (h - yMin), x + (w / 2), y + (h - yLowerQuartile), color);
DrawLine(x + (w / 2), y + (h - yUpperQuartile), x + (w / 2), y + (h - yMax), color);
// Quartile boxes
float xMargin = (2 * w / 8);
if (colorBackground != color)
DrawBar(x + xMargin, y + (h - yMedian), w - (2 * xMargin), (yMedian - yLowerQuartile), colorBackground);
DrawBox(x + xMargin, y + (h - yMedian), w - (2 * xMargin), (yMedian - yLowerQuartile), color);
if (colorBackground != color)
DrawBar(x + xMargin, y + (h - yUpperQuartile), w - (2 * xMargin), (yUpperQuartile - yMedian),
colorBackground);
DrawBox(x + xMargin, y + (h - yUpperQuartile), w - (2 * xMargin), (yUpperQuartile - yMedian), color);
// Median line
DrawLine(x + xMargin, y + (h - yMedian), x + (w - xMargin), y + (h - yMedian), color);
// Line caps
xMargin = (3 * w / 8);
if (startCap)
DrawLine(x + xMargin, y + (h - yMin), x + (w - xMargin), y + (h - yMin), color);
if (endCap)
DrawLine(x + xMargin, y + (h - yMax), x + (w - xMargin), y + (h - yMax), color);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4272062d6dbd273438736c85848ea974
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,22 @@
using System;
using Unity.PerformanceTesting.Data;
using UnityEngine;
namespace Unity.PerformanceTesting.Editor
{
/// <summary>
/// Helper class to parse test runs into performance test runs.
/// </summary>
public class TestResultXmlParser
{
/// <summary>
/// Parses performance test run from test run result xml.
/// </summary>
/// <param name="resultXmlFileName">Path to test results xml file.</param>
/// <returns>Performance test run data extracted from the NUnit xml results file.</returns>
public Run GetPerformanceTestRunFromXml(string resultXmlFileName)
{
return TestResultsParser.GetPerformanceTestRunDataFromXmlFile(resultXmlFileName);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: acd1f5cc793f24d4ba37a3c34952bde0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,211 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Xml.Linq;
using Unity.PerformanceTesting.Data;
using UnityEditor.TestTools.TestRunner.Api;
using UnityEngine;
namespace Unity.PerformanceTesting.Editor
{
static class TestResultsParser
{
internal static Run GetPerformanceTestRunData(ITestResultAdaptor testResults)
{
var testOutputs = GetTestOutputsRecursively(testResults);
return ExtractPerformanceRunData(testOutputs);
}
internal static Run GetPerformanceTestRunDataFromXmlFile(string xmlResultsPath)
{
try
{
var xmlDocument = XDocument.Load(xmlResultsPath);
return GetPerformanceTestRunData(xmlDocument);
}
catch (Exception e)
{
Debug.LogWarning("Failed to load performance test results from XML.\n" +
$"{e.GetType()}: {e.Message}\n" +
$"{e.StackTrace}");
}
return null;
}
static Run GetPerformanceTestRunData(XDocument testResults)
{
var testOutputs = testResults.Descendants("output")
.Select(outputElement => outputElement.Value)
.ToArray();
return ExtractPerformanceRunData(testOutputs);
}
static string[] GetTestOutputsRecursively(ITestResultAdaptor testResults)
{
var res = new List<string>();
if (testResults != null)
{
AccumulateTestRunOutputRecursively(testResults, res);
}
return res.ToArray();
}
static void AccumulateTestRunOutputRecursively(ITestResultAdaptor parentResult, List<string> outputs)
{
var children = parentResult.Children;
foreach (var child in children)
{
AccumulateTestRunOutputRecursively(child, outputs);
}
var currentTestOutput = parentResult.Output;
if (!string.IsNullOrEmpty(currentTestOutput))
{
outputs.Add(currentTestOutput);
}
}
internal static Run ExtractPerformanceRunData(string[] testOutputs)
{
if (testOutputs == null || testOutputs.Length == 0)
{
return null;
}
try
{
var run = ExtractPerformanceTestRunInfo(testOutputs);
if (run != null)
{
DeserializeTestResults(testOutputs, run);
}
return run;
}
catch (FormatException fe)
{
Debug.LogError($"Invalid performance test results format: {fe.Message}");
}
catch (Exception e)
{
Debug.LogError($"Unexpected exception while reading performance test results: {e.Message}");
}
return null;
}
static Run ExtractPerformanceTestRunInfo(string[] testOutputs)
{
foreach (var output in testOutputs)
{
const string pattern = @"##performancetestruninfo2:(.+)\n";
var regex = new Regex(pattern);
var matches = regex.Match(output);
if (matches.Groups.Count == 0 || matches.Captures.Count == 0)
{
continue;
}
if (matches.Groups[1].Captures.Count > 1)
{
throw new FormatException("Multiple execution metadata instances found.");
}
var json = matches.Groups[1].Value;
if (string.IsNullOrEmpty(json))
{
throw new FormatException("No execution metadata found.");
}
return ReadPerformanceTestRunJsonObject(json);
}
return null;
}
static Run ReadPerformanceTestRunJsonObject(string json)
{
try
{
return JsonUtility.FromJson<Run>(json);
}
catch (Exception e)
{
throw new FormatException($"Failed to read performance execution metadata from json string: '{json}'.\n" +
$"Exception: {e.Message}\n" +
$"{e.StackTrace}");
}
}
static void DeserializeTestResults(string[] testOutputs, Run run)
{
foreach (var output in testOutputs)
{
foreach (var line in output.Split('\n'))
{
var json = GetJsonFromHashtag("performancetestresult2", line);
if (json == null)
{
continue;
}
var result = ReadPerformanceTestResultJsonObject(json);
if (result != null)
{
run.Results.Add(result);
}
}
}
}
static PerformanceTestResult ReadPerformanceTestResultJsonObject(string json)
{
try
{
return JsonUtility.FromJson<PerformanceTestResult>(json);
}
catch (Exception e)
{
Debug.LogWarning($"Failed to read performance results from json string: '{json}'.\n" +
$"Exception: {e.Message}\n" +
$"{e.StackTrace}");
}
return null;
}
static string GetJsonFromHashtag(string tag, string line)
{
if (!line.Contains($"##{tag}:"))
{
return null;
}
var jsonStart = line.IndexOf('{');
var openBrackets = 0;
var stringIndex = jsonStart;
while (openBrackets > 0 || stringIndex == jsonStart)
{
var character = line[stringIndex];
switch (character)
{
case '{':
openBrackets++;
break;
case '}':
openBrackets--;
break;
}
stringIndex++;
}
var jsonEnd = stringIndex;
return line.Substring(jsonStart, jsonEnd - jsonStart);
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 6ad2e365b2be44c3aedc5892edc0ae8d
timeCreated: 1742825212

View File

@@ -0,0 +1,208 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using Unity.PerformanceTesting.Data;
using Unity.PerformanceTesting.Editor;
using Unity.PerformanceTesting.Runtime;
using UnityEditor;
using UnityEngine;
using UnityEngine.TestTools;
using UnityEditor.Build;
using UnityEditor.Build.Reporting;
[assembly: PrebuildSetup(typeof(TestRunBuilder))]
[assembly: PostBuildCleanup(typeof(TestRunBuilder))]
namespace Unity.PerformanceTesting.Editor
{
internal class TestRunBuilder : IPrebuildSetup, IPostBuildCleanup, IPreprocessBuildWithReport, IPostprocessBuildWithReport
{
private const string cleanResources = "PT_ResourcesCleanup";
public int callbackOrder
{
get { return 0; }
}
public void OnPreprocessBuild(BuildReport report)
{
CreateResourcesFolder();
var run = CreateBuildInfo();
SaveToStorage(run, Utils.TestRunPath);
var settings = new RunSettings(Environment.GetCommandLineArgs());
SaveToStorage(settings, Utils.RunSettingsPath);
}
public void OnPostprocessBuild(BuildReport report)
{
Cleanup();
}
public void Setup()
{
EditorPrefs.SetBool(cleanResources, false);
var run = CreateRunInfo();
SaveToPrefs(run, Utils.PlayerPrefKeyRunJSON);
var settings = new RunSettings(Environment.GetCommandLineArgs());
SaveToPrefs(settings, Utils.PlayerPrefKeySettingsJSON);
}
#if !UNITY_2021_1_OR_NEWER
private static List<string> cachedDependencies;
#endif
static List<string> GetPackageDependencies()
{
#if !UNITY_2021_1_OR_NEWER
if (cachedDependencies != null)
return cachedDependencies;
#endif
IEnumerable<UnityEditor.PackageManager.PackageInfo> packages;
#if !UNITY_2021_1_OR_NEWER
var listRequest = UnityEditor.PackageManager.Client.List(true);
while (!listRequest.IsCompleted)
System.Threading.Thread.Sleep(10);
if (listRequest.Status == UnityEditor.PackageManager.StatusCode.Failure)
Debug.LogError("Failed to list local packages");
packages = new List<UnityEditor.PackageManager.PackageInfo>(listRequest.Result);
#else
packages = UnityEditor.PackageManager.PackageInfo.GetAllRegisteredPackages();
#endif
var reformated = packages.Select(p => $"{p.name}@{p.version}").ToList();
#if !UNITY_2021_1_OR_NEWER
cachedDependencies = reformated;
#endif
return reformated;
}
public void Cleanup()
{
DeleteFileAndMeta(Utils.TestRunPath);
DeleteFileAndMeta(Utils.RunSettingsPath);
if (EditorPrefs.GetBool(cleanResources) && Directory.Exists("Assets/Resources"))
{
Directory.Delete("Assets/Resources/", true);
if(File.Exists("Assets/Resources.meta")) {File.Delete("Assets/Resources.meta");}
}
AssetDatabase.Refresh();
}
private void DeleteFileAndMeta(string path)
{
if (File.Exists(path)) { File.Delete(path); }
var metaPath = path + ".meta";
if (File.Exists(metaPath)) { File.Delete(metaPath); }
}
private static Data.Editor GetEditorInfo()
{
var fullVersion = UnityEditorInternal.InternalEditorUtility.GetFullUnityVersion();
const string pattern = @"(.+\.+.+\.\w+)|((?<=\().+(?=\)))";
var matches = Regex.Matches(fullVersion, pattern);
return new Data.Editor
{
Branch = GetEditorBranch(),
Version = matches[0].Value,
Changeset = matches[1].Value,
Date = UnityEditorInternal.InternalEditorUtility.GetUnityVersionDate(),
};
}
private static string GetEditorBranch()
{
foreach (var method in typeof(UnityEditorInternal.InternalEditorUtility).GetMethods())
{
if (method.Name.Contains("GetUnityBuildBranch"))
{
return (string) method.Invoke(null, null);
}
}
return "null";
}
private static void SetBuildSettings(Run run)
{
if (run.Player == null) run.Player = new Player();
run.Player.GpuSkinning = PlayerSettings.gpuSkinning;
#if UNITY_2021_2_OR_NEWER
run.Player.ScriptingBackend = PlayerSettings
.GetScriptingBackend(NamedBuildTarget.FromBuildTargetGroup(EditorUserBuildSettings.selectedBuildTargetGroup)).ToString();
#else
run.Player.ScriptingBackend = PlayerSettings
.GetScriptingBackend(EditorUserBuildSettings.selectedBuildTargetGroup).ToString();
#endif
run.Player.RenderThreadingMode = PlayerSettings.graphicsJobs ? PlayerSettings.graphicsJobMode.ToString() :
PlayerSettings.MTRendering ? "MultiThreaded" : "SingleThreaded";
run.Player.AndroidTargetSdkVersion = PlayerSettings.Android.targetSdkVersion.ToString();
run.Player.AndroidBuildSystem = EditorUserBuildSettings.androidBuildSystem.ToString();
run.Player.BuildTarget = EditorUserBuildSettings.activeBuildTarget.ToString();
run.Player.StereoRenderingPath = PlayerSettings.stereoRenderingPath.ToString();
}
public Run CreateRunInfo()
{
var run = new Run();
run.Editor = GetEditorInfo();
run.Dependencies = GetPackageDependencies();
SetBuildSettings(run);
run.Date = Utils.ConvertToUnixTimestamp(DateTime.Now);
return run;
}
public Run CreateBuildInfo()
{
var run = new Run();
run.Editor = GetEditorInfo();
run.Dependencies = GetPackageDependencies();
SetBuildSettings(run);
return run;
}
public Run GetPerformanceTestRun()
{
var run = CreateRunInfo();
Metadata.SetRuntimeSettings(run);
return run;
}
private void CreateResourcesFolder()
{
if (Directory.Exists(Utils.ResourcesPath))
{
EditorPrefs.SetBool(cleanResources, false);
return;
}
EditorPrefs.SetBool(cleanResources, true);
AssetDatabase.CreateFolder("Assets", "Resources");
}
private string SaveToStorage(object obj, string path)
{
var json = JsonUtility.ToJson(obj);
File.WriteAllText(path, json);
return json;
}
private string SaveToPrefs(object obj, string key)
{
var json = JsonUtility.ToJson(obj, true);
PlayerPrefs.SetString(key, json);
return json;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3e68133cd13ef104088b87bbf4f00320
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,57 @@
using System;
using UnityEditor;
using UnityEditor.TestRunner.CommandLineParser;
using UnityEditor.TestTools.TestRunner.Api;
using UnityEngine;
namespace Unity.PerformanceTesting.Editor
{
[InitializeOnLoad]
class TestRunnerInitializer
{
static TestRunnerInitializer()
{
var args = GetCmdLineArguments();
var resultsHandler = args.IsCmdLineRun && !string.IsNullOrEmpty(args.PerfTestResults)
? CreateCmdLineResultsHandler(args)
: ScriptableObject.CreateInstance<PerformanceTestRunSaver>();
var api = ScriptableObject.CreateInstance<TestRunnerApi>();
api.RegisterCallbacks(resultsHandler);
}
static ICallbacks CreateCmdLineResultsHandler(CmdLineArguments cmdLineArguments)
{
var callbacks = ScriptableObject.CreateInstance<CmdLineResultsSavingCallbacks>();
callbacks.SetResultsLocation(cmdLineArguments.PerfTestResults);
return callbacks;
}
static CmdLineArguments GetCmdLineArguments()
{
var isCmdLineTestRun = false;
string resultFilePath = null;
var optionSet = new CommandLineOptionSet(
new CommandLineOption("runTests", () => { isCmdLineTestRun = true; }),
new CommandLineOption("runEditorTests", () => { isCmdLineTestRun = true; }),
new CommandLineOption("perfTestResults", filePath => { resultFilePath = filePath; })
);
optionSet.Parse(Environment.GetCommandLineArgs());
return new CmdLineArguments(isCmdLineTestRun, resultFilePath);
}
class CmdLineArguments
{
public bool IsCmdLineRun;
public string PerfTestResults;
public CmdLineArguments(bool isCmdLineRun, string perfTestResults)
{
IsCmdLineRun = isCmdLineRun;
PerfTestResults = perfTestResults;
}
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 4ea07ee0847c4e359272dd9b57e1cb2b
timeCreated: 1742825056

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: b6a3ada20a8c4030907eb96c0cb331bc
timeCreated: 1683027438

View File

@@ -0,0 +1,39 @@
using System;
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine.UIElements;
namespace Unity.PerformanceTesting.Editor.UIElements
{
internal class ToolbarWithSearch : VisualElement
{
private string m_searchString;
public Action<string> SearchTextChanged;
public ToolbarWithSearch()
{
// Create the toolbar and search field elements
var toolbar = new Toolbar();
var searchField = new ToolbarSearchField();
// Add the toolbar and search field elements to this element
Add(toolbar);
Add(searchField);
}
public void Draw()
{
EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
EditorGUI.BeginChangeCheck();
m_searchString = EditorGUILayout.TextField(m_searchString, EditorStyles.toolbarSearchField);
if (EditorGUI.EndChangeCheck()) SearchTextChanged?.Invoke(m_searchString);
EditorGUILayout.EndHorizontal();
}
public void ClearSearchString()
{
m_searchString = string.Empty;
SearchTextChanged?.Invoke(m_searchString);
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: f660b03f07d34d62990e579338856f5f
timeCreated: 1683033109

View File

@@ -0,0 +1,30 @@
{
"name": "Unity.PerformanceTesting.Editor",
"rootNamespace": "",
"references": [
"Unity.PerformanceTesting",
"UnityEngine.TestRunner",
"UnityEditor.TestRunner"
],
"includePlatforms": [
"Editor"
],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": true,
"precompiledReferences": [
"nunit.framework.dll"
],
"autoReferenced": false,
"defineConstraints": [
"UNITY_TESTS_FRAMEWORK"
],
"versionDefines": [
{
"name": "com.unity.test-framework",
"expression": "",
"define": "UNITY_TESTS_FRAMEWORK"
}
],
"noEngineReferences": false
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 9f843d4b38153f04b9cee0502d568525
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant: