Skip to content

Commit

Permalink
[FEATURE] Add base for profiler (#27)
Browse files Browse the repository at this point in the history
  • Loading branch information
Neirda24 authored Apr 16, 2024
1 parent 606a0a2 commit 4bce233
Show file tree
Hide file tree
Showing 22 changed files with 694 additions and 34 deletions.
7 changes: 5 additions & 2 deletions .php-cs-fixer.dist.php
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
<?php

$finder = (new PhpCsFixer\Finder())
->in(__DIR__)
->exclude(__DIR__.'/var/')
->in([
__DIR__ . '/config',
__DIR__ . '/src',
__DIR__ . '/tests',
])
;

return (new PhpCsFixer\Config())
Expand Down
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@
"phpstan/phpstan-symfony": "^1.3",
"phpunit/phpunit": "^10.4",
"symfony/framework-bundle": "^6.4 || ^7.0",
"symfony/twig-bundle": "^6.4 || ^7.0"
"symfony/twig-bundle": "^6.4 || ^7.0",
"symfony/stopwatch": "^6.4 || ^7.0"
},
"config": {
"allow-plugins": {
Expand Down
29 changes: 29 additions & 0 deletions config/debug.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

use Sensiolabs\GotenbergBundle\DataCollector\GotenbergDataCollector;
use Sensiolabs\GotenbergBundle\Debug\TraceableGotenberg;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
use Symfony\Component\DependencyInjection\Reference;
use function Symfony\Component\DependencyInjection\Loader\Configurator\abstract_arg;
use function Symfony\Component\DependencyInjection\Loader\Configurator\service;
use function Symfony\Component\DependencyInjection\Loader\Configurator\tagged_locator;

return static function (ContainerConfigurator $container): void {
$services = $container->services();

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

$services->set('sensiolabs_gotenberg.data_collector', GotenbergDataCollector::class)
->args([
service('sensiolabs_gotenberg'),
tagged_locator('sensiolabs_gotenberg.builder'),
abstract_arg('All default options will be set through the configuration.'),
])
->tag('data_collector', ['template' => '@SensiolabsGotenberg/Collector/sensiolabs_gotenberg.html.twig', 'id' => 'sensiolabs_gotenberg'])
;
};
5 changes: 2 additions & 3 deletions config/services.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,12 @@
use Sensiolabs\GotenbergBundle\Twig\GotenbergAssetExtension;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use function Symfony\Component\DependencyInjection\Loader\Configurator\abstract_arg;
use function Symfony\Component\DependencyInjection\Loader\Configurator\param;
use function Symfony\Component\DependencyInjection\Loader\Configurator\service;
use function Symfony\Component\DependencyInjection\Loader\Configurator\tagged_locator;

return function (ContainerConfigurator $container): void {
return static function (ContainerConfigurator $container): void {
$services = $container->services();

$services->set('sensiolabs_gotenberg.client', GotenbergClient::class)
Expand All @@ -32,7 +31,7 @@
->args([
service(Filesystem::class),
param('kernel.project_dir'),
abstract_arg('base_directory to assets'),
abstract_arg('assets_directory to assets'),
])
->alias(AssetBaseDirFormatter::class, 'sensiolabs_gotenberg.asset.base_dir_formatter')
;
Expand Down
4 changes: 4 additions & 0 deletions phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ parameters:
- 'src'
- 'tests'
ignoreErrors:
-
message: "#^Cannot use array destructuring on array\\<int, string\\|null\\>\\|null\\.$#"
count: 1
path: src/DataCollector/GotenbergDataCollector.php
-
message: "#^Method Sensiolabs\\\\GotenbergBundle\\\\Tests\\\\Kernel\\:\\:configureContainer\\(\\) is unused\\.$#"
count: 1
Expand Down
4 changes: 2 additions & 2 deletions src/Builder/AbstractPdfBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ abstract class AbstractPdfBuilder implements PdfBuilderInterface
*/
protected array $formFields = [];

private ?string $fileName = null;
private string|null $fileName = null;

private string $headerDisposition = HeaderUtils::DISPOSITION_INLINE;

Expand Down Expand Up @@ -129,7 +129,7 @@ protected function addNormalizer(string $key, \Closure $normalizer): void
*
* @return list<array<string, mixed>>
*/
private function addToMultipart(string $key, array|string|int|float|bool|DataPart $value, ?\Closure $preCallback = null): array
private function addToMultipart(string $key, array|string|int|float|bool|DataPart $value, \Closure|null $preCallback = null): array
{
if (null !== $preCallback) {
$result = [];
Expand Down
9 changes: 5 additions & 4 deletions src/Builder/UrlPdfBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ final class UrlPdfBuilder extends AbstractChromiumPdfBuilder
public function __construct(
GotenbergClientInterface $gotenbergClient,
AssetBaseDirFormatter $asset,
?Environment $twig = null,
private readonly ?UrlGeneratorInterface $urlGenerator = null,
Environment|null $twig = null,
private readonly UrlGeneratorInterface|null $urlGenerator = null,
) {
parent::__construct($gotenbergClient, $asset, $twig);
}
Expand All @@ -32,12 +32,13 @@ public function url(string $url): self
}

/**
* @param string $name #Route
* @param string $name #Route
* @param array<mixed> $parameters
*/
public function route(string $name, array $parameters = []): self
{
if (null === $this->urlGenerator) {
throw new \LogicException(\sprintf('Router is required to use "%s" method. Try to run "composer require symfony/routing".', __METHOD__));
throw new \LogicException(sprintf('Router is required to use "%s" method. Try to run "composer require symfony/routing".', __METHOD__));
}

return $this->url($this->urlGenerator->generate($name, $parameters, UrlGeneratorInterface::ABSOLUTE_URL));
Expand Down
154 changes: 154 additions & 0 deletions src/DataCollector/GotenbergDataCollector.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
<?php

namespace Sensiolabs\GotenbergBundle\DataCollector;

use Sensiolabs\GotenbergBundle\Builder\PdfBuilderInterface;
use Sensiolabs\GotenbergBundle\Debug\Builder\TraceablePdfBuilder;
use Sensiolabs\GotenbergBundle\Debug\TraceableGotenberg;
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\Cloner\Data;

final class GotenbergDataCollector extends DataCollector implements LateDataCollectorInterface
{
/**
* @param ServiceLocator<PdfBuilderInterface> $builders
* @param array<mixed> $defaultOptions
*/
public function __construct(
private readonly TraceableGotenberg $traceableGotenberg,
private readonly ServiceLocator $builders,
private readonly array $defaultOptions,
) {
}

public function collect(Request $request, Response $response, \Throwable|null $exception = null): void
{
$this->data['request_total_memory'] = 0;
$this->data['request_total_size'] = 0;
$this->data['request_total_time'] = 0;
$this->data['request_count'] = 0;
$this->data['builders'] = [];

foreach ($this->builders->getProvidedServices() as $id => $type) {
$builder = $this->builders->get($id);

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

if (str_starts_with($id, '.sensiolabs_gotenberg.builder.')) {
[$id] = sscanf($id, '.sensiolabs_gotenberg.builder.%s');
}

$this->data['builders'][$id] = [
'class' => $builder::class,
'default_options' => $this->defaultOptions[$id] ?? [],
'pdfs' => [],
];
}
}

public function getName(): string
{
return 'sensiolabs_gotenberg';
}

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());
}
}

/**
* @param int<0, max> $size
*
* @return array{float, string}
*/
private function formatSize(int $size): array
{
return match (true) {
($size / 1024 < 1) => [$size, 'B'],
($size / (1024 ** 2) < 1) => [round($size / 1024, 2), 'kB'],
($size / (1024 ** 3) < 1) => [round($size / (1024 ** 2), 2), 'MB'],
($size / (1024 ** 4) < 1) => [round($size / (1024 ** 3), 2), 'GB'],
($size / (1024 ** 5) < 1) => [round($size / (1024 ** 4), 2), 'TB'],
default => throw new \LogicException('File too big'),
};
}

/**
* @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
* }>
* }>
* }>
*/
public function getBuilders(): array
{
return $this->data['builders'] ?? [];
}

public function getRequestCount(): int
{
return $this->data['request_count'] ?? 0;
}

public function getRequestTotalTime(): int|float
{
return $this->data['request_total_time'] ?? 0.0;
}

public function getRequestTotalMemory(): int
{
return $this->data['request_total_memory'] ?? 0;
}

/**
* @return array{float, string}
*/
public function getRequestTotalSize(): array
{
return $this->formatSize($this->data['request_total_size'] ?? 0);
}
}
99 changes: 99 additions & 0 deletions src/Debug/Builder/TraceablePdfBuilder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
<?php

namespace Sensiolabs\GotenbergBundle\Debug\Builder;

use Sensiolabs\GotenbergBundle\Builder\PdfBuilderInterface;
use Sensiolabs\GotenbergBundle\Client\PdfResponse;
use Symfony\Component\Stopwatch\Stopwatch;

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

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

private int $totalGenerated = 0;

private static int $count = 0;

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

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

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

$fileName = 'Unknown.pdf';
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->pdfs[] = [
'calls' => $this->calls,
'time' => $swEvent->getDuration(),
'memory' => $swEvent->getMemory(),
'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<PdfBuilderInterface>, 'method': string, 'arguments': array<mixed>}>}>
*/
public function getPdfs(): array
{
return $this->pdfs;
}

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

0 comments on commit 4bce233

Please sign in to comment.