Skip to content

Commit

Permalink
🎉 inital commit
Browse files Browse the repository at this point in the history
  • Loading branch information
Kanti committed Aug 6, 2022
0 parents commit 53cac8c
Show file tree
Hide file tree
Showing 14 changed files with 508 additions and 0 deletions.
15 changes: 15 additions & 0 deletions Classes/Dto/Time.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

declare(strict_types=1);

namespace Kanti\ServerTiming\Dto;

final class Time
{
/** @var string */
public $key;
/** @var float[] */
public $startTime = [];
/** @var float[] */
public $stopTime = [];
}
44 changes: 44 additions & 0 deletions Classes/Middleware/AdminpanelSqlLoggingMiddleware.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

declare(strict_types=1);

namespace Kanti\ServerTiming\Middleware;

use Doctrine\DBAL\Logging\LoggerChain;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use TYPO3\CMS\Adminpanel\Log\DoctrineSqlLogger;
use TYPO3\CMS\Adminpanel\Utility\StateUtility;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Utility\GeneralUtility;

class AdminpanelSqlLoggingMiddleware implements MiddlewareInterface
{
/**
* Enable SQL Logging as early as possible to catch all queries if the admin panel is active
*
* @param ServerRequestInterface $request
* @param RequestHandlerInterface $handler
* @return ResponseInterface
*/
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
if (StateUtility::isActivatedForUser() && StateUtility::isOpen()) {
$connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
$connection = $connectionPool->getConnectionByName(ConnectionPool::DEFAULT_CONNECTION_NAME);
$connection->getConfiguration()->setSQLLogger(
new LoggerChain(
array_filter(
[
GeneralUtility::makeInstance(DoctrineSqlLogger::class),
$connection->getConfiguration()->getSQLLogger(),
]
)
)
);
}
return $handler->handle($request);
}
}
46 changes: 46 additions & 0 deletions Classes/Middleware/FirstMiddleware.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

declare(strict_types=1);

namespace Kanti\ServerTiming\Middleware;

use Doctrine\DBAL\Logging\SQLLogger;
use Kanti\ServerTiming\Utility\TimingUtility;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Utility\DebuggerUtility;

class FirstMiddleware implements MiddlewareInterface
{
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
TimingUtility::start('middleware Inward');
$this->registerSqlLogger();
$response = $handler->handle($request);
TimingUtility::end('middleware Outward');
return $response;
}

protected function registerSqlLogger(): void
{
$connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
$connection = $connectionPool->getConnectionByName(ConnectionPool::DEFAULT_CONNECTION_NAME);
$connection->getConfiguration()->setSQLLogger(
new class implements SQLLogger {
public function startQuery($sql, ?array $params = null, ?array $types = null)
{
TimingUtility::start('sql', true);
}

public function stopQuery()
{
TimingUtility::end('sql');
}
}
);
}
}
24 changes: 24 additions & 0 deletions Classes/Middleware/LastMiddleware.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

declare(strict_types=1);

namespace Kanti\ServerTiming\Middleware;

use Kanti\ServerTiming\Utility\TimingUtility;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;

class LastMiddleware implements MiddlewareInterface
{
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
TimingUtility::end('middleware Inward');
TimingUtility::start('requestHandler');
$response = $handler->handle($request);
TimingUtility::end('requestHandler');
TimingUtility::start('middleware Outward');
return $response;
}
}
28 changes: 28 additions & 0 deletions Classes/Utility/GuzzleUtility.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

declare(strict_types=1);

namespace Kanti\ServerTiming\Utility;

use Closure;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;

class GuzzleUtility
{
public static function getHandler(): Closure
{
return static function (callable $handler): Closure {
return static function (RequestInterface $request, array $options) use ($handler): ResponseInterface {
$str = 'guzzle ' . $request->getMethod();
if ($request->getUri()) {
$str .= ' ' . $request->getUri()->getHost();
}
$stop = TimingUtility::stopWatch($str);
$response = $handler($request, $options);
$stop();
return $response;
};
};
}
}
125 changes: 125 additions & 0 deletions Classes/Utility/TimingUtility.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
<?php

declare(strict_types=1);

namespace Kanti\ServerTiming\Utility;

use Closure;
use Kanti\ServerTiming\Dto\Time;
use TYPO3\CMS\Extbase\Utility\DebuggerUtility;

final class TimingUtility
{
/** @var Time[] */
private static $order = [];
/** @var array<string, Time> */
private static $keyRef = [];
/** @var array<string, \Closure> */
private static $stopWatchStack = [];
/** @var bool */
private static $registered = false;

public static function start(string $key, bool $isTotal = false): void
{
$s = self::stopWatch($key, $isTotal);
if (isset(self::$stopWatchStack[$key])) {
if (!$isTotal) {
throw new \Exception('only one measurement at a time, use TimingUtility::stopWatch() for parallel measurements');
}
}
self::$stopWatchStack[$key] = $s;
}

public static function end(string $key): void
{
if (!isset(self::$stopWatchStack[$key])) {
throw new \Exception('where is no measurement with this key');
}
$stop = self::$stopWatchStack[$key];
$stop();
}

public static function stopWatch(string $key, bool $isTotal = false): \Closure
{
if ($isTotal) {
if (isset(self::$keyRef[$key])) {
$time = self::$keyRef[$key];
} else {
$time = new Time();
$time->key = $key;
self::$keyRef[$key] = $time;
self::$order[] = $time;
}
} else {
$time = new Time();
$time->key = $key;
self::$order[] = $time;
}
$time->startTime[] = microtime(true);


if (!self::$registered) {
register_shutdown_function(static function () {
self::shutdown();
});
self::$registered = true;
}

return static function () use ($time) {
$time->stopTime[] = microtime(true);
};
}

private static function shutdown(): void
{
if (PHP_SAPI === 'cli') {
return;
}
self::end('php');
$timings = [];
$keyCount = [];
foreach (self::$order as $index => $time) {
$keyCount[$time->key] = $keyCount[$time->key] ?? -1;
$keyCount[$time->key]++;
$singleTimes = array_filter(
array_map(static function (float $startTime, ?float $endTime) {
if ($endTime === null) {
$endTime = microtime(true);
}
return ($endTime - $startTime) * 1000;
}, $time->startTime, $time->stopTime)
);
$totalTime = array_sum($singleTimes);
$count = count($time->startTime);
$key = $time->key;
if ($keyCount[$time->key]) {
$key .= $keyCount[$time->key];
}
if ($count > 1) {
$key .= ' count:' . $count;
}
$timings[] = self::timingString($index, $key, $totalTime);
rsort($singleTimes);
if (count($singleTimes) > 1) {
foreach (array_slice($singleTimes, 0, 3) as $subIndex => $subTime) {
$timings[] = self::timingString($index, $time->key . ' top: ' . ($subIndex + 1), $subTime, $subIndex);
}
}
}
if (count($timings) > 70) {
$timings = [self::timingString(0, 'To Many measurements ' . count($timings), 1.0)];
}
if ($timings) {
header(sprintf('Server-Timing: %s', implode(',', $timings)), false);
}
}

private static function timingString(int $index, string $key, float $duration, ?int $subIndex = null): string
{
$subIndexString = '';
if ($subIndex !== null) {
$subIndexString = '_' . str_pad((string)$subIndex, 3, '0');
}
return sprintf('%02d%s;desc="%s";dur=%0.2f', $index, $subIndexString, $key, $duration);
}
}
35 changes: 35 additions & 0 deletions Classes/XClass/CoreRequestFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

declare(strict_types=1);

namespace Kanti\ServerTiming\XClass;

use GuzzleHttp\Client;
use GuzzleHttp\ClientInterface;
use GuzzleHttp\HandlerStack;
use TYPO3\CMS\Core\Http\RequestFactory;
use TYPO3\CMS\Core\Utility\GeneralUtility;

/**
* only used for TYPO3 v9
* makes the option $GLOBALS['TYPO3_CONF_VARS']['HTTP']['handler'] availalble in v9
* is a default option in >=v10
*/
class CoreRequestFactory extends RequestFactory
{
protected function getClient(): ClientInterface
{
$httpOptions = $GLOBALS['TYPO3_CONF_VARS']['HTTP'];
$httpOptions['verify'] = filter_var($httpOptions['verify'], FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE) ?? $httpOptions['verify'];

if (isset($GLOBALS['TYPO3_CONF_VARS']['HTTP']['handler']) && is_array($GLOBALS['TYPO3_CONF_VARS']['HTTP']['handler'])) {
$stack = HandlerStack::create();
foreach ($GLOBALS['TYPO3_CONF_VARS']['HTTP']['handler'] ?? [] as $name => $handler) {
$stack->push($handler, (string)$name);
}
$httpOptions['handler'] = $stack;
}

return GeneralUtility::makeInstance(Client::class, $httpOptions);
}
}
25 changes: 25 additions & 0 deletions Classes/XClass/ExtbaseDispatcher.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

declare(strict_types=1);

namespace Kanti\ServerTiming\XClass;

use Kanti\ServerTiming\Utility\TimingUtility;
use TYPO3\CMS\Extbase\Mvc\Dispatcher;
use TYPO3\CMS\Extbase\Mvc\RequestInterface;
use TYPO3\CMS\Extbase\Mvc\ResponseInterface;
use TYPO3\CMS\Extbase\Mvc\Web\Request;

class ExtbaseDispatcher extends Dispatcher
{
public function dispatch(RequestInterface $request, ResponseInterface $response)
{
$str = 'extbase ' . str_replace('\\', '_', $request->getControllerObjectName());
if ($request instanceof Request) {
$str .= '->' . $request->getControllerActionName();
}
$stop = TimingUtility::stopWatch($str);
parent::dispatch($request, $response);
$stop();
}
}
38 changes: 38 additions & 0 deletions Configuration/RequestMiddlewares.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

return [
'frontend' => [
'server-timing/first' => [
'target' => \Kanti\ServerTiming\Middleware\FirstMiddleware::class,
'before' => [
'staticfilecache/fallback',
'typo3/cms-frontend/timetracker',
],
],
'server-timing/last' => [
'target' => \Kanti\ServerTiming\Middleware\LastMiddleware::class,
'after' => [
'solr/service/pageexporter',
'typo3/cms-frontend/output-compression',
],
],
],
'backend' => [
'server-timing/first' => [
'target' => \Kanti\ServerTiming\Middleware\FirstMiddleware::class,
'before' => [
'typo3/cms-core/normalized-params-attribute',
'typo3/cms-backend/locked-backend',
],
],
'server-timing/last' => [
'target' => \Kanti\ServerTiming\Middleware\LastMiddleware::class,
'after' => [
'typo3/cms-frontend/output-compression',
'typo3/cms-backend/response-headers',
'typo3/cms-backend/site-resolver',
'typo3/cms-backend/legacy-document-template',
],
],
],
];
Loading

0 comments on commit 53cac8c

Please sign in to comment.