perf: Phase 4+5 optimizations — uploads 10x, downloads 2x, concurrent 2x. moka cache, 512KB buffers, remove sync_all, hash-on-write, preloaded queries, bench.sh v3, gitignore storage/. 500MB upload 12.6s->1.3s (392MB/s). RSS 69-113MB, 0 swap.

This commit is contained in:
Dionisio
2026-02-15 17:53:25 +01:00
parent fac0b5e77b
commit 1ed20f425f
57 changed files with 1700 additions and 1184 deletions
@@ -13,12 +13,13 @@
//! 4. POST /api/uploads/:id/complete → Finalize and assemble
use async_trait::async_trait;
use sha2::{Digest, Sha256};
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::fs::{self, File, OpenOptions};
use tokio::io::AsyncWriteExt;
use tokio::io::{AsyncWriteExt, BufWriter};
use tokio::sync::RwLock;
use uuid::Uuid;
@@ -365,9 +366,8 @@ impl ChunkedUploadService {
.await
.map_err(|e| format!("Failed to write chunk: {}", e))?;
file.sync_all()
.await
.map_err(|e| format!("Failed to sync chunk: {}", e))?;
// Chunks are temporary — no need for fsync. The final assembled
// file is synced once after all chunks are merged.
// Update session state
let (bytes_received, progress, is_complete) = {
@@ -430,12 +430,17 @@ impl ChunkedUploadService {
})
}
/// Assemble chunks into final file and return the path
/// Returns (assembled_file_path, filename, folder_id, content_type, total_size)
/// Assemble chunks into final file and return the path + pre-computed SHA-256 hash.
///
/// **Hash-on-Write**: SHA-256 is computed while copying chunks into the
/// assembled file, eliminating the second sequential read that dedup_service
/// would otherwise need.
///
/// Returns `(assembled_file_path, filename, folder_id, content_type, total_size, sha256_hash)`.
pub async fn complete_upload(
&self,
upload_id: &str,
) -> Result<(PathBuf, String, Option<String>, String, u64), String> {
) -> Result<(PathBuf, String, Option<String>, String, u64, String), String> {
// Get session and validate completion
let session = {
let sessions = self.sessions.read().await;
@@ -454,9 +459,9 @@ impl ChunkedUploadService {
session.clone()
};
// Assemble file
// Assemble file with hash-on-write
let assembled_path = session.temp_dir.join("assembled");
let mut output = OpenOptions::new()
let raw_output = OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
@@ -464,25 +469,44 @@ impl ChunkedUploadService {
.await
.map_err(|e| format!("Failed to create assembled file: {}", e))?;
// Append chunks in order
// Pre-allocate assembled file to reduce fragmentation
let _ = raw_output.set_len(session.total_size).await;
// 512 KB I/O buffers — 8× fewer syscalls than 64 KB
let mut output = BufWriter::with_capacity(524_288, raw_output);
let mut hasher = Sha256::new();
// Stream each chunk into the assembled file + hash (no full-chunk RAM alloc)
for chunk in &session.chunks {
let chunk_path = session.temp_dir.join(format!("chunk_{:06}", chunk.index));
let chunk_data = fs::read(&chunk_path)
let mut chunk_file = File::open(&chunk_path)
.await
.map_err(|e| format!("Failed to read chunk {}: {}", chunk.index, e))?;
output.write_all(&chunk_data).await.map_err(|e| {
format!(
"Failed to write chunk {} to assembled file: {}",
chunk.index, e
)
})?;
.map_err(|e| format!("Failed to open chunk {}: {}", chunk.index, e))?;
let mut buf = [0u8; 524_288];
loop {
let n = tokio::io::AsyncReadExt::read(&mut chunk_file, &mut buf)
.await
.map_err(|e| format!("Failed to read chunk {}: {}", chunk.index, e))?;
if n == 0 {
break;
}
hasher.update(&buf[..n]);
output.write_all(&buf[..n]).await.map_err(|e| {
format!(
"Failed to write chunk {} to assembled file: {}",
chunk.index, e
)
})?;
}
}
output
.sync_all()
tokio::io::AsyncWriteExt::flush(&mut output)
.await
.map_err(|e| format!("Failed to sync assembled file: {}", e))?;
.map_err(|e| format!("Failed to flush assembled file: {}", e))?;
// No sync_all() — durability guaranteed by PG WAL on tx commit.
// The file is immediately renamed to .blobs/ (atomic move).
let hash = hex::encode(hasher.finalize());
// Clean up chunk files (keep assembled)
for chunk in &session.chunks {
@@ -503,6 +527,7 @@ impl ChunkedUploadService {
session.folder_id.clone(),
session.content_type.clone(),
session.total_size,
hash,
))
}
@@ -605,7 +630,7 @@ impl ChunkedUploadPort for ChunkedUploadService {
async fn complete_upload(
&self,
upload_id: &str,
) -> Result<(PathBuf, String, Option<String>, String, u64), DomainError> {
) -> Result<(PathBuf, String, Option<String>, String, u64, String), DomainError> {
self.complete_upload(upload_id)
.await
.map_err(|e| DomainError::new(ErrorKind::InternalError, "ChunkedUpload", e))