Skip to content

Commit 0b597f2

Browse files
authored
Merge branch 'master' into release
2 parents 4965722 + 041ad12 commit 0b597f2

File tree

6 files changed

+113
-207
lines changed

6 files changed

+113
-207
lines changed

.github/workflows/pylint.yml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
name: Pylint
2+
3+
on: [push]
4+
5+
jobs:
6+
build:
7+
runs-on: ubuntu-latest
8+
strategy:
9+
matrix:
10+
python-version: ["3.8", "3.9", "3.10"]
11+
steps:
12+
- uses: actions/checkout@v4
13+
- name: Set up Python ${{ matrix.python-version }}
14+
uses: actions/setup-python@v3
15+
with:
16+
python-version: ${{ matrix.python-version }}
17+
- name: Install dependencies
18+
run: |
19+
python -m pip install --upgrade pip
20+
pip install pylint
21+
- name: Analysing the code with pylint
22+
run: |
23+
pylint $(git ls-files '*.py')
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: 69 additions & 185 deletions
Original file line numberDiff line numberDiff line change
@@ -1,176 +1,72 @@
1-
from __future__ import absolute_import, division, unicode_literals
2-
3-
from colorsys import hsv_to_rgb
41
import cProfile
52
import inspect
6-
import os
3+
from io import StringIO
74
from pstats import Stats
8-
from six import PY2
95

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
109
from django.urls import resolve
1110
from django.utils.translation import gettext_lazy as _
12-
from django.utils.safestring import mark_safe
13-
try:
14-
from django.utils.six.moves import cStringIO
15-
except ImportError:
16-
from io import StringIO as cStringIO
17-
from debug_toolbar.panels import Panel
1811
from django.views.generic.base import View
19-
2012
from line_profiler import LineProfiler, show_func
2113

2214
from . import signals
2315

2416

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

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

14440
def line_stats_text(self):
14541
if self._line_stats_text is None:
14642
lstats = self.statobj.line_stats
14743
if self.func in lstats.timings:
148-
out = cStringIO()
44+
out = StringIO()
14945
fn, lineno, name = self.func
15046
try:
151-
show_func(fn,
152-
lineno,
153-
name,
154-
lstats.timings[self.func],
155-
lstats.unit, stream=out)
47+
show_func(fn, lineno, name, lstats.timings[self.func], lstats.unit, stream=out)
15648
self._line_stats_text = out.getvalue()
15749
except ZeroDivisionError:
158-
self._line_stats_text = ("There was a ZeroDivisionError, "
159-
"total_time was probably zero")
50+
self._line_stats_text = 'There was a ZeroDivisionError, total_time was probably zero'
16051
else:
16152
self._line_stats_text = False
16253
return self._line_stats_text
16354

16455

16556
class ProfilingPanel(Panel):
166-
"""
167-
Panel that displays profiling information.
168-
"""
169-
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')
17064

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

173-
def _unwrap_closure_and_profile(self, func):
69+
def _unwrap_closure_and_profile(self, func) -> None:
17470
if not hasattr(func, '__code__') or func in self.added:
17571
return
17672

@@ -179,99 +75,87 @@ def _unwrap_closure_and_profile(self, func):
17975
self.line_profiler.add_function(func)
18076
for subfunc in getattr(func, 'profile_additional', []):
18177
self._unwrap_closure_and_profile(subfunc)
182-
if PY2:
183-
func_closure = func.func_closure
184-
else:
185-
func_closure = func.__closure__
18678

187-
if func_closure:
79+
if func_closure := func.__closure__:
18880
for cell in func_closure:
18981
target = cell.cell_contents
19082
if hasattr(target, '__code__'):
191-
self._unwrap_closure_and_profile(cell.cell_contents)
83+
self._unwrap_closure_and_profile(target)
19284
if inspect.isclass(target) and View in inspect.getmro(target):
19385
for name, value in inspect.getmembers(target):
194-
if not name.startswith('__') and (
195-
inspect.ismethod(value) or
196-
inspect.isfunction(value)
197-
):
86+
if not name.startswith('__') and (inspect.ismethod(value) or inspect.isfunction(value)):
19887
self._unwrap_closure_and_profile(value)
19988

20089
def process_request(self, request):
201-
view_func, view_args, view_kwargs = resolve(request.path)
202-
self.view_func = view_func
90+
self.view_func, view_args, view_kwargs = resolve(request.path)
20391
self.profiler = cProfile.Profile()
20492
self.line_profiler = LineProfiler()
20593
self.added = set()
20694
self._unwrap_closure_and_profile(self.view_func)
207-
signals.profiler_setup.send(sender=self,
208-
profiler=self.line_profiler,
209-
view_func=view_func,
210-
view_args=view_args,
211-
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+
)
212102
self.line_profiler.enable_by_count()
213103
out = self.profiler.runcall(super().process_request, request)
214104
self.line_profiler.disable_by_count()
215105
return out
216106

217-
def add_node(self, func_list, func, max_depth, cum_time=0.1):
218-
"""
219-
add_node does a depth first traversal of the call graph, appending a
220-
FunctionCall object to func_list, so that the Django template only
221-
has to do a single for loop over func_list that can render a tree
222-
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.
223110
224-
Parameters:
225-
func_list is an array that will have a FunctionCall for each call
226-
added to it
111+
Parameters
112+
----------
113+
func_list is an array that will have a FunctionCall for each call added to it
227114
func is a FunctionCall object that will have all its callees added
228115
max_depth is the maximum depth we should recurse
229-
cum_time is the minimum cum_time a function should have to be
230-
included in the output
116+
cum_time is the minimum cum_time a function should have to be included in the output
117+
231118
"""
232119
func_list.append(func)
233120
func.has_subfuncs = False
234-
# this function somewhat dangerously relies on FunctionCall to set its
235-
# subfuncs' depth argument correctly
121+
# this function somewhat dangerously relies on FunctionCall to set its subfuncs' depth argument correctly
236122
if func.depth >= max_depth:
237123
return
238124

239-
# func.subfuncs returns FunctionCall objects
240125
subs = sorted(func.subfuncs(), key=FunctionCall.cumtime, reverse=True)
241126
for subfunc in subs:
242-
# a sub function is important if it takes a long time or it has
243-
# line_stats
244-
if (subfunc.cumtime() >= cum_time or
245-
(hasattr(self.stats, 'line_stats') and
246-
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+
):
247134
func.has_subfuncs = True
248135
self.add_node(
249136
func_list=func_list,
250137
func=subfunc,
251138
max_depth=max_depth,
252-
cum_time=subfunc.cumtime()/16)
139+
cum_time=subfunc.cumtime() / dt_settings.get_config()['PROFILER_THRESHOLD_RATIO'] / 2,
140+
)
253141

254142
def generate_stats(self, request, response):
255143
if not hasattr(self, 'profiler'):
256-
return None
144+
return
257145
# Could be delayed until the panel content is requested (perf. optim.)
258146
self.profiler.create_stats()
259-
self.stats = DjangoDebugToolbarStats(self.profiler)
147+
self.stats = Stats(self.profiler)
260148
self.stats.line_stats = self.line_profiler.get_stats()
261149
self.stats.calc_callees()
262150

263151
func_list = []
264-
root_func = self.stats.get_root_func(self.view_func)
265-
266-
if root_func is not None:
267-
root_node = FunctionCall(statobj=self.stats,
268-
func=root_func,
269-
depth=0)
152+
if root_func := cProfile.label(self.view_func.__code__):
153+
root_node = FunctionCall(statobj=self.stats, func=root_func, depth=0)
270154
self.add_node(
271155
func_list=func_list,
272156
func=root_node,
273-
max_depth=10,
274-
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'],
275159
)
276160
# else:
277161
# 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()

0 commit comments

Comments
 (0)