From 6ba4ffb375b4f2a554eca2797823d698a72bd2ce Mon Sep 17 00:00:00 2001 From: Brennan Date: Wed, 18 Dec 2024 14:13:35 -0800 Subject: [PATCH 01/12] wip --- .../src/NamedPipeTransportOptions.cs | 9 +- .../src/Client/SocketConnectionFactory.cs | 5 +- .../src/PublicAPI.Unshipped.txt | 1 + .../src/SocketConnectionContextFactory.cs | 4 +- .../src/SocketConnectionFactoryOptions.cs | 16 ++- .../src/SocketConnectionListener.cs | 6 +- .../src/SocketTransportFactory.cs | 20 +++- .../src/SocketTransportOptions.cs | 3 +- .../Buffers.MemoryPool/MemoryPoolFactory.cs | 12 +- .../PinnedBlockMemoryPool.cs | 110 ++++++++++++++++++ .../PinnedBlockMemoryPoolMetrics.cs | 86 ++++++++++++++ .../Http2CatIServiceCollectionExtensions.cs | 1 + 12 files changed, 255 insertions(+), 18 deletions(-) create mode 100644 src/Shared/Buffers.MemoryPool/PinnedBlockMemoryPoolMetrics.cs diff --git a/src/Servers/Kestrel/Transport.NamedPipes/src/NamedPipeTransportOptions.cs b/src/Servers/Kestrel/Transport.NamedPipes/src/NamedPipeTransportOptions.cs index a1d7d47f4854..c4626725f4d7 100644 --- a/src/Servers/Kestrel/Transport.NamedPipes/src/NamedPipeTransportOptions.cs +++ b/src/Servers/Kestrel/Transport.NamedPipes/src/NamedPipeTransportOptions.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Buffers; +using System.Diagnostics.Metrics; using System.IO.Pipes; namespace Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes; @@ -116,5 +117,11 @@ public static NamedPipeServerStream CreateDefaultNamedPipeServerStream(CreateNam } } - internal Func> MemoryPoolFactory { get; set; } = PinnedBlockMemoryPoolFactory.Create; + internal Func> MemoryPoolFactory { get; set; } = () => PinnedBlockMemoryPoolFactory.Create(new DummyMeterFactory()); + private sealed class DummyMeterFactory : IMeterFactory + { + public Meter Create(MeterOptions options) => new Meter(options); + + public void Dispose() { } + } } diff --git a/src/Servers/Kestrel/Transport.Sockets/src/Client/SocketConnectionFactory.cs b/src/Servers/Kestrel/Transport.Sockets/src/Client/SocketConnectionFactory.cs index db77bbe811d3..9735ef59e2d0 100644 --- a/src/Servers/Kestrel/Transport.Sockets/src/Client/SocketConnectionFactory.cs +++ b/src/Servers/Kestrel/Transport.Sockets/src/Client/SocketConnectionFactory.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Buffers; +using System.Diagnostics.Metrics; using System.IO.Pipelines; using System.Net; using System.Net.Sockets; @@ -21,13 +22,13 @@ internal sealed class SocketConnectionFactory : IConnectionFactory, IAsyncDispos private readonly PipeOptions _outputOptions; private readonly SocketSenderPool _socketSenderPool; - public SocketConnectionFactory(IOptions options, ILoggerFactory loggerFactory) + public SocketConnectionFactory(IOptions options, ILoggerFactory loggerFactory, IMeterFactory meterFactory) { ArgumentNullException.ThrowIfNull(options); ArgumentNullException.ThrowIfNull(loggerFactory); _options = options.Value; - _memoryPool = options.Value.MemoryPoolFactory(); + _memoryPool = options.Value.MemoryPoolFactory(meterFactory); _trace = loggerFactory.CreateLogger("Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.Client"); var maxReadBufferSize = _options.MaxReadBufferSize ?? 0; diff --git a/src/Servers/Kestrel/Transport.Sockets/src/PublicAPI.Unshipped.txt b/src/Servers/Kestrel/Transport.Sockets/src/PublicAPI.Unshipped.txt index 7dc5c58110bf..db4f3151423d 100644 --- a/src/Servers/Kestrel/Transport.Sockets/src/PublicAPI.Unshipped.txt +++ b/src/Servers/Kestrel/Transport.Sockets/src/PublicAPI.Unshipped.txt @@ -1 +1,2 @@ #nullable enable +Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.SocketTransportFactory.SocketTransportFactory(Microsoft.Extensions.Options.IOptions! options, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory, System.Diagnostics.Metrics.IMeterFactory! meterFactory) -> void diff --git a/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionContextFactory.cs b/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionContextFactory.cs index db98e36842e1..0dbe798b0f44 100644 --- a/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionContextFactory.cs +++ b/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionContextFactory.cs @@ -47,7 +47,7 @@ public SocketConnectionContextFactory(SocketConnectionFactoryOptions options, IL for (var i = 0; i < _settingsCount; i++) { - var memoryPool = _options.MemoryPoolFactory(); + var memoryPool = _options.MemoryPoolFactory(_options.MeterFactory); var transportScheduler = options.UnsafePreferInlineScheduling ? PipeScheduler.Inline : new IOQueue(); _settings[i] = new QueueSettings() @@ -62,7 +62,7 @@ public SocketConnectionContextFactory(SocketConnectionFactoryOptions options, IL } else { - var memoryPool = _options.MemoryPoolFactory(); + var memoryPool = _options.MemoryPoolFactory(_options.MeterFactory); var transportScheduler = options.UnsafePreferInlineScheduling ? PipeScheduler.Inline : PipeScheduler.ThreadPool; _settings = new QueueSettings[] diff --git a/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionFactoryOptions.cs b/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionFactoryOptions.cs index 403f6fc108de..e5d653484ade 100644 --- a/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionFactoryOptions.cs +++ b/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionFactoryOptions.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Buffers; +using System.Diagnostics.Metrics; namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets; @@ -13,9 +14,9 @@ public class SocketConnectionFactoryOptions /// /// Create a new instance. /// - public SocketConnectionFactoryOptions() { } + public SocketConnectionFactoryOptions() { MeterFactory = new DummyMeterFactory(); } - internal SocketConnectionFactoryOptions(SocketTransportOptions transportOptions) + internal SocketConnectionFactoryOptions(SocketTransportOptions transportOptions, IMeterFactory meterFactory) { IOQueueCount = transportOptions.IOQueueCount; WaitForDataBeforeAllocatingBuffer = transportOptions.WaitForDataBeforeAllocatingBuffer; @@ -24,6 +25,7 @@ internal SocketConnectionFactoryOptions(SocketTransportOptions transportOptions) UnsafePreferInlineScheduling = transportOptions.UnsafePreferInlineScheduling; MemoryPoolFactory = transportOptions.MemoryPoolFactory; FinOnError = transportOptions.FinOnError; + MeterFactory = meterFactory; } // Opt-out flag for back compat. Remove in 9.0 (or make public). @@ -67,5 +69,13 @@ internal SocketConnectionFactoryOptions(SocketTransportOptions transportOptions) /// public bool UnsafePreferInlineScheduling { get; set; } - internal Func> MemoryPoolFactory { get; set; } = PinnedBlockMemoryPoolFactory.Create; + internal IMeterFactory MeterFactory { get; set; } + internal Func> MemoryPoolFactory { get; set; } = PinnedBlockMemoryPoolFactory.Create; + + internal sealed class DummyMeterFactory : IMeterFactory + { + public Meter Create(MeterOptions options) => new Meter(options); + + public void Dispose() { } + } } diff --git a/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionListener.cs b/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionListener.cs index c8f0cf8ad543..3f7333471e47 100644 --- a/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionListener.cs +++ b/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionListener.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; +using System.Diagnostics.Metrics; using System.Net; using System.Net.Sockets; using Microsoft.AspNetCore.Connections; @@ -22,13 +23,14 @@ internal sealed class SocketConnectionListener : IConnectionListener internal SocketConnectionListener( EndPoint endpoint, SocketTransportOptions options, - ILoggerFactory loggerFactory) + ILoggerFactory loggerFactory, + IMeterFactory meterFactory) { EndPoint = endpoint; _options = options; var logger = loggerFactory.CreateLogger("Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets"); _logger = logger; - _factory = new SocketConnectionContextFactory(new SocketConnectionFactoryOptions(options), logger); + _factory = new SocketConnectionContextFactory(new SocketConnectionFactoryOptions(options, meterFactory), logger); } internal void Bind() diff --git a/src/Servers/Kestrel/Transport.Sockets/src/SocketTransportFactory.cs b/src/Servers/Kestrel/Transport.Sockets/src/SocketTransportFactory.cs index 16c1efb78aee..5d8a1e9b3fa5 100644 --- a/src/Servers/Kestrel/Transport.Sockets/src/SocketTransportFactory.cs +++ b/src/Servers/Kestrel/Transport.Sockets/src/SocketTransportFactory.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics.Metrics; using System.Net; using System.Net.Sockets; using Microsoft.AspNetCore.Connections; @@ -16,6 +17,7 @@ public sealed class SocketTransportFactory : IConnectionListenerFactory, IConnec { private readonly SocketTransportOptions _options; private readonly ILoggerFactory _logger; + private readonly IMeterFactory _meterFactory; /// /// Initializes a new instance of the class. @@ -24,19 +26,26 @@ public sealed class SocketTransportFactory : IConnectionListenerFactory, IConnec /// The logger factory. public SocketTransportFactory( IOptions options, - ILoggerFactory loggerFactory) + ILoggerFactory loggerFactory) : this(options, loggerFactory, new DummyMeterFactory()) + { } + + public SocketTransportFactory( + IOptions options, + ILoggerFactory loggerFactory, + IMeterFactory meterFactory) { ArgumentNullException.ThrowIfNull(options); ArgumentNullException.ThrowIfNull(loggerFactory); _options = options.Value; _logger = loggerFactory; + _meterFactory = meterFactory; } /// public ValueTask BindAsync(EndPoint endpoint, CancellationToken cancellationToken = default) { - var transport = new SocketConnectionListener(endpoint, _options, _logger); + var transport = new SocketConnectionListener(endpoint, _options, _logger, _meterFactory); transport.Bind(); return new ValueTask(transport); } @@ -52,4 +61,11 @@ public bool CanBind(EndPoint endpoint) _ => false }; } + + private sealed class DummyMeterFactory : IMeterFactory + { + public Meter Create(MeterOptions options) => new Meter(options); + + public void Dispose() { } + } } diff --git a/src/Servers/Kestrel/Transport.Sockets/src/SocketTransportOptions.cs b/src/Servers/Kestrel/Transport.Sockets/src/SocketTransportOptions.cs index ad1a877df63c..f2df33dac704 100644 --- a/src/Servers/Kestrel/Transport.Sockets/src/SocketTransportOptions.cs +++ b/src/Servers/Kestrel/Transport.Sockets/src/SocketTransportOptions.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Buffers; +using System.Diagnostics.Metrics; using System.Net; using System.Net.Sockets; using Microsoft.AspNetCore.Connections; @@ -166,5 +167,5 @@ public static Socket CreateDefaultBoundListenSocket(EndPoint endpoint) return listenSocket; } - internal Func> MemoryPoolFactory { get; set; } = System.Buffers.PinnedBlockMemoryPoolFactory.Create; + internal Func> MemoryPoolFactory { get; set; } = PinnedBlockMemoryPoolFactory.Create; } diff --git a/src/Shared/Buffers.MemoryPool/MemoryPoolFactory.cs b/src/Shared/Buffers.MemoryPool/MemoryPoolFactory.cs index 68880100c9c7..0b6d2f606713 100644 --- a/src/Shared/Buffers.MemoryPool/MemoryPoolFactory.cs +++ b/src/Shared/Buffers.MemoryPool/MemoryPoolFactory.cs @@ -1,21 +1,23 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics.Metrics; + namespace System.Buffers; internal static class PinnedBlockMemoryPoolFactory { - public static MemoryPool Create() + public static MemoryPool Create(IMeterFactory meterFactory) { #if DEBUG - return new DiagnosticMemoryPool(CreatePinnedBlockMemoryPool()); + return new DiagnosticMemoryPool(CreatePinnedBlockMemoryPool(meterFactory)); #else - return CreatePinnedBlockMemoryPool(); + return CreatePinnedBlockMemoryPool(meterFactory); #endif } - public static MemoryPool CreatePinnedBlockMemoryPool() + public static MemoryPool CreatePinnedBlockMemoryPool(IMeterFactory meterFactory) { - return new PinnedBlockMemoryPool(); + return new PinnedBlockMemoryPool(meterFactory); } } diff --git a/src/Shared/Buffers.MemoryPool/PinnedBlockMemoryPool.cs b/src/Shared/Buffers.MemoryPool/PinnedBlockMemoryPool.cs index 6c74b477c821..421308a9c841 100644 --- a/src/Shared/Buffers.MemoryPool/PinnedBlockMemoryPool.cs +++ b/src/Shared/Buffers.MemoryPool/PinnedBlockMemoryPool.cs @@ -2,6 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Concurrent; +using System.Diagnostics; +using System.Diagnostics.Metrics; #nullable enable @@ -39,6 +41,18 @@ internal sealed class PinnedBlockMemoryPool : MemoryPool /// private bool _isDisposed; // To detect redundant calls + private readonly long _memoryLimit = 30_000; + + private readonly PinnedBlockMemoryPoolMetrics _metrics; + + private long _evictionDelays; // Total time eviction tasks were delayed (ms) + private long _evictionDurations; // Total time spent on eviction (ms) + private long _currentMemory; + + private volatile bool _evictionInProgress; + private long _lastEvictionStartTimestamp; + private readonly EvictionWorkItem _evictionWorkItem; + private readonly object _disposeSync = new object(); /// @@ -46,6 +60,28 @@ internal sealed class PinnedBlockMemoryPool : MemoryPool /// private const int AnySize = -1; + public PinnedBlockMemoryPool(IMeterFactory meterFactory) + { + _metrics = new(meterFactory); + + _evictionWorkItem = new EvictionWorkItem(this); + var conserveMemory = Environment.GetEnvironmentVariable("DOTNET_GCConserveMemory") + ?? Environment.GetEnvironmentVariable("COMPlus_GCConserveMemory") + ?? "0"; + + if (!int.TryParse(conserveMemory, out var conserveSetting)) + { + conserveSetting = 0; + } + + conserveSetting = Math.Clamp(conserveSetting, 0, 9); + + // Will be a value between 1 and .1f + var conserveRatio = 1.0f - (conserveSetting / 10.0f); + // Adjust memory limit to be between 100% and 10% of original value + _memoryLimit = (long)(_memoryLimit * conserveRatio); + } + public override IMemoryOwner Rent(int size = AnySize) { if (size > _blockSize) @@ -60,9 +96,17 @@ public override IMemoryOwner Rent(int size = AnySize) if (_blocks.TryDequeue(out var block)) { + _metrics.UpdateCurrentMemory(-block.Memory.Length); + Interlocked.Add(ref _currentMemory, -block.Memory.Length); + _metrics.Rent(block.Memory.Length); + // block successfully taken from the stack - return it return block; } + + _metrics.IncrementTotalMemory(BlockSize); + _metrics.Rent(BlockSize); + return new MemoryPoolBlock(this, BlockSize); } @@ -84,8 +128,59 @@ internal void Return(MemoryPoolBlock block) if (!_isDisposed) { + _metrics.UpdateCurrentMemory(block.Memory.Length); + Interlocked.Add(ref _currentMemory, block.Memory.Length); + _blocks.Enqueue(block); } + + TryScheduleEviction(); + } + + private void TryScheduleEviction() + { + if (_currentMemory > _memoryLimit && !_evictionInProgress) + { + if (Interlocked.CompareExchange(ref _evictionInProgress, true, false) == false) + { + _metrics.StartEviction(); + + _lastEvictionStartTimestamp = Stopwatch.GetTimestamp(); + ThreadPool.UnsafeQueueUserWorkItem(_evictionWorkItem, preferLocal: false); + } + } + } + + private void PerformEviction() + { + var now = Stopwatch.GetTimestamp(); + + try + { + // Measure delay since the eviction was triggered + + var delayMs = Stopwatch.GetElapsedTime(_lastEvictionStartTimestamp).TotalMilliseconds; + Interlocked.Add(ref _evictionDelays, (long)delayMs); + + long evictedMemoryThisPass = 0; + + // Remove from queue and let GC clean the memory up + while (_currentMemory > _memoryLimit && _blocks.TryDequeue(out var block)) + { + Interlocked.Add(ref _currentMemory, -block.Memory.Length); + _metrics.UpdateCurrentMemory(-block.Memory.Length); + _metrics.EvictBlock(block.Memory.Length); + + evictedMemoryThisPass += block.Memory.Length; + } + + Debug.WriteLine($"Evicted {evictedMemoryThisPass} bytes."); + } + finally + { + Interlocked.Add(ref _evictionDurations, (long)Stopwatch.GetElapsedTime(now).TotalMilliseconds); + _evictionInProgress = false; + } } protected override void Dispose(bool disposing) @@ -109,4 +204,19 @@ protected override void Dispose(bool disposing) } } } + + private sealed class EvictionWorkItem : IThreadPoolWorkItem + { + private readonly PinnedBlockMemoryPool _pool; + + public EvictionWorkItem(PinnedBlockMemoryPool pool) + { + _pool = pool; + } + + public void Execute() + { + _pool.PerformEviction(); + } + } } diff --git a/src/Shared/Buffers.MemoryPool/PinnedBlockMemoryPoolMetrics.cs b/src/Shared/Buffers.MemoryPool/PinnedBlockMemoryPoolMetrics.cs new file mode 100644 index 000000000000..c2b6c6a04713 --- /dev/null +++ b/src/Shared/Buffers.MemoryPool/PinnedBlockMemoryPoolMetrics.cs @@ -0,0 +1,86 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.Metrics; +using System.Diagnostics; +using System.Runtime.CompilerServices; + +#nullable enable + +namespace System.Buffers; + +internal sealed class PinnedBlockMemoryPoolMetrics +{ + // Note: Dot separated instead of dash. + public const string MeterName = "System.Buffers.PinnedBlockMemoryPool"; + + private readonly Meter _meter; + private readonly UpDownCounter _currentMemory; + private readonly UpDownCounter _totalAllocatedMemory; + private readonly UpDownCounter _evictedBlocks; + private readonly UpDownCounter _evictedMemory; + private readonly UpDownCounter _evictionAttempts; + private readonly Counter _usageRate; + + public PinnedBlockMemoryPoolMetrics(IMeterFactory meterFactory) + { + _meter = meterFactory.Create(MeterName); + + _currentMemory = _meter.CreateUpDownCounter( + "pinnedblockmemorypool.current_memory", + unit: "{bytes}", + description: "Number of bytes that are currently pooled by the pinned block memory pool."); + + _totalAllocatedMemory = _meter.CreateUpDownCounter( + "pinnedblockmemorypool.total_allocated", + unit: "{bytes}", + description: "Total number of allocations made by the pinned block memory pool."); + + _evictedBlocks = _meter.CreateUpDownCounter( + "pinnedblockmemorypool.evicted_blocks", + unit: "{blocks}", + description: "Total number of pooled blocks that have been evicted."); + + _evictedMemory = _meter.CreateUpDownCounter( + "pinnedblockmemorypool.evicted_memory", + unit: "{bytes}", + description: "Total number of bytes that have been evicted."); + + _evictionAttempts = _meter.CreateUpDownCounter( + "pinnedblockmemorypool.eviction_attempts", + unit: "{eviction}", + description: "Total number of eviction attempts."); + + _usageRate = _meter.CreateCounter( + "pinnedblockmemorypool.usage_rate", + unit: "bytes", + description: "Rate of bytes rented from the pool." + ); + } + + public void UpdateCurrentMemory(int bytes) + { + _currentMemory.Add(bytes); + } + + public void IncrementTotalMemory(int bytes) + { + _totalAllocatedMemory.Add(bytes); + } + + public void EvictBlock(int bytes) + { + _evictedBlocks.Add(1); + _evictedMemory.Add(bytes); + } + + public void StartEviction() + { + _evictionAttempts.Add(1); + } + + public void Rent(int bytes) + { + _usageRate.Add(bytes); + } +} diff --git a/src/Shared/Http2cat/Http2CatIServiceCollectionExtensions.cs b/src/Shared/Http2cat/Http2CatIServiceCollectionExtensions.cs index 4544b04b2da2..b8e43139277a 100644 --- a/src/Shared/Http2cat/Http2CatIServiceCollectionExtensions.cs +++ b/src/Shared/Http2cat/Http2CatIServiceCollectionExtensions.cs @@ -12,6 +12,7 @@ internal static class Http2CatIServiceCollectionExtensions { public static IServiceCollection UseHttp2Cat(this IServiceCollection services, Action configureOptions) { + services.AddMetrics(); services.AddSingleton(); services.AddHostedService(); services.Configure(configureOptions); From de7c126cf7be01bef19e87bc7fd914fa8c1762f6 Mon Sep 17 00:00:00 2001 From: Brennan Date: Wed, 19 Feb 2025 11:28:40 -0800 Subject: [PATCH 02/12] stash --- src/Servers/HttpSys/src/HttpSysListener.cs | 10 +- src/Servers/IIS/IIS/src/Core/IISHttpServer.cs | 10 +- .../shared/test/Http3/Http3InMemory.cs | 10 +- .../Kestrel/shared/test/TestContextFactory.cs | 10 +- .../Kestrel/shared/test/TestServiceContext.cs | 10 +- .../Http2/Http2TestBase.cs | 10 +- .../KestrelMetricsTests.cs | 4 +- .../PinnedBlockMemoryPool.cs | 110 ++++++++++-------- 8 files changed, 118 insertions(+), 56 deletions(-) diff --git a/src/Servers/HttpSys/src/HttpSysListener.cs b/src/Servers/HttpSys/src/HttpSysListener.cs index 933c627cd56e..45ddfa39053d 100644 --- a/src/Servers/HttpSys/src/HttpSysListener.cs +++ b/src/Servers/HttpSys/src/HttpSysListener.cs @@ -3,6 +3,7 @@ using System.Buffers; using System.Diagnostics; +using System.Diagnostics.Metrics; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.HttpSys.Internal; using Microsoft.AspNetCore.WebUtilities; @@ -33,7 +34,14 @@ internal sealed partial class HttpSysListener : IDisposable // 0.5 seconds per request. Respond with a 400 Bad Request. private const int UnknownHeaderLimit = 1000; - internal MemoryPool MemoryPool { get; } = PinnedBlockMemoryPoolFactory.Create(); + internal sealed class DummyMeterFactory : IMeterFactory + { + public Meter Create(MeterOptions options) => new Meter(options); + + public void Dispose() { } + } + + internal MemoryPool MemoryPool { get; } = PinnedBlockMemoryPoolFactory.Create(new DummyMeterFactory()); private volatile State _state; // m_State is set only within lock blocks, but often read outside locks. diff --git a/src/Servers/IIS/IIS/src/Core/IISHttpServer.cs b/src/Servers/IIS/IIS/src/Core/IISHttpServer.cs index 6abaa5b2c0db..a3f6c8349dd2 100644 --- a/src/Servers/IIS/IIS/src/Core/IISHttpServer.cs +++ b/src/Servers/IIS/IIS/src/Core/IISHttpServer.cs @@ -3,6 +3,7 @@ using System.Buffers; using System.Diagnostics; +using System.Diagnostics.Metrics; using System.Runtime.InteropServices; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Builder; @@ -18,10 +19,17 @@ namespace Microsoft.AspNetCore.Server.IIS.Core; internal sealed class IISHttpServer : IServer { + internal sealed class DummyMeterFactory : IMeterFactory + { + public Meter Create(MeterOptions options) => new Meter(options); + + public void Dispose() { } + } + private const string WebSocketVersionString = "WEBSOCKET_VERSION"; private IISContextFactory? _iisContextFactory; - private readonly MemoryPool _memoryPool = new PinnedBlockMemoryPool(); + private readonly MemoryPool _memoryPool = new PinnedBlockMemoryPool(new DummyMeterFactory()); private GCHandle _httpServerHandle; private readonly IHostApplicationLifetime _applicationLifetime; private readonly ILogger _logger; diff --git a/src/Servers/Kestrel/shared/test/Http3/Http3InMemory.cs b/src/Servers/Kestrel/shared/test/Http3/Http3InMemory.cs index 64ebcdb07b41..4d6e9ff917df 100644 --- a/src/Servers/Kestrel/shared/test/Http3/Http3InMemory.cs +++ b/src/Servers/Kestrel/shared/test/Http3/Http3InMemory.cs @@ -5,6 +5,7 @@ using System.Buffers; using System.Collections.Concurrent; using System.Diagnostics; +using System.Diagnostics.Metrics; using System.Globalization; using System.IO.Pipelines; using System.Net.Http; @@ -70,11 +71,18 @@ public void OnTimeout(TimeoutReason reason) } } + internal sealed class DummyMeterFactory : IMeterFactory + { + public Meter Create(MeterOptions options) => new Meter(options); + + public void Dispose() { } + } + internal ServiceContext _serviceContext; private FakeTimeProvider _fakeTimeProvider; internal HttpConnection _httpConnection; internal readonly TimeoutControl _timeoutControl; - internal readonly MemoryPool _memoryPool = PinnedBlockMemoryPoolFactory.Create(); + internal readonly MemoryPool _memoryPool = PinnedBlockMemoryPoolFactory.Create(new DummyMeterFactory()); internal readonly ConcurrentQueue _streamContextPool = new ConcurrentQueue(); protected Task _connectionTask; internal ILogger Logger { get; } diff --git a/src/Servers/Kestrel/shared/test/TestContextFactory.cs b/src/Servers/Kestrel/shared/test/TestContextFactory.cs index 3baf69b6348a..692d1f1b0fdb 100644 --- a/src/Servers/Kestrel/shared/test/TestContextFactory.cs +++ b/src/Servers/Kestrel/shared/test/TestContextFactory.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Buffers; +using System.Diagnostics.Metrics; using System.IO.Pipelines; using System.Net; using Microsoft.AspNetCore.Connections; @@ -73,6 +74,13 @@ public static HttpConnectionContext CreateHttpConnectionContext( return context; } + internal sealed class DummyMeterFactory : IMeterFactory + { + public Meter Create(MeterOptions options) => new Meter(options); + + public void Dispose() { } + } + public static HttpMultiplexedConnectionContext CreateHttp3ConnectionContext( MultiplexedConnectionContext connectionContext = null, ServiceContext serviceContext = null, @@ -93,7 +101,7 @@ public static HttpMultiplexedConnectionContext CreateHttp3ConnectionContext( connectionContext, serviceContext ?? CreateServiceContext(new KestrelServerOptions()), connectionFeatures ?? new FeatureCollection(), - memoryPool ?? PinnedBlockMemoryPoolFactory.Create(), + memoryPool ?? PinnedBlockMemoryPoolFactory.Create(new DummyMeterFactory()), localEndPoint, remoteEndPoint, metricsContext) diff --git a/src/Servers/Kestrel/shared/test/TestServiceContext.cs b/src/Servers/Kestrel/shared/test/TestServiceContext.cs index 85692f1fe341..f517ba9add6c 100644 --- a/src/Servers/Kestrel/shared/test/TestServiceContext.cs +++ b/src/Servers/Kestrel/shared/test/TestServiceContext.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Buffers; +using System.Diagnostics.Metrics; using System.IO.Pipelines; using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal; @@ -73,7 +74,14 @@ private void Initialize(ILoggerFactory loggerFactory, KestrelTrace kestrelTrace, public FakeTimeProvider FakeTimeProvider { get; set; } - public Func> MemoryPoolFactory { get; set; } = System.Buffers.PinnedBlockMemoryPoolFactory.Create; + public Func> MemoryPoolFactory { get; set; } = () => System.Buffers.PinnedBlockMemoryPoolFactory.Create(new DummyMeterFactory()); public string DateHeaderValue => DateHeaderValueManager.GetDateHeaderValues().String; + + internal sealed class DummyMeterFactory : IMeterFactory + { + public Meter Create(MeterOptions options) => new Meter(options); + + public void Dispose() { } + } } diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs index e4d452b91aa0..4fe1c40a800b 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs @@ -5,6 +5,7 @@ using System.Buffers.Binary; using System.Collections.Concurrent; using System.Diagnostics; +using System.Diagnostics.Metrics; using System.Globalization; using System.IO.Pipelines; using System.Net.Http; @@ -112,7 +113,14 @@ protected static IEnumerable> ReadRateRequestHeader protected static readonly byte[] _noData = new byte[0]; protected static readonly byte[] _maxData = Encoding.ASCII.GetBytes(new string('a', Http2PeerSettings.MinAllowedMaxFrameSize)); - private readonly MemoryPool _memoryPool = PinnedBlockMemoryPoolFactory.Create(); + internal sealed class DummyMeterFactory : IMeterFactory + { + public Meter Create(MeterOptions options) => new Meter(options); + + public void Dispose() { } + } + + private readonly MemoryPool _memoryPool = PinnedBlockMemoryPoolFactory.Create(new DummyMeterFactory()); internal readonly Http2PeerSettings _clientSettings = new Http2PeerSettings(); internal readonly HPackDecoder _hpackDecoder; diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/KestrelMetricsTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/KestrelMetricsTests.cs index ef415b5a0623..3ef612b5857b 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/KestrelMetricsTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/KestrelMetricsTests.cs @@ -273,7 +273,7 @@ public async Task Http1Connection_ServerShutdown_Abort() var serviceContext = new TestServiceContext(LoggerFactory, metrics: new KestrelMetrics(testMeterFactory)) { - MemoryPoolFactory = PinnedBlockMemoryPoolFactory.CreatePinnedBlockMemoryPool, + //MemoryPoolFactory = PinnedBlockMemoryPoolFactory.CreatePinnedBlockMemoryPool, ShutdownTimeout = TimeSpan.Zero }; @@ -612,7 +612,7 @@ public async Task Http2Connection_ServerShutdown_Abort() var serviceContext = new TestServiceContext(LoggerFactory, metrics: new KestrelMetrics(testMeterFactory)) { ShutdownTimeout = TimeSpan.Zero, - MemoryPoolFactory = PinnedBlockMemoryPoolFactory.CreatePinnedBlockMemoryPool + //MemoryPoolFactory = PinnedBlockMemoryPoolFactory.CreatePinnedBlockMemoryPool }; var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); diff --git a/src/Shared/Buffers.MemoryPool/PinnedBlockMemoryPool.cs b/src/Shared/Buffers.MemoryPool/PinnedBlockMemoryPool.cs index 421308a9c841..1bf947b1e5bd 100644 --- a/src/Shared/Buffers.MemoryPool/PinnedBlockMemoryPool.cs +++ b/src/Shared/Buffers.MemoryPool/PinnedBlockMemoryPool.cs @@ -43,15 +43,17 @@ internal sealed class PinnedBlockMemoryPool : MemoryPool private readonly long _memoryLimit = 30_000; - private readonly PinnedBlockMemoryPoolMetrics _metrics; + //private readonly PinnedBlockMemoryPoolMetrics _metrics; private long _evictionDelays; // Total time eviction tasks were delayed (ms) private long _evictionDurations; // Total time spent on eviction (ms) - private long _currentMemory; + public long _currentMemory; + public long _evictedMemory; - private volatile bool _evictionInProgress; - private long _lastEvictionStartTimestamp; - private readonly EvictionWorkItem _evictionWorkItem; + private readonly long _lastEvictionStartTimestamp; + private readonly PeriodicTimer _timer; + private int _rentCount; + private int _returnCount; private readonly object _disposeSync = new object(); @@ -62,9 +64,10 @@ internal sealed class PinnedBlockMemoryPool : MemoryPool public PinnedBlockMemoryPool(IMeterFactory meterFactory) { - _metrics = new(meterFactory); + _ = meterFactory; + //_timeProvider = timeProvider; + //_metrics = new(meterFactory); - _evictionWorkItem = new EvictionWorkItem(this); var conserveMemory = Environment.GetEnvironmentVariable("DOTNET_GCConserveMemory") ?? Environment.GetEnvironmentVariable("COMPlus_GCConserveMemory") ?? "0"; @@ -80,6 +83,24 @@ public PinnedBlockMemoryPool(IMeterFactory meterFactory) var conserveRatio = 1.0f - (conserveSetting / 10.0f); // Adjust memory limit to be between 100% and 10% of original value _memoryLimit = (long)(_memoryLimit * conserveRatio); + + _timer = new PeriodicTimer(TimeSpan.FromSeconds(10)); + _ = RunTimer(); + } + + private async Task RunTimer() + { + try + { + while (await _timer.WaitForNextTickAsync()) + { + PerformEviction(); + } + } + catch (Exception ex) + { + Debug.WriteLine(ex); + } } public override IMemoryOwner Rent(int size = AnySize) @@ -94,18 +115,17 @@ public override IMemoryOwner Rent(int size = AnySize) MemoryPoolThrowHelper.ThrowObjectDisposedException(MemoryPoolThrowHelper.ExceptionArgument.MemoryPool); } + Interlocked.Increment(ref _rentCount); + if (_blocks.TryDequeue(out var block)) { - _metrics.UpdateCurrentMemory(-block.Memory.Length); Interlocked.Add(ref _currentMemory, -block.Memory.Length); - _metrics.Rent(block.Memory.Length); // block successfully taken from the stack - return it return block; } - _metrics.IncrementTotalMemory(BlockSize); - _metrics.Rent(BlockSize); + Interlocked.Increment(ref _rentCount); return new MemoryPoolBlock(this, BlockSize); } @@ -126,29 +146,14 @@ internal void Return(MemoryPoolBlock block) block.IsLeased = false; #endif + Interlocked.Increment(ref _returnCount); + if (!_isDisposed) { - _metrics.UpdateCurrentMemory(block.Memory.Length); Interlocked.Add(ref _currentMemory, block.Memory.Length); _blocks.Enqueue(block); } - - TryScheduleEviction(); - } - - private void TryScheduleEviction() - { - if (_currentMemory > _memoryLimit && !_evictionInProgress) - { - if (Interlocked.CompareExchange(ref _evictionInProgress, true, false) == false) - { - _metrics.StartEviction(); - - _lastEvictionStartTimestamp = Stopwatch.GetTimestamp(); - ThreadPool.UnsafeQueueUserWorkItem(_evictionWorkItem, preferLocal: false); - } - } } private void PerformEviction() @@ -158,20 +163,43 @@ private void PerformEviction() try { // Measure delay since the eviction was triggered - + var delayMs = Stopwatch.GetElapsedTime(_lastEvictionStartTimestamp).TotalMilliseconds; Interlocked.Add(ref _evictionDelays, (long)delayMs); long evictedMemoryThisPass = 0; + var currentCount = _blocks.Count; + var burstAmount = 0; + // If any activity + if (_rentCount + _returnCount > 0) + { + // Trending less traffic + if (_returnCount > _rentCount) + { + burstAmount = currentCount / _returnCount - _rentCount; + } + // Traffic staying the same, try removing some blocks since we probably have excess + else if (_returnCount == _rentCount) + { + burstAmount = Math.Min(10, currentCount / 20); + } + } + // If no activity + else + { + burstAmount = Math.Max(1, currentCount / 10); + } + _rentCount = 0; + _returnCount = 0; // Remove from queue and let GC clean the memory up - while (_currentMemory > _memoryLimit && _blocks.TryDequeue(out var block)) + while (burstAmount > 0 && _blocks.TryDequeue(out var block)) { Interlocked.Add(ref _currentMemory, -block.Memory.Length); - _metrics.UpdateCurrentMemory(-block.Memory.Length); - _metrics.EvictBlock(block.Memory.Length); + Interlocked.Add(ref _evictedMemory, block.Memory.Length); evictedMemoryThisPass += block.Memory.Length; + burstAmount--; } Debug.WriteLine($"Evicted {evictedMemoryThisPass} bytes."); @@ -179,7 +207,6 @@ private void PerformEviction() finally { Interlocked.Add(ref _evictionDurations, (long)Stopwatch.GetElapsedTime(now).TotalMilliseconds); - _evictionInProgress = false; } } @@ -196,6 +223,8 @@ protected override void Dispose(bool disposing) if (disposing) { + _timer.Dispose(); + // Discard blocks in pool while (_blocks.TryDequeue(out _)) { @@ -204,19 +233,4 @@ protected override void Dispose(bool disposing) } } } - - private sealed class EvictionWorkItem : IThreadPoolWorkItem - { - private readonly PinnedBlockMemoryPool _pool; - - public EvictionWorkItem(PinnedBlockMemoryPool pool) - { - _pool = pool; - } - - public void Execute() - { - _pool.PerformEviction(); - } - } } From 3a1b78010cef055b0eb4c952a038881df69ab62b Mon Sep 17 00:00:00 2001 From: Brennan Date: Thu, 27 Feb 2025 15:16:34 -0800 Subject: [PATCH 03/12] IMemoryPoolFactory --- .../src/IMemoryPoolFactory.cs | 18 ++ .../PublicAPI/net10.0/PublicAPI.Unshipped.txt | 2 + .../PublicAPI/net462/PublicAPI.Unshipped.txt | 2 + .../netstandard2.0/PublicAPI.Unshipped.txt | 2 + .../netstandard2.1/PublicAPI.Unshipped.txt | 2 + .../Core/src/Internal/KestrelServerImpl.cs | 10 +- .../Internal/PinnedBlockMemoryPoolFactory.cs | 43 ++++ src/Servers/Kestrel/Core/src/KestrelServer.cs | 3 +- ...soft.AspNetCore.Server.Kestrel.Core.csproj | 4 +- .../Kestrel/Core/test/KestrelServerTests.cs | 3 +- .../src/WebHostBuilderKestrelExtensions.cs | 4 + .../src/NamedPipeTransportOptions.cs | 9 +- .../src/Client/SocketConnectionFactory.cs | 5 +- .../src/Internal/DefaultMemoryPoolFactory.cs | 17 ++ .../src/Internal/SocketConnection.cs | 3 +- ...re.Server.Kestrel.Transport.Sockets.csproj | 1 - .../src/PublicAPI.Unshipped.txt | 1 - .../src/SocketConnectionContextFactory.cs | 4 +- .../src/SocketConnectionFactoryOptions.cs | 19 +- .../src/SocketConnectionListener.cs | 6 +- .../src/SocketTransportFactory.cs | 12 +- .../src/SocketTransportOptions.cs | 5 +- .../src/WebHostBuilderSocketExtensions.cs | 7 + .../Kestrel/shared/test/TestContextFactory.cs | 9 +- .../Kestrel/shared/test/TestServiceContext.cs | 14 +- .../DiagnosticMemoryPoolFactory.cs | 5 +- .../test/TransportTestHelpers/TestServer.cs | 15 +- .../FunctionalTests/Http2/ShutdownTests.cs | 2 +- .../MaxRequestBufferSizeTests.cs | 25 +-- .../KestrelMetricsTests.cs | 2 +- .../TestTransport/TestServer.cs | 2 +- .../TransportSelector.cs | 15 +- .../Buffers.MemoryPool/MemoryPoolFactory.cs | 19 +- .../PinnedBlockMemoryPool.cs | 183 +++++++++--------- .../PinnedBlockMemoryPoolMetrics.cs | 26 +-- 35 files changed, 281 insertions(+), 218 deletions(-) create mode 100644 src/Servers/Connections.Abstractions/src/IMemoryPoolFactory.cs create mode 100644 src/Servers/Kestrel/Core/src/Internal/PinnedBlockMemoryPoolFactory.cs create mode 100644 src/Servers/Kestrel/Transport.Sockets/src/Internal/DefaultMemoryPoolFactory.cs diff --git a/src/Servers/Connections.Abstractions/src/IMemoryPoolFactory.cs b/src/Servers/Connections.Abstractions/src/IMemoryPoolFactory.cs new file mode 100644 index 000000000000..4ae7cc4257e6 --- /dev/null +++ b/src/Servers/Connections.Abstractions/src/IMemoryPoolFactory.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Buffers; + +namespace Microsoft.AspNetCore.Connections; + +/// +/// +/// +public interface IMemoryPoolFactory +{ + /// + /// + /// + /// + MemoryPool CreatePool(); +} diff --git a/src/Servers/Connections.Abstractions/src/PublicAPI/net10.0/PublicAPI.Unshipped.txt b/src/Servers/Connections.Abstractions/src/PublicAPI/net10.0/PublicAPI.Unshipped.txt index 7dc5c58110bf..18a4e0cf0893 100644 --- a/src/Servers/Connections.Abstractions/src/PublicAPI/net10.0/PublicAPI.Unshipped.txt +++ b/src/Servers/Connections.Abstractions/src/PublicAPI/net10.0/PublicAPI.Unshipped.txt @@ -1 +1,3 @@ #nullable enable +Microsoft.AspNetCore.Connections.IMemoryPoolFactory +Microsoft.AspNetCore.Connections.IMemoryPoolFactory.CreatePool() -> System.Buffers.MemoryPool! diff --git a/src/Servers/Connections.Abstractions/src/PublicAPI/net462/PublicAPI.Unshipped.txt b/src/Servers/Connections.Abstractions/src/PublicAPI/net462/PublicAPI.Unshipped.txt index 7dc5c58110bf..18a4e0cf0893 100644 --- a/src/Servers/Connections.Abstractions/src/PublicAPI/net462/PublicAPI.Unshipped.txt +++ b/src/Servers/Connections.Abstractions/src/PublicAPI/net462/PublicAPI.Unshipped.txt @@ -1 +1,3 @@ #nullable enable +Microsoft.AspNetCore.Connections.IMemoryPoolFactory +Microsoft.AspNetCore.Connections.IMemoryPoolFactory.CreatePool() -> System.Buffers.MemoryPool! diff --git a/src/Servers/Connections.Abstractions/src/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt b/src/Servers/Connections.Abstractions/src/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt index 7dc5c58110bf..18a4e0cf0893 100644 --- a/src/Servers/Connections.Abstractions/src/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt +++ b/src/Servers/Connections.Abstractions/src/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt @@ -1 +1,3 @@ #nullable enable +Microsoft.AspNetCore.Connections.IMemoryPoolFactory +Microsoft.AspNetCore.Connections.IMemoryPoolFactory.CreatePool() -> System.Buffers.MemoryPool! diff --git a/src/Servers/Connections.Abstractions/src/PublicAPI/netstandard2.1/PublicAPI.Unshipped.txt b/src/Servers/Connections.Abstractions/src/PublicAPI/netstandard2.1/PublicAPI.Unshipped.txt index 7dc5c58110bf..18a4e0cf0893 100644 --- a/src/Servers/Connections.Abstractions/src/PublicAPI/netstandard2.1/PublicAPI.Unshipped.txt +++ b/src/Servers/Connections.Abstractions/src/PublicAPI/netstandard2.1/PublicAPI.Unshipped.txt @@ -1 +1,3 @@ #nullable enable +Microsoft.AspNetCore.Connections.IMemoryPoolFactory +Microsoft.AspNetCore.Connections.IMemoryPoolFactory.CreatePool() -> System.Buffers.MemoryPool! diff --git a/src/Servers/Kestrel/Core/src/Internal/KestrelServerImpl.cs b/src/Servers/Kestrel/Core/src/Internal/KestrelServerImpl.cs index 9324e481104c..ab03c06fcaaf 100644 --- a/src/Servers/Kestrel/Core/src/Internal/KestrelServerImpl.cs +++ b/src/Servers/Kestrel/Core/src/Internal/KestrelServerImpl.cs @@ -40,8 +40,9 @@ public KestrelServerImpl( IHttpsConfigurationService httpsConfigurationService, ILoggerFactory loggerFactory, DiagnosticSource? diagnosticSource, - KestrelMetrics metrics) - : this(transportFactories, multiplexedFactories, httpsConfigurationService, CreateServiceContext(options, loggerFactory, diagnosticSource, metrics)) + KestrelMetrics metrics, + IEnumerable heartbeatHandlers) + : this(transportFactories, multiplexedFactories, httpsConfigurationService, CreateServiceContext(options, loggerFactory, diagnosticSource, metrics, heartbeatHandlers)) { } @@ -73,7 +74,8 @@ internal KestrelServerImpl( _transportManager = new TransportManager(_transportFactories, _multiplexedTransportFactories, _httpsConfigurationService, ServiceContext); } - private static ServiceContext CreateServiceContext(IOptions options, ILoggerFactory loggerFactory, DiagnosticSource? diagnosticSource, KestrelMetrics metrics) + private static ServiceContext CreateServiceContext(IOptions options, ILoggerFactory loggerFactory, DiagnosticSource? diagnosticSource, KestrelMetrics metrics, + IEnumerable heartbeatHandlers) { ArgumentNullException.ThrowIfNull(options); ArgumentNullException.ThrowIfNull(loggerFactory); @@ -87,7 +89,7 @@ private static ServiceContext CreateServiceContext(IOptions _pools = new(); + + public PinnedBlockMemoryPoolFactory(IMeterFactory meterFactory) + { + _meterFactory = meterFactory; + } + + public MemoryPool CreatePool() + { + // TODO: wire up PinnedBlockMemoryPool's dispose to remove from _pools + var pool = new PinnedBlockMemoryPool(_meterFactory); + + _pools.TryAdd(pool, pool); + +#if DEBUG + return new DiagnosticMemoryPool(pool); +#else + return pool; +#endif + } + + public void OnHeartbeat() + { + foreach (var pool in _pools) + { + pool.Value.PerformEviction(); + } + } +} diff --git a/src/Servers/Kestrel/Core/src/KestrelServer.cs b/src/Servers/Kestrel/Core/src/KestrelServer.cs index 7f2909c77cf6..3b91510218f3 100644 --- a/src/Servers/Kestrel/Core/src/KestrelServer.cs +++ b/src/Servers/Kestrel/Core/src/KestrelServer.cs @@ -37,7 +37,8 @@ public KestrelServer(IOptions options, IConnectionListener new SimpleHttpsConfigurationService(), loggerFactory, diagnosticSource: null, - new KestrelMetrics(new DummyMeterFactory())); + new KestrelMetrics(new DummyMeterFactory()), + heartbeatHandlers: []); } /// diff --git a/src/Servers/Kestrel/Core/src/Microsoft.AspNetCore.Server.Kestrel.Core.csproj b/src/Servers/Kestrel/Core/src/Microsoft.AspNetCore.Server.Kestrel.Core.csproj index 9199ae066c0a..0a8384a65166 100644 --- a/src/Servers/Kestrel/Core/src/Microsoft.AspNetCore.Server.Kestrel.Core.csproj +++ b/src/Servers/Kestrel/Core/src/Microsoft.AspNetCore.Server.Kestrel.Core.csproj @@ -1,4 +1,4 @@ - + Core components of ASP.NET Core Kestrel cross-platform web server. @@ -37,6 +37,7 @@ + @@ -70,6 +71,7 @@ + diff --git a/src/Servers/Kestrel/Core/test/KestrelServerTests.cs b/src/Servers/Kestrel/Core/test/KestrelServerTests.cs index e688812a6075..be0c0de49cd5 100644 --- a/src/Servers/Kestrel/Core/test/KestrelServerTests.cs +++ b/src/Servers/Kestrel/Core/test/KestrelServerTests.cs @@ -310,7 +310,8 @@ private static KestrelServerImpl CreateKestrelServer( httpsConfigurationService, loggerFactory ?? new LoggerFactory(new[] { new KestrelTestLoggerProvider() }), diagnosticSource: null, - metrics ?? new KestrelMetrics(new TestMeterFactory())); + metrics ?? new KestrelMetrics(new TestMeterFactory()), + heartbeatHandlers: []); } [Fact] diff --git a/src/Servers/Kestrel/Kestrel/src/WebHostBuilderKestrelExtensions.cs b/src/Servers/Kestrel/Kestrel/src/WebHostBuilderKestrelExtensions.cs index 7186715a6c5f..894ce210dca2 100644 --- a/src/Servers/Kestrel/Kestrel/src/WebHostBuilderKestrelExtensions.cs +++ b/src/Servers/Kestrel/Kestrel/src/WebHostBuilderKestrelExtensions.cs @@ -88,6 +88,10 @@ public static IWebHostBuilder UseKestrelCore(this IWebHostBuilder hostBuilder) services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + + services.AddSingleton(); + services.TryAddEnumerable(ServiceDescriptor.Singleton(sp => sp.GetRequiredService())); + services.AddSingleton(sp => sp.GetRequiredService()); }); if (OperatingSystem.IsWindows()) diff --git a/src/Servers/Kestrel/Transport.NamedPipes/src/NamedPipeTransportOptions.cs b/src/Servers/Kestrel/Transport.NamedPipes/src/NamedPipeTransportOptions.cs index c4626725f4d7..34b57c2fe3c2 100644 --- a/src/Servers/Kestrel/Transport.NamedPipes/src/NamedPipeTransportOptions.cs +++ b/src/Servers/Kestrel/Transport.NamedPipes/src/NamedPipeTransportOptions.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Buffers; -using System.Diagnostics.Metrics; using System.IO.Pipes; namespace Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes; @@ -117,11 +116,5 @@ public static NamedPipeServerStream CreateDefaultNamedPipeServerStream(CreateNam } } - internal Func> MemoryPoolFactory { get; set; } = () => PinnedBlockMemoryPoolFactory.Create(new DummyMeterFactory()); - private sealed class DummyMeterFactory : IMeterFactory - { - public Meter Create(MeterOptions options) => new Meter(options); - - public void Dispose() { } - } + internal Func> MemoryPoolFactory { get; set; } = () => PinnedBlockMemoryPoolFactory.Create(); } diff --git a/src/Servers/Kestrel/Transport.Sockets/src/Client/SocketConnectionFactory.cs b/src/Servers/Kestrel/Transport.Sockets/src/Client/SocketConnectionFactory.cs index 9735ef59e2d0..74bdeba7f64a 100644 --- a/src/Servers/Kestrel/Transport.Sockets/src/Client/SocketConnectionFactory.cs +++ b/src/Servers/Kestrel/Transport.Sockets/src/Client/SocketConnectionFactory.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Buffers; -using System.Diagnostics.Metrics; using System.IO.Pipelines; using System.Net; using System.Net.Sockets; @@ -22,13 +21,13 @@ internal sealed class SocketConnectionFactory : IConnectionFactory, IAsyncDispos private readonly PipeOptions _outputOptions; private readonly SocketSenderPool _socketSenderPool; - public SocketConnectionFactory(IOptions options, ILoggerFactory loggerFactory, IMeterFactory meterFactory) + public SocketConnectionFactory(IOptions options, ILoggerFactory loggerFactory) { ArgumentNullException.ThrowIfNull(options); ArgumentNullException.ThrowIfNull(loggerFactory); _options = options.Value; - _memoryPool = options.Value.MemoryPoolFactory(meterFactory); + _memoryPool = options.Value.MemoryPoolFactory.CreatePool(); _trace = loggerFactory.CreateLogger("Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.Client"); var maxReadBufferSize = _options.MaxReadBufferSize ?? 0; diff --git a/src/Servers/Kestrel/Transport.Sockets/src/Internal/DefaultMemoryPoolFactory.cs b/src/Servers/Kestrel/Transport.Sockets/src/Internal/DefaultMemoryPoolFactory.cs new file mode 100644 index 000000000000..6a5883617420 --- /dev/null +++ b/src/Servers/Kestrel/Transport.Sockets/src/Internal/DefaultMemoryPoolFactory.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Buffers; +using Microsoft.AspNetCore.Connections; + +namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.Internal; + +internal sealed class DefaultMemoryPoolFactory : IMemoryPoolFactory +{ + public static DefaultMemoryPoolFactory Instance { get; } = new DefaultMemoryPoolFactory(); + + public MemoryPool CreatePool() + { + return MemoryPool.Shared; + } +} diff --git a/src/Servers/Kestrel/Transport.Sockets/src/Internal/SocketConnection.cs b/src/Servers/Kestrel/Transport.Sockets/src/Internal/SocketConnection.cs index e93c929f0a5b..e8655b08d0ff 100644 --- a/src/Servers/Kestrel/Transport.Sockets/src/Internal/SocketConnection.cs +++ b/src/Servers/Kestrel/Transport.Sockets/src/Internal/SocketConnection.cs @@ -12,7 +12,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.Internal; internal sealed partial class SocketConnection : TransportConnection { - private static readonly int MinAllocBufferSize = PinnedBlockMemoryPool.BlockSize / 2; + // PinnedBlockMemoryPool.BlockSize / 2 + private const int MinAllocBufferSize = 4096 / 2; private readonly Socket _socket; private readonly ILogger _logger; diff --git a/src/Servers/Kestrel/Transport.Sockets/src/Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.csproj b/src/Servers/Kestrel/Transport.Sockets/src/Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.csproj index 055f5f8e297e..4e1ff2339148 100644 --- a/src/Servers/Kestrel/Transport.Sockets/src/Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.csproj +++ b/src/Servers/Kestrel/Transport.Sockets/src/Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.csproj @@ -12,7 +12,6 @@ - diff --git a/src/Servers/Kestrel/Transport.Sockets/src/PublicAPI.Unshipped.txt b/src/Servers/Kestrel/Transport.Sockets/src/PublicAPI.Unshipped.txt index db4f3151423d..7dc5c58110bf 100644 --- a/src/Servers/Kestrel/Transport.Sockets/src/PublicAPI.Unshipped.txt +++ b/src/Servers/Kestrel/Transport.Sockets/src/PublicAPI.Unshipped.txt @@ -1,2 +1 @@ #nullable enable -Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.SocketTransportFactory.SocketTransportFactory(Microsoft.Extensions.Options.IOptions! options, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory, System.Diagnostics.Metrics.IMeterFactory! meterFactory) -> void diff --git a/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionContextFactory.cs b/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionContextFactory.cs index 0dbe798b0f44..ced4374e8ae6 100644 --- a/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionContextFactory.cs +++ b/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionContextFactory.cs @@ -47,7 +47,7 @@ public SocketConnectionContextFactory(SocketConnectionFactoryOptions options, IL for (var i = 0; i < _settingsCount; i++) { - var memoryPool = _options.MemoryPoolFactory(_options.MeterFactory); + var memoryPool = _options.MemoryPoolFactory.CreatePool(); var transportScheduler = options.UnsafePreferInlineScheduling ? PipeScheduler.Inline : new IOQueue(); _settings[i] = new QueueSettings() @@ -62,7 +62,7 @@ public SocketConnectionContextFactory(SocketConnectionFactoryOptions options, IL } else { - var memoryPool = _options.MemoryPoolFactory(_options.MeterFactory); + var memoryPool = _options.MemoryPoolFactory.CreatePool(); var transportScheduler = options.UnsafePreferInlineScheduling ? PipeScheduler.Inline : PipeScheduler.ThreadPool; _settings = new QueueSettings[] diff --git a/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionFactoryOptions.cs b/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionFactoryOptions.cs index e5d653484ade..e28c7a09df09 100644 --- a/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionFactoryOptions.cs +++ b/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionFactoryOptions.cs @@ -1,8 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Buffers; -using System.Diagnostics.Metrics; +using Microsoft.AspNetCore.Connections; +using Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.Internal; namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets; @@ -14,9 +14,9 @@ public class SocketConnectionFactoryOptions /// /// Create a new instance. /// - public SocketConnectionFactoryOptions() { MeterFactory = new DummyMeterFactory(); } + public SocketConnectionFactoryOptions() { } - internal SocketConnectionFactoryOptions(SocketTransportOptions transportOptions, IMeterFactory meterFactory) + internal SocketConnectionFactoryOptions(SocketTransportOptions transportOptions) { IOQueueCount = transportOptions.IOQueueCount; WaitForDataBeforeAllocatingBuffer = transportOptions.WaitForDataBeforeAllocatingBuffer; @@ -25,7 +25,6 @@ internal SocketConnectionFactoryOptions(SocketTransportOptions transportOptions, UnsafePreferInlineScheduling = transportOptions.UnsafePreferInlineScheduling; MemoryPoolFactory = transportOptions.MemoryPoolFactory; FinOnError = transportOptions.FinOnError; - MeterFactory = meterFactory; } // Opt-out flag for back compat. Remove in 9.0 (or make public). @@ -69,13 +68,5 @@ internal SocketConnectionFactoryOptions(SocketTransportOptions transportOptions, /// public bool UnsafePreferInlineScheduling { get; set; } - internal IMeterFactory MeterFactory { get; set; } - internal Func> MemoryPoolFactory { get; set; } = PinnedBlockMemoryPoolFactory.Create; - - internal sealed class DummyMeterFactory : IMeterFactory - { - public Meter Create(MeterOptions options) => new Meter(options); - - public void Dispose() { } - } + internal IMemoryPoolFactory MemoryPoolFactory { get; set; } = DefaultMemoryPoolFactory.Instance; } diff --git a/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionListener.cs b/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionListener.cs index 3f7333471e47..c8f0cf8ad543 100644 --- a/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionListener.cs +++ b/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionListener.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; -using System.Diagnostics.Metrics; using System.Net; using System.Net.Sockets; using Microsoft.AspNetCore.Connections; @@ -23,14 +22,13 @@ internal sealed class SocketConnectionListener : IConnectionListener internal SocketConnectionListener( EndPoint endpoint, SocketTransportOptions options, - ILoggerFactory loggerFactory, - IMeterFactory meterFactory) + ILoggerFactory loggerFactory) { EndPoint = endpoint; _options = options; var logger = loggerFactory.CreateLogger("Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets"); _logger = logger; - _factory = new SocketConnectionContextFactory(new SocketConnectionFactoryOptions(options, meterFactory), logger); + _factory = new SocketConnectionContextFactory(new SocketConnectionFactoryOptions(options), logger); } internal void Bind() diff --git a/src/Servers/Kestrel/Transport.Sockets/src/SocketTransportFactory.cs b/src/Servers/Kestrel/Transport.Sockets/src/SocketTransportFactory.cs index 5d8a1e9b3fa5..0f19fcd9e31d 100644 --- a/src/Servers/Kestrel/Transport.Sockets/src/SocketTransportFactory.cs +++ b/src/Servers/Kestrel/Transport.Sockets/src/SocketTransportFactory.cs @@ -17,7 +17,6 @@ public sealed class SocketTransportFactory : IConnectionListenerFactory, IConnec { private readonly SocketTransportOptions _options; private readonly ILoggerFactory _logger; - private readonly IMeterFactory _meterFactory; /// /// Initializes a new instance of the class. @@ -26,26 +25,19 @@ public sealed class SocketTransportFactory : IConnectionListenerFactory, IConnec /// The logger factory. public SocketTransportFactory( IOptions options, - ILoggerFactory loggerFactory) : this(options, loggerFactory, new DummyMeterFactory()) - { } - - public SocketTransportFactory( - IOptions options, - ILoggerFactory loggerFactory, - IMeterFactory meterFactory) + ILoggerFactory loggerFactory) { ArgumentNullException.ThrowIfNull(options); ArgumentNullException.ThrowIfNull(loggerFactory); _options = options.Value; _logger = loggerFactory; - _meterFactory = meterFactory; } /// public ValueTask BindAsync(EndPoint endpoint, CancellationToken cancellationToken = default) { - var transport = new SocketConnectionListener(endpoint, _options, _logger, _meterFactory); + var transport = new SocketConnectionListener(endpoint, _options, _logger); transport.Bind(); return new ValueTask(transport); } diff --git a/src/Servers/Kestrel/Transport.Sockets/src/SocketTransportOptions.cs b/src/Servers/Kestrel/Transport.Sockets/src/SocketTransportOptions.cs index f2df33dac704..326179b4b1ca 100644 --- a/src/Servers/Kestrel/Transport.Sockets/src/SocketTransportOptions.cs +++ b/src/Servers/Kestrel/Transport.Sockets/src/SocketTransportOptions.cs @@ -1,11 +1,10 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Buffers; -using System.Diagnostics.Metrics; using System.Net; using System.Net.Sockets; using Microsoft.AspNetCore.Connections; +using Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.Internal; namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets; @@ -167,5 +166,5 @@ public static Socket CreateDefaultBoundListenSocket(EndPoint endpoint) return listenSocket; } - internal Func> MemoryPoolFactory { get; set; } = PinnedBlockMemoryPoolFactory.Create; + internal IMemoryPoolFactory MemoryPoolFactory { get; set; } = DefaultMemoryPoolFactory.Instance; } diff --git a/src/Servers/Kestrel/Transport.Sockets/src/WebHostBuilderSocketExtensions.cs b/src/Servers/Kestrel/Transport.Sockets/src/WebHostBuilderSocketExtensions.cs index 046522e22048..a8c2f0d63180 100644 --- a/src/Servers/Kestrel/Transport.Sockets/src/WebHostBuilderSocketExtensions.cs +++ b/src/Servers/Kestrel/Transport.Sockets/src/WebHostBuilderSocketExtensions.cs @@ -3,7 +3,9 @@ using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets; +using Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.Internal; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; namespace Microsoft.AspNetCore.Hosting; @@ -26,6 +28,11 @@ public static IWebHostBuilder UseSockets(this IWebHostBuilder hostBuilder) return hostBuilder.ConfigureServices(services => { services.AddSingleton(); + services.TryAddSingleton(); + services.AddOptions().Configure((SocketTransportOptions options, IMemoryPoolFactory factory) => + { + options.MemoryPoolFactory = factory; + }); }); } diff --git a/src/Servers/Kestrel/shared/test/TestContextFactory.cs b/src/Servers/Kestrel/shared/test/TestContextFactory.cs index 692d1f1b0fdb..95231da58820 100644 --- a/src/Servers/Kestrel/shared/test/TestContextFactory.cs +++ b/src/Servers/Kestrel/shared/test/TestContextFactory.cs @@ -74,13 +74,6 @@ public static HttpConnectionContext CreateHttpConnectionContext( return context; } - internal sealed class DummyMeterFactory : IMeterFactory - { - public Meter Create(MeterOptions options) => new Meter(options); - - public void Dispose() { } - } - public static HttpMultiplexedConnectionContext CreateHttp3ConnectionContext( MultiplexedConnectionContext connectionContext = null, ServiceContext serviceContext = null, @@ -101,7 +94,7 @@ public static HttpMultiplexedConnectionContext CreateHttp3ConnectionContext( connectionContext, serviceContext ?? CreateServiceContext(new KestrelServerOptions()), connectionFeatures ?? new FeatureCollection(), - memoryPool ?? PinnedBlockMemoryPoolFactory.Create(new DummyMeterFactory()), + memoryPool ?? PinnedBlockMemoryPoolFactory.Create(), localEndPoint, remoteEndPoint, metricsContext) diff --git a/src/Servers/Kestrel/shared/test/TestServiceContext.cs b/src/Servers/Kestrel/shared/test/TestServiceContext.cs index f517ba9add6c..96161e37a6b5 100644 --- a/src/Servers/Kestrel/shared/test/TestServiceContext.cs +++ b/src/Servers/Kestrel/shared/test/TestServiceContext.cs @@ -4,6 +4,7 @@ using System.Buffers; using System.Diagnostics.Metrics; using System.IO.Pipelines; +using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; @@ -74,14 +75,19 @@ private void Initialize(ILoggerFactory loggerFactory, KestrelTrace kestrelTrace, public FakeTimeProvider FakeTimeProvider { get; set; } - public Func> MemoryPoolFactory { get; set; } = () => System.Buffers.PinnedBlockMemoryPoolFactory.Create(new DummyMeterFactory()); + public IMemoryPoolFactory MemoryPoolFactory { get; set; } = new WrappingMemoryPoolFactory(() => System.Buffers.PinnedBlockMemoryPoolFactory.Create()); public string DateHeaderValue => DateHeaderValueManager.GetDateHeaderValues().String; - internal sealed class DummyMeterFactory : IMeterFactory + internal sealed class WrappingMemoryPoolFactory : IMemoryPoolFactory { - public Meter Create(MeterOptions options) => new Meter(options); + private readonly Func> _memoryPoolFactory; - public void Dispose() { } + public WrappingMemoryPoolFactory(Func> memoryPoolFactory) + { + _memoryPoolFactory = memoryPoolFactory; + } + + public MemoryPool CreatePool() => _memoryPoolFactory(); } } diff --git a/src/Servers/Kestrel/shared/test/TransportTestHelpers/DiagnosticMemoryPoolFactory.cs b/src/Servers/Kestrel/shared/test/TransportTestHelpers/DiagnosticMemoryPoolFactory.cs index d27bf8e11635..3b010de6478f 100644 --- a/src/Servers/Kestrel/shared/test/TransportTestHelpers/DiagnosticMemoryPoolFactory.cs +++ b/src/Servers/Kestrel/shared/test/TransportTestHelpers/DiagnosticMemoryPoolFactory.cs @@ -6,10 +6,11 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Microsoft.AspNetCore.Connections; namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests; -public class DiagnosticMemoryPoolFactory +public class DiagnosticMemoryPoolFactory : IMemoryPoolFactory { private readonly bool _allowLateReturn; @@ -24,7 +25,7 @@ public DiagnosticMemoryPoolFactory(bool allowLateReturn = false, bool rentTracki _pools = new List(); } - public MemoryPool Create() + public MemoryPool CreatePool() { lock (_pools) { diff --git a/src/Servers/Kestrel/shared/test/TransportTestHelpers/TestServer.cs b/src/Servers/Kestrel/shared/test/TransportTestHelpers/TestServer.cs index 4c1813ed371a..2c520774b89a 100644 --- a/src/Servers/Kestrel/shared/test/TransportTestHelpers/TestServer.cs +++ b/src/Servers/Kestrel/shared/test/TransportTestHelpers/TestServer.cs @@ -1,26 +1,19 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; -using System.Collections.Generic; using System.Globalization; -using System.Linq; using System.Net; -using System.Reflection; -using System.Threading; -using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.AspNetCore.InternalTesting; +using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Xunit; namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests; @@ -74,7 +67,7 @@ public TestServer(RequestDelegate app, TestServiceContext context, Action { webHostBuilder @@ -85,6 +78,10 @@ public TestServer(RequestDelegate app, TestServiceContext context, Action { + if (context.MemoryPoolFactory != null) + { + services.AddSingleton(context.MemoryPoolFactory); + } services.AddSingleton(this); services.AddSingleton(context.LoggerFactory); services.AddSingleton(); diff --git a/src/Servers/Kestrel/test/FunctionalTests/Http2/ShutdownTests.cs b/src/Servers/Kestrel/test/FunctionalTests/Http2/ShutdownTests.cs index 7b2469a1b217..2a91a1a49dcd 100644 --- a/src/Servers/Kestrel/test/FunctionalTests/Http2/ShutdownTests.cs +++ b/src/Servers/Kestrel/test/FunctionalTests/Http2/ShutdownTests.cs @@ -168,7 +168,7 @@ public async Task GracefulTurnsAbortiveIfRequestsDoNotFinish() var testContext = new TestServiceContext(LoggerFactory) { - MemoryPoolFactory = () => new PinnedBlockMemoryPool() + MemoryPoolFactory = new TestServiceContext.WrappingMemoryPoolFactory(() => PinnedBlockMemoryPoolFactory.CreatePinnedBlockMemoryPool()), }; ThrowOnUngracefulShutdown = false; diff --git a/src/Servers/Kestrel/test/FunctionalTests/MaxRequestBufferSizeTests.cs b/src/Servers/Kestrel/test/FunctionalTests/MaxRequestBufferSizeTests.cs index 309c89be5a48..caca6a6fd5dd 100644 --- a/src/Servers/Kestrel/test/FunctionalTests/MaxRequestBufferSizeTests.cs +++ b/src/Servers/Kestrel/test/FunctionalTests/MaxRequestBufferSizeTests.cs @@ -1,23 +1,17 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; -using System.Buffers; -using System.Collections.Generic; -using System.IO; -using System.Linq; using System.Net; using System.Net.Sockets; using System.Text; -using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Server.Kestrel.FunctionalTests; using Microsoft.AspNetCore.InternalTesting; +using Microsoft.AspNetCore.Server.Kestrel.FunctionalTests; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -using Xunit; #if SOCKETS namespace Microsoft.AspNetCore.Server.Kestrel.Sockets.FunctionalTests; @@ -132,7 +126,7 @@ public async Task LargeUpload(long? maxRequestBufferSize, bool connectionAdapter var memoryPoolFactory = new DiagnosticMemoryPoolFactory(allowLateReturn: true); - using (var host = await StartHost(maxRequestBufferSize, data, connectionAdapter, startReadingRequestBody, clientFinishedSendingRequestBody, memoryPoolFactory.Create)) + using (var host = await StartHost(maxRequestBufferSize, data, connectionAdapter, startReadingRequestBody, clientFinishedSendingRequestBody, memoryPoolFactory)) { var port = host.GetPort(); using (var socket = CreateSocket(port)) @@ -225,7 +219,7 @@ public async Task ServerShutsDownGracefullyWhenMaxRequestBufferSizeExceeded() var memoryPoolFactory = new DiagnosticMemoryPoolFactory(allowLateReturn: true); - using (var host = await StartHost(16 * 1024, data, false, startReadingRequestBody, clientFinishedSendingRequestBody, memoryPoolFactory.Create)) + using (var host = await StartHost(16 * 1024, data, false, startReadingRequestBody, clientFinishedSendingRequestBody, memoryPoolFactory)) { var port = host.GetPort(); using (var socket = CreateSocket(port)) @@ -306,9 +300,9 @@ private async Task StartHost(long? maxRequestBufferSize, bool useConnectionAdapter, TaskCompletionSource startReadingRequestBody, TaskCompletionSource clientFinishedSendingRequestBody, - Func> memoryPoolFactory = null) + IMemoryPoolFactory memoryPoolFactory = null) { - var host = TransportSelector.GetHostBuilder(memoryPoolFactory, maxRequestBufferSize) + var host = TransportSelector.GetHostBuilder(maxRequestBufferSize) .ConfigureWebHost(webHostBuilder => { webHostBuilder @@ -341,6 +335,13 @@ private async Task StartHost(long? maxRequestBufferSize, options.Limits.MaxRequestBodySize = _dataLength; }) .UseContentRoot(Directory.GetCurrentDirectory()) + .ConfigureServices(services => + { + if (memoryPoolFactory != null) + { + services.AddSingleton(memoryPoolFactory); + } + }) .Configure(app => app.Run(async context => { await startReadingRequestBody.Task.TimeoutAfter(TimeSpan.FromSeconds(120)); diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/KestrelMetricsTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/KestrelMetricsTests.cs index 3ef612b5857b..0696040218be 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/KestrelMetricsTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/KestrelMetricsTests.cs @@ -273,7 +273,7 @@ public async Task Http1Connection_ServerShutdown_Abort() var serviceContext = new TestServiceContext(LoggerFactory, metrics: new KestrelMetrics(testMeterFactory)) { - //MemoryPoolFactory = PinnedBlockMemoryPoolFactory.CreatePinnedBlockMemoryPool, + MemoryPoolFactory = new TestServiceContext.WrappingMemoryPoolFactory(() => PinnedBlockMemoryPoolFactory.CreatePinnedBlockMemoryPool()), ShutdownTimeout = TimeSpan.Zero }; diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/TestTransport/TestServer.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/TestTransport/TestServer.cs index 0ee3a41d48ec..0a252d78387c 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/TestTransport/TestServer.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/TestTransport/TestServer.cs @@ -70,7 +70,7 @@ public TestServer(RequestDelegate app, TestServiceContext context, Action> memoryPoolFactory = null, - long? maxReadBufferSize = null) + public static IHostBuilder GetHostBuilder(long? maxReadBufferSize = null) { return new HostBuilder() .ConfigureWebHost(webHostBuilder => { - webHostBuilder - .UseSockets(options => - { - options.MemoryPoolFactory = memoryPoolFactory ?? options.MemoryPoolFactory; - options.MaxReadBufferSize = maxReadBufferSize; - }); + webHostBuilder.UseSockets(options => + { + options.MaxReadBufferSize = maxReadBufferSize; + }); }); } } diff --git a/src/Shared/Buffers.MemoryPool/MemoryPoolFactory.cs b/src/Shared/Buffers.MemoryPool/MemoryPoolFactory.cs index 0b6d2f606713..e61ba1cfe261 100644 --- a/src/Shared/Buffers.MemoryPool/MemoryPoolFactory.cs +++ b/src/Shared/Buffers.MemoryPool/MemoryPoolFactory.cs @@ -5,10 +5,13 @@ namespace System.Buffers; +#nullable enable + internal static class PinnedBlockMemoryPoolFactory { - public static MemoryPool Create(IMeterFactory meterFactory) + public static MemoryPool Create(IMeterFactory? meterFactory = null) { + meterFactory ??= NoopMeterFactory.Instance; #if DEBUG return new DiagnosticMemoryPool(CreatePinnedBlockMemoryPool(meterFactory)); #else @@ -16,8 +19,20 @@ public static MemoryPool Create(IMeterFactory meterFactory) #endif } - public static MemoryPool CreatePinnedBlockMemoryPool(IMeterFactory meterFactory) + public static MemoryPool CreatePinnedBlockMemoryPool(IMeterFactory? meterFactory = null) { + meterFactory ??= NoopMeterFactory.Instance; return new PinnedBlockMemoryPool(meterFactory); } + + private sealed class NoopMeterFactory : IMeterFactory + { + public static NoopMeterFactory Instance = new NoopMeterFactory(); + + public Meter Create(MeterOptions options) => new Meter(options); + + public void Dispose() + { + } + } } diff --git a/src/Shared/Buffers.MemoryPool/PinnedBlockMemoryPool.cs b/src/Shared/Buffers.MemoryPool/PinnedBlockMemoryPool.cs index 1bf947b1e5bd..93fb3992f2d2 100644 --- a/src/Shared/Buffers.MemoryPool/PinnedBlockMemoryPool.cs +++ b/src/Shared/Buffers.MemoryPool/PinnedBlockMemoryPool.cs @@ -41,19 +41,13 @@ internal sealed class PinnedBlockMemoryPool : MemoryPool /// private bool _isDisposed; // To detect redundant calls - private readonly long _memoryLimit = 30_000; + private readonly PinnedBlockMemoryPoolMetrics _metrics; - //private readonly PinnedBlockMemoryPoolMetrics _metrics; - - private long _evictionDelays; // Total time eviction tasks were delayed (ms) - private long _evictionDurations; // Total time spent on eviction (ms) public long _currentMemory; public long _evictedMemory; - private readonly long _lastEvictionStartTimestamp; - private readonly PeriodicTimer _timer; - private int _rentCount; - private int _returnCount; + private uint _rentCount; + private uint _returnCount; private readonly object _disposeSync = new object(); @@ -62,45 +56,14 @@ internal sealed class PinnedBlockMemoryPool : MemoryPool /// private const int AnySize = -1; - public PinnedBlockMemoryPool(IMeterFactory meterFactory) + public PinnedBlockMemoryPool() + : this(NoopMeterFactory.Instance) { - _ = meterFactory; - //_timeProvider = timeProvider; - //_metrics = new(meterFactory); - - var conserveMemory = Environment.GetEnvironmentVariable("DOTNET_GCConserveMemory") - ?? Environment.GetEnvironmentVariable("COMPlus_GCConserveMemory") - ?? "0"; - - if (!int.TryParse(conserveMemory, out var conserveSetting)) - { - conserveSetting = 0; - } - - conserveSetting = Math.Clamp(conserveSetting, 0, 9); - - // Will be a value between 1 and .1f - var conserveRatio = 1.0f - (conserveSetting / 10.0f); - // Adjust memory limit to be between 100% and 10% of original value - _memoryLimit = (long)(_memoryLimit * conserveRatio); - - _timer = new PeriodicTimer(TimeSpan.FromSeconds(10)); - _ = RunTimer(); } - private async Task RunTimer() + public PinnedBlockMemoryPool(IMeterFactory meterFactory) { - try - { - while (await _timer.WaitForNextTickAsync()) - { - PerformEviction(); - } - } - catch (Exception ex) - { - Debug.WriteLine(ex); - } + _metrics = new(meterFactory); } public override IMemoryOwner Rent(int size = AnySize) @@ -115,17 +78,25 @@ public override IMemoryOwner Rent(int size = AnySize) MemoryPoolThrowHelper.ThrowObjectDisposedException(MemoryPoolThrowHelper.ExceptionArgument.MemoryPool); } - Interlocked.Increment(ref _rentCount); + //Interlocked.Increment(ref _rentCount); + //++_rentCount; + ScalableCount(ref _rentCount); if (_blocks.TryDequeue(out var block)) { + _metrics.UpdateCurrentMemory(-block.Memory.Length); + _metrics.Rent(block.Memory.Length); Interlocked.Add(ref _currentMemory, -block.Memory.Length); // block successfully taken from the stack - return it return block; } - Interlocked.Increment(ref _rentCount); + _metrics.IncrementTotalMemory(BlockSize); + _metrics.Rent(BlockSize); + //Interlocked.Increment(ref _rentCount); + //++_rentCount; + ScalableCount(ref _rentCount); return new MemoryPoolBlock(this, BlockSize); } @@ -146,68 +117,59 @@ internal void Return(MemoryPoolBlock block) block.IsLeased = false; #endif - Interlocked.Increment(ref _returnCount); + //Interlocked.Increment(ref _returnCount); + //++_returnCount; + ScalableCount(ref _returnCount); if (!_isDisposed) { + _metrics.UpdateCurrentMemory(block.Memory.Length); Interlocked.Add(ref _currentMemory, block.Memory.Length); _blocks.Enqueue(block); } } - private void PerformEviction() + public void PerformEviction() { - var now = Stopwatch.GetTimestamp(); - - try + long evictedMemoryThisPass = 0; + var currentCount = (uint)_blocks.Count; + var burstAmount = 0u; + // If any activity + if (_rentCount + _returnCount > 0) { - // Measure delay since the eviction was triggered - - var delayMs = Stopwatch.GetElapsedTime(_lastEvictionStartTimestamp).TotalMilliseconds; - Interlocked.Add(ref _evictionDelays, (long)delayMs); - - long evictedMemoryThisPass = 0; - var currentCount = _blocks.Count; - var burstAmount = 0; - // If any activity - if (_rentCount + _returnCount > 0) + // Trending less traffic + if (_returnCount > _rentCount) { - // Trending less traffic - if (_returnCount > _rentCount) - { - burstAmount = currentCount / _returnCount - _rentCount; - } - // Traffic staying the same, try removing some blocks since we probably have excess - else if (_returnCount == _rentCount) - { - burstAmount = Math.Min(10, currentCount / 20); - } - } - // If no activity - else - { - burstAmount = Math.Max(1, currentCount / 10); + burstAmount = Math.Min(currentCount / 100, (_returnCount - _rentCount) / 5); } - _rentCount = 0; - _returnCount = 0; - - // Remove from queue and let GC clean the memory up - while (burstAmount > 0 && _blocks.TryDequeue(out var block)) + // Traffic staying the same, try removing some blocks since we probably have excess + else if (_returnCount == _rentCount) { - Interlocked.Add(ref _currentMemory, -block.Memory.Length); - Interlocked.Add(ref _evictedMemory, block.Memory.Length); - - evictedMemoryThisPass += block.Memory.Length; - burstAmount--; + burstAmount = Math.Max(1, currentCount / 100); } - - Debug.WriteLine($"Evicted {evictedMemoryThisPass} bytes."); } - finally + // If no activity + else { - Interlocked.Add(ref _evictionDurations, (long)Stopwatch.GetElapsedTime(now).TotalMilliseconds); + burstAmount = Math.Max(10, currentCount / 20); } + _rentCount = 0; + _returnCount = 0; + + // Remove from queue and let GC clean the memory up + while (burstAmount > 0 && _blocks.TryDequeue(out var block)) + { + _metrics.UpdateCurrentMemory(-block.Memory.Length); + _metrics.EvictBlock(block.Memory.Length); + Interlocked.Add(ref _currentMemory, -block.Memory.Length); + Interlocked.Add(ref _evictedMemory, block.Memory.Length); + + evictedMemoryThisPass += block.Memory.Length; + burstAmount--; + } + + Debug.WriteLine($"Evicted {evictedMemoryThisPass} bytes."); } protected override void Dispose(bool disposing) @@ -223,8 +185,6 @@ protected override void Dispose(bool disposing) if (disposing) { - _timer.Dispose(); - // Discard blocks in pool while (_blocks.TryDequeue(out _)) { @@ -233,4 +193,43 @@ protected override void Dispose(bool disposing) } } } + + private sealed class NoopMeterFactory : IMeterFactory + { + public static NoopMeterFactory Instance { get; } = new(); + + public Meter Create(MeterOptions options) => new Meter(options); + + public void Dispose() + { + } + } + + // https://github.com/dotnet/runtime/blob/db681fb307d754c3746ffb40e0634e4c4e0caa9e/docs/design/features/ScalableApproximateCounting.md + static void ScalableCount(ref uint counter) + { + // Start using random for counting after 2^12 (4096) + const int threshold = 8; + uint count = counter; + uint delta = 1; +#if true + if (count > 0) + { + int logCount = 31 - (int)uint.LeadingZeroCount(count); + + if (logCount >= threshold) + { + delta = 1u << (logCount - (threshold - 1)); + uint rand = (uint)Random.Shared.Next(); + bool update = (rand & (delta - 1)) == 0; + if (!update) + { + return; + } + } + } +#endif + + Interlocked.Add(ref counter, delta); + } } diff --git a/src/Shared/Buffers.MemoryPool/PinnedBlockMemoryPoolMetrics.cs b/src/Shared/Buffers.MemoryPool/PinnedBlockMemoryPoolMetrics.cs index c2b6c6a04713..ab9413542c1a 100644 --- a/src/Shared/Buffers.MemoryPool/PinnedBlockMemoryPoolMetrics.cs +++ b/src/Shared/Buffers.MemoryPool/PinnedBlockMemoryPoolMetrics.cs @@ -16,10 +16,8 @@ internal sealed class PinnedBlockMemoryPoolMetrics private readonly Meter _meter; private readonly UpDownCounter _currentMemory; - private readonly UpDownCounter _totalAllocatedMemory; - private readonly UpDownCounter _evictedBlocks; - private readonly UpDownCounter _evictedMemory; - private readonly UpDownCounter _evictionAttempts; + private readonly Counter _totalAllocatedMemory; + private readonly Counter _evictedMemory; private readonly Counter _usageRate; public PinnedBlockMemoryPoolMetrics(IMeterFactory meterFactory) @@ -31,26 +29,16 @@ public PinnedBlockMemoryPoolMetrics(IMeterFactory meterFactory) unit: "{bytes}", description: "Number of bytes that are currently pooled by the pinned block memory pool."); - _totalAllocatedMemory = _meter.CreateUpDownCounter( + _totalAllocatedMemory = _meter.CreateCounter( "pinnedblockmemorypool.total_allocated", unit: "{bytes}", description: "Total number of allocations made by the pinned block memory pool."); - _evictedBlocks = _meter.CreateUpDownCounter( - "pinnedblockmemorypool.evicted_blocks", - unit: "{blocks}", - description: "Total number of pooled blocks that have been evicted."); - - _evictedMemory = _meter.CreateUpDownCounter( + _evictedMemory = _meter.CreateCounter( "pinnedblockmemorypool.evicted_memory", unit: "{bytes}", description: "Total number of bytes that have been evicted."); - _evictionAttempts = _meter.CreateUpDownCounter( - "pinnedblockmemorypool.eviction_attempts", - unit: "{eviction}", - description: "Total number of eviction attempts."); - _usageRate = _meter.CreateCounter( "pinnedblockmemorypool.usage_rate", unit: "bytes", @@ -70,15 +58,9 @@ public void IncrementTotalMemory(int bytes) public void EvictBlock(int bytes) { - _evictedBlocks.Add(1); _evictedMemory.Add(bytes); } - public void StartEviction() - { - _evictionAttempts.Add(1); - } - public void Rent(int bytes) { _usageRate.Add(bytes); From c4f250adbcd5fe72bf5e0f9ff41e5189cceccf65 Mon Sep 17 00:00:00 2001 From: Brennan Date: Thu, 27 Feb 2025 15:43:07 -0800 Subject: [PATCH 04/12] cleanup --- .../Transport.Sockets/src/SocketTransportFactory.cs | 8 -------- .../src/WebHostBuilderSocketExtensions.cs | 1 + .../Kestrel/shared/test/Http3/Http3InMemory.cs | 11 +---------- src/Servers/Kestrel/shared/test/TestContextFactory.cs | 1 - src/Servers/Kestrel/shared/test/TestServiceContext.cs | 1 - .../InMemory.FunctionalTests/Http2/Http2TestBase.cs | 10 +--------- .../InMemory.FunctionalTests/KestrelMetricsTests.cs | 2 +- 7 files changed, 4 insertions(+), 30 deletions(-) diff --git a/src/Servers/Kestrel/Transport.Sockets/src/SocketTransportFactory.cs b/src/Servers/Kestrel/Transport.Sockets/src/SocketTransportFactory.cs index 0f19fcd9e31d..16c1efb78aee 100644 --- a/src/Servers/Kestrel/Transport.Sockets/src/SocketTransportFactory.cs +++ b/src/Servers/Kestrel/Transport.Sockets/src/SocketTransportFactory.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Diagnostics.Metrics; using System.Net; using System.Net.Sockets; using Microsoft.AspNetCore.Connections; @@ -53,11 +52,4 @@ public bool CanBind(EndPoint endpoint) _ => false }; } - - private sealed class DummyMeterFactory : IMeterFactory - { - public Meter Create(MeterOptions options) => new Meter(options); - - public void Dispose() { } - } } diff --git a/src/Servers/Kestrel/Transport.Sockets/src/WebHostBuilderSocketExtensions.cs b/src/Servers/Kestrel/Transport.Sockets/src/WebHostBuilderSocketExtensions.cs index a8c2f0d63180..c0c29db82541 100644 --- a/src/Servers/Kestrel/Transport.Sockets/src/WebHostBuilderSocketExtensions.cs +++ b/src/Servers/Kestrel/Transport.Sockets/src/WebHostBuilderSocketExtensions.cs @@ -28,6 +28,7 @@ public static IWebHostBuilder UseSockets(this IWebHostBuilder hostBuilder) return hostBuilder.ConfigureServices(services => { services.AddSingleton(); + services.TryAddSingleton(); services.AddOptions().Configure((SocketTransportOptions options, IMemoryPoolFactory factory) => { diff --git a/src/Servers/Kestrel/shared/test/Http3/Http3InMemory.cs b/src/Servers/Kestrel/shared/test/Http3/Http3InMemory.cs index 4d6e9ff917df..d7fa851f2b2b 100644 --- a/src/Servers/Kestrel/shared/test/Http3/Http3InMemory.cs +++ b/src/Servers/Kestrel/shared/test/Http3/Http3InMemory.cs @@ -1,11 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; using System.Buffers; using System.Collections.Concurrent; using System.Diagnostics; -using System.Diagnostics.Metrics; using System.Globalization; using System.IO.Pipelines; using System.Net.Http; @@ -71,18 +69,11 @@ public void OnTimeout(TimeoutReason reason) } } - internal sealed class DummyMeterFactory : IMeterFactory - { - public Meter Create(MeterOptions options) => new Meter(options); - - public void Dispose() { } - } - internal ServiceContext _serviceContext; private FakeTimeProvider _fakeTimeProvider; internal HttpConnection _httpConnection; internal readonly TimeoutControl _timeoutControl; - internal readonly MemoryPool _memoryPool = PinnedBlockMemoryPoolFactory.Create(new DummyMeterFactory()); + internal readonly MemoryPool _memoryPool = PinnedBlockMemoryPoolFactory.Create(); internal readonly ConcurrentQueue _streamContextPool = new ConcurrentQueue(); protected Task _connectionTask; internal ILogger Logger { get; } diff --git a/src/Servers/Kestrel/shared/test/TestContextFactory.cs b/src/Servers/Kestrel/shared/test/TestContextFactory.cs index 95231da58820..3baf69b6348a 100644 --- a/src/Servers/Kestrel/shared/test/TestContextFactory.cs +++ b/src/Servers/Kestrel/shared/test/TestContextFactory.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Buffers; -using System.Diagnostics.Metrics; using System.IO.Pipelines; using System.Net; using Microsoft.AspNetCore.Connections; diff --git a/src/Servers/Kestrel/shared/test/TestServiceContext.cs b/src/Servers/Kestrel/shared/test/TestServiceContext.cs index 96161e37a6b5..c0bbace9d6dd 100644 --- a/src/Servers/Kestrel/shared/test/TestServiceContext.cs +++ b/src/Servers/Kestrel/shared/test/TestServiceContext.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Buffers; -using System.Diagnostics.Metrics; using System.IO.Pipelines; using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Server.Kestrel.Core; diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs index 4fe1c40a800b..e4d452b91aa0 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs @@ -5,7 +5,6 @@ using System.Buffers.Binary; using System.Collections.Concurrent; using System.Diagnostics; -using System.Diagnostics.Metrics; using System.Globalization; using System.IO.Pipelines; using System.Net.Http; @@ -113,14 +112,7 @@ protected static IEnumerable> ReadRateRequestHeader protected static readonly byte[] _noData = new byte[0]; protected static readonly byte[] _maxData = Encoding.ASCII.GetBytes(new string('a', Http2PeerSettings.MinAllowedMaxFrameSize)); - internal sealed class DummyMeterFactory : IMeterFactory - { - public Meter Create(MeterOptions options) => new Meter(options); - - public void Dispose() { } - } - - private readonly MemoryPool _memoryPool = PinnedBlockMemoryPoolFactory.Create(new DummyMeterFactory()); + private readonly MemoryPool _memoryPool = PinnedBlockMemoryPoolFactory.Create(); internal readonly Http2PeerSettings _clientSettings = new Http2PeerSettings(); internal readonly HPackDecoder _hpackDecoder; diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/KestrelMetricsTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/KestrelMetricsTests.cs index 0696040218be..dd31710935c5 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/KestrelMetricsTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/KestrelMetricsTests.cs @@ -612,7 +612,7 @@ public async Task Http2Connection_ServerShutdown_Abort() var serviceContext = new TestServiceContext(LoggerFactory, metrics: new KestrelMetrics(testMeterFactory)) { ShutdownTimeout = TimeSpan.Zero, - //MemoryPoolFactory = PinnedBlockMemoryPoolFactory.CreatePinnedBlockMemoryPool + MemoryPoolFactory = new TestServiceContext.WrappingMemoryPoolFactory(() => PinnedBlockMemoryPoolFactory.CreatePinnedBlockMemoryPool()) }; var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); From d44c3aff0434f8f33793b960f3e4311278095d59 Mon Sep 17 00:00:00 2001 From: Brennan Date: Fri, 28 Feb 2025 16:17:44 -0800 Subject: [PATCH 05/12] rebase --- .../Core/src/Microsoft.AspNetCore.Server.Kestrel.Core.csproj | 1 - .../Kestrel/Core/test/Http1/Http1ConnectionTestsBase.cs | 2 +- .../Microsoft.AspNetCore.Server.Kestrel.Core.Tests.csproj | 1 - src/Servers/Kestrel/Core/test/TestHelpers/TestInput.cs | 2 +- .../Kestrel/perf/Microbenchmarks/Http1ConnectionBenchmark.cs | 2 +- .../Http1ConnectionParsingOverheadBenchmark.cs | 2 +- .../perf/Microbenchmarks/Http1LargeWritingBenchmark.cs | 2 +- .../Kestrel/perf/Microbenchmarks/Http1ReadingBenchmark.cs | 2 +- .../Kestrel/perf/Microbenchmarks/Http1WritingBenchmark.cs | 2 +- ...Microsoft.AspNetCore.Server.Kestrel.Microbenchmarks.csproj | 1 - .../Kestrel/perf/Microbenchmarks/RequestParsingBenchmark.cs | 2 +- .../perf/Microbenchmarks/ResponseHeaderCollectionBenchmark.cs | 2 +- src/Servers/Kestrel/shared/test/Http3/Http3InMemory.cs | 2 +- src/Servers/Kestrel/shared/test/TestContextFactory.cs | 2 +- .../test/InMemory.FunctionalTests/Http2/Http2TestBase.cs | 2 +- .../InMemory.FunctionalTests/InMemory.FunctionalTests.csproj | 1 - .../test/InMemory.FunctionalTests/KestrelMetricsTests.cs | 4 ++-- .../Interop.FunctionalTests/Interop.FunctionalTests.csproj | 1 + 18 files changed, 15 insertions(+), 18 deletions(-) diff --git a/src/Servers/Kestrel/Core/src/Microsoft.AspNetCore.Server.Kestrel.Core.csproj b/src/Servers/Kestrel/Core/src/Microsoft.AspNetCore.Server.Kestrel.Core.csproj index 0a8384a65166..c35c22dd8d94 100644 --- a/src/Servers/Kestrel/Core/src/Microsoft.AspNetCore.Server.Kestrel.Core.csproj +++ b/src/Servers/Kestrel/Core/src/Microsoft.AspNetCore.Server.Kestrel.Core.csproj @@ -71,7 +71,6 @@ - diff --git a/src/Servers/Kestrel/Core/test/Http1/Http1ConnectionTestsBase.cs b/src/Servers/Kestrel/Core/test/Http1/Http1ConnectionTestsBase.cs index 3fd5631ed42c..ae967abbfddc 100644 --- a/src/Servers/Kestrel/Core/test/Http1/Http1ConnectionTestsBase.cs +++ b/src/Servers/Kestrel/Core/test/Http1/Http1ConnectionTestsBase.cs @@ -31,7 +31,7 @@ protected override void Initialize(TestContext context, MethodInfo methodInfo, o { base.Initialize(context, methodInfo, testMethodArguments, testOutputHelper); - _pipelineFactory = PinnedBlockMemoryPoolFactory.Create(); + _pipelineFactory = System.Buffers.PinnedBlockMemoryPoolFactory.Create(); var options = new PipeOptions(_pipelineFactory, readerScheduler: PipeScheduler.Inline, writerScheduler: PipeScheduler.Inline, useSynchronizationContext: false); var pair = DuplexPipe.CreateConnectionPair(options, options); diff --git a/src/Servers/Kestrel/Core/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests.csproj b/src/Servers/Kestrel/Core/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests.csproj index 509856074d7c..a25b8efed754 100644 --- a/src/Servers/Kestrel/Core/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests.csproj +++ b/src/Servers/Kestrel/Core/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests.csproj @@ -14,7 +14,6 @@ - diff --git a/src/Servers/Kestrel/Core/test/TestHelpers/TestInput.cs b/src/Servers/Kestrel/Core/test/TestHelpers/TestInput.cs index 914fa9bb5f58..f79218d1ec21 100644 --- a/src/Servers/Kestrel/Core/test/TestHelpers/TestInput.cs +++ b/src/Servers/Kestrel/Core/test/TestHelpers/TestInput.cs @@ -24,7 +24,7 @@ class TestInput : IDisposable public TestInput(KestrelTrace log = null, ITimeoutControl timeoutControl = null) { - _memoryPool = PinnedBlockMemoryPoolFactory.Create(); + _memoryPool = System.Buffers.PinnedBlockMemoryPoolFactory.Create(); var options = new PipeOptions(pool: _memoryPool, readerScheduler: PipeScheduler.Inline, writerScheduler: PipeScheduler.Inline, useSynchronizationContext: false); var pair = DuplexPipe.CreateConnectionPair(options, options); Transport = pair.Transport; diff --git a/src/Servers/Kestrel/perf/Microbenchmarks/Http1ConnectionBenchmark.cs b/src/Servers/Kestrel/perf/Microbenchmarks/Http1ConnectionBenchmark.cs index 16697ca82441..b2bf9a4440d7 100644 --- a/src/Servers/Kestrel/perf/Microbenchmarks/Http1ConnectionBenchmark.cs +++ b/src/Servers/Kestrel/perf/Microbenchmarks/Http1ConnectionBenchmark.cs @@ -27,7 +27,7 @@ public class Http1ConnectionBenchmark [GlobalSetup] public void Setup() { - var memoryPool = PinnedBlockMemoryPoolFactory.Create(); + var memoryPool = System.Buffers.PinnedBlockMemoryPoolFactory.Create(); var options = new PipeOptions(memoryPool, readerScheduler: PipeScheduler.Inline, writerScheduler: PipeScheduler.Inline, useSynchronizationContext: false); var pair = DuplexPipe.CreateConnectionPair(options, options); diff --git a/src/Servers/Kestrel/perf/Microbenchmarks/Http1ConnectionParsingOverheadBenchmark.cs b/src/Servers/Kestrel/perf/Microbenchmarks/Http1ConnectionParsingOverheadBenchmark.cs index 356953b8cafa..e1e2061ea0fd 100644 --- a/src/Servers/Kestrel/perf/Microbenchmarks/Http1ConnectionParsingOverheadBenchmark.cs +++ b/src/Servers/Kestrel/perf/Microbenchmarks/Http1ConnectionParsingOverheadBenchmark.cs @@ -23,7 +23,7 @@ public class Http1ConnectionParsingOverheadBenchmark [IterationSetup] public void Setup() { - var memoryPool = PinnedBlockMemoryPoolFactory.Create(); + var memoryPool = System.Buffers.PinnedBlockMemoryPoolFactory.Create(); var options = new PipeOptions(memoryPool, readerScheduler: PipeScheduler.Inline, writerScheduler: PipeScheduler.Inline, useSynchronizationContext: false); var pair = DuplexPipe.CreateConnectionPair(options, options); diff --git a/src/Servers/Kestrel/perf/Microbenchmarks/Http1LargeWritingBenchmark.cs b/src/Servers/Kestrel/perf/Microbenchmarks/Http1LargeWritingBenchmark.cs index fa8d0dddf4e8..192e09bd7ff3 100644 --- a/src/Servers/Kestrel/perf/Microbenchmarks/Http1LargeWritingBenchmark.cs +++ b/src/Servers/Kestrel/perf/Microbenchmarks/Http1LargeWritingBenchmark.cs @@ -28,7 +28,7 @@ public class Http1LargeWritingBenchmark [GlobalSetup] public void GlobalSetup() { - _memoryPool = PinnedBlockMemoryPoolFactory.Create(); + _memoryPool = System.Buffers.PinnedBlockMemoryPoolFactory.Create(); _http1Connection = MakeHttp1Connection(); _consumeResponseBodyTask = ConsumeResponseBody(); } diff --git a/src/Servers/Kestrel/perf/Microbenchmarks/Http1ReadingBenchmark.cs b/src/Servers/Kestrel/perf/Microbenchmarks/Http1ReadingBenchmark.cs index a5c18cbadf4d..587e0dc056c5 100644 --- a/src/Servers/Kestrel/perf/Microbenchmarks/Http1ReadingBenchmark.cs +++ b/src/Servers/Kestrel/perf/Microbenchmarks/Http1ReadingBenchmark.cs @@ -35,7 +35,7 @@ public class Http1ReadingBenchmark [GlobalSetup] public void GlobalSetup() { - _memoryPool = PinnedBlockMemoryPoolFactory.Create(); + _memoryPool = System.Buffers.PinnedBlockMemoryPoolFactory.Create(); _http1Connection = MakeHttp1Connection(); } diff --git a/src/Servers/Kestrel/perf/Microbenchmarks/Http1WritingBenchmark.cs b/src/Servers/Kestrel/perf/Microbenchmarks/Http1WritingBenchmark.cs index 31db6447384b..122f8ac0f4eb 100644 --- a/src/Servers/Kestrel/perf/Microbenchmarks/Http1WritingBenchmark.cs +++ b/src/Servers/Kestrel/perf/Microbenchmarks/Http1WritingBenchmark.cs @@ -35,7 +35,7 @@ public class Http1WritingBenchmark [GlobalSetup] public void GlobalSetup() { - _memoryPool = PinnedBlockMemoryPoolFactory.Create(); + _memoryPool = System.Buffers.PinnedBlockMemoryPoolFactory.Create(); _http1Connection = MakeHttp1Connection(); _consumeResponseBodyTask = ConsumeResponseBody(); } diff --git a/src/Servers/Kestrel/perf/Microbenchmarks/Microsoft.AspNetCore.Server.Kestrel.Microbenchmarks.csproj b/src/Servers/Kestrel/perf/Microbenchmarks/Microsoft.AspNetCore.Server.Kestrel.Microbenchmarks.csproj index 528290dd4396..0d4fff0fdc0f 100644 --- a/src/Servers/Kestrel/perf/Microbenchmarks/Microsoft.AspNetCore.Server.Kestrel.Microbenchmarks.csproj +++ b/src/Servers/Kestrel/perf/Microbenchmarks/Microsoft.AspNetCore.Server.Kestrel.Microbenchmarks.csproj @@ -18,7 +18,6 @@ - diff --git a/src/Servers/Kestrel/perf/Microbenchmarks/RequestParsingBenchmark.cs b/src/Servers/Kestrel/perf/Microbenchmarks/RequestParsingBenchmark.cs index 819ca5441d3b..cb3e893f40ed 100644 --- a/src/Servers/Kestrel/perf/Microbenchmarks/RequestParsingBenchmark.cs +++ b/src/Servers/Kestrel/perf/Microbenchmarks/RequestParsingBenchmark.cs @@ -24,7 +24,7 @@ public class RequestParsingBenchmark [IterationSetup] public void Setup() { - _memoryPool = PinnedBlockMemoryPoolFactory.Create(); + _memoryPool = System.Buffers.PinnedBlockMemoryPoolFactory.Create(); var options = new PipeOptions(_memoryPool, readerScheduler: PipeScheduler.Inline, writerScheduler: PipeScheduler.Inline, useSynchronizationContext: false); var pair = DuplexPipe.CreateConnectionPair(options, options); diff --git a/src/Servers/Kestrel/perf/Microbenchmarks/ResponseHeaderCollectionBenchmark.cs b/src/Servers/Kestrel/perf/Microbenchmarks/ResponseHeaderCollectionBenchmark.cs index 983f39f28eed..3bd558e5f797 100644 --- a/src/Servers/Kestrel/perf/Microbenchmarks/ResponseHeaderCollectionBenchmark.cs +++ b/src/Servers/Kestrel/perf/Microbenchmarks/ResponseHeaderCollectionBenchmark.cs @@ -172,7 +172,7 @@ private void Unknown(int count) [IterationSetup] public void Setup() { - var memoryPool = PinnedBlockMemoryPoolFactory.Create(); + var memoryPool = System.Buffers.PinnedBlockMemoryPoolFactory.Create(); var options = new PipeOptions(memoryPool, readerScheduler: PipeScheduler.Inline, writerScheduler: PipeScheduler.Inline, useSynchronizationContext: false); var pair = DuplexPipe.CreateConnectionPair(options, options); diff --git a/src/Servers/Kestrel/shared/test/Http3/Http3InMemory.cs b/src/Servers/Kestrel/shared/test/Http3/Http3InMemory.cs index d7fa851f2b2b..e0db616b73d9 100644 --- a/src/Servers/Kestrel/shared/test/Http3/Http3InMemory.cs +++ b/src/Servers/Kestrel/shared/test/Http3/Http3InMemory.cs @@ -73,7 +73,7 @@ public void OnTimeout(TimeoutReason reason) private FakeTimeProvider _fakeTimeProvider; internal HttpConnection _httpConnection; internal readonly TimeoutControl _timeoutControl; - internal readonly MemoryPool _memoryPool = PinnedBlockMemoryPoolFactory.Create(); + internal readonly MemoryPool _memoryPool = System.Buffers.PinnedBlockMemoryPoolFactory.Create(); internal readonly ConcurrentQueue _streamContextPool = new ConcurrentQueue(); protected Task _connectionTask; internal ILogger Logger { get; } diff --git a/src/Servers/Kestrel/shared/test/TestContextFactory.cs b/src/Servers/Kestrel/shared/test/TestContextFactory.cs index 3baf69b6348a..5fa2f1c65a12 100644 --- a/src/Servers/Kestrel/shared/test/TestContextFactory.cs +++ b/src/Servers/Kestrel/shared/test/TestContextFactory.cs @@ -93,7 +93,7 @@ public static HttpMultiplexedConnectionContext CreateHttp3ConnectionContext( connectionContext, serviceContext ?? CreateServiceContext(new KestrelServerOptions()), connectionFeatures ?? new FeatureCollection(), - memoryPool ?? PinnedBlockMemoryPoolFactory.Create(), + memoryPool ?? System.Buffers.PinnedBlockMemoryPoolFactory.Create(), localEndPoint, remoteEndPoint, metricsContext) diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs index e4d452b91aa0..bf66a8bbd5f0 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs @@ -112,7 +112,7 @@ protected static IEnumerable> ReadRateRequestHeader protected static readonly byte[] _noData = new byte[0]; protected static readonly byte[] _maxData = Encoding.ASCII.GetBytes(new string('a', Http2PeerSettings.MinAllowedMaxFrameSize)); - private readonly MemoryPool _memoryPool = PinnedBlockMemoryPoolFactory.Create(); + private readonly MemoryPool _memoryPool = System.Buffers.PinnedBlockMemoryPoolFactory.Create(); internal readonly Http2PeerSettings _clientSettings = new Http2PeerSettings(); internal readonly HPackDecoder _hpackDecoder; diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/InMemory.FunctionalTests.csproj b/src/Servers/Kestrel/test/InMemory.FunctionalTests/InMemory.FunctionalTests.csproj index 3fd95d1cc652..000dd65ca229 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/InMemory.FunctionalTests.csproj +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/InMemory.FunctionalTests.csproj @@ -17,7 +17,6 @@ - diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/KestrelMetricsTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/KestrelMetricsTests.cs index dd31710935c5..f24b730db08a 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/KestrelMetricsTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/KestrelMetricsTests.cs @@ -273,7 +273,7 @@ public async Task Http1Connection_ServerShutdown_Abort() var serviceContext = new TestServiceContext(LoggerFactory, metrics: new KestrelMetrics(testMeterFactory)) { - MemoryPoolFactory = new TestServiceContext.WrappingMemoryPoolFactory(() => PinnedBlockMemoryPoolFactory.CreatePinnedBlockMemoryPool()), + MemoryPoolFactory = new TestServiceContext.WrappingMemoryPoolFactory(() => System.Buffers.PinnedBlockMemoryPoolFactory.CreatePinnedBlockMemoryPool()), ShutdownTimeout = TimeSpan.Zero }; @@ -612,7 +612,7 @@ public async Task Http2Connection_ServerShutdown_Abort() var serviceContext = new TestServiceContext(LoggerFactory, metrics: new KestrelMetrics(testMeterFactory)) { ShutdownTimeout = TimeSpan.Zero, - MemoryPoolFactory = new TestServiceContext.WrappingMemoryPoolFactory(() => PinnedBlockMemoryPoolFactory.CreatePinnedBlockMemoryPool()) + MemoryPoolFactory = new TestServiceContext.WrappingMemoryPoolFactory(() => System.Buffers.PinnedBlockMemoryPoolFactory.CreatePinnedBlockMemoryPool()) }; var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); diff --git a/src/Servers/Kestrel/test/Interop.FunctionalTests/Interop.FunctionalTests.csproj b/src/Servers/Kestrel/test/Interop.FunctionalTests/Interop.FunctionalTests.csproj index 7d5156c84f9d..e44eb2d00fca 100644 --- a/src/Servers/Kestrel/test/Interop.FunctionalTests/Interop.FunctionalTests.csproj +++ b/src/Servers/Kestrel/test/Interop.FunctionalTests/Interop.FunctionalTests.csproj @@ -27,6 +27,7 @@ + From 64cbbee451acdf6f16a97bf8b78d46b5ee0dc735 Mon Sep 17 00:00:00 2001 From: Brennan Date: Tue, 18 Mar 2025 14:54:32 -0700 Subject: [PATCH 06/12] wip --- .../Kestrel/Kestrel/src/WebHostBuilderKestrelExtensions.cs | 4 ++-- .../Transport.Sockets/src/WebHostBuilderSocketExtensions.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Servers/Kestrel/Kestrel/src/WebHostBuilderKestrelExtensions.cs b/src/Servers/Kestrel/Kestrel/src/WebHostBuilderKestrelExtensions.cs index 894ce210dca2..0ac94ac6f8f2 100644 --- a/src/Servers/Kestrel/Kestrel/src/WebHostBuilderKestrelExtensions.cs +++ b/src/Servers/Kestrel/Kestrel/src/WebHostBuilderKestrelExtensions.cs @@ -7,7 +7,6 @@ using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; -using Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; @@ -79,10 +78,11 @@ public static IWebHostBuilder UseKestrel(this IWebHostBuilder hostBuilder) /// public static IWebHostBuilder UseKestrelCore(this IWebHostBuilder hostBuilder) { + hostBuilder.UseSockets(); hostBuilder.ConfigureServices(services => { // Don't override an already-configured transport - services.TryAddSingleton(); + //services.TryAddSingleton(); services.AddTransient, KestrelServerOptionsSetup>(); services.AddSingleton(); diff --git a/src/Servers/Kestrel/Transport.Sockets/src/WebHostBuilderSocketExtensions.cs b/src/Servers/Kestrel/Transport.Sockets/src/WebHostBuilderSocketExtensions.cs index c0c29db82541..17bd17b2eac3 100644 --- a/src/Servers/Kestrel/Transport.Sockets/src/WebHostBuilderSocketExtensions.cs +++ b/src/Servers/Kestrel/Transport.Sockets/src/WebHostBuilderSocketExtensions.cs @@ -27,7 +27,7 @@ public static IWebHostBuilder UseSockets(this IWebHostBuilder hostBuilder) { return hostBuilder.ConfigureServices(services => { - services.AddSingleton(); + services.TryAddSingleton(); services.TryAddSingleton(); services.AddOptions().Configure((SocketTransportOptions options, IMemoryPoolFactory factory) => From f17e3eb1cda620fb59a19bf40ec0dfd02170fd71 Mon Sep 17 00:00:00 2001 From: Brennan Date: Thu, 20 Mar 2025 12:18:12 -0700 Subject: [PATCH 07/12] namedpipe --- .../src/Internal/NamedPipeConnectionListener.cs | 2 +- ...AspNetCore.Server.Kestrel.Transport.NamedPipes.csproj | 1 + .../src/NamedPipeTransportOptions.cs | 5 +++-- .../src/WebHostBuilderNamedPipeExtensions.cs | 9 +++++++++ ...ft.AspNetCore.Server.Kestrel.Transport.Sockets.csproj | 1 + .../src/SocketConnectionFactoryOptions.cs | 2 +- .../Transport.Sockets/src/SocketTransportOptions.cs | 2 +- .../src/WebHostBuilderSocketExtensions.cs | 3 ++- .../src/Internal => shared}/DefaultMemoryPoolFactory.cs | 2 +- 9 files changed, 20 insertions(+), 7 deletions(-) rename src/Servers/Kestrel/{Transport.Sockets/src/Internal => shared}/DefaultMemoryPoolFactory.cs (86%) diff --git a/src/Servers/Kestrel/Transport.NamedPipes/src/Internal/NamedPipeConnectionListener.cs b/src/Servers/Kestrel/Transport.NamedPipes/src/Internal/NamedPipeConnectionListener.cs index ab172bc28476..8a4edca25a57 100644 --- a/src/Servers/Kestrel/Transport.NamedPipes/src/Internal/NamedPipeConnectionListener.cs +++ b/src/Servers/Kestrel/Transport.NamedPipes/src/Internal/NamedPipeConnectionListener.cs @@ -40,7 +40,7 @@ public NamedPipeConnectionListener( _log = loggerFactory.CreateLogger("Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes"); _endpoint = endpoint; _options = options; - _memoryPool = options.MemoryPoolFactory(); + _memoryPool = options.MemoryPoolFactory.CreatePool(); _listeningToken = _listeningTokenSource.Token; // Have to create the pool here (instead of DI) because the pool is specific to an endpoint. _poolPolicy = new NamedPipeServerStreamPoolPolicy(endpoint, options); diff --git a/src/Servers/Kestrel/Transport.NamedPipes/src/Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.csproj b/src/Servers/Kestrel/Transport.NamedPipes/src/Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.csproj index e7b0816ec136..5eb6ba09868e 100644 --- a/src/Servers/Kestrel/Transport.NamedPipes/src/Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.csproj +++ b/src/Servers/Kestrel/Transport.NamedPipes/src/Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.csproj @@ -21,6 +21,7 @@ + diff --git a/src/Servers/Kestrel/Transport.NamedPipes/src/NamedPipeTransportOptions.cs b/src/Servers/Kestrel/Transport.NamedPipes/src/NamedPipeTransportOptions.cs index 34b57c2fe3c2..542553bcc50f 100644 --- a/src/Servers/Kestrel/Transport.NamedPipes/src/NamedPipeTransportOptions.cs +++ b/src/Servers/Kestrel/Transport.NamedPipes/src/NamedPipeTransportOptions.cs @@ -1,8 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Buffers; using System.IO.Pipes; +using Microsoft.AspNetCore.Connections; +using Microsoft.AspNetCore.Server.Kestrel.Internal; namespace Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes; @@ -116,5 +117,5 @@ public static NamedPipeServerStream CreateDefaultNamedPipeServerStream(CreateNam } } - internal Func> MemoryPoolFactory { get; set; } = () => PinnedBlockMemoryPoolFactory.Create(); + internal IMemoryPoolFactory MemoryPoolFactory { get; set; } = DefaultMemoryPoolFactory.Instance; } diff --git a/src/Servers/Kestrel/Transport.NamedPipes/src/WebHostBuilderNamedPipeExtensions.cs b/src/Servers/Kestrel/Transport.NamedPipes/src/WebHostBuilderNamedPipeExtensions.cs index 53f94f801e74..9d9962fb6479 100644 --- a/src/Servers/Kestrel/Transport.NamedPipes/src/WebHostBuilderNamedPipeExtensions.cs +++ b/src/Servers/Kestrel/Transport.NamedPipes/src/WebHostBuilderNamedPipeExtensions.cs @@ -3,6 +3,7 @@ using System.Runtime.Versioning; using Microsoft.AspNetCore.Connections; +using Microsoft.AspNetCore.Server.Kestrel.Internal; using Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes; using Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.Internal; using Microsoft.Extensions.DependencyInjection; @@ -33,7 +34,15 @@ public static IWebHostBuilder UseNamedPipes(this IWebHostBuilder hostBuilder) { services.TryAddSingleton(); services.AddSingleton(); + + services.TryAddSingleton(); + services.AddOptions().Configure((NamedPipeTransportOptions options, IMemoryPoolFactory factory) => + { + // Set the IMemoryPoolFactory from DI on NamedPipeTransportOptions. Usually this should be the PinnedBlockMemoryPoolFactory from UseKestrelCore. + options.MemoryPoolFactory = factory; + }); }); + return hostBuilder; } diff --git a/src/Servers/Kestrel/Transport.Sockets/src/Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.csproj b/src/Servers/Kestrel/Transport.Sockets/src/Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.csproj index 4e1ff2339148..3977a94713c2 100644 --- a/src/Servers/Kestrel/Transport.Sockets/src/Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.csproj +++ b/src/Servers/Kestrel/Transport.Sockets/src/Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.csproj @@ -17,6 +17,7 @@ + diff --git a/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionFactoryOptions.cs b/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionFactoryOptions.cs index e28c7a09df09..9858095e8f60 100644 --- a/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionFactoryOptions.cs +++ b/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionFactoryOptions.cs @@ -2,7 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.AspNetCore.Connections; -using Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.Internal; +using Microsoft.AspNetCore.Server.Kestrel.Internal; namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets; diff --git a/src/Servers/Kestrel/Transport.Sockets/src/SocketTransportOptions.cs b/src/Servers/Kestrel/Transport.Sockets/src/SocketTransportOptions.cs index 326179b4b1ca..fa22127e2a1a 100644 --- a/src/Servers/Kestrel/Transport.Sockets/src/SocketTransportOptions.cs +++ b/src/Servers/Kestrel/Transport.Sockets/src/SocketTransportOptions.cs @@ -4,7 +4,7 @@ using System.Net; using System.Net.Sockets; using Microsoft.AspNetCore.Connections; -using Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.Internal; +using Microsoft.AspNetCore.Server.Kestrel.Internal; namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets; diff --git a/src/Servers/Kestrel/Transport.Sockets/src/WebHostBuilderSocketExtensions.cs b/src/Servers/Kestrel/Transport.Sockets/src/WebHostBuilderSocketExtensions.cs index 17bd17b2eac3..759e184d7570 100644 --- a/src/Servers/Kestrel/Transport.Sockets/src/WebHostBuilderSocketExtensions.cs +++ b/src/Servers/Kestrel/Transport.Sockets/src/WebHostBuilderSocketExtensions.cs @@ -2,8 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.AspNetCore.Connections; +using Microsoft.AspNetCore.Server.Kestrel.Internal; using Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets; -using Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.Internal; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -32,6 +32,7 @@ public static IWebHostBuilder UseSockets(this IWebHostBuilder hostBuilder) services.TryAddSingleton(); services.AddOptions().Configure((SocketTransportOptions options, IMemoryPoolFactory factory) => { + // Set the IMemoryPoolFactory from DI on SocketTransportOptions. Usually this should be the PinnedBlockMemoryPoolFactory from UseKestrelCore. options.MemoryPoolFactory = factory; }); }); diff --git a/src/Servers/Kestrel/Transport.Sockets/src/Internal/DefaultMemoryPoolFactory.cs b/src/Servers/Kestrel/shared/DefaultMemoryPoolFactory.cs similarity index 86% rename from src/Servers/Kestrel/Transport.Sockets/src/Internal/DefaultMemoryPoolFactory.cs rename to src/Servers/Kestrel/shared/DefaultMemoryPoolFactory.cs index 6a5883617420..61e8c23742a0 100644 --- a/src/Servers/Kestrel/Transport.Sockets/src/Internal/DefaultMemoryPoolFactory.cs +++ b/src/Servers/Kestrel/shared/DefaultMemoryPoolFactory.cs @@ -4,7 +4,7 @@ using System.Buffers; using Microsoft.AspNetCore.Connections; -namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.Internal; +namespace Microsoft.AspNetCore.Server.Kestrel.Internal; internal sealed class DefaultMemoryPoolFactory : IMemoryPoolFactory { From a1c262c00be5bd1a940aac2e17cbeaa83e2b842d Mon Sep 17 00:00:00 2001 From: Brennan Date: Fri, 21 Mar 2025 13:58:51 -0700 Subject: [PATCH 08/12] api review --- .../Connections.Abstractions/src/IMemoryPoolFactory.cs | 4 ++-- .../src/PublicAPI/net10.0/PublicAPI.Unshipped.txt | 4 ++-- .../src/PublicAPI/net462/PublicAPI.Unshipped.txt | 4 ++-- .../src/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt | 4 ++-- .../src/PublicAPI/netstandard2.1/PublicAPI.Unshipped.txt | 4 ++-- .../Core/src/Internal/PinnedBlockMemoryPoolFactory.cs | 4 ++-- .../Kestrel/Kestrel/src/WebHostBuilderKestrelExtensions.cs | 2 +- .../src/Internal/NamedPipeConnectionListener.cs | 2 +- .../Transport.NamedPipes/src/NamedPipeTransportOptions.cs | 2 +- .../src/WebHostBuilderNamedPipeExtensions.cs | 4 ++-- .../Transport.Sockets/src/Client/SocketConnectionFactory.cs | 2 +- .../Transport.Sockets/src/SocketConnectionContextFactory.cs | 4 ++-- .../Transport.Sockets/src/SocketConnectionFactoryOptions.cs | 2 +- .../Kestrel/Transport.Sockets/src/SocketTransportOptions.cs | 2 +- .../Transport.Sockets/src/WebHostBuilderSocketExtensions.cs | 4 ++-- src/Servers/Kestrel/shared/DefaultMemoryPoolFactory.cs | 4 ++-- src/Servers/Kestrel/shared/test/TestServiceContext.cs | 6 +++--- .../TransportTestHelpers/DiagnosticMemoryPoolFactory.cs | 4 ++-- .../Kestrel/shared/test/TransportTestHelpers/TestServer.cs | 2 +- .../test/FunctionalTests/MaxRequestBufferSizeTests.cs | 4 ++-- .../InMemory.FunctionalTests/TestTransport/TestServer.cs | 2 +- 21 files changed, 35 insertions(+), 35 deletions(-) diff --git a/src/Servers/Connections.Abstractions/src/IMemoryPoolFactory.cs b/src/Servers/Connections.Abstractions/src/IMemoryPoolFactory.cs index 4ae7cc4257e6..e2e2d09806e3 100644 --- a/src/Servers/Connections.Abstractions/src/IMemoryPoolFactory.cs +++ b/src/Servers/Connections.Abstractions/src/IMemoryPoolFactory.cs @@ -8,11 +8,11 @@ namespace Microsoft.AspNetCore.Connections; /// /// /// -public interface IMemoryPoolFactory +public interface IMemoryPoolFactory { /// /// /// /// - MemoryPool CreatePool(); + MemoryPool Create(); } diff --git a/src/Servers/Connections.Abstractions/src/PublicAPI/net10.0/PublicAPI.Unshipped.txt b/src/Servers/Connections.Abstractions/src/PublicAPI/net10.0/PublicAPI.Unshipped.txt index 18a4e0cf0893..dec7f8f71c13 100644 --- a/src/Servers/Connections.Abstractions/src/PublicAPI/net10.0/PublicAPI.Unshipped.txt +++ b/src/Servers/Connections.Abstractions/src/PublicAPI/net10.0/PublicAPI.Unshipped.txt @@ -1,3 +1,3 @@ #nullable enable -Microsoft.AspNetCore.Connections.IMemoryPoolFactory -Microsoft.AspNetCore.Connections.IMemoryPoolFactory.CreatePool() -> System.Buffers.MemoryPool! +Microsoft.AspNetCore.Connections.IMemoryPoolFactory +Microsoft.AspNetCore.Connections.IMemoryPoolFactory.Create() -> System.Buffers.MemoryPool! diff --git a/src/Servers/Connections.Abstractions/src/PublicAPI/net462/PublicAPI.Unshipped.txt b/src/Servers/Connections.Abstractions/src/PublicAPI/net462/PublicAPI.Unshipped.txt index 18a4e0cf0893..dec7f8f71c13 100644 --- a/src/Servers/Connections.Abstractions/src/PublicAPI/net462/PublicAPI.Unshipped.txt +++ b/src/Servers/Connections.Abstractions/src/PublicAPI/net462/PublicAPI.Unshipped.txt @@ -1,3 +1,3 @@ #nullable enable -Microsoft.AspNetCore.Connections.IMemoryPoolFactory -Microsoft.AspNetCore.Connections.IMemoryPoolFactory.CreatePool() -> System.Buffers.MemoryPool! +Microsoft.AspNetCore.Connections.IMemoryPoolFactory +Microsoft.AspNetCore.Connections.IMemoryPoolFactory.Create() -> System.Buffers.MemoryPool! diff --git a/src/Servers/Connections.Abstractions/src/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt b/src/Servers/Connections.Abstractions/src/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt index 18a4e0cf0893..dec7f8f71c13 100644 --- a/src/Servers/Connections.Abstractions/src/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt +++ b/src/Servers/Connections.Abstractions/src/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt @@ -1,3 +1,3 @@ #nullable enable -Microsoft.AspNetCore.Connections.IMemoryPoolFactory -Microsoft.AspNetCore.Connections.IMemoryPoolFactory.CreatePool() -> System.Buffers.MemoryPool! +Microsoft.AspNetCore.Connections.IMemoryPoolFactory +Microsoft.AspNetCore.Connections.IMemoryPoolFactory.Create() -> System.Buffers.MemoryPool! diff --git a/src/Servers/Connections.Abstractions/src/PublicAPI/netstandard2.1/PublicAPI.Unshipped.txt b/src/Servers/Connections.Abstractions/src/PublicAPI/netstandard2.1/PublicAPI.Unshipped.txt index 18a4e0cf0893..dec7f8f71c13 100644 --- a/src/Servers/Connections.Abstractions/src/PublicAPI/netstandard2.1/PublicAPI.Unshipped.txt +++ b/src/Servers/Connections.Abstractions/src/PublicAPI/netstandard2.1/PublicAPI.Unshipped.txt @@ -1,3 +1,3 @@ #nullable enable -Microsoft.AspNetCore.Connections.IMemoryPoolFactory -Microsoft.AspNetCore.Connections.IMemoryPoolFactory.CreatePool() -> System.Buffers.MemoryPool! +Microsoft.AspNetCore.Connections.IMemoryPoolFactory +Microsoft.AspNetCore.Connections.IMemoryPoolFactory.Create() -> System.Buffers.MemoryPool! diff --git a/src/Servers/Kestrel/Core/src/Internal/PinnedBlockMemoryPoolFactory.cs b/src/Servers/Kestrel/Core/src/Internal/PinnedBlockMemoryPoolFactory.cs index 508dd3c1cf6e..60cf5f38c5ea 100644 --- a/src/Servers/Kestrel/Core/src/Internal/PinnedBlockMemoryPoolFactory.cs +++ b/src/Servers/Kestrel/Core/src/Internal/PinnedBlockMemoryPoolFactory.cs @@ -9,7 +9,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal; -internal sealed class PinnedBlockMemoryPoolFactory : IMemoryPoolFactory, IHeartbeatHandler +internal sealed class PinnedBlockMemoryPoolFactory : IMemoryPoolFactory, IHeartbeatHandler { private readonly IMeterFactory _meterFactory; private readonly ConcurrentDictionary _pools = new(); @@ -19,7 +19,7 @@ public PinnedBlockMemoryPoolFactory(IMeterFactory meterFactory) _meterFactory = meterFactory; } - public MemoryPool CreatePool() + public MemoryPool Create() { // TODO: wire up PinnedBlockMemoryPool's dispose to remove from _pools var pool = new PinnedBlockMemoryPool(_meterFactory); diff --git a/src/Servers/Kestrel/Kestrel/src/WebHostBuilderKestrelExtensions.cs b/src/Servers/Kestrel/Kestrel/src/WebHostBuilderKestrelExtensions.cs index 0ac94ac6f8f2..45af6dc7543d 100644 --- a/src/Servers/Kestrel/Kestrel/src/WebHostBuilderKestrelExtensions.cs +++ b/src/Servers/Kestrel/Kestrel/src/WebHostBuilderKestrelExtensions.cs @@ -91,7 +91,7 @@ public static IWebHostBuilder UseKestrelCore(this IWebHostBuilder hostBuilder) services.AddSingleton(); services.TryAddEnumerable(ServiceDescriptor.Singleton(sp => sp.GetRequiredService())); - services.AddSingleton(sp => sp.GetRequiredService()); + services.AddSingleton>(sp => sp.GetRequiredService()); }); if (OperatingSystem.IsWindows()) diff --git a/src/Servers/Kestrel/Transport.NamedPipes/src/Internal/NamedPipeConnectionListener.cs b/src/Servers/Kestrel/Transport.NamedPipes/src/Internal/NamedPipeConnectionListener.cs index 8a4edca25a57..39465a4f3219 100644 --- a/src/Servers/Kestrel/Transport.NamedPipes/src/Internal/NamedPipeConnectionListener.cs +++ b/src/Servers/Kestrel/Transport.NamedPipes/src/Internal/NamedPipeConnectionListener.cs @@ -40,7 +40,7 @@ public NamedPipeConnectionListener( _log = loggerFactory.CreateLogger("Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes"); _endpoint = endpoint; _options = options; - _memoryPool = options.MemoryPoolFactory.CreatePool(); + _memoryPool = options.MemoryPoolFactory.Create(); _listeningToken = _listeningTokenSource.Token; // Have to create the pool here (instead of DI) because the pool is specific to an endpoint. _poolPolicy = new NamedPipeServerStreamPoolPolicy(endpoint, options); diff --git a/src/Servers/Kestrel/Transport.NamedPipes/src/NamedPipeTransportOptions.cs b/src/Servers/Kestrel/Transport.NamedPipes/src/NamedPipeTransportOptions.cs index 542553bcc50f..5bc0b4e4f54c 100644 --- a/src/Servers/Kestrel/Transport.NamedPipes/src/NamedPipeTransportOptions.cs +++ b/src/Servers/Kestrel/Transport.NamedPipes/src/NamedPipeTransportOptions.cs @@ -117,5 +117,5 @@ public static NamedPipeServerStream CreateDefaultNamedPipeServerStream(CreateNam } } - internal IMemoryPoolFactory MemoryPoolFactory { get; set; } = DefaultMemoryPoolFactory.Instance; + internal IMemoryPoolFactory MemoryPoolFactory { get; set; } = DefaultMemoryPoolFactory.Instance; } diff --git a/src/Servers/Kestrel/Transport.NamedPipes/src/WebHostBuilderNamedPipeExtensions.cs b/src/Servers/Kestrel/Transport.NamedPipes/src/WebHostBuilderNamedPipeExtensions.cs index 9d9962fb6479..d2121264ca6f 100644 --- a/src/Servers/Kestrel/Transport.NamedPipes/src/WebHostBuilderNamedPipeExtensions.cs +++ b/src/Servers/Kestrel/Transport.NamedPipes/src/WebHostBuilderNamedPipeExtensions.cs @@ -35,8 +35,8 @@ public static IWebHostBuilder UseNamedPipes(this IWebHostBuilder hostBuilder) services.TryAddSingleton(); services.AddSingleton(); - services.TryAddSingleton(); - services.AddOptions().Configure((NamedPipeTransportOptions options, IMemoryPoolFactory factory) => + services.TryAddSingleton, DefaultMemoryPoolFactory>(); + services.AddOptions().Configure((NamedPipeTransportOptions options, IMemoryPoolFactory factory) => { // Set the IMemoryPoolFactory from DI on NamedPipeTransportOptions. Usually this should be the PinnedBlockMemoryPoolFactory from UseKestrelCore. options.MemoryPoolFactory = factory; diff --git a/src/Servers/Kestrel/Transport.Sockets/src/Client/SocketConnectionFactory.cs b/src/Servers/Kestrel/Transport.Sockets/src/Client/SocketConnectionFactory.cs index 74bdeba7f64a..ef6f2c771579 100644 --- a/src/Servers/Kestrel/Transport.Sockets/src/Client/SocketConnectionFactory.cs +++ b/src/Servers/Kestrel/Transport.Sockets/src/Client/SocketConnectionFactory.cs @@ -27,7 +27,7 @@ public SocketConnectionFactory(IOptions options, ILogger ArgumentNullException.ThrowIfNull(loggerFactory); _options = options.Value; - _memoryPool = options.Value.MemoryPoolFactory.CreatePool(); + _memoryPool = options.Value.MemoryPoolFactory.Create(); _trace = loggerFactory.CreateLogger("Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.Client"); var maxReadBufferSize = _options.MaxReadBufferSize ?? 0; diff --git a/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionContextFactory.cs b/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionContextFactory.cs index ced4374e8ae6..86d2ec2a2d22 100644 --- a/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionContextFactory.cs +++ b/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionContextFactory.cs @@ -47,7 +47,7 @@ public SocketConnectionContextFactory(SocketConnectionFactoryOptions options, IL for (var i = 0; i < _settingsCount; i++) { - var memoryPool = _options.MemoryPoolFactory.CreatePool(); + var memoryPool = _options.MemoryPoolFactory.Create(); var transportScheduler = options.UnsafePreferInlineScheduling ? PipeScheduler.Inline : new IOQueue(); _settings[i] = new QueueSettings() @@ -62,7 +62,7 @@ public SocketConnectionContextFactory(SocketConnectionFactoryOptions options, IL } else { - var memoryPool = _options.MemoryPoolFactory.CreatePool(); + var memoryPool = _options.MemoryPoolFactory.Create(); var transportScheduler = options.UnsafePreferInlineScheduling ? PipeScheduler.Inline : PipeScheduler.ThreadPool; _settings = new QueueSettings[] diff --git a/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionFactoryOptions.cs b/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionFactoryOptions.cs index 9858095e8f60..8a8c9c78a665 100644 --- a/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionFactoryOptions.cs +++ b/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionFactoryOptions.cs @@ -68,5 +68,5 @@ internal SocketConnectionFactoryOptions(SocketTransportOptions transportOptions) /// public bool UnsafePreferInlineScheduling { get; set; } - internal IMemoryPoolFactory MemoryPoolFactory { get; set; } = DefaultMemoryPoolFactory.Instance; + internal IMemoryPoolFactory MemoryPoolFactory { get; set; } = DefaultMemoryPoolFactory.Instance; } diff --git a/src/Servers/Kestrel/Transport.Sockets/src/SocketTransportOptions.cs b/src/Servers/Kestrel/Transport.Sockets/src/SocketTransportOptions.cs index fa22127e2a1a..0ccdff75efa7 100644 --- a/src/Servers/Kestrel/Transport.Sockets/src/SocketTransportOptions.cs +++ b/src/Servers/Kestrel/Transport.Sockets/src/SocketTransportOptions.cs @@ -166,5 +166,5 @@ public static Socket CreateDefaultBoundListenSocket(EndPoint endpoint) return listenSocket; } - internal IMemoryPoolFactory MemoryPoolFactory { get; set; } = DefaultMemoryPoolFactory.Instance; + internal IMemoryPoolFactory MemoryPoolFactory { get; set; } = DefaultMemoryPoolFactory.Instance; } diff --git a/src/Servers/Kestrel/Transport.Sockets/src/WebHostBuilderSocketExtensions.cs b/src/Servers/Kestrel/Transport.Sockets/src/WebHostBuilderSocketExtensions.cs index 759e184d7570..fd1225596ab3 100644 --- a/src/Servers/Kestrel/Transport.Sockets/src/WebHostBuilderSocketExtensions.cs +++ b/src/Servers/Kestrel/Transport.Sockets/src/WebHostBuilderSocketExtensions.cs @@ -29,8 +29,8 @@ public static IWebHostBuilder UseSockets(this IWebHostBuilder hostBuilder) { services.TryAddSingleton(); - services.TryAddSingleton(); - services.AddOptions().Configure((SocketTransportOptions options, IMemoryPoolFactory factory) => + services.TryAddSingleton, DefaultMemoryPoolFactory>(); + services.AddOptions().Configure((SocketTransportOptions options, IMemoryPoolFactory factory) => { // Set the IMemoryPoolFactory from DI on SocketTransportOptions. Usually this should be the PinnedBlockMemoryPoolFactory from UseKestrelCore. options.MemoryPoolFactory = factory; diff --git a/src/Servers/Kestrel/shared/DefaultMemoryPoolFactory.cs b/src/Servers/Kestrel/shared/DefaultMemoryPoolFactory.cs index 61e8c23742a0..0df9b70f21fb 100644 --- a/src/Servers/Kestrel/shared/DefaultMemoryPoolFactory.cs +++ b/src/Servers/Kestrel/shared/DefaultMemoryPoolFactory.cs @@ -6,11 +6,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal; -internal sealed class DefaultMemoryPoolFactory : IMemoryPoolFactory +internal sealed class DefaultMemoryPoolFactory : IMemoryPoolFactory { public static DefaultMemoryPoolFactory Instance { get; } = new DefaultMemoryPoolFactory(); - public MemoryPool CreatePool() + public MemoryPool Create() { return MemoryPool.Shared; } diff --git a/src/Servers/Kestrel/shared/test/TestServiceContext.cs b/src/Servers/Kestrel/shared/test/TestServiceContext.cs index c0bbace9d6dd..eda50f884d8e 100644 --- a/src/Servers/Kestrel/shared/test/TestServiceContext.cs +++ b/src/Servers/Kestrel/shared/test/TestServiceContext.cs @@ -74,11 +74,11 @@ private void Initialize(ILoggerFactory loggerFactory, KestrelTrace kestrelTrace, public FakeTimeProvider FakeTimeProvider { get; set; } - public IMemoryPoolFactory MemoryPoolFactory { get; set; } = new WrappingMemoryPoolFactory(() => System.Buffers.PinnedBlockMemoryPoolFactory.Create()); + public IMemoryPoolFactory MemoryPoolFactory { get; set; } = new WrappingMemoryPoolFactory(() => System.Buffers.PinnedBlockMemoryPoolFactory.Create()); public string DateHeaderValue => DateHeaderValueManager.GetDateHeaderValues().String; - internal sealed class WrappingMemoryPoolFactory : IMemoryPoolFactory + internal sealed class WrappingMemoryPoolFactory : IMemoryPoolFactory { private readonly Func> _memoryPoolFactory; @@ -87,6 +87,6 @@ public WrappingMemoryPoolFactory(Func> memoryPoolFactory) _memoryPoolFactory = memoryPoolFactory; } - public MemoryPool CreatePool() => _memoryPoolFactory(); + public MemoryPool Create() => _memoryPoolFactory(); } } diff --git a/src/Servers/Kestrel/shared/test/TransportTestHelpers/DiagnosticMemoryPoolFactory.cs b/src/Servers/Kestrel/shared/test/TransportTestHelpers/DiagnosticMemoryPoolFactory.cs index 3b010de6478f..91a266249cd1 100644 --- a/src/Servers/Kestrel/shared/test/TransportTestHelpers/DiagnosticMemoryPoolFactory.cs +++ b/src/Servers/Kestrel/shared/test/TransportTestHelpers/DiagnosticMemoryPoolFactory.cs @@ -10,7 +10,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests; -public class DiagnosticMemoryPoolFactory : IMemoryPoolFactory +public class DiagnosticMemoryPoolFactory : IMemoryPoolFactory { private readonly bool _allowLateReturn; @@ -25,7 +25,7 @@ public DiagnosticMemoryPoolFactory(bool allowLateReturn = false, bool rentTracki _pools = new List(); } - public MemoryPool CreatePool() + public MemoryPool Create() { lock (_pools) { diff --git a/src/Servers/Kestrel/shared/test/TransportTestHelpers/TestServer.cs b/src/Servers/Kestrel/shared/test/TransportTestHelpers/TestServer.cs index 2c520774b89a..88ad8f350c17 100644 --- a/src/Servers/Kestrel/shared/test/TransportTestHelpers/TestServer.cs +++ b/src/Servers/Kestrel/shared/test/TransportTestHelpers/TestServer.cs @@ -80,7 +80,7 @@ public TestServer(RequestDelegate app, TestServiceContext context, Action(context.MemoryPoolFactory); + services.AddSingleton>(context.MemoryPoolFactory); } services.AddSingleton(this); services.AddSingleton(context.LoggerFactory); diff --git a/src/Servers/Kestrel/test/FunctionalTests/MaxRequestBufferSizeTests.cs b/src/Servers/Kestrel/test/FunctionalTests/MaxRequestBufferSizeTests.cs index caca6a6fd5dd..3ddaa21972e5 100644 --- a/src/Servers/Kestrel/test/FunctionalTests/MaxRequestBufferSizeTests.cs +++ b/src/Servers/Kestrel/test/FunctionalTests/MaxRequestBufferSizeTests.cs @@ -300,7 +300,7 @@ private async Task StartHost(long? maxRequestBufferSize, bool useConnectionAdapter, TaskCompletionSource startReadingRequestBody, TaskCompletionSource clientFinishedSendingRequestBody, - IMemoryPoolFactory memoryPoolFactory = null) + IMemoryPoolFactory memoryPoolFactory = null) { var host = TransportSelector.GetHostBuilder(maxRequestBufferSize) .ConfigureWebHost(webHostBuilder => @@ -339,7 +339,7 @@ private async Task StartHost(long? maxRequestBufferSize, { if (memoryPoolFactory != null) { - services.AddSingleton(memoryPoolFactory); + services.AddSingleton>(memoryPoolFactory); } }) .Configure(app => app.Run(async context => diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/TestTransport/TestServer.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/TestTransport/TestServer.cs index 0a252d78387c..e6d4f2edef29 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/TestTransport/TestServer.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/TestTransport/TestServer.cs @@ -70,7 +70,7 @@ public TestServer(RequestDelegate app, TestServiceContext context, Action Date: Wed, 26 Mar 2025 18:04:15 -0700 Subject: [PATCH 09/12] progress --- .../Internal/PinnedBlockMemoryPoolFactory.cs | 16 +- .../test/Http1/Http1ConnectionTestsBase.cs | 2 +- .../test/Http1/Http1OutputProducerTests.cs | 2 +- .../Core/test/HttpResponseHeadersTests.cs | 2 +- .../test/PinnedBlockMemoryPoolFactoryTests.cs | 107 ++++++++++ .../Core/test/PinnedBlockMemoryPoolTests.cs | 199 +++++++++++++++++- .../Core/test/PipelineExtensionTests.cs | 2 +- .../Kestrel/Core/test/StartLineTests.cs | 2 +- .../Core/test/TestHelpers/TestInput.cs | 2 +- .../WebHostBuilderKestrelExtensionsTests.cs | 33 ++- .../Microbenchmarks/ChunkWriterBenchmark.cs | 2 +- .../HeaderCollectionBenchmark.cs | 2 +- .../Http1ConnectionBenchmark.cs | 2 +- ...Http1ConnectionParsingOverheadBenchmark.cs | 2 +- .../Http1LargeWritingBenchmark.cs | 2 +- .../Microbenchmarks/Http1ReadingBenchmark.cs | 2 +- .../Microbenchmarks/Http1WritingBenchmark.cs | 2 +- .../Http2/Http2ConnectionBenchmarkBase.cs | 2 +- .../Http2/Http2FrameWriterBenchmark.cs | 2 +- .../HttpProtocolFeatureCollection.cs | 2 +- .../PipeThroughputBenchmark.cs | 2 +- .../RequestParsingBenchmark.cs | 2 +- .../ResponseHeaderCollectionBenchmark.cs | 2 +- .../shared/test/Http3/Http3InMemory.cs | 2 +- .../Kestrel/shared/test/TestContextFactory.cs | 2 +- .../Kestrel/shared/test/TestServiceContext.cs | 2 +- .../FunctionalTests/Http2/ShutdownTests.cs | 2 +- .../Http2/Http2TestBase.cs | 2 +- .../KestrelMetricsTests.cs | 4 +- .../Buffers.MemoryPool/MemoryPoolFactory.cs | 2 +- .../PinnedBlockMemoryPool.cs | 64 ++++-- .../PinnedBlockMemoryPoolMetrics.cs | 2 +- 32 files changed, 422 insertions(+), 53 deletions(-) create mode 100644 src/Servers/Kestrel/Core/test/PinnedBlockMemoryPoolFactoryTests.cs diff --git a/src/Servers/Kestrel/Core/src/Internal/PinnedBlockMemoryPoolFactory.cs b/src/Servers/Kestrel/Core/src/Internal/PinnedBlockMemoryPoolFactory.cs index 60cf5f38c5ea..b01ae01affaa 100644 --- a/src/Servers/Kestrel/Core/src/Internal/PinnedBlockMemoryPoolFactory.cs +++ b/src/Servers/Kestrel/Core/src/Internal/PinnedBlockMemoryPoolFactory.cs @@ -12,32 +12,34 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal; internal sealed class PinnedBlockMemoryPoolFactory : IMemoryPoolFactory, IHeartbeatHandler { private readonly IMeterFactory _meterFactory; + private readonly TimeProvider _timeProvider; private readonly ConcurrentDictionary _pools = new(); - public PinnedBlockMemoryPoolFactory(IMeterFactory meterFactory) + public PinnedBlockMemoryPoolFactory(IMeterFactory meterFactory, TimeProvider? timeProvider = null) { + _timeProvider = timeProvider ?? TimeProvider.System; _meterFactory = meterFactory; } public MemoryPool Create() { - // TODO: wire up PinnedBlockMemoryPool's dispose to remove from _pools var pool = new PinnedBlockMemoryPool(_meterFactory); + pool.DisposeCallback = (self) => + { + _pools.TryRemove(self, out _); + }; _pools.TryAdd(pool, pool); -#if DEBUG - return new DiagnosticMemoryPool(pool); -#else return pool; -#endif } public void OnHeartbeat() { + var now = _timeProvider.GetUtcNow(); foreach (var pool in _pools) { - pool.Value.PerformEviction(); + pool.Value.TryScheduleEviction(now); } } } diff --git a/src/Servers/Kestrel/Core/test/Http1/Http1ConnectionTestsBase.cs b/src/Servers/Kestrel/Core/test/Http1/Http1ConnectionTestsBase.cs index ae967abbfddc..4b460630c9f3 100644 --- a/src/Servers/Kestrel/Core/test/Http1/Http1ConnectionTestsBase.cs +++ b/src/Servers/Kestrel/Core/test/Http1/Http1ConnectionTestsBase.cs @@ -31,7 +31,7 @@ protected override void Initialize(TestContext context, MethodInfo methodInfo, o { base.Initialize(context, methodInfo, testMethodArguments, testOutputHelper); - _pipelineFactory = System.Buffers.PinnedBlockMemoryPoolFactory.Create(); + _pipelineFactory = TestMemoryPoolFactory.Create(); var options = new PipeOptions(_pipelineFactory, readerScheduler: PipeScheduler.Inline, writerScheduler: PipeScheduler.Inline, useSynchronizationContext: false); var pair = DuplexPipe.CreateConnectionPair(options, options); diff --git a/src/Servers/Kestrel/Core/test/Http1/Http1OutputProducerTests.cs b/src/Servers/Kestrel/Core/test/Http1/Http1OutputProducerTests.cs index b927baefa60f..0683b9ea6c84 100644 --- a/src/Servers/Kestrel/Core/test/Http1/Http1OutputProducerTests.cs +++ b/src/Servers/Kestrel/Core/test/Http1/Http1OutputProducerTests.cs @@ -22,7 +22,7 @@ public class Http1OutputProducerTests : IDisposable public Http1OutputProducerTests() { - _memoryPool = PinnedBlockMemoryPoolFactory.Create(); + _memoryPool = TestMemoryPoolFactory.Create(); } public void Dispose() diff --git a/src/Servers/Kestrel/Core/test/HttpResponseHeadersTests.cs b/src/Servers/Kestrel/Core/test/HttpResponseHeadersTests.cs index a68adb6daf5b..e2105939f504 100644 --- a/src/Servers/Kestrel/Core/test/HttpResponseHeadersTests.cs +++ b/src/Servers/Kestrel/Core/test/HttpResponseHeadersTests.cs @@ -21,7 +21,7 @@ public class HttpResponseHeadersTests [Fact] public void InitialDictionaryIsEmpty() { - using (var memoryPool = PinnedBlockMemoryPoolFactory.Create()) + using (var memoryPool = TestMemoryPoolFactory.Create()) { var options = new PipeOptions(memoryPool, readerScheduler: PipeScheduler.Inline, writerScheduler: PipeScheduler.Inline, useSynchronizationContext: false); var pair = DuplexPipe.CreateConnectionPair(options, options); diff --git a/src/Servers/Kestrel/Core/test/PinnedBlockMemoryPoolFactoryTests.cs b/src/Servers/Kestrel/Core/test/PinnedBlockMemoryPoolFactoryTests.cs new file mode 100644 index 000000000000..e1eae6256146 --- /dev/null +++ b/src/Servers/Kestrel/Core/test/PinnedBlockMemoryPoolFactoryTests.cs @@ -0,0 +1,107 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Buffers; +using System.Collections.Concurrent; +using System.Reflection; +using Microsoft.AspNetCore.InternalTesting; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal; +using Microsoft.Extensions.Time.Testing; + +namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests; + +public class PinnedBlockMemoryPoolFactoryTests +{ + [Fact] + public void CreatePool() + { + var factory = new PinnedBlockMemoryPoolFactory(new TestMeterFactory()); + var pool = factory.Create(); + Assert.NotNull(pool); + Assert.IsType(pool); + } + + [Fact] + public void CreateMultiplePools() + { + var factory = new PinnedBlockMemoryPoolFactory(new TestMeterFactory()); + var pool1 = factory.Create(); + var pool2 = factory.Create(); + + Assert.NotNull(pool1); + Assert.NotNull(pool2); + Assert.NotSame(pool1, pool2); + } + + [Fact] + public void DisposePoolRemovesFromFactory() + { + var factory = new PinnedBlockMemoryPoolFactory(new TestMeterFactory()); + var pool = factory.Create(); + Assert.NotNull(pool); + + var dict = (ConcurrentDictionary)(typeof(PinnedBlockMemoryPoolFactory) + .GetField("_pools", BindingFlags.NonPublic | BindingFlags.Instance) + ?.GetValue(factory)); + Assert.Single(dict); + + pool.Dispose(); + Assert.Empty(dict); + } + + [Fact] + public async Task FactoryHeartbeatWorks() + { + var timeProvider = new FakeTimeProvider(DateTimeOffset.UtcNow.AddDays(1)); + var factory = new PinnedBlockMemoryPoolFactory(new TestMeterFactory(), timeProvider); + + // Use 2 pools to make sure they all get triggered by the heartbeat + var pool = Assert.IsType(factory.Create()); + var pool2 = Assert.IsType(factory.Create()); + + var blocks = new List>(); + for (var i = 0; i < 10000; i++) + { + blocks.Add(pool.Rent()); + blocks.Add(pool2.Rent()); + } + + foreach (var block in blocks) + { + block.Dispose(); + } + blocks.Clear(); + + // First eviction pass likely won't do anything since the pool was just very active + factory.OnHeartbeat(); + + var previousCount = pool.BlockCount(); + var previousCount2 = pool2.BlockCount(); + timeProvider.Advance(TimeSpan.FromSeconds(10)); + factory.OnHeartbeat(); + + await VerifyPoolEviction(pool, previousCount); + await VerifyPoolEviction(pool2, previousCount2); + + timeProvider.Advance(TimeSpan.FromSeconds(10)); + + previousCount = pool.BlockCount(); + previousCount2 = pool2.BlockCount(); + factory.OnHeartbeat(); + + await VerifyPoolEviction(pool, previousCount); + await VerifyPoolEviction(pool2, previousCount2); + + static async Task VerifyPoolEviction(PinnedBlockMemoryPool pool, int previousCount) + { + var maxWait = TimeSpan.FromSeconds(5); + while (pool.BlockCount() > previousCount - (previousCount / 30) && maxWait > TimeSpan.Zero) + { + await Task.Delay(50); + maxWait -= TimeSpan.FromMilliseconds(50); + } + + Assert.InRange(pool.BlockCount(), previousCount - (previousCount / 10), previousCount - (previousCount / 30)); + } + } +} diff --git a/src/Servers/Kestrel/Core/test/PinnedBlockMemoryPoolTests.cs b/src/Servers/Kestrel/Core/test/PinnedBlockMemoryPoolTests.cs index 30d1f1c799a4..e72d5b096975 100644 --- a/src/Servers/Kestrel/Core/test/PinnedBlockMemoryPoolTests.cs +++ b/src/Servers/Kestrel/Core/test/PinnedBlockMemoryPoolTests.cs @@ -1,8 +1,7 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Buffers; -using Xunit; namespace Microsoft.Extensions.Internal.Test; @@ -25,4 +24,200 @@ public void DisposeWithActiveBlocksWorks() var block = memoryPool.Rent(); memoryPool.Dispose(); } + + [Fact] + public void CanEvictBlocks() + { + using var memoryPool = new PinnedBlockMemoryPool(); + + var block = memoryPool.Rent(); + block.Dispose(); + Assert.Equal(1, memoryPool.BlockCount()); + + // First eviction does nothing because we double counted the initial rent due to it needing to allocate + memoryPool.PerformEviction(); + Assert.Equal(1, memoryPool.BlockCount()); + + memoryPool.PerformEviction(); + Assert.Equal(0, memoryPool.BlockCount()); + } + + [Fact] + public void EvictsSmallAmountOfBlocksWhenTrafficIsTheSame() + { + using var memoryPool = new PinnedBlockMemoryPool(); + + var blocks = new List>(); + for (var i = 0; i < 10000; i++) + { + blocks.Add(memoryPool.Rent()); + } + Assert.Equal(0, memoryPool.BlockCount()); + memoryPool.PerformEviction(); + + foreach (var block in blocks) + { + block.Dispose(); + } + blocks.Clear(); + Assert.Equal(10000, memoryPool.BlockCount()); + memoryPool.PerformEviction(); + + var originalCount = memoryPool.BlockCount(); + for (var j = 0; j < 100; j++) + { + var previousCount = memoryPool.BlockCount(); + // Rent and return at the same rate + for (var i = 0; i < 100; i++) + { + blocks.Add(memoryPool.Rent()); + } + foreach (var block in blocks) + { + block.Dispose(); + } + blocks.Clear(); + + Assert.Equal(previousCount, memoryPool.BlockCount()); + + // Eviction while rent+return is the same + memoryPool.PerformEviction(); + Assert.InRange(memoryPool.BlockCount(), previousCount - (previousCount / 100), previousCount - 1); + } + + Assert.True(memoryPool.BlockCount() <= originalCount - 100, "Evictions should have removed some blocks"); + } + + [Fact] + public void DoesNotEvictBlocksWhenActive() + { + using var memoryPool = new PinnedBlockMemoryPool(); + + var blocks = new List>(); + for (var i = 0; i < 10000; i++) + { + blocks.Add(memoryPool.Rent()); + } + Assert.Equal(0, memoryPool.BlockCount()); + memoryPool.PerformEviction(); + + foreach (var block in blocks) + { + block.Dispose(); + } + blocks.Clear(); + Assert.Equal(10000, memoryPool.BlockCount()); + memoryPool.PerformEviction(); + var previousCount = memoryPool.BlockCount(); + + // Simulate active usage, rent without returning + for (var i = 0; i < 100; i++) + { + blocks.Add(memoryPool.Rent()); + } + previousCount -= 100; + + // Eviction while pool is actively used should not remove blocks + memoryPool.PerformEviction(); + Assert.Equal(previousCount, memoryPool.BlockCount()); + } + + [Fact] + public void EvictsBlocksGraduallyWhenIdle() + { + using var memoryPool = new PinnedBlockMemoryPool(); + + var blocks = new List>(); + for (var i = 0; i < 10000; i++) + { + blocks.Add(memoryPool.Rent()); + } + Assert.Equal(0, memoryPool.BlockCount()); + memoryPool.PerformEviction(); + + foreach (var block in blocks) + { + block.Dispose(); + } + blocks.Clear(); + Assert.Equal(10000, memoryPool.BlockCount()); + // Eviction after returning everything to reset internal counters + memoryPool.PerformEviction(); + + // Eviction should happen gradually over multiple calls + for (var i = 0; i < 10; i++) + { + var previousCount = memoryPool.BlockCount(); + memoryPool.PerformEviction(); + // Eviction while idle should remove 10-30% of blocks + Assert.InRange(memoryPool.BlockCount(), previousCount - (previousCount / 10), previousCount - (previousCount / 30)); + } + + // Ensure all blocks are evicted eventually + var count = memoryPool.BlockCount(); + do + { + count = memoryPool.BlockCount(); + memoryPool.PerformEviction(); + } + // Make sure the loop makes forward progress + while (count != 0 && count != memoryPool.BlockCount()); + + Assert.Equal(0, memoryPool.BlockCount()); + } + + [Fact] + public async Task EvictionsAreScheduled() + { + using var memoryPool = new PinnedBlockMemoryPool(); + + var blocks = new List>(); + for (var i = 0; i < 10000; i++) + { + blocks.Add(memoryPool.Rent()); + } + Assert.Equal(0, memoryPool.BlockCount()); + + foreach (var block in blocks) + { + block.Dispose(); + } + blocks.Clear(); + Assert.Equal(10000, memoryPool.BlockCount()); + // Eviction after returning everything to reset internal counters + memoryPool.PerformEviction(); + + Assert.Equal(10000, memoryPool.BlockCount()); + + var previousCount = memoryPool.BlockCount(); + + // Scheduling only works every 10 seconds and is initialized to UtcNow + 10 when the pool is constructed + Assert.False(memoryPool.TryScheduleEviction(DateTime.UtcNow)); + + Assert.True(memoryPool.TryScheduleEviction(DateTime.UtcNow.AddSeconds(10))); + + var maxWait = TimeSpan.FromSeconds(5); + while (memoryPool.BlockCount() > previousCount - (previousCount / 30) && maxWait > TimeSpan.Zero) + { + await Task.Delay(50); + maxWait -= TimeSpan.FromMilliseconds(50); + } + + Assert.InRange(memoryPool.BlockCount(), previousCount - (previousCount / 10), previousCount - (previousCount / 30)); + + // Since we scheduled successfully, we now need to wait 10 seconds to schedule again. + Assert.False(memoryPool.TryScheduleEviction(DateTime.UtcNow.AddSeconds(10))); + + previousCount = memoryPool.BlockCount(); + Assert.True(memoryPool.TryScheduleEviction(DateTime.UtcNow.AddSeconds(20))); + + maxWait = TimeSpan.FromSeconds(5); + while (memoryPool.BlockCount() > previousCount - (previousCount / 30) && maxWait > TimeSpan.Zero) + { + await Task.Delay(50); + maxWait -= TimeSpan.FromMilliseconds(50); + } + + Assert.InRange(memoryPool.BlockCount(), previousCount - (previousCount / 10), previousCount - (previousCount / 30)); + } } diff --git a/src/Servers/Kestrel/Core/test/PipelineExtensionTests.cs b/src/Servers/Kestrel/Core/test/PipelineExtensionTests.cs index e9dfeff6d9bb..361886b9f3be 100644 --- a/src/Servers/Kestrel/Core/test/PipelineExtensionTests.cs +++ b/src/Servers/Kestrel/Core/test/PipelineExtensionTests.cs @@ -16,7 +16,7 @@ public class PipelineExtensionTests : IDisposable private const int _ulongMaxValueLength = 20; private readonly Pipe _pipe; - private readonly MemoryPool _memoryPool = PinnedBlockMemoryPoolFactory.Create(); + private readonly MemoryPool _memoryPool = TestMemoryPoolFactory.Create(); public PipelineExtensionTests() { diff --git a/src/Servers/Kestrel/Core/test/StartLineTests.cs b/src/Servers/Kestrel/Core/test/StartLineTests.cs index d80b9d009ad5..234939468177 100644 --- a/src/Servers/Kestrel/Core/test/StartLineTests.cs +++ b/src/Servers/Kestrel/Core/test/StartLineTests.cs @@ -515,7 +515,7 @@ public void AuthorityForms(string rawTarget, string path, string query) public StartLineTests() { - MemoryPool = PinnedBlockMemoryPoolFactory.Create(); + MemoryPool = TestMemoryPoolFactory.Create(); var options = new PipeOptions(MemoryPool, readerScheduler: PipeScheduler.Inline, writerScheduler: PipeScheduler.Inline, useSynchronizationContext: false); var pair = DuplexPipe.CreateConnectionPair(options, options); Transport = pair.Transport; diff --git a/src/Servers/Kestrel/Core/test/TestHelpers/TestInput.cs b/src/Servers/Kestrel/Core/test/TestHelpers/TestInput.cs index f79218d1ec21..88a5c8cfea69 100644 --- a/src/Servers/Kestrel/Core/test/TestHelpers/TestInput.cs +++ b/src/Servers/Kestrel/Core/test/TestHelpers/TestInput.cs @@ -24,7 +24,7 @@ class TestInput : IDisposable public TestInput(KestrelTrace log = null, ITimeoutControl timeoutControl = null) { - _memoryPool = System.Buffers.PinnedBlockMemoryPoolFactory.Create(); + _memoryPool = TestMemoryPoolFactory.Create(); var options = new PipeOptions(pool: _memoryPool, readerScheduler: PipeScheduler.Inline, writerScheduler: PipeScheduler.Inline, useSynchronizationContext: false); var pair = DuplexPipe.CreateConnectionPair(options, options); Transport = pair.Transport; diff --git a/src/Servers/Kestrel/Kestrel/test/WebHostBuilderKestrelExtensionsTests.cs b/src/Servers/Kestrel/Kestrel/test/WebHostBuilderKestrelExtensionsTests.cs index b24da893ab53..271d12084893 100644 --- a/src/Servers/Kestrel/Kestrel/test/WebHostBuilderKestrelExtensionsTests.cs +++ b/src/Servers/Kestrel/Kestrel/test/WebHostBuilderKestrelExtensionsTests.cs @@ -1,17 +1,17 @@ // 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; using System.IO.Pipelines; using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Server.Kestrel.Core; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; using Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.Internal; using Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets; using Microsoft.Extensions.DependencyInjection; -using Xunit; +using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Server.Kestrel.Tests; @@ -116,4 +116,33 @@ public void ServerIsKestrelServerImpl() Assert.Equal(PipeScheduler.ThreadPool, server.ServiceContext.Scheduler); Assert.Equal(TimeProvider.System, server.ServiceContext.TimeProvider); } + + [Fact] + public void MemoryPoolFactorySetCorrectly() + { + var hostBuilder = new WebHostBuilder() + .UseSockets() + .UseKestrel() + .Configure(app => { }); + + var host = hostBuilder.Build(); + + var memoryPoolFactory = Assert.IsType(host.Services.GetRequiredService>()); + Assert.Null(host.Services.GetService>()); + + Assert.Same(memoryPoolFactory, host.Services.GetRequiredService>().Value.MemoryPoolFactory); + + // Swap order of UseKestrel and UseSockets + hostBuilder = new WebHostBuilder() + .UseKestrel() + .UseSockets() + .Configure(app => { }); + + host = hostBuilder.Build(); + + memoryPoolFactory = Assert.IsType(host.Services.GetRequiredService>()); + Assert.Null(host.Services.GetService>()); + + Assert.Same(memoryPoolFactory, host.Services.GetRequiredService>().Value.MemoryPoolFactory); + } } diff --git a/src/Servers/Kestrel/perf/Microbenchmarks/ChunkWriterBenchmark.cs b/src/Servers/Kestrel/perf/Microbenchmarks/ChunkWriterBenchmark.cs index f4f5ca0348cf..adf66afad01a 100644 --- a/src/Servers/Kestrel/perf/Microbenchmarks/ChunkWriterBenchmark.cs +++ b/src/Servers/Kestrel/perf/Microbenchmarks/ChunkWriterBenchmark.cs @@ -20,7 +20,7 @@ public class ChunkWriterBenchmark [GlobalSetup] public void Setup() { - _memoryPool = PinnedBlockMemoryPoolFactory.Create(); + _memoryPool = TestMemoryPoolFactory.Create(); var pipe = new Pipe(new PipeOptions(_memoryPool)); _reader = pipe.Reader; _writer = pipe.Writer; diff --git a/src/Servers/Kestrel/perf/Microbenchmarks/HeaderCollectionBenchmark.cs b/src/Servers/Kestrel/perf/Microbenchmarks/HeaderCollectionBenchmark.cs index 2ba73acc204e..88448414f662 100644 --- a/src/Servers/Kestrel/perf/Microbenchmarks/HeaderCollectionBenchmark.cs +++ b/src/Servers/Kestrel/perf/Microbenchmarks/HeaderCollectionBenchmark.cs @@ -324,7 +324,7 @@ string ReadHeaders() [IterationSetup] public void Setup() { - var memoryPool = PinnedBlockMemoryPoolFactory.Create(); + var memoryPool = TestMemoryPoolFactory.Create(); var options = new PipeOptions(memoryPool, readerScheduler: PipeScheduler.Inline, writerScheduler: PipeScheduler.Inline, useSynchronizationContext: false); var pair = DuplexPipe.CreateConnectionPair(options, options); diff --git a/src/Servers/Kestrel/perf/Microbenchmarks/Http1ConnectionBenchmark.cs b/src/Servers/Kestrel/perf/Microbenchmarks/Http1ConnectionBenchmark.cs index b2bf9a4440d7..4c73e1b10d5e 100644 --- a/src/Servers/Kestrel/perf/Microbenchmarks/Http1ConnectionBenchmark.cs +++ b/src/Servers/Kestrel/perf/Microbenchmarks/Http1ConnectionBenchmark.cs @@ -27,7 +27,7 @@ public class Http1ConnectionBenchmark [GlobalSetup] public void Setup() { - var memoryPool = System.Buffers.PinnedBlockMemoryPoolFactory.Create(); + var memoryPool = TestMemoryPoolFactory.Create(); var options = new PipeOptions(memoryPool, readerScheduler: PipeScheduler.Inline, writerScheduler: PipeScheduler.Inline, useSynchronizationContext: false); var pair = DuplexPipe.CreateConnectionPair(options, options); diff --git a/src/Servers/Kestrel/perf/Microbenchmarks/Http1ConnectionParsingOverheadBenchmark.cs b/src/Servers/Kestrel/perf/Microbenchmarks/Http1ConnectionParsingOverheadBenchmark.cs index e1e2061ea0fd..a4cc32b06b6b 100644 --- a/src/Servers/Kestrel/perf/Microbenchmarks/Http1ConnectionParsingOverheadBenchmark.cs +++ b/src/Servers/Kestrel/perf/Microbenchmarks/Http1ConnectionParsingOverheadBenchmark.cs @@ -23,7 +23,7 @@ public class Http1ConnectionParsingOverheadBenchmark [IterationSetup] public void Setup() { - var memoryPool = System.Buffers.PinnedBlockMemoryPoolFactory.Create(); + var memoryPool = TestMemoryPoolFactory.Create(); var options = new PipeOptions(memoryPool, readerScheduler: PipeScheduler.Inline, writerScheduler: PipeScheduler.Inline, useSynchronizationContext: false); var pair = DuplexPipe.CreateConnectionPair(options, options); diff --git a/src/Servers/Kestrel/perf/Microbenchmarks/Http1LargeWritingBenchmark.cs b/src/Servers/Kestrel/perf/Microbenchmarks/Http1LargeWritingBenchmark.cs index 192e09bd7ff3..35c7b5ad9388 100644 --- a/src/Servers/Kestrel/perf/Microbenchmarks/Http1LargeWritingBenchmark.cs +++ b/src/Servers/Kestrel/perf/Microbenchmarks/Http1LargeWritingBenchmark.cs @@ -28,7 +28,7 @@ public class Http1LargeWritingBenchmark [GlobalSetup] public void GlobalSetup() { - _memoryPool = System.Buffers.PinnedBlockMemoryPoolFactory.Create(); + _memoryPool = TestMemoryPoolFactory.Create(); _http1Connection = MakeHttp1Connection(); _consumeResponseBodyTask = ConsumeResponseBody(); } diff --git a/src/Servers/Kestrel/perf/Microbenchmarks/Http1ReadingBenchmark.cs b/src/Servers/Kestrel/perf/Microbenchmarks/Http1ReadingBenchmark.cs index 587e0dc056c5..8bacc955ad04 100644 --- a/src/Servers/Kestrel/perf/Microbenchmarks/Http1ReadingBenchmark.cs +++ b/src/Servers/Kestrel/perf/Microbenchmarks/Http1ReadingBenchmark.cs @@ -35,7 +35,7 @@ public class Http1ReadingBenchmark [GlobalSetup] public void GlobalSetup() { - _memoryPool = System.Buffers.PinnedBlockMemoryPoolFactory.Create(); + _memoryPool = TestMemoryPoolFactory.Create(); _http1Connection = MakeHttp1Connection(); } diff --git a/src/Servers/Kestrel/perf/Microbenchmarks/Http1WritingBenchmark.cs b/src/Servers/Kestrel/perf/Microbenchmarks/Http1WritingBenchmark.cs index 122f8ac0f4eb..6d6d1de8a350 100644 --- a/src/Servers/Kestrel/perf/Microbenchmarks/Http1WritingBenchmark.cs +++ b/src/Servers/Kestrel/perf/Microbenchmarks/Http1WritingBenchmark.cs @@ -35,7 +35,7 @@ public class Http1WritingBenchmark [GlobalSetup] public void GlobalSetup() { - _memoryPool = System.Buffers.PinnedBlockMemoryPoolFactory.Create(); + _memoryPool = TestMemoryPoolFactory.Create(); _http1Connection = MakeHttp1Connection(); _consumeResponseBodyTask = ConsumeResponseBody(); } diff --git a/src/Servers/Kestrel/perf/Microbenchmarks/Http2/Http2ConnectionBenchmarkBase.cs b/src/Servers/Kestrel/perf/Microbenchmarks/Http2/Http2ConnectionBenchmarkBase.cs index 8c4f22ddfc42..bf90e4859427 100644 --- a/src/Servers/Kestrel/perf/Microbenchmarks/Http2/Http2ConnectionBenchmarkBase.cs +++ b/src/Servers/Kestrel/perf/Microbenchmarks/Http2/Http2ConnectionBenchmarkBase.cs @@ -43,7 +43,7 @@ public abstract class Http2ConnectionBenchmarkBase public virtual void GlobalSetup() { - _memoryPool = PinnedBlockMemoryPoolFactory.Create(); + _memoryPool = TestMemoryPoolFactory.Create(); var options = new PipeOptions(_memoryPool, readerScheduler: PipeScheduler.Inline, writerScheduler: PipeScheduler.Inline, useSynchronizationContext: false); diff --git a/src/Servers/Kestrel/perf/Microbenchmarks/Http2/Http2FrameWriterBenchmark.cs b/src/Servers/Kestrel/perf/Microbenchmarks/Http2/Http2FrameWriterBenchmark.cs index 03589ed07b77..138e82a97c85 100644 --- a/src/Servers/Kestrel/perf/Microbenchmarks/Http2/Http2FrameWriterBenchmark.cs +++ b/src/Servers/Kestrel/perf/Microbenchmarks/Http2/Http2FrameWriterBenchmark.cs @@ -24,7 +24,7 @@ public class Http2FrameWriterBenchmark [GlobalSetup] public void GlobalSetup() { - _memoryPool = PinnedBlockMemoryPoolFactory.Create(); + _memoryPool = TestMemoryPoolFactory.Create(); var options = new PipeOptions(_memoryPool, readerScheduler: PipeScheduler.Inline, writerScheduler: PipeScheduler.Inline, useSynchronizationContext: false); _pipe = new Pipe(options); diff --git a/src/Servers/Kestrel/perf/Microbenchmarks/HttpProtocolFeatureCollection.cs b/src/Servers/Kestrel/perf/Microbenchmarks/HttpProtocolFeatureCollection.cs index bb513fd002af..297325a1e15b 100644 --- a/src/Servers/Kestrel/perf/Microbenchmarks/HttpProtocolFeatureCollection.cs +++ b/src/Servers/Kestrel/perf/Microbenchmarks/HttpProtocolFeatureCollection.cs @@ -226,7 +226,7 @@ public IHttpNotFoundFeature Get_IHttpNotFoundFeature() public HttpProtocolFeatureCollection() { - var memoryPool = PinnedBlockMemoryPoolFactory.Create(); + var memoryPool = TestMemoryPoolFactory.Create(); var options = new PipeOptions(memoryPool, readerScheduler: PipeScheduler.Inline, writerScheduler: PipeScheduler.Inline, useSynchronizationContext: false); var pair = DuplexPipe.CreateConnectionPair(options, options); diff --git a/src/Servers/Kestrel/perf/Microbenchmarks/PipeThroughputBenchmark.cs b/src/Servers/Kestrel/perf/Microbenchmarks/PipeThroughputBenchmark.cs index 9a941cab1b63..3550af9079f0 100644 --- a/src/Servers/Kestrel/perf/Microbenchmarks/PipeThroughputBenchmark.cs +++ b/src/Servers/Kestrel/perf/Microbenchmarks/PipeThroughputBenchmark.cs @@ -19,7 +19,7 @@ public class PipeThroughputBenchmark [IterationSetup] public void Setup() { - _memoryPool = PinnedBlockMemoryPoolFactory.Create(); + _memoryPool = TestMemoryPoolFactory.Create(); _pipe = new Pipe(new PipeOptions(_memoryPool)); } diff --git a/src/Servers/Kestrel/perf/Microbenchmarks/RequestParsingBenchmark.cs b/src/Servers/Kestrel/perf/Microbenchmarks/RequestParsingBenchmark.cs index cb3e893f40ed..0a0cb4b9002a 100644 --- a/src/Servers/Kestrel/perf/Microbenchmarks/RequestParsingBenchmark.cs +++ b/src/Servers/Kestrel/perf/Microbenchmarks/RequestParsingBenchmark.cs @@ -24,7 +24,7 @@ public class RequestParsingBenchmark [IterationSetup] public void Setup() { - _memoryPool = System.Buffers.PinnedBlockMemoryPoolFactory.Create(); + _memoryPool = TestMemoryPoolFactory.Create(); var options = new PipeOptions(_memoryPool, readerScheduler: PipeScheduler.Inline, writerScheduler: PipeScheduler.Inline, useSynchronizationContext: false); var pair = DuplexPipe.CreateConnectionPair(options, options); diff --git a/src/Servers/Kestrel/perf/Microbenchmarks/ResponseHeaderCollectionBenchmark.cs b/src/Servers/Kestrel/perf/Microbenchmarks/ResponseHeaderCollectionBenchmark.cs index 3bd558e5f797..359931b0f371 100644 --- a/src/Servers/Kestrel/perf/Microbenchmarks/ResponseHeaderCollectionBenchmark.cs +++ b/src/Servers/Kestrel/perf/Microbenchmarks/ResponseHeaderCollectionBenchmark.cs @@ -172,7 +172,7 @@ private void Unknown(int count) [IterationSetup] public void Setup() { - var memoryPool = System.Buffers.PinnedBlockMemoryPoolFactory.Create(); + var memoryPool = TestMemoryPoolFactory.Create(); var options = new PipeOptions(memoryPool, readerScheduler: PipeScheduler.Inline, writerScheduler: PipeScheduler.Inline, useSynchronizationContext: false); var pair = DuplexPipe.CreateConnectionPair(options, options); diff --git a/src/Servers/Kestrel/shared/test/Http3/Http3InMemory.cs b/src/Servers/Kestrel/shared/test/Http3/Http3InMemory.cs index e0db616b73d9..55321b0c33b4 100644 --- a/src/Servers/Kestrel/shared/test/Http3/Http3InMemory.cs +++ b/src/Servers/Kestrel/shared/test/Http3/Http3InMemory.cs @@ -73,7 +73,7 @@ public void OnTimeout(TimeoutReason reason) private FakeTimeProvider _fakeTimeProvider; internal HttpConnection _httpConnection; internal readonly TimeoutControl _timeoutControl; - internal readonly MemoryPool _memoryPool = System.Buffers.PinnedBlockMemoryPoolFactory.Create(); + internal readonly MemoryPool _memoryPool = TestMemoryPoolFactory.Create(); internal readonly ConcurrentQueue _streamContextPool = new ConcurrentQueue(); protected Task _connectionTask; internal ILogger Logger { get; } diff --git a/src/Servers/Kestrel/shared/test/TestContextFactory.cs b/src/Servers/Kestrel/shared/test/TestContextFactory.cs index 5fa2f1c65a12..c019bc0939fb 100644 --- a/src/Servers/Kestrel/shared/test/TestContextFactory.cs +++ b/src/Servers/Kestrel/shared/test/TestContextFactory.cs @@ -93,7 +93,7 @@ public static HttpMultiplexedConnectionContext CreateHttp3ConnectionContext( connectionContext, serviceContext ?? CreateServiceContext(new KestrelServerOptions()), connectionFeatures ?? new FeatureCollection(), - memoryPool ?? System.Buffers.PinnedBlockMemoryPoolFactory.Create(), + memoryPool ?? TestMemoryPoolFactory.Create(), localEndPoint, remoteEndPoint, metricsContext) diff --git a/src/Servers/Kestrel/shared/test/TestServiceContext.cs b/src/Servers/Kestrel/shared/test/TestServiceContext.cs index eda50f884d8e..2987d52f5461 100644 --- a/src/Servers/Kestrel/shared/test/TestServiceContext.cs +++ b/src/Servers/Kestrel/shared/test/TestServiceContext.cs @@ -74,7 +74,7 @@ private void Initialize(ILoggerFactory loggerFactory, KestrelTrace kestrelTrace, public FakeTimeProvider FakeTimeProvider { get; set; } - public IMemoryPoolFactory MemoryPoolFactory { get; set; } = new WrappingMemoryPoolFactory(() => System.Buffers.PinnedBlockMemoryPoolFactory.Create()); + public IMemoryPoolFactory MemoryPoolFactory { get; set; } = new WrappingMemoryPoolFactory(() => TestMemoryPoolFactory.Create()); public string DateHeaderValue => DateHeaderValueManager.GetDateHeaderValues().String; diff --git a/src/Servers/Kestrel/test/FunctionalTests/Http2/ShutdownTests.cs b/src/Servers/Kestrel/test/FunctionalTests/Http2/ShutdownTests.cs index 2a91a1a49dcd..7d9ba4bd8f70 100644 --- a/src/Servers/Kestrel/test/FunctionalTests/Http2/ShutdownTests.cs +++ b/src/Servers/Kestrel/test/FunctionalTests/Http2/ShutdownTests.cs @@ -168,7 +168,7 @@ public async Task GracefulTurnsAbortiveIfRequestsDoNotFinish() var testContext = new TestServiceContext(LoggerFactory) { - MemoryPoolFactory = new TestServiceContext.WrappingMemoryPoolFactory(() => PinnedBlockMemoryPoolFactory.CreatePinnedBlockMemoryPool()), + MemoryPoolFactory = new TestServiceContext.WrappingMemoryPoolFactory(() => TestMemoryPoolFactory.CreatePinnedBlockMemoryPool()), }; ThrowOnUngracefulShutdown = false; diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs index bf66a8bbd5f0..6fdfc0153206 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs @@ -112,7 +112,7 @@ protected static IEnumerable> ReadRateRequestHeader protected static readonly byte[] _noData = new byte[0]; protected static readonly byte[] _maxData = Encoding.ASCII.GetBytes(new string('a', Http2PeerSettings.MinAllowedMaxFrameSize)); - private readonly MemoryPool _memoryPool = System.Buffers.PinnedBlockMemoryPoolFactory.Create(); + private readonly MemoryPool _memoryPool = TestMemoryPoolFactory.Create(); internal readonly Http2PeerSettings _clientSettings = new Http2PeerSettings(); internal readonly HPackDecoder _hpackDecoder; diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/KestrelMetricsTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/KestrelMetricsTests.cs index f24b730db08a..efa7e242aa78 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/KestrelMetricsTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/KestrelMetricsTests.cs @@ -273,7 +273,7 @@ public async Task Http1Connection_ServerShutdown_Abort() var serviceContext = new TestServiceContext(LoggerFactory, metrics: new KestrelMetrics(testMeterFactory)) { - MemoryPoolFactory = new TestServiceContext.WrappingMemoryPoolFactory(() => System.Buffers.PinnedBlockMemoryPoolFactory.CreatePinnedBlockMemoryPool()), + MemoryPoolFactory = new TestServiceContext.WrappingMemoryPoolFactory(() => TestMemoryPoolFactory.CreatePinnedBlockMemoryPool()), ShutdownTimeout = TimeSpan.Zero }; @@ -612,7 +612,7 @@ public async Task Http2Connection_ServerShutdown_Abort() var serviceContext = new TestServiceContext(LoggerFactory, metrics: new KestrelMetrics(testMeterFactory)) { ShutdownTimeout = TimeSpan.Zero, - MemoryPoolFactory = new TestServiceContext.WrappingMemoryPoolFactory(() => System.Buffers.PinnedBlockMemoryPoolFactory.CreatePinnedBlockMemoryPool()) + MemoryPoolFactory = new TestServiceContext.WrappingMemoryPoolFactory(() => TestMemoryPoolFactory.CreatePinnedBlockMemoryPool()) }; var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); diff --git a/src/Shared/Buffers.MemoryPool/MemoryPoolFactory.cs b/src/Shared/Buffers.MemoryPool/MemoryPoolFactory.cs index e61ba1cfe261..858e8c4ac1fb 100644 --- a/src/Shared/Buffers.MemoryPool/MemoryPoolFactory.cs +++ b/src/Shared/Buffers.MemoryPool/MemoryPoolFactory.cs @@ -7,7 +7,7 @@ namespace System.Buffers; #nullable enable -internal static class PinnedBlockMemoryPoolFactory +internal static class TestMemoryPoolFactory { public static MemoryPool Create(IMeterFactory? meterFactory = null) { diff --git a/src/Shared/Buffers.MemoryPool/PinnedBlockMemoryPool.cs b/src/Shared/Buffers.MemoryPool/PinnedBlockMemoryPool.cs index 93fb3992f2d2..faee67df1d73 100644 --- a/src/Shared/Buffers.MemoryPool/PinnedBlockMemoryPool.cs +++ b/src/Shared/Buffers.MemoryPool/PinnedBlockMemoryPool.cs @@ -12,7 +12,7 @@ namespace System.Buffers; /// /// Used to allocate and distribute re-usable blocks of memory. /// -internal sealed class PinnedBlockMemoryPool : MemoryPool +internal sealed class PinnedBlockMemoryPool : MemoryPool, IThreadPoolWorkItem { /// /// The size of a block. 4096 is chosen because most operating systems use 4k pages. @@ -30,6 +30,11 @@ internal sealed class PinnedBlockMemoryPool : MemoryPool /// public static int BlockSize => _blockSize; + /// + /// Optional callback to call when pool is being disposed. + /// + public Action? DisposeCallback { get; set; } + /// /// Thread-safe collection of blocks which are currently in the pool. A slab will pre-allocate all of the block tracking objects /// and add them to this collection. When memory is requested it is taken from here first, and when it is returned it is re-added. @@ -43,8 +48,9 @@ internal sealed class PinnedBlockMemoryPool : MemoryPool private readonly PinnedBlockMemoryPoolMetrics _metrics; - public long _currentMemory; - public long _evictedMemory; + private long _currentMemory; + private long _evictedMemory; + private DateTimeOffset _nextEviction = DateTime.UtcNow.AddSeconds(10); private uint _rentCount; private uint _returnCount; @@ -96,6 +102,10 @@ public override IMemoryOwner Rent(int size = AnySize) _metrics.Rent(BlockSize); //Interlocked.Increment(ref _rentCount); //++_rentCount; + + // We already counted this Rent call above, but since we're now allocating (need more blocks) + // that means the pool is 'very' active and we probably shouldn't evict blocks, so we count again + // to reduce the chance of eviction occurring this cycle. ScalableCount(ref _rentCount); return new MemoryPoolBlock(this, BlockSize); @@ -130,32 +140,56 @@ internal void Return(MemoryPoolBlock block) } } - public void PerformEviction() + public bool TryScheduleEviction(DateTimeOffset now) + { + if (now >= _nextEviction) + { + _nextEviction = now.AddSeconds(10); + ThreadPool.UnsafeQueueUserWorkItem(this, preferLocal: false); + return true; + } + + return false; + } + + void IThreadPoolWorkItem.Execute() + { + PerformEviction(); + } + + internal void PerformEviction() { - long evictedMemoryThisPass = 0; var currentCount = (uint)_blocks.Count; var burstAmount = 0u; + + var rentCount = _rentCount; + var returnCount = _returnCount; + _rentCount = 0; + _returnCount = 0; + // If any activity - if (_rentCount + _returnCount > 0) + if (rentCount + returnCount > 0) { // Trending less traffic - if (_returnCount > _rentCount) + if (returnCount > rentCount) { - burstAmount = Math.Min(currentCount / 100, (_returnCount - _rentCount) / 5); + // Remove the lower of 1% of the current blocks and 20% of the difference between rented and returned + burstAmount = Math.Min(currentCount / 100, (returnCount - rentCount) / 5); } // Traffic staying the same, try removing some blocks since we probably have excess - else if (_returnCount == _rentCount) + else if (returnCount == rentCount) { + // Remove 1% of the current blocks (or at least 1) burstAmount = Math.Max(1, currentCount / 100); } + // else trending more traffic so we don't want to evict anything } // If no activity else { + // Remove 5% of the current blocks (or at least 10) burstAmount = Math.Max(10, currentCount / 20); } - _rentCount = 0; - _returnCount = 0; // Remove from queue and let GC clean the memory up while (burstAmount > 0 && _blocks.TryDequeue(out var block)) @@ -165,11 +199,8 @@ public void PerformEviction() Interlocked.Add(ref _currentMemory, -block.Memory.Length); Interlocked.Add(ref _evictedMemory, block.Memory.Length); - evictedMemoryThisPass += block.Memory.Length; burstAmount--; } - - Debug.WriteLine($"Evicted {evictedMemoryThisPass} bytes."); } protected override void Dispose(bool disposing) @@ -183,6 +214,8 @@ protected override void Dispose(bool disposing) { _isDisposed = true; + DisposeCallback?.Invoke(this); + if (disposing) { // Discard blocks in pool @@ -194,6 +227,9 @@ protected override void Dispose(bool disposing) } } + // Used for testing + public int BlockCount() => _blocks.Count; + private sealed class NoopMeterFactory : IMeterFactory { public static NoopMeterFactory Instance { get; } = new(); diff --git a/src/Shared/Buffers.MemoryPool/PinnedBlockMemoryPoolMetrics.cs b/src/Shared/Buffers.MemoryPool/PinnedBlockMemoryPoolMetrics.cs index ab9413542c1a..970c3cc87f2c 100644 --- a/src/Shared/Buffers.MemoryPool/PinnedBlockMemoryPoolMetrics.cs +++ b/src/Shared/Buffers.MemoryPool/PinnedBlockMemoryPoolMetrics.cs @@ -41,7 +41,7 @@ public PinnedBlockMemoryPoolMetrics(IMeterFactory meterFactory) _usageRate = _meter.CreateCounter( "pinnedblockmemorypool.usage_rate", - unit: "bytes", + unit: "{bytes}", description: "Rate of bytes rented from the pool." ); } From bd9cd25b1856c480c5c0a053d26c034627257d6f Mon Sep 17 00:00:00 2001 From: Brennan Date: Tue, 1 Apr 2025 13:25:41 -0700 Subject: [PATCH 10/12] stash --- src/Servers/HttpSys/src/HttpSysListener.cs | 9 +- src/Servers/HttpSys/src/MessagePump.cs | 6 +- .../src/WebHostBuilderHttpSysExtensions.cs | 5 ++ .../FunctionalTests/Listener/ServerTests.cs | 3 +- .../FunctionalTests/Listener/Utilities.cs | 7 +- .../HttpSys/test/FunctionalTests/Utilities.cs | 5 +- .../HttpSys/test/NonHelixTests/Utilities.cs | 5 +- ...oft.AspNetCore.Server.Kestrel.Tests.csproj | 1 + .../WebHostBuilderKestrelExtensionsTests.cs | 85 ++++++++++++++++++- .../DefaultMemoryPoolFactory.cs | 68 +++++++++++++++ .../PinnedBlockMemoryPool.cs | 4 +- 11 files changed, 182 insertions(+), 16 deletions(-) create mode 100644 src/Shared/Buffers.MemoryPool/DefaultMemoryPoolFactory.cs diff --git a/src/Servers/HttpSys/src/HttpSysListener.cs b/src/Servers/HttpSys/src/HttpSysListener.cs index 45ddfa39053d..695871439d9c 100644 --- a/src/Servers/HttpSys/src/HttpSysListener.cs +++ b/src/Servers/HttpSys/src/HttpSysListener.cs @@ -4,6 +4,7 @@ using System.Buffers; using System.Diagnostics; using System.Diagnostics.Metrics; +using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.HttpSys.Internal; using Microsoft.AspNetCore.WebUtilities; @@ -34,14 +35,14 @@ internal sealed partial class HttpSysListener : IDisposable // 0.5 seconds per request. Respond with a 400 Bad Request. private const int UnknownHeaderLimit = 1000; - internal sealed class DummyMeterFactory : IMeterFactory + internal sealed class NoopMeterFactory : IMeterFactory { public Meter Create(MeterOptions options) => new Meter(options); public void Dispose() { } } - internal MemoryPool MemoryPool { get; } = PinnedBlockMemoryPoolFactory.Create(new DummyMeterFactory()); + internal MemoryPool MemoryPool { get; } private volatile State _state; // m_State is set only within lock blocks, but often read outside locks. @@ -52,7 +53,7 @@ public void Dispose() { } private readonly object _internalLock; - public HttpSysListener(HttpSysOptions options, ILoggerFactory loggerFactory) + public HttpSysListener(HttpSysOptions options, IMemoryPoolFactory memoryPoolFactory, ILoggerFactory loggerFactory) { ArgumentNullException.ThrowIfNull(options); ArgumentNullException.ThrowIfNull(loggerFactory); @@ -62,6 +63,8 @@ public HttpSysListener(HttpSysOptions options, ILoggerFactory loggerFactory) throw new PlatformNotSupportedException(); } + MemoryPool = memoryPoolFactory.Create(); + Options = options; Logger = loggerFactory.CreateLogger(); diff --git a/src/Servers/HttpSys/src/MessagePump.cs b/src/Servers/HttpSys/src/MessagePump.cs index 695c45d3b4e9..ae0a8a5a66cc 100644 --- a/src/Servers/HttpSys/src/MessagePump.cs +++ b/src/Servers/HttpSys/src/MessagePump.cs @@ -4,6 +4,7 @@ using System.Diagnostics; using System.Linq; using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Hosting.Server.Features; using Microsoft.AspNetCore.Http.Features; @@ -27,12 +28,13 @@ internal sealed partial class MessagePump : IServer, IServerDelegationFeature private readonly ServerAddressesFeature _serverAddresses; - public MessagePump(IOptions options, ILoggerFactory loggerFactory, IAuthenticationSchemeProvider authentication) + public MessagePump(IOptions options, IMemoryPoolFactory memoryPoolFactory, + ILoggerFactory loggerFactory, IAuthenticationSchemeProvider authentication) { ArgumentNullException.ThrowIfNull(options); ArgumentNullException.ThrowIfNull(loggerFactory); _options = options.Value; - Listener = new HttpSysListener(_options, loggerFactory); + Listener = new HttpSysListener(_options, memoryPoolFactory, loggerFactory); _logger = loggerFactory.CreateLogger(); if (_options.Authentication.Schemes != AuthenticationSchemes.None) diff --git a/src/Servers/HttpSys/src/WebHostBuilderHttpSysExtensions.cs b/src/Servers/HttpSys/src/WebHostBuilderHttpSysExtensions.cs index fb0feaa23609..390e868a6135 100644 --- a/src/Servers/HttpSys/src/WebHostBuilderHttpSysExtensions.cs +++ b/src/Servers/HttpSys/src/WebHostBuilderHttpSysExtensions.cs @@ -1,10 +1,13 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Buffers; using System.Runtime.Versioning; +using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Server.HttpSys; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Hosting; @@ -45,6 +48,8 @@ public static IWebHostBuilder UseHttpSys(this IWebHostBuilder hostBuilder) }; }); services.AddAuthenticationCore(); + + services.TryAddSingleton, DefaultMemoryPoolFactory>(); }); } diff --git a/src/Servers/HttpSys/test/FunctionalTests/Listener/ServerTests.cs b/src/Servers/HttpSys/test/FunctionalTests/Listener/ServerTests.cs index 8a7a9d4da76e..d961ce3a0353 100644 --- a/src/Servers/HttpSys/test/FunctionalTests/Listener/ServerTests.cs +++ b/src/Servers/HttpSys/test/FunctionalTests/Listener/ServerTests.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Buffers; using System.IO; using System.Net; using System.Net.Http; @@ -132,7 +133,7 @@ public void Server_RegisterUnavailablePrefix_ThrowsActionableHttpSysException() var options = new HttpSysOptions(); options.UrlPrefixes.Add(address1); - using var listener = new HttpSysListener(options, new LoggerFactory()); + using var listener = new HttpSysListener(options, new DefaultMemoryPoolFactory(), new LoggerFactory()); var exception = Assert.Throws(() => listener.Start()); diff --git a/src/Servers/HttpSys/test/FunctionalTests/Listener/Utilities.cs b/src/Servers/HttpSys/test/FunctionalTests/Listener/Utilities.cs index 409faaa5b13d..5e9cdadfdb62 100644 --- a/src/Servers/HttpSys/test/FunctionalTests/Listener/Utilities.cs +++ b/src/Servers/HttpSys/test/FunctionalTests/Listener/Utilities.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Buffers; using System.Threading.Tasks; using Microsoft.AspNetCore.HttpSys.Internal; using Microsoft.Extensions.Logging; @@ -47,7 +48,7 @@ internal static HttpSysListener CreateDynamicHttpServer(string basePath, out str var options = new HttpSysOptions(); options.UrlPrefixes.Add(prefix); options.RequestQueueName = prefix.Port; // Convention for use with CreateServerOnExistingQueue - var listener = new HttpSysListener(options, new LoggerFactory()); + var listener = new HttpSysListener(options, new DefaultMemoryPoolFactory(), new LoggerFactory()); try { listener.Start(); @@ -76,7 +77,7 @@ internal static HttpSysListener CreateHttpsServer() internal static HttpSysListener CreateServer(string scheme, string host, int port, string path) { - var listener = new HttpSysListener(new HttpSysOptions(), new LoggerFactory()); + var listener = new HttpSysListener(new HttpSysOptions(), new DefaultMemoryPoolFactory(), new LoggerFactory()); listener.Options.UrlPrefixes.Add(UrlPrefix.Create(scheme, host, port, path)); listener.Start(); return listener; @@ -86,7 +87,7 @@ internal static HttpSysListener CreateServer(Action configureOpt { var options = new HttpSysOptions(); configureOptions(options); - var listener = new HttpSysListener(options, new LoggerFactory()); + var listener = new HttpSysListener(options, new DefaultMemoryPoolFactory(), new LoggerFactory()); listener.Start(); return listener; } diff --git a/src/Servers/HttpSys/test/FunctionalTests/Utilities.cs b/src/Servers/HttpSys/test/FunctionalTests/Utilities.cs index ef28510f1dac..c1b17c72fc59 100644 --- a/src/Servers/HttpSys/test/FunctionalTests/Utilities.cs +++ b/src/Servers/HttpSys/test/FunctionalTests/Utilities.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Buffers; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -112,13 +113,13 @@ internal static IHost CreateDynamicHost(string basePath, out string root, out st } internal static MessagePump CreatePump(ILoggerFactory loggerFactory) - => new MessagePump(Options.Create(new HttpSysOptions()), loggerFactory ?? new LoggerFactory(), new AuthenticationSchemeProvider(Options.Create(new AuthenticationOptions()))); + => new MessagePump(Options.Create(new HttpSysOptions()), new DefaultMemoryPoolFactory(), loggerFactory ?? new LoggerFactory(), new AuthenticationSchemeProvider(Options.Create(new AuthenticationOptions()))); internal static MessagePump CreatePump(Action configureOptions, ILoggerFactory loggerFactory) { var options = new HttpSysOptions(); configureOptions(options); - return new MessagePump(Options.Create(options), loggerFactory ?? new LoggerFactory(), new AuthenticationSchemeProvider(Options.Create(new AuthenticationOptions()))); + return new MessagePump(Options.Create(options), new DefaultMemoryPoolFactory(), loggerFactory ?? new LoggerFactory(), new AuthenticationSchemeProvider(Options.Create(new AuthenticationOptions()))); } internal static IServer CreateDynamicHttpServer(string basePath, out string root, out string baseAddress, Action configureOptions, RequestDelegate app, ILoggerFactory loggerFactory) diff --git a/src/Servers/HttpSys/test/NonHelixTests/Utilities.cs b/src/Servers/HttpSys/test/NonHelixTests/Utilities.cs index aa5880561e54..a276f56d93ca 100644 --- a/src/Servers/HttpSys/test/NonHelixTests/Utilities.cs +++ b/src/Servers/HttpSys/test/NonHelixTests/Utilities.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Buffers; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Hosting.Server.Features; @@ -31,13 +32,13 @@ internal static IServer CreateHttpServer(out string baseAddress, RequestDelegate } internal static MessagePump CreatePump(ILoggerFactory loggerFactory = null) - => new MessagePump(Options.Create(new HttpSysOptions()), loggerFactory ?? new LoggerFactory(), new AuthenticationSchemeProvider(Options.Create(new AuthenticationOptions()))); + => new MessagePump(Options.Create(new HttpSysOptions()), new DefaultMemoryPoolFactory(), loggerFactory ?? new LoggerFactory(), new AuthenticationSchemeProvider(Options.Create(new AuthenticationOptions()))); internal static MessagePump CreatePump(Action configureOptions, ILoggerFactory loggerFactory = null) { var options = new HttpSysOptions(); configureOptions(options); - return new MessagePump(Options.Create(options), loggerFactory ?? new LoggerFactory(), new AuthenticationSchemeProvider(Options.Create(new AuthenticationOptions()))); + return new MessagePump(Options.Create(options), new DefaultMemoryPoolFactory(), loggerFactory ?? new LoggerFactory(), new AuthenticationSchemeProvider(Options.Create(new AuthenticationOptions()))); } internal static IServer CreateDynamicHttpServer(string basePath, out string root, out string baseAddress, Action configureOptions, RequestDelegate app) diff --git a/src/Servers/Kestrel/Kestrel/test/Microsoft.AspNetCore.Server.Kestrel.Tests.csproj b/src/Servers/Kestrel/Kestrel/test/Microsoft.AspNetCore.Server.Kestrel.Tests.csproj index ea39d1e46d6f..301d784d9e6d 100644 --- a/src/Servers/Kestrel/Kestrel/test/Microsoft.AspNetCore.Server.Kestrel.Tests.csproj +++ b/src/Servers/Kestrel/Kestrel/test/Microsoft.AspNetCore.Server.Kestrel.Tests.csproj @@ -18,6 +18,7 @@ + diff --git a/src/Servers/Kestrel/Kestrel/test/WebHostBuilderKestrelExtensionsTests.cs b/src/Servers/Kestrel/Kestrel/test/WebHostBuilderKestrelExtensionsTests.cs index 271d12084893..26a33f387764 100644 --- a/src/Servers/Kestrel/Kestrel/test/WebHostBuilderKestrelExtensionsTests.cs +++ b/src/Servers/Kestrel/Kestrel/test/WebHostBuilderKestrelExtensionsTests.cs @@ -1,13 +1,18 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics; using System.IO.Pipelines; +using System.Reflection; using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting.Server; +using Microsoft.AspNetCore.InternalTesting; using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; +using Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes; using Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.Internal; using Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets; using Microsoft.Extensions.DependencyInjection; @@ -115,10 +120,25 @@ public void ServerIsKestrelServerImpl() Assert.IsType(server.ServiceContext.Metrics); Assert.Equal(PipeScheduler.ThreadPool, server.ServiceContext.Scheduler); Assert.Equal(TimeProvider.System, server.ServiceContext.TimeProvider); + + var handlers = (IHeartbeatHandler[])typeof(Heartbeat).GetField("_callbacks", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(server.ServiceContext.Heartbeat); + Assert.Collection(handlers, + handler => + { + Assert.Equal(typeof(DateHeaderValueManager), handler.GetType()); + }, + handler => + { + Assert.Equal(typeof(ConnectionManager), handler.GetType()); + }, + handler => + { + Assert.Equal(typeof(PinnedBlockMemoryPoolFactory), handler.GetType()); + }); } [Fact] - public void MemoryPoolFactorySetCorrectly() + public void MemoryPoolFactorySetCorrectlyWithSockets() { var hostBuilder = new WebHostBuilder() .UseSockets() @@ -145,4 +165,67 @@ public void MemoryPoolFactorySetCorrectly() Assert.Same(memoryPoolFactory, host.Services.GetRequiredService>().Value.MemoryPoolFactory); } + + [Fact] + public void SocketsHasDefaultMemoryPool() + { + var hostBuilder = new WebHostBuilder() + .UseSockets() + .Configure(app => { }); + + var host = hostBuilder.Build(); + + var memoryPoolFactory = host.Services.GetRequiredService>(); + Assert.IsNotType(memoryPoolFactory); + Assert.Null(host.Services.GetService>()); + + Assert.Same(memoryPoolFactory, host.Services.GetRequiredService>().Value.MemoryPoolFactory); + } + + [ConditionalFact] + [NamedPipesSupported] + public void MemoryPoolFactorySetCorrectlyWithNamedPipes() + { + var hostBuilder = new WebHostBuilder() + .UseNamedPipes() + .UseKestrel() + .Configure(app => { }); + + var host = hostBuilder.Build(); + + var memoryPoolFactory = Assert.IsType(host.Services.GetRequiredService>()); + Assert.Null(host.Services.GetService>()); + + Assert.Same(memoryPoolFactory, host.Services.GetRequiredService>().Value.MemoryPoolFactory); + + // Swap order of UseKestrel and UseNamedPipes + hostBuilder = new WebHostBuilder() + .UseKestrel() + .UseNamedPipes() + .Configure(app => { }); + + host = hostBuilder.Build(); + + memoryPoolFactory = Assert.IsType(host.Services.GetRequiredService>()); + Assert.Null(host.Services.GetService>()); + + Assert.Same(memoryPoolFactory, host.Services.GetRequiredService>().Value.MemoryPoolFactory); + } + + [ConditionalFact] + [NamedPipesSupported] + public void NamedPipesHasDefaultMemoryPool() + { + var hostBuilder = new WebHostBuilder() + .UseNamedPipes() + .Configure(app => { }); + + var host = hostBuilder.Build(); + + var memoryPoolFactory = host.Services.GetRequiredService>(); + Assert.IsNotType(memoryPoolFactory); + Assert.Null(host.Services.GetService>()); + + Assert.Same(memoryPoolFactory, host.Services.GetRequiredService>().Value.MemoryPoolFactory); + } } diff --git a/src/Shared/Buffers.MemoryPool/DefaultMemoryPoolFactory.cs b/src/Shared/Buffers.MemoryPool/DefaultMemoryPoolFactory.cs new file mode 100644 index 000000000000..3eaa6914b362 --- /dev/null +++ b/src/Shared/Buffers.MemoryPool/DefaultMemoryPoolFactory.cs @@ -0,0 +1,68 @@ +// 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.Diagnostics; +using System.Diagnostics.Metrics; +using Microsoft.AspNetCore.Connections; + +namespace System.Buffers; + +internal sealed class DefaultMemoryPoolFactory : IMemoryPoolFactory, IDisposable +{ + private readonly IMeterFactory _meterFactory; + private readonly ConcurrentDictionary _pools = new(); + private readonly PeriodicTimer _timer; + + public DefaultMemoryPoolFactory(IMeterFactory? meterFactory = null) + { + _meterFactory = meterFactory ?? NoopMeterFactory.Instance; + _timer = new PeriodicTimer(TimeSpan.FromSeconds(10)); + _ = Task.Run(async () => + { + try + { + while (await _timer.WaitForNextTickAsync()) + { + foreach (var pool in _pools.Keys) + { + pool.PerformEviction(); + } + } + } + catch (Exception ex) + { + Debug.WriteLine(ex); + } + }); + } + + public MemoryPool Create() + { + var pool = new PinnedBlockMemoryPool(_meterFactory); + pool.DisposeCallback = (self) => + { + _pools.TryRemove(self, out _); + }; + + _pools.TryAdd(pool, pool); + + return pool; + } + + public void Dispose() + { + _timer.Dispose(); + } + + private sealed class NoopMeterFactory : IMeterFactory + { + public static NoopMeterFactory Instance = new NoopMeterFactory(); + + public Meter Create(MeterOptions options) => new Meter(options); + + public void Dispose() + { + } + } +} diff --git a/src/Shared/Buffers.MemoryPool/PinnedBlockMemoryPool.cs b/src/Shared/Buffers.MemoryPool/PinnedBlockMemoryPool.cs index faee67df1d73..77e2e167b665 100644 --- a/src/Shared/Buffers.MemoryPool/PinnedBlockMemoryPool.cs +++ b/src/Shared/Buffers.MemoryPool/PinnedBlockMemoryPool.cs @@ -245,10 +245,10 @@ public void Dispose() static void ScalableCount(ref uint counter) { // Start using random for counting after 2^12 (4096) - const int threshold = 8; + //const int threshold = 12; uint count = counter; uint delta = 1; -#if true +#if false if (count > 0) { int logCount = 31 - (int)uint.LeadingZeroCount(count); From 9ae6a5b0e3dfc673a4a76f37c35e68cf56c1da7b Mon Sep 17 00:00:00 2001 From: Brennan Date: Fri, 18 Apr 2025 14:27:33 -0700 Subject: [PATCH 11/12] iis --- src/Servers/IIS/IIS/src/Core/IISHttpServer.cs | 13 ++++--------- .../IIS/IIS/src/WebHostBuilderIISExtensions.cs | 5 +++++ .../src/Internal/PinnedBlockMemoryPoolFactory.cs | 5 +++-- .../Buffers.MemoryPool/DefaultMemoryPoolFactory.cs | 7 +++++-- 4 files changed, 17 insertions(+), 13 deletions(-) diff --git a/src/Servers/IIS/IIS/src/Core/IISHttpServer.cs b/src/Servers/IIS/IIS/src/Core/IISHttpServer.cs index a3f6c8349dd2..df0484310b0e 100644 --- a/src/Servers/IIS/IIS/src/Core/IISHttpServer.cs +++ b/src/Servers/IIS/IIS/src/Core/IISHttpServer.cs @@ -3,10 +3,10 @@ using System.Buffers; using System.Diagnostics; -using System.Diagnostics.Metrics; using System.Runtime.InteropServices; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Hosting.Server.Features; using Microsoft.AspNetCore.Http.Features; @@ -19,17 +19,10 @@ namespace Microsoft.AspNetCore.Server.IIS.Core; internal sealed class IISHttpServer : IServer { - internal sealed class DummyMeterFactory : IMeterFactory - { - public Meter Create(MeterOptions options) => new Meter(options); - - public void Dispose() { } - } - private const string WebSocketVersionString = "WEBSOCKET_VERSION"; private IISContextFactory? _iisContextFactory; - private readonly MemoryPool _memoryPool = new PinnedBlockMemoryPool(new DummyMeterFactory()); + private readonly MemoryPool _memoryPool; private GCHandle _httpServerHandle; private readonly IHostApplicationLifetime _applicationLifetime; private readonly ILogger _logger; @@ -68,10 +61,12 @@ public IISHttpServer( IHostApplicationLifetime applicationLifetime, IAuthenticationSchemeProvider authentication, IConfiguration configuration, + IMemoryPoolFactory memoryPoolFactory, IOptions options, ILogger logger ) { + _memoryPool = memoryPoolFactory.Create(); _nativeApplication = nativeApplication; _applicationLifetime = applicationLifetime; _logger = logger; diff --git a/src/Servers/IIS/IIS/src/WebHostBuilderIISExtensions.cs b/src/Servers/IIS/IIS/src/WebHostBuilderIISExtensions.cs index 302f1af5a6c4..c7d9aaf39a13 100644 --- a/src/Servers/IIS/IIS/src/WebHostBuilderIISExtensions.cs +++ b/src/Servers/IIS/IIS/src/WebHostBuilderIISExtensions.cs @@ -1,11 +1,14 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Buffers; using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Server.IIS; using Microsoft.AspNetCore.Server.IIS.Core; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; namespace Microsoft.AspNetCore.Hosting; @@ -53,6 +56,8 @@ public static IWebHostBuilder UseIIS(this IWebHostBuilder hostBuilder) options.IisMaxRequestSizeLimit = iisConfigData.maxRequestBodySize; } ); + + services.TryAddSingleton, DefaultMemoryPoolFactory>(); }); } diff --git a/src/Servers/Kestrel/Core/src/Internal/PinnedBlockMemoryPoolFactory.cs b/src/Servers/Kestrel/Core/src/Internal/PinnedBlockMemoryPoolFactory.cs index b01ae01affaa..cc9a270c016f 100644 --- a/src/Servers/Kestrel/Core/src/Internal/PinnedBlockMemoryPoolFactory.cs +++ b/src/Servers/Kestrel/Core/src/Internal/PinnedBlockMemoryPoolFactory.cs @@ -24,13 +24,14 @@ public PinnedBlockMemoryPoolFactory(IMeterFactory meterFactory, TimeProvider? ti public MemoryPool Create() { var pool = new PinnedBlockMemoryPool(_meterFactory); + + _pools.TryAdd(pool, pool); + pool.DisposeCallback = (self) => { _pools.TryRemove(self, out _); }; - _pools.TryAdd(pool, pool); - return pool; } diff --git a/src/Shared/Buffers.MemoryPool/DefaultMemoryPoolFactory.cs b/src/Shared/Buffers.MemoryPool/DefaultMemoryPoolFactory.cs index 3eaa6914b362..ed43227f731d 100644 --- a/src/Shared/Buffers.MemoryPool/DefaultMemoryPoolFactory.cs +++ b/src/Shared/Buffers.MemoryPool/DefaultMemoryPoolFactory.cs @@ -8,6 +8,8 @@ namespace System.Buffers; +#nullable enable + internal sealed class DefaultMemoryPoolFactory : IMemoryPoolFactory, IDisposable { private readonly IMeterFactory _meterFactory; @@ -40,13 +42,14 @@ public DefaultMemoryPoolFactory(IMeterFactory? meterFactory = null) public MemoryPool Create() { var pool = new PinnedBlockMemoryPool(_meterFactory); + + _pools.TryAdd(pool, pool); + pool.DisposeCallback = (self) => { _pools.TryRemove(self, out _); }; - _pools.TryAdd(pool, pool); - return pool; } From fdfaad579bfe3b417f16148973ee6c674da348f6 Mon Sep 17 00:00:00 2001 From: Brennan Date: Tue, 22 Apr 2025 10:24:00 -0700 Subject: [PATCH 12/12] fb --- .../Internal/PinnedBlockMemoryPoolFactory.cs | 6 +++--- .../DefaultMemoryPoolFactory.cs | 14 +++++++------ .../PinnedBlockMemoryPool.cs | 20 +++++++++++++------ 3 files changed, 25 insertions(+), 15 deletions(-) diff --git a/src/Servers/Kestrel/Core/src/Internal/PinnedBlockMemoryPoolFactory.cs b/src/Servers/Kestrel/Core/src/Internal/PinnedBlockMemoryPoolFactory.cs index cc9a270c016f..9b9a1b768aff 100644 --- a/src/Servers/Kestrel/Core/src/Internal/PinnedBlockMemoryPoolFactory.cs +++ b/src/Servers/Kestrel/Core/src/Internal/PinnedBlockMemoryPoolFactory.cs @@ -27,10 +27,10 @@ public MemoryPool Create() _pools.TryAdd(pool, pool); - pool.DisposeCallback = (self) => + pool.OnPoolDisposed(static (state, self) => { - _pools.TryRemove(self, out _); - }; + ((ConcurrentDictionary)state!).TryRemove(self, out _); + }, _pools); return pool; } diff --git a/src/Shared/Buffers.MemoryPool/DefaultMemoryPoolFactory.cs b/src/Shared/Buffers.MemoryPool/DefaultMemoryPoolFactory.cs index ed43227f731d..1df976b30828 100644 --- a/src/Shared/Buffers.MemoryPool/DefaultMemoryPoolFactory.cs +++ b/src/Shared/Buffers.MemoryPool/DefaultMemoryPoolFactory.cs @@ -10,17 +10,18 @@ namespace System.Buffers; #nullable enable -internal sealed class DefaultMemoryPoolFactory : IMemoryPoolFactory, IDisposable +internal sealed class DefaultMemoryPoolFactory : IMemoryPoolFactory, IAsyncDisposable { private readonly IMeterFactory _meterFactory; private readonly ConcurrentDictionary _pools = new(); private readonly PeriodicTimer _timer; + private readonly Task _timerTask; public DefaultMemoryPoolFactory(IMeterFactory? meterFactory = null) { _meterFactory = meterFactory ?? NoopMeterFactory.Instance; _timer = new PeriodicTimer(TimeSpan.FromSeconds(10)); - _ = Task.Run(async () => + _timerTask = Task.Run(async () => { try { @@ -45,17 +46,18 @@ public MemoryPool Create() _pools.TryAdd(pool, pool); - pool.DisposeCallback = (self) => + pool.OnPoolDisposed(static (state, self) => { - _pools.TryRemove(self, out _); - }; + ((ConcurrentDictionary)state!).TryRemove(self, out _); + }, _pools); return pool; } - public void Dispose() + public async ValueTask DisposeAsync() { _timer.Dispose(); + await _timerTask; } private sealed class NoopMeterFactory : IMeterFactory diff --git a/src/Shared/Buffers.MemoryPool/PinnedBlockMemoryPool.cs b/src/Shared/Buffers.MemoryPool/PinnedBlockMemoryPool.cs index 77e2e167b665..268514d8bac9 100644 --- a/src/Shared/Buffers.MemoryPool/PinnedBlockMemoryPool.cs +++ b/src/Shared/Buffers.MemoryPool/PinnedBlockMemoryPool.cs @@ -3,6 +3,7 @@ using System.Collections.Concurrent; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Metrics; #nullable enable @@ -30,11 +31,6 @@ internal sealed class PinnedBlockMemoryPool : MemoryPool, IThreadPoolWorkI /// public static int BlockSize => _blockSize; - /// - /// Optional callback to call when pool is being disposed. - /// - public Action? DisposeCallback { get; set; } - /// /// Thread-safe collection of blocks which are currently in the pool. A slab will pre-allocate all of the block tracking objects /// and add them to this collection. When memory is requested it is taken from here first, and when it is returned it is re-added. @@ -57,6 +53,9 @@ internal sealed class PinnedBlockMemoryPool : MemoryPool, IThreadPoolWorkI private readonly object _disposeSync = new object(); + private Action? _onPoolDisposed; + private object? _onPoolDisposedState; + /// /// This default value passed in to Rent to use the default value for the pool. /// @@ -72,6 +71,15 @@ public PinnedBlockMemoryPool(IMeterFactory meterFactory) _metrics = new(meterFactory); } + /// + /// Register a callback to call when the pool is being disposed. + /// + public void OnPoolDisposed(Action onPoolDisposed, object? state = null) + { + _onPoolDisposed = onPoolDisposed; + _onPoolDisposedState = state; + } + public override IMemoryOwner Rent(int size = AnySize) { if (size > _blockSize) @@ -214,7 +222,7 @@ protected override void Dispose(bool disposing) { _isDisposed = true; - DisposeCallback?.Invoke(this); + _onPoolDisposed?.Invoke(_onPoolDisposedState, this); if (disposing) {