Lade...
 

Performance (InstantView)

Performantes InstantView

Die Performanz einer Anwendung ist in erster Linie von der Performance der Infrastruktur abhängig, aber natürlich auch vom abzuarbeitendem Programmcode der Anwendung.

Der eher an der reinen Anwendung interessierte InstantView®-Entwickler soll so weit wie möglich von Überlegungen zur Performance befreit werden. In diesem Sinne realisiert die CyberEnterprise®-Architektur einige Konzepte einer automatischen Optimierung:

  • Impliziter Start einer Datenbanktransaktion zum spätest möglichen Zeitpunkt
  • Anordnung neu erzeugter der Objekte in der Datenbank ist durch die einmal zu treffende Beschreibung des Datenbanklayouts gesteuert. Zusammenfassung logisch stark miteinander verbundener Objekte durch das Konzept "Lazy Creator"
  • Write-Lock für ein Objekt nur dann, wenn tatsächlich "abweichende" Daten in das Objekt geschrieben werden (dieser Mechanismus kann jedoch auch zu Deadlocks führen).
  • Optimierte Windowobjekte wie ObjectListView.

Solange in einem Verarbeitungsschritt mit nur wenigen Objekten  (< 2000) gearbeitet wird, sind Optimierungen im InstantView®-Code überflüssig.

Das Gegenteil trifft (leider) zu, sobald viele Objekte dargestellt, getestet oder auf irgend eine andere Weise in den algorithmischen Ablauf einbezogen werden.
Dann können einfache Änderungen unter Umständen Zeiten von über einer Stunde auf wenige Minuten reduzieren.

Folgende InstantView®-Elemente werden hinsichtlich des Zeitbedarfs genauer betrachtet:

 

1. Vektoren

Zunächst ein Beispiel: Mit FindAll(CX_ITEM) findet man in einer Datenbank 100.000 Objekte dieser Klasse. Nach einem Kriterium (hier: Datenfeld uniqueID beginnt mit 'A') werden Objekte ausgewählt und in einem Vektor für folgende Verarbeitungsschritte festgehalten. Im Test erfüllt genau die Hälfte aller Objekte das Auswahlkriterium. Damit wird der Aufbau des Vektors entscheidend für den Zeitbedarf.

Var(vector)

Define(BadExample)    // schlechte Performance
  [] -> vector
  FindAll(CX_ITEM)
  iterate
  {
    LocalVar(item) -> item
    1 item Copy(uniqueID) Left "A" = if { item vector | -> vector }
  }
;



Define(MuchBetterExample)   // deutlich besser
  [] -> vector
  FindAll(CX_ITEM)
  iterate
  {
    LocalVar(item) -> item
    1 item Copy(uniqueID) Left "A" = if { item vector Insert }
  }
;

Beide Codebeispiele führen zum gewünschten Resultat. Hinsichtlich des Zeitbedarfs gibt es aber signifikante Unterschiede:

perform1.png

Woran liegt das?

Im ersten Beispiel wird für jedes ausgewählte Objekt - also 50 000 mal - ein neuer Vektor erzeugt.

Die Anweisungen

    element vector1 | -> vector2

sind sinnvoll, wenn sowohl vector1 als auch vector2 weiter benötigt werden. Aber

    element vector | -> vector  

sollte immer dann, wenn es sich um viele Elemente handelt, durch

    element vector Insert

ersetzt werden.

Dies ist im zweiten Beispiel der Fall, und der Zeitbedarf sinkt auf unter 1/40. Die Hauptursache: Im Fall 1 müssen alle die 49 999 überflüssigen Vektoren von der Garbagecollection fortgeräumt werden.

Falls die Anzahl der Elemente in auf viele Millionen anwächst, dann kann auch die interne Reallocation des Vektors messbar werden. In diesem Fall kann als zusätzliche Optimierung der Vektor in der Zielgröße vorallokiert werden (CreateVector).

2. ObjectListView, ObjectList und ObjectCombobox

Listbox füllen

Define(BadFill)   FindAll(CX_ITEM) iterate   {     LocalVar(item) -> item     1 item Copy(uniqueID) Left "A" = if { item FillObox(, obx) } Index 2000 < ifnot break   } ; Define(GoodFill)   LocalVar(vector) [] -> vector   FindAll(CX_ITEM) iterate   {     LocalVar(item) -> item     1 item Copy(uniqueID) Left "A" = if { item vector Insert } Index 2000 < ifnot break   } vector FillObox(, obx) ;

Wie beim vorhergehenden Beispiel (Vektoren) werden Objekte nach einem Kriterium ausgewählt. Die ausgewählten Objekte sollen in Tabellenform dargestellt werden (das Windowobjekt "obx" ist ObjectListView, ObjectList oder ObjectCombobox). Lohnt es sich bei 2.000 Objekten (1.000 davon erfüllen das vorgegebene Kriterium) die ausgewählten Objekte vorher in einem Vektor zu sammeln?

Hier ist der Zeitbedarf in Sekunden wenn zwei Spalten dargestellt werden:

perform2.png
Wie man in dem Vergleich gut erkennen kann, ist die ObjectListView auf die Darstellung von vielen Objekten hin optimiert und sollte bevorzugt zur Darstellung von großen Datenmengen verwendet werden.

Der große Zeitgewinn beim Einfügen von Vektoren entsteht dadurch, dass sich die Widgets nach jedem FillObox zeichnen müssen, sodass das Einfügen einzelner Objekte in eine Liste immer langsamer sein wird, als eine ganze Collection/Vektor einzufügen, da die Liste im letzten Fall nur einmalig gezeichnet werden muss. Für die ObjectList und ObjectCombobox ist der Unterschied noch größer da hier die Spaltenbreiten nach jedem Einfügen anhand aller Einträge neu berechnet werden müssen (Flag: AUTO_POSITION), während dies in der ObjectListView nur anhand der aktuell sichtbaren Elemente durchgeführt wird.
Bei sortierten Listen ist dieser Effekt umso größer, da ein FillObox mit einzelnen Elementen in diesem Fall sortiert einfügt. Das sortierte Einfügen ist für einzelne Elemente effizient, aber bei sehr vielen Elementen ist es schneller, die Elemente in die Liste unsortiert einzufügen und die gesamte Liste anschließend einmalig zu sortieren. Der letzte Ansatz wird von FillObox automatisch gewählt, wenn eine Collection/Vektor von Elementen gefüllt wird. 
Der Geschwindigkeitsunterschied wird im folgenden Diagramm für die ObjectListView anhand von 10.000 Objekten veranschaulicht:

perform2.png

Hinweis: Die gleichen Überlegungen gelten, wenn Objekte mit UpdateObox eingefüllt werden (um Doppeldarstellungen zu vermeiden)

Schlussfolgerung

1. Wenn keine nur für ObjectList realisierten Features benötigt werden, immer ObjectListView benutzen

2. Sehr viele Objekte in Vektor bzw. transienter Collection sammeln und nur 1 x FillObox aufrufen - dies gilt besonders für ObjectList und ObjectCombobox mit Flag AUTO_POSITION.

3. Collections - Set oder List

Werden viele Objekte in einer Collection aufgesammelt, hat die Entscheidung zwischen LIST oder SET einen großen Einfluss auf die Performance:

Merkmale der Anwendung welche Collection ist vorzuziehen
Objekte sollen "aufgesammelt" werden, es gibt keine Tests auf die Existenz eines Objekts (auch nicht implizit ... siehe unten). 
Iterationsreihenfolge soll der Einfügereihenfolge entsprechen.
LIST
Es gibt Existenztests, z.B. auch implizit, wenn einfach verhindert werden soll, dass das gleiche Objekt mehrfach in der Collection vorkommt. Iterationsreihenfolge ist egal. SET
Falls Duplikate erlaubt sind, die Reihenfolge egal ist, aber schnell auf Existenz gerüft werden soll BAG

Das folgende Diagramm zeigt den Zeitaufwand für das Einfügen von n Objekten in die unterschiedlichen Collection-Typen. Daraus erkennt man deutlich, dass LIST für große Datenmengen am besten geeignet ist und bei kleineren Datenmengen die Wahl der Collection keinen großen Einfluss auf die Performance hat.

perform3a.png

 

 

Der folgende Abschnitt beschäftigt sich mit Collections vom Typ SET:

Test auf die Existenz eines Objekts

Der Test, ob ein Objekt als Element in einer Menge von Objekten enthalten ist, kann für das Zeitverhalten relevant sein.

Mengen persistenter und/oder transienter Objekte werden gebildet durch

  • (persistente und transiente) Collections
  • Vektoren (immer transient)
     

Collections existieren als array, list, set und bag.
Die InstantView®-Anweisung Contains gibt Auskunft darüber, ob ein Objekt Element einer Collection / eines Vektors ist.

Zeitbedarf für diesen Existenz-Test mit einem Vektor / einer Collection mit n Elementen:

  Zeitbedarf in Abhängigkeit von n ist proportional zu
Vektor n
Collection (array) n
Collection (list) n
Collection (set) log n
Collection(bag) log n

 

Ist die Objektmenge ein Vektor oder eine Collection vom Typ array oder list, so lohnt es sich bei einer hinreichend großen Anzahl von Tests, die Objektmenge in eine transiente Collection vom Typ set zu kopieren und gegen diese Kopie zu testen.

Was ist mit einer "hinreichend großen" Anzahl gemeint?
Im folgenden Beispiel wird eine Collection mit 10.000 bzw. 100.000 Elementen (=ELEMENT_COUNT) durchsucht. Der nachfolgende Code sucht dabei (TEST_COUNT) zufällige Elemente aus der Collection raus und misst, wie lange die Prüfung auf Existenz dieser Elemente insgesamt dauert.

Contains mit LIST
Var(ELEMENT_COUNT) 10000 -> ELEMENT_COUNT Var(TEST_COUNT) 10000 -> TEST_COUNT Var(coll, vecCopy) [] -> vecCopy CreateTransCollection(LIST) -> coll // Create our collection to test and a vector copy FindAll(CX_PERSON) iterate { Index ELEMENT_COUNT < ifnot { Drop break } Dup coll Insert vecCopy Insert } // Now select TEST_COUNT random elements form vecCopy and insert them into testVec Var(testVec) [] -> testVec do Index TEST_COUNT < ifnot break 0 ELEMENT_COUNT 1 - GetManager(TEST) Call(GenerateInt) vecCopy GetElement testVec Insert loop Var(t1,t2) CreateTransObject(CX_TIME) -> t1 // Now perform TEST_COUNT existence checks with the randomly selected elements from testVec testVec iterate { coll Contains Drop } CreateTransObject(CX_TIME) -> t2 "s" t2 t1 - Call(Convert) Attention(,INFO)

Im Gegensatz dazu kopiert der folgende Code zunächst die gesamte Liste in ein neues SET und führt dann die Existenzprüfung auf der Kopie durch.

Contains mit SET-Kopie
Var(ELEMENT_COUNT) 10000 -> ELEMENT_COUNT Var(TEST_COUNT) 10000 -> TEST_COUNT Var(coll, vecCopy) [] -> vecCopy CreateTransCollection(LIST) -> coll // Create our collection to test and a vector copy FindAll(CX_PERSON) iterate { Index ELEMENT_COUNT < ifnot { Drop break } Dup coll Insert vecCopy Insert } // Now select TEST_COUNT random elements form vecCopy and insert them into testVec Var(testVec) [] -> testVec do Index TEST_COUNT < ifnot break 0 ELEMENT_COUNT 1 - GetManager(TEST) Call(GenerateInt) vecCopy GetElement testVec Insert loop Var(t1,t2) CreateTransObject(CX_TIME) -> t1 // Copy the list into a SET first Var(set) CreateTransCollection(SET) -> set set coll += // Now perform TEST_COUNT existence checks with the randomly selected elements from testVec testVec iterate { set Contains Drop } CreateTransObject(CX_TIME) -> t2 "s" t2 t1 - Call(Convert) Attention(,INFO)

 

Mit 10.000 Elementen in der Collection und variabler Anzahl an Existenzchecks:
perform3b
und mit 100.000 Elementen in der Collection:

perform3c

Die Anzahl der Tests muss so groß sein, dass sich der Mehraufwand für das Kopieren der Collection lohnt. Der positive Effekt des Sets macht sich erst bemerkbar, wenn die Anzahl der Tests mindestens 10% der Anzahl an Elementen in der Collection entspricht. Im nachfolgenden Graph ist die Dauer der Kopie gegen die Anzahl der Elemente in der Collection abgetragen und man sieht, dass die Dauer für die Set-Kopie mindestens linear wächst und unser Test mit 100.000 Elementen über 90% der Zeit nur mit der Set-Kopie beschäftigt war.

Schussfolgerung:

Der Ansatz, eine Liste in ein Set zu kopieren, um schneller auf die Existenz prüfen zu können lohnt sich also nur in Sonderfällen mit extrem vielen Abfragen.

4. Laderoutinen

Der Name InstantView® verweist auf Visualisierung von Daten, nicht unbedingt auf Batch-Läufe.
Wenn Laderoutinen mit InstantView® geschrieben werden, können die Punkte 1. und 3. ganz in besonderem Maße relevant sein.
Häufig ausgeführte Queries sollten durch Datenbank-Indexe beschleunigt werden.  

Laderoutinen können durch folgende Maßnahmen zusätzlich beschleunigt werden:

CreatePersObject beschleunigen (DeadlockPrevention)

Um Deadlocks zwischen zwei Clients auszuschließen, die den gleichen Code durchlaufen, stellen CreatePersObject und TriggeredStateMonitor den Lock-Modus der Datenbank auf (WRITE,PAGE) um, bevor sie lesend auf die Root-Entry-Point-Collection (CreatePersObject) oder den Start-Zustand (TriggeredStateMonitor) zugreifen und nach Abschluss der Operation wird der Lock-Modus wieder zurückgesetzt. Dieser Wechsel kann verhindert werden, indem der DeadlockPrevention-Mechanismus für den Ladelauf vollständig deaktiviert wird, oder indem ein BeginLock(WRITE) zu Beginn des Ladelaufs einmalig ausgeführt wird. In beiden Fällen fällt der Wechsel des Locking-Modus dann weg, wodurch CreatePersObject deutlich schneller durchlaufen wird.

Nachfolgend der Test-Code und die Messergebnisse für 100.000x CreatePersObject (jede Messung wurde in einer leeren Datenbank durchgeführt).

CreatePersObject DeadlockPrevention-Testcode
//FALSE GetManager(OBJECT) Call(EnableDeadlockPrevention) //BeginLock(WRITE) Var(OBJECT_COUNT) 100000 -> OBJECT_COUNT Var(t1,t2) CreateTransObject(CX_TIME) -> t1 do { Index OBJECT_COUNT < ifnot break CreatePersObject(CX_PERSON) Drop } loop EndTXN CreateTransObject(CX_TIME) -> t2 "s" t2 t1 - Call(Convert) Attention(,INFO)

 

perform5

Wie man erkennt, sorgt der DeadlockPrevention-Mechanismus dafür, dass CreatePersObject 7x langsamer läuft und bei Ladeläufen kann es einen messbaren Unterschied geben, wenn das Write-Lock nur einmalig oder gar nicht gesetzt wird. Der Zeitunterschied zwischen dem vollständigen Abschalten des Mechanismus und dem einmaligen Write-Lock vor dem Ladelauf ist marginal und der Ansatz mit BeginLock(WRITE) sollte grundsätzlich bevorzugt werden, da dieser den Lock-Modus nach dem Ladelauf wieder zurücksetzt und den Deadlockschutz nicht deaktiviert. 

Im Normalbetrieb macht es aber aus Performance-Gründen keinen Sinn, vor jedem CreatePersObject ein BeginLock(WRITE) zu setzen, denn die Ausführung eines einzelnen CreatePersObject liegt trotz der Locking-Bremse noch im µs-Bereich und der potenzielle Performance-Gewinn wäre für einen Nutzer nicht messbar.

Dieser Unterschied macht sich nur bei Ladeläufen bemerkbar, die massiv persistente Objekte anlegen. Der Test ist auch kein repräsentatives Beispiel für einen Ladelauf, da das erzeugte Objekt hier nicht mit Daten befüllt wird. In einem echten Anwendungsbeispiel lag der gemessene Zeitunterschied über den gesamten Ladelauf bei 25%.

 

Transiente Collections abmelden

Werden mit CreateTransObject sehr viele Objekte erzeugt, die über einen längeren Zeitraum existieren sollen (und deshalb z.B. in einem Vektor gehalten werden), kann die Garbage-Collection des ClassiX®-System extrem langsam werden, da sie in regelmäßigen Abständen alle Variablen aller Module durchgeht und prüft, welche Objekte über diese Variablen erreichbar sind, um die unerreichbaren Objekte zu löschen. Dazu müssen auch Vektoren und transiente Collections durchsucht werden.

Es ist dann sinnvoll, die Objekte mit CreateTransObject(..., KEEP) zu erzeugen, damit die Objekte nicht von der GarbageCollection gelöscht werden können. Falls die Objekte in einem Vektor/Collection gehalten werden, dann kann dieser Vektor/Collection per UnprotectContents für die GarbageCollection so markiert werden, dass die GarbageCollection den Inhalt des Vektors/Collection nicht prüft und die Anzahl der Elemente damit keinen Einfluss auf die Performance der GarbageCollection hat. 

Um die temporären Objekte wieder sauber abzuräumen, sollte das Flag im Vektor/Collection nach dem Lauf per ProtectContents wieder entfernt werden und die per CreateTransObject(..., KEEP) erstellten Elemente sollten per Register wieder an der GarbageCollection angemeldet werden.

 

Transaktionssplitting

Alle Änderungen am persistenten Speicher müssen für die Dauer einer Transaktion in einem Transaktionslog mitgeschrieben werden, damit die Änderungen am Ende der Transaktion  in die Datenbank geschrieben oder wieder zurückgerollt werden können. Bei größeren Ladeläufen kann es vorkommen, dass dieses Transaktionslog stark anwächst und die Verwaltung der Änderungen eine messbare Zeit in Anspruch nimmt. Falls dieser Verdacht auftritt, dann kann versucht werden, den Lauf auf mehrere kleine Transaktionen zu verteilen, damit das Log nicht zu groß anwächst. 

Kleine Transaktionen sind insbesondere dann wichtig, wenn viele Änderungen im persistenten Bereich im Live-Betrieb durchgeführt werden, denn dann sind alle Pages, die verändert wurden für die Dauer der Transaktion für alle anderen Clients gesperrt und diese müssen auf das Ende der Transaktion warten.

Bei reinen Ladeläufen sollte Transaktionssplitting jedoch mit Bedacht eingesetzt werden und nur dann, wenn die lange Transaktion als mögliches Performance-Problem identifiziert wurde. Die nachfolgenden Messungen zeigen, dass ein Transaktionssplitting in vielen Fällen keinen Effekt zeigt oder die Performance sogar erheblich verschlechtert, deshalb kann hier keine Empfehlung für eine spezifische Anzahl an Objekten pro Transaktion gegeben werden.

 

Für den Test wurde die Dauer für das Erzeugen und Befüllen von 1.000.000 persistenten Objekten in einer leeren Datenbank abhängig von der gewählten Anzahl an Objekten pro Transaktion mit folgendem Code gemessen.

FALSE GetManager(OBJECT) Call(EnableDeadlockPrevention) Var(OBJECT_COUNT) 1000000 -> OBJECT_COUNT Var(OBJECTS_PER_TXN) 100000 -> OBJECTS_PER_TXN Var(t1,t2) CreateTransObject(CX_TIME) -> t1 do { Index OBJECT_COUNT < ifnot break Index OBJECTS_PER_TXN Mod ifnot { EndTXN BeginTXN } // restart txn LocalVar(firstName, name, person) CreatePersObject(CX_PERSON) -> person 3 10 GetManager(TEST) Call(GenerateName) -> firstName 3 15 GetManager(TEST) Call(GenerateName) -> name firstName person Put(firstName) name person Put(name) // combine name and index into uniqueId name Index + person Put(uniqueID) } loop EndTXN CreateTransObject(CX_TIME) -> t2 "s" t2 t1 - Call(Convert) Attention(,INFO)

 

Im Ergebnis ist die Anzahl der Objekte pro Transaktion gegen die Gesamtdauer des Laufs abgetragen und man sieht, dass es keinen signifikanten Unterschied zwischen einer Transaktion und 10 Transaktionen gibt. Ab 20 Transaktionen wird der Lauf mit der Anzahl der Transaktionen immer langsamer, da jede Transaktion auch einen gewissen Overhead hat.

perform3c

 

Effizientes Einlesen von Excel-Dateien

Excel-Dateien (.xlsx) stellen ein häufig verwendetes (wenn auch dafür nicht gut geeignetes) Datenaustauschformat dar, da die meisten Programme eine Excel-Schnittstelle zum Import/Export anbieten. Die exportierten Excel-Dateien können einige hundert Megabyte groß sein und weil Excel ein gepacktes Format ist, können die entpackten Datenmengen noch viel größer werden. Im Lesemodus werden die enthaltenen .xml-Dateien von der CX_EXCEL_XML-Klasse ins TEMP-Verzeichnis entpackt und von dort aus mit einem SAX-Parser eingelesen. Der Vorteil dieser Methode liegt darin, dass damit theoretisch beliebig große Dateien eingelesen werden können, da diese nicht in den Speicher geladen werden müsen. 

Hinweis: Praktisch ist die Größe durch die aktuell verwendete ZIP-Bibliothek auf .xlsx-Dateien beschränkt, deren entpackter Inhalt < 2GB groß ist.

 

Intern hat unser Excel-Objekt 5 SAX-Scanner pro Excel-Arbeitsblatt, die sich ausschließlich vorwärts bewegen können. Wird eine Zelle per GetValue angefragt, dann wird der Scanner, der am nächsten an dieser Zelle steht bis zu der gewünschten Position weitergeschaltet. Falls alle Scanner hinter der angefragten Zelle stehen, dann wird der am weitesten stehende Scanner wieder an den Anfang des Arbeitsblatts gesetzt. Diese vorgehensweise sorgt dafür, dass man große Arbeitsblätter sehr effizient einlesen kann, solange die Daten von oben nach unten eingelesen werden und innerhalb der Spalten nicht übermäßig viel gesprungen wird.

Das folgende Beispiel zeigt den Performance-Unterschied zwischen korrekter Leserichtung, verkehrter Leserichtung und korrekter Leserichtung mit Sprüngen innerhalb der Zellen. Die getestete .xlsx-Datei besteht aus einem Arbeitsblatt mit 2000 Zeilen x 6 Spalten die aufsteigend mit Zahlen befüllt sind.

Leseperformance Test-Code
Var(excel) CreateTransObject(CX_EXCEL_XML) -> excel "CX_ROOTDIR\\testdata.xlsx" excel Call(LoadFromFile) Var(rows, columns) 2000 -> rows [ 1 2 3 4 5 6 ] -> columns // forward column order //[ 1 6 2 5 3 4 ] -> columns // jumping column order //[ 6 5 4 3 2 1 ] -> columns // reverse column order Var(row, column) 0 -> row //rows -> row // for descending row order Var(t1,t2) CreateTransObject(CX_TIME) -> t1 do { Incr(row) columns iterate(BACKWARD) { row 1 excel Call(GetValue) Drop } row rows > ! //Decr(row) // for descending row order //row 0 > } while CreateTransObject(CX_TIME) -> t2 "s" t2 t1 - Call(Convert) Attention(,INFO)

 

Nachfolgend die Ergebnisse für das Einlesen der 14.000 Zellen:

perform6

Dabei steht Z+ für aufsteigend eingelesene Zeilen (Z- für absteigend), S+ für aufsteigend eingelesene Spalten, Sx für sprunghaft eingelesene Spalten und S- für absteigend eingelesene Spalten.

Wie man an den Messwerten erkennen kann, spielt die Reihenfolge, in der Daten aus eine Excel-Datei eingelesen werden selbst bei einer recht kleinen Datei mit 2000 x 7 Zellen eine erhebliche Rolle. Dadurch dass die Excel-Klasse 5 SAX-Scanner verwendet, kann während des Einlesens in den Spalten in gewissem Umfang ohne Performance-Einbußen gesprungen werden. Wird die Reihenfolge jedoch missachtet, dann muss die Datei immer wieder vom Anfang an durchgelesen werden, was hier zu einem Geschwindigkeitsunterschied von Faktor 500 führt. Der Faktor wird natürlich größer, je größer die einzulesende Datei ist.

 

Indexmaintenance mit Funktion UniqueIDIndexMaint

Die Indexnachführung für Indizies über die Felder (uniqueID und transaction) ist beim Laden von Objekten der Klasse CX_TRANSACTION bzw. davon abgeleiteter Klassen sehr zeitaufwendig. Deswegen sollte, wenn möglich für die Dauer des Ladevorgangs entsprechende Indizies deaktiviert und nach dem Ladevorgang wieder aktiviert werden.

 

5. Objekte mit weiteren, errechneten Daten in einer ObjectListView darstellen

Zusammenfassung:

  1. Mehrere Spalten benötigen die selben Vorbereitungen, um ihre Daten darzustellen.
  2. Ein Makro führt die Vorbereitungen durch. Diese Zwischenergebnisse werden in Variablen abgelegt. Natürlich können auch mehrere Makros verschiedene Vorbereitungen durchführen, wenn z.B. Spalten 1 und 2 Vorbereitung A benötigen und Spalten 3 und 4 Vorbereitung B.
  3. Der Rückgabewert von (2) ist das Objekt, für das die Vorbereitungen durchgeführt wurden (mit anderen Worten: Der Stack sieht am Ende des Makros so aus wie zu Beginn). Auf diese Weise kann der Zugriffsausdruck der Spalte fortgeführt werden.
  4. Die Makros, die das Endergebnis für die Spalte liefern, greifen auf die Zwischenergebnisse aus (2) zurück.
  5. Das Flag OPTIMIZE muss bei der ObjectListView gesetzt sein, damit die Vorbereitungen nicht mehrfach durchgeführt werden und die Performance gedrückt wird.

Beschreibung:

Neben den einzelnen Feldern eines Objektes können in einer ListView auch weitere Daten dargestellt werden, die für jedes Objekt einzeln errechnet werden, z.T. mit Hilfe anderer Objekte. Möglich macht das der Zugriffsausdruck call(). Angenommen, zwei Spalten beziehen sich auf das selbe Zwischenergebnis:

Var(pCustomer) Define(Prepare) Dup Call(Customer) -> pCustomer ; Define(CustomerName) pCustomer Copy(name) ; Define(CustomerID) pCustomer Copy(uniqueID) ; [ "call(Prepare).call(CustomerName)" ] SetFormat [ "call(Prepare).call(CustomerID)" ] SetFormat

Beide Customer-Makros verlassen sich darauf, dass die Variable pCustomer korrekt gesetzt ist. Es ist gefährlich, call(Prepare) bei der zweiten Spalte wegzulassen, schließlich wird die zweite Spalte erst nach der ersten Spalte gezeichnet. Dem ist jedoch nicht so beim Sortieren! Wenn die zweite Spalte sortiert wird, wird die erste Spalte nicht angefasst, und damit stimmen sämtliche Variablen nicht, in denen Zwischenergebnisse gespeichert sind!

Das Makro Prepare liefert das Objekt, für das es aufgerufen wurde, wieder zurück, so dass der Zugriffsausdruck fortgeführt werden kann.

Noch ist diese Technik sehr ineffizient, da für beide Spalten pCustomer neu ermittelt wird. Im Profiler würde sich folgendes Bild ergeben:
call(Prepare)
call(CustomerName)
call(Prepare)
call(CustomerID)

Durch das Flag OPTIMIZE der ObjectListView jedoch wird der Teilausdruck call(Prepare) für die gesamte Zeile nur ein einziges Mal ausgeführt:
call(Prepare)
call(CustomerName)
call(CustomerID)

Beim Sortieren wird call(Prepare) für jede Zeile und Spalte einzeln aufgerufen, hier bewirkt das Flag OPTIMIZE nichts.

 

6. Weitere Performance-Pitfalls

Datenbankmodus nicht auf Anwendungsfall abgestimmt

Im regulären Betrieb von ClassiX kann der falsche Datenbankmodus (OpenDBSetDBMode & BeginTXN) eine schlechte Performance zur Folge haben. Die Wahl eines Datenbankmodus ist immer ein Trade-Off zwischen verschiedenen Aspekten und es kann keine allgemeingültige Empfehlung für alle Anwendungsfälle vorgegeben werden. Hier ist eine Aufstellung der zugrunde liegenden Überlegungen mit einer Empfehlung für einige Anwendungsfälle. Die falsche Datenbankmoduswahl kann unter anderem zu folgenden Problemen führen:

  • Der Client-Cache wird durch häufigen Wechsel des Datenbankmodus ständig verworfen, was die Lese-/Schreibperformance stark beeinträchtigt.
  • Ein Client, der im "KEEP_UPDATE"-Modus nur lesende Operationen durchführt, wird von anderen, schreibenden Clients blockiert, oder blockiert diese und muss das Ende der Transaktion abwarten. Dies kann auch zu einem Deadlock führen, wodurch eine der beiden Transaktionen abgebrochen werden muss.

Locking-Modus nicht auf Anwendungsfall abgestimmt

Auch beim Locking-Modus gilt, dass die falsche Anwendung ein Performance-Problem darstellen kann. Write-Locks (BeginLock & SetDefaultLockMode) werden hauptsächlich verwendet, um Deadlocks zu verhindern, die einen Transaktionsabbruch zur Folge hätten. Die Locks sorgen dafür, dass zwei Clients, die auf das gleiche Objekt zunächst lesend und anschließend schreibend zugrifen, keine Deadlocks, weil der erste Lesezugriff dafür sorgt, dass der zweite Client das Transaktionsende abwarten muss, ehe das Objekt vom zweiten Client gelesen werden kann. Diese Wartezeiten können sich im Multi-User-Betrieb schnell als Performanceproblem bemerkbar machen. Die Wartezeiten fallen insbesondere dann ins Gewicht, wenn:

  • ein sehr pessimistischer Default-Locking-Modus für alle Clients eingestellt ist (Bsp: "WRITE" "DATABASE")
  • es viele lang laufende Transaktionen gibt

Mehrfaches Unlink auf COLL- oder REL-Slot

Für die Performance von Reorganisationsläufen kann es hilfreich sein zu wissen, dass Unlink beim Entfernen von Objekten aus Slots vom Typ COLL oder REL_M1 oder REL_MN die zugrunde liegende Collection (eine Liste) von hinten nach vorne durchsucht. Für den Fall, dass ein Objekt eine riesige Collection mit mehreren hunderttausend Elementen hat und dort viele tausend Elemente entfernt werden sollen, dann sollten diese Elemente möglichst in der umgekehrten Reihenfolge entfernt werden, mit der sie hinzugefügt wurden, sodass Unlink in jedem Schritt möglichst wenig Elemente prüfen muss.