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 memory consumption by cleaning up garbage references #35

Merged
merged 1 commit into from
Jun 13, 2018
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@
"require": {
"php": ">=5.3",
"react/event-loop": "^1.0 || ^0.5 || ^0.4 || ^0.3.5",
"react/promise": "~2.1|~1.2",
"react/promise-timer": "~1.0"
"react/promise": "^2.7 || ^1.2.1",
"react/promise-timer": "^1.5"
},
"require-dev": {
"phpunit/phpunit": "^6.4 || ^5.7 || ^4.8.35"
Expand Down
28 changes: 21 additions & 7 deletions src/functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,10 @@ function ($error) use (&$exception, &$rejected, &$wait, $loop) {
}
);

// Explicitly overwrite argument with null value. This ensure that this
// argument does not show up in the stack trace in PHP 7+ only.
$promise = null;

while ($wait) {
$loop->run();
}
Expand Down Expand Up @@ -120,33 +124,38 @@ function ($error) use (&$exception, &$rejected, &$wait, $loop) {
*/
function awaitAny(array $promises, LoopInterface $loop, $timeout = null)
{
// Explicitly overwrite argument with null value. This ensure that this
// argument does not show up in the stack trace in PHP 7+ only.
$all = $promises;
$promises = null;

try {
// Promise\any() does not cope with an empty input array, so reject this here
if (!$promises) {
if (!$all) {
throw new UnderflowException('Empty input array');
}

$ret = await(Promise\any($promises)->then(null, function () {
$ret = await(Promise\any($all)->then(null, function () {
// rejects with an array of rejection reasons => reject with Exception instead
throw new Exception('All promises rejected');
}), $loop, $timeout);
} catch (TimeoutException $e) {
// the timeout fired
// => try to cancel all promises (rejected ones will be ignored anyway)
_cancelAllPromises($promises);
_cancelAllPromises($all);

throw $e;
} catch (Exception $e) {
// if the above throws, then ALL promises are already rejected
// => try to cancel all promises (rejected ones will be ignored anyway)
_cancelAllPromises($promises);
_cancelAllPromises($all);

throw new UnderflowException('No promise could resolve', 0, $e);
}

// if we reach this, then ANY of the given promises resolved
// => try to cancel all promises (settled ones will be ignored anyway)
_cancelAllPromises($promises);
_cancelAllPromises($all);

return $ret;
}
Expand Down Expand Up @@ -180,12 +189,17 @@ function awaitAny(array $promises, LoopInterface $loop, $timeout = null)
*/
function awaitAll(array $promises, LoopInterface $loop, $timeout = null)
{
// Explicitly overwrite argument with null value. This ensure that this
// argument does not show up in the stack trace in PHP 7+ only.
$all = $promises;
$promises = null;

try {
return await(Promise\all($promises), $loop, $timeout);
return await(Promise\all($all), $loop, $timeout);
} catch (Exception $e) {
// ANY of the given promises rejected or the timeout fired
// => try to cancel all promises (rejected ones will be ignored anyway)
_cancelAllPromises($promises);
_cancelAllPromises($all);

throw $e;
}
Expand Down
24 changes: 24 additions & 0 deletions tests/FunctionAwaitAllTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -104,4 +104,28 @@ public function testAwaitAllPendingWillThrowAndCallCancellerOnTimeout()
$this->assertTrue($cancelled);
}
}

/**
* @requires PHP 7
*/
public function testAwaitAllPendingPromiseWithTimeoutAndCancellerShouldNotCreateAnyGarbageReferences()
{
if (class_exists('React\Promise\When')) {
$this->markTestSkipped('Not supported on legacy Promise v1 API');
}

gc_collect_cycles();

$promise = new \React\Promise\Promise(function () { }, function () {
throw new RuntimeException();
});
try {
Block\awaitAll(array($promise), $this->loop, 0.001);
} catch (Exception $e) {
// no-op
}
unset($promise, $e);

$this->assertEquals(0, gc_collect_cycles());
}
}
26 changes: 25 additions & 1 deletion tests/FunctionAwaitAnyTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public function testAwaitAnyFirstResolvedConcurrently()
}

/**
* @expectedException UnderflowException
* @expectedException UnderflowException
*/
public function testAwaitAnyAllRejected()
{
Expand Down Expand Up @@ -97,4 +97,28 @@ public function testAwaitAnyPendingWillThrowAndCallCancellerOnTimeout()
$this->assertTrue($cancelled);
}
}

/**
* @requires PHP 7
*/
public function testAwaitAnyPendingPromiseWithTimeoutAndCancellerShouldNotCreateAnyGarbageReferences()
{
if (class_exists('React\Promise\When')) {
$this->markTestSkipped('Not supported on legacy Promise v1 API');
}

gc_collect_cycles();

$promise = new \React\Promise\Promise(function () { }, function () {
throw new RuntimeException();
});
try {
Block\awaitAny(array($promise), $this->loop, 0.001);
} catch (Exception $e) {
// no-op
}
unset($promise, $e);

$this->assertEquals(0, gc_collect_cycles());
}
}
134 changes: 134 additions & 0 deletions tests/FunctionAwaitTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -103,4 +103,138 @@ public function testAwaitOnceWithTimeoutWillResolvemmediatelyAndCleanUpTimeout()

$this->assertLessThan(0.1, $time);
}

public function testAwaitOneResolvesShouldNotCreateAnyGarbageReferences()
{
if (class_exists('React\Promise\When') && PHP_VERSION_ID >= 50400) {
$this->markTestSkipped('Not supported on legacy Promise v1 API with PHP 5.4+');
}

gc_collect_cycles();

$promise = Promise\resolve(1);
Block\await($promise, $this->loop);
unset($promise);

$this->assertEquals(0, gc_collect_cycles());
}

public function testAwaitOneRejectedShouldNotCreateAnyGarbageReferences()
{
if (class_exists('React\Promise\When') && PHP_VERSION_ID >= 50400) {
$this->markTestSkipped('Not supported on legacy Promise v1 API with PHP 5.4+');
}

gc_collect_cycles();

$promise = Promise\reject(new RuntimeException());
try {
Block\await($promise, $this->loop);
} catch (Exception $e) {
// no-op
}
unset($promise, $e);

$this->assertEquals(0, gc_collect_cycles());
}

public function testAwaitOneRejectedWithTimeoutShouldNotCreateAnyGarbageReferences()
{
if (class_exists('React\Promise\When') && PHP_VERSION_ID >= 50400) {
$this->markTestSkipped('Not supported on legacy Promise v1 API with PHP 5.4+');
}

gc_collect_cycles();

$promise = Promise\reject(new RuntimeException());
try {
Block\await($promise, $this->loop, 0.001);
} catch (Exception $e) {
// no-op
}
unset($promise, $e);

$this->assertEquals(0, gc_collect_cycles());
}

public function testAwaitNullValueShouldNotCreateAnyGarbageReferences()
{
if (class_exists('React\Promise\When') && PHP_VERSION_ID >= 50400) {
$this->markTestSkipped('Not supported on legacy Promise v1 API with PHP 5.4+');
}

gc_collect_cycles();

$promise = Promise\reject(null);
try {
Block\await($promise, $this->loop);
} catch (Exception $e) {
// no-op
}
unset($promise, $e);

$this->assertEquals(0, gc_collect_cycles());
}

/**
* @requires PHP 7
*/
public function testAwaitPendingPromiseWithTimeoutAndCancellerShouldNotCreateAnyGarbageReferences()
{
if (class_exists('React\Promise\When')) {
$this->markTestSkipped('Not supported on legacy Promise v1 API');
}

gc_collect_cycles();

$promise = new \React\Promise\Promise(function () { }, function () {
throw new RuntimeException();
});
try {
Block\await($promise, $this->loop, 0.001);
} catch (Exception $e) {
// no-op
}
unset($promise, $e);

$this->assertEquals(0, gc_collect_cycles());
}

/**
* @requires PHP 7
*/
public function testAwaitPendingPromiseWithTimeoutAndWithoutCancellerShouldNotCreateAnyGarbageReferences()
{
gc_collect_cycles();

$promise = new \React\Promise\Promise(function () { });
try {
Block\await($promise, $this->loop, 0.001);
} catch (Exception $e) {
// no-op
}
unset($promise, $e);

$this->assertEquals(0, gc_collect_cycles());
}

/**
* @requires PHP 7
*/
public function testAwaitPendingPromiseWithTimeoutAndNoOpCancellerShouldNotCreateAnyGarbageReferences()
{
gc_collect_cycles();

$promise = new \React\Promise\Promise(function () { }, function () {
// no-op
});
try {
Block\await($promise, $this->loop, 0.001);
} catch (Exception $e) {
// no-op
}
unset($promise, $e);

$this->assertEquals(0, gc_collect_cycles());
}
}