go-ws/ws_bench_test.go
Snider 53d8a15544 test: expand Phase 0 coverage — 16 new tests, 9 benchmarks, SPDX headers
Add 16 additional test functions covering Hub.Run lifecycle (register,
broadcast delivery, unregister, duplicate unregister), Subscribe/
Unsubscribe channel management (multiple channels, idempotent, partial
leave), SendToChannel with multiple subscribers, SendProcessOutput/
SendProcessStatus edge cases (no subscribers, non-zero exit), readPump
ping timestamp verification, writePump batch message delivery, and
integration tests (unsubscribe stops delivery, broadcast reaches all,
disconnect cleans up everything, concurrent broadcast+subscribe).

Add ws_bench_test.go with 9 comprehensive benchmarks using b.Loop()
(Go 1.25+) and b.ReportAllocs(): broadcast (100 clients), channel send
(50 subscribers), parallel variants, message marshalling, WebSocket
end-to-end round-trip, subscribe/unsubscribe cycle, multi-channel
fanout, and concurrent subscriber registration.

Add SPDX-Licence-Identifier headers to ws.go, ws_test.go, ws_bench_test.go.

go vet clean, go test -race clean, 66 tests + 11 benchmarks total.

Co-Authored-By: Virgil <virgil@lethean.io>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 05:14:11 +00:00

291 lines
6.3 KiB
Go

// SPDX-Licence-Identifier: EUPL-1.2
package ws
import (
"context"
"encoding/json"
"fmt"
"net/http/httptest"
"sync"
"testing"
"github.com/gorilla/websocket"
)
// BenchmarkBroadcast_100 measures broadcast throughput with 100 connected clients.
// Uses b.Loop() (Go 1.25+) and b.ReportAllocs() for accurate profiling.
func BenchmarkBroadcast_100(b *testing.B) {
hub := NewHub()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go hub.Run(ctx)
numClients := 100
clients := make([]*Client, numClients)
for i := 0; i < numClients; i++ {
clients[i] = &Client{
hub: hub,
send: make(chan []byte, 4096),
subscriptions: make(map[string]bool),
}
hub.register <- clients[i]
}
for hub.ClientCount() < numClients {
}
msg := Message{Type: TypeEvent, Data: "bench"}
b.ResetTimer()
b.ReportAllocs()
for b.Loop() {
_ = hub.Broadcast(msg)
}
b.StopTimer()
for _, c := range clients {
for len(c.send) > 0 {
<-c.send
}
}
}
// BenchmarkSendToChannel_50 measures channel-targeted delivery with 50 subscribers.
func BenchmarkSendToChannel_50(b *testing.B) {
hub := NewHub()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go hub.Run(ctx)
numSubscribers := 50
for i := 0; i < numSubscribers; i++ {
client := &Client{
hub: hub,
send: make(chan []byte, 4096),
subscriptions: make(map[string]bool),
}
hub.mu.Lock()
hub.clients[client] = true
hub.mu.Unlock()
hub.Subscribe(client, "bench-channel")
}
msg := Message{Type: TypeEvent, Data: "bench-chan"}
b.ResetTimer()
b.ReportAllocs()
for b.Loop() {
_ = hub.SendToChannel("bench-channel", msg)
}
}
// BenchmarkBroadcast_Parallel measures concurrent broadcast throughput.
func BenchmarkBroadcast_Parallel(b *testing.B) {
hub := NewHub()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go hub.Run(ctx)
numClients := 100
clients := make([]*Client, numClients)
for i := 0; i < numClients; i++ {
clients[i] = &Client{
hub: hub,
send: make(chan []byte, 8192),
subscriptions: make(map[string]bool),
}
hub.register <- clients[i]
}
for hub.ClientCount() < numClients {
}
msg := Message{Type: TypeEvent, Data: "parallel-bench"}
b.ResetTimer()
b.ReportAllocs()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
_ = hub.Broadcast(msg)
}
})
}
// BenchmarkMarshalMessage measures the cost of JSON message serialisation.
func BenchmarkMarshalMessage(b *testing.B) {
msg := Message{
Type: TypeProcessOutput,
Channel: "process:bench-1",
ProcessID: "bench-1",
Data: "output line from the build process",
}
b.ReportAllocs()
for b.Loop() {
data, _ := json.Marshal(msg)
_ = data
}
}
// BenchmarkWebSocketEndToEnd measures a full round-trip through a real
// WebSocket connection: server broadcasts, client receives.
func BenchmarkWebSocketEndToEnd(b *testing.B) {
hub := NewHub()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go hub.Run(ctx)
server := httptest.NewServer(hub.Handler())
defer server.Close()
url := "ws" + server.URL[4:] // http -> ws
conn, _, err := websocket.DefaultDialer.Dial(url, nil)
if err != nil {
b.Fatalf("dial failed: %v", err)
}
defer conn.Close()
for hub.ClientCount() < 1 {
}
msg := Message{Type: TypeEvent, Data: "e2e-bench"}
b.ResetTimer()
b.ReportAllocs()
for b.Loop() {
if err := hub.Broadcast(msg); err != nil {
b.Fatalf("broadcast: %v", err)
}
_, _, err := conn.ReadMessage()
if err != nil {
b.Fatalf("read: %v", err)
}
}
}
// BenchmarkSubscribeUnsubscribe measures subscribe/unsubscribe cycle throughput.
func BenchmarkSubscribeUnsubscribe(b *testing.B) {
hub := NewHub()
client := &Client{
hub: hub,
send: make(chan []byte, 256),
subscriptions: make(map[string]bool),
}
hub.mu.Lock()
hub.clients[client] = true
hub.mu.Unlock()
b.ReportAllocs()
for b.Loop() {
hub.Subscribe(client, "bench-sub")
hub.Unsubscribe(client, "bench-sub")
}
}
// BenchmarkSendToChannel_Parallel measures concurrent channel sends.
func BenchmarkSendToChannel_Parallel(b *testing.B) {
hub := NewHub()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go hub.Run(ctx)
numSubscribers := 50
clients := make([]*Client, numSubscribers)
for i := 0; i < numSubscribers; i++ {
clients[i] = &Client{
hub: hub,
send: make(chan []byte, 8192),
subscriptions: make(map[string]bool),
}
hub.mu.Lock()
hub.clients[clients[i]] = true
hub.mu.Unlock()
hub.Subscribe(clients[i], "parallel-chan")
}
msg := Message{Type: TypeEvent, Data: "p-bench"}
b.ResetTimer()
b.ReportAllocs()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
_ = hub.SendToChannel("parallel-chan", msg)
}
})
}
// BenchmarkMultiChannelFanout measures broadcasting to multiple channels
// with different subscriber counts.
func BenchmarkMultiChannelFanout(b *testing.B) {
hub := NewHub()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go hub.Run(ctx)
numChannels := 10
subsPerChannel := 10
channels := make([]string, numChannels)
for ch := 0; ch < numChannels; ch++ {
channels[ch] = fmt.Sprintf("fanout-%d", ch)
for s := 0; s < subsPerChannel; s++ {
client := &Client{
hub: hub,
send: make(chan []byte, 4096),
subscriptions: make(map[string]bool),
}
hub.mu.Lock()
hub.clients[client] = true
hub.mu.Unlock()
hub.Subscribe(client, channels[ch])
}
}
msg := Message{Type: TypeEvent, Data: "fanout"}
b.ResetTimer()
b.ReportAllocs()
for b.Loop() {
for _, ch := range channels {
_ = hub.SendToChannel(ch, msg)
}
}
}
// BenchmarkConcurrentSubscribers measures the cost of subscribing many
// clients concurrently to the same channel.
func BenchmarkConcurrentSubscribers(b *testing.B) {
hub := NewHub()
b.ReportAllocs()
for b.Loop() {
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
defer wg.Done()
client := &Client{
hub: hub,
send: make(chan []byte, 1),
subscriptions: make(map[string]bool),
}
hub.Subscribe(client, "conc-sub-bench")
}()
}
wg.Wait()
// Reset for next iteration
hub.mu.Lock()
hub.channels = make(map[string]map[*Client]bool)
hub.mu.Unlock()
}
}