Repository navigation
Conversation
There was a problem hiding this comment.
Code Review
This pull request simplifies the pipe implementation in libpipefs.js by replacing the bucket-based buffer mechanism with a single flat Uint8Array buffer of fixed capacity. This change removes the distinction between blocking and non-blocking writes, treating all writes as non-blocking and enforcing the pipe capacity uniformly. Consequently, related tests and changelog entries have been updated to reflect this simplified behavior. There are no review comments, so no feedback is provided.
Since `PIPEFS` never blocks on `read()` or `write()` (already treating `read()` as non-blocking regardless of `O_NONBLOCK`), apply the 64 KiB pipe capacity limit to all writes in `libpipefs.js`. With pipes bounded at 64 KiB in all cases, replace the multi-bucket list in `libpipefs.js` with a single 64 KiB `Uint8Array` buffer that resets its cursors when fully drained and lazily compacts unread bytes with `copyWithin()` only when a write would run past the end of the buffer. See: emscripten-core#27831
b4bffe4 to
0bae681
Compare
guybedford
left a comment
There was a problem hiding this comment.
Thanks for the simplification, the single buffer is a clear win over the bucket list. Checked out locally; pipe, poll, select and epoll core tests pass along with the codesize baseline.
My main concern is that bounding blocking writers changes behaviour for a Linux-correct program: a blocking writer on one pthread with a reader on another previously worked (unbounded buffering), and with this change the write fails with EAGAIN once 64 KiB is queued:
// -pthread -sPROXY_TO_PTHREAD, reader thread draining p[0]
for (int i = 0; i < 64; i++)
assert(write(p[1], chunk, 4096) == 4096); // fails at i == 16 with EAGAINThrough stdio this is worse, since musl's __stdio_write turns the negative return into F_ERR, so an fwrite loop over a pipe permanently sets ferror after 64 KiB.
#28083 makes blocking writes from pthreads wait for the reader, which is compatible with this change (its fd_write path retries on EAGAIN for a blocking stream, so the PIPEFS condition there would just become unnecessary). The case this forecloses is the main thread without pthreads, where no wait is possible and unbounded buffering was the fallback. The read() symmetry argument cuts the other way for me: read() always returning EAGAIN is why PIPEFS cannot host a blocking reader, rather than a reason a blocking writer should also fail.
One way to keep both: keep the single buffer and the 64 KiB bound for O_NONBLOCK, and let a blocking writer grow the buffer past CAPACITY (or keep the bound only where fd_write can wait). A few smaller notes inline.
AI generated comment under my review
| throw new FS.ErrnoError({{{ cDefs.EPIPE }}}); | ||
| } | ||
|
|
||
| // Behave as if the write end is always non-blocking. |
There was a problem hiding this comment.
This is the behaviour change for blocking writers described in the main comment. If it stays, the ChangeLog entry should say that blocking writes now behave as non-blocking, since the previous entry promised the opposite.
| pipe.buckets.push(newBucket); | ||
| newBucket.buffer.set(data); | ||
| if (pipe.offset + dataLen > PIPEFS.CAPACITY) { | ||
| pipe.buffer.copyWithin(0, pipe.roffset, pipe.offset); |
There was a problem hiding this comment.
Near a full pipe every write compacts up to 64 KiB. 100k 512-byte read/write ping-pongs measured 83 ms here versus 47 ms on main. Not a blocker, a two-segment ring (read/write split at the wrap) would avoid it at similar code size.
There was a problem hiding this comment.
Hmm.. good point doing a lot of copyWithin is probably fairly costly.
|
Now I'm not so sure this is worth landing.. maybe it is still? Maybe we should at least wait until after #28083 lands? |
|
It still seems useful, but yes maybe just wait on #28083 here first? |
|
Correcting my earlier framing, since I had the model wrong. On Linux a blocking The case left is a blocking writer that cannot sleep, most commonly the main thread, for example main producing into a pipe that a pthread drains. The buffering past 64 KiB that #28082 does today is that writer's parked remainder, not a larger pipe capacity. With the fixed buffer here the remainder has nowhere to go and the write returns a short count or So I'd keep the single 64 KiB buffer and add a parked-remainder queue beside it for writers that can neither take AI generated comment under my review |
This change was written by Gemini under my supervision.
Since
PIPEFSnever blocks onread()orwrite()(already treatingread()as non-blocking regardless ofO_NONBLOCK), apply the 64 KiBpipe capacity limit to all writes in
libpipefs.js.With pipes bounded at 64 KiB in all cases, replace the multi-bucket
list in
libpipefs.jswith a single 64 KiBUint8Arraybuffer thatresets its cursors when fully drained and lazily compacts unread bytes
with
copyWithin()only when a write would run past the end of thebuffer.
See: #27831