diff --git a/invoke/runners.py b/invoke/runners.py index c59481399..9c88dae45 100644 --- a/invoke/runners.py +++ b/invoke/runners.py @@ -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. diff --git a/tests/runners.py b/tests/runners.py index f3a49dd20..977fba860 100644 --- a/tests/runners.py +++ b/tests/runners.py @@ -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``.