1
- import inspect
2
- import sys
1
+ import functools
3
2
import time
4
3
5
- try :
6
- from django .utils .connection import ConnectionProxy
7
- except ImportError :
8
- ConnectionProxy = None
9
-
4
+ from asgiref .local import Local
10
5
from django .conf import settings
11
- from django .core import cache
12
- from django .core .cache import DEFAULT_CACHE_ALIAS , CacheHandler
13
- from django .core .cache .backends .base import BaseCache
14
- from django .dispatch import Signal
15
- from django .middleware import cache as middleware_cache
6
+ from django .core .cache import CacheHandler , caches
16
7
from django .utils .translation import gettext_lazy as _ , ngettext
17
8
18
9
from debug_toolbar import settings as dt_settings
24
15
tidy_stacktrace ,
25
16
)
26
17
27
- cache_called = Signal ()
28
-
29
-
30
- def send_signal (method ):
31
- def wrapped (self , * args , ** kwargs ):
32
- t = time .time ()
33
- value = method (self , * args , ** kwargs )
34
- t = time .time () - t
35
-
36
- if dt_settings .get_config ()["ENABLE_STACKTRACES" ]:
37
- stacktrace = tidy_stacktrace (reversed (get_stack ()))
38
- else :
39
- stacktrace = []
40
-
41
- template_info = get_template_info ()
42
- cache_called .send (
43
- sender = self .__class__ ,
44
- time_taken = t ,
45
- name = method .__name__ ,
46
- return_value = value ,
47
- args = args ,
48
- kwargs = kwargs ,
49
- trace = stacktrace ,
50
- template_info = template_info ,
51
- backend = self .cache ,
52
- )
53
- return value
54
-
55
- return wrapped
56
-
57
-
58
- class CacheStatTracker (BaseCache ):
59
- """A small class used to track cache calls."""
60
-
61
- def __init__ (self , cache ):
62
- self .cache = cache
63
-
64
- def __repr__ (self ):
65
- return "<CacheStatTracker for %s>" % repr (self .cache )
66
-
67
- def _get_func_info (self ):
68
- frame = sys ._getframe (3 )
69
- info = inspect .getframeinfo (frame )
70
- return (info [0 ], info [1 ], info [2 ], info [3 ])
71
-
72
- def __contains__ (self , key ):
73
- return self .cache .__contains__ (key )
74
-
75
- def __getattr__ (self , name ):
76
- return getattr (self .cache , name )
77
-
78
- @send_signal
79
- def add (self , * args , ** kwargs ):
80
- return self .cache .add (* args , ** kwargs )
81
-
82
- @send_signal
83
- def get (self , * args , ** kwargs ):
84
- return self .cache .get (* args , ** kwargs )
85
-
86
- @send_signal
87
- def set (self , * args , ** kwargs ):
88
- return self .cache .set (* args , ** kwargs )
89
-
90
- @send_signal
91
- def get_or_set (self , * args , ** kwargs ):
92
- return self .cache .get_or_set (* args , ** kwargs )
93
-
94
- @send_signal
95
- def touch (self , * args , ** kwargs ):
96
- return self .cache .touch (* args , ** kwargs )
97
-
98
- @send_signal
99
- def delete (self , * args , ** kwargs ):
100
- return self .cache .delete (* args , ** kwargs )
101
-
102
- @send_signal
103
- def clear (self , * args , ** kwargs ):
104
- return self .cache .clear (* args , ** kwargs )
105
-
106
- @send_signal
107
- def has_key (self , * args , ** kwargs ):
108
- # Ignore flake8 rules for has_key since we need to support caches
109
- # that may be using has_key.
110
- return self .cache .has_key (* args , ** kwargs ) # noqa: W601
111
-
112
- @send_signal
113
- def incr (self , * args , ** kwargs ):
114
- return self .cache .incr (* args , ** kwargs )
115
-
116
- @send_signal
117
- def decr (self , * args , ** kwargs ):
118
- return self .cache .decr (* args , ** kwargs )
119
-
120
- @send_signal
121
- def get_many (self , * args , ** kwargs ):
122
- return self .cache .get_many (* args , ** kwargs )
123
-
124
- @send_signal
125
- def set_many (self , * args , ** kwargs ):
126
- self .cache .set_many (* args , ** kwargs )
127
-
128
- @send_signal
129
- def delete_many (self , * args , ** kwargs ):
130
- self .cache .delete_many (* args , ** kwargs )
131
-
132
- @send_signal
133
- def incr_version (self , * args , ** kwargs ):
134
- return self .cache .incr_version (* args , ** kwargs )
135
-
136
- @send_signal
137
- def decr_version (self , * args , ** kwargs ):
138
- return self .cache .decr_version (* args , ** kwargs )
139
-
140
-
141
- class CacheHandlerPatch (CacheHandler ):
142
- def __init__ (self , settings = None ):
143
- self ._djdt_wrap = True
144
- super ().__init__ (settings = settings )
145
-
146
- def create_connection (self , alias ):
147
- actual_cache = super ().create_connection (alias )
148
- if self ._djdt_wrap :
149
- return CacheStatTracker (actual_cache )
150
- else :
151
- return actual_cache
152
-
153
-
154
- middleware_cache .caches = CacheHandlerPatch ()
18
+ # The order of the methods in this list determines the order in which they are listed in
19
+ # the Commands table in the panel content.
20
+ WRAPPED_CACHE_METHODS = [
21
+ "add" ,
22
+ "get" ,
23
+ "set" ,
24
+ "get_or_set" ,
25
+ "touch" ,
26
+ "delete" ,
27
+ "clear" ,
28
+ "get_many" ,
29
+ "set_many" ,
30
+ "delete_many" ,
31
+ "has_key" ,
32
+ "incr" ,
33
+ "decr" ,
34
+ "incr_version" ,
35
+ "decr_version" ,
36
+ ]
155
37
156
38
157
39
class CachePanel (Panel ):
@@ -161,43 +43,57 @@ class CachePanel(Panel):
161
43
162
44
template = "debug_toolbar/panels/cache.html"
163
45
46
+ _context_locals = Local ()
47
+
164
48
def __init__ (self , * args , ** kwargs ):
165
49
super ().__init__ (* args , ** kwargs )
166
50
self .total_time = 0
167
51
self .hits = 0
168
52
self .misses = 0
169
53
self .calls = []
170
- self .counts = {
171
- "add" : 0 ,
172
- "get" : 0 ,
173
- "set" : 0 ,
174
- "get_or_set" : 0 ,
175
- "touch" : 0 ,
176
- "delete" : 0 ,
177
- "clear" : 0 ,
178
- "get_many" : 0 ,
179
- "set_many" : 0 ,
180
- "delete_many" : 0 ,
181
- "has_key" : 0 ,
182
- "incr" : 0 ,
183
- "decr" : 0 ,
184
- "incr_version" : 0 ,
185
- "decr_version" : 0 ,
186
- }
187
- cache_called .connect (self ._store_call_info )
54
+ self .counts = {name : 0 for name in WRAPPED_CACHE_METHODS }
55
+
56
+ @classmethod
57
+ def current_instance (cls ):
58
+ """
59
+ Return the currently enabled CachePanel instance or None.
60
+
61
+ If a request is in process with a CachePanel enabled, this will return that
62
+ panel (based on the current thread or async task). Otherwise it will return
63
+ None.
64
+ """
65
+ return getattr (cls ._context_locals , "current_instance" , None )
66
+
67
+ @classmethod
68
+ def ready (cls ):
69
+ if not hasattr (CacheHandler , "_djdt_patched" ):
70
+ # Wrap the CacheHander.create_connection() method to monkey patch any new
71
+ # cache connections that are opened while instrumentation is enabled. In
72
+ # the interests of thread safety, this is done once at startup time and
73
+ # never removed.
74
+ original_method = CacheHandler .create_connection
75
+
76
+ @functools .wraps (original_method )
77
+ def wrapper (self , alias ):
78
+ cache = original_method (self , alias )
79
+ panel = cls .current_instance ()
80
+ if panel is not None :
81
+ panel ._monkey_patch_cache (cache )
82
+ return cache
83
+
84
+ CacheHandler .create_connection = wrapper
85
+ CacheHandler ._djdt_patched = True
188
86
189
87
def _store_call_info (
190
88
self ,
191
- sender ,
192
- name = None ,
193
- time_taken = 0 ,
194
- return_value = None ,
195
- args = None ,
196
- kwargs = None ,
197
- trace = None ,
198
- template_info = None ,
199
- backend = None ,
200
- ** kw ,
89
+ name ,
90
+ time_taken ,
91
+ return_value ,
92
+ args ,
93
+ kwargs ,
94
+ trace ,
95
+ template_info ,
96
+ backend ,
201
97
):
202
98
if name == "get" or name == "get_or_set" :
203
99
if return_value is None :
@@ -226,6 +122,69 @@ def _store_call_info(
226
122
}
227
123
)
228
124
125
+ def _record_call (self , cache , name , original_method , args , kwargs ):
126
+ # Some cache backends implement certain cache methods in terms of other cache
127
+ # methods (e.g. get_or_set() in terms of get() and add()). In order to only
128
+ # record the calls made directly by the user code, set the _djdt_recording flag
129
+ # here to cause the monkey patched cache methods to skip recording additional
130
+ # calls made during the course of this call.
131
+ cache ._djdt_recording = True
132
+ t = time .time ()
133
+ value = original_method (* args , ** kwargs )
134
+ t = time .time () - t
135
+ cache ._djdt_recording = False
136
+
137
+ if dt_settings .get_config ()["ENABLE_STACKTRACES" ]:
138
+ stacktrace = tidy_stacktrace (reversed (get_stack ()))
139
+ else :
140
+ stacktrace = []
141
+
142
+ self ._store_call_info (
143
+ name = name ,
144
+ time_taken = t ,
145
+ return_value = value ,
146
+ args = args ,
147
+ kwargs = kwargs ,
148
+ trace = stacktrace ,
149
+ template_info = get_template_info (),
150
+ backend = cache ,
151
+ )
152
+ return value
153
+
154
+ def _monkey_patch_method (self , cache , name ):
155
+ original_method = getattr (cache , name )
156
+
157
+ @functools .wraps (original_method )
158
+ def wrapper (* args , ** kwargs ):
159
+ # If this call is being made as part of the implementation of another cache
160
+ # method, don't record it.
161
+ if cache ._djdt_recording :
162
+ return original_method (* args , ** kwargs )
163
+ else :
164
+ return self ._record_call (cache , name , original_method , args , kwargs )
165
+
166
+ wrapper ._djdt_wrapped = original_method
167
+ setattr (cache , name , wrapper )
168
+
169
+ def _monkey_patch_cache (self , cache ):
170
+ if not hasattr (cache , "_djdt_patched" ):
171
+ for name in WRAPPED_CACHE_METHODS :
172
+ self ._monkey_patch_method (cache , name )
173
+ cache ._djdt_patched = True
174
+ cache ._djdt_recording = False
175
+
176
+ @staticmethod
177
+ def _unmonkey_patch_cache (cache ):
178
+ if hasattr (cache , "_djdt_patched" ):
179
+ for name in WRAPPED_CACHE_METHODS :
180
+ original_method = getattr (cache , name )._djdt_wrapped
181
+ if original_method .__func__ == getattr (cache .__class__ , name ):
182
+ delattr (cache , name )
183
+ else :
184
+ setattr (cache , name , original_method )
185
+ del cache ._djdt_patched
186
+ del cache ._djdt_recording
187
+
229
188
# Implement the Panel API
230
189
231
190
nav_title = _ ("Cache" )
@@ -249,26 +208,22 @@ def title(self):
249
208
) % {"count" : count }
250
209
251
210
def enable_instrumentation (self ):
252
- for alias in cache .caches :
253
- if not isinstance (cache .caches [alias ], CacheStatTracker ):
254
- cache .caches [alias ] = CacheStatTracker (cache .caches [alias ])
255
-
256
- if not isinstance (middleware_cache .caches , CacheHandlerPatch ):
257
- middleware_cache .caches = cache .caches
258
-
259
- # Wrap the patched cache inside Django's ConnectionProxy
260
- if ConnectionProxy :
261
- cache .cache = ConnectionProxy (cache .caches , DEFAULT_CACHE_ALIAS )
211
+ # Monkey patch all open cache connections. Django maintains cache connections
212
+ # on a per-thread/async task basis, so this will not affect any concurrent
213
+ # requests. The monkey patch of CacheHander.create_connection() installed in
214
+ # the .ready() method will ensure that any new cache connections that get opened
215
+ # during this request will also be monkey patched.
216
+ for cache in caches .all (initialized_only = True ):
217
+ self ._monkey_patch_cache (cache )
218
+ # Mark this panel instance as the current one for the active thread/async task
219
+ # context. This will be used by the CacheHander.create_connection() monkey
220
+ # patch.
221
+ self ._context_locals .current_instance = self
262
222
263
223
def disable_instrumentation (self ):
264
- for alias in cache .caches :
265
- if isinstance (cache .caches [alias ], CacheStatTracker ):
266
- cache .caches [alias ] = cache .caches [alias ].cache
267
- if ConnectionProxy :
268
- cache .cache = ConnectionProxy (cache .caches , DEFAULT_CACHE_ALIAS )
269
- # While it can be restored to the original, any views that were
270
- # wrapped with the cache_page decorator will continue to use a
271
- # monkey patched cache.
224
+ del self ._context_locals .current_instance
225
+ for cache in caches .all (initialized_only = True ):
226
+ self ._unmonkey_patch_cache (cache )
272
227
273
228
def generate_stats (self , request , response ):
274
229
self .record_stats (
0 commit comments