In Mendix Quelle est la longueur d'une chaîne ? | Mendix

Passer au contenu principal

In Mendix Quelle est la longueur d'une chaîne ?

In Mendix Quelle est la longueur d'une chaîne ?

Il s'agit du deuxième article de ma série de blogs sur l'efficacité dans Mendix applications. Dans la première de la série (Santé et efficacité en Mendix), j'ai mis en évidence quelques-unes des méthodes simples qui pourraient vous permettre d'améliorer l'efficacité de votre low-code. Je vais maintenant essayer de m'attaquer à quelque chose de plus difficile.

À deux reprises au cours des cinq dernières années, j’ai dû faire face à des exigences qui demandaient essentiellement de parcourir toutes ces données et de créer un fichier texte à partir de celles-ci.

La première consistait à produire un fichier texte séparé par des tabulations représentant un ensemble de données dans le Mendix application. La seconde nécessitait la création d'un fichier Typescript à partir d'un ensemble de données qui avait été créé dans l'application. Ces deux nécessaire pour prendre en charge la création de fichiers texte très longs car les ensembles de données pourraient être assez volumineux.

Il existe aujourd'hui d'excellents modules sur la Marketplace (par exemple le CSV Module) qui peut aider à la production de fichiers CSV/TSV, mais une sortie de texte plus arbitraire nécessite une solution alternative.

Exercices de test

Pour recueillir des statistiques à des fins de comparaison, j'utilise une application créée en Mendix 9.15.1, déployé sur un Taille moyenne environnement (max 2 CPU, max 2 Go de mémoire, base de données Postgres) courir dans un AWS EKS Privé Mendix Cloud qui comprend Grafana surveillance.

Chaque exercice est courir cinq foisAvant chaque série de cinq exercices,l'application est arrêtée et démarrée pour minimiser le possible la mise en cache influences sur les résultats. les meilleurs et les pires résultats sont rejetés et la les trois autres sont en moyenneLes exercices ne sont pas nécessairement exécutés dans l’ordre présenté ici.

L'application utilisée est disponible sur GitHub ici.

Le point de départ

Nous avons une liste de Mendix objets et nous avons un microflux qui va générer le texte que nous voulons à partir d'un de ces objets. Pour simplifier les choses, je vais simplement travailler avec une seule entité de données, même si dans un scénario réel, de grands arbres d'objets pourraient être impliqués. OutputDocument est la spécialisation FileDocument qui reçoit les résultats du processus de génération de chaîne.

L'entité commerciale et le document de sortie dans le modèle de domaine
Le microflux GetEntityToString qui génère le texte pour une instance de BusinessEntity

Pour produire le fichier de sortie, nous extrayons les enregistrements de données par lots de 2,500 XNUMX enregistrements, transmettons la liste des objets et ajoutons le texte généré pour chacun à une chaîne de collection dont la taille augmente. Lorsque la liste est épuisée, la chaîne que nous avons accumulée est écrite dans un OutputDocument. L'OutputDocument créé enregistre également l'algorithme utilisé, le nombre d'enregistrements traités, le temps nécessaire à l'exécution du test et le hachage du fichier généré. Le hachage est généré et enregistré afin que nous puissions confirmer que toutes les méthodes utilisées pour générer une sortie identique proviennent des mêmes données sources.

Microflux TheStartingPointBuildString

Alors, exécutons ceci.

J'amorce la base de données avec Dossiers 25,000 avec des chaînes de texte « aléatoires » de 500 caractères chacun et des valeurs entières aléatoires, puis exécutez le microflux ci-dessus.

Cela nous donne une moyenne de 78.81 secondes pour créer la chaîne et l'enregistrer dans le FileDocument. Maintenant, doubler les données taille à 50,000 les enregistrements et réexécutez le microflow. Je suppose que nous pouvons nous attendre à ce que cela prenne moins de 200 secondes. Nous devons au moins supposer que maintenant les récupérations par lots seront plus lentes car le volume d'enregistrements a augmenté bien que nous ayons un index sur la clé.

Oh wow ! C'était donc ça une moyenne de 334.05 secondesJe n'essaierai pas un très grand ensemble de données à moins d'avoir un ou deux films à regarder pendant qu'il tourne…

Alors pourquoi un doublement du volume des données devrait-il entraîner une multiplication par quatre du temps écoulé ?

Bien sûr, nous ne pouvons pas en être complètement certains sans utiliser d’outils de profilage, mais nous pouvons faire une supposition intelligente quant au principal coupable.

Extrait de TheStartingPointBuildString

L'action Change Variable ajoute la chaîne renvoyée par le sous-microflux GetEntityToString aux résultats précédents déjà stockés dans la variable Output. Sauf qu'elle ne fait pas exactement cela. In Mendix une chaîne est immuable donc pour créer la nouvelle valeur pour la variable de sortie, Mendix doit créer une nouvelle chaîne consistant une copie de l'original Sortie ajoutée avec une copie de EntityOutput et enregistrez cette nouvelle chaîne à la place de la valeur de sortie précédente, qui est ignorée.

Au fur et à mesure que la valeur de sortie s'allonge, la quantité de texte à copier augmente, et le processus devient de plus en plus gourmand en temps et en ressources.

Alors essayons un peu de réingénierie en utilisant du code Java pour voir si nous pouvons éviter la copie répétitive de longues chaînes et réduire le temps d'exécution.

Mémoire tampon

Au lieu de construire la longue chaîne dans un Mendix chaîne variable, ici nous allons construire la chaîne dans un tampon qui est stocké dans le contexte de l'action utilisateur en cours, puis à la fin stocker ce tampon dans le FileDocument. Le contexte n'est accessible qu'à partir d'une action Java, nous devons donc le construire en Java.

Le microflow que nous utilisons est très similaire à l'original, mais il appelle une action Java pour ajouter la chaîne suivante sur le tampon stocké dans la mémoire de contexte, et une autre action Java pour déplacer celle-ci terminée dans le FileDocument à la fin.

Microflux MemoryBufferBuildString

Les deux actions Java ressemblent à ceci. Nous utilisons un ByteArrayOutputStream pour stocker les données que nous convertissons ensuite en ByteArrayInputStream pour déplacer le résultat dans le FileDocument.

Action Java AppendStringToMemoryBuffer
Action Java MoveMemoryBufferToDocument

Alors, quand on exécute ceci, qu'obtenons-nous ?

bien pour Dossiers 25,000 nous obtenons une moyenne de 1.90 secondes.

Et pour Dossiers 50,000 nous obtenons une moyenne de 2.94 secondes.

Je pense que vous conviendrez qu'il s'agit d'une amélioration remarquable par rapport à l'algorithme original (334 secondes contre 3 secondes). Cela semble indiquer que nous sommes sur la bonne voie.

Mais pouvons-nous encore améliorer cela ? Bien que la vitesse soit considérablement améliorée, nous stockons beaucoup de texte dans un tampon qui est la mémoire de l'application, et cela pourrait finir par mettre la pression sur l'application. Mendix mémoire d'exécution.

Tampon de fichier

Une autre approche pourrait atténuer le problème potentiel d'utilisation de la mémoire et toujours fonctionner mieux que la méthode d'origine. Cette version du processus écrit le texte généré dans un fichier temporaire afin que nous n'ayons pas à le conserver dans le Mendix mémoire d'exécution.

Cette option est plus complexe et nécessite l'utilisation de deux microflux et de deux actions Java. Le premier microflux appelle la première action Java qui appelle le deuxième microflux qui appelle la deuxième action Java. Les raisons de cette situation seront expliquées plus loin.

Pour démarrer, le microflux FileBufferBuildString configure les éléments, puis appelle l'action Java BuildStringInFileBuffer et enfin enregistre les résultats supplémentaires (hachage, nombre d'enregistrements et heure) dans le document.

Microflux FileBufferBuildString

L'action Java BuildStringDocumentInFileBuffer prend le FileDocument et un pointeur de microflow (un argument facultatif pour ce microflow), crée le fichier temporaire dans l'emplacement du fichier temporaire Java, stocke les détails du fichier ouvert dans la mémoire de contexte, puis appelle le microflow donné dans le pointeur. Lorsque ce microflow revient, il lit le contenu du fichier temporaire dans le FileDocument et nettoie, supprimant le fichier et les objets de contexte.

Action Java BuildStringDocumentInFileBuffer

Le microflux SUB_FileBufferBuildString est appelé par l'action Java BuildStringDocumentInFileBuffer. Il exécute la boucle qui lit les lots d'enregistrements, génère les chaînes pour chaque enregistrement, puis appelle l'action Java AppendStringToFileBuffer pour les enregistrer.

Microflux SUB_FileBufferBuildString

L'action Java AppendStringtoFileBuffer extrait les informations du fichier temporaire du contexte (qui y ont été enregistrées par BuildStringDocumentInFileBuffer) et écrit la chaîne d'un seul enregistrement à la fin du fichier temporaire.

capture d'écran du code
Action Java AppendStringToFileBuffer

OK, donc cet arrangement (microflow-appels-java-appels-microflow-appels-java) est un peu alambiqué et pourrait être considéré comme obscur et allant à l'encontre de ce que j'ai dit dans mon article de blog précédent (lisibilité versus maintenabilité), il devrait donc être bien documenté pour les développeurs ultérieurs.

Cette approche a une bonne raison : elle est sûre. Si quelque chose devait se passer mal au cours du processus, la première action Java peut nettoyer le fichier temporaire et le descripteur de fichier ouvert avant de revenir au premier microflux, ce qui réduit les risques que l'application dans son ensemble soit compromise. Il existe également une alternative dans l'application accompagnant ce blog (appelée TempStorage) — voir Quelle est la longueur d'une chaîne sur GitHub- qui n'utilise pas l'imbrication de microflux et d'actions Java mais qui exige que l'appelant soit beaucoup plus prudent lors du nettoyage en cas de problème.

Alors, quels sont les résultats ? Pour Dossiers 25,000 ça a pris 1.23 secondes, et pour Dossiers 50,000 ça a pris 2.75 secondes.

Mémoire tampon et Tampon de fichier sont très proches en termes de performances, mais nous pouvons voir maintenant que l'original Le point de départ L'algorithme est de loin le moins performant des trois, et pour les besoins de cet exercice, nous n'avons plus besoin de l'utiliser. Pouvons-nous choisir entre les deux autres ?

Les éliminatoires

J'ai mentionné l'utilisation de la mémoire comme étant un autre facteur lors de la comparaison de ces solutions, je devrais donc obtenir des statistiques à ce sujet également. Pour plus de clarté, utilisons un échantillon plus large de données de test — Dossiers 500,000.

La population a été mise en place, puis l'application a été redémarrée et la Mémoire tampon le processus a été exécuté une fois. Avec Grafana mis en place sur ce Cloud Privé, nous pouvons obtenir quelques informations sur l'utilisation des ressources :

Aie! Mémoire tampon n'est pas assez fort pour gérer cette taille de tâche et l'application a manqué de mémoire lors du remplissage du tampon dans AppendStringToMemoryBuffer. Tampon de fichier faire mieux ? L'application a été redémarrée et Tampon de fichier Était dirigé:

Attends ! Alors Tampon de fichier également échoué mais cela est mort avec Out of Memory après le FileDocument a été créé et renseigné avec succès et pendant le processus, il lit l'intégralité du fichier généré dans une chaîne (CommunityCommons StringFromFile) pour calculer la valeur de hachage des données. Le fichier est trop volumineux pour faire cela. J'ai donc désactivé le code qui lit la chaîne et appelle l'action Hash (à l'aide d'une constante), j'ai relancé le processus et cela a fonctionné.

Alors oui, c'est terminé et il semblerait que nous ayons un gagnant !

Pour faire bonne mesure, j'ai mis en place un Enregistrement 1,000,000 ensemble de données et exécuté Tampon de fichier encore une fois et cela aussi terminé.

Votre kilométrage peut varier

La manière dont tout cela fonctionne dans une situation réelle dépend bien sûr de nombreux facteurs qui ne relèvent pas du cadre de cet article, mais j'espère que cela a été utile pour montrer comment, dans des circonstances exceptionnelles, vous pouvez améliorer la résilience et les performances de la génération de chaînes, et en effet comment Java, en général, peut être utilisé pour améliorer les performances.

Dans mon prochain article sur l'efficacité, je prévois de montrer comment certaines opérations longues peuvent être améliorées en utilisant le Mendix Fonctionnalité de file d'attente des tâches.

Remerciements

Mes remerciements à Arjen Wisse pour ses précieux conseils et à Arjen Lammers pour le module CSV intéressant mentionné en haut de cet article, duquel j'ai emprunté sans vergogne des idées.

Choisissez votre langue