去年10月,优步的移动工程团队开始努力改善应用程序的性能,到目前为止,我们已经取得了很大的进展,在一些关键的过渡中,我们的速度提高了50%以上。在早期,我们了解到某些类型的性能问题对于根本原因来说并不重要。例如,很容易在主线程上发现I/O。在这些情况下,我们发现传统的Android性能分析工具足以进行调试。
然而,更复杂的调查有时由于数据不完整或不准确而不确定。我们最初难以回答的一些问题包括“为什么某些动画的启动速度很慢?”以及“为什么TextView的通货膨胀在某些情况下会减缓?”在遇到限制之后Android Studio CPU Profiler,我们制作了Nanoscope,一个内部工具,为我们提供更好的追踪方法。
自从实施我们的初始原型以来,我们已经在内部示踪剂上使用并迭代,现在能够自信地调试这些困难的性能问题。在其他发现之外,我们已经发现构建动画硬件层比预期更昂贵,而那个TextView文本自动化如果您不使用速度要慢得多粒度。
我们的内部方法追踪器继续为我们提供前所未有的应用性能洞察,所以我们决定与Android社区的其他成员分享这个工具。今天,我们很兴奋地发布纳米镜:一个非常精确的Android方法跟踪工具。
2.在Nanoscope可视化器中探索火焰图。
动机
我们理解利用现有工具的价值,并相信新的工具有充分的理由,所以在深入研究Nanoscope的工作原理之前,我们将看看Android Studio的性能工具,以及它们在哪些方面没有达到我们的要求。
Android Studio方法跟踪
像纳莫克一样,Android Studio提供了方法跟踪功能。我们的主封锁是Android Studio的方法跟踪仪表引入的显着性能开销。
点击到动画开头的时间。
在启用Android Studio方法跟踪的情况下,我们的一些关键转换运行速度慢了多个数量级。由于额外的日志逻辑,任何方法跟踪都会在一定程度上降低运行时性能,但在这种失真程度上,产生的性能概要不再是正常应用程序使用的准确表示,对我们的性能调查也没有用处。
Android Studio方法抽样
除了方法跟踪之外,Android Studio还提供了方法采样作为承诺对运行时性能的影响显着降低了替代方案。我们通过配置采样频率测试了此功能,确实可以使用极少的开销来示出,但它带来了权衡。在较低的频率下,采取较少的测量值,因此在精度的成本下降低了总开销,如图1所示:
该应用程序在这种情况下运行顺利,但跟踪缺少许多重要细节。较高频率产生更完整的轨迹,但需要更多的测量,提高对性能的影响,如图2所示:
对于大约三周的生产数据反馈循环,我们必须准确地理解代码的性能概要,以免在提交修复后三周发现它未能解决问题或使事情变得更糟。我们发现,在任何给定的频率下,Android Studio方法抽样都缺乏我们所要求的细节或准确性。在这一点上,我们开始怀疑是否可能两者兼得。
纳莫斯科设计
在构建原型之前,首次决定使我们的工具是追踪还是基于样本。为避免任何涉及不完整数据的疑虑,我们降落在基于踪影的实现。由于每个样本需要在跟踪测量只记录当前方法标识符时,我们也理解最佳采样工具的最佳采样工具比最佳跟踪工具效率低于最佳跟踪工具。
我们来到的解决方案是一个极其低于开销的追踪工具,可以为我们提供快速调试业绩问题所需的细节和准确性。原型的早期结果很有希望,并鼓励我们继续致力于最终成为纳米腔的工作。
我们用Nanoscope取得的性能水平依赖于与操作系统的深度集成。为了实现这一点,我们实现了Nanoscope作为一个分叉Android开源项目(AOSP)。此策略还可作为纳米查名为用户输入的最大障碍,因为它需要运行自定义操作系统的设备(2018年5月7日更新:您现在可以通过启动设备而不闪烁一个设备毫微秒示波器模拟器),但完全控制操作系统,我们的策略非常简单:
- 分配数组以保存我们的跟踪数据。
- 在方法条目上:
- 写入时间戳和方法指针,以指示对调用堆栈的推送
- 在方法退出:
- 写入时间戳和空指针以指示来自呼叫堆栈的流行音乐
翻译
我们的第一项任务是装配翻译器。幸运的是,解释器执行的所有方法都流经一个方法:
我们添加了Tracestart和Traceend方法来做重降低记录我们的跟踪数据的最小提升:
我们首先通过检查跟踪数据数组是否存在来确定是否为线程启用了跟踪。然后,我们编写一个方法标识符(或用于pop的nullptr),后面跟着当前时间戳,该时间戳直接从计时器寄存器中检索,以获得最佳性能。
编译器
并非所有Java方法都由解释器执行。有些方法是AOT或吉特-compiled进入机器指令并直接执行。在这些情况下,我们可以发出呼叫TraceStart/追逐方法,但我们避免通过在每个编译方法的开始和结尾内联入等效组装说明来避免跳跃。下面是我们生成的64位装配表条目:
我们为方法退出生成类似的说明,还包括对32位编译器的支持。
结果
我们一直致力于最小化每个方法执行的逻辑,我们对结果感到非常自豪。在跟踪时,Nanoscope为每个方法引入了20纳秒的开销,在启动序列中引入的总开销不到10%。除了前面提到的发现之外,下面是一些性能问题的例子,多亏了Nanoscope,我们现在已经深入了解了这些问题:
- 由于初始化Chromium, webview只有在第一次初始化时才会很慢。
- 花在谷歌Maps初始化上的大部分时间是由于类加载。
- MenuView对每个点击事件执行重新绑定/布局/膨胀(github问题)。
Nanoscope的准确性也使得对局部性能行为进行分类变得容易,而不是依赖于生产测量的平均值。我们现在可以快速回答以下问题,例如:
- 在与视图相关的操作中,过渡的百分比百分比?
- 哪个转型的百分比归因于我们的平台图书馆?
- 转换中有多少百分比是在RxJava中进行的?
自从我们开始使用Nanoscope,扭曲或丢失的数据不再是我们在Uber调试Android性能问题时的障碍。
下一个步骤
如果您想了解更多关于架构的信息,请查看维基,如果您有趣的提高应用程序的表现,请考虑给予纳米镜一试。
Uber还有很多其他有趣的问题需要解决,我们正在招人!






