Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

perf: optimizing functions #7

Merged
merged 7 commits into from
Nov 8, 2023
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
49 changes: 49 additions & 0 deletions benchmark.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/* eslint no-undef: "off" */

import {Buffer} from 'node:buffer';
import {randomBytes} from 'node:crypto';
import benchmark from 'benchmark';
import {
base64ToString,
concatUint8Arrays,
hexToUint8Array,
isUint8Array,
stringToBase64,
stringToUint8Array,
uint8ArrayToBase64,
uint8ArrayToHex,
uint8ArrayToString,
} from './index.js';

const oneMb = 1024 * 1024;
const largeUint8Array = new Uint8Array(randomBytes(oneMb).buffer);
const textFromUint8Array = uint8ArrayToString(largeUint8Array);
const base64FromUint8Array = Buffer.from(textFromUint8Array).toString('base64');
const hexFromUint8Array = uint8ArrayToHex(largeUint8Array);

const suite = new benchmark.Suite();

suite.add('isUint8Array', () => isUint8Array(largeUint8Array));

suite.add('concatUint8Arrays with 2 arrays', () => concatUint8Arrays([largeUint8Array, largeUint8Array]));

suite.add('concatUint8Arrays with 3 arrays', () => concatUint8Arrays([largeUint8Array, largeUint8Array]));

suite.add('concatUint8Arrays with 4 arrays', () => concatUint8Arrays([largeUint8Array, largeUint8Array, largeUint8Array, largeUint8Array]));

suite.add('uint8ArrayToString', () => uint8ArrayToString(largeUint8Array));

suite.add('stringToUint8Array', () => stringToUint8Array(textFromUint8Array));

suite.add('uint8ArrayToBase64', () => uint8ArrayToBase64(largeUint8Array));

suite.add('stringToBase64', () => stringToBase64(textFromUint8Array));

suite.add('base64ToString', () => base64ToString(base64FromUint8Array));

suite.add('uint8ArrayToHex', () => uint8ArrayToHex(largeUint8Array));

suite.add('hexToUint8Array', () => hexToUint8Array(hexFromUint8Array));

suite.on('cycle', event => console.log(event.target.toString()));
suite.run({async: false});
35 changes: 31 additions & 4 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
const objectToString = Object.prototype.toString;
const uint8ArrayStringified = '[object Uint8Array]';

export function isUint8Array(value) {
return value && objectToString.call(value) === '[object Uint8Array]';
if (!value) {
return false;
}

if (value.constructor === Uint8Array) {
return true;
}

return objectToString.call(value) === uint8ArrayStringified;
}

export function assertUint8Array(value) {
Expand Down Expand Up @@ -92,9 +101,11 @@ export function compareUint8Arrays(a, b) {
return 0;
}

const cachedDecoder = new globalThis.TextDecoder();

export function uint8ArrayToString(array) {
assertUint8Array(array);
return (new globalThis.TextDecoder()).decode(array);
return cachedDecoder.decode(array);
}

function assertString(value) {
Expand All @@ -103,9 +114,11 @@ function assertString(value) {
}
}

const cachedEncoder = new globalThis.TextEncoder();

export function stringToUint8Array(string) {
assertString(string);
return (new globalThis.TextEncoder()).encode(string);
return cachedEncoder.encode(string);
}

function base64ToBase64Url(base64) {
Expand All @@ -116,11 +129,25 @@ function base64UrlToBase64(base64url) {
return base64url.replaceAll('-', '+').replaceAll('_', '/');
}

// Reference: https://phuoc.ng/collection/this-vs-that/concat-vs-push/
const MAX_BLOCK_SIZE = 65_535;

export function uint8ArrayToBase64(array, {urlSafe = false} = {}) {
assertUint8Array(array);

let base64;

if (array.length < MAX_BLOCK_SIZE) {
// Required as `btoa` and `atob` don't properly support Unicode: https://developer.mozilla.org/en-US/docs/Glossary/Base64#the_unicode_problem
const base64 = globalThis.btoa(String.fromCodePoint(...array));
base64 = globalThis.btoa(String.fromCodePoint.apply(this, array));
} else {
base64 = '';
for (const value of array) {
base64 += String.fromCodePoint(value);
}

base64 = globalThis.btoa(base64);
}

return urlSafe ? base64ToBase64Url(base64) : base64;
}
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"devDependencies": {
"ava": "^5.3.1",
"typescript": "^5.2.2",
"xo": "^0.56.0"
"xo": "^0.56.0",
"benchmark": "2.1.4"
}
}
6 changes: 6 additions & 0 deletions test.js
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,12 @@ test('uint8ArrayToBase64 and base64ToUint8Array', t => {
t.deepEqual(base64ToUint8Array(base64), fixture);
});

test('should handle uint8ArrayToBase64 with 200k items', t => {
const fixture = stringToUint8Array('H'.repeat(200_000));
const base64 = uint8ArrayToBase64(fixture);
t.deepEqual(base64ToUint8Array(base64), fixture);
});

test('uint8ArrayToBase64 and base64ToUint8Array #2', t => {
const fixture = stringToUint8Array('a Ā 𐀀 文 🦄');
t.deepEqual(base64ToUint8Array(uint8ArrayToBase64(base64ToUint8Array(uint8ArrayToBase64(fixture)))), fixture);
Expand Down