perf: round 9 — decorator PUT reactivation, session/search/dedup alloc purges, PROPFIND join!, folder-level cascade
Benchmark-gated round (benches/ROUND9.md): every change carries a BEFORE/AFTER bench with equivalence/safety gates; verdicts below are from the committed harnesses on 4 cores / local PG 16. Backend: - Blob decorators (Retry/Cached) now forward put_blob_from_bytes_unsynced + sync_blobs — the trait default had silently reinstated HEAD-before-PUT per chunk on decorated remote stacks, undoing ROUND3 §8. Full production stack: 500 probes -> 0, 1.9x wall at 10 ms RTT (bench_s3_put §3). - NC PROPFIND per-page enrichment triple (favorites / oc:fileid / dead props) overlapped with tokio::join!: 2.07x local, 2.86x at 5 ms RTT (bench_nc_enrich_join, injected-latency decide-by-bench). - Search enrichment consumes its DTOs and carries the interned Arc<str> display fields end-to-end (SearchFileResultDto type change, OpenAPI shape preserved): enrich_file 2.0x, 11.6 -> 2.2 allocs/row; the NC REPORT conversion stops re-running all three classifiers per row (bench_search_enrich). - NC session Arc end-to-end: SharedNcSession extractor (8 -> 0 allocs), Arc<FolderDto> chroot cache (4 -> 0/hit), single shared Arc<CurrentUser> + lazy span render (11 -> 6/build) (bench_nc_session). - Storage micro-pack: atomic create_new chunk writes (2.1x fresh), stream_chunks over the manifest Arc (4097 -> 0 allocs/read incl. the Range path), manifest single-flight (herd 64 -> 1 loads), hex_lower for chunk Content-MD5 (18 -> 1 allocs) (bench_storage_micro). - OCS capabilities memoized into OnceLock<[Bytes;2]>: 237x, 102 -> 0 allocs/poll, byte-identical (bench_capabilities_static). - Drive::is_empty COUNT(*) sum -> EXISTS: 34.4x on a 100k-file drive (bench_drive_is_empty). - favorites/recents row-map ROUND7 port: path/name/blob_hash moved, -2.75 allocs/row (bench_resource_row_map §2). - Folder rows decode binary UUIDs (ROUND6 §10 port): 1.03-1.07x page fetch, honest verdict incl. one noise-band wash documented (bench_folder_uuid_decode). - Authz: file cascade decision decomposed into memoized folder-level decision + direct-grant lookup (ROUND8 deferred item). Cold shared-album first view 592 -> 418 µs/thumb; warm path unchanged; safety gates incl. new direct-grant sibling isolation, revoke-flush re-verified, full integration authz suite green (bench_thumbnail_cascade_cache). Frontend (vitest gates committed beside the code): - resolveLabel/resolveRecipient O(directory) scan -> id-keyed Map: 13.9x (recipients.bench.test.ts). - ResourceList selection-prune effect skips when nothing is selected (100 -> 0 Set builds per drain) and the photos timeline reads a listener-fed mobile flag instead of matchMedia per recompute (listDerives.bench.test.ts). Verification: cargo fmt + clippy --all-features --all-targets -D warnings clean; 524 unit + 554 integration (--cfg integration_tests) tests pass; frontend npm run check clean with 293 vitest tests green. Deferred with rationale in ROUND9.md: CalDAV authz-before-fetch reorder (maintainer sign-off), per-page batched parent resolution, JWT-claims Arc<str>, batch_operations signature widening. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XDc9VtXvskJ6dnMRraSndn
This commit is contained in:
@@ -218,7 +218,7 @@ pub fn nc_href(username: &str, subpath: &str) -> String {
|
||||
pub async fn handle_nc_webdav(
|
||||
state: Arc<AppState>,
|
||||
req: Request<Body>,
|
||||
session: crate::interfaces::nextcloud::session::NcSession,
|
||||
session: crate::interfaces::nextcloud::session::SharedNcSession,
|
||||
subpath: String,
|
||||
) -> Result<Response<Body>, AppError> {
|
||||
// Validate up-front that we have a chroot — every method below is
|
||||
@@ -1566,19 +1566,29 @@ fn build_nc_streaming_propfind(
|
||||
}
|
||||
let batch_len = batch.len();
|
||||
|
||||
// Per-page enrichment: favorites + oc:fileids, two batch queries.
|
||||
let favs = if let Some(fav) = fav_svc {
|
||||
let items: Vec<(&str, &str)> =
|
||||
batch.iter().map(|f| (f.id.as_str(), "file")).collect();
|
||||
fav.batch_check_favorites(user_id, &items).await.unwrap_or_default()
|
||||
} else {
|
||||
HashSet::new()
|
||||
};
|
||||
// Per-page enrichment: favorites + oc:fileids + dead props —
|
||||
// three independent reads over the same id batch, overlapped
|
||||
// with `join!` so a page pays ~max(RTT) instead of 3×RTT
|
||||
// (each query still batched per page: DEAD-PROPS.md). The
|
||||
// round-7 deferred "serial pairs" item, adopted for this
|
||||
// per-page triple after the injected-latency A/B in
|
||||
// benches/ROUND9.md showed no local-PG regression.
|
||||
let fav_items: Vec<(&str, &str)> =
|
||||
batch.iter().map(|f| (f.id.as_str(), "file")).collect();
|
||||
let file_uuids: Vec<&str> = batch.iter().map(|f| f.id.as_str()).collect();
|
||||
let (file_id_map, _) = batch_resolve_ids(file_id_svc, &file_uuids, &[]).await;
|
||||
// One batched dead-props query per page, not one per child
|
||||
// (benches/DEAD-PROPS.md).
|
||||
let file_deads = files_dead_props_map(&state.webdav_dead_props, &batch).await;
|
||||
let (favs, (file_id_map, _), file_deads) = tokio::join!(
|
||||
async {
|
||||
if let Some(fav) = fav_svc {
|
||||
fav.batch_check_favorites(user_id, &fav_items)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
} else {
|
||||
HashSet::new()
|
||||
}
|
||||
},
|
||||
batch_resolve_ids(file_id_svc, &file_uuids, &[]),
|
||||
files_dead_props_map(&state.webdav_dead_props, &batch),
|
||||
);
|
||||
|
||||
let mut chunk = Vec::with_capacity(batch_len * 1024);
|
||||
{
|
||||
@@ -1624,18 +1634,23 @@ fn build_nc_streaming_propfind(
|
||||
break;
|
||||
}
|
||||
|
||||
let favs = if let Some(fav) = fav_svc {
|
||||
let items: Vec<(&str, &str)> =
|
||||
batch.iter().map(|sf| (sf.id.as_str(), "folder")).collect();
|
||||
fav.batch_check_favorites(user_id, &items).await.unwrap_or_default()
|
||||
} else {
|
||||
HashSet::new()
|
||||
};
|
||||
// Same overlapped enrichment triple as the file pages above.
|
||||
let fav_items: Vec<(&str, &str)> =
|
||||
batch.iter().map(|sf| (sf.id.as_str(), "folder")).collect();
|
||||
let folder_uuids: Vec<&str> = batch.iter().map(|sf| sf.id.as_str()).collect();
|
||||
let (_, sub_id_map) = batch_resolve_ids(file_id_svc, &[], &folder_uuids).await;
|
||||
// Batched — see benches/DEAD-PROPS.md.
|
||||
let sub_deads =
|
||||
folders_dead_props_map(&state.webdav_dead_props, &batch).await;
|
||||
let (favs, (_, sub_id_map), sub_deads) = tokio::join!(
|
||||
async {
|
||||
if let Some(fav) = fav_svc {
|
||||
fav.batch_check_favorites(user_id, &fav_items)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
} else {
|
||||
HashSet::new()
|
||||
}
|
||||
},
|
||||
batch_resolve_ids(file_id_svc, &[], &folder_uuids),
|
||||
folders_dead_props_map(&state.webdav_dead_props, &batch),
|
||||
);
|
||||
|
||||
let mut chunk = Vec::with_capacity(batch.len() * 1024);
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user