-
Notifications
You must be signed in to change notification settings - Fork 1.4k
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
Improve performance of projects with large numbers of consecutive item updates without wildcards Fixes #5776 #5853
Changes from 10 commits
1cc6fd0
68d8d32
2128721
b69cf22
e4816f2
2f91f98
c4763c0
fa1eb3b
d6a7a46
afd6a69
7841914
61b9eca
9e5fe73
07d3927
e0bf461
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 |
---|---|---|
|
@@ -16,7 +16,6 @@ | |
using Shouldly; | ||
using InvalidProjectFileException = Microsoft.Build.Exceptions.InvalidProjectFileException; | ||
using Xunit; | ||
using System.Runtime.InteropServices; | ||
|
||
namespace Microsoft.Build.UnitTests.OM.Definition | ||
{ | ||
|
@@ -2607,6 +2606,141 @@ public void LastUpdateWins() | |
ObjectModelHelpers.AssertItemHasMetadata(expectedUpdate, items[0]); | ||
} | ||
|
||
[Theory] | ||
[InlineData("abc", "def", "abc")] | ||
[InlineData("abc", "de*", "abc")] | ||
[InlineData("a*c", "def", "abc")] | ||
[InlineData("abc", "def", "*bc")] | ||
[InlineData("abc", "d*f", "*bc")] | ||
[InlineData("*c", "d*f", "*bc")] | ||
[InlineData("a*", "d*", "abc")] | ||
public void UpdatesProceedInOrder(string first, string second, string third) | ||
{ | ||
string contents = $@" | ||
<i Include='abc'> | ||
<m1>m1_contents</m1> | ||
</i> | ||
<j Include='def'> | ||
<m1>m1_contents</m1> | ||
</j> | ||
<i Update='{first}'> | ||
<m1>first</m1> | ||
</i> | ||
<j Update='{second}'> | ||
<m1>second</m1> | ||
</j> | ||
<i Update='{third}'> | ||
<m1>third</m1> | ||
</i> | ||
"; | ||
IList<ProjectItem> items = ObjectModelHelpers.GetItemsFromFragment(contents, allItems: true); | ||
Dictionary<string, string> expectedUpdatei = new Dictionary<string, string> | ||
{ | ||
{"m1", "third" } | ||
}; | ||
Dictionary<string, string> expectedUpdatej = new Dictionary<string, string> | ||
{ | ||
{"m1", "second" } | ||
}; | ||
|
||
ObjectModelHelpers.AssertItemHasMetadata(expectedUpdatei, items[0]); | ||
ObjectModelHelpers.AssertItemHasMetadata(expectedUpdatej, items[1]); | ||
} | ||
|
||
[Fact] | ||
public void UpdatingIndividualItemsProceedsInOrder() | ||
{ | ||
string contents = @" | ||
<i Include='a;b;c'> | ||
<m1>m1_contents</m1> | ||
</i> | ||
<i Update='a'> | ||
<m1>second</m1> | ||
</i> | ||
<i Update='b'> | ||
<m1>third</m1> | ||
</i> | ||
<i Update='c'> | ||
<m1>fourth</m1> | ||
</i> | ||
<afterFirst Include='@(i)' /> | ||
rainersigwald marked this conversation as resolved.
Show resolved
Hide resolved
|
||
<i Update='*'> | ||
<m1>sixth</m1> | ||
</i> | ||
<afterSecond Include='@(i)' /> | ||
<i Update='b'> | ||
<m1>seventh</m1> | ||
</i> | ||
<afterThird Include='@(i)' /> | ||
<i Update='c'> | ||
<m1>eighth</m1> | ||
</i> | ||
<afterFourth Include='@(i)' /> | ||
"; | ||
IList<ProjectItem> items = ObjectModelHelpers.GetItemsFromFragment(contents, allItems: true); | ||
Dictionary<string, string> expectedAfterFirsta = new Dictionary<string, string> | ||
{ | ||
{"m1", "second" } | ||
}; | ||
Dictionary<string, string> expectedAfterFirstb = new Dictionary<string, string> | ||
{ | ||
{"m1", "third" } | ||
}; | ||
Dictionary<string, string> expectedAfterFirstc = new Dictionary<string, string> | ||
{ | ||
{"m1", "fourth" } | ||
}; | ||
Dictionary<string, string> expectedAfterSeconda = new Dictionary<string, string> | ||
{ | ||
{"m1", "sixth" } | ||
}; | ||
Dictionary<string, string> expectedAfterSecondb = new Dictionary<string, string> | ||
{ | ||
{"m1", "sixth" } | ||
}; | ||
Dictionary<string, string> expectedAfterSecondc = new Dictionary<string, string> | ||
{ | ||
{"m1", "sixth" } | ||
}; | ||
Dictionary<string, string> expectedAfterThirda = new Dictionary<string, string> | ||
{ | ||
{"m1", "sixth" } | ||
}; | ||
Dictionary<string, string> expectedAfterThirdb = new Dictionary<string, string> | ||
{ | ||
{"m1", "seventh" } | ||
}; | ||
Dictionary<string, string> expectedAfterThirdc = new Dictionary<string, string> | ||
{ | ||
{"m1", "sixth" } | ||
}; | ||
Dictionary<string, string> expectedAfterFourtha = new Dictionary<string, string> | ||
{ | ||
{"m1", "sixth" } | ||
}; | ||
Dictionary<string, string> expectedAfterFourthb = new Dictionary<string, string> | ||
{ | ||
{"m1", "seventh" } | ||
}; | ||
Dictionary<string, string> expectedAfterFourthc = new Dictionary<string, string> | ||
{ | ||
{"m1", "eighth" } | ||
}; | ||
|
||
ObjectModelHelpers.AssertItemHasMetadata(expectedAfterFirsta, items[3]); | ||
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. Should AssertItemHasMetadata have an overload that accepts a tuple name-value, so you can eliminate all these dictionaries in the test? |
||
ObjectModelHelpers.AssertItemHasMetadata(expectedAfterFirstb, items[4]); | ||
ObjectModelHelpers.AssertItemHasMetadata(expectedAfterFirstc, items[5]); | ||
ObjectModelHelpers.AssertItemHasMetadata(expectedAfterSeconda, items[6]); | ||
ObjectModelHelpers.AssertItemHasMetadata(expectedAfterSecondb, items[7]); | ||
ObjectModelHelpers.AssertItemHasMetadata(expectedAfterSecondc, items[8]); | ||
ObjectModelHelpers.AssertItemHasMetadata(expectedAfterThirda, items[9]); | ||
ObjectModelHelpers.AssertItemHasMetadata(expectedAfterThirdb, items[10]); | ||
ObjectModelHelpers.AssertItemHasMetadata(expectedAfterThirdc, items[11]); | ||
ObjectModelHelpers.AssertItemHasMetadata(expectedAfterFourtha, items[12]); | ||
ObjectModelHelpers.AssertItemHasMetadata(expectedAfterFourthb, items[13]); | ||
ObjectModelHelpers.AssertItemHasMetadata(expectedAfterFourthc, items[14]); | ||
} | ||
|
||
[Fact] | ||
public void UpdateWithNoMetadataShouldNotAffectItems() | ||
{ | ||
|
@@ -2842,6 +2976,25 @@ public void UpdateFromReferencedItemShouldBeCaseInsensitive() | |
ObjectModelHelpers.AssertItemHasMetadata(expectedMetadataA, items[1]); | ||
} | ||
|
||
[Fact] | ||
public void UpdateFromReferencedItemShouldBeCaseInsensitive2() | ||
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. Can you name this more explicitly? |
||
{ | ||
string content = @" | ||
<to Include='a' /> | ||
|
||
<to Update='A' m='m1_contents' />"; | ||
|
||
IList<ProjectItem> items = ObjectModelHelpers.GetItemsFromFragment(content, true); | ||
|
||
var expectedMetadataA = new Dictionary<string, string> | ||
{ | ||
{"m", "m1_contents"}, | ||
}; | ||
|
||
items[0].ItemType.ShouldBe("to"); | ||
ObjectModelHelpers.AssertItemHasMetadata(expectedMetadataA, items[0]); | ||
} | ||
|
||
[Fact] | ||
public void UndeclaredQualifiedMetadataReferencesInUpdateShouldResolveToEmptyStrings() | ||
{ | ||
|
@@ -3180,28 +3333,15 @@ public void UpdateAndRemoveShouldUseCaseInsensitiveMatching() | |
|
||
IList<ProjectItem> items = ObjectModelHelpers.GetItemsFromFragment(content); | ||
|
||
if (FileUtilities.GetIsFileSystemCaseSensitive()) | ||
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'm concerned about this removal. Isn't it a behavior change? 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. Yes, but we decided this was best. See #5888 (comment) |
||
{ | ||
var expectedUpdated = new Dictionary<string, string> | ||
{ | ||
{"m1", "m1_contents"}, | ||
{"m2", "m2_contents"}, | ||
}; | ||
items.ShouldHaveSingleItem(); | ||
|
||
ObjectModelHelpers.AssertItemHasMetadata(expectedUpdated, items[0]); | ||
} | ||
else | ||
var expectedUpdated = new Dictionary<string, string> | ||
{ | ||
items.ShouldHaveSingleItem(); | ||
|
||
var expectedUpdated = new Dictionary<string, string> | ||
{ | ||
{"m1", "m1_updated"}, | ||
{"m2", "m2_updated"}, | ||
}; | ||
{"m1", "m1_updated"}, | ||
{"m2", "m2_updated"}, | ||
}; | ||
|
||
ObjectModelHelpers.AssertItemHasMetadata(expectedUpdated, items[0]); | ||
} | ||
ObjectModelHelpers.AssertItemHasMetadata(expectedUpdated, items[0]); | ||
} | ||
|
||
public static IEnumerable<Object[]> UpdateAndRemoveShouldWorkWithEscapedCharactersTestData | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -15,6 +15,9 @@ internal partial class LazyItemEvaluator<P, I, M, D> | |
class UpdateOperation : LazyItemOperation | ||
{ | ||
private readonly ImmutableList<ProjectMetadataElement> _metadata; | ||
private ImmutableList<ItemBatchingContext>.Builder _itemsToUpdate = null; | ||
private ItemSpecMatchesItem _matchItemSpec = null; | ||
private bool? _needToExpandMetadataForEachItem = null; | ||
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. Double checking my own review of this file, it looks like the only changes made here are caching the items needed to update and reorganizing some code into functions? 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. Also: +10 points for general code cleanup of legacy code. 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. Yep, pretty much. I should note that I didn't do the caching part because of expected perf gains but because I wanted to access these from different functions. |
||
|
||
public UpdateOperation(OperationBuilderWithMetadata builder, LazyItemEvaluator<P, I, M, D> lazyEvaluator) | ||
: base(builder, lazyEvaluator) | ||
|
@@ -43,23 +46,77 @@ protected override void ApplyImpl(ImmutableList<ItemData>.Builder listBuilder, I | |
return; | ||
} | ||
|
||
ItemSpecMatchesItem matchItemspec; | ||
bool? needToExpandMetadataForEachItem = null; | ||
SetMatchItemSpec(); | ||
_itemsToUpdate ??= ImmutableList.CreateBuilder<ItemBatchingContext>(); | ||
_itemsToUpdate.Clear(); | ||
|
||
for (int i = 0; i < listBuilder.Count; i++) | ||
{ | ||
var itemData = listBuilder[i]; | ||
|
||
var matchResult = _matchItemSpec(_itemSpec, itemData.Item); | ||
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. Nice to have while you're here: A comment here suggesting what's happening, ideally with a small example. Something like: "see if, for example, *.cs matches somefilename.cs" ( don't use this example 🙂) |
||
|
||
if (matchResult.IsMatch) | ||
{ | ||
listBuilder[i] = UpdateItem(listBuilder[i], matchResult.CapturedItemsFromReferencedItemTypes); | ||
} | ||
} | ||
|
||
DecorateItemsWithMetadata(_itemsToUpdate.ToImmutableList(), _metadata, _needToExpandMetadataForEachItem); | ||
} | ||
|
||
/// <summary> | ||
/// Apply the Update operation to the item if it matches. | ||
/// </summary> | ||
/// <param name="item">The item to check for a match.</param> | ||
/// <returns>The updated item.</returns> | ||
internal ItemData UpdateItem(ItemData item) | ||
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. Definitely summarize this method since it's internal. |
||
{ | ||
if (_conditionResult) | ||
{ | ||
SetMatchItemSpec(); | ||
_itemsToUpdate ??= ImmutableList.CreateBuilder<ItemBatchingContext>(); | ||
_itemsToUpdate.Clear(); | ||
MatchResult matchResult = _matchItemSpec(_itemSpec, item.Item); | ||
if (matchResult.IsMatch) | ||
{ | ||
ItemData clonedData = UpdateItem(item, matchResult.CapturedItemsFromReferencedItemTypes); | ||
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. Shouldn't you call the private 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. What do you mean? That's what it's doing. |
||
DecorateItemsWithMetadata(_itemsToUpdate.ToImmutableList(), _metadata, _needToExpandMetadataForEachItem); | ||
return clonedData; | ||
} | ||
} | ||
return item; | ||
} | ||
|
||
private ItemData UpdateItem(ItemData item, Dictionary<string, I> capturedItemsFromReferencedItemTypes) | ||
{ | ||
// items should be deep immutable, so clone and replace items before mutating them | ||
// otherwise, with GetItems caching enabled, the mutations would leak into the cache causing | ||
// future operations to mutate the state of past operations | ||
ItemData clonedData = item.Clone(_itemFactory, _itemElement); | ||
_itemsToUpdate.Add(new ItemBatchingContext(clonedData.Item, capturedItemsFromReferencedItemTypes)); | ||
return clonedData; | ||
} | ||
|
||
/// <summary> | ||
/// This sets the function used to determine whether an item matches an item spec. | ||
/// </summary> | ||
private void SetMatchItemSpec() | ||
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'd add a quick description here or at the call site. Preferably here. |
||
{ | ||
if (ItemspecContainsASingleBareItemReference(_itemSpec, _itemElement.ItemType)) | ||
{ | ||
// Perf optimization: If the Update operation references itself (e.g. <I Update="@(I)"/>) | ||
// then all items are updated and matching is not necessary | ||
matchItemspec = (itemSpec, item) => new MatchResult(true, null); | ||
_matchItemSpec = (itemSpec, item) => new MatchResult(true, null); | ||
} | ||
else if (ItemSpecContainsItemReferences(_itemSpec) | ||
&& QualifiedMetadataReferencesExist(_metadata, out needToExpandMetadataForEachItem) | ||
&& QualifiedMetadataReferencesExist(_metadata, out _needToExpandMetadataForEachItem) | ||
&& !Traits.Instance.EscapeHatches.DoNotExpandQualifiedMetadataInUpdateOperation) | ||
{ | ||
var itemReferenceFragments = _itemSpec.Fragments.OfType<ItemSpec<P,I>.ItemExpressionFragment>().ToArray(); | ||
var nonItemReferenceFragments = _itemSpec.Fragments.Where(f => !(f is ItemSpec<P,I>.ItemExpressionFragment)).ToArray(); | ||
var itemReferenceFragments = _itemSpec.Fragments.OfType<ItemSpec<P, I>.ItemExpressionFragment>().ToArray(); | ||
var nonItemReferenceFragments = _itemSpec.Fragments.Where(f => !(f is ItemSpec<P, I>.ItemExpressionFragment)).ToArray(); | ||
|
||
matchItemspec = (itemSpec, item) => | ||
_matchItemSpec = (itemSpec, item) => | ||
{ | ||
var isMatch = nonItemReferenceFragments.Any(f => f.IsMatch(item.EvaluatedInclude)); | ||
Dictionary<string, I> capturedItemsFromReferencedItemTypes = null; | ||
|
@@ -84,30 +141,8 @@ protected override void ApplyImpl(ImmutableList<ItemData>.Builder listBuilder, I | |
} | ||
else | ||
{ | ||
matchItemspec = (itemSpec, item) => new MatchResult(itemSpec.MatchesItem(item), null); | ||
} | ||
|
||
var itemsToUpdate = ImmutableList.CreateBuilder<ItemBatchingContext>(); | ||
|
||
for (int i = 0; i < listBuilder.Count; i++) | ||
{ | ||
var itemData = listBuilder[i]; | ||
|
||
var matchResult = matchItemspec(_itemSpec, itemData.Item); | ||
|
||
if (matchResult.IsMatch) | ||
{ | ||
// items should be deep immutable, so clone and replace items before mutating them | ||
// otherwise, with GetItems caching enabled, the mutations would leak into the cache causing | ||
// future operations to mutate the state of past operations | ||
var clonedItemData = listBuilder[i].Clone(_itemFactory, _itemElement); | ||
listBuilder[i] = clonedItemData; | ||
|
||
itemsToUpdate.Add(new ItemBatchingContext(clonedItemData.Item, matchResult.CapturedItemsFromReferencedItemTypes)); | ||
} | ||
_matchItemSpec = (itemSpec, item) => new MatchResult(itemSpec.MatchesItem(item), null); | ||
} | ||
|
||
DecorateItemsWithMetadata(itemsToUpdate.ToImmutableList(), _metadata, needToExpandMetadataForEachItem); | ||
} | ||
|
||
private bool QualifiedMetadataReferencesExist(ImmutableList<ProjectMetadataElement> metadata, out bool? needToExpandMetadataForEachItem) | ||
|
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.
Deleted to avoid merge conflict.