perf: round 6 backend — CardDAV cursor streaming, borrowed NC id chain, binary UUID decode, one-alloc hex

Benchmark-gated (equivalence + BEFORE/AFTER in examples/bench_*, results
and reproduce commands in benches/ROUND6.md):

- CardDAV whole-book REPORT + depth-1 PROPFIND stream through a PG
  cursor (stream_contacts_by_book, 500-contact pages) instead of
  materialising every vCard twice: 8 000 contacts TTFB 37.4 → 7.6 ms
  (4.9x), peak heap 19.0 → 7.0 MiB (2.7x), wall -23%; REPORT and
  PROPFIND byte-identical to the buffered writers.

- NC numeric-id chain fully borrowed: get_or_create_file_ids/folder_ids
  take &[&str] and return HashMap<Uuid, i64>; batch_resolve_ids callers
  (PROPFIND pages, REPORT, trashbin, OCS search) pass id slices and look
  up via nc_id_of. 2.006 → 0.006 allocs/child (334x), 1.53x wall per
  500-child page. batch_check_favorites binds &[&str] as text[].

- file_blob_read_repository listing SELECTs drop id::text/folder_id::text
  server casts: rows decode binary Uuid (16 vs 36 bytes on the wire) and
  render once in row_to_file. A/B on 500-row pages: 1.225 → 1.044 ms
  mean (1.17x), p95 1.686 → 1.345 (bench_uuid_text_cast; single-row,
  param and min() sites left as-is deliberately).

- IncrementalHasher::finalize_hex renders through common::fmt::hex_lower
  instead of one format! per digest byte: 18 → 1 (md5) / 35 → 1 (sha256)
  allocs per chunk finalize, 14-15x wall.

- Share landing overlaps the access-count UPDATE with the unlock fetch
  via tokio::join! (one round-trip off every public link hit).

- REJECTED by benchmark and reverted: try_join_all fan-out of the
  batch-favorites authz pre-check — 42.6 → 56.4 ms cold, 0.15 → 0.23 ms
  warm against local-socket PG (bench_favorites_authz kept as evidence).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017aJu9ghvuT8WqC31ZEGTBA
This commit is contained in:
Claude
2026-07-18 09:03:33 +00:00
parent 61c9470981
commit 9729f033b2
24 changed files with 2012 additions and 154 deletions
@@ -146,6 +146,14 @@ impl ContactStoragePort for ContactStorageAdapter {
.await
}
fn stream_contacts_by_book(
&self,
address_book_id: Uuid,
) -> futures::stream::BoxStream<'static, Result<Contact, DomainError>> {
self.contact_repository
.stream_contacts_by_book(address_book_id)
}
async fn get_contacts_by_address_book_paginated(
&self,
address_book_id: &Uuid,
@@ -278,6 +278,45 @@ impl ContactRepository for ContactPgRepository {
Ok(contacts)
}
fn stream_contacts_by_book(
&self,
address_book_id: Uuid,
) -> futures::stream::BoxStream<'static, ContactRepositoryResult<Contact>> {
// ONE ordered scan served through a PG cursor — the CardDAV
// multistatus emitters page over this stream so only a page of
// contacts is resident (same design as the CalDAV round-5
// cursor; contacts have no master/exception bundling, so pages
// can cut anywhere).
let pool = self.pool.clone();
let stream: futures::stream::BoxStream<'static, ContactRepositoryResult<Contact>> =
Box::pin(async_stream::try_stream! {
let mut conn = pool.acquire().await.map_err(|e| {
DomainError::database_error(format!("Failed to acquire connection: {}", e))
})?;
let mut rows = sqlx::query(
r#"
SELECT
id, address_book_id, uid, full_name, first_name, last_name, nickname,
email, phone, address, organization, title, notes, photo_url,
birthday, anniversary, vcard, etag, created_at, updated_at
FROM carddav.contacts
WHERE address_book_id = $1
ORDER BY full_name, first_name, last_name
"#,
)
.bind(address_book_id)
.fetch(&mut *conn);
use futures::TryStreamExt;
while let Some(row) = rows.try_next().await.map_err(|e| {
DomainError::database_error(format!("Failed to stream contacts: {}", e))
})? {
yield Self::row_to_contact(&row)?;
}
});
stream
}
async fn get_contacts_by_address_book(
&self,
address_book_id: &Uuid,
@@ -259,8 +259,9 @@ impl FavoritesRepositoryPort for FavoritesPgRepository {
return Ok(HashSet::new());
}
// Collect just the IDs for the IN clause
let ids: Vec<String> = item_ids.iter().map(|(id, _)| id.to_string()).collect();
// Collect just the IDs for the IN clause — sqlx binds `&[&str]` as
// text[], so no per-id String is needed.
let ids: Vec<&str> = item_ids.iter().map(|(id, _)| *id).collect();
let rows = sqlx::query(
"SELECT item_id FROM auth.user_favorites WHERE user_id = $1 AND item_id = ANY($2)",
@@ -11,9 +11,9 @@
/// Post-D7-step-6: `storage.files.user_id` dropped, so it's no
/// longer projected.
type MediaFileRow = (
String, // id
Uuid, // id (binary decode; benches/ROUND6.md §10)
String, // name
Option<String>, // folder_id
Option<Uuid>, // folder_id
Option<String>, // folder path
i64, // size
String, // mime_type
@@ -83,9 +83,9 @@ const CALLER_CAN_READ_DRIVE: &str = "EXISTS (\
/// longer part of the tuple; `row_to_file` populates the entity's
/// legacy `user_id` field with `None`.
type FileRow = (
Uuid,
String,
String,
Option<String>,
Option<Uuid>,
Option<String>,
i64,
String,
@@ -269,7 +269,7 @@ impl FileBlobReadRepository {
let where_clause = conditions.join(" AND ");
let sql = format!(
"SELECT fi.id::text, fi.name, fi.folder_id::text, fo.path, \
"SELECT fi.id, fi.name, fi.folder_id, fo.path, \
fi.size, fi.mime_type, \
EXTRACT(EPOCH FROM fi.created_at)::bigint, \
EXTRACT(EPOCH FROM fi.updated_at)::bigint, \
@@ -319,7 +319,7 @@ impl FileBlobReadRepository {
}
let rows = sqlx::query_as::<_, FileRow>(
"SELECT fi.id::text, fi.name, fi.folder_id::text, fo.path, \
"SELECT fi.id, fi.name, fi.folder_id, fo.path, \
fi.size, fi.mime_type, \
EXTRACT(EPOCH FROM fi.created_at)::bigint, \
EXTRACT(EPOCH FROM fi.updated_at)::bigint, \
@@ -415,9 +415,9 @@ impl FileBlobReadRepository {
#[allow(clippy::too_many_arguments)]
fn row_to_file(
id: String,
id: Uuid,
name: String,
folder_id: Option<String>,
folder_id: Option<Uuid>,
folder_path: Option<String>,
size: i64,
mime_type: String,
@@ -428,12 +428,12 @@ impl FileBlobReadRepository {
updated_by: Option<Uuid>,
) -> Result<File, DomainError> {
File::from_materialized_row(
id,
id.to_string(),
name,
folder_path.as_deref(),
size as u64,
mime_type,
folder_id,
folder_id.map(|u| u.to_string()),
created_at as u64,
modified_at as u64,
blob_hash,
@@ -550,7 +550,7 @@ impl FileBlobReadRepository {
AND (g.expires_at IS NULL OR g.expires_at > NOW())
AND (d.policies->>'include_in_photo_index')::boolean = true
)
SELECT top.id::text, top.name, top.folder_id::text, fo.path,
SELECT top.id, top.name, top.folder_id, fo.path,
top.size, top.mime_type,
EXTRACT(EPOCH FROM top.created_at)::bigint,
EXTRACT(EPOCH FROM top.updated_at)::bigint,
@@ -679,9 +679,9 @@ impl FileReadPort for FileBlobReadRepository {
let row = sqlx::query_as::<
_,
(
String, // id
Uuid, // id (binary decode)
String, // name
Option<String>, // folder_id
Option<Uuid>, // folder_id
Option<String>, // folder path
i64, // size
String, // mime_type
@@ -693,7 +693,7 @@ impl FileReadPort for FileBlobReadRepository {
),
>(
r#"
SELECT fi.id::text, fi.name, fi.folder_id::text, fo.path,
SELECT fi.id, fi.name, fi.folder_id, fo.path,
fi.size, fi.mime_type,
EXTRACT(EPOCH FROM fi.created_at)::bigint,
EXTRACT(EPOCH FROM fi.updated_at)::bigint,
@@ -726,9 +726,9 @@ impl FileReadPort for FileBlobReadRepository {
let row = sqlx::query_as::<
_,
(
Uuid,
String,
String,
Option<String>,
Option<Uuid>,
Option<String>,
i64,
String,
@@ -740,7 +740,7 @@ impl FileReadPort for FileBlobReadRepository {
),
>(
r#"
SELECT fi.id::text, fi.name, fi.folder_id::text, fo.path,
SELECT fi.id, fi.name, fi.folder_id, fo.path,
fi.size, fi.mime_type,
EXTRACT(EPOCH FROM fi.created_at)::bigint,
EXTRACT(EPOCH FROM fi.updated_at)::bigint,
@@ -768,7 +768,7 @@ impl FileReadPort for FileBlobReadRepository {
let rows: Vec<FileRow> = if let Some(fid) = folder_id {
sqlx::query_as(
r#"
SELECT fi.id::text, fi.name, fi.folder_id::text, fo.path,
SELECT fi.id, fi.name, fi.folder_id, fo.path,
fi.size, fi.mime_type,
EXTRACT(EPOCH FROM fi.created_at)::bigint,
EXTRACT(EPOCH FROM fi.updated_at)::bigint,
@@ -787,7 +787,7 @@ impl FileReadPort for FileBlobReadRepository {
} else {
sqlx::query_as(
r#"
SELECT fi.id::text, fi.name, fi.folder_id::text, fo.path,
SELECT fi.id, fi.name, fi.folder_id, fo.path,
fi.size, fi.mime_type,
EXTRACT(EPOCH FROM fi.created_at)::bigint,
EXTRACT(EPOCH FROM fi.updated_at)::bigint,
@@ -848,7 +848,7 @@ impl FileReadPort for FileBlobReadRepository {
};
let sql = format!(
r#"
SELECT fi.id::text, fi.name, fi.folder_id::text, fo.path,
SELECT fi.id, fi.name, fi.folder_id, fo.path,
fi.size, fi.mime_type,
EXTRACT(EPOCH FROM fi.created_at)::bigint,
EXTRACT(EPOCH FROM fi.updated_at)::bigint,
@@ -1014,9 +1014,9 @@ impl FileReadPort for FileBlobReadRepository {
sqlx::query_as::<
_,
(
Uuid,
String,
String,
Option<String>,
Option<Uuid>,
Option<String>,
i64,
String,
@@ -1028,7 +1028,7 @@ impl FileReadPort for FileBlobReadRepository {
),
>(
r#"
SELECT fi.id::text, fi.name, fi.folder_id::text, fo.path,
SELECT fi.id, fi.name, fi.folder_id, fo.path,
fi.size, fi.mime_type,
EXTRACT(EPOCH FROM fi.created_at)::bigint,
EXTRACT(EPOCH FROM fi.updated_at)::bigint,
@@ -1052,9 +1052,9 @@ impl FileReadPort for FileBlobReadRepository {
sqlx::query_as::<
_,
(
Uuid,
String,
String,
Option<String>,
Option<Uuid>,
Option<String>,
i64,
String,
@@ -1066,7 +1066,7 @@ impl FileReadPort for FileBlobReadRepository {
),
>(
r#"
SELECT fi.id::text, fi.name, fi.folder_id::text, fo.path,
SELECT fi.id, fi.name, fi.folder_id, fo.path,
fi.size, fi.mime_type,
EXTRACT(EPOCH FROM fi.created_at)::bigint,
EXTRACT(EPOCH FROM fi.updated_at)::bigint,
@@ -1107,12 +1107,12 @@ impl FileReadPort for FileBlobReadRepository {
let stream = async_stream::try_stream! {
let mut row_stream = sqlx::query_as::<_, (
String, String, Option<String>, Option<String>,
Uuid, String, Option<Uuid>, Option<String>,
i64, String, i64, i64, String,
Option<Uuid>, Option<Uuid>, // created_by, updated_by (§14)
)>(
r#"
SELECT fi.id::text, fi.name, fi.folder_id::text, fo.path,
SELECT fi.id, fi.name, fi.folder_id, fo.path,
fi.size, fi.mime_type,
EXTRACT(EPOCH FROM fi.created_at)::bigint,
EXTRACT(EPOCH FROM fi.updated_at)::bigint,
@@ -1195,7 +1195,7 @@ impl FileReadPort for FileBlobReadRepository {
let offset_bind = bind_idx + 2;
let sql = format!(
"SELECT fi.id::text, fi.name, fi.folder_id::text, fo.path, \
"SELECT fi.id, fi.name, fi.folder_id, fo.path, \
fi.size, fi.mime_type, \
EXTRACT(EPOCH FROM fi.created_at)::bigint, \
EXTRACT(EPOCH FROM fi.updated_at)::bigint, \
@@ -1214,9 +1214,9 @@ impl FileReadPort for FileBlobReadRepository {
let mut query = sqlx::query_as::<
_,
(
Uuid,
String,
String,
Option<String>,
Option<Uuid>,
Option<String>,
i64,
String,
@@ -1327,7 +1327,7 @@ impl FileReadPort for FileBlobReadRepository {
// ── Single query with COUNT(*) OVER() ──
let sql = format!(
"SELECT fi.id::text, fi.name, fi.folder_id::text, fo.path, \
"SELECT fi.id, fi.name, fi.folder_id, fo.path, \
fi.size, fi.mime_type, \
EXTRACT(EPOCH FROM fi.created_at)::bigint, \
EXTRACT(EPOCH FROM fi.updated_at)::bigint, \
@@ -1346,9 +1346,9 @@ impl FileReadPort for FileBlobReadRepository {
let mut query = sqlx::query_as::<
_,
(
Uuid,
String,
String,
Option<String>,
Option<Uuid>,
Option<String>,
i64,
String,
@@ -1420,7 +1420,7 @@ impl FileReadPort for FileBlobReadRepository {
let rows: Vec<FileRow> = if let Some(fid) = folder_id {
sqlx::query_as(
r#"
SELECT fi.id::text, fi.name, fi.folder_id::text, fo.path,
SELECT fi.id, fi.name, fi.folder_id, fo.path,
fi.size, fi.mime_type,
EXTRACT(EPOCH FROM fi.created_at)::bigint,
EXTRACT(EPOCH FROM fi.updated_at)::bigint,
@@ -1450,7 +1450,7 @@ impl FileReadPort for FileBlobReadRepository {
} else {
sqlx::query_as(
r#"
SELECT fi.id::text, fi.name, fi.folder_id::text, fo.path,
SELECT fi.id, fi.name, fi.folder_id, fo.path,
fi.size, fi.mime_type,
EXTRACT(EPOCH FROM fi.created_at)::bigint,
EXTRACT(EPOCH FROM fi.updated_at)::bigint,