Browse Source
Merge pull request #1465 from etobella/11.0-mig-profiler
Merge pull request #1465 from etobella/11.0-mig-profiler
[11.0] Backport of profilerpull/1569/head
Pedro M. Baeza
6 years ago
committed by
GitHub
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 798 additions and 0 deletions
-
1.travis.yml
-
33profiler/README.rst
-
3profiler/__init__.py
-
15profiler/__manifest__.py
-
59profiler/hooks.py
-
2profiler/models/__init__.py
-
487profiler/models/profiler_profile.py
-
3profiler/security/ir.model.access.csv
-
3profiler/tests/__init__.py
-
45profiler/tests/test_profiling.py
-
147profiler/views/profiler_profile_view.xml
@ -0,0 +1,33 @@ |
|||
.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg |
|||
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html |
|||
:alt: License: AGPL-3 |
|||
|
|||
============= |
|||
Odoo Profiler |
|||
============= |
|||
|
|||
Integration of python cprofile and postgresql logging collector for Odoo |
|||
Check the Profiler menu in admin menu |
|||
|
|||
Credits |
|||
======= |
|||
|
|||
Contributors |
|||
------------ |
|||
|
|||
* Moisés López <moylop260@vauxoo.com> |
|||
|
|||
Maintainer |
|||
---------- |
|||
|
|||
.. image:: https://odoo-community.org/logo.png |
|||
:alt: Odoo Community Association |
|||
:target: https://odoo-community.org |
|||
|
|||
This module is maintained by the OCA. |
|||
|
|||
OCA, or the Odoo Community Association, is a nonprofit organization whose |
|||
mission is to support the collaborative development of Odoo features and |
|||
promote its widespread use. |
|||
|
|||
To contribute to this module, please visit https://odoo-community.org. |
@ -0,0 +1,3 @@ |
|||
|
|||
from . import models |
|||
from .hooks import post_load |
@ -0,0 +1,15 @@ |
|||
{ |
|||
'name': "profiler", |
|||
'author': "Vauxoo, Odoo Community Association (OCA)", |
|||
'website': "https://github.com/OCA/server-tools/tree/12.0/profiler", |
|||
'category': 'Tests', |
|||
'version': '11.0.1.0.0', |
|||
'license': 'AGPL-3', |
|||
'depends': ["document"], |
|||
'data': [ |
|||
'security/ir.model.access.csv', |
|||
'views/profiler_profile_view.xml', |
|||
], |
|||
'post_load': 'post_load', |
|||
'installable': True, |
|||
} |
@ -0,0 +1,59 @@ |
|||
|
|||
import logging |
|||
|
|||
from odoo.http import WebRequest |
|||
from odoo import http |
|||
from odoo.sql_db import Cursor |
|||
|
|||
from .models.profiler_profile import ProfilerProfile |
|||
|
|||
_logger = logging.getLogger(__name__) |
|||
|
|||
|
|||
def patch_web_request_call_function(): |
|||
"""Modify Odoo entry points so that profile can record. |
|||
|
|||
Odoo is a multi-threaded program. Therefore, the :data:`profile` object |
|||
needs to be enabled/disabled each in each thread to capture all the |
|||
execution. |
|||
|
|||
For instance, Odoo spawns a new thread for each request. |
|||
""" |
|||
_logger.info('Patching http.WebRequest._call_function') |
|||
webreq_f_origin = WebRequest._call_function |
|||
|
|||
def webreq_f(*args, **kwargs): |
|||
with ProfilerProfile.profiling(): |
|||
return webreq_f_origin(*args, **kwargs) |
|||
WebRequest._call_function = webreq_f |
|||
|
|||
|
|||
def patch_cursor_init(): |
|||
_logger.info('Patching sql_dp.Cursor.__init__') |
|||
cursor_f_origin = Cursor.__init__ |
|||
|
|||
def init_f(self, *args, **kwargs): |
|||
cursor_f_origin(self, *args, **kwargs) |
|||
enable = ProfilerProfile.activate_deactivate_pglogs |
|||
if enable is not None: |
|||
self._obj.execute('SET log_min_duration_statement TO "%s"' % |
|||
((not enable) * -1,)) |
|||
Cursor.__init__ = init_f |
|||
|
|||
|
|||
def patch_dispatch_rpc(): |
|||
_logger.info('Patching Dispatch RPC http.dispatch_rpc') |
|||
|
|||
dispatch_rpc = http.dispatch_rpc |
|||
|
|||
def dispatch_rpc_f(service_name, method, params): |
|||
with ProfilerProfile.profiling(): |
|||
return dispatch_rpc(service_name, method, params) |
|||
|
|||
http.dispatch_rpc = dispatch_rpc_f |
|||
|
|||
|
|||
def post_load(): |
|||
patch_web_request_call_function() |
|||
patch_cursor_init() |
|||
patch_dispatch_rpc() |
@ -0,0 +1,2 @@ |
|||
|
|||
from . import profiler_profile |
@ -0,0 +1,487 @@ |
|||
|
|||
import base64 |
|||
import logging |
|||
import os |
|||
import pstats |
|||
import re |
|||
import subprocess |
|||
import sys |
|||
from contextlib import contextmanager |
|||
from cProfile import Profile |
|||
|
|||
import lxml.html |
|||
from psycopg2 import OperationalError, ProgrammingError |
|||
|
|||
from odoo import _, api, exceptions, fields, models, sql_db, tools |
|||
|
|||
if sys.version_info[0] >= 3: |
|||
from io import StringIO as IO |
|||
else: |
|||
from io import BytesIO as IO |
|||
|
|||
DATETIME_FORMAT_FILE = "%Y%m%d_%H%M%S" |
|||
CPROFILE_EMPTY_CHARS = b"{0" |
|||
PGOPTIONS = { |
|||
'log_min_duration_statement': '0', |
|||
'client_min_messages': 'notice', |
|||
'log_min_messages': 'warning', |
|||
'log_min_error_statement': 'error', |
|||
'log_duration': 'off', |
|||
'log_error_verbosity': 'verbose', |
|||
'log_lock_waits': 'on', |
|||
'log_statement': 'none', |
|||
'log_temp_files': '0', |
|||
} |
|||
PGOPTIONS_ENV = ' '.join(["-c %s=%s" % (param, value) |
|||
for param, value in PGOPTIONS.items()]) |
|||
PY_STATS_FIELDS = [ |
|||
'ncalls', |
|||
'tottime', 'tt_percall', |
|||
'cumtime', 'ct_percall', |
|||
'file', 'lineno', 'method', |
|||
] |
|||
LINE_STATS_RE = re.compile( |
|||
r'(?P<%s>\d+/?\d+|\d+)\s+(?P<%s>\d+\.?\d+)\s+(?P<%s>\d+\.?\d+)\s+' |
|||
r'(?P<%s>\d+\.?\d+)\s+(?P<%s>\d+\.?\d+)\s+(?P<%s>.*):(?P<%s>\d+)' |
|||
r'\((?P<%s>.*)\)' % tuple(PY_STATS_FIELDS)) |
|||
|
|||
_logger = logging.getLogger(__name__) |
|||
|
|||
|
|||
class ProfilerProfilePythonLine(models.Model): |
|||
_name = 'profiler.profile.python.line' |
|||
_description = 'Profiler Python Line to save cProfiling results' |
|||
_rec_name = 'cprof_fname' |
|||
_order = 'cprof_cumtime DESC' |
|||
|
|||
profile_id = fields.Many2one('profiler.profile', required=True, |
|||
ondelete='cascade') |
|||
cprof_tottime = fields.Float("Total time") |
|||
cprof_ncalls = fields.Float("Calls") |
|||
cprof_nrcalls = fields.Float("Recursive Calls") |
|||
cprof_ttpercall = fields.Float("Time per call") |
|||
cprof_cumtime = fields.Float("Cumulative time") |
|||
cprof_ctpercall = fields.Float("CT per call") |
|||
cprof_fname = fields.Char("Filename:lineno(method)") |
|||
|
|||
|
|||
class ProfilerProfile(models.Model): |
|||
_name = 'profiler.profile' |
|||
_description = 'Profiler Profile' |
|||
|
|||
@api.model |
|||
def _find_loggers_path(self): |
|||
try: |
|||
self.env.cr.execute("SHOW log_directory") |
|||
except ProgrammingError: |
|||
return |
|||
log_directory = self.env.cr.fetchone()[0] |
|||
self.env.cr.execute("SHOW log_filename") |
|||
log_filename = self.env.cr.fetchone()[0] |
|||
log_path = os.path.join(log_directory, log_filename) |
|||
if not os.path.isabs(log_path): |
|||
# It is relative path then join data_directory |
|||
self.env.cr.execute("SHOW data_directory") |
|||
data_dir = self.env.cr.fetchone()[0] |
|||
log_path = os.path.join(data_dir, log_path) |
|||
return log_path |
|||
|
|||
name = fields.Char(required=True) |
|||
enable_python = fields.Boolean(default=True) |
|||
enable_postgresql = fields.Boolean( |
|||
default=False, |
|||
help="It requires postgresql server logs seudo-enabled") |
|||
use_py_index = fields.Boolean( |
|||
name="Get cProfiling report", default=False, |
|||
help="Index human-readable cProfile attachment." |
|||
"\nTo access this report, you must open the cprofile attachment view " |
|||
"using debug mode.\nWarning: Uses more resources.") |
|||
date_started = fields.Char(readonly=True) |
|||
date_finished = fields.Char(readonly=True) |
|||
state = fields.Selection([ |
|||
('enabled', 'Enabled'), |
|||
('disabled', 'Disabled'), |
|||
], default='disabled', readonly=True, required=True) |
|||
description = fields.Text(readonly=True) |
|||
attachment_count = fields.Integer(compute="_compute_attachment_count") |
|||
pg_log_path = fields.Char(help="Getting the path to the logger", |
|||
default=_find_loggers_path) |
|||
pg_remote = fields.Char() |
|||
pg_stats_slowest_html = fields.Html( |
|||
"PostgreSQL Stats - Slowest", readonly=True) |
|||
pg_stats_time_consuming_html = fields.Html( |
|||
"PostgreSQL Stats - Time Consuming", readonly=True) |
|||
pg_stats_most_frequent_html = fields.Html( |
|||
"PostgreSQL Stats - Most Frequent", readonly=True) |
|||
py_stats_lines = fields.One2many( |
|||
"profiler.profile.python.line", "profile_id", "PY Stats Lines") |
|||
|
|||
@api.multi |
|||
def _compute_attachment_count(self): |
|||
for record in self: |
|||
self.attachment_count = self.env['ir.attachment'].search_count([ |
|||
('res_model', '=', self._name), ('res_id', '=', record.id)]) |
|||
|
|||
@api.onchange('enable_postgresql') |
|||
def onchange_enable_postgresql(self): |
|||
if not self.enable_postgresql: |
|||
return |
|||
try: |
|||
self.env.cr.execute("SHOW config_file") |
|||
except ProgrammingError: |
|||
pg_config_file = None |
|||
else: |
|||
pg_config_file = self.env.cr.fetchone()[0] |
|||
db_host = tools.config.get('db_host') |
|||
if db_host == 'localhost' or db_host == '127.0.0.1': |
|||
db_host = False |
|||
if db_host and pg_config_file: |
|||
pg_config_file = 'postgres@%s:%s' % (db_host, pg_config_file) |
|||
self.pg_remote = db_host |
|||
|
|||
self.description = ( |
|||
"You need seudo-enable logs from your " |
|||
"postgresql-server configuration file.\n\t- %s\n" |
|||
"or your can looking for the service using: " |
|||
"'ps aux | grep postgres'\n\n" |
|||
) % pg_config_file |
|||
self.description += """Adds the following parameters: |
|||
# Pre-enable logs |
|||
logging_collector=on |
|||
log_destination='stderr' |
|||
log_directory='/var/log/postgresql' |
|||
log_filename='postgresql.log' |
|||
log_rotation_age=0 |
|||
log_checkpoints=on |
|||
log_hostname=on |
|||
log_line_prefix='%t [%p]: [%l-1] db=%d,user=%u ' |
|||
log_connections=on |
|||
log_disconnections=on |
|||
lc_messages='C' |
|||
log_timezone='UTC' |
|||
|
|||
Reload configuration using the following query: |
|||
- select pg_reload_conf() |
|||
Or restart the postgresql server service. |
|||
|
|||
FYI This module will enable the following parameter from the client |
|||
It's not needed added them to configuration file if database user is |
|||
superuser or use PGOPTIONS environment variable in the terminal |
|||
that you start your odoo server. |
|||
If you don't add these parameters or PGOPTIONS this module will try do it. |
|||
# Enable logs from postgresql.conf |
|||
log_min_duration_statement=0 |
|||
client_min_messages=notice |
|||
log_min_messages=warning |
|||
log_min_error_statement=error |
|||
log_duration=off |
|||
log_error_verbosity=verbose |
|||
log_lock_waits=on |
|||
log_statement='none' |
|||
log_temp_files=0 |
|||
|
|||
# Or enable logs from PGOPTIONS environment variable before to start odoo |
|||
# server |
|||
export PGOPTIONS="-c log_min_duration_statement=0 \\ |
|||
-c client_min_messages=notice -c log_min_messages=warning \\ |
|||
-c log_min_error_statement=error -c log_connections=on \\ |
|||
-c log_disconnections=on -c log_duration=off -c log_error_verbosity=verbose \\ |
|||
-c log_lock_waits=on -c log_statement='none' -c log_temp_files=0" |
|||
~/odoo_path/odoo-bin ... |
|||
""" |
|||
|
|||
profile = Profile() |
|||
enabled = None |
|||
pglogs_enabled = None |
|||
|
|||
# True to activate it False to inactivate None to do nothing |
|||
activate_deactivate_pglogs = None |
|||
|
|||
# Params dict with values before to change it. |
|||
psql_params_original = {} |
|||
|
|||
@api.model |
|||
def now_utc(self): |
|||
self.env.cr.execute("SHOW log_timezone") |
|||
zone = self.env.cr.fetchone()[0] |
|||
self.env.cr.execute("SELECT to_char(current_timestamp AT TIME " |
|||
"ZONE %s, 'YYYY-MM-DD HH24:MI:SS')", (zone,)) |
|||
now = self.env.cr.fetchall()[0][0] |
|||
# now = fields.Datetime.to_string( |
|||
# fields.Datetime.context_timestamp(self, datetime.now())) |
|||
return now |
|||
|
|||
@api.multi |
|||
def enable(self): |
|||
self.ensure_one() |
|||
if tools.config.get('workers'): |
|||
raise exceptions.UserError( |
|||
_("Start the odoo server using the parameter '--workers=0'")) |
|||
_logger.info("Enabling profiler") |
|||
self.write(dict( |
|||
date_started=self.now_utc(), |
|||
state='enabled' |
|||
)) |
|||
ProfilerProfile.enabled = self.enable_python |
|||
self._reset_postgresql() |
|||
|
|||
@api.multi |
|||
def _reset_postgresql(self): |
|||
if not self.enable_postgresql: |
|||
return |
|||
if ProfilerProfile.pglogs_enabled: |
|||
_logger.info("Using postgresql.conf or PGOPTIONS predefined.") |
|||
return |
|||
os.environ['PGOPTIONS'] = ( |
|||
PGOPTIONS_ENV if self.state == 'enabled' else '') |
|||
self._reset_connection(self.state == 'enabled') |
|||
|
|||
def _reset_connection(self, enable): |
|||
for connection in sql_db._Pool._connections: |
|||
with connection[0].cursor() as pool_cr: |
|||
params = (PGOPTIONS if enable |
|||
else ProfilerProfile.psql_params_original) |
|||
for param, value in params.items(): |
|||
try: |
|||
pool_cr.execute('SET %s TO %s' % (param, value)) |
|||
except (OperationalError, ProgrammingError) as oe: |
|||
pool_cr.connection.rollback() |
|||
raise exceptions.UserError( |
|||
_("It's not possible change parameter.\n%s\n" |
|||
"Please, disable postgresql or re-enable it " |
|||
"in order to read the instructions") % str(oe)) |
|||
ProfilerProfile.activate_deactivate_pglogs = enable |
|||
|
|||
def get_stats_string(self, cprofile_path): |
|||
pstats_stream = IO() |
|||
pstats_obj = pstats.Stats(cprofile_path, stream=pstats_stream) |
|||
pstats_obj.sort_stats('cumulative') |
|||
pstats_obj.print_stats() |
|||
pstats_stream.seek(0) |
|||
stats_string = pstats_stream.read() |
|||
pstats_stream = None |
|||
return stats_string |
|||
|
|||
@api.multi |
|||
def dump_postgresql_logs(self, indexed=None): |
|||
self.ensure_one() |
|||
self.description = '' |
|||
pgbadger_cmd = self._get_pgbadger_command() |
|||
if pgbadger_cmd is None: |
|||
return |
|||
pgbadger_cmd_str = subprocess.list2cmdline(pgbadger_cmd) |
|||
self.description += ( |
|||
'\nRunning the command: %s') % pgbadger_cmd_str |
|||
result = tools.exec_command_pipe(*pgbadger_cmd) |
|||
datas = result[1].read() |
|||
if not datas: |
|||
self.description += "\nPgbadger output is empty!" |
|||
return |
|||
fname = self._get_attachment_name("pg_stats", ".html") |
|||
self.env['ir.attachment'].create({ |
|||
'name': fname, |
|||
'res_id': self.id, |
|||
'res_model': self._name, |
|||
'datas': base64.b64encode(datas), |
|||
'datas_fname': fname, |
|||
'description': 'pgbadger html output', |
|||
}) |
|||
xpaths = [ |
|||
'//*[@id="slowest-individual-queries"]', |
|||
'//*[@id="time-consuming-queries"]', |
|||
'//*[@id="most-frequent-queries"]', |
|||
] |
|||
# pylint: disable=unbalanced-tuple-unpacking |
|||
self.pg_stats_slowest_html, self.pg_stats_time_consuming_html, \ |
|||
self.pg_stats_most_frequent_html = self._compute_pgbadger_html( |
|||
datas, xpaths) |
|||
|
|||
@staticmethod |
|||
def _compute_pgbadger_html(html_doc, xpaths): |
|||
html = lxml.html.document_fromstring(html_doc) |
|||
result = [] |
|||
for this_xpath in xpaths: |
|||
this_result = html.xpath(this_xpath) |
|||
result.append( |
|||
tools.html_sanitize(lxml.html.tostring(this_result[0]))) |
|||
return result |
|||
|
|||
@api.multi |
|||
def _get_pgbadger_command(self): |
|||
self.ensure_one() |
|||
# TODO: Catch early the following errors. |
|||
try: |
|||
pgbadger_bin = tools.find_in_path('pgbadger') |
|||
except IOError: |
|||
self.description += ( |
|||
"\nInstall 'apt-get install pgbadger'") |
|||
return |
|||
try: |
|||
if not self.pg_log_path: |
|||
raise IOError |
|||
with open(self.pg_log_path, "r"): |
|||
pass |
|||
except IOError: |
|||
self.description += ( |
|||
"\nCheck if exists and has permission to read the log file." |
|||
"\nMaybe running: chmod 604 '%s'" |
|||
) % self.pg_log_path |
|||
return |
|||
pgbadger_cmd = [ |
|||
pgbadger_bin, '-f', 'stderr', '--sample', '15', |
|||
'-o', '-', '-x', 'html', '--quiet', |
|||
'-T', self.name, |
|||
'-d', self.env.cr.dbname, |
|||
'-b', self.date_started, |
|||
'-e', self.date_finished, |
|||
self.pg_log_path, |
|||
] |
|||
return pgbadger_cmd |
|||
|
|||
def _get_attachment_name(self, prefix, suffix): |
|||
started = fields.Datetime.from_string( |
|||
self.date_started).strftime(DATETIME_FORMAT_FILE) |
|||
finished = fields.Datetime.from_string( |
|||
self.date_finished).strftime(DATETIME_FORMAT_FILE) |
|||
fname = '%s_%d_%s_to_%s%s' % ( |
|||
prefix, self.id, started, finished, suffix) |
|||
return fname |
|||
|
|||
@api.model |
|||
def dump_stats(self): |
|||
attachment = None |
|||
with tools.osutil.tempdir() as dump_dir: |
|||
cprofile_fname = self._get_attachment_name("py_stats", ".cprofile") |
|||
cprofile_path = os.path.join(dump_dir, cprofile_fname) |
|||
_logger.info("Dumping cProfile '%s'", cprofile_path) |
|||
ProfilerProfile.profile.dump_stats(cprofile_path) |
|||
with open(cprofile_path, "rb") as f_cprofile: |
|||
datas = f_cprofile.read() |
|||
if datas and datas != CPROFILE_EMPTY_CHARS: |
|||
attachment = self.env['ir.attachment'].create({ |
|||
'name': cprofile_fname, |
|||
'res_id': self.id, |
|||
'res_model': self._name, |
|||
'datas': base64.b64encode(datas), |
|||
'datas_fname': cprofile_fname, |
|||
'description': 'cProfile dump stats', |
|||
}) |
|||
_logger.info("A datas was saved, here %s", attachment.name) |
|||
try: |
|||
if self.use_py_index: |
|||
py_stats = self.get_stats_string(cprofile_path) |
|||
self.env['profiler.profile.python.line'].search([ |
|||
('profile_id', '=', self.id)]).unlink() |
|||
for py_stat_line in py_stats.splitlines(): |
|||
py_stat_line = py_stat_line.strip('\r\n ') |
|||
py_stat_line_match = LINE_STATS_RE.match( |
|||
py_stat_line) if py_stat_line else None |
|||
if not py_stat_line_match: |
|||
continue |
|||
data = dict(( |
|||
field, py_stat_line_match.group(field)) |
|||
for field in PY_STATS_FIELDS) |
|||
data['rcalls'], data['calls'] = ( |
|||
"%(ncalls)s/%(ncalls)s" % data).split('/')[:2] |
|||
self.env['profiler.profile.python.line'].create({ |
|||
'cprof_tottime': data['tottime'], |
|||
'cprof_ncalls': data['calls'], |
|||
'cprof_nrcalls': data['rcalls'], |
|||
'cprof_ttpercall': data['tt_percall'], |
|||
'cprof_cumtime': data['cumtime'], |
|||
'cprof_ctpercall': data['ct_percall'], |
|||
'cprof_fname': ( |
|||
"%(file)s:%(lineno)s (%(method)s)" % data), |
|||
'profile_id': self.id, |
|||
}) |
|||
attachment.index_content = py_stats |
|||
except IOError: |
|||
# Fancy feature but not stop process if fails |
|||
_logger.info("There was an error while getting the stats" |
|||
"from the cprofile_path") |
|||
# pylint: disable=unnecessary-pass |
|||
pass |
|||
self.dump_postgresql_logs() |
|||
_logger.info("cProfile stats stored.") |
|||
else: |
|||
_logger.info("cProfile stats empty.") |
|||
return attachment |
|||
|
|||
@api.multi |
|||
def clear(self, reset_date=True): |
|||
self.ensure_one() |
|||
_logger.info("Clear profiler") |
|||
if reset_date: |
|||
self.date_started = self.now_utc() |
|||
ProfilerProfile.profile.clear() |
|||
|
|||
@api.multi |
|||
def disable(self): |
|||
self.ensure_one() |
|||
_logger.info("Disabling profiler") |
|||
ProfilerProfile.enabled = False |
|||
self.state = 'disabled' |
|||
self.date_finished = self.now_utc() |
|||
self.dump_stats() |
|||
self.clear(reset_date=False) |
|||
self._reset_postgresql() |
|||
|
|||
@staticmethod |
|||
@contextmanager |
|||
def profiling(): |
|||
"""Thread local profile management, according to the shared "enabled" |
|||
""" |
|||
if ProfilerProfile.enabled: |
|||
_logger.debug("Catching profiling") |
|||
ProfilerProfile.profile.enable() |
|||
try: |
|||
yield |
|||
finally: |
|||
if ProfilerProfile.enabled: |
|||
ProfilerProfile.profile.disable() |
|||
|
|||
@api.multi |
|||
def action_view_attachment(self): |
|||
attachments = self.env['ir.attachment'].search([ |
|||
('res_model', '=', self._name), ('res_id', '=', self.id)]) |
|||
action = self.env.ref("base.action_attachment").read()[0] |
|||
action['domain'] = [('id', 'in', attachments.ids)] |
|||
return action |
|||
|
|||
@api.model |
|||
def set_pgoptions_enabled(self): |
|||
"""Verify if postgresql has configured the parameters for logging""" |
|||
ProfilerProfile.pglogs_enabled = True |
|||
pgoptions_enabled = bool(os.environ.get('PGOPTIONS')) |
|||
_logger.info('Logging enabled from environment ' |
|||
'variable PGOPTIONS? %s', pgoptions_enabled) |
|||
if pgoptions_enabled: |
|||
return |
|||
pgparams_required = { |
|||
'log_min_duration_statement': '0', |
|||
} |
|||
for param, value in pgparams_required.items(): |
|||
# pylint: disable=sql-injection |
|||
self.env.cr.execute("SHOW %s" % param) |
|||
db_value = self.env.cr.fetchone()[0].lower() |
|||
if value.lower() != db_value: |
|||
ProfilerProfile.pglogs_enabled = False |
|||
break |
|||
ProfilerProfile.psql_params_original = self.get_psql_params( |
|||
self.env.cr, PGOPTIONS.keys()) |
|||
_logger.info('Logging enabled from postgresql.conf? %s', |
|||
ProfilerProfile.pglogs_enabled) |
|||
|
|||
@staticmethod |
|||
def get_psql_params(cr, params): |
|||
result = {} |
|||
for param in set(params): |
|||
# pylint: disable=sql-injection |
|||
cr.execute('SHOW %s' % param) |
|||
result.update(cr.dictfetchone()) |
|||
return result |
|||
|
|||
@api.model |
|||
def _setup_complete(self): |
|||
self.set_pgoptions_enabled() |
|||
return super(ProfilerProfile, self)._setup_complete() |
@ -0,0 +1,3 @@ |
|||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink |
|||
access_profiler_profile_admin,profiler profile admin,model_profiler_profile,base.group_system,1,1,1,1 |
|||
access_profiler_profile_line_admin,profiler profile line admin,model_profiler_profile_python_line,base.group_system,1,1,1,1 |
@ -0,0 +1,3 @@ |
|||
# Copyright 2018 Vauxoo (https://www.vauxoo.com) <info@vauxoo.com> |
|||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). |
|||
from . import test_profiling |
@ -0,0 +1,45 @@ |
|||
|
|||
# Copyright 2018 Vauxoo (https://www.vauxoo.com) <info@vauxoo.com> |
|||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). |
|||
from odoo.tests.common import HttpCase |
|||
|
|||
|
|||
class TestProfiling(HttpCase): |
|||
|
|||
def test_profile_creation(self): |
|||
"""We are testing the creation of a profile.""" |
|||
prof_obj = self.env['profiler.profile'] |
|||
profile = prof_obj.create({'name': 'this_profiler'}) |
|||
self.assertEqual(0, profile.attachment_count) |
|||
profile.enable() |
|||
self.assertFalse(self.xmlrpc_common.authenticate( |
|||
self.env.cr.dbname, 'this is not a user', |
|||
'this is not a password', {})) |
|||
profile.disable() |
|||
|
|||
def test_profile_creation_with_py(self): |
|||
"""We are testing the creation of a profile. with py index""" |
|||
prof_obj = self.env['profiler.profile'] |
|||
profile = prof_obj.create({ |
|||
'name': 'this_profiler', |
|||
'use_py_index': True, |
|||
}) |
|||
self.assertEqual(0, profile.attachment_count) |
|||
profile.enable() |
|||
self.assertFalse(self.xmlrpc_common.authenticate( |
|||
self.env.cr.dbname, 'this is not a user', |
|||
'this is not a password', {})) |
|||
profile.disable() |
|||
|
|||
def test_onchange(self): |
|||
prof_obj = self.env['profiler.profile'] |
|||
profile = prof_obj.create({'name': 'this_profiler'}) |
|||
self.assertFalse(profile.description) |
|||
profile.enable_postgresql = True |
|||
profile.onchange_enable_postgresql() |
|||
self.assertTrue(profile.description) |
|||
profile.enable() |
|||
self.assertFalse(self.xmlrpc_common.authenticate( |
|||
self.env.cr.dbname, 'this is not a user', |
|||
'this is not a password', {})) |
|||
profile.disable() |
@ -0,0 +1,147 @@ |
|||
<odoo> |
|||
<record model="ir.ui.view" id="view_profile_list"> |
|||
<field name="name">view profile list</field> |
|||
<field name="model">profiler.profile</field> |
|||
<field name="arch" type="xml"> |
|||
<tree> |
|||
<field name="name"/> |
|||
<field name="enable_python"/> |
|||
<field name="use_py_index"/> |
|||
<field name="enable_postgresql"/> |
|||
<field name="date_started"/> |
|||
<field name="date_finished"/> |
|||
<field name="state"/> |
|||
</tree> |
|||
</field> |
|||
</record> |
|||
|
|||
<record model="ir.ui.view" id="view_profiling_lines"> |
|||
<field name="name">view profiling_lines</field> |
|||
<field name="model">profiler.profile.python.line</field> |
|||
<field name="arch" type="xml"> |
|||
<tree> |
|||
<field name="cprof_ncalls"/> |
|||
<field name="cprof_nrcalls"/> |
|||
<field name="cprof_tottime"/> |
|||
<field name="cprof_ttpercall"/> |
|||
<field name="cprof_cumtime"/> |
|||
<field name="cprof_ctpercall"/> |
|||
<field name="cprof_fname"/> |
|||
</tree> |
|||
</field> |
|||
</record> |
|||
<record id="view_profiling_lines_search" model="ir.ui.view"> |
|||
<field name="name">view.profiling.lines.search</field> |
|||
<field name="model">profiler.profile.python.line</field> |
|||
<field name="arch" type="xml"> |
|||
<search string="Search Profiling lines"> |
|||
<field name="profile_id"/> |
|||
<field name="cprof_fname"/> |
|||
</search> |
|||
</field> |
|||
</record> |
|||
<record id="action_view_profiling_lines" model="ir.actions.act_window"> |
|||
<field name="name">Profiling lines</field> |
|||
<field name="res_model">profiler.profile.python.line</field> |
|||
<field name="view_type">form</field> |
|||
<field name="view_mode">tree,form</field> |
|||
<field eval="False" name="view_id"/> |
|||
<field name="domain">[]</field> |
|||
<field name="context">{}</field> |
|||
</record> |
|||
|
|||
<record model="ir.ui.view" id="view_profile_form"> |
|||
<field name="name">view profile form</field> |
|||
<field name="model">profiler.profile</field> |
|||
<field name="arch" type="xml"> |
|||
<form string="Profile"> |
|||
<header> |
|||
<button name="enable" string="Enable" type="object" |
|||
groups="base.group_system" states="disabled"/> |
|||
<button name="disable" string="Disable" type="object" |
|||
groups="base.group_system" states="enabled"/> |
|||
<button name="clear" string="Clear" type="object" |
|||
groups="base.group_system" states="enabled"/> |
|||
<field name="state" widget="statusbar" statusbar_visible="disabled,enabled"/> |
|||
</header> |
|||
<sheet> |
|||
<div class="oe_button_box" name="button_box"> |
|||
<button name="%(action_view_profiling_lines)s" |
|||
type="action" string="View profiling lines" |
|||
class="oe_stat_button" |
|||
icon="fa-share-square-o" |
|||
context="{'search_default_profile_id': active_id, 'default_profile_id': active_id}" |
|||
attrs="{'invisible': ['|', ('enable_python', '=', False), ('date_finished', '=', False)]}"> |
|||
</button> |
|||
|
|||
<button name="action_view_attachment" |
|||
type="object" |
|||
class="oe_stat_button" |
|||
icon="fa-pencil-square-o" |
|||
attrs="{'invisible': [('attachment_count', '=', 0)]}"> |
|||
<field name="attachment_count" widget="statinfo" string="Attachments"/> |
|||
</button> |
|||
</div> |
|||
<div class="oe_title"> |
|||
<h1> |
|||
<field name="name"/> |
|||
</h1> |
|||
</div> |
|||
<group> |
|||
<group> |
|||
<field name="enable_python" attrs="{'readonly': [('state','=', 'enabled')]}"/> |
|||
<field name="use_py_index"/> |
|||
</group> |
|||
<group> |
|||
<field name="enable_postgresql" attrs="{'readonly': [('state','=', 'enabled')]}"/> |
|||
<field name="pg_log_path"/> |
|||
<field name="pg_remote"/> |
|||
</group> |
|||
<group> |
|||
<field name="date_started"/> |
|||
<field name="date_finished"/> |
|||
</group> |
|||
<group colspan="4"> |
|||
<field name="description" nolabel="1"/> |
|||
</group> |
|||
</group> |
|||
<notebook> |
|||
<page string="PostgreSQL Stats - Slowest" attrs="{'invisible': ['|', ('enable_postgresql', '=', False), ('date_finished', '=', False)]}"> |
|||
<field name="pg_stats_slowest_html" nolabel="1" colspan="4"/> |
|||
</page> |
|||
<page string="PostgreSQL Stats - Time Consuming" attrs="{'invisible': ['|', ('enable_postgresql', '=', False), ('date_finished', '=', False)]}"> |
|||
<field name="pg_stats_time_consuming_html" nolabel="1" colspan="4"/> |
|||
</page> |
|||
<page string="PostgreSQL Stats - Most Frequent" attrs="{'invisible': ['|', ('enable_postgresql', '=', False), ('date_finished', '=', False)]}"> |
|||
<field name="pg_stats_most_frequent_html" nolabel="1" colspan="4"/> |
|||
</page> |
|||
<page string="Python Stats - Profiling Lines" attrs="{'invisible': ['|', ('enable_python', '=', False), ('date_finished', '=', False)]}"> |
|||
<field name="py_stats_lines" nolabel="1" colspan="4"> |
|||
<tree> |
|||
<field name="cprof_ncalls"/> |
|||
<field name="cprof_nrcalls"/> |
|||
<field name="cprof_tottime"/> |
|||
<field name="cprof_ttpercall"/> |
|||
<field name="cprof_cumtime"/> |
|||
<field name="cprof_ctpercall"/> |
|||
<field name="cprof_fname"/> |
|||
</tree> |
|||
</field> |
|||
</page> |
|||
|
|||
</notebook> |
|||
</sheet> |
|||
</form> |
|||
</field> |
|||
</record> |
|||
|
|||
<record model="ir.actions.act_window" id="profile_action_window"> |
|||
<field name="name">Profiler</field> |
|||
<field name="res_model">profiler.profile</field> |
|||
<field name="view_mode">tree,form</field> |
|||
</record> |
|||
<menuitem name="Profiler" id="menu_profiler_root" web_icon="profiler,static/description/icon.png"/> |
|||
<menuitem name="Profiler" id="menu_profiler" parent="menu_profiler_root"/> |
|||
<menuitem name="Profile" id="menu_profile" parent="menu_profiler" |
|||
action="profile_action_window"/> |
|||
</odoo> |
Write
Preview
Loading…
Cancel
Save
Reference in new issue