Skip to content
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

Output the cause of the error when an error occurs at converters #364

Merged
merged 1 commit into from
Nov 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 12 additions & 49 deletions src/Command/Converter/SpeedscopeCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,21 @@

namespace Reli\Command\Converter;

use Reli\Converter\ParsedCallTrace;
use Reli\Converter\PhpSpyCompatibleParser;
use Reli\Converter\Speedscope\SpeedscopeConverter;
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 __construct(
private SpeedscopeConverter $speedscope_converter,
private PhpSpyCompatibleParser $parser,
) {
parent::__construct();
}

public function configure(): void
{
$this->setName('converter:speedscope')
Expand All @@ -30,57 +37,13 @@ public function configure(): void

public function execute(InputInterface $input, OutputInterface $output): int
{
$parser = new PhpSpyCompatibleParser();
$output->write(
json_encode(
$this->collectFrames(
$parser->parseFile(STDIN)
)
\json_encode(
$this->speedscope_converter->collectFrames(
$this->parser->parseFile(STDIN)
),
)
);
return 0;
}

/** @param iterable<ParsedCallTrace> $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->function_name,
'file' => $call_frame->file_name,
'line' => $call_frame->lineno,
];
$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[] = \array_reverse($sampled_stack);
$counter++;
}
return [
"\$schema" => "https://www.speedscope.app/file-format-schema.json",
'shared' => [
'frames' => $result_frames,
],
'profiles' => [[
'type' => 'sampled',
'name' => 'php profile',
'unit' => 'none',
'startValue' => 0,
'endValue' => $counter,
'samples' => $sampled_stacks,
'weights' => array_fill(0, count($sampled_stacks), 1),
]]
];
}
}
19 changes: 19 additions & 0 deletions src/Converter/OriginalDataContext.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

/**
* This file is part of the reliforp/reli-prof package.
*
* (c) sji <sji@sj-i.dev>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace Reli\Converter;

interface OriginalDataContext
{
public function toString(): string;
}
1 change: 1 addition & 0 deletions src/Converter/ParsedCallFrame.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ public function __construct(
public string $function_name,
public string $file_name,
public int $lineno,
public ?OriginalDataContext $original_context = null,
) {
}
}
27 changes: 27 additions & 0 deletions src/Converter/PhpSpyCompatibleDataContext.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

/**
* This file is part of the reliforp/reli-prof package.
*
* (c) sji <sji@sj-i.dev>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace Reli\Converter;

class PhpSpyCompatibleDataContext implements OriginalDataContext
{
public function __construct(
public int $lineno,
) {
}

public function toString(): string
{
return 'line:' . $this->lineno;
}
}
38 changes: 28 additions & 10 deletions src/Converter/PhpSpyCompatibleParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
namespace Reli\Converter;

use PhpCast\Cast;
use Webmozart\Assert\Assert;

final class PhpSpyCompatibleParser
{
Expand All @@ -25,49 +24,68 @@ final class PhpSpyCompatibleParser
public function parseFile($fp): iterable
{
$buffer = [];
$lineno_before_parse = 0;
while (($line = fgets($fp)) !== false) {
$lineno_before_parse++;
$line = trim($line);
if ($line !== '') {
$buffer[] = $line;
$buffer[] = [$line, $lineno_before_parse];
continue;
}
yield $this->parsePhpSpyCompatible($buffer);
$buffer = [];
}
}

/** @param string[] $buffer */
/** @param array{string, int}[] $buffer */
private function parsePhpSpyCompatible(array $buffer): ParsedCallTrace
{
$frames = [];
foreach ($buffer as $line_buffer) {
foreach ($buffer as [$line_buffer, $lineno_before_parse]) {
$result = explode(' ', $line_buffer);
[$depth, $name, $file_line] = $result;
if ($depth === '#') { // comment
continue;
}
Assert::stringNotEmpty($file_line);
[$file, $line] = $this->splitLineNumberAndFilePath($file_line);
[$file, $line] = $this->splitLineNumberAndFilePath($file_line, $lineno_before_parse);
$frames[] = new ParsedCallFrame(
$name,
$file,
$line,
new PhpSpyCompatibleDataContext($lineno_before_parse),
);
}
return new ParsedCallTrace(...$frames);
}

/**
* @param non-empty-string $file_line
* @param string $file_line
* @return array{non-empty-string, int}
*/
private function splitLineNumberAndFilePath(string $file_line): array
private function splitLineNumberAndFilePath(string $file_line, int $lineno_before_parse): array
{
if ($file_line === '') {
throw new PhpSpyCompatibleParserException(
'missing line number and file path at line' . $lineno_before_parse,
$lineno_before_parse,
);
}

$separator_position = strrpos($file_line, ':');
Assert::notFalse($separator_position);
if ($separator_position === false) {
throw new PhpSpyCompatibleParserException(
'missing separator ":" between line number and file path at line ' . $lineno_before_parse,
$lineno_before_parse,
);
}
$line = substr($file_line, $separator_position + 1);
$file = substr($file_line, 0, $separator_position);
Assert::stringNotEmpty($file);
if ($file === '') {
throw new PhpSpyCompatibleParserException(
'missing file path at line ' . $lineno_before_parse,
$lineno_before_parse,
);
}
return [$file, Cast::toInt($line)];
}
}
28 changes: 28 additions & 0 deletions src/Converter/PhpSpyCompatibleParserException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

/**
* This file is part of the reliforp/reli-prof package.
*
* (c) sji <sji@sj-i.dev>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace Reli\Converter;

use Throwable;

final class PhpSpyCompatibleParserException extends \Exception
{
public function __construct(
string $message,
public int $lineno_before_parse,
int $code = 0,
?Throwable $previous = null
) {
parent::__construct($message, $code, $previous);
}
}
72 changes: 72 additions & 0 deletions src/Converter/Speedscope/SpeedscopeConverter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?php

/**
* This file is part of the reliforp/reli-prof package.
*
* (c) sji <sji@sj-i.dev>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace Reli\Converter\Speedscope;

use Reli\Converter\ParsedCallTrace;

class SpeedscopeConverter
{
/** @param iterable<ParsedCallTrace> $call_frames */
public function collectFrames(iterable $call_frames): array
{
$mapper = fn(array $value): string | false => \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->function_name,
'file' => $call_frame->file_name,
'line' => $call_frame->lineno,
];
$mapper_key = $mapper($frame);
if ($mapper_key === false) {
throw new SpeedscopeConverterException(
'json_encode failed at '
. ($call_frame->original_context?->toString() ?? 'unknown location')
. ': '
. \json_last_error_msg()
);
}
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[] = \array_reverse($sampled_stack);
$counter++;
}
return [
"\$schema" => "https://www.speedscope.app/file-format-schema.json",
'shared' => [
'frames' => $result_frames,
],
'profiles' => [[
'type' => 'sampled',
'name' => 'php profile',
'unit' => 'none',
'startValue' => 0,
'endValue' => $counter,
'samples' => $sampled_stacks,
'weights' => array_fill(0, count($sampled_stacks), 1),
]]
];
}
}
25 changes: 25 additions & 0 deletions src/Converter/Speedscope/SpeedscopeConverterException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

/**
* This file is part of the reliforp/reli-prof package.
*
* (c) sji <sji@sj-i.dev>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace Reli\Converter\Speedscope;

final class SpeedscopeConverterException extends \Exception
{
public function __construct(
string $message,
int $code = 0,
?\Throwable $previous = null
) {
parent::__construct($message, $code, $previous);
}
}
Loading