daemon: CPU time under sustained output is 1.3-1.9x the leading peer's #399
Labels
No labels
priority/P0
priority/P1
priority/P2
release/v0.1.0
status/blocked
status/planned
type/bug
type/design
type/test-gap
type/tracker
No milestone
No project
No assignees
1 participant
Due date
No due date set.
Dependencies
No dependencies set
Reference
natsukium/felis#399
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Under sustained output felis spends more CPU than its peers (
docs/reference/benchmarks.md, felis9d1d842f):On macOS felis used about 1.65 cores for the length of the pass against alacritty's 0.9-1.0, and the published split puts 95% or more of it in the daemon (#398 questions that split; settle it first). #397 addresses the client's per-cell hashing and does not touch the daemon.
Throughput is not the problem: felis leads DOOM-fire's frame rate on both platforms. The cost is CPU spent per byte parsed.
Plan:
perf-traceskill). #394 has to land first, or the samply harness cannot find the systemd-run hand-off daemon.Done when the daemon's CPU per workload drops by a measured amount without losing frame rate or
cattime, or the profile shows the remaining cost is inherent and the reason is recorded.Linux profile of the daemon (kilimanjaro, i5-12400F, niri 60 Hz). felis
mainef96bb46, release build with line tables. Load average (1 min) is given with each figure. The host was running CI the whole time (load 5-21), so treat single-run wall-clock figures as noisy; per-thread CPU splits and profile shares are stable across runs.Method. samply cannot run here:
perf_event_paranoidis 2 and there is no sudo. I usedperf7.2.7 in user-only mode (-e cycles:u --call-graph lbr), with the daemon started under perf (perf record … -- felis-daemon serve --socket $SOCK) and the client pointed at that socket. That is the same launch shape as the fixedsamply-profile.sh(#394). Kernel time is not sampled at this paranoid level, so per-thread user/sys split,readsyscall counts (/proc/<pid>/task/<tid>/iosyscr/rchar) and voluntary context switches come from/procread at the pass's start and end markers.Where the daemon's CPU goes, per thread
felis-pty-parserfelis-pty-reader(user + sys)tokio-rt-workerscrolling12 s, manualThe reader thread, which only calls
read(2)and appends to the swap buffer, costs more than the parser on vtebench and about 28% of the daemon on DOOM-fire.DOOM-fire parser profile (self time, % of parser samples)
Grid::put_grapheme(crates/felis-grid/src/editing.rs:209) 11.0,Params::push_run(crates/felis-vt/src/lib.rs:319) 10.7,Grid::print_wide_str(crates/felis-grid/src/sink.rs:896) 9.7,build_sinkclosure withParser::advanceinlined (crates/felis-daemon/src/parse_sink.rs:122) 8.9,StyleTable::intern(crates/felis-grid/src/style_table.rs:71) 8.2,Grid::csi_dispatch(crates/felis-grid/src/sink.rs:240) 7.4,Parser::advance_byte(crates/felis-vt/src/machine.rs:75) 6.0,unicode_width::lookup_width6.1,Attributes::apply_sgr4.2,hash_one<Attributes>4.0,Attributes::equivalent3.7,from_utf83.3.Inclusive:
print_utf8_run36%,dispatch_csi33%, andresync_pen_style(crates/felis-grid/src/lib.rs:1706, theStyleTable::internhash lookup on every SGR) 21%.vtebench
scrollingparser:Grid::print_str60% self, of whichcrates/felis-grid/src/sink.rs:136alone is 24%. That line is the wide-partner check, which loads the target cell of a row that scrolling has just recycled, and costs a cache miss per line.scroll_full_screen_into_history11%,scroll_region_up6%.The suspects, ranked
The reader's spin (
MasterReader::read,crates/felis-pty/src/unix.rs:67-104,READ_SPIN_WINDOW50 µs). OnEAGAINit retries non-blockingread(2)for 50 µs beforepoll(2). That window was measured against the macOS 1 KiB lockstep (3cc9cba0). On Linux, the same DOOM-fire pass with the spin set to 0 (env-gated local patch, not committed):The frame rate is unchanged within noise, and reader user time drops by 1.4-1.7 s per 12 s, about 7% of the daemon.
Tiny reads on line-heavy output. On
scrollingthe reader does 13-16 bytes perread(2), about 1.2 M reads/s, with 8-9.7 s of sys time per 12 s. The spin makes no difference there: the data is always present in 13-byte pieces. A local patch that sleeps 20 µs after a read under 256-2048 bytes while the parser is busy cut the reader from about 9.9 s to 0.76 s per 12 s (-92%), with scrolling medians 78-99 ms against 89-109 ms. The same back-off applied unconditionally starves the parser under DOOM-fire (933 → 400 fps; 4 KiB is the ceiling of one PTY read on Linux,N_TTY_BUF_SIZE), so a real fix must back off only while the parser still has a backlog. Details in #400.SGR style interning, 21% of the parser under DOOM-fire. Every SGR re-interns the whole
Attributesthrough the hash map (hash plus equality). A one-entry cache for the last(Attributes, StyleId), or skipping the intern when the SGR left the pen unchanged, removes most of that for streams that repeat colors. Expected gain: up to about 15-20% of parser time on DOOM-fire.CSI parameter parsing (
Params::push_run), 10.7% of the parser: the next parser-side candidate after interning.Ruled out: the #380 flood-drain yield and parse-lock hand-off. All tokio workers together spend 0.41 s per 15 s of DOOM-fire (2% of the daemon), and the parser thread made about 190 voluntary context switches per second (2,858 in 15 s), so the hand-off is not a wakeup storm.
Ruled out: per-paint client-update encoding.
wire::encode_rowplusput_attr_runare 0.46% of all samples.Ruled out: reader→parser copies.
memmoveis 0.26% of samples. The copy into the swap buffer is negligible next to the syscall cost.Expected combined gain on Linux: removing most of the reader cost (items 1-2) takes about 25-55% off daemon CPU depending on the workload (5.75 of 20.3 s on DOOM-fire, 71.5 of 124 s on vtebench), which would put felis's vtebench total (132 s published) under alacritty's 104 s. Item 3 is a further 10-15% of daemon CPU on DOOM-fire. Before landing either, re-measure on a quiet host, and on macOS, where the spin was a measured win.