1- import inspect
2- import sys
1+ import functools
32import time
43
5- try :
6- from django .utils .connection import ConnectionProxy
7- except ImportError :
8- ConnectionProxy = None
9-
4+ from asgiref .local import Local
105from 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
167from django .utils .translation import gettext_lazy as _ , ngettext
178
189from debug_toolbar import settings as dt_settings
2415 tidy_stacktrace ,
2516)
2617
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+ ]
15537
15638
15739class CachePanel (Panel ):
@@ -161,43 +43,57 @@ class CachePanel(Panel):
16143
16244 template = "debug_toolbar/panels/cache.html"
16345
46+ _context_locals = Local ()
47+
16448 def __init__ (self , * args , ** kwargs ):
16549 super ().__init__ (* args , ** kwargs )
16650 self .total_time = 0
16751 self .hits = 0
16852 self .misses = 0
16953 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
18886
18987 def _store_call_info (
19088 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 ,
20197 ):
20298 if name == "get" or name == "get_or_set" :
20399 if return_value is None :
@@ -226,6 +122,69 @@ def _store_call_info(
226122 }
227123 )
228124
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+
229188 # Implement the Panel API
230189
231190 nav_title = _ ("Cache" )
@@ -249,26 +208,23 @@ def title(self):
249208 ) % {"count" : count }
250209
251210 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
262222
263223 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+ if hasattr (self ._context_locals , "current_instance" ):
225+ del self ._context_locals .current_instance
226+ for cache in caches .all (initialized_only = True ):
227+ self ._unmonkey_patch_cache (cache )
272228
273229 def generate_stats (self , request , response ):
274230 self .record_stats (
0 commit comments