pyperf 2.7.0__tar.gz → 2.8.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pyperf-2.7.0/pyperf.egg-info → pyperf-2.8.0}/PKG-INFO +1 -1
- {pyperf-2.7.0 → pyperf-2.8.0}/doc/changelog.rst +16 -0
- {pyperf-2.7.0 → pyperf-2.8.0}/doc/cli.rst +2 -2
- {pyperf-2.7.0 → pyperf-2.8.0}/doc/conf.py +1 -1
- {pyperf-2.7.0 → pyperf-2.8.0}/doc/examples/bench_time_func.py +3 -2
- {pyperf-2.7.0 → pyperf-2.8.0}/doc/run_benchmark.rst +4 -2
- {pyperf-2.7.0 → pyperf-2.8.0}/doc/runner.rst +4 -0
- {pyperf-2.7.0 → pyperf-2.8.0}/doc/system.rst +1 -1
- {pyperf-2.7.0 → pyperf-2.8.0}/pyperf/__init__.py +1 -1
- {pyperf-2.7.0 → pyperf-2.8.0}/pyperf/_collect_metadata.py +1 -5
- {pyperf-2.7.0 → pyperf-2.8.0}/pyperf/_command.py +26 -9
- {pyperf-2.7.0 → pyperf-2.8.0}/pyperf/_formatter.py +2 -2
- pyperf-2.8.0/pyperf/_hooks.py +110 -0
- {pyperf-2.7.0 → pyperf-2.8.0}/pyperf/_manager.py +15 -4
- {pyperf-2.7.0 → pyperf-2.8.0}/pyperf/_process_time.py +54 -9
- {pyperf-2.7.0 → pyperf-2.8.0}/pyperf/_runner.py +18 -9
- {pyperf-2.7.0 → pyperf-2.8.0}/pyperf/_system.py +96 -56
- {pyperf-2.7.0 → pyperf-2.8.0}/pyperf/_utils.py +32 -0
- {pyperf-2.7.0 → pyperf-2.8.0}/pyperf/_worker.py +13 -14
- {pyperf-2.7.0 → pyperf-2.8.0}/pyperf/tests/test_metadata.py +3 -3
- {pyperf-2.7.0 → pyperf-2.8.0}/pyperf/tests/test_perf_cli.py +20 -7
- {pyperf-2.7.0 → pyperf-2.8.0}/pyperf/tests/test_runner.py +27 -0
- {pyperf-2.7.0 → pyperf-2.8.0}/pyperf/tests/test_utils.py +2 -2
- {pyperf-2.7.0 → pyperf-2.8.0/pyperf.egg-info}/PKG-INFO +1 -1
- {pyperf-2.7.0 → pyperf-2.8.0}/pyperf.egg-info/SOURCES.txt +1 -0
- pyperf-2.8.0/pyperf.egg-info/entry_points.txt +6 -0
- {pyperf-2.7.0 → pyperf-2.8.0}/pyproject.toml +4 -0
- pyperf-2.7.0/pyperf.egg-info/entry_points.txt +0 -2
- {pyperf-2.7.0 → pyperf-2.8.0}/COPYING +0 -0
- {pyperf-2.7.0 → pyperf-2.8.0}/MANIFEST.in +0 -0
- {pyperf-2.7.0 → pyperf-2.8.0}/README.rst +0 -0
- {pyperf-2.7.0 → pyperf-2.8.0}/TODO.rst +0 -0
- {pyperf-2.7.0 → pyperf-2.8.0}/doc/Makefile +0 -0
- {pyperf-2.7.0 → pyperf-2.8.0}/doc/analyze.rst +0 -0
- {pyperf-2.7.0 → pyperf-2.8.0}/doc/api.rst +0 -0
- {pyperf-2.7.0 → pyperf-2.8.0}/doc/developer_guide.rst +0 -0
- {pyperf-2.7.0 → pyperf-2.8.0}/doc/examples/bench_async_func.py +0 -0
- {pyperf-2.7.0 → pyperf-2.8.0}/doc/examples/bench_async_func_with_loop_factory.py +0 -0
- {pyperf-2.7.0 → pyperf-2.8.0}/doc/examples/bench_command.py +0 -0
- {pyperf-2.7.0 → pyperf-2.8.0}/doc/examples/bench_func.py +0 -0
- {pyperf-2.7.0 → pyperf-2.8.0}/doc/examples/bench_timeit.py +0 -0
- {pyperf-2.7.0 → pyperf-2.8.0}/doc/examples/export_csv.py +0 -0
- {pyperf-2.7.0 → pyperf-2.8.0}/doc/examples/hist_scipy.py +0 -0
- {pyperf-2.7.0 → pyperf-2.8.0}/doc/examples/plot.py +0 -0
- {pyperf-2.7.0 → pyperf-2.8.0}/doc/examples.rst +0 -0
- {pyperf-2.7.0 → pyperf-2.8.0}/doc/hist_scipy_telco.png +0 -0
- {pyperf-2.7.0 → pyperf-2.8.0}/doc/index.rst +0 -0
- {pyperf-2.7.0 → pyperf-2.8.0}/doc/make.bat +0 -0
- {pyperf-2.7.0 → pyperf-2.8.0}/doc/outliers.json.gz +0 -0
- {pyperf-2.7.0 → pyperf-2.8.0}/doc/py2.json.gz +0 -0
- {pyperf-2.7.0 → pyperf-2.8.0}/doc/py3.json.gz +0 -0
- {pyperf-2.7.0 → pyperf-2.8.0}/doc/timeit_strip.json.gz +0 -0
- {pyperf-2.7.0 → pyperf-2.8.0}/doc/user_guide.rst +0 -0
- {pyperf-2.7.0 → pyperf-2.8.0}/pyperf/__main__.py +0 -0
- {pyperf-2.7.0 → pyperf-2.8.0}/pyperf/_bench.py +0 -0
- {pyperf-2.7.0 → pyperf-2.8.0}/pyperf/_cli.py +0 -0
- {pyperf-2.7.0 → pyperf-2.8.0}/pyperf/_compare.py +0 -0
- {pyperf-2.7.0 → pyperf-2.8.0}/pyperf/_cpu_utils.py +0 -0
- {pyperf-2.7.0 → pyperf-2.8.0}/pyperf/_linux_memory.py +0 -0
- {pyperf-2.7.0 → pyperf-2.8.0}/pyperf/_metadata.py +0 -0
- {pyperf-2.7.0 → pyperf-2.8.0}/pyperf/_psutil_memory.py +0 -0
- {pyperf-2.7.0 → pyperf-2.8.0}/pyperf/_timeit.py +0 -0
- {pyperf-2.7.0 → pyperf-2.8.0}/pyperf/_timeit_cli.py +0 -0
- {pyperf-2.7.0 → pyperf-2.8.0}/pyperf/_win_memory.py +0 -0
- {pyperf-2.7.0 → pyperf-2.8.0}/pyperf/tests/__init__.py +0 -0
- {pyperf-2.7.0 → pyperf-2.8.0}/pyperf/tests/mult_list_bench.py +0 -0
- {pyperf-2.7.0 → pyperf-2.8.0}/pyperf/tests/mult_list_py36.json +0 -0
- {pyperf-2.7.0 → pyperf-2.8.0}/pyperf/tests/mult_list_py36_tags.json +0 -0
- {pyperf-2.7.0 → pyperf-2.8.0}/pyperf/tests/mult_list_py37.json +0 -0
- {pyperf-2.7.0 → pyperf-2.8.0}/pyperf/tests/mult_list_py37_tags.json +0 -0
- {pyperf-2.7.0 → pyperf-2.8.0}/pyperf/tests/mult_list_py38.json +0 -0
- {pyperf-2.7.0 → pyperf-2.8.0}/pyperf/tests/replay.py +0 -0
- {pyperf-2.7.0 → pyperf-2.8.0}/pyperf/tests/telco.json +0 -0
- {pyperf-2.7.0 → pyperf-2.8.0}/pyperf/tests/test_bench.py +0 -0
- {pyperf-2.7.0 → pyperf-2.8.0}/pyperf/tests/test_cli.py +0 -0
- {pyperf-2.7.0 → pyperf-2.8.0}/pyperf/tests/test_examples.py +0 -0
- {pyperf-2.7.0 → pyperf-2.8.0}/pyperf/tests/test_misc.py +0 -0
- {pyperf-2.7.0 → pyperf-2.8.0}/pyperf/tests/test_system.py +0 -0
- {pyperf-2.7.0 → pyperf-2.8.0}/pyperf/tests/test_timeit.py +0 -0
- {pyperf-2.7.0 → pyperf-2.8.0}/pyperf/tests/track_memory.json +0 -0
- {pyperf-2.7.0 → pyperf-2.8.0}/pyperf.egg-info/dependency_links.txt +0 -0
- {pyperf-2.7.0 → pyperf-2.8.0}/pyperf.egg-info/requires.txt +0 -0
- {pyperf-2.7.0 → pyperf-2.8.0}/pyperf.egg-info/top_level.txt +0 -0
- {pyperf-2.7.0 → pyperf-2.8.0}/setup.cfg +0 -0
- {pyperf-2.7.0 → pyperf-2.8.0}/tox.ini +0 -0
|
@@ -1,6 +1,22 @@
|
|
|
1
1
|
Changelog
|
|
2
2
|
=========
|
|
3
3
|
|
|
4
|
+
* Benchmarks now have a timeout.
|
|
5
|
+
Patch by Diego Russo.
|
|
6
|
+
* The example code in bench_time_func.py uses the more modern `time.perf_counter()`.
|
|
7
|
+
Patch by Hajime Senuma.
|
|
8
|
+
* pystats are now correctly collected in subprocesses.
|
|
9
|
+
Patch by Michael Droettboom.
|
|
10
|
+
* pyperf now supports "hooks" to add custom functionality around each benchmarking call.
|
|
11
|
+
pystats are no longer collected automatically, and you must pass `--hook pystats`.
|
|
12
|
+
Patch by Michael Droettboom.
|
|
13
|
+
* Memory units are now correctly stated in KiB and MiB.
|
|
14
|
+
Patch by Ungve Mardal Moe.
|
|
15
|
+
* max_mem_rss is now correctly measured on macOS.
|
|
16
|
+
Patch by Michael Droettboom.
|
|
17
|
+
* `pyperf system tune` now works correctly on aarch64.
|
|
18
|
+
Patch by Diego Russo.
|
|
19
|
+
|
|
4
20
|
Version 2.7.0 (2024-05-18)
|
|
5
21
|
---------------------------
|
|
6
22
|
|
|
@@ -229,8 +229,8 @@ Example::
|
|
|
229
229
|
Values:
|
|
230
230
|
|
|
231
231
|
* `Median <https://en.wikipedia.org/wiki/Median>`_
|
|
232
|
-
* "std dev": `Standard deviation
|
|
233
|
-
<https://en.wikipedia.org/wiki/
|
|
232
|
+
* "std dev": `Standard deviation
|
|
233
|
+
<https://en.wikipedia.org/wiki/Standard_deviation>`_
|
|
234
234
|
|
|
235
235
|
See also `Outlier (Wikipedia) <https://en.wikipedia.org/wiki/Outlier>`_.
|
|
236
236
|
|
|
@@ -50,7 +50,7 @@ author = 'Victor Stinner'
|
|
|
50
50
|
# built documents.
|
|
51
51
|
#
|
|
52
52
|
# The short X.Y version.
|
|
53
|
-
version = release = '2.
|
|
53
|
+
version = release = '2.8.0'
|
|
54
54
|
|
|
55
55
|
# The language for content autogenerated by Sphinx. Refer to documentation
|
|
56
56
|
# for a list of supported languages.
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
+
import time
|
|
2
3
|
import pyperf
|
|
3
4
|
|
|
4
5
|
|
|
5
6
|
def bench_dict(loops, mydict):
|
|
6
7
|
range_it = range(loops)
|
|
7
|
-
t0 =
|
|
8
|
+
t0 = time.perf_counter()
|
|
8
9
|
|
|
9
10
|
for _ in range_it:
|
|
10
11
|
mydict['0']
|
|
@@ -18,7 +19,7 @@ def bench_dict(loops, mydict):
|
|
|
18
19
|
mydict['800']
|
|
19
20
|
mydict['900']
|
|
20
21
|
|
|
21
|
-
return
|
|
22
|
+
return time.perf_counter() - t0
|
|
22
23
|
|
|
23
24
|
|
|
24
25
|
runner = pyperf.Runner()
|
|
@@ -192,12 +192,14 @@ the benchmark less reliable since two runs may use a different number of warmup
|
|
|
192
192
|
values.
|
|
193
193
|
|
|
194
194
|
Specializer statistics (``pystats``)
|
|
195
|
-
|
|
195
|
+
====================================
|
|
196
196
|
|
|
197
197
|
``pyperf`` has built-in support for `specializer statistics (``pystats``) <https://docs.python.org/dev/using/configure.html#cmdoption-enable-pystats>`_.
|
|
198
|
-
If running benchmarks on a CPython built with the ``--enable-pystats`` flag, pyperf will
|
|
198
|
+
If running benchmarks on a CPython built with the ``--enable-pystats`` flag, when you pass ``--hook pystats``, pyperf will collect ``pystats`` on the benchmark code by calling ``sys._stats_on`` immediately before the benchmark and calling ``sys._stats_off`` immediately after.
|
|
199
199
|
Stats are not collected when running ``pyperf``'s own code or when warming up or calibrating the benchmarks.
|
|
200
200
|
|
|
201
|
+
**New in 2.8.0:** The ``--hook pystats`` flag must be given to collect pystats.
|
|
202
|
+
|
|
201
203
|
Due to the overhead of collecting the statistics, the timing results will be meaningless.
|
|
202
204
|
|
|
203
205
|
The `Tools/scripts/summarize_stats.py <https://github.com/python/cpython/blob/main/Tools/scripts/summarize_stats.py>`_ script can be used to summarize the statistics in a human-readable form.
|
|
@@ -98,6 +98,7 @@ Option::
|
|
|
98
98
|
--inherit-environ=VARS
|
|
99
99
|
--copy-env
|
|
100
100
|
--no-locale
|
|
101
|
+
--timeout TIMEOUT
|
|
101
102
|
--track-memory
|
|
102
103
|
--tracemalloc
|
|
103
104
|
|
|
@@ -140,6 +141,9 @@ Option::
|
|
|
140
141
|
- ``LC_TELEPHONE``
|
|
141
142
|
- ``LC_TIME``
|
|
142
143
|
|
|
144
|
+
* ``--timeout``: set a timeout in seconds for an execution of the benchmark.
|
|
145
|
+
If the benchmark execution times out, pyperf exits with error code 124.
|
|
146
|
+
There is no time out by default.
|
|
143
147
|
* ``--tracemalloc``: Use the ``tracemalloc`` module to track Python memory
|
|
144
148
|
allocation and get the peak of memory usage in metadata
|
|
145
149
|
(``tracemalloc_peak``). The module is only available on Python 3.4 and newer.
|
|
@@ -23,7 +23,7 @@ be checked in benchmark metadata: it is enabled if the ``cpu_affinity``
|
|
|
23
23
|
|
|
24
24
|
:func:`os.sched_setaffinity` is used to pin processes.
|
|
25
25
|
|
|
26
|
-
Even if no CPU is isolated, CPU
|
|
26
|
+
Even if no CPU is isolated, CPU pinning makes benchmarks more stable: use the
|
|
27
27
|
``--affinity`` command line option.
|
|
28
28
|
|
|
29
29
|
Check the CPU topology for HyperThreading and NUMA for best performances.
|
|
@@ -125,10 +125,6 @@ def collect_python_metadata(metadata):
|
|
|
125
125
|
if not gc.isenabled():
|
|
126
126
|
metadata['python_gc'] = 'disabled'
|
|
127
127
|
|
|
128
|
-
# pystats enabled?
|
|
129
|
-
if hasattr(sys, "_stats_clear"):
|
|
130
|
-
metadata['pystats'] = 'enabled'
|
|
131
|
-
|
|
132
128
|
|
|
133
129
|
def read_proc(path):
|
|
134
130
|
path = proc_path(path)
|
|
@@ -208,7 +204,7 @@ def collect_system_metadata(metadata):
|
|
|
208
204
|
|
|
209
205
|
def collect_memory_metadata(metadata):
|
|
210
206
|
if resource is not None:
|
|
211
|
-
metadata["mem_max_rss"] = get_max_rss()
|
|
207
|
+
metadata["mem_max_rss"] = get_max_rss(children=False)
|
|
212
208
|
|
|
213
209
|
# Note: Don't collect VmPeak of /proc/self/status on Linux because it is
|
|
214
210
|
# not accurate. See pyperf._linux_memory for more accurate memory metrics.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import functools
|
|
2
|
+
import json
|
|
2
3
|
import os.path
|
|
3
4
|
import subprocess
|
|
4
5
|
import sys
|
|
@@ -7,6 +8,26 @@ from pyperf._utils import shell_quote, popen_communicate
|
|
|
7
8
|
from pyperf._worker import WorkerTask
|
|
8
9
|
|
|
9
10
|
|
|
11
|
+
def parse_subprocess_data(output):
|
|
12
|
+
# Parse the data send from the subprocess.
|
|
13
|
+
# It is three lines containing:
|
|
14
|
+
# - The runtime (in seconds)
|
|
15
|
+
# - max_rss (or -1, if not able to compute)
|
|
16
|
+
# - The metadata to add to the benchmark entry, as a JSON dictionary
|
|
17
|
+
|
|
18
|
+
rss = None
|
|
19
|
+
metadata = {}
|
|
20
|
+
try:
|
|
21
|
+
lines = output.splitlines()
|
|
22
|
+
timing = float(lines[0])
|
|
23
|
+
rss = int(lines[1])
|
|
24
|
+
metadata = json.loads(lines[2])
|
|
25
|
+
except ValueError:
|
|
26
|
+
raise ValueError("failed to parse worker output: %r" % output)
|
|
27
|
+
|
|
28
|
+
return timing, rss, metadata
|
|
29
|
+
|
|
30
|
+
|
|
10
31
|
def bench_command(command, task, loops):
|
|
11
32
|
path = os.path.dirname(__file__)
|
|
12
33
|
script = os.path.join(path, '_process_time.py')
|
|
@@ -22,19 +43,15 @@ def bench_command(command, task, loops):
|
|
|
22
43
|
raise Exception("Command failed with exit code %s"
|
|
23
44
|
% proc.returncode)
|
|
24
45
|
|
|
25
|
-
rss =
|
|
26
|
-
try:
|
|
27
|
-
lines = output.splitlines()
|
|
28
|
-
timing = float(lines[0])
|
|
29
|
-
if len(lines) >= 2:
|
|
30
|
-
rss = int(lines[1])
|
|
31
|
-
except ValueError:
|
|
32
|
-
raise ValueError("failed to parse script output: %r" % output)
|
|
46
|
+
timing, rss, metadata = parse_subprocess_data(output)
|
|
33
47
|
|
|
34
|
-
if rss:
|
|
48
|
+
if rss and rss > 0:
|
|
35
49
|
# store the maximum
|
|
36
50
|
max_rss = task.metadata.get('command_max_rss', 0)
|
|
37
51
|
task.metadata['command_max_rss'] = max(max_rss, rss)
|
|
52
|
+
|
|
53
|
+
task.metadata.update(metadata)
|
|
54
|
+
|
|
38
55
|
return timing
|
|
39
56
|
|
|
40
57
|
|
|
@@ -30,9 +30,9 @@ def format_filesize(size):
|
|
|
30
30
|
return '%.0f byte' % size
|
|
31
31
|
|
|
32
32
|
if size > 10 * 1024 * 1024:
|
|
33
|
-
return '%.1f
|
|
33
|
+
return '%.1f MiB' % (size / (1024.0 * 1024.0))
|
|
34
34
|
|
|
35
|
-
return '%.1f
|
|
35
|
+
return '%.1f KiB' % (size / 1024.0)
|
|
36
36
|
|
|
37
37
|
|
|
38
38
|
def format_filesizes(sizes):
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# Hooks are installable context managers defined as entry points so that
|
|
2
|
+
# arbitrary code can by run right before and after the actual internal
|
|
3
|
+
# benchmarking code is run.
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
import abc
|
|
7
|
+
import importlib.metadata
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get_hooks():
|
|
12
|
+
hook_prefix = "pyperf.hook"
|
|
13
|
+
entry_points = importlib.metadata.entry_points()
|
|
14
|
+
if sys.version_info[:2] < (3, 10):
|
|
15
|
+
group = entry_points[hook_prefix]
|
|
16
|
+
else:
|
|
17
|
+
group = entry_points.select(group=hook_prefix)
|
|
18
|
+
return group
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def get_hook_names():
|
|
22
|
+
return (x.name for x in get_hooks())
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_selected_hooks(hook_names):
|
|
26
|
+
if hook_names is None:
|
|
27
|
+
return
|
|
28
|
+
|
|
29
|
+
hook_mapping = {hook.name: hook for hook in get_hooks()}
|
|
30
|
+
for hook_name in hook_names:
|
|
31
|
+
yield hook_mapping[hook_name]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def instantiate_selected_hooks(hook_names):
|
|
35
|
+
hook_managers = {}
|
|
36
|
+
for hook in get_selected_hooks(hook_names):
|
|
37
|
+
try:
|
|
38
|
+
hook_managers[hook.name] = hook.load()()
|
|
39
|
+
except HookError as e:
|
|
40
|
+
print(f"ERROR setting up hook '{hook.name}':", file=sys.stderr)
|
|
41
|
+
print(str(e), file=sys.stderr)
|
|
42
|
+
sys.exit(1)
|
|
43
|
+
|
|
44
|
+
return hook_managers
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class HookError(Exception):
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class HookBase(abc.ABC):
|
|
52
|
+
def __init__(self):
|
|
53
|
+
"""
|
|
54
|
+
Create a new instance of the hook.
|
|
55
|
+
"""
|
|
56
|
+
pass
|
|
57
|
+
|
|
58
|
+
def teardown(self, _metadata):
|
|
59
|
+
"""
|
|
60
|
+
Called when the hook is completed for a process. May add any information
|
|
61
|
+
collected to the passed-in `metadata` dictionary.
|
|
62
|
+
"""
|
|
63
|
+
pass
|
|
64
|
+
|
|
65
|
+
def __enter__(self):
|
|
66
|
+
"""
|
|
67
|
+
Called immediately before running benchmark code.
|
|
68
|
+
|
|
69
|
+
May be called multiple times per instance.
|
|
70
|
+
"""
|
|
71
|
+
pass
|
|
72
|
+
|
|
73
|
+
def __exit__(self, _exc_type, _exc_value, _traceback):
|
|
74
|
+
"""
|
|
75
|
+
Called immediately after running benchmark code.
|
|
76
|
+
"""
|
|
77
|
+
pass
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class _test_hook(HookBase):
|
|
81
|
+
def __init__(self):
|
|
82
|
+
self._count = 0
|
|
83
|
+
|
|
84
|
+
def teardown(self, metadata):
|
|
85
|
+
metadata["_test_hook"] = self._count
|
|
86
|
+
|
|
87
|
+
def __enter__(self):
|
|
88
|
+
self._count += 1
|
|
89
|
+
|
|
90
|
+
def __exit__(self, _exc_type, _exc_value, _traceback):
|
|
91
|
+
pass
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class pystats(HookBase):
|
|
95
|
+
def __init__(self):
|
|
96
|
+
if not hasattr(sys, "_stats_on"):
|
|
97
|
+
raise HookError(
|
|
98
|
+
"Can not collect pystats because python was not built with --enable-pystats"
|
|
99
|
+
)
|
|
100
|
+
sys._stats_off()
|
|
101
|
+
sys._stats_clear()
|
|
102
|
+
|
|
103
|
+
def teardown(self, metadata):
|
|
104
|
+
metadata["pystats"] = "enabled"
|
|
105
|
+
|
|
106
|
+
def __enter__(self):
|
|
107
|
+
sys._stats_on()
|
|
108
|
+
|
|
109
|
+
def __exit__(self, _exc_type, _exc_value, _traceback):
|
|
110
|
+
sys._stats_off()
|
|
@@ -7,6 +7,8 @@ from pyperf._formatter import format_number
|
|
|
7
7
|
from pyperf._utils import MS_WINDOWS, create_environ, create_pipe, popen_killer
|
|
8
8
|
|
|
9
9
|
|
|
10
|
+
EXIT_TIMEOUT = 60
|
|
11
|
+
|
|
10
12
|
# Limit to 5 calibration processes
|
|
11
13
|
# (10 if calibration is needed for loops and warmups)
|
|
12
14
|
MAX_CALIBRATION = 5
|
|
@@ -69,6 +71,13 @@ class Manager:
|
|
|
69
71
|
if args.profile:
|
|
70
72
|
cmd.extend(['--profile', args.profile])
|
|
71
73
|
|
|
74
|
+
if args.timeout:
|
|
75
|
+
cmd.extend(['--timeout', str(args.timeout)])
|
|
76
|
+
|
|
77
|
+
if args.hook:
|
|
78
|
+
for hook in args.hook:
|
|
79
|
+
cmd.extend(['--hook', hook])
|
|
80
|
+
|
|
72
81
|
if self.runner._add_cmdline_args:
|
|
73
82
|
self.runner._add_cmdline_args(cmd, args)
|
|
74
83
|
|
|
@@ -98,10 +107,12 @@ class Manager:
|
|
|
98
107
|
proc = subprocess.Popen(cmd, env=env, **kw)
|
|
99
108
|
|
|
100
109
|
with popen_killer(proc):
|
|
101
|
-
|
|
102
|
-
bench_json =
|
|
103
|
-
|
|
104
|
-
|
|
110
|
+
try:
|
|
111
|
+
bench_json = rpipe.read_text(timeout=self.args.timeout)
|
|
112
|
+
exitcode = proc.wait(timeout=EXIT_TIMEOUT)
|
|
113
|
+
except TimeoutError as exc:
|
|
114
|
+
print(exc)
|
|
115
|
+
sys.exit(124)
|
|
105
116
|
|
|
106
117
|
if exitcode:
|
|
107
118
|
raise RuntimeError("%s failed with exit code %s"
|
|
@@ -14,6 +14,8 @@ Measure wall-time, not CPU time.
|
|
|
14
14
|
If resource.getrusage() is available: compute the maximum RSS memory in bytes
|
|
15
15
|
per process and writes it into stdout as a second line.
|
|
16
16
|
"""
|
|
17
|
+
import contextlib
|
|
18
|
+
import json
|
|
17
19
|
import os
|
|
18
20
|
import subprocess
|
|
19
21
|
import sys
|
|
@@ -26,9 +28,13 @@ except ImportError:
|
|
|
26
28
|
resource = None
|
|
27
29
|
|
|
28
30
|
|
|
29
|
-
def get_max_rss():
|
|
31
|
+
def get_max_rss(*, children):
|
|
30
32
|
if resource is not None:
|
|
31
|
-
|
|
33
|
+
if children:
|
|
34
|
+
resource_type = resource.RUSAGE_CHILDREN
|
|
35
|
+
else:
|
|
36
|
+
resource_type = resource.RUSAGE_SELF
|
|
37
|
+
usage = resource.getrusage(resource_type)
|
|
32
38
|
if sys.platform == 'darwin':
|
|
33
39
|
return usage.ru_maxrss
|
|
34
40
|
return usage.ru_maxrss * 1024
|
|
@@ -61,7 +67,7 @@ def bench_process(loops, args, kw, profile_filename=None):
|
|
|
61
67
|
args = [args[0], "-m", "cProfile", "-o", temp_profile_filename] + args[1:]
|
|
62
68
|
|
|
63
69
|
for _ in range_it:
|
|
64
|
-
start_rss = get_max_rss()
|
|
70
|
+
start_rss = get_max_rss(children=True)
|
|
65
71
|
|
|
66
72
|
proc = subprocess.Popen(args, **kw)
|
|
67
73
|
with proc:
|
|
@@ -75,7 +81,7 @@ def bench_process(loops, args, kw, profile_filename=None):
|
|
|
75
81
|
os.unlink(temp_profile_filename)
|
|
76
82
|
sys.exit(exitcode)
|
|
77
83
|
|
|
78
|
-
rss = get_max_rss() - start_rss
|
|
84
|
+
rss = get_max_rss(children=True) - start_rss
|
|
79
85
|
max_rss = max(max_rss, rss)
|
|
80
86
|
|
|
81
87
|
if profile_filename:
|
|
@@ -87,6 +93,39 @@ def bench_process(loops, args, kw, profile_filename=None):
|
|
|
87
93
|
return (dt, max_rss)
|
|
88
94
|
|
|
89
95
|
|
|
96
|
+
def load_hooks(metadata):
|
|
97
|
+
hook_names = []
|
|
98
|
+
while "--hook" in sys.argv:
|
|
99
|
+
hook_idx = sys.argv.index("--hook")
|
|
100
|
+
hook_name = sys.argv[hook_idx + 1]
|
|
101
|
+
hook_names.append(hook_name)
|
|
102
|
+
del sys.argv[hook_idx]
|
|
103
|
+
del sys.argv[hook_idx]
|
|
104
|
+
|
|
105
|
+
if len(hook_names):
|
|
106
|
+
# Only import pyperf if we know we have hooks
|
|
107
|
+
import pyperf._hooks
|
|
108
|
+
|
|
109
|
+
hook_managers = pyperf._hooks.instantiate_selected_hooks(hook_names)
|
|
110
|
+
metadata["hooks"] = ", ".join(hook_managers.values())
|
|
111
|
+
else:
|
|
112
|
+
hook_managers = {}
|
|
113
|
+
|
|
114
|
+
return hook_managers
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def write_data(dt, max_rss, metadata, out=sys.stdout):
|
|
118
|
+
# Write the data that is communicated back to the main orchestration process.
|
|
119
|
+
# It is three lines containing:
|
|
120
|
+
# - The runtime (in seconds)
|
|
121
|
+
# - max_rss (or -1, if not able to compute)
|
|
122
|
+
# - The metadata to add to the benchmark entry, as a JSON dictionary
|
|
123
|
+
print(dt, file=out)
|
|
124
|
+
print(max_rss or -1, file=out)
|
|
125
|
+
json.dump(metadata, fp=out)
|
|
126
|
+
print(file=out)
|
|
127
|
+
|
|
128
|
+
|
|
90
129
|
def main():
|
|
91
130
|
# Make sure that the pyperf module wasn't imported
|
|
92
131
|
if 'pyperf' in sys.modules:
|
|
@@ -107,6 +146,9 @@ def main():
|
|
|
107
146
|
else:
|
|
108
147
|
profile_filename = None
|
|
109
148
|
|
|
149
|
+
metadata = {}
|
|
150
|
+
hook_managers = load_hooks(metadata)
|
|
151
|
+
|
|
110
152
|
loops = int(sys.argv[1])
|
|
111
153
|
args = sys.argv[2:]
|
|
112
154
|
|
|
@@ -121,15 +163,18 @@ def main():
|
|
|
121
163
|
kw['stdout'] = devnull
|
|
122
164
|
kw['stderr'] = subprocess.STDOUT
|
|
123
165
|
|
|
124
|
-
|
|
166
|
+
with contextlib.ExitStack() as stack:
|
|
167
|
+
for hook in hook_managers.values():
|
|
168
|
+
stack.enter_context(hook)
|
|
169
|
+
dt, max_rss = bench_process(loops, args, kw, profile_filename)
|
|
125
170
|
|
|
126
171
|
if devnull is not None:
|
|
127
172
|
devnull.close()
|
|
128
173
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
174
|
+
for hook in hook_managers.values():
|
|
175
|
+
hook.teardown(metadata)
|
|
176
|
+
|
|
177
|
+
write_data(dt, max_rss, metadata)
|
|
133
178
|
|
|
134
179
|
|
|
135
180
|
if __name__ == "__main__":
|
|
@@ -11,6 +11,7 @@ from pyperf._cpu_utils import (format_cpu_list, parse_cpu_list,
|
|
|
11
11
|
get_isolated_cpus, set_cpu_affinity,
|
|
12
12
|
set_highest_priority)
|
|
13
13
|
from pyperf._formatter import format_timedelta
|
|
14
|
+
from pyperf._hooks import get_hook_names
|
|
14
15
|
from pyperf._utils import (MS_WINDOWS, MAC_OS, abs_executable,
|
|
15
16
|
WritePipe, get_python_names,
|
|
16
17
|
merge_profile_stats)
|
|
@@ -75,14 +76,7 @@ class Runner:
|
|
|
75
76
|
loops=0, min_time=0.1, metadata=None,
|
|
76
77
|
show_name=True,
|
|
77
78
|
program_args=None, add_cmdline_args=None,
|
|
78
|
-
_argparser=None):
|
|
79
|
-
|
|
80
|
-
# Reset the stats collection if running a --enable-pystats build
|
|
81
|
-
try:
|
|
82
|
-
sys._stats_off()
|
|
83
|
-
sys._stats_clear()
|
|
84
|
-
except AttributeError:
|
|
85
|
-
pass
|
|
79
|
+
_argparser=None, warmups=1):
|
|
86
80
|
|
|
87
81
|
# Watchdog: ensure that only once instance of Runner (or a Runner
|
|
88
82
|
# subclass) is created per process to prevent bad surprises
|
|
@@ -165,7 +159,7 @@ class Runner:
|
|
|
165
159
|
help='number of values per process (default: %s)'
|
|
166
160
|
% values)
|
|
167
161
|
parser.add_argument('-w', '--warmups',
|
|
168
|
-
type=positive_or_nul,
|
|
162
|
+
type=positive_or_nul, default=warmups,
|
|
169
163
|
help='number of skipped values per run used '
|
|
170
164
|
'to warmup the benchmark')
|
|
171
165
|
parser.add_argument('-l', '--loops',
|
|
@@ -189,6 +183,10 @@ class Runner:
|
|
|
189
183
|
'value, used to calibrate the number of '
|
|
190
184
|
'loops (default: %s)'
|
|
191
185
|
% format_timedelta(min_time))
|
|
186
|
+
parser.add_argument('--timeout',
|
|
187
|
+
help='Specify a timeout in seconds for a single '
|
|
188
|
+
'benchmark execution (default: disabled)',
|
|
189
|
+
type=strictly_positive)
|
|
192
190
|
parser.add_argument('--worker', action='store_true',
|
|
193
191
|
help='Worker process, run the benchmark.')
|
|
194
192
|
parser.add_argument('--worker-task', type=positive_or_nul, metavar='TASK_ID',
|
|
@@ -248,6 +246,13 @@ class Runner:
|
|
|
248
246
|
help='Collect profile data using cProfile '
|
|
249
247
|
'and output to the given file.')
|
|
250
248
|
|
|
249
|
+
hook_names = list(get_hook_names())
|
|
250
|
+
parser.add_argument(
|
|
251
|
+
'--hook', action="append", choices=hook_names,
|
|
252
|
+
metavar=f"{', '.join(x for x in hook_names if not x.startswith('_'))}",
|
|
253
|
+
help='Use the given pyperf hooks'
|
|
254
|
+
)
|
|
255
|
+
|
|
251
256
|
memory = parser.add_mutually_exclusive_group()
|
|
252
257
|
memory.add_argument('--tracemalloc', action="store_true",
|
|
253
258
|
help='Trace memory allocations using tracemalloc')
|
|
@@ -732,6 +737,10 @@ class Runner:
|
|
|
732
737
|
if self.args.profile:
|
|
733
738
|
command.extend(["--profile", self.args.profile])
|
|
734
739
|
|
|
740
|
+
if self.args.hook:
|
|
741
|
+
for hook in self.args.hook:
|
|
742
|
+
command.extend(["--hook", hook])
|
|
743
|
+
|
|
735
744
|
# Use lazy import to limit imports on 'import pyperf'
|
|
736
745
|
from pyperf._command import BenchCommandTask
|
|
737
746
|
task = BenchCommandTask(self, name, command)
|