Performance Optimization
This guide covers advanced performance optimization techniques for CVSS Parser, including benchmarking, memory management, and concurrent processing strategies.
Overview
Performance optimization is crucial for applications that process large volumes of CVSS vectors. This guide covers:
- Benchmarking and profiling
- Memory optimization
- Concurrent processing
- Caching strategies
- Batch processing
- Resource pooling
Benchmarking
Basic Benchmarks
go
package main
import (
"testing"
"github.com/scagogogo/cvss-parser/pkg/cvss"
"github.com/scagogogo/cvss-parser/pkg/parser"
)
func BenchmarkVectorParsing(b *testing.B) {
vectorStr := "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H"
b.ResetTimer()
for i := 0; i < b.N; i++ {
parser := parser.NewCvss3xParser(vectorStr)
_, err := parser.Parse()
if err != nil {
b.Fatal(err)
}
}
}
func BenchmarkScoreCalculation(b *testing.B) {
vectorStr := "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H"
parser := parser.NewCvss3xParser(vectorStr)
vector, _ := parser.Parse()
b.ResetTimer()
for i := 0; i < b.N; i++ {
calculator := cvss.NewCalculator(vector)
_, err := calculator.Calculate()
if err != nil {
b.Fatal(err)
}
}
}
func BenchmarkEndToEnd(b *testing.B) {
vectorStr := "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H"
b.ResetTimer()
for i := 0; i < b.N; i++ {
parser := parser.NewCvss3xParser(vectorStr)
vector, err := parser.Parse()
if err != nil {
b.Fatal(err)
}
calculator := cvss.NewCalculator(vector)
_, err = calculator.Calculate()
if err != nil {
b.Fatal(err)
}
}
}
Memory Benchmarks
go
func BenchmarkMemoryAllocation(b *testing.B) {
vectorStr := "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H"
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
parser := parser.NewCvss3xParser(vectorStr)
vector, _ := parser.Parse()
calculator := cvss.NewCalculator(vector)
calculator.Calculate()
}
}
Memory Optimization
Object Pooling
go
import "sync"
var parserPool = sync.Pool{
New: func() interface{} {
return parser.NewCvss3xParser("")
},
}
var calculatorPool = sync.Pool{
New: func() interface{} {
return &cvss.Calculator{}
},
}
func ProcessVectorOptimized(vectorStr string) (float64, error) {
// Get parser from pool
p := parserPool.Get().(*parser.Cvss3xParser)
defer parserPool.Put(p)
// Reset and use parser
p.SetVector(vectorStr)
vector, err := p.Parse()
if err != nil {
return 0, err
}
// Get calculator from pool
calc := calculatorPool.Get().(*cvss.Calculator)
defer calculatorPool.Put(calc)
// Reset and use calculator
calc.SetVector(vector)
return calc.Calculate()
}
Memory-Efficient Batch Processing
go
func ProcessVectorsBatch(vectors []string, batchSize int) ([]float64, error) {
results := make([]float64, 0, len(vectors))
for i := 0; i < len(vectors); i += batchSize {
end := i + batchSize
if end > len(vectors) {
end = len(vectors)
}
batch := vectors[i:end]
batchResults, err := processBatch(batch)
if err != nil {
return nil, err
}
results = append(results, batchResults...)
// Force GC after each batch to manage memory
if i%10000 == 0 {
runtime.GC()
}
}
return results, nil
}
func processBatch(vectors []string) ([]float64, error) {
results := make([]float64, len(vectors))
for i, vectorStr := range vectors {
score, err := ProcessVectorOptimized(vectorStr)
if err != nil {
return nil, err
}
results[i] = score
}
return results, nil
}
Concurrent Processing
Worker Pool Pattern
go
type VectorJob struct {
Vector string
Index int
}
type VectorResult struct {
Score float64
Index int
Error error
}
func ProcessVectorsConcurrent(vectors []string, numWorkers int) ([]float64, error) {
jobs := make(chan VectorJob, len(vectors))
results := make(chan VectorResult, len(vectors))
// Start workers
for w := 0; w < numWorkers; w++ {
go vectorWorker(jobs, results)
}
// Send jobs
for i, vector := range vectors {
jobs <- VectorJob{Vector: vector, Index: i}
}
close(jobs)
// Collect results
scores := make([]float64, len(vectors))
for i := 0; i < len(vectors); i++ {
result := <-results
if result.Error != nil {
return nil, result.Error
}
scores[result.Index] = result.Score
}
return scores, nil
}
func vectorWorker(jobs <-chan VectorJob, results chan<- VectorResult) {
for job := range jobs {
score, err := ProcessVectorOptimized(job.Vector)
results <- VectorResult{
Score: score,
Index: job.Index,
Error: err,
}
}
}
Pipeline Processing
go
func ProcessVectorsPipeline(vectors []string) <-chan VectorResult {
results := make(chan VectorResult)
go func() {
defer close(results)
// Stage 1: Parse vectors
parsed := parseVectorsPipeline(vectors)
// Stage 2: Calculate scores
for vector := range parsed {
if vector.Error != nil {
results <- VectorResult{Error: vector.Error, Index: vector.Index}
continue
}
calculator := cvss.NewCalculator(vector.Vector)
score, err := calculator.Calculate()
results <- VectorResult{
Score: score,
Index: vector.Index,
Error: err,
}
}
}()
return results
}
type ParsedVector struct {
Vector *cvss.Cvss3x
Index int
Error error
}
func parseVectorsPipeline(vectors []string) <-chan ParsedVector {
parsed := make(chan ParsedVector)
go func() {
defer close(parsed)
for i, vectorStr := range vectors {
parser := parser.NewCvss3xParser(vectorStr)
vector, err := parser.Parse()
parsed <- ParsedVector{
Vector: vector,
Index: i,
Error: err,
}
}
}()
return parsed
}
Caching Strategies
LRU Cache Implementation
go
import "container/list"
type LRUCache struct {
capacity int
cache map[string]*list.Element
list *list.List
mutex sync.RWMutex
}
type CacheEntry struct {
key string
value float64
}
func NewLRUCache(capacity int) *LRUCache {
return &LRUCache{
capacity: capacity,
cache: make(map[string]*list.Element),
list: list.New(),
}
}
func (c *LRUCache) Get(key string) (float64, bool) {
c.mutex.RLock()
defer c.mutex.RUnlock()
if elem, exists := c.cache[key]; exists {
c.list.MoveToFront(elem)
return elem.Value.(*CacheEntry).value, true
}
return 0, false
}
func (c *LRUCache) Put(key string, value float64) {
c.mutex.Lock()
defer c.mutex.Unlock()
if elem, exists := c.cache[key]; exists {
c.list.MoveToFront(elem)
elem.Value.(*CacheEntry).value = value
return
}
if c.list.Len() >= c.capacity {
oldest := c.list.Back()
if oldest != nil {
c.list.Remove(oldest)
delete(c.cache, oldest.Value.(*CacheEntry).key)
}
}
entry := &CacheEntry{key: key, value: value}
elem := c.list.PushFront(entry)
c.cache[key] = elem
}
// Cached vector processor
type CachedProcessor struct {
cache *LRUCache
}
func NewCachedProcessor(cacheSize int) *CachedProcessor {
return &CachedProcessor{
cache: NewLRUCache(cacheSize),
}
}
func (cp *CachedProcessor) ProcessVector(vectorStr string) (float64, error) {
// Check cache first
if score, found := cp.cache.Get(vectorStr); found {
return score, nil
}
// Process vector
score, err := ProcessVectorOptimized(vectorStr)
if err != nil {
return 0, err
}
// Cache result
cp.cache.Put(vectorStr, score)
return score, nil
}
Profiling and Monitoring
CPU Profiling
go
import (
"os"
"runtime/pprof"
)
func ProfileCPU(filename string, fn func()) error {
f, err := os.Create(filename)
if err != nil {
return err
}
defer f.Close()
if err := pprof.StartCPUProfile(f); err != nil {
return err
}
defer pprof.StopCPUProfile()
fn()
return nil
}
// Usage
func main() {
vectors := generateTestVectors(100000)
err := ProfileCPU("cpu.prof", func() {
ProcessVectorsConcurrent(vectors, 8)
})
if err != nil {
log.Fatal(err)
}
}
Memory Profiling
go
func ProfileMemory(filename string, fn func()) error {
fn()
f, err := os.Create(filename)
if err != nil {
return err
}
defer f.Close()
runtime.GC()
return pprof.WriteHeapProfile(f)
}
Performance Metrics
go
type PerformanceMetrics struct {
ProcessedVectors int64
TotalDuration time.Duration
ErrorCount int64
CacheHits int64
CacheMisses int64
mutex sync.RWMutex
}
func (pm *PerformanceMetrics) RecordProcessing(duration time.Duration, err error) {
pm.mutex.Lock()
defer pm.mutex.Unlock()
pm.ProcessedVectors++
pm.TotalDuration += duration
if err != nil {
pm.ErrorCount++
}
}
func (pm *PerformanceMetrics) RecordCacheHit() {
pm.mutex.Lock()
defer pm.mutex.Unlock()
pm.CacheHits++
}
func (pm *PerformanceMetrics) RecordCacheMiss() {
pm.mutex.Lock()
defer pm.mutex.Unlock()
pm.CacheMisses++
}
func (pm *PerformanceMetrics) GetStats() (float64, float64, float64) {
pm.mutex.RLock()
defer pm.mutex.RUnlock()
avgDuration := float64(pm.TotalDuration) / float64(pm.ProcessedVectors)
errorRate := float64(pm.ErrorCount) / float64(pm.ProcessedVectors)
cacheHitRate := float64(pm.CacheHits) / float64(pm.CacheHits + pm.CacheMisses)
return avgDuration, errorRate, cacheHitRate
}
Best Practices
1. Choose the Right Approach
- Single vectors: Direct processing
- Small batches (< 1000): Simple batch processing
- Large batches (> 1000): Concurrent processing with worker pools
- Streaming data: Pipeline processing
2. Memory Management
- Use object pools for frequently created objects
- Process data in batches to control memory usage
- Force GC periodically for long-running processes
- Monitor memory usage with profiling
3. Caching Strategy
- Cache parsed vectors for repeated processing
- Use LRU cache to limit memory usage
- Consider cache invalidation strategies
- Monitor cache hit rates
4. Concurrent Processing
- Use worker pools for CPU-bound tasks
- Limit the number of goroutines to avoid overhead
- Use buffered channels to prevent blocking
- Handle errors gracefully in concurrent code
Performance Testing
Load Testing
go
func TestHighLoad(t *testing.T) {
vectors := generateTestVectors(100000)
start := time.Now()
results, err := ProcessVectorsConcurrent(vectors, runtime.NumCPU())
duration := time.Since(start)
require.NoError(t, err)
require.Len(t, results, len(vectors))
rate := float64(len(vectors)) / duration.Seconds()
t.Logf("Processed %d vectors in %v (%.0f vectors/sec)",
len(vectors), duration, rate)
// Assert minimum performance
assert.Greater(t, rate, 1000.0, "Processing rate should be > 1000 vectors/sec")
}
Stress Testing
go
func TestMemoryStress(t *testing.T) {
var m1, m2 runtime.MemStats
runtime.GC()
runtime.ReadMemStats(&m1)
// Process large number of vectors
for i := 0; i < 10; i++ {
vectors := generateTestVectors(10000)
_, err := ProcessVectorsBatch(vectors, 1000)
require.NoError(t, err)
}
runtime.GC()
runtime.ReadMemStats(&m2)
memIncrease := m2.Alloc - m1.Alloc
t.Logf("Memory increase: %d KB", memIncrease/1024)
// Assert memory usage is reasonable
assert.Less(t, memIncrease, uint64(10*1024*1024),
"Memory increase should be < 10MB")
}
Next Steps
After optimizing performance, you can explore:
- Production Deployment - Enterprise deployment patterns
- Monitoring and Alerting - Production monitoring
- Testing Guide - Comprehensive testing strategies
Related Documentation
- Benchmarking Guide - Detailed performance analysis
- Memory Management - Advanced memory optimization
- Concurrent Programming - Go concurrency patterns