// Copyright Epic Games, Inc. All Rights Reserved. using EpicGames.Core; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Xml; 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 int Execute(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.bUsePrecompiled, BuildConfiguration.bSkipRulesCompile, BuildConfiguration.bForceRulesCompile, 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 0; } class ModuleInfo { public UEBuildModule Module; public string Chain; 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, string Chain) { this.Module = Module; this.Chain = Chain; } } private void AnalyzeTarget(TargetDescriptor TargetDescriptor, BuildConfiguration BuildConfiguration, ILogger Logger) { // Create a makefile for the target UEBuildTarget Target = UEBuildTarget.Create(TargetDescriptor, BuildConfiguration.bSkipRulesCompile, BuildConfiguration.bForceRulesCompile, BuildConfiguration.bUsePrecompiled, Logger); DirectoryReference.CreateDirectory(Target.ReceiptFileName.Directory); // Find the shortest path from the target to each module Dictionary ModuleToInfo = new Dictionary(); List RootModuleNames = new List(Target.Rules.ExtraModuleNames); if (Target.Rules.LaunchModuleName != null) { RootModuleNames.Add(Target.Rules.LaunchModuleName); } foreach (string RootModuleName in RootModuleNames) { UEBuildModule Module = Target.GetModuleByName(RootModuleName); if (Module != null) { string Chain = $"target -> {RootModuleName}"; ModuleToInfo[Module] = new ModuleInfo(Module, Chain); } } // Also enable all the plugin modules foreach (UEBuildPlugin Plugin in Target.BuildPlugins!) { foreach (UEBuildModule Module in Plugin.Modules) { string Chain = $"{Plugin.ReferenceChain} -> {Module.Name}"; ModuleToInfo[Module] = new ModuleInfo(Module, Chain); } } // Set of visited modules HashSet VisitedModules = new HashSet(); // Recurse out to find new modules and the shortest path to each List SourceModules = new List(ModuleToInfo.Keys); while (SourceModules.Count > 0) { List TargetModules = new List(); foreach (UEBuildModule SourceModule in SourceModules) { int Idx = TargetModules.Count; SourceModule.GetAllDependencyModules(TargetModules, VisitedModules, true, false, true); for (; Idx < TargetModules.Count; Idx++) { UEBuildModule TargetModule = TargetModules[Idx]; string Chain = $"{ModuleToInfo[SourceModule].Chain} -> {TargetModule.Name}"; ModuleToInfo[TargetModule] = new ModuleInfo(TargetModule, Chain); } } SourceModules = TargetModules; } // 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.LogWarning("Missing module '{MissingModuleName}'", MissingModule.Name); } // 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); // 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); // 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()))); } } // 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)); } } } private 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 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 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; } } }