misc: add a record_timings utility decorator (#85102)

This commit is contained in:
Frédéric Péters 2023-12-23 10:29:45 +01:00
parent 614c148dd3
commit 8badd75012
4 changed files with 54 additions and 23 deletions

View File

@ -2635,4 +2635,4 @@ def test_field_live_too_long(pub, freezer):
assert pub.loggederror_class.count() == 1
logged_error = pub.loggederror_class.select()[0]
assert logged_error.summary == '/live call is taking too long'
assert 'timings = {' in logged_error.traceback
assert 'timings = ' in logged_error.traceback

View File

@ -34,6 +34,7 @@ from wcs.blocks import BlockSubWidget, BlockWidget
from wcs.fields import FileField
from wcs.qommon.admin.texts import TextsDirectory
from wcs.qommon.upload_storage import get_storage_object
from wcs.utils import record_timings
from wcs.wf.editable import EditableWorkflowStatusItem
from wcs.workflows import RedisplayFormException
@ -812,26 +813,18 @@ class FormStatusPage(Directory, FormTemplateMixin):
return redirect(file_url)
@classmethod
@record_timings(name='/live call', record_if_over=5)
def live_process_fields(cls, form, formdata, displayed_fields):
if form is None:
return json.dumps({'result': {}})
t0 = time.time()
timings = {'relative_start': get_request().get_duration()}
def add_timing(key):
nonlocal t0
new_t0 = time.time()
timings[key], t0 = '%.4f' % (new_t0 - t0), new_t0
def reset_timing():
nonlocal t0
t0 = time.time()
request = get_request()
request.add_timing_mark('relative_start')
result = {}
for field in displayed_fields:
result[field.id] = {'visible': field.is_visible(formdata.data, formdata.formdef)}
add_timing('visibility')
request.add_timing_mark('visibility')
modified_field_ids = get_request().form.get('modified_field_id[]') or []
modified_field_varnames = set()
@ -853,7 +846,7 @@ class FormStatusPage(Directory, FormTemplateMixin):
break
for field in displayed_fields:
reset_timing()
t0 = time.time()
if field.key in ('item', 'items') and field.data_source:
data_source = data_sources.get_object(field.data_source)
if data_source.type not in (
@ -876,7 +869,7 @@ class FormStatusPage(Directory, FormTemplateMixin):
# but reduce payload weight by removing the API URLs
for options in result[field.id]['items']:
options.pop('api', None)
add_timing(f'item-options-{field.id}')
request.add_timing_mark(f'item-options-{field.id}', relative_start=t0)
def get_all_field_widgets(form):
for widget in form.widgets:
@ -892,7 +885,7 @@ class FormStatusPage(Directory, FormTemplateMixin):
block_row += 1
for block, block_row, field, widget in get_all_field_widgets(form):
reset_timing()
t0 = time.time()
if block:
try:
block_data = formdata.data.get(block.id)['data'][block_row]
@ -966,12 +959,9 @@ class FormStatusPage(Directory, FormTemplateMixin):
entry['content'] = value
entry['locked'] = locked
add_timing(f'field-block-{block.id}-row-{block_row}' if block else f'field-{field.id}')
if (get_request().get_duration() - timings['relative_start']) > 5:
# timings will be displayed in the traceback part of the error.
timings['total'] = get_request().get_duration()
get_publisher().record_error(_('/live call is taking too long'))
request.add_timing_mark(
f'field-block-{block.id}-row-{block_row}' if block else f'field-{field.id}', relative_start=t0
)
return json.dumps({'result': result})

View File

@ -42,6 +42,7 @@ class HTTPRequest(quixote.http_request.HTTPRequest):
self.ignore_session = False
self.wscalls_cache = {}
self.datasources_cache = {}
self.timings = []
# keep a copy of environment to make sure it's not reused along
# uwsgi/gunicorn processes.
self.environ = copy.copy(self.environ)
@ -252,3 +253,19 @@ class HTTPRequest(quixote.http_request.HTTPRequest):
def get_duration(self):
return time.time() - self.t0
def start_timing(self, name):
self.timings.append({'name': name, 'start': time.time()})
return self.timings[-1]
def stop_timing(self, timing):
timing['end'] = time.time()
timing['duration'] = timing['end'] - timing['start']
return timing['duration']
def add_timing_mark(self, name, relative_start=None):
timestamp = time.time()
duration = timestamp - (
relative_start or self.timings[-1].get('timestamp') or self.timings[-1].get('start')
)
self.timings.append({'mark': name, 'timestamp': timestamp, 'duration': duration})

View File

@ -19,9 +19,10 @@ import os
import re
import django.template.loaders.filesystem
from quixote import get_publisher, get_request
from .qommon import _
from .qommon.publisher import get_cfg, get_publisher
from .qommon.publisher import get_cfg
from .qommon.template import get_theme_directory
@ -167,3 +168,26 @@ def grep_strings(string, hit_function):
else:
continue
break
class record_timings:
def __init__(self, name=None, record_if_over=None):
self.record_if_over = record_if_over
self.name = name
def __call__(self, func):
name = self.name or func.__name__
def f(*args, **kwargs):
request = get_request()
timing = request.start_timing(name=name)
try:
return func(*args, **kwargs)
finally:
duration = request.stop_timing(timing)
if self.record_if_over and duration > self.record_if_over:
# timings will be displayed in the traceback part of the error.
timings = request.timings # noqa pylint: disable=unused-variable
get_publisher().record_error(_('%s is taking too long') % name)
return f