// 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); } }