From c44359336bcb23b4d15e3aa5b64ec8c1005ffa84 Mon Sep 17 00:00:00 2001 From: Sergey Miryanov Date: Fri, 20 Mar 2026 01:14:33 +0500 Subject: [PATCH 1/5] Add hooks parameter to Runner --- pyperf/_hooks.py | 8 +++---- pyperf/_runner.py | 8 +++++-- pyperf/_worker.py | 3 ++- pyperf/tests/test_runner.py | 42 +++++++++++++++++++++++++++++++++++++ 4 files changed, 54 insertions(+), 7 deletions(-) diff --git a/pyperf/_hooks.py b/pyperf/_hooks.py index 9d910327..f7880413 100644 --- a/pyperf/_hooks.py +++ b/pyperf/_hooks.py @@ -29,18 +29,18 @@ def get_hook_names(): return (x.name for x in get_hooks()) -def get_selected_hooks(hook_names): +def get_selected_hooks(hook_names, hooks = None): if hook_names is None: return - hook_mapping = {hook.name: hook for hook in get_hooks()} + hook_mapping = {hook.name: hook for hook in get_hooks() + tuple(hooks or ())} for hook_name in hook_names: yield hook_mapping[hook_name] -def instantiate_selected_hooks(hook_names): +def instantiate_selected_hooks(hook_names, hooks = None): hook_managers = {} - for hook in get_selected_hooks(hook_names): + for hook in get_selected_hooks(hook_names, hooks): try: hook_managers[hook.name] = hook.load()() except HookError as e: diff --git a/pyperf/_runner.py b/pyperf/_runner.py index d87a2f69..ba1d1f31 100644 --- a/pyperf/_runner.py +++ b/pyperf/_runner.py @@ -77,7 +77,7 @@ def __init__(self, values=None, processes=None, loops=0, min_time=0.1, metadata=None, show_name=True, program_args=None, add_cmdline_args=None, - _argparser=None, warmups=1): + _argparser=None, warmups=1, hooks=None): # Watchdog: ensure that only once instance of Runner (or a Runner # subclass) is created per process to prevent bad surprises @@ -247,7 +247,11 @@ def __init__(self, values=None, processes=None, help='Collect profile data using cProfile ' 'and output to the given file.') - hook_names = list(get_hook_names()) + self._custom_hooks = hooks or [] + if not isinstance(self._custom_hooks, (list, tuple)): + raise RuntimeError("hooks parameter should be list, tuple or None") + + hook_names = list(get_hook_names()) + [hook.name for hook in self._custom_hooks] parser.add_argument( '--hook', action="append", choices=hook_names, metavar=f"{', '.join(x for x in hook_names if not x.startswith('_'))}", diff --git a/pyperf/_worker.py b/pyperf/_worker.py index 67a7452f..3547faeb 100644 --- a/pyperf/_worker.py +++ b/pyperf/_worker.py @@ -31,6 +31,7 @@ def __init__(self, runner, name, task_func, func_metadata): self.args = args self.task_func = task_func self.loops = args.loops + self._custom_hooks = runner._custom_hooks self.metadata = dict(runner.metadata) if func_metadata: @@ -61,7 +62,7 @@ def _compute_values(self, values, nvalue, task_func = self.task_func - hook_managers = instantiate_selected_hooks(args.hook) + hook_managers = instantiate_selected_hooks(args.hook, self._custom_hooks) if len(hook_managers): self.metadata["hooks"] = ", ".join(hook_managers.keys()) diff --git a/pyperf/tests/test_runner.py b/pyperf/tests/test_runner.py index fc2fb58a..a3603ccd 100644 --- a/pyperf/tests/test_runner.py +++ b/pyperf/tests/test_runner.py @@ -10,6 +10,7 @@ import pyperf from pyperf import tests +from pyperf._hooks import HookBase from pyperf._utils import create_pipe, MS_WINDOWS, shell_quote @@ -518,6 +519,47 @@ def test_hook_command(self): self.assertEqual(bench.get_metadata()["hooks"], "_test_hook") + def test_custom_hook(self): + class State: + inited = 0 + entered = 0 + exited = 0 + + s = State() + + class CustomHook(HookBase): + name = "custom_hook" + + @staticmethod + def load(): + return lambda: CustomHook(s) + + def __init__(self, state): + self.state = state + self.state.inited += 1 + + def __enter__(self): + self.state.entered += 1 + + def __exit__(self, _exc_type, _exc_value, _traceback): + self.state.exited += 1 + + def teardown(self, _metadata): + _metadata[self.name] = "done" + + kwargs = {"hooks" : [CustomHook]} + args = '-l1 -w0 -n1 --worker --verbose --hook custom_hook'.split() + runner = self.create_runner(args, **kwargs) + def time_func(loops): + return 1.0 + + bench = runner.bench_time_func('bench1', time_func) + self.assertEqual(bench.get_metadata()["hooks"], CustomHook.name) + self.assertEqual(bench.get_metadata()[CustomHook.name], "done") + self.assertEqual(s.inited, 1) + self.assertEqual(s.entered, 1) + self.assertEqual(s.exited, 1) + def test_single_instance(self): runner1 = self.create_runner([]) # noqa with self.assertRaises(RuntimeError): From 1f935f7091f38df8784397bbfbaf66c5ea25f6c3 Mon Sep 17 00:00:00 2001 From: Sergey Miryanov Date: Fri, 20 Mar 2026 01:21:33 +0500 Subject: [PATCH 2/5] Update docs --- doc/api.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/doc/api.rst b/doc/api.rst index 538ceec9..490c0159 100644 --- a/doc/api.rst +++ b/doc/api.rst @@ -488,7 +488,7 @@ BenchmarkSuite class Runner class ------------ -.. class:: Runner(values=3, warmups=1, processes=20, loops=0, min_time=0.1, metadata=None, show_name=True, program_args=None, add_cmdline_args=None) +.. class:: Runner(values=3, warmups=1, processes=20, loops=0, min_time=0.1, metadata=None, show_name=True, program_args=None, add_cmdline_args=None, hooks=None) Tool to run a benchmark in text mode. @@ -512,6 +512,9 @@ Runner class (``list``) which must be modified in place and *args* is the :attr:`args` attribute of the runner. + *hooks* is a list of custom hooks. Each hook should implement ``HookBase`` and + have ``name`` attribute and ``load`` method that returns instance of the hook. + If *show_name* is true, displays the benchmark name. If isolated CPUs are detected, the CPU affinity is automatically From 25544c13c9ee7166d91deaf79ff6748ffde9bf2b Mon Sep 17 00:00:00 2001 From: Sergey Miryanov Date: Fri, 20 Mar 2026 16:33:08 +0500 Subject: [PATCH 3/5] Apply suggestions from code review Co-authored-by: Victor Stinner --- doc/api.rst | 2 +- pyperf/_hooks.py | 4 ++-- pyperf/_runner.py | 2 +- pyperf/tests/test_runner.py | 7 +++---- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/doc/api.rst b/doc/api.rst index 490c0159..aa4ba8ff 100644 --- a/doc/api.rst +++ b/doc/api.rst @@ -512,7 +512,7 @@ Runner class (``list``) which must be modified in place and *args* is the :attr:`args` attribute of the runner. - *hooks* is a list of custom hooks. Each hook should implement ``HookBase`` and + *hooks* is a list or tuple of custom hooks. Each hook should implement ``HookBase`` and have ``name`` attribute and ``load`` method that returns instance of the hook. If *show_name* is true, displays the benchmark name. diff --git a/pyperf/_hooks.py b/pyperf/_hooks.py index f7880413..6192ee81 100644 --- a/pyperf/_hooks.py +++ b/pyperf/_hooks.py @@ -29,7 +29,7 @@ def get_hook_names(): return (x.name for x in get_hooks()) -def get_selected_hooks(hook_names, hooks = None): +def get_selected_hooks(hook_names, hooks=None): if hook_names is None: return @@ -38,7 +38,7 @@ def get_selected_hooks(hook_names, hooks = None): yield hook_mapping[hook_name] -def instantiate_selected_hooks(hook_names, hooks = None): +def instantiate_selected_hooks(hook_names, hooks=None): hook_managers = {} for hook in get_selected_hooks(hook_names, hooks): try: diff --git a/pyperf/_runner.py b/pyperf/_runner.py index ba1d1f31..3a7ab11d 100644 --- a/pyperf/_runner.py +++ b/pyperf/_runner.py @@ -247,7 +247,7 @@ def __init__(self, values=None, processes=None, help='Collect profile data using cProfile ' 'and output to the given file.') - self._custom_hooks = hooks or [] + self._custom_hooks = hooks or () if not isinstance(self._custom_hooks, (list, tuple)): raise RuntimeError("hooks parameter should be list, tuple or None") diff --git a/pyperf/tests/test_runner.py b/pyperf/tests/test_runner.py index a3603ccd..c77e0251 100644 --- a/pyperf/tests/test_runner.py +++ b/pyperf/tests/test_runner.py @@ -544,12 +544,11 @@ def __enter__(self): def __exit__(self, _exc_type, _exc_value, _traceback): self.state.exited += 1 - def teardown(self, _metadata): - _metadata[self.name] = "done" + def teardown(self, metadata): + metadata[self.name] = "done" - kwargs = {"hooks" : [CustomHook]} args = '-l1 -w0 -n1 --worker --verbose --hook custom_hook'.split() - runner = self.create_runner(args, **kwargs) + runner = self.create_runner(args, hooks=[CustomHook]) def time_func(loops): return 1.0 From ececeaca00929f35a33dfe70dcf9d10f6f5ec493 Mon Sep 17 00:00:00 2001 From: Sergey Miryanov Date: Fri, 20 Mar 2026 23:23:01 +0500 Subject: [PATCH 4/5] Add documentation for HookBase --- doc/api.rst | 34 ++++++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/doc/api.rst b/doc/api.rst index aa4ba8ff..40450acb 100644 --- a/doc/api.rst +++ b/doc/api.rst @@ -485,6 +485,35 @@ BenchmarkSuite class It can be ``None``. +HookBase class +-------------- + +.. class:: HookBase() + + Hook used to do some actions before and after benchmark's run. It can collect + statistics, run external tools. + + Methods: + + .. method:: __enter__() + + Called immediately before runnig benchmark code. + + May be called multiple times per instance. + + .. method:: __exit__(exc_type, exc_value, traceback): + + Called immediately after runnig benchmark code. + + May be called multiple times per instance. + + .. method:: teardown(metadata) + + Called when the hook is completed for a process. + + May add any information collected to the passed-in ``metadata`` dictionary. + + Runner class ------------ @@ -512,8 +541,9 @@ Runner class (``list``) which must be modified in place and *args* is the :attr:`args` attribute of the runner. - *hooks* is a list or tuple of custom hooks. Each hook should implement ``HookBase`` and - have ``name`` attribute and ``load`` method that returns instance of the hook. + *hooks* is a list or tuple of custom hooks. Each hook should implement + :class:`HookBase` and have ``name`` attribute and ``load`` method that + returns instance of the hook. If *show_name* is true, displays the benchmark name. From f95e534a37b7bdc37ce3f2c9ad37cf8d288b2fdf Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Sat, 21 Mar 2026 11:03:45 +0100 Subject: [PATCH 5/5] Apply suggestions from code review Co-authored-by: Victor Stinner --- doc/api.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/api.rst b/doc/api.rst index 40450acb..1e079fa5 100644 --- a/doc/api.rst +++ b/doc/api.rst @@ -503,7 +503,7 @@ HookBase class .. method:: __exit__(exc_type, exc_value, traceback): - Called immediately after runnig benchmark code. + Called immediately after running benchmark code. May be called multiple times per instance. @@ -542,7 +542,7 @@ Runner class attribute of the runner. *hooks* is a list or tuple of custom hooks. Each hook should implement - :class:`HookBase` and have ``name`` attribute and ``load`` method that + :class:`HookBase` and have ``name`` attribute and ``load()`` method that returns instance of the hook. If *show_name* is true, displays the benchmark name.