-
Notifications
You must be signed in to change notification settings - Fork 4.1k
/
Copy pathCleanableWeakComHandleTable.cs
218 lines (178 loc) · 7.96 KB
/
CleanableWeakComHandleTable.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
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
// 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.
#nullable disable
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.VisualStudio.LanguageServices.Implementation.Utilities;
namespace Microsoft.VisualStudio.LanguageServices.Implementation.Interop
{
/// <summary>
/// Special collection for storing a table of COM objects weakly that provides
/// logic for cleaning up dead references in a time-sliced way. Public members of this
/// collection are affinitized to the foreground thread.
/// </summary>
internal class CleanableWeakComHandleTable<TKey, TValue> : ForegroundThreadAffinitizedObject
where TValue : class
{
private const int DefaultCleanUpThreshold = 25;
private static readonly TimeSpan s_defaultCleanUpTimeSlice = TimeSpan.FromMilliseconds(15);
private readonly Dictionary<TKey, WeakComHandle<TValue, TValue>> _table;
private readonly HashSet<TKey> _deadKeySet;
/// <summary>
/// The upper limit of items that the collection will store before clean up is recommended.
/// </summary>
public int CleanUpThreshold { get; }
/// <summary>
/// The amount of time that can pass during clean up it returns.
/// </summary>
public TimeSpan CleanUpTimeSlice { get; }
private int _itemsAddedSinceLastCleanUp;
private bool _needsCleanUp;
public bool NeedsCleanUp => _needsCleanUp;
public CleanableWeakComHandleTable(IThreadingContext threadingContext, int? cleanUpThreshold = null, TimeSpan? cleanUpTimeSlice = null)
: base(threadingContext)
{
_table = new Dictionary<TKey, WeakComHandle<TValue, TValue>>();
_deadKeySet = new HashSet<TKey>();
CleanUpThreshold = cleanUpThreshold ?? DefaultCleanUpThreshold;
CleanUpTimeSlice = cleanUpTimeSlice ?? s_defaultCleanUpTimeSlice;
}
/// <summary>
/// Cleans up references to dead objects in the table. This operation will yield to other foreground operations
/// any time execution exceeds <see cref="CleanUpTimeSlice"/>.
/// </summary>
public async Task CleanUpDeadObjectsAsync(IAsynchronousOperationListener listener)
{
using var _ = listener.BeginAsyncOperation(nameof(CleanUpDeadObjectsAsync));
Debug.Assert(ThreadingContext.JoinableTaskContext.IsOnMainThread, "This method is optimized for cases where calls do not yield before checking _needsCleanUp.");
await ThreadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(ThreadingContext.DisposalToken);
if (!_needsCleanUp)
{
return;
}
// Immediately mark as not needing cleanup; this operation will clean up the table by the time it returns.
_needsCleanUp = false;
var timeSlice = new TimeSlice(CleanUpTimeSlice);
await CollectDeadKeysAsync().ConfigureAwait(true);
await RemoveDeadKeysAsync().ConfigureAwait(true);
return;
// Local functions
async Task CollectDeadKeysAsync()
{
// This method returns after making a complete pass enumerating the elements of _table without finding
// any entries that are not alive. If a pass exceeds the allowed time slice after finding one or more
// dead entries, the pass yields before processing the elements found so far and restarting the
// enumeration.
//
// ⚠ This method may interleave with other asynchronous calls to CleanUpDeadObjectsAsync.
var cleanUpEnumerator = _table.GetEnumerator();
while (cleanUpEnumerator.MoveNext())
{
var pair = cleanUpEnumerator.Current;
if (!pair.Value.IsAlive())
{
_deadKeySet.Add(pair.Key);
if (timeSlice.IsOver)
{
// Yield before processing items found so far.
await ResetTimeSliceAsync().ConfigureAwait(true);
// Process items found prior to exceeding the time slice. Due to interleaving, it is
// possible for this call to process items found by another asynchronous call to
// CollectDeadKeysAsync, or for another asynchronous call to RemoveDeadKeysAsync to process
// all items prior to this call.
await RemoveDeadKeysAsync().ConfigureAwait(true);
// Obtain a new enumerator since the previous one may be invalidated.
cleanUpEnumerator = _table.GetEnumerator();
}
}
}
}
async Task RemoveDeadKeysAsync()
{
while (_deadKeySet.Count > 0)
{
// Fully process one item from _deadKeySet before the possibility of yielding
var key = _deadKeySet.First();
_deadKeySet.Remove(key);
Debug.Assert(_table.ContainsKey(key), "Key not found in table.");
_table.Remove(key);
if (timeSlice.IsOver)
{
await ResetTimeSliceAsync().ConfigureAwait(true);
}
}
}
async Task ResetTimeSliceAsync()
{
await listener.Delay(DelayTimeSpan.NearImmediate, ThreadingContext.DisposalToken).ConfigureAwait(true);
timeSlice = new TimeSlice(CleanUpTimeSlice);
}
}
public void Add(TKey key, TValue value)
{
this.AssertIsForeground();
if (value == null)
{
throw new ArgumentNullException(nameof(value));
}
if (_table.ContainsKey(key))
{
throw new InvalidOperationException($"Key already exists in table: {(key != null ? key.ToString() : "<null>")}.");
}
_itemsAddedSinceLastCleanUp++;
if (_itemsAddedSinceLastCleanUp >= CleanUpThreshold)
{
_needsCleanUp = true;
_itemsAddedSinceLastCleanUp = 0;
}
_table.Add(key, new WeakComHandle<TValue, TValue>(value));
}
public TValue Remove(TKey key)
{
this.AssertIsForeground();
if (_deadKeySet.Contains(key))
{
_deadKeySet.Remove(key);
}
if (_table.TryGetValue(key, out var handle))
{
_table.Remove(key);
return handle.ComAggregateObject;
}
return null;
}
public bool ContainsKey(TKey key)
{
this.AssertIsForeground();
return _table.ContainsKey(key);
}
public bool TryGetValue(TKey key, out TValue value)
{
this.AssertIsForeground();
if (_table.TryGetValue(key, out var handle))
{
value = handle.ComAggregateObject;
return value != null;
}
value = null;
return false;
}
public IEnumerable<TValue> Values
{
get
{
foreach (var keyValuePair in _table)
{
yield return keyValuePair.Value.ComAggregateObject;
}
}
}
}
}