perf: round 19 — auth/WOPI/vCard/PROPFIND per-request & per-row alloc cuts

Benchmark-gated (examples/bench_round19_micro.rs, benches/ROUND19.md): every
section ships a BEFORE/AFTER counting-allocator arm with a byte/-value
equivalence gate and a GATE-FAIL-rollback exit. All eight pass. No Postgres.

- M1 verify_basic_auth cache key: blake3::hash(format!("{u}:{p}")) → incremental
  Hasher (byte-identical key, 2→0 allocs on every Basic-auth DAV request)
- M2 WopiTokenService: prebuild Validation/DecodingKey/EncodingKey in new()
  instead of per-call (mirrors JwtTokenService; 16→12 allocs/validate)
- V1/V2 vCard emit (contact_to_vcard/generate_vcard): FN fallback drops the
  throwaway to_string, NOTE skips the escape copy for newline-free notes, REV
  uses new common::fmt::compact_ical_utc stack renderer (11.5× vs chrono
  strftime, 3→0 allocs); per-contact 9→4 allocs
- M4 trash_service::row_to_item_dto: move name/path/blob_hash out of the owned
  row instead of cloning (3 clones/file row gone)
- M5 search cache key: Uuid::hyphenated().encode_lower stack buffer instead of
  to_string (identical u64 key, 1→0 allocs/request)
- M6 streaming PROPFIND: reuse one href buffer across the page instead of a
  format! per child (native + NC handlers; 192→3 allocs on a 64-child page)
- M7 nextcloud extract_url_user: return Cow instead of forcing into_owned
  (zero-alloc on the common ASCII-username path)

common::fmt::compact_ical_utc added with chrono-parity unit tests (CASES +
60-year sweep). cargo fmt + clippy --all-targets clean; 526 lib unit tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ront9bk7YMoffVQkGG47gh
This commit is contained in:
Claude
2026-07-19 22:29:48 +00:00
parent dc0c53ea0f
commit 9754aecfa9
13 changed files with 1240 additions and 70 deletions
+12 -10
View File
@@ -816,13 +816,15 @@ async fn build_streaming_propfind_response(
let mut chunk = Vec::with_capacity(batch.len() * 800);
{
let mut w = Writer::new(&mut chunk);
// One href buffer reused across the page instead of a fresh
// `format!` String per child (benches/ROUND19.md §M6).
let mut href = String::new();
for subfolder in batch.iter() {
let child_dead = dead_props_for(&subfolder.id, &subfolder_deads);
let href = format!(
"{}{}/",
base_href,
utf8_percent_encode(&subfolder.name, PATH_SEGMENT_ENCODE_SET)
);
href.clear();
href.push_str(&base_href);
href.extend(utf8_percent_encode(&subfolder.name, PATH_SEGMENT_ENCODE_SET));
href.push('/');
WebDavAdapter::write_folder_entry_with_dead_props(&mut w, subfolder, &propfind_request, &href, child_dead, quota)
.map_err(|e| std::io::Error::other(e.to_string()))?;
}
@@ -861,13 +863,13 @@ async fn build_streaming_propfind_response(
let mut chunk = Vec::with_capacity(batch_len * 800);
{
let mut w = Writer::new(&mut chunk);
// One href buffer reused across the page (benches/ROUND19.md §M6).
let mut href = String::new();
for file in batch.iter() {
let child_dead = dead_props_for(&file.id, &file_deads);
let href = format!(
"{}{}",
base_href,
utf8_percent_encode(&file.name, PATH_SEGMENT_ENCODE_SET)
);
href.clear();
href.push_str(&base_href);
href.extend(utf8_percent_encode(&file.name, PATH_SEGMENT_ENCODE_SET));
WebDavAdapter::write_file_entry_with_dead_props(&mut w, file, &propfind_request, &href, child_dead)
.map_err(|e| std::io::Error::other(e.to_string()))?;
}