From c30fb2bcde1e46369778e8d18b2f8384f10f21a8 Mon Sep 17 00:00:00 2001 From: sji Date: Thu, 7 Oct 2021 01:14:30 +0900 Subject: [PATCH 1/3] add support for speedscope --- src/Command/Converter/SpeedscopeCommand.php | 124 ++++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 src/Command/Converter/SpeedscopeCommand.php diff --git a/src/Command/Converter/SpeedscopeCommand.php b/src/Command/Converter/SpeedscopeCommand.php new file mode 100644 index 00000000..3e686477 --- /dev/null +++ b/src/Command/Converter/SpeedscopeCommand.php @@ -0,0 +1,124 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace PhpProfiler\Command\Converter; + +use PhpCast\Cast; +use PhpProfiler\Lib\PhpInternals\Opcodes\OpcodeV80; +use PhpProfiler\Lib\PhpInternals\Types\Zend\Opline; +use PhpProfiler\Lib\PhpProcessReader\CallFrame; +use PhpProfiler\Lib\PhpProcessReader\CallTrace; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +final class SpeedscopeCommand extends Command +{ + public function configure(): void + { + $this->setName('converter:speedscope') + ->setDescription('convert traces to the speedscope file format') + ; + } + + public function execute(InputInterface $input, OutputInterface $output): int + { + $output->write( + json_encode( + $this->collectFrames( + $this->getTraceIterator(STDIN) + ) + ) + ); + return 0; + } + + /** + * @param resource $fp + * @return iterable + */ + private function getTraceIterator($fp): iterable + { + $buffer = []; + while (($line = fgets($fp)) !== false) { + $line = trim($line); + if ($line !== '') { + $buffer[] = $line; + continue; + } + yield $this->parsePhpSpyCompatible($buffer); + $buffer = []; + } + } + + /** @param string[] $buffer */ + private function parsePhpSpyCompatible(array $buffer): CallTrace + { + $frames = []; + foreach ($buffer as $line_buffer) { + $result = explode(' ', $line_buffer); + [$_depth, $name, $file_line] = $result; + [$file, $line] = explode(':', $file_line); + $frames[] = new CallFrame( + '', + $name, + $file, + new Opline(0, 0, 0, 0, Cast::toInt($line), new OpcodeV80(0), 0, 0, 0), + ); + } + return new CallTrace(...$frames); + } + + /** @param iterable $call_frames */ + private function collectFrames(iterable $call_frames): array + { + $mapper = fn (array $value): string => \json_encode($value); + $trace_map = []; + $result_frames = []; + $sampled_stacks = []; + $counter = 0; + foreach ($call_frames as $frames) { + $sampled_stack = []; + foreach ($frames->call_frames as $call_frame) { + $frame = [ + 'name' => $call_frame->getFullyQualifiedFunctionName(), + 'file' => $call_frame->file_name, + 'line' => $call_frame->getLineno(), + ]; + $mapper_key = $mapper($frame); + if (!isset($trace_map[$mapper_key])) { + $result_frames[] = $frame; + $trace_map[$mapper_key] = array_key_last($result_frames); + } + $sampled_stack[] = $trace_map[$mapper_key]; + } + $sampled_stacks[] = $sampled_stack; + $counter++; + } + return [ + "\$schema" => "https://www.speedscope.app/file-format-schema.json", + 'shared' => [ + 'frames' => $result_frames, + ], + 'profiles' => [[ + 'type' => 'sampled', + 'name' => 'test', + 'unit' => 'none', + 'startValue' => 0, + 'endValue' => $counter, + 'samples' => $sampled_stacks, + 'weights' => array_fill(0, count($sampled_stacks), 1), + ]] + ]; + } +} From a6c89d4e2976cfbc6a4e0bea3a998965619e8e1d Mon Sep 17 00:00:00 2001 From: sji Date: Fri, 8 Oct 2021 23:36:53 +0900 Subject: [PATCH 2/3] skip comments on converting phpspy compatible traces to the speedscope format and fix ordering of traces --- src/Command/Converter/SpeedscopeCommand.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Command/Converter/SpeedscopeCommand.php b/src/Command/Converter/SpeedscopeCommand.php index 3e686477..85735bf3 100644 --- a/src/Command/Converter/SpeedscopeCommand.php +++ b/src/Command/Converter/SpeedscopeCommand.php @@ -67,7 +67,10 @@ private function parsePhpSpyCompatible(array $buffer): CallTrace $frames = []; foreach ($buffer as $line_buffer) { $result = explode(' ', $line_buffer); - [$_depth, $name, $file_line] = $result; + [$depth, $name, $file_line] = $result; + if ($depth === '#') { // comment + continue; + } [$file, $line] = explode(':', $file_line); $frames[] = new CallFrame( '', @@ -102,7 +105,7 @@ private function collectFrames(iterable $call_frames): array } $sampled_stack[] = $trace_map[$mapper_key]; } - $sampled_stacks[] = $sampled_stack; + $sampled_stacks[] = \array_reverse($sampled_stack); $counter++; } return [ From d5b882ed3c259db0caeef45b073ea5abda15b2c6 Mon Sep 17 00:00:00 2001 From: sji Date: Fri, 8 Oct 2021 23:56:54 +0900 Subject: [PATCH 3/3] update README --- README.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 049eaa81..6c6d040a 100644 --- a/README.md +++ b/README.md @@ -186,9 +186,19 @@ docker run -it --security-opt="apparmor=unconfined" --cap-add=SYS_PTRACE --pid=h ### Generate flamegraphs from traces ```bash sudo php ./php-profiler inspector:trace -p >traces -./tools/stackcollapse-phpspy/stackcollapse-phpspy.pl flame.svg +./php-profiler converter:flamegraph flame.svg ``` +### Generate the [speedscope](https://github.com/jlfwong/speedscope) format from phpspy compatible traces +```bash +sudo php ./php-profiler inspector:trace -p >traces +./php-profiler converter:speedscope profile.speedscope.json +speedscope profile.speedscope.json +``` + +See [#101](https://github.com/sj-i/php-profiler/pull/101). + + # LICENSE - MIT (mostly) - Some C headers defining internal structures are extracted from php-src. They are licensed under the zend engine license. See src/Lib/PhpInternals/Headers . So here are the words required by the zend engine license.