Cut per-request origin work: batched article loads, no session write per page
The origin was 3–8 ms per page, almost all of it per-statement overhead: an issue page ran two statements per pick (~55 on a 25-article issue) and every signed-in page wrote its session row back because `take_flash` called `Session::remove`, which marks the session modified even when the key is absent. - `Db::get_articles` loads an issue's articles and their social rows in two statements; both branches of `web::issue::load` use it. The single-id and batch queries share one projection via a macro. - `take_flash` reads before removing, and touches a signed-in session at most once a day so the inactivity expiry still slides. Anonymous requests never create a session. - `Server-Timing: app;dur=<ms>` on every response, outermost layer. - `reject_early_data`: 425 for a non-safe method that arrived as TLS 0-RTT data, so nginx `ssl_early_data on` is safe (RFC 8470 §5.2). - `[profile.release]`: fat LTO, one codegen unit (binary 46 → 29 MB). Docs: the Cloudflare proxy was retired on 2026-09-05 after measuring +43 ms per signed-in page from Boston; README reverse-proxy section is now the direct setup (upstream keepalive, 0-RTT lines) and the CDN runbook carries a retired-status banner. Dev seed, app-side: `/` 21 → 5 statements, 3.7 → 1.0 ms; `/feed.xml` 44 → 12, 9.1 → 3.0 ms; session writes per signed-in page 1 → 0. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Va5eMEmWEnjMXBsBob5FDW
This commit is contained in:
1 parent
bc40773964
commit
e727d53b85
8 files changed
+353
-27
No files matched your search
+13
-2
@@ -89,8 +89,17 @@ pub async fn load(
|
||||
};
|
||||
let (mut issue, from_json) = if let Some(raw) = row.issue_json.as_deref() {
|
||||
let mut issue: Issue = serde_json::from_str(raw).context("decoding issues.issue_json")?;
|
||||
// The snapshot's articles are refreshed from the live rows (social
|
||||
// scores move after publish) in one batched lookup.
|
||||
let ids: Vec<ArticleId> = issue
|
||||
.lineup
|
||||
.picks
|
||||
.iter()
|
||||
.map(|pick| pick.article.id)
|
||||
.collect();
|
||||
let mut articles = db.get_articles(&ids).await?;
|
||||
for pick in &mut issue.lineup.picks {
|
||||
if let Some(article) = db.get_article(pick.article.id).await? {
|
||||
if let Some(article) = articles.remove(&pick.article.id) {
|
||||
pick.article = article;
|
||||
}
|
||||
}
|
||||
@@ -103,12 +112,14 @@ pub async fn load(
|
||||
.bind(date.to_string())
|
||||
.fetch_all(db.pool())
|
||||
.await?;
|
||||
let ids: Vec<ArticleId> = rows.iter().map(|row| row.get("article_id")).collect();
|
||||
let mut articles = db.get_articles(&ids).await?;
|
||||
let mut picks = Vec::with_capacity(rows.len());
|
||||
let mut seen_sections = Vec::new();
|
||||
let mut summaries = BTreeMap::new();
|
||||
for pick_row in rows {
|
||||
let article_id: i64 = pick_row.get("article_id");
|
||||
let Some(article) = db.get_article(article_id).await? else {
|
||||
let Some(article) = articles.remove(&article_id) else {
|
||||
continue;
|
||||
};
|
||||
let section: String = pick_row.get("section");
|
||||
|
||||
Reference in new issue
Block a user