fast-json-stringify 7.1.0 → 7.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -5
- package/benchmark/bench-cmp-lib.js +63 -38
- package/benchmark/bench-fixtures.js +54 -0
- package/benchmark/bench-large-array.js +64 -0
- package/benchmark/bench-thread.js +4 -2
- package/benchmark/bench-utils.js +41 -0
- package/benchmark/fixtures/api-responses.js +266 -0
- package/index.js +251 -59
- package/lib/location.js +2 -1
- package/lib/serializer.js +33 -0
- package/package.json +1 -1
- package/test/allof.test.js +51 -0
- package/test/array.test.js +147 -0
- package/test/schema-depth.test.js +14 -2
package/README.md
CHANGED
|
@@ -623,11 +623,15 @@ In order to improve that the user can set the `largeArrayMechanism` and
|
|
|
623
623
|
|
|
624
624
|
`largeArrayMechanism`'s default value is `default`. Valid values for it are:
|
|
625
625
|
|
|
626
|
-
- `default` - This option
|
|
627
|
-
still
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
626
|
+
- `default` - This option keeps the full feature set of this lib: every element
|
|
627
|
+
is still coerced to the type declared in `items`, and type errors are still
|
|
628
|
+
thrown. Arrays whose `items` schema is a plain `{ type: 'string' }` (no
|
|
629
|
+
`format`, `nullable`, `const`, or combinator keyword) are serialized at or
|
|
630
|
+
above `largeArraySize` by copying the elements and calling the native
|
|
631
|
+
`JSON.stringify` on the copy, which is several times faster than appending one
|
|
632
|
+
element at a time. If an element is not a string, that element and the ones
|
|
633
|
+
after it go through the regular per-element coercion, so the output is the
|
|
634
|
+
same as before.
|
|
631
635
|
- `json-stringify` - This option will remove support for schema validation
|
|
632
636
|
within **large arrays** completely. By doing so the overhead previously
|
|
633
637
|
mentioned is nulled, greatly improving execution time. Mind there's no change
|
|
@@ -642,6 +646,32 @@ integer-like values, such as:
|
|
|
642
646
|
- `'2e4'` - _note this will be converted to `2`, not `20000`_
|
|
643
647
|
- `1.5` - _note this will be converted to `1`_
|
|
644
648
|
|
|
649
|
+
##### Number and integer arrays
|
|
650
|
+
|
|
651
|
+
With `largeArrayMechanism: 'default'`, arrays whose `items` schema is a plain
|
|
652
|
+
`{ type: 'number' }` or `{ type: 'integer' }` (no `nullable`, `const`, `$ref`,
|
|
653
|
+
or combinator keyword) use the same copy-then-`JSON.stringify` strategy, but
|
|
654
|
+
with their own length cutoffs that do not depend on `largeArraySize`. The
|
|
655
|
+
cutoffs are the array lengths above which the copy beats the per-element loop,
|
|
656
|
+
and they depend on the V8 version:
|
|
657
|
+
|
|
658
|
+
| Node.js | `number` | `integer` |
|
|
659
|
+
|---|---:|---:|
|
|
660
|
+
| 22 and later | 128 elements | 512 elements |
|
|
661
|
+
| 20 | 300000 elements | 300000 elements |
|
|
662
|
+
|
|
663
|
+
Below the cutoff the array is serialized element by element as before. Above
|
|
664
|
+
it, the elements are copied until one is found that is not already a `number`
|
|
665
|
+
(or not already an integer, for `integer`); that element and the ones after it
|
|
666
|
+
go through the regular per-element coercion and validation (`null` still
|
|
667
|
+
becomes `0`, `rounding` still applies, `NaN` still throws). The output is
|
|
668
|
+
byte-identical either way. Arrays of `boolean`, `null`, objects, or any other
|
|
669
|
+
`items` schema are not affected.
|
|
670
|
+
|
|
671
|
+
In [standalone mode](#standalone) the cutoffs are baked into the generated
|
|
672
|
+
code, so the generated module inherits the cutoffs of the Node.js version that
|
|
673
|
+
ran `build()`, not of the one that runs the module.
|
|
674
|
+
|
|
645
675
|
<a name="unsafe"></a>
|
|
646
676
|
#### Unsafe string
|
|
647
677
|
By default, the library escapes all strings. With the 'unsafe' format, the string isn't escaped. This has a potentially dangerous security issue. You can use it only if you are sure that your data doesn't need escaping. The advantage is a significant performance improvement.
|
|
@@ -748,6 +778,12 @@ may require Ajv runtime helpers used by the generated validation code. Custom
|
|
|
748
778
|
Ajv formats used by these schemas must support Ajv's standalone code
|
|
749
779
|
generation.
|
|
750
780
|
|
|
781
|
+
The generated code also bakes in the length cutoffs used for
|
|
782
|
+
[number and integer arrays](#largearrays). Those cutoffs are chosen for the
|
|
783
|
+
Node.js version that ran `build()`; a module built on Node.js 20 and run on
|
|
784
|
+
Node.js 22 or later keeps the Node.js 20 cutoffs, and vice versa. The output is
|
|
785
|
+
the same either way, only the speed differs.
|
|
786
|
+
|
|
751
787
|
<a name="acknowledgments"></a>
|
|
752
788
|
## Acknowledgments
|
|
753
789
|
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
+
const assert = require('node:assert/strict')
|
|
3
4
|
const { Bench } = require('tinybench')
|
|
5
|
+
const apiResponses = require('./fixtures/api-responses')
|
|
4
6
|
const suite = new Bench({
|
|
5
7
|
name: 'Library Comparison Benchmarks',
|
|
6
8
|
setup: (_task, mode) => {
|
|
@@ -173,139 +175,162 @@ for (let i = 0; i < MULTI_ARRAY_LENGTH; i++) {
|
|
|
173
175
|
multiArray[i] = obj
|
|
174
176
|
}
|
|
175
177
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
+
// Some serializers return unflattened strings. Fastify calls Buffer.byteLength()
|
|
179
|
+
// for Content-Length, which triggers flattening; include that cost in every case.
|
|
180
|
+
suite.add('fast-json-stringify: creation and first stringify', function () {
|
|
181
|
+
Buffer.byteLength(FJS(schema)(obj))
|
|
178
182
|
})
|
|
179
|
-
suite.add('compile-json-stringify: creation', function () {
|
|
180
|
-
CJS(schemaCJS)
|
|
183
|
+
suite.add('compile-json-stringify: creation and first stringify', function () {
|
|
184
|
+
Buffer.byteLength(CJS(schemaCJS)(obj))
|
|
181
185
|
})
|
|
182
|
-
suite.add('AJV: creation', function () {
|
|
183
|
-
ajv.compileSerializer(schemaAJVJTD)
|
|
186
|
+
suite.add('AJV: creation and first stringify', function () {
|
|
187
|
+
Buffer.byteLength(ajv.compileSerializer(schemaAJVJTD)(obj))
|
|
184
188
|
})
|
|
185
|
-
suite.add('json-accelerator: creation', function () {
|
|
186
|
-
createAccelerator(schema)
|
|
189
|
+
suite.add('json-accelerator: creation and first stringify', function () {
|
|
190
|
+
Buffer.byteLength(createAccelerator(schema)(obj))
|
|
187
191
|
})
|
|
188
192
|
|
|
189
193
|
suite.add('JSON.stringify: array', function () {
|
|
190
|
-
JSON.stringify(multiArray)
|
|
194
|
+
Buffer.byteLength(JSON.stringify(multiArray))
|
|
191
195
|
})
|
|
192
196
|
|
|
193
197
|
suite.add('fast-json-stringify [default]: array', function () {
|
|
194
|
-
stringifyArrayDefault(multiArray)
|
|
198
|
+
Buffer.byteLength(stringifyArrayDefault(multiArray))
|
|
195
199
|
})
|
|
196
200
|
|
|
197
201
|
suite.add('json-accelerator: array', function () {
|
|
198
|
-
accelArray(multiArray)
|
|
202
|
+
Buffer.byteLength(accelArray(multiArray))
|
|
199
203
|
})
|
|
200
204
|
|
|
201
205
|
suite.add('fast-json-stringify [json-stringify]: array', function () {
|
|
202
|
-
stringifyArrayJSONStringify(multiArray)
|
|
206
|
+
Buffer.byteLength(stringifyArrayJSONStringify(multiArray))
|
|
203
207
|
})
|
|
204
208
|
|
|
205
209
|
suite.add('compile-json-stringify: array', function () {
|
|
206
|
-
CJSStringifyArray(multiArray)
|
|
210
|
+
Buffer.byteLength(CJSStringifyArray(multiArray))
|
|
207
211
|
})
|
|
208
212
|
|
|
209
213
|
suite.add('AJV: array', function () {
|
|
210
|
-
ajvSerializeArray(multiArray)
|
|
214
|
+
Buffer.byteLength(ajvSerializeArray(multiArray))
|
|
211
215
|
})
|
|
212
216
|
|
|
213
217
|
suite.add('JSON.stringify: large array', function () {
|
|
214
|
-
JSON.stringify(largeArray)
|
|
218
|
+
Buffer.byteLength(JSON.stringify(largeArray))
|
|
215
219
|
})
|
|
216
220
|
|
|
217
221
|
suite.add('fast-json-stringify [default]: large array', function () {
|
|
218
|
-
stringifyArrayDefault(largeArray)
|
|
222
|
+
Buffer.byteLength(stringifyArrayDefault(largeArray))
|
|
219
223
|
})
|
|
220
224
|
|
|
221
225
|
suite.add('fast-json-stringify [json-stringify]: large array', function () {
|
|
222
|
-
stringifyArrayJSONStringify(largeArray)
|
|
226
|
+
Buffer.byteLength(stringifyArrayJSONStringify(largeArray))
|
|
223
227
|
})
|
|
224
228
|
|
|
225
229
|
suite.add('compile-json-stringify: large array', function () {
|
|
226
|
-
CJSStringifyArray(largeArray)
|
|
230
|
+
Buffer.byteLength(CJSStringifyArray(largeArray))
|
|
227
231
|
})
|
|
228
232
|
|
|
229
233
|
suite.add('AJV: large array', function () {
|
|
230
|
-
ajvSerializeArray(largeArray)
|
|
234
|
+
Buffer.byteLength(ajvSerializeArray(largeArray))
|
|
231
235
|
})
|
|
232
236
|
|
|
233
237
|
suite.add('JSON.stringify: long string', function () {
|
|
234
|
-
JSON.stringify(str)
|
|
238
|
+
Buffer.byteLength(JSON.stringify(str))
|
|
235
239
|
})
|
|
236
240
|
|
|
237
241
|
suite.add('fast-json-stringify: long string', function () {
|
|
238
|
-
stringifyString(str)
|
|
242
|
+
Buffer.byteLength(stringifyString(str))
|
|
239
243
|
})
|
|
240
244
|
|
|
241
245
|
suite.add('json-accelerator: long string', function () {
|
|
242
|
-
stringifyString(str)
|
|
246
|
+
Buffer.byteLength(stringifyString(str))
|
|
243
247
|
})
|
|
244
248
|
|
|
245
249
|
suite.add('compile-json-stringify: long string', function () {
|
|
246
|
-
CJSStringifyString(str)
|
|
250
|
+
Buffer.byteLength(CJSStringifyString(str))
|
|
247
251
|
})
|
|
248
252
|
|
|
249
253
|
suite.add('AJV: long string', function () {
|
|
250
|
-
ajvSerializeString(str)
|
|
254
|
+
Buffer.byteLength(ajvSerializeString(str))
|
|
251
255
|
})
|
|
252
256
|
|
|
253
257
|
suite.add('JSON.stringify: short string', function () {
|
|
254
|
-
JSON.stringify('hello world')
|
|
258
|
+
Buffer.byteLength(JSON.stringify('hello world'))
|
|
255
259
|
})
|
|
256
260
|
|
|
257
261
|
suite.add('fast-json-stringify: short string', function () {
|
|
258
|
-
stringifyString('hello world')
|
|
262
|
+
Buffer.byteLength(stringifyString('hello world'))
|
|
259
263
|
})
|
|
260
264
|
|
|
261
265
|
suite.add('json-accelerator: short string', function () {
|
|
262
|
-
accelString('hello world')
|
|
266
|
+
Buffer.byteLength(accelString('hello world'))
|
|
263
267
|
})
|
|
264
268
|
|
|
265
269
|
suite.add('compile-json-stringify: short string', function () {
|
|
266
|
-
CJSStringifyString('hello world')
|
|
270
|
+
Buffer.byteLength(CJSStringifyString('hello world'))
|
|
267
271
|
})
|
|
268
272
|
|
|
269
273
|
suite.add('AJV: short string', function () {
|
|
270
|
-
ajvSerializeString('hello world')
|
|
274
|
+
Buffer.byteLength(ajvSerializeString('hello world'))
|
|
271
275
|
})
|
|
272
276
|
|
|
273
277
|
suite.add('JSON.stringify: obj', function () {
|
|
274
|
-
JSON.stringify(obj)
|
|
278
|
+
Buffer.byteLength(JSON.stringify(obj))
|
|
275
279
|
})
|
|
276
280
|
|
|
277
281
|
suite.add('fast-json-stringify: obj', function () {
|
|
278
|
-
stringify(obj)
|
|
282
|
+
Buffer.byteLength(stringify(obj))
|
|
279
283
|
})
|
|
280
284
|
|
|
281
285
|
suite.add('json-accelerator: obj', function () {
|
|
282
|
-
accelStringify(obj)
|
|
286
|
+
Buffer.byteLength(accelStringify(obj))
|
|
283
287
|
})
|
|
284
288
|
|
|
285
289
|
suite.add('compile-json-stringify: obj', function () {
|
|
286
|
-
CJSStringify(obj)
|
|
290
|
+
Buffer.byteLength(CJSStringify(obj))
|
|
287
291
|
})
|
|
288
292
|
|
|
289
293
|
suite.add('AJV: obj', function () {
|
|
290
|
-
ajvSerialize(obj)
|
|
294
|
+
Buffer.byteLength(ajvSerialize(obj))
|
|
291
295
|
})
|
|
292
296
|
|
|
293
297
|
suite.add('JSON.stringify: date', function () {
|
|
294
|
-
JSON.stringify(date)
|
|
298
|
+
Buffer.byteLength(JSON.stringify(date))
|
|
295
299
|
})
|
|
296
300
|
|
|
297
301
|
suite.add('fast-json-stringify: date', function () {
|
|
298
|
-
stringifyDate(date)
|
|
302
|
+
Buffer.byteLength(stringifyDate(date))
|
|
299
303
|
})
|
|
300
304
|
|
|
301
305
|
suite.add('json-accelerate: date', function () {
|
|
302
|
-
accelDate(date)
|
|
306
|
+
Buffer.byteLength(accelDate(date))
|
|
303
307
|
})
|
|
304
308
|
|
|
305
309
|
suite.add('compile-json-stringify: date', function () {
|
|
306
|
-
CJSStringifyDate(date)
|
|
310
|
+
Buffer.byteLength(CJSStringifyDate(date))
|
|
307
311
|
})
|
|
308
312
|
|
|
313
|
+
for (const { name, schema: responseSchema, input } of apiResponses) {
|
|
314
|
+
const stringifyResponse = FJS(responseSchema)
|
|
315
|
+
// Check outside the timed loop that the schema preserves the complete response.
|
|
316
|
+
assert.deepStrictEqual(JSON.parse(stringifyResponse(input)), JSON.parse(JSON.stringify(input)))
|
|
317
|
+
|
|
318
|
+
suite.add(`JSON.stringify: ${name}`, function () {
|
|
319
|
+
Buffer.byteLength(JSON.stringify(input))
|
|
320
|
+
})
|
|
321
|
+
suite.add(`fast-json-stringify: ${name}`, function () {
|
|
322
|
+
Buffer.byteLength(stringifyResponse(input))
|
|
323
|
+
})
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (process.argv.includes('--fjs-vs-json')) {
|
|
327
|
+
for (const task of suite.tasks) {
|
|
328
|
+
if (!task.name.startsWith('fast-json-stringify') && !task.name.startsWith('JSON.stringify:')) {
|
|
329
|
+
suite.remove(task.name)
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
309
334
|
suite.run().then(() => {
|
|
310
335
|
const results = suite.tasks.map(task => ({
|
|
311
336
|
name: task.name,
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const arrayKinds = ['integer', 'number', 'short-string', 'long-string', 'flat-object', 'nested-object', 'nullable-number', 'Float64Array', 'Int32Array']
|
|
4
|
+
const arraySizes = [1e3, 2e4, 1e5, 1e6]
|
|
5
|
+
const mechanisms = ['default', 'json-stringify', 'native']
|
|
6
|
+
const flatSchema = { type: 'object', properties: { s: { type: 'string' }, n: { type: 'number' }, b: { type: 'boolean' } } }
|
|
7
|
+
const longString = 'a"quoted" string\\with\ncontrols '.repeat(4)
|
|
8
|
+
|
|
9
|
+
function arrayFixture (kind, size) {
|
|
10
|
+
let items
|
|
11
|
+
let value
|
|
12
|
+
switch (kind) {
|
|
13
|
+
case 'integer':
|
|
14
|
+
case 'Int32Array':
|
|
15
|
+
items = { type: 'integer' }
|
|
16
|
+
value = i => (i % 2001) - 1000
|
|
17
|
+
break
|
|
18
|
+
case 'number':
|
|
19
|
+
case 'Float64Array':
|
|
20
|
+
items = { type: 'number' }
|
|
21
|
+
value = i => ((i % 2001) - 1000) / 10
|
|
22
|
+
break
|
|
23
|
+
case 'short-string':
|
|
24
|
+
items = { type: 'string' }
|
|
25
|
+
value = i => `hello ${i % 100}`
|
|
26
|
+
break
|
|
27
|
+
case 'long-string':
|
|
28
|
+
items = { type: 'string' }
|
|
29
|
+
value = () => longString
|
|
30
|
+
break
|
|
31
|
+
case 'flat-object':
|
|
32
|
+
items = flatSchema
|
|
33
|
+
value = i => ({ s: `hello ${i % 100}`, n: i % 1000, b: i % 2 === 0 })
|
|
34
|
+
break
|
|
35
|
+
case 'nested-object':
|
|
36
|
+
items = { type: 'object', properties: { id: { type: 'integer' }, inner: { type: 'array', items: flatSchema } } }
|
|
37
|
+
value = i => ({ id: i, inner: [{ s: 'hello', n: i % 1000, b: true }, { s: 'world', n: i / 10, b: false }] })
|
|
38
|
+
break
|
|
39
|
+
case 'nullable-number':
|
|
40
|
+
items = { type: 'number', nullable: true }
|
|
41
|
+
value = i => i % 4 === 0 ? null : i / 10
|
|
42
|
+
break
|
|
43
|
+
default:
|
|
44
|
+
throw new Error(`Unknown array kind: ${kind}`)
|
|
45
|
+
}
|
|
46
|
+
const input = kind === 'Float64Array'
|
|
47
|
+
? Float64Array.from({ length: size }, (_, i) => value(i))
|
|
48
|
+
: kind === 'Int32Array'
|
|
49
|
+
? Int32Array.from({ length: size }, (_, i) => value(i))
|
|
50
|
+
: Array.from({ length: size }, (_, i) => value(i))
|
|
51
|
+
return { schema: { type: 'array', items }, input, typed: ArrayBuffer.isView(input) }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
module.exports = { arrayKinds, arraySizes, mechanisms, arrayFixture }
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const assert = require('node:assert/strict')
|
|
4
|
+
const { parseArgs } = require('node:util')
|
|
5
|
+
const { isMainThread, parentPort, workerData } = require('node:worker_threads')
|
|
6
|
+
const { Bench } = require('tinybench')
|
|
7
|
+
const build = require('..')
|
|
8
|
+
const { arrayKinds, arraySizes, mechanisms, arrayFixture } = require('./bench-fixtures')
|
|
9
|
+
const { positiveNumber, runWorker, saveResults } = require('./bench-utils')
|
|
10
|
+
|
|
11
|
+
async function measure ({ kind, size, mechanism, time, iterations }) {
|
|
12
|
+
const { schema, input, typed } = arrayFixture(kind, size)
|
|
13
|
+
const stringify = mechanism === 'native' ? JSON.stringify : build(schema, { largeArrayMechanism: mechanism })
|
|
14
|
+
if (typed && mechanism !== 'native') {
|
|
15
|
+
// Rejection is existing behavior; do not time exceptions as serialization.
|
|
16
|
+
assert.throws(() => stringify(input), TypeError)
|
|
17
|
+
return { kind, size, mechanism, status: 'unsupported (TypeError)' }
|
|
18
|
+
}
|
|
19
|
+
assert.equal(stringify(input), JSON.stringify(input))
|
|
20
|
+
const bytes = Buffer.byteLength(stringify(input))
|
|
21
|
+
const bench = new Bench({ time, iterations, warmupTime: 100, warmupIterations: 2, throws: true })
|
|
22
|
+
bench.add(kind, () => {
|
|
23
|
+
Buffer.byteLength(stringify(input))
|
|
24
|
+
})
|
|
25
|
+
await bench.run()
|
|
26
|
+
const { latency, throughput } = bench.tasks[0].result
|
|
27
|
+
return {
|
|
28
|
+
kind,
|
|
29
|
+
size,
|
|
30
|
+
mechanism,
|
|
31
|
+
status: 'ok',
|
|
32
|
+
bytes,
|
|
33
|
+
ops: throughput.p50,
|
|
34
|
+
meanOps: throughput.mean,
|
|
35
|
+
medianMs: latency.p50,
|
|
36
|
+
rme: latency.rme,
|
|
37
|
+
samples: latency.samplesCount
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function main () {
|
|
42
|
+
const { values } = parseArgs({ options: { kind: { type: 'string' }, size: { type: 'string' }, time: { type: 'string', default: '300' }, iterations: { type: 'string', default: '8' }, json: { type: 'string' } } })
|
|
43
|
+
const kinds = values.kind ? [values.kind] : arrayKinds
|
|
44
|
+
if (kinds.some(kind => !arrayKinds.includes(kind))) throw new Error(`kind must be one of ${arrayKinds.join(', ')}`)
|
|
45
|
+
const sizes = values.size ? [positiveNumber(values.size, 'size', true)] : arraySizes
|
|
46
|
+
const config = { time: positiveNumber(values.time, 'time'), iterations: positiveNumber(values.iterations, 'iterations', true) }
|
|
47
|
+
const rows = []
|
|
48
|
+
console.log('| Kind | Size | Mechanism | Median ops/sec | Latency RME % | Samples |')
|
|
49
|
+
console.log('| --- | ---: | --- | ---: | ---: | ---: |')
|
|
50
|
+
for (const size of sizes) {
|
|
51
|
+
for (const kind of kinds) {
|
|
52
|
+
for (const mechanism of mechanisms) {
|
|
53
|
+
const row = await runWorker(__filename, { kind, size, mechanism, ...config })
|
|
54
|
+
rows.push(row)
|
|
55
|
+
console.log(`| ${kind} | ${size} | ${mechanism} | ${row.ops?.toFixed(2) ?? row.status} | ${row.rme?.toFixed(2) ?? '—'} | ${row.samples ?? '—'} |`)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
console.log('\nTyped arrays are rejected by FJS; native serializes indexed objects. Each measured call includes Buffer.byteLength().')
|
|
60
|
+
saveResults(values.json, { ...config, kinds, sizes }, rows)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (isMainThread) main().catch(err => { console.error(err); process.exitCode = 1 })
|
|
64
|
+
else measure(workerData).then(result => parentPort.postMessage(result)).catch(err => { throw err })
|
|
@@ -18,10 +18,12 @@ const FJS = require('..')
|
|
|
18
18
|
const stringify = benchmark.compile ? null : FJS(benchmark.schema, benchmark.options)
|
|
19
19
|
|
|
20
20
|
bench.add(benchmark.name, () => {
|
|
21
|
+
// Some serializers return unflattened strings. Fastify calls Buffer.byteLength()
|
|
22
|
+
// for Content-Length, which triggers flattening; include that cost here too.
|
|
21
23
|
if (benchmark.compile) {
|
|
22
|
-
FJS(benchmark.schema, benchmark.options)(benchmark.input)
|
|
24
|
+
Buffer.byteLength(FJS(benchmark.schema, benchmark.options)(benchmark.input))
|
|
23
25
|
} else {
|
|
24
|
-
stringify(benchmark.input)
|
|
26
|
+
Buffer.byteLength(stringify(benchmark.input))
|
|
25
27
|
}
|
|
26
28
|
}).run().then(() => {
|
|
27
29
|
const task = bench.tasks[0]
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { Worker } = require('node:worker_threads')
|
|
4
|
+
const { writeFileSync } = require('node:fs')
|
|
5
|
+
const os = require('node:os')
|
|
6
|
+
|
|
7
|
+
function positiveNumber (value, name, integer = false) {
|
|
8
|
+
const result = Number(value)
|
|
9
|
+
if (!Number.isFinite(result) || result <= 0 || (integer && !Number.isInteger(result))) {
|
|
10
|
+
throw new Error(`${name} must be a positive ${integer ? 'integer' : 'number'}`)
|
|
11
|
+
}
|
|
12
|
+
return result
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function runWorker (file, workerData) {
|
|
16
|
+
return new Promise((resolve, reject) => {
|
|
17
|
+
const worker = new Worker(file, { workerData })
|
|
18
|
+
let result
|
|
19
|
+
worker.on('message', value => { result = value })
|
|
20
|
+
worker.on('error', reject)
|
|
21
|
+
worker.on('exit', code => {
|
|
22
|
+
if (code !== 0 || result === undefined) reject(new Error(`Benchmark worker exited ${code} without a valid result`))
|
|
23
|
+
else resolve(result)
|
|
24
|
+
})
|
|
25
|
+
})
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function saveResults (file, config, rows) {
|
|
29
|
+
if (!file) return
|
|
30
|
+
writeFileSync(file, JSON.stringify({
|
|
31
|
+
timestamp: new Date().toISOString(),
|
|
32
|
+
node: process.version,
|
|
33
|
+
platform: `${process.platform}/${process.arch}`,
|
|
34
|
+
cpu: os.cpus()[0].model,
|
|
35
|
+
cpus: os.cpus().length,
|
|
36
|
+
config,
|
|
37
|
+
rows
|
|
38
|
+
}, null, 2) + '\n')
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
module.exports = { positiveNumber, runWorker, saveResults }
|