proto-socket/go/bench/stress.go

913 lines
26 KiB
Go

//go:build ignore
// Same-language Go stress / benchmark harness.
//
// 고성능 병렬 운용 기준선 Milestone의 lang-baseline Task에서, TypeScript stress.ts와
// 같은 출력 계약(ROW|/SKIP|/SUMMARY|)으로 Go same-language TCP/WS baseline을 남긴다.
// 절대 성능 합격선은 고정하지 않고 local 환경 baseline(throughput, p50/p95/p99 latency,
// peak heap)을 기록하며, 안정성 합격선만 hard fail로 둔다: timeout 0, nonce mismatch 0,
// response type mismatch 0, connection별 FIFO 위반 0, 종료 후 pending leak 0.
//
// 실행: go run ./bench/stress.go [--mode=quick|full] [--transport=tcp,ws] [--profiles=a,b]
package main
import (
"context"
"fmt"
"math/rand"
"net"
"os"
"runtime"
"sort"
"strings"
"sync"
"sync/atomic"
"time"
"google.golang.org/protobuf/proto"
"nhooyr.io/websocket"
toki "git.toki-labs.com/toki/proto-socket/go"
"git.toki-labs.com/toki/proto-socket/go/packets"
)
const (
host = "127.0.0.1"
language = "Go"
wsPath = "/"
transTCP = "tcp"
transWS = "ws"
)
var allProfiles = []string{"roundtrip", "burst", "sustained", "parallel", "slow-mix", "payload"}
// payload size matrix 축. quick은 작은 샘플(1KB/64KB), full은 Milestone 기준(1KB/64KB/1MB)을 측정한다.
const payloadSeed = "0123456789abcdef"
var (
payloadSizesQuick = []int{1024, 65536}
payloadSizesFull = []int{1024, 65536, 1048576}
)
// stability는 안정성 위반 카운터다. 0이 아니면 해당 row는 FAIL이고 프로세스도 non-zero exit한다.
type stability struct {
timeouts atomic.Int64
nonceMismatch atomic.Int64
typeMismatch atomic.Int64
fifoViolations atomic.Int64
pendingLeak atomic.Int64
}
func (s *stability) violations() int64 {
return s.timeouts.Load() + s.nonceMismatch.Load() + s.typeMismatch.Load() +
s.fifoViolations.Load() + s.pendingLeak.Load()
}
type clientHandle struct {
comm *toki.Communicator
send func(proto.Message) error
close func() error
}
type serverHandle struct {
port int
stop func()
}
var (
rowsEmitted int
totalViol int64
)
func parserMap() toki.ParserMap {
return toki.ParserMap{
toki.TypeNameOf(&packets.TestData{}): func(b []byte) (proto.Message, error) {
m := &packets.TestData{}
return m, proto.Unmarshal(b, m)
},
}
}
func logf(format string, args ...any) {
fmt.Fprintf(os.Stderr, format+"\n", args...)
}
func memMb() float64 {
var m runtime.MemStats
runtime.ReadMemStats(&m)
return float64(m.HeapAlloc) / (1024 * 1024)
}
func freePort() (int, error) {
l, err := net.Listen("tcp", host+":0")
if err != nil {
return 0, err
}
port := l.Addr().(*net.TCPAddr).Port
_ = l.Close()
return port, nil
}
func testDataPayloadBytes(message string) int {
b, err := proto.Marshal(&packets.TestData{Index: 0, Message: message})
if err != nil {
return 0
}
return len(b)
}
// payloadFiller는 size 바이트 길이의 deterministic filler string을 만든다.
func payloadFiller(size int) string {
if size <= 0 {
return ""
}
var b strings.Builder
b.Grow(size + len(payloadSeed))
for b.Len() < size {
b.WriteString(payloadSeed)
}
return b.String()[:size]
}
// payloadLabel은 payload 크기를 사람이 읽는 축 이름으로 바꾼다(1024 -> 1KB, 1048576 -> 1MB).
func payloadLabel(bytes int) string {
switch {
case bytes >= 1048576 && bytes%1048576 == 0:
return fmt.Sprintf("%dMB", bytes/1048576)
case bytes >= 1024 && bytes%1024 == 0:
return fmt.Sprintf("%dKB", bytes/1024)
default:
return fmt.Sprintf("%dB", bytes)
}
}
func percentile(sorted []float64, p float64) float64 {
if len(sorted) == 0 {
return 0
}
rank := int((p/100.0)*float64(len(sorted))+0.999999) - 1
if rank < 0 {
rank = 0
}
if rank > len(sorted)-1 {
rank = len(sorted) - 1
}
return sorted[rank]
}
type rowInput struct {
profile string
axis string
transport string
payloadBytes int
clientCount int
requests int
throughput float64
p50, p95, p99 float64
memMb float64
stab *stability
}
func emitRow(r rowInput) {
viol := r.stab.violations()
status := "PASS"
if viol != 0 {
status = "FAIL"
}
totalViol += viol
rowsEmitted++
fields := []string{
"ROW",
r.profile,
r.axis,
language,
r.transport,
fmt.Sprintf("%d", r.payloadBytes),
fmt.Sprintf("%d", r.clientCount),
fmt.Sprintf("%d", r.requests),
fmt.Sprintf("%.1f", r.throughput),
fmt.Sprintf("%.3f", r.p50),
fmt.Sprintf("%.3f", r.p95),
fmt.Sprintf("%.3f", r.p99),
fmt.Sprintf("%d", r.stab.timeouts.Load()),
fmt.Sprintf("%d", r.stab.nonceMismatch.Load()),
fmt.Sprintf("%d", r.stab.typeMismatch.Load()),
fmt.Sprintf("%d", r.stab.fifoViolations.Load()),
fmt.Sprintf("%d", r.stab.pendingLeak.Load()),
"0", // queueBacklog: same-language baseline에는 inbound gateway가 없다.
"0", // gatewayBacklog
"off",
fmt.Sprintf("%.1f", r.memMb),
status,
}
fmt.Println(strings.Join(fields, "|"))
}
func emitSkip(profile, axis, transport, reason string) {
fmt.Printf("SKIP|%s|%s|%s|%s|%s\n", profile, axis, language, transport, reason)
}
func classifyRequestError(err error, s *stability) {
msg := err.Error()
switch {
case strings.Contains(msg, "timeout") || strings.Contains(msg, "deadline"):
s.timeouts.Add(1)
case strings.Contains(msg, "type mismatch"):
s.typeMismatch.Add(1)
default:
s.nonceMismatch.Add(1)
}
}
// startRequestServer는 request-response echo 서버를 띄운다.
//
// request-response 축에서는 client가 concurrency C로 동시에 send하므로 server의 frame 도착 순서가
// 결정적이지 않다. 따라서 여기서 req.index 단조 증가를 FIFO 위반으로 보면 안 된다. per-connection
// FIFO는 단일 connection에서 순차 send하는 burst 축에서 검증한다. 여기서는 응답 정확성(nonce/type
// matching)과 timeout만 안정성 합격선으로 둔다.
func startRequestServer(ctx context.Context, transport string, s *stability) (*serverHandle, error) {
port, err := freePort()
if err != nil {
return nil, err
}
register := func(comm *toki.Communicator) {
toki.AddRequestListenerTyped[*packets.TestData, *packets.TestData](comm, func(req *packets.TestData) (*packets.TestData, error) {
return &packets.TestData{Index: req.GetIndex() * 2, Message: "echo:" + req.GetMessage()}, nil
})
}
return startServer(ctx, transport, port, register)
}
func startServer(ctx context.Context, transport string, port int, onConnected func(*toki.Communicator)) (*serverHandle, error) {
switch transport {
case transTCP:
srv := toki.NewTcpServer(host, port, func(conn net.Conn) *toki.TcpClient {
return toki.NewTcpClient(conn, 0, 0, parserMap())
})
srv.OnClientConnected = func(client *toki.TcpClient) { onConnected(&client.Communicator) }
if err := srv.Start(ctx); err != nil {
return nil, err
}
return &serverHandle{port: port, stop: func() { _ = srv.Stop() }}, nil
case transWS:
srv := toki.NewWsServer(host, port, wsPath, func(conn *websocket.Conn) *toki.WsClient {
return toki.NewWsClient(conn, 0, 0, parserMap())
})
srv.OnClientConnected = func(client *toki.WsClient) { onConnected(&client.Communicator) }
if err := srv.Start(ctx); err != nil {
return nil, err
}
return &serverHandle{port: port, stop: func() { _ = srv.Stop() }}, nil
default:
return nil, fmt.Errorf("unknown transport %q", transport)
}
}
func dial(ctx context.Context, transport string, port int) (*clientHandle, error) {
switch transport {
case transTCP:
c, err := toki.DialTcp(ctx, host, port, 0, 0, parserMap())
if err != nil {
return nil, err
}
return &clientHandle{comm: &c.Communicator, send: c.Send, close: c.Close}, nil
case transWS:
c, err := toki.DialWsWithHeartbeat(ctx, host, port, wsPath, 0, 0, parserMap())
if err != nil {
return nil, err
}
return &clientHandle{comm: &c.Communicator, send: c.Send, close: c.Close}, nil
default:
return nil, fmt.Errorf("unknown transport %q", transport)
}
}
func dialWithRetry(transport string, port int) (*clientHandle, error) {
deadline := time.Now().Add(3 * time.Second)
var lastErr error
for time.Now().Before(deadline) {
ctx, cancel := context.WithTimeout(context.Background(), 300*time.Millisecond)
h, err := dial(ctx, transport, port)
cancel()
if err == nil {
return h, nil
}
lastErr = err
time.Sleep(50 * time.Millisecond)
}
return nil, fmt.Errorf("connect %s:%d timed out: %w", transport, port, lastErr)
}
// runRequestLoad는 한 connection에서 concurrency C로 request batch를 보내고 latency를 모은다.
func runRequestLoad(h *clientHandle, total, concurrency int, timeout time.Duration, s *stability) []float64 {
var mu sync.Mutex
latencies := make([]float64, 0, total)
issued := 0
nextIndex := int32(0)
for issued < total {
batchSize := concurrency
if total-issued < batchSize {
batchSize = total - issued
}
var wg sync.WaitGroup
for i := 0; i < batchSize; i++ {
idx := nextIndex
nextIndex++
wg.Add(1)
go func(index int32) {
defer wg.Done()
started := time.Now()
res, err := toki.SendRequestTyped[*packets.TestData, *packets.TestData](
h.comm,
&packets.TestData{Index: index, Message: fmt.Sprintf("req-%d", index)},
timeout,
)
if err != nil {
classifyRequestError(err, s)
return
}
elapsed := float64(time.Since(started).Microseconds()) / 1000.0
mu.Lock()
latencies = append(latencies, elapsed)
mu.Unlock()
if res.GetIndex() != index*2 || res.GetMessage() != fmt.Sprintf("echo:req-%d", index) {
s.nonceMismatch.Add(1)
}
}(idx)
}
issued += batchSize
wg.Wait()
}
return latencies
}
const maxSustainedLatencySamples = 100000
type boundedLatencyCollector struct {
mu sync.Mutex
samples []float64
count int
maxSamples int
rng *rand.Rand
}
func newBoundedLatencyCollector(maxSamples int) *boundedLatencyCollector {
return &boundedLatencyCollector{
samples: make([]float64, 0, 4096),
maxSamples: maxSamples,
rng: rand.New(rand.NewSource(42)),
}
}
func (c *boundedLatencyCollector) Add(elapsed float64) {
c.mu.Lock()
defer c.mu.Unlock()
c.count++
if len(c.samples) < c.maxSamples {
c.samples = append(c.samples, elapsed)
} else {
j := c.rng.Intn(c.count)
if j < c.maxSamples {
c.samples[j] = elapsed
}
}
}
type collectorSnapshot struct {
samples []float64
count int
}
func (c *boundedLatencyCollector) Snapshot() collectorSnapshot {
c.mu.Lock()
defer c.mu.Unlock()
samplesCopy := append([]float64(nil), c.samples...)
return collectorSnapshot{
samples: samplesCopy,
count: c.count,
}
}
func summarizeWithCount(profile, axis, transport string, samples []float64, totalCount int, elapsedMs float64, clientCount int, s *stability, observedMem float64) {
sorted := append([]float64(nil), samples...)
sort.Float64s(sorted)
throughput := 0.0
if elapsedMs > 0 {
throughput = float64(totalCount) / elapsedMs * 1000.0
}
emitRow(rowInput{
profile: profile,
axis: axis,
transport: transport,
payloadBytes: testDataPayloadBytes("req-0"),
clientCount: clientCount,
requests: totalCount,
throughput: throughput,
p50: percentile(sorted, 50),
p95: percentile(sorted, 95),
p99: percentile(sorted, 99),
memMb: observedMem,
stab: s,
})
}
func summarize(profile, axis, transport string, latencies []float64, elapsedMs float64, clientCount int, s *stability, observedMem float64) {
sorted := append([]float64(nil), latencies...)
sort.Float64s(sorted)
throughput := 0.0
if elapsedMs > 0 {
throughput = float64(len(latencies)) / elapsedMs * 1000.0
}
emitRow(rowInput{
profile: profile,
axis: axis,
transport: transport,
payloadBytes: testDataPayloadBytes("req-0"),
clientCount: clientCount,
requests: len(latencies),
throughput: throughput,
p50: percentile(sorted, 50),
p95: percentile(sorted, 95),
p99: percentile(sorted, 99),
memMb: observedMem,
stab: s,
})
}
func profileRoundtrip(transport string, mode string) {
concurrencies := []int{1, 16, 64, 256}
batches := 2
timeout := 5 * time.Second
if mode == "full" {
batches = 20
timeout = 15 * time.Second
}
logf("[roundtrip] transport=%s mode=%s concurrencies=1,16,64,256 batches/level=%d", transport, mode, batches)
for _, c := range concurrencies {
s := &stability{}
total := c * batches
ctx, cancel := context.WithCancel(context.Background())
srv, err := startRequestServer(ctx, transport, s)
if err != nil {
cancel()
s.timeouts.Add(1)
logf("[roundtrip] c=%d server error=%v", c, err)
summarize("roundtrip", fmt.Sprintf("concurrency=%d", c), transport, nil, 0, 1, s, memMb())
continue
}
h, err := dialWithRetry(transport, srv.port)
if err != nil {
s.timeouts.Add(1)
logf("[roundtrip] c=%d dial error=%v", c, err)
} else {
started := time.Now()
latencies := runRequestLoad(h, total, c, timeout, s)
elapsed := float64(time.Since(started).Microseconds()) / 1000.0
summarize("roundtrip", fmt.Sprintf("concurrency=%d", c), transport, latencies, elapsed, 1, s, memMb())
_ = h.close()
if h.comm.IsAlive() {
s.pendingLeak.Add(1)
}
}
srv.stop()
cancel()
logf("[roundtrip] transport=%s concurrency=%d done violations=%d", transport, c, s.violations())
}
}
func profileBurst(transport string, mode string) {
counts := []int{200}
if mode == "full" {
counts = []int{1000, 10000, 100000}
}
logf("[burst] transport=%s mode=%s counts=%v", transport, mode, counts)
for _, count := range counts {
s := &stability{}
var received atomic.Int64
var lastIndex atomic.Int64
lastIndex.Store(-1)
done := make(chan struct{})
var doneOnce sync.Once
ctx, cancel := context.WithCancel(context.Background())
srv, err := startServer(ctx, transport, mustPort(), func(comm *toki.Communicator) {
toki.AddListenerTyped[*packets.TestData](comm, func(msg *packets.TestData) {
if int64(msg.GetIndex()) <= lastIndex.Load() {
s.fifoViolations.Add(1)
}
lastIndex.Store(int64(msg.GetIndex()))
if received.Add(1) >= int64(count) {
doneOnce.Do(func() { close(done) })
}
})
})
if err != nil {
cancel()
s.timeouts.Add(1)
logf("[burst] count=%d server error=%v", count, err)
emitThroughput("burst", fmt.Sprintf("count=%d", count), transport, 0, 0, 1, testDataPayloadBytes("b-0"), s, memMb())
continue
}
h, err := dialWithRetry(transport, srv.port)
if err != nil {
s.timeouts.Add(1)
srv.stop()
cancel()
emitThroughput("burst", fmt.Sprintf("count=%d", count), transport, 0, 0, 1, testDataPayloadBytes("b-0"), s, memMb())
continue
}
started := time.Now()
for i := 0; i < count; i++ {
if err := h.send(&packets.TestData{Index: int32(i), Message: fmt.Sprintf("b-%d", i)}); err != nil {
s.timeouts.Add(1)
break
}
}
timeout := 10 * time.Second
if mode == "full" {
timeout = 60 * time.Second
}
select {
case <-done:
case <-time.After(timeout):
s.timeouts.Add(1)
logf("[burst] count=%d dispatch timeout received=%d", count, received.Load())
}
elapsed := float64(time.Since(started).Microseconds()) / 1000.0
if received.Load() != int64(count) {
s.pendingLeak.Add(1)
}
emitThroughput("burst", fmt.Sprintf("count=%d", count), transport, count, elapsed, 1, testDataPayloadBytes("b-0"), s, memMb())
_ = h.close()
if h.comm.IsAlive() {
s.pendingLeak.Add(1)
}
srv.stop()
cancel()
logf("[burst] transport=%s count=%d received=%d violations=%d", transport, count, received.Load(), s.violations())
}
}
func emitThroughput(profile, axis, transport string, count int, elapsedMs float64, clientCount, payloadBytes int, s *stability, observedMem float64) {
throughput := 0.0
if elapsedMs > 0 {
throughput = float64(count) / elapsedMs * 1000.0
}
emitRow(rowInput{
profile: profile,
axis: axis,
transport: transport,
payloadBytes: payloadBytes,
clientCount: clientCount,
requests: count,
throughput: throughput,
memMb: observedMem,
stab: s,
})
}
func profileSustained(transport string, mode string) {
durationsMs := []int{2000}
if mode == "full" {
durationsMs = []int{30000, 300000, 1800000}
}
concurrency := 16
timeout := 15 * time.Second
logf("[sustained] transport=%s mode=%s durations=%vms concurrency=%d", transport, mode, durationsMs, concurrency)
for _, durationMs := range durationsMs {
s := &stability{}
peak := memMb()
ctx, cancel := context.WithCancel(context.Background())
srv, err := startRequestServer(ctx, transport, s)
if err != nil {
cancel()
s.timeouts.Add(1)
summarize("sustained", fmt.Sprintf("duration=%dms", durationMs), transport, nil, 0, 1, s, peak)
continue
}
h, err := dialWithRetry(transport, srv.port)
if err != nil {
s.timeouts.Add(1)
summarize("sustained", fmt.Sprintf("duration=%dms", durationMs), transport, nil, 0, 1, s, peak)
srv.stop()
cancel()
continue
}
// To avoid unbounded memory growth during long sustained runs, we use a bounded
// latency collector that performs deterministic reservoir sampling.
// Note: The memory metric for sustained runs is influenced by this sample cap constraint.
collector := newBoundedLatencyCollector(maxSustainedLatencySamples)
nextIndex := int32(0)
deadline := time.Now().Add(time.Duration(durationMs) * time.Millisecond)
for time.Now().Before(deadline) {
var wg sync.WaitGroup
for i := 0; i < concurrency; i++ {
idx := nextIndex
nextIndex++
wg.Add(1)
go func(index int32) {
defer wg.Done()
started := time.Now()
res, err := toki.SendRequestTyped[*packets.TestData, *packets.TestData](
h.comm, &packets.TestData{Index: index, Message: fmt.Sprintf("s-%d", index)}, timeout)
if err != nil {
classifyRequestError(err, s)
return
}
elapsed := float64(time.Since(started).Microseconds()) / 1000.0
collector.Add(elapsed)
if res.GetIndex() != index*2 || res.GetMessage() != fmt.Sprintf("echo:s-%d", index) {
s.nonceMismatch.Add(1)
}
}(idx)
}
wg.Wait()
if cur := memMb(); cur > peak {
peak = cur
}
}
snapshot := collector.Snapshot()
summarizeWithCount("sustained", fmt.Sprintf("duration=%dms", durationMs), transport, snapshot.samples, snapshot.count, float64(durationMs), 1, s, peak)
_ = h.close()
if h.comm.IsAlive() {
s.pendingLeak.Add(1)
}
srv.stop()
cancel()
logf("[sustained] transport=%s duration=%dms done peakMemMb=%.1f violations=%d", transport, durationMs, peak, s.violations())
}
}
func profileParallel(transport string, mode string) {
clientCounts := []int{4}
perClient := 50
if mode == "full" {
clientCounts = []int{16, 128, 512, 1024}
perClient = 500
}
concurrency := 8
timeout := 15 * time.Second
logf("[parallel] transport=%s mode=%s clients=%v perClient=%d", transport, mode, clientCounts, perClient)
for _, clientCount := range clientCounts {
s := &stability{}
ctx, cancel := context.WithCancel(context.Background())
srv, err := startRequestServer(ctx, transport, s)
if err != nil {
s.timeouts.Add(1)
summarize("parallel", fmt.Sprintf("clients=%d", clientCount), transport, nil, 0, clientCount, s, memMb())
cancel()
continue
}
handles := make([]*clientHandle, 0, clientCount)
for i := 0; i < clientCount; i++ {
h, err := dialWithRetry(transport, srv.port)
if err != nil {
s.timeouts.Add(1)
continue
}
handles = append(handles, h)
}
var mu sync.Mutex
allLatencies := make([]float64, 0, clientCount*perClient)
started := time.Now()
var wg sync.WaitGroup
for _, h := range handles {
wg.Add(1)
go func(h *clientHandle) {
defer wg.Done()
lat := runRequestLoad(h, perClient, concurrency, timeout, s)
mu.Lock()
allLatencies = append(allLatencies, lat...)
mu.Unlock()
}(h)
}
wg.Wait()
elapsed := float64(time.Since(started).Microseconds()) / 1000.0
summarize("parallel", fmt.Sprintf("clients=%d", clientCount), transport, allLatencies, elapsed, clientCount, s, memMb())
for _, h := range handles {
_ = h.close()
if h.comm.IsAlive() {
s.pendingLeak.Add(1)
}
}
srv.stop()
cancel()
logf("[parallel] transport=%s clients=%d done violations=%d", transport, clientCount, s.violations())
}
}
func profileSlowMix(transport string, mode string) {
clientCount := 4
perClient := 20
slowEvery := 5
delayMs := 50
if mode == "full" {
clientCount = 16
perClient = 200
slowEvery = 10
delayMs = 100
}
axis := fmt.Sprintf("clients=%d,slowEvery=%d,delayMs=%d", clientCount, slowEvery, delayMs)
logf("[slow-mix] transport=%s mode=%s %s perClient=%d", transport, mode, axis, perClient)
reason := "deferred: optimize Epic — Go public request API does not expose deterministic same-connection in-flight slow/fast response-arrival order; concurrent SendRequestTyped completion order is goroutine-scheduling dependent"
emitSkip("slow-mix", axis, transport, reason)
logf("[slow-mix] transport=%s deferred=%s", transport, reason)
}
// profilePayload는 payload size matrix를 측정한다. 동일 connection request-response를 1KB/64KB/1MB
// payload에서 돌리고, message field를 deterministic filler로 채워 실제 serialized payload bytes,
// payload별 p50/p95/p99 latency, throughput, peak heap, stability counters를 결과 row에 남긴다.
func profilePayload(transport string, mode string) {
sizes := payloadSizesQuick
concurrency := 4
requests := 20
timeout := 10 * time.Second
if mode == "full" {
sizes = payloadSizesFull
concurrency = 8
requests = 100
timeout = 30 * time.Second
}
labels := make([]string, len(sizes))
for i, sz := range sizes {
labels[i] = payloadLabel(sz)
}
logf("[payload] transport=%s mode=%s sizes=%s concurrency=%d requests/size=%d",
transport, mode, strings.Join(labels, ","), concurrency, requests)
for _, size := range sizes {
s := &stability{}
filler := payloadFiller(size)
expectedEcho := "echo:" + filler
payloadBytes := testDataPayloadBytes(filler)
axis := "payload=" + payloadLabel(size)
peak := memMb()
ctx, cancel := context.WithCancel(context.Background())
srv, err := startRequestServer(ctx, transport, s)
if err != nil {
cancel()
s.timeouts.Add(1)
logf("[payload] size=%s server error=%v", payloadLabel(size), err)
emitRow(rowInput{profile: "payload", axis: axis, transport: transport, payloadBytes: payloadBytes, clientCount: 1, memMb: peak, stab: s})
continue
}
h, err := dialWithRetry(transport, srv.port)
if err != nil {
s.timeouts.Add(1)
logf("[payload] size=%s dial error=%v", payloadLabel(size), err)
emitRow(rowInput{profile: "payload", axis: axis, transport: transport, payloadBytes: payloadBytes, clientCount: 1, memMb: peak, stab: s})
srv.stop()
cancel()
continue
}
var mu sync.Mutex
latencies := make([]float64, 0, requests)
issued := 0
nextIndex := int32(0)
started := time.Now()
for issued < requests {
batchSize := concurrency
if requests-issued < batchSize {
batchSize = requests - issued
}
var wg sync.WaitGroup
for i := 0; i < batchSize; i++ {
idx := nextIndex
nextIndex++
wg.Add(1)
go func(index int32) {
defer wg.Done()
reqStart := time.Now()
res, err := toki.SendRequestTyped[*packets.TestData, *packets.TestData](
h.comm, &packets.TestData{Index: index, Message: filler}, timeout)
if err != nil {
classifyRequestError(err, s)
return
}
elapsed := float64(time.Since(reqStart).Microseconds()) / 1000.0
mu.Lock()
latencies = append(latencies, elapsed)
mu.Unlock()
if res.GetIndex() != index*2 || res.GetMessage() != expectedEcho {
s.nonceMismatch.Add(1)
}
}(idx)
}
issued += batchSize
wg.Wait()
if cur := memMb(); cur > peak {
peak = cur
}
}
elapsedMs := float64(time.Since(started).Microseconds()) / 1000.0
sorted := append([]float64(nil), latencies...)
sort.Float64s(sorted)
throughput := 0.0
if elapsedMs > 0 {
throughput = float64(len(latencies)) / elapsedMs * 1000.0
}
emitRow(rowInput{
profile: "payload",
axis: axis,
transport: transport,
payloadBytes: payloadBytes,
clientCount: 1,
requests: len(latencies),
throughput: throughput,
p50: percentile(sorted, 50),
p95: percentile(sorted, 95),
p99: percentile(sorted, 99),
memMb: peak,
stab: s,
})
_ = h.close()
if h.comm.IsAlive() {
s.pendingLeak.Add(1)
}
srv.stop()
cancel()
logf("[payload] transport=%s size=%s bytes=%d peakMemMb=%.1f violations=%d",
transport, payloadLabel(size), payloadBytes, peak, s.violations())
}
}
// mustPort는 burst 헬퍼에서 free port 확보 실패를 panic이 아니라 0으로 떨어뜨려 server start가 BLOCKED를 유도하게 한다.
func mustPort() int {
p, err := freePort()
if err != nil {
return 0
}
return p
}
func parseListArg(argv []string, prefix string, def []string) []string {
for _, a := range argv {
if strings.HasPrefix(a, prefix) {
raw := strings.Split(a[len(prefix):], ",")
out := make([]string, 0, len(raw))
for _, v := range raw {
v = strings.TrimSpace(v)
if v != "" {
out = append(out, v)
}
}
if len(out) > 0 {
return out
}
}
}
return def
}
func main() {
argv := os.Args[1:]
mode := "quick"
for _, a := range argv {
switch {
case a == "--full" || a == "--mode=full":
mode = "full"
case a == "--quick" || a == "--mode=quick":
mode = "quick"
}
}
transports := parseListArg(argv, "--transport=", []string{transTCP, transWS})
transports = parseListArg(argv, "--transports=", transports)
profiles := parseListArg(argv, "--profiles=", allProfiles)
profiles = parseListArg(argv, "--profile=", profiles)
logf("INFO stress harness language=%s mode=%s transports=%s profiles=%s typeName=%s",
language, mode, strings.Join(transports, ","), strings.Join(profiles, ","), toki.TypeNameOf(&packets.TestData{}))
runners := map[string]func(string, string){
"roundtrip": profileRoundtrip,
"burst": profileBurst,
"sustained": profileSustained,
"parallel": profileParallel,
"slow-mix": profileSlowMix,
"payload": profilePayload,
}
for _, transport := range transports {
if transport != transTCP && transport != transWS {
emitSkip("all", "transport", transport, "unsupported transport for Go same-language baseline")
continue
}
for _, p := range profiles {
runner, ok := runners[p]
if !ok {
emitSkip(p, "profile", transport, "profile not part of Go same-language baseline (gateway is TypeScript-specific)")
continue
}
runner(transport, mode)
}
}
status := "PASS"
if totalViol != 0 {
status = "FAIL"
}
fmt.Printf("SUMMARY|status=%s|language=%s|mode=%s|transports=%s|profiles=%s|rows=%d|stability_violations=%d\n",
status, language, mode, strings.Join(transports, ","), strings.Join(profiles, ","), rowsEmitted, totalViol)
if status != "PASS" {
os.Exit(1)
}
}