Skip to content

Commit 401efc3

Browse files
committed
add optimizer v2
1 parent 17bb4c2 commit 401efc3

File tree

6 files changed

+297
-93
lines changed

6 files changed

+297
-93
lines changed

benchmark/bench-thread.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
const { workerData: benchmark, parentPort } = require('worker_threads')
44

55
const Benchmark = require('benchmark')
6-
Benchmark.options.minSamples = 500
6+
Benchmark.options.minSamples = 100
77

88
const suite = Benchmark.Suite()
99

index.js

+27-26
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const Serializer = require('./lib/serializer')
1111
const Validator = require('./lib/validator')
1212
const RefResolver = require('./lib/ref-resolver')
1313
const Location = require('./lib/location')
14+
const optimize = require('./lib/optimize')
1415

1516
let largeArraySize = 2e4
1617
let largeArrayMechanism = 'default'
@@ -27,6 +28,18 @@ const validLargeArrayMechanisms = [
2728
'json-stringify'
2829
]
2930

31+
const serializerFns = `
32+
const {
33+
asString,
34+
asInteger,
35+
asNumber,
36+
asBoolean,
37+
asDateTime,
38+
asDate,
39+
asTime,
40+
} = serializer
41+
`
42+
3043
const addComma = '!addComma && (addComma = true) || (json += \',\')'
3144

3245
function isValidSchema (schema, name) {
@@ -119,21 +132,8 @@ function build (schema, options) {
119132
const location = new Location(schema, context.rootSchemaId)
120133
const code = buildValue(context, location, 'input')
121134

122-
let contextFunctionCode
123-
124-
// If we have only the invocation of the 'anonymous0' function, we would
125-
// basically just wrap the 'anonymous0' function in the 'main' function and
126-
// and the overhead of the intermediate variable 'json'. We can avoid the
127-
// wrapping and the unnecessary memory allocation by aliasing 'anonymous0' to
128-
// 'main'
129-
if (code === 'json += anonymous0(input)') {
130-
contextFunctionCode = `
131-
${context.functions.join('\n')}
132-
const main = anonymous0
133-
return main
134-
`
135-
} else {
136-
contextFunctionCode = `
135+
let contextFunctionCode = `
136+
${serializerFns}
137137
function main (input) {
138138
let json = ''
139139
${code}
@@ -142,7 +142,8 @@ function build (schema, options) {
142142
${context.functions.join('\n')}
143143
return main
144144
`
145-
}
145+
146+
contextFunctionCode = optimize(contextFunctionCode)
146147

147148
const serializer = new Serializer(options)
148149
const validator = new Validator(options.ajv)
@@ -263,7 +264,7 @@ function buildExtraObjectPropertiesSerializer (context, location) {
263264
code += `
264265
if (/${propertyKey.replace(/\\*\//g, '\\/')}/.test(key)) {
265266
${addComma}
266-
json += serializer.asString(key) + ':'
267+
json += asString(key) + ':'
267268
${buildValue(context, propertyLocation, 'value')}
268269
continue
269270
}
@@ -278,13 +279,13 @@ function buildExtraObjectPropertiesSerializer (context, location) {
278279
if (additionalPropertiesSchema === true) {
279280
code += `
280281
${addComma}
281-
json += serializer.asString(key) + ':' + JSON.stringify(value)
282+
json += asString(key) + ':' + JSON.stringify(value)
282283
`
283284
} else {
284285
const propertyLocation = location.getPropertyLocation('additionalProperties')
285286
code += `
286287
${addComma}
287-
json += serializer.asString(key) + ':'
288+
json += asString(key) + ':'
288289
${buildValue(context, propertyLocation, 'value')}
289290
`
290291
}
@@ -743,21 +744,21 @@ function buildSingleTypeSerializer (context, location, input) {
743744
return 'json += \'null\''
744745
case 'string': {
745746
if (schema.format === 'date-time') {
746-
return `json += serializer.asDateTime(${input})`
747+
return `json += asDateTime(${input})`
747748
} else if (schema.format === 'date') {
748-
return `json += serializer.asDate(${input})`
749+
return `json += asDate(${input})`
749750
} else if (schema.format === 'time') {
750-
return `json += serializer.asTime(${input})`
751+
return `json += asTime(${input})`
751752
} else {
752-
return `json += serializer.asString(${input})`
753+
return `json += asString(${input})`
753754
}
754755
}
755756
case 'integer':
756-
return `json += serializer.asInteger(${input})`
757+
return `json += asInteger(${input})`
757758
case 'number':
758-
return `json += serializer.asNumber(${input})`
759+
return `json += asNumber(${input})`
759760
case 'boolean':
760-
return `json += serializer.asBoolean(${input})`
761+
return `json += asBoolean(${input})`
761762
case 'object': {
762763
const funcName = buildObject(context, location)
763764
return `json += ${funcName}(${input})`

lib/optimize.js

+89
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
'use strict'
2+
3+
const returnFnRE = /^\s+return ([.a-zA-Z0-9]+)\(\w+\)$/
4+
const fnRE = /^\s*function\s+/
5+
const fnNameRE = /^\s+function ([a-zA-Z0-9_]+) \(input\) {$/
6+
const jsonConcatRE = /^\s*json\s*\+=/
7+
const letJsonRE = /^\s*let json =/
8+
const returnJsonRE = /^\s*return json\s*$/
9+
const returnEmptyStringRE = /^\s*return '' \+/
10+
const closingCurlyBracketRE = /^\s*}\s*$/
11+
/**
12+
* @param {Array<string>} code
13+
* @returns {Array<string>}
14+
*/
15+
function optimize (raw) {
16+
const code = raw.split('\n')
17+
/**
18+
* @type {Array<string>}
19+
*/
20+
const dedupedLevel1 = []
21+
22+
for (let i = 0; i < code.length; i++) {
23+
if (i > 0 && jsonConcatRE.test(code[i]) && jsonConcatRE.test(code[i - 1])) {
24+
const mergedEntry = code[i - 1] + ' +' + code[i].substring(code[i].indexOf('json +=') + 7)
25+
dedupedLevel1.pop() // Remove the previous entry
26+
dedupedLevel1.push(mergedEntry)
27+
} else {
28+
dedupedLevel1.push(code[i])
29+
}
30+
}
31+
32+
/**
33+
* @type {Array<string>}
34+
*/
35+
const dedupedLevel2 = []
36+
for (let i = 0; i < dedupedLevel1.length; i++) {
37+
if (i > 0 && jsonConcatRE.test(dedupedLevel1[i]) && letJsonRE.test(dedupedLevel1[i - 1])) {
38+
const mergedEntry = dedupedLevel1[i - 1] + ' +' + dedupedLevel1[i].substring(dedupedLevel1[i].indexOf('json +=') + 7)
39+
dedupedLevel2.pop() // Remove the previous entry
40+
dedupedLevel2.push(mergedEntry)
41+
} else {
42+
dedupedLevel2.push(dedupedLevel1[i])
43+
}
44+
}
45+
46+
/**
47+
* @type {Array<string>}
48+
*/
49+
const dedupedLevel3 = []
50+
for (let i = 0; i < dedupedLevel2.length; i++) {
51+
if (i > 0 && returnJsonRE.test(dedupedLevel2[i]) && letJsonRE.test(dedupedLevel2[i - 1])) {
52+
const mergedEntry = dedupedLevel2[i].slice(0, dedupedLevel2[i].indexOf('return') + 6) + dedupedLevel2[i - 1].substring(dedupedLevel2[i - 1].indexOf('let json =') + 10)
53+
dedupedLevel3.pop() // Remove the previous entry
54+
dedupedLevel3.push(mergedEntry)
55+
} else {
56+
dedupedLevel3.push(dedupedLevel2[i])
57+
}
58+
}
59+
60+
/**
61+
* @type {Array<string>}
62+
*/
63+
for (let i = 0; i < dedupedLevel3.length; i++) {
64+
if (returnEmptyStringRE.test(dedupedLevel3[i])) {
65+
dedupedLevel3[i] = dedupedLevel3[i].replace('return \'\' +', 'return')
66+
}
67+
}
68+
69+
const dedupedLevel4 = []
70+
for (let i = 0; i < dedupedLevel3.length; i++) {
71+
if (
72+
fnRE.test(dedupedLevel3[i]) &&
73+
returnFnRE.test(dedupedLevel3[i + 1]) &&
74+
closingCurlyBracketRE.test(dedupedLevel3[i + 2])
75+
) {
76+
const serializerFnName = dedupedLevel3[i + 1].match(returnFnRE)[1]
77+
const fnName = dedupedLevel3[i].match(fnNameRE)[1]
78+
const whitespace = dedupedLevel3[i].slice(0, dedupedLevel3[i].indexOf('f'))
79+
dedupedLevel4[i] = `${whitespace}const ${fnName} = ${serializerFnName}`
80+
i += 2
81+
} else {
82+
dedupedLevel4.push(dedupedLevel3[i])
83+
}
84+
}
85+
86+
return dedupedLevel4.join('\n')
87+
}
88+
89+
module.exports = optimize

0 commit comments

Comments
 (0)