From bee5de958b7d47c005d58ffea504fecb50c7d71e Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Mon, 10 Jan 2022 08:34:03 +0100 Subject: [PATCH] Fast forward resolved/rejected promises with await This makes `await`ing an already resolved promise significantly faster. Ported from: https://github.com/reactphp/async/pull/18 --- src/functions.php | 59 +++++++++++++++++++++++++++++++++------------ tests/AwaitTest.php | 7 ++++++ 2 files changed, 51 insertions(+), 15 deletions(-) diff --git a/src/functions.php b/src/functions.php index 0f2ef7e..13f65f7 100644 --- a/src/functions.php +++ b/src/functions.php @@ -51,18 +51,46 @@ function await(PromiseInterface $promise) { $wait = true; - $resolved = null; - $exception = null; + $resolved = false; $rejected = false; + $resolvedValue = null; + $rejectedThrowable = null; $promise->then( - function ($c) use (&$resolved, &$wait) { - $resolved = $c; + function ($c) use (&$resolved, &$resolvedValue, &$wait) { + $resolvedValue = $c; + $resolved = true; $wait = false; Loop::stop(); }, - function ($error) use (&$exception, &$rejected, &$wait) { - $exception = $error; + function ($error) use (&$rejected, &$rejectedThrowable, &$wait) { + // promise is rejected with an unexpected value (Promise API v1 or v2 only) + if (!$error instanceof \Exception && !$error instanceof \Throwable) { + $error = new \UnexpectedValueException( + 'Promise rejected with unexpected value of type ' . (is_object($error) ? get_class($error) : gettype($error)) + ); + + // avoid garbage references by replacing all closures in call stack. + // what a lovely piece of code! + $r = new \ReflectionProperty('Exception', 'trace'); + $trace = $r->getValue($error); + + // Exception trace arguments only available when zend.exception_ignore_args is not set + // @codeCoverageIgnoreStart + foreach ($trace as $ti => $one) { + if (isset($one['args'])) { + foreach ($one['args'] as $ai => $arg) { + if ($arg instanceof \Closure) { + $trace[$ti]['args'][$ai] = 'Object(' . \get_class($arg) . ')'; + } + } + } + } + // @codeCoverageIgnoreEnd + $r->setValue($error, $trace); + } + + $rejectedThrowable = $error; $rejected = true; $wait = false; Loop::stop(); @@ -73,22 +101,23 @@ function ($error) use (&$exception, &$rejected, &$wait) { // argument does not show up in the stack trace in PHP 7+ only. $promise = null; + if ($rejected) { + throw $rejectedThrowable; + } + + if ($resolved) { + return $resolvedValue; + } + while ($wait) { Loop::run(); } if ($rejected) { - // promise is rejected with an unexpected value (Promise API v1 or v2 only) - if (!$exception instanceof \Exception && !$exception instanceof \Throwable) { - $exception = new \UnexpectedValueException( - 'Promise rejected with unexpected value of type ' . (is_object($exception) ? get_class($exception) : gettype($exception)) - ); - } - - throw $exception; + throw $rejectedThrowable; } - return $resolved; + return $resolvedValue; } /** diff --git a/tests/AwaitTest.php b/tests/AwaitTest.php index 1acc7e3..cd153ce 100644 --- a/tests/AwaitTest.php +++ b/tests/AwaitTest.php @@ -145,6 +145,13 @@ public function testAwaitShouldNotCreateAnyGarbageReferencesForPromiseRejectedWi $this->assertEquals(0, gc_collect_cycles()); } + public function testAlreadyFulfilledPromiseShouldShortCircuitAndNotRunLoop() + { + for ($i = 0; $i < 6; $i++) { + $this->assertSame($i, React\Async\await(React\Promise\resolve($i))); + } + } + public function setExpectedException($exception, $exceptionMessage = '', $exceptionCode = null) { if (method_exists($this, 'expectException')) {