抖动是指阻止可感知工作运行的随机系统行为。本页介绍了如何识别和解决与抖动相关的卡顿问题。
应用线程调度器延迟
调度器延迟是抖动最明显的症状:应该运行的进程已变为可运行状态,但未在相当长的时间内运行。延迟的重要性因上下文而异。例如
- 应用中的随机辅助线程可能会被延迟数毫秒而不会出现问题。
- 应用的 UI 线程可能可以容忍 1-2 毫秒的抖动。
- 以 SCHED_FIFO 运行的驱动程序 kthread 如果在运行前可运行 500 微秒,则可能会导致问题。
可运行时间可以在 systrace 中通过线程运行段之前的蓝色条识别出来。可运行时间也可以通过线程的 sched_wakeup
事件与指示线程执行开始的 sched_switch
事件之间的时间长度来确定。
运行时间过长的线程
可运行时间过长的应用 UI 线程可能会导致问题。具有较长可运行时间的较低级别线程通常有不同的原因,但尝试将 UI 线程可运行时间推向零可能需要修复导致较低级别线程具有较长可运行时间的一些相同问题。为了缓解延迟
- 使用 cpusets,如热节流中所述。
- 增加 CONFIG_HZ 值。
- 从历史上看,在 arm 和 arm64 平台上,该值已设置为 100。但是,这只是历史的偶然,对于交互式设备来说,这不是一个好的值。CONFIG_HZ=100 意味着一个 jiffy 是 10 毫秒长,这意味着 CPU 之间的负载均衡可能需要 20 毫秒(两个 jiffy)才能发生。这可能会显着加剧负载系统上的卡顿。
- 最近的设备(Nexus 5X、Nexus 6P、Pixel 和 Pixel XL)已随 CONFIG_HZ=300 一起发布。这应该只有微不足道的功耗成本,同时显着改善可运行时间。如果您在更改 CONFIG_HZ 后确实看到功耗或性能问题显着增加,则可能是您的某个驱动程序正在使用基于原始 jiffy 而不是毫秒的计时器并转换为 jiffy。这通常很容易修复(请参阅补丁,该补丁在转换为 CONFIG_HZ=300 时修复了 Nexus 5X 和 6P 上的 kgsl 计时器问题)。
- 最后,我们在 Nexus/Pixel 上尝试了 CONFIG_HZ=1000,发现由于 RCU 开销减少,它提供了明显的性能和功耗降低。
仅通过这两个更改,设备在负载下的 UI 线程可运行时间应该会看起来好得多。
使用 sys.use_fifo_ui
您可以尝试通过将 sys.use_fifo_ui
属性设置为 1 来将 UI 线程可运行时间驱动为零。
警告:除非您有容量感知的 RT 调度器,否则请勿在异构 CPU 配置上使用此选项。并且,目前,没有当前发布的 RT 调度器是容量感知的。我们正在为 EAS 开发一个,但尚未发布。默认的 RT 调度器完全基于 RT 优先级以及 CPU 是否已经有相同或更高优先级的 RT 线程。
因此,如果更高优先级的 FIFO kthread 恰好在同一个大核上唤醒,则默认的 RT 调度器会很乐意将您相对长时间运行的 UI 线程从高频大核移动到最小频率的小核。这将导致显着的性能下降。由于此选项尚未在发布的 Android 设备上使用,如果您想使用它,请与 Android 性能团队联系以帮助您验证它。
当启用 sys.use_fifo_ui
时,ActivityManager 会跟踪顶部应用的 UI 线程和 RenderThread(两个最关键的 UI 线程),并将这些线程设置为 SCHED_FIFO 而不是 SCHED_OTHER。这有效地消除了 UI 和 RenderThread 的抖动;我们收集的启用此选项的跟踪显示,可运行时间约为微秒而不是毫秒。
然而,由于 RT 负载均衡器不是容量感知的,因此应用启动性能降低了 30%,因为负责启动应用的 UI 线程将从 2.1Ghz gold Kryo 内核移动到 1.5GHz silver Kryo 内核。借助容量感知的 RT 负载均衡器,我们在批量操作中看到了相同的性能,并且在我们的许多 UI 基准测试中,第 95 和第 99 百分位帧时间减少了 10-15%。
中断流量
由于 ARM 平台默认情况下仅向 CPU 0 传递中断,因此我们建议使用 IRQ 均衡器(Qualcomm 平台上的 irqbalance 或 msm_irqbalance)。
在 Pixel 开发期间,我们发现卡顿可以直接归因于中断使 CPU 0 过载。例如,如果 mdss_fb0
线程在 CPU 0 上调度,则由于显示器在扫描输出之前几乎立即触发的中断,卡顿的可能性会大大增加。mdss_fb0
将在非常紧张的期限内处于自己的工作中间,然后它将丢失一些时间给 MDSS 中断处理程序。最初,我们尝试通过将 mdss_fb0 线程的 CPU 亲和性设置为 CPU 1-3 以避免与中断冲突来解决此问题,但后来我们意识到我们尚未启用 msm_irqbalance。启用 msm_irqbalance 后,即使 mdss_fb0 和 MDSS 中断都在同一个 CPU 上,由于其他中断的争用减少,卡顿也得到了显着改善。
这可以在 systrace 中通过查看 sched 部分以及 irq 部分来识别。sched 部分显示了已调度的内容,但 irq 部分中的重叠区域意味着中断在此期间运行,而不是通常调度的进程。如果您看到中断期间占用大量时间,您的选项包括
- 加快中断处理程序的速度。
- 从一开始就阻止中断发生。
- 更改中断的频率,使其与可能干扰的其他常规工作不同步(如果是常规中断)。
- 直接设置中断的 CPU 亲和性并阻止其被均衡。
- 设置中断干扰的线程的 CPU 亲和性以避免中断。
- 依靠中断均衡器将中断移动到负载较小的 CPU。
通常不建议设置 CPU 亲和性,但在特定情况下可能很有用。一般来说,对于大多数常见中断,很难预测系统的状态,但是如果您有一组非常特定的条件会触发某些中断,而系统比平时更受约束(例如 VR),则显式的 CPU 亲和性可能是一个不错的解决方案。
长时间 softirq
当 softirq 运行时,它会禁用抢占。softirq 也可以在内核中的许多位置触发,并且可以在用户进程内部运行。如果 softirq 活动足够多,用户进程将停止运行 softirq,并且 ksoftirqd 会唤醒以运行 softirq 并进行负载均衡。通常,这很好。但是,单个非常长的 softirq 可能会对系统造成严重破坏。
softirq 在跟踪的 irq 部分中是可见的,因此如果可以在跟踪时重现问题,则很容易发现它们。由于 softirq 可以在用户进程中运行,因此糟糕的 softirq 也可能表现为用户进程内部额外的运行时,而没有明显的原因。如果您看到这种情况,请检查 irq 部分,看看是否是 softirq 造成的。
驱动程序禁用抢占或 IRQ 时间过长
禁用抢占或中断时间过长(数十毫秒)会导致卡顿。通常,卡顿表现为线程变为可运行状态,但在特定 CPU 上未运行,即使可运行线程的优先级明显高于(或 SCHED_FIFO)另一个线程。
一些指南
- 如果可运行线程是 SCHED_FIFO,而运行线程是 SCHED_OTHER,则运行线程已禁用抢占或中断。
- 如果可运行线程的优先级明显高于(100)运行线程(120),则如果可运行线程在两个 jiffy 内未运行,则运行线程可能已禁用抢占或中断。
- 如果可运行线程和运行线程具有相同的优先级,则如果可运行线程在 20 毫秒内未运行,则运行线程可能已禁用抢占或中断。
请记住,运行中断处理程序会阻止您处理其他中断,这也禁用了抢占。
识别违规区域的另一种选择是使用 preemptirqsoff 跟踪器(请参阅使用动态 ftrace)。此跟踪器可以更深入地了解不可中断区域的根本原因(例如函数名称),但需要更多侵入性工作才能启用。虽然它可能对性能有更大的影响,但绝对值得一试。
工作队列的错误使用
中断处理程序通常需要执行可以在中断上下文之外运行的工作,从而使工作能够分配到内核中的不同线程。驱动程序开发人员可能会注意到内核具有一个非常方便的系统范围的异步任务功能,称为工作队列,并且可能会将其用于与中断相关的工作。
但是,工作队列几乎总是解决此问题的错误答案,因为它们始终是 SCHED_OTHER。许多硬件中断都处于性能的关键路径中,必须立即运行。工作队列不能保证它们何时运行。每次我们在性能的关键路径中看到工作队列时,它都是零星卡顿的来源,无论设备如何。在 Pixel 上,使用旗舰处理器,我们看到如果设备处于负载下,则单个工作队列可能会延迟长达 7 毫秒,具体取决于调度器行为和系统上运行的其他内容。
对于需要在单独线程中处理类似中断的工作的驱动程序,应创建自己的 SCHED_FIFO kthread,而不是工作队列。有关使用 kthread_work 函数执行此操作的帮助,请参阅此补丁。
框架锁争用
框架锁争用可能是卡顿或其他性能问题的来源。它通常由 ActivityManagerService 锁引起,但也可能在其他锁中看到。例如,PowerManagerService 锁可能会影响屏幕开启性能。如果您在设备上看到这种情况,则没有好的修复方法,因为它只能通过对框架进行架构改进来改进。但是,如果您正在修改在 system_server 内部运行的代码,则避免长时间持有锁至关重要,尤其是 ActivityManagerService 锁。
Binder 锁争用
从历史上看,binder 只有一个全局锁。如果运行 binder 事务的线程在持有锁时被抢占,则在原始线程释放锁之前,没有其他线程可以执行 binder 事务。这很糟糕;binder 争用可能会阻止系统中的所有内容,包括向显示器发送 UI 更新(UI 线程通过 binder 与 SurfaceFlinger 通信)。
Android 6.0 包含多个补丁,通过在持有 binder 锁时禁用抢占来改进此行为。这之所以安全,仅仅是因为 binder 锁应该只持有几微秒的实际运行时。这在无争用情况下显着提高了性能,并通过防止在持有 binder 锁时发生大多数调度器切换来防止争用。但是,不能在持有 binder 锁的整个运行时内禁用抢占,这意味着对于可能休眠的函数(例如 copy_from_user)启用了抢占,这可能会导致与原始情况相同的抢占。当我们向上游发送补丁时,他们立即告诉我们这是历史上最糟糕的主意。(我们同意他们的观点,但我们也无法反驳补丁在防止卡顿方面的功效。)
进程内的 fd 争用
这很少见。您的卡顿可能不是由此引起的。
也就是说,如果您的进程中有多个线程写入同一个 fd,则可能会看到此 fd 上的争用,但是我们在 Pixel 启动期间唯一一次看到这种情况是在测试中,其中低优先级线程试图占用所有 CPU 时间,而单个高优先级线程在同一进程中运行。所有线程都在写入跟踪标记 fd,如果低优先级线程持有 fd 锁然后被抢占,则高优先级线程可能会被阻塞在跟踪标记 fd 上。当从低优先级线程禁用跟踪时,没有性能问题。
我们无法在任何其他情况下重现这种情况,但值得指出的是,这可能是跟踪时出现性能问题的潜在原因。
不必要的 CPU 空闲转换
在处理 IPC,尤其是多进程管道时,通常会看到以下运行时行为的变体
- 线程 A 在 CPU 1 上运行。
- 线程 A 唤醒线程 B。
- 线程 B 开始在 CPU 2 上运行。
- 线程 A 立即进入休眠状态,在线程 B 完成当前工作后被线程 B 唤醒。
开销的一个常见来源在步骤 2 和 3 之间。如果 CPU 2 处于空闲状态,则必须将其恢复到活动状态,然后线程 B 才能运行。根据 SOC 以及空闲的深度,这可能需要数十微秒才能使线程 B 开始运行。如果 IPC 每一侧的实际运行时足够接近开销,则 CPU 空闲转换可能会显着降低该管道的整体性能。Android 遇到这种情况最常见的地方是在 binder 事务附近,许多使用 binder 的服务最终看起来都像上面描述的情况。
首先,在您的内核驱动程序中使用 wake_up_interruptible_sync()
函数,并从任何自定义调度程序中支持它。将其视为要求,而不是提示。Binder 今天使用它,它在同步 binder 事务中避免不必要的 CPU 空闲转换方面有很大帮助。
其次,确保您的 cpuidle 转换时间是真实的,并且 cpuidle governor 正在正确考虑这些时间。如果您的 SOC 在最深空闲状态中来回抖动,那么进入最深空闲状态将无法节省功耗。
日志记录
日志记录对于 CPU 周期或内存来说不是免费的,因此不要向日志缓冲区发送垃圾邮件。日志记录会在您的应用(直接)和日志守护程序中花费周期。在发布设备之前删除任何调试日志。
I/O 问题
I/O 操作是抖动的常见来源。如果线程访问内存映射文件并且该页面不在页面缓存中,则会发生页面错误并从磁盘读取页面。这会阻塞线程(通常为 10 毫秒以上),并且如果发生在 UI 渲染的关键路径中,则可能导致卡顿。I/O 操作的原因太多,无法在此处讨论,但在尝试改善 I/O 行为时,请检查以下位置
- PinnerService。PinnerService 在 Android 7.0 中添加,使框架能够锁定页面缓存中的某些文件。这会删除供任何其他进程使用的内存,但如果有一些已知先验会定期使用的文件,则 mlock 这些文件可能有效。
在运行 Android 7.0 的 Pixel 和 Nexus 6P 设备上,我们 mlock 了四个文件- /system/framework/arm64/boot-framework.oat
- /system/framework/oat/arm64/services.odex
- /system/framework/arm64/boot.oat
- /system/framework/arm64/boot-core-libart.oat
- 加密。I/O 问题的另一个可能原因。我们发现,与基于 CPU 的加密或使用可通过 DMA 访问的硬件块相比,内联加密提供了最佳性能。最重要的是,与基于 CPU 的加密相比,内联加密减少了与 I/O 相关的抖动。由于对页面缓存的获取通常在 UI 渲染的关键路径中,因此基于 CPU 的加密会在关键路径中引入额外的 CPU 负载,这会增加比仅 I/O 获取更多的抖动。
基于 DMA 的硬件加密引擎也存在类似的问题,因为即使有其他关键工作可运行,内核也必须花费周期来管理该工作。我们强烈建议任何构建新硬件的 SOC 供应商都包含对内联加密的支持。
激进的小任务打包
一些调度器提供将小任务打包到单个 CPU 内核上的支持,以尝试通过使更多 CPU 长时间处于空闲状态来降低功耗。虽然这对于吞吐量和功耗来说效果很好,但它可能对延迟造成灾难性影响。在 UI 渲染的关键路径中,有几个短时间运行的线程可以被认为是小的;如果这些线程在缓慢迁移到其他 CPU 时被延迟,则会导致卡顿。我们建议非常保守地使用小任务打包。
页面缓存抖动
没有足够可用内存的设备在执行长时间运行的操作(例如打开新应用)时可能会突然变得非常迟缓。应用的跟踪可能会显示,即使在通常不阻塞 I/O 的情况下,它在特定运行期间也始终阻塞在 I/O 中。这通常是页面缓存抖动的迹象,尤其是在内存较少的设备上。
识别此问题的一种方法是使用 pagecache 标记获取 systrace,并将该跟踪馈送到 system/extras/pagecache/pagecache.py
中的脚本。pagecache.py 将将文件映射到页面缓存的各个请求转换为每个文件的聚合统计信息。如果您发现读取的文件字节数多于该文件在磁盘上的总大小,那么您肯定遇到了页面缓存抖动。
这意味着您的工作负载(通常是单个应用加上 system_server)所需的工作集大于设备上页面缓存可用的内存量。因此,当工作负载的一部分在页面缓存中获得所需的数据时,另一部分将在不久的将来使用的数据将被逐出,并且必须再次获取,从而导致问题再次发生,直到负载完成。这是设备上可用内存不足时出现性能问题的根本原因。
没有万无一失的方法来修复页面缓存抖动,但有一些方法可以尝试在给定设备上改进这一点。
- 在持久进程中使用更少的内存。持久进程使用的内存越少,应用和页面缓存可用的内存就越多。
- 审核您为设备设置的 carveout,以确保您没有不必要地从操作系统中删除内存。我们已经看到过这样的情况:用于调试的 carveout 意外地保留在发布的内核配置中,消耗了数十兆字节的内存。这可能会导致是否发生页面缓存抖动的差异,尤其是在内存较少的设备上。
- 如果您在 system_server 中的关键文件上看到页面缓存抖动,请考虑固定这些文件。这将增加其他地方的内存压力,但它可能会充分修改行为以避免抖动。
- 重新调整 lowmemorykiller 以尝试保持更多可用内存。lowmemorykiller 的阈值基于绝对可用内存和页面缓存,因此提高在给定的 oom_adj 级别杀死进程的阈值可能会导致更好的行为,但代价是增加了后台应用死亡。
- 尝试使用 ZRAM。我们在 Pixel 上使用 ZRAM,即使 Pixel 有 4GB,因为它可以帮助处理很少使用的脏页。