aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Alexandre Alapetite <alexandre@alapetite.fr> 2024-03-01 10:08:25 +0100
committerGravatar GitHub <noreply@github.com> 2024-03-01 10:08:25 +0100
commit5e54d5bc58dd815a84d12da1218c09e2c200b6eb (patch)
treede87559d5dfa6cf028752257290c882012e4f575
parent96484d22a145fdef1d9002dc323f02e3b8ed2135 (diff)
Reduce API memory consumption (#6137)
`echo json_encode(...)` is very memory demanding for large responses, so optimised. Contributes to https://github.com/FreshRSS/FreshRSS/issues/6136 https://github.com/FreshRSS/FreshRSS/pull/6013#discussion_r1506779881
-rw-r--r--lib/lib_rss.php57
-rw-r--r--p/api/greader.php10
2 files changed, 63 insertions, 4 deletions
diff --git a/lib/lib_rss.php b/lib/lib_rss.php
index e01630316..880b4e65a 100644
--- a/lib/lib_rss.php
+++ b/lib/lib_rss.php
@@ -5,6 +5,24 @@ if (version_compare(PHP_VERSION, FRESHRSS_MIN_PHP_VERSION, '<')) {
die(sprintf('FreshRSS error: FreshRSS requires PHP %s+!', FRESHRSS_MIN_PHP_VERSION));
}
+if (!function_exists('array_is_list')) {
+ /**
+ * Polyfill for PHP <8.1
+ * https://php.net/array-is-list#127044
+ * @param array<mixed> $array
+ */
+ function array_is_list(array $array): bool {
+ $i = -1;
+ foreach ($array as $k => $v) {
+ ++$i;
+ if ($k !== $i) {
+ return false;
+ }
+ }
+ return true;
+ }
+}
+
if (!function_exists('mb_strcut')) {
function mb_strcut(string $str, int $start, ?int $length = null, string $encoding = 'UTF-8'): string {
return substr($str, $start, $length) ?: '';
@@ -89,6 +107,45 @@ function classAutoloader(string $class): void {
spl_autoload_register('classAutoloader');
//</Auto-loading>
+/**
+ * Memory efficient replacement of `echo json_encode(...)`
+ * @param array<mixed>|mixed $json
+ * @param int $optimisationDepth Number of levels for which to perform memory optimisation
+ * before calling the faster native JSON serialisation.
+ * Set to negative value for infinite depth.
+ */
+function echoJson($json, int $optimisationDepth = -1): void {
+ if ($optimisationDepth === 0 || !is_array($json)) {
+ echo json_encode($json, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
+ return;
+ }
+ $first = true;
+ if (array_is_list($json)) {
+ echo '[';
+ foreach ($json as $item) {
+ if ($first) {
+ $first = false;
+ } else {
+ echo ',';
+ }
+ echoJson($item, $optimisationDepth - 1);
+ }
+ echo ']';
+ } else {
+ echo '{';
+ foreach ($json as $key => $value) {
+ if ($first) {
+ $first = false;
+ } else {
+ echo ',';
+ }
+ echo json_encode($key, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE), ':';
+ echoJson($value, $optimisationDepth - 1);
+ }
+ echo '}';
+ }
+}
+
function idn_to_puny(string $url): string {
if (function_exists('idn_to_ascii')) {
$idn = parse_url($url, PHP_URL_HOST);
diff --git a/p/api/greader.php b/p/api/greader.php
index 615f83567..d2fa810d0 100644
--- a/p/api/greader.php
+++ b/p/api/greader.php
@@ -715,8 +715,9 @@ final class GReaderAPI {
$response['continuation'] = '' . $entry->id();
}
}
-
- echo json_encode($response, JSON_OPTIONS), "\n";
+ unset($entries, $entryDAO, $items);
+ gc_collect_cycles();
+ echoJson($response, 2); // $optimisationDepth=2 as we are interested in being memory efficient for {"items":[...]}
exit();
}
@@ -805,8 +806,9 @@ final class GReaderAPI {
'updated' => time(),
'items' => $items,
);
-
- echo json_encode($response, JSON_OPTIONS), "\n";
+ unset($entries, $entryDAO, $items);
+ gc_collect_cycles();
+ echoJson($response, 2); // $optimisationDepth=2 as we are interested in being memory efficient for {"items":[...]}
exit();
}