Skip to content
This repository was archived by the owner on Jun 22, 2023. It is now read-only.

Commit ed417b0

Browse files
authored
Merge pull request bcherny#447 from bcherny/perf/optimize-optimizer
Optimize optimizer to avoid deep JSON serialization (fix bcherny#422)
2 parents f943f32 + 6fbcbc8 commit ed417b0

11 files changed

+13932
-70
lines changed

Diff for: package.json

-1
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,6 @@
5555
"glob-promise": "^3.4.0",
5656
"is-glob": "^4.0.1",
5757
"json-schema-ref-parser": "^9.0.9",
58-
"json-stringify-safe": "^5.0.1",
5958
"lodash": "^4.17.20",
6059
"minimist": "^1.2.5",
6160
"mkdirp": "^1.0.4",

Diff for: src/generator.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {omit} from 'lodash'
1+
import {memoize, omit} from 'lodash'
22
import {DEFAULT_OPTIONS, Options} from './index'
33
import {
44
AST,
@@ -156,7 +156,7 @@ function declareNamedTypes(ast: AST, options: Options, rootASTName: string, proc
156156
return type
157157
}
158158

159-
function generateType(ast: AST, options: Options): string {
159+
function generateTypeUnmemoized(ast: AST, options: Options): string {
160160
const type = generateRawType(ast, options)
161161

162162
if (options.strictIndexSignatures && ast.keyName === '[k: string]') {
@@ -165,6 +165,7 @@ function generateType(ast: AST, options: Options): string {
165165

166166
return type
167167
}
168+
export const generateType = memoize(generateTypeUnmemoized)
168169

169170
function generateRawType(ast: AST, options: Options): string {
170171
log('magenta', 'generator', ast)

Diff for: src/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ export async function compile(schema: JSONSchema4, name: string, options: Partia
157157
const parsed = parse(normalized, _options)
158158
log('blue', 'parser', time(), '✅ Result:', parsed)
159159

160-
const optimized = optimize(parsed)
160+
const optimized = optimize(parsed, _options)
161161
if (process.env.VERBOSE) {
162162
if (isDeepStrictEqual(parsed, optimized)) {
163163
log('cyan', 'optimizer', time(), '✅ No change')

Diff for: src/optimizer.ts

+42-23
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
1-
import stringify = require('json-stringify-safe')
21
import {uniqBy} from 'lodash'
2+
import {Options} from '.'
3+
import {generateType} from './generator'
34
import {AST, T_ANY, T_UNKNOWN} from './types/AST'
45
import {log} from './utils'
56

6-
export function optimize(ast: AST, processed = new Set<AST>()): AST {
7-
log('cyan', 'optimizer', ast, processed.has(ast) ? '(FROM CACHE)' : '')
8-
7+
export function optimize(ast: AST, options: Options, processed = new Set<AST>()): AST {
98
if (processed.has(ast)) {
109
return ast
1110
}
@@ -15,41 +14,61 @@ export function optimize(ast: AST, processed = new Set<AST>()): AST {
1514
switch (ast.type) {
1615
case 'INTERFACE':
1716
return Object.assign(ast, {
18-
params: ast.params.map(_ => Object.assign(_, {ast: optimize(_.ast, processed)}))
17+
params: ast.params.map(_ => Object.assign(_, {ast: optimize(_.ast, options, processed)}))
1918
})
2019
case 'INTERSECTION':
2120
case 'UNION':
21+
// Start with the leaves...
22+
const optimizedAST = Object.assign(ast, {
23+
params: ast.params.map(_ => optimize(_, options, processed))
24+
})
25+
2226
// [A, B, C, Any] -> Any
23-
if (ast.params.some(_ => _.type === 'ANY')) {
24-
log('cyan', 'optimizer', '[A, B, C, Any] -> Any', ast)
27+
if (optimizedAST.params.some(_ => _.type === 'ANY')) {
28+
log('cyan', 'optimizer', '[A, B, C, Any] -> Any', optimizedAST)
2529
return T_ANY
2630
}
2731

2832
// [A, B, C, Unknown] -> Unknown
29-
if (ast.params.some(_ => _.type === 'UNKNOWN')) {
30-
log('cyan', 'optimizer', '[A, B, C, Unknown] -> Unknown', ast)
33+
if (optimizedAST.params.some(_ => _.type === 'UNKNOWN')) {
34+
log('cyan', 'optimizer', '[A, B, C, Unknown] -> Unknown', optimizedAST)
3135
return T_UNKNOWN
3236
}
3337

38+
// [A (named), A] -> [A (named)]
39+
if (
40+
optimizedAST.params.every(_ => {
41+
const a = generateType(omitStandaloneName(_), options)
42+
const b = generateType(omitStandaloneName(optimizedAST.params[0]), options)
43+
return a === b
44+
}) &&
45+
optimizedAST.params.some(_ => _.standaloneName !== undefined)
46+
) {
47+
log('cyan', 'optimizer', '[A (named), A] -> [A (named)]', optimizedAST)
48+
optimizedAST.params = optimizedAST.params.filter(_ => _.standaloneName !== undefined)
49+
}
50+
3451
// [A, B, B] -> [A, B]
35-
const shouldTakeStandaloneNameIntoAccount = ast.params.filter(_ => _.standaloneName).length > 1
36-
const params = uniqBy(
37-
ast.params,
38-
_ => `
39-
${_.type}-
40-
${shouldTakeStandaloneNameIntoAccount ? _.standaloneName : ''}-
41-
${stringify((_ as any).params)}
42-
`
43-
)
44-
if (params.length !== ast.params.length) {
45-
log('cyan', 'optimizer', '[A, B, B] -> [A, B]', ast)
46-
ast.params = params
52+
const params = uniqBy(optimizedAST.params, _ => generateType(_, options))
53+
if (params.length !== optimizedAST.params.length) {
54+
log('cyan', 'optimizer', '[A, B, B] -> [A, B]', optimizedAST)
55+
optimizedAST.params = params
4756
}
4857

49-
return Object.assign(ast, {
50-
params: ast.params.map(_ => optimize(_, processed))
58+
return Object.assign(optimizedAST, {
59+
params: optimizedAST.params.map(_ => optimize(_, options, processed))
5160
})
5261
default:
5362
return ast
5463
}
5564
}
65+
66+
// TODO: More clearly disambiguate standalone names vs. aliased names instead.
67+
function omitStandaloneName<A extends AST>(ast: A): A {
68+
switch (ast.type) {
69+
case 'ENUM':
70+
return ast
71+
default:
72+
return {...ast, standaloneName: undefined}
73+
}
74+
}

0 commit comments

Comments
 (0)