-
Notifications
You must be signed in to change notification settings - Fork 32
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
Calls and execution model #391
Comments
Very interesting thought @josevalim ! I like your idea of explicitly passing the answer-to pid down instead of just assuming Let me think out loud: Currently we send a hardcoded
tuple back to We could change the signature like this - call_exported_function(store_or_caller, instance, name, params, from)
+ call_exported_function(store_or_caller, instance, name, params, {answer_to_pid, tag, forward_term}) So, in my Wasmex GenServer I'd call What do you think?
True - I think! To be honest, I don't know how
Good question! Your understanding is correct, you could call an instance more than once while it still processes old calls. Concurrent calls would access the same IO buffers and, maybe worse, the same memory. This is why the Wasmex Genserver blocks and waits for WASM to return. It's a safe API for general use only because we block. To my understanding WASM operates under the assumption of single threads. It lacks multi-threading features (like atomics, to implement mutexes). I haven't tried, but I imagine one could safely call into WASM functions concurrently if they don't access shared state (e.g. they are purely functional). That's a pretty strict limitation, though, with a single shared memory and shared IO. There is the WASM threads proposal to improve this situation. With our recent (still unreleased) work to expose wasmtimes compilation flags, it would be easy to provide a flag to enable wasmtimes implementation of the threads proposal. It would be the WASM binaries job to implement their functions thread safe. Unfortunately the thread proposal isn't fully implemented in wasmtime yet - they say:
|
That's the trick: we don't need the atom. If we allow
However, if the instance needs to be wrapped in a GenServer, then we cannot skip the GenServer. I think think it is still worth a change for a cleaner API, but not many benefits beyond that.
I think we are blocking the client, but not the server. Therefore the current implementation would still allow multiple invocations. You would need a Oh, let me ask another question: I was wondering if it would be possible for someone to call a Phoenix.PubSub function directly from wasm. :) Can we pass strings around? |
Awesome! I like the proposed
Ha! [insert homer simpson d'oh! sound here]. Thanks, and I would love to review a PR implementing the proposed queue. Actually, for both changes, I'm also happy to implement them. It really comes down to how much time you want to spend and if it's fun to you :) I don't want to waste your precious time - it's my bug after all.
I like where this goes! :) You totally can using bytes = File.read!("pubsub.wasm")
imports = %{env: %{
"send_pubsub" => {:fn, [], [], fn _context -> elixir_fn_sending_things() end}
}}
{:ok, pid} = Wasmex.start_link(%{bytes: bytes, imports})
Wasmex.call_function(pid, "_start", []) See https://hexdocs.pm/wasmex/Wasmex.Instance.html#new/3-example for the docs. The WASM binary could call that imported function I'm happy to help with further examples or links to wasmex tests, collaborate on your test code, or have a call or whatever helps :) Let me note that yielding execution from Rust to Elixir isn't the most performant thing on earth. Rust needs to send an Erlang message to the Wasmex GenServer, which looks up the fn, executes it and calls back into Rust so it can continue WASM execution. But there is a much nicer and much more performant future ahead :) One of my long term goals with Wasmex is to provide helper functions written in Rust for Elixir interop. I want Elixir and WASM to work together as nicely as e.g. JS and WASM works. These helper functions would be provided as rust-based imported functions similar to how we have all WASI function written in rust being provided to WASM with a simple These rust elixir helper functions could do things like sending messages to pids (and much more, like creating elixir types, or serializing/deserializing elixir types from/to WASM). I'm currently building the fundamentals to that vision with Wasmex, but could give it an early start if you have a wish for such a helper function :) Now I have a question :) What's your expectation on implementation speed on these things? Depending on stress at work and life I might not be the fastest implementor. But I'm happy to open the repo up to more collaborators or just do it myself at my own pace. Don't get me wrong, I'm super hyped. Just want to get and set expectations. |
I did this 100x already. So it will be quick to draft something!
No speed. I am just curious about exposing the Erlang VM and the Phoenix ecosystem to WASM developers, so they can leverage things like Distributed PubSub and Presence. One of the things that would be nice is to make WASM preemptive, which I believe we can do with fuel. This way we could fully have WASM actors. However, this brings another question about the WASM implementation. I believe each call runs in a separate Rust thread. Is that a OS thread or a green thread? I think we can have three options here:
Something else to consider is if you can control how WASM allocates memory. If you can use Finally, I am wondering if it is worth introducing the concept of ImportMap to Wasmer, so you can define the imports only once and have them pre-allocated in Rust. This way you don't have to allocate on each new instance. Sorry if this is a lot but there is absolutely no rush. I am just excited about the model. |
💜
correct! let me cite the relevant wasmtime doc
We don't run on an async config yet - to make that happen we must switch to async rust which might be a mid-sized rewrite, but sounds like a good next step. But
correct! currently it is a OS thread. Do you prefer another approach? an OS thread is "hidden" from Erlang instrumentation, where an Erlang-managed thread might be easier to debug if things go wrong 🤔
I believe I could change how (not how much - that already works using Wasmex.StoreLimits.memory_size) wasmtime creates memory by having a custom MemoryCreator, but I need to do more research here. Good point!
I think this could be a nice and reasonable optimization. I haven't done much benchmarking and performance optimizations yet, so I bet there are many more such things. We should probably start by having benchmarks first, to be able to see the impact of such change. I really love all your suggestions, questions, and ideas. I'll go through this discussion in a quiet minute and create tickets out of it. |
Ok, so to recap:
I am loving the back and forth too! <3 |
The thread would eventually terminate and (as normal) attempt to send the Erlang message with its return value. I believe
|
Alright! So we should hold with the ideas in this PR ( |
Alright, I gave approaches 2. and 3. some thinking and both are doable with different tradeoffs. I'm very interested in your experience @josevalim on what is better - do you see pros/cons I missed? Which would you think scales better to ~10k concurrent calls? Separate Erlang processes for blocking NIF callsEvery potentially blocking call (Module compilation, WASM function calls) run in their own Erlang process. They call into a dirty NIF function, blocking the Erlang process. To be able to call "imported functions" (Elixir implemented functions that WASM can call into) each Pros:
Cons:
Async RustNIF calls never block because we handle any incoming call asynchronously in Rust. Requires our Rust code to "swallow the complexity" Pros:
Cons:
|
Thanks for the analysis. Honestly, at the moment, I can't say one approach is better than the other. But I do have one question: is there any documentation of what is needed for the integration between wasmtime and fuel/epochs? Do they fully base themselves on the async Rust API? I am asking because we could perhaps make an async Rust backend based on enif_schedule_nif. I agree with @hansihe's comment on Rustler that |
WHAOR @josevalim I'm sorry. I just saw that I accidentally ghosted you for almost two years 😅 💔 Good news is that the time brought some news. I believe the route to go async Rust is not feasible. I gave it a few honest attempts and called for Rust help but it looks like the mix of async, Rust memory-ownership, and NIF calls broke me enough to stick with the current OS threads model.. In the meantime Wasm moved to a new approach (Wasm To all the points you raised earlier: Most are actually still relevant good ideas - especially having a queue for calls to the server in case the client is busy. I'll see that we can implement this idea for components from the start 👀 |
Hi @tessi! Great project!
My initial suggestion was around the
call_exported_function
. At the moment, you are passingfrom
and returning to the caller, but I would consider accepting two arguments insteadpid
and a opaque term calledtag
. And you will reply like this:This way, you could do this inside the GenServer:
And still match on
{{:exported_return, from}, result}
inhandle_info
. But by breaking it apart, then you could also allow someone to call directly into the instance and get the reply directly, without going to the GenServer.In fact, you could keep the same API as today, because
from
is already a{pid, tag}
format, and reply from the NIF as:And that means you would skip sending the result to the GenServer.
However, as I was thinking about this, I started wondering what happens on concurrent calls in an instance. Because calls are non-blocking, today it is possible to call the same instance more than once. What happens on concurrent calls? What happens if those calls read from stdin or write to stdout? Do they use the same buffer or is it per call?
Thanks! <3
The text was updated successfully, but these errors were encountered: