Skip to content

Commit

Permalink
feat: 🎸 add code
Browse files Browse the repository at this point in the history
  • Loading branch information
streamich committed Apr 9, 2024
1 parent a0f7123 commit 429b52d
Show file tree
Hide file tree
Showing 27 changed files with 713 additions and 14 deletions.
28 changes: 14 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,26 +42,26 @@ than 40 chars, it falls back to Node `Buffer` implementation, if available.
Encoding:

```
node src/__bench__/base64/encode.js
@jsonjoy.com/base64 toBase64(uint8) x 1,257,419 ops/sec ±1.19% (93 runs sampled), 795 ns/op
@jsonjoy.com/base64 createToBase64()(uint8) x 868,953 ops/sec ±0.96% (96 runs sampled), 1151 ns/op
js-base64 x 974,991 ops/sec ±0.73% (94 runs sampled), 1026 ns/op
fast-base64-encode x 428,545 ops/sec ±1.67% (96 runs sampled), 2333 ns/op
base64-js x 295,165 ops/sec ±1.59% (98 runs sampled), 3388 ns/op
Buffer.from(uint8).toString('base64'); x 973,173 ops/sec ±0.65% (95 runs sampled), 1028 ns/op
Fastest is json-pack/util/base64 toBase64(uint8)
node src/__bench__/encode.js
util/base64 toBase64(uint8) x 1,531,283 ops/sec ±0.30% (92 runs sampled), 653 ns/op
util/base64 createToBase64()(uint8) x 946,364 ops/sec ±0.76% (100 runs sampled), 1057 ns/op
js-base64 x 1,103,190 ops/sec ±1.27% (96 runs sampled), 906 ns/op
fast-base64-encode x 500,225 ops/sec ±0.64% (96 runs sampled), 1999 ns/op
base64-js x 328,368 ops/sec ±0.25% (95 runs sampled), 3045 ns/op
Buffer.from(uint8).toString('base64'); x 1,099,420 ops/sec ±0.20% (100 runs sampled), 910 ns/op
Fastest is util/base64 toBase64(uint8)
```

Decoding:

```
node src/__bench__/decode.js
@jsonjoy.com/base64 fromBase64(str) x 602,268 ops/sec ±1.09% (88 runs sampled), 1660 ns/op
@jsonjoy.com/base64 createFromBase64()(str) x 392,345 ops/sec ±0.96% (91 runs sampled), 2549 ns/op
Buffer.from(str, 'base64') x 498,609 ops/sec ±1.66% (93 runs sampled), 2006 ns/op
base64-js x 439,246 ops/sec ±0.94% (89 runs sampled), 2277 ns/op
js-base64 x 151,694 ops/sec ±0.51% (99 runs sampled), 6592 ns/op
Fastest is json-pack/util/base64 fromBase64(str)
@jsonjoy.com/base64 fromBase64(str) x 756,989 ops/sec ±0.46% (97 runs sampled), 1321 ns/op
@jsonjoy.com/base64 createFromBase64()(str) x 475,591 ops/sec ±0.37% (96 runs sampled), 2103 ns/op
Buffer.from(str, 'base64') x 545,012 ops/sec ±0.33% (101 runs sampled), 1835 ns/op
base64-js x 487,015 ops/sec ±1.19% (94 runs sampled), 2053 ns/op
js-base64 x 173,049 ops/sec ±0.20% (99 runs sampled), 5779 ns/op
Fastest is @jsonjoy.com/base64 fromBase64(str)
```


Expand Down
77 changes: 77 additions & 0 deletions src/__bench__/decode.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
const Benchmark = require('benchmark');
const toBase64 = require('../../lib').toBase64;
const {bufferToUint8Array} = require('../../lib/util/buffers/bufferToUint8Array');
const {fromBase64, createFromBase64} = require('../../lib');
const {toByteArray} = require('base64-js');
const {decode: decodeJsBase64} = require('js-base64');

const fromBase642 = createFromBase64();

const generateBlob = (length) => {
const uint8 = new Uint8Array(length);
for (let i = 0; i < length; i++) {
uint8[i] = Math.floor(Math.random() * 256);
}
return uint8;
};

const str4 = toBase64(generateBlob(4));
const str8 = toBase64(generateBlob(8));
const str16 = toBase64(generateBlob(16));
const str24 = toBase64(generateBlob(24));
const str32 = toBase64(generateBlob(32));
const str64 = toBase64(generateBlob(64));
const str128 = toBase64(generateBlob(128));
const str256 = toBase64(generateBlob(256));

const suite = new Benchmark.Suite();

const encoders = [
{
name: `@jsonjoy.com/base64 fromBase64(str)`,
decode: (str) => fromBase64(str),
},
{
name: `@jsonjoy.com/base64 createFromBase64()(str)`,
decode: (str) => fromBase642(str),
},
{
name: `Buffer.from(str, 'base64')`,
decode: (str) => bufferToUint8Array(Buffer.from(str, 'base64')),
},
{
name: `base64-js`,
decode: (str) => toByteArray(str),
},
{
name: `js-base64`,
decode: (str) => decodeJsBase64(str),
},
];

for (const encoder of encoders) {
// Warm up
for (let i = 0; i < 100000; i++) {
encoder.decode(str8);
encoder.decode(str256);
}
suite.add(encoder.name, () => {
encoder.decode(str4);
encoder.decode(str8);
encoder.decode(str16);
encoder.decode(str24);
encoder.decode(str32);
encoder.decode(str64);
encoder.decode(str128);
encoder.decode(str256);
});
}

suite
.on('cycle', function (event) {
console.log(String(event.target) + `, ${Math.round(1000000000 / event.target.hz)} ns/op`);
})
.on('complete', function () {
console.log('Fastest is ' + this.filter('fastest').map('name'));
})
.run();
98 changes: 98 additions & 0 deletions src/__bench__/encode.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
const Benchmark = require('benchmark');
const {toBase64, createToBase64} = require('../../lib');
const {fromByteArray} = require('base64-js');
const {encode: encodeJsBase64} = require('js-base64');

const toBase64Native = createToBase64();

const generateBlob = (length) => {
const uint8 = new Uint8Array(length);
for (let i = 0; i < length; i++) {
uint8[i] = Math.floor(Math.random() * 256);
}
return uint8;
};

const arr8 = generateBlob(9);
const arr16 = generateBlob(17);
const arr32 = generateBlob(33);
const arr64 = generateBlob(65);
const arr128 = generateBlob(127);
const arr256 = generateBlob(257);
const arr512 = generateBlob(513);
const arr1024 = generateBlob(1025);

// fast-base64-encode
const table = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'.split('');
const fastBase64Encode = (source) => {
let out = '';
let tmp;
const length = source.byteLength;
const extraLength = length % 3;
const baseLength = length - extraLength;
for (let i = 0; i < baseLength; i += 3) {
tmp = ((source[i] & 0xff) << 16) | ((source[i + 1] & 0xff) << 8) | (source[i + 2] & 0xff);
out += table[(tmp >> 18) & 0x3f] + table[(tmp >> 12) & 0x3f] + table[(tmp >> 6) & 0x3f] + table[tmp & 0x3f];
}
if (extraLength) {
if (extraLength === 1) {
tmp = source[baseLength] & 0xff;
out += table[tmp >> 2] + table[(tmp << 4) & 0x3f] + '==';
} else {
tmp = ((source[baseLength] & 0xff) << 8) | (source[baseLength + 1] & 0xff);
out += table[tmp >> 10] + table[(tmp >> 4) & 0x3f] + table[(tmp << 2) & 0x3f] + '=';
}
}
return out;
};

const suite = new Benchmark.Suite();

const encoders = [
{
name: `@jsonjoy.com/base64 toBase64(uint8)`,
encode: (uint8) => toBase64(uint8),
},
{
name: `@jsonjoy.com/base64 createToBase64()(uint8)`,
encode: (uint8) => toBase64Native(uint8, uint8.length),
},
{
name: `js-base64`,
encode: (uint8) => encodeJsBase64(uint8),
},
{
name: `fast-base64-encode`,
encode: (uint8) => fastBase64Encode(uint8),
},
{
name: `base64-js`,
encode: (uint8) => fromByteArray(uint8),
},
{
name: `Buffer.from(uint8).toString('base64');`,
encode: (uint8) => Buffer.from(uint8).toString('base64'),
},
];

for (const encoder of encoders) {
suite.add(encoder.name, () => {
encoder.encode(arr8);
encoder.encode(arr16);
encoder.encode(arr32);
encoder.encode(arr64);
encoder.encode(arr128);
// encoder.encode(arr256);
// encoder.encode(arr512);
// encoder.encode(arr1024);
});
}

suite
.on('cycle', function (event) {
console.log(String(event.target) + `, ${Math.round(1000000000 / event.target.hz)} ns/op`);
})
.on('complete', function () {
console.log('Fastest is ' + this.filter('fastest').map('name'));
})
.run();
20 changes: 20 additions & 0 deletions src/__tests__/decode-base64url.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import {toBase64} from '../toBase64';
import {fromBase64Url} from '../fromBase64Url';

const generateBlob = (): Uint8Array => {
const length = Math.floor(Math.random() * 100);
const uint8 = new Uint8Array(length);
for (let i = 0; i < length; i++) {
uint8[i] = Math.floor(Math.random() * 256);
}
return uint8;
};

test('works', () => {
for (let i = 0; i < 100; i++) {
const blob = generateBlob();
const encoded = toBase64(blob).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
const decoded2 = fromBase64Url(encoded);
expect(decoded2).toEqual(blob);
}
});
31 changes: 31 additions & 0 deletions src/__tests__/decode-bin.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import {toBase64Bin} from '../toBase64Bin';
import {fromBase64Bin} from '../fromBase64Bin';

const generateBlob = (): Uint8Array => {
const length = Math.floor(Math.random() * 100);
const uint8 = new Uint8Array(length);
for (let i = 0; i < length; i++) {
uint8[i] = Math.floor(Math.random() * 256);
}
return uint8;
};

test('works', () => {
for (let i = 0; i < 100; i++) {
const blob = generateBlob();
const dest = new Uint8Array(blob.length * 4);
const length = toBase64Bin(blob, 0, blob.length, new DataView(dest.buffer), 0);
const encoded = dest.subarray(0, length);
const view = new DataView(encoded.buffer);
const decoded = fromBase64Bin(view, 0, encoded.length);
let padding = 0;
if (encoded.length > 0 && view.getUint8(encoded.length - 1) === 0x3d) padding++;
if (encoded.length > 1 && view.getUint8(encoded.length - 2) === 0x3d) padding++;
const decoded2 = fromBase64Bin(view, 0, encoded.length - padding);
// console.log('blob', blob);
// console.log('encoded', encoded);
// console.log('decoded', decoded);
expect(decoded).toEqual(blob);
expect(decoded2).toEqual(blob);
}
});
33 changes: 33 additions & 0 deletions src/__tests__/decode.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import {toBase64} from '../toBase64';
import {fromBase64} from '../fromBase64';
import {createFromBase64} from '../createFromBase64';

const fromBase64_2 = createFromBase64();

const generateBlob = (): Uint8Array => {
const length = Math.floor(Math.random() * 100);
const uint8 = new Uint8Array(length);
for (let i = 0; i < length; i++) {
uint8[i] = Math.floor(Math.random() * 256);
}
return uint8;
};

test('works', () => {
for (let i = 0; i < 100; i++) {
const blob = generateBlob();
const encoded = toBase64(blob);
const decoded1 = fromBase64_2(encoded);
const decoded2 = fromBase64(encoded);
expect(decoded1).toEqual(blob);
expect(decoded2).toEqual(blob);
}
});

test('handles invalid values', () => {
for (let i = 0; i < 100; i++) {
const blob = generateBlob();
const encoded = toBase64(blob);
expect(() => fromBase64_2(encoded + '!!!!')).toThrowError(new Error('INVALID_BASE64_STRING'));
}
});
23 changes: 23 additions & 0 deletions src/__tests__/encode-base64url.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import {toBase64Url} from '../toBase64Url';

const generateBlob = (): Uint8Array => {
const length = Math.floor(Math.random() * 100) + 1;
const uint8 = new Uint8Array(length);
for (let i = 0; i < length; i++) {
uint8[i] = Math.floor(Math.random() * 256);
}
return uint8;
};

test('works', () => {
for (let i = 0; i < 100; i++) {
const blob = generateBlob();
const expected = Buffer.from(blob).toString('base64');
const base64url = toBase64Url(blob, blob.length);
let encoded = base64url.replace(/-/g, '+').replace(/_/g, '/');
const mod = encoded.length % 4;
if (mod === 2) encoded += '==';
else if (mod === 3) encoded += '=';
expect(encoded).toEqual(expected);
}
});
38 changes: 38 additions & 0 deletions src/__tests__/encode-bin.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import {toBase64} from '../toBase64';
import {createToBase64Bin} from '../createToBase64Bin';
import {createToBase64BinUint8} from '../createToBase64BinUint8';
import {bufferToUint8Array} from '../util/buffers/bufferToUint8Array';
import {copy} from '../util/buffers/copy';

const encode = createToBase64Bin('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/', '=');
const encodeUint8 = createToBase64BinUint8('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/', '=');
const encodeNoPadding = createToBase64Bin('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/');

const generateBlob = (): Uint8Array => {
const length = Math.floor(Math.random() * 100) + 1;
const uint8 = new Uint8Array(length);
for (let i = 0; i < length; i++) {
uint8[i] = Math.floor(Math.random() * 256);
}
return uint8;
};

test('works', () => {
for (let i = 0; i < 100; i++) {
const blob = generateBlob();
const result = bufferToUint8Array(Buffer.from(toBase64(blob)));
const binWithBuffer = new Uint8Array(result.length + 3);
encode(blob, 0, blob.length, new DataView(binWithBuffer.buffer), 3);
const dupe = copy(blob);
encodeNoPadding(blob, 0, blob.length, new DataView(binWithBuffer.buffer), 3);
expect(dupe).toEqual(blob);
const dupe2 = copy(blob);
encodeUint8(blob, 0, blob.length, binWithBuffer, 3);
expect(dupe2).toEqual(blob);
const encoded = binWithBuffer.subarray(3);
// console.log(result);
// console.log(binWithBuffer);
// console.log(encoded);
expect(result).toEqual(encoded);
}
});
24 changes: 24 additions & 0 deletions src/__tests__/encode.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import {toBase64} from '../toBase64';
import {createToBase64} from '../createToBase64';

const encode2 = createToBase64();

const generateBlob = (): Uint8Array => {
const length = Math.floor(Math.random() * 100) + 1;
const uint8 = new Uint8Array(length);
for (let i = 0; i < length; i++) {
uint8[i] = Math.floor(Math.random() * 256);
}
return uint8;
};

test('works', () => {
for (let i = 0; i < 100; i++) {
const blob = generateBlob();
const result = toBase64(blob);
const result2 = encode2(blob, blob.byteLength);
const expected = Buffer.from(blob).toString('base64');
expect(result).toBe(expected);
expect(result2).toBe(expected);
}
});
2 changes: 2 additions & 0 deletions src/__tests__/setup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Jest setup.
process.env.JEST = true;
2 changes: 2 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
export const hasBuffer = typeof Buffer === 'function' && typeof Buffer.from === 'function';
Loading

0 comments on commit 429b52d

Please sign in to comment.