fast-json-stringify 3.0.3 → 4.0.0

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.
@@ -8,6 +8,8 @@ jobs:
8
8
  benchmark:
9
9
  if: ${{ github.event.label.name == 'benchmark' }}
10
10
  runs-on: ubuntu-latest
11
+ permissions:
12
+ contents: read
11
13
  outputs:
12
14
  PR-BENCH-12: ${{ steps.benchmark-pr.outputs.BENCH_RESULT12 }}
13
15
  PR-BENCH-14: ${{ steps.benchmark-pr.outputs.BENCH_RESULT14 }}
@@ -19,12 +21,13 @@ jobs:
19
21
  matrix:
20
22
  node-version: [12, 14, 16]
21
23
  steps:
22
- - uses: actions/checkout@v2
24
+ - uses: actions/checkout@v3
23
25
  with:
24
- ref: ${{github.event.pull_request.head.ref}}
26
+ persist-credentials: false
27
+ ref: ${{github.event.pull_request.head.sha}}
25
28
  repository: ${{github.event.pull_request.head.repo.full_name}}
26
29
 
27
- - uses: actions/setup-node@v2
30
+ - uses: actions/setup-node@v3
28
31
  with:
29
32
  node-version: ${{ matrix.node-version }}
30
33
 
@@ -43,7 +46,7 @@ jobs:
43
46
  echo "::set-output name=BENCH_RESULT${{matrix.node-version}}::$content"
44
47
 
45
48
  # main benchmark
46
- - uses: actions/checkout@v2
49
+ - uses: actions/checkout@v3
47
50
  with:
48
51
  ref: 'master'
49
52
 
@@ -64,6 +67,8 @@ jobs:
64
67
  output-benchmark:
65
68
  needs: [benchmark]
66
69
  runs-on: ubuntu-latest
70
+ permissions:
71
+ pull-requests: write
67
72
  steps:
68
73
  - name: Comment PR
69
74
  uses: thollander/actions-comment-pull-request@v1
@@ -12,36 +12,6 @@ on:
12
12
 
13
13
  jobs:
14
14
  test:
15
- runs-on: ${{ matrix.os }}
16
- strategy:
17
- matrix:
18
- node-version: [14, 16, 17]
19
- os: [macos-latest, ubuntu-latest, windows-latest]
20
-
21
- steps:
22
- - uses: actions/checkout@v2
23
-
24
- - name: Use Node.js
25
- uses: actions/setup-node@v2
26
- with:
27
- node-version: ${{ matrix.node-version }}
28
-
29
- - name: Install Dependencies
30
- run: |
31
- npm install --ignore-scripts
32
-
33
- - name: Run Tests
34
- run: |
35
- npm test
36
-
37
- automerge:
38
- needs: test
39
- runs-on: ubuntu-latest
40
- permissions:
41
- pull-requests: write
42
- contents: write
43
- steps:
44
- - uses: fastify/github-action-merge-dependabot@v3
45
- with:
46
- github-token: ${{ secrets.GITHUB_TOKEN }}
47
- target: minor
15
+ uses: fastify/workflows/.github/workflows/plugins-ci.yml@v3
16
+ with:
17
+ auto-merge-exclude: 'chalk'
package/README.md CHANGED
@@ -2,13 +2,12 @@
2
2
 
3
3
  ![CI](https://github.com/fastify/fast-json-stringify/workflows/CI/badge.svg)
4
4
  [![NPM version](https://img.shields.io/npm/v/fast-json-stringify.svg?style=flat)](https://www.npmjs.com/package/fast-json-stringify)
5
- [![Known Vulnerabilities](https://snyk.io/test/github/fastify/fast-json-stringify/badge.svg)](https://snyk.io/test/github/fastify/fast-json-stringify)
6
5
  [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat)](https://standardjs.com/)
7
6
  [![NPM downloads](https://img.shields.io/npm/dm/fast-json-stringify.svg?style=flat)](https://www.npmjs.com/package/fast-json-stringify)
8
7
 
9
8
 
10
- __fast-json-stringify__ is significantly faster than `JSON.stringify()` for small payloads.
11
- Its performance advantage shrinks as your payload grows.
9
+ __fast-json-stringify__ is significantly faster than `JSON.stringify()` for small payloads.
10
+ Its performance advantage shrinks as your payload grows.
12
11
  It pairs well with [__flatstr__](https://www.npmjs.com/package/flatstr), which triggers a V8 optimization that improves performance when eventually converting the string to a `Buffer`.
13
12
 
14
13
 
@@ -56,11 +55,12 @@ compile-json-stringify date format x 1,086,187 ops/sec ±0.16% (99 runs sampled)
56
55
  - <a href="#missingFields">`Missing fields`</a>
57
56
  - <a href="#patternProperties">`Pattern Properties`</a>
58
57
  - <a href="#additionalProperties">`Additional Properties`</a>
59
- - <a href="#anyof">`AnyOf` and `OneOf`</a>
58
+ - <a href="#AnyOf-and-OneOf">`AnyOf` and `OneOf`</a>
60
59
  - <a href="#ref">`Reuse - $ref`</a>
61
60
  - <a href="#long">`Long integers`</a>
62
61
  - <a href="#integer">`Integers`</a>
63
62
  - <a href="#nullable">`Nullable`</a>
63
+ - <a href="#largearrays">`Large Arrays`</a>
64
64
  - <a href="#security">`Security Notice`</a>
65
65
  - <a href="#acknowledgements">`Acknowledgements`</a>
66
66
  - <a href="#license">`License`</a>
@@ -117,6 +117,8 @@ const stringify = fastJson(mySchema, {
117
117
  - `schema`: external schemas references by $ref property. [More details](#ref)
118
118
  - `ajv`: [ajv v8 instance's settings](https://ajv.js.org/options.html) for those properties that require `ajv`. [More details](#anyof)
119
119
  - `rounding`: setup how the `integer` types will be rounded when not integers. [More details](#integer)
120
+ - `largeArrayMechanism`: set the mechanism that should be used to handle large
121
+ (by default `20000` or more items) arrays. [More details](#largearrays)
120
122
 
121
123
 
122
124
  <a name="api"></a>
@@ -157,18 +159,17 @@ And nested ones, too.
157
159
 
158
160
  **Note**: In the case of string formatted Date and not Date Object, there will be no manipulation on it. It should be properly formatted.
159
161
 
160
- Example with a MomentJS object:
162
+ Example with a Date object:
161
163
 
162
164
  ```javascript
163
- const moment = require('moment')
164
-
165
165
  const stringify = fastJson({
166
166
  title: 'Example Schema with string date-time field',
167
167
  type: 'string',
168
168
  format: 'date-time'
169
169
  })
170
170
 
171
- console.log(stringify(moment())) // '"YYYY-MM-DDTHH:mm:ss.sssZ"'
171
+ const date = new Date()
172
+ console.log(stringify(date)) // '"YYYY-MM-DDTHH:mm:ss.sssZ"'
172
173
  ```
173
174
 
174
175
 
@@ -268,7 +269,7 @@ const stringify = fastJson({
268
269
  const obj = {
269
270
  nickname: 'nick',
270
271
  matchfoo: 42,
271
- otherfoo: 'str'
272
+ otherfoo: 'str',
272
273
  matchnum: 3
273
274
  }
274
275
 
@@ -284,7 +285,7 @@ console.log(stringify(obj)) // '{"matchfoo":"42","otherfoo":"str","matchnum":3,"
284
285
  If *additionalProperties* is not present or is set to `false`, every property that is not explicitly listed in the *properties* and *patternProperties* objects,will be ignored, as described in <a href="#missingFields">Missing fields</a>.
285
286
  Missing fields are ignored to avoid having to rewrite objects before serializing. However, other schema rules would throw in similar situations.
286
287
  If *additionalProperties* is set to `true`, it will be used by `JSON.stringify` to stringify the additional properties. If you want to achieve maximum performance, we strongly encourage you to use a fixed schema where possible.
287
- The additional properties will always be serialzied at the end of the object.
288
+ The additional properties will always be serialized at the end of the object.
288
289
  Example:
289
290
  ```javascript
290
291
  const stringify = fastJson({
@@ -311,7 +312,7 @@ const stringify = fastJson({
311
312
  const obj = {
312
313
  nickname: 'nick',
313
314
  matchfoo: 42,
314
- otherfoo: 'str'
315
+ otherfoo: 'str',
315
316
  matchnum: 3,
316
317
  nomatchstr: 'valar morghulis',
317
318
  nomatchint: 313
@@ -582,6 +583,59 @@ Otherwise, instead of raising an error, null values will be coerced as follows:
582
583
  - `string` -> `""`
583
584
  - `boolean` -> `false`
584
585
 
586
+ <a name="largearrays"></a>
587
+ #### Large Arrays
588
+
589
+ Large arrays are, for the scope of this document, defined as arrays containing,
590
+ by default, `20000` elements or more. That value can be adjusted via the option
591
+ parameter `largeArraySize`.
592
+
593
+ At some point the overhead caused by the default mechanism used by
594
+ `fast-json-stringify` to handle arrays starts increasing exponentially, leading
595
+ to slow overall executions.
596
+
597
+ ##### Settings
598
+
599
+ In order to improve that the user can set the `largeArrayMechanism` and
600
+ `largeArraySize` options.
601
+
602
+ `largeArrayMechanism`'s default value is `default`. Valid values for it are:
603
+
604
+ - `default` - This option is a compromise between performance and feature set by
605
+ still providing the expected functionality out of this lib but giving up some
606
+ possible performance gain. With this option set, **large arrays** would be
607
+ stringified by joining their stringified elements using `Array.join` instead of
608
+ string concatenation for better performance
609
+ - `json-stringify` - This option will remove support for schema validation
610
+ within **large arrays** completely. By doing so the overhead previously
611
+ mentioned is nulled, greatly improving execution time. Mind there's no change
612
+ in behavior for arrays not considered _large_
613
+
614
+ `largeArraySize`'s default value is `20000`. Valid values for it are
615
+ integer-like values, such as:
616
+
617
+ - `20000`
618
+ - `2e4`
619
+ - `'20000'`
620
+ - `'2e4'` - _note this will be converted to `2`, not `20000`_
621
+ - `1.5` - _note this will be converted to `1`_
622
+
623
+ ##### Benchmarks
624
+
625
+ For reference, here goes some benchmarks for comparison over the three
626
+ mechanisms. Benchmarks conducted on an old machine.
627
+
628
+ - Machine: `ST1000LM024 HN-M 1TB HDD, Intel Core i7-3610QM @ 2.3GHz, 12GB RAM, 4C/8T`.
629
+ - Node.js `v16.13.1`
630
+
631
+ ```
632
+ JSON.stringify large array x 157 ops/sec ±0.73% (86 runs sampled)
633
+ fast-json-stringify large array default x 48.72 ops/sec ±4.92% (48 runs sampled)
634
+ fast-json-stringify large array json-stringify x 157 ops/sec ±0.76% (86 runs sampled)
635
+ compile-json-stringify large array x 175 ops/sec ±4.47% (79 runs sampled)
636
+ AJV Serialize large array x 58.76 ops/sec ±4.59% (60 runs sampled)
637
+ ```
638
+
585
639
  <a name="security"></a>
586
640
  ## Security notice
587
641
 
@@ -0,0 +1,114 @@
1
+ 'use strict'
2
+
3
+ const { spawn } = require('child_process')
4
+
5
+ const chalk = require('chalk')
6
+ const inquirer = require('inquirer')
7
+ const simpleGit = require('simple-git')
8
+
9
+ const git = simpleGit(process.cwd())
10
+
11
+ const COMMAND = 'npm run bench'
12
+ const DEFAULT_BRANCH = 'master'
13
+ const PERCENT_THRESHOLD = 5
14
+
15
+ async function selectBranchName (message, branches) {
16
+ const result = await inquirer.prompt([{
17
+ type: 'list',
18
+ name: 'branch',
19
+ choices: branches,
20
+ loop: false,
21
+ pageSize: 20,
22
+ message
23
+ }])
24
+ return result.branch
25
+ }
26
+
27
+ async function executeCommandOnBranch (command, branch) {
28
+ console.log(chalk.grey(`Checking out "${branch}"`))
29
+ await git.checkout(branch)
30
+
31
+ console.log(chalk.grey(`Execute "${command}"`))
32
+ const childProcess = spawn(command, { stdio: 'pipe', shell: true })
33
+
34
+ let result = ''
35
+ childProcess.stdout.on('data', (data) => {
36
+ process.stdout.write(data.toString())
37
+ result += data.toString()
38
+ })
39
+
40
+ await new Promise(resolve => childProcess.on('close', resolve))
41
+
42
+ console.log()
43
+
44
+ return parseBenchmarksStdout(result)
45
+ }
46
+
47
+ function parseBenchmarksStdout (text) {
48
+ const results = []
49
+
50
+ const lines = text.split('\n')
51
+ for (const line of lines) {
52
+ const match = /^(.+?)(\.*) x (.+) ops\/sec .*$/.exec(line)
53
+ if (match !== null) {
54
+ results.push({
55
+ name: match[1],
56
+ alignedName: match[1] + match[2],
57
+ result: parseInt(match[3].split(',').join(''))
58
+ })
59
+ }
60
+ }
61
+
62
+ return results
63
+ }
64
+
65
+ function compareResults (featureBranch, mainBranch) {
66
+ for (const { name, alignedName, result: mainBranchResult } of mainBranch) {
67
+ const featureBranchBenchmark = featureBranch.find(result => result.name === name)
68
+ if (featureBranchBenchmark) {
69
+ const featureBranchResult = featureBranchBenchmark.result
70
+ const percent = (featureBranchResult - mainBranchResult) * 100 / mainBranchResult
71
+ const roundedPercent = Math.round(percent * 100) / 100
72
+
73
+ const percentString = roundedPercent > 0 ? `+${roundedPercent}%` : `${roundedPercent}%`
74
+ const message = alignedName + percentString.padStart(7, '.')
75
+
76
+ if (roundedPercent > PERCENT_THRESHOLD) {
77
+ console.log(chalk.green(message))
78
+ } else if (roundedPercent < -PERCENT_THRESHOLD) {
79
+ console.log(chalk.red(message))
80
+ } else {
81
+ console.log(message)
82
+ }
83
+ }
84
+ }
85
+ }
86
+
87
+ (async function () {
88
+ const branches = await git.branch()
89
+ const currentBranch = branches.branches[branches.current]
90
+
91
+ let featureBranch = null
92
+ let mainBranch = null
93
+
94
+ if (process.argv[2] === '--ci') {
95
+ featureBranch = currentBranch.name
96
+ mainBranch = DEFAULT_BRANCH
97
+ } else {
98
+ featureBranch = await selectBranchName('Select the branch you want to compare (feature branch):', branches.all)
99
+ mainBranch = await selectBranchName('Select the branch you want to compare with (main branch):', branches.all)
100
+ }
101
+
102
+ try {
103
+ const featureBranchResult = await executeCommandOnBranch(COMMAND, featureBranch)
104
+ const mainBranchResult = await executeCommandOnBranch(COMMAND, mainBranch)
105
+ compareResults(featureBranchResult, mainBranchResult)
106
+ } catch (error) {
107
+ console.error('Switch to origin branch due to an error', error.message)
108
+ }
109
+
110
+ await git.checkout(currentBranch.commit)
111
+ await git.checkout(currentBranch.name)
112
+
113
+ console.log(chalk.gray(`Back to ${currentBranch.name} ${currentBranch.commit}`))
114
+ })()
@@ -3,6 +3,10 @@
3
3
  const benchmark = require('benchmark')
4
4
  const suite = new benchmark.Suite()
5
5
 
6
+ const STR_LEN = 1e4
7
+ const LARGE_ARRAY_SIZE = 2e4
8
+ const MULTI_ARRAY_LENGTH = 1e3
9
+
6
10
  const schema = {
7
11
  title: 'Example Schema',
8
12
  type: 'object',
@@ -89,7 +93,8 @@ const obj = {
89
93
 
90
94
  const date = new Date()
91
95
 
92
- const multiArray = []
96
+ const multiArray = new Array(MULTI_ARRAY_LENGTH)
97
+ const largeArray = new Array(LARGE_ARRAY_SIZE)
93
98
 
94
99
  const CJS = require('compile-json-stringify')
95
100
  const CJSStringify = CJS(schemaCJS)
@@ -97,9 +102,12 @@ const CJSStringifyArray = CJS(arraySchemaCJS)
97
102
  const CJSStringifyDate = CJS(dateFormatSchemaCJS)
98
103
  const CJSStringifyString = CJS({ type: 'string' })
99
104
 
100
- const FJS = require('.')
105
+ const FJS = require('..')
101
106
  const stringify = FJS(schema)
102
- const stringifyArray = FJS(arraySchema)
107
+ const stringifyArrayDefault = FJS(arraySchema)
108
+ const stringifyArrayJSONStringify = FJS(arraySchema, {
109
+ largeArrayMechanism: 'json-stringify'
110
+ })
103
111
  const stringifyDate = FJS(dateFormatSchema)
104
112
  const stringifyString = FJS({ type: 'string' })
105
113
  let str = ''
@@ -110,18 +118,48 @@ const ajvSerialize = ajv.compileSerializer(schemaAJVJTD)
110
118
  const ajvSerializeArray = ajv.compileSerializer(arraySchemaAJVJTD)
111
119
  const ajvSerializeString = ajv.compileSerializer({ type: 'string' })
112
120
 
121
+ const getRandomString = (length) => {
122
+ if (!Number.isInteger(length)) {
123
+ throw new Error('Expected integer length')
124
+ }
125
+
126
+ const validCharacters = 'abcdefghijklmnopqrstuvwxyz'
127
+ const nValidCharacters = 26
128
+
129
+ let result = ''
130
+ for (let i = 0; i < length; ++i) {
131
+ result += validCharacters[Math.floor(Math.random() * nValidCharacters)]
132
+ }
133
+
134
+ return result[0].toUpperCase() + result.slice(1)
135
+ }
136
+
113
137
  // eslint-disable-next-line
114
- for (var i = 0; i < 10000; i++) {
138
+ for (let i = 0; i < STR_LEN; i++) {
139
+ largeArray[i] = {
140
+ firstName: getRandomString(8),
141
+ lastName: getRandomString(6),
142
+ age: Math.ceil(Math.random() * 99)
143
+ }
144
+
115
145
  str += i
116
146
  if (i % 100 === 0) {
117
147
  str += '"'
118
148
  }
119
149
  }
120
150
 
151
+ for (let i = STR_LEN; i < LARGE_ARRAY_SIZE; ++i) {
152
+ largeArray[i] = {
153
+ firstName: getRandomString(10),
154
+ lastName: getRandomString(4),
155
+ age: Math.ceil(Math.random() * 99)
156
+ }
157
+ }
158
+
121
159
  Number(str)
122
160
 
123
- for (i = 0; i < 1000; i++) {
124
- multiArray.push(obj)
161
+ for (let i = 0; i < MULTI_ARRAY_LENGTH; i++) {
162
+ multiArray[i] = obj
125
163
  }
126
164
 
127
165
  suite.add('FJS creation', function () {
@@ -138,8 +176,12 @@ suite.add('JSON.stringify array', function () {
138
176
  JSON.stringify(multiArray)
139
177
  })
140
178
 
141
- suite.add('fast-json-stringify array', function () {
142
- stringifyArray(multiArray)
179
+ suite.add('fast-json-stringify array default', function () {
180
+ stringifyArrayDefault(multiArray)
181
+ })
182
+
183
+ suite.add('fast-json-stringify array json-stringify', function () {
184
+ stringifyArrayJSONStringify(multiArray)
143
185
  })
144
186
 
145
187
  suite.add('compile-json-stringify array', function () {
@@ -150,6 +192,26 @@ suite.add('AJV Serialize array', function () {
150
192
  ajvSerializeArray(multiArray)
151
193
  })
152
194
 
195
+ suite.add('JSON.stringify large array', function () {
196
+ JSON.stringify(largeArray)
197
+ })
198
+
199
+ suite.add('fast-json-stringify large array default', function () {
200
+ stringifyArrayDefault(largeArray)
201
+ })
202
+
203
+ suite.add('fast-json-stringify large array json-stringify', function () {
204
+ stringifyArrayJSONStringify(largeArray)
205
+ })
206
+
207
+ suite.add('compile-json-stringify large array', function () {
208
+ CJSStringifyArray(largeArray)
209
+ })
210
+
211
+ suite.add('AJV Serialize large array', function () {
212
+ ajvSerializeArray(largeArray)
213
+ })
214
+
153
215
  suite.add('JSON.stringify long string', function () {
154
216
  JSON.stringify(str)
155
217
  })
@@ -0,0 +1,21 @@
1
+ 'use strict'
2
+
3
+ const { workerData: benchmark, parentPort } = require('worker_threads')
4
+
5
+ const Benchmark = require('benchmark')
6
+ Benchmark.options.minSamples = 500
7
+
8
+ const suite = Benchmark.Suite()
9
+
10
+ const FJS = require('..')
11
+ const stringify = FJS(benchmark.schema)
12
+
13
+ suite
14
+ .add(benchmark.name, () => {
15
+ stringify(benchmark.input)
16
+ })
17
+ .on('cycle', (event) => {
18
+ parentPort.postMessage(String(event.target))
19
+ })
20
+ .on('complete', () => {})
21
+ .run()