From 9c3298f5208867ad30daa92e094ce2ce160b1375 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Wed, 26 Jul 2023 18:28:59 -0500 Subject: [PATCH 01/10] Add optimized lookup lists for selected events in the compiler. --- src/CompiledListenerProviderBase.php | 40 ++++++---- src/ProviderBuilder.php | 20 +++++ src/ProviderCompiler.php | 105 ++++++++++++++++++++++++- tests/CompiledListenerProviderTest.php | 26 ++++++ 4 files changed, 171 insertions(+), 20 deletions(-) diff --git a/src/CompiledListenerProviderBase.php b/src/CompiledListenerProviderBase.php index 36228b2..4f62a41 100644 --- a/src/CompiledListenerProviderBase.php +++ b/src/CompiledListenerProviderBase.php @@ -21,6 +21,8 @@ class CompiledListenerProviderBase implements ListenerProviderInterface /** @var array */ protected const LISTENERS = []; + protected const OPTIMIZED = []; + public function __construct(ContainerInterface $container) { $this->container = $container; @@ -31,30 +33,36 @@ public function __construct(ContainerInterface $container) */ public function getListenersForEvent(object $event): iterable { + if (isset(static::OPTIMIZED[$event::class])) { + return array_map(fn(array $listener) => $this->makeListener($listener), static::OPTIMIZED[$event::class]); + } + $count = count(static::LISTENERS); $ret = []; for ($i = 0; $i < $count; ++$i) { /** @var array $listener */ $listener = static::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[] = $this->makeListener($listener); } } return $ret; } + + protected function makeListener(array $listener): callable + { + // Turn this into a match() in PHP 8. + switch ($listener['entryType']) { + case ListenerFunctionEntry::class: + return $listener['listener']; + case ListenerStaticMethodEntry::class: + return [$listener['class'], $listener['method']]; + case ListenerServiceEntry::class: + return function (object $event) use ($listener): void { + $this->container->get($listener['serviceName'])->{$listener['method']}($event); + }; + default: + throw new \RuntimeException(sprintf('No such listener type found in compiled container definition: %s', $listener['entryType'])); + } + } } diff --git a/src/ProviderBuilder.php b/src/ProviderBuilder.php index 0516d75..6be7a04 100644 --- a/src/ProviderBuilder.php +++ b/src/ProviderBuilder.php @@ -19,11 +19,31 @@ class ProviderBuilder implements OrderedProviderInterface, \IteratorAggregate */ protected OrderedCollection $listeners; + /** + * @var array + */ + 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 $event + */ + public function optimizeEvent(string $event): void + { + $this->optimizedEvents[] = $event; + } + + public function optimizedEvents(): array + { + return $this->optimizedEvents; + } + public function addListener(callable $listener, ?int $priority = null, ?string $id = null, ?string $type = null): string { if ($attributes = $this->getAttributes($listener)) { diff --git a/src/ProviderCompiler.php b/src/ProviderCompiler.php index 81864a9..65867c7 100644 --- a/src/ProviderCompiler.php +++ b/src/ProviderCompiler.php @@ -18,17 +18,101 @@ class ProviderCompiler * @param string $namespace * the namespace for the compiled class. */ - public function compile(ProviderBuilder $listeners, $stream, string $class = 'CompiledListenerProvider', string $namespace = '\\Crell\\Tukio\\Compiled'): void - { + public function compile( + ProviderBuilder $listeners, + $stream, + string $class = 'CompiledListenerProvider', + string $namespace = '\\Crell\\Tukio\\Compiled' + ): void { fwrite($stream, $this->createPreamble($class, $namespace)); + $this->writeMainListenersList($listeners, $stream); + + $this->writeOptimizedList($listeners, $stream); + + fwrite($stream, $this->createClosing()); + } + + protected function writeMainListenersList(ProviderBuilder $listeners, $stream): void + { + fwrite($stream, $this->startMainListenersList()); + /** @var CompileableListenerEntryInterface $listenerEntry */ foreach ($listeners as $listenerEntry) { $item = $this->createEntry($listenerEntry); fwrite($stream, $item); } - fwrite($stream, $this->createClosing()); + fwrite($stream, $this->endMainListenersList()); + } + + protected function writeOptimizedList(ProviderBuilder $listeners, $stream): void + { + fwrite($stream, $this->startOptimizedList()); + + $listenerDefs = iterator_to_array($listeners); + + foreach ($listeners->optimizedEvents() as $event) { + $ancestors = $this->classAncestors($event); + + fwrite($stream, $this->startOptimizedEntry($event)); + + $relevantListeners = array_filter($listenerDefs, fn(CompileableListenerEntryInterface $entry) => in_array($entry->getProperties()['type'], $ancestors)); + + /** @var CompileableListenerEntryInterface $listenerEntry */ + foreach ($relevantListeners as $listenerEntry) { + $item = $this->createEntry($listenerEntry); + fwrite($stream, $item); + } + + fwrite($stream, $this->endOptimizedEntry()); + } + + fwrite($stream, $this->endOptimizedList()); + } + + protected function startOptimizedEntry(string $event): string + { + return << [ +END; + } + + protected function endOptimizedEntry(): string + { + return <<<'END' + ], +END; + } + + /** + * Returns a list of all class and interface parents of a class. + * + * @return array + */ + protected function classAncestors(string $class, bool $includeClass = true): array + { + // These methods both return associative arrays, making + safe. + $ancestors = class_parents($class) + class_implements($class); + return $includeClass + ? [$class => $class] + $ancestors + : $ancestors + ; + } + + protected function startOptimizedList(): string + { + return <<assertEquals('BACD', implode($event->result())); } + public function test_optimize_event(): void + { + $class = 'OptimizedEventProvider'; + $namespace = 'Test\\Space'; + + $builder = new ProviderBuilder(); + $container = new MockContainer(); + + // Just to make the following lines shorter and easier to read. + $ns = '\\Crell\\Tukio\\'; + $builder->addListener("{$ns}event_listener_one", -4, 'id-1'); + $builder->addListenerBefore('id-1', "{$ns}event_listener_two", 'id-2'); + $builder->addListenerAfter('id-2', "{$ns}event_listener_three", 'id-3'); + $builder->addListenerAfter('id-3', "{$ns}event_listener_four"); + + $builder->optimizeEvent(CollectingEvent::class); + + $provider = $this->makeProvider($builder, $container, $class, $namespace); + + $event = new CollectingEvent(); + foreach ($provider->getListenersForEvent($event) as $listener) { + $listener($event); + } + + $this->assertEquals('BACD', implode($event->result())); + } } From ed8eb307f008f6bec4e9d40863763630e8ad91f5 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Wed, 26 Jul 2023 19:11:11 -0500 Subject: [PATCH 02/10] Update benchmarks. --- benchmarks/CompiledProviderBench.php | 18 ++--- benchmarks/OptimizedCompiledProviderBench.php | 71 +++++++++++++++++++ benchmarks/OrderedProviderBench.php | 7 +- benchmarks/ProviderBenchBase.php | 24 ++++--- phpbench.json | 16 ++--- 5 files changed, 98 insertions(+), 38 deletions(-) create mode 100644 benchmarks/OptimizedCompiledProviderBench.php diff --git a/benchmarks/CompiledProviderBench.php b/benchmarks/CompiledProviderBench.php index feab0aa..c0f45e9 100644 --- a/benchmarks/CompiledProviderBench.php +++ b/benchmarks/CompiledProviderBench.php @@ -11,7 +11,11 @@ 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"}) @@ -20,11 +24,6 @@ */ class CompiledProviderBench extends ProviderBenchBase { - /** - * @var ListenerProviderInterface - */ - protected $provider; - /** @var string */ protected static $filename = 'compiled_provider.php'; @@ -55,12 +54,11 @@ public static function createCompiledProvider(): void public static function removeCompiledProvider(): void { - unlink(static::$filename); + //unlink(static::$filename); } 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(); @@ -68,8 +66,4 @@ public function setUp(): void $compiledClassName = static::$namespace . '\\' . static::$className; $this->provider = new $compiledClassName($container); } - - public static function fakeListener(CollectingEvent $task): void - { - } } diff --git a/benchmarks/OptimizedCompiledProviderBench.php b/benchmarks/OptimizedCompiledProviderBench.php new file mode 100644 index 0000000..e1cf3e2 --- /dev/null +++ b/benchmarks/OptimizedCompiledProviderBench.php @@ -0,0 +1,71 @@ +next(); + + foreach(range(1, static::$numListeners) as $counter) { + $builder->addListener([static::class, 'fakeListener'], $priority->current()); + $priority->next(); + } + + $builder->optimizeEvent(CollectingEvent::class); + + // Write the generated compiler out to a temp file. + $out = fopen(static::$filename, 'w'); + $compiler->compile($builder, $out, static::$className, static::$namespace); + fclose($out); + } + + public static function removeCompiledProvider(): void + { + //unlink(static::$filename); + } + + public function setUp(): void + { + include static::$filename; + + $container = new MockContainer(); + + $compiledClassName = static::$namespace . '\\' . static::$className; + $this->provider = new $compiledClassName($container); + } +} diff --git a/benchmarks/OrderedProviderBench.php b/benchmarks/OrderedProviderBench.php index f5807cd..64b33d7 100644 --- a/benchmarks/OrderedProviderBench.php +++ b/benchmarks/OrderedProviderBench.php @@ -14,11 +14,6 @@ */ class OrderedProviderBench extends ProviderBenchBase { - /** - * @var ListenerProviderInterface - */ - protected $provider; - public function setUp(): void { $this->provider = new OrderedListenerProvider(); @@ -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(); } } diff --git a/benchmarks/ProviderBenchBase.php b/benchmarks/ProviderBenchBase.php index a0e1def..40db072 100644 --- a/benchmarks/ProviderBenchBase.php +++ b/benchmarks/ProviderBenchBase.php @@ -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 @@ -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 + { } } diff --git a/phpbench.json b/phpbench.json index 2d096b9..f98f228 100644 --- a/phpbench.json +++ b/phpbench.json @@ -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" } From 3c378349574cc7667e581d0f1a245d55f1d11548 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Wed, 26 Jul 2023 19:11:41 -0500 Subject: [PATCH 03/10] Inline the listener creation, for performance. --- src/CompiledListenerProviderBase.php | 59 +++++++++++++++++++--------- src/ProviderCompiler.php | 4 ++ 2 files changed, 44 insertions(+), 19 deletions(-) diff --git a/src/CompiledListenerProviderBase.php b/src/CompiledListenerProviderBase.php index 4f62a41..b7855ec 100644 --- a/src/CompiledListenerProviderBase.php +++ b/src/CompiledListenerProviderBase.php @@ -34,7 +34,30 @@ public function __construct(ContainerInterface $container) public function getListenersForEvent(object $event): iterable { if (isset(static::OPTIMIZED[$event::class])) { - return array_map(fn(array $listener) => $this->makeListener($listener), static::OPTIMIZED[$event::class]); + $count = count(static::OPTIMIZED[$event::class]); + $ret = []; + for ($i = 0; $i < $count; ++$i) { + $listener = static::OPTIMIZED[$event::class]; + 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'])); + } + } + } + return $ret; } $count = count(static::LISTENERS); @@ -43,26 +66,24 @@ public function getListenersForEvent(object $event): iterable /** @var array $listener */ $listener = static::LISTENERS[$i]; if ($event instanceof $listener['type']) { - $ret[] = $this->makeListener($listener); + // 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'])); + } } } return $ret; } - - protected function makeListener(array $listener): callable - { - // Turn this into a match() in PHP 8. - switch ($listener['entryType']) { - case ListenerFunctionEntry::class: - return $listener['listener']; - case ListenerStaticMethodEntry::class: - return [$listener['class'], $listener['method']]; - case ListenerServiceEntry::class: - return function (object $event) use ($listener): void { - $this->container->get($listener['serviceName'])->{$listener['method']}($event); - }; - default: - throw new \RuntimeException(sprintf('No such listener type found in compiled container definition: %s', $listener['entryType'])); - } - } } diff --git a/src/ProviderCompiler.php b/src/ProviderCompiler.php index 65867c7..30e680e 100644 --- a/src/ProviderCompiler.php +++ b/src/ProviderCompiler.php @@ -112,6 +112,7 @@ protected function endOptimizedList(): string { return <<<'END' ]; + END; } @@ -134,6 +135,7 @@ protected function createPreamble(string $class, string $namespace): string class {$class} extends CompiledListenerProviderBase { + END; } @@ -150,6 +152,7 @@ protected function endMainListenersList(): string { return <<<'END' ]; + END; } @@ -157,6 +160,7 @@ protected function createClosing(): string { return <<<'END' } + END; } } From 3ec7252cdb79655956e49ad42b6c0addbf623ddb Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Wed, 26 Jul 2023 20:25:02 -0500 Subject: [PATCH 04/10] Pre-compute the callable for the compiled container, so it can simply be returned directly. --- benchmarks/CompiledProviderBench.php | 21 ++-- benchmarks/OptimizedCompiledProviderBench.php | 60 +-------- src/CompiledListenerProviderBase.php | 55 ++------- src/ProviderCompiler.php | 114 +++++++++++++----- 4 files changed, 107 insertions(+), 143 deletions(-) diff --git a/benchmarks/CompiledProviderBench.php b/benchmarks/CompiledProviderBench.php index c0f45e9..3708faa 100644 --- a/benchmarks/CompiledProviderBench.php +++ b/benchmarks/CompiledProviderBench.php @@ -5,6 +5,7 @@ namespace Crell\Tukio\Benchmarks; use Crell\Tukio\CollectingEvent; +use Crell\Tukio\DummyEvent; use Crell\Tukio\MockContainer; use Crell\Tukio\ProviderBuilder; use Crell\Tukio\ProviderCompiler; @@ -24,14 +25,13 @@ */ class CompiledProviderBench extends ProviderBenchBase { - /** @var string */ - protected static $filename = 'compiled_provider.php'; + protected static string $filename = 'compiled_provider.php'; - /** @var string */ - protected static $className = 'CompiledProvider'; + protected static string $className = 'CompiledProvider'; - /** @var string */ - protected static $namespace = 'Test\\Space'; + protected static string $namespace = 'Test\\Space'; + + protected static array $optimizeClasses = []; public static function createCompiledProvider(): void { @@ -41,11 +41,16 @@ 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(); } + foreach (static::$optimizeClasses as $class) { + $builder->optimizeEvent($class); + } + // Write the generated compiler out to a temp file. $out = fopen(static::$filename, 'w'); $compiler->compile($builder, $out, static::$className, static::$namespace); @@ -54,7 +59,7 @@ public static function createCompiledProvider(): void public static function removeCompiledProvider(): void { - //unlink(static::$filename); + unlink(static::$filename); } public function setUp(): void diff --git a/benchmarks/OptimizedCompiledProviderBench.php b/benchmarks/OptimizedCompiledProviderBench.php index e1cf3e2..5399149 100644 --- a/benchmarks/OptimizedCompiledProviderBench.php +++ b/benchmarks/OptimizedCompiledProviderBench.php @@ -5,67 +5,13 @@ namespace Crell\Tukio\Benchmarks; use Crell\Tukio\CollectingEvent; -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 Crell\Tukio\DummyEvent; 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; /** * @Groups({"Providers"}) - * @BeforeClassMethods({"createCompiledProvider"}) - * @AfterClassMethods({"removeCompiledProvider"}) */ -class OptimizedCompiledProviderBench extends ProviderBenchBase +class OptimizedCompiledProviderBench extends CompiledProviderBench { - /** @var string */ - protected static $filename = 'compiled_provider.php'; - - /** @var string */ - protected static $className = 'CompiledProvider'; - - /** @var string */ - protected static $namespace = 'Test\\Space'; - - public static function createCompiledProvider(): void - { - $builder = new ProviderBuilder(); - $compiler = new ProviderCompiler(); - - $priority = new \InfiniteIterator(new \ArrayIterator(static::$listenerPriorities)); - $priority->next(); - - foreach(range(1, static::$numListeners) as $counter) { - $builder->addListener([static::class, 'fakeListener'], $priority->current()); - $priority->next(); - } - - $builder->optimizeEvent(CollectingEvent::class); - - // Write the generated compiler out to a temp file. - $out = fopen(static::$filename, 'w'); - $compiler->compile($builder, $out, static::$className, static::$namespace); - fclose($out); - } - - public static function removeCompiledProvider(): void - { - //unlink(static::$filename); - } - - public function setUp(): void - { - include static::$filename; - - $container = new MockContainer(); - - $compiledClassName = static::$namespace . '\\' . static::$className; - $this->provider = new $compiledClassName($container); - } + protected static array $optimizeClasses = [CollectingEvent::class, DummyEvent::class]; } diff --git a/src/CompiledListenerProviderBase.php b/src/CompiledListenerProviderBase.php index b7855ec..4365275 100644 --- a/src/CompiledListenerProviderBase.php +++ b/src/CompiledListenerProviderBase.php @@ -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; @@ -19,9 +16,9 @@ class CompiledListenerProviderBase implements ListenerProviderInterface // entry types in the classes seen in getListenerForEvent(). See each class's getProperties() method for the // exact structure. /** @var array */ - protected const LISTENERS = []; + protected array $listeners = []; - protected const OPTIMIZED = []; + protected array $optimized = []; public function __construct(ContainerInterface $container) { @@ -33,55 +30,17 @@ public function __construct(ContainerInterface $container) */ public function getListenersForEvent(object $event): iterable { - if (isset(static::OPTIMIZED[$event::class])) { - $count = count(static::OPTIMIZED[$event::class]); - $ret = []; - for ($i = 0; $i < $count; ++$i) { - $listener = static::OPTIMIZED[$event::class]; - 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'])); - } - } - } - return $ret; + if (isset($this->optimized[$event::class])) { + return $this->optimized[$event::class]; } - $count = count(static::LISTENERS); + $count = count($this->listeners); $ret = []; for ($i = 0; $i < $count; ++$i) { /** @var array $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; diff --git a/src/ProviderCompiler.php b/src/ProviderCompiler.php index 30e680e..e1fe08e 100644 --- a/src/ProviderCompiler.php +++ b/src/ProviderCompiler.php @@ -5,6 +5,9 @@ namespace Crell\Tukio; use Crell\Tukio\Entry\CompileableListenerEntryInterface; +use Crell\Tukio\Entry\ListenerFunctionEntry; +use Crell\Tukio\Entry\ListenerServiceEntry; +use Crell\Tukio\Entry\ListenerStaticMethodEntry; class ProviderCompiler { @@ -50,18 +53,21 @@ protected function writeOptimizedList(ProviderBuilder $listeners, $stream): void { fwrite($stream, $this->startOptimizedList()); - $listenerDefs = iterator_to_array($listeners); + $listenerDefs = iterator_to_array($listeners, false); foreach ($listeners->optimizedEvents() as $event) { $ancestors = $this->classAncestors($event); fwrite($stream, $this->startOptimizedEntry($event)); - $relevantListeners = array_filter($listenerDefs, fn(CompileableListenerEntryInterface $entry) => in_array($entry->getProperties()['type'], $ancestors)); + $relevantListeners = array_filter($listenerDefs, + static fn(CompileableListenerEntryInterface $entry) + => in_array($entry->getProperties()['type'], $ancestors, true) + ); /** @var CompileableListenerEntryInterface $listenerEntry */ foreach ($relevantListeners as $listenerEntry) { - $item = $this->createEntry($listenerEntry); + $item = $this->createOptimizedEntry($listenerEntry); fwrite($stream, $item); } @@ -74,7 +80,7 @@ protected function writeOptimizedList(ProviderBuilder $listeners, $stream): void protected function startOptimizedEntry(string $event): string { return << [ + \\$event::class => [ END; } @@ -85,6 +91,76 @@ protected function endOptimizedEntry(): string END; } + protected function createOptimizedEntry(CompileableListenerEntryInterface $listenerEntry): string + { + $listener = $listenerEntry->getProperties(); + switch ($listener['entryType']) { + case ListenerFunctionEntry::class: + $ret = "'{$listener['listener']}'"; + break; + case ListenerStaticMethodEntry::class: + $ret = var_export([$listener['class'], $listener['method']], true); + break; + case ListenerServiceEntry::class: + $ret = sprintf('fn(object $event) => $this->container->get(\'%s\')->%s($event)', $listener['serviceName'], $listener['method']); + break; + default: + throw new \RuntimeException(sprintf('No such listener type found in compiled container definition: %s', $listener['entryType'])); + } + + return $ret . ',' . PHP_EOL; + } + + protected function createEntry(CompileableListenerEntryInterface $listenerEntry): string + { + $listener = $listenerEntry->getProperties(); + switch ($listener['entryType']) { + case ListenerFunctionEntry::class: + $ret = var_export(['type' => $listener['type'], 'callable' => $listener['listener']], true); + break; + case ListenerStaticMethodEntry::class: + $ret = var_export(['type' => $listener['type'], 'callable' => [$listener['class'], $listener['method']]], true); + break; + case ListenerServiceEntry::class: + $callable = sprintf('fn(object $event) => $this->container->get(\'%s\')->%s($event)', $listener['serviceName'], $listener['method']); + $ret = << '{$listener['type']}', + 'callable' => $callable, + ] +END; + + break; + default: + throw new \RuntimeException(sprintf('No such listener type found in compiled container definition: %s', $listener['entryType'])); + } + + return $ret . ',' . PHP_EOL; + } + + protected function createPreamble(string $class, string $namespace): string + { + return <<optimized = [ END; } @@ -113,36 +189,13 @@ protected function endOptimizedList(): string return <<<'END' ]; -END; - } - - protected function createEntry(CompileableListenerEntryInterface $listenerEntry): string - { - return var_export($listenerEntry->getProperties(), true) . ',' . PHP_EOL; - } - - protected function createPreamble(string $class, string $namespace): string - { - return <<listeners = [ END; @@ -159,7 +212,8 @@ protected function endMainListenersList(): string protected function createClosing(): string { return <<<'END' -} + } // Close constructor +} // Close class END; } From 11e19e9296721e229c2f1456a7a727ad3bd5c686 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Wed, 26 Jul 2023 20:27:57 -0500 Subject: [PATCH 05/10] PHPStan types. --- src/CompiledListenerProviderBase.php | 1 + src/ProviderBuilder.php | 3 +++ src/ProviderCompiler.php | 8 ++++++++ 3 files changed, 12 insertions(+) diff --git a/src/CompiledListenerProviderBase.php b/src/CompiledListenerProviderBase.php index 4365275..ad60610 100644 --- a/src/CompiledListenerProviderBase.php +++ b/src/CompiledListenerProviderBase.php @@ -18,6 +18,7 @@ class CompiledListenerProviderBase implements ListenerProviderInterface /** @var array */ protected array $listeners = []; + /** @var array */ protected array $optimized = []; public function __construct(ContainerInterface $container) diff --git a/src/ProviderBuilder.php b/src/ProviderBuilder.php index 6be7a04..44e6975 100644 --- a/src/ProviderBuilder.php +++ b/src/ProviderBuilder.php @@ -39,6 +39,9 @@ public function optimizeEvent(string $event): void $this->optimizedEvents[] = $event; } + /** + * @return array + */ public function optimizedEvents(): array { return $this->optimizedEvents; diff --git a/src/ProviderCompiler.php b/src/ProviderCompiler.php index e1fe08e..4162151 100644 --- a/src/ProviderCompiler.php +++ b/src/ProviderCompiler.php @@ -36,6 +36,10 @@ public function compile( fwrite($stream, $this->createClosing()); } + /** + * @param resource $stream + * A writeable stream to which to write the compiled code. + */ protected function writeMainListenersList(ProviderBuilder $listeners, $stream): void { fwrite($stream, $this->startMainListenersList()); @@ -49,6 +53,10 @@ protected function writeMainListenersList(ProviderBuilder $listeners, $stream): fwrite($stream, $this->endMainListenersList()); } + /** + * @param resource $stream + * A writeable stream to which to write the compiled code. + */ protected function writeOptimizedList(ProviderBuilder $listeners, $stream): void { fwrite($stream, $this->startOptimizedList()); From 9da3b2b1aa2e61c7cfeb282dbd2963c97ce16335 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Wed, 26 Jul 2023 20:49:53 -0500 Subject: [PATCH 06/10] Add optimization to README. --- README.md | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 75a6e15..61437e8 100644 --- a/README.md +++ b/README.md @@ -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. From ba7232508ec5add07b5faf843b58993045629c2e Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Wed, 26 Jul 2023 20:57:48 -0500 Subject: [PATCH 07/10] Update Docker setup for PHP 7.4. --- docker/php/74/Dockerfile | 10 ++++------ docker/php/74/xdebug.ini | 2 +- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/docker/php/74/Dockerfile b/docker/php/74/Dockerfile index ae74d50..10cdff0 100644 --- a/docker/php/74/Dockerfile +++ b/docker/php/74/Dockerfile @@ -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 diff --git a/docker/php/74/xdebug.ini b/docker/php/74/xdebug.ini index d2090a9..b1ec307 100644 --- a/docker/php/74/xdebug.ini +++ b/docker/php/74/xdebug.ini @@ -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 From 785184502d3028724e4dcf4a51cd87caf246fd69 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Wed, 26 Jul 2023 20:58:02 -0500 Subject: [PATCH 08/10] Add missing class added for benchmarks. --- tests/DummyEvent.php | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 tests/DummyEvent.php diff --git a/tests/DummyEvent.php b/tests/DummyEvent.php new file mode 100644 index 0000000..a7936b6 --- /dev/null +++ b/tests/DummyEvent.php @@ -0,0 +1,10 @@ + Date: Wed, 26 Jul 2023 20:58:14 -0500 Subject: [PATCH 09/10] Remove PHP 7.4-incompatible syntax. --- src/CompiledListenerProviderBase.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/CompiledListenerProviderBase.php b/src/CompiledListenerProviderBase.php index ad60610..114eb1e 100644 --- a/src/CompiledListenerProviderBase.php +++ b/src/CompiledListenerProviderBase.php @@ -31,8 +31,9 @@ public function __construct(ContainerInterface $container) */ public function getListenersForEvent(object $event): iterable { - if (isset($this->optimized[$event::class])) { - return $this->optimized[$event::class]; + // @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); From 8a802d91f86a3741516417c0a2d3eb852bb78d59 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Mon, 31 Jul 2023 17:08:52 -0500 Subject: [PATCH 10/10] Rename methods to allow passing multiple events to optimize at once. --- benchmarks/CompiledProviderBench.php | 4 +--- src/ProviderBuilder.php | 8 ++++---- src/ProviderCompiler.php | 2 +- tests/CompiledListenerProviderTest.php | 2 +- 4 files changed, 7 insertions(+), 9 deletions(-) diff --git a/benchmarks/CompiledProviderBench.php b/benchmarks/CompiledProviderBench.php index 3708faa..96682d9 100644 --- a/benchmarks/CompiledProviderBench.php +++ b/benchmarks/CompiledProviderBench.php @@ -47,9 +47,7 @@ public static function createCompiledProvider(): void $priority->next(); } - foreach (static::$optimizeClasses as $class) { - $builder->optimizeEvent($class); - } + $builder->optimizeEvents(...static::$optimizeClasses); // Write the generated compiler out to a temp file. $out = fopen(static::$filename, 'w'); diff --git a/src/ProviderBuilder.php b/src/ProviderBuilder.php index 44e6975..ccf90ad 100644 --- a/src/ProviderBuilder.php +++ b/src/ProviderBuilder.php @@ -32,17 +32,17 @@ public function __construct() /** * Pre-specify an event class that should have an optimized listener list built. * - * @param class-string $event + * @param class-string ...$events */ - public function optimizeEvent(string $event): void + public function optimizeEvents(string ...$events): void { - $this->optimizedEvents[] = $event; + $this->optimizedEvents = [...$this->optimizedEvents, ...$events]; } /** * @return array */ - public function optimizedEvents(): array + public function getOptimizedEvents(): array { return $this->optimizedEvents; } diff --git a/src/ProviderCompiler.php b/src/ProviderCompiler.php index 4162151..89e283f 100644 --- a/src/ProviderCompiler.php +++ b/src/ProviderCompiler.php @@ -63,7 +63,7 @@ protected function writeOptimizedList(ProviderBuilder $listeners, $stream): void $listenerDefs = iterator_to_array($listeners, false); - foreach ($listeners->optimizedEvents() as $event) { + foreach ($listeners->getOptimizedEvents() as $event) { $ancestors = $this->classAncestors($event); fwrite($stream, $this->startOptimizedEntry($event)); diff --git a/tests/CompiledListenerProviderTest.php b/tests/CompiledListenerProviderTest.php index 702521b..d919f62 100644 --- a/tests/CompiledListenerProviderTest.php +++ b/tests/CompiledListenerProviderTest.php @@ -171,7 +171,7 @@ public function test_optimize_event(): void $builder->addListenerAfter('id-2', "{$ns}event_listener_three", 'id-3'); $builder->addListenerAfter('id-3', "{$ns}event_listener_four"); - $builder->optimizeEvent(CollectingEvent::class); + $builder->optimizeEvents(CollectingEvent::class); $provider = $this->makeProvider($builder, $container, $class, $namespace);