Cast AID Agent 60785a52f5 Add SMS API load testing service for Cast
Go service that load tests the Cast SMS/OTP API with configurable
concurrent threads, target RPS, ramp-up period, and message parts.
Features live stats, final report with SMS parts tracking, and
config warnings for suboptimal test parameters.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 10:19:20 +01:00

117 lines
2.9 KiB
Go

package stats
import (
"fmt"
"sync"
"sync/atomic"
"time"
)
type Stats struct {
TotalRequests atomic.Int64
SuccessCount atomic.Int64
FailCount atomic.Int64
TotalLatencyMs atomic.Int64
MinLatencyMs atomic.Int64
MaxLatencyMs atomic.Int64
TotalSMSParts atomic.Int64
StatusCodes sync.Map
}
func (s *Stats) RecordLatency(latencyMs int64) {
s.TotalLatencyMs.Add(latencyMs)
// Update min latency
for {
cur := s.MinLatencyMs.Load()
if cur != 0 && cur <= latencyMs {
break
}
if s.MinLatencyMs.CompareAndSwap(cur, latencyMs) {
break
}
}
// Update max latency
for {
cur := s.MaxLatencyMs.Load()
if cur >= latencyMs {
break
}
if s.MaxLatencyMs.CompareAndSwap(cur, latencyMs) {
break
}
}
}
func (s *Stats) RecordStatusCode(code int) {
key := fmt.Sprintf("%d", code)
if val, ok := s.StatusCodes.Load(key); ok {
counter := val.(*atomic.Int64)
counter.Add(1)
} else {
counter := &atomic.Int64{}
counter.Add(1)
s.StatusCodes.Store(key, counter)
}
}
func (s *Stats) PrintLive(elapsed time.Duration) {
total := s.TotalRequests.Load()
success := s.SuccessCount.Load()
fail := s.FailCount.Load()
avgLatency := int64(0)
if total > 0 {
avgLatency = s.TotalLatencyMs.Load() / total
}
actualRPS := float64(total) / elapsed.Seconds()
fmt.Printf("\r[%s] Reqs: %d | OK: %d | Fail: %d | Avg: %dms | Min: %dms | Max: %dms | RPS: %.1f ",
elapsed.Round(time.Second),
total, success, fail,
avgLatency,
s.MinLatencyMs.Load(),
s.MaxLatencyMs.Load(),
actualRPS,
)
}
func (s *Stats) PrintFinalReport(duration time.Duration) {
total := s.TotalRequests.Load()
success := s.SuccessCount.Load()
fail := s.FailCount.Load()
avgLatency := int64(0)
if total > 0 {
avgLatency = s.TotalLatencyMs.Load() / total
}
fmt.Println("\n")
fmt.Println("========================================")
fmt.Println(" LOAD TEST FINAL REPORT ")
fmt.Println("========================================")
fmt.Printf("Duration: %s\n", duration.Round(time.Millisecond))
fmt.Printf("Total Requests: %d\n", total)
fmt.Printf("Successful: %d (%.1f%%)\n", success, pct(success, total))
fmt.Printf("Failed: %d (%.1f%%)\n", fail, pct(fail, total))
fmt.Printf("Avg Latency: %d ms\n", avgLatency)
fmt.Printf("Min Latency: %d ms\n", s.MinLatencyMs.Load())
fmt.Printf("Max Latency: %d ms\n", s.MaxLatencyMs.Load())
fmt.Printf("Actual RPS: %.1f\n", float64(total)/duration.Seconds())
fmt.Printf("Total SMS Parts: %d\n", s.TotalSMSParts.Load())
fmt.Println("----------------------------------------")
fmt.Println("Status Code Breakdown:")
s.StatusCodes.Range(func(key, value any) bool {
counter := value.(*atomic.Int64)
fmt.Printf(" HTTP %s: %d\n", key.(string), counter.Load())
return true
})
fmt.Println("========================================")
}
func pct(part, total int64) float64 {
if total == 0 {
return 0
}
return float64(part) / float64(total) * 100
}