Skip to content

Commit 097e131

Browse files
committed
perf: some quick refactoring to test #1527
1 parent 3a02703 commit 097e131

File tree

1 file changed

+135
-32
lines changed

1 file changed

+135
-32
lines changed

lab/benchmark.py

+135-32
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
import time
1212
from pathlib import Path
1313

14-
from typing import Dict, Iterable, Iterator, List, Optional, Tuple
14+
from typing import Any, Dict, Iterable, Iterator, List, Optional, Tuple
1515

1616

1717
class ShellSession:
@@ -116,17 +116,21 @@ class ProjectToTest:
116116

117117
# Where can we clone the project from?
118118
git_url: Optional[str] = None
119+
slug: Optional[str] = None
119120

120121
def __init__(self):
121-
if self.git_url:
122-
self.slug = self.git_url.split("/")[-1]
123-
self.dir = Path(self.slug)
122+
if not self.slug:
123+
if self.git_url:
124+
self.slug = self.git_url.split("/")[-1]
124125

125-
def get_source(self, shell):
126-
"""Get the source of the project."""
126+
def make_dir(self):
127+
self.dir = Path(f"work_{self.slug}")
127128
if self.dir.exists():
128129
rmrf(self.dir)
129-
shell.run_command(f"git clone {self.git_url}")
130+
131+
def get_source(self, shell):
132+
"""Get the source of the project."""
133+
shell.run_command(f"git clone {self.git_url} {self.dir}")
130134

131135
def prep_environment(self, env):
132136
"""Prepare the environment to run the test suite.
@@ -135,20 +139,41 @@ def prep_environment(self, env):
135139
"""
136140
pass
137141

142+
def tweak_coverage_settings(self, settings: Iterable[Tuple[str, Any]]) -> Iterator[None]:
143+
"""Tweak the coverage settings.
144+
145+
NOTE: This is not properly factored, and is only used by ToxProject now!!!
146+
"""
147+
pass
148+
138149
def run_no_coverage(self, env):
139150
"""Run the test suite with no coverage measurement."""
140151
pass
141152

142-
def run_with_coverage(self, env, pip_args, cov_options):
153+
def run_with_coverage(self, env, pip_args, cov_tweaks):
143154
"""Run the test suite with coverage measurement."""
144155
pass
145156

146157

158+
class EmptyProject(ProjectToTest):
159+
"""A dummy project for testing other parts of this code."""
160+
def __init__(self, slug: str="empty", fake_durations: Iterable[float]=(1.23,)):
161+
self.slug = slug
162+
self.durations = iter(itertools.cycle(fake_durations))
163+
164+
def get_source(self, shell):
165+
pass
166+
167+
def run_with_coverage(self, env, pip_args, cov_tweaks):
168+
"""Run the test suite with coverage measurement."""
169+
return next(self.durations)
170+
171+
147172
class ToxProject(ProjectToTest):
148173
"""A project using tox to run the test suite."""
149174

150175
def prep_environment(self, env):
151-
env.shell.run_command(f"{env.python} -m pip install tox")
176+
env.shell.run_command(f"{env.python} -m pip install 'tox<4'")
152177
self.run_tox(env, env.pyver.toxenv, "--notest")
153178

154179
def run_tox(self, env, toxenv, toxargs=""):
@@ -159,26 +184,30 @@ def run_tox(self, env, toxenv, toxargs=""):
159184
def run_no_coverage(self, env):
160185
return self.run_tox(env, env.pyver.toxenv, "--skip-pkg-install")
161186

162-
def run_with_coverage(self, env, pip_args, cov_options):
163-
assert not cov_options, f"ToxProject.run_with_coverage can't take cov_options={cov_options!r}"
187+
def run_with_coverage(self, env, pip_args, cov_tweaks):
164188
self.run_tox(env, env.pyver.toxenv, "--notest")
165189
env.shell.run_command(
166190
f".tox/{env.pyver.toxenv}/bin/python -m pip install {pip_args}"
167191
)
168-
return self.run_tox(env, env.pyver.toxenv, "--skip-pkg-install")
192+
with self.tweak_coverage_settings(cov_tweaks):
193+
self.pre_check(env) # NOTE: Not properly factored, and only used from here.
194+
duration = self.run_tox(env, env.pyver.toxenv, "--skip-pkg-install")
195+
self.post_check(env) # NOTE: Not properly factored, and only used from here.
196+
return duration
169197

170198

171199
class ProjectPytestHtml(ToxProject):
172200
"""pytest-dev/pytest-html"""
173201

174202
git_url = "https://github.com/pytest-dev/pytest-html"
175203

176-
def run_with_coverage(self, env, pip_args, cov_options):
204+
def run_with_coverage(self, env, pip_args, cov_tweaks):
205+
raise Exception("This doesn't work because options changed to tweaks")
177206
covenv = env.pyver.toxenv + "-cov"
178207
self.run_tox(env, covenv, "--notest")
179208
env.shell.run_command(f".tox/{covenv}/bin/python -m pip install {pip_args}")
180-
if cov_options:
181-
replace = ("# reference: https", f"[run]\n{cov_options}\n#")
209+
if cov_tweaks:
210+
replace = ("# reference: https", f"[run]\n{cov_tweaks}\n#")
182211
else:
183212
replace = ("", "")
184213
with file_replace(Path(".coveragerc"), *replace):
@@ -206,6 +235,34 @@ class ProjectAttrs(ToxProject):
206235

207236
git_url = "https://github.com/python-attrs/attrs"
208237

238+
def tweak_coverage_settings(self, tweaks: Iterable[Tuple[str, Any]]) -> Iterator[None]:
239+
return tweak_toml_coverage_settings("pyproject.toml", tweaks)
240+
241+
def pre_check(self, env):
242+
env.shell.run_command("cat pyproject.toml")
243+
244+
def post_check(self, env):
245+
env.shell.run_command("ls -al")
246+
247+
248+
def tweak_toml_coverage_settings(toml_file: str, tweaks: Iterable[Tuple[str, Any]]) -> Iterator[None]:
249+
if tweaks:
250+
toml_inserts = []
251+
for name, value in tweaks:
252+
if isinstance(value, bool):
253+
toml_inserts.append(f"{name} = {str(value).lower()}")
254+
elif isinstance(value, str):
255+
toml_inserts.append(f"{name} = '{value}'")
256+
else:
257+
raise Exception(f"Can't tweak toml setting: {name} = {value!r}")
258+
header = "[tool.coverage.run]\n"
259+
insert = header + "\n".join(toml_inserts) + "\n"
260+
else:
261+
header = insert = ""
262+
return file_replace(Path(toml_file), header, insert)
263+
264+
265+
209266

210267
class AdHocProject(ProjectToTest):
211268
"""A standalone program to run locally."""
@@ -232,7 +289,7 @@ def run_no_coverage(self, env):
232289
env.shell.run_command(f"{env.python} {self.python_file}")
233290
return env.shell.last_duration
234291

235-
def run_with_coverage(self, env, pip_args, cov_options):
292+
def run_with_coverage(self, env, pip_args, cov_tweaks):
236293
env.shell.run_command(f"{env.python} -m pip install {pip_args}")
237294
with change_dir(self.cur_dir):
238295
env.shell.run_command(
@@ -265,15 +322,13 @@ class PyVersion:
265322
# The tox environment to run this Python
266323
toxenv: str
267324

268-
269325
class Python(PyVersion):
270326
"""A version of CPython to use."""
271327

272328
def __init__(self, major, minor):
273329
self.command = self.slug = f"python{major}.{minor}"
274330
self.toxenv = f"py{major}{minor}"
275331

276-
277332
class PyPy(PyVersion):
278333
"""A version of PyPy to use."""
279334

@@ -288,6 +343,7 @@ def __init__(self, path, slug):
288343
self.slug = slug
289344
self.toxenv = None
290345

346+
291347
@dataclasses.dataclass
292348
class Coverage:
293349
"""A version of coverage.py to use, maybe None."""
@@ -296,33 +352,33 @@ class Coverage:
296352
# Arguments for "pip install ..."
297353
pip_args: Optional[str] = None
298354
# Tweaks to the .coveragerc file
299-
options: Optional[str] = None
355+
tweaks: Optional[Iterable[Tuple[str, Any]]] = None
300356

301357
class CoveragePR(Coverage):
302358
"""A version of coverage.py from a pull request."""
303-
def __init__(self, number, options=None):
359+
def __init__(self, number, tweaks=None):
304360
super().__init__(
305361
slug=f"#{number}",
306362
pip_args=f"git+https://github.com/nedbat/coveragepy.git@refs/pull/{number}/merge",
307-
options=options,
363+
tweaks=tweaks,
308364
)
309365

310366
class CoverageCommit(Coverage):
311367
"""A version of coverage.py from a specific commit."""
312-
def __init__(self, sha, options=None):
368+
def __init__(self, sha, tweaks=None):
313369
super().__init__(
314370
slug=sha,
315371
pip_args=f"git+https://github.com/nedbat/coveragepy.git@{sha}",
316-
options=options,
372+
tweaks=tweaks,
317373
)
318374

319375
class CoverageSource(Coverage):
320376
"""The coverage.py in a working tree."""
321-
def __init__(self, directory, options=None):
377+
def __init__(self, directory, tweaks=None):
322378
super().__init__(
323379
slug="source",
324380
pip_args=directory,
325-
options=options,
381+
tweaks=tweaks,
326382
)
327383

328384

@@ -337,6 +393,8 @@ class Env:
337393

338394
ResultData = Dict[Tuple[str, str, str], float]
339395

396+
DIMENSION_NAMES = ["proj", "pyver", "cov"]
397+
340398
class Experiment:
341399
"""A particular time experiment to run."""
342400

@@ -353,9 +411,18 @@ def __init__(
353411

354412
def run(self, num_runs: int = 3) -> None:
355413
results = []
414+
total_runs = (
415+
len(self.projects) *
416+
len(self.py_versions) *
417+
len(self.cov_versions) *
418+
num_runs
419+
)
420+
total_run_nums = iter(itertools.count(start=1))
421+
356422
for proj in self.projects:
357423
print(f"Testing with {proj.slug}")
358424
with ShellSession(f"output_{proj.slug}.log") as shell:
425+
proj.make_dir()
359426
proj.get_source(shell)
360427

361428
for pyver in self.py_versions:
@@ -366,20 +433,23 @@ def run(self, num_runs: int = 3) -> None:
366433
shell.run_command(f"{python} -V")
367434
env = Env(pyver, python, shell)
368435

369-
with change_dir(Path(proj.slug)):
436+
with change_dir(proj.dir):
370437
print(f"Prepping for {proj.slug} {pyver.slug}")
371438
proj.prep_environment(env)
372439
for cov_ver in self.cov_versions:
373440
durations = []
374441
for run_num in range(num_runs):
442+
total_run_num = next(total_run_nums)
375443
print(
376-
f"Running tests, cov={cov_ver.slug}, {run_num+1} of {num_runs}"
444+
f"Running tests, cov={cov_ver.slug}, " +
445+
f"{run_num+1} of {num_runs}, " +
446+
f"total {total_run_num}/{total_runs}"
377447
)
378448
if cov_ver.pip_args is None:
379449
dur = proj.run_no_coverage(env)
380450
else:
381451
dur = proj.run_with_coverage(
382-
env, cov_ver.pip_args, cov_ver.options,
452+
env, cov_ver.pip_args, cov_ver.tweaks,
383453
)
384454
print(f"Tests took {dur:.3f}s")
385455
durations.append(dur)
@@ -411,7 +481,7 @@ def show_results(
411481

412482
table_axes = [dimensions[rowname] for rowname in rows]
413483
data_order = [*rows, column]
414-
remap = [data_order.index(datum) for datum in ["proj", "pyver", "cov"]]
484+
remap = [data_order.index(datum) for datum in DIMENSION_NAMES]
415485

416486
WIDTH = 20
417487
def as_table_row(vals):
@@ -445,8 +515,12 @@ def as_table_row(vals):
445515
PERF_DIR = Path("/tmp/covperf")
446516

447517
def run_experiment(
448-
py_versions: List[PyVersion], cov_versions: List[Coverage], projects: List[ProjectToTest],
449-
rows: List[str], column: str, ratios: Iterable[Tuple[str, str, str]] = (),
518+
py_versions: List[PyVersion],
519+
cov_versions: List[Coverage],
520+
projects: List[ProjectToTest],
521+
rows: List[str],
522+
column: str,
523+
ratios: Iterable[Tuple[str, str, str]] = (),
450524
):
451525
slugs = [v.slug for v in py_versions + cov_versions + projects]
452526
if len(set(slugs)) != len(slugs):
@@ -456,6 +530,8 @@ def run_experiment(
456530
ratio_slugs = [rslug for ratio in ratios for rslug in ratio[1:]]
457531
if any(rslug not in slugs for rslug in ratio_slugs):
458532
raise Exception(f"Ratio slug doesn't match a slug: {ratio_slugs}, {slugs}")
533+
if set(rows + [column]) != set(DIMENSION_NAMES):
534+
raise Exception(f"All of these must be in rows or column: {', '.join(DIMENSION_NAMES)}")
459535

460536
print(f"Removing and re-making {PERF_DIR}")
461537
rmrf(PERF_DIR)
@@ -466,7 +542,7 @@ def run_experiment(
466542
exp.show_results(rows=rows, column=column, ratios=ratios)
467543

468544

469-
if 1:
545+
if 0:
470546
run_experiment(
471547
py_versions=[
472548
#Python(3, 11),
@@ -489,3 +565,30 @@ def run_experiment(
489565
("94231 vs 3.10", "94231", "v3.10.5"),
490566
],
491567
)
568+
569+
570+
if 1:
571+
run_experiment(
572+
py_versions=[
573+
Python(3, 11),
574+
],
575+
cov_versions=[
576+
Coverage("701", "coverage==7.0.1"),
577+
Coverage("701.dynctx", "coverage==7.0.1", [("dynamic_context", "test_function")]),
578+
Coverage("702", "coverage==7.0.2"),
579+
Coverage("702.dynctx", "coverage==7.0.2", [("dynamic_context", "test_function")]),
580+
],
581+
projects=[
582+
#EmptyProject("empty", [1.2, 3.4]),
583+
#EmptyProject("dummy", [6.9, 7.1]),
584+
#ProjectDateutil(),
585+
ProjectAttrs(),
586+
],
587+
rows=["proj", "pyver"],
588+
column="cov",
589+
ratios=[
590+
(".2 vs .1", "702", "701"),
591+
(".1 dynctx cost", "701.dynctx", "701"),
592+
(".2 dynctx cost", "702.dynctx", "702"),
593+
],
594+
)

0 commit comments

Comments
 (0)