From 4edfa2a6cbcad125cd5348ae6dae36810885983f Mon Sep 17 00:00:00 2001 From: Gyeongjae Choi Date: Fri, 13 Feb 2026 14:51:58 +0900 Subject: [PATCH 1/7] Implement inprocess manager --- pyperf/_inprocess_manager.py | 33 +++++++++++++++++++++++++++++++++ pyperf/_runner.py | 25 +++++++++++++++++++++++-- 2 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 pyperf/_inprocess_manager.py diff --git a/pyperf/_inprocess_manager.py b/pyperf/_inprocess_manager.py new file mode 100644 index 00000000..b3ed8e1e --- /dev/null +++ b/pyperf/_inprocess_manager.py @@ -0,0 +1,33 @@ +import pyperf +from pyperf._manager import Manager +from pyperf._worker import WorkerProcessTask + + +class InProcessManager(Manager): + def __init__(self, runner, task): + super().__init__(runner) + self._task_func = task.task_func + self._task_name = task.name + self._func_metadata = { + k: v for k, v in task.metadata.items() if k not in ("name",) + } + self._inner_loops = task.inner_loops + + def spawn_worker(self, calibrate_loops, calibrate_warmups): + args = self.args + args.calibrate_loops = int(calibrate_loops == 1) + args.recalibrate_loops = int(calibrate_loops > 1) + args.calibrate_warmups = int(calibrate_warmups == 1) + args.recalibrate_warmups = int(calibrate_warmups > 1) + + task = WorkerProcessTask( + self.runner, + self._task_name, + self._task_func, + self._func_metadata, + ) + task.inner_loops = self._inner_loops + run = task.create_run() + + bench = pyperf.Benchmark((run,)) + return pyperf.BenchmarkSuite([bench]) diff --git a/pyperf/_runner.py b/pyperf/_runner.py index d87a2f69..d983cb27 100644 --- a/pyperf/_runner.py +++ b/pyperf/_runner.py @@ -190,6 +190,13 @@ def __init__(self, values=None, processes=None, type=strictly_positive) parser.add_argument('--worker', action='store_true', help='Worker process, run the benchmark.') + parser.add_argument('--in-process', action='store_true', + dest='in_process', + help='Run benchmark in the current process ' + 'without spawning worker subprocesses. ' + 'Only used for environments that ' + 'do not support subprocesses, ' + 'like WebAssembly.') parser.add_argument('--worker-task', type=positive_or_nul, metavar='TASK_ID', help='Identifier of the worker task: ' 'only execute the benchmark function TASK_ID') @@ -266,8 +273,9 @@ def _multiline_output(self): return self.args.verbose or multiline_output(self.args) def _only_in_worker(self, option): - if not self.args.worker: - raise CLIError("option %s requires --worker" % option) + if not self.args.worker and not self.args.in_process: + raise CLIError("option %s requires --worker or --in-process" + % option) def _process_args_impl(self): args = self.args @@ -461,6 +469,8 @@ def _main(self, task): try: if args.worker: bench = self._worker(task) + elif args.in_process: + bench = self._in_process(task) elif args.compare_to: self._compare_to() bench = None @@ -684,6 +694,17 @@ def _manager(self): self._display_result(bench) return bench + def _in_process(self, task): + from pyperf._inprocess_manager import InProcessManager + + if self.args.verbose and self._worker_task > 0: + print() + bench = InProcessManager(self, task).create_bench() + if not self.args.quiet: + print() + self._display_result(bench) + return bench + def _compare_to(self): # Use lazy import to limit imports on 'import pyperf' from pyperf._compare import timeit_compare_benchs From 97dce8a1c1e0d4922298ad998c5a9bf6df2289c1 Mon Sep 17 00:00:00 2001 From: Gyeongjae Choi Date: Fri, 13 Feb 2026 14:52:13 +0900 Subject: [PATCH 2/7] Unittest --- pyperf/tests/test_inprocess.py | 137 +++++++++++++++++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 pyperf/tests/test_inprocess.py diff --git a/pyperf/tests/test_inprocess.py b/pyperf/tests/test_inprocess.py new file mode 100644 index 00000000..67c22507 --- /dev/null +++ b/pyperf/tests/test_inprocess.py @@ -0,0 +1,137 @@ +import os.path +import tempfile +import unittest +from unittest import mock + +import pyperf +from pyperf import tests + + +def check_args(loops, a, b): + if a != 1: + raise ValueError + if b != 2: + raise ValueError + return loops + + +class TestInProcess(unittest.TestCase): + def create_runner(self, args, **kwargs): + pyperf.Runner._created.clear() + runner = pyperf.Runner(**kwargs) + runner._cpu_affinity = lambda: None + runner.parse_args(args) + return runner + + def fake_timer(self): + t = self._timer_value + self._timer_value += 1.0 + return t + + def exec_in_process(self, *extra_args, name="bench", time_func=None, **kwargs): + self._timer_value = 0.0 + + def fake_get_clock_info(clock): + class ClockInfo: + implementation = "fake_clock" + resolution = 1.0 + + return ClockInfo() + + args = ["--in-process", "-p1", "-l1", "-w1"] + list(extra_args) + runner = self.create_runner(args, **kwargs) + + with mock.patch("time.perf_counter", self.fake_timer): + with mock.patch("time.get_clock_info", fake_get_clock_info): + with tests.capture_stdout() as stdout: + with tests.capture_stderr() as stderr: + if time_func: + bench = runner.bench_time_func(name, time_func) + else: + bench = runner.bench_func(name, check_args, None, 1, 2) + + stdout = stdout.getvalue() + stderr = stderr.getvalue() + return bench, stdout, stderr + + def test_bench_func(self): + bench, stdout, _ = self.exec_in_process() + self.assertIsInstance(bench, pyperf.Benchmark) + self.assertEqual(bench.get_name(), "bench") + + def test_bench_time_func(self): + def time_func(loops): + return 1.0 + + bench, stdout, _ = self.exec_in_process(time_func=time_func) + self.assertIsInstance(bench, pyperf.Benchmark) + self.assertEqual(bench.get_name(), "bench") + self.assertEqual(bench.get_nvalue(), 3) + + def test_values_count(self): + bench, _, _ = self.exec_in_process("-n5") + self.assertEqual(bench.get_nvalue(), 5) + + def test_json_output(self): + with tests.temporary_directory() as tmpdir: + filename = os.path.join(tmpdir, "test.json") + bench, _, _ = self.exec_in_process("--output", filename) + loaded = pyperf.Benchmark.load(filename) + self.assertEqual(loaded.get_name(), bench.get_name()) + self.assertEqual(loaded.get_nvalue(), bench.get_nvalue()) + + def test_calibrate_loops(self): + def time_func(loops): + return loops * 1e-6 + + bench, stdout, _ = self.exec_in_process( + "-p1", "-w0", "-n2", "--min-time=0.001", time_func=time_func + ) + self.assertIsInstance(bench, pyperf.Benchmark) + + def test_two_benchmarks(self): + self._timer_value = 0.0 + + def fake_get_clock_info(clock): + class ClockInfo: + implementation = "fake_clock" + resolution = 1.0 + + return ClockInfo() + + args = ["--in-process", "-p1", "-l1", "-w0", "-n3"] + runner = self.create_runner(args) + + def time_func1(loops): + return 1.0 + + def time_func2(loops): + return 2.0 + + with mock.patch("time.perf_counter", self.fake_timer): + with mock.patch("time.get_clock_info", fake_get_clock_info): + with tests.capture_stdout(): + bench1 = runner.bench_time_func("bench1", time_func1) + bench2 = runner.bench_time_func("bench2", time_func2) + + self.assertEqual(bench1.get_name(), "bench1") + self.assertEqual(bench1.get_values(), (1.0, 1.0, 1.0)) + self.assertEqual(bench2.get_name(), "bench2") + self.assertEqual(bench2.get_values(), (2.0, 2.0, 2.0)) + + def test_show_name(self): + bench, stdout, _ = self.exec_in_process(name="NAME") + self.assertIn("NAME:", stdout) + + def test_show_name_false(self): + bench, stdout, _ = self.exec_in_process(name="NAME", show_name=False) + self.assertNotIn("NAME:", stdout) + + def test_no_subprocess_spawned(self): + with mock.patch("pyperf._manager.Manager.spawn_worker") as mock_spawn: + bench, _, _ = self.exec_in_process() + mock_spawn.assert_not_called() + + +if __name__ == "__main__": + unittest.main() From 24d5d6109d0a37a2c6449e65abf4778cd957730f Mon Sep 17 00:00:00 2001 From: Gyeongjae Choi Date: Fri, 13 Feb 2026 14:52:29 +0900 Subject: [PATCH 3/7] psutil not available in wasm --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index caa49942..01b3f3b5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,7 +46,7 @@ classifiers = [ ] # Also update: README.rst, docs/run_benchmark.rst requires-python = ">=3.9" -dependencies = ["psutil>=5.9.0"] +dependencies = ["psutil>=5.9.0; platform_machine != 'wasm32'"] [project.optional-dependencies] dev = [ From b6bc17e1a997ecbc66a757bd248ced8d18aa0e5e Mon Sep 17 00:00:00 2001 From: Gyeongjae Choi Date: Fri, 13 Feb 2026 14:52:35 +0900 Subject: [PATCH 4/7] Integration test --- .github/workflows/build.yml | 20 ++++++ pyperf/tests/test_pyodide.sh | 123 +++++++++++++++++++++++++++++++++++ 2 files changed, 143 insertions(+) create mode 100755 pyperf/tests/test_pyodide.sh diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c9d9da59..7950cff5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -65,3 +65,23 @@ jobs: - name: Run Tox # Run tox using the version of Python in `PATH` run: tox -e py + pyodide: + runs-on: ubuntu-latest + env: + PYODIDE_VERSION: '0.29.2' + PYTHON_VERSION: '3.13' + NODE_VERSION: '24' + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-python@v6 + with: + python-version: ${{ env.PYTHON_VERSION }} + - uses: actions/setup-node@v6 + with: + node-version: ${{ env.NODE_VERSION }} + - name: Install pyodide-build to run integration tests + run: | + pip install pyodide-build + pyodide xbuildenv install ${{ env.PYODIDE_VERSION }} + - name: Run Pyodide integration tests + run: ./pyperf/tests/test_pyodide.sh diff --git a/pyperf/tests/test_pyodide.sh b/pyperf/tests/test_pyodide.sh new file mode 100755 index 00000000..a154bfe7 --- /dev/null +++ b/pyperf/tests/test_pyodide.sh @@ -0,0 +1,123 @@ +#!/usr/bin/env bash +# Integration test: run pyperf --in-process inside Pyodide + Node.js. +# +# This script sets up a Pyodide virtual environment, installs pyperf into it, +# and runs a simple benchmark using --in-process mode to verify that pyperf +# works in environments without subprocess support. +# +# Usage: +# pip install pyodide-build +# pyodide xbuildenv install 0.29.3 +# ./test_pyodide.sh +# +# Prerequisites +# - Python 3.13 +# - Node.js 24+ +set -euo pipefail + +VENV_DIR=".venv-pyodide-test" +RESULT_FILE="$(mktemp)" +rm -rf "$RESULT_FILE" "$VENV_DIR" +trap 'rm -rf "$RESULT_FILE" "$VENV_DIR"' EXIT + +# --- Setup --- + +echo "==> Creating Pyodide virtual environment" +pyodide venv "$VENV_DIR" +source "$VENV_DIR/bin/activate" + +echo "==> Installing pyperf into Pyodide venv" +pip install . + +# --- Test 1: bench_time_func with --in-process --- + +echo "==> Running bench_time_func benchmark (--in-process)" +python -c " +import pyperf + +import time, math + +def bench_nqueens(loops, n=8): + t0 = time.perf_counter() + for _ in range(loops): + solutions = [] + def solve(queens, row): + if row == n: + solutions.append(queens[:]) + return + for col in range(n): + if col not in queens: + diag1 = set(q + i for i, q in enumerate(queens)) + diag2 = set(q - i for i, q in enumerate(queens)) + if col + row not in diag1 and col - row not in diag2: + queens.append(col) + solve(queens, row + 1) + queens.pop() + solve([], 0) + return time.perf_counter() - t0 + +runner = pyperf.Runner() +runner.parse_args(['--in-process', '-w1', '-n3', '-l1', '--output', '$RESULT_FILE']) +runner.bench_time_func('nqueens', bench_nqueens) +" + +# --- Verify output --- + +echo "==> Verifying JSON output" +python -c " +import json, sys + +with open('$RESULT_FILE') as f: + data = json.load(f) + +name = data['metadata']['name'] +runs = data['benchmarks'][0]['runs'] +n_values = len([r for r in runs if 'values' in r]) + +print(f'Benchmark: {name}') +print(f'Value runs: {n_values}') + +assert name == 'nqueens', f'Expected name nqueens, got {name}' +assert n_values > 0, f'Expected at least 1 value run, got {n_values}' +print('OK: JSON output is valid') +" + +# --- Test 2: bench_func with --in-process --- + +echo "==> Running bench_func benchmark (--in-process)" +python -c " +import pyperf + +def pidigits(n): + k, a, b, a1, b1 = 2, 4, 1, 12, 4 + digits = [] + while len(digits) < n: + p, q, k = k * k, 2 * k + 1, k + 1 + a, b, a1, b1 = a1, b1, p * a + q * a1, p * b + q * b1 + d, d1 = a // b, a1 // b1 + while d == d1 and len(digits) < n: + digits.append(d) + a, a1 = 10 * (a % b), 10 * (a1 % b1) + d, d1 = a // b, a1 // b1 + return digits + +runner = pyperf.Runner() +runner.parse_args(['--in-process', '-p1', '-w0', '-n2', '-l1', '--quiet']) +bench = runner.bench_func('pidigits', pidigits, 100) +assert bench is not None, 'bench_func returned None' +assert bench.get_nvalue() == 2, f'Expected 2 values, got {bench.get_nvalue()}' +print('OK: bench_func works') +" + +# --- Test 3: python -m pyperf show --- + +echo "==> Running python -m pyperf show" +python -m pyperf show "$RESULT_FILE" + +echo "==> Running python -m pyperf stats" +python -m pyperf stats "$RESULT_FILE" + +echo "==> Running python -m pyperf dump --quiet" +python -m pyperf dump --quiet "$RESULT_FILE" + +echo "OK: python -m pyperf works" From 507588c72ee8e19ef9353a1b710fdb6fcdb63f34 Mon Sep 17 00:00:00 2001 From: Gyeongjae Choi Date: Fri, 13 Feb 2026 15:08:55 +0900 Subject: [PATCH 5/7] Fix lint --- pyperf/tests/test_inprocess.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pyperf/tests/test_inprocess.py b/pyperf/tests/test_inprocess.py index 67c22507..265e72ac 100644 --- a/pyperf/tests/test_inprocess.py +++ b/pyperf/tests/test_inprocess.py @@ -1,5 +1,4 @@ import os.path -import tempfile import unittest from unittest import mock @@ -38,7 +37,7 @@ class ClockInfo: return ClockInfo() - args = ["--in-process", "-p1", "-l1", "-w1"] + list(extra_args) + args = ["--in-process", "-p1", "-n3", "-l1", "-w1"] + list(extra_args) runner = self.create_runner(args, **kwargs) with mock.patch("time.perf_counter", self.fake_timer): From 91872cccc9927bea7afde3a6717c93b7cd88fe8d Mon Sep 17 00:00:00 2001 From: Gyeongjae Choi Date: Fri, 20 Feb 2026 10:46:54 +0900 Subject: [PATCH 6/7] Test timeit --- pyperf/tests/test_pyodide.sh | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/pyperf/tests/test_pyodide.sh b/pyperf/tests/test_pyodide.sh index a154bfe7..53ee2718 100755 --- a/pyperf/tests/test_pyodide.sh +++ b/pyperf/tests/test_pyodide.sh @@ -109,7 +109,14 @@ assert bench.get_nvalue() == 2, f'Expected 2 values, got {bench.get_nvalue()}' print('OK: bench_func works') " -# --- Test 3: python -m pyperf show --- +# --- Test 3: python -m pyperf timeit --in-process --- + +echo "==> Running python -m pyperf timeit --in-process" +python -m pyperf timeit --in-process -p1 -n3 -l1 -w1 \ + -s "import math" \ + "sum(math.sqrt(i) for i in range(1000))" + +# --- Test 4: python -m pyperf subcommands --- echo "==> Running python -m pyperf show" python -m pyperf show "$RESULT_FILE" @@ -120,4 +127,4 @@ python -m pyperf stats "$RESULT_FILE" echo "==> Running python -m pyperf dump --quiet" python -m pyperf dump --quiet "$RESULT_FILE" -echo "OK: python -m pyperf works" +echo "OK: all tests passed" From 23f9952e6a7ad9ad50cfd61b3ff41fd9f89c7e74 Mon Sep 17 00:00:00 2001 From: Gyeongjae Choi Date: Mon, 23 Feb 2026 13:41:12 +0900 Subject: [PATCH 7/7] revert pyodide related changes --- .github/workflows/build.yml | 20 ------ pyperf/tests/test_pyodide.sh | 130 ----------------------------------- pyproject.toml | 2 +- 3 files changed, 1 insertion(+), 151 deletions(-) delete mode 100755 pyperf/tests/test_pyodide.sh diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7950cff5..c9d9da59 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -65,23 +65,3 @@ jobs: - name: Run Tox # Run tox using the version of Python in `PATH` run: tox -e py - pyodide: - runs-on: ubuntu-latest - env: - PYODIDE_VERSION: '0.29.2' - PYTHON_VERSION: '3.13' - NODE_VERSION: '24' - steps: - - uses: actions/checkout@v6 - - uses: actions/setup-python@v6 - with: - python-version: ${{ env.PYTHON_VERSION }} - - uses: actions/setup-node@v6 - with: - node-version: ${{ env.NODE_VERSION }} - - name: Install pyodide-build to run integration tests - run: | - pip install pyodide-build - pyodide xbuildenv install ${{ env.PYODIDE_VERSION }} - - name: Run Pyodide integration tests - run: ./pyperf/tests/test_pyodide.sh diff --git a/pyperf/tests/test_pyodide.sh b/pyperf/tests/test_pyodide.sh deleted file mode 100755 index 53ee2718..00000000 --- a/pyperf/tests/test_pyodide.sh +++ /dev/null @@ -1,130 +0,0 @@ -#!/usr/bin/env bash -# Integration test: run pyperf --in-process inside Pyodide + Node.js. -# -# This script sets up a Pyodide virtual environment, installs pyperf into it, -# and runs a simple benchmark using --in-process mode to verify that pyperf -# works in environments without subprocess support. -# -# Usage: -# pip install pyodide-build -# pyodide xbuildenv install 0.29.3 -# ./test_pyodide.sh -# -# Prerequisites -# - Python 3.13 -# - Node.js 24+ -set -euo pipefail - -VENV_DIR=".venv-pyodide-test" -RESULT_FILE="$(mktemp)" -rm -rf "$RESULT_FILE" "$VENV_DIR" -trap 'rm -rf "$RESULT_FILE" "$VENV_DIR"' EXIT - -# --- Setup --- - -echo "==> Creating Pyodide virtual environment" -pyodide venv "$VENV_DIR" -source "$VENV_DIR/bin/activate" - -echo "==> Installing pyperf into Pyodide venv" -pip install . - -# --- Test 1: bench_time_func with --in-process --- - -echo "==> Running bench_time_func benchmark (--in-process)" -python -c " -import pyperf - -import time, math - -def bench_nqueens(loops, n=8): - t0 = time.perf_counter() - for _ in range(loops): - solutions = [] - def solve(queens, row): - if row == n: - solutions.append(queens[:]) - return - for col in range(n): - if col not in queens: - diag1 = set(q + i for i, q in enumerate(queens)) - diag2 = set(q - i for i, q in enumerate(queens)) - if col + row not in diag1 and col - row not in diag2: - queens.append(col) - solve(queens, row + 1) - queens.pop() - solve([], 0) - return time.perf_counter() - t0 - -runner = pyperf.Runner() -runner.parse_args(['--in-process', '-w1', '-n3', '-l1', '--output', '$RESULT_FILE']) -runner.bench_time_func('nqueens', bench_nqueens) -" - -# --- Verify output --- - -echo "==> Verifying JSON output" -python -c " -import json, sys - -with open('$RESULT_FILE') as f: - data = json.load(f) - -name = data['metadata']['name'] -runs = data['benchmarks'][0]['runs'] -n_values = len([r for r in runs if 'values' in r]) - -print(f'Benchmark: {name}') -print(f'Value runs: {n_values}') - -assert name == 'nqueens', f'Expected name nqueens, got {name}' -assert n_values > 0, f'Expected at least 1 value run, got {n_values}' -print('OK: JSON output is valid') -" - -# --- Test 2: bench_func with --in-process --- - -echo "==> Running bench_func benchmark (--in-process)" -python -c " -import pyperf - -def pidigits(n): - k, a, b, a1, b1 = 2, 4, 1, 12, 4 - digits = [] - while len(digits) < n: - p, q, k = k * k, 2 * k + 1, k + 1 - a, b, a1, b1 = a1, b1, p * a + q * a1, p * b + q * b1 - d, d1 = a // b, a1 // b1 - while d == d1 and len(digits) < n: - digits.append(d) - a, a1 = 10 * (a % b), 10 * (a1 % b1) - d, d1 = a // b, a1 // b1 - return digits - -runner = pyperf.Runner() -runner.parse_args(['--in-process', '-p1', '-w0', '-n2', '-l1', '--quiet']) -bench = runner.bench_func('pidigits', pidigits, 100) -assert bench is not None, 'bench_func returned None' -assert bench.get_nvalue() == 2, f'Expected 2 values, got {bench.get_nvalue()}' -print('OK: bench_func works') -" - -# --- Test 3: python -m pyperf timeit --in-process --- - -echo "==> Running python -m pyperf timeit --in-process" -python -m pyperf timeit --in-process -p1 -n3 -l1 -w1 \ - -s "import math" \ - "sum(math.sqrt(i) for i in range(1000))" - -# --- Test 4: python -m pyperf subcommands --- - -echo "==> Running python -m pyperf show" -python -m pyperf show "$RESULT_FILE" - -echo "==> Running python -m pyperf stats" -python -m pyperf stats "$RESULT_FILE" - -echo "==> Running python -m pyperf dump --quiet" -python -m pyperf dump --quiet "$RESULT_FILE" - -echo "OK: all tests passed" diff --git a/pyproject.toml b/pyproject.toml index 01b3f3b5..caa49942 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,7 +46,7 @@ classifiers = [ ] # Also update: README.rst, docs/run_benchmark.rst requires-python = ">=3.9" -dependencies = ["psutil>=5.9.0; platform_machine != 'wasm32'"] +dependencies = ["psutil>=5.9.0"] [project.optional-dependencies] dev = [