// Copyright (C) 2026 Dcntrlized // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published // by the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package realtime import ( "sort" "testing" "time" "github.com/google/uuid" "github.com/gorilla/websocket" ) // TestBroadcastLatency measures the fan-out path on real sockets: from the // instant Broadcast is called to the instant BOTH subscribers have decoded the // frame. It excludes the HTTP action and the database insert that precede a // trigger in production, so it is the floor for the end-to-end number. // // It logs p50/p95/max and only fails on a gross regression (p95 > 500ms) to // stay useful without being flaky on a loaded machine. func TestBroadcastLatency(t *testing.T) { alertID := uuid.New() _, wsURL, hub := startTestServer(t, alertID) first := dial(t, wsURL) second := dial(t, wsURL) waitClients(t, hub, alertID, 2) const iterations = 100 samples := make([]time.Duration, 0, iterations) for i := 0; i < iterations; i++ { eventID := uuid.NewString() frame := AlarmFrame{ Type: "alarm", AlertID: alertID.String(), EventID: eventID, Action: "trigger", Message: "latency probe", ServerTime: time.Now().UTC().Format(time.RFC3339Nano), } start := time.Now() if delivered := hub.Broadcast(alertID, frame); delivered != 2 { t.Fatalf("iteration %d: Broadcast delivered to %d clients, want 2", i, delivered) } for _, conn := range []*websocket.Conn{first, second} { got := readAlarm(t, conn) if got.EventID != eventID { t.Fatalf("iteration %d: event id = %q, want %q", i, got.EventID, eventID) } } samples = append(samples, time.Since(start)) } sort.Slice(samples, func(i, j int) bool { return samples[i] < samples[j] }) p50 := samples[len(samples)*50/100] p95 := samples[len(samples)*95/100] max := samples[len(samples)-1] t.Logf("broadcast -> 2 clients decoded: n=%d p50=%v p95=%v max=%v", len(samples), p50, p95, max) if p95 > 500*time.Millisecond { t.Fatalf("p95 latency %v exceeds the 500ms budget", p95) } }