Skip to content

Commit 93d9698

Browse files
authored
Merge pull request #3823 from SukkaW/improve-speed
perf(#3819/#3030): 30x speed up
2 parents 530b3fa + b05bfd7 commit 93d9698

15 files changed

Lines changed: 305 additions & 560 deletions

File tree

.changeset/easy-poets-tie.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@hey-api/codegen-core": patch
3+
---
4+
5+
**symbol**: speed up symbol registry cache

.changeset/six-pianos-refuse.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@hey-api/codegen-core": patch
3+
---
4+
5+
**planner**: speed up identifier conflict detector

.changeset/spicy-suns-report.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@hey-api/shared": patch
3+
---
4+
5+
**utils**: speed up deep equality check

.changeset/tasty-fans-lick.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@hey-api/shared": patch
3+
---
4+
5+
**graph**: speed up graph builder

dev/inputs.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ const specsPath = path.join(__dirname, '..', 'specs');
44

55
export const inputs = {
66
circular: path.resolve(specsPath, '3.0.x', 'circular.yaml'),
7+
clerk: path.resolve(specsPath, '3.0.x', 'clerk-2025-11-10.yaml'),
8+
cloudflare: path.resolve(specsPath, '3.0.x', 'cloudflare-v4.json'),
79
full: path.resolve(specsPath, '3.1.x', 'full.yaml'),
810
local: 'http://localhost:8000/openapi.json',
911
opencode: path.resolve(specsPath, '3.1.x', 'opencode.yaml'),

packages/codegen-core/src/planner/planner.ts

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -407,21 +407,18 @@ export class Planner {
407407
symbol: Symbol;
408408
},
409409
): void {
410-
const { ctx, file, node, scope, scopesToUpdate, symbol } = args;
410+
const { file, node, scope, scopesToUpdate, symbol } = args;
411411
if (this.cacheResolvedNames.has(symbol.id)) return;
412412

413413
const baseName = symbol.name;
414414
let finalName =
415415
node?.nameSanitizer?.(baseName) ?? symbol.node?.nameSanitizer?.(baseName) ?? baseName;
416416
let attempt = 1;
417417

418-
const localNames = ctx.localNames(scope);
419418
while (true) {
420419
const language = node?.language || symbol.node?.language || file.language;
421420

422-
const kinds = [...(localNames.get(finalName) ?? [])];
423-
424-
const ok = kinds.every((kind) => canDeclarationsShareIdentifier(language, symbol.kind, kind));
421+
const ok = this.nameIsAvailable({ kind: symbol.kind, language, name: finalName, scope });
425422
if (ok) break;
426423

427424
const resolver =
@@ -447,6 +444,39 @@ export class Planner {
447444
}
448445
}
449446

447+
/**
448+
* Checks whether `name` can be used for a new symbol of `kind` in `scope`.
449+
*
450+
* Walks up the scope chain and verifies that every existing declaration with
451+
* that name is compatible (i.e., can share the same identifier) with `kind`.
452+
* This avoids copying the entire accumulated name map on every call.
453+
*/
454+
private nameIsAvailable({
455+
kind,
456+
language,
457+
name,
458+
scope,
459+
}: {
460+
kind: SymbolKind;
461+
language: string | undefined;
462+
name: string;
463+
scope: Scope;
464+
}): boolean {
465+
let current: Scope | undefined = scope;
466+
while (current) {
467+
const kinds = current.localNames.get(name);
468+
if (kinds) {
469+
for (const existingKind of kinds) {
470+
if (!canDeclarationsShareIdentifier(language, kind, existingKind)) {
471+
return false;
472+
}
473+
}
474+
}
475+
current = current.parent;
476+
}
477+
return true;
478+
}
479+
450480
/**
451481
* Updates the provided name scope with the symbol's final name and kind.
452482
*

packages/codegen-core/src/symbols/registry.ts

Lines changed: 115 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,13 @@ type QueryCacheKey = string;
88
type SymbolId = number;
99

1010
export 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
}

packages/shared/src/graph/__tests__/walk.test.ts

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ describe('walkTopological', () => {
1111
const makeGraph = (deps: Array<[string, Array<string>]>, nodes: Array<string>) => {
1212
const nodeDependencies = new Map<string, Set<string>>();
1313
const subtreeDependencies = new Map<string, Set<string>>();
14-
const reverseNodeDependencies = new Map<string, Set<string>>();
1514
const nodesMap = new Map<string, any>();
1615

1716
for (const name of nodes) {
@@ -22,16 +21,11 @@ describe('walkTopological', () => {
2221
const s = new Set<string>(toList);
2322
nodeDependencies.set(from, s);
2423
subtreeDependencies.set(from, new Set<string>(toList));
25-
for (const to of toList) {
26-
if (!reverseNodeDependencies.has(to)) reverseNodeDependencies.set(to, new Set());
27-
reverseNodeDependencies.get(to)!.add(from);
28-
}
2924
}
3025

3126
return {
3227
nodeDependencies,
3328
nodes: nodesMap,
34-
reverseNodeDependencies,
3529
subtreeDependencies,
3630
transitiveDependencies: new Map<string, Set<string>>(),
3731
} as unknown as Graph;
@@ -183,13 +177,11 @@ describe('walkTopological', () => {
183177
const nodeDependencies = new Map<string, Set<string>>();
184178
nodeDependencies.set(schema, new Set([param]));
185179
const subtreeDependencies = new Map<string, Set<string>>();
186-
const reverseNodeDependencies = new Map<string, Set<string>>();
187180
const nodesMap = new Map<string, any>();
188181
for (const n of nodes) nodesMap.set(n, { key: null, node: {}, parentPointer: null });
189182
const graph = {
190183
nodeDependencies,
191184
nodes: nodesMap,
192-
reverseNodeDependencies,
193185
subtreeDependencies,
194186
transitiveDependencies: new Map<string, Set<string>>(),
195187
} as unknown as Graph;
@@ -215,7 +207,6 @@ describe('walkTopological', () => {
215207
const graph = {
216208
nodeDependencies,
217209
nodes: nodesMap,
218-
reverseNodeDependencies: new Map<string, Set<string>>(),
219210
subtreeDependencies: new Map<string, Set<string>>(),
220211
transitiveDependencies: new Map<string, Set<string>>(),
221212
} as unknown as Graph;
@@ -240,7 +231,6 @@ describe('walkTopological', () => {
240231
const graph = {
241232
nodeDependencies,
242233
nodes: nodesMap,
243-
reverseNodeDependencies: new Map<string, Set<string>>(),
244234
subtreeDependencies,
245235
transitiveDependencies: new Map<string, Set<string>>(),
246236
} as unknown as Graph;

0 commit comments

Comments
 (0)