iT邦幫忙

2026 iThome 鐵人賽

DAY 16
0
Software Development

Windows 桌面軟體 CI 實戰:從系統工具開發到 Zero-RDP 自動化測試錄影系列 第 16

Day 16:0.10 秒 vs 7.84 秒——冷啟動比任何人的預算都久

  • 分享至 

  • xImage
  •  

第四章開始。這一章講的都是同一件事的不同面向:你在自己機器上調出來的參數,在 CI 上是錯的。


那個 80 倍的數字

同一份測試程式碼、同樣啟動記事本(Notepad)、同樣等待主視窗出現。從送出啟動指令到 UIA 探索到該視窗:

Runner 環境 作業系統 SKU launch_appwindow_discovered
windows-latest(x64) Windows Server 2025 0.10 秒
windows-11-arm(ARM64) Windows 11 Client 7.84 秒(一般冷啟動) / 17.18 秒(高負載峰值)

近百倍的差距。

初次看到這個數字的人,直覺往往會得出一個看似合理的推論:「ARM64 上有模擬層,而且雲端虛擬機的 I/O 太慢,架構差異導致了近百倍的效能差距。」

這個推論從頭到尾都是錯的。


不是架構差異,是 SKU 與應用程式本體不同

在自動化測試與跨平台驗證中,有一條非常殘酷的真實法則:SKU 差異往往比架構差異更致命。

這近百倍的差距,根本不是「x64 比 ARM64 快 80 倍」,而是你在兩個環境裡啟動的根本是完全不同的兩個軟體

1. windows-latest 執行的是傳統 Win32 記事本

GitHub Actions 的 windows-latest 映像底層是 Windows Server(例如 Windows Server 2022 或 2025)。在實際的 CI job log 中,檢查套件會直接印出:

Microsoft.WindowsNotepad: not a packaged app on this image

在 Server SKU 上,C:\Windows\System32\notepad.exe 是一個只有幾百 KB 的純 C / Win32 傳統桌面應用程式。沒有現代 UI 框架、沒有套件容器、沒有任何額外相依性,建立處理程序到刻出視窗只需要 0.10 秒。

2. windows-11-arm 執行的是打包的 WinUI 3 現代應用

windows-11-arm 執行的是 Windows 11 Client。在 CI 的預備步驟日誌中,它清楚回報:

Microsoft.WindowsNotepad: 11.2512.29.0 [Ok]

在 Windows 11 上,記事本早就被改寫為現代的 MSIX 打包應用程式(Microsoft.WindowsNotepad),而且在 ARM64 runner 上它也是 100% 原生的 ARM64 二進位檔,根本沒有走任何 Prism x64 模擬。

更關鍵的是,你在命令列敲下 notepad.exe 時,它並不是直接載入本體:

  1. App Execution Alias 交棒C:\Windows\System32\notepad.exe 在 Win11 下只是一個約 300 KB 的代理啟動器(Launcher)。它透過系統的 Execution Alias 機制,將啟動請求轉發給 Windows 的套件子系統。
  2. AppContainer 與 DCOM 啟用:系統必須解析 AppX 套件圖譜、建立沙盒容器(AppContainer)、透過 DCOM 啟動真正的套件處理程序。
  3. WinUI 3 / XAML 執行階段載入:載入 Windows App SDK、初始化 XAML 視覺樹、建構字型快取。在無競爭的正常冷機啟動下,這一連串動作需要 7.84 秒
  4. 背景更新競爭(Store 換包)與資源緊繃:在全新開機的 CI 機器上,連網後 Microsoft Store 服務(AppXSvc)會偵測並在背景下載新版套件,若疊加記憶體分頁緊縮,啟動耗時會飆升至 17.18 秒;更甚者,若前一個測試剛好關閉記事本,Store 趁機換包,新啟動請求會直接卡在背景等待(如 run 33956741564),整整 30 秒都無法產生任何視窗:
WindowDiscoveryTimeoutError: No new window appeared within 30.0s (cmd=['notepad.exe'])
  Visible windows at the moment discovery gave up (11 total):
        class='#32770' pid=6508 title='Performance Options'
        class='DummyDWMListenerWindow' pid=880 title=''
        class='Shell_TrayWnd' pid=880 title=''
    (nothing new appeared at all - the launch produced no window)

0.10 秒對 7.84 秒(乃至極端情況的 17 秒),比的不是 CPU 架構,而是一個 0 依賴的三十歲 Win32 程式 與一個 走完整現代生命週期、在冷機上初次載入且隨時面臨 Store 換包的 WinUI 3 打包應用


為什麼這會變成「偽 flaky」測試?

如果在自己的開發機上調測,你看到的往往是一台「暖過機」的系統:套件已載入過快取、字型快取早已就緒、Store 沒有在背後抓更新。在你的筆電上,Win11 記事本可能 0.5 秒就開好了。

於是你直覺地寫下一個自以為寬鬆的等待:

window = Window.find(title_pattern="Notepad", timeout=5.0)

五秒,比平常量到的慢了整整十倍,任何人審 code 都會覺得綽綽有餘。

但在全新的 Windows 11 CI runner 上,這行程式碼必死無疑。它會在 5.0 秒準時噴出:

WindowDiscoveryTimeoutError: Window failed to appear within 5.0s

而這類失敗最陰險的地方在於它的間歇性

  • 如果該輪 runner 資源剛好充裕、或是第二個測試啟動時快取已經建好,它可能在 4.8 秒驚險過關;
  • 如果剛好撞上背景 AppXSvc 換包或冷啟動峰值,它就耗時 17 秒而逾時。

工程師看到「有時綠、有時紅」,直覺反應就是給測試加上 @pytest.mark.flaky(reruns=3),或者歸咎於「Windows CI 就是不穩定」。

但它根本不是隨機的不穩定,它是逾時預算從一開始就給錯了。 間歇性失敗是症狀,不是診斷。把系統性延遲誤診成 flaky,代價就是真正的環境問題永遠不會被看見。


地板值:預設 30 秒

託管啟動的預設探索逾時應該直接給出足夠寬裕的地板值:

session.app(NOTEPAD)  # 預設 30 秒探索逾時

30 秒對「開一個記事本」來說荒謬嗎?在你的開發機上是。在剛開機的 Windows 11 ARM64 runner 上,它是覆蓋 7.84 秒常態冷啟動、17 秒資源緊縮峰值,以及背景換包競爭的合理餘裕。

這裡有個常見的反對意見:「逾時設這麼長,測試會不會變很慢?」

不會。

逾時是上界,不是等待時間。 視窗探索是輪詢的——一旦視窗出現就立即返回。在 windows-latest 上它 0.10 秒就回來了,那個 30 秒從來沒有被消耗過。

只有在真的失敗(程式崩潰、名稱拼錯、或真的死鎖)時,測試才會等到 30 秒。而那時候,你本來就必須付出等待成本——差別只在於你付出的是「30 秒後得到確切的失敗結論」,還是「5 秒後得到一個誤導人的偽陽性逾時」。

寬鬆的啟動逾時不會讓成功的測試變慢一毫秒,只會讓失敗的測試多花一點時間。在 CI 預算裡,這個交換幾乎永遠划算。


測量,不要猜

30 秒這個數字不是憑空捏造的,而是從 session_events.json(Day 4)實際計算出來的:

launch = next(e for e in events if e["type"] == "launch_app")
found  = next(e for e in events if e["type"] == "window_discovered")
print(f"啟動到視窗就緒耗時:{found['timestamp'] - launch['timestamp']:.2f} 秒")

這就是第一章那些診斷產物的回報。有了每次 run 精確到浮點數秒的實測數字,「逾時該設多少」從無意義的猜測變成查表動作。

而且它是持續可觀測的:哪天 runner 的映像換了、記事本更新了、啟動時間漂移到 25 秒,你會在數字上第一時間看到趨勢,而不是在不明不白的測試失敗率裡瞎猜。


不同生命週期的操作,不能共用相同的逾時量級

許多自動化腳本習慣全域設一個 timeout=10 到處套用。這會導致兩頭落空:啟動應用時太短,控制項操作時又太長。

合理的逾時應該依據底層牽動的作業系統層級分級:

操作類型 底層發生的事 合理逾時量級
啟動應用程式並探索主視窗 AppContainer 沙盒、DCOM 啟用、執行階段載入、處理程序樹初始化 30 秒
元素操作的後置條件驗證(Day 8) UI 訊息迴圈處理、控制項重繪、內部狀態變更 2–3 秒
尋找已經存在的視窗或控制項 UIA 樹走訪、快取查詢 1–2 秒

等一個按鈕反白,是等一個 UI 訊息迴圈跑一圈(毫秒級);等一個現代應用程式冷啟動,是等作業系統完成成百上千個 I/O、權限檢查與框架載入(秒級)。把它們給同一個逾時值,只會讓短的那個容易誤殺啟動,長的那個讓每次找不到元素都要枯等半分鐘。


反過來也成立:快得反常也是訊號

記住精確時間戳還有另一個至關重要的用途:不只太慢是問題,太快往往是致命訊號。

Day 7 提過一個典型案例:某個應用程式在全新環境下,真正的冷啟動與刻出主視窗至少要 19 秒。但在某次 CI run 中,探索函式竟然在 1.2 秒 內就回報「找到視窗」了。

那 1.2 秒絕對不是效能突飛猛進——它找到的是應用程式因為缺少執行階段相依性而自己彈出的錯誤對話框

如果測試的尋找條件寫得太寬鬆(例如只依據處理程序名稱比對),測試就會把這個錯誤視窗誤當成主畫面,接著在下一步找不到任何操作元素而莫名其妙崩潰。

所以「這一步花了多久」在兩個方向上都具有判斷力:

時間特徵 現場可能發生的事
比預期久很多 冷啟動、套件安裝換包中、資源競爭、或前景被系統視窗劫持
比預期快很多 你找到的根本不是主畫面,而是崩潰回報或錯誤對話框

時間不是單純的計時器,它本身就是具有判斷力的斷言依據。


小結

  • 近百倍的差距來自系統 SKU 與軟體本質,不是純硬體架構:Windows Server 跑的是 0.10 秒刻出畫面的純 Win32 傳統程式,Windows 11 跑的是冷啟動需 7.84 秒(極端情況達 17 秒)的 WinUI 3 打包應用。
  • 冷機與暖機的鴻溝巨大:開發機上的快取與暖機狀態,在全新啟動的 CI 環境中完全不復存在。
  • 別把系統性延遲誤診成 flaky:啟動逾時要給足上限(30 秒地板值),因為它是上界而非等待時間,不會拖慢成功執行的速度。
  • 不同層級的操作給不同量級的逾時:啟動給 30 秒,元素後置條件驗證給 2 秒,不要一把抓。
  • 快得反常與慢得離譜同樣危險:1.2 秒找到視窗往往代表抓到了錯誤彈窗,精確的時間差分能即時揭露偽陽性。

明天講另一種冷啟動失敗:測試明明啟動了應用程式,探索卻根本沒看到新視窗——因為上一輪的實例還活著。


上一篇
Day 15:在一個「全都是整合測試」的領域裡,把純函式挖出來
下一篇
Day 17:啟動了,但沒有新視窗——單例應用與幽靈視窗
系列文
Windows 桌面軟體 CI 實戰:從系統工具開發到 Zero-RDP 自動化測試錄影24
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言