Skip to content
This repository has been archived by the owner on May 9, 2022. It is now read-only.

TY-1637 Add bindgen for wasm/js #51

Merged
merged 29 commits into from
Apr 30, 2021
Merged

Conversation

Robert-Steiner
Copy link
Contributor

@Robert-Steiner Robert-Steiner commented Apr 21, 2021

first implementation of wasm bindings

Prerequisites

  • wasm-pack
  • rustup target add wasm32-unknown-unknown

Build

wasm-pack build

Test

wasm-pack test --node

you can also run cargo expand ai if you are interested to see what wasm-bindgen does on the rust side

Run it in the browser

ln -s $(pwd)/../data/ $(pwd)/data
wasm-pack build --target web --release
python3 -m http.server

Profiling

with wasm-pack build --target web --profiling you can create an optimised build with debug-info turned on that will allow you to profile the execution of the wasm module using the build-in browser developer tools

Chrome
Screen Shot 2021-04-22 at 11 06 56

Firefox
Screen Shot 2021-04-22 at 11 17 52

catch_unwind

Catching panics is not possible at the current stage of development. The default panic strategy for wasm32-unknown-unknown is PanicStrategy::Abort but it also seems that it is the only possible value.

I have tested it with panic="unwind" and panic="abort" and the result was the same. The closure in catch_unwind was never executed.

So what will happens in case of a panic?

In case of a panic, the process aborts, which causes a trap in WASM which in turn leads to a WebAssembly.RuntimeError which can be caught on the JS side with try\catch.

Summarized:

Rust Panic -> Abort -> WASM trap -> WASM RuntimeError -> catch on JS side via try\catch

It is important to know that we can continue to use the WASM module, however it might be faulty.

Link

What can we still do?

  • we can't catch panics at the moment but we can at least print the stack trace to the console by using console_error_panic_hook. Without it, we only get a runtime error which does not contain any useful information.

Screen Shot 2021-04-26 at 10 23 00

More Links

Error handling

In the event of a panic, a WebAssembly.RuntimeError is thrown, which can be caught using a try/catch on the JS side. (Not sure if we can do something similar in Dart)

try {
	ai = new WXaynAi(vocab_buffer, model_buffer, undefined);
}
catch (e) {
	if (e instanceof WebAssembly.RuntimeError) {
		// Panic
	} else {
		...
	}
}

I used wasm-tracing-allocator to check for memory leaks after a panic and yes, there is memory that has not been freed. As suggested in we should in this case recreate the wasm instance, however, I couldn't find any resources describing how to achieve this. Also I couldn't find anything about how to handle a WebAssembly.RuntimeError in general. (I'll ask in the rust discord)

In any case, calling ai.free() in the catch block doesn’t seem to be the solution:

Uncaught (in promise) Error: recursive use of an object detected which would lead to unsafe aliasing in rust
    at imports.wbg.__wbindgen_throw (xayn_ai_ffi_wasm.js:357)
    at wasm_bindgen::throw_str::h0f1c6b79f7e0b42b (:8000/pkg/xayn_ai_ffi_wasm_bg.wasm:wasm-function[6172]:0x47f1de)
    at wasm_bindgen::__rt::borrow_fail::h9eaf8de49d649f35 (:8000/pkg/xayn_ai_ffi_wasm_bg.wasm:wasm-function[6171]:0x47f1d3)
    at __wbg_wxaynai_free (:8000/pkg/xayn_ai_ffi_wasm_bg.wasm:wasm-function[381]:0x1f4104)
    at WXaynAi.free (xayn_ai_ffi_wasm.js:175)
    at run ((index):39)

Nonetheless, I added console_error_panic_hook so that we can at least see the stack trace in the console.

Besides the WebAssembly.RuntimeError we have to handle our error type (ExternError). It is automatically deserialized into a JS object, so we can directly access the code and message fields.

{
	code: 4, 
	message: "Failed to initialize the ai: Failed to build the m…decode Protobuf message: unexpected end group tag"
}

TODO:

  • tests
  • documentation

some useful links:

@acrrd
Copy link
Contributor

acrrd commented Apr 22, 2021

Regarding error types would be nice to have a type similar to ExternalError that looks like { code: i32, msg: String } and then convert that to a JsValue. We can later also try to have CCode in common between the two so that we can share all the conversion code in dart.

@Robert-Steiner
Copy link
Contributor Author

Regarding error types would be nice to have a type similar to ExternalError that looks like { code: i32, msg: String } and then convert that to a JsValue. We can later also try to have CCode in common between the two so that we can share all the conversion code in dart.

good idea, I will take a look today what we can do

Copy link
Contributor

@janpetschexain janpetschexain left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this looks much slimmer than the c ffi 😀

currently the functions are stand-alone, if i understood correctly we can even put them into a plain old impl WXaynAi {} block and even refer to &self as usual (with some additional wasm-bindgen attribute usage).

xayn-ai-ffi-wasm/src/utils.rs Outdated Show resolved Hide resolved
xayn-ai-ffi-wasm/src/utils.rs Outdated Show resolved Hide resolved
xayn-ai-ffi-wasm/src/ai.rs Outdated Show resolved Hide resolved
xayn-ai-ffi-wasm/src/ai.rs Outdated Show resolved Hide resolved
xayn-ai-ffi-wasm/src/ai.rs Outdated Show resolved Hide resolved
@Robert-Steiner
Copy link
Contributor Author

this looks much slimmer than the c ffi 😀

currently the functions are stand-alone, if i understood correctly we can even put them into a plain old impl WXaynAi {} block and even refer to &self as usual (with some additional wasm-bindgen attribute usage).

Good point👍 I moved them into the impl block of WXaynAi

@Robert-Steiner
Copy link
Contributor Author

Regarding error types would be nice to have a type similar to ExternalError that looks like { code: i32, msg: String } and then convert that to a JsValue. We can later also try to have CCode in common between the two so that we can share all the conversion code in dart.

What we can do is serialising ExternError into a js object and deserialise it on the dart side into a XaynAiException. This way we could reuse the CCode + some of the dart error code.

Screen Shot 2021-04-22 at 18 58 53

@acrrd
Copy link
Contributor

acrrd commented Apr 23, 2021

What we can do is serialising ExternError into a js object and deserialise it on the dart side into a XaynAiException. This way we could reuse the CCode + some of the dart error code.

I'm not sure about this. ExternError has a pointer, so we will need to serialize it and then manually release the memory. Maybe we can have a diffferent version ExternError in the wasm crate that use a String. Not sure how much code we can reuse at the moment apart from the error code because with the C ffi we need to call free, while with wasm the data that we return will be owned by dart.

@acrrd
Copy link
Contributor

acrrd commented Apr 23, 2021

this looks much slimmer than the c ffi grinning
currently the functions are stand-alone, if i understood correctly we can even put them into a plain old impl WXaynAi {} block and even refer to &self as usual (with some additional wasm-bindgen attribute usage).

Good point+1 I moved them into the impl block of WXaynAi

Please check if this will create a javascript object and methods on it instead of functions. Would be nice to have the same way of calling the functions from dart for the ffi so that in the future we can try to unify some code and hopefully have an abstract ffi layer that the rest of lib can use.

@Robert-Steiner
Copy link
Contributor Author

What we can do is serialising ExternError into a js object and deserialise it on the dart side into a XaynAiException. This way we could reuse the CCode + some of the dart error code.

I'm not sure about this. ExternError has a pointer, so we will need to serialize it and then manually release the memory. Maybe we can have a diffferent version ExternError in the wasm crate that use a String. Not sure how much code we can reuse at the moment apart from the error code because with the C ffi we need to call free, while with wasm the data that we return will be owned by dart.

Sorry, I wan't clear here. I didn't mean the ffi_support::ExternError, but a structure in wasm that has the same name like you wrote here

Maybe we can have a diffferent version ExternError in the wasm crate that use a String

#[derive(Serialize)]
pub struct ExternError {
   code: i32,
   message: String,
}

Not sure how much code we can reuse at the moment apart from the error code because with the C ffi we need to call free, while with wasm the data that we return will be owned by dart.

true but my hope is that we can (in wasm) return an ExternError and deserialize it into a XaynAiException

@Robert-Steiner
Copy link
Contributor Author

Please check if this will create a javascript object and methods on it instead of functions.

It indeed creates a JS object but i think it's worth it because it makes the api and code simpler.

Would be nice to have the same way of calling the functions from dart for the ffi so that in the future we can try to unify some code and hopefully have an abstract ffi layer that the rest of lib can use.

I'm not sure if this would bring us any advantages if the wasm API is similar to the c ffi, since in wasm we don't have to pass the external error, but instead can use the result type. The Wasm API is more similar to the API from CXaynAi. But I'm open to change it if you want.

@Robert-Steiner Robert-Steiner force-pushed the TY-1637-wasm-bindgen branch 2 times, most recently from 0d7ec57 to a5ad9ed Compare April 28, 2021 12:36
xayn-ai-ffi-wasm/src/error.rs Outdated Show resolved Hide resolved
@@ -0,0 +1 @@
/Users/robert/projects/xayn-ai/xayn-ai-ffi-wasm/../data/
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

even though this is a symlink file, it seems to resolve to your local machine. do you know if this is just a git artifact or do we need to set up the symlink differently?

when i try to follow the symlink locally, after checking out the branch on my machine, it says that the path is invalid, which probably hints at an incorrect symlink setup.

Copy link
Contributor Author

@Robert-Steiner Robert-Steiner Apr 28, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've removed it. I added it to commit by mistake. I included the command to generate the symlink in the readme

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sorry, this wasn't meant to be included in the review, i saw that you made changes to this file while i was reviewing the pr and i thought my comment was removed.

is saw that you temporarily used relative paths in get_data("../../data/etc"), it seems this didn't work out?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is possible, but we then have to start the web server in the root of the repo.
I added a git-friendly symlink to the repo like @acrrd suggested :)

xayn-ai-ffi-wasm/src/ai.rs Show resolved Hide resolved
xayn-ai-ffi-wasm/src/ai.rs Outdated Show resolved Hide resolved
}

// #[wasm_bindgen_test]
// fn test_model_invalid() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i guess we could set this test up similarly to test_vocab_invalid(), couldn't we?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes but it does not fail if we pass an empty array :/
Should we create a ticket to fix this?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that's definitely worth a ticket. i guess it might produce some "default empty" model in tract, which we probably don't want to allow in our pipeline. in the end we need more tests for the builder in xayn-ai.

}
catch (e) {
if (e instanceof WebAssembly.RuntimeError) {
// Panic
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since you said we can't use ai.free() in here, it seems to me that the only safe way to ensure that no memory is leaked on a panic, is to run this code in a separate thread/isolate and to kill it on panic. but this is quite coarse, maybe we can find a more sensitve solution, i'm not sure though given the current state of development of wasm.

Copy link
Contributor Author

@Robert-Steiner Robert-Steiner Apr 29, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The part isn't clear to me either. I'm not sure if it is fine just to recreate the wasm model via await init(); or if we actually need to reload the website. Maybe it's worth creating a ticket and diving deeper into this topic.

is to run this code in a separate thread/isolate and to kill it on panic.

I'm not sure if a dart isolate will produce something similar like an isolate on the js side. I think dart will just look for the function name in the window object and call that if it exists.

https://github.com/xaynetwork/xayn_search/blob/ca352dca3ca5a89c26b4bc8a8b0dc4eac4cfe130/xayn_web/web/index.html#L40-L44

But I could be wrong

Maybe we can utilise web workers for that.

Copy link
Contributor

@rustonaut rustonaut Apr 29, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Darts isolates are basically modeled after WebWorkers, partially because google initially intended it to be a language focused on transpilling to JS (by now it's more complex, but I think dart in browser still uses WebWorkers for Isolates).

Furthermore due to computations taking time the apps anyway run it in their own isolate (and with that thread) and the browser app should probably run it in a WebWorker (which it probably will do anyway as it runs it in a dart isolate transpilled to js I think).

But we probably don't want to "fully" kill it, instead it would be enough to kill it's "state" but not it's "code" instance (to reuse JITed code etc.). (Sorry I don't remember the proper WebAssembly terms.)

So I also think that it's best to open an issue and investigate it later one.

Especially I don't know the state of "moving" WebAssembly instances into isolates, we might need to make sure we only initiate it inside of the isolate/WebWorker, or it might just work 🤷.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the integration on mobile if a panic is thrown the isolate is shout down and recreated, we can have the same thing on wasm and WebWorks. On wasm is even more important to avoid memory leak because a web page can live long, while the app is closed more frequently. The app create us already in the isolate so don't have to care about that.

Copy link
Contributor

@rustonaut rustonaut Apr 29, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes.

Through it turns out dart doesn't automatically map Isolates to WebWorkers when sharing a code base
(through there are libraries which abstract over it).

Relevant parts would be:

Through if panics are not rare it should be better to clear state, e.g. by reruning WebAssembly.instantiate(...) but not rerun the compilation or worse download step. But that would be an optimization either way.

@Robert-Steiner
Copy link
Contributor Author

regarding "running tests in the browser", we just need to put wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); in the #[cfg(test)] module. The downside is that we can no longer run the tests in node. Only one of the two options is possible. All tests are passing in firefox, chrome and safari. (For safari i had to enable the WebDriver support via safaridriver --enable)

But i think running the tests in node is enough right now, wdyt?

@acrrd
Copy link
Contributor

acrrd commented Apr 28, 2021

Since we target a browser would be nice to have them running in a browser, especially if we can target all the main one; so if we can run them on the ci on the browser I'm voting for that.
If we want both I think a feature flag would be enough to enable the option in the code.

Do you know how reading the model is handled? was that automatic or you needed to do something?

@Robert-Steiner
Copy link
Contributor Author

Robert-Steiner commented Apr 28, 2021

Since we target a browser would be nice to have them running in a browser, especially if we can target all the main one; so if we can run them on the ci on the browser I'm voting for that.
If we want both I think a feature flag would be enough to enable the option in the code.

Alright I will change it

Do you know how reading the model is handled? was that automatic or you needed to do something?

The vocab + model are embedded in the wasm model via include_bytes!. I haven't found a better solution :(

@acrrd
Copy link
Contributor

acrrd commented Apr 28, 2021

Since we target a browser would be nice to have them running in a browser, especially if we can target all the main one; so if we can run them on the ci on the browser I'm voting for that.
If we want both I think a feature flag would be enough to enable the option in the code.

Alright I will change it

Do you think that we will gain something to test also on node?

Do you know how reading the model is handled? was that automatic or you needed to do something?

The vocab + model are embedded in the wasm model via include_bytes!. I haven't found a better solution :(

It's ok, it is only in test. It could be a good general solution if we don't want to have the app downloading the models in the future to reduce install size.

@Robert-Steiner Robert-Steiner marked this pull request as ready for review April 29, 2021 13:12
Copy link
Contributor

@janpetschexain janpetschexain left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm 👍


#[wasm_bindgen]
impl WXaynAi {
#[wasm_bindgen(constructor)]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think our current order of attributes is to first have the doc attribute and below that all other code related attributes.

pub fn new(
vocab: &[u8],
model: &[u8],
serialized: Option<Box<[u8]>>,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wherever we take a Box<T> or a Vec<T> in this api, it transfers ownership and as the docs state this thing has to be allocated properly before. depending on where the allocation is performed we could replace some of the owned arguments with references, but i guess it makes sense to revisit that case by case later on.

xayn-ai-ffi-wasm/src/ai.rs Outdated Show resolved Hide resolved
@acrrd acrrd self-requested a review April 29, 2021 14:52
## Running the example

```shell
ln -s $(pwd)/../data/ $(pwd)/example/data
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can have a relative symlink to data already in the repo.

}
}

#[cfg(target_arch = "wasm32")]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can group the two in one line

Suggested change
#[cfg(target_arch = "wasm32")]
#[cfg(test, target_arch = "wasm32")]

xayn-ai-ffi-wasm/src/error.rs Outdated Show resolved Hide resolved
xayn-ai-ffi-wasm/src/error.rs Outdated Show resolved Hide resolved
xayn-ai-ffi-wasm/src/utils.rs Outdated Show resolved Hide resolved
@acrrd acrrd self-requested a review April 29, 2021 18:54
@@ -0,0 +1,40 @@
# Xayn AI WASM
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can move this in the README in root of the project. In this or in a future PR. There there are already the info about android and ios there.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good idea 👍

@Robert-Steiner Robert-Steiner merged commit 7292b36 into master Apr 30, 2021
@Robert-Steiner Robert-Steiner deleted the TY-1637-wasm-bindgen branch April 30, 2021 13:26
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants