From e58f77e431c7f9404bd76f6b7cf5b6d7841c0b0f Mon Sep 17 00:00:00 2001 From: randomicon00 <20146907+randomicon00@users.noreply.github.com> Date: Tue, 17 May 2022 14:52:48 +0100 Subject: [PATCH] perf(ext/web): Add fast path for non-streaming TextDecoder (#14217) Co-authored-by: Divy Srivastava --- ext/web/08_text_encoding.js | 28 +++++++++++------- ext/web/Cargo.toml | 4 +++ ext/web/benches/encoding.rs | 54 +++++++++++++++++++++++++++++++++ ext/web/lib.rs | 59 +++++++++++++++++++++++++++++++++++++ 4 files changed, 134 insertions(+), 11 deletions(-) create mode 100644 ext/web/benches/encoding.rs diff --git a/ext/web/08_text_encoding.js b/ext/web/08_text_encoding.js index 4f26089b1d..86ac5ab3c0 100644 --- a/ext/web/08_text_encoding.js +++ b/ext/web/08_text_encoding.js @@ -95,16 +95,6 @@ context: "Argument 2", }); - // TODO(lucacasonato): add fast path for non-streaming decoder & decode - - if (this.#rid === null) { - this.#rid = core.opSync("op_encoding_new_decoder", { - label: this.#encoding, - fatal: this.#fatal, - ignoreBom: this.#ignoreBOM, - }); - } - try { try { if (ArrayBufferIsView(input)) { @@ -132,12 +122,28 @@ // with a TypedArray argument copies the data. input = new Uint8Array(input); } + + if (!options.stream && this.#rid === null) { + return core.opSync("op_encoding_decode_single", input, { + label: this.#encoding, + fatal: this.#fatal, + ignoreBom: this.#ignoreBOM, + }); + } + + if (this.#rid === null) { + this.#rid = core.opSync("op_encoding_new_decoder", { + label: this.#encoding, + fatal: this.#fatal, + ignoreBom: this.#ignoreBOM, + }); + } return core.opSync("op_encoding_decode", input, { rid: this.#rid, stream: options.stream, }); } finally { - if (!options.stream) { + if (!options.stream && this.#rid) { core.close(this.#rid); this.#rid = null; } diff --git a/ext/web/Cargo.toml b/ext/web/Cargo.toml index 27d4d81471..08cd5d7b09 100644 --- a/ext/web/Cargo.toml +++ b/ext/web/Cargo.toml @@ -28,6 +28,10 @@ deno_bench_util = { version = "0.46.0", path = "../../bench_util" } deno_url = { version = "0.52.0", path = "../url" } deno_webidl = { version = "0.52.0", path = "../webidl" } +[[bench]] +name = "encoding" +harness = false + [[bench]] name = "timers_ops" harness = false diff --git a/ext/web/benches/encoding.rs b/ext/web/benches/encoding.rs new file mode 100644 index 0000000000..5b8b2a9886 --- /dev/null +++ b/ext/web/benches/encoding.rs @@ -0,0 +1,54 @@ +use deno_core::Extension; + +use deno_bench_util::bench_js_sync; +use deno_bench_util::bench_or_profile; +use deno_bench_util::bencher::{benchmark_group, Bencher}; +use deno_web::BlobStore; + +struct Permissions; + +impl deno_web::TimersPermission for Permissions { + fn allow_hrtime(&mut self) -> bool { + false + } + fn check_unstable( + &self, + _state: &deno_core::OpState, + _api_name: &'static str, + ) { + unreachable!() + } +} + +fn setup() -> Vec { + vec![ + deno_webidl::init(), + deno_url::init(), + deno_web::init::(BlobStore::default(), None), + Extension::builder() + .js(vec![( + "setup", + Box::new(|| { + Ok( + r#" + const { TextDecoder } = globalThis.__bootstrap.encoding; + const hello12k = Deno.core.encode("hello world\n".repeat(1e3)); + "# + .to_owned(), + ) + }), + )]) + .state(|state| { + state.put(Permissions {}); + Ok(()) + }) + .build(), + ] +} + +fn bench_encode_12kb(b: &mut Bencher) { + bench_js_sync(b, r#"new TextDecoder().decode(hello12k);"#, setup); +} + +benchmark_group!(benches, bench_encode_12kb); +bench_or_profile!(benches); diff --git a/ext/web/lib.rs b/ext/web/lib.rs index 0d57bf10e7..ae5cf2d803 100644 --- a/ext/web/lib.rs +++ b/ext/web/lib.rs @@ -90,6 +90,7 @@ pub fn init( op_base64_atob::decl(), op_base64_btoa::decl(), op_encoding_normalize_label::decl(), + op_encoding_decode_single::decl(), op_encoding_new_decoder::decl(), op_encoding_decode::decl(), op_encoding_encode_into::decl(), @@ -215,6 +216,64 @@ fn op_encoding_normalize_label(label: String) -> Result { Ok(encoding.name().to_lowercase()) } +#[op] +fn op_encoding_decode_single( + data: ZeroCopyBuf, + options: DecoderOptions, +) -> Result { + let DecoderOptions { + label, + ignore_bom, + fatal, + } = options; + + let encoding = Encoding::for_label(label.as_bytes()).ok_or_else(|| { + range_error(format!( + "The encoding label provided ('{}') is invalid.", + label + )) + })?; + + let mut decoder = if ignore_bom { + encoding.new_decoder_without_bom_handling() + } else { + encoding.new_decoder_with_bom_removal() + }; + + let max_buffer_length = decoder + .max_utf16_buffer_length(data.len()) + .ok_or_else(|| range_error("Value too large to decode."))?; + + let mut output = vec![0; max_buffer_length]; + + if fatal { + let (result, _, written) = + decoder.decode_to_utf16_without_replacement(&data, &mut output, true); + match result { + DecoderResult::InputEmpty => { + output.truncate(written); + Ok(output.into()) + } + DecoderResult::OutputFull => { + Err(range_error("Provided buffer too small.")) + } + DecoderResult::Malformed(_, _) => { + Err(type_error("The encoded data is not valid.")) + } + } + } else { + let (result, _, written, _) = + decoder.decode_to_utf16(&data, &mut output, true); + match result { + CoderResult::InputEmpty => { + output.truncate(written); + Ok(output.into()) + } + CoderResult::OutputFull => Err(range_error("Provided buffer too small.")), + } + } +} + #[op] fn op_encoding_new_decoder( state: &mut OpState,