221c1f31b0
Backend (each change benchmark-gated with BEFORE replicas + equivalence gates; see examples/bench_round11_micro.rs, bench_round11_queries.rs, bench_log_writer.rs and benches/ROUND11.md — final numbers land in the follow-up doc commit): - StoragePath re-representation: single canonical joined String, segments derived on demand; File/Folder drop the duplicated path_string field (4000→1000 allocs per 500-row listing page) - Display classifier fusion: classify_display shares one stack-lowered extension across the three decision trees; call sites in FileDto, folder/favorites/recent handlers, trash, path-resolver (+ interning where Arc::from was still used) - /status.php and /openapi.json memoized into OnceLock<Bytes> (openapi rebuilt a 171 KiB spec per request: 2.8 ms → 18 ns) - NC upload-session PROPFIND: write! + pre-sized body + stack RFC2822 dates (2.3-2.6x, 2582→772 allocs at 256 chunks) - REST download: dead FileDto clone removed (capture mime/size + move) - CalendarEventDto/TrashedItem into_parts moves (11 KiB ical_data memcpy gone per CalDAV row); CardDAV getlastmodified stack render - 4xx path: borrowed ErrorResponse serialize, ErrorKind::as_str, not_found/already_exists clone kill - vCard emit via write!; search page moved out with into_iter skip/take; content-hit UUIDs parsed once; group last-user check via HashSet - RateLimiter: lock-free get + insert (and_upsert_with variant REJECTED by benchmark); CSRF token borrow-compare + borrowed cookie extraction - Thumbnail/preview ETags built from as_str (Debug-identical bytes) - Encrypted backend: encrypt_in_place_detached single-buffer write path, chunk-sized reserve in collect_stream; retry labels made lazy - PG: deferred upload registration 3→1 round-trips (persist_file CTE template); direct_grant_cache for Calendar/AddressBook/Playlist authz (single-flight + set_role/clear_role invalidation); expand_user tokio::join!; geo clusters min(uuid)::text; recluster face assignment batched into one UNNEST update - People recluster cosine: norms precomputed once (bit-identical gate) - NC capabilities poll logs demoted to debug; tracing-appender dep added for the log-writer benchmark Frontend: - ResourceList.selectedEntries O(N)-per-toggle → id-index projection O(k log k); favorites/recent consume the batchToolbar snippet param and drop their duplicate filter + dead selectedIds mirror - Recent: star state via new favoriteIds prop — a star click no longer rebuilds all N entries - admin timeAgo >30d fallback uses the cached Intl.DateTimeFormat - vitest gates in src/lib/components/round11.bench.test.ts Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ABhTEHuGujvwoodh67Kga7
158 lines
5.4 KiB
Rust
158 lines
5.4 KiB
Rust
//! Round-11 log-writer benchmark — synchronous fmt layer (stdout under a
|
||
//! global lock, on the async workers) vs `tracing_appender::non_blocking`
|
||
//! with `lossy(false)` (audit lines must never drop; the emitting thread
|
||
//! blocks only if the 128k-line channel fills).
|
||
//!
|
||
//! Two writer profiles:
|
||
//! - fast: stdout redirected to /dev/null (best case for the sync arm)
|
||
//! - slow: a writer that burns ~20 µs per line under the same lock,
|
||
//! modelling a laggy pipe / journald / TTY consumer
|
||
//!
|
||
//! The global subscriber can only be installed once per process, so the
|
||
//! arm is chosen via env and the harness runs the binary once per arm:
|
||
//!
|
||
//! BENCH_LOG_ARM=sync cargo run --release --features bench --example bench_log_writer >/dev/null
|
||
//! BENCH_LOG_ARM=nonblocking cargo run --release --features bench --example bench_log_writer >/dev/null
|
||
//! BENCH_LOG_WRITER=slow BENCH_LOG_ARM=... (slow-writer profile)
|
||
//!
|
||
//! Measurements print to stderr. Emits 4 workers × 25k events; reports
|
||
//! total wall, per-event p50/p99/p999 emit latency, and (for the
|
||
//! non-blocking arm) confirms zero dropped lines via a line count gate
|
||
//! (lossy(false) + guard flush).
|
||
|
||
use std::io::Write;
|
||
use std::sync::atomic::{AtomicU64, Ordering};
|
||
use std::time::Instant;
|
||
|
||
use tracing_subscriber::layer::SubscriberExt;
|
||
use tracing_subscriber::util::SubscriberInitExt;
|
||
|
||
static LINES: AtomicU64 = AtomicU64::new(0);
|
||
|
||
/// Counts lines then forwards to stdout (which the run command redirects
|
||
/// to /dev/null). The `slow` profile burns ~20 µs per write while holding
|
||
/// the caller's lock, modelling a slow consumer.
|
||
struct CountingWriter {
|
||
slow: bool,
|
||
}
|
||
|
||
impl Write for CountingWriter {
|
||
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
|
||
LINES.fetch_add(1, Ordering::Relaxed);
|
||
if self.slow {
|
||
let t = Instant::now();
|
||
while t.elapsed().as_micros() < 20 {
|
||
std::hint::spin_loop();
|
||
}
|
||
}
|
||
std::io::stdout().write_all(buf)?;
|
||
Ok(buf.len())
|
||
}
|
||
fn flush(&mut self) -> std::io::Result<()> {
|
||
std::io::stdout().flush()
|
||
}
|
||
}
|
||
|
||
#[derive(Clone)]
|
||
struct MakeCounting {
|
||
slow: bool,
|
||
}
|
||
impl<'a> tracing_subscriber::fmt::MakeWriter<'a> for MakeCounting {
|
||
type Writer = CountingWriter;
|
||
fn make_writer(&'a self) -> Self::Writer {
|
||
CountingWriter { slow: self.slow }
|
||
}
|
||
}
|
||
|
||
fn main() {
|
||
let arm = std::env::var("BENCH_LOG_ARM").unwrap_or_else(|_| "sync".into());
|
||
let slow = std::env::var("BENCH_LOG_WRITER").as_deref() == Ok("slow");
|
||
let workers = 4usize;
|
||
let per_worker = 25_000u64;
|
||
|
||
// Same filter shape as main.rs.
|
||
let filter = tracing_subscriber::EnvFilter::new("info,http=warn,http::web=error");
|
||
|
||
// Keep the non-blocking guard alive for the whole run.
|
||
let _guard: Option<tracing_appender::non_blocking::WorkerGuard> = match arm.as_str() {
|
||
"nonblocking" => {
|
||
let (nb, guard) = tracing_appender::non_blocking::NonBlockingBuilder::default()
|
||
.lossy(false)
|
||
.finish(CountingWriter { slow });
|
||
tracing_subscriber::registry()
|
||
.with(filter)
|
||
.with(tracing_subscriber::fmt::layer().with_writer(nb))
|
||
.init();
|
||
Some(guard)
|
||
}
|
||
_ => {
|
||
tracing_subscriber::registry()
|
||
.with(filter)
|
||
.with(tracing_subscriber::fmt::layer().with_writer(MakeCounting { slow }))
|
||
.init();
|
||
None
|
||
}
|
||
};
|
||
|
||
let rt = tokio::runtime::Builder::new_multi_thread()
|
||
.worker_threads(workers)
|
||
.enable_all()
|
||
.build()
|
||
.unwrap();
|
||
|
||
let (wall, mut lat_us): (f64, Vec<f64>) = rt.block_on(async {
|
||
let t0 = Instant::now();
|
||
let mut handles = Vec::new();
|
||
for w in 0..workers {
|
||
handles.push(tokio::spawn(async move {
|
||
let mut lats = Vec::with_capacity(per_worker as usize);
|
||
for i in 0..per_worker {
|
||
let t = Instant::now();
|
||
tracing::info!(worker = w, seq = i, "bench log line with a few fields");
|
||
lats.push(t.elapsed().as_secs_f64() * 1e6);
|
||
if i % 512 == 0 {
|
||
tokio::task::yield_now().await;
|
||
}
|
||
}
|
||
lats
|
||
}));
|
||
}
|
||
let mut all = Vec::new();
|
||
for h in handles {
|
||
all.extend(h.await.unwrap());
|
||
}
|
||
(t0.elapsed().as_secs_f64(), all)
|
||
});
|
||
|
||
// Flush (drop guard for non-blocking) before counting lines.
|
||
drop(_guard);
|
||
std::thread::sleep(std::time::Duration::from_millis(200));
|
||
|
||
lat_us.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
||
let pct = |p: f64| lat_us[((lat_us.len() as f64 * p) as usize).min(lat_us.len() - 1)];
|
||
let total = workers as u64 * per_worker;
|
||
let emitted = LINES.load(Ordering::Relaxed);
|
||
|
||
eprintln!(
|
||
"arm={arm} writer={} events={total} wall={:.3}s ({:.0} ev/s)",
|
||
if slow {
|
||
"slow(20µs)"
|
||
} else {
|
||
"fast(/dev/null)"
|
||
},
|
||
wall,
|
||
total as f64 / wall
|
||
);
|
||
eprintln!(
|
||
" emit latency µs: p50={:.1} p99={:.1} p999={:.1} max={:.1}",
|
||
pct(0.50),
|
||
pct(0.99),
|
||
pct(0.999),
|
||
lat_us[lat_us.len() - 1]
|
||
);
|
||
eprintln!(
|
||
" gate[no lines dropped]: {}",
|
||
if emitted >= total { "OK" } else { "FAILED" }
|
||
);
|
||
}
|