From 820c18968a945acb5a205e3e65402fe5b5c644f8 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 3 Apr 2023 23:41:18 +0200 Subject: [PATCH] Add C# class library optimization for normal game builds (without AOT) --- Source/Editor/Cooker/Steps/DeployDataStep.cpp | 20 ++++ .../Flax.Build/Build/DotNet/DotNetAOT.cs | 104 +++++++++++++++--- .../Tools/Flax.Build/Utilities/MonoCecil.cs | 13 +++ 3 files changed, 121 insertions(+), 16 deletions(-) diff --git a/Source/Editor/Cooker/Steps/DeployDataStep.cpp b/Source/Editor/Cooker/Steps/DeployDataStep.cpp index 6ec9165fd..8c8a45d7c 100644 --- a/Source/Editor/Cooker/Steps/DeployDataStep.cpp +++ b/Source/Editor/Cooker/Steps/DeployDataStep.cpp @@ -225,6 +225,26 @@ bool DeployDataStep::Perform(CookingData& data) } } } + + // Optimize deployed C# class library (remove DLLs unused by scripts) + if (aotMode == DotNetAOTModes::None && buildSettings.SkipUnusedDotnetLibsPackaging) + { + LOG(Info, "Optimizing .NET class library size to include only used assemblies"); + const String logFile = data.CacheDirectory / TEXT("StripDotnetLibs.txt"); + String args = String::Format( + TEXT("-log -logfile=\"{}\" -runDotNetClassLibStripping -mutex -binaries=\"{}\""), + logFile, data.DataOutputPath); + for (const String& define : data.CustomDefines) + { + args += TEXT(" -D"); + args += define; + } + if (ScriptsBuilder::RunBuildTool(args)) + { + data.Error(TEXT("Failed to optimize .Net class library.")); + return true; + } + } } #else if (!FileSystem::DirectoryExists(dstMono)) diff --git a/Source/Tools/Flax.Build/Build/DotNet/DotNetAOT.cs b/Source/Tools/Flax.Build/Build/DotNet/DotNetAOT.cs index c8ed9836a..1613226d9 100644 --- a/Source/Tools/Flax.Build/Build/DotNet/DotNetAOT.cs +++ b/Source/Tools/Flax.Build/Build/DotNet/DotNetAOT.cs @@ -56,19 +56,88 @@ namespace Flax.Build public static void RunDotNetAOT() { Log.Info("Running .NET AOT in mode " + AOTMode); - DotNetAOT.Run(); + DotNetAOT.RunAOT(); + } + + /// + /// Executes .NET class library stripping process as a part of the game cooking (called by DeployDataStep in Editor). + /// + [CommandLine("runDotNetClassLibStripping", "")] + public static void RunDotNetClassLibStripping() + { + Log.Info("Running .NET class Library stripping"); + DotNetAOT.RunClassLibStripping(); } } /// /// The DotNet Ahead of Time Compilation (AOT) feature. /// - public static class DotNetAOT + internal static class DotNetAOT { - /// - /// Executes AOT process as a part of the game cooking (called by PrecompileAssembliesStep in Editor). - /// - public static void Run() + internal static void RunClassLibStripping() + { + var outputPath = Configuration.BinariesFolder; // Provided by DeployDataStep + if (!Directory.Exists(outputPath)) + throw new Exception("Missing output folder " + outputPath); + var dotnetOutputPath = Path.Combine(outputPath, "Dotnet"); + if (!Directory.Exists(dotnetOutputPath)) + return; + + // Find input files + var inputFiles = Directory.GetFiles(outputPath, "*.dll", SearchOption.TopDirectoryOnly).ToList(); + inputFiles.RemoveAll(FilterAssembly); + for (int i = 0; i < inputFiles.Count; i++) + inputFiles[i] = Utilities.NormalizePath(inputFiles[i]); + inputFiles.Sort(); + + // Peek class library folder + var coreLibPaths = Directory.GetFiles(dotnetOutputPath, "System.Private.CoreLib.dll", SearchOption.AllDirectories); + if (coreLibPaths.Length != 1) + throw new Exception("Invalid C# class library setup in " + dotnetOutputPath); + var dotnetLibPath = Utilities.NormalizePath(Path.GetDirectoryName(coreLibPaths[0])); + + using (var assemblyResolver = new MonoCecil.BasicAssemblyResolver()) + { + assemblyResolver.SearchDirectories.Add(outputPath); + assemblyResolver.SearchDirectories.Add(dotnetLibPath); + + // Build list of all used assemblies + var assembliesPaths = new List(); + foreach (var inputFile in inputFiles) + { + try + { + BuildAssembliesList(inputFile, assembliesPaths, assemblyResolver, string.Empty, null); + } + catch (Exception) + { + } + } + + // Get all C# class lib assemblies + var stdLibFiles = Directory.GetFiles(dotnetLibPath, "*.dll", SearchOption.TopDirectoryOnly).ToList(); + stdLibFiles.RemoveAll(FilterAssembly); + for (int i = 0; i < stdLibFiles.Count; i++) + stdLibFiles[i] = Utilities.NormalizePath(stdLibFiles[i]); + + // Remove any unused C# class lib assemblies + long sizeOfRemoved = 0; + foreach (var file in stdLibFiles) + { + if (!assembliesPaths.Contains(file)) + { + Log.Info("Removing unused C# assembly: " + Path.GetFileName(file)); + var fileInfo = new FileInfo(file); + sizeOfRemoved += fileInfo.Length; + fileInfo.Delete(); + } + } + Log.Info("Removed C# assemblies size: " + (sizeOfRemoved / (1024 * 1024) + " MB")); + } + } + + internal static void RunAOT() { var platform = Configuration.BuildPlatforms[0]; var arch = Configuration.BuildArchitectures[0]; @@ -435,19 +504,22 @@ namespace Flax.Build internal static void BuildAssembliesList(string assemblyPath, AssemblyNameReference assemblyReference, List outputList, IAssemblyResolver assemblyResolver, string callerPath, HashSet warnings) { - // Detect usage of C# API that is not supported in AOT builds var assemblyName = Path.GetFileName(assemblyPath); - if (assemblyReference.Name.Contains("System.Linq.Expressions") || - assemblyReference.Name.Contains("System.Reflection.Emit") || - assemblyReference.Name.Contains("System.Reflection.Emit.ILGeneration")) + if (warnings != null) { - if (!warnings.Contains(assemblyReference.Name)) + // Detect usage of C# API that is not supported in AOT builds + if (assemblyReference.Name.Contains("System.Linq.Expressions") || + assemblyReference.Name.Contains("System.Reflection.Emit") || + assemblyReference.Name.Contains("System.Reflection.Emit.ILGeneration")) { - warnings.Add(assemblyReference.Name); - if (callerPath.Length != 0) - Log.Warning($"Warning! Assembly '{assemblyName}' (referenced by '{callerPath}') references '{assemblyReference.Name}' which is not supported in AOT builds and might cause error (due to lack of JIT at runtime)."); - else - Log.Warning($"Warning! Assembly '{assemblyName}' references '{assemblyReference.Name}' which is not supported in AOT builds and might cause error (due to lack of JIT at runtime)."); + if (!warnings.Contains(assemblyReference.Name)) + { + warnings.Add(assemblyReference.Name); + if (callerPath.Length != 0) + Log.Warning($"Warning! Assembly '{assemblyName}' (referenced by '{callerPath}') references '{assemblyReference.Name}' which is not supported in AOT builds and might cause error (due to lack of JIT at runtime)."); + else + Log.Warning($"Warning! Assembly '{assemblyName}' references '{assemblyReference.Name}' which is not supported in AOT builds and might cause error (due to lack of JIT at runtime)."); + } } } if (callerPath.Length != 0) diff --git a/Source/Tools/Flax.Build/Utilities/MonoCecil.cs b/Source/Tools/Flax.Build/Utilities/MonoCecil.cs index 00f9fbf9b..0301e33d2 100644 --- a/Source/Tools/Flax.Build/Utilities/MonoCecil.cs +++ b/Source/Tools/Flax.Build/Utilities/MonoCecil.cs @@ -22,6 +22,19 @@ namespace Flax.Build public HashSet SearchDirectories = new(); + public AssemblyDefinition Resolve(string path) + { + var name = Path.GetFileNameWithoutExtension(path); + foreach (var e in _cache) + { + if (string.Equals(e.Value.Name.Name, name, StringComparison.OrdinalIgnoreCase)) + return e.Value; + } + var assembly = ModuleDefinition.ReadModule(path, new ReaderParameters()).Assembly; + _cache[assembly.FullName] = assembly; + return assembly; + } + public AssemblyDefinition Resolve(AssemblyNameReference name) { return Resolve(name, new ReaderParameters());