// Copyright Epic Games, Inc. All Rights Reserved. using AutomationTool; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; using EpicGames.Core; using UnrealBuildTool; using UnrealBuildBase; using Microsoft.Extensions.Logging; namespace AutomationTool.Benchmark { [Help("Runs benchmarks and reports overall results")] [Help("Example1: RunUAT BenchmarkBuild -all -project=Unreal")] [Help("Example2: RunUAT BenchmarkBuild -allcompile -project=Unreal+EngineTest -platform=PS4")] [Help("Example3: RunUAT BenchmarkBuild -editor -client -cook -cooknoshaderddc -cooknoddc -xge -noxge -singlecompile -nopcompile -project=Unreal+QAGame+EngineTest -platform=WIn64+PS4+XboxOne+Switch -iterations=3")] [Help("preview", "List everything that will run but don't do it")] [Help("project=", "Do tests on the specified project(s). E.g. -project=Unreal+FortniteGame+QAGame")] [Help("editor", "Time building the editor")] [Help("client", "Time building the for the specified platform(s)")] [Help("compile", "Time compiling the target")] [Help("singlecompile", "Do a single-file compile")] [Help("nopcompile", "Do a nothing-needs-compiled compile")] [Help("AllCompile", "Shorthand for -compile -singlecompile -nopcompile")] [Help("platform=", "Specify the platform(s) to use for client compilation/cooking, if empty the local platform be used if -client or -cook is specified")] [Help("xge", "Do a pass with XGE / FASTBuild (default)")] [Help("noxge", "Do a pass without XGE / FASTBuild")] [Help("cores=X+Y+Z", "Do noxge builds with these processor counts (default is Environment.ProcessorCount)")] [Help("editor-startup", "Time launching the editor. Specify maps with -editor-startup=map1+map2")] [Help("editor-pie", "Time pie'ing for a project (only valid when -project is specified). Specify maps with -editor-pie=map1+map2")] [Help("editor-game", "Time launching the editor as -game (only valid when -project is specified). Specify maps with -editor-game=map1+map2")] [Help("AllEditor", "Shorthand for -editor-startup -editor-pie -editor-game")] [Help("editor-maps", "Map to Launch/PIE with (only valid when using a single project. Same as setting editor-pie=m1+m2, editor-startup=m1+m2 individually ")] [Help("cook", "Time cooking the project for the specified platform(s). Specify maps with -editor-cook=map1+map2")] [Help("cook-iterative", "Time an iterative cook for the specified platform(s) (will run a cook first if -cook is not specified). Specify maps with -editor-cook-iterative=map1+map2")] [Help("AllCook", "Shorthand for -cook -cook-iterative")] [Help("warmddc", "Cook / PIE with a warm DDC")] [Help("hotddc", "Cook / PIE with a hot local DDC (an untimed pre-run is performed)")] [Help("coldddc", "Cook / PIE with a cold local DDC (a temporary folder is used)")] [Help("coldddc-noshared", "Cook / PIE with a cold local DDC and no shared ddc ")] [Help("noshaderddc", "Cook / PIE with no shaders in the DDC")] [Help("AllDDC", "Shorthand for -coldddc -coldddc-noshared -noshaderddc -hotddc")] [Help("All", "Shorthand for -editor -client -AllCompile -AllEditor -AllCook -AllDDC")] [Help("editorxge", "Do a pass with XGE for editor DDC (default)")] [Help("noeditorxge", "Do a pass without XGE for editor DDC")] [Help("UBTArgs=", "Extra args to use when compiling. -UBTArgs=\"-foo\" -UBT2Args=\"-bar\" will run two compile passes with -foo and -bar")] [Help("CookArgs=", "Extra args to use when cooking. -CookArgs=\"-foo\" -Cook2Args=\"-bar\" will run two cook passes with -foo and -bar")] [Help("LaunchArgs=", "Extra args to use for launching. -LaunchArgs=\"-foo\" -Launch2Args=\"-bar\" will run two launch passes with -foo and -bar")] [Help("PIEArgs=", "Extra args to use for PIE. -PIEArgs=\"-foo\" -PIE2Args=\"-bar\" will run two PIE passes with -foo and -bar")] [Help("iterations=", "How many times to perform each test)")] [Help("wait=", "How many seconds to wait between each test)")] [Help("csv", "Name/path of file to write CSV results to. If empty the local machine name will be used")] [Help("noclean", "Don't build from clean. (Mostly just to speed things up when testing)")] [Help("nopostclean", "Don't clean artifacts after a task when building a lot of platforms/projects")] class BenchmarkBuild : BuildCommand { class BenchmarkOptions : BuildCommand { public bool Preview = false; public bool DoUETests = false; public IEnumerable ProjectsToTest = Enumerable.Empty(); public IEnumerable PlatformsToTest = Enumerable.Empty(); // building public bool DoBuildEditorTests = false; public bool DoBuildClientTests = false; public bool DoCompileTests = false; public bool DoNoCompileTests = false; public bool DoSingleCompileTests = false; public IEnumerable CoresForLocalJobs = Enumerable.Empty(); // cooking public bool DoCookTests = false; public bool DoIterativeCookTests = false; // editor PIE tests public bool DoPIETests = false; // editor startup tests public bool DoLaunchEditorTests = false; public bool DoLaunchEditorGameTests = false; public IEnumerable StartupMapList = Enumerable.Empty(); public IEnumerable PIEMapList = Enumerable.Empty(); public IEnumerable GameMapList = Enumerable.Empty(); public IEnumerable CookMapList = Enumerable.Empty(); // misc public int Iterations = 1; public UBTBuildOptions BuildOptions = UBTBuildOptions.None; public int TimeBetweenTasks = 0; public List UBTArgs = new List(); public List CookArgs = new List(); public List PIEArgs = new List(); public List LaunchArgs = new List(); public string FileName = string.Format("{0}_Results.csv", Unreal.MachineName); public SortedSet XGEOptions = new SortedSet(); public SortedSet DDCOptions = new SortedSet(); public void ParseParams(string[] InParams) { this.Params = InParams; bool AllThings = ParseParam("all"); bool AllCompile = AllThings || ParseParam("AllCompile"); bool AllCooks = AllThings || ParseParam("AllCook"); bool AllEditor = AllThings || ParseParam("AllEditor"); bool AllClient = AllThings || ParseParam("AllClient"); bool AllDDC = AllThings || ParseParam("AllDDC"); Preview = ParseParam("preview"); DoUETests = AllThings || ParseParam("Unreal"); // targets DoBuildEditorTests = AllThings | ParseParam("editor"); DoBuildClientTests = AllThings | ParseParam("client"); // compile tests DoCompileTests = AllCompile | ParseParam("compile"); DoSingleCompileTests = AllCompile | ParseParam("singlecompile"); DoNoCompileTests = AllCompile | ParseParam("nopcompile"); // cooking DoCookTests = AllCooks | ParseParam("cook"); DoIterativeCookTests = AllCooks | ParseParam("cook-iterative"); // editor launch tests DoLaunchEditorTests = AllEditor | ParseParam("editor-startup"); DoLaunchEditorGameTests = AllEditor | ParseParam("editor-game"); DoPIETests = AllEditor | ParseParam("editor-pie"); var DDCCommandLineArgs = new Dictionary { {"warmddc", DDCTaskOptions.WarmDDC }, {"coldddc", DDCTaskOptions.ColdDDC }, {"coldddc-noshared", DDCTaskOptions.ColdDDCNoShared }, {"noshaderddc", DDCTaskOptions.NoShaderDDC }, {"hotddc", DDCTaskOptions.HotDDC }, }; foreach (var K in DDCCommandLineArgs.Keys) { if (ParseParam(K)) { DDCOptions.Add(DDCCommandLineArgs[K]); } else if (K != "warmddc" && AllDDC) { DDCOptions.Add(DDCCommandLineArgs[K]); } } var XGECommandLineArgs = new Dictionary { {"xge", XGETaskOptions.WithXGE }, {"noxge", XGETaskOptions.NoXGE }, {"noeditorxge", XGETaskOptions.NoEditorXGE }, {"editorxge", XGETaskOptions.WithEditorXGE } }; foreach (var K in XGECommandLineArgs.Keys) { if (ParseParam(K)) { XGEOptions.Add(XGECommandLineArgs[K]); } } Preview = ParseParam("Preview"); Iterations = ParseParamInt("Iterations", Iterations); TimeBetweenTasks = ParseParamInt("Wait", TimeBetweenTasks); // allow up to 10 UBT, Cook, PIE via -UBTArgs=etc, -UBT2Args=etc2, -CookArgs=etc -Cook2Args=etc2 etc for (int i = 0; i < 10; i++) { string PostFix = i == 0 ? "" : (i+1).ToString(); // Parse CookArgs, Cook2Args etc string CookParam = ParseParamValue("Cook" + PostFix + "Args", null); if (CookParam != null) { CookArgs.Add(CookParam); } else if (i == 0) { // add a default for the first cook CookArgs.Add(""); } // Parse PIEArgs, PIE22Args etc string PIEParam = ParseParamValue("PIE" + PostFix + "Args", null); if (PIEParam != null) { PIEArgs.Add(PIEParam); } else if (i == 0) { // add a default for the first PIE PIEArgs.Add(""); } // Parse LaunchArgs, Launch2Args etc string LaunchParam = ParseParamValue("Launch" + PostFix + "Args", null); if (!string.IsNullOrEmpty(LaunchParam)) { LaunchArgs.Add(LaunchParam); } else if (i == 0) { // add a default for the first launch LaunchArgs.Add(""); } // Parse UBTArgs, UBT2Args etc string UBTParam = ParseParamValue("UBT" + PostFix + "Args", null); if (!string.IsNullOrEmpty(UBTParam)) { UBTArgs.Add(UBTParam); } else if (i == 0) { // add a default for the first compile UBTArgs.Add(""); } } FileName = ParseParamValue("csv", FileName); // Parse the project arg { string ProjectsArg = ParseParamValue("project", null); ProjectsArg = ParseParamValue("projects", ProjectsArg); // Look at the project argument and verify it's a valid uproject if (!string.IsNullOrEmpty(ProjectsArg)) { ProjectsToTest = ProjectsArg.Split(new[] { '+', ',' }, StringSplitOptions.RemoveEmptyEntries); } } // Parse and validate platform list from arguments { string PlatformArg = ParseParamValue("platform", ""); PlatformArg = ParseParamValue("platforms", PlatformArg); if (!string.IsNullOrEmpty(PlatformArg)) { List ClientPlatforms = new List(); var PlatformList = PlatformArg.Split(new[] { '+', ',' }, StringSplitOptions.RemoveEmptyEntries); foreach (var Platform in PlatformList) { UnrealTargetPlatform PlatformEnum; if (!UnrealTargetPlatform.TryParse(Platform, out PlatformEnum)) { throw new AutomationException("{0} is not a valid Unreal Platform", Platform); } ClientPlatforms.Add(PlatformEnum); } PlatformsToTest = ClientPlatforms; } else { PlatformsToTest = new[] { BuildHostPlatform.Current.Platform }; } } // clean by default if (!ParseParam("noclean")) { BuildOptions |= UBTBuildOptions.PreClean; } // post-clean if we're building a lot of stuff if (!(ParseParam("nopostclean") && !ParseParam("noclean")) /*&& (PlatformsToTest.Count() > 1 || ProjectsToTest.Count() > 1)*/) { BuildOptions |= UBTBuildOptions.PostClean; Logger.LogInformation("Building multiple platforms. Will clean each platform after build step to save space. (use -nopostclean to prevent this)"); } // parse processor args { string ProcessorArg = ParseParamValue("cores", ""); if (!string.IsNullOrEmpty(ProcessorArg)) { var ProcessorList = ProcessorArg.Split(new[] { '+', ',' }, StringSplitOptions.RemoveEmptyEntries); CoresForLocalJobs = ProcessorList.Select(P => Convert.ToInt32(P)); } } Func ParseMapList = (string ArgName) => { string ArgValue = ParseParamValue(ArgName, ""); if (!string.IsNullOrEmpty(ArgValue)) { // don't remove empty entries so people can get the project default and map2 via +map2 return ArgValue.Split(new[] { '+', ',' }, StringSplitOptions.None); } return new string[] { }; }; // parse map args { // primary arg that sets all three var EditorMaps = ParseMapList("editor-maps"); if (EditorMaps.Any()) { StartupMapList = EditorMaps; PIEMapList = EditorMaps; GameMapList = EditorMaps; CookMapList = EditorMaps; } else { StartupMapList = ParseMapList("editor-startup"); PIEMapList = ParseMapList("editor-pie"); GameMapList = ParseMapList("editor-game"); CookMapList = ParseMapList("cook"); CookMapList = ParseMapList("cook-iterative"); } } bool DefaultToXGE = BenchmarkBuildTask.SupportsAcceleration; // If they specified cores, ensure NoXGE is on if (CoresForLocalJobs.Any()) { XGEOptions.Add(XGETaskOptions.NoXGE); } if (!DDCOptions.Any()) { DDCOptions.Add(DDCTaskOptions.WarmDDC); } // If the user provided no XGE / NoXGE compile flags, then give them a default if (!XGEOptions.Contains(XGETaskOptions.WithXGE) && !XGEOptions.Contains(XGETaskOptions.NoXGE)) { XGEOptions.Add(DefaultToXGE ? XGETaskOptions.WithXGE : XGETaskOptions.NoXGE); } // If the user provided no XGE / NoXGE editor flags, then give them a default if (!XGEOptions.Contains(XGETaskOptions.WithEditorXGE) && !XGEOptions.Contains(XGETaskOptions.NoEditorXGE)) { XGEOptions.Add(DefaultToXGE ? XGETaskOptions.WithEditorXGE : XGETaskOptions.NoEditorXGE); } // Make sure there's a default here if (!CoresForLocalJobs.Any()) { CoresForLocalJobs = new int[] { 0 }; } // sanity if (!BenchmarkBuildTask.SupportsAcceleration) { if (XGEOptions.Contains(XGETaskOptions.WithXGE) || XGEOptions.Contains(XGETaskOptions.WithEditorXGE)) { Logger.LogWarning("XGE requested but is not available. Removing XGE options"); XGEOptions.Remove(XGETaskOptions.WithXGE); XGEOptions.Remove(XGETaskOptions.WithEditorXGE); } } } } struct BenchmarkResult { public TimeSpan TaskTime { get; set; } public bool Failed { get; set; } } public BenchmarkBuild() { } public override ExitCode Execute() { BenchmarkOptions Options = new BenchmarkOptions(); Options.ParseParams(this.Params); List Tasks = new List(); Dictionary> Results = new Dictionary>(); for (int ProjectIndex = 0; ProjectIndex < Options.ProjectsToTest.Count(); ProjectIndex++) { string Project = Options.ProjectsToTest.ElementAt(ProjectIndex); FileReference ProjectFile = ProjectUtils.FindProjectFileFromName(Project); if (ProjectFile == null && !Project.Equals("Unreal", StringComparison.OrdinalIgnoreCase)) { throw new AutomationException("Could not find project file for {0}", Project); } bool TargetIsClientBuild = ProjectSupportsClientBuild(ProjectFile); ProjectTargetInfo EditorTarget = new ProjectTargetInfo(ProjectFile, BuildHostPlatform.Current.Platform, TargetIsClientBuild); // Do compile tests of editor and platforms if (Options.DoBuildEditorTests) { Tasks.AddRange(AddBuildTests(ProjectFile, BuildHostPlatform.Current.Platform, "Editor", Options)); } if (Options.DoBuildClientTests) { foreach (var ClientPlatform in Options.PlatformsToTest) { ProjectTargetInfo PlatformTarget = new ProjectTargetInfo(ProjectFile, ClientPlatform, TargetIsClientBuild); // do build tests Tasks.AddRange(AddBuildTests(ProjectFile, ClientPlatform, TargetIsClientBuild ? "Client" : "Game", Options)); } } var XGEEditorOptions = Options.XGEOptions.Where(Opt => (Opt == XGETaskOptions.WithEditorXGE || Opt == XGETaskOptions.NoEditorXGE)); List EditorTasks = new List(); if (Options.DoLaunchEditorTests) { EditorTasks.AddRange(AddEditorTests(EditorTarget, Options.StartupMapList, Options.LaunchArgs, Options.CoresForLocalJobs, XGEEditorOptions, Options.DDCOptions, EditorTasks.Any())); } // do PIE tests, so long as there's a project if (Options.DoPIETests && EditorTarget.ProjectFile != null) { EditorTasks.AddRange(AddEditorTests(EditorTarget, Options.PIEMapList, Options.PIEArgs, Options.CoresForLocalJobs, XGEEditorOptions, Options.DDCOptions, EditorTasks.Any())); } // do PIE tests, so long as there's a project if (Options.DoLaunchEditorGameTests && EditorTarget.ProjectFile != null) { EditorTasks.AddRange(AddEditorTests(EditorTarget, Options.GameMapList, Options.LaunchArgs, Options.CoresForLocalJobs, XGEEditorOptions, Options.DDCOptions, EditorTasks.Any())); } // cook tests foreach (var ClientPlatform in Options.PlatformsToTest) { ProjectTargetInfo PlatformTarget = new ProjectTargetInfo(ProjectFile, ClientPlatform, TargetIsClientBuild); // do cook tests,. so long as there's a project if (Options.DoCookTests && PlatformTarget.ProjectFile != null) { EditorTasks.AddRange(AddEditorTests(PlatformTarget, Options.CookMapList, Options.CookArgs, Options.CoresForLocalJobs, XGEEditorOptions, Options.DDCOptions, EditorTasks.Any())); } // do cook tests,. so long as there's a project if (Options.DoIterativeCookTests && PlatformTarget.ProjectFile != null) { int[] CoreLimit = { 0 }; XGETaskOptions[] DefaultXGE = { BenchmarkBuildTask.SupportsAcceleration ? XGETaskOptions.WithEditorXGE : XGETaskOptions.NoEditorXGE }; DDCTaskOptions[] WarmDDC = { DDCTaskOptions.WarmDDC }; // If not running any cooks run a single warm one so we can get iterative values if (!Options.DoCookTests) { var WarmupTasks = AddEditorTests(PlatformTarget, Options.CookMapList, Options.CookArgs, CoreLimit, DefaultXGE, WarmDDC, EditorTasks.Any()); WarmupTasks.ToList().ForEach(T => T.SkipReport = true); EditorTasks.AddRange(WarmupTasks); } EditorTasks.AddRange(AddEditorTests(PlatformTarget, Options.CookMapList, Options.CookArgs, CoreLimit, XGEEditorOptions, WarmDDC, EditorTasks.Any())); } } Tasks.AddRange(EditorTasks); } Logger.LogInformation("Will execute tasks:"); foreach (var Task in Tasks) { Logger.LogInformation("{Arg0}", Task.FullName); } if (!Options.Preview) { // create results lists foreach (var Task in Tasks) { Results.Add(Task, new List()); } DateTime StartTime = DateTime.Now; for (int i = 0; i < Options.Iterations; i++) { foreach (var Task in Tasks) { Logger.LogInformation("Starting task {Arg0} (Pass {Arg1})", Task.FullName, i + 1); Task.Run(); Logger.LogInformation("Task {Arg0} took {Arg1}", Task.FullName, Task.TaskTime.ToString(@"hh\:mm\:ss")); if (Task.Failed) { Logger.LogError("Task failed! Benchmark time may be inaccurate."); } if (Task.SkipReport) { Logger.LogInformation("Skipping reporting of {Arg0}", Task.FullName); } else { Results[Task].Add(new BenchmarkResult { TaskTime = Task.TaskTime, Failed = Task.Failed }); // write results so far WriteCSVResults(Options.FileName, Tasks, Results); } Logger.LogInformation("Waiting {Arg0} secs until next task", Options.TimeBetweenTasks); Thread.Sleep(Options.TimeBetweenTasks * 1000); } } Logger.LogInformation("**********************************************************************"); Logger.LogInformation("Test Results:"); foreach (var Task in Tasks) { string TimeString = ""; if (Task.SkipReport) { continue; } IEnumerable TaskResults = Results[Task]; foreach (var Result in TaskResults) { if (TimeString.Length > 0) { TimeString += ", "; } if (Result.Failed) { TimeString += "Failed"; } else { TimeString += Result.TaskTime.ToString(@"hh\:mm\:ss"); } } var AvgTimeString = ""; if (TaskResults.Count() > 1) { var AvgTime = new TimeSpan(TaskResults.Select(R => R.TaskTime).Sum(T => T.Ticks) / TaskResults.Count()); AvgTimeString = string.Format(" (Avg: {0})", AvgTime.ToString(@"hh\:mm\:ss")); } Logger.LogInformation("Task {Arg0}:\t\t{TimeString}{AvgTimeString}", Task.FullName, TimeString, AvgTimeString); } Logger.LogInformation("**********************************************************************"); TimeSpan Elapsed = DateTime.Now - StartTime; Logger.LogInformation("Total benchmark time: {Arg0}", Elapsed.ToString(@"hh\:mm\:ss")); WriteCSVResults(Options.FileName, Tasks, Results); } return ExitCode.Success; } IEnumerable AddBuildTests(FileReference InProjectFile, UnrealTargetPlatform InPlatform, string InTargetName, BenchmarkOptions InOptions) { List NewTasks = new List(); if (InOptions.DoCompileTests) { IEnumerable UBTArgList = InOptions.UBTArgs.Any() ? InOptions.UBTArgs : new[] { "" }; if (InOptions.XGEOptions.Contains(XGETaskOptions.WithXGE)) { foreach (string UBTArgs in UBTArgList) { NewTasks.Add(new BenchmarkBuildTask(InProjectFile, InTargetName, InPlatform, XGETaskOptions.WithXGE, UBTArgs, 0, InOptions.BuildOptions)); } } if (InOptions.XGEOptions.Contains(XGETaskOptions.NoXGE)) { foreach (int ProcessorCount in InOptions.CoresForLocalJobs) { foreach (string UBTArgs in UBTArgList) { NewTasks.Add(new BenchmarkBuildTask(InProjectFile, InTargetName, InPlatform, XGETaskOptions.NoXGE, UBTArgs, ProcessorCount, InOptions.BuildOptions)); } } } } // If the user requested a single-compile /nop-compile and we haven't built anything, add one now if ((InOptions.DoSingleCompileTests || InOptions.DoNoCompileTests) && NewTasks.Any() == false) { NewTasks.Add(new BenchmarkBuildTask(InProjectFile, InTargetName, InPlatform, BenchmarkBuildTask.SupportsAcceleration ? XGETaskOptions.WithXGE : XGETaskOptions.NoXGE, "", 0)); } if (InOptions.DoSingleCompileTests) { // note, don't clean since we build normally then build again NewTasks.Add(new BenchmarkSingleCompileTask(InProjectFile, InTargetName, InPlatform, InOptions.XGEOptions.First())); } if (InOptions.DoNoCompileTests) { // note, don't clean since we build normally then build a single file NewTasks.Add(new BenchmarkNopCompileTask(InProjectFile, InTargetName, InPlatform, InOptions.XGEOptions.First())); } // clean stuff if we're doing compilation tasks that aren't the editor as we can use masses of disk space... if (InOptions.DoCompileTests) { if (InOptions.BuildOptions.HasFlag(UBTBuildOptions.PostClean) && !InTargetName.Equals("Editor", StringComparison.OrdinalIgnoreCase)) { var Task = new BenchmarkCleanBuildTask(InProjectFile, InTargetName, InPlatform); Task.SkipReport = true; NewTasks.Add(Task); } } return NewTasks; } IEnumerable AddEditorTests(ProjectTargetInfo InTargetInfo, IEnumerable InMaps, IEnumerable InArgVariations, IEnumerable CoreVariations, IEnumerable InXGEOptions, IEnumerable InDDCOptions, bool SkipBuildEditor) where T : BenchmarkEditorTaskBase { if (InTargetInfo == null) { return Enumerable.Empty(); } List NewTasks = new List(); IEnumerable ArgVariations = InArgVariations.Any() ? InArgVariations : new List { "" }; IEnumerable MapVariations = InMaps.Any() ? InMaps : new List { "" }; // If the user is running a hotddc test and there's only one type, run a warm pass first if (InDDCOptions.Contains(DDCTaskOptions.HotDDC) && InDDCOptions.Count() == 1) { ProjectTaskOptions TaskOptions = new ProjectTaskOptions(DDCTaskOptions.WarmDDC, InXGEOptions.First(), "", MapVariations.First(), 0); var NewTask = Activator.CreateInstance(typeof(T), new object[] { InTargetInfo, TaskOptions, SkipBuildEditor }) as BenchmarkEditorTaskBase; NewTask.SkipReport = true; NewTasks.Add(NewTask); // don't build the editor again SkipBuildEditor = true; } foreach (string Args in ArgVariations) { foreach (var Map in MapVariations) { foreach (XGETaskOptions XGEOption in InXGEOptions) { bool bCoreVariations = XGEOption == XGETaskOptions.NoEditorXGE && CoreVariations.Any(); IEnumerable CoresForJobs = bCoreVariations ? CoreVariations : new int[] { 0 }; foreach (var CoreLimit in CoresForJobs) { // DDC must be last expansion as things are ordered with assumptions foreach (var DDCOption in InDDCOptions) { ProjectTaskOptions TaskOptions = new ProjectTaskOptions(DDCOption, XGEOption, Args, Map, CoreLimit); var NewTask = Activator.CreateInstance(typeof(T), new object[] { InTargetInfo, TaskOptions, SkipBuildEditor }) as BenchmarkTaskBase; NewTasks.Add(NewTask); // don't build the editor again SkipBuildEditor = true; } } } } } return NewTasks; } /// /// Writes our current result to a CSV file. It's expected that this function is called multiple times so results are /// updated as we go /// void WriteCSVResults(string InFileName, IEnumerable InTasks, Dictionary> InResults) { Logger.LogInformation("Writing results to {InFileName}", InFileName); try { List Lines = new List(); // first line is machine name,CPU count,Iteration 1, Iteration 2 etc string FirstLine = string.Format("{0},{1} Cores,StartTime", Unreal.MachineName, Environment.ProcessorCount); if (InTasks.Count() > 0) { int Iterations = InResults[InTasks.First()].Count(); if (Iterations > 0) { for (int i = 0; i < Iterations; i++) { FirstLine += ","; FirstLine += string.Format("Iteration {0}", i + 1); } if (Iterations > 1) { FirstLine += ",Average"; } } } Lines.Add(FirstLine); foreach (var Task in InTasks.Where(T => T.SkipReport == false)) { // start with Name, StartTime string Line = string.Format("{0},{1},{2}", Task.ProjectName, Task.TaskNameWithModifiers, Task.StartTime.ToString("yyyy-dd-MM HH:mm:ss")); IEnumerable TaskResults = InResults[Task]; bool DidFail = false; // now append all iteration times foreach (BenchmarkResult Result in TaskResults) { Line += ","; if (Result.Failed) { Line += "FAILED"; DidFail = true; } else { Line += Result.TaskTime.ToString(@"hh\:mm\:ss"); } } if (TaskResults.Count() > 1) { if (DidFail) { Line += ",FAILED"; } else { var AvgTime = new TimeSpan(TaskResults.Select(R => R.TaskTime).Sum(T => T.Ticks) / InResults[Task].Count()); Line += "," + AvgTime.ToString(@"hh\:mm\:ss"); } } Lines.Add(Line); } File.WriteAllLines(InFileName, Lines.ToArray()); } catch (Exception Ex) { Logger.LogError("Failed to write CSV to {InFileName}. {Ex}", InFileName, Ex); } } /// /// Returns true/false based on whether the project supports a client configuration /// /// /// bool ProjectSupportsClientBuild(FileReference InProjectFile) { if (InProjectFile == null) { // UE return true; } ProjectProperties Properties = ProjectUtils.GetProjectProperties(InProjectFile); return Properties.Targets.Where(T => T.Rules.Type == TargetType.Client).Any(); } } }