During daily development of ODOO8 modules, one thing I have been noticing is the slowness when ODOO first boot up. We are observing up to minutes of a bootup time. This plainly is too slow. Watching debug prints indicates that ODOO scans through all modules listed on Python path and builds a map in memory before dev server starts responding to request. Intuitively I suspected such loading practice is causing an unnecessary delay. This aggravates further when ODOO server becomes completly unusable if any of the module chokes during this loading process. But this is too common a scenario during development. One consequence of this is that debugging becomes difficult. Often enough ODOO doesn't yield useful trace track to help determine the root cause of an error. Another problem of such approach is the availability of servic becoming binary — it either fully works or nothing runs. By analogy, the ODOO loading process is like compiling phase in running a C code. No executable is available until the compiler and linker are happy.
Under this light, this article analyzes this booting process with a hope to understand why ODOO takes such an approach and how we could improve or circumvent during development in order to gain efficiency. Considering a developer lives with this bootup process as a daily need, it becomes interesting how we can reduce this bootup time.
Tracing through ODOO boot code, we have illustrated steps of ODOO's loading process in diagram below:
Why does ODOO call
tools.config.parse_config twice? The function
fullfills the same purpose by parsing the configuration file and
builds the in memory
tools.config global variable. This is redundant.
The meat of this loading is to boot up a HTTP server. ODOO offers three types of servers:
But which one to use? If configuration option
non zero, it will pick the
PreforkServer; otherwise, it takes
if openerp.evented: server = GeventServer(openerp.service.wsgi_server.application) elif config['workers']: server = PreforkServer(openerp.service.wsgi_server.application) else: server = ThreadedServer(openerp.service.wsgi_server.application)
ThreadedServer runs in a single thread, where
select to handle multiprocessing. This choice apparently makes the
prefork server more appealing for production site. ODOO's own manual
on deployment also confirms this:
Odoo includes built-in HTTP servers, using either multithreading or multiprocessing. For production use, it is recommended to use the multiprocessing server as it increases stability, makes somewhat better use of computing resources and can be better monitored and resource-restricted. * Multiprocessing is enabled by configuring a non-zero number of worker processes, the number of workers should be based on the number of cores in the machine (possibly with some room for cron workers depending on how much cron work is predicted) * Worker limits can be configured based on the hardware configuration to avoid resources exhaustion
How good is ODOO's default server then? Diagram below illustrates the truth of an ODOO threaded server. Under layers of function call, ODOO's threaded server is simply a Python native HTTPServer instance. So the question boils down to how good Python's native http server is comparing to some well known names — nginx, Apache, lighthttpd, or even further, WSGI servers such as Guicorn and uwsgi.
Runtime call trace
Diagram below illustrates a full call trace while ODOO8 boots up. This was generated by Pycallgraph.
pycallgraph -v -d -s -i openerp.\* -e openerp.report.\* -e openerp.tools.config.configmanager.\* -e openerp.loglevels.\* -e openerp.tools.\* --max-depth=10 graphviz --output-file=odoo8.png -- odoo.py -c ../works/contract/8290.conf
The decision to exclude these patters, in particular, the
openerp.report.* is purely out of layout consideration — it
clutters the diagram too much. This, on the other hand, could be
considered as a clue why bootup is slow.
Note that no tool can do static analysis of code and generate a call stack. The only sure way to create this is to run the code and monitor its runtime stack, in which case language such as Python shines due its interpreting nature. The Python GIL will keep track the entire call trace in its memory and knows details of call stack.
After examining ODOO code and its run time, two areas I would recommend ODOO to reconsider its approach:
- how configuration options are being shared among modules
- using ThreadedServer as default
First concern coming out of this analysis is how ODOO handles
values in the configuration file. In
openerp.tools ODOO defined
thus will expose the config to the caller. This essentially makes the configuration object globally. I don't know whether I should call this a trick or a bad practice. So in short, ODOO's configuration is global.
The proper way to handle this is to either set it up as
making caller to parse the disk conf file if it needs information.
The former will build an in memory image. As long as it is in
read-only mode, no locking mechanism is needed. The downside is that
disck changes won't necessarily update the in memory object. The
latter is better, though more expansive since disk file is accessed
My suspecion is that native server is suitable for development environment, but lacks features to be production ready. I'm considering features such as plugin loading (think Apache2), configuration, security, HTTP protocol support, SSL...?
In short, ODOO's
ThreadedServer works fine as development tool,
but should not be used for production deployment unless, well,
the client is the least sophisticated (oh boy there are so many
of them that it is actually creating a whole nine yard
of different problems than this one) and creates minimal load
— by Feng Xia