Skip to content
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

ResolvePackageFileConflicts performance enhancements #1805

Merged
merged 6 commits into from
Dec 14, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 31 additions & 2 deletions src/Tasks/Common/ConflictResolution/ConflictItem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ internal interface IConflictItem
Version FileVersion { get; }
string PackageId { get; }
string DisplayName { get; }

// NOTE: Technically this should be NuGetVersion because System.Version doesn't work with semver.
// However, the only scenarios we need to support this property for in conflict resolution is stable versions
// of System packages. PackageVersion will be null if System.Version can't parse the version (i.e. if is pre-release)
Version PackageVersion { get; }
}

// Wraps an ITask item and adds lazy evaluated properties used by Conflict resolution.
Expand Down Expand Up @@ -103,7 +108,7 @@ public string FileName
{
if (_fileName == null)
{
_fileName = OriginalItem == null ? String.Empty : OriginalItem.GetMetadata(MetadataNames.FileName) + OriginalItem.GetMetadata(MetadataNames.Extension);
_fileName = OriginalItem == null ? String.Empty : Path.GetFileName(OriginalItem.ItemSpec);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI - this change is unrelated to the package override work, but I saw roughly a 200ms gain in OrchardCore by making this change.

}
return _fileName;
}
Expand Down Expand Up @@ -165,7 +170,31 @@ public string PackageId
}
private set { _packageId = value; }
}


private bool _hasPackageVersion;
private Version _packageVersion;
public Version PackageVersion
{
get
{
if (!_hasPackageVersion)
{
_packageVersion = null;

var packageVersionString = OriginalItem?.GetMetadata(nameof(MetadataNames.NuGetPackageVersion)) ?? String.Empty;

if (packageVersionString.Length != 0)
{
Version.TryParse(packageVersionString, out _packageVersion);
}

// PackageVersion may be null but don't try to recalculate it
_hasPackageVersion = true;
}

return _packageVersion;
}
}

private string _sourcePath;
public string SourcePath
Expand Down
10 changes: 9 additions & 1 deletion src/Tasks/Common/ConflictResolution/ConflictResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,13 @@ internal class ConflictResolver<TConflictItem> where TConflictItem : class, ICon
private Dictionary<string, TConflictItem> winningItemsByKey = new Dictionary<string, TConflictItem>();
private ILog log;
private PackageRank packageRank;
private PackageOverrideResolver<TConflictItem> packageOverrideResolver;

public ConflictResolver(PackageRank packageRank, ILog log)
public ConflictResolver(PackageRank packageRank, PackageOverrideResolver<TConflictItem> packageOverrideResolver, ILog log)
{
this.log = log;
this.packageRank = packageRank;
this.packageOverrideResolver = packageOverrideResolver;
}

public void ResolveConflicts(IEnumerable<TConflictItem> conflictItems, Func<TConflictItem, string> getItemKey,
Expand Down Expand Up @@ -87,6 +89,12 @@ public ConflictResolver(PackageRank packageRank, ILog log)

private TConflictItem ResolveConflict(TConflictItem item1, TConflictItem item2)
{
var winner = packageOverrideResolver.Resolve(item1, item2);
if (winner != null)
{
return winner;
}

string conflictMessage = string.Format(CultureInfo.CurrentCulture, Strings.EncounteredConflict,
item1.DisplayName,
item2.DisplayName);
Expand Down
1 change: 1 addition & 0 deletions src/Tasks/Common/ConflictResolution/MetadataNames.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ static class MetadataNames
public const string FileName = "FileName";
public const string HintPath = "HintPath";
public const string NuGetPackageId = "NuGetPackageId";
public const string NuGetPackageVersion = "NuGetPackageVersion";
public const string Path = "Path";
public const string Private = "Private";
public const string TargetPath = "TargetPath";
Expand Down
64 changes: 64 additions & 0 deletions src/Tasks/Common/ConflictResolution/PackageOverride.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Build.Framework;

namespace Microsoft.NET.Build.Tasks.ConflictResolution
{
/// <summary>
/// A PackageOverride contains information about a package that overrides
/// a set of packages up to a certain version.
/// </summary>
/// <remarks>
/// For example, Microsoft.NETCore.App overrides System.Console up to version 4.3.0,
/// System.IO up to version version 4.3.0, etc.
/// </remarks>
internal class PackageOverride
{
public string PackageName { get; }
public Dictionary<string, Version> OverridenPackages { get; }

private PackageOverride(string packageName, IEnumerable<Tuple<string, Version>> overridenPackages)
{
PackageName = packageName;

OverridenPackages = new Dictionary<string, Version>(StringComparer.OrdinalIgnoreCase);
foreach (Tuple<string, Version> package in overridenPackages)
{
OverridenPackages[package.Item1] = package.Item2;
}
}

public static PackageOverride Create(ITaskItem packageOverrideItem)
{
string packageName = packageOverrideItem.ItemSpec;
string overridenPackagesString = packageOverrideItem.GetMetadata(MetadataKeys.OverridenPackages);

return new PackageOverride(packageName, CreateOverridenPackages(overridenPackagesString));
}

private static IEnumerable<Tuple<string, Version>> CreateOverridenPackages(string overridenPackagesString)
{
if (!string.IsNullOrEmpty(overridenPackagesString))
{
overridenPackagesString = overridenPackagesString.Trim();
string[] overridenPackagesAndVersions = overridenPackagesString.Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
foreach (string overridenPackagesAndVersion in overridenPackagesAndVersions)
{
string trimmedOverridenPackagesAndVersion = overridenPackagesAndVersion.Trim();
int separatorIndex = trimmedOverridenPackagesAndVersion.IndexOf('|');
if (separatorIndex != -1)
{
if (Version.TryParse(trimmedOverridenPackagesAndVersion.Substring(separatorIndex + 1), out Version version))
{
yield return Tuple.Create(trimmedOverridenPackagesAndVersion.Substring(0, separatorIndex), version);
}
}
}
}
}
}
}
106 changes: 106 additions & 0 deletions src/Tasks/Common/ConflictResolution/PackageOverrideResolver.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using System.Collections.Generic;
using Microsoft.Build.Framework;

namespace Microsoft.NET.Build.Tasks.ConflictResolution
{
/// <summary>
/// Resolves conflicts between items by allowing specific packages to override
/// all items coming from a set of packages up to a certain version of each package.
/// </summary>
internal class PackageOverrideResolver<TConflictItem> where TConflictItem : class, IConflictItem
{
private ITaskItem[] _packageOverrideItems;
private Lazy<Dictionary<string, PackageOverride>> _packageOverrides;

public PackageOverrideResolver(ITaskItem[] packageOverrideItems)
{
_packageOverrideItems = packageOverrideItems;
_packageOverrides = new Lazy<Dictionary<string, PackageOverride>>(() => BuildPackageOverrides());
}

public Dictionary<string, PackageOverride> PackageOverrides => _packageOverrides.Value;

private Dictionary<string, PackageOverride> BuildPackageOverrides()
{
Dictionary<string, PackageOverride> result;

if (_packageOverrideItems?.Length > 0)
{
result = new Dictionary<string, PackageOverride>(_packageOverrideItems.Length, StringComparer.OrdinalIgnoreCase);

foreach (ITaskItem packageOverrideItem in _packageOverrideItems)
{
PackageOverride packageOverride = PackageOverride.Create(packageOverrideItem);

if (result.TryGetValue(packageOverride.PackageName, out PackageOverride existing))
{
MergePackageOverrides(packageOverride, existing);
}
else
{
result[packageOverride.PackageName] = packageOverride;
}
}
}
else
{
result = null;
}

return result;
}

/// <summary>
/// Merges newPackageOverride into existingPackageOverride by adding all the new overriden packages
/// and taking the highest version when they both contain the same overriden package.
/// </summary>
private static void MergePackageOverrides(PackageOverride newPackageOverride, PackageOverride existingPackageOverride)
{
foreach (KeyValuePair<string, Version> newOverride in newPackageOverride.OverridenPackages)
{
if (existingPackageOverride.OverridenPackages.TryGetValue(newOverride.Key, out Version existingOverrideVersion))
{
if (existingOverrideVersion < newOverride.Value)
{
existingPackageOverride.OverridenPackages[newOverride.Key] = newOverride.Value;
}
}
else
{
existingPackageOverride.OverridenPackages[newOverride.Key] = newOverride.Value;
}
}
}

public TConflictItem Resolve(TConflictItem item1, TConflictItem item2)
{
if (PackageOverrides != null)
{
PackageOverride packageOverride;
Version version;
if (item1.PackageId != null
&& PackageOverrides.TryGetValue(item1.PackageId, out packageOverride)
&& packageOverride.OverridenPackages.TryGetValue(item2.PackageId, out version)
&& item2.PackageVersion != null
&& item2.PackageVersion <= version)
{
return item1;
}
else if (item2.PackageId != null
&& PackageOverrides.TryGetValue(item2.PackageId, out packageOverride)
&& packageOverride.OverridenPackages.TryGetValue(item1.PackageId, out version)
&& item1.PackageVersion != null
&& item1.PackageVersion <= version)
{
return item2;
}
}

return null;
}
}
}
18 changes: 15 additions & 3 deletions src/Tasks/Common/ConflictResolution/ResolvePackageFileConflicts.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,17 @@ public class ResolvePackageFileConflicts : TaskBase
/// </summary>
public string[] PreferredPackages { get; set; }

/// <summary>
/// A collection of items that contain information of which packages get overriden
/// by which packages before doing any other conflict resolution.
/// </summary>
/// <remarks>
/// This is an optimizaiton so AssemblyVersions, FileVersions, etc. don't need to be read
/// in the default cases where platform packages (Microsoft.NETCore.App) should override specific packages
/// (System.Console v4.3.0).
/// </remarks>
public ITaskItem[] PackageOverrides { get; set; }

[Output]
public ITaskItem[] ReferencesWithoutConflicts { get; set; }

Expand All @@ -44,6 +55,7 @@ protected override void ExecuteCore()
{
var log = new MSBuildLog(Log);
var packageRanks = new PackageRank(PreferredPackages);
var packageOverrides = new PackageOverrideResolver<ConflictItem>(PackageOverrides);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How much perf did you give back by creating this every time the task runs? I thought the version with a hard coded structure created every time was significantly slower than a static list.

The parsing logic looks very heavy on allocations: split, trim, tuple, yield.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My original mistake was doing the data creation in the ConflictResolver constructor, which is called 3x per invocation of the task. So that's why it came up when I profiled at that time (it was 3x what it needed to be).

I did some profiling of my current approach, and this creation takes <10% of the time of the new task. The real time left in the task (according to PerfView) is getting Item Metadata multiple times for every ConflictItem. This accounts for ~60% of the time of the new task according to the profiles I've looked at.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BTW - I've updated the original post with the perf numbers I'm seeing on my machine.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, I'm satisfied. We can tune the rest if it ever it shows up and this is clearly a nice win already.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You made my realize that I should be lazy-loading this data for the case where there are no conflicts. No reason to build the dictionary up, until we need it. I've pushed a change for that.


// Treat assemblies from FrameworkList.xml as platform assemblies that also get considered at compile time
IEnumerable<ConflictItem> compilePlatformItems = null;
Expand All @@ -60,7 +72,7 @@ protected override void ExecuteCore()
// resolve conflicts at compile time
var referenceItems = GetConflictTaskItems(References, ConflictItemType.Reference).ToArray();

var compileConflictScope = new ConflictResolver<ConflictItem>(packageRanks, log);
var compileConflictScope = new ConflictResolver<ConflictItem>(packageRanks, packageOverrides, log);

compileConflictScope.ResolveConflicts(referenceItems,
ci => ItemUtilities.GetReferenceFileName(ci.OriginalItem),
Expand All @@ -74,7 +86,7 @@ protected override void ExecuteCore()
}

// resolve conflicts that class in output
var runtimeConflictScope = new ConflictResolver<ConflictItem>(packageRanks, log);
var runtimeConflictScope = new ConflictResolver<ConflictItem>(packageRanks, packageOverrides, log);

runtimeConflictScope.ResolveConflicts(referenceItems,
ci => ItemUtilities.GetReferenceTargetPath(ci.OriginalItem),
Expand All @@ -95,7 +107,7 @@ protected override void ExecuteCore()

// resolve conflicts with platform (eg: shared framework) items
// we only commit the platform items since its not a conflict if other items share the same filename.
var platformConflictScope = new ConflictResolver<ConflictItem>(packageRanks, log);
var platformConflictScope = new ConflictResolver<ConflictItem>(packageRanks, packageOverrides, log);
var platformItems = PlatformManifests?.SelectMany(pm => PlatformManifestReader.LoadConflictItems(pm.ItemSpec, log)) ?? Enumerable.Empty<ConflictItem>();

if (compilePlatformItems != null)
Expand Down
3 changes: 3 additions & 0 deletions src/Tasks/Common/MetadataKeys.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,5 +50,8 @@ internal static class MetadataKeys

// Publish Target Manifest
public const string RuntimeStoreManifestNames = "RuntimeStoreManifestNames";

// Conflict Resolution
public const string OverridenPackages = "OverridenPackages";
}
}
Loading