Files
odoo_source/odoo/tools/profiler.py
T
Christophe Matthieu b04bf31e29 [FIX] odoo profiler: Calculation error for loops
The previous line in a loop calculates the number of queries made until
the end of the loop. This causes the number of queries to be counted
twice.
Don't blacklist the entry point method.
Check the methods called from a blacklisted method.
2017-08-09 14:05:07 +02:00

199 lines
6.8 KiB
Python

# -*- coding: utf-8 -*-
from decorator import decorator
import inspect
import logging
import sys
import time
import odoo
_logger = logging.getLogger(__name__)
class _LogTracer(object):
def __init__(self, whitelist=None, blacklist=None, files=None, deep=False):
self.profiles = {}
self.whitelist = whitelist
self.blacklist = blacklist
self.files = files
self.deep = deep
self.first_frame = None
def tracer(self, frame, event, arg):
if not self.first_frame:
self.first_frame = frame.f_code
if not self.deep and self.first_frame != frame.f_code:
return self.tracer
if frame.f_code.co_name in ['<genexpr>', '__getattr__', '__iter__', '__init__']:
return
if 'self' not in frame.f_locals:
return self.tracer
if self.files and frame.f_code.co_filename not in self.files:
return self.tracer
in_self = frame.f_locals['self']
if isinstance(in_self, (odoo.sql_db.Cursor, odoo.sql_db.TestCursor, odoo.sql_db.LazyCursor)):
return self.tracer
model = getattr(in_self, '_name', None)
if self.whitelist and model not in self.whitelist:
return self.tracer
if model in self.blacklist and self.first_frame != frame.f_code:
return self.tracer
if frame.f_code not in self.profiles:
try:
lines, firstline = inspect.getsourcelines(frame)
self.profiles[frame.f_code] = {
'model': model,
'filename': frame.f_code.co_filename,
'firstline': firstline,
'code': lines,
'calls': [],
'nb': 0,
}
except Exception:
return
codeProfile = self.profiles[frame.f_code]
if not frame.f_lineno:
codeProfile['nb'] += 1
cr = getattr(in_self, '_cr', None)
codeProfile['calls'].append({
'event': event,
'lineno': frame.f_lineno,
'queries': cr and cr.sql_log_count,
'time': time.time(),
'callno': codeProfile['nb'],
})
return self.tracer
def profile(method=None, whitelist=None, blacklist=(None,), files=None,
minimum_time=0, minimum_queries=0):
"""
Decorate an entry point method.
If profile is used without params, log as shallow mode else, log
all methods for all odoo models by applying the optional filters.
:param whitelist: None or list of model names to display in the log
(Default: None)
:type whitelist: list or None
:param files: None or list of filenames to display in the log
(Default: None)
:type files: list or None
:param list blacklist: list model names to remove from the log
(Default: remove non odoo model from the log: [None])
:param int minimum_time: minimum time (ms) to display a method
(Default: 0)
:param int minimum_queries: minimum sql queries to display a method
(Default: 0)
.. code-block:: python
from odoo.tools.profiler import profile
class SaleOrder(models.Model):
...
@api.model
@profile # log only this create method
def create(self, vals):
...
@api.multi
@profile() # log all methods for all odoo models
def unlink(self):
...
@profile(whitelist=['sale.order', 'ir.model.data'])
def action_quotation_send(self):
...
@profile(files=['/home/openerp/odoo/odoo/addons/sale/models/sale.py'])
def write(self):
...
NB: The use of the profiler modifies the execution time
"""
deep = not method
def _odooProfile(method, *args, **kwargs):
log_tracer = _LogTracer(whitelist=whitelist, blacklist=blacklist, files=files, deep=deep)
sys.settrace(log_tracer.tracer)
try:
result = method(*args, **kwargs)
finally:
sys.settrace(None)
log = ["\n%-10s%-10s%s\n" % ('calls', 'queries', 'ms')]
for v in log_tracer.profiles.values():
v['report'] = {}
l = len(v['calls'])
for k, call in enumerate(v['calls']):
if k+1 >= l:
continue
if call['lineno'] not in v['report']:
v['report'][call['lineno']] = {
'nb_queries': 0,
'delay': 0,
'nb': 0,
}
v['report'][call['lineno']]['nb'] += 1
n = k+1
while k+1 <= l and v['calls'][k+1]['callno'] != call['callno']:
n += 1
if n >= l:
continue
next_call = v['calls'][n]
if next_call['queries'] is not None:
v['report'][call['lineno']]['nb_queries'] += next_call['queries'] - call.get('queries', 0)
v['report'][call['lineno']]['delay'] += next_call['time'] - call['time']
queries = 0
delay = 0
for call in v['report'].values():
queries += call['nb_queries']
delay += call['delay']
if minimum_time and minimum_time > delay*1000:
continue
if minimum_queries and minimum_queries > queries:
continue
# todo: no color if output in a file
log.append("\033[1;33m%s %s--------------------- %s, %s\033[1;0m\n\n" % (v['model'] or '', '-' * (15-len(v['model'] or '')), v['filename'], v['firstline']))
for lineno, line in enumerate(v['code']):
if (lineno + v['firstline']) in v['report']:
data = v['report'][lineno + v['firstline']]
log.append("%-10s%-10s%-10s%s" % (
str(data['nb']) if 'nb_queries' in data else '.',
str(data.get('nb_queries', '')),
str(round(data['delay']*100000)/100) if 'delay' in data else '',
line[:-1]))
else:
log.append(" " * 30)
log.append(line[:-1])
log.append('\n')
log.append("\nTotal:\n%-10s%-10d%-10s\n\n" % (
str(data['nb']),
queries,
str(round(delay*100000)/100)))
_logger.info(''.join(log))
return result
if not method:
return lambda method: decorator(_odooProfile, method)
wrapper = decorator(_odooProfile, method)
return wrapper