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

Provide an API in dedicate worker for executing event loop #10596

Open
burningtnt opened this issue Sep 2, 2024 · 8 comments
Open

Provide an API in dedicate worker for executing event loop #10596

burningtnt opened this issue Sep 2, 2024 · 8 comments
Labels
addition/proposal New features or enhancements needs implementer interest Moving the issue forward requires implementers to express interest topic: workers

Comments

@burningtnt
Copy link

What problem are you trying to solve?

Some of the codes require extremely high performance. Therefore, these codes will use Web Worker and make their codes like this:

state = "A_STATE"
while (true) {
    switch (state) {
        ...
    }
}

Therefore, this Web Worker will never release the CPU and give the browser a chance to execute the event loop, which means, dozens of API, including self.onmessage, fetch, OffscreenCanvas cannot being used in this context.

What solutions exist today?

For the developers of these applications, they will use SharedArrayBuffer to receive the messages from the main thread, as this API provide a way to transfer messages without releasing the CPU.

They even use the XHR request and set sync=true to send a request to a ServiceWorker and transfer things back by catching this network request.

Some people may argue that why they don't refactor there codes to something like this:

let step = 0;
let executePart = () => { ... }

setTimeout(function myself() {
    executePart()
    
    setTimeout(myself)
}, 0)

These codes will reduce the performance as each executePart will wait about 4 - 10 ms before getting execute.

How would you solve it?

Adding an API like processEventLoop() for WebWorker only.

This API can be invoked in any place in WebWorker. Once it's invoked, the browser will pause this JS code and execute things including promise, setTimeout, event handle (onmessage, ...).

The codes will be like:

let queue = []
self.onmessage = (ev) => { queue.push(...) }

while (true) {
    for (let ev of queue) {
        ...
    }
    
    execute();
    
    processEventLoop()
}

This API can also take one argument, which is a promise, like let result = processEventLoop(fetch( ... )).
This is like await, but it's not required to invoked in a async function.
(The problem that why we don't use async function is that this will also reduce performance)

Anything else?

These API are strange because they provide another way to execute a promise. It's like OffscreenCanvasContext2D.commit(), as this provides a solution without release the CPU.

@burningtnt burningtnt added addition/proposal New features or enhancements needs implementer interest Moving the issue forward requires implementers to express interest labels Sep 2, 2024
@Kaiido
Copy link
Member

Kaiido commented Sep 3, 2024

I'm not sure about the benefits from this seemingly synchronous API. The code will be paused, the JS execution will halt, the various queued tasks might be themselves executing async scripts / queuing new microtasks etc.
To me it sounds like a simple await scheduler.postTask(() => {}, { priority: "user-visible" })1 would achieve the same, even in terms of performances, without breaking the whole event-loop concept.

But if we go back to the use-cases, it seems that indeed a recurring issue is that there is no way to intercept messages from the main thread synchronously. Maybe that should be tackled on its own. For instance, we could imagine an API that would expose the queued messages synchronously, along with a way to flush them synchronously. Though that still seems very exotic, I guess this is more workable with than breaking the event-loop concept.

I'm not sure about the setTimeout case though, if one runs such a busy loop, they'll probably implement their own timer if needed.

Also note that ctx.commit() has been removed, it's now expected that the committing of an OffscreenCanvas to its placeholder <canvas> is done in the Worker's update the rendering.

Footnotes

  1. In case one wants to let only update the rendering execute, then "user-blocking" might be more appropriate.

@burningtnt
Copy link
Author

burningtnt commented Sep 3, 2024 via email

@Kaiido
Copy link
Member

Kaiido commented Sep 3, 2024

The 4ms delay is for reentrant timer tasks, i.e. for setTimeout (after 4 to 6 nesting levels).

Awaiting a microtask also takes some time, I'm not denying it, but it will be in µs not ms. My point is that your proposal would also execute tasks and microtasks, and pause / switch / resume the execution scripts, which will take the same amount of time as if you just awaited the queued task. I don't think this proposal avoids what actually costs performance.

@burningtnt
Copy link
Author

burningtnt commented Sep 3, 2024

The 4ms delay is for reentrant timer tasks, i.e. for setTimeout (after 4 to 6 nesting levels).

Awaiting a microtask also takes some time, I'm not denying it, but it will be in µs not ms. My point is that your proposal would also execute tasks and microtasks, and pause / switch / resume the execution scripts, which will take the same amount of time as if you just awaited the queued task. I don't think this proposal avoids what actually costs performance.

The problem that why I don't want to use await promiseInstance is that this will make a function async, which means it will return a Promise<...> and this kind of 'async pollution' will 'pollute' all the invokations and make every all the invokations on this stack trace waste times when awaiting the function.

let function0 = async () => { await ... }
let function1 = async () => {
    ...
    let res = await function0();
    
    ... // consume the ressult
}
let function2 = async () => {
    ...
    let res = await function1();
    ...
}

This structure will make the JS engine halt for three times when invoking function2. In the real situation, this might cause much more pausing and resuming process and reduce performance.

What about providing a dedicated await that only await the provided promise? like awaitPromiseSync(promise)? The JS engine will only execute the codes bind to this promise or wait this native promise returned by something like fetch instead of executing the event loop, other promises.

By the way, the problem here is not simply access the event queue from the main thread.
It's about how to await a promise in high performance code.

@burningtnt
Copy link
Author

OK. I found this API has already been provided but it doesn't belong to any API Standard.

It's importScripts. I tested it on Google Chrome, Firefox and Safari. All of their JS engine actually run the promises in the event loop.

Maybe this is a bad implementation because it will take about 1ms and greatly relies on the implementation.

@burningtnt burningtnt closed this as not planned Won't fix, can't repro, duplicate, stale Sep 6, 2024
@burningtnt
Copy link
Author

Well after testing I found that importScript will run the codes but the state of any promise won't change :(

@burningtnt burningtnt reopened this Sep 6, 2024
@pshaughn
Copy link
Contributor

pshaughn commented Sep 7, 2024

I believe doing this to MessageEvents would need major changes to the specification of Javascript's event model itself. ECMA TC39 would be the venue for those changes, not WHATWG.

Within the scope of APIs that WHATWG could standardize, this seems like it could be a situation that calls for a different type of inter-thread communication object entirely, unconnected to the existing message or stream APIs, with mostly SharedArrayBuffer-like semantics but a "queue" shape instead of an "array" shape.

For a finite queue depth of easily serialized/deserialized messages, a SharedArrayBuffer can already be used for this. It seems to me that the specific cases that would need something more are two: (1) you can't calculate and reserve the maximum queue capacity in advance, or (2) serializing the messages into and out of bytes would be unacceptable overhead compared to structured cloning. I can imagine situations involving video frames where both of these cases are true simultaneously.

@burningtnt
Copy link
Author

I believe doing this to MessageEvents would need major changes to the specification of Javascript's event model itself. ECMA TC39 would be the venue for those changes, not WHATWG.

Within the scope of APIs that WHATWG could standardize, this seems like it could be a situation that calls for a different type of inter-thread communication object entirely, unconnected to the existing message or stream APIs, with mostly SharedArrayBuffer-like semantics but a "queue" shape instead of an "array" shape.

For a finite queue depth of easily serialized/deserialized messages, a SharedArrayBuffer can already be used for this. It seems to me that the specific cases that would need something more are two: (1) you can't calculate and reserve the maximum queue capacity in advance, or (2) serializing the messages into and out of bytes would be unacceptable overhead compared to structured cloning. I can imagine situations involving video frames where both of these cases are true simultaneously.

Any suggestion for the place to submiting a feature request? Sorry I'm new to here :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
addition/proposal New features or enhancements needs implementer interest Moving the issue forward requires implementers to express interest topic: workers
Development

No branches or pull requests

3 participants