msbuild: ResolveAssemblyReference is slow on .NET Core with many references

ResolveAssemblyReferences runs unconditionally even on incremental builds (because the user may have installed a targeting pack or, for full-framework scenarios, GACed an assembly). This makes it performance-sensitive.

With the advent of .NET Core micro-assemblies, it has become common to pass many, many references to RAR. This exacerbates performance problems.

@rynowak was kind enough to run a profiler to see this: image

About this issue

  • Original URL
  • State: open
  • Created 7 years ago
  • Reactions: 18
  • Comments: 64 (62 by maintainers)

Commits related to this issue

Most upvoted comments

Now that a few of these changes are in, I’ve taken some quick sample performance summaries building a couple .NET Core projects using the current tip of mater https://github.com/Microsoft/msbuild/commit/b630e674c729787d57a485ef5338915643eb1cea to show improvements we’ve made in RAR times since 15.9, although there’s still plenty of room for improvement. These are all 3rd builds, incremental no-op. Included single-proc since multi-proc timings are added cumulatively across processes, but still useful to see.

MSBuild 15.9

WebLargeCore Single-Proc

Target Performance Summary:
    16923 ms  Build                                    130 calls

Task Performance Summary:
     5289 ms  ResolveAssemblyReference                 129 calls

WebLargeCore Multi-Proc

Target Performance Summary:
     6568 ms  Build                                    130 calls

Task Performance Summary:
     9855 ms  ResolveAssemblyReference                 129 calls

OrchardCore Single-Proc

Target Performance Summary:
    19573 ms  Build                                    133 calls

Task Performance Summary:
     6471 ms  ResolveAssemblyReference                 132 calls

OrchardCore Multi-Proc

Target Performance Summary:
     7888 ms  Build                                    133 calls

Task Performance Summary:
    13469 ms  ResolveAssemblyReference                 132 calls

MSBuild Master

WebLargeCore Single-Proc (-44% RAR time)

Target Performance Summary:
    13996 ms  Build                                    130 calls

Task Performance Summary:
     2976 ms  ResolveAssemblyReference                 129 calls

WebLargeCore Multi-Proc

Target Performance Summary:
     5329 ms  Build                                    130 calls

Task Performance Summary:
     6154 ms  ResolveAssemblyReference                 129 calls

OrchardCore Single-Proc (-47% RAR time)

Target Performance Summary:
    15618 ms  Build                                    133 calls

Task Performance Summary:
     3398 ms  ResolveAssemblyReference                 132 calls

OrchardCore Multi-Proc


Target Performance Summary:
     6039 ms  Build                                    133 calls

Task Performance Summary:
     7147 ms  ResolveAssemblyReference                 132 calls

I just collected some data from a dotnet/runtime libraries build with ~1400 evaluations (outer + inner builds). I crossed out the tasks that are dotnet/runtime specific (a fix for ValidatePackage no-op build times is tracked via https://github.com/dotnet/sdk/issues/23517).

image

Assuming RAR only runs in inner-builds, this should represent the time it takes RAR to serve ~1100 inner builds. I was looking into speeding up incremental builds in dotnet/runtime libs and I believe the highest impact contributors on the msbuild side are:

  • Project evaluation (55% of the overall fully incremental build). Once during static graph restore and then again during the actual build.
  • RAR (17%)
  • ResolveTargetingPackAssets (2.5%)
  • ResolvePackageFileConflicts (2%)

23% are dotnet/runtime specific and must be fixed by ourselves. Just wanted to share this data in case it helps.

If you are generating reference items, can you try adding ExternallyResolved=true metadata to them?

Han, maybe exactly what I’m looking for, gonna try that, thanks 👍

One thing of note: MSBuild only calls that AssemblyName constructor because AssemblyName.Clone() isn’t available in .NET Standard 1.x. One possible improvement here would be to move to .NET Core 2.0.