Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cythonize the call loop #263

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ jobs:

install:
- pip install -U pip
- pip install -U --force-reinstall setuptools tox
- pip install -U --force-reinstall setuptools tox cython

script:
- tox
Expand Down
2 changes: 2 additions & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@ include tox.ini
include LICENSE
graft testing
recursive-exclude * *.pyc *.pyo
include src/pluggy/callers/cythonized.pyx
include src/pluggy/callers/cythonized.c
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ requires = [
"setuptools",
"setuptools-scm",
"wheel",
"Cython",
]

[tool.towncrier]
Expand Down
35 changes: 34 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
from setuptools import setup
from setuptools.command.sdist import sdist as _sdist
from setuptools.extension import Extension


classifiers = [
"Development Status :: 4 - Beta",
Expand Down Expand Up @@ -29,6 +32,34 @@
}


cmdclass = {}


class sdist(_sdist):
"""Custom sdist building using cython
"""

def run(self):
# Make sure the compiled Cython files in the distribution
# are up-to-date
from Cython.Build import cythonize

cythonize(["src/pluggy/callers/cythonized.pyx"])
_sdist.run(self)


try:
from Cython.Build import cythonize

print("Building Cython extension(s)")
exts = cythonize(["src/pluggy/callers/cythonized.pyx"])
cmdclass["sdist"] = sdist
except ImportError:
# When Cython is not installed build from C sources
print("Building C extension(s)")
exts = [Extension("pluggy.callers.cythonized", ["src/pluggy/callers/cythonized.c"])]


def main():
setup(
name="pluggy",
Expand All @@ -45,8 +76,10 @@ def main():
install_requires=['importlib-metadata>=0.12;python_version<"3.8"'],
extras_require=EXTRAS_REQUIRE,
classifiers=classifiers,
packages=["pluggy"],
packages=["pluggy", "pluggy.callers"],
package_dir={"": "src"},
ext_modules=exts,
cmdclass=cmdclass,
)


Expand Down
5 changes: 4 additions & 1 deletion src/pluggy/callers.py → src/pluggy/callers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@
"""
import sys

from ._result import HookCallError, _Result, _raise_wrapfail
from .._result import HookCallError, _Result, _raise_wrapfail
from .cythonized import _c_multicall

__all__ = ["_multicall", "_c_multicall"]


def _multicall(hook_impls, caller_kwargs, firstresult=False):
Expand Down
64 changes: 64 additions & 0 deletions src/pluggy/callers/cythonized.pyx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
"""
Cynthonized hook call loop.

This is currently maintained as a verbatim copy of
``pluggy.callers._multicall()``.

NOTE: In order to build this source you must have cython installed.
"""
import sys

from .._result import _Result, _raise_wrapfail, HookCallError


cpdef _c_multicall(list hook_impls, dict caller_kwargs, bint firstresult=False):
"""Execute a call into multiple python functions/methods and return the
result(s).

``caller_kwargs`` comes from _HookCaller.__call__().
"""
__tracebackhide__ = True
results = []
excinfo = None
try: # run impl and wrapper setup functions in a loop
teardowns = []
try:
for hook_impl in reversed(hook_impls):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i wonder if there is a way to line up implementations better so we cna do less work and less work on calls,

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@RonnyPfannschmidt oh there definitely is. I actually have some ideas but it's going to require some rejigging of how implementations are stored/managed in the manger iirc and I think this may have something to do with the way we couple tracing to that management (see #262 and #217). I also think we should let @bluetech play with it in the context of pytest before we get too ahead of ourselves - micro-benchmarks shouldn't be our guiding light IMO.

I also thought about this a while back with ideas for the rework of internals.. let's see if I can find some notes.

@RonnyPfannschmidt another question I have is how to implement both of these implementations (cython and python) without too much duplicate work.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@goodboy as cython has a python mode, where a annotated file is next to the implementation file, i beleive we can do it better these days (by implementing it in python, and then cythonizing those files

Copy link
Contributor Author

@goodboy goodboy Aug 9, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So then it is as simple as running cython on the original code?
Haven't looked at cython in ages.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

im going to sprint on this next week, i hope to get a sync with simon from datasette about async use of pluggy before so we can enable that as well

try:
args = [caller_kwargs[argname] for argname in hook_impl.argnames]
except KeyError:
for argname in hook_impl.argnames:
if argname not in caller_kwargs:
raise HookCallError(
"hook call must provide argument %r" % (argname,))

if hook_impl.hookwrapper:
try:
gen = hook_impl.function(*args)
next(gen) # first yield
teardowns.append(gen)
except StopIteration:
_raise_wrapfail(gen, "did not yield")
else:
res = hook_impl.function(*args)
if res is not None:
results.append(res)
if firstresult: # halt further impl calls
break
except BaseException:
excinfo = sys.exc_info()
finally:
if firstresult: # first result hooks return a single value
outcome = _Result(results[0] if results else None, excinfo)
else:
outcome = _Result(results, excinfo)

# run all wrapper post-yield blocks
for gen in reversed(teardowns):
try:
gen.send(outcome)
_raise_wrapfail(gen, "has second yield")
except StopIteration:
pass

return outcome.get_result()
19 changes: 12 additions & 7 deletions testing/benchmark.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,18 @@
import pytest
from pluggy import HookspecMarker, HookimplMarker
from pluggy.hooks import HookImpl
from pluggy.callers import _multicall
from pluggy.callers import _multicall, _c_multicall

hookspec = HookspecMarker("example")
hookimpl = HookimplMarker("example")


def MC(methods, kwargs, firstresult=False):
def MC(methods, kwargs, callertype, firstresult=False):
hookfuncs = []
for method in methods:
f = HookImpl(None, "<temp>", method, method.example_impl)
hookfuncs.append(f)
return _multicall(hookfuncs, kwargs, firstresult=firstresult)
return callertype(hookfuncs, kwargs, firstresult=firstresult)


@hookimpl
Expand All @@ -38,9 +38,14 @@ def wrappers(request):
return [wrapper for i in range(request.param)]


def inner_exec(methods):
return MC(methods, {"arg1": 1, "arg2": 2, "arg3": 3})
@pytest.fixture(params=[_multicall, _c_multicall], ids=lambda item: item.__name__)
def callertype(request):
return request.param


def test_hook_and_wrappers_speed(benchmark, hooks, wrappers):
benchmark(inner_exec, hooks + wrappers)
def inner_exec(methods, callertype):
return MC(methods, {"arg1": 1, "arg2": 2, "arg3": 3}, callertype)


def test_hook_and_wrappers_speed(benchmark, hooks, wrappers, callertype):
benchmark(inner_exec, hooks + wrappers, callertype)
1 change: 1 addition & 0 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ commands=pytest {posargs:testing/benchmark.py}
deps=
pytest
pytest-benchmark
cython

[testenv:linting]
skip_install = true
Expand Down