From 591fcbd22ec9952529f57ea6bcb8cb281073b952 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Sun, 4 Feb 2024 00:48:28 +0400 Subject: [PATCH 01/21] Add FilesObserver --- src/Config/FilesObserver.php | 14 +++++ src/Processable.php | 2 +- src/Proto/Frame/Sentry.php | 3 +- src/Service/FilesObserver.php | 69 +++++++++++++++++++++++ src/Service/FilesObserver/FileInfo.php | 46 +++++++++++++++ src/Service/FilesObserver/Handler.php | 77 ++++++++++++++++++++++++++ 6 files changed, 209 insertions(+), 2 deletions(-) create mode 100644 src/Config/FilesObserver.php create mode 100644 src/Service/FilesObserver.php create mode 100644 src/Service/FilesObserver/FileInfo.php create mode 100644 src/Service/FilesObserver/Handler.php diff --git a/src/Config/FilesObserver.php b/src/Config/FilesObserver.php new file mode 100644 index 00000000..d6c9e351 --- /dev/null +++ b/src/Config/FilesObserver.php @@ -0,0 +1,14 @@ + SentryEnvelope::fromArray($data, $time), $data['type'] === SentryStore::SENTRY_FRAME_TYPE => SentryStore::fromArray($data, $time), diff --git a/src/Service/FilesObserver.php b/src/Service/FilesObserver.php new file mode 100644 index 00000000..9aa90bb1 --- /dev/null +++ b/src/Service/FilesObserver.php @@ -0,0 +1,69 @@ +fibers[] = new Fiber(function () use ($config) { + foreach (Handler::generate($config) as $fileInfo) { + $this->propagateFrame($fileInfo); + } + }); + } + } + + public function process(): void + { + if ($this->cancelled) { + return; + } + + foreach ($this->fibers as $key => $fiber) { + try { + $fiber->isStarted() ? $fiber->resume() : $fiber->start(); + + if ($fiber->isTerminated()) { + unset($this->fibers[$key]); + } + } catch (\Throwable $e) { + $this->logger->exception($e); + unset($this->fibers[$key]); + } + } + } + + public function cancel(): void + { + $this->cancelled = true; + $this->fibers = []; + } + + private function propagateFrame(FileInfo $info): void + { + $frame = new FileFrame($info); + $this->buffer->addFrame($frame); + } +} diff --git a/src/Service/FilesObserver/FileInfo.php b/src/Service/FilesObserver/FileInfo.php new file mode 100644 index 00000000..12c304ce --- /dev/null +++ b/src/Service/FilesObserver/FileInfo.php @@ -0,0 +1,46 @@ +getPathname(), + $fileInfo->getSize(), + $fileInfo->getCTime(), + $fileInfo->getMTime(), + ); + } + + public function toArray(): array + { + return [ + 'path' => $this->path, + 'size' => $this->size, + 'ctime' => $this->ctime, + 'mtime' => $this->mtime, + ]; + } + + public static function fromArray(array $data): self + { + return new self( + $data['path'], + $data['size'], + $data['ctime'], + $data['mtime'], + ); + } +} diff --git a/src/Service/FilesObserver/Handler.php b/src/Service/FilesObserver/Handler.php new file mode 100644 index 00000000..ebdec478 --- /dev/null +++ b/src/Service/FilesObserver/Handler.php @@ -0,0 +1,77 @@ + */ + private array $cache = []; + private readonly string $path; + + private function __construct( + Config $config, + ) { + $this->path = $config->path; + $this->timer = new Timer($config->interval); + } + + /** + * @return \Generator + */ + public static function generate(Config $config): \Generator + { + $self = new self($config); + do { + yield from $self->syncFiles(); + $self->timer->wait()->reset(); + } while (true); + } + + /** + * @return list + */ + private function syncFiles(): array + { + $files = $this->getFiles(); + $newFiles = []; + + foreach ($files as $fileInfo) { + if (\array_key_exists($fileInfo->getRealPath(), $this->cache)) { + continue; + } + + $info = FileInfo::fromSplFileInfo($fileInfo); + $this->cache[$fileInfo->getRealPath()] = $info; + $newFiles[] = $info; + } + + return $newFiles; + } + + /** + * @return \Traversable + */ + private function getFiles(): \Traversable + { + /** @var \Iterator<\SplFileInfo> $iterator */ + $iterator = new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator($this->path, \RecursiveDirectoryIterator::SKIP_DOTS), + \RecursiveIteratorIterator::SELF_FIRST, + ); + + foreach ($iterator as $fileInfo) { + if ($fileInfo->isFile()) { + yield $fileInfo; + } + } + } +} From 9b093a6b57306151ba3abea1cd75f259533a7604 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Sun, 4 Feb 2024 01:24:22 +0400 Subject: [PATCH 02/21] Add Profiler frames --- src/Proto/Frame/Profiler.php | 28 ++++++++++++++++++ src/Proto/Frame/Profiler/File.php | 40 ++++++++++++++++++++++++++ src/Proto/Frame/Profiler/Payload.php | 39 +++++++++++++++++++++++++ src/Proto/Server/Version/V1.php | 1 + src/ProtoType.php | 1 + src/Service/FilesObserver.php | 2 +- src/Service/FilesObserver/FileInfo.php | 2 +- src/Service/FilesObserver/Handler.php | 8 ++++-- 8 files changed, 117 insertions(+), 4 deletions(-) create mode 100644 src/Proto/Frame/Profiler.php create mode 100644 src/Proto/Frame/Profiler/File.php create mode 100644 src/Proto/Frame/Profiler/Payload.php diff --git a/src/Proto/Frame/Profiler.php b/src/Proto/Frame/Profiler.php new file mode 100644 index 00000000..d58c0599 --- /dev/null +++ b/src/Proto/Frame/Profiler.php @@ -0,0 +1,28 @@ + File::fromArray($data, $time), + $data['type'] === Payload::PROFILE_FRAME_TYPE => Payload::fromArray($data, $time), + default => throw new \InvalidArgumentException('Unknown Profile frame type.'), + }; + } +} diff --git a/src/Proto/Frame/Profiler/File.php b/src/Proto/Frame/Profiler/File.php new file mode 100644 index 00000000..c99e81b0 --- /dev/null +++ b/src/Proto/Frame/Profiler/File.php @@ -0,0 +1,40 @@ +fileInfo->toArray()); + } + + public static function fromArray(array $data, DateTimeImmutable $time): static + { + return new self(FileInfo::fromArray($data), $time); + } +} diff --git a/src/Proto/Frame/Profiler/Payload.php b/src/Proto/Frame/Profiler/Payload.php new file mode 100644 index 00000000..d83c5ff5 --- /dev/null +++ b/src/Proto/Frame/Profiler/Payload.php @@ -0,0 +1,39 @@ +payload); + } + + public static function fromArray(array $data, DateTimeImmutable $time): static + { + return new self($data, $time); + } +} diff --git a/src/Proto/Server/Version/V1.php b/src/Proto/Server/Version/V1.php index 348a94f2..6ba1e65b 100644 --- a/src/Proto/Server/Version/V1.php +++ b/src/Proto/Server/Version/V1.php @@ -90,6 +90,7 @@ function (array $item): Frame { ProtoType::VarDumper->value => Frame\VarDumper::fromString($payload, $date), ProtoType::HTTP->value => Frame\Http::fromString($payload, $date), ProtoType::Sentry->value => Frame\Sentry::fromString($payload, $date), + ProtoType::Profiler->value => Frame\Profiler::fromString($payload, $date), default => throw new RuntimeException('Invalid type.'), }; }, diff --git a/src/ProtoType.php b/src/ProtoType.php index d9aaa2dd..12fa3b2d 100644 --- a/src/ProtoType.php +++ b/src/ProtoType.php @@ -15,4 +15,5 @@ enum ProtoType: string case Monolog = 'monolog'; case Binary = 'binary'; case Sentry = 'sentry'; + case Profiler = 'profiler'; } diff --git a/src/Service/FilesObserver.php b/src/Service/FilesObserver.php index 9aa90bb1..2066ea3e 100644 --- a/src/Service/FilesObserver.php +++ b/src/Service/FilesObserver.php @@ -8,7 +8,7 @@ use Buggregator\Trap\Config\FilesObserver as Config; use Buggregator\Trap\Processable; use Buggregator\Trap\Proto\Buffer; -use Buggregator\Trap\Proto\Frame\Profile\File as FileFrame; +use Buggregator\Trap\Proto\Frame\Profiler\File as FileFrame; use Buggregator\Trap\Service\FilesObserver\FileInfo; use Buggregator\Trap\Service\FilesObserver\Handler; use Fiber; diff --git a/src/Service/FilesObserver/FileInfo.php b/src/Service/FilesObserver/FileInfo.php index 12c304ce..3b33d5c7 100644 --- a/src/Service/FilesObserver/FileInfo.php +++ b/src/Service/FilesObserver/FileInfo.php @@ -17,7 +17,7 @@ public function __construct( public static function fromSplFileInfo(\SplFileInfo $fileInfo): self { return new self( - $fileInfo->getPathname(), + $fileInfo->getRealPath(), $fileInfo->getSize(), $fileInfo->getCTime(), $fileInfo->getMTime(), diff --git a/src/Service/FilesObserver/Handler.php b/src/Service/FilesObserver/Handler.php index ebdec478..9eba8309 100644 --- a/src/Service/FilesObserver/Handler.php +++ b/src/Service/FilesObserver/Handler.php @@ -43,17 +43,21 @@ private function syncFiles(): array { $files = $this->getFiles(); $newFiles = []; + $newState = []; foreach ($files as $fileInfo) { - if (\array_key_exists($fileInfo->getRealPath(), $this->cache)) { + $path = $fileInfo->getRealPath(); + if (!\is_string($path) || \array_key_exists($path, $this->cache)) { + $newState[$path] = $this->cache[$path]; continue; } $info = FileInfo::fromSplFileInfo($fileInfo); - $this->cache[$fileInfo->getRealPath()] = $info; + $newState[$path] = $info; $newFiles[] = $info; } + $this->cache = $newState; return $newFiles; } From 371adfb8d62d7603b92132b55a01990970d44bdb Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Sun, 4 Feb 2024 01:48:16 +0400 Subject: [PATCH 03/21] Simplify Plain console renderer --- resources/templates/plain.php | 16 ---------------- src/Sender/Console/Renderer/Plain.php | 23 ++++++++++------------- 2 files changed, 10 insertions(+), 29 deletions(-) delete mode 100644 resources/templates/plain.php diff --git a/resources/templates/plain.php b/resources/templates/plain.php deleted file mode 100644 index f3be719d..00000000 --- a/resources/templates/plain.php +++ /dev/null @@ -1,16 +0,0 @@ -
- - - - - -
date
- -

- -

- -
- -
-
diff --git a/src/Sender/Console/Renderer/Plain.php b/src/Sender/Console/Renderer/Plain.php index dddd3bc8..ba0d874b 100644 --- a/src/Sender/Console/Renderer/Plain.php +++ b/src/Sender/Console/Renderer/Plain.php @@ -6,6 +6,7 @@ use Buggregator\Trap\Proto\Frame; use Buggregator\Trap\Sender\Console\Renderer; +use Buggregator\Trap\Sender\Console\Support\Common; use Symfony\Component\Console\Output\OutputInterface; /** @@ -15,11 +16,6 @@ */ final class Plain implements Renderer { - public function __construct( - private readonly TemplateRenderer $renderer, - ) { - } - public function isSupport(Frame $frame): bool { return true; @@ -27,13 +23,14 @@ public function isSupport(Frame $frame): bool public function render(OutputInterface $output, Frame $frame): void { - $this->renderer->render( - 'plain', - [ - 'date' => $frame->time->format('Y-m-d H:i:s.u'), - 'channel' => \strtoupper($frame->type->value), - 'body' => \htmlspecialchars((string)$frame), - ] - ); + Common::renderHeader1($output, $frame->type->value); + + Common::renderMetadata($output, [ + 'Time' => $frame->time->format('Y-m-d H:i:s.u'), + 'Frame' => $frame::class, + ]); + + Common::renderHeader2($output, 'Payload:'); + $output->writeln((string)$frame); } } From 486b25c32338fd30198cad8de26d70f2211c781f Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Mon, 19 Feb 2024 00:14:17 +0400 Subject: [PATCH 04/21] Add parsing and sending of XHProf files into the UI --- src/Application.php | 25 ++++ src/Config/FilesObserver.php | 11 ++ src/Sender/Frontend/FrameMapper.php | 1 + src/Sender/Frontend/Mapper/Profiler.php | 24 ++++ src/Service/FilesObserver.php | 12 +- src/Service/FilesObserver/FileInfo.php | 13 +++ src/Service/FilesObserver/Filter/XHProf.php | 115 +++++++++++++++++++ src/Service/FilesObserver/FrameConverter.php | 25 ++++ src/Service/FilesObserver/Handler.php | 21 ++-- 9 files changed, 233 insertions(+), 14 deletions(-) create mode 100644 src/Sender/Frontend/Mapper/Profiler.php create mode 100644 src/Service/FilesObserver/Filter/XHProf.php create mode 100644 src/Service/FilesObserver/FrameConverter.php diff --git a/src/Application.php b/src/Application.php index d59a9a5a..4230311a 100644 --- a/src/Application.php +++ b/src/Application.php @@ -8,6 +8,7 @@ use Buggregator\Trap\Handler\Http\Handler\Websocket; use Buggregator\Trap\Handler\Http\Middleware; use Buggregator\Trap\Proto\Buffer; +use Buggregator\Trap\Service\FilesObserver\Filter\XHProf; use Buggregator\Trap\Socket\Client; use Buggregator\Trap\Socket\Server; use Buggregator\Trap\Socket\SocketStream; @@ -64,6 +65,7 @@ public function __construct( $this->processors[] = $inspector; $withFrontend and $this->configureFrontend(8000); + $this->configureFileObserver(); foreach ($map as $config) { $this->prepareServerFiber($config, $inspector, $this->logger); @@ -139,6 +141,11 @@ function () { } } ); + foreach ($this->processors as $processor) { + if ($processor instanceof Cancellable) { + $processor->cancel(); + } + } } /** @@ -217,4 +224,22 @@ public function configureFrontend(int $port): void $this->processors[] = $wsSender; $this->prepareServerFiber(new SocketServer(port: $port), $inspector, $this->logger); } + + private function configureFileObserver() + { + // todo add + // \ini_get('xdebug.output_dir'), + + if (false !== ($path = \ini_get('xhprof.output_dir'))) { + $this->processors[] = new Service\FilesObserver( + $this->logger, + $this->buffer, + new Config\FilesObserver( + path: $path, + converter: XHProf::class, + interval: 2.0, + ), + ); + } + } } diff --git a/src/Config/FilesObserver.php b/src/Config/FilesObserver.php index d6c9e351..ca33471d 100644 --- a/src/Config/FilesObserver.php +++ b/src/Config/FilesObserver.php @@ -4,10 +4,21 @@ namespace Buggregator\Trap\Config; +use Buggregator\Trap\Service\FilesObserver\FrameConverter; + +/** + * @internal + */ final class FilesObserver { + /** + * @param non-empty-string $path + * @param class-string $converter + * @param float $interval + */ public function __construct( public readonly string $path, + public readonly string $converter, public readonly float $interval = 5.0, ) { } diff --git a/src/Sender/Frontend/FrameMapper.php b/src/Sender/Frontend/FrameMapper.php index c69d1f7d..91a6f5d7 100644 --- a/src/Sender/Frontend/FrameMapper.php +++ b/src/Sender/Frontend/FrameMapper.php @@ -20,6 +20,7 @@ public function map(Frame $frame): Event Frame\Sentry\SentryStore::class => (new Mapper\SentryStore())->map($frame), Frame\Sentry\SentryEnvelope::class => (new Mapper\SentryEnvelope())->map($frame), Frame\Monolog::class => (new Mapper\Monolog())->map($frame), + Frame\Profiler\Payload::class => (new Mapper\Profiler())->map($frame), default => throw new \InvalidArgumentException('Unknown frame type ' . $frame::class), }; } diff --git a/src/Sender/Frontend/Mapper/Profiler.php b/src/Sender/Frontend/Mapper/Profiler.php new file mode 100644 index 00000000..3d4776dd --- /dev/null +++ b/src/Sender/Frontend/Mapper/Profiler.php @@ -0,0 +1,24 @@ +payload, + timestamp: (float)$frame->time->format('U.u'), + ); + } +} diff --git a/src/Service/FilesObserver.php b/src/Service/FilesObserver.php index 2066ea3e..b5d09b7f 100644 --- a/src/Service/FilesObserver.php +++ b/src/Service/FilesObserver.php @@ -6,10 +6,10 @@ use Buggregator\Trap\Cancellable; use Buggregator\Trap\Config\FilesObserver as Config; +use Buggregator\Trap\Logger; use Buggregator\Trap\Processable; use Buggregator\Trap\Proto\Buffer; -use Buggregator\Trap\Proto\Frame\Profiler\File as FileFrame; -use Buggregator\Trap\Service\FilesObserver\FileInfo; +use Buggregator\Trap\Proto\Frame; use Buggregator\Trap\Service\FilesObserver\Handler; use Fiber; @@ -23,13 +23,14 @@ final class FilesObserver implements Processable, Cancellable private array $fibers = []; public function __construct( + private readonly Logger $logger, private readonly Buffer $buffer, Config ...$configs, ) { foreach ($configs as $config) { $this->fibers[] = new Fiber(function () use ($config) { - foreach (Handler::generate($config) as $fileInfo) { - $this->propagateFrame($fileInfo); + foreach (Handler::generate($config) as $frame) { + $this->propagateFrame($frame); } }); } @@ -61,9 +62,8 @@ public function cancel(): void $this->fibers = []; } - private function propagateFrame(FileInfo $info): void + private function propagateFrame(Frame $frame): void { - $frame = new FileFrame($info); $this->buffer->addFrame($frame); } } diff --git a/src/Service/FilesObserver/FileInfo.php b/src/Service/FilesObserver/FileInfo.php index 3b33d5c7..0a22945a 100644 --- a/src/Service/FilesObserver/FileInfo.php +++ b/src/Service/FilesObserver/FileInfo.php @@ -4,6 +4,9 @@ namespace Buggregator\Trap\Service\FilesObserver; +/** + * @internal + */ final class FileInfo { public function __construct( @@ -43,4 +46,14 @@ public static function fromArray(array $data): self $data['mtime'], ); } + + public function getExtension(): string + { + return \pathinfo($this->path, PATHINFO_EXTENSION); + } + + public function getName(): string + { + return \pathinfo($this->path, PATHINFO_FILENAME); + } } diff --git a/src/Service/FilesObserver/Filter/XHProf.php b/src/Service/FilesObserver/Filter/XHProf.php new file mode 100644 index 00000000..66c208ef --- /dev/null +++ b/src/Service/FilesObserver/Filter/XHProf.php @@ -0,0 +1,115 @@ +getExtension() === 'xhprof'; + } + + public function convert(FileInfo $file): \Traversable + { + try { + // todo read in a stream + $content = \file_get_contents($file->path); + + $data = \unserialize($content, ['allowed_classes' => false]); + + $payload = $this->dataToPayload($data); + $payload['date'] = $file->mtime; + $payload['hostname'] = \explode('.', $file->getName(), 2)[0]; + + yield new ProfilerFrame\Payload( + $payload, + ); + } catch (\Throwable $e) { + // todo log + var_dump($e->getMessage()); + } + } + + private function dataToPayload(array $data): array + { + /** @var array> $data */ + $peaks = [ + 'cpu' => 0, + 'ct' => 0, + 'mu' => 0, + 'pmu' => 0, + 'wt' => 0, + ]; + + $edges = []; + /** @var array> $parents */ + $parents = []; + $i = 0; + foreach ($data as $key => $value) { + [$caller, $callee] = \explode('==>', $key, 2) + [1 => null]; + if ($callee === null) { + [$caller, $callee] = [null, $caller]; + } + + $edge = [ + 'callee' => $callee, + 'caller' => $caller, + 'cost' => [ + 'cpu' => (int)$value['cpu'], + 'ct' => (int)$value['ct'], + 'mu' => (int)$value['mu'], + 'pmu' => (int)$value['pmu'], + 'wt' => (int)$value['wt'], + ], + ]; + + $edges['e' . ++$i] = &$edge; + $parents[$callee] = &$edge['cost']; + + $peaks['cpu'] = \max($peaks['cpu'], $edge['cost']['cpu']); + $peaks['ct'] = \max($peaks['ct'], $edge['cost']['ct']); + $peaks['mu'] = \max($peaks['mu'], $edge['cost']['mu']); + $peaks['pmu'] = \max($peaks['pmu'], $edge['cost']['pmu']); + $peaks['wt'] = \max($peaks['wt'], $edge['cost']['wt']); + + unset($edge); + } + + $edges = \array_reverse($edges); + + // calc percentages and delta + foreach ($edges as &$value) { + $cost = &$value['cost']; + $cost['p_cpu'] = $peaks['cpu'] > 0 ? \round($cost['cpu'] / $peaks['cpu'] * 100, 2) : 0; + $cost['p_ct'] = $peaks['ct'] > 0 ? \round($cost['ct'] / $peaks['ct'] * 100, 2) : 0; + $cost['p_mu'] = $peaks['mu'] > 0 ? \round($cost['mu'] / $peaks['mu'] * 100, 2) : 0; + $cost['p_pmu'] = $peaks['pmu'] > 0 ? \round($cost['pmu'] / $peaks['pmu'] * 100, 2) : 0; + $cost['p_wt'] = $peaks['wt'] > 0 ? \round($cost['wt'] / $peaks['wt'] * 100, 2) : 0; + + $caller = $value['caller']; + if ($caller !== null) { + $cost['d_cpu'] = $cost['cpu'] - ($parents[$caller]['cpu'] ?? 0); + $cost['d_ct'] = $cost['ct'] - ($parents[$caller]['ct'] ?? 0); + $cost['d_mu'] = $cost['mu'] - ($parents[$caller]['mu'] ?? 0); + $cost['d_pmu'] = $cost['pmu'] - ($parents[$caller]['pmu'] ?? 0); + $cost['d_wt'] = $cost['wt'] - ($parents[$caller]['wt'] ?? 0); + } + unset($value, $cost); + } + unset($parents); + + return [ + 'edges' => $edges, + 'peaks' => $peaks, + ]; + } +} diff --git a/src/Service/FilesObserver/FrameConverter.php b/src/Service/FilesObserver/FrameConverter.php new file mode 100644 index 00000000..48e35680 --- /dev/null +++ b/src/Service/FilesObserver/FrameConverter.php @@ -0,0 +1,25 @@ + + */ + public function convert(FileInfo $file): iterable; +} diff --git a/src/Service/FilesObserver/Handler.php b/src/Service/FilesObserver/Handler.php index 9eba8309..36258bcd 100644 --- a/src/Service/FilesObserver/Handler.php +++ b/src/Service/FilesObserver/Handler.php @@ -5,6 +5,7 @@ namespace Buggregator\Trap\Service\FilesObserver; use Buggregator\Trap\Config\FilesObserver as Config; +use Buggregator\Trap\Proto\Frame; use Buggregator\Trap\Support\Timer; /** @@ -16,22 +17,27 @@ final class Handler /** @var array */ private array $cache = []; private readonly string $path; + private FrameConverter $converter; private function __construct( Config $config, ) { $this->path = $config->path; $this->timer = new Timer($config->interval); + $this->converter = new ($config->converter)(); } /** - * @return \Generator + * @return \Generator */ public static function generate(Config $config): \Generator { $self = new self($config); do { - yield from $self->syncFiles(); + foreach ($self->syncFiles() as $info) { + yield from $self->converter->convert($info); + } + $self->timer->wait()->reset(); } while (true); } @@ -45,14 +51,13 @@ private function syncFiles(): array $newFiles = []; $newState = []; - foreach ($files as $fileInfo) { - $path = $fileInfo->getRealPath(); + foreach ($files as $info) { + $path = $info->path; if (!\is_string($path) || \array_key_exists($path, $this->cache)) { $newState[$path] = $this->cache[$path]; continue; } - $info = FileInfo::fromSplFileInfo($fileInfo); $newState[$path] = $info; $newFiles[] = $info; } @@ -62,7 +67,7 @@ private function syncFiles(): array } /** - * @return \Traversable + * @return \Traversable */ private function getFiles(): \Traversable { @@ -73,8 +78,8 @@ private function getFiles(): \Traversable ); foreach ($iterator as $fileInfo) { - if ($fileInfo->isFile()) { - yield $fileInfo; + if ($fileInfo->isFile() && $this->converter->validate($info = FileInfo::fromSplFileInfo($fileInfo))) { + yield $info; } } } From cf367cebf79a807bb274102da5315b08dacf6f4e Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Tue, 27 Feb 2024 13:13:21 +0400 Subject: [PATCH 05/21] Better profile spans sorting; temporary fixes --- src/Service/FilesObserver/Filter/XHProf.php | 60 +++++++++++++++++++-- src/Socket/Client.php | 3 +- src/Socket/Server.php | 3 +- 3 files changed, 61 insertions(+), 5 deletions(-) diff --git a/src/Service/FilesObserver/Filter/XHProf.php b/src/Service/FilesObserver/Filter/XHProf.php index 66c208ef..50e8bf69 100644 --- a/src/Service/FilesObserver/Filter/XHProf.php +++ b/src/Service/FilesObserver/Filter/XHProf.php @@ -53,7 +53,13 @@ private function dataToPayload(array $data): array $edges = []; /** @var array> $parents */ $parents = []; + /** @var array> $parents items with unknown caller */ + $callerLess = []; $i = 0; + \uasort($data, static function (array $a, array $b) { + return $b['wt'] <=> $a['wt']; + }); + // $data = \array_reverse($data, true); foreach ($data as $key => $value) { [$caller, $callee] = \explode('==>', $key, 2) + [1 => null]; if ($callee === null) { @@ -72,8 +78,29 @@ private function dataToPayload(array $data): array ], ]; - $edges['e' . ++$i] = &$edge; - $parents[$callee] = &$edge['cost']; + // if (++$j > 10) { + // print_r(\array_keys($parents)); + // print_r(\array_keys($edges)); + // die; + // } + + if ($caller !== null && !\array_key_exists($caller, $parents) && $caller !== $callee) { + $callerLess[$caller][] = &$edge; + // echo "CALLER: $caller\n"; + // echo "CALLEE: $callee\n"; + } else { + // echo "CALLER: $callee\n"; + $parents[$callee] = &$edge['cost']; + $edges['e' . ++$i] = &$edge; + if (\array_key_exists($callee, $callerLess)) { + foreach ($callerLess[$callee] as $item) { + $edges['a' . ++$i] = &$item; + $parents[$item['callee']] = &$item['cost']; + unset($item); + } + unset($callerLess[$callee]); + } + } $peaks['cpu'] = \max($peaks['cpu'], $edge['cost']['cpu']); $peaks['ct'] = \max($peaks['ct'], $edge['cost']['ct']); @@ -84,7 +111,34 @@ private function dataToPayload(array $data): array unset($edge); } - $edges = \array_reverse($edges); + // Merge callerLess items + while ($callerLess !== []) { + $merged = 0; + foreach ($callerLess as $caller => $items) { + if (\array_key_exists($caller, $parents)) { + foreach ($items as &$item) { + $edges['c' . ++$i] = &$item; + $parents[$item['callee']] = &$item['cost']; + unset($item); + } + ++$merged; + unset($callerLess[$caller]); + } + } + + // Just merge all as is + if ($merged === 0) { + foreach ($callerLess as $items) { + foreach ($items as &$item) { + $edges['f' . ++$i] = &$item; + $parents[$item['callee']] = &$item['cost']; + unset($item); + } + } + + $callerLess = []; + } + } // calc percentages and delta foreach ($edges as &$value) { diff --git a/src/Socket/Client.php b/src/Socket/Client.php index a8dad04b..4898c7bf 100644 --- a/src/Socket/Client.php +++ b/src/Socket/Client.php @@ -99,7 +99,8 @@ public function process(): void if ($this->toDisconnect && $this->writeQueue === []) { // Wait for the socket buffer to be flushed. - (new Timer(0.005))->wait(); + // todo + // (new Timer(0.005))->wait(); throw new ClientDisconnected(); } Fiber::suspend(); diff --git a/src/Socket/Server.php b/src/Socket/Server.php index 7cb8daeb..990fbc82 100644 --- a/src/Socket/Server.php +++ b/src/Socket/Server.php @@ -43,7 +43,8 @@ private function __construct( ) { $this->socket = @\socket_create_listen($port); /** @link https://github.com/buggregator/trap/pull/14 */ - \socket_set_option($this->socket, \SOL_SOCKET, \SO_LINGER, ['l_linger' => 0, 'l_onoff' => 1]); + // todo + // \socket_set_option($this->socket, \SOL_SOCKET, \SO_LINGER, ['l_linger' => 0, 'l_onoff' => 1]); if ($this->socket === false) { throw new \RuntimeException('Socket create failed.'); From aaed096a420854306ddc00fa6eae3ef954fda90c Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Wed, 10 Apr 2024 18:28:54 +0400 Subject: [PATCH 06/21] Correct buffer size in the HTTP Emitter --- src/Handler/Http/Emitter.php | 3 ++- src/Socket/Client.php | 4 ++-- src/Socket/Server.php | 1 - 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Handler/Http/Emitter.php b/src/Handler/Http/Emitter.php index 9930f38f..20739b73 100644 --- a/src/Handler/Http/Emitter.php +++ b/src/Handler/Http/Emitter.php @@ -17,8 +17,9 @@ final class Emitter { /** * Preferred chunk size to be read from the stream before emitting. A value of 0 disables stream response. + * Value greater than 100 KB might not work with Linux Docker. */ - public static int $bufferSize = 2_097_152; // 2MB + public static int $bufferSize = 1024 * 100; /** * Send {@see ResponseInterface} to the client. diff --git a/src/Socket/Client.php b/src/Socket/Client.php index 4898c7bf..2594393a 100644 --- a/src/Socket/Client.php +++ b/src/Socket/Client.php @@ -98,9 +98,9 @@ public function process(): void } if ($this->toDisconnect && $this->writeQueue === []) { - // Wait for the socket buffer to be flushed. - // todo + # Wait for the socket buffer to be flushed. // (new Timer(0.005))->wait(); + throw new ClientDisconnected(); } Fiber::suspend(); diff --git a/src/Socket/Server.php b/src/Socket/Server.php index 990fbc82..73c1ae50 100644 --- a/src/Socket/Server.php +++ b/src/Socket/Server.php @@ -43,7 +43,6 @@ private function __construct( ) { $this->socket = @\socket_create_listen($port); /** @link https://github.com/buggregator/trap/pull/14 */ - // todo // \socket_set_option($this->socket, \SOL_SOCKET, \SO_LINGER, ['l_linger' => 0, 'l_onoff' => 1]); if ($this->socket === false) { From 2321f1637b51d9145ce352564ab71c18c013a642 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Sun, 14 Apr 2024 22:59:50 +0400 Subject: [PATCH 07/21] Refactor Profiler frame --- src/Proto/Frame/Profiler.php | 27 ++++--- src/Proto/Frame/Profiler/File.php | 40 ----------- src/Proto/Frame/Profiler/Payload.php | 80 ++++++++++++++++----- src/Proto/Frame/Profiler/Type.php | 15 ++++ src/Sender/Console/Renderer/Profiler.php | 42 +++++++++++ src/Sender/ConsoleSender.php | 1 + src/Sender/Frontend/FrameMapper.php | 2 +- src/Sender/Frontend/Mapper/Profiler.php | 4 +- src/Service/FilesObserver/Filter/XHProf.php | 8 ++- 9 files changed, 150 insertions(+), 69 deletions(-) delete mode 100644 src/Proto/Frame/Profiler/File.php create mode 100644 src/Proto/Frame/Profiler/Type.php create mode 100644 src/Sender/Console/Renderer/Profiler.php diff --git a/src/Proto/Frame/Profiler.php b/src/Proto/Frame/Profiler.php index d58c0599..a1b72e42 100644 --- a/src/Proto/Frame/Profiler.php +++ b/src/Proto/Frame/Profiler.php @@ -5,8 +5,7 @@ namespace Buggregator\Trap\Proto\Frame; use Buggregator\Trap\Proto\Frame; -use Buggregator\Trap\Proto\Frame\Profiler\File; -use Buggregator\Trap\Proto\Frame\Profiler\Payload; +use Buggregator\Trap\ProtoType; use Buggregator\Trap\Support\Json; use DateTimeImmutable; @@ -14,15 +13,27 @@ * @internal * @psalm-internal Buggregator */ -abstract class Profiler extends Frame +final class Profiler extends Frame { + public function __construct( + public readonly Frame\Profiler\Payload $payload, + DateTimeImmutable $time = new DateTimeImmutable(), + ) { + parent::__construct(ProtoType::Profiler, $time); + } + public static function fromString(string $payload, DateTimeImmutable $time): static { $data = Json::decode($payload); - return match (true) { - $data['type'] === File::PROFILE_FRAME_TYPE => File::fromArray($data, $time), - $data['type'] === Payload::PROFILE_FRAME_TYPE => Payload::fromArray($data, $time), - default => throw new \InvalidArgumentException('Unknown Profile frame type.'), - }; + + return new self(Frame\Profiler\Payload::fromArray($data), $time); + } + + /** + * @throws \JsonException + */ + public function __toString(): string + { + return Json::encode($this->payload->jsonSerialize() + ['']); } } diff --git a/src/Proto/Frame/Profiler/File.php b/src/Proto/Frame/Profiler/File.php deleted file mode 100644 index c99e81b0..00000000 --- a/src/Proto/Frame/Profiler/File.php +++ /dev/null @@ -1,40 +0,0 @@ -fileInfo->toArray()); - } - - public static function fromArray(array $data, DateTimeImmutable $time): static - { - return new self(FileInfo::fromArray($data), $time); - } -} diff --git a/src/Proto/Frame/Profiler/Payload.php b/src/Proto/Frame/Profiler/Payload.php index d83c5ff5..c65cc398 100644 --- a/src/Proto/Frame/Profiler/Payload.php +++ b/src/Proto/Frame/Profiler/Payload.php @@ -4,36 +4,84 @@ namespace Buggregator\Trap\Proto\Frame\Profiler; -use Buggregator\Trap\Proto\Frame; -use Buggregator\Trap\ProtoType; -use Buggregator\Trap\Support\Json; -use DateTimeImmutable; +use Buggregator\Trap\Proto\Frame\Profiler\Type as PayloadType; +use Buggregator\Trap\Service\FilesObserver\FileInfo; /** + * @psalm-type Metadata = array{ + * date: int, + * hostname: non-empty-string, + * filename?: non-empty-string, + * ... + * } + * @psalm-type Calls = array{ + * edges: array, + * peaks: array + * } + * * @internal * @psalm-internal Buggregator */ -final class Payload extends Frame\Profiler +class Payload implements \JsonSerializable { - public const PROFILE_FRAME_TYPE = 'payload'; - - public function __construct( - public array $payload, - DateTimeImmutable $time = new DateTimeImmutable(), + /** + * @param PayloadType $type + * @param Metadata $metadata + * @param \Closure(): Calls $callsProvider + */ + private function __construct( + public readonly PayloadType $type, + private array $metadata, + private \Closure $callsProvider, ) { - parent::__construct(ProtoType::Profiler, $time); + $this->metadata['type'] = $type->value; + } + + public static function fromArray(array $data, ?Type $type = null): static + { + $metadata = $data; + unset($metadata['edges'], $metadata['peaks']); + return new static( + $type ?? PayloadType::from($data['type']), + $metadata, + static fn(): array => $data, + ); + } + + public static function fromFile(FileInfo $fileInfo): static + { + // todo + // $metadata = $data; + // unset($metadata['edges'], $metadata['peaks']); + // return new static(PayloadType::from($data['type']), $metadata, fn(): array => $data); } /** - * @throws \JsonException + * @return Calls */ - public function __toString(): string + public function getCalls(): array { - return Json::encode($this->payload); + return ($this->callsProvider)(); } - public static function fromArray(array $data, DateTimeImmutable $time): static + /** + * @return Metadata + */ + public function getMetadata(): array + { + return $this->metadata; + } + + public function toArray(): array + { + return ['type' => $this->type->value] + $this->getCalls() + $this->getMetadata(); + } + + /** + * @return array{type: non-empty-string}&Calls&Metadata + */ + public function jsonSerialize(): array { - return new self($data, $time); + return $this->toArray(); } } diff --git a/src/Proto/Frame/Profiler/Type.php b/src/Proto/Frame/Profiler/Type.php new file mode 100644 index 00000000..3b445d2e --- /dev/null +++ b/src/Proto/Frame/Profiler/Type.php @@ -0,0 +1,15 @@ + + * + * @internal + */ +final class Profiler implements Renderer +{ + public function isSupport(Frame $frame): bool + { + return $frame->type === ProtoType::Profiler; + } + + public function render(OutputInterface $output, Frame $frame): void + { + \assert($frame instanceof Frame\Profiler); + + $subtitle = $frame->payload->type->value; + Common::renderHeader1($output, 'PROFILER', $subtitle); + + $metadata = $frame->payload->getMetadata(); + $data = []; + isset($metadata['date']) && \is_numeric($metadata['date']) + and $data['Time'] = new DateTimeImmutable('@' . $metadata['date']); + isset($metadata['hostname']) and $data['Hostname'] = $metadata['hostname']; + isset($metadata['filename']) and $data['File name'] = $metadata['filename']; + + Common::renderMetadata($output, $data); + } +} diff --git a/src/Sender/ConsoleSender.php b/src/Sender/ConsoleSender.php index 1d626a1a..adf846f9 100644 --- a/src/Sender/ConsoleSender.php +++ b/src/Sender/ConsoleSender.php @@ -35,6 +35,7 @@ public static function create(OutputInterface $output): self $renderer->register(new Renderer\Monolog($templateRenderer)); $renderer->register(new Renderer\Smtp()); $renderer->register(new Renderer\Http()); + $renderer->register(new Renderer\Profiler()); $renderer->register(new Renderer\Binary()); $renderer->register(new Renderer\Plain($templateRenderer)); diff --git a/src/Sender/Frontend/FrameMapper.php b/src/Sender/Frontend/FrameMapper.php index 91a6f5d7..da83fe1c 100644 --- a/src/Sender/Frontend/FrameMapper.php +++ b/src/Sender/Frontend/FrameMapper.php @@ -20,7 +20,7 @@ public function map(Frame $frame): Event Frame\Sentry\SentryStore::class => (new Mapper\SentryStore())->map($frame), Frame\Sentry\SentryEnvelope::class => (new Mapper\SentryEnvelope())->map($frame), Frame\Monolog::class => (new Mapper\Monolog())->map($frame), - Frame\Profiler\Payload::class => (new Mapper\Profiler())->map($frame), + Frame\Profiler::class => (new Mapper\Profiler())->map($frame), default => throw new \InvalidArgumentException('Unknown frame type ' . $frame::class), }; } diff --git a/src/Sender/Frontend/Mapper/Profiler.php b/src/Sender/Frontend/Mapper/Profiler.php index 3d4776dd..7c7accd2 100644 --- a/src/Sender/Frontend/Mapper/Profiler.php +++ b/src/Sender/Frontend/Mapper/Profiler.php @@ -12,12 +12,12 @@ */ final class Profiler { - public function map(\Buggregator\Trap\Proto\Frame\Profiler\Payload $frame): Event + public function map(\Buggregator\Trap\Proto\Frame\Profiler $frame): Event { return new Event( uuid: Uuid::generate(), type: 'profiler', - payload: $frame->payload, + payload: $frame->payload->toArray(), timestamp: (float)$frame->time->format('U.u'), ); } diff --git a/src/Service/FilesObserver/Filter/XHProf.php b/src/Service/FilesObserver/Filter/XHProf.php index 50e8bf69..b01b072a 100644 --- a/src/Service/FilesObserver/Filter/XHProf.php +++ b/src/Service/FilesObserver/Filter/XHProf.php @@ -29,10 +29,14 @@ public function convert(FileInfo $file): \Traversable $payload = $this->dataToPayload($data); $payload['date'] = $file->mtime; $payload['hostname'] = \explode('.', $file->getName(), 2)[0]; + $payload['filename'] = $file->getName(); - yield new ProfilerFrame\Payload( - $payload, + yield new ProfilerFrame( + ProfilerFrame\Payload::fromArray($payload, ProfilerFrame\Type::XHProf), ); + // yield new ProfilerFrame( + // ProfilerFrame\Payload::fromFile($file), + // ); } catch (\Throwable $e) { // todo log var_dump($e->getMessage()); From df472bc427ff6226a5689fc18eea3baa67a54fd6 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Tue, 16 Apr 2024 14:39:43 +0400 Subject: [PATCH 08/21] Cleanup --- src/Proto/Frame/Profiler/Payload.php | 21 ++++++++++------ src/Service/FilesObserver/Filter/XHProf.php | 27 +++++++++++---------- 2 files changed, 27 insertions(+), 21 deletions(-) diff --git a/src/Proto/Frame/Profiler/Payload.php b/src/Proto/Frame/Profiler/Payload.php index c65cc398..63d9bf4a 100644 --- a/src/Proto/Frame/Profiler/Payload.php +++ b/src/Proto/Frame/Profiler/Payload.php @@ -37,6 +37,19 @@ private function __construct( $this->metadata['type'] = $type->value; } + /** + * @param PayloadType $type + * @param Metadata $metadata + * @param \Closure(): Calls $callsProvider + */ + public static function new( + PayloadType $type, + array $metadata, + \Closure $callsProvider, + ): self { + return new static($type, $metadata, $callsProvider); + } + public static function fromArray(array $data, ?Type $type = null): static { $metadata = $data; @@ -48,14 +61,6 @@ public static function fromArray(array $data, ?Type $type = null): static ); } - public static function fromFile(FileInfo $fileInfo): static - { - // todo - // $metadata = $data; - // unset($metadata['edges'], $metadata['peaks']); - // return new static(PayloadType::from($data['type']), $metadata, fn(): array => $data); - } - /** * @return Calls */ diff --git a/src/Service/FilesObserver/Filter/XHProf.php b/src/Service/FilesObserver/Filter/XHProf.php index b01b072a..ae449192 100644 --- a/src/Service/FilesObserver/Filter/XHProf.php +++ b/src/Service/FilesObserver/Filter/XHProf.php @@ -21,22 +21,23 @@ public function validate(FileInfo $file): bool public function convert(FileInfo $file): \Traversable { try { - // todo read in a stream - $content = \file_get_contents($file->path); - - $data = \unserialize($content, ['allowed_classes' => false]); - - $payload = $this->dataToPayload($data); - $payload['date'] = $file->mtime; - $payload['hostname'] = \explode('.', $file->getName(), 2)[0]; - $payload['filename'] = $file->getName(); + $metadata = [ + 'date' => $file->mtime, + 'hostname' => \explode('.', $file->getName(), 2)[0], + 'filename' => $file->getName(), + ]; yield new ProfilerFrame( - ProfilerFrame\Payload::fromArray($payload, ProfilerFrame\Type::XHProf), + ProfilerFrame\Payload::new( + type: ProfilerFrame\Type::XHProf, + metadata: $metadata, + callsProvider: function () use ($file): array { + $content = \file_get_contents($file->path); + $data = \unserialize($content, ['allowed_classes' => false]); + return $this->dataToPayload($data); + }, + ), ); - // yield new ProfilerFrame( - // ProfilerFrame\Payload::fromFile($file), - // ); } catch (\Throwable $e) { // todo log var_dump($e->getMessage()); From a276d29173f895e2ecf1da251225d1be97eb9e4e Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Tue, 16 Apr 2024 15:07:00 +0400 Subject: [PATCH 09/21] Handle FS error when profiler files are read --- src/Info.php | 2 +- src/Service/FilesObserver.php | 2 +- src/Service/FilesObserver/Filter/XHProf.php | 1 + src/Service/FilesObserver/Handler.php | 28 +++++++++++++-------- 4 files changed, 21 insertions(+), 12 deletions(-) diff --git a/src/Info.php b/src/Info.php index b5f9104b..dedfa639 100644 --- a/src/Info.php +++ b/src/Info.php @@ -10,7 +10,7 @@ class Info { public const NAME = 'Buggregator Trap'; - public const VERSION = '1.4.6'; + public const VERSION = '1.4.7'; public const LOGO_CLI_COLOR = <<fibers[] = new Fiber(function () use ($config) { - foreach (Handler::generate($config) as $frame) { + foreach (Handler::generate($config, $this->logger) as $frame) { $this->propagateFrame($frame); } }); diff --git a/src/Service/FilesObserver/Filter/XHProf.php b/src/Service/FilesObserver/Filter/XHProf.php index ae449192..b0771bc0 100644 --- a/src/Service/FilesObserver/Filter/XHProf.php +++ b/src/Service/FilesObserver/Filter/XHProf.php @@ -27,6 +27,7 @@ public function convert(FileInfo $file): \Traversable 'filename' => $file->getName(), ]; + /** @psalm-suppress MixedArgumentTypeCoercion */ yield new ProfilerFrame( ProfilerFrame\Payload::new( type: ProfilerFrame\Type::XHProf, diff --git a/src/Service/FilesObserver/Handler.php b/src/Service/FilesObserver/Handler.php index 36258bcd..85258cde 100644 --- a/src/Service/FilesObserver/Handler.php +++ b/src/Service/FilesObserver/Handler.php @@ -5,6 +5,7 @@ namespace Buggregator\Trap\Service\FilesObserver; use Buggregator\Trap\Config\FilesObserver as Config; +use Buggregator\Trap\Logger; use Buggregator\Trap\Proto\Frame; use Buggregator\Trap\Support\Timer; @@ -21,6 +22,7 @@ final class Handler private function __construct( Config $config, + private readonly Logger $logger, ) { $this->path = $config->path; $this->timer = new Timer($config->interval); @@ -30,9 +32,9 @@ private function __construct( /** * @return \Generator */ - public static function generate(Config $config): \Generator + public static function generate(Config $config, Logger $logger): \Generator { - $self = new self($config); + $self = new self($config, $logger); do { foreach ($self->syncFiles() as $info) { yield from $self->converter->convert($info); @@ -71,16 +73,22 @@ private function syncFiles(): array */ private function getFiles(): \Traversable { - /** @var \Iterator<\SplFileInfo> $iterator */ - $iterator = new \RecursiveIteratorIterator( - new \RecursiveDirectoryIterator($this->path, \RecursiveDirectoryIterator::SKIP_DOTS), - \RecursiveIteratorIterator::SELF_FIRST, - ); + try { + /** @var \Iterator<\SplFileInfo> $iterator */ + $iterator = new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator($this->path, \RecursiveDirectoryIterator::SKIP_DOTS), + \RecursiveIteratorIterator::SELF_FIRST, + ); - foreach ($iterator as $fileInfo) { - if ($fileInfo->isFile() && $this->converter->validate($info = FileInfo::fromSplFileInfo($fileInfo))) { - yield $info; + foreach ($iterator as $fileInfo) { + if ($fileInfo->isFile() && $this->converter->validate($info = FileInfo::fromSplFileInfo($fileInfo))) { + yield $info; + } } + } catch (\Throwable $e) { + $this->logger->info('Failed to read files from path `%s`', $this->path); + $this->logger->exception($e); + return []; } } } From c9c6aeb1c844207c744ff3bd17e48445aeefe26b Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 31 May 2024 15:28:38 +0000 Subject: [PATCH 10/21] style(php-cs-fixer): lint php files and fix coding standards --- src/Application.php | 2 +- src/Config/FilesObserver.php | 3 +-- src/Proto/Frame/Profiler.php | 5 ++--- src/Proto/Frame/Profiler/Payload.php | 1 - src/Sender/Console/Renderer/Plain.php | 2 +- src/Sender/Console/Renderer/Profiler.php | 3 +-- src/Sender/Frontend/Mapper/Profiler.php | 2 +- src/Service/FilesObserver.php | 6 +++--- src/Service/FilesObserver/FileInfo.php | 23 ++++++++++----------- src/Service/FilesObserver/Filter/XHProf.php | 12 +++++------ src/Service/FilesObserver/Handler.php | 3 +++ 11 files changed, 30 insertions(+), 32 deletions(-) diff --git a/src/Application.php b/src/Application.php index 7292b744..b7eba093 100644 --- a/src/Application.php +++ b/src/Application.php @@ -228,7 +228,7 @@ private function createServer(SocketServer $config, Inspector $inspector): Serve ); } - private function configureFileObserver() + private function configureFileObserver(): void { // todo add // \ini_get('xdebug.output_dir'), diff --git a/src/Config/FilesObserver.php b/src/Config/FilesObserver.php index ca33471d..2818e54a 100644 --- a/src/Config/FilesObserver.php +++ b/src/Config/FilesObserver.php @@ -20,6 +20,5 @@ public function __construct( public readonly string $path, public readonly string $converter, public readonly float $interval = 5.0, - ) { - } + ) {} } diff --git a/src/Proto/Frame/Profiler.php b/src/Proto/Frame/Profiler.php index a1b72e42..578faac9 100644 --- a/src/Proto/Frame/Profiler.php +++ b/src/Proto/Frame/Profiler.php @@ -7,7 +7,6 @@ use Buggregator\Trap\Proto\Frame; use Buggregator\Trap\ProtoType; use Buggregator\Trap\Support\Json; -use DateTimeImmutable; /** * @internal @@ -17,12 +16,12 @@ final class Profiler extends Frame { public function __construct( public readonly Frame\Profiler\Payload $payload, - DateTimeImmutable $time = new DateTimeImmutable(), + \DateTimeImmutable $time = new \DateTimeImmutable(), ) { parent::__construct(ProtoType::Profiler, $time); } - public static function fromString(string $payload, DateTimeImmutable $time): static + public static function fromString(string $payload, \DateTimeImmutable $time): static { $data = Json::decode($payload); diff --git a/src/Proto/Frame/Profiler/Payload.php b/src/Proto/Frame/Profiler/Payload.php index 63d9bf4a..4162b40f 100644 --- a/src/Proto/Frame/Profiler/Payload.php +++ b/src/Proto/Frame/Profiler/Payload.php @@ -5,7 +5,6 @@ namespace Buggregator\Trap\Proto\Frame\Profiler; use Buggregator\Trap\Proto\Frame\Profiler\Type as PayloadType; -use Buggregator\Trap\Service\FilesObserver\FileInfo; /** * @psalm-type Metadata = array{ diff --git a/src/Sender/Console/Renderer/Plain.php b/src/Sender/Console/Renderer/Plain.php index ba0d874b..13eaa272 100644 --- a/src/Sender/Console/Renderer/Plain.php +++ b/src/Sender/Console/Renderer/Plain.php @@ -31,6 +31,6 @@ public function render(OutputInterface $output, Frame $frame): void ]); Common::renderHeader2($output, 'Payload:'); - $output->writeln((string)$frame); + $output->writeln((string) $frame); } } diff --git a/src/Sender/Console/Renderer/Profiler.php b/src/Sender/Console/Renderer/Profiler.php index 0799ff4d..2ebaba68 100644 --- a/src/Sender/Console/Renderer/Profiler.php +++ b/src/Sender/Console/Renderer/Profiler.php @@ -8,7 +8,6 @@ use Buggregator\Trap\ProtoType; use Buggregator\Trap\Sender\Console\Renderer; use Buggregator\Trap\Sender\Console\Support\Common; -use DateTimeImmutable; use Symfony\Component\Console\Output\OutputInterface; /** @@ -33,7 +32,7 @@ public function render(OutputInterface $output, Frame $frame): void $metadata = $frame->payload->getMetadata(); $data = []; isset($metadata['date']) && \is_numeric($metadata['date']) - and $data['Time'] = new DateTimeImmutable('@' . $metadata['date']); + and $data['Time'] = new \DateTimeImmutable('@' . $metadata['date']); isset($metadata['hostname']) and $data['Hostname'] = $metadata['hostname']; isset($metadata['filename']) and $data['File name'] = $metadata['filename']; diff --git a/src/Sender/Frontend/Mapper/Profiler.php b/src/Sender/Frontend/Mapper/Profiler.php index 7c7accd2..c0ec77df 100644 --- a/src/Sender/Frontend/Mapper/Profiler.php +++ b/src/Sender/Frontend/Mapper/Profiler.php @@ -18,7 +18,7 @@ public function map(\Buggregator\Trap\Proto\Frame\Profiler $frame): Event uuid: Uuid::generate(), type: 'profiler', payload: $frame->payload->toArray(), - timestamp: (float)$frame->time->format('U.u'), + timestamp: (float) $frame->time->format('U.u'), ); } } diff --git a/src/Service/FilesObserver.php b/src/Service/FilesObserver.php index e4b4a272..98ad0a2e 100644 --- a/src/Service/FilesObserver.php +++ b/src/Service/FilesObserver.php @@ -11,7 +11,6 @@ use Buggregator\Trap\Proto\Buffer; use Buggregator\Trap\Proto\Frame; use Buggregator\Trap\Service\FilesObserver\Handler; -use Fiber; /** * @internal @@ -19,7 +18,8 @@ final class FilesObserver implements Processable, Cancellable { private bool $cancelled = false; - /** @var Fiber[] */ + + /** @var \Fiber[] */ private array $fibers = []; public function __construct( @@ -28,7 +28,7 @@ public function __construct( Config ...$configs, ) { foreach ($configs as $config) { - $this->fibers[] = new Fiber(function () use ($config) { + $this->fibers[] = new \Fiber(function () use ($config): void { foreach (Handler::generate($config, $this->logger) as $frame) { $this->propagateFrame($frame); } diff --git a/src/Service/FilesObserver/FileInfo.php b/src/Service/FilesObserver/FileInfo.php index 0a22945a..c8572edb 100644 --- a/src/Service/FilesObserver/FileInfo.php +++ b/src/Service/FilesObserver/FileInfo.php @@ -14,8 +14,7 @@ public function __construct( public readonly int $size, public readonly int $ctime, public readonly int $mtime, - ) { - } + ) {} public static function fromSplFileInfo(\SplFileInfo $fileInfo): self { @@ -27,16 +26,6 @@ public static function fromSplFileInfo(\SplFileInfo $fileInfo): self ); } - public function toArray(): array - { - return [ - 'path' => $this->path, - 'size' => $this->size, - 'ctime' => $this->ctime, - 'mtime' => $this->mtime, - ]; - } - public static function fromArray(array $data): self { return new self( @@ -47,6 +36,16 @@ public static function fromArray(array $data): self ); } + public function toArray(): array + { + return [ + 'path' => $this->path, + 'size' => $this->size, + 'ctime' => $this->ctime, + 'mtime' => $this->mtime, + ]; + } + public function getExtension(): string { return \pathinfo($this->path, PATHINFO_EXTENSION); diff --git a/src/Service/FilesObserver/Filter/XHProf.php b/src/Service/FilesObserver/Filter/XHProf.php index b0771bc0..02cfde68 100644 --- a/src/Service/FilesObserver/Filter/XHProf.php +++ b/src/Service/FilesObserver/Filter/XHProf.php @@ -41,7 +41,7 @@ public function convert(FileInfo $file): \Traversable ); } catch (\Throwable $e) { // todo log - var_dump($e->getMessage()); + \var_dump($e->getMessage()); } } @@ -76,11 +76,11 @@ private function dataToPayload(array $data): array 'callee' => $callee, 'caller' => $caller, 'cost' => [ - 'cpu' => (int)$value['cpu'], - 'ct' => (int)$value['ct'], - 'mu' => (int)$value['mu'], - 'pmu' => (int)$value['pmu'], - 'wt' => (int)$value['wt'], + 'cpu' => (int) $value['cpu'], + 'ct' => (int) $value['ct'], + 'mu' => (int) $value['mu'], + 'pmu' => (int) $value['pmu'], + 'wt' => (int) $value['wt'], ], ]; diff --git a/src/Service/FilesObserver/Handler.php b/src/Service/FilesObserver/Handler.php index 85258cde..f2f9a9e3 100644 --- a/src/Service/FilesObserver/Handler.php +++ b/src/Service/FilesObserver/Handler.php @@ -15,9 +15,12 @@ final class Handler { private readonly Timer $timer; + /** @var array */ private array $cache = []; + private readonly string $path; + private FrameConverter $converter; private function __construct( From c15e5fea056b96bbcd7736e5d6ed0aee55472a93 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Fri, 31 May 2024 22:22:59 +0400 Subject: [PATCH 11/21] feat: added configs for profilers with mapping to php.ini; added PhpIni config attribute; files observer is created using container now --- src/Application.php | 23 +++++--------- src/Config/FilesObserver.php | 24 -------------- src/Config/Server/Files/ObserverConfig.php | 32 +++++++++++++++++++ src/Config/Server/Files/SPX.php | 17 ++++++++++ src/Config/Server/Files/XDebug.php | 17 ++++++++++ src/Config/Server/Files/XHProf.php | 20 ++++++++++++ src/Service/Config/ConfigLoader.php | 5 +++ src/Service/Config/PhpIni.php | 16 ++++++++++ src/Service/Container.php | 5 ++- src/Service/FilesObserver.php | 14 +++++++-- src/Service/FilesObserver/FrameConverter.php | 2 +- src/Service/FilesObserver/Handler.php | 33 +++++++++++++------- 12 files changed, 154 insertions(+), 54 deletions(-) delete mode 100644 src/Config/FilesObserver.php create mode 100644 src/Config/Server/Files/ObserverConfig.php create mode 100644 src/Config/Server/Files/SPX.php create mode 100644 src/Config/Server/Files/XDebug.php create mode 100644 src/Config/Server/Files/XHProf.php create mode 100644 src/Service/Config/PhpIni.php diff --git a/src/Application.php b/src/Application.php index b7eba093..dd041d12 100644 --- a/src/Application.php +++ b/src/Application.php @@ -4,13 +4,15 @@ namespace Buggregator\Trap; +use Buggregator\Trap\Config\Server\Files\SPX as SPXFileConfig; +use Buggregator\Trap\Config\Server\Files\XDebug as XDebugFileConfig; +use Buggregator\Trap\Config\Server\Files\XHProf as XHProfFileConfig; use Buggregator\Trap\Config\Server\Frontend as FrontendConfig; use Buggregator\Trap\Config\Server\SocketServer; use Buggregator\Trap\Handler\Http\Handler\Websocket; use Buggregator\Trap\Handler\Http\Middleware; use Buggregator\Trap\Proto\Buffer; use Buggregator\Trap\Service\Container; -use Buggregator\Trap\Service\FilesObserver\Filter\XHProf; use Buggregator\Trap\Socket\Client; use Buggregator\Trap\Socket\Server; use Buggregator\Trap\Socket\SocketStream; @@ -230,19 +232,10 @@ private function createServer(SocketServer $config, Inspector $inspector): Serve private function configureFileObserver(): void { - // todo add - // \ini_get('xdebug.output_dir'), - - if (false !== ($path = \ini_get('xhprof.output_dir'))) { - $this->processors[] = new Service\FilesObserver( - $this->logger, - $this->buffer, - new Config\FilesObserver( - path: $path, - converter: XHProf::class, - interval: 2.0, - ), - ); - } + $this->processors[] = $this->container->make(Service\FilesObserver::class, [ + $this->container->get(XHProfFileConfig::class), + $this->container->get(XDebugFileConfig::class), + $this->container->get(SPXFileConfig::class), + ]); } } diff --git a/src/Config/FilesObserver.php b/src/Config/FilesObserver.php deleted file mode 100644 index 2818e54a..00000000 --- a/src/Config/FilesObserver.php +++ /dev/null @@ -1,24 +0,0 @@ - $converter - * @param float $interval - */ - public function __construct( - public readonly string $path, - public readonly string $converter, - public readonly float $interval = 5.0, - ) {} -} diff --git a/src/Config/Server/Files/ObserverConfig.php b/src/Config/Server/Files/ObserverConfig.php new file mode 100644 index 00000000..96db4b8b --- /dev/null +++ b/src/Config/Server/Files/ObserverConfig.php @@ -0,0 +1,32 @@ +|null $converter */ + public ?string $converterClass = null; + + /** @var float Scan interval in seconds */ + public float $scanInterval = 5.0; + + /** + * @psalm-assert-if-true non-empty-string $this->path + * @psalm-assert-if-true class-string $this->converterClass + */ + public function isValid(): bool + { + return $this->path !== null && $this->converterClass !== null && $this->path !== '' + && \is_a($this->converterClass, FrameConverter::class, true) && $this->scanInterval > 0.0; + } +} diff --git a/src/Config/Server/Files/SPX.php b/src/Config/Server/Files/SPX.php new file mode 100644 index 00000000..01ca54ef --- /dev/null +++ b/src/Config/Server/Files/SPX.php @@ -0,0 +1,17 @@ + $this->env[$attribute->name] ?? null, $attribute instanceof InputOption => $this->inputOptions[$attribute->name] ?? null, $attribute instanceof InputArgument => $this->inputArguments[$attribute->name] ?? null, + $attribute instanceof PhpIni => (static fn(string|false $value): ?string => match ($value) { + // Option does not exist or set to null + '', false => null, + default => $value, + })(\ini_get($attribute->option)), default => null, }; diff --git a/src/Service/Config/PhpIni.php b/src/Service/Config/PhpIni.php new file mode 100644 index 00000000..bf8efd8a --- /dev/null +++ b/src/Service/Config/PhpIni.php @@ -0,0 +1,16 @@ +injector->make($class, \array_merge((array) $binding, $arguments)); } catch (\Throwable $e) { - throw new class(previous: $e) extends \RuntimeException implements NotFoundExceptionInterface {}; + throw new class( + "Unable to create object of class $class.", + previous: $e, + ) extends \RuntimeException implements NotFoundExceptionInterface {}; } } diff --git a/src/Service/FilesObserver.php b/src/Service/FilesObserver.php index 98ad0a2e..cf19d553 100644 --- a/src/Service/FilesObserver.php +++ b/src/Service/FilesObserver.php @@ -5,14 +5,19 @@ namespace Buggregator\Trap\Service; use Buggregator\Trap\Cancellable; -use Buggregator\Trap\Config\FilesObserver as Config; +use Buggregator\Trap\Config\Server\Files\ObserverConfig as Config; use Buggregator\Trap\Logger; use Buggregator\Trap\Processable; use Buggregator\Trap\Proto\Buffer; use Buggregator\Trap\Proto\Frame; use Buggregator\Trap\Service\FilesObserver\Handler; +use Yiisoft\Injector\Injector; /** + * The service orchestrates the process of scanning files in directories. + * + * There are {@see Handler} instances for each configuration that are executed in fibers. + * * @internal */ final class FilesObserver implements Processable, Cancellable @@ -23,13 +28,18 @@ final class FilesObserver implements Processable, Cancellable private array $fibers = []; public function __construct( + private readonly Container $container, private readonly Logger $logger, private readonly Buffer $buffer, Config ...$configs, ) { foreach ($configs as $config) { + if (!$config->isValid()) { + continue; + } + $this->fibers[] = new \Fiber(function () use ($config): void { - foreach (Handler::generate($config, $this->logger) as $frame) { + foreach ($this->container->make(Handler::class, [$config]) as $frame) { $this->propagateFrame($frame); } }); diff --git a/src/Service/FilesObserver/FrameConverter.php b/src/Service/FilesObserver/FrameConverter.php index 48e35680..d8e8d261 100644 --- a/src/Service/FilesObserver/FrameConverter.php +++ b/src/Service/FilesObserver/FrameConverter.php @@ -19,7 +19,7 @@ interface FrameConverter public function validate(FileInfo $file): bool; /** - * @return iterable + * @return iterable */ public function convert(FileInfo $file): iterable; } diff --git a/src/Service/FilesObserver/Handler.php b/src/Service/FilesObserver/Handler.php index f2f9a9e3..41e651c7 100644 --- a/src/Service/FilesObserver/Handler.php +++ b/src/Service/FilesObserver/Handler.php @@ -4,46 +4,57 @@ namespace Buggregator\Trap\Service\FilesObserver; -use Buggregator\Trap\Config\FilesObserver as Config; +use Buggregator\Trap\Config\Server\Files\ObserverConfig as Config; use Buggregator\Trap\Logger; use Buggregator\Trap\Proto\Frame; +use Buggregator\Trap\Service\Container; use Buggregator\Trap\Support\Timer; /** + * The handler is responsible for scanning files in a directory and converting them into frames. + * It does it in a loop with a given interval. + * + * @see Config + * * @internal + * + * @implements \IteratorAggregate */ -final class Handler +final class Handler implements \IteratorAggregate { private readonly Timer $timer; /** @var array */ private array $cache = []; + /** @var non-empty-string */ private readonly string $path; private FrameConverter $converter; - private function __construct( + public function __construct( Config $config, private readonly Logger $logger, + Container $container, ) { + $config->isValid() or throw new \InvalidArgumentException('Invalid configuration.'); + $this->path = $config->path; - $this->timer = new Timer($config->interval); - $this->converter = new ($config->converter)(); + $this->timer = new Timer($config->scanInterval); + $this->converter = $container->make($config->converterClass, [$config]); } /** - * @return \Generator + * @return \Traversable */ - public static function generate(Config $config, Logger $logger): \Generator + public function getIterator(): \Traversable { - $self = new self($config, $logger); do { - foreach ($self->syncFiles() as $info) { - yield from $self->converter->convert($info); + foreach ($this->syncFiles() as $info) { + yield from $this->converter->convert($info); } - $self->timer->wait()->reset(); + $this->timer->wait()->reset(); } while (true); } From f834bce91c5b2078b090cd1a8d18bc60dfa572ec Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 31 May 2024 18:23:37 +0000 Subject: [PATCH 12/21] style(php-cs-fixer): lint php files and fix coding standards --- src/Service/Container.php | 5 +---- src/Service/FilesObserver.php | 1 - 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/Service/Container.php b/src/Service/Container.php index 11db1c2a..32acfafb 100644 --- a/src/Service/Container.php +++ b/src/Service/Container.php @@ -88,10 +88,7 @@ public function make(string $class, array $arguments = []): object try { $result = $this->injector->make($class, \array_merge((array) $binding, $arguments)); } catch (\Throwable $e) { - throw new class( - "Unable to create object of class $class.", - previous: $e, - ) extends \RuntimeException implements NotFoundExceptionInterface {}; + throw new class("Unable to create object of class $class.", previous: $e, ) extends \RuntimeException implements NotFoundExceptionInterface {}; } } diff --git a/src/Service/FilesObserver.php b/src/Service/FilesObserver.php index cf19d553..25ceb30f 100644 --- a/src/Service/FilesObserver.php +++ b/src/Service/FilesObserver.php @@ -11,7 +11,6 @@ use Buggregator\Trap\Proto\Buffer; use Buggregator\Trap\Proto\Frame; use Buggregator\Trap\Service\FilesObserver\Handler; -use Yiisoft\Injector\Injector; /** * The service orchestrates the process of scanning files in directories. From 144cf7eb96bea90b0b997e70628c1476810945c7 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Sat, 1 Jun 2024 00:28:54 +0400 Subject: [PATCH 13/21] chore: polishing --- src/Config/Server/Files/XHProf.php | 2 +- src/Proto/Frame/Profiler/Type.php | 1 + src/Service/Config/ConfigLoader.php | 5 ++++- .../{Filter => Converter}/XHProf.php | 20 +++++++++++-------- 4 files changed, 18 insertions(+), 10 deletions(-) rename src/Service/FilesObserver/{Filter => Converter}/XHProf.php (92%) diff --git a/src/Config/Server/Files/XHProf.php b/src/Config/Server/Files/XHProf.php index 83baa606..13a3bc22 100644 --- a/src/Config/Server/Files/XHProf.php +++ b/src/Config/Server/Files/XHProf.php @@ -5,7 +5,7 @@ namespace Buggregator\Trap\Config\Server\Files; use Buggregator\Trap\Service\Config\PhpIni; -use Buggregator\Trap\Service\FilesObserver\Filter\XHProf as Converter; +use Buggregator\Trap\Service\FilesObserver\Converter\XHProf as Converter; /** * @internal diff --git a/src/Proto/Frame/Profiler/Type.php b/src/Proto/Frame/Profiler/Type.php index 3b445d2e..6e95a466 100644 --- a/src/Proto/Frame/Profiler/Type.php +++ b/src/Proto/Frame/Profiler/Type.php @@ -12,4 +12,5 @@ enum Type: string { case XHProf = 'XHProf'; case XDebug = 'XDebug'; + case SPX = 'SPX'; } diff --git a/src/Service/Config/ConfigLoader.php b/src/Service/Config/ConfigLoader.php index 6a4b2d76..f1cdb8f5 100644 --- a/src/Service/Config/ConfigLoader.php +++ b/src/Service/Config/ConfigLoader.php @@ -59,7 +59,10 @@ private function injectValue(object $config, \ReflectionProperty $property, arra /** @var mixed $value */ $value = match (true) { - $attribute instanceof XPath => @$this->xml?->xpath($attribute->path)[$attribute->key], + $attribute instanceof XPath => (static fn(?array $value, int $key): mixed => match (true) { + \array_key_exists($key, $value) => $value[$key], + default => null, + })($this->xml?->xpath($attribute->path), $attribute->key), $attribute instanceof Env => $this->env[$attribute->name] ?? null, $attribute instanceof InputOption => $this->inputOptions[$attribute->name] ?? null, $attribute instanceof InputArgument => $this->inputArguments[$attribute->name] ?? null, diff --git a/src/Service/FilesObserver/Filter/XHProf.php b/src/Service/FilesObserver/Converter/XHProf.php similarity index 92% rename from src/Service/FilesObserver/Filter/XHProf.php rename to src/Service/FilesObserver/Converter/XHProf.php index 02cfde68..9c1abbc5 100644 --- a/src/Service/FilesObserver/Filter/XHProf.php +++ b/src/Service/FilesObserver/Converter/XHProf.php @@ -2,8 +2,9 @@ declare(strict_types=1); -namespace Buggregator\Trap\Service\FilesObserver\Filter; +namespace Buggregator\Trap\Service\FilesObserver\Converter; +use Buggregator\Trap\Logger; use Buggregator\Trap\Proto\Frame\Profiler as ProfilerFrame; use Buggregator\Trap\Service\FilesObserver\FileInfo; use Buggregator\Trap\Service\FilesObserver\FrameConverter as FileFilterInterface; @@ -13,6 +14,10 @@ */ final class XHProf implements FileFilterInterface { + public function __construct( + private readonly Logger $logger, + ) {} + public function validate(FileInfo $file): bool { return $file->getExtension() === 'xhprof'; @@ -40,8 +45,7 @@ public function convert(FileInfo $file): \Traversable ), ); } catch (\Throwable $e) { - // todo log - \var_dump($e->getMessage()); + $this->logger->exception($e); } } @@ -76,11 +80,11 @@ private function dataToPayload(array $data): array 'callee' => $callee, 'caller' => $caller, 'cost' => [ - 'cpu' => (int) $value['cpu'], - 'ct' => (int) $value['ct'], - 'mu' => (int) $value['mu'], - 'pmu' => (int) $value['pmu'], - 'wt' => (int) $value['wt'], + 'cpu' => (int)$value['cpu'], + 'ct' => (int)$value['ct'], + 'mu' => (int)$value['mu'], + 'pmu' => (int)$value['pmu'], + 'wt' => (int)$value['wt'], ], ]; From 75bfef375a4d6d8c9974682165b95d6d2fdc738f Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 31 May 2024 20:29:40 +0000 Subject: [PATCH 14/21] style(php-cs-fixer): lint php files and fix coding standards --- src/Service/FilesObserver/Converter/XHProf.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Service/FilesObserver/Converter/XHProf.php b/src/Service/FilesObserver/Converter/XHProf.php index 9c1abbc5..d27b5a70 100644 --- a/src/Service/FilesObserver/Converter/XHProf.php +++ b/src/Service/FilesObserver/Converter/XHProf.php @@ -80,11 +80,11 @@ private function dataToPayload(array $data): array 'callee' => $callee, 'caller' => $caller, 'cost' => [ - 'cpu' => (int)$value['cpu'], - 'ct' => (int)$value['ct'], - 'mu' => (int)$value['mu'], - 'pmu' => (int)$value['pmu'], - 'wt' => (int)$value['wt'], + 'cpu' => (int) $value['cpu'], + 'ct' => (int) $value['ct'], + 'mu' => (int) $value['mu'], + 'pmu' => (int) $value['pmu'], + 'wt' => (int) $value['wt'], ], ]; From de2277ce21eba1d6f6dd327bcc4faa81a22a391c Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Sat, 1 Jun 2024 16:38:32 +0400 Subject: [PATCH 15/21] feat: sort profiler edges using Tree abstraction; add envs TRAP_XHPROF_SORT and TRAP_XHPROF_PATH --- src/Config/Server/Files/XHProf.php | 6 + .../FilesObserver/Converter/Branch.php | 33 ++++ src/Service/FilesObserver/Converter/Cost.php | 129 ++++++++++++++ src/Service/FilesObserver/Converter/Edge.php | 26 +++ src/Service/FilesObserver/Converter/Tree.php | 144 ++++++++++++++++ .../FilesObserver/Converter/XHProf.php | 163 +++++++----------- 6 files changed, 402 insertions(+), 99 deletions(-) create mode 100644 src/Service/FilesObserver/Converter/Branch.php create mode 100644 src/Service/FilesObserver/Converter/Cost.php create mode 100644 src/Service/FilesObserver/Converter/Edge.php create mode 100644 src/Service/FilesObserver/Converter/Tree.php diff --git a/src/Config/Server/Files/XHProf.php b/src/Config/Server/Files/XHProf.php index 13a3bc22..5e3679c8 100644 --- a/src/Config/Server/Files/XHProf.php +++ b/src/Config/Server/Files/XHProf.php @@ -4,6 +4,7 @@ namespace Buggregator\Trap\Config\Server\Files; +use Buggregator\Trap\Service\Config\Env; use Buggregator\Trap\Service\Config\PhpIni; use Buggregator\Trap\Service\FilesObserver\Converter\XHProf as Converter; @@ -12,7 +13,12 @@ */ final class XHProf extends ObserverConfig { + /** @var int<0, 3> Edges sorting algorithm */ + #[Env('TRAP_XHPROF_SORT')] + public int $algorithm = 1; + /** @var non-empty-string|null Path to XHProf files */ + #[Env('TRAP_XHPROF_PATH')] #[PhpIni('xhprof.output_dir')] public ?string $path = null; diff --git a/src/Service/FilesObserver/Converter/Branch.php b/src/Service/FilesObserver/Converter/Branch.php new file mode 100644 index 00000000..6c23e4f9 --- /dev/null +++ b/src/Service/FilesObserver/Converter/Branch.php @@ -0,0 +1,33 @@ +> $children + * @param Branch|null $parent + */ + public function __construct( + public object $item, + public readonly string $id, + public readonly ?string $parentId, + public array $children = [], + public ?Branch $parent = null, + ) {} + + public function __destruct() + { + unset($this->item, $this->children, $this->parent); + } +} diff --git a/src/Service/FilesObserver/Converter/Cost.php b/src/Service/FilesObserver/Converter/Cost.php new file mode 100644 index 00000000..94ec3ebd --- /dev/null +++ b/src/Service/FilesObserver/Converter/Cost.php @@ -0,0 +1,129 @@ + */ + public int $d_cpu = 0; + + /** @var int<0, max> */ + public int $d_ct = 0; + + /** @var int<0, max> */ + public int $d_mu = 0; + + /** @var int<0, max> */ + public int $d_pmu = 0; + + /** @var int<0, max> */ + public int $d_wt = 0; + + /** + * @param int<0, max> $ct + * @param int<0, max> $wt + * @param int<0, max> $cpu + * @param int<0, max> $mu + * @param int<0, max> $pmu + */ + public function __construct( + public readonly int $ct, + public readonly int $wt, + public readonly int $cpu, + public readonly int $mu, + public readonly int $pmu, + ) {} + + /** + * @param array{ + * ct: int<0, max>, + * wt: int<0, max>, + * cpu: int<0, max>, + * mu: int<0, max>, + * pmu: int<0, max>, + * p_ct?: float<0, 100>, + * p_wt?: float<0, 100>, + * p_cpu?: float<0, 100>, + * p_mu?: float<0, 100>, + * p_pmu?: float<0, 100>, + * d_ct?: int<0, max>, + * d_wt?: int<0, max>, + * d_cpu?: int<0, max>, + * d_mu?: int<0, max>, + * d_pmu?: int<0, max> + * } $data + */ + public static function fromArray(array $data): self + { + $self = new self( + $data['ct'], + $data['wt'], + $data['cpu'], + $data['mu'], + $data['pmu'], + ); + \array_key_exists('p_ct', $data) and $self->p_ct = $data['p_ct']; + \array_key_exists('p_wt', $data) and $self->p_wt = $data['p_wt']; + \array_key_exists('p_cpu', $data) and $self->p_cpu = $data['p_cpu']; + \array_key_exists('p_mu', $data) and $self->p_mu = $data['p_mu']; + \array_key_exists('p_pmu', $data) and $self->p_pmu = $data['p_pmu']; + \array_key_exists('d_ct', $data) and $self->d_ct = $data['d_ct']; + \array_key_exists('d_wt', $data) and $self->d_wt = $data['d_wt']; + \array_key_exists('d_cpu', $data) and $self->d_cpu = $data['d_cpu']; + \array_key_exists('d_mu', $data) and $self->d_mu = $data['d_mu']; + \array_key_exists('d_pmu', $data) and $self->d_pmu = $data['d_pmu']; + + return $self; + } + + /** + * @return array{ + * ct: int<0, max>, + * wt: int<0, max>, + * cpu: int<0, max>, + * mu: int<0, max>, + * pmu: int<0, max>, + * p_ct: float<0, 100>, + * p_wt: float<0, 100>, + * p_cpu: float<0, 100>, + * p_mu: float<0, 100>, + * p_pmu: float<0, 100>, + * d_ct: int<0, max>, + * d_wt: int<0, max>, + * d_cpu: int<0, max>, + * d_mu: int<0, max>, + * d_pmu: int<0, max> + * } + */ + public function jsonSerialize(): array + { + return [ + 'ct' => $this->ct, + 'wt' => $this->wt, + 'cpu' => $this->cpu, + 'mu' => $this->mu, + 'pmu' => $this->pmu, + 'p_ct' => $this->p_ct, + 'p_wt' => $this->p_wt, + 'p_cpu' => $this->p_cpu, + 'p_mu' => $this->p_mu, + 'p_pmu' => $this->p_pmu, + 'd_ct' => $this->d_ct, + 'd_wt' => $this->d_wt, + 'd_cpu' => $this->d_cpu, + 'd_mu' => $this->d_mu, + 'd_pmu' => $this->d_pmu, + ]; + } +} diff --git a/src/Service/FilesObserver/Converter/Edge.php b/src/Service/FilesObserver/Converter/Edge.php new file mode 100644 index 00000000..59bae540 --- /dev/null +++ b/src/Service/FilesObserver/Converter/Edge.php @@ -0,0 +1,26 @@ + $this->caller, + 'callee' => $this->callee, + 'cost' => $this->cost, + ]; + } +} diff --git a/src/Service/FilesObserver/Converter/Tree.php b/src/Service/FilesObserver/Converter/Tree.php new file mode 100644 index 00000000..30c92395 --- /dev/null +++ b/src/Service/FilesObserver/Converter/Tree.php @@ -0,0 +1,144 @@ +> + * + * @internal + */ +final class Tree implements \IteratorAggregate +{ + /** @var array> */ + private array $root = []; + + /** @var array> */ + private array $all = []; + + /** @var array> */ + private array $lostChildren = []; + + /** + * @template-covariant T of object + * + * @param list $edges + * @param callable(T): non-empty-string $getCurrent Get current node id + * @param callable(T): (non-empty-string|null) $getParent Get parent node id + * + * @return self + */ + public static function fromEdgesList(array $edges, callable $getCurrent, callable $getParent): self + { + $tree = new self(); + + foreach ($edges as $edge) { + $current = $getCurrent($edge); + $parent = $getParent($edge); + + $tree->addItem($edge, $current, $parent); + } + + return $tree; + } + + /** + * @param TItem $item + * @param non-empty-string $id + * @param non-empty-string|null $parentId + */ + public function addItem(object $item, string $id, ?string $parentId): void + { + $branch = new Branch($item, $id, $parentId); + $this->all[$id] = $branch; + + if ($parentId === null) { + $this->root[$id] = $branch; + } else { + $branch->parent = $this->all[$parentId] ?? null; + + $branch->parent === null + ? $this->lostChildren[$id] = $branch + : $branch->parent->children[] = $branch; + } + + foreach ($this->lostChildren as $lostChild) { + if ($lostChild->parentId === $id) { + $branch->children[] = $lostChild; + unset($this->lostChildren[$lostChild->id]); + } + } + } + + /** + * Iterate all the branches without sorting and hierarchy. + * + * @return \Traversable> + */ + public function getIterator(): \Traversable + { + yield from $this->all; + } + + /** + * Yield items by the level in the hierarchy with custom sorting in level scope + * + * @param callable(Branch, Branch): int $sorter + * + * @return \Traversable + */ + public function getItemsSortedV1(?callable $sorter): \Traversable + { + $level = 0; + $queue = [$level => $this->root]; + processLevel: + while ($queue[$level] !== []) { + $branch = \array_shift($queue[$level]); + yield $branch->item; + + // Fill the next level + $queue[$level + 1] ??= []; + \array_unshift($queue[$level + 1], ...$branch->children); + } + + if (\array_key_exists(++$level, $queue)) { + $sorter === null or \usort($queue[$level], $sorter); + + goto processLevel; + } + } + + + /** + * Yield items deep-first. + * + * @param callable(Branch, Branch): int $sorter + * + * @return \Traversable + */ + public function getItemsSortedV0(?callable $sorter): \Traversable + { + $queue = $this->root; + while (\count($queue) > 0) { + $branch = \array_shift($queue); + yield $branch->item; + + $children = $branch->children; + $sorter === null or \usort($children, $sorter); + + \array_unshift($queue, ...$children); + } + } + + public function __destruct() + { + foreach ($this->all as $branch) { + $branch->__destruct(); + } + + unset($this->all, $this->root, $this->lostChildren); + } +} diff --git a/src/Service/FilesObserver/Converter/XHProf.php b/src/Service/FilesObserver/Converter/XHProf.php index d27b5a70..83eaa837 100644 --- a/src/Service/FilesObserver/Converter/XHProf.php +++ b/src/Service/FilesObserver/Converter/XHProf.php @@ -4,18 +4,28 @@ namespace Buggregator\Trap\Service\FilesObserver\Converter; +use Buggregator\Trap\Config\Server\Files\XHProf as XHProfConfig; use Buggregator\Trap\Logger; use Buggregator\Trap\Proto\Frame\Profiler as ProfilerFrame; use Buggregator\Trap\Service\FilesObserver\FileInfo; use Buggregator\Trap\Service\FilesObserver\FrameConverter as FileFilterInterface; /** + * @psalm-type RawData = array, + * wt: int<0, max>, + * cpu: int<0, max>, + * mu: int<0, max>, + * pmu: int<0, max> + * }> + * * @internal */ final class XHProf implements FileFilterInterface { public function __construct( private readonly Logger $logger, + private readonly XHProfConfig $config, ) {} public function validate(FileInfo $file): bool @@ -39,6 +49,7 @@ public function convert(FileInfo $file): \Traversable metadata: $metadata, callsProvider: function () use ($file): array { $content = \file_get_contents($file->path); + /** @var RawData $data */ $data = \unserialize($content, ['allowed_classes' => false]); return $this->dataToPayload($data); }, @@ -49,6 +60,9 @@ public function convert(FileInfo $file): \Traversable } } + /** + * @param RawData $data + */ private function dataToPayload(array $data): array { /** @var array> $data */ @@ -60,119 +74,70 @@ private function dataToPayload(array $data): array 'wt' => 0, ]; - $edges = []; - /** @var array> $parents */ - $parents = []; - /** @var array> $parents items with unknown caller */ - $callerLess = []; - $i = 0; - \uasort($data, static function (array $a, array $b) { - return $b['wt'] <=> $a['wt']; - }); - // $data = \array_reverse($data, true); + /** @var Tree $tree */ + $tree = new Tree(); + + // \uasort($data, static function (array $a, array $b) { + // return $b['wt'] <=> $a['wt']; + // }); + foreach ($data as $key => $value) { [$caller, $callee] = \explode('==>', $key, 2) + [1 => null]; if ($callee === null) { [$caller, $callee] = [null, $caller]; } - $edge = [ - 'callee' => $callee, - 'caller' => $caller, - 'cost' => [ - 'cpu' => (int) $value['cpu'], - 'ct' => (int) $value['ct'], - 'mu' => (int) $value['mu'], - 'pmu' => (int) $value['pmu'], - 'wt' => (int) $value['wt'], - ], - ]; - - // if (++$j > 10) { - // print_r(\array_keys($parents)); - // print_r(\array_keys($edges)); - // die; - // } - - if ($caller !== null && !\array_key_exists($caller, $parents) && $caller !== $callee) { - $callerLess[$caller][] = &$edge; - // echo "CALLER: $caller\n"; - // echo "CALLEE: $callee\n"; - } else { - // echo "CALLER: $callee\n"; - $parents[$callee] = &$edge['cost']; - $edges['e' . ++$i] = &$edge; - if (\array_key_exists($callee, $callerLess)) { - foreach ($callerLess[$callee] as $item) { - $edges['a' . ++$i] = &$item; - $parents[$item['callee']] = &$item['cost']; - unset($item); - } - unset($callerLess[$callee]); - } - } - - $peaks['cpu'] = \max($peaks['cpu'], $edge['cost']['cpu']); - $peaks['ct'] = \max($peaks['ct'], $edge['cost']['ct']); - $peaks['mu'] = \max($peaks['mu'], $edge['cost']['mu']); - $peaks['pmu'] = \max($peaks['pmu'], $edge['cost']['pmu']); - $peaks['wt'] = \max($peaks['wt'], $edge['cost']['wt']); - - unset($edge); - } + $edge = new Edge( + caller: $caller, + callee: $callee, + cost: Cost::fromArray($value), + ); - // Merge callerLess items - while ($callerLess !== []) { - $merged = 0; - foreach ($callerLess as $caller => $items) { - if (\array_key_exists($caller, $parents)) { - foreach ($items as &$item) { - $edges['c' . ++$i] = &$item; - $parents[$item['callee']] = &$item['cost']; - unset($item); - } - ++$merged; - unset($callerLess[$caller]); - } - } + $peaks['cpu'] = \max($peaks['cpu'], $edge->cost->cpu); + $peaks['ct'] = \max($peaks['ct'], $edge->cost->ct); + $peaks['mu'] = \max($peaks['mu'], $edge->cost->mu); + $peaks['pmu'] = \max($peaks['pmu'], $edge->cost->pmu); + $peaks['wt'] = \max($peaks['wt'], $edge->cost->wt); - // Just merge all as is - if ($merged === 0) { - foreach ($callerLess as $items) { - foreach ($items as &$item) { - $edges['f' . ++$i] = &$item; - $parents[$item['callee']] = &$item['cost']; - unset($item); - } - } - - $callerLess = []; - } + $tree->addItem($edge, $edge->callee, $edge->caller); } - // calc percentages and delta - foreach ($edges as &$value) { - $cost = &$value['cost']; - $cost['p_cpu'] = $peaks['cpu'] > 0 ? \round($cost['cpu'] / $peaks['cpu'] * 100, 2) : 0; - $cost['p_ct'] = $peaks['ct'] > 0 ? \round($cost['ct'] / $peaks['ct'] * 100, 2) : 0; - $cost['p_mu'] = $peaks['mu'] > 0 ? \round($cost['mu'] / $peaks['mu'] * 100, 2) : 0; - $cost['p_pmu'] = $peaks['pmu'] > 0 ? \round($cost['pmu'] / $peaks['pmu'] * 100, 2) : 0; - $cost['p_wt'] = $peaks['wt'] > 0 ? \round($cost['wt'] / $peaks['wt'] * 100, 2) : 0; - - $caller = $value['caller']; - if ($caller !== null) { - $cost['d_cpu'] = $cost['cpu'] - ($parents[$caller]['cpu'] ?? 0); - $cost['d_ct'] = $cost['ct'] - ($parents[$caller]['ct'] ?? 0); - $cost['d_mu'] = $cost['mu'] - ($parents[$caller]['mu'] ?? 0); - $cost['d_pmu'] = $cost['pmu'] - ($parents[$caller]['pmu'] ?? 0); - $cost['d_wt'] = $cost['wt'] - ($parents[$caller]['wt'] ?? 0); + // Calc percentages and delta + /** @var Branch $branch */ + foreach ($tree->getIterator() as $branch) { + $cost = $branch->item->cost; + $cost->p_cpu = $peaks['cpu'] > 0 ? \round($cost->cpu / $peaks['cpu'] * 100, 3) : 0; + $cost->p_ct = $peaks['ct'] > 0 ? \round($cost->ct / $peaks['ct'] * 100, 3) : 0; + $cost->p_mu = $peaks['mu'] > 0 ? \round($cost->mu / $peaks['mu'] * 100, 3) : 0; + $cost->p_pmu = $peaks['pmu'] > 0 ? \round($cost->pmu / $peaks['pmu'] * 100, 3) : 0; + $cost->p_wt = $peaks['wt'] > 0 ? \round($cost->wt / $peaks['wt'] * 100, 3) : 0; + + if ($branch->parent !== null) { + $parentCost = $branch->parent->item->cost; + $cost->d_cpu = $cost->cpu - ($parentCost->cpu); + $cost->d_ct = $cost->ct - ($parentCost->ct); + $cost->d_mu = $cost->mu - ($parentCost->mu); + $cost->d_pmu = $cost->pmu - ($parentCost->pmu); + $cost->d_wt = $cost->wt - ($parentCost->wt); } - unset($value, $cost); } - unset($parents); return [ - 'edges' => $edges, + 'edges' => \iterator_to_array(match ($this->config->algorithm) { + // Deep-first + 0 => $tree->getItemsSortedV0(null), + // Deep-first with sorting by WT + 1 => $tree->getItemsSortedV0( + static fn(Branch $a, Branch $b): int => $b->item->cost->wt <=> $a->item->cost->wt, + ), + // Level-by-level + 2 => $tree->getItemsSortedV1(null), + // Level-by-level with sorting by WT + 3 => $tree->getItemsSortedV1( + static fn(Branch $a, Branch $b): int => $b->item->cost->wt <=> $a->item->cost->wt, + ), + default => throw new \LogicException('Unknown XHProf sorting algorithm.'), + }), 'peaks' => $peaks, ]; } From aea92617959b29f7735e18f01c9961163297321d Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 1 Jun 2024 12:39:21 +0000 Subject: [PATCH 16/21] style(php-cs-fixer): lint php files and fix coding standards --- src/Service/FilesObserver/Converter/Cost.php | 4 ++++ src/Service/FilesObserver/Converter/Tree.php | 1 - 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Service/FilesObserver/Converter/Cost.php b/src/Service/FilesObserver/Converter/Cost.php index 94ec3ebd..b5e63787 100644 --- a/src/Service/FilesObserver/Converter/Cost.php +++ b/src/Service/FilesObserver/Converter/Cost.php @@ -10,9 +10,13 @@ final class Cost implements \JsonSerializable { public float $p_cpu = 0; + public float $p_ct = 0; + public float $p_mu = 0; + public float $p_pmu = 0; + public float $p_wt = 0; /** @var int<0, max> */ diff --git a/src/Service/FilesObserver/Converter/Tree.php b/src/Service/FilesObserver/Converter/Tree.php index 30c92395..3725795d 100644 --- a/src/Service/FilesObserver/Converter/Tree.php +++ b/src/Service/FilesObserver/Converter/Tree.php @@ -111,7 +111,6 @@ public function getItemsSortedV1(?callable $sorter): \Traversable } } - /** * Yield items deep-first. * From 5939b7d5f9c1789c1dde339c6d51919d482ed3d0 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Sat, 1 Jun 2024 18:04:29 +0400 Subject: [PATCH 17/21] chore: add short comment for the TRAP_XHPROF_SORT env; update composer.lock --- composer.lock | 44 +++++++++++++++--------------- src/Config/Server/Files/XHProf.php | 11 ++++++-- 2 files changed, 31 insertions(+), 24 deletions(-) diff --git a/composer.lock b/composer.lock index 8bd01d12..e4a115fc 100644 --- a/composer.lock +++ b/composer.lock @@ -2290,16 +2290,16 @@ }, { "name": "friendsofphp/php-cs-fixer", - "version": "v3.57.2", + "version": "v3.58.1", "source": { "type": "git", "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", - "reference": "22f7f3145606df92b02fb1bd22c30abfce956d3c" + "reference": "04e9424025677a86914b9a4944dbbf4060bb0aff" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/22f7f3145606df92b02fb1bd22c30abfce956d3c", - "reference": "22f7f3145606df92b02fb1bd22c30abfce956d3c", + "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/04e9424025677a86914b9a4944dbbf4060bb0aff", + "reference": "04e9424025677a86914b9a4944dbbf4060bb0aff", "shasum": "" }, "require": { @@ -2378,7 +2378,7 @@ ], "support": { "issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues", - "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.57.2" + "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.58.1" }, "funding": [ { @@ -2386,7 +2386,7 @@ "type": "github" } ], - "time": "2024-05-20T20:41:57+00:00" + "time": "2024-05-29T16:39:07+00:00" }, { "name": "google/protobuf", @@ -3341,16 +3341,16 @@ }, { "name": "phpstan/phpdoc-parser", - "version": "1.29.0", + "version": "1.29.1", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "536889f2b340489d328f5ffb7b02bb6b183ddedc" + "reference": "fcaefacf2d5c417e928405b71b400d4ce10daaf4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/536889f2b340489d328f5ffb7b02bb6b183ddedc", - "reference": "536889f2b340489d328f5ffb7b02bb6b183ddedc", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/fcaefacf2d5c417e928405b71b400d4ce10daaf4", + "reference": "fcaefacf2d5c417e928405b71b400d4ce10daaf4", "shasum": "" }, "require": { @@ -3382,22 +3382,22 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/1.29.0" + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.29.1" }, - "time": "2024-05-06T12:04:23+00:00" + "time": "2024-05-31T08:52:43+00:00" }, { "name": "phpstan/phpstan", - "version": "1.11.2", + "version": "1.11.3", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "0d5d4294a70deb7547db655c47685d680e39cfec" + "reference": "e64220a05c1209fc856d58e789c3b7a32c0bb9a5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/0d5d4294a70deb7547db655c47685d680e39cfec", - "reference": "0d5d4294a70deb7547db655c47685d680e39cfec", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/e64220a05c1209fc856d58e789c3b7a32c0bb9a5", + "reference": "e64220a05c1209fc856d58e789c3b7a32c0bb9a5", "shasum": "" }, "require": { @@ -3442,7 +3442,7 @@ "type": "github" } ], - "time": "2024-05-24T13:23:04+00:00" + "time": "2024-05-31T13:53:37+00:00" }, { "name": "phpstan/phpstan-deprecation-rules", @@ -6379,16 +6379,16 @@ }, { "name": "wayofdev/cs-fixer-config", - "version": "v1.4.5", + "version": "v1.5.0", "source": { "type": "git", "url": "https://github.com/wayofdev/php-cs-fixer-config.git", - "reference": "d38222297a12344cb968b85213878534ffffbefc" + "reference": "1300d46e72b7893b038c429585206981820fb4e8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/wayofdev/php-cs-fixer-config/zipball/d38222297a12344cb968b85213878534ffffbefc", - "reference": "d38222297a12344cb968b85213878534ffffbefc", + "url": "https://api.github.com/repos/wayofdev/php-cs-fixer-config/zipball/1300d46e72b7893b038c429585206981820fb4e8", + "reference": "1300d46e72b7893b038c429585206981820fb4e8", "shasum": "" }, "require": { @@ -6453,7 +6453,7 @@ "type": "github" } ], - "time": "2024-05-28T13:37:07+00:00" + "time": "2024-05-29T08:43:41+00:00" }, { "name": "webmozart/assert", diff --git a/src/Config/Server/Files/XHProf.php b/src/Config/Server/Files/XHProf.php index 5e3679c8..502c6ee6 100644 --- a/src/Config/Server/Files/XHProf.php +++ b/src/Config/Server/Files/XHProf.php @@ -13,9 +13,16 @@ */ final class XHProf extends ObserverConfig { - /** @var int<0, 3> Edges sorting algorithm */ + /** + * @var int<0, 3> Edges sorting algorithm + * Where: + * 0 - Deep-first + * 1 - Deep-first with sorting by WT + * 2 - Level-by-level + * 3 - Level-by-level with sorting by WT + */ #[Env('TRAP_XHPROF_SORT')] - public int $algorithm = 1; + public int $algorithm = 3; /** @var non-empty-string|null Path to XHProf files */ #[Env('TRAP_XHPROF_PATH')] From 3e7ce4c53f14fe0c6762d3d18ed16fccc0af0ec9 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Sat, 1 Jun 2024 18:22:56 +0400 Subject: [PATCH 18/21] chore(psalm): fix psalm issues --- src/Service/FilesObserver/Converter/Cost.php | 10 +++--- src/Service/FilesObserver/Converter/Edge.php | 4 +++ .../FilesObserver/Converter/XHProf.php | 36 +++++++++++-------- 3 files changed, 30 insertions(+), 20 deletions(-) diff --git a/src/Service/FilesObserver/Converter/Cost.php b/src/Service/FilesObserver/Converter/Cost.php index b5e63787..d96bab71 100644 --- a/src/Service/FilesObserver/Converter/Cost.php +++ b/src/Service/FilesObserver/Converter/Cost.php @@ -19,19 +19,19 @@ final class Cost implements \JsonSerializable public float $p_wt = 0; - /** @var int<0, max> */ + /** @var int */ public int $d_cpu = 0; - /** @var int<0, max> */ + /** @var int */ public int $d_ct = 0; - /** @var int<0, max> */ + /** @var int */ public int $d_mu = 0; - /** @var int<0, max> */ + /** @var int */ public int $d_pmu = 0; - /** @var int<0, max> */ + /** @var int */ public int $d_wt = 0; /** diff --git a/src/Service/FilesObserver/Converter/Edge.php b/src/Service/FilesObserver/Converter/Edge.php index 59bae540..7d30b748 100644 --- a/src/Service/FilesObserver/Converter/Edge.php +++ b/src/Service/FilesObserver/Converter/Edge.php @@ -9,6 +9,10 @@ */ final class Edge implements \JsonSerializable { + /** + * @param non-empty-string|null $caller + * @param non-empty-string $callee + */ public function __construct( public readonly ?string $caller, public readonly string $callee, diff --git a/src/Service/FilesObserver/Converter/XHProf.php b/src/Service/FilesObserver/Converter/XHProf.php index 83eaa837..8a0ab73e 100644 --- a/src/Service/FilesObserver/Converter/XHProf.php +++ b/src/Service/FilesObserver/Converter/XHProf.php @@ -19,6 +19,9 @@ * pmu: int<0, max> * }> * + * @psalm-import-type Metadata from \Buggregator\Trap\Proto\Frame\Profiler\Payload + * @psalm-import-type Calls from \Buggregator\Trap\Proto\Frame\Profiler\Payload + * * @internal */ final class XHProf implements FileFilterInterface @@ -33,16 +36,19 @@ public function validate(FileInfo $file): bool return $file->getExtension() === 'xhprof'; } + /** + * @return \Traversable + */ public function convert(FileInfo $file): \Traversable { try { + /** @var Metadata $metadata */ $metadata = [ 'date' => $file->mtime, 'hostname' => \explode('.', $file->getName(), 2)[0], 'filename' => $file->getName(), ]; - /** @psalm-suppress MixedArgumentTypeCoercion */ yield new ProfilerFrame( ProfilerFrame\Payload::new( type: ProfilerFrame\Type::XHProf, @@ -62,10 +68,10 @@ public function convert(FileInfo $file): \Traversable /** * @param RawData $data + * @return Calls */ private function dataToPayload(array $data): array { - /** @var array> $data */ $peaks = [ 'cpu' => 0, 'ct' => 0, @@ -77,15 +83,13 @@ private function dataToPayload(array $data): array /** @var Tree $tree */ $tree = new Tree(); - // \uasort($data, static function (array $a, array $b) { - // return $b['wt'] <=> $a['wt']; - // }); - foreach ($data as $key => $value) { - [$caller, $callee] = \explode('==>', $key, 2) + [1 => null]; - if ($callee === null) { + [$caller, $callee] = \explode('==>', $key, 2) + [1 => '']; + if ($callee === '') { [$caller, $callee] = [null, $caller]; } + $caller === '' and $caller = null; + \assert($callee !== ''); $edge = new Edge( caller: $caller, @@ -102,8 +106,10 @@ private function dataToPayload(array $data): array $tree->addItem($edge, $edge->callee, $edge->caller); } - // Calc percentages and delta - /** @var Branch $branch */ + /** + * Calc percentages and delta + * @var Branch $branch Needed for IDE + */ foreach ($tree->getIterator() as $branch) { $cost = $branch->item->cost; $cost->p_cpu = $peaks['cpu'] > 0 ? \round($cost->cpu / $peaks['cpu'] * 100, 3) : 0; @@ -114,11 +120,11 @@ private function dataToPayload(array $data): array if ($branch->parent !== null) { $parentCost = $branch->parent->item->cost; - $cost->d_cpu = $cost->cpu - ($parentCost->cpu); - $cost->d_ct = $cost->ct - ($parentCost->ct); - $cost->d_mu = $cost->mu - ($parentCost->mu); - $cost->d_pmu = $cost->pmu - ($parentCost->pmu); - $cost->d_wt = $cost->wt - ($parentCost->wt); + $cost->d_cpu = $cost->cpu - $parentCost->cpu; + $cost->d_ct = $cost->ct - $parentCost->ct; + $cost->d_mu = $cost->mu - $parentCost->mu; + $cost->d_pmu = $cost->pmu - $parentCost->pmu; + $cost->d_wt = $cost->wt - $parentCost->wt; } } From 41c4a76a7601b67c148344638677eb2a6253155d Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Sat, 1 Jun 2024 18:58:51 +0400 Subject: [PATCH 19/21] chore(psalm): fix psalm issues --- src/Config/Server/Files/ObserverConfig.php | 4 +- src/Config/Server/Files/XHProf.php | 2 + src/Service/FilesObserver/Converter/Cost.php | 60 ++++++++++---------- src/Service/FilesObserver/Converter/Tree.php | 14 +++-- 4 files changed, 42 insertions(+), 38 deletions(-) diff --git a/src/Config/Server/Files/ObserverConfig.php b/src/Config/Server/Files/ObserverConfig.php index 96db4b8b..e9a20ac7 100644 --- a/src/Config/Server/Files/ObserverConfig.php +++ b/src/Config/Server/Files/ObserverConfig.php @@ -11,10 +11,10 @@ */ abstract class ObserverConfig { - /* @var non-empty-string|null $path */ + /** @var non-empty-string|null */ public ?string $path = null; - /** @var class-string|null $converter */ + /** @var class-string|null */ public ?string $converterClass = null; /** @var float Scan interval in seconds */ diff --git a/src/Config/Server/Files/XHProf.php b/src/Config/Server/Files/XHProf.php index 502c6ee6..4f614564 100644 --- a/src/Config/Server/Files/XHProf.php +++ b/src/Config/Server/Files/XHProf.php @@ -7,6 +7,7 @@ use Buggregator\Trap\Service\Config\Env; use Buggregator\Trap\Service\Config\PhpIni; use Buggregator\Trap\Service\FilesObserver\Converter\XHProf as Converter; +use Buggregator\Trap\Service\FilesObserver\FrameConverter; /** * @internal @@ -29,5 +30,6 @@ final class XHProf extends ObserverConfig #[PhpIni('xhprof.output_dir')] public ?string $path = null; + /** @var class-string|null */ public ?string $converterClass = Converter::class; } diff --git a/src/Service/FilesObserver/Converter/Cost.php b/src/Service/FilesObserver/Converter/Cost.php index d96bab71..d16de636 100644 --- a/src/Service/FilesObserver/Converter/Cost.php +++ b/src/Service/FilesObserver/Converter/Cost.php @@ -56,16 +56,16 @@ public function __construct( * cpu: int<0, max>, * mu: int<0, max>, * pmu: int<0, max>, - * p_ct?: float<0, 100>, - * p_wt?: float<0, 100>, - * p_cpu?: float<0, 100>, - * p_mu?: float<0, 100>, - * p_pmu?: float<0, 100>, - * d_ct?: int<0, max>, - * d_wt?: int<0, max>, - * d_cpu?: int<0, max>, - * d_mu?: int<0, max>, - * d_pmu?: int<0, max> + * p_ct?: float, + * p_wt?: float, + * p_cpu?: float, + * p_mu?: float, + * p_pmu?: float, + * d_ct?: int, + * d_wt?: int, + * d_cpu?: int, + * d_mu?: int, + * d_pmu?: int * } $data */ public static function fromArray(array $data): self @@ -77,16 +77,16 @@ public static function fromArray(array $data): self $data['mu'], $data['pmu'], ); - \array_key_exists('p_ct', $data) and $self->p_ct = $data['p_ct']; - \array_key_exists('p_wt', $data) and $self->p_wt = $data['p_wt']; - \array_key_exists('p_cpu', $data) and $self->p_cpu = $data['p_cpu']; - \array_key_exists('p_mu', $data) and $self->p_mu = $data['p_mu']; - \array_key_exists('p_pmu', $data) and $self->p_pmu = $data['p_pmu']; - \array_key_exists('d_ct', $data) and $self->d_ct = $data['d_ct']; - \array_key_exists('d_wt', $data) and $self->d_wt = $data['d_wt']; - \array_key_exists('d_cpu', $data) and $self->d_cpu = $data['d_cpu']; - \array_key_exists('d_mu', $data) and $self->d_mu = $data['d_mu']; - \array_key_exists('d_pmu', $data) and $self->d_pmu = $data['d_pmu']; + $self->p_ct = $data['p_ct'] ?? 0; + $self->p_wt = $data['p_wt'] ?? 0; + $self->p_cpu = $data['p_cpu'] ?? 0; + $self->p_mu = $data['p_mu'] ?? 0; + $self->p_pmu = $data['p_pmu'] ?? 0; + $self->d_ct = $data['d_ct'] ?? 0; + $self->d_wt = $data['d_wt'] ?? 0; + $self->d_cpu = $data['d_cpu'] ?? 0; + $self->d_mu = $data['d_mu'] ?? 0; + $self->d_pmu = $data['d_pmu'] ?? 0; return $self; } @@ -98,16 +98,16 @@ public static function fromArray(array $data): self * cpu: int<0, max>, * mu: int<0, max>, * pmu: int<0, max>, - * p_ct: float<0, 100>, - * p_wt: float<0, 100>, - * p_cpu: float<0, 100>, - * p_mu: float<0, 100>, - * p_pmu: float<0, 100>, - * d_ct: int<0, max>, - * d_wt: int<0, max>, - * d_cpu: int<0, max>, - * d_mu: int<0, max>, - * d_pmu: int<0, max> + * p_ct: float, + * p_wt: float, + * p_cpu: float, + * p_mu: float, + * p_pmu: float, + * d_ct: int, + * d_wt: int, + * d_cpu: int, + * d_mu: int, + * d_pmu: int * } */ public function jsonSerialize(): array diff --git a/src/Service/FilesObserver/Converter/Tree.php b/src/Service/FilesObserver/Converter/Tree.php index 3725795d..53731262 100644 --- a/src/Service/FilesObserver/Converter/Tree.php +++ b/src/Service/FilesObserver/Converter/Tree.php @@ -23,9 +23,9 @@ final class Tree implements \IteratorAggregate private array $lostChildren = []; /** - * @template-covariant T of object + * @template T of object * - * @param list $edges + * @param array $edges * @param callable(T): non-empty-string $getCurrent Get current node id * @param callable(T): (non-empty-string|null) $getParent Get parent node id * @@ -33,25 +33,26 @@ final class Tree implements \IteratorAggregate */ public static function fromEdgesList(array $edges, callable $getCurrent, callable $getParent): self { + /** @var self $tree */ $tree = new self(); foreach ($edges as $edge) { - $current = $getCurrent($edge); - $parent = $getParent($edge); + $id = $getCurrent($edge); + $parentId = $getParent($edge); - $tree->addItem($edge, $current, $parent); + $tree->addItem($edge, $id, $parentId); } return $tree; } /** - * @param TItem $item * @param non-empty-string $id * @param non-empty-string|null $parentId */ public function addItem(object $item, string $id, ?string $parentId): void { + /** @var TItem $item */ $branch = new Branch($item, $id, $parentId); $this->all[$id] = $branch; @@ -93,6 +94,7 @@ public function getIterator(): \Traversable public function getItemsSortedV1(?callable $sorter): \Traversable { $level = 0; + /** @var array, list>> $queue */ $queue = [$level => $this->root]; processLevel: while ($queue[$level] !== []) { From 0403cb33c67a7bed024f59719756800d09e1f76e Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Sat, 1 Jun 2024 23:46:22 +0400 Subject: [PATCH 20/21] chore(psalm): fix psalm issues --- psalm-baseline.xml | 14 ----------- src/Client/TrapHandle.php | 1 + src/Client/TrapHandle/StackTrace.php | 18 +++++++++++---- src/Command/Run.php | 2 ++ src/Config/Server/Files/ObserverConfig.php | 1 + src/Proto/Frame/Profiler.php | 4 ++++ src/Proto/Frame/Profiler/Payload.php | 19 +++++++++++---- src/Sender/ConsoleSender.php | 2 +- src/Service/Config/ConfigLoader.php | 8 +++---- src/Service/FilesObserver/FileInfo.php | 15 ++++++++++++ src/Service/FilesObserver/Handler.php | 3 +-- src/Socket/Client.php | 8 +++++-- src/functions.php | 27 +++++++++++----------- 13 files changed, 77 insertions(+), 45 deletions(-) diff --git a/psalm-baseline.xml b/psalm-baseline.xml index c2612d42..438f66cf 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -222,18 +222,4 @@ - - - - - - - - - - - - - - diff --git a/src/Client/TrapHandle.php b/src/Client/TrapHandle.php index 24041b36..f4ba6edf 100644 --- a/src/Client/TrapHandle.php +++ b/src/Client/TrapHandle.php @@ -41,6 +41,7 @@ public static function fromArray(array $array): self * * @param int<0, max> $number The tick number. * @param float $delta The time delta between the current and previous tick. + * @param int<0, max> $memory The memory usage. * * @internal */ diff --git a/src/Client/TrapHandle/StackTrace.php b/src/Client/TrapHandle/StackTrace.php index a8a9bb06..4072bee2 100644 --- a/src/Client/TrapHandle/StackTrace.php +++ b/src/Client/TrapHandle/StackTrace.php @@ -47,11 +47,19 @@ public static function stackTrace(string $baseDir = '', bool $provideObjects = f $cwdLen = \strlen($dir); $stack = []; $internal = false; - foreach ( - \debug_backtrace( - ($provideObjects ? \DEBUG_BACKTRACE_PROVIDE_OBJECT : 0) | \DEBUG_BACKTRACE_IGNORE_ARGS, - ) as $frame - ) { + + /** @var array{ + * function: non-empty-string, + * line?: int, + * file?: string, + * class?: class-string, + * type?: string, + * object?: object, + * args?: list + * } $frame */ + foreach (\debug_backtrace( + ($provideObjects ? \DEBUG_BACKTRACE_PROVIDE_OBJECT : 0) | \DEBUG_BACKTRACE_IGNORE_ARGS, + ) as $frame) { $class = $frame['class'] ?? ''; if (\str_starts_with($class, 'Buggregator\\Trap\\Client\\')) { $internal = true; diff --git a/src/Command/Run.php b/src/Command/Run.php index 69d430c0..a303e48c 100644 --- a/src/Command/Run.php +++ b/src/Command/Run.php @@ -95,7 +95,9 @@ public function createRegistry(OutputInterface $output): Sender\SenderRegistry public function getSubscribedSignals(): array { $result = []; + /** @psalm-suppress MixedAssignment */ \defined('SIGINT') and $result[] = \SIGINT; + /** @psalm-suppress MixedAssignment */ \defined('SIGTERM') and $result[] = \SIGTERM; return $result; diff --git a/src/Config/Server/Files/ObserverConfig.php b/src/Config/Server/Files/ObserverConfig.php index e9a20ac7..0d7e7a5e 100644 --- a/src/Config/Server/Files/ObserverConfig.php +++ b/src/Config/Server/Files/ObserverConfig.php @@ -26,6 +26,7 @@ abstract class ObserverConfig */ public function isValid(): bool { + /** @psalm-suppress RedundantCondition */ return $this->path !== null && $this->converterClass !== null && $this->path !== '' && \is_a($this->converterClass, FrameConverter::class, true) && $this->scanInterval > 0.0; } diff --git a/src/Proto/Frame/Profiler.php b/src/Proto/Frame/Profiler.php index 578faac9..df15912d 100644 --- a/src/Proto/Frame/Profiler.php +++ b/src/Proto/Frame/Profiler.php @@ -9,6 +9,9 @@ use Buggregator\Trap\Support\Json; /** + * @psalm-import-type Calls from \Buggregator\Trap\Proto\Frame\Profiler\Payload + * @psalm-import-type Metadata from \Buggregator\Trap\Proto\Frame\Profiler\Payload + * * @internal * @psalm-internal Buggregator */ @@ -23,6 +26,7 @@ public function __construct( public static function fromString(string $payload, \DateTimeImmutable $time): static { + /** @var array{type: non-empty-string}&Calls&Metadata $data */ $data = Json::decode($payload); return new self(Frame\Profiler\Payload::fromArray($data), $time); diff --git a/src/Proto/Frame/Profiler/Payload.php b/src/Proto/Frame/Profiler/Payload.php index 4162b40f..685d1798 100644 --- a/src/Proto/Frame/Profiler/Payload.php +++ b/src/Proto/Frame/Profiler/Payload.php @@ -21,7 +21,7 @@ * @internal * @psalm-internal Buggregator */ -class Payload implements \JsonSerializable +final class Payload implements \JsonSerializable { /** * @param PayloadType $type @@ -46,17 +46,25 @@ public static function new( array $metadata, \Closure $callsProvider, ): self { - return new static($type, $metadata, $callsProvider); + return new self($type, $metadata, $callsProvider); } + /** + * @param array{type: non-empty-string}&Calls&Metadata $data + * @param PayloadType|null $type + */ public static function fromArray(array $data, ?Type $type = null): static { $metadata = $data; unset($metadata['edges'], $metadata['peaks']); - return new static( + + /** @var \Closure(): Calls $provider */ + $provider = static fn(): array => $data; + + return new self( $type ?? PayloadType::from($data['type']), $metadata, - static fn(): array => $data, + $provider, ); } @@ -76,6 +84,9 @@ public function getMetadata(): array return $this->metadata; } + /** + * @return array{type: non-empty-string}&Calls&Metadata + */ public function toArray(): array { return ['type' => $this->type->value] + $this->getCalls() + $this->getMetadata(); diff --git a/src/Sender/ConsoleSender.php b/src/Sender/ConsoleSender.php index 821ee2a1..a5d9ed0d 100644 --- a/src/Sender/ConsoleSender.php +++ b/src/Sender/ConsoleSender.php @@ -43,7 +43,7 @@ public static function create(OutputInterface $output): self $renderer->register(new Renderer\Http()); $renderer->register(new Renderer\Profiler()); $renderer->register(new Renderer\Binary()); - $renderer->register(new Renderer\Plain($templateRenderer)); + $renderer->register(new Renderer\Plain()); return new self($renderer); } diff --git a/src/Service/Config/ConfigLoader.php b/src/Service/Config/ConfigLoader.php index f1cdb8f5..9fde86c8 100644 --- a/src/Service/Config/ConfigLoader.php +++ b/src/Service/Config/ConfigLoader.php @@ -59,10 +59,10 @@ private function injectValue(object $config, \ReflectionProperty $property, arra /** @var mixed $value */ $value = match (true) { - $attribute instanceof XPath => (static fn(?array $value, int $key): mixed => match (true) { - \array_key_exists($key, $value) => $value[$key], - default => null, - })($this->xml?->xpath($attribute->path), $attribute->key), + $attribute instanceof XPath => (static fn(array|false|null $value, int $key): mixed + => \is_array($value) && \array_key_exists($key, $value) + ? $value[$key] + : null)($this->xml?->xpath($attribute->path), $attribute->key), $attribute instanceof Env => $this->env[$attribute->name] ?? null, $attribute instanceof InputOption => $this->inputOptions[$attribute->name] ?? null, $attribute instanceof InputArgument => $this->inputArguments[$attribute->name] ?? null, diff --git a/src/Service/FilesObserver/FileInfo.php b/src/Service/FilesObserver/FileInfo.php index c8572edb..020064c2 100644 --- a/src/Service/FilesObserver/FileInfo.php +++ b/src/Service/FilesObserver/FileInfo.php @@ -9,6 +9,12 @@ */ final class FileInfo { + /** + * @param non-empty-string $path + * @param int<0, max> $size + * @param int<0, max> $ctime + * @param int<0, max> $mtime + */ public function __construct( public readonly string $path, public readonly int $size, @@ -18,6 +24,7 @@ public function __construct( public static function fromSplFileInfo(\SplFileInfo $fileInfo): self { + /** @psalm-suppress ArgumentTypeCoercion */ return new self( $fileInfo->getRealPath(), $fileInfo->getSize(), @@ -26,6 +33,14 @@ public static function fromSplFileInfo(\SplFileInfo $fileInfo): self ); } + /** + * @param array{ + * path: non-empty-string, + * size: int<0, max>, + * ctime: int<0, max>, + * mtime: int<0, max> + * } $data + */ public static function fromArray(array $data): self { return new self( diff --git a/src/Service/FilesObserver/Handler.php b/src/Service/FilesObserver/Handler.php index 41e651c7..bda3f919 100644 --- a/src/Service/FilesObserver/Handler.php +++ b/src/Service/FilesObserver/Handler.php @@ -69,7 +69,7 @@ private function syncFiles(): array foreach ($files as $info) { $path = $info->path; - if (!\is_string($path) || \array_key_exists($path, $this->cache)) { + if (\array_key_exists($path, $this->cache)) { $newState[$path] = $this->cache[$path]; continue; } @@ -102,7 +102,6 @@ private function getFiles(): \Traversable } catch (\Throwable $e) { $this->logger->info('Failed to read files from path `%s`', $this->path); $this->logger->exception($e); - return []; } } } diff --git a/src/Socket/Client.php b/src/Socket/Client.php index 9b1a2692..61d0ec55 100644 --- a/src/Socket/Client.php +++ b/src/Socket/Client.php @@ -112,7 +112,9 @@ public function process(): void */ public function setOnPayload(callable $callable): void { - $this->onPayload = @\Closure::bind($callable(...), $this) ?? $callable(...); + $closure = $callable(...); + /** @psalm-suppress PossiblyNullPropertyAssignmentValue, InvalidArgument */ + $this->onPayload = @\Closure::bind($closure, $this) ?? $closure; } /** @@ -121,7 +123,9 @@ public function setOnPayload(callable $callable): void */ public function setOnClose(callable $callable): void { - $this->onClose = @\Closure::bind($callable(...), $this) ?? $callable(...); + $closure = $callable(...); + /** @psalm-suppress PossiblyNullPropertyAssignmentValue, InvalidArgument */ + $this->onClose = @\Closure::bind($closure, $this) ?? $closure; } public function send(string $payload): void diff --git a/src/functions.php b/src/functions.php index 88f0f5fa..8e77d978 100644 --- a/src/functions.php +++ b/src/functions.php @@ -54,11 +54,13 @@ function tr(mixed ...$values): mixed $mem = $time = \microtime(true); try { if ($values === []) { + /** @var int<0, max> $memory */ + $memory = \memory_get_usage(); /** @psalm-suppress InternalMethod */ return TrapHandle::fromTicker( $counter, $counter === 0 ? 0 : $mem - $previous, - \memory_get_usage(), + $memory, )->return(); } @@ -95,16 +97,15 @@ function td(mixed ...$values): never * Register the var-dump caster for protobuf messages */ if (\class_exists(AbstractCloner::class)) { - /** @psalm-suppress MixedAssignment */ - AbstractCloner::$defaultCasters[Message::class] ??= [ProtobufCaster::class, 'cast']; - /** @psalm-suppress MixedAssignment */ - AbstractCloner::$defaultCasters[RepeatedField::class] ??= [ProtobufCaster::class, 'castRepeated']; - /** @psalm-suppress MixedAssignment */ - AbstractCloner::$defaultCasters[MapField::class] ??= [ProtobufCaster::class, 'castMap']; - /** @psalm-suppress MixedAssignment */ - AbstractCloner::$defaultCasters[EnumValue::class] ??= [ProtobufCaster::class, 'castEnum']; - /** @psalm-suppress MixedAssignment */ - AbstractCloner::$defaultCasters[Trace::class] = [TraceCaster::class, 'cast']; - /** @psalm-suppress MixedAssignment */ - AbstractCloner::$defaultCasters[TraceFile::class] = [TraceCaster::class, 'castLine']; + /** @var array $casters */ + $casters = &AbstractCloner::$defaultCasters; + + $casters[Message::class] ??= [ProtobufCaster::class, 'cast']; + $casters[RepeatedField::class] ??= [ProtobufCaster::class, 'castRepeated']; + $casters[MapField::class] ??= [ProtobufCaster::class, 'castMap']; + $casters[EnumValue::class] ??= [ProtobufCaster::class, 'castEnum']; + $casters[Trace::class] = [TraceCaster::class, 'cast']; + $casters[TraceFile::class] = [TraceCaster::class, 'castLine']; + + unset($casters); } From fd1889c25d23473982c954e66cee7898313b1c0f Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Sun, 2 Jun 2024 00:08:43 +0400 Subject: [PATCH 21/21] chore(psalm): fix psalm issues --- src/functions.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/functions.php b/src/functions.php index 8e77d978..97a75873 100644 --- a/src/functions.php +++ b/src/functions.php @@ -97,9 +97,13 @@ function td(mixed ...$values): never * Register the var-dump caster for protobuf messages */ if (\class_exists(AbstractCloner::class)) { - /** @var array $casters */ + /** @psalm-suppress UnsupportedPropertyReferenceUsage */ $casters = &AbstractCloner::$defaultCasters; - + /** + * Define var-dump related casters for protobuf messages and traces. + * + * @var array $casters + */ $casters[Message::class] ??= [ProtobufCaster::class, 'cast']; $casters[RepeatedField::class] ??= [ProtobufCaster::class, 'castRepeated']; $casters[MapField::class] ??= [ProtobufCaster::class, 'castMap'];