// Package loadtest implementiert die für QA-07 gemeinsam genutzte // Last- und Leistungsmessung: parallele Sessions ausführen, Latenzen // aufzeichnen, Durchsatz und Ressourcenverbrauch bestimmen. k6/vegeta // sind auf diesem Rechner nicht installierbar (Projektregel: keine // zusätzlichen Toolchains/Dienste installieren) — echter Ersatz ohne // externe Abhängigkeit: reale nebenläufige TCP-Sessions über die // Go-Standardbibliothek, gegen die echten, in dieser Sitzung gebauten // Protokollserver. package loadtest import ( "context" "os" "runtime" "sort" "strconv" "strings" "sync" "time" ) // Result fasst Latenz- und Durchsatzwerte eines Lasttest-Laufs // zusammen (Akzeptanzkriterium 2). type Result struct { Protocol string Concurrency int Sessions int Errors int Duration time.Duration Latencies []time.Duration // aufsteigend sortiert } // Throughput liefert Sessions pro Sekunde über die Gesamtlaufzeit. func (r Result) Throughput() float64 { if r.Duration <= 0 { return 0 } return float64(r.Sessions) / r.Duration.Seconds() } // Percentile liefert die Latenz am p-ten Perzentil (0.0–1.0). func (r Result) Percentile(p float64) time.Duration { if len(r.Latencies) == 0 { return 0 } idx := int(p * float64(len(r.Latencies)-1)) if idx < 0 { idx = 0 } if idx >= len(r.Latencies) { idx = len(r.Latencies) - 1 } return r.Latencies[idx] } // Max liefert die höchste gemessene Latenz. func (r Result) Max() time.Duration { if len(r.Latencies) == 0 { return 0 } return r.Latencies[len(r.Latencies)-1] } // Run führt totalSessions Sessions über maximal concurrency parallele // Worker aus (Akzeptanzkriterium 1: realistische Anzahl paralleler // Sessions) und misst je Session die Latenz von sessionFunc. func Run(ctx context.Context, protocol string, concurrency, totalSessions int, sessionFunc func(ctx context.Context) error) Result { var mu sync.Mutex latencies := make([]time.Duration, 0, totalSessions) errCount := 0 sem := make(chan struct{}, concurrency) var wg sync.WaitGroup start := time.Now() for i := 0; i < totalSessions; i++ { sem <- struct{}{} wg.Add(1) go func() { defer wg.Done() defer func() { <-sem }() t0 := time.Now() err := sessionFunc(ctx) elapsed := time.Since(t0) mu.Lock() latencies = append(latencies, elapsed) if err != nil { errCount++ } mu.Unlock() }() } wg.Wait() duration := time.Since(start) sort.Slice(latencies, func(i, j int) bool { return latencies[i] < latencies[j] }) return Result{ Protocol: protocol, Concurrency: concurrency, Sessions: totalSessions, Errors: errCount, Duration: duration, Latencies: latencies, } } // ResourceSample ist eine Momentaufnahme des Ressourcenverbrauchs // DIESES Prozesses (Akzeptanzkriterium/Pflichtprüfung 3): Heap über die // Go-Runtime, kumulierte CPU-Zeit über /proc/self/stat (Linux, kein // externes Werkzeug nötig). type ResourceSample struct { HeapAllocBytes uint64 CPUTimeSeconds float64 } func SampleResources() ResourceSample { var m runtime.MemStats runtime.ReadMemStats(&m) return ResourceSample{HeapAllocBytes: m.HeapAlloc, CPUTimeSeconds: readSelfCPUTimeSeconds()} } // readSelfCPUTimeSeconds liefert die kumulierte User+System-CPU-Zeit // dieses Prozesses in Sekunden. Liefert 0, wenn /proc/self/stat nicht // lesbar ist (z. B. auf Nicht-Linux-Systemen) — Aufrufer behandeln das // als "keine CPU-Messung verfügbar", kein harter Fehler. func readSelfCPUTimeSeconds() float64 { data, err := os.ReadFile("/proc/self/stat") if err != nil { return 0 } fields := strings.Fields(string(data)) if len(fields) < 15 { return 0 } utime, err1 := strconv.ParseFloat(fields[13], 64) stime, err2 := strconv.ParseFloat(fields[14], 64) if err1 != nil || err2 != nil { return 0 } const clockTicksPerSecond = 100 // Linux-Standard-USER_HZ return (utime + stime) / clockTicksPerSecond }