From 704ee33f503c96b96c2682b946a11b3b42318ba7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anssi=20K=C3=A4=C3=A4ri=C3=A4inen?= Date: Sun, 16 Dec 2012 03:53:25 +0200 Subject: [PATCH] Fixed #16679 -- Use caching to speed up signal sending --- django/db/models/signals.py | 17 ++++---- django/dispatch/dispatcher.py | 73 ++++++++++++++++++++++++----------- 2 files changed, 60 insertions(+), 30 deletions(-) diff --git a/django/db/models/signals.py b/django/db/models/signals.py index 4666169bec..2ef54a7ca7 100644 --- a/django/db/models/signals.py +++ b/django/db/models/signals.py @@ -2,15 +2,16 @@ from django.dispatch import Signal 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) diff --git a/django/dispatch/dispatcher.py b/django/dispatch/dispatcher.py index 8d26e58bf4..65c5c408ff 100644 --- a/django/dispatch/dispatcher.py +++ b/django/dispatch/dispatcher.py @@ -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 @@ class Signal(object): 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 @@ class Signal(object): 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 @@ class Signal(object): 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 @@ class Signal(object): 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 @@ class Signal(object): 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 @@ class Signal(object): 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 @@ class Signal(object): # 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): """