-
-
Notifications
You must be signed in to change notification settings - Fork 146
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
Enforce \Exception instances as rejection values #46
Comments
+1 👍 |
👍 |
Note that |
Good catch, thanks 👍 |
I think you should allow public function __construct($e)
{
if (PHP_MAJOR_VERSION < 7) {
if (!$e instanceof \Exception) {
throw new \InvalidArgumentException;
}
} elseif (!$e instanceof \Throwable) {
throw new \InvalidArgumentException;
}
} |
Another good catch 👍 |
Creating exceptions is very heavy for PHP due to filling them with data on creation and not on throwing. Just check what happens when you create 10k of promises, and reject them all. In best sitatuion you get MemoryOverflow error, in worst core dumped. It is reasonable to allow rejection with different values than Throwable for this purpose. I think the good idea would be to allow string or Throwable or some lazy Throwable object defined inside library. |
Maybe we could solve this if some() would reject with the first exception, and not wait for other values? And make a separate function returning a promise that resolves into an array of rejected values. I think a rejected value should always be something |
I think you might be mistaking about memory consumption, I ran the following code:
And I get 4-5Mb of used memory (that is 500 bytes per Exception object). Maybe you were using some xdebug options that add additional information into an exception? I also tried this code: use React\Promise\Deferred;
$p = [];
for ($i=0; $i < 10000; $i++) {
$p[] = new Deferred();
}
foreach ($p as $deferred) {
$deferred->reject(new \Exception("Test"));
}
var_dump(memory_get_peak_usage()); It prints 20 Mb peak memory usage (2 Kb per rejected promise). So it is not exceptions that consume memory but something else. Promises create a lot of anonymous functions inside, maybe that could be the reason. |
@codedokode This is known mistake of PHP that it popullates Exception on creation and not on throwing like in other programming languages, for example in JAVA. There is no dyning that. The example you provided without using exceptions would take about ~10 Mb peak memory, meaning the exception took 10 Mb. It is already high enough for exceptions that literally have empty stack trace. Now try the same example in real world application, where the thrown exception would have about ~10-20 elements in their stack trace. You will run out of memory. |
@khelle I would call that a design choice rather than mistake. I made measurements with code that emulates a "complex" application: <?php
class A1 { public static function test() { A2::test(); } };
class A2 { public static function test() { A3::test(); } };
class A3 { public static function test() { A4::test(); } };
class A4 { public static function test() { A5::test(); } };
class A5 { public static function test() { A6::test(); } };
class A6 { public static function test() { A7::test(); } };
class A7 { public static function test() { A8::test(); } };
class A8 { public static function test() { A9::test(); } };
class A9 { public static function test() { A10::test(); } };
class A10 { public static function test() { A11::test(); } };
class A11 { public static function test() { A12::test(); } };
class A12 { public static function test() { A13::test(); } };
class A13 { public static function test() { A14::test(); } };
class A14 { public static function test() { A15::test(); } };
class A15 { public static function test() {
throw new \Exception("Test");
} };
$list = [];
for ($i=0; $i < 10000; $i++) {
try {
A1::test();
} catch (Exception $e) {
$list[] = $e;
}
}
var_dump(memory_get_peak_usage()); It shows 132Mb peak usage (13 Kb per exception object) on my machine (32-bit) without xdebug and 152Mb if xdebug is enabled. Maybe this should be reported to PHP developers. Maybe they will want to optimize it. By the way in PHP7 the exceptions are no longer required to be inherited from Exception class. You can write your own class even without any fields. Or you could reuse objects. |
react is moving away from non-exception as rejection values see reactphp/promise#46
We need to address memory leaks before enforcing exceptions as rejection reasons. Consider following example: new React\Promise\Promise(
function ($resolve, $reject) {
$reject('Rejected.');
}
);
collectGarbage();
new React\Promise\Promise(
function ($resolve, $reject) {
$reject(new \RuntimeException('Rejected.'));
}
);
collectGarbage();
function collectGarbage()
{
printf(
"%.3fMB => %d => %.3fMB (%.3fMB)\n",
memory_get_usage() / 1024 / 1024,
gc_collect_cycles(),
memory_get_usage() / 1024 / 1024,
memory_get_peak_usage() / 1024 / 1024
);
} Output:
As you can see, using an exception as rejection reason created 27 garbage objects. |
@valga These are circular references, not memory leaks. |
@valga A small elaboration why these circular references are created when rejecting with exceptions: When creating a new React\Promise\Promise(
function ($resolve, $reject) {
$reject(new \RuntimeException('Rejected.'));
}
); The There has been a thread on |
Thanks @kelunik for the explanation. 👍 I think, there isn't much we can do about this since we can't throw without referencing arguments (like FTR, by convention, we already always reject promises throughout all ReactPHP with exceptions. There is #55 and also #56 discussing cleaning up resources on cancellation, but that would be a BC break and only allowed in the next major version. |
@jsor Did you think about using Regarding the backtrace: I guess the only think that can be done is using |
Although you could lower a number of circular references by throwing an exception in $loop->nextTick(function () use ($reject) {
$reject(new \RuntimeException('Error'));
}); |
How so? The
The number of circular references isn't smaller because of a smaller stack trace. But in that case |
Because you have to write something like
It doesn't solve the problem, because |
It doesn't matter whether it's in |
Yes, i did. I Studied tc39/proposal-cancelable-promises and alike. I tried hard to like it, but I couldn't quite manage it. ;)
In the proposal i made, calls to |
What makes you feel uneasy with these tokens?
That's probably the only reasonable choice with that API, because the future can't be known and cancellation can't wait potential future handlers being attached. |
I just find the API not very intuitive. |
If the closure is in stack trace, and the closure has a reference to that variable, stack trace also contains a reference to that variable. Consider 2 examples: $loop = React\EventLoop\Factory::create();
new \React\Promise\Promise(function ($resolve, $reject) use ($loop) {
$loop->nextTick(function () use ($reject) {
$reject(debug_backtrace());
});
});
$loop->run();
var_dump(gc_collect_cycles());
$loop = React\EventLoop\Factory::create();
new \React\Promise\Promise(function ($resolve, $reject) use ($loop) {
$loop->nextTick(function () use ($reject) {
$reject(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS));
});
});
$loop->run();
var_dump(gc_collect_cycles());
|
I guess you're right, forgot that closures are also just objects. You can use that: <?php
require __DIR__ . "/vendor/autoload.php";
$loop = React\EventLoop\Factory::create();
new \React\Promise\Promise(function ($resolve, $reject) use ($loop) {
$loop->nextTick(function () use (&$reject) {
$r = $reject;
$reject = null;
$r(debug_backtrace());
});
});
$loop->run();
var_dump(gc_collect_cycles()); |
@kelunik Very nice catch! This does indeed prevent these arguments from showing up in the call stack for PHP 7+ and HHVM, here's a gist to reproduce this https://3v4l.org/OiDr4 |
Thank you for looking into this! I agree that this is something we absolutely want to look in to! Thanks to the above analysis, I think we located some of the issues why the above code does indeed create some cyclic garbage references. Let's not call this a "memory leak", because memory was eventually freed, but this clearly caused some unexpected and significant memory growth.
I've just filed #113 which improves this for the somewhat common use case of simply throwing an In a follow-up PR we'll look into avoiding cyclic garbage references when these arguments are actually used. This can be achieved by explicitly binding the closure to another instance and then passing references variables to them. This does require some significant testing effort. In the meantime, this can also be worked around by explicitly storing a reference to |
This discussion got a bit sidetracked due to possible memory issues when using Exceptions, so I'm trying to wrap this up. All such memory issues have been addressed via the referenced tickets that have just been released via https://github.com/reactphp/promise/releases/tag/v2.6.0. Thank you all for participating in this discussion and bringing up some very good pointers! This means that there seem to be no other open issues with regards to enforcing Exceptions. |
This also removes support for rejecting with a Promise instance.
Decide whether the rejection value is still an optional argument and can be
null
. Enforcing an argument is more inline withthrow
(you can't thrownull
) and let's us removeUnhandledRejectionException
.The text was updated successfully, but these errors were encountered: