1
0
mirror of https://github.com/danog/amp.git synced 2025-01-22 05:11:42 +01:00

Expose boolean AMP_DEBUG for performance tuning

Amp Future instances double both as Promisor and Promise
implementations when AMP_DEBUG is defined and set to false.
This switch allows private Promise resolution safety by
default at the expense of performance.

Amp applications should set AMP_DEBUG to false in production
environments to maximize performance.
This commit is contained in:
Daniel Lowrey 2015-05-18 23:57:34 -04:00
parent 0973bba5ef
commit 3af013d418
17 changed files with 225 additions and 242 deletions

View File

@ -2,112 +2,8 @@
namespace Amp;
class Future implements Promisor, Promise {
private $isResolved = false;
private $watchers = [];
private $whens = [];
private $error;
private $result;
/**
* Retrieve the Promise placeholder for this deferred value
*
* This implementation acts as both Promisor and Promise so we simply return the
* current instance. If users require a Promisor that can only be resolved by code
* holding a reference to the Promisor they may instead use Amp\PrivateFuture.
*
* @return \Amp\Promise
*/
public function promise() {
return $this;
}
/**
* {@inheritDoc}
*/
public function when(callable $func) {
if ($this->isResolved) {
$func($this->error, $this->result);
} else {
$this->whens[] = $func;
}
}
/**
* {@inheritDoc}
*/
public function watch(callable $func) {
if (!$this->isResolved) {
$this->watchers[] = $func;
}
}
/**
* {@inheritDoc}
* @throws \LogicException if the promise has already resolved
*/
public function update($progress) {
if ($this->isResolved) {
throw new \LogicException(
'Cannot update resolved promise'
);
}
foreach ($this->watchers as $watcher) {
$watcher($progress);
}
}
/**
* {@inheritDoc}
* @throws \LogicException if the promise has already resolved or the result is the current instance
*/
public function succeed($result = null) {
if ($this->isResolved) {
throw new \LogicException(
'Promise already resolved'
);
} elseif ($result === $this) {
throw new \LogicException(
'A Promise cannot act as its own resolution result'
);
} elseif ($result instanceof Promise) {
$result->when(function(\Exception $error = null, $result = null) {
if ($error) {
$this->fail($error);
} else {
$this->succeed($result);
}
});
} else {
$this->isResolved = true;
$this->result = $result;
$error = null;
foreach ($this->whens as $when) {
$when($error, $result);
}
$this->whens = $this->watchers = [];
}
}
/**
* {@inheritDoc}
* @throws \LogicException if the promise has already resolved
*/
public function fail(\Exception $error) {
if ($this->isResolved) {
throw new \LogicException(
'Promise already resolved'
);
}
$this->isResolved = true;
$this->error = $error;
$result = null;
foreach ($this->whens as $when) {
$when($error, $result);
}
$this->whens = $this->watchers = [];
}
if (!defined("AMP_DEBUG") || \AMP_DEBUG) {
final class Future implements Promisor { use PrivatePromisor; }
} else {
final class Future implements Promisor, Promise { use PublicPromisor; }
}

81
lib/Placeholder.php Normal file
View File

@ -0,0 +1,81 @@
<?php
namespace Amp;
/**
* A placeholder value that will be resolved at some point in the future by
* the Promisor that created it.
*/
trait Placeholder {
private $isResolved = false;
private $watchers = [];
private $whens = [];
private $error;
private $result;
/**
* Notify the $func callback when the promise resolves (whether successful or not)
*
* @param callable $func
* @return self
*/
public function when(callable $func) {
if ($this->isResolved) {
call_user_func($func, $this->error, $this->result);
} else {
$this->whens[] = $func;
}
return $this;
}
/**
* Notify the $func callback when resolution progress events are emitted
*
* @param callable $func
* @return self
*/
public function watch(callable $func) {
if (!$this->isResolved) {
$this->watchers[] = $func;
}
return $this;
}
private function update($progress) {
if ($this->isResolved) {
throw new \LogicException(
"Cannot update resolved promise"
);
}
foreach ($this->watchers as $watcher) {
call_user_func($watcher, $progress);
}
}
private function resolve(\Exception $error = null, $result = null) {
if ($this->isResolved) {
throw new \LogicException(
"Promise already resolved"
);
} elseif ($result === $this) {
throw new \LogicException(
"A Promise cannot act as its own resolution result"
);
} elseif ($result instanceof Promise) {
$result->when(function($error, $result) {
$this->resolve($error, $result);
});
} else {
$this->isResolved = true;
$this->error = $error;
$this->result = $result;
foreach ($this->whens as $when) {
call_user_func($when, $error, $result);
}
$this->whens = $this->watchers = [];
}
}
}

View File

@ -3,12 +3,11 @@
namespace Amp;
/**
* A PrivateFuture creates a read-only Promise that may *only* be fulfilled by holders of the
* actual PrivateFuture instance. This provides an additional layer of API protection over
* the standard Future Promisor implementation whose Promise can be resolved by any code
* holding a reference to the Future instance.
* A PrivatePromisor creates read-only Promise instances that can only be
* resolved by holders of the PrivatePromisor instance. This creates an
* additional layer of API protection beyond the PublicPromisor.
*/
class PrivateFuture implements Promisor {
trait PrivatePromisor {
private $resolver;
private $updater;
private $promise;
@ -27,7 +26,7 @@ class PrivateFuture implements Promisor {
}
/**
* Promise future fulfillment via a temporary placeholder value
* Promise future fulfillment of the returned placeholder value
*
* @return \Amp\Promise
*/
@ -46,7 +45,7 @@ class PrivateFuture implements Promisor {
}
/**
* Resolve the promised value as a success
* Resolve the associated promise placeholder as a success
*
* @param mixed $result
* @return void
@ -56,7 +55,7 @@ class PrivateFuture implements Promisor {
}
/**
* Resolve the promised value as a failure
* Resolve the associated promise placeholder as a failure
*
* @param \Exception $error
* @return void

View File

@ -16,10 +16,10 @@ class PromiseStream {
*/
public function __construct(Promise $watchedPromise) {
$this->state = self::WAIT;
$this->promisors[] = new PrivateFuture;
$this->promisors[] = new Future;
$watchedPromise->watch(function($data) {
$this->state = self::NOTIFY;
$this->promisors[$this->index + 1] = new PrivateFuture;
$this->promisors[$this->index + 1] = new Future;
$this->promisors[$this->index++]->succeed($data);
});
$watchedPromise->when(function($error, $result) {

56
lib/PublicPromisor.php Normal file
View File

@ -0,0 +1,56 @@
<?php
namespace Amp;
trait PublicPromisor {
use Placeholder;
/**
* Promise future fulfillment via a temporary placeholder value
*
* This implementation acts as both Promisor and Promise so we simply return the
* current instance.
*
* @return \Amp\Promise
*/
public function promise() {
return $this;
}
/**
* Update watchers of progress resolving the promised value
*
* @param mixed $progress
* @return void
*/
public function update($progress) {
if ($this->isResolved) {
throw new \LogicException(
'Cannot update resolved promise'
);
}
foreach ($this->watchers as $watcher) {
$watcher($progress);
}
}
/**
* Resolve the promised value as a success
*
* @param mixed $result
* @return void
*/
public function succeed($result = null) {
return $this->resolve($error = null, $result);
}
/**
* Resolve the promised value as a failure
*
* @return void
*/
public function fail(\Exception $error) {
return $this->resolve($error, $result = null);
}
}

View File

@ -7,75 +7,5 @@ namespace Amp;
* the Promisor that created it.
*/
class Unresolved implements Promise {
private $isResolved = false;
private $watchers = [];
private $whens = [];
private $error;
private $result;
/**
* Notify the $func callback when the promise resolves (whether successful or not)
*
* @param callable $func
* @return self
*/
public function when(callable $func) {
if ($this->isResolved) {
call_user_func($func, $this->error, $this->result);
} else {
$this->whens[] = $func;
}
return $this;
}
/**
* Notify the $func callback when resolution progress events are emitted
*
* @param callable $func
* @return self
*/
public function watch(callable $func) {
if (!$this->isResolved) {
$this->watchers[] = $func;
}
return $this;
}
private function resolve(\Exception $error = null, $result = null) {
if ($this->isResolved) {
throw new \LogicException(
"Promise already resolved"
);
} elseif ($result === $this) {
throw new \LogicException(
"A Promise cannot act as its own resolution result"
);
} elseif ($result instanceof Promise) {
$result->when(function($error, $result) {
$this->resolve($error, $result);
});
} else {
$this->isResolved = true;
$this->error = $error;
$this->result = $result;
foreach ($this->whens as $when) {
call_user_func($when, $error, $result);
}
$this->whens = $this->watchers = [];
}
}
private function update($progress) {
if ($this->isResolved) {
throw new \LogicException(
"Cannot update resolved promise"
);
}
foreach ($this->watchers as $watcher) {
call_user_func($watcher, $progress);
}
}
use Placeholder;
}

View File

@ -189,9 +189,7 @@ function all(array $promises) {
});
}
// We can return $promisor directly because the Future Promisor implementation
// also implements Promise for convenience
return $promisor;
return $promisor->promise();
}
/**
@ -243,9 +241,7 @@ function some(array $promises) {
});
}
// We can return $promisor directly because the Future Promisor implementation
// also implements Promise for convenience
return $promisor;
return $promisor->promise();
}
/**
@ -282,9 +278,7 @@ function any(array $promises) {
});
}
// We can return $promisor directly because the Future Promisor implementation
// also implements Promise for convenience
return $promisor;
return $promisor->promise();
}
/**
@ -323,9 +317,7 @@ function first(array $promises) {
});
}
// We can return $promisor directly because the Future Promisor implementation
// also implements Promise for convenience
return $promisor;
return $promisor->promise();
}
/**
@ -365,9 +357,7 @@ function map(array $promises, callable $functor) {
});
}
// We can return $promisor directly because the Future Promisor implementation
// also implements Promise for convenience
return $promisor;
return $promisor->promise();
}
/**
@ -410,9 +400,7 @@ function filter(array $promises, callable $functor) {
});
}
// We can return $promisor directly because the Future Promisor implementation
// also implements Promise for convenience
return $promisor;
return $promisor->promise();
}
/**

View File

@ -1,4 +1,4 @@
<phpunit bootstrap="./vendor/autoload.php" colors="true">
<phpunit bootstrap="./test/bootstrap.php" colors="true">
<testsuites>
<testsuite name="Tests">
<directory>./test</directory>

View File

@ -5,7 +5,7 @@ namespace Amp\Test;
use Amp\NativeReactor;
use Amp\Success;
use Amp\Failure;
use Amp\PrivateFuture;
use Amp\Future;
use Amp\PromiseStream;
class FunctionsTest extends \PHPUnit_Framework_TestCase {
@ -210,7 +210,7 @@ class FunctionsTest extends \PHPUnit_Framework_TestCase {
public function testCoroutineResolutionBuffersYieldedPromiseStream() {
(new NativeReactor)->run(function($reactor) {
$promisor = new PrivateFuture;
$promisor = new Future;
$reactor->repeat(function($reactor, $watcherId) use (&$i, $promisor) {
$i++;
$promisor->update($i);
@ -231,7 +231,7 @@ class FunctionsTest extends \PHPUnit_Framework_TestCase {
*/
public function testCoroutineResolutionThrowsOnPromiseStreamBufferFailure() {
(new NativeReactor)->run(function($reactor) {
$promisor = new PrivateFuture;
$promisor = new Future;
$reactor->repeat(function($reactor, $watcherId) use (&$i, $promisor) {
$promisor->fail(new \Exception("test"));
}, 10);

View File

@ -0,0 +1,9 @@
<?php
namespace Amp\Test;
class PlaceholderPrivateTest extends PlaceholderTest {
protected function getPromisor() {
return new PromisorPrivateImpl;
}
}

View File

@ -0,0 +1,9 @@
<?php
namespace Amp\Test;
class PlaceholderPublicTest extends PlaceholderTest {
protected function getPromisor() {
return new PromisorPublicImpl;
}
}

View File

@ -2,13 +2,13 @@
namespace Amp\Test;
use Amp\PrivateFuture;
use Amp\Future;
use Amp\NativeReactor;
class UnresolvedTest extends \PHPUnit_Framework_TestCase {
abstract class PlaceholderTest {
abstract protected function getPromisor();
public function testWatchInvokesCallbackWithResultIfAlreadySucceeded() {
$promisor = new PrivateFuture;
$promisor = $this->getPromisor();
$promise = $promisor->promise();
$promisor->succeed(42);
$promise->watch(function($p, $e, $r) {
@ -19,7 +19,7 @@ class UnresolvedTest extends \PHPUnit_Framework_TestCase {
}
public function testWatchInvokesCallbackWithErrorIfAlreadyFailed() {
$promisor = new PrivateFuture;
$promisor = $this->getPromisor();
$promise = $promisor->promise();
$exception = new \Exception('test');
$promisor->fail($exception);
@ -35,7 +35,7 @@ class UnresolvedTest extends \PHPUnit_Framework_TestCase {
* @expectedExceptionMessage Promise already resolved
*/
public function testSucceedThrowsIfAlreadyResolved() {
$promisor = new PrivateFuture;
$promisor = $this->getPromisor();
$promisor->succeed(42);
$promisor->succeed('zanzibar');
}
@ -45,7 +45,7 @@ class UnresolvedTest extends \PHPUnit_Framework_TestCase {
* @expectedExceptionMessage A Promise cannot act as its own resolution result
*/
public function testSucceedThrowsIfPromiseIsTheResolutionValue() {
$promisor = new PrivateFuture;
$promisor = $this->getPromisor();
$promise = $promisor->promise();
$promisor->succeed($promise);
}
@ -55,15 +55,15 @@ class UnresolvedTest extends \PHPUnit_Framework_TestCase {
* @expectedExceptionMessage Promise already resolved
*/
public function testFailThrowsIfAlreadyResolved() {
$promisor = new PrivateFuture;
$promisor = $this->getPromisor();
$promisor->succeed(42);
$promisor->fail(new \Exception);
}
public function testSucceedingWithPromisePipelinesResult() {
(new NativeReactor)->run(function($reactor) {
$promisor = new PrivateFuture;
$next = new Future;
$promisor = $this->getPromisor();
$next = $this->getPromisor();
$reactor->once(function() use ($next) {
$next->succeed(42);
@ -81,8 +81,8 @@ class UnresolvedTest extends \PHPUnit_Framework_TestCase {
*/
public function testFailingWithPromisePipelinesResult() {
(new NativeReactor)->run(function($reactor) {
$promisor = new PrivateFuture;
$next = new Future;
$promisor = $this->getPromisor();
$next = $this->getPromisor();
$reactor->once(function() use ($next) {
$next->fail(new \RuntimeException('fugazi'));
@ -92,4 +92,5 @@ class UnresolvedTest extends \PHPUnit_Framework_TestCase {
yield $promisor->promise();
});
}
}

View File

@ -1,11 +0,0 @@
<?php
namespace Amp\Test;
use Amp\PrivateFuture;
class PrivateFutureTest extends PromisorTest {
protected function getPromisor() {
return new PrivateFuture;
}
}

View File

@ -4,7 +4,6 @@ namespace Amp\Test;
use Amp\PromiseStream;
use Amp\NativeReactor;
use Amp\PrivateFuture;
class PromiseStreamTest extends \PHPUnit_Framework_TestCase {
@ -14,7 +13,7 @@ class PromiseStreamTest extends \PHPUnit_Framework_TestCase {
*/
public function testStreamThrowsIfPromiseFails() {
(new NativeReactor)->run(function($reactor) {
$promisor = new PrivateFuture;
$promisor = new PromisorPrivateImpl;
$reactor->repeat(function($reactor, $watcherId) use (&$i, $promisor) {
$i++;
$promisor->update($i);
@ -35,7 +34,7 @@ class PromiseStreamTest extends \PHPUnit_Framework_TestCase {
* @expectedExceptionMessage Cannot advance stream: previous Promise not yet resolved
*/
public function testStreamThrowsIfPrematurelyIterated() {
$promisor = new PrivateFuture;
$promisor = new PromisorPrivateImpl;
$stream = (new PromiseStream($promisor->promise()))->stream();
$stream->next();
}
@ -45,7 +44,7 @@ class PromiseStreamTest extends \PHPUnit_Framework_TestCase {
* @expectedExceptionMessage Cannot advance stream: subject Promise failed
*/
public function testStreamThrowsIfIteratedAfterFailure() {
$promisor = new PrivateFuture;
$promisor = new PromisorPrivateImpl;
$promisor->fail(new \Exception("test"));
$stream = (new PromiseStream($promisor->promise()))->stream();
$stream->next();

View File

@ -0,0 +1,12 @@
<?php
namespace Amp\Test;
use Amp\Promisor;
use Amp\Test\PromisorPrivateImpl;
class PromisorPrivateTest extends PromisorTest {
protected function getPromisor() {
return new PromisorPrivateImpl;
}
}

View File

@ -2,14 +2,16 @@
namespace Amp\Test;
use Amp\Future;
use Amp\Promisor;
use Amp\Test\PromisorPublicImpl;
class FutureTest extends PromisorTest {
class PromisorPublicTest extends PromisorTest {
protected function getPromisor() {
return new Future;
return new PromisorPublicImpl;
}
public function testPromiseReturnsSelf() {
$promisor = new Future;
$promisor = new PromisorPublicImpl;
$this->assertSame($promisor, $promisor->promise());
}
}

12
test/bootstrap.php Normal file
View File

@ -0,0 +1,12 @@
<?php
namespace Amp\Test;
require __DIR__ . "/../vendor/autoload.php";
class PromisorPrivateImpl implements \Amp\Promisor {
use \Amp\PrivatePromisor;
}
class PromisorPublicImpl implements \Amp\Promisor, \Amp\Promise {
use \Amp\PublicPromisor;
}