feat: finer frappe Recorder control with decorator (#19220)

Currently frappe recorder can be enabled globally and profiles every
request. This is often way too much info. If you already know where
problem lies you use this decorator sparingly to only profile relevant
functions.

Usage:
```py
from frappe.recorder import record_queries

@record_queries
def sus_slow_function():
    frappe.db.sql("select everything from everywhere")
```
This commit is contained in:
Ankush Menat 2022-12-09 14:18:49 +05:30 committed by GitHub
parent 442ba5dbf2
commit ec3f705e4f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 49 additions and 7 deletions

View file

@ -1,11 +1,13 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
import datetime
import functools
import inspect
import json
import re
import time
from collections import Counter
from typing import Callable
import sqlparse
@ -61,9 +63,9 @@ def get_current_stack_frames():
pass
def record():
def record(force=False):
if __debug__:
if frappe.cache().get_value(RECORDER_INTERCEPT_FLAG):
if frappe.cache().get_value(RECORDER_INTERCEPT_FLAG) or force:
frappe.local._recorder = Recorder()
@ -78,11 +80,19 @@ class Recorder:
self.uuid = frappe.generate_hash(length=10)
self.time = datetime.datetime.now()
self.calls = []
self.path = frappe.request.path
self.cmd = frappe.local.form_dict.cmd or ""
self.method = frappe.request.method
self.headers = dict(frappe.local.request.headers)
self.form_dict = frappe.local.form_dict
if frappe.request:
self.path = frappe.request.path
self.cmd = frappe.local.form_dict.cmd or ""
self.method = frappe.request.method
self.headers = dict(frappe.local.request.headers)
self.form_dict = frappe.local.form_dict
else:
self.path = None
self.cmd = None
self.method = None
self.headers = None
self.form_dict = None
_patch()
def register(self, data):
@ -125,6 +135,10 @@ def _patch():
frappe.db.sql = sql
def _unpatch():
frappe.db.sql = frappe.db._sql
def do_not_record(function):
def wrapper(*args, **kwargs):
if hasattr(frappe.local, "_recorder"):
@ -189,3 +203,19 @@ def export_data(*args, **kwargs):
def delete(*args, **kwargs):
frappe.cache().delete_value(RECORDER_REQUEST_SPARSE_HASH)
frappe.cache().delete_value(RECORDER_REQUEST_HASH)
def record_queries(func: Callable):
"""Decorator to profile a specific function using recorder."""
@functools.wraps(func)
def wrapped(*args, **kwargs):
record(force=True)
frappe.local._recorder.path = f"Function call: {func.__module__}.{func.__qualname__}"
ret = func(*args, **kwargs)
dump()
_unpatch()
print("Recorded queries, open recorder to view them.")
return ret
return wrapped

View file

@ -123,3 +123,15 @@ class TestRecorder(FrappeTestCase):
def test_error_page_rendering(self):
content = get_response_content("error")
self.assertIn("Error", content)
class TestRecorderDeco(FrappeTestCase):
def test_recorder_flag(self):
frappe.recorder.delete()
@frappe.recorder.record_queries
def test():
frappe.get_all("User")
test()
self.assertTrue(frappe.recorder.get())