CTS-KB

CI テスト 29 分 → 5.6 分 — 実測が「有力仮説」を殺し、退行前より速くなった日【番外編】

⏱ 約 10 分で読めます
#クロスファミリーレビュー#Codex#Claude Code#CI/CD#パフォーマンス#PostgreSQL#Marten#テスト

📖 はじめに — エンジンは「事件」のためではなく「平時」のためにある

終章で検証エンジンに計測層がはまり、シリーズは完結した。第II部の 6 回は「エンジンをどう組んだか」の記録だが、組んだ仕組みの真価は、派手な事件ではなくごく平凡な 1 タスクが日常運用の中をどう流れるかに出る。

シリーズを書き終えた直後、そのサンプルにうってつけのタスクが流れてきた。CI の .NET テストジョブが数日で倍増するという、どこのチームにもある性能退行だ。本稿はこの 1 件が、起票 → 文書レビュー → 実測 → オーナー承認 → Codex 実装 → Claude 検証 → CI 実測という検証エンジンのフルコースを同日中に通過した記録である。結果だけ先に書くと:

時点dotnet:test 全体うち基盤統合テスト(Infrastructure.Tests)
退行前(6/28)14.0 分11 分 31 秒(1,073 件)
跳ね始め(7/2)20.7 分
退行後(7/5)29.0 分25 分 00 秒(1,086 件)
是正後(7/6)5.6 分44 秒(1,091 件・約 34 倍)

「29 分を退行前の 14 分に戻す」タスクが、なぜ 5.6 分で着地したのか。そこにこのシリーズらしい学びが 1 つ埋まっている。

🚨 事件 — 増分の 100% が 1 プロジェクト、テスト数はほぼ不変

観測から入る。MR パイプラインの実測(glab api で取得)では、dotnet:test は 6/28 の 14.0 分から 7/5 の 29.0 分へ、数営業日で倍増していた。切り分けの算術はシンプルだ。

  • 増分の 100% が Infrastructure.Tests(統合テスト)1 プロジェクト。他 18 テストプロジェクトはすべて秒オーダーで横ばい
  • テスト数はほぼ不変(1,073 → 1,086・+13 件)。つまり「テストが増えた」のではなく、1 テストあたりのコストが 0.64 秒 → 1.38 秒(約 2.2 倍)に膨張した
  • 時系列は、Marten マルチテナント基盤の刷新 — テナント別リストパーティション化と、スキーマを前倒しで実体化する drop → materialize の導入コミット(7/2 夜)— と正確に一致する

決め手は兄弟プロジェクトとの差分だった。9 個ある PostgreSQL fixture のうち、per-test クリーンアップで Store.Advanced.Clean.DeleteAllDocumentsAsync()(全ドキュメント型 × 全テナントパーティションを TRUNCATE)を呼んでいるのは Infrastructure.Tests だけ。他 8 プロジェクトは対象型限定の DeleteWhere<T> 方式で、同じパーティション化の下でも退行していない。パーティション表が前倒し materialize で数百枚実体化された結果、この「無差別全 TRUNCATE を毎テスト」が激重化した — という機序が状況証拠から浮かぶ。

副次影響も出ていた。当日の別 MR は 37 分実行の末に No space left on device で失敗(テストは全 PASS)。実行長期化とパーティション表の増加がランナーのディスクを食い切り始めており、性能退行はディスク枯渇失敗とも地続きだった。放置の選択肢はない。

🔪 有力仮説が死ぬまで — 半日

ここからが検証エンジンの出番だ。是正案は 2 候補あった。

  • 案 A: Marten のドキュメント親表への単発 raw TRUNCATE(物理表を直接落とすのでテナント跨ぎで確実)
  • 案 B: 兄弟プロジェクトと同型の、対象限定 DELETE 方式

起票時点の本命は A だった。「TRUNCATE を 1 文にまとめれば速いはず」— もっともらしく聞こえる。この「有力仮説」は、2 段階で殺された。

第 1 撃: design-reviewer が機序の誤認を指摘する

文書レビューで design-reviewer が MUST を打った — TRUNCATE は空テーブルでも relation ごとに relfilenode 置換・カタログ更新のコストを払う。単発文にまとめて減るのは文数・ロック・コミット回数だけで、数百の子パーティション分の per-relation 処理は消えない。「A が最安」という前提ランク付け自体が PostgreSQL の機序誤認であり、A/B は実測で決める二候補に格下げされた。

このプロジェクトには「有名 OSS の内部挙動は推測で語らず、ソースと実測で確定する」という運用 memory があり、決定記録にも推測実装の禁止(是正前に per-test 実測で機序を確定する)が明文化された。

第 2 撃: Phase 1 実測が 770 倍差で決着をつける

materialize 済みスキーマ(ドキュメント親表 63・子パーティション 225・ほぼ空)で、クリーンアップ 1 回あたりのコストをサーバーサイド実測(N=50 平均)した。

#アプローチper-cleanup 実測
0カタログ問い合わせのみ(参考)0.202 ms
1現行: TRUNCATE 105 表 CASCADE623 ms
2案 A: TRUNCATE 63 親表 CASCADE(単発化)640 ms — 改善ゼロ
3案 B: DELETE FROM 63 親表0.81 ms — 現行比約 770 倍

あわせて Marten のソース(V8.37.0 の DocumentCleaner)を確認すると、現行実装は既に全表を 1 コマンドにバッチして TRUNCATE していた。「単発化すれば速くなる」という A の前提は、そもそも存在しない伸びしろだったのだ。実測は仮説を殺すだけでなく、算術の裏も取れた — 623 ms × 約 1,090 テスト ≒ 11 分。観測された退行増分とぴったり一致する。

近傍がほぼ空のテーブルなら、行 DELETE は relation churn を伴わずニアゼロで済む。兄弟 8 プロジェクトが DELETE 系で退行していない事実とも整合し、B 確定・A 却下。採用形は「fixture 初期化時に mt_doc_ 親表を 1 回だけ introspect してキャッシュし、raw DELETE FROM を流す」方式(B-raw)に落ちた。型の列挙が不要で新型追加に自動追随し、raw DELETE は Marten のテナントフィルタを経由しないため多テナント残存の取りこぼしを構造的に回避できる。テナントプロビジョニングを支える制御表(mt_tenant_partitions など)は対象外 — 誤って落とすと fixture の初期化ごと静かに壊れる。

🛠️ 是正 — ハードゲートを越えて、Codex が実装する

ここまでが「決める」フェーズ。ここから先は、第II部で組んだ流れがそのまま回る。

🛠️ 是正 — ハードゲートを越えて、Codex が実装する

起票 +文書レビュー

Phase 1実測ベンチ

ハードゲート(オーナー承認)

Codex 実装(ルール 12)

Claude verifier3 者検証

CI 実測(AT 判定)

ハードゲート。A/B の選定はオーナー判断と定義してあったので、実測値の報告 → 承認(B-raw 採用・A 却下の確認)までは Phase 2 に着手しない。「フェーズごとに聞かずに進めてよい」という平時の自走運用の中でも、意思決定ポイントだけは構造的に止まる。

ルール 12 の自己適用ミスも、いつもの多層網が拾った。当初のタスクリストは本件を「自走モード(クロスファミリー除外類型)」と書いていたが、doc-reviewer がこれに MUST を打つ — テスト fixture のロジック改修は除外 3 類型(docs 等のみ / 1〜2 行の自明修正 / Codex 不可時 fallback)のどれにも文言上該当せず、TDD 委譲の例外規定と混同していた。決定記録を改め、Phase 2 実装は Codex 既定へ。実装編で組んだ codex exec パイプラインが run-id つきで実装し、introspect のクォート処理や制御表除外まで含む差分を返してきた。

性能是正には classic Red がない。挙動を変えない改修なので、TDD の赤 → 緑は作れない。代わりに test-writer が特性テストを先に置いた — 各テナントに書き込み → クリーンアップ → 全テナントで 0 件を、Marten API のクエリだけでなく物理表の生カウントで裏取りする回帰網だ。現行実装でも B-raw 後でも緑であること(= 挙動不変の安全ネット)を確認してから差し替える。

検証は verifier 3 者が全 PASS・差し戻し 0 回。フィルタなし全実行は 1,091/1,091 全緑・17 秒 — B-raw 前はクリーンアップだけで 623 ms × 1,091 ≒ 11 分が支配していたのだから、当然そうなる。レビュー指摘は終章で作ったばかりの finding 台帳に記帳され、計測層は稼働初週から平時のデータを溜め始めた。

📉 結果 — そして「退行前 = 健全」が崩れる

CI 実測(グリーンパイプライン・全 19 プロジェクト Failed 0):

  • dotnet:test 全体: 29.0 分 → 5.6 分(目標は 16 分以下だった)
  • Infrastructure.Tests: 25 分 → 44 秒(約 34 倍。目標は 13 分以下だった)
  • ディスク枯渇失敗の再発なし

ここで冒頭の問いに戻る。目標は「退行前水準(11.5 分+許容誤差)へ戻す」だった。なぜ退行前を桁で下回ったのか。

答えは、退行前の 11.5 分というベースライン自体が per-test TRUNCATE の隠れコストに支配されていたからだ。TRUNCATE は空表でも relation ごとのコストを払う。それを 1,000 回強のテストごとに実行していた「健全」時代のスイートは、実際のテスト実行 44 秒 + クリーンアップ約 11 分、という構造だったことになる。パーティション化はその隠れコストを 25 分へ悪化させただけで、原因の本体は退行のずっと前から鎮座していた。退行調査のつもりで始めた是正は、結果として元から存在した構造的コストの除去になった。

学びを 3 行に圧縮する。

  1. 「退行前 = 健全」と決めつけない。 戻す先のベースラインにもコスト構造の疑いをかける価値がある。今回それは約 15 倍分の埋蔵金だった
  2. 有力仮説ほど実測で殺す価値がある。 案 A は文書レビュー(機序の指摘)と実測(改善ゼロ)の二段で死んだ。どちらか片方だけなら、効果ゼロの是正を「完了」していた可能性がある
  3. 有名ライブラリの内部挙動はソースで確定する。 「Marten が毎回バラバラに TRUNCATE しているはず」という想像は、ソース 20 行の確認で消えた

🧾 まとめ — 検証エンジンの「平時の 1 日」

第II部の各回が答えたのは「なぜこの層が要るか」だった。本稿が示したのはその総合演習で、平凡な CI 退行 1 件が同日中に踏んだ装置は — 文書レビュー 2 ラウンド(機序誤認とルール適用ミスの検出)・実測ベンチ・オーナーのハードゲート・Codex 実装 → Claude 検証のクロスファミリー分業・特性テストによる挙動保証・verifier 証跡・finding 台帳への記帳 — と、シリーズで組んだほぼ全部だ。

どの装置も、この日のために特別に動員されたわけではない。全部が既定の通り道だった。エンジンを組む 17 本は長い道のりだったが、組み上がったエンジンの上では、770 倍と 34 倍のつく是正が「いつもの 1 日」として流れただけの話だ。検証エンジンの価値は、平常運転のまま高い水準の仕事が再現され続けることにある。人が張り付いて指示するのではなく、設計済みのループの上をタスクが自走するこの運用形態は、近年ループエンジニアリングと呼ばれるようになった考え方そのものだ。

📚 シリーズ記事(ステアリング駆動開発・実践編)

序章

  1. AI 駆動開発が積み上げる技術的負債

第I部: 技術的負債ドメイン別の実録(総点検ガイド + 8 ドメイン・全 9 回)

  1. リファクタリング総点検ガイド — 7 つの観点と進め方
  2. DDD/SOLID/BC 編 — god class 一掃と境界の機械ガード
  3. SAGA 編 — 新アーキテクチャ挑戦と再発ゲート
  4. Atomic Design リファクタ編 — 47 page 新規移植
  5. Storybook × a11y 実機編
  6. テスト品質・網羅性編
  7. Python ジョブ群編
  8. パフォーマンス編
  9. セキュリティ編

第II部: 検証エンジン(クロスファミリー検証・全 6 回)

  1. 総論 — マルチ LLM の 2 系統と見取り図
  2. 裏取り編 — +18.1pt 論文の検証
  3. 設計編 — finder/verifier 分業と逆順禁止
  4. TDD×ハーネス編 — テスト保護と三層ゲート
  5. 実装編 — Claude Code の中から Codex を動かす
  6. モデル戦略編 — ティア割当と「買うか組むか」

終章

  1. クロスファミリー検証の最終型 —「正しく作る」から「自分で正しさを測り直す」へ — 検証エンジンに「計測」の層をはめ、自己修正ループを閉じる

番外編

  1. CI テスト 29 分 → 5.6 分 — 検証エンジンの平時運用実録(本記事)
  2. GPT-5.6 sol 切替の当日実録 — もう一つのデフォルト追従 — CLI 更新が黙って替える finder と、当日中の再計測
  3. 遊休 90% の枠に仕事を振る — Codex 上流調査と verifier バッチ化 — 逆順禁止の境界を ADR で確定し、読む仕事を遊休枠へ移した 1 日
  4. ハーネスが単一プロジェクトを卒業する — プラグイン化と二層配布 — 4 プロジェクト展開への切り出しと、version + SHA ピンによる非強制追従
  5. 69分で5リリース — Opus 5 当日対応が暴いた共通ハーネスの死角 — 新モデル対応を起点に、配布・文書・CI の回帰を二消費者で検出した 69 分

関連記事