diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-22 11:15:46 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:14 -0300 |
| commit | 31cb659ded4cf50af5903fc107f8c868ee3c7311 (patch) | |
| tree | 8ceaf0ae085cfee121a1b4bdcb923b2c03b01a60 /features.txt | |
| parent | a1d5ee19a648abc65b557dffa14a3b2f70577286 (diff) | |
| download | pardes-31cb659ded4cf50af5903fc107f8c868ee3c7311.tar.gz pardes-31cb659ded4cf50af5903fc107f8c868ee3c7311.zip | |
Answer 9P on the connection's task, so a session can open its own tree
The editor's loop was the only thing that could answer a 9P request, which
made the editor's own syscalls through a mount of its own tree -- a Look at
/mnt/9p/pardes/<me>/anything under a `9ns --mntgen` view, a Save into it --
requests only the blocked loop could serve. The name-based refusal that
followed (ownMountSuffix) and the in-process routing of a mount of oneself
(Client.sameSession) were patches over that, and both are gone, with the
mailbox that shipped every request to the editor's thread.
One rule replaces them, `pardes.turn`: the core is single-threaded, the
editor's thread has the turn by default and gives it up in two kinds of gap
-- while it waits for input and while a step of it is out in a host syscall
-- and a cloud9 connection task takes it in those gaps to answer. `out`
counts the steps that are out, from any thread: while one is, the core reads
consistently but that step still holds pointers into it, so a request that
would change a pane (a write, a truncation, an rmdir) is parked in the
engine and retried when the turn is next given up with nothing out, and the
editor's own wake waits for the count to reach zero. It is never a write of
its own that a step waits on out there -- writes come from a shell
performing a save between steps -- so a parked request is never the
syscall's own, and making a pane or rendering a screen need not park:
every yield sits before its step's mutation, so the layout and the surface
are whole under it. A changing request that queued effects is answered
once the editor has performed them (`echo Save > exec` returns with the
file written, as acme's `put` does), and it settles the way a step does,
because without that a /log reader waited for the user's next keystroke.
Every host syscall on a user path has to give the turn up, not fs.zig's
alone: the first end-to-end run hung in `inotify_add_watch` performing the
new pane's watch effect. PDFs and images are read whole at open, so no
draw goes out into the host. The core's allocator takes its fixed buffer
through the lock-free interface, since a connection task allocates while
the editor's thread is out in a syscall that allocates too. A Restore puts
the replacement in first and releases every task waiting on the old core.
cloud9 (pinned at eb1a104) parks an open, a truncating wstat, a clunk and a
remove on `again`, not only reads and writes, and answers a parked job
whose fid was clunked without asking the backend.
Verified: test/selfmount.py runs the editor under `9ns --mntgen` and
Looks at, reads and Saves its own tree through the mount; a unit test pins
that a change parks while the editor is out mid-step and lands when it
rests, while a read is answered in the window. 9P over the Unix socket
against a tty session, same machine, Debug builds: a read of /index 278us
-> 61us, a truncating body write 1184us -> 609us, exec Save 718us -> 583us;
the gesture benchmark is unchanged (geometric mean 0.997 over 53 cells).
Also from the reviews: a notice chip over an image or PDF pane was painted
out by the picture drawn after the cells, so pictures give up the rows; in
the GUI a tree-sitter context band painted over the chip, so body layers
are emitted first; a message is one row of printable text, its 256-byte
cut never leaves half a glyph, and one wider than its pane keeps its tail
(the file name, the reason) rather than its head.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Diffstat (limited to 'features.txt')
| -rw-r--r-- | features.txt | 56 |
1 files changed, 56 insertions, 0 deletions
diff --git a/features.txt b/features.txt index fa1c0f6b..0db7c497 100644 --- a/features.txt +++ b/features.txt @@ -202,3 +202,59 @@ means what it says when fed back as configuration -- the symmetry the report was Left failing, deliberately: `nested-optout`. See docs/divergences.md -- two levels of nesting prepend U+E016 (vaxis's F3) to typed lines, which is a real bug in the key-forwarding path and not a stale expectation. + +9P is answered on the connection's task now, not by the editor's loop. The loop used to be the only thing that could answer a +request, which made the editor's own syscalls through a mount of its own tree -- a Look at /mnt/9p/pardes/<me>/anything under a +`9ns --mntgen` view, a Save into it -- a request only the blocked loop could serve. The name-based refusal that followed +(ownMountSuffix) and the in-process routing of a mount of oneself (Client.sameSession) were patches over that, and both are gone. +What replaced them is one rule, `pardes.turn`: the core is single-threaded, the editor's thread has the turn by default and gives it +up in two kinds of gap -- while it waits for input (`rest`/`wake`, one site per shell) and while a step is out in a host syscall +(`yield`/`back`, inside the leaf calls in fs.zig and the few host-side ones: the file watcher's marks, a shell's cwd stat) -- and a +cloud9 connection task takes it in those gaps to answer. Which gap it is matters: waiting, anything goes; out in a syscall +mid-step, the core reads consistently but the step still holds pointers into it, so a request that would change a pane +(tree.needsQuiet: writes, truncations, /pane/new, rmdir, and the screen open because it renders) is parked in the engine with +Status.again and retried when the turn is next given up quiet. That needed cloud9's engine to park an open, a truncating wstat, a +clunk and a remove, not only reads and writes -- a parked job goes back into the job slot on retry, and a walk still cannot park +because its names live in the input frame. A changing request that queued effects waits (`awaitSettled`) until the editor's pump has +performed them before it is answered, so `echo Save > exec` still returns with the file written, the way `put` does in acme; and a +request settles the way a step does (`sync`, `fsReport`, `events.announce`), because without that a `/log` reader waited for the +user's next keystroke to hear about the pane the request made. + +Two traps that cost real time. The first end-to-end run hung anyway: the Look completed on the connection task, then the editor woke +to perform the new pane's watch effect, and `inotify_add_watch` on a FUSE path is itself a request -- made while holding the turn. +The rule is therefore every host syscall on a user path, not fs.zig's alone; PDFs are read whole and opened from memory for the same +reason (MuPDF reads a file lazily at every page). The second: a parked write one byte longer than `park_data_max` fails EAGAIN, and +the client's largest write is msize less the 23-byte Twrite header, not less iohdrsz; a large Save to a peer's /os path came back +"save: Remote" until the engine's room was sized from a client. test/selfmount.py runs the editor under `9ns --mntgen` and Looks at, +reads and Saves its own tree through the mount; a unit test in 9p_io.zig pins that a change parks while the editor is out mid-step +and lands when it rests, while a read is answered in the window. + +Performance, same machine, Debug builds, 9P over the Unix socket against a tty session: a read of /index 278us -> 61us and a pane +body 283us -> 64us, because a read no longer waits for the editor to wake; a truncating body write 1184us -> 609us; exec Msg 696us +-> 543us; pane/new + rmdir 1535us -> 1295us; exec Save 718us -> 583us. A change still costs one editor frame, since the editor +draws it before it rests again. + +Left as they are: QUIC still runs on the editor's thread (cloud9's QUIC adapter is not std.Io based), so a QUIC client of a session +that Looks at its own QUIC-mounted tree would still wait on the loop; the Tty9p kernel mount lives in the helper's private +namespace, so a Look on `$PARDES_MOUNT/...` from that shell opens nothing in the editor -- no longer a deadlock, only a namespace +the editor is not in; and a message wider than its pane now keeps its tail rather than its head (msg.snap `msgtail`). + +Two adversarial reviews of that design. The concurrency one found the lost wake for real: a connection task's own yield saved the +editor's mid-step "not quiet" and restored it after the editor had rested, so a resting editor looked busy forever and every change +parked with nobody to wake it -- `quiet` is now `out`, a count of steps out in a syscall from any thread, and nothing saves or +restores it. Behind it, three more: the editor could take its turn back while a connection task's step was still out (that step's +pointers dangling when it returned) -- `wake` now waits for the count to reach zero; the core's allocator was a stack-fallback +over a FixedBufferAllocator, thread-safe only in Debug where the DebugAllocator wrapped it -- the fixed buffer is taken through +its lock-free interface now; and a Restore let a task waiting on the old core wake after that core was freed -- `reset` swaps the +listener's core first and releases every waiter, which then finds the core changed and answers nothing. It also showed that +parking `pane/new` and `screen` would have hung a Look at exactly those two paths in one's own mount (the editor's own open would +park, waiting for a rest that cannot come), so those are answered mid-step: every yield sits before its step's mutation, so the +layout and the surface are whole under it. Images are read at open for the same reason PDFs are. + +The rendering one found the chips drawn out from under: a placed image or PDF page is drawn after the cells (the GUI's image pass, +kitty's z-order), so a chip over a picture was painted over -- pictures give up the rows now, text keeps the overlay -- and in the +GUI a tree-sitter context band was emitted after the tag layers and painted over the chip, so body layers go first. A message is +one row of printable text (a language server's multi-line report read as one line with U+FFFD per newline), the 256-byte cut +never leaves half a glyph, and an over-wide message cut inside a wide glyph now drops that glyph rather than clipping its last. +The simplicity one cut the vestiges: `ninep_identity`, the restore's copy of listener addresses, `setWake`, the mailbox's +`Drained` name, the 64K read buffer, and the two macOS entry points whose multi-line signatures the wrapping had missed. |
