Uvicorn: Excessive CPU usage when using the reload option

Created on 19 Oct 2019  Â·  13Comments  Â·  Source: encode/uvicorn

Hi.

While developing an application, I noticed high CPU usage when using uvicorn with the reload option. I investigated further and found that the reload option uses a very simple implementation of a file change watcher that consume a lot of resources.

I propose to use the watchdog library when available for that.

I have done and initial implementation here. What do you guys think?

Most helpful comment

Presumably we'd agree that #479 is an improvement here, right?

All 13 comments

Looks fantastic, yeah!
Only taken a quick look but seems like you’ve got the right idea for the implementation.
I’d suggest we have watchdog as an optional dependency.

Thanks.

I thought about that, but then I saw that there's already an open issue for optional dependencies (#219). Should we wait for that one to be closed?

I’d go ahead with the PR in any case. Figuring out how it fits in with our package dependency choices is the easy bit here.

Been meaning to get to this (everytime I need to turn on dev reloading :) Glad to see you have done this @davidrios - I couldn't find the PR for it. Anything I can help with?

Here is an async one that may be more suitable for this. https://github.com/samuelcolvin/watchgod

Problem I've run into is that I have several services that I'm developing simultaneously where 5% cpu stacks up quickly - I've been turning reload on and off pretty regularly when I'm working on things across multiple services. Would be nice to just be able to start them all up with reload turned on.

In the meantime I found using reload-dits to be quite effective

Le ven. 8 nov. 2019 à 2:53 PM, William Hayes notifications@github.com a
écrit :

Problem I've run into is that I have several services that I'm developing
simultaneously where 5% cpu stacks up quickly - I've been turning reload on
and off pretty regularly when I'm working on things across multiple
services. Would be nice to just be able to start them all up with reload
turned on.

—
You are receiving this because you are subscribed to this thread.
Reply to this email directly, view it on GitHub
https://github.com/encode/uvicorn/issues/452?email_source=notifications&email_token=AAINSPXYWJZIO2QFWX7NXVTQSVVN3A5CNFSM4JCM6TI2YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOEDSD7NQ#issuecomment-551829430,
or unsubscribe
https://github.com/notifications/unsubscribe-auth/AAINSPU4NBLA6IHE5ILRA7TQSVVN3ANCNFSM4JCM6TIQ
.

Hi @euri10 - I can't find reload-dits using google.

I'm soryy it's a typo, I meant

  --reload-dir TEXT               Set reload directories explicitly, instead
                                  of using 'sys.path'.

set it to your code app only and it drops cpu usage significantly

Ah - thank you @euri10 - now things are running better.

I added the following line to my docker-compose.yml file:

command: ["uvicorn", "main:app", "--host", "0.0.0.0", "--debug", "--port", "80", "--reload-dir", "/app"]

and I updated https://medium.com/@williamhayes/fastapi-starlette-debug-vs-prod-5f7561db3a59 so I'll remember this in the future :)

Presumably we'd agree that #479 is an improvement here, right?

So --reload (not --reload-dirs) (desc of #479 was confusing) is set to CWD by default. Yes - that is an improvement. Thanks!

Thank you for your work @davidrios!

The high cpu issue made it impossible for us to use the --reload option even with the --reload-dirs option.

I had to find a quick temporary solution for this and wrote a standalone reloader script - will post it here to serve as a workaround option until the proper support is released. We're using UNIX domain sockets, I haven't tested this with other configurations.

import logging
import os
import threading
import time
from os.path import dirname, join
from subprocess import Popen

from watchdog.events import FileSystemEvent, PatternMatchingEventHandler
from watchdog.observers import Observer

logging.basicConfig(level=logging.INFO, format='%(asctime)s %(levelname)s [%(module)s] %(message)s ')

logger = logging.getLogger(__name__)

_PROCESS_TERMINATE_TIMEOUT = 2


class PythonFileEventHandler(PatternMatchingEventHandler):

    def __init__(self, patterns=None, ignore_patterns=None, ignore_directories=False, case_sensitive=False):
        super().__init__(patterns, ignore_patterns, ignore_directories, case_sensitive)

        self.lock = lock
        self.process = process

    def on_any_event(self, event: FileSystemEvent):
        logger.info(f"File {event.src_path} changed, reloading")

        with self.lock:
            # Terminate the previous process
            if self.process:
                self.process.terminate()
                self.process.wait(_PROCESS_TERMINATE_TIMEOUT)

            # Start a new process
            self.process = _start_process()


def _start_process():
    return Popen([
        "python", os.path.join("utils", "run_uvicorn.py")
    ])


if __name__ == "__main__":
    # NOTE: Use a lock to prevent the reload being fired rapidly in succession
    #       At least PyCharm's auto save causes two events to be fired virtually at the same time -> only one reload
    lock = threading.Lock()

    process = _start_process()

    observer = Observer()

    event_handler = PythonFileEventHandler(
        patterns=[
            "*.py",
            "*.ini"
        ]
    )

    try:
        observer.schedule(
            event_handler=event_handler,
            path=os.path.abspath(join(dirname(__file__), '..')),
            recursive=True
        )
        observer.start()

        while True:
            time.sleep(1.0)

    except KeyboardInterrupt:
        observer.stop()
        process.terminate()
        process.wait(_PROCESS_TERMINATE_TIMEOUT)

Was this page helpful?
0 / 5 - 0 ratings

Related issues

tomchristie picture tomchristie  Â·  4Comments

vikahl picture vikahl  Â·  4Comments

madkote picture madkote  Â·  7Comments

rspadim picture rspadim  Â·  5Comments

ipmb picture ipmb  Â·  7Comments