In Mendix 文字列の長さはどれくらいですか?
これは、効率性に関するブログシリーズの第2弾です。 Mendix アプリ。シリーズの最初の記事(健康と効率 Mendix) では、ローコードの効率を向上させる簡単な方法をいくつか紹介しました。次は、もっと難しいことに取り組んでみたいと思います。
過去 5 年間に 2 回、基本的に「このすべてのデータを調べて、そこからテキスト ファイルを作成する」という要件に対処する必要がありました。
1つ目は、タブ区切りのテキストファイルを作成し、 Mendix アプリ。2つ目は、アプリで構築されたデータセットからTypescriptファイルを作成する必要がありました。これら2つは 非常に長いテキストファイルの作成をサポートするために必要 データ セットがかなり大きくなる可能性があるためです。
今ではマーケットプレイスに素晴らしいモジュールがあります(例えば CSV CSV/TSV ファイルの生成に役立つモジュールがありますが、より任意のテキスト出力には別のソリューションが必要です。

テスト演習
比較のための統計を収集するために、私はで作成されたアプリを使用しています Mendix 9.15.1、配備された ミディアムサイズ 環境 (最大 2 CPU、最大 2 GB メモリ、Postgres データベース)が走る AWS EKSプライベート Mendix クラウド これは含まれて グラファナ モニタリング。
各演習は 5回走る5つのエクササイズの各セットを実行する前にアプリが停止して起動する 可能性を最小限に抑える キャッシング 結果に影響を与えます。 最良と最悪な結果は破棄される 他の3つは平均化される演習は必ずしもここで示した順序で実行されるわけではありません。
使用したアプリはGitHubで入手可能です こちら.
出発点
のリストがあります Mendix オブジェクトがあり、それらのオブジェクトの 1 つから必要なテキストを生成するマイクロフローがあります。簡単にするために、単一のデータ エンティティのみを扱いますが、実際のシナリオでは、オブジェクトの大きなツリーが関係する可能性があります。 OutputDocumentは、文字列生成プロセスの結果を受け取るFileDocument特殊化です。.

BusinessEntity のインスタンスのテキストを生成する GetEntityToString マイクロフロー出力ファイルを生成するには、データ レコードを 2,500 レコードのバッチで取り出し、オブジェクトのリストを渡し、それぞれに対して生成されたテキストを、サイズが大きくなるコレクション文字列に追加します。リストが使い果たされると、蓄積された文字列が OutputDocument に書き込まれます。作成された OutputDocument には、使用されたアルゴリズム、処理されたレコードの数、テストの実行にかかった時間、生成されたファイルのハッシュも記録されます。ハッシュが生成されて保存されるので、同一の出力を生成するために使用されたすべてのメソッドが同じソース データからのものであることを確認できます。

ではこれを実行してみましょう。
私はデータベースを準備します 25,000レコード 'ランダム'なテキスト文字列で 各500文字 ランダムな整数値を作成して、上記のマイクロフローを実行します。

平均すると 78.81 文字列を構築してFileDocumentに保存するのに数秒かかります。では、 データを2倍にする サイズ 50,000 レコードを取得してマイクロフローを再実行します。おそらく 200 秒未満で完了すると思われます。キーにインデックスがあるにもかかわらず、レコードの量が増えたため、バッチ取得が遅くなると想定する必要があります。

わあ!そうだったんですね 平均334.05秒実行中に映画を 1 つか 2 つ観る予定がない限り、本当に大きなデータセットを試すつもりはありません…
では、なぜデータ量が 2 倍になると経過時間が 4 倍になるのでしょうか?
まあ、プロファイリング ツールを使用しなければ完全に確信することはできませんが、主な犯人について賢明な推測をすることはできます。

変数の変更アクションは、GetEntityToString サブマイクロフローから返された文字列を、出力変数に既に保存されている以前の結果に追加します。ただし、正確には追加されません。 In Mendix 文字列は不変である 出力変数の新しい値を作成するには、 Mendix 新しい文字列を作成する必要があります からなる オリジナルのコピー EntityOutput のコピーが出力に追加され、破棄される以前の Output 値の代わりにその新しい文字列が保存されます。
出力の値が長くなると、テキストのコピー量が増加しますそして、プロセスにはますます多くの時間とリソースが必要になります。
では、少しリエンジニアリングを試してみましょう Javaコードを使用する 長い文字列の繰り返しコピーを回避し、実行時間を短縮できるかどうかを確認します。
メモリバッファ
長い文字列を Mendix 変数文字列の場合、現在のユーザー アクションのコンテキストに格納されるバッファーに文字列を構築し、最後にそのバッファーを FileDocument に格納します。コンテキストには Java アクションからのみアクセスできるため、これを Java で構築する必要があります。
私たちが使用するマイクロフローはオリジナルと非常に似ていますが、コンテキスト メモリに保存されているバッファーに次の文字列を追加するために 1 つの Java アクションを呼び出し、最後に完了した文字列を FileDocument に移動するために別の Java アクションを呼び出します。

2 つの Java アクションは次のようになります。ByteArrayOutputStream を使用してデータを保存し、それを ByteArrayInputStream に変換して結果を FileDocument に移動します。


では、これを実行すると何が得られるのでしょうか?
よく 25,000レコード 平均すると 1.90 seconds.

そして、のために 50,000レコード 平均すると 2.94 seconds.

これは、元のアルゴリズムに比べて目覚ましい進歩である (334 秒に対して 3 秒) ということに、皆さんも同意していただけると思います。これは、私たちが正しい方向に進んでいることを示しているようです。
しかし、これをさらに改善することはできるでしょうか?速度は大幅に向上しましたが、アプリのメモリであるバッファに大量のテキストを保存しているため、 Mendix ランタイムメモリ。
ファイルバッファ
別のアプローチでは、潜在的なメモリ使用量の問題を軽減し、元の方法よりもパフォーマンスが向上する可能性があります。このバージョンのプロセスでは、生成されたテキストを一時ファイルに書き込むため、 Mendix ランタイムメモリ。
このオプションはより複雑で、2 つのマイクロフローと 2 つの Java アクションを使用する必要があります。最初のマイクロフローは最初の Java アクションを呼び出し、最初の Java アクションは 2 番目のマイクロフローを呼び出し、2 番目の Java アクションは 2 番目の Java アクションを呼び出します。その理由については後で説明します。
FileBufferBuildString マイクロフローを開始するには、設定を行い、BuildStringInFileBuffer Java アクションを呼び出し、最後に補足結果 (ハッシュ、レコード数、および時間) をドキュメントに保存します。

BuildStringDocumentInFileBuffer Java アクションは、FileDocument とマイクロフロー ポインター (そのマイクロフローのオプション引数) を受け取り、Java 一時ファイルの場所に一時ファイルを作成し、開いているファイルの詳細をコンテキスト メモリに保存してから、ポインターで指定されたマイクロフローを呼び出します。そのマイクロフローが戻ると、一時ファイルの内容を FileDocument に読み込み、クリーンアップして、ファイルとコンテキスト オブジェクトを削除します。

SUB_FileBufferBuildString マイクロフローは、BuildStringDocumentInFileBuffer Java アクションによって呼び出されます。これは、レコードのバッチを読み取り、各レコードの文字列を生成し、AppendStringToFileBuffer Java アクションを呼び出してそれらを保存するループを実行します。

AppendStringtoFileBuffer Java アクションは、コンテキスト (BuildStringDocumentInFileBuffer によってそこに保存されたもの) から一時ファイル情報を取得し、単一レコードの文字列を一時ファイルの末尾に書き込みます。

さて、この配置(マイクロフロー呼び出し-Java呼び出し-マイクロフロー呼び出し-Java)は少し複雑でわかりにくく、以前のブログ記事で述べたこと(読みやすさと保守性)に反していると思われるかもしれません。 後続の開発者のために十分に文書化しておく必要がある.
このアプローチには、安全であるという理由が 1 つあります。プロセスで何か問題が発生した場合、最初の Java アクションは、最初のマイクロフローに戻る前に一時ファイルと開いているファイル記述子をクリーンアップできるため、アプリ全体が危険にさらされる可能性が低くなります。このブログに付属するアプリには、代替手段 (TempStorage と呼ばれる) もあります。 GitHub の文字列の長さはどれくらいか - マイクロフローと Java アクションのネストを使用しませんが、問題が発生した場合に備えて、呼び出し側はクリーンアップについてより慎重に行う必要があります。
それで、結果はどうなったでしょうか? 25,000レコード かかった 1.23 秒、そして 50,000レコード かかった 2.75 seconds.


メモリバッファ (NAIST) と ファイルバッファ パフォーマンスは非常に近いですが、元の 出発点 アルゴリズムは 3 つの中でパフォーマンスがはるかに低く、この演習の目的上、これ以上使用する必要はありません。他の 2 つから選択することはできますか?
プレーオフ
これらのソリューションを比較する際のもう 1 つの要素としてメモリ使用量を挙げたので、これについても統計を取る必要があります。わかりやすくするために、より大きなテスト データのサンプルを使用しましょう。 500,000レコード.
人口が設定され、アプリが再起動され、 メモリバッファ プロセスは1回実行されました。 グラファナ このプライベート クラウドを設定すると、リソースの使用状況に関する情報を取得できます。


イテッ! メモリバッファ 十分に強くない このサイズのジョブを処理するには、AppendStringToMemoryBufferでバッファを設定するときにアプリのメモリが不足しました。 ファイルバッファ 改善されましたか?アプリが再起動され、 ファイルバッファ 実行されました:


待って!それで ファイルバッファ これも失敗しましたが、メモリ不足で終了しました After FileDocument は正常に作成され、データが入力され、プロセス中に生成されたファイル全体を文字列 (CommunityCommons StringFromFile) に読み取り、データのハッシュ値を計算します。ファイルが大きすぎるため、これを行うことができません。そのため、文字列を読み取り、ハッシュ アクション (定数を使用) を呼び出すコードを無効にして、プロセスを再実行したところ、うまくいきました。

はい、完成しました。勝者が出たようです!
念のため、 1,000,000レコード データセットと実行 ファイルバッファ もう一度、これも完了しました。

結果は状況によって異なります
もちろん、これが現実の状況でどのように機能するかは、この記事の範囲外にある多くの要因に依存しますが、例外的な状況下で文字列生成の回復力とパフォーマンスを向上させる方法、そして実際に Java を一般にどのように使用してパフォーマンスを向上させることができるかを示す上で、この記事が役に立ったことを願っています。
次回の効率化の投稿では、組み込みの Mendix タスク キュー機能。
謝辞
貴重なアドバイスをくれた Arjen Wisse 氏と、この投稿の冒頭で言及したクールな CSV モジュールを提供してくれた Arjen Lammers 氏に感謝します。私は恥ずかしげもなくこのモジュールからアイデアを拝借しました。