Skip to content

Commit d889bf2

Browse files
committed
Merge pull request #453 from aaugustin/disable-instrumentation-for-disabled-panels
Disable instrumentation for disabled panels
2 parents 8c7a8ec + f5eaa8e commit d889bf2

File tree

12 files changed

+118
-75
lines changed

12 files changed

+118
-75
lines changed

debug_toolbar/middleware.py

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,6 @@ class DebugToolbarMiddleware(object):
5454
"""
5555
debug_toolbars = {}
5656

57-
@classmethod
58-
def get_current(cls):
59-
return cls.debug_toolbars.get(threading.current_thread().ident)
60-
6157
def __init__(self):
6258
self._urlconfs = {}
6359

@@ -92,10 +88,10 @@ def process_request(self, request):
9288

9389
toolbar = DebugToolbar(request)
9490
for panel in toolbar.panels:
95-
panel.disabled = panel.dom_id() in request.COOKIES
96-
panel.enabled = not panel.disabled
97-
if panel.disabled:
91+
panel.enabled = panel.dom_id() not in request.COOKIES
92+
if not panel.enabled:
9893
continue
94+
panel.enable_instrumentation()
9995
panel.process_request(request)
10096
self.__class__.debug_toolbars[threading.current_thread().ident] = toolbar
10197

@@ -106,7 +102,7 @@ def process_view(self, request, view_func, view_args, view_kwargs):
106102
return
107103
result = None
108104
for panel in toolbar.panels:
109-
if panel.disabled:
105+
if not panel.enabled:
110106
continue
111107
response = panel.process_view(request, view_func, view_args, view_kwargs)
112108
if response:
@@ -131,12 +127,13 @@ def process_response(self, request, response):
131127
{'redirect_to': redirect_to}
132128
)
133129
response.cookies = cookies
130+
for panel in toolbar.panels:
131+
if not panel.enabled:
132+
continue
133+
panel.process_response(request, response)
134+
panel.disable_instrumentation()
134135
if ('gzip' not in response.get('Content-Encoding', '') and
135136
response.get('Content-Type', '').split(';')[0] in _HTML_TYPES):
136-
for panel in toolbar.panels:
137-
if panel.disabled:
138-
continue
139-
panel.process_response(request, response)
140137
response.content = replace_insensitive(
141138
force_text(response.content, encoding=settings.DEFAULT_CHARSET),
142139
self.tag,

debug_toolbar/panels/__init__.py

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,38 +10,56 @@ class DebugPanel(object):
1010
"""
1111
# name = 'Base'
1212
# template = 'debug_toolbar/panels/base.html'
13-
has_content = False # If content returns something, set to True in subclass
13+
14+
# If content returns something, set to True in subclass
15+
has_content = False
16+
17+
# This can be set to False in instances if the panel is disabled.
18+
enabled = True
1419

1520
# We'll maintain a local context instance so we can expose our template
1621
# context variables to panels which need them:
1722
context = {}
1823

1924
# Panel methods
25+
2026
def __init__(self, toolbar, context={}):
2127
self.toolbar = toolbar
2228
self.context.update(context)
2329
self.slug = slugify(self.name)
2430

31+
def content(self):
32+
if self.has_content:
33+
context = self.context.copy()
34+
context.update(self.get_stats())
35+
return render_to_string(self.template, context)
36+
2537
def dom_id(self):
2638
return 'djDebug%sPanel' % (self.name.replace(' ', ''))
2739

40+
# Titles and subtitles
41+
2842
def nav_title(self):
29-
"""Title showing in toolbar"""
43+
"""Title showing in sidebar"""
3044
raise NotImplementedError
3145

3246
def nav_subtitle(self):
33-
"""Subtitle showing until title in toolbar"""
47+
"""Subtitle showing under title in sidebar"""
3448
return ''
3549

3650
def title(self):
3751
"""Title showing in panel"""
3852
raise NotImplementedError
3953

40-
def content(self):
41-
if self.has_content:
42-
context = self.context.copy()
43-
context.update(self.get_stats())
44-
return render_to_string(self.template, context)
54+
# Enable and disable (expensive) instrumentation
55+
56+
def enable_instrumentation(self):
57+
pass
58+
59+
def disable_instrumentation(self):
60+
pass
61+
62+
# Store and retrieve stats (shared between panels)
4563

4664
def record_stats(self, stats):
4765
panel_stats = self.toolbar.stats.get(self.slug)

debug_toolbar/panels/cache.py

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
from django.conf import settings
88
from django.core import cache
9-
from django.core.cache import get_cache as base_get_cache
9+
from django.core.cache import cache as original_cache, get_cache as original_get_cache
1010
from django.core.cache.backends.base import BaseCache
1111
from django.dispatch import Signal
1212
from django.template import Node
@@ -123,6 +123,10 @@ def decr_version(self, *args, **kwargs):
123123
return self.cache.decr_version(*args, **kwargs)
124124

125125

126+
def get_cache(*args, **kwargs):
127+
return CacheStatTracker(original_get_cache(*args, **kwargs))
128+
129+
126130
class CacheDebugPanel(DebugPanel):
127131
"""
128132
Panel that displays the cache statistics.
@@ -195,6 +199,16 @@ def title(self):
195199
'Cache calls from %(count)d backends',
196200
count) % dict(count=count)
197201

202+
def enable_instrumentation(self):
203+
# This isn't thread-safe because cache connections aren't thread-local
204+
# in Django, unlike database connections.
205+
cache.cache = CacheStatTracker(cache.cache)
206+
cache.get_cache = get_cache
207+
208+
def disable_instrumentation(self):
209+
cache.cache = original_cache
210+
cache.get_cache = original_get_cache
211+
198212
def process_response(self, request, response):
199213
self.record_stats({
200214
'total_calls': len(self.calls),
@@ -204,12 +218,3 @@ def process_response(self, request, response):
204218
'misses': self.misses,
205219
'counts': self.counts,
206220
})
207-
208-
209-
def get_cache_debug(*args, **kwargs):
210-
base_cache = base_get_cache(*args, **kwargs)
211-
return CacheStatTracker(base_cache)
212-
213-
214-
cache.cache = CacheStatTracker(cache.cache)
215-
cache.get_cache = get_cache_debug

debug_toolbar/panels/logger.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,9 @@ def emit(self, record):
7373
logging.root.setLevel(logging.NOTSET)
7474
logging.root.addHandler(logging_handler) # register with logging
7575

76+
# We don't use enable/disable_instrumentation because we can't make these
77+
# functions thread-safe and (hopefully) logging isn't too expensive.
78+
7679
try:
7780
import logbook
7881
logbook_supported = True

debug_toolbar/panels/sql.py

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,28 +4,13 @@
44
from copy import copy
55

66
from django.db import connections
7-
from django.db.backends import BaseDatabaseWrapper
87
from django.utils.translation import ugettext_lazy as _, ungettext_lazy as __
98

109
from debug_toolbar.forms import SQLSelectForm
11-
from debug_toolbar.middleware import DebugToolbarMiddleware
1210
from debug_toolbar.panels import DebugPanel
1311
from debug_toolbar.utils import render_stacktrace
1412
from debug_toolbar.utils.sql import reformat_sql
1513
from debug_toolbar.utils.tracking.db import CursorWrapper
16-
from debug_toolbar.utils.tracking import replace_method
17-
18-
19-
@replace_method(BaseDatabaseWrapper, 'cursor')
20-
def cursor(original, self):
21-
result = original(self)
22-
23-
djdt = DebugToolbarMiddleware.get_current()
24-
if not djdt:
25-
return result
26-
logger = djdt.get_panel(SQLDebugPanel)
27-
28-
return CursorWrapper(result, self, logger=logger)
2914

3015

3116
def get_isolation_level_display(engine, level):
@@ -131,6 +116,16 @@ def title(self):
131116
'SQL Queries from %(count)d connections',
132117
count) % dict(count=count)
133118

119+
def enable_instrumentation(self):
120+
# This is thread-safe because database connections are thread-local.
121+
for connection in connections.all():
122+
old_cursor = connection.cursor
123+
connection.cursor = lambda: CursorWrapper(old_cursor(), connection, self)
124+
125+
def disable_instrumentation(self):
126+
for connection in connections.all():
127+
del connection.cursor
128+
134129
def process_response(self, request, response):
135130
if self._queries:
136131
width_ratio_tally = 0

debug_toolbar/panels/template.py

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@
1818
# Code taken and adapted from Simon Willison and Django Snippets:
1919
# http://www.djangosnippets.org/snippets/766/
2020

21-
# Monkeypatch instrumented test renderer from django.test.utils - we could use
22-
# django.test.utils.setup_test_environment for this but that would also set up
23-
# e-mail interception, which we don't want
21+
# Monkey-patch to enable the template_rendered signal. The receiver returns
22+
# immediately when the panel is disabled to keep the overhead small.
23+
2424
from django.test.utils import instrumented_test_render
2525
from django.template import Template
2626

@@ -56,13 +56,17 @@ def __init__(self, *args, **kwargs):
5656
template_rendered.connect(self._store_template_info)
5757

5858
def _store_template_info(self, sender, **kwargs):
59-
t = kwargs['template']
60-
if t.name and t.name.startswith('debug_toolbar/'):
61-
return # skip templates that we are generating through the debug toolbar.
62-
context_data = kwargs['context']
59+
if not self.enabled:
60+
return
61+
62+
template, context = kwargs['template'], kwargs['context']
63+
64+
# Skip templates that we are generating through the debug toolbar.
65+
if template.name and template.name.startswith('debug_toolbar/'):
66+
return
6367

6468
context_list = []
65-
for context_layer in context_data.dicts:
69+
for context_layer in context.dicts:
6670
temp_layer = {}
6771
if hasattr(context_layer, 'items'):
6872
for key, value in context_layer.items():
@@ -102,6 +106,7 @@ def _store_template_info(self, sender, **kwargs):
102106
context_list.append(pformat(temp_layer))
103107
except UnicodeEncodeError:
104108
pass
109+
105110
kwargs['context'] = [force_text(item) for item in context_list]
106111
self.templates.append(kwargs)
107112

debug_toolbar/templates/debug_toolbar/base.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
{% if panel.has_content and panel.enabled %}
2525
<a href="{{ panel.url|default:"#" }}" title="{{ panel.title }}" class="{{ panel.dom_id }}">
2626
{% else %}
27-
<div class="contentless{% if panel.disabled %} disabled{% endif %}">
27+
<div class="contentless{% if not panel.enabled %} disabled{% endif %}">
2828
{% endif %}
2929
{{ panel.nav_title }}
3030
{% if panel.enabled %}
Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +0,0 @@
1-
from __future__ import unicode_literals
2-
3-
4-
def replace_method(klass, method_name):
5-
original = getattr(klass, method_name)
6-
7-
def inner(callback):
8-
def wrapped(*args, **kwargs):
9-
return callback(original, *args, **kwargs)
10-
11-
actual = getattr(original, '__wrapped__', original)
12-
wrapped.__wrapped__ = actual
13-
wrapped.__doc__ = getattr(actual, '__doc__', None)
14-
wrapped.__name__ = actual.__name__
15-
16-
setattr(klass, method_name, wrapped)
17-
return wrapped
18-
19-
return inner

tests/panels/test_cache.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# coding: utf-8
2+
3+
from __future__ import unicode_literals
4+
5+
from django.core import cache
6+
7+
from debug_toolbar.panels.cache import CacheDebugPanel
8+
9+
from ..base import BaseTestCase
10+
11+
12+
class CachePanelTestCase(BaseTestCase):
13+
14+
def setUp(self):
15+
super(CachePanelTestCase, self).setUp()
16+
self.panel = self.toolbar.get_panel(CacheDebugPanel)
17+
self.panel.enable_instrumentation()
18+
19+
def tearDown(self):
20+
self.panel.disable_instrumentation()
21+
super(CachePanelTestCase, self).tearDown()
22+
23+
def test_recording(self):
24+
self.assertEqual(len(self.panel.calls), 0)
25+
cache.cache.set('foo', 'bar')
26+
cache.cache.get('foo')
27+
cache.cache.delete('foo')
28+
self.assertEqual(len(self.panel.calls), 3)

tests/panels/test_sql.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ class SQLPanelTestCase(BaseTestCase):
1717
def setUp(self):
1818
super(SQLPanelTestCase, self).setUp()
1919
self.panel = self.toolbar.get_panel(SQLDebugPanel)
20+
self.panel.enable_instrumentation()
21+
22+
def tearDown(self):
23+
self.panel.disable_instrumentation()
24+
super(SQLPanelTestCase, self).tearDown()
2025

2126
def test_recording(self):
2227
self.assertEqual(len(self.panel._queries), 0)

0 commit comments

Comments
 (0)