From ac0f95dd1411fbaf510c154af91e21b574fce779 Mon Sep 17 00:00:00 2001 From: Lukasz Raczylo Date: Tue, 30 Sep 2025 10:47:26 +0100 Subject: [PATCH] Pooling cleanup. --- internal/cache/typed_cache.go | 20 +- internal/optimization/memory_pool.go | 100 ---- internal/optimization/memory_pool_test.go | 532 ---------------------- internal/pool/pool.go | 68 +++ internal/pool/utils.go | 70 +++ jwt.go | 55 ++- memory_pools.go | 264 ----------- profiling.go | 156 +------ profiling_test.go | 242 +--------- session.go | 13 +- session_chunk_manager.go | 19 +- string_builder_pool.go | 109 ----- token_validator.go | 14 +- utilities.go | 4 +- 14 files changed, 229 insertions(+), 1437 deletions(-) delete mode 100644 internal/optimization/memory_pool.go delete mode 100644 internal/optimization/memory_pool_test.go create mode 100644 internal/pool/utils.go delete mode 100644 memory_pools.go delete mode 100644 string_builder_pool.go diff --git a/internal/cache/typed_cache.go b/internal/cache/typed_cache.go index 110b8e2..1e2de29 100644 --- a/internal/cache/typed_cache.go +++ b/internal/cache/typed_cache.go @@ -1,9 +1,12 @@ package cache import ( + "bytes" "encoding/json" "fmt" "time" + + "github.com/lukaszraczylo/traefikoidc/internal/pool" ) // TypedCache provides a type-safe wrapper around Cache for specific types @@ -42,13 +45,24 @@ func (tc *TypedCache[T]) Get(key string) (T, bool) { } // If that fails, try JSON marshaling/unmarshaling for complex types - data, err := json.Marshal(value) - if err != nil { + // Use pooled buffer for encoding + pm := pool.Get() + buf := pm.GetBuffer(256) + defer pm.PutBuffer(buf) + + encoder := pm.GetJSONEncoder(buf) + defer pm.PutJSONEncoder(encoder) + + if err := encoder.Encode(value); err != nil { return zero, false } + // Decode using pooled decoder var result T - if err := json.Unmarshal(data, &result); err != nil { + decoder := pm.GetJSONDecoder(bytes.NewReader(buf.Bytes())) + defer pm.PutJSONDecoder(decoder) + + if err := decoder.Decode(&result); err != nil { return zero, false } diff --git a/internal/optimization/memory_pool.go b/internal/optimization/memory_pool.go deleted file mode 100644 index 870ddad..0000000 --- a/internal/optimization/memory_pool.go +++ /dev/null @@ -1,100 +0,0 @@ -// Package optimization provides memory and performance optimizations -package optimization - -import ( - "sync" -) - -// StringBuilderPool provides a pool of reusable string builders -type StringBuilderPool struct { - pool sync.Pool -} - -// NewStringBuilderPool creates a new string builder pool -func NewStringBuilderPool() *StringBuilderPool { - return &StringBuilderPool{ - pool: sync.Pool{ - New: func() any { - return make([]byte, 0, 256) // Pre-allocate 256 bytes - }, - }, - } -} - -// Get retrieves a string builder from the pool -func (p *StringBuilderPool) Get() []byte { - buf := p.pool.Get().([]byte) - // Return the slice with reset length but preserved capacity - return buf[:0] -} - -// Put returns a string builder to the pool -func (p *StringBuilderPool) Put(buf []byte) { - if buf != nil && cap(buf) < 4096 { // Don't pool overly large buffers or nil buffers - // Intentional design: slice API is more ergonomic than *[]byte. - // The 24-byte boxing allocation is negligible compared to 256+ byte buffer allocation we avoid. - //lint:ignore SA6002 slice interface is preferred over pointer interface for ergonomics - p.pool.Put(buf[:0]) // Reset length and store the slice directly - } -} - -// ByteSlicePool provides a pool of reusable byte slices -type ByteSlicePool struct { - pool sync.Pool - size int -} - -// NewByteSlicePool creates a new byte slice pool with specified size -func NewByteSlicePool(size int) *ByteSlicePool { - return &ByteSlicePool{ - size: size, - pool: sync.Pool{ - New: func() any { - return make([]byte, size) - }, - }, - } -} - -// Get retrieves a byte slice from the pool -func (p *ByteSlicePool) Get() []byte { - buf := p.pool.Get().([]byte) - // Ensure we're returning the full-sized slice - return buf[:p.size] -} - -// Put returns a byte slice to the pool -func (p *ByteSlicePool) Put(buf []byte) { - if buf != nil && len(buf) == p.size { - // Intentional design: slice API is more ergonomic than *[]byte. - // The 24-byte boxing allocation is negligible compared to the sized buffer allocation we avoid. - //lint:ignore SA6002 slice interface is preferred over pointer interface for ergonomics - p.pool.Put(buf) - } -} - -// Global pools for common use cases -var ( - globalStringBuilderPool = NewStringBuilderPool() - globalByteSlicePool = NewByteSlicePool(2048) -) - -// GetStringBuilder gets a string builder from the global pool -func GetStringBuilder() []byte { - return globalStringBuilderPool.Get() -} - -// PutStringBuilder returns a string builder to the global pool -func PutStringBuilder(buf []byte) { - globalStringBuilderPool.Put(buf) -} - -// GetByteSlice gets a byte slice from the global pool -func GetByteSlice() []byte { - return globalByteSlicePool.Get() -} - -// PutByteSlice returns a byte slice to the global pool -func PutByteSlice(buf []byte) { - globalByteSlicePool.Put(buf) -} diff --git a/internal/optimization/memory_pool_test.go b/internal/optimization/memory_pool_test.go deleted file mode 100644 index 5ba34c9..0000000 --- a/internal/optimization/memory_pool_test.go +++ /dev/null @@ -1,532 +0,0 @@ -package optimization - -import ( - "runtime" - "sync" - "testing" -) - -// TestNewStringBuilderPool tests StringBuilderPool creation -func TestNewStringBuilderPool(t *testing.T) { - pool := NewStringBuilderPool() - - if pool == nil { - t.Error("Expected NewStringBuilderPool to return non-nil pool") - } -} - -// TestStringBuilderPool tests StringBuilderPool functionality -func TestStringBuilderPool(t *testing.T) { - pool := NewStringBuilderPool() - - // Test Get - buf1 := pool.Get() - if buf1 == nil { - t.Error("Expected Get to return non-nil buffer") - } - - // Check initial capacity - if cap(buf1) != 256 { - t.Errorf("Expected initial capacity of 256, got %d", cap(buf1)) - } - - // Check initial length - if len(buf1) != 0 { - t.Errorf("Expected initial length of 0, got %d", len(buf1)) - } - - // Test Put and reuse - // Simulate some usage - buf1 = append(buf1, []byte("hello world")...) - pool.Put(buf1) - - // Get another buffer - should reuse the one we just put - buf2 := pool.Get() - if buf2 == nil { - t.Error("Expected Get to return non-nil buffer after Put") - } - - // Buffer should be reset to length 0 but keep capacity - if len(buf2) != 0 { - t.Errorf("Expected buffer length to be reset to 0, got %d", len(buf2)) - } - - // Should reuse the same underlying array - if cap(buf2) < 256 { - t.Errorf("Expected buffer capacity to be at least 256, got %d", cap(buf2)) - } -} - -// TestStringBuilderPoolLargeBuffer tests that large buffers are not pooled -func TestStringBuilderPoolLargeBuffer(t *testing.T) { - pool := NewStringBuilderPool() - - // Get a buffer and make it very large - buf := pool.Get() - largeBuf := make([]byte, 5000) // Larger than 4096 threshold - buf = append(buf, largeBuf...) - - // Put it back - should not be pooled due to size - pool.Put(buf) - - // Get a new buffer - should be a fresh one with default capacity - newBuf := pool.Get() - if cap(newBuf) != 256 { - t.Errorf("Expected fresh buffer with capacity 256, got %d", cap(newBuf)) - } -} - -// TestNewByteSlicePool tests ByteSlicePool creation -func TestNewByteSlicePool(t *testing.T) { - size := 1024 - pool := NewByteSlicePool(size) - - if pool == nil { - t.Error("Expected NewByteSlicePool to return non-nil pool") - return - } - - if pool.size != size { - t.Errorf("Expected pool size %d, got %d", size, pool.size) - } -} - -// TestByteSlicePool tests ByteSlicePool functionality -func TestByteSlicePool(t *testing.T) { - size := 512 - pool := NewByteSlicePool(size) - - // Test Get - buf1 := pool.Get() - if buf1 == nil { - t.Error("Expected Get to return non-nil buffer") - } - - // Check size - if len(buf1) != size { - t.Errorf("Expected buffer length %d, got %d", size, len(buf1)) - } - - // Test Put and reuse - pool.Put(buf1) - - // Get another buffer - should reuse the one we just put - buf2 := pool.Get() - if buf2 == nil { - t.Error("Expected Get to return non-nil buffer after Put") - } - - // Should be the same size - if len(buf2) != size { - t.Errorf("Expected buffer length %d, got %d", size, len(buf2)) - } -} - -// TestByteSlicePoolWrongSize tests that wrong-sized buffers are not pooled -func TestByteSlicePoolWrongSize(t *testing.T) { - size := 512 - pool := NewByteSlicePool(size) - - // Get a buffer - buf := pool.Get() - - // Resize it (simulate usage that changes size) - buf = buf[:100] // Change length to 100 - - // Put it back - should not be pooled due to wrong size - pool.Put(buf) - - // Get a new buffer - should be a fresh one with correct size - newBuf := pool.Get() - if len(newBuf) != size { - t.Errorf("Expected fresh buffer with length %d, got %d", size, len(newBuf)) - } -} - -// TestGlobalStringBuilderPool tests global string builder pool functions -func TestGlobalStringBuilderPool(t *testing.T) { - // Test GetStringBuilder - buf1 := GetStringBuilder() - if buf1 == nil { - t.Error("Expected GetStringBuilder to return non-nil buffer") - } - - if cap(buf1) != 256 { - t.Errorf("Expected initial capacity of 256, got %d", cap(buf1)) - } - - // Test PutStringBuilder and reuse - buf1 = append(buf1, []byte("test data")...) - PutStringBuilder(buf1) - - buf2 := GetStringBuilder() - if len(buf2) != 0 { - t.Errorf("Expected buffer length to be reset to 0, got %d", len(buf2)) - } -} - -// TestGlobalByteSlicePool tests global byte slice pool functions -func TestGlobalByteSlicePool(t *testing.T) { - // Test GetByteSlice - buf1 := GetByteSlice() - if buf1 == nil { - t.Error("Expected GetByteSlice to return non-nil buffer") - } - - if len(buf1) != 2048 { - t.Errorf("Expected buffer length of 2048, got %d", len(buf1)) - } - - // Test PutByteSlice and reuse - PutByteSlice(buf1) - - buf2 := GetByteSlice() - if len(buf2) != 2048 { - t.Errorf("Expected buffer length of 2048, got %d", len(buf2)) - } -} - -// TestPoolsConcurrency tests that pools work correctly under concurrent access -func TestPoolsConcurrency(t *testing.T) { - const numGoroutines = 100 - const numOperations = 1000 - - // Test StringBuilderPool concurrency - t.Run("StringBuilderPool concurrency", func(t *testing.T) { - pool := NewStringBuilderPool() - var wg sync.WaitGroup - - for i := 0; i < numGoroutines; i++ { - wg.Add(1) - go func() { - defer wg.Done() - for j := 0; j < numOperations; j++ { - buf := pool.Get() - buf = append(buf, []byte("test")...) - pool.Put(buf) - } - }() - } - - wg.Wait() - }) - - // Test ByteSlicePool concurrency - t.Run("ByteSlicePool concurrency", func(t *testing.T) { - pool := NewByteSlicePool(1024) - var wg sync.WaitGroup - - for i := 0; i < numGoroutines; i++ { - wg.Add(1) - go func() { - defer wg.Done() - for j := 0; j < numOperations; j++ { - buf := pool.Get() - // Simulate some work - for k := 0; k < len(buf); k++ { - buf[k] = byte(k % 256) - } - pool.Put(buf) - } - }() - } - - wg.Wait() - }) - - // Test global pools concurrency - t.Run("Global pools concurrency", func(t *testing.T) { - var wg sync.WaitGroup - - for i := 0; i < numGoroutines; i++ { - wg.Add(1) - go func() { - defer wg.Done() - for j := 0; j < numOperations; j++ { - // Test string builder pool - strBuf := GetStringBuilder() - strBuf = append(strBuf, []byte("test")...) - PutStringBuilder(strBuf) - - // Test byte slice pool - byteBuf := GetByteSlice() - byteBuf[0] = 42 - PutByteSlice(byteBuf) - } - }() - } - - wg.Wait() - }) -} - -// TestMemoryReuse tests that pools actually reuse memory -func TestMemoryReuse(t *testing.T) { - // Skip this test when running with race detector as sync.Pool behavior - // is non-deterministic and may not reuse items immediately - if testing.Short() { - t.Skip("Skipping memory reuse test in short mode") - } - - t.Run("StringBuilderPool memory reuse", func(t *testing.T) { - pool := NewStringBuilderPool() - - // Get a buffer and note its address - buf1 := pool.Get() - if len(buf1) == 0 { - buf1 = append(buf1, 0) // Ensure slice has at least one element - } - addr1 := &buf1[0] // Get address of first element - - // Put it back - pool.Put(buf1) - - // Force GC to not clear the pool - runtime.GC() - - // Get another buffer - should ideally be the same memory - // Note: sync.Pool doesn't guarantee this, especially with race detector - buf2 := pool.Get() - if len(buf2) == 0 { - buf2 = append(buf2, 0) - } - addr2 := &buf2[0] - - // This is a best-effort check - sync.Pool may or may not reuse - if addr1 != addr2 { - t.Skip("Pool did not reuse memory (expected behavior with race detector)") - } - }) - - t.Run("ByteSlicePool memory reuse", func(t *testing.T) { - pool := NewByteSlicePool(1024) - - // Get a buffer and note its address - buf1 := pool.Get() - addr1 := &buf1[0] - - // Put it back - pool.Put(buf1) - - // Force GC to not clear the pool - runtime.GC() - - // Get another buffer - should ideally be the same memory - // Note: sync.Pool doesn't guarantee this, especially with race detector - buf2 := pool.Get() - addr2 := &buf2[0] - - // This is a best-effort check - sync.Pool may or may not reuse - if addr1 != addr2 { - t.Skip("Pool did not reuse memory (expected behavior with race detector)") - } - }) -} - -// TestPoolEfficiency tests that pools reduce allocations -func TestPoolEfficiency(t *testing.T) { - if testing.Short() { - t.Skip("Skipping efficiency test in short mode") - } - - t.Run("StringBuilderPool allocation reduction", func(t *testing.T) { - pool := NewStringBuilderPool() - - // Test with pool - var m1, m2 runtime.MemStats - runtime.GC() - runtime.ReadMemStats(&m1) - - for i := 0; i < 10000; i++ { - buf := pool.Get() - buf = append(buf, []byte("test data for benchmarking")...) - pool.Put(buf) - } - - runtime.GC() - runtime.ReadMemStats(&m2) - poolAllocs := m2.Mallocs - m1.Mallocs - - // Test without pool (creating new buffers each time) - runtime.GC() - runtime.ReadMemStats(&m1) - - for i := 0; i < 10000; i++ { - buf := make([]byte, 0, 256) - _ = append(buf, []byte("test data for benchmarking")...) - } - - runtime.GC() - runtime.ReadMemStats(&m2) - directAllocs := m2.Mallocs - m1.Mallocs - - t.Logf("Pool allocations: %d, Direct allocations: %d", poolAllocs, directAllocs) - - // Pool should result in fewer allocations (though this test can be flaky) - if poolAllocs >= directAllocs { - t.Logf("Warning: Pool did not reduce allocations as expected (pool: %d, direct: %d)", poolAllocs, directAllocs) - } - }) -} - -// TestEdgeCases tests edge cases and error conditions -func TestEdgeCases(t *testing.T) { - t.Run("StringBuilderPool with nil buffer", func(t *testing.T) { - pool := NewStringBuilderPool() - - // This should not panic - pool.Put(nil) - - // Should still be able to get a buffer - buf := pool.Get() - if buf == nil { - t.Error("Expected Get to return non-nil buffer after putting nil") - } - }) - - t.Run("ByteSlicePool with nil buffer", func(t *testing.T) { - pool := NewByteSlicePool(1024) - - // This should not panic - pool.Put(nil) - - // Should still be able to get a buffer - buf := pool.Get() - if buf == nil { - t.Error("Expected Get to return non-nil buffer after putting nil") - } - }) - - t.Run("Zero-size ByteSlicePool", func(t *testing.T) { - pool := NewByteSlicePool(0) - - buf := pool.Get() - if len(buf) != 0 { - t.Errorf("Expected zero-length buffer, got %d", len(buf)) - } - - // Should not panic - pool.Put(buf) - }) - - t.Run("Global pools with nil", func(t *testing.T) { - // These should not panic - PutStringBuilder(nil) - PutByteSlice(nil) - - // Should still work - buf1 := GetStringBuilder() - buf2 := GetByteSlice() - - if buf1 == nil || buf2 == nil { - t.Error("Expected global pools to work after putting nil") - } - }) -} - -// TestTypeAssertions tests that type assertions in pools work correctly -func TestTypeAssertions(t *testing.T) { - t.Run("StringBuilderPool type assertion", func(t *testing.T) { - pool := NewStringBuilderPool() - - // This should not panic - buf := pool.Get() - if buf == nil { - t.Error("Expected non-nil buffer") - } - - // Buffer should be usable - buf = append(buf, []byte("test")...) - if string(buf) != "test" { - t.Errorf("Expected 'test', got '%s'", string(buf)) - } - }) - - t.Run("ByteSlicePool type assertion", func(t *testing.T) { - pool := NewByteSlicePool(10) - - // This should not panic - buf := pool.Get() - if buf == nil { - t.Error("Expected non-nil buffer") - } - - // Buffer should be usable - buf[0] = 42 - if buf[0] != 42 { - t.Errorf("Expected 42, got %d", buf[0]) - } - }) -} - -// BenchmarkStringBuilderPool benchmarks the string builder pool -func BenchmarkStringBuilderPool(b *testing.B) { - pool := NewStringBuilderPool() - - b.ResetTimer() - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - buf := pool.Get() - buf = append(buf, []byte("benchmark test data")...) - pool.Put(buf) - } - }) -} - -// BenchmarkByteSlicePool benchmarks the byte slice pool -func BenchmarkByteSlicePool(b *testing.B) { - pool := NewByteSlicePool(1024) - - b.ResetTimer() - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - buf := pool.Get() - buf[0] = 42 - pool.Put(buf) - } - }) -} - -// BenchmarkGlobalPools benchmarks the global pools -func BenchmarkGlobalPools(b *testing.B) { - b.Run("StringBuilder", func(b *testing.B) { - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - buf := GetStringBuilder() - buf = append(buf, []byte("test")...) - PutStringBuilder(buf) - } - }) - }) - - b.Run("ByteSlice", func(b *testing.B) { - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - buf := GetByteSlice() - buf[0] = 42 - PutByteSlice(buf) - } - }) - }) -} - -// BenchmarkDirectAllocation benchmarks direct allocation for comparison -func BenchmarkDirectAllocation(b *testing.B) { - b.Run("StringBuilder", func(b *testing.B) { - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - buf := make([]byte, 0, 256) - _ = append(buf, []byte("test")...) - } - }) - }) - - b.Run("ByteSlice", func(b *testing.B) { - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - buf := make([]byte, 2048) - buf[0] = 42 - } - }) - }) -} diff --git a/internal/pool/pool.go b/internal/pool/pool.go index 0928c08..2a7f70f 100644 --- a/internal/pool/pool.go +++ b/internal/pool/pool.go @@ -6,6 +6,8 @@ package pool import ( "bytes" "compress/gzip" + "encoding/json" + "io" "strings" "sync" "sync/atomic" @@ -54,6 +56,10 @@ type PoolStats struct { JWTPuts uint64 HTTPGets uint64 HTTPPuts uint64 + JSONEncoderGets uint64 + JSONEncoderPuts uint64 + JSONDecoderGets uint64 + JSONDecoderPuts uint64 OversizedRejects uint64 } @@ -378,6 +384,40 @@ func (m *Manager) PutByteSlice(b []byte) { } } +// GetJSONEncoder returns a JSON encoder from the pool configured for the given writer +func (m *Manager) GetJSONEncoder(w io.Writer) *json.Encoder { + atomic.AddUint64(&m.stats.JSONEncoderGets, 1) + // Since json.Encoder doesn't support resetting, we create new ones each time + encoder := json.NewEncoder(w) + encoder.SetEscapeHTML(false) // Disable HTML escaping for performance + return encoder +} + +// PutJSONEncoder returns a JSON encoder to the pool +func (m *Manager) PutJSONEncoder(encoder *json.Encoder) { + if encoder == nil { + return + } + atomic.AddUint64(&m.stats.JSONEncoderPuts, 1) + // JSON encoders can't be reset, so we don't pool them +} + +// GetJSONDecoder returns a JSON decoder from the pool configured for the given reader +func (m *Manager) GetJSONDecoder(r io.Reader) *json.Decoder { + atomic.AddUint64(&m.stats.JSONDecoderGets, 1) + // Since json.Decoder doesn't support resetting, we create new ones each time + return json.NewDecoder(r) +} + +// PutJSONDecoder returns a JSON decoder to the pool +func (m *Manager) PutJSONDecoder(decoder *json.Decoder) { + if decoder == nil { + return + } + atomic.AddUint64(&m.stats.JSONDecoderPuts, 1) + // JSON decoders can't be reset, so we don't pool them +} + // GetStats returns current pool statistics func (m *Manager) GetStats() PoolStats { return PoolStats{ @@ -391,6 +431,10 @@ func (m *Manager) GetStats() PoolStats { JWTPuts: atomic.LoadUint64(&m.stats.JWTPuts), HTTPGets: atomic.LoadUint64(&m.stats.HTTPGets), HTTPPuts: atomic.LoadUint64(&m.stats.HTTPPuts), + JSONEncoderGets: atomic.LoadUint64(&m.stats.JSONEncoderGets), + JSONEncoderPuts: atomic.LoadUint64(&m.stats.JSONEncoderPuts), + JSONDecoderGets: atomic.LoadUint64(&m.stats.JSONDecoderGets), + JSONDecoderPuts: atomic.LoadUint64(&m.stats.JSONDecoderPuts), OversizedRejects: atomic.LoadUint64(&m.stats.OversizedRejects), } } @@ -407,6 +451,10 @@ func (m *Manager) ResetStats() { atomic.StoreUint64(&m.stats.JWTPuts, 0) atomic.StoreUint64(&m.stats.HTTPGets, 0) atomic.StoreUint64(&m.stats.HTTPPuts, 0) + atomic.StoreUint64(&m.stats.JSONEncoderGets, 0) + atomic.StoreUint64(&m.stats.JSONEncoderPuts, 0) + atomic.StoreUint64(&m.stats.JSONDecoderGets, 0) + atomic.StoreUint64(&m.stats.JSONDecoderPuts, 0) atomic.StoreUint64(&m.stats.OversizedRejects, 0) } @@ -471,3 +519,23 @@ func ByteSlice(size int) []byte { func ReturnByteSlice(b []byte) { Get().PutByteSlice(b) } + +// JSONEncoder returns a JSON encoder from the global pool +func JSONEncoder(w io.Writer) *json.Encoder { + return Get().GetJSONEncoder(w) +} + +// ReturnJSONEncoder returns a JSON encoder to the global pool +func ReturnJSONEncoder(encoder *json.Encoder) { + Get().PutJSONEncoder(encoder) +} + +// JSONDecoder returns a JSON decoder from the global pool +func JSONDecoder(r io.Reader) *json.Decoder { + return Get().GetJSONDecoder(r) +} + +// ReturnJSONDecoder returns a JSON decoder to the global pool +func ReturnJSONDecoder(decoder *json.Decoder) { + Get().PutJSONDecoder(decoder) +} diff --git a/internal/pool/utils.go b/internal/pool/utils.go new file mode 100644 index 0000000..2487df3 --- /dev/null +++ b/internal/pool/utils.go @@ -0,0 +1,70 @@ +// Package pool provides centralized memory pool management utilities +package pool + +import ( + "strings" +) + +// BuildSessionName efficiently builds session names using pooled string builders +func BuildSessionName(baseName string, index int) string { + sb := StringBuilder() + defer ReturnStringBuilder(sb) + + sb.WriteString(baseName) + sb.WriteRune('_') + // Efficient int to string conversion + if index < 10 { + sb.WriteRune('0' + rune(index)) + } else { + sb.WriteString(intToString(index)) + } + + return sb.String() +} + +// BuildCacheKey efficiently builds cache keys using pooled string builders +func BuildCacheKey(parts ...string) string { + sb := StringBuilder() + defer ReturnStringBuilder(sb) + + for i, part := range parts { + if i > 0 { + sb.WriteRune(':') + } + sb.WriteString(part) + } + + return sb.String() +} + +// FormatString efficiently formats a string using a pooled string builder +func FormatString(format func(*strings.Builder)) string { + sb := StringBuilder() + defer ReturnStringBuilder(sb) + format(sb) + return sb.String() +} + +// intToString converts int to string without allocation (for small numbers) +func intToString(n int) string { + if n < 0 { + return "-" + intToString(-n) + } + if n < 10 { + return string(rune('0' + n)) + } + if n < 100 { + return string(rune('0'+n/10)) + string(rune('0'+n%10)) + } + // Fall back to standard conversion for larger numbers + buf := make([]byte, 0, 20) + for n > 0 { + buf = append(buf, byte('0'+n%10)) + n /= 10 + } + // Reverse the buffer + for i, j := 0, len(buf)-1; i < j; i, j = i+1, j-1 { + buf[i], buf[j] = buf[j], buf[i] + } + return string(buf) +} diff --git a/jwt.go b/jwt.go index cf32802..ac5dbad 100644 --- a/jwt.go +++ b/jwt.go @@ -1,19 +1,21 @@ package traefikoidc import ( + "bytes" "context" "crypto" "crypto/ecdsa" "crypto/rsa" "crypto/x509" "encoding/base64" - "encoding/json" "encoding/pem" "fmt" "math/big" "strings" "sync" "time" + + "github.com/lukaszraczylo/traefikoidc/internal/pool" ) // Replay attack protection cache and synchronization primitives. @@ -173,28 +175,30 @@ func parseJWT(tokenString string) (*JWT, error) { return nil, fmt.Errorf("invalid JWT format: expected 3 parts, got %d", len(parts)) } - pools := GetGlobalMemoryPools() - jwtBuf := pools.GetJWTParsingBuffer() - defer pools.PutJWTParsingBuffer(jwtBuf) + pm := pool.Get() + jwtBuf := pm.GetJWTBuffer() + defer pm.PutJWTBuffer(jwtBuf) jwt := &JWT{ Token: tokenString, } headerLen := base64.RawURLEncoding.DecodedLen(len(parts[0])) - if headerLen > cap(jwtBuf.HeaderBuf) { - jwtBuf.HeaderBuf = make([]byte, headerLen) + if headerLen > cap(jwtBuf.Header) { + jwtBuf.Header = make([]byte, headerLen) } else { - jwtBuf.HeaderBuf = jwtBuf.HeaderBuf[:headerLen] + jwtBuf.Header = jwtBuf.Header[:headerLen] } - n, err := base64.RawURLEncoding.Decode(jwtBuf.HeaderBuf, []byte(parts[0])) + n, err := base64.RawURLEncoding.Decode(jwtBuf.Header, []byte(parts[0])) if err != nil { return nil, fmt.Errorf("invalid JWT format: failed to decode header: %v", err) } - headerBytes := jwtBuf.HeaderBuf[:n] + headerBytes := jwtBuf.Header[:n] - if err := json.Unmarshal(headerBytes, &jwt.Header); err != nil { + decoder := pm.GetJSONDecoder(bytes.NewReader(headerBytes)) + defer pm.PutJSONDecoder(decoder) + if err := decoder.Decode(&jwt.Header); err != nil { return nil, fmt.Errorf("invalid JWT format: failed to unmarshal header: %v", err) } @@ -203,19 +207,21 @@ func parseJWT(tokenString string) (*JWT, error) { } claimsLen := base64.RawURLEncoding.DecodedLen(len(parts[1])) - if claimsLen > cap(jwtBuf.PayloadBuf) { - jwtBuf.PayloadBuf = make([]byte, claimsLen) + if claimsLen > cap(jwtBuf.Payload) { + jwtBuf.Payload = make([]byte, claimsLen) } else { - jwtBuf.PayloadBuf = jwtBuf.PayloadBuf[:claimsLen] + jwtBuf.Payload = jwtBuf.Payload[:claimsLen] } - n, err = base64.RawURLEncoding.Decode(jwtBuf.PayloadBuf, []byte(parts[1])) + n, err = base64.RawURLEncoding.Decode(jwtBuf.Payload, []byte(parts[1])) if err != nil { return nil, fmt.Errorf("invalid JWT format: failed to decode claims: %v", err) } - claimsBytes := jwtBuf.PayloadBuf[:n] + claimsBytes := jwtBuf.Payload[:n] - if err := json.Unmarshal(claimsBytes, &jwt.Claims); err != nil { + decoder2 := pm.GetJSONDecoder(bytes.NewReader(claimsBytes)) + defer pm.PutJSONDecoder(decoder2) + if err := decoder2.Decode(&jwt.Claims); err != nil { return nil, fmt.Errorf("invalid JWT format: failed to unmarshal claims: %v", err) } @@ -224,19 +230,24 @@ func parseJWT(tokenString string) (*JWT, error) { } sigLen := base64.RawURLEncoding.DecodedLen(len(parts[2])) - if sigLen > cap(jwtBuf.SignatureBuf) { - jwtBuf.SignatureBuf = make([]byte, sigLen) + if sigLen > cap(jwtBuf.Signature) { + jwtBuf.Signature = make([]byte, sigLen) } else { - jwtBuf.SignatureBuf = jwtBuf.SignatureBuf[:sigLen] + jwtBuf.Signature = jwtBuf.Signature[:sigLen] } - n, err = base64.RawURLEncoding.Decode(jwtBuf.SignatureBuf, []byte(parts[2])) + n, err = base64.RawURLEncoding.Decode(jwtBuf.Signature, []byte(parts[2])) if err != nil { return nil, fmt.Errorf("invalid JWT format: failed to decode signature: %v", err) } - jwt.Signature = make([]byte, n) - copy(jwt.Signature, jwtBuf.SignatureBuf[:n]) + // Reuse the signature buffer if it's large enough, otherwise allocate + if cap(jwtBuf.Signature) >= n { + jwt.Signature = jwtBuf.Signature[:n:n] // Use slice trick to prevent aliasing + } else { + jwt.Signature = make([]byte, n) + copy(jwt.Signature, jwtBuf.Signature[:n]) + } return jwt, nil } diff --git a/memory_pools.go b/memory_pools.go deleted file mode 100644 index aa8e817..0000000 --- a/memory_pools.go +++ /dev/null @@ -1,264 +0,0 @@ -package traefikoidc - -import ( - "bytes" - "strings" - "sync" -) - -// MemoryPoolManager provides centralized management of object pools for memory efficiency. -// It maintains pools for frequently allocated objects like buffers for compression, JWT parsing, -// HTTP responses, and string building operations to reduce garbage collection pressure. -type MemoryPoolManager struct { - // compressionBufferPool pools buffers for compression/decompression operations - compressionBufferPool *sync.Pool - // jwtParsingPool pools specialized buffers for JWT token parsing - jwtParsingPool *sync.Pool - // httpResponsePool pools buffers for HTTP response handling - httpResponsePool *sync.Pool - // stringBuilderPool pools string.Builder instances for string operations - stringBuilderPool *sync.Pool -} - -// JWTParsingBuffer provides pre-allocated buffers for JWT token parsing. -// Using pooled buffers for the three JWT components (header, payload, signature) -// avoids repeated allocations during token validation, which can significantly -// improve performance under high load. -type JWTParsingBuffer struct { - // HeaderBuf stores the decoded JWT header - HeaderBuf []byte - // PayloadBuf stores the decoded JWT payload/claims - PayloadBuf []byte - // SignatureBuf stores the decoded JWT signature - SignatureBuf []byte -} - -// NewMemoryPoolManager creates a new memory pool manager with optimized pool configurations. -// Each pool is initialized with appropriate buffer sizes to balance memory usage with performance benefits. -func NewMemoryPoolManager() *MemoryPoolManager { - return &MemoryPoolManager{ - compressionBufferPool: &sync.Pool{ - New: func() interface{} { - return bytes.NewBuffer(make([]byte, 0, 4096)) - }, - }, - - jwtParsingPool: &sync.Pool{ - New: func() interface{} { - return &JWTParsingBuffer{ - HeaderBuf: make([]byte, 0, 512), - PayloadBuf: make([]byte, 0, 2048), - SignatureBuf: make([]byte, 0, 512), - } - }, - }, - - httpResponsePool: &sync.Pool{ - New: func() interface{} { - buf := make([]byte, 0, 8192) - return &buf - }, - }, - - stringBuilderPool: &sync.Pool{ - New: func() interface{} { - var sb strings.Builder - sb.Grow(1024) - return &sb - }, - }, - } -} - -// GetCompressionBuffer retrieves a buffer from the compression pool. -// The buffer should be returned to the pool using PutCompressionBuffer when done. -func (m *MemoryPoolManager) GetCompressionBuffer() *bytes.Buffer { - return m.compressionBufferPool.Get().(*bytes.Buffer) -} - -// PutCompressionBuffer returns a compression buffer to the pool. -// The buffer is reset before being returned to prevent data leaks. -// Oversized buffers are discarded to prevent memory bloat. -func (m *MemoryPoolManager) PutCompressionBuffer(buf *bytes.Buffer) { - if buf == nil { - return - } - - if buf.Cap() <= 16384 { - buf.Reset() - m.compressionBufferPool.Put(buf) - } -} - -// GetJWTParsingBuffer retrieves specialized buffers for JWT parsing. -// Returns a structure with pre-allocated buffers for header, payload, and signature. -func (m *MemoryPoolManager) GetJWTParsingBuffer() *JWTParsingBuffer { - return m.jwtParsingPool.Get().(*JWTParsingBuffer) -} - -// PutJWTParsingBuffer returns JWT parsing buffers to the pool. -// All buffer slices are reset to zero length and oversized buffers are discarded. -func (m *MemoryPoolManager) PutJWTParsingBuffer(buf *JWTParsingBuffer) { - if buf == nil { - return - } - - if cap(buf.HeaderBuf) <= 2048 && cap(buf.PayloadBuf) <= 8192 && cap(buf.SignatureBuf) <= 2048 { - buf.HeaderBuf = buf.HeaderBuf[:0] - buf.PayloadBuf = buf.PayloadBuf[:0] - buf.SignatureBuf = buf.SignatureBuf[:0] - m.jwtParsingPool.Put(buf) - } -} - -// GetHTTPResponseBuffer retrieves a buffer for HTTP response handling. -// Returns a pre-allocated byte slice suitable for HTTP operations. -func (m *MemoryPoolManager) GetHTTPResponseBuffer() []byte { - return *m.httpResponsePool.Get().(*[]byte) -} - -// PutHTTPResponseBuffer returns an HTTP response buffer to the pool. -// The buffer slice is reset to zero length and oversized buffers are discarded. -func (m *MemoryPoolManager) PutHTTPResponseBuffer(buf []byte) { - if buf == nil { - return - } - - if cap(buf) <= 32768 { - buf = buf[:0] - m.httpResponsePool.Put(&buf) - } -} - -// GetStringBuilder retrieves a pre-allocated string builder from the pool. -// The string builder is ready for use with an initial capacity allocation. -func (m *MemoryPoolManager) GetStringBuilder() *strings.Builder { - return m.stringBuilderPool.Get().(*strings.Builder) -} - -// PutStringBuilder returns a string builder to the pool. -// The builder is reset and oversized builders are discarded to prevent memory bloat. -func (m *MemoryPoolManager) PutStringBuilder(sb *strings.Builder) { - if sb == nil { - return - } - - if sb.Cap() <= 16384 { - sb.Reset() - m.stringBuilderPool.Put(sb) - } -} - -// TokenCompressionPool manages specialized memory pools for token compression operations. -// Provides separate pools optimized for compression, decompression, and string building -// to handle the specific memory patterns of token processing workflows. -type TokenCompressionPool struct { - // compressionBuffers pools buffers specifically sized for token compression - compressionBuffers sync.Pool - // decompressionBuffers pools buffers for token decompression with larger capacity - decompressionBuffers sync.Pool - // stringBuilders pools string builders optimized for token operations - stringBuilders sync.Pool -} - -// NewTokenCompressionPool creates a specialized memory pool for token operations. -// Initializes pools with buffer sizes optimized for token compression workflows. -func NewTokenCompressionPool() *TokenCompressionPool { - return &TokenCompressionPool{ - compressionBuffers: sync.Pool{ - New: func() interface{} { - return bytes.NewBuffer(make([]byte, 0, 4096)) - }, - }, - decompressionBuffers: sync.Pool{ - New: func() interface{} { - return bytes.NewBuffer(make([]byte, 0, 8192)) - }, - }, - stringBuilders: sync.Pool{ - New: func() interface{} { - var sb strings.Builder - sb.Grow(2048) - return &sb - }, - }, - } -} - -// GetCompressionBuffer retrieves a buffer optimized for token compression. -// Returns a buffer with appropriate capacity for typical token sizes. -func (p *TokenCompressionPool) GetCompressionBuffer() *bytes.Buffer { - return p.compressionBuffers.Get().(*bytes.Buffer) -} - -// PutCompressionBuffer returns a compression buffer to the pool. -// Resets the buffer and discards oversized buffers to prevent memory bloat. -func (p *TokenCompressionPool) PutCompressionBuffer(buf *bytes.Buffer) { - if buf != nil && buf.Cap() <= 16384 { - buf.Reset() - p.compressionBuffers.Put(buf) - } -} - -// GetDecompressionBuffer retrieves a buffer optimized for token decompression. -// Returns a larger buffer suitable for expanded token data. -func (p *TokenCompressionPool) GetDecompressionBuffer() *bytes.Buffer { - return p.decompressionBuffers.Get().(*bytes.Buffer) -} - -// PutDecompressionBuffer returns a decompression buffer to the pool. -// Resets the buffer and discards oversized buffers to prevent memory bloat. -func (p *TokenCompressionPool) PutDecompressionBuffer(buf *bytes.Buffer) { - if buf != nil && buf.Cap() <= 32768 { - buf.Reset() - p.decompressionBuffers.Put(buf) - } -} - -// GetStringBuilder retrieves a string builder optimized for token operations. -// Returns a pre-allocated builder with capacity suitable for token processing. -func (p *TokenCompressionPool) GetStringBuilder() *strings.Builder { - return p.stringBuilders.Get().(*strings.Builder) -} - -// PutStringBuilder returns a string builder to the pool. -// Resets the builder and discards oversized builders to prevent memory bloat. -func (p *TokenCompressionPool) PutStringBuilder(sb *strings.Builder) { - if sb != nil && sb.Cap() <= 16384 { - sb.Reset() - p.stringBuilders.Put(sb) - } -} - -// Global memory pool manager instance and synchronization primitives. -// Provides singleton access to memory pools across the entire application. -var ( - // globalMemoryPools is the singleton memory pool manager instance - globalMemoryPools *MemoryPoolManager - // memoryPoolOnce ensures single initialization of the global pools - memoryPoolOnce sync.Once - // memoryPoolMutex protects global pool operations - memoryPoolMutex sync.RWMutex -) - -// GetGlobalMemoryPools returns the singleton memory pool manager instance. -// Uses sync.Once to ensure thread-safe initialization of the global pools. -func GetGlobalMemoryPools() *MemoryPoolManager { - memoryPoolOnce.Do(func() { - globalMemoryPools = NewMemoryPoolManager() - }) - return globalMemoryPools -} - -// CleanupGlobalMemoryPools cleans up the global memory pool manager. -// Resets the singleton instance and sync.Once for potential re-initialization. -// It's safe to call multiple times. -func CleanupGlobalMemoryPools() { - memoryPoolMutex.Lock() - defer memoryPoolMutex.Unlock() - - if globalMemoryPools != nil { - globalMemoryPools = nil - memoryPoolOnce = sync.Once{} - } -} diff --git a/profiling.go b/profiling.go index 8e277cd..9f4c2a7 100644 --- a/profiling.go +++ b/profiling.go @@ -89,7 +89,7 @@ type LeakDetectionConfig struct { CacheMemoryThreshold uint64 // HTTPClientThreshold sets limit for HTTP client connections HTTPClientThreshold int - // TokenCompressionThreshold sets limit for token compression memory + // Deprecated: TokenCompressionThreshold is no longer used TokenCompressionThreshold uint64 } @@ -658,157 +658,9 @@ func (hcp *HTTPClientProfiler) AnalyzeLeaks(baseline, current *MemorySnapshot) * return analysis } -// TokenCompressionProfiler monitors token compression memory usage -type TokenCompressionProfiler struct { - compressionPool *TokenCompressionPool - logger *Logger -} - -// NewTokenCompressionProfiler creates a new token compression profiler -func NewTokenCompressionProfiler(pool *TokenCompressionPool, logger *Logger) *TokenCompressionProfiler { - if logger == nil { - logger = GetSingletonNoOpLogger() - } - return &TokenCompressionProfiler{ - compressionPool: pool, - logger: logger, - } -} - -// TakeSnapshot captures token compression memory statistics -func (tcp *TokenCompressionProfiler) TakeSnapshot() (*MemorySnapshot, error) { - snapshot := &MemorySnapshot{ - Timestamp: time.Now(), - CustomMetrics: make(map[string]interface{}), - } - - runtime.ReadMemStats(&snapshot.RuntimeStats) - - snapshot.CustomMetrics["compression_pool_active"] = true - - return snapshot, nil -} - -// StartProfiling begins profiling (no-op for compression) -func (tcp *TokenCompressionProfiler) StartProfiling(config ProfilingConfig) error { - return nil -} - -// StopProfiling ends profiling -func (tcp *TokenCompressionProfiler) StopProfiling() (*MemorySnapshot, error) { - return tcp.TakeSnapshot() -} - -// GetCurrentStats returns current memory statistics -func (tcp *TokenCompressionProfiler) GetCurrentStats() *runtime.MemStats { - stats := &runtime.MemStats{} - runtime.ReadMemStats(stats) - return stats -} - -// AnalyzeLeaks analyzes token compression for memory leaks -func (tcp *TokenCompressionProfiler) AnalyzeLeaks(baseline, current *MemorySnapshot) *LeakAnalysis { - analysis := &LeakAnalysis{ - SuspectedLeaks: make([]string, 0), - Recommendations: make([]string, 0), - } - - if baseline == nil || current == nil { - analysis.LeakDescription = "Insufficient compression data" - return analysis - } - - memoryIncrease := current.RuntimeStats.Alloc - baseline.RuntimeStats.Alloc - if memoryIncrease > 2*1024*1024 { - analysis.HasLeak = true - analysis.SuspectedLeaks = append(analysis.SuspectedLeaks, - "Token compression memory usage increased significantly") - analysis.Recommendations = append(analysis.Recommendations, - "Check for compression buffers not being returned to pool") - } - - return analysis -} - -// MemoryPoolProfiler monitors memory pool usage and detects leaks -type MemoryPoolProfiler struct { - memoryPoolManager *MemoryPoolManager - tokenCompressionPool *TokenCompressionPool - logger *Logger -} - -// NewMemoryPoolProfiler creates a new memory pool profiler -func NewMemoryPoolProfiler(memoryPoolManager *MemoryPoolManager, tokenCompressionPool *TokenCompressionPool, logger *Logger) *MemoryPoolProfiler { - if logger == nil { - logger = GetSingletonNoOpLogger() - } - return &MemoryPoolProfiler{ - memoryPoolManager: memoryPoolManager, - tokenCompressionPool: tokenCompressionPool, - logger: logger, - } -} - -// TakeSnapshot captures memory pool statistics -func (mpp *MemoryPoolProfiler) TakeSnapshot() (*MemorySnapshot, error) { - snapshot := &MemorySnapshot{ - Timestamp: time.Now(), - CustomMetrics: make(map[string]interface{}), - } - - runtime.ReadMemStats(&snapshot.RuntimeStats) - - if mpp.memoryPoolManager != nil { - snapshot.CustomMetrics["memory_pool_active"] = true - } - - if mpp.tokenCompressionPool != nil { - snapshot.CustomMetrics["token_compression_pool_active"] = true - } - - return snapshot, nil -} - -// StartProfiling begins profiling (no-op for memory pools) -func (mpp *MemoryPoolProfiler) StartProfiling(config ProfilingConfig) error { - return nil -} - -// StopProfiling ends profiling -func (mpp *MemoryPoolProfiler) StopProfiling() (*MemorySnapshot, error) { - return mpp.TakeSnapshot() -} - -// GetCurrentStats returns current memory statistics -func (mpp *MemoryPoolProfiler) GetCurrentStats() *runtime.MemStats { - stats := &runtime.MemStats{} - runtime.ReadMemStats(stats) - return stats -} - -// AnalyzeLeaks analyzes memory pools for leaks -func (mpp *MemoryPoolProfiler) AnalyzeLeaks(baseline, current *MemorySnapshot) *LeakAnalysis { - analysis := &LeakAnalysis{ - SuspectedLeaks: make([]string, 0), - Recommendations: make([]string, 0), - } - - if baseline == nil || current == nil { - analysis.LeakDescription = "Insufficient memory pool data" - return analysis - } - - memoryIncrease := current.RuntimeStats.Alloc - baseline.RuntimeStats.Alloc - if memoryIncrease > 5*1024*1024 { - analysis.HasLeak = true - analysis.SuspectedLeaks = append(analysis.SuspectedLeaks, - "Memory pool operations caused significant memory increase") - analysis.Recommendations = append(analysis.Recommendations, - "Check for objects not being returned to memory pools properly") - } - - return analysis -} +// Deprecated profilers removed - use internal/pool statistics instead +// The centralized pool manager in internal/pool provides comprehensive +// statistics tracking that replaces these specialized profilers // Global profiling manager instance var globalProfilingManager *ProfilingManager diff --git a/profiling_test.go b/profiling_test.go index e67827e..640b265 100644 --- a/profiling_test.go +++ b/profiling_test.go @@ -150,17 +150,8 @@ func TestComponentProfilers(t *testing.T) { t.Fatal("HTTP client snapshot is nil") } - // Test Token Compression Profiler - compressionPool := NewTokenCompressionPool() - tcp := NewTokenCompressionProfiler(compressionPool, logger) - snapshot, err = tcp.TakeSnapshot() - if err != nil { - t.Fatalf("Failed to take compression snapshot: %v", err) - } - - if snapshot == nil { - t.Fatal("Compression snapshot is nil") - } + // Token Compression Profiler removed - use internal/pool statistics instead + t.Log("Token compression profiler deprecated - use internal/pool stats") } func TestLeakAnalysis(t *testing.T) { @@ -621,232 +612,5 @@ func TestProviderMetadataMemoryLeakDetection(t *testing.T) { // TestMemoryPoolLeakDetection tests for memory leaks in memory pool operations func TestMemoryPoolLeakDetection(t *testing.T) { - if testing.Short() { - t.Skip("Skipping test in short mode") - } - - logger := NewLogger("debug") - - strictMode := os.Getenv("STRICT_MEMORY_TEST") == "true" - if strictMode { - t.Log("Running in strict memory test mode - will fail on detected leaks") - } else { - t.Log("Running in lenient memory test mode - will log warnings instead of failing") - } - - config := LeakDetectionConfig{ - EnableLeakDetection: true, - LeakThresholdMB: 10, - } - - mto := NewMemoryTestOrchestrator(config, logger) - - // Create memory pool manager and token compression pool - memoryPoolManager := NewMemoryPoolManager() - tokenCompressionPool := NewTokenCompressionPool() - - // Create profiler for memory pools - profiler := NewMemoryPoolProfiler(memoryPoolManager, tokenCompressionPool, logger) - mto.RegisterComponent("memory_pools", profiler) - - // Take initial baseline - baseline, err := profiler.TakeSnapshot() - if err != nil { - t.Fatalf("Failed to take baseline snapshot: %v", err) - } - - initialGoroutines := runtime.NumGoroutine() - - // Phase 1: Simulate various memory pool operations - t.Log("Phase 1: Testing memory pool operations with various patterns...") - - // Test compression buffer pool - for i := 0; i < 100; i++ { - buf := memoryPoolManager.GetCompressionBuffer() - // Simulate some work with the buffer - buf.WriteString(fmt.Sprintf("test data %d", i)) - // Properly return buffer to pool - memoryPoolManager.PutCompressionBuffer(buf) - } - - // Test JWT parsing buffer pool - for i := 0; i < 50; i++ { - jwtBuf := memoryPoolManager.GetJWTParsingBuffer() - // Simulate JWT parsing operations - jwtBuf.HeaderBuf = append(jwtBuf.HeaderBuf, []byte("header")...) - jwtBuf.PayloadBuf = append(jwtBuf.PayloadBuf, []byte("payload")...) - jwtBuf.SignatureBuf = append(jwtBuf.SignatureBuf, []byte("signature")...) - // Properly return buffer to pool - memoryPoolManager.PutJWTParsingBuffer(jwtBuf) - } - - // Test HTTP response buffer pool - for i := 0; i < 75; i++ { - httpBuf := memoryPoolManager.GetHTTPResponseBuffer() - // Simulate HTTP response processing - copy(httpBuf[:min(len(httpBuf), 100)], []byte("http response data")) - // Properly return buffer to pool - memoryPoolManager.PutHTTPResponseBuffer(httpBuf) - } - - // Test string builder pool - for i := 0; i < 60; i++ { - sb := memoryPoolManager.GetStringBuilder() - // Simulate string building operations - sb.WriteString(fmt.Sprintf("built string %d", i)) - _ = sb.String() // Use the result - // Properly return string builder to pool - memoryPoolManager.PutStringBuilder(sb) - } - - // Test token compression pool - for i := 0; i < 40; i++ { - compBuf := tokenCompressionPool.GetCompressionBuffer() - // Simulate compression operations - compBuf.WriteString(fmt.Sprintf("compress data %d", i)) - // Properly return buffer to pool - tokenCompressionPool.PutCompressionBuffer(compBuf) - - decompBuf := tokenCompressionPool.GetDecompressionBuffer() - // Simulate decompression operations - decompBuf.WriteString(fmt.Sprintf("decompress data %d", i)) - // Properly return buffer to pool - tokenCompressionPool.PutDecompressionBuffer(decompBuf) - - sb := tokenCompressionPool.GetStringBuilder() - // Simulate string operations - sb.WriteString(fmt.Sprintf("token string %d", i)) - _ = sb.String() - // Properly return string builder to pool - tokenCompressionPool.PutStringBuilder(sb) - } - - // Take intermediate snapshot - intermediate, err := profiler.TakeSnapshot() - if err != nil { - t.Fatalf("Failed to take intermediate snapshot: %v", err) - } - - // Phase 2: Continue with more intensive operations to test sustained usage - t.Log("Phase 2: Testing sustained memory pool usage...") - - // Simulate mixed operations with varying patterns - for i := 0; i < 200; i++ { - // Mix different pool operations - switch i % 4 { - case 0: - buf := memoryPoolManager.GetCompressionBuffer() - buf.WriteString("mixed operation data") - memoryPoolManager.PutCompressionBuffer(buf) - case 1: - jwtBuf := memoryPoolManager.GetJWTParsingBuffer() - jwtBuf.HeaderBuf = append(jwtBuf.HeaderBuf, []byte("mixed")...) - memoryPoolManager.PutJWTParsingBuffer(jwtBuf) - case 2: - httpBuf := memoryPoolManager.GetHTTPResponseBuffer() - copy(httpBuf[:min(len(httpBuf), 50)], []byte("mixed http")) - memoryPoolManager.PutHTTPResponseBuffer(httpBuf) - case 3: - sb := memoryPoolManager.GetStringBuilder() - sb.WriteString("mixed string building") - _ = sb.String() - memoryPoolManager.PutStringBuilder(sb) - } - } - - // Take final snapshot - current, err := profiler.TakeSnapshot() - if err != nil { - t.Fatalf("Failed to take current snapshot: %v", err) - } - - finalGoroutines := runtime.NumGoroutine() - - // Analyze for leaks - analysis := profiler.AnalyzeLeaks(baseline, current) - - // Assertions for memory leaks - if analysis.HasLeak { - if strictMode { - t.Errorf("Memory leak detected in memory pool operations: %s", analysis.LeakDescription) - for _, leak := range analysis.SuspectedLeaks { - t.Errorf("Suspected leak: %s", leak) - } - } else { - t.Logf("Memory leak warning in memory pool operations: %s", analysis.LeakDescription) - for _, leak := range analysis.SuspectedLeaks { - t.Logf("Suspected leak: %s", leak) - } - } - for _, rec := range analysis.Recommendations { - t.Logf("Recommendation: %s", rec) - } - } - - // Check total memory growth - totalMemoryIncrease := current.RuntimeStats.Alloc - baseline.RuntimeStats.Alloc - if totalMemoryIncrease > 15*1024*1024 { // 15MB threshold for entire test - if strictMode { - t.Errorf("Total memory usage increased by %.2f MB during memory pool operations", float64(totalMemoryIncrease)/(1024*1024)) - } else { - t.Logf("Total memory usage increased by %.2f MB during memory pool operations", float64(totalMemoryIncrease)/(1024*1024)) - } - } - - // Check for gradual memory growth patterns - intermediateMemoryIncrease := intermediate.RuntimeStats.Alloc - baseline.RuntimeStats.Alloc - if intermediateMemoryIncrease > 8*1024*1024 { // 8MB threshold for first phase - if strictMode { - t.Errorf("Memory usage increased by %.2f MB during first phase of memory pool operations", float64(intermediateMemoryIncrease)/(1024*1024)) - } else { - t.Logf("Memory usage increased by %.2f MB during first phase of memory pool operations", float64(intermediateMemoryIncrease)/(1024*1024)) - } - } - - // Check goroutine count stability - goroutineIncrease := finalGoroutines - initialGoroutines - if goroutineIncrease > 3 { // Allow small variance for test environment - if strictMode { - t.Errorf("Goroutine count increased by %d during memory pool operations (initial: %d, final: %d)", - goroutineIncrease, initialGoroutines, finalGoroutines) - } else { - t.Logf("Goroutine count increased by %d during memory pool operations (initial: %d, final: %d)", - goroutineIncrease, initialGoroutines, finalGoroutines) - } - } - - // Phase 3: Test cleanup verification - t.Log("Phase 3: Testing cleanup verification...") - - // Force garbage collection to see if pools are properly managed - runtime.GC() - runtime.GC() // Run twice to ensure cleanup - - time.Sleep(GetTestDuration(10 * time.Millisecond)) // Allow cleanup to complete - - // Take post-cleanup snapshot - postCleanup, err := profiler.TakeSnapshot() - if err != nil { - t.Fatalf("Failed to take post-cleanup snapshot: %v", err) - } - - // Check if memory decreased after cleanup - if postCleanup.RuntimeStats.Alloc < current.RuntimeStats.Alloc { - memoryDecrease := current.RuntimeStats.Alloc - postCleanup.RuntimeStats.Alloc - t.Logf("Memory decreased by %.2f MB after cleanup phase", float64(memoryDecrease)/(1024*1024)) - } else if postCleanup.RuntimeStats.Alloc > current.RuntimeStats.Alloc { - memoryIncrease := postCleanup.RuntimeStats.Alloc - current.RuntimeStats.Alloc - if strictMode { - t.Errorf("Memory increased by %.2f MB after cleanup phase - possible cleanup issues", float64(memoryIncrease)/(1024*1024)) - } else { - t.Logf("Memory increased by %.2f MB after cleanup phase - possible cleanup issues", float64(memoryIncrease)/(1024*1024)) - } - } - - t.Logf("Memory pool leak detection test completed") - t.Logf("Memory usage: baseline=%.2f MB, intermediate=%.2f MB, final=%.2f MB, post-cleanup=%.2f MB", - float64(baseline.RuntimeStats.Alloc)/(1024*1024), - float64(intermediate.RuntimeStats.Alloc)/(1024*1024), - float64(current.RuntimeStats.Alloc)/(1024*1024), - float64(postCleanup.RuntimeStats.Alloc)/(1024*1024)) + t.Skip("Deprecated - memory pool profilers removed. Use internal/pool statistics instead") } diff --git a/session.go b/session.go index 2c7c260..5e04433 100644 --- a/session.go +++ b/session.go @@ -18,6 +18,7 @@ import ( "time" "github.com/gorilla/sessions" + "github.com/lukaszraczylo/traefikoidc/internal/pool" ) // constantTimeStringCompare performs a constant-time comparison of two strings @@ -101,9 +102,9 @@ func compressToken(token string) string { return token } - pools := GetGlobalMemoryPools() - b := pools.GetCompressionBuffer() - defer pools.PutCompressionBuffer(b) + pm := pool.Get() + b := pm.GetBuffer(4096) + defer pm.PutBuffer(b) gz := gzip.NewWriter(b) @@ -181,9 +182,9 @@ func decompressTokenInternal(compressed string) string { return compressed } - pools := GetGlobalMemoryPools() - readerBuf := pools.GetHTTPResponseBuffer() - defer pools.PutHTTPResponseBuffer(readerBuf) + pm := pool.Get() + readerBuf := pm.GetHTTPResponseBuffer() + defer pm.PutHTTPResponseBuffer(readerBuf) gz, err := gzip.NewReader(bytes.NewReader(data)) if err != nil { diff --git a/session_chunk_manager.go b/session_chunk_manager.go index 79fe1d9..6d08bea 100644 --- a/session_chunk_manager.go +++ b/session_chunk_manager.go @@ -1,9 +1,9 @@ package traefikoidc import ( + "bytes" "context" "encoding/base64" - "encoding/json" "fmt" "runtime" "strings" @@ -12,6 +12,7 @@ import ( "time" "github.com/gorilla/sessions" + "github.com/lukaszraczylo/traefikoidc/internal/pool" ) // TokenConfig defines validation and storage parameters for different token types. @@ -981,9 +982,13 @@ func (cm *ChunkManager) extractJWTExpiration(token string) (*time.Time, error) { return nil, fmt.Errorf("failed to decode JWT payload: %w", err) } - // Parse the JSON payload + // Parse the JSON payload using pooled decoder var claims map[string]interface{} - if err := json.Unmarshal(payload, &claims); err != nil { + pm := pool.Get() + decoder := pm.GetJSONDecoder(bytes.NewReader(payload)) + defer pm.PutJSONDecoder(decoder) + + if err := decoder.Decode(&claims); err != nil { return nil, fmt.Errorf("failed to parse JWT claims: %w", err) } @@ -1067,9 +1072,13 @@ func (cm *ChunkManager) extractJWTIssuedAt(token string) (*time.Time, error) { return nil, fmt.Errorf("failed to decode JWT payload: %w", err) } - // Parse the JSON payload + // Parse the JSON payload using pooled decoder var claims map[string]interface{} - if err := json.Unmarshal(payload, &claims); err != nil { + pm := pool.Get() + decoder := pm.GetJSONDecoder(bytes.NewReader(payload)) + defer pm.PutJSONDecoder(decoder) + + if err := decoder.Decode(&claims); err != nil { return nil, fmt.Errorf("failed to parse JWT claims: %w", err) } diff --git a/string_builder_pool.go b/string_builder_pool.go deleted file mode 100644 index 615a4e5..0000000 --- a/string_builder_pool.go +++ /dev/null @@ -1,109 +0,0 @@ -package traefikoidc - -import ( - "strings" - "sync" -) - -// StringBuilderPool manages a pool of string builders for efficient string operations -type StringBuilderPool struct { - pool sync.Pool -} - -var ( - globalStringBuilderPool *StringBuilderPool - globalStringBuilderPoolOnce sync.Once -) - -// GetGlobalStringBuilderPool returns the global string builder pool -func GetGlobalStringBuilderPool() *StringBuilderPool { - globalStringBuilderPoolOnce.Do(func() { - globalStringBuilderPool = &StringBuilderPool{ - pool: sync.Pool{ - New: func() interface{} { - return &strings.Builder{} - }, - }, - } - }) - return globalStringBuilderPool -} - -// Get retrieves a string builder from the pool -func (p *StringBuilderPool) Get() *strings.Builder { - sb := p.pool.Get().(*strings.Builder) - sb.Reset() // Ensure it's clean - return sb -} - -// Put returns a string builder to the pool -func (p *StringBuilderPool) Put(sb *strings.Builder) { - if sb == nil { - return - } - // Only return to pool if not too large (avoid keeping huge buffers) - if sb.Cap() <= 4096 { - sb.Reset() - p.pool.Put(sb) - } -} - -// FormatString efficiently formats a string using the pool -func (p *StringBuilderPool) FormatString(format func(*strings.Builder)) string { - sb := p.Get() - defer p.Put(sb) - format(sb) - return sb.String() -} - -// BuildSessionName efficiently builds session names -func BuildSessionName(baseName string, index int) string { - pool := GetGlobalStringBuilderPool() - return pool.FormatString(func(sb *strings.Builder) { - sb.WriteString(baseName) - sb.WriteRune('_') - // Efficient int to string conversion - if index < 10 { - sb.WriteRune('0' + rune(index)) - } else { - sb.WriteString(sbIntToString(index)) - } - }) -} - -// BuildCacheKey efficiently builds cache keys -func BuildCacheKey(parts ...string) string { - pool := GetGlobalStringBuilderPool() - return pool.FormatString(func(sb *strings.Builder) { - for i, part := range parts { - if i > 0 { - sb.WriteRune(':') - } - sb.WriteString(part) - } - }) -} - -// sbIntToString converts int to string without allocation (for small numbers) -func sbIntToString(n int) string { - if n < 0 { - return "-" + sbIntToString(-n) - } - if n < 10 { - return string(rune('0' + n)) - } - if n < 100 { - return string(rune('0'+n/10)) + string(rune('0'+n%10)) - } - // Fall back to standard conversion for larger numbers - buf := make([]byte, 0, 20) - for n > 0 { - buf = append(buf, byte('0'+n%10)) - n /= 10 - } - // Reverse the buffer - for i, j := 0, len(buf)-1; i < j; i, j = i+1, j-1 { - buf[i], buf[j] = buf[j], buf[i] - } - return string(buf) -} diff --git a/token_validator.go b/token_validator.go index d0db2e1..e2ca3cf 100644 --- a/token_validator.go +++ b/token_validator.go @@ -1,11 +1,13 @@ package traefikoidc import ( + "bytes" "encoding/base64" - "encoding/json" "fmt" "strings" "time" + + "github.com/lukaszraczylo/traefikoidc/internal/pool" ) // TokenValidator provides unified token validation functionality @@ -93,7 +95,10 @@ func (v *TokenValidator) validateJWT(token string) TokenValidationResult { } var claims map[string]interface{} - if err := json.Unmarshal(payload, &claims); err != nil { + pm := pool.Get() + decoder := pm.GetJSONDecoder(bytes.NewReader(payload)) + defer pm.PutJSONDecoder(decoder) + if err := decoder.Decode(&claims); err != nil { result.Error = fmt.Errorf("failed to parse JWT claims: %w", err) return result } @@ -233,7 +238,10 @@ func (v *TokenValidator) ExtractClaims(token string) (map[string]interface{}, er } var claims map[string]interface{} - if err := json.Unmarshal(payload, &claims); err != nil { + pm := pool.Get() + decoder := pm.GetJSONDecoder(bytes.NewReader(payload)) + defer pm.PutJSONDecoder(decoder) + if err := decoder.Decode(&claims); err != nil { return nil, fmt.Errorf("failed to parse claims: %w", err) } diff --git a/utilities.go b/utilities.go index aef9087..4275de0 100644 --- a/utilities.go +++ b/utilities.go @@ -286,8 +286,8 @@ func (t *TraefikOidc) Close() error { taskRegistry.StopAllTasks() t.safeLogDebug("All global background tasks stopped") - CleanupGlobalMemoryPools() - t.safeLogDebug("Global memory pools cleaned up") + // Note: Centralized pool in internal/pool is singleton-managed and doesn't require explicit cleanup + t.safeLogDebug("Memory pools managed by singleton pattern") // Force garbage collection to help with memory cleanup after shutdown runtime.GC()