Skip to content

Commit 1087f17

Browse files
committed
test: new benchmark.py for comparing performance
Also, delete the old perf/ directory which isn't useful.
1 parent 0aa1070 commit 1087f17

File tree

4 files changed

+300
-488
lines changed

4 files changed

+300
-488
lines changed

lab/benchmark.py

Lines changed: 300 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,300 @@
1+
"""Run performance comparisons for versions of coverage"""
2+
3+
import contextlib
4+
import dataclasses
5+
import os
6+
import shutil
7+
import statistics
8+
import subprocess
9+
import time
10+
from pathlib import Path
11+
12+
from typing import Iterator, List, Optional, Tuple, Union
13+
14+
15+
class ShellSession:
16+
"""A logged shell session.
17+
18+
The duration of the last command is available as .last_duration.
19+
"""
20+
21+
def __init__(self, output_filename: str):
22+
self.output_filename = output_filename
23+
self.last_duration: float = 0
24+
25+
def __enter__(self):
26+
self.foutput = open(self.output_filename, "a", encoding="utf-8")
27+
print(f"Logging output to {os.path.abspath(self.output_filename)}")
28+
return self
29+
30+
def __exit__(self, exc_type, exc_value, traceback):
31+
self.foutput.close()
32+
33+
def print(self, *args, **kwargs):
34+
print(*args, **kwargs, file=self.foutput)
35+
36+
def run_command(self, cmd: str) -> str:
37+
"""
38+
Run a command line (with a shell).
39+
40+
Returns:
41+
str: the output of the command.
42+
43+
"""
44+
self.print(f"\n========================\n$ {cmd}")
45+
start = time.perf_counter()
46+
proc = subprocess.run(
47+
cmd,
48+
shell=True,
49+
check=False,
50+
stdout=subprocess.PIPE,
51+
stderr=subprocess.STDOUT,
52+
)
53+
output = proc.stdout.decode("utf-8")
54+
self.last_duration = time.perf_counter() - start
55+
self.print(output, end="")
56+
self.print(f"(was: {cmd})")
57+
self.print(f"(in {os.getcwd()}, duration: {self.last_duration:.3f}s)")
58+
59+
if proc.returncode != 0:
60+
self.print(f"ERROR: command returned {proc.returncode}")
61+
raise Exception(
62+
f"Command failed ({proc.returncode}): {cmd!r}, output was:\n{output}"
63+
)
64+
65+
return output.strip()
66+
67+
68+
def rmrf(path: Path) -> None:
69+
"""
70+
Remove a directory tree. It's OK if it doesn't exist.
71+
"""
72+
if path.exists():
73+
shutil.rmtree(path)
74+
75+
76+
@contextlib.contextmanager
77+
def change_dir(newdir: Path) -> Iterator[Path]:
78+
"""
79+
Change to a new directory, and then change back.
80+
81+
Will make the directory if needed.
82+
"""
83+
old_dir = os.getcwd()
84+
newdir.mkdir(parents=True, exist_ok=True)
85+
os.chdir(newdir)
86+
try:
87+
yield newdir
88+
finally:
89+
os.chdir(old_dir)
90+
91+
92+
@contextlib.contextmanager
93+
def file_replace(file_name: Path, old_text: str, new_text: str) -> Iterator[None]:
94+
"""
95+
Replace some text in `file_name`, and change it back.
96+
"""
97+
if old_text:
98+
file_text = file_name.read_text()
99+
if old_text not in file_text:
100+
raise Exception("Old text {old_text!r} not found in {file_name}")
101+
updated_text = file_text.replace(old_text, new_text)
102+
file_name.write_text(updated_text)
103+
try:
104+
yield
105+
finally:
106+
if old_text:
107+
file_name.write_text(file_text)
108+
109+
110+
class ProjectToTest:
111+
"""Information about a project to use as a test case."""
112+
113+
# Where can we clone the project from?
114+
git_url: Optional[str] = None
115+
116+
def __init__(self):
117+
self.slug = self.git_url.split("/")[-1]
118+
self.dir = Path(self.slug)
119+
120+
def get_source(self, shell):
121+
"""Get the source of the project."""
122+
if self.dir.exists():
123+
rmrf(self.dir)
124+
shell.run_command(f"git clone {self.git_url}")
125+
126+
def prep_environment(self, env):
127+
"""Prepare the environment to run the test suite.
128+
129+
This is not timed.
130+
"""
131+
pass
132+
133+
def run_no_coverage(self, env):
134+
"""Run the test suite with no coverage measurement."""
135+
pass
136+
137+
def run_with_coverage(self, env, pip_args, cov_options):
138+
"""Run the test suite with coverage measurement."""
139+
pass
140+
141+
142+
class ToxProject(ProjectToTest):
143+
"""A project using tox to run the test suite."""
144+
145+
def prep_environment(self, env):
146+
env.shell.run_command(f"{env.python} -m pip install tox")
147+
self.run_tox(env, env.pyver.toxenv, "--notest")
148+
149+
def run_tox(self, env, toxenv, toxargs=""):
150+
env.shell.run_command(f"{env.python} -m tox -e {toxenv} {toxargs}")
151+
return env.shell.last_duration
152+
153+
def run_no_coverage(self, env):
154+
return self.run_tox(env, env.pyver.toxenv, "--skip-pkg-install")
155+
156+
157+
class PytestHtml(ToxProject):
158+
"""pytest-dev/pytest-html"""
159+
160+
git_url = "https://github.com/pytest-dev/pytest-html"
161+
162+
def run_with_coverage(self, env, pip_args, cov_options):
163+
covenv = env.pyver.toxenv + "-cov"
164+
self.run_tox(env, covenv, "--notest")
165+
env.shell.run_command(f".tox/{covenv}/bin/python -m pip install {pip_args}")
166+
if cov_options:
167+
replace = ("# reference: https", f"[run]\n{cov_options}\n#")
168+
else:
169+
replace = ("", "")
170+
with file_replace(Path(".coveragerc"), *replace):
171+
env.shell.run_command("cat .coveragerc")
172+
env.shell.run_command(f".tox/{covenv}/bin/python -m coverage debug sys")
173+
return self.run_tox(env, covenv, "--skip-pkg-install")
174+
175+
176+
class PyVersion:
177+
# The command to run this Python
178+
command: str
179+
# The tox environment to run this Python
180+
toxenv: str
181+
182+
183+
class Python(PyVersion):
184+
"""A version of CPython to use."""
185+
186+
def __init__(self, major, minor):
187+
self.command = f"python{major}.{minor}"
188+
self.toxenv = f"py{major}{minor}"
189+
190+
191+
class PyPy(PyVersion):
192+
"""A version of PyPy to use."""
193+
194+
def __init__(self, major, minor):
195+
self.command = f"pypy{major}.{minor}"
196+
self.toxenv = f"pypy{major}{minor}"
197+
198+
199+
@dataclasses.dataclass
200+
class Env:
201+
"""An environment to run a test suite in."""
202+
203+
pyver: PyVersion
204+
python: Path
205+
shell: ShellSession
206+
207+
208+
def run_experiments(
209+
py_versions: List[PyVersion],
210+
cov_versions: List[Tuple[str, Optional[str], Optional[str]]],
211+
projects: List[ProjectToTest],
212+
num_runs=3,
213+
):
214+
"""Run test suites under different conditions."""
215+
216+
for proj in projects:
217+
print(f"Testing with {proj.git_url}")
218+
with ShellSession(f"output_{proj.slug}.log") as shell:
219+
proj.get_source(shell)
220+
221+
for pyver in py_versions:
222+
print(f"Making venv for {proj.slug} {pyver.command}")
223+
venv_dir = f"venv_{proj.slug}_{pyver.command}"
224+
shell.run_command(f"{pyver.command} -m venv {venv_dir}")
225+
python = Path.cwd() / f"{venv_dir}/bin/python"
226+
shell.run_command(f"{python} -V")
227+
env = Env(pyver, python, shell)
228+
229+
with change_dir(Path(proj.slug)):
230+
print(f"Prepping for {proj.slug} {pyver.command}")
231+
proj.prep_environment(env)
232+
for cov_slug, cov_pip, cov_options in cov_versions:
233+
durations = []
234+
for run_num in range(num_runs):
235+
print(
236+
f"Running tests, cov={cov_slug}, {run_num+1} of {num_runs}"
237+
)
238+
if cov_pip is None:
239+
dur = proj.run_no_coverage(env)
240+
else:
241+
dur = proj.run_with_coverage(env, cov_pip, cov_options)
242+
print(f"Tests took {dur:.3f}s")
243+
durations.append(dur)
244+
med = statistics.median(durations)
245+
print(
246+
f"## Median for {pyver.command}, cov={cov_slug}: {med:.3f}s"
247+
)
248+
249+
250+
PERF_DIR = Path("/tmp/covperf")
251+
252+
253+
print(f"Removing and re-making {PERF_DIR}")
254+
rmrf(PERF_DIR)
255+
256+
with change_dir(PERF_DIR):
257+
258+
run_experiments(
259+
py_versions=[
260+
Python(3, 10),
261+
],
262+
cov_versions=[
263+
("none", None, None),
264+
("6.4", "coverage==6.4", ""),
265+
("6.4 timid", "coverage==6.4", "timid=True"),
266+
(
267+
"PR 1381",
268+
"git+https://github.com/cfbolz/coveragepy.git@f_trace_lines",
269+
"",
270+
),
271+
(
272+
"PR 1381 timid",
273+
"git+https://github.com/cfbolz/coveragepy.git@f_trace_lines",
274+
"timid=True",
275+
),
276+
],
277+
projects=[
278+
PytestHtml(),
279+
],
280+
num_runs=3,
281+
)
282+
283+
run_experiments(
284+
py_versions=[
285+
PyPy(3, 9),
286+
],
287+
cov_versions=[
288+
("none", None, None),
289+
("6.4", "coverage==6.4", ""),
290+
(
291+
"PR 1381",
292+
"git+https://github.com/cfbolz/coveragepy.git@f_trace_lines",
293+
"",
294+
),
295+
],
296+
projects=[
297+
PytestHtml(),
298+
],
299+
num_runs=3,
300+
)

perf/bug397.py

Lines changed: 0 additions & 54 deletions
This file was deleted.

0 commit comments

Comments
 (0)