-
Notifications
You must be signed in to change notification settings - Fork 7.9k
Implementation for StackFrame class as an alternative to debug_backtrace() #5820
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
Conversation
@brzuchal did it address https://bugs.php.net/bug.php?id=62325 , ie. can it return a |
@carusogabriel indeed adding an |
|
||
final class StackFrame | ||
{ | ||
public static function getTrace(int $options = DEBUG_BACKTRACE_PROVIDE_OBJECT, int $limit = 0): array {} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can the same method prototype be added for Throwable
interface as well?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@mvorisek No it can't. Throwable
don't decide on how to collect stack trace - it's being populated on every exception object creation thus limiting it on getTrace()
doesn't change frames collection process.
It is also not possible to provide objects on exception trace since they would hold a reference which in case of exception is being thrown inside ctor would block calling dtor.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@mvorisek No it can't.
Throwable
don't decide on how to collect stack trace - it's being populated on every exception object creation thus limiting it ongetTrace()
doesn't change frames collection process.
yes, I meant require the new prototype for all classes implementing Throwable
, populate object as default and return/convert them to an array (like now) based on the $options
. I think this is possible and it will largely improve the debugging possibilities with object/closure present.
It is also not possible to provide objects on exception trace since they would hold a reference which in case of exception is being thrown inside ctor would block calling dtor.
for exceptions in ctor, we might simply not populate the object for the 1st StackFrame
(which makes sense, as the object is not fully constructed)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@mvorisek all Throwable
objects already have a property with trace array, therefore you can enable access to it by reflection and manipulate to suit your preferences. It's not that getTrace()
builds something it's a simple getter for an array which is accessible from the private scope and it really does nothing more (see https://github.com/php/php-src/blob/master/Zend/zend_exceptions.c#L429-L438 ).
For ctor throwing exception, it's not that obvious, you can nest them again so reducing it for one first frame doesn't solve the problem (see https://bugs.php.net/bug.php?id=45351).
As I wrote before what I can propose is to expose a WeakReference
instead of an object instance. IIRC this would not affect refcounting and would allow reaching the object from exception if it still exists without changing current PHP behaviour.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@mvorisek all
Throwable
objects already have a property with trace array, therefore you can enable access to it by reflection and manipulate to suit your preferences. It's not thatgetTrace()
builds something it's a simple getter for an array which is accessible from the private scope and it really does nothing more (see https://github.com/php/php-src/blob/master/Zend/zend_exceptions.c#L429-L438 ).
getTrace()
impl. can change and stay fully BC
For ctor throwing exception, it's not that obvious, you can nest them again so reducing it for one first frame doesn't solve the problem (see https://bugs.php.net/bug.php?id=45351).
As I wrote before what I can propose is to expose a
WeakRef
instead of an object instance. IIRC this would not affect refcounting and would allow reaching the object from exception if it still exists without changing current PHP behaviour.
It makes sense - use weak ref internally and return object or null (if null originally or weak ref was released)
Other approach migh be to not populate not fully constructed object at all (even if constructor might complete later without an exception).
I think the 2nd approach is more consistent, consider these usecases:
funtion run() {
$a = new class() {
public function __construct() {
throw new Exception();
}
};
}
try {
run();
} catch (Exception $e) {
$obj = $e->getTrace(DEBUG_BACKTRACE_PROVIDE_OBJECT)[0]->getObject();
// both approaches will/should return null
}
funtion run() {
$a = new class() {
public function run() {
throw new Exception();
}
};
$a->run();
}
try {
run();
} catch (Exception $e) {
$obj = $e->getTrace(DEBUG_BACKTRACE_PROVIDE_OBJECT)[0]->getObject();
// WeakRef impl. will/might return null because `$a` is no longer referenced
}
public function getType(): ?string {} | ||
|
||
public function getArgs(): array {} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It isn't enough to just declare that it implements ArrayAccess. You also have to declare the stubs and support calling offsetGet directly
php > echo (new ReflectionClass('StackFrame'))->offsetGet('file');
Warning: Uncaught Error: Call to undefined method ReflectionClass::offsetGet() in php shell code:1
Stack trace:
#0 {main}
thrown in php shell code on line 1
php > var_export((new ReflectionClass('StackFrame'))->isAbstract());
true
There seem to be some memory leaks reported when this is built with <?php
function my_call($fn, $val) {
return $fn($val);
}
my_call(fn() => StackFrame::getTrace(), strtoupper('xyz')); // strtoupper is locale-dependent and can't be a constant
Some other miscellaneous comments:
This RFC fell off my radar and I wasn't sure of the planned voting date (and I'd assumed others would have similar questions to anything I would end up asking), so I hadn't gotten around to taking a closer look at this earlier. |
Right now, the below example of a benchmark I wrote isn't a fair benchmark, because the implementation is leaking memory(hence the raised memory_limit) - it'd do better after fixing that. I'd assume (with the current php engine) that if more properties than just the line number were accessed, linesum would comparatively be faster at processing the additional field than linesum_object.
<?php
function linesum(): int {
$total = 0;
foreach (debug_backtrace() as $frame) {
$total += $frame['line'];
}
return $total;
}
function linesum_object(): int {
$total = 0;
foreach (StackFrame::getTrace() as $frame) {
$total += $frame->line;
}
return $total;
}
function recurse(int $depth, int $iter) {
if ($depth <= 0) {
$t1 = microtime(true);
$total1 = 0;
for ($i = 0; $i < $iter; $i++) {
$total1 = linesum();
}
$t2 = microtime(true);
$total2 = 0;
for ($i = 0; $i < $iter; $i++) {
$total2 = linesum_object();
}
$t3 = microtime(true);
printf("linesum=%d iter=%d elapsed=%.3f\n", $total1, $iter, $t2 - $t1);
printf("linesum_object=%d iter=%d elapsed=%.3f\n", $total2, $iter, $t3 - $t2);
return;
}
recurse($depth - 1, $iter);
}
recurse(2000, 1000); |
zval val; | ||
zend_string *prop; | ||
|
||
ZVAL_NULL(&val); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I forget exactly how you declare a typed property that is initially undeclared. Maybe ZVAL_UNDEF(&val)
, but it might be more useful to just add MAY_BE_NULL to almost all of the ZEND_TYPE_INIT_MASK values.
In any case, this is initializing the property with a default that's intended to be impossible for unserialization, constructors, php code, etc. to set. This may cause problems with the engine or opcache if opcache ever supports inferences on typed properties? Things taking references to the properties may also behave unexpectedly. (e.g. with --enable-debug
validating typed properties actually have the correct types)
php > array_map(fn() => $GLOBALS['x'] = StackFrame::getTrace()[0], [2]);
php > var_export($x);
StackFrame::__set_state(array(
'file' => NULL,
'line' => NULL,
'function' => '{closure}',
'class' => NULL,
'type' => NULL,
'object' => NULL,
'object_class' => NULL,
'closure' => NULL,
'args' =>
array (
0 => 2,
),
))
php > var_export($x->file);
NULL
php > $x->file = 'file.php';
php > $x->file = null;
Warning: Uncaught TypeError: Cannot assign null to property StackFrame::$file of type string in php shell code:1
Stack trace:
#0 {main}
thrown in php shell code on line 1
@@ -1969,7 +1969,14 @@ ZEND_API void zend_fetch_debug_backtrace(zval *return_value, int skip_last, int | |||
|
|||
while (ptr && (limit == 0 || frameno < limit)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of using limit=0
to mean "unlimited", can we please use INT_MAX
? It will simplify the code here:
while (ptr && frameno < limit) {
We can add a constant for it if it helps comprehension.
Aside from simplifying code I much prefer it when 0
isn't used for "infinite" or "max", because as we --limit
the stack length with get smaller and smaller until we hit 0
, and then magically it gets bigger again? INT_MAX
is a much better fit, IMO.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@morrisonlevi thanks for comments on this. I'll consider it if I start with this implementation as a separate PECL extension.
Closing as this feature has been declined (see https://wiki.php.net/rfc/stack-frame-class#vote) |
This PR implements StackFrame class