配置系统跟踪

您可以配置系统跟踪,在短时间内捕获应用的 CPU 和线程配置文件。然后,您可以使用系统跟踪的输出报告来提高游戏的性能。

设置基于游戏的系统跟踪

Systrace 工具可通过以下两种方式使用:

Systrace 是一款低级工具,它具有以下特点:

  • 提供真实数据。Systrace 直接从内核捕获输出,因此它捕获的指标与一系列系统调用报告的指标几乎相同。
  • 占用资源少。Systrace 对设备引入的开销极低,通常小于 1%,因为它将数据流式传输到内存缓冲区中。

最佳设置

为该工具提供一组合理的参数非常重要

  • 类别:最适合用于基于游戏的系统跟踪的类别是:{schedfreqidleamwmgfxviewsyncbinder_driverhaldalvik}。
  • 缓冲区大小:一般规则是,每 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 系统应用,请完成以下步骤以捕获系统跟踪,该跟踪应用了类别集、缓冲区大小和自定义事件的最佳实践

  1. 启用跟踪可调试应用选项。

    要使用此设置,设备必须有 256 MB 或 512 MB 可用(取决于 CPU 是 4 核还是 8 核),并且每块 64 MB 的内存都必须作为连续块可用。

  2. 选择类别,然后启用以下列表中的类别

    • am:Activity Manager
    • binder_driver:Binder 内核驱动程序
    • dalvik:Dalvik VM
    • freq:CPU 频率
    • gfx:图形
    • hal:硬件模块
    • idle:CPU 空闲
    • sched:CPU 调度
    • sync:同步
    • view:View System
    • wm:Window Manager
  3. 启用录制跟踪

  4. 加载您的游戏。

  5. 在游戏中执行与您要衡量其设备性能的游戏玩法相对应的互动。

  6. 在您的游戏遇到不良行为后不久,关闭系统跟踪。

您已捕获分析该问题所需的性能统计信息。

为了节省磁盘空间,设备上的系统跟踪以压缩跟踪格式 (*.ctrace) 保存文件。生成报告时,要解压缩此文件,请使用命令行程序并包含 --from-file 选项

python systrace.py --from-file=/data/local/traces/my_game_trace.ctrace \
  -o my_systrace_report.html

改进特定性能区域

本部分重点介绍了移动游戏中常见的几个性能问题,并介绍了如何识别和改进游戏的这些方面。

加载速度

玩家希望尽快进入游戏,因此尽可能缩短游戏的加载时间非常重要。以下措施通常有助于缩短加载时间:

  • 执行惰性加载 如果您在游戏的连续场景或关卡中使用相同的素材资源,则只需加载这些素材资源一次。
  • 减小素材资源的大小。 这样,您就可以将这些素材资源的未压缩版本与游戏的 APK 捆绑在一起。
  • 使用磁盘高效的压缩方法。 例如 zlib
  • 使用 IL2CPP 而不是 mono。(仅当您使用 Unity 时适用。)IL2CPP 可为您的 C# 脚本提供更好的执行性能。
  • 使您的游戏多线程化。 如需了解更多详情,请参阅帧速率一致性部分。

帧速率一致性

游戏体验最重要的元素之一是实现一致的帧速率。为了更容易实现此目标,请遵循本节中讨论的优化技巧。

多线程

在开发多平台应用时,将游戏中的所有活动都放在一个线程中是很自然的。虽然这种执行方法在许多游戏引擎中实现起来很简单,但在 Android 设备上运行时却远非最佳选择。因此,单线程游戏通常加载缓慢且缺乏一致的帧速率。

图 1 所示的 Systrace 显示了游戏一次仅在一个 CPU 上运行的典型行为

Diagram of threads
within a system trace

图 1. 单线程游戏的 Systrace 报告

为了提高游戏的性能,请使您的游戏多线程化。通常,最佳模型是使用 2 个线程:

  • 一个游戏线程,包含游戏的主模块并发送渲染命令。
  • 一个渲染线程,接收渲染命令并将其转换为设备的 GPU 可用于显示场景的图形命令。

Vulkan API 扩展了此模型,因为它能够并行推送 2 个公共缓冲区。使用此功能,您可以将多个渲染线程分布到多个 CPU 上,从而进一步提高场景的渲染时间。

您还可以进行一些引擎特定的更改以提升游戏的多线程性能:

  • 如果您正在使用 Unity 游戏引擎开发游戏,请启用多线程渲染GPU 蒙皮选项。
  • 如果您使用的是自定义渲染引擎,请确保渲染命令流水线和图形命令流水线正确对齐;否则,可能会导致游戏场景显示延迟。

应用这些更改后,您应该会看到游戏至少同时占用 2 个 CPU,如图 2 所示

Diagram of threads
within a system trace

图 2. 多线程游戏的 Systrace 报告

界面元素加载

Diagram of a frame
  stack within a system trace
图 3. 同时渲染数十个界面元素的游戏的 Systrace 报告

在创建功能丰富的游戏时,同时向玩家显示许多不同的选项和操作是很诱人的。但是,为了保持一致的帧速率,重要的是要考虑移动显示器相对较小的尺寸,并尽可能简化您的界面。

图 3 中显示的 Systrace 报告是一个界面帧的示例,该界面帧正在尝试渲染过多的元素,超出了移动设备的功能。

一个好的目标是将界面更新时间缩短到 2-3 毫秒。您可以通过执行类似于以下的优化来实现如此快速的更新:

  • 仅更新屏幕上已移动的元素。
  • 限制界面纹理和图层的数量。考虑合并使用相同材质的图形调用,例如着色器和纹理。
  • 将元素动画操作延迟到 GPU。
  • 执行更积极的视锥体剔除和遮挡剔除。
  • 如果可能,使用 Vulkan API 执行绘制操作。Vulkan 上的绘制调用开销较低。

功耗

即使在进行了上一节中讨论的优化之后,您可能会发现游戏的帧速率在游戏开始的 45-50 分钟内下降。此外,设备可能会随着时间的推移开始发热并消耗更多电量。

在许多情况下,这种不理想的散热和功耗情况与游戏工作负载在设备 CPU 上的分布方式有关。为了提高游戏的功耗效率,请应用以下各节中显示的最佳实践。

将内存密集型线程保持在一个 CPU 上

在许多移动设备上,L1 缓存位于特定的 CPU 上,而 L2 缓存位于共享时钟的 CPU 集合上。为了最大限度地提高 L1 缓存命中率,通常最好将游戏的主线程以及任何其他内存密集型线程保持在一个 CPU 上运行。

将短时工作延迟到低功耗 CPU

大多数游戏引擎,包括 Unity,都知道将工作线程操作延迟到与游戏主线程不同的 CPU 上。但是,引擎不了解设备的具体架构,也无法像您一样预测游戏的工作负载。

大多数系统级芯片设备至少有 2 个共享时钟,一个用于设备的快速 CPU,一个用于设备的慢速 CPU。这种架构的一个结果是,如果一个快速 CPU 需要以最大速度运行,那么所有其他快速 CPU 也会以最大速度运行。

图 4 中所示的示例报告显示了利用快速 CPU 的游戏。但是,这种高活动水平会迅速产生大量功耗和热量。

Diagram of threads
within a system trace

图 4. Systrace 报告显示线程与设备 CPU 的次优分配

为了降低整体功耗,最好建议调度程序将短时工作(例如加载音频、运行工作线程和执行协调器)延迟到设备上的一组慢速 CPU。在保持所需帧速率的同时,尽可能将更多此类工作转移到慢速 CPU 上。

大多数设备在快速 CPU 之前列出慢速 CPU,但您不能假设您的设备的 SOC 使用此顺序。要检查,请在 GitHub 上运行与此CPU 拓扑发现代码中所示命令类似的命令。

了解设备上的哪些 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 的性能,这可能会以意想不到的方式影响游戏。包含复杂图形、大量计算或持续网络活动的游戏更容易遇到问题。

使用热力 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,这会导致玩家输入与输入在屏幕上生效之间出现明显的延迟。

要确定您是否可以改进游戏的帧速,请完成以下步骤:

  1. 生成包含 gfxinput 类别的 Systrace 报告。这些类别包含特别有用的测量结果,用于确定触控到显示延迟。
  2. 检查 Systrace 报告的 SurfaceView 部分。过度填充的缓冲区会导致待处理的缓冲区绘制数量在 1 到 2 之间波动,如图 5 所示

    Diagram of
buffer queue within a system trace

    图 5. Systrace 报告显示过度填充的缓冲区周期性地过满而无法接受绘制命令

为了减轻帧速中的这种不一致性,请完成以下各节中描述的操作:

将 Android 帧速 API 集成到您的游戏中

Android 帧速 API 可帮助您执行帧交换并定义交换间隔,从而使您的游戏保持更一致的帧速率。

降低游戏非界面素材资源的解析度

现代移动设备的显示器包含比玩家能够处理的更多像素,因此可以进行降采样,使 5 甚至 10 个像素的连续区域都包含一种颜色。鉴于大多数显示缓存的结构,最好只沿一个维度降低分辨率

但是,不要降低游戏界面元素的解析度。保留这些元素的线宽对于为所有玩家保持足够大的触摸目标大小很重要。

渲染流畅度

当 SurfaceFlinger 锁定显示缓冲区以在游戏中显示场景时,CPU 活动会暂时增加。如果 CPU 活动的这些峰值发生不均匀,您的游戏可能会出现卡顿。图 6 中的图表描述了发生这种情况的原因

Diagram of frames
missing a Vsync window because they started drawing too late

图 6. Systrace 报告显示帧如何错过 Vsync

如果帧开始绘制过晚,即使只晚了几毫秒,它也可能错过下一个显示窗口。然后,帧必须等到下一个 Vsync 才能显示(游戏以 30 FPS 运行时为 33 毫秒),这会导致玩家感觉到明显的延迟。

为了解决这种情况,请使用Android 帧速 API,它始终在 VSync 波前呈现新帧。

内存状态

当您的游戏长时间运行时,设备可能会遇到内存不足错误。

在这种情况下,请检查 Systrace 报告中的 CPU 活动,并查看系统调用 kswapd 守护程序的频率。如果在游戏执行期间有许多调用,最好仔细查看游戏如何管理和清理内存。

如需了解更多信息,请参阅在游戏中有效管理内存

线程状态

在浏览 Systrace 报告的典型元素时,您可以通过在报告中选择线程来查看给定线程在每个可能的线程状态中花费的时间量,如图 7 所示

Diagram of a
Systrace report

图 7. Systrace 报告显示选择线程后报告如何显示该线程的状态摘要

如图 7 所示,您可能会发现您的游戏线程并未像它们应该那样经常处于“运行”或“可运行”状态。以下列表显示了给定线程可能周期性地转换为异常状态的几个常见原因:

  • 如果线程长时间处于休眠状态,它可能正在遭受锁争用或等待 GPU 活动。
  • 如果线程一直被 I/O 阻塞,那要么是您一次从磁盘读取了太多数据,要么是您的游戏正在抖动。

其他资源

要了解有关提高游戏性能的更多信息,请参阅以下其他资源:

视频