quicken¶
Make Python tools start fast.
When a quickened script is executed the first time it starts a server in the background, paying a one time cost to speed up execution for every other execution.
Quicken only speeds up applications on Linux, but transparently falls back to executing scripts directly on unsupported platforms, with minimal overhead.
Generally, an application can benefit if:
- It takes more than 100ms to start on an average machine
python -X importtime
shows that the startup time is related to module importing
To see how fast an app can be, check out the latest benchmark results in CI and interpretation in wiki here.
Usage¶
quicken
CLI¶
The quicken
command can be use to quicken plain Python scripts that look like
# script.py
...
def main():
pass
if __name__ == '__main__':
main()
Running quicken run --file script.py
followed by arguments will start the application server and
run all code before if __name__ == '__main__'
. For the first and subsequent commands, only
the code in if __name__ == '__main__'
will be executed. If the script is updated then a new
server will be started.
To see the status of the server: quicken status --file script.py
To stop the server: quicken stop --file script.py
The server is identified using the full path to the script.
Note¶
__file__
is set to the full, resolved path to the file provided to--file
, unlike Python which sets it to the path provided on the command line. This is so the code beforeif __name__ == '__main__'
and the code after it see the same path even if changing directories or the path provided to the command.
quicken.script
¶
quicken.script
can wrap console_scripts
as supported by several Python packaging tools.
The console_script
/entrypoint format is quicken.script:module.path._.function.path
.
For example, if our console script is hello=hello.cli:main
, then we would use
helloc=quicken.script:hello.cli._.main
.
Once set up, we can use helloc
just like hello
, but it should be faster after the
first time.
Since quicken
is new, it would be wise to provide a second command for testing as
above, instead of only having a quicken-based command. We use a c
suffix since
it’s a c
lient.
If using setuptools (setup.py
):
setup(
# ...
entry_points={
'console_scripts': [
'hello=hello.cli:main',
# With quicken
'helloc=quicken.script:hello.cli._.main',
],
},
# ...
)
If using poetry
[tools.poetry.scripts]
hello = "hello.cli:main"
# With quicken
helloc = "quicken.script:hello.cli._.main"
If using flit
[tools.flit.scripts]
hello = "hello.cli:main"
# With quicken
helloc = "quicken.script:hello.cli._.main"
quicken.ctl_script
¶
Similar to the above, using quicken.ctl_script
provides a CLI to stop and
check the status of a quicken server.
Setuptools example:
setup(
...
entry_points={
'console_scripts': [
'hello=hello.cli:main',
# With quicken
'helloc=quicken.script:hello.cli._.main',
# Server control command
'helloctl=quicken.ctl_script:hello.cli._.main',
],
},
...
)
Then we can use helloctl status
to see the server status information and
helloctl stop
to stop the application server.
Options¶
Quicken has several options regardless of how it is invoked:
- logging - set
QUICKEN_LOG_FILE
to an absolute file path and debug logs will be traced to it. Note that server logs will only be traced if this environment variable is set for the command that starts the server. - idle timeout - by default any quicken server will shut down after 24 hours of
inactivity. This can be changed by setting
QUICKEN_IDLE_TIMEOUT
to the desired time (in seconds). This will only take effect if this environment variable is set for the command that starts the server.
Why¶
Python command-line tools can feel slow. There are tricks that can be used to speed up startup, but implementing them in individual packages is not scalable, and can slow development. The purpose of this project is:
- provide one way to speed up app startup, with a focus on strategies that can apply across a large number of applications using normal Python development conventions
- find areas of improvement that can be folded back into Python itself
- make it easier to focus on application logic and not startup time concerns
Limitations¶
- Unix only.
- Debugging may be less obvious for end users or contributors.
- Access to the socket file implies access to the server and ability to run commands. The library tries to
mandate that the directory used for runtime files is only owned by the user, for best results use
XDG_RUNTIME_DIR
as provided bypam_systemd
or the equivalent for your distribution.
Tips¶
- Profile import time with -X importtime, see if your startup is actually the problem. If it’s not then this package will not help you.
- Ensure your package can be built as a wheel, even if it’s not distributed as
one. When wheels are installed they create scripts that do not import
pkg_resources
, which can save 60ms+ depending on disk speed and caching.
Development¶
poetry install
poetry run pytest -ra
Compatibility¶
For quicken.script
and quicken.ctl_script
, we will try to adhere to:
status
normal text output format compatibility may be changedstatus --json
output will not be changed for the"status"
key (which will have value either"up"
or"down"
)
Differences and restrictions¶
The library tries to be transparent for applications. Specifically here is the behavior you can expect:
- command-line arguments:
sys.argv
is set to the list of arguments of the client - file descriptors for stdin/stdout/stderr:
sys.stdin
,sys.stdout
, andsys.stderr
are sent from the client to the command process, any console loggers are re-initialized with the new streams - environment:
os.environ
is copied from the client to the command process - current working directory: we change directory to the cwd of the client process
- umask is set to the same as the client
The setup above is guaranteed to be done before the registered script function is invoked.
In addition:
- signals sent to the client that can be forwarded are sent to the command process
- for SIGTSTP (C-z at a terminal) and SIGTTIN, we send SIGSTOP to the command process and then stop the client process
- when continued, we attempt to send SIGCONT to the command process from the client process
- for SIGKILL, which cannot be intercepted, the server recognizes when the connection to the client is broken and will kill the command process soon after
- when the command runner exits, the client exits with the same exit code
- if a client and the server differ in group id or supplementary group ids then a new server process is started before the command is run
While the registered script function is executed in a command process, the initial import of the module is done by the first client that is executed. For that reason, there are several things that should be avoided outside of the registered script function:
- starting threads - because we fork to create the command runner process, it may cause undesirable effects if threads are created
- read configuration based on environment, arguments, or current working directory - if done when a module is imported then it will capture the values of the client that started the server
- set signal handlers - this will only be setting signal handlers for the first client starting the server and these are overridden to forward signals to the command runner process
- start sub-processes
- reading plugin information (from e.g.
pkg_resources
) - this will only be at server start time, and not when the command is actually run
Currently the following is unsupported at any point:
atexit
handlers - they will not be run at the end of the handler processsetuid
orsetgid
are currently unsupported