diff options
| author | 2025-10-01 20:21:24 +0200 | |
|---|---|---|
| committer | 2025-10-01 20:21:24 +0200 | |
| commit | 2601897c55e2040c21591f7b4f6041f71393346b (patch) | |
| tree | dcc55c41de5a91540ca61bce121917080fb6da4b /p/api/greader.php | |
| parent | a2446e2b309cd1330ca75f943e5f209ea8c3403e (diff) | |
API optimisation: more streaming of outputs (#8041)
* API optimisation: more streaming of outputs
I spotted a memory issue when testing https://github.com/FreshRSS/FreshRSS/pull/7714
Attempt to stream results more, instead of keeping too much in memory.
Could be further improved.
* Apply suggestions from code review
Co-authored-by: Alexis Degrugillier <aledeg@users.noreply.github.com>
* Minor whitespace JSON formatting
---------
Co-authored-by: Alexis Degrugillier <aledeg@users.noreply.github.com>
Diffstat (limited to 'p/api/greader.php')
| -rw-r--r-- | p/api/greader.php | 116 |
1 files changed, 81 insertions, 35 deletions
diff --git a/p/api/greader.php b/p/api/greader.php index 538292f58..bcf127986 100644 --- a/p/api/greader.php +++ b/p/api/greader.php @@ -49,7 +49,7 @@ if (PHP_INT_SIZE < 8) { //32-bit } } -const JSON_OPTIONS = JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE; +const JSON_OPTIONS = JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE; function headerVariable(string $headerName, string $varName): string { $header = ''; @@ -554,20 +554,24 @@ final class GReaderAPI { } /** - * @param list<FreshRSS_Entry> $entries - * @return list<array<string,mixed>> + * @param iterable<FreshRSS_Entry> $entries + * @param list<numeric-string>|null $e_ids List of entry IDs if known, for performance + * @return Generator<int,array<string,mixed>> */ - private static function entriesToArray(array $entries): array { - if (empty($entries)) { - return []; - } + private static function entriesToArray(iterable $entries, ?array $e_ids = null): Generator { $catDAO = FreshRSS_Factory::createCategoryDao(); $categories = $catDAO->listCategories(prePopulateFeeds: true); $tagDAO = FreshRSS_Factory::createTagDao(); - $entryIdsTagNames = $tagDAO->getEntryIdsTagNames($entries); + if (is_array($e_ids)) { + $entryIdsTagNames = $tagDAO->getEntryIdsTagNames($e_ids); + } else { + // If we do not have the list of entry IDs, we first need to iterate through all entries + //TODO: Improve: avoid iterator_to_array. Type test only for PHP < 8.2 + $entries = array_values(is_array($entries) ? $entries : iterator_to_array($entries)); + $entryIdsTagNames = $tagDAO->getEntryIdsTagNames($entries); + } - $items = []; foreach ($entries as $item) { /** @var FreshRSS_Entry|null $entry */ $entry = Minz_ExtensionManager::callHook(Minz_HookType::EntryBeforeDisplay, $item); @@ -581,9 +585,8 @@ final class GReaderAPI { } $entry->_feed($feed); - $items[] = $entry->toGReader('compat', $entryIdsTagNames['e_' . $entry->id()] ?? []); + yield $entry->toGReader('compat', $entryIdsTagNames['e_' . $entry->id()] ?? []); } - return $items; } /** @@ -684,29 +687,54 @@ final class GReaderAPI { order: $order === 'o' ? 'ASC' : 'DESC', continuation_id: $continuation, limit: $count); - $entries = array_values(iterator_to_array($entries)); //TODO: Improve $items = self::entriesToArray($entries); if ($continuation !== '0') { - array_shift($items); //Discard first element that was already sent in the previous response + //Discard first element that was already sent in the previous response + $items = new LimitIterator($items, offset: 1); $count--; } - $response = [ - 'id' => 'user/-/state/com.google/reading-list', - 'updated' => time(), - 'items' => $items, - ]; - if (count($entries) >= $count) { - $entry = end($entries); - if ($entry != false) { - $response['continuation'] = '' . $entry->id(); + $time = time(); + $nbItems = 0; + $lastEntryId = 0; + + // Note: This section must be streamed to avoid memory issues with large responses + echo <<<TXT +{ + "id": "user/-/state/com.google/reading-list", + "updated": $time, + "items": [ + +TXT; + foreach ($items as $item) { + if (!is_array($item) || empty($item)) { + continue; + } + if ($nbItems > 0) { + echo ",\n"; } + $lastEntryId = is_numeric($item['frss:id'] ?? null) ? (int)$item['frss:id'] : 0; + unset($item['frss:id']); + echo json_encode($item, JSON_OPTIONS); + $nbItems++; } - unset($entries, $entryDAO, $items); - gc_collect_cycles(); - echoJson($response, 2); // $optimisationDepth=2 as we are interested in being memory efficient for {"items":[...]} + echo <<<'TXT' + + ] +TXT; + if ($nbItems >= $count && $lastEntryId > 0) { + echo <<<TXT +, + "continuation": "$lastEntryId" +TXT; + } + echo <<<'TXT' + +} + +TXT; exit(); } @@ -801,18 +829,36 @@ final class GReaderAPI { $entryDAO = FreshRSS_Factory::createEntryDao(); $entries = $entryDAO->listByIds($e_ids, order: $order === 'o' ? 'ASC' : 'DESC'); - $entries = array_values(iterator_to_array($entries)); //TODO: Improve - $items = self::entriesToArray($entries); + $items = self::entriesToArray($entries, $e_ids); + $time = time(); + $nbItems = 0; - $response = [ - 'id' => 'user/-/state/com.google/reading-list', - 'updated' => time(), - 'items' => $items, - ]; - unset($entries, $entryDAO, $items); - gc_collect_cycles(); - echoJson($response, 2); // $optimisationDepth=2 as we are interested in being memory efficient for {"items":[...]} + // Note: This section must be streamed to avoid memory issues with large responses + echo <<<TXT +{ + "id": "user/-/state/com.google/reading-list", + "updated": $time, + "items": [ + +TXT; + foreach ($items as $item) { + if (!is_array($item) || empty($item)) { + continue; + } + if ($nbItems > 0) { + echo ",\n"; + } + unset($item['frss:id']); + echo json_encode($item, JSON_OPTIONS); + $nbItems++; + } + echo <<<'TXT' + + ] +} + +TXT; exit(); } |
