Skip to content

Commit

Permalink
Merge pull request #457 from clue-labs/clock
Browse files Browse the repository at this point in the history
Improve performance, add internal `Clock`, reuse clock in same tick
  • Loading branch information
WyriHaximus committed May 20, 2022
2 parents 4862e84 + 9c2d98f commit 55ec42a
Show file tree
Hide file tree
Showing 7 changed files with 254 additions and 63 deletions.
54 changes: 54 additions & 0 deletions src/Io/Clock.php
@@ -0,0 +1,54 @@
<?php

namespace React\Http\Io;

use React\EventLoop\LoopInterface;

/**
* [internal] Clock source that returns current timestamp and memoize clock for same tick
*
* This is mostly used as an internal optimization to avoid unneeded syscalls to
* get the current system time multiple times within the same loop tick. For the
* purpose of the HTTP server, the clock is assumed to not change to a
* significant degree within the same loop tick. If you need a high precision
* clock source, you may want to use `\hrtime()` instead (PHP 7.3+).
*
* The API is modelled to resemble the PSR-20 `ClockInterface` (in draft at the
* time of writing this), but uses a `float` return value for performance
* reasons instead.
*
* Note that this is an internal class only and nothing you should usually care
* about for outside use.
*
* @internal
*/
class Clock
{
/** @var LoopInterface $loop */
private $loop;

/** @var ?float */
private $now;

public function __construct(LoopInterface $loop)
{
$this->loop = $loop;
}

/** @return float */
public function now()
{
if ($this->now === null) {
$this->now = \microtime(true);

// remember clock for current loop tick only and update on next tick
$now =& $this->now;
$this->loop->futureTick(function () use (&$now) {
assert($now !== null);
$now = null;
});
}

return $this->now;
}
}
12 changes: 10 additions & 2 deletions src/Io/RequestHeaderParser.php
Expand Up @@ -24,6 +24,14 @@ class RequestHeaderParser extends EventEmitter
{
private $maxSize = 8192;

/** @var Clock */
private $clock;

public function __construct(Clock $clock)
{
$this->clock = $clock;
}

public function handle(ConnectionInterface $conn)
{
$buffer = '';
Expand Down Expand Up @@ -155,8 +163,8 @@ public function parseRequest($headers, $remoteSocketUri, $localSocketUri)
// create new obj implementing ServerRequestInterface by preserving all
// previous properties and restoring original request-target
$serverParams = array(
'REQUEST_TIME' => \time(),
'REQUEST_TIME_FLOAT' => \microtime(true)
'REQUEST_TIME' => (int) ($now = $this->clock->now()),
'REQUEST_TIME_FLOAT' => $now
);

// scheme is `http` unless TLS is used
Expand Down
11 changes: 6 additions & 5 deletions src/Io/StreamingServer.php
Expand Up @@ -84,7 +84,9 @@ final class StreamingServer extends EventEmitter
{
private $callback;
private $parser;
private $loop;

/** @var Clock */
private $clock;

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

$this->loop = $loop;

$this->callback = $requestHandler;
$this->parser = new RequestHeaderParser();
$this->clock = new Clock($loop);
$this->parser = new RequestHeaderParser($this->clock);

$that = $this;
$this->parser->on('headers', function (ServerRequestInterface $request, ConnectionInterface $conn) use ($that) {
Expand Down Expand Up @@ -255,7 +256,7 @@ public function handleResponse(ConnectionInterface $connection, ServerRequestInt
// assign default "Date" header from current time automatically
if (!$response->hasHeader('Date')) {
// IMF-fixdate = day-name "," SP date1 SP time-of-day SP GMT
$response = $response->withHeader('Date', gmdate('D, d M Y H:i:s') . ' GMT');
$response = $response->withHeader('Date', gmdate('D, d M Y H:i:s', (int) $this->clock->now()) . ' GMT');
} elseif ($response->getHeaderLine('Date') === ''){
$response = $response->withoutHeader('Date');
}
Expand Down
8 changes: 6 additions & 2 deletions tests/HttpServerTest.php
Expand Up @@ -54,9 +54,13 @@ public function testConstructWithoutLoopAssignsLoopAutomatically()
$ref->setAccessible(true);
$streamingServer = $ref->getValue($http);

$ref = new \ReflectionProperty($streamingServer, 'loop');
$ref = new \ReflectionProperty($streamingServer, 'clock');
$ref->setAccessible(true);
$loop = $ref->getValue($streamingServer);
$clock = $ref->getValue($streamingServer);

$ref = new \ReflectionProperty($clock, 'loop');
$ref->setAccessible(true);
$loop = $ref->getValue($clock);

$this->assertInstanceOf('React\EventLoop\LoopInterface', $loop);
}
Expand Down
43 changes: 43 additions & 0 deletions tests/Io/ClockTest.php
@@ -0,0 +1,43 @@
<?php

namespace React\Tests\Http\Io;

use PHPUnit\Framework\TestCase;
use React\Http\Io\Clock;

class ClockTest extends TestCase
{
public function testNowReturnsSameTimestampMultipleTimesInSameTick()
{
$loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();

$clock = new Clock($loop);

$now = $clock->now();
$this->assertTrue(is_float($now)); // assertIsFloat() on PHPUnit 8+
$this->assertEquals($now, $clock->now());
}

public function testNowResetsMemoizedTimestampOnFutureTick()
{
$tick = null;
$loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
$loop->expects($this->once())->method('futureTick')->with($this->callback(function ($cb) use (&$tick) {
$tick = $cb;
return true;
}));

$clock = new Clock($loop);

$now = $clock->now();

$ref = new \ReflectionProperty($clock, 'now');
$ref->setAccessible(true);
$this->assertEquals($now, $ref->getValue($clock));

$this->assertNotNull($tick);
$tick();

$this->assertNull($ref->getValue($clock));
}
}

0 comments on commit 55ec42a

Please sign in to comment.