一次在本機使用 Instruments 觀察時,結果顯示「首屏大約一秒」,但合併後的版本在遠端測試中卻明顯變慢。問題通常不在工具本身,而是測量範圍缺乏明確邊界:網路等待、JSON 解析、資料庫寫入與 UI 提交全被合併成一段總耗時,只要機器狀態稍有變化,結果就不再具備可比性。更可靠的做法是使用 os_signpost 標記業務區間,再透過雲端 Mac 上的 xctrace,以固定情境反覆採集資料。
先定義可重複的關鍵路徑
不要先問「整個 App 有多快」,而應先選擇一個起點與終點都穩定的使用者操作。例如進入訂單頁面後,從讀取本機快取開始,到首批清單儲存格完成資料綁定為止。將它拆分成三個區間:
| 區間 | 起點 | 終點 | 是否適合門禁 |
|---|---|---|---|
load_cache |
發起快取讀取 | 傳回結構化資料 | 適合 |
decode_payload |
開始解碼 | 產生領域物件 | 適合 |
render_first_batch |
提交首批模型 | 首批檢視完成版面配置 | 適合 |
wait_remote_api |
發起外部請求 | 收到回應 | 僅供診斷 |
外部網路耗時會受到線路與伺服器狀態影響,不適合直接用來阻止合併。效能門禁應優先涵蓋程序內、輸入固定且能重複觸發的工作。
一個區間只能回答一個問題。如果同一個標記同時包含網路、磁碟與渲染,即使超過門檻,仍然無法判斷應該修正哪個部分。
加入穩定且可關聯的埋點
使用固定的 subsystem 與 category,區間名稱也不要拼接使用者資料。需要並行測量時,應為每次業務操作建立獨立的 OSSignpostID,避免相鄰任務的起點與終點配對錯誤。
import os
enum PerfTrace {
static let log = OSLog(
subsystem: "com.example.mobile",
category: "CriticalPath"
)
static func begin(_ name: StaticString) -> OSSignpostID {
let id = OSSignpostID(log: log)
os_signpost(.begin, log: log, name: name, signpostID: id)
return id
}
static func end(_ name: StaticString, id: OSSignpostID) {
os_signpost(.end, log: log, name: name, signpostID: id)
}
}
呼叫端必須使用 defer 確保區間正確結束,否則例外分支可能留下只有起點、沒有終點的區間:
let traceID = PerfTrace.begin("decode_payload")
defer { PerfTrace.end("decode_payload", id: traceID) }
let models = try decoder.decode([Item].self, from: fixtureData)
不要將權杖、檔案內容或使用者識別資訊寫入 signpost 文字。效能證據會成為建置產物的一部分,因此欄位應維持低基數,並且能夠安全封存。
固定 xctrace 的執行條件
先在圖形介面中確認範本名稱與區間確實可見,再切換到命令列。測試應用程式應使用固定的資料夾具、固定的模擬器型號,以及相同的建置設定。採集前先終止舊程序,避免受到上一次執行殘留狀態的影響。
set -euo pipefail
OUT="$PWD/perf-artifacts"
mkdir -p "$OUT"
xcrun xctrace record \
--template "Points of Interest" \
--time-limit 45s \
--output "$OUT/critical-path.trace" \
--launch -- "$APP_PATH" \
-PerfScenario first-batch \
-PerfFixture "$PWD/Fixtures/items.json"
xcrun xctrace export \
--input "$OUT/critical-path.trace" \
--toc > "$OUT/toc.xml"
xctrace export 的 XPath 與表格結構可能隨工具版本改變,因此解析指令碼應先確認目標 schema 是否存在,再讀取區間。不要把依賴欄位位置的 awk 視為長期介面;應優先解析匯出的 XML,並在版本升級時保留一份最小 trace 作為契約測試。
在 LemonVM 的遠端環境中執行時,應先記錄 xcodebuild -version、系統版本、建置設定與測試夾具雜湊。節點或工具鏈變更後,應重新建立基準線,而不是直接把新舊環境的數據放在一起比較。
以多輪樣本建立門檻
單次執行很容易受到首次載入、背景索引與溫度狀態影響。建議先進行一次預熱,再執行至少五輪有效採集。門禁應比較中位數,同時為最慢的樣本設定較寬鬆的上限。
假設基準線中位數為 420 ms,可以將警示線設定為基準線的 1.15 倍,也就是 483 ms;但不要將這個比例套用到所有區間。耗時數十毫秒的小型函式與耗時數秒的資料準備流程,其波動模型並不相同。
判定指令碼至少應區分三類結果:
- 無法產生 trace:基礎設施失敗,不判定為效能退步。
- 缺少目標區間:情境或埋點失敗,需要修正測試。
- 有效樣本超過門檻:效能門禁失敗,並封存相關證據。
基準線檔案應與程式碼一併審查,內容包括區間名稱、樣本數、中位數、允許上限、工具鏈版本與夾具雜湊。任何提高閾值的變更都應在提交說明中註明原因,避免門檻只升不降。
排除常見的假性退步
如果首次執行較慢、後續則趨於穩定,通常是快取或動態連結準備造成的,應將冷啟動與熱路徑分成兩組。若所有輪次都很慢,再檢查程式碼變更。只有某一輪出現異常時,則應先確認當時是否有索引、備份、額外模擬器或並行建置正在執行。
還需要檢查以下項目:
- Release 與 Debug 資料不可混用;
- 不同模擬器系統版本應分別維護基準線;
- 每輪測試前都要還原相同的業務狀態;
- 禁止效能任務與大型建置任務並行;
- 將 trace、解析結果與標準錯誤輸出一併封存;
- 無法解析區間時,禁止預設寫入
0 ms; - 逾時退出後,必須終止受測應用程式與殘留的採集程序。
如果測試依賴圖形工作階段,應在任務開始前增加一次可見性預檢;如果只測試純解析或儲存層,則應盡量使用獨立的測試目標,減少介面狀態造成的雜訊。
讓失敗結果能夠直接採取行動
門禁輸出不應只有「效能失敗」。至少應列出區間名稱、目前中位數、基準線、變化比例、有效樣本數與 trace 路徑。例如:
FAIL render_first_batch
median_ms=512
baseline_ms=420
change=21.9%
samples=7
trace=perf-artifacts/run-06.trace
審查者看到結果後,應能直接下載對應的 trace、定位到具體區間,並判斷問題是業務程式碼退步,還是測量環境異常。穩定的效能門禁並不是要求每次數字完全相同,而是要讓相同輸入、相同工具鏈與相同執行條件下的變化可解釋、可複查。先從一條高價值路徑開始,持續觀察其波動,再逐步增加區間,會比一次加入數十個不可靠的指標更有效。
常見問題
os_signpost 效能門檻應該使用平均值還是中位數?
優先使用多輪有效樣本的中位數,並同時限制高分位數或最大允許值。平均值容易被單次系統抖動拉偏,單次結果也不適合作為合併門檻。
xctrace 採集失敗時應該直接判定效能退步嗎?
不應該。採集失敗、區間缺失與效能超限應使用不同結束碼;前兩者屬於基礎設施或情境問題,取得有效樣本後才能判定效能退步。
所有 os_signpost 區間都適合放進持續整合嗎?
不適合。只選擇起訖點穩定、業務語意明確且能由固定輸入重複觸發的關鍵路徑;依賴人工操作或外部即時服務的區間應保留為診斷指標。
依任務週期選擇 LemonVM 雲端 Mac
Lemon M4 與 Lemon M4 Pro 覆蓋新加坡、東京、首爾、香港及美國西部,支援日租、週租、月租與季租。