Files
2025-05-18 13:04:45 +08:00

1487 lines
52 KiB
C#

// Copyright Epic Games, Inc. All Rights Reserved.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using AutomationTool;
using Gauntlet;
using EpicGames.Core;
using Log = Gauntlet.Log;
using UnrealBuildBase; // for Unreal.RootDirectory
using UnrealBuildTool; // for UnrealTargetPlatform
using static AutomationTool.CommandUtils;
using UE;
using System.Configuration;
using System.Diagnostics;
using System.Reflection;
using System.Text;
namespace AutomatedPerfTest
{
public interface IAutomatedPerfTest
{
List<string> GetTestsFromConfig();
}
/// <summary>
/// Implementation of a Gauntlet TestNode for AutomatedPerfTest plugin
/// </summary>
/// <typeparam name="TConfigClass"></typeparam>
public abstract class AutomatedPerfTestNode<TConfigClass> : UnrealTestNode<TConfigClass>
where TConfigClass : AutomatedPerfTestConfigBase, new()
{
public string SummaryTable = "default";
public AutomatedPerfTestNode(UnrealTestContext InContext) : base(InContext)
{
// We need to save off the build name as if this is a preflight that suffix will be stripped
// after GetConfiguration is called. This will cause a mismatch in CreateReport.
OriginalBuildName = Globals.Params.ParseValue("BuildName", InContext.BuildInfo.BuildName);
Log.Info("Setting OriginalBuildName to {OriginalBuildName}", OriginalBuildName);
TestGuid = Guid.NewGuid();
Log.Info("Your Test GUID is :\n" + TestGuid.ToString() + '\n');
InitHandledErrors();
LogParser = null;
}
public override bool StartTest(int Pass, int InNumPasses)
{
LogParser = null;
return base.StartTest(Pass, InNumPasses);
}
public class HandledError
{
public string ClientErrorString;
public string GauntletErrorString;
/// <summary>
/// String name for the log category that should be used to filter errors. Defaults to null, i.e. no filter.
/// </summary>
public string CategoryName;
// If error is verbose, will output debugging information such as state
public bool Verbose;
public HandledError(string ClientError, string GauntletError, string Category, bool VerboseIn = false)
{
ClientErrorString = ClientError;
GauntletErrorString = GauntletError;
CategoryName = Category;
Verbose = VerboseIn;
}
}
/// <summary>
/// List of errors with special-cased gauntlet messages.
/// </summary>
public List<HandledError> HandledErrors { get; set; }
/// <summary>
/// Guid associated with each test run for ease of differentiation between different runs on same build.
/// </summary>
public Guid TestGuid { get; protected set; }
/// <summary>
/// Base artifact output path for current instance of this test node.
/// </summary>
public string BaseOutputPath { get; protected set; }
/// <summary>
/// Returns the APT Performance Artifact Path.
/// </summary>
/// <param name="Platform"></param>
/// <returns>{ProjectName}/Saved/Performance/{SubTest}/{Platform}</returns>
public string GetPerformanceReportArtifactOutputPath(UnrealTargetPlatform Platform)
{
if(BaseOutputPath == null)
{
return TempPerfCSVDir.FullName;
}
return Path.Combine(BaseOutputPath, GetSubtestName(GetCachedConfiguration()), Platform.ToString());
}
/// <summary>
/// Track client log messages that have been written to the test logs.
/// </summary>
private UnrealLogStreamParser LogParser;
/// <summary>
// Temporary directory for perf report CSVs
/// </summary>
private DirectoryInfo TempPerfCSVDir => new DirectoryInfo(Path.Combine(Unreal.RootDirectory.FullName, "GauntletTemp", "PerfReportCSVs"));
/// <summary>
// Holds the build name as is, since if this is a preflight the suffix will be stripped after GetConfiguration is called.
/// </summary>
private string OriginalBuildName = null;
/// <summary>
/// If true, local reports will be generated at the end of the perf test.
/// </summary>
private bool GenerateLocalReport = false;
/// <summary>
/// If true, the resulting CSV files will be imported via Perf Report Server
/// importer.
/// </summary>
private bool GeneratePRSReport = false;
/// <summary>
/// Set up the base list of possible expected errors, plus the messages to deliver if encountered.
/// </summary>
protected virtual void InitHandledErrors()
{
HandledErrors = new List<HandledError>();
}
protected virtual string GetNormalizedInsightsFileName(string FileName) => FileName.Replace(".csv", ".utrace");
protected string GetNormalizedInsightsFileName(string CSVFileName, string TestTypeLiteral)
{
int Index = CSVFileName.IndexOf(TestTypeLiteral);
if (Index < 0)
{
Index = CSVFileName.ToLower().IndexOf(".csv");
}
else
{
Index = Index + TestTypeLiteral.Length;
}
return CSVFileName.Substring(0, Index) + ".utrace";
}
/// <summary>
/// Periodically called while test is running. Updates logs.
/// </summary>
public override void TickTest()
{
IAppInstance App = null;
if (TestInstance.ClientApps == null)
{
App = TestInstance.ServerApp;
}
else
{
if (TestInstance.ClientApps.Length > 0)
{
App = TestInstance.ClientApps.First();
}
}
if (App != null)
{
if (LogParser == null)
{
LogParser = new UnrealLogStreamParser(App.GetLogBufferReader());
}
LogParser.ReadStream();
string LogChannelName = Context.BuildInfo.ProjectName + "Test";
List<string> TestLines = LogParser.GetLogFromChannel(LogChannelName, false).ToList();
string LogCategory = "Log" + LogChannelName;
string LogCategoryError = LogCategory + ": Error:";
string LogCategoryWarning = LogCategory + ": Warning:";
foreach (string Line in TestLines)
{
if (Line.StartsWith(LogCategoryError))
{
ReportError(Line);
}
else if (Line.StartsWith(LogCategoryWarning))
{
ReportWarning(Line);
}
else
{
Log.Info(Line);
}
}
}
base.TickTest();
}
/// <summary>
/// This allows using a per-branch config to ignore certain issues
/// that were inherited from Main and will be addressed there
/// </summary>
/// <param name="InArtifacts"></param>
/// <returns></returns>
protected override UnrealLog CreateLogSummaryFromArtifact(UnrealRoleArtifacts InArtifacts)
{
UnrealLog LogSummary = base.CreateLogSummaryFromArtifact(InArtifacts);
IgnoredIssueConfig IgnoredIssues = new IgnoredIssueConfig();
string IgnoredIssuePath = GetCachedConfiguration().IgnoredIssuesConfigAbsPath;
if (!File.Exists(IgnoredIssuePath))
{
Log.Info("No IgnoredIssue Config found at {0}", IgnoredIssuePath);
}
else if (IgnoredIssues.LoadFromFile(IgnoredIssuePath))
{
Log.Info("Loaded IgnoredIssue config from {0}", IgnoredIssuePath);
IEnumerable<UnrealLog.CallstackMessage> IgnoredEnsures = LogSummary.Ensures.Where(E => IgnoredIssues.IsEnsureIgnored(this.Name, E.Message));
IEnumerable<UnrealLog.LogEntry> IgnoredWarnings = LogSummary.LogEntries.Where(E => E.Level == UnrealLog.LogLevel.Warning && IgnoredIssues.IsWarningIgnored(this.Name, E.Message));
IEnumerable<UnrealLog.LogEntry> IgnoredErrors = LogSummary.LogEntries.Where(E => E.Level == UnrealLog.LogLevel.Error && IgnoredIssues.IsErrorIgnored(this.Name, E.Message));
if (IgnoredEnsures.Any())
{
Log.Info("Ignoring {0} ensures.", IgnoredEnsures.Count());
Log.Info("\t{0}", string.Join("\n\t", IgnoredEnsures.Select(E => E.Message)));
LogSummary.Ensures = LogSummary.Ensures.Except(IgnoredEnsures).ToArray();
}
if (IgnoredWarnings.Any())
{
Log.Info("Ignoring {0} warnings.", IgnoredWarnings.Count());
Log.Info("\t{0}", string.Join("\n\t", IgnoredWarnings.Select(E => E.Message)));
LogSummary.LogEntries = LogSummary.LogEntries.Except(IgnoredWarnings).ToArray();
}
if (IgnoredErrors.Any())
{
Log.Info("Ignoring {0} errors.", IgnoredErrors.Count());
Log.Info("\t{0}", string.Join("\n\t", IgnoredErrors.Select(E => E.Message)));
LogSummary.LogEntries = LogSummary.LogEntries.Except(IgnoredErrors).ToArray();
}
}
return LogSummary;
}
protected override UnrealProcessResult GetExitCodeAndReason(StopReason InReason, UnrealLog InLogSummary, UnrealRoleArtifacts InArtifacts, out string ExitReason, out int ExitCode)
{
// Check for login failure
UnrealLogParser Parser = new UnrealLogParser(InArtifacts.AppInstance.GetLogReader());
TConfigClass Config = GetCachedConfiguration();
ExitReason = "";
ExitCode = -1;
foreach (HandledError ErrorToCheck in HandledErrors)
{
string[] MatchingErrors = Parser.GetErrors(ErrorToCheck.CategoryName).Where(E => E.Contains(ErrorToCheck.ClientErrorString)).ToArray();
if (MatchingErrors.Length > 0)
{
ExitReason = string.Format("Test Error: {0} {1}", ErrorToCheck.GauntletErrorString, ErrorToCheck.Verbose ? "\"" + MatchingErrors[0] + "\"" : "");
ExitCode = -1;
return UnrealProcessResult.TestFailure;
}
}
// If this is a Test Target Configuration and we have configured to ignore logging,
// we can check the process exit code and return from here. This is especially useful
// when logging is disabled in Test builds, but we still want to exit the test
// successfully.
UnrealTargetConfiguration TargetConfig = InArtifacts.SessionRole.Configuration;
bool bIgnoreLoggingInTest = Config.IgnoreTestBuildLogging && TargetConfig == UnrealTargetConfiguration.Test && !InLogSummary.EngineInitialized;
bool bTestHasErrorLog = InLogSummary.FatalError != null || InLogSummary.HasTestExitCode;
// This is a major assumption that Gauntlet captures the process exit code on all
// platforms and is the only thing indicating if the test has actually passed or
// failed in the absence of logging.
int ProcessExitCode = InArtifacts.AppInstance.ExitCode;
if (InReason == StopReason.Completed && bIgnoreLoggingInTest && ProcessExitCode == 0)
{
ExitCode = 0;
ExitReason = "Test build exited successfully without logs.";
return UnrealProcessResult.ExitOk;
}
else if (InReason == StopReason.Completed && bIgnoreLoggingInTest && ProcessExitCode != 0 && !bTestHasErrorLog)
{
// Process has not exited cleanly and we do not have any error log messages.
// Fail the test here and provide context to user.
ExitCode = ProcessExitCode;
ExitReason = "Test build exited with error. Please enable logging in build for more information.";
return UnrealProcessResult.TestFailure;
}
// Let the user know that if their tests are failing but process exit code is 0
// and they have logging disabled in Test builds that they can ignore logging
// failures if they wish to do so.
if(!InLogSummary.EngineInitialized && TargetConfig == UnrealTargetConfiguration.Test && ProcessExitCode == 0)
{
Log.Warning("*** Engine Initialization log not detected in Test build with Exit Code 0. " +
"This test will fail. " +
"Try passing `-AutomatedPerfTest.IgnoreTestBuildLogging` while running this test " +
"or pass `-set:APTIgnoreTestBuildLogging=true` if running via BuildGraph if you " +
"wish to ignore log parsing checks in this test or recompile with logging enabled. ***");
}
return base.GetExitCodeAndReason(InReason, InLogSummary, InArtifacts, out ExitReason, out ExitCode);
}
public override ITestReport CreateReport(TestResult Result, UnrealTestContext Context, UnrealBuildSource Build, IEnumerable<UnrealRoleResult> Artifacts, string ArtifactPath)
{
UnrealTargetPlatform Platform = Context.GetRoleContext(UnrealTargetRole.Client).Platform;
TConfigClass Config = GetCachedConfiguration();
string OutputPath = GetPerformanceReportArtifactOutputPath(Platform);
// Always render reshape reports, failed tests may contain validation of interest
if (Config.DoGPUReshape)
{
Config.WriteTestResultsForHorde = true;
RenderGPUReshapeReport(ArtifactPath, OutputPath);
}
if (Result == TestResult.Passed)
{
if (Config.DoInsightsTrace)
{
CopyInsightsTraceToOutput(ArtifactPath, OutputPath);
}
if (GetCurrentPass() <= GetNumPasses() && Config.DoCSVProfiler)
{
// Our artifacts from each iteration such as the client log will be overwritten by subsequent iterations so we need to copy them out to another dir
// to preserve them until we're ready to make our report on the final iteration.
CopyPerfFilesToOutputDir(ArtifactPath, OutputPath);
bool bGeneratedLocalReport = false;
bool bGeneratedPRSReport = false;
// Local report generation is useful for people conducting tests locally without a centralized server (for A/B testing for instance).
// To make it work out of the box, enable by default for non-build machine runs
if (GenerateLocalReport)
{
// NOTE: This does not currently work with long paths due to the CsvTools not properly supporting them.
Log.Info("Generating local performance reports using PerfReportTool.");
bGeneratedLocalReport = GenerateLocalPerfReport(Platform, OutputPath);
}
// On build machines, default to producing PRS report
if (GeneratePRSReport)
{
Dictionary<string, dynamic> CommonDataSourceFields = new Dictionary<string, dynamic>
{
{"HordeJobUrl", Globals.Params.ParseValue("JobDetails", null)}
};
Log.Info("Creating perf server importer with build name {BuildName}", OriginalBuildName);
string DataSourceName = GetConfiguration().DataSourceName;
string ImportDirOverride = Globals.Params.ParseValue("PerfReportServerImportDir", null);
Log.Info("Creating PRS Importer for data source '{0}' and import dir override (if any) '{1}'.",
DataSourceName, ImportDirOverride);
ICsvImporter Importer = ReportGenUtils.CreatePerfReportServerImporter(DataSourceName, OriginalBuildName,
IsBuildMachine, ImportDirOverride, CommonDataSourceFields);
if (Importer != null)
{
// Recursively grab all the csv files we copied to the temp dir and convert them to binary.
List<FileInfo> AllBinaryCsvFiles = ReportGenUtils.CollectAndConvertCsvFilesToBinary(OutputPath);
if (AllBinaryCsvFiles.Count == 0)
{
throw new AutomationException($"No Csv files found in {OutputPath}");
}
// The corresponding log for each csv sits in the same subdirectory as the csv file itself.
IEnumerable<CsvImportEntry> ImportEntries = AllBinaryCsvFiles
.Select(CsvFile => new CsvImportEntry(CsvFile.FullName, Path.Combine(CsvFile.Directory.FullName, "ClientOutput.log")));
// todo update this so it associates videos with the correct CSVs
IEnumerable<CsvImportEntry> CsvImportEntries = ImportEntries as CsvImportEntry[] ?? ImportEntries.ToArray();
if (GetConfiguration().DoInsightsTrace)
{
string InsightsFilename = Path.GetFileNameWithoutExtension(CsvImportEntries.First().CsvFilename);
InsightsFilename = GetNormalizedInsightsFileName(InsightsFilename);
// recursively look for trace files that match the CSV's filename in the artifact path
string[] MatchingTraces = FindFiles($"*{InsightsFilename}", true, ArtifactPath);
if(MatchingTraces.Length > 0)
{
if (MatchingTraces.Length > 1)
{
Log.Warning("Multiple Insights traces were found in {ArtifactPath} matching pattern *{InsightsFilename}. Only the first will be attached to the CSV import for this test.",
ArtifactPath, InsightsFilename);
}
CsvImportEntries.First().AddAdditionalFile("Insights", MatchingTraces.First());
}
else
{
Log.Warning("Insights was requested, but no matching insights traces were found matching pattern *{InsightsFilename} in {ArtifactPath}",
InsightsFilename, ArtifactPath);
}
}
if (GetConfiguration().DoVideoCapture)
{
string VideoPath = Path.Combine(ArtifactPath, "Client", "Videos");
string[] VideoFiles = Directory.GetFiles(VideoPath, "*.mp4");
if (VideoFiles.Length > 0)
{
foreach (var VideoFile in VideoFiles)
{
CsvImportEntries.First().AddAdditionalFile("Video", Path.Combine(VideoPath, VideoFile));
}
}
else
{
Log.Warning("Video capture was requested, but no videos were found in path {VideoPath}", VideoPath);
}
}
// Create the import batch
Importer.Import(CsvImportEntries);
// trust blindly for now...
bGeneratedPRSReport = true;
}
else
{
Log.Warning("Unable to create PRS Importer.");
}
// Cleanup the temp dir
if(TempPerfCSVDir.Exists)
{
TempPerfCSVDir.Delete(recursive: true);
}
}
if (!bGeneratedLocalReport && !bGeneratedPRSReport)
{
Log.Warning("Did not generate neither local nor a PRS report.");
}
}
}
else
{
Log.Warning("Skipping performance report generation because the perf report test failed.");
}
ITestReport Report = base.CreateReport(Result, Context, Build, Artifacts, ArtifactPath);
if (Config.DoGPUReshape)
{
Report = ChainGPUReshapeReports(Config, Result, Report, OutputPath);
}
return Report;
}
/// <summary>
/// Get report type for current configuration
/// </summary>
/// <param name="ReportType"></param>
/// <param name="SummaryTableType"></param>
/// <param name="HistoricalReportType"></param>
private void GetReportType(out string ReportType, out string SummaryTableType, out string HistoricalReportType)
{
ReportType = "ClientPerf";
SummaryTableType = SummaryTable;
HistoricalReportType = "autoPerfReportStandard";
if (GetCachedConfiguration().DoLLM)
{
ReportType = "LLM";
SummaryTableType = "autoPerfReportLlm";
HistoricalReportType = "autoPerfReportLlm";
}
}
/// <summary>
/// Produces a detailed csv report using PerfReportTool.
/// Also, stores perf data in the perf cache, and generates a historic report using the data the cache contains.
/// </summary>
private bool GenerateLocalPerfReport(UnrealTargetPlatform Platform, string OutputPath)
{
string perfreportTool = "";
perfreportTool = Path.Combine("PerfreportTool.dll");
var ToolPath = FileReference.Combine(Unreal.EngineDirectory, "Binaries", "DotNET", "CsvTools", perfreportTool);
if (!FileReference.Exists(ToolPath))
{
Log.Error("Failed to find perf report utility at this path: \"{ToolPath}\".", ToolPath);
return false;
}
var ReportConfigDir = GetCachedConfiguration().ReportConfigDir;
if (string.IsNullOrEmpty(ReportConfigDir))
{
// default to the report types and graphs provided by APT
ReportConfigDir = Path.Combine(Unreal.EngineDirectory.ToString(), "Plugins", "Performance", "AutomatedPerfTesting", "Build", "Scripts", "PerfReport");
}
var ReportPath = GetCachedConfiguration().ReportPath;
if(string.IsNullOrEmpty(ReportPath))
{
ReportPath = Path.Combine(OutputPath, "Reports");
}
string ReportCacheDir = Path.Combine(OutputPath, "Cache");
// Csv files may have been output in one of two places.
// Check both...
var CsvsPaths = new[]
{
Path.Combine(OutputPath, "CSV")
};
var DiscoveredCsvs = new List<string>();
foreach (var CsvsPath in CsvsPaths)
{
if (Directory.Exists(CsvsPath))
{
DiscoveredCsvs.AddRange(
from CsvFile in Directory.GetFiles(CsvsPath, "*.csv", SearchOption.AllDirectories)
select CsvFile);
}
}
if (DiscoveredCsvs.Count == 0)
{
Log.Error("Test completed successfully but no csv profiling results were found. Searched paths were:\r\n {Paths}", string.Join("\r\n ", CsvsPaths.Select(s => $"\"{s}\"")));
return false;
}
// Find the newest csv file and get its directory
// (PerfReportTool will only output cached data in -csvdir mode)
var NewestFile =
(from CsvFile in DiscoveredCsvs
let Timestamp = File.GetCreationTimeUtc(CsvFile)
orderby Timestamp descending
select CsvFile).First();
var NewestDir = Path.GetDirectoryName(NewestFile);
Log.Info("Using perf report cache directory \"{ReportCacheDir}\".", ReportCacheDir);
Log.Info("Using perf report output directory \"{ReportPath}\".", ReportPath);
Log.Info("Using csv results directory \"{NewestDir}\". Generating historic perf report data...", NewestDir);
// Make sure the cache and output directories exist
if (!Directory.Exists(ReportCacheDir))
{
try { Directory.CreateDirectory(ReportCacheDir); }
catch (Exception Ex)
{
Log.Error("Failed to create perf report cache directory \"{ReportCacheDir}\". {Ex}", ReportCacheDir, Ex);
return false;
}
}
if (!Directory.Exists(ReportPath))
{
try { Directory.CreateDirectory(ReportPath); }
catch (Exception Ex)
{
Log.Error("Failed to create perf report output directory \"{ReportPath}\". {Ex}", ReportPath, Ex);
return false;
}
}
// Win64 is actually called "Windows" in csv profiles
var PlatformNameFilter = Platform == UnrealTargetPlatform.Win64 ? "Windows" : $"{Platform}";
string SearchPattern = $"{Context.BuildInfo.ProjectName}*";
string ReportType = null;
string SummaryTableType = null;
string HistoricalSummaryType = null;
GetReportType(out ReportType, out SummaryTableType, out HistoricalSummaryType);
// Produce the detailed report, and update the perf cache
var DetailedReportPath = Path.Combine(ReportPath, "Detailed");
string[] CsvGenerationArgs = new[]
{
$"{ToolPath.FullName}",
$"-csvdir \"{NewestDir}\"",
$"-reportType \"{ReportType}\"",
$"-o \"{DetailedReportPath}\"",
$"-reportxmlbasedir \"{ReportConfigDir}\"",
$"-summaryTable {SummaryTableType}",
$"-summaryTableCache \"{ReportCacheDir}\"",
$"-metadatafilter platform=\"{PlatformNameFilter}\""
};
string PerfReportToolArgs = string.Join(' ', CsvGenerationArgs);
RunAndLog(CmdEnv, CmdEnv.DotnetMsbuildPath, PerfReportToolArgs, out int ErrorCode);
if (ErrorCode != 0)
{
Log.Error("PerfReportTool returned error code \"{ErrorCode}\" while generating detailed report.", ErrorCode);
}
// Now generate the all-time historic summary report
HistoricReport("HistoricReport_AllTime", new[]
{
$"platform={PlatformNameFilter}"
});
// 14 days historic report
HistoricReport($"HistoricReport_14Days", new[]
{
$"platform={PlatformNameFilter}",
$"starttimestamp>={DateTimeOffset.Now.ToUnixTimeSeconds() - (14 * 60L * 60L * 24L)}"
});
// 7 days historic report
HistoricReport($"HistoricReport_7Days", new[]
{
$"platform={PlatformNameFilter}",
$"starttimestamp>={DateTimeOffset.Now.ToUnixTimeSeconds() - (7 * 60L * 60L * 24L)}"
});
void HistoricReport(string Name, IEnumerable<string> Filter)
{
var Args = new[]
{
$"{ToolPath.FullName}",
$"-reportType \"{ReportType}\"",
$"-summarytablecachein \"{ReportCacheDir}\"",
$"-summaryTableFilename \"{Name}.html\"",
$"-reportxmlbasedir \"{ReportConfigDir}\"",
$"-o \"{ReportPath}\"",
$"-metadatafilter \"{string.Join(" and ", Filter)}\"",
$"-summaryTable {HistoricalSummaryType}",
$"-condensedSummaryTable {HistoricalSummaryType}",
$"-reportLinkRootPath \"{DetailedReportPath}\\\"",
"-emailtable",
"-recurse"
};
var ArgStr = string.Join(" ", Args);
RunAndLog(CmdEnv, CmdEnv.DotnetMsbuildPath, ArgStr, out ErrorCode);
if (ErrorCode != 0)
{
Log.Error("PerfReportTool returned error code \"{ErrorCode}\" while generating historic report.", ErrorCode);
}
}
return true;
}
private void CopyPerfFilesToOutputDir(string FromArtifactPath, string ToOutputDir = null)
{
if(string.IsNullOrEmpty(ToOutputDir))
{
// Fallback path
ToOutputDir = TempPerfCSVDir.FullName;
}
DirectoryInfo OutputDirectory = new DirectoryInfo(ToOutputDir);
if (!OutputDirectory.Exists)
{
Log.Info("Creating temp perf csv dir: {OutputDirectory}", OutputDirectory);
OutputDirectory.Create();
}
DirectoryInfo CSVDirectory = new DirectoryInfo(Path.Combine(ToOutputDir, "CSV"));
if (!CSVDirectory.Exists)
{
Log.Info($"Creating CSV Directory: {CSVDirectory}");
CSVDirectory.Create();
}
string ClientArtifactDir = Path.Combine(FromArtifactPath, "Client");
string ClientLogPath = Path.Combine(ClientArtifactDir, "ClientOutput.log");
string CSVPath = PathUtils.FindRelevantPath(ClientArtifactDir, "Profiling", "CSV");
if (string.IsNullOrEmpty(CSVPath))
{
Log.Warning("Failed to find CSV folder folder in {ClientArtifactDir}", ClientArtifactDir);
return;
}
// Grab all the csv files that have valid metadata.
// We don't want to convert to binary in place as the legacy reports require the raw csv.
List<FileInfo> CsvFiles = ReportGenUtils.CollectValidCsvFiles(CSVPath);
if (CsvFiles.Count > 0)
{
// We only want to copy the latest file as the other will have already been copied when this was run for those iterations.
CsvFiles.SortBy(Info => Info.LastWriteTimeUtc);
FileInfo LatestCsvFile = CsvFiles.Last();
// Create a subdir for each pass as we want to store the csv and log together in the same dir to make it easier to find them later.
string PassDir = Path.Combine(ToOutputDir, "Logs");
Directory.CreateDirectory(PassDir);
FileInfo LogFileInfo = new FileInfo(ClientLogPath);
if (LogFileInfo.Exists)
{
string Guid = TestGuid.ToString();
string DestLogFile = $"CL{Context.BuildInfo.Changelist}-Pass{GetCurrentPass()}-{Guid.Substring(0, Guid.IndexOf("-"))}-{LogFileInfo.Name}";
string LogDestPath = Path.Combine(PassDir, DestLogFile);
Log.Info("Copying Log {ClientLogPath} To {LogDest}", ClientLogPath, LogDestPath);
LogFileInfo.CopyTo(LogDestPath, true);
}
else
{
Log.Warning("No log file was found at {ClientLogPath}", ClientLogPath);
}
string Extension = LatestCsvFile.Extension;
string OutputCSVFile = $"{LatestCsvFile.Name.Replace(Extension, "")}-Pass{GetCurrentPass()}{Extension}";
string CsvDestPath = Path.Combine(CSVDirectory.FullName, OutputCSVFile);
Log.Info("Copying Csv {CsvPath} To {CsvDestPath}", LatestCsvFile.FullName, CsvDestPath);
LatestCsvFile.CopyTo(CsvDestPath, true);
}
else
{
Log.Warning("No valid csv files found in {CSVPath}", CSVPath);
}
}
protected virtual string GetSubtestName(TConfigClass Config)
{
// Options like DoLLM and DoInsightsTrace are heavy enough to be in their own subtest type
// Since they are not exclusive, create a yet another subtest type if both are specified
if (Config.DoLLM && Config.DoInsightsTrace)
{
throw new AutomationException($"Running Insights trace with LLM is not a practical test due to LLM's large overhead.");
}
if (Config.DoInsightsTrace)
{
if (Config.TraceChannels != "default,screenshot,stats")
{
throw new AutomationException($"Running Insights trace with non-default channels {Config.TraceChannels}, new subtest type is needed to avoid contaminating 'Insights' subtest results.");
}
return "Insights";
}
if (Config.DoLLM)
{
return "LLM";
}
if (Config.DoGPUPerf)
{
return "GPUPerf";
}
if (Config.DoGPUReshape)
{
return "GPUReshape";
}
return "Perf";
}
public override TConfigClass GetConfiguration()
{
TConfigClass Config = base.GetConfiguration();
Config.MaxDuration = Context.TestParams.ParseValue("MaxDuration", 60 * 60); // 1 hour max
if(string.IsNullOrEmpty(Config.PerfOutputPath))
{
Config.PerfOutputPath = Path.Combine(Context.BuildInfo.ProjectPath.Directory.FullName, "Saved", "Performance");
}
BaseOutputPath = Path.Combine(Config.PerfOutputPath, GetType().Name);
UnrealTestRole ClientRole = Config.RequireRole(UnrealTargetRole.Client);
// the controller will be added by the subclasses
ClientRole.CommandLineParams.AddOrAppendParamValue("logcmds", "LogHttp Verbose, LogAutomatedPerfTest Verbose");
ClientRole.CommandLineParams.Add("-deterministic");
Log.Info("AutomatedPerfTestNode<>.GetConfiguration(): Config.DoFPSChart={0}, Config.DoCSVProfiler={1}, Config.DoVideoCapture={2}, Config.DoInsightsTrace={3}, Config.DoLLM={4}",
Config.DoFPSChart, Config.DoCSVProfiler, Config.DoVideoCapture, Config.DoInsightsTrace, Config.DoLLM);
ClientRole.CommandLineParams.AddOrAppendParamValue("AutomatedPerfTest.TestID", Config.TestID);
if (Config.DeviceProfileOverride != String.Empty)
{
ClientRole.CommandLineParams.AddOrAppendParamValue("AutomatedPerfTest.DeviceProfileOverride", Config.DeviceProfileOverride);
}
if (Config.DoInsightsTrace)
{
ClientRole.CommandLineParams.Add("AutomatedPerfTest.DoInsightsTrace");
if (Config.TraceChannels != String.Empty)
{
ClientRole.CommandLineParams.AddOrAppendParamValue("AutomatedPerfTest.TraceChannels", Config.TraceChannels);
}
}
if (Config.DoLLM)
{
ClientRole.CommandLineParams.Add("llm");
ClientRole.CommandLineParams.Add("llmcsv");
}
if (Config.DoGPUPerf)
{
// see ReplayRun.py, reducedAsyncComputeCommands
// We enable r.nanite.asyncrasterization.shadowdepths because nanite overlaps with itself, so it's possible to time accurately without distorting other timings
ClientRole.CommandLineParams.AddOrAppendParamValue("execcmds", "r.StencilLODMode 1");
ClientRole.CommandLineParams.AddOrAppendParamValue("execcmds", "r.VolumetricRenderTarget.PreferAsyncCompute 0");
ClientRole.CommandLineParams.AddOrAppendParamValue("execcmds", "r.LumenScene.Lighting.AsyncCompute 0");
ClientRole.CommandLineParams.AddOrAppendParamValue("execcmds", "r.Lumen.DiffuseIndirect.AsyncCompute 0");
ClientRole.CommandLineParams.AddOrAppendParamValue("execcmds", "r.Bloom.AsyncCompute 0");
ClientRole.CommandLineParams.AddOrAppendParamValue("execcmds", "r.nanite.asyncrasterization.shadowdepths 1");
ClientRole.CommandLineParams.AddOrAppendParamValue("execcmds", "r.TSR.AsyncCompute 0");
ClientRole.CommandLineParams.AddOrAppendParamValue("execcmds", "r.RayTracing.AsyncBuild 0");
ClientRole.CommandLineParams.AddOrAppendParamValue("execcmds", "r.DFShadowAsyncCompute 0");
ClientRole.CommandLineParams.AddOrAppendParamValue("execcmds", "r.AmbientOcclusion.Compute 1"); // 1 here means compute on the graphics pipe
ClientRole.CommandLineParams.AddOrAppendParamValue("execcmds", "r.LocalFogVolume.TileCullingUseAsync 0");
ClientRole.CommandLineParams.AddOrAppendParamValue("execcmds", "r.SkyAtmosphereASyncCompute 0");
}
if (Config.DoGPUPerf || Config.LockDynamicRes)
{
ClientRole.CommandLineParams.Add("AutomatedPerfTest.LockDynamicRes");
}
if(Config.DoFPSChart)
{
ClientRole.CommandLineParams.Add("AutomatedPerfTest.DoFPSChart");
}
if (Config.DoCSVProfiler)
{
ConfigureCSVProfiler(Config, ClientRole);
}
if (Config.DoVideoCapture)
{
ClientRole.CommandLineParams.Add("AutomatedPerfTest.DoVideoCapture");
}
return Config;
}
private void RenderGPUReshapeReport(string ArtifactPath, string OutputPath)
{
InternalUtils.SafeCreateDirectory(OutputPath, true);
// Hardcoded path, for now
var GPUReshapePath = Path.Combine(Unreal.RootDirectory.FullName, "Engine/Restricted/NotForLicensees/Binaries/ThirdParty/GPUReshape/Win64/GPUReshape.exe");
// Get all reports, search each test case
var Reports = Directory.GetFiles(ArtifactPath, "*GRS.Report.json", SearchOption.AllDirectories).ToList();
if (Reports.Count == 0)
{
Log.Error($"No GPU-Reshape reports found in '{ArtifactPath}'");
return;
}
foreach (string ReportPath in Reports)
{
// Default to HTML report, for now
string RenderName = Path.GetFileNameWithoutExtension(ReportPath);
string RenderPath = Path.Combine(OutputPath, $"{RenderName}.html");
// Setup arguemnts
StringBuilder Arguments = new();
Arguments.Append("render");
Arguments.Append($" -report=\"{ReportPath}\"");
Arguments.Append($" -out=\"{RenderPath}\"");
// Render the report
Log.Info($"Rendering GPUReshape ('{GPUReshapePath}') report with arguments `{Arguments}`");
if (Process.Start(GPUReshapePath, Arguments.ToString()) is not {} process)
{
Log.Error("Failed to start render process");
continue;
}
process.WaitForExit();
}
}
private ITestReport ChainGPUReshapeReports(TConfigClass Config, TestResult Result, ITestReport Report, string OutputPath)
{
foreach (string GRSReportPath in Directory.GetFiles(OutputPath, "*GRS.Report.html"))
{
// Create a typical report per, just keeps a link around
if (CreateSimpleReportForHorde(Result) is not HordeReport.SimpleTestReport ChildReport)
{
continue;
}
// Add to artifact
string BaseName = Path.GetFileName(GRSReportPath);
ChildReport.AttachArtifact(GRSReportPath, BaseName);
// TODO: The general artifact linking is a work in progress, pending the V2 format
ChildReport.TestName = $"GPU Reshape - {Config.GPUReshapeWorkspace}";
ChildReport.URLLink = $"/api/v2/artifacts/{Context.WorkerJobID}/file?path={BaseName}&inline=true";
// Chain to last
switch (Report)
{
case HordeReport.BaseHordeReport BaseReport:
BaseReport.AttachDependencyReport(ChildReport, ChildReport.TestName);
break;
default:
Report = ChildReport;
break;
}
}
return Report;
}
public void CopyInsightsTraceToOutput(string FromArtifactPath, string ToOutputPath)
{
Log.Info("Copying test insights trace from artifact path to report cache");
// find all the available trace paths
var DiscoveredTraces = new List<string>();
if (Directory.Exists(FromArtifactPath))
{
DiscoveredTraces.AddRange(
from TraceFile in Directory.GetFiles(FromArtifactPath, "*.utrace", SearchOption.AllDirectories)
select TraceFile);
}
// if we couldn't find any traces, report that and bail out
if (DiscoveredTraces.Count == 0)
{
Log.Error("Test completed successfully but no trace results were found. Searched path was {ArtifactPath}", FromArtifactPath);
return;
}
// iterate over each of the discovered traces (there should be one for each test case that was run)
// first, sort the cases by timestamp
string[] SortedTraces =
(from TraceFile in DiscoveredTraces
let Timestamp = File.GetCreationTimeUtc(TraceFile)
orderby Timestamp descending
select TraceFile).ToArray();
var ReportPath = Path.Combine(ToOutputPath, "Traces");
if (SortedTraces.Length > 0)
{
string Filename = Path.GetFileNameWithoutExtension(SortedTraces[0]);
string PerfTracePath = Path.Combine(ReportPath, Filename + ".utrace");
Log.Info("Copying latest utrace file from {ArtifactPath} to Perf .utrace path: {PerfTracePath}", FromArtifactPath,
PerfTracePath);
// just try the copy over, and log a failure, but don't bail out of the test.
try
{
InternalUtils.SafeCreateDirectory(Path.GetDirectoryName(PerfTracePath), true);
File.Copy(SortedTraces[0], PerfTracePath);
}
catch (Exception e)
{
Log.Warning("Failed to copy local trace file: {Text}", e);
}
}
}
protected void ConfigureCSVProfiler(TConfigClass Config, UnrealTestRole ClientRole)
{
if (Globals.Params.ParseParam("LocalReports") || (!IsBuildMachine && !Globals.Params.ParseParam("NoLocalReports")))
{
GenerateLocalReport = true;
}
if((IsBuildMachine || Globals.Params.ParseParam("PerfReportServer")) &&
!Globals.Params.ParseParam("SkipPerfReportServer"))
{
GeneratePRSReport = true;
}
ClientRole.CommandLineParams.Add("AutomatedPerfTest.DoCSVProfiler");
ClientRole.CommandLineParams.Add("csvGpuStats");
// Add CSV metadata
List<string> CsvMetadata =
[
$"testname={Context.BuildInfo.ProjectName}",
"gauntletTestType=AutomatedPerfTest",
$"gauntletSubTest={GetSubtestName(Config)}",
"testBuildIsPreflight=" + (ReportGenUtils.IsTestingPreflightBuild(OriginalBuildName) ? "1" : "0"),
$"testBuildVersion={OriginalBuildName}",
"testconfigname=" + Config.TestConfigName,
];
if (!string.IsNullOrEmpty(Context.BuildInfo.Branch) && Context.BuildInfo.Changelist != 0)
{
CsvMetadata.Add("branch=" + Context.BuildInfo.Branch);
CsvMetadata.Add("changelist=" + Context.BuildInfo.Changelist);
}
if (Config.DoGPUPerf)
{
CsvMetadata.Add("ReducedAsyncCompute=1");
}
ClientRole.CommandLineParams.Add("csvMetadata", "\"" + String.Join(",", CsvMetadata) + "\"");
}
static protected void GetConfigValues(UnrealTestContext Context, string IniSection, string IniElement, out IReadOnlyList<string> Values)
{
IniConfigUtil.GetConfigHierarchy(Context, ConfigHierarchyType.Engine).TryGetValues(IniSection, IniElement, out Values);
}
static protected string GetPathInProject(UnrealTestContext Context, string InPath)
{
return Path.Combine(Context.BuildInfo.ProjectPath.Directory.FullName, InPath);
}
static protected void ReadConfigArray(UnrealTestContext Context, string IniSection, string IniElement, Action<string> Process)
{
IReadOnlyList<string> Configs = null;
GetConfigValues(Context, IniSection, IniElement, out Configs);
List<string> ConfigList = Configs == null ? new List<string>() : Configs.ToList();
ConfigList.ForEach(Process);
}
}
/// <summary>
/// Implementation of a Gauntlet TestNode for AutomatedPerfTest plugin
/// </summary>
/// <typeparam name="TConfigClass"></typeparam>
public abstract class AutomatedSequencePerfTestNode<TConfigClass> : AutomatedPerfTestNode<TConfigClass>, IAutomatedPerfTest
where TConfigClass : AutomatedSequencePerfTestConfig, new()
{
public AutomatedSequencePerfTestNode(UnrealTestContext InContext) : base(InContext)
{
SummaryTable = "sequence";
}
public override TConfigClass GetConfiguration()
{
TConfigClass Config = base.GetConfiguration();
Config.DataSourceName = Config.GetDataSourceName(Context.BuildInfo.ProjectName, "Sequence");
// extend the role(s) that we initialized in the base class
if (Config.GetRequiredRoles(UnrealTargetRole.Client).Any())
{
foreach(UnrealTestRole ClientRole in Config.GetRequiredRoles(UnrealTargetRole.Client))
{
ClientRole.Controllers.Add("AutomatedSequencePerfTest");
// if a specific MapSequenceComboName was defined in the commandline to UAT, then add that to the commandline for the role
if (!string.IsNullOrEmpty(Config.MapSequenceComboName))
{
// use add Unique, since there should only ever be one of these specified
ClientRole.CommandLineParams.AddUnique($"AutomatedPerfTest.SequencePerfTest.MapSequenceName",
Config.MapSequenceComboName);
}
}
}
return Config;
}
public List<string> GetTestsFromConfig()
{
List<string> OutSequenceList = new List<string>();
ReadConfigArray(Context,
"/Script/AutomatedPerfTesting.AutomatedSequencePerfTestProjectSettings",
"MapsAndSequencesToTest",
Config =>
{
Dictionary<string, string> SequenceConfig = IniConfigUtil.ParseDictionaryFromConfigString(Config);
string ComboName;
if (SequenceConfig.TryGetValue("ComboName", out ComboName))
{
OutSequenceList.Add(ComboName.Replace("\"", ""));
}
});
return OutSequenceList;
}
protected override string GetNormalizedInsightsFileName(string CSVFileName)
{
return GetNormalizedInsightsFileName(CSVFileName, "_Sequence");
}
}
/// <summary>
/// Implementation of a Gauntlet TestNode for AutomatedPerfTest plugin
/// </summary>
/// <typeparam name="TConfigClass"></typeparam>
public abstract class AutomatedReplayPerfTestNode<TConfigClass> : AutomatedPerfTestNode<TConfigClass>, IAutomatedPerfTest
where TConfigClass : AutomatedReplayPerfTestConfig, new()
{
public AutomatedReplayPerfTestNode(UnrealTestContext InContext) : base(InContext)
{
Config = null;
}
/// <summary>
/// Handler to explicitly copy files to the device. It is assumed this
/// is called when Gauntlet configures the device before starting the
/// tests.
/// </summary>
public void CopyReplayToDevice(ITargetDevice Device)
{
if (Config == null)
{
Log.Warning("ConfigureDevice() called before Test Node Configuration is set.");
return;
}
// At the moment in some pathways we don't seem to copy
// files in a role. This ensures we have a copy of the
// replay files on the target device regardless.
if (Config.GetRequiredRoles(UnrealTargetRole.Client).Any())
{
foreach (UnrealTestRole ClientRole in Config.GetRequiredRoles(UnrealTargetRole.Client))
{
UnrealTargetPlatform Platform = Context.GetRoleContext(UnrealTargetRole.Client).Platform;
if (Device.Platform == Platform && ClientRole.FilesToCopy.Any())
{
Device.CopyAdditionalFiles(ClientRole.FilesToCopy);
}
}
}
}
public override TConfigClass GetConfiguration()
{
Config = base.GetConfiguration();
Config.DataSourceName = Config.GetDataSourceName(Context.BuildInfo.ProjectName, ReplayDataSourceName);
// Since we do not have a device instance at this stage. These platform support
// instances would help if we would still like to know if the target platform has
// certain features or functionality we need to be aware of in order to pass the
// right configuration to Gauntlet.
IEnumerable<IPlatformTargetSupport> PlatformSupportInstances = Gauntlet.Utils.InterfaceHelpers.FindImplementations<IPlatformTargetSupport>();
IEnumerable<UnrealTestRole> ClientRoles = Config.GetRequiredRoles(UnrealTargetRole.Client);
// extend the role(s) that we initialized in the base class
if (ClientRoles.Any())
{
foreach (UnrealTestRole ClientRole in ClientRoles)
{
ClientRole.Controllers.Add("AutomatedReplayPerfTest");
ClientRole.FilesToCopy.Clear();
UnrealTargetPlatform Platform = Context.GetRoleContext(UnrealTargetRole.Client).Platform;
IPlatformTargetSupport MountablePlatform = PlatformSupportInstances
.Where(TargetSupport => TargetSupport.IsHostMountingSupported() && TargetSupport.Platform == Platform)
.FirstOrDefault();
List<string> ReplaysFromConfig = GetTestsFromConfig();
// Replay name not provided
if (string.IsNullOrEmpty(Config.ReplayName))
{
if(ReplaysFromConfig == null || !ReplaysFromConfig.Any())
{
throw new AutomationException("No replays found in settings or provided via arguments.");
}
// Use the first replay found. We need to extract the replay path from settings as we need to
// post process the path depending on the target platform.
Config.ReplayName = ReplaysFromConfig[0];
}
else
{
foreach(string Replay in ReplaysFromConfig)
{
if(!string.IsNullOrEmpty(Replay) && Replay.Contains(Config.ReplayName))
{
Log.Info("Found replay in settings");
Config.ReplayName = Replay;
break;
}
}
}
string ReplayName = Path.GetFullPath(Config.ReplayName);
bool bFileExists = File.Exists(ReplayName);
if (!bFileExists)
{
// In case the replay file specified is not in settings
// or not an absolute path. Check under project directory
// to be sure.
ReplayName = GetPathInProject(Context, Config.ReplayName);
bFileExists = File.Exists(ReplayName);
}
if(!bFileExists)
{
throw new AutomationException($"Replay file '{Config.ReplayName}' not found");
}
if (MountablePlatform != null)
{
// If current device supports host mounting of files, update the path and use host mounting
ReplayName = MountablePlatform.GetHostMountedPath(ReplayName);
Log.Info($"Host Mountable Platform Detected. Updated Replay File Path to: {ReplayName} ");
}
else
{
// Copy replay file to Demos folder in device if host mounting is not supported
Log.Info($"Copy Replay File Path to Device: {ReplayName}");
UnrealFileToCopy FileToCopy = new UnrealFileToCopy(ReplayName, EIntendedBaseCopyDirectory.Demos, Path.GetFileName(ReplayName));
ClientRole.FilesToCopy.Add(FileToCopy);
ClientRole.ConfigureDevice = CopyReplayToDevice;
// If we copy to the "Demos" folder, we just need to pass the Replay Name without
// path and extension as the replay subsystem will automatically pick up the file
// from this folder.
ReplayName = Path.GetFileNameWithoutExtension(ReplayName);
}
ClientRole.CommandLineParams.AddUnique(ReplayParamName, ReplayName);
Log.Info($"{ReplayParamName}=\"{ReplayName}\"");
}
}
return Config;
}
public List<string> GetTestsFromConfig()
{
List<string> OutReplayList = new List<string>();
ReadConfigArray(Context,
"/Script/AutomatedPerfTesting.AutomatedReplayPerfTestProjectSettings",
"ReplaysToTest",
Config =>
{
Dictionary<string, string> ReplayConfig = IniConfigUtil.ParseDictionaryFromConfigString(Config);
string Path;
if (ReplayConfig.TryGetValue("FilePath", out Path))
{
OutReplayList.Add(GetPathInProject(Context, Path.Replace("\"", "")));
}
});
return OutReplayList;
}
protected override string GetNormalizedInsightsFileName(string CSVFileName)
{
return GetNormalizedInsightsFileName(CSVFileName, "_Replay");
}
private readonly string ReplayDataSourceName = "ReplayRun";
private readonly string ReplayParamName = "AutomatedPerfTest.ReplayPerfTest.ReplayName";
private TConfigClass Config;
}
/// <summary>
/// Implementation of a Gauntlet TestNode for AutomatedPerfTest plugin
/// </summary>
/// <typeparam name="TConfigClass"></typeparam>
public abstract class AutomatedStaticCameraPerfTestNode<TConfigClass> : AutomatedPerfTestNode<TConfigClass>, IAutomatedPerfTest
where TConfigClass : AutomatedStaticCameraPerfTestConfig, new()
{
public AutomatedStaticCameraPerfTestNode(UnrealTestContext InContext) : base(InContext)
{
SummaryTable = "staticcamera";
}
public List<string> GetTestsFromConfig()
{
List<string> OutMaps = new List<string>();
ReadConfigArray(Context,
"/Script/AutomatedPerfTesting.AutomatedStaticCameraPerfTestProjectSettings",
"MapsToTest",
Map => OutMaps.Add(Map.Replace("\"", "")));
return OutMaps;
}
public override TConfigClass GetConfiguration()
{
TConfigClass Config = base.GetConfiguration();
Config.DataSourceName = Config.GetDataSourceName(Context.BuildInfo.ProjectName, "StaticCamera");
// extend the role(s) that we initialized in the base class
if (Config.GetRequiredRoles(UnrealTargetRole.Client).Any())
{
foreach(UnrealTestRole ClientRole in Config.GetRequiredRoles(UnrealTargetRole.Client))
{
ClientRole.Controllers.Add("AutomatedPlacedStaticCameraPerfTest");
// if a specific MapName was defined in the commandline to UAT, then add that to the commandline for the role
if (!string.IsNullOrEmpty(Config.MapName))
{
// use add Unique, since there should only ever be one of these specified
ClientRole.CommandLineParams.AddUnique($"AutomatedPerfTest.StaticCameraPerfTest.MapName",
Config.MapName);
}
}
}
return Config;
}
protected override string GetNormalizedInsightsFileName(string CSVFileName)
{
return GetNormalizedInsightsFileName(CSVFileName, "_StaticCamera");
}
}
/// <summary>
/// "Standard issue" implementation usable for samples that don't need anything more advanced
/// </summary>
public class SequenceTest : AutomatedSequencePerfTestNode<AutomatedSequencePerfTestConfig>
{
public SequenceTest(Gauntlet.UnrealTestContext InContext)
: base(InContext)
{
}
}
/// <summary>
/// "Standard issue" implementation usable for samples that don't need anything more advanced
/// </summary>
public class StaticCameraTest : AutomatedStaticCameraPerfTestNode<AutomatedStaticCameraPerfTestConfig>
{
public StaticCameraTest(Gauntlet.UnrealTestContext InContext)
: base(InContext)
{
}
}
/// <summary>
/// Implementation of a Gauntlet TestNode for AutomatedPerfTest plugin
/// </summary>
/// <typeparam name="TConfigClass"></typeparam>
public abstract class AutomatedMaterialPerfTestNode<TConfigClass> : AutomatedPerfTestNode<TConfigClass>, IAutomatedPerfTest
where TConfigClass : AutomatedMaterialPerfTestConfig, new()
{
public AutomatedMaterialPerfTestNode(UnrealTestContext InContext) : base(InContext)
{
SummaryTable = "materials";
}
public List<string> GetTestsFromConfig()
{
List<string> OutMaterials = new List<string>();
ReadConfigArray(Context,
"/Script/AutomatedPerfTesting.AutomatedMaterialPerfTestProjectSettings",
"MaterialsToTest",
Material => OutMaterials.Add(Material.Replace("\"", "")));
return OutMaterials;
}
public override TConfigClass GetConfiguration()
{
TConfigClass Config = base.GetConfiguration();
Config.DataSourceName = Config.GetDataSourceName(Context.BuildInfo.ProjectName, "Material");
// extend the role(s) that we initialized in the base class
if (Config.GetRequiredRoles(UnrealTargetRole.Client).Any())
{
foreach(UnrealTestRole ClientRole in Config.GetRequiredRoles(UnrealTargetRole.Client))
{
ClientRole.Controllers.Add("AutomatedMaterialPerfTest");
}
}
return Config;
}
protected override string GetNormalizedInsightsFileName(string CSVFileName)
{
return GetNormalizedInsightsFileName(CSVFileName, "_Materials");
}
}
/// <summary>
/// "Standard issue" implementation usable for samples that don't need anything more advanced
/// </summary>
public class MaterialTest : AutomatedMaterialPerfTestNode<AutomatedMaterialPerfTestConfig>
{
public MaterialTest(Gauntlet.UnrealTestContext InContext)
: base(InContext)
{
}
}
/// <summary>
/// "Standard issue" implementation usable for samples that don't need anything more advanced
/// </summary>
public class ReplayTest : AutomatedReplayPerfTestNode<AutomatedReplayPerfTestConfig>
{
public ReplayTest(Gauntlet.UnrealTestContext InContext)
: base(InContext)
{
}
}
public class DefaultTest : AutomatedPerfTestNode<AutomatedPerfTestConfigBase>
{
public DefaultTest(UnrealTestContext InContext)
: base(InContext)
{
InitDefaultTestTypes();
}
private ITestNode CreateDefaultTestType(out Type TestNodeType)
{
IAutomatedPerfTest TestNode = null;
TestNodeType = null;
foreach (Type TestType in CandidateTestTypes)
{
try
{
ConstructorInfo Constructor = TestType.GetConstructor([typeof(UnrealTestContext)]);
TestNode = Constructor?.Invoke([Context]) as IAutomatedPerfTest;
}
catch (Exception)
{
continue;
}
// Return the first candidate test type which have test(s) configured in project settings
TestNodeType = TestType;
List<string> Tests = TestNode.GetTestsFromConfig();
if (Tests != null && Tests.Count > 0)
{
// Once we have found the first compatible test, bail out.
break;
}
}
return TestNode as ITestNode;
}
public void InitDefaultTestTypes()
{
CandidateTestTypes = Gauntlet.Utils.InterfaceHelpers.FindTypes<IAutomatedPerfTest>(true, bConcreteTypesOnly:true).ToHashSet();
}
public override AutomatedPerfTestConfigBase GetConfiguration()
{
if (CachedConfig != null)
{
return CachedConfig;
}
Type TestType;
ITestNode TestNode = CreateDefaultTestType(out TestType);
if(TestNode == null)
{
throw new AutomationException("Could not find a default test for given project. " +
"Configure one of the available Automated Perf Tests in settings before re-running this test.");
}
dynamic TestNodeObject = Convert.ChangeType(TestNode, TestType);
AutomatedPerfTestConfigBase Config = TestNodeObject?.GetConfiguration();
// Let Default Test enable CSV profiler by default.
Config.DoCSVProfiler = true;
ConfigureCSVProfiler(Config, Config.RequireRole(UnrealTargetRole.Client));
// Pull all info we need from derived test node.
CachedConfig = Config;
BaseOutputPath = TestNodeObject?.BaseOutputPath;
return Config;
}
private HashSet<Type> CandidateTestTypes = new HashSet<Type>();
}
}