perf: round 20 — iCal/vCard parse allocs, owned-DTO moves, Result-collect pre-size, NC etag/favorites emit
Benchmark-gated (benches/ROUND20.md), same rule as rounds 2-19: every change ships with a BEFORE/AFTER counting-allocator micro-benchmark and a byte-value equivalence gate; a non-winning AFTER is rolled back (never applied). The rollback rule is encoded in the harness (GATE FAIL exit). All 8 sections pass. Reproduce: cargo run --release --features bench --example bench_round20_micro - A1 CalendarEvent iCal parse: replace the throwaway per-property HashMap<String,Vec<String>> (DTSTART/DTEND/RECURRENCE-ID) with a direct VALUE=DATE scan; prop_with_params kept #[cfg(test)] (6->2 allocs/event, 4.2x) - A2 UserDto::from: add User::into_parts and MOVE image (<=512 KiB data URI) + ui_preferences JSON instead of cloning on every /api/auth/me (27->14 allocs) - A3 parse_vcard: drop the per-line to_ascii_uppercase copy + the lines Vec; promote ascii_ci_contains to common::text and share it (8->1 allocs/contact) - A4 Calendar/AddressBook DTO: into_parts move incl. custom_properties map (18->10) - I1 file-listing repos: collect::<Result<Vec>>() size-hints to 0 and grows from capacity 0; pre-size with Vec::with_capacity (8->1 container reallocs, 4 sites) - I4 plaintext_stream: lazy emit iterator instead of eager Vec collect (43x wall) - C1 NC write_etag_element: borrowed pre-escaped quote events, no owned quoted String/escape re-alloc; byte-identical output (3->0 allocs/PROPFIND row) - C3 NC favorites REPORT: map.remove() move instead of get().clone() (~7 allocs/fav) Deferred (documented in ROUND20.md): NC oc:id/trashbin buffer reuse, I1 sibling CardDAV/CalDAV listing paths, Contact JSONB Json<Vec<_>> decode, dedup settle_batch &str bind, and a fast DoS-resistant hasher for hot trusted-key maps (needs a dependency decision). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JsJjcVX9RoN96DMa35Wqzd
This commit is contained in:
@@ -114,14 +114,14 @@ async fn handle_filter_files(
|
||||
}
|
||||
}
|
||||
|
||||
let file_map: HashMap<String, FileDto> = file_service
|
||||
let mut file_map: HashMap<String, FileDto> = file_service
|
||||
.get_files_by_ids(&file_ids)
|
||||
.await
|
||||
.map_err(|e| AppError::internal_error(format!("Failed to resolve favorite files: {e}")))?
|
||||
.into_iter()
|
||||
.map(|f| (f.id.clone(), f))
|
||||
.collect();
|
||||
let folder_map: HashMap<String, FolderDto> = folder_service
|
||||
let mut folder_map: HashMap<String, FolderDto> = folder_service
|
||||
.get_folders_by_ids(&folder_ids)
|
||||
.await
|
||||
.map_err(|e| AppError::internal_error(format!("Failed to resolve favorite folders: {e}")))?
|
||||
@@ -131,16 +131,21 @@ async fn handle_filter_files(
|
||||
|
||||
let mut files: Vec<FileDto> = Vec::new();
|
||||
let mut folders: Vec<FolderDto> = Vec::new();
|
||||
// Move the DTO out of the map instead of cloning it: the maps are built
|
||||
// just above solely to hydrate `files`/`folders` in favorites order and are
|
||||
// dropped at fn end, so the clone was pure waste. `favorites.item_id` is
|
||||
// unique per user, so `remove` drops nothing needed and the favorites order
|
||||
// is preserved (benches/ROUND20.md §C3).
|
||||
for fav in &favorites {
|
||||
match fav.item_type.as_str() {
|
||||
"file" => {
|
||||
if let Some(f) = file_map.get(&fav.item_id) {
|
||||
files.push(f.clone());
|
||||
if let Some(f) = file_map.remove(&fav.item_id) {
|
||||
files.push(f);
|
||||
}
|
||||
}
|
||||
"folder" => {
|
||||
if let Some(f) = folder_map.get(&fav.item_id) {
|
||||
folders.push(f.clone());
|
||||
if let Some(f) = folder_map.remove(&fav.item_id) {
|
||||
folders.push(f);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
|
||||
@@ -1990,18 +1990,30 @@ pub fn write_date_element<W: std::io::Write>(
|
||||
}
|
||||
}
|
||||
|
||||
/// `d:getetag` with the HTTP quoting — one exactly-sized allocation
|
||||
/// instead of `format!`'s grow-from-empty.
|
||||
/// `d:getetag` with the HTTP quoting — zero allocations.
|
||||
///
|
||||
/// The two `"` quotes are emitted as borrowed pre-escaped text events around
|
||||
/// the escaped etag body. `quick_xml` renders a literal `"` as `"`, so
|
||||
/// this is byte-identical to escaping `"{etag}"` as one owned string — but with
|
||||
/// no `with_capacity` quoted String and no escape re-allocation (the whole-string
|
||||
/// escape re-allocated an owned Cow because the string contained `"`). On a
|
||||
/// 500-child PROPFIND page this is called per file AND per folder row
|
||||
/// (benches/ROUND20.md §C1: 3 → 0 allocs/row).
|
||||
pub fn write_etag_element<W: std::io::Write>(
|
||||
xml: &mut Writer<W>,
|
||||
tag: &str,
|
||||
etag: &str,
|
||||
) -> Result<(), String> {
|
||||
let mut quoted = String::with_capacity(etag.len() + 2);
|
||||
quoted.push('"');
|
||||
quoted.push_str(etag);
|
||||
quoted.push('"');
|
||||
write_text_element(xml, tag, "ed)
|
||||
xml.write_event(Event::Start(BytesStart::new(tag)))
|
||||
.xml_err()?;
|
||||
xml.write_event(Event::Text(BytesText::from_escaped(""")))
|
||||
.xml_err()?;
|
||||
xml.write_event(Event::Text(BytesText::new(etag)))
|
||||
.xml_err()?;
|
||||
xml.write_event(Event::Text(BytesText::from_escaped(""")))
|
||||
.xml_err()?;
|
||||
xml.write_event(Event::End(BytesEnd::new(tag))).xml_err()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn write_text_element<W: std::io::Write>(
|
||||
|
||||
Reference in New Issue
Block a user