test-suite-speedup は何をしますか?
概要
遅いスイートは、二つの間違った手を誘います。それは、最も遅く見えるファイルを手動で書き換えることと、ローカルのストップウォッチの計測値をそのまま改善結果として報告することです。どちらも本質を外しています。時間は通常、いくつかの構造的な場所——繰り返される重いセットアップ、並列実行できるのに直列実行されているファイル、一つのCIジョブで二つのジョブ分の仕事をしている箇所——に集中しており、他の処理が同時に動いているときに取得したローカルタイミングはノイズでしかありません。このスキルは、まずCI上でのコストをランク付けし、最も多くのファイルに影響を与えるレバーを選択し、CIのbefore/after比較表を合格判定テストとして扱います。
コア原則: 成果物は、同じCIパス上の実測されたCI比較であり、ローカルタイミングやグリーンチェックではありません。
参照マップ — 各手順を実行するときにのみ読み込んでください:
| ファイル | 内容 |
|---|---|
references/ci-measurement.md | 複数実行にわたる手順のランク付け、pytest -vログからのファイル別ランキング、ランナータイプのノイズ、before/afterレポート、期間シャードの更新 |
references/pytest-slow-test-causes.md | 遅いpytestファイルをフェーズとシグネチャで分類する方法;エスケープされたsleep;到達不能なプロダクトコードの待機;スイート全体のハーミティックパッチ(クラウドクライアント、ライブラリのアイドルsleep)とそのガードテスト;ワークストリームの単一プロセスプロファイリング;ランタイムcreate_allの絞り込み |
references/pytest-shared-setup.md | ホットなファクトリの共有可能な部分をその呼び出し元から見積もる方法;高価なpytestオブジェクトを安全に共有する方法:事前事後の証明、最小限の変更で配線、変更選択との相互作用 |
references/vitest-isolation.md | JS/vitestスイート:待機型と計算型のファイル分類、CI上でのみ遅いファイル、インポートコストが支配的なケース、モジュール別インポートプロファイリング、バレルライブラリの事前バンドル、分離プールと共有プール、リークプローブ、シャッフルチェック |
references/parallel-runner.md | 並列安全性プローブ、直列パーティション、ランナー実装ルール、CIマトリックス分割 |
references/ci-redundant-runs.md | 発生すべきでない実行:ツリーによるバックマージ/プッシュ履歴の分類、同一ツリーの結果の再利用、より新しい実実行による差し替え(Actions API、メッセージ解析ではない)、ジョブ別の並行性、if:ゲートをコードとしてテストする方法、コミットテキストからスキップマーカーを除外する方法 |
scripts/ci_leg_series.sh | 指定日以降の全プッシュ実行におけるレグ別テストステップ秒数(TSV)— ステップ変更を特定するための時系列データ;バックグラウンドで実行 |
scripts/ci_durations_diff.py | 各ステップ変更の前後における実行ペアの比較用:収集カウント、pytestサマリー、新規または増加した--durationsエントリ |
scripts/boto_call_counter.py | テストごとの実際のbotocore呼び出しをカウントするpytestプラグイン — cProfileがbotocore配下でSSL/ソケット待機を示している場合に実行 |
使用するタイミング
- テストスイート、またはCIワークフローのテストジョブが遅すぎて、ユーザーが高速化を望んでいる場合。
- 以前の高速化の後、ユーザーが「他に何が遅いのか?」と尋ねた場合。
- 計測目標なしのコピー/重複排除のためのクリーンアップには使用しないでください — 再利用のためのテスト再構築は高速化ではありません(
test-suite-consolidationを参照)。
プロセス
ステップ 1: 同じCIパスでベースラインを測定
ベースラインはCIから取得し、ローカル実行からは決して取得しないでください。ベースラインはafter実行と同じCIパスを実行する必要があります:PRのベースがパスを選択する場合(例えばprodは完全なスイートをマトリックスレグに分割して実行し、devは変更選択により1レグのみ実行する場合)、同じベースに対するPRの最新の成功実行を取得し、時間を比較する前にpassed/skipped/deselectedカウントが一致することを確認してください。その実行からステップごとのタイミングを取得し、1つの大きなステップをランナーのセクションバナーで分割し、ワークフローファイルのコメントを読んでください — メンテナーは多くの場合、意図した修正をすでに書き留めています。次に、単一の実行ではなく複数回の実行にわたってステップをランク付けし、ランナーの実際のvCPU数と設定されたワーカー数を比較してください。コマンド → references/ci-measurement.md。
何か提案する前に、既存の高速化に関するIssueやPRを検索してください。クローズされたものや捨てられた実験PRも含めてください(gh pr list --state all --search "xdist"、--search "test_durations"):それらは測定され、却下された代替案と、それらが却下された条件を記録しています — セットアップコストが支配的だったときに否定されたレバーが、CPUバウンドのテストが増えれば効果を発揮するかもしれません。
完了条件:マッチするCIパス上のベースライン実行IDが指定され、そのテストカウントが記録され、数秒を超えるすべてのステップについて、複数実行にわたる中央値とp90が取得されていること。
ステップ 2: レバーを選択する前に期待される効果を見積もる
効果 ≈ (そのバッチがカバーするスイート時間の割合) × (そのサブセットでのファイルあたりの節約量)。スイートの2%を占めるバッチでは、ファイルあたり60%削減しても全体で1~2%程度の効果 — CIノイズ以下であり、ユーザーには「ほとんど変わらなかった」と読まれます。数百のファイルを一度に動かすレバー(共有テストヘルパー、プール分類器、ランナータイプ)を優先し、ファイル単位の移行は長いテールとして提示してください。期待される効果は最初に明確に述べてください。
完了条件:各候補レバーについて、その入力値とともに書面での見積もりが作成され、CIノイズ以下の見積もりはそのようにラベル付けされていること。
ステップ 3: ファイルをランク付けし、各トップファイルを分類
pytest -vをログ出力するフルスイートのCI実行から、スイート全体にわたってファイルをランク付けします(変更選択実行はすべてのテストをカバーしません)。まず、既知の答えがある実行でランキングを検証してください — 修正前の実行は、修正したファイルを最初にランク付けする必要があります。次に、修正を提案する前に、各トップファイルをフェーズ(setup / call / teardown)と形状で分類します:
- pytest →
references/pytest-slow-test-causes.md - vitest / JS →
references/vitest-isolation.md
ファイルごとのローカルタイミングは形状の確認のみに使用します:各ファイルをバックグラウンドで実行し、なぜ遅いのか(ケースごとのプロセス生成、実際のsleep — テストの入力文字列内のsleepは遅延ではありません)を調べ、別のスイートが実行されている間に取得した数値を決して報告しないでください。各ファイルについて、CPU時間(user+sys)を経過時間とともに記録してください。経過時間に対してCPU使用率が低いファイルは、実時間(レートリミッター、バックオフ、ポーリング)で待機しており、修正はテスト側の疑似時間です。より多くのワーカーやインポート削減は役に立ちません。CI上では遅いが、CIのワーカー数と環境でローカルでは速いファイルは、CIのOSとランタイムメジャーバージョン(CIのvCPUに制限されたnode:<major>コンテナ)での再現が必要であり、推測は避けてください。まず、そのファイルの下でCIログがすでに出力している遅いテスト行を読んでください → references/vitest-isolation.md。pytestの場合、ファイルごとのCI/ローカル比をスイートの通常の比と比較してください。その比が数倍以上高いファイルはCIのみのコストですが、その原因は、PR自身のCI実行がそのファイルを動かすまでの仮説です:LinuxコンテナのA/Bテスト(例えばsqlite synchronous=OFFでスケジューラシミュレーションを約35%削減)がGitHubランナーに引き継がれず、同じテストが--durationsを維持した場合。そのようなレバーは、最初のPR実行で測定可能なターゲットと一緒にのみ出荷してください → references/pytest-slow-test-causes.md。
遅いスイートの原因は通常、複合的なものです:直列ワーカー、待機ファイル、そして重いファイルごとのインポート。レバーを一つの表でランク付けしてください:原因 · エビデンス(測定 vs 推定) · CI秒数 · 修正 · 期待される節約量。
--durationsの名前から推測された原因(「おそらくDAGを再パースしている」)は、2~3の遅いテストでcProfile cumtimeを実行して示されるまでの仮説です。計画やIssueに原因を記載する前にプロファイリングを行い、プロファイルが異なる結果を示した場合はレポートでその原因を撤回してください。
完了条件:ランク上位の各ファイルが分類テーブルからの原因を持つか、明示的に「調査が必要」とマークされていること。
ステップ 4: 構造的な修正を以下の順序で選択
- ホットなファイル内部で繰り返される高価なセットアップを削除する(呼び出しごとに同じ重いオブジェクト — DAGバッグ、アプリ、スキーマ — を再構築するテスト)。キャッシュを考案する前に、リポジトリの既存のセッションフィクスチャを再利用し、まず共有が安全であることを証明し、次にベースブランチとフィーチャーブランチのファイルコピーが同じプロダクションミュータントで失敗することを証明してください。モジュール/セッションキャッシュは、1つのpytestプロセス内でのみ重複排除を行います:シャード化されたCIでは、節約量をカウントする前に、各コンシューマーがどのレグで実行されるかを確認してください。別のテストがパラメータ化された1ケースの高価な結果のみを再読み取りする場合、キャッシュする代わりにアサーションをそのケースに折り込み(シャード非依存)、移動したアサーションの1つを壊して、それがまだ実行されることを証明してください →
references/pytest-shared-setup.md。 - CIエントリポイント内で、独立したテストファイルを並列実行する。
- CIジョブをマトリックスレグに分割して、半分を並行して実行する。
- その後でのみ、最も遅いファイルのケースごとのプロセス生成を書き換える — 広範囲に影響するが、効果は控えめ。提案はしますが、デフォルトにはしません。
コストの大部分が発生する必要のない実行全体(ボットのバックマージ、別のブランチですでにテスト済みの同一ツリー、間もなく追い越されるプッシュ)である場合、そのレバーは上記のすべてに優先します:実行履歴をツリーで分類し、実行全体をスキップまたは再利用します → references/ci-redundant-runs.md。
完了条件:選択されたレバーがステップ2の見積もりとともに名前が挙げられ、スキップされた高次レバーには1行の理由が記載されていること。
ステップ 5: 並列安全性を証明し、ランナー変更を実装
すべてのファイルを1つの使い捨て状態ディレクトリで並列実行し、それぞれを独自の出力ファイルと終了コードファイルに出力し、失敗をリストアップし、各失敗を単独で再実行して、並行バグと環境問題を分離します。ワーキングツリーを編集するファイルを、すでに持っているマーカーでキー指定された直列グループに分割します。次にランナーを実装し、選択した場合はCIマトリックス分割を実装します。手順とルール → references/parallel-runner.md。
完了条件:すべての並列プローブ失敗が分類され(並行性 / 環境 / 既存)、直列グループが手動リストではなくgrepから導出され、ジョブ名変更の前に必要なステータスチェックが確認されていること。
ステップ 6: CIで検証し、ローカル実行に依存しない
変更したテストコードは、プロダクトコードと同様にリポジトリ独自のコーディングガイドラインに従ってください — パススルーラッパーなし、重複の抽出、新しいヘルパーのためのミューテーションプルーフ。ローカルでは、変更したファイルとlintのみを実行し、プッシュし、ターンを保持する代わりにバックグラウンドでPRのチェックを監視してください。検証ノートを書く前にPRのレビュースレッドを読んでください:ボットレビュアーはプッシュ後数分以内に投稿します。
テストがローカル環境でのみ失敗する場合(オプショナル依存関係のvenv、ローカルのフェイクサービス)、変更に触れる前に、未変更のベースで使い捨てのデタッチドワークツリーでそのテストを1つ実行してください:同じ失敗がそこにあれば、それは環境問題であり、リグレッションではありません。コミットのNot-tested:トレーラーとPR本文にその旨を記載し、その依存関係をインストールするCIジョブ(その生ログ:サマリー行とgrep -c FAILED = 0)でクローズし、ローカル実行ではクローズしないでください。
ローカルのフルスイートのベース対ブランチ比較は、「新しい失敗はない」を再現された範囲でのみサポートします:ブランチのみの失敗が1つのenvエラーを共有する場合は、「ブランチのみのN個の失敗、すべて;はベースで再現されました」と述べ、残りはCI実行でクローズさせてください — 「新しい失敗は0」は誇張であり、それが到達したすべての表面で訂正する必要があります。
スイート全体のスタブをマージしても安全と呼ぶ前に、結果がどのように変わる可能性があるかを列挙し、それぞれにプローブを用意してください:(1) それをバイパスするモジュールレベルのバインド(git grep -nE "from <mod> import <fn>" — インポート時にバインドされた名前は本物の関数を保持します);(2) スタブの例外をキャッチしないプロダクション呼び出し元、およびテストがそれらをパッチされずに実行するかどうか(before実行からのテストごとの呼び出しカウントがそれを示します);(3) 本物のパスをアサートするテスト — これらはセッションスタブの内部でネストして、自分自身でパッチする必要があります;(4) パッチされた名前を移動させる進行中のライブラリアップグレード(pytest-slow-test-causes.mdのバージョン耐性ルールを参照)。それぞれを確認済みとして報告するか、確認するための推論として報告してください。
ボットレビュアーは短いポーリングよりも長く実行されることがあります:Codexコネクタはレビュー中にPRに👀をマークし、所見なしで終了すると👍に変更しますが、20分を超えることもあります。待機に上限を設け、マージブリーフィングの前に一度リアクションとスレッドを再読み込みし、ポーリングを延長しないでください。立っている👍が最新のプッシュに属するかどうかを判断するには、そのタイムスタンプ(gh api graphql → pullRequest{reactions(first:20){nodes{content createdAt user{login}}}})をプッシュ時間と比較してください — プッシュより古い👍は前回のラウンドの判定です。
pull_request実行は、その時点のベースチップへのヘッドのマージをテストし、PR自身のベースコミットはテストしません:ジョブログからベースshaを読み取り(HEAD is now at <m> Merge <head> into <base>)、そのベースコミットのプッシュ実行と比較してください。
ファイルごとのメトリックは、ベースラインとPR実行の間にスイートが成長した場合のafter測定です:レグ全体の秒数には、その後他のコミットのテストが混ざります(最初にcollected N itemsカウントを比較してください)。両方の実行からのタッチされたファイルのCI秒数を結果として報告し、レグ時間はコンテキストとして報告してください。ベースをPRにマージした後は、以前のバンドではなく、同じ期間の他のPRの実行から比較バンドを再構築してください:ベース自身のスイートは成長し続けるため、古いバンドは実際の改善をリグレッションに見せかける可能性があります。バンド自体が動いた場合はレポートにその旨を記載してください。
完了条件:ベースラインと同じパス上のブランチのCI実行が完了し、すべての未解決のレビュー所見に回答されていること。
ステップ 7: before/afterレポートを作成
両方の実行IDを含む表:ジョブ壁時間(現在 = 最も遅いレグ)、以前遅かったステップ、および変更されていない半分、さらにレグごとのテストサマリー行。複数のPRにまたがる改善の場合は、一つの表:ベースライン → マージされた各ステップ → 現在、各PRの貢献割合を含む。PRが実行しなかったCIパスは未検証として名前を挙げ、各実行は1回であること(ランナー分散は未測定)と、ステップ変更によって追加されたテストが含まれていることを述べてください。パスを説明するために、ワークフローコメントから直接引用したCIパス図を含めてください。報告書に、プッシュされたPRが選択ベースで実行した場合の節約量と、トリガーパスで実行した場合の節約量の両方を記載し、選択が外部からは見えないことを説明してください → references/ci-measurement.md のbefore/afterレポートセクションを参照。
完了条件:レポートがPR本文の上部に記載されていること(スレッドやコードレビューコメントのコメントではありません)。