// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; using EpicGames.Core; using Microsoft.Extensions.Logging; namespace UnrealBuildTool.Modes { /// /// Outputs information about the given target, including a module dependecy graph (in .gefx format and list of module references) /// [ToolMode("Analyze", ToolModeOptions.XmlConfig | ToolModeOptions.BuildPlatforms | ToolModeOptions.SingleInstance | ToolModeOptions.StartPrefetchingEngine | ToolModeOptions.ShowExecutionTime)] class AnalyzeMode : ToolMode { /// /// Execute the command /// /// Command line arguments /// Exit code /// public override Task ExecuteAsync(CommandLineArguments Arguments, ILogger Logger) { Arguments.ApplyTo(this); // Create the build configuration object, and read the settings BuildConfiguration BuildConfiguration = new BuildConfiguration(); XmlConfig.ApplyTo(BuildConfiguration); Arguments.ApplyTo(BuildConfiguration); // Parse all the target descriptors List TargetDescriptors = TargetDescriptor.ParseCommandLine(Arguments, BuildConfiguration, Logger); // Generate the compile DB for each target using (ISourceFileWorkingSet WorkingSet = new EmptySourceFileWorkingSet()) { // Find the compile commands for each file in the target Dictionary FileToCommand = new Dictionary(); foreach (TargetDescriptor TargetDescriptor in TargetDescriptors) { AnalyzeTarget(TargetDescriptor, BuildConfiguration, Logger); } } return Task.FromResult(0); } class ModuleInfo { public UEBuildModule Module; public IReadOnlyList IncludeChain; public HashSet InwardRefs = new HashSet(); public HashSet UniqueInwardRefs = new HashSet(); public HashSet OutwardRefs = new HashSet(); public HashSet UniqueOutwardRefs = new HashSet(); public List ObjectFiles = new List(); public long ObjSize = 0; public List BinaryFiles = new List(); public long BinSize = 0; public ModuleInfo(UEBuildModule Module, params string[] IncludeChain) { this.Module = Module; this.IncludeChain = IncludeChain; } public string Chain => String.Join(" -> ", IncludeChain.Where(x => !String.IsNullOrEmpty(x))); } private static void AnalyzeModuleChains(string ModuleName, List ParentChain, UEBuildTarget Target, Dictionary ModuleToInfo, HashSet Visited, ILogger Logger) { // Prevent recursive includes, they'll never be shorter if (ParentChain.Contains(ModuleName)) { return; } UEBuildModule Module = Target.GetModuleByName(ModuleName); List CurrentChain = new(ParentChain); CurrentChain.Add(Module.Name); if (!ModuleToInfo.ContainsKey(Module)) { ModuleToInfo[Module] = new ModuleInfo(Module, CurrentChain.ToArray()); } if (ModuleToInfo[Module].IncludeChain.Count > CurrentChain.Count) { // Now we need to recheck all downstream dependencies again because the chain may be shorter List RecheckModules = new(); Module.GetAllDependencyModules(RecheckModules, new(), true, false, true); Visited.ExceptWith(RecheckModules); Logger.LogDebug("Found shorter chain for {Module} {Prev} -> {New}, rechecking {Count} already visited dependencies", Module.Name, ModuleToInfo[Module].IncludeChain.Count, CurrentChain.Count, RecheckModules.Count); ModuleToInfo[Module].IncludeChain = CurrentChain.ToArray(); } else if (Visited.Contains(Module)) { return; } Visited.Add(Module); List TargetModules = new(); Module.GetAllDependencyModules(TargetModules, new(), true, false, true); TargetModules.ForEach(x => AnalyzeModuleChains(x.Name, CurrentChain, Target, ModuleToInfo, Visited, Logger)); } private static void AnalyzeTarget(TargetDescriptor TargetDescriptor, BuildConfiguration BuildConfiguration, ILogger Logger) { // Create a makefile for the target UEBuildTarget Target = UEBuildTarget.Create(TargetDescriptor, BuildConfiguration, Logger); DirectoryReference.CreateDirectory(Target.ReceiptFileName.Directory); // Find the shortest path from the target to each module HashSet Visited = new(); Dictionary ModuleToInfo = new Dictionary(); if (Target.Rules.LaunchModuleName != null) { AnalyzeModuleChains(Target.Rules.LaunchModuleName, new List() { "target" }, Target, ModuleToInfo, Visited, Logger); } foreach (string RootModuleName in Target.Rules.ExtraModuleNames) { AnalyzeModuleChains(RootModuleName, new List() { "target" }, Target, ModuleToInfo, Visited, Logger); } // Also enable all the plugin modules foreach (UEBuildPlugin Plugin in Target.BuildPlugins!) { foreach (UEBuildModule Module in Plugin.Modules) { if (!ModuleToInfo.ContainsKey(Module)) { AnalyzeModuleChains(Module.Name, new List() { "target", Plugin.ReferenceChain, Plugin.File.GetFileName() }, Target, ModuleToInfo, Visited, Logger); } } } if (Target.Rules.bBuildAllModules) { foreach (UEBuildBinary Binary in Target.Binaries) { foreach (UEBuildModule Module in Binary.Modules) { if (!ModuleToInfo.ContainsKey(Module)) { // quick hack to make allmodules always worse (empty entries are ignored when writing) List IncludeChain = Enumerable.Repeat(String.Empty, 1000).ToList(); IncludeChain.Add("allmodules option"); if (Module.Rules.Plugin != null) { IncludeChain.Add(Module.Rules.Plugin.File.GetFileName()); AnalyzeModuleChains(Module.Name, IncludeChain, Target, ModuleToInfo, Visited, Logger); } else { AnalyzeModuleChains(Module.Name, IncludeChain, Target, ModuleToInfo, Visited, Logger); } } } } } // Find all the outward dependencies of each module foreach ((UEBuildModule SourceModule, ModuleInfo SourceModuleInfo) in ModuleToInfo) { SourceModuleInfo.OutwardRefs.Add(SourceModule); SourceModule.GetAllDependencyModules(new List(), SourceModuleInfo.OutwardRefs, false, false, false); SourceModuleInfo.OutwardRefs.Remove(SourceModule); } // Find the direct output dependencies of each module foreach ((UEBuildModule SourceModule, ModuleInfo SourceModuleInfo) in ModuleToInfo) { SourceModuleInfo.UniqueOutwardRefs = new HashSet(SourceModuleInfo.OutwardRefs); foreach (UEBuildModule TargetModule in SourceModuleInfo.OutwardRefs) { HashSet VisitedTargetModules = new HashSet(); VisitedTargetModules.Add(SourceModule); List DependencyModules = new List(); TargetModule.GetAllDependencyModules(DependencyModules, VisitedTargetModules, false, false, false); DependencyModules.Remove(TargetModule); SourceModuleInfo.UniqueOutwardRefs.ExceptWith(DependencyModules); } } // Find the direct inward dependencies of each module foreach ((UEBuildModule SourceModule, ModuleInfo SourceModuleInfo) in ModuleToInfo) { foreach (UEBuildModule TargetModule in SourceModuleInfo.OutwardRefs) { ModuleToInfo[TargetModule].InwardRefs.Add(SourceModule); } foreach (UEBuildModule TargetModule in SourceModuleInfo.UniqueOutwardRefs) { ModuleToInfo[TargetModule].UniqueInwardRefs.Add(SourceModule); } } // Estimate the size of object files for each module foreach ((UEBuildModule SourceModule, ModuleInfo SourceModuleInfo) in ModuleToInfo) { if (DirectoryReference.Exists(SourceModule.IntermediateDirectory)) { foreach (FileReference IntermediateFile in DirectoryReference.EnumerateFiles(SourceModule.IntermediateDirectory, "*", SearchOption.AllDirectories)) { if (IntermediateFile.HasExtension(".obj") || IntermediateFile.HasExtension(".o")) { SourceModuleInfo.ObjectFiles.Add(IntermediateFile); SourceModuleInfo.ObjSize += IntermediateFile.ToFileInfo().Length; } } } } HashSet MissingModules = new HashSet(); foreach (UEBuildBinary Binary in Target.Binaries) { long BinSize = 0; foreach (FileReference OutputFilePath in Binary.OutputFilePaths) { FileInfo OutputFileInfo = OutputFilePath.ToFileInfo(); if (OutputFileInfo.Exists) { BinSize += OutputFileInfo.Length; } } foreach (UEBuildModule Module in Binary.Modules) { ModuleInfo? ModuleInfo; if (!ModuleToInfo.TryGetValue(Module, out ModuleInfo)) { MissingModules.Add(Module); continue; } ModuleInfo.BinaryFiles.AddRange(Binary.OutputFilePaths); ModuleInfo.BinSize += BinSize; } } // Warn about any missing modules foreach (UEBuildModule MissingModule in MissingModules.OrderBy(x => x.Name)) { Logger.LogInformation("Missing module '{MissingModuleName}'", MissingModule.Name); } List> AnalyzeProducts = new(); // Generate the dependency graph between modules FileReference DependencyGraphFile = Target.ReceiptFileName.ChangeExtension(".Dependencies.gexf"); Logger.LogInformation("Writing dependency graph to {DependencyGraphFile}...", DependencyGraphFile); WriteDependencyGraph(Target, ModuleToInfo, DependencyGraphFile); AnalyzeProducts.Add(new(DependencyGraphFile, BuildProductType.BuildResource)); // Generate the dependency graph between modules FileReference ShortestPathGraphFile = Target.ReceiptFileName.ChangeExtension(".ShortestPath.gexf"); Logger.LogInformation("Writing shortest-path graph to {ShortestPathGraphFile}...", ShortestPathGraphFile); WriteShortestPathGraph(Target, ModuleToInfo, ShortestPathGraphFile); AnalyzeProducts.Add(new(ShortestPathGraphFile, BuildProductType.BuildResource)); // Write all the target stats as a text file FileReference TextFile = Target.ReceiptFileName.ChangeExtension(".txt"); Logger.LogInformation("Writing module information to {TextFile}", TextFile); using (StreamWriter Writer = new StreamWriter(TextFile.FullName)) { Writer.WriteLine("All modules in {0}, ordered by number of indirect references", Target.TargetName); foreach (ModuleInfo ModuleInfo in ModuleToInfo.Values.OrderByDescending(x => x.InwardRefs.Count).ThenBy(x => x.BinSize)) { Writer.WriteLine(""); Writer.WriteLine("Module: \"{0}\"", ModuleInfo.Module.Name); Writer.WriteLine("Shortest path: {0}", ModuleInfo.Chain); WriteDependencyList(Writer, "Unique inward refs: ", ModuleInfo.UniqueInwardRefs); WriteDependencyList(Writer, "Unique outward refs: ", ModuleInfo.UniqueOutwardRefs); WriteDependencyList(Writer, "Recursive inward refs: ", ModuleInfo.InwardRefs); WriteDependencyList(Writer, "Recursive outward refs: ", ModuleInfo.OutwardRefs); Writer.WriteLine("Object size: {0:n0}kb", (ModuleInfo.ObjSize + 1023) / 1024); Writer.WriteLine("Object files: {0}", String.Join(", ", ModuleInfo.ObjectFiles.Select(x => x.GetFileName()))); Writer.WriteLine("Binary size: {0:n0}kb", (ModuleInfo.BinSize + 1023) / 1024); Writer.WriteLine("Binary files: {0}", String.Join(", ", ModuleInfo.BinaryFiles.Select(x => x.GetFileName()))); } } AnalyzeProducts.Add(new(TextFile, BuildProductType.BuildResource)); // Write all the target stats as a CSV file FileReference CsvFile = Target.ReceiptFileName.ChangeExtension(".csv"); Logger.LogInformation("Writing module information to {CsvFile}", CsvFile); using (StreamWriter Writer = new StreamWriter(CsvFile.FullName)) { List Columns = new List(); Columns.Add("Module"); Columns.Add("ShortestPath"); Columns.Add("NumUniqueInwardRefs"); Columns.Add("UniqueInwardRefs"); Columns.Add("NumRecursiveInwardRefs"); Columns.Add("RecursiveInwardRefs"); Columns.Add("NumUniqueOutwardRefs"); Columns.Add("UniqueOutwardRefs"); Columns.Add("NumRecursiveOutwardRefs"); Columns.Add("RecursiveOutwardRefs"); Columns.Add("ObjSize"); Columns.Add("ObjFiles"); Columns.Add("BinSize"); Columns.Add("BinFiles"); Writer.WriteLine(String.Join(",", Columns)); foreach (ModuleInfo ModuleInfo in ModuleToInfo.Values.OrderByDescending(x => x.InwardRefs.Count).ThenBy(x => x.BinSize)) { Columns.Clear(); Columns.Add(ModuleInfo.Module.Name); Columns.Add(ModuleInfo.Chain); Columns.Add($"{ModuleInfo.UniqueInwardRefs.Count}"); Columns.Add($"\"{String.Join(", ", ModuleInfo.UniqueInwardRefs.Select(x => x.Name))}\""); Columns.Add($"{ModuleInfo.InwardRefs.Count}"); Columns.Add($"\"{String.Join(", ", ModuleInfo.InwardRefs.Select(x => x.Name))}\""); Columns.Add($"{ModuleInfo.UniqueOutwardRefs.Count}"); Columns.Add($"\"{String.Join(", ", ModuleInfo.UniqueOutwardRefs.Select(x => x.Name))}\""); Columns.Add($"{ModuleInfo.OutwardRefs.Count}"); Columns.Add($"\"{String.Join(", ", ModuleInfo.OutwardRefs.Select(x => x.Name))}\""); Columns.Add($"{ModuleInfo.ObjSize}"); Columns.Add($"\"{String.Join(", ", ModuleInfo.ObjectFiles.Select(x => x.GetFileName()))}\""); Columns.Add($"{ModuleInfo.BinSize}"); Columns.Add($"\"{String.Join(", ", ModuleInfo.BinaryFiles.Select(x => x.GetFileName()))}\""); Writer.WriteLine(String.Join(",", Columns)); } } AnalyzeProducts.Add(new(CsvFile, BuildProductType.BuildResource)); foreach (FileReference ManifestFileName in Target.Rules.ManifestFileNames) { Target.GenerateManifest(ManifestFileName, AnalyzeProducts, Logger); } } private static void WriteDependencyList(TextWriter Writer, string Prefix, HashSet Modules) { if (Modules.Count == 0) { Writer.WriteLine("{0} 0", Prefix); } else { Writer.WriteLine("{0} {1} ({2})", Prefix, Modules.Count, String.Join(", ", Modules.Select(x => x.Name).OrderBy(x => x))); } } private static void WriteDependencyGraph(UEBuildTarget Target, Dictionary ModuleToInfo, FileReference FileName) { List Nodes = new List(); Dictionary ModuleToNode = new Dictionary(); foreach (ModuleInfo ModuleInfo in ModuleToInfo.Values) { GraphNode Node = new GraphNode(ModuleInfo.Module.Name); long Size; if (Target.ShouldCompileMonolithic()) { Size = ModuleInfo.ObjSize; } else { Size = ModuleInfo.BinSize; } Node.Size = 1.0f + (Size / (50.0f * 1024.0f * 1024.0f)); Nodes.Add(Node); ModuleToNode[ModuleInfo.Module] = Node; } List Edges = new List(); foreach ((UEBuildModule SourceModule, ModuleInfo SourceModuleInfo) in ModuleToInfo) { GraphNode SourceNode = ModuleToNode[SourceModule]; foreach (UEBuildModule TargetModule in SourceModuleInfo.UniqueOutwardRefs) { ModuleInfo TargetModuleInfo = ModuleToInfo[TargetModule]; GraphNode? TargetNode; if (ModuleToNode.TryGetValue(TargetModule, out TargetNode)) { GraphEdge Edge = new GraphEdge(SourceNode, TargetNode); Edge.Thickness = TargetModuleInfo.InwardRefs.Count; Edges.Add(Edge); } } } GraphVisualization.WriteGraphFile(FileName, $"Module dependency graph for {Target.TargetName}", Nodes, Edges); } private static void WriteShortestPathGraph(UEBuildTarget Target, Dictionary ModuleToInfo, FileReference FileName) { Dictionary NameToNode = new Dictionary(StringComparer.Ordinal); HashSet<(GraphNode, GraphNode)> EdgesSet = new HashSet<(GraphNode, GraphNode)>(); List Edges = new List(); foreach ((UEBuildModule Module, ModuleInfo ModuleInfo) in ModuleToInfo) { string[] Parts = ModuleInfo.Chain.Split(" -> "); GraphNode? PrevNode = null; foreach (string Part in Parts) { GraphNode? NextNode; if (!NameToNode.TryGetValue(Part, out NextNode)) { NextNode = new GraphNode(Part); NameToNode[Part] = NextNode; } if (PrevNode != null && EdgesSet.Add((PrevNode, NextNode))) { GraphEdge Edge = new GraphEdge(PrevNode, NextNode); Edges.Add(Edge); } PrevNode = NextNode; } } GraphVisualization.WriteGraphFile(FileName, $"Module dependency graph for {Target.TargetName}", NameToNode.Values.ToList(), Edges); } private static HashSet GetDirectDependencyModules(UEBuildModule Module) { HashSet ReferencedModules = new HashSet(); Module.GetAllDependencyModules(new List(), ReferencedModules, true, false, false); HashSet Modules = new HashSet(Module.GetDirectDependencyModules()); Modules.ExceptWith(ReferencedModules); return Modules; } } }