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 <noreply@anthropic.com>
This commit is contained in:
@@ -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).
|
- 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 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
|
### Fixed
|
||||||
|
|
||||||
|
|||||||
@@ -521,22 +521,22 @@ impl Db {
|
|||||||
// Of this person's feeds only, as the unread count is: counting every item of every feed on
|
// 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
|
// 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.
|
// 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(
|
let entries = counts(
|
||||||
"entries",
|
"entries",
|
||||||
"SELECT e.feed_id, count(*) AS n FROM entries e
|
"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
|
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",
|
WHERE (CAST($2 AS TEXT) IS NULL OR e.feed_id = $2) GROUP BY e.feed_id",
|
||||||
vec![user_id.into(), one()],
|
vec![user_id.into(), one()],
|
||||||
)
|
);
|
||||||
.await?;
|
|
||||||
let downloaded = counts(
|
let downloaded = counts(
|
||||||
"downloaded",
|
"downloaded",
|
||||||
"SELECT x.feed_id, count(*) AS n FROM enclosures x
|
"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
|
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",
|
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()],
|
vec![user_id.into(), one()],
|
||||||
)
|
);
|
||||||
.await?;
|
|
||||||
let unread = counts(
|
let unread = counts(
|
||||||
"unread",
|
"unread",
|
||||||
"SELECT e.feed_id, count(*) AS n FROM entries e
|
"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)
|
WHERE h.user_id = $1 AND h.feed_id = e.feed_id AND h.guid = e.guid)
|
||||||
GROUP BY e.feed_id",
|
GROUP BY e.feed_id",
|
||||||
vec![user_id.into(), one()],
|
vec![user_id.into(), one()],
|
||||||
)
|
);
|
||||||
.await?;
|
let blocked = tracing::Instrument::instrument(
|
||||||
let mut blocked: HashMap<String, Vec<String>> = tracing::Instrument::instrument(
|
|
||||||
blocklists::Entity::find().filter(blocklists::Column::UserId.eq(user_id)).all(&self.orm),
|
blocklists::Entity::find().filter(blocklists::Column::UserId.eq(user_id)).all(&self.orm),
|
||||||
step("blocked"),
|
step("blocked"),
|
||||||
)
|
);
|
||||||
.await?
|
|
||||||
.into_iter()
|
|
||||||
.map(|b| (b.feed_id, keywords(Some(b.words)).unwrap_or_default()))
|
|
||||||
.collect();
|
|
||||||
let mut rows = feeds::Entity::find();
|
let mut rows = feeds::Entity::find();
|
||||||
if let Some(id) = only {
|
if let Some(id) = only {
|
||||||
rows = rows.filter(feeds::Column::Id.eq(id));
|
rows = rows.filter(feeds::Column::Id.eq(id));
|
||||||
}
|
}
|
||||||
Ok(tracing::Instrument::instrument(rows.all(&self.orm), step("feeds"))
|
let rows = tracing::Instrument::instrument(rows.all(&self.orm), step("feeds"));
|
||||||
.await?
|
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<String, Vec<String>> =
|
||||||
|
blocked.into_iter().map(|b| (b.feed_id, keywords(Some(b.words)).unwrap_or_default())).collect();
|
||||||
|
Ok(rows
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|f| {
|
.map(|f| {
|
||||||
let id = f.id.clone();
|
let id = f.id.clone();
|
||||||
|
|||||||
@@ -1596,12 +1596,23 @@ async fn entry_page(
|
|||||||
limit,
|
limit,
|
||||||
search = search.is_some(),
|
search = search.is_some(),
|
||||||
);
|
);
|
||||||
let mut rows = match signal {
|
let list = async {
|
||||||
|
match signal {
|
||||||
Some(id) => {
|
Some(id) => {
|
||||||
db.signal_entries(user_id, id, filter, search, page.offset, limit, &order).instrument(in_list).await?
|
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(|| {
|
step("sanitize").in_scope(|| {
|
||||||
let mut sanitizer = feed_sanitizer();
|
let mut sanitizer = feed_sanitizer();
|
||||||
for row in &mut rows {
|
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?;
|
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 }))
|
Ok(Json(EntryPage { total, entries: rows }))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user