Skip to content

Commit d972649

Browse files
committed
Improve memory consumption by cleaning up garbage references
1 parent 604513e commit d972649

File tree

5 files changed

+154
-9
lines changed

5 files changed

+154
-9
lines changed

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
"require": {
2121
"php": ">=5.3",
2222
"react/event-loop": "^1.0 || ^0.5 || ^0.4 || ^0.3.5",
23-
"react/promise": "~2.1|~1.2"
23+
"react/promise": "^2.6.0 || ^1.2.1"
2424
},
2525
"require-dev": {
2626
"phpunit/phpunit": "^6.4 || ^5.7 || ^4.8.35"

src/functions.php

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,12 @@ function timeout(PromiseInterface $promise, $time, LoopInterface $loop)
1313
// thus leaving responsibility to the input promise.
1414
$canceller = null;
1515
if ($promise instanceof CancellablePromiseInterface) {
16-
$canceller = array($promise, 'cancel');
16+
// pass promise by reference to clean reference after cancellation handler
17+
// has been invoked once in order to avoid garbage references in call stack.
18+
$canceller = function () use (&$promise) {
19+
$promise->cancel();
20+
$promise = null;
21+
};
1722
}
1823

1924
return new Promise(function ($resolve, $reject) use ($loop, $time, $promise) {
@@ -38,12 +43,15 @@ function timeout(PromiseInterface $promise, $time, LoopInterface $loop)
3843
}
3944

4045
// start timeout timer which will cancel the input promise
41-
$timer = $loop->addTimer($time, function () use ($time, $promise, $reject) {
46+
$timer = $loop->addTimer($time, function () use ($time, &$promise, $reject) {
4247
$reject(new TimeoutException($time, 'Timed out after ' . $time . ' seconds'));
4348

49+
// try to invoke cancellation handler of input promise and then clean
50+
// reference in order to avoid garbage references in call stack.
4451
if ($promise instanceof CancellablePromiseInterface) {
4552
$promise->cancel();
4653
}
54+
$promise = null;
4755
});
4856
}, $canceller);
4957
}
@@ -55,11 +63,12 @@ function resolve($time, LoopInterface $loop)
5563
$timer = $loop->addTimer($time, function () use ($time, $resolve) {
5664
$resolve($time);
5765
});
58-
}, function ($resolve, $reject, $notify) use (&$timer, $loop) {
59-
// cancelling this promise will cancel the timer and reject
66+
}, function () use (&$timer, $loop) {
67+
// cancelling this promise will cancel the timer, clean the reference
68+
// in order to avoid garbage references in call stack and then reject.
6069
$loop->cancelTimer($timer);
70+
$timer = null;
6171

62-
$resolve = $reject = $notify = $timer = $loop = null;
6372
throw new \RuntimeException('Timer cancelled');
6473
});
6574
}

tests/FunctionRejectTest.php

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,42 @@ public function testPromiseExpiredWillBeRejectedOnTimeout()
3838
$this->expectPromiseRejected($promise);
3939
}
4040

41-
public function testCancelingPromiseWillRejectTimer()
41+
public function testCancellingPromiseWillRejectTimer()
4242
{
4343
$promise = Timer\reject(0.01, $this->loop);
4444

4545
$promise->cancel();
4646

4747
$this->expectPromiseRejected($promise);
4848
}
49+
50+
public function testWaitingForPromiseToRejectDoesNotLeaveGarbageCycles()
51+
{
52+
if (class_exists('React\Promise\When')) {
53+
$this->markTestSkipped('Not supported on legacy Promise v1 API');
54+
}
55+
56+
gc_collect_cycles();
57+
58+
$promise = Timer\reject(0.01, $this->loop);
59+
$this->loop->run();
60+
unset($promise);
61+
62+
$this->assertEquals(0, gc_collect_cycles());
63+
}
64+
65+
public function testCancellingPromiseDoesNotLeaveGarbageCycles()
66+
{
67+
if (class_exists('React\Promise\When')) {
68+
$this->markTestSkipped('Not supported on legacy Promise v1 API');
69+
}
70+
71+
gc_collect_cycles();
72+
73+
$promise = Timer\reject(0.01, $this->loop);
74+
$promise->cancel();
75+
unset($promise);
76+
77+
$this->assertEquals(0, gc_collect_cycles());
78+
}
4979
}

tests/FunctionResolveTest.php

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,12 +60,42 @@ public function testCancellingPromiseWillCancelLoopTimer()
6060
$promise->cancel();
6161
}
6262

63-
public function testCancelingPromiseWillRejectTimer()
63+
public function testCancellingPromiseWillRejectTimer()
6464
{
6565
$promise = Timer\resolve(0.01, $this->loop);
6666

6767
$promise->cancel();
6868

6969
$this->expectPromiseRejected($promise);
7070
}
71+
72+
public function testWaitingForPromiseToResolveDoesNotLeaveGarbageCycles()
73+
{
74+
if (class_exists('React\Promise\When')) {
75+
$this->markTestSkipped('Not supported on legacy Promise v1 API');
76+
}
77+
78+
gc_collect_cycles();
79+
80+
$promise = Timer\resolve(0.01, $this->loop);
81+
$this->loop->run();
82+
unset($promise);
83+
84+
$this->assertEquals(0, gc_collect_cycles());
85+
}
86+
87+
public function testCancellingPromiseDoesNotLeaveGarbageCycles()
88+
{
89+
if (class_exists('React\Promise\When')) {
90+
$this->markTestSkipped('Not supported on legacy Promise v1 API');
91+
}
92+
93+
gc_collect_cycles();
94+
95+
$promise = Timer\resolve(0.01, $this->loop);
96+
$promise->cancel();
97+
unset($promise);
98+
99+
$this->assertEquals(0, gc_collect_cycles());
100+
}
71101
}

tests/FunctionTimeoutTest.php

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
use React\Promise\Timer;
66
use React\Promise;
77

8-
class FunctionTimerTest extends TestCase
8+
class FunctionTimeoutTest extends TestCase
99
{
1010
public function testResolvedWillResolveRightAway()
1111
{
@@ -166,4 +166,80 @@ public function testCancelTimeoutWillResolveIfGivenPromiseWillResolve()
166166
$this->expectPromiseResolved($promise);
167167
$this->expectPromiseResolved($timeout);
168168
}
169+
170+
public function testWaitingForPromiseToResolveBeforeTimeoutDoesNotLeaveGarbageCycles()
171+
{
172+
if (class_exists('React\Promise\When')) {
173+
$this->markTestSkipped('Not supported on legacy Promise v1 API');
174+
}
175+
176+
gc_collect_cycles();
177+
178+
$promise = Timer\resolve(0.01, $this->loop);
179+
180+
$promise = Timer\timeout($promise, 1.0, $this->loop);
181+
182+
$this->loop->run();
183+
unset($promise);
184+
185+
$this->assertEquals(0, gc_collect_cycles());
186+
}
187+
188+
public function testWaitingForPromiseToRejectBeforeTimeoutDoesNotLeaveGarbageCycles()
189+
{
190+
if (class_exists('React\Promise\When')) {
191+
$this->markTestSkipped('Not supported on legacy Promise v1 API');
192+
}
193+
194+
gc_collect_cycles();
195+
196+
$promise = Timer\reject(0.01, $this->loop);
197+
198+
$promise = Timer\timeout($promise, 1.0, $this->loop);
199+
200+
$this->loop->run();
201+
unset($promise);
202+
203+
$this->assertEquals(0, gc_collect_cycles());
204+
}
205+
206+
public function testWaitingForPromiseToTimeoutDoesNotLeaveGarbageCycles()
207+
{
208+
if (class_exists('React\Promise\When')) {
209+
$this->markTestSkipped('Not supported on legacy Promise v1 API');
210+
}
211+
212+
gc_collect_cycles();
213+
214+
$promise = new \React\Promise\Promise(function () { }, function () {
215+
throw new \RuntimeException();
216+
});
217+
218+
$promise = Timer\timeout($promise, 0.01, $this->loop);
219+
220+
$this->loop->run();
221+
unset($promise);
222+
223+
$this->assertEquals(0, gc_collect_cycles());
224+
}
225+
226+
public function testCancellingPromiseDoesNotLeaveGarbageCycles()
227+
{
228+
if (class_exists('React\Promise\When')) {
229+
$this->markTestSkipped('Not supported on legacy Promise v1 API');
230+
}
231+
232+
gc_collect_cycles();
233+
234+
$promise = new \React\Promise\Promise(function () { }, function () {
235+
throw new \RuntimeException();
236+
});
237+
238+
$loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
239+
$promise = Timer\timeout($promise, 0.01, $loop);
240+
$promise->cancel();
241+
unset($promise);
242+
243+
$this->assertEquals(0, gc_collect_cycles());
244+
}
169245
}

0 commit comments

Comments
 (0)