From 4b8cd18cca4dda56118969dcbe47844eb6188825 Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Mon, 12 Oct 2026 16:25:57 +0200 Subject: [PATCH] perf(files): cache the mimetype table Every request that reads a file cache entry loaded the whole mimetypes table from the database. Keep it in the local cache instead. Mimetypes are only ever added, so a stale cache can only miss new mimetypes: on a miss the table is loaded from the database again, at most once per request. Assisted-by: ClaudeCode:claude-opus-5-5 Signed-off-by: Carl Schwan --- lib/private/Files/Type/Loader.php | 64 ++++++++++++++++++----------- tests/lib/Files/Type/LoaderTest.php | 45 +++++++++++++++++++- 2 files changed, 83 insertions(+), 26 deletions(-) diff --git a/lib/private/Files/Type/Loader.php b/lib/private/Files/Type/Loader.php index 49cd44d1772db..f27e68de06abe 100644 --- a/lib/private/Files/Type/Loader.php +++ b/lib/private/Files/Type/Loader.php @@ -12,6 +12,8 @@ use OCP\AppFramework\Db\TTransactional; use OCP\DB\Exception as DBException; use OCP\Files\IMimeTypeLoader; +use OCP\ICache; +use OCP\ICacheFactory; use OCP\IDBConnection; /** @@ -28,14 +30,16 @@ class Loader implements IMimeTypeLoader { /** @psalm-var array */ protected array $mimetypeIds; - /** - * @param IDBConnection $dbConnection - */ + private bool $loadedFromDatabase = false; + private ICache $cache; + public function __construct( private IDBConnection $dbConnection, + ICacheFactory $cacheFactory, ) { $this->mimetypes = []; $this->mimetypeIds = []; + $this->cache = $cacheFactory->createLocal('mimetypes'); } /** @@ -46,10 +50,10 @@ public function getMimetypeById(int $id): ?string { if (!$this->mimetypes) { $this->loadMimetypes(); } - if (isset($this->mimetypes[$id])) { - return $this->mimetypes[$id]; + if (!isset($this->mimetypes[$id]) && !$this->loadedFromDatabase) { + $this->loadMimetypes(true); } - return null; + return $this->mimetypes[$id] ?? null; } /** @@ -57,13 +61,10 @@ public function getMimetypeById(int $id): ?string { */ #[\Override] public function getId(string $mimetype): int { - if (!$this->mimetypeIds) { - $this->loadMimetypes(); - } - if (isset($this->mimetypeIds[$mimetype])) { - return $this->mimetypeIds[$mimetype]; + if (!$this->exists($mimetype)) { + return $this->store($mimetype); } - return $this->store($mimetype); + return $this->mimetypeIds[$mimetype]; } /** @@ -74,6 +75,9 @@ public function exists(string $mimetype): bool { if (!$this->mimetypeIds) { $this->loadMimetypes(); } + if (!isset($this->mimetypeIds[$mimetype]) && !$this->loadedFromDatabase) { + $this->loadMimetypes(true); + } return isset($this->mimetypeIds[$mimetype]); } @@ -84,6 +88,8 @@ public function exists(string $mimetype): bool { public function reset(): void { $this->mimetypes = []; $this->mimetypeIds = []; + $this->loadedFromDatabase = false; + $this->cache->remove('mimetypes'); } /** @@ -124,25 +130,33 @@ protected function store(string $mimetype): int { $this->mimetypes[$mimetypeId] = $mimetype; $this->mimetypeIds[$mimetype] = $mimetypeId; + $this->cache->remove('mimetypes'); return $mimetypeId; } /** - * Load all mimetypes from DB + * Load all mimetypes, from the cache unless $fromDatabase is set. Mimetypes + * are only ever added, so the cache can only miss new ones, which are + * then loaded from the database. */ - private function loadMimetypes(): void { - $qb = $this->dbConnection->getQueryBuilder(); - $qb->select('id', 'mimetype') - ->from('mimetypes'); - - $result = $qb->executeQuery(); - $results = $result->fetchAllAssociative(); - $result->closeCursor(); - - foreach ($results as $row) { - $this->mimetypes[(int)$row['id']] = $row['mimetype']; - $this->mimetypeIds[$row['mimetype']] = (int)$row['id']; + private function loadMimetypes(bool $fromDatabase = false): void { + $mimetypes = $fromDatabase ? null : $this->cache->get('mimetypes'); + if (!is_array($mimetypes)) { + $qb = $this->dbConnection->getQueryBuilder(); + $qb->select('id', 'mimetype') + ->from('mimetypes'); + $result = $qb->executeQuery(); + $mimetypes = []; + foreach ($result->fetchAllAssociative() as $row) { + $mimetypes[(int)$row['id']] = (string)$row['mimetype']; + } + $result->closeCursor(); + $this->cache->set('mimetypes', $mimetypes); + $this->loadedFromDatabase = true; } + + $this->mimetypes = $mimetypes; + $this->mimetypeIds = array_flip($mimetypes); } /** diff --git a/tests/lib/Files/Type/LoaderTest.php b/tests/lib/Files/Type/LoaderTest.php index 35c549f321e94..0713ff48b10b6 100644 --- a/tests/lib/Files/Type/LoaderTest.php +++ b/tests/lib/Files/Type/LoaderTest.php @@ -9,6 +9,8 @@ namespace Test\Files\Type; use OC\Files\Type\Loader; +use OC\Memcache\ArrayCache; +use OCP\ICacheFactory; use OCP\IDBConnection; use OCP\Server; use PHPUnit\Framework\Attributes\Group; @@ -17,6 +19,7 @@ #[Group('DB')] class LoaderTest extends TestCase { protected IDBConnection $db; + protected ArrayCache $cache; protected Loader $loader; #[\Override] @@ -24,7 +27,8 @@ protected function setUp(): void { parent::setUp(); $this->db = Server::get(IDBConnection::class); - $this->loader = new Loader($this->db); + $this->cache = new ArrayCache(''); + $this->loader = $this->createLoader(); } #[\Override] @@ -35,6 +39,7 @@ protected function tearDown(): void { 'mimetype', $deleteMimetypes->createPositionalParameter('testing/%') )); $deleteMimetypes->executeStatement(); + $this->loader->reset(); parent::tearDown(); } @@ -84,4 +89,42 @@ public function testStoreExists(): void { $this->assertEquals($mimetypeId, $mimetypeId2); } + + private function createLoader(): Loader { + $cacheFactory = $this->createMock(ICacheFactory::class); + $cacheFactory->method('createLocal')->willReturn($this->cache); + return new Loader($this->db, $cacheFactory); + } + + private function insertMimetype(string $mimetype): int { + $qb = $this->db->getQueryBuilder(); + $qb->insert('mimetypes') + ->values(['mimetype' => $qb->createPositionalParameter($mimetype)]) + ->executeStatement(); + return $qb->getLastInsertId(); + } + + public function testMimetypesAreLoadedFromTheCache(): void { + $this->cache->set('mimetypes', [123456 => 'testing/cached']); + + $this->assertSame('testing/cached', $this->createLoader()->getMimetypeById(123456)); + } + + public function testMimetypeMissingInTheCacheIsLoadedFromTheDatabase(): void { + $this->loader->getMimetypeById(1); + $id = $this->insertMimetype('testing/added-elsewhere'); + + $loader = $this->createLoader(); + $this->assertSame('testing/added-elsewhere', $loader->getMimetypeById($id)); + $this->assertTrue($loader->exists('testing/added-elsewhere')); + $this->assertSame($id, $loader->getId('testing/added-elsewhere')); + } + + public function testStoreInvalidatesTheCache(): void { + $this->loader->getMimetypeById(1); + $this->assertTrue($this->cache->hasKey('mimetypes')); + + $this->loader->getId('testing/stored'); + $this->assertFalse($this->cache->hasKey('mimetypes')); + } }