22// Licensed under the MIT license.
33
44using System ;
5+ using System . Collections . Concurrent ;
56using System . Diagnostics ;
67using System . Numerics ;
78using System . Runtime . CompilerServices ;
@@ -26,6 +27,11 @@ public sealed class LimitedFixedBufferPool : IDisposable
2627 readonly int maxAllocationSize ;
2728 readonly ILogger logger ;
2829
30+ /// <summary>
31+ /// Pool owner type, packed into byte 1 of each <see cref="PoolEntry.source"/>.
32+ /// </summary>
33+ readonly int ownerByte ;
34+
2935 /// <summary>
3036 /// Min allocation size
3137 /// </summary>
@@ -41,16 +47,29 @@ public sealed class LimitedFixedBufferPool : IDisposable
4147 /// </summary>
4248 int totalOutOfBoundAllocations ;
4349
50+ #if DEBUG
51+ /// <summary>
52+ /// Tracks all outstanding (checked-out) pool entries for leak diagnosis.
53+ /// </summary>
54+ readonly ConcurrentDictionary < PoolEntry , byte > outstandingEntries = new ( ) ;
55+
56+ /// <summary>
57+ /// Timeout in milliseconds for Dispose to wait before logging outstanding entries.
58+ /// </summary>
59+ const int DisposeWaitDiagnosticMs = 5_000 ;
60+ #endif
61+
4462 /// <summary>
4563 /// Constructor
4664 /// </summary>
47- public LimitedFixedBufferPool ( int minAllocationSize , int maxEntriesPerLevel = 16 , int numLevels = 4 , ILogger logger = null )
65+ public LimitedFixedBufferPool ( int minAllocationSize , int maxEntriesPerLevel = 16 , int numLevels = 4 , PoolOwnerType ownerType = PoolOwnerType . Unknown , ILogger logger = null )
4866 {
4967 this . minAllocationSize = minAllocationSize ;
5068 this . maxAllocationSize = minAllocationSize << ( numLevels - 1 ) ;
5169 this . maxEntriesPerLevel = maxEntriesPerLevel ;
5270 this . numLevels = numLevels ;
5371 this . logger = logger ;
72+ this . ownerByte = ( int ) ownerType << 8 ;
5473 pool = new PoolLevel [ numLevels ] ;
5574 }
5675
@@ -85,6 +104,9 @@ public bool Validate(NetworkBufferSettings settings)
85104 [ MethodImpl ( MethodImplOptions . AggressiveInlining ) ]
86105 public void Return ( PoolEntry buffer )
87106 {
107+ #if DEBUG
108+ outstandingEntries . TryRemove ( buffer , out _ ) ;
109+ #endif
88110 var level = Position ( buffer . entry . Length ) ;
89111 if ( level >= 0 )
90112 {
@@ -107,9 +129,10 @@ public void Return(PoolEntry buffer)
107129 /// Get buffer
108130 /// </summary>
109131 /// <param name="size"></param>
132+ /// <param name="bufferType">Identifies the caller for leak diagnosis.</param>
110133 /// <returns></returns>
111134 [ MethodImpl ( MethodImplOptions . AggressiveInlining ) ]
112- public unsafe PoolEntry Get ( int size )
135+ public unsafe PoolEntry Get ( int size , PoolEntryBufferType bufferType = PoolEntryBufferType . Unknown )
113136 {
114137 if ( Interlocked . Increment ( ref totalReferences ) < 0 )
115138 {
@@ -118,6 +141,8 @@ public unsafe PoolEntry Get(int size)
118141 return null ;
119142 }
120143
144+ var source = ownerByte | ( int ) bufferType ;
145+
121146 var level = Position ( size ) ;
122147 if ( level == - 1 ) Interlocked . Increment ( ref totalOutOfBoundAllocations ) ;
123148
@@ -132,10 +157,19 @@ public unsafe PoolEntry Get(int size)
132157 {
133158 Interlocked . Decrement ( ref pool [ level ] . size ) ;
134159 page . Reuse ( ) ;
160+ page . source = source ;
161+ #if DEBUG
162+ outstandingEntries [ page ] = 0 ;
163+ #endif
135164 return page ;
136165 }
137166 }
138- return new PoolEntry ( size , this ) ;
167+ var entry = new PoolEntry ( size , this ) ;
168+ entry . source = source ;
169+ #if DEBUG
170+ outstandingEntries [ entry ] = 0 ;
171+ #endif
172+ return entry ;
139173 }
140174
141175 /// <summary>
@@ -157,23 +191,37 @@ public void Purge()
157191 }
158192
159193 /// <summary>
160- /// Dipose pool entries from all levels
194+ /// Dispose pool entries from all levels
161195 /// NOTE:
162196 /// This is used to destroy the instance and reclaim all allocated buffer pool entries.
163197 /// As a consequence it spin waits until totalReferences goes back down to 0 and blocks any future allocations.
198+ /// In DEBUG builds, logs outstanding unreturned entries after a timeout for leak diagnosis.
164199 /// </summary>
165200 [ MethodImpl ( MethodImplOptions . AggressiveInlining ) ]
166201 public void Dispose ( )
167202 {
168- #if HANGDETECT
169- int count = 0 ;
203+ #if DEBUG
204+ var sw = Stopwatch . StartNew ( ) ;
205+ var diagnosed = false ;
170206#endif
171207 while ( totalReferences > int . MinValue &&
172208 Interlocked . CompareExchange ( ref totalReferences , int . MinValue , 0 ) != 0 )
173209 {
174- #if HANGDETECT
175- if ( ++ count % 10000 == 0 )
176- logger ? . LogTrace ( "Dispose iteration {count}, {activeHandlerCount}" , count , activeHandlerCount ) ;
210+ #if DEBUG
211+ if ( ! diagnosed && sw . ElapsedMilliseconds > DisposeWaitDiagnosticMs )
212+ {
213+ diagnosed = true ;
214+ var remaining = totalReferences ;
215+ var ownerType = ( PoolOwnerType ) ( ownerByte >> 8 ) ;
216+ logger ? . LogError ( "LimitedFixedBufferPool.Dispose blocked with {remaining} unreturned references (poolOwner={ownerType}). Outstanding entries:" , remaining , ownerType ) ;
217+ foreach ( var kvp in outstandingEntries )
218+ {
219+ var entryBufferType = ( PoolEntryBufferType ) ( kvp . Key . source & 0xFF ) ;
220+ var entryOwnerType = ( PoolOwnerType ) ( ( kvp . Key . source >> 8 ) & 0xFF ) ;
221+ logger ? . LogError ( " Unreturned buffer: ownerType={ownerType}, bufferType={bufferType}, size={size}" ,
222+ entryOwnerType , entryBufferType , kvp . Key . entry . Length ) ;
223+ }
224+ }
177225#endif
178226 Thread . Yield ( ) ;
179227 }
0 commit comments