Georges Racinet on purity
10 years ago
committed by
Moisés López
11 changed files with 301 additions and 0 deletions
-
14.hgignore
-
48README.rst
-
32profiler/__init__.py
-
52profiler/__openerp__.py
-
50profiler/controllers/__init__.py
-
0profiler/controllers/main.py
-
20profiler/core.py
-
BINprofiler/static/src/img/icon.png
-
3profiler/static/src/js/boot.js
-
32profiler/static/src/js/profiler.js
-
50profiler/view/menu.xml
@ -0,0 +1,14 @@ |
|||
syntax: glob |
|||
|
|||
*.swp |
|||
*.orig |
|||
*.pyc |
|||
*.pyo |
|||
*.log |
|||
*\# |
|||
*.\#* |
|||
*~ |
|||
|
|||
.pydevproject |
|||
.project |
|||
.installed.cfg |
@ -0,0 +1,48 @@ |
|||
cProfile integration for OpenERP |
|||
================================ |
|||
|
|||
The module ``profiler`` provides a very basic integration of |
|||
the standard ``cProfile`` into OpenERP/Odoo. |
|||
|
|||
Basic usage |
|||
----------- |
|||
|
|||
After installation, a new menu "Profiler" gets available in the |
|||
administration menu, with four items: |
|||
|
|||
* Start profiling |
|||
* Stop profiling |
|||
* Dump stats: retrieve from the browser a stats file, in the standard |
|||
cProfile format (see Python documentation and performance wiki page |
|||
for exploitation tips). |
|||
* Clear stats |
|||
|
|||
Advantages |
|||
---------- |
|||
|
|||
Executing Python code under the profiler is not really hard, but this |
|||
module allows to do it in OpenERP context such that: |
|||
|
|||
* no direct modification of main server Python code or addons is needed |
|||
(although it could be pretty simple depending on the need) |
|||
* subtleties about threads are taken care of. In particular, the |
|||
accumulation of stats over several requests is correct. |
|||
|
|||
Caveats |
|||
------- |
|||
|
|||
* enabling the profile in one database actually does it for the whole |
|||
instance |
|||
* multiprocessing (``--workers``) is *not* taken into account |
|||
* currently developped and tested with OpenERP 7.0 only |
|||
* no special care for uninstallion : currently a restart is needed to |
|||
finish uninstalling. |
|||
* requests not going through web controllers are currently not taken |
|||
into account |
|||
* UI is currently quite crude, but that's good enough for now |
|||
|
|||
Credit |
|||
------ |
|||
|
|||
Remotely inspired from ZopeProfiler, although there is no online |
|||
visualisation and there may never be one. |
@ -0,0 +1,32 @@ |
|||
import controllers # noqa |
|||
from cProfile import Profile |
|||
|
|||
|
|||
def patch_openerp(): |
|||
"""Modify OpenERP/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, OpenERP 7 spawns a new thread for each request. |
|||
""" |
|||
from openerp.addons.web.http import JsonRequest |
|||
from .core import profiling |
|||
orig_dispatch = JsonRequest.dispatch |
|||
|
|||
def dispatch(*args, **kwargs): |
|||
with profiling(): |
|||
return orig_dispatch(*args, **kwargs) |
|||
JsonRequest.dispatch = dispatch |
|||
|
|||
|
|||
def create_profile(): |
|||
"""Create the global, shared profile object.""" |
|||
from . import core |
|||
core.profile = Profile() |
|||
|
|||
|
|||
def post_load(): |
|||
create_profile() |
|||
patch_openerp() |
@ -0,0 +1,52 @@ |
|||
#============================================================================== |
|||
# = |
|||
# profiler module for OpenERP, cProfile integration for Odoo/OpenERP |
|||
# Copyright (C) 2014 Anybox <http://anybox.fr> |
|||
# = |
|||
# This file is a part of profiler |
|||
# = |
|||
# profiler is free software: you can redistribute it and/or modify |
|||
# it under the terms of the GNU Affero General Public License v3 or later |
|||
# as published by the Free Software Foundation, either version 3 of the |
|||
# License, or (at your option) any later version. |
|||
# = |
|||
# profiler is distributed in the hope that it will be useful, |
|||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
# GNU Affero General Public License v3 or later for more details. |
|||
# = |
|||
# You should have received a copy of the GNU Affero General Public License |
|||
# v3 or later along with this program. |
|||
# If not, see <http://www.gnu.org/licenses/>. |
|||
# = |
|||
#============================================================================== |
|||
{ |
|||
'name': 'profiler', |
|||
'version': '0.1', |
|||
'category': 'devtools', |
|||
'description': """ |
|||
cprofile integration for Odoo/OpenERP. Check the Profiler menu in admin menu |
|||
""", |
|||
'author': 'Georges Racinet', |
|||
'website': 'http://anybox.fr', |
|||
'depends': ['base', 'web'], |
|||
'data': [ |
|||
'view/menu.xml', |
|||
], |
|||
'test': [ |
|||
], |
|||
'demo': [ |
|||
], |
|||
'js': [ |
|||
'static/src/js/profiler.js', |
|||
], |
|||
'qweb': [ |
|||
], |
|||
'css': [ |
|||
], |
|||
'installable': True, |
|||
'application': False, |
|||
'auto_install': False, |
|||
'license': 'AGPL-3', |
|||
'post_load': 'post_load', |
|||
} |
@ -0,0 +1,50 @@ |
|||
import os |
|||
import logging |
|||
from datetime import datetime |
|||
from tempfile import mkstemp |
|||
import openerp.addons.web.http as openerpweb |
|||
from .. import core |
|||
|
|||
logger = logging.getLogger(__name__) |
|||
|
|||
|
|||
class ProfilerController(openerpweb.Controller): |
|||
|
|||
_cp_path = '/web/profiler' |
|||
|
|||
@openerpweb.jsonrequest |
|||
def enable(self, request): |
|||
logger.info("Enabling") |
|||
core.enabled = True |
|||
|
|||
@openerpweb.jsonrequest |
|||
def disable(self, request): |
|||
logger.info("Disabling") |
|||
core.disabled = True |
|||
|
|||
@openerpweb.jsonrequest |
|||
def clear(self, request): |
|||
core.profile.clear() |
|||
logger.info("Cleared stats") |
|||
|
|||
@openerpweb.httprequest |
|||
def dump(self, request, token): |
|||
"""Provide the stats as a file download. |
|||
|
|||
Uses a temporary file, because apparently there's no API to |
|||
dump stats in a stream directly. |
|||
""" |
|||
handle, path = mkstemp(prefix='profiling') |
|||
core.profile.dump_stats(path) |
|||
stream = os.fdopen(handle) |
|||
os.unlink(path) # TODO POSIX only ? |
|||
stream.seek(0) |
|||
filename = 'openerp_%s.stats' % datetime.now().isoformat() |
|||
# can't close the stream even in a context manager: it'll be needed |
|||
# after the return from this method, we'll let Python's GC do its job |
|||
return request.make_response( |
|||
stream, |
|||
headers=[ |
|||
('Content-Disposition', 'attachment; filename="%s"' % filename), |
|||
('Content-Type', 'application/octet-stream') |
|||
], cookies={'fileToken': int(token)}) |
@ -0,0 +1,20 @@ |
|||
from contextlib import contextmanager |
|||
profile = None |
|||
"""The thread-shared profile object. |
|||
""" |
|||
|
|||
enabled = False |
|||
"""Indicates if the whole profiling functionality is globally active or not. |
|||
""" |
|||
|
|||
|
|||
@contextmanager |
|||
def profiling(): |
|||
"""Thread local profile management, according to the shared :data:`enabled` |
|||
""" |
|||
if enabled: |
|||
profile.enable() |
|||
yield |
|||
|
|||
if enabled: |
|||
profile.disable() |
After Width: 80 | Height: 80 | Size: 2.1 KiB |
@ -0,0 +1,3 @@ |
|||
openerp.profiler = function(instance) { |
|||
openerp.profiler.profiler_enable(); |
|||
}; |
@ -0,0 +1,32 @@ |
|||
openerp.profiler = function(instance) { |
|||
openerp.profiler.profiler_enable(instance); |
|||
}; |
|||
|
|||
openerp.profiler.profiler_enable = function(instance) { |
|||
instance.profiler.controllers = { |
|||
'profiler.enable': 'enable', |
|||
'profiler.disable': 'disable', |
|||
'profiler.clear': 'clear', |
|||
}; |
|||
instance.profiler.simple_action = function(parent, action) { |
|||
console.info(action); |
|||
parent.session.rpc('/web/profiler/' + instance.profiler.controllers[action.tag], {}); |
|||
}; |
|||
|
|||
instance.profiler.dump = function(parent, action) { |
|||
$.blockUI(); |
|||
parent.session.get_file({ |
|||
url: '/web/profiler/dump', |
|||
complete: $.unblockUI |
|||
}); |
|||
|
|||
}; |
|||
instance.web.client_actions.add("profiler.enable", |
|||
"instance.profiler.simple_action"); |
|||
instance.web.client_actions.add("profiler.disable", |
|||
"instance.profiler.simple_action"); |
|||
instance.web.client_actions.add("profiler.clear", |
|||
"instance.profiler.simple_action"); |
|||
instance.web.client_actions.add("profiler.dump", |
|||
"instance.profiler.dump"); |
|||
}; |
@ -0,0 +1,50 @@ |
|||
<?xml version="1.0" encoding="UTF-8"?> |
|||
<openerp> |
|||
<data> |
|||
|
|||
<menuitem id="menu_main" |
|||
parent="base.menu_administration" |
|||
sequence="0" |
|||
name="Profiler"/> |
|||
|
|||
<record model="ir.actions.client" id="action_profiler_enable"> |
|||
<field name="name">Enable Profiler</field> |
|||
<field name="tag">profiler.enable</field> |
|||
</record> |
|||
<menuitem id="menu_enable" |
|||
parent="menu_main" |
|||
sequence="10" |
|||
action="action_profiler_enable" |
|||
name="Start profiling"/> |
|||
|
|||
<record model="ir.actions.client" id="action_profiler_disable"> |
|||
<field name="name">Disable Profiler</field> |
|||
<field name="tag">profiler.disable</field> |
|||
</record> |
|||
<menuitem id="menu_disable" |
|||
parent="menu_main" |
|||
sequence="20" |
|||
action="action_profiler_disable" |
|||
name="Stop profiling"/> |
|||
|
|||
<record model="ir.actions.client" id="action_profiler_dump"> |
|||
<field name="name">Dump Stats</field> |
|||
<field name="tag">profiler.dump</field> |
|||
</record> |
|||
<menuitem id="menu_dump" |
|||
parent="menu_main" |
|||
sequence="30" |
|||
action="action_profiler_dump" |
|||
name="Dump stats"/> |
|||
|
|||
<record model="ir.actions.client" id="action_profiler_clear"> |
|||
<field name="name">Clear Stats</field> |
|||
<field name="tag">profiler.clear</field> |
|||
</record> |
|||
<menuitem id="menu_clear" |
|||
parent="menu_main" |
|||
sequence="40" |
|||
action="action_profiler_clear" |
|||
name="Clear stats"/> |
|||
</data> |
|||
</openerp> |
Write
Preview
Loading…
Cancel
Save
Reference in new issue