2022-05-31 15:44:53 -04:00
// 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 ;
2023-05-30 18:38:07 -04:00
using EpicGames.Core ;
using Microsoft.Extensions.Logging ;
2023-08-10 15:38:17 -04:00
using UnrealBuildBase ;
2023-05-05 11:19:57 -04:00
using UnrealBuildTool.Artifacts ;
2022-05-31 15:44:53 -04:00
namespace UnrealBuildTool
{
2023-05-05 11:19:57 -04:00
2022-05-31 15:44:53 -04:00
/// <summary>
/// This executor uses async Tasks to process the action graph
/// </summary>
class ParallelExecutor : ActionExecutor
{
2023-05-30 19:55:37 -04:00
/// <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
2022-05-31 15:44:53 -04:00
/// <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]
2023-05-30 19:55:37 -04:00
private static double ProcessorCountMultiplier = 1.0 ;
2022-05-31 15:44:53 -04:00
/// <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]
2023-05-30 19:55:37 -04:00
private static double MemoryPerActionBytes = 1.5 * 1024 * 1024 * 1024 ;
2022-05-31 15:44:53 -04:00
/// <summary>
/// The priority to set for spawned processes.
/// Valid Settings: Idle, BelowNormal, Normal, AboveNormal, High
2023-01-06 19:29:19 -05:00
/// Default: BelowNormal or Normal for an Asymmetrical processor as BelowNormal can cause scheduling issues.
2022-05-31 15:44:53 -04:00
/// </summary>
[XmlConfigFile]
2023-05-30 19:55:37 -04:00
protected static ProcessPriorityClass ProcessPriority = Utils . IsAsymmetricalProcessor ( ) ? ProcessPriorityClass . Normal : ProcessPriorityClass . BelowNormal ;
2022-05-31 15:44:53 -04:00
/// <summary>
/// When enabled, will stop compiling targets after a compile error occurs.
/// </summary>
[XmlConfigFile]
2023-05-30 19:55:37 -04:00
private static bool bStopCompilationAfterErrors = false ;
2022-05-31 15:44:53 -04:00
/// <summary>
/// Whether to show compilation times along with worst offenders or not.
/// </summary>
[XmlConfigFile]
2023-08-10 15:38:17 -04:00
private static bool bShowCompilationTimes = Unreal . IsBuildMachine ( ) ;
2022-05-31 15:44:53 -04:00
/// <summary>
/// Whether to show compilation times for each executed action
/// </summary>
[XmlConfigFile]
2023-08-10 15:38:17 -04:00
private static bool bShowPerActionCompilationTimes = Unreal . IsBuildMachine ( ) ;
2022-05-31 15:44:53 -04:00
/// <summary>
/// Whether to log command lines for actions being executed
/// </summary>
[XmlConfigFile]
2023-05-30 19:55:37 -04:00
private static bool bLogActionCommandLines = false ;
2022-05-31 15:44:53 -04:00
/// <summary>
/// Add target names for each action executed
/// </summary>
[XmlConfigFile]
2023-05-30 19:55:37 -04:00
private static bool bPrintActionTargetNames = false ;
2022-05-31 15:44:53 -04:00
2023-03-08 17:08:22 -05:00
/// <summary>
/// Whether to take into account the Action's weight when determining to do more work or not.
/// </summary>
[XmlConfigFile]
2023-05-30 19:55:37 -04:00
protected static bool bUseActionWeights = false ;
2023-03-08 17:08:22 -05:00
/// <summary>
/// Whether to show CPU utilization after the work is complete.
/// </summary>
[XmlConfigFile]
2023-08-10 15:38:17 -04:00
protected static bool bShowCPUUtilization = Unreal . IsBuildMachine ( ) ;
2023-03-08 17:08:22 -05:00
2022-05-31 15:44:53 -04:00
/// <summary>
/// Collapse non-error output lines
/// </summary>
2023-05-30 19:55:37 -04:00
private bool bCompactOutput = false ;
2022-05-31 15:44:53 -04:00
/// <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' } ;
2022-09-09 19:16:18 -04:00
public static int GetDefaultNumParallelProcesses ( int MaxLocalActions , bool bAllCores , ILogger Logger )
2022-05-31 15:44:53 -04:00
{
double MemoryPerActionBytesComputed = Math . Max ( MemoryPerActionBytes , MemoryPerActionBytesOverride ) ;
if ( MemoryPerActionBytesComputed > MemoryPerActionBytes )
{
Logger . LogInformation ( "Overriding MemoryPerAction with target-defined value of {Memory} bytes" , MemoryPerActionBytesComputed / 1024 / 1024 / 1024 ) ;
}
2023-03-10 19:27:40 -05:00
return Utils . GetMaxActionsToExecuteInParallel ( MaxLocalActions , bAllCores ? 1.0f : ProcessorCountMultiplier , bAllCores , Convert . ToInt64 ( MemoryPerActionBytesComputed ) ) ;
2022-05-31 15:44:53 -04:00
}
/// <summary>
/// Constructor
/// </summary>
/// <param name="MaxLocalActions">How many actions to execute in parallel</param>
2022-09-09 19:16:18 -04:00
/// <param name="bAllCores">Consider logical cores when determining how many total cpu cores are available</param>
2022-05-31 15:44:53 -04:00
/// <param name="bCompactOutput">Should output be written in a compact fashion</param>
/// <param name="Logger">Logger for output</param>
2022-09-09 19:16:18 -04:00
public ParallelExecutor ( int MaxLocalActions , bool bAllCores , bool bCompactOutput , ILogger Logger )
2023-03-09 14:51:40 -05:00
: base ( Logger )
2022-05-31 15:44:53 -04:00
{
XmlConfig . ApplyTo ( this ) ;
2022-09-09 19:16:18 -04:00
// Figure out how many processors to use
NumParallelProcesses = GetDefaultNumParallelProcesses ( MaxLocalActions , bAllCores , Logger ) ;
2022-05-31 15:44:53 -04:00
this . bCompactOutput = bCompactOutput ;
}
/// <summary>
/// Returns the name of this executor
/// </summary>
2023-05-30 18:01:50 -04:00
public override string Name = > "Parallel" ;
2022-05-31 15:44:53 -04:00
/// <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 ;
}
2024-03-27 14:05:42 -04:00
/// <summary>
/// Telemetry event for this executor
/// </summary>
protected TelemetryExecutorEvent ? telemetryEvent ;
/// <inheritdoc/>
public override TelemetryExecutorEvent ? GetTelemetryEvent ( ) = > telemetryEvent ;
2022-05-31 15:44:53 -04:00
/// <summary>
2023-05-05 11:19:57 -04:00
/// Create an action queue
2022-05-31 15:44:53 -04:00
/// </summary>
2023-05-05 11:19:57 -04:00
/// <param name="actionsToExecute">Actions to be executed</param>
/// <param name="actionArtifactCache">Artifact cache</param>
2024-04-24 16:28:17 -04:00
/// <param name="maxActionArtifactCacheTasks">Max artifact tasks that can execute in parallel</param>
2023-05-05 11:19:57 -04:00
/// <param name="logger">Logging interface</param>
/// <returns>Action queue</returns>
2024-04-24 16:28:17 -04:00
public ImmediateActionQueue CreateActionQueue ( IEnumerable < LinkedAction > actionsToExecute , IActionArtifactCache ? actionArtifactCache , int maxActionArtifactCacheTasks , ILogger logger )
2022-05-31 15:44:53 -04:00
{
2024-04-24 16:28:17 -04:00
return new ( actionsToExecute , actionArtifactCache , maxActionArtifactCacheTasks , "Compiling C++ source code..." , x = > WriteToolOutput ( x ) , ( ) = > FlushToolOutput ( ) , logger )
2023-05-05 11:19:57 -04:00
{
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 ( ) )
2022-10-28 13:27:22 -04:00
{
return true ;
}
2024-03-27 14:05:42 -04:00
DateTime startTimeUTC = DateTime . UtcNow ;
bool result ;
2023-05-05 11:19:57 -04:00
// The "useAutomaticQueue" should always be true unless manual queue is being tested
bool useAutomaticQueue = true ;
if ( useAutomaticQueue )
2022-05-31 15:44:53 -04:00
{
2024-04-24 16:28:17 -04:00
using ImmediateActionQueue queue = CreateActionQueue ( ActionsToExecute , actionArtifactCache , NumParallelProcesses , Logger ) ;
2023-05-05 11:19:57 -04:00
int actionLimit = Math . Min ( NumParallelProcesses , queue . TotalActions ) ;
2023-07-09 18:50:33 -04:00
queue . CreateAutomaticRunner ( action = > RunAction ( queue , action ) , bUseActionWeights , actionLimit , NumParallelProcesses ) ;
2023-05-05 11:19:57 -04:00
queue . Start ( ) ;
queue . StartManyActions ( ) ;
2024-03-27 14:05:42 -04:00
result = await queue . RunTillDone ( ) ;
2024-05-08 15:17:35 -04:00
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 ) ;
2023-01-31 01:16:30 -05:00
}
2023-05-05 11:19:57 -04:00
else
2023-01-31 01:16:30 -05:00
{
2024-04-24 16:28:17 -04:00
using ImmediateActionQueue queue = CreateActionQueue ( ActionsToExecute , actionArtifactCache , NumParallelProcesses , Logger ) ;
2023-05-05 11:19:57 -04:00
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 ( ) ;
2024-03-27 14:05:42 -04:00
result = await queue . RunTillDone ( ) ;
2024-05-08 15:17:35 -04:00
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 ) ;
2022-05-31 15:44:53 -04:00
}
2024-03-27 14:05:42 -04:00
return result ;
2023-05-05 11:19:57 -04:00
}
2023-01-31 01:16:30 -05:00
2023-05-10 08:40:51 -04:00
private static Func < Task > ? RunAction ( ImmediateActionQueue queue , LinkedAction action )
2023-05-05 11:19:57 -04:00
{
return async ( ) = >
{
2023-05-10 08:40:51 -04:00
ExecuteResults results = await RunAction ( action , queue . ProcessGroup , queue . CancellationToken ) ;
queue . OnActionCompleted ( action , results . ExitCode = = 0 , results ) ;
2023-05-05 11:19:57 -04:00
} ;
2022-05-31 15:44:53 -04:00
}
2023-06-13 20:26:15 -04:00
protected static async Task < ExecuteResults > RunAction ( LinkedAction Action , ManagedProcessGroup ProcessGroup , CancellationToken CancellationToken , string? AdditionalDescription = null )
2022-05-31 15:44:53 -04:00
{
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 ( ) ;
2023-12-01 18:29:06 -05:00
await Process . WaitForExitAsync ( CancellationToken ) ;
2022-05-31 15:44:53 -04:00
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 ;
2023-06-13 20:26:15 -04:00
return new ExecuteResults ( LogLines , ExitCode , ExecutionTime , ProcessorTime , AdditionalDescription ) ;
2022-05-31 15:44:53 -04:00
}
}
/// <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>
2022-09-09 19:16:18 -04:00
public static int GetMaxParallelProcesses ( ILogger Logger ) = > ParallelExecutor . GetDefaultNumParallelProcesses ( 0 , false , Logger ) ;
2022-09-30 11:12:48 -04:00
/// <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 ) ;
2022-05-31 15:44:53 -04:00
}
}