Skip to content

Add IMemoryPoolFactory and cleanup memory pool while idle #61554

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions src/Servers/Connections.Abstractions/src/IMemoryPoolFactory.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Interface for creating memory pools.
/// </summary>
public interface IMemoryPoolFactory<T>
{
/// <summary>
/// Creates a new instance of a memory pool.
/// </summary>
/// <returns>A new memory pool instance.</returns>
MemoryPool<T> Create();
}
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
#nullable enable
Microsoft.AspNetCore.Connections.IMemoryPoolFactory<T>
Microsoft.AspNetCore.Connections.IMemoryPoolFactory<T>.Create() -> System.Buffers.MemoryPool<T>!
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
#nullable enable
Microsoft.AspNetCore.Connections.IMemoryPoolFactory<T>
Microsoft.AspNetCore.Connections.IMemoryPoolFactory<T>.Create() -> System.Buffers.MemoryPool<T>!
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
#nullable enable
Microsoft.AspNetCore.Connections.IMemoryPoolFactory<T>
Microsoft.AspNetCore.Connections.IMemoryPoolFactory<T>.Create() -> System.Buffers.MemoryPool<T>!
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
#nullable enable
Microsoft.AspNetCore.Connections.IMemoryPoolFactory<T>
Microsoft.AspNetCore.Connections.IMemoryPoolFactory<T>.Create() -> System.Buffers.MemoryPool<T>!
7 changes: 5 additions & 2 deletions src/Servers/HttpSys/src/HttpSysListener.cs
Original file line number Diff line number Diff line change
@@ -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<byte> MemoryPool { get; } = PinnedBlockMemoryPoolFactory.Create();
internal MemoryPool<byte> 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<byte> 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<HttpSysListener>();
6 changes: 4 additions & 2 deletions src/Servers/HttpSys/src/MessagePump.cs
Original file line number Diff line number Diff line change
@@ -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<HttpSysOptions> options, ILoggerFactory loggerFactory, IAuthenticationSchemeProvider authentication)
public MessagePump(IOptions<HttpSysOptions> options, IMemoryPoolFactory<byte> 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<MessagePump>();

if (_options.Authentication.Schemes != AuthenticationSchemes.None)
4 changes: 4 additions & 0 deletions src/Servers/HttpSys/src/WebHostBuilderHttpSysExtensions.cs
Original file line number Diff line number Diff line change
@@ -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<IMemoryPoolFactory<byte>, DefaultMemoryPoolFactory>();
});
}

Original file line number Diff line number Diff line change
@@ -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<HttpSysException>(() => listener.Start());

Original file line number Diff line number Diff line change
@@ -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<HttpSysOptions> 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;
}
5 changes: 3 additions & 2 deletions src/Servers/HttpSys/test/FunctionalTests/Utilities.cs
Original file line number Diff line number Diff line change
@@ -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<HttpSysOptions> 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<HttpSysOptions> configureOptions, RequestDelegate app, ILoggerFactory loggerFactory)
5 changes: 3 additions & 2 deletions src/Servers/HttpSys/test/NonHelixTests/Utilities.cs
Original file line number Diff line number Diff line change
@@ -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<HttpSysOptions> 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<HttpSysOptions> configureOptions, RequestDelegate app)
5 changes: 4 additions & 1 deletion src/Servers/IIS/IIS/src/Core/IISHttpServer.cs
Original file line number Diff line number Diff line change
@@ -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<byte> _memoryPool = new PinnedBlockMemoryPool();
private readonly MemoryPool<byte> _memoryPool;
private GCHandle _httpServerHandle;
private readonly IHostApplicationLifetime _applicationLifetime;
private readonly ILogger<IISHttpServer> _logger;
@@ -60,10 +61,12 @@ public IISHttpServer(
IHostApplicationLifetime applicationLifetime,
IAuthenticationSchemeProvider authentication,
IConfiguration configuration,
IMemoryPoolFactory<byte> memoryPoolFactory,
IOptions<IISServerOptions> options,
ILogger<IISHttpServer> logger
)
{
_memoryPool = memoryPoolFactory.Create();
_nativeApplication = nativeApplication;
_applicationLifetime = applicationLifetime;
_logger = logger;
4 changes: 4 additions & 0 deletions src/Servers/IIS/IIS/src/WebHostBuilderIISExtensions.cs
Original file line number Diff line number Diff line change
@@ -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<IMemoryPoolFactory<byte>, DefaultMemoryPoolFactory>();
});
}

10 changes: 6 additions & 4 deletions src/Servers/Kestrel/Core/src/Internal/KestrelServerImpl.cs
Original file line number Diff line number Diff line change
@@ -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<IHeartbeatHandler> 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<KestrelServerOptions> options, ILoggerFactory loggerFactory, DiagnosticSource? diagnosticSource, KestrelMetrics metrics)
private static ServiceContext CreateServiceContext(IOptions<KestrelServerOptions> options, ILoggerFactory loggerFactory, DiagnosticSource? diagnosticSource, KestrelMetrics metrics,
IEnumerable<IHeartbeatHandler> heartbeatHandlers)
{
ArgumentNullException.ThrowIfNull(options);
ArgumentNullException.ThrowIfNull(loggerFactory);
@@ -87,7 +89,7 @@ private static ServiceContext CreateServiceContext(IOptions<KestrelServerOptions
var dateHeaderValueManager = new DateHeaderValueManager(TimeProvider.System);

var heartbeat = new Heartbeat(
new IHeartbeatHandler[] { dateHeaderValueManager, connectionManager },
[ dateHeaderValueManager, connectionManager, ..heartbeatHandlers ],
TimeProvider.System,
DebuggerWrapper.Singleton,
trace,
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// 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.AspNetCore.Connections;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;

namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal;

internal sealed class PinnedBlockMemoryPoolFactory : IMemoryPoolFactory<byte>, IHeartbeatHandler
{
private readonly IMeterFactory _meterFactory;
private readonly TimeProvider _timeProvider;
private readonly ConcurrentDictionary<PinnedBlockMemoryPool, PinnedBlockMemoryPool> _pools = new();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Could ask @stephentoub to create a ConcurrentHashTable<T> 😉 or change the value to a recognised atomic type by ConcurrentDictionary e.g. nuint

    private readonly ConcurrentDictionary<PinnedBlockMemoryPool, nuint> _pools = new();

Will save on GC write barriers; thought probably not too important

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Link to any documentation or discussion about this?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Micro-optimization on the correct data structure not being available in runtime (as both key and value are always the same and value never read)


public PinnedBlockMemoryPoolFactory(IMeterFactory meterFactory, TimeProvider? timeProvider = null)
{
_timeProvider = timeProvider ?? TimeProvider.System;
_meterFactory = meterFactory;
}

public MemoryPool<byte> Create()
{
var pool = new PinnedBlockMemoryPool(_meterFactory);

_pools.TryAdd(pool, pool);

pool.OnPoolDisposed(static (state, self) =>
{
((ConcurrentDictionary<PinnedBlockMemoryPool, PinnedBlockMemoryPool>)state!).TryRemove(self, out _);
}, _pools);

return pool;
}

public void OnHeartbeat()
{
var now = _timeProvider.GetUtcNow();
foreach (var pool in _pools)
{
pool.Value.TryScheduleEviction(now);
}
}
}
3 changes: 2 additions & 1 deletion src/Servers/Kestrel/Core/src/KestrelServer.cs
Original file line number Diff line number Diff line change
@@ -37,7 +37,8 @@ public KestrelServer(IOptions<KestrelServerOptions> options, IConnectionListener
new SimpleHttpsConfigurationService(),
loggerFactory,
diagnosticSource: null,
new KestrelMetrics(new DummyMeterFactory()));
new KestrelMetrics(new DummyMeterFactory()),
heartbeatHandlers: []);
}

/// <inheritdoc />
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<Description>Core components of ASP.NET Core Kestrel cross-platform web server.</Description>
@@ -37,6 +37,7 @@
<Compile Include="$(SharedSourceRoot)Obsoletions.cs" LinkBase="Shared" />
<Compile Include="$(RepoRoot)src\Shared\TaskToApm.cs" Link="Internal\TaskToApm.cs" />
<Compile Include="$(SharedSourceRoot)Metrics\MetricsExtensions.cs" />
<Compile Include="$(RepoRoot)src\Shared\Buffers.MemoryPool\*.cs" LinkBase="MemoryPool" />
</ItemGroup>

<ItemGroup>
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
@@ -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);

Original file line number Diff line number Diff line change
@@ -22,7 +22,7 @@ public class Http1OutputProducerTests : IDisposable

public Http1OutputProducerTests()
{
_memoryPool = PinnedBlockMemoryPoolFactory.Create();
_memoryPool = TestMemoryPoolFactory.Create();
}

public void Dispose()
Loading