エンジニアリングレビュー:2025年版

Everett Pompeii

Everett Pompeii


新しいテクノロジーを開発する際、例えば Bencher のような場合、「退屈な技術を選ぶこと」と「平均を打ち破ること」の間に基本的な緊張関係があります。この綱引きにおいて自分がどの立場にいるのか、瞬間的には判断するのが難しいことがあります。Rustプログラミング言語は3年ごとに新しい Rustエディション を発表します。このペースは良いものだと思います。大きな進展を遂げるには十分に長い時間ですが、あまりにも遠くに離れ過ぎないほど短い時間です。Bencherがこの春で3年を迎えるにあたり、ここまで来たすべてのエンジニアリング決定について振り返る良い機会だと思いました。

この投稿では、過去3年間にわたるBencherの「イノベーショントークン」の使い方を振り返ります。Bencherは継続的なベンチマーク ツールのオープンソース スイートです。私はBencherアーキテクチャ のフロントエンドから始まり、全体のスタックを見ていきます。各ステップで、どのようにここまで来たのかを議論し、各エンジニアリング決定がどのように展開したかに関するバイナリ判定を与えます。

フロントエンド

フロントエンドライブラリ

C++開発者から脱却中の身として、私はRustが非常に好きです。 もし私に選択権があったなら、BencherをフルスタックでRustで書くことができたでしょう。 Bencherのレポの奥深くを掘り下げると、私がまさにそれを試みているのを見ることができます。 YewSeedSycamoreを試してみました。 これらのライブラリは一部のプロジェクトでうまく機能するかもしれませんが、どうしても克服できなかった大きな問題がありました: JavaScriptインタープ

Rustを通じてWASMからJSインタープは可能ですが、 それは簡単ではないだろうと感じました。 私はBencherに非常にインタラクティブなプロットを持たせたいと考えていました。 これには、D3のようなライブラリを使う必要があり、つまりJSインタープが必要でした。

それで、JavaScriptを使う必要がある場合、どのライブラリを選ぶべきでしょうか?

過去に試したRustクレートに戻ると、 YewはReact HooksのRustアナログです。 以前にReact Hooksを使用してフロントエンドを構築しデプロイしたことがあるので、 このフレームワークについて最も知識を持っていました。 しかし、React Hooksのライフサイクルは非常に複雑で、 注意すべきポイントや奇妙な端ケースがたくさんあることに気づきました。

私は関数型リアクティブプログラミング(FRP)の基本的な原則が本当に好きです。 これが私をElmとそのRust版であるSeedを試すことに導きました。 残念ながら、Elmを使うことはRustを使うことと同じ問題に苦しみます。 Elmは独自のJavaScriptインタープを必要とします。 また、Elmアーキテクチャが私には少し制限的すぎると感じました。

試したRustフレームワークの中で、私はSycamoreが最も気に入りました。 SycamoreはSolidに触発されました。 Solidについて知れば知るほど、それが好きになりました。 Reactとは異なり、Solidは仮想DOMを使用しません。 代わりに、素のJavaScriptにコンパイルされます。 これにより、はるかに速く、軽量で、扱いやすくなります。 Solidは、細粒度のリアクティブ性を可能にするいくつかの強力なプリミティブで構成されています。 UIの何かが更新されると、それに依存するコードだけが再実行されます。 過去3年間、Solidは使っていてとても快適だったと感じています。

テクノロジー評価
Yew
Seed
Sycamore
Elm
SolidJS

フロントエンドフレームワーク

Solid自体は単なるライブラリです。 モダンなフロントエンドを構築するためには、 完全なウェブアプリフレームワークを使用する必要がありました。 シンプルさを保ちたかったので、私はすべての卵をSolidというバスケットに入れ、 最初にSolidStartを使用しました。 当時、SolidStartはシングルページアプリ (SPA) のみをサポートしていました。

SPAはスタートするには問題ありませんでした。 しかし最終的には、SEOのようなことを気にし始める必要があることもあります。 私はより多くのBencherドキュメントを書くようになっていました。 サイトの学習セクションも計画していました。 これはクライアントサイドレンダリング (CSR) と 静的サイトジェネレーション (SSG) の両方を必要とすることを意味しました。 SolidStartは非常に若かったため、私のすべてのニーズを満たすことができませんでした。

Astroについて学び、試してみた結果、 Bencherのフロントエンド全体をSolidStartからAstroに移植することに決めました。 これにはいくつかの欠点がありました。 最も明白なのは、関与する努力です。 正直に言って、それほど悪くはありませんでした。 Astroにはアイランドアーキテクチャと 一級のSolidインテグレーションがあります。 また、多くのロジックをSolid Routerから取ることができ、 うまくいったようでした。

今日でも存在する大きな妥協は、 Bencherがシングルページアプリからマルチページアプリになったことです。 コンソール内でクリックするほとんどの場所は、 ページ全体の再レンダリングを引き起こします。 Astroには、最初に切り替えを行ったときに ビューのトランジションの約束がありました。 それを試してみましたが、バグがありました。 まだ見直しが必要です。

その間に、SolidStartは少し追いついているようです。 現在ではCSRとSSGの両方をサポートしています。 ただし、私が必要とするように同じサイトで両方が機能するかどうかを確認していません。 過去の問題です。

テクノロジー評価
SolidStart
Astro

フロントエンド言語

AstroにはTypeScriptの組み込みサポートがあります。 SolidStartからAstroへの移行に伴い、JavaScriptからTypeScriptへの移行も開始しました。 BencherのTypeScript設定はAstroのstrictest設定に設定されています。 ただし、Astroはビルド中に型チェックを行いません。 執筆時点では、Bencherにはまだ604の型エラーがあります。 これらの型エラーはコード編集時のヒントのように使用されますが、ビルドをブロックすることはありません(まだ)。

また、BencherのRustデータ型をTypeScriptのフロントエンドと同期させるためにTypeshareを追加しました。 これはBencher Consoleの開発に非常に役立っています。 さらに、ユーザー名やメールアドレスなどのフィールドバリデーターは、WASM経由でRustコードとTypeScriptフロントエンドの間で共有されています。 SolidStartとAstroの両方でWASMを動作させるのは少し手間がかかりました。 フロントエンドで見られる最大のエラーのクラスは、WASM関数が呼び出される場所ですが、WASMモジュールがまだロードされていないというものです。 どう修正するかは分かりましたが、時々忘れてしまい、再び問題が発生します。

Rustコードから自動生成される共有の型とバリデーターの両方によって、フロントエンドとのインターフェースが非常に容易になりました。 これらはCIでチェックされるため、同期が崩れることはありません。 HTTPリクエストが正しく形成されていることを確認するだけで、すべてがうまく動作します。 これにより、フルスタックでRustを使用できないことの痛みが少し和らぎます。

テクノロジー判定
Rust
JavaScript
TypeScript
Typeshare
WASM

フロントエンドホスティング

私がSolidに「全力投球」することを決めたのは、NetlifyがSolidの作成者をフルタイムで雇ったという点に大いに影響を受けました。Netlifyの最大の競争相手はVercelです。VercelはNext.jsを作り維持しています。そして私は、NetlifyはSolidを彼らのNext.jsにしたいと考えていると思いました。したがって、SolidStartサイトをホスティングするのにNetlifyより良い場所はないだろうと思いました。

デフォルトでは、Netlifyは独自のビルドシステムを使用させようとします。Netlifyのビルドシステムを使用すると、アトミックデプロイを行うのが非常に難しくなります。Netlifyは、バックエンドパイプラインが失敗してもフロントエンドを公開してしまいます。それは非常に悪いです!これにより、フロントエンドをバックエンドと同じCI/CD環境でビルドして、NetlifyのCLIを使って最新バージョンをアップロードするようにしました。SolidStartからAstroに移行したときも、同じCI/CDセットアップを維持することができました。Astroには、公式のNetlifyインテグレーションがあります。

Bencherはしばらくの間、Netlifyの無料枠内に収まっていました。ただし、Bencherの人気が高まるにつれて、無料枠の制限を超え始めています。アストロサイトをsst on AWSに移すことも考えています。しかし、現時点ではコスト削減のための努力が見合わないと感じています。

技術判定
Netlifyビルド
Netlifyデプロイ

バックエンド

バックエンド言語

Rust.

テクノロジー判断
Rust

HTTPサーバーフレームワーク

RustのHTTPサーバーフレームワークを選ぶ際の主な考慮事項の一つは、組み込みのOpenAPI仕様サポートでした。 TypeshareとフロントエンドのWASMの設定に投資したのと同じ理由で、その仕様からAPIドキュメントとクライアントを自動生成する機能を望んでいました。 この機能が組み込みであり、サードパーティのアドオンでないことが重要でした。 自動化が実際に価値があるためには、ほぼ100%動作する必要があります。 つまり、メンテナンスと互換性の問題は、コアフレームワークエンジニア自体が負う必要があります。 そうでなければ、必然的にエッジケースの地獄に陥るでしょう。

もう一つの重要な考慮事項は、放棄されるリスクです。 かつて有望だったいくつかのRust HTTPフレームワークは、今ではほとんど放棄されています。 私が見つけた唯一のOpenAPI仕様サポートを組み込んだフレームワークで、信頼するに足るものはDropshotでした。 DropshotはOxide Computerによって作成され、現在もメンテナンスされています。

これまでのところ、Dropshotには大きな問題が一つしかありませんでした。 APIサーバーによってエラーが生成されると、レスポンスヘッダーの欠如により、フロントエンドでCORSのエラーが発生します。 そのため、ウェブフロントエンドはユーザーに対して非常に役立つエラーメッセージを表示できません。 修正を上流に統合するよりも、Bencherをより簡単かつ直感的に使用できるようにすることに注力しました。 しかし、その解決策は100行未満のコードであることが判明しました。冗談じゃない!

ちなみに、axumフレームワークはBencherの作業を始めた時点ではまだリリースされていませんでした。 もしその時点で存在していたら、多くのサードパーティのOpenAPIアドオンの一つと組み合わせて試していたかもしれません。 幸運にも、axumはまだ私を惑わせることはありませんでした。 Dropshotは素晴らしい選択でした。この点についてはAPIクライアントセクションをご覧ください。

テクノロジー判定
Dropshot

データベース

Bencherをできるだけシンプルに保とうとしています。 最初のバージョンのBencherでは、ベンチマーク結果自体も含めて、すべてをURLクエリパラメータで受け取るようにしていました。 しかし、すぐにすべてのブラウザにはURLの長さに制限があることを学びました。 納得ですね。

次に考えたのが、ベンチマーク結果をgitに保存し、プロットと結果を含んだ静的なHTMLファイルを生成する方法です。 ただし、このアプローチには2つの大きな欠点があります。 まず、git cloneの時間が、ヘビーユーザーには耐えられない長さになる可能性があります。 次に、すべての履歴データがHTMLファイルに含まれることになり、ヘビーユーザーには非常に長い初期ロード時間を強いることになります。 デベロッパーツールは、ヘビーユーザーに優しくあるべきです。

結果的に、私の問題には解決策があります。 それはデータベースというものです。

では、Postgresを取り入れて終わりにしない理由は何でしょうか? まあ、本当にBencherをセルフホストできるようにしたかったんです。 アーキテクチャをシンプルにできればできるほど、他の人がセルフホストするのが簡単(かつ安価)になります。 すでにフロントエンドとバックエンドを分けたため、2つのコンテナを必要としていました。 3つ目を避けられるか? そうです、避けられます!

Bencherの前には、テストデータベースとしてSQLiteのみを使用していました。 開発者体験は素晴らしかったですが、プロダクションでそれを動かすことは考えていませんでした。 その後、Litestreamに出会いました。 LitestreamはSQLiteのためのディザスタリカバリツールです。 バックグラウンドで動作し、S3や他の任意のデータストアに継続的に変更をレプリケートします。 これにより使いやすく、運用コストも非常に低く、 特にS3は書き込みに対して課金しないため、 小規模インスタンスだと1日数セントで済むことになります。

Litestreamに出会った当時、ライブリードレプリカが間もなく登場すると約束されていました。 しかし、これが実現されることはありませんでした。 提案された代替プロジェクトは、同じ開発者による後継プロジェクトである LiteFS でした。 しかし、LiteFSには大きな欠点があります。 すべてのレプリカがダウンすると、組み込みのディザスタリカバリを提供していません。 複数のレプリカを持つためには、アプリケーションロジックにリーダーかライターかの概念を組み込む必要があります。 そして決定的な障壁は、レプリカを管理するために常にConsul インスタンスを稼働させる必要があることでした。 SQLiteを使う最大の理由は、他のサービスを避けることでした。 幸いにも、Bencher CloudでLiteFSを使おうとはしませんでした。 というのも、LiteFS Cloudはローンチから1年で終了し、 LiteFS自体もほとんど死んでいます

現在、デプロイ間の小さなダウンタイムはBencher CLIによって対応されています。 将来的には、Kamalを使ってゼロダウンタイムデプロイに移行する予定です。 Rails 8.0がKamalとSQLiteをデフォルトにするにあたり、 KamalとLitestreamがうまく連携することにかなり自信を持っています。

テクノロジー結論
URLクエリパラメータ
git + HTML
SQLite
Litestream
LiteFS

データベースドライバー

データベースに近づくほど、より厳密な型付けを求めます。 フロントエンドでは少し気軽にやってもかまいません。 もし間違えても、次に本番環境へプッシュするときにはすべてが正されます。 しかし、データベースを破損させると、それを修正するのはかなりの手間です。 そのことを念頭に置き、私はDieselを選びました。

DieselはRust向けの強く型付けされたオブジェクトリレーショナルマッパー(ORM)であり、クエリビルダーです。 コンパイル時にすべてのデータベース操作をチェックし、ランタイムエラーを防ぎます。 このコンパイル時のチェックにより、DieselはSQLに対するゼロコストアブストラクションとなります。 私の側でパフォーマンスチューニングで1200倍速くした際に小さなバグがありましたが、Dieselを使用している間にランタイムのSQLエラーは発生していません。

🐰 おもしろ情報: Dieselはベンチマークツールとしてベンチャーを使用しています!継続的ベンチマークにも利用しています!

技術評価
Diesel

バックエンドホスティング

同じようにSolidを使っていたのでフロントエンドホスティングにはNetlifyを選んだのと同じ理由で、Litestreamを利用していたのでバックエンドホスティングにはFly.ioを選びました。Fly.ioはちょうどLitestreamの作成者をフルタイムで雇っていました。上記のように、Litestreamのこの作業は最終的にLiteFSによって吸収され、LiteFSは今ではなくなりました。なので、私が期待していたようにはなりませんでした。

将来的にKamalに切り替える際には、Fly.ioからも移行します。Fly.ioでは2度の大規模な障害が発生し、そのたびにベンチャーが半日ダウンしました。しかし、最大の問題は、Litestreamを使用することによるインピーダンスミスマッチです。

Fly.ioのダッシュボードにログインするたびに、この警告メッセージが表示されます:

ℹ あなたのアプリは単一のマシンで動作しています

1つのコマンドで複数のマシンでアプリをスケーリングして高可用性を確保しましょう:

fly scale count 2

スケーリングについて詳細はドキュメントをご確認ください。

しかし、Litestreamを使っていると、依然として複数のマシンを使うことはできません!約束したようなリードレプリケーションを提供することは決してありませんでした!

なので、これは少し皮肉であり、またフラストレーションを感じます。一時期、libSQLTursoを調べたこともあります。しかし、libSQLはレプリケーション用に特別なバックエンドサーバが必要であり、それがDieselで動作しない原因になっています。いずれにしても、また別のサポート終了のシャットダウンを回避したようです。LimboというTursoのSQLiteをRustで書き直したものに非常に興味がありますが、すぐに移行することはないでしょう。次のステップは、良好で安定したVMをKamalで動かすことです。

LitestreamのレプリケーションのためのAWS S3バックエンドは、問題なく機能しています。LitestreamとFly.ioに対する突然の変更にもかかわらず、BencherでLitestreamを使用するという判断は正しかったと考えています。Bencher Cloudでスケーリングの問題に直面し始めていますが、これは良い問題であると考えています。

技術評価
Fly.io
AWS S3

CLI

CLIライブラリ

RustのCLIを作成する際、Clapは事実上の標準といえるでしょう。 だからこそ、私が初めてBencherを公開デモしたとき、 その作成者であるEd Pageがそこにいたことには驚かされました! 🤩

時間が経つにつれて、Clapができる便利なことがますます増えているのを感じます。 ちょっと恥ずかしいのですが、最近になってようやく default_valueオプションを発見しました。 これらの機能すべてが、私がbencher CLIで維持しなければならないコード量を 大幅に削減してくれます。

🐰豆知識: ClapはBencherを使用してバイナリサイズを追跡しています

テクノロジー結果
Clap

APIクライアント

BencherHTTPサーバーフレームワークとしてDropshotを選んだ主な理由は、組み込みでOpenAPIスペックを生成する機能があったからです。将来的にはそのスペックからAPIクライアントを自動生成できることを期待していました。1年ほど後、Dropshotの開発者たちはProgenitorを提供しました。

ProgenitorはDropshotの補完的な存在です。DropshotからのOpenAPIスペックを使用して、ProgenitorはRustのAPIクライアントを位置指定パターンで生成できます:

client.instance_create("bencher", "api", None)

またはビルダーパターンで:

client.instance_create().organization("bencher").project("api").send()

個人的には後者が好みなので、Bencherではこちらを使用しています。ProgenitorはAPIとやり取りするためのClap CLI全体を生成することも可能ですが、それは使用していません。特にbencher runのようなコマンドでは、より細やかな制御が必要だったためです。

生成された型に関して見つけた唯一の注目すべき欠点は、JSON Schemaの制約により、itemキーが欠落している場合と値がnullに設定されているitemキーを区別する必要があるときに、Option<Option<Item>>を単純に使用できないことです。これはdouble_optionのようなもので可能ですが、JSON Schemaのレベルではすべて同じに見えます。flatteneduntaggedの内部構造体enumを使用してもDropshotとうまく連携しません。一番の解決策として見つけたのは、トップレベルのタグなしenumを使用することでした。ただし、現段階ではAPI全体でこのようなフィールドは2つしかないので、大した問題ではありません。

技術評価
Progenitor

開発

開発環境

私がBencherでの作業を始めたとき、人々はlocalhostの終わりを求めていました。私はすでに新しい開発用ラップトップが必要な時期を過ぎていたので、クラウド開発環境を試してみることにしました。当時、GitHub Workspacesは私のユースケースには一般利用(GA)されていなかったため、Gitpodを選びました。

この実験は約6か月続きました。結論として、クラウド開発環境はサイドプロジェクトにはうまく機能しません。5分だけ作業をしたいですか? できません!開発環境が1,000回目の再初期化を行うのを待たなければなりません。週末の午後に本気で作業を進めたい? できません!開発環境が使用中にランダムに停止します。何度も何度も。

これらの問題は有料ユーザーとして直面しました。月額25ドルで、5年ごとにスペックのはるかに優れた新品のM1 MacBook Proが手に入ることができます。Gitpodが料金を固定から使用ベースに変更すると発表したとき、私は彼らが私のプランをキャンセルさせ、apple.comへ向かいました。

これらはすべてGitpodのKubernetesの使用という廃止された決定に関する問題だったのかもしれません。しかし、私はBencherを使って別のクラウド開発環境を試すつもりはありません。最終的には、Gitpodの設定をdevコンテナに移植して、コントリビューターが開始しやすいようにしました。しかし私は、localhostを使い続けます。

技術評価
Gitpod
M1 MacBook Pro

継続的インテグレーション

Bencherはオープンソースです。 現代のオープンソースプロジェクトとして、GitHub上に存在しなければならないようなものです。 継続的インテグレーション (CI) の最小抵抗経路はGitHub Actionsです。 年々、YAMLベースのCI DSLが嫌いになってきました。 それぞれ独自の癖があり、GitHubのような大企業の場合、❌ アイコンの代わりに ⚠️ アイコンが表示される問題が何年も放置されることもあります

これが動機となってDaggerを試してみました。当時、Daggerはこの難解な言語であるCUEを介してのみ使用できました。私は試しました。 本当に試しました。週末丸々使って。 もしかしたら当時ChatGPTが存在していたら、なんとか通り抜けられたかもしれません。 でも、私だけではありませんでした。 結局Daggerは、より理にかなったSDKのためにCUEのサポートを終了しました。 でもその時点では、私にとっては手遅れでした。

Daggerに敗れた私は、YAML CI DSLの運命を受け入れ、Bencherは現在GitHub Actionsを使用しています。 なんと、Bencher CLI GitHub Actionまで作りました。 世の中に見たい変化、いや問題を起こす人になれということですね。

テクノロジー評価
Dagger
GitHub Actions⚠️

結論

Bencherを構築する過程で、エンジニアリングの決定にはそれぞれのトレードオフがあることをたくさん学びました。 今なら異なる選択をするだろうと思うこともありますが、それは良いことです。 それは、プロジェクトを通していくつかの知識を得たことを意味しています。 全体として、現在のBencherの状況には非常に満足しています。 Bencherは私のノートのスケッチから、成長するユーザーベース、活気あるコミュニティ、支払い顧客を持つ本格的な製品になりました。 これからの3年間でどんな展開があるのか楽しみです!

スタックコンポーネント技術評価
フロントエンドフロントエンドライブラリYew
Seed
Sycamore
Elm
SolidJS
フロントエンド言語Rust
JavaScript
TypeScript
Typeshare
WASM
フロントエンドホスティングNetlify Builds
Netlify Deploys
バックエンドバックエンド言語Rust
HTTPサーバーフレームワークDropshot
データベースURL Query Params
git + HTML
SQLite
Litestream
LiteFS
データベースドライバーDiesel
バックエンドホスティングFly.io
AWS S3
CLICLIライブラリClap
APIクライアントProgenitor
開発開発者環境Gitpod
M1 MacBook Pro
継続的インテグレーションDagger
GitHub Actions⚠️

Bencher: 連続ベンチマーキング

🐰 Bencher

Bencherは、連続ベンチマーキングツールのスイートです。 パフォーマンスの後退があなたのユーザーに影響を与えたことはありますか? Bencherなら、それが起こるのを防げた可能性があります。 Bencherは、パフォーマンスの低下を_productionに到達する_前に検出し、防止することを可能にします。

  • 実行: お気に入りのベンチマーキングツールを使用してベンチマークをローカルまたはCIで実行します。 bencher CLIは単にあなたの既存のベンチマークハーネスをラップし、その結果を保存します。
  • 追跡: ベンチマークの結果を時間と共に追跡します。ソースブランチ、テストベッド、測定基準に基づいてBencherのWebコンソールを使用して結果を監視、クエリ、グラフ化します。
  • キャッチ: CIでパフォーマンスの後退をキャッチします。Bencherは最先端のカスタマイズ可能な分析を使用して、パフォーマンスの後退がProductionに到達する前にそれを検出します。

機能の後退を防ぐためにユニットテストがCIで実行されるのと同じ理由で、Bencherを使用してCIでベンチマークを実行してパフォーマンスの後退を防ぐべきです。パフォーマンスのバグはバグです!

CIでパフォーマンスの回帰を捉えるのを開始してください - Bencher Cloudを無料で試す

🤖 このドキュメントは OpenAI GPT-4 によって自動的に生成されました。 正確ではない可能性があり、間違いが含まれている可能性があります。 エラーを見つけた場合は、GitHub で問題を開いてください。.