Skip to content

Commit be4e952

Browse files
committed
Merge remote-tracking branch 'living180/stacktrace_rework'
* living180/stacktrace_rework: Update change log Add deprecation warnings to old stack trace functions Use new stack trace functionality for CachePanel Use new stack trace functionality for SQLPanel Add new stack trace functionality
2 parents 3b12312 + 1c586c9 commit be4e952

File tree

6 files changed

+145
-20
lines changed

6 files changed

+145
-20
lines changed

debug_toolbar/middleware.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
from debug_toolbar import settings as dt_settings
1212
from debug_toolbar.toolbar import DebugToolbar
13+
from debug_toolbar.utils import clear_stack_trace_caches
1314

1415
_HTML_TYPES = ("text/html", "application/xhtml+xml")
1516

@@ -56,6 +57,7 @@ def __call__(self, request):
5657
# Run panels like Django middleware.
5758
response = toolbar.process_request(request)
5859
finally:
60+
clear_stack_trace_caches()
5961
# Deactivate instrumentation ie. monkey-unpatch. This must run
6062
# regardless of the response. Keep 'return' clauses below.
6163
for panel in reversed(toolbar.enabled_panels):

debug_toolbar/panels/cache.py

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,8 @@
66
from django.core.cache import CacheHandler, caches
77
from django.utils.translation import gettext_lazy as _, ngettext
88

9-
from debug_toolbar import settings as dt_settings
109
from debug_toolbar.panels import Panel
11-
from debug_toolbar.utils import (
12-
get_stack,
13-
get_template_info,
14-
render_stacktrace,
15-
tidy_stacktrace,
16-
)
10+
from debug_toolbar.utils import get_stack_trace, get_template_info, render_stacktrace
1711

1812
# The order of the methods in this list determines the order in which they are listed in
1913
# the Commands table in the panel content.
@@ -135,18 +129,13 @@ def _record_call(self, cache, name, original_method, args, kwargs):
135129
t = time.time() - t
136130
cache._djdt_recording = False
137131

138-
if dt_settings.get_config()["ENABLE_STACKTRACES"]:
139-
stacktrace = tidy_stacktrace(reversed(get_stack()))
140-
else:
141-
stacktrace = []
142-
143132
self._store_call_info(
144133
name=name,
145134
time_taken=t,
146135
return_value=value,
147136
args=args,
148137
kwargs=kwargs,
149-
trace=stacktrace,
138+
trace=get_stack_trace(),
150139
template_info=get_template_info(),
151140
backend=cache,
152141
)

debug_toolbar/panels/sql/tracking.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from django.utils.encoding import force_str
77

88
from debug_toolbar import settings as dt_settings
9-
from debug_toolbar.utils import get_stack, get_template_info, tidy_stacktrace
9+
from debug_toolbar.utils import get_stack_trace, get_template_info
1010

1111
try:
1212
from psycopg2._json import Json as PostgresJson
@@ -155,10 +155,6 @@ def _record(self, method, sql, params):
155155
finally:
156156
stop_time = time()
157157
duration = (stop_time - start_time) * 1000
158-
if dt_settings.get_config()["ENABLE_STACKTRACES"]:
159-
stacktrace = tidy_stacktrace(reversed(get_stack()))
160-
else:
161-
stacktrace = []
162158
_params = ""
163159
try:
164160
_params = json.dumps(self._decode(params))
@@ -180,7 +176,7 @@ def _record(self, method, sql, params):
180176
"raw_sql": sql,
181177
"params": _params,
182178
"raw_params": params,
183-
"stacktrace": stacktrace,
179+
"stacktrace": get_stack_trace(),
184180
"start_time": start_time,
185181
"stop_time": stop_time,
186182
"is_slow": duration > dt_settings.get_config()["SQL_WARNING_THRESHOLD"],

debug_toolbar/utils.py

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
import inspect
2+
import linecache
23
import os.path
34
import sys
5+
import warnings
46
from importlib import import_module
57
from pprint import pformat
68

79
import django
10+
from asgiref.local import Local
811
from django.core.exceptions import ImproperlyConfigured
912
from django.template import Node
1013
from django.utils.html import format_html
@@ -18,6 +21,9 @@
1821
threading = None
1922

2023

24+
_local_data = Local()
25+
26+
2127
# Figure out some paths
2228
django_path = os.path.realpath(os.path.dirname(django.__file__))
2329

@@ -44,6 +50,15 @@ def omit_path(path):
4450
return any(path.startswith(hidden_path) for hidden_path in hidden_paths)
4551

4652

53+
def _stack_trace_deprecation_warning():
54+
warnings.warn(
55+
"get_stack() and tidy_stacktrace() are deprecated in favor of"
56+
" get_stack_trace()",
57+
DeprecationWarning,
58+
stacklevel=2,
59+
)
60+
61+
4762
def tidy_stacktrace(stack):
4863
"""
4964
Clean up stacktrace and remove all entries that are excluded by the
@@ -52,6 +67,8 @@ def tidy_stacktrace(stack):
5267
``stack`` should be a list of frame tuples from ``inspect.stack()`` or
5368
``debug_toolbar.utils.get_stack()``.
5469
"""
70+
_stack_trace_deprecation_warning()
71+
5572
trace = []
5673
for frame, path, line_no, func_name, text in (f[:5] for f in stack):
5774
if omit_path(os.path.realpath(path)):
@@ -234,6 +251,8 @@ def get_stack(context=1):
234251
235252
Modified version of ``inspect.stack()`` which calls our own ``getframeinfo()``
236253
"""
254+
_stack_trace_deprecation_warning()
255+
237256
frame = sys._getframe(1)
238257
framelist = []
239258
while frame:
@@ -242,6 +261,99 @@ def get_stack(context=1):
242261
return framelist
243262

244263

264+
def _stack_frames(depth=1):
265+
frame = inspect.currentframe()
266+
while frame is not None:
267+
if depth > 0:
268+
depth -= 1
269+
else:
270+
yield frame
271+
frame = frame.f_back
272+
273+
274+
class _StackTraceRecorder:
275+
def __init__(self, excluded_paths):
276+
self.excluded_paths = excluded_paths
277+
self.filename_cache = {}
278+
self.is_excluded_cache = {}
279+
280+
def get_source_file(self, frame):
281+
frame_filename = frame.f_code.co_filename
282+
283+
value = self.filename_cache.get(frame_filename)
284+
if value is None:
285+
filename = inspect.getsourcefile(frame)
286+
if filename is None:
287+
is_source = False
288+
filename = frame_filename
289+
else:
290+
is_source = True
291+
# Ensure linecache validity the first time this recorder
292+
# encounters the filename in this frame.
293+
linecache.checkcache(filename)
294+
value = (filename, is_source)
295+
self.filename_cache[frame_filename] = value
296+
297+
return value
298+
299+
def is_excluded_path(self, path):
300+
excluded = self.is_excluded_cache.get(path)
301+
if excluded is None:
302+
resolved_path = os.path.realpath(path)
303+
excluded = any(
304+
resolved_path.startswith(excluded_path)
305+
for excluded_path in self.excluded_paths
306+
)
307+
self.is_excluded_cache[path] = excluded
308+
return excluded
309+
310+
def get_stack_trace(self, include_locals=False, depth=1):
311+
trace = []
312+
for frame in _stack_frames(depth=depth + 1):
313+
filename, is_source = self.get_source_file(frame)
314+
315+
if self.is_excluded_path(filename):
316+
continue
317+
318+
line_no = frame.f_lineno
319+
func_name = frame.f_code.co_name
320+
321+
if is_source:
322+
module = inspect.getmodule(frame, filename)
323+
module_globals = module.__dict__ if module is not None else None
324+
source_line = linecache.getline(
325+
filename, line_no, module_globals
326+
).strip()
327+
else:
328+
source_line = ""
329+
330+
frame_locals = frame.f_locals if include_locals else None
331+
332+
trace.append((filename, line_no, func_name, source_line, frame_locals))
333+
trace.reverse()
334+
return trace
335+
336+
337+
def get_stack_trace(depth=1):
338+
config = dt_settings.get_config()
339+
if config["ENABLE_STACKTRACES"]:
340+
stack_trace_recorder = getattr(_local_data, "stack_trace_recorder", None)
341+
if stack_trace_recorder is None:
342+
stack_trace_recorder = _StackTraceRecorder(hidden_paths)
343+
_local_data.stack_trace_recorder = stack_trace_recorder
344+
return stack_trace_recorder.get_stack_trace(
345+
include_locals=config["ENABLE_STACKTRACES_LOCALS"],
346+
depth=depth,
347+
)
348+
else:
349+
return []
350+
351+
352+
def clear_stack_trace_caches():
353+
if hasattr(_local_data, "stack_trace_recorder"):
354+
del _local_data.stack_trace_recorder
355+
356+
245357
class ThreadCollector:
246358
def __init__(self):
247359
if threading is None:

docs/changes.rst

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,17 @@ Change log
1111
* Fixed the cache panel to correctly count cache misses from the get_many()
1212
cache method.
1313
* Removed some obsolete compatibility code from the stack trace recording code.
14+
* Added a new mechanism for capturing stack traces which includes per-request
15+
caching to reduce expensive file system operations. Updated the cache and
16+
SQL panels to record stack traces using this new mechanism.
17+
18+
Deprecated features
19+
~~~~~~~~~~~~~~~~~~~
20+
21+
* The ``debug_toolbar.utils.get_stack()`` and
22+
``debug_toolbar.utils.tidy_stacktrace()`` functions are deprecated in favor
23+
of the new ``debug_toolbar.utils.get_stack_trace()`` function. They will
24+
removed in the next major version of the Debug Toolbar.
1425

1526
3.4.0 (2022-05-03)
1627
------------------

tests/test_utils.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import unittest
22

3-
from debug_toolbar.utils import get_name_from_obj, render_stacktrace
3+
from debug_toolbar.utils import (
4+
get_name_from_obj,
5+
get_stack,
6+
render_stacktrace,
7+
tidy_stacktrace,
8+
)
49

510

611
class GetNameFromObjTestCase(unittest.TestCase):
@@ -47,3 +52,13 @@ def test_importlib_path_issue_1612(self):
4752
'<span class="djdt-file">&lt;frozen importlib._bootstrap&gt;</span> in',
4853
result,
4954
)
55+
56+
57+
class StackTraceTestCase(unittest.TestCase):
58+
def test_deprecated_functions(self):
59+
with self.assertWarns(DeprecationWarning):
60+
stack = get_stack()
61+
self.assertEqual(stack[0][1], __file__)
62+
with self.assertWarns(DeprecationWarning):
63+
stack_trace = tidy_stacktrace(reversed(stack))
64+
self.assertEqual(stack_trace[-1][0], __file__)

0 commit comments

Comments
 (0)