Skip to content
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

[5.8] Track abstracts being resolved, fire resolving events once #26112

Closed
wants to merge 7 commits into from

Conversation

tomlankhorst
Copy link
Contributor

@tomlankhorst tomlankhorst commented Oct 13, 2018

Fixes #23699 by tracking abstracts that are currently being resolved.
If an object is built that is an instanceof of an object that is already on the stack, firing the resolving() callbacks is inhibited.
These callbacks are fired eventually when the instanceof object is resolved.

This is a continuation of #23701.

I've ran some benchmarks on the adaptions of the ContainerTests that interacts a lot with the $resolveStack because of the nested interfaces.
E.g., for testResolvingCallbacksAreCalledForNestedDependencies an adaption that resolves many times:

$container = new Container;
$resolving_dependency_interface_invocations = 0;
$resolving_dependency_implementation_invocations = 0;
$resolving_dependent_invocations = 0;

$container->bind(IContainerContractStub::class, ContainerImplementationStub::class);

$container->resolving(IContainerContractStub::class, function () use (&$resolving_dependency_interface_invocations) {
    $resolving_dependency_interface_invocations++;
});

$container->resolving(ContainerImplementationStub::class, function () use (&$resolving_dependency_implementation_invocations) {
    $resolving_dependency_implementation_invocations++;
});

$container->resolving(ContainerNestedDependentStubTwo::class, function () use (&$resolving_dependent_invocations) {
    $resolving_dependent_invocations++;
});

for($i = 0; $i < 100; $i++){
    $container->make(ContainerNestedDependentStubTwo::class, compact('i'));
    $container->make(ContainerNestedDependentStubTwo::class, compact('i'));
}

I've also adapted and benched:

  • testResolvingCallbacksAreCalledOnceForImplementations
  • testResolvingCallbacksAreCalledForSpecificAbstracts

https://github.com/tomlankhorst/framework/tree/resolve-stack-bench
https://github.com/tomlankhorst/framework/pull/1/files

Using PHPBench, 10 revs, 10 its (for a total of 10.000 calls per resolve).
phpbench run tests/Container/ContainerTest.php --tag=no_resolve_stack --store --retry-threshold=4 --revs=10 --iterations=10

Comparing this to the current master without the resolve stack (that shows the erroneous behaviour of #23699):

phpbench report --uuid=tag:no_resolve_stack --uuid=tag:resolve_stack --report='{extends: compare, compare: tag, cols: ['subject']}'
+-------------------------------------------------------+---------------------------+------------------------+
| subject                                               | tag:no_resolve_stack:mean | tag:resolve_stack:mean |
+-------------------------------------------------------+---------------------------+------------------------+
| testResolvingCallbacksAreCalledForSpecificAbstracts   | 3,879.860μs               | 4,191.810μs            |
| testResolvingCallbacksAreCalledOnceForImplementations | 8,213.690μs               | 9,426.250μs            |
| testResolvingCallbacksAreCalledForNestedDependencies  | 54,412.570μs              | 60,539.490μs           |
+-------------------------------------------------------+---------------------------+------------------------+

So, there's an average performance impact of 10% on resolving. I realize this impact is significant but I also realize that the current behaviour is not acceptable.

*/
protected function pendingInResolveStack($object)
{
foreach ($this->resolveStack as $resolving) {
Copy link
Contributor

@lucasmichot lucasmichot Oct 13, 2018

Choose a reason for hiding this comment

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

Not sure we have to resolve the last element on each loop. I would make sense to extract it to a variable before.
Or even slicing the resolveStack.

Copy link
Contributor

Choose a reason for hiding this comment

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

This would also work:

return collect($this->resolveStack)->slice(0, -1)->contains(function ($resolving) {
    $object instanceof $resolving;
});

Copy link
Contributor Author

@tomlankhorst tomlankhorst Oct 13, 2018

Choose a reason for hiding this comment

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

It does, but it's a big performance hit.

phpbench run tests/Container/ContainerTest.php --revs=100
+-------------------------------------------------------+------------------+-------------------+
| subject                                               | tag:collect:mean | tag:original:mean |
+-------------------------------------------------------+------------------+-------------------+
| testResolvingCallbacksAreCalledForSpecificAbstracts   | 5,917.960μs      | 4,046.610μs       |
| testResolvingCallbacksAreCalledOnceForImplementations | 23,971.640μs     | 9,072.640μs       |
| testResolvingCallbacksAreCalledForNestedDependencies  | 99,831.230μs     | 61,386.800μs      |
+-------------------------------------------------------+------------------+-------------------+

original is this PR, collect is your alternative.

Note that the loop breaks through return as soon as a match is found.

@taylorotwell
Copy link
Member

taylorotwell commented Oct 23, 2018

Does your solution even make sense though? What if you have a resolving stack with two objects of the same type but that are totally different, unrelated instances? You need to call the resolving callback for each instance in that case but you're checking for instanceof and will exclude one of them?

@tomlankhorst
Copy link
Contributor Author

Two independent instances of a certain type will not end up in the same stack except when there is a cyclic dependency anyway. The stack will contain a trace of classes that are being resolved.

Test testResolvingCallbacksAreCalledOnceForImplementations shows this:

  • Resolving ContainerNestedDependentStubTwo requires ContainerDependentStubTwo
  • Resolving ContainerDependentStubTwo requires two instances of IContainerContractStub
  • Resolving IContainerContractStub returns an instance of ContainerImplementationStub

The test asserts that resolving ContainerNestedDependentStubTwo triggers:

  • 1 x $container->resolving(ContainerNestedDependentStubTwo::class,...)
  • 2 x $container->resolving(ContainerImplementationStub::class,...)
  • 2 x $container->resolving(IContainerContractStub::class,...)

@taylorotwell
Copy link
Member

What is the performance impact of this on the typical Laravel web request?

@taylorotwell
Copy link
Member

Closing pending inactivity.

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

Successfully merging this pull request may close these issues.

3 participants