From 9814868e19a67ab7e0ab49a8dcf830e589a7f5ef Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Mon, 5 Oct 2026 14:44:02 +0200 Subject: [PATCH] perf(preview): cache the hardware concurrency Every preview request reads /proc/cpuinfo twice to compute the default preview concurrency. The kernel generates that file on each read, which costs as much as looking up a cached preview. Cache the value in the local cache instead. Assisted-by: ClaudeCode:claude-opus-5-5 Signed-off-by: Carl Schwan --- lib/private/Preview/Generator.php | 18 ++++++++++++++++-- lib/private/PreviewManager.php | 2 ++ tests/lib/Preview/GeneratorTest.php | 22 ++++++++++++++++++++++ 3 files changed, 40 insertions(+), 2 deletions(-) diff --git a/lib/private/Preview/Generator.php b/lib/private/Preview/Generator.php index 94fea7314b565..8c0bd6f24ce7f 100644 --- a/lib/private/Preview/Generator.php +++ b/lib/private/Preview/Generator.php @@ -21,6 +21,7 @@ use OCP\Files\SimpleFS\InMemoryFile; use OCP\Files\SimpleFS\ISimpleFile; use OCP\IAppConfig; +use OCP\ICacheFactory; use OCP\IConfig; use OCP\IImage; use OCP\Image; @@ -46,6 +47,7 @@ public function __construct( private readonly PreviewMapper $previewMapper, private readonly StorageFactory $storageFactory, private readonly PreviewMigrationService $migrationService, + private readonly ICacheFactory $cacheFactory, ) { } @@ -261,7 +263,19 @@ public static function unguardWithSemaphore(false|\SysvSemaphore $semId): bool { * * @return int number of concurrent threads, or 0 if it cannot be determined */ - private static function getHardwareConcurrency(): int { + private function getHardwareConcurrency(): int { + // /proc/cpuinfo is slow to read + $cache = $this->cacheFactory->createLocal('preview'); + $cached = $cache->get('hardware_concurrency'); + if (is_int($cached)) { + return $cached; + } + $concurrency = self::readHardwareConcurrency(); + $cache->set('hardware_concurrency', $concurrency, 3600); + return $concurrency; + } + + private static function readHardwareConcurrency(): int { if (function_exists('ini_get')) { $openBasedir = ini_get('open_basedir'); if (empty($openBasedir) || strpos($openBasedir, '/proc/cpuinfo') !== false) { @@ -291,7 +305,7 @@ public function getNumConcurrentPreviews(string $type): int { return $this->cachedNumConcurrentPreviews[$type]; } - $hardwareConcurrency = self::getHardwareConcurrency(); + $hardwareConcurrency = $this->getHardwareConcurrency(); switch ($type) { case 'preview_concurrency_all': $fallback = $hardwareConcurrency > 0 ? $hardwareConcurrency * 2 : 8; diff --git a/lib/private/PreviewManager.php b/lib/private/PreviewManager.php index c3f780de39ecb..69b74f032fc83 100644 --- a/lib/private/PreviewManager.php +++ b/lib/private/PreviewManager.php @@ -58,6 +58,7 @@ use OCP\Files\SimpleFS\ISimpleFile; use OCP\IAppConfig; use OCP\IBinaryFinder; +use OCP\ICacheFactory; use OCP\IConfig; use OCP\IPreview; use OCP\Preview\IProviderV2; @@ -161,6 +162,7 @@ private function getGenerator(): Generator { $this->container->get(PreviewMapper::class), $this->container->get(StorageFactory::class), $this->container->get(PreviewMigrationService::class), + $this->container->get(ICacheFactory::class), ); } return $this->generator; diff --git a/tests/lib/Preview/GeneratorTest.php b/tests/lib/Preview/GeneratorTest.php index 1b62768ef390d..526ac19cff10b 100644 --- a/tests/lib/Preview/GeneratorTest.php +++ b/tests/lib/Preview/GeneratorTest.php @@ -483,6 +483,28 @@ public function testCorrectSize(int $maxX, int $maxY, int $reqX, int $reqY, bool } } + public function testHardwareConcurrencyFromCache(): void { + $this->getAutoMock(IConfig::class)->method('getSystemValueInt') + ->willReturnCallback(fn ($key, $default) => $default); + $cache = $this->getCacheAutoMock('preview'); + $cache->method('get')->with('hardware_concurrency')->willReturn(3); + $cache->expects($this->never())->method('set'); + + $this->assertSame(6, $this->generator->getNumConcurrentPreviews('preview_concurrency_all')); + $this->assertSame(3, $this->generator->getNumConcurrentPreviews('preview_concurrency_new')); + } + + public function testHardwareConcurrencyIsCached(): void { + $this->getAutoMock(IConfig::class)->method('getSystemValueInt') + ->willReturnCallback(fn ($key, $default) => $default); + $cache = $this->getCacheAutoMock('preview'); + $cache->method('get')->with('hardware_concurrency')->willReturn(null); + $cache->expects($this->once())->method('set') + ->with('hardware_concurrency', $this->isInt(), 3600); + + $this->generator->getNumConcurrentPreviews('preview_concurrency_new'); + } + public function testUnreadbleFile(): void { $file = $this->createMock(File::class); $file->method('isReadable')