perf: fix issues #1,#2,#3,#13 from performance audit

- #1  image_transcode: dedicated rayon pool + moka cache (no tokio blocking)
- #2  ltree materialized paths: eliminate N+1 folder/file queries
- #3  compute_content_hash: read blob_hash column instead of loading file into RAM
- #13 event delegation + DocumentFragment: ~15 delegated listeners replace ~19k per-item listeners

Backend: 82/82 tests pass, cargo check clean.
Frontend: ui.js and app.js syntax-validated via Node.js.
This commit is contained in:
Dionisio
2026-02-16 13:10:44 +01:00
parent e69de1643f
commit b7bd656a43
16 changed files with 949 additions and 905 deletions
-3
View File
@@ -159,9 +159,6 @@ pub trait DedupPort: Send + Sync + 'static {
/// Returns `true` if the blob was deleted (ref_count reached 0).
async fn remove_reference(&self, hash: &str) -> Result<bool, DomainError>;
/// Calculate SHA-256 hash of in-memory content.
fn hash_bytes(&self, content: &[u8]) -> String;
/// Calculate SHA-256 hash of a file (streaming).
async fn hash_file(&self, path: &Path) -> Result<String, DomainError>;
+6
View File
@@ -57,6 +57,12 @@ pub trait FileReadPort: Send + Sync + 'static {
/// Gets the parent folder ID from a path (WebDAV).
async fn get_parent_folder_id(&self, path: &str) -> Result<String, DomainError>;
/// Gets the content-addressable blob hash for a file (O(1) DB lookup).
///
/// Returns the SHA-256 hash stored in `storage.files.blob_hash`.
/// Used for dedup reference tracking without loading file content.
async fn get_blob_hash(&self, file_id: &str) -> Result<String, DomainError>;
/// Find a file by its logical path (folder_name/.../file_name).
///
/// The default implementation falls back to `list_files(None)` + linear
@@ -12,7 +12,7 @@ use tracing::{debug, error, info, warn};
/// Service for file management operations (move, delete).
///
/// The `delete_with_cleanup` method internalises:
/// 1. Content-hash computation for dedup tracking
/// 1. Blob-hash lookup for dedup tracking (O(1) DB read)
/// 2. Trash-first soft-delete
/// 3. Fallback to permanent delete
/// 4. Dedup reference-count decrement
@@ -57,18 +57,22 @@ impl FileManagementService {
// ── private helpers ──────────────────────────────────────────
/// Compute the content hash for dedup tracking. Returns `None` on failure.
async fn compute_content_hash(&self, id: &str) -> Option<String> {
let dedup = self.dedup_service.as_ref()?;
/// Look up the blob hash from the database. Returns `None` when
/// dedup is inactive or the file is not found.
///
/// This is O(1) — a single `SELECT blob_hash` by primary key.
/// It replaces the old `compute_content_hash` which loaded the
/// entire file into RAM just to re-derive the same hash.
async fn lookup_blob_hash(&self, id: &str) -> Option<String> {
self.dedup_service.as_ref()?; // dedup must be active
let file_read = self.file_read.as_ref()?;
match file_read.get_file_content(id).await {
Ok(content) => {
let hash = dedup.hash_bytes(&content);
debug!("🔗 DEDUP: File {} has content hash: {}", id, &hash[..12]);
match file_read.get_blob_hash(id).await {
Ok(hash) => {
info!("🔗 DEDUP: File {} has blob hash: {}", id, &hash[..12]);
Some(hash)
}
Err(e) => {
debug!("Could not read file content for dedup: {}", e);
warn!("Could not get blob hash for dedup: {}", e);
None
}
}
@@ -177,8 +181,8 @@ impl FileManagementUseCase for FileManagementService {
/// Smart delete: trash-first with dedup reference cleanup.
async fn delete_with_cleanup(&self, id: &str, user_id: &str) -> Result<bool, DomainError> {
// Step 1: Compute content hash for dedup tracking
let content_hash = self.compute_content_hash(id).await;
// Step 1: Look up blob hash for dedup tracking (O(1) DB read)
let content_hash = self.lookup_blob_hash(id).await;
// Step 2: Try trash (soft delete)
if let Some(trash) = &self.trash_service {
@@ -479,6 +479,10 @@ mod tests {
async fn get_parent_folder_id(&self, _path: &str) -> Result<String, DomainError> {
unimplemented!()
}
async fn get_blob_hash(&self, _file_id: &str) -> Result<String, DomainError> {
Ok(String::new())
}
}
#[async_trait]
@@ -179,6 +179,10 @@ impl FileReadPort for MockFileRepository {
async fn get_parent_folder_id(&self, _path: &str) -> std::result::Result<String, DomainError> {
unimplemented!()
}
async fn get_blob_hash(&self, _file_id: &str) -> std::result::Result<String, DomainError> {
Ok(String::new())
}
}
#[async_trait]