Skip to content

Commit

Permalink
perf(ext/web): optimize atob/btoa (#13841)
Browse files Browse the repository at this point in the history
Follow up to #13839, optimizing `base64_roundtrip` ~20x (~125ms => ~6.5ms)
  • Loading branch information
AaronO authored Mar 5, 2022
1 parent 96dc742 commit 72d593f
Show file tree
Hide file tree
Showing 3 changed files with 83 additions and 64 deletions.
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ jobs:
~/.cargo/registry/index
~/.cargo/registry/cache
~/.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
- name: Cache build output (main)
Expand All @@ -279,7 +279,7 @@ jobs:
!./target/*/*.zip
!./target/*/*.tar.gz
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.
- name: Cache build output (PR)
Expand All @@ -295,7 +295,7 @@ jobs:
!./target/*/*.tar.gz
key: never_saved
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'.
- name: Skip save cache (PR)
Expand Down
58 changes: 24 additions & 34 deletions ext/web/05_base64.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,10 @@
"use strict";

((window) => {
const core = Deno.core;
const webidl = window.__bootstrap.webidl;
const {
forgivingBase64Encode,
forgivingBase64Decode,
} = window.__bootstrap.infra;
const { DOMException } = window.__bootstrap.domException;
const {
ArrayPrototypeMap,
StringPrototypeCharCodeAt,
ArrayPrototypeJoin,
SafeArrayIterator,
StringFromCharCode,
TypedArrayFrom,
Uint8Array,
} = window.__bootstrap.primordials;
const { TypeError } = window.__bootstrap.primordials;

/**
* @param {string} data
Expand All @@ -36,13 +25,17 @@
prefix,
context: "Argument 1",
});

const uint8Array = forgivingBase64Decode(data);
const result = ArrayPrototypeMap(
[...new SafeArrayIterator(uint8Array)],
(byte) => StringFromCharCode(byte),
);
return ArrayPrototypeJoin(result, "");
try {
return core.opSync("op_base64_atob", data);
} catch (e) {
if (e instanceof TypeError) {
throw new DOMException(
"Failed to decode base64: invalid character",
"InvalidCharacterError",
);
}
throw e;
}
}

/**
Expand All @@ -56,20 +49,17 @@
prefix,
context: "Argument 1",
});
const byteArray = ArrayPrototypeMap(
[...new SafeArrayIterator(data)],
(char) => {
const charCode = StringPrototypeCharCodeAt(char, 0);
if (charCode > 0xff) {
throw new DOMException(
"The string to be encoded contains characters outside of the Latin1 range.",
"InvalidCharacterError",
);
}
return charCode;
},
);
return forgivingBase64Encode(TypedArrayFrom(Uint8Array, byteArray));
try {
return core.opSync("op_base64_btoa", data);
} catch (e) {
if (e instanceof TypeError) {
throw new DOMException(
"The string to be encoded contains characters outside of the Latin1 range.",
"InvalidCharacterError",
);
}
throw e;
}
}

window.__bootstrap.base64 = {
Expand Down
83 changes: 56 additions & 27 deletions ext/web/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use deno_core::include_js_files;
use deno_core::op_async;
use deno_core::op_sync;
use deno_core::url::Url;
use deno_core::ByteString;
use deno_core::Extension;
use deno_core::OpState;
use deno_core::Resource;
Expand Down Expand Up @@ -85,6 +86,8 @@ pub fn init<P: TimersPermission + 'static>(
.ops(vec![
("op_base64_decode", op_sync(op_base64_decode)),
("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_sync(op_encoding_normalize_label),
Expand Down Expand Up @@ -146,21 +149,42 @@ pub fn init<P: TimersPermission + 'static>(
}

fn op_base64_decode(
_state: &mut OpState,
_: &mut OpState,
input: String,
_: (),
) -> 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 input ends with one or two U+003D EQUALS SIGN (=) characters,
// remove them from input."
if input.len() % 4 == 0 {
if input.ends_with("==") {
input = &input[..input.len() - 2]
} else if input.ends_with('=') {
input = &input[..input.len() - 1]
}
}
let input = match input.len() % 4 == 0 {
true if input.ends_with(b"==") => &input[..input.len() - 2],
true if input.ends_with(b"=") => &input[..input.len() - 1],
_ => input,
};

// "If the length of input divides by 4 leaving a remainder of 1,
// throw an InvalidCharacterError exception and abort these steps."
Expand All @@ -170,38 +194,43 @@ fn op_base64_decode(
);
}

if input
.chars()
.any(|c| c != '+' && c != '/' && !c.is_alphanumeric())
{
return Err(
let cfg = base64::Config::new(base64::CharacterSet::Standard, true)
.decode_allow_trailing_bits(true);
let out = base64::decode_config(input, cfg).map_err(|err| match err {
base64::DecodeError::InvalidByte(_, _) => {
DomExceptionInvalidCharacterError::new(
"Failed to decode base64: invalid character",
)
.into(),
);
}

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!(
}
_ => DomExceptionInvalidCharacterError::new(&format!(
"Failed to decode base64: {:?}",
err
))
)),
})?;
Ok(ZeroCopyBuf::from(out))

Ok(out)
}

fn op_base64_encode(
_state: &mut OpState,
_: &mut OpState,
s: ZeroCopyBuf,
_: (),
) -> 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)
.decode_allow_trailing_bits(true);
let out = base64::encode_config(&s, cfg);
Ok(out)
base64::encode_config(s.as_ref(), cfg)
}

#[derive(Deserialize)]
Expand Down

0 comments on commit 72d593f

Please sign in to comment.