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

Merged
merged 17 commits into from
Jun 9, 2025
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,50 @@
// 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;
using Microsoft.Extensions.Logging;

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

internal sealed class PinnedBlockMemoryPoolFactory : IMemoryPoolFactory<byte>, 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<PinnedBlockMemoryPool, nuint> _pools = new();

public PinnedBlockMemoryPoolFactory(IMeterFactory meterFactory, TimeProvider? timeProvider = null, ILogger<PinnedBlockMemoryPoolFactory>? logger = null)
{
_timeProvider = timeProvider ?? TimeProvider.System;
_meterFactory = meterFactory;
_logger = logger;
}

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

_pools.TryAdd(pool, nuint.Zero);

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

return pool;
}

public void OnHeartbeat()
{
var now = _timeProvider.GetUtcNow();
foreach (var pool in _pools)
{
pool.Key.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()
Original file line number Diff line number Diff line change
@@ -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);
3 changes: 2 additions & 1 deletion src/Servers/Kestrel/Core/test/KestrelServerTests.cs
Original file line number Diff line number Diff line change
@@ -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]
Original file line number Diff line number Diff line change
@@ -14,7 +14,6 @@
<Compile Include="$(KestrelSharedSourceRoot)KnownHeaders.cs" LinkBase="shared" />
<Compile Include="$(KestrelSharedSourceRoot)\HPackHeaderWriter.cs" Link="Http2\HPackHeaderWriter.cs" />
<Compile Include="$(KestrelSharedSourceRoot)\Http2HeadersEnumerator.cs" Link="Http2\Http2HeadersEnumerator.cs" />
<Compile Include="$(RepoRoot)src\Shared\Buffers.MemoryPool\*.cs" LinkBase="MemoryPool" />
<Compile Include="$(KestrelSharedSourceRoot)\CorrelationIdGenerator.cs" Link="Internal\CorrelationIdGenerator.cs" />
<Compile Include="$(SharedSourceRoot)test\Shared.Tests\runtime\Http2\*.cs" LinkBase="Shared\runtime\Http2" />
<Compile Include="$(SharedSourceRoot)test\Shared.Tests\runtime\Http3\*.cs" LinkBase="Shared\runtime\Http3" />
@@ -29,6 +28,7 @@
<Reference Include="Microsoft.Extensions.DependencyInjection" />
<Reference Include="Microsoft.Extensions.FileProviders.Physical" />
<Reference Include="Microsoft.Extensions.Logging" />
<Reference Include="Microsoft.Extensions.Diagnostics.Testing" />
<Reference Include="Microsoft.Extensions.TimeProvider.Testing" />
</ItemGroup>
</Project>
113 changes: 113 additions & 0 deletions src/Servers/Kestrel/Core/test/PinnedBlockMemoryPoolFactoryTests.cs
Original file line number Diff line number Diff line change
@@ -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<PinnedBlockMemoryPool>(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<PinnedBlockMemoryPool, nuint>)(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<PinnedBlockMemoryPool>(factory.Create());
var pool2 = Assert.IsType<PinnedBlockMemoryPool>(factory.Create());

var blocks = new List<IMemoryOwner<byte>>();
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));
}
}
}
354 changes: 352 additions & 2 deletions src/Servers/Kestrel/Core/test/PinnedBlockMemoryPoolTests.cs
Original file line number Diff line number Diff line change
@@ -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<IMemoryOwner<byte>>();
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<IMemoryOwner<byte>>();
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<IMemoryOwner<byte>>();
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<IMemoryOwner<byte>>();
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<long>(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<long>(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<long>(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<long>(testMeterFactory, "Microsoft.AspNetCore.MemoryPool", "aspnetcore.memorypool.evicted_memory");

var pool = new PinnedBlockMemoryPool(testMeterFactory);

// Fill the pool with some blocks
var blocks = new List<IMemoryOwner<byte>>();
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<long>(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();
}
}
2 changes: 1 addition & 1 deletion src/Servers/Kestrel/Core/test/PipelineExtensionTests.cs
Original file line number Diff line number Diff line change
@@ -16,7 +16,7 @@ public class PipelineExtensionTests : IDisposable
private const int _ulongMaxValueLength = 20;

private readonly Pipe _pipe;
private readonly MemoryPool<byte> _memoryPool = PinnedBlockMemoryPoolFactory.Create();
private readonly MemoryPool<byte> _memoryPool = TestMemoryPoolFactory.Create();

public PipelineExtensionTests()
{
2 changes: 1 addition & 1 deletion src/Servers/Kestrel/Core/test/StartLineTests.cs
Original file line number Diff line number Diff line change
@@ -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;
2 changes: 1 addition & 1 deletion src/Servers/Kestrel/Core/test/TestHelpers/TestInput.cs
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
@@ -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)
/// </returns>
public static IWebHostBuilder UseKestrelCore(this IWebHostBuilder hostBuilder)
{
hostBuilder.UseSockets();
hostBuilder.ConfigureServices(services =>
{
// Don't override an already-configured transport
services.TryAddSingleton<IConnectionListenerFactory, SocketTransportFactory>();

services.AddTransient<IConfigureOptions<KestrelServerOptions>, KestrelServerOptionsSetup>();
services.AddSingleton<IHttpsConfigurationService, HttpsConfigurationService>();
services.AddSingleton<IServer, KestrelServerImpl>();
services.AddSingleton<KestrelMetrics>();

services.AddSingleton<PinnedBlockMemoryPoolFactory>();
services.TryAddEnumerable(ServiceDescriptor.Singleton<IHeartbeatHandler, PinnedBlockMemoryPoolFactory>(sp => sp.GetRequiredService<PinnedBlockMemoryPoolFactory>()));
services.AddSingleton<IMemoryPoolFactory<byte>>(sp => sp.GetRequiredService<PinnedBlockMemoryPoolFactory>());
});

if (OperatingSystem.IsWindows())
Original file line number Diff line number Diff line change
@@ -18,6 +18,7 @@
<Content Include="$(KestrelRoot)Core\src\Internal\Infrastructure\HttpUtilities.Generated.cs" LinkBase="shared\GeneratedContent" CopyToOutputDirectory="PreserveNewest" />
<Content Include="$(KestrelSharedSourceRoot)\TransportConnection.Generated.cs" LinkBase="shared\GeneratedContent" CopyToOutputDirectory="PreserveNewest" />
<Content Include="$(KestrelSharedSourceRoot)\TransportMultiplexedConnection.Generated.cs" LinkBase="shared\GeneratedContent" CopyToOutputDirectory="PreserveNewest" />
<Compile Include="$(KestrelSharedSourceRoot)test\TransportTestHelpers\NamedPipesSupportedAttribute.cs" Link="shared\TransportTestHelpers\NamedPipesSupportedAttribute.cs" />

<ProjectReference Include="..\..\tools\CodeGenerator\CodeGenerator.csproj" />
<Reference Include="Microsoft.AspNetCore.Server.Kestrel" />
Original file line number Diff line number Diff line change
@@ -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<KestrelMetrics>(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<PinnedBlockMemoryPoolFactory>(host.Services.GetRequiredService<IMemoryPoolFactory<byte>>());
Assert.Null(host.Services.GetService<IMemoryPoolFactory<int>>());

Assert.Same(memoryPoolFactory, host.Services.GetRequiredService<IOptions<SocketTransportOptions>>().Value.MemoryPoolFactory);

// Swap order of UseKestrel and UseSockets
hostBuilder = new WebHostBuilder()
.UseKestrel()
.UseSockets()
.Configure(app => { });

host = hostBuilder.Build();

memoryPoolFactory = Assert.IsType<PinnedBlockMemoryPoolFactory>(host.Services.GetRequiredService<IMemoryPoolFactory<byte>>());
Assert.Null(host.Services.GetService<IMemoryPoolFactory<int>>());

Assert.Same(memoryPoolFactory, host.Services.GetRequiredService<IOptions<SocketTransportOptions>>().Value.MemoryPoolFactory);
}

[Fact]
public void SocketsHasDefaultMemoryPool()
{
var hostBuilder = new WebHostBuilder()
.UseSockets()
.Configure(app => { });

var host = hostBuilder.Build();

var memoryPoolFactory = host.Services.GetRequiredService<IMemoryPoolFactory<byte>>();
Assert.IsNotType<PinnedBlockMemoryPoolFactory>(memoryPoolFactory);
Assert.Null(host.Services.GetService<IMemoryPoolFactory<int>>());

Assert.Same(memoryPoolFactory, host.Services.GetRequiredService<IOptions<SocketTransportOptions>>().Value.MemoryPoolFactory);
}

[ConditionalFact]
[NamedPipesSupported]
public void MemoryPoolFactorySetCorrectlyWithNamedPipes()
{
var hostBuilder = new WebHostBuilder()
.UseNamedPipes()
.UseKestrel()
.Configure(app => { });

var host = hostBuilder.Build();

var memoryPoolFactory = Assert.IsType<PinnedBlockMemoryPoolFactory>(host.Services.GetRequiredService<IMemoryPoolFactory<byte>>());
Assert.Null(host.Services.GetService<IMemoryPoolFactory<int>>());

Assert.Same(memoryPoolFactory, host.Services.GetRequiredService<IOptions<NamedPipeTransportOptions>>().Value.MemoryPoolFactory);

// Swap order of UseKestrel and UseNamedPipes
hostBuilder = new WebHostBuilder()
.UseKestrel()
.UseNamedPipes()
.Configure(app => { });

host = hostBuilder.Build();

memoryPoolFactory = Assert.IsType<PinnedBlockMemoryPoolFactory>(host.Services.GetRequiredService<IMemoryPoolFactory<byte>>());
Assert.Null(host.Services.GetService<IMemoryPoolFactory<int>>());

Assert.Same(memoryPoolFactory, host.Services.GetRequiredService<IOptions<NamedPipeTransportOptions>>().Value.MemoryPoolFactory);
}

[ConditionalFact]
[NamedPipesSupported]
public void NamedPipesHasDefaultMemoryPool()
{
var hostBuilder = new WebHostBuilder()
.UseNamedPipes()
.Configure(app => { });

var host = hostBuilder.Build();

var memoryPoolFactory = host.Services.GetRequiredService<IMemoryPoolFactory<byte>>();
Assert.IsNotType<PinnedBlockMemoryPoolFactory>(memoryPoolFactory);
Assert.Null(host.Services.GetService<IMemoryPoolFactory<int>>());

Assert.Same(memoryPoolFactory, host.Services.GetRequiredService<IOptions<NamedPipeTransportOptions>>().Value.MemoryPoolFactory);
}
}
Original file line number Diff line number Diff line change
@@ -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);
Original file line number Diff line number Diff line change
@@ -21,6 +21,7 @@
<Compile Include="$(KestrelSharedSourceRoot)\TransportConnection.cs" Link="Internal\TransportConnection.cs" />
<Compile Include="$(KestrelSharedSourceRoot)\TransportConnection.Generated.cs" Link="Internal\TransportConnection.Generated.cs" />
<Compile Include="$(KestrelSharedSourceRoot)\TransportConnection.FeatureCollection.cs" Link="Internal\TransportConnection.FeatureCollection.cs" />
<Compile Include="$(KestrelSharedSourceRoot)\DefaultSimpleMemoryPoolFactory.cs" Link="Internal\DefaultSimpleMemoryPoolFactory.cs" />
</ItemGroup>

<ItemGroup>
Original file line number Diff line number Diff line change
@@ -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<MemoryPool<byte>> MemoryPoolFactory { get; set; } = PinnedBlockMemoryPoolFactory.Create;
internal IMemoryPoolFactory<byte> MemoryPoolFactory { get; set; } = DefaultSimpleMemoryPoolFactory.Instance;
}
Original file line number Diff line number Diff line change
@@ -33,7 +33,15 @@ public static IWebHostBuilder UseNamedPipes(this IWebHostBuilder hostBuilder)
{
services.TryAddSingleton<ObjectPoolProvider, DefaultObjectPoolProvider>();
services.AddSingleton<IConnectionListenerFactory, NamedPipeTransportFactory>();

services.TryAddSingleton<IMemoryPoolFactory<byte>, DefaultMemoryPoolFactory>();
services.AddOptions<NamedPipeTransportOptions>().Configure((NamedPipeTransportOptions options, IMemoryPoolFactory<byte> factory) =>
{
// Set the IMemoryPoolFactory from DI on NamedPipeTransportOptions. Usually this should be the PinnedBlockMemoryPoolFactory from UseKestrelCore.
options.MemoryPoolFactory = factory;
});
});

return hostBuilder;
}

Original file line number Diff line number Diff line change
@@ -27,7 +27,7 @@ public SocketConnectionFactory(IOptions<SocketTransportOptions> 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;
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
@@ -12,12 +12,12 @@
</PropertyGroup>

<ItemGroup>
<Compile Include="$(RepoRoot)src\Shared\Buffers.MemoryPool\*.cs" LinkBase="MemoryPool" />
<Compile Include="$(KestrelSharedSourceRoot)\CorrelationIdGenerator.cs" Link="Internal\CorrelationIdGenerator.cs" />
<Compile Include="$(SharedSourceRoot)ServerInfrastructure\DuplexPipe.cs" Link="Internal\DuplexPipe.cs" />
<Compile Include="$(KestrelSharedSourceRoot)\TransportConnection.cs" Link="Internal\TransportConnection.cs" />
<Compile Include="$(KestrelSharedSourceRoot)\TransportConnection.Generated.cs" Link="Internal\TransportConnection.Generated.cs" />
<Compile Include="$(KestrelSharedSourceRoot)\TransportConnection.FeatureCollection.cs" Link="Internal\TransportConnection.FeatureCollection.cs" />
<Compile Include="$(KestrelSharedSourceRoot)\DefaultSimpleMemoryPoolFactory.cs" Link="Internal\DefaultSimpleMemoryPoolFactory.cs" />
</ItemGroup>

<ItemGroup>
Original file line number Diff line number Diff line change
@@ -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[]
Original file line number Diff line number Diff line change
@@ -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)
/// </remarks>
public bool UnsafePreferInlineScheduling { get; set; }

internal Func<MemoryPool<byte>> MemoryPoolFactory { get; set; } = PinnedBlockMemoryPoolFactory.Create;
internal IMemoryPoolFactory<byte> MemoryPoolFactory { get; set; } = DefaultSimpleMemoryPoolFactory.Instance;
}
Original file line number Diff line number Diff line change
@@ -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<MemoryPool<byte>> MemoryPoolFactory { get; set; } = System.Buffers.PinnedBlockMemoryPoolFactory.Create;
internal IMemoryPoolFactory<byte> MemoryPoolFactory { get; set; } = DefaultSimpleMemoryPoolFactory.Instance;
Copy link
Member

Choose a reason for hiding this comment

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

Are we at all concerned with breaking reflection-based code that was setting this previously?

Copy link
Member Author

Choose a reason for hiding this comment

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

Not really. The couple teams we gave code to were told the private reflection wouldn't work in 10.0.

}
Original file line number Diff line number Diff line change
@@ -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<IConnectionListenerFactory, SocketTransportFactory>();
services.TryAddSingleton<IConnectionListenerFactory, SocketTransportFactory>();

services.TryAddSingleton<IMemoryPoolFactory<byte>, DefaultSimpleMemoryPoolFactory>();
services.AddOptions<SocketTransportOptions>().Configure((SocketTransportOptions options, IMemoryPoolFactory<byte> factory) =>
{
// Set the IMemoryPoolFactory from DI on SocketTransportOptions. Usually this should be the PinnedBlockMemoryPoolFactory from UseKestrelCore.
options.MemoryPoolFactory = factory;
Copy link
Member

Choose a reason for hiding this comment

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

Given that we're changing the type of the MemoryPoolFactory from a Func to an IMemoryPoolFactory meaning we're breaking reflection anyway, why flow the factory through the options instead of constructor injecting it in SocketTransportFactory? Just to minimize churn? I guess this also ensures anyone constructing it themselves with an options instance resolved from DI gets the appropriate IMemoryPoolFactory.

I think it's fine to leave it, but I think it's our last chance not to flow the factory through options if we don't want to.

Copy link
Member Author

Choose a reason for hiding this comment

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

It would mean new API (since the type is public) or using the internal impl trick and switching to that. We'd need to do it on NamedPipes as well.

});
});
}

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

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

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

Original file line number Diff line number Diff line change
@@ -28,7 +28,7 @@ public class Http1LargeWritingBenchmark
[GlobalSetup]
public void GlobalSetup()
{
_memoryPool = PinnedBlockMemoryPoolFactory.Create();
_memoryPool = TestMemoryPoolFactory.Create();
_http1Connection = MakeHttp1Connection();
_consumeResponseBodyTask = ConsumeResponseBody();
}
Original file line number Diff line number Diff line change
@@ -35,7 +35,7 @@ public class Http1ReadingBenchmark
[GlobalSetup]
public void GlobalSetup()
{
_memoryPool = PinnedBlockMemoryPoolFactory.Create();
_memoryPool = TestMemoryPoolFactory.Create();
_http1Connection = MakeHttp1Connection();
}

Original file line number Diff line number Diff line change
@@ -35,7 +35,7 @@ public class Http1WritingBenchmark
[GlobalSetup]
public void GlobalSetup()
{
_memoryPool = PinnedBlockMemoryPoolFactory.Create();
_memoryPool = TestMemoryPoolFactory.Create();
_http1Connection = MakeHttp1Connection();
_consumeResponseBodyTask = ConsumeResponseBody();
}
Original file line number Diff line number Diff line change
@@ -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);

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

Original file line number Diff line number Diff line change
@@ -18,7 +18,6 @@
<Compile Include="$(KestrelSharedSourceRoot)test\PipeWriterHttp2FrameExtensions.cs" Link="Internal\PipeWriterHttp2FrameExtensions.cs" />
<Compile Include="$(KestrelSharedSourceRoot)\HPackHeaderWriter.cs" Link="Http2\HPackHeaderWriter.cs" />
<Compile Include="$(KestrelSharedSourceRoot)\Http2HeadersEnumerator.cs" Link="Http2\Http2HeadersEnumerator.cs" />
<Compile Include="$(RepoRoot)src\Shared\Buffers.MemoryPool\*.cs" LinkBase="MemoryPool" />
<Compile Include="$(KestrelSharedSourceRoot)test\KestrelTestLoggerProvider.cs" />
<Compile Include="$(KestrelSharedSourceRoot)test\TestApplicationErrorLogger.cs" />
<Compile Include="$(KestrelSharedSourceRoot)test\TestHttp1Connection.cs" />
Original file line number Diff line number Diff line change
@@ -19,7 +19,7 @@ public class PipeThroughputBenchmark
[IterationSetup]
public void Setup()
{
_memoryPool = PinnedBlockMemoryPoolFactory.Create();
_memoryPool = TestMemoryPoolFactory.Create();
_pipe = new Pipe(new PipeOptions(_memoryPool));
}

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

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

17 changes: 17 additions & 0 deletions src/Servers/Kestrel/shared/DefaultSimpleMemoryPoolFactory.cs
Original file line number Diff line number Diff line change
@@ -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<byte>
{
public static DefaultSimpleMemoryPoolFactory Instance { get; } = new DefaultSimpleMemoryPoolFactory();

public MemoryPool<byte> Create()
{
return MemoryPool<byte>.Shared;
}
}
3 changes: 1 addition & 2 deletions src/Servers/Kestrel/shared/test/Http3/Http3InMemory.cs
Original file line number Diff line number Diff line change
@@ -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<byte> _memoryPool = PinnedBlockMemoryPoolFactory.Create();
internal readonly MemoryPool<byte> _memoryPool = TestMemoryPoolFactory.Create();
internal readonly ConcurrentQueue<TestStreamContext> _streamContextPool = new ConcurrentQueue<TestStreamContext>();
protected Task _connectionTask;
internal ILogger Logger { get; }
2 changes: 1 addition & 1 deletion src/Servers/Kestrel/shared/test/TestContextFactory.cs
Original file line number Diff line number Diff line change
@@ -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)
15 changes: 14 additions & 1 deletion src/Servers/Kestrel/shared/test/TestServiceContext.cs
Original file line number Diff line number Diff line change
@@ -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<MemoryPool<byte>> MemoryPoolFactory { get; set; } = System.Buffers.PinnedBlockMemoryPoolFactory.Create;
public IMemoryPoolFactory<byte> MemoryPoolFactory { get; set; } = new WrappingMemoryPoolFactory(() => TestMemoryPoolFactory.Create());

public string DateHeaderValue => DateHeaderValueManager.GetDateHeaderValues().String;

internal sealed class WrappingMemoryPoolFactory : IMemoryPoolFactory<byte>
{
private readonly Func<MemoryPool<byte>> _memoryPoolFactory;

public WrappingMemoryPoolFactory(Func<MemoryPool<byte>> memoryPoolFactory)
{
_memoryPoolFactory = memoryPoolFactory;
}

public MemoryPool<byte> Create() => _memoryPoolFactory();
}
}
Original file line number Diff line number Diff line change
@@ -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<byte>
{
private readonly bool _allowLateReturn;

Original file line number Diff line number Diff line change
@@ -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<Kestre
_app = app;
Context = context;

_host = TransportSelector.GetHostBuilder(context.MemoryPoolFactory, context.ServerOptions.Limits.MaxRequestBufferSize)
_host = TransportSelector.GetHostBuilder(context.ServerOptions.Limits.MaxRequestBufferSize)
.ConfigureWebHost(webHostBuilder =>
{
webHostBuilder
@@ -85,6 +78,10 @@ public TestServer(RequestDelegate app, TestServiceContext context, Action<Kestre
})
.ConfigureServices(services =>
{
if (context.MemoryPoolFactory != null)
{
services.AddSingleton<IMemoryPoolFactory<byte>>(context.MemoryPoolFactory);
}
services.AddSingleton<IStartup>(this);
services.AddSingleton(context.LoggerFactory);
services.AddSingleton<IHttpsConfigurationService, HttpsConfigurationService>();
Original file line number Diff line number Diff line change
@@ -168,7 +168,7 @@ public async Task GracefulTurnsAbortiveIfRequestsDoNotFinish()

var testContext = new TestServiceContext(LoggerFactory)
{
MemoryPoolFactory = () => new PinnedBlockMemoryPool()
MemoryPoolFactory = new TestServiceContext.WrappingMemoryPoolFactory(() => TestMemoryPoolFactory.CreatePinnedBlockMemoryPool()),
};

ThrowOnUngracefulShutdown = false;
Original file line number Diff line number Diff line change
@@ -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<IHost> StartHost(long? maxRequestBufferSize,
bool useConnectionAdapter,
TaskCompletionSource startReadingRequestBody,
TaskCompletionSource clientFinishedSendingRequestBody,
Func<MemoryPool<byte>> memoryPoolFactory = null)
IMemoryPoolFactory<byte> memoryPoolFactory = null)
{
var host = TransportSelector.GetHostBuilder(memoryPoolFactory, maxRequestBufferSize)
var host = TransportSelector.GetHostBuilder(maxRequestBufferSize)
.ConfigureWebHost(webHostBuilder =>
{
webHostBuilder
@@ -341,6 +335,13 @@ private async Task<IHost> StartHost(long? maxRequestBufferSize,
options.Limits.MaxRequestBodySize = _dataLength;
})
.UseContentRoot(Directory.GetCurrentDirectory())
.ConfigureServices(services =>
{
if (memoryPoolFactory != null)
{
services.AddSingleton<IMemoryPoolFactory<byte>>(memoryPoolFactory);
}
})
.Configure(app => app.Run(async context =>
{
await startReadingRequestBody.Task.TimeoutAfter(TimeSpan.FromSeconds(120));
Original file line number Diff line number Diff line change
@@ -112,7 +112,7 @@ protected static IEnumerable<KeyValuePair<string, string>> 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<byte> _memoryPool = PinnedBlockMemoryPoolFactory.Create();
private readonly MemoryPool<byte> _memoryPool = TestMemoryPoolFactory.Create();

internal readonly Http2PeerSettings _clientSettings = new Http2PeerSettings();
internal readonly HPackDecoder _hpackDecoder;
Original file line number Diff line number Diff line change
@@ -17,7 +17,6 @@
<Compile Include="$(KestrelSharedSourceRoot)test\Http3\*.cs" LinkBase="shared" />
<Compile Include="$(KestrelSharedSourceRoot)\HPackHeaderWriter.cs" Link="Http2\HPackHeaderWriter.cs" />
<Compile Include="$(KestrelSharedSourceRoot)\Http2HeadersEnumerator.cs" Link="Http2\Http2HeadersEnumerator.cs" />
<Compile Include="$(RepoRoot)src\Shared\Buffers.MemoryPool\*.cs" LinkBase="MemoryPool" />
<Compile Include="$(KestrelSharedSourceRoot)\CompletionPipeReader.cs" Link="Internal\CompletionPipeReader.cs" />
<Compile Include="$(KestrelSharedSourceRoot)\CompletionPipeWriter.cs" Link="Internal\CompletionPipeWriter.cs" />
<Compile Include="$(KestrelSharedSourceRoot)\ConnectionCompletion.cs" Link="Internal\ConnectionCompletion.cs" />
Original file line number Diff line number Diff line change
@@ -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);
Original file line number Diff line number Diff line change
@@ -70,7 +70,7 @@ public TestServer(RequestDelegate app, TestServiceContext context, Action<Kestre
{
_app = app;
Context = context;
_memoryPool = context.MemoryPoolFactory();
_memoryPool = context.MemoryPoolFactory.Create();
_transportFactory = new InMemoryTransportFactory();
HttpClientSlim = new InMemoryHttpClientSlim(this);

Original file line number Diff line number Diff line change
@@ -8,6 +8,7 @@
using System.Net.Http;
using System.Net.Security;
using System.Text;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Connections.Features;
using Microsoft.AspNetCore.Hosting;
Original file line number Diff line number Diff line change
@@ -27,6 +27,7 @@
<Compile Include="$(SharedSourceRoot)Metrics\TestMeterFactory.cs" LinkBase="shared" />
<Compile Include="$(KestrelSharedSourceRoot)test\TransportTestHelpers\TlsAlpnSupportedAttribute.cs" Link="shared\TransportTestHelpers\TlsAlpnSupportedAttribute.cs" />
<Compile Include="$(KestrelSharedSourceRoot)test\ServerRetryHelper.cs" LinkBase="shared" />
<Compile Include="$(RepoRoot)src\Shared\Buffers.MemoryPool\*.cs" LinkBase="MemoryPool" />
</ItemGroup>

<ItemGroup>
Original file line number Diff line number Diff line change
@@ -1,27 +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;
using System.Buffers;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;

namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests;

public static class TransportSelector
{
public static IHostBuilder GetHostBuilder(Func<MemoryPool<byte>> 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;
});
});
}
}
66 changes: 66 additions & 0 deletions src/Shared/Buffers.MemoryPool/DefaultMemoryPoolFactory.cs
Original file line number Diff line number Diff line change
@@ -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<byte>, IAsyncDisposable
{
private readonly IMeterFactory? _meterFactory;
private readonly ConcurrentDictionary<PinnedBlockMemoryPool, bool> _pools = new();
private readonly PeriodicTimer _timer;
private readonly Task _timerTask;
private readonly ILogger? _logger;

public DefaultMemoryPoolFactory(IMeterFactory? meterFactory = null, ILogger<DefaultMemoryPoolFactory>? 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<byte> Create()
{
var pool = new PinnedBlockMemoryPool(_meterFactory, _logger);

_pools.TryAdd(pool, true);

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

return pool;
}

public async ValueTask DisposeAsync()
{
_timer.Dispose();
await _timerTask;
}
}
3 changes: 2 additions & 1 deletion src/Shared/Buffers.MemoryPool/DiagnosticMemoryPool.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Used to allocate and distribute re-usable blocks of memory.
3 changes: 2 additions & 1 deletion src/Shared/Buffers.MemoryPool/DiagnosticPoolBlock.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Block tracking object used by the byte buffer memory pool. A slab is a large allocation which is divided into smaller blocks. The
3 changes: 2 additions & 1 deletion src/Shared/Buffers.MemoryPool/MemoryPoolBlock.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Wraps an array allocated in the pinned object heap in a reusable block of managed memory
19 changes: 12 additions & 7 deletions src/Shared/Buffers.MemoryPool/MemoryPoolFactory.cs
Original file line number Diff line number Diff line change
@@ -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<byte> Create()
public static MemoryPool<byte> 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<byte> CreatePinnedBlockMemoryPool()
public static MemoryPool<byte> CreatePinnedBlockMemoryPool(IMeterFactory? meterFactory = null)
{
return new PinnedBlockMemoryPool();
return new PinnedBlockMemoryPool(meterFactory);
}
}
2 changes: 1 addition & 1 deletion src/Shared/Buffers.MemoryPool/MemoryPoolThrowHelper.cs
Original file line number Diff line number Diff line change
@@ -5,7 +5,7 @@
using System.Runtime.CompilerServices;
using System.Text;

namespace System.Buffers;
namespace Microsoft.AspNetCore;

internal sealed class MemoryPoolThrowHelper
{
148 changes: 146 additions & 2 deletions src/Shared/Buffers.MemoryPool/PinnedBlockMemoryPool.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Used to allocate and distribute re-usable blocks of memory.
/// </summary>
internal sealed class PinnedBlockMemoryPool : MemoryPool<byte>
internal sealed class PinnedBlockMemoryPool : MemoryPool<byte>, IThreadPoolWorkItem
{
/// <summary>
/// The size of a block. 4096 is chosen because most operating systems use 4k pages.
/// </summary>
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);

/// <summary>
/// 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<byte>
/// </summary>
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<object?, PinnedBlockMemoryPool>? _onPoolDisposed;
private object? _onPoolDisposedState;

/// <summary>
/// This default value passed in to Rent to use the default value for the pool.
/// </summary>
private const int AnySize = -1;

public PinnedBlockMemoryPool(IMeterFactory? meterFactory = null, ILogger? logger = null)
{
_metrics = meterFactory is null ? null : new PinnedBlockMemoryPoolMetrics(meterFactory);
_logger = logger;
}

/// <summary>
/// Register a callback to call when the pool is being disposed.
/// </summary>
public void OnPoolDisposed(Action<object?, PinnedBlockMemoryPool> onPoolDisposed, object? state = null)
{
_onPoolDisposed = onPoolDisposed;
_onPoolDisposedState = state;
}

public override IMemoryOwner<byte> Rent(int size = AnySize)
{
if (size > _blockSize)
@@ -58,11 +95,26 @@ public override IMemoryOwner<byte> 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.");
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
_logger?.LogError(ex, "Error while performing eviction in PinnedBlockMemoryPool.");
_logger?.LogCritical(ex, "Error while performing eviction in PinnedBlockMemoryPool.");

I think a critical log is appropriate considering something must have gone horribly wrong to get here.

Copy link
Member Author

Choose a reason for hiding this comment

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

I chose error because we're still going to be running eviction code next iteration. Critical to me feels more like we stopped doing work now and in the future because something is horribly wrong. i.e. the accept loop in Kestrel: https://source.dot.net/#Microsoft.AspNetCore.Server.Kestrel.Core/Internal/ConnectionDispatcher.cs,74

}
}

/// <summary>
/// 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.
/// </summary>
internal void PerformEviction()
Copy link
Member

@JamesNK JamesNK Jun 3, 2025

Choose a reason for hiding this comment

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

Do other heartbeat activities log?

I'm wondering if there should be some trace-level logging here. For example, if the memory pool decides to evict memory then write a log with details about why and how much.

Copy link
Member

Choose a reason for hiding this comment

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

Kestrel does eviction of Http2Stream streams on the heartbeat. I don't think it's logged...

{
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))
Copy link
Member

Choose a reason for hiding this comment

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

I wonder if it'd be interesting to emit some metric if this _blocks.TryDequeue operation fails. It seems like this would be a good indicator that our heuristics aren't working as well as we'd like given how conservative we try to make the burst amount.

Copy link
Member Author

Choose a reason for hiding this comment

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

Leave a comment on #61594? That could happen any time we are performing eviction and new traffic starts coming in, or if the pool was low and the heuristics didn't limit themselves to the size of the pool.

{
_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;
}
64 changes: 64 additions & 0 deletions src/Shared/Buffers.MemoryPool/PinnedBlockMemoryPoolMetrics.cs
Original file line number Diff line number Diff line change
@@ -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<long> _currentMemory;
private readonly Counter<long> _totalAllocatedMemory;
private readonly Counter<long> _evictedMemory;
private readonly Counter<long> _totalRented;

public PinnedBlockMemoryPoolMetrics(IMeterFactory meterFactory)
Copy link
Member

Choose a reason for hiding this comment

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

Do we want a metric for peak?

Copy link
Member Author

@BrennanConroy BrennanConroy May 15, 2025

Choose a reason for hiding this comment

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

total_allocated covers that? Oh, nvm. Yeah, peak might be interesting.

Copy link
Member Author

Choose a reason for hiding this comment

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

I think peak is just measuring the max of current_memory?

{
_meter = meterFactory.Create(MeterName);
Copy link
Member

@JamesNK JamesNK May 15, 2025

Choose a reason for hiding this comment

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

It looks like there could be multiple instances of memory pool used across assemblies with shared source. Fortunatly I believe that as long as the same IMeterFactory is used for all of them (injected by DI), the meter and counters created will be the same instances. Metrics values will be aggregated and saved to the same location.

However, I think a unit test to double check that is 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.

Added a test and updated our TestMeterFactory implementation to return the same Meter instance for similar meters. Same as the default meter factory impl: https://source.dot.net/#Microsoft.Extensions.Diagnostics/Metrics/DefaultMeterFactory.cs,37


_currentMemory = _meter.CreateUpDownCounter<long>(
"aspnetcore.memorypool.current_memory",
unit: "{bytes}",
description: "Number of bytes that are currently pooled by the pool.");

_totalAllocatedMemory = _meter.CreateCounter<long>(
"aspnetcore.memorypool.total_allocated",
unit: "{bytes}",
description: "Total number of allocations made by the pool.");

_evictedMemory = _meter.CreateCounter<long>(
"aspnetcore.memorypool.evicted_memory",
unit: "{bytes}",
description: "Total number of bytes that have been evicted.");

_totalRented = _meter.CreateCounter<long>(
"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);
}
}
Original file line number Diff line number Diff line change
@@ -12,6 +12,7 @@ internal static class Http2CatIServiceCollectionExtensions
{
public static IServiceCollection UseHttp2Cat(this IServiceCollection services, Action<Http2CatOptions> configureOptions)
{
services.AddMetrics();
services.AddSingleton<IConnectionFactory, SocketConnectionFactory>();
services.AddHostedService<Http2CatHostedService>();
services.Configure(configureOptions);
17 changes: 14 additions & 3 deletions src/Shared/Metrics/TestMeterFactory.cs
Original file line number Diff line number Diff line change
@@ -8,13 +8,24 @@ namespace Microsoft.AspNetCore.InternalTesting;

internal sealed class TestMeterFactory : IMeterFactory
{
private readonly Lock _lock = new();

public List<Meter> Meters { get; } = new List<Meter>();

public Meter Create(MeterOptions options)
{
var meter = new Meter(options.Name, options.Version, Array.Empty<KeyValuePair<string, object>>(), 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<KeyValuePair<string, object>>(), scope: this);
Meters.Add(meter);
return meter;
}
}

public void Dispose()