## 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>
259 lines
5.6 KiB
Go
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
|
|
}
|