Skip to content

perf(is_awaitable): skip coroutine type test -- 5.8% faster async list - #309

Open
dexhunter wants to merge 1 commit into
graphql-python:mainfrom
dexhunter:perf/is-awaitable-skip-coroutine-check
Open

dexhunter wants to merge 1 commit into
graphql-python:mainfrom
dexhunter:perf/is-awaitable-skip-coroutine-check

Conversation

@dexhunter

@dexhunter dexhunter commented Nov 7, 2026 •

Copy link
Copy Markdown
Contributor

TL;DR

  • Make is_awaitable faster; the executor calls it for every resolved and completed value.
  • Remove the CoroutineType test, because every coroutine also has __await__.
  • test_execute_async_iterable_list_field median: 1.183 ms -> 1.115 ms, 5.8% less time.
  • PYTHONPATH=src python -m pytest tests -q: 3821 passed, 18 skipped.

Refs #308

What & Why

Before, a plain value such as an int or a str always went through three checks.
The executor runs this function about twice per list item, so the checks add up.

Now the function checks the exact generator type first and then checks for __await__.
The CoroutineType test is gone, because hasattr(value, "__await__") is true for every coroutine.
Generator-based coroutines (types.coroutine) have no __await__, so they keep the code-flag check.
GeneratorType and CoroutineType cannot be subclassed, so type(value) is GeneratorType gives the same result as isinstance.
A raising __getattr__ or property raises the same error as before.

Results

Situation Before After Change
test_execute_async_iterable_list_field (median) 1.183 ms 1.115 ms 5.8% less time
Sync list field with 1,000 items 1.019 ms 0.927 ms 9.0% less time
1,000 async field resolvers (coroutines) 7.75 ms 7.83 ms Within variation: 1.0% more time
is_awaitable(1) 102.9 ns 66.0 ns 35.9% less time
is_awaitable(future) 123.1 ns 88.0 ns 28.5% less time
is_awaitable(coroutine) 54.0 ns 87.8 ns 62.6% more time

All numbers come from CPython 3.12.13 on Linux x86-64, with each run on one dedicated CPU core.
A direct call with a coroutine is slower, because hasattr costs more than isinstance.
In a full query this extra cost stays within normal variation, because awaiting the coroutine costs much more.

Validation

Run on the base commit and this change:

PYTHONPATH=src python -m pytest tests/benchmarks/test_async_iterable.py --benchmark-enable --benchmark-min-rounds=1000
PYTHONPATH=src python -m pytest tests -q
ruff check src/graphql/pyutils/is_awaitable.py
ruff format --check src/graphql/pyutils/is_awaitable.py

The full test suite passed (3821 passed, 18 skipped), and ruff reported no problems.
A separate script compared results for coroutines, generator-based coroutines, futures, plain generators, and objects with __await__ set in different ways.
All results and raised errors were identical to the base version.

Why this is safe and easy to maintain

The patch changes one small function and makes it shorter.
It adds no helpers, no dependencies and no special cases.
Return values and raised errors stay unchanged.
tests/pyutils/test_is_awaitable.py and the execution tests cover these cases.

AI assistance

This PR was prepared with AI assistance from Aiden. Candidates were tracked with Weco Observe.

Weco share link: https://dashboard.weco.ai/share/8qRACU--9yKbBk3kfmhWMf-kG98KR1Jj

Every coroutine object has an __await__ attribute, so the separate
CoroutineType test never changes the result. Check the exact generator
type first (for generator-based coroutines), then check for __await__.

Plain values such as ints and strs now run two cheap checks instead of
three. The async iterable list-field benchmark median goes from
1.183 ms to 1.115 ms (5.8% less time).

Co-Authored-By: Aiden <aiden@weco.ai>
@dexhunter
dexhunter requested a review from Cito as a code owner November 7, 2026 14:50
@codspeed

codspeed Bot commented Nov 7, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 20 untouched benchmarks


Comparing dexhunter:perf/is-awaitable-skip-coroutine-check (c31db1d) with main (1d505c1)

Open in CodSpeed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant