Skip to content

Commit 24cd9e8

Browse files
authored
Merge pull request #116 from clue-labs/static-settle
Improve memory consumption by using static resolve and reject callback without binding to promise
2 parents da16050 + 1482a9e commit 24cd9e8

File tree

2 files changed

+100
-29
lines changed

2 files changed

+100
-29
lines changed

src/Promise.php

Lines changed: 60 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -146,15 +146,6 @@ private function resolver(callable $onFulfilled = null, callable $onRejected = n
146146
};
147147
}
148148

149-
private function resolve($value = null)
150-
{
151-
if (null !== $this->result) {
152-
return;
153-
}
154-
155-
$this->settle(resolve($value));
156-
}
157-
158149
private function reject($reason = null)
159150
{
160151
if (null !== $this->result) {
@@ -228,23 +219,75 @@ private function call(callable $callback)
228219
if ($args === 0) {
229220
$callback();
230221
} else {
222+
// keep a reference to this promise instance for the static resolve/reject functions.
223+
// see also resolveFunction() and rejectFunction() for more details.
224+
$target =& $this;
225+
231226
$callback(
232-
function ($value = null) {
233-
$this->resolve($value);
234-
},
235-
function ($reason = null) {
236-
$this->reject($reason);
237-
},
238-
self::notifier($this->progressHandlers)
227+
self::resolveFunction($target),
228+
self::rejectFunction($target),
229+
self::notifyFunction($this->progressHandlers)
239230
);
240231
}
241232
} catch (\Throwable $e) {
233+
$target = null;
242234
$this->reject($e);
243235
} catch (\Exception $e) {
236+
$target = null;
244237
$this->reject($e);
245238
}
246239
}
247240

241+
/**
242+
* Creates a static resolver callback that is not bound to a promise instance.
243+
*
244+
* Moving the closure creation to a static method allows us to create a
245+
* callback that is not bound to a promise instance. By passing the target
246+
* promise instance by reference, we can still execute its resolving logic
247+
* and still clear this reference when settling the promise. This helps
248+
* avoiding garbage cycles if any callback creates an Exception.
249+
*
250+
* These assumptions are covered by the test suite, so if you ever feel like
251+
* refactoring this, go ahead, any alternative suggestions are welcome!
252+
*
253+
* @param Promise $target
254+
* @return callable
255+
*/
256+
private static function resolveFunction(self &$target)
257+
{
258+
return function ($value = null) use (&$target) {
259+
if ($target !== null) {
260+
$target->settle(resolve($value));
261+
$target = null;
262+
}
263+
};
264+
}
265+
266+
/**
267+
* Creates a static rejection callback that is not bound to a promise instance.
268+
*
269+
* Moving the closure creation to a static method allows us to create a
270+
* callback that is not bound to a promise instance. By passing the target
271+
* promise instance by reference, we can still execute its rejection logic
272+
* and still clear this reference when settling the promise. This helps
273+
* avoiding garbage cycles if any callback creates an Exception.
274+
*
275+
* These assumptions are covered by the test suite, so if you ever feel like
276+
* refactoring this, go ahead, any alternative suggestions are welcome!
277+
*
278+
* @param Promise $target
279+
* @return callable
280+
*/
281+
private static function rejectFunction(self &$target)
282+
{
283+
return function ($reason = null) use (&$target) {
284+
if ($target !== null) {
285+
$target->reject($reason);
286+
$target = null;
287+
}
288+
};
289+
}
290+
248291
/**
249292
* Creates a static progress callback that is not bound to a promise instance.
250293
*
@@ -260,7 +303,7 @@ function ($reason = null) {
260303
* @param array $progressHandlers
261304
* @return callable
262305
*/
263-
private static function notifier(&$progressHandlers)
306+
private static function notifyFunction(&$progressHandlers)
264307
{
265308
return function ($update = null) use (&$progressHandlers) {
266309
foreach ($progressHandlers as $handler) {

tests/PromiseTest.php

Lines changed: 40 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,19 @@ public function shouldRejectIfResolverThrowsException()
4949
}
5050

5151
/** @test */
52-
public function shouldRejectWithoutCreatingGarbageCyclesIfResolverThrowsException()
52+
public function shouldResolveWithoutCreatingGarbageCyclesIfResolverResolvesWithException()
53+
{
54+
gc_collect_cycles();
55+
$promise = new Promise(function ($resolve) {
56+
$resolve(new \Exception('foo'));
57+
});
58+
unset($promise);
59+
60+
$this->assertSame(0, gc_collect_cycles());
61+
}
62+
63+
/** @test */
64+
public function shouldRejectWithoutCreatingGarbageCyclesIfResolverThrowsExceptionWithoutResolver()
5365
{
5466
gc_collect_cycles();
5567
$promise = new Promise(function () {
@@ -60,20 +72,36 @@ public function shouldRejectWithoutCreatingGarbageCyclesIfResolverThrowsExceptio
6072
$this->assertSame(0, gc_collect_cycles());
6173
}
6274

63-
/**
64-
* test that checks number of garbage cycles after throwing from a resolver
65-
* that has its arguments explicitly set to null (reassigned arguments only
66-
* show up in the stack trace in PHP 7, so we can't test this on legacy PHP)
67-
*
68-
* @test
69-
* @requires PHP 7
70-
* @link https://3v4l.org/OiDr4
71-
*/
72-
public function shouldRejectWithoutCreatingGarbageCyclesIfResolverThrowsExceptionWithResolveAndRejectUnset()
75+
/** @test */
76+
public function shouldRejectWithoutCreatingGarbageCyclesIfResolverRejectsWithException()
77+
{
78+
gc_collect_cycles();
79+
$promise = new Promise(function ($resolve, $reject) {
80+
$reject(new \Exception('foo'));
81+
});
82+
unset($promise);
83+
84+
$this->assertSame(0, gc_collect_cycles());
85+
}
86+
87+
/** @test */
88+
public function shouldRejectWithoutCreatingGarbageCyclesIfCancellerRejectsWithException()
89+
{
90+
gc_collect_cycles();
91+
$promise = new Promise(function ($resolve, $reject) { }, function ($resolve, $reject) {
92+
$reject(new \Exception('foo'));
93+
});
94+
$promise->cancel();
95+
unset($promise);
96+
97+
$this->assertSame(0, gc_collect_cycles());
98+
}
99+
100+
/** @test */
101+
public function shouldRejectWithoutCreatingGarbageCyclesIfResolverThrowsException()
73102
{
74103
gc_collect_cycles();
75104
$promise = new Promise(function ($resolve, $reject) {
76-
$resolve = $reject = null;
77105
throw new \Exception('foo');
78106
});
79107
unset($promise);

0 commit comments

Comments
 (0)