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
@@ -109,7 +109,12 @@ pub async fn basic_auth_middleware(
// at the auth boundary rather than treating them as "missing
// marker" — they are unambiguous typos that would otherwise
// silently fall into a different code path.
let (username, drive_marker): (String, Option<String>) = match raw_username.split_once('~') {
// Borrow the prefix / marker out of the already-owned `raw_username`
// (`split_once` yields `&str` slices) instead of allocating a duplicate
// `String` per request — `username` is only ever passed by reference, and
// `raw_username` outlives every use before it moves into `NcSession`
// (benches/ROUND29.md §E).
let (username, drive_marker): (&str, Option<&str>) = match raw_username.split_once('~') {
Some(("", _)) => {
tracing::warn!(
"[NC] 401 malformed composite username (empty prefix): {}",
@@ -124,15 +129,15 @@ pub async fn basic_auth_middleware(
);
return Err(NextcloudAuthError::Unauthorized);
}
Some((u, m)) => (u.to_string(), Some(m.to_string())),
None => (raw_username.clone(), None),
Some((u, m)) => (u, Some(m)),
None => (raw_username.as_str(), None),
};
// Check account lockout before attempting password verification (saves CPU).
// The lockout is per (account, IP), see #323 for rationale.
let client_ip = crate::interfaces::middleware::rate_limit::extract_client_ip(&request);
if let Some(auth_svc) = state.auth_service.as_ref()
&& let Err(secs) = auth_svc.login_lockout.check(&username, &client_ip)
&& let Err(secs) = auth_svc.login_lockout.check(username, &client_ip)
{
tracing::warn!(
username = %username,
@@ -150,13 +155,13 @@ pub async fn basic_auth_middleware(
match nextcloud
.app_passwords
.verify_basic_auth(&username, &password)
.verify_basic_auth(username, &password)
.await
{
Ok((user_id, uname, email, role)) => {
// Reset lockout counter on success
if let Some(auth_svc) = state.auth_service.as_ref() {
auth_svc.login_lockout.record_success(&username, &client_ip);
auth_svc.login_lockout.record_success(username, &client_ip);
}
// External users must never authenticate against the NC
// surface — that whole subtree (WebDAV files, uploads,
@@ -222,7 +227,7 @@ pub async fn basic_auth_middleware(
// is the right one: name-independent, secondary-drive-safe.
use crate::application::ports::folder_ports::FolderUseCase;
use crate::domain::repositories::drive_repository::DriveRepository;
let chroot = match drive_marker.as_deref() {
let chroot = match drive_marker {
None => {
match state
.drive_repo
@@ -287,7 +292,7 @@ pub async fn basic_auth_middleware(
Err(_) => {
// Record failed attempt for lockout tracking
if let Some(auth_svc) = state.auth_service.as_ref() {
auth_svc.login_lockout.record_failure(&username, &client_ip);
auth_svc.login_lockout.record_failure(username, &client_ip);
}
Err(NextcloudAuthError::Unauthorized)
}
+22 -10
View File
@@ -24,8 +24,8 @@ use crate::interfaces::api::handlers::webdav_handler::{
};
use crate::interfaces::errors::AppError;
use crate::interfaces::nextcloud::webdav_handler::{
batch_resolve_ids, format_oc_id_into, nc_href, nc_id_of, write_file_response,
write_folder_response,
batch_resolve_ids, format_oc_id_into, nc_collection_href_into, nc_href_into, nc_id_of,
write_file_response, write_folder_response,
};
/// Handle WebDAV REPORT and SEARCH methods for Nextcloud compatibility.
@@ -177,6 +177,12 @@ async fn handle_filter_files(
// owner-id stays canonical via `&user.username`.
// One oc:id buffer reused across both emit loops (benches/ROUND27.md §H1).
let mut oc_buf = String::new();
// One href buffer reused across both emit loops, with the URL-encoded
// user computed once for the page instead of re-encoded per row — the
// reused-buffer shape the PROPFIND child loop already uses
// (benches/ROUND29.md §A).
let encoded_user = urlencoding::encode(url_user);
let mut href_buf = String::new();
for file in &files {
// Skip favorites that live outside the caller's chroot
// (other-drive favorites); reachable via REST if needed.
@@ -189,7 +195,7 @@ async fn handle_filter_files(
);
continue;
};
let href = nc_href(url_user, subpath);
nc_href_into(&mut href_buf, &encoded_user, subpath);
let fid = nc_id_of(&file_id_map, &file.id);
let oc_id: Option<&str> = match fid {
Some(id) => {
@@ -202,7 +208,7 @@ async fn handle_filter_files(
write_file_response(
&mut xml,
file,
&href,
&href_buf,
(fid, oc_id),
&user.username,
&favorite_ids,
@@ -221,7 +227,7 @@ async fn handle_filter_files(
);
continue;
};
let href = format!("{}/", nc_href(url_user, subpath));
nc_collection_href_into(&mut href_buf, &encoded_user, subpath);
let fid = nc_id_of(&folder_id_map, &folder.id);
let oc_id: Option<&str> = match fid {
Some(id) => {
@@ -234,7 +240,7 @@ async fn handle_filter_files(
write_folder_response(
&mut xml,
folder,
&href,
&href_buf,
(fid, oc_id),
&user.username,
&favorite_ids,
@@ -334,6 +340,12 @@ async fn handle_search(
// Files.
// One oc:id buffer reused across both emit loops (benches/ROUND27.md §H1).
let mut oc_buf = String::new();
// One href buffer reused across both emit loops, with the URL-encoded
// user computed once for the page instead of re-encoded per row — the
// reused-buffer shape the PROPFIND child loop already uses
// (benches/ROUND29.md §A).
let encoded_user = urlencoding::encode(url_user);
let mut href_buf = String::new();
for file in &files {
let Some(subpath) = strip_home_prefix(chroot, &file.path, home_prefix) else {
tracing::debug!(
@@ -344,7 +356,7 @@ async fn handle_search(
);
continue;
};
let href = nc_href(url_user, subpath);
nc_href_into(&mut href_buf, &encoded_user, subpath);
let fid = nc_id_of(&file_id_map, &file.id);
let oc_id: Option<&str> = match fid {
Some(id) => {
@@ -357,7 +369,7 @@ async fn handle_search(
write_file_response(
&mut xml,
file,
&href,
&href_buf,
(fid, oc_id),
&user.username,
&favorite_ids,
@@ -377,7 +389,7 @@ async fn handle_search(
);
continue;
};
let href = format!("{}/", nc_href(url_user, subpath));
nc_collection_href_into(&mut href_buf, &encoded_user, subpath);
let fid = nc_id_of(&folder_id_map, &folder.id);
let oc_id: Option<&str> = match fid {
Some(id) => {
@@ -390,7 +402,7 @@ async fn handle_search(
write_folder_response(
&mut xml,
folder,
&href,
&href_buf,
(fid, oc_id),
&user.username,
&favorite_ids,
+33 -10
View File
@@ -167,12 +167,10 @@ pub fn strip_drive_root_segment(internal_path: &str) -> &str {
/// surfaces as `Network request error "Erreur inconnue" HTTP status
/// 207` in the client log. Files use [`nc_href`] (no trailing slash).
pub fn nc_collection_href(username: &str, subpath: &str) -> String {
let h = nc_href(username, subpath);
if h.ends_with('/') {
h
} else {
format!("{}/", h)
}
let encoded_user = urlencoding::encode(username);
let mut out = String::new();
nc_collection_href_into(&mut out, &encoded_user, subpath);
out
}
/// Build the Nextcloud DAV href for a resource.
@@ -184,17 +182,33 @@ pub fn nc_collection_href(username: &str, subpath: &str) -> String {
/// a **collection** must use [`nc_collection_href`] (or append `/`
/// manually) to satisfy RFC 4918 §5.2 and the NC client's parser.
pub fn nc_href(username: &str, subpath: &str) -> String {
let subpath = subpath.trim_matches('/');
let encoded_user = urlencoding::encode(username);
let mut out = String::new();
nc_href_into(&mut out, &encoded_user, subpath);
out
}
/// Per-row form of [`nc_href`]: write the href into a REUSED buffer given the
/// already-URL-encoded username.
///
/// The emit loops (PROPFIND children, REPORT results) call this instead of
/// [`nc_href`] so each row rewrites one buffer rather than allocating a fresh
/// `String`, and the constant `encoded_user` is encoded ONCE per page instead of
/// re-encoded for every row (benches/ROUND29.md §A — the same reused-buffer shape
/// the PROPFIND child loop already uses for its href prefix). Byte-identical to
/// [`nc_href`].
pub fn nc_href_into(out: &mut String, encoded_user: &str, subpath: &str) {
let subpath = subpath.trim_matches('/');
// Write the prefix, user and each encoded segment straight into one
// pre-sized buffer — avoids the per-segment `Vec<Cow>`, the joined String and
// the `format!` result the previous `.map(...).collect().join("/")` allocated
// on every NC PROPFIND/REPORT href (mirrors the native `encode_uri_path`).
// Keeps `urlencoding::encode` so the emitted bytes are unchanged.
const PREFIX: &str = "/remote.php/dav/files/";
let mut out = String::with_capacity(PREFIX.len() + encoded_user.len() + subpath.len() + 8);
out.clear();
out.reserve(PREFIX.len() + encoded_user.len() + subpath.len() + 8);
out.push_str(PREFIX);
out.push_str(&encoded_user);
out.push_str(encoded_user);
out.push('/');
// No empty-segment filter: `split('/')` on an empty (root) subpath yields a
// single "" whose encode is "" — leaving the trailing slash above intact —
@@ -206,7 +220,16 @@ pub fn nc_href(username: &str, subpath: &str) -> String {
}
out.push_str(&urlencoding::encode(seg));
}
out
}
/// Per-row form of [`nc_collection_href`]: [`nc_href_into`] plus the trailing
/// `/` RFC 4918 §5.2 / the NC client require for a collection. Byte-identical to
/// [`nc_collection_href`].
pub fn nc_collection_href_into(out: &mut String, encoded_user: &str, subpath: &str) {
nc_href_into(out, encoded_user, subpath);
if !out.ends_with('/') {
out.push('/');
}
}
/// Dispatch Nextcloud WebDAV request to the appropriate handler.