diff --git a/src/Microsoft.VisualStudio.Threading/InternalUtilities.cs b/src/Microsoft.VisualStudio.Threading/InternalUtilities.cs
index f9eaba00a..0a89f2a44 100644
--- a/src/Microsoft.VisualStudio.Threading/InternalUtilities.cs
+++ b/src/Microsoft.VisualStudio.Threading/InternalUtilities.cs
@@ -2,6 +2,7 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
using System.Collections.Generic;
+using System.Threading;
namespace Microsoft.VisualStudio.Threading;
@@ -10,6 +11,50 @@ namespace Microsoft.VisualStudio.Threading;
///
internal static class InternalUtilities
{
+ ///
+ /// Takes a list from a single-slot pool, allocating only when the slot is empty or its capacity is insufficient.
+ ///
+ /// The type of list elements.
+ /// The slot containing an available, empty list.
+ /// The minimum required capacity.
+ /// An exclusively owned, empty list with the requested capacity.
+ internal static List RentList(ref List? pool, int capacity)
+ {
+ Requires.Range(capacity >= 0, nameof(capacity));
+
+ List? list = Interlocked.Exchange(ref pool, null);
+ if (list is null)
+ {
+ return new List(capacity);
+ }
+
+ if (list.Capacity < capacity)
+ {
+ list.Capacity = capacity;
+ }
+
+ return list;
+ }
+
+ ///
+ /// Clears a list and returns it to a single-slot pool if its capacity is within the retention limit.
+ ///
+ /// The type of list elements.
+ /// The slot to return the list to if empty.
+ /// The exclusively owned list to return.
+ /// The maximum capacity of a list to retain.
+ internal static void ReturnList(ref List? pool, List list, int maximumRetainedCapacity)
+ {
+ Requires.NotNull(list, nameof(list));
+ Requires.Range(maximumRetainedCapacity >= 0, nameof(maximumRetainedCapacity));
+
+ list.Clear();
+ if (list.Capacity <= maximumRetainedCapacity)
+ {
+ Interlocked.CompareExchange(ref pool, list, null);
+ }
+ }
+
///
/// Removes an element from the middle of a queue without disrupting the other elements.
///
diff --git a/src/Microsoft.VisualStudio.Threading/JoinableTask.cs b/src/Microsoft.VisualStudio.Threading/JoinableTask.cs
index 2cf498fdc..21e334a0b 100644
--- a/src/Microsoft.VisualStudio.Threading/JoinableTask.cs
+++ b/src/Microsoft.VisualStudio.Threading/JoinableTask.cs
@@ -275,6 +275,17 @@ internal bool IsFullyCompleted
{
get
{
+ JoinableTaskFlags currentState = this.state;
+ if ((currentState & JoinableTaskFlags.CompleteFinalized) != 0)
+ {
+ return true;
+ }
+
+ if ((currentState & JoinableTaskFlags.CompleteRequested) == 0)
+ {
+ return false;
+ }
+
using (this.JoinableTaskContext.NoMessagePumpSynchronizationContext.Apply())
{
lock (this.JoinableTaskContext.SyncContextLock)
diff --git a/src/Microsoft.VisualStudio.Threading/JoinableTaskDependencyGraph.cs b/src/Microsoft.VisualStudio.Threading/JoinableTaskDependencyGraph.cs
index c0de4ac3f..e58316341 100644
--- a/src/Microsoft.VisualStudio.Threading/JoinableTaskDependencyGraph.cs
+++ b/src/Microsoft.VisualStudio.Threading/JoinableTaskDependencyGraph.cs
@@ -16,8 +16,12 @@ namespace Microsoft.VisualStudio.Threading;
///
internal static class JoinableTaskDependencyGraph
{
+ private const int MaxRetainedNotificationListCapacity = 64;
+
private static readonly HashSet EmptySet = new HashSet();
+ private static List? notificationListPool;
+
///
/// Gets a value indicating whether there is no child depenent item.
/// This method is expected to be used with the JTF lock.
@@ -297,7 +301,7 @@ internal struct JoinableTaskDependentData
///
/// When the value in an entry is decremented to 0, the entry is removed from the map.
///
- private Dictionary childDependentNodes;
+ private SmallDependencyMap childDependentNodes;
///
/// The head of a singly linked list of records to track which task may process events of this task.
@@ -348,58 +352,56 @@ internal static JoinableTaskCollection.JoinRelease AddDependency(IJoinableTaskDe
using (parentTaskOrCollection.JoinableTaskContext.NoMessagePumpSynchronizationContext.Apply())
{
List? eventsNeedNotify = null;
- lock (parentTaskOrCollection.JoinableTaskContext.SyncContextLock)
+ try
{
- var joinableTask = joinChild as JoinableTask;
- if (joinableTask?.IsFullyCompleted == true)
+ lock (parentTaskOrCollection.JoinableTaskContext.SyncContextLock)
{
- return default(JoinableTaskCollection.JoinRelease);
- }
+ var joinableTask = joinChild as JoinableTask;
+ if (joinableTask?.IsFullyCompleted == true)
+ {
+ return default(JoinableTaskCollection.JoinRelease);
+ }
- ref JoinableTaskDependentData data = ref parentTaskOrCollection.GetJoinableTaskDependentData();
- if (data.childDependentNodes is null)
- {
- data.childDependentNodes = new Dictionary(capacity: 2);
- }
+ ref JoinableTaskDependentData data = ref parentTaskOrCollection.GetJoinableTaskDependentData();
+ if (data.childDependentNodes is null)
+ {
+ data.childDependentNodes = new SmallDependencyMap();
+ }
- if (data.childDependentNodes.TryGetValue(joinChild, out int refCount) && !parentTaskOrCollection.NeedRefCountChildDependencies)
- {
- return default(JoinableTaskCollection.JoinRelease);
+ if (data.childDependentNodes.TryGetValue(joinChild, out int refCount) && !parentTaskOrCollection.NeedRefCountChildDependencies)
+ {
+ return default(JoinableTaskCollection.JoinRelease);
+ }
+
+ data.childDependentNodes[joinChild] = ++refCount;
+ if (refCount == 1)
+ {
+ // This constitutes a significant change, so we should apply synchronous task tracking to the new child.
+ joinChild.OnAddedToDependency(parentTaskOrCollection);
+ AddDependingSynchronousTaskToChild(parentTaskOrCollection, joinChild, ref eventsNeedNotify);
+
+ parentTaskOrCollection.OnDependencyAdded(joinChild);
+ }
}
- data.childDependentNodes[joinChild] = ++refCount;
- if (refCount == 1)
+ // We explicitly do this outside our lock.
+ if (eventsNeedNotify is object)
{
- // This constitutes a significant change, so we should apply synchronous task tracking to the new child.
- joinChild.OnAddedToDependency(parentTaskOrCollection);
- IReadOnlyCollection? tasksNeedNotify = AddDependingSynchronousTaskToChild(parentTaskOrCollection, joinChild);
- if (tasksNeedNotify.Count > 0)
+ foreach (AsyncManualResetEvent? queueEvent in eventsNeedNotify)
{
- eventsNeedNotify = new List(tasksNeedNotify.Count);
- foreach (PendingNotification taskToNotify in tasksNeedNotify)
- {
- AsyncManualResetEvent? notifyEvent = taskToNotify.SynchronousTask.RegisterPendingEventsForSynchrousTask(taskToNotify.TaskHasPendingMessages, taskToNotify.NewPendingMessagesCount);
- if (notifyEvent is object)
- {
- eventsNeedNotify.Add(notifyEvent);
- }
- }
+ queueEvent.PulseAll();
}
-
- parentTaskOrCollection.OnDependencyAdded(joinChild);
}
- }
- // We explicitly do this outside our lock.
- if (eventsNeedNotify is object)
+ return new JoinableTaskCollection.JoinRelease(parentTaskOrCollection, joinChild);
+ }
+ finally
{
- foreach (AsyncManualResetEvent? queueEvent in eventsNeedNotify)
+ if (eventsNeedNotify is object)
{
- queueEvent.PulseAll();
+ InternalUtilities.ReturnList(ref notificationListPool, eventsNeedNotify, MaxRetainedNotificationListCapacity);
}
}
-
- return new JoinableTaskCollection.JoinRelease(parentTaskOrCollection, joinChild);
}
}
@@ -478,7 +480,7 @@ internal static void AddSelfAndDescendentOrJoinedJobs(IJoinableTaskDependent tas
}
}
- Dictionary? childDependentNodes = taskOrCollection.GetJoinableTaskDependentData().childDependentNodes;
+ SmallDependencyMap? childDependentNodes = taskOrCollection.GetJoinableTaskDependentData().childDependentNodes;
if (childDependentNodes is object)
{
foreach (KeyValuePair item in childDependentNodes)
@@ -562,7 +564,7 @@ internal static void ComputeSelfAndDescendentOrJoinedJobsAndRemainTasks(IJoinabl
return;
}
- Dictionary? dependencies = taskOrCollection.GetJoinableTaskDependentData().childDependentNodes;
+ SmallDependencyMap? dependencies = taskOrCollection.GetJoinableTaskDependentData().childDependentNodes;
if (dependencies is object)
{
foreach (KeyValuePair item in dependencies)
@@ -690,7 +692,7 @@ internal void OnTaskCompleted(IJoinableTaskDependent thisDependentNode)
if (this.childDependentNodes is object)
{
- Dictionary.KeyCollection? childrenTasks = this.childDependentNodes.Keys;
+ IReadOnlyCollection childrenTasks = this.childDependentNodes.Keys;
while (existingTaskTracking is object)
{
RemoveDependingSynchronousTaskFrom(childrenTasks, existingTaskTracking.SynchronousTask, force: existingTaskTracking.SynchronousTask == thisDependentNode);
@@ -763,8 +765,8 @@ internal IReadOnlyCollection GetDependingSynchronousTasks(bool for
///
/// The current joinableTask or collection owns the data.
/// The new child task.
- /// Pairs of synchronous tasks we need notify and the event source triggering it, plus the number of pending events.
- private static IReadOnlyCollection AddDependingSynchronousTaskToChild(IJoinableTaskDependent dependentNode, IJoinableTaskDependent child)
+ /// Receives the events to pulse after releasing the JTF lock.
+ private static void AddDependingSynchronousTaskToChild(IJoinableTaskDependent dependentNode, IJoinableTaskDependent child, ref List? eventsNeedNotify)
{
Requires.NotNull(dependentNode, nameof(dependentNode));
Requires.NotNull(child, nameof(child));
@@ -774,10 +776,9 @@ private static IReadOnlyCollection AddDependingSynchronousT
int count = data.CountOfDependingSynchronousTasks();
if (count == 0)
{
- return Array.Empty();
+ return;
}
- var tasksNeedNotify = new List(count);
DependentSynchronousTask? existingTaskTracking = data.dependingSynchronousTaskTracking;
while (existingTaskTracking is object)
{
@@ -785,13 +786,16 @@ private static IReadOnlyCollection AddDependingSynchronousT
JoinableTask? eventTriggeringTask = AddDependingSynchronousTask(child, existingTaskTracking.SynchronousTask, ref totalEventNumber);
if (eventTriggeringTask is object)
{
- tasksNeedNotify.Add(new PendingNotification(existingTaskTracking.SynchronousTask, eventTriggeringTask, totalEventNumber));
+ AsyncManualResetEvent? notifyEvent = existingTaskTracking.SynchronousTask.RegisterPendingEventsForSynchrousTask(eventTriggeringTask, totalEventNumber);
+ if (notifyEvent is object)
+ {
+ eventsNeedNotify ??= InternalUtilities.RentList(ref notificationListPool, count);
+ eventsNeedNotify.Add(notifyEvent);
+ }
}
existingTaskTracking = existingTaskTracking.Next;
}
-
- return tasksNeedNotify;
}
///
@@ -1051,38 +1055,6 @@ private void RemoveDependingSynchronousTaskFromChild(IJoinableTaskDependent chil
}
}
- ///
- /// The record of a pending notification we need send to the synchronous task that we have some new messages to process.
- ///
- private readonly struct PendingNotification
- {
- internal PendingNotification(JoinableTask synchronousTask, JoinableTask taskHasPendingMessages, int newPendingMessagesCount)
- {
- Requires.NotNull(synchronousTask, nameof(synchronousTask));
- Requires.NotNull(taskHasPendingMessages, nameof(taskHasPendingMessages));
-
- this.SynchronousTask = synchronousTask;
- this.TaskHasPendingMessages = taskHasPendingMessages;
- this.NewPendingMessagesCount = newPendingMessagesCount;
- }
-
- ///
- /// Gets the synchronous task which need process new messages.
- ///
- internal JoinableTask SynchronousTask { get; }
-
- ///
- /// Gets one JoinableTask which may have pending messages. We may have multiple new JoinableTasks which contains pending messages.
- /// This is just one of them. It gives the synchronous task a way to start quickly without searching all messages.
- ///
- internal JoinableTask TaskHasPendingMessages { get; }
-
- ///
- /// Gets the total number of new pending messages. The real number could be less than that, but should not be more than that.
- ///
- internal int NewPendingMessagesCount { get; }
- }
-
///
/// A single linked list to maintain synchronous JoinableTask depends on the current task,
/// which may process the queue of the current task.
diff --git a/src/Microsoft.VisualStudio.Threading/SmallDependencyMap`1.cs b/src/Microsoft.VisualStudio.Threading/SmallDependencyMap`1.cs
new file mode 100644
index 000000000..ca4832009
--- /dev/null
+++ b/src/Microsoft.VisualStudio.Threading/SmallDependencyMap`1.cs
@@ -0,0 +1,325 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+
+namespace Microsoft.VisualStudio.Threading;
+
+///
+/// Maps dependency keys to reference counts.
+///
+/// The type of dependency keys.
+///
+/// This collection is not thread-safe. All access must be synchronized by the caller.
+///
+internal sealed class SmallDependencyMap : IReadOnlyCollection
+ where TKey : class
+{
+ private Dictionary? dictionary;
+ private TKey? key0;
+ private TKey? key1;
+ private TKey? key2;
+ private int value0;
+ private int value1;
+ private int value2;
+ private int count;
+ private int used;
+ private int freeList = -1;
+ private int version;
+
+ ///
+ /// Gets the number of dependencies.
+ ///
+ public int Count => this.dictionary?.Count ?? this.count;
+
+ ///
+ /// Gets the dependency keys.
+ ///
+ internal IReadOnlyCollection Keys => this;
+
+ ///
+ /// Gets or sets the reference count for a dependency.
+ ///
+ /// The dependency key.
+ internal int this[TKey key]
+ {
+ get
+ {
+ if (this.TryGetValue(key, out int value))
+ {
+ return value;
+ }
+
+ throw new KeyNotFoundException();
+ }
+
+ set
+ {
+ Requires.NotNull(key, nameof(key));
+ if (this.dictionary is object)
+ {
+ this.dictionary[key] = value;
+ return;
+ }
+
+ int index = this.FindEntry(key);
+ if (index < 0)
+ {
+ if (this.count == 3)
+ {
+ // A dictionary initially sized for two entries grows from three to seven on the fourth add.
+ // Keeping the first three entries here avoids its initial bucket and entry arrays entirely.
+ var expanded = new Dictionary(capacity: 4)
+ {
+ [this.key0!] = this.value0,
+ [this.key1!] = this.value1,
+ [this.key2!] = this.value2,
+ [key] = value,
+ };
+ this.dictionary = expanded;
+ this.key0 = this.key1 = this.key2 = null;
+ this.version++;
+ return;
+ }
+
+ if (this.freeList >= 0)
+ {
+ index = this.freeList;
+ this.freeList = this.GetValue(index);
+ }
+ else
+ {
+ index = this.used++;
+ }
+
+ this.count++;
+ }
+
+ this.SetEntry(index, key, value);
+ this.version++;
+ }
+ }
+
+ ///
+ /// Gets an allocation-free enumerator over dependencies and reference counts.
+ ///
+ /// An enumerator over the entries.
+ public Enumerator GetEnumerator() => new Enumerator(this);
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return this.dictionary is object
+ ? ((IEnumerable)this.dictionary.Keys).GetEnumerator()
+ : new KeyEnumerator(this);
+ }
+
+ IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)this).GetEnumerator();
+
+ ///
+ /// Gets the reference count for a dependency if present.
+ ///
+ /// The dependency key.
+ /// Receives the reference count, or zero if the dependency is absent.
+ /// Whether the dependency is present.
+ internal bool TryGetValue(TKey key, out int value)
+ {
+ Requires.NotNull(key, nameof(key));
+ if (this.dictionary is object)
+ {
+ return this.dictionary.TryGetValue(key, out value);
+ }
+
+ int index = this.FindEntry(key);
+ value = index >= 0 ? this.GetValue(index) : 0;
+ return index >= 0;
+ }
+
+ ///
+ /// Checks whether a dependency is present.
+ ///
+ /// The dependency key.
+ /// Whether the dependency is present.
+ internal bool ContainsKey(TKey key) => this.TryGetValue(key, out _);
+
+ ///
+ /// Removes a dependency.
+ ///
+ /// The dependency key.
+ /// Whether the dependency was removed.
+ internal bool Remove(TKey key)
+ {
+ Requires.NotNull(key, nameof(key));
+ if (this.dictionary is object)
+ {
+ return this.dictionary.Remove(key);
+ }
+
+ int index = this.FindEntry(key);
+ if (index < 0)
+ {
+ return false;
+ }
+
+ this.SetEntry(index, null, this.freeList);
+ this.freeList = index;
+ this.count--;
+ this.version++;
+ return true;
+ }
+
+ private int FindEntry(TKey key)
+ {
+ EqualityComparer comparer = EqualityComparer.Default;
+ if (this.key0 is object && comparer.Equals(this.key0, key))
+ {
+ return 0;
+ }
+
+ if (this.key1 is object && comparer.Equals(this.key1, key))
+ {
+ return 1;
+ }
+
+ if (this.key2 is object && comparer.Equals(this.key2, key))
+ {
+ return 2;
+ }
+
+ return -1;
+ }
+
+ private TKey? GetKey(int index) => index == 0 ? this.key0 : index == 1 ? this.key1 : this.key2;
+
+ private int GetValue(int index) => index == 0 ? this.value0 : index == 1 ? this.value1 : this.value2;
+
+ private void SetEntry(int index, TKey? key, int value)
+ {
+ switch (index)
+ {
+ case 0:
+ this.key0 = key;
+ this.value0 = value;
+ break;
+ case 1:
+ this.key1 = key;
+ this.value1 = value;
+ break;
+ default:
+ this.key2 = key;
+ this.value2 = value;
+ break;
+ }
+ }
+
+ private void CheckVersion(int expectedVersion)
+ {
+ if (this.version != expectedVersion)
+ {
+ throw new InvalidOperationException();
+ }
+ }
+
+ ///
+ /// Enumerates dependencies and reference counts without allocating.
+ ///
+ internal struct Enumerator
+ {
+ private readonly SmallDependencyMap owner;
+ private readonly int version;
+ private Dictionary.Enumerator dictionaryEnumerator;
+ private int index;
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The map to enumerate.
+ internal Enumerator(SmallDependencyMap owner)
+ {
+ this.owner = owner;
+ this.version = owner.version;
+ this.dictionaryEnumerator = owner.dictionary?.GetEnumerator() ?? default;
+ this.index = -1;
+ this.Current = default;
+ }
+
+ ///
+ /// Gets the current entry.
+ ///
+ public KeyValuePair Current { get; private set; }
+
+ ///
+ /// Advances to the next entry.
+ ///
+ /// Whether another entry is available.
+ public bool MoveNext()
+ {
+ this.owner.CheckVersion(this.version);
+ if (this.owner.dictionary is object)
+ {
+ bool found = this.dictionaryEnumerator.MoveNext();
+ this.Current = this.dictionaryEnumerator.Current;
+ return found;
+ }
+
+ while (++this.index < this.owner.used)
+ {
+ if (this.owner.GetKey(this.index) is TKey key)
+ {
+ this.Current = new KeyValuePair(key, this.owner.GetValue(this.index));
+ return true;
+ }
+ }
+
+ this.Current = default;
+ return false;
+ }
+ }
+
+ private struct KeyEnumerator : IEnumerator
+ {
+ private readonly SmallDependencyMap owner;
+ private readonly int version;
+ private int index;
+
+ internal KeyEnumerator(SmallDependencyMap owner)
+ {
+ this.owner = owner;
+ this.version = owner.version;
+ this.index = -1;
+ this.Current = null!;
+ }
+
+ public TKey Current { get; private set; }
+
+ object IEnumerator.Current => this.Current ?? throw new InvalidOperationException();
+
+ public bool MoveNext()
+ {
+ this.owner.CheckVersion(this.version);
+ while (++this.index < this.owner.used)
+ {
+ if (this.owner.GetKey(this.index) is TKey key)
+ {
+ this.Current = key;
+ return true;
+ }
+ }
+
+ this.Current = null!;
+ return false;
+ }
+
+ public void Reset()
+ {
+ this.owner.CheckVersion(this.version);
+ this.index = -1;
+ this.Current = null!;
+ }
+
+ public void Dispose()
+ {
+ }
+ }
+}
diff --git a/test/Microsoft.VisualStudio.Threading.Tests/InternalUtilitiesTests.cs b/test/Microsoft.VisualStudio.Threading.Tests/InternalUtilitiesTests.cs
index bec84f2c4..e9bc7ee36 100644
--- a/test/Microsoft.VisualStudio.Threading.Tests/InternalUtilitiesTests.cs
+++ b/test/Microsoft.VisualStudio.Threading.Tests/InternalUtilitiesTests.cs
@@ -1,11 +1,163 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+using System;
using System.Collections.Generic;
using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
public class InternalUtilitiesTests
{
+ [Theory]
+ [InlineData(0)]
+ [InlineData(1)]
+ [InlineData(64)]
+ public void RentListAllocatesRequestedCapacityWhenEmpty(int capacity)
+ {
+ List