Skip to content

Commit

Permalink
Merge pull request #72281 from CyrusNajmabadi/filePath
Browse files Browse the repository at this point in the history
  • Loading branch information
CyrusNajmabadi authored Feb 27, 2024
2 parents ce17b58 + 0d1ab07 commit e3d617b
Show file tree
Hide file tree
Showing 4 changed files with 183 additions and 81 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis;

/// <summary>
/// Helper type that keeps track of all the file paths for all the documents in a solution snapshot and all the document
/// ids each maps to.
/// </summary>
internal readonly struct FilePathToDocumentIdsMap
{
private static readonly ImmutableDictionary<string, ImmutableArray<DocumentId>> s_emptyMap
= ImmutableDictionary.Create<string, ImmutableArray<DocumentId>>(StringComparer.OrdinalIgnoreCase);
public static readonly FilePathToDocumentIdsMap Empty = new(isFrozen: false, s_emptyMap);

/// <summary>
/// Whether or not this map corresponds to a frozen solution. Frozen solutions commonly drop many documents
/// (because only documents whose trees have been parsed are kept out). To keep things fast, instead of actually
/// dropping all those files from our <see cref="_map"/> we instead only keep track of added documents and mark that
/// we're frozen. Then, when a client actually asks for the document ids in a particular solution, we only return the
/// actual set present in that solution instance.
/// </summary>
public readonly bool IsFrozen;
private readonly ImmutableDictionary<string, ImmutableArray<DocumentId>> _map;

private FilePathToDocumentIdsMap(bool isFrozen, ImmutableDictionary<string, ImmutableArray<DocumentId>> map)
{
IsFrozen = isFrozen;
_map = map;
}

public bool TryGetValue(string filePath, out ImmutableArray<DocumentId> documentIdsWithPath)
=> _map.TryGetValue(filePath, out documentIdsWithPath);

public static bool operator ==(FilePathToDocumentIdsMap left, FilePathToDocumentIdsMap right)
=> left.IsFrozen == right.IsFrozen && left._map == right._map;

public static bool operator !=(FilePathToDocumentIdsMap left, FilePathToDocumentIdsMap right)
=> !(left == right);

public override int GetHashCode()
=> throw new NotSupportedException();

public override bool Equals([NotNullWhen(true)] object? obj)
=> obj is FilePathToDocumentIdsMap map && Equals(map);

public Builder ToBuilder()
=> new(IsFrozen, _map.ToBuilder());

public FilePathToDocumentIdsMap ToFrozen()
=> IsFrozen ? this : new(isFrozen: true, _map);

public readonly struct Builder
{
private readonly bool _isFrozen;
private readonly ImmutableDictionary<string, ImmutableArray<DocumentId>>.Builder _builder;

public Builder(bool isFrozen, ImmutableDictionary<string, ImmutableArray<DocumentId>>.Builder builder)
{
_isFrozen = isFrozen;
_builder = builder;
}

public FilePathToDocumentIdsMap ToImmutable()
=> new(_isFrozen, _builder.ToImmutable());

public bool TryGetValue(string filePath, out ImmutableArray<DocumentId> documentIdsWithPath)
=> _builder.TryGetValue(filePath, out documentIdsWithPath);

public void Add(string? filePath, DocumentId documentId)
{
if (RoslynString.IsNullOrEmpty(filePath))
return;

_builder.MultiAdd(filePath, documentId);
}

public void Remove(string? filePath, DocumentId documentId)
{
if (RoslynString.IsNullOrEmpty(filePath))
return;

if (!this.TryGetValue(filePath, out var documentIdsWithPath) || !documentIdsWithPath.Contains(documentId))
throw new ArgumentException($"The given documentId was not found");

_builder.MultiRemove(filePath, documentId);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1064,8 +1064,18 @@ private SolutionCompilationState ComputeFrozenSnapshot(CancellationToken cancell
var newIdToProjectStateMapBuilder = this.SolutionState.ProjectStates.ToBuilder();
var newIdToTrackerMapBuilder = _projectIdToTrackerMap.ToBuilder();

var filePathToDocumentIdsMapBuilder = this.SolutionState.FilePathToDocumentIdsMap.ToBuilder();
var filePathToDocumentIdsMapChanged = false;
// Keep track of the files that were potentially added between the last frozen snapshot point we have for a
// project and now. Specifically, if a file was removed in reality, it may show us as an add as we are
// effectively jumping back to a prior point in time for a particular project. We want all those files (and
// related doc ids) to be present in the frozen solution we hand back.
//
// Note: we only keep track of added files. We do not keep track of removed files. This is intentionally done
// for performance reasons. Specifically, it is quite normal for a project to drop all documents when frozen
// (for example, when no documents have been parsed in it). Actually dropping all these files from this map is
// very expensive. This does mean that the FilePathToDocumentIdsMap will be a superset of all files. That's
// ok. We'll mark this map as being frozen (and thus potentially containing a superset of legal ids), and later
// on our helpers will check for that and filter down to the set that is in a solution when queried.
var filePathToDocumentIdsMapBuilder = this.SolutionState.FilePathToDocumentIdsMap.ToFrozen().ToBuilder();

foreach (var projectId in this.SolutionState.ProjectIds)
{
Expand Down Expand Up @@ -1104,18 +1114,16 @@ private SolutionCompilationState ComputeFrozenSnapshot(CancellationToken cancell

if (oldProjectState != newProjectState)
{
CheckDocumentStates(oldProjectState.DocumentStates, newProjectState.DocumentStates);
CheckDocumentStates(oldProjectState.AdditionalDocumentStates, newProjectState.AdditionalDocumentStates);
CheckDocumentStates(oldProjectState.AnalyzerConfigDocumentStates, newProjectState.AnalyzerConfigDocumentStates);
AddMissingOrChangedFilePathMappings(filePathToDocumentIdsMapBuilder, oldProjectState.DocumentStates, newProjectState.DocumentStates);
AddMissingOrChangedFilePathMappings(filePathToDocumentIdsMapBuilder, oldProjectState.AdditionalDocumentStates, newProjectState.AdditionalDocumentStates);
AddMissingOrChangedFilePathMappings(filePathToDocumentIdsMapBuilder, oldProjectState.AnalyzerConfigDocumentStates, newProjectState.AnalyzerConfigDocumentStates);
}
}

var newIdToProjectStateMap = newIdToProjectStateMapBuilder.ToImmutable();
var newIdToTrackerMap = newIdToTrackerMapBuilder.ToImmutable();

var filePathToDocumentIdsMap = filePathToDocumentIdsMapChanged
? filePathToDocumentIdsMapBuilder.ToImmutable()
: null;
var filePathToDocumentIdsMap = filePathToDocumentIdsMapBuilder.ToImmutable();

var dependencyGraph = SolutionState.CreateDependencyGraph(this.SolutionState.ProjectIds, newIdToProjectStateMap);

Expand All @@ -1132,38 +1140,37 @@ private SolutionCompilationState ComputeFrozenSnapshot(CancellationToken cancell

return newCompilationState;

void CheckDocumentStates<TDocumentState>(
static void AddMissingOrChangedFilePathMappings<TDocumentState>(
FilePathToDocumentIdsMap.Builder filePathToDocumentIdsMapBuilder,
TextDocumentStates<TDocumentState> oldStates,
TextDocumentStates<TDocumentState> newStates) where TDocumentState : TextDocumentState
{
if (oldStates.Equals(newStates))
return;

// Get the trivial sets of documents that are present in one set but not the other.

foreach (var documentId in newStates.GetAddedStateIds(oldStates))
{
filePathToDocumentIdsMapChanged = true;
SolutionState.AddDocumentFilePath(newStates.GetRequiredState(documentId), filePathToDocumentIdsMapBuilder);
}

foreach (var documentId in newStates.GetRemovedStateIds(oldStates))
// We want to make sure that all the documents in the new-state are properly represented in the file map.
// It's ok if old-state documents are still in the map as GetDocumentIdsWithFilePath will filter them out
// later since we're producing a frozen-partial map.
//
// Iterating over the new-states has an additional benefit. For projects that haven't ever been looked at
// (so they haven't really parsed any documents), this will results in empty new-states. So this loop will
// be almost a no-op for most non-relevant projects.
foreach (var (documentId, newDocumentState) in newStates.States)
{
filePathToDocumentIdsMapChanged = true;
SolutionState.RemoveDocumentFilePath(oldStates.GetRequiredState(documentId), filePathToDocumentIdsMapBuilder);
}
if (!oldStates.TryGetState(documentId, out var oldDocumentState))
{
// Keep track of files that are definitely added. Make sure the added doc is in the file path map.
filePathToDocumentIdsMapBuilder.Add(newDocumentState.FilePath, documentId);

// Now go through the states that are in both sets. We have to check these all as it is possible for
// document to change its file path without its id changing.
foreach (var (documentId, oldDocumentState) in oldStates.States)
{
if (newStates.States.TryGetValue(documentId, out var newDocumentState) &&
oldDocumentState != newDocumentState &&
oldDocumentState.FilePath != newDocumentState.FilePath)
}
else if (oldDocumentState != newDocumentState &&
oldDocumentState.FilePath != newDocumentState.FilePath)
{
filePathToDocumentIdsMapChanged = true;
SolutionState.RemoveDocumentFilePath(oldDocumentState, filePathToDocumentIdsMapBuilder);
SolutionState.AddDocumentFilePath(newDocumentState, filePathToDocumentIdsMapBuilder);
// Otherwise, if the document is in both, but the file name changed, then remove the old mapping
// and add the new mapping. Importantly, we don't want other linked files with the *old* path
// to consider this document one of their linked brethren.
filePathToDocumentIdsMapBuilder.Remove(oldDocumentState.FilePath, oldDocumentState.Id);
filePathToDocumentIdsMapBuilder.Add(newDocumentState.FilePath, newDocumentState.Id);
}
}
}
Expand Down
Loading

0 comments on commit e3d617b

Please sign in to comment.