147 lines
5.7 KiB
Python
147 lines
5.7 KiB
Python
|
# -*- coding: utf-8 -*-
|
||
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||
|
|
||
|
import base64
|
||
|
import datetime
|
||
|
import json
|
||
|
import logging
|
||
|
|
||
|
from dateutil.relativedelta import relativedelta
|
||
|
|
||
|
from odoo import fields, models, api, _
|
||
|
from odoo.exceptions import UserError
|
||
|
from odoo.http import request
|
||
|
from odoo.tools.profiler import make_session
|
||
|
from odoo.tools.speedscope import Speedscope
|
||
|
|
||
|
_logger = logging.getLogger(__name__)
|
||
|
|
||
|
|
||
|
class IrProfile(models.Model):
|
||
|
_name = 'ir.profile'
|
||
|
_description = 'Profiling results'
|
||
|
_log_access = False # avoid useless foreign key on res_user
|
||
|
_order = 'session desc, id desc'
|
||
|
_allow_sudo_commands = False
|
||
|
|
||
|
create_date = fields.Datetime('Creation Date')
|
||
|
|
||
|
session = fields.Char('Session', index=True)
|
||
|
name = fields.Char('Description')
|
||
|
duration = fields.Float('Duration')
|
||
|
|
||
|
init_stack_trace = fields.Text('Initial stack trace', prefetch=False)
|
||
|
|
||
|
sql = fields.Text('Sql', prefetch=False)
|
||
|
sql_count = fields.Integer('Queries Count')
|
||
|
traces_async = fields.Text('Traces Async', prefetch=False)
|
||
|
traces_sync = fields.Text('Traces Sync', prefetch=False)
|
||
|
qweb = fields.Text('Qweb', prefetch=False)
|
||
|
entry_count = fields.Integer('Entry count')
|
||
|
|
||
|
speedscope = fields.Binary('Speedscope', compute='_compute_speedscope')
|
||
|
speedscope_url = fields.Text('Open', compute='_compute_speedscope_url')
|
||
|
|
||
|
@api.autovacuum
|
||
|
def _gc_profile(self):
|
||
|
# remove profiles older than 30 days
|
||
|
domain = [('create_date', '<', fields.Datetime.now() - datetime.timedelta(days=30))]
|
||
|
return self.sudo().search(domain).unlink()
|
||
|
|
||
|
def _compute_speedscope(self):
|
||
|
for execution in self:
|
||
|
sp = Speedscope(init_stack_trace=json.loads(execution.init_stack_trace))
|
||
|
if execution.sql:
|
||
|
sp.add('sql', json.loads(execution.sql))
|
||
|
if execution.traces_async:
|
||
|
sp.add('frames', json.loads(execution.traces_async))
|
||
|
if execution.traces_sync:
|
||
|
sp.add('settrace', json.loads(execution.traces_sync))
|
||
|
|
||
|
result = json.dumps(sp.add_default().make())
|
||
|
execution.speedscope = base64.b64encode(result.encode('utf-8'))
|
||
|
|
||
|
def _compute_speedscope_url(self):
|
||
|
for profile in self:
|
||
|
profile.speedscope_url = f'/web/speedscope/{profile.id}'
|
||
|
|
||
|
def _enabled_until(self):
|
||
|
"""
|
||
|
If the profiling is enabled, return until when it is enabled.
|
||
|
Otherwise return ``None``.
|
||
|
"""
|
||
|
limit = self.env['ir.config_parameter'].sudo().get_param('base.profiling_enabled_until', '')
|
||
|
return limit if str(fields.Datetime.now()) < limit else None
|
||
|
|
||
|
@api.model
|
||
|
def set_profiling(self, profile=None, collectors=None, params=None):
|
||
|
"""
|
||
|
Enable or disable profiling for the current user.
|
||
|
|
||
|
:param profile: ``True`` to enable profiling, ``False`` to disable it.
|
||
|
:param list collectors: optional list of collectors to use (string)
|
||
|
:param dict params: optional parameters set on the profiler object
|
||
|
"""
|
||
|
# Note: parameters are coming from a rpc calls or route param (public user),
|
||
|
# meaning that corresponding session variables are client-defined.
|
||
|
# This allows to activate any profiler, but can be
|
||
|
# dangerous handling request.session.profile_collectors/profile_params.
|
||
|
if profile:
|
||
|
limit = self._enabled_until()
|
||
|
_logger.info("User %s started profiling", self.env.user.name)
|
||
|
if not limit:
|
||
|
request.session.profile_session = None
|
||
|
if self.env.user._is_system():
|
||
|
return {
|
||
|
'type': 'ir.actions.act_window',
|
||
|
'view_mode': 'form',
|
||
|
'res_model': 'base.enable.profiling.wizard',
|
||
|
'target': 'new',
|
||
|
'views': [[False, 'form']],
|
||
|
}
|
||
|
raise UserError(_('Profiling is not enabled on this database. Please contact an administrator.'))
|
||
|
if not request.session.profile_session:
|
||
|
request.session.profile_session = make_session(self.env.user.name)
|
||
|
request.session.profile_expiration = limit
|
||
|
if request.session.profile_collectors is None:
|
||
|
request.session.profile_collectors = []
|
||
|
if request.session.profile_params is None:
|
||
|
request.session.profile_params = {}
|
||
|
elif profile is not None:
|
||
|
request.session.profile_session = None
|
||
|
|
||
|
if collectors is not None:
|
||
|
request.session.profile_collectors = collectors
|
||
|
|
||
|
if params is not None:
|
||
|
request.session.profile_params = params
|
||
|
|
||
|
return {
|
||
|
'session': request.session.profile_session,
|
||
|
'collectors': request.session.profile_collectors,
|
||
|
'params': request.session.profile_params,
|
||
|
}
|
||
|
|
||
|
|
||
|
class EnableProfilingWizard(models.TransientModel):
|
||
|
_name = 'base.enable.profiling.wizard'
|
||
|
_description = "Enable profiling for some time"
|
||
|
|
||
|
duration = fields.Selection([
|
||
|
('minutes_5', "5 Minutes"),
|
||
|
('hours_1', "1 Hour"),
|
||
|
('days_1', "1 Day"),
|
||
|
('months_1', "1 Month"),
|
||
|
], string="Enable profiling for")
|
||
|
expiration = fields.Datetime("Enable profiling until", compute='_compute_expiration', store=True, readonly=False)
|
||
|
|
||
|
@api.depends('duration')
|
||
|
def _compute_expiration(self):
|
||
|
for record in self:
|
||
|
unit, quantity = (record.duration or 'days_0').split('_')
|
||
|
record.expiration = fields.Datetime.now() + relativedelta(**{unit: int(quantity)})
|
||
|
|
||
|
def submit(self):
|
||
|
self.env['ir.config_parameter'].set_param('base.profiling_enabled_until', self.expiration)
|
||
|
return False
|