ジェネレーターのパフォーマンスを向上させるためのヒント
MPS ジェネレーターを高速化し、400% によってビルド速度を向上させる
Daniel Stiegler (Modellwerkstatt) と V á clav Pech 著 (JetBrains)
Daniel Stieger は、Java ローコードプラットフォーム MoWare Werkbank を提供するオーストリアの企業、modellwerkstatt.org のパートナーです。このプラットフォームは、ビジネスアプリケーション、つまり特定の企業の特定のビジネスプロセスをサポートするカスタムアプリケーションの効率的な開発に焦点を当てています。ローコードプラットフォームは、緊密に統合された 3 つのドメイン固有言語 (DSL) で構成され、すべて JetBrains MPS 内で開発されています。
導入
この記事では、DSL を適用した最大のプロジェクトである商品管理システム (MMS) について説明します。ただし、表面的な概要を説明するだけではなく、JetBrains MPS の詳細を深く掘り下げていきます。具体的には、MPS コードジェネレーターを使用した経験と、どのようにしてコード生成速度を 3 倍以上にすることができたのかを調査します。13 年前の MMS ストーリーの冒頭から、JetBrains MPS コードジェネレーターを効率的に操作するための実践的なヒントをいくつか紹介します。
当面の課題
13 年前、オーストリア最大の食品小売業者の 1 つである MPREIS に雇われ、同社の 300 近くのブランチ向けの商品管理システム (MMS) を開発しました。このようなシステムの主なタスクは、入荷および発送を含むすべての商品の移動、および償却 (破損、盗難など) を追跡することです。MMS を使用すると、金額と数量の両方を考慮した商品在庫を管理できます。さらに、手動調達 (商品の発注など) などのビジネスプロセスも完全にサポートされなければなりません。このプロジェクトは社内開発であったため、MPREIS は独自の要件に特別な注意を払いたいと考えていました。今後は、ビジネスプロセスの変化に迅速かつ動的に対応できるようにしたいと考えていました。さらに、革新的な店舗管理コンセプトの実験を可能にするシステムも望んでいました。社内開発により、少なくとも今後 30 年間はブランチベースの小売業が強化されるはずです。高度な柔軟性を確保し、プロセスの変更を迅速かつ効率的に行うことができるようにする必要があります。
経営陣は、戦略的に重要な MMS を開発するための新しく革新的なアプローチを求めました。最新の (Java) オープンソーステクノロジへの方向性が望まれていました。さらに、DSL と協力して MMS の開発をより抽象的なレベルにプルアップすることを提案しました。より高いレベルの抽象化により、生産性が向上するだけでなく、開発者の注意がビジネスドメイン自体、関連データ、固有の制約、エンドユーザーにさらに移るはずです。ただし、理想的には、開発者は、そもそも技術的な必要性や基礎となる技術ベースプラットフォームに気を取られるべきではありません。
2010 年にプロジェクトを開始したとき、DSL と必要なコードジェネレーターを実装するための主要なツールとして JetBrains MPS を特定しました。他のツールと比較して、特に射影エディターとそれが提供する可能性、特に複数の DSL を同時に操作することに確信を持っていました。これにより、より具体的なタスク用に複数の小さな DSL を設計できるようになり、さらに DSL を緊密に統合することができます。MPS には、すぐに使える Java 言語の完全な実装が再利用可能な DSL として同梱されています。当時、これはおそらく私たちにとって最も重要なことでした。射影エディターに関する短いビデオ(英語)を見つけることができます
Jetbrains.mps.base language を使用すると、完全な Java 言語が拡張可能な DSL として実装されます。つまり、その言語を単純に拡張したり、独自の DSL に特定の概念を再利用したりすることができます。典型的な候補は一般式または型システムですが、そうでない場合は自分で作成する必要があり、非常に複雑な作業になります。私たちにとって、Java 式と、「if」ステートメントや「for」ステートメントなどの基本的な制御構造が特に興味深いものでした。振り返ってみると、DSL を Java 風味にするというこの決定は、他の開発者の参入障壁を下げるのに非常に役立ちました。結局のところ、Java は広く普及しているよく知られた言語です。
不思議なことに、Jetbrains MPS はその時までに独自の Java DSL を他の複数の DSL で拡張していました。2010 年に、Java の関数型コレクション言語 (jetbrains.mps.baselang.collections) に特に感銘を受けました。この DSL を使用すると、当時は存在しなかった Java Streams に似た、純粋に関数型のスタイルでコレクションに対する操作を作成できます。実際、DSL 構文はストリームよりも簡潔であるとさえ認識していました。包括的なクロージャー (jetbrains.mps.baselang.closure) のサポートも、当時はすぐに利用できました。
MMS プロジェクト、DSL、現状
MMS 全体とそのコンポーネントを JetBrains MPS IDE のみで開発しました。3 つの DSL を並行して考案し、実装しました。MMS プロジェクトで使用される 3 つの DSL は次のとおりです。
org.modellwerkstatt.manmap | Manmap は、SQL データベース用のコードジェネレーターを備えたオブジェクトリレーショナルマッパーであり、ユーザーがエンティティを簡単に取得および保存できるようにします。マンマップに焦点を当てることにしたのは、その透明性と「理解しやすい」動作のためです。 |
---|---|
org.modellwerkstatt.objectflow | Objectflow は、たとえば、集約、エンティティ、値オブジェクト (戦術的なドメイン駆動設計に従う) を使用してドメインモデルをモデリングするための DSL です。サービスに加えて、コマンドの概念も提供され、エンドユーザーの対話の説明が簡素化されます。 |
org.modellwerkstatt.dataUx | DataUx は、よく知られたマスター / 詳細パターンやコマンドの生成元となる必要なメニュー構造など、テクノロジーに依存しないエンドユーザー UI のモデリングに使用されます。DataUx を使用すると、UI テクノロジー全体を数年後に簡単に置き換えることができます。 |
3 つの DSL はすべて JetBrains MPS Java DSL (BaseLanguage) に基づいており、そこから型システムと基本的なセマンティック指向も取得しています。この記事では、DSL の再利用について強く主張します。Java BaseLanguage の拡張により、独自の DSL 開発が大幅に増加しました。当社では、JetBrains コレクション言語を集中的に使用して、優れた機能的な構文で数量ベースのビジネスロジックを定式化します。現在までのところ、欠点は見つかっていません。それどころか、JetBrains MPS は 2023 年においても安定した成熟した言語ワークベンチであると認識しています。
図 1 は、テクノロジーに依存しない方法で必要な UI をモデル化するために使用される DataUx DSL の概要を示しています。グリッドレイアウトでは、オーダー (ドイツ語で Bestellung) の 7 つのフィールドが 2 列に表示され、フォーム (無効) にレイアウトされます。フォームは最小の高さ (-1) のグリッドの最初の行にあります。2 行目は、最大高さ (1*) のテーブルを示します。DataUx DSL を使用した UI モデリングに関するより詳細な古い記事は、DZONE(英語) にあります。
2023 年の夏の時点で、MMS は上に示したものと同様の 374 の UI ページで構成されています。必要な UI コントローラーも DSL 経由で指定され、別の 488 個のいわゆるルートコンセプトインスタンスに貢献します。MMS のドメインモデルには 128 のエンティティと 87 の値オブジェクトが含まれており、これらは 245 のサービスと 169 のリポジトリで使用されます。これらの上位レベルの概念に基づいて、コードジェネレーターは、合計 1,130,000 行の生産コード (つまり、テストと関連コードを除く) を含む 2,886 個の Java ファイルを生成しました。このシステムは、バーコードスキャナを備えた約 550 台の Android デバイスと約 500 台のデスクトップコンピューター上で非常に安定して動作します。
これまでのところ、すべてがうまくいっているようです ! ただし、開発者の観点から見ると、MMS に取り組んで MPS 内で直接起動することは、長年にわたってますます。遅くなってきています。MMS の実行可能コードの再生成には、標準的なコンピューターで約 3 ~ 4 分かかりました。当初はモデル数も少なく、アプリケーションの生成や起動時間も非常に速かったです。13 年間の開発を経て、プロジェクトには複数のソリューションにわたって 139 個の MPS モデルが蓄積されました。開発者はアプリケーションを再生成しながら新聞を読み始めました。2023 年夏の主なゴールの 1 つは、生成時間を大幅に短縮することです。
コードジェネレーターの最適化
当社の DSL スタックには複数のコードジェネレーターが含まれています。当社の各 DSL には独自のコードジェネレーターが付属しています。JetBrains ベース言語、コレクション言語、クロージャー言語もそれぞれ 1 つずつ提供します。BaseLanguageInternal ジェネレーターを追加すると、合計 7 つの異なるコードジェネレーターが得られます。DSL を構想するときに生成速度に特別な注意を払っていなかったため、いくつかの実行不可能なタスクや MPS ジェネレーターのさまざまな特性が確実に見つかりました。心の片隅で、ジェネレーターの推奨プラクティスやルールの一部に違反しているためはないか、あるいはジェネレーターのルールやテンプレートを次善の方法で構築しているためはないかと疑っていました。
ジェネレーターと一般的なセットアップを分析する前に、生成した行の数に少々驚きました。全体として、MMS は 2,192 のルート概念で構成され、それらは 2,885 の Java ファイルに変換されます。ファイルの内容は合計 100 万行を超えるコードでした。これらは非常に大きな数字なので、生成に非常に時間がかかるのも不思議ではありません。
対策 1: 賢いランタイムを開発し、そのランタイムを活用するコードを生成する
今振り返ってみると、最初にコードジェネレーターを設計したとき、明らかなパターンに従っていました。基本的に、必要かどうかに関係なく、すべてのコードはジェネレーターテンプレートに直接入力されました。次の簡単な例は、以前のアプローチを簡単に示しています。
図 2 のジェネレーターテンプレート内の Java クラスは、IGenSelControlled
インターフェースを直接実装します。saveAndValidate()
や collectDelegateChanges()
などの定型メソッドはテンプレートに直接含まれていますが、入力モデルに応じてメソッドの動作に違いはありません。メソッドの実装はまったく変わりません。実際、実装は常に同じです。同じ図 2 からもわかるように、同じ特性のメソッドが 1 つではなく複数存在します。
図 3 は、そのジェネレーターテンプレートのリファクタリングされたバージョンを示しています。Java クラスは別の Table クラスを拡張しました。上記の静的コードはすべて拡張クラスに実装されます。さらに、入力モデルの影響を受ける動作は、ブール型プロパティの形式で実装されます。これは、図 3 のコンストラクター宣言で確認できます。Table クラスの動作を構成するには、プロパティマクロが適用された 3 つの異なるブールスイッチを使用できます。さらに、前者のメソッド selectionChanged()
は、より複雑なジェネレーターフラグメントがどのようにリファクタリングされたかを示しています。コードの静的部分はランタイム Table クラスに移動しましたが、動的部分は calcSelectionSummeryLineText()
に移動しました。このメソッドにはパラメーターと戻り値が付属します。メソッド自体は非常にコンパクトです。ドメインロジックを計算する式は、入力モデルから生成されたクラスに渡されます。ここで、そのメソッドは Table クラスによって呼び出され、図 2 と同じ動作に似ています。
全体として、独自のコードジェネレーターでの同様のリファクタリングにより、生成される行数が 100 万行以上から 484,000 行まで削減されます。静的コードをジェネレーターテンプレートからランタイムに移動することにより、生成およびコンパイルされるコードの量が半分になりました。これにより、開発者マシンでの生成とコンパイル時間が約 2 分短縮されます。非常に重要な改善です。コードを生成するための強力なランタイム環境を構築することを強くお勧めします。
対策 2: 概念を名前で参照しないでください
MPS は、これまでモデル生成パフォーマンスレポートをサポートしてきました。このレポートタイプは、ジェネレーターの設計者に、モデルごとに使用されるさまざまな生成ステップに関するパフォーマンスのインサイトを提供します。レポートは、最も時間を費やしているジェネレーターのステップが最初にリストされるように並べ替えられます。ジェネレーターのステップに費やされた時間と、単一ステップ内のアクティビティを簡単に特定できます。また、ウィービングルール、リダクションルール、デルタ変更に費やした時間も表示されます。私たちにとって特に価値があるのは、参照の復元に費やした時間です。
参照の復元は、参照マクロでターゲット名のみを操作する場合に、実際のターゲットの概念を検索するプロセスです。MPS 参照マクロは、ターゲットの概念 (node<>
または node-ptr<>
) または単に名前 (文字列) を返すことができます。プレーン名を使用する場合、MPS は出力モデル内で参照されるターゲットを見つける作業を引き継ぎます。ただし、この追加作業には時間がかかる場合があります。これは、「参照の復元」として報告される期間と同じです。
MMS プロジェクトのコンテキストでは、参照の復元に費やされた時間は一見すると重要ではありませんでした。ただし、140 個のモデルと複数の生成ステップを使用している場合、参照の復元に 1 ~ 2 秒の時間がかかります。ジェネレーターの別のリファクタリングで、ターゲットの概念の参照に一般的に対処しようとしました。これを行うには複数の方法が考えられます。
名前による参照に関する主な対策は、ジェネレーターマッピングラベルを積極的に使用することです。以下の図 4 に示すように、マッピングラベル「DelegateFD」がループマクロに付加され、クラス内で生成された各クラスフィールドにラベルが付けられます。このラベル付けプロセスにより、入力概念インスタンスと生成された概念インスタンスの間に関係が作成されます。後で、ジェネレーターの設計者は、これらのラベルを介して入力コンセプトによって生成されたコンセプトを検索できます。通常、これが参照マクロの目的です。特定の入力概念インスタンスに基づいて、出力モデルで関連して生成されたターゲットを参照したいと考えています。図 4 に示す例では、ジェネレーターの設計者は、生成されたクラスフィールドの 1 つを検索する可能性があります。
ラベルのマッピングを試すのは簡単なプロセスです。必要なのは、生成されたコードにラベルを付け、それらのラベルを参照マクロで使用することだけです。これは非常に簡単です。ラベル付けされた生成コードが、参照マクロが適用されたコードと同じ出力モデル内に存在する限り、ラベルのマッピングによって追加の負担が追加されることはありません。モデル間でインスタンスを参照する場合、状況は少し複雑になります。デフォルトでは、異なる入力モデルと出力モデル間でラベルにアクセスすることはできません。たとえば、当社のジェネレーターの場合、そのような状況はすぐに起こります。モデル A では、「サービスコンポーネント」S が宣言されており、そのサービスコンポーネント S はモデル B からも使用され、アクセスされます。モデル B で生成されたコードは、モデル A で生成されたコードを参照する必要があります。このシナリオには 2 つの解決策があります。
モデル間の変換を行う代わりにテキストを直接生成する場合、参照の復元は不要になります。これは、ジェネレーターでモデル間を参照するときに使用したものです。図 4 のスーパークラスコンストラクター呼び出しでは、JetBrains BaseLanguageInternal 言語の概念を使用して、実行時に Java クラスのクラスインスタンスを決定します。この概念は、標準の Java .class
式とは異なります。テキストとして完全修飾名を受け入れ、出力モデル内の概念の準備タイプを決定するための分類子を受け入れます。その概念に対応するジェネレーター (JetBrains TextGen) は、指定された名前に .class
を追加することで Java テキストに直接変換します。図 4 からわかるように、参照マクロの代わりにプロパティマクロが使用されています。この BaseLanguageInternal 概念を使用する場合、参照の復元は廃止されます。
TextGen を使用してテキストに直接変換する、さらに類似した BaseLanguageInternal の概念があります。モデル間の参照が必要な場合、ジェネレーター内のそれらの一部に依存しました。これにより、MMS 全体の生成プロセスでさらに約 45 秒節約されます。BaseLanguageInternal のものはいくぶん「内部」的なものであるように見えますが、ジェネレーターのリファクタリングを必要とするような概念への重大な変更は発生しませんでした。テキストを直接生成するこのアプローチは、この状況におけるクロスモデル参照シナリオに対する実行可能な解決策です。テキストが生成された後は、他の変換を適用できないことに注意してください。
モデルの相互参照状況を処理するために推奨される新しいアプローチ、つまりチェックポイントと組み合わせた生成計画もあります。生成計画を使用すると、開発者はモデルの生成順序を明示的に指定できるため、生成プロセスをより適切に制御できるようになります。生成計画では、生成プロセスに含める必要があるすべての言語をリストし、適切に順序付けし、オプションでジェネレーターが現在の一時モデルを保存するチェックポイントを指定します。これらのモデルは、生成プロセスのさらに後の自動モデル間参照解決に使用できます。従来のアプローチでジェネレーターの「ジェネレーターの優先順位」を指定しましたが、さらにジェネレータープランも試してみました。生成計画を立てるのは実に簡単です。ジェネレーターを上から順にリストすることで、すべての優先順位を指定できます。言語スタックとそのジェネレーターの完全な計画を図 5 に示します。
ご覧のとおり、モデルの相互参照に関しては BaseLanguageInternal アプローチに依存しているため、計画ではチェックポイントを指定しませんでした。ただし、モデル A で「サービスコンポーネント」 S が宣言され、そのサービスコンポーネント S がモデル B からも使用され、アクセスされる状況に似せるために、デモ目的で簡単な言語を作成しました。非常に単純化した「サービスコンポーネント」は次のようになります。ジェネレーターを使用して Java クラスに変換されます。モデル B から生成されたコードは、モデル A の生成されたコード内のまさにそのクラスを参照する必要があります。生成計画にチェックポイントを追加すると (つまり、デモ言語の「applygenerator」コマンドの後に「checkpoint」キーワードを追加するだけです)、すべてがすでに完了しています。MPS は、ラベルと入力によって生成されたコンセプトを検索するときに、チェックポイントモデルを自動的に考慮します。メカニズムはモデル内参照解決の場合とまったく同じです。ジェネレーターの設計者として、追加で考慮すべきことはありません。実際、デモ言語でチェックポイントを使用して生成計画をセットアップすることはまったく面倒ではないことがわかり、さらに、ラベルを使用したクロスモデル参照もすぐに機能しました。
対策 3: ジェネレーターでの「.type」の計算には注意する
Jetbrains.mps.lang.typesystem 言語は、ジェネレーター内の任意のノードの型を決定するための .type
操作を提供します。通常、さまざまな決定は式の型に基づいて行われるため、これは式を処理する場合に非常に役立ちます。例: 式に適用できるメソッドは、その式の最も外側の概念が変数参照であるか別のメソッド呼び出しであるかではなく、その式の型に依存します。
特定のノードのタイプに基づいてジェネレーターでさまざまな変換を実行することは珍しいことではありません。ただし、.type
を使用した型の計算は、かなり CPU を集中的に使用する操作です。開発マシンでいくつかの測定を行いました。大まかな指標として、単一の .type
計算には 2 – 30 ミリ秒かかります。これは最初は特に問題にならないように見えますが、MMS 全体が 400,000 ノードで構成されていることを考慮すると、慎重に適用しないと、ジェネレーターでの .type
計算の時間が増加する可能性があります。
設定で「タイプ計算に費やした時間」を選択すると、ジェネレーターでの .type
計算に費やした時間も、前述の「モデル生成パフォーマンスレポート」にレポートされます。残念ながら、この場合、ジェネレーターの設計者はパフォーマンス情報に圧倒されてしまいます。ご想像のとおり、これらのパフォーマンスに関するヒントをすべて確認するのは、非常に面倒なプロセスです。
リファクタリングの 1 つは、まったく同じ式の型計算が複数回行われないようにすることで、生成時間を最小限に抑えることを特に目的としていました。ケースは次のとおりです。私たちの言語の 1 つでは、非プリミティブ値の算術演算が導入されました (Java BigDecimal
クラスなど)。この言語のユーザーは、add()
へのメソッド呼び出しの代わりに、単純に「+」を使用できます。コードジェネレーターは、コード内の式のタイプをインスペクションすることにより、コードをわずかに再フォーマットします。型が BigDecimal
で「+」が適用されている場合、式を変換する必要があります。
比較演算 (== / < / <= など) も言語の一部として許可したため、compareTo()
メソッドの呼び出しに変換する必要がありました。同じ再フォーマットロジックを言語内のすべての比較とすべての算術演算に適用する必要がありました。素朴なアプローチでは、変換先の異なるメソッド呼び出しごとに 1 つの異なる MPS ジェネレーター削減ルールを作成しました ( 図 6 を参照)。このような各リダクションルールには .type
計算の条件が含まれていたため、入力モデルの式の同じ部分に対して複数の計算が行われることになりました。
これらの計算を最小限に抑えるために、リファクタリングによる段階的なプロセスを導入しました。左部分と右部分を持つ何らかの形式の操作が BigDecimal
に適用されると、一時オブジェクトで計算された型を生成コンテキストに保存し、その情報を再利用します。つまり、一般的な BinaryOperation
に対して「継承」が有効になっているリダクションルールを 1 つだけ使用して、条件内の左側の式の型をチェックします。その型が BigDecimal
または Int
の場合、その型が保存され、適切な式が評価されて保存されます。その情報は MPS テンプレートスイッチに渡され、それに応じて操作が変換されます。テンプレートスイッチには .type
計算が含まれておらず、同じ式に対して複数の計算が存在することもありません。
ジェネレーターでの .type
計算は CPU を大量に使用する操作であるため、生成が大幅に遅くなる可能性があります。私たちの場合、このリファクタリングによってパフォーマンスがわずかに向上しただけでした。ただし、ジェネレーターに対する最も重要な変更は、生成されるコードの総量が最小限に抑えられたことです。当然のことながら、これにより、生成中にインスペクションされる式の数が大幅に減少し、生成プロセスで型の計算が繰り返される問題の軽減に役立ちました。
生成速度と開発者の満足度
理論的には、DSL の利点は多岐にわたります。これらは、ソリューション開発時の生産性の向上を約束します。読み、書き、理解する必要があるプログラムの行数が単純に減少します。エレガントで簡潔な構文により読みやすさが向上します。DSL では、ドメインの本質的な概念を直接マッピングすると、通常、意味論的により表現力が高まるため、コンテンツの広範な検証と検証が可能になります。最後になりましたが、DSL は、開発チーム内や各ドメインの他の専門家とのコミュニケーションに非常に役立ちます。
DSL にはこれらの一般的な利点がありますが、エンドユーザーのための最終ソリューションだけに焦点を当てるべきではありません。DSL を適用する際の開発チームの満足度も重要です。プロジェクトで DSL を使用することに伴う利点を享受するには、やる気のある開発者チームが必要条件です。
MMS システムを MPS 内で独占的に開発し、プロジェクトの過程で考案した 3 つの DSL を並行して適用しました。13 年間の開発を経て、MMS プロジェクト自体は、DSL にもかかわらず、400,000 ノードを超える 139 モデルにまで成長しました。これらのノードは、7 つの異なるコードジェネレーターのスタックによって Java コードに変換されます。3 つは私たち自身が作成したもので、4 つは既存の JetBrains 言語から取得したものです。MMS プロジェクトに取り組む開発者にとって、時間の経過とともに生成速度がますます。問題となり、ソリューション全体を再生成する際にさらなる忍耐が求められました。開発者の満足度を損なう可能性のある状況。
JetBrains MPS の生成メカニズムは一般的に遅いとは言えませんが、大規模なプロジェクトのコードジェネレーターは慎重に設計する必要があります。この記事では、生成プロセスを加速するためのアイデアと対策を導入しました。最大規模のプロジェクトの 1 つを通じて学んだ教訓について報告しました。ジェネレーターの大規模なリファクタリングの後、標準の開発マシンで完全なプロジェクトの再構築にかかる生成時間とコンパイル時間を 3 ~ 4 分からわずか 1 分に短縮することができました。この方法を使用すると、ツールを使用した日常の作業で快適なエクスペリエンスを維持しながら、開発者に表現力の高い言語を提供できます。
関連ページ:
研究
MPS に関する実際の顧客体験に基づいた技術的な研究を参照してください。プラグインに飛び込む: PlantMPS プラグインジェネレーターのパフォーマンスを向上させるためのヒント
JetBrains MPS プロジェクトへの貢献
バグレポートを提出する:バグの報告は、参加するための最も簡単な方法です。バグレポートは提出に時間がかからず、開発者にとって非常に役立ちます。問題を発見したら、JetBrains MPS issue tracker に報告してください。環境 (OS、JDK、MPS バージョン) に関する情報、問題を再現する手順、問題の説明を必ず提供してください。新しい問題を作成すると、トラッカーは同様の既存の問題を一覧表示します。重複する問題を避けるために確認してください。現在の問題に賛成票を投じてください。問...