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:
@@ -293,16 +293,20 @@ impl FileBlobReadRepository {
|
||||
DomainError::internal_error("FileBlobRead", format!("hydrate by ids: {e}"))
|
||||
})?;
|
||||
|
||||
rows.into_iter()
|
||||
.map(
|
||||
|(id, name, fid, fpath, size, mime, ca, ma, blob_hash, cb, ub)| {
|
||||
Self::row_to_file(id, name, fid, fpath, size, mime, ca, ma, blob_hash, cb, ub)
|
||||
},
|
||||
)
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|e| {
|
||||
DomainError::internal_error("FileBlobRead", format!("hydrate mapping: {e}"))
|
||||
})
|
||||
// Pre-size the result Vec. `collect::<Result<Vec<_>, _>>()` size-hints
|
||||
// to 0 (the Result shunt may short-circuit on any element), so the Vec
|
||||
// grows from capacity 0 — ~⌈log₂N⌉ reallocations, memcpy-ing the
|
||||
// accumulated File rows each grow (benches/ROUND20.md §I1).
|
||||
let mut files = Vec::with_capacity(rows.len());
|
||||
for (id, name, fid, fpath, size, mime, ca, ma, blob_hash, cb, ub) in rows {
|
||||
files.push(
|
||||
Self::row_to_file(id, name, fid, fpath, size, mime, ca, ma, blob_hash, cb, ub)
|
||||
.map_err(|e| {
|
||||
DomainError::internal_error("FileBlobRead", format!("hydrate mapping: {e}"))
|
||||
})?,
|
||||
);
|
||||
}
|
||||
Ok(files)
|
||||
}
|
||||
|
||||
/// Batch-fetch files by id — the by-ids counterpart of [`get_file`],
|
||||
@@ -337,19 +341,21 @@ impl FileBlobReadRepository {
|
||||
DomainError::internal_error("FileBlobRead", format!("get_files_by_ids: {e}"))
|
||||
})?;
|
||||
|
||||
rows.into_iter()
|
||||
.map(
|
||||
|(id, name, fid, fpath, size, mime, ca, ma, blob_hash, cb, ub)| {
|
||||
Self::row_to_file(id, name, fid, fpath, size, mime, ca, ma, blob_hash, cb, ub)
|
||||
},
|
||||
)
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|e| {
|
||||
DomainError::internal_error(
|
||||
"FileBlobRead",
|
||||
format!("get_files_by_ids mapping: {e}"),
|
||||
)
|
||||
})
|
||||
// Pre-size the result Vec (see the size-hint note in `hydrate`,
|
||||
// benches/ROUND20.md §I1).
|
||||
let mut files = Vec::with_capacity(rows.len());
|
||||
for (id, name, fid, fpath, size, mime, ca, ma, blob_hash, cb, ub) in rows {
|
||||
files.push(
|
||||
Self::row_to_file(id, name, fid, fpath, size, mime, ca, ma, blob_hash, cb, ub)
|
||||
.map_err(|e| {
|
||||
DomainError::internal_error(
|
||||
"FileBlobRead",
|
||||
format!("get_files_by_ids mapping: {e}"),
|
||||
)
|
||||
})?,
|
||||
);
|
||||
}
|
||||
Ok(files)
|
||||
}
|
||||
|
||||
/// Returns `drive_id` for a given file. Drives the permission-floor
|
||||
@@ -1254,15 +1260,16 @@ impl FileReadPort for FileBlobReadRepository {
|
||||
// total_count is the same in every row; 0 when result set is empty.
|
||||
let total_count = rows.first().map_or(0, |r| r.11) as usize;
|
||||
|
||||
let files = rows
|
||||
.into_iter()
|
||||
.map(
|
||||
|(id, name, fid, fpath, size, mime, ca, ma, blob_hash, cb, ub, _total)| {
|
||||
Self::row_to_file(id, name, fid, fpath, size, mime, ca, ma, blob_hash, cb, ub)
|
||||
},
|
||||
)
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|e| DomainError::internal_error("FileBlobRead", format!("mapping: {e}")))?;
|
||||
// Pre-size the result Vec (size-hint note in `hydrate`, ROUND20 §I1).
|
||||
let mut files = Vec::with_capacity(rows.len());
|
||||
for (id, name, fid, fpath, size, mime, ca, ma, blob_hash, cb, ub, _total) in rows {
|
||||
files.push(
|
||||
Self::row_to_file(id, name, fid, fpath, size, mime, ca, ma, blob_hash, cb, ub)
|
||||
.map_err(|e| {
|
||||
DomainError::internal_error("FileBlobRead", format!("mapping: {e}"))
|
||||
})?,
|
||||
);
|
||||
}
|
||||
|
||||
Ok((files, total_count))
|
||||
}
|
||||
@@ -1384,17 +1391,16 @@ impl FileReadPort for FileBlobReadRepository {
|
||||
|
||||
let total_count = rows.first().map_or(0, |r| r.11) as usize;
|
||||
|
||||
let files = rows
|
||||
.into_iter()
|
||||
.map(
|
||||
|(id, name, fid, fpath, size, mime, ca, ma, blob_hash, cb, ub, _total)| {
|
||||
Self::row_to_file(id, name, fid, fpath, size, mime, ca, ma, blob_hash, cb, ub)
|
||||
},
|
||||
)
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|e| {
|
||||
DomainError::internal_error("FileBlobRead", format!("subtree mapping: {e}"))
|
||||
})?;
|
||||
// Pre-size the result Vec (size-hint note in `hydrate`, ROUND20 §I1).
|
||||
let mut files = Vec::with_capacity(rows.len());
|
||||
for (id, name, fid, fpath, size, mime, ca, ma, blob_hash, cb, ub, _total) in rows {
|
||||
files.push(
|
||||
Self::row_to_file(id, name, fid, fpath, size, mime, ca, ma, blob_hash, cb, ub)
|
||||
.map_err(|e| {
|
||||
DomainError::internal_error("FileBlobRead", format!("subtree mapping: {e}"))
|
||||
})?,
|
||||
);
|
||||
}
|
||||
|
||||
Ok((files, total_count))
|
||||
}
|
||||
|
||||
@@ -140,13 +140,18 @@ where
|
||||
}
|
||||
|
||||
/// Turn a decrypted payload into a stream of bounded, zero-copy slices.
|
||||
///
|
||||
/// The emit-slice iterator is handed to `stream::iter` lazily — the closure
|
||||
/// owns `data` (a refcounted `Bytes`), so each `slice` is produced on demand
|
||||
/// as the consumer polls, rather than eagerly `collect`ing a `Vec` of
|
||||
/// ⌈len/64 KiB⌉ slice handles up front (benches/ROUND20.md §I4).
|
||||
fn plaintext_stream(data: Bytes) -> BlobStream {
|
||||
let len = data.len();
|
||||
let slices: Vec<Result<Bytes, std::io::Error>> = (0..len)
|
||||
.step_by(PLAINTEXT_EMIT_SIZE)
|
||||
.map(|off| Ok(data.slice(off..len.min(off + PLAINTEXT_EMIT_SIZE))))
|
||||
.collect();
|
||||
Box::pin(futures::stream::iter(slices))
|
||||
Box::pin(futures::stream::iter(
|
||||
(0..len)
|
||||
.step_by(PLAINTEXT_EMIT_SIZE)
|
||||
.map(move |off| Ok(data.slice(off..len.min(off + PLAINTEXT_EMIT_SIZE)))),
|
||||
))
|
||||
}
|
||||
|
||||
impl BlobStorageBackend for EncryptedBlobBackend {
|
||||
|
||||
Reference in New Issue
Block a user