perf: round 22 — hot-GET HeaderMap borrow, native-WebDAV/CalDAV etag borrowed quotes, FileDto content_hash move, CalendarEvent stamp, ShareItemType case-fold
Benchmark-gated, same rule as ROUND2-21: every change ships with a
BEFORE/AFTER counting-allocator benchmark and a byte/-value equivalence
gate; an AFTER that fails to reduce allocations exits non-zero (rollback).
See benches/ROUND22.md and examples/bench_round22_micro.rs. All arms
no-Postgres.
- H1: the hot GET handlers (get_thumbnail, download_file, list_files_query,
list_photos, NextCloud preview, public-share download/access) take
`req: Request` last and read `req.headers()` by borrow instead of axum's
HeaderMap extractor, whose FromRequestParts impl clones the whole request
header table just to read 1-3 headers (the ROUND14 §A4 middleware pattern,
finally propagated to the handlers). 2 -> 0 allocs/req · 9.95x wall.
- W1: native WebDAV write_etag_quoted — the etag emitter for every /webdav/
PROPFIND row (per file AND per folder, up to 500/page) — emits the quotes
as borrowed pre-escaped " text events instead of escaping a "{etag}"
String (the ROUND20 §C1 / ROUND21 §R4 pattern). 3 -> 0 allocs/row.
- C1: CalDAV getetag routed through a shared write_quoted_etag helper across
all 5 sites (3 per-event + 2 per-calendar); the now-dead etag: &mut String
buffer threaded through write_event_response/standard/requested props + the
two per-page buffers removed. 2 -> 0 allocs/row.
- D1: FileDto::from reuses the moved parts.blob_hash instead of cloning it
via the content_hash() getter (the ROUND19/20 move-not-clone sweep missed
it — hash/etag are read before into_parts()). Per file row of every
listing. 1 -> 0 allocs/row.
- E1: CalendarEvent::update_time_range/update_all_day stamp timed
DTSTART/DTEND via fmt::compact_ical_utc stack render (chrono fallback out
of range) instead of the %Y%m%dT%H%M%SZ strftime interpreter. 4 -> 0.
- S1: ShareItemType::try_from uses eq_ignore_ascii_case instead of a
throwaway to_lowercase() String. 1 -> 0 allocs/parse.
Verified: cargo clippy --features bench --all-targets -D warnings clean,
cargo fmt --all --check clean, cargo test --lib --features bench = 529
passed / 0 failed (incl. the OpenAPI-spec-validity test guarding the H1
utoipa-handler signature change).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DKyQ4AnYtgp1JtjzweyMeo
This commit is contained in:
@@ -17,6 +17,23 @@ use crate::application::adapters::webdav_adapter::{
|
||||
};
|
||||
use crate::application::dtos::calendar_dto::{CalendarDto, CalendarEventDto};
|
||||
|
||||
/// Emit a WebDAV `getetag` body as `"…"` with the surrounding quotes written as
|
||||
/// borrowed pre-escaped `"` text events around the escaped etag body.
|
||||
///
|
||||
/// Byte-identical to escaping a `"{etag}"` String — `quick_xml`'s
|
||||
/// `BytesText::new` escapes a literal `"` → `"`, re-allocating an owned
|
||||
/// `Cow` — but with 0 heap allocs (the NextCloud ROUND20 §C1 / CardDAV
|
||||
/// ROUND21 §R4 pattern, applied to the CalDAV emitter it missed). The caller
|
||||
/// writes the surrounding `<D:getetag>…</D:getetag>` tags. Every `etag` body
|
||||
/// here is a bare `Uuid` (`calendar.id` / `anchor.id`), so the escaped body is
|
||||
/// itself a borrow — 0 allocs/row.
|
||||
fn write_quoted_etag<W: Write>(xml_writer: &mut Writer<W>, etag: &str) -> Result<()> {
|
||||
xml_writer.write_event(Event::Text(BytesText::from_escaped(""")))?;
|
||||
xml_writer.write_event(Event::Text(BytesText::new(etag)))?;
|
||||
xml_writer.write_event(Event::Text(BytesText::from_escaped(""")))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Parse a CalDAV `time-range` element's `start` / `end` attribute
|
||||
/// value into a UTC `DateTime`.
|
||||
///
|
||||
@@ -794,9 +811,9 @@ impl CalDavAdapter {
|
||||
Self::write_lastmodified_text(xml_writer, calendar.updated_at)?;
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("D:getlastmodified")))?;
|
||||
|
||||
// ETag
|
||||
// ETag (borrowed pre-escaped quotes, §C1/§R4 — was format! + escape)
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("D:getetag")))?;
|
||||
xml_writer.write_event(Event::Text(BytesText::new(&format!("\"{}\"", calendar.id))))?;
|
||||
write_quoted_etag(xml_writer, &calendar.id)?;
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("D:getetag")))?;
|
||||
|
||||
// Content type for calendar collection
|
||||
@@ -924,10 +941,7 @@ impl CalDavAdapter {
|
||||
}
|
||||
("DAV:", "getetag") => {
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("D:getetag")))?;
|
||||
xml_writer.write_event(Event::Text(BytesText::new(&format!(
|
||||
"\"{}\"",
|
||||
calendar.id
|
||||
))))?;
|
||||
write_quoted_etag(xml_writer, &calendar.id)?;
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("D:getetag")))?;
|
||||
}
|
||||
("DAV:", "getcontenttype") => {
|
||||
@@ -1117,11 +1131,11 @@ impl CalDavAdapter {
|
||||
events: &[CalendarEventDto],
|
||||
base_href: &str,
|
||||
) -> Result<()> {
|
||||
// Reused per-event buffers (cleared each iteration) so a whole PROPFIND
|
||||
// page allocates the href/etag storage once instead of twice per event
|
||||
// (benches/ROUND14.md §A6).
|
||||
// Reused per-event href buffer (cleared each iteration) so a whole
|
||||
// PROPFIND page allocates the href storage once instead of per event
|
||||
// (benches/ROUND14.md §A6). The etag no longer needs a buffer — it is
|
||||
// emitted via `write_quoted_etag` (borrowed pre-escaped quotes).
|
||||
let mut event_href = String::with_capacity(base_href.len() + 48);
|
||||
let mut etag = String::new();
|
||||
for bundle in group_events_by_uid(events) {
|
||||
// The master (sorted first by group_events_by_uid)
|
||||
// supplies the ETag anchor + getlastmodified. If
|
||||
@@ -1148,11 +1162,9 @@ impl CalDavAdapter {
|
||||
// resourcetype (empty for non-collection)
|
||||
xml_writer.write_event(Event::Empty(BytesStart::new("D:resourcetype")))?;
|
||||
|
||||
// getetag — anchor row's id (reused buffer, benches/ROUND14.md §A6)
|
||||
// getetag — anchor row's id (borrowed pre-escaped quotes, §C1/§R4)
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("D:getetag")))?;
|
||||
etag.clear();
|
||||
let _ = std::fmt::Write::write_fmt(&mut etag, format_args!("\"{}\"", anchor.id));
|
||||
xml_writer.write_event(Event::Text(BytesText::new(&etag)))?;
|
||||
write_quoted_etag(xml_writer, &anchor.id)?;
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("D:getetag")))?;
|
||||
|
||||
// getcontenttype
|
||||
@@ -1215,10 +1227,10 @@ impl CalDavAdapter {
|
||||
CalDavReportType::CalendarMultiget { props, .. } => props,
|
||||
CalDavReportType::SyncCollection { props, .. } => props,
|
||||
};
|
||||
// Reused per-event href + etag buffers for the whole REPORT page
|
||||
// (benches/ROUND14.md §A6).
|
||||
// Reused per-event href buffer for the whole REPORT page
|
||||
// (benches/ROUND14.md §A6). The etag is emitted via `write_quoted_etag`
|
||||
// (borrowed pre-escaped quotes) and no longer needs a buffer.
|
||||
let mut href = String::with_capacity(base_href.len() + 48);
|
||||
let mut etag = String::new();
|
||||
for bundle in group_events_by_uid(events) {
|
||||
let anchor = match bundle.first() {
|
||||
Some(e) => *e,
|
||||
@@ -1229,7 +1241,7 @@ impl CalDavAdapter {
|
||||
&mut href,
|
||||
format_args!("{}{}.ics", base_href, anchor.ical_uid),
|
||||
);
|
||||
Self::write_event_response(xml_writer, &bundle, props, &href, &mut etag)?;
|
||||
Self::write_event_response(xml_writer, &bundle, props, &href)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -1266,7 +1278,6 @@ impl CalDavAdapter {
|
||||
bundle: &[&CalendarEventDto],
|
||||
props: &[QualifiedName],
|
||||
href: &str,
|
||||
etag: &mut String,
|
||||
) -> Result<()> {
|
||||
let anchor = bundle
|
||||
.first()
|
||||
@@ -1289,10 +1300,10 @@ impl CalDavAdapter {
|
||||
|
||||
// If no specific props requested, return all common ones
|
||||
if props.is_empty() {
|
||||
Self::write_event_standard_props(xml_writer, anchor, bundle, etag)?;
|
||||
Self::write_event_standard_props(xml_writer, anchor, bundle)?;
|
||||
} else {
|
||||
// Write specifically requested properties
|
||||
Self::write_event_requested_props(xml_writer, anchor, bundle, props, etag)?;
|
||||
Self::write_event_requested_props(xml_writer, anchor, bundle, props)?;
|
||||
}
|
||||
|
||||
// End prop
|
||||
@@ -1320,22 +1331,17 @@ impl CalDavAdapter {
|
||||
xml_writer: &mut Writer<W>,
|
||||
anchor: &CalendarEventDto,
|
||||
bundle: &[&CalendarEventDto],
|
||||
etag: &mut String,
|
||||
) -> Result<()> {
|
||||
// Common WebDAV properties
|
||||
|
||||
// Resource type (empty for non-collection)
|
||||
xml_writer.write_event(Event::Empty(BytesStart::new("D:resourcetype")))?;
|
||||
|
||||
// ETag anchored on the master (or first exception in
|
||||
// a master-less bundle — pathological state today).
|
||||
// Reused buffer (benches/ROUND14.md §A6).
|
||||
// ETag anchored on the master (or first exception in a master-less
|
||||
// bundle — pathological state today). Borrowed pre-escaped quotes
|
||||
// (§C1/§R4), 0 allocs/event.
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("D:getetag")))?;
|
||||
etag.clear();
|
||||
etag.push('"');
|
||||
etag.push_str(&anchor.id);
|
||||
etag.push('"');
|
||||
xml_writer.write_event(Event::Text(BytesText::new(etag.as_str())))?;
|
||||
write_quoted_etag(xml_writer, &anchor.id)?;
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("D:getetag")))?;
|
||||
|
||||
// Content type
|
||||
@@ -1370,7 +1376,6 @@ impl CalDavAdapter {
|
||||
anchor: &CalendarEventDto,
|
||||
bundle: &[&CalendarEventDto],
|
||||
props: &[QualifiedName],
|
||||
etag: &mut String,
|
||||
) -> Result<()> {
|
||||
for prop in props {
|
||||
match (prop.namespace.as_str(), prop.name.as_str()) {
|
||||
@@ -1380,12 +1385,7 @@ impl CalDavAdapter {
|
||||
}
|
||||
("DAV:", "getetag") => {
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("D:getetag")))?;
|
||||
// Reused buffer (benches/ROUND14.md §A6).
|
||||
etag.clear();
|
||||
etag.push('"');
|
||||
etag.push_str(&anchor.id);
|
||||
etag.push('"');
|
||||
xml_writer.write_event(Event::Text(BytesText::new(etag.as_str())))?;
|
||||
write_quoted_etag(xml_writer, &anchor.id)?;
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("D:getetag")))?;
|
||||
}
|
||||
("DAV:", "getcontenttype") => {
|
||||
|
||||
@@ -809,12 +809,17 @@ impl WebDavAdapter {
|
||||
|
||||
fn write_etag_quoted<W: Write>(xml_writer: &mut Writer<W>, etag: &str) -> Result<()> {
|
||||
xml_writer.write_event(Event::Start(BytesStart::new("D:getetag")))?;
|
||||
// One exactly-sized allocation instead of format!'s grow-from-empty.
|
||||
let mut quoted = String::with_capacity(etag.len() + 2);
|
||||
quoted.push('"');
|
||||
quoted.push_str(etag);
|
||||
quoted.push('"');
|
||||
xml_writer.write_event(Event::Text(BytesText::new("ed)))?;
|
||||
// Borrowed pre-escaped quotes (the ROUND20 §C1 NextCloud / ROUND21 §R4
|
||||
// CardDAV pattern the native WebDAV adapter never got): `BytesText::new`
|
||||
// escapes a literal `"` → `"`, re-allocating an owned `Cow`, so the
|
||||
// old `"{etag}"` String paid TWO allocs/row (the sized buffer + the
|
||||
// escape). Emit the two quotes as borrowed pre-escaped `"` text
|
||||
// events around the escaped body — byte-identical output, 0 allocs/row
|
||||
// on the hottest native-WebDAV PROPFIND path (per file AND per folder,
|
||||
// up to PROPFIND_BATCH_SIZE=500 rows/page).
|
||||
xml_writer.write_event(Event::Text(BytesText::from_escaped(""")))?;
|
||||
xml_writer.write_event(Event::Text(BytesText::new(etag)))?;
|
||||
xml_writer.write_event(Event::Text(BytesText::from_escaped(""")))?;
|
||||
xml_writer.write_event(Event::End(BytesEnd::new("D:getetag")))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -93,10 +93,15 @@ impl From<File> for FileDto {
|
||||
// already-extracted parts. `content_hash` is just the raw
|
||||
// blob hash; `etag` is the cache token derived from it.
|
||||
let etag = file.etag();
|
||||
let content_hash = file.content_hash().to_string();
|
||||
|
||||
// Consume the entity by moving all fields — zero heap allocations
|
||||
// for id, name, path, folder_id (previously 4× .to_string()).
|
||||
// for id, name, path, folder_id (previously 4× .to_string()), and now
|
||||
// for `content_hash` too: `into_parts()` moves `blob_hash` out, so it is
|
||||
// reused verbatim below instead of cloning it through the
|
||||
// `content_hash()` getter. The moved `parts.blob_hash` was previously
|
||||
// dropped unused while the getter clone paid 1 alloc/row on every file
|
||||
// listing (folder browse, streaming PROPFIND, search/favorites/recent
|
||||
// hydration). `etag` is still computed first from the live entity.
|
||||
let parts = file.into_parts();
|
||||
|
||||
// Display fields come from closed static tables and MIME values
|
||||
@@ -123,7 +128,7 @@ impl From<File> for FileDto {
|
||||
category,
|
||||
size_formatted,
|
||||
sort_date: None,
|
||||
content_hash,
|
||||
content_hash: parts.blob_hash,
|
||||
etag,
|
||||
created_by: parts.created_by,
|
||||
updated_by: parts.updated_by,
|
||||
|
||||
Reference in New Issue
Block a user