From 4724195791f3c8dee6d51677438d507a19b3a1a8 Mon Sep 17 00:00:00 2001 From: "Dylan K. Taylor" Date: Wed, 5 Apr 2023 23:02:28 +0100 Subject: [PATCH] Improved performance of event calls This change significantly reduces the amount of work done by event handlers. Instead of traversing all of the priorities and event parent chain multiple times, we reduce event handlers down to a simple list, which doesn't require any logic to iterate over. Previously, calling an event with lots of parents costed more than an event which directly descended from Event. In addition, we had to do a lot of usually useless work to check all priorities, when in practice, only NORMAL will be used in almost all cases. This change makes it more cost effective to implement the feature suggested by #5678; however, it will still require additional changes. --- src/event/Event.php | 11 ++---- src/event/HandlerList.php | 49 +++++++++++++++++++++++++++ src/event/RegisteredListenerCache.php | 35 +++++++++++++++++++ 3 files changed, 86 insertions(+), 9 deletions(-) create mode 100644 src/event/RegisteredListenerCache.php diff --git a/src/event/Event.php b/src/event/Event.php index df0f6bb03..1ae7bb96f 100644 --- a/src/event/Event.php +++ b/src/event/Event.php @@ -59,15 +59,8 @@ abstract class Event{ ++self::$eventCallDepth; try{ - foreach(EventPriority::ALL as $priority){ - $currentList = $handlerList; - while($currentList !== null){ - foreach($currentList->getListenersByPriority($priority) as $registration){ - $registration->callEvent($this); - } - - $currentList = $currentList->getParent(); - } + foreach($handlerList->getListenerList() as $registration){ + $registration->callEvent($this); } }finally{ --self::$eventCallDepth; diff --git a/src/event/HandlerList.php b/src/event/HandlerList.php index 40d4fc566..7774ea840 100644 --- a/src/event/HandlerList.php +++ b/src/event/HandlerList.php @@ -31,6 +31,11 @@ class HandlerList{ /** @var RegisteredListener[][] */ private array $handlerSlots = []; + private RegisteredListenerCache $handlerCache; + + /** @var RegisteredListenerCache[] */ + private array $affectedHandlerCaches = []; + /** * @phpstan-template TEvent of Event * @phpstan-param class-string $class @@ -39,6 +44,11 @@ class HandlerList{ private string $class, private ?HandlerList $parentList ){ + $this->handlerCache = new RegisteredListenerCache(); + for($list = $this; $list !== null; $list = $list->parentList){ + $list->affectedHandlerCaches[spl_object_id($this->handlerCache)] = $this->handlerCache; + } + $this->handlerSlots = array_fill_keys(EventPriority::ALL, []); } @@ -50,6 +60,7 @@ class HandlerList{ throw new \InvalidArgumentException("This listener is already registered to priority {$listener->getPriority()} of event {$this->class}"); } $this->handlerSlots[$listener->getPriority()][spl_object_id($listener)] = $listener; + $this->invalidateAffectedCaches(); } /** @@ -59,6 +70,7 @@ class HandlerList{ foreach($listeners as $listener){ $this->register($listener); } + $this->invalidateAffectedCaches(); } /** @@ -78,10 +90,12 @@ class HandlerList{ }elseif($object instanceof RegisteredListener){ unset($this->handlerSlots[$object->getPriority()][spl_object_id($object)]); } + $this->invalidateAffectedCaches(); } public function clear() : void{ $this->handlerSlots = array_fill_keys(EventPriority::ALL, []); + $this->invalidateAffectedCaches(); } /** @@ -94,4 +108,39 @@ class HandlerList{ public function getParent() : ?HandlerList{ return $this->parentList; } + + /** + * Invalidates all known caches which might be affected by this list's contents. + */ + private function invalidateAffectedCaches() : void{ + foreach($this->affectedHandlerCaches as $cache){ + $cache->list = null; + } + } + + /** + * @return RegisteredListener[] + * @phpstan-return list + */ + public function getListenerList() : array{ + if($this->handlerCache->list !== null){ + return $this->handlerCache->list; + } + + $handlerLists = []; + for($currentList = $this; $currentList !== null; $currentList = $currentList->parentList){ + $handlerLists[] = $currentList; + } + + $listeners = []; + foreach(EventPriority::ALL as $priority){ + foreach($handlerLists as $currentList){ + foreach($currentList->getListenersByPriority($priority) as $registration){ + $listeners[] = $registration; + } + } + } + + return $this->handlerCache->list = $listeners; + } } diff --git a/src/event/RegisteredListenerCache.php b/src/event/RegisteredListenerCache.php new file mode 100644 index 000000000..1b675c716 --- /dev/null +++ b/src/event/RegisteredListenerCache.php @@ -0,0 +1,35 @@ + + */ + public ?array $list = null; +}