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
Optimize Match-on-metadata #6035
Changes from 11 commits
ba4b653
a124438
8db0cab
6c68b2b
c8d7089
17f5225
978bf35
ab951b8
9d335f2
f71a467
c9263be
08d561b
d7ce8b2
87361a6
47492ae
7fe9587
58b8a84
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -249,7 +249,18 @@ private void ExecuteRemove(ProjectItemGroupTaskItemInstance child, ItemBucket bu | |
} | ||
else | ||
{ | ||
itemsToRemove = FindItemsUsingMatchOnMetadata(group, child, bucket, matchOnMetadata, matchingOptions); | ||
ImmutableList<string> metadataList = matchOnMetadata.ToImmutableList(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why the conversion to |
||
ItemSpec<ProjectPropertyInstance, ProjectItemInstance> itemSpec = new(child.Remove, bucket.Expander, child.RemoveLocation, Project.Directory, true); | ||
ProjectFileErrorUtilities.VerifyThrowInvalidProjectFile( | ||
itemSpec.Fragments.Count == 1 | ||
&& itemSpec.Fragments.First() is ItemSpec<ProjectPropertyInstance, ProjectItemInstance>.ItemExpressionFragment | ||
&& matchOnMetadata.Count == 1, | ||
new BuildEventFileInfo(string.Empty), | ||
"OM_MatchOnMetadataIsRestrictedToOnlyOneReferencedItem", | ||
child.RemoveLocation, | ||
child.Remove); | ||
MetadataSet<ProjectPropertyInstance, ProjectItemInstance> metadataSet = new(matchingOptions, metadataList, itemSpec); | ||
itemsToRemove = group.Where(item => metadataSet.Contains(metadataList.Select(m => item.GetMetadataValue(m)))).ToList(); | ||
} | ||
|
||
if (itemsToRemove != null) | ||
|
@@ -268,29 +279,6 @@ private void ExecuteRemove(ProjectItemGroupTaskItemInstance child, ItemBucket bu | |
} | ||
} | ||
|
||
private List<ProjectItemInstance> FindItemsUsingMatchOnMetadata( | ||
ICollection<ProjectItemInstance> items, | ||
ProjectItemGroupTaskItemInstance child, | ||
ItemBucket bucket, | ||
HashSet<string> matchOnMetadata, | ||
MatchOnMetadataOptions options) | ||
{ | ||
ErrorUtilities.VerifyThrowArgumentNull(matchOnMetadata, nameof(matchOnMetadata)); | ||
|
||
var itemSpec = new ItemSpec<ProjectPropertyInstance, ProjectItemInstance>(child.Remove, bucket.Expander, child.RemoveLocation, Project.Directory, true); | ||
|
||
ProjectFileErrorUtilities.VerifyThrowInvalidProjectFile( | ||
itemSpec.Fragments.Count == 1 | ||
&& itemSpec.Fragments.First() is ItemSpec<ProjectPropertyInstance, ProjectItemInstance>.ItemExpressionFragment | ||
&& matchOnMetadata.Count == 1, | ||
new BuildEventFileInfo(string.Empty), | ||
"OM_MatchOnMetadataIsRestrictedToOnlyOneReferencedItem", | ||
child.RemoveLocation, | ||
child.Remove); | ||
|
||
return items.Where(item => itemSpec.MatchesItemOnMetadata(item, matchOnMetadata, options)).ToList(); | ||
} | ||
|
||
/// <summary> | ||
/// Modifies items in the world - specifically, changes their metadata. Changes to items that are part of the project manifest are backed up, so | ||
/// they can be reverted when the project is reset after the end of the build. | ||
|
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -3,6 +3,7 @@ | |||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
using System; | ||||||||||||||||||||||||||||||||
using System.Collections.Generic; | ||||||||||||||||||||||||||||||||
using System.Collections.Immutable; | ||||||||||||||||||||||||||||||||
using System.Linq; | ||||||||||||||||||||||||||||||||
using Microsoft.Build.Globbing; | ||||||||||||||||||||||||||||||||
using Microsoft.Build.Internal; | ||||||||||||||||||||||||||||||||
|
@@ -85,24 +86,6 @@ public override bool IsMatch(string itemToMatch) | |||||||||||||||||||||||||||||||
return ReferencedItems.Any(v => v.ItemAsValueFragment.IsMatch(itemToMatch)); | ||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
public override bool IsMatchOnMetadata(IItem item, IEnumerable<string> metadata, MatchOnMetadataOptions options) | ||||||||||||||||||||||||||||||||
{ | ||||||||||||||||||||||||||||||||
return ReferencedItems.Any(referencedItem => | ||||||||||||||||||||||||||||||||
metadata.All(m => !item.GetMetadataValue(m).Equals(string.Empty) && MetadataComparer(options, item.GetMetadataValue(m), referencedItem.Item.GetMetadataValue(m)))); | ||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
private bool MetadataComparer(MatchOnMetadataOptions options, string itemMetadata, string referencedItemMetadata) | ||||||||||||||||||||||||||||||||
{ | ||||||||||||||||||||||||||||||||
if (options.Equals(MatchOnMetadataOptions.PathLike)) | ||||||||||||||||||||||||||||||||
{ | ||||||||||||||||||||||||||||||||
return FileUtilities.ComparePathsNoThrow(itemMetadata, referencedItemMetadata, ProjectDirectory); | ||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
else | ||||||||||||||||||||||||||||||||
{ | ||||||||||||||||||||||||||||||||
return String.Equals(itemMetadata, referencedItemMetadata, options.Equals(MatchOnMetadataOptions.CaseInsensitive) ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal); | ||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
public override IMSBuildGlob ToMSBuildGlob() | ||||||||||||||||||||||||||||||||
{ | ||||||||||||||||||||||||||||||||
return MsBuildGlob; | ||||||||||||||||||||||||||||||||
|
@@ -310,26 +293,6 @@ public bool MatchesItem(I item) | |||||||||||||||||||||||||||||||
return false; | ||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
/// <summary> | ||||||||||||||||||||||||||||||||
/// Return true if any of the given <paramref name="metadata" /> matches the metadata on <paramref name="item" /> | ||||||||||||||||||||||||||||||||
/// </summary> | ||||||||||||||||||||||||||||||||
/// <param name="item">The item to attempt to find a match for based on matching metadata</param> | ||||||||||||||||||||||||||||||||
/// <param name="metadata">Names of metadata to look for matches for</param> | ||||||||||||||||||||||||||||||||
/// <param name="options">metadata option matching</param> | ||||||||||||||||||||||||||||||||
/// <returns></returns> | ||||||||||||||||||||||||||||||||
public bool MatchesItemOnMetadata(IItem item, IEnumerable<string> metadata, MatchOnMetadataOptions options) | ||||||||||||||||||||||||||||||||
{ | ||||||||||||||||||||||||||||||||
foreach (var fragment in Fragments) | ||||||||||||||||||||||||||||||||
{ | ||||||||||||||||||||||||||||||||
if (fragment.IsMatchOnMetadata(item, metadata, options)) | ||||||||||||||||||||||||||||||||
{ | ||||||||||||||||||||||||||||||||
return true; | ||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
return false; | ||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
/// <summary> | ||||||||||||||||||||||||||||||||
/// Return the fragments that match against the given <paramref name="itemToMatch" /> | ||||||||||||||||||||||||||||||||
/// </summary> | ||||||||||||||||||||||||||||||||
|
@@ -456,14 +419,6 @@ public virtual bool IsMatch(string itemToMatch) | |||||||||||||||||||||||||||||||
return FileMatcher.IsMatch(itemToMatch); | ||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
/// <summary> | ||||||||||||||||||||||||||||||||
/// Returns true if <paramref name="itemToMatch" /> matches any ReferencedItems based on <paramref name="metadata" /> and <paramref name="options" />. | ||||||||||||||||||||||||||||||||
/// </summary> | ||||||||||||||||||||||||||||||||
public virtual bool IsMatchOnMetadata(IItem itemToMatch, IEnumerable<string> metadata, MatchOnMetadataOptions options) | ||||||||||||||||||||||||||||||||
{ | ||||||||||||||||||||||||||||||||
return false; | ||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
public virtual IMSBuildGlob ToMSBuildGlob() | ||||||||||||||||||||||||||||||||
{ | ||||||||||||||||||||||||||||||||
return MsBuildGlob; | ||||||||||||||||||||||||||||||||
|
@@ -504,4 +459,78 @@ public GlobFragment(string textFragment, string projectDirectory) | |||||||||||||||||||||||||||||||
&& TextFragment[2] == '*' | ||||||||||||||||||||||||||||||||
&& FileUtilities.IsAnySlash(TextFragment[3]); | ||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
internal class MetadataSet<P, I> where P : class, IProperty where I : class, IItem, IMetadataTable | ||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. super-nit: The class can be |
||||||||||||||||||||||||||||||||
{ | ||||||||||||||||||||||||||||||||
private Dictionary<string, MetadataSet<P, I>> children; | ||||||||||||||||||||||||||||||||
Func<string, string> normalize; | ||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: Names missing leading underscores, both fields can be |
||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
internal MetadataSet(MatchOnMetadataOptions options, ImmutableList<string> metadata, ItemSpec<P, I> itemSpec) | ||||||||||||||||||||||||||||||||
{ | ||||||||||||||||||||||||||||||||
StringComparer comparer = options == MatchOnMetadataOptions.CaseSensitive ? StringComparer.Ordinal : StringComparer.OrdinalIgnoreCase; | ||||||||||||||||||||||||||||||||
children = new Dictionary<string, MetadataSet<P, I>>(comparer); | ||||||||||||||||||||||||||||||||
normalize = options == MatchOnMetadataOptions.PathLike ? p => FileUtilities.NormalizePathForComparisonNoThrow(p, Environment.CurrentDirectory) : p => p; | ||||||||||||||||||||||||||||||||
foreach (ItemSpec<P, I>.ItemExpressionFragment frag in itemSpec.Fragments) | ||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How does this type check, isn't ItemSpec.Fragments a list of the base class ItemSpecFragment? ( There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good question. It might be casting it automatically when it sees that, but I'm not sure. I don't think we should care about other kinds of fragments, since I don't think they can have metadata, so I think this is ok, but that was a lot of "I think"s. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As far as whether this is actually a safe, I should have mentioned that the type is checked before the MetadataSet is constructed, so it should always be correct. |
||||||||||||||||||||||||||||||||
{ | ||||||||||||||||||||||||||||||||
foreach (ItemSpec<P, I>.ReferencedItem referencedItem in frag.ReferencedItems) | ||||||||||||||||||||||||||||||||
{ | ||||||||||||||||||||||||||||||||
this.Add(metadata.Select(m => referencedItem.Item.GetMetadataValue(m)), comparer); | ||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
private MetadataSet(StringComparer comparer) | ||||||||||||||||||||||||||||||||
{ | ||||||||||||||||||||||||||||||||
children = new Dictionary<string, MetadataSet<P, I>>(comparer); | ||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
// Relies on IEnumerable returning the metadata in a reasonable order. Reasonable? | ||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The question is whether the matching |
||||||||||||||||||||||||||||||||
private void Add(IEnumerable<string> metadata, StringComparer comparer) | ||||||||||||||||||||||||||||||||
{ | ||||||||||||||||||||||||||||||||
MetadataSet<P, I> current = this; | ||||||||||||||||||||||||||||||||
foreach (string m in metadata) | ||||||||||||||||||||||||||||||||
{ | ||||||||||||||||||||||||||||||||
string normalizedString = normalize(m); | ||||||||||||||||||||||||||||||||
if (current.children.TryGetValue(normalizedString, out MetadataSet<P, I> child)) | ||||||||||||||||||||||||||||||||
{ | ||||||||||||||||||||||||||||||||
current = child; | ||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
else | ||||||||||||||||||||||||||||||||
{ | ||||||||||||||||||||||||||||||||
current.children.Add(normalizedString, new MetadataSet<P, I>(comparer)); | ||||||||||||||||||||||||||||||||
current = current.children[normalizedString]; | ||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: Redundant dictionary lookup.
Suggested change
|
||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
internal bool Contains(IEnumerable<string> metadata) | ||||||||||||||||||||||||||||||||
{ | ||||||||||||||||||||||||||||||||
bool nonEmptyFound = false; | ||||||||||||||||||||||||||||||||
MetadataSet<P, I> curr = this; | ||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. super-nit: |
||||||||||||||||||||||||||||||||
foreach (string m in metadata) | ||||||||||||||||||||||||||||||||
{ | ||||||||||||||||||||||||||||||||
if (!String.IsNullOrEmpty(m)) | ||||||||||||||||||||||||||||||||
{ | ||||||||||||||||||||||||||||||||
nonEmptyFound = true; | ||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why isn't this There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. From #4997: But you're right—the code I'm replacing checks for string.Empty and automatically returns false. Given our conversation in the PR review meeting, I'll change to that. Thanks! |
||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
if (!curr.children.TryGetValue(normalize(m), out curr)) | ||||||||||||||||||||||||||||||||
{ | ||||||||||||||||||||||||||||||||
return false; | ||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
return nonEmptyFound; | ||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
public enum MatchOnMetadataOptions | ||||||||||||||||||||||||||||||||
{ | ||||||||||||||||||||||||||||||||
CaseSensitive, | ||||||||||||||||||||||||||||||||
CaseInsensitive, | ||||||||||||||||||||||||||||||||
PathLike | ||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
public static class MatchOnMetadataConstants | ||||||||||||||||||||||||||||||||
{ | ||||||||||||||||||||||||||||||||
public const MatchOnMetadataOptions MatchOnMetadataOptionsDefaultValue = MatchOnMetadataOptions.CaseSensitive; | ||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,6 +14,7 @@ class RemoveOperation : LazyItemOperation | |
{ | ||
readonly ImmutableList<string> _matchOnMetadata; | ||
readonly MatchOnMetadataOptions _matchOnMetadataOptions; | ||
private MetadataSet<P, I> metadataSet; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: Naming convention, field name is missing the leading underscore. |
||
|
||
public RemoveOperation(RemoveOperationBuilder builder, LazyItemEvaluator<P, I, M, D> lazyEvaluator) | ||
: base(builder, lazyEvaluator) | ||
|
@@ -55,13 +56,23 @@ protected override ImmutableList<I> SelectItems(ImmutableList<ItemData>.Builder | |
var items = ImmutableHashSet.CreateBuilder<I>(); | ||
foreach (ItemData item in listBuilder) | ||
{ | ||
if (_matchOnMetadata.IsEmpty ? _itemSpec.MatchesItem(item.Item) : _itemSpec.MatchesItemOnMetadata(item.Item, _matchOnMetadata, _matchOnMetadataOptions)) | ||
if (_matchOnMetadata.IsEmpty ? _itemSpec.MatchesItem(item.Item) : MatchesItemOnMetadata(item.Item)) | ||
items.Add(item.Item); | ||
} | ||
|
||
return items.ToImmutableList(); | ||
} | ||
|
||
private bool MatchesItemOnMetadata(I item) | ||
{ | ||
if (metadataSet == null) | ||
Forgind marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
metadataSet = new MetadataSet<P, I>(_matchOnMetadataOptions, _matchOnMetadata, _itemSpec); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: This lazy initialization of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I can put it in the constructor—shouldn't affect perf, assuming operations are always executed. |
||
} | ||
|
||
return metadataSet.Contains(_matchOnMetadata.Select(m => item.GetMetadataValue(m))); | ||
} | ||
|
||
protected override void SaveItems(ImmutableList<I> items, ImmutableList<ItemData>.Builder listBuilder) | ||
{ | ||
if (!_conditionResult) | ||
|
@@ -100,15 +111,4 @@ public RemoveOperationBuilder(ProjectItemElement itemElement, bool conditionResu | |
} | ||
} | ||
} | ||
|
||
public enum MatchOnMetadataOptions | ||
{ | ||
CaseSensitive, | ||
CaseInsensitive, | ||
PathLike | ||
} | ||
|
||
public static class MatchOnMetadataConstants { | ||
public const MatchOnMetadataOptions MatchOnMetadataOptionsDefaultValue = MatchOnMetadataOptions.CaseSensitive; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd keep this as a separate method to have ExecuteRemove read nicer by only contain top level intent and delegating more nitty gritty implementation details to helper methods. Like the other if branch.