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

Better support for sync routes in FastAPI #257

Open
joerick opened this issue Jul 22, 2023 · 3 comments
Open

Better support for sync routes in FastAPI #257

joerick opened this issue Jul 22, 2023 · 3 comments

Comments

@joerick
Copy link
Owner

joerick commented Jul 22, 2023

FastAPI profiling is complicated by the fact that-

If you use def instead of async def, FastAPI runs the request asynchronously on a separate default execution thread to not block IO. So it essentially runs everything async for you under the hood whether you want it or not. Adding async def to the app signals FastAPI to not use that separate default execution thread, which ironically WILL block IO unless the application uses await somewhere down line to free up the thread for handling another request.

Originally posted by @coneybeare in #219 (comment)

This means that non-async routes in FastAPI don't show up in the profile output. Async routes work fine. Perhaps there's a way we could create a first-party middleware for this? I'm not a FastAPI user, so PRs would be welcome.

@spolloni
Copy link

spolloni commented Dec 6, 2023

Perhaps there's a way we could create a first-party middleware for this?

would really love to see this

@lambyqq
Copy link

lambyqq commented Feb 28, 2024

Hi~ IMO a broader request is to support tracking "sync-to-async" function execution in general (not limited to sync routes), which would be tremendously useful to profile FastAPI services that use a mix of asyncio/coroutines and thread(s)/threadpool(s) to execute synchronous IO functions.
Currently, when async code executes a sync function in a separate thread/threadpool:

def blocking_io_fn(param):
   # do some IO work

async def api_route():
   ...
   await asyncio.to_thread(blocking_io_fn, arg)
   ...
   # or we can execute concurrent blocking operations in threadpool
   corotines = [
      asyncio.get_event_loop().run_in_executor(None, blocking_io_fn, i) for i in range(5)
   ]
   await asyncio.gather(*corotines)

the profiling result completely loses any info about blocking_io_fn, the result would contain something like [await] or epoll.poll which is not useful at all...

@lambyqq
Copy link

lambyqq commented Feb 28, 2024

here's a dummy example (I'm using asgiref.sync.SyncToAsync as convenient helper to execute sync func in threadpool while propagating context, noticing that loop.run_in_executor() does not propagate context by default):

from asgiref.sync import SyncToAsync

async def api_route():
        def _dummy_fn():
            sleep(random.uniform(1.0, 2.0))

        await asyncio.gather(
            *[SyncToAsync(_dummy_fn, thread_sensitive=False)() for _ in range(8)])

Then profiling result looks like:
image
which isn't useful at all (except that it indicates we are gathering & awaiting 8 futures), but it doesn't show any information about _dummy_fn

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

No branches or pull requests

3 participants