Skip to content

Commit

Permalink
Merge pull request #21 from Crell/explicit-optimize-compiler
Browse files Browse the repository at this point in the history
Opt-in optimization for the compiled provider
  • Loading branch information
Crell authored Jul 31, 2023
2 parents 7f7441f + 8a802d9 commit 64aa515
Show file tree
Hide file tree
Showing 13 changed files with 331 additions and 80 deletions.
34 changes: 33 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -300,9 +300,41 @@ $provider = new Name\Space\Of\My\App\MyCompiledProvider($container);

And boom! `$provider` is now a fully functional Provider you can pass to a Dispatcher. It will work just like any other, but faster.


But what if you want to have most of your listeners pre-registered, but have some that you add conditionally at runtime? Have a look at the FIG's [`AggregateProvider`](https://github.com/php-fig/event-dispatcher-util/blob/master/src/AggregateProvider.php), and combine your compiled Provider with an instance of `OrderedListenerProvider`.

### Compiler optimization

The `ProviderBuilder` has one other trick. If you specify one or more events via the `optimizeEvent($class)` method, then the compiler will pre-compute what listeners apply to it based on its type, including its parent classes and interfaces. The result is a constant-time simple array lookup for those events, also known as "virtually instantaneous."

```php
use Crell\Tukio\ProviderBuilder;
use Crell\Tukio\ProviderCompiler;

$builder = new ProviderBuilder();

$builder->addListener('listenerA', 100);
$builder->addListenerAfter('listenerA', 'listenerB');
$builder->addListener([Listen::class, 'listen']);
$builder->addListenerService('listeners', 'listen', CollectingEvent::class);
$builder->addSubscriber('subscriber', Subscriber::class);

// Here's where you specify what events you know you will have.
// Returning the listeners for these events will be near instant.
$builder->optimizeEvent(EvenOne::class);
$builder->optimizeEvent(EvenTwo::class);

$compiler = new ProviderCompiler();

// Write the generated compiler out to a file.
$filename = 'MyCompiledProvider.php';
$out = fopen($filename, 'w');

// Here's the magic:
$compiler->compile($builder, $out, 'MyCompiledProvider', '\\Name\\Space\\Of\\My\\App');

fclose($out);
```

### Attribute-based registration

If you are using PHP 8.0 or later, Tukio fully supports attributes as a means of registration for both `OrderedListenerProvider` and `ProviderBuilder`. There are four relevant attributes: `Listener`, `ListenerPriority`, `ListenerBefore`, and `ListenerAfter`. All can be used with sequential parameters or named parameters. In most cases, named parameters will be more self-documenting. All attributes are valid only on functions and methods.
Expand Down
31 changes: 14 additions & 17 deletions benchmarks/CompiledProviderBench.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,18 @@
namespace Crell\Tukio\Benchmarks;

use Crell\Tukio\CollectingEvent;
use Crell\Tukio\DummyEvent;
use Crell\Tukio\MockContainer;
use Crell\Tukio\ProviderBuilder;
use Crell\Tukio\ProviderCompiler;
use PhpBench\Benchmark\Metadata\Annotations\AfterClassMethods;
use PhpBench\Benchmark\Metadata\Annotations\BeforeClassMethods;
use PhpBench\Benchmark\Metadata\Annotations\Groups;
use Psr\EventDispatcher\ListenerProviderInterface;
use PhpBench\Benchmark\Metadata\Annotations\Iterations;
use PhpBench\Benchmark\Metadata\Annotations\OutputTimeUnit;
use PhpBench\Benchmark\Metadata\Annotations\RetryThreshold;
use PhpBench\Benchmark\Metadata\Annotations\Revs;
use PhpBench\Benchmark\Metadata\Annotations\Warmup;

/**
* @Groups({"Providers"})
Expand All @@ -20,19 +25,13 @@
*/
class CompiledProviderBench extends ProviderBenchBase
{
/**
* @var ListenerProviderInterface
*/
protected $provider;
protected static string $filename = 'compiled_provider.php';

/** @var string */
protected static $filename = 'compiled_provider.php';
protected static string $className = 'CompiledProvider';

/** @var string */
protected static $className = 'CompiledProvider';
protected static string $namespace = 'Test\\Space';

/** @var string */
protected static $namespace = 'Test\\Space';
protected static array $optimizeClasses = [];

public static function createCompiledProvider(): void
{
Expand All @@ -42,11 +41,14 @@ public static function createCompiledProvider(): void
$priority = new \InfiniteIterator(new \ArrayIterator(static::$listenerPriorities));
$priority->next();

foreach(range(1, static::$numListeners) as $counter) {
foreach(range(1, ceil(static::$numListeners/2)) as $counter) {
$builder->addListener([static::class, 'fakeListener'], $priority->current());
$builder->addListenerService('Foo', 'bar', DummyEvent::class, $priority->current());
$priority->next();
}

$builder->optimizeEvents(...static::$optimizeClasses);

// Write the generated compiler out to a temp file.
$out = fopen(static::$filename, 'w');
$compiler->compile($builder, $out, static::$className, static::$namespace);
Expand All @@ -60,16 +62,11 @@ public static function removeCompiledProvider(): void

public function setUp(): void
{
// Now include it. If there's a parse error PHP will throw a ParseError and PHPUnit will catch it for us.
include static::$filename;

$container = new MockContainer();

$compiledClassName = static::$namespace . '\\' . static::$className;
$this->provider = new $compiledClassName($container);
}

public static function fakeListener(CollectingEvent $task): void
{
}
}
17 changes: 17 additions & 0 deletions benchmarks/OptimizedCompiledProviderBench.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

declare(strict_types=1);

namespace Crell\Tukio\Benchmarks;

use Crell\Tukio\CollectingEvent;
use Crell\Tukio\DummyEvent;
use PhpBench\Benchmark\Metadata\Annotations\Groups;

/**
* @Groups({"Providers"})
*/
class OptimizedCompiledProviderBench extends CompiledProviderBench
{
protected static array $optimizeClasses = [CollectingEvent::class, DummyEvent::class];
}
7 changes: 1 addition & 6 deletions benchmarks/OrderedProviderBench.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,6 @@
*/
class OrderedProviderBench extends ProviderBenchBase
{
/**
* @var ListenerProviderInterface
*/
protected $provider;

public function setUp(): void
{
$this->provider = new OrderedListenerProvider();
Expand All @@ -27,7 +22,7 @@ public function setUp(): void
$priority->next();

foreach(range(1, static::$numListeners) as $counter) {
$this->provider->addListener(static function(CollectingEvent $task): void {}, $priority->current());
$this->provider->addListener([static::class, 'fakeListener'], $priority->current());
$priority->next();
}
}
Expand Down
24 changes: 15 additions & 9 deletions benchmarks/ProviderBenchBase.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,25 @@
use Crell\Tukio\CollectingEvent;
use PhpBench\Benchmark\Metadata\Annotations\Groups;
use PhpBench\Benchmark\Metadata\Annotations\Iterations;
use PhpBench\Benchmark\Metadata\Annotations\OutputTimeUnit;
use PhpBench\Benchmark\Metadata\Annotations\RetryThreshold;
use PhpBench\Benchmark\Metadata\Annotations\Revs;
use PhpBench\Benchmark\Metadata\Annotations\Warmup;
use Psr\EventDispatcher\ListenerProviderInterface;

/**
* @Groups({"Providers"})
* @Revs(1000)
* @Iterations(5)
* @Revs(50)
* @Iterations(10)
* @Warmup(2)
* @OutputTimeUnit("milliseconds", precision=4)
* @RetryThreshold(10.0)
*/
abstract class ProviderBenchBase extends TukioBenchmarks
{
/**
* @var ListenerProviderInterface
*/
protected $provider;
protected ListenerProviderInterface $provider;

/** @var int */
protected static $numListeners = 5000;
protected static int $numListeners = 1000;

/**
* @var array<int>
Expand All @@ -47,6 +49,10 @@ public function bench_match_provider(): void
$listeners = $this->provider->getListenersForEvent($task);

// Run out the generator.
iterator_to_array($listeners);
is_array($listeners) || iterator_to_array($listeners);
}

public static function fakeListener(CollectingEvent $task): void
{
}
}
10 changes: 4 additions & 6 deletions docker/php/74/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
FROM php:7.4.28-cli
FROM php:7.4-cli
WORKDIR /usr/src/myapp

COPY --from=composer:latest /usr/bin/composer /usr/bin/composer

RUN apt-get update && apt-get install zip unzip git -y \
&& pecl install xdebug \
&& php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" \
&& php -r "if (hash_file('sha384', 'composer-setup.php') === '906a84df04cea2aa72f40b5f787e49f22d4c2f19492ac310e8cba5b96ac8b64115ac402c8cd292b8a03482574915d1a8') { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;" \
&& php composer-setup.php --install-dir=/usr/bin --filename=composer \
&& php -r "unlink('composer-setup.php');" \
&& mkdir /.composer && chmod 777 /.composer
&& pecl install pcov
2 changes: 1 addition & 1 deletion docker/php/74/xdebug.ini
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
zend_extension=/usr/local/lib/php/extensions/no-debug-non-zts-20210902/xdebug.so
zend_extension=/usr/local/lib/php/extensions/no-debug-non-zts-20190902/xdebug.so
xdebug.output_dir=profiles
16 changes: 5 additions & 11 deletions phpbench.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,7 @@
{
"bootstrap": "vendor/autoload.php",
"path": "benchmarks",
"retry_threshold": 5,
"reports": {
"short": {
"title": "Short summary",
"extends": "aggregate",
"cols": ["benchmark", "subject", "mode", "mean", "stdev", "rstdev", "mem_peak"],
"break": ["benchmark"]
}
}
"runner.path": "benchmarks",
"$schema":"./vendor/phpbench/phpbench/phpbench.schema.json",
"runner.bootstrap": "vendor/autoload.php",
"runner.php_disable_ini": true,
"runner.file_pattern": "*Bench.php"
}
34 changes: 12 additions & 22 deletions src/CompiledListenerProviderBase.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,6 @@

namespace Crell\Tukio;

use Crell\Tukio\Entry\ListenerFunctionEntry;
use Crell\Tukio\Entry\ListenerServiceEntry;
use Crell\Tukio\Entry\ListenerStaticMethodEntry;
use Psr\Container\ContainerInterface;
use Psr\EventDispatcher\ListenerProviderInterface;

Expand All @@ -19,7 +16,10 @@ class CompiledListenerProviderBase implements ListenerProviderInterface
// entry types in the classes seen in getListenerForEvent(). See each class's getProperties() method for the
// exact structure.
/** @var array<mixed> */
protected const LISTENERS = [];
protected array $listeners = [];

/** @var array<class-string, mixed> */
protected array $optimized = [];

public function __construct(ContainerInterface $container)
{
Expand All @@ -31,28 +31,18 @@ public function __construct(ContainerInterface $container)
*/
public function getListenersForEvent(object $event): iterable
{
$count = count(static::LISTENERS);
// @todo Switch to ::class syntax in PHP 8.
if (isset($this->optimized[get_class($event)])) {
return $this->optimized[get_class($event)];
}

$count = count($this->listeners);
$ret = [];
for ($i = 0; $i < $count; ++$i) {
/** @var array<mixed> $listener */
$listener = static::LISTENERS[$i];
$listener = $this->listeners[$i];
if ($event instanceof $listener['type']) {
// Turn this into a match() in PHP 8.
switch ($listener['entryType']) {
case ListenerFunctionEntry::class:
$ret[] = $listener['listener'];
break;
case ListenerStaticMethodEntry::class:
$ret[] = [$listener['class'], $listener['method']];
break;
case ListenerServiceEntry::class:
$ret[] = function (object $event) use ($listener): void {
$this->container->get($listener['serviceName'])->{$listener['method']}($event);
};
break;
default:
throw new \RuntimeException(sprintf('No such listener type found in compiled container definition: %s', $listener['entryType']));
}
$ret[] = $listener['callable'];
}
}
return $ret;
Expand Down
23 changes: 23 additions & 0 deletions src/ProviderBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,34 @@ class ProviderBuilder implements OrderedProviderInterface, \IteratorAggregate
*/
protected OrderedCollection $listeners;

/**
* @var array<class-string>
*/
protected array $optimizedEvents = [];

public function __construct()
{
$this->listeners = new OrderedCollection();
}

/**
* Pre-specify an event class that should have an optimized listener list built.
*
* @param class-string ...$events
*/
public function optimizeEvents(string ...$events): void
{
$this->optimizedEvents = [...$this->optimizedEvents, ...$events];
}

/**
* @return array<class-string>
*/
public function getOptimizedEvents(): array
{
return $this->optimizedEvents;
}

public function addListener(callable $listener, ?int $priority = null, ?string $id = null, ?string $type = null): string
{
if ($attributes = $this->getAttributes($listener)) {
Expand Down
Loading

0 comments on commit 64aa515

Please sign in to comment.