-
Notifications
You must be signed in to change notification settings - Fork 10.3k
Add IMemoryPoolFactory and cleanup memory pool while idle #61554
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
src/Servers/Kestrel/Kestrel/src/WebHostBuilderKestrelExtensions.cs
Outdated
Show resolved
Hide resolved
private readonly Counter<long> _evictedMemory; | ||
private readonly Counter<long> _usageRate; | ||
|
||
public PinnedBlockMemoryPoolMetrics(IMeterFactory meterFactory) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we want a metric for peak?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
total_allocated covers that? Oh, nvm. Yeah, peak might be interesting.
|
||
private long _currentMemory; | ||
private long _evictedMemory; | ||
private DateTimeOffset _nextEviction = DateTime.UtcNow.AddSeconds(10); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are we going to regret not making this configurable?
{ | ||
} | ||
|
||
public PinnedBlockMemoryPool(IMeterFactory meterFactory) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe we should pass an options object so we can easily add future options
PerformEviction(); | ||
} | ||
|
||
internal void PerformEviction() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you add a comment that explains how this works? Use AI to generate it 😄
|
||
internal sealed class PinnedBlockMemoryPoolMetrics | ||
{ | ||
// Note: Dot separated instead of dash. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is that uncommon?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, it's not uncommon for a meter name to use dots. All of our other meter names used. I think at one point the kestrel event source used dashes, but that's very old. Brennan might have copied that comment from there.
Comment doesn't seem nessessary
|
||
internal sealed class PinnedBlockMemoryPoolMetrics | ||
{ | ||
// Note: Dot separated instead of dash. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, it's not uncommon for a meter name to use dots. All of our other meter names used. I think at one point the kestrel event source used dashes, but that's very old. Brennan might have copied that comment from there.
Comment doesn't seem nessessary
internal sealed class PinnedBlockMemoryPoolMetrics | ||
{ | ||
// Note: Dot separated instead of dash. | ||
public const string MeterName = "System.Buffers.PinnedBlockMemoryPool"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure if these metrics should be on a new meter. Couldn't they be on a meter the same name as Kestrel's meter?
A new meter name means folks would need to configure OTEL to export the kestrel meter, and this System.Buffers.PinnedBlockMemoryPool
meter. Having memory stats related to kestrel be on the Kestrel meter makes more sense to me.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see there are multiple frameworks that use System.Buffers.PinnedBlockMemoryPool
. Maybe it does make sense for it to be its own meter. Although if you have multiple frameworks using the pool at the same time then numbers will get merged together. Is that would you intend?
A solution would be to have an owner
tag that says who used the memory. When a memory pool is created the owner could pass in their name, e.g. kestrel
, sockets
, iis
, etc, and the name is internally passed to counters when they're changed. An owner
tag would let you view total memory usage, vs just Kestrel HTTP layer, vs sockets layer, vs IIS (if running together in the same process)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the meter should be renamed to indicate that it's specific to ASP.NET Core. Maybe Microsoft.AspNetCore.MemoryPool
? That is simple and obvious what the meter is for.
I don't think people care that the underlying pool is implemented using pinned blocks.
_meter = meterFactory.Create(MeterName); | ||
|
||
_currentMemory = _meter.CreateUpDownCounter<long>( | ||
"pinnedblockmemorypool.current_memory", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This name should probably be something like aspnetcore.memorypool.current_memory
.
Use aspnetcore.memorypool.*
as the prefix for counter names.
_usageRate = _meter.CreateCounter<long>( | ||
"pinnedblockmemorypool.usage_rate", | ||
unit: "{bytes}", | ||
description: "Rate of bytes rented from the pool." | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How is this a rate?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's what counters are. It just depends how you view the data.
https://learn.microsoft.com/dotnet/core/diagnostics/metrics-instrumentation#create-a-custom-metric
For Counter instruments, the convention is that collection tools show the total count and/or the rate at which the count is increasing
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The runtime doesn't control whether it will be viewed as a rate so the counter name and description shouldn't presume that it will be. I'd suggest naming it based on the thing that is actually being counted. An example similar to this for total bytes allocated from the GC: RuntimeMetrics.cs
|
||
public PinnedBlockMemoryPoolMetrics(IMeterFactory meterFactory) | ||
{ | ||
_meter = meterFactory.Create(MeterName); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like there could be multiple instances of memory pool used across assemblies with shared source. Fortunatly I believe that as long as the same IMeterFactory
is used for all of them (injected by DI), the meter and counters created will be the same instances. Metrics values will be aggregated and saved to the same location.
However, I think a unit test to double check that is important.
@BrennanConroy FYI naming guidelines for OTEL are here: https://opentelemetry.io/docs/specs/semconv/general/naming/ |
Here’s a review of the updated Key Changes and Features
Strengths
Potential Issues / Suggestions
Summary Table
ConclusionThis is a robust, production-grade adaptive memory pool.
Overall: 👍 Looks great and ready for real workloads. |
Adding the ability to release (to the GC) memory from the
PinnedBlockMemoryPool
. It does this by counting the rent and return calls and using heuristics to decide if it should free up memory or not based on pool usage.Additionally implements the newly approved
IMemoryPoolFactory<T>
interface and plumbs it through Kestrel, IIS, and HttpSys with the default implementation forIMemoryPoolFactory<byte>
being thePinnedBlockMemoryPool
.Kestrel makes use of its heartbeat loop to clean up the memory pools, whereas IIS and HttpSys don't have the equivalent so they use a
PeriodicTimer
approach.Adds metrics to the pool. Issue for those is at #61594.
Closes #61003
Closes #27394
Closes #55490
Did a few perf runs to see the impact of
Interlocked.Increment
on theRent
andReturn
paths.Did longer 60 second runs to make sure the memory pools had plenty of opportunity to run clean up logic and see the impact of incrementing counters.
TLDR; didn't notice a perf impact with this PR and so removed the ScalableApproximateCounting code for now. Can always add it back if we find problems.