From 5ce097bbb7a0c000340094e388f9dfdd60651c84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Untersch=C3=BCtz?= Date: Thu, 27 Nov 2025 21:59:54 +0100 Subject: [PATCH] add bench --- benchmark/main.go | 183 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 183 insertions(+) create mode 100644 benchmark/main.go diff --git a/benchmark/main.go b/benchmark/main.go new file mode 100644 index 0000000..793ca9f --- /dev/null +++ b/benchmark/main.go @@ -0,0 +1,183 @@ +package main + +import ( + "bytes" + "encoding/json" + "flag" + "fmt" + "io" + "log" + "net/http" + "sync" + "sync/atomic" + "time" +) + +// Konfiguration (über Flags steuerbar) +var ( + targetURL string + numClients int + duration time.Duration + chunkTicks int +) + +// Statistiken (Atomar für Thread-Sicherheit) +var ( + totalRequests int64 + totalErrors int64 + totalLatency int64 // in Millisekunden +) + +// Datenstrukturen für Kommunikation (Minimal) +type StartResponse struct { + SessionID string `json:"sessionId"` +} + +type Input struct { + Tick int `json:"t"` + Act string `json:"act"` +} + +type ValidateRequest struct { + SessionID string `json:"sessionId"` + Inputs []Input `json:"inputs"` + TotalTicks int `json:"totalTicks"` +} + +func main() { + // 1. Argumente parsen + flag.StringVar(&targetURL, "url", "http://localhost:8080", "URL zum Server") + flag.IntVar(&numClients, "clients", 50, "Anzahl gleichzeitiger Spieler") + flag.DurationVar(&duration, "time", 10*time.Second, "Dauer des Tests (z.B. 10s, 1m)") + flag.IntVar(&chunkTicks, "ticks", 60, "Simulierte Ticks pro Anfrage (Last)") + flag.Parse() + + fmt.Printf("🚀 Starte Benchmark auf %s\n", targetURL) + fmt.Printf("👥 Spieler (Clients): %d\n", numClients) + fmt.Printf("⏱️ Dauer: %s\n", duration) + fmt.Printf("🏋️ Last pro Request: %d Ticks Physik\n", chunkTicks) + fmt.Println("---------------------------------------------------") + + // 2. Clients starten + var wg sync.WaitGroup + start := time.Now() + + // Kanal um den Workern das Stop-Signal zu geben + stopChan := make(chan struct{}) + + // Timer für das Testende + time.AfterFunc(duration, func() { + close(stopChan) + }) + + for i := 0; i < numClients; i++ { + wg.Add(1) + go runClient(i, &wg, stopChan) + } + + // Warten bis alle fertig sind + wg.Wait() + totalTime := time.Since(start) + + // 3. Auswertung + printReport(totalTime) +} + +func runClient(id int, wg *sync.WaitGroup, stopChan <-chan struct{}) { + defer wg.Done() + + // A. Session Starten + sessionID, err := startSession() + if err != nil { + log.Printf("[Client %d] Start Fehler: %v", id, err) + atomic.AddInt64(&totalErrors, 1) + return + } + + // B. Game Loop simulieren + client := &http.Client{Timeout: 5 * time.Second} + + for { + select { + case <-stopChan: + return // Testzeit abgelaufen + default: + // Wir simulieren einen Chunk (z.B. Spieler rennt einfach geradeaus) + reqData := ValidateRequest{ + SessionID: sessionID, + Inputs: []Input{}, // Keine Eingaben = Nur Rennen + TotalTicks: chunkTicks, + } + + payload, _ := json.Marshal(reqData) + + reqStart := time.Now() + resp, err := client.Post(targetURL+"/api/validate", "application/json", bytes.NewBuffer(payload)) + latency := time.Since(reqStart).Milliseconds() + + if err != nil || resp.StatusCode != 200 { + // Fehler zählen + atomic.AddInt64(&totalErrors, 1) + if resp != nil { + io.Copy(io.Discard, resp.Body) // Body leeren + resp.Body.Close() + } + } else { + // Erfolg zählen + atomic.AddInt64(&totalRequests, 1) + atomic.AddInt64(&totalLatency, latency) + io.Copy(io.Discard, resp.Body) + resp.Body.Close() + } + + // Optional: Kurz warten um "echte" 60 FPS zu simulieren? + // Für Stresstests lassen wir das weg, wir wollen sehen was MAXIMAL geht. + // time.Sleep(100 * time.Millisecond) + } + } +} + +func startSession() (string, error) { + resp, err := http.Post(targetURL+"/api/start", "application/json", nil) + if err != nil { + return "", err + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + return "", fmt.Errorf("status %d", resp.StatusCode) + } + + var data StartResponse + if err := json.NewDecoder(resp.Body).Decode(&data); err != nil { + return "", err + } + return data.SessionID, nil +} + +func printReport(totalTime time.Duration) { + reqs := atomic.LoadInt64(&totalRequests) + errs := atomic.LoadInt64(&totalErrors) + lat := atomic.LoadInt64(&totalLatency) + + avgLat := 0.0 + if reqs > 0 { + avgLat = float64(lat) / float64(reqs) + } + + rps := float64(reqs) / totalTime.Seconds() + + fmt.Println("\n📊 ERGEBNISSE") + fmt.Println("---------------------------------------------------") + fmt.Printf("✅ Erfolgreiche Requests: %d\n", reqs) + fmt.Printf("❌ Fehler: %d\n", errs) + fmt.Printf("⚡ Requests pro Sekunde: %.2f Req/s\n", rps) + fmt.Printf("🐢 Ø Antwortzeit: %.2f ms\n", avgLat) + fmt.Println("---------------------------------------------------") + + if avgLat > 100 { + fmt.Println("⚠️ WARNUNG: Server antwortet langsam (>100ms). Spieler könnten Lags spüren.") + } else { + fmt.Println("✨ PERFEKT: Server ist schnell genug für flüssiges Spielen!") + } +}