Georges Racinet on purity
11 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