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).
264 lines
6.7 KiB
Go
264 lines
6.7 KiB
Go
// 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)
|
||
}
|
||
}
|