diff options
author | Carl Schwan <carl@carlschwan.eu> | 2022-04-04 13:56:37 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-04-04 13:56:37 +0300 |
commit | 135bdb3d5830672214149fa0b75fadd29b20b844 (patch) | |
tree | f2c1bc68c733829c90090f71d712f78efbc77083 /lib | |
parent | 498d3aea060ed496e2b5351108718e198b021d00 (diff) | |
parent | 7d272c54d013538746d6731097ec37f360effb5d (diff) |
Merge pull request #30823 from nextcloud/work/profiler
Built-in profiler
This adds the required API for collecting information about requests. This information
can then be displayed with the new 'profiler' app.
Diffstat (limited to 'lib')
30 files changed, 1834 insertions, 157 deletions
diff --git a/lib/autoloader.php b/lib/autoloader.php index c8eebee3e0c..a29b9aece79 100644 --- a/lib/autoloader.php +++ b/lib/autoloader.php @@ -38,6 +38,7 @@ namespace OC; use \OCP\AutoloadNotAllowedException; use OCP\ILogger; +use OCP\ICache; class Autoloader { /** @var bool */ @@ -182,9 +183,9 @@ class Autoloader { /** * Sets the optional low-latency cache for class to path mapping. * - * @param \OC\Memcache\Cache $memoryCache Instance of memory cache. + * @param ICache $memoryCache Instance of memory cache. */ - public function setMemoryCache(\OC\Memcache\Cache $memoryCache = null): void { + public function setMemoryCache(ICache $memoryCache = null): void { $this->memoryCache = $memoryCache; } } diff --git a/lib/base.php b/lib/base.php index f3c3e4f31cb..21889272dd7 100644 --- a/lib/base.php +++ b/lib/base.php @@ -608,6 +608,7 @@ class OC { $eventLogger->end('request'); }); $eventLogger->start('boot', 'Initialize'); + $eventLogger->start('runtime', 'Runtime (total - autoloader)'); // Override php.ini and log everything if we're troubleshooting if (self::$config->getValue('loglevel') === ILogger::DEBUG) { diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index 4dbb3cc0d05..2df13618053 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -196,6 +196,8 @@ return array( 'OCP\\Dashboard\\RegisterWidgetEvent' => $baseDir . '/lib/public/Dashboard/RegisterWidgetEvent.php', 'OCP\\Dashboard\\Service\\IEventsService' => $baseDir . '/lib/public/Dashboard/Service/IEventsService.php', 'OCP\\Dashboard\\Service\\IWidgetsService' => $baseDir . '/lib/public/Dashboard/Service/IWidgetsService.php', + 'OCP\\DataCollector\\AbstractDataCollector' => $baseDir . '/lib/public/DataCollector/AbstractDataCollector.php', + 'OCP\\DataCollector\\IDataCollector' => $baseDir . '/lib/public/DataCollector/IDataCollector.php', 'OCP\\Defaults' => $baseDir . '/lib/public/Defaults.php', 'OCP\\Diagnostics\\IEvent' => $baseDir . '/lib/public/Diagnostics/IEvent.php', 'OCP\\Diagnostics\\IEventLogger' => $baseDir . '/lib/public/Diagnostics/IEventLogger.php', @@ -467,6 +469,8 @@ return array( 'OCP\\Preview\\IVersionedPreviewFile' => $baseDir . '/lib/public/Preview/IVersionedPreviewFile.php', 'OCP\\Profile\\ILinkAction' => $baseDir . '/lib/public/Profile/ILinkAction.php', 'OCP\\Profile\\ParameterDoesNotExistException' => $baseDir . '/lib/public/Profile/ParameterDoesNotExistException.php', + 'OCP\\Profiler\\IProfile' => $baseDir . '/lib/public/Profiler/IProfile.php', + 'OCP\\Profiler\\IProfiler' => $baseDir . '/lib/public/Profiler/IProfiler.php', 'OCP\\Remote\\Api\\IApiCollection' => $baseDir . '/lib/public/Remote/Api/IApiCollection.php', 'OCP\\Remote\\Api\\IApiFactory' => $baseDir . '/lib/public/Remote/Api/IApiFactory.php', 'OCP\\Remote\\Api\\ICapabilitiesApi' => $baseDir . '/lib/public/Remote/Api/ICapabilitiesApi.php', @@ -1025,6 +1029,7 @@ return array( 'OC\\DB\\Connection' => $baseDir . '/lib/private/DB/Connection.php', 'OC\\DB\\ConnectionAdapter' => $baseDir . '/lib/private/DB/ConnectionAdapter.php', 'OC\\DB\\ConnectionFactory' => $baseDir . '/lib/private/DB/ConnectionFactory.php', + 'OC\\DB\\DbDataCollector' => $baseDir . '/lib/private/DB/DbDataCollector.php', 'OC\\DB\\Exceptions\\DbalException' => $baseDir . '/lib/private/DB/Exceptions/DbalException.php', 'OC\\DB\\MigrationException' => $baseDir . '/lib/private/DB/MigrationException.php', 'OC\\DB\\MigrationService' => $baseDir . '/lib/private/DB/MigrationService.php', @@ -1035,6 +1040,7 @@ return array( 'OC\\DB\\MySQLMigrator' => $baseDir . '/lib/private/DB/MySQLMigrator.php', 'OC\\DB\\MySqlTools' => $baseDir . '/lib/private/DB/MySqlTools.php', 'OC\\DB\\OCSqlitePlatform' => $baseDir . '/lib/private/DB/OCSqlitePlatform.php', + 'OC\\DB\\ObjectParameter' => $baseDir . '/lib/private/DB/ObjectParameter.php', 'OC\\DB\\OracleConnection' => $baseDir . '/lib/private/DB/OracleConnection.php', 'OC\\DB\\OracleMigrator' => $baseDir . '/lib/private/DB/OracleMigrator.php', 'OC\\DB\\PgSqlTools' => $baseDir . '/lib/private/DB/PgSqlTools.php', @@ -1279,8 +1285,10 @@ return array( 'OC\\Memcache\\CASTrait' => $baseDir . '/lib/private/Memcache/CASTrait.php', 'OC\\Memcache\\Cache' => $baseDir . '/lib/private/Memcache/Cache.php', 'OC\\Memcache\\Factory' => $baseDir . '/lib/private/Memcache/Factory.php', + 'OC\\Memcache\\LoggerWrapperCache' => $baseDir . '/lib/private/Memcache/LoggerWrapperCache.php', 'OC\\Memcache\\Memcached' => $baseDir . '/lib/private/Memcache/Memcached.php', 'OC\\Memcache\\NullCache' => $baseDir . '/lib/private/Memcache/NullCache.php', + 'OC\\Memcache\\ProfilerWrapperCache' => $baseDir . '/lib/private/Memcache/ProfilerWrapperCache.php', 'OC\\Memcache\\Redis' => $baseDir . '/lib/private/Memcache/Redis.php', 'OC\\MemoryInfo' => $baseDir . '/lib/private/MemoryInfo.php', 'OC\\Migration\\BackgroundRepair' => $baseDir . '/lib/private/Migration/BackgroundRepair.php', @@ -1347,6 +1355,10 @@ return array( 'OC\\Profile\\Actions\\WebsiteAction' => $baseDir . '/lib/private/Profile/Actions/WebsiteAction.php', 'OC\\Profile\\ProfileManager' => $baseDir . '/lib/private/Profile/ProfileManager.php', 'OC\\Profile\\TProfileHelper' => $baseDir . '/lib/private/Profile/TProfileHelper.php', + 'OC\\Profiler\\FileProfilerStorage' => $baseDir . '/lib/private/Profiler/FileProfilerStorage.php', + 'OC\\Profiler\\Profile' => $baseDir . '/lib/private/Profiler/Profile.php', + 'OC\\Profiler\\Profiler' => $baseDir . '/lib/private/Profiler/Profiler.php', + 'OC\\Profiler\\RoutingDataCollector' => $baseDir . '/lib/private/Profiler/RoutingDataCollector.php', 'OC\\RedisFactory' => $baseDir . '/lib/private/RedisFactory.php', 'OC\\Remote\\Api\\ApiBase' => $baseDir . '/lib/private/Remote/Api/ApiBase.php', 'OC\\Remote\\Api\\ApiCollection' => $baseDir . '/lib/private/Remote/Api/ApiCollection.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index 0b64f70f6fd..cd5d30b3574 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -225,6 +225,8 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OCP\\Dashboard\\RegisterWidgetEvent' => __DIR__ . '/../../..' . '/lib/public/Dashboard/RegisterWidgetEvent.php', 'OCP\\Dashboard\\Service\\IEventsService' => __DIR__ . '/../../..' . '/lib/public/Dashboard/Service/IEventsService.php', 'OCP\\Dashboard\\Service\\IWidgetsService' => __DIR__ . '/../../..' . '/lib/public/Dashboard/Service/IWidgetsService.php', + 'OCP\\DataCollector\\AbstractDataCollector' => __DIR__ . '/../../..' . '/lib/public/DataCollector/AbstractDataCollector.php', + 'OCP\\DataCollector\\IDataCollector' => __DIR__ . '/../../..' . '/lib/public/DataCollector/IDataCollector.php', 'OCP\\Defaults' => __DIR__ . '/../../..' . '/lib/public/Defaults.php', 'OCP\\Diagnostics\\IEvent' => __DIR__ . '/../../..' . '/lib/public/Diagnostics/IEvent.php', 'OCP\\Diagnostics\\IEventLogger' => __DIR__ . '/../../..' . '/lib/public/Diagnostics/IEventLogger.php', @@ -496,6 +498,8 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OCP\\Preview\\IVersionedPreviewFile' => __DIR__ . '/../../..' . '/lib/public/Preview/IVersionedPreviewFile.php', 'OCP\\Profile\\ILinkAction' => __DIR__ . '/../../..' . '/lib/public/Profile/ILinkAction.php', 'OCP\\Profile\\ParameterDoesNotExistException' => __DIR__ . '/../../..' . '/lib/public/Profile/ParameterDoesNotExistException.php', + 'OCP\\Profiler\\IProfile' => __DIR__ . '/../../..' . '/lib/public/Profiler/IProfile.php', + 'OCP\\Profiler\\IProfiler' => __DIR__ . '/../../..' . '/lib/public/Profiler/IProfiler.php', 'OCP\\Remote\\Api\\IApiCollection' => __DIR__ . '/../../..' . '/lib/public/Remote/Api/IApiCollection.php', 'OCP\\Remote\\Api\\IApiFactory' => __DIR__ . '/../../..' . '/lib/public/Remote/Api/IApiFactory.php', 'OCP\\Remote\\Api\\ICapabilitiesApi' => __DIR__ . '/../../..' . '/lib/public/Remote/Api/ICapabilitiesApi.php', @@ -1054,6 +1058,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OC\\DB\\Connection' => __DIR__ . '/../../..' . '/lib/private/DB/Connection.php', 'OC\\DB\\ConnectionAdapter' => __DIR__ . '/../../..' . '/lib/private/DB/ConnectionAdapter.php', 'OC\\DB\\ConnectionFactory' => __DIR__ . '/../../..' . '/lib/private/DB/ConnectionFactory.php', + 'OC\\DB\\DbDataCollector' => __DIR__ . '/../../..' . '/lib/private/DB/DbDataCollector.php', 'OC\\DB\\Exceptions\\DbalException' => __DIR__ . '/../../..' . '/lib/private/DB/Exceptions/DbalException.php', 'OC\\DB\\MigrationException' => __DIR__ . '/../../..' . '/lib/private/DB/MigrationException.php', 'OC\\DB\\MigrationService' => __DIR__ . '/../../..' . '/lib/private/DB/MigrationService.php', @@ -1064,6 +1069,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OC\\DB\\MySQLMigrator' => __DIR__ . '/../../..' . '/lib/private/DB/MySQLMigrator.php', 'OC\\DB\\MySqlTools' => __DIR__ . '/../../..' . '/lib/private/DB/MySqlTools.php', 'OC\\DB\\OCSqlitePlatform' => __DIR__ . '/../../..' . '/lib/private/DB/OCSqlitePlatform.php', + 'OC\\DB\\ObjectParameter' => __DIR__ . '/../../..' . '/lib/private/DB/ObjectParameter.php', 'OC\\DB\\OracleConnection' => __DIR__ . '/../../..' . '/lib/private/DB/OracleConnection.php', 'OC\\DB\\OracleMigrator' => __DIR__ . '/../../..' . '/lib/private/DB/OracleMigrator.php', 'OC\\DB\\PgSqlTools' => __DIR__ . '/../../..' . '/lib/private/DB/PgSqlTools.php', @@ -1308,8 +1314,10 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OC\\Memcache\\CASTrait' => __DIR__ . '/../../..' . '/lib/private/Memcache/CASTrait.php', 'OC\\Memcache\\Cache' => __DIR__ . '/../../..' . '/lib/private/Memcache/Cache.php', 'OC\\Memcache\\Factory' => __DIR__ . '/../../..' . '/lib/private/Memcache/Factory.php', + 'OC\\Memcache\\LoggerWrapperCache' => __DIR__ . '/../../..' . '/lib/private/Memcache/LoggerWrapperCache.php', 'OC\\Memcache\\Memcached' => __DIR__ . '/../../..' . '/lib/private/Memcache/Memcached.php', 'OC\\Memcache\\NullCache' => __DIR__ . '/../../..' . '/lib/private/Memcache/NullCache.php', + 'OC\\Memcache\\ProfilerWrapperCache' => __DIR__ . '/../../..' . '/lib/private/Memcache/ProfilerWrapperCache.php', 'OC\\Memcache\\Redis' => __DIR__ . '/../../..' . '/lib/private/Memcache/Redis.php', 'OC\\MemoryInfo' => __DIR__ . '/../../..' . '/lib/private/MemoryInfo.php', 'OC\\Migration\\BackgroundRepair' => __DIR__ . '/../../..' . '/lib/private/Migration/BackgroundRepair.php', @@ -1376,6 +1384,10 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OC\\Profile\\Actions\\WebsiteAction' => __DIR__ . '/../../..' . '/lib/private/Profile/Actions/WebsiteAction.php', 'OC\\Profile\\ProfileManager' => __DIR__ . '/../../..' . '/lib/private/Profile/ProfileManager.php', 'OC\\Profile\\TProfileHelper' => __DIR__ . '/../../..' . '/lib/private/Profile/TProfileHelper.php', + 'OC\\Profiler\\FileProfilerStorage' => __DIR__ . '/../../..' . '/lib/private/Profiler/FileProfilerStorage.php', + 'OC\\Profiler\\Profile' => __DIR__ . '/../../..' . '/lib/private/Profiler/Profile.php', + 'OC\\Profiler\\Profiler' => __DIR__ . '/../../..' . '/lib/private/Profiler/Profiler.php', + 'OC\\Profiler\\RoutingDataCollector' => __DIR__ . '/../../..' . '/lib/private/Profiler/RoutingDataCollector.php', 'OC\\RedisFactory' => __DIR__ . '/../../..' . '/lib/private/RedisFactory.php', 'OC\\Remote\\Api\\ApiBase' => __DIR__ . '/../../..' . '/lib/private/Remote/Api/ApiBase.php', 'OC\\Remote\\Api\\ApiCollection' => __DIR__ . '/../../..' . '/lib/private/Remote/Api/ApiCollection.php', diff --git a/lib/private/AppFramework/App.php b/lib/private/AppFramework/App.php index 6c2f905afa5..feebb32d5bc 100644 --- a/lib/private/AppFramework/App.php +++ b/lib/private/AppFramework/App.php @@ -34,11 +34,16 @@ namespace OC\AppFramework; use OC\AppFramework\DependencyInjection\DIContainer; use OC\AppFramework\Http\Dispatcher; use OC\AppFramework\Http\Request; +use OC\Diagnostics\EventLogger; +use OCP\Profiler\IProfiler; +use OC\Profiler\RoutingDataCollector; +use OCP\AppFramework\QueryException; use OCP\AppFramework\Http; use OCP\AppFramework\Http\ICallbackResponse; use OCP\AppFramework\Http\IOutput; -use OCP\AppFramework\QueryException; +use OCP\Diagnostics\IEventLogger; use OCP\HintException; +use OCP\IConfig; use OCP\IRequest; /** @@ -114,20 +119,30 @@ class App { * @throws HintException */ public static function main(string $controllerName, string $methodName, DIContainer $container, array $urlParams = null) { + /** @var IProfiler $profiler */ + $profiler = $container->get(IProfiler::class); + $config = $container->get(IConfig::class); + // Disable profiler on the profiler UI + $profiler->setEnabled($profiler->isEnabled() && !is_null($urlParams) && isset($urlParams['_route']) && !str_starts_with($urlParams['_route'], 'profiler.')); + if ($profiler->isEnabled()) { + \OC::$server->get(IEventLogger::class)->activate(); + $profiler->add(new RoutingDataCollector($container['AppName'], $controllerName, $methodName)); + } + if (!is_null($urlParams)) { /** @var Request $request */ - $request = $container->query(IRequest::class); + $request = $container->get(IRequest::class); $request->setUrlParameters($urlParams); } elseif (isset($container['urlParams']) && !is_null($container['urlParams'])) { /** @var Request $request */ - $request = $container->query(IRequest::class); + $request = $container->get(IRequest::class); $request->setUrlParameters($container['urlParams']); } $appName = $container['AppName']; // first try $controllerName then go for \OCA\AppName\Controller\$controllerName try { - $controller = $container->query($controllerName); + $controller = $container->get($controllerName); } catch (QueryException $e) { if (strpos($controllerName, '\\Controller\\') !== false) { // This is from a global registered app route that is not enabled. @@ -158,6 +173,16 @@ class App { $io = $container[IOutput::class]; + if ($profiler->isEnabled()) { + /** @var EventLogger $eventLogger */ + $eventLogger = $container->get(IEventLogger::class); + $eventLogger->end('runtime'); + $profile = $profiler->collect($container->get(IRequest::class), $response); + $profiler->saveProfile($profile); + $io->setHeader('X-Debug-Token:' . $profile->getToken()); + $io->setHeader('Server-Timing: token;desc="' . $profile->getToken() . '"'); + } + if (!is_null($httpHeaders)) { $io->setHeader($httpHeaders); } diff --git a/lib/private/AppFramework/Utility/SimpleContainer.php b/lib/private/AppFramework/Utility/SimpleContainer.php index 598c66b6aba..429382aa223 100644 --- a/lib/private/AppFramework/Utility/SimpleContainer.php +++ b/lib/private/AppFramework/Utility/SimpleContainer.php @@ -38,6 +38,7 @@ use Psr\Container\ContainerInterface; use ReflectionClass; use ReflectionException; use ReflectionParameter; +use ReflectionNamedType; use function class_exists; /** @@ -78,12 +79,13 @@ class SimpleContainer implements ArrayAccess, ContainerInterface, IContainer { $resolveName = $parameter->getName(); // try to find out if it is a class or a simple parameter - if ($parameterType !== null && !$parameterType->isBuiltin()) { + if ($parameterType !== null && ($parameterType instanceof ReflectionNamedType) && !$parameterType->isBuiltin()) { $resolveName = $parameterType->getName(); } try { - $builtIn = $parameter->hasType() && $parameter->getType()->isBuiltin(); + $builtIn = $parameter->hasType() && ($parameter->getType() instanceof ReflectionNamedType) + && $parameter->getType()->isBuiltin(); return $this->query($resolveName, !$builtIn); } catch (QueryException $e) { // Service not found, use the default value when available @@ -91,7 +93,7 @@ class SimpleContainer implements ArrayAccess, ContainerInterface, IContainer { return $parameter->getDefaultValue(); } - if ($parameterType !== null && !$parameterType->isBuiltin()) { + if ($parameterType !== null && ($parameterType instanceof ReflectionNamedType) && !$parameterType->isBuiltin()) { $resolveName = $parameter->getName(); try { return $this->query($resolveName); diff --git a/lib/private/Cache/CappedMemoryCache.php b/lib/private/Cache/CappedMemoryCache.php index 9260bf1f6b3..0a3300435eb 100644 --- a/lib/private/Cache/CappedMemoryCache.php +++ b/lib/private/Cache/CappedMemoryCache.php @@ -115,4 +115,8 @@ class CappedMemoryCache implements ICache, \ArrayAccess { $this->remove($key); } } + + public static function isAvailable(): bool { + return true; + } } diff --git a/lib/private/Cache/File.php b/lib/private/Cache/File.php index 0ecd894d2d2..a96a7cd9c0b 100644 --- a/lib/private/Cache/File.php +++ b/lib/private/Cache/File.php @@ -203,4 +203,8 @@ class File implements ICache { } } } + + public static function isAvailable(): bool { + return true; + } } diff --git a/lib/private/DB/Connection.php b/lib/private/DB/Connection.php index 0cd310550b6..2e38b1ddf5e 100644 --- a/lib/private/DB/Connection.php +++ b/lib/private/DB/Connection.php @@ -42,6 +42,7 @@ use Doctrine\DBAL\Driver; use Doctrine\DBAL\Exception; use Doctrine\DBAL\Exception\ConstraintViolationException; use Doctrine\DBAL\Exception\NotNullConstraintViolationException; +use Doctrine\DBAL\Logging\DebugStack; use Doctrine\DBAL\Platforms\MySQLPlatform; use Doctrine\DBAL\Platforms\OraclePlatform; use Doctrine\DBAL\Platforms\PostgreSQL94Platform; @@ -55,6 +56,7 @@ use OCP\PreConditionNotMetException; use OC\DB\QueryBuilder\QueryBuilder; use OC\SystemConfig; use Psr\Log\LoggerInterface; +use OCP\Profiler\IProfiler; class Connection extends \Doctrine\DBAL\Connection { /** @var string */ @@ -76,6 +78,9 @@ class Connection extends \Doctrine\DBAL\Connection { /** @var int */ protected $queriesExecuted = 0; + /** @var DbDataCollector|null */ + protected $dbDataCollector = null; + /** * Initializes a new instance of the Connection class. * @@ -102,6 +107,16 @@ class Connection extends \Doctrine\DBAL\Connection { $this->systemConfig = \OC::$server->getSystemConfig(); $this->logger = \OC::$server->get(LoggerInterface::class); + + /** @var \OCP\Profiler\IProfiler */ + $profiler = \OC::$server->get(IProfiler::class); + if ($profiler->isEnabled()) { + $this->dbDataCollector = new DbDataCollector($this); + $profiler->add($this->dbDataCollector); + $debugStack = new DebugStack(); + $this->dbDataCollector->setDebugStack($debugStack); + $this->_config->setSQLLogger($debugStack); + } } /** diff --git a/lib/private/DB/DbDataCollector.php b/lib/private/DB/DbDataCollector.php new file mode 100644 index 00000000000..d708955b10e --- /dev/null +++ b/lib/private/DB/DbDataCollector.php @@ -0,0 +1,154 @@ +<?php + +declare(strict_types = 1); +/** + * @copyright 2022 Carl Schwan <carl@carlschwan.eu> + * + * @author Carl Schwan <carl@carlschwan.eu> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OC\DB; + +use Doctrine\DBAL\Logging\DebugStack; +use Doctrine\DBAL\Types\ConversionException; +use Doctrine\DBAL\Types\Type; +use OC\AppFramework\Http\Request; +use OCP\AppFramework\Http\Response; + +class DbDataCollector extends \OCP\DataCollector\AbstractDataCollector { + protected ?DebugStack $debugStack = null; + private Connection $connection; + + /** + * DbDataCollector constructor. + */ + public function __construct(Connection $connection) { + $this->connection = $connection; + } + + public function setDebugStack(DebugStack $debugStack, $name = 'default'): void { + $this->debugStack = $debugStack; + } + + /** + * @inheritDoc + */ + public function collect(Request $request, Response $response, \Throwable $exception = null): void { + $queries = $this->sanitizeQueries($this->debugStack->queries); + + $this->data = [ + 'queries' => $queries, + ]; + } + + public function getName(): string { + return 'db'; + } + + public function getQueries(): array { + return $this->data['queries']; + } + + private function sanitizeQueries(array $queries): array { + foreach ($queries as $i => $query) { + $queries[$i] = $this->sanitizeQuery($query); + } + + return $queries; + } + + private function sanitizeQuery(array $query): array { + $query['explainable'] = true; + $query['runnable'] = true; + if (null === $query['params']) { + $query['params'] = []; + } + if (!\is_array($query['params'])) { + $query['params'] = [$query['params']]; + } + if (!\is_array($query['types'])) { + $query['types'] = []; + } + foreach ($query['params'] as $j => $param) { + $e = null; + if (isset($query['types'][$j])) { + // Transform the param according to the type + $type = $query['types'][$j]; + if (\is_string($type)) { + $type = Type::getType($type); + } + if ($type instanceof Type) { + $query['types'][$j] = $type->getBindingType(); + try { + $param = $type->convertToDatabaseValue($param, $this->connection->getDatabasePlatform()); + } catch (\TypeError $e) { + } catch (ConversionException $e) { + } + } + } + + [$query['params'][$j], $explainable, $runnable] = $this->sanitizeParam($param, $e); + if (!$explainable) { + $query['explainable'] = false; + } + + if (!$runnable) { + $query['runnable'] = false; + } + } + + return $query; + } + + /** + * Sanitizes a param. + * + * The return value is an array with the sanitized value and a boolean + * indicating if the original value was kept (allowing to use the sanitized + * value to explain the query). + */ + private function sanitizeParam($var, ?\Throwable $error): array { + if (\is_object($var)) { + return [$o = new ObjectParameter($var, $error), false, $o->isStringable() && !$error]; + } + + if ($error) { + return ['⚠'.$error->getMessage(), false, false]; + } + + if (\is_array($var)) { + $a = []; + $explainable = $runnable = true; + foreach ($var as $k => $v) { + [$value, $e, $r] = $this->sanitizeParam($v, null); + $explainable = $explainable && $e; + $runnable = $runnable && $r; + $a[$k] = $value; + } + + return [$a, $explainable, $runnable]; + } + + if (\is_resource($var)) { + return [sprintf('/* Resource(%s) */', get_resource_type($var)), false, false]; + } + + return [$var, true, true]; + } +} diff --git a/lib/private/DB/ObjectParameter.php b/lib/private/DB/ObjectParameter.php new file mode 100644 index 00000000000..61ac16018d8 --- /dev/null +++ b/lib/private/DB/ObjectParameter.php @@ -0,0 +1,71 @@ +<?php + +declare(strict_types = 1); + +/* + * This file is part of the Symfony package. + * + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +/** + * @copyright 2022 Carl Schwan <carl@carlschwan.eu> + * + * @author Carl Schwan <carl@carlschwan.eu> + * @author Fabien Potencier <fabien@symfony.com> + * + * @license AGPL-3.0-or-later AND MIT + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OC\DB; + +final class ObjectParameter { + private $object; + private $error; + private $stringable; + private $class; + + /** + * @param object $object + */ + public function __construct($object, ?\Throwable $error) { + $this->object = $object; + $this->error = $error; + $this->stringable = \is_callable([$object, '__toString']); + $this->class = \get_class($object); + } + + /** + * @return object + */ + public function getObject() { + return $this->object; + } + + public function getError(): ?\Throwable { + return $this->error; + } + + public function isStringable(): bool { + return $this->stringable; + } + + public function getClass(): string { + return $this->class; + } +} diff --git a/lib/private/Diagnostics/EventLogger.php b/lib/private/Diagnostics/EventLogger.php index 35cef0be3f5..c7b89002ea9 100644 --- a/lib/private/Diagnostics/EventLogger.php +++ b/lib/private/Diagnostics/EventLogger.php @@ -60,7 +60,8 @@ class EventLogger implements IEventLogger { } public function isLoggingActivated(): bool { - $systemValue = (bool)$this->config->getValue('diagnostics.logging', false); + $systemValue = (bool)$this->config->getValue('diagnostics.logging', false) + || (bool)$this->config->getValue('profiler', false); if ($systemValue && $this->config->getValue('debug', false)) { return true; diff --git a/lib/private/Memcache/APCu.php b/lib/private/Memcache/APCu.php index 56345890bf2..f0eb98b9db2 100644 --- a/lib/private/Memcache/APCu.php +++ b/lib/private/Memcache/APCu.php @@ -148,10 +148,7 @@ class APCu extends Cache implements IMemcache { } } - /** - * @return bool - */ - public static function isAvailable() { + public static function isAvailable(): bool { if (!extension_loaded('apcu')) { return false; } elseif (!\OC::$server->get(IniGetWrapper::class)->getBool('apc.enabled')) { diff --git a/lib/private/Memcache/ArrayCache.php b/lib/private/Memcache/ArrayCache.php index b89aff0b7ed..13597a068b3 100644 --- a/lib/private/Memcache/ArrayCache.php +++ b/lib/private/Memcache/ArrayCache.php @@ -153,7 +153,7 @@ class ArrayCache extends Cache implements IMemcache { /** * {@inheritDoc} */ - public static function isAvailable() { + public static function isAvailable(): bool { return true; } } diff --git a/lib/private/Memcache/Factory.php b/lib/private/Memcache/Factory.php index 73206aac011..604f764c03c 100644 --- a/lib/private/Memcache/Factory.php +++ b/lib/private/Memcache/Factory.php @@ -31,6 +31,7 @@ */ namespace OC\Memcache; +use OCP\Profiler\IProfiler; use OCP\ICache; use OCP\ICacheFactory; use OCP\IMemcache; @@ -39,39 +40,39 @@ use Psr\Log\LoggerInterface; class Factory implements ICacheFactory { public const NULL_CACHE = NullCache::class; - /** - * @var string $globalPrefix - */ - private $globalPrefix; + private string $globalPrefix; private LoggerInterface $logger; /** - * @var string $localCacheClass + * @var ?class-string<ICache> $localCacheClass + */ + private ?string $localCacheClass; + + /** + * @var ?class-string<ICache> $distributedCacheClass */ - private $localCacheClass; + private ?string $distributedCacheClass; /** - * @var string $distributedCacheClass + * @var ?class-string<IMemcache> $lockingCacheClass */ - private $distributedCacheClass; + private ?string $lockingCacheClass; + + private string $logFile; + + private IProfiler $profiler; /** - * @var string $lockingCacheClass + * @param string $globalPrefix + * @param LoggerInterface $logger + * @param ?class-string<ICache> $localCacheClass + * @param ?class-string<ICache> $distributedCacheClass + * @param ?class-string<IMemcache> $lockingCacheClass + * @param string $logFile */ - private $lockingCacheClass; - - /** @var string */ - private $logFile; - - public function __construct( - string $globalPrefix, - LoggerInterface $logger, - ?string $localCacheClass = null, - ?string $distributedCacheClass = null, - ?string $lockingCacheClass = null, - string $logFile = '' - ) { + public function __construct(string $globalPrefix, LoggerInterface $logger, IProfiler $profiler, + ?string $localCacheClass = null, ?string $distributedCacheClass = null, ?string $lockingCacheClass = null, string $logFile = '') { $this->logger = $logger; $this->logFile = $logFile; $this->globalPrefix = $globalPrefix; @@ -103,6 +104,7 @@ class Factory implements ICacheFactory { $this->localCacheClass = $localCacheClass; $this->distributedCacheClass = $distributedCacheClass; $this->lockingCacheClass = $lockingCacheClass; + $this->profiler = $profiler; } /** @@ -112,7 +114,19 @@ class Factory implements ICacheFactory { * @return IMemcache */ public function createLocking(string $prefix = ''): IMemcache { - return new $this->lockingCacheClass($this->globalPrefix . '/' . $prefix, $this->logFile); + assert($this->lockingCacheClass !== null); + $cache = new $this->lockingCacheClass($this->globalPrefix . '/' . $prefix); + if ($this->profiler->isEnabled() && $this->lockingCacheClass === '\OC\Memcache\Redis') { + // We only support the profiler with Redis + $cache = new ProfilerWrapperCache($cache, 'Locking'); + $this->profiler->add($cache); + } + + if ($this->lockingCacheClass === Redis::class && + $this->logFile !== '' && is_writable(dirname($this->logFile)) && (!file_exists($this->logFile) || is_writable($this->logFile))) { + $cache = new LoggerWrapperCache($cache, $this->logFile); + } + return $cache; } /** @@ -122,7 +136,19 @@ class Factory implements ICacheFactory { * @return ICache */ public function createDistributed(string $prefix = ''): ICache { - return new $this->distributedCacheClass($this->globalPrefix . '/' . $prefix, $this->logFile); + assert($this->distributedCacheClass !== null); + $cache = new $this->distributedCacheClass($this->globalPrefix . '/' . $prefix); + if ($this->profiler->isEnabled() && $this->distributedCacheClass === '\OC\Memcache\Redis') { + // We only support the profiler with Redis + $cache = new ProfilerWrapperCache($cache, 'Distributed'); + $this->profiler->add($cache); + } + + if ($this->distributedCacheClass === Redis::class && $this->logFile !== '' + && is_writable(dirname($this->logFile)) && (!file_exists($this->logFile) || is_writable($this->logFile))) { + $cache = new LoggerWrapperCache($cache, $this->logFile); + } + return $cache; } /** @@ -132,7 +158,19 @@ class Factory implements ICacheFactory { * @return ICache */ public function createLocal(string $prefix = ''): ICache { - return new $this->localCacheClass($this->globalPrefix . '/' . $prefix, $this->logFile); + assert($this->localCacheClass !== null); + $cache = new $this->localCacheClass($this->globalPrefix . '/' . $prefix); + if ($this->profiler->isEnabled() && $this->localCacheClass === '\OC\Memcache\Redis') { + // We only support the profiler with Redis + $cache = new ProfilerWrapperCache($cache, 'Local'); + $this->profiler->add($cache); + } + + if ($this->localCacheClass === Redis::class && $this->logFile !== '' + && is_writable(dirname($this->logFile)) && (!file_exists($this->logFile) || is_writable($this->logFile))) { + $cache = new LoggerWrapperCache($cache, $this->logFile); + } + return $cache; } /** diff --git a/lib/private/Memcache/LoggerWrapperCache.php b/lib/private/Memcache/LoggerWrapperCache.php new file mode 100644 index 00000000000..55c0e76db79 --- /dev/null +++ b/lib/private/Memcache/LoggerWrapperCache.php @@ -0,0 +1,177 @@ +<?php + +declare(strict_types = 1); +/** + * @copyright 2022 Carl Schwan <carl@carlschwan.eu> + * + * @author Carl Schwan <carl@carlschwan.eu> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OC\Memcache; + +use OCP\IMemcacheTTL; + +/** + * Cache wrapper that logs the cache operation in a log file + */ +class LoggerWrapperCache extends Cache implements IMemcacheTTL { + /** @var Redis */ + protected $wrappedCache; + + /** @var string $logFile */ + private $logFile; + + /** @var string $prefix */ + protected $prefix; + + public function __construct(Redis $wrappedCache, string $logFile) { + parent::__construct($wrappedCache->getPrefix()); + $this->wrappedCache = $wrappedCache; + $this->logFile = $logFile; + } + + /** + * @return string Prefix used for caching purposes + */ + public function getPrefix() { + return $this->prefix; + } + + protected function getNameSpace() { + return $this->prefix; + } + + /** @inheritDoc */ + public function get($key) { + file_put_contents( + $this->logFile, + $this->getNameSpace() . '::get::' . $key . "\n", + FILE_APPEND + ); + return $this->wrappedCache->get($key); + } + + /** @inheritDoc */ + public function set($key, $value, $ttl = 0) { + file_put_contents( + $this->logFile, + $this->getNameSpace() . '::set::' . $key . '::' . $ttl . '::' . json_encode($value) . "\n", + FILE_APPEND + ); + + return $this->wrappedCache->set($key, $value, $$ttl); + } + + /** @inheritDoc */ + public function hasKey($key) { + file_put_contents( + $this->logFile, + $this->getNameSpace() . '::hasKey::' . $key . "\n", + FILE_APPEND + ); + + return $this->wrappedCache->hasKey($key); + } + + /** @inheritDoc */ + public function remove($key) { + file_put_contents( + $this->logFile, + $this->getNameSpace() . '::remove::' . $key . "\n", + FILE_APPEND + ); + + return $this->wrappedCache->remove($key); + } + + /** @inheritDoc */ + public function clear($prefix = '') { + file_put_contents( + $this->logFile, + $this->getNameSpace() . '::clear::' . $prefix . "\n", + FILE_APPEND + ); + + return $this->wrappedCache->clear($prefix); + } + + /** @inheritDoc */ + public function add($key, $value, $ttl = 0) { + file_put_contents( + $this->logFile, + $this->getNameSpace() . '::add::' . $key . '::' . $value . "\n", + FILE_APPEND + ); + + return $this->wrappedCache->add($key, $value, $ttl); + } + + /** @inheritDoc */ + public function inc($key, $step = 1) { + file_put_contents( + $this->logFile, + $this->getNameSpace() . '::inc::' . $key . "\n", + FILE_APPEND + ); + + return $this->wrappedCache->inc($key, $step); + } + + /** @inheritDoc */ + public function dec($key, $step = 1) { + file_put_contents( + $this->logFile, + $this->getNameSpace() . '::dec::' . $key . "\n", + FILE_APPEND + ); + + return $this->wrappedCache->dec($key, $step); + } + + /** @inheritDoc */ + public function cas($key, $old, $new) { + file_put_contents( + $this->logFile, + $this->getNameSpace() . '::cas::' . $key . "\n", + FILE_APPEND + ); + + return $this->wrappedCache->cas($key, $old, $new); + } + + /** @inheritDoc */ + public function cad($key, $old) { + file_put_contents( + $this->logFile, + $this->getNameSpace() . '::cad::' . $key . "\n", + FILE_APPEND + ); + + return $this->wrappedCache->cad($key, $old); + } + + /** @inheritDoc */ + public function setTTL($key, $ttl) { + $this->wrappedCache->setTTL($key, $ttl); + } + + public static function isAvailable(): bool { + return true; + } +} diff --git a/lib/private/Memcache/Memcached.php b/lib/private/Memcache/Memcached.php index f78be581d63..db4aa7ba9cc 100644 --- a/lib/private/Memcache/Memcached.php +++ b/lib/private/Memcache/Memcached.php @@ -196,7 +196,7 @@ class Memcached extends Cache implements IMemcache { return $result; } - public static function isAvailable() { + public static function isAvailable(): bool { return extension_loaded('memcached'); } diff --git a/lib/private/Memcache/NullCache.php b/lib/private/Memcache/NullCache.php index 7b56ec932f4..fc41595dfe1 100644 --- a/lib/private/Memcache/NullCache.php +++ b/lib/private/Memcache/NullCache.php @@ -67,7 +67,7 @@ class NullCache extends Cache implements \OCP\IMemcache { return true; } - public static function isAvailable() { + public static function isAvailable(): bool { return true; } } diff --git a/lib/private/Memcache/ProfilerWrapperCache.php b/lib/private/Memcache/ProfilerWrapperCache.php new file mode 100644 index 00000000000..8e9b160ba0e --- /dev/null +++ b/lib/private/Memcache/ProfilerWrapperCache.php @@ -0,0 +1,220 @@ +<?php + +declare(strict_types = 1); +/** + * @copyright 2022 Carl Schwan <carl@carlschwan.eu> + * + * @author Carl Schwan <carl@carlschwan.eu> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OC\Memcache; + +use OC\AppFramework\Http\Request; +use OCP\AppFramework\Http\Response; +use OCP\DataCollector\AbstractDataCollector; +use OCP\IMemcacheTTL; + +/** + * Cache wrapper that logs profiling information + */ +class ProfilerWrapperCache extends AbstractDataCollector implements IMemcacheTTL, \ArrayAccess { + /** @var Redis $wrappedCache*/ + protected $wrappedCache; + + /** @var string $prefix */ + protected $prefix; + + /** @var string $type */ + private $type; + + public function __construct(Redis $wrappedCache, string $type) { + $this->prefix = $wrappedCache->getPrefix(); + $this->wrappedCache = $wrappedCache; + $this->type = $type; + $this->data['queries'] = []; + $this->data['cacheHit'] = 0; + $this->data['cacheMiss'] = 0; + } + + public function getPrefix(): string { + return $this->prefix; + } + + /** @inheritDoc */ + public function get($key) { + $start = microtime(true); + $ret = $this->wrappedCache->get($key); + if ($ret === null) { + $this->data['cacheMiss']++; + } else { + $this->data['cacheHit']++; + } + $this->data['queries'][] = [ + 'start' => $start, + 'end' => microtime(true), + 'op' => $this->getPrefix() . '::get::' . $key, + ]; + return $ret; + } + + /** @inheritDoc */ + public function set($key, $value, $ttl = 0) { + $start = microtime(true); + $ret = $this->wrappedCache->set($key, $value, $ttl); + $this->data['queries'][] = [ + 'start' => $start, + 'end' => microtime(true), + 'op' => $this->getPrefix() . '::set::' . $key, + ]; + return $ret; + } + + /** @inheritDoc */ + public function hasKey($key) { + $start = microtime(true); + $ret = $this->wrappedCache->hasKey($key); + $this->data['queries'][] = [ + 'start' => $start, + 'end' => microtime(true), + 'op' => $this->getPrefix() . '::hasKey::' . $key, + ]; + return $ret; + } + + /** @inheritDoc */ + public function remove($key) { + $start = microtime(true); + $ret = $this->wrappedCache->remove($key); + $this->data['queries'][] = [ + 'start' => $start, + 'end' => microtime(true), + 'op' => $this->getPrefix() . '::remove::' . $key, + ]; + return $ret; + } + + /** @inheritDoc */ + public function clear($prefix = '') { + $start = microtime(true); + $ret = $this->wrappedCache->clear($prefix); + $this->data['queries'][] = [ + 'start' => $start, + 'end' => microtime(true), + 'op' => $this->getPrefix() . '::clear::' . $prefix, + ]; + return $ret; + } + + /** @inheritDoc */ + public function add($key, $value, $ttl = 0) { + $start = microtime(true); + $ret = $this->wrappedCache->add($key, $value, $ttl); + $this->data['queries'][] = [ + 'start' => $start, + 'end' => microtime(true), + 'op' => $this->getPrefix() . '::add::' . $key, + ]; + return $ret; + } + + /** @inheritDoc */ + public function inc($key, $step = 1) { + $start = microtime(true); + $ret = $this->wrappedCache->inc($key, $step); + $this->data['queries'][] = [ + 'start' => $start, + 'end' => microtime(true), + 'op' => $this->getPrefix() . '::inc::' . $key, + ]; + return $ret; + } + + /** @inheritDoc */ + public function dec($key, $step = 1) { + $start = microtime(true); + $ret = $this->wrappedCache->dec($key, $step); + $this->data['queries'][] = [ + 'start' => $start, + 'end' => microtime(true), + 'op' => $this->getPrefix() . '::dev::' . $key, + ]; + return $ret; + } + + /** @inheritDoc */ + public function cas($key, $old, $new) { + $start = microtime(true); + $ret = $this->wrappedCache->cas($key, $old, $new); + $this->data['queries'][] = [ + 'start' => $start, + 'end' => microtime(true), + 'op' => $this->getPrefix() . '::cas::' . $key, + ]; + return $ret; + } + + /** @inheritDoc */ + public function cad($key, $old) { + $start = microtime(true); + $ret = $this->wrappedCache->cad($key, $old); + $this->data['queries'][] = [ + 'start' => $start, + 'end' => microtime(true), + 'op' => $this->getPrefix() . '::cad::' . $key, + ]; + return $ret; + } + + /** @inheritDoc */ + public function setTTL($key, $ttl) { + $this->wrappedCache->setTTL($key, $ttl); + } + + public function offsetExists($offset): bool { + return $this->hasKey($offset); + } + + public function offsetSet($offset, $value): void { + $this->set($offset, $value); + } + + /** + * @return mixed + */ + #[\ReturnTypeWillChange] + public function offsetGet($offset) { + return $this->get($offset); + } + + public function offsetUnset($offset): void { + $this->remove($offset); + } + + public function collect(Request $request, Response $response, \Throwable $exception = null): void { + // Nothing to do here $data is already ready + } + + public function getName(): string { + return 'cache/' . $this->type . '/' . $this->prefix; + } + + public static function isAvailable(): bool { + return true; + } +} diff --git a/lib/private/Memcache/Redis.php b/lib/private/Memcache/Redis.php index 63180dd8066..9b07da2d99c 100644 --- a/lib/private/Memcache/Redis.php +++ b/lib/private/Memcache/Redis.php @@ -37,38 +37,16 @@ class Redis extends Cache implements IMemcacheTTL { */ private static $cache = null; - private $logFile; - public function __construct($prefix = '', string $logFile = '') { parent::__construct($prefix); - $this->logFile = $logFile; if (is_null(self::$cache)) { self::$cache = \OC::$server->getGetRedisFactory()->getInstance(); } } - /** - * entries in redis get namespaced to prevent collisions between ownCloud instances and users - */ - protected function getNameSpace() { - return $this->prefix; - } - - private function logEnabled(): bool { - return $this->logFile !== '' && is_writable(dirname($this->logFile)) && (!file_exists($this->logFile) || is_writable($this->logFile)); - } - public function get($key) { - if ($this->logEnabled()) { - file_put_contents( - $this->logFile, - $this->getNameSpace() . '::get::' . $key . "\n", - FILE_APPEND - ); - } - - $result = self::$cache->get($this->getNameSpace() . $key); - if ($result === false && !self::$cache->exists($this->getNameSpace() . $key)) { + $result = self::$cache->get($this->getPrefix() . $key); + if ($result === false && !self::$cache->exists($this->getPrefix() . $key)) { return null; } else { return json_decode($result, true); @@ -76,43 +54,19 @@ class Redis extends Cache implements IMemcacheTTL { } public function set($key, $value, $ttl = 0) { - if ($this->logEnabled()) { - file_put_contents( - $this->logFile, - $this->getNameSpace() . '::set::' . $key . '::' . $ttl . '::' . json_encode($value) . "\n", - FILE_APPEND - ); - } - if ($ttl > 0) { - return self::$cache->setex($this->getNameSpace() . $key, $ttl, json_encode($value)); + return self::$cache->setex($this->getPrefix() . $key, $ttl, json_encode($value)); } else { - return self::$cache->set($this->getNameSpace() . $key, json_encode($value)); + return self::$cache->set($this->getPrefix() . $key, json_encode($value)); } } public function hasKey($key) { - if ($this->logEnabled()) { - file_put_contents( - $this->logFile, - $this->getNameSpace() . '::hasKey::' . $key . "\n", - FILE_APPEND - ); - } - - return (bool)self::$cache->exists($this->getNameSpace() . $key); + return (bool)self::$cache->exists($this->getPrefix() . $key); } public function remove($key) { - if ($this->logEnabled()) { - file_put_contents( - $this->logFile, - $this->getNameSpace() . '::remove::' . $key . "\n", - FILE_APPEND - ); - } - - if (self::$cache->del($this->getNameSpace() . $key)) { + if (self::$cache->del($this->getPrefix() . $key)) { return true; } else { return false; @@ -120,15 +74,7 @@ class Redis extends Cache implements IMemcacheTTL { } public function clear($prefix = '') { - if ($this->logEnabled()) { - file_put_contents( - $this->logFile, - $this->getNameSpace() . '::clear::' . $prefix . "\n", - FILE_APPEND - ); - } - - $prefix = $this->getNameSpace() . $prefix . '*'; + $prefix = $this->getPrefix() . $prefix . '*'; $keys = self::$cache->keys($prefix); $deleted = self::$cache->del($keys); @@ -153,14 +99,6 @@ class Redis extends Cache implements IMemcacheTTL { if ($ttl !== 0 && is_int($ttl)) { $args['ex'] = $ttl; } - if ($this->logEnabled()) { - file_put_contents( - $this->logFile, - $this->getNameSpace() . '::add::' . $key . '::' . $value . "\n", - FILE_APPEND - ); - } - return self::$cache->set($this->getPrefix() . $key, $value, $args); } @@ -173,15 +111,7 @@ class Redis extends Cache implements IMemcacheTTL { * @return int | bool */ public function inc($key, $step = 1) { - if ($this->logEnabled()) { - file_put_contents( - $this->logFile, - $this->getNameSpace() . '::inc::' . $key . "\n", - FILE_APPEND - ); - } - - return self::$cache->incrBy($this->getNameSpace() . $key, $step); + return self::$cache->incrBy($this->getPrefix() . $key, $step); } /** @@ -192,18 +122,10 @@ class Redis extends Cache implements IMemcacheTTL { * @return int | bool */ public function dec($key, $step = 1) { - if ($this->logEnabled()) { - file_put_contents( - $this->logFile, - $this->getNameSpace() . '::dec::' . $key . "\n", - FILE_APPEND - ); - } - if (!$this->hasKey($key)) { return false; } - return self::$cache->decrBy($this->getNameSpace() . $key, $step); + return self::$cache->decrBy($this->getPrefix() . $key, $step); } /** @@ -215,21 +137,13 @@ class Redis extends Cache implements IMemcacheTTL { * @return bool */ public function cas($key, $old, $new) { - if ($this->logEnabled()) { - file_put_contents( - $this->logFile, - $this->getNameSpace() . '::cas::' . $key . "\n", - FILE_APPEND - ); - } - if (!is_int($new)) { $new = json_encode($new); } - self::$cache->watch($this->getNameSpace() . $key); + self::$cache->watch($this->getPrefix() . $key); if ($this->get($key) === $old) { $result = self::$cache->multi() - ->set($this->getNameSpace() . $key, $new) + ->set($this->getPrefix() . $key, $new) ->exec(); return $result !== false; } @@ -245,18 +159,10 @@ class Redis extends Cache implements IMemcacheTTL { * @return bool */ public function cad($key, $old) { - if ($this->logEnabled()) { - file_put_contents( - $this->logFile, - $this->getNameSpace() . '::cad::' . $key . "\n", - FILE_APPEND - ); - } - - self::$cache->watch($this->getNameSpace() . $key); + self::$cache->watch($this->getPrefix() . $key); if ($this->get($key) === $old) { $result = self::$cache->multi() - ->del($this->getNameSpace() . $key) + ->del($this->getPrefix() . $key) ->exec(); return $result !== false; } @@ -265,10 +171,10 @@ class Redis extends Cache implements IMemcacheTTL { } public function setTTL($key, $ttl) { - self::$cache->expire($this->getNameSpace() . $key, $ttl); + self::$cache->expire($this->getPrefix() . $key, $ttl); } - public static function isAvailable() { + public static function isAvailable(): bool { return \OC::$server->getGetRedisFactory()->isAvailable(); } } diff --git a/lib/private/Profiler/FileProfilerStorage.php b/lib/private/Profiler/FileProfilerStorage.php new file mode 100644 index 00000000000..ce09ed51ed9 --- /dev/null +++ b/lib/private/Profiler/FileProfilerStorage.php @@ -0,0 +1,286 @@ +<?php + +declare(strict_types = 1); +/** + * @copyright 2022 Carl Schwan <carl@carlschwan.eu> + * + * @author Carl Schwan <carl@carlschwan.eu> + * @author Alexandre Salomé <alexandre.salome@gmail.com> + * + * @license AGPL-3.0-or-later AND MIT + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OC\Profiler; + +use OCP\Profiler\IProfile; + +/** + * Storage for profiler using files. + */ +class FileProfilerStorage { + // Folder where profiler data are stored. + private string $folder; + + /** + * Constructs the file storage using a "dsn-like" path. + * + * Example : "file:/path/to/the/storage/folder" + * + * @throws \RuntimeException + */ + public function __construct(string $folder) { + $this->folder = $folder; + + if (!is_dir($this->folder) && false === @mkdir($this->folder, 0777, true) && !is_dir($this->folder)) { + throw new \RuntimeException(sprintf('Unable to create the storage directory (%s).', $this->folder)); + } + } + + public function find(?string $url, ?int $limit, ?string $method, int $start = null, int $end = null, string $statusCode = null): array { + $file = $this->getIndexFilename(); + + if (!file_exists($file)) { + return []; + } + + $file = fopen($file, 'r'); + fseek($file, 0, \SEEK_END); + + $result = []; + while (\count($result) < $limit && $line = $this->readLineFromFile($file)) { + $values = str_getcsv($line); + [$csvToken, $csvMethod, $csvUrl, $csvTime, $csvParent, $csvStatusCode] = $values; + $csvTime = (int) $csvTime; + + if ($url && false === strpos($csvUrl, $url) || $method && false === strpos($csvMethod, $method) || $statusCode && false === strpos($csvStatusCode, $statusCode)) { + continue; + } + + if (!empty($start) && $csvTime < $start) { + continue; + } + + if (!empty($end) && $csvTime > $end) { + continue; + } + + $result[$csvToken] = [ + 'token' => $csvToken, + 'method' => $csvMethod, + 'url' => $csvUrl, + 'time' => $csvTime, + 'parent' => $csvParent, + 'status_code' => $csvStatusCode, + ]; + } + + fclose($file); + + return array_values($result); + } + + public function purge(): void { + $flags = \FilesystemIterator::SKIP_DOTS; + $iterator = new \RecursiveDirectoryIterator($this->folder, $flags); + $iterator = new \RecursiveIteratorIterator($iterator, \RecursiveIteratorIterator::CHILD_FIRST); + + foreach ($iterator as $file) { + if (is_file($file)) { + unlink($file); + } else { + rmdir($file); + } + } + } + + public function read(string $token): ?IProfile { + if (!$token || !file_exists($file = $this->getFilename($token))) { + return null; + } + + if (\function_exists('gzcompress')) { + $file = 'compress.zlib://'.$file; + } + + return $this->createProfileFromData($token, unserialize(file_get_contents($file))); + } + + /** + * @throws \RuntimeException + */ + public function write(IProfile $profile): bool { + $file = $this->getFilename($profile->getToken()); + + $profileIndexed = is_file($file); + if (!$profileIndexed) { + // Create directory + $dir = \dirname($file); + if (!is_dir($dir) && false === @mkdir($dir, 0777, true) && !is_dir($dir)) { + throw new \RuntimeException(sprintf('Unable to create the storage directory (%s).', $dir)); + } + } + + $profileToken = $profile->getToken(); + // when there are errors in sub-requests, the parent and/or children tokens + // may equal the profile token, resulting in infinite loops + $parentToken = $profile->getParentToken() !== $profileToken ? $profile->getParentToken() : null; + $childrenToken = array_filter(array_map(function (IProfile $p) use ($profileToken) { + return $profileToken !== $p->getToken() ? $p->getToken() : null; + }, $profile->getChildren())); + + // Store profile + $data = [ + 'token' => $profileToken, + 'parent' => $parentToken, + 'children' => $childrenToken, + 'data' => $profile->getCollectors(), + 'method' => $profile->getMethod(), + 'url' => $profile->getUrl(), + 'time' => $profile->getTime(), + 'status_code' => $profile->getStatusCode(), + ]; + + $context = stream_context_create(); + + if (\function_exists('gzcompress')) { + $file = 'compress.zlib://'.$file; + stream_context_set_option($context, 'zlib', 'level', 3); + } + + if (false === file_put_contents($file, serialize($data), 0, $context)) { + return false; + } + + if (!$profileIndexed) { + // Add to index + if (false === $file = fopen($this->getIndexFilename(), 'a')) { + return false; + } + + fputcsv($file, [ + $profile->getToken(), + $profile->getMethod(), + $profile->getUrl(), + $profile->getTime(), + $profile->getParentToken(), + $profile->getStatusCode(), + ]); + fclose($file); + } + + return true; + } + + /** + * Gets filename to store data, associated to the token. + * + * @return string The profile filename + */ + protected function getFilename(string $token): string { + // Uses 4 last characters, because first are mostly the same. + $folderA = substr($token, -2, 2); + $folderB = substr($token, -4, 2); + + return $this->folder.'/'.$folderA.'/'.$folderB.'/'.$token; + } + + /** + * Gets the index filename. + * + * @return string The index filename + */ + protected function getIndexFilename(): string { + return $this->folder.'/index.csv'; + } + + /** + * Reads a line in the file, backward. + * + * This function automatically skips the empty lines and do not include the line return in result value. + * + * @param resource $file The file resource, with the pointer placed at the end of the line to read + * + * @return ?string A string representing the line or null if beginning of file is reached + */ + protected function readLineFromFile($file): ?string { + $line = ''; + $position = ftell($file); + + if (0 === $position) { + return null; + } + + while (true) { + $chunkSize = min($position, 1024); + $position -= $chunkSize; + fseek($file, $position); + + if (0 === $chunkSize) { + // bof reached + break; + } + + $buffer = fread($file, $chunkSize); + + if (false === ($upTo = strrpos($buffer, "\n"))) { + $line = $buffer.$line; + continue; + } + + $position += $upTo; + $line = substr($buffer, $upTo + 1).$line; + fseek($file, max(0, $position), \SEEK_SET); + + if ('' !== $line) { + break; + } + } + + return '' === $line ? null : $line; + } + + protected function createProfileFromData(string $token, array $data, IProfile $parent = null): IProfile { + $profile = new Profile($token); + $profile->setMethod($data['method']); + $profile->setUrl($data['url']); + $profile->setTime($data['time']); + $profile->setStatusCode($data['status_code']); + $profile->setCollectors($data['data']); + + if (!$parent && $data['parent']) { + $parent = $this->read($data['parent']); + } + + if ($parent) { + $profile->setParent($parent); + } + + foreach ($data['children'] as $token) { + if (!$token || !file_exists($file = $this->getFilename($token))) { + continue; + } + + if (\function_exists('gzcompress')) { + $file = 'compress.zlib://'.$file; + } + + $profile->addChild($this->createProfileFromData($token, unserialize(file_get_contents($file)), $profile)); + } + + return $profile; + } +} diff --git a/lib/private/Profiler/Profile.php b/lib/private/Profiler/Profile.php new file mode 100644 index 00000000000..648c49c0330 --- /dev/null +++ b/lib/private/Profiler/Profile.php @@ -0,0 +1,168 @@ +<?php + +declare(strict_types = 1); +/** + * @copyright 2022 Carl Schwan <carl@carlschwan.eu> + * + * @author Carl Schwan <carl@carlschwan.eu> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OC\Profiler; + +use OCP\DataCollector\IDataCollector; +use OCP\Profiler\IProfile; + +class Profile implements \JsonSerializable, IProfile { + private string $token; + + private ?int $time = null; + + private ?string $url = null; + + private ?string $method = null; + + private ?int $statusCode = null; + + /** @var array<string, IDataCollector> */ + private array $collectors = []; + + private ?IProfile $parent = null; + + /** @var IProfile[] */ + private array $children = []; + + public function __construct(string $token) { + $this->token = $token; + } + + public function getToken(): string { + return $this->token; + } + + public function setToken(string $token): void { + $this->token = $token; + } + + public function getTime(): ?int { + return $this->time; + } + + public function setTime(int $time): void { + $this->time = $time; + } + + public function getUrl(): ?string { + return $this->url; + } + + public function setUrl(string $url): void { + $this->url = $url; + } + + public function getMethod(): ?string { + return $this->method; + } + + public function setMethod(string $method): void { + $this->method = $method; + } + + public function getStatusCode(): ?int { + return $this->statusCode; + } + + public function setStatusCode(int $statusCode): void { + $this->statusCode = $statusCode; + } + + public function addCollector(IDataCollector $collector) { + $this->collectors[$collector->getName()] = $collector; + } + + public function getParent(): ?IProfile { + return $this->parent; + } + + public function setParent(?IProfile $parent): void { + $this->parent = $parent; + } + + public function getParentToken(): ?string { + return $this->parent ? $this->parent->getToken() : null; + } + + /** @return IProfile[] */ + public function getChildren(): array { + return $this->children; + } + + /** + * @param IProfile[] $children + */ + public function setChildren(array $children): void { + $this->children = []; + foreach ($children as $child) { + $this->addChild($child); + } + } + + public function addChild(IProfile $profile): void { + $this->children[] = $profile; + $profile->setParent($this); + } + + /** + * @return IDataCollector[] + */ + public function getCollectors(): array { + return $this->collectors; + } + + /** + * @param IDataCollector[] $collectors + */ + public function setCollectors(array $collectors): void { + $this->collectors = $collectors; + } + + public function __sleep(): array { + return ['token', 'parent', 'children', 'collectors', 'method', 'url', 'time', 'statusCode']; + } + + #[\ReturnTypeWillChange] + public function jsonSerialize() { + // Everything but parent + return [ + 'token' => $this->token, + 'method' => $this->method, + 'children' => $this->children, + 'url' => $this->url, + 'statusCode' => $this->statusCode, + 'time' => $this->time, + 'collectors' => $this->collectors, + ]; + } + + public function getCollector(string $collectorName): ?IDataCollector { + if (!array_key_exists($collectorName, $this->collectors)) { + return null; + } + return $this->collectors[$collectorName]; + } +} diff --git a/lib/private/Profiler/Profiler.php b/lib/private/Profiler/Profiler.php new file mode 100644 index 00000000000..8aa800fbc6d --- /dev/null +++ b/lib/private/Profiler/Profiler.php @@ -0,0 +1,105 @@ +<?php + +declare(strict_types = 1); + +/** + * @copyright 2021 Carl Schwan <carl@carlschwan.eu> + * + * @author Carl Schwan <carl@carlschwan.eu> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OC\Profiler; + +use OC\AppFramework\Http\Request; +use OCP\AppFramework\Http\Response; +use OCP\DataCollector\IDataCollector; +use OCP\Profiler\IProfiler; +use OCP\Profiler\IProfile; +use OC\SystemConfig; + +class Profiler implements IProfiler { + /** @var array<string, IDataCollector> */ + private array $dataCollectors = []; + + private ?FileProfilerStorage $storage = null; + + private bool $enabled = false; + + public function __construct(SystemConfig $config) { + $this->enabled = $config->getValue('profiler', false); + if ($this->enabled) { + $this->storage = new FileProfilerStorage($config->getValue('datadirectory', \OC::$SERVERROOT . '/data') . '/profiler'); + } + } + + public function add(IDataCollector $dataCollector): void { + $this->dataCollectors[$dataCollector->getName()] = $dataCollector; + } + + public function loadProfileFromResponse(Response $response): ?IProfile { + if (!$token = $response->getHeaders()['X-Debug-Token']) { + return null; + } + + return $this->loadProfile($token); + } + + public function loadProfile(string $token): ?IProfile { + return $this->storage->read($token); + } + + public function saveProfile(IProfile $profile): bool { + return $this->storage->write($profile); + } + + public function collect(Request $request, Response $response): IProfile { + $profile = new Profile($request->getId()); + $profile->setTime(time()); + $profile->setUrl($request->getRequestUri()); + $profile->setMethod($request->getMethod()); + $profile->setStatusCode($response->getStatus()); + foreach ($this->dataCollectors as $dataCollector) { + $dataCollector->collect($request, $response, null); + + // We clone for subrequests + $profile->addCollector(clone $dataCollector); + } + return $profile; + } + + /** + * @return array[] + */ + public function find(?string $url, ?int $limit, ?string $method, ?int $start, ?int $end, + string $statusCode = null): array { + return $this->storage->find($url, $limit, $method, $start, $end, $statusCode); + } + + public function dataProviders(): array { + return array_keys($this->dataCollectors); + } + + public function isEnabled(): bool { + return $this->enabled; + } + + public function setEnabled(bool $enabled): void { + $this->enabled = $enabled; + } +} diff --git a/lib/private/Profiler/RoutingDataCollector.php b/lib/private/Profiler/RoutingDataCollector.php new file mode 100644 index 00000000000..e6659230879 --- /dev/null +++ b/lib/private/Profiler/RoutingDataCollector.php @@ -0,0 +1,55 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright 2022 Carl Schwan <carl@carlschwan.eu> + * + * @author Carl Schwan <carl@carlschwan.eu> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OC\Profiler; + +use OC\AppFramework\Http\Request; +use OCP\AppFramework\Http\Response; +use OCP\DataCollector\AbstractDataCollector; + +class RoutingDataCollector extends AbstractDataCollector { + private string $appName; + private string $controllerName; + private string $actionName; + + public function __construct(string $appName, string $controllerName, string $actionName) { + $this->appName = $appName; + $this->controllerName = $controllerName; + $this->actionName = $actionName; + } + + public function collect(Request $request, Response $response, \Throwable $exception = null): void { + $this->data = [ + 'appName' => $this->appName, + 'controllerName' => $this->controllerName, + 'actionName' => $this->actionName, + ]; + } + + public function getName(): string { + return 'router'; + } +} diff --git a/lib/private/Server.php b/lib/private/Server.php index 682e6fa06ce..b214ba3ce54 100644 --- a/lib/private/Server.php +++ b/lib/private/Server.php @@ -260,6 +260,8 @@ use OCA\Files_External\Service\UserStoragesService; use OCA\Files_External\Service\UserGlobalStoragesService; use OCA\Files_External\Service\GlobalStoragesService; use OCA\Files_External\Service\BackendService; +use OCP\Profiler\IProfiler; +use OC\Profiler\Profiler; /** * Class Server @@ -344,6 +346,10 @@ class Server extends ServerContainer implements IServerContainer { ); }); + $this->registerService(IProfiler::class, function (Server $c) { + return new Profiler($c->get(SystemConfig::class)); + }); + $this->registerService(\OCP\Encryption\IManager::class, function (Server $c) { $view = new View(); $util = new Encryption\Util( @@ -691,9 +697,9 @@ class Server extends ServerContainer implements IServerContainer { $this->registerDeprecatedAlias('UserCache', ICache::class); $this->registerService(Factory::class, function (Server $c) { - $arrayCacheFactory = new \OC\Memcache\Factory( - '', - $c->get(LoggerInterface::class), + $profiler = $c->get(IProfiler::class); + $arrayCacheFactory = new \OC\Memcache\Factory('', $c->get(LoggerInterface::class), + $profiler, ArrayCache::class, ArrayCache::class, ArrayCache::class @@ -717,9 +723,9 @@ class Server extends ServerContainer implements IServerContainer { $instanceId = \OC_Util::getInstanceId(); $path = \OC::$SERVERROOT; $prefix = md5($instanceId . '-' . $version . '-' . $path); - return new \OC\Memcache\Factory( - $prefix, + return new \OC\Memcache\Factory($prefix, $c->get(LoggerInterface::class), + $profiler, $config->getSystemValue('memcache.local', null), $config->getSystemValue('memcache.distributed', null), $config->getSystemValue('memcache.locking', null), @@ -769,6 +775,7 @@ class Server extends ServerContainer implements IServerContainer { $c->get(KnownUserService::class) ); }); + $this->registerAlias(IAvatarManager::class, AvatarManager::class); /** @deprecated 19.0.0 */ $this->registerDeprecatedAlias('AvatarManager', AvatarManager::class); @@ -861,7 +868,6 @@ class Server extends ServerContainer implements IServerContainer { } $connectionParams = $factory->createConnectionParams(); $connection = $factory->getConnection($type, $connectionParams); - $connection->getConfiguration()->setSQLLogger($c->getQueryLogger()); return $connection; }); /** @deprecated 19.0.0 */ diff --git a/lib/public/DataCollector/AbstractDataCollector.php b/lib/public/DataCollector/AbstractDataCollector.php new file mode 100644 index 00000000000..68298671b7b --- /dev/null +++ b/lib/public/DataCollector/AbstractDataCollector.php @@ -0,0 +1,87 @@ +<?php + +declare(strict_types=1); +/** + * @copyright 2022 Carl Schwan <carl@carlschwan.eu> + * + * @author Carl Schwan <carl@carlschwan.eu> + * @author Fabien Potencier <fabien@symfony.com> + * + * @license AGPL-3.0-or-later AND MIT + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OCP\DataCollector; + +/** + * Children of this class must store the collected data in + * the data property. + * + * @author Fabien Potencier <fabien@symfony.com> + * @author Bernhard Schussek <bschussek@symfony.com> + * @author Carl Schwan <carl@carlschwan.eu> + * @since 24.0.0 + */ +abstract class AbstractDataCollector implements IDataCollector, \JsonSerializable { + /** @var array */ + protected $data = []; + + /** + * @since 24.0.0 + */ + public function getName(): string { + return static::class; + } + + /** + * Reset the state of the profiler. By default it only empties the + * $this->data contents, but you can override this method to do + * additional cleaning. + * @since 24.0.0 + */ + public function reset(): void { + $this->data = []; + } + + /** + * @since 24.0.0 + */ + public function __sleep(): array { + return ['data']; + } + + /** + * @internal to prevent implementing \Serializable + * @since 24.0.0 + */ + final protected function serialize() { + } + + /** + * @internal to prevent implementing \Serializable + * @since 24.0.0 + */ + final protected function unserialize(string $data) { + } + + /** + * @since 24.0.0 + */ + #[\ReturnTypeWillChange] + public function jsonSerialize() { + return $this->data; + } +} diff --git a/lib/public/DataCollector/IDataCollector.php b/lib/public/DataCollector/IDataCollector.php new file mode 100644 index 00000000000..0fb914727df --- /dev/null +++ b/lib/public/DataCollector/IDataCollector.php @@ -0,0 +1,55 @@ +<?php + +declare(strict_types=1); +/** + * @copyright 2022 Carl Schwan <carl@carlschwan.eu> + * + * @author Carl Schwan <carl@carlschwan.eu> + * @author Fabien Potencier <fabien@symfony.com> + * + * @license AGPL-3.0-or-later AND MIT + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OCP\DataCollector; + +use OC\AppFramework\Http\Request; +use OCP\AppFramework\Http\Response; + +/** + * DataCollectorInterface. + * + * @since 24.0.0 + */ +interface IDataCollector { + /** + * Collects data for the given Request and Response. + * @since 24.0.0 + */ + public function collect(Request $request, Response $response, \Throwable $exception = null): void; + + /** + * Reset the state of the profiler. + * @since 24.0.0 + */ + public function reset(): void; + + /** + * Returns the name of the collector. + * @since 24.0.0 + */ + public function getName(): string; +} diff --git a/lib/public/ICache.php b/lib/public/ICache.php index 47b0e2f4c3d..0e818277f60 100644 --- a/lib/public/ICache.php +++ b/lib/public/ICache.php @@ -76,4 +76,10 @@ interface ICache { * @since 6.0.0 */ public function clear($prefix = ''); + + /** + * Check if the cache implementation is available + * @since 24.0.0 + */ + public static function isAvailable(): bool; } diff --git a/lib/public/Profiler/IProfile.php b/lib/public/Profiler/IProfile.php new file mode 100644 index 00000000000..1831496a5a7 --- /dev/null +++ b/lib/public/Profiler/IProfile.php @@ -0,0 +1,168 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright 2022 Carl Schwan <carl@carlschwan.eu> + * + * @author Carl Schwan <carl@carlschwan.eu> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OCP\Profiler; + +use OCP\DataCollector\IDataCollector; + +/** + * This interface store the results of the profiling of one + * request. You can get the saved profiles from the @see IProfiler. + * + * ```php + * <?php + * $profiler = \OC::$server->get(IProfiler::class); + * $profiles = $profiler->find('/settings/users', 10); + * ``` + * + * This interface is meant to be used directly and not extended. + * @since 24.0.0 + */ +interface IProfile { + /** + * Get the token of the profile + * @since 24.0.0 + */ + public function getToken(): string; + + /** + * Set the token of the profile + * @since 24.0.0 + */ + public function setToken(string $token): void; + + /** + * Get the time of the profile + * @since 24.0.0 + */ + public function getTime(): ?int; + + /** + * Set the time of the profile + * @since 24.0.0 + */ + public function setTime(int $time): void; + + /** + * Get the url of the profile + * @since 24.0.0 + */ + public function getUrl(): ?string; + + /** + * Set the url of the profile + * @since 24.0.0 + */ + public function setUrl(string $url): void; + + /** + * Get the method of the profile + * @since 24.0.0 + */ + public function getMethod(): ?string; + + /** + * Set the method of the profile + * @since 24.0.0 + */ + public function setMethod(string $method): void; + + /** + * Get the status code of the profile + * @since 24.0.0 + */ + public function getStatusCode(): ?int; + + /** + * Set the status code of the profile + * @since 24.0.0 + */ + public function setStatusCode(int $statusCode): void; + + /** + * Add a data collector to the profile + * @since 24.0.0 + */ + public function addCollector(IDataCollector $collector); + + /** + * Get the parent profile to this profile + * @since 24.0.0 + */ + public function getParent(): ?IProfile; + + /** + * Set the parent profile to this profile + * @since 24.0.0 + */ + public function setParent(?IProfile $parent): void; + + /** + * Get the parent token to this profile + * @since 24.0.0 + */ + public function getParentToken(): ?string; + + /** + * Get the profile's children + * @return IProfile[] + * @since 24.0.0 + **/ + public function getChildren(): array; + + /** + * Set the profile's children + * @param IProfile[] $children + * @since 24.0.0 + */ + public function setChildren(array $children): void; + + /** + * Add the child profile + * @since 24.0.0 + */ + public function addChild(IProfile $profile): void; + + /** + * Get all the data collectors + * @return IDataCollector[] + * @since 24.0.0 + */ + public function getCollectors(): array; + + /** + * Set all the data collectors + * @param IDataCollector[] $collectors + * @since 24.0.0 + */ + public function setCollectors(array $collectors): void; + + /** + * Get a data collector by name + * @since 24.0.0 + */ + public function getCollector(string $collectorName): ?IDataCollector; +} diff --git a/lib/public/Profiler/IProfiler.php b/lib/public/Profiler/IProfiler.php new file mode 100644 index 00000000000..78325089523 --- /dev/null +++ b/lib/public/Profiler/IProfiler.php @@ -0,0 +1,101 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright 2022 Carl Schwan <carl@carlschwan.eu> + * + * @author Carl Schwan <carl@carlschwan.eu> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OCP\Profiler; + +use OC\AppFramework\Http\Request; +use OCP\AppFramework\Http\Response; +use OCP\DataCollector\IDataCollector; + +/** + * This interface allows to interact with the built-in Nextcloud profiler. + * @since 24.0.0 + */ +interface IProfiler { + /** + * Add a new data collector to the profiler. This allows to later on + * collect all the data from every registered collector. + * + * @see IDataCollector + * @since 24.0.0 + */ + public function add(IDataCollector $dataCollector): void; + + /** + * Load a profile from a response object + * @since 24.0.0 + */ + public function loadProfileFromResponse(Response $response): ?IProfile; + + /** + * Load a profile from the response token + * @since 24.0.0 + */ + public function loadProfile(string $token): ?IProfile; + + /** + * Save a profile on the disk. This allows to later load it again in the + * profiler user interface. + * @since 24.0.0 + */ + public function saveProfile(IProfile $profile): bool; + + /** + * Find a profile from various search parameters + * @since 24.0.0 + */ + public function find(?string $url, ?int $limit, ?string $method, ?int $start, ?int $end, string $statusCode = null): array; + + /** + * Get the list of data providers by identifier + * @return string[] + * @since 24.0.0 + */ + public function dataProviders(): array; + + /** + * Check if the profiler is enabled. + * + * If it is not enabled, data provider shouldn't be created and + * shouldn't collect any data. + * @since 24.0.0 + */ + public function isEnabled(): bool; + + /** + * Set if the profiler is enabled. + * @see isEnabled + * @since 24.0.0 + */ + public function setEnabled(bool $enabled): void; + + /** + * Collect all the information from the current request and construct + * a IProfile from it. + * @since 24.0.0 + */ + public function collect(Request $request, Response $response): IProfile; +} |