1
- from colorsys import hsv_to_rgb
2
1
import cProfile
3
2
import inspect
4
- import os
3
+ from io import StringIO
5
4
from pstats import Stats
6
5
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
7
9
from django .urls import resolve
8
10
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
12
11
from django .views .generic .base import View
13
-
14
12
from line_profiler import LineProfiler , show_func
15
13
16
14
from . import signals
17
15
18
16
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
53
21
self ._line_stats_text = None
54
22
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
-
91
23
def subfuncs (self ):
92
- i = 0
93
24
h , s , _ = self .hsv
94
25
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 ):
97
27
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
+ )
137
39
138
40
def line_stats_text (self ):
139
41
if self ._line_stats_text is None :
@@ -142,29 +44,29 @@ def line_stats_text(self):
142
44
out = StringIO ()
143
45
fn , lineno , name = self .func
144
46
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 )
150
48
self ._line_stats_text = out .getvalue ()
151
49
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'
154
51
else :
155
52
self ._line_stats_text = False
156
53
return self ._line_stats_text
157
54
158
55
159
56
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' )
164
64
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'
166
68
167
- def _unwrap_closure_and_profile (self , func ):
69
+ def _unwrap_closure_and_profile (self , func ) -> None :
168
70
if not hasattr (func , '__code__' ) or func in self .added :
169
71
return
170
72
@@ -173,96 +75,87 @@ def _unwrap_closure_and_profile(self, func):
173
75
self .line_profiler .add_function (func )
174
76
for subfunc in getattr (func , 'profile_additional' , []):
175
77
self ._unwrap_closure_and_profile (subfunc )
176
- func_closure = func .__closure__
177
78
178
- if func_closure :
79
+ if func_closure := func . __closure__ :
179
80
for cell in func_closure :
180
81
target = cell .cell_contents
181
82
if hasattr (target , '__code__' ):
182
- self ._unwrap_closure_and_profile (cell . cell_contents )
83
+ self ._unwrap_closure_and_profile (target )
183
84
if inspect .isclass (target ) and View in inspect .getmro (target ):
184
85
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 )):
189
87
self ._unwrap_closure_and_profile (value )
190
88
191
89
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 )
194
91
self .profiler = cProfile .Profile ()
195
92
self .line_profiler = LineProfiler ()
196
93
self .added = set ()
197
94
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
+ )
203
102
self .line_profiler .enable_by_count ()
204
103
out = self .profiler .runcall (super ().process_request , request )
205
104
self .line_profiler .disable_by_count ()
206
105
return out
207
106
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.
214
110
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
218
114
func is a FunctionCall object that will have all its callees added
219
115
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
+
222
118
"""
223
119
func_list .append (func )
224
120
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
227
122
if func .depth >= max_depth :
228
123
return
229
124
230
- # func.subfuncs returns FunctionCall objects
231
125
subs = sorted (func .subfuncs (), key = FunctionCall .cumtime , reverse = True )
232
126
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
+ ):
238
134
func .has_subfuncs = True
239
135
self .add_node (
240
136
func_list = func_list ,
241
137
func = subfunc ,
242
138
max_depth = max_depth ,
243
- cum_time = subfunc .cumtime ()/ 16 )
139
+ cum_time = subfunc .cumtime () / dt_settings .get_config ()['PROFILER_THRESHOLD_RATIO' ] / 2 ,
140
+ )
244
141
245
142
def generate_stats (self , request , response ):
246
143
if not hasattr (self , 'profiler' ):
247
- return None
144
+ return
248
145
# Could be delayed until the panel content is requested (perf. optim.)
249
146
self .profiler .create_stats ()
250
- self .stats = DjangoDebugToolbarStats (self .profiler )
147
+ self .stats = Stats (self .profiler )
251
148
self .stats .line_stats = self .line_profiler .get_stats ()
252
149
self .stats .calc_callees ()
253
150
254
151
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 )
261
154
self .add_node (
262
155
func_list = func_list ,
263
156
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' ],
266
159
)
267
160
# else:
268
161
# what should we do if we didn't detect a root function? It's not
0 commit comments