@@ -42,11 +42,15 @@ interface QueryRecord {
4242}
4343
4444export class RetrievalStatsCollector {
45- private _records : QueryRecord [ ] = [ ] ;
45+ // Ring buffer: O(1) write, avoids O(n) Array.shift() GC pressure.
46+ private _records : ( QueryRecord | undefined ) [ ] = [ ] ;
47+ private _head = 0 ; // next write position
48+ private _count = 0 ; // number of valid records
4649 private readonly _maxRecords : number ;
4750
4851 constructor ( maxRecords = 1000 ) {
4952 this . _maxRecords = maxRecords ;
53+ this . _records = new Array ( maxRecords ) ;
5054 }
5155
5256 /**
@@ -55,18 +59,31 @@ export class RetrievalStatsCollector {
5559 * @param source - Query source identifier (e.g. "manual", "auto-recall")
5660 */
5761 recordQuery ( trace : RetrievalTrace , source : string ) : void {
58- this . _records . push ( { trace, source } ) ;
59- // Evict oldest if over capacity
60- if ( this . _records . length > this . _maxRecords ) {
61- this . _records . shift ( ) ;
62+ this . _records [ this . _head ] = { trace, source } ;
63+ this . _head = ( this . _head + 1 ) % this . _maxRecords ;
64+ if ( this . _count < this . _maxRecords ) {
65+ this . _count ++ ;
6266 }
6367 }
6468
69+ /** Return records in insertion order (oldest → newest). Used by getStats(). */
70+ private _getRecords ( ) : QueryRecord [ ] {
71+ if ( this . _count === 0 ) return [ ] ;
72+ const result : QueryRecord [ ] = [ ] ;
73+ const start = this . _count < this . _maxRecords ? 0 : this . _head ;
74+ for ( let i = 0 ; i < this . _count ; i ++ ) {
75+ const rec = this . _records [ ( start + i ) % this . _maxRecords ] ;
76+ if ( rec !== undefined ) result . push ( rec ) ;
77+ }
78+ return result ;
79+ }
80+
6581 /**
6682 * Compute aggregate statistics from all recorded queries.
83+ * Iterates ring buffer directly — avoids intermediate array allocation from _getRecords().
6784 */
6885 getStats ( ) : AggregateStats {
69- const n = this . _records . length ;
86+ const n = this . _count ;
7087 if ( n === 0 ) {
7188 return {
7289 totalQueries : 0 ,
@@ -90,28 +107,27 @@ export class RetrievalStatsCollector {
90107 const queriesBySource : Record < string , number > = { } ;
91108 const dropsByStage : Record < string , number > = { } ;
92109
93- for ( const { trace, source } of this . _records ) {
110+ // Iterate ring buffer directly (no intermediate array allocation).
111+ const start = n < this . _maxRecords ? 0 : this . _head ;
112+ for ( let i = 0 ; i < n ; i ++ ) {
113+ const rec = this . _records [ ( start + i ) % this . _maxRecords ] ;
114+ if ( rec === undefined ) continue ;
115+ const { trace, source } = rec ;
116+
94117 totalLatency += trace . totalMs ;
95118 totalResults += trace . finalCount ;
96119 latencies . push ( trace . totalMs ) ;
97120
98- if ( trace . finalCount === 0 ) {
99- zeroResultQueries ++ ;
100- }
121+ if ( trace . finalCount === 0 ) zeroResultQueries ++ ;
101122
102123 queriesBySource [ source ] = ( queriesBySource [ source ] || 0 ) + 1 ;
103-
104124 for ( const stage of trace . stages ) {
105125 const dropped = stage . inputCount - stage . outputCount ;
106126 if ( dropped > 0 ) {
107127 dropsByStage [ stage . name ] = ( dropsByStage [ stage . name ] || 0 ) + dropped ;
108128 }
109- if ( stage . name === "rerank" ) {
110- rerankUsed ++ ;
111- }
112- if ( stage . name === "noise_filter" && dropped > 0 ) {
113- noiseFiltered ++ ;
114- }
129+ if ( stage . name === "rerank" ) rerankUsed ++ ;
130+ if ( stage . name === "noise_filter" && dropped > 0 ) noiseFiltered ++ ;
115131 }
116132 }
117133
@@ -142,11 +158,13 @@ export class RetrievalStatsCollector {
142158 * Reset all collected statistics.
143159 */
144160 reset ( ) : void {
145- this . _records = [ ] ;
161+ this . _records = new Array ( this . _maxRecords ) ;
162+ this . _head = 0 ;
163+ this . _count = 0 ;
146164 }
147165
148166 /** Number of recorded queries. */
149167 get count ( ) : number {
150- return this . _records . length ;
168+ return this . _count ;
151169 }
152170}
0 commit comments