diff --git a/lib/Loop.php b/lib/Loop.php index 3452047..1127a19 100644 --- a/lib/Loop.php +++ b/lib/Loop.php @@ -332,6 +332,18 @@ final class Loop self::$driver->unreference($watcherId); } + /** + * Returns the current loop time in millisecond increments. Note this value does not necessarily correlate to + * wall-clock time, rather the value returned is meant to be used in relative comparisons to prior values returned + * by this method (intervals, expiration calculations, etc.) and is only updated once per loop tick. + * + * @return int + */ + public static function now(): int + { + return self::$driver->now(); + } + /** * Stores information in the loop bound registry. * diff --git a/lib/Loop/Driver.php b/lib/Loop/Driver.php index 64272ed..ffb9508 100644 --- a/lib/Loop/Driver.php +++ b/lib/Loop/Driver.php @@ -590,6 +590,20 @@ abstract class Driver ($this->errorHandler)($exception); } + /** + * Returns the current loop time in millisecond increments. Note this value does not necessarily correlate to + * wall-clock time, rather the value returned is meant to be used in relative comparisons to prior values returned + * by this method (intervals, expiration calculations, etc.) and is only updated once per loop tick. + * + * Extending classes should override this function to return a value cached once per loop tick. + * + * @return int + */ + public function now(): int + { + return \microtime(true) * self::MILLISEC_PER_SEC; + } + /** * Get the underlying loop handle. * diff --git a/lib/Loop/EvDriver.php b/lib/Loop/EvDriver.php index 8e2eb79..1f6175d 100644 --- a/lib/Loop/EvDriver.php +++ b/lib/Loop/EvDriver.php @@ -11,22 +11,38 @@ class EvDriver extends Driver { /** @var \EvSignal[]|null */ private static $activeSignals; + /** @var \EvLoop */ private $handle; + /** @var \EvWatcher[] */ private $events = []; + /** @var callable */ private $ioCallback; + /** @var callable */ private $timerCallback; + /** @var callable */ private $signalCallback; + /** @var \EvSignal[] */ private $signals = []; + /** @var int Internal timestamp for now. */ + private $now = 0; + + /** @var bool */ + private $nowUpdateNeeded = false; + + /** @var int Loop time offset from microtime() */ + private $nowOffset; + public function __construct() { $this->handle = new \EvLoop; + $this->nowOffset = (int) (\microtime(true) * self::MILLISEC_PER_SEC); if (self::$activeSignals === null) { self::$activeSignals = &$this->signals; @@ -179,6 +195,19 @@ class EvDriver extends Driver parent::stop(); } + /** + * {@inheritdoc} + */ + public function now(): int + { + if ($this->nowUpdateNeeded) { + $this->now = (int) (\microtime(true) * self::MILLISEC_PER_SEC) - $this->nowOffset; + $this->nowUpdateNeeded = false; + } + + return $this->now; + } + /** * {@inheritdoc} */ @@ -192,6 +221,7 @@ class EvDriver extends Driver */ protected function dispatch(bool $blocking) { + $this->nowUpdateNeeded = true; $this->handle->run($blocking ? \Ev::RUN_ONCE : \Ev::RUN_ONCE | \Ev::RUN_NOWAIT); } diff --git a/lib/Loop/EventDriver.php b/lib/Loop/EventDriver.php index da54955..d6a5c7e 100644 --- a/lib/Loop/EventDriver.php +++ b/lib/Loop/EventDriver.php @@ -11,25 +11,38 @@ class EventDriver extends Driver { /** @var \Event[]|null */ private static $activeSignals; + /** @var \EventBase */ private $handle; + /** @var \Event[] */ private $events = []; + /** @var callable */ private $ioCallback; + /** @var callable */ private $timerCallback; + /** @var callable */ private $signalCallback; + /** @var \Event[] */ private $signals = []; + + /** @var bool */ + private $nowUpdateNeeded = false; + /** @var int Internal timestamp for now. */ - private $now; + private $now = 0; + + /** @var int Loop time offset from microtime() */ + private $nowOffset; public function __construct() { $this->handle = new \EventBase; - $this->now = (int) (\microtime(true) * self::MILLISEC_PER_SEC); + $this->nowOffset = (int) (\microtime(true) * self::MILLISEC_PER_SEC); if (self::$activeSignals === null) { self::$activeSignals = &$this->signals; @@ -184,6 +197,19 @@ class EventDriver extends Driver parent::stop(); } + /** + * {@inheritdoc} + */ + public function now(): int + { + if ($this->nowUpdateNeeded) { + $this->now = (int) (\microtime(true) * self::MILLISEC_PER_SEC) - $this->nowOffset; + $this->nowUpdateNeeded = false; + } + + return $this->now; + } + /** * {@inheritdoc} */ @@ -197,8 +223,8 @@ class EventDriver extends Driver */ protected function dispatch(bool $blocking) { + $this->nowUpdateNeeded = true; $this->handle->loop($blocking ? \EventBase::LOOP_ONCE : \EventBase::LOOP_ONCE | \EventBase::LOOP_NONBLOCK); - $this->now = (int) (\microtime(true) * self::MILLISEC_PER_SEC); } /** @@ -206,7 +232,7 @@ class EventDriver extends Driver */ protected function activate(array $watchers) { - $now = (int) (\microtime(true) * self::MILLISEC_PER_SEC); + $now = (int) (\microtime(true) * self::MILLISEC_PER_SEC) - $this->nowOffset; foreach ($watchers as $watcher) { if (!isset($this->events[$id = $watcher->id])) { @@ -262,7 +288,7 @@ class EventDriver extends Driver switch ($watcher->type) { case Watcher::DELAY: case Watcher::REPEAT: - $interval = $watcher->value - ($now - $this->now); + $interval = $watcher->value - ($now - $this->now()); $this->events[$id]->add($interval > 0 ? $interval / self::MILLISEC_PER_SEC : 0); break; diff --git a/lib/Loop/NativeDriver.php b/lib/Loop/NativeDriver.php index 273a1e9..9d7007f 100644 --- a/lib/Loop/NativeDriver.php +++ b/lib/Loop/NativeDriver.php @@ -30,8 +30,14 @@ class NativeDriver extends Driver /** @var \Amp\Loop\Watcher[][] */ private $signalWatchers = []; + /** @var bool */ + private $nowUpdateNeeded = false; + /** @var int Internal timestamp for now. */ - private $now; + private $now = 0; + + /** @var int Loop time offset from microtime() */ + private $nowOffset; /** @var bool */ private $signalHandling; @@ -40,7 +46,7 @@ class NativeDriver extends Driver { $this->timerQueue = new \SplPriorityQueue(); $this->signalHandling = \extension_loaded("pcntl"); - $this->now = (int) (\microtime(true) * self::MILLISEC_PER_SEC); + $this->nowOffset = (int) (\microtime(true) * self::MILLISEC_PER_SEC); } /** @@ -57,6 +63,19 @@ class NativeDriver extends Driver return parent::onSignal($signo, $callback, $data); } + /** + * {@inheritdoc} + */ + public function now(): int + { + if ($this->nowUpdateNeeded) { + $this->now = (int) (\microtime(true) * self::MILLISEC_PER_SEC) - $this->nowOffset; + $this->nowUpdateNeeded = false; + } + + return $this->now; + } + /** * {@inheritdoc} */ @@ -67,14 +86,14 @@ class NativeDriver extends Driver protected function dispatch(bool $blocking) { + $this->nowUpdateNeeded = true; + $this->selectStreams( $this->readStreams, $this->writeStreams, $blocking ? $this->getTimeout() : 0 ); - $this->now = (int) (\microtime(true) * self::MILLISEC_PER_SEC); - if (!empty($this->timerExpires)) { $scheduleQueue = []; @@ -89,14 +108,14 @@ class NativeDriver extends Driver continue; } - if ($this->timerExpires[$id] > $this->now) { // Timer at top of queue has not expired. + if ($this->timerExpires[$id] > $this->now()) { // Timer at top of queue has not expired. break; } $this->timerQueue->extract(); if ($watcher->type & Watcher::REPEAT) { - $expiration = $this->now + $watcher->value; + $expiration = $this->now() + $watcher->value; $this->timerExpires[$watcher->id] = $expiration; $scheduleQueue[] = [$watcher, $expiration]; } else { @@ -283,7 +302,7 @@ class NativeDriver extends Driver case Watcher::DELAY: case Watcher::REPEAT: - $expiration = $this->now + $watcher->value; + $expiration = $this->now() + $watcher->value; $this->timerExpires[$watcher->id] = $expiration; $this->timerQueue->insert([$watcher, $expiration], -$expiration); break; diff --git a/lib/Loop/UvDriver.php b/lib/Loop/UvDriver.php index de21661..ff60d8f 100644 --- a/lib/Loop/UvDriver.php +++ b/lib/Loop/UvDriver.php @@ -166,6 +166,14 @@ class UvDriver extends Driver return \extension_loaded("uv"); } + /** + * {@inheritdoc} + */ + public function now(): int + { + return \uv_now($this->handle); + } + /** * {@inheritdoc} */ diff --git a/test/Loop/DriverTest.php b/test/Loop/DriverTest.php index 73130a1..9bec2ca 100644 --- a/test/Loop/DriverTest.php +++ b/test/Loop/DriverTest.php @@ -1555,6 +1555,23 @@ abstract class DriverTest extends TestCase $this->assertNotSame(0, $j); } + public function testNow() + { + $now = $this->loop->now(); + $this->loop->delay(100, function () use ($now) { + $now += 100; + $new = $this->loop->now(); + + // Allow a few milliseconds of inaccuracy. + $this->assertGreaterThanOrEqual($now - 5, $new); + $this->assertLessThanOrEqual($now + 5, $new); + + // Same time should be returned from later call. + $this->assertSame($new, $this->loop->now()); + }); + $this->loop->run(); + } + public function testBug163ConsecutiveDelayed() { $emits = 3; diff --git a/test/LoopTest.php b/test/LoopTest.php index 73ffda6..19755ff 100644 --- a/test/LoopTest.php +++ b/test/LoopTest.php @@ -46,12 +46,30 @@ class LoopTest extends TestCase }); } + public function testNow() + { + Loop::run(function () { + $now = Loop::now(); + Loop::delay(100, function () use ($now) { + $now += 100; + $new = Loop::now(); + + // Allow a few milliseconds of inaccuracy. + $this->assertGreaterThanOrEqual($now - 5, $new); + $this->assertLessThanOrEqual($now + 5, $new); + + // Same time should be returned from later call. + $this->assertSame($new, Loop::now()); + }); + }); + } + public function testGet() { $this->assertInstanceOf(Loop\Driver::class, Loop::get()); } - public function testGetInto() + public function testGetInfo() { $this->assertSame(Loop::get()->getInfo(), Loop::getInfo()); }