Skip the buffer join in respond() when no watchers are attached - #1083
Open
aryansk wants to merge 1 commit into
Open
Skip the buffer join in respond() when no watchers are attached#1083aryansk wants to merge 1 commit into
aryansk wants to merge 1 commit into
Conversation
respond() joins the entire accumulated capture buffer on every output chunk, even when no StreamWatcher is registered -- the common case. That is O(n^2) in total output: with a few hundred MB of subprocess output, runs took hours (the reporter measured ~2h for 300MB). Bail out early when self.watchers is empty, which preserves behavior exactly (the join result is only consumed by the watcher loop) and drops the hot-path cost to a no-op. A watcher-free run of 8000 x 4KB chunks goes from ~10.4s to ~0.5ms. Fixes pyinvoke#1079 🤖 Generated with Codebuff Co-Authored-By: Codebuff <noreply@codebuff.com>
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.
What
Fixes #1079 —
Runner.respond()no longer pays an O(n²) join cost when noStreamWatcheris attached, which is the default/common case.Why
respond()joins the entire accumulated capture buffer on every output chunk, then feeds it to the watcher loop. With no watchers registered (the normal case), that join result is unused — but it still runs, making output handling quadratic in total output size. The reporter measured ~2 hours to process a 300MB pytest output;Channel.sendall-style slicing was fixed elsewhere for the same reason (#892 reference in the issue).Local benchmark, simulating
_handle_output's append-then-respond loop with 4KB chunks and no watchers:How
Early-return from
respond()whenself.watchersis empty. Behavior is identical — the join's result is only consumed inside the watcher loop, so skipping it when there are no watchers cannot change observable behavior. Watcher-driven autoresponding (thewatcherskwarg ofrun) is untouched.Tests
Added
Runner_.watchers.no_watchers_skips_the_buffer_joinintests/runners.py: a non-string buffer element is the canary — pre-fix,respond(["chunk", 42])raisedTypeErrorfrom the join; post-fix it is a no-op. Full suite: 860 passed; the 114 failures are pre-existing onmainin this environment (pty/terminal thread handling), identical with and without this change. Ruff + black clean (pre-existing repo-wide drift excluded).🤖 Generated with Codebuff