impuls/lib/python3.11/site-packages/debug_toolbar/panels/profiling.py

186 lines
5.5 KiB
Python

import cProfile
import os
from colorsys import hsv_to_rgb
from pstats import Stats
from django.conf import settings
from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _
from debug_toolbar import settings as dt_settings
from debug_toolbar.panels import Panel
class FunctionCall:
def __init__(
self, statobj, func, depth=0, stats=None, id=0, parent_ids=[], hsv=(0, 0.5, 1)
):
self.statobj = statobj
self.func = func
if stats:
self.stats = stats
else:
self.stats = statobj.stats[func][:4]
self.depth = depth
self.id = id
self.parent_ids = parent_ids
self.hsv = hsv
def parent_classes(self):
return self.parent_classes
def background(self):
r, g, b = hsv_to_rgb(*self.hsv)
return f"rgb({r * 100:f}%,{g * 100:f}%,{b * 100:f}%)"
def is_project_func(self):
"""
Check if the function is from the project code.
Project code is identified by the BASE_DIR setting
which is used in Django projects by default.
"""
if hasattr(settings, "BASE_DIR"):
file_name, _, _ = self.func
return (
str(settings.BASE_DIR) in file_name
and "/site-packages/" not in file_name
and "/dist-packages/" not in file_name
)
return None
def func_std_string(self): # match what old profile produced
func_name = self.func
if func_name[:2] == ("~", 0):
# special case for built-in functions
name = func_name[2]
if name.startswith("<") and name.endswith(">"):
return "{%s}" % name[1:-1]
else:
return name
else:
file_name, line_num, method = self.func
idx = file_name.find("/site-packages/")
if idx > -1:
file_name = file_name[(idx + 14) :]
split_path = file_name.rsplit(os.sep, 1)
if len(split_path) > 1:
file_path, file_name = file_name.rsplit(os.sep, 1)
else:
file_path = "<module>"
return format_html(
'<span class="djdt-path">{0}/</span>'
'<span class="djdt-file">{1}</span>'
' in <span class="djdt-func">{3}</span>'
'(<span class="djdt-lineno">{2}</span>)',
file_path,
file_name,
line_num,
method,
)
def subfuncs(self):
i = 0
h, s, v = self.hsv
count = len(self.statobj.all_callees[self.func])
for func, stats in self.statobj.all_callees[self.func].items():
i += 1
h1 = h + (i / count) / (self.depth + 1)
if stats[3] == 0:
s1 = 0
else:
s1 = s * (stats[3] / self.stats[3])
yield FunctionCall(
self.statobj,
func,
self.depth + 1,
stats=stats,
id=str(self.id) + "_" + str(i),
parent_ids=self.parent_ids + [self.id],
hsv=(h1, s1, 1),
)
def count(self):
return self.stats[1]
def tottime(self):
return self.stats[2]
def cumtime(self):
cc, nc, tt, ct = self.stats
return self.stats[3]
def tottime_per_call(self):
cc, nc, tt, ct = self.stats
if nc == 0:
return 0
return tt / nc
def cumtime_per_call(self):
cc, nc, tt, ct = self.stats
if cc == 0:
return 0
return ct / cc
def indent(self):
return 16 * self.depth
class ProfilingPanel(Panel):
"""
Panel that displays profiling information.
"""
title = _("Profiling")
template = "debug_toolbar/panels/profiling.html"
capture_project_code = dt_settings.get_config()["PROFILER_CAPTURE_PROJECT_CODE"]
def process_request(self, request):
self.profiler = cProfile.Profile()
return self.profiler.runcall(super().process_request, request)
def add_node(self, func_list, func, max_depth, cum_time):
func_list.append(func)
func.has_subfuncs = False
if func.depth < max_depth:
for subfunc in func.subfuncs():
# Always include the user's code
if subfunc.stats[3] >= cum_time or (
self.capture_project_code
and subfunc.is_project_func()
and subfunc.stats[3] > 0
):
func.has_subfuncs = True
self.add_node(func_list, subfunc, max_depth, cum_time)
def generate_stats(self, request, response):
if not hasattr(self, "profiler"):
return None
# Could be delayed until the panel content is requested (perf. optim.)
self.profiler.create_stats()
self.stats = Stats(self.profiler)
self.stats.calc_callees()
root_func = cProfile.label(super().process_request.__code__)
if root_func in self.stats.stats:
root = FunctionCall(self.stats, root_func, depth=0)
func_list = []
cum_time_threshold = (
root.stats[3] / dt_settings.get_config()["PROFILER_THRESHOLD_RATIO"]
)
self.add_node(
func_list,
root,
dt_settings.get_config()["PROFILER_MAX_DEPTH"],
cum_time_threshold,
)
self.record_stats({"func_list": func_list})