Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,16 @@ jobs:
os:
- ubuntu-latest
node_version:
- 16
- 18
- 20
- 22
- 24
- 26
name: Running tests with Node ${{ matrix.node_version }} on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: ${{ matrix.node_version }}
- run: |
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ npm-debug.log
yarn.lock
coverage
.nyc_output
.tap/
.vscode
package-lock.json
34 changes: 34 additions & 0 deletions benchmark-utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
'use strict'

const assert = require('node:assert/strict')
const { cpus } = require('node:os')

const { Suite } = require('bench-node')

module.exports = function runBenchmarks (cases, seconds = 2) {
let length = 0
const suite = new Suite({
repeatSuite: 7,
reporter (results) {
console.log(`${cpus()[0].model}; Node ${process.version}; V8 ${process.versions.v8}`)
for (const result of results) {
const runs = result.opsSecPerRun
const mean = runs.reduce((total, value) => total + value, 0) / runs.length
const deviation = Math.sqrt(runs.reduce((total, value) => total + (value - mean) ** 2, 0) / (runs.length - 1))
const sorted = [...runs].sort((left, right) => left - right).slice(1, -1)
const rate = sorted.reduce((total, value) => total + value, 0) / sorted.length
console.log(`${result.name.padEnd(34)} ${Math.round(rate).toLocaleString('en-US')} ops/sec (7 runs, CV ${(deviation / mean * 100).toFixed(2)}%)`)
assert.ok(deviation / mean < 0.03, `${result.name}: excessive variance; rates: ${runs.map(Math.round).join(', ')}; rerun on a quiet host`)
}
console.log('Output checksum:', length)
}
})
for (const [name, fn] of cases) {
const expected = fn()
assert.equal(typeof expected, 'string')
for (let iteration = 0; iteration < 50000; iteration++) length ^= fn().length
assert.equal(fn(), expected)
suite.add(name, { minTime: 0.05, maxTime: seconds }, () => { length ^= fn().length })
}
return suite.run()
}
104 changes: 17 additions & 87 deletions benchmark.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
'use strict'

const Benchmark = require('bench-node')
const suite = new Benchmark.Suite()
const runBenchmarks = require('./benchmark-utils')
const stringify = require('.').configure({ deterministic: true })

const array = Array.from({ length: 10 }, (_, i) => i)
Expand All @@ -21,88 +20,19 @@ deepCirc.deep.deep.circ = deepCirc
deepCirc.deep.circ = deepCirc
deepCirc.array = array

// One arg "simple"
suite.add('simple: simple object', function () {
stringify(obj)
})
suite.add('simple: circular ', function () {
stringify(circ)
})
suite.add('simple: deep ', function () {
stringify(deep)
})
suite.add('simple: deep circular', function () {
stringify(deepCirc)
})

// Two args "replacer"
suite.add('\nreplacer: simple object', function () {
stringify(obj, (_, v) => v)
})
suite.add('replacer: circular ', function () {
stringify(circ, (_, v) => v)
})
suite.add('replacer: deep ', function () {
stringify(deep, (_, v) => v)
})
suite.add('replacer: deep circular', function () {
stringify(deepCirc, (_, v) => v)
})

// Two args "array"
suite.add('\narray: simple object', function () {
stringify(obj, ['array'])
})
suite.add('array: circular ', function () {
stringify(circ, ['array'])
})
suite.add('array: deep ', function () {
stringify(deep, ['array'])
})
suite.add('array: deep circular', function () {
stringify(deepCirc, ['array'])
})

// Three args "full replacer"
suite.add('\nfull replacer: simple object', function () {
stringify(obj, (_, v) => v, 2)
})
suite.add('full replacer: circular ', function () {
stringify(circ, (_, v) => v, 2)
})
suite.add('full replacer: deep ', function () {
stringify(deep, (_, v) => v, 2)
})
suite.add('full replacer: deep circular', function () {
stringify(deepCirc, (_, v) => v, 2)
})

// Three args "full array"
suite.add('\nfull array: simple object', function () {
stringify(obj, ['array'], 2)
})
suite.add('full array: circular ', function () {
stringify(circ, ['array'], 2)
})
suite.add('full array: deep ', function () {
stringify(deep, ['array'], 2)
})
suite.add('full array: deep circular', function () {
stringify(deepCirc, ['array'], 2)
})

// Three args "indentation only"
suite.add('\nindentation: simple object', function () {
stringify(obj, null, 2)
})
suite.add('indentation: circular ', function () {
stringify(circ, null, 2)
})
suite.add('indentation: deep ', function () {
stringify(deep, null, 2)
})
suite.add('indentation: deep circular', function () {
stringify(deepCirc, null, 2)
})

suite.run()
const cases = []
const replacer = (_, value) => value
for (const [mode, args] of [
['simple', []],
['replacer', [replacer]],
['array', [['array']]],
['full replacer', [replacer, 2]],
['full array', [['array'], 2]],
['indentation', [null, 2]]
]) {
for (const [name, value] of [['simple object', obj], ['circular', circ], ['deep', deep], ['deep circular', deepCirc]]) {
cases.push([`${mode}: ${name}`, () => stringify(value, ...args)])
}
}

runBenchmarks(cases)
81 changes: 67 additions & 14 deletions compare.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
'use strict'

const Benchmark = require('bench-node')
const suite = new Benchmark.Suite()
const assert = require('node:assert/strict')

const runBenchmarks = require('./benchmark-utils')
const testData = require('./test.json')

const stringifyPackages = {
Expand All @@ -16,19 +17,71 @@ const stringifyPackages = {
'safe-stable-stringify': require('.')
}

for (const name in stringifyPackages) {
let fn
if (typeof stringifyPackages[name] === 'function') {
fn = stringifyPackages[name]
} else if (typeof stringifyPackages[name] === 'string') {
fn = require(name)[stringifyPackages[name]]
} else {
fn = require(name)
for (const name of Object.keys(stringifyPackages)) {
if (typeof stringifyPackages[name] === 'string') {
stringifyPackages[name] = require(name)[stringifyPackages[name]]
} else if (stringifyPackages[name] === true) {
stringifyPackages[name] = require(name)
}
}

const telemetry = {
device: { id: 'weather-station-17', firmware: '3.2.1', online: true },
recordedAt: new Date('2026-01-15T12:30:00.000Z'),
payload: Uint8Array.from({ length: 128 }, (_, index) => (index * 17) % 256),
histogram: Uint32Array.from({ length: 32 }, (_, index) => index * index * 13),
temperatures: Float32Array.from({ length: 24 }, (_, index) => 18 + index / 8),
pressure: new Float64Array([1013.25, 1013.5, NaN, 1013.75]),
signature: Buffer.from('e7c93f142ae4907e8fae3cb589e0f87a', 'hex'),
location: { latitude: 52.52, longitude: 13.405 },
alarms: [false, true, false]
}
const counters = {
...telemetry,
sequence: 18446744073709551001n,
timestampNs: 1768480200000000000n,
totals: new BigUint64Array([9007199254740993n, 18446744073709551001n]),
deltas: new BigInt64Array([-9007199254740993n, 33n, 0n])
}
const bigintReplacer = (_, value) => typeof value === 'bigint' ? String(value) : value
const bigintOptions = { replacer: bigintReplacer }
const deterministicOptions = { replacer: bigintReplacer, cycles: true }
const bigintPackages = {
'json-stable-stringify': value => stringifyPackages['json-stable-stringify'](value, bigintOptions),
'json-stringify-deterministic': value => stringifyPackages['json-stringify-deterministic'](value, deterministicOptions),
'fast-safe-stringify': value => stringifyPackages['fast-safe-stringify'](value, bigintReplacer),
'safe-stable-stringify': require('.').configure({ bigint: 'string' })
}

async function compare (title, value, implementations) {
console.log(title)
const expected = JSON.parse(JSON.stringify(value, bigintReplacer))
const cases = []
for (const name of Object.keys(stringifyPackages)) {
const fn = implementations[name]
if (fn === undefined) {
let output
try {
output = stringifyPackages[name](value)
} catch (error) {
assert.ok(error instanceof TypeError)
}
if (output !== undefined) assert.notDeepEqual(JSON.parse(output), expected)
console.log(`${name}: BigInt is not supported`)
continue
}
const output = fn(value)
assert.deepEqual(JSON.parse(output), expected)
if (value === testData) assert.equal(output, require('.')(value))
cases.push([name, () => fn(value)])
}
await runBenchmarks(cases, 2)
}

suite.add(name, function () {
fn(testData)
})
async function main () {
await compare('JSON-compatible object', testData, stringifyPackages)
await compare('Typed arrays, Date and Buffer', telemetry, stringifyPackages)
await compare('Typed arrays, Date, Buffer and BigInt (decimal strings)', counters, bigintPackages)
}

suite.run()
main()
Loading
Loading