瀏覽代碼

Add a built-in profiler inside Nextcloud

The webui is provided by a seperate application named profiler

Signed-off-by: Carl Schwan <carl@carlschwan.eu>
tags/v24.0.0beta3
Carl Schwan 2 年之前
父節點
當前提交
7d272c54d0
共有 43 個檔案被更改,包括 2001 行新增190 行删除
  1. 1
    0
      apps/dav/composer/composer/autoload_classmap.php
  2. 1
    0
      apps/dav/composer/composer/autoload_static.php
  3. 47
    0
      apps/dav/lib/Profiler/ProfilerPlugin.php
  4. 21
    10
      apps/dav/lib/Server.php
  5. 1
    0
      apps/user_ldap/composer/composer/autoload_classmap.php
  6. 1
    0
      apps/user_ldap/composer/composer/autoload_static.php
  7. 50
    0
      apps/user_ldap/lib/DataCollector/LdapDataCollector.php
  8. 23
    9
      apps/user_ldap/lib/LDAP.php
  9. 0
    6
      build/psalm-baseline.xml
  10. 8
    0
      config/config.sample.php
  11. 1
    1
      core/templates/layout.user.php
  12. 3
    2
      lib/autoloader.php
  13. 1
    0
      lib/base.php
  14. 12
    0
      lib/composer/composer/autoload_classmap.php
  15. 12
    0
      lib/composer/composer/autoload_static.php
  16. 29
    4
      lib/private/AppFramework/App.php
  17. 5
    3
      lib/private/AppFramework/Utility/SimpleContainer.php
  18. 4
    0
      lib/private/Cache/CappedMemoryCache.php
  19. 4
    0
      lib/private/Cache/File.php
  20. 15
    0
      lib/private/DB/Connection.php
  21. 154
    0
      lib/private/DB/DbDataCollector.php
  22. 71
    0
      lib/private/DB/ObjectParameter.php
  23. 2
    1
      lib/private/Diagnostics/EventLogger.php
  24. 1
    4
      lib/private/Memcache/APCu.php
  25. 1
    1
      lib/private/Memcache/ArrayCache.php
  26. 63
    25
      lib/private/Memcache/Factory.php
  27. 177
    0
      lib/private/Memcache/LoggerWrapperCache.php
  28. 1
    1
      lib/private/Memcache/Memcached.php
  29. 1
    1
      lib/private/Memcache/NullCache.php
  30. 220
    0
      lib/private/Memcache/ProfilerWrapperCache.php
  31. 15
    109
      lib/private/Memcache/Redis.php
  32. 286
    0
      lib/private/Profiler/FileProfilerStorage.php
  33. 168
    0
      lib/private/Profiler/Profile.php
  34. 105
    0
      lib/private/Profiler/Profiler.php
  35. 55
    0
      lib/private/Profiler/RoutingDataCollector.php
  36. 12
    6
      lib/private/Server.php
  37. 87
    0
      lib/public/DataCollector/AbstractDataCollector.php
  38. 55
    0
      lib/public/DataCollector/IDataCollector.php
  39. 6
    0
      lib/public/ICache.php
  40. 168
    0
      lib/public/Profiler/IProfile.php
  41. 101
    0
      lib/public/Profiler/IProfiler.php
  42. 4
    1
      tests/lib/AppFramework/AppTest.php
  43. 9
    6
      tests/lib/Memcache/FactoryTest.php

+ 1
- 0
apps/dav/composer/composer/autoload_classmap.php 查看文件

@@ -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',

+ 1
- 0
apps/dav/composer/composer/autoload_static.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',

+ 47
- 0
apps/dav/lib/Profiler/ProfilerPlugin.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());
}
}

+ 21
- 10
apps/dav/lib/Server.php 查看文件

@@ -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 {

+ 1
- 0
apps/user_ldap/composer/composer/autoload_classmap.php 查看文件

@@ -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',

+ 1
- 0
apps/user_ldap/composer/composer/autoload_static.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',

+ 50
- 0
apps/user_ldap/lib/DataCollector/LdapDataCollector.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 {
}
}

+ 23
- 9
apps/user_ldap/lib/LDAP.php 查看文件

@@ -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",

+ 0
- 6
build/psalm-baseline.xml 查看文件

@@ -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-&gt;tar-&gt;extractInString($path)</code>

+ 8
- 0
config/config.sample.php 查看文件

@@ -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

+ 1
- 1
core/templates/layout.user.php 查看文件

@@ -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>

+ 3
- 2
lib/autoloader.php 查看文件

@@ -38,6 +38,7 @@ namespace OC;

use \OCP\AutoloadNotAllowedException;
use OCP\ILogger;
use OCP\ICache;

class Autoloader {
/** @var bool */
@@ -182,9 +183,9 @@ class Autoloader {
/**
* Sets the optional low-latency cache for class to path mapping.
*
* @param \OC\Memcache\Cache $memoryCache Instance of memory cache.
* @param ICache $memoryCache Instance of memory cache.
*/
public function setMemoryCache(\OC\Memcache\Cache $memoryCache = null): void {
public function setMemoryCache(ICache $memoryCache = null): void {
$this->memoryCache = $memoryCache;
}
}

+ 1
- 0
lib/base.php 查看文件

@@ -608,6 +608,7 @@ class OC {
$eventLogger->end('request');
});
$eventLogger->start('boot', 'Initialize');
$eventLogger->start('runtime', 'Runtime (total - autoloader)');

// Override php.ini and log everything if we're troubleshooting
if (self::$config->getValue('loglevel') === ILogger::DEBUG) {

+ 12
- 0
lib/composer/composer/autoload_classmap.php 查看文件

@@ -196,6 +196,8 @@ return array(
'OCP\\Dashboard\\RegisterWidgetEvent' => $baseDir . '/lib/public/Dashboard/RegisterWidgetEvent.php',
'OCP\\Dashboard\\Service\\IEventsService' => $baseDir . '/lib/public/Dashboard/Service/IEventsService.php',
'OCP\\Dashboard\\Service\\IWidgetsService' => $baseDir . '/lib/public/Dashboard/Service/IWidgetsService.php',
'OCP\\DataCollector\\AbstractDataCollector' => $baseDir . '/lib/public/DataCollector/AbstractDataCollector.php',
'OCP\\DataCollector\\IDataCollector' => $baseDir . '/lib/public/DataCollector/IDataCollector.php',
'OCP\\Defaults' => $baseDir . '/lib/public/Defaults.php',
'OCP\\Diagnostics\\IEvent' => $baseDir . '/lib/public/Diagnostics/IEvent.php',
'OCP\\Diagnostics\\IEventLogger' => $baseDir . '/lib/public/Diagnostics/IEventLogger.php',
@@ -467,6 +469,8 @@ return array(
'OCP\\Preview\\IVersionedPreviewFile' => $baseDir . '/lib/public/Preview/IVersionedPreviewFile.php',
'OCP\\Profile\\ILinkAction' => $baseDir . '/lib/public/Profile/ILinkAction.php',
'OCP\\Profile\\ParameterDoesNotExistException' => $baseDir . '/lib/public/Profile/ParameterDoesNotExistException.php',
'OCP\\Profiler\\IProfile' => $baseDir . '/lib/public/Profiler/IProfile.php',
'OCP\\Profiler\\IProfiler' => $baseDir . '/lib/public/Profiler/IProfiler.php',
'OCP\\Remote\\Api\\IApiCollection' => $baseDir . '/lib/public/Remote/Api/IApiCollection.php',
'OCP\\Remote\\Api\\IApiFactory' => $baseDir . '/lib/public/Remote/Api/IApiFactory.php',
'OCP\\Remote\\Api\\ICapabilitiesApi' => $baseDir . '/lib/public/Remote/Api/ICapabilitiesApi.php',
@@ -1025,6 +1029,7 @@ return array(
'OC\\DB\\Connection' => $baseDir . '/lib/private/DB/Connection.php',
'OC\\DB\\ConnectionAdapter' => $baseDir . '/lib/private/DB/ConnectionAdapter.php',
'OC\\DB\\ConnectionFactory' => $baseDir . '/lib/private/DB/ConnectionFactory.php',
'OC\\DB\\DbDataCollector' => $baseDir . '/lib/private/DB/DbDataCollector.php',
'OC\\DB\\Exceptions\\DbalException' => $baseDir . '/lib/private/DB/Exceptions/DbalException.php',
'OC\\DB\\MigrationException' => $baseDir . '/lib/private/DB/MigrationException.php',
'OC\\DB\\MigrationService' => $baseDir . '/lib/private/DB/MigrationService.php',
@@ -1035,6 +1040,7 @@ return array(
'OC\\DB\\MySQLMigrator' => $baseDir . '/lib/private/DB/MySQLMigrator.php',
'OC\\DB\\MySqlTools' => $baseDir . '/lib/private/DB/MySqlTools.php',
'OC\\DB\\OCSqlitePlatform' => $baseDir . '/lib/private/DB/OCSqlitePlatform.php',
'OC\\DB\\ObjectParameter' => $baseDir . '/lib/private/DB/ObjectParameter.php',
'OC\\DB\\OracleConnection' => $baseDir . '/lib/private/DB/OracleConnection.php',
'OC\\DB\\OracleMigrator' => $baseDir . '/lib/private/DB/OracleMigrator.php',
'OC\\DB\\PgSqlTools' => $baseDir . '/lib/private/DB/PgSqlTools.php',
@@ -1279,8 +1285,10 @@ return array(
'OC\\Memcache\\CASTrait' => $baseDir . '/lib/private/Memcache/CASTrait.php',
'OC\\Memcache\\Cache' => $baseDir . '/lib/private/Memcache/Cache.php',
'OC\\Memcache\\Factory' => $baseDir . '/lib/private/Memcache/Factory.php',
'OC\\Memcache\\LoggerWrapperCache' => $baseDir . '/lib/private/Memcache/LoggerWrapperCache.php',
'OC\\Memcache\\Memcached' => $baseDir . '/lib/private/Memcache/Memcached.php',
'OC\\Memcache\\NullCache' => $baseDir . '/lib/private/Memcache/NullCache.php',
'OC\\Memcache\\ProfilerWrapperCache' => $baseDir . '/lib/private/Memcache/ProfilerWrapperCache.php',
'OC\\Memcache\\Redis' => $baseDir . '/lib/private/Memcache/Redis.php',
'OC\\MemoryInfo' => $baseDir . '/lib/private/MemoryInfo.php',
'OC\\Migration\\BackgroundRepair' => $baseDir . '/lib/private/Migration/BackgroundRepair.php',
@@ -1347,6 +1355,10 @@ return array(
'OC\\Profile\\Actions\\WebsiteAction' => $baseDir . '/lib/private/Profile/Actions/WebsiteAction.php',
'OC\\Profile\\ProfileManager' => $baseDir . '/lib/private/Profile/ProfileManager.php',
'OC\\Profile\\TProfileHelper' => $baseDir . '/lib/private/Profile/TProfileHelper.php',
'OC\\Profiler\\FileProfilerStorage' => $baseDir . '/lib/private/Profiler/FileProfilerStorage.php',
'OC\\Profiler\\Profile' => $baseDir . '/lib/private/Profiler/Profile.php',
'OC\\Profiler\\Profiler' => $baseDir . '/lib/private/Profiler/Profiler.php',
'OC\\Profiler\\RoutingDataCollector' => $baseDir . '/lib/private/Profiler/RoutingDataCollector.php',
'OC\\RedisFactory' => $baseDir . '/lib/private/RedisFactory.php',
'OC\\Remote\\Api\\ApiBase' => $baseDir . '/lib/private/Remote/Api/ApiBase.php',
'OC\\Remote\\Api\\ApiCollection' => $baseDir . '/lib/private/Remote/Api/ApiCollection.php',

+ 12
- 0
lib/composer/composer/autoload_static.php 查看文件

@@ -225,6 +225,8 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c
'OCP\\Dashboard\\RegisterWidgetEvent' => __DIR__ . '/../../..' . '/lib/public/Dashboard/RegisterWidgetEvent.php',
'OCP\\Dashboard\\Service\\IEventsService' => __DIR__ . '/../../..' . '/lib/public/Dashboard/Service/IEventsService.php',
'OCP\\Dashboard\\Service\\IWidgetsService' => __DIR__ . '/../../..' . '/lib/public/Dashboard/Service/IWidgetsService.php',
'OCP\\DataCollector\\AbstractDataCollector' => __DIR__ . '/../../..' . '/lib/public/DataCollector/AbstractDataCollector.php',
'OCP\\DataCollector\\IDataCollector' => __DIR__ . '/../../..' . '/lib/public/DataCollector/IDataCollector.php',
'OCP\\Defaults' => __DIR__ . '/../../..' . '/lib/public/Defaults.php',
'OCP\\Diagnostics\\IEvent' => __DIR__ . '/../../..' . '/lib/public/Diagnostics/IEvent.php',
'OCP\\Diagnostics\\IEventLogger' => __DIR__ . '/../../..' . '/lib/public/Diagnostics/IEventLogger.php',
@@ -496,6 +498,8 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c
'OCP\\Preview\\IVersionedPreviewFile' => __DIR__ . '/../../..' . '/lib/public/Preview/IVersionedPreviewFile.php',
'OCP\\Profile\\ILinkAction' => __DIR__ . '/../../..' . '/lib/public/Profile/ILinkAction.php',
'OCP\\Profile\\ParameterDoesNotExistException' => __DIR__ . '/../../..' . '/lib/public/Profile/ParameterDoesNotExistException.php',
'OCP\\Profiler\\IProfile' => __DIR__ . '/../../..' . '/lib/public/Profiler/IProfile.php',
'OCP\\Profiler\\IProfiler' => __DIR__ . '/../../..' . '/lib/public/Profiler/IProfiler.php',
'OCP\\Remote\\Api\\IApiCollection' => __DIR__ . '/../../..' . '/lib/public/Remote/Api/IApiCollection.php',
'OCP\\Remote\\Api\\IApiFactory' => __DIR__ . '/../../..' . '/lib/public/Remote/Api/IApiFactory.php',
'OCP\\Remote\\Api\\ICapabilitiesApi' => __DIR__ . '/../../..' . '/lib/public/Remote/Api/ICapabilitiesApi.php',
@@ -1054,6 +1058,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c
'OC\\DB\\Connection' => __DIR__ . '/../../..' . '/lib/private/DB/Connection.php',
'OC\\DB\\ConnectionAdapter' => __DIR__ . '/../../..' . '/lib/private/DB/ConnectionAdapter.php',
'OC\\DB\\ConnectionFactory' => __DIR__ . '/../../..' . '/lib/private/DB/ConnectionFactory.php',
'OC\\DB\\DbDataCollector' => __DIR__ . '/../../..' . '/lib/private/DB/DbDataCollector.php',
'OC\\DB\\Exceptions\\DbalException' => __DIR__ . '/../../..' . '/lib/private/DB/Exceptions/DbalException.php',
'OC\\DB\\MigrationException' => __DIR__ . '/../../..' . '/lib/private/DB/MigrationException.php',
'OC\\DB\\MigrationService' => __DIR__ . '/../../..' . '/lib/private/DB/MigrationService.php',
@@ -1064,6 +1069,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c
'OC\\DB\\MySQLMigrator' => __DIR__ . '/../../..' . '/lib/private/DB/MySQLMigrator.php',
'OC\\DB\\MySqlTools' => __DIR__ . '/../../..' . '/lib/private/DB/MySqlTools.php',
'OC\\DB\\OCSqlitePlatform' => __DIR__ . '/../../..' . '/lib/private/DB/OCSqlitePlatform.php',
'OC\\DB\\ObjectParameter' => __DIR__ . '/../../..' . '/lib/private/DB/ObjectParameter.php',
'OC\\DB\\OracleConnection' => __DIR__ . '/../../..' . '/lib/private/DB/OracleConnection.php',
'OC\\DB\\OracleMigrator' => __DIR__ . '/../../..' . '/lib/private/DB/OracleMigrator.php',
'OC\\DB\\PgSqlTools' => __DIR__ . '/../../..' . '/lib/private/DB/PgSqlTools.php',
@@ -1308,8 +1314,10 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c
'OC\\Memcache\\CASTrait' => __DIR__ . '/../../..' . '/lib/private/Memcache/CASTrait.php',
'OC\\Memcache\\Cache' => __DIR__ . '/../../..' . '/lib/private/Memcache/Cache.php',
'OC\\Memcache\\Factory' => __DIR__ . '/../../..' . '/lib/private/Memcache/Factory.php',
'OC\\Memcache\\LoggerWrapperCache' => __DIR__ . '/../../..' . '/lib/private/Memcache/LoggerWrapperCache.php',
'OC\\Memcache\\Memcached' => __DIR__ . '/../../..' . '/lib/private/Memcache/Memcached.php',
'OC\\Memcache\\NullCache' => __DIR__ . '/../../..' . '/lib/private/Memcache/NullCache.php',
'OC\\Memcache\\ProfilerWrapperCache' => __DIR__ . '/../../..' . '/lib/private/Memcache/ProfilerWrapperCache.php',
'OC\\Memcache\\Redis' => __DIR__ . '/../../..' . '/lib/private/Memcache/Redis.php',
'OC\\MemoryInfo' => __DIR__ . '/../../..' . '/lib/private/MemoryInfo.php',
'OC\\Migration\\BackgroundRepair' => __DIR__ . '/../../..' . '/lib/private/Migration/BackgroundRepair.php',
@@ -1376,6 +1384,10 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c
'OC\\Profile\\Actions\\WebsiteAction' => __DIR__ . '/../../..' . '/lib/private/Profile/Actions/WebsiteAction.php',
'OC\\Profile\\ProfileManager' => __DIR__ . '/../../..' . '/lib/private/Profile/ProfileManager.php',
'OC\\Profile\\TProfileHelper' => __DIR__ . '/../../..' . '/lib/private/Profile/TProfileHelper.php',
'OC\\Profiler\\FileProfilerStorage' => __DIR__ . '/../../..' . '/lib/private/Profiler/FileProfilerStorage.php',
'OC\\Profiler\\Profile' => __DIR__ . '/../../..' . '/lib/private/Profiler/Profile.php',
'OC\\Profiler\\Profiler' => __DIR__ . '/../../..' . '/lib/private/Profiler/Profiler.php',
'OC\\Profiler\\RoutingDataCollector' => __DIR__ . '/../../..' . '/lib/private/Profiler/RoutingDataCollector.php',
'OC\\RedisFactory' => __DIR__ . '/../../..' . '/lib/private/RedisFactory.php',
'OC\\Remote\\Api\\ApiBase' => __DIR__ . '/../../..' . '/lib/private/Remote/Api/ApiBase.php',
'OC\\Remote\\Api\\ApiCollection' => __DIR__ . '/../../..' . '/lib/private/Remote/Api/ApiCollection.php',

+ 29
- 4
lib/private/AppFramework/App.php 查看文件

@@ -34,11 +34,16 @@ namespace OC\AppFramework;
use OC\AppFramework\DependencyInjection\DIContainer;
use OC\AppFramework\Http\Dispatcher;
use OC\AppFramework\Http\Request;
use OC\Diagnostics\EventLogger;
use OCP\Profiler\IProfiler;
use OC\Profiler\RoutingDataCollector;
use OCP\AppFramework\QueryException;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\ICallbackResponse;
use OCP\AppFramework\Http\IOutput;
use OCP\AppFramework\QueryException;
use OCP\Diagnostics\IEventLogger;
use OCP\HintException;
use OCP\IConfig;
use OCP\IRequest;

/**
@@ -114,20 +119,30 @@ class App {
* @throws HintException
*/
public static function main(string $controllerName, string $methodName, DIContainer $container, array $urlParams = null) {
/** @var IProfiler $profiler */
$profiler = $container->get(IProfiler::class);
$config = $container->get(IConfig::class);
// Disable profiler on the profiler UI
$profiler->setEnabled($profiler->isEnabled() && !is_null($urlParams) && isset($urlParams['_route']) && !str_starts_with($urlParams['_route'], 'profiler.'));
if ($profiler->isEnabled()) {
\OC::$server->get(IEventLogger::class)->activate();
$profiler->add(new RoutingDataCollector($container['AppName'], $controllerName, $methodName));
}

if (!is_null($urlParams)) {
/** @var Request $request */
$request = $container->query(IRequest::class);
$request = $container->get(IRequest::class);
$request->setUrlParameters($urlParams);
} elseif (isset($container['urlParams']) && !is_null($container['urlParams'])) {
/** @var Request $request */
$request = $container->query(IRequest::class);
$request = $container->get(IRequest::class);
$request->setUrlParameters($container['urlParams']);
}
$appName = $container['AppName'];

// first try $controllerName then go for \OCA\AppName\Controller\$controllerName
try {
$controller = $container->query($controllerName);
$controller = $container->get($controllerName);
} catch (QueryException $e) {
if (strpos($controllerName, '\\Controller\\') !== false) {
// This is from a global registered app route that is not enabled.
@@ -158,6 +173,16 @@ class App {

$io = $container[IOutput::class];

if ($profiler->isEnabled()) {
/** @var EventLogger $eventLogger */
$eventLogger = $container->get(IEventLogger::class);
$eventLogger->end('runtime');
$profile = $profiler->collect($container->get(IRequest::class), $response);
$profiler->saveProfile($profile);
$io->setHeader('X-Debug-Token:' . $profile->getToken());
$io->setHeader('Server-Timing: token;desc="' . $profile->getToken() . '"');
}

if (!is_null($httpHeaders)) {
$io->setHeader($httpHeaders);
}

+ 5
- 3
lib/private/AppFramework/Utility/SimpleContainer.php 查看文件

@@ -38,6 +38,7 @@ use Psr\Container\ContainerInterface;
use ReflectionClass;
use ReflectionException;
use ReflectionParameter;
use ReflectionNamedType;
use function class_exists;

/**
@@ -78,12 +79,13 @@ class SimpleContainer implements ArrayAccess, ContainerInterface, IContainer {
$resolveName = $parameter->getName();

// try to find out if it is a class or a simple parameter
if ($parameterType !== null && !$parameterType->isBuiltin()) {
if ($parameterType !== null && ($parameterType instanceof ReflectionNamedType) && !$parameterType->isBuiltin()) {
$resolveName = $parameterType->getName();
}

try {
$builtIn = $parameter->hasType() && $parameter->getType()->isBuiltin();
$builtIn = $parameter->hasType() && ($parameter->getType() instanceof ReflectionNamedType)
&& $parameter->getType()->isBuiltin();
return $this->query($resolveName, !$builtIn);
} catch (QueryException $e) {
// Service not found, use the default value when available
@@ -91,7 +93,7 @@ class SimpleContainer implements ArrayAccess, ContainerInterface, IContainer {
return $parameter->getDefaultValue();
}

if ($parameterType !== null && !$parameterType->isBuiltin()) {
if ($parameterType !== null && ($parameterType instanceof ReflectionNamedType) && !$parameterType->isBuiltin()) {
$resolveName = $parameter->getName();
try {
return $this->query($resolveName);

+ 4
- 0
lib/private/Cache/CappedMemoryCache.php 查看文件

@@ -115,4 +115,8 @@ class CappedMemoryCache implements ICache, \ArrayAccess {
$this->remove($key);
}
}

public static function isAvailable(): bool {
return true;
}
}

+ 4
- 0
lib/private/Cache/File.php 查看文件

@@ -203,4 +203,8 @@ class File implements ICache {
}
}
}

public static function isAvailable(): bool {
return true;
}
}

+ 15
- 0
lib/private/DB/Connection.php 查看文件

@@ -42,6 +42,7 @@ use Doctrine\DBAL\Driver;
use Doctrine\DBAL\Exception;
use Doctrine\DBAL\Exception\ConstraintViolationException;
use Doctrine\DBAL\Exception\NotNullConstraintViolationException;
use Doctrine\DBAL\Logging\DebugStack;
use Doctrine\DBAL\Platforms\MySQLPlatform;
use Doctrine\DBAL\Platforms\OraclePlatform;
use Doctrine\DBAL\Platforms\PostgreSQL94Platform;
@@ -55,6 +56,7 @@ use OCP\PreConditionNotMetException;
use OC\DB\QueryBuilder\QueryBuilder;
use OC\SystemConfig;
use Psr\Log\LoggerInterface;
use OCP\Profiler\IProfiler;

class Connection extends \Doctrine\DBAL\Connection {
/** @var string */
@@ -76,6 +78,9 @@ class Connection extends \Doctrine\DBAL\Connection {
/** @var int */
protected $queriesExecuted = 0;

/** @var DbDataCollector|null */
protected $dbDataCollector = null;

/**
* Initializes a new instance of the Connection class.
*
@@ -102,6 +107,16 @@ class Connection extends \Doctrine\DBAL\Connection {

$this->systemConfig = \OC::$server->getSystemConfig();
$this->logger = \OC::$server->get(LoggerInterface::class);

/** @var \OCP\Profiler\IProfiler */
$profiler = \OC::$server->get(IProfiler::class);
if ($profiler->isEnabled()) {
$this->dbDataCollector = new DbDataCollector($this);
$profiler->add($this->dbDataCollector);
$debugStack = new DebugStack();
$this->dbDataCollector->setDebugStack($debugStack);
$this->_config->setSQLLogger($debugStack);
}
}

/**

+ 154
- 0
lib/private/DB/DbDataCollector.php 查看文件

@@ -0,0 +1,154 @@
<?php

declare(strict_types = 1);
/**
* @copyright 2022 Carl Schwan <carl@carlschwan.eu>
*
* @author Carl Schwan <carl@carlschwan.eu>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

namespace OC\DB;

use Doctrine\DBAL\Logging\DebugStack;
use Doctrine\DBAL\Types\ConversionException;
use Doctrine\DBAL\Types\Type;
use OC\AppFramework\Http\Request;
use OCP\AppFramework\Http\Response;

class DbDataCollector extends \OCP\DataCollector\AbstractDataCollector {
protected ?DebugStack $debugStack = null;
private Connection $connection;

/**
* DbDataCollector constructor.
*/
public function __construct(Connection $connection) {
$this->connection = $connection;
}

public function setDebugStack(DebugStack $debugStack, $name = 'default'): void {
$this->debugStack = $debugStack;
}

/**
* @inheritDoc
*/
public function collect(Request $request, Response $response, \Throwable $exception = null): void {
$queries = $this->sanitizeQueries($this->debugStack->queries);

$this->data = [
'queries' => $queries,
];
}

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

public function getQueries(): array {
return $this->data['queries'];
}

private function sanitizeQueries(array $queries): array {
foreach ($queries as $i => $query) {
$queries[$i] = $this->sanitizeQuery($query);
}

return $queries;
}

private function sanitizeQuery(array $query): array {
$query['explainable'] = true;
$query['runnable'] = true;
if (null === $query['params']) {
$query['params'] = [];
}
if (!\is_array($query['params'])) {
$query['params'] = [$query['params']];
}
if (!\is_array($query['types'])) {
$query['types'] = [];
}
foreach ($query['params'] as $j => $param) {
$e = null;
if (isset($query['types'][$j])) {
// Transform the param according to the type
$type = $query['types'][$j];
if (\is_string($type)) {
$type = Type::getType($type);
}
if ($type instanceof Type) {
$query['types'][$j] = $type->getBindingType();
try {
$param = $type->convertToDatabaseValue($param, $this->connection->getDatabasePlatform());
} catch (\TypeError $e) {
} catch (ConversionException $e) {
}
}
}

[$query['params'][$j], $explainable, $runnable] = $this->sanitizeParam($param, $e);
if (!$explainable) {
$query['explainable'] = false;
}

if (!$runnable) {
$query['runnable'] = false;
}
}

return $query;
}

/**
* Sanitizes a param.
*
* The return value is an array with the sanitized value and a boolean
* indicating if the original value was kept (allowing to use the sanitized
* value to explain the query).
*/
private function sanitizeParam($var, ?\Throwable $error): array {
if (\is_object($var)) {
return [$o = new ObjectParameter($var, $error), false, $o->isStringable() && !$error];
}

if ($error) {
return ['⚠ '.$error->getMessage(), false, false];
}

if (\is_array($var)) {
$a = [];
$explainable = $runnable = true;
foreach ($var as $k => $v) {
[$value, $e, $r] = $this->sanitizeParam($v, null);
$explainable = $explainable && $e;
$runnable = $runnable && $r;
$a[$k] = $value;
}

return [$a, $explainable, $runnable];
}

if (\is_resource($var)) {
return [sprintf('/* Resource(%s) */', get_resource_type($var)), false, false];
}

return [$var, true, true];
}
}

+ 71
- 0
lib/private/DB/ObjectParameter.php 查看文件

@@ -0,0 +1,71 @@
<?php

declare(strict_types = 1);

/*
* This file is part of the Symfony package.
*
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
/**
* @copyright 2022 Carl Schwan <carl@carlschwan.eu>
*
* @author Carl Schwan <carl@carlschwan.eu>
* @author Fabien Potencier <fabien@symfony.com>
*
* @license AGPL-3.0-or-later AND MIT
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

namespace OC\DB;

final class ObjectParameter {
private $object;
private $error;
private $stringable;
private $class;

/**
* @param object $object
*/
public function __construct($object, ?\Throwable $error) {
$this->object = $object;
$this->error = $error;
$this->stringable = \is_callable([$object, '__toString']);
$this->class = \get_class($object);
}

/**
* @return object
*/
public function getObject() {
return $this->object;
}

public function getError(): ?\Throwable {
return $this->error;
}

public function isStringable(): bool {
return $this->stringable;
}

public function getClass(): string {
return $this->class;
}
}

+ 2
- 1
lib/private/Diagnostics/EventLogger.php 查看文件

@@ -60,7 +60,8 @@ class EventLogger implements IEventLogger {
}

public function isLoggingActivated(): bool {
$systemValue = (bool)$this->config->getValue('diagnostics.logging', false);
$systemValue = (bool)$this->config->getValue('diagnostics.logging', false)
|| (bool)$this->config->getValue('profiler', false);

if ($systemValue && $this->config->getValue('debug', false)) {
return true;

+ 1
- 4
lib/private/Memcache/APCu.php 查看文件

@@ -148,10 +148,7 @@ class APCu extends Cache implements IMemcache {
}
}

/**
* @return bool
*/
public static function isAvailable() {
public static function isAvailable(): bool {
if (!extension_loaded('apcu')) {
return false;
} elseif (!\OC::$server->get(IniGetWrapper::class)->getBool('apc.enabled')) {

+ 1
- 1
lib/private/Memcache/ArrayCache.php 查看文件

@@ -153,7 +153,7 @@ class ArrayCache extends Cache implements IMemcache {
/**
* {@inheritDoc}
*/
public static function isAvailable() {
public static function isAvailable(): bool {
return true;
}
}

+ 63
- 25
lib/private/Memcache/Factory.php 查看文件

@@ -31,6 +31,7 @@
*/
namespace OC\Memcache;

use OCP\Profiler\IProfiler;
use OCP\ICache;
use OCP\ICacheFactory;
use OCP\IMemcache;
@@ -39,39 +40,39 @@ use Psr\Log\LoggerInterface;
class Factory implements ICacheFactory {
public const NULL_CACHE = NullCache::class;

/**
* @var string $globalPrefix
*/
private $globalPrefix;
private string $globalPrefix;

private LoggerInterface $logger;

/**
* @var string $localCacheClass
* @var ?class-string<ICache> $localCacheClass
*/
private ?string $localCacheClass;

/**
* @var ?class-string<ICache> $distributedCacheClass
*/
private $localCacheClass;
private ?string $distributedCacheClass;

/**
* @var string $distributedCacheClass
* @var ?class-string<IMemcache> $lockingCacheClass
*/
private $distributedCacheClass;
private ?string $lockingCacheClass;

private string $logFile;

private IProfiler $profiler;

/**
* @var string $lockingCacheClass
* @param string $globalPrefix
* @param LoggerInterface $logger
* @param ?class-string<ICache> $localCacheClass
* @param ?class-string<ICache> $distributedCacheClass
* @param ?class-string<IMemcache> $lockingCacheClass
* @param string $logFile
*/
private $lockingCacheClass;

/** @var string */
private $logFile;

public function __construct(
string $globalPrefix,
LoggerInterface $logger,
?string $localCacheClass = null,
?string $distributedCacheClass = null,
?string $lockingCacheClass = null,
string $logFile = ''
) {
public function __construct(string $globalPrefix, LoggerInterface $logger, IProfiler $profiler,
?string $localCacheClass = null, ?string $distributedCacheClass = null, ?string $lockingCacheClass = null, string $logFile = '') {
$this->logger = $logger;
$this->logFile = $logFile;
$this->globalPrefix = $globalPrefix;
@@ -103,6 +104,7 @@ class Factory implements ICacheFactory {
$this->localCacheClass = $localCacheClass;
$this->distributedCacheClass = $distributedCacheClass;
$this->lockingCacheClass = $lockingCacheClass;
$this->profiler = $profiler;
}

/**
@@ -112,7 +114,19 @@ class Factory implements ICacheFactory {
* @return IMemcache
*/
public function createLocking(string $prefix = ''): IMemcache {
return new $this->lockingCacheClass($this->globalPrefix . '/' . $prefix, $this->logFile);
assert($this->lockingCacheClass !== null);
$cache = new $this->lockingCacheClass($this->globalPrefix . '/' . $prefix);
if ($this->profiler->isEnabled() && $this->lockingCacheClass === '\OC\Memcache\Redis') {
// We only support the profiler with Redis
$cache = new ProfilerWrapperCache($cache, 'Locking');
$this->profiler->add($cache);
}

if ($this->lockingCacheClass === Redis::class &&
$this->logFile !== '' && is_writable(dirname($this->logFile)) && (!file_exists($this->logFile) || is_writable($this->logFile))) {
$cache = new LoggerWrapperCache($cache, $this->logFile);
}
return $cache;
}

/**
@@ -122,7 +136,19 @@ class Factory implements ICacheFactory {
* @return ICache
*/
public function createDistributed(string $prefix = ''): ICache {
return new $this->distributedCacheClass($this->globalPrefix . '/' . $prefix, $this->logFile);
assert($this->distributedCacheClass !== null);
$cache = new $this->distributedCacheClass($this->globalPrefix . '/' . $prefix);
if ($this->profiler->isEnabled() && $this->distributedCacheClass === '\OC\Memcache\Redis') {
// We only support the profiler with Redis
$cache = new ProfilerWrapperCache($cache, 'Distributed');
$this->profiler->add($cache);
}

if ($this->distributedCacheClass === Redis::class && $this->logFile !== ''
&& is_writable(dirname($this->logFile)) && (!file_exists($this->logFile) || is_writable($this->logFile))) {
$cache = new LoggerWrapperCache($cache, $this->logFile);
}
return $cache;
}

/**
@@ -132,7 +158,19 @@ class Factory implements ICacheFactory {
* @return ICache
*/
public function createLocal(string $prefix = ''): ICache {
return new $this->localCacheClass($this->globalPrefix . '/' . $prefix, $this->logFile);
assert($this->localCacheClass !== null);
$cache = new $this->localCacheClass($this->globalPrefix . '/' . $prefix);
if ($this->profiler->isEnabled() && $this->localCacheClass === '\OC\Memcache\Redis') {
// We only support the profiler with Redis
$cache = new ProfilerWrapperCache($cache, 'Local');
$this->profiler->add($cache);
}

if ($this->localCacheClass === Redis::class && $this->logFile !== ''
&& is_writable(dirname($this->logFile)) && (!file_exists($this->logFile) || is_writable($this->logFile))) {
$cache = new LoggerWrapperCache($cache, $this->logFile);
}
return $cache;
}

/**

+ 177
- 0
lib/private/Memcache/LoggerWrapperCache.php 查看文件

@@ -0,0 +1,177 @@
<?php

declare(strict_types = 1);
/**
* @copyright 2022 Carl Schwan <carl@carlschwan.eu>
*
* @author Carl Schwan <carl@carlschwan.eu>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

namespace OC\Memcache;

use OCP\IMemcacheTTL;

/**
* Cache wrapper that logs the cache operation in a log file
*/
class LoggerWrapperCache extends Cache implements IMemcacheTTL {
/** @var Redis */
protected $wrappedCache;

/** @var string $logFile */
private $logFile;

/** @var string $prefix */
protected $prefix;

public function __construct(Redis $wrappedCache, string $logFile) {
parent::__construct($wrappedCache->getPrefix());
$this->wrappedCache = $wrappedCache;
$this->logFile = $logFile;
}

/**
* @return string Prefix used for caching purposes
*/
public function getPrefix() {
return $this->prefix;
}

protected function getNameSpace() {
return $this->prefix;
}

/** @inheritDoc */
public function get($key) {
file_put_contents(
$this->logFile,
$this->getNameSpace() . '::get::' . $key . "\n",
FILE_APPEND
);
return $this->wrappedCache->get($key);
}

/** @inheritDoc */
public function set($key, $value, $ttl = 0) {
file_put_contents(
$this->logFile,
$this->getNameSpace() . '::set::' . $key . '::' . $ttl . '::' . json_encode($value) . "\n",
FILE_APPEND
);

return $this->wrappedCache->set($key, $value, $$ttl);
}

/** @inheritDoc */
public function hasKey($key) {
file_put_contents(
$this->logFile,
$this->getNameSpace() . '::hasKey::' . $key . "\n",
FILE_APPEND
);

return $this->wrappedCache->hasKey($key);
}

/** @inheritDoc */
public function remove($key) {
file_put_contents(
$this->logFile,
$this->getNameSpace() . '::remove::' . $key . "\n",
FILE_APPEND
);

return $this->wrappedCache->remove($key);
}

/** @inheritDoc */
public function clear($prefix = '') {
file_put_contents(
$this->logFile,
$this->getNameSpace() . '::clear::' . $prefix . "\n",
FILE_APPEND
);

return $this->wrappedCache->clear($prefix);
}

/** @inheritDoc */
public function add($key, $value, $ttl = 0) {
file_put_contents(
$this->logFile,
$this->getNameSpace() . '::add::' . $key . '::' . $value . "\n",
FILE_APPEND
);

return $this->wrappedCache->add($key, $value, $ttl);
}

/** @inheritDoc */
public function inc($key, $step = 1) {
file_put_contents(
$this->logFile,
$this->getNameSpace() . '::inc::' . $key . "\n",
FILE_APPEND
);

return $this->wrappedCache->inc($key, $step);
}

/** @inheritDoc */
public function dec($key, $step = 1) {
file_put_contents(
$this->logFile,
$this->getNameSpace() . '::dec::' . $key . "\n",
FILE_APPEND
);

return $this->wrappedCache->dec($key, $step);
}

/** @inheritDoc */
public function cas($key, $old, $new) {
file_put_contents(
$this->logFile,
$this->getNameSpace() . '::cas::' . $key . "\n",
FILE_APPEND
);

return $this->wrappedCache->cas($key, $old, $new);
}

/** @inheritDoc */
public function cad($key, $old) {
file_put_contents(
$this->logFile,
$this->getNameSpace() . '::cad::' . $key . "\n",
FILE_APPEND
);

return $this->wrappedCache->cad($key, $old);
}

/** @inheritDoc */
public function setTTL($key, $ttl) {
$this->wrappedCache->setTTL($key, $ttl);
}

public static function isAvailable(): bool {
return true;
}
}

+ 1
- 1
lib/private/Memcache/Memcached.php 查看文件

@@ -196,7 +196,7 @@ class Memcached extends Cache implements IMemcache {
return $result;
}

public static function isAvailable() {
public static function isAvailable(): bool {
return extension_loaded('memcached');
}


+ 1
- 1
lib/private/Memcache/NullCache.php 查看文件

@@ -67,7 +67,7 @@ class NullCache extends Cache implements \OCP\IMemcache {
return true;
}

public static function isAvailable() {
public static function isAvailable(): bool {
return true;
}
}

+ 220
- 0
lib/private/Memcache/ProfilerWrapperCache.php 查看文件

@@ -0,0 +1,220 @@
<?php

declare(strict_types = 1);
/**
* @copyright 2022 Carl Schwan <carl@carlschwan.eu>
*
* @author Carl Schwan <carl@carlschwan.eu>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

namespace OC\Memcache;

use OC\AppFramework\Http\Request;
use OCP\AppFramework\Http\Response;
use OCP\DataCollector\AbstractDataCollector;
use OCP\IMemcacheTTL;

/**
* Cache wrapper that logs profiling information
*/
class ProfilerWrapperCache extends AbstractDataCollector implements IMemcacheTTL, \ArrayAccess {
/** @var Redis $wrappedCache*/
protected $wrappedCache;

/** @var string $prefix */
protected $prefix;

/** @var string $type */
private $type;

public function __construct(Redis $wrappedCache, string $type) {
$this->prefix = $wrappedCache->getPrefix();
$this->wrappedCache = $wrappedCache;
$this->type = $type;
$this->data['queries'] = [];
$this->data['cacheHit'] = 0;
$this->data['cacheMiss'] = 0;
}

public function getPrefix(): string {
return $this->prefix;
}

/** @inheritDoc */
public function get($key) {
$start = microtime(true);
$ret = $this->wrappedCache->get($key);
if ($ret === null) {
$this->data['cacheMiss']++;
} else {
$this->data['cacheHit']++;
}
$this->data['queries'][] = [
'start' => $start,
'end' => microtime(true),
'op' => $this->getPrefix() . '::get::' . $key,
];
return $ret;
}

/** @inheritDoc */
public function set($key, $value, $ttl = 0) {
$start = microtime(true);
$ret = $this->wrappedCache->set($key, $value, $ttl);
$this->data['queries'][] = [
'start' => $start,
'end' => microtime(true),
'op' => $this->getPrefix() . '::set::' . $key,
];
return $ret;
}

/** @inheritDoc */
public function hasKey($key) {
$start = microtime(true);
$ret = $this->wrappedCache->hasKey($key);
$this->data['queries'][] = [
'start' => $start,
'end' => microtime(true),
'op' => $this->getPrefix() . '::hasKey::' . $key,
];
return $ret;
}

/** @inheritDoc */
public function remove($key) {
$start = microtime(true);
$ret = $this->wrappedCache->remove($key);
$this->data['queries'][] = [
'start' => $start,
'end' => microtime(true),
'op' => $this->getPrefix() . '::remove::' . $key,
];
return $ret;
}

/** @inheritDoc */
public function clear($prefix = '') {
$start = microtime(true);
$ret = $this->wrappedCache->clear($prefix);
$this->data['queries'][] = [
'start' => $start,
'end' => microtime(true),
'op' => $this->getPrefix() . '::clear::' . $prefix,
];
return $ret;
}

/** @inheritDoc */
public function add($key, $value, $ttl = 0) {
$start = microtime(true);
$ret = $this->wrappedCache->add($key, $value, $ttl);
$this->data['queries'][] = [
'start' => $start,
'end' => microtime(true),
'op' => $this->getPrefix() . '::add::' . $key,
];
return $ret;
}

/** @inheritDoc */
public function inc($key, $step = 1) {
$start = microtime(true);
$ret = $this->wrappedCache->inc($key, $step);
$this->data['queries'][] = [
'start' => $start,
'end' => microtime(true),
'op' => $this->getPrefix() . '::inc::' . $key,
];
return $ret;
}

/** @inheritDoc */
public function dec($key, $step = 1) {
$start = microtime(true);
$ret = $this->wrappedCache->dec($key, $step);
$this->data['queries'][] = [
'start' => $start,
'end' => microtime(true),
'op' => $this->getPrefix() . '::dev::' . $key,
];
return $ret;
}

/** @inheritDoc */
public function cas($key, $old, $new) {
$start = microtime(true);
$ret = $this->wrappedCache->cas($key, $old, $new);
$this->data['queries'][] = [
'start' => $start,
'end' => microtime(true),
'op' => $this->getPrefix() . '::cas::' . $key,
];
return $ret;
}

/** @inheritDoc */
public function cad($key, $old) {
$start = microtime(true);
$ret = $this->wrappedCache->cad($key, $old);
$this->data['queries'][] = [
'start' => $start,
'end' => microtime(true),
'op' => $this->getPrefix() . '::cad::' . $key,
];
return $ret;
}

/** @inheritDoc */
public function setTTL($key, $ttl) {
$this->wrappedCache->setTTL($key, $ttl);
}

public function offsetExists($offset): bool {
return $this->hasKey($offset);
}

public function offsetSet($offset, $value): void {
$this->set($offset, $value);
}

/**
* @return mixed
*/
#[\ReturnTypeWillChange]
public function offsetGet($offset) {
return $this->get($offset);
}

public function offsetUnset($offset): void {
$this->remove($offset);
}

public function collect(Request $request, Response $response, \Throwable $exception = null): void {
// Nothing to do here $data is already ready
}

public function getName(): string {
return 'cache/' . $this->type . '/' . $this->prefix;
}

public static function isAvailable(): bool {
return true;
}
}

+ 15
- 109
lib/private/Memcache/Redis.php 查看文件

@@ -37,38 +37,16 @@ class Redis extends Cache implements IMemcacheTTL {
*/
private static $cache = null;

private $logFile;

public function __construct($prefix = '', string $logFile = '') {
parent::__construct($prefix);
$this->logFile = $logFile;
if (is_null(self::$cache)) {
self::$cache = \OC::$server->getGetRedisFactory()->getInstance();
}
}

/**
* entries in redis get namespaced to prevent collisions between ownCloud instances and users
*/
protected function getNameSpace() {
return $this->prefix;
}

private function logEnabled(): bool {
return $this->logFile !== '' && is_writable(dirname($this->logFile)) && (!file_exists($this->logFile) || is_writable($this->logFile));
}

public function get($key) {
if ($this->logEnabled()) {
file_put_contents(
$this->logFile,
$this->getNameSpace() . '::get::' . $key . "\n",
FILE_APPEND
);
}

$result = self::$cache->get($this->getNameSpace() . $key);
if ($result === false && !self::$cache->exists($this->getNameSpace() . $key)) {
$result = self::$cache->get($this->getPrefix() . $key);
if ($result === false && !self::$cache->exists($this->getPrefix() . $key)) {
return null;
} else {
return json_decode($result, true);
@@ -76,43 +54,19 @@ class Redis extends Cache implements IMemcacheTTL {
}

public function set($key, $value, $ttl = 0) {
if ($this->logEnabled()) {
file_put_contents(
$this->logFile,
$this->getNameSpace() . '::set::' . $key . '::' . $ttl . '::' . json_encode($value) . "\n",
FILE_APPEND
);
}

if ($ttl > 0) {
return self::$cache->setex($this->getNameSpace() . $key, $ttl, json_encode($value));
return self::$cache->setex($this->getPrefix() . $key, $ttl, json_encode($value));
} else {
return self::$cache->set($this->getNameSpace() . $key, json_encode($value));
return self::$cache->set($this->getPrefix() . $key, json_encode($value));
}
}

public function hasKey($key) {
if ($this->logEnabled()) {
file_put_contents(
$this->logFile,
$this->getNameSpace() . '::hasKey::' . $key . "\n",
FILE_APPEND
);
}

return (bool)self::$cache->exists($this->getNameSpace() . $key);
return (bool)self::$cache->exists($this->getPrefix() . $key);
}

public function remove($key) {
if ($this->logEnabled()) {
file_put_contents(
$this->logFile,
$this->getNameSpace() . '::remove::' . $key . "\n",
FILE_APPEND
);
}

if (self::$cache->del($this->getNameSpace() . $key)) {
if (self::$cache->del($this->getPrefix() . $key)) {
return true;
} else {
return false;
@@ -120,15 +74,7 @@ class Redis extends Cache implements IMemcacheTTL {
}

public function clear($prefix = '') {
if ($this->logEnabled()) {
file_put_contents(
$this->logFile,
$this->getNameSpace() . '::clear::' . $prefix . "\n",
FILE_APPEND
);
}

$prefix = $this->getNameSpace() . $prefix . '*';
$prefix = $this->getPrefix() . $prefix . '*';
$keys = self::$cache->keys($prefix);
$deleted = self::$cache->del($keys);

@@ -153,14 +99,6 @@ class Redis extends Cache implements IMemcacheTTL {
if ($ttl !== 0 && is_int($ttl)) {
$args['ex'] = $ttl;
}
if ($this->logEnabled()) {
file_put_contents(
$this->logFile,
$this->getNameSpace() . '::add::' . $key . '::' . $value . "\n",
FILE_APPEND
);
}


return self::$cache->set($this->getPrefix() . $key, $value, $args);
}
@@ -173,15 +111,7 @@ class Redis extends Cache implements IMemcacheTTL {
* @return int | bool
*/
public function inc($key, $step = 1) {
if ($this->logEnabled()) {
file_put_contents(
$this->logFile,
$this->getNameSpace() . '::inc::' . $key . "\n",
FILE_APPEND
);
}

return self::$cache->incrBy($this->getNameSpace() . $key, $step);
return self::$cache->incrBy($this->getPrefix() . $key, $step);
}

/**
@@ -192,18 +122,10 @@ class Redis extends Cache implements IMemcacheTTL {
* @return int | bool
*/
public function dec($key, $step = 1) {
if ($this->logEnabled()) {
file_put_contents(
$this->logFile,
$this->getNameSpace() . '::dec::' . $key . "\n",
FILE_APPEND
);
}

if (!$this->hasKey($key)) {
return false;
}
return self::$cache->decrBy($this->getNameSpace() . $key, $step);
return self::$cache->decrBy($this->getPrefix() . $key, $step);
}

/**
@@ -215,21 +137,13 @@ class Redis extends Cache implements IMemcacheTTL {
* @return bool
*/
public function cas($key, $old, $new) {
if ($this->logEnabled()) {
file_put_contents(
$this->logFile,
$this->getNameSpace() . '::cas::' . $key . "\n",
FILE_APPEND
);
}

if (!is_int($new)) {
$new = json_encode($new);
}
self::$cache->watch($this->getNameSpace() . $key);
self::$cache->watch($this->getPrefix() . $key);
if ($this->get($key) === $old) {
$result = self::$cache->multi()
->set($this->getNameSpace() . $key, $new)
->set($this->getPrefix() . $key, $new)
->exec();
return $result !== false;
}
@@ -245,18 +159,10 @@ class Redis extends Cache implements IMemcacheTTL {
* @return bool
*/
public function cad($key, $old) {
if ($this->logEnabled()) {
file_put_contents(
$this->logFile,
$this->getNameSpace() . '::cad::' . $key . "\n",
FILE_APPEND
);
}

self::$cache->watch($this->getNameSpace() . $key);
self::$cache->watch($this->getPrefix() . $key);
if ($this->get($key) === $old) {
$result = self::$cache->multi()
->del($this->getNameSpace() . $key)
->del($this->getPrefix() . $key)
->exec();
return $result !== false;
}
@@ -265,10 +171,10 @@ class Redis extends Cache implements IMemcacheTTL {
}

public function setTTL($key, $ttl) {
self::$cache->expire($this->getNameSpace() . $key, $ttl);
self::$cache->expire($this->getPrefix() . $key, $ttl);
}

public static function isAvailable() {
public static function isAvailable(): bool {
return \OC::$server->getGetRedisFactory()->isAvailable();
}
}

+ 286
- 0
lib/private/Profiler/FileProfilerStorage.php 查看文件

@@ -0,0 +1,286 @@
<?php

declare(strict_types = 1);
/**
* @copyright 2022 Carl Schwan <carl@carlschwan.eu>
*
* @author Carl Schwan <carl@carlschwan.eu>
* @author Alexandre Salomé <alexandre.salome@gmail.com>
*
* @license AGPL-3.0-or-later AND MIT
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

namespace OC\Profiler;

use OCP\Profiler\IProfile;

/**
* Storage for profiler using files.
*/
class FileProfilerStorage {
// Folder where profiler data are stored.
private string $folder;

/**
* Constructs the file storage using a "dsn-like" path.
*
* Example : "file:/path/to/the/storage/folder"
*
* @throws \RuntimeException
*/
public function __construct(string $folder) {
$this->folder = $folder;

if (!is_dir($this->folder) && false === @mkdir($this->folder, 0777, true) && !is_dir($this->folder)) {
throw new \RuntimeException(sprintf('Unable to create the storage directory (%s).', $this->folder));
}
}

public function find(?string $url, ?int $limit, ?string $method, int $start = null, int $end = null, string $statusCode = null): array {
$file = $this->getIndexFilename();

if (!file_exists($file)) {
return [];
}

$file = fopen($file, 'r');
fseek($file, 0, \SEEK_END);

$result = [];
while (\count($result) < $limit && $line = $this->readLineFromFile($file)) {
$values = str_getcsv($line);
[$csvToken, $csvMethod, $csvUrl, $csvTime, $csvParent, $csvStatusCode] = $values;
$csvTime = (int) $csvTime;

if ($url && false === strpos($csvUrl, $url) || $method && false === strpos($csvMethod, $method) || $statusCode && false === strpos($csvStatusCode, $statusCode)) {
continue;
}

if (!empty($start) && $csvTime < $start) {
continue;
}

if (!empty($end) && $csvTime > $end) {
continue;
}

$result[$csvToken] = [
'token' => $csvToken,
'method' => $csvMethod,
'url' => $csvUrl,
'time' => $csvTime,
'parent' => $csvParent,
'status_code' => $csvStatusCode,
];
}

fclose($file);

return array_values($result);
}

public function purge(): void {
$flags = \FilesystemIterator::SKIP_DOTS;
$iterator = new \RecursiveDirectoryIterator($this->folder, $flags);
$iterator = new \RecursiveIteratorIterator($iterator, \RecursiveIteratorIterator::CHILD_FIRST);

foreach ($iterator as $file) {
if (is_file($file)) {
unlink($file);
} else {
rmdir($file);
}
}
}

public function read(string $token): ?IProfile {
if (!$token || !file_exists($file = $this->getFilename($token))) {
return null;
}

if (\function_exists('gzcompress')) {
$file = 'compress.zlib://'.$file;
}

return $this->createProfileFromData($token, unserialize(file_get_contents($file)));
}

/**
* @throws \RuntimeException
*/
public function write(IProfile $profile): bool {
$file = $this->getFilename($profile->getToken());

$profileIndexed = is_file($file);
if (!$profileIndexed) {
// Create directory
$dir = \dirname($file);
if (!is_dir($dir) && false === @mkdir($dir, 0777, true) && !is_dir($dir)) {
throw new \RuntimeException(sprintf('Unable to create the storage directory (%s).', $dir));
}
}

$profileToken = $profile->getToken();
// when there are errors in sub-requests, the parent and/or children tokens
// may equal the profile token, resulting in infinite loops
$parentToken = $profile->getParentToken() !== $profileToken ? $profile->getParentToken() : null;
$childrenToken = array_filter(array_map(function (IProfile $p) use ($profileToken) {
return $profileToken !== $p->getToken() ? $p->getToken() : null;
}, $profile->getChildren()));

// Store profile
$data = [
'token' => $profileToken,
'parent' => $parentToken,
'children' => $childrenToken,
'data' => $profile->getCollectors(),
'method' => $profile->getMethod(),
'url' => $profile->getUrl(),
'time' => $profile->getTime(),
'status_code' => $profile->getStatusCode(),
];

$context = stream_context_create();

if (\function_exists('gzcompress')) {
$file = 'compress.zlib://'.$file;
stream_context_set_option($context, 'zlib', 'level', 3);
}

if (false === file_put_contents($file, serialize($data), 0, $context)) {
return false;
}

if (!$profileIndexed) {
// Add to index
if (false === $file = fopen($this->getIndexFilename(), 'a')) {
return false;
}

fputcsv($file, [
$profile->getToken(),
$profile->getMethod(),
$profile->getUrl(),
$profile->getTime(),
$profile->getParentToken(),
$profile->getStatusCode(),
]);
fclose($file);
}

return true;
}

/**
* Gets filename to store data, associated to the token.
*
* @return string The profile filename
*/
protected function getFilename(string $token): string {
// Uses 4 last characters, because first are mostly the same.
$folderA = substr($token, -2, 2);
$folderB = substr($token, -4, 2);

return $this->folder.'/'.$folderA.'/'.$folderB.'/'.$token;
}

/**
* Gets the index filename.
*
* @return string The index filename
*/
protected function getIndexFilename(): string {
return $this->folder.'/index.csv';
}

/**
* Reads a line in the file, backward.
*
* This function automatically skips the empty lines and do not include the line return in result value.
*
* @param resource $file The file resource, with the pointer placed at the end of the line to read
*
* @return ?string A string representing the line or null if beginning of file is reached
*/
protected function readLineFromFile($file): ?string {
$line = '';
$position = ftell($file);

if (0 === $position) {
return null;
}

while (true) {
$chunkSize = min($position, 1024);
$position -= $chunkSize;
fseek($file, $position);

if (0 === $chunkSize) {
// bof reached
break;
}

$buffer = fread($file, $chunkSize);

if (false === ($upTo = strrpos($buffer, "\n"))) {
$line = $buffer.$line;
continue;
}

$position += $upTo;
$line = substr($buffer, $upTo + 1).$line;
fseek($file, max(0, $position), \SEEK_SET);

if ('' !== $line) {
break;
}
}

return '' === $line ? null : $line;
}

protected function createProfileFromData(string $token, array $data, IProfile $parent = null): IProfile {
$profile = new Profile($token);
$profile->setMethod($data['method']);
$profile->setUrl($data['url']);
$profile->setTime($data['time']);
$profile->setStatusCode($data['status_code']);
$profile->setCollectors($data['data']);

if (!$parent && $data['parent']) {
$parent = $this->read($data['parent']);
}

if ($parent) {
$profile->setParent($parent);
}

foreach ($data['children'] as $token) {
if (!$token || !file_exists($file = $this->getFilename($token))) {
continue;
}

if (\function_exists('gzcompress')) {
$file = 'compress.zlib://'.$file;
}

$profile->addChild($this->createProfileFromData($token, unserialize(file_get_contents($file)), $profile));
}

return $profile;
}
}

+ 168
- 0
lib/private/Profiler/Profile.php 查看文件

@@ -0,0 +1,168 @@
<?php

declare(strict_types = 1);
/**
* @copyright 2022 Carl Schwan <carl@carlschwan.eu>
*
* @author Carl Schwan <carl@carlschwan.eu>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

namespace OC\Profiler;

use OCP\DataCollector\IDataCollector;
use OCP\Profiler\IProfile;

class Profile implements \JsonSerializable, IProfile {
private string $token;

private ?int $time = null;

private ?string $url = null;

private ?string $method = null;

private ?int $statusCode = null;

/** @var array<string, IDataCollector> */
private array $collectors = [];

private ?IProfile $parent = null;

/** @var IProfile[] */
private array $children = [];

public function __construct(string $token) {
$this->token = $token;
}

public function getToken(): string {
return $this->token;
}

public function setToken(string $token): void {
$this->token = $token;
}

public function getTime(): ?int {
return $this->time;
}

public function setTime(int $time): void {
$this->time = $time;
}

public function getUrl(): ?string {
return $this->url;
}

public function setUrl(string $url): void {
$this->url = $url;
}

public function getMethod(): ?string {
return $this->method;
}

public function setMethod(string $method): void {
$this->method = $method;
}

public function getStatusCode(): ?int {
return $this->statusCode;
}

public function setStatusCode(int $statusCode): void {
$this->statusCode = $statusCode;
}

public function addCollector(IDataCollector $collector) {
$this->collectors[$collector->getName()] = $collector;
}

public function getParent(): ?IProfile {
return $this->parent;
}

public function setParent(?IProfile $parent): void {
$this->parent = $parent;
}

public function getParentToken(): ?string {
return $this->parent ? $this->parent->getToken() : null;
}

/** @return IProfile[] */
public function getChildren(): array {
return $this->children;
}

/**
* @param IProfile[] $children
*/
public function setChildren(array $children): void {
$this->children = [];
foreach ($children as $child) {
$this->addChild($child);
}
}

public function addChild(IProfile $profile): void {
$this->children[] = $profile;
$profile->setParent($this);
}

/**
* @return IDataCollector[]
*/
public function getCollectors(): array {
return $this->collectors;
}

/**
* @param IDataCollector[] $collectors
*/
public function setCollectors(array $collectors): void {
$this->collectors = $collectors;
}

public function __sleep(): array {
return ['token', 'parent', 'children', 'collectors', 'method', 'url', 'time', 'statusCode'];
}

#[\ReturnTypeWillChange]
public function jsonSerialize() {
// Everything but parent
return [
'token' => $this->token,
'method' => $this->method,
'children' => $this->children,
'url' => $this->url,
'statusCode' => $this->statusCode,
'time' => $this->time,
'collectors' => $this->collectors,
];
}

public function getCollector(string $collectorName): ?IDataCollector {
if (!array_key_exists($collectorName, $this->collectors)) {
return null;
}
return $this->collectors[$collectorName];
}
}

+ 105
- 0
lib/private/Profiler/Profiler.php 查看文件

@@ -0,0 +1,105 @@
<?php

declare(strict_types = 1);

/**
* @copyright 2021 Carl Schwan <carl@carlschwan.eu>
*
* @author Carl Schwan <carl@carlschwan.eu>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

namespace OC\Profiler;

use OC\AppFramework\Http\Request;
use OCP\AppFramework\Http\Response;
use OCP\DataCollector\IDataCollector;
use OCP\Profiler\IProfiler;
use OCP\Profiler\IProfile;
use OC\SystemConfig;

class Profiler implements IProfiler {
/** @var array<string, IDataCollector> */
private array $dataCollectors = [];

private ?FileProfilerStorage $storage = null;

private bool $enabled = false;

public function __construct(SystemConfig $config) {
$this->enabled = $config->getValue('profiler', false);
if ($this->enabled) {
$this->storage = new FileProfilerStorage($config->getValue('datadirectory', \OC::$SERVERROOT . '/data') . '/profiler');
}
}

public function add(IDataCollector $dataCollector): void {
$this->dataCollectors[$dataCollector->getName()] = $dataCollector;
}

public function loadProfileFromResponse(Response $response): ?IProfile {
if (!$token = $response->getHeaders()['X-Debug-Token']) {
return null;
}

return $this->loadProfile($token);
}

public function loadProfile(string $token): ?IProfile {
return $this->storage->read($token);
}

public function saveProfile(IProfile $profile): bool {
return $this->storage->write($profile);
}

public function collect(Request $request, Response $response): IProfile {
$profile = new Profile($request->getId());
$profile->setTime(time());
$profile->setUrl($request->getRequestUri());
$profile->setMethod($request->getMethod());
$profile->setStatusCode($response->getStatus());
foreach ($this->dataCollectors as $dataCollector) {
$dataCollector->collect($request, $response, null);

// We clone for subrequests
$profile->addCollector(clone $dataCollector);
}
return $profile;
}

/**
* @return array[]
*/
public function find(?string $url, ?int $limit, ?string $method, ?int $start, ?int $end,
string $statusCode = null): array {
return $this->storage->find($url, $limit, $method, $start, $end, $statusCode);
}

public function dataProviders(): array {
return array_keys($this->dataCollectors);
}

public function isEnabled(): bool {
return $this->enabled;
}

public function setEnabled(bool $enabled): void {
$this->enabled = $enabled;
}
}

+ 55
- 0
lib/private/Profiler/RoutingDataCollector.php 查看文件

@@ -0,0 +1,55 @@
<?php

declare(strict_types=1);

/**
* @copyright 2022 Carl Schwan <carl@carlschwan.eu>
*
* @author Carl Schwan <carl@carlschwan.eu>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

namespace OC\Profiler;

use OC\AppFramework\Http\Request;
use OCP\AppFramework\Http\Response;
use OCP\DataCollector\AbstractDataCollector;

class RoutingDataCollector extends AbstractDataCollector {
private string $appName;
private string $controllerName;
private string $actionName;

public function __construct(string $appName, string $controllerName, string $actionName) {
$this->appName = $appName;
$this->controllerName = $controllerName;
$this->actionName = $actionName;
}

public function collect(Request $request, Response $response, \Throwable $exception = null): void {
$this->data = [
'appName' => $this->appName,
'controllerName' => $this->controllerName,
'actionName' => $this->actionName,
];
}

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

+ 12
- 6
lib/private/Server.php 查看文件

@@ -260,6 +260,8 @@ use OCA\Files_External\Service\UserStoragesService;
use OCA\Files_External\Service\UserGlobalStoragesService;
use OCA\Files_External\Service\GlobalStoragesService;
use OCA\Files_External\Service\BackendService;
use OCP\Profiler\IProfiler;
use OC\Profiler\Profiler;

/**
* Class Server
@@ -344,6 +346,10 @@ class Server extends ServerContainer implements IServerContainer {
);
});

$this->registerService(IProfiler::class, function (Server $c) {
return new Profiler($c->get(SystemConfig::class));
});

$this->registerService(\OCP\Encryption\IManager::class, function (Server $c) {
$view = new View();
$util = new Encryption\Util(
@@ -691,9 +697,9 @@ class Server extends ServerContainer implements IServerContainer {
$this->registerDeprecatedAlias('UserCache', ICache::class);

$this->registerService(Factory::class, function (Server $c) {
$arrayCacheFactory = new \OC\Memcache\Factory(
'',
$c->get(LoggerInterface::class),
$profiler = $c->get(IProfiler::class);
$arrayCacheFactory = new \OC\Memcache\Factory('', $c->get(LoggerInterface::class),
$profiler,
ArrayCache::class,
ArrayCache::class,
ArrayCache::class
@@ -717,9 +723,9 @@ class Server extends ServerContainer implements IServerContainer {
$instanceId = \OC_Util::getInstanceId();
$path = \OC::$SERVERROOT;
$prefix = md5($instanceId . '-' . $version . '-' . $path);
return new \OC\Memcache\Factory(
$prefix,
return new \OC\Memcache\Factory($prefix,
$c->get(LoggerInterface::class),
$profiler,
$config->getSystemValue('memcache.local', null),
$config->getSystemValue('memcache.distributed', null),
$config->getSystemValue('memcache.locking', null),
@@ -769,6 +775,7 @@ class Server extends ServerContainer implements IServerContainer {
$c->get(KnownUserService::class)
);
});

$this->registerAlias(IAvatarManager::class, AvatarManager::class);
/** @deprecated 19.0.0 */
$this->registerDeprecatedAlias('AvatarManager', AvatarManager::class);
@@ -861,7 +868,6 @@ class Server extends ServerContainer implements IServerContainer {
}
$connectionParams = $factory->createConnectionParams();
$connection = $factory->getConnection($type, $connectionParams);
$connection->getConfiguration()->setSQLLogger($c->getQueryLogger());
return $connection;
});
/** @deprecated 19.0.0 */

+ 87
- 0
lib/public/DataCollector/AbstractDataCollector.php 查看文件

@@ -0,0 +1,87 @@
<?php

declare(strict_types=1);
/**
* @copyright 2022 Carl Schwan <carl@carlschwan.eu>
*
* @author Carl Schwan <carl@carlschwan.eu>
* @author Fabien Potencier <fabien@symfony.com>
*
* @license AGPL-3.0-or-later AND MIT
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

namespace OCP\DataCollector;

/**
* Children of this class must store the collected data in
* the data property.
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Bernhard Schussek <bschussek@symfony.com>
* @author Carl Schwan <carl@carlschwan.eu>
* @since 24.0.0
*/
abstract class AbstractDataCollector implements IDataCollector, \JsonSerializable {
/** @var array */
protected $data = [];

/**
* @since 24.0.0
*/
public function getName(): string {
return static::class;
}

/**
* Reset the state of the profiler. By default it only empties the
* $this->data contents, but you can override this method to do
* additional cleaning.
* @since 24.0.0
*/
public function reset(): void {
$this->data = [];
}

/**
* @since 24.0.0
*/
public function __sleep(): array {
return ['data'];
}

/**
* @internal to prevent implementing \Serializable
* @since 24.0.0
*/
final protected function serialize() {
}

/**
* @internal to prevent implementing \Serializable
* @since 24.0.0
*/
final protected function unserialize(string $data) {
}

/**
* @since 24.0.0
*/
#[\ReturnTypeWillChange]
public function jsonSerialize() {
return $this->data;
}
}

+ 55
- 0
lib/public/DataCollector/IDataCollector.php 查看文件

@@ -0,0 +1,55 @@
<?php

declare(strict_types=1);
/**
* @copyright 2022 Carl Schwan <carl@carlschwan.eu>
*
* @author Carl Schwan <carl@carlschwan.eu>
* @author Fabien Potencier <fabien@symfony.com>
*
* @license AGPL-3.0-or-later AND MIT
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

namespace OCP\DataCollector;

use OC\AppFramework\Http\Request;
use OCP\AppFramework\Http\Response;

/**
* DataCollectorInterface.
*
* @since 24.0.0
*/
interface IDataCollector {
/**
* Collects data for the given Request and Response.
* @since 24.0.0
*/
public function collect(Request $request, Response $response, \Throwable $exception = null): void;

/**
* Reset the state of the profiler.
* @since 24.0.0
*/
public function reset(): void;

/**
* Returns the name of the collector.
* @since 24.0.0
*/
public function getName(): string;
}

+ 6
- 0
lib/public/ICache.php 查看文件

@@ -76,4 +76,10 @@ interface ICache {
* @since 6.0.0
*/
public function clear($prefix = '');

/**
* Check if the cache implementation is available
* @since 24.0.0
*/
public static function isAvailable(): bool;
}

+ 168
- 0
lib/public/Profiler/IProfile.php 查看文件

@@ -0,0 +1,168 @@
<?php

declare(strict_types=1);

/**
* @copyright 2022 Carl Schwan <carl@carlschwan.eu>
*
* @author Carl Schwan <carl@carlschwan.eu>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

namespace OCP\Profiler;

use OCP\DataCollector\IDataCollector;

/**
* This interface store the results of the profiling of one
* request. You can get the saved profiles from the @see IProfiler.
*
* ```php
* <?php
* $profiler = \OC::$server->get(IProfiler::class);
* $profiles = $profiler->find('/settings/users', 10);
* ```
*
* This interface is meant to be used directly and not extended.
* @since 24.0.0
*/
interface IProfile {
/**
* Get the token of the profile
* @since 24.0.0
*/
public function getToken(): string;

/**
* Set the token of the profile
* @since 24.0.0
*/
public function setToken(string $token): void;

/**
* Get the time of the profile
* @since 24.0.0
*/
public function getTime(): ?int;

/**
* Set the time of the profile
* @since 24.0.0
*/
public function setTime(int $time): void;

/**
* Get the url of the profile
* @since 24.0.0
*/
public function getUrl(): ?string;

/**
* Set the url of the profile
* @since 24.0.0
*/
public function setUrl(string $url): void;

/**
* Get the method of the profile
* @since 24.0.0
*/
public function getMethod(): ?string;

/**
* Set the method of the profile
* @since 24.0.0
*/
public function setMethod(string $method): void;

/**
* Get the status code of the profile
* @since 24.0.0
*/
public function getStatusCode(): ?int;

/**
* Set the status code of the profile
* @since 24.0.0
*/
public function setStatusCode(int $statusCode): void;

/**
* Add a data collector to the profile
* @since 24.0.0
*/
public function addCollector(IDataCollector $collector);

/**
* Get the parent profile to this profile
* @since 24.0.0
*/
public function getParent(): ?IProfile;

/**
* Set the parent profile to this profile
* @since 24.0.0
*/
public function setParent(?IProfile $parent): void;

/**
* Get the parent token to this profile
* @since 24.0.0
*/
public function getParentToken(): ?string;

/**
* Get the profile's children
* @return IProfile[]
* @since 24.0.0
**/
public function getChildren(): array;

/**
* Set the profile's children
* @param IProfile[] $children
* @since 24.0.0
*/
public function setChildren(array $children): void;

/**
* Add the child profile
* @since 24.0.0
*/
public function addChild(IProfile $profile): void;

/**
* Get all the data collectors
* @return IDataCollector[]
* @since 24.0.0
*/
public function getCollectors(): array;

/**
* Set all the data collectors
* @param IDataCollector[] $collectors
* @since 24.0.0
*/
public function setCollectors(array $collectors): void;

/**
* Get a data collector by name
* @since 24.0.0
*/
public function getCollector(string $collectorName): ?IDataCollector;
}

+ 101
- 0
lib/public/Profiler/IProfiler.php 查看文件

@@ -0,0 +1,101 @@
<?php

declare(strict_types=1);

/**
* @copyright 2022 Carl Schwan <carl@carlschwan.eu>
*
* @author Carl Schwan <carl@carlschwan.eu>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

namespace OCP\Profiler;

use OC\AppFramework\Http\Request;
use OCP\AppFramework\Http\Response;
use OCP\DataCollector\IDataCollector;

/**
* This interface allows to interact with the built-in Nextcloud profiler.
* @since 24.0.0
*/
interface IProfiler {
/**
* Add a new data collector to the profiler. This allows to later on
* collect all the data from every registered collector.
*
* @see IDataCollector
* @since 24.0.0
*/
public function add(IDataCollector $dataCollector): void;

/**
* Load a profile from a response object
* @since 24.0.0
*/
public function loadProfileFromResponse(Response $response): ?IProfile;

/**
* Load a profile from the response token
* @since 24.0.0
*/
public function loadProfile(string $token): ?IProfile;

/**
* Save a profile on the disk. This allows to later load it again in the
* profiler user interface.
* @since 24.0.0
*/
public function saveProfile(IProfile $profile): bool;

/**
* Find a profile from various search parameters
* @since 24.0.0
*/
public function find(?string $url, ?int $limit, ?string $method, ?int $start, ?int $end, string $statusCode = null): array;

/**
* Get the list of data providers by identifier
* @return string[]
* @since 24.0.0
*/
public function dataProviders(): array;

/**
* Check if the profiler is enabled.
*
* If it is not enabled, data provider shouldn't be created and
* shouldn't collect any data.
* @since 24.0.0
*/
public function isEnabled(): bool;

/**
* Set if the profiler is enabled.
* @see isEnabled
* @since 24.0.0
*/
public function setEnabled(bool $enabled): void;

/**
* Collect all the information from the current request and construct
* a IProfile from it.
* @since 24.0.0
*/
public function collect(Request $request, Response $response): IProfile;
}

+ 4
- 1
tests/lib/AppFramework/AppTest.php 查看文件

@@ -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())

+ 9
- 6
tests/lib/Memcache/FactoryTest.php 查看文件

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

Loading…
取消
儲存