summaryrefslogtreecommitdiff
path: root/docs/registry.typ
diff options
context:
space:
mode:
Diffstat (limited to 'docs/registry.typ')
-rw-r--r--docs/registry.typ133
1 files changed, 133 insertions, 0 deletions
diff --git a/docs/registry.typ b/docs/registry.typ
index 3db77c65..40af056a 100644
--- a/docs/registry.typ
+++ b/docs/registry.typ
@@ -423,6 +423,34 @@ rejected whole.
#ev("src/detached/server.zig:238-241")[the daemon is one `poll(2)` over 50 slots, and `:565-569` records that it has no worker pool.]
#verdict[Deferred until a second machine exists AND `9P-12` has settled, because the proxy's shape depends entirely on which layer 9P occupies. Reopen with a named use case, not with an architecture.]
+
+ #note("unblocked", "2026-08-27")[
+ *Reopen this.* The deferral rested on one objection and steps 4 and 5
+ dissolved it without meaning to.
+
+ The objection was that a proxied `Twalk` cannot be answered until the remote
+ `Rwalk` arrives, so the router needs per-tag continuations, tag remapping,
+ flush forwarding and fid invalidation — machinery a core that must not block
+ has nowhere to put. Three of those four now exist as shipped primitives:
+
+ #ev("src/9p.zig")[`Server.retry()` re-offers a parked request oldest-first and `reply()` RE-PARKS it when the answer is still `.again`. The park table is therefore a continuation store that already survives across frames, and it is the one the FUSE mount has used all along.]
+ #ev("src/9p.zig")[`Client` is sans-io: `submit()` hands back a tag and never waits, `take()` returns a completed operation or null. Its tag table is indexed BY the tag, so attributing an out-of-order reply is one bounds check — which is the remapping the objection was about.]
+
+ So a proxy is now a loop, not a subsystem: `Server.next()` gives a request,
+ `Client.submit()` forwards it, the answer is `.again`, and each frame
+ `Server.retry()` offers it back until `Client.take()` completes and the real
+ reply goes out. Nothing blocks, nothing is added to the core, and the
+ daemon's poll set grows by one descriptor per upstream.
+
+ What is still genuinely missing is fid invalidation on a connection that
+ dies mid-walk, and a decision about whether `Tflush` forwards or is answered
+ locally. Both are small and neither is architectural.
+
+ Re-cost before building: the "several hundred lines" the draft claimed was
+ wrong in the other direction too. Measure it against the ≈200 lines this
+ loop looks like, and against `src/fs9_client.zig`'s 622, which already does
+ the connect-and-pump half.
+ ]
]
#entry("9P-11", "A 9P server on the ESP32-P4 — real, cheap, and a second firmware image", state: "decided", tags: ("board", "motivating-case"))[
@@ -1070,3 +1098,108 @@ Three of those four are achievable. One is not, and `9P-20` says which.
codebase is the same severity: the comments are how the next change is made.
]
]
+
+#pagebreak()
+
+= After the build
+
+Five steps shipped, and the shape of what is left is not the shape the note
+predicted. These entries are written from the tree as built, not from the plan.
+
+#entry("9P-25", "The tree behind the server is pluggable, and that was not planned", state: "decided", tags: ("architecture", "windfall"))[
+ `Server` is `pub fn Server(comptime fs: type) type`, duck-typed on exactly
+ `fs.Req`, `fs.Reply` and `fs.Reply.Attr`. That shape was chosen for a boring
+ reason — importing `acmefs.zig` drags `pardes.zig` into `zig test` — and it
+ turned out to be the most useful thing in the file.
+
+ #ev("src/acmefs.zig")[filesystem one: the acme control tree, 9 ops.]
+ #ev("src/board9p.zig")[filesystem two: 867 lines that re-declare the same `Op`, `Status`, `Req`, `Reply` and `handle`, and are served by the same `Server` with no translation layer at all.]
+
+ So "serve X over 9P" is no longer a protocol question. It is: write a
+ `handle()` over nine operations, and get a wire, a fid table, directory
+ cursors, `Tflush`, error strings and both freestanding targets for free.
+
+ #verdict[
+ Treat `Server(fs)` as the extension point it accidentally became, and say so
+ where someone will look. Candidates that are now cheap and were never on a
+ list: the LSP surface as a tree, a session dump as a tree, the config as a
+ tree. None of them needs a line of 9P.
+
+ The discipline that keeps this from becoming a plugin system: nine
+ operations and no tenth. A filesystem that wants a tenth wants an API.
+ ]
+]
+
+#entry("9P-26", "What is cheap now, ranked, and step 6 is not first", state: "open", tags: ("sequencing", "next"))[
+ Step 6 — a remote display serving `screen` and `input` while the core is its
+ client — is cheaper than it was, because its one hard prerequisite is done:
+ `Client` exists, is 152 bytes, and blocks nowhere. And `board9p` proved that
+ writing a second filesystem behind `Server(fs)` is a day's work, which is
+ exactly what a `host` tree would be.
+
+ But four things are now cheaper than step 6 AND serve the stated goals more
+ directly. Ranked by value over cost:
+
+ / A serial transport for the client: #[the board image exists and speaks 9P on UART0; `src/fs9_client.zig` speaks unix sockets. One transport away from the motivating case being real, and it is the smallest item here.]
+ / TCP: #[`fs9_service` and `fs9_client` are `AF_UNIX` only. "Integrating over the network" was one of the four stated goals and it is currently a socket family, not a design problem.]
+ / macOS and the browser: #[step 4's entire portability argument — that a 9P server needs no kernel — is UNEXERCISED. Nobody has run `9pfuse` against the socket on macOS, and the web build has no client. This is the payoff that justified the growth in `9P-20`, and it is closer to zero code than anything else on the list.]
+ / Aggregation: #[unblocked, see `9P-10`'s note. Serves "chaining a pardes to another", which is the goal `9P-22` named as the whole point.]
+
+ #q[Step 6 also changed SHAPE, and the note should be rewritten before it is built. It was sold as "the board stops being a shrunken pardes and becomes a terminal for a full core". What got built is the INVERSE: the board is a 9P server of its own devices and the desktop is the client. Both are useful and they are different images. Which one is step 6 — and is a board that shows a remote core's screen worth an image, now that a board that exposes its pins is already flashed?]
+
+ #note("scope", "2026-08-27")[
+ Unchanged by any of this: `9P-23`'s measurement. Local hosts keep the direct
+ vtable call, because a tree costs tty and gui about +204 lines each and the
+ web shell +142 Zig plus ≈500 JavaScript. Step 6 is remote displays only, and
+ the moment it is argued for the window in front of you, that number is the
+ answer.
+ ]
+]
+
+#entry("9P-27", "What three adversaries found in steps 3, 4 and 5", state: "landed", tags: ("bug", "lesson", "review"))[
+ Steps 1 and 2 were reviewed and the pass found a 100%-CPU spin in the smallest
+ change of the chain (`9P-24`). Steps 3, 4 and 5 — 1,005, 9,158 and 3,535 line
+ diffs — were verified on the happy path and shipped unreviewed. Three agents,
+ one per step, told to assume the happy path works and look elsewhere.
+
+ Eight defects. Six fixed, five reproduced with measurements before and after.
+
+ / Remote crash of the whole daemon: #[`Server.push` takes nothing once `startFrame` gives up on the framing, and `fs9_service.fill` asserted it took everything. One `size[4]` of zero plus one later byte reached `unreachable` — every pane, every frontend and the FUSE mount gone. The same stuck buffer separately made `room == 0` return without reading while poll reported ready forever: *99.7% of a core*, in one `write(2)`, from any process with the uid.]
+ / A 177-second freeze of the editor: #[`fs9_client.connect` ran `connect(2)` on a still-BLOCKING socket, before `setNonblock` and before the deadline existed. On AF_UNIX a full accept backlog waits in `unix_wait_for_peer` for `sk_sndtimeo`, which is forever, and the core is single-threaded. Measured at *177.3 s*, ending only because the peer was killed. Now 2.03 s, the budget.]
+ / A 64 KiB pty read wiping the queue: #[`queue_cap` is 65536 and a record must fit in `queue_cap - 4`, but every host reads a pty master with a 64 KiB buffer and a single read really returns 65536 on Linux. The eviction loop then emptied the queue and dropped the new record too, silently. Deterministic, not a race.]
+ / Four silent sockets denying `--fs9` forever: #[nothing took a slot back, and there are four. `version(5)` requires `Tversion` first and `msize == 0` already meant "has not versioned", so the frontend transport's own five-second rule applied unchanged.]
+ / EMFILE spinning a core: #[`accept` treated every failure as EAGAIN, but EMFILE leaves the connection in the backlog and poll is level triggered. *99.8% of one core.*]
+ / `max_fids = 32` refuting the step's own acceptance clause: #[a mounting client keeps a fid per cached inode, and `docs/9p.typ` §12.4 makes `9pfuse` the proof. At 32, `find` produced *57 consecutive `Rerror`s*. Now 256 for a host and `board_fids` 32 for the board.]
+
+ #verdict[
+ Three of the six are the SAME defect as `9P-24`: a descriptor left in a
+ level-triggered poll set with no error arm, or a wait with no deadline. That
+ is now four instances across five steps, every one of them a whole core or
+ a frozen editor, and every one written by someone who had just read the
+ previous one.
+
+ So make it a rule rather than a lesson: *every descriptor this project adds
+ to a poll set needs an error arm, and every wait needs a deadline set before
+ the thing it is waiting on can begin.* Both are checkable by reading, and
+ both were missed by authors who knew the rule.
+ ]
+
+ #note("method", "2026-08-27")[
+ Two observations about the reviewing, worth more than the bugs.
+
+ The instruction that worked was *"assume the happy path works — it has been
+ demonstrated live — and look everywhere else."* Every finding came from
+ somewhere the demonstration could not reach: a peer that stalls, a mount
+ that is torn down, a buffer at exactly its limit. The demonstrations were
+ all real and all passed, and none of them would ever have found any of this.
+
+ And the reviewers were told to *measure*. Every serious finding arrived with
+ `/proc/<pid>/stat` before and after, or a wall-clock figure, or a count of
+ consecutive errors. A report saying "this could spin" would have been argued
+ with; "998 jiffies per 10 s against a 0-jiffy baseline, here is the script"
+ could not be. The cost of asking for that was a reviewer that took forty
+ minutes instead of ten.
+ ]
+
+ #q[Two findings are NOT fixed and should be. A parked `pty/data` read whose client is gone keeps consuming the queue destructively — measured as 34% of a pane's output going nowhere on a long-running daemon, with the trigger not isolated in 23 attempts. And a `Tclunk` does not sweep park slots on the fid it retires, so a `close(2)` with a read in flight strands the tag forever; 32 of those and the connection can never serve a blocking read again. Both are reachable by ordinary client behaviour.]
+]