-
Notifications
You must be signed in to change notification settings - Fork 71
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
Initial Javascript browser backend #405
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks very cool!
The implementations both use Brr which I find a lot easier to use, but if we want less dependencies etc. I'm happy enough to refactor that out if we want to?
According to opam list --required-by=brr --recursive
, brr
actually has fewer dependencies than js_of_ocaml
(though both depend on js_of_ocaml-compiler
). Though I see you depend on js_of_ocaml
too. Maybe we can remove that instead (it pulls in Lwt, for example)?
We could also perhaps provide an Eio_js module in the same vein as Eio_main, although it would be restricted to only the subset of things the browser can support so I'm not sure how useful that would be?
Eio_main
is useful because you often want to write a program and not care whether it will run on Linux, macos, etc. Eio_js would only be useful if people are likely to want to write applications that could be run either in the browser or under node, which seems doubtful to me.
Testing JS code is a little tricky
I don't do much web stuff, so no idea.
lib_eio_js/browser/eio_browser.ml
Outdated
match t.timeout with | ||
| None -> | ||
let id = G.set_timeout ~ms:0 (fun () -> t.timeout <- None; schedule t) in | ||
t.timeout <- Some id; | ||
schedule t | ||
| Some _id -> () | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like this is asking the browser to run schedule
again soon, unless we already asked it to do that.
I'd expect to see this code in enqueue_thread
, when we have something to do, rather than in schedule
when we're idle. Does the current code just busy-wait until there's something to do?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's the idea yeah, afaict using set_timeout
with ~ms:0
allows us to yield to underlying JS event loop. So if the timeout is already set schedule
effectively exits but our main
promise below is not yet fulfilled so we don't get passed that point, the event loop is given a chance to run any events, timeouts etc. and then our schedule
gets fired and we check to see if anything is ready.
But your question made me rethink it and I think there's a nicer solution where the scheduler becomes an event listener, see this change patricoferris@935b2e8
I've updated this PR to be only the browser backend so hopefully it is easier to review. The scheduler also now uses the event based approach I described above. W.r.t testing -- I've added some Alcotests that compile to the browser and then render to the DOM. This pulls in Alcotest and Ansi (for nice colours) for testing which is maybe a bit much, especially considering we can't really hook it up to CI. The tests are all currently passing I thought I would just show an example. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is looking pretty good (comments inline). CI is failing, though.
lib_eio_js/browser/eio_browser.ml
Outdated
to the head too, which we need. *) | ||
mutable run_q : (unit -> unit) Run_queue.t; | ||
mutable pending_io : int; | ||
mutable scheduler : Scheduler.t option; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is this optional?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, I implemented it in some weird back to front way that meant there was an interdependency between the schedule function and the scheduler. This is now fixed. Note as well we don't need to track the pending_io anymore either.
lib_eio_js/browser/eio_browser.ml
Outdated
let cancelled = ref true in | ||
Fiber_context.set_cancel_fn k.fiber (fun exn -> cancelled := true; enqueue_failed_thread st k exn); | ||
Fut.await fut (fun v -> | ||
Fiber_context.clear_cancel_fn k.fiber; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks wrong. If I understand correctly, cancellation will go like this:
- Someone creates a
Fut
andawait
s it. - The await is cancelled.
cancelled
is set totrue
and the waiter is resumed. - The fiber starts a new operation.
Fut
is later resolved. It (incorrectly) clears the cancel function of the fiber's new operation.
Fixing that just requires putting the clear inside the if
.
It's also a bit of a problem that we leak memory every time we await and then cancel (until the Fut is finally resolved). Eio generally tries to avoid doing that, but it may be impossible with the Fut API.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep quite right! I've added a comment about the memory leak. I'm not sure there's a way around this because Fut
is based on JS promises and they are eager so don't have a way to cancel them iiuc.
lib_eio_js/browser/eio_browser.ml
Outdated
enter_io @@ fun st k -> | ||
let listener = ref None in | ||
Fiber_context.set_cancel_fn k.fiber (fun exn -> Option.iter Ev.unlisten !listener; enqueue_failed_thread st k exn); | ||
let v = listen (fun v -> enqueue_thread st k v) in |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This one never clears the cancel function, so cancelling after it returns (or after it is enqueued) may try to resume twice.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, should be fixed now. Also removed the need for the listener ref.
lib_eio_js/browser/eio_browser.ml
Outdated
(* Resume the next runnable fiber, if any. *) | ||
let schedule t : unit = | ||
match Run_queue.pop t.run_q with | ||
| Some f -> f (); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why doesn't this need to call schedule
again after f
is done?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, good question!
So after a bit of head scratching, I think the answer is it should call schedule again but because of how everything is implemented it doesn't actually need too... maybe. So I think this is because the only time we could potentially hang is if there is more than one item in the run_q and there is no scheduled wakeup call. I think the only way to get more than one thing in the queue is if you do some "IO" like the timeout (so not by forking, awaiting promises etc.) and because every single "IO" operation sends a wakeup event to our scheduler then the run_q will always be fully processed (but perhaps more slowly). At any rate, I've added a call to schedule here!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need to be careful here. In Eio_linux
, schedule
returns [`Exit_scheduler]
and a suspended fiber has type ('a, [`Exit_scheduler]) continuation
. This indicates that resuming a continuation commits to keeping things going (i.e. calling schedule
again).
Your run_q
has type (unit -> unit) Run_queue.t
, which suggests it doesn't do that.
It would probably be best to decide which one you're doing and update the types to check it. If you call schedule
when you don't need to, you can easily make your continuation handler not be tail-recursive, which is bad.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point! So in 9ff3108 I've changed the scheduler again (sorry...). It is now much more reminiscent of the luv scheduler modulo Luv.Async.send
is now a call to requestIdleCallback
the idea being anytime we fall through to the final "exit" promise we'll be idle and allow the scheduler to be woken up. This means schedule
(now wakeup
) is only ever called when needed. Unfortunately safari doesn't implement requestIdleCallback
but this seems like a shim people have used in their libraries. I do note that react.js used to use it in their scheduler but I think decided that it wasn't aggressive enough, I don't know if that's a problem or not here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this may suffer from the same problem as #427
Is this fixed with dune 3.7.0? Looks like most tests are now passing, but there's a problem with the lower-bounds. |
Yep! Is it okay to make everything use |
Just to be clear (the message might have been lost above), the current implementation suffers the same problem as Eio_luv with performing effects across a C call (a JS call in this case). So the following fails: module Echo = struct
type _ Effect.t += Echo : string -> unit Effect.t
let run f =
Effect.Deep.try_with f ()
{
effc =
(fun (type b) (eff : b Effect.t) ->
match eff with
| Echo string ->
Some
(fun (k : (b, unit) Effect.Deep.continuation) ->
print_endline string;
Effect.Deep.continue k ())
| _ -> None);
}
end
let () =
Echo.run @@ fun () ->
let p =
Eio_browser.run @@ fun _ ->
Eio.Fiber.yield ();
Effect.perform (Echo.Echo "world")
in
Fut.await p (fun () -> ()) I was wondering if we could turn the scheduler into a generator function or something, but haven't had the time to look into it or I don't know if that would fix things either. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the scheduling needs more documentation. I couldn't convince myself it was correct. I pushed a commit here that adds some types to try to clarify things:
https://github.com/talex5/eio/commits/js
There, I set the continuation return type to suspend
, which indicates that the scheduler found the run queue empty and so decided to suspend itself and wait for a callback.
I think the wakeup function might get called in some cases when it's already running (or going to get called).
Also, I think all places where you use enter_unchecked
you do actually need to check that the fiber isn't already cancelled before starting.
Definitely, I could also not convince myself of the correctness either ^^" I've tried a slightly different approach in patricoferris@bf3f701 which I need to document but I think is already simpler and similar to some of the ideas in |
2a91186
to
bb9ca8a
Compare
It also dawned on me a little whilst testing this code that perhaps this really doesn't need to be in Eio at all since the JS browser backend is unlikely to ever do any IO (at least not in the Maybe this is a good example of why having |
It is its own library (
It can already live elsewhere if it wants to. The only advantage of having it in this repository is making sure we keep it up-to-date if we change the core API. Making it separate may well be a good idea. The
In theory, you only need But really you'll end up wanting more. Users of a backend need That just leaves the OS types ( It might be useful to split the core out to convince people it's modular, though (that's why it's a separate library already). The main problem with splitting out
Is that something we should fix? |
Do we have an ETA for this being merged? |
We will resume this work in the next few weeks. |
Anything I can do to help here? |
Co-authored-by: Thomas Leonard <[email protected]>
I believe @vouillon has started looking at this. |
Did we decide to put this in a separate repository? Setting that up might be useful, but I don't want to conflict with anything that @vouillon is doing. |
In the short-term I've made a separate package https://github.com/patricoferris/eio_browser -- I'm happy to maintain and release this, but should probably go into ocaml-multicore ? |
Sounds good. Do you have permission to transfer it? If not maybe @Sudha247 can add it (or you could transfer it to me and I can do it). |
@talex5 perfect, transfer requested to you :)) |
Done: https://github.com/ocaml-multicore/eio_browser However, I no longer have admin rights on it, so can't give you access! I think @Sudha247 should be able to do it. |
I guess we should move #680 to the new repository too? |
Yep I think that makes sense. @balat Do you want to open your new PR against that repository which perhaps we could rename to eio_js ? |
I'm not sure it really makes sense to move #680 to the new repository, since there is not much in common in the end. i should probably set up a different repository instead. |
@patricoferris what do you think about replacing the current eio_browser repository with #680? Is there any reason to have both? |
@talex5 I have also created a repository vouillon/eio_js which could be moved to ocaml-multicore. I'm currently keeping it in sync with #680. I would be interested in feedback from both of you. |
Yes, apologies, I did mean open #680 to ocaml-multicore/eio_browser to replace it and if we need to rename it to eio_js then let's do that. Probably it is easier to mport vouillon/eio_js into this org and delete eio_browser entirely |
Either way, this PR is no longer relevant here. |
This PR started off as both a Node and Browser backend, I'll follow up with Node so now only the Browser backend is here.
This PR is initial support for Javascript backends in Eio with two new packages:
eio_node
andeio_browser
. It's important to note that this is just a first step to getting full support and a releasable set of packages.Implementation
Javascript, both in Node and the browser, are inherently single-threaded and event-driven. As far as I can tell this means there's no simple way to yield to the event loop, instead when you get to that point you either turn your computation into something that emits an event when it is done or you create a Javascript promise and
await
that. Botheio_node
andeio_browser
have opted for the latter and both make the assumption that they will not have domain manager support.The implementations return a promise for the computation (an
'a Fut.t
) which a user could then await in their program if they want to sequence it with something else. Both implementations keep track of pending IO operations using a counter (this need not be Atomic as we only have a single domain) and make sure to enqueue a "wakeup" if there is pending IO. In Node this wakeup issetImmediate
which is called on the next event loop iteration (see https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/). In the browser this is not available so I've used asetTimeout
with a0
second argument.Currently
Eio_node
only support Filesystem access to make the reviewing a little easier. It shouldn't be too difficult to add support for the rest of thestdenv
pieces in follow-up PRs along with better cancellation support if possible. Nodejs is more or less a wrapper around Libuv so a lot can be taken from that.Other node implementation ideas
It is a little annoying that the main loop returns an
'a Fut.t
, but I couldn't work out a way to not make this the case. I had tried using two Worker Threads and synchronising with Atomics, but couldn't get that to work.Running Locally
At the time of writing, there seems to be a few issues with
dune
/js_of_ocaml
which I think are being fixed w.r.t to separate compilation. You will need to get a development version ofdune
with fixes like ocaml/dune#6828 and ocaml/dune#6714 but I still seem to have problems with the--enable=effects
flag, so for testing you will need to rundune build --profile=release
for now.Questions
Brr
which I find a lot easier to use, but if we want less dependencies etc. I'm happy enough to refactor that out if we want to?Eio_js
module in the same vein asEio_main
, although it would be restricted to only the subset of things the browser can support so I'm not sure how useful that would be?enabled_if
and check for a sufficiently new enoughnode
binary and run it. For the browser, there's not a whole lot we can do beyond manually checking (we could perhaps extend the Alcotest tests and use that e.g. https://github.com/patricoferris/irmin-browser-tests)