/// <summary> /// Saves a UBTMakefile to disk /// </summary> /// <param name="TargetDescs">List of targets. Order is not important</param> static void SaveUBTMakefile( List<TargetDescriptor> TargetDescs, UBTMakefile UBTMakefile ) { if( !UBTMakefile.IsValidMakefile() ) { throw new BuildException( "Can't save a makefile that has invalid contents. See UBTMakefile.IsValidMakefile()" ); } var TimerStartTime = DateTime.UtcNow; var UBTMakefileItem = FileItem.GetItemByFullPath( GetUBTMakefilePath( TargetDescs ) ); // @todo ubtmake: Optimization: The UBTMakefile saved for game projects is upwards of 9 MB. We should try to shrink its content if possible // @todo ubtmake: Optimization: C# Serialization may be too slow for these big Makefiles. Loading these files often shows up as the slower part of the assembling phase. // Serialize the cache to disk. try { Directory.CreateDirectory(Path.GetDirectoryName(UBTMakefileItem.AbsolutePath)); using (FileStream Stream = new FileStream(UBTMakefileItem.AbsolutePath, FileMode.Create, FileAccess.Write)) { BinaryFormatter Formatter = new BinaryFormatter(); Formatter.Serialize(Stream, UBTMakefile); } } catch (Exception Ex) { Console.Error.WriteLine("Failed to write makefile: {0}", Ex.Message); } if (BuildConfiguration.bPrintPerformanceInfo) { var TimerDuration = DateTime.UtcNow - TimerStartTime; Log.TraceInformation("Saving makefile took " + TimerDuration.TotalSeconds + "s"); } }
public static ECompilationResult RunUBT(string[] Arguments) { bool bSuccess = true; var RunUBTInitStartTime = DateTime.UtcNow; // Reset global configurations ActionGraph.ResetAllActions(); // We need to allow the target platform to perform the 'reset' as well... UnrealTargetPlatform ResetPlatform = UnrealTargetPlatform.Unknown; UnrealTargetConfiguration ResetConfiguration; UEBuildTarget.ParsePlatformAndConfiguration(Arguments, out ResetPlatform, out ResetConfiguration); var BuildPlatform = UEBuildPlatform.GetBuildPlatform(ResetPlatform); BuildPlatform.ResetBuildConfiguration(ResetPlatform, ResetConfiguration); // now that we have the platform, we can set the intermediate path to include the platform/architecture name BuildConfiguration.PlatformIntermediateFolder = Path.Combine(BuildConfiguration.BaseIntermediateFolder, ResetPlatform.ToString(), BuildPlatform.GetActiveArchitecture()); string ExecutorName = "Unknown"; ECompilationResult BuildResult = ECompilationResult.Succeeded; var ToolChain = UEToolChain.GetPlatformToolChain(BuildPlatform.GetCPPTargetPlatform(ResetPlatform)); string EULAViolationWarning = null; Thread CPPIncludesThread = null; try { List<string[]> TargetSettings = ParseCommandLineFlags(Arguments); int ArgumentIndex; // action graph implies using the dependency resolve cache bool GeneratingActionGraph = Utils.ParseCommandLineFlag(Arguments, "-graph", out ArgumentIndex ); if (GeneratingActionGraph) { BuildConfiguration.bUseIncludeDependencyResolveCache = true; } bool CreateStub = Utils.ParseCommandLineFlag(Arguments, "-nocreatestub", out ArgumentIndex); if (CreateStub || (String.IsNullOrEmpty(Environment.GetEnvironmentVariable("uebp_LOCAL_ROOT")) && BuildHostPlatform.Current.Platform == UnrealTargetPlatform.Mac)) { BuildConfiguration.bCreateStubIPA = false; } if( BuildConfiguration.bPrintPerformanceInfo ) { var RunUBTInitTime = (DateTime.UtcNow - RunUBTInitStartTime).TotalSeconds; Log.TraceInformation( "RunUBT initialization took " + RunUBTInitTime + "s" ); } var TargetDescs = new List<TargetDescriptor>(); { var TargetDescstStartTime = DateTime.UtcNow; foreach (string[] TargetSetting in TargetSettings) { TargetDescs.AddRange( UEBuildTarget.ParseTargetCommandLine( TargetSetting ) ); } if( BuildConfiguration.bPrintPerformanceInfo ) { var TargetDescsTime = (DateTime.UtcNow - TargetDescstStartTime).TotalSeconds; Log.TraceInformation( "Target descriptors took " + TargetDescsTime + "s" ); } } if (UnrealBuildTool.bIsInvalidatingMakefilesOnly) { Log.TraceInformation("Invalidating makefiles only in this run."); if (TargetDescs.Count != 1) { Log.TraceError("You have to provide one target name for makefile invalidation."); return ECompilationResult.OtherCompilationError; } InvalidateMakefiles(TargetDescs[0]); return ECompilationResult.Succeeded; } UEBuildConfiguration.bHotReloadFromIDE = UEBuildConfiguration.bAllowHotReloadFromIDE && TargetDescs.Count == 1 && !TargetDescs[0].bIsEditorRecompile && ShouldDoHotReloadFromIDE(TargetDescs[0]); bool bIsHotReload = UEBuildConfiguration.bHotReloadFromIDE || ( TargetDescs.Count == 1 && TargetDescs[0].OnlyModules.Count > 0 && TargetDescs[0].ForeignPlugins.Count == 0 ); TargetDescriptor HotReloadTargetDesc = bIsHotReload ? TargetDescs[0] : null; if (ProjectFileGenerator.bGenerateProjectFiles) { // Create empty timestamp file to record when was the last time we regenerated projects. Directory.CreateDirectory( Path.GetDirectoryName( ProjectFileGenerator.ProjectTimestampFile ) ); File.Create(ProjectFileGenerator.ProjectTimestampFile).Dispose(); } if( !ProjectFileGenerator.bGenerateProjectFiles ) { if( BuildConfiguration.bUseUBTMakefiles ) { // If we're building UHT without a Mutex, we'll need to assume that we're building the same targets and that no caches // should be invalidated for this run. This is important when UBT is invoked from within UBT in order to compile // UHT. In that (very common) case we definitely don't want to have to rebuild our cache from scratch. bool bMustAssumeSameTargets = false; if( TargetDescs[0].TargetName.Equals( "UnrealHeaderTool", StringComparison.InvariantCultureIgnoreCase ) ) { int NoMutexArgumentIndex; if (Utils.ParseCommandLineFlag(Arguments, "-NoMutex", out NoMutexArgumentIndex)) { bMustAssumeSameTargets = true; } } bool bIsBuildingSameTargetsAsLastTime = false; if( bMustAssumeSameTargets ) { bIsBuildingSameTargetsAsLastTime = true; } else { string TargetCollectionName = MakeTargetCollectionName( TargetDescs ); string LastBuiltTargetsFileName = bIsHotReload ? "HotReloadLastBuiltTargets.txt" : "LastBuiltTargets.txt"; string LastBuiltTargetsFilePath = Path.Combine( BuildConfiguration.BaseIntermediatePath, LastBuiltTargetsFileName ); if( File.Exists( LastBuiltTargetsFilePath ) && Utils.ReadAllText( LastBuiltTargetsFilePath ) == TargetCollectionName ) { // @todo ubtmake: Because we're using separate files for hot reload vs. full compiles, it's actually possible that includes will // become out of date without us knowing if the developer ping-pongs between hot reloading target A and building target B normally. // To fix this we can not use a different file name for last built targets, but the downside is slower performance when // performing the first hot reload after compiling normally (forces full include dependency scan) bIsBuildingSameTargetsAsLastTime = true; } // Save out the name of the targets we're building if( !bIsBuildingSameTargetsAsLastTime ) { Directory.CreateDirectory( Path.GetDirectoryName( LastBuiltTargetsFilePath ) ); File.WriteAllText( LastBuiltTargetsFilePath, TargetCollectionName, Encoding.UTF8 ); } if( !bIsBuildingSameTargetsAsLastTime ) { // Can't use super fast include checking unless we're building the same set of targets as last time, because // we might not know about all of the C++ include dependencies for already-up-to-date shared build products // between the targets bNeedsFullCPPIncludeRescan = true; Log.TraceInformation( "Performing full C++ include scan ({0} a new target)", bIsHotReload ? "hot reloading" : "building" ); } } } } UBTMakefile UBTMakefile = null; { // If we're generating project files, then go ahead and wipe out the existing UBTMakefile for every target, to make sure that // it gets a full dependency scan next time. // NOTE: This is just a safeguard and doesn't have to be perfect. We also check for newer project file timestamps in LoadUBTMakefile() string UBTMakefilePath = UnrealBuildTool.GetUBTMakefilePath( TargetDescs ); if( ProjectFileGenerator.bGenerateProjectFiles ) // @todo ubtmake: This is only hit when generating IntelliSense for project files. Probably should be done right inside ProjectFileGenerator.bat { // @todo ubtmake: Won't catch multi-target cases as GPF always builds one target at a time for Intellisense // Delete the UBTMakefile if (File.Exists(UBTMakefilePath)) { UEBuildTarget.CleanFile(UBTMakefilePath); } } // Make sure the gather phase is executed if we're not actually building anything if( ProjectFileGenerator.bGenerateProjectFiles || UEBuildConfiguration.bGenerateManifest || UEBuildConfiguration.bCleanProject || BuildConfiguration.bXGEExport || UEBuildConfiguration.bGenerateExternalFileList || GeneratingActionGraph) { UnrealBuildTool.bIsGatheringBuild_Unsafe = true; } // Were we asked to run in 'assembler only' mode? If so, let's check to see if that's even possible by seeing if // we have a valid UBTMakefile already saved to disk, ready for us to load. if( UnrealBuildTool.bIsAssemblingBuild_Unsafe && !UnrealBuildTool.bIsGatheringBuild_Unsafe ) { // @todo ubtmake: Mildly terrified of BuildConfiguration/UEBuildConfiguration globals that were set during the Prepare phase but are not set now. We may need to save/load all of these, otherwise // we'll need to call SetupGlobalEnvironment on all of the targets (maybe other stuff, too. See PreBuildStep()) // Try to load the UBTMakefile. It will only be loaded if it has valid content and is not determined to be out of date. string ReasonNotLoaded; UBTMakefile = LoadUBTMakefile( UBTMakefilePath, out ReasonNotLoaded ); // Invalid makefile if only modules have changed if (UBTMakefile != null && !TargetDescs.SelectMany(x => x.OnlyModules) .Select(x => new Tuple<string, bool>(x.OnlyModuleName.ToLower(), string.IsNullOrWhiteSpace(x.OnlyModuleSuffix))) .SequenceEqual( UBTMakefile.Targets.SelectMany(x => x.OnlyModules) .Select(x => new Tuple<string, bool>(x.OnlyModuleName.ToLower(), string.IsNullOrWhiteSpace(x.OnlyModuleSuffix))) ) ) { UBTMakefile = null; ReasonNotLoaded = "modules to compile have changed"; } if( UBTMakefile == null ) { // If the Makefile couldn't be loaded, then we're not going to be able to continue in "assembler only" mode. We'll do both // a 'gather' and 'assemble' in the same run. This will take a while longer, but subsequent runs will be fast! UnrealBuildTool.bIsGatheringBuild_Unsafe = true; FileItem.ClearCaches(); Log.TraceInformation( "Creating makefile for {0}{1}{2} ({3})", bIsHotReload ? "hot reloading " : "", TargetDescs[0].TargetName, TargetDescs.Count > 1 ? ( " (and " + ( TargetDescs.Count - 1 ).ToString() + " more)" ) : "", ReasonNotLoaded ); } } // OK, after this point it is safe to access the UnrealBuildTool.IsGatheringBuild and UnrealBuildTool.IsAssemblingBuild properties. // These properties will not be changing again during this session/ bIsSafeToCheckIfGatheringOrAssemblingBuild = true; } List<UEBuildTarget> Targets; if( UBTMakefile != null && !IsGatheringBuild && IsAssemblingBuild ) { // If we've loaded a makefile, then we can fill target information from this file! Targets = UBTMakefile.Targets; } else { var TargetInitStartTime = DateTime.UtcNow; Targets = new List<UEBuildTarget>(); foreach( var TargetDesc in TargetDescs ) { var Target = UEBuildTarget.CreateTarget( TargetDesc ); if ((Target == null) && (UEBuildConfiguration.bCleanProject)) { continue; } Targets.Add(Target); } if( BuildConfiguration.bPrintPerformanceInfo ) { var TargetInitTime = (DateTime.UtcNow - TargetInitStartTime).TotalSeconds; Log.TraceInformation( "Target init took " + TargetInitTime + "s" ); } } // Build action lists for all passed in targets. var OutputItemsForAllTargets = new List<FileItem>(); var TargetNameToUObjectModules = new Dictionary<string, List<UHTModuleInfo>>( StringComparer.InvariantCultureIgnoreCase ); foreach (var Target in Targets) { var TargetStartTime = DateTime.UtcNow; if (bIsHotReload) { // Don't produce new DLLs if there's been no code changes UEBuildConfiguration.bSkipLinkingWhenNothingToCompile = true; Log.TraceInformation("Compiling game modules for hot reload"); } // When in 'assembler only' mode, we'll load this cache later on a worker thread. It takes a long time to load! if( !( !UnrealBuildTool.IsGatheringBuild && UnrealBuildTool.IsAssemblingBuild ) ) { // Load the direct include dependency cache. CPPEnvironment.IncludeDependencyCache.Add( Target, DependencyCache.Create( DependencyCache.GetDependencyCachePathForTarget(Target) ) ); } // We don't need this dependency cache in 'gather only' mode if( BuildConfiguration.bUseUBTMakefiles && !( UnrealBuildTool.IsGatheringBuild && !UnrealBuildTool.IsAssemblingBuild ) ) { // Load the cache that contains the list of flattened resolved includes for resolved source files // @todo ubtmake: Ideally load this asynchronously at startup and only block when it is first needed and not finished loading CPPEnvironment.FlatCPPIncludeDependencyCache.Add( Target, FlatCPPIncludeDependencyCache.Create( Target ) ); } if( UnrealBuildTool.IsGatheringBuild ) { List<FileItem> TargetOutputItems; List<UHTModuleInfo> TargetUObjectModules; BuildResult = Target.Build(ToolChain, out TargetOutputItems, out TargetUObjectModules, out EULAViolationWarning); if(BuildResult != ECompilationResult.Succeeded) { break; } OutputItemsForAllTargets.AddRange( TargetOutputItems ); // Update mapping of the target name to the list of UObject modules in this target TargetNameToUObjectModules[ Target.GetTargetName() ] = TargetUObjectModules; if ( (BuildConfiguration.bXGEExport && UEBuildConfiguration.bGenerateManifest) || (!ProjectFileGenerator.bGenerateProjectFiles && !UEBuildConfiguration.bGenerateManifest && !UEBuildConfiguration.bCleanProject)) { // Generate an action graph if we were asked to do that. The graph generation needs access to the include dependency cache, so // we generate it before saving and cleaning that up. if( GeneratingActionGraph ) { // The graph generation feature currently only works with a single target at a time. This is because of how we need access // to include dependencies for the target, but those aren't kept around as we process multiple targets if( TargetSettings.Count != 1 ) { throw new BuildException( "ERROR: The '-graph' option only works with a single target at a time" ); } ActionGraph.FinalizeActionGraph(); var ActionsToExecute = ActionGraph.AllActions; var VisualizationType = ActionGraph.ActionGraphVisualizationType.OnlyCPlusPlusFilesAndHeaders; ActionGraph.SaveActionGraphVisualization( Target, Path.Combine( BuildConfiguration.BaseIntermediatePath, Target.GetTargetName() + ".gexf" ), Target.GetTargetName(), VisualizationType, ActionsToExecute ); } } var TargetBuildTime = (DateTime.UtcNow - TargetStartTime).TotalSeconds; // Send out telemetry for this target Telemetry.SendEvent("TargetBuildStats.2", "AppName", Target.AppName, "GameName", Target.TargetName, "Platform", Target.Platform.ToString(), "Configuration", Target.Configuration.ToString(), "CleanTarget", UEBuildConfiguration.bCleanProject.ToString(), "Monolithic", Target.ShouldCompileMonolithic().ToString(), "CreateDebugInfo", Target.IsCreatingDebugInfo().ToString(), "TargetType", Target.TargetType.ToString(), "TargetCreateTimeSec", TargetBuildTime.ToString("0.00") ); } } if (BuildResult == ECompilationResult.Succeeded && ( (BuildConfiguration.bXGEExport && UEBuildConfiguration.bGenerateManifest) || (!GeneratingActionGraph && !ProjectFileGenerator.bGenerateProjectFiles && !UEBuildConfiguration.bGenerateManifest && !UEBuildConfiguration.bCleanProject && !UEBuildConfiguration.bGenerateExternalFileList) )) { if( UnrealBuildTool.IsGatheringBuild ) { ActionGraph.FinalizeActionGraph(); UBTMakefile = new UBTMakefile(); UBTMakefile.AllActions = ActionGraph.AllActions; // For now simply treat all object files as the root target. { var PrerequisiteActionsSet = new HashSet<Action>(); foreach (var OutputItem in OutputItemsForAllTargets) { ActionGraph.GatherPrerequisiteActions(OutputItem, ref PrerequisiteActionsSet); } UBTMakefile.PrerequisiteActions = PrerequisiteActionsSet.ToArray(); } foreach( System.Collections.DictionaryEntry EnvironmentVariable in Environment.GetEnvironmentVariables() ) { UBTMakefile.EnvironmentVariables.Add( Tuple.Create( (string)EnvironmentVariable.Key, (string)EnvironmentVariable.Value ) ); } UBTMakefile.TargetNameToUObjectModules = TargetNameToUObjectModules; UBTMakefile.Targets = Targets; if( BuildConfiguration.bUseUBTMakefiles ) { // We've been told to prepare to build, so let's go ahead and save out our action graph so that we can use in a later invocation // to assemble the build. Even if we are configured to assemble the build in this same invocation, we want to save out the // Makefile so that it can be used on subsequent 'assemble only' runs, for the fastest possible iteration times // @todo ubtmake: Optimization: We could make 'gather + assemble' mode slightly faster by saving this while busy compiling (on our worker thread) SaveUBTMakefile( TargetDescs, UBTMakefile ); } } if( UnrealBuildTool.IsAssemblingBuild ) { // If we didn't build the graph in this session, then we'll need to load a cached one if( !UnrealBuildTool.IsGatheringBuild ) { ActionGraph.AllActions = UBTMakefile.AllActions; // Patch action history for hot reload when running in assembler mode. In assembler mode, the suffix on the output file will be // the same for every invocation on that makefile, but we need a new suffix each time. if( bIsHotReload ) { PatchActionHistoryForHotReloadAssembling( HotReloadTargetDesc.OnlyModules ); } foreach( var EnvironmentVariable in UBTMakefile.EnvironmentVariables ) { // @todo ubtmake: There may be some variables we do NOT want to clobber. Environment.SetEnvironmentVariable( EnvironmentVariable.Item1, EnvironmentVariable.Item2 ); } // If any of the targets need UHT to be run, we'll go ahead and do that now foreach( var Target in Targets ) { List<UHTModuleInfo> TargetUObjectModules; if( UBTMakefile.TargetNameToUObjectModules.TryGetValue( Target.GetTargetName(), out TargetUObjectModules ) ) { if( TargetUObjectModules.Count > 0 ) { // Execute the header tool string ModuleInfoFileName = Path.GetFullPath( Path.Combine( Target.ProjectIntermediateDirectory, "UnrealHeaderTool.manifest" ) ); ECompilationResult UHTResult = ECompilationResult.OtherCompilationError; if (!ExternalExecution.ExecuteHeaderToolIfNecessary(Target, GlobalCompileEnvironment:null, UObjectModules:TargetUObjectModules, ModuleInfoFileName:ModuleInfoFileName, UHTResult:ref UHTResult)) { Log.TraceInformation("UnrealHeaderTool failed for target '" + Target.GetTargetName() + "' (platform: " + Target.Platform.ToString() + ", module info: " + ModuleInfoFileName + ")."); BuildResult = UHTResult; break; } } } } } if( BuildResult.Succeeded() ) { // Make sure any old DLL files from in-engine recompiles aren't lying around. Must be called after the action graph is finalized. ActionGraph.DeleteStaleHotReloadDLLs(); // Plan the actions to execute for the build. Dictionary<UEBuildTarget,List<FileItem>> TargetToOutdatedPrerequisitesMap; List<Action> ActionsToExecute = ActionGraph.GetActionsToExecute(UBTMakefile.PrerequisiteActions, Targets, out TargetToOutdatedPrerequisitesMap); // Display some stats to the user. Log.TraceVerbose( "{0} actions, {1} outdated and requested actions", ActionGraph.AllActions.Count, ActionsToExecute.Count ); if (!bIsHotReload) { // clean up any stale modules foreach (UEBuildTarget Target in Targets) { Target.CleanStaleModules(); } } ToolChain.PreBuildSync(); // Cache indirect includes for all outdated C++ files. We kick this off as a background thread so that it can // perform the scan while we're compiling. It usually only takes up to a few seconds, but we don't want to hurt // our best case UBT iteration times for this task which can easily be performed asynchronously if( BuildConfiguration.bUseUBTMakefiles && TargetToOutdatedPrerequisitesMap.Count > 0 ) { CPPIncludesThread = CreateThreadForCachingCPPIncludes( TargetToOutdatedPrerequisitesMap ); CPPIncludesThread.Start(); } // Execute the actions. bSuccess = ActionGraph.ExecuteActions(ActionsToExecute, out ExecutorName); // if the build succeeded, write the receipts and do any needed syncing if (bSuccess) { foreach (UEBuildTarget Target in Targets) { Target.WriteReceipt(); ToolChain.PostBuildSync(Target); } if (ActionsToExecute.Count == 0 && UEBuildConfiguration.bSkipLinkingWhenNothingToCompile) { BuildResult = ECompilationResult.UpToDate; } } else { BuildResult = ECompilationResult.OtherCompilationError; } } } } } catch (BuildException Exception) { // Output the message only, without the call stack Log.TraceInformation(Exception.Message); BuildResult = ECompilationResult.OtherCompilationError; } catch (Exception Exception) { Log.TraceInformation("ERROR: {0}", Exception); BuildResult = ECompilationResult.OtherCompilationError; } // Wait until our CPPIncludes dependency scanner thread has finished if( CPPIncludesThread != null ) { CPPIncludesThread.Join(); } // Save the include dependency cache. { // NOTE: It's very important that we save the include cache, even if a build exception was thrown (compile error, etc), because we need to make sure that // any C++ include dependencies that we computed for out of date source files are saved. Remember, the build may fail *after* some build products // are successfully built. If we didn't save our dependency cache after build failures, source files for those build products that were successsfully // built before the failure would not be considered out of date on the next run, so this is our only chance to cache C++ includes for those files! foreach( var DependencyCache in CPPEnvironment.IncludeDependencyCache.Values ) { DependencyCache.Save(); } CPPEnvironment.IncludeDependencyCache.Clear(); foreach( var FlatCPPIncludeDependencyCache in CPPEnvironment.FlatCPPIncludeDependencyCache.Values ) { FlatCPPIncludeDependencyCache.Save(); } CPPEnvironment.FlatCPPIncludeDependencyCache.Clear(); } if(EULAViolationWarning != null) { Log.TraceWarning("WARNING: {0}", EULAViolationWarning); } // Figure out how long we took to execute. double BuildDuration = (DateTime.UtcNow - StartTime).TotalSeconds; if (ExecutorName == "Local" || ExecutorName == "Distcc" || ExecutorName == "SNDBS") { Log.WriteLineIf(BuildConfiguration.bLogDetailedActionStats || BuildConfiguration.bPrintDebugInfo, TraceEventType.Information, "Cumulative action seconds ({0} processors): {1:0.00} building projects, {2:0.00} compiling, {3:0.00} creating app bundles, {4:0.00} generating debug info, {5:0.00} linking, {6:0.00} other", System.Environment.ProcessorCount, TotalBuildProjectTime, TotalCompileTime, TotalCreateAppBundleTime, TotalGenerateDebugInfoTime, TotalLinkTime, TotalOtherActionsTime ); Telemetry.SendEvent("BuildStatsTotal.2", "ExecutorName", ExecutorName, "TotalUBTWallClockTimeSec", BuildDuration.ToString("0.00"), "TotalBuildProjectThreadTimeSec", TotalBuildProjectTime.ToString("0.00"), "TotalCompileThreadTimeSec", TotalCompileTime.ToString("0.00"), "TotalCreateAppBundleThreadTimeSec", TotalCreateAppBundleTime.ToString("0.00"), "TotalGenerateDebugInfoThreadTimeSec", TotalGenerateDebugInfoTime.ToString("0.00"), "TotalLinkThreadTimeSec", TotalLinkTime.ToString("0.00"), "TotalOtherActionsThreadTimeSec", TotalOtherActionsTime.ToString("0.00") ); Log.TraceInformation("Total build time: {0:0.00} seconds", BuildDuration); // reset statistics TotalBuildProjectTime = 0; TotalCompileTime = 0; TotalCreateAppBundleTime = 0; TotalGenerateDebugInfoTime = 0; TotalLinkTime = 0; TotalOtherActionsTime = 0; } else { if (ExecutorName == "XGE") { Log.TraceInformation("XGE execution time: {0:0.00} seconds", BuildDuration); } Telemetry.SendEvent("BuildStatsTotal.2", "ExecutorName", ExecutorName, "TotalUBTWallClockTimeSec", BuildDuration.ToString("0.00") ); } return BuildResult; }