From 8f517a2ea4e3cde1d6c00119419066ffbbdb65c6 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Fri, 1 Aug 2025 17:44:35 +0800 Subject: [PATCH 1/7] Server memory pool metrics updates --- .../src/IMemoryPoolFactory.cs | 3 +- .../src/MemoryPoolOptions.cs | 15 +++ .../PublicAPI/net10.0/PublicAPI.Unshipped.txt | 6 +- .../PublicAPI/net462/PublicAPI.Unshipped.txt | 6 +- .../netstandard2.0/PublicAPI.Unshipped.txt | 6 +- .../netstandard2.1/PublicAPI.Unshipped.txt | 6 +- src/Servers/IIS/IIS/src/Core/IISHttpServer.cs | 2 +- .../IIS/src/WebHostBuilderIISExtensions.cs | 1 + .../Internal/PinnedBlockMemoryPoolFactory.cs | 11 +- .../test/PinnedBlockMemoryPoolFactoryTests.cs | 28 ++-- .../Core/test/PinnedBlockMemoryPoolTests.cs | 77 +++++++++-- .../src/WebHostBuilderKestrelExtensions.cs | 1 + .../Internal/NamedPipeConnectionListener.cs | 2 +- .../src/WebHostBuilderNamedPipeExtensions.cs | 1 + .../src/Client/SocketConnectionFactory.cs | 2 +- .../src/SocketConnectionContextFactory.cs | 10 +- .../src/SocketConnectionFactoryOptions.cs | 1 + .../shared/DefaultSimpleMemoryPoolFactory.cs | 2 +- .../Kestrel/shared/test/TestServiceContext.cs | 2 +- .../DiagnosticMemoryPoolFactory.cs | 2 +- .../TestTransport/TestServer.cs | 2 +- .../DefaultMemoryPoolFactory.cs | 10 +- .../Buffers.MemoryPool/MemoryPoolFactory.cs | 10 +- .../Buffers.MemoryPool/MemoryPoolMetrics.cs | 122 ++++++++++++++++++ .../PinnedBlockMemoryPool.cs | 46 +++---- .../PinnedBlockMemoryPoolMetrics.cs | 64 --------- 26 files changed, 292 insertions(+), 146 deletions(-) create mode 100644 src/Servers/Connections.Abstractions/src/MemoryPoolOptions.cs create mode 100644 src/Shared/Buffers.MemoryPool/MemoryPoolMetrics.cs delete mode 100644 src/Shared/Buffers.MemoryPool/PinnedBlockMemoryPoolMetrics.cs diff --git a/src/Servers/Connections.Abstractions/src/IMemoryPoolFactory.cs b/src/Servers/Connections.Abstractions/src/IMemoryPoolFactory.cs index 16ffcee42687..fa28fbb2bfa5 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); } diff --git a/src/Servers/Connections.Abstractions/src/MemoryPoolOptions.cs b/src/Servers/Connections.Abstractions/src/MemoryPoolOptions.cs new file mode 100644 index 000000000000..74abfee0ee75 --- /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 sealed 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..e3298ce057ea 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) -> 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..e3298ce057ea 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) -> 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..e3298ce057ea 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) -> 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..e3298ce057ea 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) -> 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/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..a0fc2c62dda8 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) { - 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..adfdc22ee5cc 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,8 +16,8 @@ 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(pool); } @@ -24,9 +25,9 @@ public void CreatePool() [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); @@ -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)(typeof(PinnedBlockMemoryPoolFactory) @@ -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(factory.Create()); - var pool2 = Assert.IsType(factory.Create()); + var pool = Assert.IsType(factory.Create(new MemoryPoolOptions())); + var pool2 = Assert.IsType(factory.Create(new MemoryPoolOptions())); var blocks = new List>(); for (var i = 0; i < 10000; i++) @@ -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); + } } diff --git a/src/Servers/Kestrel/Core/test/PinnedBlockMemoryPoolTests.cs b/src/Servers/Kestrel/Core/test/PinnedBlockMemoryPoolTests.cs index 1f8d0edebe39..02c624d22624 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; @@ -231,16 +232,21 @@ public async Task EvictionsAreScheduled() public void CurrentMemoryMetricTracksPooledMemory() { 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.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(); @@ -267,9 +273,9 @@ 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()); @@ -290,9 +296,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 +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(); @@ -315,9 +329,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 +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"]); } } @@ -352,10 +367,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 +390,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..3f28915b73a6 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) { return MemoryPool.Shared; } diff --git a/src/Servers/Kestrel/shared/test/TestServiceContext.cs b/src/Servers/Kestrel/shared/test/TestServiceContext.cs index 2987d52f5461..33d55de71c11 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) => _memoryPoolFactory(); } } diff --git a/src/Servers/Kestrel/shared/test/TransportTestHelpers/DiagnosticMemoryPoolFactory.cs b/src/Servers/Kestrel/shared/test/TransportTestHelpers/DiagnosticMemoryPoolFactory.cs index 91a266249cd1..78d4b95f5924 100644 --- a/src/Servers/Kestrel/shared/test/TransportTestHelpers/DiagnosticMemoryPoolFactory.cs +++ b/src/Servers/Kestrel/shared/test/TransportTestHelpers/DiagnosticMemoryPoolFactory.cs @@ -25,7 +25,7 @@ public DiagnosticMemoryPoolFactory(bool allowLateReturn = false, bool rentTracki _pools = new List(); } - public MemoryPool Create() + public MemoryPool Create(MemoryPoolOptions options) { lock (_pools) { 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) { - 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..18f46b27e160 --- /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 UsedMemoryName = "aspnetcore.memory_pool.used"; + 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 _usedMemoryCounter; + private readonly Counter _allocatedMemoryCounter; + private readonly Counter _evictedMemoryCounter; + private readonly Counter _rentedMemoryCounter; + + public MemoryPoolMetrics(IMeterFactory meterFactory) + { + _meter = meterFactory.Create(MeterName); + + _usedMemoryCounter = _meter.CreateUpDownCounter( + UsedMemoryName, + unit: "By", + description: "Number of bytes that are currently used by the pool."); + + _allocatedMemoryCounter = _meter.CreateCounter( + AllocatedMemoryName, + unit: "By", + description: "Total number of allocations made by the pool."); + + _evictedMemoryCounter = _meter.CreateCounter( + EvictedMemoryName, + unit: "By", + description: "Total number of bytes that have been evicted from the pool."); + + _rentedMemoryCounter = _meter.CreateCounter( + RentedMemoryName, + unit: "By", + description: "Total number of bytes rented from the pool."); + } + + public void UpdateUsedMemory(int bytes, string? owner) + { + if (_usedMemoryCounter.Enabled) + { + UpdateUsedMemoryCore(bytes, owner); + } + } + + private void UpdateUsedMemoryCore(int bytes, string? owner) + { + var tags = new TagList(); + AddOwner(ref tags, owner); + + _usedMemoryCounter.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..1154ae24feaa 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,15 +48,14 @@ 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(); @@ -68,9 +67,10 @@ internal sealed class PinnedBlockMemoryPool : MemoryPool, IThreadPoolWorkI /// 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 +99,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?.UpdateUsedMemory(-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 @@ -138,8 +137,7 @@ internal void Return(MemoryPoolBlock block) if (!_isDisposed) { - _metrics?.UpdateCurrentMemory(block.Memory.Length); - Interlocked.Add(ref _currentMemory, block.Memory.Length); + _metrics?.UpdateUsedMemory(block.Memory.Length, _owner); _blocks.Enqueue(block); } @@ -178,8 +176,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 +211,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?.UpdateUsedMemory(-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); - } -} From e8a5921d4c5d653cfd0d08f6e0b8c697de5e54b3 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Fri, 1 Aug 2025 18:05:33 +0800 Subject: [PATCH 2/7] Fix build --- src/Servers/HttpSys/src/HttpSysListener.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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; From d991f4450b27352f0a94e9b1b6fd21bcb8cc87d0 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Sat, 2 Aug 2025 14:35:57 +0800 Subject: [PATCH 3/7] PR feedback --- .../src/IMemoryPoolFactory.cs | 2 +- .../PublicAPI/net10.0/PublicAPI.Unshipped.txt | 2 +- .../PublicAPI/net462/PublicAPI.Unshipped.txt | 2 +- .../netstandard2.0/PublicAPI.Unshipped.txt | 2 +- .../netstandard2.1/PublicAPI.Unshipped.txt | 2 +- .../Internal/PinnedBlockMemoryPoolFactory.cs | 4 ++-- .../test/PinnedBlockMemoryPoolFactoryTests.cs | 21 +++++++++++++------ .../shared/DefaultSimpleMemoryPoolFactory.cs | 2 +- .../Kestrel/shared/test/TestServiceContext.cs | 2 +- .../DiagnosticMemoryPoolFactory.cs | 4 ++-- .../DefaultMemoryPoolFactory.cs | 2 +- .../PinnedBlockMemoryPool.cs | 3 +++ 12 files changed, 30 insertions(+), 18 deletions(-) diff --git a/src/Servers/Connections.Abstractions/src/IMemoryPoolFactory.cs b/src/Servers/Connections.Abstractions/src/IMemoryPoolFactory.cs index fa28fbb2bfa5..1cfa7e7f9215 100644 --- a/src/Servers/Connections.Abstractions/src/IMemoryPoolFactory.cs +++ b/src/Servers/Connections.Abstractions/src/IMemoryPoolFactory.cs @@ -15,5 +15,5 @@ public interface IMemoryPoolFactory /// /// Options for configuring the memory pool. /// A new memory pool instance. - MemoryPool Create(MemoryPoolOptions options); + MemoryPool Create(MemoryPoolOptions? options = null); } 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 e3298ce057ea..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,6 +1,6 @@ #nullable enable Microsoft.AspNetCore.Connections.IMemoryPoolFactory -Microsoft.AspNetCore.Connections.IMemoryPoolFactory.Create(Microsoft.AspNetCore.Connections.MemoryPoolOptions! options) -> 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? 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 e3298ce057ea..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,6 +1,6 @@ #nullable enable Microsoft.AspNetCore.Connections.IMemoryPoolFactory -Microsoft.AspNetCore.Connections.IMemoryPoolFactory.Create(Microsoft.AspNetCore.Connections.MemoryPoolOptions! options) -> 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? 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 e3298ce057ea..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,6 +1,6 @@ #nullable enable Microsoft.AspNetCore.Connections.IMemoryPoolFactory -Microsoft.AspNetCore.Connections.IMemoryPoolFactory.Create(Microsoft.AspNetCore.Connections.MemoryPoolOptions! options) -> 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? 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 e3298ce057ea..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,6 +1,6 @@ #nullable enable Microsoft.AspNetCore.Connections.IMemoryPoolFactory -Microsoft.AspNetCore.Connections.IMemoryPoolFactory.Create(Microsoft.AspNetCore.Connections.MemoryPoolOptions! options) -> 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? diff --git a/src/Servers/Kestrel/Core/src/Internal/PinnedBlockMemoryPoolFactory.cs b/src/Servers/Kestrel/Core/src/Internal/PinnedBlockMemoryPoolFactory.cs index a0fc2c62dda8..777c7aa611df 100644 --- a/src/Servers/Kestrel/Core/src/Internal/PinnedBlockMemoryPoolFactory.cs +++ b/src/Servers/Kestrel/Core/src/Internal/PinnedBlockMemoryPoolFactory.cs @@ -24,9 +24,9 @@ public PinnedBlockMemoryPoolFactory(MemoryPoolMetrics metrics, TimeProvider? tim _logger = logger; } - public MemoryPool Create(MemoryPoolOptions options) + public MemoryPool Create(MemoryPoolOptions? options = null) { - var pool = new PinnedBlockMemoryPool(options.Owner, _metrics, _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 adfdc22ee5cc..815cf572a713 100644 --- a/src/Servers/Kestrel/Core/test/PinnedBlockMemoryPoolFactoryTests.cs +++ b/src/Servers/Kestrel/Core/test/PinnedBlockMemoryPoolFactoryTests.cs @@ -17,17 +17,26 @@ public class PinnedBlockMemoryPoolFactoryTests public void CreatePool() { var factory = CreateMemoryPoolFactory(); - var pool = factory.Create(new MemoryPoolOptions()); + var pool = factory.Create(); Assert.NotNull(pool); Assert.IsType(pool); } + [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 = CreateMemoryPoolFactory(); - var pool1 = factory.Create(new MemoryPoolOptions()); - var pool2 = factory.Create(new MemoryPoolOptions()); + var pool1 = factory.Create(); + var pool2 = factory.Create(); Assert.NotNull(pool1); Assert.NotNull(pool2); @@ -38,7 +47,7 @@ public void CreateMultiplePools() public void DisposePoolRemovesFromFactory() { var factory = CreateMemoryPoolFactory(); - var pool = factory.Create(new MemoryPoolOptions()); + var pool = factory.Create(); Assert.NotNull(pool); var dict = (ConcurrentDictionary)(typeof(PinnedBlockMemoryPoolFactory) @@ -57,8 +66,8 @@ public async Task FactoryHeartbeatWorks() var factory = CreateMemoryPoolFactory(timeProvider); // Use 2 pools to make sure they all get triggered by the heartbeat - var pool = Assert.IsType(factory.Create(new MemoryPoolOptions())); - var pool2 = Assert.IsType(factory.Create(new MemoryPoolOptions())); + var pool = Assert.IsType(factory.Create()); + var pool2 = Assert.IsType(factory.Create()); var blocks = new List>(); for (var i = 0; i < 10000; i++) diff --git a/src/Servers/Kestrel/shared/DefaultSimpleMemoryPoolFactory.cs b/src/Servers/Kestrel/shared/DefaultSimpleMemoryPoolFactory.cs index 3f28915b73a6..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(MemoryPoolOptions options) + 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 33d55de71c11..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(MemoryPoolOptions options) => _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 78d4b95f5924..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(MemoryPoolOptions options) + 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/Shared/Buffers.MemoryPool/DefaultMemoryPoolFactory.cs b/src/Shared/Buffers.MemoryPool/DefaultMemoryPoolFactory.cs index a667b6768d89..46679c1422bb 100644 --- a/src/Shared/Buffers.MemoryPool/DefaultMemoryPoolFactory.cs +++ b/src/Shared/Buffers.MemoryPool/DefaultMemoryPoolFactory.cs @@ -44,7 +44,7 @@ public DefaultMemoryPoolFactory(MemoryPoolMetrics? metrics = null, ILogger Create(MemoryPoolOptions options) + public MemoryPool Create(MemoryPoolOptions? options = null) { var pool = new PinnedBlockMemoryPool(options?.Owner, _metrics, _logger); diff --git a/src/Shared/Buffers.MemoryPool/PinnedBlockMemoryPool.cs b/src/Shared/Buffers.MemoryPool/PinnedBlockMemoryPool.cs index 1154ae24feaa..78d269bbea84 100644 --- a/src/Shared/Buffers.MemoryPool/PinnedBlockMemoryPool.cs +++ b/src/Shared/Buffers.MemoryPool/PinnedBlockMemoryPool.cs @@ -62,6 +62,9 @@ internal sealed class PinnedBlockMemoryPool : MemoryPool, IThreadPoolWorkI 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. /// From 992e4e33ce220b538be6f8ed56f3f1ac2f49416c Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Sat, 2 Aug 2025 14:57:38 +0800 Subject: [PATCH 4/7] Rename used counter --- .../src/MemoryPoolOptions.cs | 2 +- .../test/PinnedBlockMemoryPoolFactoryTests.cs | 1 + .../Buffers.MemoryPool/MemoryPoolMetrics.cs | 30 +++++++++---------- .../PinnedBlockMemoryPool.cs | 14 ++++----- 4 files changed, 24 insertions(+), 23 deletions(-) diff --git a/src/Servers/Connections.Abstractions/src/MemoryPoolOptions.cs b/src/Servers/Connections.Abstractions/src/MemoryPoolOptions.cs index 74abfee0ee75..7d966729a6d1 100644 --- a/src/Servers/Connections.Abstractions/src/MemoryPoolOptions.cs +++ b/src/Servers/Connections.Abstractions/src/MemoryPoolOptions.cs @@ -6,7 +6,7 @@ namespace Microsoft.AspNetCore.Connections; /// /// Options for configuring a memory pool. /// -public sealed class MemoryPoolOptions +public class MemoryPoolOptions { /// /// Gets or sets the owner of the memory pool. This is used for logging and diagnostics purposes. diff --git a/src/Servers/Kestrel/Core/test/PinnedBlockMemoryPoolFactoryTests.cs b/src/Servers/Kestrel/Core/test/PinnedBlockMemoryPoolFactoryTests.cs index 815cf572a713..7ca077aeff26 100644 --- a/src/Servers/Kestrel/Core/test/PinnedBlockMemoryPoolFactoryTests.cs +++ b/src/Servers/Kestrel/Core/test/PinnedBlockMemoryPoolFactoryTests.cs @@ -20,6 +20,7 @@ public void CreatePool() var pool = factory.Create(); Assert.NotNull(pool); Assert.IsType(pool); + Assert.Null(Assert.IsType(pool).Owner); } [Fact] diff --git a/src/Shared/Buffers.MemoryPool/MemoryPoolMetrics.cs b/src/Shared/Buffers.MemoryPool/MemoryPoolMetrics.cs index 18f46b27e160..c80541ba79de 100644 --- a/src/Shared/Buffers.MemoryPool/MemoryPoolMetrics.cs +++ b/src/Shared/Buffers.MemoryPool/MemoryPoolMetrics.cs @@ -12,13 +12,13 @@ internal sealed class MemoryPoolMetrics { public const string MeterName = "Microsoft.AspNetCore.MemoryPool"; - public const string UsedMemoryName = "aspnetcore.memory_pool.used"; + 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 _usedMemoryCounter; + private readonly UpDownCounter _pooledMemoryCounter; private readonly Counter _allocatedMemoryCounter; private readonly Counter _evictedMemoryCounter; private readonly Counter _rentedMemoryCounter; @@ -27,41 +27,41 @@ public MemoryPoolMetrics(IMeterFactory meterFactory) { _meter = meterFactory.Create(MeterName); - _usedMemoryCounter = _meter.CreateUpDownCounter( - UsedMemoryName, + _pooledMemoryCounter = _meter.CreateUpDownCounter( + PooledMemoryName, unit: "By", - description: "Number of bytes that are currently used by the pool."); + description: "Number of bytes currently held by the memory pool and available for reuse."); _allocatedMemoryCounter = _meter.CreateCounter( - AllocatedMemoryName, + AllocatedMemoryName, unit: "By", - description: "Total number of allocations made by the pool."); + description: "Total number of allocations made by the memory pool."); _evictedMemoryCounter = _meter.CreateCounter( - EvictedMemoryName, + EvictedMemoryName, unit: "By", - description: "Total number of bytes that have been evicted from the pool."); + description: "Total number of bytes that have been evicted from the memory pool."); _rentedMemoryCounter = _meter.CreateCounter( RentedMemoryName, unit: "By", - description: "Total number of bytes rented from the pool."); + description: "Total number of bytes rented from the memory pool."); } - public void UpdateUsedMemory(int bytes, string? owner) + public void UpdatePooledMemory(int bytes, string? owner) { - if (_usedMemoryCounter.Enabled) + if (_pooledMemoryCounter.Enabled) { - UpdateUsedMemoryCore(bytes, owner); + UpdatePooledMemoryCore(bytes, owner); } } - private void UpdateUsedMemoryCore(int bytes, string? owner) + private void UpdatePooledMemoryCore(int bytes, string? owner) { var tags = new TagList(); AddOwner(ref tags, owner); - _usedMemoryCounter.Add(bytes, tags); + _pooledMemoryCounter.Add(bytes, tags); } public void AddAllocatedMemory(int bytes, string? owner) diff --git a/src/Shared/Buffers.MemoryPool/PinnedBlockMemoryPool.cs b/src/Shared/Buffers.MemoryPool/PinnedBlockMemoryPool.cs index 78d269bbea84..c50f2b6adcd4 100644 --- a/src/Shared/Buffers.MemoryPool/PinnedBlockMemoryPool.cs +++ b/src/Shared/Buffers.MemoryPool/PinnedBlockMemoryPool.cs @@ -63,7 +63,7 @@ internal sealed class PinnedBlockMemoryPool : MemoryPool, IThreadPoolWorkI private object? _onPoolDisposedState; // Internal for tests. - internal string Owner => _owner; + internal string? Owner => _owner; /// /// This default value passed in to Rent to use the default value for the pool. @@ -102,7 +102,7 @@ public override IMemoryOwner Rent(int size = AnySize) if (_blocks.TryDequeue(out var block)) { - _metrics?.UpdateUsedMemory(-block.Memory.Length, _owner); + _metrics?.UpdatePooledMemory(-block.Memory.Length, _owner); _metrics?.AddRentedMemory(block.Memory.Length, _owner); // block successfully taken from the stack - return it @@ -131,16 +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?.UpdateUsedMemory(block.Memory.Length, _owner); + _metrics?.UpdatePooledMemory(block.Memory.Length, _owner); _blocks.Enqueue(block); } @@ -214,7 +214,7 @@ internal void PerformEviction() // Remove from queue and let GC clean the memory up while (burstAmount > 0 && _blocks.TryDequeue(out var block)) { - _metrics?.UpdateUsedMemory(-block.Memory.Length, _owner); + _metrics?.UpdatePooledMemory(-block.Memory.Length, _owner); _metrics?.AddEvictedMemory(block.Memory.Length, _owner); burstAmount--; From 145105c907ce64aa58cf71985075659cc5c86421 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Sat, 2 Aug 2025 15:22:53 +0800 Subject: [PATCH 5/7] Fix build --- .../Core/test/PinnedBlockMemoryPoolTests.cs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/Servers/Kestrel/Core/test/PinnedBlockMemoryPoolTests.cs b/src/Servers/Kestrel/Core/test/PinnedBlockMemoryPoolTests.cs index 02c624d22624..4a610563471d 100644 --- a/src/Servers/Kestrel/Core/test/PinnedBlockMemoryPoolTests.cs +++ b/src/Servers/Kestrel/Core/test/PinnedBlockMemoryPoolTests.cs @@ -229,10 +229,10 @@ public async Task EvictionsAreScheduled() } [Fact] - public void CurrentMemoryMetricTracksPooledMemory() + public void PooledMemoryMetricTracksPooledMemory() { var testMeterFactory = new TestMeterFactory(); - using var currentMemoryMetric = new MetricCollector(testMeterFactory, "Microsoft.AspNetCore.MemoryPool", MemoryPoolMetrics.UsedMemoryName); + using var currentMemoryMetric = new MetricCollector(testMeterFactory, "Microsoft.AspNetCore.MemoryPool", MemoryPoolMetrics.PooledMemoryName); var pool = CreateMemoryPool(owner: "test", meterFactory: testMeterFactory); @@ -282,6 +282,18 @@ public void TotalAllocatedMetricTracksAllocatedMemory() 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()); From 6040cbafd7120193d5c020592cea07de1d0d6729 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Mon, 4 Aug 2025 12:14:21 +0800 Subject: [PATCH 6/7] Apply suggestions from code review --- src/Shared/Buffers.MemoryPool/MemoryPoolMetrics.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Shared/Buffers.MemoryPool/MemoryPoolMetrics.cs b/src/Shared/Buffers.MemoryPool/MemoryPoolMetrics.cs index c80541ba79de..d4e3d1d11cac 100644 --- a/src/Shared/Buffers.MemoryPool/MemoryPoolMetrics.cs +++ b/src/Shared/Buffers.MemoryPool/MemoryPoolMetrics.cs @@ -30,17 +30,17 @@ public MemoryPoolMetrics(IMeterFactory meterFactory) _pooledMemoryCounter = _meter.CreateUpDownCounter( PooledMemoryName, unit: "By", - description: "Number of bytes currently held by the memory pool and available for reuse."); + description: "Number of bytes currently pooled and available for reuse."); _allocatedMemoryCounter = _meter.CreateCounter( AllocatedMemoryName, unit: "By", - description: "Total number of allocations made by the memory pool."); + 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 that have been evicted from the memory pool."); + description: "Total number of bytes evicted from the memory pool. Eviction occurs when idle pooled memory is reclaimed."); _rentedMemoryCounter = _meter.CreateCounter( RentedMemoryName, From f55f383589be2d0786488929aebf881233742aba Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Fri, 8 Aug 2025 11:36:25 +0800 Subject: [PATCH 7/7] Feedback --- src/Shared/Buffers.MemoryPool/MemoryPoolMetrics.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Shared/Buffers.MemoryPool/MemoryPoolMetrics.cs b/src/Shared/Buffers.MemoryPool/MemoryPoolMetrics.cs index d4e3d1d11cac..a8d085390fcd 100644 --- a/src/Shared/Buffers.MemoryPool/MemoryPoolMetrics.cs +++ b/src/Shared/Buffers.MemoryPool/MemoryPoolMetrics.cs @@ -40,7 +40,7 @@ public MemoryPoolMetrics(IMeterFactory meterFactory) _evictedMemoryCounter = _meter.CreateCounter( EvictedMemoryName, unit: "By", - description: "Total number of bytes evicted from the memory pool. Eviction occurs when idle pooled memory is reclaimed."); + 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,