From 21fa0ae47622f603ad85260ef741abd637ca9246 Mon Sep 17 00:00:00 2001 From: rays Date: Fri, 23 Oct 2026 15:28:09 +0000 Subject: [PATCH] Run the item page's count and the feed list's queries together (#346) entry_page awaited the page's query and then its count, about 40 and 24 ms on /api/entries, the busiest route; feed_list awaited five independent queries in turn (entries, downloaded, unread, blocked, feeds), 59 ms of a 67 ms /api/feeds, with the pool's connections idle. Both now use tokio::try_join!. On SQLite they queue on its connection as before. Co-Authored-By: Claude Opus 5.5 --- crates/ipx/CHANGELOG.md | 1 + crates/ipx/src/db.rs | 32 ++++++++++++++++++-------------- crates/ipx/src/web.rs | 23 +++++++++++++++-------- 3 files changed, 34 insertions(+), 22 deletions(-) diff --git a/crates/ipx/CHANGELOG.md b/crates/ipx/CHANGELOG.md index 9cbde91..a34c56d 100644 --- a/crates/ipx/CHANGELOG.md +++ b/crates/ipx/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Traces show more of where a request's time goes: a page of items' query and its files apart, artwork's cache, check and fetch, the status counts, and each part of changing or leaving a feed (#350). - A page of items finds its files by feed as well as item: it read every file's row in the database, about a quarter of a second a page (#344). +- A page of items asks for its count beside the items, and the feed list asks its five questions at once, where each waited for the one before (#346). ### Fixed diff --git a/crates/ipx/src/db.rs b/crates/ipx/src/db.rs index b999bdf..6565dd7 100644 --- a/crates/ipx/src/db.rs +++ b/crates/ipx/src/db.rs @@ -521,22 +521,22 @@ impl Db { // 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. + // Five queries that need nothing of each other, run at once: one after another they were + // 59 ms of a 67 ms feed list, on a pool of idle connections (#346). let entries = counts( "entries", "SELECT e.feed_id, count(*) AS n FROM entries e JOIN subscriptions sub ON sub.user_id = $1 AND sub.feed_id = e.feed_id WHERE (CAST($2 AS TEXT) IS NULL OR e.feed_id = $2) GROUP BY e.feed_id", vec![user_id.into(), one()], - ) - .await?; + ); let downloaded = counts( "downloaded", "SELECT x.feed_id, count(*) AS n FROM enclosures x JOIN subscriptions sub ON sub.user_id = $1 AND sub.feed_id = x.feed_id 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?; + ); let unread = counts( "unread", "SELECT e.feed_id, count(*) AS n FROM entries e @@ -549,22 +549,26 @@ impl Db { WHERE h.user_id = $1 AND h.feed_id = e.feed_id AND h.guid = e.guid) GROUP BY e.feed_id", vec![user_id.into(), one()], - ) - .await?; - let mut blocked: HashMap> = tracing::Instrument::instrument( + ); + let blocked = tracing::Instrument::instrument( blocklists::Entity::find().filter(blocklists::Column::UserId.eq(user_id)).all(&self.orm), step("blocked"), - ) - .await? - .into_iter() - .map(|b| (b.feed_id, keywords(Some(b.words)).unwrap_or_default())) - .collect(); + ); let mut rows = feeds::Entity::find(); if let Some(id) = only { rows = rows.filter(feeds::Column::Id.eq(id)); } - Ok(tracing::Instrument::instrument(rows.all(&self.orm), step("feeds")) - .await? + let rows = tracing::Instrument::instrument(rows.all(&self.orm), step("feeds")); + let (entries, downloaded, unread, blocked, rows) = tokio::try_join!( + entries, + downloaded, + unread, + async { blocked.await.map_err(anyhow::Error::from) }, + async { rows.await.map_err(anyhow::Error::from) }, + )?; + let mut blocked: HashMap> = + blocked.into_iter().map(|b| (b.feed_id, keywords(Some(b.words)).unwrap_or_default())).collect(); + Ok(rows .into_iter() .map(|f| { let id = f.id.clone(); diff --git a/crates/ipx/src/web.rs b/crates/ipx/src/web.rs index ab2b329..1fb4d7f 100644 --- a/crates/ipx/src/web.rs +++ b/crates/ipx/src/web.rs @@ -1596,12 +1596,23 @@ async fn entry_page( limit, search = search.is_some(), ); - let mut rows = match signal { - Some(id) => { - db.signal_entries(user_id, id, filter, search, page.offset, limit, &order).instrument(in_list).await? + let list = async { + match signal { + Some(id) => { + db.signal_entries(user_id, id, filter, search, page.offset, limit, &order).instrument(in_list).await + } + None => db.entries_of(user_id, feeds, filter, search, page.offset, limit, &order).instrument(in_list).await, } - None => db.entries_of(user_id, feeds, filter, search, page.offset, limit, &order).instrument(in_list).await?, }; + let count = async { + match signal { + Some(id) => db.signal_count(user_id, id, filter, search).instrument(step("count_in")).await, + None => db.count_of(user_id, feeds, filter, search).instrument(step("count_in")).await, + } + }; + // The count beside the list, not after it: each waited on the other, about 40 and 24 ms on the + // busiest route (#346). + let (mut rows, total) = tokio::try_join!(list, count)?; step("sanitize").in_scope(|| { let mut sanitizer = feed_sanitizer(); for row in &mut rows { @@ -1609,10 +1620,6 @@ async fn entry_page( } }); crate::deadair::for_reader(&state.ctx, user_id, &mut rows).instrument(step("deadair")).await?; - let total = match signal { - Some(id) => db.signal_count(user_id, id, filter, search).instrument(step("count_in")).await?, - None => db.count_of(user_id, feeds, filter, search).instrument(step("count_in")).await?, - }; Ok(Json(EntryPage { total, entries: rows })) }