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.
Files changed (85) hide show
  1. {pyperf-2.7.0/pyperf.egg-info → pyperf-2.8.0}/PKG-INFO +1 -1
  2. {pyperf-2.7.0 → pyperf-2.8.0}/doc/changelog.rst +16 -0
  3. {pyperf-2.7.0 → pyperf-2.8.0}/doc/cli.rst +2 -2
  4. {pyperf-2.7.0 → pyperf-2.8.0}/doc/conf.py +1 -1
  5. {pyperf-2.7.0 → pyperf-2.8.0}/doc/examples/bench_time_func.py +3 -2
  6. {pyperf-2.7.0 → pyperf-2.8.0}/doc/run_benchmark.rst +4 -2
  7. {pyperf-2.7.0 → pyperf-2.8.0}/doc/runner.rst +4 -0
  8. {pyperf-2.7.0 → pyperf-2.8.0}/doc/system.rst +1 -1
  9. {pyperf-2.7.0 → pyperf-2.8.0}/pyperf/__init__.py +1 -1
  10. {pyperf-2.7.0 → pyperf-2.8.0}/pyperf/_collect_metadata.py +1 -5
  11. {pyperf-2.7.0 → pyperf-2.8.0}/pyperf/_command.py +26 -9
  12. {pyperf-2.7.0 → pyperf-2.8.0}/pyperf/_formatter.py +2 -2
  13. pyperf-2.8.0/pyperf/_hooks.py +110 -0
  14. {pyperf-2.7.0 → pyperf-2.8.0}/pyperf/_manager.py +15 -4
  15. {pyperf-2.7.0 → pyperf-2.8.0}/pyperf/_process_time.py +54 -9
  16. {pyperf-2.7.0 → pyperf-2.8.0}/pyperf/_runner.py +18 -9
  17. {pyperf-2.7.0 → pyperf-2.8.0}/pyperf/_system.py +96 -56
  18. {pyperf-2.7.0 → pyperf-2.8.0}/pyperf/_utils.py +32 -0
  19. {pyperf-2.7.0 → pyperf-2.8.0}/pyperf/_worker.py +13 -14
  20. {pyperf-2.7.0 → pyperf-2.8.0}/pyperf/tests/test_metadata.py +3 -3
  21. {pyperf-2.7.0 → pyperf-2.8.0}/pyperf/tests/test_perf_cli.py +20 -7
  22. {pyperf-2.7.0 → pyperf-2.8.0}/pyperf/tests/test_runner.py +27 -0
  23. {pyperf-2.7.0 → pyperf-2.8.0}/pyperf/tests/test_utils.py +2 -2
  24. {pyperf-2.7.0 → pyperf-2.8.0/pyperf.egg-info}/PKG-INFO +1 -1
  25. {pyperf-2.7.0 → pyperf-2.8.0}/pyperf.egg-info/SOURCES.txt +1 -0
  26. pyperf-2.8.0/pyperf.egg-info/entry_points.txt +6 -0
  27. {pyperf-2.7.0 → pyperf-2.8.0}/pyproject.toml +4 -0
  28. pyperf-2.7.0/pyperf.egg-info/entry_points.txt +0 -2
  29. {pyperf-2.7.0 → pyperf-2.8.0}/COPYING +0 -0
  30. {pyperf-2.7.0 → pyperf-2.8.0}/MANIFEST.in +0 -0
  31. {pyperf-2.7.0 → pyperf-2.8.0}/README.rst +0 -0
  32. {pyperf-2.7.0 → pyperf-2.8.0}/TODO.rst +0 -0
  33. {pyperf-2.7.0 → pyperf-2.8.0}/doc/Makefile +0 -0
  34. {pyperf-2.7.0 → pyperf-2.8.0}/doc/analyze.rst +0 -0
  35. {pyperf-2.7.0 → pyperf-2.8.0}/doc/api.rst +0 -0
  36. {pyperf-2.7.0 → pyperf-2.8.0}/doc/developer_guide.rst +0 -0
  37. {pyperf-2.7.0 → pyperf-2.8.0}/doc/examples/bench_async_func.py +0 -0
  38. {pyperf-2.7.0 → pyperf-2.8.0}/doc/examples/bench_async_func_with_loop_factory.py +0 -0
  39. {pyperf-2.7.0 → pyperf-2.8.0}/doc/examples/bench_command.py +0 -0
  40. {pyperf-2.7.0 → pyperf-2.8.0}/doc/examples/bench_func.py +0 -0
  41. {pyperf-2.7.0 → pyperf-2.8.0}/doc/examples/bench_timeit.py +0 -0
  42. {pyperf-2.7.0 → pyperf-2.8.0}/doc/examples/export_csv.py +0 -0
  43. {pyperf-2.7.0 → pyperf-2.8.0}/doc/examples/hist_scipy.py +0 -0
  44. {pyperf-2.7.0 → pyperf-2.8.0}/doc/examples/plot.py +0 -0
  45. {pyperf-2.7.0 → pyperf-2.8.0}/doc/examples.rst +0 -0
  46. {pyperf-2.7.0 → pyperf-2.8.0}/doc/hist_scipy_telco.png +0 -0
  47. {pyperf-2.7.0 → pyperf-2.8.0}/doc/index.rst +0 -0
  48. {pyperf-2.7.0 → pyperf-2.8.0}/doc/make.bat +0 -0
  49. {pyperf-2.7.0 → pyperf-2.8.0}/doc/outliers.json.gz +0 -0
  50. {pyperf-2.7.0 → pyperf-2.8.0}/doc/py2.json.gz +0 -0
  51. {pyperf-2.7.0 → pyperf-2.8.0}/doc/py3.json.gz +0 -0
  52. {pyperf-2.7.0 → pyperf-2.8.0}/doc/timeit_strip.json.gz +0 -0
  53. {pyperf-2.7.0 → pyperf-2.8.0}/doc/user_guide.rst +0 -0
  54. {pyperf-2.7.0 → pyperf-2.8.0}/pyperf/__main__.py +0 -0
  55. {pyperf-2.7.0 → pyperf-2.8.0}/pyperf/_bench.py +0 -0
  56. {pyperf-2.7.0 → pyperf-2.8.0}/pyperf/_cli.py +0 -0
  57. {pyperf-2.7.0 → pyperf-2.8.0}/pyperf/_compare.py +0 -0
  58. {pyperf-2.7.0 → pyperf-2.8.0}/pyperf/_cpu_utils.py +0 -0
  59. {pyperf-2.7.0 → pyperf-2.8.0}/pyperf/_linux_memory.py +0 -0
  60. {pyperf-2.7.0 → pyperf-2.8.0}/pyperf/_metadata.py +0 -0
  61. {pyperf-2.7.0 → pyperf-2.8.0}/pyperf/_psutil_memory.py +0 -0
  62. {pyperf-2.7.0 → pyperf-2.8.0}/pyperf/_timeit.py +0 -0
  63. {pyperf-2.7.0 → pyperf-2.8.0}/pyperf/_timeit_cli.py +0 -0
  64. {pyperf-2.7.0 → pyperf-2.8.0}/pyperf/_win_memory.py +0 -0
  65. {pyperf-2.7.0 → pyperf-2.8.0}/pyperf/tests/__init__.py +0 -0
  66. {pyperf-2.7.0 → pyperf-2.8.0}/pyperf/tests/mult_list_bench.py +0 -0
  67. {pyperf-2.7.0 → pyperf-2.8.0}/pyperf/tests/mult_list_py36.json +0 -0
  68. {pyperf-2.7.0 → pyperf-2.8.0}/pyperf/tests/mult_list_py36_tags.json +0 -0
  69. {pyperf-2.7.0 → pyperf-2.8.0}/pyperf/tests/mult_list_py37.json +0 -0
  70. {pyperf-2.7.0 → pyperf-2.8.0}/pyperf/tests/mult_list_py37_tags.json +0 -0
  71. {pyperf-2.7.0 → pyperf-2.8.0}/pyperf/tests/mult_list_py38.json +0 -0
  72. {pyperf-2.7.0 → pyperf-2.8.0}/pyperf/tests/replay.py +0 -0
  73. {pyperf-2.7.0 → pyperf-2.8.0}/pyperf/tests/telco.json +0 -0
  74. {pyperf-2.7.0 → pyperf-2.8.0}/pyperf/tests/test_bench.py +0 -0
  75. {pyperf-2.7.0 → pyperf-2.8.0}/pyperf/tests/test_cli.py +0 -0
  76. {pyperf-2.7.0 → pyperf-2.8.0}/pyperf/tests/test_examples.py +0 -0
  77. {pyperf-2.7.0 → pyperf-2.8.0}/pyperf/tests/test_misc.py +0 -0
  78. {pyperf-2.7.0 → pyperf-2.8.0}/pyperf/tests/test_system.py +0 -0
  79. {pyperf-2.7.0 → pyperf-2.8.0}/pyperf/tests/test_timeit.py +0 -0
  80. {pyperf-2.7.0 → pyperf-2.8.0}/pyperf/tests/track_memory.json +0 -0
  81. {pyperf-2.7.0 → pyperf-2.8.0}/pyperf.egg-info/dependency_links.txt +0 -0
  82. {pyperf-2.7.0 → pyperf-2.8.0}/pyperf.egg-info/requires.txt +0 -0
  83. {pyperf-2.7.0 → pyperf-2.8.0}/pyperf.egg-info/top_level.txt +0 -0
  84. {pyperf-2.7.0 → pyperf-2.8.0}/setup.cfg +0 -0
  85. {pyperf-2.7.0 → pyperf-2.8.0}/tox.ini +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pyperf
3
- Version: 2.7.0
3
+ Version: 2.8.0
4
4
  Summary: Python module to run and analyze benchmarks
5
5
  Author-email: Victor Stinner <vstinner@redhat.com>
6
6
  Maintainer-email: Dong-hee Na <donghee.na@python.org>
@@ -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 (standard error)
233
- <https://en.wikipedia.org/wiki/Standard_error>`_
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.7.0'
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 = pyperf.perf_counter()
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 pyperf.perf_counter() - t0
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 automatically collect ``pystats`` on the benchmark code by calling ``sys._stats_on`` immediately before the benchmark and calling ``sys._stats_off`` immediately after.
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 pining makes benchmarks more stable: use the
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.
@@ -1,6 +1,6 @@
1
1
  from time import perf_counter
2
2
 
3
- VERSION = (2, 7, 0)
3
+ VERSION = (2, 8, 0)
4
4
  __version__ = '.'.join(map(str, VERSION))
5
5
 
6
6
  # Export pyperf.perf_counter for backward compatibility with pyperf 1.7
@@ -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 = None
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 MB' % (size / (1024.0 * 1024.0))
33
+ return '%.1f MiB' % (size / (1024.0 * 1024.0))
34
34
 
35
- return '%.1f kB' % (size / 1024.0)
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
- with rpipe.open_text() as rfile:
102
- bench_json = rfile.read()
103
-
104
- exitcode = proc.wait()
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
- usage = resource.getrusage(resource.RUSAGE_CHILDREN)
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
- dt, max_rss = bench_process(loops, args, kw, profile_filename)
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
- # Write timing in seconds into stdout
130
- print(dt)
131
- if max_rss:
132
- print(max_rss)
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)