Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve the performance of 'string enums' #3915

Merged
merged 14 commits into from
May 21, 2024
Merged
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@
* Generate JS bindings for WebIDL dictionary setters instead of using `Reflect`. This increases the size of the Web API bindings but should be more performant. Also, importing getters/setters from JS now supports specifying the JS attribute name as a string, e.g. `#[wasm_bindgen(method, setter = "x-cdm-codecs")]`.
[#3898](https://github.com/rustwasm/wasm-bindgen/pull/3898)

* Greatly improve the performance of sending WebIDL 'import enums' across the JavaScript boundary by converting the enum variant string to/from an int.
Davidster marked this conversation as resolved.
Show resolved Hide resolved
[#3915](https://github.com/rustwasm/wasm-bindgen/pull/3915)

### Fixed

* Copy port from headless test server when using `WASM_BINDGEN_TEST_ADDRESS`.
Expand Down
133 changes: 67 additions & 66 deletions crates/backend/src/codegen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use crate::ast;
use crate::encode;
use crate::Diagnostic;
use once_cell::sync::Lazy;
use proc_macro2::{Ident, Literal, Span, TokenStream};
use proc_macro2::{Ident, Span, TokenStream};
use quote::format_ident;
use quote::quote_spanned;
use quote::{quote, ToTokens};
Expand Down Expand Up @@ -1024,114 +1024,115 @@ impl ToTokens for ast::ImportType {
impl ToTokens for ast::ImportEnum {
fn to_tokens(&self, tokens: &mut TokenStream) {
let vis = &self.vis;
let name = &self.name;
let expect_string = format!("attempted to convert invalid {} into JSValue", name);
let enum_name = &self.name;
let name_str = enum_name.to_string();
let name_len = name_str.len() as u32;
let name_chars = name_str.chars().map(|c| c as u32);
Davidster marked this conversation as resolved.
Show resolved Hide resolved
let variants = &self.variants;
let variant_strings = &self.variant_values;
let variant_count = self.variant_values.len() as u32;
let variant_values = &self.variant_values;
let variant_indices = (0..variant_count).collect::<Vec<_>>();
let invalid = variant_count;
let hole = variant_count + 1;
let attrs = &self.rust_attrs;

let mut current_idx: usize = 0;
let variant_indexes: Vec<Literal> = variants
.iter()
.map(|_| {
let this_index = current_idx;
current_idx += 1;
Literal::usize_unsuffixed(this_index)
})
.collect();

// Borrow variant_indexes because we need to use it multiple times inside the quote! macro
let variant_indexes_ref = &variant_indexes;

// A vector of EnumName::VariantName tokens for this enum
let variant_paths: Vec<TokenStream> = self
.variants
.iter()
.map(|v| quote!(#name::#v).into_token_stream())
.map(|v| quote!(#enum_name::#v).into_token_stream())
.collect();

// Borrow variant_paths because we need to use it multiple times inside the quote! macro
let variant_paths_ref = &variant_paths;

let wasm_bindgen = &self.wasm_bindgen;

let describe_variants = self.variant_values.iter().map(|variant_value| {
let length = variant_value.len() as u32;
let chars = variant_value.chars().map(|c| c as u32);
Davidster marked this conversation as resolved.
Show resolved Hide resolved
quote! {
inform(#length);
#(inform(#chars);)*
}
});

(quote! {
#(#attrs)*
#vis enum #name {
#(#variants = #variant_indexes_ref,)*
#[non_exhaustive]
daxpedda marked this conversation as resolved.
Show resolved Hide resolved
#[repr(u32)]
#vis enum #enum_name {
#(#variants = #variant_indices,)*
#[automatically_derived]
#[doc(hidden)]
__Nonexhaustive,
__Invalid
}

#[automatically_derived]
impl #name {
fn from_str(s: &str) -> Option<#name> {
match s {
#(#variant_strings => Some(#variant_paths_ref),)*
_ => None,
}
}

fn to_str(&self) -> &'static str {
match self {
#(#variant_paths_ref => #variant_strings,)*
#name::__Nonexhaustive => panic!(#expect_string),
}
}
Davidster marked this conversation as resolved.
Show resolved Hide resolved

#vis fn from_js_value(obj: &#wasm_bindgen::JsValue) -> Option<#name> {
obj.as_string().and_then(|obj_str| Self::from_str(obj_str.as_str()))
}
}
Davidster marked this conversation as resolved.
Show resolved Hide resolved
impl #wasm_bindgen::convert::IntoWasmAbi for #enum_name {
type Abi = u32;

// It should really be using &str for all of these, but that requires some major changes to cli-support
#[automatically_derived]
impl #wasm_bindgen::describe::WasmDescribe for #name {
fn describe() {
<#wasm_bindgen::JsValue as #wasm_bindgen::describe::WasmDescribe>::describe()
#[inline]
fn into_abi(self) -> u32 {
self as u32
}
}

#[automatically_derived]
impl #wasm_bindgen::convert::IntoWasmAbi for #name {
type Abi = <#wasm_bindgen::JsValue as #wasm_bindgen::convert::IntoWasmAbi>::Abi;
impl #wasm_bindgen::convert::FromWasmAbi for #enum_name {
type Abi = u32;

#[inline]
fn into_abi(self) -> Self::Abi {
<#wasm_bindgen::JsValue as #wasm_bindgen::convert::IntoWasmAbi>::into_abi(self.into())
unsafe fn from_abi(val: u32) -> Self {
match val {
#(#variant_indices => #variant_paths_ref,)*
#invalid => #enum_name::__Invalid,
_ => unreachable!("The JS binding should only ever produce a valid value or the specific 'invalid' value")
}
}
}

#[automatically_derived]
impl #wasm_bindgen::convert::FromWasmAbi for #name {
type Abi = <#wasm_bindgen::JsValue as #wasm_bindgen::convert::FromWasmAbi>::Abi;

unsafe fn from_abi(js: Self::Abi) -> Self {
let s = <#wasm_bindgen::JsValue as #wasm_bindgen::convert::FromWasmAbi>::from_abi(js);
#name::from_js_value(&s).unwrap_or(#name::__Nonexhaustive)
}
impl #wasm_bindgen::convert::OptionFromWasmAbi for #enum_name {
#[inline]
fn is_none(val: &u32) -> bool { *val == #hole }
}

#[automatically_derived]
impl #wasm_bindgen::convert::OptionIntoWasmAbi for #name {
impl #wasm_bindgen::convert::OptionIntoWasmAbi for #enum_name {
#[inline]
fn none() -> Self::Abi { <::js_sys::Object as #wasm_bindgen::convert::OptionIntoWasmAbi>::none() }
fn none() -> Self::Abi { #hole }
}

#[automatically_derived]
impl #wasm_bindgen::convert::OptionFromWasmAbi for #name {
#[inline]
fn is_none(abi: &Self::Abi) -> bool { <::js_sys::Object as #wasm_bindgen::convert::OptionFromWasmAbi>::is_none(abi) }
impl #wasm_bindgen::describe::WasmDescribe for #enum_name {
fn describe() {
use #wasm_bindgen::describe::*;
inform(IMPORT_ENUM);
inform(#name_len);
#(inform(#name_chars);)*
inform(#invalid);
inform(#hole);
Davidster marked this conversation as resolved.
Show resolved Hide resolved
inform(#variant_count);
#(#describe_variants)*
}
}

#[automatically_derived]
impl From<#name> for #wasm_bindgen::JsValue {
fn from(obj: #name) -> #wasm_bindgen::JsValue {
#wasm_bindgen::JsValue::from(obj.to_str())
impl #wasm_bindgen::__rt::core::convert::From<#enum_name> for
#wasm_bindgen::JsValue
{
fn from(val: #enum_name) -> Self {
#wasm_bindgen::JsValue::from_str(
match val {
#(#variant_paths_ref => #variant_values,)*
#enum_name::__Invalid => #wasm_bindgen::throw_str("Converting an invalid import enum back to a string is currently not supported"),
daxpedda marked this conversation as resolved.
Show resolved Hide resolved
_ => unreachable!("All possible variants should have been checked")
Davidster marked this conversation as resolved.
Show resolved Hide resolved
}
)
}
}
}).to_tokens(tokens);
})
.to_tokens(tokens);
}
}

Expand Down
25 changes: 24 additions & 1 deletion crates/cli-support/src/descriptor.rs
Davidster marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ tys! {
EXTERNREF
NAMED_EXTERNREF
ENUM
IMPORT_ENUM
RUST_STRUCT
CHAR
OPTIONAL
Expand Down Expand Up @@ -67,7 +68,16 @@ pub enum Descriptor {
String,
Externref,
NamedExternref(String),
Enum { name: String, hole: u32 },
Enum {
name: String,
hole: u32,
},
ImportEnum {
name: String,
invalid: u32,
hole: u32,
Davidster marked this conversation as resolved.
Show resolved Hide resolved
variant_values: Vec<String>,
},
RustStruct(String),
Char,
Option(Box<Descriptor>),
Expand Down Expand Up @@ -156,6 +166,19 @@ impl Descriptor {
let hole = get(data);
Descriptor::Enum { name, hole }
}
IMPORT_ENUM => {
let name = get_string(data);
let invalid = get(data);
let hole = get(data);
let variant_count = get(data);
let variant_values = (0..variant_count).map(|_| get_string(data)).collect();
Descriptor::ImportEnum {
name,
invalid,
hole,
variant_values,
}
}
RUST_STRUCT => {
let name = get_string(data);
Descriptor::RustStruct(name)
Expand Down
91 changes: 91 additions & 0 deletions crates/cli-support/src/js/binding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -668,6 +668,96 @@ fn instruction(
}
}

Instruction::WasmToImportEnum { variant_values } => {
let index = js.pop();

// e.g. ["a","b","c"][someIndex]
let mut enum_val_expr = String::new();
enum_val_expr.push('[');
for variant in variant_values {
enum_val_expr.push_str(&format!("\"{variant}\","));
}
enum_val_expr.push(']');
enum_val_expr.push('[');
enum_val_expr.push_str(&index);
enum_val_expr.push(']');

js.push(enum_val_expr)
}

Instruction::OptionWasmToImportEnum {
variant_values,
hole,
} => {
let index = js.pop();

let mut enum_val_expr = String::new();
enum_val_expr.push('[');
for variant in variant_values {
enum_val_expr.push_str(&format!("\"{variant}\","));
}
enum_val_expr.push(']');
enum_val_expr.push('[');
enum_val_expr.push_str(&index);
enum_val_expr.push(']');

// e.g. someIndex === 4 ? undefined : (["a","b","c"][someIndex])
// |
// currently, hole = variant_count + 1
js.push(format!(
"{index} === {hole} ? undefined : ({enum_val_expr})"
))
}

Instruction::ImportEnumToWasm {
variant_values,
invalid,
} => {
let enum_val = js.pop();

// e.g. {"a":0,"b":1,"c":2}[someEnumVal] ?? 3
// |
// currently, invalid = variant_count
let mut enum_val_expr = String::new();
enum_val_expr.push('{');
for (i, variant) in variant_values.iter().enumerate() {
enum_val_expr.push_str(&format!("\"{variant}\":{i},"));
}
enum_val_expr.push('}');
enum_val_expr.push('[');
enum_val_expr.push_str(&enum_val);
enum_val_expr.push(']');
enum_val_expr.push_str(&format!(" ?? {invalid}"));

js.push(enum_val_expr)
}

Instruction::OptionImportEnumToWasm {
variant_values,
invalid,
hole,
} => {
let enum_val = js.pop();

let mut enum_val_expr = String::new();
enum_val_expr.push('{');
for (i, variant) in variant_values.iter().enumerate() {
enum_val_expr.push_str(&format!("\"{variant}\":{i},"));
}
enum_val_expr.push('}');
enum_val_expr.push('[');
enum_val_expr.push_str(&enum_val);
enum_val_expr.push(']');
enum_val_expr.push_str(&format!(" ?? {invalid}"));

// e.g. someEnumVal == undefined ? 4 : ({"a":0,"b":1,"c":2}[someEnumVal] ?? 3)
// |
// double equals here in case it's null
js.push(format!(
"{enum_val} == undefined ? {hole} : ({enum_val_expr})"
))
}

Instruction::MemoryToString(mem) => {
let len = js.pop();
let ptr = js.pop();
Expand Down Expand Up @@ -1377,6 +1467,7 @@ fn adapter2ts(ty: &AdapterType, dst: &mut String) {
AdapterType::NamedExternref(name) => dst.push_str(name),
AdapterType::Struct(name) => dst.push_str(name),
AdapterType::Enum(name) => dst.push_str(name),
AdapterType::ImportEnum(name) => dst.push_str(name),
AdapterType::Function => dst.push_str("any"),
}
}
26 changes: 26 additions & 0 deletions crates/cli-support/src/wit/incoming.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,16 @@ impl InstructionBuilder<'_, '_> {
&[AdapterType::I32],
);
},
Descriptor::ImportEnum { name, variant_values, invalid, .. } => {
self.instruction(
&[AdapterType::ImportEnum(name.clone())],
Instruction::ImportEnumToWasm {
variant_values: variant_values.clone(),
invalid: *invalid,
},
&[AdapterType::I32],
);
},
Descriptor::Ref(d) => self.incoming_ref(false, d)?,
Descriptor::RefMut(d) => self.incoming_ref(true, d)?,
Descriptor::Option(d) => self.incoming_option(d)?,
Expand Down Expand Up @@ -296,6 +306,22 @@ impl InstructionBuilder<'_, '_> {
&[AdapterType::I32],
);
}
Descriptor::ImportEnum {
name,
variant_values,
invalid,
hole,
} => {
self.instruction(
&[AdapterType::ImportEnum(name.clone()).option()],
Instruction::OptionImportEnumToWasm {
variant_values: variant_values.clone(),
invalid: *invalid,
hole: *hole,
},
&[AdapterType::I32],
);
}
Descriptor::RustStruct(name) => {
self.instruction(
&[AdapterType::Struct(name.clone()).option()],
Expand Down
Loading