diff --git a/src/Servers/Connections.Abstractions/src/IMemoryPoolFactory.cs b/src/Servers/Connections.Abstractions/src/IMemoryPoolFactory.cs new file mode 100644 index 000000000000..16ffcee42687 --- /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; + +/// +/// Interface for creating memory pools. +/// +public interface IMemoryPoolFactory +{ + /// + /// Creates a new instance of a memory pool. + /// + /// A new memory pool instance. + 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 7dc5c58110bf..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 +1,3 @@ #nullable enable +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 7dc5c58110bf..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 +1,3 @@ #nullable enable +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 7dc5c58110bf..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 +1,3 @@ #nullable enable +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 7dc5c58110bf..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 +1,3 @@ #nullable enable +Microsoft.AspNetCore.Connections.IMemoryPoolFactory +Microsoft.AspNetCore.Connections.IMemoryPoolFactory.Create() -> System.Buffers.MemoryPool! diff --git a/src/Servers/HttpSys/src/HttpSysListener.cs b/src/Servers/HttpSys/src/HttpSysListener.cs index 933c627cd56e..c84a86382198 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 Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.HttpSys.Internal; using Microsoft.AspNetCore.WebUtilities; @@ -33,7 +34,7 @@ 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 MemoryPool MemoryPool { get; } private volatile State _state; // m_State is set only within lock blocks, but often read outside locks. @@ -44,7 +45,7 @@ internal sealed partial class HttpSysListener : IDisposable private readonly object _internalLock; - public HttpSysListener(HttpSysOptions options, ILoggerFactory loggerFactory) + public HttpSysListener(HttpSysOptions options, IMemoryPoolFactory memoryPoolFactory, ILoggerFactory loggerFactory) { ArgumentNullException.ThrowIfNull(options); ArgumentNullException.ThrowIfNull(loggerFactory); @@ -54,6 +55,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..cf768cc64469 100644 --- a/src/Servers/HttpSys/src/WebHostBuilderHttpSysExtensions.cs +++ b/src/Servers/HttpSys/src/WebHostBuilderHttpSysExtensions.cs @@ -2,9 +2,11 @@ // The .NET Foundation licenses this file to you under the MIT license. 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 +47,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/IIS/IIS/src/Core/IISHttpServer.cs b/src/Servers/IIS/IIS/src/Core/IISHttpServer.cs index 6abaa5b2c0db..df0484310b0e 100644 --- a/src/Servers/IIS/IIS/src/Core/IISHttpServer.cs +++ b/src/Servers/IIS/IIS/src/Core/IISHttpServer.cs @@ -6,6 +6,7 @@ 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; @@ -21,7 +22,7 @@ internal sealed class IISHttpServer : IServer private const string WebSocketVersionString = "WEBSOCKET_VERSION"; private IISContextFactory? _iisContextFactory; - private readonly MemoryPool _memoryPool = new PinnedBlockMemoryPool(); + private readonly MemoryPool _memoryPool; private GCHandle _httpServerHandle; private readonly IHostApplicationLifetime _applicationLifetime; private readonly ILogger _logger; @@ -60,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..b151931e0ae6 100644 --- a/src/Servers/IIS/IIS/src/WebHostBuilderIISExtensions.cs +++ b/src/Servers/IIS/IIS/src/WebHostBuilderIISExtensions.cs @@ -2,10 +2,12 @@ // The .NET Foundation licenses this file to you under the MIT license. 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 +55,8 @@ public static IWebHostBuilder UseIIS(this IWebHostBuilder hostBuilder) options.IisMaxRequestSizeLimit = iisConfigData.maxRequestBodySize; } ); + + services.TryAddSingleton, DefaultMemoryPoolFactory>(); }); } 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, IHeartbeatHandler +{ + private readonly IMeterFactory _meterFactory; + private readonly ILogger? _logger; + private readonly TimeProvider _timeProvider; + // micro-optimization: Using nuint as the value type to avoid GC write barriers; could replace with ConcurrentHashSet if that becomes available + private readonly ConcurrentDictionary _pools = new(); + + public PinnedBlockMemoryPoolFactory(IMeterFactory meterFactory, TimeProvider? timeProvider = null, ILogger? logger = null) + { + _timeProvider = timeProvider ?? TimeProvider.System; + _meterFactory = meterFactory; + _logger = logger; + } + + public MemoryPool Create() + { + var pool = new PinnedBlockMemoryPool(_meterFactory, _logger); + + _pools.TryAdd(pool, nuint.Zero); + + pool.OnPoolDisposed(static (state, self) => + { + ((ConcurrentDictionary)state!).TryRemove(self, out _); + }, _pools); + + return pool; + } + + public void OnHeartbeat() + { + var now = _timeProvider.GetUtcNow(); + foreach (var pool in _pools) + { + pool.Key.TryScheduleEviction(now); + } + } +} 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..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 @@ -1,4 +1,4 @@ - + Core components of ASP.NET Core Kestrel cross-platform web server. @@ -37,6 +37,7 @@ + diff --git a/src/Servers/Kestrel/Core/test/DiagnosticMemoryPoolTests.cs b/src/Servers/Kestrel/Core/test/DiagnosticMemoryPoolTests.cs index 100f50ad7645..adceaa3a5c0e 100644 --- a/src/Servers/Kestrel/Core/test/DiagnosticMemoryPoolTests.cs +++ b/src/Servers/Kestrel/Core/test/DiagnosticMemoryPoolTests.cs @@ -1,10 +1,11 @@ -// 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; using System.Buffers; using System.Runtime.InteropServices; using System.Threading.Tasks; +using Microsoft.AspNetCore; using Xunit; namespace Microsoft.Extensions.Internal.Test; diff --git a/src/Servers/Kestrel/Core/test/Http1/Http1ConnectionTestsBase.cs b/src/Servers/Kestrel/Core/test/Http1/Http1ConnectionTestsBase.cs index 3fd5631ed42c..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 = 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/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/Core/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests.csproj b/src/Servers/Kestrel/Core/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests.csproj index 509856074d7c..37aea90bcf3c 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 @@ - @@ -29,6 +28,7 @@ + diff --git a/src/Servers/Kestrel/Core/test/PinnedBlockMemoryPoolFactoryTests.cs b/src/Servers/Kestrel/Core/test/PinnedBlockMemoryPoolFactoryTests.cs new file mode 100644 index 000000000000..4d23015459eb --- /dev/null +++ b/src/Servers/Kestrel/Core/test/PinnedBlockMemoryPoolFactoryTests.cs @@ -0,0 +1,113 @@ +// 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) + { + // Because the eviction happens on a thread pool thread, we need to wait for it to complete + // and the only way to do that (without adding a test hook in the pool code) is to delay. + // But we don't want to add an arbitrary delay, so we do a short delay with block count checks + // to reduce the wait time. + var maxWait = TimeSpan.FromSeconds(5); + while (pool.BlockCount() > previousCount - (previousCount / 30) && maxWait > TimeSpan.Zero) + { + await Task.Delay(50); + maxWait -= TimeSpan.FromMilliseconds(50); + } + + // Assert that the block count has decreased by 3.3-10%. + // This relies on the current implementation of eviction logic which may change in the future. + 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..f35fd05c36ef 100644 --- a/src/Servers/Kestrel/Core/test/PinnedBlockMemoryPoolTests.cs +++ b/src/Servers/Kestrel/Core/test/PinnedBlockMemoryPoolTests.cs @@ -1,8 +1,12 @@ -// 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; +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.InternalTesting; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal; +using Microsoft.Extensions.Diagnostics.Metrics.Testing; +using Microsoft.Extensions.Time.Testing; namespace Microsoft.Extensions.Internal.Test; @@ -25,4 +29,350 @@ 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 + var time = DateTime.UtcNow; + Assert.False(memoryPool.TryScheduleEviction(time)); + + Assert.True(memoryPool.TryScheduleEviction(time.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(time.AddSeconds(10))); + + previousCount = memoryPool.BlockCount(); + Assert.True(memoryPool.TryScheduleEviction(time.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)); + } + + [Fact] + public void CurrentMemoryMetricTracksPooledMemory() + { + var testMeterFactory = new TestMeterFactory(); + using var currentMemoryMetric = new MetricCollector(testMeterFactory, "Microsoft.AspNetCore.MemoryPool", "aspnetcore.memorypool.current_memory"); + + var pool = new PinnedBlockMemoryPool(testMeterFactory); + + Assert.Empty(currentMemoryMetric.GetMeasurementSnapshot()); + + var mem = pool.Rent(); + mem.Dispose(); + + Assert.Collection(currentMemoryMetric.GetMeasurementSnapshot(), m => Assert.Equal(PinnedBlockMemoryPool.BlockSize, m.Value)); + + mem = pool.Rent(); + + Assert.Equal(-1 * PinnedBlockMemoryPool.BlockSize, currentMemoryMetric.LastMeasurement.Value); + + var mem2 = pool.Rent(); + + mem.Dispose(); + mem2.Dispose(); + + Assert.Equal(2 * PinnedBlockMemoryPool.BlockSize, currentMemoryMetric.GetMeasurementSnapshot().EvaluateAsCounter()); + + // Eviction after returning everything to reset internal counters + pool.PerformEviction(); + + // Trigger eviction + pool.PerformEviction(); + + // Verify eviction updates current memory metric + Assert.Equal(0, currentMemoryMetric.GetMeasurementSnapshot().EvaluateAsCounter()); + } + + [Fact] + public void TotalAllocatedMetricTracksAllocatedMemory() + { + var testMeterFactory = new TestMeterFactory(); + using var totalMemoryMetric = new MetricCollector(testMeterFactory, "Microsoft.AspNetCore.MemoryPool", "aspnetcore.memorypool.total_allocated"); + + var pool = new PinnedBlockMemoryPool(testMeterFactory); + + Assert.Empty(totalMemoryMetric.GetMeasurementSnapshot()); + + var mem1 = pool.Rent(); + var mem2 = pool.Rent(); + + // Each Rent that allocates a new block should increment total memory by block size + Assert.Equal(2 * PinnedBlockMemoryPool.BlockSize, totalMemoryMetric.GetMeasurementSnapshot().EvaluateAsCounter()); + + mem1.Dispose(); + mem2.Dispose(); + + // Disposing (returning) blocks does not affect total memory + Assert.Equal(2 * PinnedBlockMemoryPool.BlockSize, totalMemoryMetric.GetMeasurementSnapshot().EvaluateAsCounter()); + } + + [Fact] + public void TotalRentedMetricTracksRentOperations() + { + var testMeterFactory = new TestMeterFactory(); + using var rentMetric = new MetricCollector(testMeterFactory, "Microsoft.AspNetCore.MemoryPool", "aspnetcore.memorypool.total_rented"); + + var pool = new PinnedBlockMemoryPool(testMeterFactory); + + Assert.Empty(rentMetric.GetMeasurementSnapshot()); + + var mem1 = pool.Rent(); + var mem2 = pool.Rent(); + + // Each Rent should record the size of the block rented + Assert.Collection(rentMetric.GetMeasurementSnapshot(), + m => Assert.Equal(PinnedBlockMemoryPool.BlockSize, m.Value), + m => Assert.Equal(PinnedBlockMemoryPool.BlockSize, m.Value)); + + mem1.Dispose(); + mem2.Dispose(); + + // Disposing does not affect rent metric + Assert.Equal(2 * PinnedBlockMemoryPool.BlockSize, rentMetric.GetMeasurementSnapshot().EvaluateAsCounter()); + } + + [Fact] + public void EvictedMemoryMetricTracksEvictedMemory() + { + var testMeterFactory = new TestMeterFactory(); + using var evictMetric = new MetricCollector(testMeterFactory, "Microsoft.AspNetCore.MemoryPool", "aspnetcore.memorypool.evicted_memory"); + + var pool = new PinnedBlockMemoryPool(testMeterFactory); + + // Fill the pool with some blocks + var blocks = new List>(); + for (int i = 0; i < 10; i++) + { + blocks.Add(pool.Rent()); + } + foreach (var block in blocks) + { + block.Dispose(); + } + blocks.Clear(); + + Assert.Empty(evictMetric.GetMeasurementSnapshot()); + + // Eviction after returning everything to reset internal counters + pool.PerformEviction(); + + // Trigger eviction + pool.PerformEviction(); + + // At least some blocks should be evicted, each eviction records block size + Assert.NotEmpty(evictMetric.GetMeasurementSnapshot()); + foreach (var measurement in evictMetric.GetMeasurementSnapshot()) + { + Assert.Equal(PinnedBlockMemoryPool.BlockSize, measurement.Value); + } + } + + // Smoke test to ensure that metrics are aggregated across multiple pools if the same meter factory is used + [Fact] + public void MetricsAreAggregatedAcrossPoolsWithSameMeterFactory() + { + var testMeterFactory = new TestMeterFactory(); + using var rentMetric = new MetricCollector(testMeterFactory, "Microsoft.AspNetCore.MemoryPool", "aspnetcore.memorypool.total_rented"); + + var pool1 = new PinnedBlockMemoryPool(testMeterFactory); + var pool2 = new PinnedBlockMemoryPool(testMeterFactory); + + var mem1 = pool1.Rent(); + var mem2 = pool2.Rent(); + + // Both pools should contribute to the same metric stream + Assert.Equal(2 * PinnedBlockMemoryPool.BlockSize, rentMetric.GetMeasurementSnapshot().EvaluateAsCounter()); + + mem1.Dispose(); + mem2.Dispose(); + + // Renting and returning from both pools should not interfere with metric collection + var mem3 = pool1.Rent(); + var mem4 = pool2.Rent(); + + Assert.Equal(4 * PinnedBlockMemoryPool.BlockSize, rentMetric.GetMeasurementSnapshot().EvaluateAsCounter()); + + mem3.Dispose(); + mem4.Dispose(); + } } 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 914fa9bb5f58..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 = 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/src/WebHostBuilderKestrelExtensions.cs b/src/Servers/Kestrel/Kestrel/src/WebHostBuilderKestrelExtensions.cs index 7186715a6c5f..0869ba8169b8 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,15 +78,17 @@ 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.AddTransient, KestrelServerOptionsSetup>(); 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/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 b24da893ab53..26a33f387764 100644 --- a/src/Servers/Kestrel/Kestrel/test/WebHostBuilderKestrelExtensionsTests.cs +++ b/src/Servers/Kestrel/Kestrel/test/WebHostBuilderKestrelExtensionsTests.cs @@ -1,17 +1,22 @@ // 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.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; -using Xunit; +using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Server.Kestrel.Tests; @@ -115,5 +120,112 @@ 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 MemoryPoolFactorySetCorrectlyWithSockets() + { + 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); + } + + [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/Servers/Kestrel/Transport.NamedPipes/src/Internal/NamedPipeConnectionListener.cs b/src/Servers/Kestrel/Transport.NamedPipes/src/Internal/NamedPipeConnectionListener.cs index ab172bc28476..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(); + _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/Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.csproj b/src/Servers/Kestrel/Transport.NamedPipes/src/Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.csproj index e7b0816ec136..61735851bb84 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 a1d7d47f4854..133dc1ca6f8b 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; } = DefaultSimpleMemoryPoolFactory.Instance; } diff --git a/src/Servers/Kestrel/Transport.NamedPipes/src/WebHostBuilderNamedPipeExtensions.cs b/src/Servers/Kestrel/Transport.NamedPipes/src/WebHostBuilderNamedPipeExtensions.cs index 53f94f801e74..3b64cc2c4f7e 100644 --- a/src/Servers/Kestrel/Transport.NamedPipes/src/WebHostBuilderNamedPipeExtensions.cs +++ b/src/Servers/Kestrel/Transport.NamedPipes/src/WebHostBuilderNamedPipeExtensions.cs @@ -33,7 +33,15 @@ public static IWebHostBuilder UseNamedPipes(this IWebHostBuilder hostBuilder) { services.TryAddSingleton(); services.AddSingleton(); + + 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; + }); }); + return hostBuilder; } diff --git a/src/Servers/Kestrel/Transport.Sockets/src/Client/SocketConnectionFactory.cs b/src/Servers/Kestrel/Transport.Sockets/src/Client/SocketConnectionFactory.cs index db77bbe811d3..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(); + _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/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..9f225bde1142 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,12 +12,12 @@ - + diff --git a/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionContextFactory.cs b/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionContextFactory.cs index db98e36842e1..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(); + 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(); + 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 403f6fc108de..e77950c87c8e 100644 --- a/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionFactoryOptions.cs +++ b/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionFactoryOptions.cs @@ -1,7 +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 Microsoft.AspNetCore.Connections; +using Microsoft.AspNetCore.Server.Kestrel.Internal; namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets; @@ -67,5 +68,5 @@ internal SocketConnectionFactoryOptions(SocketTransportOptions transportOptions) /// public bool UnsafePreferInlineScheduling { get; set; } - internal Func> MemoryPoolFactory { get; set; } = PinnedBlockMemoryPoolFactory.Create; + internal IMemoryPoolFactory MemoryPoolFactory { get; set; } = DefaultSimpleMemoryPoolFactory.Instance; } diff --git a/src/Servers/Kestrel/Transport.Sockets/src/SocketTransportOptions.cs b/src/Servers/Kestrel/Transport.Sockets/src/SocketTransportOptions.cs index ad1a877df63c..822e665b1795 100644 --- a/src/Servers/Kestrel/Transport.Sockets/src/SocketTransportOptions.cs +++ b/src/Servers/Kestrel/Transport.Sockets/src/SocketTransportOptions.cs @@ -1,10 +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.Net; using System.Net.Sockets; using Microsoft.AspNetCore.Connections; +using Microsoft.AspNetCore.Server.Kestrel.Internal; namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets; @@ -166,5 +166,5 @@ public static Socket CreateDefaultBoundListenSocket(EndPoint endpoint) return listenSocket; } - internal Func> MemoryPoolFactory { get; set; } = System.Buffers.PinnedBlockMemoryPoolFactory.Create; + internal IMemoryPoolFactory MemoryPoolFactory { get; set; } = DefaultSimpleMemoryPoolFactory.Instance; } diff --git a/src/Servers/Kestrel/Transport.Sockets/src/WebHostBuilderSocketExtensions.cs b/src/Servers/Kestrel/Transport.Sockets/src/WebHostBuilderSocketExtensions.cs index 046522e22048..c5d7c54b77f3 100644 --- a/src/Servers/Kestrel/Transport.Sockets/src/WebHostBuilderSocketExtensions.cs +++ b/src/Servers/Kestrel/Transport.Sockets/src/WebHostBuilderSocketExtensions.cs @@ -2,8 +2,10 @@ // 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.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; namespace Microsoft.AspNetCore.Hosting; @@ -25,7 +27,14 @@ public static IWebHostBuilder UseSockets(this IWebHostBuilder hostBuilder) { return hostBuilder.ConfigureServices(services => { - services.AddSingleton(); + services.TryAddSingleton(); + + services.TryAddSingleton, DefaultSimpleMemoryPoolFactory>(); + 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/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 16697ca82441..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 = 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 356953b8cafa..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 = 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 fa8d0dddf4e8..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 = 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 a5c18cbadf4d..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 = 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 31db6447384b..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 = 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/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/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 819ca5441d3b..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 = 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 983f39f28eed..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 = 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/DefaultSimpleMemoryPoolFactory.cs b/src/Servers/Kestrel/shared/DefaultSimpleMemoryPoolFactory.cs new file mode 100644 index 000000000000..d3c4f94c333c --- /dev/null +++ b/src/Servers/Kestrel/shared/DefaultSimpleMemoryPoolFactory.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.Internal; + +internal sealed class DefaultSimpleMemoryPoolFactory : IMemoryPoolFactory +{ + public static DefaultSimpleMemoryPoolFactory Instance { get; } = new DefaultSimpleMemoryPoolFactory(); + + public MemoryPool Create() + { + return MemoryPool.Shared; + } +} diff --git a/src/Servers/Kestrel/shared/test/Http3/Http3InMemory.cs b/src/Servers/Kestrel/shared/test/Http3/Http3InMemory.cs index 64ebcdb07b41..55321b0c33b4 100644 --- a/src/Servers/Kestrel/shared/test/Http3/Http3InMemory.cs +++ b/src/Servers/Kestrel/shared/test/Http3/Http3InMemory.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; using System.Buffers; using System.Collections.Concurrent; using System.Diagnostics; @@ -74,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 = 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 3baf69b6348a..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 ?? 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 85692f1fe341..2987d52f5461 100644 --- a/src/Servers/Kestrel/shared/test/TestServiceContext.cs +++ b/src/Servers/Kestrel/shared/test/TestServiceContext.cs @@ -3,6 +3,7 @@ using System.Buffers; 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; @@ -73,7 +74,19 @@ private void Initialize(ILoggerFactory loggerFactory, KestrelTrace kestrelTrace, public FakeTimeProvider FakeTimeProvider { get; set; } - public Func> MemoryPoolFactory { get; set; } = System.Buffers.PinnedBlockMemoryPoolFactory.Create; + public IMemoryPoolFactory MemoryPoolFactory { get; set; } = new WrappingMemoryPoolFactory(() => TestMemoryPoolFactory.Create()); public string DateHeaderValue => DateHeaderValueManager.GetDateHeaderValues().String; + + internal sealed class WrappingMemoryPoolFactory : IMemoryPoolFactory + { + private readonly Func> _memoryPoolFactory; + + public WrappingMemoryPoolFactory(Func> memoryPoolFactory) + { + _memoryPoolFactory = 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 d27bf8e11635..91a266249cd1 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; diff --git a/src/Servers/Kestrel/shared/test/TransportTestHelpers/TestServer.cs b/src/Servers/Kestrel/shared/test/TransportTestHelpers/TestServer.cs index 4c1813ed371a..88ad8f350c17 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..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 PinnedBlockMemoryPool() + MemoryPoolFactory = new TestServiceContext.WrappingMemoryPoolFactory(() => TestMemoryPoolFactory.CreatePinnedBlockMemoryPool()), }; ThrowOnUngracefulShutdown = false; diff --git a/src/Servers/Kestrel/test/FunctionalTests/MaxRequestBufferSizeTests.cs b/src/Servers/Kestrel/test/FunctionalTests/MaxRequestBufferSizeTests.cs index 309c89be5a48..3ddaa21972e5 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/Http2/Http2TestBase.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs index e4d452b91aa0..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 = 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/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 ef415b5a0623..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 = 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 = PinnedBlockMemoryPoolFactory.CreatePinnedBlockMemoryPool + MemoryPoolFactory = new TestServiceContext.WrappingMemoryPoolFactory(() => TestMemoryPoolFactory.CreatePinnedBlockMemoryPool()) }; var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/TestTransport/TestServer.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/TestTransport/TestServer.cs index 0ee3a41d48ec..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 + diff --git a/src/Servers/Kestrel/test/Sockets.FunctionalTests/TransportSelector.cs b/src/Servers/Kestrel/test/Sockets.FunctionalTests/TransportSelector.cs index f5933759a282..0584487755c7 100644 --- a/src/Servers/Kestrel/test/Sockets.FunctionalTests/TransportSelector.cs +++ b/src/Servers/Kestrel/test/Sockets.FunctionalTests/TransportSelector.cs @@ -1,8 +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; -using System.Buffers; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Hosting; @@ -10,18 +8,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests; public static class TransportSelector { - public static IHostBuilder GetHostBuilder(Func> 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/DefaultMemoryPoolFactory.cs b/src/Shared/Buffers.MemoryPool/DefaultMemoryPoolFactory.cs new file mode 100644 index 000000000000..347debba8212 --- /dev/null +++ b/src/Shared/Buffers.MemoryPool/DefaultMemoryPoolFactory.cs @@ -0,0 +1,66 @@ +// 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.Diagnostics; +using System.Diagnostics.Metrics; +using Microsoft.AspNetCore.Connections; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore; + +#nullable enable + +internal sealed class DefaultMemoryPoolFactory : IMemoryPoolFactory, IAsyncDisposable +{ + private readonly IMeterFactory? _meterFactory; + private readonly ConcurrentDictionary _pools = new(); + private readonly PeriodicTimer _timer; + private readonly Task _timerTask; + private readonly ILogger? _logger; + + public DefaultMemoryPoolFactory(IMeterFactory? meterFactory = null, ILogger? logger = null) + { + _meterFactory = meterFactory; + _logger = logger; + _timer = new PeriodicTimer(PinnedBlockMemoryPool.DefaultEvictionDelay); + _timerTask = Task.Run(async () => + { + try + { + while (await _timer.WaitForNextTickAsync()) + { + foreach (var pool in _pools.Keys) + { + pool.PerformEviction(); + } + } + } + catch (Exception ex) + { + _logger?.LogCritical(ex, "Error while evicting memory from pools."); + } + }); + } + + public MemoryPool Create() + { + var pool = new PinnedBlockMemoryPool(_meterFactory, _logger); + + _pools.TryAdd(pool, true); + + pool.OnPoolDisposed(static (state, self) => + { + ((ConcurrentDictionary)state!).TryRemove(self, out _); + }, _pools); + + return pool; + } + + public async ValueTask DisposeAsync() + { + _timer.Dispose(); + await _timerTask; + } +} diff --git a/src/Shared/Buffers.MemoryPool/DiagnosticMemoryPool.cs b/src/Shared/Buffers.MemoryPool/DiagnosticMemoryPool.cs index 2250dd045427..064b1ef8ceff 100644 --- a/src/Shared/Buffers.MemoryPool/DiagnosticMemoryPool.cs +++ b/src/Shared/Buffers.MemoryPool/DiagnosticMemoryPool.cs @@ -1,9 +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.Linq; -namespace System.Buffers; +namespace Microsoft.AspNetCore; /// /// Used to allocate and distribute re-usable blocks of memory. diff --git a/src/Shared/Buffers.MemoryPool/DiagnosticPoolBlock.cs b/src/Shared/Buffers.MemoryPool/DiagnosticPoolBlock.cs index d2c0dfd18917..411e98f9df00 100644 --- a/src/Shared/Buffers.MemoryPool/DiagnosticPoolBlock.cs +++ b/src/Shared/Buffers.MemoryPool/DiagnosticPoolBlock.cs @@ -1,12 +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.Diagnostics; using System.Runtime.InteropServices; #nullable enable -namespace System.Buffers; +namespace Microsoft.AspNetCore; /// /// Block tracking object used by the byte buffer memory pool. A slab is a large allocation which is divided into smaller blocks. The diff --git a/src/Shared/Buffers.MemoryPool/MemoryPoolBlock.cs b/src/Shared/Buffers.MemoryPool/MemoryPoolBlock.cs index abeca699c6b7..cf2233188c37 100644 --- a/src/Shared/Buffers.MemoryPool/MemoryPoolBlock.cs +++ b/src/Shared/Buffers.MemoryPool/MemoryPoolBlock.cs @@ -1,9 +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.Runtime.InteropServices; -namespace System.Buffers; +namespace Microsoft.AspNetCore; /// /// Wraps an array allocated in the pinned object heap in a reusable block of managed memory diff --git a/src/Shared/Buffers.MemoryPool/MemoryPoolFactory.cs b/src/Shared/Buffers.MemoryPool/MemoryPoolFactory.cs index 68880100c9c7..24679e1be23d 100644 --- a/src/Shared/Buffers.MemoryPool/MemoryPoolFactory.cs +++ b/src/Shared/Buffers.MemoryPool/MemoryPoolFactory.cs @@ -1,21 +1,26 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace System.Buffers; +using System.Buffers; +using System.Diagnostics.Metrics; -internal static class PinnedBlockMemoryPoolFactory +namespace Microsoft.AspNetCore; + +#nullable enable + +internal static class TestMemoryPoolFactory { - public static MemoryPool Create() + public static MemoryPool Create(IMeterFactory? meterFactory = null) { #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 = null) { - return new PinnedBlockMemoryPool(); + return new PinnedBlockMemoryPool(meterFactory); } } diff --git a/src/Shared/Buffers.MemoryPool/MemoryPoolThrowHelper.cs b/src/Shared/Buffers.MemoryPool/MemoryPoolThrowHelper.cs index de41f133a1a2..036f00798237 100644 --- a/src/Shared/Buffers.MemoryPool/MemoryPoolThrowHelper.cs +++ b/src/Shared/Buffers.MemoryPool/MemoryPoolThrowHelper.cs @@ -5,7 +5,7 @@ using System.Runtime.CompilerServices; using System.Text; -namespace System.Buffers; +namespace Microsoft.AspNetCore; internal sealed class MemoryPoolThrowHelper { diff --git a/src/Shared/Buffers.MemoryPool/PinnedBlockMemoryPool.cs b/src/Shared/Buffers.MemoryPool/PinnedBlockMemoryPool.cs index 6c74b477c821..4e6be0f91052 100644 --- a/src/Shared/Buffers.MemoryPool/PinnedBlockMemoryPool.cs +++ b/src/Shared/Buffers.MemoryPool/PinnedBlockMemoryPool.cs @@ -1,22 +1,31 @@ // 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.Diagnostics.Metrics; +using Microsoft.Extensions.Logging; #nullable enable -namespace System.Buffers; +namespace Microsoft.AspNetCore; /// /// 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. /// private const int _blockSize = 4096; + // 10 seconds chosen arbitrarily. Trying to avoid running eviction too frequently + // to avoid trashing if the server gets batches of requests, but want to run often + // enough to avoid memory bloat if the server is idle for a while. + // This can be tuned later if needed. + public static readonly TimeSpan DefaultEvictionDelay = TimeSpan.FromSeconds(10); + /// /// Max allocation block size for pooled blocks, /// larger values can be leased but they will be disposed after use rather than returned to the pool. @@ -39,13 +48,41 @@ internal sealed class PinnedBlockMemoryPool : MemoryPool /// private bool _isDisposed; // To detect redundant calls + private readonly PinnedBlockMemoryPoolMetrics? _metrics; + private readonly ILogger? _logger; + + private long _currentMemory; + private long _evictedMemory; + private DateTimeOffset _nextEviction = DateTime.UtcNow.Add(DefaultEvictionDelay); + + private uint _rentCount; + private uint _returnCount; + 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. /// private const int AnySize = -1; + public PinnedBlockMemoryPool(IMeterFactory? meterFactory = null, ILogger? logger = null) + { + _metrics = meterFactory is null ? null : new PinnedBlockMemoryPoolMetrics(meterFactory); + _logger = logger; + } + + /// + /// 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) @@ -58,11 +95,26 @@ 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); + _metrics?.Rent(block.Memory.Length); + Interlocked.Add(ref _currentMemory, -block.Memory.Length); + // block successfully taken from the stack - return it return block; } + + _metrics?.IncrementTotalMemory(BlockSize); + _metrics?.Rent(BlockSize); + + // 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. + Interlocked.Increment(ref _rentCount); + return new MemoryPoolBlock(this, BlockSize); } @@ -82,12 +134,94 @@ 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); } } + public bool TryScheduleEviction(DateTimeOffset now) + { + if (now >= _nextEviction) + { + _nextEviction = now.Add(DefaultEvictionDelay); + ThreadPool.UnsafeQueueUserWorkItem(this, preferLocal: false); + return true; + } + + return false; + } + + void IThreadPoolWorkItem.Execute() + { + try + { + PerformEviction(); + } + catch (Exception ex) + { + // Log the exception, but don't let it crash the thread pool + _logger?.LogError(ex, "Error while performing eviction in PinnedBlockMemoryPool."); + } + } + + /// + /// Examines the current usage and activity of the memory pool and evicts a calculated number of unused memory blocks. + /// The eviction policy is adaptive: if the pool is underutilized or idle, more blocks are evicted; + /// if activity is high, fewer or no blocks are evicted. + /// Evicted blocks are removed from the pool and their memory is unrooted for garbage collection. + /// + internal void PerformEviction() + { + 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) + { + // Trending less traffic + if (returnCount > rentCount) + { + // 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) + { + // 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); + } + + // 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); + + burstAmount--; + } + } + protected override void Dispose(bool disposing) { if (_isDisposed) @@ -97,8 +231,15 @@ protected override void Dispose(bool disposing) lock (_disposeSync) { + if (_isDisposed) + { + return; + } + _isDisposed = true; + _onPoolDisposed?.Invoke(_onPoolDisposedState, this); + if (disposing) { // Discard blocks in pool @@ -109,4 +250,7 @@ protected override void Dispose(bool disposing) } } } + + // Used for testing + public int BlockCount() => _blocks.Count; } diff --git a/src/Shared/Buffers.MemoryPool/PinnedBlockMemoryPoolMetrics.cs b/src/Shared/Buffers.MemoryPool/PinnedBlockMemoryPoolMetrics.cs new file mode 100644 index 000000000000..213fe3e84218 --- /dev/null +++ b/src/Shared/Buffers.MemoryPool/PinnedBlockMemoryPoolMetrics.cs @@ -0,0 +1,64 @@ +// 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; + +#nullable enable + +namespace Microsoft.AspNetCore; + +internal sealed class PinnedBlockMemoryPoolMetrics +{ + public const string MeterName = "Microsoft.AspNetCore.MemoryPool"; + + private readonly Meter _meter; + private readonly UpDownCounter _currentMemory; + private readonly Counter _totalAllocatedMemory; + private readonly Counter _evictedMemory; + private readonly Counter _totalRented; + + public PinnedBlockMemoryPoolMetrics(IMeterFactory meterFactory) + { + _meter = meterFactory.Create(MeterName); + + _currentMemory = _meter.CreateUpDownCounter( + "aspnetcore.memorypool.current_memory", + unit: "{bytes}", + description: "Number of bytes that are currently pooled by the pool."); + + _totalAllocatedMemory = _meter.CreateCounter( + "aspnetcore.memorypool.total_allocated", + unit: "{bytes}", + description: "Total number of allocations made by the pool."); + + _evictedMemory = _meter.CreateCounter( + "aspnetcore.memorypool.evicted_memory", + unit: "{bytes}", + description: "Total number of bytes that have been evicted."); + + _totalRented = _meter.CreateCounter( + "aspnetcore.memorypool.total_rented", + unit: "{bytes}", + description: "Total number of rented bytes from the pool."); + } + + public void UpdateCurrentMemory(int bytes) + { + _currentMemory.Add(bytes); + } + + public void IncrementTotalMemory(int bytes) + { + _totalAllocatedMemory.Add(bytes); + } + + public void EvictBlock(int bytes) + { + _evictedMemory.Add(bytes); + } + + public void Rent(int bytes) + { + _totalRented.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); diff --git a/src/Shared/Metrics/TestMeterFactory.cs b/src/Shared/Metrics/TestMeterFactory.cs index 26f6b5f32a43..83b02580addd 100644 --- a/src/Shared/Metrics/TestMeterFactory.cs +++ b/src/Shared/Metrics/TestMeterFactory.cs @@ -8,13 +8,24 @@ namespace Microsoft.AspNetCore.InternalTesting; internal sealed class TestMeterFactory : IMeterFactory { + private readonly Lock _lock = new(); + public List Meters { get; } = new List(); public Meter Create(MeterOptions options) { - var meter = new Meter(options.Name, options.Version, Array.Empty>(), scope: this); - Meters.Add(meter); - return meter; + lock (_lock) + { + // Simulate DefaultMeterFactory behavior of returning the same meter instance for the same name/version. + if (Meters.FirstOrDefault(m => m.Name == options.Name && m.Version == options.Version) is { } existingMeter) + { + return existingMeter; + } + + var meter = new Meter(options.Name, options.Version, Array.Empty>(), scope: this); + Meters.Add(meter); + return meter; + } } public void Dispose()