Skip to content

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

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

async yield function #3359

Closed
emilk opened this issue Mar 16, 2023 · 8 comments
Closed

async yield function #3359

emilk opened this issue Mar 16, 2023 · 8 comments
Labels

Comments

@emilk
Copy link

emilk commented Mar 16, 2023

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
Copy link
Collaborator

daxpedda commented Mar 17, 2023

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
Copy link
Author

emilk commented Mar 19, 2023

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

@daxpedda
Copy link
Collaborator

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
Copy link
Collaborator

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
Copy link
Collaborator

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
Copy link
Author

emilk commented Mar 21, 2023

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

@daxpedda
Copy link
Collaborator

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
Copy link
Collaborator

daxpedda commented Apr 8, 2023

I discovered even more in the meantine:

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

@daxpedda daxpedda closed this as not planned Won't fix, can't repro, duplicate, stale May 12, 2023
@rustwasm rustwasm locked and limited conversation to collaborators Jun 9, 2023
@daxpedda daxpedda converted this issue into discussion #3476 Jun 9, 2023

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

Labels
Projects
None yet
Development

No branches or pull requests

2 participants