Files
UnrealEngineUWP/Engine/Source/Programs/UnrealBuildTool/Executors/ParallelExecutor.cs
2022-05-31 15:44:53 -04:00

490 lines
18 KiB
C#

// Copyright Epic Games, Inc. All Rights Reserved.
using EpicGames.Core;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
namespace UnrealBuildTool
{
/// <summary>
/// This executor uses async Tasks to process the action graph
/// </summary>
class ParallelExecutor : ActionExecutor
{
/// <summary>
/// Maximum processor count for local execution.
/// </summary>
[XmlConfigFile]
private static int MaxProcessorCount = int.MaxValue;
/// <summary>
/// Processor count multiplier for local execution. Can be below 1 to reserve CPU for other tasks.
/// When using the local executor (not XGE), run a single action on each CPU core. Note that you can set this to a larger value
/// to get slightly faster build times in many cases, but your computer's responsiveness during compiling may be much worse.
/// This value is ignored if the CPU does not support hyper-threading.
/// </summary>
[XmlConfigFile]
private static double ProcessorCountMultiplier = 1.0;
/// <summary>
/// Free memory per action in bytes, used to limit the number of parallel actions if the machine is memory starved.
/// Set to 0 to disable free memory checking.
/// </summary>
[XmlConfigFile]
private static double MemoryPerActionBytes = 1.5 * 1024 * 1024 * 1024;
/// <summary>
/// The priority to set for spawned processes.
/// Valid Settings: Idle, BelowNormal, Normal, AboveNormal, High
/// Default: BelowNormal
/// </summary>
[XmlConfigFile]
private static ProcessPriorityClass ProcessPriority = ProcessPriorityClass.BelowNormal;
/// <summary>
/// When enabled, will stop compiling targets after a compile error occurs.
/// </summary>
[XmlConfigFile]
private static bool bStopCompilationAfterErrors = false;
/// <summary>
/// Whether to show compilation times along with worst offenders or not.
/// </summary>
[XmlConfigFile]
private static bool bShowCompilationTimes = false;
/// <summary>
/// Whether to show compilation times for each executed action
/// </summary>
[XmlConfigFile]
private static bool bShowPerActionCompilationTimes = false;
/// <summary>
/// Whether to log command lines for actions being executed
/// </summary>
[XmlConfigFile]
private static bool bLogActionCommandLines = false;
/// <summary>
/// Add target names for each action executed
/// </summary>
[XmlConfigFile]
private static bool bPrintActionTargetNames = false;
/// <summary>
/// Collapse non-error output lines
/// </summary>
private bool bCompactOutput = false;
/// <summary>
/// How many processes that will be executed in parallel
/// </summary>
public int NumParallelProcesses { get; private set; }
private static readonly char[] LineEndingSplit = new char[] { '\n', '\r' };
public static int GetDefaultNumParallelProcesses(ILogger Logger)
{
double MemoryPerActionBytesComputed = Math.Max(MemoryPerActionBytes, MemoryPerActionBytesOverride);
if (MemoryPerActionBytesComputed > MemoryPerActionBytes)
{
Logger.LogInformation("Overriding MemoryPerAction with target-defined value of {Memory} bytes", MemoryPerActionBytesComputed / 1024 / 1024 / 1024);
}
return Utils.GetMaxActionsToExecuteInParallel(MaxProcessorCount, ProcessorCountMultiplier, Convert.ToInt64(MemoryPerActionBytesComputed));
}
/// <summary>
/// Constructor
/// </summary>
/// <param name="MaxLocalActions">How many actions to execute in parallel</param>
/// <param name="bCompactOutput">Should output be written in a compact fashion</param>
/// <param name="Logger">Logger for output</param>
public ParallelExecutor(int MaxLocalActions, bool bCompactOutput, ILogger Logger)
{
XmlConfig.ApplyTo(this);
// if specified this caps how many processors we can use
if (MaxLocalActions > 0)
{
NumParallelProcesses = MaxLocalActions;
}
else
{
// Figure out how many processors to use
NumParallelProcesses = GetDefaultNumParallelProcesses(Logger);
}
this.bCompactOutput = bCompactOutput;
}
/// <summary>
/// Returns the name of this executor
/// </summary>
public override string Name
{
get { return "Parallel"; }
}
/// <summary>
/// Checks whether the task executor can be used
/// </summary>
/// <returns>True if the task executor can be used</returns>
public static bool IsAvailable()
{
return true;
}
protected class ExecuteResults
{
public List<string> LogLines { get; private set; }
public int ExitCode { get; private set; }
public TimeSpan ExecutionTime { get; private set; }
public TimeSpan ProcessorTime { get; private set; }
public string? AdditionalDescription { get; protected set; } = null;
public ExecuteResults(List<string> LogLines, int ExitCode, TimeSpan ExecutionTime, TimeSpan ProcessorTime)
{
this.LogLines = LogLines;
this.ExitCode = ExitCode;
this.ProcessorTime = ProcessorTime;
this.ExecutionTime = ExecutionTime;
}
public ExecuteResults(List<string> LogLines, int ExitCode)
{
this.LogLines = LogLines;
this.ExitCode = ExitCode;
}
}
/// <summary>
/// Executes the specified actions locally.
/// </summary>
/// <returns>True if all the tasks successfully executed, or false if any of them failed.</returns>
public override bool ExecuteActions(List<LinkedAction> InputActions, ILogger Logger)
{
int NumCompletedActions = 0;
int TotalActions = InputActions.Count;
int ActualNumParallelProcesses = Math.Min(TotalActions, NumParallelProcesses);
using ManagedProcessGroup ProcessGroup = new ManagedProcessGroup();
using SemaphoreSlim MaxProcessSemaphore = new SemaphoreSlim(ActualNumParallelProcesses, ActualNumParallelProcesses);
using ProgressWriter ProgressWriter = new ProgressWriter("Compiling C++ source code...", false, Logger);
Logger.LogInformation("Building {NumActions} {Actions} with {NumProcesses} {Processes}...", TotalActions, (TotalActions == 1 ? "action" : "actions"), ActualNumParallelProcesses, (ActualNumParallelProcesses == 1 ? "process" : "processes"));
Dictionary<LinkedAction, Task<ExecuteResults>> ExecuteTasks = new Dictionary<LinkedAction, Task<ExecuteResults>>();
List<Task> LogTasks = new List<Task>();
using LogIndentScope Indent = new LogIndentScope(" ");
CancellationTokenSource CancellationTokenSource = new CancellationTokenSource();
CancellationToken CancellationToken = CancellationTokenSource.Token;
// Create a task for every action
foreach (LinkedAction Action in InputActions)
{
if (ExecuteTasks.ContainsKey(Action))
{
continue;
}
Task<ExecuteResults> ExecuteTask = CreateExecuteTask(Action, InputActions, ExecuteTasks, ProcessGroup, MaxProcessSemaphore, CancellationToken);
Task LogTask = ExecuteTask.ContinueWith(antecedent => LogCompletedAction(Action, antecedent, CancellationTokenSource, ProgressWriter, TotalActions, ref NumCompletedActions, Logger), CancellationToken);
ExecuteTasks.Add(Action, ExecuteTask);
LogTasks.Add(LogTask);
}
Task SummaryTask = Task.Factory.ContinueWhenAll(LogTasks.ToArray(), (AntecedentTasks) => TraceSummary(ExecuteTasks, ProcessGroup, Logger), CancellationToken);
SummaryTask.Wait();
// Return if all tasks succeeded
return ExecuteTasks.Values.All(x => x.Result.ExitCode == 0);
}
protected static Task<ExecuteResults> CreateExecuteTask(LinkedAction Action, List<LinkedAction> InputActions, Dictionary<LinkedAction, Task<ExecuteResults>> ExecuteTasks, ManagedProcessGroup ProcessGroup, SemaphoreSlim MaxProcessSemaphore, CancellationToken CancellationToken)
{
List<LinkedAction> PrerequisiteActions = Action.PrerequisiteActions.Where(x => InputActions.Contains(x)).ToList();
if (PrerequisiteActions.Count == 0)
{
return Task.Factory.StartNew(
() => ExecuteAction(new Task<ExecuteResults>[0], Action, ProcessGroup, MaxProcessSemaphore, CancellationToken),
CancellationToken,
TaskCreationOptions.LongRunning,
TaskScheduler.Current
).Unwrap();
}
// Create tasks for any preresquite actions if they don't exist already
List<Task<ExecuteResults>> PrerequisiteTasks = new List<Task<ExecuteResults>>();
foreach (var PrerequisiteAction in PrerequisiteActions)
{
if (!ExecuteTasks.ContainsKey(PrerequisiteAction))
{
ExecuteTasks.Add(PrerequisiteAction, CreateExecuteTask(PrerequisiteAction, InputActions, ExecuteTasks, ProcessGroup, MaxProcessSemaphore, CancellationToken));
}
PrerequisiteTasks.Add(ExecuteTasks[PrerequisiteAction]);
}
return Task.Factory.ContinueWhenAll(
PrerequisiteTasks.ToArray(),
(AntecedentTasks) => ExecuteAction(AntecedentTasks, Action, ProcessGroup, MaxProcessSemaphore, CancellationToken),
CancellationToken,
TaskContinuationOptions.LongRunning,
TaskScheduler.Current
).Unwrap();
}
protected static async Task WaitPrerequsiteActions(LinkedAction Action, Dictionary<LinkedAction, Task<ExecuteResults>> Tasks)
{
// Wait for all PrerequisiteActions to complete
ExecuteResults[] Results = await Task.WhenAll(Action.PrerequisiteActions.Select(x => Tasks[x]));
// Cancel tasks if any PrerequisiteActions fail, unless a PostBuildStep
if (Action.ActionType != ActionType.PostBuildStep && Results.Any(x => x.ExitCode != 0))
{
throw new OperationCanceledException();
}
}
protected static async Task<ExecuteResults> ExecuteAction(Task<ExecuteResults>[] AntecedentTasks, LinkedAction Action, ManagedProcessGroup ProcessGroup, SemaphoreSlim MaxProcessSemaphore, CancellationToken CancellationToken)
{
Task? SemaphoreTask = null;
try
{
// Cancel tasks if any PrerequisiteActions fail, unless a PostBuildStep
if (Action.ActionType != ActionType.PostBuildStep && AntecedentTasks.Any(x => x.Result.ExitCode != 0))
{
throw new OperationCanceledException();
}
// Limit the number of concurrent processes that will run in parallel
SemaphoreTask = MaxProcessSemaphore.WaitAsync(CancellationToken);
await SemaphoreTask;
return await RunAction(Action, ProcessGroup, CancellationToken);
}
catch (OperationCanceledException)
{
return new ExecuteResults(new List<string>(), int.MaxValue);
}
catch (Exception Ex)
{
Log.WriteException(Ex, null);
return new ExecuteResults(new List<string>(), int.MaxValue);
}
finally
{
if (SemaphoreTask?.Status == TaskStatus.RanToCompletion)
{
MaxProcessSemaphore.Release();
}
}
}
protected static async Task<ExecuteResults> RunAction(LinkedAction Action, ManagedProcessGroup ProcessGroup, CancellationToken CancellationToken)
{
CancellationToken.ThrowIfCancellationRequested();
using ManagedProcess Process = new ManagedProcess(ProcessGroup, Action.CommandPath.FullName, Action.CommandArguments, Action.WorkingDirectory.FullName, null, null, ProcessPriority);
using MemoryStream StdOutStream = new MemoryStream();
await Process.CopyToAsync(StdOutStream, CancellationToken);
CancellationToken.ThrowIfCancellationRequested();
Process.WaitForExit();
List<string> LogLines = Console.OutputEncoding.GetString(StdOutStream.GetBuffer(), 0, Convert.ToInt32(StdOutStream.Length)).Split(LineEndingSplit, StringSplitOptions.RemoveEmptyEntries).ToList();
int ExitCode = Process.ExitCode;
TimeSpan ProcessorTime = Process.TotalProcessorTime;
TimeSpan ExecutionTime = Process.ExitTime - Process.StartTime;
return new ExecuteResults(LogLines, ExitCode, ExecutionTime, ProcessorTime);
}
private static int s_previousLineLength = -1;
protected void LogCompletedAction(LinkedAction Action, Task<ExecuteResults> ExecuteTask, CancellationTokenSource CancellationTokenSource, ProgressWriter ProgressWriter, int TotalActions, ref int NumCompletedActions, ILogger Logger)
{
List<string> LogLines = new List<string>();
int ExitCode = int.MaxValue;
TimeSpan ExecutionTime = TimeSpan.Zero;
TimeSpan ProcessorTime = TimeSpan.Zero;
string? AdditionalDescription = null;
if (ExecuteTask.Status == TaskStatus.RanToCompletion)
{
ExecuteResults ExecuteTaskResult = ExecuteTask.Result;
LogLines = ExecuteTaskResult.LogLines;
ExitCode = ExecuteTaskResult.ExitCode;
ExecutionTime = ExecuteTaskResult.ExecutionTime;
ProcessorTime = ExecuteTaskResult.ProcessorTime;
AdditionalDescription = ExecuteTaskResult.AdditionalDescription;
}
// Write it to the log
string Description = string.Empty;
if (Action.bShouldOutputStatusDescription || LogLines.Count == 0)
{
Description = $"{(Action.CommandDescription ?? Action.CommandPath.GetFileNameWithoutExtension())} {Action.StatusDescription}".Trim();
}
else if (LogLines.Count > 0)
{
Description = $"{(Action.CommandDescription ?? Action.CommandPath.GetFileNameWithoutExtension())} {LogLines[0]}".Trim();
}
if (!string.IsNullOrEmpty(AdditionalDescription))
{
Description = $"{AdditionalDescription} {Description}";
}
lock (ProgressWriter)
{
int CompletedActions;
CompletedActions = Interlocked.Increment(ref NumCompletedActions);
ProgressWriter.Write(CompletedActions, TotalActions);
// Cancelled
if (ExitCode == int.MaxValue)
{
Logger.LogInformation("[{CompletedActions}/{TotalActions}] {Description} cancelled", CompletedActions, TotalActions, Description);
return;
}
string TargetDetails = "";
TargetDescriptor? Target = Action.Target;
if (bPrintActionTargetNames && Target != null)
{
TargetDetails = $"[{Target.Name} {Target.Platform} {Target.Configuration}]";
}
if (bLogActionCommandLines)
{
Logger.LogDebug("[{CompletedActions}/{TotalActions}]{TargetDetails} Command: {CommandPath} {CommandArguments}", CompletedActions, TotalActions, TargetDetails, Action.CommandPath, Action.CommandArguments);
}
string CompilationTimes = "";
if (bShowPerActionCompilationTimes)
{
if (ProcessorTime.Ticks > 0)
{
CompilationTimes = $" (Wall: {ExecutionTime.TotalSeconds:0.00}s CPU: {ProcessorTime.TotalSeconds:0.00}s)";
}
else
{
CompilationTimes = $" (Wall: {ExecutionTime.TotalSeconds:0.00}s)";
}
}
string message = ($"[{CompletedActions}/{TotalActions}]{TargetDetails}{CompilationTimes} {Description}");
if (bCompactOutput)
{
if (s_previousLineLength > 0)
{
// move the cursor to the far left position, one line back
Console.CursorLeft = 0;
Console.CursorTop -= 1;
// clear the line
Console.Write("".PadRight(s_previousLineLength));
// move the cursor back to the left, so output is written to the desired location
Console.CursorLeft = 0;
}
}
s_previousLineLength = message.Length;
Log.TraceInformation("{0}", message); // Need to send this through registered event parser; using old logger
foreach (string Line in LogLines.Skip(Action.bShouldOutputStatusDescription ? 0 : 1))
{
// suppress library creation messages when writing compact output
if (bCompactOutput && Line.StartsWith(" Creating library ") && Line.EndsWith(".exp"))
{
continue;
}
Log.TraceInformation("{0}", Line);
// Prevent overwriting of logged lines
s_previousLineLength = -1;
}
if (ExitCode != 0)
{
// BEGIN TEMPORARY TO CATCH PVS-STUDIO ISSUES
if (LogLines.Count == 0)
{
Logger.LogError("{TargetDetails} {Description}: Exited with error code {ExitCode}", TargetDetails, Description, ExitCode);
Logger.LogInformation("{TargetDetails} {Description}: WorkingDirectory {WorkingDirectory}", TargetDetails, Description, Action.WorkingDirectory);
Logger.LogInformation("{TargetDetails} {Description}: {CommandPath} {CommandArguments}", TargetDetails, Description, Action.CommandPath, Action.CommandArguments);
}
// END TEMPORARY
// prevent overrwriting of error text
s_previousLineLength = -1;
// Cancel all other pending tasks
if (bStopCompilationAfterErrors)
{
CancellationTokenSource.Cancel();
}
}
}
}
protected static void TraceSummary(Dictionary<LinkedAction, Task<ExecuteResults>> Tasks, ManagedProcessGroup ProcessGroup, ILogger Logger)
{
if (!bShowCompilationTimes)
{
return;
}
Logger.LogInformation("");
if (ProcessGroup.TotalProcessorTime.Ticks > 0)
{
Logger.LogInformation("Total CPU Time: {TotalSeconds} s", ProcessGroup.TotalProcessorTime.TotalSeconds);
Logger.LogInformation("");
}
var CompletedTasks = Tasks.Where(x => x.Value.Status == TaskStatus.RanToCompletion).OrderByDescending(x => x.Value.Result.ExecutionTime).Take(20);
if (CompletedTasks.Any())
{
Logger.LogInformation("Compilation Time Top {CompletedTaskCount}", CompletedTasks.Count());
Logger.LogInformation("");
foreach (var Pair in CompletedTasks)
{
string Description = $"{(Pair.Key.Inner.CommandDescription ?? Pair.Key.Inner.CommandPath.GetFileNameWithoutExtension())} {Pair.Key.Inner.StatusDescription}".Trim();
if (Pair.Value.Result.ProcessorTime.Ticks > 0)
{
Logger.LogInformation("{Description} [ Wall Time {ExecutionTime:0.00} s / CPU Time {ProcessorTime:0.00} s ]", Description, Pair.Value.Result.ExecutionTime.TotalSeconds, Pair.Value.Result.ProcessorTime.TotalSeconds);
}
else
{
Logger.LogInformation("{Description} [ Time {ExecutionTime:0.00} s ]", Description, Pair.Value.Result.ExecutionTime.TotalSeconds);
}
}
Logger.LogInformation("");
}
}
}
/// <summary>
/// Publicly visible static class that allows external access to the parallel executor config
/// </summary>
public static class ParallelExecutorConfiguration
{
/// <summary>
/// Maximum number of processes that should be used for execution
/// </summary>
public static int GetMaxParallelProcesses(ILogger Logger) => ParallelExecutor.GetDefaultNumParallelProcesses(Logger);
}
}