Skip to content

Refactor async poll()/select() onto a per-inode readiness wait-queue#27226

Open
guybedford wants to merge 6 commits into
emscripten-core:mainfrom
guybedford:readiness-refactor
Open

Refactor async poll()/select() onto a per-inode readiness wait-queue#27226
guybedford wants to merge 6 commits into
emscripten-core:mainfrom
guybedford:readiness-refactor

Conversation

@guybedford

@guybedford guybedford commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator

This refactors out a partial base from #27207.

Move the readiness wait-queue onto FSNode (addListener/notifyListeners) so dup'd fds share one queue, and rewrite the suspending poll() path (readPollfds/writePollfds/pollWait) on top of it. Producers (SOCKFS, PIPEFS) now notify the node on readiness transitions, and close() wakes waiters with POLLNVAL.

  • FSNode gains addListener(cb, exclusive) / notifyListeners(flags) — a per-inode Set of listener entries. It lives on the node (not the fd) so dup'd fds share one queue. Only nodes with real readiness (sockets, pipes) ever populate it; always-ready types (regular files, ttys) never touch it.
  • The suspending poll() path is rewritten on top of it (readPollfds/writePollfds/pollWait/pollOne), replacing the old doPollAsync + makeNotifyCallback machinery.
  • Producers now notify the node on a readiness transition: PIPEFS on writes, SOCKFS via its emit bridge, and close() wakes any waiter with POLLNVAL.
  • Socket hangup semantics tightened: a peer half-close is POLLRDHUP (only a fully closed connection is POLLHUP), and a queued client makes a listening socket POLLIN.

The stream_ops.poll backend handler signature changes from poll(stream, timeout) to poll(stream) returning the current readiness mask.

Note: the exclusive parameter on addListener and the round-robin wakeup path in notifyListeners are deliberately layered in here as groundwork for the EPOLLEXCLUSIVE handling that lands in the epoll followup (#27207). No caller passes exclusive in this PR yet, so that branch is currently inert — it's included now to keep the wait-queue API stable across the split.

Move the readiness wait-queue onto FSNode (addListener/notifyListeners) so
dup'd fds share one queue, and rewrite the suspending poll() path
(readPollfds/writePollfds/pollWait) on top of it. Producers (SOCKFS, PIPEFS)
now notify the node on readiness transitions, and close() wakes waiters with
POLLNVAL.

The stream_ops.poll backend handler signature changes from
poll(stream, timeout) to poll(stream) returning the current readiness mask.
@guybedford guybedford force-pushed the readiness-refactor branch from f5b9a75 to 38d299d Compare July 1, 2026 00:26

@sbc100 sbc100 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great! Thanks for splitting this out.

lgtm with some comments

Comment thread src/lib/libsyscall.js Outdated
Comment thread src/lib/libsyscall.js Outdated
Comment thread src/lib/libsyscall.js
Comment thread src/lib/libsyscall.js
// When proxied from a worker (PTHREADS) or able to suspend (ASYNCIFY/JSPI),
// block on the wait-queue: read the interests out of memory, wait, then
// write revents back into the (still-live) pollfd array and resolve.
if (isAsyncContext) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we should just skip pollWait here when timeout == 0? i.e. should this be if (isAsyncContext && timeout)? I guess that would be change compared the current behaviour so maybe shouldn't be part of this change ?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changing this would require changing the proxy runner to use Promise.resolve(return).then() instead of just calling rtn.then(...). Could work though.

Comment thread src/lib/libsyscall.js
flags &= events | {{{ cDefs.POLLERR }}} | {{{ cDefs.POLLHUP }}} | {{{ cDefs.POLLNVAL }}};
if (flags) count++;
{{{ makeSetValue('pollfd', C_STRUCTS.pollfd.revents, 'flags', 'i16') }}};
if (timeout > 0) timer = setTimeout(() => finish(0), timeout);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could also skip this if condition too.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one surprised me too - apparently negative timeouts are permitted to mean indefinite, so we do actually need to do this polarity check.

Comment thread src/lib/libsyscall.js Outdated
Comment thread src/lib/libsyscall.js Outdated
@guybedford guybedford force-pushed the readiness-refactor branch from 54603a4 to 6d38401 Compare July 1, 2026 01:44
Comment thread src/lib/libsyscall.js Outdated
Comment thread src/lib/libsyscall.js Outdated

@sbc100 sbc100 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great!

Comment thread ChangeLog.md Outdated
return {
listeners,
entry
};

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason not to make this (and the above inline object) a single line ? maybe its JS sytle to use multi-line in these cases?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is to match terser's beautify output I believe?

// (not the fd) so dup'd fds share one queue. Only nodes that derive real
// readiness (sockets, pipes, and an epoll's own node) ever use this -
// always-ready types (regular files, ttys) never register or notify.
addListener(cb, exclusive = false) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit picking now, but this change does need the exclusive part.. maybe at least mention in the PR description that this change adds this even though its not technically using it? (Or leave to for the followup where its used?)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call — kept the exclusive machinery but added a note in the PR description that it's groundwork for the EPOLLEXCLUSIVE path in the epoll followup (#27207) and is currently inert here (no caller passes exclusive).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants