1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-12-01 16:51:13 -05:00

perf(ext/web): optimize atob/btoa (#13841)

Follow up to #13839, optimizing `base64_roundtrip` ~20x (~125ms => ~6.5ms)
This commit is contained in:
Aaron O'Mullan 2022-03-05 20:12:30 +01:00 committed by Yoshiya Hinosawa
parent 55a706e0ca
commit d55acb4c0f
No known key found for this signature in database
GPG key ID: 0E8BFAA8A5B4E92B
3 changed files with 83 additions and 64 deletions

View file

@ -263,7 +263,7 @@ jobs:
~/.cargo/registry/index ~/.cargo/registry/index
~/.cargo/registry/cache ~/.cargo/registry/cache
~/.cargo/git/db ~/.cargo/git/db
key: 4-cargo-home-${{ matrix.os }}-${{ hashFiles('Cargo.lock') }} key: 5-cargo-home-${{ matrix.os }}-${{ hashFiles('Cargo.lock') }}
# In main branch, always creates fresh cache # In main branch, always creates fresh cache
- name: Cache build output (main) - name: Cache build output (main)
@ -279,7 +279,7 @@ jobs:
!./target/*/*.zip !./target/*/*.zip
!./target/*/*.tar.gz !./target/*/*.tar.gz
key: | key: |
4-cargo-target-${{ matrix.os }}-${{ matrix.profile }}-${{ github.sha }} 5-cargo-target-${{ matrix.os }}-${{ matrix.profile }}-${{ github.sha }}
# Restore cache from the latest 'main' branch build. # Restore cache from the latest 'main' branch build.
- name: Cache build output (PR) - name: Cache build output (PR)
@ -295,7 +295,7 @@ jobs:
!./target/*/*.tar.gz !./target/*/*.tar.gz
key: never_saved key: never_saved
restore-keys: | restore-keys: |
4-cargo-target-${{ matrix.os }}-${{ matrix.profile }}- 5-cargo-target-${{ matrix.os }}-${{ matrix.profile }}-
# Don't save cache after building PRs or branches other than 'main'. # Don't save cache after building PRs or branches other than 'main'.
- name: Skip save cache (PR) - name: Skip save cache (PR)

View file

@ -9,21 +9,10 @@
"use strict"; "use strict";
((window) => { ((window) => {
const core = Deno.core;
const webidl = window.__bootstrap.webidl; const webidl = window.__bootstrap.webidl;
const {
forgivingBase64Encode,
forgivingBase64Decode,
} = window.__bootstrap.infra;
const { DOMException } = window.__bootstrap.domException; const { DOMException } = window.__bootstrap.domException;
const { const { TypeError } = window.__bootstrap.primordials;
ArrayPrototypeMap,
StringPrototypeCharCodeAt,
ArrayPrototypeJoin,
SafeArrayIterator,
StringFromCharCode,
TypedArrayFrom,
Uint8Array,
} = window.__bootstrap.primordials;
/** /**
* @param {string} data * @param {string} data
@ -36,13 +25,17 @@
prefix, prefix,
context: "Argument 1", context: "Argument 1",
}); });
try {
const uint8Array = forgivingBase64Decode(data); return core.opSync("op_base64_atob", data);
const result = ArrayPrototypeMap( } catch (e) {
[...new SafeArrayIterator(uint8Array)], if (e instanceof TypeError) {
(byte) => StringFromCharCode(byte), throw new DOMException(
); "Failed to decode base64: invalid character",
return ArrayPrototypeJoin(result, ""); "InvalidCharacterError",
);
}
throw e;
}
} }
/** /**
@ -56,20 +49,17 @@
prefix, prefix,
context: "Argument 1", context: "Argument 1",
}); });
const byteArray = ArrayPrototypeMap( try {
[...new SafeArrayIterator(data)], return core.opSync("op_base64_btoa", data);
(char) => { } catch (e) {
const charCode = StringPrototypeCharCodeAt(char, 0); if (e instanceof TypeError) {
if (charCode > 0xff) { throw new DOMException(
throw new DOMException( "The string to be encoded contains characters outside of the Latin1 range.",
"The string to be encoded contains characters outside of the Latin1 range.", "InvalidCharacterError",
"InvalidCharacterError", );
); }
} throw e;
return charCode; }
},
);
return forgivingBase64Encode(TypedArrayFrom(Uint8Array, byteArray));
} }
window.__bootstrap.base64 = { window.__bootstrap.base64 = {

View file

@ -12,6 +12,7 @@ use deno_core::include_js_files;
use deno_core::op_async; use deno_core::op_async;
use deno_core::op_sync; use deno_core::op_sync;
use deno_core::url::Url; use deno_core::url::Url;
use deno_core::ByteString;
use deno_core::Extension; use deno_core::Extension;
use deno_core::OpState; use deno_core::OpState;
use deno_core::Resource; use deno_core::Resource;
@ -85,6 +86,8 @@ pub fn init<P: TimersPermission + 'static>(
.ops(vec![ .ops(vec![
("op_base64_decode", op_sync(op_base64_decode)), ("op_base64_decode", op_sync(op_base64_decode)),
("op_base64_encode", op_sync(op_base64_encode)), ("op_base64_encode", op_sync(op_base64_encode)),
("op_base64_atob", op_sync(op_base64_atob)),
("op_base64_btoa", op_sync(op_base64_btoa)),
( (
"op_encoding_normalize_label", "op_encoding_normalize_label",
op_sync(op_encoding_normalize_label), op_sync(op_encoding_normalize_label),
@ -146,21 +149,42 @@ pub fn init<P: TimersPermission + 'static>(
} }
fn op_base64_decode( fn op_base64_decode(
_state: &mut OpState, _: &mut OpState,
input: String, input: String,
_: (), _: (),
) -> Result<ZeroCopyBuf, AnyError> { ) -> Result<ZeroCopyBuf, AnyError> {
let mut input: &str = &input.replace(|c| char::is_ascii_whitespace(&c), ""); let mut input = input.into_bytes();
input.retain(|c| !c.is_ascii_whitespace());
Ok(b64_decode(&input)?.into())
}
fn op_base64_atob(
_: &mut OpState,
s: ByteString,
_: (),
) -> Result<ByteString, AnyError> {
let mut s = s.0;
s.retain(|c| !c.is_ascii_whitespace());
// If padding is expected, fail if not 4-byte aligned
if s.len() % 4 != 0 && (s.ends_with(b"==") || s.ends_with(b"=")) {
return Err(
DomExceptionInvalidCharacterError::new("Failed to decode base64.").into(),
);
}
Ok(ByteString(b64_decode(&s)?))
}
fn b64_decode(input: &[u8]) -> Result<Vec<u8>, AnyError> {
// "If the length of input divides by 4 leaving no remainder, then: // "If the length of input divides by 4 leaving no remainder, then:
// if input ends with one or two U+003D EQUALS SIGN (=) characters, // if input ends with one or two U+003D EQUALS SIGN (=) characters,
// remove them from input." // remove them from input."
if input.len() % 4 == 0 { let input = match input.len() % 4 == 0 {
if input.ends_with("==") { true if input.ends_with(b"==") => &input[..input.len() - 2],
input = &input[..input.len() - 2] true if input.ends_with(b"=") => &input[..input.len() - 1],
} else if input.ends_with('=') { _ => input,
input = &input[..input.len() - 1] };
}
}
// "If the length of input divides by 4 leaving a remainder of 1, // "If the length of input divides by 4 leaving a remainder of 1,
// throw an InvalidCharacterError exception and abort these steps." // throw an InvalidCharacterError exception and abort these steps."
@ -170,38 +194,43 @@ fn op_base64_decode(
); );
} }
if input let cfg = base64::Config::new(base64::CharacterSet::Standard, true)
.chars() .decode_allow_trailing_bits(true);
.any(|c| c != '+' && c != '/' && !c.is_alphanumeric()) let out = base64::decode_config(input, cfg).map_err(|err| match err {
{ base64::DecodeError::InvalidByte(_, _) => {
return Err(
DomExceptionInvalidCharacterError::new( DomExceptionInvalidCharacterError::new(
"Failed to decode base64: invalid character", "Failed to decode base64: invalid character",
) )
.into(), }
); _ => DomExceptionInvalidCharacterError::new(&format!(
}
let cfg = base64::Config::new(base64::CharacterSet::Standard, true)
.decode_allow_trailing_bits(true);
let out = base64::decode_config(&input, cfg).map_err(|err| {
DomExceptionInvalidCharacterError::new(&format!(
"Failed to decode base64: {:?}", "Failed to decode base64: {:?}",
err err
)) )),
})?; })?;
Ok(ZeroCopyBuf::from(out))
Ok(out)
} }
fn op_base64_encode( fn op_base64_encode(
_state: &mut OpState, _: &mut OpState,
s: ZeroCopyBuf, s: ZeroCopyBuf,
_: (), _: (),
) -> Result<String, AnyError> { ) -> Result<String, AnyError> {
Ok(b64_encode(&s))
}
fn op_base64_btoa(
_: &mut OpState,
s: ByteString,
_: (),
) -> Result<String, AnyError> {
Ok(b64_encode(&s))
}
fn b64_encode(s: impl AsRef<[u8]>) -> String {
let cfg = base64::Config::new(base64::CharacterSet::Standard, true) let cfg = base64::Config::new(base64::CharacterSet::Standard, true)
.decode_allow_trailing_bits(true); .decode_allow_trailing_bits(true);
let out = base64::encode_config(&s, cfg); base64::encode_config(s.as_ref(), cfg)
Ok(out)
} }
#[derive(Deserialize)] #[derive(Deserialize)]