perf: round 16 — shares-lane & contextMap incremental builders, folder/href/disposition/preview alloc cuts

Finishes the route-level half of the O(N²/page) grouped-listing class ROUND15
fixed inside ResourceList, plus a backend CPU/alloc micro-pack. Every change is
benchmark-gated with a hard rollback rule; no PostgreSQL needed for any arm
(benches/ROUND16.md).

Frontend (vitest):
- F1 "My shares" lanes: the `lanes` $derived.by re-bucketed the whole
  accumulated grant list on every page and every grant edit. SharedLanesBuilder
  re-emits only the fresh page (fan-out + first-appearance header), reusing
  untouched lanes' array refs. 25.5x fewer emit calls, 8.8x wall, O(N²/page)→O(N).
- F2 contextMap (trash/recent/favorites/shared-with-me): each rebuilt a fresh
  N-key Map, re-hashing every accumulated id, per page. primeContextPage holds a
  persistent SvelteMap primed per page (the shipped favoriteIds shape).
  25.5x fewer entry calls, 7.2x wall.
- Extracted the shared O(1) append test (isAppendExtension); F1's gate re-covers it.

Backend (counting-allocator):
- M1 folder display constants Arc::from -> intern_display (3 sites): 3 -> 0 allocs/row.
- M2 build_content_disposition (every download + Range seek): 3 -> 1 alloc, 6x, 2.67x wall.
- M3 nc_href (every NC PROPFIND/REPORT href): Vec+join+format -> one pre-sized
  buffer, keeping urlencoding::encode (byte-identical). 38 -> 27 allocs/op.
- M4 NC preview fileId: collect-then-parse -> borrow-slice parse. 4 -> 0 allocs.

Gates: sharedLanes/listContext.bench.test.ts, examples/bench_round16_micro.rs
(GATE PASS all sections). Frontend: vitest 331 pass, svelte-check clean.
Backend: clippy -D warnings clean, 524 lib tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0193NjactJVqfU32gxeJDj8m
This commit is contained in:
Claude
2026-07-19 17:30:22 +00:00
parent 8537b5949a
commit 955f4a7b9f
20 changed files with 1388 additions and 123 deletions
+30 -9
View File
@@ -1153,18 +1153,39 @@ pub(super) fn build_content_disposition(name: &str, mime: &str, force_inline: bo
.remove(b'`')
.remove(b'|')
.remove(b'~');
let encoded = utf8_percent_encode(name, RFC5987_SET).to_string();
// Fast path: a name whose every byte is an RFC 5987 attr-char needs neither
// percent-encoding nor ASCII-fallback filtering ('"' and '\\' are not
// attr-chars, so none is substituted), so `filename` and `filename*` are the
// name verbatim — one allocation (the header) instead of three.
let all_attr_char = name.bytes().all(|b| {
b.is_ascii_alphanumeric()
|| matches!(
b,
b'!' | b'#' | b'$' | b'&' | b'+' | b'-' | b'.' | b'^' | b'_' | b'`' | b'|' | b'~'
)
});
if all_attr_char {
return format!("{disposition}; filename=\"{name}\"; filename*=UTF-8''{name}");
}
let ascii_safe: String = name
.chars()
.filter(|c| c.is_ascii_graphic() || *c == ' ')
.map(|c| match c {
// Slow path: assemble the header in one pre-sized buffer, writing the ASCII
// fallback and the percent-encoded form in place — no throwaway `ascii_safe`
// / `encoded` Strings. Sized for the worst case (every byte → %XX) so it
// never grows.
let mut out = String::with_capacity(disposition.len() + name.len() * 4 + 32);
out.push_str(disposition);
out.push_str("; filename=\"");
for c in name.chars().filter(|c| c.is_ascii_graphic() || *c == ' ') {
out.push(match c {
'"' | '\\' => '_',
_ => c,
})
.collect();
format!("{disposition}; filename=\"{ascii_safe}\"; filename*=UTF-8''{encoded}")
});
}
out.push_str("\"; filename*=UTF-8''");
for chunk in utf8_percent_encode(name, RFC5987_SET) {
out.push_str(chunk);
}
out
}
// ── Route handlers (free functions) ──────────────────────────────────────────