Skip to content

Commit 844ae2c

Browse files
committed
cleanup
1 parent d8595d6 commit 844ae2c

File tree

5 files changed

+87
-193
lines changed

5 files changed

+87
-193
lines changed
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
21
def profile_additional(*subfuncs):
32
def inner(func):
43
func.profile_additional = subfuncs
54
return func
5+
66
return inner

debug_toolbar_line_profiler/panel.py

Lines changed: 68 additions & 175 deletions
Original file line numberDiff line numberDiff line change
@@ -1,139 +1,41 @@
1-
from colorsys import hsv_to_rgb
21
import cProfile
32
import inspect
4-
import os
3+
from io import StringIO
54
from pstats import Stats
65

6+
from debug_toolbar import settings as dt_settings
7+
from debug_toolbar.panels import Panel
8+
from debug_toolbar.panels.profiling import FunctionCall as DjDTFunctionCall
79
from django.urls import resolve
810
from django.utils.translation import gettext_lazy as _
9-
from django.utils.safestring import mark_safe
10-
from io import StringIO
11-
from debug_toolbar.panels import Panel
1211
from django.views.generic.base import View
13-
1412
from line_profiler import LineProfiler, show_func
1513

1614
from . import signals
1715

1816

19-
class DjangoDebugToolbarStats(Stats):
20-
__root = None
21-
22-
def get_root_func(self, view_func):
23-
if self.__root is None:
24-
filename = view_func.__code__.co_filename
25-
firstlineno = view_func.__code__.co_firstlineno
26-
for func, (_, __, ___, ____, callers) in self.stats.items():
27-
if (len(callers) >= 0
28-
and func[0] == filename
29-
and func[1] == firstlineno):
30-
self.__root = func
31-
break
32-
return self.__root
33-
34-
35-
class FunctionCall:
36-
"""
37-
The FunctionCall object is a helper object that encapsulates some of the
38-
complexity of working with pstats/cProfile objects
39-
40-
"""
41-
def __init__(self, statobj, func, depth=0, stats=None,
42-
_id="0", parent_ids=[], hsv=(0, 0.5, 1)):
43-
self.statobj = statobj
44-
self.func = func
45-
if stats:
46-
self.stats = stats
47-
else:
48-
self.stats = statobj.stats[func][:4]
49-
self.depth = depth
50-
self.id = _id
51-
self.parent_ids = parent_ids
52-
self.hsv = hsv
17+
class FunctionCall(DjDTFunctionCall):
18+
def __init__(self, *args, **kwargs) -> None:
19+
super().__init__(*args, **kwargs)
20+
self.has_subfuncs = False
5321
self._line_stats_text = None
5422

55-
def parent_classes(self):
56-
return self.parent_classes
57-
58-
def background(self):
59-
r, g, b = hsv_to_rgb(*self.hsv)
60-
return 'rgb(%f%%,%f%%,%f%%)' % (r * 100, g * 100, b * 100)
61-
62-
def func_std_string(self): # match what old profile produced
63-
func_name = self.func
64-
if func_name[:2] == ('~', 0):
65-
# special case for built-in functions
66-
name = func_name[2]
67-
if name.startswith('<') and name.endswith('>'):
68-
return '{%s}' % name[1:-1]
69-
else:
70-
return name
71-
else:
72-
file_name, line_num, method = self.func
73-
idx = file_name.find('/site-packages/')
74-
if idx > -1:
75-
file_name = file_name[(idx + 14):]
76-
77-
file_items = file_name.rsplit(os.sep, 1)
78-
file_path, file_name = file_items if len(file_items) > 1 else [
79-
None, file_name]
80-
81-
return mark_safe(
82-
'<span class="path">{0}/</span>'
83-
'<span class="file">{1}</span>'
84-
' in <span class="func">{3}</span>'
85-
'(<span class="lineno">{2}</span>)'.format(
86-
file_path,
87-
file_name,
88-
line_num,
89-
method))
90-
9123
def subfuncs(self):
92-
i = 0
9324
h, s, _ = self.hsv
9425
count = len(self.statobj.all_callees[self.func])
95-
for func, stats in self.statobj.all_callees[self.func].items():
96-
i += 1
26+
for i, (func, stats) in enumerate(self.statobj.all_callees[self.func].items(), 1):
9727
h1 = h + (i / count) / (self.depth + 1)
98-
if stats[3] == 0 or self.stats[3] == 0:
99-
s1 = 0
100-
else:
101-
s1 = s * (stats[3] / self.stats[3])
102-
yield FunctionCall(self.statobj,
103-
func,
104-
self.depth + 1,
105-
stats=stats,
106-
_id=self.id + '_' + str(i),
107-
parent_ids=self.parent_ids + [self.id],
108-
hsv=(h1, s1, 1))
109-
110-
def count(self):
111-
return self.stats[1]
112-
113-
def tottime(self):
114-
return self.stats[2]
115-
116-
def cumtime(self):
117-
return self.stats[3]
118-
119-
def tottime_per_call(self):
120-
cc, nc, tt, ct = self.stats
121-
122-
if nc == 0:
123-
return 0
124-
125-
return tt / nc
126-
127-
def cumtime_per_call(self):
128-
cc, _, __, ct = self.stats
129-
130-
if cc == 0:
131-
return 0
132-
133-
return ct / cc
134-
135-
def indent(self):
136-
return 16 * self.depth
28+
s1 = 0 if stats[3] == 0 or self.stats[3] == 0 else s * (stats[3] / self.stats[3])
29+
30+
yield FunctionCall(
31+
self.statobj,
32+
func,
33+
self.depth + 1,
34+
stats=stats,
35+
id=f'{self.id}_{i}',
36+
parent_ids=[*self.parent_ids, self.id],
37+
hsv=(h1, s1, 1),
38+
)
13739

13840
def line_stats_text(self):
13941
if self._line_stats_text is None:
@@ -142,29 +44,29 @@ def line_stats_text(self):
14244
out = StringIO()
14345
fn, lineno, name = self.func
14446
try:
145-
show_func(fn,
146-
lineno,
147-
name,
148-
lstats.timings[self.func],
149-
lstats.unit, stream=out)
47+
show_func(fn, lineno, name, lstats.timings[self.func], lstats.unit, stream=out)
15048
self._line_stats_text = out.getvalue()
15149
except ZeroDivisionError:
152-
self._line_stats_text = ("There was a ZeroDivisionError, "
153-
"total_time was probably zero")
50+
self._line_stats_text = 'There was a ZeroDivisionError, total_time was probably zero'
15451
else:
15552
self._line_stats_text = False
15653
return self._line_stats_text
15754

15855

15956
class ProfilingPanel(Panel):
160-
"""
161-
Panel that displays profiling information.
162-
"""
163-
title = _('Profiling')
57+
"""Panel that displays profiling information."""
58+
59+
capture_project_code = dt_settings.get_config()['PROFILER_CAPTURE_PROJECT_CODE']
60+
61+
@property
62+
def title(self) -> str:
63+
return _('Profiling')
16464

165-
template = 'debug_toolbar_line_profiler/panels/profiling.html'
65+
@property
66+
def template(self) -> str:
67+
return 'debug_toolbar_line_profiler/panels/profiling.html'
16668

167-
def _unwrap_closure_and_profile(self, func):
69+
def _unwrap_closure_and_profile(self, func) -> None:
16870
if not hasattr(func, '__code__') or func in self.added:
16971
return
17072

@@ -173,96 +75,87 @@ def _unwrap_closure_and_profile(self, func):
17375
self.line_profiler.add_function(func)
17476
for subfunc in getattr(func, 'profile_additional', []):
17577
self._unwrap_closure_and_profile(subfunc)
176-
func_closure = func.__closure__
17778

178-
if func_closure:
79+
if func_closure := func.__closure__:
17980
for cell in func_closure:
18081
target = cell.cell_contents
18182
if hasattr(target, '__code__'):
182-
self._unwrap_closure_and_profile(cell.cell_contents)
83+
self._unwrap_closure_and_profile(target)
18384
if inspect.isclass(target) and View in inspect.getmro(target):
18485
for name, value in inspect.getmembers(target):
185-
if not name.startswith('__') and (
186-
inspect.ismethod(value) or
187-
inspect.isfunction(value)
188-
):
86+
if not name.startswith('__') and (inspect.ismethod(value) or inspect.isfunction(value)):
18987
self._unwrap_closure_and_profile(value)
19088

19189
def process_request(self, request):
192-
view_func, view_args, view_kwargs = resolve(request.path)
193-
self.view_func = view_func
90+
self.view_func, view_args, view_kwargs = resolve(request.path)
19491
self.profiler = cProfile.Profile()
19592
self.line_profiler = LineProfiler()
19693
self.added = set()
19794
self._unwrap_closure_and_profile(self.view_func)
198-
signals.profiler_setup.send(sender=self,
199-
profiler=self.line_profiler,
200-
view_func=view_func,
201-
view_args=view_args,
202-
view_kwargs=view_kwargs)
95+
signals.profiler_setup.send(
96+
sender=self,
97+
profiler=self.line_profiler,
98+
view_func=self.view_func,
99+
view_args=view_args,
100+
view_kwargs=view_kwargs,
101+
)
203102
self.line_profiler.enable_by_count()
204103
out = self.profiler.runcall(super().process_request, request)
205104
self.line_profiler.disable_by_count()
206105
return out
207106

208-
def add_node(self, func_list, func, max_depth, cum_time=0.1):
209-
"""
210-
add_node does a depth first traversal of the call graph, appending a
211-
FunctionCall object to func_list, so that the Django template only
212-
has to do a single for loop over func_list that can render a tree
213-
structure
107+
def add_node(self, func_list: list, func: FunctionCall, max_depth: int, cum_time: float):
108+
"""add_node does a depth first traversal of the call graph, appending a FunctionCall object to func_list, so
109+
that the Django template only has to do a single for loop over func_list that can render a tree structure.
214110
215-
Parameters:
216-
func_list is an array that will have a FunctionCall for each call
217-
added to it
111+
Parameters
112+
----------
113+
func_list is an array that will have a FunctionCall for each call added to it
218114
func is a FunctionCall object that will have all its callees added
219115
max_depth is the maximum depth we should recurse
220-
cum_time is the minimum cum_time a function should have to be
221-
included in the output
116+
cum_time is the minimum cum_time a function should have to be included in the output
117+
222118
"""
223119
func_list.append(func)
224120
func.has_subfuncs = False
225-
# this function somewhat dangerously relies on FunctionCall to set its
226-
# subfuncs' depth argument correctly
121+
# this function somewhat dangerously relies on FunctionCall to set its subfuncs' depth argument correctly
227122
if func.depth >= max_depth:
228123
return
229124

230-
# func.subfuncs returns FunctionCall objects
231125
subs = sorted(func.subfuncs(), key=FunctionCall.cumtime, reverse=True)
232126
for subfunc in subs:
233-
# a sub function is important if it takes a long time or it has
234-
# line_stats
235-
if (subfunc.cumtime() >= cum_time or
236-
(hasattr(self.stats, 'line_stats') and
237-
subfunc.func in self.stats.line_stats.timings)):
127+
# a sub function is important if it takes a long time or it has line_stats
128+
is_project_code = bool(self.capture_project_code and subfunc.is_project_func())
129+
if (
130+
subfunc.cumtime() >= cum_time
131+
or (is_project_code and subfunc.cumtime() > 0)
132+
or (hasattr(self.stats, 'line_stats') and subfunc.func in self.stats.line_stats.timings)
133+
):
238134
func.has_subfuncs = True
239135
self.add_node(
240136
func_list=func_list,
241137
func=subfunc,
242138
max_depth=max_depth,
243-
cum_time=subfunc.cumtime()/16)
139+
cum_time=subfunc.cumtime() / dt_settings.get_config()['PROFILER_THRESHOLD_RATIO'] / 2,
140+
)
244141

245142
def generate_stats(self, request, response):
246143
if not hasattr(self, 'profiler'):
247-
return None
144+
return
248145
# Could be delayed until the panel content is requested (perf. optim.)
249146
self.profiler.create_stats()
250-
self.stats = DjangoDebugToolbarStats(self.profiler)
147+
self.stats = Stats(self.profiler)
251148
self.stats.line_stats = self.line_profiler.get_stats()
252149
self.stats.calc_callees()
253150

254151
func_list = []
255-
root_func = self.stats.get_root_func(self.view_func)
256-
257-
if root_func is not None:
258-
root_node = FunctionCall(statobj=self.stats,
259-
func=root_func,
260-
depth=0)
152+
if root_func := cProfile.label(self.view_func.__code__):
153+
root_node = FunctionCall(statobj=self.stats, func=root_func, depth=0)
261154
self.add_node(
262155
func_list=func_list,
263156
func=root_node,
264-
max_depth=10,
265-
cum_time=root_node.cumtime() / 8
157+
max_depth=dt_settings.get_config()['PROFILER_MAX_DEPTH'],
158+
cum_time=root_node.cumtime() / dt_settings.get_config()['PROFILER_THRESHOLD_RATIO'],
266159
)
267160
# else:
268161
# what should we do if we didn't detect a root function? It's not
Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
11
import django.dispatch
22

3-
43
profiler_setup = django.dispatch.Signal()

debug_toolbar_line_profiler/templates/debug_toolbar_line_profiler/panels/profiling.html

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
{% load i18n %}{% load static %}
2-
<table width="100%">
1+
{% load i18n %}
2+
<table>
33
<thead>
44
<tr>
55
<th>{% trans "Call" %}</th>
@@ -12,16 +12,15 @@
1212
</thead>
1313
<tbody>
1414
{% for call in func_list %}
15-
<!-- style="background:{{ call.background }}" -->
16-
<tr class="djDebugProfileRow{% for parent_id in call.parent_ids %} djToggleDetails_{{ parent_id }}{% endfor %}" depth="{{ call.depth }}" id="_{{ call.id }}">
15+
<tr class="djdt-profile-row {% if call.is_project_func %}djdt-highlighted {% endif %} {% for parent_id in call.parent_ids %} djToggleDetails_{{ parent_id }}{% endfor %}" id="profilingMain_{{ call.id }}">
1716
<td>
18-
<div style="padding-left: {{ call.indent }}px;">
17+
<div data-djdt-styles="paddingLeft:{{ call.indent }}px">
1918
{% if call.has_subfuncs %}
20-
<a class="djProfileToggleDetails djToggleSwitch" data-toggle-id="{{ call.id }}" data-toggle-open="+" data-toggle-close="-" data-toggle-name="" href="javascript:void(0)">-</a>
19+
<button type="button" class="djProfileToggleDetails djToggleSwitch" data-toggle-name="profilingMain" data-toggle-id="{{ call.id }}">-</button>
2120
{% else %}
2221
<span class="djNoToggleSwitch"></span>
2322
{% endif %}
24-
<span class="stack">{{ call.func_std_string }}</span>
23+
<span class="djdt-stack">{{ call.func_std_string }}</span>
2524
</div>
2625
</td>
2726
<td>{{ call.cumtime|floatformat:3 }}</td>
@@ -33,7 +32,7 @@
3332
{% if call.line_stats_text %}
3433
<tr class="djToggleDetails_{{ call.id }}{% for parent_id in call.parent_ids %} djToggleDetails_{{ parent_id }}{% endfor %}">
3534
<td colspan="6">
36-
<div style="padding-left: {{ call.indent }}px;"><pre>{{ call.line_stats_text }}</pre></div>
35+
<div style="padding-left:{{ call.indent }}px"><pre>{{ call.line_stats_text }}</pre></div>
3736
</td>
3837
</tr>
3938
{% endif %}

0 commit comments

Comments
 (0)