Skip to content
This repository was archived by the owner on Jan 29, 2020. It is now read-only.

Performance increasement by integer priority #17

Merged
merged 5 commits into from
Nov 6, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
142 changes: 83 additions & 59 deletions src/EventManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,22 @@ class EventManager implements EventManagerInterface
/**
* Subscribed events and their listeners
*
* STRUCTURE:
* [
* <string name> => [
* <int priority> => [
* 0 => [<callable listener>, ...]
* ],
* ...
* ],
* ...
* ]
*
* NOTE:
* This structure helps us to reuse the list of listeners
* instead of first iterating over it and generating a new one
* -> In result it improves performance by up to 25% even if it looks a bit strange
*
* @var array[]
*/
protected $events = [];
Expand Down Expand Up @@ -116,8 +132,14 @@ public function trigger($eventName, $target = null, $argv = [])
{
$event = clone $this->eventPrototype;
$event->setName($eventName);
$event->setTarget($target);
$event->setParams($argv);

if ($target !== null) {
$event->setTarget($target);
}

if ($argv) {
$event->setParams($argv);
}

return $this->triggerListeners($event);
}
Expand All @@ -129,8 +151,14 @@ public function triggerUntil(callable $callback, $eventName, $target = null, $ar
{
$event = clone $this->eventPrototype;
$event->setName($eventName);
$event->setTarget($target);
$event->setParams($argv);

if ($target !== null) {
$event->setTarget($target);
}

if ($argv) {
$event->setParams($argv);
}

return $this->triggerListeners($event, $callback);
}
Expand Down Expand Up @@ -164,8 +192,7 @@ public function attach($eventName, callable $listener, $priority = 1)
));
}

$this->events[$eventName][((int) $priority) . '.0'][] = $listener;

$this->events[$eventName][(int) $priority][0][] = $listener;
return $listener;
}

Expand Down Expand Up @@ -197,36 +224,35 @@ public function detach(callable $listener, $eventName = null, $force = false)
}

foreach ($this->events[$eventName] as $priority => $listeners) {
foreach ($listeners as $index => $evaluatedListener) {
foreach ($listeners[0] as $index => $evaluatedListener) {
if ($evaluatedListener !== $listener) {
continue;
}

// Found the listener; remove it.
unset($this->events[$eventName][$priority][$index]);
unset($this->events[$eventName][$priority][0][$index]);

// If the queue for the given priority is empty, remove it.
if (empty($this->events[$eventName][$priority])) {
if (empty($this->events[$eventName][$priority][0])) {
unset($this->events[$eventName][$priority]);
break;
}
}
}

// If the queue for the given event is empty, remove it.
if (empty($this->events[$eventName])) {
unset($this->events[$eventName]);
break;
}
// If the queue for the given event is empty, remove it.
if (empty($this->events[$eventName])) {
unset($this->events[$eventName]);
}
}

/**
* @inheritDoc
*/
public function clearListeners($event)
public function clearListeners($eventName)
{
if (isset($this->events[$event])) {
unset($this->events[$event]);
if (isset($this->events[$eventName])) {
unset($this->events[$eventName]);
}
}

Expand Down Expand Up @@ -262,58 +288,56 @@ protected function triggerListeners(EventInterface $event, callable $callback =
throw new Exception\RuntimeException('Event is missing a name; cannot trigger!');
}

// Initial value of stop propagation flag should be false
$event->stopPropagation(false);

$responses = new ResponseCollection();
if (isset($this->events[$name])) {
$listOfListenersByPriority = $this->events[$name];

foreach ($this->getListenersByEventName($name) as $listener) {
$response = $listener($event);
$responses->push($response);

// If the event was asked to stop propagating, do so
if ($event->propagationIsStopped()) {
$responses->setStopped(true);
break;
if (isset($this->events['*'])) {
foreach ($this->events['*'] as $priority => $listOfListeners) {
$listOfListenersByPriority[$priority][] = $listOfListeners[0];
}
}
} elseif (isset($this->events['*'])) {
$listOfListenersByPriority = $this->events['*'];
} else {
$listOfListenersByPriority = [];
}

// If the result causes our validation callback to return true,
// stop propagation
if ($callback && $callback($response)) {
$responses->setStopped(true);
break;
if ($this->sharedManager) {
foreach ($this->sharedManager->getListeners($this->identifiers, $name) as $priority => $listeners) {
$listOfListenersByPriority[$priority][] = $listeners;
}
}

return $responses;
}

/**
* Get listeners for the currently triggered event.
*
* @param string $eventName
* @return callable[]
*/
private function getListenersByEventName($eventName)
{
$listeners = array_merge_recursive(
isset($this->events[$eventName]) ? $this->events[$eventName] : [],
isset($this->events['*']) ? $this->events['*'] : [],
$this->sharedManager ? $this->sharedManager->getListeners($this->identifiers, $eventName) : []
);

krsort($listeners, SORT_NUMERIC);
// Sort by priority in reverse order
krsort($listOfListenersByPriority);

$listenersForEvent = [];
// Initial value of stop propagation flag should be false
$event->stopPropagation(false);

foreach ($listeners as $priority => $listenersByPriority) {
foreach ($listenersByPriority as $listener) {
// Performance note: after some testing, it appears that accumulating listeners and sending
// them at the end of the method is FASTER than using generators (ie. yielding)
$listenersForEvent[] = $listener;
// Execute listeners
$responses = new ResponseCollection();
foreach ($listOfListenersByPriority as $listOfListeners) {
foreach ($listOfListeners as $listeners) {
foreach ($listeners as $listener) {
$response = $listener($event);
$responses->push($response);

// If the event was asked to stop propagating, do so
if ($event->propagationIsStopped()) {
$responses->setStopped(true);
return $responses;
}

// If the result causes our validation callback to return true,
// stop propagation
if ($callback && $callback($response)) {
$responses->setStopped(true);
return $responses;
}
}
}
}

return $listenersForEvent;
return $responses;
}
}
55 changes: 35 additions & 20 deletions src/SharedEventManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ public function attach($identifier, $event, callable $listener, $priority = 1)
));
}

$this->identifiers[$identifier][$event][((int) $priority) . '.0'][] = $listener;
$this->identifiers[$identifier][$event][(int) $priority][] = $listener;
}

/**
Expand Down Expand Up @@ -141,7 +141,6 @@ public function detach(callable $listener, $identifier = null, $eventName = null
unset($this->identifiers[$identifier][$eventName]);
break;
}

}

// Is the identifier queue now empty? Remove it.
Expand All @@ -153,8 +152,8 @@ public function detach(callable $listener, $identifier = null, $eventName = null
/**
* Retrieve all listeners for a given identifier and event
*
* @param array $identifiers
* @param string $eventName
* @param string[] $identifiers
* @param string $eventName
* @return array[]
* @throws Exception\InvalidArgumentException
*/
Expand All @@ -167,7 +166,7 @@ public function getListeners(array $identifiers, $eventName)
));
}

$listeners = [];
$returnListeners = [];

foreach ($identifiers as $identifier) {
if ('*' === $identifier || ! is_string($identifier) || empty($identifier)) {
Expand All @@ -177,34 +176,50 @@ public function getListeners(array $identifiers, $eventName)
));
}

$listenersByIdentifier = isset($this->identifiers[$identifier]) ? $this->identifiers[$identifier] : [];

$listeners = array_merge_recursive(
$listeners,
isset($listenersByIdentifier[$eventName]) ? $listenersByIdentifier[$eventName] : [],
isset($listenersByIdentifier['*']) ? $listenersByIdentifier['*'] : []
);
if (isset($this->identifiers[$identifier])) {
$listenersByIdentifier = $this->identifiers[$identifier];
if (isset($listenersByIdentifier[$eventName])) {
foreach ($listenersByIdentifier[$eventName] as $priority => $listeners) {
$returnListeners[$priority][] = $listeners;
}
}
if (isset($listenersByIdentifier['*'])) {
foreach ($listenersByIdentifier['*'] as $priority => $listeners) {
$returnListeners[$priority][] = $listeners;
}
}
}
}

if (isset($this->identifiers['*']) && ! in_array('*', $identifiers)) {
if (isset($this->identifiers['*'])) {
$wildcardIdentifier = $this->identifiers['*'];
if (isset($wildcardIdentifier[$eventName])) {
foreach ($wildcardIdentifier[$eventName] as $priority => $listeners) {
$returnListeners[$priority][] = $listeners;
}
}
if (isset($wildcardIdentifier['*'])) {
foreach ($wildcardIdentifier['*'] as $priority => $listeners) {
$returnListeners[$priority][] = $listeners;
}
}
}

$listeners = array_merge_recursive(
$listeners,
isset($wildcardIdentifier[$eventName]) ? $wildcardIdentifier[$eventName] : [],
isset($wildcardIdentifier['*']) ? $wildcardIdentifier['*'] : []
);
foreach ($returnListeners as $priority => $listOfListeners) {
// Argument unpacking requires PHP-5.6
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can now use argument unpacking! \o/

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, wait... package still supports PHP 5.5... @weierophinney are you fine with dropping 5.5 for 3.1.0?

// $listeners[$priority] = array_merge(...$listOfListeners);
$returnListeners[$priority] = call_user_func_array('array_merge', $listOfListeners);
}

return $listeners;
return $returnListeners;
}

/**
* @inheritDoc
*/
public function clearListeners($identifier, $eventName = null)
{
if (! array_key_exists($identifier, $this->identifiers)) {
if (! isset($this->identifiers[$identifier])) {
return false;
}

Expand Down
11 changes: 7 additions & 4 deletions src/Test/EventListenerIntrospectionTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -66,13 +66,16 @@ private function getListenersForEvent($event, EventManager $events, $withPriorit
{
$r = new ReflectionProperty($events, 'events');
$r->setAccessible(true);
$listeners = $r->getValue($events);
$internal = $r->getValue($events);

if (! isset($listeners[$event])) {
return $this->traverseListeners([]);
$listeners = [];
foreach (isset($internal[$event]) ? $internal[$event] : [] as $p => $listOfListeners) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Better naming needed

foreach ($listOfListeners as $l) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

better naming needed (do not abbreviate)

$listeners[$p] = isset($listeners[$p]) ? array_merge($listeners[$p], $l) : $l;
}
}

return $this->traverseListeners($listeners[$event], $withPriority);
return $this->traverseListeners($listeners, $withPriority);
}

/**
Expand Down
7 changes: 6 additions & 1 deletion test/EventManagerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,12 @@ public function getListenersForEvent($event, EventManager $manager)
$r->setAccessible(true);
$events = $r->getValue($manager);

return isset($events[$event]) ? $events[$event] : [];
$listenersByPriority = isset($events[$event]) ? $events[$event] : [];
foreach ($listenersByPriority as $priority => & $listeners) {
$listeners = $listeners[0];
}

return $listenersByPriority;
}

public function testAttachShouldAddListenerToEvent()
Expand Down
4 changes: 2 additions & 2 deletions test/SharedEventManagerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public function setUp()

public function getListeners(SharedEventManager $manager, array $identifiers, $event, $priority = 1)
{
$priority = (int) $priority . '.0';
$priority = (int) $priority;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note to self: this will affect every single component we've upgraded that implements a "getListeners" method of some sort for introspecting attached listeners.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Which may be a rationale for re-instating the getListeners() method. That said, it could be marked private, and libraries could use reflection in order to query it.

This is me brainstorming via github comments. Sue me.

$listeners = $manager->getListeners($identifiers, $event);
if (! isset($listeners[$priority])) {
return [];
Expand Down Expand Up @@ -247,7 +247,7 @@ public function testClearListenersDoesNothingIfNoEventsRegisteredForIdentifier()
$this->manager->clearListeners('IDENTIFIER', 'EVENT');

// getListeners() always pulls in wildcard listeners
$this->assertEquals(['1.0' => [
$this->assertEquals([1 => [
$this->callback,
]], $this->manager->getListeners([ 'IDENTIFIER' ], 'EVENT'));
}
Expand Down
1 change: 0 additions & 1 deletion test/TestAsset/StaticEventsMock.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ public function getListeners($id, $event = null)
*/
public function attach($identifier, $event, callable $listener, $priority = 1)
{

}

/**
Expand Down