Implement performance optimizations v1.1.0

- Replace map-based Params with slice-based structure for 25-40% reduction in allocations
- Implement zero-allocation path parsing with pre-allocation
- Add enhanced memory pooling for parameters, segments, and string builders
- Optimize wildcard parameter handling with string builders
- Add comprehensive performance benchmarks
- Achieve 14-49% performance improvements across all route types
- Maintain full API compatibility

Performance improvements:
- Static routes: 14.6% faster (682.1 vs 798.5 ns/op)
- Parameter routes: 24.7% faster (868.9 vs 1154 ns/op)
- Wildcard routes: 41.6% faster (979.3 vs 1676 ns/op)
- Multiple parameters: 41.5% faster (1026 vs 1753 ns/op)
- Middleware: 49.0% faster (1930 vs 3782 ns/op)
- Route groups: 49.5% faster (1442 vs 2855 ns/op)
- Large router: 24.5% faster (833.0 vs 1103 ns/op)
This commit is contained in:
2025-10-26 18:06:15 +01:00
parent db42316b49
commit 045ac8b5a3
5 changed files with 352 additions and 38 deletions

200
performance_test.go Normal file
View File

@@ -0,0 +1,200 @@
package sux
import (
"net/http"
"net/http/httptest"
"runtime"
"testing"
)
// BenchmarkParameterOperations benchmarks the new parameter handling
func BenchmarkParameterOperations(b *testing.B) {
params := Params{
keys: make([]string, 0, 4),
values: make([]string, 0, 4),
}
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
params.Set("id", "123")
params.Set("name", "test")
_ = params.Get("id")
_ = params.Get("name")
params.Reset()
}
}
// BenchmarkPathParsing compares old vs new path parsing
func BenchmarkPathParsingOld(b *testing.B) {
path := "/users/123/posts/456/comments/789"
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_ = parsePath(path)
}
}
func BenchmarkPathParsingNew(b *testing.B) {
path := "/users/123/posts/456/comments/789"
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_ = parsePathZeroAlloc(path)
}
}
// BenchmarkMemoryUsage tests memory efficiency under load
func BenchmarkMemoryUsage(b *testing.B) {
router := New()
// Add routes with different patterns
router.GET("/", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
router.GET("/users/:id", func(w http.ResponseWriter, r *http.Request) {
params := ParamsFromContext(r)
_ = params.Get("id")
w.WriteHeader(http.StatusOK)
})
router.GET("/files/*path", func(w http.ResponseWriter, r *http.Request) {
params := ParamsFromContext(r)
_ = params.Get("path")
w.WriteHeader(http.StatusOK)
})
requests := []struct {
method string
path string
}{
{"GET", "/"},
{"GET", "/users/123"},
{"GET", "/files/docs/readme.txt"},
}
var reqs []*http.Request
for _, req := range requests {
for i := 0; i < 100; i++ {
reqs = append(reqs, httptest.NewRequest(req.method, req.path, nil))
}
}
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
w := httptest.NewRecorder()
router.ServeHTTP(w, reqs[i%len(reqs)])
}
}
// BenchmarkConcurrentAccess tests performance under high concurrency
func BenchmarkConcurrentAccess(b *testing.B) {
router := New()
router.GET("/test/:id", func(w http.ResponseWriter, r *http.Request) {
params := ParamsFromContext(r)
_ = params.Get("id")
w.WriteHeader(http.StatusOK)
})
req := httptest.NewRequest("GET", "/test/123", nil)
b.ResetTimer()
b.ReportAllocs()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
}
})
}
// BenchmarkGCPressure measures GC pressure from allocations
func BenchmarkGCPressure(b *testing.B) {
router := New()
router.GET("/users/:id/posts/:postId", func(w http.ResponseWriter, r *http.Request) {
params := ParamsFromContext(r)
_ = params.Get("id") + params.Get("postId")
w.WriteHeader(http.StatusOK)
})
req := httptest.NewRequest("GET", "/users/123/posts/456", nil)
// Force GC before benchmark
runtime.GC()
var m1, m2 runtime.MemStats
runtime.ReadMemStats(&m1)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
}
b.StopTimer()
runtime.ReadMemStats(&m2)
b.Logf("Allocs: %d, TotalAlloc: %d bytes", m2.Mallocs-m1.Mallocs, m2.TotalAlloc-m1.TotalAlloc)
}
// BenchmarkWildcardOptimization tests the wildcard parameter optimization
func BenchmarkWildcardOptimization(b *testing.B) {
router := New()
router.GET("/files/*path", func(w http.ResponseWriter, r *http.Request) {
params := ParamsFromContext(r)
_ = params.Get("path")
w.WriteHeader(http.StatusOK)
})
// Test with varying path lengths
paths := []string{
"/files/file.txt",
"/files/docs/readme.txt",
"/files/docs/api/v1/users.json",
"/files/very/deep/nested/path/with/many/segments/file.txt",
}
var reqs []*http.Request
for _, path := range paths {
for i := 0; i < 25; i++ {
reqs = append(reqs, httptest.NewRequest("GET", path, nil))
}
}
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
w := httptest.NewRecorder()
router.ServeHTTP(w, reqs[i%len(reqs)])
}
}
// BenchmarkPoolEfficiency tests the efficiency of our pooling strategy
func BenchmarkPoolEfficiency(b *testing.B) {
router := New()
router.GET("/test/:param", func(w http.ResponseWriter, r *http.Request) {
params := ParamsFromContext(r)
_ = params.Get("param")
w.WriteHeader(http.StatusOK)
})
req := httptest.NewRequest("GET", "/test/value", nil)
b.ResetTimer()
b.ReportAllocs()
// This should show minimal allocations due to pooling
for i := 0; i < b.N; i++ {
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
}
}