Skip to content

[EventDispatcher] Delay instantiation of a service until the respective listener is triggered in ContainerAwareEventDispatcher #12019

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 7 commits into from

Conversation

znerol
Copy link
Contributor

@znerol znerol commented Sep 24, 2014

Q A
Bug fix? no
New feature? no
BC breaks? no
Deprecations? no
Tests pass? yes
Fixed tickets #12007
License MIT
Doc PR no

@znerol
Copy link
Contributor Author

znerol commented Sep 24, 2014

I think I left the order of methods as they were before:

master:
public function __construct(ContainerInterface $container)
public function addListenerService($eventName, $callback, $priority = 0)
public function removeListener($eventName, $listener)
public function hasListeners($eventName = null)
public function getListeners($eventName = null)
public function addSubscriberService($serviceId, $class)
public function dispatch($eventName, Event $event = null)
public function getContainer()
protected function lazyLoad($eventName)

modified:
public function __construct(ContainerInterface $container)
public function addListenerService($eventName, $callback, $priority = 0)
public function removeListener($eventName, $listener)
public function addSubscriberService($serviceId, $class)
public function getContainer()

The patience algorithm yields a more readable patch: git diff master --patience

…not instantiate services in removeListener, if not necessary
@znerol
Copy link
Contributor Author

znerol commented Sep 25, 2014

Ok, fixed the coding style issues and also prevent removeListener() from instantiating services if it is not necessary.

if (null === $eventName) {
foreach (array_keys($this->listenerIds) as $serviceEventName) {
$this->lazyLoad($serviceEventName);
$introspect = ($this->container instanceof IntrospectableContainerInterface);
Copy link
Member

Choose a reason for hiding this comment

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

parenthesis are not needed

@stof
Copy link
Member

stof commented Sep 25, 2014

I think this will break the integration with the TraceableDispatcher (used for the profiler panel). All lazy listeners will not be displayed as being a closure instead of identifying the real listener, making the events panel and the timeline integration useless for them (quite all listeners are lazy-loaded in Symfony)

@znerol
Copy link
Contributor Author

znerol commented Sep 25, 2014

@stof do you think it is worth fixing #12007 in Symfony or is this only relevant for Drupal?

@stof
Copy link
Member

stof commented Sep 25, 2014

Well, it might be worth improving this in Symfony itself, but it must be done in a way which does not break the debugging capabilities

@znerol
Copy link
Contributor Author

znerol commented Sep 25, 2014

From an architectural POV, do you think it would be legit to reassign the responsibility of providing information about the listeners to the actual event dispatcher? I.e. introduce a public method ContainerAwareEventManager::getListenerInfo().

Another option would be to introduce a protected method EventManager::getCallable($listener) which can be overridden from the container aware dispatcher and then use that in doDispatch to turn the listener into a callable.

@znerol
Copy link
Contributor Author

znerol commented Sep 25, 2014

Would it be possible to change the return value of EventDispatcherInterface::getListeners() from array to Traversable?

@stof
Copy link
Member

stof commented Sep 25, 2014

@znerol adding getListenerInfo() would not work, because it would be usable only if it is in the EventDispatcherInterface itself, and it cannot for BC reasons.

Maybe we could have a special invokable proxy in the EventDispatcher component, which would be used by the ContainerAwareDispatcher (rather than using a closure), and could be known by the TraceableDispatcher to describe the inner callable instead

@znerol
Copy link
Contributor Author

znerol commented Sep 25, 2014

So you are proposing something which is working like:

$callable = new LazyServiceMethodProxy($container, 'service.id', 'method_name');
$callable('some', 'params');

This looks like it could be useful outside of the event dispatcher component. Does something like this exist already?

@stof
Copy link
Member

stof commented Sep 25, 2014

@znerol given that it needs to live either in the DI or EventDispatcher components to be usable here, and that it is not that useful in DI IMO (using proxy for services themselves is much more likely to be useful as the case where you inject a callable built with a method on a service is not that common), I would keep it in EventDispatcher, alongside the ContainerAwareDispatcher.

and I would create an interface implemented by this class and supported by the TraceableDispatcher:

interface IntrospectableCallable
{
    /**
     * @return callable
     */ 
    public function getInnerCallable(); 
}

return true;
}
$proxy = function ($event, $eventName, $dispatcher) use ($container, $serviceId, $method) {
call_user_func(array($container->get($serviceId), $method), $event, $eventName, $dispatcher);
Copy link
Member

Choose a reason for hiding this comment

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

Why not just use:

$container->get($serviceId)->$method($event, $eventName, $dispatcher);

@znerol
Copy link
Contributor Author

znerol commented Sep 26, 2014

Did some wall-time benchmarking (gist) and the results indicate that this version of the ContainerAwareEventDispatcher has a slightly better runtime than the one in HEAD. This mostly affects subsequent invocations of an event handler. The performance improvement on the first call to dispatch is within the margin of error.

A repetition of the experiment in #12007 did result in the same performance improvement for the Drupal request event chain.

I also manually verified that the events are shown properly in the symfony debug toolbar.

@znerol znerol changed the title Delaying instantiation of a service until the respective listener is triggered in ContainerAwareEventDispatcher [EventDispatcher] Delay instantiation of a service until the respective listener is triggered in ContainerAwareEventDispatcher Sep 26, 2014
@@ -268,6 +269,13 @@ private function getListenerInfo($listener, $eventName)
$info = array(
'event' => $eventName,
);
// Unpack lazy container service listener.
if ($listener instanceof LazyServiceListener) {
Copy link
Member

Choose a reason for hiding this comment

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

This looks like a hack. What about implementing the IntrospectableCallable as suggested by @stof instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The very reason for this patch is to prevent preliminary service instantiation. I'm not comfortable with adding a public getInnerCallable which suffers exactly from this side-effect.

Then there is already a WrappedListener class. I initially was tempted to extract a WrappedListenerInterface from there but that would have made the code in TraceableEventDispatcher less obvious. Mainly because in that case getWrappedListener would have been used for two very different things. Introducing IntrospectableCallable without touching the WrappedListener also would result in a confusing coexistence of two similar concepts in the same component.

Copy link
Member

Choose a reason for hiding this comment

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

@znerol your implementation also triggers the lazy-loading of the service when tracing events. But instead of providing an interface usable by other implementations of a lazy-loading dispatcher, it is tied to the Symfony container

/**
* Returns the container.
*/
public function getContainer()
Copy link
Member

Choose a reason for hiding this comment

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

IMO, you should not have these getters once you add the method returning the callable

@znerol
Copy link
Contributor Author

znerol commented Sep 26, 2014

your implementation also triggers the lazy-loading of the service when tracing events.

This is obvious, the code is very explicit about that fact.

But instead of providing an interface usable by other implementations of a lazy-loading dispatcher, it is tied to the Symfony container.

The LazyServiceListener is used internally by the ContainerAwareEventDispatcher and both of them depend on the Symfony ContainerInterface. It is impossible to swap out the container with another implementation and still continue to use the ContainerAwareEventDispatcher, but as far as I can tell this is not different in the master branch. Another implementation of the lazy-loading dispatcher operating on something different than the Symfony container would need to bring in its own helper class anyway.

It indeed seems odd that the TraceableEventDispatcher has knowledge about the LazyServiceListener. But let me stress that also the getInnerCallable() method would only ever be useful in the context of debugging. Therefore if we'd implement the latter, we'd end up with debug-code in production with potential negative side-effects.

@stof
Copy link
Member

stof commented Sep 26, 2014

@znerol the goal of making TraceableEventDispatcher depend on an interface rather than the LazyServiceListener is that could be possible to have a different lazy dispatcher using its own implementation of this interface and beign supported by the TraceableEventDispatcher directly, without having to edit it. Your current implementation would force replacing the TraceableEventDispatcher if you bring your own lazy-loading dispatcher (or getting useless tracing).

And I'm not saying that the IntrospectableListener should be introspected for something else than debugging. But having the interface for it would be cleaner (and it would avoid having to make TraceableEventDispatcher aware of the way LazyServiceListener makes the listener lazy, which is the case currently).

@znerol
Copy link
Contributor Author

znerol commented Sep 26, 2014

@stof I'd like to understand the real-world consequences of this issue and I'm now trying to find alternative event dispatcher implementations which may be affected by this change. If you know of any, please link them here.

@stof
Copy link
Member

stof commented Sep 26, 2014

@znerol I don't know of any such implementation, but someone could do one in the future (at some point, there was a PR to make such a dispatcher implementation based on Pimple for Silex, but it has not been merged). Having a design allowing such extension in the component will be great (and it does not have any drawback compared to your current implementation supporting the LazyServiceListener in the TraceableEventListener, while making the code cleaner, so this would be a win)

@znerol
Copy link
Contributor Author

znerol commented Sep 26, 2014

Ok, this is the situation: I pointed out the drawback of @stof suggestion and he rightfully criticizes the ugliness in TraceableEventListener which this patch introduces. I assume there is enough information in the comments for someone else to chip in and express his/hers opinion such that we can eventually decide on how to proceed.

To summarize: In my point of view introducing a public LazyServiceListener::getInnerCallable() is unacceptable because such a method would have exactly the side-effect I'm trying to eliminate as part of this patch, i.e. potentially preliminary service instantiation. Even more so because its only purpose would be to simplify debug code and maintain automatic compatibility for potential third-party lazy dispatcher implementations which are not using the Symfony container but something different and at the same time desire to use the TraceableEventListener.

@Nicofuma
Copy link
Contributor

I don't see why introducing LazyServiceListener::getInnerCallable() could be unacceptable. When will be used this method? maybe internally in the __invoke() method and in the TraceableEventDispatcher (so during debugging) nowhere else, right? Off course you can use it yourself... but does it really matters?

Moreover if we don't, we a patch in the TraceableEventDispatcher that needs a specific comment to explain the meaning of what we can consider as a hack.
By the way I don't like the new strong dependency to the Symfony Container but I can live with it. But, for me, it's part of the LazyServiceListener class logic and that's why it looks weird in the TraceableEventDispatcher and needs a specific comment.

@znerol
Copy link
Contributor Author

znerol commented Sep 29, 2014

I discovered another aspect about this change which should be taken into account when deciding on how to proceed. With the patch the addListenerService() and addSubscriberService() call gets heavier than before because it now needs to instantiate a helper class. My benchmark shows that the time to construct the event dispatcher service is roughly doubled by this patch.

@fabpot
Copy link
Member

fabpot commented Oct 1, 2014

@znerol The question is: is it significant? I mean, double time seems like a lot, but if we are talking about a very small number, it might be insignificant.

@stof
Copy link
Member

stof commented Oct 1, 2014

In my point of view introducing a public LazyServiceListener::getInnerCallable() is unacceptable because such a method would have exactly the side-effect I'm trying to eliminate as part of this patch, i.e. potentially preliminary service instantiation.

Your own implementation of the TraceableDispatcher integration also suffers from this side-effect. My proposal does not introduce the side-effect. It makes the existing behavior of this PR cleaner (and more powerful for other integrations).
The fact that calling getInnerCallable triggers the lazy-loading of the listener and so should be avoided in production code whenever possible is something which should go in the documentation of the method.

@znerol
Copy link
Contributor Author

znerol commented Oct 1, 2014

@fabpot Depends on the number of subscribers. With a growing number even the time necessary to construct the service in the current implementation can grow rather big. I've benchmarked that and compared it to a method where all service ids would be simply injectied as an array into the constructor of the event dispatcher. The performance gain is impressive, however it suffers from the same problems outlined by @stof in #12069. Therefore it is likely that Drupal will use a custom implementation of the ContainerAwareEventDispatcher

It is possible that the benchmark is flawed, it has not been confirmed by anybody yet.

@znerol
Copy link
Contributor Author

znerol commented Oct 7, 2014

I close this, turns out that Drupal will very likely get its own version of the event dispatcher. There is no point in solving the lazy loading issue here if that degrades service construction performance even more.

@znerol znerol closed this Oct 7, 2014
@Crell
Copy link
Contributor

Crell commented Oct 10, 2014

znerol, how is Drupal using its own dispatcher better than improving Symfony's? It sounds like fabpot is open to it. There's no need to close this PR, even if we did commit an alternate implementation to Drupal.

@fabpot
Copy link
Member

fabpot commented Oct 10, 2014

@znerol Can you elaborate. I'm more than willing to make things faster for everyone. Instead of creating your own event dispatcher, is't there a way to make things better for everyone? I thought we were close to an acceptable solution with this PR.

@znerol
Copy link
Contributor Author

znerol commented Oct 10, 2014

It looks like the main problem is the myriad of function calls executed upon instantiation of the event manager service. The benchmarks are over here: https://www.drupal.org/node/1972300#comment-9216069

@Nicofuma tried a similar optimization here #12069 but it turned out that this would violate the expectation that changed priorities should be reflected without the need of recompiling the container.

@znerol
Copy link
Contributor Author

znerol commented Oct 10, 2014

This pull request here is even worse when it comes to instantiation performance and hence we'd need to try something different anyway. Therefore let's take the discussion to #12007

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants