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 <noreply@anthropic.com>
This commit is contained in:
parent
23fdcf89ae
commit
3c96f12f74
24
internal/cache/cache_bench_test.go
vendored
24
internal/cache/cache_bench_test.go
vendored
@ -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()
|
||||
|
||||
30
internal/cache/cache_test.go
vendored
30
internal/cache/cache_test.go
vendored
@ -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)
|
||||
|
||||
23
internal/cache/file_cache.go
vendored
23
internal/cache/file_cache.go
vendored
@ -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)
|
||||
}
|
||||
|
||||
24
internal/cache/file_cache_allocation_test.go
vendored
24
internal/cache/file_cache_allocation_test.go
vendored
@ -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++
|
||||
}
|
||||
})
|
||||
|
||||
@ -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)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user