Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ public interface IMemoryPoolFactory<T>
/// <summary>
/// Creates a new instance of a memory pool.
/// </summary>
/// <param name="options">Options for configuring the memory pool.</param>
/// <returns>A new memory pool instance.</returns>
MemoryPool<T> Create();
MemoryPool<T> Create(MemoryPoolOptions options);
Copy link
Member

Choose a reason for hiding this comment

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

I was thinking this should be optional so you don't have to new up options every time you create a pool? Are your thoughts that since you shouldn't be creating a new pool very often it's fine to require the small allocation cost?

Copy link
Member Author

@JamesNK JamesNK Aug 2, 2025

Choose a reason for hiding this comment

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

Sure, it could be optional. That would follow what Channel has:

image

Do you think there should be two create methods on the interface, or one with the options as optional?

}
15 changes: 15 additions & 0 deletions src/Servers/Connections.Abstractions/src/MemoryPoolOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.AspNetCore.Connections;

/// <summary>
/// Options for configuring a memory pool.
/// </summary>
public sealed class MemoryPoolOptions
{
/// <summary>
/// Gets or sets the owner of the memory pool. This is used for logging and diagnostics purposes.
/// </summary>
public string? Owner { get; set; }
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
#nullable enable
Microsoft.AspNetCore.Connections.IMemoryPoolFactory<T>
Microsoft.AspNetCore.Connections.IMemoryPoolFactory<T>.Create() -> System.Buffers.MemoryPool<T>!
Microsoft.AspNetCore.Connections.IMemoryPoolFactory<T>.Create(Microsoft.AspNetCore.Connections.MemoryPoolOptions! options) -> System.Buffers.MemoryPool<T>!
Microsoft.AspNetCore.Connections.MemoryPoolOptions
Microsoft.AspNetCore.Connections.MemoryPoolOptions.MemoryPoolOptions() -> void
Microsoft.AspNetCore.Connections.MemoryPoolOptions.Owner.get -> string?
Microsoft.AspNetCore.Connections.MemoryPoolOptions.Owner.set -> void
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
#nullable enable
Microsoft.AspNetCore.Connections.IMemoryPoolFactory<T>
Microsoft.AspNetCore.Connections.IMemoryPoolFactory<T>.Create() -> System.Buffers.MemoryPool<T>!
Microsoft.AspNetCore.Connections.IMemoryPoolFactory<T>.Create(Microsoft.AspNetCore.Connections.MemoryPoolOptions! options) -> System.Buffers.MemoryPool<T>!
Microsoft.AspNetCore.Connections.MemoryPoolOptions
Microsoft.AspNetCore.Connections.MemoryPoolOptions.MemoryPoolOptions() -> void
Microsoft.AspNetCore.Connections.MemoryPoolOptions.Owner.get -> string?
Microsoft.AspNetCore.Connections.MemoryPoolOptions.Owner.set -> void
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
#nullable enable
Microsoft.AspNetCore.Connections.IMemoryPoolFactory<T>
Microsoft.AspNetCore.Connections.IMemoryPoolFactory<T>.Create() -> System.Buffers.MemoryPool<T>!
Microsoft.AspNetCore.Connections.IMemoryPoolFactory<T>.Create(Microsoft.AspNetCore.Connections.MemoryPoolOptions! options) -> System.Buffers.MemoryPool<T>!
Microsoft.AspNetCore.Connections.MemoryPoolOptions
Microsoft.AspNetCore.Connections.MemoryPoolOptions.MemoryPoolOptions() -> void
Microsoft.AspNetCore.Connections.MemoryPoolOptions.Owner.get -> string?
Microsoft.AspNetCore.Connections.MemoryPoolOptions.Owner.set -> void
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
#nullable enable
Microsoft.AspNetCore.Connections.IMemoryPoolFactory<T>
Microsoft.AspNetCore.Connections.IMemoryPoolFactory<T>.Create() -> System.Buffers.MemoryPool<T>!
Microsoft.AspNetCore.Connections.IMemoryPoolFactory<T>.Create(Microsoft.AspNetCore.Connections.MemoryPoolOptions! options) -> System.Buffers.MemoryPool<T>!
Microsoft.AspNetCore.Connections.MemoryPoolOptions
Microsoft.AspNetCore.Connections.MemoryPoolOptions.MemoryPoolOptions() -> void
Microsoft.AspNetCore.Connections.MemoryPoolOptions.Owner.get -> string?
Microsoft.AspNetCore.Connections.MemoryPoolOptions.Owner.set -> void
2 changes: 1 addition & 1 deletion src/Servers/HttpSys/src/HttpSysListener.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ public HttpSysListener(HttpSysOptions options, IMemoryPoolFactory<byte> memoryPo
throw new PlatformNotSupportedException();
}

MemoryPool = memoryPoolFactory.Create();
MemoryPool = memoryPoolFactory.Create(new MemoryPoolOptions { Owner = "httpsys" });

Options = options;

Expand Down
2 changes: 1 addition & 1 deletion src/Servers/IIS/IIS/src/Core/IISHttpServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ public IISHttpServer(
ILogger<IISHttpServer> logger
)
{
_memoryPool = memoryPoolFactory.Create();
_memoryPool = memoryPoolFactory.Create(new MemoryPoolOptions { Owner = "iis" });
_nativeApplication = nativeApplication;
_applicationLifetime = applicationLifetime;
_logger = logger;
Expand Down
1 change: 1 addition & 0 deletions src/Servers/IIS/IIS/src/WebHostBuilderIISExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ public static IWebHostBuilder UseIIS(this IWebHostBuilder hostBuilder)
);

services.TryAddSingleton<IMemoryPoolFactory<byte>, DefaultMemoryPoolFactory>();
services.TryAddSingleton<MemoryPoolMetrics>();
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@

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;
Expand All @@ -12,22 +11,22 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal;

internal sealed class PinnedBlockMemoryPoolFactory : IMemoryPoolFactory<byte>, IHeartbeatHandler
{
private readonly IMeterFactory _meterFactory;
private readonly MemoryPoolMetrics _metrics;
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)
public PinnedBlockMemoryPoolFactory(MemoryPoolMetrics metrics, TimeProvider? timeProvider = null, ILogger<PinnedBlockMemoryPoolFactory>? logger = null)
{
_timeProvider = timeProvider ?? TimeProvider.System;
_meterFactory = meterFactory;
_metrics = metrics;
_logger = logger;
}

public MemoryPool<byte> Create()
public MemoryPool<byte> Create(MemoryPoolOptions options)
{
var pool = new PinnedBlockMemoryPool(_meterFactory, _logger);
var pool = new PinnedBlockMemoryPool(options.Owner, _metrics, _logger);

_pools.TryAdd(pool, nuint.Zero);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Buffers;
using System.Collections.Concurrent;
using System.Reflection;
using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.InternalTesting;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal;
using Microsoft.Extensions.Time.Testing;
Expand All @@ -15,18 +16,18 @@ public class PinnedBlockMemoryPoolFactoryTests
[Fact]
public void CreatePool()
{
var factory = new PinnedBlockMemoryPoolFactory(new TestMeterFactory());
var pool = factory.Create();
var factory = CreateMemoryPoolFactory();
var pool = factory.Create(new MemoryPoolOptions());
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();
var factory = CreateMemoryPoolFactory();
var pool1 = factory.Create(new MemoryPoolOptions());
var pool2 = factory.Create(new MemoryPoolOptions());

Assert.NotNull(pool1);
Assert.NotNull(pool2);
Expand All @@ -36,8 +37,8 @@ public void CreateMultiplePools()
[Fact]
public void DisposePoolRemovesFromFactory()
{
var factory = new PinnedBlockMemoryPoolFactory(new TestMeterFactory());
var pool = factory.Create();
var factory = CreateMemoryPoolFactory();
var pool = factory.Create(new MemoryPoolOptions());
Assert.NotNull(pool);

var dict = (ConcurrentDictionary<PinnedBlockMemoryPool, nuint>)(typeof(PinnedBlockMemoryPoolFactory)
Expand All @@ -53,11 +54,11 @@ public void DisposePoolRemovesFromFactory()
public async Task FactoryHeartbeatWorks()
{
var timeProvider = new FakeTimeProvider(DateTimeOffset.UtcNow.AddDays(1));
var factory = new PinnedBlockMemoryPoolFactory(new TestMeterFactory(), timeProvider);
var factory = CreateMemoryPoolFactory(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 pool = Assert.IsType<PinnedBlockMemoryPool>(factory.Create(new MemoryPoolOptions()));
var pool2 = Assert.IsType<PinnedBlockMemoryPool>(factory.Create(new MemoryPoolOptions()));

var blocks = new List<IMemoryOwner<byte>>();
for (var i = 0; i < 10000; i++)
Expand Down Expand Up @@ -110,4 +111,11 @@ static async Task VerifyPoolEviction(PinnedBlockMemoryPool pool, int previousCou
Assert.InRange(pool.BlockCount(), previousCount - (previousCount / 10), previousCount - (previousCount / 30));
}
}

private static PinnedBlockMemoryPoolFactory CreateMemoryPoolFactory(TimeProvider timeProvider = null)
{
return new PinnedBlockMemoryPoolFactory(
new MemoryPoolMetrics(new TestMeterFactory()),
timeProvider: timeProvider);
}
}
77 changes: 63 additions & 14 deletions src/Servers/Kestrel/Core/test/PinnedBlockMemoryPoolTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.Buffers;
using System.Diagnostics.Metrics;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.InternalTesting;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal;
Expand Down Expand Up @@ -231,16 +232,21 @@ public async Task EvictionsAreScheduled()
public void CurrentMemoryMetricTracksPooledMemory()
{
var testMeterFactory = new TestMeterFactory();
using var currentMemoryMetric = new MetricCollector<long>(testMeterFactory, "Microsoft.AspNetCore.MemoryPool", "aspnetcore.memory_pool.current_memory");
using var currentMemoryMetric = new MetricCollector<long>(testMeterFactory, "Microsoft.AspNetCore.MemoryPool", MemoryPoolMetrics.UsedMemoryName);

var pool = new PinnedBlockMemoryPool(testMeterFactory);
var pool = CreateMemoryPool(owner: "test", meterFactory: testMeterFactory);

Assert.Empty(currentMemoryMetric.GetMeasurementSnapshot());

var mem = pool.Rent();
mem.Dispose();

Assert.Collection(currentMemoryMetric.GetMeasurementSnapshot(), m => Assert.Equal(PinnedBlockMemoryPool.BlockSize, m.Value));
Assert.Collection(currentMemoryMetric.GetMeasurementSnapshot(),
m =>
{
Assert.Equal(PinnedBlockMemoryPool.BlockSize, m.Value);
Assert.Equal("test", (string)m.Tags["aspnetcore.memory_pool.owner"]);
});

mem = pool.Rent();

Expand All @@ -267,9 +273,9 @@ public void CurrentMemoryMetricTracksPooledMemory()
public void TotalAllocatedMetricTracksAllocatedMemory()
{
var testMeterFactory = new TestMeterFactory();
using var totalMemoryMetric = new MetricCollector<long>(testMeterFactory, "Microsoft.AspNetCore.MemoryPool", "aspnetcore.memory_pool.total_allocated");
using var totalMemoryMetric = new MetricCollector<long>(testMeterFactory, "Microsoft.AspNetCore.MemoryPool", MemoryPoolMetrics.AllocatedMemoryName);

var pool = new PinnedBlockMemoryPool(testMeterFactory);
var pool = CreateMemoryPool(owner: "test", meterFactory: testMeterFactory);

Assert.Empty(totalMemoryMetric.GetMeasurementSnapshot());

Expand All @@ -290,9 +296,9 @@ public void TotalAllocatedMetricTracksAllocatedMemory()
public void TotalRentedMetricTracksRentOperations()
{
var testMeterFactory = new TestMeterFactory();
using var rentMetric = new MetricCollector<long>(testMeterFactory, "Microsoft.AspNetCore.MemoryPool", "aspnetcore.memory_pool.total_rented");
using var rentMetric = new MetricCollector<long>(testMeterFactory, "Microsoft.AspNetCore.MemoryPool", MemoryPoolMetrics.RentedMemoryName);

var pool = new PinnedBlockMemoryPool(testMeterFactory);
var pool = CreateMemoryPool(owner: "test", meterFactory: testMeterFactory);

Assert.Empty(rentMetric.GetMeasurementSnapshot());

Expand All @@ -301,8 +307,16 @@ public void TotalRentedMetricTracksRentOperations()

// 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));
m =>
{
Assert.Equal(PinnedBlockMemoryPool.BlockSize, m.Value);
Assert.Equal("test", (string)m.Tags["aspnetcore.memory_pool.owner"]);
},
m =>
{
Assert.Equal(PinnedBlockMemoryPool.BlockSize, m.Value);
Assert.Equal("test", (string)m.Tags["aspnetcore.memory_pool.owner"]);
});

mem1.Dispose();
mem2.Dispose();
Expand All @@ -315,9 +329,9 @@ public void TotalRentedMetricTracksRentOperations()
public void EvictedMemoryMetricTracksEvictedMemory()
{
var testMeterFactory = new TestMeterFactory();
using var evictMetric = new MetricCollector<long>(testMeterFactory, "Microsoft.AspNetCore.MemoryPool", "aspnetcore.memory_pool.evicted_memory");
using var evictMetric = new MetricCollector<long>(testMeterFactory, "Microsoft.AspNetCore.MemoryPool", MemoryPoolMetrics.EvictedMemoryName);

var pool = new PinnedBlockMemoryPool(testMeterFactory);
var pool = CreateMemoryPool(owner: "test", meterFactory: testMeterFactory);

// Fill the pool with some blocks
var blocks = new List<IMemoryOwner<byte>>();
Expand All @@ -344,6 +358,7 @@ public void EvictedMemoryMetricTracksEvictedMemory()
foreach (var measurement in evictMetric.GetMeasurementSnapshot())
{
Assert.Equal(PinnedBlockMemoryPool.BlockSize, measurement.Value);
Assert.Equal("test", (string)measurement.Tags["aspnetcore.memory_pool.owner"]);
}
}

Expand All @@ -352,10 +367,10 @@ public void EvictedMemoryMetricTracksEvictedMemory()
public void MetricsAreAggregatedAcrossPoolsWithSameMeterFactory()
{
var testMeterFactory = new TestMeterFactory();
using var rentMetric = new MetricCollector<long>(testMeterFactory, "Microsoft.AspNetCore.MemoryPool", "aspnetcore.memory_pool.total_rented");
using var rentMetric = new MetricCollector<long>(testMeterFactory, "Microsoft.AspNetCore.MemoryPool", MemoryPoolMetrics.RentedMemoryName);

var pool1 = new PinnedBlockMemoryPool(testMeterFactory);
var pool2 = new PinnedBlockMemoryPool(testMeterFactory);
var pool1 = CreateMemoryPool(owner: "test", meterFactory: testMeterFactory);
var pool2 = CreateMemoryPool(owner: "test", meterFactory: testMeterFactory);

var mem1 = pool1.Rent();
var mem2 = pool2.Rent();
Expand All @@ -375,4 +390,38 @@ public void MetricsAreAggregatedAcrossPoolsWithSameMeterFactory()
mem3.Dispose();
mem4.Dispose();
}

[Fact]
public void MetricsWithDifferentOwners()
{
var testMeterFactory = new TestMeterFactory();
using var rentMetric = new MetricCollector<long>(testMeterFactory, "Microsoft.AspNetCore.MemoryPool", MemoryPoolMetrics.RentedMemoryName);

var pool1 = CreateMemoryPool(owner: "test1", meterFactory: testMeterFactory);
var pool2 = CreateMemoryPool(owner: "test2", meterFactory: testMeterFactory);

var mem1 = pool1.Rent();
var mem2 = pool2.Rent();

// Both pools should contribute to the same metric stream but with different owners
Assert.Collection(rentMetric.GetMeasurementSnapshot(),
m =>
{
Assert.Equal(PinnedBlockMemoryPool.BlockSize, m.Value);
Assert.Equal("test1", (string)m.Tags["aspnetcore.memory_pool.owner"]);
},
m =>
{
Assert.Equal(PinnedBlockMemoryPool.BlockSize, m.Value);
Assert.Equal("test2", (string)m.Tags["aspnetcore.memory_pool.owner"]);
});

mem1.Dispose();
mem2.Dispose();
}

private static PinnedBlockMemoryPool CreateMemoryPool(string owner, TestMeterFactory meterFactory)
{
return new PinnedBlockMemoryPool(owner: owner, metrics: new MemoryPoolMetrics(meterFactory));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ public static IWebHostBuilder UseKestrelCore(this IWebHostBuilder hostBuilder)
services.AddSingleton<KestrelMetrics>();

services.AddSingleton<PinnedBlockMemoryPoolFactory>();
services.AddSingleton<MemoryPoolMetrics>();
services.TryAddEnumerable(ServiceDescriptor.Singleton<IHeartbeatHandler, PinnedBlockMemoryPoolFactory>(sp => sp.GetRequiredService<PinnedBlockMemoryPoolFactory>()));
services.AddSingleton<IMemoryPoolFactory<byte>>(sp => sp.GetRequiredService<PinnedBlockMemoryPoolFactory>());
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public NamedPipeConnectionListener(
_log = loggerFactory.CreateLogger("Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes");
_endpoint = endpoint;
_options = options;
_memoryPool = options.MemoryPoolFactory.Create();
_memoryPool = options.MemoryPoolFactory.Create(new MemoryPoolOptions { Owner = "kestrel" });
Copy link
Member

Choose a reason for hiding this comment

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

Should these be more specific; kestrel_namedpipe?

Copy link
Member Author

Choose a reason for hiding this comment

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

Originally I had them be specific, e.g. "namedpipe", but the memory pool is set on the connection that is created, which is then used throughout Kestrel for other reasons. I think it would be confusing if a memory pool that says it is owned by named pipes transport is then used by HTTPs middleware for example.

_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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ public static IWebHostBuilder UseNamedPipes(this IWebHostBuilder hostBuilder)
services.AddSingleton<IConnectionListenerFactory, NamedPipeTransportFactory>();

services.TryAddSingleton<IMemoryPoolFactory<byte>, DefaultMemoryPoolFactory>();
services.TryAddSingleton<MemoryPoolMetrics>();
services.AddOptions<NamedPipeTransportOptions>().Configure((NamedPipeTransportOptions options, IMemoryPoolFactory<byte> factory) =>
{
// Set the IMemoryPoolFactory from DI on NamedPipeTransportOptions. Usually this should be the PinnedBlockMemoryPoolFactory from UseKestrelCore.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public SocketConnectionFactory(IOptions<SocketTransportOptions> options, ILogger
ArgumentNullException.ThrowIfNull(loggerFactory);

_options = options.Value;
_memoryPool = options.Value.MemoryPoolFactory.Create();
_memoryPool = options.Value.MemoryPoolFactory.Create(SocketConnectionFactoryOptions.MemoryPoolOptions);
_trace = loggerFactory.CreateLogger("Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.Client");

var maxReadBufferSize = _options.MaxReadBufferSize ?? 0;
Expand Down
Loading
Loading