1
- from __future__ import absolute_import , division , unicode_literals
2
-
3
- from colorsys import hsv_to_rgb
4
1
import cProfile
5
2
import inspect
6
- import os
3
+ from io import StringIO
7
4
from pstats import Stats
8
- from six import PY2
9
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
10
9
from django .urls import resolve
11
10
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
18
11
from django .views .generic .base import View
19
-
20
12
from line_profiler import LineProfiler , show_func
21
13
22
14
from . import signals
23
15
24
16
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
59
21
self ._line_stats_text = None
60
22
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
-
97
23
def subfuncs (self ):
98
- i = 0
99
24
h , s , _ = self .hsv
100
25
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 ):
103
27
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
+ )
143
39
144
40
def line_stats_text (self ):
145
41
if self ._line_stats_text is None :
146
42
lstats = self .statobj .line_stats
147
43
if self .func in lstats .timings :
148
- out = cStringIO ()
44
+ out = StringIO ()
149
45
fn , lineno , name = self .func
150
46
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 )
156
48
self ._line_stats_text = out .getvalue ()
157
49
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'
160
51
else :
161
52
self ._line_stats_text = False
162
53
return self ._line_stats_text
163
54
164
55
165
56
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' )
170
64
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'
172
68
173
- def _unwrap_closure_and_profile (self , func ):
69
+ def _unwrap_closure_and_profile (self , func ) -> None :
174
70
if not hasattr (func , '__code__' ) or func in self .added :
175
71
return
176
72
@@ -179,99 +75,87 @@ def _unwrap_closure_and_profile(self, func):
179
75
self .line_profiler .add_function (func )
180
76
for subfunc in getattr (func , 'profile_additional' , []):
181
77
self ._unwrap_closure_and_profile (subfunc )
182
- if PY2 :
183
- func_closure = func .func_closure
184
- else :
185
- func_closure = func .__closure__
186
78
187
- if func_closure :
79
+ if func_closure := func . __closure__ :
188
80
for cell in func_closure :
189
81
target = cell .cell_contents
190
82
if hasattr (target , '__code__' ):
191
- self ._unwrap_closure_and_profile (cell . cell_contents )
83
+ self ._unwrap_closure_and_profile (target )
192
84
if inspect .isclass (target ) and View in inspect .getmro (target ):
193
85
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 )):
198
87
self ._unwrap_closure_and_profile (value )
199
88
200
89
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 )
203
91
self .profiler = cProfile .Profile ()
204
92
self .line_profiler = LineProfiler ()
205
93
self .added = set ()
206
94
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
+ )
212
102
self .line_profiler .enable_by_count ()
213
103
out = self .profiler .runcall (super ().process_request , request )
214
104
self .line_profiler .disable_by_count ()
215
105
return out
216
106
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.
223
110
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
227
114
func is a FunctionCall object that will have all its callees added
228
115
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
+
231
118
"""
232
119
func_list .append (func )
233
120
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
236
122
if func .depth >= max_depth :
237
123
return
238
124
239
- # func.subfuncs returns FunctionCall objects
240
125
subs = sorted (func .subfuncs (), key = FunctionCall .cumtime , reverse = True )
241
126
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
+ ):
247
134
func .has_subfuncs = True
248
135
self .add_node (
249
136
func_list = func_list ,
250
137
func = subfunc ,
251
138
max_depth = max_depth ,
252
- cum_time = subfunc .cumtime ()/ 16 )
139
+ cum_time = subfunc .cumtime () / dt_settings .get_config ()['PROFILER_THRESHOLD_RATIO' ] / 2 ,
140
+ )
253
141
254
142
def generate_stats (self , request , response ):
255
143
if not hasattr (self , 'profiler' ):
256
- return None
144
+ return
257
145
# Could be delayed until the panel content is requested (perf. optim.)
258
146
self .profiler .create_stats ()
259
- self .stats = DjangoDebugToolbarStats (self .profiler )
147
+ self .stats = Stats (self .profiler )
260
148
self .stats .line_stats = self .line_profiler .get_stats ()
261
149
self .stats .calc_callees ()
262
150
263
151
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 )
270
154
self .add_node (
271
155
func_list = func_list ,
272
156
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' ],
275
159
)
276
160
# else:
277
161
# what should we do if we didn't detect a root function? It's not
0 commit comments