saghul / txiki.js

A tiny JavaScript runtime
MIT License
2.52k stars 169 forks source link

libuv handle lifecycle #185

Closed johnrobinsn closed 6 months ago

johnrobinsn commented 2 years ago

Let me know if there is a better place to ask this... or discuss...

Currently the libuv (loop) manages the lifecycle of a set of uv handles. In txiki.js a loop is bound to the runtime rather than to a context. if contexts are allowed to be dynamically started and shutdown (maybe for an iframe like construct in a UI runtime). what's the best way to cleanly shutdown handles (timers, outstanding connections etc) that are scoped to a single context. Right now it seems a bit awkward given that the loop is held at the runtime level. I have a workaround now that seems to work OK for now... but would appreciate other thoughts on how best to manage.

saghul commented 2 years ago

Good points.

TBH txikijs uses 1 context and 1 runtime so I didn't give it much thought in the beginning.

I never envisioned the iframe case.

Multiple contexts could be supported 2 ways: having libuv in the runtime and multiple contexts can use the same loop, or move to a per context event loop.

Right off the bat I'm not sure which one would be better going forward.

What are your thoughts on this? Happy to discuss!

johnrobinsn commented 2 years ago

Right now I'm creating multiple runtimes (1 ctx each) and and running all the loops on the same thread by manually pumping each with uv_run(&qrt->loop, UV_RUN_NOWAIT)... works fine... I have fairly complicated scenes with recursively nested "iframes" each with one of these one-to-one runtime/ctx(s)... Added some things to clean up handles when I shutdown the runtime/ctx etc.

I have to manually pump libuv anyway to cooperate with my "windowing/ui loop". All this works for me for the time being (so I'm not stuck) and I didn't have to deeply refactor txiki.js.

I do have to take care about sharing objects between contexts because they're in different runtimes... my UI kit has a way to proxy/bridge these objects so I'm able to do it without to much fanfare... but something I need to be careful about.

I'm still thinking about it as well... But I keep thinking that it would be lighter etc and I'd have to take less care with sharing objects if handles where scoped to a ctx (and multiple ctx(s) could share the runtime). I haven't taken a look at the cost of multiple runtimes. Will share more as I work through it.

Thanks.

johnrobinsn commented 2 years ago

Also any feedback on PR #184 (quickjs debugger)? if not interesting I won't invest in the pull request anymore... But works pretty well (a good starting point anyway).

johnrobinsn commented 2 years ago

And just to explain a bit more... I was able to pretty easily use multiple contexts with a single runtime.... But I wasn't able to easily shutdown the handles associated with a single ctx... Since the bookkeeping is at the runtime level...

saghul commented 2 years ago

Sorry for the delay, I'm only getting back to this now. I don't have a clear answer yet. The only thing that seems to apply in an environment such as this one is "realms". It can be supported, see https://github.com/bellard/quickjs/blob/b5e62895c619d4ffc75c9d822c8d85f1ece77e5b/run-test262.c#L779 but I'm not sure about its usefulness.

By looking at the jobs API (which is where the event loop needs to integrate to tap into resolving promises and other async operations) it looks to me like the event loop needs to be tied to the QuickJS runtime: https://github.com/bellard/quickjs/blob/master/quickjs.h#L869-L875 The JS_ExecutePendingJob function returns the context where the executed job is running, so that somehow tells me that the loop needs to be tied to the runtime. At least for practical reasons here.

Any reason why workers are not a good fit? They have their own runtime and context, and run on a different thread.

johnrobinsn commented 2 years ago

Thanks Saghul for getting back on this...

UI applications typically don't don't do rendering from multiple threads. Cocoa for example really fights you here. Not that it's impossible but pretty messy. Most of my rendering is currently GL-based (supports webgl using qjs etc), which can be done from multiple threads but there are memory and other resource implications. I will likely support multi-threaded rendering soon but I don't want it to be the default for these overhead reasons.

I do think having the libuv handles scoped to the ctx would be a better design. But I recognize it's a big refactor. I'm able to do what I need for now using the multiple run-times approach on a single thread works reasonably well for me.

saghul commented 2 years ago

Any reason why using multiple workers is not a good idea / possibility?

I'd be open to exploring the possibility, but on the surface I don't know how the pending job execution API would be integrated in a multi event loop environment.

johnrobinsn commented 2 years ago

each worker is a separate thread. and as I mentioned rendering from these different threads (graphics memory in the form of fbos, composition overhead of different surfaces and synchronizing composition across different threads) has overhead that I don't want to pay at least in the default case.

It's been a while ago... but I was able to run multiple tjs/qjs contexts in a single UV loop with promises, timers etc. with minor changes (a few lines of code) to tjs.. All seemed to work... but I didn't look too closely at the qjs "jobs" api. The main problem that I ran into was that when I wanted to "close" one of those tjs/qjs contexts, I wasn't able to readily clean up the handles just for that tjs/qjs context since the handles are scoped to the tjs runtime.

Given all of this, right now I'm effectively using a "multiple worker" like approach... (Although not using your worker api directly.) I'm just running multiple tjs runtimes/uvloops on the same thread. Pumping them manually using uv_run(&qrt->loop, UV_RUN_NOWAIT); from my main GUI (window) event loop. This works well enough for me now.

saghul commented 2 years ago

each worker is a separate thread. and as I mentioned rendering from these different threads (graphics memory in the form of fbos, composition overhead of different surfaces and synchronizing composition across different threads) has overhead that I don't want to pay at least in the default case.

Ah gotcha I managed to miss that.

saghul commented 6 months ago

I'm going to close this for now. The refactor would be too big and supporting a shared library build is kind of a requirement, which is not something that I'm focused on.