|
5 | 5 | > `setProperty` in MetaModels) wurde **versucht und wieder verworfen**, siehe |
6 | 6 | > [Verworfen: der Vergleich auf der Speicherform](#verworfen-der-vergleich-auf-der-speicherform). |
7 | 7 | > |
8 | | -> **Danach gemessen statt geraten** — und das Ergebnis stellt alles Weitere infrage: |
9 | | -> [Unser Code macht 5,6 % der Laufzeit aus](#wo-die-zeit-wirklich-hingeht), der Rest ist |
10 | | -> Entwicklungs-Infrastruktur. Wer hier weiterarbeitet, sollte **zuerst im prod-Modus messen**. |
| 8 | +> **Danach gemessen statt geraten.** Kernbefund: |
| 9 | +> [Derselbe Speichervorgang braucht in prod 746 ms statt 4.188 ms](#derselbe-vorgang-in-prod) |
| 10 | +> — Faktor 5,6. In Produktion gibt es kein Laufzeitproblem. Wer dennoch optimiert, muss **in |
| 11 | +> prod messen**: Im dev-Modus verdeckt die Werkzeugschicht alles andere. |
11 | 12 |
|
12 | 13 | ## Der Befund |
13 | 14 |
|
@@ -191,14 +192,74 @@ Herkunft: |
191 | 192 | | **MetaModels** | **959 ms** | **3,5 %** | |
192 | 193 | | **dc-general** | **570 ms** | **2,1 %** | |
193 | 194 |
|
194 | | -**Unser Code macht 5,6 % aus.** Über 40 % sind Entwicklungs-Infrastruktur, die in Produktion |
195 | | -gar nicht läuft. Auffällig sind **14.320 Log-Einträge pro Speichervorgang**; sie erklären auch |
196 | | -einen großen Teil der Symfony-Zeile (`Request::getUri()` u. ä. wird 14.215-mal aufgerufen, |
197 | | -einmal je Log-Eintrag durch den Request-Processor). |
| 195 | +Im dev-Modus macht unser Code **5,6 %** aus; über 40 % sind Entwicklungs-Infrastruktur, die in |
| 196 | +Produktion gar nicht läuft. Auffällig sind **14.320 Log-Einträge pro Speichervorgang**; sie |
| 197 | +erklären auch einen großen Teil der Symfony-Zeile (`Request::getUri()` u. ä. wird 14.215-mal |
| 198 | +aufgerufen, einmal je Log-Eintrag durch den Request-Processor). |
| 199 | + |
| 200 | +> **Diese Zahlen taugen nicht zur Priorisierung.** Sie beschreiben den dev-Modus, nicht das |
| 201 | +> Produkt. Der prod-Lauf im nächsten Abschnitt kommt zu einer ganz anderen Verteilung — dort |
| 202 | +> sind es **29 %** statt 5,6 %. Wer aus dem dev-Profil ableitet, woran er arbeiten sollte, |
| 203 | +> arbeitet am Profiler. |
198 | 204 |
|
199 | 205 | Einschränkung: Xdebug instrumentiert jeden Funktionsaufruf und überzeichnet daher Code mit |
200 | 206 | vielen kleinen Aufrufen — also gerade die Instrumentierung selbst. Ihr Anteil ist eher zu hoch |
201 | | -angesetzt. Am Verhältnis ändert das nichts: Es ist zu deutlich, um am Ergebnis zu rütteln. |
| 207 | +angesetzt. |
| 208 | + |
| 209 | +## Derselbe Vorgang in prod |
| 210 | + |
| 211 | +`APP_ENV=prod`, Xdebug aus, sechs Läufe nach zwei Aufwärmläufen: |
| 212 | + |
| 213 | +| | Median | Min | Max | |
| 214 | +|---|---:|---:|---:| |
| 215 | +| dev | 4.188 ms | 4.147 | 5.932 | |
| 216 | +| **prod** | **746 ms** | 707 | 806 | |
| 217 | + |
| 218 | +**Faktor 5,6.** Ein Speichervorgang mit 27 Widgets dauert in Produktion drei Viertel einer |
| 219 | +Sekunde. Das ist kein Laufzeitproblem — das gesamte Thema war zu einem großen Teil eine |
| 220 | +Eigenschaft der Entwicklungsumgebung. |
| 221 | + |
| 222 | +Die Verteilung im prod-Profil sieht völlig anders aus als in dev: |
| 223 | + |
| 224 | +| Herkunft | dev | **prod** | |
| 225 | +|---|---:|---:| |
| 226 | +| Contao-Core | 5,2 % | **20,8 %** | |
| 227 | +| Symfony (übrige) | 23,1 % | 18,5 % | |
| 228 | +| **MetaModels** | 3,5 % | **15,9 %** | |
| 229 | +| PHP-intern / Aufwärmen | 10,3 % | 14,5 % | |
| 230 | +| **dc-general** | 2,1 % | **13,3 %** | |
| 231 | +| Doctrine | 3,8 % | 6,7 % | |
| 232 | +| Logging / Monolog | 16,7 % | **2,8 %** | |
| 233 | +| Debug-EventDispatcher | 17,7 % | **0 %** | |
| 234 | +| Profiler / VarDumper | 7,5 % | **0 %** | |
| 235 | +| PhpParser | 9,2 % | 0 % | |
| 236 | + |
| 237 | +Unser Code ist in prod **29 %** — der größte zusammenhängende Block. Von den 14.320 |
| 238 | +Log-Einträgen bleiben 2,8 % Restkosten; das Logging war ein dev-Artefakt. |
| 239 | + |
| 240 | +### Konkrete Fundstellen in prod |
| 241 | + |
| 242 | +| Eigenzeit | Aufrufe | Stelle | |
| 243 | +|---:|---:|---| |
| 244 | +| 196 ms | 834 | `EventDispatcher->callListeners` (996 Dispatches gesamt) | |
| 245 | +| ~196 ms | 2.583 | Composer-Autoloader (`findFile`, `loadClass`, Closure) | |
| 246 | +| ~235 ms | 40 / 26.887 | Contao-Twig: `getInheritanceChains`, `getFirst`, `ThemeNamespace->match` | |
| 247 | +| ~112 ms | **~6.000** | `RequestScopeDeterminator` + `ScopeMatcher->isBackendRequest` | |
| 248 | + |
| 249 | +Die letzte Zeile ist **unsere** und die einzige, die klar nach einem Fehler aussieht: |
| 250 | +`RequestScopeDeterminator->currentScopeIsUnknown()` läuft **5.811-mal** und |
| 251 | +`getCurrentScope()` **6.287-mal** für einen einzigen Speichervorgang. Die Antwort ändert sich |
| 252 | +innerhalb einer Anfrage nicht — ein Zwischenspeichern je Request wäre naheliegend und |
| 253 | +risikoarm. Größenordnung: rund 3,5 % der Rechenzeit, also etwa 25 ms von 746 ms. |
| 254 | + |
| 255 | +Der Composer-Autoloader mit 2.583 Klassenladungen deutet darauf hin, dass im Devstack kein |
| 256 | +optimierter Classmap erzeugt wird (`composer dump-autoload -o`) — eine reine |
| 257 | +Bereitstellungsfrage, kein Code. |
| 258 | + |
| 259 | +> **Beim Umschalten auf prod:** Der prod-Container-Cache war veraltet und quittierte den ersten |
| 260 | +> Versuch mit einem 500 (`WidgetBuilder::__construct()`, zu wenige Argumente — Stand vor dem |
| 261 | +> DI-Umbau). `cache:clear --env=prod` genügt; `cache:warmup` allein baut einen vorhandenen |
| 262 | +> Container nicht neu. |
202 | 263 |
|
203 | 264 | ### Xdebug kostet Faktor 2,6 |
204 | 265 |
|
@@ -229,15 +290,20 @@ Webserver tatsächlich geladen hat. |
229 | 290 |
|
230 | 291 | ## Was offen bleibt |
231 | 292 |
|
232 | | -1. **Ein Profillauf im prod-Modus.** Alles oben ist im dev-Modus gemessen, wo über 40 % der |
233 | | - Zeit auf Werkzeuge entfallen, die in Produktion fehlen. Erst prod zeigt die echte |
234 | | - Verteilung der Anwendungslast — vorher ist jede weitere Optimierung Raten. |
235 | | -2. **Die 14.320 Log-Einträge je Speichervorgang.** Auch in Produktion nicht umsonst, und die |
236 | | - Zahl allein ist ein Geruch. |
237 | | -3. **Die Änderungserkennung je Attributtyp** — begrenzt durch die 332 ms Datenbankzeit. |
238 | | -4. **Die 76 `getWidget`-Aufrufe.** Zu klären, welche Durchläufe das sind und ob sich einer |
239 | | - davon einsparen lässt. |
240 | | -5. **Der Sprachwechsel je Property** bei übersetzten Modellen — noch nicht gemessen. |
| 293 | +Vorweg: **In Produktion gibt es kein Laufzeitproblem** (746 ms). Alles Folgende ist Kür, kein |
| 294 | +Pflichtprogramm — und lohnt nur, wenn es zugleich den Code klarer macht. |
| 295 | + |
| 296 | +1. **`RequestScopeDeterminator` je Request zwischenspeichern.** ~6.000 Auswertungen derselben, |
| 297 | + innerhalb einer Anfrage unveränderlichen Frage. Der einzige Fund, der klar nach einem |
| 298 | + Fehler aussieht; rund 25 ms. |
| 299 | +2. **996 Event-Dispatches je Speichervorgang** — mit 196 ms der größte Einzelposten. Ob das zu |
| 300 | + viel ist, ist eine Architekturfrage, keine Optimierungsfrage. |
| 301 | +3. **Optimierter Composer-Classmap im Devstack** (`dump-autoload -o`) — Bereitstellung, kein |
| 302 | + Code, ~6 %. |
| 303 | +4. **Die Änderungserkennung je Attributtyp** — begrenzt durch die 332 ms Datenbankzeit im dev- |
| 304 | + Profil, in prod entsprechend weniger. Nach dem prod-Ergebnis kaum noch lohnend. |
| 305 | +5. **Die 76 `getWidget`-Aufrufe** und **der Sprachwechsel je Property** bei übersetzten |
| 306 | + Modellen — beides unverändert offen, beides nach diesen Zahlen nachrangig. |
241 | 307 |
|
242 | 308 | ## Messung wiederholen |
243 | 309 |
|
|
0 commit comments