// Copyright 1998-2017 Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.IO; using System.Diagnostics; using System.Xml; using System.Text.RegularExpressions; using System.Linq; using System.Management; using System.Reflection; using System.Threading; using System.ServiceProcess; namespace UnrealBuildTool { class SNDBS : ActionExecutor { /// /// Processor count multiplier for local execution. Can be below 1 to reserve CPU for other tasks. /// [XmlConfigFile] public double ProcessorCountMultiplier = 1.0; /// /// Maximum processor count for local execution. /// [XmlConfigFile] public int MaxProcessorCount = int.MaxValue; /// /// The number of actions to execute in parallel is trying to keep the CPU busy enough in presence of I/O stalls. /// int MaxActionsToExecuteInParallel; /// /// Unique id for new jobs /// int JobNumber; public SNDBS() { XmlConfig.ApplyTo(this); } public override string Name { get { return "SNDBS"; } } /// /// Used when debugging Actions outputs all action return values to debug out /// /// Sending object /// Event arguments (In this case, the line of string output) static protected void ActionDebugOutput(object sender, DataReceivedEventArgs e) { string Output = e.Data; if (Output == null) { return; } Log.TraceInformation(Output); } internal bool ExecuteLocalActions(List InLocalActions, Dictionary InActionThreadDictionary, int TotalNumJobs) { // Time to sleep after each iteration of the loop in order to not busy wait. const float LoopSleepTime = 0.1f; bool LocalActionsResult = true; while (true) { // Count the number of pending and still executing actions. int NumUnexecutedActions = 0; int NumExecutingActions = 0; foreach (Action Action in InLocalActions) { ActionThread ActionThread = null; bool bFoundActionProcess = InActionThreadDictionary.TryGetValue(Action, out ActionThread); if (bFoundActionProcess == false) { NumUnexecutedActions++; } else if (ActionThread != null) { if (ActionThread.bComplete == false) { NumUnexecutedActions++; NumExecutingActions++; } } } // 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 (Action Action in InLocalActions) { ActionThread ActionProcess = null; bool bFoundActionProcess = InActionThreadDictionary.TryGetValue(Action, out ActionProcess); if (bFoundActionProcess == false) { if (NumExecutingActions < Math.Max(1, MaxActionsToExecuteInParallel)) { // Determine whether there are any prerequisites of the action that are outdated. bool bHasOutdatedPrerequisites = false; bool bHasFailedPrerequisites = false; foreach (FileItem PrerequisiteItem in Action.PrerequisiteItems) { if (PrerequisiteItem.ProducingAction != null && InLocalActions.Contains(PrerequisiteItem.ProducingAction)) { ActionThread PrerequisiteProcess = null; bool bFoundPrerequisiteProcess = InActionThreadDictionary.TryGetValue(PrerequisiteItem.ProducingAction, 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. InActionThreadDictionary.Add(Action, null); } // If there aren't any outdated prerequisites of this action, execute it. else if (!bHasOutdatedPrerequisites) { ActionThread ActionThread = new ActionThread(Action, JobNumber, TotalNumJobs); ActionThread.Run(); InActionThreadDictionary.Add(Action, ActionThread); NumExecutingActions++; JobNumber++; } } } } System.Threading.Thread.Sleep(TimeSpan.FromSeconds(LoopSleepTime)); } return LocalActionsResult; } internal bool ExecuteActions(List InActions, Dictionary InActionThreadDictionary) { // Build the script file that will be executed by SN-DBS StreamWriter ScriptFile; string ScriptFilename = Path.Combine(UnrealBuildTool.EngineDirectory.FullName, "Intermediate", "Build", "SNDBS.bat"); FileStream ScriptFileStream = new FileStream(ScriptFilename, FileMode.Create, FileAccess.ReadWrite, FileShare.Read); ScriptFile = new StreamWriter(ScriptFileStream); ScriptFile.AutoFlush = true; int NumScriptedActions = 0; List LocalActions = new List(); ActionThread DummyActionThread = new ActionThread(null, 1, 1); foreach (Action Action in InActions) { ActionThread ActionProcess = null; bool bFoundActionProcess = InActionThreadDictionary.TryGetValue(Action, out ActionProcess); if (bFoundActionProcess == false) { // Determine whether there are any prerequisites of the action that are outdated. bool bHasOutdatedPrerequisites = false; bool bHasFailedPrerequisites = false; foreach (FileItem PrerequisiteItem in Action.PrerequisiteItems) { if (PrerequisiteItem.ProducingAction != null && InActions.Contains(PrerequisiteItem.ProducingAction)) { ActionThread PrerequisiteProcess = null; bool bFoundPrerequisiteProcess = InActionThreadDictionary.TryGetValue(PrerequisiteItem.ProducingAction, 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. InActionThreadDictionary.Add(Action, null); } // If there aren't any outdated prerequisites of this action, execute it. else if (!bHasOutdatedPrerequisites) { if (Action.bCanExecuteRemotely == false || Action.bCanExecuteRemotelyWithSNDBS == false) { // Execute locally LocalActions.Add(Action); } else { // Add to script for execution by SN-DBS string NewCommandArguments = "\"" + Action.CommandPath + "\"" + " " + Action.CommandArguments; ScriptFile.WriteLine(ActionThread.ExpandEnvironmentVariables(NewCommandArguments)); InActionThreadDictionary.Add(Action, DummyActionThread); Action.StartTime = Action.EndTime = DateTimeOffset.Now; Log.TraceInformation("[{0}/{1}] {2} {3}", JobNumber, InActions.Count, Action.CommandDescription, Action.StatusDescription); JobNumber++; NumScriptedActions++; } } } } ScriptFile.Flush(); ScriptFile.Close(); ScriptFile.Dispose(); ScriptFile = null; if (NumScriptedActions > 0) { // Create the process string SCERoot = Environment.GetEnvironmentVariable("SCE_ROOT_DIR"); string SNDBSExecutable = Path.Combine(SCERoot, "Common/SN-DBS/bin/dbsbuild.exe"); ProcessStartInfo PSI = new ProcessStartInfo(SNDBSExecutable, String.Format("-q -p UE4 -s \"{0}\"", FileReference.Combine(UnrealBuildTool.EngineDirectory, "Intermediate", "Build", "sndbs.bat").FullName)); PSI.RedirectStandardOutput = true; PSI.RedirectStandardError = true; PSI.UseShellExecute = false; PSI.CreateNoWindow = true; PSI.WorkingDirectory = Path.GetFullPath("."); ; Process NewProcess = new Process(); NewProcess.StartInfo = PSI; NewProcess.OutputDataReceived += new DataReceivedEventHandler(ActionDebugOutput); NewProcess.ErrorDataReceived += new DataReceivedEventHandler(ActionDebugOutput); DateTimeOffset StartTime = DateTimeOffset.Now; NewProcess.Start(); NewProcess.BeginOutputReadLine(); NewProcess.BeginErrorReadLine(); NewProcess.WaitForExit(); TimeSpan Duration; DateTimeOffset EndTime = DateTimeOffset.Now; if (EndTime == DateTimeOffset.MinValue) { Duration = DateTimeOffset.Now - StartTime; } else { Duration = EndTime - StartTime; } DummyActionThread.bComplete = true; int ExitCode = NewProcess.ExitCode; if (ExitCode != 0) { return false; } } // Execute local tasks if (LocalActions.Count > 0) { return ExecuteLocalActions(LocalActions, InActionThreadDictionary, InActions.Count); } return true; } public static bool IsAvailable() { string SCERoot = Environment.GetEnvironmentVariable("SCE_ROOT_DIR"); if(SCERoot == null) { return false; } if (!File.Exists(Path.Combine(SCERoot, "Common/SN-DBS/bin/dbsbuild.exe"))) { return false; } ServiceController[] services = ServiceController.GetServices(); foreach (ServiceController service in services) { if (service.ServiceName.StartsWith("SNDBS") && service.Status == ServiceControllerStatus.Running) { return true; } } return false; } public override bool ExecuteActions(List Actions, bool bLogDetailedActionStats) { bool SNDBSResult = true; if (Actions.Count > 0) { // Use WMI to figure out physical cores, excluding hyper threading. int NumCores = 0; if (!Utils.IsRunningOnMono) { try { using (ManagementObjectSearcher Mos = new System.Management.ManagementObjectSearcher("Select * from Win32_Processor")) { ManagementObjectCollection MosCollection = Mos.Get(); foreach (ManagementBaseObject Item in MosCollection) { NumCores += int.Parse(Item["NumberOfCores"].ToString()); } } } catch (Exception Ex) { Log.TraceWarning("Unable to get the number of Cores: {0}", Ex.ToString()); Log.TraceWarning("Falling back to processor count."); } } // On some systems this requires a hot fix to work so we fall back to using the (logical) processor count. if (NumCores == 0) { NumCores = System.Environment.ProcessorCount; } // The number of actions to execute in parallel is trying to keep the CPU busy enough in presence of I/O stalls. MaxActionsToExecuteInParallel = 0; // The CPU has more logical cores than physical ones, aka uses hyper-threading. if (NumCores < System.Environment.ProcessorCount) { MaxActionsToExecuteInParallel = (int)(NumCores * ProcessorCountMultiplier); } // No hyper-threading. Only kicking off a task per CPU to keep machine responsive. else { MaxActionsToExecuteInParallel = NumCores; } MaxActionsToExecuteInParallel = Math.Min(MaxActionsToExecuteInParallel, MaxProcessorCount); JobNumber = 1; Dictionary ActionThreadDictionary = new Dictionary(); while (true) { bool bUnexecutedActions = false; foreach (Action Action in Actions) { ActionThread ActionThread = null; bool bFoundActionProcess = ActionThreadDictionary.TryGetValue(Action, out ActionThread); if (bFoundActionProcess == false) { bUnexecutedActions = true; if(!ExecuteActions(Actions, ActionThreadDictionary)) { return false; } break; } } if (bUnexecutedActions == false) { break; } } Log.WriteLineIf(bLogDetailedActionStats, LogEventType.Console, "-------- Begin Detailed Action Stats ----------------------------------------------------------"); Log.WriteLineIf(bLogDetailedActionStats, LogEventType.Console, "^Action Type^Duration (seconds)^Tool^Task^Using PCH"); double TotalThreadSeconds = 0; // Check whether any of the tasks failed and log action stats if wanted. foreach (KeyValuePair ActionProcess in ActionThreadDictionary) { Action Action = ActionProcess.Key; ActionThread ActionThread = ActionProcess.Value; // Check for pending actions, preemptive failure if (ActionThread == null) { SNDBSResult = false; continue; } // Check for executed action but general failure if (ActionThread.ExitCode != 0) { SNDBSResult = false; } // Log CPU time, tool and task. double ThreadSeconds = Action.Duration.TotalSeconds; Log.WriteLineIf(bLogDetailedActionStats, LogEventType.Console, "^{0}^{1:0.00}^{2}^{3}^{4}", Action.ActionType.ToString(), ThreadSeconds, Path.GetFileName(Action.CommandPath), Action.StatusDescription, Action.bIsUsingPCH); // Keep track of total thread seconds spent on tasks. TotalThreadSeconds += ThreadSeconds; } Log.TraceInformation("-------- End Detailed Actions Stats -----------------------------------------------------------"); // Log total CPU seconds and numbers of processors involved in tasks. Log.WriteLineIf(bLogDetailedActionStats || UnrealBuildTool.bPrintDebugInfo, LogEventType.Console, "Cumulative thread seconds ({0} processors): {1:0.00}", System.Environment.ProcessorCount, TotalThreadSeconds); } return SNDBSResult; } } }