Count only the person's own feeds in the feed list (#165)
The traces #164 added showed feed_list as nearly all of GET /api/feeds, and in it the item count at 184-316 ms, about 70% of the request: it counted every item of every feed in the database, 1534 feeds, for someone reading 146, on every feed list and every live update of one. The file count did the same over every file row, 37-50 ms. Both now join the person's subscriptions, as the unread count already did, and so read only their feeds' rows, through the entries key and the enclosures_entry index, both on feed_id first. The Directory also reads feed_list, for every feed's title and artwork, and uses no count; a feed the person does not read is still listed, with none. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
- The feed list loads faster: it counts the items and files of your own feeds, not of every feed on the server.
|
||||||
- Downloading or deleting a file the server no longer has answers "not found" instead of a server error.
|
- Downloading or deleting a file the server no longer has answers "not found" instead of a server error.
|
||||||
- A feed's page in the Directory, and subscribing from it, no longer wait while the whole Directory is put together to check that the one feed is in it.
|
- A feed's page in the Directory, and subscribing from it, no longer wait while the whole Directory is put together to check that the one feed is in it.
|
||||||
- The feed list, and each live update of it, no longer asks the database for a group's title once for every feed in the group.
|
- The feed list, and each live update of it, no longer asks the database for a group's title once for every feed in the group.
|
||||||
|
|||||||
25
src/db.rs
25
src/db.rs
@@ -398,18 +398,23 @@ impl Db {
|
|||||||
.map(|r| Ok((r.try_get::<String>("", "feed_id")?, r.try_get::<i64>("", "n")?)))
|
.map(|r| Ok((r.try_get::<String>("", "feed_id")?, r.try_get::<i64>("", "n")?)))
|
||||||
.collect::<Result<HashMap<_, _>>>()
|
.collect::<Result<HashMap<_, _>>>()
|
||||||
};
|
};
|
||||||
|
// Of this person's feeds only, as the unread count is: counting every item of every feed on
|
||||||
|
// the server, 1534 feeds for someone reading 146, was 184-316 ms, 70% of the feed list's
|
||||||
|
// time (#165). The Directory reads this for every feed's title and artwork, not a count.
|
||||||
let entries = counts(
|
let entries = counts(
|
||||||
"entries",
|
"entries",
|
||||||
"SELECT feed_id, count(*) AS n FROM entries
|
"SELECT e.feed_id, count(*) AS n FROM entries e
|
||||||
WHERE (CAST($1 AS TEXT) IS NULL OR feed_id = $1) GROUP BY feed_id",
|
JOIN subscriptions sub ON sub.user_id = $1 AND sub.feed_id = e.feed_id
|
||||||
vec![one()],
|
WHERE (CAST($2 AS TEXT) IS NULL OR e.feed_id = $2) GROUP BY e.feed_id",
|
||||||
|
vec![user_id.into(), one()],
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
let downloaded = counts(
|
let downloaded = counts(
|
||||||
"downloaded",
|
"downloaded",
|
||||||
"SELECT feed_id, count(*) AS n FROM enclosures
|
"SELECT x.feed_id, count(*) AS n FROM enclosures x
|
||||||
WHERE path IS NOT NULL AND (CAST($1 AS TEXT) IS NULL OR feed_id = $1) GROUP BY feed_id",
|
JOIN subscriptions sub ON sub.user_id = $1 AND sub.feed_id = x.feed_id
|
||||||
vec![one()],
|
WHERE x.path IS NOT NULL AND (CAST($2 AS TEXT) IS NULL OR x.feed_id = $2) GROUP BY x.feed_id",
|
||||||
|
vec![user_id.into(), one()],
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
let unread = counts(
|
let unread = counts(
|
||||||
@@ -2301,9 +2306,9 @@ mod tests {
|
|||||||
let db = Db::memory().await.unwrap();
|
let db = Db::memory().await.unwrap();
|
||||||
db.exec_for_test(
|
db.exec_for_test(
|
||||||
"INSERT INTO users (id, name, is_admin) VALUES (1,'ray',true),(2,'sam',false);
|
"INSERT INTO users (id, name, is_admin) VALUES (1,'ray',true),(2,'sam',false);
|
||||||
INSERT INTO subscriptions (user_id, feed_id) VALUES (1,'f'),(1,'g'),(2,'f');
|
INSERT INTO subscriptions (user_id, feed_id) VALUES (1,'f'),(1,'g'),(2,'f'),(2,'h');
|
||||||
INSERT INTO feeds (id, url, title, ttl_mins) VALUES ('f','u','F',30),('g','v','G',null);
|
INSERT INTO feeds (id, url, title, ttl_mins) VALUES ('f','u','F',30),('g','v','G',null),('h','w','H',null);
|
||||||
INSERT INTO entries (feed_id, guid, first_seen) VALUES ('f','a',0),('f','b',0),('f','c',0),('g','d',0);
|
INSERT INTO entries (feed_id, guid, first_seen) VALUES ('f','a',0),('f','b',0),('f','c',0),('g','d',0),('h','e',0);
|
||||||
INSERT INTO entry_state (user_id, feed_id, guid, read) VALUES (1,'f','a',true),(2,'f','b',true);
|
INSERT INTO entry_state (user_id, feed_id, guid, read) VALUES (1,'f','a',true),(2,'f','b',true);
|
||||||
INSERT INTO hidden (user_id, feed_id, guid) VALUES (1,'f','c');
|
INSERT INTO hidden (user_id, feed_id, guid) VALUES (1,'f','c');
|
||||||
INSERT INTO enclosures (id, feed_id, guid, url, path, state) VALUES
|
INSERT INTO enclosures (id, feed_id, guid, url, path, state) VALUES
|
||||||
@@ -2324,6 +2329,8 @@ mod tests {
|
|||||||
assert_eq!(one.ttl_mins, db.http_state(id).await.unwrap().ttl_mins, "{id}");
|
assert_eq!(one.ttl_mins, db.http_state(id).await.unwrap().ttl_mins, "{id}");
|
||||||
}
|
}
|
||||||
assert_eq!((list["f"].unread, list["g"].unread), (1, 1)); // a read, c hidden; sam's read is sam's
|
assert_eq!((list["f"].unread, list["g"].unread), (1, 1)); // a read, c hidden; sam's read is sam's
|
||||||
|
// Only sam reads h: still listed, for its title, but its items are not counted for ray (#165).
|
||||||
|
assert_eq!((list["h"].summary.title.as_deref(), list["h"].summary.entries), (Some("H"), 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|||||||
Reference in New Issue
Block a user