Skip to content

Reduce stacktrace capture overhead #1606

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 3 commits into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 66 additions & 6 deletions debug_toolbar/utils.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import functools
import inspect
import os.path
import re
Expand All @@ -19,6 +20,54 @@
threading = None


# A lighter-weight (and somewhat less-featureful) alternative to the
# functools.lru_cache decorator. Does not support keyword arguments. Does not
# use any locking internally, as the worst result of any race condition would
# be a slight inaccuracy in the cache entry access count, possibly resulting in
# a more-recently-accessed entry being discarded if the cache is too full. Its
# main distinctive is that it supports an optional key function, which
# transforms the function arguments into a cache key if the original arguments
# cannot or should not be used as a cache key.
class LRUCache:
SENTINEL = object()
DEFAULT_MAX_SIZE = 256

class Entry:
def __init__(self, value):
self.value = value
self.access_count = 0

def __init__(self, key_func=None, max_size=None):
self.cache = {}
self.counter = 0
self.key_func = key_func
if max_size is None:
max_size = self.DEFAULT_MAX_SIZE
self.max_size = max_size

def __call__(self, f):
@functools.wraps(f)
def wrapper(*args):
if self.key_func is None:
cache_key = args
else:
cache_key = self.key_func(*args)
entry = self.cache.get(cache_key, self.SENTINEL)
if entry is self.SENTINEL:
entry = self.Entry(f(*args))
while len(self.cache) >= self.max_size:
lru_key, _ = min(
self.cache.items(), key=lambda item: item[1].access_count
)
self.cache.pop(lru_key, None)
self.cache[cache_key] = entry
self.counter += 1
entry.access_count = self.counter
return entry.value

return wrapper


# Figure out some paths
django_path = os.path.realpath(os.path.dirname(django.__file__))

Expand All @@ -41,7 +90,12 @@ def get_module_path(module_name):
]


# os.path.realpath() is expensive since it has to hit the filesystem. Since
# omit_path() is called for each stack frame in tidy_stacktrace(), apply the
# LRUCache() decorator to reduce the cost.
@LRUCache()
def omit_path(path):
path = os.path.realpath(path)
return any(path.startswith(hidden_path) for hidden_path in hidden_paths)


Expand All @@ -56,7 +110,7 @@ def tidy_stacktrace(stack):
"""
trace = []
for frame, path, line_no, func_name, text in (f[:5] for f in stack):
if omit_path(os.path.realpath(path)):
if omit_path(path):
continue
text = "".join(text).strip() if text else ""
frame_locals = (
Expand Down Expand Up @@ -167,19 +221,25 @@ def get_name_from_obj(obj):
return name


def getframeinfo(frame, context=1):
# getframeinfo() is quite expensive as it hits the filesystem multiple
# times. Apply the LRUCache decorator to reduce this cost. Using the frame
# objects in cache keys directly is undesirable as it would cause the frames
# (and their associated locals) to stay in memory indefinitely. Instead use
# the filename and line number as the cache key (this has the happy benefit of
# improving the cache hit rate as well).
@LRUCache(key_func=lambda frame: (frame.f_code.co_filename, frame.f_lineno))
def getframeinfo(frame):
"""
Get information about a frame or traceback object.

A tuple of five things is returned: the filename, the line number of
the current line, the function name, a list of lines of context from
the source code, and the index of the current line within that list.
The optional second argument specifies the number of lines of context
to return, which are centered around the current line.

This originally comes from ``inspect`` but is modified to handle issues
with ``findsource()``.
"""
context = 1
if inspect.istraceback(frame):
lineno = frame.tb_lineno
frame = frame.tb_frame
Expand Down Expand Up @@ -232,7 +292,7 @@ def get_sorted_request_variable(variable):
return [(k, variable.getlist(k)) for k in sorted(variable)]


def get_stack(context=1):
def get_stack():
"""
Get a list of records for a frame and all higher (calling) frames.

Expand All @@ -244,7 +304,7 @@ def get_stack(context=1):
frame = sys._getframe(1)
framelist = []
while frame:
framelist.append((frame,) + getframeinfo(frame, context))
framelist.append((frame,) + getframeinfo(frame))
frame = frame.f_back
return framelist

Expand Down