// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.Text; using System.Text.RegularExpressions; using System.Diagnostics; using System.IO; using System.Reflection; using System.Threading; using System.Linq; using EpicGames.Core; namespace UnrealBuildTool { class ActionThread { /// /// Cache the exit code from the command so that the executor can report errors /// public int ExitCode = 0; /// /// Set to true only when the local or RPC action is complete /// public bool bComplete = false; /// /// Cache the action that this thread is managing /// LinkedAction Action; /// /// For reporting status to the user /// int JobNumber; int TotalJobs; /// /// Constructor, takes the action to process /// public ActionThread(LinkedAction InAction, int InJobNumber, int InTotalJobs) { Action = InAction; JobNumber = InJobNumber; TotalJobs = InTotalJobs; } /// /// Used when debuging Actions outputs all action return values to debug out /// /// Sending object /// Event arguments (In this case, the line of string output) protected void ActionDebugOutput(object sender, DataReceivedEventArgs e) { string Output = e.Data; if (Output == null) { return; } Log.TraceInformation(Output); } /// /// The actual function to run in a thread. This is potentially long and blocking /// private void ThreadFunc() { string? Args = Action.CommandArguments; // TODO: Process Arguments follow windows conventions in .NET Core // Which means single quotes ' are not considered quotes. // see https://github.com/dotnet/runtime/issues/29857 // also see UE-102580 // for rules see https://docs.microsoft.com/en-us/cpp/cpp/main-function-command-line-args Args = Args?.Replace('\'', '\"'); // Create the action's process. ProcessStartInfo ActionStartInfo = new ProcessStartInfo(); ActionStartInfo.WorkingDirectory = Action.WorkingDirectory.FullName; ActionStartInfo.FileName = Action.CommandPath.FullName; ActionStartInfo.Arguments = Args; ActionStartInfo.UseShellExecute = false; ActionStartInfo.RedirectStandardInput = false; ActionStartInfo.RedirectStandardOutput = false; ActionStartInfo.RedirectStandardError = false; // Log command-line used to execute task if debug info printing is enabled. Log.TraceVerbose("Executing: {0} {1}", ActionStartInfo.FileName, ActionStartInfo.Arguments); // Log summary if wanted. if (Action.bShouldOutputStatusDescription) { string CommandDescription = Action.CommandDescription != null ? Action.CommandDescription : Path.GetFileName(ActionStartInfo.FileName); if (string.IsNullOrEmpty(CommandDescription)) { Log.TraceInformation(Action.StatusDescription); } else { Log.TraceInformation("[{0}/{1}] {2} {3}", JobNumber, TotalJobs, CommandDescription, Action.StatusDescription); } } // Try to launch the action's process, and produce a friendly error message if it fails. Process? ActionProcess = null; try { try { ActionProcess = new Process(); ActionProcess.StartInfo = ActionStartInfo; ActionStartInfo.RedirectStandardOutput = true; ActionStartInfo.RedirectStandardError = true; ActionProcess.OutputDataReceived += new DataReceivedEventHandler(ActionDebugOutput); ActionProcess.ErrorDataReceived += new DataReceivedEventHandler(ActionDebugOutput); ActionProcess.Start(); ActionProcess.BeginOutputReadLine(); ActionProcess.BeginErrorReadLine(); } catch (Exception ex) { Log.TraceError("Failed to start local process for action: {0} {1}", Action.CommandPath, Action.CommandArguments); Log.WriteException(ex, null); ExitCode = 1; bComplete = true; return; } // wait for process to start // NOTE: this may or may not be necessary; seems to depend on whether the system UBT is running on start the process in a timely manner. int checkIterations = 0; bool haveConfiguredProcess = false; do { if (ActionProcess.HasExited) { if (haveConfiguredProcess == false) Debug.WriteLine("Process for action exited before able to configure!"); break; } if (!haveConfiguredProcess) { try { ActionProcess.PriorityClass = ProcessPriorityClass.BelowNormal; haveConfiguredProcess = true; } catch (Exception) { } break; } Thread.Sleep(10); checkIterations++; } while (checkIterations < 100); if (checkIterations == 100) { throw new BuildException("Failed to configure local process for action: {0} {1}", Action.CommandPath, Action.CommandArguments); } // block until it's complete // @todo iosmerge: UBT had started looking at: if (Utils.IsValidProcess(Process)) // do we need to check that in the thread model? ActionProcess.WaitForExit(); // capture exit code ExitCode = ActionProcess.ExitCode; } finally { // As the process has finished now, free its resources. On non-Windows platforms, processes depend // on POSIX/BSD threading and these are limited per application. Disposing the Process releases // these thread resources. if (ActionProcess != null) ActionProcess.Close(); } // we are done!! bComplete = true; } /// /// Starts a thread and runs the action in that thread /// public void Run() { Thread T = new Thread(ThreadFunc); T.Start(); } }; class LocalExecutor : ActionExecutor { /// /// 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. /// [XmlConfigFile] double ProcessorCountMultiplier = 1.0; /// /// Maximum processor count for local execution. /// [XmlConfigFile] int MaxProcessorCount = int.MaxValue; /// /// How many processes that will be executed in parallel /// int NumParallelProcesses; /// /// Constructor /// /// How many actions to execute in parallel. When 0 a default will be chosen based on system resources public LocalExecutor(int MaxLocalActions=0) { XmlConfig.ApplyTo(this); // if specified this caps how many processors we can use if (MaxLocalActions > 0) { NumParallelProcesses = MaxLocalActions; } else { // Figure out how many processors to use NumParallelProcesses = GetMaxActionsToExecuteInParallel(); } } public override string Name { get { return "Local"; } } /// /// Determines the maximum number of actions to execute in parallel, taking into account the resources available on this machine. /// /// Max number of actions to execute in parallel public virtual int GetMaxActionsToExecuteInParallel() { // Get the number of logical processors int NumLogicalCores = Utils.GetLogicalProcessorCount(); // Use WMI to figure out physical cores, excluding hyper threading. int NumPhysicalCores = Utils.GetPhysicalProcessorCount(); if (NumPhysicalCores == -1) { NumPhysicalCores = NumLogicalCores; } // The number of actions to execute in parallel is trying to keep the CPU busy enough in presence of I/O stalls. int MaxActionsToExecuteInParallel = 0; if (NumPhysicalCores < NumLogicalCores && ProcessorCountMultiplier != 1.0) { // The CPU has more logical cores than physical ones, aka uses hyper-threading. // Use multiplier if provided MaxActionsToExecuteInParallel = (int)(NumPhysicalCores * ProcessorCountMultiplier); } else if (NumPhysicalCores < NumLogicalCores && NumPhysicalCores > 4) { // The CPU has more logical cores than physical ones, aka uses hyper-threading. // Use average of logical and physical if we have "lots of cores" MaxActionsToExecuteInParallel = Math.Max((int)(NumPhysicalCores + NumLogicalCores) / 2, NumLogicalCores - 4); } // No hyper-threading. Only kicking off a task per CPU to keep machine responsive. else { MaxActionsToExecuteInParallel = NumPhysicalCores; } MaxActionsToExecuteInParallel = Math.Max(1, Math.Min(MaxActionsToExecuteInParallel, MaxProcessorCount)); return MaxActionsToExecuteInParallel; } /// /// Executes the specified actions locally. /// /// True if all the tasks successfully executed, or false if any of them failed. public override bool ExecuteActions(List Actions) { // Time to sleep after each iteration of the loop in order to not busy wait. const float LoopSleepTime = 0.1f; // The number of actions to execute in parallel is trying to keep the CPU busy enough in presence of I/O stalls. Log.TraceInformation("Performing {0} actions ({1} in parallel)", Actions.Count, NumParallelProcesses); Dictionary ActionThreadDictionary = new Dictionary(); int JobNumber = 1; using (ProgressWriter ProgressWriter = new ProgressWriter("Compiling C++ source code...", false)) { int ProgressValue = 0; while (true) { // Count the number of pending and still executing actions. int NumUnexecutedActions = 0; int NumExecutingActions = 0; foreach (LinkedAction Action in Actions) { ActionThread? ActionThread = null; bool bFoundActionProcess = ActionThreadDictionary.TryGetValue(Action, out ActionThread); if (bFoundActionProcess == false) { NumUnexecutedActions++; } else if (ActionThread != null) { if (ActionThread.bComplete == false) { NumUnexecutedActions++; NumExecutingActions++; } } } // Update the current progress int NewProgressValue = Actions.Count + 1 - NumUnexecutedActions; if (ProgressValue != NewProgressValue) { ProgressWriter.Write(ProgressValue, Actions.Count + 1); ProgressValue = NewProgressValue; } // If there aren't any pending actions left, we're done executing. if (NumUnexecutedActions == 0) { break; } // If there are fewer actions executing than the maximum, look for pending actions that don't have any outdated // prerequisites. foreach (LinkedAction Action in Actions) { ActionThread? ActionProcess = null; bool bFoundActionProcess = ActionThreadDictionary.TryGetValue(Action, out ActionProcess); if (bFoundActionProcess == false) { if (NumExecutingActions < Math.Max(1, NumParallelProcesses)) { // Determine whether there are any prerequisites of the action that are outdated. bool bHasOutdatedPrerequisites = false; bool bHasFailedPrerequisites = false; foreach (LinkedAction PrerequisiteAction in Action.PrerequisiteActions) { if (Actions.Contains(PrerequisiteAction)) { ActionThread? PrerequisiteProcess = null; bool bFoundPrerequisiteProcess = ActionThreadDictionary.TryGetValue(PrerequisiteAction, out PrerequisiteProcess); if (bFoundPrerequisiteProcess == true) { if (PrerequisiteProcess == null) { bHasFailedPrerequisites = true; } else if (PrerequisiteProcess.bComplete == false) { bHasOutdatedPrerequisites = true; } else if (PrerequisiteProcess.ExitCode != 0) { bHasFailedPrerequisites = true; } } else { bHasOutdatedPrerequisites = true; } } } // If there are any failed prerequisites of this action, don't execute it. if (bHasFailedPrerequisites) { // Add a null entry in the dictionary for this action. ActionThreadDictionary.Add(Action, null); } // If there aren't any outdated prerequisites of this action, execute it. else if (!bHasOutdatedPrerequisites) { ActionThread ActionThread = new ActionThread(Action, JobNumber, Actions.Count); JobNumber++; try { ActionThread.Run(); } catch (Exception ex) { throw new BuildException(ex, "Failed to start thread for action: {0} {1}\r\n{2}", Action.CommandPath, Action.CommandArguments, ex.ToString()); } ActionThreadDictionary.Add(Action, ActionThread); NumExecutingActions++; } } } } System.Threading.Thread.Sleep(TimeSpan.FromSeconds(LoopSleepTime)); } } // Check whether any of the tasks failed and log action stats if wanted. bool bSuccess = true; foreach (KeyValuePair ActionProcess in ActionThreadDictionary) { LinkedAction Action = ActionProcess.Key; ActionThread? ActionThread = ActionProcess.Value; // Check for pending actions, preemptive failure if (ActionThread == null) { bSuccess = false; continue; } // Check for executed action but general failure if (ActionThread.ExitCode != 0) { bSuccess = false; } } return bSuccess; } }; }