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,534 @@
|
||||
//! CalDAV whole-calendar response benchmark — buffered vs streamed (ROUND5).
|
||||
//!
|
||||
//! The REPORT path (no-range calendar-query, sync-collection) and the
|
||||
//! collection `.ics` GET used to (a) materialise EVERY event DTO of the
|
||||
//! calendar in one Vec (owned `ical_data` per row), then (b) render the
|
||||
//! complete multistatus / VCALENDAR into a second in-RAM buffer — the
|
||||
//! calendar resident twice, TTFB = full generation. AFTER streams ONE
|
||||
//! window-ordered scan (`MIN(start_time) OVER (PARTITION BY ical_uid)`)
|
||||
//! through a PG cursor and cuts pages at UID boundaries — same-UID rows
|
||||
//! never split, bundle order equals the buffered first-appearance
|
||||
//! order, and only a page of rows is resident. (A first keyset-paged
|
||||
//! shape re-aggregated per page — 3-4x wall — and a per-uid ANY
|
||||
//! hydration paid ~20 µs per index descent — both measured and
|
||||
//! discarded; see ROUND5.md.)
|
||||
//!
|
||||
//! This bench drives the REAL repository methods + adapter writers both
|
||||
//! ways at the repo layer (authz gates are identical constants on both
|
||||
//! sides and excluded). BEFORE uses the surviving buffered generator
|
||||
//! (byte-stable refactor of the old monolith) + a verbatim copy of the
|
||||
//! removed `generate_full_calendar_ical`. Gates: streamed concatenation
|
||||
//! byte-identical to the buffered output for BOTH the multistatus and
|
||||
//! the ICS body (seeded with strictly distinct start times so ordering
|
||||
//! is deterministic).
|
||||
//!
|
||||
//! Run (needs Postgres up; reads DATABASE_URL from .env):
|
||||
//! cargo run --release --features bench --example bench_caldav_stream
|
||||
//! Tunables (env): BENCH_EVENTS (4000), BENCH_PAGE (500), BENCH_PASSES (9).
|
||||
|
||||
use std::alloc::{GlobalAlloc, Layout, System};
|
||||
use std::env;
|
||||
use std::fmt::Write as _;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use chrono::{DateTime, TimeZone, Utc};
|
||||
use oxicloud::application::adapters::caldav_adapter::{
|
||||
CalDavAdapter, CalDavReportType, bench as caldav_bench,
|
||||
};
|
||||
use oxicloud::application::dtos::calendar_dto::CalendarEventDto;
|
||||
use oxicloud::domain::repositories::calendar_event_repository::CalendarEventRepository;
|
||||
use oxicloud::infrastructure::repositories::pg::CalendarEventPgRepository;
|
||||
use sqlx::PgPool;
|
||||
use sqlx::postgres::PgPoolOptions;
|
||||
use uuid::Uuid;
|
||||
|
||||
// ─── Peak-live-heap tracking allocator ──────────────────────────────────────
|
||||
|
||||
static LIVE: AtomicU64 = AtomicU64::new(0);
|
||||
static PEAK: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
struct PeakAlloc;
|
||||
|
||||
fn bump(sz: u64) {
|
||||
let live = LIVE.fetch_add(sz, Ordering::Relaxed) + sz;
|
||||
PEAK.fetch_max(live, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
unsafe impl GlobalAlloc for PeakAlloc {
|
||||
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
|
||||
bump(layout.size() as u64);
|
||||
unsafe { System.alloc(layout) }
|
||||
}
|
||||
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
|
||||
LIVE.fetch_sub(layout.size() as u64, Ordering::Relaxed);
|
||||
unsafe { System.dealloc(ptr, layout) }
|
||||
}
|
||||
unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
|
||||
if new_size > layout.size() {
|
||||
bump((new_size - layout.size()) as u64);
|
||||
} else {
|
||||
LIVE.fetch_sub((layout.size() - new_size) as u64, Ordering::Relaxed);
|
||||
}
|
||||
unsafe { System.realloc(ptr, layout, new_size) }
|
||||
}
|
||||
unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
|
||||
bump(layout.size() as u64);
|
||||
unsafe { System.alloc_zeroed(layout) }
|
||||
}
|
||||
}
|
||||
|
||||
#[global_allocator]
|
||||
static GLOBAL: PeakAlloc = PeakAlloc;
|
||||
|
||||
// ─── BEFORE: verbatim copy of the removed whole-calendar ICS builder ────────
|
||||
|
||||
#[allow(clippy::all)]
|
||||
mod before {
|
||||
use super::*;
|
||||
|
||||
/// Verbatim copy of the removed `generate_full_calendar_ical`.
|
||||
pub fn generate_full_calendar_ical(calendar_name: &str, events: &[CalendarEventDto]) -> String {
|
||||
let mut buf = String::with_capacity(256 + events.len() * 320);
|
||||
let _ = write!(
|
||||
buf,
|
||||
"BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//OxiCloud//NONSGML Calendar//EN\r\nX-WR-CALNAME:{}\r\n",
|
||||
calendar_name
|
||||
);
|
||||
for group in caldav_bench::group_events_by_uid(events) {
|
||||
for event in group {
|
||||
if let Some(chunk) = caldav_bench::extract_vevent_chunk(&event.ical_data) {
|
||||
buf.push_str(chunk);
|
||||
if !buf.ends_with('\n') {
|
||||
buf.push_str("\r\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
buf.push_str("END:VCALENDAR\r\n");
|
||||
buf
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Seed ───────────────────────────────────────────────────────────────────
|
||||
|
||||
fn vevent_body(uid: &str, start: DateTime<Utc>, exception: bool) -> String {
|
||||
let dt = start.format("%Y%m%dT%H%M%SZ");
|
||||
let dtend = (start + chrono::Duration::minutes(45)).format("%Y%m%dT%H%M%SZ");
|
||||
let mut v = String::with_capacity(640);
|
||||
v.push_str("BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//OxiCloud//NONSGML Calendar//EN\r\n");
|
||||
v.push_str("BEGIN:VEVENT\r\n");
|
||||
let _ = write!(v, "UID:{uid}\r\nDTSTAMP:20260701T120000Z\r\n");
|
||||
let _ = write!(v, "DTSTART:{dt}\r\nDTEND:{dtend}\r\n");
|
||||
if exception {
|
||||
let _ = write!(v, "RECURRENCE-ID:{dt}\r\n");
|
||||
} else {
|
||||
v.push_str("RRULE:FREQ=WEEKLY;BYDAY=WE\r\n");
|
||||
}
|
||||
let _ = write!(v, "SUMMARY:Reunión {uid}\r\n");
|
||||
v.push_str("LOCATION:Sala 3\r\nSTATUS:CONFIRMED\r\n");
|
||||
v.push_str("BEGIN:VALARM\r\nACTION:DISPLAY\r\nTRIGGER:-PT10M\r\nEND:VALARM\r\n");
|
||||
v.push_str("END:VEVENT\r\nEND:VCALENDAR\r\n");
|
||||
v
|
||||
}
|
||||
|
||||
struct Seeded {
|
||||
calendar_id: Uuid,
|
||||
owner_id: Uuid,
|
||||
}
|
||||
|
||||
async fn seed(pool: &PgPool, n: usize) -> Seeded {
|
||||
let owner_id: Uuid = sqlx::query_scalar(
|
||||
"INSERT INTO auth.users (username, email, role)
|
||||
VALUES ('bench_calstream', 'bench_calstream@bench.invalid', 'user') RETURNING id",
|
||||
)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.expect("seed user");
|
||||
let calendar_id: Uuid = sqlx::query_scalar(
|
||||
"INSERT INTO caldav.calendars (id, name, owner_id)
|
||||
VALUES (gen_random_uuid(), 'Agenda grande', $1) RETURNING id",
|
||||
)
|
||||
.bind(owner_id)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.expect("seed calendar");
|
||||
|
||||
let base = Utc.with_ymd_and_hms(2026, 1, 5, 8, 0, 0).unwrap();
|
||||
let mut tx = pool.begin().await.expect("begin");
|
||||
for i in 0..n {
|
||||
// 20% of rows are exception overrides sharing the previous
|
||||
// master's UID; every start_time is strictly distinct so the
|
||||
// response ordering is deterministic (byte-identity gate).
|
||||
let exception = i % 5 == 4;
|
||||
let master = if exception { i - 1 } else { i };
|
||||
let uid = format!("evt-{master:06}@oxicloud.bench");
|
||||
let start = base + chrono::Duration::seconds((i as i64) * 137);
|
||||
let recurrence: Option<DateTime<Utc>> = exception.then_some(start);
|
||||
sqlx::query(
|
||||
"INSERT INTO caldav.calendar_events
|
||||
(id, calendar_id, summary, start_time, end_time, all_day,
|
||||
rrule, ical_uid, ical_data, recurrence_id)
|
||||
VALUES (gen_random_uuid(), $1, $2, $3, $4, false, $5, $6, $7, $8)",
|
||||
)
|
||||
.bind(calendar_id)
|
||||
.bind(format!("Reunión {i}"))
|
||||
.bind(start)
|
||||
.bind(start + chrono::Duration::minutes(45))
|
||||
.bind((!exception).then_some("FREQ=WEEKLY;BYDAY=WE"))
|
||||
.bind(&uid)
|
||||
.bind(vevent_body(&uid, start, exception))
|
||||
.bind(recurrence)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.expect("seed event");
|
||||
}
|
||||
tx.commit().await.expect("commit");
|
||||
Seeded {
|
||||
calendar_id,
|
||||
owner_id,
|
||||
}
|
||||
}
|
||||
|
||||
async fn cleanup(pool: &PgPool, s: &Seeded) {
|
||||
let _ = sqlx::query("DELETE FROM caldav.calendar_events WHERE calendar_id = $1")
|
||||
.bind(s.calendar_id)
|
||||
.execute(pool)
|
||||
.await;
|
||||
let _ = sqlx::query("DELETE FROM caldav.calendars WHERE id = $1")
|
||||
.bind(s.calendar_id)
|
||||
.execute(pool)
|
||||
.await;
|
||||
let _ = sqlx::query("DELETE FROM auth.users WHERE id = $1")
|
||||
.bind(s.owner_id)
|
||||
.execute(pool)
|
||||
.await;
|
||||
}
|
||||
|
||||
// ─── Pipelines ──────────────────────────────────────────────────────────────
|
||||
|
||||
fn report_shape() -> CalDavReportType {
|
||||
CalDavReportType::CalendarQuery {
|
||||
props: vec![],
|
||||
time_range: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// BEFORE: the buffered pipeline — full entity fetch → full DTO Vec →
|
||||
/// one whole-response buffer. Returns (ttfb_ms, wall_ms, bytes).
|
||||
async fn buffered_report(
|
||||
repo: &CalendarEventPgRepository,
|
||||
calendar_id: &Uuid,
|
||||
base_href: &str,
|
||||
) -> (f64, f64, Vec<u8>) {
|
||||
let t0 = Instant::now();
|
||||
let events: Vec<CalendarEventDto> = repo
|
||||
.list_events_by_calendar(calendar_id)
|
||||
.await
|
||||
.expect("list events")
|
||||
.into_iter()
|
||||
.map(CalendarEventDto::from)
|
||||
.collect();
|
||||
let mut out = Vec::with_capacity(events.len() * 1024);
|
||||
CalDavAdapter::generate_calendar_events_response(&mut out, &events, &report_shape(), base_href)
|
||||
.expect("generate");
|
||||
let wall = t0.elapsed().as_secs_f64() * 1e3;
|
||||
// Buffered: the first byte is only available when everything is.
|
||||
(wall, wall, out)
|
||||
}
|
||||
|
||||
/// AFTER: the streaming pipeline — uid-keyset pages, per-page hydration,
|
||||
/// header/page/footer chunks (the handler's loop over the same public
|
||||
/// pieces). Returns (ttfb_ms, wall_ms, concatenated bytes).
|
||||
async fn streamed_report(
|
||||
repo: &CalendarEventPgRepository,
|
||||
calendar_id: &Uuid,
|
||||
base_href: &str,
|
||||
page_uids: usize,
|
||||
) -> (f64, f64, Vec<u8>) {
|
||||
let t0 = Instant::now();
|
||||
let mut ttfb = None;
|
||||
let mut all = Vec::new();
|
||||
let report = report_shape();
|
||||
|
||||
let mut chunk = Vec::with_capacity(256);
|
||||
{
|
||||
let mut w = quick_xml::Writer::new(&mut chunk);
|
||||
CalDavAdapter::write_caldav_multistatus_start(&mut w).expect("start");
|
||||
}
|
||||
all.extend_from_slice(&chunk);
|
||||
|
||||
{
|
||||
use futures::TryStreamExt;
|
||||
let mut rows = repo.stream_events_uid_order(*calendar_id);
|
||||
let mut page: Vec<CalendarEventDto> = Vec::with_capacity(page_uids + 32);
|
||||
loop {
|
||||
let next = rows
|
||||
.try_next()
|
||||
.await
|
||||
.expect("stream row")
|
||||
.map(CalendarEventDto::from);
|
||||
let flush = match &next {
|
||||
Some(ev) => {
|
||||
page.len() >= page_uids
|
||||
&& page.last().is_some_and(|p| p.ical_uid != ev.ical_uid)
|
||||
}
|
||||
None => !page.is_empty(),
|
||||
};
|
||||
if flush {
|
||||
let mut chunk = Vec::with_capacity(page.len() * 1024 + 128);
|
||||
{
|
||||
let mut w = quick_xml::Writer::new(&mut chunk);
|
||||
CalDavAdapter::write_report_page(&mut w, &page, &report, base_href)
|
||||
.expect("page");
|
||||
}
|
||||
if ttfb.is_none() && !all.is_empty() {
|
||||
// header already emitted; first data page complete
|
||||
}
|
||||
page.clear();
|
||||
all.extend_from_slice(&chunk);
|
||||
ttfb.get_or_insert_with(|| t0.elapsed().as_secs_f64() * 1e3);
|
||||
}
|
||||
match next {
|
||||
Some(ev) => page.push(ev),
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut chunk = Vec::with_capacity(32);
|
||||
{
|
||||
let mut w = quick_xml::Writer::new(&mut chunk);
|
||||
CalDavAdapter::write_caldav_multistatus_end(&mut w).expect("end");
|
||||
}
|
||||
all.extend_from_slice(&chunk);
|
||||
(
|
||||
ttfb.unwrap_or(f64::NAN),
|
||||
t0.elapsed().as_secs_f64() * 1e3,
|
||||
all,
|
||||
)
|
||||
}
|
||||
|
||||
/// TTFB for the streaming path measured honestly: time until the FIRST
|
||||
/// PAGE chunk (header + one hydrated page) exists — the moment real
|
||||
/// bytes could hit the socket.
|
||||
async fn streamed_report_ttfb(
|
||||
repo: &CalendarEventPgRepository,
|
||||
calendar_id: &Uuid,
|
||||
base_href: &str,
|
||||
page_uids: usize,
|
||||
) -> f64 {
|
||||
use futures::TryStreamExt;
|
||||
let t0 = Instant::now();
|
||||
let mut rows = repo.stream_events_uid_order(*calendar_id);
|
||||
let mut page: Vec<CalendarEventDto> = Vec::with_capacity(page_uids + 32);
|
||||
while let Some(ev) = rows.try_next().await.expect("stream row") {
|
||||
let ev = CalendarEventDto::from(ev);
|
||||
if page.len() >= page_uids && page.last().is_some_and(|p| p.ical_uid != ev.ical_uid) {
|
||||
break;
|
||||
}
|
||||
page.push(ev);
|
||||
}
|
||||
let mut chunk = Vec::with_capacity(page.len() * 1024 + 256);
|
||||
{
|
||||
let mut w = quick_xml::Writer::new(&mut chunk);
|
||||
CalDavAdapter::write_caldav_multistatus_start(&mut w).expect("start");
|
||||
CalDavAdapter::write_report_page(&mut w, &page, &report_shape(), base_href).expect("page");
|
||||
}
|
||||
std::hint::black_box(&chunk);
|
||||
t0.elapsed().as_secs_f64() * 1e3
|
||||
}
|
||||
|
||||
fn p50(mut xs: Vec<f64>) -> f64 {
|
||||
xs.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
||||
xs[xs.len() / 2]
|
||||
}
|
||||
|
||||
fn reset_peak() {
|
||||
PEAK.store(LIVE.load(Ordering::Relaxed), Ordering::Relaxed);
|
||||
}
|
||||
|
||||
fn peak_mib() -> f64 {
|
||||
PEAK.load(Ordering::Relaxed) as f64 / (1024.0 * 1024.0)
|
||||
}
|
||||
|
||||
#[tokio::main(flavor = "multi_thread")]
|
||||
async fn main() {
|
||||
dotenvy::dotenv().ok();
|
||||
let url = env::var("DATABASE_URL")
|
||||
.or_else(|_| env::var("OXICLOUD_DB_CONNECTION_STRING"))
|
||||
.expect("set DATABASE_URL — the dev Postgres URL");
|
||||
let n: usize = env::var("BENCH_EVENTS")
|
||||
.ok()
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(4000);
|
||||
let page_uids: usize = env::var("BENCH_PAGE")
|
||||
.ok()
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(500);
|
||||
let passes: usize = env::var("BENCH_PASSES")
|
||||
.ok()
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(9);
|
||||
|
||||
let pool = Arc::new(
|
||||
PgPoolOptions::new()
|
||||
.max_connections(10)
|
||||
.min_connections(10)
|
||||
.acquire_timeout(Duration::from_secs(10))
|
||||
.connect(&url)
|
||||
.await
|
||||
.expect("connect Postgres"),
|
||||
);
|
||||
|
||||
let seeded = seed(&pool, n).await;
|
||||
let repo = CalendarEventPgRepository::new(pool.clone());
|
||||
let base_href = format!("/caldav/{}/", seeded.calendar_id);
|
||||
|
||||
println!(
|
||||
"bench_caldav_stream — {n} events (20% exceptions), page={page_uids} uids, {passes} passes\n"
|
||||
);
|
||||
|
||||
// ── [1] REPORT (multistatus) ────────────────────────────────────────────
|
||||
// Warm-up + equivalence gate first.
|
||||
let (_, _, before_bytes) = buffered_report(&repo, &seeded.calendar_id, &base_href).await;
|
||||
let (_, _, after_bytes) =
|
||||
streamed_report(&repo, &seeded.calendar_id, &base_href, page_uids).await;
|
||||
let gate_report = before_bytes == after_bytes;
|
||||
|
||||
let mut b_wall = Vec::new();
|
||||
let mut a_wall = Vec::new();
|
||||
let mut a_ttfb = Vec::new();
|
||||
for _ in 0..passes {
|
||||
let (_, w, out) = buffered_report(&repo, &seeded.calendar_id, &base_href).await;
|
||||
std::hint::black_box(out);
|
||||
b_wall.push(w);
|
||||
let (_, w, out) = streamed_report(&repo, &seeded.calendar_id, &base_href, page_uids).await;
|
||||
std::hint::black_box(out);
|
||||
a_wall.push(w);
|
||||
a_ttfb.push(streamed_report_ttfb(&repo, &seeded.calendar_id, &base_href, page_uids).await);
|
||||
}
|
||||
// Peak-heap arms, measured in isolation.
|
||||
reset_peak();
|
||||
let (_, _, out) = buffered_report(&repo, &seeded.calendar_id, &base_href).await;
|
||||
drop(out);
|
||||
let peak_before = peak_mib();
|
||||
reset_peak();
|
||||
// Streamed peak: emulate the socket by dropping each chunk — reuse
|
||||
// the pipeline but without accumulating (accumulation would charge
|
||||
// the response size to the streaming arm).
|
||||
{
|
||||
use futures::TryStreamExt;
|
||||
let t0 = Instant::now();
|
||||
let report = report_shape();
|
||||
let mut rows = repo.stream_events_uid_order(seeded.calendar_id);
|
||||
let mut page: Vec<CalendarEventDto> = Vec::with_capacity(page_uids + 32);
|
||||
loop {
|
||||
let next = rows
|
||||
.try_next()
|
||||
.await
|
||||
.expect("stream row")
|
||||
.map(CalendarEventDto::from);
|
||||
let flush = match &next {
|
||||
Some(ev) => {
|
||||
page.len() >= page_uids
|
||||
&& page.last().is_some_and(|p| p.ical_uid != ev.ical_uid)
|
||||
}
|
||||
None => !page.is_empty(),
|
||||
};
|
||||
if flush {
|
||||
let mut chunk = Vec::with_capacity(page.len() * 1024 + 128);
|
||||
{
|
||||
let mut w = quick_xml::Writer::new(&mut chunk);
|
||||
CalDavAdapter::write_report_page(&mut w, &page, &report, &base_href)
|
||||
.expect("page");
|
||||
}
|
||||
std::hint::black_box(&chunk);
|
||||
page.clear();
|
||||
}
|
||||
match next {
|
||||
Some(ev) => page.push(ev),
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
std::hint::black_box(t0.elapsed());
|
||||
}
|
||||
let peak_after = peak_mib();
|
||||
|
||||
let bw = p50(b_wall);
|
||||
let aw = p50(a_wall);
|
||||
let at = p50(a_ttfb);
|
||||
println!("[1] REPORT calendar-query (no range) TTFB ms wall ms peak heap MiB");
|
||||
println!(" BEFORE (buffered) {bw:8.1} {bw:8.1} {peak_before:10.1}");
|
||||
println!(
|
||||
" AFTER (streamed) {at:8.1} {aw:8.1} {peak_after:10.1} TTFB {:.1}x, heap {:.1}x lower",
|
||||
bw / at,
|
||||
peak_before / peak_after
|
||||
);
|
||||
|
||||
// ── [2] Collection GET (.ics) ───────────────────────────────────────────
|
||||
let events_all: Vec<CalendarEventDto> = repo
|
||||
.list_events_by_calendar(&seeded.calendar_id)
|
||||
.await
|
||||
.expect("list")
|
||||
.into_iter()
|
||||
.map(CalendarEventDto::from)
|
||||
.collect();
|
||||
let before_ics = before::generate_full_calendar_ical("Agenda grande", &events_all);
|
||||
drop(events_all);
|
||||
// Streamed ICS: header + per-page chunks + footer (the handler loop).
|
||||
let mut after_ics = String::from(
|
||||
"BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//OxiCloud//NONSGML Calendar//EN\r\nX-WR-CALNAME:Agenda grande\r\n",
|
||||
);
|
||||
let ics_pages: Vec<Vec<CalendarEventDto>> = {
|
||||
use futures::TryStreamExt;
|
||||
let mut rows = repo.stream_events_uid_order(seeded.calendar_id);
|
||||
let mut pages = Vec::new();
|
||||
let mut page: Vec<CalendarEventDto> = Vec::with_capacity(page_uids + 32);
|
||||
while let Some(ev) = rows.try_next().await.expect("stream row") {
|
||||
let ev = CalendarEventDto::from(ev);
|
||||
if page.len() >= page_uids && page.last().is_some_and(|p| p.ical_uid != ev.ical_uid) {
|
||||
pages.push(std::mem::take(&mut page));
|
||||
}
|
||||
page.push(ev);
|
||||
}
|
||||
if !page.is_empty() {
|
||||
pages.push(page);
|
||||
}
|
||||
pages
|
||||
};
|
||||
for events in &ics_pages {
|
||||
let events = &events[..];
|
||||
let mut chunk = String::with_capacity(events.len() * 384);
|
||||
for group in caldav_bench::group_events_by_uid(events) {
|
||||
for event in group {
|
||||
if let Some(vevent) = caldav_bench::extract_vevent_chunk(&event.ical_data) {
|
||||
chunk.push_str(vevent);
|
||||
if !chunk.ends_with('\n') {
|
||||
chunk.push_str("\r\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
after_ics.push_str(&chunk);
|
||||
}
|
||||
after_ics.push_str("END:VCALENDAR\r\n");
|
||||
let gate_ics = before_ics == after_ics;
|
||||
println!(
|
||||
"[2] collection GET .ics: {} bytes, streamed == buffered: {}",
|
||||
before_ics.len(),
|
||||
if gate_ics { "OK" } else { "MISMATCH" }
|
||||
);
|
||||
|
||||
cleanup(&pool, &seeded).await;
|
||||
|
||||
println!(
|
||||
"\n[gate] multistatus byte-identical: {} · ICS byte-identical: {}",
|
||||
if gate_report { "OK" } else { "FAILED" },
|
||||
if gate_ics { "OK" } else { "FAILED" }
|
||||
);
|
||||
if !gate_report || !gate_ics {
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
@@ -245,15 +245,15 @@ async fn main() {
|
||||
.list_readable_by(user_id)
|
||||
.await
|
||||
.expect("repo list")
|
||||
.into_iter()
|
||||
.map(|d| (d.drive.id, d.root_folder_name))
|
||||
.iter()
|
||||
.map(|d| (d.drive.id, d.root_folder_name.clone()))
|
||||
.collect();
|
||||
let warm: Vec<(Uuid, String)> = repo
|
||||
.list_readable_by(user_id)
|
||||
.await
|
||||
.expect("repo list warm")
|
||||
.into_iter()
|
||||
.map(|d| (d.drive.id, d.root_folder_name))
|
||||
.iter()
|
||||
.map(|d| (d.drive.id, d.root_folder_name.clone()))
|
||||
.collect();
|
||||
if before_rows != cold || cold != warm {
|
||||
eprintln!(
|
||||
|
||||
@@ -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