Skip to content

Commit 1da8240

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. - Had to adjust the filename reported for XA2002, should optionally call `Path.GetFileNameWithoutExtension` if the name ends with `.dll`. The resulting code *should* be the same, except we are using SRM over Mono.Cecil. ~~ Other changes ~~ [xabuild.exe] remove SRM reference This appears to fix the build on macOS, we had this workaround from a mono bump in the past. Since xabuild has its own version of System.Reflection.Metadata that was already loaded, we weren't loading the one we are using in XA's MSBuild tasks. Things appear to work without the reference now. ~~ Downstream ~~ We will need to add the following assemblies to the installer: - `System.Reflection.Metadata.dll` - `System.Collections.Immutable.dll`
1 parent 434d2d8 commit 1da8240

File tree

8 files changed

+199
-83
lines changed

8 files changed

+199
-83
lines changed

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

Lines changed: 60 additions & 59 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);
79-
80-
var assemblies = new Dictionary<string, ITaskItem> ();
74+
foreach (var dir in ReferenceAssembliesDirectory.Split (new char [] { ';' }, StringSplitOptions.RemoveEmptyEntries))
75+
resolver.AddSearchDirectory (dir);
8176

82-
var topAssemblyReferences = new List<AssemblyDefinition> ();
77+
var assemblies = new Dictionary<string, ITaskItem> (Assemblies.Length);
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++) {
@@ -245,7 +238,10 @@ void AddAssemblyReferences (DirectoryAssemblyResolver resolver, Dictionary<strin
245238
references.Append ('`');
246239
}
247240

248-
string missingAssembly = Path.GetFileNameWithoutExtension (ex.FileName);
241+
string missingAssembly = ex.FileName;
242+
if (missingAssembly.EndsWith (".dll", StringComparison.OrdinalIgnoreCase)) {
243+
missingAssembly = Path.GetFileNameWithoutExtension (missingAssembly);
244+
}
249245
string message = $"Can not resolve reference: `{missingAssembly}`, referenced by {references}.";
250246
if (MonoAndroidHelper.IsFrameworkAssembly (ex.FileName)) {
251247
LogCodedError ("XA2002", $"{message} Perhaps it doesn't exist in the Mono for Android profile?");
@@ -261,25 +257,30 @@ void AddAssemblyReferences (DirectoryAssemblyResolver resolver, Dictionary<strin
261257
resolutionPath.RemoveAt (resolutionPath.Count - 1);
262258
}
263259

264-
void CheckAssemblyAttributes (AssemblyDefinition assembly)
260+
void CheckAssemblyAttributes (AssemblyDefinition assembly, MetadataReader reader)
265261
{
266-
foreach (var att in assembly.CustomAttributes) {
267-
switch (att.AttributeType.FullName) {
262+
foreach (var handle in assembly.GetCustomAttributes ()) {
263+
var attribute = reader.GetCustomAttribute (handle);
264+
switch (reader.GetCustomAttributeFullName (attribute)) {
268265
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));
266+
var decoded = attribute.DecodeValue (DummyCustomAttributeProvider.Instance);
267+
if (decoded.FixedArguments.Length > 0) {
268+
string file = decoded.FixedArguments [0].Value?.ToString ();
269+
if (string.IsNullOrWhiteSpace (file))
270+
LogError ("In referenced assembly {0}, Java.Interop.DoNotPackageAttribute requires non-null file name.", assembly.GetAssemblyName ().FullName);
271+
do_not_package_atts.Add (Path.GetFileName (file));
272+
}
273273
}
274274
break;
275275
case "System.Runtime.Versioning.TargetFrameworkAttribute": {
276-
foreach (var p in att.ConstructorArguments) {
277-
var value = p.Value.ToString ();
278-
if (value.StartsWith ("MonoAndroid")) {
276+
var decoded = attribute.DecodeValue (DummyCustomAttributeProvider.Instance);
277+
foreach (var p in decoded.FixedArguments) {
278+
var value = p.Value?.ToString ();
279+
if (value != null && value.StartsWith ("MonoAndroid", StringComparison.Ordinal)) {
279280
var values = value.Split ('=');
280281
var apiLevel = MonoAndroidHelper.SupportedVersions.GetApiLevelFromFrameworkVersion (values [1]);
281282
if (apiLevel != null) {
282-
var assemblyName = assembly.Name.Name;
283+
var assemblyName = reader.GetString (assembly.Name);
283284
Log.LogDebugMessage ("{0}={1}", assemblyName, apiLevel);
284285
api_levels [assemblyName] = apiLevel.Value;
285286
}
@@ -305,7 +306,7 @@ static LinkModes ParseLinkMode (string linkmode)
305306
return mode;
306307
}
307308

308-
void AddI18nAssemblies (DirectoryAssemblyResolver resolver, Dictionary<string, ITaskItem> assemblies)
309+
void AddI18nAssemblies (MetadataResolver resolver, Dictionary<string, ITaskItem> assemblies)
309310
{
310311
var i18n = Linker.ParseI18nAssemblies (I18nAssemblies);
311312
var link = ParseLinkMode (LinkMode);
@@ -332,10 +333,10 @@ void AddI18nAssemblies (DirectoryAssemblyResolver resolver, Dictionary<string, I
332333
ResolveI18nAssembly (resolver, "I18N.West", assemblies);
333334
}
334335

335-
void ResolveI18nAssembly (DirectoryAssemblyResolver resolver, string name, Dictionary<string, ITaskItem> assemblies)
336+
void ResolveI18nAssembly (MetadataResolver resolver, string name, Dictionary<string, ITaskItem> assemblies)
336337
{
337-
var assembly = resolver.Resolve (AssemblyNameReference.Parse (name));
338-
var assemblyFullPath = Path.GetFullPath (assembly.MainModule.FileName);
338+
var assembly = resolver.Resolve (name);
339+
var assemblyFullPath = Path.GetFullPath (assembly);
339340
assemblies [name] = CreateAssemblyTaskItem (assemblyFullPath);
340341
}
341342

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+
}

0 commit comments

Comments
 (0)