Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Mono] Add the capability of trimming IL code of individual methods #86722

Merged
merged 27 commits into from
Jun 16, 2023
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
0c4bf19
Add the capability of trimming individual methods
fanyang-mono May 24, 2023
d817f30
Fix build errors
fanyang-mono May 24, 2023
634ef1e
Remove printf's
fanyang-mono May 24, 2023
b766c33
Add the option to use compiled-methods-outfile
fanyang-mono May 25, 2023
10b3b01
Avoid trimming shared methods when they are still in use
fanyang-mono May 26, 2023
2bc884e
Add parameter description
fanyang-mono May 26, 2023
b23d833
Add the option to trim compiled methods
fanyang-mono May 26, 2023
f6d99d7
Address review feedback
fanyang-mono May 31, 2023
69bf960
Add metadata MethodTokenFile to CompiledAssemblies
fanyang-mono May 31, 2023
1f0fcbb
Add GUID checks and use metadata of assemblies
fanyang-mono Jun 2, 2023
eaa939c
Create smaller functions and use hex value
fanyang-mono Jun 2, 2023
f517e79
Update src/tasks/AotCompilerTask/MonoAOTCompiler.cs
fanyang-mono Jun 5, 2023
ae66d52
Move parameter validation code
fanyang-mono Jun 5, 2023
30306e4
Update src/tasks/MonoTargetsTasks/ILStrip/ILStrip.cs
fanyang-mono Jun 5, 2023
56a781a
Update src/tasks/MonoTargetsTasks/ILStrip/ILStrip.cs
fanyang-mono Jun 5, 2023
78981c8
Update src/tasks/MonoTargetsTasks/ILStrip/ILStrip.cs
fanyang-mono Jun 5, 2023
b519b4c
Update src/tasks/MonoTargetsTasks/ILStrip/ILStrip.cs
fanyang-mono Jun 5, 2023
3322022
Update src/tasks/MonoTargetsTasks/ILStrip/ILStrip.cs
fanyang-mono Jun 5, 2023
1a83902
Update src/tasks/MonoTargetsTasks/ILStrip/ILStrip.cs
fanyang-mono Jun 5, 2023
f1b2753
Update src/tasks/MonoTargetsTasks/ILStrip/ILStrip.cs
fanyang-mono Jun 5, 2023
328e9be
Update src/tasks/MonoTargetsTasks/ILStrip/ILStrip.cs
fanyang-mono Jun 5, 2023
10226b7
Add more error handling
fanyang-mono Jun 5, 2023
fc088bd
Provide a list of trimmed assemblies as output
fanyang-mono Jun 12, 2023
3aad84a
Update src/tasks/MonoTargetsTasks/ILStrip/ILStrip.cs
fanyang-mono Jun 13, 2023
91803d9
Address coding style feedbacks
fanyang-mono Jun 13, 2023
8cb9621
Fix var anmes
fanyang-mono Jun 13, 2023
ee2b37f
Delete trimmed assemblies after copy
fanyang-mono Jun 15, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions src/mono/mono/mini/aot-compiler.c
Original file line number Diff line number Diff line change
Expand Up @@ -9828,8 +9828,9 @@ compile_method (MonoAotCompile *acfg, MonoMethod *method)
mono_atomic_inc_i32 (&acfg->stats.ccount);

if (acfg->aot_opts.compiled_methods_outfile && acfg->compiled_methods_outfile != NULL) {
if (!mono_method_is_generic_impl (method) && method->token != 0)
fprintf (acfg->compiled_methods_outfile, "%x\n", method->token);
if (!mono_method_is_generic_impl (method) && method->token != 0) {
fprintf (acfg->compiled_methods_outfile, "%d\n", method->token);
fanyang-mono marked this conversation as resolved.
Show resolved Hide resolved
}
}
}

Expand Down Expand Up @@ -14830,6 +14831,8 @@ aot_assembly (MonoAssembly *ass, guint32 jit_opts, MonoAotOptions *aot_options)

if (acfg->aot_opts.compiled_methods_outfile && acfg->dedup_phase != DEDUP_COLLECT) {
acfg->compiled_methods_outfile = fopen (acfg->aot_opts.compiled_methods_outfile, "w+");
fprintf(acfg->compiled_methods_outfile, "%s\n", ass->image->filename);

fanyang-mono marked this conversation as resolved.
Show resolved Hide resolved
if (!acfg->compiled_methods_outfile)
aot_printerrf (acfg, "Unable to open compiled-methods-outfile specified file %s\n", acfg->aot_opts.compiled_methods_outfile);
}
Expand Down
21 changes: 21 additions & 0 deletions src/mono/sample/HelloWorld/HelloWorld.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
LibraryFormat="$(_AotLibraryFormat)"
Assemblies="@(AotInputAssemblies)"
OutputDir="$(PublishDir)"
CollectCompiledMethods="$(StripILCode)"
CompiledMethodsOutputPath="$(MethodTokenFilePath)"
fanyang-mono marked this conversation as resolved.
Show resolved Hide resolved
IntermediateOutputPath="$(IntermediateOutputPath)"
UseAotDataFile="$(UseAotDataFile)"
CacheFilePath="$(IntermediateOutputPath)aot_compiler_cache.json"
Expand All @@ -35,4 +37,23 @@
<Output TaskParameter="CompiledAssemblies" ItemName="BundleAssemblies" />
</MonoAOTCompiler>
</Target>

<UsingTask TaskName="ILStrip"
AssemblyFile="$(MonoTargetsTasksAssemblyPath)" />

<Target Name="StripILCode" Condition="'$(StripILCode)' == 'true'" AfterTargets="AOTCompileApp">
fanyang-mono marked this conversation as resolved.
Show resolved Hide resolved
<PropertyGroup>
<TrimIndividualMethods>true</TrimIndividualMethods>
ivanpovazan marked this conversation as resolved.
Show resolved Hide resolved
</PropertyGroup>

<ItemGroup>
<MethodTokenFiles Include="$(MethodTokenFilePath)/*.txt" />
ivanpovazan marked this conversation as resolved.
Show resolved Hide resolved
</ItemGroup>

<ILStrip
TrimIndividualMethods="$(TrimIndividualMethods)"
MethodTokenFiles="@(MethodTokenFiles)"
AssemblyPath="$(PublishDir)">
</ILStrip>
</Target>
</Project>
4 changes: 4 additions & 0 deletions src/mono/sample/HelloWorld/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ MONO_CONFIG?=Debug
MONO_ARCH?=$(shell . $(TOP)eng/native/init-os-and-arch.sh && echo $${arch})
TARGET_OS?=$(shell . $(TOP)eng/native/init-os-and-arch.sh && echo $${os})
AOT?=false
StripILCode?=false
MethodTokenFilePath?= #<path-to-a-writable-directory>

#NET_TRACE_PATH=<path-to-trace-of-sample>
#PGO_BINARY_PATH=<path-to-dotnet-pgo-executable>
Expand All @@ -18,6 +20,8 @@ publish:
-c $(MONO_CONFIG) \
-r $(TARGET_OS)-$(MONO_ARCH) \
/p:RunAOTCompilation=$(AOT) \
/p:StripILCode=$(StripILCode) \
/p:MethodTokenFilePath=$(MethodTokenFilePath) \
'/p:NetTracePath="$(NET_TRACE_PATH)"' \
'/p:PgoBinaryPath="$(PGO_BINARY_PATH)"' \
'/p:MibcProfilePath="$(MIBC_PROFILE_PATH)"'
Expand Down
30 changes: 30 additions & 0 deletions src/tasks/AotCompilerTask/MonoAOTCompiler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,16 @@ public class MonoAOTCompiler : Microsoft.Build.Utilities.Task
/// </summary>
public bool UseDwarfDebug { get; set; }

/// <summary>
/// Instructs the AOT compiler to print the list of aot compiled methods
/// </summary>
public bool CollectCompiledMethods { get; set; }

/// <summary>
/// Directory to store the aot output when using switch compiled-methods-outfile
/// </summary>
public string? CompiledMethodsOutputPath { get; set; }
fanyang-mono marked this conversation as resolved.
Show resolved Hide resolved

/// <summary>
/// File to use for profile-guided optimization, *only* the methods described in the file will be AOT compiled.
/// </summary>
Expand Down Expand Up @@ -711,6 +721,26 @@ private PrecompileArguments GetPrecompileArgumentsFor(ITaskItem assemblyItem, st
aotArgs.Add("dedup-skip");
}

if (CollectCompiledMethods)
{
if (string.IsNullOrEmpty(CompiledMethodsOutputPath))
fanyang-mono marked this conversation as resolved.
Show resolved Hide resolved
{
Log.LogMessage(MessageImportance.Low, "Skipping collecting the list of aot compiled methods, cause the value of CompiledMethodsOutputPath is empty.");
fanyang-mono marked this conversation as resolved.
Show resolved Hide resolved
}
else
{
if (!Directory.Exists(CompiledMethodsOutputPath))
{
Directory.CreateDirectory(CompiledMethodsOutputPath);
}
fanyang-mono marked this conversation as resolved.
Show resolved Hide resolved
string assemblyFileName = Path.GetFileName(assembly);
string assemblyName = assemblyFileName.Replace(".", "_");
string outputFileName = assemblyName + "_compiled_methods.txt";
string outputFilePath = Path.Combine(CompiledMethodsOutputPath, outputFileName);
aotArgs.Add($"compiled-methods-outfile={outputFilePath}");
}
}

// compute output mode and file names
if (parsedAotMode == MonoAotMode.LLVMOnly || parsedAotMode == MonoAotMode.LLVMOnlyInterp)
{
Expand Down
199 changes: 180 additions & 19 deletions src/tasks/MonoTargetsTasks/ILStrip/ILStrip.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,45 +12,86 @@
using CilStrip.Mono.Cecil.Binary;
using CilStrip.Mono.Cecil.Cil;
using CilStrip.Mono.Cecil.Metadata;
using System.Reflection.Metadata;
fanyang-mono marked this conversation as resolved.
Show resolved Hide resolved
using System.Reflection.Metadata.Ecma335;
using System.Reflection.PortableExecutable;
using System.Buffers;

public class ILStrip : Microsoft.Build.Utilities.Task
{
/// <summary>
/// Assemblies to be stripped.
/// The assemblies will be modified in place if OutputPath metadata is not set.
/// </summary>
[Required]
public ITaskItem[] Assemblies { get; set; } = Array.Empty<ITaskItem>();

/// <summary>
/// Disable parallel stripping
/// </summary>
public bool DisableParallelStripping { get; set; }

/// <summary>
/// Enable the feature of trimming indiviual methods
/// </summary>
public bool TrimIndividualMethods { get; set; }

/// <summary>
/// Methods to be trimmed, identified by method token
/// </summary>
public ITaskItem[] MethodTokenFiles { get; set; } = Array.Empty<ITaskItem>();

public override bool Execute()
radical marked this conversation as resolved.
Show resolved Hide resolved
{
if (Assemblies.Length == 0)
if (!TrimIndividualMethods)
{
throw new ArgumentException($"'{nameof(Assemblies)}' is required.", nameof(Assemblies));
}
if (Assemblies.Length == 0)
lambdageek marked this conversation as resolved.
Show resolved Hide resolved
{
throw new ArgumentException($"'{nameof(Assemblies)}' is required.", nameof(Assemblies));
}

int allowedParallelism = DisableParallelStripping ? 1 : Math.Min(Assemblies.Length, Environment.ProcessorCount);
if (BuildEngine is IBuildEngine9 be9)
allowedParallelism = be9.RequestCores(allowedParallelism);
ParallelLoopResult result = Parallel.ForEach(Assemblies,
new ParallelOptions { MaxDegreeOfParallelism = allowedParallelism },
(assemblyItem, state) =>
{
if (!StripAssembly(assemblyItem))
state.Stop();
});

if (!result.IsCompleted && !Log.HasLoggedErrors)
{
Log.LogError("Unknown failure occurred while IL stripping assemblies. Check logs to get more details.");
int allowedParallelism = DisableParallelStripping ? 1 : Math.Min(Assemblies.Length, Environment.ProcessorCount);
if (BuildEngine is IBuildEngine9 be9)
allowedParallelism = be9.RequestCores(allowedParallelism);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
int allowedParallelism = DisableParallelStripping ? 1 : Math.Min(Assemblies.Length, Environment.ProcessorCount);
if (BuildEngine is IBuildEngine9 be9)
allowedParallelism = be9.RequestCores(allowedParallelism);
int allowedParallelism = DisableParallelStripping ? 1 : BuildEngine9.RequestCores(Math.Min(Assemblies.Length, Environment.ProcessorCount));

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I hit an error when applying your change

System.ArgumentOutOfRangeException: Specified argument was out of the range of valid values.

ParallelLoopResult result = Parallel.ForEach(Assemblies,
new ParallelOptions { MaxDegreeOfParallelism = allowedParallelism },
(assemblyItem, state) =>
{
if (!StripAssembly(assemblyItem))
state.Stop();
});

if (!result.IsCompleted && !Log.HasLoggedErrors)
{
Log.LogError("Unknown failure occurred while IL stripping assemblies. Check logs to get more details.");
}

return !Log.HasLoggedErrors;
}
else
{
if (MethodTokenFiles.Length == 0)
{
throw new ArgumentException($"'{nameof(MethodTokenFiles)}' is required.", nameof(MethodTokenFiles));
}

int allowedParallelism = DisableParallelStripping ? 1 : Math.Min(MethodTokenFiles.Length, Environment.ProcessorCount);
if (BuildEngine is IBuildEngine9 be9)
allowedParallelism = be9.RequestCores(allowedParallelism);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
int allowedParallelism = DisableParallelStripping ? 1 : Math.Min(MethodTokenFiles.Length, Environment.ProcessorCount);
if (BuildEngine is IBuildEngine9 be9)
allowedParallelism = be9.RequestCores(allowedParallelism);
int allowedParallelism = DisableParallelStripping ? 1 : BuildEngine9.RequestCores(Math.Min(Assemblies.Length, Environment.ProcessorCount));

ParallelLoopResult result = Parallel.ForEach(MethodTokenFiles,
new ParallelOptions { MaxDegreeOfParallelism = allowedParallelism },
(methodTokenFileItem, state) =>
{
if (!TrimMethods(methodTokenFileItem))
state.Stop();
});

return !Log.HasLoggedErrors;
if (!result.IsCompleted && !Log.HasLoggedErrors)
{
Log.LogError("Unknown failure occurred while IL stripping assemblies. Check logs to get more details.");
}

return !Log.HasLoggedErrors;
}
}

private bool StripAssembly(ITaskItem assemblyItem)
Expand Down Expand Up @@ -81,4 +122,124 @@ private bool StripAssembly(ITaskItem assemblyItem)
return true;
}

private bool TrimMethods(ITaskItem methodTokenFileItem)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function is doing too much. It has at least 6 responsibilities:

  1. Open the token file and read the first line
  2. Determine the names for the input assembly, and the destination where it will be written
  3. Open the assembly for reading
  4. Read the metadata token file and the assembly method definitions and compute the hash of methods to zero out
  5. Open the memory stream for writing and actually zero out the methods
  6. Copy the memory stream to the destination file
  7. Move files to the final place

Each of these things should be a separate function.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about now?

{
string methodTokenFile = methodTokenFileItem.ItemSpec;
if (!File.Exists(methodTokenFile))
fanyang-mono marked this conversation as resolved.
Show resolved Hide resolved
{
Log.LogMessage(MessageImportance.Low, $"[ILStrip] {methodTokenFile} doesn't exit.");
fanyang-mono marked this conversation as resolved.
Show resolved Hide resolved
return true;
}

using (StreamReader sr = new StreamReader(methodTokenFile))
fanyang-mono marked this conversation as resolved.
Show resolved Hide resolved
{
string? assemblyFilePath = sr.ReadLine();
fanyang-mono marked this conversation as resolved.
Show resolved Hide resolved
if (string.IsNullOrEmpty(assemblyFilePath))
{
return true;
fanyang-mono marked this conversation as resolved.
Show resolved Hide resolved
}

if (!File.Exists(assemblyFilePath))
fanyang-mono marked this conversation as resolved.
Show resolved Hide resolved
{
Log.LogMessage(MessageImportance.Low, $"[ILStrip] {assemblyFilePath} doesn't exit.");
fanyang-mono marked this conversation as resolved.
Show resolved Hide resolved
return true;
}

string? line = sr.ReadLine();
if (!string.IsNullOrEmpty(line))
{
string? assemblyPath = Path.GetDirectoryName(assemblyFilePath);
string? assemblyName = Path.GetFileNameWithoutExtension(assemblyFilePath);
string trimmedAssemblyFilePath;
string newName_assemblyFilePath;
if (string.IsNullOrEmpty(assemblyPath))
{
trimmedAssemblyFilePath = assemblyName + "_new.dll";
newName_assemblyFilePath = assemblyName + "_old.dll";
}
else
{
trimmedAssemblyFilePath = Path.Combine(assemblyPath, (assemblyName + "_new.dll"));
newName_assemblyFilePath = Path.Combine(assemblyPath, (assemblyName + "_old.dll"));
}

using (FileStream fs = File.Open(assemblyFilePath, FileMode.Open),
os = File.Open(trimmedAssemblyFilePath, FileMode.Create))
{
MemoryStream memStream = new MemoryStream((int)fs.Length);
fs.CopyTo(memStream);

fs.Position = 0;
PEReader peReader = new PEReader(fs, PEStreamOptions.LeaveOpen);
MetadataReader mr = peReader.GetMetadataReader();

Dictionary<int, int> token_to_rva = new Dictionary<int, int>();
Dictionary<int, int> method_body_uses = new Dictionary<int, int>();

foreach (MethodDefinitionHandle mdefh in mr.MethodDefinitions)
{
int methodToken = MetadataTokens.GetToken(mr, mdefh);
MethodDefinition mdef = mr.GetMethodDefinition(mdefh);
int rva = mdef.RelativeVirtualAddress;

token_to_rva.Add(methodToken, rva);

if (method_body_uses.TryGetValue(rva, out var count))
{
method_body_uses[rva]++;
}
else
{
method_body_uses.Add(rva, 1);
}
}

do
{
int methodToken2Trim = Convert.ToInt32(line);
fanyang-mono marked this conversation as resolved.
Show resolved Hide resolved
int rva2Trim = token_to_rva[methodToken2Trim];
method_body_uses[rva2Trim]--;
} while ((line = sr.ReadLine()) != null);

foreach (var kvp in method_body_uses)
{
int rva = kvp.Key;
int count = kvp.Value;
if (count == 0)
{
MethodBodyBlock mb = peReader.GetMethodBody(rva);
int methodSize = mb.Size;
int sectionIndex = peReader.PEHeaders.GetContainingSectionIndex(rva);
int relativeOffset = rva - peReader.PEHeaders.SectionHeaders[sectionIndex].VirtualAddress;
int actualLoc = peReader.PEHeaders.SectionHeaders[sectionIndex].PointerToRawData + relativeOffset;

byte[] zeroBuffer;
zeroBuffer = ArrayPool<byte>.Shared.Rent(methodSize);
for (int i = 0; i < zeroBuffer.Length; i++)
{
zeroBuffer[i] = 0x0;
}
fanyang-mono marked this conversation as resolved.
Show resolved Hide resolved

memStream.Position = actualLoc;
int firstbyte = memStream.ReadByte();
int headerFlag = firstbyte & 0b11;
int headerSize = headerFlag == 2 ? 1 : 4;

memStream.Position = actualLoc + headerSize;
memStream.Write(zeroBuffer, 0, methodSize - headerSize);

ArrayPool<byte>.Shared.Return(zeroBuffer);
}
}
memStream.Position = 0;
memStream.CopyTo(os);
}

File.Move(assemblyFilePath, newName_assemblyFilePath);
File.Move(trimmedAssemblyFilePath, assemblyFilePath);
lambdageek marked this conversation as resolved.
Show resolved Hide resolved
}
}

return true;
}
}