Browse Source

Merge pull request #1465 from etobella/11.0-mig-profiler

[11.0] Backport of profiler
pull/1569/head
Pedro M. Baeza 5 years ago
committed by GitHub
parent
commit
8e17343552
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      .travis.yml
  2. 33
      profiler/README.rst
  3. 3
      profiler/__init__.py
  4. 15
      profiler/__manifest__.py
  5. 59
      profiler/hooks.py
  6. 2
      profiler/models/__init__.py
  7. 487
      profiler/models/profiler_profile.py
  8. 3
      profiler/security/ir.model.access.csv
  9. 3
      profiler/tests/__init__.py
  10. 45
      profiler/tests/test_profiling.py
  11. 147
      profiler/views/profiler_profile_view.xml

1
.travis.yml

@ -12,6 +12,7 @@ addons:
packages:
- expect-dev # provides unbuffer utility
- python-lxml # because pip installation is slow
- pgbadger
- nsca-client
env:

33
profiler/README.rst

@ -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.

3
profiler/__init__.py

@ -0,0 +1,3 @@
from . import models
from .hooks import post_load

15
profiler/__manifest__.py

@ -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,
}

59
profiler/hooks.py

@ -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()

2
profiler/models/__init__.py

@ -0,0 +1,2 @@
from . import profiler_profile

487
profiler/models/profiler_profile.py

@ -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()

3
profiler/security/ir.model.access.csv

@ -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

3
profiler/tests/__init__.py

@ -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

45
profiler/tests/test_profiling.py

@ -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()

147
profiler/views/profiler_profile_view.xml

@ -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>
Loading…
Cancel
Save