1733567404
2024-12-06 12:28:00
テストの遅さはどのプロジェクトにとっても足かせになりますが、大規模に運営されているエンタープライズレベルのプロジェクトでは特に苦痛となる可能性があります。これにより、CI の遅延、デプロイメント時間の遅延、そしてまったく退屈な開発者エクスペリエンスが発生する可能性があります。 Evil Martians は最近、クライアントが CI パイプラインを最適化し、コードをより迅速にテストおよび変更し、最終的には 5倍速い 結果!この投稿では、それを実現するためのいくつかのテクニックとツールについて説明します。おそらく、あなたも同じことをしたいと思うでしょう。
まず、クライアントの背景について説明します。 パワーホームリフォーム (「POWER」) は、窓、サイディング、屋根材、ドア、屋根裏の断熱材、太陽光発電屋根パネルなどの外装改修を専門とする国内最大の住宅外装リフォーム業者です。社内にも社外にも、この会社が高い基準を持っていることは明らかなので、私たちはその基準を満たすよう努めました。
技術的に言えば、このプロジェクトは Ruby on Rails 上に構築されており、コンポーネントベースのアーキテクチャ設計 (別名) に従っています。 モジュラーモノリス)、数十のコンポーネントを備えています。
最適化の鳥瞰図
このパフォーマンス向上を達成するために、次の組み合わせを使用しました。 Rスペック ハッキング、プロファイリングの習熟度(たとえば、 スタックプロフ)、そしてもちろん、私たち自身の テストプロフ ツールキット。
まず、Stackprof を使用してテスト スイート全体の予備検査を実施しました。これにより、ファクトリ、ロギング、API 呼び出しなど、改善すべき一般的な事項を特定することができました。次に、全体的な最適化を行った後、次のように切り替えました。 地元 テストの最適化。たとえば、特定のテスト ファイルの分析とリファクタリング。
イリーナ・ナザロワ Evil Martians の CEO
全体として、私たちは 3 つの PR を提出 (およびマージ) し、それぞれが異なるレベルでテスト スイートの改善をもたらしました。この投稿では、これら 3 つの最適化マイルストーンについて説明します。
テスト最適化の考え方の 3 つの柱
話は以上ですが、深くなりすぎる前に少し待ってみましょう。テストを最適化する前に、大幅な改善を実現するために必要な考え方を理解することが重要です。この考え方は次の 3 つの柱に基づいています。
初め、 最適化すべき最も価値のあるものを探す。たとえば、すべてのテストで使用されるユーザー ファクトリを最適化すると、単一のテストを最適化するよりもはるかに多くの価値が得られます。 monthly_report_spec.rb ファイル (変更または使用されることはほとんどありません)。
2番、 簡単に実現できる成果を見つけ、過剰な最適化を行わない。ある方法に行き詰まりがちで、テストのパフォーマンスを 1 秒向上させるのに 4 時間かかる場合は、ここでの優先順位を再考する必要があります。
三番目、 タイミングを計る また、最適化の前後でタイミングを常に比較します。驚くべき増加が起こる可能性がありますが、これは私たちが実際に望んでいることではありません。
状況の理解
まず、明確な概要を把握し、ボトルネックを特定するために、さまざまな種類の指標を調べる必要がありました。簡単に言うと、これらのメトリクスは、合計テスト時間、コールスタック内の最も遅いメソッド、および最も使用されている (そして最も遅い) ファクトリです。
ツールのセットアップ
まず、Stackprof をインストールして構成し、 テストプロフ。 Stackprof は Ruby のコールスタック プロファイラーであり、TestProf はテストのプロファイリングと最適化のための高度なツールボックスです (これも Evil Martians によって作成されました!)。
覚えておいてください: 測定値を記録しておく必要があります。私たちの場合、テスト スイートの開始時の実行時間は次のとおりでした。 53分。この数字をどこまで減らすことができると思いますか?推測して答えを見つけて読み続けてください (警告: 驚かれるかもしれません)!
Stackprof 経由で収集されたコール スタックを見て、「なぜこんなに遅いのか」の概要を理解しましょう。ちなみに、これは、遅いテスト スイートを扱うときに最初に行うべきことです。
コールスタックから学ぶ
アプリケーションのコールスタックを読み取る最良の方法は、コールスタックを フレームグラフ。そのために、次の結果をロードできます。 TEST_STACK_PROF=1 SAMPLE=1000 bin/rspec に命令する スピードスコープ、Stackprof および他のプロファイラー (つまり、Ruby のものだけではありません) をサポートするフレームグラフ ビューアーです。
実際にフレームグラフを習得するには時間がかかりますが、Speedscope の「サンドイッチ」ビューを使用すると、すぐにヒントが得られます。私たちのケースでは、いくつかの不審な Active Record コールバックを特定しました。
History
NitroSearch::Elastic::Indexing
コールスタックフレームグラフの #auto_add_history
また、多くの低レベルの Active Record 呼び出しも確認できます (#exec_prepare、など)および工場関連の呼び出し(FactoryBot#create)。 TestProf を使用している工場を詳しく見てみましょう FactoryProf プロファイラー。
FactoryProf で工場を分析する
FactoryProf を使用すると、どのファクトリーが最も使用され、時間がかかっているかを確認できます (この場合、 project そして estimate 工場がテスト時間に最も貢献しました)。
工場向けの非常に便利なフレームグラフのようなプロファイラーもあります (FPROF=flamegraph bin/rspec)強調表示できる 工場のカスケード:
最適化前のFactoryFlame
上の画像では、カスケードの 1 つがわかります。 create(:candidate_interview)、多数の依存レコードが作成されます。
また、これらのカスケードは、 project そして estimate 工場。
このプロファイリング情報を利用して、最適化の適用を開始しました。 パッチ 一つ一つ。
PR #1: 全体的な最適化
いくつかの Active Record コールバック (例: History#auto_add_history) はテスト実行中頻繁に呼び出されていました。それでも、対応する 副作用 すべてのテストの約 1% でのみ必要となります。したがって、テスト時間を短縮するために、テストではデフォルトでそれらを無効にし、必要な場合にのみ有効にすることができます。
そのために、私たちは テスト容易性 パターン: 特別な要素を追加する Testing テストでのコールバックの呼び出しを制御する機能のモジュール:
module Testing
class self
def fake? = @fake
def fake! = @fake = true
def real! = @fake = false
end
def auto_add_history
return if History::Testing.fake?
end
end
ここでは、人気のあるものを追跡します Sidekiq::Testing 多くの Ruby 開発者にとって馴染みのあるインターフェイスです。さて、私たちの中では、 rails_helper.rb 次のことができます。
History::Testing.fake!
RSpec.configure do |config|
config.before(:each) do |example|
if example.metadata[:auto_history] == true
History::Testing.real!
else
History::Testing.fake!
end
end
end
ここでは、RSpec のタグ付け機能を使用して、条件に応じてコールバックの動作を切り替えます。自動履歴機能に依存する機能をテストする必要がある場合は、常に :auto_history 対応するテスト例へ:
describe History do
let(:fp) { create(:finance_project) }
it "automatically record history on creation", :auto_history do
h = fp.histories.last
expect(h.owner_changes["roofing_sold"]).to eq([nil, true])
expect(h.activity).to eq("Created the Finance Project")
end
end
同様のテクニックを適用した後、 NitroSearch::Elastic::Indexing モジュールを使用して、合計テスト時間を短縮します 53分から33分に短縮。そしてそれはほんの始まりにすぎません!
PR #2: 工場の最適化
2つの工場が見つかりました。 project そして estimate、(FactoryProf レポートによると) 最も遅かったです。私たちは彼らのソース コードを調べたところ、共通の分母があることがわかりました。 home そして owner デフォルトの関連付け:
factory :estimate do
home
association :owner, factory: :user
end
このようなものを見た場合はどうすればよいですか?関連付けをコメントアウトして、失敗したテストの数を確認してください。私たちの場合、その数はそれほど大きくなかったので (数千のテストのうち約 50)、関連付けの作成を対応する特性に移動し、必要に応じてそれらを使用することにしました。
factory :estimate do
trait :with_home do
home
end
trait :with_owner do
association :owner, factory: :user
end
end
不要な関連付けを削除した後のファクトリのフレームグラフは次のとおりです。スタックは大幅に減少しました (全体のテスト時間はさらに約 15% 短縮されました)。
最適化後の FactoryFlame
治具化 nitro_user
また、ほぼすべてのテストでデフォルトのユーザーが作成されていることにも気付きました (User.current)。テストで見つかった一般的なイディオムは次のとおりです。 User.current || create(:nitro_user)。ユーザーの作成には他のファクトリーの作成も含まれるため、常に再作成するオーバーヘッドがかなり顕著でした。
TestProf を使用して、デフォルトのユーザー作成をフィクスチャに移動することにしました。 任意のフィクスチャ 道具:
require "test_prof/recipes/rspec/any_fixture"
using TestProf::AnyFixture::DSL
RSpec.shared_context "fixture:nitro_user" do
let(:nitro_user) { fixture(:nitro_user) }
end
RSpec.configure do |config|
config.before(:suite) do
fixture(:nitro_user) { FactoryBot.create(:nitro_user) }
end
config.include_context "fixture:nitro_user"
end
フィクスチャの導入後、明示的な作成をすべて削除しました。 User.current パフォーマンスをさらに向上させるためにテストで使用します。
データベースの状態とそのクリーンさについて
私たちが取り組んでいる間、 nitro_user フィクスチャの導入では、トランザクション フィクスチャが使用されていたとしても、テストの実行後にデータベースが完全に元の状態に保たれていないことがわかりました。
簡単に検索するといくつか出てきました before(:all) テストでの使用法。これを次のように置き換えました。 before_all– 元の RSpec フックのトランザクション バージョン。
また、データベースの状態をリセットするための便利な (そして条件付き) フックも導入しました。これにより、まだ修正されていない他のブランチで作業している開発者は、グローバルな状態の問題を簡単に修正できます。
RSpec.configure do |config|
config.prepend_before(:suite) do
ActiveRecord::Tasks::DatabaseTasks.truncate_all if ENV["CLEAN_DB"]
end
end
また、サードパーティのデータベース クリーナーを使用せずに、プレーンな Active Record を使用していることにも注意してください。
最終的に、2 回目の PR により、テスト時間はさらに短縮されました。 21分 (念のために言っておきますが、ステップ 1 の後は 33 分でしたが、当初は 53 分でした!)。
しかし、そこで止まらず、さらに詳細な最適化を続けました。
PR #3: 特定のテスト ファイルの最適化
TestProf のキラー機能の 1 つをすでに紹介しました。 before_all、前のステップでは、実際の 力 その兄弟から来ています—let_it_be。の let_it_be ヘルパーは以下と同様に機能します let! (使用の観点から) ただし、パフォーマンスに大きな違いがあります。データは一度作成され、ファイル内 (または RSpec コンテキスト内) のすべてのテストで再利用されます。それで、私たちは見ることができます let_it_be として 地元の備品。
への移行 let_it_be ファイルごとに実行する必要があります。この移行をどこから開始するか、最初にどのファイルを選択するかを判断するにはどうすればよいでしょうか?
そのために、TestProf は、と呼ばれる特定のプロファイラーを提供します。 Rスペックディセクト。テストにかかる時間を示します。 let/let! そして before(:each) 宣言:
Top 10 slowest suites (by `let` time):
HiringRequisition (./spec/models/hiring_requisition_spec.rb:5) – 00:04.126 (378 / 59) of 00:05.019 (82.21%)
...
以下のレポート例は、実行時間の約半分が spec/models/hiring_requisition_spec.rb ファイルが占有されている let 表現。どのようにリファクタリングしたかを見てみましょう。
…しかし、リファクタリングを行う前に、FactoryProf を介してファクトリ プロファイルをキャプチャすることにより、テスト ファイルの現在の状態を記録しました。私たちが取得した最初の統計は次のとおりです。
$ FPROF=1 rspec spec/models/hiring_requisition_spec.rb
Total time: 00:03.368 (out of 00:07.064)
name total top-level total time
user 237 109 3.8929s
office_location 86 41 2.3305s
...
さて、この情報が何を示しているかについては深く議論するつもりはありませんが、テストをどのようにリファクタリングしたかを見てみましょう。
- let(:office_location) { create(:office_location, territory: create(:territory)) }
+ let_it_be(:default_user) { create_default(:user) }
+ let_it_be(:office_location) { create(:office_location, territory: create(:territory)) }
何が変わったのでしょうか?まずは交換しました let と let_it_be 確実に office_location レコードはファイル全体に対して 1 回だけ作成されます。さらに、 create_default 最適化これにより、必要なすべての工場で生成されたオブジェクトでユーザー レコードを再利用できるようになりました。 user 協会。
変更の効果を測定するために、FactoryProf を再度実行し、次の結果が得られました。
FPROF=1 rspec spec/models/hiring_requisition_spec.rb
Total time: 00:01.039 (out of 00:04.341)
name total top-level total time
office_location 11 11 0.1609s
...
ご覧のとおり、合計時間は 3 秒 (約 50%) 減少し、工場出荷時の時間は 3 秒から 1 秒に減少しました。これは、単一のファイル変更としては悪くありません。
私たちは適用を続けました let_it_be / create_default 他のファイルにリファクタリングし、数百のファイルのうち数十のファイルにアプローチした後、満足のいく結果に達しました。 11分。
要約すると、3 つの最適化ステップにより、次のようなテストの高速化が可能になりました。 ~5回 (53 分から 11 分) 作業時間はわずか 2 週間です。いかがでしょうか?遅いテストについてサポートが必要な場合は、お気軽にお問い合わせください。
#テストを #倍高速化したツールとテクニック #Martian #ChroniclesEvil #Martians #のチーム #ブログ