Skip to content

⚡️ Speed up function in_async_context by 82% - #3955

Closed
misrasaurabh1 wants to merge 1 commit into
strawberry-graphql:mainfrom
misrasaurabh1:codeflash/optimize-in_async_context-md4t2j8v
Closed

⚡️ Speed up function in_async_context by 82%#3955
misrasaurabh1 wants to merge 1 commit into
strawberry-graphql:mainfrom
misrasaurabh1:codeflash/optimize-in_async_context-md4t2j8v

Conversation

@misrasaurabh1

@misrasaurabh1 misrasaurabh1 commented Jul 23, 2025

Copy link
Copy Markdown
Contributor

Saurabh's comments - This is sometimes preferred by other users because it looks cleaner. Plus since this seems to be used in important parts of the codebase, that's why i am opening it here.

📄 82% (0.82x) speedup for in_async_context in strawberry/utils/inspect.py

⏱️ Runtime : 25.6 microseconds 14.1 microseconds (best of 32 runs)

📝 Explanation and details

Here’s an optimized version that avoids the relatively expensive try/except path and asyncio.get_running_loop() call.
Instead, it directly checks the loop using the faster asyncio._get_running_loop() "private" function, which is what get_running_loop() uses internally but without error handling overhead.

This saves a function call, exception handling, and is safe as of Python 3.7+ (including 3.12).

This is the fastest and least memory-heavy way in stock Python 3.12+.
https://github.com/python/cpython/blob/b13a5df52fc854d1097e8b5419cb8802dc4059e0/Lib/asyncio/mixins.py#L13

Correctness verification report:

Test Status
⚙️ Existing Unit Tests 3 Passed
🌀 Generated Regression Tests 16 Passed
⏪ Replay Tests 23 Passed
🔎 Concolic Coverage Tests 1 Passed
📊 Tests Coverage 100.0%
⚙️ Existing Unit Tests and Runtime
Test File::Test Function Original ⏱️ Optimized ⏱️ Speedup
codeflash_concolic_mk522t7p/tmpwowt6j5b/test_concolic_coverage.py::test_in_async_context 1.18μs 684ns ✅71.8%
test_inspect.py::test_in_async_context_sync 1.19μs 709ns ✅67.1%
test_pytest_inlinesnapshotdisable_teststest_dataloaders_py_teststest_inspect_py_teststest_deprecations_py__replay_test_0.py::test_strawberry_utils_inspect_in_async_context 3.49μs 1.77μs ✅97.5%
test_pytest_inlinesnapshotdisable_teststypestest_resolver_types_py_teststypestest_parent_type_future_anno__replay_test_0.py::test_strawberry_utils_inspect_in_async_context 9.12μs 4.70μs ✅94.0%
🌀 Generated Regression Tests and Runtime
import asyncio
import threading
import time

# imports
import pytest  # used for our unit tests
from strawberry.utils.inspect import in_async_context

# unit tests

# 1. Basic Test Cases

def test_in_async_context_basic_sync():
    """Test that in_async_context returns False in a normal synchronous context."""
    codeflash_output = in_async_context() # 1.25μs -> 670ns (86.6% faster)

@pytest.mark.asyncio
async def test_in_async_context_basic_async():
    """Test that in_async_context returns True inside an async function with a running event loop."""
    codeflash_output = in_async_context()



def test_in_async_context_no_event_loop_set_but_created():
    """Test that in_async_context returns False if an event loop is created but not running."""
    loop = asyncio.new_event_loop()
    # Do not set as current, do not run
    try:
        codeflash_output = in_async_context()
    finally:
        loop.close()

def test_in_async_context_event_loop_set_but_not_running():
    """Test that in_async_context returns False if an event loop is set but not running."""
    loop = asyncio.new_event_loop()
    asyncio.set_event_loop(loop)
    try:
        codeflash_output = in_async_context()
    finally:
        asyncio.set_event_loop(None)
        loop.close()





def test_in_async_context_after_event_loop_closed():
    """Test that in_async_context returns False after the event loop is closed."""
    loop = asyncio.new_event_loop()
    asyncio.set_event_loop(loop)
    loop.close()
    asyncio.set_event_loop(None)
    codeflash_output = in_async_context() # 2.22μs -> 988ns (124% faster)

# 3. Large Scale Test Cases

@pytest.mark.asyncio
async def test_in_async_context_many_concurrent_tasks():
    """Test that in_async_context returns True in many concurrent async tasks."""
    results = []
    async def worker(idx):
        # Each worker should see the event loop is running
        results.append((idx, in_async_context()))
    await asyncio.gather(*(worker(i) for i in range(500)))




import asyncio
import concurrent.futures
import threading

# imports
import pytest  # used for our unit tests
from strawberry.utils.inspect import in_async_context

# ---------------------------
# unit tests for in_async_context
# ---------------------------

# 1. Basic Test Cases

def test_in_sync_context_returns_false():
    """Test that in_async_context returns False in a normal synchronous context."""
    codeflash_output = in_async_context() # 1.07μs -> 578ns (85.3% faster)

@pytest.mark.asyncio
async def test_in_async_context_returns_true():
    """Test that in_async_context returns True inside an async function."""
    codeflash_output = in_async_context()





def test_in_main_thread_with_stopped_loop():
    """Test that in_async_context returns False after the loop is closed."""
    loop = asyncio.new_event_loop()
    asyncio.set_event_loop(loop)
    loop.close()
    asyncio.set_event_loop(None)
    codeflash_output = in_async_context() # 1.55μs -> 1.02μs (52.3% faster)


def test_in_async_with_context_manager():
    """Test that in_async_context returns True inside an async context manager."""
    class DummyAsyncCM:
        async def __aenter__(self):
            return in_async_context()
        async def __aexit__(self, exc_type, exc, tb):
            return False
    async def runner():
        async with DummyAsyncCM() as val:
            return val





from strawberry.utils.inspect import in_async_context

def test_in_async_context():
    in_async_context()
⏪ Replay Tests and Runtime
Test File::Test Function Original ⏱️ Optimized ⏱️ Speedup
codeflash_concolic_mk522t7p/tmpwowt6j5b/test_concolic_coverage.py::test_in_async_context 1.18μs 684ns ✅71.8%
test_inspect.py::test_in_async_context_sync 1.19μs 709ns ✅67.1%
test_pytest_inlinesnapshotdisable_teststest_dataloaders_py_teststest_inspect_py_teststest_deprecations_py__replay_test_0.py::test_strawberry_utils_inspect_in_async_context 3.49μs 1.77μs ✅97.5%
test_pytest_inlinesnapshotdisable_teststypestest_resolver_types_py_teststypestest_parent_type_future_anno__replay_test_0.py::test_strawberry_utils_inspect_in_async_context 9.12μs 4.70μs ✅94.0%

To edit these changes git checkout codeflash/optimize-in_async_context-md4t2j8v and push.

Codeflash

Summary by Sourcery

Enhancements:

  • Optimize in_async_context to use asyncio._get_running_loop for faster detection of a running event loop, eliminating exception overhead and achieving an ~82% speedup

Here’s an optimized version that avoids the relatively expensive try/except path and `asyncio.get_running_loop()` call.  
Instead, it directly checks the loop using the faster `asyncio._get_running_loop()` "private" function, which is what `get_running_loop()` uses internally but without error handling overhead.

This saves a function call, exception handling, and is safe as of Python 3.7+ (including 3.12).



This is the fastest and least memory-heavy way in stock Python 3.12+.  
(Django does this in their async context checks as well:  
https://github.com/django/django/blob/main/django/utils/asyncio.py)
@sourcery-ai

sourcery-ai Bot commented Jul 23, 2025

Copy link
Copy Markdown
Contributor

Reviewer's Guide

The PR replaces the slower try/except-based check in in_async_context with a direct call to asyncio._get_running_loop(), removing exception handling overhead and achieving an 82% speedup.

File-Level Changes

Change Details Files
Use private loop retrieval for async context check
  • Updated function doc comment to reference private API usage
  • Removed try/except block around asyncio.get_running_loop()
  • Replaced logic with direct return of asyncio._get_running_loop() is not None
strawberry/utils/inspect.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hey @misrasaurabh1 - I've reviewed your changes and they look great!


Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@botberry

Copy link
Copy Markdown
Member

Hi, thanks for contributing to Strawberry 🍓!

We noticed that this PR is missing a RELEASE.md file. We use that to automatically do releases here on GitHub and, most importantly, to PyPI!

So as soon as this PR is merged, a release will be made 🚀.

Here's an example of RELEASE.md:

Release type: patch

Description of the changes, ideally with some examples, if adding a new feature.

Release type can be one of patch, minor or major. We use semver, so make sure to pick the appropriate type. If in doubt feel free to ask :)

Here's the tweet text:

🆕 Release (next) is out! Thanks to Saurabh Misra for the PR 👏

Get it here 👉 https://strawberry.rocks/release/(next)

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Greptile Summary

This PR optimizes the in_async_context() function in strawberry/utils/inspect.py to achieve an 82% performance improvement. The change replaces the original try/except pattern that used asyncio.get_running_loop() with a direct call to the private asyncio._get_running_loop() API.

The original implementation attempted to get the running event loop and caught RuntimeError exceptions when no loop was present, returning False in that case. The optimized version directly calls asyncio._get_running_loop() and checks if the result is None, which is functionally equivalent but avoids the overhead of exception handling and an additional function call.

This function is used throughout the Strawberry GraphQL codebase to determine whether code is executing in an asynchronous context, which is critical for proper handling of async resolvers, dataloaders, and other async operations. The performance improvement is particularly valuable since this utility is likely called frequently during GraphQL query execution.

The change follows a pattern used by Django's async utilities, suggesting this is a recognized optimization technique in the Python ecosystem for performance-sensitive async context detection.

Confidence score: 3/5

  • This PR is moderately safe to merge but has some risk due to reliance on private APIs
  • The score reflects the trade-off between significant performance gains and potential future compatibility issues with the private _get_running_loop() API
  • Files that need more attention: strawberry/utils/inspect.py - the implementation relies on a private asyncio API that could change in future Python versions

1 file reviewed, no comments

Edit Code Review Bot Settings | Greptile

@codecov

codecov Bot commented Jul 23, 2025

Copy link
Copy Markdown

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 94.41%. Comparing base (10a381d) to head (9734e8b).
Report is 16 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3955      +/-   ##
==========================================
- Coverage   94.75%   94.41%   -0.35%     
==========================================
  Files         520      528       +8     
  Lines       33947    34338     +391     
  Branches     1759     1803      +44     
==========================================
+ Hits        32168    32419     +251     
- Misses       1497     1627     +130     
- Partials      282      292      +10     
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@codspeed-hq

codspeed-hq Bot commented Jul 23, 2025

Copy link
Copy Markdown

CodSpeed Performance Report

Merging #3955 will not alter performance

Comparing misrasaurabh1:codeflash/optimize-in_async_context-md4t2j8v (9734e8b) with main (d79dd55)

Summary

✅ 26 untouched benchmarks

return False
else:
return True
return asyncio._get_running_loop() is not None

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@bellini666 bellini666 Jul 23, 2025

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Hrmm interesting..

Yeah, apparently get_running_loop uses that and raises RuntimeError if it returns None

This change would prevent the if loop is None from there, which makes us have to except RuntimeError, which, at least until Python 3.11, had some overhead (3.11 introduced zero-cost exceptions)

My only concern here is to depend on a private member that could change anytime and cause issues without us knowing.

And checking the difference in times, before it was taking 0,00119 ms, with this change, it takes 0,000709 ms. Yes, it is a 67% performance improvement 🤣, but at the same time negligible, considering we don't call this that much

Sorry codeflash, I don't think this is a good change 😅

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

yup, let's close it /cc @misrasaurabh1

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

yep the speedup was tiny but thought that this might be on the hot path, so thought might be good to merge. I plan to sync with @patrick91 about the types of optimizations that matters the most to your project

@patrick91 patrick91 closed this Jul 23, 2025
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.

4 participants