framework icon indicating copy to clipboard operation
framework copied to clipboard

Classes locator optimization

Open butschster opened this issue 2 years ago • 1 comments

We have one performance problem with using ClasssesInterface. A lot of components or packages use this class for specific class location. And every package scan files again and again.

It would be great if SpiralFramework has an ability to scan all required directories and to notify all subscribers about found classes.

butschster avatar Feb 17 '22 08:02 butschster

Hi, @butschster. I'd like to help with this feature, because we use a lot of tokenization, which slows down bootstrap applications and tests. I have ideas about this, which I have implemented in my project, but I will not risk doing the PR, as you may have a completely different implementation. But I'll describe it in this issue.

  1. First, we need an interface for tokenization listeners. It might look something like this:
<?php

declare(strict_types=1);

interface TokenizationListenerInterface
{
    public function listen(\ReflectionClass $r): void;
    public function finalize(): void;
}

The finalize method is needed, first, so that listeners can clear their state if they have been accumulating it, and second, so that they can pass the accumulated objects to some service if it is waiting for them in the constructor, for example.

  1. After that, we need some locator with which we can collect the listeners interested in tokenization and invoke them at the moment of tokenization.
<?php

declare(strict_types=1);

final class TokenizationListenerLocator
{
    /**
     * @var TokenizationListenerInterface[]
     */
    private array $tokenizationListeners = [];

    public function addListener(TokenizationListenerInterface $tokenizationListener): void
    {
        $this->tokenizationListeners[] = $tokenizationListener;
    }

    public function invokeListeners(\ReflectionClass $r): void
    {
        foreach ($this->tokenizationListeners as $tokenizationListener) {
            $tokenizationListener->listen($r);
        }
    }
    
    public function finalize(): void
    {
        foreach ($this->tokenizationListeners as $tokenizationListener) {
            $tokenizationListener->finalize();
        }

        unset($this->tokenizationListeners);
    }
}
  1. Third, we need a point where we do the tokenization and notify the listeners. To do this we can use AbstractKernel callbacks, but for that we need a different type of callback, since we need to wait for the bootstrap of the application. Let's call them, for example, bootrstrappedCallbacks and call them right before the application serve method.
<?php

declare(strict_types=1);

abstract class AbstractKernel
{
    ...
    
    /** @var array<Closure>  */
    private $bootrstrappedCallbacks = [];
   
    public function __construct()
    {
         ...
         $this->container->bindSingleton(TokenizationListenerLocator::class, new TokenizationListenerLocator());
    }
    
    public function bootstrapped(Closure ...$callbacks): void
    {
        foreach ($callbacks as $callback) {
            $this->bootrstrappedCallbacks[] = $callback;
        }
    }
    
    public function serve()
    {
        $this->fireCallbacks($this->bootrstrappedCallbacks);
        ....
    }
}
  1. Now we need to implement the tokenization itself:
<?php

declare(strict_types=1);

final class CoreBootloader extends Bootloader
{
    private TokenizationListenerLocator $tokenizationListenerLocator;

    public function __construct(TokenizationListenerLocator $tokenizationListenerLocator)
    {
        $this->tokenizationListenerLocator = $tokenizationListenerLocator;
    }
    
    public function addTokenizationListener(TokenizationListenerInterface $tokenizationListener): void
    {
        $this->tokenizationListenerLocator->addListener($tokenizationListener);
    }

    public function boot(AbstractKernel $kernel): void
    {
        $kernel->bootstrapped(function (ClassesInterface $classes): void {
            foreach ($classes->getClasses() as $class) {
                $this->tokenizationListenerLocator->invokeListeners($class);
            }

            $this->tokenizationListenerLocator->finalize();
        });
    }
}

As an example, here is an example of such a listener and its configuration:

<?php

declare(strict_types=1);

namespace App;

use Spiral\Attributes\ReaderInterface;
use Spiral\Boot\TokenizationListenerInterface;
use Spiral\Core\Container;

final class MessageHandlerTokenizationListener implements TokenizationListenerInterface
{
    /**
     * @var object[]
     */
    private array $messageHandlers = [];

    public function __construct(
        private readonly ReaderInterface $reader,
        private readonly Container $container,
    ) {
    }

    public function listen(\ReflectionClass $r): void
    {
        if (null !== ($definition = $this->reader->firstClassMetadata($r, MessageHandler::class))) {
            $this->messageHandlers[] = $this->container->get($definition->handler);
        }
    }

    public function finalize(): void
    {
        $this->container->bindSingleton(MessageBus::class, function (): MessageBus {
            return new MessageBus($this->messageHandlers);
        });

        $this->messageHandlers = [];
    }
}


...

final class AppBootloader extends Bootloader
{
    public function boot(CoreBootloader $core): void
    {
        $core->addTokenizationListener(new EventTokenizationListener(new AttributeReader()));
    }
}

kafkiansky avatar Jul 23 '22 12:07 kafkiansky