stemedb/cmd/load-test/metrics.go
jordan 157dbbb9eb feat: Complete Aphoria Phase 8-9 + UAT suite (90/90 tests passing)
## Phase 8: Enterprise Extractor Improvements 
- 14 security extractors (TLS, JWT, SQL injection, XSS, etc.)
- 10 framework-specific extractors (Spring, Django, Rails, etc.)
- Config file security detection (YAML, TOML)

## Phase 9: Autonomous Extractor Generation 
- Shadow mode executor with TP/FP tracking
- Graduation pipeline with confidence thresholds
- Auto-rollback on regression detection
- Cross-project pattern syncing

## UAT Suite Complete (14 scripts, 90 tests)
- test-core-detection.sh (6 tests)
- test-declarative-extractors.sh (5 tests)
- test-domain-frameworks.sh (5 tests)
- test-domain-unreal.sh (3 tests)
- test-llm-extraction.sh (6 tests)
- test-eval-harness.sh (5 tests)
- test-cross-language.sh (3 tests)
- test-precommit-performance.sh (4 tests)
- test-output-formats.sh (8 tests)
- test-drift-detection.sh (6 tests)
- test-exit-codes.sh (12 tests)
+ 3 more scripts

## Other Changes
- Updated roadmap to mark Phase 8-9 complete
- Added .gitignore entries for build artifacts
- Updated pre-commit: 800 line limit, exclude tests/data/cmd

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-06 22:50:55 -07:00

259 lines
5.6 KiB
Go

package main
import (
"sort"
"sync"
"time"
)
// Metrics collects performance data during load tests.
type Metrics struct {
mu sync.Mutex
// Write metrics
writeLatencies []time.Duration
writeErrors int
writeCount int
// Read metrics
readLatencies []time.Duration
readErrors int
readCount int
// Timing
startTime time.Time
endTime time.Time
// Degradation tracking
baselineP99 time.Duration
loadedP99 time.Duration
degradationPct float64
readerCount int
backgroundWrite int
}
// NewMetrics creates a new Metrics collector.
func NewMetrics() *Metrics {
return &Metrics{
startTime: time.Now(),
}
}
// RecordWrite records a write operation latency and error status.
func (m *Metrics) RecordWrite(latency time.Duration, err error) {
m.mu.Lock()
defer m.mu.Unlock()
m.writeLatencies = append(m.writeLatencies, latency)
m.writeCount++
if err != nil {
m.writeErrors++
}
}
// RecordRead records a read operation latency and error status.
func (m *Metrics) RecordRead(latency time.Duration, err error) {
m.mu.Lock()
defer m.mu.Unlock()
m.readLatencies = append(m.readLatencies, latency)
m.readCount++
if err != nil {
m.readErrors++
}
}
// Stop marks the end of the test.
func (m *Metrics) Stop() {
m.mu.Lock()
defer m.mu.Unlock()
m.endTime = time.Now()
}
// Duration returns the test duration.
func (m *Metrics) Duration() time.Duration {
m.mu.Lock()
defer m.mu.Unlock()
end := m.endTime
if end.IsZero() {
end = time.Now()
}
return end.Sub(m.startTime)
}
// WriteCount returns the total number of write operations.
func (m *Metrics) WriteCount() int {
m.mu.Lock()
defer m.mu.Unlock()
return m.writeCount
}
// WriteErrors returns the number of failed write operations.
func (m *Metrics) WriteErrors() int {
m.mu.Lock()
defer m.mu.Unlock()
return m.writeErrors
}
// WriteErrorRate returns the write error rate as a percentage.
func (m *Metrics) WriteErrorRate() float64 {
m.mu.Lock()
defer m.mu.Unlock()
if m.writeCount == 0 {
return 0
}
return float64(m.writeErrors) / float64(m.writeCount) * 100
}
// WriteThroughput returns the average writes per second.
func (m *Metrics) WriteThroughput() float64 {
m.mu.Lock()
defer m.mu.Unlock()
var duration time.Duration
if m.endTime.IsZero() {
duration = time.Since(m.startTime)
} else {
duration = m.endTime.Sub(m.startTime)
}
if duration <= 0 {
return 0
}
return float64(m.writeCount) / duration.Seconds()
}
// WriteP50 returns the 50th percentile write latency.
func (m *Metrics) WriteP50() time.Duration {
return m.writePercentile(0.50)
}
// WriteP99 returns the 99th percentile write latency.
func (m *Metrics) WriteP99() time.Duration {
return m.writePercentile(0.99)
}
// WriteMax returns the maximum write latency.
func (m *Metrics) WriteMax() time.Duration {
m.mu.Lock()
defer m.mu.Unlock()
if len(m.writeLatencies) == 0 {
return 0
}
sorted := make([]time.Duration, len(m.writeLatencies))
copy(sorted, m.writeLatencies)
sort.Slice(sorted, func(i, j int) bool { return sorted[i] < sorted[j] })
return sorted[len(sorted)-1]
}
func (m *Metrics) writePercentile(p float64) time.Duration {
m.mu.Lock()
defer m.mu.Unlock()
if len(m.writeLatencies) == 0 {
return 0
}
sorted := make([]time.Duration, len(m.writeLatencies))
copy(sorted, m.writeLatencies)
sort.Slice(sorted, func(i, j int) bool { return sorted[i] < sorted[j] })
idx := int(float64(len(sorted)-1) * p)
return sorted[idx]
}
// ReadCount returns the total number of read operations.
func (m *Metrics) ReadCount() int {
m.mu.Lock()
defer m.mu.Unlock()
return m.readCount
}
// ReadErrors returns the number of failed read operations.
func (m *Metrics) ReadErrors() int {
m.mu.Lock()
defer m.mu.Unlock()
return m.readErrors
}
// ReadP50 returns the 50th percentile read latency.
func (m *Metrics) ReadP50() time.Duration {
return m.readPercentile(0.50)
}
// ReadP99 returns the 99th percentile read latency.
func (m *Metrics) ReadP99() time.Duration {
return m.readPercentile(0.99)
}
func (m *Metrics) readPercentile(p float64) time.Duration {
m.mu.Lock()
defer m.mu.Unlock()
if len(m.readLatencies) == 0 {
return 0
}
sorted := make([]time.Duration, len(m.readLatencies))
copy(sorted, m.readLatencies)
sort.Slice(sorted, func(i, j int) bool { return sorted[i] < sorted[j] })
idx := int(float64(len(sorted)-1) * p)
return sorted[idx]
}
// SetBaselineP99 records the baseline read P99 latency (no concurrent load).
func (m *Metrics) SetBaselineP99(latency time.Duration) {
m.mu.Lock()
defer m.mu.Unlock()
m.baselineP99 = latency
}
// SetLoadedP99 records the P99 read latency under concurrent load.
func (m *Metrics) SetLoadedP99(latency time.Duration) {
m.mu.Lock()
defer m.mu.Unlock()
m.loadedP99 = latency
}
// SetDegradation records the degradation percentage and test parameters.
func (m *Metrics) SetDegradation(degradationPct float64, readers, backgroundWriteRPS int) {
m.mu.Lock()
defer m.mu.Unlock()
m.degradationPct = degradationPct
m.readerCount = readers
m.backgroundWrite = backgroundWriteRPS
}
// BaselineP99 returns the baseline P99 latency.
func (m *Metrics) BaselineP99() time.Duration {
m.mu.Lock()
defer m.mu.Unlock()
return m.baselineP99
}
// LoadedP99 returns the P99 latency under load.
func (m *Metrics) LoadedP99() time.Duration {
m.mu.Lock()
defer m.mu.Unlock()
return m.loadedP99
}
// DegradationPct returns the latency degradation percentage.
func (m *Metrics) DegradationPct() float64 {
m.mu.Lock()
defer m.mu.Unlock()
return m.degradationPct
}
// ReaderCount returns the number of concurrent readers tested.
func (m *Metrics) ReaderCount() int {
m.mu.Lock()
defer m.mu.Unlock()
return m.readerCount
}