aboutsummaryrefslogtreecommitdiff
path: root/p/scripts
diff options
context:
space:
mode:
authorGravatar Alexandre Alapetite <alexandre@alapetite.fr> 2025-12-04 19:11:31 +0100
committerGravatar GitHub <noreply@github.com> 2025-12-04 19:11:31 +0100
commit78e40c6fe3afe7f815ef9d32646610e2d5436ba3 (patch)
tree44ac9cb2255b4253beabcce3d23d81d8508e5211 /p/scripts
parent794d56e10f694688d60a26d4848fd82027722838 (diff)
Scaling of user statistics (#8277)
Fix https://github.com/FreshRSS/FreshRSS/issues/8268 To better support user management on FreshRSS instance with many users. SQL speed improved. On a reduced test with 5 users, including some large accounts (PostgreSQL on a very tiny and slow server), improving from ~2.3s to ~1.8s, which gives ~20% speed improvement. Then tested with 1000 users, with only the default feed (on my old desktop computer): ```sh for i in {1..1000}; do ./cli/create-user.php --user=freshrss$i --password=freshrss; done app/actualize_script.php cli/access-permissions.sh ``` SQLite: ```console $ time cli/user-info.php | wc -l 1001 real 0m1.366s user 0m0.908s sys 0m0.475s ``` PostgreSQL: ```console $ time cli/user-info.php | wc -l 1001 real 0m28.498s user 0m12.137s sys 0m2.217s ``` MariaDB: ```console # time ./cli/user-info.php | wc -l 1001 real 0m49.485s user 0m1.276s sys 0m2.258s ``` Yes, SQLite is much faster - not a surprise for such use-cases, where the TCP connection is not re-used. I have added some CLI options to disable some statistics: ```sh cli/user-info.php --no-db-size --no-db-counts ``` For the Web UI, I have disabled detailed user statistics if it takes too long, and retrieve missing user statistics asynchronously via JavaScript. Lazy loading of the user details based on IntersectionObserver, with maximum 10 requests in parallel. Web UI tested on 1000 users as well. Checked with SeaMonkey.
Diffstat (limited to 'p/scripts')
-rw-r--r--p/scripts/extra.js55
1 files changed, 55 insertions, 0 deletions
diff --git a/p/scripts/extra.js b/p/scripts/extra.js
index dfeff9293..9eeefabfb 100644
--- a/p/scripts/extra.js
+++ b/p/scripts/extra.js
@@ -524,6 +524,60 @@ function init_details_attributes() {
});
}
+function init_user_stats() {
+ const active = new Set();
+ const queue = [];
+ const limit = 10; // Ensure not too many concurrent requests
+
+ const processQueue = () => {
+ while (queue.length > 0 && active.size < limit) {
+ const row = queue.shift();
+ const promise = (async () => {
+ row.removeAttribute('data-need-ajax');
+ try {
+ const username = row.querySelector('.username').textContent.trim();
+ const url = '?c=user&a=details&username=' + encodeURIComponent(username) + '&ajax=1';
+ const response = await fetch(url);
+ const html = await response.text();
+ const parser = new DOMParser();
+ const doc = parser.parseFromString(html, 'text/html');
+ row.querySelector('.feed-count').innerHTML = doc.querySelector('.feed_count').innerHTML;
+ row.querySelector('.article-count').innerHTML = doc.querySelector('.article_count').innerHTML;
+ row.querySelector('.database-size').innerHTML = doc.querySelector('.database_size').innerHTML;
+ } catch (err) {
+ console.error('Error fetching user stats', err);
+ }
+ })();
+
+ promise.finally(() => {
+ active.delete(promise);
+ processQueue();
+ });
+ active.add(promise);
+ }
+ };
+
+ // Retrieve user stats when the row becomes visible
+ const timers = new WeakMap();
+ const observer = new IntersectionObserver((entries) => {
+ entries.forEach(entry => {
+ if (entry.isIntersecting) {
+ const timer = setTimeout(() => {
+ // But wait a bit to avoid triggering on fast scrolls
+ observer.unobserve(entry.target);
+ queue.push(entry.target);
+ processQueue();
+ }, 100);
+ timers.set(entry.target, timer);
+ } else {
+ clearTimeout(timers.get(entry.target));
+ }
+ });
+ });
+
+ document.querySelectorAll('tr[data-need-ajax]').forEach(row => observer.observe(row));
+}
+
function init_extra_afterDOM() {
if (!window.context) {
if (window.console) {
@@ -544,6 +598,7 @@ function init_extra_afterDOM() {
init_2stateButton();
init_update_feed();
init_details_attributes();
+ init_user_stats();
data_auto_leave_validation(document.body);