624 lines
		
	
	
		
			30 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
			
		
		
	
	
			624 lines
		
	
	
		
			30 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
| using System;
 | |
| using System.Collections.Generic;
 | |
| using System.Reflection;
 | |
| using Unity.Collections;
 | |
| using Unity.Collections.LowLevel.Unsafe;
 | |
| 
 | |
| namespace Unity.PerformanceTesting.Benchmark
 | |
| {
 | |
|     internal static class BenchmarkRunner
 | |
|     {
 | |
|         static string progressTitle;
 | |
| 
 | |
|         static void StartProgress(string title, int typeIndex, int typeCount, string typeName) =>
 | |
|             progressTitle = $"Benchmarking {title} {typeIndex + 1}/{typeCount} - {typeName}";
 | |
| 
 | |
|         static void EndProgress()
 | |
|         {
 | |
| #if UNITY_EDITOR
 | |
|             UnityEditor.EditorUtility.ClearProgressBar();
 | |
| #endif
 | |
|         }
 | |
| 
 | |
|         static void SetProgressText(string text, float unitProgress)
 | |
|         {
 | |
| #if UNITY_EDITOR
 | |
|             if (UnityEditor.EditorUtility.DisplayCancelableProgressBar(progressTitle, text, unitProgress))
 | |
|                 throw new Exception("User cancelled benchmark operation");
 | |
| #endif
 | |
|         }
 | |
| 
 | |
|         /// <summary>
 | |
|         /// Contains a combination of a BenchmarkComparison attributed enum and the Type with perf. measurements
 | |
|         /// to determine names for the benchmark.
 | |
|         ///
 | |
|         /// Also contains reflected info on the enum defined and external benchmark values used to organize
 | |
|         /// benchmark tests and results, though this will not vary between different Types with perf. measurments.
 | |
|         /// These constant values are also associated with a classification of enum-defined vs external, and
 | |
|         /// baseline vs not.
 | |
|         ///
 | |
|         /// There may only be one baseline per benchmark comparison type.
 | |
|         /// </summary>
 | |
|         class BenchmarkComparisonTypeData
 | |
|         {
 | |
|             public string defaultName;
 | |
|             public Type enumType;
 | |
| 
 | |
|             public string[] names;
 | |
|             public int[] values;
 | |
|             public BenchmarkResultType[] resultTypes;
 | |
| 
 | |
|             public SampleUnit resultUnit;
 | |
|             public int resultDecimalPlaces;
 | |
|             public BenchmarkRankingStatistic resultStatistic;
 | |
| 
 | |
|             public BenchmarkComparisonTypeData(int variants)
 | |
|             {
 | |
|                 names = new string[variants];
 | |
|                 values = new int[variants];
 | |
|                 resultTypes = new BenchmarkResultType[variants];
 | |
|                 enumType = null;
 | |
|                 defaultName = null;
 | |
|                 resultUnit = SampleUnit.Millisecond;
 | |
|                 resultDecimalPlaces = 3;
 | |
|                 resultStatistic = BenchmarkRankingStatistic.Median;
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         /// <summary>
 | |
|         /// Given a System.Type that contains performance test methods, reflect the setup to a benchmark comparison.
 | |
|         /// Throws on any errors with the setup.
 | |
|         /// </summary>
 | |
|         unsafe static BenchmarkComparisonTypeData GatherComparisonStructure(Type t)
 | |
|         {
 | |
|             //--------
 | |
|             // Determine and validate the benchmark comparison this type is intended for
 | |
|             //--------
 | |
|             Type benchmarkEnumType = null;
 | |
|             foreach(var attributeData in t.GetCustomAttributesData())
 | |
|             {
 | |
|                 if (attributeData.AttributeType == typeof(BenchmarkAttribute))
 | |
|                 {
 | |
|                     benchmarkEnumType = (Type)attributeData.ConstructorArguments[0].Value;
 | |
|                     break;
 | |
|                 }
 | |
|             }
 | |
|             if (benchmarkEnumType == null)
 | |
|                 throw new ArgumentException($"Exactly one [{nameof(BenchmarkAttribute)}] must exist on the type {t.Name} to generate benchmark data");
 | |
| 
 | |
|             // Find the baseline and the formatting for its title name (could be external to the enum or included)
 | |
|             CustomAttributeData attrBenchmarkComparison = null;
 | |
|             List<CustomAttributeData> attrBenchmarkComparisonExternal = new List<CustomAttributeData>();
 | |
|             CustomAttributeData attrBenchmarkFormat = null;
 | |
|             foreach (var attributeData in benchmarkEnumType.GetCustomAttributesData())
 | |
|             {
 | |
|                 if (attributeData.AttributeType == typeof(BenchmarkComparisonAttribute))
 | |
|                 {
 | |
|                     attrBenchmarkComparison = attributeData;
 | |
|                 }
 | |
|                 // Find any other external comparisons
 | |
|                 else if (attributeData.AttributeType == typeof(BenchmarkComparisonExternalAttribute))
 | |
|                 {
 | |
|                     attrBenchmarkComparisonExternal.Add(attributeData);
 | |
|                 }
 | |
|                 // Find optional formatting of table results
 | |
|                 else if (attributeData.AttributeType == typeof(BenchmarkComparisonDisplayAttribute))
 | |
|                 {
 | |
|                     attrBenchmarkFormat = attributeData;
 | |
|                 }
 | |
|             }
 | |
|             if (attrBenchmarkComparison == null)
 | |
|                 throw new ArgumentException($"Exactly one [{nameof(BenchmarkComparisonAttribute)}] must exist on the enum {benchmarkEnumType.Name} to generate benchmark data and define the baseline");
 | |
| 
 | |
|             //--------
 | |
|             // Collect values and name formatting for enum and external
 | |
|             //--------
 | |
| 
 | |
|             // Enum field values
 | |
|             var enumFields = benchmarkEnumType.GetFields(BindingFlags.Static | BindingFlags.Public);
 | |
|             var enumCount = enumFields.Length;
 | |
|             var enumValues = stackalloc int[enumCount];
 | |
|             var enumValuesSet = new HashSet<int>(enumCount);
 | |
|             for (int i = 0; i < enumCount; i++)
 | |
|             {
 | |
|                 int value = (int)enumFields[i].GetRawConstantValue();
 | |
|                 enumValues[i] = value;
 | |
|                 enumValuesSet.Add(value);
 | |
|             }
 | |
| 
 | |
|             var enumFormats = new List<string>(enumCount);
 | |
|             foreach(var x in enumFields)
 | |
|             {
 | |
|                 int oldCount = enumFormats.Count;
 | |
|                 foreach (var attributeData in x.GetCustomAttributesData())
 | |
|                 {
 | |
|                     if (attributeData.AttributeType == typeof(BenchmarkNameAttribute))
 | |
|                     {
 | |
|                         enumFormats.Add((string)attributeData.ConstructorArguments[0].Value);
 | |
|                         break;
 | |
|                     }
 | |
|                 }
 | |
|                 if (oldCount == enumFormats.Count)
 | |
|                     throw new ArgumentException($"{x.Name} as well as all other enum values in {benchmarkEnumType.Name} must have a single [{nameof(BenchmarkNameAttribute)}] defined");
 | |
|             }
 | |
| 
 | |
|             // External values
 | |
|             var externalValues = new List<int>(attrBenchmarkComparisonExternal.Count);
 | |
|             foreach(var x in attrBenchmarkComparisonExternal)
 | |
|             {
 | |
|                 var externalValue = (int)x.ConstructorArguments[0].Value;
 | |
|                 if (enumValuesSet.Contains(externalValue))
 | |
|                     throw new ArgumentException($"Externally-defined benchmark values for {benchmarkEnumType.Name} must not be a duplicate of another enum-defined or externally-defined benchmark value for {benchmarkEnumType.Name}");
 | |
|             }
 | |
|             var externalFormats = new List<string>(attrBenchmarkComparisonExternal.Count);
 | |
|             foreach(var x in attrBenchmarkComparisonExternal)
 | |
|             {
 | |
|                 externalFormats.Add((string)x.ConstructorArguments[1].Value);
 | |
|             }
 | |
|             
 | |
|             var externalCount = externalValues.Count;
 | |
| 
 | |
|             // Baseline value
 | |
|             int baselineValue = (int)attrBenchmarkComparison.ConstructorArguments[0].Value;
 | |
|             string externalBaselineFormat = null;
 | |
|             if (attrBenchmarkComparison.ConstructorArguments.Count == 1)
 | |
|             {
 | |
|                 if (!enumValuesSet.Contains(baselineValue))
 | |
|                     throw new ArgumentException($"{baselineValue} not found in enum {benchmarkEnumType.Name}. Either specify an existing value as the baseline, or add a formatting string for the externally defined baseline value.");
 | |
|             }
 | |
|             else
 | |
|             {
 | |
|                 if (enumValuesSet.Contains(baselineValue))
 | |
|                     throw new ArgumentException($"To specify an enum-defined benchmark baseline in {benchmarkEnumType.Name}, pass only the argument {baselineValue} without a name, as the name requires definition in the enum");
 | |
|                 if (externalValues.Contains(baselineValue))
 | |
|                     throw new ArgumentException($"To specify an external-defined benchmark baseline in {benchmarkEnumType.Name}, define only in [{nameof(BenchmarkComparisonAttribute)}] and omit also defining with [{nameof(BenchmarkComparisonExternalAttribute)}]");
 | |
|                 externalBaselineFormat = (string)attrBenchmarkComparison.ConstructorArguments[1].Value;
 | |
|             }
 | |
| 
 | |
|             // Total
 | |
|             int variantCount = enumCount + externalCount + (externalBaselineFormat == null ? 0 : 1);
 | |
| 
 | |
|             //--------
 | |
|             // Collect name overrides on the specific type with benchmarking methods
 | |
|             //--------
 | |
| 
 | |
|             string defaultNameOverride = null;
 | |
|             var nameOverride = new Dictionary<int, string>();
 | |
|             foreach (var attr in t.CustomAttributes)
 | |
|             {
 | |
|                 if (attr.AttributeType == typeof(BenchmarkNameOverrideAttribute))
 | |
|                 {
 | |
|                     if (attr.ConstructorArguments.Count == 1)
 | |
|                     {
 | |
|                         if (defaultNameOverride != null)
 | |
|                             throw new ArgumentException($"No more than one default name override is allowed for {t.Name} using [{nameof(BenchmarkNameOverrideAttribute)}]");
 | |
|                         defaultNameOverride = (string)attr.ConstructorArguments[0].Value;
 | |
|                     }
 | |
|                     else
 | |
|                     {
 | |
|                         int valueOverride = (int)attr.ConstructorArguments[0].Value;
 | |
|                         if (nameOverride.ContainsKey(valueOverride))
 | |
|                             throw new ArgumentException($"No more than one name override is allowed for benchmark comparison value {valueOverride} using [{nameof(BenchmarkNameOverrideAttribute)}]");
 | |
|                         nameOverride[valueOverride] = (string)attr.ConstructorArguments[1].Value;
 | |
|                     }
 | |
|                 }
 | |
|             }
 | |
| 
 | |
|             //--------
 | |
|             // Record all the information
 | |
|             //--------
 | |
| 
 | |
|             var ret = new BenchmarkComparisonTypeData(variantCount);
 | |
|             ret.defaultName = defaultNameOverride ?? t.Name;
 | |
|             ret.enumType = benchmarkEnumType;
 | |
| 
 | |
|             // Result optional custom formatting
 | |
|             if (attrBenchmarkFormat != null)
 | |
|             {
 | |
|                 ret.resultUnit = (SampleUnit)attrBenchmarkFormat.ConstructorArguments[0].Value;
 | |
|                 ret.resultDecimalPlaces = (int)attrBenchmarkFormat.ConstructorArguments[1].Value;
 | |
|                 ret.resultStatistic = (BenchmarkRankingStatistic)attrBenchmarkFormat.ConstructorArguments[2].Value;
 | |
|             }
 | |
| 
 | |
|             // Enum field values
 | |
|             for (int i = 0; i < enumCount; i++)
 | |
|             {
 | |
|                 ret.names[i] = enumFormats[i];
 | |
|                 ret.values[i] = enumValues[i];
 | |
|                 ret.resultTypes[i] = baselineValue == ret.values[i] ? BenchmarkResultType.NormalBaseline : BenchmarkResultType.Normal;
 | |
|             }
 | |
| 
 | |
|             // External values
 | |
|             for (int i = 0; i < externalCount; i++)
 | |
|             {
 | |
|                 ret.names[enumCount + i] = externalFormats[i];
 | |
|                 ret.values[enumCount + i] = externalValues[i];
 | |
|                 ret.resultTypes[enumCount + i] = BenchmarkResultType.External;
 | |
|             }
 | |
| 
 | |
|             // External baseline value if it exists
 | |
|             if (externalBaselineFormat != null)
 | |
|             {
 | |
|                 ret.names[variantCount - 1] = externalBaselineFormat;
 | |
|                 ret.values[variantCount - 1] = baselineValue;
 | |
|                 ret.resultTypes[variantCount - 1] = BenchmarkResultType.ExternalBaseline;
 | |
|             }
 | |
| 
 | |
|             for (int i = 0; i < variantCount; i++)
 | |
|             {
 | |
|                 if (nameOverride.TryGetValue(ret.values[i], out string name))
 | |
|                     ret.names[i] = string.Format(ret.names[i], name);
 | |
|                 else
 | |
|                     ret.names[i] = string.Format(ret.names[i], ret.defaultName);
 | |
|             }
 | |
|             
 | |
|             if (new HashSet<int>(ret.values).Count != ret.values.Length)
 | |
|                 throw new ArgumentException($"Each enum value and external value in {benchmarkEnumType.Name} must be unique");
 | |
| 
 | |
|             return ret;
 | |
|         }
 | |
| 
 | |
|         /// <summary>
 | |
|         /// Reflects all possible arguments to a performance test method. Finds the parameter which benchmark comparisons
 | |
|         /// are based around (must be an enum type decorated with [BenchmarkComparison] attribute).
 | |
|         ///
 | |
|         /// There is a (usually small) finite set of arguments possible in performance test methods due to
 | |
|         /// requiring [Values(a, b, c)] attribute on any parameter that isn't a bool or enum.
 | |
|         /// </summary>
 | |
|         static void GatherAllArguments(ParameterInfo[] paramInfo, string methodName, BenchmarkComparisonTypeData structure, out int[] argCounts, out CustomAttributeTypedArgument[][] argValues, out string[] argNames, out int paramForComparison)
 | |
|         {
 | |
|             paramForComparison = -1;
 | |
| 
 | |
|             argCounts = new int[paramInfo.Length];
 | |
|             argValues = new CustomAttributeTypedArgument[paramInfo.Length][];
 | |
|             argNames = new string[paramInfo.Length];
 | |
|             for (int p = 0; p < paramInfo.Length; p++)
 | |
|             {
 | |
|                 // It is correct to throw if a parameter doesn't include Values attribute, NUnit errors as well
 | |
|                 CustomAttributeData valuesAttribute = null;
 | |
|                 foreach (var cad in paramInfo[p].GetCustomAttributesData())
 | |
|                 {
 | |
|                     if (cad.AttributeType == typeof(NUnit.Framework.ValuesAttribute))
 | |
|                     {
 | |
|                         valuesAttribute = cad;
 | |
|                         break;
 | |
|                     }
 | |
|                 }
 | |
|                 if (valuesAttribute == null)
 | |
|                     throw new ArgumentException($"No [Values(...)] attribute found for parameter {paramInfo[p].Name} in {methodName}");
 | |
| 
 | |
|                 var values = valuesAttribute.ConstructorArguments;
 | |
| 
 | |
|                 argNames[p] = paramInfo[p].Name;
 | |
| 
 | |
|                 if (paramInfo[p].ParameterType.IsEnum && paramInfo[p].ParameterType.GetCustomAttribute<BenchmarkComparisonAttribute>() != null)
 | |
|                 {
 | |
|                     // [Values] <comparisonEnumType> <paramName>
 | |
|                     //
 | |
|                     // values.Count must be 0 or inconsistent benchmark measurements might be made.
 | |
|                     // Alternatively, we could treat as if it had no arguments for benchmarks, and allow performance testing for regressions
 | |
|                     // to be more specific, but for now it seems like a good idea to perf. test all valid combinations we offer, and in fact
 | |
|                     // a good idea to enforce that in some manner.
 | |
| 
 | |
|                     if (paramInfo[p].ParameterType != structure.enumType)
 | |
|                         throw new ArgumentException($"The method {methodName} parameterizes benchmark comparison type {paramInfo[p].ParameterType.Name} but only supports {structure.enumType.Name}.");
 | |
| 
 | |
|                     if (paramForComparison != -1)
 | |
|                         throw new ArgumentException($"More than one parameter specifies {structure.enumType.Name}. Only one may exist.");
 | |
| 
 | |
|                     paramForComparison = p;
 | |
| 
 | |
|                     argCounts[p] = structure.resultTypes.Length;
 | |
|                     argValues[p] = new CustomAttributeTypedArgument[argCounts[p]];
 | |
| 
 | |
|                     // [Values(...)] <comparisonEnumType> <paramName>
 | |
|                     // This specifies comparison critera, and any excluded values will be shown as not available in the results report
 | |
| 
 | |
|                     if (values.Count == 0)
 | |
|                     {
 | |
|                         // [Values]
 | |
|                         // This is the normal usage encompassing all comparison types
 | |
| 
 | |
|                         for (int e = 0; e < argCounts[p]; e++)
 | |
|                             argValues[p][e] = new CustomAttributeTypedArgument(structure.values[e]);
 | |
|                     }
 | |
|                     else
 | |
|                     {
 | |
|                         // [Values(1-to-3-arguments)] <comparisonEnumType> <paramName>
 | |
|                         var ctorValues = values;
 | |
| 
 | |
|                         if (values.Count == 1 && values[0].ArgumentType == typeof(object[]))
 | |
|                         {
 | |
|                             // [Values(more-than-3-arguments)] <comparisonEnumType> <paramName>
 | |
|                             //
 | |
|                             // This is for ValuesAttribute(params object[] args)
 | |
| 
 | |
|                             var arrayValue = values[0].Value as System.Collections.Generic.IList<CustomAttributeTypedArgument>;
 | |
|                             ctorValues = arrayValue;
 | |
|                         }
 | |
| 
 | |
|                         for (int e = 0; e < argCounts[p]; e++)
 | |
|                         {
 | |
|                             if (structure.resultTypes[e] == BenchmarkResultType.External || structure.resultTypes[e] == BenchmarkResultType.ExternalBaseline)
 | |
|                                 argValues[p][e] = new CustomAttributeTypedArgument(structure.values[e]);
 | |
|                             else
 | |
|                                 argValues[p][e] = default;  // We can later check if ArgumentType is null to determine an unused comparison test
 | |
|                         }
 | |
| 
 | |
|                         // If we don't include NormalBaseline values, it is an error - you can't not include a baseline
 | |
|                         bool hasNormalBaseline = false;
 | |
|                         string normalBaselineName = null;
 | |
|                         for (int i = 0; i < structure.resultTypes.Length; i++)
 | |
|                         {
 | |
|                             if (structure.resultTypes[i] == BenchmarkResultType.NormalBaseline)
 | |
|                             {
 | |
|                                 hasNormalBaseline = true;
 | |
|                                 normalBaselineName = structure.enumType.GetEnumNames()[i];
 | |
|                             }
 | |
|                         }
 | |
| 
 | |
|                         bool specifiedBaseline = !hasNormalBaseline;
 | |
|                         for (int ca = 0; ca < ctorValues.Count; ca++)
 | |
|                         {
 | |
|                             // Ensure it's not some alternative value cast to the enum type such as an external baseline identifying value
 | |
|                             // because that would end up as part of the Performance Test Framework tests.
 | |
|                             if (ctorValues[ca].ArgumentType != structure.enumType)
 | |
|                                 throw new ArgumentException($"Only {structure.enumType} values may be specified. External comparison types are always added automatically.");
 | |
| 
 | |
|                             // Find the index this value would have been at, and set the argValue there to the struct.values for it
 | |
|                             for (int v = 0; v < structure.values.Length; v++)
 | |
|                             {
 | |
|                                 if (structure.values[v] == (int)ctorValues[ca].Value)
 | |
|                                 {
 | |
|                                     argValues[p][v] = new CustomAttributeTypedArgument(structure.values[v]);
 | |
|                                     if (structure.resultTypes[v] == BenchmarkResultType.NormalBaseline)
 | |
|                                         specifiedBaseline = true;
 | |
|                                 }
 | |
|                             }
 | |
|                         }
 | |
| 
 | |
|                         if (!specifiedBaseline)
 | |
|                             throw new ArgumentException($"This comparison type requires the baseline {structure.enumType.Name}.{normalBaselineName} to be measured.");
 | |
|                     }
 | |
|                 }
 | |
|                 else if (values.Count == 0)
 | |
|                 {
 | |
|                     // [Values] <type> <paramName>
 | |
|                     //
 | |
|                     // This has default behaviour for bools and enums, otherwise error
 | |
| 
 | |
|                     if (paramInfo[p].ParameterType == typeof(bool))
 | |
|                     {
 | |
|                         argCounts[p] = 2;
 | |
|                         argValues[p] = new CustomAttributeTypedArgument[]
 | |
|                         {
 | |
|                             new CustomAttributeTypedArgument(true),
 | |
|                             new CustomAttributeTypedArgument(false)
 | |
|                         };
 | |
|                     }
 | |
|                     else if (paramInfo[p].ParameterType.IsEnum)
 | |
|                     {
 | |
|                         var enumValues = Enum.GetValues(paramInfo[p].ParameterType);
 | |
|                         argCounts[p] = enumValues.Length;
 | |
|                         argValues[p] = new CustomAttributeTypedArgument[argCounts[p]];
 | |
|                         for (int e = 0; e < argCounts[p]; e++)
 | |
|                             argValues[p][e] = new CustomAttributeTypedArgument(enumValues.GetValue(e));
 | |
|                     }
 | |
|                     else
 | |
|                         throw new ArgumentException($"[Values] attribute of parameter {paramInfo[p].Name} in {methodName} is empty");
 | |
|                 }
 | |
|                 else if (values.Count == 1 && values[0].ArgumentType == typeof(object[]))
 | |
|                 {
 | |
|                     // [Values(more-than-3-arguments)] <type> <paramName>
 | |
|                     //
 | |
|                     // This is for ValuesAttribute(params object[] args)
 | |
| 
 | |
|                     var arrayValue = values[0].Value as System.Collections.Generic.IList<CustomAttributeTypedArgument>;
 | |
|                     argValues[p] = new CustomAttributeTypedArgument[arrayValue.Count];
 | |
|                     arrayValue.CopyTo(argValues[p], 0);
 | |
|                     argCounts[p] = arrayValue.Count;
 | |
|                 }
 | |
|                 else
 | |
|                 {
 | |
|                     // [Values(1-to-3-arguments)] <type> <paramName>
 | |
|                     argValues[p] = new CustomAttributeTypedArgument[values.Count];
 | |
|                     values.CopyTo(argValues[p], 0);
 | |
|                     argCounts[p] = values.Count;
 | |
|                 }
 | |
|             }
 | |
| 
 | |
|             if (paramForComparison == -1)
 | |
|                 throw new ArgumentException($"No benchmark comparison is parameterized. One must be specified");
 | |
|         }
 | |
| 
 | |
|         /// <summary>
 | |
|         /// Given
 | |
|         /// a) X number of permutations for all arguments to each parameter in a performance test method
 | |
|         /// b) the possible arguments to each parameter
 | |
|         /// c) the parameter defining the benchmark comparison
 | |
|         /// 
 | |
|         /// Return
 | |
|         /// a) the argument set (called variant) for Permutation[0 to X-1]
 | |
|         /// b) the isolated benchmark comparison index, based on the benchmark comparison enum values, for this variant
 | |
|         /// </summary>
 | |
|         static BenchmarkResultType GetVariantArguments(int variantIndex, BenchmarkComparisonTypeData structure, int paramForComparison, CustomAttributeTypedArgument[][] argValues, int[] argCounts, out object[] args, out int comparisonIndex)
 | |
|         {
 | |
|             comparisonIndex = 0;
 | |
| 
 | |
|             int numParams = argValues.Length;
 | |
| 
 | |
|             // Calculate ValuesAttribute indices for each parameter
 | |
|             // Calculate actual comparison index to ensure only benchmarks comparison are bunched together
 | |
|             int[] argValueIndex = new int[numParams];
 | |
|             for (int p = 0, argSet = variantIndex, comparisonMult = 1; p < numParams; p++)
 | |
|             {
 | |
|                 argValueIndex[p] = argSet % argCounts[p];
 | |
|                 argSet = (argSet - argValueIndex[p]) / argCounts[p];
 | |
| 
 | |
|                 if (p != paramForComparison)
 | |
|                 {
 | |
|                     comparisonIndex += argValueIndex[p] * comparisonMult;
 | |
|                     comparisonMult *= argCounts[p];
 | |
|                 }
 | |
|             }
 | |
| 
 | |
|             // Find each argument using above ValuesAttribute indices
 | |
|             args = new object[numParams];
 | |
|             if (argValues[paramForComparison][argValueIndex[paramForComparison]].ArgumentType == null)
 | |
|                 return BenchmarkResultType.Ignored;
 | |
| 
 | |
|             for (int p = 0; p < numParams; p++)
 | |
|                 args[p] = argValues[p][argValueIndex[p]].Value;
 | |
| 
 | |
|             return structure.resultTypes[argValueIndex[paramForComparison]];
 | |
|         }
 | |
| 
 | |
|         /// <summary>
 | |
|         /// Runs benchmarking for all defined benchmark methods in a type.
 | |
|         /// </summary>
 | |
|         static BenchmarkReportGroup GatherGroupData(Type t, BenchmarkComparisonTypeData structure)
 | |
|         {
 | |
|             var group = new BenchmarkReportGroup(structure.defaultName, structure.names, structure.resultTypes, structure.resultDecimalPlaces);
 | |
|             uint groupFootnoteBit = BenchmarkResults.kFlagFootnotes;
 | |
| 
 | |
|             var allMethods = t.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
 | |
|             var methods = new List<MethodInfo>(allMethods.Length);
 | |
|             foreach (var m in allMethods)
 | |
|             {
 | |
|                 if (m.GetCustomAttribute<NUnit.Framework.TestAttribute>() != null && m.GetCustomAttribute<PerformanceAttribute>() != null)
 | |
|                     methods.Add(m);
 | |
|             }
 | |
| 
 | |
|             var inst = Activator.CreateInstance(t);
 | |
|             for (int m = 0; m < methods.Count; m++)
 | |
|             {
 | |
|                 var method = methods[m];
 | |
| 
 | |
|                 // Get ValueAttributes information for all parameters
 | |
|                 GatherAllArguments(method.GetParameters(), $"{t.Name}.{method.Name}", structure,
 | |
|                 out var argCounts, out var argValues, out var argNames, out var paramForComparison);
 | |
| 
 | |
|                 // Record any footnotes for this method
 | |
|                 uint comparisonFootnoteFlags = 0;
 | |
|                 foreach (var cad in method.GetCustomAttributesData())
 | |
|                 {
 | |
|                     if (cad.AttributeType != typeof(BenchmarkTestFootnoteAttribute))
 | |
|                         continue;
 | |
| 
 | |
|                     var footnoteText = new NativeText($"{method.Name}(", Allocator.Persistent);
 | |
|                     int paramsShown = 0;
 | |
|                     for (int p = 0; p < argNames.Length; p++)
 | |
|                     {
 | |
|                         if (p == paramForComparison)
 | |
|                             continue;
 | |
| 
 | |
|                         if (paramsShown++ > 0)
 | |
|                             footnoteText.Append(", ");
 | |
|                         footnoteText.Append(argNames[p]);
 | |
|                     }
 | |
|                     footnoteText.Append(")");
 | |
|                     if (cad.ConstructorArguments.Count == 1)
 | |
|                         footnoteText.Append($" -- {(string)cad.ConstructorArguments[0].Value}");
 | |
|                     group.customFootnotes.Add(groupFootnoteBit, footnoteText);
 | |
|                     comparisonFootnoteFlags |= groupFootnoteBit;
 | |
|                     groupFootnoteBit <<= 1;
 | |
|                 }
 | |
| 
 | |
|                 // Calculate number of variations based on all ValuesAttributes + parameters
 | |
|                 int totalVariants = 1;
 | |
|                 for (int p = 0; p < argCounts.Length; p++)
 | |
|                     totalVariants *= argCounts[p];
 | |
|                 int numComparisons = totalVariants / argCounts[paramForComparison];
 | |
| 
 | |
|                 BenchmarkReportComparison[] comparison = new BenchmarkReportComparison[numComparisons];
 | |
| 
 | |
|                 for (int i = 0; i < totalVariants; i++)
 | |
|                 {
 | |
|                     SetProgressText($"Running benchmark {i + 1}/{totalVariants} for {method.Name}", (float)(m + 1) / methods.Count);
 | |
| 
 | |
|                     // comparisonIndex indicates the variation of a complete benchmark comparison. i.e.
 | |
|                     // you could be benchmarking between 3 different variants (such as NativeArray vs UnsafeArray vs C# Array)
 | |
|                     // but you may also have 4 versions of that (such as 1000 elements, 10000 elements, 100000, and 1000000)
 | |
|                     BenchmarkResultType resultType = GetVariantArguments(i, structure, paramForComparison, argValues, argCounts,
 | |
|                         out var args, out int comparisonIndex);
 | |
|                     if (resultType == BenchmarkResultType.Ignored)
 | |
|                     {
 | |
|                         if (comparison[comparisonIndex].comparisonName.IsEmpty)
 | |
|                             comparison[comparisonIndex] = new BenchmarkReportComparison(method.Name);
 | |
|                         comparison[comparisonIndex].results.Add(BenchmarkResults.Ignored);
 | |
|                         continue;
 | |
|                     }
 | |
| 
 | |
|                     if (comparison[comparisonIndex].comparisonName.IsEmpty)
 | |
|                     {
 | |
|                         string paramsString = null;
 | |
|                         for (int p = 0; p < argCounts.Length; p++)
 | |
|                         {
 | |
|                             if (p == paramForComparison)
 | |
|                                 continue;
 | |
|                             if (paramsString == null)
 | |
|                                 paramsString = $"({args[p]}";
 | |
|                             else
 | |
|                                 paramsString += $", {args[p]}";
 | |
|                         }
 | |
| 
 | |
|                         if (paramsString != null)
 | |
|                             comparison[comparisonIndex] = new BenchmarkReportComparison($"{method.Name}{paramsString})");
 | |
|                         else
 | |
|                             comparison[comparisonIndex] = new BenchmarkReportComparison(method.Name);
 | |
|                     }
 | |
| 
 | |
|                     // Call the performance method
 | |
|                     method.Invoke(inst, args);
 | |
| 
 | |
|                     var results = BenchmarkMeasure.CalculateLastResults(structure.resultUnit, structure.resultStatistic);
 | |
|                     comparison[comparisonIndex].results.Add(results);
 | |
|                 }
 | |
| 
 | |
|                 // Add all sets of comparisons to the full group
 | |
|                 for (int i = 0; i < numComparisons; i++)
 | |
|                 {
 | |
|                     comparison[i].footnoteFlags |= comparisonFootnoteFlags;
 | |
|                     comparison[i].RankResults(structure.resultTypes);
 | |
|                     group.comparisons.Add(comparison[i]);
 | |
|                 }
 | |
|             }
 | |
| 
 | |
|             return group;
 | |
|         }
 | |
| 
 | |
|         /// <summary>
 | |
|         /// Runs benchmarking for all given types.
 | |
|         /// </summary>
 | |
|         /// <param name="title">The title to the full report</param>
 | |
|         /// <param name="benchmarkTypes">An array of types each marked with <see cref="BenchmarkAttribute"/></param>
 | |
|         /// <returns></returns>
 | |
|         public static BenchmarkReports RunBenchmarks(string title, Type[] benchmarkTypes)
 | |
|         {
 | |
|             BenchmarkMeasure.ForBenchmarks = true;
 | |
|             BenchmarkReports reports = default;
 | |
| 
 | |
|             try
 | |
|             {
 | |
|                 reports = new BenchmarkReports(title);
 | |
| 
 | |
|                 for (int i = 0; i < benchmarkTypes.Length; i++)
 | |
|                 {
 | |
|                     StartProgress(title, i, benchmarkTypes.Length, benchmarkTypes[i].Name);
 | |
|                     SetProgressText("Gathering benchmark data", 0);
 | |
|                     var benchmarkStructure = GatherComparisonStructure(benchmarkTypes[i]);
 | |
|                     var group = GatherGroupData(benchmarkTypes[i], benchmarkStructure);
 | |
|                     reports.groups.Add(group);
 | |
|                 }
 | |
|             }
 | |
|             finally
 | |
|             {
 | |
|                 BenchmarkMeasure.ForBenchmarks = false;
 | |
|                 EndProgress();
 | |
|             }
 | |
| 
 | |
|             return reports;
 | |
|         }
 | |
|     }
 | |
| }
 |