perf: round 21 — CalDAV/CardDAV row-mapper pre-size, dedup hash-bind & digest-key dedup, CardDAV etag/BDAY emit, NC trashbin content-type

Round 21 of the benchmark-gated perf sweep. Six behaviour-preserving,
allocation-reducing changes, each with a BEFORE/AFTER counting-allocator
section in examples/bench_round21_micro.rs and a byte/-value equivalence
gate; all six pass their deterministic alloc gate (a non-winning AFTER
exits 1 = rollback).

- R1: pre-size the 16 CalDAV/CardDAV row-mapper Vecs (+1 HashMap) with
  Vec::with_capacity(rows.len()) — the ROUND20 §I1 file-side pattern
  extended to the calendar/contact repos it deferred. 7 → 1 allocs/op.
- R2: settle_batch binds a borrowed Vec<&str> instead of cloning every
  chunk hash into a Vec<String> (sqlx encodes &[&str] as text[]
  identically; favorites_pg_repository.rs:271 precedent). 33 → 1 allocs/op,
  39x wall.
- R3: store_loose_chunks keys its intra-request dedup set on the raw
  [u8;32] BLAKE3 digest and moves the hex on a duplicate (the ROUND17 §D2
  pattern applied to the delta-upload sibling). 401 → 209 allocs/op.
- R4: CardDAV getetag emits borrowed pre-escaped &quot; quotes via a shared
  write_quoted_etag helper (ROUND20 §C1 pattern, all 4 CardDAV etag sites).
  3 → 0 allocs/op.
- R5: BDAY stamped via the new fmt::compact_date stack renderer instead of
  chrono's strftime interpreter (chrono fallback out of the 4-digit-year
  range; byte-identical, unit-tested vs chrono). 2 → 0 allocs/op, 10.5x wall.
- R6: NC trashbin folder content-type via Cow::Borrowed instead of
  .to_string() on the constant (ROUND16 §M1 pattern). 1 → 0 allocs/op.

See benches/ROUND21.md for the full write-up and the deferred-items list
(HeaderMap-clone hot handlers, Query→typed-struct, WebDAV dead-props
HashSet, and others surfaced by the audit that want their own validated
pass). Validated: cargo fmt, cargo clippy --features bench --all-targets
-D warnings, cargo test --lib --features test_utils (529 passed).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015gHVq5Wy2TzdWeSqtEmK6m
This commit is contained in:
Claude
2026-07-20 08:48:42 +00:00
parent aec9b8f037
commit 77f13ac643
11 changed files with 910 additions and 55 deletions
+36 -14
View File
@@ -17,6 +17,21 @@ use crate::application::adapters::webdav_adapter::{
use crate::application::dtos::address_book_dto::AddressBookDto;
use crate::application::dtos::contact_dto::ContactDto;
/// Emit a WebDAV `getetag` value as `"…"` with the surrounding quotes written
/// as borrowed pre-escaped `&quot;` text events around the escaped etag body.
///
/// Byte-identical to escaping the whole `"{etag}"` String — `quick_xml` escapes
/// a literal `"` to `&quot;`, so the one-String form re-allocated an owned `Cow`
/// on write — but with **0 heap allocs per contact** (the NextCloud
/// `write_etag_element` pattern, benches/ROUND20.md §C1). Called per contact on
/// the CardDAV multiget/PROPFIND emit path.
fn write_quoted_etag<W: Write>(xml_writer: &mut Writer<W>, etag: &str) -> Result<()> {
xml_writer.write_event(Event::Text(BytesText::from_escaped("&quot;")))?;
xml_writer.write_event(Event::Text(BytesText::new(etag)))?;
xml_writer.write_event(Event::Text(BytesText::from_escaped("&quot;")))?;
Ok(())
}
/// Render a requested property as a namespaced response element name, mapping
/// the known namespaces to their response prefixes (`D:` for DAV, `CR:` for
/// CardDAV). Used for the catch-all arms of the requested-property writers so
@@ -386,7 +401,7 @@ impl CardDavAdapter {
// getetag
xml_writer.write_event(Event::Start(BytesStart::new("D:getetag")))?;
xml_writer.write_event(Event::Text(BytesText::new(&format!("\"{}\"", book.id))))?;
write_quoted_etag(xml_writer, &book.id)?;
xml_writer.write_event(Event::End(BytesEnd::new("D:getetag")))?;
// getcontenttype
@@ -468,8 +483,7 @@ impl CardDavAdapter {
}
("DAV:", "getetag") => {
xml_writer.write_event(Event::Start(BytesStart::new("D:getetag")))?;
xml_writer
.write_event(Event::Text(BytesText::new(&format!("\"{}\"", book.id))))?;
write_quoted_etag(xml_writer, &book.id)?;
xml_writer.write_event(Event::End(BytesEnd::new("D:getetag")))?;
}
("DAV:", "getcontenttype") => {
@@ -755,11 +769,7 @@ impl CardDavAdapter {
xml_writer.write_event(Event::Empty(BytesStart::new("D:resourcetype")))?;
xml_writer.write_event(Event::Start(BytesStart::new("D:getetag")))?;
let mut quoted = String::with_capacity(contact.etag.len() + 2);
quoted.push('"');
quoted.push_str(&contact.etag);
quoted.push('"');
xml_writer.write_event(Event::Text(BytesText::new(&quoted)))?;
write_quoted_etag(xml_writer, &contact.etag)?;
xml_writer.write_event(Event::End(BytesEnd::new("D:getetag")))?;
xml_writer.write_event(Event::Start(BytesStart::new("D:getcontenttype")))?;
@@ -779,11 +789,7 @@ impl CardDavAdapter {
}
("DAV:", "getetag") => {
xml_writer.write_event(Event::Start(BytesStart::new("D:getetag")))?;
let mut quoted = String::with_capacity(contact.etag.len() + 2);
quoted.push('"');
quoted.push_str(&contact.etag);
quoted.push('"');
xml_writer.write_event(Event::Text(BytesText::new(&quoted)))?;
write_quoted_etag(xml_writer, &contact.etag)?;
xml_writer.write_event(Event::End(BytesEnd::new("D:getetag")))?;
}
("DAV:", "getcontenttype") => {
@@ -1017,7 +1023,23 @@ pub fn contact_to_vcard(contact: &ContactDto) -> String {
}
}
if let Some(bday) = &contact.birthday {
let _ = write!(vcard, "BDAY:{}\r\n", bday.format("%Y-%m-%d"));
// Stack render (byte-identical to chrono's `%Y-%m-%d`) with the chrono
// fallback for out-of-range years — drops the strftime interpreter + a
// heap alloc per contact-with-birthday (fmt::compact_date is the
// date-only companion to the §V2 REV renderer above).
use chrono::Datelike as _;
let mut bday_buf = [0u8; 10];
match crate::common::fmt::compact_date(&mut bday_buf, bday.year(), bday.month(), bday.day())
{
Some(s) => {
vcard.push_str("BDAY:");
vcard.push_str(s);
vcard.push_str("\r\n");
}
None => {
let _ = write!(vcard, "BDAY:{}\r\n", bday.format("%Y-%m-%d"));
}
}
}
if let Some(photo) = &contact.photo_url {
let _ = write!(vcard, "PHOTO;VALUE=URI:{}\r\n", photo);
+53
View File
@@ -241,6 +241,31 @@ pub fn compact_ical_utc(buf: &mut [u8; 16], secs: i64) -> Option<&str> {
Some(std::str::from_utf8(&buf[..]).expect("ascii"))
}
/// `chrono::NaiveDate::format("%Y-%m-%d")` for a calendar date: the vCard
/// `BDAY` / ISO date form `2026-07-17` (10 bytes) written into `buf`.
///
/// The vCard emit path (`contact_to_vcard`) stamps `BDAY` per
/// contact-with-birthday, and `write!(…, "{}", date.format("%Y-%m-%d"))` runs
/// chrono's strftime interpreter and heap-allocates — the same interpreter cost
/// [`compact_ical_utc`] removed for the `REV` stamp (benches/ROUND19.md §V2:
/// 3→0 allocs). This is the date-only companion to that helper.
///
/// Takes the pre-split `year`/`month`/`day` (so `fmt` stays chrono-free off the
/// test path); callers read them via `chrono::Datelike`. Returns `None` when
/// `year` is outside the fixed-width 4-digit range — where chrono widens or
/// sign-prefixes `%Y` — so callers keep the chrono path as fallback.
pub fn compact_date(buf: &mut [u8; 10], year: i32, month: u32, day: u32) -> Option<&str> {
if !(0..=9999).contains(&year) {
return None;
}
push4(buf, 0, year as i64);
buf[4] = b'-';
push2(buf, 5, month);
buf[7] = b'-';
push2(buf, 8, day);
Some(std::str::from_utf8(&buf[..]).expect("ascii"))
}
/// Append the upper-cased form of `s` to `buf` without a temporary `String`.
///
/// Byte-identical to `buf.push_str(&s.to_uppercase())` — same
@@ -349,14 +374,42 @@ mod tests {
}
}
#[test]
fn compact_date_matches_chrono() {
use chrono::{Datelike, NaiveDate};
// Padding (day/month < 10), leap day, min/max in-range 4-digit year,
// 3-digit year (chrono zero-pads %Y to 4).
let cases = [
(2026, 7, 17),
(2000, 2, 29),
(2005, 7, 1),
(1970, 1, 1),
(9999, 12, 31),
(1, 1, 1),
(876, 5, 9),
];
for (y, m, d) in cases {
let date = NaiveDate::from_ymd_opt(y, m, d).unwrap();
let mut buf = [0u8; 10];
assert_eq!(
compact_date(&mut buf, date.year(), date.month(), date.day()).expect("in range"),
date.format("%Y-%m-%d").to_string(),
"date={y}-{m}-{d}"
);
}
}
#[test]
fn out_of_range_falls_back() {
let mut b3 = [0u8; 25];
let mut b2 = [0u8; 31];
let mut bc = [0u8; 16];
let mut bd = [0u8; 10];
assert!(rfc3339_utc(&mut b3, -1).is_none());
assert!(rfc2822_utc(&mut b2, -1).is_none());
assert!(compact_ical_utc(&mut bc, -1).is_none());
assert!(compact_date(&mut bd, -1, 1, 1).is_none());
assert!(compact_date(&mut bd, 10000, 1, 1).is_none());
assert!(rfc3339_utc(&mut b3, MAX_4DIGIT_YEAR_SECS + 1).is_none());
assert!(compact_ical_utc(&mut bc, MAX_4DIGIT_YEAR_SECS + 1).is_none());
}
@@ -182,7 +182,7 @@ impl CalendarEventRepository for CalendarEventPgRepository {
DomainError::database_error(format!("Failed to get events in time range: {}", e))
})?;
let mut events = Vec::new();
let mut events = Vec::with_capacity(rows.len());
for row in rows {
let mut event = CalendarEvent::with_id(
row.get("id"),
@@ -288,7 +288,7 @@ impl CalendarEventRepository for CalendarEventPgRepository {
DomainError::database_error(format!("Failed to get events by calendar: {}", e))
})?;
let mut events = Vec::new();
let mut events = Vec::with_capacity(rows.len());
for row in rows {
let mut event = CalendarEvent::with_id(
row.get("id"),
@@ -341,7 +341,7 @@ impl CalendarEventRepository for CalendarEventPgRepository {
DomainError::database_error(format!("Failed to find events by summary: {}", e))
})?;
let mut events = Vec::new();
let mut events = Vec::with_capacity(rows.len());
for row in rows {
let mut event = CalendarEvent::with_id(
row.get("id"),
@@ -515,7 +515,7 @@ impl CalendarEventRepository for CalendarEventPgRepository {
DomainError::database_error(format!("Failed to get calendar events by UIDs: {}", e))
})?;
let mut events = Vec::new();
let mut events = Vec::with_capacity(rows.len());
for row in rows {
let mut event = CalendarEvent::with_id(
row.get("id"),
@@ -664,7 +664,7 @@ impl CalendarEventRepository for CalendarEventPgRepository {
))
})?;
let mut events = Vec::new();
let mut events = Vec::with_capacity(rows.len());
for row in rows {
let event = CalendarEvent::with_id(
row.get("id"),
@@ -719,7 +719,7 @@ impl CalendarEventRepository for CalendarEventPgRepository {
DomainError::database_error(format!("Failed to find recurring events in range: {}", e))
})?;
let mut events = Vec::new();
let mut events = Vec::with_capacity(rows.len());
for row in rows {
let event = CalendarEvent::with_id(
row.get("id"),
@@ -211,7 +211,7 @@ impl CalendarRepository for CalendarPgRepository {
DomainError::database_error(format!("Failed to get calendars by owner: {}", e))
})?;
let mut calendars = Vec::new();
let mut calendars = Vec::with_capacity(rows.len());
for row in rows {
let calendar = Calendar::with_id(
row.get("id"),
@@ -292,7 +292,7 @@ impl CalendarRepository for CalendarPgRepository {
DomainError::database_error(format!("Failed to get public calendars: {}", e))
})?;
let mut calendars = Vec::new();
let mut calendars = Vec::with_capacity(rows.len());
for row in rows {
let calendar = Calendar::with_id(
row.get("id"),
@@ -400,7 +400,7 @@ impl CalendarRepository for CalendarPgRepository {
DomainError::database_error(format!("Failed to get calendar properties: {}", e))
})?;
let mut properties = std::collections::HashMap::new();
let mut properties = std::collections::HashMap::with_capacity(rows.len());
for row in rows {
properties.insert(row.get("name"), row.get("value"));
}
@@ -218,7 +218,7 @@ impl ContactGroupRepository for ContactGroupPgRepository {
)
})?;
let mut contacts = Vec::new();
let mut contacts = Vec::with_capacity(rows.len());
for row in &rows {
let email_json: JsonValue = row.get("email");
let phone_json: JsonValue = row.get("phone");
@@ -271,7 +271,7 @@ impl ContactRepository for ContactPgRepository {
DomainError::database_error(format!("Failed to get contacts by uids: {}", e))
})?;
let mut contacts = Vec::new();
let mut contacts = Vec::with_capacity(rows.len());
for row in &rows {
contacts.push(Self::row_to_contact(row)?);
}
@@ -339,7 +339,7 @@ impl ContactRepository for ContactPgRepository {
DomainError::database_error(format!("Failed to get contacts by address book: {}", e))
})?;
let mut contacts = Vec::new();
let mut contacts = Vec::with_capacity(rows.len());
for row in &rows {
contacts.push(Self::row_to_contact(row)?);
}
@@ -376,7 +376,7 @@ impl ContactRepository for ContactPgRepository {
))
})?;
let mut contacts = Vec::new();
let mut contacts = Vec::with_capacity(rows.len());
for row in &rows {
contacts.push(Self::row_to_contact(row)?);
}
@@ -404,7 +404,7 @@ impl ContactRepository for ContactPgRepository {
DomainError::database_error(format!("Failed to get contacts by email: {}", e))
})?;
let mut contacts = Vec::new();
let mut contacts = Vec::with_capacity(rows.len());
for row in &rows {
contacts.push(Self::row_to_contact(row)?);
}
@@ -434,7 +434,7 @@ impl ContactRepository for ContactPgRepository {
DomainError::database_error(format!("Failed to get contacts by group: {}", e))
})?;
let mut contacts = Vec::new();
let mut contacts = Vec::with_capacity(rows.len());
for row in &rows {
contacts.push(Self::row_to_contact(row)?);
}
@@ -474,7 +474,7 @@ impl ContactRepository for ContactPgRepository {
.await
.map_err(|e| DomainError::database_error(format!("Failed to search contacts: {}", e)))?;
let mut contacts = Vec::new();
let mut contacts = Vec::with_capacity(rows.len());
for row in &rows {
contacts.push(Self::row_to_contact(row)?);
}
+39 -21
View File
@@ -860,7 +860,12 @@ impl DedupService {
let mut received: Vec<(String, u64)> = Vec::new();
let mut new_rows: Vec<(String, i64)> = Vec::new();
let mut seen: HashSet<String> = HashSet::new();
// Intra-request dedup set keyed on the raw 32-byte BLAKE3 digest
// (`[u8; 32]`, `Copy` — no per-distinct-chunk 64-byte `String` heap
// key), mirroring the streaming ingest loop (benches/ROUND17.md §D2).
// hex ↔ digest is bijective, so membership is identical to the old
// `HashSet<String>`.
let mut seen: HashSet<[u8; 32]> = HashSet::new();
while let Some(frame) = frames.next().await {
let data = frame?;
@@ -870,14 +875,21 @@ impl DedupService {
data.len()
)));
}
let hash = blake3::hash(&data).to_hex().to_string();
received.push((hash.clone(), data.len() as u64));
if seen.insert(hash.clone()) {
let len = data.len() as i64;
let digest = blake3::hash(&data);
let hash = digest.to_hex().to_string();
let len = data.len();
if seen.insert(*digest.as_bytes()) {
self.backend
.put_blob_from_bytes_unsynced(&hash, data)
.await?;
new_rows.push((hash, len));
// First occurrence: `received` needs a copy, `new_rows` moves it.
received.push((hash.clone(), len as u64));
new_rows.push((hash, len as i64));
} else {
// Duplicate within this request — move the hex into `received`
// (no clone; the blob is already registered by its first
// occurrence). Same `received` sequence, input order preserved.
received.push((hash, len as u64));
}
}
@@ -1216,24 +1228,30 @@ impl DedupService {
return Ok(());
}
let mut guard = state.lock().await;
let hashes: Vec<String> = batch.iter().map(|(h, _)| h.clone()).collect();
// Pin-or-classify in one statement: rows that exist take this
// session's reference NOW; hashes not returned don't exist and are
// ours to write.
let pinned: HashSet<String> = sqlx::query_scalar::<_, String>(
"UPDATE storage.blobs SET ref_count = ref_count + 1, orphaned_at = NULL
WHERE hash = ANY($1)
RETURNING hash",
)
.bind(&hashes)
.fetch_all(pool.as_ref())
.await
.map_err(|e| {
DomainError::internal_error("Dedup", format!("Failed to pin existing chunks: {e}"))
})?
.into_iter()
.collect();
// ours to write. Bind borrowed `&str`s — sqlx encodes `&[&str]` to
// `text[]` identically to the owned Strings the old `.clone()` built,
// so no per-chunk hash String is allocated just to run the query
// (the pattern favorites_pg_repository.rs:271 already uses). The
// borrow is scoped so it ends before `batch` is moved below.
let pinned: HashSet<String> = {
let hashes: Vec<&str> = batch.iter().map(|(h, _)| h.as_str()).collect();
sqlx::query_scalar::<_, String>(
"UPDATE storage.blobs SET ref_count = ref_count + 1, orphaned_at = NULL
WHERE hash = ANY($1)
RETURNING hash",
)
.bind(&hashes)
.fetch_all(pool.as_ref())
.await
.map_err(|e| {
DomainError::internal_error("Dedup", format!("Failed to pin existing chunks: {e}"))
})?
.into_iter()
.collect()
};
let mut to_write: Vec<(String, Bytes)> = Vec::with_capacity(batch.len());
for (hash, data) in batch {
+6 -4
View File
@@ -465,11 +465,13 @@ fn write_trash_item_response<W: std::io::Write>(
.map_err(|e| e.to_string())?;
}
// d:getcontenttype
let content_type = if item.item_type == "folder" {
"httpd/unix-directory".to_string()
// d:getcontenttype — the folder constant is borrowed (`Cow::Borrowed`, 0
// allocs per trashed folder row); only the file branch (mime_guess) still
// allocates its owned String (ROUND16 §M1 `Cow<'static, str>` pattern).
let content_type: std::borrow::Cow<'static, str> = if item.item_type == "folder" {
std::borrow::Cow::Borrowed("httpd/unix-directory")
} else {
mime_from_name(&item.name)
std::borrow::Cow::Owned(mime_from_name(&item.name))
};
write_text_element(xml, "d:getcontenttype", &content_type)?;