diff --git a/src/Servers/Connections.Abstractions/src/IMemoryPoolFactory.cs b/src/Servers/Connections.Abstractions/src/IMemoryPoolFactory.cs index 16ffcee42687..1cfa7e7f9215 100644 --- a/src/Servers/Connections.Abstractions/src/IMemoryPoolFactory.cs +++ b/src/Servers/Connections.Abstractions/src/IMemoryPoolFactory.cs @@ -13,6 +13,7 @@ public interface IMemoryPoolFactory /// /// Creates a new instance of a memory pool. /// + /// Options for configuring the memory pool. /// A new memory pool instance. - MemoryPool Create(); + MemoryPool Create(MemoryPoolOptions? options = null); } diff --git a/src/Servers/Connections.Abstractions/src/MemoryPoolOptions.cs b/src/Servers/Connections.Abstractions/src/MemoryPoolOptions.cs new file mode 100644 index 000000000000..7d966729a6d1 --- /dev/null +++ b/src/Servers/Connections.Abstractions/src/MemoryPoolOptions.cs @@ -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; + +/// +/// Options for configuring a memory pool. +/// +public class MemoryPoolOptions +{ + /// + /// Gets or sets the owner of the memory pool. This is used for logging and diagnostics purposes. + /// + public string? Owner { get; set; } +} diff --git a/src/Servers/Connections.Abstractions/src/PublicAPI/net10.0/PublicAPI.Unshipped.txt b/src/Servers/Connections.Abstractions/src/PublicAPI/net10.0/PublicAPI.Unshipped.txt index dec7f8f71c13..4e96c215b0ab 100644 --- a/src/Servers/Connections.Abstractions/src/PublicAPI/net10.0/PublicAPI.Unshipped.txt +++ b/src/Servers/Connections.Abstractions/src/PublicAPI/net10.0/PublicAPI.Unshipped.txt @@ -1,3 +1,7 @@ #nullable enable Microsoft.AspNetCore.Connections.IMemoryPoolFactory -Microsoft.AspNetCore.Connections.IMemoryPoolFactory.Create() -> System.Buffers.MemoryPool! +Microsoft.AspNetCore.Connections.IMemoryPoolFactory.Create(Microsoft.AspNetCore.Connections.MemoryPoolOptions? options = null) -> System.Buffers.MemoryPool! +Microsoft.AspNetCore.Connections.MemoryPoolOptions +Microsoft.AspNetCore.Connections.MemoryPoolOptions.MemoryPoolOptions() -> void +Microsoft.AspNetCore.Connections.MemoryPoolOptions.Owner.get -> string? +Microsoft.AspNetCore.Connections.MemoryPoolOptions.Owner.set -> void diff --git a/src/Servers/Connections.Abstractions/src/PublicAPI/net462/PublicAPI.Unshipped.txt b/src/Servers/Connections.Abstractions/src/PublicAPI/net462/PublicAPI.Unshipped.txt index dec7f8f71c13..4e96c215b0ab 100644 --- a/src/Servers/Connections.Abstractions/src/PublicAPI/net462/PublicAPI.Unshipped.txt +++ b/src/Servers/Connections.Abstractions/src/PublicAPI/net462/PublicAPI.Unshipped.txt @@ -1,3 +1,7 @@ #nullable enable Microsoft.AspNetCore.Connections.IMemoryPoolFactory -Microsoft.AspNetCore.Connections.IMemoryPoolFactory.Create() -> System.Buffers.MemoryPool! +Microsoft.AspNetCore.Connections.IMemoryPoolFactory.Create(Microsoft.AspNetCore.Connections.MemoryPoolOptions? options = null) -> System.Buffers.MemoryPool! +Microsoft.AspNetCore.Connections.MemoryPoolOptions +Microsoft.AspNetCore.Connections.MemoryPoolOptions.MemoryPoolOptions() -> void +Microsoft.AspNetCore.Connections.MemoryPoolOptions.Owner.get -> string? +Microsoft.AspNetCore.Connections.MemoryPoolOptions.Owner.set -> void diff --git a/src/Servers/Connections.Abstractions/src/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt b/src/Servers/Connections.Abstractions/src/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt index dec7f8f71c13..4e96c215b0ab 100644 --- a/src/Servers/Connections.Abstractions/src/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt +++ b/src/Servers/Connections.Abstractions/src/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt @@ -1,3 +1,7 @@ #nullable enable Microsoft.AspNetCore.Connections.IMemoryPoolFactory -Microsoft.AspNetCore.Connections.IMemoryPoolFactory.Create() -> System.Buffers.MemoryPool! +Microsoft.AspNetCore.Connections.IMemoryPoolFactory.Create(Microsoft.AspNetCore.Connections.MemoryPoolOptions? options = null) -> System.Buffers.MemoryPool! +Microsoft.AspNetCore.Connections.MemoryPoolOptions +Microsoft.AspNetCore.Connections.MemoryPoolOptions.MemoryPoolOptions() -> void +Microsoft.AspNetCore.Connections.MemoryPoolOptions.Owner.get -> string? +Microsoft.AspNetCore.Connections.MemoryPoolOptions.Owner.set -> void diff --git a/src/Servers/Connections.Abstractions/src/PublicAPI/netstandard2.1/PublicAPI.Unshipped.txt b/src/Servers/Connections.Abstractions/src/PublicAPI/netstandard2.1/PublicAPI.Unshipped.txt index dec7f8f71c13..4e96c215b0ab 100644 --- a/src/Servers/Connections.Abstractions/src/PublicAPI/netstandard2.1/PublicAPI.Unshipped.txt +++ b/src/Servers/Connections.Abstractions/src/PublicAPI/netstandard2.1/PublicAPI.Unshipped.txt @@ -1,3 +1,7 @@ #nullable enable Microsoft.AspNetCore.Connections.IMemoryPoolFactory -Microsoft.AspNetCore.Connections.IMemoryPoolFactory.Create() -> System.Buffers.MemoryPool! +Microsoft.AspNetCore.Connections.IMemoryPoolFactory.Create(Microsoft.AspNetCore.Connections.MemoryPoolOptions? options = null) -> System.Buffers.MemoryPool! +Microsoft.AspNetCore.Connections.MemoryPoolOptions +Microsoft.AspNetCore.Connections.MemoryPoolOptions.MemoryPoolOptions() -> void +Microsoft.AspNetCore.Connections.MemoryPoolOptions.Owner.get -> string? +Microsoft.AspNetCore.Connections.MemoryPoolOptions.Owner.set -> void diff --git a/src/Servers/HttpSys/src/HttpSysListener.cs b/src/Servers/HttpSys/src/HttpSysListener.cs index ee9d9c081d6d..2aa28395b78e 100644 --- a/src/Servers/HttpSys/src/HttpSysListener.cs +++ b/src/Servers/HttpSys/src/HttpSysListener.cs @@ -55,7 +55,7 @@ public HttpSysListener(HttpSysOptions options, IMemoryPoolFactory memoryPo throw new PlatformNotSupportedException(); } - MemoryPool = memoryPoolFactory.Create(); + MemoryPool = memoryPoolFactory.Create(new MemoryPoolOptions { Owner = "httpsys" }); Options = options; diff --git a/src/Servers/IIS/IIS/src/Core/IISHttpServer.cs b/src/Servers/IIS/IIS/src/Core/IISHttpServer.cs index df0484310b0e..d0309104680f 100644 --- a/src/Servers/IIS/IIS/src/Core/IISHttpServer.cs +++ b/src/Servers/IIS/IIS/src/Core/IISHttpServer.cs @@ -66,7 +66,7 @@ public IISHttpServer( ILogger logger ) { - _memoryPool = memoryPoolFactory.Create(); + _memoryPool = memoryPoolFactory.Create(new MemoryPoolOptions { Owner = "iis" }); _nativeApplication = nativeApplication; _applicationLifetime = applicationLifetime; _logger = logger; diff --git a/src/Servers/IIS/IIS/src/WebHostBuilderIISExtensions.cs b/src/Servers/IIS/IIS/src/WebHostBuilderIISExtensions.cs index b151931e0ae6..44c1094a8a01 100644 --- a/src/Servers/IIS/IIS/src/WebHostBuilderIISExtensions.cs +++ b/src/Servers/IIS/IIS/src/WebHostBuilderIISExtensions.cs @@ -57,6 +57,7 @@ public static IWebHostBuilder UseIIS(this IWebHostBuilder hostBuilder) ); services.TryAddSingleton, DefaultMemoryPoolFactory>(); + services.TryAddSingleton(); }); } diff --git a/src/Servers/Kestrel/Core/src/Internal/PinnedBlockMemoryPoolFactory.cs b/src/Servers/Kestrel/Core/src/Internal/PinnedBlockMemoryPoolFactory.cs index 45870ab6c23f..777c7aa611df 100644 --- a/src/Servers/Kestrel/Core/src/Internal/PinnedBlockMemoryPoolFactory.cs +++ b/src/Servers/Kestrel/Core/src/Internal/PinnedBlockMemoryPoolFactory.cs @@ -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; @@ -12,22 +11,22 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal; internal sealed class PinnedBlockMemoryPoolFactory : IMemoryPoolFactory, 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 _pools = new(); - public PinnedBlockMemoryPoolFactory(IMeterFactory meterFactory, TimeProvider? timeProvider = null, ILogger? logger = null) + public PinnedBlockMemoryPoolFactory(MemoryPoolMetrics metrics, TimeProvider? timeProvider = null, ILogger? logger = null) { _timeProvider = timeProvider ?? TimeProvider.System; - _meterFactory = meterFactory; + _metrics = metrics; _logger = logger; } - public MemoryPool Create() + public MemoryPool Create(MemoryPoolOptions? options = null) { - var pool = new PinnedBlockMemoryPool(_meterFactory, _logger); + var pool = new PinnedBlockMemoryPool(options?.Owner, _metrics, _logger); _pools.TryAdd(pool, nuint.Zero); diff --git a/src/Servers/Kestrel/Core/test/PinnedBlockMemoryPoolFactoryTests.cs b/src/Servers/Kestrel/Core/test/PinnedBlockMemoryPoolFactoryTests.cs index 4d23015459eb..7ca077aeff26 100644 --- a/src/Servers/Kestrel/Core/test/PinnedBlockMemoryPoolFactoryTests.cs +++ b/src/Servers/Kestrel/Core/test/PinnedBlockMemoryPoolFactoryTests.cs @@ -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; @@ -15,16 +16,26 @@ public class PinnedBlockMemoryPoolFactoryTests [Fact] public void CreatePool() { - var factory = new PinnedBlockMemoryPoolFactory(new TestMeterFactory()); + var factory = CreateMemoryPoolFactory(); var pool = factory.Create(); Assert.NotNull(pool); Assert.IsType(pool); + Assert.Null(Assert.IsType(pool).Owner); + } + + [Fact] + public void CreatePoolWithOwner() + { + var factory = CreateMemoryPoolFactory(); + var pool = factory.Create(new MemoryPoolOptions { Owner = "test" }); + Assert.NotNull(pool); + Assert.Equal("test", Assert.IsType(pool).Owner); } [Fact] public void CreateMultiplePools() { - var factory = new PinnedBlockMemoryPoolFactory(new TestMeterFactory()); + var factory = CreateMemoryPoolFactory(); var pool1 = factory.Create(); var pool2 = factory.Create(); @@ -36,7 +47,7 @@ public void CreateMultiplePools() [Fact] public void DisposePoolRemovesFromFactory() { - var factory = new PinnedBlockMemoryPoolFactory(new TestMeterFactory()); + var factory = CreateMemoryPoolFactory(); var pool = factory.Create(); Assert.NotNull(pool); @@ -53,7 +64,7 @@ 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(factory.Create()); @@ -110,4 +121,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); + } } diff --git a/src/Servers/Kestrel/Core/test/PinnedBlockMemoryPoolTests.cs b/src/Servers/Kestrel/Core/test/PinnedBlockMemoryPoolTests.cs index 1f8d0edebe39..4a610563471d 100644 --- a/src/Servers/Kestrel/Core/test/PinnedBlockMemoryPoolTests.cs +++ b/src/Servers/Kestrel/Core/test/PinnedBlockMemoryPoolTests.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Buffers; +using System.Diagnostics.Metrics; using Microsoft.AspNetCore; using Microsoft.AspNetCore.InternalTesting; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal; @@ -228,19 +229,24 @@ public async Task EvictionsAreScheduled() } [Fact] - public void CurrentMemoryMetricTracksPooledMemory() + public void PooledMemoryMetricTracksPooledMemory() { var testMeterFactory = new TestMeterFactory(); - using var currentMemoryMetric = new MetricCollector(testMeterFactory, "Microsoft.AspNetCore.MemoryPool", "aspnetcore.memory_pool.current_memory"); + using var currentMemoryMetric = new MetricCollector(testMeterFactory, "Microsoft.AspNetCore.MemoryPool", MemoryPoolMetrics.PooledMemoryName); - 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(); @@ -267,15 +273,27 @@ public void CurrentMemoryMetricTracksPooledMemory() public void TotalAllocatedMetricTracksAllocatedMemory() { var testMeterFactory = new TestMeterFactory(); - using var totalMemoryMetric = new MetricCollector(testMeterFactory, "Microsoft.AspNetCore.MemoryPool", "aspnetcore.memory_pool.total_allocated"); + using var totalMemoryMetric = new MetricCollector(testMeterFactory, "Microsoft.AspNetCore.MemoryPool", MemoryPoolMetrics.AllocatedMemoryName); - var pool = new PinnedBlockMemoryPool(testMeterFactory); + var pool = CreateMemoryPool(owner: "test", meterFactory: testMeterFactory); Assert.Empty(totalMemoryMetric.GetMeasurementSnapshot()); var mem1 = pool.Rent(); var mem2 = pool.Rent(); + Assert.Collection(totalMemoryMetric.GetMeasurementSnapshot(), + 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"]); + }); + // Each Rent that allocates a new block should increment total memory by block size Assert.Equal(2 * PinnedBlockMemoryPool.BlockSize, totalMemoryMetric.GetMeasurementSnapshot().EvaluateAsCounter()); @@ -290,9 +308,9 @@ public void TotalAllocatedMetricTracksAllocatedMemory() public void TotalRentedMetricTracksRentOperations() { var testMeterFactory = new TestMeterFactory(); - using var rentMetric = new MetricCollector(testMeterFactory, "Microsoft.AspNetCore.MemoryPool", "aspnetcore.memory_pool.total_rented"); + using var rentMetric = new MetricCollector(testMeterFactory, "Microsoft.AspNetCore.MemoryPool", MemoryPoolMetrics.RentedMemoryName); - var pool = new PinnedBlockMemoryPool(testMeterFactory); + var pool = CreateMemoryPool(owner: "test", meterFactory: testMeterFactory); Assert.Empty(rentMetric.GetMeasurementSnapshot()); @@ -301,8 +319,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(); @@ -315,9 +341,9 @@ public void TotalRentedMetricTracksRentOperations() public void EvictedMemoryMetricTracksEvictedMemory() { var testMeterFactory = new TestMeterFactory(); - using var evictMetric = new MetricCollector(testMeterFactory, "Microsoft.AspNetCore.MemoryPool", "aspnetcore.memory_pool.evicted_memory"); + using var evictMetric = new MetricCollector(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>(); @@ -344,6 +370,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"]); } } @@ -352,10 +379,10 @@ public void EvictedMemoryMetricTracksEvictedMemory() public void MetricsAreAggregatedAcrossPoolsWithSameMeterFactory() { var testMeterFactory = new TestMeterFactory(); - using var rentMetric = new MetricCollector(testMeterFactory, "Microsoft.AspNetCore.MemoryPool", "aspnetcore.memory_pool.total_rented"); + using var rentMetric = new MetricCollector(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(); @@ -375,4 +402,38 @@ public void MetricsAreAggregatedAcrossPoolsWithSameMeterFactory() mem3.Dispose(); mem4.Dispose(); } + + [Fact] + public void MetricsWithDifferentOwners() + { + var testMeterFactory = new TestMeterFactory(); + using var rentMetric = new MetricCollector(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)); + } } diff --git a/src/Servers/Kestrel/Kestrel/src/WebHostBuilderKestrelExtensions.cs b/src/Servers/Kestrel/Kestrel/src/WebHostBuilderKestrelExtensions.cs index 0869ba8169b8..47a2962d005b 100644 --- a/src/Servers/Kestrel/Kestrel/src/WebHostBuilderKestrelExtensions.cs +++ b/src/Servers/Kestrel/Kestrel/src/WebHostBuilderKestrelExtensions.cs @@ -87,6 +87,7 @@ public static IWebHostBuilder UseKestrelCore(this IWebHostBuilder hostBuilder) services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.TryAddEnumerable(ServiceDescriptor.Singleton(sp => sp.GetRequiredService())); services.AddSingleton>(sp => sp.GetRequiredService()); }); diff --git a/src/Servers/Kestrel/Transport.NamedPipes/src/Internal/NamedPipeConnectionListener.cs b/src/Servers/Kestrel/Transport.NamedPipes/src/Internal/NamedPipeConnectionListener.cs index 39465a4f3219..f5d8c21ae6fb 100644 --- a/src/Servers/Kestrel/Transport.NamedPipes/src/Internal/NamedPipeConnectionListener.cs +++ b/src/Servers/Kestrel/Transport.NamedPipes/src/Internal/NamedPipeConnectionListener.cs @@ -40,7 +40,7 @@ public NamedPipeConnectionListener( _log = loggerFactory.CreateLogger("Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes"); _endpoint = endpoint; _options = options; - _memoryPool = options.MemoryPoolFactory.Create(); + _memoryPool = options.MemoryPoolFactory.Create(new MemoryPoolOptions { Owner = "kestrel" }); _listeningToken = _listeningTokenSource.Token; // Have to create the pool here (instead of DI) because the pool is specific to an endpoint. _poolPolicy = new NamedPipeServerStreamPoolPolicy(endpoint, options); diff --git a/src/Servers/Kestrel/Transport.NamedPipes/src/WebHostBuilderNamedPipeExtensions.cs b/src/Servers/Kestrel/Transport.NamedPipes/src/WebHostBuilderNamedPipeExtensions.cs index 3b64cc2c4f7e..e9d22d044b24 100644 --- a/src/Servers/Kestrel/Transport.NamedPipes/src/WebHostBuilderNamedPipeExtensions.cs +++ b/src/Servers/Kestrel/Transport.NamedPipes/src/WebHostBuilderNamedPipeExtensions.cs @@ -35,6 +35,7 @@ public static IWebHostBuilder UseNamedPipes(this IWebHostBuilder hostBuilder) services.AddSingleton(); services.TryAddSingleton, DefaultMemoryPoolFactory>(); + services.TryAddSingleton(); services.AddOptions().Configure((NamedPipeTransportOptions options, IMemoryPoolFactory factory) => { // Set the IMemoryPoolFactory from DI on NamedPipeTransportOptions. Usually this should be the PinnedBlockMemoryPoolFactory from UseKestrelCore. diff --git a/src/Servers/Kestrel/Transport.Sockets/src/Client/SocketConnectionFactory.cs b/src/Servers/Kestrel/Transport.Sockets/src/Client/SocketConnectionFactory.cs index ef6f2c771579..39b7523a7c7c 100644 --- a/src/Servers/Kestrel/Transport.Sockets/src/Client/SocketConnectionFactory.cs +++ b/src/Servers/Kestrel/Transport.Sockets/src/Client/SocketConnectionFactory.cs @@ -27,7 +27,7 @@ public SocketConnectionFactory(IOptions options, ILogger ArgumentNullException.ThrowIfNull(loggerFactory); _options = options.Value; - _memoryPool = options.Value.MemoryPoolFactory.Create(); + _memoryPool = options.Value.MemoryPoolFactory.Create(SocketConnectionFactoryOptions.MemoryPoolOptions); _trace = loggerFactory.CreateLogger("Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.Client"); var maxReadBufferSize = _options.MaxReadBufferSize ?? 0; diff --git a/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionContextFactory.cs b/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionContextFactory.cs index 86d2ec2a2d22..e2bc50ef499e 100644 --- a/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionContextFactory.cs +++ b/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionContextFactory.cs @@ -47,7 +47,7 @@ public SocketConnectionContextFactory(SocketConnectionFactoryOptions options, IL for (var i = 0; i < _settingsCount; i++) { - var memoryPool = _options.MemoryPoolFactory.Create(); + var memoryPool = _options.MemoryPoolFactory.Create(SocketConnectionFactoryOptions.MemoryPoolOptions); var transportScheduler = options.UnsafePreferInlineScheduling ? PipeScheduler.Inline : new IOQueue(); _settings[i] = new QueueSettings() @@ -62,11 +62,11 @@ public SocketConnectionContextFactory(SocketConnectionFactoryOptions options, IL } else { - var memoryPool = _options.MemoryPoolFactory.Create(); + var memoryPool = _options.MemoryPoolFactory.Create(SocketConnectionFactoryOptions.MemoryPoolOptions); var transportScheduler = options.UnsafePreferInlineScheduling ? PipeScheduler.Inline : PipeScheduler.ThreadPool; - _settings = new QueueSettings[] - { + _settings = + [ new QueueSettings() { Scheduler = transportScheduler, @@ -75,7 +75,7 @@ public SocketConnectionContextFactory(SocketConnectionFactoryOptions options, IL SocketSenderPool = new SocketSenderPool(PipeScheduler.Inline), MemoryPool = memoryPool, } - }; + ]; _settingsCount = 1; } } diff --git a/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionFactoryOptions.cs b/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionFactoryOptions.cs index e77950c87c8e..8290c5602f6c 100644 --- a/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionFactoryOptions.cs +++ b/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionFactoryOptions.cs @@ -69,4 +69,5 @@ internal SocketConnectionFactoryOptions(SocketTransportOptions transportOptions) public bool UnsafePreferInlineScheduling { get; set; } internal IMemoryPoolFactory MemoryPoolFactory { get; set; } = DefaultSimpleMemoryPoolFactory.Instance; + internal static readonly MemoryPoolOptions MemoryPoolOptions = new MemoryPoolOptions { Owner = "kestrel" }; } diff --git a/src/Servers/Kestrel/shared/DefaultSimpleMemoryPoolFactory.cs b/src/Servers/Kestrel/shared/DefaultSimpleMemoryPoolFactory.cs index d3c4f94c333c..9c36cd0cf8fc 100644 --- a/src/Servers/Kestrel/shared/DefaultSimpleMemoryPoolFactory.cs +++ b/src/Servers/Kestrel/shared/DefaultSimpleMemoryPoolFactory.cs @@ -10,7 +10,7 @@ internal sealed class DefaultSimpleMemoryPoolFactory : IMemoryPoolFactory { public static DefaultSimpleMemoryPoolFactory Instance { get; } = new DefaultSimpleMemoryPoolFactory(); - public MemoryPool Create() + public MemoryPool Create(MemoryPoolOptions? options = null) { return MemoryPool.Shared; } diff --git a/src/Servers/Kestrel/shared/test/TestServiceContext.cs b/src/Servers/Kestrel/shared/test/TestServiceContext.cs index 2987d52f5461..30b7135d939b 100644 --- a/src/Servers/Kestrel/shared/test/TestServiceContext.cs +++ b/src/Servers/Kestrel/shared/test/TestServiceContext.cs @@ -87,6 +87,6 @@ public WrappingMemoryPoolFactory(Func> memoryPoolFactory) _memoryPoolFactory = memoryPoolFactory; } - public MemoryPool Create() => _memoryPoolFactory(); + public MemoryPool Create(MemoryPoolOptions options = null) => _memoryPoolFactory(); } } diff --git a/src/Servers/Kestrel/shared/test/TransportTestHelpers/DiagnosticMemoryPoolFactory.cs b/src/Servers/Kestrel/shared/test/TransportTestHelpers/DiagnosticMemoryPoolFactory.cs index 91a266249cd1..ebaeec3f3074 100644 --- a/src/Servers/Kestrel/shared/test/TransportTestHelpers/DiagnosticMemoryPoolFactory.cs +++ b/src/Servers/Kestrel/shared/test/TransportTestHelpers/DiagnosticMemoryPoolFactory.cs @@ -25,11 +25,11 @@ public DiagnosticMemoryPoolFactory(bool allowLateReturn = false, bool rentTracki _pools = new List(); } - public MemoryPool Create() + public MemoryPool Create(MemoryPoolOptions options = null) { lock (_pools) { - var pool = new DiagnosticMemoryPool(new PinnedBlockMemoryPool(), _allowLateReturn, _rentTracking); + var pool = new DiagnosticMemoryPool(new PinnedBlockMemoryPool(options?.Owner), _allowLateReturn, _rentTracking); _pools.Add(pool); return pool; } diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/TestTransport/TestServer.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/TestTransport/TestServer.cs index e6d4f2edef29..5270e2a551b2 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/TestTransport/TestServer.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/TestTransport/TestServer.cs @@ -70,7 +70,7 @@ public TestServer(RequestDelegate app, TestServiceContext context, Action, IAsyncDisposable { - private readonly IMeterFactory? _meterFactory; + private readonly MemoryPoolMetrics? _metrics; private readonly ConcurrentDictionary _pools = new(); private readonly PeriodicTimer _timer; private readonly Task _timerTask; private readonly ILogger? _logger; - public DefaultMemoryPoolFactory(IMeterFactory? meterFactory = null, ILogger? logger = null) + public DefaultMemoryPoolFactory(MemoryPoolMetrics? metrics = null, ILogger? logger = null) { - _meterFactory = meterFactory; + _metrics = metrics; _logger = logger; _timer = new PeriodicTimer(PinnedBlockMemoryPool.DefaultEvictionDelay); _timerTask = Task.Run(async () => @@ -44,9 +44,9 @@ public DefaultMemoryPoolFactory(IMeterFactory? meterFactory = null, ILogger Create() + public MemoryPool Create(MemoryPoolOptions? options = null) { - var pool = new PinnedBlockMemoryPool(_meterFactory, _logger); + var pool = new PinnedBlockMemoryPool(options?.Owner, _metrics, _logger); _pools.TryAdd(pool, true); diff --git a/src/Shared/Buffers.MemoryPool/MemoryPoolFactory.cs b/src/Shared/Buffers.MemoryPool/MemoryPoolFactory.cs index 24679e1be23d..204f9b789818 100644 --- a/src/Shared/Buffers.MemoryPool/MemoryPoolFactory.cs +++ b/src/Shared/Buffers.MemoryPool/MemoryPoolFactory.cs @@ -10,17 +10,17 @@ namespace Microsoft.AspNetCore; internal static class TestMemoryPoolFactory { - public static MemoryPool Create(IMeterFactory? meterFactory = null) + public static MemoryPool Create(string? owner = null, IMeterFactory? meterFactory = null) { #if DEBUG - return new DiagnosticMemoryPool(CreatePinnedBlockMemoryPool(meterFactory)); + return new DiagnosticMemoryPool(CreatePinnedBlockMemoryPool(owner, meterFactory)); #else - return CreatePinnedBlockMemoryPool(meterFactory); + return CreatePinnedBlockMemoryPool(owner, meterFactory); #endif } - public static MemoryPool CreatePinnedBlockMemoryPool(IMeterFactory? meterFactory = null) + public static MemoryPool CreatePinnedBlockMemoryPool(string? owner = null, IMeterFactory? meterFactory = null) { - return new PinnedBlockMemoryPool(meterFactory); + return new PinnedBlockMemoryPool(owner: owner, metrics: meterFactory != null ? new MemoryPoolMetrics(meterFactory) : null); } } diff --git a/src/Shared/Buffers.MemoryPool/MemoryPoolMetrics.cs b/src/Shared/Buffers.MemoryPool/MemoryPoolMetrics.cs new file mode 100644 index 000000000000..a8d085390fcd --- /dev/null +++ b/src/Shared/Buffers.MemoryPool/MemoryPoolMetrics.cs @@ -0,0 +1,122 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.Diagnostics.Metrics; + +#nullable enable + +namespace Microsoft.AspNetCore; + +internal sealed class MemoryPoolMetrics +{ + public const string MeterName = "Microsoft.AspNetCore.MemoryPool"; + + public const string PooledMemoryName = "aspnetcore.memory_pool.pooled"; + public const string AllocatedMemoryName = "aspnetcore.memory_pool.allocated"; + public const string EvictedMemoryName = "aspnetcore.memory_pool.evicted"; + public const string RentedMemoryName = "aspnetcore.memory_pool.rented"; + + private readonly Meter _meter; + private readonly UpDownCounter _pooledMemoryCounter; + private readonly Counter _allocatedMemoryCounter; + private readonly Counter _evictedMemoryCounter; + private readonly Counter _rentedMemoryCounter; + + public MemoryPoolMetrics(IMeterFactory meterFactory) + { + _meter = meterFactory.Create(MeterName); + + _pooledMemoryCounter = _meter.CreateUpDownCounter( + PooledMemoryName, + unit: "By", + description: "Number of bytes currently pooled and available for reuse."); + + _allocatedMemoryCounter = _meter.CreateCounter( + AllocatedMemoryName, + unit: "By", + description: "Total number of bytes allocated by the memory pool. Allocation occurs when a memory rental request exceeds the available pooled memory."); + + _evictedMemoryCounter = _meter.CreateCounter( + EvictedMemoryName, + unit: "By", + description: "Total number of bytes evicted from the memory pool. Eviction occurs when idle pooled memory is reclaimed. Evicted memory is available for garbage collection."); + + _rentedMemoryCounter = _meter.CreateCounter( + RentedMemoryName, + unit: "By", + description: "Total number of bytes rented from the memory pool."); + } + + public void UpdatePooledMemory(int bytes, string? owner) + { + if (_pooledMemoryCounter.Enabled) + { + UpdatePooledMemoryCore(bytes, owner); + } + } + + private void UpdatePooledMemoryCore(int bytes, string? owner) + { + var tags = new TagList(); + AddOwner(ref tags, owner); + + _pooledMemoryCounter.Add(bytes, tags); + } + + public void AddAllocatedMemory(int bytes, string? owner) + { + if (_allocatedMemoryCounter.Enabled) + { + AddAllocatedMemoryCore(bytes, owner); + } + } + + private void AddAllocatedMemoryCore(int bytes, string? owner) + { + var tags = new TagList(); + AddOwner(ref tags, owner); + + _allocatedMemoryCounter.Add(bytes, tags); + } + + public void AddEvictedMemory(int bytes, string? owner) + { + if (_evictedMemoryCounter.Enabled) + { + AddEvictedMemoryCore(bytes, owner); + } + } + + private void AddEvictedMemoryCore(int bytes, string? owner) + { + var tags = new TagList(); + AddOwner(ref tags, owner); + + _evictedMemoryCounter.Add(bytes, tags); + } + + public void AddRentedMemory(int bytes, string? owner) + { + if (_rentedMemoryCounter.Enabled) + { + AddRentedMemoryCore(bytes, owner); + } + } + + private void AddRentedMemoryCore(int bytes, string? owner) + { + var tags = new TagList(); + AddOwner(ref tags, owner); + + _rentedMemoryCounter.Add(bytes, tags); + } + + private static void AddOwner(ref TagList tags, string? owner) + { + if (!string.IsNullOrEmpty(owner)) + { + tags.Add("aspnetcore.memory_pool.owner", owner); + } + } +} diff --git a/src/Shared/Buffers.MemoryPool/PinnedBlockMemoryPool.cs b/src/Shared/Buffers.MemoryPool/PinnedBlockMemoryPool.cs index 4e6be0f91052..c50f2b6adcd4 100644 --- a/src/Shared/Buffers.MemoryPool/PinnedBlockMemoryPool.cs +++ b/src/Shared/Buffers.MemoryPool/PinnedBlockMemoryPool.cs @@ -27,15 +27,15 @@ internal sealed class PinnedBlockMemoryPool : MemoryPool, IThreadPoolWorkI public static readonly TimeSpan DefaultEvictionDelay = TimeSpan.FromSeconds(10); /// - /// Max allocation block size for pooled blocks, - /// larger values can be leased but they will be disposed after use rather than returned to the pool. + /// The size of a block. 4096 is chosen because most operating systems use 4k pages. /// - public override int MaxBufferSize { get; } = _blockSize; + public static int BlockSize => _blockSize; /// - /// The size of a block. 4096 is chosen because most operating systems use 4k pages. + /// 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. /// - public static int BlockSize => _blockSize; + public override int MaxBufferSize { get; } = _blockSize; /// /// Thread-safe collection of blocks which are currently in the pool. A slab will pre-allocate all of the block tracking objects @@ -48,29 +48,32 @@ internal sealed class PinnedBlockMemoryPool : MemoryPool, IThreadPoolWorkI /// private bool _isDisposed; // To detect redundant calls - private readonly PinnedBlockMemoryPoolMetrics? _metrics; + private readonly string? _owner; + private readonly MemoryPoolMetrics? _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 ulong _rentCount; + private ulong _returnCount; private readonly object _disposeSync = new object(); private Action? _onPoolDisposed; private object? _onPoolDisposedState; + // Internal for tests. + internal string? Owner => _owner; + /// /// This default value passed in to Rent to use the default value for the pool. /// private const int AnySize = -1; - public PinnedBlockMemoryPool(IMeterFactory? meterFactory = null, ILogger? logger = null) + public PinnedBlockMemoryPool(string? owner = null, MemoryPoolMetrics? metrics = null, ILogger? logger = null) { - _metrics = meterFactory is null ? null : new PinnedBlockMemoryPoolMetrics(meterFactory); + _metrics = metrics; + _owner = owner; _logger = logger; } @@ -99,16 +102,15 @@ public override IMemoryOwner Rent(int size = AnySize) if (_blocks.TryDequeue(out var block)) { - _metrics?.UpdateCurrentMemory(-block.Memory.Length); - _metrics?.Rent(block.Memory.Length); - Interlocked.Add(ref _currentMemory, -block.Memory.Length); + _metrics?.UpdatePooledMemory(-block.Memory.Length, _owner); + _metrics?.AddRentedMemory(block.Memory.Length, _owner); // block successfully taken from the stack - return it return block; } - _metrics?.IncrementTotalMemory(BlockSize); - _metrics?.Rent(BlockSize); + _metrics?.AddAllocatedMemory(BlockSize, _owner); + _metrics?.AddRentedMemory(BlockSize, _owner); // 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 @@ -129,17 +131,16 @@ public override IMemoryOwner Rent(int size = AnySize) internal void Return(MemoryPoolBlock block) { #if BLOCK_LEASE_TRACKING - Debug.Assert(block.Pool == this, "Returned block was not leased from this pool"); - Debug.Assert(block.IsLeased, $"Block being returned to pool twice: {block.Leaser}{Environment.NewLine}"); - block.IsLeased = false; + Debug.Assert(block.Pool == this, "Returned block was not leased from this pool"); + Debug.Assert(block.IsLeased, $"Block being returned to pool twice: {block.Leaser}{Environment.NewLine}"); + block.IsLeased = false; #endif Interlocked.Increment(ref _returnCount); if (!_isDisposed) { - _metrics?.UpdateCurrentMemory(block.Memory.Length); - Interlocked.Add(ref _currentMemory, block.Memory.Length); + _metrics?.UpdatePooledMemory(block.Memory.Length, _owner); _blocks.Enqueue(block); } @@ -178,8 +179,8 @@ void IThreadPoolWorkItem.Execute() /// internal void PerformEviction() { - var currentCount = (uint)_blocks.Count; - var burstAmount = 0u; + var currentCount = (ulong)_blocks.Count; + var burstAmount = 0ul; var rentCount = _rentCount; var returnCount = _returnCount; @@ -213,10 +214,8 @@ internal void PerformEviction() // Remove from queue and let GC clean the memory up while (burstAmount > 0 && _blocks.TryDequeue(out var block)) { - _metrics?.UpdateCurrentMemory(-block.Memory.Length); - _metrics?.EvictBlock(block.Memory.Length); - Interlocked.Add(ref _currentMemory, -block.Memory.Length); - Interlocked.Add(ref _evictedMemory, block.Memory.Length); + _metrics?.UpdatePooledMemory(-block.Memory.Length, _owner); + _metrics?.AddEvictedMemory(block.Memory.Length, _owner); burstAmount--; } diff --git a/src/Shared/Buffers.MemoryPool/PinnedBlockMemoryPoolMetrics.cs b/src/Shared/Buffers.MemoryPool/PinnedBlockMemoryPoolMetrics.cs deleted file mode 100644 index 0211f2bc610c..000000000000 --- a/src/Shared/Buffers.MemoryPool/PinnedBlockMemoryPoolMetrics.cs +++ /dev/null @@ -1,64 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Diagnostics.Metrics; - -#nullable enable - -namespace Microsoft.AspNetCore; - -internal sealed class PinnedBlockMemoryPoolMetrics -{ - public const string MeterName = "Microsoft.AspNetCore.MemoryPool"; - - private readonly Meter _meter; - private readonly UpDownCounter _currentMemory; - private readonly Counter _totalAllocatedMemory; - private readonly Counter _evictedMemory; - private readonly Counter _totalRented; - - public PinnedBlockMemoryPoolMetrics(IMeterFactory meterFactory) - { - _meter = meterFactory.Create(MeterName); - - _currentMemory = _meter.CreateUpDownCounter( - "aspnetcore.memory_pool.current_memory", - unit: "By", - description: "Number of bytes that are currently pooled by the pool."); - - _totalAllocatedMemory = _meter.CreateCounter( - "aspnetcore.memory_pool.total_allocated", - unit: "By", - description: "Total number of allocations made by the pool."); - - _evictedMemory = _meter.CreateCounter( - "aspnetcore.memory_pool.evicted_memory", - unit: "By", - description: "Total number of bytes that have been evicted."); - - _totalRented = _meter.CreateCounter( - "aspnetcore.memory_pool.total_rented", - unit: "By", - 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); - } -}