您可以設定系統追蹤,用於擷取應用程式在短時間內的 CPU 和執行緒概況。接著,您就能根據系統追蹤的輸出報表提升遊戲效能。
設定遊戲型系統追蹤
您可以透過兩種方式使用 Systrace 工具:
Systrace 是具有以下特性的低階工具:
- 提供基準真相。Systrace 可直接從核心擷取輸出內容,因此擷取的指標會與一系列系統呼叫回報的結果幾乎相同。
- 耗用的資源較少。Systrace 在裝置上的負擔極低,通常不到 1%,因為會將資料串流至記憶體內緩衝區。
最佳設定
請務必為工具提供一組合理的引數:
- 類別:最適合用來進行遊戲型系統追蹤的類別,包括:{
sched
、freq
、idle
、am
、wm
、gfx
、view
、sync
、binder_driver
、hal
、dalvik
}。 緩衝區大小:一般而言,每個 CPU 核心的緩衝區大小為 10 MB,就可以涵蓋約 20 秒的追蹤記錄。舉例來說,假設裝置有兩個四核 CPU (總計 8 個核心),則適合傳入
systrace
程式的值為 80,000 KB (80 MB)。如果遊戲會執行大量的情境切換,請將緩衝區增加到每個 CPU 核心 15 MB。
自訂事件:如果您要定義自訂事件來擷取遊戲中的事件,請啟用
-a
旗標,讓 Systrace 能夠將這些自訂事件納入輸出報表。
如果您要使用 systrace
指令列程式,請套用類別組合、緩衝區空間和自訂事件的最佳做法,然後使用下列指令擷取系統追蹤記錄:
python systrace.py -a com.example.myapp -b 80000 -o my_systrace_report.html \ sched freq idle am wm gfx view sync binder_driver hal dalvik
如果您在裝置上使用 Systrace 系統應用程式,請完成下列步驟,擷取已對類別組合、緩衝區空間及自訂事件採用最佳做法的系統追蹤記錄:
啟用「追蹤可偵錯的應用程式」選項。
如要採用這項設定,裝置必須有 256 MB 或 512 MB 的可用空間 (視 CPU 有 4 或 8 個核心而定),且每個 64 MB 記憶體都須能以連續區塊的形式使用。
選擇「類別」,然後啟用下列清單中的類別:
am
:活動管理員binder_driver
:Binder Kernel 驅動程式dalvik
:Dalvik VMfreq
:CPU 頻率gfx
:圖形hal
:硬體模組idle
:CPU 閒置sched
:CPU 排程sync
:同步處理view
:查看系統wm
:視窗管理員
啟用「Record tracing」。
載入遊戲。
在遊戲中,針對要評估裝置效能的遊戲過程,執行相應的互動。
如果在遊戲中遇到不理想的行為,請立即關閉系統追蹤功能。
現在,您已經擷取了進一步分析問題所需的效能統計資料。
為節省磁碟空間,裝置端系統追蹤記錄會以壓縮追蹤格式 (*.ctrace
) 儲存檔案。如要在產生報表時解壓縮這個檔案,請使用指令列程式並加入 --from-file
選項:
python systrace.py --from-file=/data/local/traces/my_game_trace.ctrace \ -o my_systrace_report.html
改善特定效能領域
本節重點介紹手遊中常見的幾個效能問題,並說明如何識別及改善遊戲的相關部分。
載入速度
玩家都希望能盡快開始遊戲,因此請盡可能改善遊戲的載入時間。下列措施通常有助於改善載入時間:
- 執行「延遲載入」。如果遊戲中的連續場景或關卡使用相同的素材資源,請僅載入這些素材資源一次。
- 縮減素材資源大小。這樣一來,您就能將這些素材資源的未壓縮版本與遊戲的 APK 捆綁在一起。
- 使用符合磁碟效率的壓縮方法。這種方法的範例有 zlib。
- 使用 IL2CPP 而非單聲道(僅適用於使用 Unity 時。)IL2CPP 可提高 C# 指令碼的執行效能。
- 將遊戲置於多執行緒。詳情請參閱「影格速率一致性」一節。
影格速率一致性
遊戲體驗中最重要的元素之一,就是採用一致的影格速率。為輕鬆達成這個目標,請按照本節所討論的最佳化技巧進行操作。
多執行緒
面向多個平台開發時,將遊戲中的所有活動置於單一執行緒是很自然的。雖然這種執行方法可在許多遊戲引擎中輕鬆實作,但在 Android 裝置上執行時並不是最佳做法。因此,單一執行緒遊戲的載入速度通常較為緩慢,而且影格速率不一致。
圖 1 所示的 Systrace 顯示一次僅在一個 CPU 上執行遊戲的典型行為:
如要提升遊戲效能,請設計多執行緒的遊戲。一般而言,最佳模型是使用 2 個執行緒:
- 「遊戲執行緒」,包含遊戲的主要模組,且會傳送轉譯指令。
- 「轉譯執行緒」,可接收轉譯指令,並轉譯成供裝置 GPU 用於顯示場景的圖形指令。
Vulkan API 在這個模型上擴充,因為系統能夠同時推送 2 個常見的緩衝區。這項功能可將多個轉譯執行緒分散於多個 CPU,進一步縮短了場景的轉譯時間。
您也可以進行一些引擎專屬的變更,改善遊戲的多執行緒效能:
- 如果您是使用 Unity 遊戲引擎開發遊戲,請啟用「Multithreaded Rendering」和「GPU Skinning」選項。
- 如果您使用自訂轉譯引擎,請確認轉譯指令管道和圖形指令管道皆正確校正;否則可能會延遲顯示遊戲場景。
套用這些變更後,您的遊戲至少會同時占用 2 個 CPU,如圖 2 所示:
載入 UI 元素
建立功能豐富的遊戲時,您可能會想同時向玩家顯示各種選項和動作。不過,如要維持一致的影格速率,請務必考量行動裝置螢幕相對較小,並盡可能簡化 UI。
圖 3 中顯示的 Systrace 報表是一個 UI 影格範例,其試圖轉譯的元素數量超出行動裝置的能力。
建議您將 UI 更新時間縮減至 2 到 3 毫秒。請執行類似下列的最佳化作業,以達到上述更新速度:
- 僅更新畫面上移動的元素。
- 限制 UI 紋理和圖層的數量。建議您合併使用相同材質的圖形呼叫,例如著色器和紋理。
- 將元素動畫作業推遲至 GPU。
- 執行更積極的視體和遮擋剔除。
- 如果可以,請使用 Vulkan API 執行繪圖作業。Vulkan 上的繪製呼叫負擔較低。
耗電量
即使已執行上一節所述的最佳化作業,在遊戲過程的前 45 至 50 分鐘內,遊戲的畫面更新率也可能降低。此外,裝置可能會開始發熱並隨著時間消耗更多電量。
在多數情況下,這類異常的熱量與耗電量都與遊戲工作負載在裝置 CPU 上的分配情況有關。如要提高遊戲的功耗效率,請採用下列各節中所示的最佳做法。
將耗用大量記憶體的執行緒儲存在一個 CPU 上
在許多行動裝置上,L1 快取會駐留在特定的 CPU 上,而 L2 快取則會駐留在共用時鐘的 CPU 組合上。為了盡可能提高 L1 快取命中數,我們通常會保留遊戲的主執行緒,以及其他會在同一 CPU 中執行的耗用大量記憶體的執行緒。
將短時間工作推遲到效能較低的 CPU 上
大多數遊戲引擎 (包括 Unity) 都會將背景工作執行緒作業推遲到與遊戲主執行緒不同的 CPU。但是,引擎未偵測到裝置的特定架構,也無法預期遊戲的工作負載。
大多數晶片系統裝置擁有至少 2 個共用時鐘,一個用於裝置的「快速 CPU」,另一個則用於裝置的「慢速 CPU」。這個架構的結果是,如果一個快速 CPU 需要以最高速度運作,則所有其他快速 CPU 也會以最高速度運作。
圖 4 中的範例報表顯示使用快速 CPU 的遊戲。然而,這種高活動量很快就會產生大量電力和熱源。
如要降低整體的耗電量,最好向排程器建議,將持續時間較短的工作 (例如載入音訊、執行工作站執行緒以及執行編排器) 推遲到裝置上的慢速 CPU 組合。盡可能多地將這些工作轉移到慢速 CPU 上,同時維持想要的影格速率。
大多數裝置會將慢速 CPU 列在快速 CPU 之前,但您無法假設裝置的 SOC 會使用這個順序。如要檢查,請執行類似於指令的指令 這個 CPU 拓撲探索中會顯示 程式碼 。
知道裝置上哪些是慢速的 CPU 後,就可以為持續時間較短的執行緒宣告相依關係,供裝置的排程器據以運作。為此,只要在每個執行緒中加入以下程式碼即可:
#include <sched.h> #include <sys/types.h> #include <unistd.h> pid_t my_pid; // PID of the process containing your thread. // Assumes that cpu0, cpu1, cpu2, and cpu3 are the "slow CPUs". cpu_set_t my_cpu_set; CPU_ZERO(&my_cpu_set); CPU_SET(0, &my_cpu_set); CPU_SET(1, &my_cpu_set); CPU_SET(2, &my_cpu_set); CPU_SET(3, &my_cpu_set); sched_setaffinity(my_pid, sizeof(cpu_set_t), &my_cpu_set);
熱應力
當裝置過熱時,裝置可能會對 CPU 和/或 GPU 進行節流措施,進而以非預期方式影響遊戲。如果遊戲使用複雜的圖形、大量計算或持續的網路活動,就更有可能引發問題。
使用 Thermal API 即可監控裝置的溫度變化,並採取相應措施以維持較低的耗電量和裝置溫度。裝置回報熱應力時,請結束進行中的活動以降低耗電量。舉例來說,請降低影格速率或減少多邊形鑲嵌。
首先,宣告 PowerManager
物件並用 onCreate()
方法進行初始化。在物件中新增熱狀態監聽器。
Kotlin
class MainActivity : AppCompatActivity() { lateinit var powerManager: PowerManager override fun onCreate(savedInstanceState: Bundle?) { powerManager = getSystemService(Context.POWER_SERVICE) as PowerManager powerManager.addThermalStatusListener(thermalListener) } }
Java
public class MainActivity extends AppCompatActivity { PowerManager powerManager; @Override protected void onCreate(Bundle savedInstanceState) { ... powerManager = (PowerManager) getSystemService(Context.POWER_SERVICE); powerManager.addThermalStatusListener(thermalListener); } }
定義監聽器偵測到狀態變更時要採取的行動。如果您的遊戲使用 C/C++,請將程式碼新增至 onThermalStatusChanged()
中的熱狀態層級,以使用 JNI 呼叫原生遊戲程式碼,或使用原生的 Thermal API。
Kotlin
val thermalListener = object : PowerManager.OnThermalStatusChangedListener() { override fun onThermalStatusChanged(status: Int) { when (status) { PowerManager.THERMAL_STATUS_NONE -> { // No thermal status, so no action necessary } PowerManager.THERMAL_STATUS_LIGHT -> { // Add code to handle light thermal increase } PowerManager.THERMAL_STATUS_MODERATE -> { // Add code to handle moderate thermal increase } PowerManager.THERMAL_STATUS_SEVERE -> { // Add code to handle severe thermal increase } PowerManager.THERMAL_STATUS_CRITICAL -> { // Add code to handle critical thermal increase } PowerManager.THERMAL_STATUS_EMERGENCY -> { // Add code to handle emergency thermal increase } PowerManager.THERMAL_STATUS_SHUTDOWN -> { // Add code to handle immediate shutdown } } } }
Java
PowerManager.OnThermalStatusChangedListener thermalListener = new PowerManager.OnThermalStatusChangedListener () { @Override public void onThermalStatusChanged(int status) { switch (status) { case PowerManager.THERMAL_STATUS_NONE: // No thermal status, so no action necessary break; case PowerManager.THERMAL_STATUS_LIGHT: // Add code to handle light thermal increase break; case PowerManager.THERMAL_STATUS_MODERATE: // Add code to handle moderate thermal increase break; case PowerManager.THERMAL_STATUS_SEVERE: // Add code to handle severe thermal increase break; case PowerManager.THERMAL_STATUS_CRITICAL: // Add code to handle critical thermal increase break; case PowerManager.THERMAL_STATUS_EMERGENCY: // Add code to handle emergency thermal increase break; case PowerManager.THERMAL_STATUS_SHUTDOWN: // Add code to handle immediate shutdown break; } } };
觸控顯示延遲時間
盡快轉譯影格的遊戲會建立一個受 GPU 限制的情境,其中影格緩衝區會過度填充。CPU 必須等待 GPU,這樣會導致玩家的輸入以及輸入在螢幕上生效之間出現明顯延遲。
如要確定您是否能改善遊戲的影格使用速度,請完成下列步驟:
- 產生包含
gfx
和input
類別的 Systrace 報表。這些類別包含特別實用的測量結果,以確定觸控顯示延遲時間。 查看 Systrace 報表的
SurfaceView
部分。過度填充的緩衝區會導致待處理緩衝區繪圖的數量在 1 到 2 之間波動,如圖 5 所示:圖 5. 顯示一個過度填充的緩衝區並會定期填充過滿而無法接受繪圖指令的 Systrace 報表
為減少影格使用速度的不一致,請完成下列各節所述的動作:
將 Android Frame Pacing API 整合至遊戲中
Android Frame Pacing API 可協助您執行影格交換並定義交換時間間隔,讓您的遊戲維持一致的影格速率。
降低遊戲的非 UI 資產的解析度
新型行動裝置的螢幕含有遠超出玩家可處理的像素數量,因此可以縮減像素採樣,使 5 或 10 個像素的執行都包含一種顏色。以大部分螢幕快取的結構而言,建議您僅仰賴一個維度來降低解析度。
不過,請避免降低遊戲 UI 元素的解析度。請務必讓這些元素保留線條粗細,為所有玩家維持足夠大的觸控目標尺寸。
轉譯流暢度
當 SurfaceFlinger 鎖定在螢幕緩衝區以顯示遊戲場景時,CPU 活動會立即增加。如果 CPU 活動的用量突然暴增,遊戲就可能會出現延遲。圖 6 中的圖表說明了發生這個問題的原因:
如果影格開始繪圖的時間太遲 (即使延遲數毫秒),也可能會錯過下一個顯示視窗。接著,影格必須等待下一個 Vsync 狀態顯示 (以 30 FPS 執行遊戲時為 33 毫秒),這樣玩家才能看見明顯延遲的情況。
如要解決這個問題,請使用 Android Frame Pacing API,而這個 API 一律會在 VSync 波形上顯示新影格。
記憶體狀態
如果執行遊戲的時間較長,可能導致裝置發生記憶體不足錯誤。
在這種情況下,請查看 Systrace 報表中的 CPU 活動,瞭解系統呼叫 kswapd
Daemon 的頻率。如果遊戲在執行時有許多呼叫,建議您進一步瞭解遊戲的管理和清理記憶體方式。
詳情請參閱「有效管理遊戲記憶體」。
執行緒狀態
瀏覽 Systrace 報表的一般元素時,您可以在報表中選取執行緒,查看指定執行緒在每個可能的執行緒狀態上停留的時間,如圖 7 所示:
如圖 7 所示,遊戲中的執行緒未按預期頻率處於「執行中」或「可執行」狀態。下方清單顯示指定執行緒定期轉移至異常狀態的幾個常見原因:
- 如果執行緒長時間處於休眠狀態,則可能是鎖定動作爭用或等待 GPU 活動所致。
- 如果執行緒持續在 I/O 上遭到封鎖,可能是因為您從磁碟一次讀取過多資料,或者您的遊戲遭到輾轉。
其他資源
如要進一步瞭解如何改善遊戲效能,請參閱下列其他資源:
影片
- 2018 年 Android 遊戲開發人員高峰會的 Systrace for Games 簡報