forked from dotnet/roslyn
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathVersionedPullCache.CacheItem.cs
138 lines (127 loc) · 8.22 KB
/
VersionedPullCache.CacheItem.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
// 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.Collections.Immutable;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis.LanguageServer.Handler;
internal abstract partial class VersionedPullCache<TCheapVersion, TExpensiveVersion, TState, TComputedData>
{
/// <summary>
/// Internal cache item that updates state for a particular <see cref="Workspace"/> and <see cref="ProjectOrDocumentId"/> in <see cref="VersionedPullCache{TCheapVersion, TExpensiveVersion, TState, TComputedData}"/>
/// This type ensures that the state for a particular key is never updated concurrently for the same key (but different key states can be concurrent).
/// </summary>
private sealed class CacheItem(string uniqueKey)
{
/// <summary>
/// Guards access to <see cref="_lastResult"/>.
/// This ensures that a cache entry is fully updated in a single transaction.
/// </summary>
private readonly SemaphoreSlim _gate = new(initialCount: 1);
/// <summary>
/// Stores the current state associated with this cache item.
/// Guarded by <see cref="_gate"/>
///
/// <list type="bullet">
/// <item>The resultId reported to the client.</item>
/// <item>The TCheapVersion of the data that was used to calculate results.
/// <para>
/// Note that this version can change even when nothing has actually changed (for example, forking the
/// LSP text, reloading the same project). So we additionally store:</para></item>
/// <item>A TExpensiveVersion (normally a checksum) checksum that will still allow us to reuse data even when
/// unimportant changes happen that trigger the cheap version change detection.</item>
/// <item>The checksum of the data that was computed when the resultId was generated.
/// <para>
/// When the versions above change, we must recalculate the data. However sometimes that data ends up being exactly the same as the prior request.
/// When that happens, this allows us to send back an unchanged result instead of reserializing data the client already has.
/// </para>
/// </item>
/// </list>
///
/// </summary>
private (string resultId, TCheapVersion cheapVersion, TExpensiveVersion expensiveVersion, Checksum dataChecksum)? _lastResult;
/// <summary>
/// Updates the values for this cache entry. Guarded by <see cref="_gate"/>
///
/// Returns <see langword="null"/> if the previousPullResult can be re-used, otherwise returns a new resultId and the new data associated with it.
/// </summary>
public async Task<(string, TComputedData)?> UpdateCacheItemAsync(
VersionedPullCache<TCheapVersion, TExpensiveVersion, TState, TComputedData> cache,
PreviousPullResult? previousPullResult,
bool isFullyLoaded,
TState state,
string language,
CancellationToken cancellationToken)
{
// Ensure that we only update the cache item one at a time.
// This means that the computation of new data for this item only occurs sequentially.
using (await _gate.DisposableWaitAsync(cancellationToken).ConfigureAwait(false))
{
TCheapVersion cheapVersion;
TExpensiveVersion expensiveVersion;
// Check if the version we have in the cache matches the request version. If so we can re-use the resultId.
if (isFullyLoaded &&
_lastResult is not null &&
_lastResult.Value.resultId == previousPullResult?.PreviousResultId)
{
cheapVersion = await cache.ComputeCheapVersionAsync(state, cancellationToken).ConfigureAwait(false);
if (cheapVersion != null && cheapVersion.Equals(_lastResult.Value.cheapVersion))
{
// The client's resultId matches our cached resultId and the cheap version is an
// exact match for our current cheap version. We return early here to avoid calculating
// expensive versions as we know nothing is changed.
return null;
}
// The current cheap version does not match the last reported. This may be because we've forked
// or reloaded a project, so fall back to calculating the full expensive version to determine if
// anything is actually changed.
expensiveVersion = await cache.ComputeExpensiveVersionAsync(state, cancellationToken).ConfigureAwait(false);
if (expensiveVersion != null && expensiveVersion.Equals(_lastResult.Value.expensiveVersion))
{
return null;
}
}
else
{
// The versions we have in our cache (if any) do not match the ones provided by the client (if any).
// We need to calculate new results.
cheapVersion = await cache.ComputeCheapVersionAsync(state, cancellationToken).ConfigureAwait(false);
expensiveVersion = await cache.ComputeExpensiveVersionAsync(state, cancellationToken).ConfigureAwait(false);
}
// Compute the new result for the request.
var data = await cache.ComputeDataAsync(state, cancellationToken).ConfigureAwait(false);
var dataChecksum = cache.ComputeChecksum(data, language);
string newResultId;
if (_lastResult is not null && _lastResult?.resultId == previousPullResult?.PreviousResultId && _lastResult?.dataChecksum == dataChecksum)
{
// The new data we've computed is exactly the same as the data we computed last time even though the versions have changed.
// Instead of reserializing everything, we can return the same result id back to the client.
// Ensure we store the updated versions we calculated against old resultId. If we do not do this,
// subsequent requests will always fail the version comparison check (the resultId is still associated with the older version even
// though we reused it here for a newer version) and will trigger re-computation.
// By storing the updated version with the resultId we can short circuit earlier in the version checks.
_lastResult = (_lastResult.Value.resultId, cheapVersion, expensiveVersion, dataChecksum);
return null;
}
else
{
// Keep track of the results we reported here so that we can short-circuit producing results for
// the same state of the world in the future. Use a custom result-id per type (doc requests or workspace
// requests) so that clients of one don't errantly call into the other.
//
// For example, a client getting document diagnostics should not ask for workspace diagnostics with the result-ids it got for
// doc-diagnostics. The two systems are different and cannot share results, or do things like report
// what changed between each other.
//
// Note that we can safely update the map before computation as any cancellation or exception
// during computation means that the client will never recieve this resultId and so cannot ask us for it.
newResultId = $"{uniqueKey}:{cache.GetNextResultId()}";
_lastResult = (newResultId, cheapVersion, expensiveVersion, dataChecksum);
return (newResultId, data);
}
}
}
}
}