Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions invoke/runners.py
Original file line number Diff line number Diff line change
Expand Up @@ -944,6 +944,11 @@ def respond(self, buffer_: List[str]) -> None:

.. versionadded:: 1.0
"""
# Nothing to do unless at least one watcher is attached; bail early so
# the (potentially O(n^2)) join below never runs in the common case of
# running without watchers while a subprocess emits lots of output.
if not self.watchers:
return
# Join buffer contents into a single string; without this,
# StreamWatcher subclasses can't do things like iteratively scan for
# pattern matches.
Expand Down
10 changes: 10 additions & 0 deletions tests/runners.py
Original file line number Diff line number Diff line change
Expand Up @@ -982,6 +982,16 @@ def nothing_is_written_to_stdin_by_default(self):
self._runner(klass=klass).run(_)
assert not klass.write_proc_stdin.called

def no_watchers_skips_the_buffer_join(self):
# Regression test for #1079: respond() used to join the whole
# accumulated capture buffer on every chunk even with no watchers
# attached, an O(n^2) cost that made runs with large outputs
# (hundreds of MB) take hours. With no watchers it must return
# without touching the buffer -- the canary here is a non-string
# element, which raises TypeError if a join ever runs.
runner = self._runner()
runner.respond(["chunk", 42])

def _expect_response(self, **kwargs):
"""
Execute a run() w/ ``watchers`` set from ``responses``.
Expand Down