perf: round 5 — CalDAV cursor streaming, SPA interning gaps, NC href prefix, per-request micro-allocs
Seven benchmark-gated changes (benches/ROUND5.md; BEFORE/AFTER bench + equivalence gate each, rollback rule as ROUND2-4 — two intermediate CalDAV shapes measured worse and were themselves rolled back before shipping): - CalDAV whole-calendar responses (REPORT no-range/sync-collection, depth-1 collection PROPFIND, .ics GET): buffered double-residency → ONE window-ordered scan (MIN(start_time) OVER (PARTITION BY ical_uid)) streamed through a PG cursor, pages cut at UID boundaries. TTFB 23.3→11.0 ms (2.1x), peak heap 14.2→8.0 MiB at 4k events / 45→24 MiB at 12k, wall +9-15% (documented trade, ZIP-streaming class); both multistatus and ICS byte-identical to the buffered output. Rejected shapes kept in the doc: per-page GROUP-BY keyset (3-4x wall) and per-uid ANY hydration (~20 µs/index descent). - SPA listing interning gaps: folder/recent/favorites resources handlers (and the WebDAV pseudo-root) called raw Arc::from per row for the closed display set ROUND3 interned — now intern_display/intern_mime, 4→0 allocs/row, byte-identical Arc contents. - NC PROPFIND child hrefs: username + parent path encoded once per request instead of per child (543→165 ns/row, 13→4 allocs); native WebDAV href drops its intermediate encode String. - suggest enrichment: entity clone + field re-clones per keystroke row → consume + move (166.5→126.8 µs/200 rows, 20→7 allocs/row). - list_readable_by returns the cache's Arc (246→128 ns warm hit, 4→0 allocs) — deep Vec clone per DAV-selector request removed. - CardDAV REPORT: borrowed props, reused href buffer, exact-size etag quoting (3.04→2.34 ms per 5k-contact getetag poll). - Auth span records: user_id.to_string() per request ×3 → tracing::field::display. Checks: cargo fmt, clippy --all-features --all-targets -D warnings, cargo test --workspace (523 passed). Follow-ups (CardDAV streaming, &[&str] id batches, ::text UUID casts A/B, share-landing join) recorded in benches/ROUND5.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017aJu9ghvuT8WqC31ZEGTBA
This commit is contained in:
@@ -0,0 +1,570 @@
|
||||
//! Round-5 micro-allocation pack — per-request/per-row churn removed
|
||||
//! from five hot paths. Each section is BEFORE (verbatim old shape) vs
|
||||
//! AFTER (the shipped code or its exact pattern), with byte/structure
|
||||
//! equality gates. No Postgres.
|
||||
//!
|
||||
//! [1] search suggest enrichment: entity clone + 3 field re-clones per
|
||||
//! row → consume + move.
|
||||
//! [2] `list_readable_by` warm hit: deep `Vec<DriveWithRootName>`
|
||||
//! clone per request → `Arc` refcount bump.
|
||||
//! [3] SPA listing rows (folder/recent/favorites handlers): raw
|
||||
//! `Arc::from` per closed-set display field → `intern_display` /
|
||||
//! `intern_mime` lookups.
|
||||
//! [4] NC PROPFIND child hrefs: per-row re-encode of username + parent
|
||||
//! path (`nc_href`) → prefix precomputed once + name-only encode.
|
||||
//! [5] CardDAV REPORT (getetag poll): per-REPORT props clone +
|
||||
//! per-contact href String + etag `format!` → borrowed props,
|
||||
//! reused href buffer, exact-size quoting.
|
||||
//!
|
||||
//! Run (no Postgres needed):
|
||||
//! cargo run --release --features bench --example bench_micro_allocs
|
||||
//! Tunables (env): BENCH_ROWS (5000), BENCH_PASSES (60).
|
||||
|
||||
use std::alloc::{GlobalAlloc, Layout, System};
|
||||
use std::env;
|
||||
use std::hint::black_box;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::time::Instant;
|
||||
|
||||
use chrono::{TimeZone, Utc};
|
||||
use oxicloud::application::adapters::carddav_adapter::{CardDavAdapter, CardDavReportType};
|
||||
use oxicloud::application::adapters::webdav_adapter::QualifiedName;
|
||||
use oxicloud::application::dtos::contact_dto::ContactDto;
|
||||
use oxicloud::application::dtos::display_helpers::{
|
||||
category_for, icon_class_for, icon_special_class_for, intern_display, intern_mime,
|
||||
};
|
||||
use oxicloud::application::dtos::file_dto::FileDto;
|
||||
use oxicloud::application::dtos::search_dto::SearchSuggestionItem;
|
||||
use oxicloud::domain::entities::drive::{Drive, DriveKind};
|
||||
use oxicloud::domain::entities::file::File;
|
||||
use oxicloud::domain::repositories::drive_repository::DriveWithRootName;
|
||||
use oxicloud::interfaces::nextcloud::webdav_handler::nc_href;
|
||||
use uuid::Uuid;
|
||||
|
||||
// ─── Counting allocator ─────────────────────────────────────────────────────
|
||||
|
||||
static ALLOC_CALLS: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
struct CountingAlloc;
|
||||
|
||||
unsafe impl GlobalAlloc for CountingAlloc {
|
||||
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
|
||||
ALLOC_CALLS.fetch_add(1, Ordering::Relaxed);
|
||||
unsafe { System.alloc(layout) }
|
||||
}
|
||||
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
|
||||
unsafe { System.dealloc(ptr, layout) }
|
||||
}
|
||||
unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
|
||||
ALLOC_CALLS.fetch_add(1, Ordering::Relaxed);
|
||||
unsafe { System.realloc(ptr, layout, new_size) }
|
||||
}
|
||||
unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
|
||||
ALLOC_CALLS.fetch_add(1, Ordering::Relaxed);
|
||||
unsafe { System.alloc_zeroed(layout) }
|
||||
}
|
||||
}
|
||||
|
||||
#[global_allocator]
|
||||
static GLOBAL: CountingAlloc = CountingAlloc;
|
||||
|
||||
fn p50(mut xs: Vec<f64>) -> f64 {
|
||||
xs.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
||||
xs[xs.len() / 2]
|
||||
}
|
||||
|
||||
fn time_passes<T>(passes: usize, mut f: impl FnMut() -> T) -> f64 {
|
||||
let mut per = Vec::with_capacity(passes);
|
||||
for _ in 0..passes {
|
||||
let t0 = Instant::now();
|
||||
black_box(f());
|
||||
per.push(t0.elapsed().as_secs_f64() * 1e6);
|
||||
}
|
||||
p50(per)
|
||||
}
|
||||
|
||||
fn allocs_of<T>(mut f: impl FnMut() -> T) -> u64 {
|
||||
let s0 = ALLOC_CALLS.load(Ordering::Relaxed);
|
||||
black_box(f());
|
||||
ALLOC_CALLS.load(Ordering::Relaxed) - s0
|
||||
}
|
||||
|
||||
// ─── Corpus builders ────────────────────────────────────────────────────────
|
||||
|
||||
fn make_files(n: usize) -> Vec<File> {
|
||||
(0..n)
|
||||
.map(|i| {
|
||||
File::from_materialized_row(
|
||||
Uuid::from_u128(i as u128).to_string(),
|
||||
format!("documento-{i}.pdf"),
|
||||
Some("/Personal/Proyectos/2026"),
|
||||
1024 + i as u64,
|
||||
"application/pdf".to_string(),
|
||||
None,
|
||||
1_700_000_000,
|
||||
1_750_000_000,
|
||||
format!("{:032x}", i),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.expect("file")
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn compute_relevance(name: &str, q: &str) -> u32 {
|
||||
if name.to_lowercase().contains(q) {
|
||||
100
|
||||
} else {
|
||||
50
|
||||
}
|
||||
}
|
||||
|
||||
/// The suggest enrichment loop — BEFORE: per-row entity clone + field
|
||||
/// re-clones (verbatim old shape, icon helper substituted identically
|
||||
/// on both arms).
|
||||
fn suggest_before(files: &[File], q: &str) -> Vec<SearchSuggestionItem> {
|
||||
let mut out = Vec::new();
|
||||
let query_lower = q.to_lowercase();
|
||||
for file in files {
|
||||
let file_dto = FileDto::from(file.clone());
|
||||
let score = compute_relevance(&file_dto.name, &query_lower);
|
||||
out.push(SearchSuggestionItem {
|
||||
name: file_dto.name.clone(),
|
||||
item_type: "file".to_string(),
|
||||
id: file_dto.id.clone(),
|
||||
path: file_dto.path.clone(),
|
||||
icon_class: icon_class_for(&file_dto.name, &file_dto.mime_type).to_string(),
|
||||
icon_special_class: icon_special_class_for(&file_dto.name, &file_dto.mime_type)
|
||||
.to_string(),
|
||||
relevance_score: score,
|
||||
});
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// AFTER: consume + move (the shipped shape).
|
||||
fn suggest_after(files: Vec<File>, q: &str) -> Vec<SearchSuggestionItem> {
|
||||
let mut out = Vec::new();
|
||||
let query_lower = q.to_lowercase();
|
||||
for file in files {
|
||||
let file_dto = FileDto::from(file);
|
||||
let score = compute_relevance(&file_dto.name, &query_lower);
|
||||
let icon_class = icon_class_for(&file_dto.name, &file_dto.mime_type).to_string();
|
||||
let icon_special_class =
|
||||
icon_special_class_for(&file_dto.name, &file_dto.mime_type).to_string();
|
||||
out.push(SearchSuggestionItem {
|
||||
name: file_dto.name,
|
||||
item_type: "file".to_string(),
|
||||
id: file_dto.id,
|
||||
path: file_dto.path,
|
||||
icon_class,
|
||||
icon_special_class,
|
||||
relevance_score: score,
|
||||
});
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn make_drives(n: usize) -> Vec<DriveWithRootName> {
|
||||
(0..n)
|
||||
.map(|i| DriveWithRootName {
|
||||
drive: Drive {
|
||||
id: Uuid::from_u128(i as u128),
|
||||
kind: if i == 0 {
|
||||
DriveKind::Personal
|
||||
} else {
|
||||
DriveKind::Shared
|
||||
},
|
||||
default_for_user: (i == 0).then(|| Uuid::from_u128(999)),
|
||||
root_folder_id: Uuid::from_u128(1000 + i as u128),
|
||||
quota_bytes: Some(10_737_418_240),
|
||||
used_bytes: 123_456_789,
|
||||
policies: serde_json::json!({}),
|
||||
created_at: Utc.with_ymd_and_hms(2026, 1, 1, 0, 0, 0).unwrap(),
|
||||
updated_at: Utc.with_ymd_and_hms(2026, 7, 1, 0, 0, 0).unwrap(),
|
||||
},
|
||||
root_folder_name: format!("Drive número {i}"),
|
||||
caller_role: None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn make_contacts(n: usize) -> Vec<ContactDto> {
|
||||
(0..n)
|
||||
.map(|i| ContactDto {
|
||||
id: Uuid::from_u128(i as u128).to_string(),
|
||||
uid: format!("contact-{i:05}"),
|
||||
etag: format!("{:016x}", i * 2_654_435_761u64 as usize),
|
||||
full_name: Some(format!("Persona {i}")),
|
||||
..ContactDto::default()
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
// BEFORE replica of the CardDAV REPORT emitter (props.clone + per-row
|
||||
// href String + etag format!) for the getetag poll shape — the
|
||||
// address-data branch is never hit with this prop set, so the replica
|
||||
// stays self-contained.
|
||||
mod before_carddav {
|
||||
use super::*;
|
||||
use quick_xml::Writer;
|
||||
use quick_xml::events::{BytesEnd, BytesStart, BytesText, Event};
|
||||
|
||||
pub fn generate_contacts_response(
|
||||
out: &mut Vec<u8>,
|
||||
contacts: &[ContactDto],
|
||||
report: &CardDavReportType,
|
||||
base_href: &str,
|
||||
) {
|
||||
let mut xml_writer = Writer::new(out);
|
||||
xml_writer
|
||||
.write_event(Event::Start(
|
||||
BytesStart::new("D:multistatus").with_attributes([
|
||||
("xmlns:D", "DAV:"),
|
||||
("xmlns:CR", "urn:ietf:params:xml:ns:carddav"),
|
||||
]),
|
||||
))
|
||||
.unwrap();
|
||||
|
||||
let props = match report {
|
||||
CardDavReportType::AddressbookQuery { props } => props.clone(),
|
||||
CardDavReportType::AddressbookMultiget { props, .. } => props.clone(),
|
||||
CardDavReportType::SyncCollection { props, .. } => props.clone(),
|
||||
};
|
||||
|
||||
for contact in contacts {
|
||||
let href = format!("{}{}.vcf", base_href, contact.uid);
|
||||
xml_writer
|
||||
.write_event(Event::Start(BytesStart::new("D:response")))
|
||||
.unwrap();
|
||||
xml_writer
|
||||
.write_event(Event::Start(BytesStart::new("D:href")))
|
||||
.unwrap();
|
||||
xml_writer
|
||||
.write_event(Event::Text(BytesText::new(&href)))
|
||||
.unwrap();
|
||||
xml_writer
|
||||
.write_event(Event::End(BytesEnd::new("D:href")))
|
||||
.unwrap();
|
||||
xml_writer
|
||||
.write_event(Event::Start(BytesStart::new("D:propstat")))
|
||||
.unwrap();
|
||||
xml_writer
|
||||
.write_event(Event::Start(BytesStart::new("D:prop")))
|
||||
.unwrap();
|
||||
for prop in &props {
|
||||
match (prop.namespace.as_str(), prop.name.as_str()) {
|
||||
("DAV:", "resourcetype") => {
|
||||
xml_writer
|
||||
.write_event(Event::Empty(BytesStart::new("D:resourcetype")))
|
||||
.unwrap();
|
||||
}
|
||||
("DAV:", "getetag") => {
|
||||
xml_writer
|
||||
.write_event(Event::Start(BytesStart::new("D:getetag")))
|
||||
.unwrap();
|
||||
xml_writer
|
||||
.write_event(Event::Text(BytesText::new(&format!(
|
||||
"\"{}\"",
|
||||
contact.etag
|
||||
))))
|
||||
.unwrap();
|
||||
xml_writer
|
||||
.write_event(Event::End(BytesEnd::new("D:getetag")))
|
||||
.unwrap();
|
||||
}
|
||||
("DAV:", "getcontenttype") => {
|
||||
xml_writer
|
||||
.write_event(Event::Start(BytesStart::new("D:getcontenttype")))
|
||||
.unwrap();
|
||||
xml_writer
|
||||
.write_event(Event::Text(BytesText::new("text/vcard; charset=utf-8")))
|
||||
.unwrap();
|
||||
xml_writer
|
||||
.write_event(Event::End(BytesEnd::new("D:getcontenttype")))
|
||||
.unwrap();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
xml_writer
|
||||
.write_event(Event::End(BytesEnd::new("D:prop")))
|
||||
.unwrap();
|
||||
xml_writer
|
||||
.write_event(Event::Start(BytesStart::new("D:status")))
|
||||
.unwrap();
|
||||
xml_writer
|
||||
.write_event(Event::Text(BytesText::new("HTTP/1.1 200 OK")))
|
||||
.unwrap();
|
||||
xml_writer
|
||||
.write_event(Event::End(BytesEnd::new("D:status")))
|
||||
.unwrap();
|
||||
xml_writer
|
||||
.write_event(Event::End(BytesEnd::new("D:propstat")))
|
||||
.unwrap();
|
||||
xml_writer
|
||||
.write_event(Event::End(BytesEnd::new("D:response")))
|
||||
.unwrap();
|
||||
}
|
||||
xml_writer
|
||||
.write_event(Event::End(BytesEnd::new("D:multistatus")))
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let rows: usize = env::var("BENCH_ROWS")
|
||||
.ok()
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(5000);
|
||||
let passes: usize = env::var("BENCH_PASSES")
|
||||
.ok()
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(60);
|
||||
let mut ok = true;
|
||||
|
||||
println!("bench_micro_allocs — {rows} rows, {passes} passes\n");
|
||||
|
||||
// ── [1] suggest enrichment ──────────────────────────────────────────────
|
||||
{
|
||||
let files = make_files(200); // suggest is limit-bounded (~10-200)
|
||||
let t_b = time_passes(passes, || suggest_before(&files, "doc"));
|
||||
// Production AFTER consumes the caller's Vec — no clone exists.
|
||||
// The replay clone happens OUTSIDE the timed window.
|
||||
let t_a = {
|
||||
let mut per = Vec::with_capacity(passes);
|
||||
for _ in 0..passes {
|
||||
let corpus = files.clone();
|
||||
let t0 = Instant::now();
|
||||
black_box(suggest_after(corpus, "doc"));
|
||||
per.push(t0.elapsed().as_secs_f64() * 1e6);
|
||||
}
|
||||
p50(per)
|
||||
};
|
||||
// Alloc parity: charge the corpus clone to neither arm by
|
||||
// measuring BEFORE with its borrow (clones inside) and AFTER
|
||||
// seeded from a pre-cloned Vec outside the counter window.
|
||||
let a_b = allocs_of(|| suggest_before(&files, "doc")) as f64 / files.len() as f64;
|
||||
let mut pre = Some(files.clone());
|
||||
let a_a =
|
||||
allocs_of(|| suggest_after(pre.take().unwrap(), "doc")) as f64 / files.len() as f64;
|
||||
let g_b = suggest_before(&files, "doc");
|
||||
let g_a = suggest_after(files.clone(), "doc");
|
||||
let same = g_b.len() == g_a.len()
|
||||
&& g_b.iter().zip(&g_a).all(|(x, y)| {
|
||||
x.name == y.name && x.id == y.id && x.path == y.path && x.icon_class == y.icon_class
|
||||
});
|
||||
if !same {
|
||||
eprintln!("GATE FAIL suggest");
|
||||
ok = false;
|
||||
}
|
||||
println!("[1] suggest enrichment (200 rows) µs/pass allocs/row");
|
||||
println!(" BEFORE (clone per row) {t_b:8.1} {a_b:7.2}");
|
||||
println!(
|
||||
" AFTER (consume + move) {t_a:8.1} {a_a:7.2} {:.2}x",
|
||||
t_b / t_a
|
||||
);
|
||||
}
|
||||
|
||||
// ── [2] readable-drives warm hit ────────────────────────────────────────
|
||||
{
|
||||
let value = Arc::new(make_drives(3));
|
||||
let cache: moka::sync::Cache<Uuid, Arc<Vec<DriveWithRootName>>> =
|
||||
moka::sync::Cache::new(100);
|
||||
let user = Uuid::from_u128(42);
|
||||
cache.insert(user, value);
|
||||
let hit_before = || {
|
||||
let arc = cache.get(&user).expect("warm");
|
||||
let v: Vec<DriveWithRootName> = (*arc).clone(); // old: deep clone out
|
||||
v
|
||||
};
|
||||
let hit_after = || cache.get(&user).expect("warm"); // new: Arc bump
|
||||
let n_iters = 10_000u32;
|
||||
let t_b = time_passes(passes, || {
|
||||
for _ in 0..n_iters {
|
||||
black_box(hit_before());
|
||||
}
|
||||
}) / n_iters as f64
|
||||
* 1000.0;
|
||||
let t_a = time_passes(passes, || {
|
||||
for _ in 0..n_iters {
|
||||
black_box(hit_after());
|
||||
}
|
||||
}) / n_iters as f64
|
||||
* 1000.0;
|
||||
let a_b = allocs_of(hit_before);
|
||||
let a_a = allocs_of(hit_after);
|
||||
let g = hit_before();
|
||||
let ga = hit_after();
|
||||
if g.len() != ga.len() || g[0].root_folder_name != ga[0].root_folder_name {
|
||||
eprintln!("GATE FAIL readable hit");
|
||||
ok = false;
|
||||
}
|
||||
println!("[2] list_readable_by warm hit (3 drives) ns/hit allocs/hit");
|
||||
println!(" BEFORE (deep Vec clone) {t_b:8.1} {a_b:7}");
|
||||
println!(
|
||||
" AFTER (Arc refcount bump) {t_a:8.1} {a_a:7} {:.1}x",
|
||||
t_b / t_a
|
||||
);
|
||||
}
|
||||
|
||||
// ── [3] SPA listing closed-set fields ───────────────────────────────────
|
||||
{
|
||||
let names: Vec<String> = (0..rows).map(|i| format!("informe-{i}.pdf")).collect();
|
||||
let mime = "application/pdf";
|
||||
let row_before = |name: &str| {
|
||||
(
|
||||
Arc::<str>::from(mime),
|
||||
Arc::<str>::from(icon_class_for(name, mime)),
|
||||
Arc::<str>::from(icon_special_class_for(name, mime)),
|
||||
Arc::<str>::from(category_for(name, mime)),
|
||||
)
|
||||
};
|
||||
let row_after = |name: &str| {
|
||||
(
|
||||
intern_mime(mime),
|
||||
intern_display(icon_class_for(name, mime)),
|
||||
intern_display(icon_special_class_for(name, mime)),
|
||||
intern_display(category_for(name, mime)),
|
||||
)
|
||||
};
|
||||
let t_b = time_passes(passes, || {
|
||||
for n in &names {
|
||||
black_box(row_before(n));
|
||||
}
|
||||
}) / rows as f64
|
||||
* 1000.0;
|
||||
let t_a = time_passes(passes, || {
|
||||
for n in &names {
|
||||
black_box(row_after(n));
|
||||
}
|
||||
}) / rows as f64
|
||||
* 1000.0;
|
||||
let a_b = allocs_of(|| row_before(&names[0]));
|
||||
let a_a = allocs_of(|| row_after(&names[0]));
|
||||
let (bm, bi, bs, bc) = row_before(&names[0]);
|
||||
let (am, ai, as_, ac) = row_after(&names[0]);
|
||||
if *bm != *am || *bi != *ai || *bs != *as_ || *bc != *ac {
|
||||
eprintln!("GATE FAIL interning content");
|
||||
ok = false;
|
||||
}
|
||||
println!("[3] listing closed-set fields ns/row allocs/row");
|
||||
println!(" BEFORE (Arc::from ×4) {t_b:8.1} {a_b:7}");
|
||||
println!(
|
||||
" AFTER (intern lookups ×4) {t_a:8.1} {a_a:7} {:.1}x",
|
||||
t_b / t_a
|
||||
);
|
||||
}
|
||||
|
||||
// ── [4] NC PROPFIND child hrefs ─────────────────────────────────────────
|
||||
{
|
||||
let username = "ana.garcia";
|
||||
let subpath = "Personal/Proyectos 2026/Diseño";
|
||||
let names: Vec<String> = (0..rows)
|
||||
.map(|i| format!("archivo con espacios {i}.png"))
|
||||
.collect();
|
||||
// Verbatim replica of the production shape — `subpath` is a
|
||||
// const here, so the emptiness test is statically known.
|
||||
#[allow(clippy::const_is_empty)]
|
||||
let href_before = |name: &str| {
|
||||
let child_sub = if subpath.is_empty() {
|
||||
name.to_string()
|
||||
} else {
|
||||
format!("{}/{}", subpath.trim_end_matches('/'), name)
|
||||
};
|
||||
nc_href(username, &child_sub)
|
||||
};
|
||||
let prefix = {
|
||||
let base = nc_href(username, subpath);
|
||||
if base.ends_with('/') {
|
||||
base
|
||||
} else {
|
||||
format!("{base}/")
|
||||
}
|
||||
};
|
||||
let href_after = |name: &str| format!("{}{}", prefix, urlencoding::encode(name));
|
||||
let t_b = time_passes(passes, || {
|
||||
for n in &names {
|
||||
black_box(href_before(n));
|
||||
}
|
||||
}) / rows as f64
|
||||
* 1000.0;
|
||||
let t_a = time_passes(passes, || {
|
||||
for n in &names {
|
||||
black_box(href_after(n));
|
||||
}
|
||||
}) / rows as f64
|
||||
* 1000.0;
|
||||
let a_b = allocs_of(|| href_before(&names[0]));
|
||||
let a_a = allocs_of(|| href_after(&names[0]));
|
||||
for n in names.iter().take(50) {
|
||||
if href_before(n) != href_after(n) {
|
||||
eprintln!("GATE FAIL href: {} != {}", href_before(n), href_after(n));
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
println!("[4] NC child hrefs (depth-3 parent) ns/row allocs/row");
|
||||
println!(" BEFORE (nc_href per row) {t_b:8.1} {a_b:7}");
|
||||
println!(
|
||||
" AFTER (prefix + name encode) {t_a:8.1} {a_a:7} {:.1}x",
|
||||
t_b / t_a
|
||||
);
|
||||
}
|
||||
|
||||
// ── [5] CardDAV REPORT getetag poll ─────────────────────────────────────
|
||||
{
|
||||
let contacts = make_contacts(rows);
|
||||
let report = CardDavReportType::AddressbookQuery {
|
||||
props: vec![
|
||||
QualifiedName::new("DAV:", "getetag"),
|
||||
QualifiedName::new("DAV:", "getcontenttype"),
|
||||
],
|
||||
};
|
||||
let base = "/carddav/libreta/";
|
||||
let run_before = || {
|
||||
let mut out = Vec::with_capacity(contacts.len() * 256);
|
||||
before_carddav::generate_contacts_response(&mut out, &contacts, &report, base);
|
||||
out
|
||||
};
|
||||
let run_after = || {
|
||||
let mut out = Vec::with_capacity(contacts.len() * 256);
|
||||
CardDavAdapter::generate_contacts_response(&mut out, &contacts, &report, base)
|
||||
.expect("generate");
|
||||
out
|
||||
};
|
||||
let t_b = time_passes(passes.min(30), run_before);
|
||||
let t_a = time_passes(passes.min(30), run_after);
|
||||
let xb = run_before();
|
||||
let xa = run_after();
|
||||
if xb != xa {
|
||||
let at = xb.iter().zip(&xa).position(|(a, b)| a != b).unwrap_or(0);
|
||||
eprintln!(
|
||||
"GATE FAIL carddav at byte {at}: …{}… vs …{}…",
|
||||
String::from_utf8_lossy(&xb[at.saturating_sub(60)..(at + 60).min(xb.len())]),
|
||||
String::from_utf8_lossy(&xa[at.saturating_sub(60)..(at + 60).min(xa.len())]),
|
||||
);
|
||||
ok = false;
|
||||
}
|
||||
println!("[5] CardDAV REPORT getetag ({rows} contacts) µs/report");
|
||||
println!(" BEFORE (clone + format! churn) {t_b:8.1}");
|
||||
println!(
|
||||
" AFTER (borrow + reuse + exact-size) {t_a:8.1} {:.2}x",
|
||||
t_b / t_a
|
||||
);
|
||||
}
|
||||
|
||||
println!(
|
||||
"\n[gate] {}",
|
||||
if ok {
|
||||
"OK (identical outputs)"
|
||||
} else {
|
||||
"FAILED"
|
||||
}
|
||||
);
|
||||
if !ok {
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user