From 102988ce8528e47cd3363ea6906b0fe514d23637 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Thu, 26 Mar 2026 18:21:17 +0530 Subject: [PATCH] added caching layer --- .../Modules/Presence/HTTP/Iterative/XList.php | 98 ++++++++++++++++--- tests/e2e/Services/Realtime/PresenceBase.php | 29 +++--- 2 files changed, 104 insertions(+), 23 deletions(-) diff --git a/src/Appwrite/Platform/Modules/Presence/HTTP/Iterative/XList.php b/src/Appwrite/Platform/Modules/Presence/HTTP/Iterative/XList.php index ecd551e494..d10da458ec 100644 --- a/src/Appwrite/Platform/Modules/Presence/HTTP/Iterative/XList.php +++ b/src/Appwrite/Platform/Modules/Presence/HTTP/Iterative/XList.php @@ -39,28 +39,104 @@ class XList extends Action // just for getting group based presence list based on permissions public function action(UtopiaResponse $response, Database $dbForProject, Authorization $authorization, Document $user): void { + $requestingUserId = $user->getId(); + $hostname = $dbForProject->getAdapter()->getHostname(); + $usersCacheKey = \sprintf( + '%s-cache-%s:%s:%s:collection:presence', + $dbForProject->getCacheName(), + $hostname ?? '', + $dbForProject->getNamespace(), + $dbForProject->getTenant() + ); + $presenceLogsCacheKey = \sprintf( + '%s-cache-%s:%s:%s:collection:presenceLogs:user:%s', + $dbForProject->getCacheName(), + $hostname ?? '', + $dbForProject->getNamespace(), + $dbForProject->getTenant(), + $requestingUserId + ); + $ttl = 60; try { $presenceLogs = []; + $totalStart = microtime(true); + $userStart = microtime(true); - $users = $authorization->skip(fn () => $dbForProject->find('presence', [ - Query::limit(10000), - ])); - foreach ($users as $user) { + $presenceCacheStart = microtime(true); + $cachedPresenceLogs = $dbForProject->getCache()->load($presenceLogsCacheKey, $ttl); + if($cachedPresenceLogs !== null && $cachedPresenceLogs !== false && \is_array($cachedPresenceLogs)){ + $presenceLogs = array_map(fn($doc) => new Document($doc), $cachedPresenceLogs); + $presenceLogEnd = microtime(true) - $presenceCacheStart; + $totalEnd = microtime(true) - $totalStart; + Console::info(sprintf( + "Cache | Requesting User [%s] | [Total][time] %.2f ms | [Users][time] %.2f ms | [PresenceCache][time] %.2f ms\n", + $requestingUserId, + $totalEnd * 1000, + 0, + $presenceLogEnd * 1000 + )); + + $response->dynamic(new Document([ + 'presences' => $presenceLogs, + 'total' => \count($presenceLogs), + ]), UtopiaResponse::MODEL_PRESENCE_LIST); + + return; + } + + $cachedUsers = $dbForProject->getCache()->load($usersCacheKey, $ttl); + if($cachedUsers !== null && $cachedUsers !== false && \is_array($cachedUsers)){ + $users = array_map(fn($doc) => new Document($doc), $cachedUsers); + } + else{ + $users = $authorization->skip(fn () => $dbForProject->find('presence', [ + Query::limit(10000), + ])); + + // saving to cache + $documentsArray = \array_map(function ($doc) { + return $doc->getArrayCopy(); + }, $users); + $dbForProject->getCache()->save($usersCacheKey, $documentsArray); + } + + $userEnd = microtime(true) - $userStart; + $presenceLogStart = microtime(true); + foreach ($users as $presenceUser) { + // $presenceCacheKey = \sprintf( + // '%s-cache-%s:%s:%s:collection:presenceLogs:user:%s', + // $dbForProject->getCacheName(), + // $hostname ?? '', + // $dbForProject->getNamespace(), + // $dbForProject->getTenant(), + // $presenceUser['userId'] ?? '' + // ); + $presenceLog = $dbForProject->findOne('presenceLogs', [ - Query::equal('userId', [$user['userId']]), - Query::orderDesc('$updatedAt'), - // Tie-breaker: `$updatedAt` has only millisecond precision. - Query::orderDesc('$id'), - Query::limit(1), - ]); + Query::equal('userId', [$presenceUser['userId']]), + Query::orderDesc('$updatedAt'), + // Tie-breaker: `$updatedAt` has only millisecond precision. + Query::orderDesc('$id'), + Query::limit(1), + ]); + $presenceLogs[] = $presenceLog; } + $dbForProject->getCache()->save($presenceLogsCacheKey, $presenceLogs); + $presenceLogEnd = microtime(true) - $presenceLogStart; + $totalEnd = microtime(true) - $totalStart; } catch (OrderException $e) { throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL, "The order column '{$e->getAttribute()}' had a null value. Cursor pagination requires all rows order column values are non-null."); } catch (QueryException) { throw new Exception(Exception::GENERAL_QUERY_INVALID); } - + Console::info(sprintf( + "Requesting User [%s] | [Total][time] %.2f ms | [Users][time] %.2f ms | [PresenceLogs][time] %.2f ms\n", + $requestingUserId, + $totalEnd * 1000, + $userEnd * 1000, + $presenceLogEnd * 1000 + )); $response->dynamic(new Document([ 'presences' => $presenceLogs, 'total' => \count($presenceLogs), diff --git a/tests/e2e/Services/Realtime/PresenceBase.php b/tests/e2e/Services/Realtime/PresenceBase.php index d079ed2907..40d9a70df4 100644 --- a/tests/e2e/Services/Realtime/PresenceBase.php +++ b/tests/e2e/Services/Realtime/PresenceBase.php @@ -763,16 +763,19 @@ abstract class PresenceBase extends Scope // Pick 2 viewers from each group: first two indices within group. $viewerIndices = []; - for ($g = 0; $g < $numGroups; $g++) { + for ($g = 0; $g < $numGroups && \count($viewerIndices) < $viewerCount; $g++) { $base = $g * $groupSize; + $viewerIndices[] = $base; - if ($viewerCount >= 10) { - // keep exactly 10; should trigger only if group math changes + if (\count($viewerIndices) >= $viewerCount) { break; } - $viewerIndices[] = $base + 1; + + // Add the next index within the group if it exists. + if ($base + 1 < $totalUsers) { + $viewerIndices[] = $base + 1; + } } - $viewerIndices = \array_slice($viewerIndices, 0, $viewerCount); $statusSampleOwnerOffsets = [0, 50, 99]; @@ -828,11 +831,12 @@ abstract class PresenceBase extends Scope // Allow realtime write to settle. \usleep(100000); - for ($occurrence = 0; $occurrence < $occurrences; $occurrence++) { - foreach ($viewerIndices as $viewerSampleIndex) { - $viewer = $users[$viewerSampleIndex]; - $group = (int) \floor($viewerSampleIndex / $groupSize); + // Fetch listPresence `occurrences` times per viewer, in-order. + foreach ($viewerIndices as $viewerSampleIndex) { + $viewer = $users[$viewerSampleIndex]; + $group = (int) \floor($viewerSampleIndex / $groupSize); + for ($occurrence = 0; $occurrence < $occurrences; $occurrence++) { $benchmark = $this->fetchPresenceListAs($viewer); $this->assertEquals(200, $benchmark['response']['headers']['status-code']); $this->assertGreaterThan(0.0, $benchmark['elapsedMs']); @@ -861,10 +865,11 @@ abstract class PresenceBase extends Scope } Console::info(\sprintf( - '[Presence Benchmark F] cycle=%d occurrence=%d listCallsThisOcc=%d', + '[Presence Benchmark F] cycle=%d viewerIndex=%d viewerGroup=%d occurrences=%d', $cycle, - $occurrence + 1, - $viewerCount + $viewerSampleIndex, + $group, + $occurrences )); } }