Skip to content

Commit a3e46fd

Browse files
[Xamarin.Android.Build.Tasks] use System.Reflection.Metadata in <ResolveAssemblies/>
Context: https://github.com/dotnet/corefx/tree/master/src/System.Reflection.Metadata/src/System/Reflection/Metadata Context: https://github.com/jonathanpeppers/Benchmarks There is a new System.Reflection.Metadata library from corefx for reading .NET assemblies. It is a bit more performant than Mono.Cecil because it is a different library with different opinions. Some notes about System.Reflection.Metadata: - SRM has a forward "reader" style API - SRM uses lots of structs, and you have to do an additional call to lookup strings generally. - SRM, as far as I have seen, doesn't have APIs to modify and write out new assemblies. - SRM only supports "portable" pdb files. - SRM is not well documented yet. To discover usage, I read source code and/or unit tests. From my benchmark above, it seems that SRM is 10x faster on Windows/.NET framework and 5x faster on macOS/Mono. So it makes sense for use to use SRM when reading assemblies (and we don't need symbols), and continue with Mono.Cecil for the linker and other things that modify assemblies. There are a few places we can take advantage of SRM, but the simplest with a reasonable impact was `ResolveAssemblies`: Before: 320 ms ResolveAssemblies 1 calls After: 112 ms ResolveAssemblies 1 calls So a ~200ms savings on this MSBuild task, which runs on *every* build. This was the Xamarin.Forms test project in this repo: a build with no changes. ~~ Changes ~~ - Added a `MetadataResolver` type, as a way to cache `PEReader` instances. This is a comparable drop-in replacement for `DirectoryAssemblyResolver`. - `MonoAndroidHelper.IsReferenceAssembly` now uses `System.Reflection.Metadata` instead of `Mono.Cecil`. This is used in a few other MSBuild tasks. - A `MetadataExtensions` provides an extension method to simplify getting the full name of a custom attribute. We can add more here as needed. The resulting code *should* be the same, except we are using SRM over Mono.Cecil. ~~ Downstream ~~ We will need to add the following assemblies to the installer: - `System.Reflection.Metadata.dll` - `System.Collections.Immutable.dll`
1 parent 3576678 commit a3e46fd

File tree

7 files changed

+195
-79
lines changed

7 files changed

+195
-79
lines changed

src/Xamarin.Android.Build.Tasks/Tasks/ResolveAssemblies.cs

Lines changed: 56 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,18 @@
11
// Copyright (C) 2011, Xamarin Inc.
22
// Copyright (C) 2010, Novell Inc.
33

4-
using System;
5-
using System.Collections.Generic;
6-
using System.Linq;
74
using Microsoft.Build.Framework;
85
using Microsoft.Build.Utilities;
9-
using Mono.Cecil;
106
using MonoDroid.Tuner;
7+
using NuGet.Frameworks;
8+
using NuGet.ProjectModel;
9+
using System;
10+
using System.Collections.Generic;
1111
using System.IO;
12+
using System.Linq;
13+
using System.Reflection.Metadata;
1214
using System.Text;
1315
using Xamarin.Android.Tools;
14-
using NuGet.Common;
15-
using NuGet.Frameworks;
16-
using NuGet.ProjectModel;
17-
18-
using Java.Interop.Tools.Cecil;
1916

2017
namespace Xamarin.Android.Tasks
2118
{
@@ -62,7 +59,7 @@ public override bool Execute ()
6259
Yield ();
6360
try {
6461
System.Threading.Tasks.Task.Run (() => {
65-
using (var resolver = new DirectoryAssemblyResolver (this.CreateTaskLogger (), loadDebugSymbols: false)) {
62+
using (var resolver = new MetadataResolver ()) {
6663
Execute (resolver);
6764
}
6865
}, Token).ContinueWith (Complete);
@@ -72,14 +69,13 @@ public override bool Execute ()
7269
}
7370
}
7471

75-
void Execute (DirectoryAssemblyResolver resolver)
72+
void Execute (MetadataResolver resolver)
7673
{
77-
foreach (var dir in ReferenceAssembliesDirectory.Split (new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries))
78-
resolver.SearchDirectories.Add (dir);
74+
var assemblies = new Dictionary<string, ITaskItem> (Assemblies.Length);
75+
foreach (var dir in ReferenceAssembliesDirectory.Split (new char [] { ';' }, StringSplitOptions.RemoveEmptyEntries))
76+
resolver.AddSearchDirectory (dir);
7977

80-
var assemblies = new Dictionary<string, ITaskItem> ();
81-
82-
var topAssemblyReferences = new List<AssemblyDefinition> ();
78+
var topAssemblyReferences = new List<string> (Assemblies.Length);
8379
var logger = new NuGetLogger((s) => {
8480
LogDebugMessage ("{0}", s);
8581
});
@@ -92,32 +88,28 @@ void Execute (DirectoryAssemblyResolver resolver)
9288
try {
9389
foreach (var assembly in Assemblies) {
9490
var assembly_path = Path.GetDirectoryName (assembly.ItemSpec);
95-
96-
if (!resolver.SearchDirectories.Contains (assembly_path))
97-
resolver.SearchDirectories.Add (assembly_path);
91+
resolver.AddSearchDirectory (assembly_path);
9892

9993
// Add each user assembly and all referenced assemblies (recursive)
100-
var assemblyDef = resolver.Load (assembly.ItemSpec);
101-
if (assemblyDef == null)
102-
throw new InvalidOperationException ("Failed to load assembly " + assembly.ItemSpec);
103-
if (MonoAndroidHelper.IsReferenceAssembly (assemblyDef)) {
94+
string resolved_assembly = resolver.Resolve (assembly.ItemSpec);
95+
if (MonoAndroidHelper.IsReferenceAssembly (resolved_assembly)) {
10496
// Resolve "runtime" library
105-
var asmFullPath = Path.GetFullPath (assembly.ItemSpec);
10697
if (lockFile != null)
107-
assemblyDef = ResolveRuntimeAssemblyForReferenceAssembly (lockFile, resolver, asmFullPath);
108-
if (lockFile == null || assemblyDef == null) {
109-
LogCodedWarning ("XA0107", asmFullPath, 0, "Ignoring {0} as it is a Reference Assembly", asmFullPath);
98+
resolved_assembly = ResolveRuntimeAssemblyForReferenceAssembly (lockFile, assembly.ItemSpec);
99+
if (lockFile == null || resolved_assembly == null) {
100+
LogCodedWarning ("XA0107", resolved_assembly, 0, "Ignoring {0} as it is a Reference Assembly", resolved_assembly);
110101
continue;
111102
}
112103
}
113-
topAssemblyReferences.Add (assemblyDef);
104+
topAssemblyReferences.Add (resolved_assembly);
114105
var taskItem = new TaskItem (assembly) {
115-
ItemSpec = Path.GetFullPath (assemblyDef.MainModule.FileName),
106+
ItemSpec = Path.GetFullPath (resolved_assembly),
116107
};
117108
if (string.IsNullOrEmpty (taskItem.GetMetadata ("ReferenceAssembly"))) {
118109
taskItem.SetMetadata ("ReferenceAssembly", taskItem.ItemSpec);
119110
}
120-
assemblies [assemblyDef.Name.Name] = taskItem;
111+
string assemblyName = Path.GetFileNameWithoutExtension (resolved_assembly);
112+
assemblies [assemblyName] = taskItem;
121113
}
122114
} catch (Exception ex) {
123115
LogError ("Exception while loading assemblies: {0}", ex);
@@ -171,7 +163,7 @@ void Execute (DirectoryAssemblyResolver resolver)
171163
readonly Dictionary<string, int> api_levels = new Dictionary<string, int> ();
172164
int indent = 2;
173165

174-
AssemblyDefinition ResolveRuntimeAssemblyForReferenceAssembly (LockFile lockFile, DirectoryAssemblyResolver resolver, string assemblyPath)
166+
string ResolveRuntimeAssemblyForReferenceAssembly (LockFile lockFile, string assemblyPath)
175167
{
176168
if (string.IsNullOrEmpty(TargetMoniker))
177169
return null;
@@ -200,16 +192,16 @@ AssemblyDefinition ResolveRuntimeAssemblyForReferenceAssembly (LockFile lockFile
200192
path = Path.Combine (folder.Path, libraryPath.Path, runtime.Path).Replace('/', Path.DirectorySeparatorChar);
201193
if (!File.Exists (path))
202194
continue;
203-
LogDebugMessage ($"Attempting to load {path}");
204-
return resolver.Load (path, forceLoad: true);
195+
return path;
205196
}
206197
return null;
207198
}
208199

209-
void AddAssemblyReferences (DirectoryAssemblyResolver resolver, Dictionary<string, ITaskItem> assemblies, AssemblyDefinition assembly, List<string> resolutionPath)
200+
void AddAssemblyReferences (MetadataResolver resolver, Dictionary<string, ITaskItem> assemblies, string assemblyPath, List<string> resolutionPath)
210201
{
211-
var assemblyName = assembly.Name.Name;
212-
var fullPath = Path.GetFullPath (assembly.MainModule.FileName);
202+
var reader = resolver.GetAssemblyReader (assemblyPath);
203+
var assembly = reader.GetAssemblyDefinition ();
204+
var assemblyName = reader.GetString (assembly.Name);
213205

214206
// Don't repeat assemblies we've already done
215207
bool topLevel = resolutionPath == null;
@@ -219,22 +211,23 @@ void AddAssemblyReferences (DirectoryAssemblyResolver resolver, Dictionary<strin
219211
if (resolutionPath == null)
220212
resolutionPath = new List<string>();
221213

222-
CheckAssemblyAttributes (assembly);
214+
CheckAssemblyAttributes (assembly, reader);
223215

224-
LogMessage ("{0}Adding assembly reference for {1}, recursively...", new string (' ', indent), assembly.Name);
225-
resolutionPath.Add (assembly.Name.Name);
216+
LogMessage ("{0}Adding assembly reference for {1}, recursively...", new string (' ', indent), assemblyName);
217+
resolutionPath.Add (assemblyName);
226218
indent += 2;
227219

228220
// Add this assembly
229221
if (!topLevel) {
230-
assemblies [assemblyName] = CreateAssemblyTaskItem (fullPath);
222+
assemblies [assemblyName] = CreateAssemblyTaskItem (Path.GetFullPath (assemblyPath));
231223
}
232224

233225
// Recurse into each referenced assembly
234-
foreach (AssemblyNameReference reference in assembly.MainModule.AssemblyReferences) {
235-
AssemblyDefinition reference_assembly;
226+
foreach (var handle in reader.AssemblyReferences) {
227+
var reference = reader.GetAssemblyReference (handle);
228+
string reference_assembly;
236229
try {
237-
reference_assembly = resolver.Resolve (reference);
230+
reference_assembly = resolver.Resolve (reader.GetString (reference.Name));
238231
} catch (FileNotFoundException ex) {
239232
var references = new StringBuilder ();
240233
for (int i = 0; i < resolutionPath.Count; i++) {
@@ -261,25 +254,30 @@ void AddAssemblyReferences (DirectoryAssemblyResolver resolver, Dictionary<strin
261254
resolutionPath.RemoveAt (resolutionPath.Count - 1);
262255
}
263256

264-
void CheckAssemblyAttributes (AssemblyDefinition assembly)
257+
void CheckAssemblyAttributes (AssemblyDefinition assembly, MetadataReader reader)
265258
{
266-
foreach (var att in assembly.CustomAttributes) {
267-
switch (att.AttributeType.FullName) {
259+
foreach (var handle in assembly.GetCustomAttributes ()) {
260+
var attribute = reader.GetCustomAttribute (handle);
261+
switch (reader.GetCustomAttributeFullName (attribute)) {
268262
case "Java.Interop.DoNotPackageAttribute": {
269-
string file = (string)att.ConstructorArguments.First ().Value;
270-
if (string.IsNullOrWhiteSpace (file))
271-
LogError ("In referenced assembly {0}, Java.Interop.DoNotPackageAttribute requires non-null file name.", assembly.FullName);
272-
do_not_package_atts.Add (Path.GetFileName (file));
263+
var decoded = attribute.DecodeValue (DummyCustomAttributeProvider.Instance);
264+
if (decoded.FixedArguments.Length > 0) {
265+
string file = decoded.FixedArguments [0].Value?.ToString ();
266+
if (string.IsNullOrWhiteSpace (file))
267+
LogError ("In referenced assembly {0}, Java.Interop.DoNotPackageAttribute requires non-null file name.", assembly.GetAssemblyName ().FullName);
268+
do_not_package_atts.Add (Path.GetFileName (file));
269+
}
273270
}
274271
break;
275272
case "System.Runtime.Versioning.TargetFrameworkAttribute": {
276-
foreach (var p in att.ConstructorArguments) {
277-
var value = p.Value.ToString ();
278-
if (value.StartsWith ("MonoAndroid")) {
273+
var decoded = attribute.DecodeValue (DummyCustomAttributeProvider.Instance);
274+
foreach (var p in decoded.FixedArguments) {
275+
var value = p.Value?.ToString ();
276+
if (value != null && value.StartsWith ("MonoAndroid", StringComparison.Ordinal)) {
279277
var values = value.Split ('=');
280278
var apiLevel = MonoAndroidHelper.SupportedVersions.GetApiLevelFromFrameworkVersion (values [1]);
281279
if (apiLevel != null) {
282-
var assemblyName = assembly.Name.Name;
280+
var assemblyName = reader.GetString (assembly.Name);
283281
Log.LogDebugMessage ("{0}={1}", assemblyName, apiLevel);
284282
api_levels [assemblyName] = apiLevel.Value;
285283
}
@@ -305,7 +303,7 @@ static LinkModes ParseLinkMode (string linkmode)
305303
return mode;
306304
}
307305

308-
void AddI18nAssemblies (DirectoryAssemblyResolver resolver, Dictionary<string, ITaskItem> assemblies)
306+
void AddI18nAssemblies (MetadataResolver resolver, Dictionary<string, ITaskItem> assemblies)
309307
{
310308
var i18n = Linker.ParseI18nAssemblies (I18nAssemblies);
311309
var link = ParseLinkMode (LinkMode);
@@ -332,10 +330,10 @@ void AddI18nAssemblies (DirectoryAssemblyResolver resolver, Dictionary<string, I
332330
ResolveI18nAssembly (resolver, "I18N.West", assemblies);
333331
}
334332

335-
void ResolveI18nAssembly (DirectoryAssemblyResolver resolver, string name, Dictionary<string, ITaskItem> assemblies)
333+
void ResolveI18nAssembly (MetadataResolver resolver, string name, Dictionary<string, ITaskItem> assemblies)
336334
{
337-
var assembly = resolver.Resolve (AssemblyNameReference.Parse (name));
338-
var assemblyFullPath = Path.GetFullPath (assembly.MainModule.FileName);
335+
var assembly = resolver.Resolve (name);
336+
var assemblyFullPath = Path.GetFullPath (assembly);
339337
assemblies [name] = CreateAssemblyTaskItem (assemblyFullPath);
340338
}
341339

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
using System.Reflection.Metadata;
2+
3+
namespace Xamarin.Android.Tasks
4+
{
5+
/// <summary>
6+
/// A helper type for System.Reflection.Metadata. Getting the value of custom attribute arguments is a bit convoluted, if you merely want the values.
7+
///
8+
/// This interface allows usage such as:
9+
/// CustomAttribute attribute = reader.GetCustomAttribute (handle);
10+
/// CustomAttributeValue<object> decoded = attribute.DecodeValue (DummyCustomAttributeProvider.Instance);
11+
/// </summary>
12+
public class DummyCustomAttributeProvider : ICustomAttributeTypeProvider<object>
13+
{
14+
public static readonly DummyCustomAttributeProvider Instance = new DummyCustomAttributeProvider ();
15+
16+
public object GetPrimitiveType (PrimitiveTypeCode typeCode) => null;
17+
18+
public object GetSystemType () => null;
19+
20+
public object GetSZArrayType (object elementType) => null;
21+
22+
public object GetTypeFromDefinition (MetadataReader reader, TypeDefinitionHandle handle, byte rawTypeKind) => null;
23+
24+
public object GetTypeFromReference (MetadataReader reader, TypeReferenceHandle handle, byte rawTypeKind) => null;
25+
26+
public object GetTypeFromSerializedName (string name) => null;
27+
28+
public PrimitiveTypeCode GetUnderlyingEnumType (object type) => default (PrimitiveTypeCode);
29+
30+
public bool IsSystemType (object type) => false;
31+
}
32+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
using System.Reflection.Metadata;
2+
3+
namespace Xamarin.Android.Tasks
4+
{
5+
public static class MetadataExtensions
6+
{
7+
public static string GetCustomAttributeFullName (this MetadataReader reader, CustomAttribute attribute)
8+
{
9+
if (attribute.Constructor.Kind == HandleKind.MemberReference) {
10+
var ctor = reader.GetMemberReference ((MemberReferenceHandle)attribute.Constructor);
11+
var type = reader.GetTypeReference ((TypeReferenceHandle)ctor.Parent);
12+
return reader.GetString (type.Namespace) + "." + reader.GetString (type.Name);
13+
} else if (attribute.Constructor.Kind == HandleKind.MethodDefinition) {
14+
var ctor = reader.GetMethodDefinition ((MethodDefinitionHandle)attribute.Constructor);
15+
var type = reader.GetTypeDefinition (ctor.GetDeclaringType ());
16+
return reader.GetString (type.Namespace) + "." + reader.GetString (type.Name);
17+
}
18+
return null;
19+
}
20+
}
21+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.IO;
4+
using System.Reflection.Metadata;
5+
using System.Reflection.PortableExecutable;
6+
7+
namespace Xamarin.Android.Tasks
8+
{
9+
/// <summary>
10+
/// A replacement for DirectoryAssemblyResolver, using System.Reflection.Metadata
11+
/// </summary>
12+
public class MetadataResolver : IDisposable
13+
{
14+
readonly Dictionary<string, PEReader> cache = new Dictionary<string, PEReader> ();
15+
readonly List<string> searchDirectories = new List<string> ();
16+
17+
public MetadataReader GetAssemblyReader (string assemblyName)
18+
{
19+
var key = Path.GetFileNameWithoutExtension (assemblyName);
20+
if (!cache.TryGetValue (key, out PEReader reader)) {
21+
var assemblyPath = Resolve (assemblyName);
22+
cache.Add (key, reader = new PEReader (File.OpenRead (assemblyPath)));
23+
}
24+
return reader.GetMetadataReader ();
25+
}
26+
27+
public void AddSearchDirectory (string directory)
28+
{
29+
directory = Path.GetFullPath (directory);
30+
if (!searchDirectories.Contains (directory))
31+
searchDirectories.Add (directory);
32+
}
33+
34+
public string Resolve (string assemblyName)
35+
{
36+
string assemblyPath = assemblyName;
37+
if (!assemblyPath.EndsWith (".dll", StringComparison.OrdinalIgnoreCase)) {
38+
assemblyPath += ".dll";
39+
}
40+
if (File.Exists (assemblyPath)) {
41+
return assemblyPath;
42+
}
43+
foreach (var dir in searchDirectories) {
44+
var path = Path.Combine (dir, assemblyPath);
45+
if (File.Exists (path))
46+
return path;
47+
}
48+
49+
throw new FileNotFoundException ($"Could not load assembly '{assemblyName}'.", assemblyName);
50+
}
51+
52+
public void Dispose ()
53+
{
54+
foreach (var provider in cache.Values) {
55+
provider.Dispose ();
56+
}
57+
cache.Clear ();
58+
}
59+
}
60+
}

src/Xamarin.Android.Build.Tasks/Utilities/MonoAndroidHelper.cs

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,18 @@
22
using System.Collections.Generic;
33
using System.Diagnostics;
44
using System.Linq;
5-
using System.Text;
65
using System.IO;
6+
using System.Reflection.Metadata;
7+
using System.Reflection.PortableExecutable;
78
using System.Security.Cryptography;
8-
using Mono.Security.Cryptography;
99
using Xamarin.Android.Tools;
1010
using Xamarin.Tools.Zip;
11-
using Mono.Cecil;
1211

1312
#if MSBUILD
1413
using Microsoft.Build.Framework;
1514
using Microsoft.Build.Utilities;
1615
#endif
1716

18-
using Xamarin.Android.Tools;
19-
2017
namespace Xamarin.Android.Tasks
2118
{
2219
public class MonoAndroidHelper
@@ -307,16 +304,18 @@ public static bool IsFrameworkAssembly (string assembly, bool checkSdkPath)
307304

308305
public static bool IsReferenceAssembly (string assembly)
309306
{
310-
var rp = new ReaderParameters { ReadSymbols = false };
311-
using (var a = AssemblyDefinition.ReadAssembly (assembly, rp))
312-
return IsReferenceAssembly (a);
313-
}
314-
315-
public static bool IsReferenceAssembly (AssemblyDefinition assembly)
316-
{
317-
if (!assembly.HasCustomAttributes)
307+
using (var stream = File.OpenRead (assembly))
308+
using (var pe = new PEReader (stream)) {
309+
var reader = pe.GetMetadataReader ();
310+
var assemblyDefinition = reader.GetAssemblyDefinition ();
311+
foreach (var handle in assemblyDefinition.GetCustomAttributes ()) {
312+
var attribute = reader.GetCustomAttribute (handle);
313+
var attributeName = reader.GetCustomAttributeFullName (attribute);
314+
if (attributeName == "System.Runtime.CompilerServices.ReferenceAssemblyAttribute")
315+
return true;
316+
}
318317
return false;
319-
return assembly.CustomAttributes.Any (t => t.AttributeType.FullName == "System.Runtime.CompilerServices.ReferenceAssemblyAttribute");
318+
}
320319
}
321320

322321
public static bool ExistsInFrameworkPath (string assembly)

0 commit comments

Comments
 (0)