1
0
mirror of https://github.com/danog/amp.git synced 2024-12-11 17:09:40 +01:00

Enforce timer interval as minimum time to execution (#319)

Co-authored-by: Aaron Piotrowski <aaron@trowski.com>
This commit is contained in:
Niklas Keller 2020-07-14 21:45:35 +02:00 committed by GitHub
parent 0b802a501e
commit 05483cdbef
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 162 additions and 64 deletions

View File

@ -31,7 +31,7 @@ install:
- travis/install-ev.sh - travis/install-ev.sh
- travis/install-event.sh - travis/install-event.sh
- if [[ ${TRAVIS_PHP_VERSION:0:3} == "7.0" ]]; then composer remove --dev vimeo/psalm; fi - if [[ ${TRAVIS_PHP_VERSION:0:3} == "7.0" ]]; then composer remove --dev psalm/phar; fi
- composer update -n --prefer-dist - composer update -n --prefer-dist
- mkdir -p coverage/cov coverage/bin - mkdir -p coverage/cov coverage/bin
@ -45,7 +45,7 @@ script:
- php vendor/bin/phpunit --verbose --group memoryleak - php vendor/bin/phpunit --verbose --group memoryleak
- php vendor/bin/phpunit --verbose --exclude-group memoryleak --coverage-php coverage/cov/main.cov - php vendor/bin/phpunit --verbose --exclude-group memoryleak --coverage-php coverage/cov/main.cov
- PHP_CS_FIXER_IGNORE_ENV=1 php vendor/bin/php-cs-fixer --diff --dry-run -v fix - PHP_CS_FIXER_IGNORE_ENV=1 php vendor/bin/php-cs-fixer --diff --dry-run -v fix
- if [[ ${TRAVIS_PHP_VERSION:0:3} == "7.0" ]]; then echo "Skipped psalm static analysis"; else vendor/bin/psalm; fi - if [[ ${TRAVIS_PHP_VERSION:0:3} == "7.0" ]]; then echo "Skipped psalm static analysis"; else vendor/bin/psalm.phar; fi
after_script: after_script:
- curl -OL https://github.com/php-coveralls/php-coveralls/releases/download/v1.0.0/coveralls.phar - curl -OL https://github.com/php-coveralls/php-coveralls/releases/download/v1.0.0/coveralls.phar

View File

@ -41,7 +41,7 @@
"amphp/php-cs-fixer-config": "dev-master", "amphp/php-cs-fixer-config": "dev-master",
"react/promise": "^2", "react/promise": "^2",
"phpunit/phpunit": "^6.0.9 | ^7", "phpunit/phpunit": "^6.0.9 | ^7",
"vimeo/psalm": "^3.11@dev", "psalm/phar": "^3.11@dev",
"jetbrains/phpstorm-stubs": "^2019.3" "jetbrains/phpstorm-stubs": "^2019.3"
}, },
"autoload": { "autoload": {

View File

@ -227,6 +227,7 @@ abstract class Driver
$watcher->id = $this->nextId++; $watcher->id = $this->nextId++;
$watcher->callback = $callback; $watcher->callback = $callback;
$watcher->value = $delay; $watcher->value = $delay;
$watcher->expiration = $this->now() + $delay;
$watcher->data = $data; $watcher->data = $data;
$this->watchers[$watcher->id] = $watcher; $this->watchers[$watcher->id] = $watcher;
@ -263,6 +264,7 @@ abstract class Driver
$watcher->id = $this->nextId++; $watcher->id = $this->nextId++;
$watcher->callback = $callback; $watcher->callback = $callback;
$watcher->value = $interval; $watcher->value = $interval;
$watcher->expiration = $this->now() + $interval;
$watcher->data = $data; $watcher->data = $data;
$this->watchers[$watcher->id] = $watcher; $this->watchers[$watcher->id] = $watcher;
@ -408,6 +410,14 @@ abstract class Driver
$this->nextTickQueue[$watcher->id] = $watcher; $this->nextTickQueue[$watcher->id] = $watcher;
break; break;
case Watcher::REPEAT:
case Watcher::DELAY:
\assert(\is_int($watcher->value));
$watcher->expiration = $this->now() + $watcher->value;
$this->enableQueue[$watcher->id] = $watcher;
break;
default: default:
$this->enableQueue[$watcher->id] = $watcher; $this->enableQueue[$watcher->id] = $watcher;
break; break;

View File

@ -32,8 +32,6 @@ class EvDriver extends Driver
private $signals = []; private $signals = [];
/** @var int Internal timestamp for now. */ /** @var int Internal timestamp for now. */
private $now; private $now;
/** @var bool */
private $nowUpdateNeeded = false;
/** @var int Loop time offset */ /** @var int Loop time offset */
private $nowOffset; private $nowOffset;
@ -213,10 +211,7 @@ class EvDriver extends Driver
*/ */
public function now(): int public function now(): int
{ {
if ($this->nowUpdateNeeded) { $this->now = getCurrentTime() - $this->nowOffset;
$this->now = getCurrentTime() - $this->nowOffset;
$this->nowUpdateNeeded = false;
}
return $this->now; return $this->now;
} }
@ -236,7 +231,6 @@ class EvDriver extends Driver
*/ */
protected function dispatch(bool $blocking) protected function dispatch(bool $blocking)
{ {
$this->nowUpdateNeeded = true;
$this->handle->run($blocking ? \Ev::RUN_ONCE : \Ev::RUN_ONCE | \Ev::RUN_NOWAIT); $this->handle->run($blocking ? \Ev::RUN_ONCE : \Ev::RUN_ONCE | \Ev::RUN_NOWAIT);
} }
@ -247,6 +241,9 @@ class EvDriver extends Driver
*/ */
protected function activate(array $watchers) protected function activate(array $watchers)
{ {
$this->handle->nowUpdate();
$now = $this->now();
foreach ($watchers as $watcher) { foreach ($watchers as $watcher) {
if (!isset($this->events[$id = $watcher->id])) { if (!isset($this->events[$id = $watcher->id])) {
switch ($watcher->type) { switch ($watcher->type) {
@ -273,7 +270,7 @@ class EvDriver extends Driver
$interval = $watcher->value / self::MILLISEC_PER_SEC; $interval = $watcher->value / self::MILLISEC_PER_SEC;
$this->events[$id] = $this->handle->timer( $this->events[$id] = $this->handle->timer(
$interval, \max(0, ($watcher->expiration - $now) / self::MILLISEC_PER_SEC),
($watcher->type & Watcher::REPEAT) ? $interval : 0, ($watcher->type & Watcher::REPEAT) ? $interval : 0,
$this->timerCallback, $this->timerCallback,
$watcher $watcher

View File

@ -31,9 +31,6 @@ class EventDriver extends Driver
/** @var \Event[] */ /** @var \Event[] */
private $signals = []; private $signals = [];
/** @var bool */
private $nowUpdateNeeded = false;
/** @var int Internal timestamp for now. */ /** @var int Internal timestamp for now. */
private $now; private $now;
@ -235,10 +232,7 @@ class EventDriver extends Driver
*/ */
public function now(): int public function now(): int
{ {
if ($this->nowUpdateNeeded) { $this->now = getCurrentTime() - $this->nowOffset;
$this->now = getCurrentTime() - $this->nowOffset;
$this->nowUpdateNeeded = false;
}
return $this->now; return $this->now;
} }
@ -258,7 +252,6 @@ class EventDriver extends Driver
*/ */
protected function dispatch(bool $blocking) protected function dispatch(bool $blocking)
{ {
$this->nowUpdateNeeded = true;
$this->handle->loop($blocking ? \EventBase::LOOP_ONCE : \EventBase::LOOP_ONCE | \EventBase::LOOP_NONBLOCK); $this->handle->loop($blocking ? \EventBase::LOOP_ONCE : \EventBase::LOOP_ONCE | \EventBase::LOOP_NONBLOCK);
} }
@ -269,7 +262,7 @@ class EventDriver extends Driver
*/ */
protected function activate(array $watchers) protected function activate(array $watchers)
{ {
$now = getCurrentTime() - $this->nowOffset; $now = $this->now();
foreach ($watchers as $watcher) { foreach ($watchers as $watcher) {
if (!isset($this->events[$id = $watcher->id])) { if (!isset($this->events[$id = $watcher->id])) {
@ -335,7 +328,7 @@ class EventDriver extends Driver
case Watcher::REPEAT: case Watcher::REPEAT:
\assert(\is_int($watcher->value)); \assert(\is_int($watcher->value));
$interval = $watcher->value - ($now - $this->now()); $interval = \max(0, $watcher->expiration - $now);
$this->events[$id]->add($interval > 0 ? $interval / self::MILLISEC_PER_SEC : 0); $this->events[$id]->add($interval > 0 ? $interval / self::MILLISEC_PER_SEC : 0);
break; break;

View File

@ -19,15 +19,17 @@ final class TimerQueue
* Inserts the watcher into the queue. Time complexity: O(log(n)). * Inserts the watcher into the queue. Time complexity: O(log(n)).
* *
* @param Watcher $watcher * @param Watcher $watcher
* @param int $expiration
* *
* @psalm-param Watcher<int> $watcher * @psalm-param Watcher<int> $watcher
* *
* @return void * @return void
*/ */
public function insert(Watcher $watcher, int $expiration) public function insert(Watcher $watcher)
{ {
$entry = new TimerQueueEntry($watcher, $expiration); \assert($watcher->expiration !== null);
\assert(!isset($this->pointers[$watcher->id]));
$entry = new TimerQueueEntry($watcher, $watcher->expiration);
$node = \count($this->data); $node = \count($this->data);
$this->data[$node] = $entry; $this->data[$node] = $entry;

View File

@ -31,9 +31,6 @@ class NativeDriver extends Driver
/** @var Watcher[][] */ /** @var Watcher[][] */
private $signalWatchers = []; private $signalWatchers = [];
/** @var bool */
private $nowUpdateNeeded = false;
/** @var int Internal timestamp for now. */ /** @var int Internal timestamp for now. */
private $now; private $now;
@ -71,10 +68,7 @@ class NativeDriver extends Driver
*/ */
public function now(): int public function now(): int
{ {
if ($this->nowUpdateNeeded) { $this->now = getCurrentTime() - $this->nowOffset;
$this->now = getCurrentTime() - $this->nowOffset;
$this->nowUpdateNeeded = false;
}
return $this->now; return $this->now;
} }
@ -96,8 +90,6 @@ class NativeDriver extends Driver
*/ */
protected function dispatch(bool $blocking) protected function dispatch(bool $blocking)
{ {
$this->nowUpdateNeeded = true;
$this->selectStreams( $this->selectStreams(
$this->readStreams, $this->readStreams,
$this->writeStreams, $this->writeStreams,
@ -292,9 +284,7 @@ class NativeDriver extends Driver
case Watcher::DELAY: case Watcher::DELAY:
case Watcher::REPEAT: case Watcher::REPEAT:
\assert(\is_int($watcher->value)); \assert(\is_int($watcher->value));
$this->timerQueue->insert($watcher);
$expiration = $this->now() + $watcher->value;
$this->timerQueue->insert($watcher, $expiration);
break; break;
case Watcher::SIGNAL: case Watcher::SIGNAL:

View File

@ -190,6 +190,8 @@ class UvDriver extends Driver
*/ */
public function now(): int public function now(): int
{ {
\uv_update_time($this->handle);
/** @psalm-suppress TooManyArguments */ /** @psalm-suppress TooManyArguments */
return \uv_now($this->handle); return \uv_now($this->handle);
} }
@ -220,6 +222,8 @@ class UvDriver extends Driver
*/ */
protected function activate(array $watchers) protected function activate(array $watchers)
{ {
$now = $this->now();
foreach ($watchers as $watcher) { foreach ($watchers as $watcher) {
$id = $watcher->id; $id = $watcher->id;
@ -264,7 +268,7 @@ class UvDriver extends Driver
\uv_timer_start( \uv_timer_start(
$event, $event,
$watcher->value, \max(0, $watcher->expiration - $now),
($watcher->type & Watcher::REPEAT) ? $watcher->value : 0, ($watcher->type & Watcher::REPEAT) ? $watcher->value : 0,
$this->timerCallback $this->timerCallback
); );

View File

@ -51,4 +51,7 @@ class Watcher
* @psalm-var TValue * @psalm-var TValue
*/ */
public $value; public $value;
/** @var int|null */
public $expiration;
} }

View File

@ -11,6 +11,7 @@ use Amp\Loop\InvalidWatcherError;
use Amp\Loop\UnsupportedFeatureException; use Amp\Loop\UnsupportedFeatureException;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use React\Promise\RejectedPromise as RejectedReactPromise; use React\Promise\RejectedPromise as RejectedReactPromise;
use function Amp\getCurrentTime;
if (!\defined("SIGUSR1")) { if (!\defined("SIGUSR1")) {
\define("SIGUSR1", 30); \define("SIGUSR1", 30);
@ -106,6 +107,52 @@ abstract class DriverTest extends TestCase
}); });
} }
public function testCorrectTimeoutIfBlockingBeforeActivate()
{
$start = 0;
$invoked = 0;
$this->start(function (Driver $loop) use (&$start, &$invoked) {
$loop->defer(function () use ($loop, &$start, &$invoked) {
$start = getCurrentTime();
$loop->delay(1000, function () use (&$invoked) {
$invoked = getCurrentTime();
});
\usleep(500000);
});
});
$this->assertNotSame(0, $start);
$this->assertNotSame(0, $invoked);
$this->assertGreaterThanOrEqual(999, $invoked - $start);
$this->assertLessThan(1100, $invoked - $start);
}
public function testCorrectTimeoutIfBlockingBeforeDelay()
{
$start = 0;
$invoked = 0;
$this->start(function (Driver $loop) use (&$start, &$invoked) {
$start = getCurrentTime();
\usleep(500000);
$loop->delay(1000, function () use (&$invoked) {
$invoked = getCurrentTime();
});
});
$this->assertNotSame(0, $start);
$this->assertNotSame(0, $invoked);
$this->assertGreaterThanOrEqual(1500, $invoked - $start);
$this->assertLessThan(1600, $invoked - $start);
}
public function testLoopTerminatesWithOnlyUnreferencedWatchers() public function testLoopTerminatesWithOnlyUnreferencedWatchers()
{ {
$this->start(function (Driver $loop) use (&$end) { $this->start(function (Driver $loop) use (&$end) {
@ -272,18 +319,53 @@ abstract class DriverTest extends TestCase
public function provideRegistrationArgs() public function provideRegistrationArgs()
{ {
$args = [ $args = [
["defer", [function () { [
}]], "defer",
["delay", [5, function () { [
}]], function () {
["repeat", [5, function () { },
}]], ],
["onWritable", [\STDOUT, function () { ],
}]], [
["onReadable", [\STDIN, function () { "delay",
}]], [
["onSignal", [\SIGUSR1, function () { 5,
}]], function () {
},
],
],
[
"repeat",
[
5,
function () {
},
],
],
[
"onWritable",
[
\STDOUT,
function () {
},
],
],
[
"onReadable",
[
\STDIN,
function () {
},
],
],
[
"onSignal",
[
\SIGUSR1,
function () {
},
],
],
]; ];
return $args; return $args;
@ -301,7 +383,11 @@ abstract class DriverTest extends TestCase
$this->start(function (Driver $loop) use ($type, $args, &$invoked) { $this->start(function (Driver $loop) use ($type, $args, &$invoked) {
if ($type == "onReadable") { if ($type == "onReadable") {
$ends = \stream_socket_pair(\stripos(PHP_OS, "win") === 0 ? STREAM_PF_INET : STREAM_PF_UNIX, STREAM_SOCK_STREAM, STREAM_IPPROTO_IP); $ends = \stream_socket_pair(
\stripos(PHP_OS, "win") === 0 ? STREAM_PF_INET : STREAM_PF_UNIX,
STREAM_SOCK_STREAM,
STREAM_IPPROTO_IP
);
\fwrite($ends[0], "trigger readability watcher"); \fwrite($ends[0], "trigger readability watcher");
$args = [$ends[1]]; $args = [$ends[1]];
} else { } else {
@ -574,7 +660,14 @@ abstract class DriverTest extends TestCase
} }
if ($i) { if ($i) {
// explicitly use *different* streams with *different* resource ids // explicitly use *different* streams with *different* resource ids
$ends = \stream_socket_pair(\stripos(PHP_OS, "win") === 0 ? STREAM_PF_INET : STREAM_PF_UNIX, STREAM_SOCK_STREAM, STREAM_IPPROTO_IP); $ends = \stream_socket_pair(
\stripos(
PHP_OS,
"win"
) === 0 ? STREAM_PF_INET : STREAM_PF_UNIX,
STREAM_SOCK_STREAM,
STREAM_IPPROTO_IP
);
$loop->onWritable($ends[0], $fn, --$i); $loop->onWritable($ends[0], $fn, --$i);
$loop->onReadable($ends[1], function ($watcherId) use ($loop) { $loop->onReadable($ends[1], function ($watcherId) use ($loop) {
$loop->cancel($watcherId); $loop->cancel($watcherId);
@ -621,7 +714,10 @@ abstract class DriverTest extends TestCase
*/ */
public function testExecutionOrderGuarantees() public function testExecutionOrderGuarantees()
{ {
$this->expectOutputString("01 02 03 04 " . \str_repeat("05 ", 8) . "10 11 12 " . \str_repeat("13 ", 4) . "20 " . \str_repeat("21 ", 4) . "30 40 41 "); $this->expectOutputString("01 02 03 04 " . \str_repeat("05 ", 8) . "10 11 12 " . \str_repeat(
"13 ",
4
) . "20 " . \str_repeat("21 ", 4) . "30 40 41 ");
$this->start(function (Driver $loop) { $this->start(function (Driver $loop) {
// Wrap in extra defer, so driver creation time doesn't count for timers, as timers are driver creation // Wrap in extra defer, so driver creation time doesn't count for timers, as timers are driver creation
// relative instead of last tick relative before first tick. // relative instead of last tick relative before first tick.
@ -1415,7 +1511,11 @@ abstract class DriverTest extends TestCase
break; break;
case "onReadable": case "onReadable":
$ends = \stream_socket_pair(\stripos(PHP_OS, "win") === 0 ? STREAM_PF_INET : STREAM_PF_UNIX, STREAM_SOCK_STREAM, STREAM_IPPROTO_IP); $ends = \stream_socket_pair(
\stripos(PHP_OS, "win") === 0 ? STREAM_PF_INET : STREAM_PF_UNIX,
STREAM_SOCK_STREAM,
STREAM_IPPROTO_IP
);
\fwrite($ends[0], "trigger readability watcher"); \fwrite($ends[0], "trigger readability watcher");
$args[] = $ends[1]; $args[] = $ends[1];
break; break;
@ -1458,7 +1558,11 @@ abstract class DriverTest extends TestCase
public function testMultipleWatchersOnSameDescriptor() public function testMultipleWatchersOnSameDescriptor()
{ {
$sockets = \stream_socket_pair(\stripos(PHP_OS, "win") === 0 ? STREAM_PF_INET : STREAM_PF_UNIX, STREAM_SOCK_STREAM, STREAM_IPPROTO_IP); $sockets = \stream_socket_pair(
\stripos(PHP_OS, "win") === 0 ? STREAM_PF_INET : STREAM_PF_UNIX,
STREAM_SOCK_STREAM,
STREAM_IPPROTO_IP
);
\fwrite($sockets[1], "testing"); \fwrite($sockets[1], "testing");
$invoked = 0; $invoked = 0;
@ -1505,11 +1609,12 @@ abstract class DriverTest extends TestCase
public function testTimerIntervalCountedWhenNotRunning() public function testTimerIntervalCountedWhenNotRunning()
{ {
\usleep(600000); // 600ms instead of 500ms to allow for variations in timing. $this->loop->delay(1000, function () use (&$start) {
$start = \microtime(true);
$this->loop->delay(1000, function () use ($start) {
$this->assertLessThan(0.5, \microtime(true) - $start); $this->assertLessThan(0.5, \microtime(true) - $start);
}); });
\usleep(600000); // 600ms instead of 500ms to allow for variations in timing.
$start = \microtime(true);
$this->loop->run(); $this->loop->run();
} }
@ -1565,9 +1670,6 @@ abstract class DriverTest extends TestCase
// Allow a few milliseconds of inaccuracy. // Allow a few milliseconds of inaccuracy.
$this->assertGreaterThanOrEqual($now - 1, $new); $this->assertGreaterThanOrEqual($now - 1, $new);
$this->assertLessThanOrEqual($now + 10, $new); $this->assertLessThanOrEqual($now + 10, $new);
// Same time should be returned from later call.
$this->assertSame($new, $this->loop->now());
}); });
$this->loop->run(); $this->loop->run();
} }

View File

@ -62,9 +62,6 @@ class LoopTest extends BaseTest
// Allow a few milliseconds of inaccuracy. // Allow a few milliseconds of inaccuracy.
$this->assertGreaterThanOrEqual($now - 1, $new); $this->assertGreaterThanOrEqual($now - 1, $new);
$this->assertLessThanOrEqual($now + 100, $new); $this->assertLessThanOrEqual($now + 100, $new);
// Same time should be returned from later call.
$this->assertSame($new, Loop::now());
}); });
}); });
} }

View File

@ -12,7 +12,7 @@ class PsalmTest extends TestCase
public function test() public function test()
{ {
$issues = \json_decode( $issues = \json_decode(
\shell_exec('./vendor/bin/psalm --output-format=json --no-progress --config=psalm.examples.xml'), \shell_exec('./vendor/bin/psalm.phar --output-format=json --no-progress --config=psalm.examples.xml'),
true true
); );