Skip to content

Commit 9350f63

Browse files
authored
Merge pull request #368 from faster-cpython/tighter-perf
Use the new perf_record hook to only include perf in the benchmarking code
2 parents 68c1a0b + 2be4303 commit 9350f63

File tree

5 files changed

+64
-56
lines changed

5 files changed

+64
-56
lines changed

Diff for: bench_runner/scripts/run_benchmarks.py

+47-53
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
"GITHUB_REPOSITORY", "faster-cpython/bench_runner"
3434
)
3535
# Environment variables that control the execution of CPython
36-
ENV_VARS = ["PYTHON_JIT"]
36+
ENV_VARS = ["PYTHON_JIT", "PYPERF_PERF_RECORD_EXTRA_OPTS"]
3737

3838

3939
class NoBenchmarkError(Exception):
@@ -64,7 +64,7 @@ def get_benchmark_names(benchmarks: str) -> list[str]:
6464
def run_benchmarks(
6565
python: PathLike,
6666
benchmarks: str,
67-
command_prefix: Iterable[str] | None = None,
67+
/,
6868
test_mode: bool = False,
6969
extra_args: Iterable[str] | None = None,
7070
) -> None:
@@ -74,9 +74,6 @@ def run_benchmarks(
7474
if BENCHMARK_JSON.is_file():
7575
BENCHMARK_JSON.unlink()
7676

77-
if command_prefix is None:
78-
command_prefix = []
79-
8077
if test_mode:
8178
fast_arg = ["--fast"]
8279
else:
@@ -86,7 +83,6 @@ def run_benchmarks(
8683
extra_args = []
8784

8885
args = [
89-
*command_prefix,
9086
sys.executable,
9187
"-m",
9288
"pyperformance",
@@ -173,19 +169,36 @@ def collect_pystats(
173169
run_summarize_stats(python, fork, ref, "all", benchmark_links, flags=flags)
174170

175171

176-
def perf_to_csv(lines: Iterable[str], output: PathLike):
177-
event_count_prefix = "# Event count (approx.): "
178-
total = None
172+
def get_perf_lines(files: Iterable[PathLike]) -> Iterable[str]:
173+
for filename in files:
174+
p = subprocess.Popen(
175+
[
176+
"perf",
177+
"report",
178+
"--stdio",
179+
"-g",
180+
"none",
181+
"--show-total-period",
182+
"-s",
183+
"pid,symbol,dso",
184+
"-i",
185+
str(filename),
186+
],
187+
encoding="utf-8",
188+
stdout=subprocess.PIPE,
189+
bufsize=1,
190+
)
191+
assert p.stdout is not None # for pyright
192+
yield from iter(p.stdout.readline, "")
193+
p.kill()
194+
179195

196+
def perf_to_csv(lines: Iterable[str], output: PathLike):
180197
rows = []
181198
for line in lines:
182199
line = line.strip()
183-
if line.startswith(event_count_prefix):
184-
total = int(line[len(event_count_prefix) :].strip())
185-
elif line.startswith("#") or line == "":
200+
if line.startswith("#") or line == "":
186201
pass
187-
elif total is None:
188-
raise ValueError("Could not find total sample count")
189202
else:
190203
_, period, command, _, symbol, shared, _ = line.split(maxsplit=6)
191204
pid, command = command.split(":")
@@ -209,47 +222,28 @@ def collect_perf(python: PathLike, benchmarks: str):
209222
shutil.rmtree(PROFILING_RESULTS)
210223
PROFILING_RESULTS.mkdir()
211224

212-
perf_data = Path("perf.data")
225+
perf_data_glob = "perf.data.*"
213226
for benchmark in all_benchmarks:
214-
if perf_data.exists():
215-
perf_data.unlink()
216-
217-
try:
218-
run_benchmarks(
219-
python,
220-
benchmark,
221-
command_prefix=[
222-
"perf",
223-
"record",
224-
"-o",
225-
"perf.data",
226-
"--",
227-
],
227+
for filename in Path(".").glob(perf_data_glob):
228+
filename.unlink()
229+
230+
run_benchmarks(
231+
python,
232+
benchmark,
233+
extra_args=["--hook", "perf_record"],
234+
)
235+
236+
fileiter = Path(".").glob(perf_data_glob)
237+
if util.has_any_element(fileiter):
238+
perf_to_csv(
239+
get_perf_lines(fileiter),
240+
PROFILING_RESULTS / f"{benchmark}.perf.csv",
228241
)
229-
except NoBenchmarkError:
230-
pass
231242
else:
232-
if perf_data.exists():
233-
output = subprocess.check_output(
234-
[
235-
"perf",
236-
"report",
237-
"--stdio",
238-
"-g",
239-
"none",
240-
"--show-total-period",
241-
"-s",
242-
"pid,symbol,dso",
243-
"-i",
244-
"perf.data",
245-
],
246-
encoding="utf-8",
247-
)
248-
perf_to_csv(
249-
output.splitlines(), PROFILING_RESULTS / f"{benchmark}.perf.csv"
250-
)
251-
else:
252-
print(f"No perf.data file generated for {benchmark}", file=sys.stderr)
243+
print(f"No perf.data files generated for {benchmark}", file=sys.stderr)
244+
245+
for filename in Path(".").glob(perf_data_glob):
246+
filename.unlink()
253247

254248

255249
def update_metadata(
@@ -381,7 +375,7 @@ def _main(
381375
benchmarks = select_benchmarks(benchmarks)
382376

383377
if mode == "benchmark":
384-
run_benchmarks(python, benchmarks, [], test_mode)
378+
run_benchmarks(python, benchmarks, test_mode=test_mode)
385379
update_metadata(BENCHMARK_JSON, fork, ref, run_id=run_id)
386380
copy_to_directory(BENCHMARK_JSON, python, fork, ref, flags)
387381
elif mode == "perf":

Diff for: bench_runner/templates/_benchmark.src.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@ jobs:
221221
- name: Tune system
222222
if: ${{ steps.should_run.outputs.should_run != 'false' }}
223223
run: |
224-
sudo LD_LIBRARY_PATH=$LD_LIBRARY_PATH venv/bin/python -m pyperf system tune
224+
sudo LD_LIBRARY_PATH=$LD_LIBRARY_PATH venv/bin/python -m pyperf system ${{ inputs.perf && 'reset' || 'tune' }}
225225
- name: Tune for (Linux) perf
226226
if: ${{ steps.should_run.outputs.should_run != 'false' && inputs.perf }}
227227
run: |

Diff for: bench_runner/templates/env.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
PYPERFORMANCE_HASH: 9164273e5504c410a5be08d8753c91be708fdd9a
1+
PYPERFORMANCE_HASH: 56d12a8fd7cc1432835965d374929bfa7f6f7a07
22
PYSTON_BENCHMARKS_HASH: 265655e7f03ace13ec1e00e1ba299179e69f8a00

Diff for: bench_runner/util.py

+14
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import functools
22
import hashlib
3+
import itertools
34
import os
45
from pathlib import Path
56
from typing import TypeAlias, Union
@@ -41,3 +42,16 @@ def get_excluded_benchmarks() -> set[str]:
4142
if key in benchmarks_section:
4243
return set(benchmarks_section[key])
4344
return set()
45+
46+
47+
def has_any_element(iterable):
48+
"""
49+
Checks if an iterable (like a generator) has at least one element
50+
without consuming the original iterable more than necessary.
51+
"""
52+
first, iterable = itertools.tee(iterable, 2) # Create two independent iterators
53+
try:
54+
next(first) # Try to get the first element
55+
return True # If successful, the generator is not empty
56+
except StopIteration:
57+
return False # If StopIteration is raised, the generator is empty

Diff for: pyproject.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ classifiers = [
1616
]
1717
dependencies = [
1818
"matplotlib==3.10.1",
19-
"pyperf==2.8.1",
19+
"pyperf==2.9.0",
2020
"rich==13.9.4",
2121
"rich-argparse==1.7.0",
2222
"ruamel.yaml==0.18.10",

0 commit comments

Comments
 (0)