20b1ea1a6a3b5b62b45c8da77b76a908d7db64e5
18 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
12dc648cff |
perf: round 4 — one-pass row paths, drive-selector cache, CalDAV single-parse, streamed Azure, batched hydration
Nine benchmark-gated changes (benches/ROUND4.md; every one ships with a BEFORE/AFTER bench + equivalence gate, rollback rule as ROUND2/3): - Row→entity path build: one-pass StoragePath::from_folder_and_name / from_joined + normalize_storage_name_owned + alloc-free Display — 743→417 ns/file-row (1.78x), −5 allocs/row on every listing surface. - WebDAV drive-selector: per-user readable_cache (single-flight, 30 s TTL, explicit invalidation incl. membership + group changes) replaces the grants join per request — 441 µs → 0.8 µs (~550x), 0 queries warm. - CalDAV from_ical/update_ical_data: 8 full IcalParser runs per VEVENT → 1 (7.1x per PUT, 4.4x on 50-event imports); alloc-free split_vevents, chunk scan without the whole-body uppercase copy (1.4x), borrowed-key UID grouping (1.3x), REPORT props no longer cloned. - PROPFIND emit: partition Vecs dropped (single-pass 404 list) + stack rendered RFC 3339/2822 dates, sizes, quoted etags (common::fmt, chrono-byte-identical, sweep-tested) on both DAV surfaces — 1.22x per page, 17.9→12.0 allocs/row. - Grant-listing hydration: calendars/address books/playlists batch hydrate via = ANY($1) — 15 serial queries → 1 (~13x per sync poll). - user-flags cache: get→insert → try_get_with single-flight (32→1 queries per cold herd). - Azure downloads: whole-blob Vec buffering → streamed SDK pages — TTFB 349→4 ms (87x), peak heap 480→1.9 MiB (254x) on 256 MiB blobs; new OXICLOUD_AZURE_ENDPOINT_URL override (Azurite/bench hook). - Face indexing: unbounded per-image tokio::spawn → core-count semaphore, permit before blob read — peak heap 1175→176 MiB (6.7x). Checks: cargo fmt, clippy --all-features --all-targets -D warnings, cargo test --workspace (523 passed) + --features test_utils. hurl API suite and dockerized integration DB not runnable in this environment — left to CI. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017aJu9ghvuT8WqC31ZEGTBA |
||
|
|
cd4c62042a |
perf: keyset/LATERAL SQL shapes, auth+blob-cache single-flight, spool buffers, DTO interning
Round 3 of benchmark-gated optimizations (benches/ROUND3.md; every change gated by a before/after benchmark — an AFTER that did not beat its BEFORE was to be rolled back; none needed it. Equivalence gates assert identical row sequences / byte-identical output on every behavior-preserving rewrite): DB hot paths (local PG16, EXPLAIN-verified): - Web-UI listing (list_resources_paged): cursor pushed INSIDE the folders/files UNION-ALL branches as sargable row-value comparisons with per-branch ORDER/LIMIT + two partial expression indexes (folder_id, LOWER(name), id). 20k-entry folder: 26.6 -> 1.3 ms/page (19.5x); other sort modes at parity or better. New migration 20260918000000. [benches/LISTING-KEYSET.md section in ROUND3] - Photos timeline (list_media_files): per-drive CROSS JOIN LATERAL top-N on the timeline index, joins moved above the top-N. 50k-photo library: 97.4 -> 1.6 ms/page (55.7x). The old "LIMIT stops the scan early" comment was refuted by EXPLAIN. - PROPFIND sub-folders (both DAV surfaces): keyset list_folders_batch off idx_folders_unique_name replaces COUNT(*) OVER() + LIMIT/OFFSET (5k dirs: 79.7 -> 17.9 ms full walk, 4.5x). Concurrency: - Basic-auth cache single-flight (moka try_get_with): 8 concurrent DAV connections at TTL expiry paid 8 Argon2id runs (2.6 s CPU + 8x64 MiB); now 1 (300 ms). Failed verifications remain uncached. - CachedBlobBackend per-hash single-flight + unique tmp names: 16 concurrent cold readers = 16 full remote downloads racing truncating writes on ONE deterministic .tmp (corruptible cache); now 1 download (16x less egress, 2.8x wall on a shared link) and torn files can never be renamed into the cache. I/O and allocations: - Chunk-assembly reads 64K -> 512K buffers (2.3x, 8x fewer syscalls); chunk-spool writes via BufWriter 512K (5.6x, 32x fewer syscalls). - S3/Azure put_blob_from_bytes_unsynced overrides: dedup settle no longer pays a HEAD probe per new chunk (2 RTT -> 1, 1.8x); Azure stops copying every chunk (Bytes -> Body, -0.44 ms - 4 MiB alloc per 4 MiB chunk). - Entity->DTO mapping: Arc<str> interning of closed-set display fields + common MIMEs, 1-alloc etag/size formatting, FolderDto moves instead of clones. File row: 11 -> 4 allocs; folder row: 11.8 -> 1 (2.1x faster). - CardDAV REPORT: deleted dead per-contact vCard pre-generation and the O(N^2) uid scan whose result was discarded (5k contacts: 55.7 -> 5.7 ms, 9.8x); byte-identical XML asserted. - Search-results cache: byte weigher + 32 MiB budget (OXICLOUD_SEARCH_CACHE_MAX_BYTES) replaces the 1000-ENTRY cap that let ~300 MiB of enriched rows sit in RSS; read latency parity. - Dropped aws-config + aws-smithy-types (zero references; -82 dep-graph nodes, three SDK stacks gone from every build). tokio "process" is now an explicit feature (was enabled transitively by aws-config). Frontend: - Cached Intl.DateTimeFormat keyed by (locale, options) in formatDate and 4 sibling callsites: 20k dates 2612 -> 51 ms (51.6x); vitest gate asserts output identity across locales and a 3x floor. Validation: cargo fmt + clippy --all-features --all-targets -D warnings clean; 518 unit + 548 integration-cfg tests green; new-shape endpoints smoke-tested end-to-end over HTTP (all 5 listing sort modes with cursor walks, WebDAV PROPFIND Depth-1, photos timeline, Basic-auth DAV login); frontend npm run check clean, new vitest gates green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EBsU2qEzny3A8WQUEuMNCr |
||
|
|
82ee7da0d2 |
perf: serve ranges from RAM cache, stream ZIPs, overlap ingest settle, O(1) chunk gate
Round 2 of benchmark-gated optimizations (benches/ROUND2.md; every change gated by a before/after in examples/bench_round2.rs — an AFTER that did not beat its BEFORE was to be rolled back; none needed it): - Range requests (REST/DAV/shares) answered from the moka content cache for sub-10MB files: PG resolve + open/seek/read -> Bytes::slice. 256KiB seeks: 1,730/s -> 3.7M/s (p50 552us -> 0.15us). - Streaming folder/share ZIPs via tokio duplex: TTFB no longer scales with archive size (326ms -> 0.4ms on 192MiB corpus; total also faster). Content-Length dropped (size unknown up front). - NC chunked-upload per-PUT gate: O(k) directory scan+stat -> in-RAM per-session counter (lazy rebuild on cold start). 1,000-chunk upload gate cost: 33.1s -> 0.09s cumulative. - Delta download + commit-verify now use the CDC path's buffered(read_prefetch) read-ahead: 64-chunk drain at 5ms open latency 440ms -> 51ms; order preserved. - CDC ingest settles batches on a spawned task (depth-1 pipeline) so the source stream keeps flowing during PG pin + backend writes; rollback ledger shared + lock-serialized so compensation stays exact on cancellation. 512MiB paced ingest: 60-69 -> 74-75 MB/s. OXICLOUD_INGEST_OVERLAP=0 restores inline settling (ops/bench hatch). - Frontend: instant-upload BLAKE3 hashing moved off the main thread to a bounded Web Worker pool (File handles by reference); vitest gate asserts the pool beats sequential (first gate draft posting buffers was 2.6x slower and was rewritten — copies dominated). Validation: cargo fmt + clippy -D warnings clean; 514 unit + 544 integration tests green; 270 frontend tests green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CBK1RdtzyP6759Muqe1K1w |
||
|
|
aba89c4f5d |
perf: eliminate N+1 hot-path queries, cache immutable lookups, stop re-compressing compressed bytes
Every change is benchmark-verified (harness + before/after numbers in benches/, measured on this branch; reproduction commands in each doc): DAV / sync-client hot paths - PROPFIND dead-properties: one = ANY($1) query per 500-child page instead of one sequential query per child, and indexable `=` predicates instead of IS NOT DISTINCT FROM (seq scans). 2,000-child folder: 1.07-4.54 s of DB chatter -> 4-6 ms (258-773x). Applied to native + NC PROPFIND and both NC REPORT handlers. [benches/DEAD-PROPS.md] - Folder paging: keyset cursor (name > $last) + new partial index (folder_id, name) replaces LIMIT/OFFSET full-folder rescan per page. Full 20k-file walk: 1266 ms -> 77 ms (16.5x). New migration 20260917000000. [benches/PROPFIND-PAGING.md] - NC chroot / default-drive resolution: moka caches (30 s TTL, explicit invalidation on drive mutations) for find_default_for_user and the markerless chroot FolderDto. 2 uncached queries + 2 pool checkouts per NC/WebDAV/WOPI request -> sub-us moka hit (p50 0.7-3.6 ms -> ~1 us). [benches/CHROOT-CACHE.md] - Quota: PROPFINDs whose prop list never names a quota prop skip the 2-query resolution entirely (wants_quota()); the remaining lookups read 2 columns instead of the full auth.users row with its <=512 KiB avatar (11-16x, p50 3.4 ms -> 0.29 ms). Same narrow read now gates every upload quota check. [benches/QUOTA-PATH.md] CPU on the request path - ZIP exports (folder download, share ZIP, batch download): entries whose MIME says already-compressed (JPEG/MP4/zip/pdf/...) are Stored instead of Deflate - deflate ran inline on the tokio writer task at ~41 MB/s for ~0% size gain. Mixed media corpus: 4.31x wall and CPU, archive size unchanged. Shared predicate in common::mime_detect. [benches/ZIP-MEDIA.md] - Compression layers: tower-http's default maps to Brotli QUALITY 11 (verified in brotli-8.0.2 source and empirically: 90 ms per 64 KiB JSON response, 1.3 s per 700 KiB bundle). Both layers pinned to Precise(4): 99x less CPU for ~15% more bytes. SPA assets are now precompressed at build time (scripts/precompress.mjs, 77% smaller) and served via ServeDir::precompressed_br/gzip: 2016x less per-request work, and clients get the better q11 bytes. [benches/STATIC-PRECOMPRESSED.md] Batched / cached backend paths [benches/NPLUS1-AND-CACHES.md] - Content-search ReBAC re-verification: new AuthorizationEngine::check_files_read_batch (default = old loop; PgAclEngine override batches drive resolution + reuses role cache). 200 sequential point SELECTs per search -> 1-2 queries. - Batch-ZIP subtree downloads: drop per-file re-authz + per-file Recent recording (2 writes/file) for subtree entries already authorized at the root - mirrors the native folder-download path. ~6,000 statements removed from a 2,000-file archive. - CDC chunk manifests: immutable by content address, now moka-cached (weight-bounded 32 MiB, 60 s TTL, positive-only, invalidated on delete) - removes one manifest query (p50 0.44-4.4 ms) from every stream, range and full blob read. - People tab: grouped COUNT + batched cover lookup instead of dragging every face row with its 2 KiB embedding (10k faces: 30.4 ms & 21 MB -> 3.8 ms & 1.3 KB, 8.1x); merge() is one set-based UPDATE. [benches/PEOPLE-LIST.md] - Photos timeline cursor: raw timestamptz comparison instead of EXTRACT(EPOCH ...) wrapper + IS NULL OR disjunction - cursor is an index boundary again, deep scroll stops re-scanning skipped rows. - Public share landing: one atomic UPDATE ... access_count + 1 (was SELECT + full-row write-back: racy, lost updates, clobbered concurrent owner edits) - 3 round-trips -> 2 per visit. - move_to_trash: dead full-entity SELECT feeding a documented no-op removed from both branches; dead fields dropped from TrashService. - NFC normalization: is_nfc_quick fast path skips the decompose/recompose state machine for the ~100% already-NFC case (every row loaded from PG). Frontend - Large folders paint after page one (~200 items) via fetchFolderListing's new onPage hook instead of waiting for every sequential page. - Tested-and-reverted (kept for the record): cached Intl.Collator for name sorts - vitest showed it 2x SLOWER than V8's argument-less localeCompare fast path (5.6 ms vs 12.1 ms / 5k names). Sort order untouched. New bench harnesses under examples/ (bench feature): zip_media, dead_props, chroot_cache, quota_path, people_list, propfind_paging, static_precompress. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CBK1RdtzyP6759Muqe1K1w |
||
|
|
0a93123859 |
style(bench): rustfmt the blob/pool/runtime bench examples
These example targets landed unformatted on main and fail the Rustfmt CI check (`cargo fmt --all --check`); reformat them so this PR's checks pass. No logic changes. |
||
|
|
601fbdedf9 |
Revert "perf(blob): one syscall per new chunk in write_blob_bytes"
This reverts
|
||
|
|
ee51b32ba9 |
perf(blob): one syscall per new chunk in write_blob_bytes (create_new)
Replace the try_exists(stat) + File::create(O_CREAT|O_TRUNC) pair with a single OpenOptions::create_new (O_CREAT|O_EXCL), treating AlreadyExists as the existing idempotent skip. One metadata syscall per new chunk instead of two (each a spawn_blocking round-trip), and O_EXCL closes the check-then-create TOCTOU the old pair left open (a racing writer could be truncated). Honest measurement caveat (benches/BLOB-WRITE.md): the wall-clock throughput effect is BELOW the noise floor of the test environment — three 9-rep interleaved runs on the same ext4 device swing −12%..+21% at the 256 KiB CDC size, because a negative stat on a warm dentry cache is ~µs, dwarfed by the chunk's create+write+flush. So this is justified as a code-quality / correctness change (canonical idiom, strictly fewer syscalls, closes a TOCTOU, zero downside), NOT as a benchmarked perf win. The sibling idea — reusing the File handle for the fsync sweep — is deliberately NOT done: sync_blobs is a single end-of-stream sweep over all the upload's new hashes, so retaining handles would hold thousands of FDs open (>ulimit) on a large upload. The re-open sweep is a deliberate FD-frugal design. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JG5yYZ9s868mJwqT2Qz7ez |
||
|
|
013090a14e |
bench: measure peak RSS vs decode permits + record pool-concurrency results
Adds Part B (peak RSS for K concurrent decodes) to bench_pool_concurrency and records the findings in benches/POOL-CONCURRENCY.md. Honest result: under a 2-core quota the thumbnail decode pool shows flat throughput, p99, AND peak RSS (137 MiB) from K=1..16 — shrink-on-load already made each decode RAM-cheap, so over-permitting costs nothing measurable here. The effective_parallelism() migration is therefore a correctness/consistency change with no downside, mainly protecting the transcode + ffmpeg pools (and extreme host-core/quota ratios) this box can't reproduce — not a throughput win. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JG5yYZ9s868mJwqT2Qz7ez |
||
|
|
a504578303 |
bench: add CPU pool concurrency benchmark (thumbnail decode under a quota)
bench_pool_concurrency drives the real service path (Semaphore(K) gating spawn_blocking(bench_render_all)) with a gallery of concurrent callers and sweeps the decode-permit count K, reporting throughput + p50/p99. Run under taskset to model a CPU quota: it shows the effect of sizing the image pools to effective_parallelism (K=cores) vs the host count (over-subscribed). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JG5yYZ9s868mJwqT2Qz7ez |
||
|
|
b8d93bed07 |
bench: add Tokio runtime tuning benchmark (workers + blocking-pool RSS)
bench_tokio_runtime (behind the bench feature) measures the two things
build_runtime changes vs the #[tokio::main] defaults:
A) worker over-subscription — throughput + p50/p99 of an async+CPU workload
with many workers vs core-sized, run under taskset to model a CPU quota.
B) blocking-pool RSS blast radius — peak RSS flooding the blocking pool with
memory-heavy spawn_blocking tasks, default 512 vs bounded.
No Postgres needed. Lets us verify the runtime change empirically before/after.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JG5yYZ9s868mJwqT2Qz7ez
|
||
|
|
73b7feeb0f |
bench: add blob download read-ahead benchmark (read_prefetch sweep)
Adds `bench_blob_prefetch` (behind the `bench` feature) to measure the local backend's chunk read-ahead depth — the `buffered(N)` read-ahead in DedupService::stream_chunks fed by BlobStorageBackend::read_prefetch(). The bench reproduces the exact production reassembly combinator over a real LocalBlobBackend (chunk files scattered across the 256 hash-prefix dirs) and sweeps the prefetch depth under the two axes that decide whether read-ahead helps on local disk: - consumer speed: unthrottled (disk-bound) vs throttled@MB/s (network-bound) - page cache: warm vs cold (posix_fadvise DONTNEED, Linux best-effort) N=1 is current production; higher N is the candidate change. Lets us verify gains/regressions empirically before changing read_prefetch(), since the trait doc deliberately defaults local to 1. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JG5yYZ9s868mJwqT2Qz7ez |
||
|
|
f68972e368 |
perf(blob): one manifest query in read_blob_bytes instead of two
read_blob_bytes read the same storage.chunk_manifests PK row twice per full-blob read — blob_size (SELECT total_size) then read_blob_stream (SELECT chunk_hashes) — even though both columns live in one row. Fold them into a single `SELECT chunk_hashes, total_size` and share the chunk stream builder via a new stream_chunks helper. The legacy (no-manifest) path is unchanged. Output is identical; the read just costs one fewer DB round-trip. Benchmark (examples/bench_blob_manifest.rs, isolates the manifest lookup against the real Postgres): ~1.9x throughput and p50/p99 roughly halved on that sub-step; the win is the removed round-trip under pool pressure during upload bursts. Note this is the manifest sub-step only — end-to-end read_blob_bytes is dominated by the actual chunk reads, and it is a background path (thumbnail generation / EXIF / indexing), not normal gallery serving. Methodology + honest framing in benches/BLOB-MANIFEST.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
5722481c4a |
feat(thumbnails): server-side video thumbnails via ffmpeg
Videos now get a thumbnail generated eagerly server-side on upload, through the same WebP/blob-hash pipeline as photos — instead of the old browser path that only ran when the Photos grid first rendered a video tile, re-downloaded the whole video to seek a frame, and PUT 3 JPEGs back (and produced nothing at all for HEVC/.mov, which a browser <video> cannot decode). - New VideoFramePort (application) + FfmpegVideoFrameService / NoopVideoFrameService (infrastructure): shell out to the system ffmpeg (no compile-time libav dep), extract one representative frame as PNG, bounded by its own semaphore + a per-process timeout + kill_on_drop. Noop when ffmpeg is absent/disabled, so videos degrade gracefully to no thumbnail. - ThumbnailRefreshHook.on_file_created routes video/* to generate_video_thumbnails_background: stream the (decrypted, reassembled) blob to a size- and time-bounded temp file on the data volume, extract a frame, and reuse the shared render_and_persist_all_webp helper — so video thumbnails are WebP, blob-hash keyed (dedup'd) and content-negotiated, exactly like photos. - GET thumbnail serves the video's WebP to every client (byte-sniffed Content-Type); a genuine miss returns 204. - Config: OXICLOUD_ENABLE_VIDEO_THUMBNAILS (default true, needs ffmpeg detected at startup) + OXICLOUD_FFMPEG_PATH / _CONCURRENCY / _TIMEOUT_SECS / _MAX_MB. - Dockerfile installs ffmpeg in the runtime image. - Frontend: drop the client-side generateVideoThumb/frameFromVideo re-download path; the server is now the source of truth. Benchmark (examples/bench_video_thumbnails.rs, needs ffmpeg): 4/4 codecs incl. HEVC/.mov produce a thumbnail server-side (was 0% for HEVC); ~50-70 ms/frame in the background; ~3.9 KB preview WebP; up to ~23x less per-first-view transfer on the test corpus (far more on real multi-MB clips). Methodology in benches/VIDEO-THUMB.md. Hardening from an adversarial review: video render holds the decode_semaphore like the image path; the ffmpeg scale filter bounds both dimensions; the blob stream has a timeout; the temp file lives on the data volume; the size cap uses saturating_mul. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
e7b85e56e2 |
feat(thumbnails): WebP output with Accept content negotiation
Thumbnails are now generated eagerly as lossy WebP (the primary codec) and
served to clients that advertise `Accept: image/webp`; JPEG is kept as a lazy
fallback for older clients and NextCloud, generated on first request and then
cached like WebP.
- ThumbnailFormat{Webp,Jpeg} enum threaded through encode/render/generate, the
on-disk path ({hash}.webp / {hash}.jpg), the moka cache key
(file_id, size, format), and cleanup (both formats removed).
- file_handler: parse Accept -> format, format-keyed ETag, `Vary: Accept` on
every response (incl. 304) so shared caches never serve the wrong codec;
Content-Type is byte-sniffed (infer) so it always matches the bytes.
- preview_handler (NextCloud) pins JPEG.
- webp = "0.3" (vendored libwebp via cc, no system dependency).
WEBP_QUALITY=82, chosen via a quality sweep (bench Table E1): SSIM within
~0.005 of JPEG q80 (imperceptible at thumbnail scale) for ~62% fewer bytes. On
the photo-realistic bench corpus the full set (3 sizes x 3 photos) drops 65.6%
(213->73 KB); real photos with edges/text land nearer ~25-40%. Encode is +5ms,
paid once in the eager background generator (off the request path).
The bench corpus is now photo-realistic (per-channel sums of low-frequency
sinusoids) instead of white noise, which had distorted codec byte ratios.
Methodology + numbers in benches/WEBP.md.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
778d551090 |
perf(authz): cache resource owner lookups in PgAclEngine
The owner short-circuit in PgAclEngine::check ran a PK query (SELECT user_id FROM storage.folders/files WHERE id=$1) on every authorization check of a folder/file — the common case, since users mostly act on their own resources. Memoise it in an owner_cache (moka, TTL 300s, 100k cap). The owner column is immutable, so this is safe: the cache maps resource -> real owner and can never grant a non-owner access (a different caller's owner==uid test fails against the cached owner and falls through to grants); a hard-deleted resource that briefly resolves to its former owner simply fails later at execution with NotFound. The per-check sql_queries counter now increments only on a miss. Removes 1 DB query + 1 pool-connection acquisition per owner check. Magnitude is deployment-specific (query latency x whether the pool is contended); see benches/ACL-OWNER-CACHE.md. Also adds two DB perf-investigation harnesses, gated behind the `bench` feature (need the dev Postgres; zero prod impact): - examples/bench_db_pool.rs + benches/DB-POOL.md — pool size vs tail latency - examples/bench_owner_cache.rs + benches/ACL-OWNER-CACHE.md — owner query vs cache Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
b7e092ad05 |
perf(thumbnails): SIMD resize via fast_image_resize (PNG 2.6x, RAM 2.5x lower)
Replace the image crate's scalar resampler with fast_image_resize (AVX2/SSE4.1/NEON) in a shared encode_thumbnail() helper. render_all now converts to RGB8 once and SIMD-resizes the shared buffer per size. Lanczos3 for downscaling, CatmullRom when upscaling (Lanczos rings on enlargement). Also folds the duplicated path-variant generate_all_sizes_background into the shared render path -- it had missed BOTH shrink-on-load and SIMD resizing -- so every thumbnail path now goes through one optimised routine (no duplication). Measured on 14 cores vs the post-1.5 state (benches/BASELINE.md): - PNG 2.60x faster (33.6->12.9ms), GIF/WebP 1.25-1.6x: full-resolution decode paths where the resize dominates, so SIMD helps most - JPEG only ~7% (shrink-on-load already shrank the bitmap) but peak heap fell another ~2.5x (17.6->7.1MB): tight RGB buffers, RGB conversion once - quality SSIM 0.986-0.994 at identical dims (>=0.98 gate) Thumbnails are now exactly max_dim on the long side (e.g. 400x266) vs the old fit-within 399x266 -- a <=1px change, invisible under object-fit: cover. Bench example gains an exact-dims quality reference + semaphore throughput table. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
51713b218d |
perf(thumbnails): raise decode-concurrency cap cpus/2 -> cpus
Shrink-on-load decoupled peak heap from source resolution (~18-25 MB per decode regardless of MP), so the RAM ceiling that justified halving decode concurrency is gone. max_concurrent_decodes() now defaults to all cores, with an OXICLOUD_THUMBNAIL_DECODE_CONCURRENCY override for ops tuning. Measured on the real service path (Semaphore + spawn_blocking), 14 cores: - 12MP: 92.7 -> 133.7 photos/s (1.44x) - 24MP: 49.5 -> 69.9 photos/s (1.41x) peak heap unchanged; cpus*2 yields nothing, confirming cpus is the right ceiling for CPU-bound work (the gap to 2x is rayon oversubscription -- Task 1.7). Adds a semaphore-bounded throughput harness (Table D) to the bench example. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
fd5808c157 |
perf(thumbnails): shrink-on-load JPEG decode (1.8-2× faster, 5-15× less RAM)
Decode JPEGs at the smallest DCT scale (1/8·1/4·1/2·1/1) whose long axis is still ≥ the largest needed thumbnail (800px), via jpeg-decoder, instead of a full-resolution decode through the image crate. The full-res bitmap — the dominant time and RAM cost — is never materialised. PNG/GIF/WebP and unusual JPEG colour spaces (CMYK / 16-bit grey) fall back to a full decode. Extracts the shared decode + EXIF-orientation logic into decode_oriented(), removing the duplication that existed between render_thumbnail_from_data and render_all_thumbnails_from_data. Measured on 14 cores (see benches/BASELINE.md): - render_all 1.8-2.0× faster (12MP 111->61ms, 48MP 398->203ms) - peak heap 5.5-14.8× lower, now decoupled from source MP (~18-25MB regardless) - saturated throughput 3-3.6× (parallel efficiency 4.9×->8.5×) - quality SSIM 0.987-0.999 (>=0.98 gate), PSNR 47-55dB Also adds the Phase 0 benchmark harness (gated behind the `bench` feature, zero prod impact): deterministic image corpus (src/bench_support.rs), criterion latency bench (benches/thumbnails.rs), and a peak-RAM/throughput/SSIM harness (examples/bench_thumbnails_mem.rs). Baseline + before/after in benches/BASELINE.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |