From 2f6d3cd6c1426fca277892b8b43753c96e5fe317 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Tue, 28 Feb 2023 15:40:14 +0100 Subject: [PATCH] Improve performance of `Loop` by avoiding unneeded method calls --- src/Loop.php | 67 +++++++++++++++---- tests/LoopTest.php | 159 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 213 insertions(+), 13 deletions(-) diff --git a/src/Loop.php b/src/Loop.php index fd5d81c8..f74b9ef2 100644 --- a/src/Loop.php +++ b/src/Loop.php @@ -8,7 +8,7 @@ final class Loop { /** - * @var LoopInterface + * @var ?LoopInterface */ private static $instance; @@ -83,7 +83,11 @@ public static function set(LoopInterface $loop) */ public static function addReadStream($stream, $listener) { - self::get()->addReadStream($stream, $listener); + // create loop instance on demand (legacy PHP < 7 doesn't like ternaries in method calls) + if (self::$instance === null) { + self::get(); + } + self::$instance->addReadStream($stream, $listener); } /** @@ -97,7 +101,11 @@ public static function addReadStream($stream, $listener) */ public static function addWriteStream($stream, $listener) { - self::get()->addWriteStream($stream, $listener); + // create loop instance on demand (legacy PHP < 7 doesn't like ternaries in method calls) + if (self::$instance === null) { + self::get(); + } + self::$instance->addWriteStream($stream, $listener); } /** @@ -109,7 +117,9 @@ public static function addWriteStream($stream, $listener) */ public static function removeReadStream($stream) { - self::get()->removeReadStream($stream); + if (self::$instance !== null) { + self::$instance->removeReadStream($stream); + } } /** @@ -121,7 +131,9 @@ public static function removeReadStream($stream) */ public static function removeWriteStream($stream) { - self::get()->removeWriteStream($stream); + if (self::$instance !== null) { + self::$instance->removeWriteStream($stream); + } } /** @@ -134,7 +146,11 @@ public static function removeWriteStream($stream) */ public static function addTimer($interval, $callback) { - return self::get()->addTimer($interval, $callback); + // create loop instance on demand (legacy PHP < 7 doesn't like ternaries in method calls) + if (self::$instance === null) { + self::get(); + } + return self::$instance->addTimer($interval, $callback); } /** @@ -147,7 +163,11 @@ public static function addTimer($interval, $callback) */ public static function addPeriodicTimer($interval, $callback) { - return self::get()->addPeriodicTimer($interval, $callback); + // create loop instance on demand (legacy PHP < 7 doesn't like ternaries in method calls) + if (self::$instance === null) { + self::get(); + } + return self::$instance->addPeriodicTimer($interval, $callback); } /** @@ -159,7 +179,9 @@ public static function addPeriodicTimer($interval, $callback) */ public static function cancelTimer(TimerInterface $timer) { - return self::get()->cancelTimer($timer); + if (self::$instance !== null) { + self::$instance->cancelTimer($timer); + } } /** @@ -171,7 +193,12 @@ public static function cancelTimer(TimerInterface $timer) */ public static function futureTick($listener) { - self::get()->futureTick($listener); + // create loop instance on demand (legacy PHP < 7 doesn't like ternaries in method calls) + if (self::$instance === null) { + self::get(); + } + + self::$instance->futureTick($listener); } /** @@ -184,7 +211,12 @@ public static function futureTick($listener) */ public static function addSignal($signal, $listener) { - self::get()->addSignal($signal, $listener); + // create loop instance on demand (legacy PHP < 7 doesn't like ternaries in method calls) + if (self::$instance === null) { + self::get(); + } + + self::$instance->addSignal($signal, $listener); } /** @@ -197,7 +229,9 @@ public static function addSignal($signal, $listener) */ public static function removeSignal($signal, $listener) { - self::get()->removeSignal($signal, $listener); + if (self::$instance !== null) { + self::$instance->removeSignal($signal, $listener); + } } /** @@ -208,7 +242,12 @@ public static function removeSignal($signal, $listener) */ public static function run() { - self::get()->run(); + // create loop instance on demand (legacy PHP < 7 doesn't like ternaries in method calls) + if (self::$instance === null) { + self::get(); + } + + self::$instance->run(); } /** @@ -220,6 +259,8 @@ public static function run() public static function stop() { self::$stopped = true; - self::get()->stop(); + if (self::$instance !== null) { + self::$instance->stop(); + } } } diff --git a/tests/LoopTest.php b/tests/LoopTest.php index f3a13d34..833539ef 100644 --- a/tests/LoopTest.php +++ b/tests/LoopTest.php @@ -62,6 +62,19 @@ public function testStaticAddReadStreamCallsAddReadStreamOnLoopInstance() Loop::addReadStream($stream, $listener); } + public function testStaticAddReadStreamWithNoDefaultLoopCallsAddReadStreamOnNewLoopInstance() + { + $ref = new \ReflectionProperty('React\EventLoop\Loop', 'instance'); + $ref->setAccessible(true); + $ref->setValue(null); + + $stream = stream_socket_server('127.0.0.1:0'); + $listener = function () { }; + Loop::addReadStream($stream, $listener); + + $this->assertInstanceOf('React\EventLoop\LoopInterface', $ref->getValue()); + } + public function testStaticAddWriteStreamCallsAddWriteStreamOnLoopInstance() { $stream = tmpfile(); @@ -75,6 +88,19 @@ public function testStaticAddWriteStreamCallsAddWriteStreamOnLoopInstance() Loop::addWriteStream($stream, $listener); } + public function testStaticAddWriteStreamWithNoDefaultLoopCallsAddWriteStreamOnNewLoopInstance() + { + $ref = new \ReflectionProperty('React\EventLoop\Loop', 'instance'); + $ref->setAccessible(true); + $ref->setValue(null); + + $stream = stream_socket_server('127.0.0.1:0'); + $listener = function () { }; + Loop::addWriteStream($stream, $listener); + + $this->assertInstanceOf('React\EventLoop\LoopInterface', $ref->getValue()); + } + public function testStaticRemoveReadStreamCallsRemoveReadStreamOnLoopInstance() { $stream = tmpfile(); @@ -87,6 +113,18 @@ public function testStaticRemoveReadStreamCallsRemoveReadStreamOnLoopInstance() Loop::removeReadStream($stream); } + public function testStaticRemoveReadStreamWithNoDefaultLoopIsNoOp() + { + $ref = new \ReflectionProperty('React\EventLoop\Loop', 'instance'); + $ref->setAccessible(true); + $ref->setValue(null); + + $stream = tmpfile(); + Loop::removeReadStream($stream); + + $this->assertNull($ref->getValue()); + } + public function testStaticRemoveWriteStreamCallsRemoveWriteStreamOnLoopInstance() { $stream = tmpfile(); @@ -99,6 +137,18 @@ public function testStaticRemoveWriteStreamCallsRemoveWriteStreamOnLoopInstance( Loop::removeWriteStream($stream); } + public function testStaticRemoveWriteStreamWithNoDefaultLoopIsNoOp() + { + $ref = new \ReflectionProperty('React\EventLoop\Loop', 'instance'); + $ref->setAccessible(true); + $ref->setValue(null); + + $stream = tmpfile(); + Loop::removeWriteStream($stream); + + $this->assertNull($ref->getValue()); + } + public function testStaticAddTimerCallsAddTimerOnLoopInstanceAndReturnsTimerInstance() { $interval = 1.0; @@ -115,6 +165,20 @@ public function testStaticAddTimerCallsAddTimerOnLoopInstanceAndReturnsTimerInst $this->assertSame($timer, $ret); } + public function testStaticAddTimerWithNoDefaultLoopCallsAddTimerOnNewLoopInstance() + { + $ref = new \ReflectionProperty('React\EventLoop\Loop', 'instance'); + $ref->setAccessible(true); + $ref->setValue(null); + + $interval = 1.0; + $callback = function () { }; + $ret = Loop::addTimer($interval, $callback); + + $this->assertInstanceOf('React\EventLoop\TimerInterface', $ret); + $this->assertInstanceOf('React\EventLoop\LoopInterface', $ref->getValue()); + } + public function testStaticAddPeriodicTimerCallsAddPeriodicTimerOnLoopInstanceAndReturnsTimerInstance() { $interval = 1.0; @@ -131,6 +195,21 @@ public function testStaticAddPeriodicTimerCallsAddPeriodicTimerOnLoopInstanceAnd $this->assertSame($timer, $ret); } + public function testStaticAddPeriodicTimerWithNoDefaultLoopCallsAddPeriodicTimerOnNewLoopInstance() + { + $ref = new \ReflectionProperty('React\EventLoop\Loop', 'instance'); + $ref->setAccessible(true); + $ref->setValue(null); + + $interval = 1.0; + $callback = function () { }; + $ret = Loop::addPeriodicTimer($interval, $callback); + + $this->assertInstanceOf('React\EventLoop\TimerInterface', $ret); + $this->assertInstanceOf('React\EventLoop\LoopInterface', $ref->getValue()); + } + + public function testStaticCancelTimerCallsCancelTimerOnLoopInstance() { $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); @@ -143,6 +222,18 @@ public function testStaticCancelTimerCallsCancelTimerOnLoopInstance() Loop::cancelTimer($timer); } + public function testStaticCancelTimerWithNoDefaultLoopIsNoOp() + { + $ref = new \ReflectionProperty('React\EventLoop\Loop', 'instance'); + $ref->setAccessible(true); + $ref->setValue(null); + + $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); + Loop::cancelTimer($timer); + + $this->assertNull($ref->getValue()); + } + public function testStaticFutureTickCallsFutureTickOnLoopInstance() { $listener = function () { }; @@ -155,6 +246,18 @@ public function testStaticFutureTickCallsFutureTickOnLoopInstance() Loop::futureTick($listener); } + public function testStaticFutureTickWithNoDefaultLoopCallsFutureTickOnNewLoopInstance() + { + $ref = new \ReflectionProperty('React\EventLoop\Loop', 'instance'); + $ref->setAccessible(true); + $ref->setValue(null); + + $listener = function () { }; + Loop::futureTick($listener); + + $this->assertInstanceOf('React\EventLoop\LoopInterface', $ref->getValue()); + } + public function testStaticAddSignalCallsAddSignalOnLoopInstance() { $signal = 1; @@ -168,6 +271,27 @@ public function testStaticAddSignalCallsAddSignalOnLoopInstance() Loop::addSignal($signal, $listener); } + public function testStaticAddSignalWithNoDefaultLoopCallsAddSignalOnNewLoopInstance() + { + if (DIRECTORY_SEPARATOR === '\\') { + $this->markTestSkipped('Not supported on Windows'); + } + + $ref = new \ReflectionProperty('React\EventLoop\Loop', 'instance'); + $ref->setAccessible(true); + $ref->setValue(null); + + $signal = 1; + $listener = function () { }; + try { + Loop::addSignal($signal, $listener); + } catch (\BadMethodCallException $e) { + $this->markTestSkipped('Skipped: ' . $e->getMessage()); + } + + $this->assertInstanceOf('React\EventLoop\LoopInterface', $ref->getValue()); + } + public function testStaticRemoveSignalCallsRemoveSignalOnLoopInstance() { $signal = 1; @@ -181,6 +305,19 @@ public function testStaticRemoveSignalCallsRemoveSignalOnLoopInstance() Loop::removeSignal($signal, $listener); } + public function testStaticRemoveSignalWithNoDefaultLoopIsNoOp() + { + $ref = new \ReflectionProperty('React\EventLoop\Loop', 'instance'); + $ref->setAccessible(true); + $ref->setValue(null); + + $signal = 1; + $listener = function () { }; + Loop::removeSignal($signal, $listener); + + $this->assertNull($ref->getValue()); + } + public function testStaticRunCallsRunOnLoopInstance() { $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); @@ -191,6 +328,17 @@ public function testStaticRunCallsRunOnLoopInstance() Loop::run(); } + public function testStaticRunWithNoDefaultLoopCallsRunsOnNewLoopInstance() + { + $ref = new \ReflectionProperty('React\EventLoop\Loop', 'instance'); + $ref->setAccessible(true); + $ref->setValue(null); + + Loop::run(); + + $this->assertInstanceOf('React\EventLoop\LoopInterface', $ref->getValue()); + } + public function testStaticStopCallsStopOnLoopInstance() { $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); @@ -201,6 +349,17 @@ public function testStaticStopCallsStopOnLoopInstance() Loop::stop(); } + public function testStaticStopCallWithNoDefaultLoopIsNoOp() + { + $ref = new \ReflectionProperty('React\EventLoop\Loop', 'instance'); + $ref->setAccessible(true); + $ref->setValue(null); + + Loop::stop(); + + $this->assertNull($ref->getValue()); + } + /** * @after * @before