Skip to content

Commit 176fac0

Browse files
committed
Merge bitcoin/bitcoin#33141: test: Remove polling loop from test_runner (take 2)
fa4885e test: Remove polling loop from test_runner (MarcoFalke) Pull request description: (This picks up my prior attempt from bitcoin/bitcoin#13384) Currently, the test_runner is using a `time.sleep` before polling to check if any tests have completed. This is largely fine when running a few tests, or when the tests take a long time. However, when running many fast tests, this can accumulate and leave the CPU idle for no reason. A trivial improvement would be to only sleep when really needed: ```diff diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index 7c8c15f..1d9f28cee4 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -747,7 +747,6 @@ class TestHandler: dot_count = 0 while True: # Return all procs that have finished, if any. Otherwise sleep until there is one. - time.sleep(.5) ret = [] for job in self.jobs: (name, start_time, proc, testdir, log_out, log_err) = job @@ -771,6 +770,7 @@ class TestHandler: ret.append((TestResult(name, status, int(time.time() - start_time)), testdir, stdout, stderr, skip_reason)) if ret: return ret + time.sleep(.5) if self.use_term_control: print('.', end='', flush=True) dot_count += 1 ``` However, ideally there is no sleep at all. So do that by using a `ThreadPoolExecutor`. This can be tested via something like: ``` time ./bld-cmake/test/functional/test_runner.py $(for i in {1..200}; do echo -n "tool_rpcauth "; done) -j 200 ``` The result should show: * Current `master` is the slowest * The "sleep patch" from above is a bit faster (1.5x improvement) * This pull request is the fastest (2x improvement) ACKs for top commit: achow101: ACK fa4885e l0rinc: tested ACK fa4885e Eunovo: ReACK bitcoin/bitcoin@fa4885e Tree-SHA512: f097636c5d9e005781012d8e20c2886cd9968544d4d555b1d2e28982d420ff63fec15cfabb6bd30e4d3c389b8b8350a1ddad721cceaf4b7760cad38b95160175
2 parents 593d5fe + fa4885e commit 176fac0

File tree

1 file changed

+46
-33
lines changed

1 file changed

+46
-33
lines changed

test/functional/test_runner.py

Lines changed: 46 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
import argparse
1616
from collections import deque
17+
from concurrent import futures
1718
import configparser
1819
import csv
1920
import datetime
@@ -706,15 +707,15 @@ class TestHandler:
706707
"""
707708
Trigger the test scripts passed in via the list.
708709
"""
709-
710710
def __init__(self, *, num_tests_parallel, tests_dir, tmpdir, test_list, flags, use_term_control):
711711
assert num_tests_parallel >= 1
712+
self.executor = futures.ThreadPoolExecutor(max_workers=num_tests_parallel)
712713
self.num_jobs = num_tests_parallel
713714
self.tests_dir = tests_dir
714715
self.tmpdir = tmpdir
715716
self.test_list = test_list
716717
self.flags = flags
717-
self.jobs = []
718+
self.jobs = {}
718719
self.use_term_control = use_term_control
719720

720721
def done(self):
@@ -731,47 +732,59 @@ def get_next(self):
731732
test_argv = test.split()
732733
testdir = "{}/{}_{}".format(self.tmpdir, re.sub(".py$", "", test_argv[0]), portseed)
733734
tmpdir_arg = ["--tmpdir={}".format(testdir)]
734-
self.jobs.append((test,
735-
time.time(),
736-
subprocess.Popen([sys.executable, self.tests_dir + test_argv[0]] + test_argv[1:] + self.flags + portseed_arg + tmpdir_arg,
737-
text=True,
738-
stdout=log_stdout,
739-
stderr=log_stderr),
740-
testdir,
741-
log_stdout,
742-
log_stderr))
735+
736+
def proc_wait(task):
737+
task[2].wait()
738+
return task
739+
740+
task = [
741+
test,
742+
time.time(),
743+
subprocess.Popen(
744+
[sys.executable, self.tests_dir + test_argv[0]] + test_argv[1:] + self.flags + portseed_arg + tmpdir_arg,
745+
text=True,
746+
stdout=log_stdout,
747+
stderr=log_stderr,
748+
),
749+
testdir,
750+
log_stdout,
751+
log_stderr,
752+
]
753+
fut = self.executor.submit(proc_wait, task)
754+
self.jobs[fut] = test
743755
if not self.jobs:
744756
raise IndexError('pop from empty list')
745757

746758
# Print remaining running jobs when all jobs have been started.
747759
if not self.test_list:
748-
print("Remaining jobs: [{}]".format(", ".join(j[0] for j in self.jobs)))
760+
print("Remaining jobs: [{}]".format(", ".join(sorted(self.jobs.values()))))
749761

750762
dot_count = 0
751763
while True:
752764
# Return all procs that have finished, if any. Otherwise sleep until there is one.
753-
time.sleep(.5)
765+
procs = futures.wait(self.jobs.keys(), timeout=.5, return_when=futures.FIRST_COMPLETED)
766+
self.jobs = {fut: self.jobs[fut] for fut in procs.not_done}
754767
ret = []
755-
for job in self.jobs:
756-
(name, start_time, proc, testdir, log_out, log_err) = job
757-
if proc.poll() is not None:
758-
log_out.seek(0), log_err.seek(0)
759-
[stdout, stderr] = [log_file.read().decode('utf-8') for log_file in (log_out, log_err)]
760-
log_out.close(), log_err.close()
761-
skip_reason = None
762-
if proc.returncode == TEST_EXIT_PASSED and stderr == "":
763-
status = "Passed"
764-
elif proc.returncode == TEST_EXIT_SKIPPED:
765-
status = "Skipped"
766-
skip_reason = re.search(r"Test Skipped: (.*)", stdout).group(1)
767-
else:
768-
status = "Failed"
769-
self.jobs.remove(job)
770-
if self.use_term_control:
771-
clearline = '\r' + (' ' * dot_count) + '\r'
772-
print(clearline, end='', flush=True)
773-
dot_count = 0
774-
ret.append((TestResult(name, status, int(time.time() - start_time)), testdir, stdout, stderr, skip_reason))
768+
for job in procs.done:
769+
(name, start_time, proc, testdir, log_out, log_err) = job.result()
770+
771+
log_out.seek(0), log_err.seek(0)
772+
[stdout, stderr] = [log_file.read().decode('utf-8') for log_file in (log_out, log_err)]
773+
log_out.close(), log_err.close()
774+
skip_reason = None
775+
if proc.returncode == TEST_EXIT_PASSED and stderr == "":
776+
status = "Passed"
777+
elif proc.returncode == TEST_EXIT_SKIPPED:
778+
status = "Skipped"
779+
skip_reason = re.search(r"Test Skipped: (.*)", stdout).group(1)
780+
else:
781+
status = "Failed"
782+
783+
if self.use_term_control:
784+
clearline = '\r' + (' ' * dot_count) + '\r'
785+
print(clearline, end='', flush=True)
786+
dot_count = 0
787+
ret.append((TestResult(name, status, int(time.time() - start_time)), testdir, stdout, stderr, skip_reason))
775788
if ret:
776789
return ret
777790
if self.use_term_control:

0 commit comments

Comments
 (0)