Skip to content

Commit

Permalink
Feat: Rework the profiler to include screenshots (#92)
Browse files Browse the repository at this point in the history
  • Loading branch information
ConstantBqt authored Jul 11, 2024
1 parent f323d27 commit daec5e1
Show file tree
Hide file tree
Showing 12 changed files with 448 additions and 211 deletions.
11 changes: 10 additions & 1 deletion config/debug.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

use Sensiolabs\GotenbergBundle\DataCollector\GotenbergDataCollector;
use Sensiolabs\GotenbergBundle\Debug\TraceableGotenbergPdf;
use Sensiolabs\GotenbergBundle\Debug\TraceableGotenbergScreenshot;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
use Symfony\Component\DependencyInjection\Reference;
use function Symfony\Component\DependencyInjection\Loader\Configurator\abstract_arg;
Expand All @@ -11,16 +12,24 @@
return static function (ContainerConfigurator $container): void {
$services = $container->services();

$services->set('sensiolabs_gotenberg.traceable', TraceableGotenbergPdf::class)
$services->set('sensiolabs_gotenberg.traceable_pdf', TraceableGotenbergPdf::class)
->decorate('sensiolabs_gotenberg.pdf')
->args([
new Reference('.inner'),
])
;

$services->set('sensiolabs_gotenberg.traceable_screenshot', TraceableGotenbergScreenshot::class)
->decorate('sensiolabs_gotenberg.screenshot')
->args([
new Reference('.inner'),
])
;

$services->set('sensiolabs_gotenberg.data_collector', GotenbergDataCollector::class)
->args([
service('sensiolabs_gotenberg.pdf'),
service('sensiolabs_gotenberg.screenshot'),
tagged_locator('sensiolabs_gotenberg.pdf_builder'),
abstract_arg('All default options will be set through the configuration.'),
])
Expand Down
116 changes: 68 additions & 48 deletions src/DataCollector/GotenbergDataCollector.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,29 @@
namespace Sensiolabs\GotenbergBundle\DataCollector;

use Sensiolabs\GotenbergBundle\Builder\Pdf\PdfBuilderInterface;
use Sensiolabs\GotenbergBundle\Builder\Screenshot\ScreenshotBuilderInterface;
use Sensiolabs\GotenbergBundle\Debug\Builder\TraceablePdfBuilder;
use Sensiolabs\GotenbergBundle\Debug\Builder\TraceableScreenshotBuilder;
use Sensiolabs\GotenbergBundle\Debug\TraceableGotenbergPdf;
use Sensiolabs\GotenbergBundle\Debug\TraceableGotenbergScreenshot;
use Symfony\Component\DependencyInjection\ServiceLocator;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\DataCollector\DataCollector;
use Symfony\Component\HttpKernel\DataCollector\LateDataCollectorInterface;
use Symfony\Component\VarDumper\Caster\ArgsStub;
use Symfony\Component\VarDumper\Caster\CutArrayStub;
use Symfony\Component\VarDumper\Cloner\Data;

final class GotenbergDataCollector extends DataCollector implements LateDataCollectorInterface
{
/**
* @param ServiceLocator<PdfBuilderInterface> $builders
* @param array<mixed> $defaultOptions
* @param ServiceLocator<PdfBuilderInterface|ScreenshotBuilderInterface> $builders
* @param array<mixed> $defaultOptions
*/
public function __construct(
private readonly TraceableGotenbergPdf $traceableGotenberg,
private readonly TraceableGotenbergPdf $traceableGotenbergPdf,
private readonly TraceableGotenbergScreenshot $traceableGotenbergScreenshot,
private readonly ServiceLocator $builders,
private readonly array $defaultOptions,
) {
Expand All @@ -37,18 +42,19 @@ public function collect(Request $request, Response $response, \Throwable|null $e
foreach ($this->builders->getProvidedServices() as $id => $type) {
$builder = $this->builders->get($id);

if ($builder instanceof TraceablePdfBuilder) {
if ($builder instanceof TraceablePdfBuilder || $builder instanceof TraceableScreenshotBuilder) {
$builder = $builder->getInner();
}

if (str_starts_with($id, '.sensiolabs_gotenberg.pdf_builder.')) {
[$id] = sscanf($id, '.sensiolabs_gotenberg.pdf_builder.%s');
} elseif (str_starts_with($id, '.sensiolabs_gotenberg.screenshot_builder.')) {
[$id] = sscanf($id, '.sensiolabs_gotenberg.screenshot_builder.%s') ?? [$id];
}

$this->data['builders'][$id] = [
'class' => $builder::class,
'default_options' => $this->defaultOptions[$id] ?? [],
'pdfs' => [],
];
}
}
Expand All @@ -60,34 +66,49 @@ public function getName(): string

public function lateCollect(): void
{
/**
* @var string $id
* @var TraceablePdfBuilder $builder
*/
foreach ($this->traceableGotenberg->getBuilders() as [$id, $builder]) {
$this->data['builders'][$id]['pdfs'] = array_merge(
$this->data['builders'][$id]['pdfs'],
array_map(function (array $request): array {
$this->data['request_total_time'] += $request['time'];
$this->data['request_total_memory'] += $request['memory'];
$this->data['request_total_size'] += $request['size'] ?? 0;

return [
'time' => $request['time'],
'memory' => $request['memory'],
'size' => $this->formatSize($request['size'] ?? 0),
'fileName' => $request['fileName'],
'calls' => array_map(function (array $call): array {
return [
'method' => $call['method'],
'stub' => $this->cloneVar(new ArgsStub($call['arguments'], $call['method'], $call['class'])),
];
}, $request['calls']),
];
}, $builder->getPdfs()),
);

$this->data['request_count'] += \count($builder->getPdfs());
$this->lateCollectFiles($this->traceableGotenbergPdf->getBuilders(), 'pdf');
$this->lateCollectFiles($this->traceableGotenbergScreenshot->getBuilders(), 'screenshot');
}

/**
* @param list<array{string, TraceablePdfBuilder|TraceableScreenshotBuilder}> $builders
*/
private function lateCollectFiles(array $builders, string $type): void
{
foreach ($builders as [$id, $builder]) {
foreach ($builder->getFiles() as $request) {
$this->data['request_total_time'] += $request['time'];
$this->data['request_total_memory'] += $request['memory'];
$this->data['request_total_size'] += $request['size'] ?? 0;
$this->data['files'][] = [
'builderClass' => $builder->getInner()::class,
'configuration' => [
'options' => $this->cloneVar([]),
'default_options' => $this->cloneVar($this->defaultOptions[$id] ?? []),
],
'type' => $type,
'time' => $request['time'],
'memory' => $request['memory'],
'size' => $this->formatSize($request['size'] ?? 0),
'fileName' => $request['fileName'],
'calls' => array_map(function (array $call): array {
$args = array_map(function (mixed $arg): mixed {
if (\is_array($arg)) {
return new CutArrayStub($arg, array_keys($arg));
}

return $arg;
}, $call['arguments']);

return [
'method' => $call['method'],
'stub' => $this->cloneVar(new ArgsStub($args, $call['method'], $call['class'])),
];
}, $request['calls']),
];
}

$this->data['request_count'] += \count($builder->getFiles());
}
}

Expand All @@ -109,24 +130,23 @@ private function formatSize(int $size): array
}

/**
* @return array<string, array{
* 'class': string,
* 'default_options': array<mixed>,
* 'pdfs': list<array{
* 'time': float,
* 'memory': int,
* 'size': int<0, max>|null,
* 'fileName': string,
* 'calls': list<array{
* 'method': string,
* 'stub': Data
* }>
* }>
* @return list<array{
* builderClass: string,
* type: string,
* time: float,
* memory: int,
* size: int<0, max>|null,
* fileName: string,
* configuration: array<string, array<mixed, mixed>>,
* calls: list<array{
* method: string,
* stub: Data,
* }>
* }>
*/
public function getBuilders(): array
public function getFiles(): array
{
return $this->data['builders'] ?? [];
return $this->data['files'] ?? [];
}

public function getRequestCount(): int
Expand Down
2 changes: 1 addition & 1 deletion src/Debug/Builder/TraceablePdfBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ public function __call(string $name, array $arguments): mixed
/**
* @return list<array{'time': float|null, 'memory': int|null, 'size': int<0, max>|null, 'fileName': string, 'calls': list<array{'class': class-string<PdfBuilderInterface>, 'method': string, 'arguments': array<mixed>}>}>
*/
public function getPdfs(): array
public function getFiles(): array
{
return $this->pdfs;
}
Expand Down
100 changes: 100 additions & 0 deletions src/Debug/Builder/TraceableScreenshotBuilder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
<?php

namespace Sensiolabs\GotenbergBundle\Debug\Builder;

use Sensiolabs\GotenbergBundle\Builder\Screenshot\ScreenshotBuilderInterface;
use Sensiolabs\GotenbergBundle\Client\GotenbergResponse;
use Symfony\Component\Stopwatch\Stopwatch;

final class TraceableScreenshotBuilder implements ScreenshotBuilderInterface
{
/**
* @var list<array{'time': float, 'memory': int, 'size': int<0, max>|null, 'fileName': string, 'calls': list<array{'method': string, 'class': class-string<ScreenshotBuilderInterface>, 'arguments': array<mixed>}>}>
*/
private array $screenshots = [];

/**
* @var list<array{'class': class-string<ScreenshotBuilderInterface>, 'method': string, 'arguments': array<mixed>}>
*/
private array $calls = [];

private int $totalGenerated = 0;

private static int $count = 0;

public function __construct(
private readonly ScreenshotBuilderInterface $inner,
private readonly Stopwatch $stopwatch,
) {
}

public function generate(): GotenbergResponse
{
$name = self::$count.'.'.$this->inner::class.'::'.__FUNCTION__;
++self::$count;

$swEvent = $this->stopwatch->start($name, 'gotenberg.generate_screenshot');
$response = $this->inner->generate();
$swEvent->stop();

$fileName = 'Unknown';
if ($response->headers->has('Content-Disposition')) {
$matches = [];

/* @see https://onlinephp.io/c/c2606 */
preg_match('#[^;]*;\sfilename="?(?P<fileName>[^"]*)"?#', $response->headers->get('Content-Disposition', ''), $matches);
$fileName = $matches['fileName'];
}

$lengthInBytes = null;
if ($response->headers->has('Content-Length')) {
$lengthInBytes = abs((int) $response->headers->get('Content-Length'));
}

$this->screenshots[] = [
'calls' => $this->calls,
'time' => $swEvent->getDuration(),
'memory' => $swEvent->getMemory(),
'status' => $response->getStatusCode(),
'size' => $lengthInBytes,
'fileName' => $fileName,
];

++$this->totalGenerated;

return $response;
}

/**
* @param array<mixed> $arguments
*/
public function __call(string $name, array $arguments): mixed
{
$result = $this->inner->$name(...$arguments);

$this->calls[] = [
'class' => $this->inner::class,
'method' => $name,
'arguments' => $arguments,
];

if ($result === $this->inner) {
return $this;
}

return $result;
}

/**
* @return list<array{'time': float, 'memory': int, 'size': int<0, max>|null, 'fileName': string, 'calls': list<array{'class': class-string<ScreenshotBuilderInterface>, 'method': string, 'arguments': array<mixed>}>}>
*/
public function getFiles(): array
{
return $this->screenshots;
}

public function getInner(): ScreenshotBuilderInterface
{
return $this->inner;
}
}
Loading

0 comments on commit daec5e1

Please sign in to comment.