22// The .NET Foundation licenses this file to you under the MIT license.
33
44using System . Buffers ;
5+ using Microsoft . AspNetCore ;
6+ using Microsoft . AspNetCore . InternalTesting ;
7+ using Microsoft . AspNetCore . Server . Kestrel . Core . Internal ;
8+ using Microsoft . Extensions . Diagnostics . Metrics . Testing ;
9+ using Microsoft . Extensions . Time . Testing ;
510
611namespace Microsoft . Extensions . Internal . Test ;
712
@@ -192,9 +197,10 @@ public async Task EvictionsAreScheduled()
192197 var previousCount = memoryPool . BlockCount ( ) ;
193198
194199 // Scheduling only works every 10 seconds and is initialized to UtcNow + 10 when the pool is constructed
195- Assert . False ( memoryPool . TryScheduleEviction ( DateTime . UtcNow ) ) ;
200+ var time = DateTime . UtcNow ;
201+ Assert . False ( memoryPool . TryScheduleEviction ( time ) ) ;
196202
197- Assert . True ( memoryPool . TryScheduleEviction ( DateTime . UtcNow . AddSeconds ( 10 ) ) ) ;
203+ Assert . True ( memoryPool . TryScheduleEviction ( time . AddSeconds ( 10 ) ) ) ;
198204
199205 var maxWait = TimeSpan . FromSeconds ( 5 ) ;
200206 while ( memoryPool . BlockCount ( ) > previousCount - ( previousCount / 30 ) && maxWait > TimeSpan . Zero )
@@ -206,10 +212,10 @@ public async Task EvictionsAreScheduled()
206212 Assert . InRange ( memoryPool . BlockCount ( ) , previousCount - ( previousCount / 10 ) , previousCount - ( previousCount / 30 ) ) ;
207213
208214 // Since we scheduled successfully, we now need to wait 10 seconds to schedule again.
209- Assert . False ( memoryPool . TryScheduleEviction ( DateTime . UtcNow . AddSeconds ( 10 ) ) ) ;
215+ Assert . False ( memoryPool . TryScheduleEviction ( time . AddSeconds ( 10 ) ) ) ;
210216
211217 previousCount = memoryPool . BlockCount ( ) ;
212- Assert . True ( memoryPool . TryScheduleEviction ( DateTime . UtcNow . AddSeconds ( 20 ) ) ) ;
218+ Assert . True ( memoryPool . TryScheduleEviction ( time . AddSeconds ( 20 ) ) ) ;
213219
214220 maxWait = TimeSpan . FromSeconds ( 5 ) ;
215221 while ( memoryPool . BlockCount ( ) > previousCount - ( previousCount / 30 ) && maxWait > TimeSpan . Zero )
@@ -220,4 +226,153 @@ public async Task EvictionsAreScheduled()
220226
221227 Assert . InRange ( memoryPool . BlockCount ( ) , previousCount - ( previousCount / 10 ) , previousCount - ( previousCount / 30 ) ) ;
222228 }
229+
230+ [ Fact ]
231+ public void CurrentMemoryMetricTracksPooledMemory ( )
232+ {
233+ var testMeterFactory = new TestMeterFactory ( ) ;
234+ using var currentMemoryMetric = new MetricCollector < long > ( testMeterFactory , "Microsoft.AspNetCore.MemoryPool" , "aspnetcore.memorypool.current_memory" ) ;
235+
236+ var pool = new PinnedBlockMemoryPool ( testMeterFactory ) ;
237+
238+ Assert . Empty ( currentMemoryMetric . GetMeasurementSnapshot ( ) ) ;
239+
240+ var mem = pool . Rent ( ) ;
241+ mem . Dispose ( ) ;
242+
243+ Assert . Collection ( currentMemoryMetric . GetMeasurementSnapshot ( ) , m => Assert . Equal ( PinnedBlockMemoryPool . BlockSize , m . Value ) ) ;
244+
245+ mem = pool . Rent ( ) ;
246+
247+ Assert . Equal ( - 1 * PinnedBlockMemoryPool . BlockSize , currentMemoryMetric . LastMeasurement . Value ) ;
248+
249+ var mem2 = pool . Rent ( ) ;
250+
251+ mem . Dispose ( ) ;
252+ mem2 . Dispose ( ) ;
253+
254+ Assert . Equal ( 2 * PinnedBlockMemoryPool . BlockSize , currentMemoryMetric . GetMeasurementSnapshot ( ) . EvaluateAsCounter ( ) ) ;
255+
256+ // Eviction after returning everything to reset internal counters
257+ pool . PerformEviction ( ) ;
258+
259+ // Trigger eviction
260+ pool . PerformEviction ( ) ;
261+
262+ // Verify eviction updates current memory metric
263+ Assert . Equal ( 0 , currentMemoryMetric . GetMeasurementSnapshot ( ) . EvaluateAsCounter ( ) ) ;
264+ }
265+
266+ [ Fact ]
267+ public void TotalAllocatedMetricTracksAllocatedMemory ( )
268+ {
269+ var testMeterFactory = new TestMeterFactory ( ) ;
270+ using var totalMemoryMetric = new MetricCollector < long > ( testMeterFactory , "Microsoft.AspNetCore.MemoryPool" , "aspnetcore.memorypool.total_allocated" ) ;
271+
272+ var pool = new PinnedBlockMemoryPool ( testMeterFactory ) ;
273+
274+ Assert . Empty ( totalMemoryMetric . GetMeasurementSnapshot ( ) ) ;
275+
276+ var mem1 = pool . Rent ( ) ;
277+ var mem2 = pool . Rent ( ) ;
278+
279+ // Each Rent that allocates a new block should increment total memory by block size
280+ Assert . Equal ( 2 * PinnedBlockMemoryPool . BlockSize , totalMemoryMetric . GetMeasurementSnapshot ( ) . EvaluateAsCounter ( ) ) ;
281+
282+ mem1 . Dispose ( ) ;
283+ mem2 . Dispose ( ) ;
284+
285+ // Disposing (returning) blocks does not affect total memory
286+ Assert . Equal ( 2 * PinnedBlockMemoryPool . BlockSize , totalMemoryMetric . GetMeasurementSnapshot ( ) . EvaluateAsCounter ( ) ) ;
287+ }
288+
289+ [ Fact ]
290+ public void TotalRentedMetricTracksRentOperations ( )
291+ {
292+ var testMeterFactory = new TestMeterFactory ( ) ;
293+ using var rentMetric = new MetricCollector < long > ( testMeterFactory , "Microsoft.AspNetCore.MemoryPool" , "aspnetcore.memorypool.total_rented" ) ;
294+
295+ var pool = new PinnedBlockMemoryPool ( testMeterFactory ) ;
296+
297+ Assert . Empty ( rentMetric . GetMeasurementSnapshot ( ) ) ;
298+
299+ var mem1 = pool . Rent ( ) ;
300+ var mem2 = pool . Rent ( ) ;
301+
302+ // Each Rent should record the size of the block rented
303+ Assert . Collection ( rentMetric . GetMeasurementSnapshot ( ) ,
304+ m => Assert . Equal ( PinnedBlockMemoryPool . BlockSize , m . Value ) ,
305+ m => Assert . Equal ( PinnedBlockMemoryPool . BlockSize , m . Value ) ) ;
306+
307+ mem1 . Dispose ( ) ;
308+ mem2 . Dispose ( ) ;
309+
310+ // Disposing does not affect rent metric
311+ Assert . Equal ( 2 * PinnedBlockMemoryPool . BlockSize , rentMetric . GetMeasurementSnapshot ( ) . EvaluateAsCounter ( ) ) ;
312+ }
313+
314+ [ Fact ]
315+ public void EvictedMemoryMetricTracksEvictedMemory ( )
316+ {
317+ var testMeterFactory = new TestMeterFactory ( ) ;
318+ using var evictMetric = new MetricCollector < long > ( testMeterFactory , "Microsoft.AspNetCore.MemoryPool" , "aspnetcore.memorypool.evicted_memory" ) ;
319+
320+ var pool = new PinnedBlockMemoryPool ( testMeterFactory ) ;
321+
322+ // Fill the pool with some blocks
323+ var blocks = new List < IMemoryOwner < byte > > ( ) ;
324+ for ( int i = 0 ; i < 10 ; i ++ )
325+ {
326+ blocks . Add ( pool . Rent ( ) ) ;
327+ }
328+ foreach ( var block in blocks )
329+ {
330+ block . Dispose ( ) ;
331+ }
332+ blocks . Clear ( ) ;
333+
334+ Assert . Empty ( evictMetric . GetMeasurementSnapshot ( ) ) ;
335+
336+ // Eviction after returning everything to reset internal counters
337+ pool . PerformEviction ( ) ;
338+
339+ // Trigger eviction
340+ pool . PerformEviction ( ) ;
341+
342+ // At least some blocks should be evicted, each eviction records block size
343+ Assert . NotEmpty ( evictMetric . GetMeasurementSnapshot ( ) ) ;
344+ foreach ( var measurement in evictMetric . GetMeasurementSnapshot ( ) )
345+ {
346+ Assert . Equal ( PinnedBlockMemoryPool . BlockSize , measurement . Value ) ;
347+ }
348+ }
349+
350+ // Smoke test to ensure that metrics are aggregated across multiple pools if the same meter factory is used
351+ [ Fact ]
352+ public void MetricsAreAggregatedAcrossPoolsWithSameMeterFactory ( )
353+ {
354+ var testMeterFactory = new TestMeterFactory ( ) ;
355+ using var rentMetric = new MetricCollector < long > ( testMeterFactory , "Microsoft.AspNetCore.MemoryPool" , "aspnetcore.memorypool.total_rented" ) ;
356+
357+ var pool1 = new PinnedBlockMemoryPool ( testMeterFactory ) ;
358+ var pool2 = new PinnedBlockMemoryPool ( testMeterFactory ) ;
359+
360+ var mem1 = pool1 . Rent ( ) ;
361+ var mem2 = pool2 . Rent ( ) ;
362+
363+ // Both pools should contribute to the same metric stream
364+ Assert . Equal ( 2 * PinnedBlockMemoryPool . BlockSize , rentMetric . GetMeasurementSnapshot ( ) . EvaluateAsCounter ( ) ) ;
365+
366+ mem1 . Dispose ( ) ;
367+ mem2 . Dispose ( ) ;
368+
369+ // Renting and returning from both pools should not interfere with metric collection
370+ var mem3 = pool1 . Rent ( ) ;
371+ var mem4 = pool2 . Rent ( ) ;
372+
373+ Assert . Equal ( 4 * PinnedBlockMemoryPool . BlockSize , rentMetric . GetMeasurementSnapshot ( ) . EvaluateAsCounter ( ) ) ;
374+
375+ mem3 . Dispose ( ) ;
376+ mem4 . Dispose ( ) ;
377+ }
223378}
0 commit comments