Skip to content

Commit dbeeae5

Browse files
brantburnettRiPont
authored andcommitted
NCBC-XXXX: Dispose CTS when returned to pool on legacy frameworks
Motivation ---------- The pre-net6.0 implementation of the CancellationTokenSourcePool, which simply mocks the pool since a CTS is not reusable on old runtimes, is failing to Dispose of the CTS when it is returned. This can have very sigificant performance impacts on old runtimes such as .NET 4. Modifications ------------- - Dispose of the CTS when returned - Also don't capture the SynchronizationContext on operation subscriptions to cancellation tokens Results ------- Only legacy runtimes such as .NET 4 are affected. - A very signficant performance improvement because timers related to the CTS are removed from the timer queue immediately rather than waiting for garbage collection. This impact is greater the higher the rate of operations. - Removes a possible location for a deadlock in legacy ASP.NET in the case where a K/V operation times out but the request thread is blocked waiting on operation completion in sync-over-async usages. Note: This benchmark is somewhat artificial because the rate of timeout CTS cancellation relative to the rate of GC finalization is a factor. The benchmark is renting/returning timeout CTSs far faster than a real world application, and probably running GC at a different rate. This means the runtime for the WithoutDispose test is probably inflated. But it does still demonstrate the impact in a very high load scenario. BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.4037/23H2/2023Update/SunValley3) 12th Gen Intel Core i7-1270P, 1 CPU, 16 logical and 12 physical cores .NET SDK 8.0.400 [Host] : .NET 8.0.8 (8.0.824.36612), X64 RyuJIT AVX2 Job-RIFZTT : .NET Framework 4.8.1 (4.8.9261.0), X64 RyuJIT VectorSize=256 Runtime=.NET Framework 4.8 Toolchain=net48 | Method | Mean | Error | StdDev | Ratio | RatioSD | Rank | Gen0 | Gen1 | Gen2 | Allocated | Alloc Ratio | |--------------- |-----------:|----------:|----------:|------:|--------:|-----:|-------:|-------:|-------:|----------:|------------:| | WithoutDispose | 1,649.9 ns | 162.30 ns | 478.53 ns | 1.00 | 0.00 | 2 | 0.0420 | 0.0143 | 0.0010 | 259 B | 1.00 | | WithDispose | 147.8 ns | 1.83 ns | 1.62 ns | 0.10 | 0.05 | 1 | 0.0343 | - | - | 217 B | 0.84 | Change-Id: Ieaa31262aed3df5af17ec8c200db8a2e18e2b5fb Reviewed-on: https://review.couchbase.org/c/couchbase-net-client/+/215751 Reviewed-by: Richard Ponton <[email protected]> Tested-by: Build Bot <[email protected]>
1 parent d753b4d commit dbeeae5

File tree

2 files changed

+5
-4
lines changed

2 files changed

+5
-4
lines changed

src/Couchbase/Core/IO/Operations/OperationCancellationRegistration.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,11 @@ public OperationCancellationRegistration(IOperation operation, CancellationToken
4040

4141
// Since we're using static actions, register calls below are a fast noop for tokens which cannot be canceled
4242
#if NETSTANDARD2_0 || NETSTANDARD2_1 || NETCOREAPP2_1
43+
// useSynchronizationContext: false is the equivalent of awaiting ConfigureAwait(false) for the cancellation callback,
44+
// it allows the callback to run without synchronizing back to the original context.
4345
_externalRegistration = tokenPair.ExternalToken.Register(HandleExternalCancellationAction, operation);
44-
_internalRegistration = tokenPair.InternalToken.Register(HandleInternalCancellationAction, operation);
46+
_internalRegistration = tokenPair.InternalToken.Register(HandleInternalCancellationAction, operation,
47+
useSynchronizationContext: false);
4548
#else
4649
// On .NET Core 3 and later we can further optimize by not flowing the ExecutionContext using UnsafeRegister
4750
_externalRegistration = tokenPair.ExternalToken.UnsafeRegister(HandleExternalCancellationAction, operation);

src/Couchbase/Utils/CancellationTokenSourcePool.cs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,7 @@ private class CancellationTokenSourcePoolPolicy : PooledObjectPolicy<Cancellatio
5353
public CancellationTokenSource Rent(TimeSpan delay) =>
5454
new(delay);
5555

56-
public void Return(CancellationTokenSource cts)
57-
{
58-
}
56+
public void Return(CancellationTokenSource cts) => cts.Dispose();
5957

6058
public void Dispose()
6159
{

0 commit comments

Comments
 (0)