Browse Source
Merge pull request #375 from vauxoo-dev/8.0-oca-odoo-profiler-moy
Merge pull request #375 from vauxoo-dev/8.0-oca-odoo-profiler-moy
[ADD] profiler: Add module profilerpull/909/head
Pedro M. Baeza
7 years ago
committed by
GitHub
23 changed files with 717 additions and 0 deletions
-
95profiler/README.rst
-
6profiler/__init__.py
-
32profiler/__openerp__.py
-
5profiler/controllers/__init__.py
-
228profiler/controllers/main.py
-
16profiler/data/profiler_excluding.xml
-
103profiler/hooks.py
-
11profiler/security/group.xml
-
BINprofiler/static/description/clear_stats.png
-
BINprofiler/static/description/dump_stats.png
-
BINprofiler/static/description/player.png
-
BINprofiler/static/description/start_profiling.png
-
BINprofiler/static/description/stop_profiling.png
-
21profiler/static/src/css/player.css
-
52profiler/static/src/js/player.js
-
52profiler/static/src/js/test_profiler.js
-
24profiler/static/src/less/player.less
-
26profiler/static/src/xml/player.xml
-
5profiler/tests/__init__.py
-
17profiler/tests/test_profiler.py
-
12profiler/views/assets.xml
-
11profiler/views/profiler.xml
-
1requirements.txt
@ -0,0 +1,95 @@ |
|||||
|
.. 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 |
||||
|
|
||||
|
======== |
||||
|
Profiler |
||||
|
======== |
||||
|
|
||||
|
Integration of cProfile and PgBadger. |
||||
|
|
||||
|
Installation |
||||
|
============ |
||||
|
|
||||
|
To install this module, you need the following requirements: |
||||
|
|
||||
|
* Install `pgbadger <http://dalibo.github.io/pgbadger/>`_ binary package. |
||||
|
* Install `pstats_print2list <https://pypi.python.org/pypi/pstats_print2list>`_ python package. |
||||
|
* Set `PG_LOG_PATH` environment variable to know location of the `postgresql.log` file by default is `/var/lib/postgresql/9.X/main/pg_log/postgresql.log` |
||||
|
* Enable postgresql logs from postgresql's configuration file (Default location for Linux Debian is `/etc/postgresql/*/main/postgresql.conf`) |
||||
|
- Add the following lines at final (A postgresql restart is required `/etc/init.d/postgresql restart`) |
||||
|
|
||||
|
.. code-block:: text |
||||
|
|
||||
|
logging_collector=on |
||||
|
log_destination='stderr' |
||||
|
log_directory='pg_log' |
||||
|
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 ' |
||||
|
|
||||
|
|
||||
|
Configuration |
||||
|
============= |
||||
|
|
||||
|
By default profiler module adds two system parameters |
||||
|
- exclude_fnames > '/.repo_requirements,~/odoo-8.0,/usr/,>' |
||||
|
- exclude_query > 'ir_translation'. |
||||
|
|
||||
|
These parameters can be configurated in order to exclude some outputs from |
||||
|
profiling stats or pgbadger output. |
||||
|
|
||||
|
Usage |
||||
|
===== |
||||
|
|
||||
|
After installation, a player is add on the header bar, with following buttons: |
||||
|
|
||||
|
- .. figure:: static/description/player.png |
||||
|
:alt: Player to manage profiler |
||||
|
|
||||
|
|
||||
|
* Start profiling |
||||
|
- .. figure:: static/description/start_profiling.png |
||||
|
:alt: Start profiling |
||||
|
:height: 35px |
||||
|
* Stop profiling |
||||
|
- .. figure:: static/description/stop_profiling.png |
||||
|
:alt: Stop profiling |
||||
|
:height: 35px |
||||
|
* Download stats: download stats file |
||||
|
- .. figure:: static/description/dump_stats.png |
||||
|
:alt: Download cprofile stats file |
||||
|
:height: 35px |
||||
|
* Clear stats |
||||
|
- .. figure:: static/description/clear_stats.png |
||||
|
:alt: Clear and remove stats file |
||||
|
:height: 35px |
||||
|
|
||||
|
|
||||
|
Credits |
||||
|
======= |
||||
|
|
||||
|
Contributors |
||||
|
------------ |
||||
|
|
||||
|
* Georges Racinet <gracinet@anybox.fr> |
||||
|
- Remotely inspired from ZopeProfiler, although there is no online visualisation and there may never be one. |
||||
|
* Moisés López <moylop260@vauxoo.com> |
||||
|
* Hugo Adan <hugo@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,6 @@ |
|||||
|
# coding: utf-8 |
||||
|
# License AGPL-3 or later (http://www.gnu.org/licenses/lgpl). |
||||
|
# Copyright 2014 Anybox <http://anybox.fr> |
||||
|
# Copyright 2016 Vauxoo (https://www.vauxoo.com) <info@vauxoo.com> |
||||
|
from . import controllers |
||||
|
from .hooks import post_load |
@ -0,0 +1,32 @@ |
|||||
|
# coding: utf-8 |
||||
|
# License AGPL-3 or later (http://www.gnu.org/licenses/lgpl). |
||||
|
# Copyright 2014 Anybox <http://anybox.fr> |
||||
|
# Copyright 2016 Vauxoo (https://www.vauxoo.com) <info@vauxoo.com> |
||||
|
|
||||
|
{ |
||||
|
'name': 'profiler', |
||||
|
'version': '8.0.1.0.0', |
||||
|
'category': 'Tools', |
||||
|
'license': 'AGPL-3', |
||||
|
'author': 'Anybox, Vauxoo, Odoo Community Association (OCA)', |
||||
|
'website': 'https://odoo-community.org', |
||||
|
'depends': ['website'], |
||||
|
'data': [ |
||||
|
'data/profiler_excluding.xml', |
||||
|
'security/group.xml', |
||||
|
'views/profiler.xml', |
||||
|
'views/assets.xml', |
||||
|
], |
||||
|
'external_dependencies': { |
||||
|
'python': [ |
||||
|
'pstats_print2list', |
||||
|
], |
||||
|
}, |
||||
|
'qweb': [ |
||||
|
'static/src/xml/player.xml', |
||||
|
], |
||||
|
'installable': True, |
||||
|
'application': False, |
||||
|
'auto_install': False, |
||||
|
'post_load': 'post_load', |
||||
|
} |
@ -0,0 +1,5 @@ |
|||||
|
# coding: utf-8 |
||||
|
# License AGPL-3 or later (http://www.gnu.org/licenses/lgpl). |
||||
|
# Copyright 2014 Anybox <http://anybox.fr> |
||||
|
# Copyright 2016 Vauxoo (https://www.vauxoo.com) <info@vauxoo.com> |
||||
|
from . import main |
@ -0,0 +1,228 @@ |
|||||
|
# coding: utf-8 |
||||
|
# License AGPL-3 or later (http://www.gnu.org/licenses/lgpl). |
||||
|
# Copyright 2014 Anybox <http://anybox.fr> |
||||
|
# Copyright 2016 Vauxoo (https://www.vauxoo.com) <info@vauxoo.com> |
||||
|
import errno |
||||
|
import logging |
||||
|
import os |
||||
|
import sys |
||||
|
import tempfile |
||||
|
from cStringIO import StringIO |
||||
|
from datetime import datetime |
||||
|
|
||||
|
from openerp import http, sql_db, tools |
||||
|
from openerp.addons.web.controllers.main import content_disposition |
||||
|
from openerp.http import request |
||||
|
from openerp.service.db import dump_db_manifest |
||||
|
from openerp.tools.misc import find_in_path |
||||
|
|
||||
|
from ..hooks import CoreProfile as core |
||||
|
|
||||
|
_logger = logging.getLogger(__name__) |
||||
|
|
||||
|
try: |
||||
|
from pstats_print2list import get_pstats_print2list, print_pstats_list |
||||
|
except ImportError as err: # pragma: no cover |
||||
|
_logger.debug(err) |
||||
|
|
||||
|
DFTL_LOG_PATH = '/var/lib/postgresql/%s/main/pg_log/postgresql.log' |
||||
|
|
||||
|
PGOPTIONS = ( |
||||
|
'-c client_min_messages=notice -c log_min_messages=warning ' |
||||
|
'-c log_min_error_statement=error ' |
||||
|
'-c log_min_duration_statement=0 -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 ' |
||||
|
) |
||||
|
|
||||
|
|
||||
|
class Capturing(list): |
||||
|
def __enter__(self): |
||||
|
self._stdout = sys.stdout |
||||
|
sys.stdout = self._stringio = StringIO() |
||||
|
return self |
||||
|
|
||||
|
def __exit__(self, *args): |
||||
|
self.extend(self._stringio.getvalue().splitlines()) |
||||
|
del self._stringio # free up some memory |
||||
|
sys.stdout = self._stdout |
||||
|
|
||||
|
|
||||
|
class ProfilerController(http.Controller): |
||||
|
|
||||
|
_cp_path = '/web/profiler' |
||||
|
|
||||
|
player_state = 'profiler_player_clear' |
||||
|
begin_date = '' |
||||
|
end_date = '' |
||||
|
"""Indicate the state(css class) of the player: |
||||
|
|
||||
|
* profiler_player_clear |
||||
|
* profiler_player_enabled |
||||
|
* profiler_player_disabled |
||||
|
""" |
||||
|
|
||||
|
@http.route(['/web/profiler/enable'], type='json', auth="user") |
||||
|
def enable(self): |
||||
|
_logger.info("Enabling") |
||||
|
core.enabled = True |
||||
|
ProfilerController.begin_date = datetime.now().strftime( |
||||
|
"%Y-%m-%d %H:%M:%S") |
||||
|
ProfilerController.player_state = 'profiler_player_enabled' |
||||
|
os.environ['PGOPTIONS'] = PGOPTIONS |
||||
|
self.empty_cursor_pool() |
||||
|
|
||||
|
@http.route(['/web/profiler/disable'], type='json', auth="user") |
||||
|
def disable(self, **post): |
||||
|
_logger.info("Disabling") |
||||
|
core.enabled = False |
||||
|
ProfilerController.end_date = datetime.now().strftime( |
||||
|
"%Y-%m-%d %H:%M:%S") |
||||
|
ProfilerController.player_state = 'profiler_player_disabled' |
||||
|
os.environ.pop("PGOPTIONS", None) |
||||
|
self.empty_cursor_pool() |
||||
|
|
||||
|
@http.route(['/web/profiler/clear'], type='json', auth="user") |
||||
|
def clear(self, **post): |
||||
|
core.profile.clear() |
||||
|
_logger.info("Cleared stats") |
||||
|
ProfilerController.player_state = 'profiler_player_clear' |
||||
|
ProfilerController.end_date = '' |
||||
|
ProfilerController.begin_date = '' |
||||
|
|
||||
|
@http.route(['/web/profiler/dump'], type='http', auth="user") |
||||
|
def dump(self, token, **post): |
||||
|
"""Provide the stats as a file download. |
||||
|
|
||||
|
Uses a temporary file, because apparently there's no API to |
||||
|
dump stats in a stream directly. |
||||
|
""" |
||||
|
exclude_fname = self.get_exclude_fname() |
||||
|
with tools.osutil.tempdir() as dump_dir: |
||||
|
ts = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") |
||||
|
filename = 'openerp_%s' % ts |
||||
|
stats_path = os.path.join(dump_dir, '%s.stats' % filename) |
||||
|
core.profile.dump_stats(stats_path) |
||||
|
_logger.info("Pstats Command:") |
||||
|
params = {'fnames': stats_path, 'sort': 'cumulative', 'limit': 45, |
||||
|
'exclude_fnames': exclude_fname} |
||||
|
_logger.info( |
||||
|
"fnames=%(fnames)s, sort=%(sort)s," |
||||
|
" limit=%(limit)s, exclude_fnames=%(exclude_fnames)s", params) |
||||
|
pstats_list = get_pstats_print2list(**params) |
||||
|
with Capturing() as output: |
||||
|
print_pstats_list(pstats_list) |
||||
|
result_path = os.path.join(dump_dir, '%s.txt' % filename) |
||||
|
with open(result_path, "a") as res_file: |
||||
|
for line in output: |
||||
|
res_file.write('%s\n' % line) |
||||
|
# PG_BADGER |
||||
|
self.dump_pgbadger(dump_dir, 'pgbadger_output.txt', request.cr) |
||||
|
t_zip = tempfile.TemporaryFile() |
||||
|
tools.osutil.zip_dir(dump_dir, t_zip, include_dir=False) |
||||
|
t_zip.seek(0) |
||||
|
headers = [ |
||||
|
('Content-Type', 'application/octet-stream; charset=binary'), |
||||
|
('Content-Disposition', content_disposition( |
||||
|
'%s.zip' % filename))] |
||||
|
_logger.info('Download Profiler zip: %s', t_zip.name) |
||||
|
return request.make_response( |
||||
|
t_zip, headers=headers, cookies={'fileToken': token}) |
||||
|
|
||||
|
@http.route(['/web/profiler/initial_state'], type='json', auth="user") |
||||
|
def initial_state(self, **post): |
||||
|
user = request.env['res.users'].browse(request.uid) |
||||
|
return { |
||||
|
'has_player_group': user.has_group( |
||||
|
'profiler.group_profiler_player'), |
||||
|
'player_state': ProfilerController.player_state, |
||||
|
} |
||||
|
|
||||
|
def dump_pgbadger(self, dir_dump, output, cursor): |
||||
|
pgbadger = find_in_path("pgbadger") |
||||
|
if not pgbadger: |
||||
|
_logger.error("Pgbadger not found") |
||||
|
return |
||||
|
filename = os.path.join(dir_dump, output) |
||||
|
pg_version = dump_db_manifest(cursor)['pg_version'] |
||||
|
log_path = os.environ.get('PG_LOG_PATH', DFTL_LOG_PATH % pg_version) |
||||
|
if not os.path.exists(os.path.dirname(filename)): |
||||
|
try: |
||||
|
os.makedirs(os.path.dirname(filename)) |
||||
|
except OSError as exc: |
||||
|
# error is different than File exists |
||||
|
if exc.errno != errno.EEXIST: |
||||
|
_logger.error("Folder %s can not be created", |
||||
|
os.path.dirname(filename)) |
||||
|
return |
||||
|
_logger.info("Generating PG Badger report.") |
||||
|
exclude_query = self.get_exclude_query() |
||||
|
dbname = cursor.dbname |
||||
|
command = [ |
||||
|
pgbadger, '-f', 'stderr', '-T', 'Odoo-Profiler', |
||||
|
'-o', '-', '-d', dbname, '-b', ProfilerController.begin_date, |
||||
|
'-e', ProfilerController.end_date, '--sample', '2', |
||||
|
'--disable-type', '--disable-error', '--disable-hourly', |
||||
|
'--disable-session', '--disable-connection', |
||||
|
'--disable-temporary', '--quiet'] |
||||
|
command.extend(exclude_query) |
||||
|
command.append(log_path) |
||||
|
|
||||
|
_logger.info("Pgbadger Command:") |
||||
|
_logger.info(command) |
||||
|
result = tools.exec_command_pipe(*command) |
||||
|
with open(filename, 'w') as fw: |
||||
|
fw.write(result[1].read()) |
||||
|
_logger.info("Done") |
||||
|
|
||||
|
def get_exclude_fname(self): |
||||
|
efnameid = request.env.ref( |
||||
|
'profiler.default_exclude_fnames_pstas', raise_if_not_found=False) |
||||
|
if not efnameid: |
||||
|
return [] |
||||
|
return [os.path.expanduser(path) |
||||
|
for path in efnameid and efnameid.value.strip(',').split(',') |
||||
|
if path] |
||||
|
|
||||
|
def get_exclude_query(self): |
||||
|
"""Example '^(COPY|COMMIT)' |
||||
|
""" |
||||
|
equeryid = request.env.ref( |
||||
|
'profiler.default_exclude_query_pgbadger', |
||||
|
raise_if_not_found=False) |
||||
|
if not equeryid: |
||||
|
return [] |
||||
|
exclude_queries = [] |
||||
|
for path in equeryid and equeryid.value.strip(',').split(','): |
||||
|
exclude_queries.extend( |
||||
|
['--exclude-query', '"^(%s)" ' % path.encode('UTF-8')]) |
||||
|
return exclude_queries |
||||
|
|
||||
|
def empty_cursor_pool(self): |
||||
|
"""This method cleans (rollback) all current transactions over actual |
||||
|
cursor in order to avoid errors with waiting transactions. |
||||
|
- request.cr.rollback() |
||||
|
|
||||
|
Also connections on current database's only are closed by the next |
||||
|
statement |
||||
|
- dsn = openerp.sql_db.dsn(request.cr.dbname) |
||||
|
- openerp.sql_db._Pool.close_all(dsn[1]) |
||||
|
Otherwise next error will be trigger |
||||
|
'InterfaceError: connection already closed' |
||||
|
|
||||
|
Finally new cursor is assigned to the request object, this cursor will |
||||
|
take the os.environ setted. In this case the os.environ is setted with |
||||
|
all 'PGOPTIONS' required to log all sql transactions in postgres.log |
||||
|
file. |
||||
|
|
||||
|
If this method is called one more time, it will create a new cursor and |
||||
|
take the os.environ again, this is usefully if we want to reset |
||||
|
'PGOPTIONS' |
||||
|
|
||||
|
""" |
||||
|
request.cr._cnx.reset() |
||||
|
dsn = sql_db.dsn(request.cr.dbname) |
||||
|
sql_db._Pool.close_all(dsn[1]) |
||||
|
db = sql_db.db_connect(request.cr.dbname) |
||||
|
request._cr = db.cursor() |
@ -0,0 +1,16 @@ |
|||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||
|
<openerp> |
||||
|
<data> |
||||
|
<!-- exclude_fnames de pstats_print2list --> |
||||
|
<record id="default_exclude_fnames_pstas" model="ir.config_parameter"> |
||||
|
<field name="key">exclude_fnames</field> |
||||
|
<field name="value">/.repo_requirements,~/odoo-8.0,/usr/,></field> |
||||
|
</record> |
||||
|
|
||||
|
<!-- exclude-query de pgbader --> |
||||
|
<record id="default_exclude_query_pgbadger" model="ir.config_parameter"> |
||||
|
<field name="key">exclude_query</field> |
||||
|
<field name="value">ir_translation</field> |
||||
|
</record> |
||||
|
</data> |
||||
|
</openerp> |
@ -0,0 +1,103 @@ |
|||||
|
# coding: utf-8 |
||||
|
# License AGPL-3 or later (http://www.gnu.org/licenses/lgpl). |
||||
|
# Copyright 2014 Anybox <http://anybox.fr> |
||||
|
# Copyright 2016 Vauxoo (https://www.vauxoo.com) <info@vauxoo.com> |
||||
|
import logging |
||||
|
import os |
||||
|
from contextlib import contextmanager |
||||
|
from cProfile import Profile |
||||
|
|
||||
|
import openerp |
||||
|
from openerp import sql_db |
||||
|
from openerp.http import WebRequest |
||||
|
from openerp.service.server import ThreadedServer |
||||
|
|
||||
|
_logger = logging.getLogger(__name__) |
||||
|
|
||||
|
|
||||
|
class CoreProfile(object): |
||||
|
"""The thread-shared profile object""" |
||||
|
profile = None |
||||
|
# Indicates if the whole profiling functionality is globally active or not. |
||||
|
enabled = False |
||||
|
|
||||
|
|
||||
|
@contextmanager |
||||
|
def profiling(): |
||||
|
"""Thread local profile management, according to the shared :data:`enabled` |
||||
|
""" |
||||
|
if CoreProfile.enabled: |
||||
|
CoreProfile.profile.enable() |
||||
|
yield |
||||
|
|
||||
|
if CoreProfile.enabled: |
||||
|
CoreProfile.profile.disable() |
||||
|
|
||||
|
|
||||
|
def patch_odoo(): |
||||
|
"""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. |
||||
|
|
||||
|
Modify database connect method to add options to enable postgresql logging |
||||
|
based on PGOPTIONS environment variable |
||||
|
""" |
||||
|
_logger.info('Patching openerp.http.WebRequest._call_function') |
||||
|
webreq_f_origin = WebRequest._call_function |
||||
|
|
||||
|
def webreq_f(*args, **kwargs): |
||||
|
with profiling(): |
||||
|
return webreq_f_origin(*args, **kwargs) |
||||
|
WebRequest._call_function = webreq_f |
||||
|
|
||||
|
_logger.info('Patching openerp.sql_db.db_connect') |
||||
|
db_connect_origin = sql_db.db_connect |
||||
|
|
||||
|
def dbconnect_f(to, *args, **kwargs): |
||||
|
try: |
||||
|
to += " options='%s' " % (os.environ['PGOPTIONS'] or '') |
||||
|
except KeyError: |
||||
|
pass |
||||
|
return db_connect_origin(to, *args, **kwargs) |
||||
|
sql_db.db_connect = dbconnect_f |
||||
|
|
||||
|
|
||||
|
def dump_stats(): |
||||
|
"""Dump stats to standard file""" |
||||
|
_logger.info('Dump stats') |
||||
|
CoreProfile.profile.dump_stats( |
||||
|
os.path.expanduser('~/.openerp_server.stats')) |
||||
|
|
||||
|
|
||||
|
def create_profile(): |
||||
|
"""Create the global, shared profile object.""" |
||||
|
_logger.info('Create profile') |
||||
|
CoreProfile.profile = Profile() |
||||
|
|
||||
|
|
||||
|
def patch_stop(): |
||||
|
"""When the server is stopped then save the result of cProfile stats""" |
||||
|
origin_stop = ThreadedServer.stop |
||||
|
|
||||
|
_logger.info('Patching openerp.service.server.ThreadedServer.stop') |
||||
|
|
||||
|
def stop(*args, **kwargs): |
||||
|
if openerp.tools.config['test_enable']: |
||||
|
dump_stats() |
||||
|
return origin_stop(*args, **kwargs) |
||||
|
ThreadedServer.stop = stop |
||||
|
|
||||
|
|
||||
|
def post_load(): |
||||
|
_logger.info('Post load') |
||||
|
create_profile() |
||||
|
patch_odoo() |
||||
|
if openerp.tools.config['test_enable']: |
||||
|
# Enable profile in test mode for orm methods. |
||||
|
_logger.info('Enabling profiler and apply patch') |
||||
|
CoreProfile.enabled = True |
||||
|
patch_stop() |
@ -0,0 +1,11 @@ |
|||||
|
<?xml version="1.0" encoding="UTF-8"?> |
||||
|
<openerp> |
||||
|
<data noupdate="1"> |
||||
|
<record id="group_profiler_player" model="res.groups"> |
||||
|
<field name="name">Profiling capability</field> |
||||
|
</record> |
||||
|
<record model="res.users" id="base.user_root"> |
||||
|
<field name="groups_id" eval="[(4, ref('profiler.group_profiler_player'))]"/> |
||||
|
</record> |
||||
|
</data> |
||||
|
</openerp> |
After Width: 277 | Height: 58 | Size: 8.2 KiB |
After Width: 279 | Height: 52 | Size: 8.1 KiB |
After Width: 76 | Height: 30 | Size: 1.5 KiB |
After Width: 280 | Height: 49 | Size: 7.1 KiB |
After Width: 275 | Height: 57 | Size: 7.2 KiB |
@ -0,0 +1,21 @@ |
|||||
|
.oe_topbar_item.profiler_player i { |
||||
|
cursor: pointer; |
||||
|
} |
||||
|
.oe_topbar_item.profiler_player.profiler_player_enabled a.profiler_enable { |
||||
|
display: None; |
||||
|
} |
||||
|
.oe_topbar_item.profiler_player.profiler_player_enabled a.profiler_dump { |
||||
|
display: None; |
||||
|
} |
||||
|
.oe_topbar_item.profiler_player.profiler_player_disabled a.profiler_disable { |
||||
|
display: None; |
||||
|
} |
||||
|
.oe_topbar_item.profiler_player.profiler_player_clear a.profiler_disable { |
||||
|
display: None; |
||||
|
} |
||||
|
.oe_topbar_item.profiler_player.profiler_player_clear a.profiler_clear { |
||||
|
display: None; |
||||
|
} |
||||
|
.oe_topbar_item.profiler_player.profiler_player_clear a.profiler_dump { |
||||
|
display: None; |
||||
|
} |
@ -0,0 +1,52 @@ |
|||||
|
openerp.profiler = function(instance) { |
||||
|
instance.profiler.Player = instance.web.Widget.extend({ |
||||
|
template: 'profiler.player', |
||||
|
events: { |
||||
|
"click .profiler_enable": "enable", |
||||
|
"click .profiler_disable": "disable", |
||||
|
"click .profiler_clear": "clear", |
||||
|
"click .profiler_dump": "dump" |
||||
|
}, |
||||
|
apply_class: function(css_class) { |
||||
|
this.$el.removeClass('profiler_player_enabled'); |
||||
|
this.$el.removeClass('profiler_player_disabled'); |
||||
|
this.$el.removeClass('profiler_player_clear'); |
||||
|
this.$el.addClass(css_class); |
||||
|
}, |
||||
|
enable: function() { |
||||
|
this.rpc('/web/profiler/enable', {}); |
||||
|
this.apply_class('profiler_player_enabled'); |
||||
|
}, |
||||
|
disable: function() { |
||||
|
this.rpc('/web/profiler/disable', {}); |
||||
|
this.apply_class('profiler_player_disabled'); |
||||
|
}, |
||||
|
clear: function() { |
||||
|
this.rpc('/web/profiler/clear', {}); |
||||
|
this.apply_class('profiler_player_clear'); |
||||
|
}, |
||||
|
dump: function() { |
||||
|
$.blockUI(); |
||||
|
this.session.get_file({ |
||||
|
url: '/web/profiler/dump', |
||||
|
complete: $.unblockUI |
||||
|
}); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
instance.web.UserMenu.include({ |
||||
|
do_update: function () { |
||||
|
var self = this; |
||||
|
this.update_promise.done(function () { |
||||
|
self.rpc('/web/profiler/initial_state', {}).done(function(state) { |
||||
|
if (state.has_player_group) { |
||||
|
this.profiler_player = new instance.profiler.Player(this); |
||||
|
this.profiler_player.prependTo(instance.webclient.$('.oe_systray')); |
||||
|
this.profiler_player.apply_class(state.player_state); |
||||
|
} |
||||
|
}); |
||||
|
}); |
||||
|
return this._super(); |
||||
|
} |
||||
|
}); |
||||
|
}; |
@ -0,0 +1,52 @@ |
|||||
|
(function(){ |
||||
|
'use_strict'; |
||||
|
openerp.Tour.register({ |
||||
|
id: 'profile_run', |
||||
|
name: 'Profile run', |
||||
|
path: '/web', |
||||
|
mode: 'test', |
||||
|
steps: [ |
||||
|
{ |
||||
|
title: 'Check if is cleared', |
||||
|
waitFor: 'li.oe_topbar_item.profiler_player.profiler_player_clear' |
||||
|
}, |
||||
|
{ |
||||
|
title: 'Start profiling', |
||||
|
onload: function () { |
||||
|
$('a.profiler_enable').trigger('click'); |
||||
|
} |
||||
|
}, |
||||
|
{ |
||||
|
title: 'Check if is enabled', |
||||
|
waitFor: 'li.oe_topbar_item.profiler_player.profiler_player_enabled' |
||||
|
}, |
||||
|
{ |
||||
|
title: 'Stop profiling', |
||||
|
onload: function () { |
||||
|
$('a.profiler_disable').trigger('click'); |
||||
|
} |
||||
|
}, |
||||
|
{ |
||||
|
title: 'Check if is disabled', |
||||
|
waitFor: 'li.oe_topbar_item.profiler_player.profiler_player_disabled' |
||||
|
}, |
||||
|
{ |
||||
|
title: 'Dump profiling', |
||||
|
onload: function () { |
||||
|
$('a.profiler_dump').trigger('click'); |
||||
|
} |
||||
|
}, |
||||
|
{ |
||||
|
title: 'Clear profiling', |
||||
|
onload: function () { |
||||
|
$('a.profiler_clear').trigger('click'); |
||||
|
} |
||||
|
}, |
||||
|
{ |
||||
|
title: 'Check if is cleared again', |
||||
|
waitFor: 'li.oe_topbar_item.profiler_player.profiler_player_clear' |
||||
|
}, |
||||
|
|
||||
|
] |
||||
|
}); |
||||
|
}()); |
@ -0,0 +1,24 @@ |
|||||
|
#oe_main_menu_navbar .o_menu_systray { |
||||
|
|
||||
|
.oe_topbar_item.profiler_player i { |
||||
|
cursor: pointer; |
||||
|
} |
||||
|
.oe_topbar_item.profiler_player.profiler_player_enabled a.profiler_enable { |
||||
|
display: None; |
||||
|
} |
||||
|
.oe_topbar_item.profiler_player.profiler_player_enabled a.profiler_dump { |
||||
|
display: None; |
||||
|
} |
||||
|
.oe_topbar_item.profiler_player.profiler_player_disabled a.profiler_disable { |
||||
|
display: None; |
||||
|
} |
||||
|
.oe_topbar_item.profiler_player.profiler_player_clear a.profiler_disable { |
||||
|
display: None; |
||||
|
} |
||||
|
.oe_topbar_item.profiler_player.profiler_player_clear a.profiler_clear { |
||||
|
display: None; |
||||
|
} |
||||
|
.oe_topbar_item.profiler_player.profiler_player_clear a.profiler_dump { |
||||
|
display: None; |
||||
|
} |
||||
|
} |
@ -0,0 +1,26 @@ |
|||||
|
<templates> |
||||
|
<t t-name="profiler.player"> |
||||
|
<li class="oe_topbar_item profiler_player"> |
||||
|
<a href="#" class="profiler_enable"> |
||||
|
<i class="fa fa-play" title="Start profiling"/> |
||||
|
</a> |
||||
|
</li> |
||||
|
<li class="oe_topbar_item profiler_player"> |
||||
|
<a href="#" class="profiler_disable"> |
||||
|
<!-- <i class="fa fa-pause" title="Stop profiling"/> --> |
||||
|
<i class="fa fa-stop" title="Stop profiling"/> |
||||
|
</a> |
||||
|
</li> |
||||
|
<li class="oe_topbar_item profiler_player"> |
||||
|
<a href="#" class="profiler_clear"> |
||||
|
<!-- <i class="fa fa-stop" title="Clear profiling"/> --> |
||||
|
<i class="fa fa-refresh" title="Clear profiling"/> |
||||
|
</a> |
||||
|
</li> |
||||
|
<li class="oe_topbar_item profiler_player"> |
||||
|
<a href="#" class="profiler_dump"> |
||||
|
<i class="fa fa-floppy-o" title="Download stats"/> |
||||
|
</a> |
||||
|
</li> |
||||
|
</t> |
||||
|
</templates> |
@ -0,0 +1,5 @@ |
|||||
|
# coding: utf-8 |
||||
|
# License AGPL-3 or later (http://www.gnu.org/licenses/lgpl). |
||||
|
# Copyright 2014 Anybox <http://anybox.fr> |
||||
|
# Copyright 2016 Vauxoo (https://www.vauxoo.com) <info@vauxoo.com> |
||||
|
from . import test_profiler |
@ -0,0 +1,17 @@ |
|||||
|
# coding: utf-8 |
||||
|
# License AGPL-3 or later (http://www.gnu.org/licenses/lgpl). |
||||
|
# Copyright 2016 Vauxoo (https://www.vauxoo.com) <info@vauxoo.com> |
||||
|
|
||||
|
import unittest |
||||
|
|
||||
|
from openerp import tests |
||||
|
|
||||
|
|
||||
|
@tests.at_install(False) |
||||
|
@tests.post_install(True) |
||||
|
class TestProfiler(tests.HttpCase): |
||||
|
|
||||
|
@unittest.skip("phantomjs tests async for 8.0 are so flaky") |
||||
|
def test_profiler_tour(self): |
||||
|
self.phantom_js('/web', "openerp.Tour.run('profile_run', 'test')", |
||||
|
'openerp.Tour.tours.profile_run', login='admin') |
@ -0,0 +1,12 @@ |
|||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||
|
<openerp> |
||||
|
<data> |
||||
|
<template id="assets_backend_test_profiler" name="Profiler Tests" |
||||
|
inherit_id="website.assets_backend"> |
||||
|
<xpath expr="//script[last()]" position="after"> |
||||
|
<script src="/profiler/static/src/js/test_profiler.js" |
||||
|
type="text/javascript"/> |
||||
|
</xpath> |
||||
|
</template> |
||||
|
</data> |
||||
|
</openerp> |
@ -0,0 +1,11 @@ |
|||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||
|
<openerp> |
||||
|
<data> |
||||
|
<template id="profiler_assets_backend" name="profiler assets" inherit_id="web.assets_backend"> |
||||
|
<xpath expr="." position="inside"> |
||||
|
<link rel="stylesheet" href="/profiler/static/src/css/player.css" /> |
||||
|
<script type="text/javascript" src="/profiler/static/src/js/player.js" /> |
||||
|
</xpath> |
||||
|
</template> |
||||
|
</data> |
||||
|
</openerp> |
Write
Preview
Loading…
Cancel
Save
Reference in new issue