Skip to content

Commit

Permalink
Merge pull request #26 from oremanj/reorg
Browse files Browse the repository at this point in the history
Change how we locate the parent greenlet + perf improvements
  • Loading branch information
oremanj authored Feb 8, 2024
2 parents 134a865 + e7134b7 commit 3945b6e
Show file tree
Hide file tree
Showing 14 changed files with 350 additions and 198 deletions.
18 changes: 9 additions & 9 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@ jobs:
strategy:
fail-fast: false
matrix:
python: ['3.7', '3.8', '3.9', '3.10', '3.11']
python: ['3.8', '3.9', '3.10', '3.11', '3.12']
arch: ['x86', 'x64']
extra_name: ['']
old_greenlet: ['0']
include:
- python: '3.7'
- python: '3.8'
arch: 'x86'
old_greenlet: '1'
extra_name: ', older greenlet package'
Expand Down Expand Up @@ -58,23 +58,23 @@ jobs:
strategy:
fail-fast: false
matrix:
python: ['pypy-3.8', 'pypy-3.9', '3.7', '3.8', '3.9', '3.10', '3.11', '3.10-dev', '3.11-dev']
python: ['pypy-3.9', '3.8', '3.9', '3.10', '3.11', '3.12']
check_lint: ['0']
extra_name: ['']
old_greenlet: ['0']
include:
- python: '3.9'
- python: '3.12'
check_lint: '1'
extra_name: ', formatting and linting'
- python: '3.7'
- python: '3.8'
old_greenlet: '1'
extra_name: ', older greenlet package'
- python: '3.9' # NB: old greenlet not supported on >=3.10
old_greenlet: '1'
extra_name: ', older greenlet package'
- python: '3.9' # <- not actually used
pypy_nightly_branch: 'py3.9'
extra_name: ', pypy 3.9 nightly'
- python: '3.10' # <- not actually used
pypy_nightly_branch: 'py3.10'
extra_name: ', pypy 3.10 nightly'
steps:
- name: Checkout
uses: actions/checkout@v2
Expand Down Expand Up @@ -104,7 +104,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python: ['3.7', '3.8', '3.9', '3.10', '3.11']
python: ['3.8', '3.9', '3.10', '3.11', '3.12']
steps:
- name: Checkout
uses: actions/checkout@v2
Expand Down
22 changes: 0 additions & 22 deletions .travis.yml

This file was deleted.

29 changes: 19 additions & 10 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ below. This is potentially useful in a number of different situations:
* You can (cautiously) design async APIs that block in places where
you can't write ``await``, such as on attribute accesses.

``greenback`` requires Python 3.6 or later and an implementation that
``greenback`` requires Python 3.8 or later and an implementation that
supports the ``greenlet`` library. Either CPython or PyPy should work.
There are no known OS dependencies.

Expand All @@ -78,6 +78,15 @@ Quickstart
* Later, use ``greenback.await_(foo())`` as a replacement for
``await foo()`` in places where you can't write ``await``.

* If all of the places where you want to use
``greenback.await_()`` are indirectly within a single function, you can
eschew the ``await greenback.ensure_portal()`` and instead write a wrapper
around calls to that function: ``await greenback.with_portal_run(...)``
for an async function, or ``await greenback.with_portal_run_sync(...)``
for a synchronous function. These have the advantage of cleaning up the
portal (and its associated minor performance impact) as soon as the
function returns, rather than leaving it open until the task terminates.

* For more details and additional helper methods, see the
`documentation <https://greenback.readthedocs.io>`__.

Expand Down Expand Up @@ -141,25 +150,25 @@ FAQ
something.

**How does it work?** After you run ``await greenback.ensure_portal()``
in a certain task, each step of that task will run inside a greenlet.
in a certain task, that task will run inside a greenlet.
(This is achieved by interposing a "shim" coroutine in between the event
loop and the coroutine for your task; see the source code for details.)
Calls to ``greenback.await_()`` are then able to switch from that greenlet
back to the parent greenlet, which can easily perform the necessary
``await`` since it has direct access to the async environment. The
per-task-step greenlet is then resumed with the value or exception
task greenlet is then resumed with the value or exception
produced by the ``await``.

**Should I trust this in production?** Maybe; try it and see. The
technique is in some ways an awful hack, and has some performance
implications (any task in which you call ``await
greenback.ensure_portal()`` will run somewhat slower), but we're in
technique is rather low-level, and has some minor
`performance implications <https://greenback.readthedocs.io/en/latest/principle.html#performance>`__ (any task in which you call ``await
greenback.ensure_portal()`` will run a bit slower), but we're in
good company: SQLAlchemy's async ORM support is implemented in much
the same way. ``greenback`` itself is a fairly small amount of
pure-Python code on top of ``greenlet``. (There is one reasonably
safe ctypes hack that is necessary to work around a knob that's not
exposed by the asyncio acceleration extension module on CPython.)
``greenlet`` is a C module full of arcane platform-specific hacks, but
pure-Python code on top of ``greenlet``. (There is one small usage of
``ctypes`` to work around a knob that's not exposed by the asyncio
acceleration extension module on CPython.)
``greenlet`` is a C module full of platform-specific arcana, but
it's been around for a very long time and popular production-quality
concurrency systems such as ``gevent`` rely heavily on it.

Expand Down
12 changes: 11 additions & 1 deletion docs/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ below. This is potentially useful in a number of different situations:
* You can (cautiously) design async APIs that block in places where
you can't write ``await``, such as on attribute accesses.

``greenback`` requires Python 3.6 or later and an implementation that
``greenback`` requires Python 3.8 or later and an implementation that
supports the ``greenlet`` library. Either CPython or PyPy should work.
There are no known OS dependencies.

Expand All @@ -61,6 +61,16 @@ Quickstart
* Later, use ``greenback.await_(foo())`` as a replacement for
``await foo()`` in places where you can't write ``await``.

* If all of the places where you want to use
``greenback.await_()`` are indirectly within a single function, you can
eschew the ``await greenback.ensure_portal()`` and instead write a wrapper
around calls to that function: ``await greenback.with_portal_run(...)``
for an async function, or ``await greenback.with_portal_run_sync(...)``
for a synchronous function. These have the advantage of cleaning up the
portal (and its associated minor :ref:`performance impact <performance>`)
as soon as the function returns, rather than leaving it open until the task
terminates.

* For more details and additional helpers, read the rest of this documentation!

Detailed documentation
Expand Down
26 changes: 12 additions & 14 deletions docs/source/principle.rst
Original file line number Diff line number Diff line change
Expand Up @@ -187,30 +187,28 @@ The slowdown due to `greenback` is mostly proportional to the
number of times you yield to the event loop with a portal active, as well
as the number of portal creations and :func:`await_` calls you perform.
You can run the ``microbenchmark.py`` script from the Git repository
to see the numbers on your machine. On a 2020 MacBook Pro (x86), with
CPython 3.9, greenlet 1.1.2, and Trio 0.19.0, I get:
to see the numbers on your machine. On a 2023 MacBook Pro (ARM64), with
CPython 3.12, greenlet 3.0.3, and Trio 0.24.0, I get:

* Baseline: The simplest possible async operation is what Trio calls a
*checkpoint*: yield to the event loop and ask to immediately be
rescheduled again. This takes about **31.5 microseconds** on Trio and
**28 microseconds** on asyncio. (asyncio is able to take advantage
rescheduled again. This takes about **13.6 microseconds** on Trio and
**12.9 microseconds** on asyncio. (asyncio is able to take advantage
of some C acceleration here.)

* Adding the greenback portal, without making any :func:`await_` calls
yet, adds about **4 microseconds** per checkpoint.
yet, adds about **1 microsecond** per checkpoint.

* Executing each of those checkpoints through a separate
:func:`await_` adds another **10 microseconds** per :func:`await_` on
Trio, or **8 microseconds** on asyncio. (Surrounding
the entire checkpoint loop in a single :func:`await_`, by contrast,
has negligible impact.)
:func:`await_` adds about another **2 microseconds** per :func:`await_`.
(Surrounding the entire checkpoint loop in a single :func:`await_`, by
contrast, has negligible impact.)

* Creating a new portal for each of those ``await_(checkpoint())``
invocations adds another **12 microseconds** or so per portal
creation. If you don't execute any checkpoints while the portal is
active, you can create and destroy it in more like **5
microseconds**. If you use :func:`with_portal_run_sync`, portal
creation gets about **3 microseconds** faster.
invocations adds another **16 microseconds** or so per portal
creation. If you use :func:`with_portal_run_sync`, portal
creation gets about **10 microseconds** faster (so the portal is only
adding about 6 microseconds of overhead).

Keep in mind that these are microbenchmarks: your actual program is
probably not executing checkpoints in a tight loop! The more work
Expand Down
Loading

0 comments on commit 3945b6e

Please sign in to comment.