From db41311c7d380a003d3f421d7033555347791c9d Mon Sep 17 00:00:00 2001 From: James Harris Date: Wed, 13 Nov 2013 12:35:50 +1000 Subject: [PATCH 01/24] Implemented some experimental event loops with nextTick() support. * ExtEventLoop - based on the ext-event PHP extension * StreamSelectNextTickLoop - based on stream_select --- src/React/EventLoop/AbstractNextTickLoop.php | 180 ++++++++++ src/React/EventLoop/ExtEventLoop.php | 312 ++++++++++++++++++ src/React/EventLoop/LoopInterface.php | 78 +++++ src/React/EventLoop/NextTickLoopInterface.php | 21 ++ .../EventLoop/StreamSelectNextTickLoop.php | 312 ++++++++++++++++++ .../Tests/EventLoop/AbstractLoopTest.php | 63 +++- .../Tests/EventLoop/ExtEventLoopTest.php | 39 +++ .../Tests/EventLoop/NextTickTestTrait.php | 99 ++++++ .../StreamSelectNextTickLoopTest.php | 32 ++ .../EventLoop/Timer/ExtEventTimerTest.php | 17 + .../Timer/StreamSelectNextTickTimerTest.php | 13 + 11 files changed, 1148 insertions(+), 18 deletions(-) create mode 100644 src/React/EventLoop/AbstractNextTickLoop.php create mode 100644 src/React/EventLoop/ExtEventLoop.php create mode 100644 src/React/EventLoop/NextTickLoopInterface.php create mode 100644 src/React/EventLoop/StreamSelectNextTickLoop.php create mode 100644 tests/React/Tests/EventLoop/ExtEventLoopTest.php create mode 100644 tests/React/Tests/EventLoop/NextTickTestTrait.php create mode 100644 tests/React/Tests/EventLoop/StreamSelectNextTickLoopTest.php create mode 100644 tests/React/Tests/EventLoop/Timer/ExtEventTimerTest.php create mode 100644 tests/React/Tests/EventLoop/Timer/StreamSelectNextTickTimerTest.php diff --git a/src/React/EventLoop/AbstractNextTickLoop.php b/src/React/EventLoop/AbstractNextTickLoop.php new file mode 100644 index 00000000..f3eedd95 --- /dev/null +++ b/src/React/EventLoop/AbstractNextTickLoop.php @@ -0,0 +1,180 @@ +nextTickQueue = new SplQueue; + $this->explicitlyStopped = false; + } + + /** + * Enqueue a callback to be invoked once after the given interval. + * + * The execution order of timers scheduled to execute at the same time is + * not guaranteed. + * + * @param numeric $interval The number of seconds to wait before execution. + * @param callable $callback The callback to invoke. + * + * @return TimerInterface + */ + public function addTimer($interval, $callback) + { + $timer = new Timer($this, $interval, $callback, false); + + $this->scheduleTimer($timer); + + return $timer; + } + + /** + * Enqueue a callback to be invoked repeatedly after the given interval. + * + * The execution order of timers scheduled to execute at the same time is + * not guaranteed. + * + * @param numeric $interval The number of seconds to wait before execution. + * @param callable $callback The callback to invoke. + * + * @return TimerInterface + */ + public function addPeriodicTimer($interval, $callback) + { + $timer = new Timer($this, $interval, $callback, true); + + $this->scheduleTimer($timer); + + return $timer; + } + + /** + * Schedule a callback to be invoked on the next tick of the event loop. + * + * Callbacks are guaranteed to be executed in the order they are enqueued, + * before any timer or stream events. + * + * @param callable $listner The callback to invoke. + */ + public function nextTick(callable $listener) + { + $this->nextTickQueue->enqueue($listener); + } + + /** + * Perform a single iteration of the event loop. + */ + public function tick() + { + $this->tickLogic(false); + } + + /** + * Run the event loop until there are no more tasks to perform. + */ + public function run() + { + $this->explicitlyStopped = false; + + while ($this->isRunning()) { + $this->tickLogic(true); + } + } + + /** + * Instruct a running event loop to stop. + */ + public function stop() + { + $this->explicitlyStopped = true; + } + + /** + * Invoke all callbacks in the next-tick queue. + */ + protected function flushNextTickQueue() + { + while ($this->nextTickQueue->count()) { + call_user_func( + $this->nextTickQueue->dequeue(), + $this + ); + } + } + + /** + * Check if there is any pending work to do. + * + * @return boolean + */ + protected function isRunning() + { + // The loop has been explicitly stopped and should exit ... + if ($this->explicitlyStopped) { + return false; + + // The next tick queue has items on it ... + } elseif ($this->nextTickQueue->count() > 0) { + return true; + } + + return !$this->isEmpty(); + } + + /** + * Perform the low-level tick logic. + */ + protected function tickLogic($blocking) + { + $this->flushNextTickQueue(); + + $this->flushEvents( + $blocking && 0 === $this->nextTickQueue->count() + ); + } + + /** + * Get a key that can be used to identify a stream resource. + * + * @param string $stream + * + * @return integer|string + */ + protected function streamKey($stream) + { + return (int) $stream; + } + + /** + * Schedule a timer for execution. + * + * @param TimerInterface $timer + */ + abstract protected function scheduleTimer(TimerInterface $timer); + + /** + * Flush any timer and IO events. + * + * @param boolean $blocking True if loop should block waiting for next event. + */ + abstract protected function flushEvents($blocking); + + /** + * Check if the loop has any pending timers or streams. + * + * @return boolean + */ + abstract protected function isEmpty(); +} diff --git a/src/React/EventLoop/ExtEventLoop.php b/src/React/EventLoop/ExtEventLoop.php new file mode 100644 index 00000000..91917f88 --- /dev/null +++ b/src/React/EventLoop/ExtEventLoop.php @@ -0,0 +1,312 @@ +eventBase = $eventBase; + $this->timerEvents = new SplObjectStorage; + $this->streamEvents = []; + + // Closures for cancelled timers and removed stream listeners are + // kept in the keepAlive array until the flushEvents() is complete + // to prevent the PHP fatal error caused by ext-event: + // "Cannot destroy active lambda function" + $this->keepAlive = []; + + parent::__construct(); + } + + /** + * Register a listener to be notified when a stream is ready to read. + * + * @param stream $stream The PHP stream resource to check. + * @param callable $listener Invoked when the stream is ready. + */ + public function addReadStream($stream, $listener) + { + $this->addStreamEvent($stream, Event::READ, $listener); + } + + /** + * Register a listener to be notified when a stream is ready to write. + * + * @param stream $stream The PHP stream resource to check. + * @param callable $listener Invoked when the stream is ready. + */ + public function addWriteStream($stream, $listener) + { + $this->addStreamEvent($stream, Event::WRITE, $listener); + } + + /** + * Remove the read event listener for the given stream. + * + * @param stream $stream The PHP stream resource. + */ + public function removeReadStream($stream) + { + $this->removeStreamEvent($stream, Event::READ); + } + + /** + * Remove the write event listener for the given stream. + * + * @param stream $stream The PHP stream resource. + */ + public function removeWriteStream($stream) + { + $this->removeStreamEvent($stream, Event::WRITE); + } + + /** + * Remove all listeners for the given stream. + * + * @param stream $stream The PHP stream resource. + */ + public function removeStream($stream) + { + $key = $this->streamKey($stream); + + if (!array_key_exists($key, $this->streamEvents)) { + return; + } + + $entry = $this->streamEvents[$key]; + + $entry->event->free(); + + unset($this->streamEvents[$key]); + + $this->keepAlive[] = $entry->callback; + } + + /** + * Cancel a pending timer. + * + * @param TimerInterface $timer The timer to cancel. + */ + public function cancelTimer(TimerInterface $timer) + { + if ($this->isTimerActive($timer)) { + $entry = $this->timerEvents[$timer]; + $this->timerEvents->detach($timer); + $entry->event->free(); + $this->keepAlive[] = $entry->callback; + } + } + + /** + * Check if a given timer is active. + * + * @param TimerInterface $timer The timer to check. + * + * @return boolean True if the timer is still enqueued for execution. + */ + public function isTimerActive(TimerInterface $timer) + { + return $this->timerEvents->contains($timer); + } + + /** + * Flush any timer and IO events. + * + * @param boolean $blocking True if loop should block waiting for next event. + */ + protected function flushEvents($blocking) + { + $flags = EventBase::LOOP_ONCE; + + if (!$blocking) { + $flags |= EventBase::LOOP_NONBLOCK; + } + + $this->eventBase->loop($flags); + + $this->keepAlive = []; + } + + /** + * Check if the loop has any pending timers or streams. + * + * @return boolean + */ + protected function isEmpty() + { + return 0 === count($this->timerEvents) + && 0 === count($this->streamEvents); + } + + /** + * Dispatch a timer event. + * + * @param TimerInterface $timer + */ + protected function onTimerTick(TimerInterface $timer) + { + call_user_func($timer->getCallback(), $timer); + + // Clean-up one shot timers ... + if ($this->isTimerActive($timer) && !$timer->isPeriodic()) { + $this->cancelTimer($timer); + } + } + + /** + * Dispatch a stream event. + * + * @param stdClass $entry The entry from $this->streamEvents + * @param stream $stream + * @param integer $flags Bitwise flags indicating event type (Event::READ/Event::WRITE) + */ + protected function onStreamEvent($entry, $stream, $flags) + { + foreach ([Event::READ, Event::WRITE] as $flag) { + if ( + $flag === ($flags & $flag) && + is_callable($entry->listeners[$flag]) + ) { + call_user_func( + $entry->listeners[$flag], + $stream, + $this + ); + } + } + } + + /** + * Schedule a timer for execution. + * + * @param TimerInterface $timer + */ + protected function scheduleTimer(TimerInterface $timer) + { + $flags = Event::TIMEOUT; + + if ($timer->isPeriodic()) { + $flags |= Event::PERSIST; + } + + $entry = new stdClass; + $entry->callback = function () use ($timer) { + $this->onTimerTick($timer); + }; + + $entry->event = new Event( + $this->eventBase, + -1, + $flags, + $entry->callback + ); + + $this->timerEvents->attach($timer, $entry); + + $entry->event->add($timer->getInterval()); + } + + /** + * Create a new ext-event Event object, or update the existing one. + * + * @param stream $stream + * @param integer $flag Event::READ or Event::WRITE + * @param callable $listener + */ + protected function addStreamEvent($stream, $flag, $listener) + { + $key = $this->streamKey($stream); + + if (array_key_exists($key, $this->streamEvents)) { + $entry = $this->streamEvents[$key]; + } else { + $entry = new stdClass; + $entry->callback = function ($stream, $flags, $loop) use ($entry) { + $this->onStreamEvent($entry, $stream, $flags); + }; + $entry->event = null; + $entry->flags = 0; + $entry->listeners = [ + Event::READ => null, + Event::WRITE => null, + ]; + + $this->streamEvents[$key] = $entry; + } + + $entry->listeners[$flag] = $listener; + $entry->flags |= $flag; + + $this->configureStreamEvent($entry, $stream); + + $entry->event->add(); + } + + /** + * Update the ext-event Event object for this stream to stop listening to + * the given event type, or remove it entirely if it's no longer needed. + * + * @param stream $stream + * @param integer $flag Event::READ or Event::WRITE + */ + protected function removeStreamEvent($stream, $flag) + { + $key = $this->streamKey($stream); + + if (!array_key_exists($key, $this->streamEvents)) { + return; + } + + $entry = $this->streamEvents[$key]; + $entry->flags &= ~$flag; + $entry->listeners[$flag] = null; + + if (0 === $entry->flags) { + $this->removeStream($stream); + } else { + $this->configureStreamEvent($entry, $stream); + } + } + + /** + * Create or update an ext-event Event object for the stream. + */ + protected function configureStreamEvent($entry, $stream) + { + $flags = $entry->flags | Event::PERSIST; + + if ($entry->event) { + $entry->event->del(); + $entry->event->set( + $this->eventBase, $stream, $flags, $entry->callback + ); + $entry->event->add(); + } else { + $entry->event = new Event( + $this->eventBase, $stream, $flags, $entry->callback + ); + } + } +} diff --git a/src/React/EventLoop/LoopInterface.php b/src/React/EventLoop/LoopInterface.php index 37015523..ce1dcaa4 100644 --- a/src/React/EventLoop/LoopInterface.php +++ b/src/React/EventLoop/LoopInterface.php @@ -6,19 +6,97 @@ interface LoopInterface { + /** + * Register a listener to be notified when a stream is ready to read. + * + * @param stream $stream The PHP stream resource to check. + * @param callable $listener Invoked when the stream is ready. + */ public function addReadStream($stream, $listener); + + /** + * Register a listener to be notified when a stream is ready to write. + * + * @param stream $stream The PHP stream resource to check. + * @param callable $listener Invoked when the stream is ready. + */ public function addWriteStream($stream, $listener); + /** + * Remove the read event listener for the given stream. + * + * @param stream $stream The PHP stream resource. + */ public function removeReadStream($stream); + + /** + * Remove the write event listener for the given stream. + * + * @param stream $stream The PHP stream resource. + */ public function removeWriteStream($stream); + + /** + * Remove all listeners for the given stream. + * + * @param stream $stream The PHP stream resource. + */ public function removeStream($stream); + /** + * Enqueue a callback to be invoked once after the given interval. + * + * The execution order of timers scheduled to execute at the same time is + * not guaranteed. + * + * @param numeric $interval The number of seconds to wait before execution. + * @param callable $callback The callback to invoke. + * + * @return TimerInterface + */ public function addTimer($interval, $callback); + + /** + * Enqueue a callback to be invoked repeatedly after the given interval. + * + * The execution order of timers scheduled to execute at the same time is + * not guaranteed. + * + * @param numeric $interval The number of seconds to wait before execution. + * @param callable $callback The callback to invoke. + * + * @return TimerInterface + */ public function addPeriodicTimer($interval, $callback); + + /** + * Cancel a pending timer. + * + * @param TimerInterface $timer The timer to cancel. + */ public function cancelTimer(TimerInterface $timer); + + /** + * Check if a given timer is active. + * + * @param TimerInterface $timer The timer to check. + * + * @return boolean True if the timer is still enqueued for execution. + */ public function isTimerActive(TimerInterface $timer); + /** + * Perform a single iteration of the event loop. + */ public function tick(); + + /** + * Run the event loop until there are no more tasks to perform. + */ public function run(); + + /** + * Instruct a running event loop to stop. + */ public function stop(); } diff --git a/src/React/EventLoop/NextTickLoopInterface.php b/src/React/EventLoop/NextTickLoopInterface.php new file mode 100644 index 00000000..8fb65c51 --- /dev/null +++ b/src/React/EventLoop/NextTickLoopInterface.php @@ -0,0 +1,21 @@ +timerQueue = new SplPriorityQueue; + $this->timerTimestamps = new SplObjectStorage; + + parent::__construct(); + } + + /** + * Register a listener to be notified when a stream is ready to read. + * + * @param stream $stream The PHP stream resource to check. + * @param callable $listener Invoked when the stream is ready. + */ + public function addReadStream($stream, $listener) + { + $key = $this->streamKey($stream); + + if (!array_key_exists($key, $this->readStreams)) { + $this->readStreams[$key] = $stream; + $this->readListeners[$key] = $listener; + } + } + + /** + * Register a listener to be notified when a stream is ready to write. + * + * @param stream $stream The PHP stream resource to check. + * @param callable $listener Invoked when the stream is ready. + */ + public function addWriteStream($stream, $listener) + { + $key = $this->streamKey($stream); + + if (!array_key_exists($key, $this->writeStreams)) { + $this->writeStreams[$key] = $stream; + $this->writeListeners[$key] = $listener; + } + } + + /** + * Remove the read event listener for the given stream. + * + * @param stream $stream The PHP stream resource. + */ + public function removeReadStream($stream) + { + $key = $this->streamKey($stream); + + unset( + $this->readStreams[$key], + $this->readListeners[$key] + ); + } + + /** + * Remove the write event listener for the given stream. + * + * @param stream $stream The PHP stream resource. + */ + public function removeWriteStream($stream) + { + $key = $this->streamKey($stream); + + unset( + $this->writeStreams[$key], + $this->writeListeners[$key] + ); + } + + /** + * Remove all listeners for the given stream. + * + * @param stream $stream The PHP stream resource. + */ + public function removeStream($stream) + { + $this->removeReadStream($stream); + $this->removeWriteStream($stream); + } + + /** + * Cancel a pending timer. + * + * @param TimerInterface $timer The timer to cancel. + */ + public function cancelTimer(TimerInterface $timer) + { + $this->timerTimestamps->detach($timer); + } + + /** + * Check if a given timer is active. + * + * @param TimerInterface $timer The timer to check. + * + * @return boolean True if the timer is still enqueued for execution. + */ + public function isTimerActive(TimerInterface $timer) + { + return $this->timerTimestamps->contains($timer); + } + + /** + * Flush any timer and IO events. + * + * @param boolean $blocking True if loop should block waiting for next event. + */ + protected function flushEvents($blocking) + { + $this->flushTimerQueue(); + $this->waitForStreamActivity($blocking); + } + + /** + * Check if the loop has any pending timers or streams. + * + * @return boolean + */ + protected function isEmpty() + { + return 0 === count($this->timerTimestamps) + && 0 === count($this->readStreams) + && 0 === count($this->writeStreams); + } + + /** + * Get the current time in microseconds. + * + * @return integer + */ + protected function now() + { + return $this->toMicroSeconds( + microtime(true) + ); + } + + /** + * Convert the given time to microseconds. + * + * @param integer|float $seconds + * + * @return integer + */ + protected function toMicroSeconds($seconds) + { + return intval($seconds * 1000000); + } + + /** + * Emulate a stream_select() implementation that does not break when passed + * empty stream arrays. + * + * @param array &$read An array of read streams to select upon. + * @param array &$write An array of write streams to select upon. + * @param integer|null $timeout Activity timeout in microseconds, or null to wait forever. + */ + protected function streamSelect(array &$read, array &$write, $timeout) + { + if ($read || $write) { + $except = null; + + return stream_select( + $read, + $write, + $except, + $timeout === null ? null : 0, + $timeout + ); + } + + usleep($timeout); + + return 0; + } + + /** + * Schedule a timer for execution. + * + * @param TimerInterface $timer + */ + protected function scheduleTimer(TimerInterface $timer) + { + $executeAt = $this->now() + $this->toMicroSeconds( + $timer->getInterval() + ); + + $this->timerQueue->insert($timer, -$executeAt); + $this->timerTimestamps->attach($timer, $executeAt); + } + + /** + * Get the timer next schedule to tick, if any. + * + * @return TimerInterface|null + */ + protected function nextActiveTimer() + { + while ($this->timerQueue->count()) { + $timer = $this->timerQueue->top(); + + if ($this->isTimerActive($timer)) { + return $timer; + } else { + $this->timerQueue->extract(); + } + } + + return null; + } + + /** + * Push callbacks for timers that are ready into the next-tick queue. + */ + protected function flushTimerQueue() + { + $now = $this->now(); + + while ($timer = $this->nextActiveTimer()) { + + $executeAt = $this->timerTimestamps[$timer]; + + // The next time is in the future, exit the loop ... + if ($executeAt > $now) { + break; + } + + $this->timerQueue->extract(); + + call_user_func($timer->getCallback(), $timer); + + // Timer cancelled itself ... + if (!$this->isTimerActive($timer)) { + return; + // Reschedule periodic timers ... + } elseif ($timer->isPeriodic()) { + $this->scheduleTimer($timer); + // Cancel one-shot timers ... + } else { + $this->cancelTimer($timer); + } + } + } + + protected function waitForStreamActivity($blocking) + { + // The $blocking flag takes precedence ... + if (!$blocking) { + $timeout = 0; + + // There is a pending timer, only block until it is due ... + } elseif ($timer = $this->nextActiveTimer()) { + $timeout = max( + 0, + $this->timerTimestamps[$timer] - $this->now() + ); + + // The only possible event is stream activity, so wait forever ... + } elseif ($this->readStreams || $this->writeStreams) { + $timeout = null; + + // THere's nothing left to do ... + } else { + return; + } + + $read = $this->readStreams; + $write = $this->writeStreams; + + $this->streamSelect($read, $write, $timeout); + + $this->flushStreamEvents($read, $this->readListeners); + $this->flushStreamEvents($write, $this->writeListeners); + } + + protected function flushStreamEvents(array $streams, array &$listeners) + { + foreach ($streams as $stream) { + $key = $this->streamKey($stream); + + if (!array_key_exists($key, $listeners)) { + continue; + } + + call_user_func($listeners[$key], $stream, $this); + } + } +} diff --git a/tests/React/Tests/EventLoop/AbstractLoopTest.php b/tests/React/Tests/EventLoop/AbstractLoopTest.php index ec2d51d1..168f89ed 100644 --- a/tests/React/Tests/EventLoop/AbstractLoopTest.php +++ b/tests/React/Tests/EventLoop/AbstractLoopTest.php @@ -15,9 +15,14 @@ public function setUp() abstract public function createLoop(); + public function createStream() + { + return fopen('php://temp', 'r+'); + } + public function testAddReadStream() { - $input = fopen('php://temp', 'r+'); + $input = $this->createStream(); $this->loop->addReadStream($input, $this->expectCallableExactly(2)); @@ -32,7 +37,7 @@ public function testAddReadStream() public function testAddWriteStream() { - $input = fopen('php://temp', 'r+'); + $input = $this->createStream(); $this->loop->addWriteStream($input, $this->expectCallableExactly(2)); $this->loop->tick(); @@ -41,7 +46,7 @@ public function testAddWriteStream() public function testRemoveReadStreamInstantly() { - $input = fopen('php://temp', 'r+'); + $input = $this->createStream(); $this->loop->addReadStream($input, $this->expectCallableNever()); $this->loop->removeReadStream($input); @@ -53,7 +58,7 @@ public function testRemoveReadStreamInstantly() public function testRemoveReadStreamAfterReading() { - $input = fopen('php://temp', 'r+'); + $input = $this->createStream(); $this->loop->addReadStream($input, $this->expectCallableOnce()); @@ -70,7 +75,7 @@ public function testRemoveReadStreamAfterReading() public function testRemoveWriteStreamInstantly() { - $input = fopen('php://temp', 'r+'); + $input = $this->createStream(); $this->loop->addWriteStream($input, $this->expectCallableNever()); $this->loop->removeWriteStream($input); @@ -79,7 +84,7 @@ public function testRemoveWriteStreamInstantly() public function testRemoveWriteStreamAfterWriting() { - $input = fopen('php://temp', 'r+'); + $input = $this->createStream(); $this->loop->addWriteStream($input, $this->expectCallableOnce()); $this->loop->tick(); @@ -90,7 +95,7 @@ public function testRemoveWriteStreamAfterWriting() public function testRemoveStreamInstantly() { - $input = fopen('php://temp', 'r+'); + $input = $this->createStream(); $this->loop->addReadStream($input, $this->expectCallableNever()); $this->loop->addWriteStream($input, $this->expectCallableNever()); @@ -101,9 +106,33 @@ public function testRemoveStreamInstantly() $this->loop->tick(); } + public function testRemoveStreamForReadOnly() + { + $input = $this->createStream(); + + $this->loop->addReadStream($input, $this->expectCallableNever()); + $this->loop->addWriteStream($input, $this->expectCallableOnce()); + $this->loop->removeReadStream($input); + + fwrite($input, "foo\n"); + rewind($input); + $this->loop->tick(); + } + + public function testRemoveStreamForWriteOnly() + { + $input = $this->createStream(); + + $this->loop->addReadStream($input, $this->expectCallableOnce()); + $this->loop->addWriteStream($input, $this->expectCallableNever()); + $this->loop->removeWriteStream($input); + + $this->loop->tick(); + } + public function testRemoveStream() { - $input = fopen('php://temp', 'r+'); + $input = $this->createStream(); $this->loop->addReadStream($input, $this->expectCallableOnce()); $this->loop->addWriteStream($input, $this->expectCallableOnce()); @@ -121,7 +150,7 @@ public function testRemoveStream() public function testRemoveInvalid() { - $stream = fopen('php://temp', 'r+'); + $stream = $this->createStream(); // remove a valid stream from the event loop that was never added in the first place $this->loop->removeReadStream($stream); @@ -138,7 +167,7 @@ public function emptyRunShouldSimplyReturn() /** @test */ public function runShouldReturnWhenNoMoreFds() { - $input = fopen('php://temp', 'r+'); + $input = $this->createStream(); $loop = $this->loop; $this->loop->addReadStream($input, function ($stream) use ($loop) { @@ -154,7 +183,7 @@ public function runShouldReturnWhenNoMoreFds() /** @test */ public function stopShouldStopRunningLoop() { - $input = fopen('php://temp', 'r+'); + $input = $this->createStream(); $loop = $this->loop; $this->loop->addReadStream($input, function ($stream) use ($loop) { @@ -170,8 +199,8 @@ public function stopShouldStopRunningLoop() public function testIgnoreRemovedCallback() { // two independent streams, both should be readable right away - $stream1 = fopen('php://temp', 'r+'); - $stream2 = fopen('php://temp', 'r+'); + $stream1 = $this->createStream(); + $stream2 = $this->createStream(); $loop = $this->loop; $loop->addReadStream($stream1, function ($stream) use ($loop, $stream2) { @@ -179,11 +208,9 @@ public function testIgnoreRemovedCallback() $loop->removeReadStream($stream); $loop->removeReadStream($stream2); }); - $loop->addReadStream($stream2, function ($stream) use ($loop, $stream1) { - // this callback would have to be called as well, but the first stream already removed us - $loop->removeReadStream($stream); - $loop->removeReadStream($stream1); - }); + + // this callback would have to be called as well, but the first stream already removed us + $loop->addReadStream($stream2, $this->expectCallableNever()); fwrite($stream1, "foo\n"); rewind($stream1); diff --git a/tests/React/Tests/EventLoop/ExtEventLoopTest.php b/tests/React/Tests/EventLoop/ExtEventLoopTest.php new file mode 100644 index 00000000..dde5a731 --- /dev/null +++ b/tests/React/Tests/EventLoop/ExtEventLoopTest.php @@ -0,0 +1,39 @@ +markTestSkipped('libevent tests skipped on linux due to linux epoll issues.'); + } + + if (!extension_loaded('event')) { + $this->markTestSkipped('ext-event tests skipped because ext-event is not installed.'); + } + + return new ExtEventLoop; + } + + public function createStream() + { + // ext-event (as of 1.8.1) does not yet support in-memory temporary + // streams. Setting maxmemory:0 and performing a write forces PHP to + // back this temporary stream with a real file. + // + // This problem is mentioned at https://bugs.php.net/bug.php?id=64652&edit=3 + // but remains unresolved (despite that issue being closed). + $stream = fopen('php://temp/maxmemory:0', 'r+'); + + fwrite($stream, 'x'); + ftruncate($stream, 0); + + return $stream; + } +} diff --git a/tests/React/Tests/EventLoop/NextTickTestTrait.php b/tests/React/Tests/EventLoop/NextTickTestTrait.php new file mode 100644 index 00000000..8e3c43cc --- /dev/null +++ b/tests/React/Tests/EventLoop/NextTickTestTrait.php @@ -0,0 +1,99 @@ +assertSame($this->loop, $loop); + $called = true; + }; + + $this->loop->nextTick($callback); + + $this->assertFalse($called); + + $this->loop->tick(); + + $this->assertTrue($called); + } + + public function testNextTickFiresBeforeIO() + { + $stream = $this->createStream(); + + $this->loop->addWriteStream( + $stream, + function () { + echo 'stream' . PHP_EOL; + } + ); + + $this->loop->nextTick( + function () { + echo 'next-tick' . PHP_EOL; + } + ); + + $this->expectOutputString('next-tick' . PHP_EOL . 'stream' . PHP_EOL); + + $this->loop->tick(); + } + + public function testRecursiveNextTick() + { + $stream = $this->createStream(); + + $this->loop->addWriteStream( + $stream, + function () { + echo 'stream' . PHP_EOL; + } + ); + + $this->loop->nextTick( + function () { + $this->loop->nextTick( + function () { + echo 'next-tick' . PHP_EOL; + } + ); + } + ); + + $this->expectOutputString('next-tick' . PHP_EOL . 'stream' . PHP_EOL); + + $this->loop->tick(); + } + + public function testRunWaitsForNextTickEvents() + { + $stream = $this->createStream(); + + $this->loop->addWriteStream( + $stream, + function () use ($stream) { + $this->loop->removeStream($stream); + $this->loop->nextTick( + function () { + echo 'next-tick' . PHP_EOL; + } + ); + } + ); + + $this->expectOutputString('next-tick' . PHP_EOL); + + $this->loop->run(); + } +} diff --git a/tests/React/Tests/EventLoop/StreamSelectNextTickLoopTest.php b/tests/React/Tests/EventLoop/StreamSelectNextTickLoopTest.php new file mode 100644 index 00000000..aacb72d3 --- /dev/null +++ b/tests/React/Tests/EventLoop/StreamSelectNextTickLoopTest.php @@ -0,0 +1,32 @@ +loop->addTimer( + 0.05, + $this->expectCallableOnce() + ); + + $start = microtime(true); + + $this->loop->run(); + + $end = microtime(true); + $interval = $end - $start; + + $this->assertGreaterThan(0.04, $interval); + } +} diff --git a/tests/React/Tests/EventLoop/Timer/ExtEventTimerTest.php b/tests/React/Tests/EventLoop/Timer/ExtEventTimerTest.php new file mode 100644 index 00000000..a7a6d005 --- /dev/null +++ b/tests/React/Tests/EventLoop/Timer/ExtEventTimerTest.php @@ -0,0 +1,17 @@ +markTestSkipped('ext-event tests skipped because ext-event is not installed.'); + } + + return new ExtEventLoop(); + } +} diff --git a/tests/React/Tests/EventLoop/Timer/StreamSelectNextTickTimerTest.php b/tests/React/Tests/EventLoop/Timer/StreamSelectNextTickTimerTest.php new file mode 100644 index 00000000..9c2bbbef --- /dev/null +++ b/tests/React/Tests/EventLoop/Timer/StreamSelectNextTickTimerTest.php @@ -0,0 +1,13 @@ + Date: Wed, 13 Nov 2013 16:01:06 +1000 Subject: [PATCH 02/24] Trying installion of ext-event in travis configuration. --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 37ba1989..0340c258 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,6 +16,7 @@ before_script: cd libevent-0.0.5 && phpize && ./configure && make && sudo make install; echo "extension=libevent.so" >> `php --ini | grep "Loaded Configuration" | sed -e "s|.*:\s*||"`; fi" + - echo "yes" | pecl install event - composer self-update - composer install --dev --prefer-source From f0fb0af9ed129b6a362f6569241b290247ef1582 Mon Sep 17 00:00:00 2001 From: James Harris Date: Thu, 14 Nov 2013 05:55:21 +1000 Subject: [PATCH 03/24] Changed StreamSelectNextTickLoop to use existing Timers implementation. --- src/React/EventLoop/AbstractNextTickLoop.php | 48 ------- src/React/EventLoop/ExtEventLoop.php | 55 +++++++- .../EventLoop/StreamSelectNextTickLoop.php | 133 +++++++----------- src/React/EventLoop/Timer/Timers.php | 14 +- 4 files changed, 106 insertions(+), 144 deletions(-) diff --git a/src/React/EventLoop/AbstractNextTickLoop.php b/src/React/EventLoop/AbstractNextTickLoop.php index f3eedd95..60adaf2e 100644 --- a/src/React/EventLoop/AbstractNextTickLoop.php +++ b/src/React/EventLoop/AbstractNextTickLoop.php @@ -3,7 +3,6 @@ namespace React\EventLoop; use React\EventLoop\Timer\Timer; -use React\EventLoop\Timer\TimerInterface; use SplQueue; /** @@ -20,46 +19,6 @@ public function __construct() $this->explicitlyStopped = false; } - /** - * Enqueue a callback to be invoked once after the given interval. - * - * The execution order of timers scheduled to execute at the same time is - * not guaranteed. - * - * @param numeric $interval The number of seconds to wait before execution. - * @param callable $callback The callback to invoke. - * - * @return TimerInterface - */ - public function addTimer($interval, $callback) - { - $timer = new Timer($this, $interval, $callback, false); - - $this->scheduleTimer($timer); - - return $timer; - } - - /** - * Enqueue a callback to be invoked repeatedly after the given interval. - * - * The execution order of timers scheduled to execute at the same time is - * not guaranteed. - * - * @param numeric $interval The number of seconds to wait before execution. - * @param callable $callback The callback to invoke. - * - * @return TimerInterface - */ - public function addPeriodicTimer($interval, $callback) - { - $timer = new Timer($this, $interval, $callback, true); - - $this->scheduleTimer($timer); - - return $timer; - } - /** * Schedule a callback to be invoked on the next tick of the event loop. * @@ -157,13 +116,6 @@ protected function streamKey($stream) return (int) $stream; } - /** - * Schedule a timer for execution. - * - * @param TimerInterface $timer - */ - abstract protected function scheduleTimer(TimerInterface $timer); - /** * Flush any timer and IO events. * diff --git a/src/React/EventLoop/ExtEventLoop.php b/src/React/EventLoop/ExtEventLoop.php index 91917f88..bbb4c9e5 100644 --- a/src/React/EventLoop/ExtEventLoop.php +++ b/src/React/EventLoop/ExtEventLoop.php @@ -4,6 +4,7 @@ use Event; use EventBase; +use React\EventLoop\Timer\Timer; use React\EventLoop\Timer\TimerInterface; use SplObjectStorage; use stdClass; @@ -104,6 +105,46 @@ public function removeStream($stream) $this->keepAlive[] = $entry->callback; } + /** + * Enqueue a callback to be invoked once after the given interval. + * + * The execution order of timers scheduled to execute at the same time is + * not guaranteed. + * + * @param numeric $interval The number of seconds to wait before execution. + * @param callable $callback The callback to invoke. + * + * @return TimerInterface + */ + public function addTimer($interval, $callback) + { + $timer = new Timer($this, $interval, $callback, false); + + $this->scheduleTimer($timer); + + return $timer; + } + + /** + * Enqueue a callback to be invoked repeatedly after the given interval. + * + * The execution order of timers scheduled to execute at the same time is + * not guaranteed. + * + * @param numeric $interval The number of seconds to wait before execution. + * @param callable $callback The callback to invoke. + * + * @return TimerInterface + */ + public function addPeriodicTimer($interval, $callback) + { + $timer = new Timer($this, $interval, $callback, true); + + $this->scheduleTimer($timer); + + return $timer; + } + /** * Cancel a pending timer. * @@ -178,9 +219,9 @@ protected function onTimerTick(TimerInterface $timer) /** * Dispatch a stream event. * - * @param stdClass $entry The entry from $this->streamEvents - * @param stream $stream - * @param integer $flags Bitwise flags indicating event type (Event::READ/Event::WRITE) + * @param stdClass $entry The entry from $this->streamEvents + * @param stream $stream + * @param integer $flags Bitwise flags indicating event type (Event::READ/Event::WRITE) */ protected function onStreamEvent($entry, $stream, $flags) { @@ -231,8 +272,8 @@ protected function scheduleTimer(TimerInterface $timer) /** * Create a new ext-event Event object, or update the existing one. * - * @param stream $stream - * @param integer $flag Event::READ or Event::WRITE + * @param stream $stream + * @param integer $flag Event::READ or Event::WRITE * @param callable $listener */ protected function addStreamEvent($stream, $flag, $listener) @@ -268,8 +309,8 @@ protected function addStreamEvent($stream, $flag, $listener) * Update the ext-event Event object for this stream to stop listening to * the given event type, or remove it entirely if it's no longer needed. * - * @param stream $stream - * @param integer $flag Event::READ or Event::WRITE + * @param stream $stream + * @param integer $flag Event::READ or Event::WRITE */ protected function removeStreamEvent($stream, $flag) { diff --git a/src/React/EventLoop/StreamSelectNextTickLoop.php b/src/React/EventLoop/StreamSelectNextTickLoop.php index 77db4b79..f32aac8c 100644 --- a/src/React/EventLoop/StreamSelectNextTickLoop.php +++ b/src/React/EventLoop/StreamSelectNextTickLoop.php @@ -2,12 +2,9 @@ namespace React\EventLoop; -use Exception; use React\EventLoop\Timer\Timer; use React\EventLoop\Timer\TimerInterface; -use SplObjectStorage; -use SplPriorityQueue; -use SplQueue; +use React\EventLoop\Timer\Timers; /** * A stream_select() based event-loop with support for nextTick(). @@ -18,13 +15,11 @@ class StreamSelectNextTickLoop extends AbstractNextTickLoop private $readListeners = []; private $writeStreams = []; private $writeListeners = []; - private $timerQueue; - private $timerTimestamps; + private $timers; public function __construct() { - $this->timerQueue = new SplPriorityQueue; - $this->timerTimestamps = new SplObjectStorage; + $this->timers = new Timers; parent::__construct(); } @@ -102,6 +97,44 @@ public function removeStream($stream) $this->removeWriteStream($stream); } + /** + * Enqueue a callback to be invoked once after the given interval. + * + * The execution order of timers scheduled to execute at the same time is + * not guaranteed. + * + * @param numeric $interval The number of seconds to wait before execution. + * @param callable $callback The callback to invoke. + * + * @return TimerInterface + */ + public function addTimer($interval, $callback) + { + $timer = new Timer($this, $interval, $callback, false); + $this->timers->add($timer); + + return $timer; + } + + /** + * Enqueue a callback to be invoked repeatedly after the given interval. + * + * The execution order of timers scheduled to execute at the same time is + * not guaranteed. + * + * @param numeric $interval The number of seconds to wait before execution. + * @param callable $callback The callback to invoke. + * + * @return TimerInterface + */ + public function addPeriodicTimer($interval, $callback) + { + $timer = new Timer($this, $interval, $callback, true); + $this->timers->add($timer); + + return $timer; + } + /** * Cancel a pending timer. * @@ -109,7 +142,7 @@ public function removeStream($stream) */ public function cancelTimer(TimerInterface $timer) { - $this->timerTimestamps->detach($timer); + $this->timers->cancel($timer); } /** @@ -121,7 +154,7 @@ public function cancelTimer(TimerInterface $timer) */ public function isTimerActive(TimerInterface $timer) { - return $this->timerTimestamps->contains($timer); + return $this->timers->contains($timer); } /** @@ -131,7 +164,7 @@ public function isTimerActive(TimerInterface $timer) */ protected function flushEvents($blocking) { - $this->flushTimerQueue(); + $this->timers->tick(); $this->waitForStreamActivity($blocking); } @@ -142,7 +175,7 @@ protected function flushEvents($blocking) */ protected function isEmpty() { - return 0 === count($this->timerTimestamps) + return $this->timers->isEmpty() && 0 === count($this->readStreams) && 0 === count($this->writeStreams); } @@ -175,8 +208,8 @@ protected function toMicroSeconds($seconds) * Emulate a stream_select() implementation that does not break when passed * empty stream arrays. * - * @param array &$read An array of read streams to select upon. - * @param array &$write An array of write streams to select upon. + * @param array &$read An array of read streams to select upon. + * @param array &$write An array of write streams to select upon. * @param integer|null $timeout Activity timeout in microseconds, or null to wait forever. */ protected function streamSelect(array &$read, array &$write, $timeout) @@ -198,74 +231,6 @@ protected function streamSelect(array &$read, array &$write, $timeout) return 0; } - /** - * Schedule a timer for execution. - * - * @param TimerInterface $timer - */ - protected function scheduleTimer(TimerInterface $timer) - { - $executeAt = $this->now() + $this->toMicroSeconds( - $timer->getInterval() - ); - - $this->timerQueue->insert($timer, -$executeAt); - $this->timerTimestamps->attach($timer, $executeAt); - } - - /** - * Get the timer next schedule to tick, if any. - * - * @return TimerInterface|null - */ - protected function nextActiveTimer() - { - while ($this->timerQueue->count()) { - $timer = $this->timerQueue->top(); - - if ($this->isTimerActive($timer)) { - return $timer; - } else { - $this->timerQueue->extract(); - } - } - - return null; - } - - /** - * Push callbacks for timers that are ready into the next-tick queue. - */ - protected function flushTimerQueue() - { - $now = $this->now(); - - while ($timer = $this->nextActiveTimer()) { - - $executeAt = $this->timerTimestamps[$timer]; - - // The next time is in the future, exit the loop ... - if ($executeAt > $now) { - break; - } - - $this->timerQueue->extract(); - - call_user_func($timer->getCallback(), $timer); - - // Timer cancelled itself ... - if (!$this->isTimerActive($timer)) { - return; - // Reschedule periodic timers ... - } elseif ($timer->isPeriodic()) { - $this->scheduleTimer($timer); - // Cancel one-shot timers ... - } else { - $this->cancelTimer($timer); - } - } - } - protected function waitForStreamActivity($blocking) { // The $blocking flag takes precedence ... @@ -273,10 +238,10 @@ protected function waitForStreamActivity($blocking) $timeout = 0; // There is a pending timer, only block until it is due ... - } elseif ($timer = $this->nextActiveTimer()) { + } elseif ($scheduledAt = $this->timers->getFirst()) { $timeout = max( 0, - $this->timerTimestamps[$timer] - $this->now() + $scheduledAt - $this->timers->getTime() ); // The only possible event is stream activity, so wait forever ... diff --git a/src/React/EventLoop/Timer/Timers.php b/src/React/EventLoop/Timer/Timers.php index 520158c4..0c51bb03 100644 --- a/src/React/EventLoop/Timer/Timers.php +++ b/src/React/EventLoop/Timer/Timers.php @@ -56,13 +56,17 @@ public function cancel(TimerInterface $timer) public function getFirst() { - if ($this->scheduler->isEmpty()) { - return null; - } + while ($this->scheduler->count()) { + $timer = $this->scheduler->top(); + + if ($this->timers->contains($timer)) { + return $this->timers[$timer]; + } - $scheduledAt = $this->timers[$this->scheduler->top()]; + $this->scheduler->extract(); + } - return $scheduledAt; + return null; } public function isEmpty() From abbb05d81664627dc267f7dbe4673dec65e2f777 Mon Sep 17 00:00:00 2001 From: James Harris Date: Thu, 14 Nov 2013 05:56:48 +1000 Subject: [PATCH 04/24] Removed AbstractNextTickLoop::streamKey() --- src/React/EventLoop/AbstractNextTickLoop.php | 12 ------------ src/React/EventLoop/ExtEventLoop.php | 6 +++--- src/React/EventLoop/StreamSelectNextTickLoop.php | 10 +++++----- 3 files changed, 8 insertions(+), 20 deletions(-) diff --git a/src/React/EventLoop/AbstractNextTickLoop.php b/src/React/EventLoop/AbstractNextTickLoop.php index 60adaf2e..96992014 100644 --- a/src/React/EventLoop/AbstractNextTickLoop.php +++ b/src/React/EventLoop/AbstractNextTickLoop.php @@ -104,18 +104,6 @@ protected function tickLogic($blocking) ); } - /** - * Get a key that can be used to identify a stream resource. - * - * @param string $stream - * - * @return integer|string - */ - protected function streamKey($stream) - { - return (int) $stream; - } - /** * Flush any timer and IO events. * diff --git a/src/React/EventLoop/ExtEventLoop.php b/src/React/EventLoop/ExtEventLoop.php index bbb4c9e5..019c531a 100644 --- a/src/React/EventLoop/ExtEventLoop.php +++ b/src/React/EventLoop/ExtEventLoop.php @@ -90,7 +90,7 @@ public function removeWriteStream($stream) */ public function removeStream($stream) { - $key = $this->streamKey($stream); + $key = (int) $stream; if (!array_key_exists($key, $this->streamEvents)) { return; @@ -278,7 +278,7 @@ protected function scheduleTimer(TimerInterface $timer) */ protected function addStreamEvent($stream, $flag, $listener) { - $key = $this->streamKey($stream); + $key = (int) $stream; if (array_key_exists($key, $this->streamEvents)) { $entry = $this->streamEvents[$key]; @@ -314,7 +314,7 @@ protected function addStreamEvent($stream, $flag, $listener) */ protected function removeStreamEvent($stream, $flag) { - $key = $this->streamKey($stream); + $key = (int) $stream; if (!array_key_exists($key, $this->streamEvents)) { return; diff --git a/src/React/EventLoop/StreamSelectNextTickLoop.php b/src/React/EventLoop/StreamSelectNextTickLoop.php index f32aac8c..3c0632ce 100644 --- a/src/React/EventLoop/StreamSelectNextTickLoop.php +++ b/src/React/EventLoop/StreamSelectNextTickLoop.php @@ -32,7 +32,7 @@ public function __construct() */ public function addReadStream($stream, $listener) { - $key = $this->streamKey($stream); + $key = (int) $stream; if (!array_key_exists($key, $this->readStreams)) { $this->readStreams[$key] = $stream; @@ -48,7 +48,7 @@ public function addReadStream($stream, $listener) */ public function addWriteStream($stream, $listener) { - $key = $this->streamKey($stream); + $key = (int) $stream; if (!array_key_exists($key, $this->writeStreams)) { $this->writeStreams[$key] = $stream; @@ -63,7 +63,7 @@ public function addWriteStream($stream, $listener) */ public function removeReadStream($stream) { - $key = $this->streamKey($stream); + $key = (int) $stream; unset( $this->readStreams[$key], @@ -78,7 +78,7 @@ public function removeReadStream($stream) */ public function removeWriteStream($stream) { - $key = $this->streamKey($stream); + $key = (int) $stream; unset( $this->writeStreams[$key], @@ -265,7 +265,7 @@ protected function waitForStreamActivity($blocking) protected function flushStreamEvents(array $streams, array &$listeners) { foreach ($streams as $stream) { - $key = $this->streamKey($stream); + $key = (int) $stream; if (!array_key_exists($key, $listeners)) { continue; From 75b496a3cc2842c35ea5c08cd5632485e6913d5d Mon Sep 17 00:00:00 2001 From: James Harris Date: Thu, 14 Nov 2013 06:06:56 +1000 Subject: [PATCH 05/24] Abstracted the actual next-tick queue into it's own class. --- src/React/EventLoop/AbstractNextTickLoop.php | 26 +++------ .../EventLoop/NextTick/NextTickQueue.php | 57 +++++++++++++++++++ 2 files changed, 64 insertions(+), 19 deletions(-) create mode 100644 src/React/EventLoop/NextTick/NextTickQueue.php diff --git a/src/React/EventLoop/AbstractNextTickLoop.php b/src/React/EventLoop/AbstractNextTickLoop.php index 96992014..5353957c 100644 --- a/src/React/EventLoop/AbstractNextTickLoop.php +++ b/src/React/EventLoop/AbstractNextTickLoop.php @@ -2,6 +2,7 @@ namespace React\EventLoop; +use React\EventLoop\NextTick\NextTickQueue; use React\EventLoop\Timer\Timer; use SplQueue; @@ -15,7 +16,7 @@ abstract class AbstractNextTickLoop implements NextTickLoopInterface public function __construct() { - $this->nextTickQueue = new SplQueue; + $this->nextTickQueue = new NextTickQueue($this); $this->explicitlyStopped = false; } @@ -25,11 +26,11 @@ public function __construct() * Callbacks are guaranteed to be executed in the order they are enqueued, * before any timer or stream events. * - * @param callable $listner The callback to invoke. + * @param callable $listener The callback to invoke. */ public function nextTick(callable $listener) { - $this->nextTickQueue->enqueue($listener); + $this->nextTickQueue->add($listener); } /** @@ -60,19 +61,6 @@ public function stop() $this->explicitlyStopped = true; } - /** - * Invoke all callbacks in the next-tick queue. - */ - protected function flushNextTickQueue() - { - while ($this->nextTickQueue->count()) { - call_user_func( - $this->nextTickQueue->dequeue(), - $this - ); - } - } - /** * Check if there is any pending work to do. * @@ -85,7 +73,7 @@ protected function isRunning() return false; // The next tick queue has items on it ... - } elseif ($this->nextTickQueue->count() > 0) { + } elseif (!$this->nextTickQueue->isEmpty()) { return true; } @@ -97,10 +85,10 @@ protected function isRunning() */ protected function tickLogic($blocking) { - $this->flushNextTickQueue(); + $this->nextTickQueue->tick(); $this->flushEvents( - $blocking && 0 === $this->nextTickQueue->count() + $blocking && $this->nextTickQueue->isEmpty() ); } diff --git a/src/React/EventLoop/NextTick/NextTickQueue.php b/src/React/EventLoop/NextTick/NextTickQueue.php new file mode 100644 index 00000000..6a27e569 --- /dev/null +++ b/src/React/EventLoop/NextTick/NextTickQueue.php @@ -0,0 +1,57 @@ +eventLoop = $eventLoop; + $this->queue = new SplQueue; + } + + /** + * Add a callback to be invoked on the next tick of the event loop. + * + * Callbacks are guaranteed to be executed in the order they are enqueued, + * before any timer or stream events. + * + * @param callable $listener The callback to invoke. + */ + public function add(callable $listener) + { + $this->queue->enqueue($listener); + } + + /** + * Flush the callback queue. + */ + public function tick() + { + while ($this->queue->count()) { + call_user_func( + $this->queue->dequeue(), + $this->eventLoop + ); + } + } + + /** + * Check if the next tick queue is empty. + * + * @return boolean + */ + public function isEmpty() + { + return 0 === $this->queue->count(); + } +} From 35e4cd0dee73ebc62ec596f3ded861932d0577dc Mon Sep 17 00:00:00 2001 From: James Harris Date: Thu, 14 Nov 2013 06:32:47 +1000 Subject: [PATCH 06/24] Reduced method dispatching somewhat. --- src/React/EventLoop/AbstractNextTickLoop.php | 22 ++-- src/React/EventLoop/ExtEventLoop.php | 68 +++++------ .../EventLoop/StreamSelectNextTickLoop.php | 106 +++++++----------- 3 files changed, 74 insertions(+), 122 deletions(-) diff --git a/src/React/EventLoop/AbstractNextTickLoop.php b/src/React/EventLoop/AbstractNextTickLoop.php index 5353957c..40881841 100644 --- a/src/React/EventLoop/AbstractNextTickLoop.php +++ b/src/React/EventLoop/AbstractNextTickLoop.php @@ -38,6 +38,8 @@ public function nextTick(callable $listener) */ public function tick() { + $this->nextTickQueue->tick(); + $this->tickLogic(false); } @@ -49,7 +51,11 @@ public function run() $this->explicitlyStopped = false; while ($this->isRunning()) { - $this->tickLogic(true); + $this->nextTickQueue->tick(); + + $this->tickLogic( + $this->nextTickQueue->isEmpty() + ); } } @@ -80,24 +86,12 @@ protected function isRunning() return !$this->isEmpty(); } - /** - * Perform the low-level tick logic. - */ - protected function tickLogic($blocking) - { - $this->nextTickQueue->tick(); - - $this->flushEvents( - $blocking && $this->nextTickQueue->isEmpty() - ); - } - /** * Flush any timer and IO events. * * @param boolean $blocking True if loop should block waiting for next event. */ - abstract protected function flushEvents($blocking); + abstract protected function tickLogic($blocking); /** * Check if the loop has any pending timers or streams. diff --git a/src/React/EventLoop/ExtEventLoop.php b/src/React/EventLoop/ExtEventLoop.php index 019c531a..bb433f9c 100644 --- a/src/React/EventLoop/ExtEventLoop.php +++ b/src/React/EventLoop/ExtEventLoop.php @@ -154,8 +154,11 @@ public function cancelTimer(TimerInterface $timer) { if ($this->isTimerActive($timer)) { $entry = $this->timerEvents[$timer]; + $this->timerEvents->detach($timer); + $entry->event->free(); + $this->keepAlive[] = $entry->callback; } } @@ -177,7 +180,7 @@ public function isTimerActive(TimerInterface $timer) * * @param boolean $blocking True if loop should block waiting for next event. */ - protected function flushEvents($blocking) + protected function tickLogic($blocking) { $flags = EventBase::LOOP_ONCE; @@ -201,44 +204,6 @@ protected function isEmpty() && 0 === count($this->streamEvents); } - /** - * Dispatch a timer event. - * - * @param TimerInterface $timer - */ - protected function onTimerTick(TimerInterface $timer) - { - call_user_func($timer->getCallback(), $timer); - - // Clean-up one shot timers ... - if ($this->isTimerActive($timer) && !$timer->isPeriodic()) { - $this->cancelTimer($timer); - } - } - - /** - * Dispatch a stream event. - * - * @param stdClass $entry The entry from $this->streamEvents - * @param stream $stream - * @param integer $flags Bitwise flags indicating event type (Event::READ/Event::WRITE) - */ - protected function onStreamEvent($entry, $stream, $flags) - { - foreach ([Event::READ, Event::WRITE] as $flag) { - if ( - $flag === ($flags & $flag) && - is_callable($entry->listeners[$flag]) - ) { - call_user_func( - $entry->listeners[$flag], - $stream, - $this - ); - } - } - } - /** * Schedule a timer for execution. * @@ -254,7 +219,12 @@ protected function scheduleTimer(TimerInterface $timer) $entry = new stdClass; $entry->callback = function () use ($timer) { - $this->onTimerTick($timer); + call_user_func($timer->getCallback(), $timer); + + // Clean-up one shot timers ... + if ($this->isTimerActive($timer) && !$timer->isPeriodic()) { + $this->cancelTimer($timer); + } }; $entry->event = new Event( @@ -284,9 +254,6 @@ protected function addStreamEvent($stream, $flag, $listener) $entry = $this->streamEvents[$key]; } else { $entry = new stdClass; - $entry->callback = function ($stream, $flags, $loop) use ($entry) { - $this->onStreamEvent($entry, $stream, $flags); - }; $entry->event = null; $entry->flags = 0; $entry->listeners = [ @@ -294,6 +261,21 @@ protected function addStreamEvent($stream, $flag, $listener) Event::WRITE => null, ]; + $entry->callback = function ($stream, $flags, $loop) use ($entry) { + foreach ([Event::READ, Event::WRITE] as $flag) { + if ( + $flag === ($flags & $flag) && + is_callable($entry->listeners[$flag]) + ) { + call_user_func( + $entry->listeners[$flag], + $stream, + $this + ); + } + } + }; + $this->streamEvents[$key] = $entry; } diff --git a/src/React/EventLoop/StreamSelectNextTickLoop.php b/src/React/EventLoop/StreamSelectNextTickLoop.php index 3c0632ce..4433d4a9 100644 --- a/src/React/EventLoop/StreamSelectNextTickLoop.php +++ b/src/React/EventLoop/StreamSelectNextTickLoop.php @@ -162,9 +162,10 @@ public function isTimerActive(TimerInterface $timer) * * @param boolean $blocking True if loop should block waiting for next event. */ - protected function flushEvents($blocking) + protected function tickLogic($blocking) { $this->timers->tick(); + $this->waitForStreamActivity($blocking); } @@ -180,28 +181,47 @@ protected function isEmpty() && 0 === count($this->writeStreams); } - /** - * Get the current time in microseconds. - * - * @return integer - */ - protected function now() + protected function waitForStreamActivity($blocking) { - return $this->toMicroSeconds( - microtime(true) - ); - } + // The $blocking flag takes precedence ... + if (!$blocking) { + $timeout = 0; - /** - * Convert the given time to microseconds. - * - * @param integer|float $seconds - * - * @return integer - */ - protected function toMicroSeconds($seconds) - { - return intval($seconds * 1000000); + // There is a pending timer, only block until it is due ... + } elseif ($scheduledAt = $this->timers->getFirst()) { + $timeout = max(0, $scheduledAt - $this->timers->getTime()); + + // The only possible event is stream activity, so wait forever ... + } elseif ($this->readStreams || $this->writeStreams) { + $timeout = null; + + // There's nothing left to do ... + } else { + return; + } + + $read = $this->readStreams; + $write = $this->writeStreams; + + $this->streamSelect($read, $write, $timeout); + + // Invoke callbacks for read-ready streams ... + foreach ($read as $stream) { + $key = (int) $stream; + + if (array_key_exists($key, $this->readListeners)) { + call_user_func($this->readListeners[$key], $stream, $this); + } + } + + // Invoke callbacks for write-ready streams ... + foreach ($write as $stream) { + $key = (int) $stream; + + if (array_key_exists($key, $this->writeListeners)) { + call_user_func($this->writeListeners[$key], $stream, $this); + } + } } /** @@ -230,48 +250,4 @@ protected function streamSelect(array &$read, array &$write, $timeout) return 0; } - - protected function waitForStreamActivity($blocking) - { - // The $blocking flag takes precedence ... - if (!$blocking) { - $timeout = 0; - - // There is a pending timer, only block until it is due ... - } elseif ($scheduledAt = $this->timers->getFirst()) { - $timeout = max( - 0, - $scheduledAt - $this->timers->getTime() - ); - - // The only possible event is stream activity, so wait forever ... - } elseif ($this->readStreams || $this->writeStreams) { - $timeout = null; - - // THere's nothing left to do ... - } else { - return; - } - - $read = $this->readStreams; - $write = $this->writeStreams; - - $this->streamSelect($read, $write, $timeout); - - $this->flushStreamEvents($read, $this->readListeners); - $this->flushStreamEvents($write, $this->writeListeners); - } - - protected function flushStreamEvents(array $streams, array &$listeners) - { - foreach ($streams as $stream) { - $key = (int) $stream; - - if (!array_key_exists($key, $listeners)) { - continue; - } - - call_user_func($listeners[$key], $stream, $this); - } - } } From 7fac0d37a1a6b72f85dff707eb66ca339078b71a Mon Sep 17 00:00:00 2001 From: James Harris Date: Thu, 14 Nov 2013 07:36:49 +1000 Subject: [PATCH 07/24] Favour isset() over array_key_exists(). --- src/React/EventLoop/ExtEventLoop.php | 6 +++--- src/React/EventLoop/StreamSelectNextTickLoop.php | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/React/EventLoop/ExtEventLoop.php b/src/React/EventLoop/ExtEventLoop.php index bb433f9c..5cea5a27 100644 --- a/src/React/EventLoop/ExtEventLoop.php +++ b/src/React/EventLoop/ExtEventLoop.php @@ -92,7 +92,7 @@ public function removeStream($stream) { $key = (int) $stream; - if (!array_key_exists($key, $this->streamEvents)) { + if (!isset($this->streamEvents[$key])) { return; } @@ -250,7 +250,7 @@ protected function addStreamEvent($stream, $flag, $listener) { $key = (int) $stream; - if (array_key_exists($key, $this->streamEvents)) { + if (isset($this->streamEvents[$key])) { $entry = $this->streamEvents[$key]; } else { $entry = new stdClass; @@ -298,7 +298,7 @@ protected function removeStreamEvent($stream, $flag) { $key = (int) $stream; - if (!array_key_exists($key, $this->streamEvents)) { + if (!isset($this->streamEvents[$key])) { return; } diff --git a/src/React/EventLoop/StreamSelectNextTickLoop.php b/src/React/EventLoop/StreamSelectNextTickLoop.php index 4433d4a9..5c33de2b 100644 --- a/src/React/EventLoop/StreamSelectNextTickLoop.php +++ b/src/React/EventLoop/StreamSelectNextTickLoop.php @@ -34,7 +34,7 @@ public function addReadStream($stream, $listener) { $key = (int) $stream; - if (!array_key_exists($key, $this->readStreams)) { + if (!isset($this->readStreams[$key])) { $this->readStreams[$key] = $stream; $this->readListeners[$key] = $listener; } @@ -50,7 +50,7 @@ public function addWriteStream($stream, $listener) { $key = (int) $stream; - if (!array_key_exists($key, $this->writeStreams)) { + if (!isset($this->writeStreams[$key])) { $this->writeStreams[$key] = $stream; $this->writeListeners[$key] = $listener; } @@ -209,7 +209,7 @@ protected function waitForStreamActivity($blocking) foreach ($read as $stream) { $key = (int) $stream; - if (array_key_exists($key, $this->readListeners)) { + if (isset($this->readListeners[$key])) { call_user_func($this->readListeners[$key], $stream, $this); } } @@ -218,7 +218,7 @@ protected function waitForStreamActivity($blocking) foreach ($write as $stream) { $key = (int) $stream; - if (array_key_exists($key, $this->writeListeners)) { + if (isset($this->writeListeners[$key])) { call_user_func($this->writeListeners[$key], $stream, $this); } } From 514d05b7c671cb1b84f1a8f65f92d01095189694 Mon Sep 17 00:00:00 2001 From: James Harris Date: Fri, 15 Nov 2013 09:32:05 +1000 Subject: [PATCH 08/24] Removed NextTickLoopInterface, AbstractNextTickLoop and renamed StreamSelectNextTickLoop to StreamSelectLoop. --- src/React/EventLoop/AbstractNextTickLoop.php | 102 ------- src/React/EventLoop/ExtEventLoop.php | 70 +++-- src/React/EventLoop/LibEvLoop.php | 13 + src/React/EventLoop/LibEventLoop.php | 13 + src/React/EventLoop/LoopInterface.php | 10 + src/React/EventLoop/NextTickLoopInterface.php | 21 -- src/React/EventLoop/StreamSelectLoop.php | 269 ++++++++++++------ .../EventLoop/StreamSelectNextTickLoop.php | 253 ---------------- .../Tests/EventLoop/AbstractLoopTest.php | 87 ++++++ .../Tests/EventLoop/ExtEventLoopTest.php | 2 - .../Tests/EventLoop/NextTickTestTrait.php | 99 ------- .../Tests/EventLoop/StreamSelectLoopTest.php | 18 +- .../StreamSelectNextTickLoopTest.php | 32 --- .../EventLoop/Timer/AbstractTimerTest.php | 13 + .../Timer/StreamSelectNextTickTimerTest.php | 13 - 15 files changed, 385 insertions(+), 630 deletions(-) delete mode 100644 src/React/EventLoop/AbstractNextTickLoop.php delete mode 100644 src/React/EventLoop/NextTickLoopInterface.php delete mode 100644 src/React/EventLoop/StreamSelectNextTickLoop.php delete mode 100644 tests/React/Tests/EventLoop/NextTickTestTrait.php delete mode 100644 tests/React/Tests/EventLoop/StreamSelectNextTickLoopTest.php delete mode 100644 tests/React/Tests/EventLoop/Timer/StreamSelectNextTickTimerTest.php diff --git a/src/React/EventLoop/AbstractNextTickLoop.php b/src/React/EventLoop/AbstractNextTickLoop.php deleted file mode 100644 index 40881841..00000000 --- a/src/React/EventLoop/AbstractNextTickLoop.php +++ /dev/null @@ -1,102 +0,0 @@ -nextTickQueue = new NextTickQueue($this); - $this->explicitlyStopped = false; - } - - /** - * Schedule a callback to be invoked on the next tick of the event loop. - * - * Callbacks are guaranteed to be executed in the order they are enqueued, - * before any timer or stream events. - * - * @param callable $listener The callback to invoke. - */ - public function nextTick(callable $listener) - { - $this->nextTickQueue->add($listener); - } - - /** - * Perform a single iteration of the event loop. - */ - public function tick() - { - $this->nextTickQueue->tick(); - - $this->tickLogic(false); - } - - /** - * Run the event loop until there are no more tasks to perform. - */ - public function run() - { - $this->explicitlyStopped = false; - - while ($this->isRunning()) { - $this->nextTickQueue->tick(); - - $this->tickLogic( - $this->nextTickQueue->isEmpty() - ); - } - } - - /** - * Instruct a running event loop to stop. - */ - public function stop() - { - $this->explicitlyStopped = true; - } - - /** - * Check if there is any pending work to do. - * - * @return boolean - */ - protected function isRunning() - { - // The loop has been explicitly stopped and should exit ... - if ($this->explicitlyStopped) { - return false; - - // The next tick queue has items on it ... - } elseif (!$this->nextTickQueue->isEmpty()) { - return true; - } - - return !$this->isEmpty(); - } - - /** - * Flush any timer and IO events. - * - * @param boolean $blocking True if loop should block waiting for next event. - */ - abstract protected function tickLogic($blocking); - - /** - * Check if the loop has any pending timers or streams. - * - * @return boolean - */ - abstract protected function isEmpty(); -} diff --git a/src/React/EventLoop/ExtEventLoop.php b/src/React/EventLoop/ExtEventLoop.php index 5cea5a27..0cd98261 100644 --- a/src/React/EventLoop/ExtEventLoop.php +++ b/src/React/EventLoop/ExtEventLoop.php @@ -4,19 +4,22 @@ use Event; use EventBase; +use React\EventLoop\NextTick\NextTickQueue; use React\EventLoop\Timer\Timer; use React\EventLoop\Timer\TimerInterface; use SplObjectStorage; use stdClass; /** - * An ext-event based event-loop with support for nextTick(). + * An ext-event based event-loop. */ -class ExtEventLoop extends AbstractNextTickLoop +class ExtEventLoop implements LoopInterface { private $eventBase; + private $nextTickQueue; private $timerEvents; private $streamEvents; + private $running; private $keepAlive; /** @@ -29,6 +32,7 @@ public function __construct(EventBase $eventBase = null) } $this->eventBase = $eventBase; + $this->nextTickQueue = new NextTickQueue($this); $this->timerEvents = new SplObjectStorage; $this->streamEvents = []; @@ -37,8 +41,6 @@ public function __construct(EventBase $eventBase = null) // to prevent the PHP fatal error caused by ext-event: // "Cannot destroy active lambda function" $this->keepAlive = []; - - parent::__construct(); } /** @@ -176,32 +178,64 @@ public function isTimerActive(TimerInterface $timer) } /** - * Flush any timer and IO events. + * Schedule a callback to be invoked on the next tick of the event loop. * - * @param boolean $blocking True if loop should block waiting for next event. + * Callbacks are guaranteed to be executed in the order they are enqueued, + * before any timer or stream events. + * + * @param callable $listener The callback to invoke. */ - protected function tickLogic($blocking) + public function nextTick(callable $listener) { - $flags = EventBase::LOOP_ONCE; + $this->nextTickQueue->add($listener); + } - if (!$blocking) { - $flags |= EventBase::LOOP_NONBLOCK; - } + /** + * Perform a single iteration of the event loop. + * + * @param boolean $blocking True if loop should block waiting for next event. + */ + public function tick() + { + $this->nextTickQueue->tick(); - $this->eventBase->loop($flags); + $this->eventBase->loop(EventBase::LOOP_ONCE | EventBase::LOOP_NONBLOCK); $this->keepAlive = []; } /** - * Check if the loop has any pending timers or streams. - * - * @return boolean + * Run the event loop until there are no more tasks to perform. + */ + public function run() + { + $this->running = true; + + while ($this->running) { + + if ( + !$this->streamEvents + && !$this->timerEvents->count() + && $this->nextTickQueue->isEmpty() + ) { + break; + } + + $this->nextTickQueue->tick(); + + $this->eventBase->loop(EventBase::LOOP_ONCE); + + $this->keepAlive = []; + + } + } + + /** + * Instruct a running event loop to stop. */ - protected function isEmpty() + public function stop() { - return 0 === count($this->timerEvents) - && 0 === count($this->streamEvents); + $this->running = false; } /** diff --git a/src/React/EventLoop/LibEvLoop.php b/src/React/EventLoop/LibEvLoop.php index 33fa124b..9ff997aa 100644 --- a/src/React/EventLoop/LibEvLoop.php +++ b/src/React/EventLoop/LibEvLoop.php @@ -55,6 +55,19 @@ public function removeStream($stream) $this->removeWriteStream($stream); } + /** + * Schedule a callback to be invoked on the next tick of the event loop. + * + * Callbacks are guaranteed to be executed in the order they are enqueued, + * before any timer or stream events. + * + * @param callable $listner The callback to invoke. + */ + public function nextTick(callable $listener) + { + throw new \Exception('Not yet implemented.'); + } + private function addStream($stream, $listener, $flags) { $listener = $this->wrapStreamListener($stream, $listener, $flags); diff --git a/src/React/EventLoop/LibEventLoop.php b/src/React/EventLoop/LibEventLoop.php index 7fb27d6d..363a1193 100644 --- a/src/React/EventLoop/LibEventLoop.php +++ b/src/React/EventLoop/LibEventLoop.php @@ -101,6 +101,19 @@ public function removeWriteStream($stream) $this->removeStreamEvent($stream, EV_WRITE, 'write'); } + /** + * Schedule a callback to be invoked on the next tick of the event loop. + * + * Callbacks are guaranteed to be executed in the order they are enqueued, + * before any timer or stream events. + * + * @param callable $listner The callback to invoke. + */ + public function nextTick(callable $listener) + { + throw new \Exception('Not yet implemented.'); + } + protected function removeStreamEvent($stream, $eventClass, $type) { $id = (int) $stream; diff --git a/src/React/EventLoop/LoopInterface.php b/src/React/EventLoop/LoopInterface.php index ce1dcaa4..4fcb7979 100644 --- a/src/React/EventLoop/LoopInterface.php +++ b/src/React/EventLoop/LoopInterface.php @@ -85,6 +85,16 @@ public function cancelTimer(TimerInterface $timer); */ public function isTimerActive(TimerInterface $timer); + /** + * Schedule a callback to be invoked on the next tick of the event loop. + * + * Callbacks are guaranteed to be executed in the order they are enqueued, + * before any timer or stream events. + * + * @param callable $listner The callback to invoke. + */ + public function nextTick(callable $listener); + /** * Perform a single iteration of the event loop. */ diff --git a/src/React/EventLoop/NextTickLoopInterface.php b/src/React/EventLoop/NextTickLoopInterface.php deleted file mode 100644 index 8fb65c51..00000000 --- a/src/React/EventLoop/NextTickLoopInterface.php +++ /dev/null @@ -1,21 +0,0 @@ -timers = new Timers(); + $this->nextTickQueue = new NextTickQueue($this); + $this->timers = new Timers; } + /** + * Register a listener to be notified when a stream is ready to read. + * + * @param stream $stream The PHP stream resource to check. + * @param callable $listener Invoked when the stream is ready. + */ public function addReadStream($stream, $listener) { - $id = (int) $stream; + $key = (int) $stream; - if (!isset($this->readStreams[$id])) { - $this->readStreams[$id] = $stream; - $this->readListeners[$id] = $listener; + if (!isset($this->readStreams[$key])) { + $this->readStreams[$key] = $stream; + $this->readListeners[$key] = $listener; } } + /** + * Register a listener to be notified when a stream is ready to write. + * + * @param stream $stream The PHP stream resource to check. + * @param callable $listener Invoked when the stream is ready. + */ public function addWriteStream($stream, $listener) { - $id = (int) $stream; + $key = (int) $stream; - if (!isset($this->writeStreams[$id])) { - $this->writeStreams[$id] = $stream; - $this->writeListeners[$id] = $listener; + if (!isset($this->writeStreams[$key])) { + $this->writeStreams[$key] = $stream; + $this->writeListeners[$key] = $listener; } } + /** + * Remove the read event listener for the given stream. + * + * @param stream $stream The PHP stream resource. + */ public function removeReadStream($stream) { - $id = (int) $stream; + $key = (int) $stream; unset( - $this->readStreams[$id], - $this->readListeners[$id] + $this->readStreams[$key], + $this->readListeners[$key] ); } + /** + * Remove the write event listener for the given stream. + * + * @param stream $stream The PHP stream resource. + */ public function removeWriteStream($stream) { - $id = (int) $stream; + $key = (int) $stream; unset( - $this->writeStreams[$id], - $this->writeListeners[$id] + $this->writeStreams[$key], + $this->writeListeners[$key] ); } + /** + * Remove all listeners for the given stream. + * + * @param stream $stream The PHP stream resource. + */ public function removeStream($stream) { $this->removeReadStream($stream); $this->removeWriteStream($stream); } + /** + * Enqueue a callback to be invoked once after the given interval. + * + * The execution order of timers scheduled to execute at the same time is + * not guaranteed. + * + * @param numeric $interval The number of seconds to wait before execution. + * @param callable $callback The callback to invoke. + * + * @return TimerInterface + */ public function addTimer($interval, $callback) { $timer = new Timer($this, $interval, $callback, false); + $this->timers->add($timer); return $timer; } + /** + * Enqueue a callback to be invoked repeatedly after the given interval. + * + * The execution order of timers scheduled to execute at the same time is + * not guaranteed. + * + * @param numeric $interval The number of seconds to wait before execution. + * @param callable $callback The callback to invoke. + * + * @return TimerInterface + */ public function addPeriodicTimer($interval, $callback) { $timer = new Timer($this, $interval, $callback, true); + $this->timers->add($timer); return $timer; } + /** + * Cancel a pending timer. + * + * @param TimerInterface $timer The timer to cancel. + */ public function cancelTimer(TimerInterface $timer) { $this->timers->cancel($timer); } + /** + * Check if a given timer is active. + * + * @param TimerInterface $timer The timer to check. + * + * @return boolean True if the timer is still enqueued for execution. + */ public function isTimerActive(TimerInterface $timer) { return $this->timers->contains($timer); } - protected function getNextEventTimeInMicroSeconds() + /** + * Schedule a callback to be invoked on the next tick of the event loop. + * + * Callbacks are guaranteed to be executed in the order they are enqueued, + * before any timer or stream events. + * + * @param callable $listener The callback to invoke. + */ + public function nextTick(callable $listener) { - $nextEvent = $this->timers->getFirst(); + $this->nextTickQueue->add($listener); + } - if (null === $nextEvent) { - return self::QUANTUM_INTERVAL; - } + /** + * Perform a single iteration of the event loop. + */ + public function tick() + { + $this->nextTickQueue->tick(); - $currentTime = microtime(true); - if ($nextEvent > $currentTime) { - return ($nextEvent - $currentTime) * 1000000; - } + $this->timers->tick(); - return 0; + $this->waitForStreamActivity(0); } - protected function sleepOnPendingTimers() + /** + * Run the event loop until there are no more tasks to perform. + */ + public function run() { - if ($this->timers->isEmpty()) { - $this->running = false; - } else { - // We use usleep() instead of stream_select() to emulate timeouts - // since the latter fails when there are no streams registered for - // read / write events. Blame PHP for us needing this hack. - usleep($this->getNextEventTimeInMicroSeconds()); - } - } + $this->running = true; - protected function runStreamSelect($block) - { - $read = $this->readStreams ?: null; - $write = $this->writeStreams ?: null; - $except = null; + while ($this->running) { - if (!$read && !$write) { - if ($block) { - $this->sleepOnPendingTimers(); - } + $this->nextTickQueue->tick(); - return; - } + $this->timers->tick(); - $timeout = $block ? $this->getNextEventTimeInMicroSeconds() : 0; + // There is a pending timer, only block until it is due ... + if ($scheduledAt = $this->timers->getFirst()) { + $timeout = max(0, $scheduledAt - $this->timers->getTime()); - if (stream_select($read, $write, $except, 0, $timeout) > 0) { - if ($read) { - foreach ($read as $stream) { - if (!isset($this->readListeners[(int) $stream])) { - continue; - } + // The only possible event is stream activity, so wait forever ... + } elseif ($this->readStreams || $this->writeStreams) { + $timeout = null; - $listener = $this->readListeners[(int) $stream]; - call_user_func($listener, $stream, $this); - } + // There's nothing left to do ... + } else { + break; } - if ($write) { - foreach ($write as $stream) { - if (!isset($this->writeListeners[(int) $stream])) { - continue; - } - - $listener = $this->writeListeners[(int) $stream]; - call_user_func($listener, $stream, $this); - } - } + $this->waitForStreamActivity($timeout); } } - protected function loop($block = true) + /** + * Instruct a running event loop to stop. + */ + public function stop() { - $this->timers->tick(); - $this->runStreamSelect($block); - - return $this->running; + $this->running = false; } - public function tick() + /** + * Wait/check for stream activity, or until the next timer is due. + */ + protected function waitForStreamActivity($timeout) { - return $this->loop(false); - } + $read = $this->readStreams; + $write = $this->writeStreams; - public function run() - { - $this->running = true; - while ($this->loop()); + $this->streamSelect($read, $write, $timeout); + + // Invoke callbacks for read-ready streams ... + foreach ($read as $stream) { + $key = (int) $stream; + + if (isset($this->readListeners[$key])) { + call_user_func($this->readListeners[$key], $stream, $this); + } + } + + // Invoke callbacks for write-ready streams ... + foreach ($write as $stream) { + $key = (int) $stream; + + if (isset($this->writeListeners[$key])) { + call_user_func($this->writeListeners[$key], $stream, $this); + } + } + + return true; } - public function stop() + /** + * Emulate a stream_select() implementation that does not break when passed + * empty stream arrays. + * + * @param array &$read An array of read streams to select upon. + * @param array &$write An array of write streams to select upon. + * @param integer|null $timeout Activity timeout in microseconds, or null to wait forever. + */ + protected function streamSelect(array &$read, array &$write, $timeout) { - $this->running = false; + if ($read || $write) { + $except = null; + + return stream_select( + $read, + $write, + $except, + $timeout === null ? null : 0, + $timeout + ); + } + + usleep($timeout); + + return 0; } } diff --git a/src/React/EventLoop/StreamSelectNextTickLoop.php b/src/React/EventLoop/StreamSelectNextTickLoop.php deleted file mode 100644 index 5c33de2b..00000000 --- a/src/React/EventLoop/StreamSelectNextTickLoop.php +++ /dev/null @@ -1,253 +0,0 @@ -timers = new Timers; - - parent::__construct(); - } - - /** - * Register a listener to be notified when a stream is ready to read. - * - * @param stream $stream The PHP stream resource to check. - * @param callable $listener Invoked when the stream is ready. - */ - public function addReadStream($stream, $listener) - { - $key = (int) $stream; - - if (!isset($this->readStreams[$key])) { - $this->readStreams[$key] = $stream; - $this->readListeners[$key] = $listener; - } - } - - /** - * Register a listener to be notified when a stream is ready to write. - * - * @param stream $stream The PHP stream resource to check. - * @param callable $listener Invoked when the stream is ready. - */ - public function addWriteStream($stream, $listener) - { - $key = (int) $stream; - - if (!isset($this->writeStreams[$key])) { - $this->writeStreams[$key] = $stream; - $this->writeListeners[$key] = $listener; - } - } - - /** - * Remove the read event listener for the given stream. - * - * @param stream $stream The PHP stream resource. - */ - public function removeReadStream($stream) - { - $key = (int) $stream; - - unset( - $this->readStreams[$key], - $this->readListeners[$key] - ); - } - - /** - * Remove the write event listener for the given stream. - * - * @param stream $stream The PHP stream resource. - */ - public function removeWriteStream($stream) - { - $key = (int) $stream; - - unset( - $this->writeStreams[$key], - $this->writeListeners[$key] - ); - } - - /** - * Remove all listeners for the given stream. - * - * @param stream $stream The PHP stream resource. - */ - public function removeStream($stream) - { - $this->removeReadStream($stream); - $this->removeWriteStream($stream); - } - - /** - * Enqueue a callback to be invoked once after the given interval. - * - * The execution order of timers scheduled to execute at the same time is - * not guaranteed. - * - * @param numeric $interval The number of seconds to wait before execution. - * @param callable $callback The callback to invoke. - * - * @return TimerInterface - */ - public function addTimer($interval, $callback) - { - $timer = new Timer($this, $interval, $callback, false); - $this->timers->add($timer); - - return $timer; - } - - /** - * Enqueue a callback to be invoked repeatedly after the given interval. - * - * The execution order of timers scheduled to execute at the same time is - * not guaranteed. - * - * @param numeric $interval The number of seconds to wait before execution. - * @param callable $callback The callback to invoke. - * - * @return TimerInterface - */ - public function addPeriodicTimer($interval, $callback) - { - $timer = new Timer($this, $interval, $callback, true); - $this->timers->add($timer); - - return $timer; - } - - /** - * Cancel a pending timer. - * - * @param TimerInterface $timer The timer to cancel. - */ - public function cancelTimer(TimerInterface $timer) - { - $this->timers->cancel($timer); - } - - /** - * Check if a given timer is active. - * - * @param TimerInterface $timer The timer to check. - * - * @return boolean True if the timer is still enqueued for execution. - */ - public function isTimerActive(TimerInterface $timer) - { - return $this->timers->contains($timer); - } - - /** - * Flush any timer and IO events. - * - * @param boolean $blocking True if loop should block waiting for next event. - */ - protected function tickLogic($blocking) - { - $this->timers->tick(); - - $this->waitForStreamActivity($blocking); - } - - /** - * Check if the loop has any pending timers or streams. - * - * @return boolean - */ - protected function isEmpty() - { - return $this->timers->isEmpty() - && 0 === count($this->readStreams) - && 0 === count($this->writeStreams); - } - - protected function waitForStreamActivity($blocking) - { - // The $blocking flag takes precedence ... - if (!$blocking) { - $timeout = 0; - - // There is a pending timer, only block until it is due ... - } elseif ($scheduledAt = $this->timers->getFirst()) { - $timeout = max(0, $scheduledAt - $this->timers->getTime()); - - // The only possible event is stream activity, so wait forever ... - } elseif ($this->readStreams || $this->writeStreams) { - $timeout = null; - - // There's nothing left to do ... - } else { - return; - } - - $read = $this->readStreams; - $write = $this->writeStreams; - - $this->streamSelect($read, $write, $timeout); - - // Invoke callbacks for read-ready streams ... - foreach ($read as $stream) { - $key = (int) $stream; - - if (isset($this->readListeners[$key])) { - call_user_func($this->readListeners[$key], $stream, $this); - } - } - - // Invoke callbacks for write-ready streams ... - foreach ($write as $stream) { - $key = (int) $stream; - - if (isset($this->writeListeners[$key])) { - call_user_func($this->writeListeners[$key], $stream, $this); - } - } - } - - /** - * Emulate a stream_select() implementation that does not break when passed - * empty stream arrays. - * - * @param array &$read An array of read streams to select upon. - * @param array &$write An array of write streams to select upon. - * @param integer|null $timeout Activity timeout in microseconds, or null to wait forever. - */ - protected function streamSelect(array &$read, array &$write, $timeout) - { - if ($read || $write) { - $except = null; - - return stream_select( - $read, - $write, - $except, - $timeout === null ? null : 0, - $timeout - ); - } - - usleep($timeout); - - return 0; - } -} diff --git a/tests/React/Tests/EventLoop/AbstractLoopTest.php b/tests/React/Tests/EventLoop/AbstractLoopTest.php index 168f89ed..f82fae7c 100644 --- a/tests/React/Tests/EventLoop/AbstractLoopTest.php +++ b/tests/React/Tests/EventLoop/AbstractLoopTest.php @@ -220,6 +220,93 @@ public function testIgnoreRemovedCallback() $loop->run(); } + public function testNextTick() + { + $called = false; + + $callback = function ($loop) use (&$called) { + $this->assertSame($this->loop, $loop); + $called = true; + }; + + $this->loop->nextTick($callback); + + $this->assertFalse($called); + + $this->loop->tick(); + + $this->assertTrue($called); + } + + public function testNextTickFiresBeforeIO() + { + $stream = $this->createStream(); + + $this->loop->addWriteStream( + $stream, + function () { + echo 'stream' . PHP_EOL; + } + ); + + $this->loop->nextTick( + function () { + echo 'next-tick' . PHP_EOL; + } + ); + + $this->expectOutputString('next-tick' . PHP_EOL . 'stream' . PHP_EOL); + + $this->loop->tick(); + } + + public function testRecursiveNextTick() + { + $stream = $this->createStream(); + + $this->loop->addWriteStream( + $stream, + function () { + echo 'stream' . PHP_EOL; + } + ); + + $this->loop->nextTick( + function () { + $this->loop->nextTick( + function () { + echo 'next-tick' . PHP_EOL; + } + ); + } + ); + + $this->expectOutputString('next-tick' . PHP_EOL . 'stream' . PHP_EOL); + + $this->loop->tick(); + } + + public function testRunWaitsForNextTickEvents() + { + $stream = $this->createStream(); + + $this->loop->addWriteStream( + $stream, + function () use ($stream) { + $this->loop->removeStream($stream); + $this->loop->nextTick( + function () { + echo 'next-tick' . PHP_EOL; + } + ); + } + ); + + $this->expectOutputString('next-tick' . PHP_EOL); + + $this->loop->run(); + } + private function assertRunFasterThan($maxInterval) { $start = microtime(true); diff --git a/tests/React/Tests/EventLoop/ExtEventLoopTest.php b/tests/React/Tests/EventLoop/ExtEventLoopTest.php index dde5a731..3f4c4eb7 100644 --- a/tests/React/Tests/EventLoop/ExtEventLoopTest.php +++ b/tests/React/Tests/EventLoop/ExtEventLoopTest.php @@ -6,8 +6,6 @@ class ExtEventLoopTest extends AbstractLoopTest { - use NextTickTestTrait; - public function createLoop() { if ('Linux' === PHP_OS) { diff --git a/tests/React/Tests/EventLoop/NextTickTestTrait.php b/tests/React/Tests/EventLoop/NextTickTestTrait.php deleted file mode 100644 index 8e3c43cc..00000000 --- a/tests/React/Tests/EventLoop/NextTickTestTrait.php +++ /dev/null @@ -1,99 +0,0 @@ -assertSame($this->loop, $loop); - $called = true; - }; - - $this->loop->nextTick($callback); - - $this->assertFalse($called); - - $this->loop->tick(); - - $this->assertTrue($called); - } - - public function testNextTickFiresBeforeIO() - { - $stream = $this->createStream(); - - $this->loop->addWriteStream( - $stream, - function () { - echo 'stream' . PHP_EOL; - } - ); - - $this->loop->nextTick( - function () { - echo 'next-tick' . PHP_EOL; - } - ); - - $this->expectOutputString('next-tick' . PHP_EOL . 'stream' . PHP_EOL); - - $this->loop->tick(); - } - - public function testRecursiveNextTick() - { - $stream = $this->createStream(); - - $this->loop->addWriteStream( - $stream, - function () { - echo 'stream' . PHP_EOL; - } - ); - - $this->loop->nextTick( - function () { - $this->loop->nextTick( - function () { - echo 'next-tick' . PHP_EOL; - } - ); - } - ); - - $this->expectOutputString('next-tick' . PHP_EOL . 'stream' . PHP_EOL); - - $this->loop->tick(); - } - - public function testRunWaitsForNextTickEvents() - { - $stream = $this->createStream(); - - $this->loop->addWriteStream( - $stream, - function () use ($stream) { - $this->loop->removeStream($stream); - $this->loop->nextTick( - function () { - echo 'next-tick' . PHP_EOL; - } - ); - } - ); - - $this->expectOutputString('next-tick' . PHP_EOL); - - $this->loop->run(); - } -} diff --git a/tests/React/Tests/EventLoop/StreamSelectLoopTest.php b/tests/React/Tests/EventLoop/StreamSelectLoopTest.php index 71827600..4cc9c546 100644 --- a/tests/React/Tests/EventLoop/StreamSelectLoopTest.php +++ b/tests/React/Tests/EventLoop/StreamSelectLoopTest.php @@ -8,11 +8,23 @@ class StreamSelectLoopTest extends AbstractLoopTest { public function createLoop() { - return new StreamSelectLoop(); + return new StreamSelectLoop; } - public function testStreamSelectConstructor() + public function testStreamSelectTimeoutEmulation() { - $loop = new StreamSelectLoop(); + $this->loop->addTimer( + 0.05, + $this->expectCallableOnce() + ); + + $start = microtime(true); + + $this->loop->run(); + + $end = microtime(true); + $interval = $end - $start; + + $this->assertGreaterThan(0.04, $interval); } } diff --git a/tests/React/Tests/EventLoop/StreamSelectNextTickLoopTest.php b/tests/React/Tests/EventLoop/StreamSelectNextTickLoopTest.php deleted file mode 100644 index aacb72d3..00000000 --- a/tests/React/Tests/EventLoop/StreamSelectNextTickLoopTest.php +++ /dev/null @@ -1,32 +0,0 @@ -loop->addTimer( - 0.05, - $this->expectCallableOnce() - ); - - $start = microtime(true); - - $this->loop->run(); - - $end = microtime(true); - $interval = $end - $start; - - $this->assertGreaterThan(0.04, $interval); - } -} diff --git a/tests/React/Tests/EventLoop/Timer/AbstractTimerTest.php b/tests/React/Tests/EventLoop/Timer/AbstractTimerTest.php index 8537e39c..fb173c13 100644 --- a/tests/React/Tests/EventLoop/Timer/AbstractTimerTest.php +++ b/tests/React/Tests/EventLoop/Timer/AbstractTimerTest.php @@ -73,4 +73,17 @@ public function testAddPeriodicTimerCancelsItself() $this->assertSame(2, $i); } + + public function testIsTimerActive() + { + $loop = $this->createLoop(); + + $timer = $loop->addPeriodicTimer(0.001, function () {}); + + $this->assertTrue($loop->isTimerActive($timer)); + + $timer->cancel(); + + $this->assertFalse($loop->isTimerActive($timer)); + } } diff --git a/tests/React/Tests/EventLoop/Timer/StreamSelectNextTickTimerTest.php b/tests/React/Tests/EventLoop/Timer/StreamSelectNextTickTimerTest.php deleted file mode 100644 index 9c2bbbef..00000000 --- a/tests/React/Tests/EventLoop/Timer/StreamSelectNextTickTimerTest.php +++ /dev/null @@ -1,13 +0,0 @@ - Date: Fri, 15 Nov 2013 15:41:48 +1000 Subject: [PATCH 09/24] Various changes as per comments in PR. --- src/React/EventLoop/ExtEventLoop.php | 246 ++++++++++++----------- src/React/EventLoop/StreamSelectLoop.php | 5 +- 2 files changed, 135 insertions(+), 116 deletions(-) diff --git a/src/React/EventLoop/ExtEventLoop.php b/src/React/EventLoop/ExtEventLoop.php index 0cd98261..1e00ac30 100644 --- a/src/React/EventLoop/ExtEventLoop.php +++ b/src/React/EventLoop/ExtEventLoop.php @@ -8,7 +8,6 @@ use React\EventLoop\Timer\Timer; use React\EventLoop\Timer\TimerInterface; use SplObjectStorage; -use stdClass; /** * An ext-event based event-loop. @@ -17,10 +16,14 @@ class ExtEventLoop implements LoopInterface { private $eventBase; private $nextTickQueue; + private $timerCallback; private $timerEvents; - private $streamEvents; + private $streamCallback; + private $streamEvents = []; + private $streamFlags = []; + private $readListeners = []; + private $writeListeners = []; private $running; - private $keepAlive; /** * @param EventBase|null $eventBase The libevent event base object. @@ -34,13 +37,9 @@ public function __construct(EventBase $eventBase = null) $this->eventBase = $eventBase; $this->nextTickQueue = new NextTickQueue($this); $this->timerEvents = new SplObjectStorage; - $this->streamEvents = []; - // Closures for cancelled timers and removed stream listeners are - // kept in the keepAlive array until the flushEvents() is complete - // to prevent the PHP fatal error caused by ext-event: - // "Cannot destroy active lambda function" - $this->keepAlive = []; + $this->createTimerCallback(); + $this->createStreamCallback(); } /** @@ -51,7 +50,12 @@ public function __construct(EventBase $eventBase = null) */ public function addReadStream($stream, $listener) { - $this->addStreamEvent($stream, Event::READ, $listener); + $key = (int) $stream; + + if (!isset($this->readListeners[$key])) { + $this->readListeners[$key] = $listener; + $this->subscribeStreamEvent($stream, Event::READ); + } } /** @@ -62,7 +66,12 @@ public function addReadStream($stream, $listener) */ public function addWriteStream($stream, $listener) { - $this->addStreamEvent($stream, Event::WRITE, $listener); + $key = (int) $stream; + + if (!isset($this->writeListeners[$key])) { + $this->writeListeners[$key] = $listener; + $this->subscribeStreamEvent($stream, Event::WRITE, $listener); + } } /** @@ -72,7 +81,12 @@ public function addWriteStream($stream, $listener) */ public function removeReadStream($stream) { - $this->removeStreamEvent($stream, Event::READ); + $key = (int) $stream; + + if (isset($this->readListeners[$key])) { + unset($this->readListeners[$key]); + $this->unsubscribeStreamEvent($stream, Event::READ); + } } /** @@ -82,7 +96,12 @@ public function removeReadStream($stream) */ public function removeWriteStream($stream) { - $this->removeStreamEvent($stream, Event::WRITE); + $key = (int) $stream; + + if (isset($this->writeListeners[$key])) { + unset($this->writeListeners[$key]); + $this->unsubscribeStreamEvent($stream, Event::WRITE); + } } /** @@ -94,17 +113,16 @@ public function removeStream($stream) { $key = (int) $stream; - if (!isset($this->streamEvents[$key])) { - return; - } - - $entry = $this->streamEvents[$key]; - - $entry->event->free(); - - unset($this->streamEvents[$key]); + if (isset($this->streamEvents[$key])) { + $this->streamEvents[$key]->free(); - $this->keepAlive[] = $entry->callback; + unset( + $this->streamFlags[$key], + $this->streamEvents[$key], + $this->readListeners[$key], + $this->writeListeners[$key] + ); + } } /** @@ -155,13 +173,8 @@ public function addPeriodicTimer($interval, $callback) public function cancelTimer(TimerInterface $timer) { if ($this->isTimerActive($timer)) { - $entry = $this->timerEvents[$timer]; - + $this->timerEvents[$timer]->free(); $this->timerEvents->detach($timer); - - $entry->event->free(); - - $this->keepAlive[] = $entry->callback; } } @@ -200,8 +213,6 @@ public function tick() $this->nextTickQueue->tick(); $this->eventBase->loop(EventBase::LOOP_ONCE | EventBase::LOOP_NONBLOCK); - - $this->keepAlive = []; } /** @@ -213,20 +224,13 @@ public function run() while ($this->running) { - if ( - !$this->streamEvents - && !$this->timerEvents->count() - && $this->nextTickQueue->isEmpty() - ) { + $this->nextTickQueue->tick(); + + if (!$this->streamEvents && !$this->timerEvents->count()) { break; } - $this->nextTickQueue->tick(); - $this->eventBase->loop(EventBase::LOOP_ONCE); - - $this->keepAlive = []; - } } @@ -251,74 +255,48 @@ protected function scheduleTimer(TimerInterface $timer) $flags |= Event::PERSIST; } - $entry = new stdClass; - $entry->callback = function () use ($timer) { - call_user_func($timer->getCallback(), $timer); - - // Clean-up one shot timers ... - if ($this->isTimerActive($timer) && !$timer->isPeriodic()) { - $this->cancelTimer($timer); - } - }; - - $entry->event = new Event( + $this->timerEvents[$timer] = $event = new Event( $this->eventBase, -1, $flags, - $entry->callback + $this->timerCallback, + $timer ); - $this->timerEvents->attach($timer, $entry); - - $entry->event->add($timer->getInterval()); + $event->add($timer->getInterval()); } /** * Create a new ext-event Event object, or update the existing one. * - * @param stream $stream - * @param integer $flag Event::READ or Event::WRITE - * @param callable $listener + * @param stream $stream + * @param integer $flag Event::READ or Event::WRITE */ - protected function addStreamEvent($stream, $flag, $listener) + protected function subscribeStreamEvent($stream, $flag) { $key = (int) $stream; if (isset($this->streamEvents[$key])) { - $entry = $this->streamEvents[$key]; - } else { - $entry = new stdClass; - $entry->event = null; - $entry->flags = 0; - $entry->listeners = [ - Event::READ => null, - Event::WRITE => null, - ]; - - $entry->callback = function ($stream, $flags, $loop) use ($entry) { - foreach ([Event::READ, Event::WRITE] as $flag) { - if ( - $flag === ($flags & $flag) && - is_callable($entry->listeners[$flag]) - ) { - call_user_func( - $entry->listeners[$flag], - $stream, - $this - ); - } - } - }; - - $this->streamEvents[$key] = $entry; - } + $event = $this->streamEvents[$key]; - $entry->listeners[$flag] = $listener; - $entry->flags |= $flag; + $event->del(); - $this->configureStreamEvent($entry, $stream); + $event->set( + $this->eventBase, + $stream, + Event::PERSIST | ($this->streamFlags[$key] |= $flag), + $this->streamCallback + ); + } else { + $this->streamEvents[$key] = $event = new Event( + $this->eventBase, + $stream, + Event::PERSIST | ($this->streamFlags[$key] = $flag), + $this->streamCallback + ); + } - $entry->event->add(); + $event->add(); } /** @@ -328,42 +306,80 @@ protected function addStreamEvent($stream, $flag, $listener) * @param stream $stream * @param integer $flag Event::READ or Event::WRITE */ - protected function removeStreamEvent($stream, $flag) + protected function unsubscribeStreamEvent($stream, $flag) { $key = (int) $stream; - if (!isset($this->streamEvents[$key])) { + $flags = $this->streamFlags[$key] &= ~$flag; + + if (0 === $flags) { + $this->removeStream($stream); + return; } - $entry = $this->streamEvents[$key]; - $entry->flags &= ~$flag; - $entry->listeners[$flag] = null; + $event = $this->streamEvents[$key]; - if (0 === $entry->flags) { - $this->removeStream($stream); - } else { - $this->configureStreamEvent($entry, $stream); - } + $event->del(); + + $event->set( + $this->eventBase, + $stream, + Event::PERSIST | $flags, + $this->streamCallback + ); + + $event->add(); } /** - * Create or update an ext-event Event object for the stream. + * Create a callback used as the target of timer events. + * + * A reference is kept to the callback for the lifetime of the loop + * to prevent "Cannot destroy active lambda function" fatal error from + * the event extension. */ - protected function configureStreamEvent($entry, $stream) + protected function createTimerCallback() { - $flags = $entry->flags | Event::PERSIST; + $this->timerCallback = function ($streamIgnored, $flagsIgnored, $timer) { - if ($entry->event) { - $entry->event->del(); - $entry->event->set( - $this->eventBase, $stream, $flags, $entry->callback - ); - $entry->event->add(); - } else { - $entry->event = new Event( - $this->eventBase, $stream, $flags, $entry->callback - ); - } + call_user_func($timer->getCallback(), $timer); + + // Clean-up one shot timers ... + if (!$timer->isPeriodic() && $this->isTimerActive($timer)) { + $this->cancelTimer($timer); + } + + }; + } + + /** + * Create a callback used as the target of stream events. + * + * A reference is kept to the callback for the lifetime of the loop + * to prevent "Cannot destroy active lambda function" fatal error from + * the event extension. + */ + protected function createStreamCallback() + { + $this->streamCallback = function ($stream, $flags) { + + $key = (int) $stream; + + if ( + Event::READ === (Event::READ & $flags) + && isset($this->readListeners[$key]) + ) { + call_user_func($this->readListeners[$key], $stream, $this); + } + + if ( + Event::WRITE === (Event::WRITE & $flags) + && isset($this->writeListeners[$key]) + ) { + call_user_func($this->writeListeners[$key], $stream, $this); + } + + }; } } diff --git a/src/React/EventLoop/StreamSelectLoop.php b/src/React/EventLoop/StreamSelectLoop.php index 4a4e544e..ab26eeb0 100644 --- a/src/React/EventLoop/StreamSelectLoop.php +++ b/src/React/EventLoop/StreamSelectLoop.php @@ -201,7 +201,10 @@ public function run() // There is a pending timer, only block until it is due ... if ($scheduledAt = $this->timers->getFirst()) { - $timeout = max(0, $scheduledAt - $this->timers->getTime()); + + if (0 > $timeout = $scheduledAt - $this->timers->getTime()) { + $timeout = 0; + } // The only possible event is stream activity, so wait forever ... } elseif ($this->readStreams || $this->writeStreams) { From 93da5ec7c889f19d818436479696c40ceff1406e Mon Sep 17 00:00:00 2001 From: James Harris Date: Fri, 15 Nov 2013 17:58:26 +1000 Subject: [PATCH 10/24] Fixed and added test-case for next-tick events added by timers. --- src/React/EventLoop/StreamSelectLoop.php | 6 +++++- .../React/Tests/EventLoop/AbstractLoopTest.php | 18 ++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/React/EventLoop/StreamSelectLoop.php b/src/React/EventLoop/StreamSelectLoop.php index ab26eeb0..a763cb4a 100644 --- a/src/React/EventLoop/StreamSelectLoop.php +++ b/src/React/EventLoop/StreamSelectLoop.php @@ -199,8 +199,12 @@ public function run() $this->timers->tick(); + // Timers have placed more items on the next-tick queue ... + if (!$this->nextTickQueue->isEmpty()) { + $timeout = 0; + // There is a pending timer, only block until it is due ... - if ($scheduledAt = $this->timers->getFirst()) { + } elseif ($scheduledAt = $this->timers->getFirst()) { if (0 > $timeout = $scheduledAt - $this->timers->getTime()) { $timeout = 0; diff --git a/tests/React/Tests/EventLoop/AbstractLoopTest.php b/tests/React/Tests/EventLoop/AbstractLoopTest.php index f82fae7c..5318a599 100644 --- a/tests/React/Tests/EventLoop/AbstractLoopTest.php +++ b/tests/React/Tests/EventLoop/AbstractLoopTest.php @@ -307,6 +307,24 @@ function () { $this->loop->run(); } + public function testNextTickEventGeneratedByTimer() + { + $this->loop->addTimer( + 0.001, + function () { + $this->loop->nextTick( + function () { + echo 'next-tick' . PHP_EOL; + } + ); + } + ); + + $this->expectOutputString('next-tick' . PHP_EOL); + + $this->loop->run(); + } + private function assertRunFasterThan($maxInterval) { $start = microtime(true); From dc43f53ae6abc1769afca1dff329745fdc392ca5 Mon Sep 17 00:00:00 2001 From: James Harris Date: Sat, 16 Nov 2013 07:26:18 +1000 Subject: [PATCH 11/24] Removed some unnecessary left-overs. --- src/React/EventLoop/ExtEventLoop.php | 11 ++--------- src/React/EventLoop/StreamSelectLoop.php | 2 -- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/src/React/EventLoop/ExtEventLoop.php b/src/React/EventLoop/ExtEventLoop.php index 1e00ac30..2a450750 100644 --- a/src/React/EventLoop/ExtEventLoop.php +++ b/src/React/EventLoop/ExtEventLoop.php @@ -25,16 +25,9 @@ class ExtEventLoop implements LoopInterface private $writeListeners = []; private $running; - /** - * @param EventBase|null $eventBase The libevent event base object. - */ - public function __construct(EventBase $eventBase = null) + public function __construct() { - if (null === $eventBase) { - $eventBase = new EventBase; - } - - $this->eventBase = $eventBase; + $this->eventBase = new EventBase; $this->nextTickQueue = new NextTickQueue($this); $this->timerEvents = new SplObjectStorage; diff --git a/src/React/EventLoop/StreamSelectLoop.php b/src/React/EventLoop/StreamSelectLoop.php index a763cb4a..84785d38 100644 --- a/src/React/EventLoop/StreamSelectLoop.php +++ b/src/React/EventLoop/StreamSelectLoop.php @@ -258,8 +258,6 @@ protected function waitForStreamActivity($timeout) call_user_func($this->writeListeners[$key], $stream, $this); } } - - return true; } /** From a8af7bced683e692ab41db3ed1e7798d0d24e677 Mon Sep 17 00:00:00 2001 From: James Harris Date: Sat, 16 Nov 2013 08:45:38 +1000 Subject: [PATCH 12/24] Use status for unused parameters. --- src/React/EventLoop/ExtEventLoop.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/React/EventLoop/ExtEventLoop.php b/src/React/EventLoop/ExtEventLoop.php index 2a450750..7d78b9d8 100644 --- a/src/React/EventLoop/ExtEventLoop.php +++ b/src/React/EventLoop/ExtEventLoop.php @@ -334,7 +334,7 @@ protected function unsubscribeStreamEvent($stream, $flag) */ protected function createTimerCallback() { - $this->timerCallback = function ($streamIgnored, $flagsIgnored, $timer) { + $this->timerCallback = function ($_, $_, $timer) { call_user_func($timer->getCallback(), $timer); From 18a00c7dd8fb3e21c6cbf0a0cc37580555051f4a Mon Sep 17 00:00:00 2001 From: James Harris Date: Mon, 18 Nov 2013 19:21:00 +1000 Subject: [PATCH 13/24] Added nextTick() support to LibEventLoop (based off ExtEventLoop). --- src/React/EventLoop/LibEventLoop.php | 442 ++++++++++++------ .../Tests/EventLoop/AbstractLoopTest.php | 47 +- .../Tests/EventLoop/LibEventLoopTest.php | 40 +- 3 files changed, 357 insertions(+), 172 deletions(-) diff --git a/src/React/EventLoop/LibEventLoop.php b/src/React/EventLoop/LibEventLoop.php index 363a1193..68e9d49b 100644 --- a/src/React/EventLoop/LibEventLoop.php +++ b/src/React/EventLoop/LibEventLoop.php @@ -2,103 +2,194 @@ namespace React\EventLoop; -use SplObjectStorage; +use Event; +use EventBase; +use React\EventLoop\NextTick\NextTickQueue; use React\EventLoop\Timer\Timer; use React\EventLoop\Timer\TimerInterface; +use SplObjectStorage; +/** + * An ext-libevent based event-loop. + */ class LibEventLoop implements LoopInterface { - const MIN_TIMER_RESOLUTION = 0.001; - - private $base; - private $callback; - private $timers; - - private $events = array(); - private $flags = array(); - private $readCallbacks = array(); - private $writeCallbacks = array(); + const MICROSECONDS_PER_SECOND = 1000000; + + private $eventBase; + private $nextTickQueue; + private $timerCallback; + private $timerEvents; + private $streamCallback; + private $streamEvents = []; + private $streamFlags = []; + private $readListeners = []; + private $writeListeners = []; + private $running; public function __construct() { - $this->base = event_base_new(); - $this->callback = $this->createLibeventCallback(); - $this->timers = new SplObjectStorage(); + $this->eventBase = event_base_new(); + $this->nextTickQueue = new NextTickQueue($this); + $this->timerEvents = new SplObjectStorage; + + $this->createTimerCallback(); + $this->createStreamCallback(); } - protected function createLibeventCallback() + /** + * Register a listener to be notified when a stream is ready to read. + * + * @param stream $stream The PHP stream resource to check. + * @param callable $listener Invoked when the stream is ready. + */ + public function addReadStream($stream, $listener) { - $readCallbacks = &$this->readCallbacks; - $writeCallbacks = &$this->writeCallbacks; + $key = (int) $stream; - return function ($stream, $flags, $loop) use (&$readCallbacks, &$writeCallbacks) { - $id = (int) $stream; - - try { - if (($flags & EV_READ) === EV_READ && isset($readCallbacks[$id])) { - call_user_func($readCallbacks[$id], $stream, $loop); - } + if (!isset($this->readListeners[$key])) { + $this->readListeners[$key] = $listener; + $this->subscribeStreamEvent($stream, EV_READ); + } + } - if (($flags & EV_WRITE) === EV_WRITE && isset($writeCallbacks[$id])) { - call_user_func($writeCallbacks[$id], $stream, $loop); - } - } catch (\Exception $ex) { - // If one of the callbacks throws an exception we must stop the loop - // otherwise libevent will swallow the exception and go berserk. - $loop->stop(); + /** + * Register a listener to be notified when a stream is ready to write. + * + * @param stream $stream The PHP stream resource to check. + * @param callable $listener Invoked when the stream is ready. + */ + public function addWriteStream($stream, $listener) + { + $key = (int) $stream; - throw $ex; - } - }; + if (!isset($this->writeListeners[$key])) { + $this->writeListeners[$key] = $listener; + $this->subscribeStreamEvent($stream, EV_WRITE); + } } - public function addReadStream($stream, $listener) + /** + * Remove the read event listener for the given stream. + * + * @param stream $stream The PHP stream resource. + */ + public function removeReadStream($stream) { - $this->addStreamEvent($stream, EV_READ, 'read', $listener); + $key = (int) $stream; + + if (isset($this->readListeners[$key])) { + unset($this->readListeners[$key]); + $this->unsubscribeStreamEvent($stream, EV_READ); + } } - public function addWriteStream($stream, $listener) + /** + * Remove the write event listener for the given stream. + * + * @param stream $stream The PHP stream resource. + */ + public function removeWriteStream($stream) { - $this->addStreamEvent($stream, EV_WRITE, 'write', $listener); + $key = (int) $stream; + + if (isset($this->writeListeners[$key])) { + unset($this->writeListeners[$key]); + $this->unsubscribeStreamEvent($stream, EV_WRITE); + } } - protected function addStreamEvent($stream, $eventClass, $type, $listener) + /** + * Remove all listeners for the given stream. + * + * @param stream $stream The PHP stream resource. + */ + public function removeStream($stream) { - $id = (int) $stream; + $key = (int) $stream; - if ($existing = isset($this->events[$id])) { - if (($this->flags[$id] & $eventClass) === $eventClass) { - return; - } - $event = $this->events[$id]; + if (isset($this->streamEvents[$key])) { + + $event = $this->streamEvents[$key]; event_del($event); - } else { - $event = event_new(); + event_free($event); + + unset( + $this->streamFlags[$key], + $this->streamEvents[$key], + $this->readListeners[$key], + $this->writeListeners[$key] + ); } + } - $flags = isset($this->flags[$id]) ? $this->flags[$id] | $eventClass : $eventClass; - event_set($event, $stream, $flags | EV_PERSIST, $this->callback, $this); + /** + * Enqueue a callback to be invoked once after the given interval. + * + * The execution order of timers scheduled to execute at the same time is + * not guaranteed. + * + * @param numeric $interval The number of seconds to wait before execution. + * @param callable $callback The callback to invoke. + * + * @return TimerInterface + */ + public function addTimer($interval, $callback) + { + $timer = new Timer($this, $interval, $callback, false); - if (!$existing) { - // Set the base only if $event has been newly created or be ready for segfaults. - event_base_set($event, $this->base); - } + $this->scheduleTimer($timer); - event_add($event); + return $timer; + } + + /** + * Enqueue a callback to be invoked repeatedly after the given interval. + * + * The execution order of timers scheduled to execute at the same time is + * not guaranteed. + * + * @param numeric $interval The number of seconds to wait before execution. + * @param callable $callback The callback to invoke. + * + * @return TimerInterface + */ + public function addPeriodicTimer($interval, $callback) + { + $timer = new Timer($this, $interval, $callback, true); - $this->events[$id] = $event; - $this->flags[$id] = $flags; - $this->{"{$type}Callbacks"}[$id] = $listener; + $this->scheduleTimer($timer); + + return $timer; } - public function removeReadStream($stream) + /** + * Cancel a pending timer. + * + * @param TimerInterface $timer The timer to cancel. + */ + public function cancelTimer(TimerInterface $timer) { - $this->removeStreamEvent($stream, EV_READ, 'read'); + if ($this->isTimerActive($timer)) { + $event = $this->timerEvents[$timer]; + + event_del($event); + event_free($event); + + $this->timerEvents->detach($timer); + } } - public function removeWriteStream($stream) + /** + * Check if a given timer is active. + * + * @param TimerInterface $timer The timer to check. + * + * @return boolean True if the timer is still enqueued for execution. + */ + public function isTimerActive(TimerInterface $timer) { - $this->removeStreamEvent($stream, EV_WRITE, 'write'); + return $this->timerEvents->contains($timer); } /** @@ -107,129 +198,196 @@ public function removeWriteStream($stream) * Callbacks are guaranteed to be executed in the order they are enqueued, * before any timer or stream events. * - * @param callable $listner The callback to invoke. + * @param callable $listener The callback to invoke. */ public function nextTick(callable $listener) { - throw new \Exception('Not yet implemented.'); + $this->nextTickQueue->add($listener); } - protected function removeStreamEvent($stream, $eventClass, $type) + /** + * Perform a single iteration of the event loop. + * + * @param boolean $blocking True if loop should block waiting for next event. + */ + public function tick() { - $id = (int) $stream; + $this->nextTickQueue->tick(); - if (isset($this->events[$id])) { - $flags = $this->flags[$id] & ~$eventClass; + event_base_loop($this->eventBase, EVLOOP_ONCE | EVLOOP_NONBLOCK); + } - if ($flags === 0) { - // Remove if stream is not subscribed to any event at this point. - return $this->removeStream($stream); - } + /** + * Run the event loop until there are no more tasks to perform. + */ + public function run() + { + $this->running = true; - $event = $this->events[$id]; + while ($this->running) { - event_del($event); - event_free($event); - unset($this->{"{$type}Callbacks"}[$id]); + $this->nextTickQueue->tick(); - $event = event_new(); - event_set($event, $stream, $flags | EV_PERSIST, $this->callback, $this); - event_base_set($event, $this->base); - event_add($event); + if (!$this->streamEvents && !$this->timerEvents->count()) { + break; + } - $this->events[$id] = $event; - $this->flags[$id] = $flags; + event_base_loop($this->eventBase, EVLOOP_ONCE); } } - public function removeStream($stream) + /** + * Instruct a running event loop to stop. + */ + public function stop() + { + $this->running = false; + } + + /** + * Schedule a timer for execution. + * + * @param TimerInterface $timer + */ + protected function scheduleTimer(TimerInterface $timer) { - $id = (int) $stream; + $this->timerEvents[$timer] = $event = event_timer_new(); - if (isset($this->events[$id])) { - $event = $this->events[$id]; + event_timer_set($event, $this->timerCallback, $timer); - unset( - $this->events[$id], - $this->flags[$id], - $this->readCallbacks[$id], - $this->writeCallbacks[$id] - ); + event_base_set($event, $this->eventBase); - event_del($event); - event_free($event); - } + event_add( + $event, + $timer->getInterval() * self::MICROSECONDS_PER_SECOND + ); } - protected function addTimerInternal($interval, $callback, $periodic = false) + /** + * Create a new ext-libevent event resource, or update the existing one. + * + * @param stream $stream + * @param integer $flag EV_READ or EV_WRITE + */ + protected function subscribeStreamEvent($stream, $flag) { - if ($interval < self::MIN_TIMER_RESOLUTION) { - throw new \InvalidArgumentException('Timer events do not support sub-millisecond timeouts.'); - } + $key = (int) $stream; - $timer = new Timer($this, $interval, $callback, $periodic); - $resource = event_new(); + if (isset($this->streamEvents[$key])) { + $event = $this->streamEvents[$key]; - $timers = $this->timers; - $timers->attach($timer, $resource); + event_del($event); - $callback = function () use ($timers, $timer, &$callback) { - if (isset($timers[$timer])) { - call_user_func($timer->getCallback(), $timer); + event_set( + $event, + $stream, + EV_PERSIST | ($this->streamFlags[$key] |= $flag), + $this->streamCallback + ); + } else { + $this->streamEvents[$key] = $event = event_new(); - if ($timer->isPeriodic() && isset($timers[$timer])) { - event_add($timers[$timer], $timer->getInterval() * 1000000); - } else { - $timer->cancel(); - } - } - }; + event_set( + $event, + $stream, + EV_PERSIST | ($this->streamFlags[$key] = $flag), + $this->streamCallback + ); - event_timer_set($resource, $callback); - event_base_set($resource, $this->base); - event_add($resource, $interval * 1000000); + event_base_set($event, $this->eventBase); + } - return $timer; - } - public function addTimer($interval, $callback) - { - return $this->addTimerInternal($interval, $callback); + event_add($event); } - public function addPeriodicTimer($interval, $callback) + /** + * Update the ext-libevent event resource for this stream to stop listening to + * the given event type, or remove it entirely if it's no longer needed. + * + * @param stream $stream + * @param integer $flag EV_READ or EV_WRITE + */ + protected function unsubscribeStreamEvent($stream, $flag) { - return $this->addTimerInternal($interval, $callback, true); - } + $key = (int) $stream; - public function cancelTimer(TimerInterface $timer) - { - if (isset($this->timers[$timer])) { - $resource = $this->timers[$timer]; - event_del($resource); - event_free($resource); + $flags = $this->streamFlags[$key] &= ~$flag; - $this->timers->detach($timer); + if (0 === $flags) { + $this->removeStream($stream); + + return; } - } - public function isTimerActive(TimerInterface $timer) - { - return $this->timers->contains($timer); - } + $event = $this->streamEvents[$key]; - public function tick() - { - event_base_loop($this->base, EVLOOP_ONCE | EVLOOP_NONBLOCK); + event_del($event); + + event_set($event, $stream, EV_PERSIST | $flags, $this->streamCallback); + + event_add($event); } - public function run() + /** + * Create a callback used as the target of timer events. + * + * A reference is kept to the callback for the lifetime of the loop + * to prevent "Cannot destroy active lambda function" fatal error from + * the event extension. + */ + protected function createTimerCallback() { - event_base_loop($this->base); + $this->timerCallback = function ($_, $_, $timer) { + + call_user_func($timer->getCallback(), $timer); + + // Timer already cancelled ... + if (!$this->isTimerActive($timer)) { + return; + + // Reschedule periodic timers ... + } elseif ($timer->isPeriodic()) { + event_add( + $this->timerEvents[$timer], + $timer->getInterval() * self::MICROSECONDS_PER_SECOND + ); + + // Clean-up one shot timers ... + } else { + $this->cancelTimer($timer); + } + + }; } - public function stop() + /** + * Create a callback used as the target of stream events. + * + * A reference is kept to the callback for the lifetime of the loop + * to prevent "Cannot destroy active lambda function" fatal error from + * the event extension. + */ + protected function createStreamCallback() { - event_base_loopexit($this->base); + $this->streamCallback = function ($stream, $flags) { + + $key = (int) $stream; + + if ( + EV_READ === (EV_READ & $flags) + && isset($this->readListeners[$key]) + ) { + call_user_func($this->readListeners[$key], $stream, $this); + } + + if ( + EV_WRITE === (EV_WRITE & $flags) + && isset($this->writeListeners[$key]) + ) { + call_user_func($this->writeListeners[$key], $stream, $this); + } + + }; } } diff --git a/tests/React/Tests/EventLoop/AbstractLoopTest.php b/tests/React/Tests/EventLoop/AbstractLoopTest.php index 5318a599..40b237ba 100644 --- a/tests/React/Tests/EventLoop/AbstractLoopTest.php +++ b/tests/React/Tests/EventLoop/AbstractLoopTest.php @@ -20,18 +20,22 @@ public function createStream() return fopen('php://temp', 'r+'); } + public function writeToStream($stream, $content) + { + fwrite($stream, $content); + rewind($stream); + } + public function testAddReadStream() { $input = $this->createStream(); $this->loop->addReadStream($input, $this->expectCallableExactly(2)); - fwrite($input, "foo\n"); - rewind($input); + $this->writeToStream($input, "foo\n"); $this->loop->tick(); - fwrite($input, "bar\n"); - rewind($input); + $this->writeToStream($input, "bar\n"); $this->loop->tick(); } @@ -51,8 +55,7 @@ public function testRemoveReadStreamInstantly() $this->loop->addReadStream($input, $this->expectCallableNever()); $this->loop->removeReadStream($input); - fwrite($input, "bar\n"); - rewind($input); + $this->writeToStream($input, "bar\n"); $this->loop->tick(); } @@ -62,14 +65,12 @@ public function testRemoveReadStreamAfterReading() $this->loop->addReadStream($input, $this->expectCallableOnce()); - fwrite($input, "foo\n"); - rewind($input); + $this->writeToStream($input, "foo\n"); $this->loop->tick(); $this->loop->removeReadStream($input); - fwrite($input, "bar\n"); - rewind($input); + $this->writeToStream($input, "bar\n"); $this->loop->tick(); } @@ -101,8 +102,7 @@ public function testRemoveStreamInstantly() $this->loop->addWriteStream($input, $this->expectCallableNever()); $this->loop->removeStream($input); - fwrite($input, "bar\n"); - rewind($input); + $this->writeToStream($input, "bar\n"); $this->loop->tick(); } @@ -114,8 +114,7 @@ public function testRemoveStreamForReadOnly() $this->loop->addWriteStream($input, $this->expectCallableOnce()); $this->loop->removeReadStream($input); - fwrite($input, "foo\n"); - rewind($input); + $this->writeToStream($input, "foo\n"); $this->loop->tick(); } @@ -123,6 +122,8 @@ public function testRemoveStreamForWriteOnly() { $input = $this->createStream(); + $this->writeToStream($input, "foo\n"); + $this->loop->addReadStream($input, $this->expectCallableOnce()); $this->loop->addWriteStream($input, $this->expectCallableNever()); $this->loop->removeWriteStream($input); @@ -137,14 +138,12 @@ public function testRemoveStream() $this->loop->addReadStream($input, $this->expectCallableOnce()); $this->loop->addWriteStream($input, $this->expectCallableOnce()); - fwrite($input, "bar\n"); - rewind($input); + $this->writeToStream($input, "bar\n"); $this->loop->tick(); $this->loop->removeStream($input); - fwrite($input, "bar\n"); - rewind($input); + $this->writeToStream($input, "bar\n"); $this->loop->tick(); } @@ -174,8 +173,7 @@ public function runShouldReturnWhenNoMoreFds() $loop->removeStream($stream); }); - fwrite($input, "foo\n"); - rewind($input); + $this->writeToStream($input, "foo\n"); $this->assertRunFasterThan(0.005); } @@ -190,8 +188,7 @@ public function stopShouldStopRunningLoop() $loop->stop(); }); - fwrite($input, "foo\n"); - rewind($input); + $this->writeToStream($input, "foo\n"); $this->assertRunFasterThan(0.005); } @@ -212,10 +209,8 @@ public function testIgnoreRemovedCallback() // this callback would have to be called as well, but the first stream already removed us $loop->addReadStream($stream2, $this->expectCallableNever()); - fwrite($stream1, "foo\n"); - rewind($stream1); - fwrite($stream2, "foo\n"); - rewind($stream2); + $this->writeToStream($stream1, "foo\n"); + $this->writeToStream($stream2, "foo\n"); $loop->run(); } diff --git a/tests/React/Tests/EventLoop/LibEventLoopTest.php b/tests/React/Tests/EventLoop/LibEventLoopTest.php index 4b39b71f..564424d9 100644 --- a/tests/React/Tests/EventLoop/LibEventLoopTest.php +++ b/tests/React/Tests/EventLoop/LibEventLoopTest.php @@ -6,9 +6,11 @@ class LibEventLoopTest extends AbstractLoopTest { + private $fifoPath; + public function createLoop() { - if ('Linux' === PHP_OS) { + if ('Linux' === PHP_OS && !extension_loaded('posix')) { $this->markTestSkipped('libevent tests skipped on linux due to linux epoll issues.'); } @@ -16,11 +18,41 @@ public function createLoop() $this->markTestSkipped('libevent tests skipped because ext-libevent is not installed.'); } - return new LibEventLoop(); + return new LibEventLoop; + } + + public function tearDown() + { + if (file_exists($this->fifoPath)) { + unlink($this->fifoPath); + } + } + + public function createStream() + { + if ('Linux' !== PHP_OS) { + return parent::createStream(); + } + + $this->fifoPath = tempnam(sys_get_temp_dir(), 'react-'); + + unlink($this->fifoPath); + + // Use a FIFO on linux to get around lack of support for disk-based file + // descriptors when using the EPOLL back-end. + posix_mkfifo($this->fifoPath, 0600); + + $stream = fopen($this->fifoPath, 'r+'); + + return $stream; } - public function testLibEventConstructor() + public function writeToStream($stream, $content) { - $loop = new LibEventLoop(); + if ('Linux' !== PHP_OS) { + return parent::setStreamContent($stream, $content); + } + + fwrite($stream, $content); } } From 3bba9e4a68614bcbc0b08e50d83a6024bba4b6ea Mon Sep 17 00:00:00 2001 From: James Harris Date: Mon, 18 Nov 2013 20:41:41 +1000 Subject: [PATCH 14/24] Renamed NextTick namespace to Tick + enable ExtEventLoop tests for Linux. --- src/React/EventLoop/ExtEventLoop.php | 4 +-- src/React/EventLoop/LibEventLoop.php | 2 +- src/React/EventLoop/StreamSelectLoop.php | 2 +- .../{NextTick => Tick}/NextTickQueue.php | 2 +- .../Tests/EventLoop/ExtEventLoopTest.php | 30 ++++++++++++++++--- .../Tests/EventLoop/LibEventLoopTest.php | 2 +- 6 files changed, 32 insertions(+), 10 deletions(-) rename src/React/EventLoop/{NextTick => Tick}/NextTickQueue.php (97%) diff --git a/src/React/EventLoop/ExtEventLoop.php b/src/React/EventLoop/ExtEventLoop.php index 7d78b9d8..86559341 100644 --- a/src/React/EventLoop/ExtEventLoop.php +++ b/src/React/EventLoop/ExtEventLoop.php @@ -4,7 +4,7 @@ use Event; use EventBase; -use React\EventLoop\NextTick\NextTickQueue; +use React\EventLoop\Tick\NextTickQueue; use React\EventLoop\Timer\Timer; use React\EventLoop\Timer\TimerInterface; use SplObjectStorage; @@ -63,7 +63,7 @@ public function addWriteStream($stream, $listener) if (!isset($this->writeListeners[$key])) { $this->writeListeners[$key] = $listener; - $this->subscribeStreamEvent($stream, Event::WRITE, $listener); + $this->subscribeStreamEvent($stream, Event::WRITE); } } diff --git a/src/React/EventLoop/LibEventLoop.php b/src/React/EventLoop/LibEventLoop.php index 68e9d49b..62b4f656 100644 --- a/src/React/EventLoop/LibEventLoop.php +++ b/src/React/EventLoop/LibEventLoop.php @@ -4,7 +4,7 @@ use Event; use EventBase; -use React\EventLoop\NextTick\NextTickQueue; +use React\EventLoop\Tick\NextTickQueue; use React\EventLoop\Timer\Timer; use React\EventLoop\Timer\TimerInterface; use SplObjectStorage; diff --git a/src/React/EventLoop/StreamSelectLoop.php b/src/React/EventLoop/StreamSelectLoop.php index 84785d38..6ce59e98 100644 --- a/src/React/EventLoop/StreamSelectLoop.php +++ b/src/React/EventLoop/StreamSelectLoop.php @@ -2,7 +2,7 @@ namespace React\EventLoop; -use React\EventLoop\NextTick\NextTickQueue; +use React\EventLoop\Tick\NextTickQueue; use React\EventLoop\Timer\Timer; use React\EventLoop\Timer\TimerInterface; use React\EventLoop\Timer\Timers; diff --git a/src/React/EventLoop/NextTick/NextTickQueue.php b/src/React/EventLoop/Tick/NextTickQueue.php similarity index 97% rename from src/React/EventLoop/NextTick/NextTickQueue.php rename to src/React/EventLoop/Tick/NextTickQueue.php index 6a27e569..85a5abdf 100644 --- a/src/React/EventLoop/NextTick/NextTickQueue.php +++ b/src/React/EventLoop/Tick/NextTickQueue.php @@ -1,6 +1,6 @@ markTestSkipped('libevent tests skipped on linux due to linux epoll issues.'); } @@ -21,17 +21,39 @@ public function createLoop() public function createStream() { + // Use a FIFO on linux to get around lack of support for disk-based file + // descriptors when using the EPOLL back-end. + if ('Linux' === PHP_OS) { + $this->fifoPath = tempnam(sys_get_temp_dir(), 'react-'); + + unlink($this->fifoPath); + + posix_mkfifo($this->fifoPath, 0600); + + $stream = fopen($this->fifoPath, 'r+'); + // ext-event (as of 1.8.1) does not yet support in-memory temporary // streams. Setting maxmemory:0 and performing a write forces PHP to // back this temporary stream with a real file. // // This problem is mentioned at https://bugs.php.net/bug.php?id=64652&edit=3 // but remains unresolved (despite that issue being closed). - $stream = fopen('php://temp/maxmemory:0', 'r+'); + } else { + $stream = fopen('php://temp/maxmemory:0', 'r+'); - fwrite($stream, 'x'); - ftruncate($stream, 0); + fwrite($stream, 'x'); + ftruncate($stream, 0); + } return $stream; } + + public function writeToStream($stream, $content) + { + if ('Linux' !== PHP_OS) { + return parent::writeToStream($stream, $content); + } + + fwrite($stream, $content); + } } diff --git a/tests/React/Tests/EventLoop/LibEventLoopTest.php b/tests/React/Tests/EventLoop/LibEventLoopTest.php index 564424d9..2505bb3b 100644 --- a/tests/React/Tests/EventLoop/LibEventLoopTest.php +++ b/tests/React/Tests/EventLoop/LibEventLoopTest.php @@ -50,7 +50,7 @@ public function createStream() public function writeToStream($stream, $content) { if ('Linux' !== PHP_OS) { - return parent::setStreamContent($stream, $content); + return parent::writeToStream($stream, $content); } fwrite($stream, $content); From 74131f7065c291e8dbb40dbc74e143097f24f40e Mon Sep 17 00:00:00 2001 From: James Harris Date: Mon, 18 Nov 2013 21:01:13 +1000 Subject: [PATCH 15/24] Added nextTick() support to LibEvLoop. --- src/React/EventLoop/LibEvLoop.php | 224 +++++++++++++++++++++--------- 1 file changed, 159 insertions(+), 65 deletions(-) diff --git a/src/React/EventLoop/LibEvLoop.php b/src/React/EventLoop/LibEvLoop.php index 9ff997aa..05f1a925 100644 --- a/src/React/EventLoop/LibEvLoop.php +++ b/src/React/EventLoop/LibEvLoop.php @@ -2,9 +2,13 @@ namespace React\EventLoop; -use SplObjectStorage; +use libev\EventLoop; +use libev\IOEvent; +use libev\TimerEvent; +use React\EventLoop\Tick\NextTickQueue; use React\EventLoop\Timer\Timer; use React\EventLoop\Timer\TimerInterface; +use SplObjectStorage; /** * @see https://github.com/m4rw3r/php-libev @@ -13,48 +17,141 @@ class LibEvLoop implements LoopInterface { private $loop; + private $nextTickQueue; private $timers; private $readEvents = array(); private $writeEvents = array(); + private $running; public function __construct() { - $this->loop = new \libev\EventLoop(); - $this->timers = new SplObjectStorage(); + $this->loop = new EventLoop; + $this->nextTickQueue = new NextTickQueue($this); + $this->timers = new SplObjectStorage; } + /** + * Register a listener to be notified when a stream is ready to read. + * + * @param stream $stream The PHP stream resource to check. + * @param callable $listener Invoked when the stream is ready. + */ public function addReadStream($stream, $listener) { - $this->addStream($stream, $listener, \libev\IOEvent::READ); + $this->addStream($stream, $listener, IOEvent::READ); } + /** + * Register a listener to be notified when a stream is ready to write. + * + * @param stream $stream The PHP stream resource to check. + * @param callable $listener Invoked when the stream is ready. + */ public function addWriteStream($stream, $listener) { - $this->addStream($stream, $listener, \libev\IOEvent::WRITE); + $this->addStream($stream, $listener, IOEvent::WRITE); } + /** + * Remove the read event listener for the given stream. + * + * @param stream $stream The PHP stream resource. + */ public function removeReadStream($stream) { - if (isset($this->readEvents[(int)$stream])) { - $this->readEvents[(int)$stream]->stop(); - unset($this->readEvents[(int)$stream]); + if (isset($this->readEvents[(int) $stream])) { + $this->readEvents[(int) $stream]->stop(); + unset($this->readEvents[(int) $stream]); } } + /** + * Remove the write event listener for the given stream. + * + * @param stream $stream The PHP stream resource. + */ public function removeWriteStream($stream) { - if (isset($this->writeEvents[(int)$stream])) { - $this->writeEvents[(int)$stream]->stop(); - unset($this->writeEvents[(int)$stream]); + if (isset($this->writeEvents[(int) $stream])) { + $this->writeEvents[(int) $stream]->stop(); + unset($this->writeEvents[(int) $stream]); } } + /** + * Remove all listeners for the given stream. + * + * @param stream $stream The PHP stream resource. + */ public function removeStream($stream) { $this->removeReadStream($stream); $this->removeWriteStream($stream); } + /** + * Enqueue a callback to be invoked once after the given interval. + * + * The execution order of timers scheduled to execute at the same time is + * not guaranteed. + * + * @param numeric $interval The number of seconds to wait before execution. + * @param callable $callback The callback to invoke. + * + * @return TimerInterface + */ + public function addTimer($interval, $callback) + { + $timer = new Timer($this, $interval, $callback, false); + $this->setupTimer($timer); + + return $timer; + } + + /** + * Enqueue a callback to be invoked repeatedly after the given interval. + * + * The execution order of timers scheduled to execute at the same time is + * not guaranteed. + * + * @param numeric $interval The number of seconds to wait before execution. + * @param callable $callback The callback to invoke. + * + * @return TimerInterface + */ + public function addPeriodicTimer($interval, $callback) + { + $timer = new Timer($this, $interval, $callback, true); + $this->setupTimer($timer); + + return $timer; + } + + /** + * Cancel a pending timer. + * + * @param TimerInterface $timer The timer to cancel. + */ + public function cancelTimer(TimerInterface $timer) + { + if (isset($this->timers[$timer])) { + $this->loop->remove($this->timers[$timer]); + $this->timers->detach($timer); + } + } + + /** + * Check if a given timer is active. + * + * @param TimerInterface $timer The timer to check. + * + * @return boolean True if the timer is still enqueued for execution. + */ + public function isTimerActive(TimerInterface $timer) + { + return $this->timers->contains($timer); + } + /** * Schedule a callback to be invoked on the next tick of the event loop. * @@ -65,57 +162,74 @@ public function removeStream($stream) */ public function nextTick(callable $listener) { - throw new \Exception('Not yet implemented.'); + $this->nextTickQueue->add($listener); } - private function addStream($stream, $listener, $flags) + /** + * Perform a single iteration of the event loop. + */ + public function tick() { - $listener = $this->wrapStreamListener($stream, $listener, $flags); - $event = new \libev\IOEvent($listener, $stream, $flags); - $this->loop->add($event); + $this->nextTickQueue->tick(); - if (($flags & \libev\IOEvent::READ) === $flags) { - $this->readEvents[(int)$stream] = $event; - } elseif (($flags & \libev\IOEvent::WRITE) === $flags) { - $this->writeEvents[(int)$stream] = $event; - } + $this->loop->run(EventLoop::RUN_ONCE | EventLoop::RUN_NOWAIT); } - private function wrapStreamListener($stream, $listener, $flags) + /** + * Run the event loop until there are no more tasks to perform. + */ + public function run() { - if (($flags & \libev\IOEvent::READ) === $flags) { - $removeCallback = array($this, 'removeReadStream'); - } elseif (($flags & \libev\IOEvent::WRITE) === $flags) { - $removeCallback = array($this, 'removeWriteStream'); - } + $this->running = true; - return function ($event) use ($stream, $listener, $removeCallback) { - call_user_func($listener, $stream); - }; + while ($this->running) { + + $this->nextTickQueue->tick(); + + if ( + !$this->readEvents + && !$this->writeEvents + && !$this->timers->count() + ) { + break; + } + + $this->loop->run(EventLoop::RUN_ONCE); + } } - public function addTimer($interval, $callback) + /** + * Instruct a running event loop to stop. + */ + public function stop() { - $timer = new Timer($this, $interval, $callback, false); - $this->setupTimer($timer); - - return $timer; + $this->running = false; } - public function addPeriodicTimer($interval, $callback) + private function addStream($stream, $listener, $flags) { - $timer = new Timer($this, $interval, $callback, true); - $this->setupTimer($timer); + $listener = $this->wrapStreamListener($stream, $listener, $flags); + $event = new IOEvent($listener, $stream, $flags); + $this->loop->add($event); - return $timer; + if (($flags & IOEvent::READ) === $flags) { + $this->readEvents[(int) $stream] = $event; + } elseif (($flags & IOEvent::WRITE) === $flags) { + $this->writeEvents[(int) $stream] = $event; + } } - public function cancelTimer(TimerInterface $timer) + private function wrapStreamListener($stream, $listener, $flags) { - if (isset($this->timers[$timer])) { - $this->loop->remove($this->timers[$timer]); - $this->timers->detach($timer); + if (($flags & IOEvent::READ) === $flags) { + $removeCallback = array($this, 'removeReadStream'); + } elseif (($flags & IOEvent::WRITE) === $flags) { + $removeCallback = array($this, 'removeWriteStream'); } + + return function ($event) use ($stream, $listener, $removeCallback) { + call_user_func($listener, $stream); + }; } private function setupTimer(TimerInterface $timer) @@ -124,9 +238,9 @@ private function setupTimer(TimerInterface $timer) $interval = $timer->getInterval(); if ($timer->isPeriodic()) { - $libevTimer = new \libev\TimerEvent($dummyCallback, $interval, $interval); + $libevTimer = new TimerEvent($dummyCallback, $interval, $interval); } else { - $libevTimer = new \libev\TimerEvent($dummyCallback, $interval); + $libevTimer = new TimerEvent($dummyCallback, $interval); } $libevTimer->setCallback(function () use ($timer) { @@ -142,24 +256,4 @@ private function setupTimer(TimerInterface $timer) return $timer; } - - public function isTimerActive(TimerInterface $timer) - { - return $this->timers->contains($timer); - } - - public function tick() - { - $this->loop->run(\libev\EventLoop::RUN_ONCE); - } - - public function run() - { - $this->loop->run(); - } - - public function stop() - { - $this->loop->breakLoop(); - } } From 17c378f2b0810d20007ce6110f9d5805bf2331a9 Mon Sep 17 00:00:00 2001 From: James Harris Date: Thu, 21 Nov 2013 14:50:45 +1000 Subject: [PATCH 16/24] Use 'inheritdoc' for methods from LoopInterface. --- src/React/EventLoop/ExtEventLoop.php | 67 +++++------------------ src/React/EventLoop/LibEvLoop.php | 65 +++++----------------- src/React/EventLoop/LibEventLoop.php | 68 +++++------------------- src/React/EventLoop/StreamSelectLoop.php | 65 +++++----------------- 4 files changed, 52 insertions(+), 213 deletions(-) diff --git a/src/React/EventLoop/ExtEventLoop.php b/src/React/EventLoop/ExtEventLoop.php index 86559341..4d766dbd 100644 --- a/src/React/EventLoop/ExtEventLoop.php +++ b/src/React/EventLoop/ExtEventLoop.php @@ -36,10 +36,7 @@ public function __construct() } /** - * Register a listener to be notified when a stream is ready to read. - * - * @param stream $stream The PHP stream resource to check. - * @param callable $listener Invoked when the stream is ready. + * {@inheritdoc} */ public function addReadStream($stream, $listener) { @@ -52,10 +49,7 @@ public function addReadStream($stream, $listener) } /** - * Register a listener to be notified when a stream is ready to write. - * - * @param stream $stream The PHP stream resource to check. - * @param callable $listener Invoked when the stream is ready. + * {@inheritdoc} */ public function addWriteStream($stream, $listener) { @@ -68,9 +62,7 @@ public function addWriteStream($stream, $listener) } /** - * Remove the read event listener for the given stream. - * - * @param stream $stream The PHP stream resource. + * {@inheritdoc} */ public function removeReadStream($stream) { @@ -83,9 +75,7 @@ public function removeReadStream($stream) } /** - * Remove the write event listener for the given stream. - * - * @param stream $stream The PHP stream resource. + * {@inheritdoc} */ public function removeWriteStream($stream) { @@ -98,9 +88,7 @@ public function removeWriteStream($stream) } /** - * Remove all listeners for the given stream. - * - * @param stream $stream The PHP stream resource. + * {@inheritdoc} */ public function removeStream($stream) { @@ -119,15 +107,7 @@ public function removeStream($stream) } /** - * Enqueue a callback to be invoked once after the given interval. - * - * The execution order of timers scheduled to execute at the same time is - * not guaranteed. - * - * @param numeric $interval The number of seconds to wait before execution. - * @param callable $callback The callback to invoke. - * - * @return TimerInterface + * {@inheritdoc} */ public function addTimer($interval, $callback) { @@ -139,15 +119,7 @@ public function addTimer($interval, $callback) } /** - * Enqueue a callback to be invoked repeatedly after the given interval. - * - * The execution order of timers scheduled to execute at the same time is - * not guaranteed. - * - * @param numeric $interval The number of seconds to wait before execution. - * @param callable $callback The callback to invoke. - * - * @return TimerInterface + * {@inheritdoc} */ public function addPeriodicTimer($interval, $callback) { @@ -159,9 +131,7 @@ public function addPeriodicTimer($interval, $callback) } /** - * Cancel a pending timer. - * - * @param TimerInterface $timer The timer to cancel. + * {@inheritdoc} */ public function cancelTimer(TimerInterface $timer) { @@ -172,11 +142,7 @@ public function cancelTimer(TimerInterface $timer) } /** - * Check if a given timer is active. - * - * @param TimerInterface $timer The timer to check. - * - * @return boolean True if the timer is still enqueued for execution. + * {@inheritdoc} */ public function isTimerActive(TimerInterface $timer) { @@ -184,12 +150,7 @@ public function isTimerActive(TimerInterface $timer) } /** - * Schedule a callback to be invoked on the next tick of the event loop. - * - * Callbacks are guaranteed to be executed in the order they are enqueued, - * before any timer or stream events. - * - * @param callable $listener The callback to invoke. + * {@inheritdoc} */ public function nextTick(callable $listener) { @@ -197,9 +158,7 @@ public function nextTick(callable $listener) } /** - * Perform a single iteration of the event loop. - * - * @param boolean $blocking True if loop should block waiting for next event. + * {@inheritdoc} */ public function tick() { @@ -209,7 +168,7 @@ public function tick() } /** - * Run the event loop until there are no more tasks to perform. + * {@inheritdoc} */ public function run() { @@ -228,7 +187,7 @@ public function run() } /** - * Instruct a running event loop to stop. + * {@inheritdoc} */ public function stop() { diff --git a/src/React/EventLoop/LibEvLoop.php b/src/React/EventLoop/LibEvLoop.php index 05f1a925..521e1641 100644 --- a/src/React/EventLoop/LibEvLoop.php +++ b/src/React/EventLoop/LibEvLoop.php @@ -31,10 +31,7 @@ public function __construct() } /** - * Register a listener to be notified when a stream is ready to read. - * - * @param stream $stream The PHP stream resource to check. - * @param callable $listener Invoked when the stream is ready. + * {@inheritdoc} */ public function addReadStream($stream, $listener) { @@ -42,10 +39,7 @@ public function addReadStream($stream, $listener) } /** - * Register a listener to be notified when a stream is ready to write. - * - * @param stream $stream The PHP stream resource to check. - * @param callable $listener Invoked when the stream is ready. + * {@inheritdoc} */ public function addWriteStream($stream, $listener) { @@ -53,9 +47,7 @@ public function addWriteStream($stream, $listener) } /** - * Remove the read event listener for the given stream. - * - * @param stream $stream The PHP stream resource. + * {@inheritdoc} */ public function removeReadStream($stream) { @@ -66,9 +58,7 @@ public function removeReadStream($stream) } /** - * Remove the write event listener for the given stream. - * - * @param stream $stream The PHP stream resource. + * {@inheritdoc} */ public function removeWriteStream($stream) { @@ -79,9 +69,7 @@ public function removeWriteStream($stream) } /** - * Remove all listeners for the given stream. - * - * @param stream $stream The PHP stream resource. + * {@inheritdoc} */ public function removeStream($stream) { @@ -90,15 +78,7 @@ public function removeStream($stream) } /** - * Enqueue a callback to be invoked once after the given interval. - * - * The execution order of timers scheduled to execute at the same time is - * not guaranteed. - * - * @param numeric $interval The number of seconds to wait before execution. - * @param callable $callback The callback to invoke. - * - * @return TimerInterface + * {@inheritdoc} */ public function addTimer($interval, $callback) { @@ -109,15 +89,7 @@ public function addTimer($interval, $callback) } /** - * Enqueue a callback to be invoked repeatedly after the given interval. - * - * The execution order of timers scheduled to execute at the same time is - * not guaranteed. - * - * @param numeric $interval The number of seconds to wait before execution. - * @param callable $callback The callback to invoke. - * - * @return TimerInterface + * {@inheritdoc} */ public function addPeriodicTimer($interval, $callback) { @@ -128,9 +100,7 @@ public function addPeriodicTimer($interval, $callback) } /** - * Cancel a pending timer. - * - * @param TimerInterface $timer The timer to cancel. + * {@inheritdoc} */ public function cancelTimer(TimerInterface $timer) { @@ -141,11 +111,7 @@ public function cancelTimer(TimerInterface $timer) } /** - * Check if a given timer is active. - * - * @param TimerInterface $timer The timer to check. - * - * @return boolean True if the timer is still enqueued for execution. + * {@inheritdoc} */ public function isTimerActive(TimerInterface $timer) { @@ -153,12 +119,7 @@ public function isTimerActive(TimerInterface $timer) } /** - * Schedule a callback to be invoked on the next tick of the event loop. - * - * Callbacks are guaranteed to be executed in the order they are enqueued, - * before any timer or stream events. - * - * @param callable $listner The callback to invoke. + * {@inheritdoc} */ public function nextTick(callable $listener) { @@ -166,7 +127,7 @@ public function nextTick(callable $listener) } /** - * Perform a single iteration of the event loop. + * {@inheritdoc} */ public function tick() { @@ -176,7 +137,7 @@ public function tick() } /** - * Run the event loop until there are no more tasks to perform. + * {@inheritdoc} */ public function run() { @@ -199,7 +160,7 @@ public function run() } /** - * Instruct a running event loop to stop. + * {@inheritdoc} */ public function stop() { diff --git a/src/React/EventLoop/LibEventLoop.php b/src/React/EventLoop/LibEventLoop.php index 62b4f656..a2be8037 100644 --- a/src/React/EventLoop/LibEventLoop.php +++ b/src/React/EventLoop/LibEventLoop.php @@ -38,10 +38,7 @@ public function __construct() } /** - * Register a listener to be notified when a stream is ready to read. - * - * @param stream $stream The PHP stream resource to check. - * @param callable $listener Invoked when the stream is ready. + * {@inheritdoc} */ public function addReadStream($stream, $listener) { @@ -54,10 +51,7 @@ public function addReadStream($stream, $listener) } /** - * Register a listener to be notified when a stream is ready to write. - * - * @param stream $stream The PHP stream resource to check. - * @param callable $listener Invoked when the stream is ready. + * {@inheritdoc} */ public function addWriteStream($stream, $listener) { @@ -70,9 +64,7 @@ public function addWriteStream($stream, $listener) } /** - * Remove the read event listener for the given stream. - * - * @param stream $stream The PHP stream resource. + * {@inheritdoc} */ public function removeReadStream($stream) { @@ -85,9 +77,7 @@ public function removeReadStream($stream) } /** - * Remove the write event listener for the given stream. - * - * @param stream $stream The PHP stream resource. + * {@inheritdoc} */ public function removeWriteStream($stream) { @@ -100,9 +90,7 @@ public function removeWriteStream($stream) } /** - * Remove all listeners for the given stream. - * - * @param stream $stream The PHP stream resource. + * {@inheritdoc} */ public function removeStream($stream) { @@ -124,15 +112,7 @@ public function removeStream($stream) } /** - * Enqueue a callback to be invoked once after the given interval. - * - * The execution order of timers scheduled to execute at the same time is - * not guaranteed. - * - * @param numeric $interval The number of seconds to wait before execution. - * @param callable $callback The callback to invoke. - * - * @return TimerInterface + * {@inheritdoc} */ public function addTimer($interval, $callback) { @@ -144,15 +124,7 @@ public function addTimer($interval, $callback) } /** - * Enqueue a callback to be invoked repeatedly after the given interval. - * - * The execution order of timers scheduled to execute at the same time is - * not guaranteed. - * - * @param numeric $interval The number of seconds to wait before execution. - * @param callable $callback The callback to invoke. - * - * @return TimerInterface + * {@inheritdoc} */ public function addPeriodicTimer($interval, $callback) { @@ -164,9 +136,7 @@ public function addPeriodicTimer($interval, $callback) } /** - * Cancel a pending timer. - * - * @param TimerInterface $timer The timer to cancel. + * {@inheritdoc} */ public function cancelTimer(TimerInterface $timer) { @@ -181,11 +151,7 @@ public function cancelTimer(TimerInterface $timer) } /** - * Check if a given timer is active. - * - * @param TimerInterface $timer The timer to check. - * - * @return boolean True if the timer is still enqueued for execution. + * {@inheritdoc} */ public function isTimerActive(TimerInterface $timer) { @@ -193,12 +159,7 @@ public function isTimerActive(TimerInterface $timer) } /** - * Schedule a callback to be invoked on the next tick of the event loop. - * - * Callbacks are guaranteed to be executed in the order they are enqueued, - * before any timer or stream events. - * - * @param callable $listener The callback to invoke. + * {@inheritdoc} */ public function nextTick(callable $listener) { @@ -206,9 +167,7 @@ public function nextTick(callable $listener) } /** - * Perform a single iteration of the event loop. - * - * @param boolean $blocking True if loop should block waiting for next event. + * {@inheritdoc} */ public function tick() { @@ -218,7 +177,7 @@ public function tick() } /** - * Run the event loop until there are no more tasks to perform. + * {@inheritdoc} */ public function run() { @@ -237,7 +196,7 @@ public function run() } /** - * Instruct a running event loop to stop. + * {@inheritdoc} */ public function stop() { @@ -297,7 +256,6 @@ protected function subscribeStreamEvent($stream, $flag) event_base_set($event, $this->eventBase); } - event_add($event); } diff --git a/src/React/EventLoop/StreamSelectLoop.php b/src/React/EventLoop/StreamSelectLoop.php index 6ce59e98..c45bb020 100644 --- a/src/React/EventLoop/StreamSelectLoop.php +++ b/src/React/EventLoop/StreamSelectLoop.php @@ -27,10 +27,7 @@ public function __construct() } /** - * Register a listener to be notified when a stream is ready to read. - * - * @param stream $stream The PHP stream resource to check. - * @param callable $listener Invoked when the stream is ready. + * {@inheritdoc} */ public function addReadStream($stream, $listener) { @@ -43,10 +40,7 @@ public function addReadStream($stream, $listener) } /** - * Register a listener to be notified when a stream is ready to write. - * - * @param stream $stream The PHP stream resource to check. - * @param callable $listener Invoked when the stream is ready. + * {@inheritdoc} */ public function addWriteStream($stream, $listener) { @@ -59,9 +53,7 @@ public function addWriteStream($stream, $listener) } /** - * Remove the read event listener for the given stream. - * - * @param stream $stream The PHP stream resource. + * {@inheritdoc} */ public function removeReadStream($stream) { @@ -74,9 +66,7 @@ public function removeReadStream($stream) } /** - * Remove the write event listener for the given stream. - * - * @param stream $stream The PHP stream resource. + * {@inheritdoc} */ public function removeWriteStream($stream) { @@ -89,9 +79,7 @@ public function removeWriteStream($stream) } /** - * Remove all listeners for the given stream. - * - * @param stream $stream The PHP stream resource. + * {@inheritdoc} */ public function removeStream($stream) { @@ -100,15 +88,7 @@ public function removeStream($stream) } /** - * Enqueue a callback to be invoked once after the given interval. - * - * The execution order of timers scheduled to execute at the same time is - * not guaranteed. - * - * @param numeric $interval The number of seconds to wait before execution. - * @param callable $callback The callback to invoke. - * - * @return TimerInterface + * {@inheritdoc} */ public function addTimer($interval, $callback) { @@ -120,15 +100,7 @@ public function addTimer($interval, $callback) } /** - * Enqueue a callback to be invoked repeatedly after the given interval. - * - * The execution order of timers scheduled to execute at the same time is - * not guaranteed. - * - * @param numeric $interval The number of seconds to wait before execution. - * @param callable $callback The callback to invoke. - * - * @return TimerInterface + * {@inheritdoc} */ public function addPeriodicTimer($interval, $callback) { @@ -140,9 +112,7 @@ public function addPeriodicTimer($interval, $callback) } /** - * Cancel a pending timer. - * - * @param TimerInterface $timer The timer to cancel. + * {@inheritdoc} */ public function cancelTimer(TimerInterface $timer) { @@ -150,11 +120,7 @@ public function cancelTimer(TimerInterface $timer) } /** - * Check if a given timer is active. - * - * @param TimerInterface $timer The timer to check. - * - * @return boolean True if the timer is still enqueued for execution. + * {@inheritdoc} */ public function isTimerActive(TimerInterface $timer) { @@ -162,12 +128,7 @@ public function isTimerActive(TimerInterface $timer) } /** - * Schedule a callback to be invoked on the next tick of the event loop. - * - * Callbacks are guaranteed to be executed in the order they are enqueued, - * before any timer or stream events. - * - * @param callable $listener The callback to invoke. + * {@inheritdoc} */ public function nextTick(callable $listener) { @@ -175,7 +136,7 @@ public function nextTick(callable $listener) } /** - * Perform a single iteration of the event loop. + * {@inheritdoc} */ public function tick() { @@ -187,7 +148,7 @@ public function tick() } /** - * Run the event loop until there are no more tasks to perform. + * {@inheritdoc} */ public function run() { @@ -224,7 +185,7 @@ public function run() } /** - * Instruct a running event loop to stop. + * {@inheritdoc} */ public function stop() { From 7d7dd65935d7a038c72abce8f17d7128d59ce354 Mon Sep 17 00:00:00 2001 From: James Harris Date: Thu, 21 Nov 2013 14:53:32 +1000 Subject: [PATCH 17/24] Code style. --- src/React/EventLoop/ExtEventLoop.php | 57 +++++-------------- src/React/EventLoop/LibEvLoop.php | 4 +- src/React/EventLoop/LibEventLoop.php | 50 ++++------------ src/React/EventLoop/StreamSelectLoop.php | 16 ++---- src/React/EventLoop/Tick/NextTickQueue.php | 2 +- .../Tests/Dns/Query/RetryExecutorTest.php | 2 +- .../Tests/EventLoop/ExtEventLoopTest.php | 2 +- .../Tests/EventLoop/LibEventLoopTest.php | 2 +- .../Tests/EventLoop/StreamSelectLoopTest.php | 2 +- 9 files changed, 36 insertions(+), 101 deletions(-) diff --git a/src/React/EventLoop/ExtEventLoop.php b/src/React/EventLoop/ExtEventLoop.php index 4d766dbd..9ec48637 100644 --- a/src/React/EventLoop/ExtEventLoop.php +++ b/src/React/EventLoop/ExtEventLoop.php @@ -27,9 +27,9 @@ class ExtEventLoop implements LoopInterface public function __construct() { - $this->eventBase = new EventBase; + $this->eventBase = new EventBase(); $this->nextTickQueue = new NextTickQueue($this); - $this->timerEvents = new SplObjectStorage; + $this->timerEvents = new SplObjectStorage(); $this->createTimerCallback(); $this->createStreamCallback(); @@ -175,7 +175,6 @@ public function run() $this->running = true; while ($this->running) { - $this->nextTickQueue->tick(); if (!$this->streamEvents && !$this->timerEvents->count()) { @@ -207,13 +206,8 @@ protected function scheduleTimer(TimerInterface $timer) $flags |= Event::PERSIST; } - $this->timerEvents[$timer] = $event = new Event( - $this->eventBase, - -1, - $flags, - $this->timerCallback, - $timer - ); + $event = new Event($this->eventBase, -1, $flags, $this->timerCallback, $timer); + $this->timerEvents[$timer] = $event; $event->add($timer->getInterval()); } @@ -230,22 +224,15 @@ protected function subscribeStreamEvent($stream, $flag) if (isset($this->streamEvents[$key])) { $event = $this->streamEvents[$key]; + $flags = ($this->streamFlags[$key] |= $flag); $event->del(); - - $event->set( - $this->eventBase, - $stream, - Event::PERSIST | ($this->streamFlags[$key] |= $flag), - $this->streamCallback - ); + $event->set($this->eventBase, $stream, Event::PERSIST | $flags, $this->streamCallback); } else { - $this->streamEvents[$key] = $event = new Event( - $this->eventBase, - $stream, - Event::PERSIST | ($this->streamFlags[$key] = $flag), - $this->streamCallback - ); + $event = new Event($this->eventBase, $stream, Event::PERSIST | $flag, $this->streamCallback); + + $this->streamEvents[$key] = $event; + $this->streamFlags[$key] = $flag; } $event->add(); @@ -273,14 +260,7 @@ protected function unsubscribeStreamEvent($stream, $flag) $event = $this->streamEvents[$key]; $event->del(); - - $event->set( - $this->eventBase, - $stream, - Event::PERSIST | $flags, - $this->streamCallback - ); - + $event->set($this->eventBase, $stream, Event::PERSIST | $flags, $this->streamCallback); $event->add(); } @@ -294,14 +274,11 @@ protected function unsubscribeStreamEvent($stream, $flag) protected function createTimerCallback() { $this->timerCallback = function ($_, $_, $timer) { - call_user_func($timer->getCallback(), $timer); - // Clean-up one shot timers ... if (!$timer->isPeriodic() && $this->isTimerActive($timer)) { $this->cancelTimer($timer); } - }; } @@ -315,23 +292,15 @@ protected function createTimerCallback() protected function createStreamCallback() { $this->streamCallback = function ($stream, $flags) { - $key = (int) $stream; - if ( - Event::READ === (Event::READ & $flags) - && isset($this->readListeners[$key]) - ) { + if (Event::READ === (Event::READ & $flags) && isset($this->readListeners[$key])) { call_user_func($this->readListeners[$key], $stream, $this); } - if ( - Event::WRITE === (Event::WRITE & $flags) - && isset($this->writeListeners[$key]) - ) { + if (Event::WRITE === (Event::WRITE & $flags) && isset($this->writeListeners[$key])) { call_user_func($this->writeListeners[$key], $stream, $this); } - }; } } diff --git a/src/React/EventLoop/LibEvLoop.php b/src/React/EventLoop/LibEvLoop.php index 521e1641..3aec4802 100644 --- a/src/React/EventLoop/LibEvLoop.php +++ b/src/React/EventLoop/LibEvLoop.php @@ -25,9 +25,9 @@ class LibEvLoop implements LoopInterface public function __construct() { - $this->loop = new EventLoop; + $this->loop = new EventLoop(); $this->nextTickQueue = new NextTickQueue($this); - $this->timers = new SplObjectStorage; + $this->timers = new SplObjectStorage(); } /** diff --git a/src/React/EventLoop/LibEventLoop.php b/src/React/EventLoop/LibEventLoop.php index a2be8037..4a6b5fad 100644 --- a/src/React/EventLoop/LibEventLoop.php +++ b/src/React/EventLoop/LibEventLoop.php @@ -31,7 +31,7 @@ public function __construct() { $this->eventBase = event_base_new(); $this->nextTickQueue = new NextTickQueue($this); - $this->timerEvents = new SplObjectStorage; + $this->timerEvents = new SplObjectStorage(); $this->createTimerCallback(); $this->createStreamCallback(); @@ -97,8 +97,8 @@ public function removeStream($stream) $key = (int) $stream; if (isset($this->streamEvents[$key])) { - $event = $this->streamEvents[$key]; + event_del($event); event_free($event); @@ -184,7 +184,6 @@ public function run() $this->running = true; while ($this->running) { - $this->nextTickQueue->tick(); if (!$this->streamEvents && !$this->timerEvents->count()) { @@ -213,13 +212,8 @@ protected function scheduleTimer(TimerInterface $timer) $this->timerEvents[$timer] = $event = event_timer_new(); event_timer_set($event, $this->timerCallback, $timer); - event_base_set($event, $this->eventBase); - - event_add( - $event, - $timer->getInterval() * self::MICROSECONDS_PER_SECOND - ); + event_add($event, $timer->getInterval() * self::MICROSECONDS_PER_SECOND); } /** @@ -234,26 +228,18 @@ protected function subscribeStreamEvent($stream, $flag) if (isset($this->streamEvents[$key])) { $event = $this->streamEvents[$key]; + $flags = $this->streamFlags[$key] |= $flag; event_del($event); - - event_set( - $event, - $stream, - EV_PERSIST | ($this->streamFlags[$key] |= $flag), - $this->streamCallback - ); + event_set($event, $stream, EV_PERSIST | $flags, $this->streamCallback); } else { - $this->streamEvents[$key] = $event = event_new(); - - event_set( - $event, - $stream, - EV_PERSIST | ($this->streamFlags[$key] = $flag), - $this->streamCallback - ); + $event = event_new(); + event_set($event, $stream, EV_PERSIST | $flag, $this->streamCallback); event_base_set($event, $this->eventBase); + + $this->streamEvents[$key] = $event; + $this->streamFlags[$key] = $flag; } event_add($event); @@ -281,9 +267,7 @@ protected function unsubscribeStreamEvent($stream, $flag) $event = $this->streamEvents[$key]; event_del($event); - event_set($event, $stream, EV_PERSIST | $flags, $this->streamCallback); - event_add($event); } @@ -297,7 +281,6 @@ protected function unsubscribeStreamEvent($stream, $flag) protected function createTimerCallback() { $this->timerCallback = function ($_, $_, $timer) { - call_user_func($timer->getCallback(), $timer); // Timer already cancelled ... @@ -315,7 +298,6 @@ protected function createTimerCallback() } else { $this->cancelTimer($timer); } - }; } @@ -329,23 +311,15 @@ protected function createTimerCallback() protected function createStreamCallback() { $this->streamCallback = function ($stream, $flags) { - $key = (int) $stream; - if ( - EV_READ === (EV_READ & $flags) - && isset($this->readListeners[$key]) - ) { + if (EV_READ === (EV_READ & $flags) && isset($this->readListeners[$key])) { call_user_func($this->readListeners[$key], $stream, $this); } - if ( - EV_WRITE === (EV_WRITE & $flags) - && isset($this->writeListeners[$key]) - ) { + if (EV_WRITE === (EV_WRITE & $flags) && isset($this->writeListeners[$key])) { call_user_func($this->writeListeners[$key], $stream, $this); } - }; } } diff --git a/src/React/EventLoop/StreamSelectLoop.php b/src/React/EventLoop/StreamSelectLoop.php index c45bb020..940404e8 100644 --- a/src/React/EventLoop/StreamSelectLoop.php +++ b/src/React/EventLoop/StreamSelectLoop.php @@ -23,7 +23,7 @@ class StreamSelectLoop implements LoopInterface public function __construct() { $this->nextTickQueue = new NextTickQueue($this); - $this->timers = new Timers; + $this->timers = new Timers(); } /** @@ -155,7 +155,6 @@ public function run() $this->running = true; while ($this->running) { - $this->nextTickQueue->tick(); $this->timers->tick(); @@ -166,7 +165,6 @@ public function run() // There is a pending timer, only block until it is due ... } elseif ($scheduledAt = $this->timers->getFirst()) { - if (0 > $timeout = $scheduledAt - $this->timers->getTime()) { $timeout = 0; } @@ -202,7 +200,6 @@ protected function waitForStreamActivity($timeout) $this->streamSelect($read, $write, $timeout); - // Invoke callbacks for read-ready streams ... foreach ($read as $stream) { $key = (int) $stream; @@ -211,7 +208,6 @@ protected function waitForStreamActivity($timeout) } } - // Invoke callbacks for write-ready streams ... foreach ($write as $stream) { $key = (int) $stream; @@ -228,19 +224,15 @@ protected function waitForStreamActivity($timeout) * @param array &$read An array of read streams to select upon. * @param array &$write An array of write streams to select upon. * @param integer|null $timeout Activity timeout in microseconds, or null to wait forever. + * + * @return integer The total number of streams that are ready for read/write. */ protected function streamSelect(array &$read, array &$write, $timeout) { if ($read || $write) { $except = null; - return stream_select( - $read, - $write, - $except, - $timeout === null ? null : 0, - $timeout - ); + return stream_select($read, $write, $except, $timeout === null ? null : 0, $timeout); } usleep($timeout); diff --git a/src/React/EventLoop/Tick/NextTickQueue.php b/src/React/EventLoop/Tick/NextTickQueue.php index 85a5abdf..b77b84c5 100644 --- a/src/React/EventLoop/Tick/NextTickQueue.php +++ b/src/React/EventLoop/Tick/NextTickQueue.php @@ -16,7 +16,7 @@ class NextTickQueue public function __construct(LoopInterface $eventLoop) { $this->eventLoop = $eventLoop; - $this->queue = new SplQueue; + $this->queue = new SplQueue(); } /** diff --git a/tests/React/Tests/Dns/Query/RetryExecutorTest.php b/tests/React/Tests/Dns/Query/RetryExecutorTest.php index 4bf7e23f..fd3d02e6 100644 --- a/tests/React/Tests/Dns/Query/RetryExecutorTest.php +++ b/tests/React/Tests/Dns/Query/RetryExecutorTest.php @@ -172,7 +172,7 @@ protected function createPromiseMock() protected function createStandardResponse() { - $response = new Message; + $response = new Message(); $response->header->set('qr', 1); $response->questions[] = new Record('igor.io', Message::TYPE_A, Message::CLASS_IN); $response->answers[] = new Record('igor.io', Message::TYPE_A, Message::CLASS_IN, 3600, '178.79.169.131'); diff --git a/tests/React/Tests/EventLoop/ExtEventLoopTest.php b/tests/React/Tests/EventLoop/ExtEventLoopTest.php index 829b6905..a9570b40 100644 --- a/tests/React/Tests/EventLoop/ExtEventLoopTest.php +++ b/tests/React/Tests/EventLoop/ExtEventLoopTest.php @@ -16,7 +16,7 @@ public function createLoop() $this->markTestSkipped('ext-event tests skipped because ext-event is not installed.'); } - return new ExtEventLoop; + return new ExtEventLoop(); } public function createStream() diff --git a/tests/React/Tests/EventLoop/LibEventLoopTest.php b/tests/React/Tests/EventLoop/LibEventLoopTest.php index 2505bb3b..920b33cc 100644 --- a/tests/React/Tests/EventLoop/LibEventLoopTest.php +++ b/tests/React/Tests/EventLoop/LibEventLoopTest.php @@ -18,7 +18,7 @@ public function createLoop() $this->markTestSkipped('libevent tests skipped because ext-libevent is not installed.'); } - return new LibEventLoop; + return new LibEventLoop(); } public function tearDown() diff --git a/tests/React/Tests/EventLoop/StreamSelectLoopTest.php b/tests/React/Tests/EventLoop/StreamSelectLoopTest.php index 4cc9c546..55d3d165 100644 --- a/tests/React/Tests/EventLoop/StreamSelectLoopTest.php +++ b/tests/React/Tests/EventLoop/StreamSelectLoopTest.php @@ -8,7 +8,7 @@ class StreamSelectLoopTest extends AbstractLoopTest { public function createLoop() { - return new StreamSelectLoop; + return new StreamSelectLoop(); } public function testStreamSelectTimeoutEmulation() From a8fe3cf7c53d6c2a6825e524e09076217778180e Mon Sep 17 00:00:00 2001 From: James Harris Date: Thu, 21 Nov 2013 15:22:28 +1000 Subject: [PATCH 18/24] Added callable type-hint to LoopInterface, use SplQueue::isEmpty(). --- src/React/EventLoop/ExtEventLoop.php | 8 ++++---- src/React/EventLoop/LibEvLoop.php | 8 ++++---- src/React/EventLoop/LibEventLoop.php | 8 ++++---- src/React/EventLoop/LoopInterface.php | 8 ++++---- src/React/EventLoop/StreamSelectLoop.php | 8 ++++---- src/React/EventLoop/Tick/NextTickQueue.php | 4 ++-- 6 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/React/EventLoop/ExtEventLoop.php b/src/React/EventLoop/ExtEventLoop.php index 9ec48637..408c41c8 100644 --- a/src/React/EventLoop/ExtEventLoop.php +++ b/src/React/EventLoop/ExtEventLoop.php @@ -38,7 +38,7 @@ public function __construct() /** * {@inheritdoc} */ - public function addReadStream($stream, $listener) + public function addReadStream($stream, callable $listener) { $key = (int) $stream; @@ -51,7 +51,7 @@ public function addReadStream($stream, $listener) /** * {@inheritdoc} */ - public function addWriteStream($stream, $listener) + public function addWriteStream($stream, callable $listener) { $key = (int) $stream; @@ -109,7 +109,7 @@ public function removeStream($stream) /** * {@inheritdoc} */ - public function addTimer($interval, $callback) + public function addTimer($interval, callable $callback) { $timer = new Timer($this, $interval, $callback, false); @@ -121,7 +121,7 @@ public function addTimer($interval, $callback) /** * {@inheritdoc} */ - public function addPeriodicTimer($interval, $callback) + public function addPeriodicTimer($interval, callable $callback) { $timer = new Timer($this, $interval, $callback, true); diff --git a/src/React/EventLoop/LibEvLoop.php b/src/React/EventLoop/LibEvLoop.php index 3aec4802..f18ae87d 100644 --- a/src/React/EventLoop/LibEvLoop.php +++ b/src/React/EventLoop/LibEvLoop.php @@ -33,7 +33,7 @@ public function __construct() /** * {@inheritdoc} */ - public function addReadStream($stream, $listener) + public function addReadStream($stream, callable $listener) { $this->addStream($stream, $listener, IOEvent::READ); } @@ -41,7 +41,7 @@ public function addReadStream($stream, $listener) /** * {@inheritdoc} */ - public function addWriteStream($stream, $listener) + public function addWriteStream($stream, callable $listener) { $this->addStream($stream, $listener, IOEvent::WRITE); } @@ -80,7 +80,7 @@ public function removeStream($stream) /** * {@inheritdoc} */ - public function addTimer($interval, $callback) + public function addTimer($interval, callable $callback) { $timer = new Timer($this, $interval, $callback, false); $this->setupTimer($timer); @@ -91,7 +91,7 @@ public function addTimer($interval, $callback) /** * {@inheritdoc} */ - public function addPeriodicTimer($interval, $callback) + public function addPeriodicTimer($interval, callable $callback) { $timer = new Timer($this, $interval, $callback, true); $this->setupTimer($timer); diff --git a/src/React/EventLoop/LibEventLoop.php b/src/React/EventLoop/LibEventLoop.php index 4a6b5fad..006c64fa 100644 --- a/src/React/EventLoop/LibEventLoop.php +++ b/src/React/EventLoop/LibEventLoop.php @@ -40,7 +40,7 @@ public function __construct() /** * {@inheritdoc} */ - public function addReadStream($stream, $listener) + public function addReadStream($stream, callable $listener) { $key = (int) $stream; @@ -53,7 +53,7 @@ public function addReadStream($stream, $listener) /** * {@inheritdoc} */ - public function addWriteStream($stream, $listener) + public function addWriteStream($stream, callable $listener) { $key = (int) $stream; @@ -114,7 +114,7 @@ public function removeStream($stream) /** * {@inheritdoc} */ - public function addTimer($interval, $callback) + public function addTimer($interval, callable $callback) { $timer = new Timer($this, $interval, $callback, false); @@ -126,7 +126,7 @@ public function addTimer($interval, $callback) /** * {@inheritdoc} */ - public function addPeriodicTimer($interval, $callback) + public function addPeriodicTimer($interval, callable $callback) { $timer = new Timer($this, $interval, $callback, true); diff --git a/src/React/EventLoop/LoopInterface.php b/src/React/EventLoop/LoopInterface.php index 4fcb7979..59334dc1 100644 --- a/src/React/EventLoop/LoopInterface.php +++ b/src/React/EventLoop/LoopInterface.php @@ -12,7 +12,7 @@ interface LoopInterface * @param stream $stream The PHP stream resource to check. * @param callable $listener Invoked when the stream is ready. */ - public function addReadStream($stream, $listener); + public function addReadStream($stream, callable $listener); /** * Register a listener to be notified when a stream is ready to write. @@ -20,7 +20,7 @@ public function addReadStream($stream, $listener); * @param stream $stream The PHP stream resource to check. * @param callable $listener Invoked when the stream is ready. */ - public function addWriteStream($stream, $listener); + public function addWriteStream($stream, callable $listener); /** * Remove the read event listener for the given stream. @@ -54,7 +54,7 @@ public function removeStream($stream); * * @return TimerInterface */ - public function addTimer($interval, $callback); + public function addTimer($interval, callable $callback); /** * Enqueue a callback to be invoked repeatedly after the given interval. @@ -67,7 +67,7 @@ public function addTimer($interval, $callback); * * @return TimerInterface */ - public function addPeriodicTimer($interval, $callback); + public function addPeriodicTimer($interval, callable $callback); /** * Cancel a pending timer. diff --git a/src/React/EventLoop/StreamSelectLoop.php b/src/React/EventLoop/StreamSelectLoop.php index 940404e8..1928fda9 100644 --- a/src/React/EventLoop/StreamSelectLoop.php +++ b/src/React/EventLoop/StreamSelectLoop.php @@ -29,7 +29,7 @@ public function __construct() /** * {@inheritdoc} */ - public function addReadStream($stream, $listener) + public function addReadStream($stream, callable $listener) { $key = (int) $stream; @@ -42,7 +42,7 @@ public function addReadStream($stream, $listener) /** * {@inheritdoc} */ - public function addWriteStream($stream, $listener) + public function addWriteStream($stream, callable $listener) { $key = (int) $stream; @@ -90,7 +90,7 @@ public function removeStream($stream) /** * {@inheritdoc} */ - public function addTimer($interval, $callback) + public function addTimer($interval, callable $callback) { $timer = new Timer($this, $interval, $callback, false); @@ -102,7 +102,7 @@ public function addTimer($interval, $callback) /** * {@inheritdoc} */ - public function addPeriodicTimer($interval, $callback) + public function addPeriodicTimer($interval, callable $callback) { $timer = new Timer($this, $interval, $callback, true); diff --git a/src/React/EventLoop/Tick/NextTickQueue.php b/src/React/EventLoop/Tick/NextTickQueue.php index b77b84c5..5b8e1de8 100644 --- a/src/React/EventLoop/Tick/NextTickQueue.php +++ b/src/React/EventLoop/Tick/NextTickQueue.php @@ -37,7 +37,7 @@ public function add(callable $listener) */ public function tick() { - while ($this->queue->count()) { + while (!$this->queue->isEmpty()) { call_user_func( $this->queue->dequeue(), $this->eventLoop @@ -52,6 +52,6 @@ public function tick() */ public function isEmpty() { - return 0 === $this->queue->count(); + return $this->queue->isEmpty(); } } From 0ac0305d6010e92832d5579bc04f8d836cb832a7 Mon Sep 17 00:00:00 2001 From: James Harris Date: Thu, 21 Nov 2013 16:53:21 +1000 Subject: [PATCH 19/24] Simplification of LibEvLoop. --- src/React/EventLoop/LibEvLoop.php | 129 +++++++++++++----------------- 1 file changed, 55 insertions(+), 74 deletions(-) diff --git a/src/React/EventLoop/LibEvLoop.php b/src/React/EventLoop/LibEvLoop.php index f18ae87d..5c4378e2 100644 --- a/src/React/EventLoop/LibEvLoop.php +++ b/src/React/EventLoop/LibEvLoop.php @@ -18,16 +18,16 @@ class LibEvLoop implements LoopInterface { private $loop; private $nextTickQueue; - private $timers; - private $readEvents = array(); - private $writeEvents = array(); + private $timerEvents; + private $readEvents = []; + private $writeEvents = []; private $running; public function __construct() { $this->loop = new EventLoop(); $this->nextTickQueue = new NextTickQueue($this); - $this->timers = new SplObjectStorage(); + $this->timerEvents = new SplObjectStorage(); } /** @@ -35,7 +35,14 @@ public function __construct() */ public function addReadStream($stream, callable $listener) { - $this->addStream($stream, $listener, IOEvent::READ); + $callback = function () use ($stream, $listener) { + call_user_func($listener, $stream, $this); + }; + + $event = new IOEvent($callback, $stream, IOEvent::READ); + $this->loop->add($event); + + $this->readEvents[(int) $stream] = $event; } /** @@ -43,7 +50,14 @@ public function addReadStream($stream, callable $listener) */ public function addWriteStream($stream, callable $listener) { - $this->addStream($stream, $listener, IOEvent::WRITE); + $callback = function () use ($stream, $listener) { + call_user_func($listener, $stream, $this); + }; + + $event = new IOEvent($callback, $stream, IOEvent::WRITE); + $this->loop->add($event); + + $this->writeEvents[(int) $stream] = $event; } /** @@ -51,9 +65,11 @@ public function addWriteStream($stream, callable $listener) */ public function removeReadStream($stream) { - if (isset($this->readEvents[(int) $stream])) { - $this->readEvents[(int) $stream]->stop(); - unset($this->readEvents[(int) $stream]); + $key = (int) $stream; + + if (isset($this->readEvents[$key])) { + $this->readEvents[$key]->stop(); + unset($this->readEvents[$key]); } } @@ -62,9 +78,11 @@ public function removeReadStream($stream) */ public function removeWriteStream($stream) { - if (isset($this->writeEvents[(int) $stream])) { - $this->writeEvents[(int) $stream]->stop(); - unset($this->writeEvents[(int) $stream]); + $key = (int) $stream; + + if (isset($this->writeEvents[$key])) { + $this->writeEvents[$key]->stop(); + unset($this->writeEvents[$key]); } } @@ -83,7 +101,18 @@ public function removeStream($stream) public function addTimer($interval, callable $callback) { $timer = new Timer($this, $interval, $callback, false); - $this->setupTimer($timer); + + $callback = function () use ($timer) { + call_user_func($timer->getCallback(), $timer); + + if ($this->isTimerActive($timer)) { + $this->cancelTimer($timer); + } + }; + + $event = new TimerEvent($callback, $timer->getInterval()); + $this->timerEvents->attach($timer, $event); + $this->loop->add($event); return $timer; } @@ -94,7 +123,14 @@ public function addTimer($interval, callable $callback) public function addPeriodicTimer($interval, callable $callback) { $timer = new Timer($this, $interval, $callback, true); - $this->setupTimer($timer); + + $callback = function () use ($timer) { + call_user_func($timer->getCallback(), $timer); + }; + + $event = new TimerEvent($callback, $interval, $interval); + $this->timerEvents->attach($timer, $event); + $this->loop->add($event); return $timer; } @@ -104,9 +140,9 @@ public function addPeriodicTimer($interval, callable $callback) */ public function cancelTimer(TimerInterface $timer) { - if (isset($this->timers[$timer])) { - $this->loop->remove($this->timers[$timer]); - $this->timers->detach($timer); + if (isset($this->timerEvents[$timer])) { + $this->loop->remove($this->timerEvents[$timer]); + $this->timerEvents->detach($timer); } } @@ -115,7 +151,7 @@ public function cancelTimer(TimerInterface $timer) */ public function isTimerActive(TimerInterface $timer) { - return $this->timers->contains($timer); + return $this->timerEvents->contains($timer); } /** @@ -147,11 +183,7 @@ public function run() $this->nextTickQueue->tick(); - if ( - !$this->readEvents - && !$this->writeEvents - && !$this->timers->count() - ) { + if (!$this->readEvents && !$this->writeEvents && !$this->timerEvents->count()) { break; } @@ -166,55 +198,4 @@ public function stop() { $this->running = false; } - - private function addStream($stream, $listener, $flags) - { - $listener = $this->wrapStreamListener($stream, $listener, $flags); - $event = new IOEvent($listener, $stream, $flags); - $this->loop->add($event); - - if (($flags & IOEvent::READ) === $flags) { - $this->readEvents[(int) $stream] = $event; - } elseif (($flags & IOEvent::WRITE) === $flags) { - $this->writeEvents[(int) $stream] = $event; - } - } - - private function wrapStreamListener($stream, $listener, $flags) - { - if (($flags & IOEvent::READ) === $flags) { - $removeCallback = array($this, 'removeReadStream'); - } elseif (($flags & IOEvent::WRITE) === $flags) { - $removeCallback = array($this, 'removeWriteStream'); - } - - return function ($event) use ($stream, $listener, $removeCallback) { - call_user_func($listener, $stream); - }; - } - - private function setupTimer(TimerInterface $timer) - { - $dummyCallback = function () {}; - $interval = $timer->getInterval(); - - if ($timer->isPeriodic()) { - $libevTimer = new TimerEvent($dummyCallback, $interval, $interval); - } else { - $libevTimer = new TimerEvent($dummyCallback, $interval); - } - - $libevTimer->setCallback(function () use ($timer) { - call_user_func($timer->getCallback(), $timer); - - if (!$timer->isPeriodic()) { - $timer->cancel(); - } - }); - - $this->timers->attach($timer, $libevTimer); - $this->loop->add($libevTimer); - - return $timer; - } } From 3ac92ee354f78fc18023864d302bae3d0c5ee6c4 Mon Sep 17 00:00:00 2001 From: James Harris Date: Fri, 22 Nov 2013 08:42:26 +1000 Subject: [PATCH 20/24] Added @-operator error suppression to EventBase::loop() calls. --- src/React/EventLoop/ExtEventLoop.php | 6 ++++-- src/React/EventLoop/LoopInterface.php | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/React/EventLoop/ExtEventLoop.php b/src/React/EventLoop/ExtEventLoop.php index 408c41c8..3069f496 100644 --- a/src/React/EventLoop/ExtEventLoop.php +++ b/src/React/EventLoop/ExtEventLoop.php @@ -164,7 +164,8 @@ public function tick() { $this->nextTickQueue->tick(); - $this->eventBase->loop(EventBase::LOOP_ONCE | EventBase::LOOP_NONBLOCK); + // @-suppression: https://github.com/reactphp/react/pull/234#discussion-diff-7759616R226 + @$this->eventBase->loop(EventBase::LOOP_ONCE | EventBase::LOOP_NONBLOCK); } /** @@ -181,7 +182,8 @@ public function run() break; } - $this->eventBase->loop(EventBase::LOOP_ONCE); + // @-suppression: https://github.com/reactphp/react/pull/234#discussion-diff-7759616R226 + @$this->eventBase->loop(EventBase::LOOP_ONCE); } } diff --git a/src/React/EventLoop/LoopInterface.php b/src/React/EventLoop/LoopInterface.php index 59334dc1..fc1650fc 100644 --- a/src/React/EventLoop/LoopInterface.php +++ b/src/React/EventLoop/LoopInterface.php @@ -91,7 +91,7 @@ public function isTimerActive(TimerInterface $timer); * Callbacks are guaranteed to be executed in the order they are enqueued, * before any timer or stream events. * - * @param callable $listner The callback to invoke. + * @param callable $listener The callback to invoke. */ public function nextTick(callable $listener); From 87d220707061fe8ed7d0e8e85f6c5ca1e9bd065a Mon Sep 17 00:00:00 2001 From: James Harris Date: Fri, 22 Nov 2013 08:56:02 +1000 Subject: [PATCH 21/24] Code style. --- src/React/EventLoop/LibEvLoop.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/React/EventLoop/LibEvLoop.php b/src/React/EventLoop/LibEvLoop.php index 5c4378e2..1c6298a1 100644 --- a/src/React/EventLoop/LibEvLoop.php +++ b/src/React/EventLoop/LibEvLoop.php @@ -180,7 +180,6 @@ public function run() $this->running = true; while ($this->running) { - $this->nextTickQueue->tick(); if (!$this->readEvents && !$this->writeEvents && !$this->timerEvents->count()) { From cae3165f09cd50a47623408b9c57c5550a149ff3 Mon Sep 17 00:00:00 2001 From: James Harris Date: Fri, 22 Nov 2013 19:47:34 +1000 Subject: [PATCH 22/24] =?UTF-8?q?Add=201=C2=B5s=20minimum=20interval=20to?= =?UTF-8?q?=20Timer,=20and=20remove=20Timers::MIN=5FRESOLUTION.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/React/EventLoop/Timer/Timer.php | 9 +++++---- src/React/EventLoop/Timer/Timers.php | 8 -------- tests/React/Tests/EventLoop/Timer/AbstractTimerTest.php | 9 +++++++++ 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/React/EventLoop/Timer/Timer.php b/src/React/EventLoop/Timer/Timer.php index 9fc72f47..ac64d2b0 100644 --- a/src/React/EventLoop/Timer/Timer.php +++ b/src/React/EventLoop/Timer/Timer.php @@ -2,21 +2,22 @@ namespace React\EventLoop\Timer; -use InvalidArgumentException; use React\EventLoop\LoopInterface; class Timer implements TimerInterface { + const MIN_INTERVAL = 0.000001; + protected $loop; protected $interval; protected $callback; protected $periodic; protected $data; - public function __construct(LoopInterface $loop, $interval, $callback, $periodic = false, $data = null) + public function __construct(LoopInterface $loop, $interval, callable $callback, $periodic = false, $data = null) { - if (false === is_callable($callback)) { - throw new InvalidArgumentException('The callback argument must be a valid callable object'); + if ($interval < self::MIN_INTERVAL) { + $interval = self::MIN_INTERVAL; } $this->loop = $loop; diff --git a/src/React/EventLoop/Timer/Timers.php b/src/React/EventLoop/Timer/Timers.php index 0c51bb03..c183a637 100644 --- a/src/React/EventLoop/Timer/Timers.php +++ b/src/React/EventLoop/Timer/Timers.php @@ -4,12 +4,9 @@ use SplObjectStorage; use SplPriorityQueue; -use InvalidArgumentException; class Timers { - const MIN_RESOLUTION = 0.001; - private $time; private $timers; private $scheduler; @@ -33,11 +30,6 @@ public function getTime() public function add(TimerInterface $timer) { $interval = $timer->getInterval(); - - if ($interval < self::MIN_RESOLUTION) { - throw new InvalidArgumentException('Timer events do not support sub-millisecond timeouts.'); - } - $scheduledAt = $interval + $this->getTime(); $this->timers->attach($timer, $scheduledAt); diff --git a/tests/React/Tests/EventLoop/Timer/AbstractTimerTest.php b/tests/React/Tests/EventLoop/Timer/AbstractTimerTest.php index fb173c13..5ff7bb29 100644 --- a/tests/React/Tests/EventLoop/Timer/AbstractTimerTest.php +++ b/tests/React/Tests/EventLoop/Timer/AbstractTimerTest.php @@ -86,4 +86,13 @@ public function testIsTimerActive() $this->assertFalse($loop->isTimerActive($timer)); } + + public function testMinimumIntervalOneMicrosecond() + { + $loop = $this->createLoop(); + + $timer = $loop->addTimer(0, function () {}); + + $this->assertEquals(0.000001, $timer->getInterval()); + } } From c0cd61336f1e7c550b8f9ccd99934e63a427502b Mon Sep 17 00:00:00 2001 From: James Harris Date: Mon, 2 Dec 2013 09:59:21 +1000 Subject: [PATCH 23/24] Changed protected methods in event-loop implementations to private. --- src/React/EventLoop/ExtEventLoop.php | 10 +++++----- src/React/EventLoop/LibEventLoop.php | 10 +++++----- src/React/EventLoop/StreamSelectLoop.php | 2 +- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/React/EventLoop/ExtEventLoop.php b/src/React/EventLoop/ExtEventLoop.php index 3069f496..afa1d98a 100644 --- a/src/React/EventLoop/ExtEventLoop.php +++ b/src/React/EventLoop/ExtEventLoop.php @@ -200,7 +200,7 @@ public function stop() * * @param TimerInterface $timer */ - protected function scheduleTimer(TimerInterface $timer) + private function scheduleTimer(TimerInterface $timer) { $flags = Event::TIMEOUT; @@ -220,7 +220,7 @@ protected function scheduleTimer(TimerInterface $timer) * @param stream $stream * @param integer $flag Event::READ or Event::WRITE */ - protected function subscribeStreamEvent($stream, $flag) + private function subscribeStreamEvent($stream, $flag) { $key = (int) $stream; @@ -247,7 +247,7 @@ protected function subscribeStreamEvent($stream, $flag) * @param stream $stream * @param integer $flag Event::READ or Event::WRITE */ - protected function unsubscribeStreamEvent($stream, $flag) + private function unsubscribeStreamEvent($stream, $flag) { $key = (int) $stream; @@ -273,7 +273,7 @@ protected function unsubscribeStreamEvent($stream, $flag) * to prevent "Cannot destroy active lambda function" fatal error from * the event extension. */ - protected function createTimerCallback() + private function createTimerCallback() { $this->timerCallback = function ($_, $_, $timer) { call_user_func($timer->getCallback(), $timer); @@ -291,7 +291,7 @@ protected function createTimerCallback() * to prevent "Cannot destroy active lambda function" fatal error from * the event extension. */ - protected function createStreamCallback() + private function createStreamCallback() { $this->streamCallback = function ($stream, $flags) { $key = (int) $stream; diff --git a/src/React/EventLoop/LibEventLoop.php b/src/React/EventLoop/LibEventLoop.php index 006c64fa..88288003 100644 --- a/src/React/EventLoop/LibEventLoop.php +++ b/src/React/EventLoop/LibEventLoop.php @@ -207,7 +207,7 @@ public function stop() * * @param TimerInterface $timer */ - protected function scheduleTimer(TimerInterface $timer) + private function scheduleTimer(TimerInterface $timer) { $this->timerEvents[$timer] = $event = event_timer_new(); @@ -222,7 +222,7 @@ protected function scheduleTimer(TimerInterface $timer) * @param stream $stream * @param integer $flag EV_READ or EV_WRITE */ - protected function subscribeStreamEvent($stream, $flag) + private function subscribeStreamEvent($stream, $flag) { $key = (int) $stream; @@ -252,7 +252,7 @@ protected function subscribeStreamEvent($stream, $flag) * @param stream $stream * @param integer $flag EV_READ or EV_WRITE */ - protected function unsubscribeStreamEvent($stream, $flag) + private function unsubscribeStreamEvent($stream, $flag) { $key = (int) $stream; @@ -278,7 +278,7 @@ protected function unsubscribeStreamEvent($stream, $flag) * to prevent "Cannot destroy active lambda function" fatal error from * the event extension. */ - protected function createTimerCallback() + private function createTimerCallback() { $this->timerCallback = function ($_, $_, $timer) { call_user_func($timer->getCallback(), $timer); @@ -308,7 +308,7 @@ protected function createTimerCallback() * to prevent "Cannot destroy active lambda function" fatal error from * the event extension. */ - protected function createStreamCallback() + private function createStreamCallback() { $this->streamCallback = function ($stream, $flags) { $key = (int) $stream; diff --git a/src/React/EventLoop/StreamSelectLoop.php b/src/React/EventLoop/StreamSelectLoop.php index 1928fda9..a309eea3 100644 --- a/src/React/EventLoop/StreamSelectLoop.php +++ b/src/React/EventLoop/StreamSelectLoop.php @@ -193,7 +193,7 @@ public function stop() /** * Wait/check for stream activity, or until the next timer is due. */ - protected function waitForStreamActivity($timeout) + private function waitForStreamActivity($timeout) { $read = $this->readStreams; $write = $this->writeStreams; From edc337eff0bc1dc2446958191249f3c3b6046e2e Mon Sep 17 00:00:00 2001 From: James Harris Date: Thu, 5 Dec 2013 09:08:57 +1000 Subject: [PATCH 24/24] Removed ExtEventLoop --- src/React/EventLoop/ExtEventLoop.php | 308 ------------------ src/React/EventLoop/LibEventLoop.php | 2 - .../Tests/EventLoop/ExtEventLoopTest.php | 59 ---- .../EventLoop/Timer/ExtEventTimerTest.php | 17 - 4 files changed, 386 deletions(-) delete mode 100644 src/React/EventLoop/ExtEventLoop.php delete mode 100644 tests/React/Tests/EventLoop/ExtEventLoopTest.php delete mode 100644 tests/React/Tests/EventLoop/Timer/ExtEventTimerTest.php diff --git a/src/React/EventLoop/ExtEventLoop.php b/src/React/EventLoop/ExtEventLoop.php deleted file mode 100644 index afa1d98a..00000000 --- a/src/React/EventLoop/ExtEventLoop.php +++ /dev/null @@ -1,308 +0,0 @@ -eventBase = new EventBase(); - $this->nextTickQueue = new NextTickQueue($this); - $this->timerEvents = new SplObjectStorage(); - - $this->createTimerCallback(); - $this->createStreamCallback(); - } - - /** - * {@inheritdoc} - */ - public function addReadStream($stream, callable $listener) - { - $key = (int) $stream; - - if (!isset($this->readListeners[$key])) { - $this->readListeners[$key] = $listener; - $this->subscribeStreamEvent($stream, Event::READ); - } - } - - /** - * {@inheritdoc} - */ - public function addWriteStream($stream, callable $listener) - { - $key = (int) $stream; - - if (!isset($this->writeListeners[$key])) { - $this->writeListeners[$key] = $listener; - $this->subscribeStreamEvent($stream, Event::WRITE); - } - } - - /** - * {@inheritdoc} - */ - public function removeReadStream($stream) - { - $key = (int) $stream; - - if (isset($this->readListeners[$key])) { - unset($this->readListeners[$key]); - $this->unsubscribeStreamEvent($stream, Event::READ); - } - } - - /** - * {@inheritdoc} - */ - public function removeWriteStream($stream) - { - $key = (int) $stream; - - if (isset($this->writeListeners[$key])) { - unset($this->writeListeners[$key]); - $this->unsubscribeStreamEvent($stream, Event::WRITE); - } - } - - /** - * {@inheritdoc} - */ - public function removeStream($stream) - { - $key = (int) $stream; - - if (isset($this->streamEvents[$key])) { - $this->streamEvents[$key]->free(); - - unset( - $this->streamFlags[$key], - $this->streamEvents[$key], - $this->readListeners[$key], - $this->writeListeners[$key] - ); - } - } - - /** - * {@inheritdoc} - */ - public function addTimer($interval, callable $callback) - { - $timer = new Timer($this, $interval, $callback, false); - - $this->scheduleTimer($timer); - - return $timer; - } - - /** - * {@inheritdoc} - */ - public function addPeriodicTimer($interval, callable $callback) - { - $timer = new Timer($this, $interval, $callback, true); - - $this->scheduleTimer($timer); - - return $timer; - } - - /** - * {@inheritdoc} - */ - public function cancelTimer(TimerInterface $timer) - { - if ($this->isTimerActive($timer)) { - $this->timerEvents[$timer]->free(); - $this->timerEvents->detach($timer); - } - } - - /** - * {@inheritdoc} - */ - public function isTimerActive(TimerInterface $timer) - { - return $this->timerEvents->contains($timer); - } - - /** - * {@inheritdoc} - */ - public function nextTick(callable $listener) - { - $this->nextTickQueue->add($listener); - } - - /** - * {@inheritdoc} - */ - public function tick() - { - $this->nextTickQueue->tick(); - - // @-suppression: https://github.com/reactphp/react/pull/234#discussion-diff-7759616R226 - @$this->eventBase->loop(EventBase::LOOP_ONCE | EventBase::LOOP_NONBLOCK); - } - - /** - * {@inheritdoc} - */ - public function run() - { - $this->running = true; - - while ($this->running) { - $this->nextTickQueue->tick(); - - if (!$this->streamEvents && !$this->timerEvents->count()) { - break; - } - - // @-suppression: https://github.com/reactphp/react/pull/234#discussion-diff-7759616R226 - @$this->eventBase->loop(EventBase::LOOP_ONCE); - } - } - - /** - * {@inheritdoc} - */ - public function stop() - { - $this->running = false; - } - - /** - * Schedule a timer for execution. - * - * @param TimerInterface $timer - */ - private function scheduleTimer(TimerInterface $timer) - { - $flags = Event::TIMEOUT; - - if ($timer->isPeriodic()) { - $flags |= Event::PERSIST; - } - - $event = new Event($this->eventBase, -1, $flags, $this->timerCallback, $timer); - $this->timerEvents[$timer] = $event; - - $event->add($timer->getInterval()); - } - - /** - * Create a new ext-event Event object, or update the existing one. - * - * @param stream $stream - * @param integer $flag Event::READ or Event::WRITE - */ - private function subscribeStreamEvent($stream, $flag) - { - $key = (int) $stream; - - if (isset($this->streamEvents[$key])) { - $event = $this->streamEvents[$key]; - $flags = ($this->streamFlags[$key] |= $flag); - - $event->del(); - $event->set($this->eventBase, $stream, Event::PERSIST | $flags, $this->streamCallback); - } else { - $event = new Event($this->eventBase, $stream, Event::PERSIST | $flag, $this->streamCallback); - - $this->streamEvents[$key] = $event; - $this->streamFlags[$key] = $flag; - } - - $event->add(); - } - - /** - * Update the ext-event Event object for this stream to stop listening to - * the given event type, or remove it entirely if it's no longer needed. - * - * @param stream $stream - * @param integer $flag Event::READ or Event::WRITE - */ - private function unsubscribeStreamEvent($stream, $flag) - { - $key = (int) $stream; - - $flags = $this->streamFlags[$key] &= ~$flag; - - if (0 === $flags) { - $this->removeStream($stream); - - return; - } - - $event = $this->streamEvents[$key]; - - $event->del(); - $event->set($this->eventBase, $stream, Event::PERSIST | $flags, $this->streamCallback); - $event->add(); - } - - /** - * Create a callback used as the target of timer events. - * - * A reference is kept to the callback for the lifetime of the loop - * to prevent "Cannot destroy active lambda function" fatal error from - * the event extension. - */ - private function createTimerCallback() - { - $this->timerCallback = function ($_, $_, $timer) { - call_user_func($timer->getCallback(), $timer); - - if (!$timer->isPeriodic() && $this->isTimerActive($timer)) { - $this->cancelTimer($timer); - } - }; - } - - /** - * Create a callback used as the target of stream events. - * - * A reference is kept to the callback for the lifetime of the loop - * to prevent "Cannot destroy active lambda function" fatal error from - * the event extension. - */ - private function createStreamCallback() - { - $this->streamCallback = function ($stream, $flags) { - $key = (int) $stream; - - if (Event::READ === (Event::READ & $flags) && isset($this->readListeners[$key])) { - call_user_func($this->readListeners[$key], $stream, $this); - } - - if (Event::WRITE === (Event::WRITE & $flags) && isset($this->writeListeners[$key])) { - call_user_func($this->writeListeners[$key], $stream, $this); - } - }; - } -} diff --git a/src/React/EventLoop/LibEventLoop.php b/src/React/EventLoop/LibEventLoop.php index 88288003..599cb63b 100644 --- a/src/React/EventLoop/LibEventLoop.php +++ b/src/React/EventLoop/LibEventLoop.php @@ -2,8 +2,6 @@ namespace React\EventLoop; -use Event; -use EventBase; use React\EventLoop\Tick\NextTickQueue; use React\EventLoop\Timer\Timer; use React\EventLoop\Timer\TimerInterface; diff --git a/tests/React/Tests/EventLoop/ExtEventLoopTest.php b/tests/React/Tests/EventLoop/ExtEventLoopTest.php deleted file mode 100644 index a9570b40..00000000 --- a/tests/React/Tests/EventLoop/ExtEventLoopTest.php +++ /dev/null @@ -1,59 +0,0 @@ -markTestSkipped('libevent tests skipped on linux due to linux epoll issues.'); - } - - if (!extension_loaded('event')) { - $this->markTestSkipped('ext-event tests skipped because ext-event is not installed.'); - } - - return new ExtEventLoop(); - } - - public function createStream() - { - // Use a FIFO on linux to get around lack of support for disk-based file - // descriptors when using the EPOLL back-end. - if ('Linux' === PHP_OS) { - $this->fifoPath = tempnam(sys_get_temp_dir(), 'react-'); - - unlink($this->fifoPath); - - posix_mkfifo($this->fifoPath, 0600); - - $stream = fopen($this->fifoPath, 'r+'); - - // ext-event (as of 1.8.1) does not yet support in-memory temporary - // streams. Setting maxmemory:0 and performing a write forces PHP to - // back this temporary stream with a real file. - // - // This problem is mentioned at https://bugs.php.net/bug.php?id=64652&edit=3 - // but remains unresolved (despite that issue being closed). - } else { - $stream = fopen('php://temp/maxmemory:0', 'r+'); - - fwrite($stream, 'x'); - ftruncate($stream, 0); - } - - return $stream; - } - - public function writeToStream($stream, $content) - { - if ('Linux' !== PHP_OS) { - return parent::writeToStream($stream, $content); - } - - fwrite($stream, $content); - } -} diff --git a/tests/React/Tests/EventLoop/Timer/ExtEventTimerTest.php b/tests/React/Tests/EventLoop/Timer/ExtEventTimerTest.php deleted file mode 100644 index a7a6d005..00000000 --- a/tests/React/Tests/EventLoop/Timer/ExtEventTimerTest.php +++ /dev/null @@ -1,17 +0,0 @@ -markTestSkipped('ext-event tests skipped because ext-event is not installed.'); - } - - return new ExtEventLoop(); - } -} pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy