Skip to content

Commit

Permalink
Only sleep when input stream is waiting
Browse files Browse the repository at this point in the history
This change means that we don't delay reading the input stream when
there is still data available to read from it.

This provides a significant speed improvement to scripts which are
passing a populated stream of data, rather than awaiting user input from
stdin.

See #774
  • Loading branch information
meshy committed Jan 20, 2024
1 parent 506bf4e commit 63f10c2
Show file tree
Hide file tree
Showing 2 changed files with 43 additions and 6 deletions.
3 changes: 2 additions & 1 deletion invoke/runners.py
Original file line number Diff line number Diff line change
Expand Up @@ -895,7 +895,8 @@ def handle_stdin(
if self.program_finished.is_set() and not data:
break
# Take a nap so we're not chewing CPU.
time.sleep(self.input_sleep)
if data is None:
time.sleep(self.input_sleep)

def should_echo_stdin(self, input_: IO, output: IO) -> bool:
"""
Expand Down
46 changes: 41 additions & 5 deletions tests/runners.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import types
from io import StringIO
from io import BytesIO
from io import TextIOBase
from itertools import chain, repeat

from pytest import raises, skip
Expand Down Expand Up @@ -1098,16 +1099,51 @@ def subclasses_can_override_input_sleep(self):
class MyRunner(_Dummy):
input_sleep = 0.007

def fake_stdin_stream():
# The value "foo" is eventually returned.
yield "f"
# None values simulate waiting for input on stdin.
yield None
yield "o"
yield None
yield "o"
yield None
# Once the stream is closed, stdin continues to return empty strings.
while True:
yield ''

class FakeStdin(TextIOBase):
def __init__(self, stdin):
self.stream = stdin

def read(self, size):
return next(self.stream)

with patch("invoke.runners.time") as mock_time:
MyRunner(Context()).run(
_,
in_stream=StringIO("foo"),
in_stream=FakeStdin(fake_stdin_stream()),
out_stream=StringIO(), # null output to not pollute tests
)
# Just make sure the first few sleeps all look good. Can't know
# exact length of list due to stdin worker hanging out til end of
# process. Still worth testing more than the first tho.
assert mock_time.sleep.call_args_list[:3] == [call(0.007)] * 3
# Just make sure the sleeps all look good.
# There are three here because we return three "None" in fake_stdin_stream.
assert mock_time.sleep.call_args_list == [call(0.007)] * 3

@mock_subprocess()
def populated_streams_do_not_sleep(self):
class MyRunner(_Dummy):
read_chunk_size = 1

runner = MyRunner(Context())
with patch("invoke.runners.time") as mock_time:
with patch.object(runner, "wait"):
runner.run(
_,
in_stream=StringIO("lots of bytes to read"),
out_stream=StringIO(), # null output to not pollute tests
)
# Sleep should not be called before we break.
assert len(mock_time.sleep.call_args_list) == 0

class stdin_mirroring:
def _test_mirroring(self, expect_mirroring, **kwargs):
Expand Down

0 comments on commit 63f10c2

Please sign in to comment.