% !TeX root = ../../thesis.tex \chapter{Performance-Untersuchung der Anwendung} \label{ch:performance-investigation-application} Nun werden die unterschiedlichen Schichten betrachtet und möglichen Performance-Verbesserungen untersucht und deren Vor"= und Nachteile herausgearbeitet. Für die Tests wird ein aktuelles Manjaro-System mit frisch installierten Payara als Serverhost und der IntelliJ IDEA als Entwicklungsumgebung verwendet. Der Computer ist mit einer Intel CPU i7-12700K, 32 GB Arbeitsspeicher und einer SSD als Systemfestplatte ausgestattet. Zur ersten Untersuchung und der Bestimmung der Basis-Linie, wurde das Script ohne eine Änderung an dem Code und der Konfiguration mehrfach aufgerufen. Hierbei hat sich gezeigt, dass der erste Aufruf nach dem Deployment ca. 1500 ms gedauert hat. Die weiteren Aufrufe dauert dann im Durchschnitt noch 600 ms. Beim achten Aufruf des Scripts hat der Server nicht mehr reagiert und im Log ist ein OutOfMemoryError protokolliert worden. Nach einem Neustart des Servers, konnte das gleiche Verhalten wieder reproduziert werden. Daraufhin wurde das Test-Script um die Anzeige der aktuellen Speicherverwendung des Payara-Servers erweitert und diese zeitgleich zu beobachten. Diese Auswertung zeigte, dass der Server mit ca. 1500 MB RSS Nutzung an seine Grenzen stößt. Diese Grenzen wurde durch die Konfigurationsänderung im Payara-Server von \texttt{-Xmx512m} auf \texttt{-Xmx4096m} nach oben verschoben. Nun werden ca. 60 Aufrufe des Scripts benötigt, damit der Server nicht mehr reagiert. Hierbei wird aber kein OutOfMemoryError in der Log-Datei protokolliert und der Server verwendet nun ca. 4700 MB RSS. Bei allen Tests war noch mehr als die Hälfte des verfügbaren Arbeitsspeichers des Computers ungenutzt. Mit der Konfiguration \texttt{-Xmx} wird der maximal verwendbare Heap"=Speicher in der \ac{JVM} definiert. Dies zeigt direkt, dass es ein Problem in der Freigabe der Objekte gibt, da dass erhöhen des verwendbaren Arbeitsspeicher das Problem nicht löst, sondern nur verschiebt. Für alle nachfolgenden Messungen wird das Skript \ref{ap:calling_script} verwendet, welches die einzelnen Aufrufe steuert. Die Ergebnisse werden in eine Tabelle überführt, wie in der Tabelle \ref{tbl:measure-without-cache}. Hierbei werden die Aufrufzeiten der Webseite aus dem Skript für die Zeitmessung mit Mindest"=, Durchschnitt"= und Maximalzeit aufgenommen, hierbei ist eine kürzere Zeit besser. Zusätzlich wird die Anzahl der aufgerufenen SQL Abfragen ermitteln, auch hier gilt, dass weniger Aufrufe besser sind. Als letztes wird noch der verwendete Arbeitsspeicher vom \textit{Glassfish}"=Server vor und nach dem Aufruf ermittelt und die Differenz gebildet, hierbei sollte im besten Fall die Differenz bei 0 liegen. Dieser Aufbau gilt für alle weiteren Messungen. Zusätzlich werden noch die Laufzeiten der \ac{JSF} ermittelt und die durchschnittlichen Zeiten mit in der Tabelle dargestellt, und auch hier ist es besser, wenn die Zeiten kürzer sind. Als Grundlage für die Vergleiche wurden eine Messung durchgeführt, bei der alle Caches deaktiviert wurden und keine Änderung am Code vorgenommen wurde. Das Ergebnis dieser Messung ist in \ref{tbl:measure-without-cache} zu finden. Diese zeigen auch direkt ein erwartetes Ergebnis, dass der erste Aufruf bedeutend länger dauert als die Nachfolgenden. Ebenfalls sieht man eindeutig, dass die Anzahl der Anfragen nach dem ersten Aufruf immer die gleiche Anzahl besitzen. Der Speicherbedarf steigt auch relative gleichmässig, was nicht recht ins Bild passt, da hier keine Objekte im Cache gehalten werden sollten. \mytodos{Diese Tabelle vielleicht auch einfach komplett streichen, da der Datenbestand anders ist, und das wichtigste die Zeit der SQL-Abfragen nicht sichtbar ist} \begin{table}[h!] \centering \begin{tabular}{|r|r|r|r|r|r|r|r|} \hline & \multicolumn{3}{c}{Aufrufzeit (ms)} & & \multicolumn{3}{|c|}{RSS (MB)} \\ \hline \# & min & avg & max & Queries & davor & danach & diff \\ \hline 1 & 395 & 578 & 1312 & 12237 & 747.15 & 924.88 & 177.73 \\ 2 & 353 & 375 & 464 & 12080 & 924.51 & 1027.75 & 103,24 \\ 3 & 286 & 345 & 535 & 12080 & 1018.21 & 1145.36 & 127.15 \\ 4 & 291 & 307 & 340 & 12080 & 1129.91 & 1239.75 & 109,84 \\ \hline \end{tabular} \caption{Messung ohne Caches} \label{tbl:measure-without-cache} \end{table} Vor jedem weiteren Test-Lauf wurde die Domain beendet und komplett neugestartet, um mit einer frischen Instanz zu beginnen. Hierbei ist aufgefallen, dass fast immer 62 Abfragen zur Startup-Phase dazugehört haben, unabhängig von den konfigurierten Cache Einstellungen. Einige dieser Abfrage sind durch das erstellen der Materialisierten Sicht \textbf{searchreference} erklärbar. Diese Sicht wird für die Suche benötigt, und da diese Seite nicht betrachtet hier nicht betrachtet wird, wurde der Code für alle weiteren Tests deaktiviert. Da die Abfragezeiten auf der Datenbank zu gering waren, um eine Verbesserung feststellen zu können, wurde für den PostgreSQL und den Payara-Server ein Docker-Container erzeugt und diese limitiert. Die Konfiguration ist im Anhang \ref{ap:docker_config} beschrieben. Mit dem neuen Aufbau ergeben sich nun neue Messungen. Für den Speicherbedarf wird nun nicht mehr der benutzte Speicher der Anwendung beobachtet, sondern die Speichernutzung des Docker-Containers für den Payara-Server. Auch hier ist es besser, wenn es keine oder nur geringe Änderungen vor und nach dem Aufruf der Webseite gibt, ein steigender Wert zeigt an, dass der verwendete Speicher nicht sauber freigegeben werden kann. Für die Ausführungszeiten der SQL-Abfragen wurden nur die sechs Abfragen für die Darstellung der Tabelle beachtet. Hierzu zählt die Hauptabfrage der Dokumenten"=-Tabelle, die Ermittlung des letzten und ersten Eintrags in der Tabelle, die Ermittlung der Adressen des Autors, die Ermittlung der CoAutoren, die Ermittlung der Faksimile, sowie die Ermittlung der Anzahl aller vorhandenen Dokumente. Zusätzlich wird die Zeit des Rendern der Sicht gemessen. Hierbei wird zum einen die komplette Zeit des Renderns ermittelt. Innerhalb des Rendern wird dann noch die Zeit gemessen, wie lange es benötigt, die Daten aus der Datenbank zu laden, und in die Java-Objekte umzuformen. \begin{table}[h] \centering \resizebox{\textwidth}{!}{ \begin{tabular}{|r|r|r|r|r|r|r|r|r|r|r|r|r|r|r|} \hline & \multicolumn{3}{c|}{Aufrufzeit (ms)} & \multicolumn{2}{c|}{Queries (ms)} & \multicolumn{3}{c|}{Memory (MB)} & \multicolumn{3}{c|}{Render (ms)} & \multicolumn{3}{c|}{DB-load (ms)} \\ \hline \# & min & avg & max & \#"=avg & avg & start & stop & diff & min & avg & max & min & avg & max \\ \hline 1 & 451 & 682 & 1931 & 1223.0 & 30.3 & 931.3 & 986.1 & 54.8 & 440 & 666 & 1859 & 290 & 399 & 710 \\ % 12230 - 303 ms (135+ 79+ 39+ 22+17+11) (#2-6,8) 2 & 341 & 389 & 478 & 1208.0 & 31.2 & 986.5 & 1159.0 & 172.5 & 331 & 378 & 468 & 235 & 282 & 367 \\ % 24310 - 615 ms (270+156+ 78+ 56+34+21) (#2-7) 3 & 299 & 407 & 682 & 1208.0 & 33.5 & 1163.0 & 1273.0 & 110.0 & 290 & 398 & 672 & 207 & 307 & 579 \\ % 36390 - 950 ms (406+256+118+ 79+55+36) (#2-7) 4 & 278 & 359 & 424 & 1208.0 & 33.7 & 1272.0 & 1465.0 & 193.0 & 270 & 351 & 415 & 198 & 269 & 342 \\ % 48470 - 1287 ms (564+334+167+105+72+45) (#2-7) 5 & 264 & 317 & 356 & 1208.0 & 32.9 & 1467.0 & 1574.0 & 107.0 & 256 & 309 & 348 & 184 & 235 & 276 \\ % 60560 - 1616 ms (699+428+210+128+92+59) (#2-7) \hline \end{tabular} } \caption{Messung ohne Caches im Docker} \label{tbl:measure-without-cache-docker} \end{table} \section{Umgestalten der Datenbanktabellen} \label{sec:performance-investigation-application:new-table} Hierfür wurde die aktuelle Datenstruktur untersucht um zu prüfen, ob eine Umgestaltung der Tabelle einen Verbesserung bringen würden. Die typische Optimierung ist die Normalisierung der Tabellenstruktur. Die Tabellenstruktur ist aktuell schon Normalisiert, daher kann hier nichts weiter optimiert werden. Eine weitere optimierungsstrategie besteht in der Denormalisierung, um sich die Verknüpfungen der Tabellen zu sparen. Dies ist in diesem Fall nicht anwendbar, da nicht nur 1:n Beziehungen vorhanden sind, sondern auch auch n:m Beziehungen. Dadurch würden sich die Anzahl der Dokumentenliste erhöhen. Oder man muss die Duplikate auf der Serverseite zusammenführen. \section{Caching im OpenJPA} \label{sec:performance-investigation-application:caching-openjpa} Die Cache-Einstellung von OpenJPA werden über die zwei Einstellungen \texttt{openjpa.DataCache} und \texttt{openjpa.QueryCache} konfiguriert. Bei beiden Einstellungen kann zuerst einmal über ein einfaches Flag \textit{true} und \textit{false} entschieden werden ob der Cache aktiv ist. Zusätzlich kann über das Schlüsselwort \textit{CacheSize} die Anzahl der Elementen im Cache gesteuert werden. Wird diese Anzahl erreicht, dann werden zufällige Objekte aus dem Cache entfernt und in eine SoftReferenceMap übertragen. Bei der Berechnung der Anzahl der Element werden angeheftete Objekte nicht beachtet. Die Anzahl der Soft References kann ebenfalls über eine Einstellung gesteuert werden. Hierfür wird die Anzahl der Elemente über \textit{SoftReferenceSize} gesetzt, dessen Wert im Standard auf \textit{unbegrenzt} steht. Mit dem Wert \textit{0} werden die Soft Referencen komplett deaktiviert. Über die Attribute an den Entitätsklassen, können diese Referenzen ebenfalls gesteuert werden, hierzu muss eine Überwachungszeit angegeben werden. Diese Zeit gibt in ms an, wie lange ein Objekt gültig bleibt. Mit dem Wert \textit{-1} wird das Objekt nie ungültig, was ebenfalls der Standardwert ist. Zuerst wird mit aktivierten Cache mit einer Cache-Größe von 1000 Elemente getestet. Wie in \ref{tbl:measure-ojpa-active} zu sehen, dauert auch hier der erste Aufruf minimal länger als ohne aktiviertem Cache. Alle Nachfolgenden Aufrufe wiederrum sind um 100ms schneller in der Verarbeitung. Auch bei der Anzahl der Anfragen an die Datenbank kann der Rückgang der Anfragen sehr gut gesehen werden. Aktuell kann die Verringerung des wachsenden Speicherbedarfs nur nicht erklärt werden. \begin{table}[h!] \centering \resizebox{\textwidth}{!}{ \begin{tabular}{|r|r|r|r|r|r|r|r|r|r|r|r|r|r|r|} \hline & \multicolumn{3}{c|}{Aufrufzeit (ms)} & \multicolumn{2}{c|}{Queries (ms)} & \multicolumn{3}{c|}{Memory (MB)} & \multicolumn{3}{c|}{Render (ms)} & \multicolumn{3}{c|}{DB-load (ms)} \\ \hline \# & min & avg & max & \#"=avg & avg & start & stop & diff & min & avg & max & min & avg & max \\ \hline 1 & 291 & 611 & 2347 & 730.2 & 28.8 & 852.7 & 891.9 & 39.2 & 282 & 595 & 2286 & 172 & 284 & 770 \\ % 7302 - 288 ms (145+ 42+ 40+ 24+18+ 8+ 7+ 4) (#2-8,12) 2 & 278 & 319 & 422 & 667.3 & 25.8 & 892.7 & 1010.0 & 117.3 & 266 & 309 & 411 & 173 & 195 & 220 \\ % 13975 - 546 ms (282+ 81+ 70+ 47+33+14+11+ 8) (#2-9) 3 & 229 & 281 & 329 & 680.6 & 27.6 & 1011.0 & 1067.0 & 56.0 & 220 & 271 & 313 & 134 & 180 & 222 \\ % 20781 - 822 ms (430+120+ 99+ 77+49+20+16+11) (#2-9) 4 & 222 & 280 & 321 & 671.3 & 27.6 & 1067.0 & 1122.0 & 55.0 & 213 & 271 & 310 & 131 & 189 & 238 \\ % 27494 - 1098 ms (569+160+137+ 99+68+26+22+17) (#2-9) 5 & 206 & 272 & 388 & 683.6 & 27.6 & 1122.0 & 1219.0 & 97.0 & 199 & 264 & 380 & 122 & 175 & 291 \\ % 34330 - 1374 ms (704+202+171+128+86+34+27+22) (#2-9) \hline \end{tabular} } \caption{Messung mit OpenJPA-Cache und Größe auf 1000} \label{tbl:measure-ojpa-active} \end{table} Bei einer erhöhten Cache-Größe, von 1000 auf 10000, zeigt sich auf den ersten Blick ein noch besseres Bild ab, wie in \ref{tbl:measure-ojpa-active-bigger} ersichtlich ist. Der erste Aufruf entspricht der Laufzeit mit geringerer Cache-Größe, aber schon die Anfragen an die Datenbank gehen drastisch zurück. Bei den weiteren Aufrufen werden im Schnitt nun nur noch 6 Anfragen pro Seitenaufruf an die Datenbank gestellt, wodurch die Laufzeit im Schnitt nochmal um 100 ms beschleunigt werden konnte. \begin{table}[h!] \centering \resizebox{\textwidth}{!}{ \begin{tabular}{|r|r|r|r|r|r|r|r|r|r|r|r|r|r|r|} \hline & \multicolumn{3}{c|}{Aufrufzeit (ms)} & \multicolumn{2}{c|}{Queries (ms)} & \multicolumn{3}{c|}{Memory (MB)} & \multicolumn{3}{c|}{Render (ms)} & \multicolumn{3}{c|}{DB-load (ms)} \\ \hline \# & min & avg & max & \#"=avg & avg & start & stop & diff & min & avg & max & min & avg & max \\ \hline 1 & 151 & 368 & 1904 & 141.2 & 20.8 & 906.3 & 936.8 & 30.5 & 164 & 404 & 2232 & 39 & 124 & 847 \\ % 1412 - 208 ms (133+ 40+ 23+9+2+1) (#2,4-6,10,12) 2 & 133 & 143 & 159 & 6.0 & 20.5 & 935.7 & 939.3 & 3.6 & 121 & 136 & 146 & 32 & 36 & 44 \\ % 1472 - 413 ms (274+ 80+ 47+9+2+1) (#2-3,5,6,10,12) 3 & 120 & 126 & 132 & 6.0 & 19.9 & 939.4 & 942.7 & 3.3 & 116 & 136 & 256 & 32 & 47 & 167 \\ % 1532 - 612 ms (412+119+ 69+9+2+1) (#2,3,5,6,10,12) 4 & 120 & 124 & 128 & 6.0 & 21.4 & 944.3 & 945.4 & 1.1 & 105 & 113 & 125 & 30 & 32 & 39 \\ % 1592 - 826 ms (550+168+ 96+9+2+1) (#2-4,6,10,12) 5 & 109 & 114 & 131 & 6.0 & 19.7 & 945.5 & 946.7 & 1.2 & 101 & 107 & 112 & 30 & 32 & 35 \\ % 1652 - 1023 ms (683+209+119+9+2+1) (#2-4,6,10,12) \hline \end{tabular} } \caption{Messung mit OpenJPA-Cache und Größe auf 10000} \label{tbl:measure-ojpa-active-bigger} \end{table} Bei dem deaktivieren der \textit{SoftReference} und dem kleineren Cache zeigt sich keine große Differenz. Somit scheint die \textit{SoftReference} nicht das Problem für den steigenden Arbeitsspeicher zu sein, wie in Tabelle \ref{tbl:measure-ojpa-active-bigger-no-softref} zu sehen. % document, documentaddresseeperson, first/last, documentcoauthorperson, count und documentfacsimile \begin{table}[h!] \centering \resizebox{\textwidth}{!}{ \begin{tabular}{|r|r|r|r|r|r|r|r|r|r|r|r|r|r|r|} \hline & \multicolumn{3}{c|}{Aufrufzeit (ms)} & \multicolumn{2}{c|}{Queries (ms)} & \multicolumn{3}{c|}{Memory (MB)} & \multicolumn{3}{c|}{Render (ms)} & \multicolumn{3}{c|}{DB-load (ms)} \\ \hline \# & min & avg & max & \#"=avg & avg & start & stop & diff & min & avg & max & min & avg & max \\ \hline 1 & 339 & 659 & 2435 & 880.8 & 33.2 & 909.6 & 960.2 & 50.6 & 330 & 644 & 2375 & 218 & 343 & 815 \\ % 8808 - 332 ms (168+ 63+ 44+ 32+21+ 4) (#1,3-6,10) 2 & 267 & 332 & 388 & 832.1 & 28.1 & 959.7 & 1000.0 & 40.3 & 259 & 323 & 377 & 178 & 229 & 280 \\ % 17129 - 613 ms (313+111+ 82+ 48+42+17) (#1-3,5-6,8) 3 & 265 & 397 & 350 & 830.3 & 27.3 & 1001.0 & 1107.0 & 106.0 & 256 & 288 & 241 & 172 & 204 & 252 \\ % 25432 - 886 ms (455+156+125+ 64+63+23) (#1-3,5-6,8) 4 & 249 & 311 & 401 & 727.8 & 27.1 & 1108.0 & 1234.0 & 126.0 & 240 & 303 & 392 & 165 & 225 & 317 \\ % 32710 - 1157 ms (594+205+163+ 85+80+30) (#1-5,8) 5 & 268 & 296 & 325 & 931.9 & 28.0 & 1236.0 & 1239.0 & 3.0 & 260 & 288 & 318 & 192 & 217 & 244 \\ % 42029 - 1437 ms (738+254+204+106+97+38) (#1-5,8) \hline \end{tabular} } \caption{Messung mit OpenJPA-Cache und Größe auf 1000 und 0 SoftReference} \label{tbl:measure-ojpa-active-bigger-no-softref} \end{table} Die Vergleich zeigt, dass der Cache eine gute Optimierung bringt, aber dies nur dann gut funktioniert, wenn immer wieder die gleichen Objekte ermittelt werden. Sobald die Anfragen im Wechsel gerufen werden oder einfach nur der Menge der Objekte den Cache übersteigt, fällt die Verbesserung gering aus. \section{cached queries} \label{sec:performance-investigation-application:cached-query} Über die Einstellung \textit{openjpa.jdbc.QuerySQLCache} wird der Cache für abfragen aktiviert. Hierbei können Abfragen angeben werden, die aus dem Cache ausgeschlossen werden. Der QueryCache wiederrum beachtet aber nur Abfragen die keine Parameter verwenden. Das sieht man auch entsprechend der Auswertung der Aufrufe \ref{tbl:measure-cached-queries}, dass hier keine Veränderung der Aufrufzeiten stattgefunden hat. Gleich ob man mit \ac{JPQL} oder mit der Criteria API abfragt. % document, documentaddresseeperson, first/last, documentcoauthorperson, count und documentfacsimile \begin{table}[h!] \centering \resizebox{\textwidth}{!}{ \begin{tabular}{|r|r|r|r|r|r|r|r|r|r|r|r|r|r|r|} \hline & \multicolumn{3}{c|}{Aufrufzeit (ms)} & \multicolumn{2}{c|}{Queries (ms)} & \multicolumn{3}{c|}{Memory (MB)} & \multicolumn{3}{c|}{Render (ms)} & \multicolumn{3}{c|}{DB-load (ms)} \\ \hline \# & min & avg & max & \#"=avg & avg & start & stop & diff & min & avg & max & min & avg & max \\ \hline 1 & 409 & 771 & 2660 & 1222.4 & xxx & 850.4 & 982.8 & 132.4 & 366 & 633 & 2019 & 254 & 364 & 758 \\ % 12224 - 332 ms (168+ 63+ 44+ 32+21+ 4) (#1,3-6,10) 2 & 336 & 387 & 504 & 1208.0 & xxx & 982.9 & 1113.0 & 130.1 & 310 & 374 & 433 & 221 & 268 & 345 \\ % 24304 - 3 & 312 & 373 & 422 & 1208.0 & xxx & 1114.0 & 1221.0 & 107.0 & 295 & 401 & 658 & 216 & 320 & 570 \\ % 36384 - 4 & 288 & 363 & 471 & 1208.0 & xxx & 1239.0 & 1474.0 & 235.0 & 269 & 356 & 486 & 200 & 279 & 405 \\ % 48464 - 5 & 325 & 398 & 535 & 1208.0 & xxx & 1474.0 & 1666.0 & 192.0 & 280 & 466 & 804 & 208 & 390 & 725 \\ % 60544 - \hline \end{tabular} } \caption{Messung mit aktiviertem Cached Queries} \label{tbl:measure-cached-queries} \end{table} \section{Caching im \ac{JPA}} \label{sec:performance-investigation-application:caching-jpa} \mytodos{muss noch umgebaut werden, falsche Konfiguration erwischt} %Die Cache-Einstellungen von \ac{JPA} werden über mehrere Einstellungen konfiguriert. Anhand von %\texttt{eclipselink.query-results-cache} wird definiert, dass die Ergebnisse von benannten Abfragen im Cache %gespeichert werden. Für den Zugriff in den Cache, wird neben den Namen noch die übergebenen Parameter %berücksichtigt. %% https://eclipse.dev/eclipselink/documentation/2.5/concepts/cache008.htm %Der geteilte Cache, der für die Dauer der persistenten Einheit (EntityManagerFactory oder der Server) vorhanden ist, %kann über \texttt{eclipselink.cache.shared.default} gesteuert werden. Dieser kann nur aktiviert oder deaktiviert werden. %% https://wiki.eclipse.org/EclipseLink/Examples/JPA/Caching %Mit \texttt{eclipselink.cache.size.default} wird die initiale Größe des Caches definiert, hierbei ist der Standardwert %100. Die Objekt werden nicht direkt aus dem Cache entfernt, sondern erst nachdem der \ac{GC} diese freigeben hat. %Zusätzlich wird über \texttt{eclipselink.cache.type.default} die Art des Caching gesteuert. Die Einstellung mit dem %höchsten Speicherbedarf ist \textit{FULL}, bei dem alle Objekte im Cache bleiben, außer sie werden explizit gelöscht. %Die Einstellung \textit{SOFT} und \textit{WEAK} sind sehr ähnlich, der unterschied ist die Referenzierung auf die %Entität. Bei \textit{WEAK} bleiben die Objekte nur solange erhalten, wie die Anwendung selbst eine Referenz auf die %Objekte fest hält. Im Gegensatz dazu bleibt bei \textit{SOFT} die Referenz so lange bestehen, bis der \ac{GC} wegen %zu wenig Speicher Objekte aus dem Cache entfernt. %% https://eclipse.dev/eclipselink/documentation/2.5/concepts/cache002.htm %Um den Cache zu deaktivieren wurden beiden Einstellungen auf \textit{false} gestellt, die Größe auf 0 und der Cache-Typ %auf \textit{NONE}. Hierbei lag die maximale gemessene Laufzeit des ersten Aufrufs bei ca. 1300 ms und es wurden 12219 %Abfragen an die Datenbank gestellt. Bei den nachfolgenden Aufrufe lag die Aufrufzeit im Durchschnitt bei 350 ms und %12080 Abfragen. %Um den Cache wieder zu aktivieren wurden die Einstellungen auf \textit{true} gestellt, die Größe auf den Standardwert %von 100 und der Cache-Type auf \textit{SOFT} gestellt. Hierbei wurde eine maximale Laufzeit beim ersten Aufruf ebenfalls %von 1300 ms gemessen und es wurden 12218 Abfragen abgesetzt. Bei den nachfolgenden Aufrufen lag die Aufrufzeit im %Durchschnitt bei 340 ms. %Bei WEAK hat sich die Speichernutzung nur um 5MB gesteigert %\mytodos{in einer Tabelle oder Graphen darstellen?} %Wie man an den Daten erkennen kann, wird der Cache vom \ac{JPA} für diese Abfrage nicht verwendet, sonst müssten die %Anzahl der Abfragen an die Datenbank drastisch reduziert werden. Selbst die Laufzeit ändert sich nur marginal. \section{Caching in \ac{EJB}} \label{sec:performance-investigation-application:caching-ejb} Die Cache-Einstellungen des \ac{EJB} sind in der Admin-Oberfläche des Payara-Servers zu erreichen. Hier \mytodos{Cache config noch definieren} \begin{table}[h!] \centering \resizebox{\textwidth}{!}{ \begin{tabular}{|r|r|r|r|r|r|r|r|r|r|r|r|r|r|r|} \hline & \multicolumn{3}{c|}{Aufrufzeit (ms)} & \multicolumn{2}{c|}{Queries (ms)} & \multicolumn{3}{c|}{Memory (MB)} & \multicolumn{3}{c|}{Render (ms)} & \multicolumn{3}{c|}{DB-load (ms)} \\ \hline \# & min & avg & max & \#"=avg & avg & start & stop & diff & min & avg & max & min & avg & max \\ \hline 1 & 416 & 554 & 1269 & 12237 & 840.31 & 998.07 & 157.76 \\ 2 & 299 & 394 & 749 & 12080 & 973.20 & 1101.37 & 128.17 \\ 3 & 293 & 324 & 382 & 12080 & 1092.00 & 1192.87 & 100.87 \\ 4 & 281 & 318 & 398 & 12080 & 1191.25 & 1305.29 & 114.04 \\ \hline \end{tabular} } \caption{Messung mit \ac{EJB}-Cache} \label{tbl:measure-ejb-cache-active} \end{table} \section{Abfragen \ac{JPQL}} \label{sec:performance-investigation-application:query-jpql} Für die \ac{JPQL} wird ein \ac{SQL} ähnlicher Syntax verwendet um die Abfragen an die Datenbank durchzuführen. Für die Dokumentenliste wird der Code aus \ref{lst:jpql-document-list-jpql} verwendet. Die Namen mit vorangestellten Doppelpunkt sind Übergabevariablen. \begin{lstlisting}[language=Java,caption={JPQL Dokumentenliste},label=lst:jpql-document-list-jpql] SELECT DISTINCT d FROM Document d LEFT JOIN FETCH d.authorPerson LEFT JOIN FETCH d.coauthorPersonSet LEFT JOIN FETCH d.addresseePersonSet WHERE d.validUntil > :now AND d.isPublishedInDb = :published ORDER BY d.documentId ASC \end{lstlisting} In dem dazugehörigen Code am Server wird der JPQL-Code als NamedQuery hinterlegt und über den Name \textit{Document.findAll} referenziert. Eine Veränderung der Abfrage ist hier leider nicht möglich, wie man im Code \ref{lst:jpql-document-list} sehen kann. \begin{lstlisting}[language=Java,caption={Java JPQL Dokumentenliste},label=lst:jpql-document-list] List myResultList = createNamedTypedQuery("Document.findAll") .setParameter("now", _IncludeDeleted ? new Date(0) : Date.from(LocalDateTime.now().atZone(ZoneId.systemDefault()).toInstant())) .setParameter("published", true) .setFirstResult(_Start) .setMaxResults(_Size) .setHint("javax.persistence.query.fetchSize", _Size) .getResultList(); // Uebergabe der Ergebnisliste if(myResultList != null && !myResultList.isEmpty()) { myResult.addAll(myResultList); } \end{lstlisting} Da dieser Code direkt so aus dem Projekt kommt, wird hierfür keine gesonderte Zeitmessung durchgeführt, da diese der Messung \ref{tbl:measure-without-cache} entspricht. \section{Abfragen Criteria API} \label{sec:performance-investigation-application:query-criteria-api} Für die Criteria API wird die Abfrage nicht in einem SQL-Dialekt beschreiben. Hierbei werden über Attribute die Verlinkung zur Datenbank durchgeführt. An der Klasse selbst wird der Tabellenname definiert und an den Attributen die Spaltennamen. Um die Anfrage durchführen muss nun nur noch Datenklasse angegeben werden und mit den Parametern versorgt werden, wie es in \ref{lst:criteria-api} gezeigt wird. \begin{lstlisting}[language=Java,caption={Criteria API Dokumentenliste},label=lst:criteria-api] CriteriaBuilder cb = getEntityManager().getCriteriaBuilder(); CriteriaQuery cq = cb.createQuery(Document.class); Root from = cq.from(Document.class); ParameterExpression includedPara = cb.parameter(Boolean.class, "published"); ParameterExpression validPart = cb.parameter(Date.class, "now"); CriteriaQuery select = cq.select(from) .where(cb.and( cb.equal(from.get("isPublishedInDb"), includedPara), cb.greaterThan(from.get("validUntil"), validPart) )); TypedQuery typedQuery = getEntityManager().createQuery(select) .setParameter("now", _IncludeDeleted ? new Date(0) : Date.from(LocalDateTime.now().atZone(ZoneId.systemDefault()).toInstant())) .setParameter("published", true) .setFirstResult(_Start) .setMaxResults(_Size) .setHint("javax.persistence.query.fetchSize", _Size); List myResultList = typedQuery.getResultList(); // Uebergabe der Ergebnisliste if (myResultList != null && !myResultList.isEmpty()) { myResult.addAll(myResultList); } \end{lstlisting} Wie in der Messung \ref{tbl:measure-criteria-api} zu sehen, unterscheiden sich die Abfragezeiten nur marginal von denen mit \ac{JPQL}. Wenn man sich den Code im Debugger anschaut, sieht man auch, dass die zusammengesetzten Abfragen in den Java-Objekten fast identisch sind. Und in der Datenbank sind die Anfragen identisch zu denen über JPQL. \begin{table}[h!] \centering \resizebox{\textwidth}{!}{ \begin{tabular}{|r|r|r|r|r|r|r|r|r|r|r|r|r|r|r|} \hline & \multicolumn{3}{c|}{Aufrufzeit (ms)} & \multicolumn{2}{c|}{Queries (ms)} & \multicolumn{3}{c|}{Memory (MB)} & \multicolumn{3}{c|}{Render (ms)} & \multicolumn{3}{c|}{DB-load (ms)} \\ \hline \# & min & avg & max & \#"=avg & avg & start & stop & diff & min & avg & max & min & avg & max \\ \hline 1 & 396 & 572 & 1535 & 12173 & 796.59 & 970.10 & 173.51 \\ 2 & 333 & 366 & 397 & 12080 & 982.28 & 1064.12 & 81.84 \\ 3 & 286 & 339 & 554 & 12080 & 1048.12 & 1162.92 & 114.80 \\ 4 & 293 & 317 & 388 & 12080 & 1150.43 & 1263.77 & 113.34 \\ \hline \end{tabular} } \caption{Messung mit Criteria-API ohne Cache} \label{tbl:measure-criteria-api} \end{table} Daher bringt die Criteria API keinen performance Vorteil gegenüber der JPQL!=Implementierung. Somit können beide Implementierung ohne bedenken gegeneinander ausgetauscht werden, und die verwendet werden, die für den Anwendungsfall einfacher umzusetzen ist. \section{materialized views} \label{sec:performance-investigation-application:materialized-views} Materialized Views sind Sichten in der Datenbank, die beim erstellen der Sicht den aktuellen Zustand ermitteln und Zwischenspeichern. Somit wird beim Zugriff auf diese Sichten, nicht die hinterlegte Abfrage ausgeführt, sondern auf die gespeicherten Daten zugegriffen. Dies ist gerade bei vielen Joins von Vorteil. Zusätzlich können auf solchen Sichten auch Indexe erstellt werden, um noch effektiver die Abfragen bearbeiten zu können. Der größte Nachteil dieser Sichten ist, dass sie zyklisch oder bei Datenänderungen aktualisiert werden müssen, sonst läuft der Datenbestand der Sicht und der zugrundeliegenden Abfrage auseinander. Da die Hauptarbeiten auf der Webseite die Abfrage der Daten ist, und nicht das editieren, kann dieser Nachteil bei entsprechender Optimierung igoriert ewrden. In diesem Test, wurde zusätzlich zur normalen Abfragen noch die nachfolgenden einzelabfragen als Sub-Selects hinzugefügt, wie in \ref{lst:sql-materialized-view} zu sehen. Somit können die nachfolgenden einzelnen Abfragen eingespart werden. Dies wiederrum geht aber auf die Performance der Erstellung der Sicht und ihrer Aktualisierung. \begin{lstlisting}[language=SQL,caption={SQL Materialized View},label=lst:sql-materialized-view] CREATE MATERIALIZED VIEW searchdocument AS SELECT d.id, d.documentId, d.datetype, d.startdatestatus, d.startyear, d.startmonth, d.startday, d.enddatestatus, d.endyear, d.endmonth, d.endday, ( SELECT jsonb_build_object( 'personId', hp.personid, 'surname', hp.surname, 'firstname', hp.firstname, 'dateBirth', json_build_object( 'year', hp.birthstartyear, 'month', hp.birthstartmonth, 'day', hp.birthstartday ), 'dateDeath', json_build_object( 'year', hp.deathstartyear, 'month', hp.deathstartmonth, 'day', hp.deathstartday ) ) FROM historicalperson hp WHERE hp.id = d.authorperson_id AND hp.validuntil > NOW() ) as author, ( SELECT jsonb_agg(jsonb_build_object( 'personId', hcap.personid, 'surname', hcap.surname, 'firstname', hcap.firstname, 'dateBirth', json_build_object( 'year', hcap.birthstartyear, 'month', hcap.birthstartmonth, 'day', hcap.birthstartday ), 'dateDeath', json_build_object( 'year', hcap.deathstartyear, 'month', hcap.deathstartmonth, 'day', hcap.deathstartday ) )) FROM documentcoauthorperson dcap JOIN historicalperson hcap ON hcap.id = dcap.authorperson_id AND dcap.validuntil > NOW() AND hcap.validuntil > NOW() WHERE dcap.document_id = d.id ) AS coauthors, ( SELECT jsonb_agg(jsonb_build_object( 'personId', hap.personid, 'surname', hap.surname, 'firstname', hap.firstname, 'dateBirth', json_build_object( 'year', hap.birthstartyear, 'month', hap.birthstartmonth, 'day', hap.birthstartday ), 'dateDeath', json_build_object( 'year', hap.deathstartyear, 'month', hap.deathstartmonth, 'day', hap.deathstartday ) )) FROM documentaddresseeperson dap JOIN historicalperson hap ON hap.id = dap.addresseeperson_id AND dap.validuntil > NOW() AND hap.validuntil > NOW() WHERE dap.document_id = d.id ) AS addressees, sc.city, d.documentcategory, d.ispublishedindb, d.createdat, d.modifiedat, d.validuntil FROM document d LEFT JOIN sitecity sc ON sc.id = d.city_id; \end{lstlisting} % document, first/last, documentaddresseeperson, documentcoauthorperson, documentfacsimile und count % document, count, first/last \begin{table}[h!] \centering \resizebox{\textwidth}{!}{ \begin{tabular}{|r|r|r|r|r|r|r|r|r|r|r|r|r|r|r|} \hline & \multicolumn{3}{c|}{Aufrufzeit (ms)} & \multicolumn{2}{c|}{Queries (ms)} & \multicolumn{3}{c|}{Memory (MB)} & \multicolumn{3}{c|}{Render (ms)} & \multicolumn{3}{c|}{DB-load (ms)} \\ \hline \# & min & avg & max & \#"=avg & avg & start & stop & diff & min & avg & max & min & avg & max \\ \hline 1 & 203 & 315 & 808 & 17.8 & 3.0 & 851.4 & 883.9 & 32.5 \\ % 178 - 30 ms (19+11+0) (#2,4,8) 2 & 154 & 172 & 187 & 9.0 & 2.2 & 883.2 & 887.0 & 3.8 \\ % 268 - 52 ms (33+18+1) (#2,3,8) 3 & 145 & 151 & 163 & 9.0 & 2.8 & 887.7 & 895.3 & 7.6 \\ % 358 - 80 ms (52+27+1) (#2,3,8) 4 & 132 & 143 & 152 & 9.0 & 2.8 & 896.0 & 900.0 & 4.0 \\ % 448 - 108 ms (70+37+1) (#2,3,8) 5 & 121 & 125 & 132 & 9.0 & 2.4 & 900.6 & 901.0 & 0.4 \\ % 538 - 132 ms (85+46+1) (#2,3,8) \hline \end{tabular} } \caption{Messung mit Materialized View} \label{tbl:measure-materialized-view} \end{table} Wie in Tabelle \ref{tbl:measure-materialized-view} zu sehen, bringt die Verwendung der Materialized View ein Verbesserung in verschiedenen Punkten. Zum einen ist eine Verbesserung der Aufrufzeiten zu erkennen, zusätzlich fällt der Speicheranstieg weniger stark aus. Die Verbesserung der Aufrufzeiten lässt sich zusätzlich erklären, dass hier nun nur noch vier statt der 6 Abfragen an die Datenbank gestellt werden, da einzelabfragen für die Adressen der Personen und der CoAutoren komplett entfallen. Nach dem der Quellcode nochmal untersucht wurde, konnte man festellen, dass bei jeder Anfrage die gleiche Bedingung benötigt wurde. Da die Sicht nun explizit für dies Anfrage geschaffen wurde, wurde die Bedingungen nun direkt in Sicht mit integriert. Dies bedeutet eine Erweiterung der Sicht aus \ref{lst:sql-materialized-view} um \ref{lst:sql-materialized-view-ext} und das entfernen der Parameter aus dem SQL-Anfragen im Java-Code. \begin{lstlisting}[language=SQL,caption={SQL Materialized View Erweiterung},label=lst:sql-materialized-view-ext] WHERE d.validuntil > NOW() AND d.ispublishedindb = true; \end{lstlisting} \mytodos{Die Indizies noch mit aufnehmen!} Nach dem Anpassungen haben sich dann die Werte aus \ref{tbl:measure-materialized-view-ext} ergeben. \begin{table}[h!] \centering \resizebox{\textwidth}{!}{ \begin{tabular}{|r|r|r|r|r|r|r|r|r|r|r|r|r|r|r|} \hline & \multicolumn{3}{c|}{Aufrufzeit (ms)} & \multicolumn{2}{c|}{Queries (ms)} & \multicolumn{3}{c|}{Memory (MB)} & \multicolumn{3}{c|}{Render (ms)} & \multicolumn{3}{c|}{DB-load (ms)} \\ \hline \# & min & avg & max & \#"=avg & avg & start & stop & diff & min & avg & max & min & avg & max \\ \hline 1 & 241 & 348 & 859 & 16.8 & xxx & 896.0 & 932.4 & xxxx & 232 & 331 & 803 & 132 & 174 & 334 \\ % 168 - 2 & 164 & 194 & 225 & 9.0 & xxx & 933.3 & 935.9 & xxxx & 154 & 185 & 215 & 79 & 99 & 117 \\ % 258 - 3 & 147 & 161 & 179 & 9.0 & xxx & 935.8 & 938.8 & 3.0 & 139 & 152 & 167 & 68 & 77 & 86 \\ % 348 - 4 & 135 & 145 & 183 & 9.0 & xxx & 939.4 & 936.0 & -3.4 & 127 & 137 & 174 & 70 & 73 & 75 \\ % 438 - 5 & 126 & 137 & 154 & 9.0 & xxx & 936.1 & 939.1 & 3.0 & 118 & 129 & 143 & 66 & 72 & 79 \\ % 528 - \hline \end{tabular} } \caption{Messung mit erweiterter Materialized View} \label{tbl:measure-materialized-view-ext} \end{table} %\mytodos{hier noch darauf eingehen, dass die Hauptarbeit nicht beim editieren sondern bei der Anzeige ist} \mytodos{Das Render des Json in der View Betrachten, scheint der aktuelle Kostenpunkt zu sein} \mytodos{Hier könnte man auch den Query-Cache nochmal verwenden, da die anfragen nun fix wären} \mytodos{Grundlagen zur Materialized-View noch hinterlegen} \section{Statische Webseiten} \label{sec:performance-investigation-application:static-website} Wenn man die Dokumentenliste als statische Webseiten ablegt, werden die Zugriffszeiten sehr kurz sein. Darüber hinaus funktionieren in statische Webseiten aber keine Suche oder eine Sortierung. Die Sortierung könnte durch das erstellen von statischen Seite aller Möglichkeiten der Sortierung emuliert werden, diese würde den notwendigen Speicherbedarf der Webseite vervielfachen. Für die Suchanfragen ist dies nicht mehr möglich, da nicht alle Suchanfragen vorher definiert werden können. Die Umstellung der Suche auf Client!=Basis wäre noch eine Möglichkeit, dafür benötigen die Clients entsprechend Leistung und es muss eine Referenzdatei erstellt werden, die alle Informationen über die Dokumente beinhaltet, nach der gesucht werden kann. Daher ist eine Umstellung auf statische Webseiten nicht sinnvoll. \section{Client basierte Webseiten} \label{sec:performance-investigation-application:client-side-rendering} Als weitere Möglichkeit könnte man die Webseite so umbauen, dass die Daten erst im Nachgang über eine AJAX-Anfrage ermittelt und die Sortierung und Aufteilung im Client durchgeführt wird. Hierbei wird aber je nach Datenmenge ein großer Speicher am Client benötigt und die Rechenleistung wird auf den Client verschoben. Dies wiederrum ist ein Vorteil für den Serverbetreiber, da durch die Verschiebung weniger Rechenleistung am Server benötigt wird. Gleichzeitig würde man damit wiederrum schwächere Clients, wie Smartphones, aussperren, da bei diesem die notwendige Rechenleistung fehlt, um die Webseite in annehmbarer Zeit darzustellen.