The webui is provided by a seperate application named profiler Signed-off-by: Carl Schwan <carl@carlschwan.eu>tags/v24.0.0beta3
@@ -272,6 +272,7 @@ return array( | |||
'OCA\\DAV\\Migration\\Version1016Date20201109085907' => $baseDir . '/../lib/Migration/Version1016Date20201109085907.php', | |||
'OCA\\DAV\\Migration\\Version1017Date20210216083742' => $baseDir . '/../lib/Migration/Version1017Date20210216083742.php', | |||
'OCA\\DAV\\Migration\\Version1018Date20210312100735' => $baseDir . '/../lib/Migration/Version1018Date20210312100735.php', | |||
'OCA\\DAV\\Profiler\\ProfilerPlugin' => $baseDir . '/../lib/Profiler/ProfilerPlugin.php', | |||
'OCA\\DAV\\Provisioning\\Apple\\AppleProvisioningNode' => $baseDir . '/../lib/Provisioning/Apple/AppleProvisioningNode.php', | |||
'OCA\\DAV\\Provisioning\\Apple\\AppleProvisioningPlugin' => $baseDir . '/../lib/Provisioning/Apple/AppleProvisioningPlugin.php', | |||
'OCA\\DAV\\RootCollection' => $baseDir . '/../lib/RootCollection.php', |
@@ -287,6 +287,7 @@ class ComposerStaticInitDAV | |||
'OCA\\DAV\\Migration\\Version1016Date20201109085907' => __DIR__ . '/..' . '/../lib/Migration/Version1016Date20201109085907.php', | |||
'OCA\\DAV\\Migration\\Version1017Date20210216083742' => __DIR__ . '/..' . '/../lib/Migration/Version1017Date20210216083742.php', | |||
'OCA\\DAV\\Migration\\Version1018Date20210312100735' => __DIR__ . '/..' . '/../lib/Migration/Version1018Date20210312100735.php', | |||
'OCA\\DAV\\Profiler\\ProfilerPlugin' => __DIR__ . '/..' . '/../lib/Profiler/ProfilerPlugin.php', | |||
'OCA\\DAV\\Provisioning\\Apple\\AppleProvisioningNode' => __DIR__ . '/..' . '/../lib/Provisioning/Apple/AppleProvisioningNode.php', | |||
'OCA\\DAV\\Provisioning\\Apple\\AppleProvisioningPlugin' => __DIR__ . '/..' . '/../lib/Provisioning/Apple/AppleProvisioningPlugin.php', | |||
'OCA\\DAV\\RootCollection' => __DIR__ . '/..' . '/../lib/RootCollection.php', |
@@ -0,0 +1,47 @@ | |||
<?php declare(strict_types = 1); | |||
/** | |||
* @copyright 2021 Carl Schwan <carl@carlschwan.eu> | |||
* | |||
* @author Carl Schwan <carl@carlschwan.eu> | |||
* | |||
* @license AGPL-3.0-or-later | |||
* | |||
* 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 OCA\DAV\Profiler; | |||
use OCP\IRequest; | |||
use Sabre\DAV\Server; | |||
use Sabre\HTTP\RequestInterface; | |||
use Sabre\HTTP\ResponseInterface; | |||
class ProfilerPlugin extends \Sabre\DAV\ServerPlugin { | |||
private IRequest $request; | |||
public function __construct(IRequest $request) { | |||
$this->request = $request; | |||
} | |||
/** @return void */ | |||
public function initialize(Server $server) { | |||
$server->on('afterMethod:*', [$this, 'afterMethod']); | |||
} | |||
/** @return void */ | |||
public function afterMethod(RequestInterface $request, ResponseInterface $response) { | |||
$response->addHeader('X-Debug-Token', $this->request->getId()); | |||
} | |||
} |
@@ -36,6 +36,9 @@ namespace OCA\DAV; | |||
use OCA\DAV\Connector\Sabre\RequestIdHeaderPlugin; | |||
use OCP\Diagnostics\IEventLogger; | |||
use OCP\Profiler\IProfiler; | |||
use OCA\DAV\Profiler\ProfilerPlugin; | |||
use OCP\AppFramework\Http\Response; | |||
use Psr\Log\LoggerInterface; | |||
use OCA\DAV\AppInfo\PluginManager; | |||
use OCA\DAV\CalDAV\BirthdayService; | |||
@@ -78,17 +81,19 @@ use Sabre\DAV\UUIDUtil; | |||
use SearchDAV\DAV\SearchPlugin; | |||
class Server { | |||
private IRequest $request; | |||
private string $baseUri; | |||
public Connector\Sabre\Server $server; | |||
private IProfiler $profiler; | |||
public function __construct(IRequest $request, string $baseUri) { | |||
$this->profiler = \OC::$server->get(IProfiler::class); | |||
if ($this->profiler->isEnabled()) { | |||
/** @var IEventLogger $eventLogger */ | |||
$eventLogger = \OC::$server->get(IEventLogger::class); | |||
$eventLogger->start('runtime', 'DAV Runtime'); | |||
} | |||
/** @var IRequest */ | |||
private $request; | |||
/** @var string */ | |||
private $baseUri; | |||
/** @var Connector\Sabre\Server */ | |||
public $server; | |||
public function __construct(IRequest $request, $baseUri) { | |||
$this->request = $request; | |||
$this->baseUri = $baseUri; | |||
$logger = \OC::$server->getLogger(); | |||
@@ -115,6 +120,7 @@ class Server { | |||
$this->server->httpRequest->setUrl($this->request->getRequestUri()); | |||
$this->server->setBaseUri($this->baseUri); | |||
$this->server->addPlugin(new ProfilerPlugin($this->request)); | |||
$this->server->addPlugin(new BlockLegacyClientPlugin(\OC::$server->getConfig())); | |||
$this->server->addPlugin(new AnonymousOptionsPlugin()); | |||
$authPlugin = new Plugin(); | |||
@@ -343,6 +349,11 @@ class Server { | |||
$eventLogger->start('dav_server_exec', ''); | |||
$this->server->exec(); | |||
$eventLogger->end('dav_server_exec'); | |||
if ($this->profiler->isEnabled()) { | |||
$eventLogger->end('runtime'); | |||
$profile = $this->profiler->collect(\OC::$server->get(IRequest::class), new Response()); | |||
$this->profiler->saveProfile($profile); | |||
} | |||
} | |||
private function requestIsForSubtree(array $subTrees): bool { |
@@ -26,6 +26,7 @@ return array( | |||
'OCA\\User_LDAP\\ConnectionFactory' => $baseDir . '/../lib/ConnectionFactory.php', | |||
'OCA\\User_LDAP\\Controller\\ConfigAPIController' => $baseDir . '/../lib/Controller/ConfigAPIController.php', | |||
'OCA\\User_LDAP\\Controller\\RenewPasswordController' => $baseDir . '/../lib/Controller/RenewPasswordController.php', | |||
'OCA\\User_LDAP\\DataCollector\\LdapDataCollector' => $baseDir . '/../lib/DataCollector/LdapDataCollector.php', | |||
'OCA\\User_LDAP\\Events\\GroupBackendRegistered' => $baseDir . '/../lib/Events/GroupBackendRegistered.php', | |||
'OCA\\User_LDAP\\Events\\UserBackendRegistered' => $baseDir . '/../lib/Events/UserBackendRegistered.php', | |||
'OCA\\User_LDAP\\Exceptions\\AttributeNotSet' => $baseDir . '/../lib/Exceptions/AttributeNotSet.php', |
@@ -41,6 +41,7 @@ class ComposerStaticInitUser_LDAP | |||
'OCA\\User_LDAP\\ConnectionFactory' => __DIR__ . '/..' . '/../lib/ConnectionFactory.php', | |||
'OCA\\User_LDAP\\Controller\\ConfigAPIController' => __DIR__ . '/..' . '/../lib/Controller/ConfigAPIController.php', | |||
'OCA\\User_LDAP\\Controller\\RenewPasswordController' => __DIR__ . '/..' . '/../lib/Controller/RenewPasswordController.php', | |||
'OCA\\User_LDAP\\DataCollector\\LdapDataCollector' => __DIR__ . '/..' . '/../lib/DataCollector/LdapDataCollector.php', | |||
'OCA\\User_LDAP\\Events\\GroupBackendRegistered' => __DIR__ . '/..' . '/../lib/Events/GroupBackendRegistered.php', | |||
'OCA\\User_LDAP\\Events\\UserBackendRegistered' => __DIR__ . '/..' . '/../lib/Events/UserBackendRegistered.php', | |||
'OCA\\User_LDAP\\Exceptions\\AttributeNotSet' => __DIR__ . '/..' . '/../lib/Exceptions/AttributeNotSet.php', |
@@ -0,0 +1,50 @@ | |||
<?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 OCA\User_LDAP\DataCollector; | |||
use OC\AppFramework\Http\Request; | |||
use OCP\AppFramework\Http\Response; | |||
use OCP\DataCollector\AbstractDataCollector; | |||
class LdapDataCollector extends AbstractDataCollector { | |||
public function startLdapRequest(string $query, array $args): void { | |||
$this->data[] = [ | |||
'start' => microtime(true), | |||
'query' => $query, | |||
'args' => $args, | |||
'end' => microtime(true), | |||
]; | |||
} | |||
public function stopLastLdapRequest(): void { | |||
$this->data[count($this->data) - 1]['end'] = microtime(true); | |||
} | |||
public function getName(): string { | |||
return 'ldap'; | |||
} | |||
public function collect(Request $request, Response $response, \Throwable $exception = null): void { | |||
} | |||
} |
@@ -14,6 +14,7 @@ | |||
* @author Robin McCorkell <robin@mccorkell.me.uk> | |||
* @author Roeland Jago Douma <roeland@famdouma.nl> | |||
* @author Roger Szabo <roger.szabo@web.de> | |||
* @author Carl Schwan <carl@carlschwan.eu> | |||
* | |||
* @license AGPL-3.0 | |||
* | |||
@@ -32,7 +33,9 @@ | |||
*/ | |||
namespace OCA\User_LDAP; | |||
use OCP\Profiler\IProfiler; | |||
use OC\ServerNotAvailableException; | |||
use OCA\User_LDAP\DataCollector\LdapDataCollector; | |||
use OCA\User_LDAP\Exceptions\ConstraintViolationException; | |||
use OCA\User_LDAP\PagedResults\IAdapter; | |||
use OCA\User_LDAP\PagedResults\Php73; | |||
@@ -45,9 +48,18 @@ class LDAP implements ILDAPWrapper { | |||
/** @var IAdapter */ | |||
protected $pagedResultsAdapter; | |||
private ?LdapDataCollector $dataCollector = null; | |||
public function __construct(string $logFile = '') { | |||
$this->pagedResultsAdapter = new Php73(); | |||
$this->logFile = $logFile; | |||
/** @var IProfiler $profiler */ | |||
$profiler = \OC::$server->get(IProfiler::class); | |||
if ($profiler->isEnabled()) { | |||
$this->dataCollector = new LdapDataCollector(); | |||
$profiler->add($this->dataCollector); | |||
} | |||
} | |||
/** | |||
@@ -295,24 +307,26 @@ class LDAP implements ILDAPWrapper { | |||
if ($this->isResultFalse($result)) { | |||
$this->postFunctionCall(); | |||
} | |||
if ($this->dataCollector !== null) { | |||
$this->dataCollector->stopLastLdapRequest(); | |||
} | |||
return $result; | |||
} | |||
return null; | |||
} | |||
/** | |||
* @param string $functionName | |||
* @param array $args | |||
*/ | |||
private function preFunctionCall($functionName, $args) { | |||
private function preFunctionCall(string $functionName, array $args): void { | |||
$this->curFunc = $functionName; | |||
$this->curArgs = $args; | |||
if ($this->dataCollector !== null) { | |||
$args = array_map(fn ($item) => (!$this->isResource($item) ? $item : '(resource)'), $this->curArgs); | |||
$this->dataCollector->startLdapRequest($this->curFunc, $args); | |||
} | |||
if ($this->logFile !== '' && is_writable(dirname($this->logFile)) && (!file_exists($this->logFile) || is_writable($this->logFile))) { | |||
$args = array_reduce($this->curArgs, static function (array $carry, $item): array { | |||
$carry[] = !is_resource($item) ? $item : '(resource)'; | |||
return $carry; | |||
}, []); | |||
$args = array_map(fn ($item) => (!$this->isResource($item) ? $item : '(resource)'), $this->curArgs); | |||
file_put_contents( | |||
$this->logFile, | |||
$this->curFunc . '::' . json_encode($args) . "\n", |
@@ -1023,7 +1023,6 @@ | |||
</RedundantCondition> | |||
<TypeDoesNotContainType occurrences="2"> | |||
<code>get_class($res) === 'OpenSSLAsymmetricKey'</code> | |||
<code>is_object($res)</code> | |||
</TypeDoesNotContainType> | |||
</file> | |||
<file src="apps/encryption/lib/Crypto/EncryptAll.php"> | |||
@@ -2638,11 +2637,6 @@ | |||
<code>$default</code> | |||
</MoreSpecificImplementedParamType> | |||
</file> | |||
<file src="lib/private/AppFramework/Utility/SimpleContainer.php"> | |||
<UndefinedMethod occurrences="1"> | |||
<code>getName</code> | |||
</UndefinedMethod> | |||
</file> | |||
<file src="lib/private/Archive/TAR.php"> | |||
<UndefinedDocblockClass occurrences="1"> | |||
<code>$this->tar->extractInString($path)</code> |
@@ -962,6 +962,14 @@ $CONFIG = [ | |||
*/ | |||
'log_rotate_size' => 100 * 1024 * 1024, | |||
/** | |||
* Enable built-in profiler. Helpful when trying to debug performance | |||
* issues. | |||
* | |||
* Note that this has a performance impact and shouldn't be enabled | |||
* on production. | |||
*/ | |||
'profiler' => false, | |||
/** | |||
* Alternate Code Locations |
@@ -202,6 +202,6 @@ $getUserAvatar = static function (int $size) use ($_): string { | |||
<div id="content" class="app-<?php p($_['appid']) ?>" role="main"> | |||
<?php print_unescaped($_['content']); ?> | |||
</div> | |||
<div id="profiler-toolbar"></div> | |||
</body> | |||
</html> |
@@ -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; | |||
} | |||
} |
@@ -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) { |
@@ -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', |
@@ -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', |
@@ -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); | |||
} |
@@ -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); |
@@ -115,4 +115,8 @@ class CappedMemoryCache implements ICache, \ArrayAccess { | |||
$this->remove($key); | |||
} | |||
} | |||
public static function isAvailable(): bool { | |||
return true; | |||
} | |||
} |
@@ -203,4 +203,8 @@ class File implements ICache { | |||
} | |||
} | |||
} | |||
public static function isAvailable(): bool { | |||
return true; | |||
} | |||
} |
@@ -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); | |||
} | |||
} | |||
/** |
@@ -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]; | |||
} | |||
} |
@@ -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; | |||
} | |||
} |
@@ -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; |
@@ -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')) { |
@@ -153,7 +153,7 @@ class ArrayCache extends Cache implements IMemcache { | |||
/** | |||
* {@inheritDoc} | |||
*/ | |||
public static function isAvailable() { | |||
public static function isAvailable(): bool { | |||
return true; | |||
} | |||
} |
@@ -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; | |||
} | |||
/** |
@@ -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; | |||
} | |||
} |
@@ -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'); | |||
} | |||
@@ -67,7 +67,7 @@ class NullCache extends Cache implements \OCP\IMemcache { | |||
return true; | |||
} | |||
public static function isAvailable() { | |||
public static function isAvailable(): bool { | |||
return true; | |||
} | |||
} |
@@ -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; | |||
} | |||
} |
@@ -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(); | |||
} | |||
} |
@@ -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; | |||
} | |||
} |
@@ -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]; | |||
} | |||
} |
@@ -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; | |||
} | |||
} |
@@ -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'; | |||
} | |||
} |
@@ -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 */ |
@@ -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; | |||
} | |||
} |
@@ -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; | |||
} |
@@ -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; | |||
} |
@@ -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; | |||
} |
@@ -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; | |||
} |
@@ -71,7 +71,7 @@ class AppTest extends \Test\TestCase { | |||
$this->container[$this->controllerName] = $this->controller; | |||
$this->container['Dispatcher'] = $this->dispatcher; | |||
$this->container['OCP\\AppFramework\\Http\\IOutput'] = $this->io; | |||
$this->container['urlParams'] = []; | |||
$this->container['urlParams'] = ['_route' => 'not-profiler']; | |||
$this->appPath = __DIR__ . '/../../../apps/namespacetestapp'; | |||
$infoXmlPath = $this->appPath . '/appinfo/info.xml'; | |||
@@ -183,6 +183,7 @@ class AppTest extends \Test\TestCase { | |||
public function testCoreApp() { | |||
$this->container['AppName'] = 'core'; | |||
$this->container['OC\Core\Controller\Foo'] = $this->controller; | |||
$this->container['urlParams'] = ['_route' => 'not-profiler']; | |||
$return = ['HTTP/2.0 200 OK', [], [], null, new Response()]; | |||
$this->dispatcher->expects($this->once()) | |||
@@ -200,6 +201,7 @@ class AppTest extends \Test\TestCase { | |||
public function testSettingsApp() { | |||
$this->container['AppName'] = 'settings'; | |||
$this->container['OCA\Settings\Controller\Foo'] = $this->controller; | |||
$this->container['urlParams'] = ['_route' => 'not-profiler']; | |||
$return = ['HTTP/2.0 200 OK', [], [], null, new Response()]; | |||
$this->dispatcher->expects($this->once()) | |||
@@ -217,6 +219,7 @@ class AppTest extends \Test\TestCase { | |||
public function testApp() { | |||
$this->container['AppName'] = 'bar'; | |||
$this->container['OCA\Bar\Controller\Foo'] = $this->controller; | |||
$this->container['urlParams'] = ['_route' => 'not-profiler']; | |||
$return = ['HTTP/2.0 200 OK', [], [], null, new Response()]; | |||
$this->dispatcher->expects($this->once()) |
@@ -23,12 +23,13 @@ namespace Test\Memcache; | |||
use OC\Memcache\NullCache; | |||
use Psr\Log\LoggerInterface; | |||
use OCP\Profiler\IProfiler; | |||
class Test_Factory_Available_Cache1 extends NullCache { | |||
public function __construct($prefix = '') { | |||
} | |||
public static function isAvailable() { | |||
public static function isAvailable(): bool { | |||
return true; | |||
} | |||
} | |||
@@ -37,7 +38,7 @@ class Test_Factory_Available_Cache2 extends NullCache { | |||
public function __construct($prefix = '') { | |||
} | |||
public static function isAvailable() { | |||
public static function isAvailable(): bool { | |||
return true; | |||
} | |||
} | |||
@@ -46,7 +47,7 @@ class Test_Factory_Unavailable_Cache1 extends NullCache { | |||
public function __construct($prefix = '') { | |||
} | |||
public static function isAvailable() { | |||
public static function isAvailable(): bool { | |||
return false; | |||
} | |||
} | |||
@@ -55,7 +56,7 @@ class Test_Factory_Unavailable_Cache2 extends NullCache { | |||
public function __construct($prefix = '') { | |||
} | |||
public static function isAvailable() { | |||
public static function isAvailable(): bool { | |||
return false; | |||
} | |||
} | |||
@@ -119,7 +120,8 @@ class FactoryTest extends \Test\TestCase { | |||
public function testCacheAvailability($localCache, $distributedCache, $lockingCache, | |||
$expectedLocalCache, $expectedDistributedCache, $expectedLockingCache) { | |||
$logger = $this->getMockBuilder(LoggerInterface::class)->getMock(); | |||
$factory = new \OC\Memcache\Factory('abc', $logger, $localCache, $distributedCache, $lockingCache); | |||
$profiler = $this->getMockBuilder(IProfiler::class)->getMock(); | |||
$factory = new \OC\Memcache\Factory('abc', $logger, $profiler, $localCache, $distributedCache, $lockingCache); | |||
$this->assertTrue(is_a($factory->createLocal(), $expectedLocalCache)); | |||
$this->assertTrue(is_a($factory->createDistributed(), $expectedDistributedCache)); | |||
$this->assertTrue(is_a($factory->createLocking(), $expectedLockingCache)); | |||
@@ -132,6 +134,7 @@ class FactoryTest extends \Test\TestCase { | |||
$this->expectException(\OCP\HintException::class); | |||
$logger = $this->getMockBuilder(LoggerInterface::class)->getMock(); | |||
new \OC\Memcache\Factory('abc', $logger, $localCache, $distributedCache); | |||
$profiler = $this->getMockBuilder(IProfiler::class)->getMock(); | |||
new \OC\Memcache\Factory('abc', $logger, $profiler, $localCache, $distributedCache); | |||
} | |||
} |