By activating the profiler debugger, the two qweb options is added. The
qweb templates that need to be rendered are compiled into a new function
to add instructions for saving data.
`Add qweb directive context`
It's a sub-option of "Record sql" or "Record traces", add some context on
thread at current call stack level. This context stored by collector
beside stack and is used by Speedscope to add a level to the stack with
this qweb directive information.
```
directive=t-call='website.layout', xpath=/t/t
t_call_content
directive=t-foreach='5' t-as="'a', xpath=/t/t/div/div
directive=t-esc='website.search([])', xpath=/t/t/div/div/t
execute
```
`Record qweb`
Add profiling data used by ProfilingQwebView widget. In the `ir.profile`
form view, the widget display the duration and number of sql of every qweb
directives with the xml of templates. Every xml is recorded to be
consulted even if the user change the xml templates.
```xml
<t t-call="website.layout">
<div>
<div t-foreach="5" t-as="a">
<t t-esc="website.search([])"/> <!-- will display 5 separate requests -->
</div>
</div>
</t>
```
closes odoo/odoo#74712
Signed-off-by: Xavier Dollé (xdo) <xdo@odoo.com>
145 lines
5.7 KiB
Python
145 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'
|
|
|
|
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)
|
|
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
|