# OxiCloud — Deep Performance Analysis > Extreme‑optimization audit of every hot path, allocation pattern, and > concurrency strategy across 22 source files. --- ## Executive Summary OxiCloud is **already well‑architected** for performance: moka lock‑free caches everywhere, BLAKE3 hashing, dedicated rayon pool for image work, ltree GiST indexes for subtree queries, streaming I/O, and zero‑copy `Bytes` clones. The findings below target the **remaining ~15–25 % of allocatable overhead** that separates "good" from "extreme." **Impact tiers:** - 🔴 **High** — measurable latency or throughput regression on every request - 🟡 **Medium** — wasteful but amortised across many requests - 🟢 **Low** — micro‑optimisation, only matters at ≥ 10 k req/s --- ## 1. Avoidable `.clone()` calls ### 🔴 1a. `CurrentUser` cloned on every authenticated request **File:** `src/interfaces/middleware/auth.rs` The middleware extracts a `CurrentUser` (4 owned `String` fields) into Axum's request extensions. Every handler that reads it clones the struct: ```rust // auth.rs — CurrentUser has 4 String fields pub struct CurrentUser { pub id: String, pub username: String, pub email: String, pub role: String, } ``` **Fix:** Replace with `Arc` in request extensions. All downstream handlers receive `Arc::clone()` (8‑byte refcount bump) instead of 4 heap allocations: ```rust request.extensions_mut().insert(Arc::new(current_user)); // handlers: Extension(user): Extension> ``` **Estimated saving:** ~160–320 ns per request (4 × String clone of ~20‑byte UUIDs/emails). --- ### 🟡 1b. `config.clone()` during `CoreServices` construction **File:** `src/common/di.rs` ```rust // di.rs — CoreServices creation let core = CoreServices { config: config.clone(), // AppConfig is large: ~60 fields, many Strings ... }; ``` `AppConfig` contains ~60 fields including nested structs with owned `String`s. This only runs at startup, so impact is negligible — but it leaks into any service that receives `AppConfig` by value instead of `Arc`. **Fix:** Pass `Arc` everywhere. Most services already take `Arc`; unify the remaining call sites. --- ### 🟡 1c. `mime_type.clone()` in file retrieval return paths **File:** `src/application/services/file_retrieval_service.rs` ```rust // file_retrieval_service.rs — return path Ok(FileContentDto { content, mime_type: mime_type.clone(), // repeated in match arms ... }) ``` Mime type strings are typically < 30 bytes (`"image/jpeg"`) so each clone is cheap, but this happens per‑download. Using `Arc` or keeping the MIME as `&'static str` (from a lookup table of the ~30 common types) would eliminate the allocation entirely. --- ### 🟡 1d. `file.clone()` in search suggest **File:** `src/application/services/search_service.rs` ```rust // search_service.rs — suggest() results.iter().map(|file| { FileDto::from(file.clone()) // full File entity clone per suggestion }).collect() ``` **Fix:** `FileDto::from(&file)` — take by reference, build DTO fields directly. --- ### 🟡 1e. `target_folder.map(|s| s.to_string())` in batch operations **File:** `src/application/services/batch_operations.rs` ```rust // batch_operations.rs — copy_files/move_files let target_folder: Option> = target_folder_id.map(|s| Arc::from(s.as_str())); // ...per-item: target_folder.map(|s| s.to_string()) // re-allocates a String from Arc per item ``` `Arc` is correctly used to avoid N clones, but the inner closure converts it back to `String` on each iteration — allocating N identical Strings. **Fix:** Accept `Option<&str>` in the downstream service method, or if it requires `String`, store `Arc` and call `.as_ref()`. --- ## 2. String allocations replaceable by `&str` / `Cow` / `&'static str` ### 🔴 2a. `DomainError` allocates on every construction **File:** `src/domain/errors.rs` ```rust // errors.rs pub struct DomainError { pub entity_id: Option, // heap alloc pub message: String, // heap alloc pub source: Option>, // heap alloc ... } pub fn not_found(entity_type: &'static str, id: &str) -> Self { Self { entity_id: Some(id.to_string()), // alloc message: format!("{} not found", entity_type), // alloc + format ... } } ``` Error paths are not usually "hot," but in OxiCloud many operations pattern- match on errors to decide control flow (e.g. trash service checks `"not found"` in error messages via string matching): ```rust // trash_service.rs if format!("{}", e).contains("not found") { ... } ``` This is both a performance issue (formatting the error + string search) and a correctness risk. The `ErrorKind` enum already exists — use it: ```rust if matches!(e.kind, ErrorKind::NotFound) { ... } ``` **Fix for DomainError allocations:** - Use `Cow<'static, str>` for `message` (most messages are literals) - Use `Cow<'_, str>` for `entity_id` (most IDs are passed as `&str`) - Only allocate when the error crosses an async boundary --- ### 🔴 2b. `compute_relevance` allocates per result **File:** `src/application/services/search_service.rs` ```rust // search_service.rs fn compute_relevance(name: &str, query: &str) -> f64 { let name_lower = name.to_lowercase(); // alloc let query_lower = query.to_lowercase(); // alloc (same query, every iteration!) ... } ``` For a search returning 100 results, this creates 200 temporary `String`s. **Fix:** Pre-lowercase the query once before the loop; for file names use `eq_ignore_ascii_case` / `to_ascii_lowercase` (in-place capable) or `unicase::UniCase`. --- ### 🟡 2c. `enrich_file`/`enrich_folder` in search service **File:** `src/application/services/search_service.rs` ```rust // search_service.rs — enrich_file per result enriched.formatted_size = format_bytes(file.size as u64); // format!() alloc enriched.icon_class = get_icon_class(&file.mime_type); // returns String enriched.icon_special_class = get_icon_special_class(&file.mime_type); // String enriched.category = get_category(&file.mime_type); // String ``` 4 × String allocation per search result. If `get_icon_class` etc. return from a fixed set, they should return `&'static str`. --- ### 🟡 2d. `target_format.mime_type().to_string()` in transcode service **File:** `src/infrastructure/services/image_transcode_service.rs` ```rust // image_transcode_service.rs Ok((transcoded, target_format.mime_type().to_string(), true)) // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ // mime_type() returns &'static str ("image/webp"), .to_string() allocates ``` **Fix:** Change return type to `&'static str` or `Cow<'static, str>`. --- ### 🟡 2e. `file_id.to_string()` in cache key construction (thumbnails) **File:** `src/infrastructure/services/thumbnail_service.rs` ```rust let cache_key = ThumbnailCacheKey { file_id: file_id.to_string(), // alloc on each lookup size: *size, }; ``` Moka's `get()` takes `&K` and hashes it. If `file_id` is already a `String`, this clone is unnecessary — store `Arc` as key or borrow via `Borrow` trait. --- ## 3. Vec allocations ### 🟢 3a. Generally well pre‑sized Most `Vec` allocations use `with_capacity()` or rely on `collect()` from known-size iterators. **No major issues found.** Notable good patterns: ```rust // zip_service.rs let mut files_by_folder: HashMap> = HashMap::with_capacity(all_folders.len()); // batch_operations.rs — uses buffer_unordered, no Vec needed ``` ### 🟡 3b. `BatchResult` vectors not pre-sized ```rust // batch_operations.rs let mut result = BatchResult { successful: Vec::new(), // could be Vec::with_capacity(total) failed: Vec::new(), ... }; ``` Minor: `Vec::with_capacity(file_ids.len())` for `successful` avoids reallocations when most operations succeed. --- ## 4. Blocking operations inside async contexts ### 🔴 4a. `std::env::var()` on every request in rate limiter **File:** `src/interfaces/middleware/rate_limit.rs` ```rust // rate_limit.rs — extract_client_ip() fn extract_client_ip(req: &Request) -> String { let trust_proxy = std::env::var("OXICLOUD_TRUST_PROXY_HEADERS") .unwrap_or_default(); // BLOCKING SYSCALL per request ... } ``` `std::env::var()` takes a global lock on glibc's environ and is a blocking syscall. Called on **every single HTTP request**. **Fix:** Read the env var once at startup into `AppConfig` (it already exists there as `trust_proxy_headers: bool`). Pass the config to the middleware: ```rust fn extract_client_ip(req: &Request, trust_proxy: bool) -> String { ... } ``` --- ### 🟡 4b. `ip.to_string()` called twice in rate limiter **File:** `src/interfaces/middleware/rate_limit.rs` ```rust // rate_limit.rs — check_and_increment pub fn check_and_increment(&self, ip: &str) -> bool { let current = self.requests.get(ip); // hashes ip — String lookup OK // ... later: self.requests.insert(ip.to_string(), ...); // re-allocates String for key } ``` The `ip` is already a `String` at the call site (`ip.to_string()` in `extract_client_ip`). This means 2 allocations of the same IP string per request. **Fix:** Take `ip: String` by value, reuse it for insertion. --- ### 🟡 4c. `moka::sync::Cache` in JWT service (sync ops on async path) **File:** `src/infrastructure/services/jwt_service.rs` ```rust // jwt_service.rs validation_cache: moka::sync::Cache, ``` `moka::sync::Cache` performs eviction inline (not background). On hot paths this can occasionally block the Tokio thread for µs during eviction scans. For the JWT cache (50k entries, 30s TTL) this is borderline. **Fix:** Switch to `moka::future::Cache` which performs eviction in a background async task, or keep `sync` but call `run_pending_tasks()` from a periodic maintenance future. --- ## 5. HashMap hasher opportunities ### 🟡 5a. `DefaultHasher` in search cache key **File:** `src/application/services/search_service.rs` ```rust // search_service.rs use std::collections::hash_map::DefaultHasher; use std::hash::{Hash, Hasher}; fn cache_key(folder_id: Option<&str>, query: &str, ...) -> u64 { let mut hasher = DefaultHasher::new(); // SipHash-2-4 ... hasher.finish() } ``` SipHash provides HashDoS resistance which is unnecessary for an internal cache key derived from trusted inputs. Switching to `ahash::AHasher` or `fxhash` saves ~5 ns per hash (relevant when search results are cached aggressively). --- ### 🟢 5b. Moka caches use their own optimised hasher Moka internally uses a fast hasher. No action needed for moka-backed caches. --- ## 6. Lock contention patterns ### 🟢 Mostly eliminated The codebase **correctly** uses: - `moka` (lock-free segmented map) for all caches - `tokio::sync::Semaphore` for bounded concurrency (Argon2, thumbnail decode) - `AtomicU64` for hit/miss counters - No `RwLock>` patterns **One minor note:** The Argon2 semaphore is set to `MAX_CONCURRENT_HASHES = 2`: ```rust // share_service.rs const MAX_CONCURRENT_HASHES: usize = 2; let hash_semaphore = Arc::new(Semaphore::new(MAX_CONCURRENT_HASHES)); ``` This is correct for memory safety (~19 MB/hash) but could be a throughput bottleneck if many users set/verify share passwords concurrently. Consider making this configurable. --- ## 7. Unnecessary serialization/deserialization ### 🟢 No major issues found DTOs are converted with hand-written `from_entity()` and `From` impls, not round-tripped through serde. The only serde usage is at the HTTP boundary (axum's `Json`) which is unavoidable and correct. --- ## 8. Memory copies that could be zero‑copy ### 🟡 8a. File upload hashes in‑memory content after writing to disk **File:** `src/application/services/file_upload_service.rs` ```rust // file_upload_service.rs — create_file let hash = blake3::hash(content); // hashes full &[u8] in memory // content is also written to temp file... ``` For files that fit in memory (the `content: &[u8]` path), the content exists as a slice and is hashed directly — this is fine. But the same content is then written to a temp file for dedup, meaning the data is traversed twice (hash + write). **Fix:** Use `blake3::Hasher` as an `io::Write` adapter — hash while writing to disk in a single pass: ```rust let mut hasher = blake3::Hasher::new(); let mut file = File::create(&temp_path)?; let mut tee = TeeWriter::new(&mut file, &mut hasher); tee.write_all(content)?; let hash = hasher.finalize(); ``` --- ### 🟡 8b. File retrieval accumulates stream into `BytesMut` for cache **File:** `src/application/services/file_retrieval_service.rs` ```rust // file_retrieval_service.rs — cache miss for files < 10MB let mut buf = BytesMut::new(); while let Some(chunk) = stream.next().await { buf.extend_from_slice(&chunk?); } let content = buf.freeze(); // Bytes (O(1) clone) ``` This is the expected pattern for building a `Bytes` from a stream. The `BytesMut` will reallocate as it grows. Pre-sizing from the known file size would avoid reallocations: ```rust let mut buf = BytesMut::with_capacity(file.size as usize); ``` --- ## 9. Database query patterns ### 🟢 9a. No N+1 queries found All multi-entity operations use: - JOINs (`get_file` joins `storage.files` with `storage.blobs`) - `COUNT(*) OVER()` window functions for paginated counts (single query) - ltree `<@` for subtree operations (single indexed scan) - Bulk SQL (`DELETE ... WHERE folder_id IN (SELECT ...)` for trash/delete) - CTEs for atomic read-modify (`swap_blob_hash`, `copy_file`) This is excellently designed. --- ### 🟡 9b. Dynamic SQL building in search (not prepared) **File:** `src/infrastructure/repositories/pg/file_blob_read_repository.rs` ```rust // file_blob_read_repository.rs — search_files_paginated let mut sql = String::with_capacity(512); sql.push_str("SELECT ... FROM storage.files f JOIN storage.blobs b ..."); if let Some(_) = criteria.name_contains { sql.push_str(" AND f.name ILIKE ..."); } if let Some(_) = criteria.mime_type { sql.push_str(" AND f.mime_type = ..."); } // ... etc ``` Dynamic SQL cannot benefit from PostgreSQL's prepared statement cache (each unique SQL text is parsed/planned separately). For the ~8 common combinations, consider pre-building the queries or using PG's `PREPARE`/`EXECUTE`. --- ### 🟡 9c. `hash_cache` uses `String` keys **File:** `src/infrastructure/repositories/pg/file_blob_read_repository.rs` ```rust // file_blob_read_repository.rs hash_cache: Cache, // file_id → blob_hash ``` Both file IDs and blob hashes are UUIDs/hex strings (~36 bytes). Using `Arc` or a 128-bit UUID type as key would reduce per-entry heap overhead. --- ## 10. Inefficient iteration patterns ### 🟡 10a. Search results: map then sort (two passes) **File:** `src/application/services/search_service.rs` ```rust // search_service.rs let enriched: Vec<_> = results.iter().map(|f| enrich_file(f, query)).collect(); enriched.sort_by(|a, b| b.relevance.total_cmp(&a.relevance)); ``` Two passes: one to enrich (allocating N `EnrichedFileDto`s), another to sort. Could be combined into a single pass that computes relevance inline and uses `sort_unstable_by` (avoids allocation for equal-comparison temporaries): ```rust let mut enriched: Vec<_> = results.iter().map(|f| enrich_file(f, query)).collect(); enriched.sort_unstable_by(|a, b| b.relevance.total_cmp(&a.relevance)); ``` `sort_unstable_by` is ~20% faster than `sort_by` for non-trivial N. --- ### 🟡 10b. `Uuid::parse_str` called multiple times per operation (trash) **File:** `src/application/services/trash_service.rs` ```rust // trash_service.rs — restore_from_trash let trash_uuid = Uuid::parse_str(trash_id)?; let user_uuid = Uuid::parse_str(user_id)?; // ... later in delete_permanently, same two parse calls ``` UUIDs are parsed from `&str` in every trash method. If the caller already has validated UUIDs (e.g., from the auth middleware), accept `Uuid` directly to skip re-parsing. --- ### 🟡 10c. `generic_batch_operation` clones every item for error reporting **File:** `src/application/services/batch_operations.rs` ```rust // batch_operations.rs items.into_iter().map(|item| { let op = operation.clone(); async move { let op_result = op(item.clone()).await; // clone just for the error arm (item, op_result) } }) ``` `item.clone()` is only needed if the operation fails (to report which item failed). For success paths this is wasted work. Consider using an index-based approach or `Arc`. --- ## 11. Dynamic dispatch in hot paths ### 🟡 11a. `Arc` in `ApplicationServices` **File:** `src/common/di.rs` ```rust pub struct ApplicationServices { pub file_use_case_factory: Arc, ... } ``` Every file operation goes through a `dyn` trait dispatch. The vtable indirect call costs ~2 ns but — more importantly — prevents inlining and LTO across the boundary. Since there is only one concrete implementation, using a concrete type wrapped in `Arc` would allow the compiler to devirtualise and inline. --- ### 🟡 11b. `Box` in every `DomainError` **File:** `src/domain/errors.rs` ```rust pub source: Option>, ``` Every error with a source allocates a `Box`. In hot error paths (e.g., "file not found" during cache-miss-then-load), this adds ~30 ns of heap allocation. **Fix:** Use a concrete error enum or `anyhow::Error` (which uses a thin pointer and avoids the double indirection). --- ## 12. Additional findings ### 🔴 12a. `format!("{}", e).contains("not found")` for error matching **File:** `src/application/services/trash_service.rs` ```rust // trash_service.rs Err(e) => { if format!("{}", e).contains("not found") { ... } } ``` This allocates a `String`, formats the error into it, then does a substring search. Happens on every trash restore/delete for missing items. The `DomainError` already has `ErrorKind::NotFound`: ```rust if matches!(e.kind(), ErrorKind::NotFound) { ... } ``` --- ### 🟡 12b. Excessive `info!()` logging in trash service **File:** `src/application/services/trash_service.rs` The trash service has **14 `info!()` calls** per single `restore_from_trash` operation and **12** per `delete_permanently`. Each `info!` allocates `format_args!` and traverses the tracing subscriber pipeline. **Fix:** Downgrade most to `debug!()` or `trace!()`. Keep one `info!` at the entry point and one at the exit. --- ### 🟡 12c. `AppConfig::from_env()` reads ~60 env vars sequentially **File:** `src/common/config.rs` Each `std::env::var()` call acquires a global lock. At startup this is fine, but if this function were ever called more than once it would be a bottleneck. Currently only called once — **no action needed** unless hot-reloading is added. --- ### 🟢 12d. `BatchOperationService` takes `AppConfig` by value **File:** `src/application/services/batch_operations.rs` ```rust pub struct BatchOperationService { config: AppConfig, // owned, not Arc ... } ``` At construction, the entire `AppConfig` is cloned. Since this happens once at startup, impact is negligible, but it's inconsistent with other services that use `Arc`. --- ## Summary table | # | Finding | Severity | Per-request cost | Fix complexity | |---|---------|----------|-----------------|----------------| | 1a | `CurrentUser` clone per request | 🔴 High | ~200 ns | Low | | 2a | `DomainError` heap allocs | 🔴 High | ~60 ns × errors | Medium | | 2b | `compute_relevance` double lowercase | 🔴 High | ~2 µs × N results | Low | | 4a | `std::env::var()` per request | 🔴 High | ~500 ns | Low | | 12a | `format!().contains()` error matching | 🔴 High | ~200 ns | Low | | 1c | `mime_type.clone()` in retrieval | 🟡 Medium | ~30 ns | Low | | 1d | `file.clone()` in search suggest | 🟡 Medium | ~100 ns × N | Low | | 1e | `Arc` → `String` in batch ops | 🟡 Medium | ~30 ns × N items | Low | | 2c | `enrich_file` 4× String allocs | 🟡 Medium | ~120 ns × N | Medium | | 2d | `.to_string()` on `&'static str` | 🟡 Medium | ~15 ns | Low | | 2e | `file_id.to_string()` thumbnail key | 🟡 Medium | ~15 ns | Low | | 4b | IP string double-alloc in rate limiter | 🟡 Medium | ~30 ns | Low | | 4c | `moka::sync::Cache` in JWT service | 🟡 Medium | occasional µs | Medium | | 5a | SipHash for search cache key | 🟡 Medium | ~5 ns | Low | | 8a | Double-traverse in upload hash | 🟡 Medium | ~ms for large files | Medium | | 8b | `BytesMut` not pre-sized | 🟡 Medium | reallocations | Low | | 9b | Dynamic SQL not prepared | 🟡 Medium | ~50 µs parse | High | | 10a | `sort_by` → `sort_unstable_by` | 🟡 Medium | ~20% slower sort | Low | | 10b | Repeated `Uuid::parse_str` | 🟡 Medium | ~50 ns × calls | Low | | 10c | `item.clone()` in generic batch | 🟡 Medium | varies | Medium | | 11a | `dyn FileUseCaseFactory` | 🟡 Medium | ~2 ns + no inline | Medium | | 11b | `Box` per error | 🟡 Medium | ~30 ns | High | | 12b | 14× `info!()` in trash restore | 🟡 Medium | ~1 µs total | Low | | 3b | `BatchResult` vecs not pre-sized | 🟢 Low | rare realloc | Low | --- ## Recommended priority order 1. **`std::env::var()` in rate limiter** (4a) — 5-minute fix, blocks every request 2. **`CurrentUser` → `Arc`** (1a) — 30-minute refactor 3. **`format!().contains()` → `ErrorKind` match** (12a) — 15-minute fix 4. **Pre-lowercase query in search** (2b) — 10-minute fix 5. **`DomainError` use `Cow`** (2a) — 2-hour refactor, touches many files 6. **`BytesMut::with_capacity`** (8b) — 1-line fix 7. **Return `&'static str` from icon/mime helpers** (2c, 2d) — 30-minute refactor 8. **IP string reuse in rate limiter** (4b) — 10-minute fix 9. **`sort_unstable_by` in search** (10a) — 1-line fix 10. **Remaining items** — diminishing returns, schedule as convenient --- ## What's already excellent The following patterns demonstrate strong performance engineering: - **Moka lock-free caches** everywhere (file content, JWT, search, thumbnails, transcode, blob hash) — no `RwLock` anywhere - **BLAKE3** for content-addressable hashing (~5× faster than SHA-256) with `update_mmap_rayon` for large files - **Dedicated rayon thread pool** for image transcoding (isolated from Tokio's blocking pool) - **Streaming I/O** for file downloads (64 KB chunks), ZIP creation (256 KB buffer), and database cursors - **ltree GiST indexes** for O(log N) subtree operations - **`COUNT(*) OVER()`** window functions — single query for paginated results + total count - **Content-addressable dedup** with write-first strategy and atomic blob reference counting - **`HEX_PREFIXES`** compile-time lookup table avoiding `format!()` in dedup hot path - **Semaphore-bounded** Argon2 hashing (memory safety) and image decode (back-pressure) - **`Arc`** usage in batch operations for shared string references - **CTE-based atomic operations** (`swap_blob_hash`, `copy_file`) — zero round-trip waste - **PG triggers** for `ref_count` management — no Rust-side bookkeeping overhead