2019-12-26 23:01:54 -05:00
// Copyright Epic Games, Inc. All Rights Reserved.
2018-05-29 14:30:28 -04:00
2021-06-11 17:19:29 -04:00
using EpicGames.Core ;
2018-05-23 21:04:31 -04:00
using System ;
using System.Collections.Generic ;
using System.Diagnostics ;
2022-01-18 15:19:06 -05:00
using System.IO ;
2018-05-23 21:04:31 -04:00
using System.Linq ;
2022-02-16 15:16:28 -05:00
using System.Text.Json ;
2018-05-23 21:04:31 -04:00
using System.Threading ;
2022-01-18 15:19:06 -05:00
using System.Threading.Tasks ;
2018-05-23 21:04:31 -04:00
namespace UnrealBuildTool
{
/// <summary>
2022-01-18 15:19:06 -05:00
/// This executor uses async Tasks to process the action graph
2018-05-23 21:04:31 -04:00
/// </summary>
class ParallelExecutor : ActionExecutor
{
/// <summary>
/// Maximum processor count for local execution.
/// </summary>
2018-08-29 16:26:08 -04:00
[XmlConfigFile]
2022-01-18 15:19:06 -05:00
private static int MaxProcessorCount = int . MaxValue ;
2018-05-23 21:04:31 -04:00
2020-03-02 15:50:41 -05:00
/// <summary>
/// Processor count multiplier for local execution. Can be below 1 to reserve CPU for other tasks.
2021-04-13 19:04:18 -04:00
/// 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.
2020-03-02 15:50:41 -05:00
/// </summary>
[XmlConfigFile]
2022-01-18 15:19:06 -05:00
private static double ProcessorCountMultiplier = 1.0 ;
2020-03-02 15:50:41 -05:00
2021-04-13 19:04:18 -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]
2022-01-18 15:19:06 -05:00
private static double MemoryPerActionBytes = 1.5 * 1024 * 1024 * 1024 ;
2021-04-13 19:04:18 -04:00
2018-05-23 21:04:31 -04:00
/// <summary>
/// When enabled, will stop compiling targets after a compile error occurs.
/// </summary>
2018-08-29 16:26:08 -04:00
[XmlConfigFile]
2022-01-18 15:19:06 -05:00
private static bool bStopCompilationAfterErrors = false ;
2018-05-23 21:04:31 -04:00
2021-07-13 06:55:03 -04:00
/// <summary>
/// Whether to show compilation times along with worst offenders or not.
/// </summary>
[XmlConfigFile]
2022-01-18 15:19:06 -05:00
private static bool bShowCompilationTimes = false ;
2021-07-13 06:55:03 -04:00
2021-09-27 16:48:30 -04:00
/// <summary>
/// Whether to show compilation times for each executed action
/// </summary>
[XmlConfigFile]
2022-01-18 15:19:06 -05:00
private static bool bShowPerActionCompilationTimes = false ;
2021-09-27 16:48:30 -04:00
2021-11-18 14:37:34 -05:00
/// <summary>
/// Whether to log command lines for actions being executed
/// </summary>
[XmlConfigFile]
2022-01-18 15:19:06 -05:00
private static bool bLogActionCommandLines = false ;
2021-11-18 14:37:34 -05:00
/// <summary>
/// Add target names for each action executed
/// </summary>
[XmlConfigFile]
2022-01-18 15:19:06 -05:00
private static bool bPrintActionTargetNames = false ;
2021-11-18 14:37:34 -05:00
2020-03-02 15:50:41 -05:00
/// <summary>
/// How many processes that will be executed in parallel
/// </summary>
2021-04-29 19:32:06 -04:00
public int NumParallelProcesses { get ; private set ; }
2022-01-18 15:19:06 -05:00
private static readonly char [ ] LineEndingSplit = new char [ ] { '\n' , '\r' } ;
2021-04-29 19:32:06 -04:00
public static int GetDefaultNumParallelProcesses ( )
{
2021-09-16 19:35:07 -04:00
double MemoryPerActionBytesComputed = Math . Max ( MemoryPerActionBytes , MemoryPerActionBytesOverride ) ;
if ( MemoryPerActionBytesComputed > MemoryPerActionBytes )
{
Log . TraceInformation ( $"Overriding MemoryPerAction with target-defined value of {MemoryPerActionBytesComputed / 1024 / 1024 / 1024} bytes" ) ;
}
return Utils . GetMaxActionsToExecuteInParallel ( MaxProcessorCount , ProcessorCountMultiplier , Convert . ToInt64 ( MemoryPerActionBytesComputed ) ) ;
2021-04-29 19:32:06 -04:00
}
2020-03-02 15:50:41 -05:00
2018-05-23 21:04:31 -04:00
/// <summary>
/// Constructor
/// </summary>
2020-03-02 15:50:41 -05:00
/// <param name="MaxLocalActions">How many actions to execute in parallel</param>
public ParallelExecutor ( int MaxLocalActions )
2018-05-23 21:04:31 -04:00
{
XmlConfig . ApplyTo ( this ) ;
2020-03-02 15:50:41 -05:00
// if specified this caps how many processors we can use
if ( MaxLocalActions > 0 )
{
2020-08-11 01:36:57 -04:00
NumParallelProcesses = MaxLocalActions ;
2020-03-02 15:50:41 -05:00
}
else
{
// Figure out how many processors to use
2021-04-29 19:32:06 -04:00
NumParallelProcesses = GetDefaultNumParallelProcesses ( ) ;
2020-03-02 15:50:41 -05:00
}
2018-05-23 21:04:31 -04:00
}
/// <summary>
/// Returns the name of this executor
/// </summary>
public override string Name
{
get { return "Parallel" ; }
}
/// <summary>
2022-01-18 15:19:06 -05:00
/// Checks whether the task executor can be used
2018-05-23 21:04:31 -04:00
/// </summary>
2022-01-18 15:19:06 -05:00
/// <returns>True if the task executor can be used</returns>
2018-05-23 21:04:31 -04:00
public static bool IsAvailable ( )
{
2021-06-11 17:19:29 -04:00
return true ;
2018-05-23 21:04:31 -04:00
}
2022-01-18 15:19:06 -05:00
private 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 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 ;
}
}
2021-11-18 14:37:34 -05:00
2018-05-23 21:04:31 -04:00
/// <summary>
/// Executes the specified actions locally.
/// </summary>
/// <returns>True if all the tasks successfully executed, or false if any of them failed.</returns>
2020-11-25 10:13:28 -04:00
public override bool ExecuteActions ( List < LinkedAction > InputActions )
2018-05-23 21:04:31 -04:00
{
2022-01-18 15:19:06 -05:00
int NumCompletedActions = 0 ;
int TotalActions = InputActions . Count ;
int ActualNumParallelProcesses = Math . Min ( TotalActions , NumParallelProcesses ) ;
2018-05-23 21:04:31 -04:00
2022-01-18 15:19:06 -05:00
using ManagedProcessGroup ProcessGroup = new ManagedProcessGroup ( ) ;
using SemaphoreSlim MaxProcessSemaphore = new SemaphoreSlim ( ActualNumParallelProcesses , ActualNumParallelProcesses ) ;
using ProgressWriter ProgressWriter = new ProgressWriter ( "Compiling C++ source code..." , false ) ;
2021-11-18 14:37:34 -05:00
2022-02-16 15:16:28 -05:00
Log . TraceInformation ( $"Building {TotalActions} {(TotalActions == 1 ? " action " : " actions ")} with {ActualNumParallelProcesses} {(ActualNumParallelProcesses == 1 ? " process " : " processes ")}..." ) ;
2022-01-18 15:19:06 -05:00
Dictionary < LinkedAction , Task < ExecuteResults > > Tasks = new Dictionary < LinkedAction , Task < ExecuteResults > > ( ) ;
List < Task > AllTasks = 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 )
2018-05-23 21:04:31 -04:00
{
2022-01-18 15:19:06 -05:00
Task < ExecuteResults > ExecuteTask = ExecuteAction ( Action , Tasks , ProcessGroup , MaxProcessSemaphore , CancellationToken ) ;
Task LogTask = ExecuteTask . ContinueWith ( antecedent = > LogCompletedAction ( Action , antecedent , CancellationTokenSource , ProgressWriter , TotalActions , ref NumCompletedActions ) , CancellationToken ) ;
2020-04-30 16:12:03 -04:00
2022-01-18 15:19:06 -05:00
Tasks . Add ( Action , ExecuteTask ) ;
AllTasks . Add ( ExecuteTask ) ;
AllTasks . Add ( LogTask ) ;
2018-05-23 21:04:31 -04:00
}
2022-01-18 15:19:06 -05:00
// Wait for all tasks to complete
Task . WaitAll ( AllTasks . ToArray ( ) , CancellationToken ) ;
if ( bShowCompilationTimes )
2018-05-23 21:04:31 -04:00
{
2022-01-18 15:19:06 -05:00
Log . TraceInformation ( "" ) ;
if ( ProcessGroup . TotalProcessorTime . Ticks > 0 )
2021-07-13 06:55:03 -04:00
{
2022-02-16 15:16:28 -05:00
Log . TraceInformation ( $"Total CPU Time: {ProcessGroup.TotalProcessorTime.TotalSeconds} s" ) ;
2021-07-13 06:55:03 -04:00
Log . TraceInformation ( "" ) ;
2018-05-23 21:04:31 -04:00
}
2022-01-18 15:19:06 -05:00
if ( Tasks . Count > 0 )
{
2022-02-16 15:16:28 -05:00
Log . TraceInformation ( $"Compilation Time Top {Math.Min(20, Tasks.Count)}" ) ;
2022-01-18 15:19:06 -05:00
Log . TraceInformation ( "" ) ;
foreach ( var Pair in Tasks . OrderByDescending ( x = > x . Value . Result . ExecutionTime ) . Take ( 20 ) )
{
string Description = $"{(Pair.Key.Inner.CommandDescription ?? Pair.Key.Inner.CommandPath.GetFileNameWithoutExtension())} {Pair.Key.Inner.StatusDescription}" . Trim ( ) ;
if ( Pair . Value . Result . ProcessorTime . Ticks > 0 )
{
2022-02-16 15:16:28 -05:00
Log . TraceInformation ( $"{Description} [ Wall Time {Pair.Value.Result.ExecutionTime.TotalSeconds:0.00} s / CPU Time {Pair.Value.Result.ProcessorTime.TotalSeconds:0.00} s ]" ) ;
2022-01-18 15:19:06 -05:00
}
else
{
2022-02-16 15:16:28 -05:00
Log . TraceInformation ( $"{Description} [ Time {Pair.Value.Result.ExecutionTime.TotalSeconds:0.00} s ]" ) ;
2022-01-18 15:19:06 -05:00
}
}
Log . TraceInformation ( "" ) ;
}
2018-05-23 21:04:31 -04:00
}
2022-01-18 15:19:06 -05:00
// Return if all tasks succeeded
return Tasks . Values . All ( x = > x . Result . ExitCode = = 0 ) ;
2018-05-23 21:04:31 -04:00
}
2022-01-18 15:19:06 -05:00
private async Task < ExecuteResults > ExecuteAction ( LinkedAction Action , Dictionary < LinkedAction , Task < ExecuteResults > > Tasks , ManagedProcessGroup ProcessGroup , SemaphoreSlim MaxProcessSemaphore , CancellationToken CancellationToken )
2018-05-23 21:04:31 -04:00
{
2022-01-18 15:19:06 -05:00
Task ? SemaphoreTask = null ;
2019-08-01 13:07:33 -04:00
try
2018-05-23 21:04:31 -04:00
{
2022-01-18 15:19:06 -05:00
// Wait for Tasks list to be populated with any PrerequisiteActions
while ( ! Action . PrerequisiteActions . All ( x = > Tasks . ContainsKey ( x ) ) )
2019-08-01 13:07:33 -04:00
{
2022-01-18 15:19:06 -05:00
await Task . Delay ( 100 , CancellationToken ) ;
CancellationToken . ThrowIfCancellationRequested ( ) ;
2019-08-01 13:07:33 -04:00
}
2022-01-18 15:19:06 -05:00
// Wait for all PrerequisiteActions to complete
ExecuteResults [ ] Results = await Task . WhenAll ( Action . PrerequisiteActions . Select ( x = > Tasks [ x ] ) . ToArray ( ) ) ;
2022-02-16 15:16:28 -05:00
// Cancel tasks if any PrerequisiteActions fail, unless a PostBuildStep
if ( Action . ActionType ! = ActionType . PostBuildStep & & Results . Any ( x = > x . ExitCode ! = 0 ) )
2022-01-18 15:19:06 -05:00
{
throw new OperationCanceledException ( ) ;
}
CancellationToken . ThrowIfCancellationRequested ( ) ;
// Limit the number of concurrent processes that will run in parallel
SemaphoreTask = MaxProcessSemaphore . WaitAsync ( CancellationToken ) ;
await SemaphoreTask ;
CancellationToken . ThrowIfCancellationRequested ( ) ;
using ManagedProcess Process = new ManagedProcess ( ProcessGroup , Action . CommandPath . FullName , Action . CommandArguments , Action . WorkingDirectory . FullName , null , null , ProcessPriorityClass . BelowNormal ) ;
MemoryStream StdOutStream = new MemoryStream ( ) ;
await Process . CopyToAsync ( StdOutStream , CancellationToken ) ;
CancellationToken . ThrowIfCancellationRequested ( ) ;
Process . WaitForExit ( ) ;
CancellationToken . ThrowIfCancellationRequested ( ) ;
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 ) ;
}
catch ( OperationCanceledException )
{
return new ExecuteResults ( new List < string > ( ) , int . MaxValue ) ;
2019-08-01 13:07:33 -04:00
}
2021-06-11 17:19:29 -04:00
catch ( Exception Ex )
2019-08-01 13:07:33 -04:00
{
Log . WriteException ( Ex , null ) ;
2022-01-18 15:19:06 -05:00
return new ExecuteResults ( new List < string > ( ) , int . MaxValue ) ;
2018-05-23 21:04:31 -04:00
}
2022-01-18 15:19:06 -05:00
finally
2021-07-28 16:45:31 -04:00
{
2022-01-18 15:19:06 -05:00
if ( SemaphoreTask ! = null & & SemaphoreTask . Status = = TaskStatus . RanToCompletion )
{
MaxProcessSemaphore . Release ( ) ;
}
2021-07-28 16:45:31 -04:00
}
2018-05-23 21:04:31 -04:00
}
2022-01-18 15:19:06 -05:00
private void LogCompletedAction ( LinkedAction Action , Task < ExecuteResults > ExecuteTask , CancellationTokenSource CancellationTokenSource , ProgressWriter ProgressWriter , int TotalActions , ref int NumCompletedActions )
2018-05-23 21:04:31 -04:00
{
2022-01-18 15:19:06 -05:00
List < string > LogLines = new List < string > ( ) ;
int ExitCode = int . MaxValue ;
TimeSpan ExecutionTime = TimeSpan . Zero ;
TimeSpan ProcessorTime = TimeSpan . Zero ;
if ( ExecuteTask . Status = = TaskStatus . RanToCompletion )
2018-05-23 21:04:31 -04:00
{
2022-01-18 15:19:06 -05:00
ExecuteResults ExecuteTaskResult = ExecuteTask . Result ;
LogLines = ExecuteTaskResult . LogLines ;
ExitCode = ExecuteTaskResult . ExitCode ;
ExecutionTime = ExecuteTaskResult . ExecutionTime ;
ProcessorTime = ExecuteTaskResult . ProcessorTime ;
}
// 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 ( ) ;
}
lock ( ProgressWriter )
{
int CompletedActions ;
CompletedActions = Interlocked . Increment ( ref NumCompletedActions ) ;
ProgressWriter . Write ( CompletedActions , TotalActions ) ;
// Cancelled
if ( ExitCode = = int . MaxValue )
2018-05-23 21:04:31 -04:00
{
2022-02-16 15:16:28 -05:00
Log . TraceInformation ( $"[{CompletedActions}/{TotalActions}] {Description} cancelled" ) ;
2022-01-18 15:19:06 -05:00
return ;
}
string TargetDetails = "" ;
TargetDescriptor ? Target = Action . Target ;
if ( bPrintActionTargetNames & & Target ! = null )
{
TargetDetails = $"[{Target.Name} {Target.Platform} {Target.Configuration}]" ;
}
if ( bLogActionCommandLines )
{
Log . TraceLog ( $"[{CompletedActions}/{TotalActions}]{TargetDetails} Command: {Action.CommandPath} {Action.CommandArguments}" ) ;
}
string CompilationTimes = "" ;
if ( bShowPerActionCompilationTimes )
{
2022-02-16 15:16:28 -05:00
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)" ;
}
2022-01-18 15:19:06 -05:00
}
2022-02-16 15:16:28 -05:00
Log . TraceInformation ( $"[{CompletedActions}/{TotalActions}]{TargetDetails}{CompilationTimes} {Description}" ) ;
2022-01-18 15:19:06 -05:00
foreach ( string Line in LogLines . Skip ( Action . bShouldOutputStatusDescription ? 0 : 1 ) )
{
Log . TraceInformation ( Line ) ;
}
if ( ExitCode ! = 0 )
{
// BEGIN TEMPORARY TO CATCH PVS-STUDIO ISSUES
if ( LogLines . Count = = 0 )
{
2022-02-16 15:16:28 -05:00
Log . TraceError ( $"{TargetDetails} {Description}: Exited with error code {ExitCode}" ) ;
Log . TraceInformation ( $"{TargetDetails} {Description}: WorkingDirectory {Action.WorkingDirectory}" ) ;
Log . TraceInformation ( $"{TargetDetails} {Description}: {Action.CommandPath} {Action.CommandArguments}" ) ;
2022-01-18 15:19:06 -05:00
}
// END TEMPORARY
2022-02-16 15:16:28 -05:00
// Cancel all other pending tasks
if ( bStopCompilationAfterErrors )
2022-01-18 15:19:06 -05:00
{
CancellationTokenSource . Cancel ( ) ;
}
2018-05-23 21:04:31 -04:00
}
}
}
}
2021-04-29 19:32:06 -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>
public static int MaxParallelProcesses { get { return ParallelExecutor . GetDefaultNumParallelProcesses ( ) ; } }
}
2018-05-23 21:04:31 -04:00
}