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

Speed up package asset resolution #1857

Merged
merged 7 commits into from
Jan 25, 2018

Conversation

nguerrera
Copy link
Contributor

@nguerrera nguerrera commented Jan 16, 2018

Raise only package assets that are used to build

The new task (ResolvePackageAssets) unlike the old ResolvePackageDependencies, only raises items that that will actually be consumed by the build. The new
items are also much easier to consume in targets and do not require "joins" to resolve paths, querying via WithMetadataValue, etc.

I'm seeing about 10% speed gain in single MVC project, and 15% in OrchardCore on incremental dotnet build (with restore). The gains when the binary log is enabled are more pronounced: about 25% with 4X improvement in working setmand 2X improvement in .binlog size + time to open in structured log viewer! Not to mention, that the log is much easier to interpret without all of the extra items.

There is a breaking change here:

  • If you use the old items from RunResolvePackageDependencies, then you need to depend-on/invoke that target. The code has not been deleted, it just won't be invoked if you don't use it. This is not a common sceanario.
  • There are also intermediate _Xxx ("private") items that have disappeared.
  • Duplicate/unnecessary metadata has been trimmed from items (e.g. NuGetPackageId vs PackageName).

cc @dsplaisted @livarcocc @mikeharder

@mikeharder
Copy link
Contributor

I'm seeing approximately the same perf improvements:

Solution Operation 7947 7947-ResolvePackageAssets Improvement
WebSmall BuildIncrementalSourceChangedLeaf 2.8 2.5 13%
WebLarge BuildIncrementalSourceChangedLeaf 15.2 14.7 3%
OrchardCore BuildIncrementalNoChange 22.5 18.9 16%

I suspect the improvement for WebLarge is relatively small because most of the projects are class libraries with no package references. I will add package references to the WebLarge class libraries to make it closer to a real-world solution (like OrchardCore).

@nguerrera nguerrera changed the title [WIP] Speed up package asset resolution Speed up package asset resolution Jan 19, 2018
@nguerrera nguerrera requested review from dsplaisted, livarcocc and a team January 19, 2018 18:45
@nguerrera
Copy link
Contributor Author

Ready for review. I am still looking into adding some end-to-end tests for analyzers and content files because there don't seem to be any and they are impacted by this.

@@ -13,7 +13,7 @@ namespace Microsoft.NET.Build.Tasks
/// set of Packages.
/// </summary>
/// <remarks>
/// Both Items and Packages are expected to have 'PackageName' and 'PackageVersion'
/// Both Items and Packages are expected to have ('PackageName' and 'PackageVersion)' or ('NuGetPackageId' and 'NuGetPackageVersion')
Copy link
Contributor

Choose a reason for hiding this comment

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

What is the difference between PackageName and NuGetPackageId?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Different conventions were used in different places. Semantically, they are the same.

@@ -251,18 +217,17 @@ private void ProduceContentAsset(ITaskItem contentFile)
}
}

if (contentFile.GetBooleanMetadata("copyToOutput") == true)
if (contentFile.GetBooleanMetadata("CopyToOutput") == true)
Copy link
Contributor

Choose a reason for hiding this comment

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

Don't you have a MetadataKey for this?

{
string outputPath = contentFile.GetMetadata("outputPath");
string outputPath = contentFile.GetMetadata("OutputPath");
Copy link
Contributor

Choose a reason for hiding this comment

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

And this.

@@ -273,13 +238,12 @@ private void ProduceContentAsset(ITaskItem contentFile)
}

// TODO if build action is none do we even need to write the processed file above?
string buildAction = contentFile.GetMetadata("buildAction");
string buildAction = contentFile.GetMetadata("BuildAction");
Copy link
Contributor

Choose a reason for hiding this comment

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

And this.

@nguerrera nguerrera changed the title Speed up package asset resolution WIP Speed up package asset resolution Jan 20, 2018
@nguerrera
Copy link
Contributor Author

Adding back WIP because I discovered a regression and don't want this to get merged.

@dasMulli
Copy link
Contributor

on my system, https://github.com/dasMulli/cli-incremental-perf-testbed shows ~23% improvement in incremental builds - 24s down to 18.5s. (20 projects referencing Microsoft.AspNetCore.All and each other in a chain)

@nguerrera
Copy link
Contributor Author

@dasMulli, cool. Just curious, does that test use the binary logger?

Copy link
Member

@dsplaisted dsplaisted left a comment

Choose a reason for hiding this comment

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

I really really like these changes. It makes the asset resolution much easier to understand, it makes debugging with build logs much easier (since there's not so much junk), and on top of that it improves perf!

I reviewed this commit-by-commit, so some of my comments were addressed in later commits.

💯💯💯💯💯

ContentFiles = RaisePackageAssets(
runtimeTarget,
p => p.ContentFiles,
filter: asset => !string.IsNullOrEmpty(asset.PPOutputPath),
Copy link
Member

Choose a reason for hiding this comment

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

Is this filter correct? Can't there be NuGet content items where the build action is None which are copied to the output directory, or which are EmbeddedResources and so shouldn't be preprocessed? If this filter is correct, then the check inside the setup delegate is redundant.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oops. I forgot to loop back to this. The thing is that we only process the PPOutputPath files in the common case because nuget writes content items directly to generated props otherwise. There's a bool to override it though. I decided to revert the optimization, but now I'm thinking about adding it back. I need to add a content file test still.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed.

setup: (asset, item) =>
{
string locale = asset.Properties["locale"];
item.SetMetadata(MetadataKeys.Culture, "locale");
Copy link
Member

Choose a reason for hiding this comment

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

This seems wrong. Did you mean to set the metadata to the locale variable instead of the string literal "locale"? Is this consumed anywhere, and if not so should it even be added?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed.

setup: (asset, item) =>
{
string directory = Path.GetDirectoryName(asset.Path);
item.SetMetadata("DestinationSubDirectory", directory + Path.DirectorySeparatorChar);
Copy link
Member

Choose a reason for hiding this comment

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

Just to confirm, is asset.Path relative to the right directory here?

IE, it probably shouldn't start with "runtimes".

Copy link
Contributor Author

Choose a reason for hiding this comment

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

By design. It should start with runtimes.

This matches

If you dotnet publish a portable app, the RID specific assets will go to a runtimes/ folder.

{
var set = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

foreach (var group in lockFile.ProjectFileDependencyGroups)
Copy link
Member

Choose a reason for hiding this comment

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

I would really like a definitive reference for the assets file format. It would make writing and understanding code like this a lot easier...


private static string GetPackageNameFromDependency(string dependency)
{
int indexOfWhiteSpace = IndexOfWhiteSpace(dependency);
Copy link
Member

Choose a reason for hiding this comment

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

A comment about why we need to look for whitespace would be helpful here (ie the dependency value is of the format Microsoft.Build >= 15.1.0-dev).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed

DependsOnTargets="_ComputeLockFileReferences;_ComputeLockFileFrameworks">

<ItemGroup>
<ItemGroup Condition="'$(MarkPackageReferencesAsExternallyResolved)' == 'true'">
<!--
Update Reference items with NuGetPackageId metadata to set ExternallyResolved appropriately.
NetStandard.Library adds its assets in targets this way and not in the standard way that
would get ExternallyResolved set in ResolvedCompileFileDefinitions below.
Copy link
Member

Choose a reason for hiding this comment

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

This comment is now out of date, the "ResolvedCompileFileDefinitions below" it refers to has been removed.

@@ -36,6 +36,12 @@ public class ResolvePackageAssets : TaskBase

public bool MarkPackageReferencesAsExternallyResolved { get; set; }

/// <summary>
/// Check that there is at least one package dependency in the RID graph that is not in the
Copy link
Member

Choose a reason for hiding this comment

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

This looks like an incomplete comment. "Not in the" ...what?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed.

{
if (EnsureRuntimePackageDependencies && !string.IsNullOrEmpty(RuntimeIdentifier))
{
if (compileTimeTarget.Libraries.Count >= runtimeTarget.Libraries.Count)
Copy link
Member

Choose a reason for hiding this comment

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

I know you discussed this logic with me, but it's probably a good idea to get feedback from the CoreFx and NuGet folks to make sure it will work correctly.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

cc @ericstj We discussed this approach.


public static HashSet<string> GetProjectFileDependencySet(this LockFile lockFile)
{
string GetPackageNameFromDependency(string dependency)
Copy link
Member

Choose a reason for hiding this comment

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

I'd suggest a comment here explaining what the format of dependency is and thus why we're looking for the whitespace.



// A package is a TransitiveProjectReference if it is a project, is not directly referenced,
// and does not contain a placeholder compile time assembly
Copy link
Member

Choose a reason for hiding this comment

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

What would cause there to be a project in the assets file that meets the other conditions but has a placeholder file?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I couldn't repro it. I talked to @emgarten and it has something to do with assets flags on project refs, but none that I tried caused it. This is just moved from elsewhere.

Example: `Returns="_ActiveTFMFileDependencies"`

This returns the string `"_ActiveTFMFileDependencies"` not
`@(_ActiveTFMFileDependencies)".

Clearly these targets aren't actually run for their return values.

The useless values are cluttering the binlog and confusing me.
The new task (ResolvePackageAssets) unlike the old ResolvePackageDependencies,
only raises items that that will actually be consumed by the build. The new
items are also much easier to consume in targets and do not require "joins"
to resolve paths, querying via WithMetadataValue, etc.
Previously, Microsoft.NETCore.App package would do this in a custom
target with the PackageDependency items that we no longer raise by
default.

Also, use repo-toolset to generate designer code for resx during build.
There was a  project system bug preventing it from getting generated in VS.
@nguerrera nguerrera changed the title WIP Speed up package asset resolution Speed up package asset resolution Jan 25, 2018
@nguerrera nguerrera merged commit f5db1af into dotnet:master Jan 25, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants