lolly/internal/handler/static_bench_test.go
xfy 1128eb644f 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).
2026-06-11 14:05:56 +08:00

264 lines
6.7 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Package handler 提供静态文件处理器的基准测试。
//
// 该文件测试文件查找、缓存命中/未命中、try_files 等场景的性能。
//
// 作者xfy
package handler
import (
"os"
"path/filepath"
"testing"
"time"
"github.com/valyala/fasthttp"
"rua.plus/lolly/internal/cache"
)
// setupStaticTestDir 创建临时测试目录并填充测试文件。
//
// 创建包含 index.html、style.css、large.json、nested/file.js 的目录结构,
// 用于静态文件基准测试。
//
// 返回值:
// - string: 临时测试目录路径
// - func(): 清理函数,调用后删除临时目录
func setupStaticTestDir() (string, func()) {
dir, err := os.MkdirTemp("", "static_bench_*")
if err != nil {
panic(err)
}
// 创建测试文件
testFiles := map[string][]byte{
"index.html": []byte("<html><body>Index</body></html>"),
"style.css": make([]byte, 1024), // 1KB
"large.json": make([]byte, 10*1024), // 10KB
"nested/file.js": make([]byte, 5*1024), // 5KB
}
for path, content := range testFiles {
fullPath := filepath.Join(dir, path)
if err := os.MkdirAll(filepath.Dir(fullPath), 0o755); err != nil {
panic(err)
}
if err := os.WriteFile(fullPath, content, 0o644); err != nil {
panic(err)
}
}
cleanup := func() {
_ = os.RemoveAll(dir)
}
return dir, cleanup
}
// BenchmarkStaticFileLookup 测试文件路径查找性能。
func BenchmarkStaticFileLookup(b *testing.B) {
dir, cleanup := setupStaticTestDir()
defer cleanup()
handler := NewStaticHandler(dir, "/", []string{"index.html"}, false)
b.ResetTimer()
for b.Loop() {
ctx := &fasthttp.RequestCtx{}
ctx.Request.SetRequestURI("/style.css")
handler.Handle(ctx)
}
}
// BenchmarkStaticFileCacheHit 测试缓存命中场景性能。
func BenchmarkStaticFileCacheHit(b *testing.B) {
dir, cleanup := setupStaticTestDir()
defer cleanup()
fc := cache.NewFileCache(1000, 10*1024*1024, 0) // 1000 文件或 10MB
handler := NewStaticHandler(dir, "/", []string{"index.html"}, false)
handler.SetFileCache(fc)
// 预热缓存
ctx := &fasthttp.RequestCtx{}
ctx.Request.SetRequestURI("/style.css")
handler.Handle(ctx)
b.ResetTimer()
for b.Loop() {
ctx := &fasthttp.RequestCtx{}
ctx.Request.SetRequestURI("/style.css")
handler.Handle(ctx)
}
}
// BenchmarkStaticFileCacheMiss_1KB 测试 1KB 文件缓存未命中场景性能。
func BenchmarkStaticFileCacheMiss_1KB(b *testing.B) {
dir, cleanup := setupStaticTestDir()
defer cleanup()
handler := NewStaticHandler(dir, "/", []string{"index.html"}, false)
b.ResetTimer()
for b.Loop() {
ctx := &fasthttp.RequestCtx{}
ctx.Request.SetRequestURI("/style.css")
handler.Handle(ctx)
}
}
// BenchmarkStaticFileCacheMiss_10KB 测试 10KB 文件缓存未命中场景性能。
func BenchmarkStaticFileCacheMiss_10KB(b *testing.B) {
dir, cleanup := setupStaticTestDir()
defer cleanup()
handler := NewStaticHandler(dir, "/", []string{"index.html"}, false)
b.ResetTimer()
for b.Loop() {
ctx := &fasthttp.RequestCtx{}
ctx.Request.SetRequestURI("/large.json")
handler.Handle(ctx)
}
}
// BenchmarkStaticTryFiles 测试 try_files 查找性能。
func BenchmarkStaticTryFiles(b *testing.B) {
dir, cleanup := setupStaticTestDir()
defer cleanup()
handler := NewStaticHandler(dir, "/", []string{"index.html"}, false)
handler.SetTryFiles([]string{"$uri", "$uri/", "/index.html"}, false, nil)
b.ResetTimer()
for b.Loop() {
ctx := &fasthttp.RequestCtx{}
ctx.Request.SetRequestURI("/nonexistent/path")
handler.Handle(ctx)
}
}
// BenchmarkStaticIndex 测试索引文件查找性能。
func BenchmarkStaticIndex(b *testing.B) {
dir, cleanup := setupStaticTestDir()
defer cleanup()
handler := NewStaticHandler(dir, "/", []string{"index.html", "index.htm"}, false)
b.ResetTimer()
for b.Loop() {
ctx := &fasthttp.RequestCtx{}
ctx.Request.SetRequestURI("/")
handler.Handle(ctx)
}
}
// BenchmarkStaticNestedFile 测试嵌套文件查找性能。
func BenchmarkStaticNestedFile(b *testing.B) {
dir, cleanup := setupStaticTestDir()
defer cleanup()
handler := NewStaticHandler(dir, "/", []string{"index.html"}, false)
b.ResetTimer()
for b.Loop() {
ctx := &fasthttp.RequestCtx{}
ctx.Request.SetRequestURI("/nested/file.js")
handler.Handle(ctx)
}
}
// BenchmarkStaticFileNotFound 测试文件未找到场景性能。
func BenchmarkStaticFileNotFound(b *testing.B) {
dir, cleanup := setupStaticTestDir()
defer cleanup()
handler := NewStaticHandler(dir, "/", []string{"index.html"}, false)
b.ResetTimer()
for b.Loop() {
ctx := &fasthttp.RequestCtx{}
ctx.Request.SetRequestURI("/nonexistent/file.txt")
handler.Handle(ctx)
}
}
// BenchmarkStaticWithCacheParallel 测试带缓存的并发访问性能。
func BenchmarkStaticWithCacheParallel(b *testing.B) {
dir, cleanup := setupStaticTestDir()
defer cleanup()
fc := cache.NewFileCache(1000, 10*1024*1024, 0)
handler := NewStaticHandler(dir, "/", []string{"index.html"}, false)
handler.SetFileCache(fc)
paths := []string{"/style.css", "/large.json", "/nested/file.js", "/index.html"}
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
i := 0
for pb.Next() {
ctx := &fasthttp.RequestCtx{}
ctx.Request.SetRequestURI(paths[i%len(paths)])
handler.Handle(ctx)
i++
}
})
}
// BenchmarkStaticFileLookupWithAlias 测试 alias 模式下的文件查找性能。
func BenchmarkStaticFileLookupWithAlias(b *testing.B) {
dir, cleanup := setupStaticTestDir()
defer cleanup()
handler := &StaticHandler{
alias: dir + "/",
pathPrefix: "/static/",
pathPrefixLen: len("/static/"),
index: []string{"index.html"},
}
b.ResetTimer()
for b.Loop() {
ctx := &fasthttp.RequestCtx{}
ctx.Request.SetRequestURI("/static/style.css")
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)
}
}