rustwasm / wasm-bindgen

Facilitating high-level interactions between Wasm modules and JavaScript
https://rustwasm.github.io/docs/wasm-bindgen/
Apache License 2.0
7.75k stars 1.07k forks source link

`async` `yield` function #3359

Closed emilk closed 1 year ago

emilk commented 1 year ago

I have a task where I need to decode a bunch of messages from a stream. Every 10ms I want to yield to my UI task (running on requestAnimationFrame). My code is something like this:

async fn decode_in_chunks(
    mut decoder: Decoder,
    process: Box<dyn Fn(Message)>,
) {
    let mut last_yield = instant::Instant::now();

    for msg in decoder {
        process(msg);

        if last_yield.elapsed() > instant::Duration::from_millis(10) {
            // yield to the ui task
            yield_().await;
            last_yield = instant::Instant::now();
        }
    }
}

I spawn this with wasm_bindgen_futures::spawn_local

The problem is the yield_ function. I've currently defined it as so:

/// Yield to other tasks
async fn yield_() {
    sleep_ms(1).await;
}

// Hack to get async sleep on wasm
async fn sleep_ms(millis: i32) {
    let mut cb = |resolve: js_sys::Function, _reject: js_sys::Function| {
        web_sys::window()
            .unwrap()
            .set_timeout_with_callback_and_timeout_and_arguments_0(&resolve, millis)
            .expect("Failed to call set_timeout");
    };
    let p = js_sys::Promise::new(&mut cb);
    wasm_bindgen_futures::JsFuture::from(p).await.unwrap();
}

this feels very hacky, for obvious reasons.

Any suggestions for how to improve this?

daxpedda commented 1 year ago

Doesn't the usual Rust async yield work?

struct Yield {
    yielded: Option<bool>,
}

impl Yield {
    fn new() -> Self {
        Self {
            yielded: Some(false),
        }
    }
}

impl Future for Yield {
    type Output = ();

    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        let yielded = self.yielded.unwrap();

        if yielded {
            self.yielded = None;
            Poll::Ready(())
        } else {
            self.yielded = Some(true);
            cx.waker().wake_by_ref();
            Poll::Pending
        }
    }
}

Also it seems that the future you are spawning here is really blocking. Instead of putting a timer here I believe you should just yield in-between every call to process(), presumably the function that you can't make non-blocking, making this basically an async iterator/stream.

emilk commented 1 year ago

@daxpedda unfortunately that yield doesn't work at all for me. It compiles, but my async task seems to get stuck.

daxpedda commented 1 year ago

Ah, apologies, my yield example has a bug, it doesn't wake the Waker. Now that I think about it, it kinda depends on how the executor works. I fixed it in my original post.

Will play around with it myself and see if it works.

daxpedda commented 1 year ago

Alright, this doesn't seem to work at all. I'm guessing the wasm-bindgen-futures executor simply rechecks if something is ready again right away.

daxpedda commented 1 year ago

So after some testing basically my findings are that you can yield, but not how you want it. Basically you yield to the executor. So you can multiplex multiple futures by yielding, but you can't yield back to the "native" runtime because it's not part of the executor.

Obviously web workers would be the perfect solution here, unfortunately the ecosystem around that is not exactly in a good state. I can't come up with a better solution then yours otherwise.

emilk commented 1 year ago

Thanks @daxpedda, that at least clears things up a little bit for me!

daxpedda commented 1 year ago

I actually found two viable solutions for this in the meantime:

  1. window.requestIdleCallback() which can be used to do exactly what you want here. Just let the future sleep and wake it up in the callback. Though this is unsupported in Safari (Bugzilla).
  2. While looking for Safari polyfills, I found that using setTimeout() with a delay of 0 can also do the job:

    If this parameter is omitted, a value of 0 is used, meaning execute "immediately", or more accurately, the next event cycle.

    Unfortunately nested timeouts will prevent this behavior and instead delay it by 4ms.

daxpedda commented 1 year ago

I discovered even more in the meantine:

Both could do what you want and much more, really interesting developments.