From 5268c88d220577af1ac14c00e41131be13076e0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Louren=C3=A7o?= Date: Tue, 7 Nov 2023 20:01:37 -0300 Subject: [PATCH 1/7] perf: improve isUint8Array --- index.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/index.js b/index.js index bbe02f5..43c9808 100644 --- a/index.js +++ b/index.js @@ -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) { From 28c92118838a6e1f7f3c5e4cea4e1643eba702f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Louren=C3=A7o?= Date: Tue, 7 Nov 2023 20:24:53 -0300 Subject: [PATCH 2/7] perf: cache text decoder for uint8ArrayToString --- index.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/index.js b/index.js index 43c9808..820e757 100644 --- a/index.js +++ b/index.js @@ -100,10 +100,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) { @@ -125,7 +126,7 @@ function base64UrlToBase64(base64url) { return base64url.replaceAll('-', '+').replaceAll('_', '/'); } -export function uint8ArrayToBase64(array, {urlSafe = false} = {}) { +export function uint8ArrayToBase64(array, { urlSafe = false } = {}) { assertUint8Array(array); // Required as `btoa` and `atob` don't properly support Unicode: https://developer.mozilla.org/en-US/docs/Glossary/Base64#the_unicode_problem @@ -139,9 +140,9 @@ export function base64ToUint8Array(base64String) { return Uint8Array.from(globalThis.atob(base64UrlToBase64(base64String)), x => x.codePointAt(0)); } -export function stringToBase64(string, {urlSafe = false} = {}) { +export function stringToBase64(string, { urlSafe = false } = {}) { assertString(string); - return uint8ArrayToBase64(stringToUint8Array(string), {urlSafe}); + return uint8ArrayToBase64(stringToUint8Array(string), { urlSafe }); } export function base64ToString(base64String) { @@ -149,7 +150,7 @@ export function base64ToString(base64String) { return uint8ArrayToString(base64ToUint8Array(base64String)); } -const byteToHexLookupTable = Array.from({length: 256}, (_, index) => index.toString(16).padStart(2, '0')); +const byteToHexLookupTable = Array.from({ length: 256 }, (_, index) => index.toString(16).padStart(2, '0')); export function uint8ArrayToHex(array) { assertUint8Array(array); From 85b7903baddc2b2eee2fc28b232aba011f5d7388 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Louren=C3=A7o?= Date: Tue, 7 Nov 2023 20:27:41 -0300 Subject: [PATCH 3/7] perf: cache text encoder for stringToUint8Array --- index.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/index.js b/index.js index 820e757..ce26430 100644 --- a/index.js +++ b/index.js @@ -113,9 +113,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) { From 65e3aef707e88d7c7a3c8e61fc6bde3fe80ac284 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Louren=C3=A7o?= Date: Tue, 7 Nov 2023 21:20:12 -0300 Subject: [PATCH 4/7] fix: handle arrays larger than 65k on uint8ArrayToBase64 --- index.js | 17 +++++++++++++++-- test.js | 6 ++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index ce26430..c92b983 100644 --- a/index.js +++ b/index.js @@ -128,11 +128,24 @@ function base64UrlToBase64(base64url) { return base64url.replaceAll('-', '+').replaceAll('_', '/'); } +// reference: https://phuoc.ng/collection/this-vs-that/concat-vs-push/ +const MAX_BLOCK_SIZE = 65535; + export function uint8ArrayToBase64(array, { urlSafe = false } = {}) { assertUint8Array(array); - // 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)); + 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 + base64 = globalThis.btoa(String.fromCodePoint(...array)); + } else { + base64 = ''; + for (let value of array) { + base64 += String.fromCodePoint(value); + } + base64 = globalThis.btoa(base64); + } return urlSafe ? base64ToBase64Url(base64) : base64; } diff --git a/test.js b/test.js index 92fd192..afa914a 100644 --- a/test.js +++ b/test.js @@ -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); From 88de41506cc31f04fd4ac1c71dddbf6be8403d9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Louren=C3=A7o?= Date: Tue, 7 Nov 2023 21:21:04 -0300 Subject: [PATCH 5/7] perf: avoid copying the array on uint8ArrayToBase64 --- index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index c92b983..2576afe 100644 --- a/index.js +++ b/index.js @@ -137,8 +137,8 @@ export function uint8ArrayToBase64(array, { urlSafe = false } = {}) { 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 - base64 = globalThis.btoa(String.fromCodePoint(...array)); + // Required as `btoa` and `atob` don't properly support Unicode: https://developer.mozilla.org/en-US/docs/Glossary/Base64#the_unicode_problem + base64 = globalThis.btoa(String.fromCodePoint.apply(this, array)); } else { base64 = ''; for (let value of array) { From 567879c8d2e1dc7912f5af7df2ee3291df6ed091 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Louren=C3=A7o?= Date: Wed, 8 Nov 2023 10:48:52 -0300 Subject: [PATCH 6/7] chore: fixed lint issues --- index.js | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/index.js b/index.js index 2576afe..562945a 100644 --- a/index.js +++ b/index.js @@ -100,6 +100,7 @@ export function compareUint8Arrays(a, b) { return 0; } + const cachedDecoder = new globalThis.TextDecoder(); export function uint8ArrayToString(array) { @@ -128,22 +129,23 @@ function base64UrlToBase64(base64url) { return base64url.replaceAll('-', '+').replaceAll('_', '/'); } -// reference: https://phuoc.ng/collection/this-vs-that/concat-vs-push/ -const MAX_BLOCK_SIZE = 65535; +// Reference: https://phuoc.ng/collection/this-vs-that/concat-vs-push/ +const MAX_BLOCK_SIZE = 65_535; -export function uint8ArrayToBase64(array, { urlSafe = false } = {}) { +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 - base64 = globalThis.btoa(String.fromCodePoint.apply(this, array)); + base64 = globalThis.btoa(String.fromCodePoint.apply(this, array)); } else { base64 = ''; - for (let value of array) { + for (const value of array) { base64 += String.fromCodePoint(value); } + base64 = globalThis.btoa(base64); } @@ -155,9 +157,9 @@ export function base64ToUint8Array(base64String) { return Uint8Array.from(globalThis.atob(base64UrlToBase64(base64String)), x => x.codePointAt(0)); } -export function stringToBase64(string, { urlSafe = false } = {}) { +export function stringToBase64(string, {urlSafe = false} = {}) { assertString(string); - return uint8ArrayToBase64(stringToUint8Array(string), { urlSafe }); + return uint8ArrayToBase64(stringToUint8Array(string), {urlSafe}); } export function base64ToString(base64String) { @@ -165,7 +167,7 @@ export function base64ToString(base64String) { return uint8ArrayToString(base64ToUint8Array(base64String)); } -const byteToHexLookupTable = Array.from({ length: 256 }, (_, index) => index.toString(16).padStart(2, '0')); +const byteToHexLookupTable = Array.from({length: 256}, (_, index) => index.toString(16).padStart(2, '0')); export function uint8ArrayToHex(array) { assertUint8Array(array); From 33dd17b7221ac62296140abec9fa577b8723ef4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Louren=C3=A7o?= Date: Wed, 8 Nov 2023 10:50:21 -0300 Subject: [PATCH 7/7] chore: added benchmarks --- benchmark.mjs | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ package.json | 3 ++- 2 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 benchmark.mjs diff --git a/benchmark.mjs b/benchmark.mjs new file mode 100644 index 0000000..9a01ac4 --- /dev/null +++ b/benchmark.mjs @@ -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}); diff --git a/package.json b/package.json index dcef9ec..58c945d 100644 --- a/package.json +++ b/package.json @@ -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" } }