perf(cache): use Arc<str> for etag/content_type in ContentCachePort
Replace String with Arc<str> for etag and content_type fields in the content cache. String::clone() allocates and copies the full string on every cache hit (O(n)), while Arc<str>::clone() is O(1) — just an atomic ref-count increment. This eliminates 2 heap allocations per cache hit on the hottest download path. At 1000 req/s that is 2000 fewer alloc/dealloc cycles per second. Changed files: - cache_ports.rs: trait signatures String → Arc<str> - file_content_cache.rs: CacheEntry fields, get/put methods, tests - stubs.rs: StubContentCachePort signatures - file_retrieval_service.rs: caller creates Arc<str> before put()
This commit is contained in:
@@ -12,6 +12,7 @@ use crate::common::errors::DomainError;
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
/// Statistics for monitoring write-behind cache status.
|
/// Statistics for monitoring write-behind cache status.
|
||||||
#[derive(Debug, Clone, Default)]
|
#[derive(Debug, Clone, Default)]
|
||||||
@@ -111,10 +112,13 @@ pub trait ContentCachePort: Send + Sync + 'static {
|
|||||||
fn should_cache(&self, size: usize) -> bool;
|
fn should_cache(&self, size: usize) -> bool;
|
||||||
|
|
||||||
/// Get cached content. Returns `(content, etag, content_type)` on hit.
|
/// Get cached content. Returns `(content, etag, content_type)` on hit.
|
||||||
async fn get(&self, file_id: &str) -> Option<(Bytes, String, String)>;
|
///
|
||||||
|
/// `etag` and `content_type` are `Arc<str>` so cloning on cache hit is O(1)
|
||||||
|
/// (atomic ref-count increment) instead of O(n) heap allocation.
|
||||||
|
async fn get(&self, file_id: &str) -> Option<(Bytes, Arc<str>, Arc<str>)>;
|
||||||
|
|
||||||
/// Store content in the cache (may evict older entries).
|
/// Store content in the cache (may evict older entries).
|
||||||
async fn put(&self, file_id: String, content: Bytes, etag: String, content_type: String);
|
async fn put(&self, file_id: String, content: Bytes, etag: Arc<str>, content_type: Arc<str>);
|
||||||
|
|
||||||
/// Remove a file from the cache (e.g. on delete/update).
|
/// Remove a file from the cache (e.g. on delete/update).
|
||||||
async fn invalidate(&self, file_id: &str);
|
async fn invalidate(&self, file_id: &str);
|
||||||
|
|||||||
@@ -197,13 +197,14 @@ impl FileRetrievalService {
|
|||||||
|
|
||||||
// Store in cache
|
// Store in cache
|
||||||
if let Some(cache) = &self.content_cache {
|
if let Some(cache) = &self.content_cache {
|
||||||
let etag = format!("\"{}-{}\"", id, modified_at);
|
let etag: Arc<str> = format!("\"{}-{}\"", id, modified_at).into();
|
||||||
|
let ct: Arc<str> = Arc::from(&*mime_type);
|
||||||
cache
|
cache
|
||||||
.put(
|
.put(
|
||||||
id.to_string(),
|
id.to_string(),
|
||||||
content_bytes.clone(),
|
content_bytes.clone(),
|
||||||
etag,
|
etag,
|
||||||
mime_type.clone(),
|
ct,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -790,11 +790,11 @@ impl ContentCachePort for StubContentCachePort {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get(&self, _file_id: &str) -> Option<(Bytes, String, String)> {
|
async fn get(&self, _file_id: &str) -> Option<(Bytes, Arc<str>, Arc<str>)> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn put(&self, _file_id: String, _content: Bytes, _etag: String, _content_type: String) {}
|
async fn put(&self, _file_id: String, _content: Bytes, _etag: Arc<str>, _content_type: Arc<str>) {}
|
||||||
|
|
||||||
async fn invalidate(&self, _file_id: &str) {}
|
async fn invalidate(&self, _file_id: &str) {}
|
||||||
|
|
||||||
|
|||||||
@@ -40,8 +40,8 @@ impl FileContentCacheConfig {
|
|||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct CacheEntry {
|
struct CacheEntry {
|
||||||
content: Bytes,
|
content: Bytes,
|
||||||
etag: String,
|
etag: Arc<str>,
|
||||||
content_type: String,
|
content_type: Arc<str>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Lock-free concurrent file content cache backed by `moka`.
|
/// Lock-free concurrent file content cache backed by `moka`.
|
||||||
@@ -98,8 +98,9 @@ impl FileContentCache {
|
|||||||
|
|
||||||
/// Get file content from cache (lock-free read)
|
/// Get file content from cache (lock-free read)
|
||||||
///
|
///
|
||||||
/// Returns (content, etag, content_type) if found
|
/// Returns `(content, etag, content_type)` if found.
|
||||||
pub async fn get(&self, file_id: &str) -> Option<(Bytes, String, String)> {
|
/// All three clones are O(1): `Bytes` and `Arc<str>` only bump a ref count.
|
||||||
|
pub async fn get(&self, file_id: &str) -> Option<(Bytes, Arc<str>, Arc<str>)> {
|
||||||
if let Some(entry) = self.cache.get(file_id).await {
|
if let Some(entry) = self.cache.get(file_id).await {
|
||||||
self.hits.fetch_add(1, Ordering::Relaxed);
|
self.hits.fetch_add(1, Ordering::Relaxed);
|
||||||
debug!("Cache HIT for file: {}", file_id);
|
debug!("Cache HIT for file: {}", file_id);
|
||||||
@@ -123,7 +124,7 @@ impl FileContentCache {
|
|||||||
/// Put file content into cache
|
/// Put file content into cache
|
||||||
///
|
///
|
||||||
/// Moka handles eviction automatically based on weight (content size).
|
/// Moka handles eviction automatically based on weight (content size).
|
||||||
pub async fn put(&self, file_id: String, content: Bytes, etag: String, content_type: String) {
|
pub async fn put(&self, file_id: String, content: Bytes, etag: Arc<str>, content_type: Arc<str>) {
|
||||||
let size = content.len();
|
let size = content.len();
|
||||||
|
|
||||||
// Don't cache if too large
|
// Don't cache if too large
|
||||||
@@ -199,11 +200,11 @@ impl ContentCachePort for FileContentCache {
|
|||||||
FileContentCache::should_cache(self, size)
|
FileContentCache::should_cache(self, size)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get(&self, file_id: &str) -> Option<(Bytes, String, String)> {
|
async fn get(&self, file_id: &str) -> Option<(Bytes, Arc<str>, Arc<str>)> {
|
||||||
FileContentCache::get(self, file_id).await
|
FileContentCache::get(self, file_id).await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn put(&self, file_id: String, content: Bytes, etag: String, content_type: String) {
|
async fn put(&self, file_id: String, content: Bytes, etag: Arc<str>, content_type: Arc<str>) {
|
||||||
FileContentCache::put(self, file_id, content, etag, content_type).await
|
FileContentCache::put(self, file_id, content, etag, content_type).await
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -233,8 +234,8 @@ mod tests {
|
|||||||
.put(
|
.put(
|
||||||
"file1".to_string(),
|
"file1".to_string(),
|
||||||
content.clone(),
|
content.clone(),
|
||||||
"etag1".to_string(),
|
"etag1".into(),
|
||||||
"text/plain".to_string(),
|
"text/plain".into(),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
@@ -242,8 +243,8 @@ mod tests {
|
|||||||
assert!(result.is_some());
|
assert!(result.is_some());
|
||||||
let (cached_content, etag, content_type) = result.unwrap();
|
let (cached_content, etag, content_type) = result.unwrap();
|
||||||
assert_eq!(cached_content, content);
|
assert_eq!(cached_content, content);
|
||||||
assert_eq!(etag, "etag1");
|
assert_eq!(&*etag, "etag1");
|
||||||
assert_eq!(content_type, "text/plain");
|
assert_eq!(&*content_type, "text/plain");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -260,8 +261,8 @@ mod tests {
|
|||||||
.put(
|
.put(
|
||||||
"small".to_string(),
|
"small".to_string(),
|
||||||
small,
|
small,
|
||||||
"e1".to_string(),
|
"e1".into(),
|
||||||
"app/bin".to_string(),
|
"app/bin".into(),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
assert!(cache.get("small").await.is_some());
|
assert!(cache.get("small").await.is_some());
|
||||||
@@ -272,8 +273,8 @@ mod tests {
|
|||||||
.put(
|
.put(
|
||||||
"big".to_string(),
|
"big".to_string(),
|
||||||
big,
|
big,
|
||||||
"e2".to_string(),
|
"e2".into(),
|
||||||
"app/bin".to_string(),
|
"app/bin".into(),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
assert!(
|
assert!(
|
||||||
@@ -298,8 +299,8 @@ mod tests {
|
|||||||
.put(
|
.put(
|
||||||
"file1".to_string(),
|
"file1".to_string(),
|
||||||
content,
|
content,
|
||||||
"e".to_string(),
|
"e".into(),
|
||||||
"t".to_string(),
|
"t".into(),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user