perf(static): enable FileInfoCache by default with negative caching
Production static file serving now uses FileInfoCache by default with a 2-second TTL in router.go, dramatically reducing os.Stat syscalls for missing files and repeated paths. Changes: - Add negative cache support to FileInfoCache (caches 'not found' results) - Introduce statWithCache() helper in StaticHandler for uniform caching - Make FileInfoCache TTL configurable via SetTTL() - Default cacheTTL=0 disables caching in NewStaticHandler (tests compat) - router.go enables fileInfoCache with 2s TTL for all static handlers Benchmark (repeated 404s): No cache: ~2651 ns/op, 2225 B/op, 15 allocs/op With cache: ~1505 ns/op, 1905 B/op, 12 allocs/op Improvement: -43% latency, -14% allocations This addresses the dominant allocation source in v0.4.0 profile (os.statNolog at 74.95% of allocations).
This commit is contained in:
parent
445401c40f
commit
1128eb644f
@ -7,6 +7,7 @@
|
|||||||
// - 使用 TTL-only 新鲜度策略:缓存命中时不验证 ModTime
|
// - 使用 TTL-only 新鲜度策略:缓存命中时不验证 ModTime
|
||||||
// - 理由:每次验证 ModTime 仍需 os.Stat 调用,违背缓存目的
|
// - 理由:每次验证 ModTime 仍需 os.Stat 调用,违背缓存目的
|
||||||
// - 风险:TTL 内文件修改可能返回旧 FileInfo,但静态文件通常不频繁修改
|
// - 风险:TTL 内文件修改可能返回旧 FileInfo,但静态文件通常不频繁修改
|
||||||
|
// - 支持负缓存:缓存文件不存在的结果,避免重复 stat 不存在的路径
|
||||||
//
|
//
|
||||||
// 作者:xfy
|
// 作者:xfy
|
||||||
package handler
|
package handler
|
||||||
@ -19,59 +20,126 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
fileInfoCacheMaxEntries = 2000
|
fileInfoCacheMaxEntries = 2000
|
||||||
fileInfoCacheTTL = 10 * time.Second
|
defaultFileInfoCacheTTL = 10 * time.Second
|
||||||
|
defaultFileNotFoundCacheTTL = 2 * time.Second
|
||||||
)
|
)
|
||||||
|
|
||||||
// fileInfoEntry FileInfo 缓存条目
|
// fileInfoEntry FileInfo 缓存条目
|
||||||
type fileInfoEntry struct {
|
type fileInfoEntry struct {
|
||||||
path string
|
path string
|
||||||
info os.FileInfo
|
info os.FileInfo
|
||||||
|
notFound bool
|
||||||
cachedAt time.Time
|
cachedAt time.Time
|
||||||
element *list.Element
|
element *list.Element
|
||||||
}
|
}
|
||||||
|
|
||||||
// FileInfoCache FileInfo 缓存(O(1) LRU)
|
// FileInfoCache FileInfo 缓存(O(1) LRU)
|
||||||
type FileInfoCache struct {
|
type FileInfoCache struct {
|
||||||
entries map[string]*fileInfoEntry
|
entries map[string]*fileInfoEntry
|
||||||
lruList *list.List
|
lruList *list.List
|
||||||
mu sync.RWMutex
|
ttl time.Duration
|
||||||
|
notFoundTTL time.Duration
|
||||||
|
mu sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get 获取缓存的 FileInfo
|
// NewFileInfoCache 创建新的 FileInfo 缓存
|
||||||
|
func NewFileInfoCache() *FileInfoCache {
|
||||||
|
return &FileInfoCache{
|
||||||
|
entries: make(map[string]*fileInfoEntry),
|
||||||
|
lruList: list.New(),
|
||||||
|
ttl: defaultFileInfoCacheTTL,
|
||||||
|
notFoundTTL: defaultFileNotFoundCacheTTL,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetTTL 设置 FileInfo 缓存 TTL。
|
||||||
|
func (c *FileInfoCache) SetTTL(ttl time.Duration) {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
c.ttl = ttl
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get 获取缓存的 FileInfo(向后兼容)
|
||||||
|
//
|
||||||
|
// 返回值:
|
||||||
|
// - info: 缓存的 FileInfo
|
||||||
|
// - ok: 是否命中缓存(仅对存在的文件返回 true)
|
||||||
func (c *FileInfoCache) Get(filePath string) (os.FileInfo, bool) {
|
func (c *FileInfoCache) Get(filePath string) (os.FileInfo, bool) {
|
||||||
|
info, hit, exists := c.GetWithNotFound(filePath)
|
||||||
|
if !hit || !exists {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
return info, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetWithNotFound 获取缓存结果,包含负缓存(文件不存在)信息。
|
||||||
|
//
|
||||||
|
// 返回值:
|
||||||
|
// - info: 缓存的 FileInfo(仅当 exists=true 时有效)
|
||||||
|
// - hit: 是否命中缓存(包括正缓存和负缓存)
|
||||||
|
// - exists: 文件是否存在(false 表示命中了负缓存)
|
||||||
|
func (c *FileInfoCache) GetWithNotFound(filePath string) (os.FileInfo, bool, bool) {
|
||||||
c.mu.RLock()
|
c.mu.RLock()
|
||||||
entry, ok := c.entries[filePath]
|
entry, ok := c.entries[filePath]
|
||||||
if !ok {
|
if !ok {
|
||||||
c.mu.RUnlock()
|
c.mu.RUnlock()
|
||||||
return nil, false
|
return nil, false, false
|
||||||
}
|
}
|
||||||
|
|
||||||
if time.Since(entry.cachedAt) > fileInfoCacheTTL {
|
ttl := c.ttl
|
||||||
|
if ttl <= 0 {
|
||||||
|
// ttl=0 表示禁用 fileInfoCache,总是返回未命中
|
||||||
|
c.mu.RUnlock()
|
||||||
|
return nil, false, false
|
||||||
|
}
|
||||||
|
if entry.notFound {
|
||||||
|
if c.notFoundTTL > 0 {
|
||||||
|
ttl = c.notFoundTTL
|
||||||
|
} else {
|
||||||
|
ttl = defaultFileNotFoundCacheTTL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if time.Since(entry.cachedAt) > ttl {
|
||||||
c.mu.RUnlock()
|
c.mu.RUnlock()
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
if e, ok := c.entries[filePath]; ok && time.Since(e.cachedAt) > fileInfoCacheTTL {
|
if e, ok := c.entries[filePath]; ok && time.Since(e.cachedAt) > ttl {
|
||||||
c.lruList.Remove(e.element)
|
c.lruList.Remove(e.element)
|
||||||
delete(c.entries, filePath)
|
delete(c.entries, filePath)
|
||||||
}
|
}
|
||||||
c.mu.Unlock()
|
c.mu.Unlock()
|
||||||
return nil, false
|
return nil, false, false
|
||||||
}
|
}
|
||||||
|
|
||||||
info := entry.info
|
info := entry.info
|
||||||
|
notFound := entry.notFound
|
||||||
c.mu.RUnlock()
|
c.mu.RUnlock()
|
||||||
return info, true
|
return info, true, !notFound
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set 缓存 FileInfo
|
// Set 缓存 FileInfo(向后兼容)
|
||||||
func (c *FileInfoCache) Set(filePath string, info os.FileInfo) {
|
func (c *FileInfoCache) Set(filePath string, info os.FileInfo) {
|
||||||
|
c.SetWithNotFound(filePath, info, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetWithNotFound 缓存 FileInfo,支持负缓存。
|
||||||
|
//
|
||||||
|
// 参数:
|
||||||
|
// - filePath: 文件路径
|
||||||
|
// - info: FileInfo(notFound=true 时可为 nil)
|
||||||
|
// - notFound: true 表示文件不存在
|
||||||
|
func (c *FileInfoCache) SetWithNotFound(filePath string, info os.FileInfo, notFound bool) {
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
defer c.mu.Unlock()
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
// 已存在,更新
|
// 已存在,更新
|
||||||
if entry, ok := c.entries[filePath]; ok {
|
if entry, ok := c.entries[filePath]; ok {
|
||||||
entry.info = info
|
entry.info = info
|
||||||
entry.cachedAt = time.Now()
|
entry.notFound = notFound
|
||||||
|
entry.cachedAt = now
|
||||||
c.lruList.MoveToFront(entry.element)
|
c.lruList.MoveToFront(entry.element)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -90,7 +158,8 @@ func (c *FileInfoCache) Set(filePath string, info os.FileInfo) {
|
|||||||
entry := &fileInfoEntry{
|
entry := &fileInfoEntry{
|
||||||
path: filePath,
|
path: filePath,
|
||||||
info: info,
|
info: info,
|
||||||
cachedAt: time.Now(),
|
notFound: notFound,
|
||||||
|
cachedAt: now,
|
||||||
}
|
}
|
||||||
entry.element = c.lruList.PushFront(entry)
|
entry.element = c.lruList.PushFront(entry)
|
||||||
c.entries[filePath] = entry
|
c.entries[filePath] = entry
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"container/list"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
@ -16,10 +15,7 @@ func TestFileInfoCache(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
cache := &FileInfoCache{
|
cache := NewFileInfoCache()
|
||||||
entries: make(map[string]*fileInfoEntry, fileInfoCacheMaxEntries),
|
|
||||||
lruList: list.New(),
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Run("缓存未命中", func(t *testing.T) {
|
t.Run("缓存未命中", func(t *testing.T) {
|
||||||
info, ok := cache.Get(tmpFile)
|
info, ok := cache.Get(tmpFile)
|
||||||
@ -66,10 +62,7 @@ func TestFileInfoCacheTTL(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
cache := &FileInfoCache{
|
cache := NewFileInfoCache()
|
||||||
entries: make(map[string]*fileInfoEntry, fileInfoCacheMaxEntries),
|
|
||||||
lruList: list.New(),
|
|
||||||
}
|
|
||||||
realInfo, _ := os.Stat(tmpFile)
|
realInfo, _ := os.Stat(tmpFile)
|
||||||
|
|
||||||
// 存入缓存
|
// 存入缓存
|
||||||
@ -84,7 +77,7 @@ func TestFileInfoCacheTTL(t *testing.T) {
|
|||||||
// 模拟过期:修改 cachedAt
|
// 模拟过期:修改 cachedAt
|
||||||
cache.mu.Lock()
|
cache.mu.Lock()
|
||||||
if entry, exists := cache.entries[tmpFile]; exists {
|
if entry, exists := cache.entries[tmpFile]; exists {
|
||||||
entry.cachedAt = time.Now().Add(-fileInfoCacheTTL - time.Second)
|
entry.cachedAt = time.Now().Add(-cache.ttl - time.Second)
|
||||||
}
|
}
|
||||||
cache.mu.Unlock()
|
cache.mu.Unlock()
|
||||||
|
|
||||||
@ -96,10 +89,7 @@ func TestFileInfoCacheTTL(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestFileInfoCacheLRU(t *testing.T) {
|
func TestFileInfoCacheLRU(t *testing.T) {
|
||||||
cache := &FileInfoCache{
|
cache := NewFileInfoCache()
|
||||||
entries: make(map[string]*fileInfoEntry, fileInfoCacheMaxEntries),
|
|
||||||
lruList: list.New(),
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建测试文件信息
|
// 创建测试文件信息
|
||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
|
|||||||
@ -40,6 +40,40 @@ const (
|
|||||||
expiresMax = "max"
|
expiresMax = "max"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// statWithCache 从 FileInfo 缓存获取文件信息,支持负缓存。
|
||||||
|
//
|
||||||
|
// 返回值:
|
||||||
|
// - info: 文件信息(命中负缓存或出错时为 nil)
|
||||||
|
// - ok: true 表示文件存在且缓存命中
|
||||||
|
// - err: 错误信息(命中负缓存时返回 nil,表示已知文件不存在)
|
||||||
|
func (h *StaticHandler) statWithCache(filePath string) (os.FileInfo, bool, error) {
|
||||||
|
if h.fileInfoCache == nil {
|
||||||
|
info, err := os.Stat(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
return info, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
info, hit, exists := h.fileInfoCache.GetWithNotFound(filePath)
|
||||||
|
if hit {
|
||||||
|
if exists {
|
||||||
|
return info, true, nil
|
||||||
|
}
|
||||||
|
// 命中负缓存:已知文件不存在
|
||||||
|
return nil, false, os.ErrNotExist
|
||||||
|
}
|
||||||
|
|
||||||
|
// 缓存未命中,调用 os.Stat
|
||||||
|
info, err := os.Stat(filePath)
|
||||||
|
if err != nil {
|
||||||
|
h.fileInfoCache.SetWithNotFound(filePath, nil, true)
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
h.fileInfoCache.SetWithNotFound(filePath, info, false)
|
||||||
|
return info, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
// StaticHandler 静态文件处理器。
|
// StaticHandler 静态文件处理器。
|
||||||
//
|
//
|
||||||
// 提供静态文件服务,支持目录索引、文件缓存和零拷贝传输。
|
// 提供静态文件服务,支持目录索引、文件缓存和零拷贝传输。
|
||||||
@ -107,13 +141,17 @@ func NewStaticHandler(root, pathPrefix string, index []string, useSendfile bool)
|
|||||||
prefixLen = 0 // 根路径无需剥离
|
prefixLen = 0 // 根路径无需剥离
|
||||||
}
|
}
|
||||||
|
|
||||||
return &StaticHandler{
|
h := &StaticHandler{
|
||||||
root: cleanRoot,
|
root: cleanRoot,
|
||||||
pathPrefix: pathPrefix,
|
pathPrefix: pathPrefix,
|
||||||
pathPrefixLen: prefixLen,
|
pathPrefixLen: prefixLen,
|
||||||
index: index,
|
index: index,
|
||||||
useSendfile: useSendfile,
|
useSendfile: useSendfile,
|
||||||
|
fileInfoCache: NewFileInfoCache(),
|
||||||
}
|
}
|
||||||
|
// 默认 cacheTTL=0(每次验证 ModTime),同步禁用 fileInfoCache
|
||||||
|
h.fileInfoCache.SetTTL(0)
|
||||||
|
return h
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetAlias 设置路径别名。
|
// SetAlias 设置路径别名。
|
||||||
@ -267,6 +305,9 @@ func (h *StaticHandler) SetAutoIndex(enabled bool, format string, localtime, exa
|
|||||||
// 默认 TTL 为 5 秒。
|
// 默认 TTL 为 5 秒。
|
||||||
func (h *StaticHandler) SetCacheTTL(ttl time.Duration) {
|
func (h *StaticHandler) SetCacheTTL(ttl time.Duration) {
|
||||||
h.cacheTTL = ttl
|
h.cacheTTL = ttl
|
||||||
|
if h.fileInfoCache != nil {
|
||||||
|
h.fileInfoCache.SetTTL(ttl)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle 处理静态文件请求。
|
// Handle 处理静态文件请求。
|
||||||
@ -333,46 +374,19 @@ func (h *StaticHandler) handleTryFiles(ctx *fasthttp.RequestCtx, reqPath string)
|
|||||||
// 构建完整文件路径
|
// 构建完整文件路径
|
||||||
filePath := h.buildFilePath(targetPath)
|
filePath := h.buildFilePath(targetPath)
|
||||||
|
|
||||||
// 检查文件/目录是否存在
|
// 检查文件/目录是否存在(带负缓存)
|
||||||
// 先查 FileInfo 缓存
|
info, exists, _ := h.statWithCache(filePath)
|
||||||
var info os.FileInfo
|
if !exists {
|
||||||
var err error
|
continue // 不存在,尝试下一个
|
||||||
|
|
||||||
if h.fileInfoCache != nil {
|
|
||||||
if cachedInfo, ok := h.fileInfoCache.Get(filePath); ok {
|
|
||||||
info = cachedInfo
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if info == nil {
|
|
||||||
// 缓存未命中,调用 os.Stat
|
|
||||||
info, err = os.Stat(filePath)
|
|
||||||
if err != nil {
|
|
||||||
continue // 不存在,尝试下一个
|
|
||||||
}
|
|
||||||
if h.fileInfoCache != nil {
|
|
||||||
h.fileInfoCache.Set(filePath, info)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if info.IsDir() {
|
if info.IsDir() {
|
||||||
// 如果是目录,尝试查找索引文件
|
// 如果是目录,尝试查找索引文件
|
||||||
for _, idx := range h.index {
|
for _, idx := range h.index {
|
||||||
idxPath := filepath.Join(filePath, idx)
|
idxPath := filepath.Join(filePath, idx)
|
||||||
var idxInfo os.FileInfo
|
idxInfo, idxExists, _ := h.statWithCache(idxPath)
|
||||||
if h.fileInfoCache != nil {
|
if !idxExists {
|
||||||
if cachedInfo, ok := h.fileInfoCache.Get(idxPath); ok {
|
continue
|
||||||
idxInfo = cachedInfo
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if idxInfo == nil {
|
|
||||||
idxInfo, err = os.Stat(idxPath)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if h.fileInfoCache != nil {
|
|
||||||
h.fileInfoCache.Set(idxPath, idxInfo)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if !idxInfo.IsDir() {
|
if !idxInfo.IsDir() {
|
||||||
h.serveFile(ctx, idxPath, idxInfo, false)
|
h.serveFile(ctx, idxPath, idxInfo, false)
|
||||||
@ -468,25 +482,14 @@ func (h *StaticHandler) handleInternalRedirect(ctx *fasthttp.RequestCtx, targetP
|
|||||||
// tryFilesPass 为 false(默认),直接服务文件,不触发中间件
|
// tryFilesPass 为 false(默认),直接服务文件,不触发中间件
|
||||||
filePath := h.buildFilePath(targetPath)
|
filePath := h.buildFilePath(targetPath)
|
||||||
|
|
||||||
// 先查 FileInfo 缓存
|
info, exists, err := h.statWithCache(filePath)
|
||||||
var info os.FileInfo
|
if !exists {
|
||||||
var err error
|
|
||||||
|
|
||||||
if h.fileInfoCache != nil {
|
|
||||||
if cachedInfo, ok := h.fileInfoCache.Get(filePath); ok {
|
|
||||||
info = cachedInfo
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if info == nil {
|
|
||||||
info, err = os.Stat(filePath)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.SendError(ctx, utils.ErrNotFound)
|
utils.SendError(ctx, utils.ErrNotFound)
|
||||||
return
|
} else {
|
||||||
}
|
utils.SendError(ctx, utils.ErrForbidden)
|
||||||
if h.fileInfoCache != nil {
|
|
||||||
h.fileInfoCache.Set(filePath, info)
|
|
||||||
}
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if info.IsDir() {
|
if info.IsDir() {
|
||||||
@ -508,27 +511,11 @@ func (h *StaticHandler) handleStandard(ctx *fasthttp.RequestCtx, reqPath string)
|
|||||||
// 构建完整文件路径
|
// 构建完整文件路径
|
||||||
filePath := h.buildFilePath(relPath)
|
filePath := h.buildFilePath(relPath)
|
||||||
|
|
||||||
// 检查文件/目录是否存在
|
// 检查文件/目录是否存在(带负缓存)
|
||||||
// 先查 FileInfo 缓存(TTL 内信任缓存,不验证 ModTime)
|
info, exists, _ := h.statWithCache(filePath)
|
||||||
var info os.FileInfo
|
if !exists {
|
||||||
var err error
|
utils.SendError(ctx, utils.ErrNotFound)
|
||||||
|
return
|
||||||
if h.fileInfoCache != nil {
|
|
||||||
if cachedInfo, ok := h.fileInfoCache.Get(filePath); ok {
|
|
||||||
info = cachedInfo
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if info == nil {
|
|
||||||
// 缓存未命中,调用 os.Stat
|
|
||||||
info, err = os.Stat(filePath)
|
|
||||||
if err != nil {
|
|
||||||
utils.SendError(ctx, utils.ErrNotFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if h.fileInfoCache != nil {
|
|
||||||
h.fileInfoCache.Set(filePath, info)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 符号链接安全检查
|
// 符号链接安全检查
|
||||||
@ -539,11 +526,12 @@ func (h *StaticHandler) handleStandard(ctx *fasthttp.RequestCtx, reqPath string)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果是目录,尝试索引文件
|
// 如果是目录,尝试索引文件(带负缓存)
|
||||||
if info.IsDir() {
|
if info.IsDir() {
|
||||||
for _, idx := range h.index {
|
for _, idx := range h.index {
|
||||||
idxPath := filepath.Join(filePath, idx)
|
idxPath := filepath.Join(filePath, idx)
|
||||||
if idxInfo, err := os.Stat(idxPath); err == nil && !idxInfo.IsDir() {
|
idxInfo, idxExists, _ := h.statWithCache(idxPath)
|
||||||
|
if idxExists && !idxInfo.IsDir() {
|
||||||
h.serveFile(ctx, idxPath, idxInfo, true)
|
h.serveFile(ctx, idxPath, idxInfo, true)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/valyala/fasthttp"
|
"github.com/valyala/fasthttp"
|
||||||
"rua.plus/lolly/internal/cache"
|
"rua.plus/lolly/internal/cache"
|
||||||
@ -223,3 +224,40 @@ func BenchmarkStaticFileLookupWithAlias(b *testing.B) {
|
|||||||
handler.Handle(ctx)
|
handler.Handle(ctx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BenchmarkStaticFileNotFoundRepeated 测试重复访问不存在路径的性能。
|
||||||
|
//
|
||||||
|
// 启用 fileInfoCache (TTL=2s) 模拟生产配置,负缓存可避免重复的 os.Stat 调用。
|
||||||
|
func BenchmarkStaticFileNotFoundRepeated(b *testing.B) {
|
||||||
|
dir, cleanup := setupStaticTestDir()
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
handler := NewStaticHandler(dir, "/", []string{"index.html"}, false)
|
||||||
|
handler.SetCacheTTL(2 * time.Second)
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
b.ReportAllocs()
|
||||||
|
for b.Loop() {
|
||||||
|
ctx := &fasthttp.RequestCtx{}
|
||||||
|
ctx.Request.SetRequestURI("/does-not-exist.css")
|
||||||
|
handler.Handle(ctx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BenchmarkStaticFileNotFoundRepeatedNoCache 测试无 fileInfoCache 时的性能基准。
|
||||||
|
func BenchmarkStaticFileNotFoundRepeatedNoCache(b *testing.B) {
|
||||||
|
dir, cleanup := setupStaticTestDir()
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
handler := NewStaticHandler(dir, "/", []string{"index.html"}, false)
|
||||||
|
// cacheTTL=0 表示禁用 fileInfoCache(旧行为)
|
||||||
|
handler.SetCacheTTL(0)
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
b.ReportAllocs()
|
||||||
|
for b.Loop() {
|
||||||
|
ctx := &fasthttp.RequestCtx{}
|
||||||
|
ctx.Request.SetRequestURI("/does-not-exist.css")
|
||||||
|
handler.Handle(ctx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -149,9 +149,10 @@ func (s *Server) configureStaticHandler(static *config.StaticConfig, cfg *config
|
|||||||
}
|
}
|
||||||
if s.fileCache != nil {
|
if s.fileCache != nil {
|
||||||
staticHandler.SetFileCache(s.fileCache)
|
staticHandler.SetFileCache(s.fileCache)
|
||||||
// 设置默认缓存 TTL (5s)
|
|
||||||
staticHandler.SetCacheTTL(5 * time.Second)
|
|
||||||
}
|
}
|
||||||
|
// 始终启用 fileInfoCache 以减少 os.Stat 调用
|
||||||
|
// 默认 TTL 2s,在静态文件修改可见性和性能间取得平衡
|
||||||
|
staticHandler.SetCacheTTL(2 * time.Second)
|
||||||
if cfg.Compression.GzipStatic {
|
if cfg.Compression.GzipStatic {
|
||||||
// extensions: 源文件类型,为空使用默认值
|
// extensions: 源文件类型,为空使用默认值
|
||||||
// GzipStaticExtensions: 预压缩文件扩展名(如 .br, .gz)
|
// GzipStaticExtensions: 预压缩文件扩展名(如 .br, .gz)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user