mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-10-17 04:37:48 +00:00
Pooling cleanup.
This commit is contained in:
Vendored
+17
-3
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user