Skip to content

Perf/io pipelining followup - #10

Open
nyo16 wants to merge 2 commits into
masterfrom
perf/io-pipelining-followup
Open

Perf/io pipelining followup#10
nyo16 wants to merge 2 commits into
masterfrom
perf/io-pipelining-followup

Conversation

@nyo16

@nyo16 nyo16 commented Aug 1, 2026

Copy link
Copy Markdown
Owner

No description provided.

nyo16 added 2 commits July 29, 2026 10:06
…gnment

Second measurement-driven cycle against the code v1.3.0 produced. One liveness
bug, one resource leak, one one-byte constant worth 42% of read wall time, and
three responsiveness fixes. Harness committed under bench/ so the next cycle
does not re-litigate any of it from code reading.

Fixed
-----
run/2 hung forever on :input larger than ~128 KiB. run_io/3 wrote all of
:input to completion before the read loop started, so any filter command
(cat, gzip, jq) filled its 64 KiB stdout pipe, blocked in write(2), stopped
draining stdin, and left us blocked filling stdin. Default :timeout is nil,
i.e. :infinity, so there was no escape. Completed at <= 131072 bytes, hung
indefinitely at >= 262144. stream!/2 was never affected -- it has always
written from a Task. Writer and reap now live in NetRunner.InputWriter, shared
by both entry points so they cannot drift apart again. 16 MiB through cat:
run/2 1163 MB/s vs stream!/2 1130 MB/s.

run/2 and stream!/2 each leaked a Process GenServer, a Watcher, a UDS socket
and three pipe FDs per call. Neither hands the pid to the caller, so nothing
could stop them; teardown relied on an owner monitor that only fires when the
caller dies -- unbounded for a long-lived caller. Measured 100 leaked
processes per 50 calls, now 0. New public NetRunner.Process.stop/1. This also
resolves the teardown question cycle 1 left open.

Shepherd now sends MSG_CHILD_EXITED before cgroup_cleanup(), which retries
rmdir ten times with usleep(100000) between. With --cgroup-path a caller could
wait a full second in await_exit for a status the shepherd already held.
NOT VERIFIED ON LINUX -- see bench/LINUX_VERIFICATION.md.

Changed
-------
Default read size 65535 -> 65536. One byte under pipe capacity leaves one byte
behind in a saturated pipe, costing a whole extra GenServer round trip: 64 MiB
read goes 1773-1834 chunks / 26-31 ms -> 1024 chunks / 18-19 ms. 65536 is also
exactly nif_read's stack buffer, so it stays on the allocation-free path; see
ADR-9. Pipe.read/2's duplicate default was deleted rather than redirected.

consume_stderr/1 bounded at 16 chunks (~1 MiB) per pass with a self-sent
resume. Honest scope: worst concurrent handle_call under a 256 MB stderr flood
measured 279 us, not the seconds the unbounded recursion suggests -- pipe
capacity already capped it. Now 52 us, at the idle jitter floor. Drain
throughput unchanged.

Daemon.write/2 no longer blocks the Daemon on a backpressured child, and
on_output: :log coalesces instead of one Logger call per chunk.

Performance
-----------
Stream no longer polls the writer with Task.yield(writer, 0) on every stdout
chunk -- O(mailbox length) per chunk for a GenServer/LiveView consumer.
append_stderr_tail's steady-state branch builds at exactly cap in one pass
instead of concat-then-slice. Write and drain loops fold byte and syscall
counts through parameters instead of rebuilding state per iteration.

Verification
------------
make clean && make all warning-free under -Wall -Wextra -Werror;
--warnings-as-errors, format, credo, dialyzer (0 errors), mix docs (0
warnings); 184 tests passing; UBSan suite clean. All three new regression
guards were mutation-checked -- notably the stderr drain test, whose obvious
form passed the mutation because a 64 KiB pipe never lets a pass reach its
budget.

Unverified: Linux. Phase 3 is a provable no-op on macOS and the read-size win
is macOS-shaped (Linux pipes are 1 MiB). ASan build is clean but its suite
cannot run on macOS -- SIP strips DYLD_INSERT_LIBRARIES before beam.smp.
Instructions in bench/LINUX_VERIFICATION.md.
Move .claude/{audit,plans,prs} to tmp/, which is already gitignored, and
ignore /.claude/ wholesale. These are local working notes, not part of the
library.

bench/LINUX_VERIFICATION.md cross-referenced those files heavily, which would
have left the Linux handoff pointing at paths absent from a fresh clone. Every
such reference is now inlined instead: the mutation-testing lesson, ADR-9's
upper bound on the read size, and the macOS baseline it says to compare
against. The doc no longer depends on anything outside the repo, and says so.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant