Skip to content

Commit

Permalink
feat(profiler): add XHProf middleware to get profiles from web API
Browse files Browse the repository at this point in the history
  • Loading branch information
roxblnfk committed Jun 13, 2024
1 parent c2c981d commit e777831
Show file tree
Hide file tree
Showing 11 changed files with 195 additions and 38 deletions.
10 changes: 10 additions & 0 deletions resources/payloads/yii-xhprof.http

Large diffs are not rendered by default.

9 changes: 5 additions & 4 deletions src/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,11 @@ public function __construct(
new Traffic\Dispatcher\VarDumper(),
new Traffic\Dispatcher\Http(
[
new Middleware\Resources(),
new Middleware\DebugPage(),
new Middleware\RayRequestDump(),
new Middleware\SentryTrap(),
$this->container->get(Middleware\Resources::class),
$this->container->get(Middleware\DebugPage::class),
$this->container->get(Middleware\RayRequestDump::class),
$this->container->get(Middleware\SentryTrap::class),
$this->container->get(Middleware\XHProfTrap::class),
],
[new Websocket()],
),
Expand Down
3 changes: 3 additions & 0 deletions src/Command/Test.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ protected function execute(
): int {
$this->logger = new Logger($output);

// XHProf
$this->sendContent('yii-xhprof.http');

$this->dump();
\usleep(100_000);
$this->mail($output, true);
Expand Down
102 changes: 102 additions & 0 deletions src/Handler/Http/Middleware/XHProfTrap.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
<?php

declare(strict_types=1);

namespace Buggregator\Trap\Handler\Http\Middleware;

use Buggregator\Trap\Handler\Http\Middleware;
use Buggregator\Trap\Logger;
use Buggregator\Trap\Module\Profiler\Struct\Profile;
use Buggregator\Trap\Module\Profiler\XHProf\ProfileBuilder as XHProfProfileBuilder;
use Buggregator\Trap\Proto\Frame;
use Nyholm\Psr7\Response;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;

/**
* @internal
* @psalm-internal Buggregator\Trap
*
* @psalm-type XHProfMessage = array{
* profile: array,
* tags: array,
* app_name: string,
* hostname: string,
* date: positive-int
* }
*/
final class XHProfTrap implements Middleware
{
private const MAX_BODY_SIZE = 10 * 1024 * 1024;

public function __construct(
private readonly XHProfProfileBuilder $profileBuilder,
private readonly Logger $logger,
) {}

public function handle(ServerRequestInterface $request, callable $next): ResponseInterface
{
try {
if ($request->getMethod() === 'POST'
&& \str_ends_with($request->getUri()->getPath(), '/api/profiler/store')
) {
return $this->processStore($request);
}
} catch (\JsonException $e) {
// Reject invalid JSON
$this->logger->exception($e, important: true);
return new Response(400, body: 'Invalid JSON data.');
} catch (\Throwable $e) {
// Reject invalid request
$this->logger->exception($e, important: true);
return new Response(400, body: $e->getMessage());
}

return $next($request);
}

/**
* @throws \JsonException
* @throws \Throwable
*/
private function processStore(ServerRequestInterface $request): ResponseInterface
{
$size = $request->getBody()->getSize();
if ($size === null || $size > self::MAX_BODY_SIZE) {
// Reject too big content
return new Response(413);
}

/** @var XHProfMessage $payload */
$payload = \json_decode((string) $request->getBody(), true, 96, \JSON_THROW_ON_ERROR);

Check failure on line 71 in src/Handler/Http/Middleware/XHProfTrap.php

View workflow job for this annotation

GitHub Actions / phpstan (ubuntu-latest, 8.2, locked)

PHPDoc tag @var for variable $payload has no value type specified in iterable type array.

Check failure on line 71 in src/Handler/Http/Middleware/XHProfTrap.php

View workflow job for this annotation

GitHub Actions / phpstan (ubuntu-latest, 8.2, locked)

PHPDoc tag @var for variable $payload has no value type specified in iterable type array.

Check failure on line 71 in src/Handler/Http/Middleware/XHProfTrap.php

View workflow job for this annotation

GitHub Actions / phpstan (ubuntu-latest, 8.2, locked)

PHPDoc tag @var for variable $payload has no value type specified in iterable type array.

Check failure on line 71 in src/Handler/Http/Middleware/XHProfTrap.php

View workflow job for this annotation

GitHub Actions / phpstan (ubuntu-latest, 8.2, locked)

PHPDoc tag @var for variable $payload has no value type specified in iterable type array.

$metadata = $payload;
unset($metadata['profile'], $metadata['tags'], $metadata['date']);

isset($payload['profile'], $payload['tags'])

Check failure on line 76 in src/Handler/Http/Middleware/XHProfTrap.php

View workflow job for this annotation

GitHub Actions / phpstan (ubuntu-latest, 8.2, locked)

Offset 'profile' on array{profile: array, tags: array, app_name: string, hostname: string, date: int<1, max>} in isset() always exists and is not nullable.

Check failure on line 76 in src/Handler/Http/Middleware/XHProfTrap.php

View workflow job for this annotation

GitHub Actions / phpstan (ubuntu-latest, 8.2, locked)

Offset 'tags' on array{profile: array, tags: array, app_name: string, hostname: string, date: int<1, max>} in isset() always exists and is not nullable.

Check failure on line 76 in src/Handler/Http/Middleware/XHProfTrap.php

View workflow job for this annotation

GitHub Actions / psalm (ubuntu-latest, 8.2, locked)

DocblockTypeContradiction

src/Handler/Http/Middleware/XHProfTrap.php:76:9: DocblockTypeContradiction: Docblock-defined type array<array-key, mixed> for $payload['profile'] is always array<array-key, mixed> (see https://psalm.dev/155)

Check failure on line 76 in src/Handler/Http/Middleware/XHProfTrap.php

View workflow job for this annotation

GitHub Actions / phpstan (ubuntu-latest, 8.2, locked)

Offset 'profile' on array{profile: array, tags: array, app_name: string, hostname: string, date: int<1, max>} in isset() always exists and is not nullable.

Check failure on line 76 in src/Handler/Http/Middleware/XHProfTrap.php

View workflow job for this annotation

GitHub Actions / phpstan (ubuntu-latest, 8.2, locked)

Offset 'tags' on array{profile: array, tags: array, app_name: string, hostname: string, date: int<1, max>} in isset() always exists and is not nullable.

Check failure on line 76 in src/Handler/Http/Middleware/XHProfTrap.php

View workflow job for this annotation

GitHub Actions / psalm (ubuntu-latest, 8.2, locked)

DocblockTypeContradiction

src/Handler/Http/Middleware/XHProfTrap.php:76:9: DocblockTypeContradiction: Docblock-defined type array<array-key, mixed> for $payload['profile'] is always array<array-key, mixed> (see https://psalm.dev/155)
&& \is_array($payload['profile'])

Check failure on line 77 in src/Handler/Http/Middleware/XHProfTrap.php

View workflow job for this annotation

GitHub Actions / phpstan (ubuntu-latest, 8.2, locked)

Right side of && is always true.

Check failure on line 77 in src/Handler/Http/Middleware/XHProfTrap.php

View workflow job for this annotation

GitHub Actions / phpstan (ubuntu-latest, 8.2, locked)

Right side of && is always true.
&& \is_array($payload['tags'])

Check failure on line 78 in src/Handler/Http/Middleware/XHProfTrap.php

View workflow job for this annotation

GitHub Actions / phpstan (ubuntu-latest, 8.2, locked)

Right side of && is always true.

Check failure on line 78 in src/Handler/Http/Middleware/XHProfTrap.php

View workflow job for this annotation

GitHub Actions / phpstan (ubuntu-latest, 8.2, locked)

Right side of && is always true.
or throw new \InvalidArgumentException('Invalid payload');

/** @psalm-suppress MixedAssignment */
$time = $request->getAttribute('begin_at');
$time = $time instanceof \DateTimeImmutable ? $time : new \DateTimeImmutable();

\Fiber::suspend(
new Frame\Profiler(
payload: Frame\Profiler\Payload::new(
type: Frame\Profiler\Type::XHProf,
callsProvider: fn(): Profile => $this->profileBuilder->createProfile(
date: $time,
metadata: $metadata,
tags: $payload['tags'] ?? [],

Check failure on line 92 in src/Handler/Http/Middleware/XHProfTrap.php

View workflow job for this annotation

GitHub Actions / phpstan (ubuntu-latest, 8.2, locked)

Offset 'tags' on array{profile: array, tags: array, app_name: string, hostname: string, date: int<1, max>} on left side of ?? always exists and is not nullable.

Check failure on line 92 in src/Handler/Http/Middleware/XHProfTrap.php

View workflow job for this annotation

GitHub Actions / psalm (ubuntu-latest, 8.2, locked)

MixedArgumentTypeCoercion

src/Handler/Http/Middleware/XHProfTrap.php:92:31: MixedArgumentTypeCoercion: Argument 3 of Buggregator\Trap\Module\Profiler\XHProf\ProfileBuilder::createProfile expects array<non-empty-string, non-empty-string>, but parent type array<array-key, mixed> provided (see https://psalm.dev/194)

Check failure on line 92 in src/Handler/Http/Middleware/XHProfTrap.php

View workflow job for this annotation

GitHub Actions / phpstan (ubuntu-latest, 8.2, locked)

Offset 'tags' on array{profile: array, tags: array, app_name: string, hostname: string, date: int<1, max>} on left side of ?? always exists and is not nullable.

Check failure on line 92 in src/Handler/Http/Middleware/XHProfTrap.php

View workflow job for this annotation

GitHub Actions / psalm (ubuntu-latest, 8.2, locked)

MixedArgumentTypeCoercion

src/Handler/Http/Middleware/XHProfTrap.php:92:31: MixedArgumentTypeCoercion: Argument 3 of Buggregator\Trap\Module\Profiler\XHProf\ProfileBuilder::createProfile expects array<non-empty-string, non-empty-string>, but parent type array<array-key, mixed> provided (see https://psalm.dev/194)
calls: $payload['profile'] ?? [],

Check failure on line 93 in src/Handler/Http/Middleware/XHProfTrap.php

View workflow job for this annotation

GitHub Actions / phpstan (ubuntu-latest, 8.2, locked)

Offset 'profile' on array{profile: array, tags: array, app_name: string, hostname: string, date: int<1, max>} on left side of ?? always exists and is not nullable.

Check failure on line 93 in src/Handler/Http/Middleware/XHProfTrap.php

View workflow job for this annotation

GitHub Actions / psalm (ubuntu-latest, 8.2, locked)

MixedArgumentTypeCoercion

src/Handler/Http/Middleware/XHProfTrap.php:93:32: MixedArgumentTypeCoercion: Argument 4 of Buggregator\Trap\Module\Profiler\XHProf\ProfileBuilder::createProfile expects array<non-empty-string, array{cpu: int<0, max>, ct: int<0, max>, mu: int<0, max>, pmu: int<0, max>, wt: int<0, max>}>, but parent type array<array-key, mixed> provided (see https://psalm.dev/194)

Check failure on line 93 in src/Handler/Http/Middleware/XHProfTrap.php

View workflow job for this annotation

GitHub Actions / phpstan (ubuntu-latest, 8.2, locked)

Offset 'profile' on array{profile: array, tags: array, app_name: string, hostname: string, date: int<1, max>} on left side of ?? always exists and is not nullable.

Check failure on line 93 in src/Handler/Http/Middleware/XHProfTrap.php

View workflow job for this annotation

GitHub Actions / psalm (ubuntu-latest, 8.2, locked)

MixedArgumentTypeCoercion

src/Handler/Http/Middleware/XHProfTrap.php:93:32: MixedArgumentTypeCoercion: Argument 4 of Buggregator\Trap\Module\Profiler\XHProf\ProfileBuilder::createProfile expects array<non-empty-string, array{cpu: int<0, max>, ct: int<0, max>, mu: int<0, max>, pmu: int<0, max>, wt: int<0, max>}>, but parent type array<array-key, mixed> provided (see https://psalm.dev/194)
),
),
time: $time,
),
);

return new Response(200);
}
}
25 changes: 19 additions & 6 deletions src/Module/Profiler/Struct/Peaks.php
Original file line number Diff line number Diff line change
Expand Up @@ -74,12 +74,25 @@ public function jsonSerialize(): array
];
}

public function update(Cost $item): void
public function update(Cost $cost): void
{
$this->ct = \max($this->ct, $item->ct);
$this->wt = \max($this->wt, $item->wt);
$this->cpu = \max($this->cpu, $item->cpu);
$this->mu = \max($this->mu, $item->mu);
$this->pmu = \max($this->pmu, $item->pmu);
$this->ct = \max($this->ct, $cost->ct);
$this->wt = \max($this->wt, $cost->wt);
$this->cpu = \max($this->cpu, $cost->cpu);
$this->mu = \max($this->mu, $cost->mu);
$this->pmu = \max($this->pmu, $cost->pmu);
}

public function toCost(): Cost
{
return new Cost($this->ct, $this->wt, $this->cpu, $this->mu, $this->pmu);
}

public function add(Cost $cost): void
{
$this->wt += $cost->wt;
$this->cpu += $cost->cpu;
$this->mu = \max($this->mu, $cost->mu);
$this->pmu = \max($this->pmu, $cost->pmu);
}
}
30 changes: 19 additions & 11 deletions src/Module/Profiler/Struct/Tree.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@
final class Tree implements \IteratorAggregate, \Countable
{
/** @var array<non-empty-string, Branch<TItem>> */
private array $root = [];
public array $root = [];

/** @var array<non-empty-string, Branch<TItem>> */
private array $all = [];
public array $all = [];

/** @var array<non-empty-string, Branch<TItem>> */
private array $lostChildren = [];
public array $lostChildren = [];

/**
* @template T of object
Expand Down Expand Up @@ -94,6 +94,14 @@ public function iterateAll(): \Traversable
}
}

/**
* @return \Traversable<Branch<TItem>>
*/
public function iterateLostChildren(): \Traversable
{
yield from $this->lostChildren;
}

/**
* Yield items by the level in the hierarchy with custom sorting in level scope
*
Expand Down Expand Up @@ -144,6 +152,14 @@ public function getItemsSortedV0(?callable $sorter): \Traversable
}
}

/**
* @return int<0, max>
*/
public function count(): int
{
return \count($this->all);
}

public function __destruct()
{
foreach ($this->all as $branch) {
Expand All @@ -152,12 +168,4 @@ public function __destruct()

unset($this->all, $this->root, $this->lostChildren);
}

/**
* @return int<0, max>
*/
public function count(): int
{
return \count($this->all);
}
}
21 changes: 21 additions & 0 deletions src/Module/Profiler/XHProf/ProfileBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ private function dataToPayload(array $data): array
/** @var Tree<Edge> $tree */
$tree = new Tree();

unset($data['value']);

foreach (\array_reverse($data, true) as $key => $value) {
[$caller, $callee] = \explode('==>', $key, 2) + [1 => ''];
if ($callee === '') {
Expand All @@ -76,6 +78,25 @@ private function dataToPayload(array $data): array
$tree->addItem($edge, $edge->callee, $edge->caller);
}

// Add parents for lost children
$lostParents = [];
/** @var Branch<Edge> $branch */
foreach ($tree->iterateLostChildren() as $branch) {
\assert($branch->item->caller !== null);
($lostParents[$branch->item->caller] ??= new Peaks())
->add($branch->item->cost);
}
/** @var array<non-empty-string, Peaks> $lostParents */
foreach ($lostParents as $key => $peak) {
$edge = new Edge(
caller: null,
callee: $key,
cost: $peak->toCost(),
);
$tree->addItem($edge, $edge->callee, $edge->caller);
}
unset($lostParents);

/**
* Calc percentages and delta
* @var Branch<Edge> $branch Needed for IDE
Expand Down
10 changes: 5 additions & 5 deletions src/Proto/Frame/Binary.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ public function getSize(): int
return (int) $this->stream->getSize();
}

public function getStream(): StreamInterface
{
return $this->stream;
}

/**
* @throws \JsonException
*/
Expand All @@ -41,9 +46,4 @@ public function __toString(): string
'size' => $this->getSize(),
]);
}

public function getStream(): StreamInterface
{
return $this->stream;
}
}
10 changes: 5 additions & 5 deletions src/Proto/Frame/Http.php
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,11 @@ public function iterateUploadedFiles(): \Iterator
return $generator($this->request->getUploadedFiles());
}

public function getStream(): StreamInterface
{
return $this->request->getBody();
}

/**
* @throws \JsonException
*/
Expand All @@ -124,9 +129,4 @@ public function __toString(): string
'uploadedFiles' => $this->request->getUploadedFiles(),
]);
}

public function getStream(): StreamInterface
{
return $this->request->getBody();
}
}
3 changes: 1 addition & 2 deletions src/Proto/Frame/Profiler/Payload.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,7 @@ final class Payload implements \JsonSerializable
private function __construct(
public readonly PayloadType $type,
private readonly \Closure $callsProvider,
) {
}
) {}

/**
* @param PayloadType $type
Expand Down
10 changes: 5 additions & 5 deletions src/Proto/Frame/Smtp.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,16 +44,16 @@ public function getFiles(): array
return $this->message->getAttachments();
}

public function getStream(): StreamInterface
{
return $this->message->getBody();
}

/**
* @throws \JsonException
*/
public function __toString(): string
{
return Json::encode($this->message);
}

public function getStream(): StreamInterface
{
return $this->message->getBody();
}
}

0 comments on commit e777831

Please sign in to comment.