|
// 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.Generic;
using System.Threading;
using Microsoft.CodeAnalysis.LanguageServerIndexFormat.Generator.Graph;
using Microsoft.CodeAnalysis.LanguageServerIndexFormat.Generator.Writing;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis.LanguageServerIndexFormat.Generator.ResultSetTracking
{
internal sealed class SymbolHoldingResultSetTracker : IResultSetTracker
{
private readonly Dictionary<ISymbol, TrackedResultSet> _symbolToResultSetId = new Dictionary<ISymbol, TrackedResultSet>();
private readonly ReaderWriterLockSlim _readerWriterLock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion);
private readonly ILsifJsonWriter _lsifJsonWriter;
private readonly IdFactory _idFactory;
/// <summary>
/// The compilation which we are analyzing. When we make ResultSets, we attach monikers to them, and those
/// monikers express an import/export concept for symbols being consumed from another project. We must distinguish
/// from source symbols that come from the project being analyzed versus symbols from referenced compilations, so
/// we can't just use <see cref="ISymbol.Locations" /> to make the determination.
/// </summary>
private readonly Compilation _sourceCompilation;
public SymbolHoldingResultSetTracker(ILsifJsonWriter lsifJsonWriter, Compilation sourceCompilation, IdFactory idFactory)
{
_lsifJsonWriter = lsifJsonWriter;
_sourceCompilation = sourceCompilation;
_idFactory = idFactory;
}
private TrackedResultSet GetTrackedResultSet(ISymbol symbol)
{
TrackedResultSet? trackedResultSet;
// First acquire a simple read lock to see if we already have a result set; we do this with
// just a read lock to ensure we aren't contending a lot if the symbol already exists which
// is the most common case.
using (_readerWriterLock.DisposableRead())
{
if (_symbolToResultSetId.TryGetValue(symbol, out trackedResultSet))
{
return trackedResultSet;
}
}
using (var upgradeableReadLock = _readerWriterLock.DisposableUpgradeableRead())
{
// Check a second for the result set since a request could have gotten between us
if (_symbolToResultSetId.TryGetValue(symbol, out trackedResultSet))
{
return trackedResultSet;
}
// We still don't have it, so we have to start writing now
upgradeableReadLock.EnterWrite();
var resultSet = new ResultSet(_idFactory);
_lsifJsonWriter.Write(resultSet);
trackedResultSet = new TrackedResultSet(resultSet.GetId());
_symbolToResultSetId.Add(symbol, trackedResultSet);
}
// Since we're creating a ResultSet for a symbol for the first time, let's also attach the moniker. We only generate
// monikers for original definitions as we don't have a moniker system for those, but also because the place where
// monikers are needed -- cross-solution find references and go to definition -- only operates on original definitions
// anyways.
//
// This we do outside the lock -- whichever thread was the one to create this was the one that
// gets to write out the moniker, but others can use the ResultSet Id at this point.
if (SymbolMoniker.HasMoniker(symbol))
{
_ = this.GetMoniker(symbol, _sourceCompilation);
}
return trackedResultSet;
}
public Id<ResultSet> GetResultSetIdForSymbol(ISymbol symbol)
{
return GetTrackedResultSet(symbol).Id;
}
public Id<T> GetResultIdForSymbol<T>(ISymbol symbol, string edgeKind, Func<IdFactory, T> vertexCreator) where T : Vertex
{
return GetTrackedResultSet(symbol).GetResultId(edgeKind, vertexCreator, _lsifJsonWriter, _idFactory);
}
public bool ResultSetNeedsInformationalEdgeAdded(ISymbol symbol, string edgeKind)
{
return GetTrackedResultSet(symbol).ResultSetNeedsInformationalEdgeAdded(edgeKind);
}
private class TrackedResultSet
{
public Id<ResultSet> Id { get; }
/// <summary>
/// A map which holds the per-symbol results that are linked from the resultSet. The value will be null if the entry was
/// added via <see cref="ResultSetNeedsInformationalEdgeAdded"/>. Concurrent acecss is guarded with a monitor lock
/// on this field itself, with the belief that concurrent access for a single symbol is relatively rare.
/// </summary>
/// <remarks>
/// This class assumes that we more or less have two kinds of edges in the LSIF world:
///
/// 1. the resultSet might point to a node that doesn't really have any data, but simply points to other data like referenceResults.
/// In this case, it's important for clients to get to that Id.
/// 2. the resultSet points to a node that itself has data, but nobody needs to know the ID, like a hover result. In this case, those results
/// are often expensive to compute, but we do want to record that somebody is adding them somewhere.
///
/// We record the first kind of this in this dictionary with a non-null Id, and the second kind with a null ID. We could conceptually store
/// two dictionaries for this, but that will add memory pressure and also limit the catching of mistakes if people cross these two APIs.
/// </remarks>
private readonly Dictionary<string, Id<Vertex>?> _edgeKindToVertexId = new Dictionary<string, Id<Vertex>?>();
public TrackedResultSet(Id<ResultSet> id)
{
Id = id;
}
public Id<T> GetResultId<T>(string edgeLabel, Func<IdFactory, T> vertexCreator, ILsifJsonWriter lsifJsonWriter, IdFactory idFactory) where T : Vertex
{
lock (_edgeKindToVertexId)
{
if (_edgeKindToVertexId.TryGetValue(edgeLabel, out var existingId))
{
if (!existingId.HasValue)
{
throw new Exception($"This ResultSet already has an edge of {edgeLabel} as {nameof(ResultSetNeedsInformationalEdgeAdded)} was called with this edge label.");
}
// TODO: this is a violation of the type system here, really: we're assuming that all calls to this function with the same edge kind
// will have the same type parameter. If that's violated, the Id returned here isn't really the right type.
return new Id<T>(existingId.Value.NumericId);
}
var vertex = vertexCreator(idFactory);
_edgeKindToVertexId.Add(edgeLabel, vertex.GetId().As<T, Vertex>());
lsifJsonWriter.Write(vertex);
lsifJsonWriter.Write(Edge.Create(edgeLabel, Id, vertex.GetId(), idFactory));
return vertex.GetId();
}
}
public bool ResultSetNeedsInformationalEdgeAdded(string edgeKind)
{
lock (_edgeKindToVertexId)
{
if (_edgeKindToVertexId.TryGetValue(edgeKind, out var existingId))
{
if (existingId.HasValue)
{
throw new InvalidOperationException($"This edge kind was already called with a call to {nameof(GetResultId)} which would imply we are mixing edge types incorrectly.");
}
return false;
}
_edgeKindToVertexId.Add(edgeKind, null);
return true;
}
}
}
}
}
|