278 lines
11 KiB
C#
278 lines
11 KiB
C#
// 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
|
|
{
|
|
|
|
/// <summary>
|
|
/// This executor uses async Tasks to process the action graph
|
|
/// </summary>
|
|
class ParallelExecutor : ActionExecutor
|
|
{
|
|
/// <summary>
|
|
/// Maximum processor count for local execution.
|
|
/// </summary>
|
|
[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
|
|
|
|
/// <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 or Normal for an Asymmetrical processor as BelowNormal can cause scheduling issues.
|
|
/// </summary>
|
|
[XmlConfigFile]
|
|
protected static ProcessPriorityClass ProcessPriority = Utils.IsAsymmetricalProcessor() ? ProcessPriorityClass.Normal : 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 = Unreal.IsBuildMachine();
|
|
|
|
/// <summary>
|
|
/// Whether to show compilation times for each executed action
|
|
/// </summary>
|
|
[XmlConfigFile]
|
|
private static bool bShowPerActionCompilationTimes = Unreal.IsBuildMachine();
|
|
|
|
/// <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>
|
|
/// Whether to take into account the Action's weight when determining to do more work or not.
|
|
/// </summary>
|
|
[XmlConfigFile]
|
|
protected static bool bUseActionWeights = false;
|
|
|
|
/// <summary>
|
|
/// Whether to show CPU utilization after the work is complete.
|
|
/// </summary>
|
|
[XmlConfigFile]
|
|
protected static bool bShowCPUUtilization = Unreal.IsBuildMachine();
|
|
|
|
/// <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(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));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Constructor
|
|
/// </summary>
|
|
/// <param name="MaxLocalActions">How many actions to execute in parallel</param>
|
|
/// <param name="bAllCores">Consider logical cores when determining how many total cpu cores are available</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 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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the name of this executor
|
|
/// </summary>
|
|
public override string Name => "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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Telemetry event for this executor
|
|
/// </summary>
|
|
protected TelemetryExecutorEvent? telemetryEvent;
|
|
|
|
/// <inheritdoc/>
|
|
public override TelemetryExecutorEvent? GetTelemetryEvent() => telemetryEvent;
|
|
|
|
/// <summary>
|
|
/// Create an action queue
|
|
/// </summary>
|
|
/// <param name="actionsToExecute">Actions to be executed</param>
|
|
/// <param name="actionArtifactCache">Artifact cache</param>
|
|
/// <param name="maxActionArtifactCacheTasks">Max artifact tasks that can execute in parallel</param>
|
|
/// <param name="logger">Logging interface</param>
|
|
/// <returns>Action queue</returns>
|
|
public ImmediateActionQueue CreateActionQueue(IEnumerable<LinkedAction> 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,
|
|
};
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public override async Task<bool> ExecuteActionsAsync(IEnumerable<LinkedAction> 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<Task>? 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<ExecuteResults> 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<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;
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
/// <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(0, false, Logger);
|
|
|
|
/// <summary>
|
|
/// Maximum number of processes that should be used for execution
|
|
/// </summary>
|
|
public static int GetMaxParallelProcesses(int MaxLocalActions, bool bAllCores, ILogger Logger) => ParallelExecutor.GetDefaultNumParallelProcesses(MaxLocalActions, bAllCores, Logger);
|
|
}
|
|
}
|