今日は新しいRustベースのリレーコンパイラをオープンソースでリリースできることにとても興奮しています(v13.0.0-rc.1
)。この新しいコンパイラはより高速で、新しいランタイム機能をサポートし、将来のさらなる成長のための強固な基盤を提供します。
このリリースに至るまで、Facebookのコードベースは止まることなく成長してきました。私たちの規模では、コードベース内のすべてのクエリをコンパイルする時間は、開発者の生産性を犠牲にして直接増加していました。JavaScriptベースのコンパイラを最適化するためのさまざまな戦略を試しましたが(以下で説明)、パフォーマンスゲインを徐々に引き出す能力は、コードベース内のクエリの数の増加に追いつくことができませんでした。
それで、私たちはコンパイラをRustで書き直すことにしました。Rustを選びました。Rustは高速でメモリーセーフであり、大きなデータ構造をスレッド間で安全に共有できます。開発は2020年初頭に開始され、その年の終わりにコンパイラを社内で出荷しました。アプリケーション開発の中断はなく、ロールアウトはスムーズに行われました。最初の内部ベンチマークでは、コンパイラは平均してほぼ5倍、P95ではほぼ7倍優れていることが示されました。それ以来、コンパイラの性能をさらに向上させています。
この投稿では、Relayにコンパイラがある理由、新しいコンパイラで何が実現されることを期待しているか、その新機能、およびなぜRust言語を使用することを選択したのかについて説明します。新しいコンパイラの使用を急いで開始したい場合は、コンパイラパッケージのREADMEまたはリリースノートをご覧ください。
Relayにコンパイラがある理由
Relayには、安定性の保証を提供し、優れたランタイムパフォーマンスを実現するためにコンパイラがあります。
その理由を理解するには、フレームワークの使用ワークフローを考えてみましょう。Relay では開発者は、GraphQL と呼ばれる宣言型の言語を使用して、各コンポーネントに必要なデータを指定しますが、その取得方法ではありません。次に、コンパイラはこれらのコンポーネントのデータ依存関係を与えられたページのすべてのデータをフェッチするクエリに結合し、Relay アプリケーションに非常に高いレベルのパフォーマンスと安定性をもたらすアーティファクトを事前計算します。
このワークフローでは、コンパイラ
- コンポーネントが個別に論理的に扱われることを可能にして、多くのバグが発生するのを防ぎ、
- 可能な限り多くの作業をビルド時間にシフトして、Relay を使用するアプリケーションのランタイムパフォーマンスを大幅に向上させます。
これらを順番に調べましょう。
ローカルでの論理推論をサポート
Relay では、コンポーネントは GraphQL フラグメントを使用して独自のデータ要件のみを指定します。次に、コンパイラはこれらのコンポーネントのデータ依存関係を与えられたページのすべてのデータをフェッチするクエリに結合します。開発者は、データ依存関係がより大きなクエリにどのように適合するかを心配することなく、コンポーネントの記述に集中できます。
しかし、Relay はこのローカルでの論理推論をさらに進めます。コンパイラは、Relay のランタイムによって特定のコンポーネントのフラグメントによって選択されたデータのみを読み出すために使用されるファイルも生成します(これは データマスキングと呼んでいます)。したがって、コンポーネントは明示的に要求していないデータには決して(実際に、型レベルだけでなく!)アクセスしません。
このように、1 つのコンポーネントのデータ依存関係を変更しても、もう 1 つのコンポーネントに表示されるデータに影響を与えられないため、開発者はコンポーネントを個別に論理的に処理できます。これにより、Relay アプリは比類のないレベルの安定性を獲得し、多くの種類のバグが発生しなくなります。また、多くの開発者が同じコードベースに触れることができる理由の重要な要素でもあります。
ランタイムパフォーマンスの向上
Relay は、コンパイラを利用して可能な限り多くの作業をビルド時間にシフトし、Relay アプリのパフォーマンスを向上させます。
Relay コンパイラは、すべてのコンポーネントのデータ依存関係に関するグローバルな知識を持つため、手動で記述された場合と同じかさらに優れたクエリを作成できます。これを実行するには、ランタイムでは非現実的に遅くなる方法でクエリを最適化します。たとえば、生成されたクエリからアクセスできないブランチを削除し、クエリ内の同一のセクションをフラット化します。
また、これらのクエリはビルド時に生成されるため、Relay アプリケーションは GraphQL フラグメントから抽象構文木(AST)を生成したり、それらの AST を操作したり、実行時にクエリのテキストを生成したりすることはありません。代わりに、Relay コンパイラはアプリケーションの GraphQL フラグメントを、ネットワークデータをストアに書き込み、読み戻す方法を記述する、事前計算された最適化された命令(プレーンな古い Javascript データ構造)に置き換えます。
この配置には、Relay アプリケーションバンドルにスキーマを含まないこと、および永続的なクエリを使用する場合に GraphQL フラグメントの文字列表現を含まないことも利点としてあります。これによりアプリケーションのサイズが縮小され、ユーザーの帯域幅が節約され、アプリケーションのパフォーマンスが向上します。
実際、新しいコンパイラはさらに一歩進んでおり、別の方法でユーザーの帯域幅を節約します。Relayはアプリケーションサーバーにビルド時に各クエリテキストを通知し、ユニークなクエリIDを生成できます。つまり、アプリケーションは潜在的に非常に長いクエリ文字列をユーザーの遅いネットワーク経由で送信する必要がなくなります。そのような永続クエリを使用する場合、ネットワーク要求を行うためにワイヤー越しに送信する必要があるのは、クエリIDとクエリ変数だけです。
新しいコンパイラは何を実現しますか?
コンパイラ言語は、動的言語に比べて摩擦が生じ、開発者の速度を低下させると見られることがあります。しかし、Relayはコンパイラを活用して摩擦を軽減し、一般的な開発者のタスクを容易にします。たとえば、Relayは、ページネーションや新しい変数でのクエリの再フェッチなど、微妙に誤りやすい一般的な対話の高レベルプリミティブを公開します。
これらの対話は古いものから新しいクエリを生成する必要があり、そのため、ボイラープレートと重複を伴います。これは自動化の理想的な対象です。Relayはコンパイラのグローバルナレッジを活用して、開発者が1つのディレクティブを追加し、1つの関数呼び出しを変更するだけでページネーションと再フェッチを有効にします。以上です。
しかし、開発者にページネーションを簡単に追加する機能を与えることは、氷山の一角に過ぎません。コンパイラに対する私たちのビジョンは、機能の提供とボイラープレートの回避のためのさらに高レベルのツールを提供し、開発者にリアルタイムの支援と洞察を提供し、GraphQLで作業するための他のツールで使用できるパーツで構成されることです。
このプロジェクトの主な目標は、書き換えられたコンパイラのアーキテクチャによって、今後数年間でこの構想を実現できるようになることでした。
まだ実現には至っていませんが、各基準において画期的な成果を上げてきました。
たとえば、新しいコンパイラは、新しい@required
ディレクティブのサポートが付属しており、これにより、親リンクフィールドが無効になったり、読み取り時に特定のサブフィールドがNULLの場合はエラーが発生します。取るに足らない品質向上のように聞こえるかもしれませんが、コンポーネントコードの半数がNULLチェックである場合は、@required
はかなり優れた選択になります。
@required
を使用していないコンポーネント
@required
を使用している場合
次に、コンパイラは、タイプするときにフィールド名を自動的に補完し、ホバーにタイプ情報を表示するなどの機能を備えた、内部専用のVSCode拡張機能を備えています。まだ公開されていませんが、いずれ公開する予定です!このVSCode拡張機能により、GraphQLデータの操作がはるかに簡単で直感的なものになるという経験を積んでいます。
最後に、新しいコンパイラは、他のGraphQLツールで再利用できる一連の独立したモジュールとして記述されました。Relayコンパイラプラットフォームと呼んでいます。内部的には、これらのモジュールは他のコード生成ツールや、他のプラットフォームの他のGraphQLクライアントに再利用されています。
コンパイラのパフォーマンス
これまで、Relayがコンパイラを持っている理由と、書き換えによって期待できることについて説明しました。しかし、2020年にコンパイラを書き換すことを決定した理由、つまりパフォーマンスについてはまだ説明していません。
コンパイラを書き換すという決定が下される前は、コードベース内のすべてのクエリをコンパイルするのにかかる時間は、コードベースの増加に伴い、徐々に、しかし確実に遅くなっていました。パフォーマンスの向上を図る能力は、コードベース内のクエリ数の増加に追いつくことができず、この困難な状況から段階的に抜け出す方法はありませんでした。
JavaScriptの限界に到達
以前のコンパイラは JavaScript で書かれていました。これは、いくつかの理由により自然な選択でした。チームが最も経験のある言語であり、Relay ランタイムが記述されている言語(コンパイラとランタイムのコードを共有可能)、そして GraphQL のリファレンス実装とモバイル GraphQL ツールが記述されている言語です。
コンパイラの性能はしばらくの間、妥当なままでした。Node/V8 には、高度に最適化された JIT コンパイラとガベージコレクタが付属しており、注意すればかなり高速にすることができます(当社はそうしました)。しかし、コンパイル時間が長くなっていました。
追従するためにたくさんの方法を試しました。
- コンパイラをインクリメンタルにしました。変更に応じて、その変更の影響を受けた依存関係のみを再コンパイルします。
- 遅い変換(つまり flatten)を特定し、メモー化の追加など、可能なアルゴリズムの改善を行いました。
- 公式の
graphql
npm パッケージの GraphQL スキーマ表現では、スキーマを表すのにギガバイト単位のメモリが必要だったため、カスタムフォークに置き換えました。 - 最も重要なコードパスで、プロファイラによるガイド付きのマイクロ最適化を行いました。たとえば、
...
演算子を使用してオブジェクトをクローンして変更しなくなった代わりに、コピーするときにオブジェクトのプロパティを明示的にリストすることを好みました。これによりオブジェクトの隠しクラスが保持され、コードをより JIT 最適化できるようになりました。 - コンパイラを再構築して複数のワーカーにシェルアウトし、各ワーカーが単一のスキーマを処理しました。複数のスキーマを持つプロジェクトは Meta の外では一般的ではないため、これを使用してもほとんどのユーザーはシングルスレッドコンパイラを使用していたでしょう。
これらの最適化では、Relay の急速な内部採用に対応するには不十分でした。
最大の課題は、NodeJS が共有メモリを使用したマルチスレッドプログラムをサポートしていないことです。メッセージを渡すことによる通信を開始する複数のワーカーを開始するのが唯一できることです。
これはいくつかのシナリオではうまく機能します。たとえば、Jest はこのパターンを採用し、ファイルを変換するテストを実行するときにすべてのコアを利用します。これは、Jest がプロセス間で大量のデータやメモリを共有する必要がないため、適しています。
一方で、当社のスキーマは単にメモリ内の複数のインスタンスを持つには大きすぎるため、JavaScript ではスキーマ 1 つあたり 1 つ以上のスレッドで Relay コンパイラを効率的に並列化する方法がありませんでした。
Rust を選択
コンパイラを書き直すことを決めた後、プロジェクトのニーズを満たす言語を評価しました。高速でメモリセーフでコンカレンシーをサポートする言語が必要でした。できれば、実行時ではなくビルド時にコンカレンシーのバグが検出されるものでした。同時に、社内で十分にサポートされている言語が必要でした。これにより、選択肢は数少なくなりました。
- C++ はほとんどの基準を満たしましたが、習得が難しいと感じました。また、コンパイラは当社が必要とするほど安全性に協力してくれません。
- Java もおそらくまともな選択肢でした。高速でマルチコアですが、低レベルの制御はあまりありません。
- OCaml はコンパイラ空間で実績のある選択ですが、マルチスレッド化は困難です。
- Rust は高速でメモリセーフで、コンカレンシーをサポートします。スレッド間で大量のデータ構造を安全に共有することを容易にします。Rust を取り巻く一般的な興奮、チームの過去の経験、Facebook の他のチームでの使用により、これは当社にとって明白な最良の選択肢でした。
内部展開
Rust は見事に適していると判明しました! 主に JavaScript 開発者からなるチームは Rust を簡単に採用することができました. さらに、Rust の高度な型システムは多くのエラーをビルド時にキャッチし、速度を維持することに役立ちました.
開発は 2020 年初めに開始され、同年の年末に社内でコンパイラを展開しました. 初期の社内ベンチマークでは、コンパイラのパフォーマンスが平均で約 5 倍、P95 では約 7 倍向上していることが示されました. 以降、コンパイラの性能をさらに向上させています.
OSS でのリリース
本日、Relay v13 の一部として、コンパイラの新しいバージョンを公開できることを嬉しく思います. コンパイラの新しい機能は次のとおりです.
@required
ディレクティブ.- 共通フラグメントのインライン化を防ぎ、結果として生成されるファイルを小さくするために使用できる
@no_inline
ディレクティブ. - 矛盾する GraphQL フィールド、引数、およびディレクティブの検証
- TypeScript 型生成のサポート
- リモートクエリ永続化のサポート.
README と リリースノートで、コンパイラに関する詳細情報を見つけることができます!
グラフ上の派生値にアクセスする機能を開発者に提供し、ローカルデータの更新により人間工学的な構文のサポートを追加し、VSCode 拡張機能を完全に展開するなど、コンパイラ内の機能を引き続き開発しています. これらはすべてオープンソースでリリースされる予定です. このリリースを誇りに思っていますが、まだこれからです!
謝辞
このブログ記事について素晴らしいフィードバックを提供してくれた Joe Savona、Lauren Tan、Jason Bonta、Jordan Eldredge に感謝します. コンパイラのバグに関連する問題を提出してくれた ch1ffa、robrichard、orta、sync に感謝します. TypeScript サポートを追加してくれた MaartenStaa に感謝します. 現在デフォルトで有効になっている @required
ディレクティブを有効にするのがどれだけ難しいかを指摘してくれた @andrewingram に感謝します. 貢献してくれた人は他にもたくさんいます. これは真のコミュニティの取り組みでした!