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>
117 lines
2.9 KiB
Go
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
|
|
}
|