From 9a8122a12837532c9384b95b29c7d7c3b4c1f197 Mon Sep 17 00:00:00 2001 From: Sune Debel <1228354+suned@users.noreply.github.com> Date: Wed, 5 Nov 2025 00:47:16 +0100 Subject: [PATCH 1/6] add benchmarks --- benchmarks.py | 65 +++++++++++++++++++++++++++++++++++++++++++++++ justfile | 4 +++ poetry.lock | 68 +++++++++++++++++++++++++++++++++++++------------- pyproject.toml | 3 ++- 4 files changed, 122 insertions(+), 18 deletions(-) create mode 100644 benchmarks.py diff --git a/benchmarks.py b/benchmarks.py new file mode 100644 index 0000000..d5d766b --- /dev/null +++ b/benchmarks.py @@ -0,0 +1,65 @@ +from functools import reduce +from typing import Callable + +from pytest_benchmark.fixture import BenchmarkFixture +from stateless import Ability, Depend, Need, Success, handle, need, run, success, supply +from stateless.errors import UnhandledAbilityError +from typing_extensions import Never + + +def create_effect_chain(chain_length: int) -> Callable[[], Success[None]]: + """Creates a chain using functools.reduce.""" + + def yield_from( + f: Callable[[], Success[None]], + ) -> Callable[[], Success[None]]: + def _() -> Success[None]: + result = yield from f() + return result + + return _ + + # Base function + def base_function() -> Success[None]: + result = yield from success(None) + return result + + # Create chain using reduce + return reduce( + lambda acc, _: yield_from(acc), range(chain_length - 1), base_function + ) + + +def test_effect_chain(benchmark: BenchmarkFixture) -> None: + """Benchmark a long chain of functions using functools.reduce.""" + + effect = create_effect_chain(500)() + benchmark(run, effect) + + +def never_handler(_: Ability) -> Never: + raise UnhandledAbilityError() + + +test_handler = handle(never_handler) + + +def create_handler_chain(chain_length: int) -> Callable[[], Success[str]]: + def base() -> Depend[Need[str], str]: + s = yield from need(str) + return s + + def wrap_test_handler( + f: Callable[[], Depend[Need[str], str]], + ) -> Callable[[], Depend[Need[str], str]]: + return test_handler(f) + + g = reduce(lambda acc, _: wrap_test_handler(acc), range(chain_length - 1), base) + + return supply("")(g) + + +def test_handler_chain(benchmark: BenchmarkFixture) -> None: + effect = create_handler_chain(500)() + + benchmark(run, effect) diff --git a/justfile b/justfile index 553b4d4..5c7a7ec 100644 --- a/justfile +++ b/justfile @@ -5,3 +5,7 @@ test: lint: pre-commit run --all-files + + +benchmark: + pytest --benchmark-compare benchmarks.py diff --git a/poetry.lock b/poetry.lock index be0ef17..69fd69c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -485,19 +485,19 @@ test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-co [[package]] name = "pluggy" -version = "1.3.0" +version = "1.6.0" description = "plugin and hook calling mechanisms for python" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "pluggy-1.3.0-py3-none-any.whl", hash = "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"}, - {file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"}, + {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, + {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, ] [package.extras] dev = ["pre-commit", "tox"] -testing = ["pytest", "pytest-benchmark"] +testing = ["coverage", "pytest", "pytest-benchmark"] [[package]] name = "pre-commit" @@ -561,6 +561,18 @@ files = [ [package.extras] tests = ["pytest"] +[[package]] +name = "py-cpuinfo" +version = "9.0.0" +description = "Get CPU info with pure Python" +optional = false +python-versions = "*" +groups = ["dev"] +files = [ + {file = "py-cpuinfo-9.0.0.tar.gz", hash = "sha256:3cdbbf3fac90dc6f118bfd64384f309edeadd902d7c8fb17f02ffa1fc3f49690"}, + {file = "py_cpuinfo-9.0.0-py3-none-any.whl", hash = "sha256:859625bc251f64e21f077d099d4162689c762b5d6a4c3c97553d56241c9674d5"}, +] + [[package]] name = "pygments" version = "2.16.1" @@ -599,26 +611,48 @@ nodejs = ["nodejs-wheel-binaries"] [[package]] name = "pytest" -version = "7.4.3" +version = "8.4.2" description = "pytest: simple powerful testing with Python" optional = false -python-versions = ">=3.7" +python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "pytest-7.4.3-py3-none-any.whl", hash = "sha256:0d009c083ea859a71b76adf7c1d502e4bc170b80a8ef002da5806527b9591fac"}, - {file = "pytest-7.4.3.tar.gz", hash = "sha256:d989d136982de4e3b29dabcc838ad581c64e8ed52c11fbe86ddebd9da0818cd5"}, + {file = "pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79"}, + {file = "pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01"}, ] [package.dependencies] -colorama = {version = "*", markers = "sys_platform == \"win32\""} -exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} -iniconfig = "*" -packaging = "*" -pluggy = ">=0.12,<2.0" -tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} +colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1", markers = "python_version < \"3.11\""} +iniconfig = ">=1" +packaging = ">=20" +pluggy = ">=1.5,<2" +pygments = ">=2.7.2" +tomli = {version = ">=1", markers = "python_version < \"3.11\""} + +[package.extras] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-benchmark" +version = "5.2.1" +description = "A ``pytest`` fixture for benchmarking code. It will group the tests into rounds that are calibrated to the chosen timer." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pytest_benchmark-5.2.1-py3-none-any.whl", hash = "sha256:a6e18fe0df2155e9d993db6ba03bdf85324794035ad986553787024ca59e8db9"}, + {file = "pytest_benchmark-5.2.1.tar.gz", hash = "sha256:56dc1455bda7ccb540aa671c496dafc8187d2769f278e5f137689476805b6f9d"}, +] + +[package.dependencies] +py-cpuinfo = "*" +pytest = ">=8.1" [package.extras] -testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] +aspect = ["aspectlib"] +elasticsearch = ["elasticsearch"] +histogram = ["pygal", "pygaljs", "setuptools"] [[package]] name = "pyyaml" @@ -848,4 +882,4 @@ files = [ [metadata] lock-version = "2.1" python-versions = "^3.10" -content-hash = "a7d793c47c20ac4d3e5052430b0d698262a87b591a17ec8b54652510de6b4982" +content-hash = "2ad5075792eefb92e61b7d5eb29744742ae1be69311d5efc746486e385ed29fb" diff --git a/pyproject.toml b/pyproject.toml index c8bcb1d..28abf12 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,12 +15,13 @@ cloudpickle = "^3.0.0" mypy = "^1.6.1" ipdb = "^0.13.13" ipython = "^8.17.2" -pytest = "^7.4.3" +pytest = "^8" pyright = "^1.1.336" pre-commit = "^3.5.0" ruff = "^0.14.2" coverage = "^7.3.2" toml = "^0.10.2" +pytest-benchmark = "^5.2.1" [tool.mypy] From 687dadb1f542f589464825662a470a04a0085644 Mon Sep 17 00:00:00 2001 From: Sune Debel <1228354+suned@users.noreply.github.com> Date: Wed, 5 Nov 2025 00:47:56 +0100 Subject: [PATCH 2/6] add saved benchmark --- ...4b95b29c7d7c3b4c1f197_20251104_234726.json | 113 ++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 .benchmarks/Darwin-CPython-3.13-64bit/0001_9a8122a12837532c9384b95b29c7d7c3b4c1f197_20251104_234726.json diff --git a/.benchmarks/Darwin-CPython-3.13-64bit/0001_9a8122a12837532c9384b95b29c7d7c3b4c1f197_20251104_234726.json b/.benchmarks/Darwin-CPython-3.13-64bit/0001_9a8122a12837532c9384b95b29c7d7c3b4c1f197_20251104_234726.json new file mode 100644 index 0000000..c0fe16c --- /dev/null +++ b/.benchmarks/Darwin-CPython-3.13-64bit/0001_9a8122a12837532c9384b95b29c7d7c3b4c1f197_20251104_234726.json @@ -0,0 +1,113 @@ +{ + "machine_info": { + "node": "Sunes-MacBook-Pro.local", + "processor": "arm", + "machine": "arm64", + "python_compiler": "Clang 19.1.7 ", + "python_implementation": "CPython", + "python_implementation_version": "3.13.7", + "python_version": "3.13.7", + "python_build": [ + "main", + "Aug 14 2025 11:12:11" + ], + "release": "24.5.0", + "system": "Darwin", + "cpu": { + "python_version": "3.13.7.final.0 (64 bit)", + "cpuinfo_version": [ + 9, + 0, + 0 + ], + "cpuinfo_version_string": "9.0.0", + "arch": "ARM_8", + "bits": 64, + "count": 12, + "arch_string_raw": "arm64", + "brand_raw": "Apple M4 Pro" + } + }, + "commit_info": { + "id": "9a8122a12837532c9384b95b29c7d7c3b4c1f197", + "time": "2025-11-05T00:47:16+01:00", + "author_time": "2025-11-05T00:47:16+01:00", + "dirty": false, + "project": "stateless", + "branch": "spike/benchmarks" + }, + "benchmarks": [ + { + "group": null, + "name": "test_effect_chain", + "fullname": "benchmarks.py::test_effect_chain", + "params": null, + "param": null, + "extra_info": {}, + "options": { + "disable_gc": false, + "timer": "perf_counter", + "min_rounds": 5, + "max_time": 1.0, + "min_time": 5e-06, + "warmup": false + }, + "stats": { + "min": 0.00010383303742855787, + "max": 0.00030441698618233204, + "mean": 0.00012335800619201154, + "stddev": 1.5886236318605e-05, + "rounds": 2092, + "median": 0.00011991651263087988, + "iqr": 1.0708055924624205e-05, + "q1": 0.00011537497630342841, + "q3": 0.00012608303222805262, + "iqr_outliers": 108, + "stddev_outliers": 156, + "outliers": "156;108", + "ld15iqr": 0.00010383303742855787, + "hd15iqr": 0.00014233402907848358, + "ops": 8106.486403837146, + "total": 0.25806494895368814, + "iterations": 1 + } + }, + { + "group": null, + "name": "test_handler_chain", + "fullname": "benchmarks.py::test_handler_chain", + "params": null, + "param": null, + "extra_info": {}, + "options": { + "disable_gc": false, + "timer": "perf_counter", + "min_rounds": 5, + "max_time": 1.0, + "min_time": 5e-06, + "warmup": false + }, + "stats": { + "min": 0.00010491604916751385, + "max": 0.0001605829456821084, + "mean": 0.00012225847126953057, + "stddev": 5.616282082634054e-06, + "rounds": 1359, + "median": 0.0001222090795636177, + "iqr": 5.624140612781048e-06, + "q1": 0.00011916691437363625, + "q3": 0.0001247910549864173, + "iqr_outliers": 73, + "stddev_outliers": 360, + "outliers": "360;73", + "ld15iqr": 0.00011079094838351011, + "hd15iqr": 0.00013337493874132633, + "ops": 8179.39231217282, + "total": 0.16614926245529205, + "iterations": 1 + } + } + ], + "datetime": "2025-11-04T23:47:27.897366+00:00", + "version": "5.2.1" +} \ No newline at end of file From 6f53efd150857cc02e9689e7ca66b05e9d936404 Mon Sep 17 00:00:00 2001 From: Sune Debel <1228354+suned@users.noreply.github.com> Date: Wed, 5 Nov 2025 06:59:39 +0100 Subject: [PATCH 3/6] something --- benchmarks.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/benchmarks.py b/benchmarks.py index d5d766b..dad2105 100644 --- a/benchmarks.py +++ b/benchmarks.py @@ -2,10 +2,22 @@ from typing import Callable from pytest_benchmark.fixture import BenchmarkFixture -from stateless import Ability, Depend, Need, Success, handle, need, run, success, supply -from stateless.errors import UnhandledAbilityError from typing_extensions import Never +from stateless import ( + Ability, + Depend, + Handler, + Need, + Success, + handle, + need, + run, + success, + supply, +) +from stateless.errors import UnhandledAbilityError + def create_effect_chain(chain_length: int) -> Callable[[], Success[None]]: """Creates a chain using functools.reduce.""" @@ -41,7 +53,7 @@ def never_handler(_: Ability) -> Never: raise UnhandledAbilityError() -test_handler = handle(never_handler) +dummy_handler: Handler[Never] = handle(never_handler) def create_handler_chain(chain_length: int) -> Callable[[], Success[str]]: @@ -52,7 +64,7 @@ def base() -> Depend[Need[str], str]: def wrap_test_handler( f: Callable[[], Depend[Need[str], str]], ) -> Callable[[], Depend[Need[str], str]]: - return test_handler(f) + return dummy_handler(f) g = reduce(lambda acc, _: wrap_test_handler(acc), range(chain_length - 1), base) From 4c0695f87703c2cf013f97e193f795953dd40c3c Mon Sep 17 00:00:00 2001 From: Sune Debel <1228354+suned@users.noreply.github.com> Date: Fri, 7 Nov 2025 21:03:03 +0100 Subject: [PATCH 4/6] add benchmarks --- benchmarks.py | 58 ++++++++++++++++----------------------------------- 1 file changed, 18 insertions(+), 40 deletions(-) diff --git a/benchmarks.py b/benchmarks.py index dad2105..77f6f2e 100644 --- a/benchmarks.py +++ b/benchmarks.py @@ -1,9 +1,9 @@ +# ruff: noqa: D100, D103 + from functools import reduce -from typing import Callable +from typing import Any, Callable from pytest_benchmark.fixture import BenchmarkFixture -from typing_extensions import Never - from stateless import ( Ability, Depend, @@ -13,53 +13,24 @@ handle, need, run, - success, supply, ) from stateless.errors import UnhandledAbilityError +from typing_extensions import Never -def create_effect_chain(chain_length: int) -> Callable[[], Success[None]]: - """Creates a chain using functools.reduce.""" - - def yield_from( - f: Callable[[], Success[None]], - ) -> Callable[[], Success[None]]: - def _() -> Success[None]: - result = yield from f() - return result - - return _ - - # Base function - def base_function() -> Success[None]: - result = yield from success(None) - return result - - # Create chain using reduce - return reduce( - lambda acc, _: yield_from(acc), range(chain_length - 1), base_function - ) - - -def test_effect_chain(benchmark: BenchmarkFixture) -> None: - """Benchmark a long chain of functions using functools.reduce.""" - - effect = create_effect_chain(500)() - benchmark(run, effect) - - -def never_handler(_: Ability) -> Never: +def never_handler(_: Ability[Any]) -> Never: raise UnhandledAbilityError() -dummy_handler: Handler[Never] = handle(never_handler) +dummy_handler: Handler[Never] = handle(never_handler) # type: ignore -def create_handler_chain(chain_length: int) -> Callable[[], Success[str]]: +def create_effect_chain(chain_length: int, n_yields: int) -> Callable[[], Success[str]]: def base() -> Depend[Need[str], str]: - s = yield from need(str) - return s + for i in range(n_yields): + yield from need(str) + return "done" def wrap_test_handler( f: Callable[[], Depend[Need[str], str]], @@ -71,7 +42,14 @@ def wrap_test_handler( return supply("")(g) +def test_effect_chain(benchmark: BenchmarkFixture) -> None: + """Benchmark a long chain of functions using functools.reduce.""" + + effect = create_effect_chain(500, 0)() + benchmark(run, effect) + + def test_handler_chain(benchmark: BenchmarkFixture) -> None: - effect = create_handler_chain(500)() + effect = create_effect_chain(chain_length=500, n_yields=500)() benchmark(run, effect) From fa1c12e815be0c9a750daaa4053bbfe9b96d6c61 Mon Sep 17 00:00:00 2001 From: Sune Debel <1228354+suned@users.noreply.github.com> Date: Fri, 7 Nov 2025 21:04:12 +0100 Subject: [PATCH 5/6] delete benchmark run --- ...4b95b29c7d7c3b4c1f197_20251104_234726.json | 113 ------------------ 1 file changed, 113 deletions(-) delete mode 100644 .benchmarks/Darwin-CPython-3.13-64bit/0001_9a8122a12837532c9384b95b29c7d7c3b4c1f197_20251104_234726.json diff --git a/.benchmarks/Darwin-CPython-3.13-64bit/0001_9a8122a12837532c9384b95b29c7d7c3b4c1f197_20251104_234726.json b/.benchmarks/Darwin-CPython-3.13-64bit/0001_9a8122a12837532c9384b95b29c7d7c3b4c1f197_20251104_234726.json deleted file mode 100644 index c0fe16c..0000000 --- a/.benchmarks/Darwin-CPython-3.13-64bit/0001_9a8122a12837532c9384b95b29c7d7c3b4c1f197_20251104_234726.json +++ /dev/null @@ -1,113 +0,0 @@ -{ - "machine_info": { - "node": "Sunes-MacBook-Pro.local", - "processor": "arm", - "machine": "arm64", - "python_compiler": "Clang 19.1.7 ", - "python_implementation": "CPython", - "python_implementation_version": "3.13.7", - "python_version": "3.13.7", - "python_build": [ - "main", - "Aug 14 2025 11:12:11" - ], - "release": "24.5.0", - "system": "Darwin", - "cpu": { - "python_version": "3.13.7.final.0 (64 bit)", - "cpuinfo_version": [ - 9, - 0, - 0 - ], - "cpuinfo_version_string": "9.0.0", - "arch": "ARM_8", - "bits": 64, - "count": 12, - "arch_string_raw": "arm64", - "brand_raw": "Apple M4 Pro" - } - }, - "commit_info": { - "id": "9a8122a12837532c9384b95b29c7d7c3b4c1f197", - "time": "2025-11-05T00:47:16+01:00", - "author_time": "2025-11-05T00:47:16+01:00", - "dirty": false, - "project": "stateless", - "branch": "spike/benchmarks" - }, - "benchmarks": [ - { - "group": null, - "name": "test_effect_chain", - "fullname": "benchmarks.py::test_effect_chain", - "params": null, - "param": null, - "extra_info": {}, - "options": { - "disable_gc": false, - "timer": "perf_counter", - "min_rounds": 5, - "max_time": 1.0, - "min_time": 5e-06, - "warmup": false - }, - "stats": { - "min": 0.00010383303742855787, - "max": 0.00030441698618233204, - "mean": 0.00012335800619201154, - "stddev": 1.5886236318605e-05, - "rounds": 2092, - "median": 0.00011991651263087988, - "iqr": 1.0708055924624205e-05, - "q1": 0.00011537497630342841, - "q3": 0.00012608303222805262, - "iqr_outliers": 108, - "stddev_outliers": 156, - "outliers": "156;108", - "ld15iqr": 0.00010383303742855787, - "hd15iqr": 0.00014233402907848358, - "ops": 8106.486403837146, - "total": 0.25806494895368814, - "iterations": 1 - } - }, - { - "group": null, - "name": "test_handler_chain", - "fullname": "benchmarks.py::test_handler_chain", - "params": null, - "param": null, - "extra_info": {}, - "options": { - "disable_gc": false, - "timer": "perf_counter", - "min_rounds": 5, - "max_time": 1.0, - "min_time": 5e-06, - "warmup": false - }, - "stats": { - "min": 0.00010491604916751385, - "max": 0.0001605829456821084, - "mean": 0.00012225847126953057, - "stddev": 5.616282082634054e-06, - "rounds": 1359, - "median": 0.0001222090795636177, - "iqr": 5.624140612781048e-06, - "q1": 0.00011916691437363625, - "q3": 0.0001247910549864173, - "iqr_outliers": 73, - "stddev_outliers": 360, - "outliers": "360;73", - "ld15iqr": 0.00011079094838351011, - "hd15iqr": 0.00013337493874132633, - "ops": 8179.39231217282, - "total": 0.16614926245529205, - "iterations": 1 - } - } - ], - "datetime": "2025-11-04T23:47:27.897366+00:00", - "version": "5.2.1" -} \ No newline at end of file From 5d32139ee524efd3049148315cd1432520b6a8b5 Mon Sep 17 00:00:00 2001 From: Sune Debel <1228354+suned@users.noreply.github.com> Date: Fri, 7 Nov 2025 21:04:49 +0100 Subject: [PATCH 6/6] remove just benchmark recipe --- justfile | 4 ---- 1 file changed, 4 deletions(-) diff --git a/justfile b/justfile index 5c7a7ec..553b4d4 100644 --- a/justfile +++ b/justfile @@ -5,7 +5,3 @@ test: lint: pre-commit run --all-files - - -benchmark: - pytest --benchmark-compare benchmarks.py