From 3c96f12f740dafc48384b7cde24d97093f7e7f3b Mon Sep 17 00:00:00 2001 From: xfy Date: Thu, 30 Apr 2026 16:13:42 +0800 Subject: [PATCH] feat(cache): store ContentType in FileEntry for cache hits - Add ContentType field to FileEntry struct - Update Set method signature to accept contentType parameter - Use cached ContentType in static.go cache hit branches - Update all test files to use new Set signature This avoids redundant MIME type detection on cache hits, reducing lock contention in mimeutil.DetectContentType. Co-Authored-By: Claude Opus 4.7 --- internal/cache/cache_bench_test.go | 24 ++++++++-------- internal/cache/cache_test.go | 30 ++++++++++---------- internal/cache/file_cache.go | 23 +++++++++------ internal/cache/file_cache_allocation_test.go | 24 ++++++++-------- internal/handler/static.go | 13 +++++---- 5 files changed, 60 insertions(+), 54 deletions(-) diff --git a/internal/cache/cache_bench_test.go b/internal/cache/cache_bench_test.go index 60f1ad0..aca85b0 100644 --- a/internal/cache/cache_bench_test.go +++ b/internal/cache/cache_bench_test.go @@ -35,7 +35,7 @@ func BenchmarkFileCacheGet(b *testing.B) { for i := range size { path := fmt.Sprintf("/file%d.txt", i) data := []byte("cached data content") - _ = fc.Set(path, data, int64(len(data)), time.Now()) + _ = fc.Set(path, data, int64(len(data)), time.Now(), "text/plain") } b.ResetTimer() @@ -65,14 +65,14 @@ func BenchmarkFileCacheSet(b *testing.B) { for i := range size { path := fmt.Sprintf("/file%d.txt", i) data := []byte("cached data content") - _ = fc.Set(path, data, int64(len(data)), time.Now()) + _ = fc.Set(path, data, int64(len(data)), time.Now(), "text/plain") } b.ResetTimer() for i := 0; b.Loop(); i++ { path := fmt.Sprintf("/newfile%d.txt", i) data := []byte("new cached data content") - _ = fc.Set(path, data, int64(len(data)), time.Now()) + _ = fc.Set(path, data, int64(len(data)), time.Now(), "text/plain") } }) } @@ -91,7 +91,7 @@ func BenchmarkFileCacheSet_Pooled(b *testing.B) { for i := range size { path := fmt.Sprintf("/file%d.txt", i) data := []byte("cached data content") - _ = fc.Set(path, data, int64(len(data)), time.Now()) + _ = fc.Set(path, data, int64(len(data)), time.Now(), "text/plain") } b.ReportAllocs() @@ -99,7 +99,7 @@ func BenchmarkFileCacheSet_Pooled(b *testing.B) { for i := 0; b.Loop(); i++ { path := fmt.Sprintf("/newfile%d.txt", i) data := []byte("new cached data content") - _ = fc.Set(path, data, int64(len(data)), time.Now()) + _ = fc.Set(path, data, int64(len(data)), time.Now(), "text/plain") } }) } @@ -114,7 +114,7 @@ func BenchmarkFileCacheSetNoEviction(b *testing.B) { for i := 0; b.Loop(); i++ { path := fmt.Sprintf("/file%d.txt", i) data := []byte("cached data content") - _ = fc.Set(path, data, int64(len(data)), time.Now()) + _ = fc.Set(path, data, int64(len(data)), time.Now(), "text/plain") } } @@ -131,7 +131,7 @@ func BenchmarkFileCacheConcurrent(b *testing.B) { for i := range size { path := fmt.Sprintf("/file%d.txt", i) data := []byte("cached data content") - _ = fc.Set(path, data, int64(len(data)), time.Now()) + _ = fc.Set(path, data, int64(len(data)), time.Now(), "text/plain") } b.ResetTimer() @@ -142,7 +142,7 @@ func BenchmarkFileCacheConcurrent(b *testing.B) { if i%10 == 0 { path := fmt.Sprintf("/newfile%d.txt", i) data := []byte("updated data content") - _ = fc.Set(path, data, int64(len(data)), time.Now()) + _ = fc.Set(path, data, int64(len(data)), time.Now(), "text/plain") } else { path := fmt.Sprintf("/file%d.txt", i%size) fc.Get(path) @@ -163,7 +163,7 @@ func BenchmarkFileCacheGetOnly(b *testing.B) { for i := range 1000 { path := fmt.Sprintf("/static/file%d.css", i) data := make([]byte, 1024) // 1KB 数据 - _ = fc.Set(path, data, int64(len(data)), time.Now()) + _ = fc.Set(path, data, int64(len(data)), time.Now(), "text/plain") } b.ResetTimer() @@ -188,14 +188,14 @@ func BenchmarkFileCacheSizeEviction(b *testing.B) { data := make([]byte, 1024) // 1KB 每条 for i := range 1000 { path := fmt.Sprintf("/file%d.txt", i) - _ = fc.Set(path, data, int64(len(data)), time.Now()) + _ = fc.Set(path, data, int64(len(data)), time.Now(), "text/plain") } b.ResetTimer() for i := 0; b.Loop(); i++ { path := fmt.Sprintf("/newfile%d.txt", i) newData := make([]byte, 1024) - _ = fc.Set(path, newData, int64(len(newData)), time.Now()) + _ = fc.Set(path, newData, int64(len(newData)), time.Now(), "text/plain") } } @@ -208,7 +208,7 @@ func BenchmarkFileCacheLRUTouch(b *testing.B) { for i := range 100 { path := fmt.Sprintf("/file%d.txt", i) data := []byte("cached data") - _ = fc.Set(path, data, int64(len(data)), time.Now()) + _ = fc.Set(path, data, int64(len(data)), time.Now(), "text/plain") } b.ResetTimer() diff --git a/internal/cache/cache_test.go b/internal/cache/cache_test.go index e0643b5..a8b2017 100644 --- a/internal/cache/cache_test.go +++ b/internal/cache/cache_test.go @@ -36,7 +36,7 @@ func TestFileCacheSetGet(t *testing.T) { path := "/test/file.txt" data := []byte("Hello, World!") - err := fc.Set(path, data, int64(len(data)), time.Now()) + err := fc.Set(path, data, int64(len(data)), time.Now(), "text/plain") if err != nil { t.Errorf("Set() error: %v", err) } @@ -53,7 +53,7 @@ func TestFileCacheSetGet(t *testing.T) { func TestFileCacheDelete(t *testing.T) { fc := NewFileCache(10, 1024, 1*time.Hour) - _ = fc.Set("/test.txt", []byte("data"), 4, time.Now()) + _ = fc.Set("/test.txt", []byte("data"), 4, time.Now(), "text/plain") fc.Delete("/test.txt") @@ -67,12 +67,12 @@ func TestFileCacheLRUEviction(t *testing.T) { // 最大 3 个条目 fc := NewFileCache(3, 0, 1*time.Hour) - _ = fc.Set("/a", []byte("a"), 1, time.Now()) - _ = fc.Set("/b", []byte("b"), 1, time.Now()) - _ = fc.Set("/c", []byte("c"), 1, time.Now()) + _ = fc.Set("/a", []byte("a"), 1, time.Now(), "text/plain") + _ = fc.Set("/b", []byte("b"), 1, time.Now(), "text/plain") + _ = fc.Set("/c", []byte("c"), 1, time.Now(), "text/plain") // 再添加一个,应该淘汰 /a - _ = fc.Set("/d", []byte("d"), 1, time.Now()) + _ = fc.Set("/d", []byte("d"), 1, time.Now(), "text/plain") _, ok := fc.Get("/a") if ok { @@ -216,11 +216,11 @@ func TestFileCacheSizeEviction(t *testing.T) { // 最大 10 字节 fc := NewFileCache(0, 10, 1*time.Hour) - _ = fc.Set("/a", []byte("12345"), 5, time.Now()) - _ = fc.Set("/b", []byte("12345"), 5, time.Now()) + _ = fc.Set("/a", []byte("12345"), 5, time.Now(), "text/plain") + _ = fc.Set("/b", []byte("12345"), 5, time.Now(), "text/plain") // 再添加 6 字节,应该淘汰一个 - _ = fc.Set("/c", []byte("123456"), 6, time.Now()) + _ = fc.Set("/c", []byte("123456"), 6, time.Now(), "text/plain") stats := fc.Stats() if stats.Size > 10 { @@ -231,7 +231,7 @@ func TestFileCacheSizeEviction(t *testing.T) { func TestFileCacheInactiveEviction(t *testing.T) { fc := NewFileCache(10, 1024, 100*time.Millisecond) - _ = fc.Set("/test", []byte("data"), 4, time.Now()) + _ = fc.Set("/test", []byte("data"), 4, time.Now(), "text/plain") // 立即获取应该成功 _, ok := fc.Get("/test") @@ -252,8 +252,8 @@ func TestFileCacheInactiveEviction(t *testing.T) { func TestFileCacheClear(t *testing.T) { fc := NewFileCache(10, 1024, 1*time.Hour) - _ = fc.Set("/a", []byte("a"), 1, time.Now()) - _ = fc.Set("/b", []byte("b"), 1, time.Now()) + _ = fc.Set("/a", []byte("a"), 1, time.Now(), "text/plain") + _ = fc.Set("/b", []byte("b"), 1, time.Now(), "text/plain") fc.Clear() @@ -266,8 +266,8 @@ func TestFileCacheClear(t *testing.T) { func TestFileCacheStats(t *testing.T) { fc := NewFileCache(100, 1024, 1*time.Hour) - _ = fc.Set("/a", []byte("12345"), 5, time.Now()) - _ = fc.Set("/b", []byte("12345"), 5, time.Now()) + _ = fc.Set("/a", []byte("12345"), 5, time.Now(), "text/plain") + _ = fc.Set("/b", []byte("12345"), 5, time.Now(), "text/plain") stats := fc.Stats() if stats.Entries != 2 { @@ -569,7 +569,7 @@ func TestFileCacheRefreshCachedAt(t *testing.T) { data := []byte("test data") // 设置缓存 - _ = fc.Set(path, data, int64(len(data)), time.Now()) + _ = fc.Set(path, data, int64(len(data)), time.Now(), "text/plain") // 获取原始 CachedAt 时间 entry, ok := fc.Get(path) diff --git a/internal/cache/file_cache.go b/internal/cache/file_cache.go index ca21cbd..1494b5f 100644 --- a/internal/cache/file_cache.go +++ b/internal/cache/file_cache.go @@ -29,14 +29,15 @@ import ( // FileEntry 文件缓存条目,存储单个文件的缓存信息。 type FileEntry struct { - ModTime time.Time - CachedAt time.Time // 缓存时间,用于 TTL 验证(新鲜度) - LastAccess time.Time - element *list.Element - Path string - Data []byte - Size int64 - ETag string // 预计算的 ETag,避免每次请求重新计算 + ModTime time.Time + CachedAt time.Time // 缓存时间,用于 TTL 验证(新鲜度) + LastAccess time.Time + element *list.Element + Path string + Data []byte + Size int64 + ETag string // 预计算的 ETag,避免每次请求重新计算 + ContentType string // 预计算的 MIME 类型,避免每次请求重新检测 } // generateETag 基于 ModTime 和 Size 生成 ETag。 @@ -173,10 +174,11 @@ func (c *FileCache) Get(path string) (*FileEntry, bool) { // - data: 文件内容字节 // - size: 文件大小(字节) // - modTime: 文件最后修改时间 +// - contentType: MIME 类型 // // 返回值: // - error: 当前实现始终返回 nil -func (c *FileCache) Set(path string, data []byte, size int64, modTime time.Time) error { +func (c *FileCache) Set(path string, data []byte, size int64, modTime time.Time, contentType string) error { c.mu.Lock() defer c.mu.Unlock() @@ -190,6 +192,7 @@ func (c *FileCache) Set(path string, data []byte, size int64, modTime time.Time) entry.Size = size entry.ModTime = modTime entry.ETag = etag + entry.ContentType = contentType entry.CachedAt = time.Now() // 更新缓存时间 entry.LastAccess = time.Now() c.currentSize += size @@ -205,6 +208,7 @@ func (c *FileCache) Set(path string, data []byte, size int64, modTime time.Time) entry.Size = size entry.ModTime = modTime entry.ETag = etag + entry.ContentType = contentType entry.CachedAt = time.Now() entry.LastAccess = time.Now() entry.element = c.lruList.PushFront(entry) @@ -266,6 +270,7 @@ func (c *FileCache) removeEntry(entry *FileEntry) { entry.CachedAt = time.Time{} entry.LastAccess = time.Time{} entry.ETag = "" + entry.ContentType = "" entry.element = nil c.entryPool.Put(entry) } diff --git a/internal/cache/file_cache_allocation_test.go b/internal/cache/file_cache_allocation_test.go index 50c5dc0..57307b0 100644 --- a/internal/cache/file_cache_allocation_test.go +++ b/internal/cache/file_cache_allocation_test.go @@ -37,7 +37,7 @@ func BenchmarkFileCacheSetAllocation_New(b *testing.B) { for i := 0; b.Loop(); i++ { path := fmt.Sprintf("/new/file%d.txt", i) - fc.Set(path, data, size, time.Now()) + fc.Set(path, data, size, time.Now(), "text/plain") } } @@ -52,7 +52,7 @@ func BenchmarkFileCacheSetAllocation_Update(b *testing.B) { size := int64(len(data)) for i := range 1000 { path := fmt.Sprintf("/update/file%d.txt", i) - fc.Set(path, data, size, time.Now()) + fc.Set(path, data, size, time.Now(), "text/plain") } b.ReportAllocs() @@ -61,7 +61,7 @@ func BenchmarkFileCacheSetAllocation_Update(b *testing.B) { for i := 0; b.Loop(); i++ { // 循环更新已有条目 path := fmt.Sprintf("/update/file%d.txt", i%1000) - fc.Set(path, data, size, time.Now()) + fc.Set(path, data, size, time.Now(), "text/plain") } } @@ -80,7 +80,7 @@ func BenchmarkFileCacheSetAllocation_Eviction(b *testing.B) { size := int64(len(data)) for i := range 100 { path := fmt.Sprintf("/evict/file%d.txt", i) - fc.Set(path, data, size, time.Now()) + fc.Set(path, data, size, time.Now(), "text/plain") } b.ReportAllocs() @@ -89,7 +89,7 @@ func BenchmarkFileCacheSetAllocation_Eviction(b *testing.B) { for i := 0; b.Loop(); i++ { // 每个 Set 都触发淘汰 path := fmt.Sprintf("/evict/new%d.txt", i) - fc.Set(path, data, size, time.Now()) + fc.Set(path, data, size, time.Now(), "text/plain") } } @@ -105,7 +105,7 @@ func BenchmarkFileCacheSetAllocation_EvictionWithPool(b *testing.B) { // 预填充 for i := range 100 { path := fmt.Sprintf("/pool/file%d.txt", i) - fc.Set(path, data, size, time.Now()) + fc.Set(path, data, size, time.Now(), "text/plain") } b.ReportAllocs() @@ -113,7 +113,7 @@ func BenchmarkFileCacheSetAllocation_EvictionWithPool(b *testing.B) { for i := 0; b.Loop(); i++ { path := fmt.Sprintf("/pool/new%d.txt", i) - fc.Set(path, data, size, time.Now()) + fc.Set(path, data, size, time.Now(), "text/plain") } } @@ -130,7 +130,7 @@ func BenchmarkFileCacheSetAllocation_MemoryLimit(b *testing.B) { // 预填充到接近上限 for i := range 900 { path := fmt.Sprintf("/mem/file%d.txt", i) - fc.Set(path, data, size, time.Now()) + fc.Set(path, data, size, time.Now(), "text/plain") } b.ReportAllocs() @@ -138,7 +138,7 @@ func BenchmarkFileCacheSetAllocation_MemoryLimit(b *testing.B) { for i := 0; b.Loop(); i++ { path := fmt.Sprintf("/mem/new%d.txt", i) - fc.Set(path, data, size, time.Now()) + fc.Set(path, data, size, time.Now(), "text/plain") } } @@ -158,7 +158,7 @@ func BenchmarkFileCacheSetAllocation_Concurrent(b *testing.B) { i := 0 for pb.Next() { path := fmt.Sprintf("/conc/file%d.txt", i) - fc.Set(path, data, size, time.Now()) + fc.Set(path, data, size, time.Now(), "text/plain") i++ } }) @@ -174,7 +174,7 @@ func BenchmarkFileCacheSetAllocation_ConcurrentEviction(b *testing.B) { // 预填充 for i := range 100 { path := fmt.Sprintf("/concevict/file%d.txt", i) - fc.Set(path, data, size, time.Now()) + fc.Set(path, data, size, time.Now(), "text/plain") } b.ReportAllocs() @@ -184,7 +184,7 @@ func BenchmarkFileCacheSetAllocation_ConcurrentEviction(b *testing.B) { i := 0 for pb.Next() { path := fmt.Sprintf("/concevict/new%d.txt", i) - fc.Set(path, data, size, time.Now()) + fc.Set(path, data, size, time.Now(), "text/plain") i++ } }) diff --git a/internal/handler/static.go b/internal/handler/static.go index df6f704..2b5ce90 100644 --- a/internal/handler/static.go +++ b/internal/handler/static.go @@ -660,7 +660,7 @@ func (h *StaticHandler) handleStandard(ctx *fasthttp.RequestCtx, reqPath string) return } ctx.Response.SetBody(entry.Data) - ctx.Response.Header.SetContentType(mimeutil.DetectContentType(filePath)) + ctx.Response.Header.SetContentType(entry.ContentType) ctx.Response.Header.Set("ETag", entry.ETag) ctx.Response.Header.Set("Last-Modified", info.ModTime().UTC().Format(httpTimeFormat)) return @@ -681,7 +681,7 @@ func (h *StaticHandler) handleStandard(ctx *fasthttp.RequestCtx, reqPath string) h.fileCache.RefreshCachedAt(filePath) } ctx.Response.SetBody(entry.Data) - ctx.Response.Header.SetContentType(mimeutil.DetectContentType(filePath)) + ctx.Response.Header.SetContentType(entry.ContentType) ctx.Response.Header.Set("ETag", entry.ETag) ctx.Response.Header.Set("Last-Modified", info.ModTime().UTC().Format(httpTimeFormat)) return @@ -734,9 +734,9 @@ func (h *StaticHandler) serveFile(ctx *fasthttp.RequestCtx, filePath string, inf // 检查文件是否被修改 if entry.ModTime.Equal(info.ModTime()) { // 缓存命中且文件未修改 - // 使用缓存的 ETag,避免重新生成 + // 使用缓存的 ETag 和 ContentType,避免重新生成 ctx.Response.SetBody(entry.Data) - ctx.Response.Header.SetContentType(mimeutil.DetectContentType(filePath)) + ctx.Response.Header.SetContentType(entry.ContentType) ctx.Response.Header.Set("ETag", entry.ETag) ctx.Response.Header.Set("Last-Modified", info.ModTime().UTC().Format(httpTimeFormat)) h.setCacheHeaders(ctx) @@ -776,12 +776,13 @@ func (h *StaticHandler) serveFile(ctx *fasthttp.RequestCtx, filePath string, inf } // 存入缓存(仅对小文件缓存) + contentType := mimeutil.DetectContentType(filePath) if h.fileCache != nil && info.Size() < 1024*1024 { // < 1MB - _ = h.fileCache.Set(filePath, data, info.Size(), info.ModTime()) + _ = h.fileCache.Set(filePath, data, info.Size(), info.ModTime(), contentType) } ctx.Response.SetBody(data) - ctx.Response.Header.SetContentType(mimeutil.DetectContentType(filePath)) + ctx.Response.Header.SetContentType(contentType) ctx.Response.Header.Set("ETag", etag) ctx.Response.Header.Set("Last-Modified", info.ModTime().UTC().Format(httpTimeFormat)) h.setCacheHeaders(ctx)