Implement epoll APIs in the JS filesystem - #27207
Conversation
aacb3d7 to
57e98fb
Compare
sbc100
left a comment
There was a problem hiding this comment.
I think this like this direction.
I've not had time to look at all the details yet, but it seems like a great idea to unify the node events like this.
0e64f2b to
9b46f69
Compare
97a4580 to
d33958b
Compare
|
I'm liking the direction of this commit but still a little overwhelmed by the size of it. Can you think of any more ways to split it up? I'm not sure myself... for example, could we land the internal refactoring of the poll/select to use the new notification system before we land the rest of epoll? Maybe not practically separable? |
I've refactored out the JS notification changes into #27226 if that helps? |
3d7ef1b to
aaa31e8
Compare
Adds `wasm32-unknown-emscripten` as a target for mio, plus a CI job that runs the suite under Node. Resolves tokio-rs#642. Emscripten exposes a real epoll backed by its runtime event loop, so the existing Linux epoll selector is reused rather than adding a new backend. The wasm `compile_error!` guard is relaxed to let emscripten through, and the `epoll`/`eventfd`/pipe-waker cfg lists gain emscripten. Because emscripten cannot block in `epoll_wait` without JSPI/ASYNCIFY, two non-blocking readiness paths are added alongside the normal `Poll::poll`: * `Poll::new_with_callback` arms a persistent callback on the epoll fd via the runtime's `emscripten_epoll_set_callback`, delivering ready events on a fresh host tick whenever the set makes progress. The callback state is boxed, owned by the arming selector, and disarmed on drop; it may freely re-enter mio to (de)register sources. * `Registry::poll_ready` does a zero-timeout drain of the same epoll set as a readiness probe where a blocking wait is impossible. AF_UNIX support is stream-only: emscripten's node-backed sockets have no datagram primitive, so `UnixDatagram` and the `socketpair`-based helpers are not compiled there. Sockets set `O_NONBLOCK` via `fcntl` since emscripten's `socket(2)` silently strips `SOCK_NONBLOCK`/`SOCK_CLOEXEC`. The suite spawns OS threads (socket-peer test harnesses), so std is rebuilt with atomics via -Zbuild-std and linked -pthread with -sPROXY_TO_PTHREAD so the main thread can block; JSPI (-sJSPI) provides the return-to-host suspension for blocking reads/writes, and NODERAWFS/NODERAWSOCKETS back the filesystem and sockets with node's. This needs nightly + rust-src and a JSPI-capable Node (26+, or 22 with --experimental-wasm-jspi). No custom target spec is required: nightly now emits the __main_argc_argv entry point (rust-lang/rust#158937). Doctests are skipped on this target: rustdoc does not apply the emcc link args, so the examples cannot be linked with the socket/thread runtime. Temporary, until the dependencies land upstream: * Cargo.toml patches libc to guybedford/libc#emscripten for the emscripten epoll/pthread externs (rust-lang/libc#5270). * the CI job builds against the guybedford/emscripten `cf` fork, which carries the epoll callback (emscripten-core/emscripten#27207), AF_UNIX pathname stream sockets, and multicast getsockopt patches this target depends on. Suite result: 148 passed, 0 failed, 3 ignored under Node - green on CI.
Adds `wasm32-unknown-emscripten` as a target for mio, plus a CI job that runs the suite under Node. Resolves tokio-rs#642. Emscripten exposes a real epoll backed by its runtime event loop, so the existing Linux epoll selector is reused rather than adding a new backend. The wasm `compile_error!` guard is relaxed to let emscripten through, and the `epoll`/`eventfd`/pipe-waker cfg lists gain emscripten. Because emscripten cannot block in `epoll_wait` without JSPI/ASYNCIFY, two non-blocking readiness paths are added alongside the normal `Poll::poll`: * `Poll::new_with_callback` arms a persistent callback on the epoll fd via the runtime's `emscripten_epoll_set_callback`, delivering ready events on a fresh host tick whenever the set makes progress. The callback state is boxed, owned by the arming selector, and disarmed on drop; it may freely re-enter mio to (de)register sources. * `Registry::poll_ready` does a zero-timeout drain of the same epoll set as a readiness probe where a blocking wait is impossible. AF_UNIX support is stream-only: emscripten's node-backed sockets have no datagram primitive, so `UnixDatagram` and the `socketpair`-based helpers are not compiled there. Sockets set `O_NONBLOCK` via `fcntl` since emscripten's `socket(2)` silently strips `SOCK_NONBLOCK`/`SOCK_CLOEXEC`. The suite spawns OS threads (socket-peer test harnesses), so std is rebuilt with atomics via -Zbuild-std and linked -pthread with -sPROXY_TO_PTHREAD so the main thread can block; JSPI (-sJSPI) provides the return-to-host suspension for blocking reads/writes, and NODERAWFS/NODERAWSOCKETS back the filesystem and sockets with node's. This needs nightly + rust-src and a JSPI-capable Node (26+, or 22 with --experimental-wasm-jspi). No custom target spec is required: nightly now emits the __main_argc_argv entry point (rust-lang/rust#158937). Doctests are skipped on this target: rustdoc does not apply the emcc link args, so the examples cannot be linked with the socket/thread runtime. Temporary, until the dependencies land upstream: * Cargo.toml patches libc to guybedford/libc#emscripten for the emscripten epoll/pthread externs (rust-lang/libc#5270). * the CI job builds against the guybedford/emscripten `cf` fork, which carries the epoll callback (emscripten-core/emscripten#27207), AF_UNIX pathname stream sockets, and multicast getsockopt patches this target depends on. Suite result: 148 passed, 0 failed, 3 ignored under Node - green on CI.
01ddd22 to
d8e24d7
Compare
|
I've gone ahead and simplified the epoll callback design here so that the callback no longer consumes and provides ready events, but instead only reports that there are non-zero events ready to be consumed by a 0-timeout |
57dfba2 to
8b5f74d
Compare
sbc100
left a comment
There was a problem hiding this comment.
Sorry if we've already discussed this possibility before, but do you think we could split out the emscripten-specific callback stuff from the this PR, and just the epoll implementation first?
It seems like the emscripten-specific-callback API adds quite a bit of extra complexity regarding proxying and runtimekeealive.
| $newEpollInstance: () => { | ||
| // Its own (detached) node, so the epoll fd can be watched by a parent epoll | ||
| // (nesting) and carry the readiness wait-queue methods. Shared across dups. | ||
| var node = new FS.FSNode(0, 'epoll', 0, 0); |
There was a problem hiding this comment.
Is the name "epoll" here every visible to the user? i.e is it just a placeholder?
There was a problem hiding this comment.
Yeah it's just a placeholder - changed to the empty string to save space.
| }); | ||
| // Hoist the instance state onto `shared` so every dup observes one instance. | ||
| Object.assign(stream.shared, { | ||
| node, |
There was a problem hiding this comment.
Is node needed in stream.shared even though its already part of stream?
There was a problem hiding this comment.
Yes, because stream.shared is what the syscall entry points pass as the instance data, from which we extract the node.
| // armed registration can never fire again on its own, so it is terminal. | ||
| armed: 0, | ||
| // Open references (fds) to this instance; the last close reclaims it. | ||
| refcount: 1, |
There was a problem hiding this comment.
nodefs also adds refcount here for the same purpose.. i wonder if we share this code, or make this some kind of mixin?
There was a problem hiding this comment.
It could be done, although would affect every other backend including pipefs and nodefs, so would be a larger refactoring. Perhaps a possible follow-on rather?
Add epoll_create1/epoll_ctl/epoll_wait/epoll_pwait on the legacy (non-WASMFS) JS syscall layer, built on the per-inode readiness wait-queue: level- and edge-triggered modes, EPOLLONESHOT, EPOLLEXCLUSIVE, EPOLLRDHUP, nesting, dup-shared epoll instances, and blocking waits under PROXY_TO_PTHREAD, ASYNCIFY, and JSPI.
|
Thanks @sbc100 for the review! The latest PR feedback has now been addressed. I've also added a proper multi-threading test and posted the follow-on with its own review feedback integrated per the comments here in turn. |
Implements epoll, based on previously landed work #27226 and #27206.
Resolves #5033, #10556.
Adds
epoll_create1,epoll_ctl,epoll_waitandepoll_pwaiton a single fd readiness model shared withpoll().Builds off of the existing event-driven readiness model in the JS FS system with the integration point as the per-inode wait-queue, having each FS node carrying a
listenersset and producers callingnotifyNodeListeners(node, flags)on ready transitions. There is no separate or parallel readiness machinery - it integrates directly with the existing model.pollOne(fd, events)is reused on the same readiness definition.Per standard epoll semantics -
epoll_ctl ADDinstalls a new listener on the watched node. If items are already ready they are added to the ready list. That listener then appends the registration to the epoll's ready list for waking. The epoll_wait consumes the ready list, re-checking each item against its current mask viapollOne.EPOLLONESHOTclears listeners to avoid unnecessary callback firing.EPOLL_CTL_MODcan then re-arm them again.EPOLLETis implemented correctly to avoid refiring items that remain readyEPOLLEXCLUSIVEis passed for listeners allowing only one wake for multiple epoll listeners to avoid the "thundering herd".maxevents, draining follows Linux-like semantics in supporting round-robin ready calling. To achieve this without losing performance, a doubly-linked list is used for the registrations. A simpler set / array with copying could be used alternatively if we don't want to use this approach.dup(2)of an epoll fd yields another reference to the same epoll instance (registrations and ready list shared, per Linux eventpoll semantics); only the last close reclaims it.Most of the diff is tests, covering these semantics in depth including error handling, level versus edge reporting, nesting and ELOOP, fd-close auto-removal, dup instance sharing, JSPI and pthreads, real sockets, deregistration, deterministic round-robin fairness, and multi-threaded waits on a shared epoll fd (per-edge exactly-once wakeup under
EPOLLET, herd wakeup under level triggering, andEPOLLONESHOTdisarm/re-arm and exactly-once delivery against racing waiter threads).Minor semantic divergences to note:
epoll_pwaitignoressigmaskepoll_create1accepts and ignoresEPOLL_CLOEXEC(unknown flags are rejected withEINVAL)epoll_eventunder Wasm in Musl is laid out as aligned 16 rather than x86-64's packed 12 bytes.This PR originally also included
emscripten_epoll_set_callback, a non-blocking JS-callback readiness variant (usable without ASYNCIFY/JSPI), which was split out into the follow-on PR #27547.PR made with AI assistance, under my review