bachelor-thesis/chapters/thesis/chapter05.tex
2024-09-18 00:19:25 +02:00

747 lines
44 KiB
TeX

% !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 circa 1500 ms
gedauert hat. Die weiteren Aufrufe benötigen 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 circa 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
circa 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 circa 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 im \autoref{ap:calling_script} verwendet, welches die einzelnen
Aufrufe steuert. Die Ergebnisse werden in eine Tabelle überführt, wie in der \autoref{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 \autoref{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.
\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 Abfragen sind durch das Erstellen der Materialisierten Sichten
\textit{searchreference} und \textit{searchfulltext} erklärbar. Zusätzlich ist noch ein zyklischer Dienst
\textit{SearchEntityService} vorhanden, der zum Start und alle sechs Stunden den Datenbestand für die Suche aufbereitet
und entsprechend einige Abfragen an die Datenbank absetzt. Da weder die Sichten noch der Dienst für die Dokumentenliste
benötigt werden, wurde der Dienst und das Erstellen im Code für die 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
\autoref{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 Koautoren, 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. Eine weitere Möglichkeit wäre es, die Duplikate auf der
Serverseite zusammenzufü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 Referenzen 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
\autoref{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
\autoref{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}[!ht]
\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
\autoref{tbl:measure-ojpa-active-bigger-no-softref} ersichtlich.
% 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}
Der 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 die Menge
der Objekte den Cache übersteigt, fällt die Verbesserung geringer 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 in der
\autoref{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}
\mytodos{Queryzeiten fehlen nocht}
\section{Caching mit Ehcache}
\label{sec:performance-investigation-application:caching-ehcache}
Der Ehcache ist ein L2"=Cache den man direkt in OpenJPA mit integrieren kann. Hierfür sind einige Punkte zu beachten.
Zum einen muss die Reference auf das \textit{ehcache} und das \textit{ehcache"=openjpa} Packet hinzugefügt werden.
Zusätzlich dazu sind die Konfiguration \textit{openjpa.""QueryCache} und \textit{openjpa.""DataCache} auf
\textit{ehcache} anzupassen. Die Objekte oder mindestens das Hauptobjekt benötigt noch die Annotation
\textbf{@DataCache(name = "userCache")}, damit der Cache für die Klassen aktiviert wird. Als letztes muss noch der
Cache"=Manager aktiviert werden, dieser kann entweder durch Code programmiert werden oder über eine Konfiguration
in der \textit{ehcache.xml}.
Anhand der Auswertung von \ref{tbl:measure-ehcache-active} fällt auf, dass der Cache keine Geschwindigkeitsvorteile
bringt. Zusätzlich werden trotz aktiven Cache die Anzahl der Anfragen an die Datenbank nicht reduziert. Gleichzeitig
sieht man aber anhand der Statistik"=Ausgaben, dass der Ehcache verwendet wird und entsprechende Hits hat und der
OpenJPA"=Cache nicht mehr verwendet wird. Zusätzlich steigt der Speicherverbrauch stärker als in anderen Fällen.
% 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
%- & 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)
1 & 450 & 934 & 3026 & 1095.2 & xxxx & 977.1 & 1027.0 & 49.9 & 592 & 978 & 3015 & 464 & 656 & 1348 \\ % 10952 - ... ms ( .+ .+ .+.+.+.) (#.)
2 & 503 & 563 & 666 & 1206.0 & xxxx & 1028.0 & 1126.0 & 98.0 & 494 & 554 & 555 & 395 & 448 & 453 \\ % 23012 -
3 & 431 & 522 & 586 & 1206.0 & xxxx & 1137.0 & 1370.0 & 233.0 & 422 & 513 & 575 & 341 & 430 & 494 \\ % 35072 -
4 & 421 & 497 & 635 & 1206.0 & xxxx & 1371.0 & 1469.0 & 98.0 & 414 & 490 & 627 & 337 & 414 & 551 \\ % 47132 -
5 & 444 & 511 & 600 & 1206.0 & xxxx & 1469.0 & 1662.0 & 193.0 & 436 & 503 & 589 & 362 & 420 & 505 \\ % 59192 -
\hline
\end{tabular}
}
\caption{Messung mit aktiviertem Ehcache}
\label{tbl:measure-ehcache-active}
\end{table}
\section{Caching in 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 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 dem \autoref{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
aus \autoref{lst:jpql-document-list} sehen kann.
\begin{lstlisting}[language=Java,caption={Java JPQL Dokumentenliste},label=lst:jpql-document-list]
List<Document> 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 aus \autoref{tbl:measure-without-cache} entspricht.
Für die Optimierung wurden noch zusätzlich die Hints \textit{openjpa.""hint.""OptimizeResultCount},
\textit{javax.""persistence.""query.""fetchSize} und \textit{openjpa.""FetchPlan.""FetchBatchSize} gesetzt. Hierbei
konnten je nach gesetzten Wert, keine relevanten Unterschiede festgestellt werden. Hierbei wurde der Wert auf zwei
gesetzt, welcher viel zu gering ist. Als weiterer Test wurde der Wert auf angefragte Größte gestellt und auf den
20"=fachen Wert der angefragten Größe.
Ebenso bringt der Hint \textit{openjpa.""FetchPlan.""ReadLockMode} auch keinen Unterschied bei der Geschwindigkeit.
Hierbei ist erklärbar, da im Standard bei einer reinen Selektion eine Lesesperre aktiv sein muss.
Bei \textit{openjpa.""FetchPlan.""Isolation} wird gesteuert, auf welche Sperren beim laden geachtet wird. Damit könnte
man zwar schreibsperren umgehen, und würde damit die Anfrage nicht mehr blockieren lassen, aber es führt unweigerlich
zu sogenannten \glqq Dirty"=Reads\grqq, wodurch die Ausgabe verfälscht werden könnte. Daher ist diese Einstellung sehr
mit Vorsicht zu verwenden.
Mit dem Hint \textit{openjpa.""FetchPlan.""EagerFetchMode} wird definiert, wie zusammengehörige Objekte abgefragt werden.
Bei dem Wert \textit{none} werden nur die Basis"=Daten abgefragt und jedes weitere Objekt wird in einem eigenen
Statement abgefragt. Mit \textit{join} wird definiert, dass abhängige Objekte die als \glqq to-one\grqq"=Relation
definiert sind, in der Abfrage über einen Join verknüpft und damit direkt mitgeladen werden. Bei reinen
\glqq to-one\grqq"=Relation funktioniert das rekursive und spart sich damit einige einzelne Abfragen.
Bei der Einstellung \textit{parallel} wird für zwar für jedes abhängigen Objektdefinition eine Abfrage durchgeführt,
aber bei dieser wird der Einstieg über das Hauptobjekt durchgeführt. Somit muss in unserem Beispiel nicht für jedes
Dokument eine einzelne abfrage für die Koautoren durchgeführt werden, sondern es wird nur eine Abfrage abgesetzt für
alle Dokumente die ermittelt wurden. Technisch gesehen wird, die gleiche WHERE"=Abfrage nochmal durchgeführt und um
die JOINS ergänzt, um die Daten der Unterobjekte zu ermitteln.
Mit dem Hint \textit{openjpa.""FetchPlan.""SubclassFetchMode} ist die Konfiguration für Unterklassen definiert. Die
Möglichkeiten entsprechen der vom \textit{openjpa.""FetchPlan.""EagerFetchMode}.
Beim Umstellen der 2 Hints auf \textit{parallel} wird die Bearbeitungszeit fast halbiert und Anzahl der Datenbankaufrufe
wurde fast geviertelt. Dies zeigt, dass die einzelnen Aufrufe je Dokument aufwendiger sind, als eine komplette Abfrage
der abhängigen Daten und das zusammensetzen in der OpenJPA-Schicht.
Der letzte Hint \textit{openjpa.""FetchPlan.""MaxFetchDepth} schränkt die rekursive Tiefe ein, für die abhängige
Objekte mitgeladen werden. Lediglich auf Grund fehlender Datenbestände wird die Abfrage beschleunigt.
\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 \autoref{lst:criteria-api} gezeigt wird.
\begin{lstlisting}[language=Java,caption={Criteria API Dokumentenliste},label=lst:criteria-api]
CriteriaBuilder cb = getEntityManager().getCriteriaBuilder();
CriteriaQuery<Document> cq = cb.createQuery(Document.class);
Root<Document> from = cq.from(Document.class);
ParameterExpression<Boolean> includedPara = cb.parameter(Boolean.class, "published");
ParameterExpression<Date> validPart = cb.parameter(Date.class, "now");
CriteriaQuery<Document> select = cq.select(from)
.where(cb.and(
cb.equal(from.get("isPublishedInDb"), includedPara),
cb.greaterThan(from.get("validUntil"), validPart)
));
TypedQuery<Document> 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<Document> myResultList = typedQuery.getResultList();
// Uebergabe der Ergebnisliste
if (myResultList != null && !myResultList.isEmpty()) {
myResult.addAll(myResultList);
}
\end{lstlisting}
Wie in der Messung in \autoref{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.
Bei den Hints ist es das gleiche wie bei \ac{JPQL}. Auch hier haben die meisten Hints keinen merkbaren Einfluss. Die
Einstellung \textit{openjpa.""FetchPlan.""EagerFetchMode} liefert auch hier Optimierungen, wenn der Wert auf
\textit{parallel} gestellt wird. Hier wird ebenfalls die Anzahl der Anfragen reduziert und damit auch die
Geschwindigkeit optimiert.
\section{Optimierung der Abfrage}
\label{sec:performance-investigation-application:optimizing-query}
Für die Optimierung der Abfrage wird zuerst die Hauptabfrage ermittelt, die auch in \autoref{lst:documentlist} zu
sehen 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 ignoriert werden.
In diesem Test, wurde zusätzlich zur normalen Abfragen noch die nachfolgenden einzelabfragen als Sub-Selects
hinzugefügt, wie in \autoref{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}
Zusätzlich werden noch einige Indexe hinzugefügt, für eine bessere Performance bei der Abfrage, wie in
\autoref{lst:sql-materialized-view-index} zu sehen.
\begin{lstlisting}[language=SQL,caption={SQL Materialized View},label=lst:sql-materialized-view-index]
CREATE INDEX idx_searchdocument_documentid
ON searchdocument (documentid);
CREATE INDEX idx_searchdocument_author_surname_firstname
ON searchdocument ((author->>'surname'), (author->>'firstname'));
CREATE INDEX idx_searchdocument_startdate
ON searchdocument (startyear, startmonth, startday);
CREATE INDEX idx_searchdocument_addressees_first_entry
ON searchdocument
( ((addressees->0->>'surname')::text)
, ((addressees->0->>'firstname')::text));
CREATE INDEX idx_searchdocument_city
ON searchdocument (city);
CREATE INDEX idx_searchdocument_documentcategory
ON searchdocument (documentcategory);
\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 \autoref{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 Koautoren 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 \autoref{lst:sql-materialized-view} um
\autoref{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}
Nach dem Anpassungen haben sich dann die Werte aus \autoref{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}
Da bei der Materialized View das laden der Daten und das wandeln in die Java"=Objekte getrennt programmiert wurde,
können hier eigene Zeitmessungen für die zwei Schritte eingebaut werden. Hierfür wird die Zeit vor dem
\textit{map}"=Aufruf und der \textit{map}"=Aufruf gemessen. Für den ersten Aufruf, wurde ein \textit{SearchDocument}
Objekt erzeugt und immer diese Objekt zurückgegeben. Damit wurde erst mal überprüft, wie lange das ermitteln der Daten
und das durcharbeiten der Ergebnisse bestimmt. Hierbei lagen die Zeiten bei circa 1 ms für das reine Datenladen und 3 ms
für den Aufruf der \textit{map}"=Funktion. Sobald mal innerhalb der \textit{map}"=Funktion pro Eintrag ein Objekt
erzeugt, noch ohne eine Konvertierung der ermittelten Daten in das Objekt, steigt die Laufzeit schon auf 54 ms.
Wenn man nun noch die Konvertierung der Daten wieder einbaut, steigt die Laufzeit nochmal auf nun 82 ms.
Dies zeigt, alleine das erzeugen der Objekt kostet die meiste Zeit.
Bei der Verwendung des Hints \textit{openjpa.""FetchPlan.""FetchBatchSize} kann die Abfrage enorm verschlechtern. Wenn
dieser Wert zu klein oder groß definiert ist, wird die Laufzeit verschlechtert. Bei einem zu großen Wert wird die
Laufzeit der Datenbankanfrage auf circa 20 ms verlängert. Wenn der Wert zu gering gewählt ist, dann wird zwar die
Laufzeit der Datenbankanfrage minimal verkürzt, aber die \textit{map}"=Funktion wird dadurch verlängert.
Das aktivieren der Cache"=Optionen wie in \autoref{sec:performance-investigation-application:caching-openjpa} oder in
\autoref{sec:performance-investigation-application:cached-query} dargestellt, haben keine Auswirkung auf die Performance.
Dies ist dadurch erklärbar, da keine Objekte durch das OpenJPA"=Framework erstellt werden, sondern erst in der
\textit{map}"=Funktion des eigenen Codes.
Nun wird noch geprüft, welche Performance das parsen der Json"=Informationen im Server benötigt. Im ersten Schritt wird
die Parse-Funktion auskommentiert und die Seite nochmal aufgerufen. Durch diese Umstellung fällt die Laufzeit der
Datenermittlung auf circa 4 ms ab. Nun muss noch geprüft werden, welche Zeit nun der Client zum parsen der
\textit{Json}"=Daten benötigt. Hierfür werden die Daten in einem versteckten \textbf{span}"=Element hinterlegt, wie es
im \autoref{lst:jsf-datatable-json} zu sehen ist. Die hinterlegte \textit{CSS}"=Klasse ist zum auffinden der Elemente
für den späteren Javascript. Das \textbf{ajax}"=Element im Beispiel ist notwendig, damit bei einem Seitenwechsel die neu
übertragenen Elemente in eine lesbare Form gebracht werden.
\begin{lstlisting}[language=xml,caption={DataTable mit Json},label=lst:jsf-datatable-json]
<p:ajax event="page" oncomplete="convertJsonData()"/>
<p:column id="author" headerText="#{lang.List_Docs_Author}" sortable="true" sortBy="#{myObj.surname}">
<h:outputText styleClass="json-convert" style="display: none;" value="#{myObj.authorJson}"/>
</p:column>
<p:column id="Addressee"
headerText="#{lang.List_Docs_Addressee}"
sortable="true"
sortBy="#{(myObj.addresseePersonSet!=null and myObj.addresseePersonSet.size() > 0)?myObj.addresseePersonSet[0].addresseePerson:''}">
<h:outputText styleClass="json-convert" style="display: none;" value="#{myObj.adresseeJson}" data="abc"/>
</p:column>
\end{lstlisting}
Um nun die übertragenen \textit{Json}"=Daten in eine darstellbare Form zu bringen, benötigt man noch eine
JavaScript"=Funktion. Die Funktion aus \autoref{lst:jsf-datatable-json-convert} ermittelt erst alle versteckten
Elemente, parsed den Inhalt und erstellt neue \textit{HTML}"=Elemente mit dem darzustellenden Inhalt. Zusätzlich wird
noch eine Zeitmessung mit eingebaut, um die Laufzeit am Client für das Rendern in der Konsole anzuzeigen. Die Funktion
wird nun direkt nach dem die Webseite fertig geladen wurde aufgerufen.
\begin{lstlisting}[language=javascript,caption={Wandeln von Json nach Html},label=lst:jsf-datatable-json-convert]
function isEmpty(str) {
return (str === null) || (str === undefined) || (typeof str === "string" && str.length === 0);
}
function convertJsonData() {
let $jsonObj = $(".json-convert")
, start = new Date()
;
$.each($jsonObj, function() {
let json = this.innerHTML
, strEmpty = (json === null) || (typeof json === "string" && json.length === 0)
, jsonDat = strEmpty ? null : JSON.parse(json)
;
if(!strEmpty) {
let res = ""
, $that = $(this)
, $par = $that.parent()
;
$.each(jsonDat, function() {
let hasOnlyOne = isEmpty(this.surname) || isEmpty(this.firstname)
, pseudonymExists = !isEmpty(this.pseudonym)
, namePart = "<span>" + (hasOnlyOne ? this.surname + this.firstname : this.surname + ", " + this.firstname) + "</span><br/>"
, pseudoPart = pseudonymExists ? "<span class='w3-small w3-text-dark-gray'>" + this.pseudonym + "</span><br/>" : ""
;
res += namePart + pseudoPart;
});
$par.append(res);
}
});
let end = new Date()
, diff = (end - start)
;
console.log(Math.round(diff) + " ms");
}
$(document).ready(function() {
convertJsonData();
});
\end{lstlisting}
Da nun am Client Code ausgeführt wird, nachdem die Daten übertragen wurden, kann nicht mehr alles über das Script
durchgeführt werden. Daher werden nun die Laufzeiten am Server und am Client zusammenaddiert. Im Schnitt benötigt der
Aufruf auf der Serverseite nun 70 ms und am Client sind es circa 13 ms. Dies bedeutet addiert kommt man mit dieser
Lösung auf eine kürzere Laufzeit und weniger Last am Server.
\mytodos{Grundlagen zur Materialized-View noch hinterlegen}