工程紀錄

用 xcresult 建立 XCTest 耗時回歸關卡

用 xcresult 建立 XCTest 耗時回歸關卡

一項原本只需 4 秒的 XCTest 逐漸變成 11 秒,流水線仍然全數通過,團隊通常要等到任務排隊明顯加劇時才會發現。總建置時間不適合用來定位這類變化,因為依賴項解析、編譯、模擬器啟動與結果寫入都混在其中。更可靠的做法,是在 NUMACS 雲端 Mac 的每次測試任務中保留 .xcresult,擷取個別測試案例的耗時,再與受控基準比較。

先定義關卡要測量的訊號

關卡應回答「同一項測試是否持續變慢」,而不是「這次任務是否比上次多花了幾秒」。首先將指標分成三個層級:

層級 指標 用途
測試案例 單次執行耗時 定位具體的效能回歸
測試套件 中位數與高百分位數 觀察一組測試的整體漂移
CI 任務 從啟動到結束的總時長 發現基礎設施或編譯階段的異常

不要根據單次結果直接阻擋合併。短測試容易受到排程抖動影響,長測試則可能被網路、動畫或非同步等待放大。實用的判定方式是採用「雙重門檻」:只有候選值同時超過基準的相對增幅與絕對增量時,才進入複測。例如,基準為 2 秒時,增加 20% 但只多 0.1 秒不應判定失敗;基準為 40 秒時,增加 8 秒就值得進一步檢查。

效能關卡的目標不是讓每次測量結果完全相同,而是儘早辨識可重現、可歸因的效能下降。

固定執行環境並產生結果套件

先固定 Xcode 路徑、Scheme、執行目標與平行策略。模擬器型號和系統版本必須明確指定,不能依賴「目前已啟動的裝置」。同一項任務中也不要動態變更並行數量。

set -euo pipefail

RESULT_DIR="$PWD/artifacts"
RESULT_BUNDLE="$RESULT_DIR/RegressionTests.xcresult"

mkdir -p "$RESULT_DIR"
rm -rf "$RESULT_BUNDLE"

xcodebuild test \
  -workspace Example.xcworkspace \
  -scheme ExampleTests \
  -destination 'platform=iOS Simulator,name=iPhone 16,OS=18.0' \
  -resultBundlePath "$RESULT_BUNDLE" \
  -parallel-testing-enabled NO

範例中的版本只是執行環境的一部分;實際專案應鎖定已通過驗證的 Xcode 與模擬器執行階段。先執行一次不納入統計的預熱任務,讓模擬器完成啟動,並將必要的測試資源寫入磁碟。正式樣本至少執行三次;若測試本身的變異較大,應增加執行次數,而不是放寬到失去意義的門檻。

從 xcresult 擷取測試案例耗時

較新的 Xcode 可以透過 xcresulttool 輸出測試樹狀結構。命令介面會隨 Xcode 版本演進,因此解析器必須與 CI 使用的 Xcode 版本一併鎖定,並在升級前使用已保存的結果套件進行相容性測試。

xcrun xcresulttool get test-results tests \
  --path artifacts/RegressionTests.xcresult \
  --format json > artifacts/tests.json

jq -r '
  .. | objects
  | select(.nodeType? == "Test Case" and .duration? != null)
  | [.name, .duration] | @tsv
' artifacts/tests.json > artifacts/test-durations.tsv

進入比較階段前,先確認 TSV 不是空檔案,且測試數量符合預期。不能將空檔案視為「沒有回歸」而直接放行;這通常表示命令介面已變更、測試未執行,或解析條件已不再相符。測試識別碼應包含模組、類別與方法;參數化測試還應保留參數名稱,避免錯誤合併不同樣本。

正規化時不要遺失原始證據

將所有耗時統一轉換為秒,並為每筆記錄附上 commit、Xcode 版本、執行目標與任務編號。彙整檔案便於比較,但原始 .xcresult 仍應保留為任務產物。發生異常時,它還能提供失敗資訊、活動記錄與附件的上下文。

使用穩健基準取代上一次結果

基準不應等同於主分支最近一次執行的結果。單次啟動緩慢會污染後續判斷,單次異常快速也會造成大量誤報。更穩妥的方式,是收集主分支最近若干次成功樣本,為每項測試計算中位數,並記錄高百分位數或中位數絕對偏差。

建議為每項測試保存以下欄位:

{
  "ExampleTests.testParsing": {
    "median_seconds": 3.84,
    "absolute_limit_seconds": 1.5,
    "relative_limit": 0.25,
    "sample_count": 9
  }
}

候選分支第一次超出門檻後,只複測超出門檻的測試案例或其所屬套件。若複測中位數仍同時超過 median_seconds + absolute_limit_secondsmedian_seconds × (1 + relative_limit),再將任務標記為失敗。新增測試應先進入觀察期;樣本不足時只產生報告,不參與阻擋。

排除最常見的假性回歸

模擬器冷啟動是最主要的雜訊來源。測試資料初始化、首次載入字型和建立資料庫資料表,也會讓第一輪執行偏慢;應透過預熱或明確的 setUp 加以拆分。接著檢查測試是否存取網路、等待實際時間、共用使用者預設值,或重複使用上一個測試案例遺留的檔案。

平行測試可能改變 CPU、記憶體與磁碟的競爭情況。若目標是建立單一測試案例的基準,應關閉平行執行;若目標是評估真實流水線的吞吐量,則應固定 worker 數量,並將該數量納入基準維度。不同 Xcode、系統執行階段或硬體配置的資料不能直接混合。

遇到突然變慢時,依序檢查:

  1. 測試數量和執行目標是否有變;
  2. 是否發生編譯、安裝或模擬器啟動等待;
  3. 逾時是否來自輪詢與固定休眠;
  4. 測試固定資料是否擴大,或未在結束後清理;
  5. 多項任務是否同時爭用同一個工作目錄。

讓基準變更可供審查

基準檔案應納入版本控制,但一般測試任務只能讀取,不能自動覆寫。若確實存在合理的變慢,應由獨立任務根據主分支的穩定樣本產生差異,且審查者需要看到舊值、新值、樣本數與變更原因。

最終報告應按「新增回歸、已恢復、觀察中」分組,並列出絕對增量、相對增幅與複測結果。如此一來,關卡既能阻止隱藏的效能退化,也不會因單次模擬器抖動而拖慢開發流程。完成後,團隊得到的不是一只脆弱的碼表,而是一套可重現、可解釋且可審查的 XCTest 耗時基準。

常見問題

為什麼不能直接以整次 xcodebuild 的總耗時作為關卡?

總耗時包含相依套件解析、編譯、模擬器啟動與結果寫入,無法定位個別測試。應從 xcresult 擷取每個測試的耗時。

XCTest 耗時門檻應使用固定秒數還是百分比?

建議同時使用絕對增量與相對增幅,且在重複執行後兩者仍越界才判定回歸,避免短測試被微小波動誤傷。

更新效能基準線時應保留哪些證據?

應保留基準檔、對應 commit、Xcode 與執行目標版本、原始 xcresult,以及更新理由。

NUMACS 雲端 Mac

將建置工作交給獨享實體工作站

兩種 Apple Silicon 配置皆為獨享實體機,並非虛擬機器,可按日、週、月或季租用,涵蓋新加坡、東京、首爾與香港節點。

選擇裝置並下單