Skip to content

Commit 3654b6f

Browse files
author
Greg Bowler
committed
Implement PeriodicTimer
1 parent 24c8876 commit 3654b6f

7 files changed

Lines changed: 201 additions & 25 deletions

File tree

example/02-countdown.php

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
/**
3+
* This example shows how a PeriodicTimer can be used to schedule ticks
4+
* forever, combined with an IndividualTimer that will cause the loop to halt
5+
* after a specified period.
6+
*/
7+
8+
use Gt\Async\Loop;
9+
use Gt\Async\Timer\IndividualTimer;
10+
use Gt\Async\Timer\PeriodicTimer;
11+
12+
require("../vendor/autoload.php");
13+
14+
$stopTimer = new IndividualTimer(5);
15+
$periodicTimer = new PeriodicTimer(1, true);
16+
$countdownNumber = 5;
17+
18+
$loop = new Loop();
19+
20+
$periodicTimer->addCallback(function() use(&$countdownNumber) {
21+
echo "Countdown: $countdownNumber", PHP_EOL;
22+
$countdownNumber--;
23+
});
24+
$stopTimer->addCallback(function() use($loop) {
25+
echo "LIFT OFF!", PHP_EOL;
26+
$loop->halt();
27+
});
28+
29+
$loop->addTimer($periodicTimer);
30+
$loop->addTimer($stopTimer);
31+
echo "Starting...", PHP_EOL;
32+
$loop->run();

src/Loop.php

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ class Loop {
2020
private $sleepFunction;
2121
/** @var callable Function that delivers the current time in milliseconds as a float */
2222
private $timeFunction;
23+
private bool $forever;
2324

2425
public function __construct() {
2526
$this->timerList = [];
@@ -45,11 +46,17 @@ public function setTimeFunction(callable $timeFunction):void {
4546
}
4647

4748
public function run(bool $forever = true):void {
49+
$this->forever = $forever;
50+
4851
do {
4952
$numTriggered = $this->triggerNextTimers();
5053
$this->triggerCount += $numTriggered;
5154
}
52-
while($numTriggered > 0 && $forever);
55+
while($numTriggered > 0 && $this->forever);
56+
}
57+
58+
public function halt():void {
59+
$this->forever = false;
5360
}
5461

5562
public function getTriggerCount():int {

src/Timer/IndividualTimer.php

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,25 @@
22
namespace Gt\Async\Timer;
33

44
class IndividualTimer extends Timer {
5-
public function __construct(float $triggerTime = null) {
5+
/**
6+
* @param float $triggerSeconds The number of seconds in the future
7+
* that the timer will trigger. To set an absolute time,
8+
* use addTriggerTime().
9+
*/
10+
public function __construct(float $triggerSeconds = null) {
611
parent::__construct();
712

8-
if(!is_null($triggerTime)) {
9-
$this->addTriggerTime($triggerTime);
13+
if(!is_null($triggerSeconds)) {
14+
$this->addTriggerTime(
15+
call_user_func($this->timeFunction)
16+
+ $triggerSeconds
17+
);
1018
}
1119
}
1220

21+
/**
22+
* @param float $triggerTime The unix epoch of when to trigger.
23+
*/
1324
public function addTriggerTime(float $triggerTime):void {
1425
$this->triggerTimeQueue[] = $triggerTime;
1526
sort($this->triggerTimeQueue);

src/Timer/PeriodicTimer.php

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php
2+
namespace Gt\Async\Timer;
3+
4+
class PeriodicTimer extends Timer {
5+
const TRIGGER_POOL_SIZE = 1_000;
6+
7+
private float $period;
8+
private bool $immediate;
9+
10+
/**
11+
* @param float $period The number of seconds between each tick trigger.
12+
* @param bool $immediate Set to true to have the first tick trigger
13+
* immediately, followed by the period of delay. Set to false to wait
14+
* the period of delay before the first tick trigger.
15+
*/
16+
public function __construct(float $period, bool $immediate = false) {
17+
parent::__construct();
18+
19+
$this->period = $period;
20+
$this->immediate = $immediate;
21+
}
22+
23+
public function isScheduled():bool {
24+
$this->scheduleTriggerPool();
25+
return parent::isScheduled();
26+
}
27+
28+
public function getNextRunTime():?float {
29+
$this->scheduleTriggerPool();
30+
return parent::getNextRunTime();
31+
}
32+
33+
private function scheduleTriggerPool():void {
34+
$now = call_user_func($this->timeFunction);
35+
$queueSize = count($this->triggerTimeQueue);
36+
37+
if($queueSize === 0
38+
&& $this->immediate) {
39+
$this->triggerTimeQueue[] = $now;
40+
$this->immediate = false;
41+
}
42+
43+
for($i = 0; $i < self::TRIGGER_POOL_SIZE - $queueSize; $i++) {
44+
$this->triggerTimeQueue[] = $now + ((1 + $i) * $this->period);
45+
}
46+
}
47+
}

src/Timer/Timer.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ abstract class Timer {
1414
/** @var callable[] */
1515
protected array $callbackList;
1616
/** @var callable Function that delivers the current time in milliseconds as a float */
17-
private $timeFunction;
17+
protected $timeFunction;
1818

1919
public function __construct() {
2020
$this->triggerTimeQueue = [];

test/phpunit/Timer/IndividualTimerTest.php

Lines changed: 25 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,39 +7,46 @@
77
class IndividualTimerTest extends TestCase {
88
public function testConstructWithFutureTime() {
99
$epoch = microtime(true);
10-
$epochPlus50ms = $epoch + 0.05;
11-
$sut = new IndividualTimer($epochPlus50ms);
12-
self::assertEquals($epochPlus50ms, $sut->getNextRunTime());
10+
$epochPlus5s = $epoch + 5;
11+
$sut = new IndividualTimer(5);
12+
self::assertEquals(
13+
// The timer must be scheduled within one hundredth of a second of the expectation:
14+
round($epochPlus5s, 2),
15+
round($sut->getNextRunTime(), 2)
16+
);
1317
}
1418

1519
public function testConstructWithPastTime() {
1620
$epoch = microtime(true);
17-
$epochMinus50ms = $epoch - 0.05;
18-
$sut = new IndividualTimer($epochMinus50ms);
19-
self::assertEquals($epochMinus50ms, $sut->getNextRunTime());
21+
$epochMinus5s = $epoch - 5;
22+
$sut = new IndividualTimer(-5);
23+
self::assertEquals(
24+
round($epochMinus5s, 2),
25+
round($sut->getNextRunTime(), 2)
26+
);
2027
}
2128

2229
public function testTickWithFutureTime() {
23-
$sut = new IndividualTimer(microtime(true) + 1);
30+
$sut = new IndividualTimer(100);
2431
self::assertFalse($sut->tick());
2532
}
2633

2734
public function testTickWithPastTime() {
28-
$sut = new IndividualTimer(microtime(true) - 1);
35+
$sut = new IndividualTimer(-100);
2936
self::assertTrue($sut->tick());
3037
}
3138

3239
public function testIsScheduledFalseAfterRunning() {
33-
$sut = new IndividualTimer(microtime(true));
40+
$sut = new IndividualTimer(0);
3441
self::assertTrue($sut->isScheduled());
3542
$sut->tick();
3643
self::assertFalse($sut->isScheduled());
3744
}
3845

3946
public function testIsScheduledTrueAfterRunningMultipleScheduled() {
40-
$sut = new IndividualTimer(microtime(true) + 1);
41-
$sut->addTriggerTime(microtime(true) + 2);
42-
$sut->addTriggerTime(microtime(true) + 3);
47+
$sut = new IndividualTimer(1);
48+
$sut->addTriggerTime(2);
49+
$sut->addTriggerTime(3);
4350
self::assertTrue($sut->isScheduled());
4451
$sut->tick();
4552
$sut->tick();
@@ -51,8 +58,9 @@ public function testIsScheduledTrueAfterRunningMultipleScheduled() {
5158
public function testIsScheduledFalseAfterRunningMultipleScheduledAndTimeAdvances() {
5259
$epoch = 1000;
5360

54-
$sut = new IndividualTimer(1001);
61+
$sut = new IndividualTimer();
5562
$sut->setTimeFunction(function() use(&$epoch) { return ++$epoch; });
63+
$sut->addTriggerTime(1001);
5664
$sut->addTriggerTime(1002);
5765
$sut->addTriggerTime(1003);
5866
self::assertTrue($sut->isScheduled());
@@ -68,9 +76,7 @@ public function testIsScheduledFalseAfterRunningMultipleScheduledAndTimeAdvances
6876
public function testAddCallbackNotTriggeredInFuture() {
6977
$callbackCount = 0;
7078

71-
$epoch = 1000;
72-
$sut = new IndividualTimer(1001);
73-
$sut->setTimeFunction(fn() => $epoch);
79+
$sut = new IndividualTimer(1);
7480
$sut->addCallback(function() use(&$callbackCount) {
7581
$callbackCount++;
7682
});
@@ -81,9 +87,7 @@ public function testAddCallbackNotTriggeredInFuture() {
8187
public function testAddCallbackTriggeredInPast() {
8288
$callbackCount = 0;
8389

84-
$epoch = 1000;
85-
$sut = new IndividualTimer(999);
86-
$sut->setTimeFunction(fn() => $epoch);
90+
$sut = new IndividualTimer(0);
8791
$sut->addCallback(function() use(&$callbackCount) {
8892
$callbackCount++;
8993
});
@@ -95,7 +99,8 @@ public function testAddCallbackTriggeredAtCorrectTime() {
9599
$callbackCount = 0;
96100

97101
$epoch = 1000;
98-
$sut = new IndividualTimer($epoch);
102+
$sut = new IndividualTimer();
103+
$sut->addTriggerTime($epoch);
99104
$sut->addTriggerTime($epoch + 1);
100105
$sut->addTriggerTime($epoch + 2);
101106
$sut->addTriggerTime($epoch + 5);
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
<?php
2+
namespace Gt\Async\Test\Timer;
3+
4+
use Gt\Async\Timer\PeriodicTimer;
5+
use PHPUnit\Framework\TestCase;
6+
7+
class PeriodicTimerTest extends TestCase {
8+
public function testIsScheduled() {
9+
$sut = new PeriodicTimer(10);
10+
self::assertTrue($sut->isScheduled());
11+
}
12+
13+
public function testGetNextRunTime() {
14+
$epoch = 1000;
15+
$period = 1;
16+
17+
$sut = new PeriodicTimer($period);
18+
$sut->setTimeFunction(function() use(&$epoch) {
19+
return $epoch++;
20+
});
21+
22+
self::assertEquals(
23+
$epoch + $period,
24+
$sut->getNextRunTime()
25+
);
26+
}
27+
28+
public function testGetNextRunTimeDelayedAtStart() {
29+
$epoch = 1000;
30+
$period = 1;
31+
$sut = new PeriodicTimer($period);
32+
$sut->setTimeFunction(fn() => $epoch);
33+
$nextRunTime = $sut->getNextRunTime();
34+
self::assertEquals($epoch + $period, $nextRunTime);
35+
}
36+
37+
public function testGetNextRunTimeDelayedAtStartLongPeriod() {
38+
$epoch = 1000;
39+
$period = 100;
40+
$sut = new PeriodicTimer($period);
41+
$sut->setTimeFunction(fn() => $epoch);
42+
$nextRunTime = $sut->getNextRunTime();
43+
self::assertEquals($epoch + $period, $nextRunTime);
44+
}
45+
46+
public function testGetNextRunTimeImmediate() {
47+
$epoch = 1000;
48+
$period = 1;
49+
$sut = new PeriodicTimer($period, true);
50+
$sut->setTimeFunction(fn() => $epoch);
51+
$nextRunTime = $sut->getNextRunTime();
52+
self::assertEquals($epoch, $nextRunTime);
53+
}
54+
55+
public function testGetNextRunTimeImmediateMultipleTicks() {
56+
$epochStart = 1000;
57+
$epoch = $epochStart;
58+
59+
$sut = new PeriodicTimer(1, true);
60+
$sut->setTimeFunction(function() use(&$epoch) {
61+
return $epoch;
62+
});
63+
64+
for($i = 0; $i < 100; $i++) {
65+
$nextRunTime = $sut->getNextRunTime();
66+
self::assertEquals(
67+
$epochStart + $i,
68+
$nextRunTime
69+
);
70+
$sut->tick();
71+
$epoch++;
72+
}
73+
}
74+
}

0 commit comments

Comments
 (0)