Files
ProjectEleri/Plugins/DirectiveUtilities/Source/DirectiveUtilitiesTests/Private/Tests/DirectiveUtilRuntimePerformanceTest.cpp

1518 lines
51 KiB
C++

// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
#include "Libraries/DirectiveUtilArrayFunctionLibrary.h"
#include "Libraries/DirectiveUtilMathFunctionLibrary.h"
#include "Libraries/DirectiveUtilStringFunctionLibrary.h"
#include "Tests/DirectiveUtilTestObject.h"
#include "Algo/Reverse.h"
#include "HAL/FileManager.h"
#include "HAL/PlatformMisc.h"
#include "HAL/PlatformProperties.h"
#include "HAL/PlatformTime.h"
#include "Interfaces/IPluginManager.h"
#include "Kismet/KismetArrayLibrary.h"
#include "Misc/AutomationTest.h"
#include "Misc/CommandLine.h"
#include "Misc/DateTime.h"
#include "Misc/EngineVersion.h"
#include "Misc/FileHelper.h"
#include "Misc/Parse.h"
#include "Misc/Paths.h"
#include "UObject/UnrealType.h"
namespace DirectiveUtilRuntimePerformance
{
struct FResult
{
FString Name;
int32 ElementCount = 0;
int32 Parameter = 0;
double MedianMilliseconds = 0.0;
double MinimumMilliseconds = 0.0;
double MaximumMilliseconds = 0.0;
int32 SampleCount = 0;
FString GetKey() const
{
return FString::Printf(TEXT("%s|%d|%d"), *Name, ElementCount, Parameter);
}
};
struct FComparisonResult
{
FString ElementType;
FString Pattern;
int32 ElementCount = 0;
int32 MatchCount = 0;
double BeforeMedianMilliseconds = 0.0;
double BeforeMinimumMilliseconds = 0.0;
double BeforeMaximumMilliseconds = 0.0;
double AfterMedianMilliseconds = 0.0;
double AfterMinimumMilliseconds = 0.0;
double AfterMaximumMilliseconds = 0.0;
int32 SampleCount = 0;
bool bOutputsMatch = true;
};
struct FAppendComparisonResult
{
FString ElementType;
FString Scenario;
int32 SourceCount = 0;
int32 InitialTargetCount = 0;
double BeforeMedianMilliseconds = 0.0;
double BeforeMinimumMilliseconds = 0.0;
double BeforeMaximumMilliseconds = 0.0;
double AfterMedianMilliseconds = 0.0;
double AfterMinimumMilliseconds = 0.0;
double AfterMaximumMilliseconds = 0.0;
int32 SampleCount = 0;
bool bOutputsMatch = true;
};
template <typename PrepareType, typename OperationType>
FResult Measure(
const FString& Name,
const int32 ElementCount,
const int32 Parameter,
const int32 SampleCount,
PrepareType&& Prepare,
OperationType&& Operation)
{
Prepare();
Operation();
const double WarmupStartSeconds = FPlatformTime::Seconds();
do
{
Prepare();
Operation();
}
while (FPlatformTime::Seconds() - WarmupStartSeconds < 0.01);
TArray<double> Samples;
Samples.Reserve(SampleCount);
for (int32 SampleIndex = 0; SampleIndex < SampleCount; ++SampleIndex)
{
Prepare();
const uint64 StartCycles = FPlatformTime::Cycles64();
Operation();
const uint64 ElapsedCycles = FPlatformTime::Cycles64() - StartCycles;
Samples.Add(FPlatformTime::ToMilliseconds64(ElapsedCycles));
}
Samples.Sort();
FResult Result;
Result.Name = Name;
Result.ElementCount = ElementCount;
Result.Parameter = Parameter;
Result.MedianMilliseconds = Samples[Samples.Num() / 2];
Result.MinimumMilliseconds = Samples[0];
Result.MaximumMilliseconds = Samples.Last();
Result.SampleCount = SampleCount;
return Result;
}
template <typename ArrayType, typename BeforeOperationType, typename AfterOperationType>
FAppendComparisonResult MeasureAppendComparison(
const FString& ElementType,
const FString& Scenario,
const ArrayType& InitialTarget,
const ArrayType& Source,
const int32 SampleCount,
BeforeOperationType&& BeforeOperation,
AfterOperationType&& AfterOperation)
{
ArrayType BeforeTarget;
ArrayType AfterTarget;
ArrayType BeforeSource;
ArrayType AfterSource;
auto Prepare = [&]()
{
BeforeTarget = InitialTarget;
AfterTarget = InitialTarget;
BeforeSource = Source;
AfterSource = Source;
};
auto RunBefore = [&]()
{
BeforeOperation(BeforeTarget, BeforeSource);
};
auto RunAfter = [&]()
{
AfterOperation(AfterTarget, AfterSource);
};
Prepare();
RunBefore();
RunAfter();
bool bOutputsMatch = BeforeTarget == AfterTarget
&& BeforeSource == Source
&& AfterSource == Source;
TArray<double> BeforeSamples;
TArray<double> AfterSamples;
BeforeSamples.Reserve(SampleCount);
AfterSamples.Reserve(SampleCount);
auto TimeOperation = [](auto&& Operation)
{
const uint64 StartCycles = FPlatformTime::Cycles64();
Operation();
return FPlatformTime::ToMilliseconds64(FPlatformTime::Cycles64() - StartCycles);
};
for (int32 SampleIndex = 0; SampleIndex < SampleCount; ++SampleIndex)
{
Prepare();
if (SampleIndex % 2 == 0)
{
BeforeSamples.Add(TimeOperation(RunBefore));
AfterSamples.Add(TimeOperation(RunAfter));
}
else
{
AfterSamples.Add(TimeOperation(RunAfter));
BeforeSamples.Add(TimeOperation(RunBefore));
}
bOutputsMatch = bOutputsMatch
&& BeforeTarget == AfterTarget
&& BeforeSource == Source
&& AfterSource == Source;
}
BeforeSamples.Sort();
AfterSamples.Sort();
FAppendComparisonResult Result;
Result.ElementType = ElementType;
Result.Scenario = Scenario;
Result.SourceCount = Source.Num();
Result.InitialTargetCount = InitialTarget.Num();
Result.BeforeMedianMilliseconds = BeforeSamples[BeforeSamples.Num() / 2];
Result.BeforeMinimumMilliseconds = BeforeSamples[0];
Result.BeforeMaximumMilliseconds = BeforeSamples.Last();
Result.AfterMedianMilliseconds = AfterSamples[AfterSamples.Num() / 2];
Result.AfterMinimumMilliseconds = AfterSamples[0];
Result.AfterMaximumMilliseconds = AfterSamples.Last();
Result.SampleCount = SampleCount;
Result.bOutputsMatch = bOutputsMatch;
return Result;
}
template <typename ArrayType, typename BeforeOperationType, typename AfterOperationType>
FComparisonResult MeasureComparison(
const FString& ElementType,
const FString& Pattern,
const ArrayType& Source,
const int32 MatchCount,
const int32 SampleCount,
BeforeOperationType&& BeforeOperation,
AfterOperationType&& AfterOperation)
{
ArrayType BeforeValues = Source;
ArrayType AfterValues = Source;
bool bBeforeRemoved = BeforeOperation(BeforeValues);
bool bAfterRemoved = AfterOperation(AfterValues);
TArray<double> BeforeSamples;
TArray<double> AfterSamples;
BeforeSamples.Reserve(SampleCount);
AfterSamples.Reserve(SampleCount);
bool bOutputsMatch = bBeforeRemoved == bAfterRemoved && BeforeValues == AfterValues;
auto TimeOperation = [](auto&& Operation)
{
const uint64 StartCycles = FPlatformTime::Cycles64();
const bool bRemoved = Operation();
const uint64 ElapsedCycles = FPlatformTime::Cycles64() - StartCycles;
return TPair<double, bool>(FPlatformTime::ToMilliseconds64(ElapsedCycles), bRemoved);
};
for (int32 SampleIndex = 0; SampleIndex < SampleCount; ++SampleIndex)
{
BeforeValues = Source;
AfterValues = Source;
TPair<double, bool> BeforeTiming;
TPair<double, bool> AfterTiming;
if (SampleIndex % 2 == 0)
{
BeforeTiming = TimeOperation([&]() { return BeforeOperation(BeforeValues); });
AfterTiming = TimeOperation([&]() { return AfterOperation(AfterValues); });
}
else
{
AfterTiming = TimeOperation([&]() { return AfterOperation(AfterValues); });
BeforeTiming = TimeOperation([&]() { return BeforeOperation(BeforeValues); });
}
BeforeSamples.Add(BeforeTiming.Key);
AfterSamples.Add(AfterTiming.Key);
bOutputsMatch = bOutputsMatch
&& BeforeTiming.Value == AfterTiming.Value
&& BeforeValues == AfterValues;
}
BeforeSamples.Sort();
AfterSamples.Sort();
FComparisonResult Result;
Result.ElementType = ElementType;
Result.Pattern = Pattern;
Result.ElementCount = Source.Num();
Result.MatchCount = MatchCount;
Result.BeforeMedianMilliseconds = BeforeSamples[BeforeSamples.Num() / 2];
Result.BeforeMinimumMilliseconds = BeforeSamples[0];
Result.BeforeMaximumMilliseconds = BeforeSamples.Last();
Result.AfterMedianMilliseconds = AfterSamples[AfterSamples.Num() / 2];
Result.AfterMinimumMilliseconds = AfterSamples[0];
Result.AfterMaximumMilliseconds = AfterSamples.Last();
Result.SampleCount = SampleCount;
Result.bOutputsMatch = bOutputsMatch;
return Result;
}
TArray<int32> MakeSequentialIntegers(const int32 Count)
{
TArray<int32> Values;
Values.SetNumUninitialized(Count);
for (int32 Index = 0; Index < Count; ++Index)
{
Values[Index] = Index;
}
return Values;
}
TArray<int32> MakeShuffledIndices(const int32 Count)
{
TArray<int32> Indices = MakeSequentialIntegers(Count);
FRandomStream RandomStream(1729);
for (int32 Index = Count - 1; Index > 0; --Index)
{
Indices.Swap(Index, RandomStream.RandRange(0, Index));
}
return Indices;
}
TArray<int32> MakeRepeatingIntegers(const int32 Count, const int32 DistinctCount)
{
TArray<int32> Values;
Values.Reserve(Count);
for (int32 Index = 0; Index < Count; ++Index)
{
Values.Add(Index % DistinctCount);
}
return Values;
}
TArray<FDirectiveUtilPodValue> MakeSequentialPodValues(const int32 Count)
{
TArray<FDirectiveUtilPodValue> Values;
Values.SetNumUninitialized(Count);
for (int32 Index = 0; Index < Count; ++Index)
{
Values[Index].Index = Index;
Values[Index].Weight = static_cast<float>(Index) + 0.5f;
}
return Values;
}
bool IsRemovalMatch(const FString& Pattern, const int32 Index, const int32 Count)
{
if (Pattern == TEXT("single_tail"))
{
return Index == Count - 1;
}
if (Pattern == TEXT("every_64"))
{
return Index % 64 == 63;
}
if (Pattern == TEXT("clustered"))
{
return Index >= Count / 3 && Index < Count * 2 / 3;
}
if (Pattern == TEXT("alternating"))
{
return Index % 2 == 0;
}
return Pattern == TEXT("all");
}
TArray<int32> MakeRemovalIntegers(
const int32 Count,
const FString& Pattern,
const int32 ItemToRemove,
int32& OutMatchCount)
{
TArray<int32> Values;
Values.SetNumUninitialized(Count);
OutMatchCount = 0;
for (int32 Index = 0; Index < Count; ++Index)
{
const bool bMatches = Pattern != TEXT("no_match") && IsRemovalMatch(Pattern, Index, Count);
Values[Index] = bMatches ? ItemToRemove : Index + 1;
OutMatchCount += bMatches ? 1 : 0;
}
return Values;
}
TArray<FString> MakeRemovalStrings(
const int32 Count,
const FString& Pattern,
const FString& ItemToRemove,
int32& OutMatchCount)
{
TArray<FString> Values;
Values.Reserve(Count);
OutMatchCount = 0;
for (int32 Index = 0; Index < Count; ++Index)
{
const bool bMatches = Pattern != TEXT("no_match") && IsRemovalMatch(Pattern, Index, Count);
Values.Add(bMatches ? ItemToRemove : FString::Printf(TEXT("Value%06d"), Index));
OutMatchCount += bMatches ? 1 : 0;
}
return Values;
}
TArray<FString> MakeNaturalSortStrings(const int32 Count)
{
const TArray<int32> Indices = MakeShuffledIndices(Count);
TArray<FString> Values;
Values.Reserve(Count);
for (const int32 Index : Indices)
{
Values.Add(FString::Printf(TEXT("Item%d"), Index));
}
return Values;
}
TArray<FName> MakeNaturalSortNames(const int32 Count)
{
const TArray<int32> Indices = MakeShuffledIndices(Count);
TArray<FName> Values;
Values.Reserve(Count);
for (const int32 Index : Indices)
{
Values.Add(FName(*FString::Printf(TEXT("Actor%d"), Index)));
}
return Values;
}
TArray<FString> MakeStringMatchCandidates(const int32 Count)
{
TArray<FString> Candidates;
Candidates.Reserve(Count);
for (int32 Index = 0; Index < Count; ++Index)
{
Candidates.Add(FString::Printf(TEXT("DirectiveUtilityCandidate%05d"), Index));
}
return Candidates;
}
FString GetOutputPath()
{
FString OutputPath;
if (!FParse::Value(FCommandLine::Get(), TEXT("DirectiveUtilitiesPerfOutput="), OutputPath))
{
OutputPath = FPaths::ProjectSavedDir() / TEXT("Automation/DirectiveUtilities/RuntimePerformance.csv");
}
return FPaths::ConvertRelativePathToFull(OutputPath);
}
FString GetComparisonOutputPath()
{
FString OutputPath;
if (!FParse::Value(FCommandLine::Get(), TEXT("DirectiveUtilitiesPerfComparisonOutput="), OutputPath))
{
const FString RuntimeOutputPath = GetOutputPath();
OutputPath = FPaths::GetPath(RuntimeOutputPath)
/ (FPaths::GetBaseFilename(RuntimeOutputPath) + TEXT("-remove-all-comparison.csv"));
}
return FPaths::ConvertRelativePathToFull(OutputPath);
}
FString GetAppendComparisonOutputPath()
{
FString OutputPath;
if (!FParse::Value(FCommandLine::Get(), TEXT("DirectiveUtilitiesPerfAppendComparisonOutput="), OutputPath))
{
const FString RuntimeOutputPath = GetOutputPath();
OutputPath = FPaths::GetPath(RuntimeOutputPath)
/ (FPaths::GetBaseFilename(RuntimeOutputPath) + TEXT("-append-comparison.csv"));
}
return FPaths::ConvertRelativePathToFull(OutputPath);
}
FString GetInsertComparisonOutputPath()
{
const FString RuntimeOutputPath = GetOutputPath();
return FPaths::ConvertRelativePathToFull(
FPaths::GetPath(RuntimeOutputPath)
/ (FPaths::GetBaseFilename(RuntimeOutputPath) + TEXT("-insert-comparison.csv")));
}
FString GetRemoveIndicesComparisonOutputPath()
{
const FString RuntimeOutputPath = GetOutputPath();
return FPaths::ConvertRelativePathToFull(
FPaths::GetPath(RuntimeOutputPath)
/ (FPaths::GetBaseFilename(RuntimeOutputPath) + TEXT("-remove-indices-comparison.csv")));
}
FString GetBuildConfigurationName()
{
#if UE_BUILD_DEBUG
return TEXT("Debug");
#elif UE_BUILD_DEVELOPMENT
return TEXT("Development");
#elif UE_BUILD_TEST
return TEXT("Test");
#elif UE_BUILD_SHIPPING
return TEXT("Shipping");
#else
return TEXT("Unknown");
#endif
}
FString GetPluginVersion()
{
const TSharedPtr<IPlugin> Plugin = IPluginManager::Get().FindPlugin(TEXT("DirectiveUtilities"));
return Plugin.IsValid() ? Plugin->GetDescriptor().VersionName : TEXT("Unknown");
}
FString SanitizeMetadata(FString Value)
{
Value.ReplaceInline(TEXT(","), TEXT(";"));
Value.ReplaceInline(TEXT("\r"), TEXT(" "));
Value.ReplaceInline(TEXT("\n"), TEXT(" "));
return Value;
}
bool LoadBaseline(TMap<FString, double>& OutMedians, FString& OutPath)
{
if (!FParse::Value(FCommandLine::Get(), TEXT("DirectiveUtilitiesPerfBaseline="), OutPath))
{
return true;
}
OutPath = FPaths::ConvertRelativePathToFull(OutPath);
FString Contents;
if (!FFileHelper::LoadFileToString(Contents, *OutPath))
{
return false;
}
TArray<FString> Lines;
Contents.ParseIntoArrayLines(Lines);
for (const FString& Line : Lines)
{
if (Line.IsEmpty() || Line.StartsWith(TEXT("#")) || Line.StartsWith(TEXT("benchmark,")))
{
continue;
}
TArray<FString> Fields;
Line.ParseIntoArray(Fields, TEXT(","), false);
if (Fields.Num() < 4)
{
continue;
}
const FString Key = FString::Printf(
TEXT("%s|%d|%d"),
*Fields[0],
FCString::Atoi(*Fields[1]),
FCString::Atoi(*Fields[2]));
OutMedians.Add(Key, FCString::Atod(*Fields[3]));
}
return true;
}
FString BuildCsv(
const TArray<FResult>& Results,
const TMap<FString, double>& BaselineMedians,
const FString& BaselinePath)
{
FString Revision;
FParse::Value(FCommandLine::Get(), TEXT("DirectiveUtilitiesPerfRevision="), Revision);
FString Csv;
Csv += FString::Printf(TEXT("#engine,%s\n"), *SanitizeMetadata(FEngineVersion::Current().ToString()));
Csv += FString::Printf(TEXT("#platform,%hs\n"), FPlatformProperties::PlatformName());
Csv += FString::Printf(TEXT("#configuration,%s\n"), *GetBuildConfigurationName());
Csv += FString::Printf(TEXT("#plugin_version,%s\n"), *SanitizeMetadata(GetPluginVersion()));
Csv += FString::Printf(TEXT("#timestamp_utc,%s\n"), *FDateTime::UtcNow().ToIso8601());
Csv += FString::Printf(TEXT("#revision,%s\n"), *SanitizeMetadata(Revision));
if (!BaselinePath.IsEmpty())
{
Csv += FString::Printf(TEXT("#baseline,%s\n"), *BaselinePath);
}
Csv += TEXT("benchmark,element_count,parameter,median_ms,min_ms,max_ms,samples,baseline_median_ms,speedup,change_percent\n");
for (const FResult& Result : Results)
{
FString BaselineMedian;
FString Speedup;
FString ChangePercent;
if (const double* Baseline = BaselineMedians.Find(Result.GetKey()))
{
BaselineMedian = FString::Printf(TEXT("%.9f"), *Baseline);
if (*Baseline > 0.0 && Result.MedianMilliseconds > 0.0)
{
Speedup = FString::Printf(TEXT("%.4f"), *Baseline / Result.MedianMilliseconds);
ChangePercent = FString::Printf(
TEXT("%.2f"),
((*Baseline - Result.MedianMilliseconds) / *Baseline) * 100.0);
}
}
Csv += FString::Printf(
TEXT("%s,%d,%d,%.9f,%.9f,%.9f,%d,%s,%s,%s\n"),
*Result.Name,
Result.ElementCount,
Result.Parameter,
Result.MedianMilliseconds,
Result.MinimumMilliseconds,
Result.MaximumMilliseconds,
Result.SampleCount,
*BaselineMedian,
*Speedup,
*ChangePercent);
}
return Csv;
}
FString BuildComparisonCsv(const TArray<FComparisonResult>& Results)
{
FString Revision;
FParse::Value(FCommandLine::Get(), TEXT("DirectiveUtilitiesPerfRevision="), Revision);
FString Csv;
Csv += FString::Printf(TEXT("#engine,%s\n"), *SanitizeMetadata(FEngineVersion::Current().ToString()));
Csv += FString::Printf(TEXT("#platform,%hs\n"), FPlatformProperties::PlatformName());
Csv += FString::Printf(TEXT("#cpu,%s\n"), *SanitizeMetadata(FPlatformMisc::GetCPUBrand().TrimStartAndEnd()));
Csv += FString::Printf(TEXT("#configuration,%s\n"), *GetBuildConfigurationName());
Csv += FString::Printf(TEXT("#plugin_version,%s\n"), *SanitizeMetadata(GetPluginVersion()));
Csv += FString::Printf(TEXT("#timestamp_utc,%s\n"), *FDateTime::UtcNow().ToIso8601());
Csv += FString::Printf(TEXT("#revision,%s\n"), *SanitizeMetadata(Revision));
Csv += TEXT("element_type,pattern,element_count,match_count,before_median_ms,before_min_ms,before_max_ms,after_median_ms,after_min_ms,after_max_ms,samples,speedup,time_reduction_percent\n");
for (const FComparisonResult& Result : Results)
{
const double Speedup = Result.AfterMedianMilliseconds > 0.0
? Result.BeforeMedianMilliseconds / Result.AfterMedianMilliseconds
: 0.0;
const double TimeReductionPercent = Result.BeforeMedianMilliseconds > 0.0
? ((Result.BeforeMedianMilliseconds - Result.AfterMedianMilliseconds) / Result.BeforeMedianMilliseconds) * 100.0
: 0.0;
Csv += FString::Printf(
TEXT("%s,%s,%d,%d,%.9f,%.9f,%.9f,%.9f,%.9f,%.9f,%d,%.4f,%.2f\n"),
*Result.ElementType,
*Result.Pattern,
Result.ElementCount,
Result.MatchCount,
Result.BeforeMedianMilliseconds,
Result.BeforeMinimumMilliseconds,
Result.BeforeMaximumMilliseconds,
Result.AfterMedianMilliseconds,
Result.AfterMinimumMilliseconds,
Result.AfterMaximumMilliseconds,
Result.SampleCount,
Speedup,
TimeReductionPercent);
}
return Csv;
}
FString BuildAppendComparisonCsv(const TArray<FAppendComparisonResult>& Results)
{
FString Revision;
FParse::Value(FCommandLine::Get(), TEXT("DirectiveUtilitiesPerfRevision="), Revision);
FString Csv;
Csv += FString::Printf(TEXT("#engine,%s\n"), *SanitizeMetadata(FEngineVersion::Current().ToString()));
Csv += FString::Printf(TEXT("#platform,%hs\n"), FPlatformProperties::PlatformName());
Csv += FString::Printf(TEXT("#cpu,%s\n"), *SanitizeMetadata(FPlatformMisc::GetCPUBrand().TrimStartAndEnd()));
Csv += FString::Printf(TEXT("#configuration,%s\n"), *GetBuildConfigurationName());
Csv += FString::Printf(TEXT("#plugin_version,%s\n"), *SanitizeMetadata(GetPluginVersion()));
Csv += FString::Printf(TEXT("#timestamp_utc,%s\n"), *FDateTime::UtcNow().ToIso8601());
Csv += FString::Printf(TEXT("#revision,%s\n"), *SanitizeMetadata(Revision));
Csv += TEXT("element_type,scenario,source_count,initial_target_count,before_median_ms,before_min_ms,before_max_ms,after_median_ms,after_min_ms,after_max_ms,samples,speedup,time_reduction_percent\n");
for (const FAppendComparisonResult& Result : Results)
{
const double Speedup = Result.AfterMedianMilliseconds > 0.0
? Result.BeforeMedianMilliseconds / Result.AfterMedianMilliseconds
: 0.0;
const double TimeReductionPercent = Result.BeforeMedianMilliseconds > 0.0
? ((Result.BeforeMedianMilliseconds - Result.AfterMedianMilliseconds) / Result.BeforeMedianMilliseconds) * 100.0
: 0.0;
Csv += FString::Printf(
TEXT("%s,%s,%d,%d,%.9f,%.9f,%.9f,%.9f,%.9f,%.9f,%d,%.4f,%.2f\n"),
*Result.ElementType,
*Result.Scenario,
Result.SourceCount,
Result.InitialTargetCount,
Result.BeforeMedianMilliseconds,
Result.BeforeMinimumMilliseconds,
Result.BeforeMaximumMilliseconds,
Result.AfterMedianMilliseconds,
Result.AfterMinimumMilliseconds,
Result.AfterMaximumMilliseconds,
Result.SampleCount,
Speedup,
TimeReductionPercent);
}
return Csv;
}
}
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
FDirectiveUtilRuntimePerformanceTest,
"Performance.DirectiveUtilities.Runtime",
EAutomationTestFlags::EditorContext | EAutomationTestFlags::PerfFilter)
bool FDirectiveUtilRuntimePerformanceTest::RunTest(const FString& Parameters)
{
using namespace DirectiveUtilRuntimePerformance;
UDirectiveUtilTestObject* TestObject = NewObject<UDirectiveUtilTestObject>();
FArrayProperty* ArrayProperty = FindFProperty<FArrayProperty>(
UDirectiveUtilTestObject::StaticClass(),
GET_MEMBER_NAME_CHECKED(UDirectiveUtilTestObject, TestArray));
if (!TestNotNull(TEXT("Integer array property is available"), ArrayProperty))
{
return false;
}
FArrayProperty* StringArrayProperty = FindFProperty<FArrayProperty>(
UDirectiveUtilTestObject::StaticClass(),
GET_MEMBER_NAME_CHECKED(UDirectiveUtilTestObject, TestStringArray));
if (!TestNotNull(TEXT("String array property is available"), StringArrayProperty))
{
return false;
}
FArrayProperty* PodArrayProperty = FindFProperty<FArrayProperty>(
UDirectiveUtilTestObject::StaticClass(),
GET_MEMBER_NAME_CHECKED(UDirectiveUtilTestObject, TestPodArray));
if (!TestNotNull(TEXT("POD struct array property is available"), PodArrayProperty))
{
return false;
}
TestFalse(TEXT("POD struct array uses reflected grouping"), PodArrayProperty->Inner->HasAllPropertyFlags(CPF_HasGetValueTypeHash));
constexpr int32 SampleCount = 7;
TArray<FResult> Results;
TArray<FComparisonResult> ComparisonResults;
TArray<FAppendComparisonResult> AppendComparisonResults;
TArray<FAppendComparisonResult> InsertComparisonResults;
TArray<FAppendComparisonResult> RemoveIndicesComparisonResults;
for (const int32 ElementCount : {1000, 10000, 100000, 250000, 1000000})
{
const TArray<int32> Source = MakeSequentialIntegers(ElementCount);
const TArray<int32> EmptyTarget;
const TArray<int32> PopulatedTarget = MakeSequentialIntegers(ElementCount);
auto BeforeAppend = [&](TArray<int32>& Target, TArray<int32>& AppendSource)
{
UKismetArrayLibrary::GenericArray_Append(
&Target,
ArrayProperty,
&AppendSource,
ArrayProperty);
};
auto AfterAppend = [&](TArray<int32>& Target, TArray<int32>& AppendSource)
{
UDirectiveUtilArrayFunctionLibrary::GenericArray_AppendOptimized(
&Target,
ArrayProperty,
&AppendSource,
ArrayProperty);
};
AppendComparisonResults.Add(MeasureAppendComparison(
TEXT("int32"),
TEXT("empty_target"),
EmptyTarget,
Source,
SampleCount,
BeforeAppend,
AfterAppend));
AppendComparisonResults.Add(MeasureAppendComparison(
TEXT("int32"),
TEXT("populated_target"),
PopulatedTarget,
Source,
SampleCount,
BeforeAppend,
AfterAppend));
}
for (const int32 ElementCount : {1000, 10000, 100000})
{
TArray<FString> Source;
Source.Reserve(ElementCount);
for (int32 Index = 0; Index < ElementCount; ++Index)
{
Source.Add(FString::Printf(TEXT("Value%06d"), Index));
}
const TArray<FString> EmptyTarget;
AppendComparisonResults.Add(MeasureAppendComparison(
TEXT("FString"),
TEXT("empty_target"),
EmptyTarget,
Source,
SampleCount,
[&](TArray<FString>& Target, TArray<FString>& AppendSource)
{
UKismetArrayLibrary::GenericArray_Append(
&Target,
StringArrayProperty,
&AppendSource,
StringArrayProperty);
},
[&](TArray<FString>& Target, TArray<FString>& AppendSource)
{
UDirectiveUtilArrayFunctionLibrary::GenericArray_AppendOptimized(
&Target,
StringArrayProperty,
&AppendSource,
StringArrayProperty);
}));
}
for (const int32 TargetCount : {256, 1024, 4096, 16384})
{
const TArray<int32> InitialTarget = MakeSequentialIntegers(TargetCount);
TArray<int32> InsertSource = MakeSequentialIntegers(FMath::Max(1, TargetCount / 4));
for (int32& Value : InsertSource)
{
Value += TargetCount;
}
for (const TPair<FString, int32>& Scenario : {
TPair<FString, int32>(TEXT("front"), 0),
TPair<FString, int32>(TEXT("middle"), TargetCount / 2),
TPair<FString, int32>(TEXT("end"), TargetCount)
})
{
const int32 InsertIndex = Scenario.Value;
InsertComparisonResults.Add(MeasureAppendComparison(
TEXT("int32"),
Scenario.Key,
InitialTarget,
InsertSource,
SampleCount,
[&](TArray<int32>& Target, TArray<int32>& Source)
{
for (int32 SourceIndex = 0; SourceIndex < Source.Num(); ++SourceIndex)
{
UKismetArrayLibrary::GenericArray_Insert(
&Target,
ArrayProperty,
&Source[SourceIndex],
InsertIndex + SourceIndex);
}
},
[&](TArray<int32>& Target, TArray<int32>& Source)
{
UDirectiveUtilArrayFunctionLibrary::GenericArray_InsertOptimized(
&Target,
ArrayProperty,
&Source,
ArrayProperty,
InsertIndex);
}));
}
TArray<int32> RemovalIndices;
for (int32 Index = 1; Index < TargetCount; Index += 4)
{
RemovalIndices.Add(Index);
}
RemoveIndicesComparisonResults.Add(MeasureAppendComparison(
TEXT("int32"),
TEXT("every_4"),
InitialTarget,
RemovalIndices,
SampleCount,
[&](TArray<int32>& Target, TArray<int32>& Indices)
{
for (int32 IndexOffset = Indices.Num() - 1; IndexOffset >= 0; --IndexOffset)
{
UKismetArrayLibrary::GenericArray_Remove(
&Target,
ArrayProperty,
Indices[IndexOffset]);
}
},
[&](TArray<int32>& Target, TArray<int32>& Indices)
{
UDirectiveUtilArrayFunctionLibrary::GenericArray_RemoveAtIndices(
&Target,
ArrayProperty,
Indices);
}));
}
const TArray<FString> RemovalPatterns = {
TEXT("no_match"),
TEXT("single_tail"),
TEXT("every_64"),
TEXT("clustered"),
TEXT("alternating"),
TEXT("all")
};
constexpr int32 IntegerToRemove = 0;
for (const int32 ElementCount : {256, 1024, 4096, 16384})
{
for (const FString& Pattern : RemovalPatterns)
{
int32 MatchCount = 0;
const TArray<int32> Source = MakeRemovalIntegers(
ElementCount,
Pattern,
IntegerToRemove,
MatchCount);
ComparisonResults.Add(MeasureComparison(
TEXT("int32"),
Pattern,
Source,
MatchCount,
SampleCount,
[&](TArray<int32>& Values)
{
return UKismetArrayLibrary::GenericArray_RemoveItem(&Values, ArrayProperty, &IntegerToRemove);
},
[&](TArray<int32>& Values)
{
return UDirectiveUtilArrayFunctionLibrary::GenericArray_RemoveAllOccurrences(
&Values,
ArrayProperty,
&IntegerToRemove);
}));
}
}
// The scaling curve uses every_64 only to keep the stock path's runtime bounded.
for (const int32 ElementCount : {1000, 10000, 100000, 250000, 1000000})
{
int32 MatchCount = 0;
const TArray<int32> Source = MakeRemovalIntegers(
ElementCount,
TEXT("every_64"),
IntegerToRemove,
MatchCount);
ComparisonResults.Add(MeasureComparison(
TEXT("int32"),
TEXT("every_64"),
Source,
MatchCount,
SampleCount,
[&](TArray<int32>& Values)
{
return UKismetArrayLibrary::GenericArray_RemoveItem(&Values, ArrayProperty, &IntegerToRemove);
},
[&](TArray<int32>& Values)
{
return UDirectiveUtilArrayFunctionLibrary::GenericArray_RemoveAllOccurrences(
&Values,
ArrayProperty,
&IntegerToRemove);
}));
}
const FString StringToRemove = TEXT("REMOVE");
for (const int32 ElementCount : {256, 1024, 4096})
{
for (const FString& Pattern : RemovalPatterns)
{
int32 MatchCount = 0;
const TArray<FString> Source = MakeRemovalStrings(
ElementCount,
Pattern,
StringToRemove,
MatchCount);
ComparisonResults.Add(MeasureComparison(
TEXT("FString"),
Pattern,
Source,
MatchCount,
SampleCount,
[&](TArray<FString>& Values)
{
return UKismetArrayLibrary::GenericArray_RemoveItem(&Values, StringArrayProperty, &StringToRemove);
},
[&](TArray<FString>& Values)
{
return UDirectiveUtilArrayFunctionLibrary::GenericArray_RemoveAllOccurrences(
&Values,
StringArrayProperty,
&StringToRemove);
}));
}
}
for (const int32 ElementCount : {16, 256, 1024, 4096, 16384, 65536})
{
const TArray<int32> Source = MakeSequentialIntegers(ElementCount);
const int32 DenseDistinctCount = FMath::Max(1, FMath::Min(64, ElementCount / 4));
const TArray<int32> DenseSource = MakeRepeatingIntegers(ElementCount, DenseDistinctCount);
TArray<int32> Output;
int32 MostCommonItem = INDEX_NONE;
int32 MostCommonCount = 0;
Results.Add(Measure(
TEXT("RemoveDuplicatesUnique"), ElementCount, 0, SampleCount,
[&]() { TestObject->TestArray = Source; },
[&]() { UDirectiveUtilArrayFunctionLibrary::GenericArray_RemoveDuplicates(&TestObject->TestArray, ArrayProperty); }));
Results.Add(Measure(
TEXT("GetDistinctUnique"), ElementCount, 0, SampleCount,
[&]() { TestObject->TestArray = Source; Output.Reset(); },
[&]() { UDirectiveUtilArrayFunctionLibrary::GenericArray_GetDistinct(&TestObject->TestArray, ArrayProperty, &Output, ArrayProperty); }));
Results.Add(Measure(
TEXT("GetMostCommonUnique"), ElementCount, 0, SampleCount,
[&]() { TestObject->TestArray = Source; MostCommonItem = INDEX_NONE; MostCommonCount = 0; },
[&]() { UDirectiveUtilArrayFunctionLibrary::GenericArray_GetMostCommon(&TestObject->TestArray, ArrayProperty, &MostCommonItem, &MostCommonCount); }));
Results.Add(Measure(
TEXT("RemoveDuplicatesDense"), ElementCount, DenseDistinctCount, SampleCount,
[&]() { TestObject->TestArray = DenseSource; },
[&]() { UDirectiveUtilArrayFunctionLibrary::GenericArray_RemoveDuplicates(&TestObject->TestArray, ArrayProperty); }));
Results.Add(Measure(
TEXT("GetDistinctDense"), ElementCount, DenseDistinctCount, SampleCount,
[&]() { TestObject->TestArray = DenseSource; Output.Reset(); },
[&]() { UDirectiveUtilArrayFunctionLibrary::GenericArray_GetDistinct(&TestObject->TestArray, ArrayProperty, &Output, ArrayProperty); }));
Results.Add(Measure(
TEXT("GetMostCommonDense"), ElementCount, DenseDistinctCount, SampleCount,
[&]() { TestObject->TestArray = DenseSource; MostCommonItem = INDEX_NONE; MostCommonCount = 0; },
[&]() { UDirectiveUtilArrayFunctionLibrary::GenericArray_GetMostCommon(&TestObject->TestArray, ArrayProperty, &MostCommonItem, &MostCommonCount); }));
}
for (const int32 ElementCount : {256, 1024, 4096, 16384})
{
const TArray<FDirectiveUtilPodValue> Source = MakeSequentialPodValues(ElementCount);
TArray<FDirectiveUtilPodValue> Output;
FDirectiveUtilPodValue MostCommonItem{};
int32 MostCommonCount = 0;
Results.Add(Measure(
TEXT("GetDistinctUnhashableStruct"), ElementCount, 0, SampleCount,
[&]() { TestObject->TestPodArray = Source; Output.Reset(); },
[&]() { UDirectiveUtilArrayFunctionLibrary::GenericArray_GetDistinct(&TestObject->TestPodArray, PodArrayProperty, &Output, PodArrayProperty); }));
Results.Add(Measure(
TEXT("GetMostCommonUnhashableStruct"), ElementCount, 0, SampleCount,
[&]() { TestObject->TestPodArray = Source; MostCommonCount = 0; },
[&]() { UDirectiveUtilArrayFunctionLibrary::GenericArray_GetMostCommon(&TestObject->TestPodArray, PodArrayProperty, &MostCommonItem, &MostCommonCount); }));
}
for (const int32 ElementCount : {100, 1000, 10000, 100000})
{
const TArray<int32> Source = MakeSequentialIntegers(ElementCount);
TArray<float> Weights;
Weights.Reserve(ElementCount);
for (int32 Index = 0; Index < ElementCount; ++Index)
{
Weights.Add(Index % 11 == 0 ? 0.0f : static_cast<float>((Index % 17) + 1));
}
TArray<int32> Output;
FRandomStream RandomStream;
Results.Add(Measure(
TEXT("SampleWithoutReplacement"), ElementCount, 16, SampleCount,
[&]() { TestObject->TestArray = Source; Output.Reset(); RandomStream.Initialize(1337); },
[&]() { UDirectiveUtilArrayFunctionLibrary::GenericArray_Sample(&TestObject->TestArray, ArrayProperty, 16, false, &RandomStream, &Output, ArrayProperty); }));
Results.Add(Measure(
TEXT("SampleWeightedWithoutReplacement"), ElementCount, 16, SampleCount,
[&]() { TestObject->TestArray = Source; Output.Reset(); RandomStream.Initialize(1337); },
[&]() { UDirectiveUtilArrayFunctionLibrary::GenericArray_SampleWeighted(&TestObject->TestArray, ArrayProperty, Weights, 16, false, &RandomStream, &Output, ArrayProperty); }));
Results.Add(Measure(
TEXT("SampleWeightedWithReplacement"), ElementCount, 256, SampleCount,
[&]() { TestObject->TestArray = Source; Output.Reset(); RandomStream.Initialize(1337); },
[&]() { UDirectiveUtilArrayFunctionLibrary::GenericArray_SampleWeighted(&TestObject->TestArray, ArrayProperty, Weights, 256, true, &RandomStream, &Output, ArrayProperty); }));
}
{
constexpr int32 ElementCount = 100000;
const TArray<int32> Source = MakeSequentialIntegers(ElementCount);
TArray<float> Weights;
Weights.Init(1.0f, ElementCount);
TArray<int32> Output;
FRandomStream RandomStream;
int32 PageCount = 0;
for (const int32 RequestedCount : {1, 16, 24999, 25000, 25001, 50000, 75000, 100000})
{
const TCHAR* BenchmarkName = RequestedCount == 75000
? TEXT("SampleWithoutReplacementDense")
: TEXT("SampleWithoutReplacementRatio");
Results.Add(Measure(
BenchmarkName, ElementCount, RequestedCount, SampleCount,
[&]() { TestObject->TestArray = Source; Output.Reset(); RandomStream.Initialize(1337); },
[&]() { UDirectiveUtilArrayFunctionLibrary::GenericArray_Sample(&TestObject->TestArray, ArrayProperty, RequestedCount, false, &RandomStream, &Output, ArrayProperty); }));
}
Results.Add(Measure(
TEXT("SampleWithoutReplacementAlias"), ElementCount, 16, SampleCount,
[&]() { TestObject->TestArray = Source; RandomStream.Initialize(1337); },
[&]() { UDirectiveUtilArrayFunctionLibrary::GenericArray_Sample(&TestObject->TestArray, ArrayProperty, 16, false, &RandomStream, &TestObject->TestArray, ArrayProperty); }));
Results.Add(Measure(
TEXT("SampleWithReplacement"), ElementCount, 256, SampleCount,
[&]() { TestObject->TestArray = Source; Output.Reset(); RandomStream.Initialize(1337); },
[&]() { UDirectiveUtilArrayFunctionLibrary::GenericArray_Sample(&TestObject->TestArray, ArrayProperty, 256, true, &RandomStream, &Output, ArrayProperty); }));
for (const int32 RequestedCount : {1, 16, 1000, 50000})
{
Results.Add(Measure(
TEXT("SampleWeightedRatio"), ElementCount, RequestedCount, SampleCount,
[&]() { TestObject->TestArray = Source; Output.Reset(); RandomStream.Initialize(1337); },
[&]() { UDirectiveUtilArrayFunctionLibrary::GenericArray_SampleWeighted(&TestObject->TestArray, ArrayProperty, Weights, RequestedCount, false, &RandomStream, &Output, ArrayProperty); }));
}
for (const int32 PageSize : {1, 128, 4096})
{
const int32 PageIndex = PageSize == 128 ? 400 : (ElementCount / PageSize) / 2;
Results.Add(Measure(
TEXT("GetPage"), ElementCount, PageSize, SampleCount,
[&]() { TestObject->TestArray = Source; Output.Reset(); PageCount = 0; },
[&]() { UDirectiveUtilArrayFunctionLibrary::GenericArray_GetPage(&TestObject->TestArray, ArrayProperty, PageIndex, PageSize, &Output, ArrayProperty, &PageCount); }));
}
Results.Add(Measure(
TEXT("GetPageAlias"), ElementCount, 128, SampleCount,
[&]() { TestObject->TestArray = Source; PageCount = 0; },
[&]() { UDirectiveUtilArrayFunctionLibrary::GenericArray_GetPage(&TestObject->TestArray, ArrayProperty, 400, 128, &TestObject->TestArray, ArrayProperty, &PageCount); }));
Results.Add(Measure(
TEXT("SliceAliasCorrectness"), ElementCount, 128, SampleCount,
[&]() { TestObject->TestArray = Source; },
[&]() { UDirectiveUtilArrayFunctionLibrary::GenericArray_Slice(&TestObject->TestArray, ArrayProperty, 51200, 128, &TestObject->TestArray, ArrayProperty); }));
int32 ItemToCount = ElementCount - 1;
int32 OccurrenceCount = 0;
Results.Add(Measure(
TEXT("CountOccurrences"), ElementCount, 0, SampleCount,
[&]() { TestObject->TestArray = Source; OccurrenceCount = 0; },
[&]() { OccurrenceCount = UDirectiveUtilArrayFunctionLibrary::GenericArray_CountOccurrences(&TestObject->TestArray, ArrayProperty, &ItemToCount); }));
}
{
constexpr int32 ElementCount = 1000000;
const TArray<int32> Source = MakeSequentialIntegers(ElementCount);
int32 ItemToCount = ElementCount - 1;
int32 OccurrenceCount = 0;
Results.Add(Measure(
TEXT("CountOccurrences"), ElementCount, 0, SampleCount,
[&]() { TestObject->TestArray = Source; OccurrenceCount = 0; },
[&]() { OccurrenceCount = UDirectiveUtilArrayFunctionLibrary::GenericArray_CountOccurrences(&TestObject->TestArray, ArrayProperty, &ItemToCount); }));
}
for (const int32 ElementCount : {1000, 100000, 1000000})
{
const TArray<int32> Source = MakeSequentialIntegers(ElementCount);
for (const int32 Shift : {1, ElementCount / 3, ElementCount - 1})
{
Results.Add(Measure(
TEXT("Rotate"), ElementCount, Shift, SampleCount,
[&]() { TestObject->TestArray = Source; },
[&]() { UDirectiveUtilArrayFunctionLibrary::GenericArray_Rotate(&TestObject->TestArray, ArrayProperty, Shift); }));
}
}
for (const int32 ElementCount : {100, 1000, 10000})
{
const TArray<FString> SourceStrings = MakeNaturalSortStrings(ElementCount);
const TArray<FName> SourceNames = MakeNaturalSortNames(ElementCount);
TArray<FString> WorkingStrings;
TArray<FName> WorkingNames;
Results.Add(Measure(
TEXT("NaturalSortString"), ElementCount, 0, SampleCount,
[&]() { WorkingStrings = SourceStrings; },
[&]() { UDirectiveUtilArrayFunctionLibrary::NaturalSortStringArray(WorkingStrings); }));
Results.Add(Measure(
TEXT("NaturalSortName"), ElementCount, 0, SampleCount,
[&]() { WorkingNames = SourceNames; },
[&]() { UDirectiveUtilArrayFunctionLibrary::NaturalSortNameArray(WorkingNames); }));
}
for (const int32 ElementCount : {101, 1001, 100001})
{
const TArray<int32> ShuffledIndices = MakeShuffledIndices(ElementCount);
TArray<float> ShuffledFloats;
ShuffledFloats.Reserve(ElementCount);
for (const int32 Value : ShuffledIndices)
{
ShuffledFloats.Add(static_cast<float>(Value) + 0.25f);
}
float MedianResult = 0.0f;
float PercentileResult = 0.0f;
Results.Add(Measure(
TEXT("IntMedian"), ElementCount, 0, SampleCount,
[]() {},
[&]() { MedianResult = UDirectiveUtilMathFunctionLibrary::GetIntArrayMedian(ShuffledIndices); }));
Results.Add(Measure(
TEXT("FloatMedian"), ElementCount, 0, SampleCount,
[]() {},
[&]() { MedianResult = UDirectiveUtilMathFunctionLibrary::GetFloatArrayMedian(ShuffledFloats); }));
Results.Add(Measure(
TEXT("FloatPercentile"), ElementCount, 40, SampleCount,
[]() {},
[&]() { UDirectiveUtilMathFunctionLibrary::GetFloatArrayPercentile(ShuffledFloats, 40.0f, PercentileResult); }));
if (ElementCount == 100001)
{
const TArray<int32> SortedValues = MakeSequentialIntegers(ElementCount);
TArray<int32> ReverseValues = SortedValues;
Algo::Reverse(ReverseValues);
Results.Add(Measure(
TEXT("IntMedianSorted"), ElementCount, 0, SampleCount,
[]() {},
[&]() { MedianResult = UDirectiveUtilMathFunctionLibrary::GetIntArrayMedian(SortedValues); }));
Results.Add(Measure(
TEXT("IntMedianReverse"), ElementCount, 0, SampleCount,
[]() {},
[&]() { MedianResult = UDirectiveUtilMathFunctionLibrary::GetIntArrayMedian(ReverseValues); }));
}
}
for (const int32 ElementCount : {1000, 10000, 100000})
{
TArray<FVector> PointOutput;
TArray<FTransform> TransformOutput;
TArray<FIntPoint> CoordinateOutput;
TArray<FVector> EaseFromLocations;
TArray<FVector> EaseToLocations;
EaseFromLocations.Reserve(ElementCount);
EaseToLocations.Reserve(ElementCount);
for (int32 Index = 0; Index < ElementCount; ++Index)
{
const FVector Location(Index, Index * 0.5, -Index);
EaseFromLocations.Add(Location);
EaseToLocations.Add(Location + FVector(100.0, -50.0, 25.0));
}
Results.Add(Measure(
TEXT("GenerateGridPoints2D"), ElementCount, 0, SampleCount,
[&]() { PointOutput.Reset(); },
[&]() { PointOutput = UDirectiveUtilMathFunctionLibrary::GenerateGridPoints2D(
FVector::ZeroVector, FRotator::ZeroRotator, FIntPoint(ElementCount, 1), FVector2D(100.0, 100.0)); }));
Results.Add(Measure(
TEXT("GetRectangularHexGridCoordinates"), ElementCount, 0, SampleCount,
[&]() { CoordinateOutput.Reset(); },
[&]() { CoordinateOutput = UDirectiveUtilMathFunctionLibrary::GetRectangularHexGridCoordinates(
FIntPoint(ElementCount, 1)); }));
Results.Add(Measure(
TEXT("GeneratePointsOnCircle"), ElementCount, 0, SampleCount,
[&]() { PointOutput.Reset(); },
[&]() { PointOutput = UDirectiveUtilMathFunctionLibrary::GeneratePointsOnCircle(
FVector::ZeroVector, FRotator::ZeroRotator, 1000.0, ElementCount); }));
Results.Add(Measure(
TEXT("GeneratePointsOnSphere"), ElementCount, 0, SampleCount,
[&]() { PointOutput.Reset(); },
[&]() { PointOutput = UDirectiveUtilMathFunctionLibrary::GeneratePointsOnSphere(
FVector::ZeroVector, FRotator::ZeroRotator, 1000.0, ElementCount); }));
Results.Add(Measure(
TEXT("GenerateTransformsOnCircle"), ElementCount, 0, SampleCount,
[&]() { TransformOutput.Reset(); },
[&]() { TransformOutput = UDirectiveUtilMathFunctionLibrary::GenerateTransformsOnCircle(
FVector::ZeroVector, FRotator::ZeroRotator, 1000.0, ElementCount); }));
Results.Add(Measure(
TEXT("EaseLocationArrays"), ElementCount, 0, SampleCount,
[&]() { PointOutput.Reset(); },
[&]() { PointOutput = UDirectiveUtilMathFunctionLibrary::EaseLocationArrays(
EaseFromLocations, EaseToLocations, 0.5f, EDirectiveUtilEaseType::BackInOut, {}); }));
}
{
constexpr int32 ElementCount = 1000000;
TArray<FVector> PointOutput;
Results.Add(Measure(
TEXT("GenerateGridPoints2D"), ElementCount, 0, SampleCount,
[&]() { PointOutput.Reset(); },
[&]() { PointOutput = UDirectiveUtilMathFunctionLibrary::GenerateGridPoints2D(
FVector::ZeroVector, FRotator::ZeroRotator, FIntPoint(ElementCount, 1), FVector2D(100.0, 100.0)); }));
}
for (const int32 Radius : {10, 100, 250})
{
TArray<FIntPoint> HexOutput;
const int32 ElementCount = 1 + 3 * Radius * (Radius + 1);
Results.Add(Measure(
TEXT("GetHexesInRange"), ElementCount, Radius, SampleCount,
[&]() { HexOutput.Reset(); },
[&]() { HexOutput = UDirectiveUtilMathFunctionLibrary::GetHexesInRange(
FIntPoint::ZeroValue, Radius); }));
}
for (const int32 OperationCount : {1000, 100000})
{
double FalloffSum = 0.0;
Results.Add(Measure(
TEXT("RangeFalloffDefault"), OperationCount, 1, SampleCount,
[&]() { FalloffSum = 0.0; },
[&]()
{
for (int32 Index = 0; Index < OperationCount; ++Index)
{
FalloffSum += UDirectiveUtilMathFunctionLibrary::RangeFalloff(
static_cast<float>(Index % 1000), 100.0f, 900.0f);
}
}));
FRandomStream SphereStream;
FVector SphereSum = FVector::ZeroVector;
Results.Add(Measure(
TEXT("RandomPointInSphereStream"), OperationCount, 0, SampleCount,
[&]() { SphereStream.Initialize(1337); SphereSum = FVector::ZeroVector; },
[&]()
{
for (int32 Index = 0; Index < OperationCount; ++Index)
{
SphereSum += UDirectiveUtilMathFunctionLibrary::RandomPointInSphereFromStream(
SphereStream, 100.0f);
}
}));
}
for (const int32 CandidateCount : {10, 1000, 10000})
{
const TArray<FString> Candidates = MakeStringMatchCandidates(CandidateCount);
const FString Input = FString::Printf(TEXT("DirectiveUtilityCandidate%05dX"), CandidateCount - 1);
float Similarity = 0.0f;
int32 MatchIndex = INDEX_NONE;
Results.Add(Measure(
TEXT("FindBestStringMatch"), CandidateCount, Input.Len(), SampleCount,
[&]() { Similarity = 0.0f; MatchIndex = INDEX_NONE; },
[&]() { MatchIndex = UDirectiveUtilStringFunctionLibrary::FindBestStringMatch(Input, Candidates, Similarity); }));
}
for (const FComparisonResult& Result : ComparisonResults)
{
if (!Result.bOutputsMatch)
{
AddError(FString::Printf(
TEXT("RemoveAll comparison mismatch for %s elements=%d pattern=%s"),
*Result.ElementType,
Result.ElementCount,
*Result.Pattern));
}
const double Speedup = Result.AfterMedianMilliseconds > 0.0
? Result.BeforeMedianMilliseconds / Result.AfterMedianMilliseconds
: 0.0;
const double TimeReductionPercent = Result.BeforeMedianMilliseconds > 0.0
? ((Result.BeforeMedianMilliseconds - Result.AfterMedianMilliseconds) / Result.BeforeMedianMilliseconds) * 100.0
: 0.0;
AddInfo(FString::Printf(
TEXT("REMOVE_ALL_PERF type=%s elements=%d pattern=%s matches=%d before=%.6fms after=%.6fms speedup=%.3fx reduction=%.2f%%"),
*Result.ElementType,
Result.ElementCount,
*Result.Pattern,
Result.MatchCount,
Result.BeforeMedianMilliseconds,
Result.AfterMedianMilliseconds,
Speedup,
TimeReductionPercent));
}
for (const FAppendComparisonResult& Result : AppendComparisonResults)
{
if (!Result.bOutputsMatch)
{
AddError(FString::Printf(
TEXT("Append comparison mismatch for %s source=%d scenario=%s"),
*Result.ElementType,
Result.SourceCount,
*Result.Scenario));
}
const double Speedup = Result.AfterMedianMilliseconds > 0.0
? Result.BeforeMedianMilliseconds / Result.AfterMedianMilliseconds
: 0.0;
const double TimeReductionPercent = Result.BeforeMedianMilliseconds > 0.0
? ((Result.BeforeMedianMilliseconds - Result.AfterMedianMilliseconds) / Result.BeforeMedianMilliseconds) * 100.0
: 0.0;
AddInfo(FString::Printf(
TEXT("APPEND_PERF type=%s source=%d initial_target=%d scenario=%s before=%.6fms after=%.6fms speedup=%.3fx reduction=%.2f%%"),
*Result.ElementType,
Result.SourceCount,
Result.InitialTargetCount,
*Result.Scenario,
Result.BeforeMedianMilliseconds,
Result.AfterMedianMilliseconds,
Speedup,
TimeReductionPercent));
}
for (const FAppendComparisonResult& Result : InsertComparisonResults)
{
if (!Result.bOutputsMatch)
{
AddError(FString::Printf(
TEXT("Insert comparison mismatch for %s source=%d scenario=%s"),
*Result.ElementType,
Result.SourceCount,
*Result.Scenario));
}
const double Speedup = Result.AfterMedianMilliseconds > 0.0
? Result.BeforeMedianMilliseconds / Result.AfterMedianMilliseconds
: 0.0;
AddInfo(FString::Printf(
TEXT("INSERT_PERF type=%s source=%d initial_target=%d scenario=%s before=%.6fms after=%.6fms speedup=%.3fx"),
*Result.ElementType,
Result.SourceCount,
Result.InitialTargetCount,
*Result.Scenario,
Result.BeforeMedianMilliseconds,
Result.AfterMedianMilliseconds,
Speedup));
}
for (const FAppendComparisonResult& Result : RemoveIndicesComparisonResults)
{
if (!Result.bOutputsMatch)
{
AddError(FString::Printf(
TEXT("Remove At Indices comparison mismatch for %s indices=%d scenario=%s"),
*Result.ElementType,
Result.SourceCount,
*Result.Scenario));
}
const double Speedup = Result.AfterMedianMilliseconds > 0.0
? Result.BeforeMedianMilliseconds / Result.AfterMedianMilliseconds
: 0.0;
AddInfo(FString::Printf(
TEXT("REMOVE_INDICES_PERF type=%s indices=%d initial_target=%d scenario=%s before=%.6fms after=%.6fms speedup=%.3fx"),
*Result.ElementType,
Result.SourceCount,
Result.InitialTargetCount,
*Result.Scenario,
Result.BeforeMedianMilliseconds,
Result.AfterMedianMilliseconds,
Speedup));
}
TMap<FString, double> BaselineMedians;
FString BaselinePath;
if (!LoadBaseline(BaselineMedians, BaselinePath))
{
AddError(FString::Printf(TEXT("Unable to read performance baseline: %s"), *BaselinePath));
return false;
}
if (!BaselinePath.IsEmpty())
{
double MaximumRegressionPercent = 20.0;
double MinimumGatedMilliseconds = 0.5;
FParse::Value(
FCommandLine::Get(),
TEXT("DirectiveUtilitiesPerfMaxRegressionPercent="),
MaximumRegressionPercent);
FParse::Value(
FCommandLine::Get(),
TEXT("DirectiveUtilitiesPerfMinGateMilliseconds="),
MinimumGatedMilliseconds);
if (!FMath::IsFinite(MaximumRegressionPercent) || MaximumRegressionPercent < 0.0)
{
AddError(TEXT("DirectiveUtilitiesPerfMaxRegressionPercent must be finite and non-negative."));
return false;
}
if (!FMath::IsFinite(MinimumGatedMilliseconds) || MinimumGatedMilliseconds < 0.0)
{
AddError(TEXT("DirectiveUtilitiesPerfMinGateMilliseconds must be finite and non-negative."));
return false;
}
for (const FResult& Result : Results)
{
const double* Baseline = BaselineMedians.Find(Result.GetKey());
if (!Baseline || *Baseline < MinimumGatedMilliseconds)
{
continue;
}
const double RegressionPercent = ((Result.MedianMilliseconds - *Baseline) / *Baseline) * 100.0;
if (RegressionPercent > MaximumRegressionPercent)
{
AddError(FString::Printf(
TEXT("Performance regression for %s elements=%d parameter=%d: %.2f%% exceeds %.2f%%"),
*Result.Name,
Result.ElementCount,
Result.Parameter,
RegressionPercent,
MaximumRegressionPercent));
}
}
}
for (const FResult& Result : Results)
{
FString Comparison;
if (const double* Baseline = BaselineMedians.Find(Result.GetKey()))
{
const double Speedup = Result.MedianMilliseconds > 0.0 ? *Baseline / Result.MedianMilliseconds : 0.0;
const double ChangePercent = *Baseline > 0.0
? ((*Baseline - Result.MedianMilliseconds) / *Baseline) * 100.0
: 0.0;
Comparison = FString::Printf(TEXT(" baseline=%.6fms speedup=%.3fx change=%.2f%%"), *Baseline, Speedup, ChangePercent);
}
AddInfo(FString::Printf(
TEXT("RUNTIME_PERF %s elements=%d parameter=%d median=%.6fms min=%.6fms max=%.6fms samples=%d%s"),
*Result.Name,
Result.ElementCount,
Result.Parameter,
Result.MedianMilliseconds,
Result.MinimumMilliseconds,
Result.MaximumMilliseconds,
Result.SampleCount,
*Comparison));
}
const FString OutputPath = GetOutputPath();
IFileManager::Get().MakeDirectory(*FPaths::GetPath(OutputPath), true);
const FString Csv = BuildCsv(Results, BaselineMedians, BaselinePath);
TestTrue(
FString::Printf(TEXT("Performance results saved to %s"), *OutputPath),
FFileHelper::SaveStringToFile(Csv, *OutputPath));
const FString ComparisonOutputPath = GetComparisonOutputPath();
IFileManager::Get().MakeDirectory(*FPaths::GetPath(ComparisonOutputPath), true);
const FString ComparisonCsv = BuildComparisonCsv(ComparisonResults);
TestTrue(
FString::Printf(TEXT("Remove All comparison results saved to %s"), *ComparisonOutputPath),
FFileHelper::SaveStringToFile(ComparisonCsv, *ComparisonOutputPath));
const FString AppendComparisonOutputPath = GetAppendComparisonOutputPath();
IFileManager::Get().MakeDirectory(*FPaths::GetPath(AppendComparisonOutputPath), true);
const FString AppendComparisonCsv = BuildAppendComparisonCsv(AppendComparisonResults);
TestTrue(
FString::Printf(TEXT("Append comparison results saved to %s"), *AppendComparisonOutputPath),
FFileHelper::SaveStringToFile(AppendComparisonCsv, *AppendComparisonOutputPath));
const FString InsertComparisonOutputPath = GetInsertComparisonOutputPath();
IFileManager::Get().MakeDirectory(*FPaths::GetPath(InsertComparisonOutputPath), true);
TestTrue(
FString::Printf(TEXT("Insert comparison results saved to %s"), *InsertComparisonOutputPath),
FFileHelper::SaveStringToFile(
BuildAppendComparisonCsv(InsertComparisonResults),
*InsertComparisonOutputPath));
const FString RemoveIndicesComparisonOutputPath = GetRemoveIndicesComparisonOutputPath();
IFileManager::Get().MakeDirectory(*FPaths::GetPath(RemoveIndicesComparisonOutputPath), true);
TestTrue(
FString::Printf(TEXT("Remove At Indices comparison results saved to %s"), *RemoveIndicesComparisonOutputPath),
FFileHelper::SaveStringToFile(
BuildAppendComparisonCsv(RemoveIndicesComparisonResults),
*RemoveIndicesComparisonOutputPath));
return !HasAnyErrors();
}