Skip to content

Commit 55ec42a

Browse files
authored
Merge pull request #457 from clue-labs/clock
Improve performance, add internal `Clock`, reuse clock in same tick
2 parents 4862e84 + 9c2d98f commit 55ec42a

File tree

7 files changed

+254
-63
lines changed

7 files changed

+254
-63
lines changed

src/Io/Clock.php

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?php
2+
3+
namespace React\Http\Io;
4+
5+
use React\EventLoop\LoopInterface;
6+
7+
/**
8+
* [internal] Clock source that returns current timestamp and memoize clock for same tick
9+
*
10+
* This is mostly used as an internal optimization to avoid unneeded syscalls to
11+
* get the current system time multiple times within the same loop tick. For the
12+
* purpose of the HTTP server, the clock is assumed to not change to a
13+
* significant degree within the same loop tick. If you need a high precision
14+
* clock source, you may want to use `\hrtime()` instead (PHP 7.3+).
15+
*
16+
* The API is modelled to resemble the PSR-20 `ClockInterface` (in draft at the
17+
* time of writing this), but uses a `float` return value for performance
18+
* reasons instead.
19+
*
20+
* Note that this is an internal class only and nothing you should usually care
21+
* about for outside use.
22+
*
23+
* @internal
24+
*/
25+
class Clock
26+
{
27+
/** @var LoopInterface $loop */
28+
private $loop;
29+
30+
/** @var ?float */
31+
private $now;
32+
33+
public function __construct(LoopInterface $loop)
34+
{
35+
$this->loop = $loop;
36+
}
37+
38+
/** @return float */
39+
public function now()
40+
{
41+
if ($this->now === null) {
42+
$this->now = \microtime(true);
43+
44+
// remember clock for current loop tick only and update on next tick
45+
$now =& $this->now;
46+
$this->loop->futureTick(function () use (&$now) {
47+
assert($now !== null);
48+
$now = null;
49+
});
50+
}
51+
52+
return $this->now;
53+
}
54+
}

src/Io/RequestHeaderParser.php

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,14 @@ class RequestHeaderParser extends EventEmitter
2424
{
2525
private $maxSize = 8192;
2626

27+
/** @var Clock */
28+
private $clock;
29+
30+
public function __construct(Clock $clock)
31+
{
32+
$this->clock = $clock;
33+
}
34+
2735
public function handle(ConnectionInterface $conn)
2836
{
2937
$buffer = '';
@@ -155,8 +163,8 @@ public function parseRequest($headers, $remoteSocketUri, $localSocketUri)
155163
// create new obj implementing ServerRequestInterface by preserving all
156164
// previous properties and restoring original request-target
157165
$serverParams = array(
158-
'REQUEST_TIME' => \time(),
159-
'REQUEST_TIME_FLOAT' => \microtime(true)
166+
'REQUEST_TIME' => (int) ($now = $this->clock->now()),
167+
'REQUEST_TIME_FLOAT' => $now
160168
);
161169

162170
// scheme is `http` unless TLS is used

src/Io/StreamingServer.php

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,9 @@ final class StreamingServer extends EventEmitter
8484
{
8585
private $callback;
8686
private $parser;
87-
private $loop;
87+
88+
/** @var Clock */
89+
private $clock;
8890

8991
/**
9092
* Creates an HTTP server that invokes the given callback for each incoming HTTP request
@@ -104,10 +106,9 @@ public function __construct(LoopInterface $loop, $requestHandler)
104106
throw new \InvalidArgumentException('Invalid request handler given');
105107
}
106108

107-
$this->loop = $loop;
108-
109109
$this->callback = $requestHandler;
110-
$this->parser = new RequestHeaderParser();
110+
$this->clock = new Clock($loop);
111+
$this->parser = new RequestHeaderParser($this->clock);
111112

112113
$that = $this;
113114
$this->parser->on('headers', function (ServerRequestInterface $request, ConnectionInterface $conn) use ($that) {
@@ -255,7 +256,7 @@ public function handleResponse(ConnectionInterface $connection, ServerRequestInt
255256
// assign default "Date" header from current time automatically
256257
if (!$response->hasHeader('Date')) {
257258
// IMF-fixdate = day-name "," SP date1 SP time-of-day SP GMT
258-
$response = $response->withHeader('Date', gmdate('D, d M Y H:i:s') . ' GMT');
259+
$response = $response->withHeader('Date', gmdate('D, d M Y H:i:s', (int) $this->clock->now()) . ' GMT');
259260
} elseif ($response->getHeaderLine('Date') === ''){
260261
$response = $response->withoutHeader('Date');
261262
}

tests/HttpServerTest.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,13 @@ public function testConstructWithoutLoopAssignsLoopAutomatically()
5454
$ref->setAccessible(true);
5555
$streamingServer = $ref->getValue($http);
5656

57-
$ref = new \ReflectionProperty($streamingServer, 'loop');
57+
$ref = new \ReflectionProperty($streamingServer, 'clock');
5858
$ref->setAccessible(true);
59-
$loop = $ref->getValue($streamingServer);
59+
$clock = $ref->getValue($streamingServer);
60+
61+
$ref = new \ReflectionProperty($clock, 'loop');
62+
$ref->setAccessible(true);
63+
$loop = $ref->getValue($clock);
6064

6165
$this->assertInstanceOf('React\EventLoop\LoopInterface', $loop);
6266
}

tests/Io/ClockTest.php

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php
2+
3+
namespace React\Tests\Http\Io;
4+
5+
use PHPUnit\Framework\TestCase;
6+
use React\Http\Io\Clock;
7+
8+
class ClockTest extends TestCase
9+
{
10+
public function testNowReturnsSameTimestampMultipleTimesInSameTick()
11+
{
12+
$loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
13+
14+
$clock = new Clock($loop);
15+
16+
$now = $clock->now();
17+
$this->assertTrue(is_float($now)); // assertIsFloat() on PHPUnit 8+
18+
$this->assertEquals($now, $clock->now());
19+
}
20+
21+
public function testNowResetsMemoizedTimestampOnFutureTick()
22+
{
23+
$tick = null;
24+
$loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
25+
$loop->expects($this->once())->method('futureTick')->with($this->callback(function ($cb) use (&$tick) {
26+
$tick = $cb;
27+
return true;
28+
}));
29+
30+
$clock = new Clock($loop);
31+
32+
$now = $clock->now();
33+
34+
$ref = new \ReflectionProperty($clock, 'now');
35+
$ref->setAccessible(true);
36+
$this->assertEquals($now, $ref->getValue($clock));
37+
38+
$this->assertNotNull($tick);
39+
$tick();
40+
41+
$this->assertNull($ref->getValue($clock));
42+
}
43+
}

0 commit comments

Comments
 (0)