From 7e8264eae199f4ce52f1666937bca838626cacc3 Mon Sep 17 00:00:00 2001 From: Lubos Date: Wed, 20 Aug 2025 00:48:23 +0800 Subject: [PATCH] fix(parser): cache parent to children nodes --- .changeset/khaki-rabbits-explain.md | 5 + .../main/test/openapi-ts.config.ts | 6 +- .../main/test/performance.test.ts | 88 +++++++++-------- packages/openapi-ts/src/index.ts | 7 +- .../src/openApi/shared/utils/graph.ts | 95 ++++++++----------- .../src/openApi/shared/utils/graphDebug.ts | 47 +++++++++ packages/openapi-ts/src/utils/logger.ts | 68 +++++++------ 7 files changed, 173 insertions(+), 143 deletions(-) create mode 100644 .changeset/khaki-rabbits-explain.md create mode 100644 packages/openapi-ts/src/openApi/shared/utils/graphDebug.ts diff --git a/.changeset/khaki-rabbits-explain.md b/.changeset/khaki-rabbits-explain.md new file mode 100644 index 000000000..e9da4c190 --- /dev/null +++ b/.changeset/khaki-rabbits-explain.md @@ -0,0 +1,5 @@ +--- +'@hey-api/openapi-ts': patch +--- + +fix(parser): cache parent to children nodes diff --git a/packages/openapi-ts-tests/main/test/openapi-ts.config.ts b/packages/openapi-ts-tests/main/test/openapi-ts.config.ts index 5920459af..32feed08b 100644 --- a/packages/openapi-ts-tests/main/test/openapi-ts.config.ts +++ b/packages/openapi-ts-tests/main/test/openapi-ts.config.ts @@ -54,7 +54,7 @@ export default defineConfig(() => { // }, }, logs: { - // level: 'debug', + level: 'debug', path: './logs', }, // name: 'foo', @@ -80,7 +80,7 @@ export default defineConfig(() => { // // '/^[A-Z]+ /v1//', // ], }, - // orphans: false, + // orphans: true, // preserveOrder: true, // schemas: { // include: ['Foo'], @@ -190,7 +190,7 @@ export default defineConfig(() => { // mutationOptions: { // name: '{{name}}MO', // }, - // name: '@tanstack/react-query', + name: '@tanstack/react-query', // queryKeys: { // name: '{{name}}QK', // }, diff --git a/packages/openapi-ts-tests/main/test/performance.test.ts b/packages/openapi-ts-tests/main/test/performance.test.ts index e1d2b8681..d7d7ff546 100644 --- a/packages/openapi-ts-tests/main/test/performance.test.ts +++ b/packages/openapi-ts-tests/main/test/performance.test.ts @@ -1,9 +1,8 @@ import path from 'node:path'; -import { createClient } from '@hey-api/openapi-ts'; -import { describe, expect, it } from 'vitest'; +import { createClient, Logger } from '@hey-api/openapi-ts'; +import { beforeEach, describe, expect, it } from 'vitest'; -import { Performance } from '../../../openapi-ts/src/utils/logger'; import { getSpecsPath } from '../../utils'; const V3_SPEC_PATH = path.resolve(getSpecsPath(), 'v3.json'); @@ -13,57 +12,62 @@ const toOutputPath = (name: string) => path.resolve(__dirname, 'generated', name); describe('performance', () => { - it('creates client under 1500ms', async () => { - Performance.clear(); + beforeEach(() => { + performance.clearMarks(); + performance.clearMeasures(); + }); - await createClient({ - input: V3_SPEC_PATH, - logs: { - level: 'silent', + it('creates client under 1500ms', async () => { + const logger = new Logger(); + await createClient( + { + input: V3_SPEC_PATH, + logs: { + level: 'silent', + }, + output: toOutputPath('perf'), + plugins: ['@hey-api/client-fetch'], }, - output: toOutputPath('perf'), - plugins: ['@hey-api/client-fetch'], - }); - - Performance.measure('createClient'); - const measures = Performance.getEntriesByName('createClient'); + logger, + ); - expect(measures[0]!.duration).toBeLessThanOrEqual(1500); + const duration = logger.report()?.duration ?? 9999; + expect(duration).toBeLessThanOrEqual(1500); }); it('parses spec under 1500ms', async () => { - Performance.clear(); - - await createClient({ - input: V3_SPEC_PATH, - logs: { - level: 'silent', + const logger = new Logger(); + await createClient( + { + input: V3_SPEC_PATH, + logs: { + level: 'silent', + }, + output: toOutputPath('perf'), + plugins: ['@hey-api/client-fetch'], }, - output: toOutputPath('perf'), - plugins: ['@hey-api/client-fetch'], - }); - - Performance.measure('parser'); - const measures = Performance.getEntriesByName('parser'); + logger, + ); - expect(measures[0]!.duration).toBeLessThanOrEqual(1500); + const duration = logger.report()?.duration ?? 9999; + expect(duration).toBeLessThanOrEqual(1500); }); it('parses spec under 1500ms (experimental)', async () => { - Performance.clear(); - - await createClient({ - input: V3_1_X_SPEC_PATH, - logs: { - level: 'silent', + const logger = new Logger(); + await createClient( + { + input: V3_1_X_SPEC_PATH, + logs: { + level: 'silent', + }, + output: toOutputPath('perf'), + plugins: ['@hey-api/client-fetch'], }, - output: toOutputPath('perf'), - plugins: ['@hey-api/client-fetch'], - }); - - Performance.measure('parser'); - const measures = Performance.getEntriesByName('parser'); + logger, + ); - expect(measures[0]!.duration).toBeLessThanOrEqual(1500); + const duration = logger.report()?.duration ?? 9999; + expect(duration).toBeLessThanOrEqual(1500); }); }); diff --git a/packages/openapi-ts/src/index.ts b/packages/openapi-ts/src/index.ts index e2bfa04a0..215005f0f 100644 --- a/packages/openapi-ts/src/index.ts +++ b/packages/openapi-ts/src/index.ts @@ -29,6 +29,7 @@ colors.enabled = colorSupport().hasBasic; */ export const createClient = async ( userConfig?: Configs, + logger = new Logger(), ): Promise> => { const resolvedConfig = typeof userConfig === 'function' ? await userConfig() : userConfig; @@ -38,7 +39,6 @@ export const createClient = async ( try { checkNodeVersion(); - const logger = new Logger(); const eventCreateClient = logger.timeEvent('createClient'); const eventConfig = logger.timeEvent('config'); @@ -72,9 +72,7 @@ export const createClient = async ( eventCreateClient.timeEnd(); const config = configs[0]; - if (config && config.logs.level === 'debug') { - logger.report(); - } + logger.report(config && config.logs.level === 'debug'); return result; } catch (error) { @@ -134,3 +132,4 @@ export { compiler, tsc } from './tsc'; export type { UserConfig } from './types/config'; export type { LegacyIR } from './types/types'; export { utils } from './utils/exports'; +export { Logger } from './utils/logger'; diff --git a/packages/openapi-ts/src/openApi/shared/utils/graph.ts b/packages/openapi-ts/src/openApi/shared/utils/graph.ts index 865c23883..4b5ed8fde 100644 --- a/packages/openapi-ts/src/openApi/shared/utils/graph.ts +++ b/packages/openapi-ts/src/openApi/shared/utils/graph.ts @@ -75,6 +75,12 @@ export const annotateChildScopes = (nodes: Graph['nodes']): void => { } }; +interface Cache { + allDependencies: Map>; + childDependencies: Map>; + parentToChildren: Map>; +} + /** * Recursively collects all $ref dependencies in the subtree rooted at `pointer`. */ @@ -84,12 +90,12 @@ const collectAllDependenciesForPointer = ({ pointer, visited, }: { - cache: Map>; + cache: Cache; graph: Graph; pointer: string; visited: Set; }): Set => { - const cached = cache.get(pointer); + const cached = cache.allDependencies.get(pointer); if (cached) { return cached; } @@ -128,21 +134,26 @@ const collectAllDependenciesForPointer = ({ } // Recursively collect dependencies of all children - for (const [childPointer, childInfo] of graph.nodes) { - if (childInfo.parentPointer === pointer) { - const transitiveDependencies = collectAllDependenciesForPointer({ - cache, - graph, - pointer: childPointer, - visited, - }); + const children = cache.parentToChildren.get(pointer); + if (children) { + for (const childPointer of children) { + let transitiveDependencies = cache.childDependencies.get(childPointer); + if (!transitiveDependencies) { + transitiveDependencies = collectAllDependenciesForPointer({ + cache, + graph, + pointer: childPointer, + visited, + }); + cache.childDependencies.set(childPointer, transitiveDependencies); + } for (const dep of transitiveDependencies) { allDependencies.add(dep); } } } - cache.set(pointer, allDependencies); + cache.allDependencies.set(pointer, allDependencies); return allDependencies; }; @@ -462,6 +473,21 @@ export const buildGraph = ( path: [], }); + const cache: Cache = { + allDependencies: new Map(), + childDependencies: new Map(), + parentToChildren: new Map(), + }; + + for (const [pointer, nodeInfo] of graph.nodes) { + const parent = nodeInfo.parentPointer; + if (!parent) continue; + if (!cache.parentToChildren.has(parent)) { + cache.parentToChildren.set(parent, []); + } + cache.parentToChildren.get(parent)!.push(pointer); + } + for (const [pointerFrom, pointers] of graph.dependencies) { for (const pointerTo of pointers) { if (!graph.reverseDependencies.has(pointerTo)) { @@ -475,7 +501,6 @@ export const buildGraph = ( propagateScopes(graph); annotateChildScopes(graph.nodes); - const cache = new Map>(); for (const pointer of graph.nodes.keys()) { const allDependencies = collectAllDependenciesForPointer({ cache, @@ -495,49 +520,3 @@ export const buildGraph = ( return { graph }; }; - -export const analyzeGraphStructure = (graph: Graph) => { - let maxDepth = 0; - let maxChildren = 0; - - const computeDepth = (pointer: string, depth: number): void => { - maxDepth = Math.max(maxDepth, depth); - - const children = Array.from(graph.nodes.entries()) - .filter(([, nodeInfo]) => nodeInfo.parentPointer === pointer) - .map(([childPointer]) => childPointer); - - maxChildren = Math.max(maxChildren, children.length); - - for (const childPointer of children) { - computeDepth(childPointer, depth + 1); - } - }; - - const totalNodes = graph.nodes.size; - if (graph.nodes.has('#')) { - computeDepth('#', 1); - } - - return { maxChildren, maxDepth, totalNodes }; -}; - -export const exportGraphForVisualization = (graph: Graph) => { - const childrenMap = new Map(); - - for (const [pointer, nodeInfo] of graph.nodes) { - if (!nodeInfo.parentPointer) continue; - if (!childrenMap.has(nodeInfo.parentPointer)) { - childrenMap.set(nodeInfo.parentPointer, []); - } - childrenMap.get(nodeInfo.parentPointer)!.push(pointer); - } - - const nodes = Array.from(graph.nodes.keys()).map((pointer) => ({ - children: childrenMap.get(pointer)?.length ?? 0, - childrenPointers: childrenMap.get(pointer) || [], - pointer, - })); - - return nodes; -}; diff --git a/packages/openapi-ts/src/openApi/shared/utils/graphDebug.ts b/packages/openapi-ts/src/openApi/shared/utils/graphDebug.ts new file mode 100644 index 000000000..68d685ae8 --- /dev/null +++ b/packages/openapi-ts/src/openApi/shared/utils/graphDebug.ts @@ -0,0 +1,47 @@ +import type { Graph } from './graph'; + +export const analyzeGraphStructure = (graph: Graph) => { + let maxDepth = 0; + let maxChildren = 0; + + const computeDepth = (pointer: string, depth: number): void => { + maxDepth = Math.max(maxDepth, depth); + + const children = Array.from(graph.nodes.entries()) + .filter(([, nodeInfo]) => nodeInfo.parentPointer === pointer) + .map(([childPointer]) => childPointer); + + maxChildren = Math.max(maxChildren, children.length); + + for (const childPointer of children) { + computeDepth(childPointer, depth + 1); + } + }; + + const totalNodes = graph.nodes.size; + if (graph.nodes.has('#')) { + computeDepth('#', 1); + } + + return { maxChildren, maxDepth, totalNodes }; +}; + +export const exportGraphForVisualization = (graph: Graph) => { + const childrenMap = new Map(); + + for (const [pointer, nodeInfo] of graph.nodes) { + if (!nodeInfo.parentPointer) continue; + if (!childrenMap.has(nodeInfo.parentPointer)) { + childrenMap.set(nodeInfo.parentPointer, []); + } + childrenMap.get(nodeInfo.parentPointer)!.push(pointer); + } + + const nodes = Array.from(graph.nodes.keys()).map((pointer) => ({ + children: childrenMap.get(pointer)?.length ?? 0, + childrenPointers: childrenMap.get(pointer) || [], + pointer, + })); + + return nodes; +}; diff --git a/packages/openapi-ts/src/utils/logger.ts b/packages/openapi-ts/src/utils/logger.ts index da0bd870b..c0cde2649 100644 --- a/packages/openapi-ts/src/utils/logger.ts +++ b/packages/openapi-ts/src/utils/logger.ts @@ -3,6 +3,7 @@ import colors from 'ansi-colors'; interface LoggerEvent { end?: PerformanceMark; events: Array; + id: string; // unique internal key name: string; start: PerformanceMark; } @@ -16,25 +17,12 @@ interface StoredEventResult { position: ReadonlyArray; } +let loggerCounter = 0; +const nameToId = (name: string) => `${name}-${loggerCounter++}`; const idEnd = (id: string) => `${id}-end`; - const idLength = (id: string) => `${id}-length`; - const idStart = (id: string) => `${id}-start`; -export const Performance = { - clear: (): void => { - performance.clearMarks(); - performance.clearMeasures(); - }, - end: (id: string): PerformanceMark => performance.mark(idEnd(id)), - getEntriesByName: (id: string): PerformanceEntryList => - performance.getEntriesByName(idLength(id)), - measure: (id: string): PerformanceMeasure => - performance.measure(idLength(id), idStart(id), idEnd(id)), - start: (id: string): PerformanceMark => performance.mark(idStart(id)), -}; - const getSeverity = ( duration: number, percentage: number, @@ -79,27 +67,33 @@ export class Logger { } } if (event && !event.end) { - event.end = performance.mark(idEnd(event.name)); + event.end = performance.mark(idEnd(event.id)); } } - report() { + report(print: boolean = true): PerformanceMeasure | undefined { const firstEvent = this.events[0]; if (!firstEvent) return; const lastEvent = this.events[this.events.length - 1]!; + const name = 'root'; + const id = nameToId(name); const measure = performance.measure( - idLength('root'), - idStart(firstEvent.name), - idEnd(lastEvent.name), + idLength(id), + idStart(firstEvent.id), + idEnd(lastEvent.id), ); - this.reportEvent({ - end: lastEvent.end, - events: this.events, - indent: 0, - measure, - name: 'root', - start: firstEvent!.start, - }); + if (print) { + this.reportEvent({ + end: lastEvent.end, + events: this.events, + id, + indent: 0, + measure, + name, + start: firstEvent!.start, + }); + } + return measure; } private reportEvent({ @@ -114,9 +108,9 @@ export class Logger { parent.events.forEach((event, index) => { const measure = performance.measure( - idLength(event.name), - idStart(event.name), - idEnd(event.name), + idLength(event.id), + idStart(event.id), + idEnd(event.id), ); const duration = Math.ceil(measure.duration * 100) / 100; const percentage = @@ -131,7 +125,7 @@ export class Logger { const branch = index === lastIndex ? '└─ ' : '├─ '; const prefix = !indent ? '' : '│ '.repeat(indent - 1) + branch; - const maxLength = 30 - prefix.length; + const maxLength = 38 - prefix.length; const percentageBranch = !indent ? '' : '↳ '; const percentagePrefix = indent @@ -151,14 +145,14 @@ export class Logger { }); } - private start(name: string): PerformanceMark { - return performance.mark(idStart(name)); + private start(id: string): PerformanceMark { + return performance.mark(idStart(id)); } private storeEvent({ result, ...event - }: Pick & { + }: Pick & { result: StoredEventResult; }): void { const lastEventIndex = event.events.length - 1; @@ -173,9 +167,11 @@ export class Logger { } timeEvent(name: string) { - const start = this.start(name); + const id = nameToId(name); + const start = this.start(id); const event: LoggerEvent = { events: this.events, + id, name, start, }; -- 2.51.2