Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Speed up reading from in_stream #983

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
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
7 changes: 5 additions & 2 deletions invoke/runners.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import errno
import locale
import io
import os
import struct
import sys
Expand Down Expand Up @@ -71,7 +72,7 @@ class Runner:

opts: Dict[str, Any]
using_pty: bool
read_chunk_size = 1000
read_chunk_size = io.DEFAULT_BUFFER_SIZE
input_sleep = 0.01

def __init__(self, context: "Context") -> None:
Expand Down Expand Up @@ -894,8 +895,10 @@ def handle_stdin(
# race conditions re: unread stdin.)
if self.program_finished.is_set() and not data:
break
# When data is None, we're waiting for input on stdin.
# 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
47 changes: 42 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,52 @@ 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 returns 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 calls because of the Nones 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"),
# null output to not pollute tests
out_stream=StringIO(),
)
# 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