Skip to content

Commit

Permalink
feat(ops): reland fast zero copy string arguments (#17996)
Browse files Browse the repository at this point in the history
Reland #16777

The codegen is disabled in async ops and when fallback to slow call is
possible (return type is a Result) to avoid hitting this V8 bug:
#17159
  • Loading branch information
littledivy committed Mar 3, 2023
1 parent 64503fa commit 38555a6
Show file tree
Hide file tree
Showing 36 changed files with 779 additions and 95 deletions.
5 changes: 0 additions & 5 deletions cli/bench/console.js
Original file line number Diff line number Diff line change
@@ -1,8 1,3 @@
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
const count = 100000;

const start = Date.now();
for (let i = 0; i < count; i ) console.log("Hello World");
const elapsed = Date.now() - start;
const rate = Math.floor(count / (elapsed / 1000));
console.log(`time ${elapsed} ms rate ${rate}`);
5 changes: 2 additions & 3 deletions cli/bench/encode_into.js
Original file line number Diff line number Diff line change
@@ -1,19 1,18 @@
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
const queueMicrotask = globalThis.queueMicrotask || process.nextTick;
let [total, count] = typeof Deno !== "undefined"
? Deno.args
: [process.argv[2], process.argv[3]];

total = total ? parseInt(total, 0) : 50;
count = count ? parseInt(count, 10) : 1000000;
count = count ? parseInt(count, 10) : 10000000;

function bench(fun) {
const start = Date.now();
for (let i = 0; i < count; i ) fun();
const elapsed = Date.now() - start;
const rate = Math.floor(count / (elapsed / 1000));
console.log(`time ${elapsed} ms rate ${rate}`);
if (--total) queueMicrotask(() => bench(fun));
if (--total) bench(fun);
}

const encoder = new TextEncoder();
Expand Down
21 changes: 21 additions & 0 deletions cli/bench/webstorage.js
Original file line number Diff line number Diff line change
@@ -0,0 1,21 @@
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.

// Note: when benchmarking across different Deno version, make sure to clear
// the DENO_DIR cache.
let [total, count] = typeof Deno !== "undefined" ? Deno.args : [];

total = total ? parseInt(total, 0) : 50;
count = count ? parseInt(count, 10) : 1000000;

function bench(fun) {
const start = Date.now();
for (let i = 0; i < count; i ) fun(i);
const elapsed = Date.now() - start;
const rate = Math.floor(count / (elapsed / 1000));
console.log(`time ${elapsed} ms rate ${rate}`);
if (--total) queueMicrotask(() => bench(fun));
}

localStorage.clear();
localStorage.setItem("foo", "bar");
bench(() => localStorage.getItem("foo"));
4 changes: 2 additions & 2 deletions cli/tsc/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -800,9 800,9 @@ pub fn resolve_npm_package_reference_types(
}

#[op]
fn op_is_node_file(state: &mut OpState, path: String) -> bool {
fn op_is_node_file(state: &mut OpState, path: &str) -> bool {
let state = state.borrow::<State>();
match ModuleSpecifier::parse(&path) {
match ModuleSpecifier::parse(path) {
Ok(specifier) => state
.maybe_npm_resolver
.as_ref()
Expand Down
6 changes: 3 additions & 3 deletions core/ops_builtin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 108,7 @@ pub fn op_metrics(state: &mut OpState) -> (OpMetrics, Vec<OpMetrics>) {

/// Builtin utility to print to stdout/stderr
#[op]
pub fn op_print(msg: String, is_err: bool) -> Result<(), Error> {
pub fn op_print(msg: &str, is_err: bool) -> Result<(), Error> {
if is_err {
stderr().write_all(msg.as_bytes())?;
stderr().flush().unwrap();
Expand Down Expand Up @@ -153,12 153,12 @@ pub fn op_wasm_streaming_feed(
pub fn op_wasm_streaming_set_url(
state: &mut OpState,
rid: ResourceId,
url: String,
url: &str,
) -> Result<(), Error> {
let wasm_streaming =
state.resource_table.get::<WasmStreamingResource>(rid)?;

wasm_streaming.0.borrow_mut().set_url(&url);
wasm_streaming.0.borrow_mut().set_url(url);

Ok(())
}
Expand Down
6 changes: 3 additions & 3 deletions ext/url/00_url.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,20 46,20 @@ function opUrlReparse(href, setter, value) {
href,
setter,
value,
componentsBuf.buffer,
componentsBuf,
);
return getSerialization(status, href);
}

function opUrlParse(href, maybeBase) {
let status;
if (maybeBase === undefined) {
status = ops.op_url_parse(href, componentsBuf.buffer);
status = ops.op_url_parse(href, componentsBuf);
} else {
status = ops.op_url_parse_with_base(
href,
maybeBase,
componentsBuf.buffer,
componentsBuf,
);
}
return getSerialization(status, href, maybeBase);
Expand Down
19 changes: 9 additions & 10 deletions ext/url/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 38,11 @@ pub fn init() -> Extension {
#[op]
pub fn op_url_parse_with_base(
state: &mut OpState,
href: String,
base_href: String,
buf: &mut [u8],
href: &str,
base_href: &str,
buf: &mut [u32],
) -> u32 {
let base_url = match Url::parse(&base_href) {
let base_url = match Url::parse(base_href) {
Ok(url) => url,
Err(_) => return ParseStatus::Err as u32,
};
Expand All @@ -64,8 64,8 @@ pub fn op_url_get_serialization(state: &mut OpState) -> String {
}

/// Parse `href` without a `base_url`. Fills the out `buf` with URL components.
#[op]
pub fn op_url_parse(state: &mut OpState, href: String, buf: &mut [u8]) -> u32 {
#[op(fast)]
pub fn op_url_parse(state: &mut OpState, href: &str, buf: &mut [u32]) -> u32 {
parse_url(state, href, None, buf)
}

Expand Down Expand Up @@ -96,15 96,14 @@ pub fn op_url_parse(state: &mut OpState, href: String, buf: &mut [u8]) -> u32 {
#[inline]
fn parse_url(
state: &mut OpState,
href: String,
href: &str,
base_href: Option<&Url>,
buf: &mut [u8],
buf: &mut [u32],
) -> u32 {
match Url::options().base_url(base_href).parse(&href) {
match Url::options().base_url(base_href).parse(href) {
Ok(url) => {
let inner_url = quirks::internal_components(&url);

let buf: &mut [u32] = as_u32_slice(buf);
buf[0] = inner_url.scheme_end;
buf[1] = inner_url.username_end;
buf[2] = inner_url.host_start;
Expand Down
6 changes: 3 additions & 3 deletions ext/web/blob.rs
Original file line number Diff line number Diff line change
Expand Up @@ -248,10 248,10 @@ pub fn op_blob_create_object_url(

#[op]
pub fn op_blob_revoke_object_url(
state: &mut OpState,
url: String,
state: &mut deno_core::OpState,
url: &str,
) -> Result<(), AnyError> {
let url = Url::parse(&url)?;
let url = Url::parse(url)?;
let blob_store = state.borrow::<BlobStore>();
blob_store.remove_object_url(&url);
Ok(())
Expand Down
4 changes: 2 additions & 2 deletions ext/web/compression.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 41,11 @@ impl Resource for CompressionResource {
#[op]
pub fn op_compression_new(
state: &mut OpState,
format: String,
format: &str,
is_decoder: bool,
) -> ResourceId {
let w = Vec::new();
let inner = match (format.as_str(), is_decoder) {
let inner = match (format, is_decoder) {
("deflate", true) => Inner::DeflateDecoder(ZlibDecoder::new(w)),
("deflate", false) => {
Inner::DeflateEncoder(ZlibEncoder::new(w, Compression::default()))
Expand Down
52 changes: 35 additions & 17 deletions ext/web/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 269,7 @@ fn op_encoding_decode_single(
#[op]
fn op_encoding_new_decoder(
state: &mut OpState,
label: String,
label: &str,
fatal: bool,
ignore_bom: bool,
) -> Result<ResourceId, AnyError> {
Expand Down Expand Up @@ -350,25 350,43 @@ impl Resource for TextDecoderResource {
}
}

#[op(v8)]
#[op]
fn op_encoding_encode_into(
scope: &mut v8::HandleScope,
input: serde_v8::Value,
input: Cow<'_, str>,
buffer: &mut [u8],
out_buf: &mut [u32],
) -> Result<(), AnyError> {
let s = v8::Local::<v8::String>::try_from(input.v8_value)?;

let mut nchars = 0;
out_buf[1] = s.write_utf8(
scope,
buffer,
Some(&mut nchars),
v8::WriteOptions::NO_NULL_TERMINATION
| v8::WriteOptions::REPLACE_INVALID_UTF8,
) as u32;
out_buf[0] = nchars as u32;
Ok(())
) {
// Since `input` is already UTF-8, we can simply find the last UTF-8 code
// point boundary from input that fits in `buffer`, and copy the bytes up to
// that point.
let boundary = if buffer.len() >= input.len() {
input.len()
} else {
let mut boundary = buffer.len();

// The maximum length of a UTF-8 code point is 4 bytes.
for _ in 0..4 {
if input.is_char_boundary(boundary) {
break;
}
debug_assert!(boundary > 0);
boundary -= 1;
}

debug_assert!(input.is_char_boundary(boundary));
boundary
};

buffer[..boundary].copy_from_slice(input[..boundary].as_bytes());

// The `read` output parameter is measured in UTF-16 code units.
out_buf[0] = match input {
// Borrowed Cow strings are zero-copy views into the V8 heap.
// Thus, they are guarantee to be SeqOneByteString.
Cow::Borrowed(v) => v[..boundary].len() as u32,
Cow::Owned(v) => v[..boundary].encode_utf16().count() as u32,
};
out_buf[1] = boundary as u32;
}

#[op(v8)]
Expand Down
6 changes: 3 additions & 3 deletions ext/webstorage/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,8 138,8 @@ pub fn op_webstorage_key(
#[op]
pub fn op_webstorage_set(
state: &mut OpState,
key: String,
value: String,
key: &str,
value: &str,
persistent: bool,
) -> Result<(), AnyError> {
let conn = get_webstorage(state, persistent)?;
Expand Down Expand Up @@ -183,7 183,7 @@ pub fn op_webstorage_get(
#[op]
pub fn op_webstorage_remove(
state: &mut OpState,
key_name: String,
key_name: &str,
persistent: bool,
) -> Result<(), AnyError> {
let conn = get_webstorage(state, persistent)?;
Expand Down
4 changes: 3 additions & 1 deletion ops/fast_call.rs
Original file line number Diff line number Diff line change
Expand Up @@ -430,7 430,8 @@ fn q_fast_ty(v: &FastValue) -> Quote {
FastValue::V8Value => q!({ v8::Local<v8::Value> }),
FastValue::Uint8Array
| FastValue::Uint32Array
| FastValue::Float64Array => unreachable!(),
| FastValue::Float64Array
| FastValue::SeqOneByteString => unreachable!(),
}
}

Expand All @@ -450,6 451,7 @@ fn q_fast_ty_variant(v: &FastValue) -> Quote {
FastValue::Uint8Array => q!({ TypedArray(CType::Uint8) }),
FastValue::Uint32Array => q!({ TypedArray(CType::Uint32) }),
FastValue::Float64Array => q!({ TypedArray(CType::Float64) }),
FastValue::SeqOneByteString => q!({ SeqOneByteString }),
}
}

Expand Down
34 changes: 31 additions & 3 deletions ops/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -423,14 423,31 @@ fn codegen_arg(
return quote! { let #ident = (); };
}
// Fast path for `String`
if is_string(&**ty) {
if let Some(is_ref) = is_string(&**ty) {
let ref_block = if is_ref {
quote! { let #ident = #ident.as_ref(); }
} else {
quote! {}
};
return quote! {
let #ident = match #core::v8::Local::<#core::v8::String>::try_from(args.get(#idx as i32)) {
Ok(v8_string) => #core::serde_v8::to_utf8(v8_string, scope),
Err(_) => {
return #core::_ops::throw_type_error(scope, format!("Expected string at position {}", #idx));
}
};
#ref_block
};
}
// Fast path for `Cow<'_, str>`
if is_cow_str(&**ty) {
return quote! {
let #ident = match #core::v8::Local::<#core::v8::String>::try_from(args.get(#idx as i32)) {
Ok(v8_string) => ::std::borrow::Cow::Owned(#core::serde_v8::to_utf8(v8_string, scope)),
Err(_) => {
return #core::_ops::throw_type_error(scope, format!("Expected string at position {}", #idx));
}
};
};
}
// Fast path for `Option<String>`
Expand Down Expand Up @@ -701,14 718,25 @@ fn is_result(ty: impl ToTokens) -> bool {
}
}

fn is_string(ty: impl ToTokens) -> bool {
tokens(ty) == "String"
fn is_string(ty: impl ToTokens) -> Option<bool> {
let toks = tokens(ty);
if toks == "String" {
return Some(false);
}
if toks == "& str" {
return Some(true);
}
None
}

fn is_option_string(ty: impl ToTokens) -> bool {
tokens(ty) == "Option < String >"
}

fn is_cow_str(ty: impl ToTokens) -> bool {
tokens(&ty).starts_with("Cow <") && tokens(&ty).ends_with("str >")
}

enum SliceType {
U8,
U8Mut,
Expand Down
Loading

0 comments on commit 38555a6

Please sign in to comment.