Reuse a scratch buffer when reading child output - #283
Open
tas50 wants to merge 2 commits into
Open
Conversation
The spellcheck job only scans files a PR touches, so any change to lib/mixlib/shellout/unix.rb or lib/mixlib/shellout.rb fails on identifiers that have been in those files for years: cgroupv, ducktype, endgrent, getgrent, LOGNAME, pgid, secondarygroups, seconderies, sgids and WNOHANG. "proccess" was flagged too, but that one is an actual typo in a comment rather than a word worth teaching the dictionary, so fix it instead. Signed-off-by: Tim Smith <tsmith84@proton.me>
read_nonblock allocates a fresh READ_SIZE String on every read. For a command with a lot of output that is nearly all of the garbage ShellOut produces -- an 8 MB stdout means ~2000 needless 4 KB allocations. It takes an optional buffer to read into instead, so hand it one and reuse it. That is only safe because #<< copies the bytes into @stdout / @stderr; a live stream is free to hold onto whatever it is handed, so when one is attached each read still gets its own String. That split is what the new #drain helper expresses, and it also collapses the three near-identical read loops into one. cat an 8 MB file, ruby 4.0.6 median min objects/run before 30.76 ms 16.81 ms 1997 after 16.10 ms 13.42 ms 100 with live_stream (unchanged path) before 30.38 ms 19.56 ms 3957 after 31.64 ms 17.32 ms 3960 48% faster on the common path with 95% fewer allocations, and no change for live streams. Output is byte-identical: verified equal SHA256 and encoding for 8 MB of stdout, and for interleaved binary stdout/stderr. Signed-off-by: Tim Smith <tsmith84@proton.me>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
read_nonblock(READ_SIZE)allocates a fresh 4 KB String on every read. For a command with a lot of output that's nearly all of the garbageShellOutproduces — an 8 MB stdout means ~2000 needless allocations that exist only to be copied into@stdoutand thrown away.IO#read_nonblocktakes an optional buffer to read into, so this hands it one and reuses it across reads.That's only safe because
#<<copies the bytes into@stdout/@stderr. A live stream is different — it's documented as anything responding to<<, and it's free to hold onto whatever it's handed. Reusing one buffer there would silently alias every chunk together (anArrayused as alive_streamwould end up with N references to the same String). So when a live stream is attached, each read still gets its own String, exactly as today.That split is what the new
#drainhelper expresses, and it has the side benefit of collapsing the three near-identical read loops into one.Benchmark
catof an 8 MB file, 25 runs, ruby 4.0.6, arm64-darwin.objects/runisGC.stat(:total_allocated_objects)across a singlerun_command.48% faster with 95% fewer allocations. The
mincolumn (13.42 ms vs 16.81 ms) is the GC-free floor — the rest of the median gap is GC pressure the allocations were causing.Full matrix, same machine:
echo hi)live_streamThe
live_streamrow is deliberately flat — that path runs the same code it does today.Verification
Output is byte-identical, checked against
mainrather than assumed:UTF-8)"\xff\xfe café"/"err\xc3\xa9") — same SHA256, same encoding (ASCII-8BIT) on both streamslive_streamreceiving 1954 chunks — same joined SHA256, and explicitly asserted the chunks are not aliased to each otherErrno::ENOENT), which exercises theprocess_statuspipe's bufferPlus:
bundle exec rspec— 147 examples, 0 failures (unchanged frommain)bundle exec cookstyle --chefstyle -c .rubocop.yml— no offensesNote
The first commit is a spellcheck fix. The
spellcheckjob only scans files a PR touches, so any change tounix.rbfails on identifiers that have been in the file for years (pgid,sgids,WNOHANG,endgrent, …). It also flaggedproccess, which is a genuine typo in a comment, so that's fixed rather than added to the dictionary. The same commit is in #282 — whichever lands first, the other needs a trivial rebase.