Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Fixed #16679 -- Use caching to speed up signal sending

  • Loading branch information...
commit 704ee33f503c96b96c2682b946a11b3b42318ba7 1 parent 507c081
Anssi Kääriäinen akaariai authored
Showing with 60 additions and 30 deletions.
  1. +9 −8 django/db/models/signals.py
  2. +51 −22 django/dispatch/dispatcher.py
17 django/db/models/signals.py
View
@@ -2,15 +2,16 @@
class_prepared = Signal(providing_args=["class"])
-pre_init = Signal(providing_args=["instance", "args", "kwargs"])
-post_init = Signal(providing_args=["instance"])
+pre_init = Signal(providing_args=["instance", "args", "kwargs"], use_caching=True)
+post_init = Signal(providing_args=["instance"], use_caching=True)
-pre_save = Signal(providing_args=["instance", "raw", "using", "update_fields"])
-post_save = Signal(providing_args=["instance", "raw", "created", "using", "update_fields"])
+pre_save = Signal(providing_args=["instance", "raw", "using", "update_fields"],
+ use_caching=True)
+post_save = Signal(providing_args=["instance", "raw", "created", "using", "update_fields"], use_caching=True)
-pre_delete = Signal(providing_args=["instance", "using"])
-post_delete = Signal(providing_args=["instance", "using"])
+pre_delete = Signal(providing_args=["instance", "using"], use_caching=True)
+post_delete = Signal(providing_args=["instance", "using"], use_caching=True)
-post_syncdb = Signal(providing_args=["class", "app", "created_models", "verbosity", "interactive"])
+post_syncdb = Signal(providing_args=["class", "app", "created_models", "verbosity", "interactive"], use_caching=True)
-m2m_changed = Signal(providing_args=["action", "instance", "reverse", "model", "pk_set", "using"])
+m2m_changed = Signal(providing_args=["action", "instance", "reverse", "model", "pk_set", "using"], use_caching=True)
73 django/dispatch/dispatcher.py
View
@@ -10,6 +10,10 @@ def _make_id(target):
if hasattr(target, '__func__'):
return (id(target.__self__), id(target.__func__))
return id(target)
+NONE_ID = _make_id(None)
+
+# A marker for caching
+NO_RECEIVERS = object()
class Signal(object):
"""
@@ -20,8 +24,7 @@ class Signal(object):
receivers
{ receriverkey (id) : weakref(receiver) }
"""
-
- def __init__(self, providing_args=None):
+ def __init__(self, providing_args=None, use_caching=False):
"""
Create a new signal.
@@ -33,6 +36,13 @@ def __init__(self, providing_args=None):
providing_args = []
self.providing_args = set(providing_args)
self.lock = threading.Lock()
+ self.use_caching = use_caching
+ # For convenience we create empty caches even if they are not used.
+ # A note about caching: if use_caching is defined, then for each
+ # distinct sender we cache the receivers that sender has in
+ # 'sender_receivers_cache'. The cache is cleaned when .connect() or
+ # .disconnect() is called and populated on send().
+ self.sender_receivers_cache = {}
def connect(self, receiver, sender=None, weak=True, dispatch_uid=None):
"""
@@ -106,6 +116,7 @@ def connect(self, receiver, sender=None, weak=True, dispatch_uid=None):
break
else:
self.receivers.append((lookup_key, receiver))
+ self.sender_receivers_cache = {}
def disconnect(self, receiver=None, sender=None, weak=True, dispatch_uid=None):
"""
@@ -140,9 +151,10 @@ def disconnect(self, receiver=None, sender=None, weak=True, dispatch_uid=None):
if r_key == lookup_key:
del self.receivers[index]
break
+ self.sender_receivers_cache = {}
def has_listeners(self, sender=None):
- return bool(self._live_receivers(_make_id(sender)))
+ return bool(self._live_receivers(sender))
def send(self, sender, **named):
"""
@@ -163,10 +175,10 @@ def send(self, sender, **named):
Returns a list of tuple pairs [(receiver, response), ... ].
"""
responses = []
- if not self.receivers:
+ if not self.receivers or self.sender_receivers_cache.get(sender) is NO_RECEIVERS:
return responses
- for receiver in self._live_receivers(_make_id(sender)):
+ for receiver in self._live_receivers(sender):
response = receiver(signal=self, sender=sender, **named)
responses.append((receiver, response))
return responses
@@ -195,12 +207,12 @@ def send_robust(self, sender, **named):
receiver.
"""
responses = []
- if not self.receivers:
+ if not self.receivers or self.sender_receivers_cache.get(sender) is NO_RECEIVERS:
return responses
# Call each receiver with whatever arguments it can accept.
# Return a list of tuple pairs [(receiver, response), ... ].
- for receiver in self._live_receivers(_make_id(sender)):
+ for receiver in self._live_receivers(sender):
try:
response = receiver(signal=self, sender=sender, **named)
except Exception as err:
@@ -209,26 +221,43 @@ def send_robust(self, sender, **named):
responses.append((receiver, response))
return responses
- def _live_receivers(self, senderkey):
+ def _live_receivers(self, sender):
"""
Filter sequence of receivers to get resolved, live receivers.
This checks for weak references and resolves them, then returning only
live receivers.
"""
- none_senderkey = _make_id(None)
- receivers = []
-
- for (receiverkey, r_senderkey), receiver in self.receivers:
- if r_senderkey == none_senderkey or r_senderkey == senderkey:
- if isinstance(receiver, WEAKREF_TYPES):
- # Dereference the weak reference.
- receiver = receiver()
- if receiver is not None:
+ receivers = None
+ if self.use_caching:
+ receivers = self.sender_receivers_cache.get(sender)
+ # We could end up here with NO_RECEIVERS even if we do check this case in
+ # .send() prior to calling _live_receivers() due to concurrent .send() call.
+ if receivers is NO_RECEIVERS:
+ return []
+ if receivers is None:
+ with self.lock:
+ senderkey = _make_id(sender)
+ receivers = []
+ for (receiverkey, r_senderkey), receiver in self.receivers:
+ if r_senderkey == NONE_ID or r_senderkey == senderkey:
receivers.append(receiver)
- else:
- receivers.append(receiver)
- return receivers
+ if self.use_caching:
+ if not receivers:
+ self.sender_receivers_cache[sender] = NO_RECEIVERS
+ else:
+ # Note, we must cache the weakref versions.
+ self.sender_receivers_cache[sender] = receivers
+ non_weak_receivers = []
+ for receiver in receivers:
+ if isinstance(receiver, WEAKREF_TYPES):
+ # Dereference the weak reference.
+ receiver = receiver()
+ if receiver is not None:
+ non_weak_receivers.append(receiver)
+ else:
+ non_weak_receivers.append(receiver)
+ return non_weak_receivers
def _remove_receiver(self, receiver):
"""
@@ -246,8 +275,8 @@ def _remove_receiver(self, receiver):
# after we delete some items
for idx, (r_key, _) in enumerate(reversed(self.receivers)):
if r_key == key:
- del self.receivers[last_idx-idx]
-
+ del self.receivers[last_idx - idx]
+ self.sender_receivers_cache = {}
def receiver(signal, **kwargs):
"""
Please sign in to comment.
Something went wrong with that request. Please try again.