// Copyright Epic Games, Inc. All Rights Reserved.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using EpicGames.Core;
using Microsoft.Extensions.Logging;
using UnrealBuildBase;
using UnrealBuildTool.Artifacts;
namespace UnrealBuildTool
{
///
/// This executor uses async Tasks to process the action graph
///
class ParallelExecutor : ActionExecutor
{
///
/// Maximum processor count for local execution.
///
[XmlConfigFile]
[Obsolete("ParallelExecutor.MaxProcessorCount is deprecated. Please update xml to use BuildConfiguration.MaxParallelActions")]
#pragma warning disable 0169
private static int MaxProcessorCount;
#pragma warning restore 0169
///
/// 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.
///
[XmlConfigFile]
private static double ProcessorCountMultiplier = 1.0;
///
/// 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.
///
[XmlConfigFile]
private static double MemoryPerActionBytes = 1.5 * 1024 * 1024 * 1024;
///
/// The priority to set for spawned processes.
/// Valid Settings: Idle, BelowNormal, Normal, AboveNormal, High
/// Default: BelowNormal or Normal for an Asymmetrical processor as BelowNormal can cause scheduling issues.
///
[XmlConfigFile]
protected static ProcessPriorityClass ProcessPriority = Utils.IsAsymmetricalProcessor() ? ProcessPriorityClass.Normal : ProcessPriorityClass.BelowNormal;
///
/// When enabled, will stop compiling targets after a compile error occurs.
///
[XmlConfigFile]
private static bool bStopCompilationAfterErrors = false;
///
/// Whether to show compilation times along with worst offenders or not.
///
[XmlConfigFile]
private static bool bShowCompilationTimes = Unreal.IsBuildMachine();
///
/// Whether to show compilation times for each executed action
///
[XmlConfigFile]
private static bool bShowPerActionCompilationTimes = Unreal.IsBuildMachine();
///
/// Whether to log command lines for actions being executed
///
[XmlConfigFile]
private static bool bLogActionCommandLines = false;
///
/// Add target names for each action executed
///
[XmlConfigFile]
private static bool bPrintActionTargetNames = false;
///
/// Whether to take into account the Action's weight when determining to do more work or not.
///
[XmlConfigFile]
protected static bool bUseActionWeights = false;
///
/// Whether to show CPU utilization after the work is complete.
///
[XmlConfigFile]
protected static bool bShowCPUUtilization = Unreal.IsBuildMachine();
///
/// Collapse non-error output lines
///
private bool bCompactOutput = false;
///
/// How many processes that will be executed in parallel
///
public int NumParallelProcesses { get; private set; }
private static readonly char[] LineEndingSplit = new char[] { '\n', '\r' };
public static int GetDefaultNumParallelProcesses(int MaxLocalActions, bool bAllCores, 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(MaxLocalActions, bAllCores ? 1.0f : ProcessorCountMultiplier, bAllCores, Convert.ToInt64(MemoryPerActionBytesComputed));
}
///
/// Constructor
///
/// How many actions to execute in parallel
/// Consider logical cores when determining how many total cpu cores are available
/// Should output be written in a compact fashion
/// Logger for output
public ParallelExecutor(int MaxLocalActions, bool bAllCores, bool bCompactOutput, ILogger Logger)
: base(Logger)
{
XmlConfig.ApplyTo(this);
// Figure out how many processors to use
NumParallelProcesses = GetDefaultNumParallelProcesses(MaxLocalActions, bAllCores, Logger);
this.bCompactOutput = bCompactOutput;
}
///
/// Returns the name of this executor
///
public override string Name => "Parallel";
///
/// Checks whether the task executor can be used
///
/// True if the task executor can be used
public static bool IsAvailable()
{
return true;
}
///
/// Telemetry event for this executor
///
protected TelemetryExecutorEvent? telemetryEvent;
///
public override TelemetryExecutorEvent? GetTelemetryEvent() => telemetryEvent;
///
/// Create an action queue
///
/// Actions to be executed
/// Artifact cache
/// Max artifact tasks that can execute in parallel
/// Logging interface
/// Action queue
public ImmediateActionQueue CreateActionQueue(IEnumerable actionsToExecute, IActionArtifactCache? actionArtifactCache, int maxActionArtifactCacheTasks, ILogger logger)
{
return new(actionsToExecute, actionArtifactCache, maxActionArtifactCacheTasks, "Compiling C++ source code...", x => WriteToolOutput(x), () => FlushToolOutput(), logger)
{
ShowCompilationTimes = bShowCompilationTimes,
ShowCPUUtilization = bShowCPUUtilization,
PrintActionTargetNames = bPrintActionTargetNames,
LogActionCommandLines = bLogActionCommandLines,
ShowPerActionCompilationTimes = bShowPerActionCompilationTimes,
CompactOutput = bCompactOutput,
StopCompilationAfterErrors = bStopCompilationAfterErrors,
};
}
///
public override async Task ExecuteActionsAsync(IEnumerable ActionsToExecute, ILogger Logger, IActionArtifactCache? actionArtifactCache)
{
if (!ActionsToExecute.Any())
{
return true;
}
DateTime startTimeUTC = DateTime.UtcNow;
bool result;
// The "useAutomaticQueue" should always be true unless manual queue is being tested
bool useAutomaticQueue = true;
if (useAutomaticQueue)
{
using ImmediateActionQueue queue = CreateActionQueue(ActionsToExecute, actionArtifactCache, NumParallelProcesses, Logger);
int actionLimit = Math.Min(NumParallelProcesses, queue.TotalActions);
queue.CreateAutomaticRunner(action => RunAction(queue, action), bUseActionWeights, actionLimit, NumParallelProcesses);
queue.Start();
queue.StartManyActions();
result = await queue.RunTillDone();
queue.GetActionResultCounts(out int totalActions, out int succeededActions, out int failedActions, out int cacheHitActions, out int cacheMissActions);
telemetryEvent = new TelemetryExecutorEvent(Name, startTimeUTC, result, totalActions, succeededActions, failedActions, cacheHitActions, cacheMissActions, DateTime.UtcNow);
}
else
{
using ImmediateActionQueue queue = CreateActionQueue(ActionsToExecute, actionArtifactCache, NumParallelProcesses, Logger);
int actionLimit = Math.Min(NumParallelProcesses, queue.TotalActions);
ImmediateActionQueueRunner runner = queue.CreateManualRunner(action => RunAction(queue, action), bUseActionWeights, actionLimit, actionLimit);
queue.Start();
using Timer timer = new((_) => queue.StartManyActions(runner), null, 0, 500);
queue.StartManyActions();
result = await queue.RunTillDone();
queue.GetActionResultCounts(out int totalActions, out int succeededActions, out int failedActions, out int cacheHitActions, out int cacheMissActions);
telemetryEvent = new TelemetryExecutorEvent(Name, startTimeUTC, result, totalActions, succeededActions, failedActions, cacheHitActions, cacheMissActions, DateTime.UtcNow);
}
return result;
}
private static Func? RunAction(ImmediateActionQueue queue, LinkedAction action)
{
return async () =>
{
ExecuteResults results = await RunActionAsync(action, queue.ProcessGroup, queue.CancellationToken);
queue.OnActionCompleted(action, results.ExitCode == 0, results);
};
}
protected static async Task RunActionAsync(LinkedAction Action, ManagedProcessGroup ProcessGroup, CancellationToken CancellationToken, string? AdditionalDescription = null)
{
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();
await Process.WaitForExitAsync(CancellationToken);
List 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;
if (ExitCode == 0 && Action.bForceWarningsAsError && LogLines.Any(x => x.Contains("): warning: ", StringComparison.OrdinalIgnoreCase)))
{
ExitCode = 1;
LogLines = LogLines.Select(x => x.Replace("): warning: ", "): error: ", StringComparison.OrdinalIgnoreCase)).ToList();
}
return new ExecuteResults(LogLines, ExitCode, ExecutionTime, ProcessorTime, AdditionalDescription);
}
}
///
/// Publicly visible static class that allows external access to the parallel executor config
///
public static class ParallelExecutorConfiguration
{
///
/// Maximum number of processes that should be used for execution
///
public static int GetMaxParallelProcesses(ILogger Logger) => ParallelExecutor.GetDefaultNumParallelProcesses(0, false, Logger);
///
/// Maximum number of processes that should be used for execution
///
public static int GetMaxParallelProcesses(int MaxLocalActions, bool bAllCores, ILogger Logger) => ParallelExecutor.GetDefaultNumParallelProcesses(MaxLocalActions, bAllCores, Logger);
}
}