In Mendix wie lang ist ein String?
Dies ist der zweite Teil meiner Blogserie über Effizienz in Mendix Apps. Im ersten Teil der Serie (Gesundheit und Effizienz in Mendix), habe ich einige einfache Möglichkeiten hervorgehoben, mit denen Sie die Effizienz Ihres Low-Codes verbessern können. Jetzt werde ich versuchen, etwas Schwierigeres in Angriff zu nehmen.
Zweimal in den letzten fünf Jahren hatte ich es mit Anforderungen zu tun, die im Wesentlichen besagten: Gehen Sie alle diese Daten durch und erstellen Sie daraus eine Textdatei.
Das erste war die Erstellung einer tabulatorgetrennten Textdatei, die einen Datensatz in der Mendix App. Die zweite erforderte die Erstellung einer Typescript-Datei aus einem Datensatz, der in der App erstellt wurde. Beide erforderlich, um die Erstellung sehr langer Textdateien zu unterstützen da die Datensätze ziemlich groß sein können.
Mittlerweile gibt es tolle Module im Marketplace (zum Beispiel das CSV Modul), das bei der Erstellung von CSV-/TSV-Dateien helfen kann, für die Ausgabe beliebiger Texte jedoch eine alternative Lösung erfordert.

Testübungen
Um Statistiken zum Vergleich zu sammeln, verwende ich eine App, die in Mendix 9.15.1, eingesetzt auf einem Mittlere Größe Umfeld (max. 2 CPUs, max. 2 GB Speicher, Postgres-Datenbank) läuft in einem AWS EKS Privat Mendix Cloud welches beinhaltet Grafana Überwachung.
Jede Übung ist fünfmal laufen. Vor jedem Satz von fünf Übungen wird t ausgeführtDie App wird gestoppt und gestartet zur Minimierung möglicher Caching Einflüsse auf die Ergebnisse. Die beste und schlechteste Ergebnisse werden verworfen und der die anderen drei sind gemittelt. Die Übungen werden nicht unbedingt in der hier angegebenen Reihenfolge durchgeführt.
Die verwendete App ist auf GitHub verfügbar werden auf dieser Seite erläutert.
Der Ausgangspunkt
Wir haben eine Liste mit Mendix Objekte und wir haben einen Mikrofluss, der den gewünschten Text aus einem dieser Objekte generiert. Der Einfachheit halber arbeite ich nur mit einer einzigen Datenentität, obwohl in einem realen Szenario große Objektbäume beteiligt sein könnten. OutputDocument ist die FileDocument-Spezialisierung, die die Ergebnisse des String-Generierungsprozesses empfängt..

Der Mikroflow GetEntityToString, der den Text für eine Instanz von BusinessEntity generiertUm die Ausgabedatei zu erstellen, ziehen wir die Datensätze in Stapeln von 2,500 Datensätzen heraus, übergeben die Liste der Objekte und hängen den für jedes generierten Text an eine Sammlungszeichenfolge an, die immer größer wird. Wenn die Liste erschöpft ist, wird die von uns angesammelte Zeichenfolge in ein Ausgabedokument geschrieben. Das erstellte Ausgabedokument zeichnet auch den verwendeten Algorithmus, die Anzahl der verarbeiteten Datensätze, die für die Ausführung des Tests benötigte Zeit und den Hash der generierten Datei auf. Der Hash wird generiert und gespeichert, damit wir bestätigen können, dass alle zum Generieren identischer Ausgaben verwendeten Methoden aus denselben Quelldaten stammen.

Lassen Sie es uns also ausführen.
Ich bereite die Datenbank vor mit 25,000 records mit 'zufälligen' Textzeichenfolgen von Jeweils 500 Zeichen und zufällige Ganzzahlwerte und führen Sie dann den obigen Mikrofluss aus.

Das ergibt einen Durchschnitt von 78.81 Sekunden, um den String zu erstellen und ihn im FileDocument zu speichern. Jetzt wollen wir Verdoppeln Sie die Daten Größe zu 50,000 Datensätze und führen Sie den Mikrofluss erneut aus. Ich nehme an, wir können davon ausgehen, dass es weniger als 200 Sekunden dauert. Wir sollten zumindest davon ausgehen, dass die Stapelabrufe jetzt langsamer sind, da das Datensatzvolumen zugenommen hat, obwohl wir einen Index für den Schlüssel haben.

Oh wow! Das war also durchschnittlich 334.05 Sekunden. Ich werde es nicht mit wirklich großen Datensätzen versuchen, es sei denn, ich habe ein oder zwei Filme, die ich mir während der Wiedergabe ansehen kann …
Warum also sollte eine Verdoppelung des Datenvolumens zu einer Vervierfachung der verstrichenen Zeit führen?
Ohne den Einsatz von Profiling-Tools können wir nicht völlig sicher sein, aber wir können eine fundierte Vermutung über den Hauptschuldigen anstellen.

Die Aktion „Variable ändern“ hängt die Zeichenfolge, die vom Submikroflow „GetEntityToString“ zurückgegeben wird, an die vorherigen Ergebnisse an, die bereits in der Ausgabevariable gespeichert sind. Allerdings geschieht dies nicht genau. In Mendix Eine Zeichenfolge ist unveränderlich Um also den neuen Wert für die Ausgabevariable zu erstellen, Mendix muss eine neue Zeichenfolge erstellen bestehend aus eine Kopie des Originals An die Ausgabe wird eine Kopie von „EntityOutput“ angehängt. Speichern Sie diese neue Zeichenfolge anstelle des vorherigen Ausgabewerts, der verworfen wird.
Wenn der Wert in Ausgabe länger wird, erhöht sich die Menge des kopierten Textes, und der Prozess wird immer zeit- und ressourcenintensiver.
Versuchen wir also ein bisschen Re-Engineering mithilfe von Java-Code um zu sehen, ob wir das wiederholte Kopieren langer Zeichenfolgen vermeiden und die Ausführungszeit verkürzen können.
Speicherpuffer
Anstatt die lange Zeichenfolge in einem Mendix Variable Zeichenfolge, hier erstellen wir die Zeichenfolge in einem Puffer, der im Kontext der aktuellen Benutzeraktion gespeichert wird, und speichern diesen Puffer dann am Ende im FileDocument. Der Kontext ist nur von einer Java-Aktion aus zugänglich, daher müssen wir dies in Java erstellen.
Der von uns verwendete Mikrofluss ist dem Original sehr ähnlich, ruft jedoch eine Java-Aktion auf, um die nächste Zeichenfolge an den im Kontextspeicher gespeicherten Puffer anzuhängen, und eine weitere Java-Aktion, um das Abgeschlossene am Ende in das FileDocument zu verschieben.

Die beiden Java-Aktionen sehen ungefähr so aus. Wir verwenden einen ByteArrayOutputStream, um die Daten zu speichern, die wir dann in einen ByteArrayInputStream konvertieren, um das Ergebnis in das FileDocument zu verschieben.


Was erhalten wir also, wenn wir dies ausführen?
Gut für 25,000 records Wir erhalten einen Durchschnitt von 1.90 Sekunden.

Und für 50,000 records Wir erhalten einen Durchschnitt von 2.94 Sekunden.

Ich denke, Sie werden mir zustimmen, dass dies eine bemerkenswerte Verbesserung gegenüber dem ursprünglichen Algorithmus darstellt (334 Sekunden gegenüber 3 Sekunden). Dies scheint darauf hinzudeuten, dass wir auf dem richtigen Weg sind.
Aber können wir das noch weiter verbessern? Obwohl die Geschwindigkeit enorm verbessert wird, speichern wir viel Text in einem Puffer, der der Speicher der App ist, und könnten dadurch den Speicher der App belasten. Mendix Laufzeitspeicher.
Dateipuffer
Ein anderer Ansatz könnte das potenzielle Speichernutzungsproblem mildern und trotzdem eine bessere Leistung als die ursprüngliche Methode erzielen. Diese Version des Prozesses schreibt den generierten Text in eine temporäre Datei, sodass wir ihn nicht im Mendix Laufzeitspeicher.
Diese Option ist komplexer und erfordert die Verwendung von zwei Mikroflüssen und zwei Java-Aktionen. Der erste Mikrofluss ruft die erste Java-Aktion auf, die den zweiten Mikrofluss aufruft, der die zweite Java-Aktion aufruft. Die Gründe dafür werden später erläutert.
Um den FileBufferBuildString-Mikroflow zu starten, werden die Dinge eingerichtet, dann wird die Java-Aktion BuildStringInFileBuffer aufgerufen und schließlich werden die zusätzlichen Ergebnisse (Hash, Datensatzanzahl und Zeit) im Dokument gespeichert.

Die Java-Aktion BuildStringDocumentInFileBuffer verwendet das FileDocument und einen Microflow-Zeiger (ein optionales Argument für diesen Microflow), erstellt die temporäre Datei am Java-Speicherort für temporäre Dateien, speichert die Details der geöffneten Datei im Kontextspeicher und ruft dann den im Zeiger angegebenen Microflow auf. Wenn dieser Microflow zurückkehrt, liest er den Inhalt der temporären Datei in das FileDocument und führt eine Bereinigung durch, wobei die Datei und die Kontextobjekte gelöscht werden.

Der Mikroflow SUB_FileBufferBuildString wird von der Java-Aktion BuildStringDocumentInFileBuffer aufgerufen. Er führt die Schleife aus, die Datensätze in Stapeln liest, die Zeichenfolgen für jeden Datensatz generiert und dann die Java-Aktion AppendStringToFileBuffer aufruft, um sie zu speichern.

Die Java-Aktion „AppendStringtoFileBuffer“ zieht die temporären Dateiinformationen aus dem Kontext (die dort von BuildStringDocumentInFileBuffer gespeichert wurden) und schreibt die Zeichenfolge für einen einzelnen Datensatz an das Ende der temporären Datei.

OK, also diese Anordnung (Mikrofluss-Anrufe-Java-Anrufe-Mikrofluss-Anrufe-Java) ist ein wenig verworren und könnte als obskur angesehen werden und dem widersprechen, was ich in meinem früheren Blog-Beitrag gesagt habe (Lesbarkeit versus Wartbarkeit), daher sollte es für spätere Entwickler gut dokumentiert sein.
Es gibt einen guten Grund für diesen Ansatz – er ist sicher. Sollte während des Prozesses etwas schiefgehen, kann die erste Java-Aktion die temporäre Datei und den offenen Dateideskriptor bereinigen, bevor sie zum ersten Mikroflow zurückkehrt, sodass die Wahrscheinlichkeit geringer ist, dass die App als Ganzes kompromittiert wird. Es gibt auch eine Alternative in der App, die diesen Blog begleitet (TempStorage genannt) – siehe Wie lang ist ein String auf GitHub - Hierbei wird zwar nicht die Verschachtelung von Mikroflüssen und Java-Aktionen verwendet, aber der Anrufer muss beim Aufräumen viel sorgfältiger vorgehen, falls etwas schief geht.
Was sind also die Ergebnisse? Für 25,000 records es dauerte 1.23 Sekunden und für 50,000 records es dauerte 2.75 Sekunden.


Speicherpuffer , Dateipuffer sind in der Leistung sehr ähnlich, aber wir können jetzt sehen, dass die ursprüngliche Der Startpunkt Algorithmus ist von den dreien bei weitem der am wenigsten leistungsstarke und wir müssen ihn für diese Übung nicht mehr verwenden. Können wir zwischen den beiden anderen wählen?
Das Play-Off
Ich habe die Speichernutzung als einen weiteren Faktor beim Vergleich dieser Lösungen erwähnt, daher sollte ich auch dazu einige Statistiken erstellen. Um die Übersichtlichkeit zu verbessern, verwenden wir eine größere Stichprobe von Testdaten — 500,000 records.
Die Population wurde eingerichtet und dann wurde die App neu gestartet und die Speicherpuffer Prozess wurde einmal ausgeführt. Mit Grafana Auf dieser privaten Cloud können wir einige Informationen zur Ressourcennutzung abrufen:


Autsch! Speicherpuffer ist nicht stark genug um diese Größe des Auftrags zu verarbeiten und der App ging der Speicher aus, während der Puffer in AppendStringToMemoryBuffer gefüllt wurde. Wird Dateipuffer besser machen? Die App wurde neu gestartet und Dateipuffer wurde ausgeführt:


Warte! So Dateipuffer schlug auch fehl, aber dies endete mit Out of Memory nachdem Das FileDocument wurde erfolgreich erstellt und ausgefüllt und während des Prozesses wird die gesamte generierte Datei in einen String (CommunityCommons StringFromFile) gelesen, um den Hash-Wert der Daten zu berechnen. Die Datei ist dafür zu groß. Also habe ich den Code deaktiviert, der den String liest und die Hash-Aktion aufruft (mit einer Konstanten), den Prozess erneut ausgeführt und es hat funktioniert.

Also ja, es ist geschafft und wir scheinen einen Gewinner zu haben!
Zur Sicherheit habe ich ein 1,000,000-Datensatz Datensatz und lief Dateipuffer nochmal und auch dies ist abgeschlossen.

Ihr Kilometerstand kann variieren
Wie sich dies alles in einer realen Situation auswirkt, hängt natürlich von zahlreichen Faktoren ab, die über den Rahmen dieses Artikels hinausgehen. Ich hoffe jedoch, dass dieser Artikel hilfreich war, um zu zeigen, wie Sie unter außergewöhnlichen Umständen die Belastbarkeit und Leistung der Zeichenfolgengenerierung verbessern können und wie Java im Allgemeinen zur Verbesserung der Leistung eingesetzt werden kann.
In meinem nächsten Beitrag zum Thema Effizienz werde ich zeigen, wie einige langwierige Vorgänge durch die Nutzung der integrierten Mendix Aufgabenwarteschlangenfunktion.
Anerkennungen
Mein Dank geht an Arjen Wisse für seine unschätzbaren Ratschläge und an Arjen Lammers für das coole CSV-Modul, das oben in diesem Beitrag erwähnt wurde und von dem ich schamlos Ideen übernommen habe.