Skip to content

Commit 0887374

Browse files
author
Greg Bowler
committed
Simplify loop's responsibility
1 parent 9d9cc5c commit 0887374

6 files changed

Lines changed: 147 additions & 100 deletions

File tree

src/Loop.php

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,23 @@ class Loop {
1515
/** @var Timer[] */
1616
private array $timerList;
1717
private int $triggerCount;
18+
/** @var callable Function that delays execution by (int $milliseconds) */
19+
private $sleepFunction;
1820

1921
public function __construct() {
2022
$this->timerList = [];
2123
$this->triggerCount = 0;
24+
$this->sleepFunction = "usleep";
2225
}
2326

2427
public function addTimer(Timer $timer):void {
2528
$this->timerList [] = $timer;
2629
}
2730

31+
public function setSleepFunction(callable $sleepFunction):void {
32+
$this->sleepFunction = $sleepFunction;
33+
}
34+
2835
public function run():void {
2936
do {
3037
$numTriggered = $this->triggerNextTimers();
@@ -44,7 +51,10 @@ public function waitUntil(float $waitUntilEpoch):void {
4451
return;
4552
}
4653

47-
usleep($diff * 1_000_000);
54+
call_user_func(
55+
$this->sleepFunction,
56+
$diff * 1_000_000
57+
);
4858
}
4959

5060
private function triggerNextTimers():int {

src/Timer/IndividualTimer.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
namespace Gt\Async\Timer;
3+
4+
class IndividualTimer extends Timer {
5+
public function __construct(float $triggerTime) {
6+
parent::__construct();
7+
$this->addTriggerTime($triggerTime);
8+
}
9+
10+
public function addTriggerTime(float $triggerTime):void {
11+
$this->triggerTimeQueue[] = $triggerTime;
12+
sort($this->triggerTimeQueue);
13+
}
14+
}

src/Timer/OnceTimer.php

Lines changed: 0 additions & 14 deletions
This file was deleted.

src/Timer/Timer.php

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,56 @@
11
<?php
22
namespace Gt\Async\Timer;
33

4+
/**
5+
* Represents one or more trigger times. If the tick function is called when
6+
* a timer is due, it will execute the timer's callback(s).
7+
*
8+
* The Timer's tick function could be called in an infinite loop, or for better
9+
* use of CPU cycles, sleep for the duration until the Timer's next run time.
10+
*/
411
abstract class Timer {
5-
abstract public function tick():void;
6-
abstract public function getNextRunTime():?float;
12+
protected array $triggerTimeQueue;
13+
protected array $callbackList;
14+
15+
public function __construct() {
16+
$this->triggerTimeQueue = [];
17+
$this->callbackList = [];
18+
}
19+
20+
public function addCallback(callable $callback):void {
21+
$this->callbackList[] = $callback;
22+
}
23+
24+
public function getNextRunTime():?float {
25+
return $this->triggerTimeQueue[0] ?? null;
26+
}
27+
28+
/**
29+
* @return bool True if the timer ticks (it was due). False if the tick
30+
* doesn't occur (is was not due).
31+
*/
32+
public function tick():bool {
33+
$now = microtime(true);
34+
35+
do {
36+
$triggerTime = $this->triggerTimeQueue[0] ?? null;
37+
if(is_null($triggerTime)
38+
|| $triggerTime > $now) {
39+
return false;
40+
}
41+
42+
$this->executeCallbacks();
43+
array_shift($this->triggerTimeQueue);
44+
}
45+
while(isset($this->triggerTimeQueue[0])
46+
&& $this->triggerTimeQueue <= $now);
47+
48+
return true;
49+
}
50+
51+
private function executeCallbacks():void {
52+
foreach($this->callbackList as $callback) {
53+
call_user_func($callback);
54+
}
55+
}
756
}

test/phpunit/LoopTest.php

Lines changed: 41 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -13,126 +13,84 @@ public function testRunWithNoTimer() {
1313
}
1414

1515
public function testWaitUntil() {
16+
$actualDelay = null;
17+
1618
$sut = new Loop();
17-
$epoch = microtime(true);
18-
$epochPlus50ms = $epoch + 0.05;
19-
$epochPlus51ms = $epoch + 0.051;
19+
$sut->setSleepFunction(function(int $milliseconds) use (&$actualDelay) {
20+
$actualDelay = $milliseconds;
21+
});
2022

21-
$sut->waitUntil($epochPlus50ms);
22-
$epochAfter = microtime(true);
23-
self::assertGreaterThanOrEqual($epochPlus50ms, $epochAfter);
24-
self::assertLessThan($epochPlus51ms, $epochAfter);
23+
$epoch = microtime(true);
24+
$epochPlus5s = $epoch + 5;
25+
$sut->waitUntil($epochPlus5s);
26+
self::assertEquals(
27+
round(5_000_000 / 100),
28+
// Check that the delayed time is within a threshold of 1/10,000 of a second:
29+
round($actualDelay / 100)
30+
);
2531
}
2632

2733
public function testWaitUntilNegative() {
34+
$numCalls = 0;
35+
2836
$sut = new Loop();
37+
$sut->setSleepFunction(function(int $milliseconds) use (&$numCalls) {
38+
$numCalls++;
39+
});
40+
2941
$epoch = microtime(true);
30-
$epochMinus50ms = $epoch - 0.05;
31-
$tolerance = 0.001; // There should be less than a 0.001s delay.
42+
$epochMinus5s = $epoch - 5;
3243

33-
$sut->waitUntil($epochMinus50ms);
34-
$epochAfter = microtime(true);
35-
self::assertLessThan($tolerance, $epochAfter - $epoch);
44+
// Because the delay time is in the past, the sleep function should never be called.
45+
$sut->waitUntil($epochMinus5s);
46+
self::assertEquals(0, $numCalls);
3647
}
3748

3849
public function testRunWithTimer() {
3950
$epoch = microtime(true);
40-
$epochIn10milliseconds = $epoch + 0.01;
4151
$timer = self::createMock(Timer::class);
4252
$timer->method("getNextRunTime")
4353
->willReturn(
44-
$epochIn10milliseconds,
54+
$epoch + 1,
4555
null
4656
);
4757

4858
$sut = new Loop();
59+
$sut->setSleepFunction(function() {});
4960
$sut->addTimer($timer);
5061
$sut->run();
5162

5263
self::assertEquals(1, $sut->getTriggerCount());
5364
}
5465

55-
public function testRunWithTimerNoNextRunTime() {
66+
public function testRunWithTimerMultiple() {
67+
$epoch = microtime(true);
5668
$timer = self::createMock(Timer::class);
5769
$timer->method("getNextRunTime")
58-
->willReturn(null);
70+
->willReturn(
71+
$epoch + 1,
72+
$epoch + 2,
73+
$epoch + 3,
74+
null
75+
);
5976

6077
$sut = new Loop();
78+
$sut->setSleepFunction(function() {});
6179
$sut->addTimer($timer);
6280
$sut->run();
6381

64-
self::assertEquals(0, $sut->getTriggerCount());
65-
}
66-
67-
public function testRunWithTimerThatTakesLongerThanNextTimerDueTime() {
68-
$timerCallbacks = [];
69-
70-
$epoch = microtime(true);
71-
$epochPlus10ms = $epoch + 0.01;
72-
$epochPlus20ms = $epoch + 0.02;
73-
$timer1 = self::createMock(Timer::class);
74-
$timer1->method("getNextRunTime")
75-
->willReturn($epochPlus10ms, null);
76-
$timer1->method("tick")
77-
->willReturnCallback(function() use (&$timerCallbacks) {
78-
// We are waiting for a tenth of a second,
79-
// which will be longer than the timer2's due time.
80-
usleep(0.5 * 1_000_000);
81-
$timerCallbacks[] = "timer1";
82-
});
83-
84-
$timer2 = self::createMock(Timer::class);
85-
$timer2->method("getNextRunTime")
86-
->willReturn($epochPlus20ms, null);
87-
$timer2->method("tick")
88-
->willReturnCallback(function() use (&$timerCallbacks) {
89-
$timerCallbacks[] = "timer2";
90-
});
91-
92-
$sut = new Loop();
93-
$sut->addTimer($timer1);
94-
$sut->addTimer($timer2);
95-
$sut->run();
96-
97-
self::assertEquals(2, $sut->getTriggerCount());
98-
// The order of the timer callbacks should be 1 first.
99-
self::assertEquals("timer1", $timerCallbacks[0]);
100-
self::assertEquals("timer2", $timerCallbacks[1]);
82+
self::assertEquals(3, $sut->getTriggerCount());
10183
}
10284

103-
public function testRunWithTimerThatTakesLongerThanNextTimerDueTimeOutOfOrder() {
104-
$timerCallbacks = [];
105-
106-
$epoch = microtime(true);
107-
$epochPlus10ms = $epoch + 0.01;
108-
$epochPlus20ms = $epoch + 0.02;
109-
$timer1 = self::createMock(Timer::class);
110-
$timer1->method("getNextRunTime")
111-
->willReturn($epochPlus10ms, null);
112-
$timer1->method("tick")
113-
->willReturnCallback(function() use (&$timerCallbacks) {
114-
usleep(0.5 * 1_000_000);
115-
$timerCallbacks[] = "timer1";
116-
});
117-
118-
$timer2 = self::createMock(Timer::class);
119-
$timer2->method("getNextRunTime")
120-
->willReturn($epochPlus20ms, null);
121-
$timer2->method("tick")
122-
->willReturnCallback(function() use (&$timerCallbacks) {
123-
$timerCallbacks[] = "timer2";
124-
});
85+
public function testRunWithTimerNoNextRunTime() {
86+
$timer = self::createMock(Timer::class);
87+
$timer->method("getNextRunTime")
88+
->willReturn(null);
12589

12690
$sut = new Loop();
127-
// Here we add the later timer first. Below we will make sure the order
128-
// is still correct.
129-
$sut->addTimer($timer2);
130-
$sut->addTimer($timer1);
91+
$sut->addTimer($timer);
13192
$sut->run();
13293

133-
self::assertEquals(2, $sut->getTriggerCount());
134-
// The order of the timer callbacks should be 1 first.
135-
self::assertEquals("timer1", $timerCallbacks[0]);
136-
self::assertEquals("timer2", $timerCallbacks[1]);
94+
self::assertEquals(0, $sut->getTriggerCount());
13795
}
13896
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
namespace Gt\Async\Test\Timer;
3+
4+
use Gt\Async\Timer\IndividualTimer;
5+
6+
class IndividualTimerTest extends \PHPUnit\Framework\TestCase {
7+
public function testConstructWithFutureTime() {
8+
$epoch = microtime(true);
9+
$epochPlus50ms = $epoch + 0.05;
10+
$sut = new IndividualTimer($epochPlus50ms);
11+
self::assertEquals($epochPlus50ms, $sut->getNextRunTime());
12+
}
13+
14+
public function testConstructWithPastTime() {
15+
$epoch = microtime(true);
16+
$epochMinus50ms = $epoch - 0.05;
17+
$sut = new IndividualTimer($epochMinus50ms);
18+
self::assertEquals($epochMinus50ms, $sut->getNextRunTime());
19+
}
20+
21+
public function testTickWithFutureTime() {
22+
$sut = new IndividualTimer(microtime(true) + 1);
23+
self::assertFalse($sut->tick());
24+
}
25+
26+
public function testTickWithPastTime() {
27+
$sut = new IndividualTimer(microtime(true) - 1);
28+
self::assertTrue($sut->tick());
29+
}
30+
}

0 commit comments

Comments
 (0)