PHP Generator support improvement
Version 1.30.0 of the PHP probe includes significant improvements in the support of generators!
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 theyield
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!