-
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: 🎸 add json-size implementation
- Loading branch information
Showing
11 changed files
with
526 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
# `json-size` | ||
|
||
This library implements methods to calculate the size of JSON objects. | ||
It calculates the size of bytes necessary to store the final serialized JSON | ||
in UTF-8 encoding. | ||
|
||
## Usage | ||
|
||
```ts | ||
import {jsonSize} from 'json-joy/{lib,es6}/json-size'; | ||
|
||
jsonSize({1: 2, foo: 'bar'}); // 19 | ||
``` | ||
|
||
## Reference | ||
|
||
- `jsonSize` — calculates exact JSON size, as `JSON.stringify()` would return. | ||
- `jsonSizeApprox` — a faster version, which uses string nominal length for calculation. | ||
- `jsonSizeFast` — the fastest version, which uses nominal values for all JSON types. See | ||
source code for description. | ||
- `msgpackSizeFast` — same as `jsonSizeFast`, but for MessagePack values. In addition | ||
to regular JSON values it also supports binary data (by `Buffer` or `Uint8Array`), | ||
`JsonPackExtension`, and `JsonPackValue`. | ||
|
||
## Performance | ||
|
||
In most cases `json-size` will be faster than `JSON.stringify`. | ||
|
||
``` | ||
node benchmarks/json-size.js | ||
json-joy/json-size jsonSize() x 377,980 ops/sec ±0.12% (100 runs sampled), 2646 ns/op | ||
json-joy/json-size jsonSizeApprox() x 377,841 ops/sec ±0.09% (98 runs sampled), 2647 ns/op | ||
json-joy/json-size jsonSizeFast() x 2,229,344 ops/sec ±0.30% (101 runs sampled), 449 ns/op | ||
json-joy/json-size msgpackSizeFast() x 1,260,284 ops/sec ±0.10% (96 runs sampled), 793 ns/op | ||
JSON.stringify x 349,696 ops/sec ±0.08% (100 runs sampled), 2860 ns/op | ||
JSON.stringify + utf8Count x 182,977 ops/sec ±0.10% (100 runs sampled), 5465 ns/op | ||
Fastest is json-joy/json-size jsonSizeFast() | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
/* tslint:disable no-console */ | ||
|
||
// npx ts-node src/json-size/__bench__/json-size.ts | ||
|
||
import * as Benchmark from 'benchmark'; | ||
import {utf8Size} from '@jsonjoy.com/util/lib/strings/utf8'; | ||
import {jsonSize, jsonSizeApprox} from '../json'; | ||
import {jsonSizeFast} from '../jsonSizeFast'; | ||
import {msgpackSizeFast} from '../msgpackSizeFast'; | ||
|
||
const json = [ | ||
{op: 'add', path: '/foo/baz', value: 666}, | ||
{op: 'add', path: '/foo/bx', value: 666}, | ||
{op: 'add', path: '/asdf', value: 'asdfadf asdf'}, | ||
{op: 'move', path: '/arr/0', from: '/arr/1'}, | ||
{op: 'replace', path: '/foo/baz', value: 'lorem ipsum'}, | ||
{ | ||
op: 'add', | ||
path: '/docs/latest', | ||
value: { | ||
name: 'blog post', | ||
json: { | ||
id: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx', | ||
longString: | ||
'lorem ipsum dolorem, alamorem colomorem, ipsum pipsum, lorem ipsum dolorem, alamorem colomorem, ipsum pipsum, lorem ipsum dolorem, alamorem colomorem, ipsum pipsum, lorem ipsum dolorem, alamorem colomorem, ipsum pipsum, lorem ipsum dolorem, alamorem colomorem, ipsum pipsum', | ||
author: { | ||
name: 'John 💪', | ||
handle: '@johny', | ||
}, | ||
lastSeen: -12345, | ||
tags: [null, 'Sports 🏀', 'Personal', 'Travel'], | ||
pins: [ | ||
{ | ||
id: 1239494, | ||
}, | ||
], | ||
marks: [ | ||
{ | ||
x: 1, | ||
y: 1.234545, | ||
w: 0.23494, | ||
h: 0, | ||
}, | ||
], | ||
hasRetweets: false, | ||
approved: true, | ||
'👍': 33, | ||
}, | ||
}, | ||
}, | ||
]; | ||
|
||
const suite = new Benchmark.Suite(); | ||
|
||
suite | ||
.add(`json-joy/json-size jsonSize()`, () => { | ||
jsonSize(json); | ||
}) | ||
.add(`json-joy/json-size jsonSizeApprox()`, () => { | ||
jsonSizeApprox(json); | ||
}) | ||
.add(`json-joy/json-size jsonSizeFast()`, () => { | ||
jsonSizeFast(json); | ||
}) | ||
.add(`json-joy/json-size msgpackSizeFast()`, () => { | ||
msgpackSizeFast(json); | ||
}) | ||
.add(`JSON.stringify`, () => { | ||
JSON.stringify(json).length; | ||
}) | ||
.add(`JSON.stringify + utf8Count`, () => { | ||
utf8Size(JSON.stringify(json)); | ||
}) | ||
.on('cycle', (event: any) => { | ||
console.log(String(event.target) + `, ${Math.round(1000000000 / event.target.hz)} ns/op`); | ||
}) | ||
.on('complete', () => { | ||
console.log('Fastest is ' + suite.filter('fastest').map('name')); | ||
}) | ||
.run(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
import {jsonSize} from '..'; | ||
import {RandomJson} from '../../json-random/RandomJson'; | ||
import {utf8Size} from '../../strings/utf8'; | ||
|
||
const random = new RandomJson(); | ||
const iterations = 100; | ||
|
||
for (let i = 0; i < iterations; i++) { | ||
test(`calculates json size - ${i + 1}`, () => { | ||
const json = random.create(); | ||
// console.log(json); | ||
const size1 = jsonSize(json); | ||
const size2 = utf8Size(JSON.stringify(json)); | ||
expect(size1).toBe(size2); | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
import {jsonSize, jsonSizeApprox} from '../json'; | ||
import {testJsonSize} from './testJsonSize'; | ||
|
||
describe('jsonSize', () => { | ||
testJsonSize(jsonSize); | ||
}); | ||
|
||
describe('jsonSizeApprox', () => { | ||
testJsonSize(jsonSizeApprox, {simpleStringsOnly: true}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
import {jsonSizeFast} from '../jsonSizeFast'; | ||
|
||
test('computes size of single values', () => { | ||
expect(jsonSizeFast(null)).toBe(1); | ||
expect(jsonSizeFast(true)).toBe(1); | ||
expect(jsonSizeFast(false)).toBe(1); | ||
expect(jsonSizeFast(1)).toBe(9); | ||
expect(jsonSizeFast(1.1)).toBe(9); | ||
expect(jsonSizeFast('123')).toBe(7); | ||
expect(jsonSizeFast('')).toBe(4); | ||
expect(jsonSizeFast('A')).toBe(5); | ||
expect(jsonSizeFast([])).toBe(2); | ||
expect(jsonSizeFast({})).toBe(2); | ||
}); | ||
|
||
test('computes size complex object', () => { | ||
// prettier-ignore | ||
const json = { // 2 | ||
a: 1, // 2 + 1 + 9 | ||
b: true, // 2 + 1 + 1 | ||
c: false, // 2 + 1 + 1 | ||
d: null, // 2 + 1 + 1 | ||
'e.e': 2.2, // 2 + 3 + 9 | ||
f: '', // 2 + 1 + 4 + 0 | ||
g: 'asdf', // 2 + 1 + 4 + 4 | ||
h: {}, // 2 + 1 + 2 | ||
i: [ // 2 + 1 + 2 | ||
1, // 9 | ||
true, // 1 | ||
false, // 1 | ||
null, // 1 | ||
2.2, // 9 | ||
'', // 4 + 0 | ||
'asdf', // 4 + 4 | ||
{}, // 2 | ||
], | ||
}; | ||
const size = jsonSizeFast(json); | ||
|
||
// prettier-ignore | ||
expect(size).toBe( | ||
2 + | ||
2 + 1 + 9 + | ||
2 + 1 + 1 + | ||
2 + 1 + 1 + | ||
2 + 1 + 1 + | ||
2 + 3 + 9 + | ||
2 + 1 + 4 + 0 + | ||
2 + 1 + 4 + 4 + | ||
2 + 1 + 2 + | ||
2 + 1 + 2 + | ||
9 + | ||
1 + | ||
1 + | ||
1 + | ||
9 + | ||
4 + 0 + | ||
4 + 4 + | ||
2 | ||
); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
import {maxEncodingCapacity} from '../maxEncodingCapacity'; | ||
|
||
test('computes size of single values', () => { | ||
expect(maxEncodingCapacity(null)).toBe(4); | ||
expect(maxEncodingCapacity(true)).toBe(5); | ||
expect(maxEncodingCapacity(false)).toBe(5); | ||
expect(maxEncodingCapacity(1)).toBe(22); | ||
expect(maxEncodingCapacity(1.1)).toBe(22); | ||
expect(maxEncodingCapacity('123')).toBe(20); | ||
expect(maxEncodingCapacity('')).toBe(5); | ||
expect(maxEncodingCapacity('A')).toBe(10); | ||
expect(maxEncodingCapacity([])).toBe(5); | ||
expect(maxEncodingCapacity({})).toBe(5); | ||
expect(maxEncodingCapacity({foo: 1})).toBe(49); | ||
expect(maxEncodingCapacity({foo: [1]})).toBe(55); | ||
}); | ||
|
||
test('a larger value', () => { | ||
expect( | ||
maxEncodingCapacity({ | ||
name: 'cooking receipt', | ||
json: { | ||
id: '0001', | ||
type: 'donut', | ||
name: 'Cake', | ||
ppu: 0.55, | ||
batters: { | ||
batter: [ | ||
{id: '1001', type: 'Regular'}, | ||
{id: '1002', type: 'Chocolate'}, | ||
{id: '1003', type: 'Blueberry'}, | ||
{id: '1004', type: "Devil's Food"}, | ||
], | ||
}, | ||
topping: [ | ||
{id: '5001', type: 'None'}, | ||
{id: '5002', type: 'Glazed'}, | ||
{id: '5005', type: 'Sugar'}, | ||
{id: '5007', type: 'Powdered Sugar'}, | ||
{id: '5006', type: 'Chocolate with Sprinkles'}, | ||
{id: '5003', type: 'Chocolate'}, | ||
{id: '5004', type: 'Maple'}, | ||
], | ||
}, | ||
}), | ||
).toBe(1875); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
import {utf8Size} from '../../strings/utf8'; | ||
|
||
export const testJsonSize = ( | ||
jsonSize: (val: unknown) => number, | ||
{simpleStringsOnly = false}: {simpleStringsOnly?: boolean} = {}, | ||
) => { | ||
test('calculates null size', () => { | ||
expect(jsonSize(null)).toBe(4); | ||
}); | ||
|
||
test('calculates boolean sizes', () => { | ||
expect(jsonSize(true)).toBe(4); | ||
expect(jsonSize(false)).toBe(5); | ||
}); | ||
|
||
test('calculates number sizes', () => { | ||
expect(jsonSize(1)).toBe(1); | ||
expect(jsonSize(1.1)).toBe(3); | ||
expect(jsonSize(0)).toBe(1); | ||
expect(jsonSize(1.123)).toBe(5); | ||
expect(jsonSize(-1.123)).toBe(6); | ||
}); | ||
|
||
if (!simpleStringsOnly) { | ||
test('calculates string sizes', () => { | ||
expect(jsonSize('')).toBe(2); | ||
expect(jsonSize('a')).toBe(3); | ||
expect(jsonSize('abc')).toBe(5); | ||
expect(jsonSize('👨👩👦👦')).toBe(27); | ||
expect(jsonSize('büro')).toBe(7); | ||
expect(jsonSize('office')).toBe(8); | ||
}); | ||
} | ||
|
||
if (!simpleStringsOnly) { | ||
test('calculates string sizes with escaped characters', () => { | ||
expect(jsonSize('\\')).toBe(4); | ||
expect(jsonSize('"')).toBe(4); | ||
expect(jsonSize('\b')).toBe(4); | ||
expect(jsonSize('\f')).toBe(4); | ||
expect(jsonSize('\n')).toBe(4); | ||
expect(jsonSize('\r')).toBe(4); | ||
expect(jsonSize('\t')).toBe(4); | ||
}); | ||
} | ||
|
||
test('calculates array sizes', () => { | ||
expect(jsonSize([])).toBe(2); | ||
expect(jsonSize([1])).toBe(3); | ||
expect(jsonSize([1, 2, 3])).toBe(7); | ||
expect(jsonSize([1, 'büro', 3])).toBe(13); | ||
}); | ||
|
||
test('calculates object sizes', () => { | ||
expect(jsonSize({})).toBe(2); | ||
expect(jsonSize({a: 1})).toBe(2 + 3 + 1 + 1); | ||
expect(jsonSize({1: 2, foo: 'bar'})).toBe(2 + 3 + 1 + 1 + 1 + 5 + 1 + 5); | ||
}); | ||
|
||
test('calculates size of array of length 2 that begins with empty string', () => { | ||
const json = ['', -1]; | ||
const size1 = jsonSize(json); | ||
const size2 = utf8Size(JSON.stringify(json)); | ||
expect(size1).toBe(size2); | ||
}); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export * from './json'; | ||
export * from './jsonSizeFast'; | ||
export * from './maxEncodingCapacity'; |
Oops, something went wrong.