summaryrefslogtreecommitdiff
path: root/features.txt
Commit message (Collapse)AuthorAge
* Draw a frame only when there is one worth drawingGabriel Schneider5 days
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | A 9P round trip on a local Unix socket cost 9.7 ms against the SDL shell and 0.758 ms against the terminal one. The server was not slow and the wake was not broken: instrumenting the path showed every request waking the loop early (wakes=210, woke_early=212, timed_out=38 over 250 ticks) and being answered on that same pass. The cost was that `pump` answers 9P at one point in a loop that then renders and presents unconditionally, so a client's next request landed while the main thread was blocked on the display, and each round trip therefore cost a whole frame. The frame rate was governing something that has nothing to do with drawing. `Pardes.needs_frame` starts true, is set by every event except a tick with nothing animating and a filesystem request that only reads, and is cleared once a frame is presented. `pump` returns before render and present when it is false and nothing is animating. An idle editor answering reads now draws nothing at all. 9P read_fid, one RPC: gui 9.7 ms -> 0.056 ms (173x) tty 0.758 ms -> 0.062 ms (12x) Verified the shells still paint rather than going quiet: the rendered screen carries the opened file, a write through 9P redraws within the frame, and `fs-discovery-test` passes over the real wire. Suite unchanged at 778/783 with the two pre-existing crashes. Also from the adversarial review of the previous commits: `pardes --tty FILE` silently discarded the file, and `--tty MISSING` silently discarded the error pane. main.zig named the boot layout before the positional was resolved, and naming one short-circuits `Boot.of`. The choice now happens after the argument is known, and only when there is no file and no missing word. macos.zig names the same layout, so the app no longer boots a different one from the terminal and SDL shells. `pre_close_last_pane_tail` was transcribed from the NEW default rather than the old one, so the upgrade path it was added for did not exist: a workspace dumped before the tagline reorder came back with the old default welded on as a custom tail. It is now the string it claims to be. A pane two rows tall lost its message and, worse, its prompt and the cursor with it. The notice cap keeps the LAST notices now, because the prompt is last and a prompt you cannot see is one you type into blind. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
* Stack a pane's transient lines instead of letting the last one win the rowGabriel Schneider5 days
| | | | | | | | | | | | | | | | | | | | | | | | | | | A pane's message, its pending leader chord and a prompt waiting for input all wanted the same row above the tagline. The prompt won it, the leader drew over whatever was there on the right, and renderBodyLayer recomputed "is anything down there" inline to reserve a single row. Three claimants, one row, and two places deciding. Pane.Notices is that decision in one place: a short ordered list of the lines a pane is showing, rebuilt every frame by collectNotices from the state that owns each one. The body layer reserves exactly notices.len rows, the way it already reserves rows for sticky context headers, and the paint pass walks the list and gives each line a row of its own, stacked upward from the tagline. Nothing stores a second copy of the truth, so a line that goes away is simply not added next frame and the rest close the gap. One notice lands on exactly the row the message always had, and the prompt stays nearest the tagline so it keeps its cursor. Last also learned what to do when the jumplist is empty. It used to walk the jumps and, finding nothing, do nothing at all -- which is the ordinary case for a pane that opened beside this one and was never focused, such as the text pane the bare tty layout puts under the shell. It now falls back to neighbourPane: the next pane down the column, wrapping, and any other live pane failing that. Alternating with the neighbour is what Last is for. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
* Measure time to first paint, which is the startup number that mattersGabriel Schneider5 days
| | | | | | | | | | | | | | | | | | | | | | | | | | --version measures a process that exits before doing any work. On a pty the tty shell emits its first byte at 20 ms and settles at 50 ms; the gui takes ~330 ms to map its window, and that is the latency worth noticing. Of the gui's 330 ms, ~95% is CPU and only ~30 ms is waiting. Profiled warm and killed before the map: pardes-gui 28.5%, libc 20.7%, ld-linux 11.9%, libudev 10.3%, the nvidia stack ~21%. Cutting across all of those, DWARF unwinding is 20.9% of samples -- the Debug allocator capturing six frames per allocation, the same cost the previous change found, and now the largest single slice. Two of my own measurements were wrong and are corrected here. An earlier ~510 ms figure was an artifact: the probe redirected HOME, so the nvidia shader cache was cold and the driver recompiled every launch. And deferring SDL_INIT_GAMEPAD past the first frame, on the theory that libudev's 10% was gamepad enumeration, changed the map time not at all (320-366 ms either way) and libudev only 10.3% -> 9.6%: that work belongs to SDL_INIT_VIDEO bringing up Wayland seats. It is reverted rather than kept as an unmeasurable win. So nothing in the editor's own logic is slow to start. What is left is the Debug allocator and the GPU stack, and the only lever on either is the build mode -- which is not worth tripling a ten-minute build for. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
* Measure startup, and find there is nothing in it worth optimizingGabriel Schneider5 days
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | A --startup case in the perf harness times Pardes.init and the first frame for each boot layout, with the same --json and --base shape as the others, so these are a baseline rather than a one-off reading. The core boots and paints in 0.22 ms (tty) to 0.30 ms (classic): about one percent of what starting the editor costs, and nothing to win. The other 99% is one thing, found with perf record rather than guessed at. Zig's start.main allocates through the Debug-mode DebugAllocator, which captures a six-frame stack trace per allocation, and the first capture parses and sorts the DWARF unwind tables of an 800 MB binary -- 22% of all samples sit in mem.swap under that pdq sort. It is not the dynamic loader (25 us), not static initializers (the binary has no .init_array), not paging (435 page faults), and not lockStderr. std/start.zig:694 hardcodes DebugAllocator(.{}), so there is no knob short of the build mode. Controls that pin it down: a bare std.process.Init hello-world starts in 3 ms, the ReleaseFast pardes-perf binary in 4 ms, /bin/true in 1 ms, and a Debug pardes in 22 ms. The gui binary's 107 ms first run was cold page cache; warm it matches the tty one. So the conclusion recorded in features.txt is to leave it alone. Twenty-two milliseconds is imperceptible for an editor, and the only lever is switching the default build mode, which would multiply an already ten-minute build to save eighteen milliseconds of startup. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
* Make a pane by opening /pane/new, and a Plan 9 idiom passGabriel Schneider5 days
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | The Tcreate that replaced acme's /new was a step away from the idiom dressed up as a step toward it. A pane is named by a server-assigned serial, so the create ignored the client's name: `mkdir /pane/foo` succeeded and left you /pane/12. A mkdir that does not make the directory you named is worse than the read-with-side-effect it replaced, and it broke in the shell workflow that motivated the change. `create` is out of the declared features, so Tcreate is EPERM again; Tremove stays, since `rm` to close a pane is unambiguously right. /pane/new is now opened, not created: the open makes the pane, the read of that fid answers its serial, two reads agree, and closing it leaves the pane. That is /net/tcp/clone's mechanism (kernel/network/ip/devip.c, in ipopen), not acme's, and the difference is deliberate. acme allocates during the walk and lands inside the new window, so /dev/new/body works in one step, and it can afford to list `new` because a Plan 9 directory read carries every entry's stat and nothing walks. A kernel or FUSE mount walks and stats each name a listing gave it, so allocate-on-walk would make a pane per `ls -l`. Allocating on open keeps `new` listed -- a stat is not an open -- at the cost of the one-step new/body. `new` stays unreachable from an editor path, because that resolution serves Look hover previews. The idiom pass behind it, read out of the Plan 9 tree at ~/05-genizah/principia-softwarica rather than recalled: Rerror carries a string, not an errno (man 5 error: `ename[s]`), and acme names every refusal. The five refusals pardes shares with acme now say what they mean; the generic sites keep their bare errno rather than invent strings acme does not have. body and tag declare DMAPPEND, which they had always behaved as (acme(4): "always appended; the file offset is ignored"), checked first against Linux's fs/9p, which never maps the bit. excl stays unset everywhere, because acme sets DMEXCL on nothing. Blocking reads, per-object addr scope and the readable pane ctl were already right. Real stat sizes and qid versions stay: acme reports length 0 and version 0 for everything, and Linux clients need better. One bug fell out of it. open reset the addr range, so `echo '#0,#5' >addr; cat addr` answered `0 0` and `cp addr dot` copied zeros. acme(4) makes the contract explicit -- "a regular expression may be evaluated by writing it to addr and reading it back" -- and acme gets away with resetting on the 0-to-1 open only because its clients hold the fid across both. A shell cannot: that is two opens. The register is cleared by truncating it now. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
* Plan 9 idiom for the control filesystem, and the regressions a624a56 leftGabriel Schneider5 days
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | The 9P tree stops being a command language wearing a filesystem. /new created a pane as a side effect of a *read*; it is now Tcreate in /pane, with Tremove to close, which cloud9's engine has always supported and the editor never declared: tree.zig now says `features = .{ .create = true, .remove = true }`. Eleven pane ctl verbs become files that can be read as well as written -- dot, limit, dirty, mark, scroll, look, exec -- leaving ctl with `get`, the one verb no file would say better. Root /ctl splits into a read-only /status and the /look and /exec files whose write IS the click. stat carries real sizes where it used to answer 0, and qid versions track a pane's revision, so a client can poll for change without re-reading the body. Commit a624a56 moved raw-tty keys to an early-return branch that knew only Ctrl-B and bare Escape, and in the same edit deleted the paste branch below it. That cost Shift-Escape (the unconditional way out of tty mode) and both paste chords: Ctrl-V and Ctrl-Shift-V reached the child as keystrokes, so an agent CLI running in a pane took Ctrl-V for its image-paste binding and answered "No image found in clipboard". Both are restored, with tests. Nested detection was not subtly broken but deleted: 60367d8 removed nested.zig's process-ancestry walk and left "am I inside pardes" derived from PARDES_FORWARD_LOOK, which read "0" both for --nested and for "the listener did not come up". PARDES_PID now answers that question on its own, checked with kill(pid, 0); PARDES_9P and PARDES_PANE answer how to reach it; the flag is gone. The posted-9P registry also self-heals now -- a session that aborts cannot unlink its own socket, so posting sweeps entries whose target refuses a connection, symlinks only and on a definite ECONNREFUSED only. Elsewhere: tty scrolling is sticky-bottom, following new output only from the last row, with typing and entering raw mode snapping back to live; the boot layouts are a Boot enum instead of a chain of ifs, and the bare tty startup (Boot.tty, which main.zig names) opens an empty text pane under the shell while tests keep Boot.tty_shell; builtins announce themselves on the message row under a Verbose setting that is on by default; Config prints each setting the way you would type it back, so WindowOpacity 70 rather than "WindowOpacity: 70%"; LocationsConfig opens its window only when called bare; every tagline puts the word that closes the thing last, and a column now outlives its panes -- closing the last one leaves an empty pane, and only Delcol, newly on the column tagline, takes the column away. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
* Add Linux Tty9p mounted terminals and forward raw TTY keysGabriel Schneider2026-09-15