PHP Generator support improvement

By Jérôme Vieilledent, on Jan 23, 2020

While very common in Python, generators are still misunderstood or unknown to many PHP developers. Yet, they consist in a very powerful feature, especially performance-wise.

Since its first implementation in PHP 5.5 back in 2013, this feature evolved and introduced more advanced use cases like generator delegation or return expressions. This significantly improved PHP, and even lead to new usages such as asynchronicity with different new frameworks (AMPHP, ReactPHP, Tornado, or even Guzzle).

Blackfire has been supporting PHP generators for a long time, but their rendering in the call-graph was not making it easy to visualize their behavior… Until now! Version 1.30.0 of the PHP probe includes significant improvements in the support of generators, including a real differentiation between generator calls within your code.

Profiling a simple generator

Let’s consider a minimal generator:

function genLetter(): \Generator
{
    yield 'a';
    yield 'b';
    yield 'c';
}

foreach (genLetter() as $letter) {
    echo $letter.PHP_EOL;
}

/*
 * Output:
 * a
 * b
 * c
 */

Profiling this code with a version of the Blackfire probe before 1.30 produces the following:

We can see that there is no indication about genLetter() being a generator. It could be any regular function call. Furthermore, we can see that it is called 5 times, while we only expect 3 iterations. A bit confusing…

Starting with the v.1.30.0 version, the result is the following:

We can now clearly differentiate 2 different nodes:

  • genLetter, on the left, shows the first call that creates the generator itself;
  • {generator}genLetter, on the right, corresponds to the iterations made over the generator, thanks to the yield statements.

Profiling a more advanced generator

Now, let’s consider the following snippet:

// Complex generator.
function genLetter(): \Generator
{
    yield 'a';
    yield from genLettersBC();
    $foo = yield 'd';
    
    return $foo;
}

// Delegate for genLetter() generator.
function genLettersBC(): \Generator
{
    yield 'b';
    yield 'c';
}

$generator = genLetter();
foreach ($generator as $letter) {
    echo $letter.PHP_EOL;

    if ('d' === $letter) {
        $generator->send('Hello World!');
    }
}

echo $generator->getReturn().PHP_EOL;

/*
 * Output:
 * a
 * b
 * c
 * d
 * Hello World!
 */

A bit more complex, with the use of the Generator API, notably \Generator::send() and \Generator::getReturn, and a generator delegation thanks to a yield from statement.

The pre-v.1.30.0 probe produces the following call-graph:

Tricky to understand… We can see the Generator API calls but they are not considered as part of the generator function itself, which would be even more problematic when using many different generators, as the same nodes would be aggregated. Furthermore, the generator delegation to genLettersBC is also difficult to spot in the overall flow.

The same code now gives the following call-graph:

The call-graph gives a much better overview of what is happening:

  • genLetter is first called to produce the generator itself;
  • {generator}genLetter is called the number of times that the generator is iterated over (4 iterations);
  • {generator}genLetter::send / {generator}genLetter::getReturn show the calls to the Generator API;
  • Delegation to {generator}genLettersBC now appears clearly.

What’s next?

The main feature we are now missing is a consistent timeline view for generators. Such view would be very beneficial to the users of async frameworks. Our team is currently working on it and we will probably talk about it in the future 😉.

To be continued…

Happy profiling!

Jérôme Vieilledent

As a Developer Advocate, Jérôme is all about spreading the love! His technical/development background enable him to talk as a peer to peers with our developer users. You’ll read his tips and advices on performance management with Blackfire. And he’ll support you as a day-to-day user.