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

Remove the BranchId concept from teh workspace #57132

Merged
merged 16 commits into from
Oct 25, 2021

Conversation

CyrusNajmabadi
Copy link
Member

We have a hypothesis that this info really isn't needed anymore and just adds unwarranted implementation and conceptual complexity.

Attempting to A/B this has proved challenging, as these values are computed and stored during the creation of hte workspace, which leads to high complexity if the workspace then tries to depend on options. Similarly, there was high difficulty ensuring that we could get a feature-flag reported, but ensure that it's value never changed during VS runtime (as that could lead to extremely bizarre behavior where we partially used branchIds and partially did not).

Because of this, we're going to go ahead in 17.1 and just try to remove this with the hope that there is no negative fallout. If there is, we have lots of coverage time and we can restore this if necessary.

private readonly ConditionalWeakTable<Solution, Solution> _designTimeToCompileTimeSoution = new();
#else
private ConditionalWeakTable<Solution, Solution> _designTimeToCompileTimeSoution = new();
#endif
Copy link
Member Author

Choose a reason for hiding this comment

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

sadness. Framework lacks a 'clear' method on CWT.

/// Cached compile time solution for a forked branch. This is used primarily by LSP cases where
/// we fork the workspace solution and request diagnostics for the forked solution.
/// </summary>
private (int DesignTimeSolutionVersion, BranchId DesignTimeSolutionBranch, Solution CompileTimeSolution)? _forkedBranchCompileTimeCache;
Copy link
Member Author

Choose a reason for hiding this comment

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

@NTaylorMullen instead of a version+branch pointing to the CompileTimeSolution, i instead just use a CWT mapping teh actual designTimeSolution to the compileTimeSolution. As a CWT it will be released whenever people stop holding onto this solution.

Note: i checked with @dibarbet and for LSP as long as edits aren't coming in, we'll have the same solution instance, so thsi CWT should work.

Copy link
Contributor

Choose a reason for hiding this comment

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

Really appreciate you checking the LSP side of this too ❤️

@@ -54,8 +54,6 @@ public static async Task PrecalculateAsync(Document document, CancellationToken

using (Logger.LogBlock(FunctionId.SyntaxTreeIndex_Precalculate, cancellationToken))
{
Debug.Assert(document.IsFromPrimaryBranch());
Copy link
Member Author

Choose a reason for hiding this comment

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

not an unreasonable assert. we would prefer some assurances that our precalculation step is only running on the non-forked solution of some workspace. However, if that turns out to not be the case, it's not the end of the world as this ensure subsystem is checksum'ed based anyways.


private static readonly ConditionalWeakTable<BranchId, ConditionalWeakTable<ProjectId, MetadataOnlyReferenceSet>>.CreateValueCallback s_createReferenceSetMap =
_ => new ConditionalWeakTable<ProjectId, MetadataOnlyReferenceSet>();
private static readonly ConditionalWeakTable<SolutionState, ConditionalWeakTable<ProjectId, MetadataOnlyReferenceSet>>.CreateValueCallback s_createReferenceSetMap = _ => new();
Copy link
Member Author

Choose a reason for hiding this comment

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

previous logic attempted to use BranchId here for reasons i honestly can't understand. Havin this be keyed by solution woudl be broken as we wouldn't be able to cache this across simple solution changes. However, we would like to be able to cache this for a particular lifetime of a solution as long as possible. Note: critically, this is not jsut an arbitrary "i will cache a projectid to a metadatarefset forever". instead, that set holds onto things like the 'dependent semantic version' of the project in that solution. So the references are only valid as long as no top level semantic information changed.

@@ -53,7 +53,7 @@ private async ValueTask<Scope> StoreAssetsAsync(Solution solution, ProjectId? pr
var id = Interlocked.Increment(ref s_scopeId);
var solutionInfo = new PinnedSolutionInfo(
id,
fromPrimaryBranch: solutionState.BranchId == solutionState.Workspace.PrimaryBranchId,
fromPrimaryBranch: solution == solution.Workspace.CurrentSolution,
Copy link
Member Author

Choose a reason for hiding this comment

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

@tmat we still have this concept, but we could potentially remove it as all. All that happens with this is that when we hydrate the solution on the other side, if this bit is passed along, we also cache the solution into a field for reuse.

We could consider just always doing this.

Copy link
Member

Choose a reason for hiding this comment

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

Is there any race concern here about CurrentSolution moving forward?

if (solution.BranchId != _workspace.PrimaryBranchId)
{
return;
}
Copy link
Member Author

Choose a reason for hiding this comment

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

note: this logic legit just didn't make sense. if we ever have a workspace whose 'CurrentSOlution' was not the 'Primary' solution then that workspace is in a super f'ed up situation. the whole idea (generally, though of course we didnt' enforce) of the branchId was to say "is this, or was this the CurrentSolution of a workspace, as opposed to a fork of that". So if we literally just grabbed the .CurrentSolution of a workspace, and then found it was a fork itself... then that's just messed up supremely.

@@ -55,8 +54,7 @@ internal class DiagnosticComputer
_analysisKind = analysisKind;
_analyzerInfoCache = analyzerInfoCache;

// We only track performance from primary branch. All forked branch we don't care such as preview.
_performanceTracker = project.IsFromPrimaryBranch() ? project.Solution.Workspace.Services.GetService<IPerformanceTrackerService>() : null;
_performanceTracker = project.Solution.Workspace.Services.GetService<IPerformanceTrackerService>();
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 part i wasn't totally sure abuot... however the original code seems suspect to me. If we're trying track the performance of analyzers... why would we care which context they were running in? If running them on a forked solution is expensive... then c'est la vie, we now track that.

Also, if we really only want this for a particular workspace... then we should only export this from teh workspace we care about. Having it be a default service for all workspaces, that then attempts to shut itself off for some just defeats the purpose of default services.

Copy link
Member

@jasonmalinowski jasonmalinowski left a comment

Choose a reason for hiding this comment

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

There's a bit more that can be deleted; also I'm not entirely sure about the insanity around the skeleton reference generation. Not that I will pretend to understand that code generally, I suspect either we can delete more of it, or we want to use something other than SolutionState in the one map.

src/Workspaces/Core/Portable/Workspace/Workspace.cs Outdated Show resolved Hide resolved
@@ -106,31 +103,22 @@ internal class MetadataOnlyReference
// okay, now use version based cache that can live multiple compilation as long as there is no semantic changes.

// get one for the branch
if (TryGetReferenceFromBranch(solution.BranchId, projectReference, finalOrDeclarationCompilation, version, out reference))
if (TryGetReferenceFromBranch(solution, projectReference, finalOrDeclarationCompilation, version, out reference))
Copy link
Member

Choose a reason for hiding this comment

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

It's not clear to me if this will ever trigger now if the first map of compilation -> reference didn't have one, then I don't think we'd ever end up with a case where this other map has one?

Copy link
Member

Choose a reason for hiding this comment

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

If we still need this map, should it instead be keyed on ProjectId or something?

Copy link
Member Author

Choose a reason for hiding this comment

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

Talked with Jason offline. This entire type was rejiggered. It's now instance state on compilatino-tracker. Allowing it to both share data across forks, but also not have cahnges to branched solutions unintentionally leak backward to affect the main solution.

@@ -14,28 +14,46 @@

namespace Microsoft.CodeAnalysis
{
internal class MetadataOnlyReference
internal class CachedMetadataOnlyReferences
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 type is very changed will doc heavily.

/// semantics) will not leak backward to a prior compilation tracker point, causing it to see a view of the world
/// inapplicable to its current snapshot.
/// </summary>
internal class CachedSkeletonReferences
Copy link
Member Author

Choose a reason for hiding this comment

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

renamed the type to be clearer about what it stores.

Copy link
Member

Choose a reason for hiding this comment

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

File would need renaming as well.

/// with a reference to this compilation (and the same <see cref="MetadataReferenceProperties"/> is allowed
/// to reference it using the same <see cref="MetadataReference"/>.
/// </summary>
private static readonly ConditionalWeakTable<Compilation, MetadataOnlyReferenceSet> s_compilationToReferenceMap = new();
Copy link
Member Author

Choose a reason for hiding this comment

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

commented above, but this should always be safe to use as a static CWT.

/// <summary>
/// Mapping from <see cref="ProjectId"/> to the skeleton <see cref="MetadataReference"/>s for it.
/// </summary>
private ImmutableDictionary<ProjectId, MetadataOnlyReferenceSet> _projectIdToReferenceMap;
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 now moved to be instance state so that as compilation trackers fork, later forks do not bleed back to prior forks. Note; because we just hand-off the ImmutableDictionary to the forks, it also means that forks don't see what prior trackers computed if they forked first, then the prior tracker computed this data.

My feeling is that that's ok. And it prevents us from keeping track of a long chain of prior CachedSkeletonReferences types.

So we ideally get the benfit, but without a high cost to that.

/// <para/>
/// This approach works, but has the caveat that live cross language semantics are only possible when the
/// skeleton assembly can be built. This should always be the case for correct code, but it may not be the
/// case for code with errors depending on if the respective language compilat is unable to generate the skeleton
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
/// case for code with errors depending on if the respective language compilat is unable to generate the skeleton
/// case for code with errors depending on if the respective language compiler is unable to generate the skeleton


lock (_gate)
{
_projectIdToReferenceMap = _projectIdToReferenceMap.Remove(projectReference.ProjectId);
Copy link
Member

Choose a reason for hiding this comment

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

Why the remove/add in this case?

@@ -44,22 +44,26 @@ private partial class CompilationTracker : ICompilationTracker
// guarantees only one thread is building at a time
private readonly SemaphoreSlim _buildLock = new(initialCount: 1);

public CachedSkeletonReferences CachedSkeletonReferences { get; }
Copy link
Member

Choose a reason for hiding this comment

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

I'm not quite understanding why this is being held by the CompilationTracker in this case, especially given that inside the CachedSkeletonReferences is also a map keyed by ProjectId. Considering a case where you have project A that's a C# project and it's consumed both by B and C, both VB. I would have assumed we want to be sharing the skeleton references that are being consumed by B and C so you'd only need one CachedSkeletonReferences for the entire solution. And if we fork either B or C, we can still keep reusing the same skeletons for A across both. I'd expect either:

  1. There's just one for the entire solution, or.
  2. A CompilationTracker's map is holding the skeletons created for that project, which doesn't need further keying.

Did you intend this to be held by the SolutionState instead?

Copy link
Member Author

Choose a reason for hiding this comment

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

wfm.

/// semantics) will not leak backward to a prior compilation tracker point, causing it to see a view of the world
/// inapplicable to its current snapshot.
/// </summary>
internal class CachedSkeletonReferences
Copy link
Member

Choose a reason for hiding this comment

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

File would need renaming as well.

/// <summary>
/// Mapping from <see cref="ProjectId"/> to the skeleton <see cref="MetadataReference"/>s for it.
/// </summary>
private ImmutableDictionary<ProjectId, MetadataOnlyReferenceSet> _projectIdToReferenceMap;
Copy link
Member

Choose a reason for hiding this comment

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

Does anything ever ensure we remove things from this when projects are removed?

Copy link
Member Author

Choose a reason for hiding this comment

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

no. but this keys from project-id to weakref. so worse case we have a few extra entries in the dictionary.

/// semantics) will not leak backward to a prior compilation tracker point, causing it to see a view of the world
/// inapplicable to its current snapshot.
/// </summary>
internal class CachedSkeletonReferences
Copy link
Member

Choose a reason for hiding this comment

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

class

sealed

#if NETCOREAPP
private readonly ConditionalWeakTable<Solution, Solution> _designTimeToCompileTimeSoution = new();
#else
// Framework lacks both a .Clear() method. So for Framework we simulate that by just overwriting this with a
Copy link
Contributor

Choose a reason for hiding this comment

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

Food for thought for a different PR: Is this a potential worthy data structure for us to eventually have? Something like a ThuperDuperConditionalWeakTable<T,U> that has the #if directives internally and exposes a Clear, AddOrUpdate etc. for Framework?

@CyrusNajmabadi
Copy link
Member Author

@jasonmalinowski plz review.

Copy link
Member

@jasonmalinowski jasonmalinowski left a comment

Choose a reason for hiding this comment

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

Looks fine; some possible cleanup is still available in CompileTimeSolutionProvider.cs I think. The check in SolutionAssetStorage.cs is also something I can't really speak to.

@@ -53,7 +53,7 @@ private async ValueTask<Scope> StoreAssetsAsync(Solution solution, ProjectId? pr
var id = Interlocked.Increment(ref s_scopeId);
var solutionInfo = new PinnedSolutionInfo(
id,
fromPrimaryBranch: solutionState.BranchId == solutionState.Workspace.PrimaryBranchId,
fromPrimaryBranch: solution == solution.Workspace.CurrentSolution,
Copy link
Member

Choose a reason for hiding this comment

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

Is there any race concern here about CurrentSolution moving forward?

@@ -123,42 +122,17 @@ public Solution GetCompileTimeSolution(Solution designTimeSolution)
.RemoveAnalyzerConfigDocuments(configIdsToRemove.ToImmutable())
.RemoveDocuments(documentIdsToRemove.ToImmutable());

UpdateCachedCompileTimeSolution(designTimeSolution, compileTimeSolution);
#if NETCOREAPP
_designTimeToCompileTimeSoution.AddOrUpdate(designTimeSolution, compileTimeSolution);
Copy link
Member

Choose a reason for hiding this comment

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

We're under a lock here, is it possible for this to ever race with something else given we checked earlier?

Copy link
Member Author

Choose a reason for hiding this comment

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

nope. not possible. have rewritten with the understanding that this is a lock.

@CyrusNajmabadi CyrusNajmabadi merged commit 83a728a into dotnet:main Oct 25, 2021
@ghost ghost added this to the Next milestone Oct 25, 2021
@RikkiGibson RikkiGibson modified the milestones: Next, 17.1.P1 Oct 25, 2021
@CyrusNajmabadi CyrusNajmabadi deleted the branchId3 branch October 26, 2021 00:00
CyrusNajmabadi added a commit to CyrusNajmabadi/roslyn that referenced this pull request Jan 12, 2022
CyrusNajmabadi added a commit to CyrusNajmabadi/roslyn that referenced this pull request Jan 13, 2022
CyrusNajmabadi added a commit to CyrusNajmabadi/roslyn that referenced this pull request Jan 14, 2022
CyrusNajmabadi added a commit that referenced this pull request Jan 20, 2022
Revert "Merge pull request #57132 from CyrusNajmabadi/branchId3"
333fred added a commit that referenced this pull request Jan 24, 2022
…merges/release/dev17.1-vs-deps-to-main

* upstream/release/dev17.1-vs-deps:
  Cleanup missed suggestion
  Update src/Workspaces/Core/Portable/Classification/ClassifierHelper.cs
  SpillSequenceSpiller.Spill - ensure sequences under ref assignments are spilled. (#58657) (#58788)
  Correctly update the list of generators for a call to WithAnalyzerReferences
  Rename parameter for what it's actually doing
  Handle AdditiveSpans in either order
  Reduce Stack Trace Explorer automatic open behavior scope (#58819)
  Revert 58892 (#58930)
  Mark APIs as shipped (#58776)
  Revert "Merge pull request #57132 from CyrusNajmabadi/branchId3"
  Fix race in WorkCoordinator when handling document reanalysis
  Remove unnecessary lock access setting UIContexts in the workspace
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants