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; | ||
|  |         } | ||
|  |     } | ||
|  | } |