mirror of
https://github.com/AssetRipper/Cpp2IL.git
synced 2026-11-13 04:26:33 -10:00
249 lines
13 KiB
C#
249 lines
13 KiB
C#
using System.Text;
|
|
using System.Text.Json;
|
|
using Cpp2IL.Core.Api;
|
|
using Cpp2IL.Core.Logging;
|
|
using Cpp2IL.Core.Model.Contexts;
|
|
using Cpp2IL.Plugin.BuildReport.Model;
|
|
using LibCpp2IL;
|
|
using LibCpp2IL.Metadata;
|
|
|
|
namespace Cpp2IL.Plugin.BuildReport;
|
|
|
|
public class BuildReportOutputFormat : Cpp2IlOutputFormat
|
|
{
|
|
public override string OutputFormatId => "buildreport";
|
|
public override string OutputFormatName => "IL2CPP Build Report";
|
|
|
|
public override void OnOutputFormatSelected()
|
|
{
|
|
//We want readable information for metadata sizes in the build report
|
|
ClassReadingBinaryReader.EnableReadableSizeInformation = true;
|
|
}
|
|
|
|
public override void DoOutput(ApplicationAnalysisContext context, string outputRoot)
|
|
{
|
|
//This output format serves as a way to see what's taking up space (and thus, presumably, build time) in your game
|
|
//This is one of the few output formats that is targeted not at reverse-engineers, but rather at the game developer
|
|
//It's probably useless to any players or modders.
|
|
|
|
//Things that can be logged
|
|
// - Binary
|
|
// - size of il2cpp section, resources, etc
|
|
// - generic variants, most varied types, etc
|
|
// - approximate amount of code contained within each managed assembly/type, etc.
|
|
// - Metadata
|
|
// - number of types, methods, etc
|
|
// - sizes of various metadata streams (string literals etc)
|
|
|
|
|
|
//Data analysis
|
|
|
|
var binarySize = context.Binary.RawLength;
|
|
var metadataInBinarySize = context.Binary.InBinaryMetadataSize;
|
|
|
|
//Number of variants per generic base type
|
|
var genericMethodDataByBaseMd = new Dictionary<Il2CppMethodDefinition, GenericMethodData>();
|
|
|
|
foreach (var mSpec in context.Binary.AllGenericMethodSpecs)
|
|
{
|
|
var md = mSpec.MethodDefinition!;
|
|
|
|
if(!genericMethodDataByBaseMd.TryGetValue(md, out var gmd))
|
|
{
|
|
gmd = genericMethodDataByBaseMd[md] = new(context, md); //This populates generic method instruction size and variant pointers
|
|
}
|
|
|
|
gmd.NumVariants++;
|
|
}
|
|
|
|
var numberOfGenericVariants = genericMethodDataByBaseMd.Values.Sum(gmd => gmd.NumVariants);
|
|
var numberOfGenericPointers = genericMethodDataByBaseMd.Values.Sum(gmd => gmd.CountedVariantAddresses.Count);
|
|
var sizeOfAllGenerics = genericMethodDataByBaseMd.Sum(m => (long) m.Value.TotalSizeInstructions);
|
|
|
|
var mostUsedGenericMethods = genericMethodDataByBaseMd.OrderByDescending(kvp => kvp.Value.NumVariants).Take(100).ToList();
|
|
var mostSpaceConsumingGenericMethods = genericMethodDataByBaseMd.OrderByDescending(kvp => kvp.Value.TotalSizeInstructions).Take(100).ToList();
|
|
|
|
var methodsBySize = context.AllTypes.SelectMany(t => t.Methods).Where(m => m is not InjectedMethodAnalysisContext).Where(m => m.UnderlyingPointer != 0).ToDictionary(m => m, m => m.RawBytes.Length);
|
|
var numberOfNonGenerics = methodsBySize.Count;
|
|
var sizeOfAllNonGenerics = methodsBySize.Sum(m => m.Value);
|
|
var largestMethods = methodsBySize.OrderByDescending(kvp => kvp.Value).Take(100).ToList();
|
|
|
|
var inBinaryMetadataObjectsBySize = (List<KeyValuePair<Type?, int>>) context.Binary.BytesReadPerClass.OrderByDescending(kvp => kvp.Value).ToList()!;
|
|
var inBinaryMetadataUnspecifiedSize = context.Binary.InBinaryMetadataSize - inBinaryMetadataObjectsBySize.Sum(kvp => kvp.Value);
|
|
inBinaryMetadataObjectsBySize.Add(new(null, inBinaryMetadataUnspecifiedSize));
|
|
|
|
var metadataFileObjectsBySize = (List<KeyValuePair<Type?, int>>) context.Metadata.BytesReadPerClass.OrderByDescending(kvp => kvp.Value).ToList()!;
|
|
var metadataFileUnspecifiedSize = context.Metadata.Length - metadataFileObjectsBySize.Sum(kvp => kvp.Value);
|
|
metadataFileObjectsBySize.Add(new(null, (int) metadataFileUnspecifiedSize));
|
|
|
|
var attributeGeneratorsBySize = context.Assemblies
|
|
.Cast<HasCustomAttributes>()
|
|
.Concat(context.AllTypes)
|
|
.Concat(context.AllTypes.SelectMany(t => t.Methods))
|
|
.Concat(context.AllTypes.SelectMany(t => t.Fields))
|
|
.Concat(context.AllTypes.SelectMany(t => t.Events))
|
|
.Concat(context.AllTypes.SelectMany(t => t.Properties))
|
|
.Where(m => m.CaCacheGeneratorAnalysis != null)
|
|
.Select(m => m.CaCacheGeneratorAnalysis!)
|
|
.Where(a => a.UnderlyingPointer != 0)
|
|
.ToDictionary(a => a, a => a.RawBytes.Length);
|
|
var sizeOfAllCaGenerators = attributeGeneratorsBySize.Sum(a => a.Value);
|
|
var largestCaGenerators = attributeGeneratorsBySize.OrderByDescending(kvp => kvp.Value).Take(100).ToList();
|
|
|
|
var spaceUsedPerAssembly = new Dictionary<AssemblyAnalysisContext, int>();
|
|
foreach (var assemblyAnalysisContext in context.Assemblies)
|
|
{
|
|
spaceUsedPerAssembly[assemblyAnalysisContext] = 0;
|
|
|
|
//Methods
|
|
spaceUsedPerAssembly[assemblyAnalysisContext] += assemblyAnalysisContext.Types.SelectMany(t => t.Methods).Sum(m => methodsBySize.GetOrDefault(m, 0));
|
|
|
|
//Generics
|
|
var asmDef = assemblyAnalysisContext.Definition.Image;
|
|
foreach (var genericMethodData in genericMethodDataByBaseMd)
|
|
{
|
|
if(genericMethodData.Key.DeclaringType?.DeclaringAssembly != asmDef)
|
|
continue;
|
|
|
|
spaceUsedPerAssembly[assemblyAnalysisContext] += genericMethodData.Value.TotalSizeInstructions;
|
|
}
|
|
}
|
|
|
|
var sortedUsageByAssembly = spaceUsedPerAssembly.OrderByDescending(kvp => kvp.Value).ToList();
|
|
|
|
var unclassifiedSize = binarySize - sizeOfAllGenerics - sizeOfAllNonGenerics - sizeOfAllCaGenerators - metadataInBinarySize;
|
|
|
|
//Generate build report
|
|
|
|
var ret = new StringBuilder();
|
|
|
|
ret.AppendLine($"IL2CPP Binary report generated at {DateTime.Now:f}:");
|
|
ret.AppendLine();
|
|
ret.AppendLine($"Binary size: {binarySize} bytes ({binarySize / 1024f / 1024:f2}MB)");
|
|
ret.AppendLine($" Of which {numberOfGenericPointers} generic method bodies: {sizeOfAllGenerics} bytes ({sizeOfAllGenerics / 1024f / 1024:f2}MB, {(double) sizeOfAllGenerics / binarySize:p})");
|
|
ret.AppendLine($" Of which {numberOfNonGenerics} non-generic method bodies: {sizeOfAllNonGenerics} bytes ({sizeOfAllNonGenerics / 1024f / 1024:f2}MB, {(double) sizeOfAllNonGenerics / binarySize:p})");
|
|
ret.AppendLine($" Of which Custom Attribute generator bodies: {sizeOfAllCaGenerators} bytes ({sizeOfAllCaGenerators / 1024f / 1024:f2}MB, {(double) sizeOfAllCaGenerators / binarySize:p})");
|
|
ret.AppendLine($" Of which in-binary il2cpp metadata: {metadataInBinarySize} bytes ({metadataInBinarySize / 1024f / 1024:f2}MB, {(double) metadataInBinarySize / binarySize:p})");
|
|
ret.AppendLine($" Of which C++ functions (il2cpp api functions, etc) and misc data: {unclassifiedSize} bytes ({(unclassifiedSize) / 1024f / 1024:f2}MB, {(double) unclassifiedSize / binarySize:p})");
|
|
ret.AppendLine();
|
|
ret.AppendLine($"Total number of generic variants: {numberOfGenericVariants} ({numberOfGenericPointers} after generic merging)");
|
|
ret.AppendLine(" Top 100 most varied generic methods:");
|
|
ret.AppendLine();
|
|
|
|
foreach (var gMethod in mostUsedGenericMethods)
|
|
ret.AppendLine($" {GetMethodName(gMethod.Key)}, with {gMethod.Value.NumVariants} variants, reduced to {gMethod.Value.CountedVariantAddresses.Count} variants by generic sharing, totalling {gMethod.Value.TotalSizeInstructions} bytes of instructions");
|
|
|
|
ret.AppendLine();
|
|
ret.AppendLine(" Top 100 most space-consuming generic methods:");
|
|
|
|
foreach (var gMethod in mostSpaceConsumingGenericMethods)
|
|
ret.AppendLine($" {GetMethodName(gMethod.Key)}, with {gMethod.Value.NumVariants} variants, reduced to {gMethod.Value.CountedVariantAddresses.Count} variants by generic sharing, totalling {gMethod.Value.TotalSizeInstructions} bytes ({gMethod.Value.TotalSizeInstructions / 1024f / 1024:f2}MB) of instructions");
|
|
|
|
ret.AppendLine();
|
|
ret.AppendLine($"Total number of non-generic methods: {methodsBySize.Count}");
|
|
ret.AppendLine(" Top 100 largest non-generic methods:");
|
|
|
|
foreach (var method in largestMethods)
|
|
ret.AppendLine($" {GetMethodName(method.Key.Definition!)}, with {method.Value} bytes ({method.Value / 1024f / 1024:f2}MB)");
|
|
|
|
ret.AppendLine();
|
|
ret.AppendLine($"Total number of Custom Attribute generators: {attributeGeneratorsBySize.Count}");
|
|
ret.AppendLine(" Top 100 largest CA generators:");
|
|
|
|
foreach (var caGenerator in largestCaGenerators)
|
|
ret.AppendLine($" Generator for {caGenerator.Key.AssociatedMember}, with {caGenerator.Value} bytes ({caGenerator.Value / 1024f / 1024:f2}MB)");
|
|
|
|
ret.AppendLine();
|
|
ret.AppendLine(" Usage per assembly, sorted by space used (by method bodies only, no metadata):");
|
|
|
|
foreach (var kvp in sortedUsageByAssembly)
|
|
ret.AppendLine($" {kvp.Key.Definition.AssemblyName.Name}: {kvp.Value / 1024f / 1024:f3}MB ({(double) kvp.Value / binarySize:P} of binary)");
|
|
|
|
ret.AppendLine();
|
|
ret.AppendLine(" In-Binary Metadata breakdown:");
|
|
|
|
foreach (var kvp in inBinaryMetadataObjectsBySize)
|
|
if(kvp.Value >= 0)
|
|
ret.AppendLine($" {kvp.Key?.Name ?? "Unspecified"}: {kvp.Value} bytes ({kvp.Value / 1024f / 1024:f2}MB)");
|
|
|
|
ret.AppendLine();
|
|
ret.AppendLine($" Metadata file (global-metadata.dat) breakdown (total size {context.Metadata.Length} bytes, {context.Metadata.Length / 1024f / 1024:f2}MB):");
|
|
|
|
foreach (var kvp in metadataFileObjectsBySize)
|
|
if(kvp.Value >= 0)
|
|
ret.AppendLine($" {kvp.Key?.Name ?? "Unspecified"}: {kvp.Value} bytes ({kvp.Value / 1024f / 1024:f2}MB)");
|
|
|
|
//Save output
|
|
var outputPath = Path.Combine(outputRoot, "buildreport.txt");
|
|
File.WriteAllText(outputPath, ret.ToString());
|
|
|
|
Logger.InfoNewline($"Wrote human-readable build report to {outputPath}", "BuildReportOutputFormat");
|
|
|
|
//Save JSON output
|
|
var jsonOutputPath = Path.Combine(outputRoot, "buildreport.json");
|
|
|
|
var outputData = new OutputData();
|
|
|
|
outputData.Binary.RawReadableClasses = inBinaryMetadataObjectsBySize.Select(kvp => new OutputReadableClassData
|
|
{
|
|
BytesUsed = kvp.Value,
|
|
TypeName = kvp.Key?.Name ?? "Unspecified",
|
|
}).ToArray();
|
|
|
|
outputData.GlobalMetadata.RawReadableClasses = metadataFileObjectsBySize.Select(kvp => new OutputReadableClassData
|
|
{
|
|
BytesUsed = kvp.Value,
|
|
TypeName = kvp.Key?.Name ?? "Unspecified",
|
|
}).ToArray();
|
|
|
|
var jsonText = JsonSerializer.Serialize(outputData, new JsonSerializerOptions() {WriteIndented = true});
|
|
File.WriteAllText(jsonOutputPath, jsonText);
|
|
|
|
Logger.InfoNewline($"Wrote JSON build report to {jsonOutputPath}", "BuildReportOutputFormat");
|
|
}
|
|
|
|
private static string GetMethodName(Il2CppMethodDefinition md)
|
|
{
|
|
return $"{md.DeclaringType!.FullName}::{md.Name}({string.Join(", ", md.Parameters!.AsEnumerable())})";
|
|
}
|
|
|
|
private class GenericMethodData
|
|
{
|
|
public int NumVariants;
|
|
public HashSet<ulong> CountedVariantAddresses = new();
|
|
public HashSet<string> FullNames = new();
|
|
public int TotalSizeInstructions;
|
|
public List<OutputMethodData> OutputMethodData = new();
|
|
|
|
public GenericMethodData(ApplicationAnalysisContext context, Il2CppMethodDefinition md)
|
|
{
|
|
if(!context.Binary.ConcreteGenericMethods.TryGetValue(md, out var concreteGenericMethods))
|
|
return;
|
|
|
|
foreach (var cpp2IlMethodRef in concreteGenericMethods)
|
|
{
|
|
if(CountedVariantAddresses.Contains(cpp2IlMethodRef.GenericVariantPtr))
|
|
continue;
|
|
|
|
var outputMethod = new OutputMethodData()
|
|
{
|
|
FullName = cpp2IlMethodRef.ToString(),
|
|
NonGenericVersionFullName = cpp2IlMethodRef.BaseMethod.HumanReadableSignature,
|
|
IsRemovedByGenericSharing = true,
|
|
};
|
|
|
|
OutputMethodData.Add(outputMethod);
|
|
|
|
if(!context.ConcreteGenericMethodsByRef.TryGetValue(cpp2IlMethodRef, out var concreteAnalysisContext))
|
|
continue; //Eliminated by generic sharing
|
|
|
|
outputMethod.IsRemovedByGenericSharing = false;
|
|
outputMethod.MachineCodeSizeBytes = concreteAnalysisContext.RawBytes.Length;
|
|
|
|
CountedVariantAddresses.Add(cpp2IlMethodRef.GenericVariantPtr);
|
|
FullNames.Add(cpp2IlMethodRef.ToString());
|
|
TotalSizeInstructions += concreteAnalysisContext.RawBytes.Length;
|
|
}
|
|
}
|
|
}
|
|
} |