Delivering off-thread events to the isolate (porting from Spidermonkey)

64 views
Skip to first unread message

jmr

unread,
Apr 21, 2025, 12:41:21 PMApr 21
to v8-users
I'm porting an application that is using SpiderMonkey for embedding, and I have a few questions.

One of the things my application does is, it allows user (in javascript) to register for events:

func handler(...) { {}
registerEventHandler("evetName", handler);

In C++, I then store these handlers to be called later once the event arrives.

Once the event arrives (different thread than isolate is running on), I store it in a pending event list for the runtime (isolate), I then call JS_RequestInterruptCallback which interrupts the execution of the runtime (isolate), and drops into my C++ interrupt callback.

From my C++ callback, I check for pending events, and call the user provided functions (re-entering the isolate) delivering the events, and resume execution to the user.

Seems that with v8, I can get quite close to this, except isolate->RequestInterrupt does not allow re-entering the isolate/calling user code, making this not work.

I tried using EnqueueMicrotask but seems those are never delivered automatically unless the script stops, or I call PerformMicrotaskCheckpoint (which then ends up on the wrong, calling thread)

I tried posting a task to the platform (platform->GetForegroundTaskRunner(isolate)->PostTask), but given the script is long lived, I don't think this ever gets delivered.

I tried using Locker(isolate) + Isolate::Scope(isolate) + isolate->GetCurrentContext() from the event thread, and then calling the callbacks, but this crashes non-deterministically, sometimes with "Invoke in DisallowJavascriptExecutionScope".

Any guidance is appreciated.
Thanks.

jmr

unread,
Apr 21, 2025, 12:50:54 PMApr 21
to v8-users
Ok, seems that I CAN call js functions from within RequestInterrupt, things seem to work.

I guess I'm confused what this means in that case: "Registered |callback| must not reenter interrupted Isolate."

J Decker

unread,
Apr 21, 2025, 9:21:41 PMApr 21
to v8-u...@googlegroups.com
You will have to wait for the JS stack to be idle for events to get dispatched... even promises are only dispatched from an idle state in the JS stack (where it has returned fully).

You can call any JS functions in the initialization callback - but you cannot use the isolate or make a handle scope in not the right thread... so you have to wait for the uv_async_init'ed callback to be triggered before doing queued work.

If this isn't sufficient and you really need parallel or injected execution - for node there's a worker-thread module ; but the eventual results have to be dispatched at the idle level still...https://github.com/laverdet/isolated-vm for example... (this is also a C++ thread that deals directly with allocating and managing isolates)

you use `uv_async_init` once, which initialized a uv_async_t handle to be associated with a loop.  You then use uv_async_send( handle ); which triggers dispatching to that routine; you will have to create a handle scope in that callback before doing JS functions.  The handle can only be closed in the main thread also.   The work can be queued to internal tracking, and when the async callback runs, it just checks for outstanding work, and calls appropriate JS callbacks.

--
--
v8-users mailing list
v8-u...@googlegroups.com
http://groups.google.com/group/v8-users
---
You received this message because you are subscribed to the Google Groups "v8-users" group.
To unsubscribe from this group and stop receiving emails from it, send an email to v8-users+u...@googlegroups.com.
To view this discussion visit https://groups.google.com/d/msgid/v8-users/12e0876e-96b5-451f-8c88-11b4c4d8efd3n%40googlegroups.com.

J Decker

unread,
Apr 21, 2025, 9:26:21 PMApr 21
to v8-u...@googlegroups.com
On Mon, Apr 21, 2025 at 6:21 PM J Decker <d3c...@gmail.com> wrote:
You will have to wait for the JS stack to be idle for events to get dispatched... even promises are only dispatched from an idle state in the JS stack (where it has returned fully).

You can call any JS functions in the initialization callback - but you cannot use the isolate or make a handle scope in not the right thread... so you have to wait for the uv_async_init'ed callback to be triggered before doing queued work.

If this isn't sufficient and you really need parallel or injected execution - for node there's a worker-thread module ; but the eventual results have to be dispatched at the idle level still...https://github.com/laverdet/isolated-vm for example... (this is also a C++ thread that deals directly with allocating and managing isolates)

you use `uv_async_init` once, which initialized a uv_async_t handle to be associated with a loop.  You then use uv_async_send( handle ); which triggers dispatching to that routine; you will have to create a handle scope in that callback before doing JS functions.  The handle can only be closed in the main thread also.   The work can be queued to internal tracking, and when the async callback runs, it just checks for outstanding work, and calls appropriate JS callbacks.

By default - initializing handles for for uv_async_send to work - they add a reference that keeps (node) running (I see this isn't actually a node specific thread though)... you can async_unref( handle ) and that will keep it schedulable, but not hold the process open.  (may times like opening a server socket, you'd want the process to not exit until that socket is closed (which is probably never) but other situations might not be critical enough to hold the process - my GUI interface has gone this way and the window event dispatch don't actually keep the process open; but then I ended up with a keep-alive setTimeout()... Just thought it might be useful to note.

jmr

unread,
Apr 22, 2025, 2:24:32 AMApr 22
to v8-users
What does idle state mean?

The code my users run never terminates, i.e., it permanently spins waiting for events, so the stack depth never goes to 0.
None of the code is async, as the version of spidermonkey used did not have it.
They potentially call some native functions (sleep which literally does thread::sleep in C++) etc, but otherwise the code is non-async and never terminating, and always at non-zero stack depth.

This sounds like the isolate is never idle, hence there is no nice way to do this?

Surely chome somehow delivers events/setTimeout timers even if the users code has a while(true) {} ?
I assume node does it too somehow?

I guess I could check for events to deliver in every native function I expose, but that feels a bit yuck.
I could also continue doing it in the interrupt, the docs say don't do it, but it seems to work and nothing panics (I have not run very complicated code however, not sure at what point it breaks)

Thanks.

J Decker

unread,
Apr 22, 2025, 2:37:27 AMApr 22
to v8-u...@googlegroups.com
On Mon, Apr 21, 2025 at 11:24 PM jmr <audrius.b...@gmail.com> wrote:
What does idle state mean?

The code my users run never terminates, i.e., it permanently spins waiting for events, so the stack depth never goes to 0.
None of the code is async, as the version of spidermonkey used did not have it.
They potentially call some native functions (sleep which literally does thread::sleep in C++) etc, but otherwise the code is non-async and never terminating, and always at non-zero stack depth.

This sounds like the isolate is never idle, hence there is no nice way to do this?

the isolate-vm project has a execution timeout it can do - I'm not sure if that's only able to kill stuff - or if it's able to interrupt and interject code to run.
 

Surely chome somehow delivers events/setTimeout timers even if the users code has a while(true) {} ?
I assume node does it too somehow?

False.

In the console you can enter `setInterval( ()=>{console.log("tick");, 500 )`  and click the run button... should start generated console message.
You can then enter 'while( true);' and I got one more 'tick' but no further ticks, and can not enter any further commands.

 

I guess I could check for events to deliver in every native function I expose, but that feels a bit yuck.

I formulate everything as events and never poll, so this is a bit foreign to me.  Yes you will have to every once in a while poke the C++ code to do something if you're going to occupy the stack.
 
I could also continue doing it in the interrupt, the docs say don't do it, but it seems to work and nothing panics (I have not run very complicated code however, not sure at what point it breaks)

I had a feature in my code which would allow blocking a thread's execution, and call out to trigger the idle function (in node it's like  ProcessTickCallback() or something)... and was able to cause JS code to run on a deeper stack, but it was kind of brittle as I went on to build more with it, then I found I had to unwind further back to finish earlier functions; and just wasn't practical how I was doing it. 

There's nothing preventing you from running C++ code in the background on the non-main JS thread to get events, and then use uv_async_send to send those events, or otherwise trigger JS to run.... is it really JS level code that has to be compute bound?  Can't you like maybe interject a check the clock and every 250 milliseconds return to allow promises to resolve and other JS scheduled events?  .   

jmr

unread,
Apr 22, 2025, 2:50:44 AMApr 22
to v8-users
> There's nothing preventing you from running C++ code in the background on the non-main JS thread to get events, and then use uv_async_send to send those events, or otherwise trigger JS to run.... is it really JS level code that has to be compute bound? Can't you like maybe interject a check the clock and every 250 milliseconds return to allow promises to resolve and other JS scheduled events? .   


Sure, but how? Given the js code is written by users and out of my control. I can deliver events from a different thread or via uv loop is doesn't matter, the issue I have is that I have to interrupt the isolate that is running to do it, and we're saying there is no way to do that and leave the isolate in a safe state.

The project you linked just uses v8 apis, so if there is no API in v8 to do it, I'm not sure how the ptoject does that, or I'm misunderstanding the docs.

Ben Noordhuis

unread,
Apr 22, 2025, 3:56:21 AMApr 22
to v8-u...@googlegroups.com
On Tue, Apr 22, 2025 at 8:50 AM jmr <audrius.b...@gmail.com> wrote:
>
> > There's nothing preventing you from running C++ code in the background on the non-main JS thread to get events, and then use uv_async_send to send those events, or otherwise trigger JS to run.... is it really JS level code that has to be compute bound? Can't you like maybe interject a check the clock and every 250 milliseconds return to allow promises to resolve and other JS scheduled events? .
>
>
> Sure, but how? Given the js code is written by users and out of my control. I can deliver events from a different thread or via uv loop is doesn't matter, the issue I have is that I have to interrupt the isolate that is running to do it, and we're saying there is no way to do that and leave the isolate in a safe state.

Yes, that won't work with V8. If your JS code never returns, it's
effectively hung.

RequestInterrupt() is specifically designed to be used in conjunction
with TerminateExecution(), i.e., to kill JS that hangs/busy-loops.

jmr

unread,
Apr 22, 2025, 4:04:01 AMApr 22
to v8-users
Is there a reason why RequestInterrupt does not run under DisallowJavascript scope in that case?
The docs don't seem to cover that (why is that allowed), or explain what happens if you execute javascript in the interrupt.

RequestInterrupt does interrupt while(true){}, as does the equivalent interrupt in SpiderMonkey, so it does do what I want, it's just that the safety warning.

Ben Noordhuis

unread,
Apr 22, 2025, 5:16:28 AMApr 22
to v8-u...@googlegroups.com
On Tue, Apr 22, 2025 at 10:04 AM jmr <audrius.b...@gmail.com> wrote:
>
> Is there a reason why RequestInterrupt does not run under DisallowJavascript scope in that case?
> The docs don't seem to cover that (why is that allowed), or explain what happens if you execute javascript in the interrupt.
>
> RequestInterrupt does interrupt while(true){}, as does the equivalent interrupt in SpiderMonkey, so it does do what I want, it's just that the safety warning.

Probably just an oversight. RequestInterrupt predates
DisallowJavascriptExecutionScope.

The reason the documentation doesn't go in detail is because it's
effectively UB: there are no guarantees as to what will happen.
Reply all
Reply to author
Forward
0 new messages