-
Notifications
You must be signed in to change notification settings - Fork 561
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Improve base64 encoding/decoding speeds (#1985)
Adds new base64 encoding and decoding utilities that have faster performance characteristics than the current implementation. This also fixes an issue where the client would sometimes get unresponsive while encoding large files. Fixes #1984 ``` async base64 encoding of 98095974 bytes took 596 ms sync base64 encoding of 98095974 bytes took 86004 ms async base64 decoding of 98095974 bytes took 5525 ms sync base64 decoding of 98095974 bytes took 73044 ms ```
- Loading branch information
1 parent
0bbe326
commit e301bda
Showing
17 changed files
with
247 additions
and
70 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 |
---|---|---|
@@ -1,6 +1,6 @@ | ||
{ | ||
"branches": 90.11, | ||
"functions": 96.34, | ||
"lines": 97.29, | ||
"functions": 96.35, | ||
"lines": 97.3, | ||
"statements": 96.98 | ||
} |
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
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
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
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
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 |
---|---|---|
@@ -1,6 +1,6 @@ | ||
{ | ||
"branches": 96, | ||
"functions": 99, | ||
"lines": 98.68, | ||
"statements": 95.58 | ||
"branches": 96.01, | ||
"functions": 98.53, | ||
"lines": 98.6, | ||
"statements": 95.55 | ||
} |
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
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
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,67 @@ | ||
import { bytesToBase64, stringToBytes } from '@metamask/utils'; | ||
import { File } from 'buffer'; | ||
|
||
import { decodeBase64, encodeBase64 } from './base64'; | ||
import { VirtualFile } from './virtual-file'; | ||
|
||
// Very basic mock that mimics the base64 encoding logic of the browser | ||
class MockFileReader { | ||
onload?: () => any; | ||
|
||
onerror?: () => any; | ||
|
||
result?: any; | ||
|
||
error?: any; | ||
|
||
readAsDataURL(file: File) { | ||
file | ||
.arrayBuffer() | ||
.then((buffer) => { | ||
const u8 = new Uint8Array(buffer); | ||
|
||
this.result = `data:application/octet-stream;base64,${bytesToBase64( | ||
u8, | ||
)}`; | ||
|
||
this.onload?.(); | ||
}) | ||
.catch((error) => { | ||
this.error = error; | ||
this.onerror?.(); | ||
}); | ||
} | ||
} | ||
|
||
describe('encodeBase64', () => { | ||
// We can remove this once we drop Node 18 | ||
Object.defineProperty(globalThis, 'File', { | ||
value: File, | ||
}); | ||
|
||
it('encodes vfile to base64', async () => { | ||
const vfile = new VirtualFile( | ||
stringToBytes(JSON.stringify({ foo: 'bar' })), | ||
); | ||
expect(await encodeBase64(vfile)).toBe('eyJmb28iOiJiYXIifQ=='); | ||
}); | ||
|
||
it('uses FileReader API when available', async () => { | ||
Object.defineProperty(globalThis, 'FileReader', { | ||
value: MockFileReader, | ||
}); | ||
|
||
const vfile = new VirtualFile( | ||
stringToBytes(JSON.stringify({ foo: 'bar' })), | ||
); | ||
expect(await encodeBase64(vfile)).toBe('eyJmb28iOiJiYXIifQ=='); | ||
}); | ||
}); | ||
|
||
describe('decodeBase64', () => { | ||
it('decodes base64 string to bytes', async () => { | ||
expect(await decodeBase64('eyJmb28iOiJiYXIifQ==')).toStrictEqual( | ||
stringToBytes(JSON.stringify({ foo: 'bar' })), | ||
); | ||
}); | ||
}); |
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,46 @@ | ||
import { bytesToBase64 } from '@metamask/utils'; | ||
|
||
import { getBytes } from './bytes'; | ||
import type { VirtualFile } from './virtual-file'; | ||
|
||
/** | ||
* Provides fast, asynchronous base64 encoding. | ||
* | ||
* @param input - The input value, assumed to be coercable to bytes. | ||
* @returns A base64 string. | ||
*/ | ||
export async function encodeBase64(input: Uint8Array | VirtualFile | string) { | ||
const bytes = getBytes(input); | ||
// In the browser, FileReader is much faster than bytesToBase64. | ||
if ('FileReader' in globalThis) { | ||
return await new Promise((resolve, reject) => { | ||
const reader = Object.assign(new FileReader(), { | ||
onload: () => | ||
resolve( | ||
(reader.result as string).replace( | ||
'data:application/octet-stream;base64,', | ||
'', | ||
), | ||
), | ||
onerror: () => reject(reader.error), | ||
}); | ||
reader.readAsDataURL( | ||
new File([bytes], '', { type: 'application/octet-stream' }), | ||
); | ||
}); | ||
} | ||
return bytesToBase64(bytes); | ||
} | ||
|
||
/** | ||
* Provides fast, asynchronous base64 decoding. | ||
* | ||
* @param base64 - A base64 string. | ||
* @returns A Uint8Array of bytes. | ||
*/ | ||
export async function decodeBase64(base64: string) { | ||
const response = await fetch( | ||
`data:application/octet-stream;base64,${base64}`, | ||
); | ||
return new Uint8Array(await response.arrayBuffer()); | ||
} |
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,24 @@ | ||
import { getBytes } from './bytes'; | ||
import { VirtualFile } from './virtual-file'; | ||
|
||
describe('getBytes', () => { | ||
const FOO_BAR_STR = 'foo bar'; | ||
const FOO_BAR_UINT8 = new Uint8Array([ | ||
0x66, 0x6f, 0x6f, 0x20, 0x62, 0x61, 0x72, | ||
]); | ||
|
||
it('handles Uint8Array', () => { | ||
expect(getBytes(FOO_BAR_UINT8)).toStrictEqual(FOO_BAR_UINT8); | ||
}); | ||
|
||
it('handles strings', () => { | ||
expect(getBytes(FOO_BAR_STR)).toStrictEqual(FOO_BAR_UINT8); | ||
}); | ||
|
||
it('handles virtual files', () => { | ||
expect(getBytes(new VirtualFile(FOO_BAR_UINT8))).toStrictEqual( | ||
FOO_BAR_UINT8, | ||
); | ||
expect(getBytes(new VirtualFile(FOO_BAR_STR))).toStrictEqual(FOO_BAR_UINT8); | ||
}); | ||
}); |
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,21 @@ | ||
import { stringToBytes } from '@metamask/utils'; | ||
|
||
import { VirtualFile } from './virtual-file'; | ||
|
||
/** | ||
* Convert a bytes-like input value to a Uint8Array. | ||
* | ||
* @param bytes - A bytes-like value. | ||
* @returns The input value converted to a Uint8Array if necessary. | ||
*/ | ||
export function getBytes(bytes: VirtualFile | Uint8Array | string): Uint8Array { | ||
// Unwrap VirtualFiles to extract the content | ||
// The content is then either a string or Uint8Array | ||
const unwrapped = bytes instanceof VirtualFile ? bytes.value : bytes; | ||
|
||
if (typeof unwrapped === 'string') { | ||
return stringToBytes(unwrapped); | ||
} | ||
|
||
return unwrapped; | ||
} |
Oops, something went wrong.