Skip to content

Commit c8db2ed

Browse files
authored
ResolveAssemblyReference CPU optimizations (redo) (#9044)
#8916 broke some .NET Framework scenarios and was reverted. This PR is a redo of the change with the bug fixed. It turned out that the destination of the optimized CopyMetadataTo may be a transparent proxy, typically a TaskItem object living in another appdomain, which does not work well with Linq. The fix and the test coverage are in their own commits. Context Low-hanging fruit is showing in RAR performance profiles. Changes Made Avoided constructing AssemblyName on a hot path as the constructor makes expensive Fusion calls on .NET Framework. The problematic code was introduced in [RAR] Don't do I/O on SDK-provided references #8688. Added a metadata bulk-set operation to the internal IMetadataContainer interface. Calling SetMetadata for more than a couple of metadata is slow if ImmutableDictionary is used as its backing storage. RAR is heavy on metadata manipulation and switching to the new operation saves about 10% of RAR run-time when building OrchardCore. Testing Existing and new unit tests. Measured the perf impact by building OC.
1 parent 02379f2 commit c8db2ed

File tree

8 files changed

+217
-38
lines changed

8 files changed

+217
-38
lines changed

src/Build.OM.UnitTests/Instance/ProjectItemInstance_Tests.cs

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,49 @@ public void AccessorsWithMetadata()
9595
Assert.Equal("v2", item.GetMetadataValue("m2"));
9696
}
9797

98+
/// <summary>
99+
/// Basic ProjectItemInstance with metadata added using ImportMetadata
100+
/// </summary>
101+
[Fact]
102+
public void AccessorsWithImportedMetadata()
103+
{
104+
ProjectItemInstance item = GetItemInstance();
105+
106+
((IMetadataContainer)item).ImportMetadata(new Dictionary<string, string>
107+
{
108+
{ "m1", "v1" },
109+
{ "m2", "v2" },
110+
});
111+
112+
Assert.Equal("m1", item.GetMetadata("m1").Name);
113+
Assert.Equal("m2", item.GetMetadata("m2").Name);
114+
Assert.Equal("v1", item.GetMetadataValue("m1"));
115+
Assert.Equal("v2", item.GetMetadataValue("m2"));
116+
}
117+
118+
/// <summary>
119+
/// ImportMetadata adds and overwrites metadata, does not delete existing metadata
120+
/// </summary>
121+
[Fact]
122+
public void ImportMetadataAddsAndOverwrites()
123+
{
124+
ProjectItemInstance item = GetItemInstance();
125+
126+
item.SetMetadata("m1", "v1");
127+
item.SetMetadata("m2", "v0");
128+
129+
((IMetadataContainer) item).ImportMetadata(new Dictionary<string, string>
130+
{
131+
{ "m2", "v2" },
132+
{ "m3", "v3" },
133+
});
134+
135+
// m1 was not deleted, m2 was overwritten, m3 was added
136+
Assert.Equal("v1", item.GetMetadataValue("m1"));
137+
Assert.Equal("v2", item.GetMetadataValue("m2"));
138+
Assert.Equal("v3", item.GetMetadataValue("m3"));
139+
}
140+
98141
/// <summary>
99142
/// Get metadata not present
100143
/// </summary>
@@ -106,6 +149,56 @@ public void GetMissingMetadata()
106149
Assert.Equal(String.Empty, item.GetMetadataValue("X"));
107150
}
108151

152+
[Fact]
153+
public void CopyMetadataToTaskItem()
154+
{
155+
ProjectItemInstance fromItem = GetItemInstance();
156+
157+
fromItem.SetMetadata("m1", "v1");
158+
fromItem.SetMetadata("m2", "v2");
159+
160+
ITaskItem toItem = new Utilities.TaskItem();
161+
162+
((ITaskItem)fromItem).CopyMetadataTo(toItem);
163+
164+
Assert.Equal("v1", toItem.GetMetadata("m1"));
165+
Assert.Equal("v2", toItem.GetMetadata("m2"));
166+
}
167+
168+
#if FEATURE_APPDOMAIN
169+
private sealed class RemoteTaskItemFactory : MarshalByRefObject
170+
{
171+
public ITaskItem CreateTaskItem() => new Utilities.TaskItem();
172+
}
173+
174+
[Fact]
175+
public void CopyMetadataToRemoteTaskItem()
176+
{
177+
ProjectItemInstance fromItem = GetItemInstance();
178+
179+
fromItem.SetMetadata("m1", "v1");
180+
fromItem.SetMetadata("m2", "v2");
181+
182+
AppDomain appDomain = null;
183+
try
184+
{
185+
appDomain = AppDomain.CreateDomain("CopyMetadataToRemoteTaskItem", null, AppDomain.CurrentDomain.SetupInformation);
186+
RemoteTaskItemFactory itemFactory = (RemoteTaskItemFactory)appDomain.CreateInstanceFromAndUnwrap(typeof(RemoteTaskItemFactory).Module.FullyQualifiedName, typeof(RemoteTaskItemFactory).FullName);
187+
188+
ITaskItem toItem = itemFactory.CreateTaskItem();
189+
190+
((ITaskItem)fromItem).CopyMetadataTo(toItem);
191+
192+
Assert.Equal("v1", toItem.GetMetadata("m1"));
193+
Assert.Equal("v2", toItem.GetMetadata("m2"));
194+
}
195+
finally
196+
{
197+
AppDomain.Unload(appDomain);
198+
}
199+
}
200+
#endif
201+
109202
/// <summary>
110203
/// Set include
111204
/// </summary>

src/Build/Instance/ProjectItemInstance.cs

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
using System.Diagnostics.CodeAnalysis;
99
using System.IO;
1010
using System.Linq;
11+
#if FEATURE_APPDOMAIN
12+
using System.Runtime.Remoting;
13+
#endif
1114
using Microsoft.Build.BackEnd;
1215
using Microsoft.Build.Collections;
1316
using Microsoft.Build.Construction;
@@ -521,6 +524,8 @@ IDictionary ITaskItem2.CloneCustomMetadataEscaped()
521524

522525
IEnumerable<KeyValuePair<string, string>> IMetadataContainer.EnumerateMetadata() => _taskItem.EnumerateMetadata();
523526

527+
void IMetadataContainer.ImportMetadata(IEnumerable<KeyValuePair<string, string>> metadata) => _taskItem.ImportMetadata(metadata);
528+
524529
#region IMetadataTable Members
525530

526531
/// <summary>
@@ -1034,6 +1039,19 @@ public IEnumerable<KeyValuePair<string, string>> EnumerateMetadata()
10341039
}
10351040
}
10361041

1042+
/// <summary>
1043+
/// Sets the given metadata.
1044+
/// Equivalent to calling <see cref="SetMetadata(string,string)"/> for each item in <paramref name="metadata"/>.
1045+
/// </summary>
1046+
/// <param name="metadata">The metadata to set.</param>
1047+
public void ImportMetadata(IEnumerable<KeyValuePair<string, string>> metadata)
1048+
{
1049+
ProjectInstance.VerifyThrowNotImmutable(_isImmutable);
1050+
1051+
_directMetadata ??= new CopyOnWritePropertyDictionary<ProjectMetadataInstance>();
1052+
_directMetadata.ImportProperties(metadata.Select(kvp => new ProjectMetadataInstance(kvp.Key, kvp.Value, allowItemSpecModifiers: true)));
1053+
}
1054+
10371055
/// <summary>
10381056
/// Used to return metadata from another AppDomain. Can't use yield return because the
10391057
/// generated state machine is not marked as [Serializable], so we need to allocate.
@@ -1371,9 +1389,7 @@ public void CopyMetadataTo(ITaskItem destinationItem, bool addOriginalItemSpec)
13711389
originalItemSpec = destinationItem.GetMetadata("OriginalItemSpec");
13721390
}
13731391

1374-
TaskItem destinationAsTaskItem = destinationItem as TaskItem;
1375-
1376-
if (destinationAsTaskItem != null && destinationAsTaskItem._directMetadata == null)
1392+
if (destinationItem is TaskItem destinationAsTaskItem && destinationAsTaskItem._directMetadata == null)
13771393
{
13781394
ProjectInstance.VerifyThrowNotImmutable(destinationAsTaskItem._isImmutable);
13791395

@@ -1391,6 +1407,24 @@ public void CopyMetadataTo(ITaskItem destinationItem, bool addOriginalItemSpec)
13911407
destinationAsTaskItem._itemDefinitions.AddRange(_itemDefinitions);
13921408
}
13931409
}
1410+
else if (destinationItem is IMetadataContainer destinationItemAsMetadataContainer)
1411+
{
1412+
// The destination implements IMetadataContainer so we can use the ImportMetadata bulk-set operation.
1413+
IEnumerable<ProjectMetadataInstance> metadataEnumerable = MetadataCollection;
1414+
IEnumerable<KeyValuePair<string, string>> metadataToImport = metadataEnumerable
1415+
.Where(metadatum => string.IsNullOrEmpty(destinationItem.GetMetadata(metadatum.Name)))
1416+
.Select(metadatum => new KeyValuePair<string, string>(metadatum.Name, GetMetadataEscaped(metadatum.Name)));
1417+
1418+
#if FEATURE_APPDOMAIN
1419+
if (RemotingServices.IsTransparentProxy(destinationItem))
1420+
{
1421+
// Linq is not serializable so materialize the collection before making the call.
1422+
metadataToImport = metadataToImport.ToList();
1423+
}
1424+
#endif
1425+
1426+
destinationItemAsMetadataContainer.ImportMetadata(metadataToImport);
1427+
}
13941428
else
13951429
{
13961430
// OK, most likely the destination item was a Microsoft.Build.Utilities.TaskItem.

src/Framework/IMetadataContainer.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,17 @@ internal interface IMetadataContainer
2020
/// in the binary logger.
2121
/// </summary>
2222
IEnumerable<KeyValuePair<string, string>> EnumerateMetadata();
23+
24+
/// <summary>
25+
/// Sets the given metadata. The operation is equivalent to calling
26+
/// <see cref="ITaskItem.SetMetadata"/> on all metadata, but takes
27+
/// advantage of a faster bulk-set operation where applicable. The
28+
/// implementation may not perform the same parameter validation
29+
/// as SetMetadata.
30+
/// </summary>
31+
/// <param name="metadata">The metadata to set. The keys are assumed
32+
/// to be unique and values are assumed to be escaped.
33+
/// </param>
34+
void ImportMetadata(IEnumerable<KeyValuePair<string, string>> metadata);
2335
}
2436
}

src/Framework/TaskItemData.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ public TaskItemData(ITaskItem original)
4949

5050
IEnumerable<KeyValuePair<string, string>> IMetadataContainer.EnumerateMetadata() => Metadata;
5151

52+
void IMetadataContainer.ImportMetadata(IEnumerable<KeyValuePair<string, string>> metadata)
53+
=> throw new InvalidOperationException($"{nameof(TaskItemData)} does not support write operations");
54+
5255
public int MetadataCount => Metadata.Count;
5356

5457
public ICollection MetadataNames => (ICollection)Metadata.Keys;

src/Shared/TaskParameter.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -856,6 +856,14 @@ private IEnumerable<KeyValuePair<string, string>> EnumerateMetadataLazy()
856856
yield return unescaped;
857857
}
858858
}
859+
860+
public void ImportMetadata(IEnumerable<KeyValuePair<string, string>> metadata)
861+
{
862+
foreach (KeyValuePair<string, string> kvp in metadata)
863+
{
864+
SetMetadata(kvp.Key, kvp.Value);
865+
}
866+
}
859867
}
860868
}
861869
}

src/Tasks/AssemblyDependency/ReferenceTable.cs

Lines changed: 39 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -852,8 +852,7 @@ private static AssemblyNameExtension GetAssemblyNameFromItemMetadata(ITaskItem i
852852
name = item.GetMetadata(FileUtilities.ItemSpecModifiers.Filename);
853853
}
854854

855-
AssemblyName assemblyName = new AssemblyName($"{name}, Version={version}, Culture=neutral, PublicKeyToken={publicKeyToken}");
856-
return new AssemblyNameExtension(assemblyName);
855+
return new AssemblyNameExtension($"{name}, Version={version}, Culture=neutral, PublicKeyToken={publicKeyToken}");
857856
}
858857

859858
/// <summary>
@@ -2677,36 +2676,9 @@ private ITaskItem SetItemMetadata(List<ITaskItem> relatedItems, List<ITaskItem>
26772676
// Set up the main item.
26782677
TaskItem referenceItem = new TaskItem();
26792678
referenceItem.ItemSpec = reference.FullPath;
2680-
referenceItem.SetMetadata(ItemMetadataNames.resolvedFrom, reference.ResolvedSearchPath);
26812679

2682-
// Set the CopyLocal metadata.
2683-
referenceItem.SetMetadata(ItemMetadataNames.copyLocal, reference.IsCopyLocal ? "true" : "false");
2684-
2685-
// Set the Redist name metadata.
2686-
if (!String.IsNullOrEmpty(reference.RedistName))
2687-
{
2688-
referenceItem.SetMetadata(ItemMetadataNames.redist, reference.RedistName);
2689-
}
2690-
2691-
if (Reference.IsFrameworkFile(reference.FullPath, _frameworkPaths) || (_installedAssemblies?.FrameworkAssemblyEntryInRedist(assemblyName) == true))
2692-
{
2693-
if (!IsAssemblyRemovedFromDotNetFramework(assemblyName, reference.FullPath, _frameworkPaths, _installedAssemblies))
2694-
{
2695-
referenceItem.SetMetadata(ItemMetadataNames.frameworkFile, "true");
2696-
}
2697-
}
2698-
2699-
if (!String.IsNullOrEmpty(reference.ImageRuntime))
2700-
{
2701-
referenceItem.SetMetadata(ItemMetadataNames.imageRuntime, reference.ImageRuntime);
2702-
}
2703-
2704-
// The redist root is "null" when there was no IsRedistRoot flag in the Redist XML
2705-
// (or there was no redist XML at all for this item).
2706-
if (reference.IsRedistRoot != null)
2707-
{
2708-
referenceItem.SetMetadata(ItemMetadataNames.isRedistRoot, (bool)reference.IsRedistRoot ? "true" : "false");
2709-
}
2680+
IMetadataContainer referenceItemAsMetadataContainer = referenceItem;
2681+
referenceItemAsMetadataContainer.ImportMetadata(EnumerateCommonMetadata());
27102682

27112683
// If there was a primary source item, then forward metadata from it.
27122684
// It's important that the metadata from the primary source item
@@ -2880,13 +2852,45 @@ private ITaskItem SetItemMetadata(List<ITaskItem> relatedItems, List<ITaskItem>
28802852
// nonForwardableMetadata should be null here if relatedFileExtensions, satellites, serializationAssemblyFiles, and scatterFiles were all empty.
28812853
if (nonForwardableMetadata != null)
28822854
{
2883-
foreach (KeyValuePair<string, string> kvp in nonForwardableMetadata)
2884-
{
2885-
referenceItem.SetMetadata(kvp.Key, kvp.Value);
2886-
}
2855+
referenceItemAsMetadataContainer.ImportMetadata(nonForwardableMetadata);
28872856
}
28882857

28892858
return referenceItem;
2859+
2860+
// Enumerate common metadata with an iterator to allow using a more efficient bulk-set operation.
2861+
IEnumerable<KeyValuePair<string, string>> EnumerateCommonMetadata()
2862+
{
2863+
yield return new KeyValuePair<string, string>(ItemMetadataNames.resolvedFrom, reference.ResolvedSearchPath);
2864+
2865+
// Set the CopyLocal metadata.
2866+
yield return new KeyValuePair<string, string>(ItemMetadataNames.copyLocal, reference.IsCopyLocal ? "true" : "false");
2867+
2868+
// Set the Redist name metadata.
2869+
if (!string.IsNullOrEmpty(reference.RedistName))
2870+
{
2871+
yield return new KeyValuePair<string, string>(ItemMetadataNames.redist, reference.RedistName);
2872+
}
2873+
2874+
if (Reference.IsFrameworkFile(reference.FullPath, _frameworkPaths) || (_installedAssemblies?.FrameworkAssemblyEntryInRedist(assemblyName) == true))
2875+
{
2876+
if (!IsAssemblyRemovedFromDotNetFramework(assemblyName, reference.FullPath, _frameworkPaths, _installedAssemblies))
2877+
{
2878+
yield return new KeyValuePair<string, string>(ItemMetadataNames.frameworkFile, "true");
2879+
}
2880+
}
2881+
2882+
if (!string.IsNullOrEmpty(reference.ImageRuntime))
2883+
{
2884+
yield return new KeyValuePair<string, string>(ItemMetadataNames.imageRuntime, reference.ImageRuntime);
2885+
}
2886+
2887+
// The redist root is "null" when there was no IsRedistRoot flag in the Redist XML
2888+
// (or there was no redist XML at all for this item).
2889+
if (reference.IsRedistRoot != null)
2890+
{
2891+
yield return new KeyValuePair<string, string>(ItemMetadataNames.isRedistRoot, (bool)reference.IsRedistRoot ? "true" : "false");
2892+
}
2893+
}
28902894
}
28912895

28922896
/// <summary>

src/Utilities.UnitTests/TaskItem_Tests.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,25 @@ public void SetNullMetadataValue()
324324
item.GetMetadata("m").ShouldBe(string.Empty);
325325
}
326326

327+
[Fact]
328+
public void ImplementsIMetadataContainer()
329+
{
330+
Dictionary<string, string> metadata = new()
331+
{
332+
{ "a", "a1" },
333+
{ "b", "b1" },
334+
};
335+
336+
TaskItem item = new TaskItem("foo");
337+
IMetadataContainer metadataContainer = (IMetadataContainer)item;
338+
339+
metadataContainer.ImportMetadata(metadata);
340+
341+
var actualMetadata = metadataContainer.EnumerateMetadata().OrderBy(metadata => metadata.Key).ToList();
342+
var expectedMetadata = metadata.OrderBy(metadata => metadata.Value).ToList();
343+
Assert.True(actualMetadata.SequenceEqual(expectedMetadata));
344+
}
345+
327346
#if FEATURE_APPDOMAIN
328347
/// <summary>
329348
/// Test that task items can be successfully constructed based on a task item from another appdomain.

src/Utilities/TaskItem.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -480,6 +480,12 @@ IEnumerable<KeyValuePair<string, string>> IMetadataContainer.EnumerateMetadata()
480480
return EnumerateMetadataLazy();
481481
}
482482

483+
void IMetadataContainer.ImportMetadata(IEnumerable<KeyValuePair<string, string>> metadata)
484+
{
485+
_metadata ??= new CopyOnWriteDictionary<string>(MSBuildNameIgnoreCaseComparer.Default);
486+
_metadata.SetItems(metadata.Select(kvp => new KeyValuePair<string, string>(kvp.Key, kvp.Value ?? string.Empty)));
487+
}
488+
483489
private IEnumerable<KeyValuePair<string, string>> EnumerateMetadataEager()
484490
{
485491
if (_metadata == null)

0 commit comments

Comments
 (0)