From afff241353f8b9b449a7b7934a1bfc1318722145 Mon Sep 17 00:00:00 2001 From: Martin Obratil Date: Fri, 22 Dec 2023 12:03:15 +0100 Subject: [PATCH 1/5] Replace List with ConcurrentDictionary in AsyncState --- .../AsyncState.cs | 27 ++++--------------- .../FeaturesPooledPolicy.cs | 13 ++++----- .../AsyncStateTests.cs | 24 ----------------- .../FeaturesPooledPolicyTests.cs | 12 ++++----- 4 files changed, 16 insertions(+), 60 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AsyncState/AsyncState.cs b/src/Libraries/Microsoft.Extensions.AsyncState/AsyncState.cs index 4d782409238..b6c9cdb0e75 100644 --- a/src/Libraries/Microsoft.Extensions.AsyncState/AsyncState.cs +++ b/src/Libraries/Microsoft.Extensions.AsyncState/AsyncState.cs @@ -2,7 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; -using System.Collections.Generic; +using System.Collections.Concurrent; using System.Threading; using Microsoft.Extensions.ObjectPool; using Microsoft.Shared.Diagnostics; @@ -13,7 +13,7 @@ namespace Microsoft.Extensions.AsyncState; internal sealed class AsyncState : IAsyncState { private static readonly AsyncLocal _asyncContextCurrent = new(); - private static readonly ObjectPool> _featuresPool = PoolFactory.CreatePool(new FeaturesPooledPolicy()); + private static readonly ObjectPool> _featuresPool = PoolFactory.CreatePool(new FeaturesPooledPolicy()); private int _contextCount; public void Initialize() @@ -60,9 +60,7 @@ public bool TryGet(AsyncStateToken token, out object? value) return false; } - EnsureCount(_asyncContextCurrent.Value.Features, token.Index + 1); - - value = _asyncContextCurrent.Value.Features[token.Index]; + _ = _asyncContextCurrent.Value.Features.TryGetValue(token, out value); return true; } @@ -86,28 +84,13 @@ public void Set(AsyncStateToken token, object? value) Throw.InvalidOperationException("Context is not initialized"); } - EnsureCount(_asyncContextCurrent.Value.Features, token.Index + 1); - - _asyncContextCurrent.Value.Features[token.Index] = value; - } - - internal static void EnsureCount(List features, int count) - { -#if NET6_0_OR_GREATER - features.EnsureCapacity(count); -#endif - var difference = count - features.Count; - - for (int i = 0; i < difference; i++) - { - features.Add(null); - } + _asyncContextCurrent.Value.Features[token] = value; } internal int ContextCount => Volatile.Read(ref _contextCount); private sealed class AsyncStateHolder { - public List? Features { get; set; } + public ConcurrentDictionary? Features { get; set; } } } diff --git a/src/Libraries/Microsoft.Extensions.AsyncState/FeaturesPooledPolicy.cs b/src/Libraries/Microsoft.Extensions.AsyncState/FeaturesPooledPolicy.cs index 3b045465595..a7a8e64402f 100644 --- a/src/Libraries/Microsoft.Extensions.AsyncState/FeaturesPooledPolicy.cs +++ b/src/Libraries/Microsoft.Extensions.AsyncState/FeaturesPooledPolicy.cs @@ -1,27 +1,24 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Collections.Concurrent; using System.Collections.Generic; using Microsoft.Extensions.ObjectPool; namespace Microsoft.Extensions.AsyncState; -internal sealed class FeaturesPooledPolicy : IPooledObjectPolicy> +internal sealed class FeaturesPooledPolicy : IPooledObjectPolicy> { /// - public List Create() + public ConcurrentDictionary Create() { return []; } /// - public bool Return(List obj) + public bool Return(ConcurrentDictionary obj) { - for (int i = 0; i < obj.Count; i++) - { - obj[i] = null; - } - + obj.Clear(); return true; } } diff --git a/test/Libraries/Microsoft.Extensions.AsyncState.Tests/AsyncStateTests.cs b/test/Libraries/Microsoft.Extensions.AsyncState.Tests/AsyncStateTests.cs index f7a7b050eb1..20ecdb95e64 100644 --- a/test/Libraries/Microsoft.Extensions.AsyncState.Tests/AsyncStateTests.cs +++ b/test/Libraries/Microsoft.Extensions.AsyncState.Tests/AsyncStateTests.cs @@ -205,28 +205,4 @@ public void RegisterContextCorrectly() Assert.Equal(3, asyncState.ContextCount); } - - [Fact] - public void EnsureCount_IncreasesCountCorrectly() - { - var l = new List(); - AsyncState.EnsureCount(l, 5); - Assert.Equal(5, l.Count); - } - - [Fact] - public void EnsureCount_WhenCountLessThanExpected() - { - var l = new List(new object?[5]); - AsyncState.EnsureCount(l, 2); - Assert.Equal(5, l.Count); - } - - [Fact] - public void EnsureCount_WhenCountEqualWithExpected() - { - var l = new List(new object?[5]); - AsyncState.EnsureCount(l, 5); - Assert.Equal(5, l.Count); - } } diff --git a/test/Libraries/Microsoft.Extensions.AsyncState.Tests/FeaturesPooledPolicyTests.cs b/test/Libraries/Microsoft.Extensions.AsyncState.Tests/FeaturesPooledPolicyTests.cs index 909389acb95..3cc13341743 100644 --- a/test/Libraries/Microsoft.Extensions.AsyncState.Tests/FeaturesPooledPolicyTests.cs +++ b/test/Libraries/Microsoft.Extensions.AsyncState.Tests/FeaturesPooledPolicyTests.cs @@ -21,12 +21,12 @@ public void Return_ShouldNullList() { var policy = new FeaturesPooledPolicy(); - var list = policy.Create(); - list.Add(string.Empty); - list.Add(Array.Empty()); - list.Add(new object()); + var dictionary = policy.Create(); + dictionary[new AsyncStateToken(0)] = string.Empty; + dictionary[new AsyncStateToken(1)] = Array.Empty(); + dictionary[new AsyncStateToken(2)] = new object(); - Assert.True(policy.Return(list)); - Assert.All(list, el => Assert.Null(el)); + Assert.True(policy.Return(dictionary)); + Assert.All(dictionary, el => Assert.Null(el)); } } From 50bca1b61a19c477b4d7947bb0be0cbef9e20b74 Mon Sep 17 00:00:00 2001 From: Martin Obratil Date: Fri, 22 Dec 2023 13:24:27 +0100 Subject: [PATCH 2/5] Remove unused namespace in FeaturesPooledPolicy.cs --- .../Microsoft.Extensions.AsyncState/FeaturesPooledPolicy.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Libraries/Microsoft.Extensions.AsyncState/FeaturesPooledPolicy.cs b/src/Libraries/Microsoft.Extensions.AsyncState/FeaturesPooledPolicy.cs index a7a8e64402f..bac20d3419c 100644 --- a/src/Libraries/Microsoft.Extensions.AsyncState/FeaturesPooledPolicy.cs +++ b/src/Libraries/Microsoft.Extensions.AsyncState/FeaturesPooledPolicy.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Concurrent; -using System.Collections.Generic; using Microsoft.Extensions.ObjectPool; namespace Microsoft.Extensions.AsyncState; From 0e3f7387bf20e13f9cdb4d17c73f7cfa816454c5 Mon Sep 17 00:00:00 2001 From: Martin Obratil Date: Fri, 22 Dec 2023 14:36:30 +0100 Subject: [PATCH 3/5] Fixes in tests --- .../Microsoft.Extensions.AsyncState.Tests/AsyncStateTests.cs | 1 - .../FeaturesPooledPolicyTests.cs | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/test/Libraries/Microsoft.Extensions.AsyncState.Tests/AsyncStateTests.cs b/test/Libraries/Microsoft.Extensions.AsyncState.Tests/AsyncStateTests.cs index 20ecdb95e64..e541ee04865 100644 --- a/test/Libraries/Microsoft.Extensions.AsyncState.Tests/AsyncStateTests.cs +++ b/test/Libraries/Microsoft.Extensions.AsyncState.Tests/AsyncStateTests.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Threading; using System.Threading.Tasks; diff --git a/test/Libraries/Microsoft.Extensions.AsyncState.Tests/FeaturesPooledPolicyTests.cs b/test/Libraries/Microsoft.Extensions.AsyncState.Tests/FeaturesPooledPolicyTests.cs index 3cc13341743..205509b6585 100644 --- a/test/Libraries/Microsoft.Extensions.AsyncState.Tests/FeaturesPooledPolicyTests.cs +++ b/test/Libraries/Microsoft.Extensions.AsyncState.Tests/FeaturesPooledPolicyTests.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; -using System.Collections.Generic; using Xunit; namespace Microsoft.Extensions.AsyncState.Test; @@ -27,6 +26,6 @@ public void Return_ShouldNullList() dictionary[new AsyncStateToken(2)] = new object(); Assert.True(policy.Return(dictionary)); - Assert.All(dictionary, el => Assert.Null(el)); + Assert.True(dictionary.IsEmpty); } } From e152562383cf38dbdf0227c82e78df3879556dbe Mon Sep 17 00:00:00 2001 From: Martin Obratil Date: Fri, 22 Dec 2023 14:46:12 +0100 Subject: [PATCH 4/5] Replace ConcurrentDictionary with custom Features class --- .../AsyncState.cs | 15 ++++--- .../Features.cs | 45 +++++++++++++++++++ .../FeaturesPooledPolicy.cs | 9 ++-- .../FeaturesPooledPolicyTests.cs | 16 ++++--- 4 files changed, 66 insertions(+), 19 deletions(-) create mode 100644 src/Libraries/Microsoft.Extensions.AsyncState/Features.cs diff --git a/src/Libraries/Microsoft.Extensions.AsyncState/AsyncState.cs b/src/Libraries/Microsoft.Extensions.AsyncState/AsyncState.cs index b6c9cdb0e75..354beb81375 100644 --- a/src/Libraries/Microsoft.Extensions.AsyncState/AsyncState.cs +++ b/src/Libraries/Microsoft.Extensions.AsyncState/AsyncState.cs @@ -2,7 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; -using System.Collections.Concurrent; +using System.Collections.Generic; using System.Threading; using Microsoft.Extensions.ObjectPool; using Microsoft.Shared.Diagnostics; @@ -13,7 +13,7 @@ namespace Microsoft.Extensions.AsyncState; internal sealed class AsyncState : IAsyncState { private static readonly AsyncLocal _asyncContextCurrent = new(); - private static readonly ObjectPool> _featuresPool = PoolFactory.CreatePool(new FeaturesPooledPolicy()); + private static readonly ObjectPool _featuresPool = PoolFactory.CreatePool(new FeaturesPooledPolicy()); private int _contextCount; public void Initialize() @@ -22,12 +22,12 @@ public void Initialize() // Use an object indirection to hold the AsyncContext in the AsyncLocal, // so it can be cleared in all ExecutionContexts when its cleared. - var features = new AsyncStateHolder + var asyncStateHolder = new AsyncStateHolder { Features = _featuresPool.Get() }; - _asyncContextCurrent.Value = features; + _asyncContextCurrent.Value = asyncStateHolder; } public void Reset() @@ -60,7 +60,7 @@ public bool TryGet(AsyncStateToken token, out object? value) return false; } - _ = _asyncContextCurrent.Value.Features.TryGetValue(token, out value); + value = _asyncContextCurrent.Value.Features.Get(token.Index); return true; } @@ -84,13 +84,14 @@ public void Set(AsyncStateToken token, object? value) Throw.InvalidOperationException("Context is not initialized"); } - _asyncContextCurrent.Value.Features[token] = value; + _asyncContextCurrent.Value.Features.Set(token.Index, value); } internal int ContextCount => Volatile.Read(ref _contextCount); private sealed class AsyncStateHolder { - public ConcurrentDictionary? Features { get; set; } + public Features? Features { get; set; } } + } diff --git a/src/Libraries/Microsoft.Extensions.AsyncState/Features.cs b/src/Libraries/Microsoft.Extensions.AsyncState/Features.cs new file mode 100644 index 00000000000..940efa69c26 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AsyncState/Features.cs @@ -0,0 +1,45 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; + +namespace Microsoft.Extensions.AsyncState; + +internal sealed class Features +{ + private readonly List _items = []; + + public object? Get(int index) + { + return _items.Count <= index ? null : _items[index]; + } + + public void Set(int index, object? value) + { + if (_items.Count <= index) + { + lock (_items) + { + var count = index + 1; + +#if NET6_0_OR_GREATER + _items.EnsureCapacity(count); +#endif + + var difference = count - _items.Count; + + for (int i = 0; i < difference; i++) + { + _items.Add(null); + } + } + } + + _items[index] = value; + } + + public void Clear() + { + _items.Clear(); + } +} diff --git a/src/Libraries/Microsoft.Extensions.AsyncState/FeaturesPooledPolicy.cs b/src/Libraries/Microsoft.Extensions.AsyncState/FeaturesPooledPolicy.cs index bac20d3419c..6c6f442c817 100644 --- a/src/Libraries/Microsoft.Extensions.AsyncState/FeaturesPooledPolicy.cs +++ b/src/Libraries/Microsoft.Extensions.AsyncState/FeaturesPooledPolicy.cs @@ -1,21 +1,20 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Collections.Concurrent; using Microsoft.Extensions.ObjectPool; namespace Microsoft.Extensions.AsyncState; -internal sealed class FeaturesPooledPolicy : IPooledObjectPolicy> +internal sealed class FeaturesPooledPolicy : IPooledObjectPolicy { /// - public ConcurrentDictionary Create() + public Features Create() { - return []; + return new Features(); } /// - public bool Return(ConcurrentDictionary obj) + public bool Return(Features obj) { obj.Clear(); return true; diff --git a/test/Libraries/Microsoft.Extensions.AsyncState.Tests/FeaturesPooledPolicyTests.cs b/test/Libraries/Microsoft.Extensions.AsyncState.Tests/FeaturesPooledPolicyTests.cs index 205509b6585..6e360789cd9 100644 --- a/test/Libraries/Microsoft.Extensions.AsyncState.Tests/FeaturesPooledPolicyTests.cs +++ b/test/Libraries/Microsoft.Extensions.AsyncState.Tests/FeaturesPooledPolicyTests.cs @@ -12,7 +12,7 @@ public void Return_ShouldBeTrue() { var policy = new FeaturesPooledPolicy(); - Assert.True(policy.Return([])); + Assert.True(policy.Return(new Features())); } [Fact] @@ -20,12 +20,14 @@ public void Return_ShouldNullList() { var policy = new FeaturesPooledPolicy(); - var dictionary = policy.Create(); - dictionary[new AsyncStateToken(0)] = string.Empty; - dictionary[new AsyncStateToken(1)] = Array.Empty(); - dictionary[new AsyncStateToken(2)] = new object(); + var features = policy.Create(); + features.Set(0, string.Empty); + features.Set(1, Array.Empty()); + features.Set(2, new object()); - Assert.True(policy.Return(dictionary)); - Assert.True(dictionary.IsEmpty); + Assert.True(policy.Return(features)); + Assert.Null(features.Get(0)); + Assert.Null(features.Get(1)); + Assert.Null(features.Get(2)); } } From 7b8936e295d6efee9785415835cdd7d2d7fdb594 Mon Sep 17 00:00:00 2001 From: Martin Obratil Date: Fri, 22 Dec 2023 14:55:23 +0100 Subject: [PATCH 5/5] Remove unused namespace in AsyncState.cs --- src/Libraries/Microsoft.Extensions.AsyncState/AsyncState.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Libraries/Microsoft.Extensions.AsyncState/AsyncState.cs b/src/Libraries/Microsoft.Extensions.AsyncState/AsyncState.cs index 354beb81375..9fc79930954 100644 --- a/src/Libraries/Microsoft.Extensions.AsyncState/AsyncState.cs +++ b/src/Libraries/Microsoft.Extensions.AsyncState/AsyncState.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; -using System.Collections.Generic; using System.Threading; using Microsoft.Extensions.ObjectPool; using Microsoft.Shared.Diagnostics;