test-suite-speedup 是做什麼的?
概述
一個緩慢的套件會引誘兩種錯誤的處理方式:手動重寫看起來最慢的檔案,以及將本機的碼錶時間報告為改善的成果。這兩者都忽略了真正的問題。時間通常卡在少數幾個結構性位置 — 重複的重量級設置、應該可以平行執行但卻被序列化執行的檔案、一個 CI 工作做了兩個工作的份量 — 而在其他東西都在跑的同時取得的本地計時只是雜訊。這個技能首先在 CI 上對成本進行排名,選出能移動最多檔案的槓桿,並將 CI 前後對比表格視為驗收測試。
核心原則: 交付成果是在同一 CI 路徑上測量的 CI 比較結果,而不是本地計時或綠燈檢查通過。
參考地圖 — 僅在執行相關步驟時載入:
| 檔案 | 內容 |
|---|---|
references/ci-measurement.md | 跨多次執行的排名步驟、從 pytest -v 日誌進行的逐檔案排名、執行器類型雜訊、前後對比報告、持續時間分片更新 |
references/pytest-slow-test-causes.md | 按階段和特徵對緩慢的 pytest 檔案進行分類;逸出的 sleep;無法觸及的產品程式碼等待;套件範圍的封閉補丁(雲端客戶端、函式庫閒置 sleep)及守衛測試;工作流的單一進程分析;縮小執行時 create_all 的範圍 |
references/pytest-shared-setup.md | 從呼叫者評估熱門工廠的可共享部分;安全地共享昂貴的 pytest 物件:前後證明、最小變動的接線方式、變更選擇互動 |
references/vitest-isolation.md | JS/vitest 套件:等待與計算型檔案分類、僅在 CI 上慢的檔案、導入主導的成本、每模組導入分析、barrel 函式庫預打包、隔離與共享池、洩漏探測、隨機順序檢查 |
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 | pytest 插件,計算每個測試的實際 botocore 呼叫次數 — 當 cProfile 顯示 botocore 下的 SSL/socket 等待時執行它 |
使用時機
- 測試套件,或 CI 工作流程中的測試工作,太慢且使用者希望它變快。
- 使用者在先前的加速之後詢問「還有什麼很慢?」。
- 不用於沒有計時目標的複製/重複清理 — 為重用而重構測試並非加速(請參閱
test-suite-consolidation)。
流程
步驟 1:在同一 CI 路徑上建立基準
從 CI 取得基準,絕不從本地執行。基準必須與後測執行使用相同的 CI 路徑:當 PR 基底選擇了路徑(例如 prod 執行完整套件並分割成矩陣分支,而 dev 僅執行一個分支並帶有變更選擇),則使用指向同一個基底的最新成功 PR 執行,並檢查通過/跳過/取消選擇的計數是否匹配,然後再比較時間。提取該執行的每個步驟時間,依據執行器的章節標題分割一個大步驟,並閱讀工作流程檔案的註解 — 維護者通常已經寫下了預期的修復方式。然後,跨多次執行(而非一次)對步驟進行排名,並檢查執行器的實際 vCPU 數量與設定的工作者數量是否匹配。指令 → references/ci-measurement.md。
在提出任何建議之前,搜尋 issues 和 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 的作業系統和主要執行時期環境中進行再現(一個上限為 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 bag、應用程式、schema)。在發明快取之前,重用儲存庫現有的 session fixture,並先證明共享是安全的,然後證明基底和分支版本的檔案在相同的生產突變上失敗。模組/session 快取僅能在一個 pytest 行程內部進行去重:在分片的 CI 上,在計算節省量之前,檢查每個消費者執行在哪個分支。當一個單獨的測試僅重新讀取一個參數化物件的昂貴結果時,將它的斷言折疊到該案例中,而不是進行快取(與分片無關),並且破壞一個移動的斷言,以證明它仍然被執行 →
references/pytest-shared-setup.md。 - 在 CI 進入點內平行執行獨立的測試檔案。
- 將 CI 工作分割成矩陣分支,使兩半並行執行。
- 只有在這些之後,才改寫最慢檔案的每個案例產生的行程 — 變動範圍大,收益有限。提供此選項;但不要將其設為預設。
當大部分成本來自於不需要發生的整個執行時(機器人的反向合併、已在另一個分支上測試過的同一個樹狀結構、即將被超越的推送),這個槓桿優先於上述所有選項:按樹狀結構對執行歷史進行分類,並跳過或重用整個執行 → references/ci-redundant-runs.md。
完成條件:選擇的槓桿已命名並附有其步驟 2 的預估,並且任何被跳過的高階槓桿都有一個一行原因。
步驟 5:證明平行安全性並實作執行器變更
在一個一次性狀態目錄下平行執行每個檔案,每個檔案輸出到各自的輸出和退出碼檔案,列出失敗,並單獨重新執行每個失敗,以將並發 bug 與環境問題分開。將編輯工作目錄的檔案分區到一個序列化群組中,該群組使用它們已經攜帶的標記作為鍵。然後實作執行器,並且如果選擇了,實作 CI 矩陣分割。程序和規則 → references/parallel-runner.md。
完成條件:每個平行探測失敗都已分類(並發 / 環境 / 預先存在的),序列化群組是透過 grep 而不是手動列表得出的,並且在任何工作重新命名之前,所需的狀態檢查已經被檢查過。
步驟 6:在 CI 上驗證,而不是依賴本地執行
將變更的測試程式碼比照產品程式碼,遵守儲存庫自身的編碼規範 — 沒有透傳的包裝器、提取重複程式碼、為任何新的輔助功能提供突變證明。僅在本地執行受影響的檔案加上 lint,推送,並在背景下觀察 PR 的檢查,而不是佔用終端機。在撰寫任何驗證說明之前,閱讀 PR 的審查討論串:機器人審查者會在推送後的幾分鐘內發文。
當一個測試僅在本地環境中失敗(一個可選依賴的虛擬環境、一個本地偽造服務)時,在觸碰變更之前,先在一個一次性分離的工作目錄中,對未修改的基底執行該測試:那裡同樣的失敗是環境問題,而非回歸。在提交的 Not-tested: 結尾和 PR 主體中為其命名,並透過安裝該相依性的 CI 工作(其原始日誌:摘要行加上 grep -c FAILED = 0)來關閉它,而不是透過本地執行來關閉。
一個本地完整套件的基底對分支的比較支持「沒有新的失敗」,但僅在其被重現的範圍內有效:當僅分支上的失敗共享一個環境錯誤時,說明「N 個僅分支上的失敗,全部都是 ; 在基底上重現」,並讓 CI 執行關閉其餘部分 — 「0 個新失敗」的說法誇大其詞,並且需要在它觸及的每個表面上進行修正。
在宣布一個套件範圍的樁程式(stub)合併安全之前,列舉它可能如何改變結果,每個都帶有一個探測:(1) 繞過它的模組級別綁定(git grep -nE "from <mod> import <fn>" — 在導入時綁定的名稱會保留真實函式);(2) 未捕獲樁程式異常的生產呼叫者,以及是否有任何測試未經修補地執行了它們(執行前執行的每次測試呼叫計數會顯示);(3) 斷言真實路徑的測試 — 它們必須自己修補它,巢狀在 session 樁程式內部;(4) 正在進行的函式庫升級,它移動了被修補的名稱(請參閱 pytest-slow-test-causes.md 中版本容錯規則)。將每個項目報告為已檢查,或報告為一個推論,並附上什麼可以證實它。
機器人審查者的執行時間可能超過短輪詢的時間:Codex 連接器在審查時會將 PR 標記為 👀,並在完成且無發現時將其切換為 👍,有時會超過 20 分鐘。設定等待時間的界限,然後在合併簡報之前重新閱讀反應和討論串一次,而不是延長輪詢。要判斷一個現有的 👍 是否屬於最新的推送,請讀取其時間戳記(gh api graphql → pullRequest{reactions(first:20){nodes{content createdAt user{login}}}})與推送時間進行比較 — 一個比推送時間更舊的 👍 是上一輪的裁決。
一個 pull_request 執行會測試 head 合併到該時刻的基底頂端的結果,而不是 PR 自身的基底提交:從工作日誌中讀取基底 sha(HEAD is now at <m> Merge <head> into <base>),並與該基底提交的推送執行進行比較。
當套件在基準執行和 PR 執行之間增長時,每個檔案的指標是後測量:整個分支的秒數會混入其他提交的測試(首先比較 collected N items 計數)。將兩個執行中受影響檔案的 CI 秒數作為結果報告,並將分支時間作為上下文。在將基底合併到 PR 之後,使用同一時期來自其他 PR 的執行重建比較範圍,而不是使用更早的範圍:基底自身的套件持續增長,因此舊的範圍可能使一個真實的收益看起來像回歸。在報告中說明範圍本身何時移動了。
完成條件:分支在與基準相同路徑上的 CI 執行已完成,並且每個開放的審查發現都已得到回應。
步驟 7:報告前後對比
一個包含兩個執行 ID 的表格:工作牆壁時間(現在 = 最慢的分支)、先前慢的步驟、以及未改變的一半,加上每個分支的測試摘要行。對於分散在多個 PR 中的收益,使用一個表格:基準 → 每個合併的步驟 → 現在,並附上每個 PR 貢獻的比例。將 PR 未執行的 CI 路徑命名為未經驗證,並說明這只是一個執行(執行器變異無法衡量,且基線是中位數/p90)。然後,為每個檔案分類表說明:什麼從該批次中移除了,以及新的熱點可能是什麼 — 通常下一個最影響檔案數量的槓桿是什麼。被接受的修復應在提交訊息中附帶加速因素(例如 test-suite-speedup: —30%),被拒絕的則應附帶原因(例如 test-suite #42 或 test-suite-speedup: held — step 2 estimate was −$sec / gain under noise / parallel probe failed — file x changed state)。PR 主題行應該包含「test-suite-speedup」和(該 PR 的)百分比變更,並且 inspect 文件的註解應該包含該技能的名稱,以便以後的執行可以找到它。
完成條件:報告已發布。
與其他技能的關係
test-suite-speedup 專注於測量和結構性變更以減少測試執行時間。它不重構重複的測試程式碼本身(請參閱 test-suite-consolidation),也不分類或標記測試(請參閱 test-suite-classifier),但它們的輸出可以互相補充:一旦速度提升,就可以安全地在減少的噪音下合併重複項,並且分類器可以透過識別測試友善性來幫助選擇共享設置的候選者。