feat: profile requests using recorder

WIP:
- [x] Basic working feature
- [ ] Make this optional, this has insanely high overhead.
- [ ] Specify requests/function filter to profile/record. This will
  allow better recording in production sites.
- [ ] Make SQL profiling optional too
This commit is contained in:
Ankush Menat 2024-02-01 22:16:06 +05:30
parent c02f5d5876
commit 3c183344aa
3 changed files with 29 additions and 2 deletions

View file

@ -20,7 +20,9 @@
"section_break_sgro",
"form_dict",
"section_break_9jhm",
"sql_queries"
"sql_queries",
"section_break_optn",
"profile"
],
"fields": [
{
@ -107,6 +109,16 @@
"fieldtype": "Data",
"hidden": 1,
"label": "Event Type"
},
{
"fieldname": "section_break_optn",
"fieldtype": "Section Break"
},
{
"fieldname": "profile",
"fieldtype": "Code",
"label": "cProfile Output",
"read_only": 1
}
],
"hide_toolbar": 1,
@ -114,7 +126,7 @@
"index_web_pages_for_search": 1,
"is_virtual": 1,
"links": [],
"modified": "2024-01-03 16:45:47.110048",
"modified": "2024-02-01 22:13:26.505174",
"modified_by": "Administrator",
"module": "Core",
"name": "Recorder",

View file

@ -24,6 +24,7 @@ class Recorder(Document):
method: DF.Literal["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]
number_of_queries: DF.Int
path: DF.Data | None
profile: DF.Code | None
request_headers: DF.Code | None
sql_queries: DF.Table[RecorderQuery]
time: DF.Datetime | None

View file

@ -1,9 +1,12 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
import cProfile
import datetime
import functools
import inspect
import io
import json
import pstats
import re
import time
from collections import Counter
@ -148,6 +151,8 @@ class Recorder:
self.uuid = frappe.generate_hash(length=10)
self.time = datetime.datetime.now()
self.calls = []
self.profiler = cProfile.Profile()
self.profiler.enable()
if frappe.request:
self.path = frappe.request.path
self.cmd = frappe.local.form_dict.cmd or ""
@ -176,6 +181,13 @@ class Recorder:
self.calls.append(data)
def dump(self):
self.profiler.disable()
profiler_output = io.StringIO()
pstats.Stats(self.profiler, stream=profiler_output).strip_dirs().sort_stats(
"cumulative"
).print_stats()
request_data = {
"uuid": self.uuid,
"path": self.path,
@ -197,6 +209,8 @@ class Recorder:
request_data["calls"] = self.calls
request_data["headers"] = self.headers
request_data["form_dict"] = self.form_dict
request_data["profile"] = profiler_output.getvalue()
profiler_output.close()
frappe.cache.hset(RECORDER_REQUEST_HASH, self.uuid, request_data)