Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve performance, add internal Clock, reuse clock in same tick #457

Merged
merged 1 commit into from May 20, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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));
}
}