Skip to content

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

Closed
wants to merge 3 commits into from

Conversation

brzuchal
Copy link
Contributor

@brzuchal brzuchal commented Jul 7, 2020

This PR implements StackFrame class

@mvorisek
Copy link
Contributor

mvorisek commented Jul 7, 2020

@brzuchal did it address https://bugs.php.net/bug.php?id=62325 , ie. can it return a \Closure object if called thru it?

@carusogabriel
Copy link
Contributor

@brzuchal For reference: https://bugs.php.net/bug.php?id=45351

@brzuchal
Copy link
Contributor Author

brzuchal commented Jul 8, 2020

@carusogabriel indeed adding an object_class would benefit here and is quite trivial to achieve, the object shouldn't appear in Exception::getTrace() it collides with destructors when an exception is thrown inside ctor - the only way to add it is via a WeakRef instance here.


final class StackFrame
{
public static function getTrace(int $options = DEBUG_BACKTRACE_PROVIDE_OBJECT, int $limit = 0): array {}
Copy link
Contributor

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?

Copy link
Contributor Author

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.

Copy link
Contributor

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.

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)

Copy link
Contributor Author

@brzuchal brzuchal Jul 9, 2020

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.

Copy link
Contributor

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 ).

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
}

@nikic nikic added this to the PHP 8.0 milestone Jul 15, 2020
public function getType(): ?string {}

public function getArgs(): array {}
}
Copy link
Contributor

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

@TysonAndre
Copy link
Contributor

There seem to be some memory leaks reported when this is built with --enable-debug, possibly relating to this being used for frames that have args (do any tests have args), as well as reference counting closures that get added to the resulting StackFrames

<?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
» sapi/cli/php test.php
[Mon Jul 20 20:38:24 2020]  Script:  '/path/to/php-src/test.php'
/path/to/php-src/Zend/zend_string.h(141) :  Freeing 0x00007f6a10801e40 (32 bytes), script=/path/to/php-src/test.php
[Mon Jul 20 20:38:24 2020]  Script:  '/path/to/php-src/test.php'
/path/to/php-src/Zend/zend_hash.c(278) :  Freeing 0x00007f6a10858b40 (56 bytes), script=/path/to/php-src/test.php
Last leak repeated 1 time
[Mon Jul 20 20:38:24 2020]  Script:  '/path/to/php-src/test.php'
/path/to/php-src/Zend/zend_hash.c(153) :  Freeing 0x00007f6a1085c500 (264 bytes), script=/path/to/php-src/test.php
Last leak repeated 1 time
[Mon Jul 20 20:38:24 2020]  Script:  '/path/to/php-src/test.php'
/path/to/php-src/Zend/zend_closures.c(481) :  Freeing 0x00007f6a1086d300 (320 bytes), script=/path/to/php-src/test.php
=== Total 6 memory leaks detected ===

Some other miscellaneous comments:

  • The ability to fetch the closure is something I've wanted for a file, thanks for working on this
  • getFile(): string - it seems strange to have it return the empty string for an internal function. I can imagine users expecting it to either be null or a string with a file path based on other APIs already in PHP, e.g. $e->getTrace()[$i]['file'] ?? null
  • Forbidding modification through array access seems inconsistent with ways to modify it through properties. If existing code was doing that with arrays of arrays, that code would stop working (e.g. $frame['args'] = self::normalizeArgs($frame['args']))
  • I guess the lack of immutability makes sense if the goal is to process stack frames quickly.
  • https://wiki.php.net/rfc/stack-frame-class#performance mentions "On a test script with 1M recursions to produce huge result results were as above:" - Where can the source code of the test script be found? Was that just to produce the array of arrays/StackFrames?
    What about to actually process the frames for typical use cases (e.g. repeatedly calling for ~20 frames and fetching a subset of the data such as $frame->file vs $arrayFrame['file'])? I've found that switching to objects slow php code down if objects are short-lived (php seems to be faster at fetching dimensions from arrays than from fetching properties from objects.
php > $frame = (fn() => StackFrame::getTrace()[0])();
php > $frame->file = 'fake.php';
php > $frame['file'] = 'fake.php';

Warning: Uncaught Error: Cannot modify a 'StackFrame' in php shell code:1
Stack trace:
#0 {main}
  thrown in php shell code on line 1
php > unset($frame->file);
php > unset($frame['file']);

Warning: Uncaught Error: Cannot modify a 'StackFrame' in php shell code:1
Stack trace:
#0 {main}
  thrown in php shell code on line 1

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.

@TysonAndre
Copy link
Contributor

TysonAndre commented Jul 21, 2020

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.

» sapi/cli/php --no-php-ini -d zend_extension=$PWD/modules/opcache.so -d opcache.enable=1 -d opcache.enable_cli=1 -d memory_limit=4G linesum.php
linesum=68058        iter=1000 elapsed=0.265
linesum_object=68063 iter=1000 elapsed=0.606
<?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);
Copy link
Contributor

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

@nikic nikic removed this from the PHP 8.0 milestone Jul 28, 2020
@@ -1969,7 +1969,14 @@ ZEND_API void zend_fetch_debug_backtrace(zval *return_value, int skip_last, int

while (ptr && (limit == 0 || frameno < limit)) {
Copy link
Contributor

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.

Copy link
Contributor Author

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.

@Girgias
Copy link
Member

Girgias commented Nov 3, 2020

Closing as this feature has been declined (see https://wiki.php.net/rfc/stack-frame-class#vote)

@Girgias Girgias closed this Nov 3, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants