Browse Source

Initial implementation of the 'profiler' module

pull/375/head
Georges Racinet on purity 10 years ago
committed by Moisés López
parent
commit
a11a266017
  1. 14
      .hgignore
  2. 48
      README.rst
  3. 32
      profiler/__init__.py
  4. 52
      profiler/__openerp__.py
  5. 50
      profiler/controllers/__init__.py
  6. 0
      profiler/controllers/main.py
  7. 20
      profiler/core.py
  8. BIN
      profiler/static/src/img/icon.png
  9. 3
      profiler/static/src/js/boot.js
  10. 32
      profiler/static/src/js/profiler.js
  11. 50
      profiler/view/menu.xml

14
.hgignore

@ -0,0 +1,14 @@
syntax: glob
*.swp
*.orig
*.pyc
*.pyo
*.log
*\#
*.\#*
*~
.pydevproject
.project
.installed.cfg

48
README.rst

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

32
profiler/__init__.py

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

52
profiler/__openerp__.py

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

50
profiler/controllers/__init__.py

@ -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
profiler/controllers/main.py

20
profiler/core.py

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

BIN
profiler/static/src/img/icon.png

After

Width: 80  |  Height: 80  |  Size: 2.1 KiB

3
profiler/static/src/js/boot.js

@ -0,0 +1,3 @@
openerp.profiler = function(instance) {
openerp.profiler.profiler_enable();
};

32
profiler/static/src/js/profiler.js

@ -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");
};

50
profiler/view/menu.xml

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