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

Workload resolver caching #16414

Merged

Conversation

dsplaisted
Copy link
Member

@dsplaisted dsplaisted commented Mar 18, 2021

Add caching to workload resolver results, and enable workload resolver by default.

Addresses the first two items from #13849.

Rolls the workload resolution into the MSBuildSdkResolver, so that it doesn't have to duplicate the lookup of which SDK to use.

The built-in MSBuild SDK Resolver state caching doesn't work well in Visual Studio, as there are a lot of evaluations which aren't linked with a build submission ID, which is what MSBuild uses to keep resolver state. So if called from Visual Studio, the resolver keeps a single static copy of the workload resolution state, which invalidates its cache if the resolved SDK changes.

@dotnet-issue-labeler
Copy link

I couldn't figure out the best area label to add to this PR. If you have write-permissions please help me learn by adding exactly one area label.

@dsplaisted
Copy link
Member Author

Looks like using the state object that MSBuild passes in the context isn't going to be enough. I added logging for when the existing SDK resolver was called, and when loading the SDK solution from this repo in VS, there were 346 calls to the resolver where the state passed in was null, and only 38 where it was non-null.

@dsplaisted dsplaisted marked this pull request as ready for review March 24, 2021 00:50
@dsplaisted dsplaisted requested review from a team March 24, 2021 00:50
@arunchndr
Copy link
Member

arunchndr commented Mar 24, 2021

Fixes dotnet/msbuild#6060

@dsplaisted
Copy link
Member Author

Fixes dotnet/msbuild#6060

I don't think this actually fixes dotnet/msbuild#6060, rather it works around it to not make the problem any worse from adding new resolver logic.

@arunchndr
Copy link
Member

Fixes dotnet/msbuild#6060

I don't think this actually fixes dotnet/msbuild#6060, rather it works around it to not make the problem any worse from adding new resolver logic.

dotnet/msbuild#6060 was intended to address the problem of having to look up the sdk for each project in a solution and your fix looks like it targets that. Is there any residue left over that will need to be addressed in dotnet/msbuild#6060?

@@ -17,6 +17,10 @@
<DisableImplicitPackageTargetFallback>true</DisableImplicitPackageTargetFallback>

<!-- <ArtifactsShippingSymbolsDir>$(ArtifactsDir)symbols\$(Configuration)\Shipping</ArtifactsShippingSymbolsDir> -->

<!-- Flag to enable using Serilog for debug logging for how SDK resolvers are called -->
<EnableSerilogLogging>false</EnableSerilogLogging>
Copy link

Choose a reason for hiding this comment

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

The only usage is during development? You build with this flag enabled and then copy paste to VS, run it manually and see the result?

}
}

#if !NET
Copy link

Choose a reason for hiding this comment

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

What is this?

Copy link
Member Author

Choose a reason for hiding this comment

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

This is an attribute which allows using init-only properties on .NET Framework.

);


#if USE_SERILOG
Copy link

Choose a reason for hiding this comment

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

Any way to make this "more repeatable"? If another dev wants to improve the performance again. How should the new dev know what these means and how these properties should be used? What is the existing base line?

It is hard to make a unit test. But could you have a write up on how to use these logs? Something like you need to enable EnableSerilogLogging, copy the files over, and when running in VS then we should expect MSBuildSubmissionId to be the same all the time?


#nullable disable

namespace Microsoft.NET.Sdk.WorkloadMSBuildSdkResolver
Copy link

Choose a reason for hiding this comment

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

300 lines without unit test coverage. We should be able to test cache save and take.

{
return null;
}
#if USE_SERILOG
Copy link

Choose a reason for hiding this comment

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

nit: these if defs are really noisy


if (workloadResolver == null)
{
workloadResolver = new WorkloadPartialResolver();
}

if (msbuildSdksDir == null)
Copy link
Member

Choose a reason for hiding this comment

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

nit: why do we have the same condition twice? Can we combine this and line 139?

<!-- To reduce dll load (cause RPS perf regression). Directly compile files from Microsoft.DotNet.SdkResolver, and the workload resolver -->
<ItemGroup>
<Compile Include="..\Microsoft.DotNet.SdkResolver\**\*.cs" LinkBase="Microsoft.DotNet.SdkResolver" />
<Compile Include="..\Microsoft.NET.Sdk.WorkloadMSBuildSdkResolver\**\*.cs" LinkBase="Microsoft.NET.Sdk.WorkloadMSBuildSdkResolver" />
<Compile Include="..\Microsoft.NET.Sdk.WorkloadMSBuildSdkResolver\WorkloadPartialResolver.cs" LinkBase="Microsoft.NET.Sdk.WorkloadMSBuildSdkResolver" />
Copy link
Member

Choose a reason for hiding this comment

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

Why do we only need to include the partial resolver now? the other files aren't needed in VS?

Copy link
Member Author

Choose a reason for hiding this comment

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

For VS / Full Framework MSBuild, we now only use one MSBuild workload resolver, which handles both the existing SDK resolution as well as resolving workloads. The WorkloadPartialResolver (which I'm renaming to CachingWorkloadResolver) contains the workload resolution logic that will be shared between the .NET (Core) and .NET Framework resolvers.

@@ -32,6 +32,8 @@ public sealed class DotNetMSBuildSdkResolver : SdkResolver
private readonly Func<string, string> _getEnvironmentVariable;
private readonly NETCoreSdkResolver _netCoreSdkResolver;

private static CachingWorkloadResolver _staticWorkloadResolver = new CachingWorkloadResolver();
Copy link
Member

Choose a reason for hiding this comment

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

nit: Looks like this is unused if not running in VS. Initialize lazily?

// from the previous call, the cached state will be thrown out and recreated.
class CachingWorkloadResolver
{
private record CachedState
Copy link
Member

Choose a reason for hiding this comment

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

nit: sealed

}
}

if (!_enabled)
Copy link
Member

Choose a reason for hiding this comment

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

The condition looks reversed.

Suggested change
if (!_enabled)
if (_enabled)

}
}

public record SinglePathResolutionResult(
Copy link
Member

Choose a reason for hiding this comment

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

nit: sealed (applies to all result records).

public string SdkVersion { get; init; }
public IWorkloadManifestProvider ManifestProvider { get; init; }
public IWorkloadResolver WorkloadResolver { get; init; }
public ImmutableDictionary<string, ResolutionResult> CachedResults { get; init; }
Copy link
Member

Choose a reason for hiding this comment

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

Why is this an ImmutableDictionary as opposed to a regular Dictionary?

Copy link
Member Author

Choose a reason for hiding this comment

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

Originally this code was supposed to be fully thread-safe, using immutable object patterns. It's probably not necessary anymore now that everything happens in a lock. But if it's not a big deal perf-wise it seems cleaner to leave it as is.

{
public SdkResult ToSdkResult(SdkReference sdkReference, SdkResultFactory factory)
{
switch (this)
Copy link
Member

Choose a reason for hiding this comment

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

super-nit: Would this be prettier with virtual methods instead of switching on type? My experience with records is limited, feel free to tell me that this is a common pattern and I'll shut up :)

Copy link
Member Author

Choose a reason for hiding this comment

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

I don't know, this was my first time using some new C# language features :). My impression is that this is a standard pattern in functional programming.

Copy link

Choose a reason for hiding this comment

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

if objects do not have state. using virtual methods or pattern matching is pretty much the same thing. However, I prefer OO style when available just for the consistency. Plus even with a better pattern matching and record. C# is still hard to do functional style programming without discriminated union and other convenient syntax. You end up with generic inside generic and pattern matching all the time for few lines of F#. See https://www.manning.com/books/functional-programming-in-c-sharp .


public object _lockObject { get; } = new object();
private CachedState _cachedState;
private bool _enabled;
Copy link
Member

Choose a reason for hiding this comment

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

nit: readonly

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants