pyperf 2.8.0__tar.gz → 2.9.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.8.0/pyperf.egg-info → pyperf-2.9.0}/PKG-INFO +3 -4
  2. {pyperf-2.8.0 → pyperf-2.9.0}/doc/api.rst +13 -0
  3. {pyperf-2.8.0 → pyperf-2.9.0}/doc/changelog.rst +20 -0
  4. {pyperf-2.8.0 → pyperf-2.9.0}/doc/conf.py +1 -1
  5. {pyperf-2.8.0 → pyperf-2.9.0}/doc/run_benchmark.rst +14 -0
  6. {pyperf-2.8.0 → pyperf-2.9.0}/pyperf/__init__.py +1 -1
  7. {pyperf-2.8.0 → pyperf-2.9.0}/pyperf/__main__.py +6 -2
  8. {pyperf-2.8.0 → pyperf-2.9.0}/pyperf/_bench.py +41 -0
  9. {pyperf-2.8.0 → pyperf-2.9.0}/pyperf/_cli.py +25 -3
  10. {pyperf-2.8.0 → pyperf-2.9.0}/pyperf/_hooks.py +60 -0
  11. {pyperf-2.8.0 → pyperf-2.9.0}/pyperf/_process_time.py +1 -1
  12. {pyperf-2.8.0 → pyperf-2.9.0}/pyperf/_utils.py +4 -1
  13. {pyperf-2.8.0 → pyperf-2.9.0}/pyperf/tests/test_misc.py +1 -5
  14. {pyperf-2.8.0 → pyperf-2.9.0}/pyperf/tests/test_perf_cli.py +13 -3
  15. {pyperf-2.8.0 → pyperf-2.9.0}/pyperf/tests/test_runner.py +14 -8
  16. {pyperf-2.8.0 → pyperf-2.9.0/pyperf.egg-info}/PKG-INFO +3 -4
  17. {pyperf-2.8.0 → pyperf-2.9.0}/pyperf.egg-info/entry_points.txt +1 -0
  18. pyperf-2.9.0/pyperf.egg-info/requires.txt +4 -0
  19. {pyperf-2.8.0 → pyperf-2.9.0}/pyproject.toml +2 -2
  20. pyperf-2.8.0/pyperf.egg-info/requires.txt +0 -7
  21. {pyperf-2.8.0 → pyperf-2.9.0}/COPYING +0 -0
  22. {pyperf-2.8.0 → pyperf-2.9.0}/MANIFEST.in +0 -0
  23. {pyperf-2.8.0 → pyperf-2.9.0}/README.rst +0 -0
  24. {pyperf-2.8.0 → pyperf-2.9.0}/TODO.rst +0 -0
  25. {pyperf-2.8.0 → pyperf-2.9.0}/doc/Makefile +0 -0
  26. {pyperf-2.8.0 → pyperf-2.9.0}/doc/analyze.rst +0 -0
  27. {pyperf-2.8.0 → pyperf-2.9.0}/doc/cli.rst +0 -0
  28. {pyperf-2.8.0 → pyperf-2.9.0}/doc/developer_guide.rst +0 -0
  29. {pyperf-2.8.0 → pyperf-2.9.0}/doc/examples/bench_async_func.py +0 -0
  30. {pyperf-2.8.0 → pyperf-2.9.0}/doc/examples/bench_async_func_with_loop_factory.py +0 -0
  31. {pyperf-2.8.0 → pyperf-2.9.0}/doc/examples/bench_command.py +0 -0
  32. {pyperf-2.8.0 → pyperf-2.9.0}/doc/examples/bench_func.py +0 -0
  33. {pyperf-2.8.0 → pyperf-2.9.0}/doc/examples/bench_time_func.py +0 -0
  34. {pyperf-2.8.0 → pyperf-2.9.0}/doc/examples/bench_timeit.py +0 -0
  35. {pyperf-2.8.0 → pyperf-2.9.0}/doc/examples/export_csv.py +0 -0
  36. {pyperf-2.8.0 → pyperf-2.9.0}/doc/examples/hist_scipy.py +0 -0
  37. {pyperf-2.8.0 → pyperf-2.9.0}/doc/examples/plot.py +0 -0
  38. {pyperf-2.8.0 → pyperf-2.9.0}/doc/examples.rst +0 -0
  39. {pyperf-2.8.0 → pyperf-2.9.0}/doc/hist_scipy_telco.png +0 -0
  40. {pyperf-2.8.0 → pyperf-2.9.0}/doc/index.rst +0 -0
  41. {pyperf-2.8.0 → pyperf-2.9.0}/doc/make.bat +0 -0
  42. {pyperf-2.8.0 → pyperf-2.9.0}/doc/outliers.json.gz +0 -0
  43. {pyperf-2.8.0 → pyperf-2.9.0}/doc/py2.json.gz +0 -0
  44. {pyperf-2.8.0 → pyperf-2.9.0}/doc/py3.json.gz +0 -0
  45. {pyperf-2.8.0 → pyperf-2.9.0}/doc/runner.rst +0 -0
  46. {pyperf-2.8.0 → pyperf-2.9.0}/doc/system.rst +0 -0
  47. {pyperf-2.8.0 → pyperf-2.9.0}/doc/timeit_strip.json.gz +0 -0
  48. {pyperf-2.8.0 → pyperf-2.9.0}/doc/user_guide.rst +0 -0
  49. {pyperf-2.8.0 → pyperf-2.9.0}/pyperf/_collect_metadata.py +0 -0
  50. {pyperf-2.8.0 → pyperf-2.9.0}/pyperf/_command.py +0 -0
  51. {pyperf-2.8.0 → pyperf-2.9.0}/pyperf/_compare.py +0 -0
  52. {pyperf-2.8.0 → pyperf-2.9.0}/pyperf/_cpu_utils.py +0 -0
  53. {pyperf-2.8.0 → pyperf-2.9.0}/pyperf/_formatter.py +0 -0
  54. {pyperf-2.8.0 → pyperf-2.9.0}/pyperf/_linux_memory.py +0 -0
  55. {pyperf-2.8.0 → pyperf-2.9.0}/pyperf/_manager.py +0 -0
  56. {pyperf-2.8.0 → pyperf-2.9.0}/pyperf/_metadata.py +0 -0
  57. {pyperf-2.8.0 → pyperf-2.9.0}/pyperf/_psutil_memory.py +0 -0
  58. {pyperf-2.8.0 → pyperf-2.9.0}/pyperf/_runner.py +0 -0
  59. {pyperf-2.8.0 → pyperf-2.9.0}/pyperf/_system.py +0 -0
  60. {pyperf-2.8.0 → pyperf-2.9.0}/pyperf/_timeit.py +0 -0
  61. {pyperf-2.8.0 → pyperf-2.9.0}/pyperf/_timeit_cli.py +0 -0
  62. {pyperf-2.8.0 → pyperf-2.9.0}/pyperf/_win_memory.py +0 -0
  63. {pyperf-2.8.0 → pyperf-2.9.0}/pyperf/_worker.py +0 -0
  64. {pyperf-2.8.0 → pyperf-2.9.0}/pyperf/tests/__init__.py +0 -0
  65. {pyperf-2.8.0 → pyperf-2.9.0}/pyperf/tests/mult_list_bench.py +0 -0
  66. {pyperf-2.8.0 → pyperf-2.9.0}/pyperf/tests/mult_list_py36.json +0 -0
  67. {pyperf-2.8.0 → pyperf-2.9.0}/pyperf/tests/mult_list_py36_tags.json +0 -0
  68. {pyperf-2.8.0 → pyperf-2.9.0}/pyperf/tests/mult_list_py37.json +0 -0
  69. {pyperf-2.8.0 → pyperf-2.9.0}/pyperf/tests/mult_list_py37_tags.json +0 -0
  70. {pyperf-2.8.0 → pyperf-2.9.0}/pyperf/tests/mult_list_py38.json +0 -0
  71. {pyperf-2.8.0 → pyperf-2.9.0}/pyperf/tests/replay.py +0 -0
  72. {pyperf-2.8.0 → pyperf-2.9.0}/pyperf/tests/telco.json +0 -0
  73. {pyperf-2.8.0 → pyperf-2.9.0}/pyperf/tests/test_bench.py +0 -0
  74. {pyperf-2.8.0 → pyperf-2.9.0}/pyperf/tests/test_cli.py +0 -0
  75. {pyperf-2.8.0 → pyperf-2.9.0}/pyperf/tests/test_examples.py +0 -0
  76. {pyperf-2.8.0 → pyperf-2.9.0}/pyperf/tests/test_metadata.py +0 -0
  77. {pyperf-2.8.0 → pyperf-2.9.0}/pyperf/tests/test_system.py +0 -0
  78. {pyperf-2.8.0 → pyperf-2.9.0}/pyperf/tests/test_timeit.py +0 -0
  79. {pyperf-2.8.0 → pyperf-2.9.0}/pyperf/tests/test_utils.py +0 -0
  80. {pyperf-2.8.0 → pyperf-2.9.0}/pyperf/tests/track_memory.json +0 -0
  81. {pyperf-2.8.0 → pyperf-2.9.0}/pyperf.egg-info/SOURCES.txt +0 -0
  82. {pyperf-2.8.0 → pyperf-2.9.0}/pyperf.egg-info/dependency_links.txt +0 -0
  83. {pyperf-2.8.0 → pyperf-2.9.0}/pyperf.egg-info/top_level.txt +0 -0
  84. {pyperf-2.8.0 → pyperf-2.9.0}/setup.cfg +0 -0
  85. {pyperf-2.8.0 → pyperf-2.9.0}/tox.ini +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.2
2
2
  Name: pyperf
3
- Version: 2.8.0
3
+ Version: 2.9.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>
@@ -13,13 +13,12 @@ Classifier: Natural Language :: English
13
13
  Classifier: Operating System :: OS Independent
14
14
  Classifier: Programming Language :: Python :: 3
15
15
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
16
- Requires-Python: >=3.7
16
+ Requires-Python: >=3.9
17
17
  Description-Content-Type: text/x-rst
18
18
  License-File: COPYING
19
19
  Requires-Dist: psutil>=5.9.0
20
20
  Provides-Extra: dev
21
21
  Requires-Dist: tox; extra == "dev"
22
- Requires-Dist: importlib-metadata; python_version < "3.8" and extra == "dev"
23
22
 
24
23
  ******
25
24
  pyperf
@@ -345,6 +345,19 @@ Benchmark class
345
345
 
346
346
  Raise an exception if the benchmark has no values.
347
347
 
348
+ .. method:: required_nprocesses()
349
+
350
+ Determines the number of separate process runs that would be required
351
+ achieve stable results. Specifically, the target is to have 95% certainty
352
+ that there is a variance of less than 1%. If the result is greater than
353
+ the number of processes recorded in the input data, the value is
354
+ meaningless and only means "more samples are required".
355
+
356
+ The method used is described in this Wikipedia article about estimating
357
+ the sampling of a mean:
358
+
359
+ https://en.wikipedia.org/wiki/Sample_size_determination#Estimation_of_a_mean
360
+
348
361
  .. method:: update_metadata(metadata: dict)
349
362
 
350
363
  Update metadata of all runs of the benchmark.
@@ -1,6 +1,26 @@
1
1
  Changelog
2
2
  =========
3
3
 
4
+ Version 2.9.0 (2025-03-03)
5
+ --------------------------
6
+
7
+ * Feature: A new hook, ``perf_record``, is available to turn Linux perf profiling on and off around the benchmark code.
8
+ To use it, pass ``--hook perf_record``.
9
+ Patch by Matt Page.
10
+ * Feature: pyperf will now warn if you are collecting too few or too many samples for stable results.
11
+ Patch by Michael Droettboom
12
+
13
+ Version 2.8.1 (2024-11-13)
14
+ --------------------------
15
+
16
+ * Bugfix: Hooks now work correctly in subprocess benchmarks (`bench.run_command`).
17
+ Patch by Michael Droettboom
18
+ * Python 3.8 is now unsupported.
19
+ Patch by Hugo van Kemenade
20
+
21
+ Version 2.8.0 (2024-09-30)
22
+ --------------------------
23
+
4
24
  * Benchmarks now have a timeout.
5
25
  Patch by Diego Russo.
6
26
  * The example code in bench_time_func.py uses the more modern `time.perf_counter()`.
@@ -50,7 +50,7 @@ author = 'Victor Stinner'
50
50
  # built documents.
51
51
  #
52
52
  # The short X.Y version.
53
- version = release = '2.8.0'
53
+ version = release = '2.9.0'
54
54
 
55
55
  # The language for content autogenerated by Sphinx. Refer to documentation
56
56
  # for a list of supported languages.
@@ -206,3 +206,17 @@ The `Tools/scripts/summarize_stats.py <https://github.com/python/cpython/blob/ma
206
206
 
207
207
  Statistics are not cleared between runs.
208
208
  If you need to delete statistics from a previous run, remove the files in ``/tmp/py_stats`` (Unix) or ``C:\temp\py_stats`` (Windows).
209
+
210
+ Profiling benchmarks using ``perf record``
211
+ ==========================================
212
+ ``pyperf`` supports profiling benchmark execution using ``perf
213
+ record``. ``perf`` is only enabled while the benchmark is running to avoid
214
+ profiling unrelated parts of ``pyperf`` itself.
215
+
216
+ One profile data file is generated for each benchmark run. These files have
217
+ the basename of ``perf.data.<uuid>`` and are written to the current directory
218
+ by default. The directory can be overridden by setting the
219
+ ``PYPERF_PERF_RECORD_DATA_DIR`` environment variable.
220
+
221
+ The value of the ``PYPERF_PERF_RECORD_EXTRA_OPTS`` environment variable is
222
+ appended to the command line of ``perf record`` if it is provided.
@@ -1,6 +1,6 @@
1
1
  from time import perf_counter
2
2
 
3
- VERSION = (2, 8, 0)
3
+ VERSION = (2, 9, 0)
4
4
  __version__ = '.'.join(map(str, VERSION))
5
5
 
6
6
  # Export pyperf.perf_counter for backward compatibility with pyperf 1.7
@@ -455,7 +455,8 @@ def display_benchmarks(args, show_metadata=False, hist=False, stats=False,
455
455
  dump=dump,
456
456
  checks=checks,
457
457
  result=result,
458
- display_runs_args=display_runs_args)
458
+ display_runs_args=display_runs_args,
459
+ only_checks=only_checks)
459
460
 
460
461
  if bench_lines:
461
462
  empty_line(lines)
@@ -491,10 +492,13 @@ def display_benchmarks(args, show_metadata=False, hist=False, stats=False,
491
492
  empty_line(output)
492
493
  output.extend(lines)
493
494
 
495
+ contains_warning = False
494
496
  for line in output:
497
+ if line.startswith("WARNING:"):
498
+ contains_warning = True
495
499
  print(line)
496
500
 
497
- if not output and only_checks:
501
+ if not contains_warning and only_checks:
498
502
  if len(data) == 1:
499
503
  print("The benchmark seems to be stable")
500
504
  else:
@@ -424,6 +424,47 @@ class Benchmark:
424
424
  raise ValueError("MAD must be >= 0")
425
425
  return value
426
426
 
427
+ def required_nprocesses(self):
428
+ """
429
+ Determines the number of separate process runs that would be required
430
+ achieve stable results. Specifically, the target is to have 95%
431
+ certainty that there is a variance of less than 1%. If the result is
432
+ greater than the number of processes recorded in the input data, the
433
+ value is meaningless and only means "more samples are required".
434
+
435
+ The method used is described in this Wikipedia article about estimating
436
+ the sampling of a mean:
437
+
438
+ https://en.wikipedia.org/wiki/Sample_size_determination#Estimation_of_a_mean
439
+ """
440
+ # Get the means of the values per process. The values within the process
441
+ # often vary considerably (e.g. due to cache effects), but the variances
442
+ # between processes should be fairly consistent. Additionally, this
443
+ # value is intended to be advice for the number of processes to run.
444
+ values = []
445
+ for run in self._runs:
446
+ if len(run.values):
447
+ values.append(statistics.mean(run.values))
448
+
449
+ if len(values) < 2:
450
+ return None
451
+
452
+ total = math.fsum(values)
453
+ mean = total / len(values)
454
+ stddev = statistics.stdev(values)
455
+
456
+ # Normalize the stddev so we can target "percentage changed" rather than
457
+ # absolute time
458
+ sigma = stddev / mean
459
+
460
+ # 95% certainty
461
+ Z = 1.96
462
+ # 1% variation
463
+ W = 0.01
464
+
465
+ # (4Z²σ²)/(W²)
466
+ return math.ceil((4 * Z ** 2 * sigma ** 2) / (W ** 2))
467
+
427
468
  def percentile(self, p):
428
469
  if not (0 <= p <= 100):
429
470
  raise ValueError("p must be in the range [0; 100]")
@@ -400,7 +400,7 @@ def format_histogram(benchmarks, bins=20, extend=False, lines=None,
400
400
  return lines
401
401
 
402
402
 
403
- def format_checks(bench, lines=None):
403
+ def format_checks(bench, lines=None, check_too_many_processes=False):
404
404
  if lines is None:
405
405
  lines = []
406
406
 
@@ -412,6 +412,7 @@ def format_checks(bench, lines=None):
412
412
  mean = bench.mean()
413
413
  warnings = []
414
414
  warn = warnings.append
415
+ required_nprocesses = None
415
416
 
416
417
  # Display a warning if the standard deviation is greater than 10%
417
418
  # of the mean
@@ -421,6 +422,14 @@ def format_checks(bench, lines=None):
421
422
  if percent >= 10.0:
422
423
  warn("the standard deviation (%s) is %.0f%% of the mean (%s)"
423
424
  % (bench.format_value(stdev), percent, bench.format_value(mean)))
425
+ else:
426
+ # display a warning if the number of samples isn't enough to get a stable result
427
+ required_nprocesses = bench.required_nprocesses()
428
+ if (
429
+ required_nprocesses is not None and
430
+ required_nprocesses > len(bench._runs)
431
+ ):
432
+ warn("Not enough samples to get a stable result (95% certainly of less than 1% variation)")
424
433
 
425
434
  # Minimum and maximum, detect obvious outliers
426
435
  for minimum, value in (
@@ -457,6 +466,19 @@ def format_checks(bench, lines=None):
457
466
  lines.append("Use pyperf stats, pyperf dump and pyperf hist to analyze results.")
458
467
  lines.append("Use --quiet option to hide these warnings.")
459
468
 
469
+ if check_too_many_processes:
470
+ if required_nprocesses is None:
471
+ required_nprocesses = bench.required_nprocesses()
472
+ if (
473
+ required_nprocesses is not None and
474
+ required_nprocesses < len(bench._runs) * 0.75
475
+ ):
476
+ lines.append("Benchmark was run more times than necessary to get a stable result.")
477
+ lines.append(
478
+ "Consider passing processes=%d to the Runner constructor to save time." %
479
+ required_nprocesses
480
+ )
481
+
460
482
  # Warn if nohz_full+intel_pstate combo if found in cpu_config metadata
461
483
  for run in bench._runs:
462
484
  cpu_config = run._metadata.get('cpu_config')
@@ -549,7 +571,7 @@ def format_result(bench):
549
571
 
550
572
  def format_benchmark(bench, checks=True, metadata=False,
551
573
  dump=False, stats=False, hist=False, show_name=False,
552
- result=True, display_runs_args=None):
574
+ result=True, display_runs_args=None, only_checks=False):
553
575
  lines = []
554
576
 
555
577
  if metadata:
@@ -568,7 +590,7 @@ def format_benchmark(bench, checks=True, metadata=False,
568
590
  format_stats(bench, lines=lines)
569
591
 
570
592
  if checks:
571
- format_checks(bench, lines=lines)
593
+ format_checks(bench, lines=lines, check_too_many_processes=only_checks)
572
594
 
573
595
  if result:
574
596
  empty_line(lines)
@@ -5,7 +5,12 @@
5
5
 
6
6
  import abc
7
7
  import importlib.metadata
8
+ import os.path
9
+ import shlex
10
+ import subprocess
8
11
  import sys
12
+ import tempfile
13
+ import uuid
9
14
 
10
15
 
11
16
  def get_hooks():
@@ -108,3 +113,58 @@ class pystats(HookBase):
108
113
 
109
114
  def __exit__(self, _exc_type, _exc_value, _traceback):
110
115
  sys._stats_off()
116
+
117
+
118
+ class perf_record(HookBase):
119
+ """Profile the benchmark using perf-record.
120
+
121
+ Profile data is written to the current directory directory by default, or
122
+ to the value of the `PYPERF_PERF_RECORD_DATA_DIR` environment variable, if
123
+ it is provided.
124
+
125
+ Profile data files have a basename of the form `perf.data.<uuid>`
126
+
127
+ The value of the `PYPERF_PERF_RECORD_EXTRA_OPTS` environment variable is
128
+ appended to the command line of perf-record, if provided.
129
+ """
130
+
131
+ def __init__(self):
132
+ self.tempdir = tempfile.TemporaryDirectory()
133
+ self.ctl_fifo = self.mkfifo(self.tempdir.name, "ctl_fifo")
134
+ self.ack_fifo = self.mkfifo(self.tempdir.name, "ack_fifo")
135
+ perf_data_dir = os.environ.get("PYPERF_PERF_RECORD_DATA_DIR", "")
136
+ perf_data_basename = f"perf.data.{uuid.uuid4()}"
137
+ cmd = ["perf", "record",
138
+ "--pid", str(os.getpid()),
139
+ "--output", os.path.join(perf_data_dir, perf_data_basename),
140
+ "--control", f"fifo:{self.ctl_fifo},{self.ack_fifo}"]
141
+ extra_opts = os.environ.get("PYPERF_PERF_RECORD_EXTRA_OPTS", "")
142
+ cmd += shlex.split(extra_opts)
143
+ self.perf = subprocess.Popen(
144
+ cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
145
+ self.ctl_fd = open(self.ctl_fifo, "w")
146
+ self.ack_fd = open(self.ack_fifo, "r")
147
+
148
+ def __enter__(self):
149
+ self.exec_perf_cmd("enable")
150
+
151
+ def __exit__(self, _exc_type, _exc_value, _traceback):
152
+ self.exec_perf_cmd("disable")
153
+
154
+ def teardown(self, metadata):
155
+ try:
156
+ self.exec_perf_cmd("stop")
157
+ self.perf.wait(timeout=120)
158
+ finally:
159
+ self.ctl_fd.close()
160
+ self.ack_fd.close()
161
+
162
+ def mkfifo(self, tmpdir, basename):
163
+ path = os.path.join(tmpdir, basename)
164
+ os.mkfifo(path)
165
+ return path
166
+
167
+ def exec_perf_cmd(self, cmd):
168
+ self.ctl_fd.write(f"{cmd}\n")
169
+ self.ctl_fd.flush()
170
+ self.ack_fd.readline()
@@ -107,7 +107,7 @@ def load_hooks(metadata):
107
107
  import pyperf._hooks
108
108
 
109
109
  hook_managers = pyperf._hooks.instantiate_selected_hooks(hook_names)
110
- metadata["hooks"] = ", ".join(hook_managers.values())
110
+ metadata["hooks"] = ", ".join(hook_managers.keys())
111
111
  else:
112
112
  hook_managers = {}
113
113
 
@@ -270,7 +270,10 @@ def create_environ(inherit_environ, locale, copy_all):
270
270
  env = {}
271
271
  copy_env = ["PATH", "HOME", "TEMP", "COMSPEC", "SystemRoot", "SystemDrive",
272
272
  # Python specific variables
273
- "PYTHONPATH", "PYTHON_CPU_COUNT", "PYTHON_GIL"]
273
+ "PYTHONPATH", "PYTHON_CPU_COUNT", "PYTHON_GIL",
274
+ # Pyperf specific variables
275
+ "PYPERF_PERF_RECORD_DATA_DIR", "PYPERF_PERF_RECORD_EXTRA_OPTS",
276
+ ]
274
277
  if locale:
275
278
  copy_env.extend(('LANG', 'LC_ADDRESS', 'LC_ALL', 'LC_COLLATE',
276
279
  'LC_CTYPE', 'LC_IDENTIFICATION', 'LC_MEASUREMENT',
@@ -67,11 +67,7 @@ class MiscTests(unittest.TestCase):
67
67
  self.assertRaises(ValueError, parse_run_list, '1,')
68
68
 
69
69
  def test_setup_version(self):
70
- try:
71
- from importlib.metadata import version
72
- except ModuleNotFoundError:
73
- # Workaround for Python 3.7
74
- from importlib_metadata import version
70
+ from importlib.metadata import version
75
71
 
76
72
  self.assertEqual(pyperf.__version__, version("pyperf"))
77
73
 
@@ -628,8 +628,18 @@ class TestPerfCLI(BaseTestCase, unittest.TestCase):
628
628
 
629
629
  def test_check_stable(self):
630
630
  stdout = self.run_command('check', TELCO)
631
- self.assertEqual(stdout.rstrip(),
632
- 'The benchmark seems to be stable')
631
+ self.assertIn(
632
+ textwrap.dedent(
633
+ """
634
+ Benchmark was run more times than necessary to get a stable result.
635
+ Consider passing processes=7 to the Runner constructor to save time.
636
+ """
637
+ ).strip(), stdout.rstrip()
638
+ )
639
+ self.assertIn(
640
+ 'The benchmark seems to be stable',
641
+ stdout.rstrip()
642
+ )
633
643
 
634
644
  def test_command(self):
635
645
  command = [sys.executable, '-c', 'pass']
@@ -689,7 +699,7 @@ class TestPerfCLI(BaseTestCase, unittest.TestCase):
689
699
  '[1,2]*1000',
690
700
  '-o', tmp_name)
691
701
  bench = pyperf.Benchmark.load(tmp_name)
692
-
702
+
693
703
  self._check_track_memory_bench(bench, loops=5)
694
704
 
695
705
  def test_track_memory(self):
@@ -97,10 +97,7 @@ class TestRunner(unittest.TestCase):
97
97
 
98
98
  try:
99
99
  s = pstats.Stats(name)
100
- if sys.version_info < (3, 9):
101
- assert len(s.stats)
102
- else:
103
- assert len(s.get_stats_profile().func_profiles)
100
+ assert len(s.get_stats_profile().func_profiles)
104
101
  finally:
105
102
  if os.path.isfile(name):
106
103
  os.unlink(name)
@@ -123,10 +120,7 @@ class TestRunner(unittest.TestCase):
123
120
  try:
124
121
  import pstats
125
122
  s = pstats.Stats(name)
126
- if sys.version_info < (3, 9):
127
- assert len(s.stats)
128
- else:
129
- assert len(s.get_stats_profile().func_profiles)
123
+ assert len(s.get_stats_profile().func_profiles)
130
124
  finally:
131
125
  if os.path.isfile(name):
132
126
  os.unlink(name)
@@ -512,6 +506,18 @@ class TestRunner(unittest.TestCase):
512
506
  self.assertEqual(bench.get_metadata()['command'],
513
507
  ' '.join(map(shell_quote, args)))
514
508
 
509
+ def test_hook_command(self):
510
+ args = [sys.executable, '-c', 'pass']
511
+
512
+ runner = self.create_runner('-l1 -w0 -n1 --worker --hook _test_hook'.split())
513
+ with tests.capture_stdout():
514
+ bench = runner.bench_command('bench', args)
515
+
516
+ self.assertEqual(bench.get_metadata()['command'],
517
+ ' '.join(map(shell_quote, args)))
518
+ self.assertEqual(bench.get_metadata()["hooks"],
519
+ "_test_hook")
520
+
515
521
  def test_single_instance(self):
516
522
  runner1 = self.create_runner([]) # noqa
517
523
  with self.assertRaises(RuntimeError):
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.2
2
2
  Name: pyperf
3
- Version: 2.8.0
3
+ Version: 2.9.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>
@@ -13,13 +13,12 @@ Classifier: Natural Language :: English
13
13
  Classifier: Operating System :: OS Independent
14
14
  Classifier: Programming Language :: Python :: 3
15
15
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
16
- Requires-Python: >=3.7
16
+ Requires-Python: >=3.9
17
17
  Description-Content-Type: text/x-rst
18
18
  License-File: COPYING
19
19
  Requires-Dist: psutil>=5.9.0
20
20
  Provides-Extra: dev
21
21
  Requires-Dist: tox; extra == "dev"
22
- Requires-Dist: importlib-metadata; python_version < "3.8" and extra == "dev"
23
22
 
24
23
  ******
25
24
  pyperf
@@ -3,4 +3,5 @@ pyperf = pyperf.__main__:main
3
3
 
4
4
  [pyperf.hook]
5
5
  _test_hook = pyperf._hooks:_test_hook
6
+ perf_record = pyperf._hooks:perf_record
6
7
  pystats = pyperf._hooks:pystats
@@ -0,0 +1,4 @@
1
+ psutil>=5.9.0
2
+
3
+ [dev]
4
+ tox
@@ -44,19 +44,19 @@ classifiers = [
44
44
  "Programming Language :: Python :: 3",
45
45
  "Topic :: Software Development :: Libraries :: Python Modules"
46
46
  ]
47
- requires-python = ">=3.7"
47
+ requires-python = ">=3.9"
48
48
  dependencies = ["psutil>=5.9.0"]
49
49
 
50
50
  [project.optional-dependencies]
51
51
  dev = [
52
52
  'tox',
53
- 'importlib-metadata; python_version < "3.8"'
54
53
  ]
55
54
 
56
55
  [project.scripts]
57
56
  pyperf = "pyperf.__main__:main"
58
57
 
59
58
  [project.entry-points."pyperf.hook"]
59
+ perf_record = "pyperf._hooks:perf_record"
60
60
  pystats = "pyperf._hooks:pystats"
61
61
  _test_hook = "pyperf._hooks:_test_hook"
62
62
 
@@ -1,7 +0,0 @@
1
- psutil>=5.9.0
2
-
3
- [dev]
4
- tox
5
-
6
- [dev:python_version < "3.8"]
7
- importlib-metadata
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes