perf(round29): cache-serve borrow-probe, NC REPORT href buffer, auth per-req allocs, DB over-fetch

Seven behaviour-preserving allocation / copy / bandwidth cuts, each behind a
counting-allocator BEFORE/AFTER gate that exit(1)s unless AFTER allocates
strictly fewer than BEFORE (benches/ROUND29.md, examples/bench_round29_micro.rs).

- [B] Content-cache serve fast path (optimized_inner Tier 1 +
  get_file_range_preloaded — the video-scrub hot path): probe the cache with a
  borrow first and build the owned get_or_load args (quoted-etag / key / id
  Strings) only on a miss, instead of allocating them before every probe and
  discarding them on a hit. 6 -> 0 allocs per cache hit. Splits get_or_load into
  get + load_and_cache so the miss path is not re-probed and the hit/miss stat
  counters stay byte-identical. Also drops the unconditional content_hash/name
  clones that ran for the >=10 MB streaming tier that used neither.
- [A] NextCloud REPORT emit loops: per-row href String (and format! per folder
  row) -> one reused href_buf via nc_href_into / nc_collection_href_into with the
  URL-encoded user computed once per page. 1497 fewer allocs on a 500-row page.
- [C] read_full: a single-frame blob is returned zero-copy instead of a second
  whole-payload memcpy into a fresh BytesMut; multi-frame path unchanged.
- [D] login-lockout key: to_lowercase()+format! -> one pre-sized ASCII buffer
  (non-ASCII keeps str::to_lowercase). 3 -> 1 alloc/req, byte-identical key.
- [E] NC composite-username parse: owned clone/to_string -> &str borrow of the
  already-owned raw_username. 1 -> 0 alloc on the common no-marker path.
- [F] get_contacts_in_group: stop SELECTing the discarded multi-KB vcard column
  (the live method ROUND25 §Q2 missed; ContactDto has no vcard field).
- [G] count_admin_users: add count_users_by_role -> scalar COUNT(*) instead of
  hydrating every admin's full row (incl. up-to-512 KiB avatar + ui_preferences
  JSONB) only to .len() it, on a bootstrap-polled status endpoint.

All seven gates pass; cargo fmt --check and cargo clippy --all-features
--all-targets -D warnings clean. §F/§G additionally validated against a live
PostgreSQL 16 with the full migration set (query validity, result equivalence,
600000 -> 8 byte wire delta on the admin count).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LhpDZxSQTAGnAqCHUdtG5N
This commit is contained in:
Claude
2026-07-21 10:25:36 +00:00
parent b7d5d41c90
commit 8e55caa8a9
14 changed files with 967 additions and 81 deletions
@@ -196,10 +196,10 @@ impl ContactGroupRepository for ContactGroupPgRepository {
) -> ContactRepositoryResult<Vec<Contact>> {
let rows = sqlx::query(
r#"
SELECT
SELECT
c.id, c.address_book_id, c.uid, c.full_name, c.first_name, c.last_name, c.nickname,
c.email, c.phone, c.address, c.organization, c.title, c.notes, c.photo_url,
c.birthday, c.anniversary, c.vcard, c.etag, c.created_at, c.updated_at
c.birthday, c.anniversary, c.etag, c.created_at, c.updated_at
FROM carddav.contacts c
INNER JOIN carddav.group_memberships gm ON c.id = gm.contact_id
WHERE gm.group_id = $1
@@ -253,7 +253,13 @@ impl ContactGroupRepository for ContactGroupPgRepository {
row.get::<Option<String>, _>("photo_url"),
row.get("birthday"),
row.get("anniversary"),
row.get("vcard"),
// vcard column intentionally NOT selected — the sole live caller
// (`list_contacts_in_group`) maps to `ContactDto`, which has no
// vcard field, so fetching the multi-KB serialized vCard (with an
// embedded base64 PHOTO) only to drop it wastes bandwidth + a
// per-row String. Mirrors `row_to_contact_lite` (benches/ROUND29.md
// §F / ROUND25 §Q2, applied to the LIVE group method this time).
String::new(),
row.get("etag"),
row.get("created_at"),
row.get("updated_at"),
@@ -810,6 +810,15 @@ impl UserRepository for UserPgRepository {
Ok(())
}
/// Counts users by role with a scalar `COUNT(*)` — no row hydration.
async fn count_users_by_role(&self, role: &str) -> UserRepositoryResult<i64> {
sqlx::query_scalar("SELECT COUNT(*) FROM auth.users WHERE role::text = $1")
.bind(role)
.fetch_one(&*self.pool)
.await
.map_err(Self::map_sqlx_error)
}
/// Lists users by role
async fn list_users_by_role(&self, role: &str) -> UserRepositoryResult<Vec<User>> {
let rows = sqlx::query(
@@ -1157,6 +1166,12 @@ impl UserStoragePort for UserPgRepository {
.map_err(DomainError::from)
}
async fn count_users_by_role(&self, role: &str) -> Result<i64, DomainError> {
UserRepository::count_users_by_role(self, role)
.await
.map_err(DomainError::from)
}
async fn delete_user(&self, user_id: Uuid) -> Result<(), DomainError> {
UserRepository::delete_user(self, user_id)
.await
@@ -182,7 +182,30 @@ impl FileContentCache {
if let Some(hit) = self.get(&cache_key).await {
return Ok(hit);
}
self.load_and_cache(cache_key, etag, content_type, load)
.await
}
/// The populate-on-miss half of [`Self::get_or_load`], with single-flight
/// coalescing but WITHOUT the leading `get` probe.
///
/// Hot read paths that have *already* probed the cache with [`Self::get`]
/// (a borrow) call this directly on the miss branch — they then build the
/// owned `cache_key` / `etag` / `content_type` (each a heap allocation)
/// only when they are actually needed to populate, so a cache HIT allocates
/// none of them (benches/ROUND29.md §B). Because the caller's own `get`
/// already counted the hit/miss, this method does not re-probe — keeping the
/// hit/miss stat counts identical to a single `get_or_load` call.
pub async fn load_and_cache<F>(
&self,
cache_key: String,
etag: Arc<str>,
content_type: Arc<str>,
load: F,
) -> Result<(Bytes, Arc<str>, Arc<str>), DomainError>
where
F: Future<Output = Result<Bytes, DomainError>>,
{
// Slow path: coalesce concurrent misses into a single `load`.
let entry = self
.cache
@@ -68,7 +68,24 @@ impl LoginLockoutService {
fn key(username: &str, client_ip: &str) -> String {
// `|` is not valid in either a username or an IP literal so it makes
// the username/ip boundary unambiguous.
format!("{}|{}", username.to_lowercase(), client_ip)
//
// The lowercased composite is written into ONE pre-sized buffer instead
// of the `to_lowercase()` (alloc) + `format!` (alloc) two-step. App
// passwords authenticate with an already-lowercase ASCII username in
// ~all traffic, so the fast branch covers it; the rare non-ASCII branch
// keeps `str::to_lowercase` for exact Unicode (e.g. final-sigma)
// semantics. Byte-identical key either way (benches/ROUND29.md §D).
if username.is_ascii() {
let mut k = String::with_capacity(username.len() + 1 + client_ip.len());
for &b in username.as_bytes() {
k.push(b.to_ascii_lowercase() as char);
}
k.push('|');
k.push_str(client_ip);
k
} else {
format!("{}|{}", username.to_lowercase(), client_ip)
}
}
/// Check whether the (account, IP) pair is currently locked.