Skip to content

Commit be0a433

Browse files
Profiling panel improvements (#1669)
* Give users control over including more profiling data. * The profiling panel will now include more user code. By checking that the code lives in the settings.BASE_DIR directory, we know that the code was likely written by the user and thus more important to a developer when debugging code. * Highlight the project function calls in the profiling panel. * Add setting PROFILER_CAPTURE_PROJECT_CODE. This can be used to disable the attempt to include all project code. This is useful if dependencies are installed within the project. * Fix bug with test_cum_time_threshold profiling. * Include dist-packages in profiling panel docs. Co-authored-by: Matthias Kestenholz <mk@feinheit.ch>
1 parent c88a13d commit be0a433

File tree

9 files changed

+98
-5
lines changed

9 files changed

+98
-5
lines changed

debug_toolbar/panels/profiling.py

+30-4
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from colorsys import hsv_to_rgb
44
from pstats import Stats
55

6+
from django.conf import settings
67
from django.utils.html import format_html
78
from django.utils.translation import gettext_lazy as _
89

@@ -32,6 +33,22 @@ def background(self):
3233
r, g, b = hsv_to_rgb(*self.hsv)
3334
return f"rgb({r * 100:f}%,{g * 100:f}%,{b * 100:f}%)"
3435

36+
def is_project_func(self):
37+
"""
38+
Check if the function is from the project code.
39+
40+
Project code is identified by the BASE_DIR setting
41+
which is used in Django projects by default.
42+
"""
43+
if hasattr(settings, "BASE_DIR"):
44+
file_name, _, _ = self.func
45+
return (
46+
str(settings.BASE_DIR) in file_name
47+
and "/site-packages/" not in file_name
48+
and "/dist-packages/" not in file_name
49+
)
50+
return None
51+
3552
def func_std_string(self): # match what old profile produced
3653
func_name = self.func
3754
if func_name[:2] == ("~", 0):
@@ -123,19 +140,25 @@ class ProfilingPanel(Panel):
123140
title = _("Profiling")
124141

125142
template = "debug_toolbar/panels/profiling.html"
143+
capture_project_code = dt_settings.get_config()["PROFILER_CAPTURE_PROJECT_CODE"]
126144

127145
def process_request(self, request):
128146
self.profiler = cProfile.Profile()
129147
return self.profiler.runcall(super().process_request, request)
130148

131-
def add_node(self, func_list, func, max_depth, cum_time=0.1):
149+
def add_node(self, func_list, func, max_depth, cum_time):
132150
func_list.append(func)
133151
func.has_subfuncs = False
134152
if func.depth < max_depth:
135153
for subfunc in func.subfuncs():
136-
if subfunc.stats[3] >= cum_time:
154+
# Always include the user's code
155+
if subfunc.stats[3] >= cum_time or (
156+
self.capture_project_code
157+
and subfunc.is_project_func()
158+
and subfunc.stats[3] > 0
159+
):
137160
func.has_subfuncs = True
138-
self.add_node(func_list, subfunc, max_depth, cum_time=cum_time)
161+
self.add_node(func_list, subfunc, max_depth, cum_time)
139162

140163
def generate_stats(self, request, response):
141164
if not hasattr(self, "profiler"):
@@ -150,10 +173,13 @@ def generate_stats(self, request, response):
150173
if root_func in self.stats.stats:
151174
root = FunctionCall(self.stats, root_func, depth=0)
152175
func_list = []
176+
cum_time_threshold = (
177+
root.stats[3] / dt_settings.get_config()["PROFILER_THRESHOLD_RATIO"]
178+
)
153179
self.add_node(
154180
func_list,
155181
root,
156182
dt_settings.get_config()["PROFILER_MAX_DEPTH"],
157-
root.stats[3] / 8,
183+
cum_time_threshold,
158184
)
159185
self.record_stats({"func_list": func_list})

debug_toolbar/settings.py

+2
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,9 @@
3333
"django.utils.functional",
3434
),
3535
"PRETTIFY_SQL": True,
36+
"PROFILER_CAPTURE_PROJECT_CODE": True,
3637
"PROFILER_MAX_DEPTH": 10,
38+
"PROFILER_THRESHOLD_RATIO": 8,
3739
"SHOW_TEMPLATE_CONTEXT": True,
3840
"SKIP_TEMPLATE_PREFIXES": ("django/forms/widgets/", "admin/widgets/"),
3941
"SQL_WARNING_THRESHOLD": 500, # milliseconds

debug_toolbar/static/debug_toolbar/css/toolbar.css

+6
Original file line numberDiff line numberDiff line change
@@ -621,6 +621,12 @@
621621
#djDebug .djdt-highlighted {
622622
background-color: lightgrey;
623623
}
624+
#djDebug tr.djdt-highlighted.djdt-profile-row {
625+
background-color: #ffc;
626+
}
627+
#djDebug tr.djdt-highlighted.djdt-profile-row:nth-child(2n + 1) {
628+
background-color: #dd9;
629+
}
624630
@keyframes djdt-flash-new {
625631
from {
626632
background-color: green;

debug_toolbar/templates/debug_toolbar/panels/profiling.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
</thead>
1313
<tbody>
1414
{% for call in func_list %}
15-
<tr class="{% for parent_id in call.parent_ids %} djToggleDetails_{{ parent_id }}{% endfor %}" id="profilingMain_{{ 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 }}">
1616
<td>
1717
<div data-djdt-styles="paddingLeft:{{ call.indent }}px">
1818
{% if call.has_subfuncs %}

docs/changes.rst

+10
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,16 @@ Change log
44
Pending
55
-------
66

7+
* Added Profiling panel setting ``PROFILER_THRESHOLD_RATIO`` to give users
8+
better control over how many function calls are included. A higher value
9+
will include more data, but increase render time.
10+
* Update Profiling panel to include try to always include user code. This
11+
code is more important to developers than dependency code.
12+
* Highlight the project function calls in the profiling panel.
13+
* Added Profiling panel setting ``PROFILER_CAPTURE_PROJECT_CODE`` to allow
14+
users to disable the inclusion of all project code. This will be useful
15+
to project setups that have dependencies installed under
16+
``settings.BASE_DIR``.
717
* The toolbar's font stack now prefers system UI fonts.
818

919
3.6.0 (2022-08-17)

docs/configuration.rst

+26
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,18 @@ Panel options
250250
WHERE "auth_user"."username" = '''test_username'''
251251
LIMIT 21
252252

253+
* ``PROFILER_CAPTURE_PROJECT_CODE``
254+
255+
Default: ``True``
256+
257+
Panel: profiling
258+
259+
When enabled this setting will include all project function calls in the
260+
panel. Project code is defined as files in the path defined at
261+
``settings.BASE_DIR``. If you install dependencies under
262+
``settings.BASE_DIR`` in a directory other than ``sites-packages`` or
263+
``dist-packages`` you may need to disable this setting.
264+
253265
* ``PROFILER_MAX_DEPTH``
254266

255267
Default: ``10``
@@ -259,6 +271,20 @@ Panel options
259271
This setting affects the depth of function calls in the profiler's
260272
analysis.
261273

274+
* ``PROFILER_THRESHOLD_RATIO``
275+
276+
Default: ``8``
277+
278+
Panel: profiling
279+
280+
This setting affects the which calls are included in the profile. A higher
281+
value will include more function calls. A lower value will result in a faster
282+
render of the profiling panel, but will exclude data.
283+
284+
This value is used to determine the threshold of cumulative time to include
285+
the nested functions. The threshold is calculated by the root calls'
286+
cumulative time divided by this ratio.
287+
262288
* ``SHOW_TEMPLATE_CONTEXT``
263289

264290
Default: ``True``

docs/panels.rst

+6
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,12 @@ Profiling information for the processing of the request.
130130
This panel is included but inactive by default. You can activate it by default
131131
with the ``DISABLE_PANELS`` configuration option.
132132

133+
The panel will include all function calls made by your project if you're using
134+
the setting ``settings.BASE_DIR`` to point to your project's root directory.
135+
If a function is in a file within that directory and does not include
136+
``"/site-packages/"`` or ``"/dist-packages/"`` in the path, it will be
137+
included.
138+
133139
Third-party panels
134140
------------------
135141

docs/tips.rst

+2
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,8 @@ by disabling some configuration options that are enabled by default:
7777

7878
- ``ENABLE_STACKTRACES`` for the SQL and cache panels,
7979
- ``SHOW_TEMPLATE_CONTEXT`` for the template panel.
80+
- ``PROFILER_CAPTURE_PROJECT_CODE`` and ``PROFILER_THRESHOLD_RATIO`` for the
81+
profiling panel.
8082

8183
Also, check ``SKIP_TEMPLATE_PREFIXES`` when you're using template-based
8284
form widgets.

tests/panels/test_profiling.py

+15
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,21 @@ def test_insert_content(self):
3333
# ensure the panel renders correctly.
3434
content = self.panel.content
3535
self.assertIn("regular_view", content)
36+
self.assertIn("render", content)
37+
self.assertValidHTML(content)
38+
39+
@override_settings(DEBUG_TOOLBAR_CONFIG={"PROFILER_THRESHOLD_RATIO": 1})
40+
def test_cum_time_threshold(self):
41+
"""
42+
Test that cumulative time threshold excludes calls
43+
"""
44+
self._get_response = lambda request: regular_view(request, "profiling")
45+
response = self.panel.process_request(self.request)
46+
self.panel.generate_stats(self.request, response)
47+
# ensure the panel renders but doesn't include our function.
48+
content = self.panel.content
49+
self.assertIn("regular_view", content)
50+
self.assertNotIn("render", content)
3651
self.assertValidHTML(content)
3752

3853
def test_listcomp_escaped(self):

0 commit comments

Comments
 (0)