@@ -8,10 +8,13 @@ type QueryCacheKey = string;
88type SymbolId = number ;
99
1010export class SymbolRegistry implements ISymbolRegistry {
11+ /** Forward index: cache key → serialized entries it depends on. */
12+ private _cacheDependencies : Map < QueryCacheKey , ReadonlyArray < string > > = new Map ( ) ;
13+ /** Reverse index: serialized index entry → set of cache keys that depend on it. */
14+ private _dependencyToCache : Map < QueryCacheKey , Set < QueryCacheKey > > = new Map ( ) ;
1115 private _id : SymbolId = 0 ;
1216 private _indices : Map < IndexEntry [ 0 ] , Map < IndexEntry [ 1 ] , Set < SymbolId > > > = new Map ( ) ;
13- private _queryCache : Map < QueryCacheKey , ReadonlyArray < SymbolId > > = new Map ( ) ;
14- private _queryCacheDependencies : Map < QueryCacheKey , Set < QueryCacheKey > > = new Map ( ) ;
17+ private _queryCache : Map < QueryCacheKey , ReadonlyArray < Symbol > > = new Map ( ) ;
1518 private _registered : Set < SymbolId > = new Set ( ) ;
1619 private _stubs : Set < SymbolId > = new Set ( ) ;
1720 private _stubCache : Map < QueryCacheKey , SymbolId > = new Map ( ) ;
@@ -33,49 +36,17 @@ export class SymbolRegistry implements ISymbolRegistry {
3336 }
3437
3538 query ( filter : ISymbolMeta ) : ReadonlyArray < Symbol > {
36- const cacheKey = this . buildCacheKey ( filter ) ;
37- const cachedIds = this . _queryCache . get ( cacheKey ) ;
38- if ( cachedIds ) {
39- return cachedIds . map ( ( symbolId ) => this . _values . get ( symbolId ) ! ) ;
40- }
41- const sets : Array < Set < SymbolId > > = [ ] ;
4239 const indexKeySpace = this . buildIndexKeySpace ( filter ) ;
43- const cacheDependencies = new Set < QueryCacheKey > ( ) ;
44- let missed = false ;
45- for ( const indexEntry of indexKeySpace ) {
46- cacheDependencies . add ( this . serializeIndexEntry ( indexEntry ) ) ;
47- const values = this . _indices . get ( indexEntry [ 0 ] ) ;
48- if ( ! values ) {
49- missed = true ;
50- break ;
51- }
52- const set = values . get ( indexEntry [ 1 ] ) ;
53- if ( ! set ) {
54- missed = true ;
55- break ;
56- }
57- sets . push ( set ) ;
58- }
59- if ( missed || ! sets . length ) {
60- this . _queryCacheDependencies . set ( cacheKey , cacheDependencies ) ;
61- this . _queryCache . set ( cacheKey , [ ] ) ;
62- return [ ] ;
63- }
64- let result = new Set ( sets [ 0 ] ) ;
65- for ( const set of sets . slice ( 1 ) ) {
66- result = new Set ( [ ...result ] . filter ( ( symbolId ) => set . has ( symbolId ) ) ) ;
67- }
68- const resultIds = [ ...result ] ;
69- this . _queryCacheDependencies . set ( cacheKey , cacheDependencies ) ;
70- this . _queryCache . set ( cacheKey , resultIds ) ;
71- return resultIds . map ( ( symbolId ) => this . _values . get ( symbolId ) ! ) ;
40+ const cacheKey = this . cacheKeyFromKeySpace ( indexKeySpace ) ;
41+ return this . queryByKeySpace ( indexKeySpace , cacheKey ) ;
7242 }
7343
7444 reference ( meta : ISymbolMeta ) : Symbol {
75- const [ registered ] = this . query ( meta ) ;
45+ const indexKeySpace = this . buildIndexKeySpace ( meta ) ;
46+ const cacheKey = this . cacheKeyFromKeySpace ( indexKeySpace ) ;
47+ const [ registered ] = this . queryByKeySpace ( indexKeySpace , cacheKey ) ;
7648 if ( registered ) return registered ;
7749
78- const cacheKey = this . buildCacheKey ( meta ) ;
7950 const cachedId = this . _stubCache . get ( cacheKey ) ;
8051 if ( cachedId !== undefined ) return this . _values . get ( cachedId ) ! ;
8152
@@ -109,14 +80,6 @@ export class SymbolRegistry implements ISymbolRegistry {
10980 }
11081 }
11182
112- private buildCacheKey ( filter : ISymbolMeta ) : QueryCacheKey {
113- const indexKeySpace = this . buildIndexKeySpace ( filter ) ;
114- return indexKeySpace
115- . map ( ( indexEntry ) => this . serializeIndexEntry ( indexEntry ) )
116- . sort ( ) // ensure order-insensitivity
117- . join ( '|' ) ;
118- }
119-
12083 private buildIndexKeySpace ( meta : ISymbolMeta , prefix = '' ) : IndexKeySpace {
12184 const entries : Array < IndexEntry > = [ ] ;
12285 for ( const [ key , value ] of Object . entries ( meta ) ) {
@@ -130,6 +93,17 @@ export class SymbolRegistry implements ISymbolRegistry {
13093 return entries ;
13194 }
13295
96+ /**
97+ * Derives a stable, order-insensitive cache key from a pre-built key space.
98+ * Avoids rebuilding the key space when it's already available.
99+ */
100+ private cacheKeyFromKeySpace ( indexKeySpace : IndexKeySpace ) : QueryCacheKey {
101+ return indexKeySpace
102+ . map ( ( indexEntry ) => this . serializeIndexEntry ( indexEntry ) )
103+ . sort ( ) // ensure order-insensitivity
104+ . join ( '|' ) ;
105+ }
106+
133107 private indexSymbol ( symbolId : SymbolId , indexKeySpace : IndexKeySpace ) : void {
134108 for ( const [ key , value ] of indexKeySpace ) {
135109 if ( ! this . _indices . has ( key ) ) this . _indices . set ( key , new Map ( ) ) ;
@@ -141,15 +115,25 @@ export class SymbolRegistry implements ISymbolRegistry {
141115 }
142116
143117 private invalidateCache ( indexKeySpace : IndexKeySpace ) : void {
144- const changed = indexKeySpace . map ( ( indexEntry ) => this . serializeIndexEntry ( indexEntry ) ) ;
145- for ( const [ cacheKey , cacheDependencies ] of this . _queryCacheDependencies . entries ( ) ) {
146- for ( const key of changed ) {
147- if ( cacheDependencies . has ( key ) ) {
148- this . _queryCacheDependencies . delete ( cacheKey ) ;
149- this . _queryCache . delete ( cacheKey ) ;
150- break ;
118+ for ( const indexEntry of indexKeySpace ) {
119+ const serialized = this . serializeIndexEntry ( indexEntry ) ;
120+ const cacheKeys = this . _dependencyToCache . get ( serialized ) ;
121+ if ( ! cacheKeys ) continue ;
122+ for ( const cacheKey of cacheKeys ) {
123+ this . _queryCache . delete ( cacheKey ) ;
124+ // Clean up stale reverse-index references so _dependencyToCache doesn't
125+ // accumulate orphaned entries for evicted cache keys.
126+ const deps = this . _cacheDependencies . get ( cacheKey ) ;
127+ if ( deps ) {
128+ for ( const dep of deps ) {
129+ if ( dep !== serialized ) {
130+ this . _dependencyToCache . get ( dep ) ?. delete ( cacheKey ) ;
131+ }
132+ }
133+ this . _cacheDependencies . delete ( cacheKey ) ;
151134 }
152135 }
136+ this . _dependencyToCache . delete ( serialized ) ;
153137 }
154138 }
155139
@@ -163,14 +147,86 @@ export class SymbolRegistry implements ISymbolRegistry {
163147 return true ;
164148 }
165149
150+ private queryByKeySpace (
151+ indexKeySpace : IndexKeySpace ,
152+ cacheKey : QueryCacheKey ,
153+ ) : ReadonlyArray < Symbol > {
154+ const cached = this . _queryCache . get ( cacheKey ) ;
155+ if ( cached ) {
156+ return cached ;
157+ }
158+ const sets : Array < Set < SymbolId > > = [ ] ;
159+ let missed = false ;
160+ for ( const indexEntry of indexKeySpace ) {
161+ const values = this . _indices . get ( indexEntry [ 0 ] ) ;
162+ if ( ! values ) {
163+ missed = true ;
164+ break ;
165+ }
166+ const set = values . get ( indexEntry [ 1 ] ) ;
167+ if ( ! set ) {
168+ missed = true ;
169+ break ;
170+ }
171+ sets . push ( set ) ;
172+ }
173+ if ( missed || ! sets . length ) {
174+ this . _queryCache . set ( cacheKey , [ ] ) ;
175+ this . registerCacheDependencies ( cacheKey , indexKeySpace ) ;
176+ return [ ] ;
177+ }
178+
179+ // We want to do a Set intersection, but large inputs may contain a few very
180+ // large sets. The profiling showed that Set operations became a huge bottleneck
181+ // on such inputs.
182+ //
183+ // To avoid iterating over large sets multiple times, we sort the sets by size
184+ // and use the smallest set as the base to minimize iterations and deletions.
185+ sets . sort ( ( a , b ) => a . size - b . size ) ;
186+ const result = new Set ( sets [ 0 ] ) ;
187+ for ( let i = 1 ; i < sets . length ; i ++ ) {
188+ const set = sets [ i ] ! ;
189+ for ( const symbolId of result ) {
190+ if ( ! set . has ( symbolId ) ) result . delete ( symbolId ) ;
191+ }
192+ }
193+
194+ const symbols = Array . from ( result , ( symbolId ) => this . _values . get ( symbolId ) ! ) ;
195+ this . _queryCache . set ( cacheKey , symbols ) ;
196+ this . registerCacheDependencies ( cacheKey , indexKeySpace ) ;
197+ return symbols ;
198+ }
199+
200+ /**
201+ * Registers reverse-index entries so that when any of the given index entries
202+ * are invalidated, the cache key is evicted in O(1) per entry.
203+ */
204+ private registerCacheDependencies ( cacheKey : QueryCacheKey , indexKeySpace : IndexKeySpace ) : void {
205+ const serializedEntries : Array < string > = [ ] ;
206+ for ( const indexEntry of indexKeySpace ) {
207+ const serialized = this . serializeIndexEntry ( indexEntry ) ;
208+ serializedEntries . push ( serialized ) ;
209+ let cacheKeys = this . _dependencyToCache . get ( serialized ) ;
210+ if ( ! cacheKeys ) {
211+ cacheKeys = new Set ( ) ;
212+ this . _dependencyToCache . set ( serialized , cacheKeys ) ;
213+ }
214+ cacheKeys . add ( cacheKey ) ;
215+ }
216+ this . _cacheDependencies . set ( cacheKey , serializedEntries ) ;
217+ }
218+
166219 private replaceStubs ( symbol : Symbol , indexKeySpace : IndexKeySpace ) : void {
167220 for ( const stubId of this . _stubs . values ( ) ) {
168221 const stub = this . _values . get ( stubId ) ;
169- if ( stub ?. meta && this . isSubset ( this . buildIndexKeySpace ( stub . meta ) , indexKeySpace ) ) {
170- const cacheKey = this . buildCacheKey ( stub . meta ) ;
171- this . _stubCache . delete ( cacheKey ) ;
172- this . _stubs . delete ( stubId ) ;
173- stub . setCanonical ( symbol ) ;
222+ if ( stub ?. meta ) {
223+ const stubKeySpace = this . buildIndexKeySpace ( stub . meta ) ;
224+ if ( this . isSubset ( stubKeySpace , indexKeySpace ) ) {
225+ const cacheKey = this . cacheKeyFromKeySpace ( stubKeySpace ) ;
226+ this . _stubCache . delete ( cacheKey ) ;
227+ this . _stubs . delete ( stubId ) ;
228+ stub . setCanonical ( symbol ) ;
229+ }
174230 }
175231 }
176232 }
0 commit comments