# Lasttest Wie viel hält Munin aus, wenn viele Agenten gleichzeitig lesen und schreiben, in einem Projekt, in mehreren Projekten eines Workspace oder über mehrere Workspaces verteilt? Der Lasttest in `last_test.go` beantwortet das mit echten Aufrufen über HTTP und MCP, also über denselben Weg wie im Betrieb: Key-Prüfung, Protokoll, Datenbank und Suchindex. --- ## Ausführen Der Test läuft **nicht** bei `go test ./...` mit. Er braucht Zeit und lastet den Rechner voll aus. Das Build-Tag `last` schaltet ihn erst ein, wenn man ausdrücklich danach fragt: ```sh go test -tags last -run TestLast -v . ``` Mit eigenen Werten: ```sh go test -tags last -run TestLast -v . -last.agenten=128 -last.dauer=30s ``` Mit dem Race-Detektor, der Datenwettläufe zwischen Goroutinen meldet. Er macht alles etwa zehnmal langsamer, deshalb mit kleineren Werten: ```sh go test -race -tags last -run TestLast . -last.dauer=3s -last.agenten=16 -last.eintraege=100 ``` Der Test legt alles in einem temporären Verzeichnis an und räumt danach auf. Echte Daten in `data/` fasst er nicht an. ### Schalter | Schalter | Vorgabe | Bedeutung | |---|---|---| | `-last.agenten` | 32 | Gleichzeitige Agenten je Aufstellung. Jeder hat einen eigenen Key und eine eigene MCP-Sitzung. | | `-last.dauer` | 10s | Laufzeit je Aufstellung. Es gibt drei Aufstellungen, die Gesamtdauer ist also etwa das Dreifache. | | `-last.projekte` | 4 | Zahl der Projekte bzw. Workspaces in den Aufstellungen mit mehreren | | `-last.eintraege` | 500 | Einträge, die jedes Projekt vorab bekommt, damit die Suche einen realistischen Index hat | | `-last.slugs` | 20 | Bezeichner, auf die die Agenten je Projekt schreiben. Weniger heißt mehr Gedränge auf denselben Einträgen. | | `-last.schreibanteil` | 30 | Anteil von `remember` in Prozent. Der Rest teilt sich 2 : 1 auf `recall` und `get` auf. | --- ## Was der Test macht ### Drei Aufstellungen Alle drei laufen mit derselben Zahl Agenten, damit die Zahlen vergleichbar sind: | Aufstellung | Was sie zeigt | |---|---| | **ein Projekt** | Alle Agenten im selben Projekt. Der Grundwert. | | **ein Workspace, N Projekte** | Die Projekte liegen in einer Datei und teilen sich die **eine Schreibverbindung** (siehe `openPool` in `internal/store/store.go`). Bremsen sich die Projekte gegenseitig? | | **N Workspaces, je 1 Projekt** | Jeder Workspace hat seine eigene Datei. Hier kann echt parallel geschrieben werden. | ### Was ein Agent tut Jeder Agent verbindet sich mit seinem Key über `/mcp` und ruft bis zum Ende der Laufzeit in einer Schleife zufällig auf: - `remember` auf einen der umkämpften Bezeichner (`gemeinsam-00` …). Jeder Schreibvorgang enthält ein **eindeutiges Wort**, an dem sich später erkennen lässt, welche Fassung wo gelandet ist. - `recall` mit einem von acht Fachbegriffen, die in den vorab angelegten Einträgen vorkommen - `get` auf einen der vorab angelegten Einträge Das Schreibbudget (Ausbaustufe 3.8) ist im Test abgeschaltet. Es soll eine Schleife stoppen und würde sonst die Messung abwürgen. ### Was am Ende geprüft wird Der Test schlägt fehl, wenn eines davon nicht stimmt: 1. **Kein Aufruf ist fehlgeschlagen.** 2. **Kein Schreibvorgang ging verloren:** Jedes erfolgreiche `remember` erzeugt eine Fassung. Die Summe der Fassungen aller umkämpften Einträge muss genau der Zahl der erfolgreichen Aufrufe entsprechen. 3. **Der Suchindex kennt den letzten Stand:** Das eindeutige Wort der letzten gespeicherten Fassung muss den Eintrag über die Suche finden. ### Die Ausgabe lesen Für jede Aufstellung erscheint eine Tabelle: ``` Werkzeug Aufrufe je s p50 p95 p99 max Fehler verbinden 128 0 18.03ms 33.46ms 35.04ms 35.54ms 0 recall 21496 2150 4.45ms 8.02ms 23.05ms 40.33ms 0 get 11381 1138 1.04ms 2.4ms 3.18ms 7.33ms 0 remember 14269 1427 67.84ms 190.04ms 269.16ms 439.31ms 0 ``` - **je s:** erfolgreiche Aufrufe pro Sekunde - **p50:** Die Hälfte der Aufrufe war schneller, also die typische Dauer. - **p95 / p99:** Die langsamsten 5 % bzw. 1 % der Aufrufe dauerten mindestens so lange. Das sind die Werte, die ein Nutzer als „hängt manchmal“ erlebt. - **max:** der langsamste Aufruf überhaupt - **verbinden:** der MCP-Verbindungsaufbau, einmal je Agent Darunter steht `Bestand geprüft: … Fassungen, Suchindex aktuell`. --- ## Ergebnis vom 2026-10-06 **Testrechner:** Mac mit 10 Kernen. Clients und Server liefen auf derselben Maschine, jeweils 500 Einträge vorab, 30 % Schreiben, 20 umkämpfte Bezeichner je Projekt, 4 Projekte bzw. Workspaces. ### 128 Agenten, 10 Sekunden | Aufstellung | `remember` je s | `remember` p50 / p95 / p99 | `recall` p50 / p99 | `get` p50 / p99 | Fehler | |---|---|---|---|---|---| | ein Projekt | 1.427 | 68 / 190 / 269 ms | 4,5 / 23 ms | 1,0 / 3,2 ms | 0 | | ein Workspace, 4 Projekte | 1.355 | 73 / 201 / 286 ms | 4,5 / 24 ms | 1,0 / 3,4 ms | 0 | | 4 Workspaces, je 1 Projekt | 1.328 | 60 / 151 / 205 ms | 15 / 36 ms | 3,8 / 12 ms | 0 | ### 32 Agenten, 5 Sekunden | Aufstellung | `remember` je s | `remember` p50 / p99 | `recall` p50 / p99 | Fehler | |---|---|---|---|---| | ein Projekt | 1.374 | 13 / 42 ms | 4,7 / 11 ms | 0 | | ein Workspace, 4 Projekte | 1.387 | 13 / 46 ms | 4,4 / 10 ms | 0 | | 4 Workspaces, je 1 Projekt | 1.141 | 8 / 18 ms | 10 / 24 ms | 0 | In allen Läufen ging kein Schreibvorgang verloren, und der Suchindex war am Ende aktuell. Auch der Lauf mit Race-Detektor war ohne Befund. ### Was die Zahlen sagen - **Last ist bei echter Nutzung kein Thema.** Der Server schafft rund 1.300 Schreibvorgänge pro *Sekunde*. Das Schreibbudget erlaubt 1.000 pro *Stunde* und Projekt, und echte Agenten liegen bei ein paar Dutzend pro Stunde. - **Projekte im selben Workspace verhalten sich wie ein Projekt.** Sie teilen sich die Schreibverbindung, Schreibvorgänge stellen sich also an. Mit mehr Agenten wachsen deshalb die Wartezeiten beim Schreiben (p50 von 13 ms bei 32 auf 68 ms bei 128 Agenten), aber nicht die Fehler. - **Mehrere Workspaces schreiben etwas schneller, suchen aber langsamer.** Getrennte Dateien heben die Warteschlange beim Schreiben auf. Dafür arbeiten dann mehr Schreiber gleichzeitig und konkurrieren mit der Suche um dieselben Kerne. Auf einem Rechner ist die CPU die eigentliche Grenze, nicht SQLite. - **Der volle Weg kostet etwa das Sechsfache** gegenüber dem reinen Schreiben in die Datenbank (8.656/s, siehe `AUSBAUSTUFEN.md` 1.2). Dazu kommen HTTP, MCP, Key-Prüfung, Protokolleintrag und Suchindex. ### Grenzen der Messung - **Clients und Server teilen sich den Rechner.** Die Agenten verbrauchen selbst CPU. Ein Server allein schafft mehr, die Zahlen sind eher eine Untergrenze. - **Kaltstart nicht gemessen.** Der Suchindex jedes Projekts ist nach dem Anlegen der Einträge schon aufgebaut. Der erste Zugriff auf ein großes Projekt nach einem Neustart (etwa 170 ms je 10.000 Einträge, siehe `AUSBAUSTUFEN.md` 1.1) ist nicht enthalten. Während dieses Aufbaus wartet nur dieses eine Projekt. Bis 2026-10-06 warteten auch alle anderen Projekte desselben Workspace, das ist behoben (siehe unten). - **Nur MCP.** Die Weboberfläche ist nicht dabei. - **Kein Rückkanal:** Der zusätzliche SSE-Kanal, den manche MCP-Clients offen halten, ist abgeschaltet. Er kostet je Agent eine offene Verbindung, aber kaum Rechenzeit. - **Gleichmäßige Dauerlast.** Echte Agenten arbeiten in Schüben mit Pausen dazwischen. Der Test ist also deutlich härter als die Wirklichkeit. --- ## Dabei gefunden **Suchindex hielt eine ältere Fassung (behoben).** Speichern und Index aktualisieren waren zwei getrennte Schritte, und der Index übernahm den Stand, den der Aufrufer mitgab. Überholten sich zwei Schreiber auf demselben Eintrag zwischen diesen Schritten, stand Fassung 3 in der Datenbank, aber Fassung 2 im Index. `recall` fand den Eintrag dann bis zum Neustart über veraltete Wörter. Genauso konnte ein gerade gelöschter Eintrag durch eine verspätete Aktualisierung wieder in der Suche auftauchen. Unter Last ließ sich das nicht zuverlässig auslösen, das Zeitfenster liegt bei Mikrosekunden. Mit gezielt vertauschter Reihenfolge war es sofort reproduzierbar. **Behebung:** `Registry.Refresh` in `internal/search/registry.go` liest nach jedem Schreiben den aktuellen Stand aus der Datenbank nach. Der letzte Aufruf liest damit immer den neuesten Stand, egal in welcher Reihenfolge die Aufrufe ankommen. Abgesichert durch `internal/search/registry_test.go` und `internal/mcpserver/concurrency_test.go`. **Aufbau des Suchindex blockierte den ganzen Workspace (behoben).** Die Registry hatte nur eine Sperre für alle Projekte eines Workspace und hielt sie auch, während der Index eines Projekts aus der Datenbank aufgebaut wurde. Nach einem Neustart musste so jedes Projekt warten, bis das größte fertig eingelesen war. Seit 2026-10-06 hat jedes Projekt seine eigene Sperre. Abgesichert durch `TestSlowBuildDoesNotBlockOtherProjects` und `TestChangeDuringBuildIsNotLost`: Ein Schreibvorgang während des Aufbaus wartet, bis der Aufbau fertig ist, und trägt seinen Stand danach nach. **Stolperstein beim Messen:** Mit den Vorgaben von Go hält ein HTTP-Client nur zwei Verbindungen je Ziel offen. Bei 128 Agenten öffnete der Test dadurch laufend neue Verbindungen, bis dem Mac die lokalen Ports ausgingen (`connect: can't assign requested address`). Das ist ein Fehler der Messung, nicht des Servers, denn echte Agenten sitzen auf verschiedenen Rechnern. Der Test nutzt deshalb einen gemeinsamen Transport mit einer offenen Verbindung je Agent. Taucht die Meldung wieder auf, liegt die Ursache dort. --- ## Wann erneut messen - Nach Änderungen am Schreibweg: `PutEntry`, Protokoll, Suchindex, Verbindungspools in `internal/store/store.go` - Vor einem Umzug auf andere Hardware oder in einen Container mit begrenzter CPU. Dann am besten Server und Test auf getrennten Rechnern, oder den Test mit weniger Agenten. - Wenn `AUSBAUSTUFEN.md` 4.9 (mehrere Keys in einem MCP-Block) gebaut wird: Ein `recall` über mehrere Projekte belastet die Suche anders als heute. Neue Ergebnisse bitte mit Datum unter **Ergebnis** ergänzen, die alten stehen lassen. So bleibt sichtbar, wie sich Änderungen ausgewirkt haben. --- ## Nachmessung vom 2026-10-14: Suchprotokoll und Wortvarianten 32 Agenten, 10 Sekunden, ein Projekt. Seit diesem Stand schreibt jeder `recall` eine Zeile ins Suchprotokoll, und die Suche prüft Wortvarianten. | Stand | `remember` je s | `recall` p50 / p99 | |---|---|---| | 2026-10-06 (5 s) | 1.374 | 4,7 / 11 ms | | Wortvarianten, ohne Suchprotokoll | 1.278 | 5,5 / 12 ms | | Wortvarianten und Suchprotokoll | 1.074 | 8,5 / 30 ms | Fast der ganze Unterschied kommt vom Suchprotokoll: Jede Suche ist jetzt auch ein Schreibvorgang und stellt sich an derselben Schreibverbindung an wie `remember`. Für echte Nutzung bedeutungslos — dort sind es Dutzende Aufrufe pro Stunde, nicht tausend pro Sekunde. **Falls es je stört:** die Zeilen in einem Puffer sammeln und alle paar hundert Millisekunden gebündelt in einer Transaktion schreiben. Dann fällt das Protokoll aus dem Weg der Anfrage.