原文連結:Performance testing of Flutter apps 作者:Filip Hracek (from Flutter team)
Flutter 在預設狀態下就能執行得非常快,這點非常棒! 但是否這就意味著你完全不需要考慮效能呢?
答案是否定的,編寫一個速度非常慢的 Flutter 應用是完全可能的。然而,另一方面你也可以充分利用這個框架,讓你的 app 不僅快速,高效,而且使用更少的 CPU 時間和電量。
這就是我們想要的!在一個有意義的指標上來比較兩個版本的應用在統計上明顯的差異
在 Flutter 中有一些效能優化的通用指導原則:
- 更新狀態時,影響範圍儘可能地少。
- 僅當必要時才更新狀態。
- 不要在 build 方法中進行密集型計算任務,理想的話,把這些操作放在 main isolate 之外。
你可能很難相信,對於大多數效能優化的問題來說,它們的答案統統指向了這句話——“它究竟取決於什麼?”。對於特定 Widget 是否值得進行特定優化,並付出維護成本?在特定情況下的特殊處理是否合理?
對於這些問題唯一有用答案是測試和測量。量化每個選擇對效能的影響,並根據該資料做出決定。
好訊息是 Flutter 提供了出色的效能分析工具,例如包含 Flutter Inspector 的 Dart DevTools(目前還處於預覽版),或是 Android Studio 中的 Flutter Inspector(安裝了 Flutter 外掛下)。你可以使用 Flutter Driver 操作應用,並在 Profile 模式下儲存效能資訊。
然而壞訊息是,現在的手機實在是太過智慧了。?
管理器的問題(Governors)
系統級守護程式需要根據當前負載調整 CPU 和 GPU 單元的速度,但是 iOS 和 Android 的管理器卻很難量化 Flutter 應用的效能。總的來說這還是一件好事,因為它確保了平穩的效能,同時也消耗盡可能少的電量。
然而缺點是你完全可以通過提高其效率以顯著加快應用的執行速度。
下面的例子中,你可以看到如何在應用中迴圈列印一些無意義的 print 語句,使得管理器切換到更高的檔位,從而讓應用執行更快速,效能更加可預測。
管理器的問題:在預設情況下,你無法相信這些數字。在上面這個盒子圖中,我們在 x 軸上進行單獨執行(用它們開始的確切時間標記),並在 y 軸上表示構建時間。正如你所看到那樣,當我們引入一些完全不必要的列印語句時,它竟然會縮短(而不是增加) build 時間。
在這個實驗中(上圖),更差的程式碼反而導致了更快的構建時間,更快的光柵化時間和更高的幀率。如果客觀上更差的程式碼會得到更好的效能,那麼你就不能遵循這些指標。
上面僅僅是為了解釋為何移動應用效能基準不直觀,以及測試困難的一個例子。
接下來,我將分享一些 Google I/O app 上 Flutter 的例子,Developer Quest。
基本建議
- 不要在 DEBUG 模式下測量效能。只有 profile 模式下才能測量其效能。
- 在真機上測試,不要在 Android 或者 iOS 模擬器上測試。雖然模擬器軟體非常適合開發使用,但是它們在效能表現上和真機差異非常大。Flutter 不允許在模擬器上以 profile 模式執行,因為這並沒有任何意義。這種方式收集的效能資料並不是實際的效能。
- 理想的情況下是使用相同物理裝置。讓它作為你專用的效能測試機,並不再用於其他用途。
- 學習 Flutter 效能分析工具
CPU / GPU 管理器
正如我們剛才所說的,現代作業系統根據負載和一些其他的啟發式調整每個 CPU 和 GPU 的頻率。(例如,觸控螢幕通常會讓 Android 手機將其優先順序置於更高的檔位。)
在 Android 上你能夠直接關掉這些管理器,我們稱之為“scale locking”。
- 編寫一個指令碼來 scale-lock 你的測試機效能。你可以在 Skia’s recipe 中找到靈感,也可以查閱 Unix CPU API。
- 除非你正在執行像 Skia 這樣的大型基準測試,通常情況下你可能想要一些更加輕量,不那麼通用的東西。來看看 Developer Quest 中針對某些方面的 shell 指令碼吧。 例如,下面這個將 CPU 管理器設為 userspace (唯一一個不會自動調整 CPU 頻率的管理器)。
#!/usr/bin/env bash
GOV="userspace"
echo "Setting CPU governor to: ${GOV}"
adb shell "echo ${GOV} > /sys/devices/system/cpu/cpu${CPU_NO}/cpufreq/scaling_governor"
ACTUAL_GOV=`adb shell "cat /sys/devices/system/cpu/cpu${CPU_NO}/cpufreq/scaling_governor"`
echo "- result: ${ACTUAL_GOV}"
複製程式碼
- 現在的目標並不是要模擬真實效能(沒有使用者 scale-lock 的裝置),而是獲取執行時可比較的效能指標。
- 最後你需要進行測試,並在裝置上執行 shell 指令碼。只有這樣才是有效的,在這之前的效能資料都在欺騙你。
Flutter Driver
Flutter Driver 能夠自動執行應用。你可以閱讀 flutter.dev 上效能分析的文章,瞭解如何使用它來分析應用。
- 不要在效能測試時手動操作你的應用。始終使用 Flutter Driver 確保你的比較具有意義。
- 編寫你的 Flutter Driver 程式碼,以便能夠讓其執行你真正想要測試的內容。如果你正在進行常規程式效能測試,請嘗試遍歷全部應用,並執行使用者會做的操作。
- 如果你的應用具有偶然性(隨機網路事件等),請使用 mock 資料,並且保證它們儘可能相似。
- 如果需要的話,還可以使用 Timeline 的
startSync()
方法 和finishSync()
方法,來新增自定義時間軸事件。例如,當你想要測試特定方法的效能時,這很有用。將startSync()
放在它的開頭,並在方法結束時使用finishSync()
。 - 對於各個不同版本的應用,需要進行多次測試。在 Developer Quest 時我彙總了 100 次。當你測量一些比如像“第 99 位百分數”這樣繁雜的東西時,你得執行更多次才行。而對於基於 POSIX 的系統來說,只需要執行下面這樣:
for i in {1..100}; do flutter drive --target=test_driver/perf.dart --profile; done
複製程式碼
時間軸(Timeline)
時間軸是你執行 profile 模式下輸出的原始資料。Flutter 將此資訊轉儲到可被 chrome://tracing
載入的 JSON 檔案中。
- 瞭解如何在 Chrome 的 tracing timeline 中開啟完整的 timeline。你只需要開啟 Chrome 瀏覽器的
chrome://tracing
,然後點選“Load”,選擇那個 JSON 檔案。你可以在這篇 簡短指導 中獲得關於它的更多資訊。(同樣有 Flutter Timeline tooling,但目前還處於 tech preview 階段。因為在 Flutter 的 timeline tooling 準備就緒之前,Developer Quest 專案就已經開始了,所以我還沒用過那個。) - 使用 WSAD 鍵在
chrome://tracing
的 timeline 中移動,以及使用 1234 來更改操作模式。 - 首次設定效能測試時,考慮是否使用 Flutter Driver 執行完整的 Android systrace。這使您可以更深入地瞭解裝置中實際發生的情況,包括 CPU 縮放資訊。但是,不要使用完全開啟的 systrace 測量您的應用程式,因為它會讓一切變得非常慢而且更加不可預測。
- 那麼,應該如何使用 Flutter Driver 執行完整的 Android systrace 呢?首先,你得從
/path/to/your/android/sdk/platform-tools/systrace/systrace.py --atrace-categories=gfx,input,view,webview,wm,am,sm,audio,video,camera,hal,app,res,dalvik,rs,bionic,power,pm,ss,database,network,adb,pdx,sched,irq,freq,idle,disk,load,workq,memreclaim,regulators,binder_driver,binder_lock
開啟 Android systrace。 然後通過flutter run test_driver/perf.dart --profile --trace-systrace
執行 app。最後,通過flutter drive --driver=test_driver/perf_test.dart --use-existing-app=http://127.0.0.1:NNNNN/
啟動 Flutter Driver(其中 NNNNN 是Flutter run
上執行的埠給你的)。
度量
最好能看盡可能多的指標,但我發現一些相比其他更有用的指標。
-
build 的時間 和 光柵化的時間 僅在真正嚴格的效能測試中才有用(預設情況下提供的度量標準是
TimelineSummary
),這些測試除了 UI 之外不會包含太多其他內容。 -
不要把
TimelineSummary.frameCount
看作計算每秒幀數(FPS)的方法。Flutter 的配置檔案工具不能給你提供真實的幀速率資訊。TimelineSummary
雖然提供了countFrames()
方法,但它只會計算已完成的幀構建了多少次。一個優化良好的應用可以限制不必要的重建,每秒的幀數比經常重建的未經優化的應用程式少很多。 -
我個人獲得的最有用的資料是通過測量執行 Dart 程式碼所花費的總 CPU 時間得到的。這會算上 build 方法和外部執行的程式碼。假設你在 scale-locked 的裝置上執行配置檔案測試,總 CPU 時間可以很好地估算你的應用程式將消耗更多還是更少的電。
- 要找出 Dart 程式碼執行所花費的總 CPU 時間,最簡單的方法是看 timeline 中
MessageLoop:FlushTasks
事件的範圍。在製作Developer Quest
的時候,我編寫了一個 Dart 工具來提取它們。 - 要檢測 jank (即跳過的幀),請尋找極端情況。例如,對於 Developer Quest 的特定情況以及我們用於測試的裝置來說,觀察 95% 的 build 時間很有幫助。(90% 的 build 時間都很相似,即使是在比較效率水平差距非常大的程式碼也是一樣,而 99% 往往又過於亂了。但你的情況可能會有所不同。)
- 正如我之前所提到的那樣,對於你的每個版本的應用都演算它上百次, 然後使用有些許誤差的平均值或百分位資料。如果使用箱形圖就更好了!
結論
當這些都設定好了之後,你就能夠自信地比較提交和實驗。下面,你可以看到一個很常見困境的答案:“這種優化值得維護開銷嗎?”
我認為對於這個情況來說,答案是肯定的。只需幾行程式碼,我們應用的每個自動測試平均可以減少 12% 的 CPU 時間。
然而,本文的主要內容是,不同的測量方式可能會反映處非常不同的東西。嘗試過於寬泛地推斷效能測量也許十分符合直覺,但卻是錯誤的。
換句話說:“它究竟取決於什麼”。我們應該信奉這句話。
以上便是翻譯的全部內容,我認為這篇文章對我們構建高效的 Flutter 應用具有很大的指導意義,所以便翻譯出來分享給大家。
如若有譯誤還請指出。