From 5fc67d7a2546d32854fd204b00d9800d359e4b15 Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Mon, 10 Jan 2022 08:44:51 +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 | 60 +++++++++++++++++++++++++++++++++------------ tests/AwaitTest.php | 7 ++++++ 2 files changed, 51 insertions(+), 16 deletions(-) diff --git a/src/functions.php b/src/functions.php index ad91688..b83cec6 100644 --- a/src/functions.php +++ b/src/functions.php @@ -53,18 +53,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(); @@ -75,25 +103,25 @@ 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 \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; } - /** * Execute a Generator-based coroutine to "await" promises. * diff --git a/tests/AwaitTest.php b/tests/AwaitTest.php index 95a8b5f..6440c97 100644 --- a/tests/AwaitTest.php +++ b/tests/AwaitTest.php @@ -122,6 +122,13 @@ public function testAwaitShouldNotCreateAnyGarbageReferencesForRejectedPromise() $this->assertEquals(0, gc_collect_cycles()); } + public function testAlreadyFulfilledPromiseShouldNotSuspendFiber() + { + for ($i = 0; $i < 6; $i++) { + $this->assertSame($i, React\Async\await(React\Promise\resolve($i))); + } + } + public function testAwaitShouldNotCreateAnyGarbageReferencesForPromiseRejectedWithNullValue() { if (!interface_exists('React\Promise\CancellablePromiseInterface')) {